<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>一只安静的猫</title><description>想啥呢</description><link>https://www.myway5.com/</link><item><title>AI Coding 时代 Code Review 的最终形态</title><link>https://www.myway5.com/blog/ai-coding-verification-agent/</link><guid isPermaLink="true">https://www.myway5.com/blog/ai-coding-verification-agent/</guid><description>Coding Agent 已经能在几分钟内产出可运行的补丁，Code Review 的瓶颈从&quot;代码不够写&quot;变成了&quot;代码不敢信&quot;。本文梳理 Verification Agent 的几个演进方向，以及我认为最终会收敛到的六层验证栈形态。</description><pubDate>Sun, 09 Aug 2026 12:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;最近半年 Coding Agent 发展很快。SWE-Agent（普林斯顿大学开源的软件工程 Agent）、OpenHands（前身 OpenDevin，开源的自主编程 Agent 平台）、Devin（Cognition AI 推出的 AI 软件工程师）、Claude Code、Codex 这类工具，已经能在几分钟内产出一个可以跑测试的补丁。&lt;/p&gt;
&lt;p&gt;这带来了一个很实际的问题：当一个团队每天面对的不是 10 个 PR，而是 1000 个 Agent 提交时，人类 Reviewer 就成了整个系统里最慢的一环。LGTM（Looks Good To Me）从一个判断行为退化成一个盖章行为。&lt;/p&gt;
&lt;p&gt;在我看来，这里真正发生变化的不是&quot;Code Review 怎么做得更快&quot;，而是问题本身被重写了。我们都知道，传统 Code Review 建立在一个隐含前提上：代码是稀缺的、写代码本身是瓶颈，所以人有时间逐行看。这个前提在 AI Coding 时代正在崩塌。当代码被大规模、高速度地生成出来，核心矛盾就从 Generate Code 转移到了 Establish Trust（建立可信性）。&lt;/p&gt;
&lt;p&gt;那么 Code Review 的未来到底是什么形态？这篇文章简单聊聊我的看法。&lt;/p&gt;
&lt;h2&gt;为什么说 Verification 比 Coding 更难&lt;/h2&gt;
&lt;p&gt;在讨论最终形态之前，先看看今天大家在做什么。&lt;/p&gt;
&lt;p&gt;目前几乎所有 Coding Agent 的默认闭环都是同一个：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Issue
  |
Coding Agent
  |
生成 Patch
  |
Run Test
  |
失败则继续修改
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 verifier（验证者）是 test，也就是&quot;测试通过 = 改动正确&quot;。这个闭环在 benchmark 场景里跑得通，但在真实工程里大概存在这么几个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;测试覆盖不全，很多路径根本没有对应用例。&lt;/li&gt;
&lt;li&gt;隐藏的 bug、并发问题、内存泄漏、性能回退、安全漏洞，普通单测几乎不可能捕获。&lt;/li&gt;
&lt;li&gt;benchmark 本身的测试设计也可能有问题——需求描述不完整、断言过严或过松，都会导致 tests pass 并不能可靠地等价于 correctness（正确性）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OpenAI 最近对 SWE-bench Pro 的审计发现大约 30% 的任务本身是坏的（过严测试、描述不全、低覆盖测试、误导性 prompt），这从另一个角度说明了一件事：在 Agent 时代，验证问题本身比生成问题更难。评测标准里潜藏的噪声，甚至可能让一个&quot;变强的模型&quot;看起来没有进步。&lt;/p&gt;
&lt;p&gt;因此，Code Review 需要解决的核心问题从来都不是&quot;更快地看 diff&quot;，而是&quot;用什么方法建立可信性&quot;。这也直接决定了未来的形态不会是一个更聪明的 Reviewer 模型，而是一个由多层验证能力组成的系统。&lt;/p&gt;
&lt;h2&gt;几个正在发生的方向&lt;/h2&gt;
&lt;p&gt;业界还没有一个统一的名字（我倾向于叫它 Verification Agent），但多个研究方向已经在向同一个终点收敛。&lt;/p&gt;
&lt;h3&gt;Test-driven Verifier&lt;/h3&gt;
&lt;p&gt;这是当下最成熟，也最容易被误解为&quot;已经解决&quot;的方向。它把 Coding Agent 与测试框架绑成一个闭环：改代码、跑测试、失败再改，直到全部通过。前面提到的 SWE-Agent、OpenHands、Devin、Claude Code、Codex，本质都在做这件事。&lt;/p&gt;
&lt;p&gt;优点是工程简单、可以做强化学习。缺点也很明确：它把&quot;代码是否正确&quot;等价于&quot;测试是否通过&quot;，把验证问题外包给了测试设计者。当测试不完整时，它会以极高的效率生产出一堆&quot;测试通过但不解决问题&quot;的补丁。&lt;/p&gt;
&lt;h3&gt;Multi-agent Review&lt;/h3&gt;
&lt;p&gt;第二类做法是把 Reviewer 也变成 Agent，让不同角色互相 challenge：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Coder Agent
  |
Reviewer Agent
  |
Critic Agent
  |
Fix Agent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reddit 上有人做过一个非严格的对比实验，在 SWE-bench Verified 的 100 个实例上，单 Agent（Claude Opus 4.5）解决率是 80%，加上一个 Reviewer Agent（GPT-5.2）之后提升到 90%，代价是平均耗时从 3.5 分钟增加到 7.8 分钟，大约 2.2 倍。这个实验样本量不大，不能直接推广，但它揭示了一个更基本的现象：Review 是可以带来独立信息量的，只要 Reviewer 不再和 Coder 共享同一个上下文与偏见。&lt;/p&gt;
&lt;p&gt;当然，如果 Reviewer 只是用另一个 LLM 重新读一遍 diff，它仍然停留在&quot;感觉合理&quot;的层面。要往前走，Review 需要能调用工具、能形成证据，而不仅仅是重新表达一次自然语言判断。&lt;/p&gt;
&lt;h3&gt;Specification Verification&lt;/h3&gt;
&lt;p&gt;第三个方向是我个人觉得潜在价值最大、但目前几乎没有人做好的。&lt;/p&gt;
&lt;p&gt;今天大部分验证做的是 &lt;code&gt;Code -&amp;gt; Pass Test&lt;/code&gt;。真正应该做的是 &lt;code&gt;Spec -&amp;gt; Implementation -&amp;gt; Equivalent?&lt;/code&gt;，也就是验证实现和规格说明是不是一致。举个例子，一个 PR 的需求可能是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须 backward compatible（向后兼容）。&lt;/li&gt;
&lt;li&gt;P99 延迟必须小于 200ms。&lt;/li&gt;
&lt;li&gt;单实例内存必须小于 4GB。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Verification Agent 要做的不是跑一遍单测，而是自动证明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;公共 API 没有破坏性变更。&lt;/li&gt;
&lt;li&gt;Benchmark 结果满足 P99 阈值。&lt;/li&gt;
&lt;li&gt;内存 profile 满足上限。&lt;/li&gt;
&lt;li&gt;所有历史测试通过。&lt;/li&gt;
&lt;li&gt;新功能测试通过。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里被 review 的对象已经不再是代码本身，而是 Spec 是否被实现。这一步之所以难，是因为它要求需求本身是可验证的——这大概也意味着未来工程流程里，Spec 会重新变成一等公民。&lt;/p&gt;
&lt;h3&gt;Formal Verification 与 Explain + Verify&lt;/h3&gt;
&lt;p&gt;第四个方向来自更&quot;硬核&quot;的一侧。&lt;/p&gt;
&lt;p&gt;近期有一篇论文叫 &quot;The Prover Is the Judge&quot;，尝试用 Ada / SPARK（一种面向高可靠系统的编程语言与验证工具集）和 GNATprove（SPARK 的形式化证明工具）让 AI 生成安全关键代码，再由证明器验证大量证明义务，形成&quot;AI 写代码，证明器判决&quot;的闭环。论文里报告 GNATprove 自动完成了 49,280 条证明义务，覆盖了密码学、TLS 1.3、IKEv2、X.509 等组件，但作者也坦率地指出：证明器能证明什么，取决于规格和验证器的表达能力，对于某些缺陷仍然需要 known-answer tests（已知答案测试）、互操作验证或人工审查。&lt;/p&gt;
&lt;p&gt;与之并行的是 Explain + Verify 这条思路，代表性的评测是 ExplainBench（ASE 2026）。它的观察很朴素：一个 Agent 说&quot;我改好了&quot;并不足够，我们真正想知道的是&quot;为什么这样改、影响哪些模块、为什么不会 break&quot;。ExplainBench 的结果给出一个重要判断：解释质量与 Coding 能力是两个独立维度，一个补丁可能正确但解释错误，反之亦然；而且 Agent 经常错误地宣称补丁正确。论文里实现了一个 explanation audit agent（解释审计代理），通过额外运行测试来验证和修正解释，确实改善了所有被评估 Agent 的解释质量。&lt;/p&gt;
&lt;h2&gt;六层 Verification Stack&lt;/h2&gt;
&lt;p&gt;把上面这些方向叠加起来，我倾向于认为 AI Coding 时代的 Code Review 最终会长成一个分层的 Verification Stack，而不是任何单一模型或单一工具。下面简单介绍一下每一层。&lt;/p&gt;
&lt;h3&gt;Layer 1：Syntax&lt;/h3&gt;
&lt;p&gt;最底层，也是最容易被忽视的一层。编译、lint、格式化、类型检查在这一层完成。它保证&quot;代码至少是一个合法的字符串&quot;，任何上层验证都建立在这个前提之上。AI 在这一层的价值不大，现有工具已经足够。&lt;/p&gt;
&lt;h3&gt;Layer 2：Static Analysis&lt;/h3&gt;
&lt;p&gt;第二层负责结构性缺陷：死锁、竞态、资源泄漏、空指针、注入类漏洞等等。今天已经有 CodeQL（GitHub 的语义代码分析引擎）、Semgrep（轻量级静态分析工具）、Infer（Facebook 开源的静态分析器）等成熟工具。未来 LLM 的角色大概是统一调用与解释：根据改动特征选择合适的规则集，把工具产出翻译成可读的、能被 Verification Agent 引用的证据。&lt;/p&gt;
&lt;h3&gt;Layer 3：Behavioral Verification&lt;/h3&gt;
&lt;p&gt;第三层不再看代码，而是运行代码：unit test（单元测试）、integration test（集成测试）、e2e（端到端测试）、fuzz（模糊测试）、mutation test（变异测试，通过故意修改代码来检验测试用例的质量）。AI 在这一层最有价值的动作是自动补测试——针对 diff 与调用图，生成有针对性的用例，特别是补齐 mutation score（变异得分）较低的路径。它是 Test-driven Verifier 的自然升级。&lt;/p&gt;
&lt;h3&gt;Layer 4：Repository-level Reasoning&lt;/h3&gt;
&lt;p&gt;第四层是 LLM 相对于传统工具真正拉开差距的地方。它要回答的是仓库级问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个 PR 影响哪些模块？&lt;/li&gt;
&lt;li&gt;有没有引入重复实现？&lt;/li&gt;
&lt;li&gt;有没有违反既定架构？&lt;/li&gt;
&lt;li&gt;有没有造成 API drift（API 漂移）？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一层需要基于代码图（调用图、依赖图、模块图）做推理，是 diff-only Reviewer 天然做不到的。它决定了 Verification Agent 能不能看到&quot;小 PR 引发系统性风险&quot;的场景。&lt;/p&gt;
&lt;h3&gt;Layer 5：Spec Verification&lt;/h3&gt;
&lt;p&gt;第五层是我认为价值最大、也最难的一层。它把需求变成一等公民：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Spec
  |
读取代码
  |
生成验证计划
  |
执行验证
  |
证明是否满足
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里被 review 的不再是 code，而是 intent（意图）。这一层的成熟度大概直接决定了未来能不能把 Coding Agent 真正规模化地交给系统而不是个人。&lt;/p&gt;
&lt;h3&gt;Layer 6：Runtime Verification&lt;/h3&gt;
&lt;p&gt;最后一层很多人会漏掉：不是所有问题都可以在合入前发现。内存泄漏、突发流量、少见的竞态、上线后的延迟回退，本质上都是运行时问题。未来的 Verification Agent 会延伸到 merge 之后：持续观察 metrics、logs、trace、crash、canary（金丝雀发布），判断某个指标漂移是不是某个 PR 引起，必要时触发自动回滚。&lt;/p&gt;
&lt;p&gt;也就是说，Verification 不是一个 merge 前的门禁，而是覆盖整个软件生命周期的能力。&lt;/p&gt;
&lt;h2&gt;未来流程&lt;/h2&gt;
&lt;p&gt;如果把这些层组合起来，未来的流程大概会更像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Spec
  |
Coding Agent
  |
Verification Agent
  ├── Static
  ├── Dynamic
  ├── Tests
  ├── Security
  ├── Performance
  ├── Spec Matching
  ├── Explanation Audit
  └── Runtime Prediction
  |
Risk Score
  |
Human（仅高风险）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比今天的 &lt;code&gt;Human -&amp;gt; Read Diff -&amp;gt; Approve&lt;/code&gt;，可以看到两个主要差别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Human Review 不消失，但被显式地留在高风险位置。绝大多数改动由 Verification Agent 直接通过或直接拒绝，人只在系统&quot;拿不准&quot;的时候进入。&lt;/li&gt;
&lt;li&gt;Verification 的输出从&quot;同意 / 拒绝&quot;变成&quot;一个带有置信度的风险评分&quot;，可以被后续系统消费。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个变化共同推翻了 Code Review 的旧范式：Reviewer 不再是一个决定权在自己手上的角色，而是整个可信性系统里的一环。&lt;/p&gt;
&lt;h2&gt;Evidence-based Verification&lt;/h2&gt;
&lt;p&gt;如果只让我押一个具体的研究方向，我会选一个几乎还没有名字的方向：Evidence-based Verification Agent（基于证据的验证代理）。&lt;/p&gt;
&lt;p&gt;今天的 Reviewer（无论人还是 AI）给出的通常只是一个结论：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;LGTM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;未来的 Verification Agent 应该给出的是一整条证据链：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Claim:
  这个 PR 不会 break API。

Evidence:
  - AST Analysis：公共接口签名未变。
  - 142 Regression Tests：全部通过。
  - Mutation Score：93%，覆盖新增分支。
  - Call Graph：无新增循环依赖。
  - Canary Prediction：预测风险 0.03。

Confidence:
  98.7%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方向和近几年 AI 领域强调的 reasoning trace（推理轨迹）有相似的气味，但对象完全不同：reasoning trace 是模型推理过程的可解释性，而 Evidence Chain（证据链）是软件可信性的可解释性。前者服务于&quot;模型说了什么&quot;，后者服务于&quot;我们应不应该相信这次改动&quot;。&lt;/p&gt;
&lt;p&gt;顺着这个方向再往下想，有三个问题在我看来最值得投入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spec-to-Evidence Verification&lt;/strong&gt;：如何把自然语言需求自动转化为可验证的证据计划，而不仅仅是生成一些测试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repository-scale Verification Graph&lt;/strong&gt;：把整个仓库建模成依赖图、调用图与架构图，验证一次变更的系统级影响，而不是只看 PR diff。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evidence Aggregation&lt;/strong&gt;：把静态分析、动态测试、LLM 推理、运行时监控、形式化验证等信号融合成一个可解释、可校准的风险评分，而不是简单的 Approve / Reject。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果说 Coding Agent 的核心动作是 Generation（生成），那么 Verification Agent 的核心动作就应该是 Evidence（证据）。Code Review 这个名字大概率会保留下来，但它背后的东西会被彻底换掉——不是&quot;怎么看代码&quot;，而是&quot;我们凭什么相信一段代码&quot;。这大概也是 AI Coding 时代最值得做的一件事。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI, &lt;a href=&quot;https://openai.com/index/separating-signal-from-noise-coding-evaluations/&quot; rel=&quot;noopener&quot;&gt;Separating Signal from Noise in Coding Evaluations&lt;/a&gt;（2026-07）：审计 SWE-Bench Pro 数据集，估计约 30% 的任务存在破坏性问题（过严测试、需求描述不全、低覆盖测试、误导性 prompt），从评测侧说明&quot;tests pass 不等于 correctness&quot;。&lt;/li&gt;
&lt;li&gt;Reddit 社区实验：&lt;a href=&quot;https://www.reddit.com/r/ClaudeAI/comments/1qi2gh0/i_ran_100_swebench_tests_comparing_1_agent_vs_2/&quot; rel=&quot;noopener&quot;&gt;I ran 100 SWE-bench tests comparing 1 agent vs 2 agents&lt;/a&gt;：SWE-bench Verified 100 例上，单 Agent 80% → 叠加 Reviewer 后 90%，耗时 2.2 倍。非严格学术结果，但说明 Review Agent 带来独立价值。&lt;/li&gt;
&lt;li&gt;Tobias Philipp, &lt;a href=&quot;https://arxiv.org/abs/2607.14340&quot; rel=&quot;noopener&quot;&gt;The Prover Is the Judge: Verified Security Software from AI Coding Agents in Ada/SPARK&lt;/a&gt;（arXiv:2607.14340, 2026-07）：AI 在 Ada / SPARK 中编写安全关键代码，GNATprove 完成 49,280 条证明义务；证明器无法覆盖的部分仍需测试、互操作验证或人工审查。&lt;/li&gt;
&lt;li&gt;Zhiyuan Pan et al., &lt;a href=&quot;https://arxiv.org/abs/2607.26451&quot; rel=&quot;noopener&quot;&gt;ExplainBench: Evaluating Code Explanations from Agents&lt;/a&gt;（ASE 2026, arXiv:2607.26451, 2026-07）：解释质量是独立于 SWE-bench Verified 的评估维度，Agent 经常错误地宣称补丁正确；explanation audit agent 可自动改善解释质量。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>ai</category><category>ai-coding</category><category>code-review</category><category>verification-agent</category><category>agent</category><category>software-engineering</category><author>joyme123</author></item><item><title>一次网络延迟高的问题排查</title><link>https://www.myway5.com/blog/network-issue/</link><guid isPermaLink="true">https://www.myway5.com/blog/network-issue/</guid><description>客户的机房部署了多套不同架构的 k8s 集群：Intel CPU + redhat OS，海光 CPU + 麒麟 OS。其中 Intel CPU + redhat OS 的 k8s 已经稳定运行了很久。海光 CPU + 麒麟 OS 是后来上线的，主要是为了满足信创的要求。</description><pubDate>Fri, 07 Jul 2023 02:26:27 GMT</pubDate><content:encoded>&lt;h2&gt;问题背景&lt;/h2&gt;
&lt;p&gt;客户的机房部署了多套不同架构的 k8s 集群：Intel CPU + redhat OS，海光 CPU + 麒麟 OS。其中 Intel CPU + redhat OS 的 k8s 已经稳定运行了很久。海光 CPU + 麒麟 OS 是后来上线的，主要是为了满足信创的要求。但是在实际使用中发现，海光 CPU + 麒麟 OS 的 k8s 集群中，运行的服务在压测时会偶现服务请求 redis 超时的异常。&lt;/p&gt;
&lt;h2&gt;问题定位&lt;/h2&gt;
&lt;p&gt;因为同样的服务，在非信创的机器上可以正常运行，因此排除服务本身的问题。主要排查硬件+操作系统+ k8s CNI 这几个环节。这里之所以要排查 CNI 的问题，主要还是考虑这个 CNI 是专门为这个客户开发的，用来实现 Underlay 的网络，以及一系列的特殊网络需求。 整个网络的链路如图所示： &lt;img src=&quot;/uploads/wp/2023/07/Snipaste_2023-07-07_00-48-24.png&quot; alt=&quot;network1&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;抓包分析&lt;/h3&gt;
&lt;p&gt;首先是在 Pod 内用 tcpdump 进行长时间的抓包，保存的本地文件。然后进行压测来复现问题。根据日志找到出问题的时间点，用 wireshark 自带的命令对抓包文件按时间切割。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 每 10s 保存一个分片。
editcap -i 10 tcpdump.cap pod1111
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 wireshark 打开对应时间片段的抓包文件进行分析。这里因为安全要求不方便放上异常包的截图。大概描述一下问题现象：分析的是 tcp 包，表现为异常时间点 redis 回包存在大量重传，且重传的包几乎都在同一时刻到达 Pod 内。 因此下一步要排查 tcp 包的延迟发生链路上的什么位置。因此选择同时在 redis 虚拟机，物理机网卡 eth0，pod 内网卡抓包。然后用同样的方式进行问题复现。 因为有了多个位置点的抓包数据，根据 tcp 的 seq 号就可以分析同一个数据包从 redis 虚拟机发出来的时间，以及到达物理机 eth0 以及 Pod 内的时间。然后根据时间就可以找到延迟点在哪。 分析后发现延迟在 redis→物理机这条链路上。redis 出来的包因为延迟到达了物理机，因此也延迟到达 Pod 内。redis 所在的虚拟机发包后因为一直没有收到回报，因此会触发 tcp 的重传机制。但是重传的包也出现了延迟。最终原始包和重传包在某一刻同时进入了物理机。 因此怀疑是交换机上出现了延迟，但是如果是交换机的问题，那么接入该交换机的其他机器也一定会出问题才对。但是现象仅局限于信创服务器上。所以也和麒麟的供应商沟通了，他们怀疑是一个已知的 cgroup 问题导致的，出现在 4.19 前的内核里。升级内核后果然问题就解决了。&lt;/p&gt;
&lt;h2&gt;问题分析&lt;/h2&gt;
&lt;p&gt;上面说的 cgroup 问题，详细来说是 kubelet 中的 cadvisor 在采集 node 的内存信息时，会读取 /sys/fs/cgroup/memory/memory.numa_stat 信息。但是因为内核的实现会导致这个读取信息的系统调用很慢。慢的原因有两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;cgroup 是通过 cgroup 伪文件系统来管理的，可以通过删除伪文件系统中的文件目录来删除相应的 cgroup。但是内核中代表 cgroup 的结构体会仍然存在，直到所有对它的引用被释放。只有当被删除的 memory cgroup 中的页都被回收掉，相应的引用都被释放，该 memory cgroup 才会被彻底删除。系统中所有的 memory cgroup 数量可以通过 &lt;code&gt;cat /proc/cgroups&lt;/code&gt; 来查看。而内存页的回收时间与内核的回收机制有关，如果当中有一些页一直活跃的被使用，就可能永远不会被回收。&lt;/li&gt;
&lt;li&gt;cadvisor 读取 /sys/fs/cgroup/memory/memory.numa_stat 信息时，其实是一个系统调用。这个调用的实现也存在性能问题，它会遍历所有的子 cgroup 层级，累加 memory 的使用信息求和，得到总的 memory 使用情况。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，在一台一直运行的服务器上，memory cgroup 可能会达到 1w+。cadvisor 在获取 memory cgroup 时可能耗费 1s 以上的时间。在这段时间内，CPU 没法被调度给其他地方使用。 那为什么会导致网络的延迟呢？linux 网络数据包的接收，在之前的文章 &lt;a href=&quot;https://www.myway5.com/index.php/2021/07/15/linux-network-packet-receive-1/&quot; rel=&quot;noopener&quot;&gt;linux 网络数据包接收流程（一）&lt;/a&gt;中整理过。数据包到达网卡后，依赖硬中断(3)+软中断(6)来触发 CPU 对数据包进行处理。 &lt;img src=&quot;/uploads/wp/2021/07/%E6%B5%81%E9%87%8F%E8%BF%9B%E5%85%A5%E5%88%B0%E7%A1%AC%E4%BB%B6%E4%B8%AD%E6%96%AD.jpg&quot; alt=&quot;network2&quot; /&gt; 并且现在的网卡很多都是多队列的，每条队列和某个 CPU 进行绑定，由该 CPU 进行处理。因此如果这个软中断发生在 cadvisor 统计 memory cgroup，进行系统调用时，软中断的处理就可能因此而延迟。如果这个过程持续 1s+，那么引起的现象可能就是对端 tcp 出现重传。如果这个过程持续 2s+，那么因为服务本身读取 redis 的超时时间设置为 2s，就可能出现超时了。&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;长期解决方案就是升级内核。在更高的版本内核中，对 cgroup memory 的计算进行了优化，这里不再会遍历所有的子 memory cgroup 进行统计了。因为本身 cgroup 就已经维护了该信息，直接读取并返回就行了。内核相关修复：&lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v6.2-rc7&amp;amp;id=dd8657b6c1cb5e65b13445b4a038736e81cf80ea&quot; rel=&quot;noopener&quot;&gt;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v6.2-rc7&amp;amp;id=dd8657b6c1cb5e65b13445b4a038736e81cf80ea&lt;/a&gt; 短期解决方案是周期执行这条命令：echo 3 &amp;gt; /proc/sys/vm/drop_caches。这会触发内核清理 PageCache, dentries 和 inodes 缓存。这里如果是 echo 1，则只清理 PageCache。echo 2，只清理 dentries 和 inodes 缓存。&lt;/p&gt;
</content:encoded><category>linux</category><author>joyme123</author></item><item><title>runc 的输入输出</title><link>https://www.myway5.com/blog/runcio/</link><guid isPermaLink="true">https://www.myway5.com/blog/runcio/</guid><description>runc 是一个基于 OCI 规范实现的程序。在基于 containerd 的容器技术栈上，runc 是一个非常重要但又容易忽略的组件。当执行 docker, nerdctl, ctr 等命令时，其实底层都要调用 runc 去运行容器的进程。</description><pubDate>Wed, 08 Mar 2023 06:08:28 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;runc 是一个基于 OCI 规范实现的程序。在基于 containerd 的容器技术栈上，runc 是一个非常重要但又容易忽略的组件。当执行 &lt;code&gt;docker&lt;/code&gt;, &lt;code&gt;nerdctl&lt;/code&gt;, &lt;code&gt;ctr&lt;/code&gt; 等命令时，其实底层都要调用 runc 去运行容器的进程。不过这篇文章仅会涉及到 runc 的输入输出。 因为在日常的开发中，大多数开发者接触到的都是 docker，因此下面会以 docker 为例，结合底层的 runc 来说明容器进程的输入输出。&lt;/p&gt;
&lt;h2&gt;标准输入输出&lt;/h2&gt;
&lt;p&gt;在 linux 上，所有的进程都会打开三个文件描述符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;0: stdin 标准输入&lt;/li&gt;
&lt;li&gt;1: stdout 标准输出&lt;/li&gt;
&lt;li&gt;2: stderr 标准错误输出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在容器技术上，同样离不开这三个基本概念。在使用 docker run/exec 时，其实就是在 linux cgroup 和 namespace 下运行了一个普通的进程而已。所以这样的进程同样会有输入输出，当我们使用 &lt;code&gt;docker run nginx&lt;/code&gt; 的时候，nginx 的日志就会打印到终端上，其实就是将 nginx 进程的标准输入输出通过各种手段输出到终端上了。&lt;/p&gt;
&lt;h2&gt;-i 参数&lt;/h2&gt;
&lt;p&gt;docker 提供了 &lt;code&gt;-i&lt;/code&gt; 参数，来运行交互式的命令，文档上的说明是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-i,  --interactive                    Keep STDIN open even if not attached
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说即使没有使用 docker attach，也会保持 STDIN 打开。这样我们在终端的 STDIN 就会定向到容器内 sh 进程的 STDIN，这样就可以在终端上输入命令行了。 比如 &lt;code&gt;docker run -i nginx sh&lt;/code&gt;。这里就是使用 nginx 镜像启动容器，并运行了 sh 命令，然后终端的 bash 的 STDIN 定向到容器内的 sh。如下图所示： &lt;img src=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_13-01-18.png&quot; alt=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_13-01-18.png&quot; /&gt; 不过我们也可以发现，这里的输出格式似乎和在终端里 &lt;code&gt;ls&lt;/code&gt; 的输出不同。这里就要引入一个新的知识点：tty 与 pty 以及我们的 -t 参数。&lt;/p&gt;
&lt;h2&gt;-t 参数&lt;/h2&gt;
&lt;p&gt;官方文档上的说明是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-t, --tty                            Allocate a pseudo-TTY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tty 得名于早期的电传打字机(Teletype)，随着技术的发展，tty 的概念变得更宽泛，它不再指打字机这样的物理设备，很多时候指的是 linux 内核中的 tty 驱动。当我们使用无 GUI 的 linux 时，比如 server 版本的 ubuntu，我们使用 &lt;code&gt;ctrl+alt+f1~f6&lt;/code&gt;就可以在 tty1~6 之间进行切换。这里的 tty 就是由 linux 内核模拟出来的。 &lt;img src=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_13-18-38.png&quot; alt=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_13-18-38.png&quot; /&gt; 不过在 GUI 场景中，我们打开的终端并不是使用 tty，而是 pseudo-TTY(下文称之为 pty)。pty 也称为伪终端，也就是说它并不是真实的 tty 设备。引入了 pty 之后，终端的数量就不会再有限制了。我们可以打开任意多的 GUI 终端程序，每打开一个 GUI 终端，就会在 &lt;code&gt;/dev/pts/&lt;/code&gt; 下生成一个数字的文件，这个文件就是 pty slave，另外全局还有共用的一个 pty master，位于 /dev/ptmx。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过 GUI 终端，我们可以输入字符，输入的字符会发到 pty master 上，pty master 文件描述符，发送到对应的 pty slave。&lt;/li&gt;
&lt;li&gt;pty slave 将输入发送给终端上的 shell 程序，比如 bash。&lt;/li&gt;
&lt;li&gt;bash 上执行的任何命令，都会将输出发送给 bash，bash 再发送给 pty slave，之后再经过 tty driver, pty master 到 gui 终端上显示。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;pty 的工作原理如下图： &lt;img src=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_13-28-22.png&quot; alt=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_13-28-22.png&quot; /&gt; 这时候我们在回到 -t 参数，这里其实就是为容器内的 sh 进程分配了一个 pty slave。当 sh 感知到自己被分配了 pty slave 后，它会采用不同的工作模式。比如会在终端上输出提示符，对输出的格式进行调整，像 bash 之类的还是加上颜色等等。 &lt;img src=&quot;/uploads/wp/2023/03/image.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 下面这段程序会输出很多行，也说明了在 sh 里执行 ls 时，原本的输出是很多行的。
root@5f96c3475e20:/# ls | while read line; do echo $line;done
bin
boot
dev
docker-entrypoint.d
docker-entrypoint.sh
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;回到 runc&lt;/h2&gt;
&lt;p&gt;上述提到 stdio，pty 等概念，很好的解决了运行容器时的输入输出的处理。而这些能力也是 runc 本身就提供的，docker 是在其之上进行了封装。 runc 的输入输出处理有四种模式，由 -d(detach)，-t 进行组合得到。&lt;/p&gt;
&lt;h3&gt;terminal &amp;amp;&amp;amp; detached&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_13-44-19.png&quot; alt=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_13-44-19.png&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;上层的管控程序，比如 runc-shim 会创建 unix socket&lt;/li&gt;
&lt;li&gt;runc 运行在容器内，接管容器的 stdio。然后通过 unix socket 将 pty master 和 fd 发送给上层管控程序。&lt;/li&gt;
&lt;li&gt;将自己的 stdio 通过 pty slave 输入输出。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;passthrough &amp;amp;&amp;amp; detached&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_14-02-40.png&quot; alt=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_14-02-40.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;passthrough &amp;amp;&amp;amp; foreground&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_14-03-28.png&quot; alt=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_14-03-28.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;terminal &amp;amp;&amp;amp; foreground&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_14-03-22.png&quot; alt=&quot;/uploads/wp/2023/03/Snipaste_2023-03-08_14-03-22.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://www.wowotech.net/tty_framework/tty_concept.html&quot; rel=&quot;noopener&quot;&gt;http://www.wowotech.net/tty_framework/tty_concept.html&lt;/a&gt; &lt;a href=&quot;http://www.wowotech.net/tty_framework/tty_architecture.html&quot; rel=&quot;noopener&quot;&gt;http://www.wowotech.net/tty_framework/tty_architecture.html&lt;/a&gt; &lt;a href=&quot;http://www.wowotech.net/tty_framework/application_view.html&quot; rel=&quot;noopener&quot;&gt;http://www.wowotech.net/tty_framework/application_view.html&lt;/a&gt; &lt;a href=&quot;https://blog.51cto.com/u_14592069/5824829&quot; rel=&quot;noopener&quot;&gt;https://blog.51cto.com/u_14592069/5824829&lt;/a&gt; &lt;a href=&quot;https://dev.to/napicella/linux-terminals-tty-pty-and-shell-192e&quot; rel=&quot;noopener&quot;&gt;https://dev.to/napicella/linux-terminals-tty-pty-and-shell-192e&lt;/a&gt; &lt;a href=&quot;https://github.com/opencontainers/runc/blob/main/docs/terminals.md&quot; rel=&quot;noopener&quot;&gt;https://github.com/opencontainers/runc/blob/main/docs/terminals.md&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>容器技术</category><author>joyme123</author></item><item><title>kubelet PLEG 的实现与优化</title><link>https://www.myway5.com/blog/kubelet-pleg-implementation/</link><guid isPermaLink="true">https://www.myway5.com/blog/kubelet-pleg-implementation/</guid><description>PLEG 全称是 Pod Lifecycle Event Generator，用来为 kubelet 生成 container runtime 的 pod 生命周期事件，这样 kubelet 就可以根据 pod 的 spec 和 status 对比，来执行对应的控制逻辑。</description><pubDate>Mon, 27 Feb 2023 09:02:39 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;PLEG&lt;/strong&gt; 全称是 &lt;strong&gt;Pod Lifecycle Event Generator&lt;/strong&gt;，用来为 kubelet 生成 container runtime 的 pod 生命周期事件，这样 kubelet 就可以根据 pod 的 spec 和 status 对比，来执行对应的控制逻辑。 在 1.1 及之前的 kubelet 中是没有 PLEG 的实现的。kubelet 会为每个 pod 单独启动一个 worker，这个 worker 负责向 container runtime 查询该 pod 对应的 sandbox 和 container 的状态，并进行状态同步逻辑的执行。这种 one worker per pod 的 polling 模型给 kubelet 带来了较大的性能损耗。即使这个 pod 没有任何的状态变化，也要不停的对 container runtime 进行主动查询。 因此在 1.2 中，kubelet 引入了 PLEG，将所有 container runtime 上 sandbox 和 container 的状态变化事件统一到 PLEG 这个单独的组件中，实现了 one worker all pods。这种实现相比于 one worker per pod 已经带来了较大的性能提升，详细实现会在后文进行介绍。但是默认情况下，仍然需要每秒一次的主动向 container runtime 查询，在 node 负载很高的情况下，依然会有一定的性能问题，比较常见的情况是导致 node not ready，错误原因是 &lt;code&gt;PLEG is not healthy&lt;/code&gt;。 在 1.26 中，kubelet 引入了 Evented PLEG，为了和之前的 PLEG 实现区别，之前的 PLEG 称为 Generic PLEG。当然，Evented PLEG 并不是为了取代 Generic PLEG，而是和 Generic PLEG 配合，降低 Generic PLEG 的 polling 频率，从而提高性能的同时，也能保证实时性。&lt;/p&gt;
&lt;h2&gt;Generic PLEG&lt;/h2&gt;
&lt;p&gt;Generic PLEG 定时(默认1s)向 runtime 进行查询，这个过程称为 relist，这里会调用 cri 的 &lt;code&gt;ListPodSandbox&lt;/code&gt; 和 &lt;code&gt;ListContainers&lt;/code&gt;接口。runtime 返回所有的数据之后，PLEG 会根据 sandbox 和 container 上的数据，对应的 Pod 上，并更新到缓存中。同时，组装成事件向 PLEG Channel 发送。 &lt;img src=&quot;/uploads/wp/2023/02/Snipaste_2023-02-27_16-10-20.png&quot; alt=&quot;/uploads/wp/2023/02/Snipaste_2023-02-27_16-10-20.png&quot; /&gt; kubelet 会在 pod sync loop 中监听 PLEG Channel，从而针对状态变化执行相应的逻辑，来尽量保证 pod spec 和 status 的一致。&lt;/p&gt;
&lt;h2&gt;Evented PLEG&lt;/h2&gt;
&lt;p&gt;引入 Evented PLEG 后，对 Generic PLEG 做了些许调整，主要是 relist 的周期和阈值，以及对缓存的更新策略。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;relist 的同步周期由 1s 增加到 300s。同步阈值从 3min 增加到 10min。&lt;/li&gt;
&lt;li&gt;缓存更新时，updateTime 不再是取本地的时间，而是 runtime 返回的时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除此之外，Generic PLEG 会和之前一样运行，这样也保证了及时 Evented PLEG 丢失了一些 状态变更的 event，也可以由 Generic PLEG 兜底。 Evented PLEG 会调用 runtime 的 &lt;code&gt;GetContainerEvents&lt;/code&gt; 来监听 runtime 中的事件，然后生成 pod 的 event，并发送到 PLEG Channel 中供 kubelet pod sync loop 消费。 如果 Evented 不能按照预期工作（比如 runtime 不支持 GetContainerEvents），还会降级到 Generic PLEG。降级逻辑是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;停止自己。&lt;/li&gt;
&lt;li&gt;停止已有的 Generic PLEG。&lt;/li&gt;
&lt;li&gt;更新 Generic PLEG 的 relist 周期和阈值为 1s, 3min。&lt;/li&gt;
&lt;li&gt;启动新的 Generic PLEG。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2023/02/Snipaste_2023-02-27_16-58-56.png&quot; alt=&quot;/uploads/wp/2023/02/Snipaste_2023-02-27_16-58-56.png&quot; /&gt; &lt;img src=&quot;/uploads/wp/2023/02/Snipaste_2023-02-27_16-10-20-1.png&quot; alt=&quot;/uploads/wp/2023/02/Snipaste_2023-02-27_16-10-20-1.png&quot; /&gt; 因为 Evented PLEG 和 Generic PLEG 会同时更新缓存，所以在更新时还会对比当前值和缓存值的时间戳，保证当前值是更新的状态，才会更新到缓存中。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kubernetes/community/blob/4026287dc3a2d16762353b62ca2fe4b80682960a/contributors/design-proposals/node/pod-lifecycle-event-generator.md#leverage-upstream-container-events&quot; rel=&quot;noopener&quot;&gt;Kubelet: Pod Lifecycle Event Generator (PLEG)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/3386-kubelet-evented-pleg/README.md&quot; rel=&quot;noopener&quot;&gt;KEP-3386: Kubelet Evented PLEG for Better Performance&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>k8s</category><category>pleg</category><author>joyme123</author></item><item><title>Traceroute 的实现原理</title><link>https://www.myway5.com/blog/traceroute-implemetation/</link><guid isPermaLink="true">https://www.myway5.com/blog/traceroute-implemetation/</guid><description>traceroute 是一个很常用的工具，用来检查当前设备到目的 IP 地址的路径以及每个中间设备产生的延迟。如下图所示：</description><pubDate>Sat, 18 Feb 2023 07:02:07 GMT</pubDate><content:encoded>&lt;p&gt;traceroute 是一个很常用的工具，用来检查当前设备到目的 IP 地址的路径以及每个中间设备产生的延迟。如下图所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;traceroute to baidu.com (110.242.68.66), 64 hops max, 52 byte packets
 1  10.43.244.2 (10.43.244.2)  4.132 ms  2.294 ms  2.683 ms
 2  10.41.0.217 (10.41.0.217)  3.178 ms  1.846 ms  1.686 ms
 3  10.40.0.54 (10.40.0.54)  2.554 ms  2.033 ms  2.174 ms
 4  10.42.0.69 (10.42.0.69)  4.058 ms  2.905 ms  2.957 ms
 5  10.42.0.54 (10.42.0.54)  3.304 ms  3.058 ms *
 6  10.42.0.20 (10.42.0.20)  3.205 ms  3.199 ms  3.086 ms
 7  14.17.22.130 (14.17.22.130)  4.497 ms  3.424 ms  3.195 ms
 8  10.162.89.97 (10.162.89.97)  10.903 ms  4.797 ms  4.136 ms
 9  10.200.52.57 (10.200.52.57)  5.684 ms
    10.200.52.65 (10.200.52.65)  5.288 ms
    10.200.52.73 (10.200.52.73)  7.327 ms
10  * * *
11  * * *
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 mac/linux/windows 下都有类似的工具。因为在 &lt;a href=&quot;https://github.com/joyme123/gnt&quot; rel=&quot;noopener&quot;&gt;https://github.com/joyme123/gnt&lt;/a&gt; 中实现了 traceroute 的能力，这里记录一下。 traceroute 的实现中，利用了一些基本的网络协议的特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IP (网络层)数据包在网络中传输时，可以通过 TTL 字段来控制这个数据包的生命周期。每经过一个三层设备的转发，这个 TTL 都会减少1，当 TTL 变为 0 时，三层设备就会丢弃这个数据包而不是继续转发它。这样的设计可以避免网络链路形成环时，数据包会被无限的转发。&lt;/li&gt;
&lt;li&gt;中间的三层设备在丢弃数据包时，会使用 ICMP 协议，向数据包的源发送方(通过 src ip)发送 ICMP 包，来告知数据包因为 TTL 为 0 而被丢弃。当然有的三层设备不会发送这个 ICMP 消息，所以 traceroute 时部分中间环节会显示 *。并且这种 ICMP 包的 payload 都会包含源数据包的二层和三层 header。&lt;/li&gt;
&lt;li&gt;traceroute 支持多种协议: TCP、UDP 和 ICMP。当 TCP, UDP 的包到达目标设备后，如果 TCP, UDP 的目的端口不能访问，那么目标设备也会通过 ICMP 消息向源设备告知该端口不可达。如果是使用 ICMP echo request，那么目标设备会返回 ICMP echo reply 向源设备告知收到了 echo request。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有了以上的网络协议特性，traceroute 的实现可行性就有了。以使用 TCP 协议为例，具体的步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;traceroute 构造 TCP 数据，源端口根据当前进程 ID 生成，目的端口选择一个不太可能使用的端口，比如 33434。
&lt;ol&gt;
&lt;li&gt;源端口根据当前进程 ID 生成，这样收到中间网络设备的 ICMP 包时，就可以通过 payload 中携带的源数据包信息判断出这个 ICMP 是响应哪个进程的。&lt;/li&gt;
&lt;li&gt;目的端口选择一个不常用端口，是防止对目的端的 TCP 服务产生影响。并且这个目的端口每次请求都会加1。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;traceroute 构建 IP 头，TTL 一开始设置为 1。这样第一个中间设备收到后，就会丢弃这个包，并返回 ICMP 包了。后续 TTL 逐渐加 1，就可以探测到每一个中间设备了。&lt;/li&gt;
&lt;li&gt;当 traceroute 收到 ICMP 包时，先根据源端口判断这个包属不属于当前进程，再根据目的端口判断这个包是第几个发出的。比如目的端口是 33436，那么已知起始目的端口是 33034 的情况下，就知道这个包是第三个发出的。这样根据第三个包发出的时间，就知道延迟情况了。
&lt;ol&gt;
&lt;li&gt;如果这个 ICMP 包是 ttl exceeded，说明中间网络设备返回的。&lt;/li&gt;
&lt;li&gt;如果这个 ICMP 包是 destination unreachable，说明是目的设备返回的。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>网络</category><author>joyme123</author></item><item><title>Ping 与 ICMP 协议</title><link>https://www.myway5.com/blog/ping-and-icmp-protocol/</link><guid isPermaLink="true">https://www.myway5.com/blog/ping-and-icmp-protocol/</guid><description>ping 命令是一个非常常用的网络工具，通过 ICMP 协议来探测本地到远端地址之间网络的连通性，以及延迟，稳定等性能指标。但是大多数人其实对 ping 命令的实现了解的并不会太多，因为我们日常的开发工作中，很少会和 ICMP 协议打交道。</description><pubDate>Sat, 04 Feb 2023 06:34:32 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;ping 命令是一个非常常用的网络工具，通过 ICMP 协议来探测本地到远端地址之间网络的连通性，以及延迟，稳定等性能指标。但是大多数人其实对 ping 命令的实现了解的并不会太多，因为我们日常的开发工作中，很少会和 ICMP 协议打交道。因为最近在开发 &lt;a href=&quot;https://github.com/joyme123/gnt&quot; rel=&quot;noopener&quot;&gt;https://github.com/joyme123/gnt&lt;/a&gt;，目标是通过单个二进制文件，实现大多数的网络工具的能力。所以接触了一些 ping 命令的实现，这里做一些简单的记录和分享。&lt;/p&gt;
&lt;h2&gt;ping 命令基于 ICMP 协议的实现&lt;/h2&gt;
&lt;p&gt;ICMP 协议本身这里不多做介绍，网络上有很多很好很详细的资料。比如维基百科上的这篇介绍：&lt;a href=&quot;https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol#Control_messages&quot; rel=&quot;noopener&quot;&gt;Internet_Control_Message_Protocol&lt;/a&gt;。 ICMP 和 TCP/UDP 这样的协议有这很大的区别，像 TCP/UDP 这种传输层的协议，都是进程级别的，即可以通过 Port 对应到一个或多个进程。因此在运行使用 TCP/UDP 协议的应用时不需要特殊的权限。而 ICMP 则不一样，它是操作系统级别的，因此早期的 linux 上，如果应用要发送 ICMP 包，则必须通过 &lt;code&gt;socket(AF_INET, SOCK_RAW, int protocol)&lt;/code&gt; 这种方式来实现，而调用 SOCK_RAW 则需要 root 权限，或者通过 linux network capability。 因为这种特殊性，后来的 linux 又提供了 &lt;code&gt;socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)&lt;/code&gt; 这种方式去发送 ICMP，这里的 SOCK_DGRAM 是 UDP 协议使用的。不过容易误解的是，并不是说用 UDP 协议去包装或实现了 ICMP 的能力，使用这种方式发送出去的仍然是 ICMP 数据包，不过不再需要 root 权限或者特殊的 network capability 设置了。通常称这种方式为 Unprivileged ICMP。linux 同样也提供了 sysctl 的配置去限制这一能力的使用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;# 999~59999 指定了允许的用户组 ID 范围，如果所有用户组都不允许可以设置为 1 0
net.ipv4.ping_group_range = 999 59999
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过需要注意的是，通过 SOCK_DGRAM 只能发送这几种 ICMP 请求：&lt;code&gt;ICMP_ECHO&lt;/code&gt;, &lt;code&gt;ICMP_TSTAMP&lt;/code&gt; or &lt;code&gt;ICMP_MASKREQ&lt;/code&gt;. 解决了如何发送 icmp 包的问题，就可以考虑实现几个主要的 ping 命令特性了。&lt;/p&gt;
&lt;h3&gt;丢包检测&lt;/h3&gt;
&lt;p&gt;每个 ICMP 包都有一个 sequence 字段，发送的时候可以指定这个 sequence 的值，目的端响应的时候会把这个 sequence 值设置成一样的，表示响应的是哪一个请求包。这样我们就可以知道每个发送出去的 ICMP 的响应包了，那么没有响应的 sequence 就是被丢弃的包。通过这种方式就可以检测出网络中使用存在丢包现象。&lt;/p&gt;
&lt;h3&gt;延迟检测&lt;/h3&gt;
&lt;p&gt;ICMP 支持 echo request 和 reply，即通过 ICMP 协议包装的 payload 发送出去，目的端会原样返回。所以我们可以通过在 request 的时候写入发送时的时间，然后收到回包时取出这个时间就能知道延迟了。 关于写入的时间格式，实现方式上可以随意。但是建议使用 unix time，精确到微秒。然后保留 4 字节的秒+4字节的微秒，写入到 payload 的开头。这样的实现方式和 linux ping 的实现一致，像 wireshark 这种抓包工具就可以识别出来了。&lt;/p&gt;
</content:encoded><category>网络</category><author>joyme123</author></item><item><title>k8s 1.24 ServiceAccount Token 的行为变化</title><link>https://www.myway5.com/blog/k8s-1-24-serviceaccount-token-changes/</link><guid isPermaLink="true">https://www.myway5.com/blog/k8s-1-24-serviceaccount-token-changes/</guid><description>有一个 CNI 组件以 DaemonSet 的方式运行在所有的 node 上，这个 CNI Pod 会将自己的 Service Account Token 转换成 kubeconfig 并存储到主机的目录下。</description><pubDate>Wed, 18 Jan 2023 05:22:04 GMT</pubDate><content:encoded>&lt;h2&gt;起因&lt;/h2&gt;
&lt;p&gt;有一个 CNI 组件以 DaemonSet 的方式运行在所有的 node 上，这个 CNI Pod 会将自己的 Service Account Token 转换成 kubeconfig 并存储到主机的目录下。当 kubelet 调用 cni 插件时，cni 插件会使用这个 kubeconfig 去获取集群 Pod 的一些信息。 在 k8s 1.24 上出现了问题，当 CNI Pod 重启后，使用生成的 kubeconfig 就会返回 Unauthorized 的错误，即这个 token 已经过不了 APIServer 的认证了。&lt;/p&gt;
&lt;h2&gt;原因&lt;/h2&gt;
&lt;p&gt;k8s 1.24 上，ServiceAccount(下文缩写为 SA) 的 token 生成逻辑已经发生了变化，不再会自动为 SA 生成 token 并保存到 secret 中，Pod 中使用 token 时也不会再挂载这个 secret。当 Pod 使用 SA 时，默认行为如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pod 创建出来后，在 admission 阶段，有一个 serviceaccount admission 会为 Pod 挂载 token，路径同样还是在 &lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount&lt;/code&gt; 下。但是 volume 字段不再是通过 secret，而是通过 projected。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;projected:
  defaultMode: 420
  sources:
    # source 类型是 serviceAccountToken
  - serviceAccountToken:
      expirationSeconds: 3607
      path: token
  - configMap:
      items:
      - key: ca.crt
        path: ca.crt
      name: kube-root-ca.crt
  - downwardAPI:
      items:
      - fieldRef:
          apiVersion: v1
          fieldPath: metadata.namespace
        path: namespace
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Pod 调度到 Node 上后，kubelet 中的 projected volume mounter 会根据 volumesMount 中的 volume 类型，为 Pod 挂载对应的文件。当发现存在 ServiceAccountToken 类型的 projected source 时，就会调用 apiserver 的 TokenRequest 接口，为当前 Pod 请求临时的 Token。并且这个 token 的有效期只有 3607s。kubelet 会自动刷新这个 token 来保证它不会过期。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;case source.ServiceAccountToken != nil:
            tp := source.ServiceAccountToken

            // When FsGroup is set, we depend on SetVolumeOwnership to
            // change from 0600 to 0640.
            mode := *s.source.DefaultMode
            if mounterArgs.FsUser != nil || mounterArgs.FsGroup != nil {
                mode = 0600
            }

            var auds []string
            if len(tp.Audience) != 0 {
                auds = []string{tp.Audience}
            }
            tr, err := s.plugin.getServiceAccountToken(s.pod.Namespace, s.pod.Spec.ServiceAccountName, &amp;amp;authenticationv1.TokenRequest{
                Spec: authenticationv1.TokenRequestSpec{
                    Audiences:         auds,
                    ExpirationSeconds: tp.ExpirationSeconds,
                    BoundObjectRef: &amp;amp;authenticationv1.BoundObjectReference{
                        APIVersion: &quot;v1&quot;,
                        Kind:       &quot;Pod&quot;,
                        Name:       s.pod.Name,
                        UID:        s.pod.UID,
                    },
                },
            })
            if err != nil {
                errlist = append(errlist, err)
                continue
            }
            payload[tp.Path] = volumeutil.FileProjection{
                Data:   []byte(tr.Status.Token),
                Mode:   mode,
                FsUser: mounterArgs.FsUser,
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样带来的好处就是 service account 默认不再会有永久性 token，而是每个 Pod 有一个临时的 token，这个 token 默认有效期是 3607s，由 kubelet 自动刷新。并且当 Pod 删除后，该 token 也会自动失效。这在安全性上带来了很大的提升。&lt;/p&gt;
&lt;h2&gt;解决&lt;/h2&gt;
&lt;p&gt;为了和之前组件的行为保持一致，需要保证这个 token 是永久有效的。最简单的解决办法就是手动创建 service account 的 token secret。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: Secret
# 表示这个 secret 类型
type: kubernetes.io/service-account-token
metadata:
  name: mycontroller
  namespace: kube-system
  annotations:
    # service account 名称
    kubernetes.io/service-account.name: &quot;mycontroller&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;k8s 的 &lt;code&gt;tokens-controller&lt;/code&gt; 在 watch 到该 secret 时，会发现 ca, namespace, token 字段均为空，因此会自动为该 secret 填充这些字段。这样我们就获得了永久性的 token，并使用该 token 生成 kubeconfig 了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (e *TokensController) secretUpdateNeeded(secret *v1.Secret) (bool, bool, bool) {
    caData := secret.Data[v1.ServiceAccountRootCAKey]
    needsCA := len(e.rootCA) &amp;gt; 0 &amp;amp;&amp;amp; !bytes.Equal(caData, e.rootCA)

    needsNamespace := len(secret.Data[v1.ServiceAccountNamespaceKey]) == 0

    tokenData := secret.Data[v1.ServiceAccountTokenKey]
    needsToken := len(tokenData) == 0

    return needsCA, needsNamespace, needsToken
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Token 是如何做身份认证的&lt;/h2&gt;
&lt;p&gt;service account token 在不同版本下的行为不同，那么 token 本身又是如何做身份认证的呢？ token 是一个符合 JWT 规范的字符串。 对于永久性 token 来说，其中保存了 service account 的信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;iss&quot;: &quot;kubernetes/serviceaccount&quot;,
  &quot;kubernetes.io/serviceaccount/namespace&quot;: &quot;kube-system&quot;,
  &quot;kubernetes.io/serviceaccount/secret.name&quot;: &quot;mycontroller&quot;,
  &quot;kubernetes.io/serviceaccount/service-account.name&quot;: &quot;mycontroller&quot;,
  &quot;kubernetes.io/serviceaccount/service-account.uid&quot;: &quot;2f0ab840-064c-4168-b9b2-932c361e13d6&quot;,
  &quot;sub&quot;: &quot;system:serviceaccount:kube-system:mycontroller&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;apiserver 在获取到这个 token 后，根据 JWT 的规范对内容进行完整性校验。校验通过后就根据 token 中 service account 进行认证鉴权了。 对于临时性(pod) token 来说，内容就稍有不同了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;aud&quot;: [
    &quot;https://kubernetes.default.svc.cluster.local&quot;
  ],
  &quot;exp&quot;: 1705344168,
  &quot;iat&quot;: 1673808168,
  &quot;iss&quot;: &quot;https://kubernetes.default.svc.cluster.local&quot;,
  &quot;kubernetes.io&quot;: {
    &quot;namespace&quot;: &quot;kube-system&quot;,
    &quot;pod&quot;: {
      &quot;name&quot;: &quot;mycontroller-lr99n&quot;,
      &quot;uid&quot;: &quot;f8a3c6c7-c41c-4a33-9329-f40d208a03e6&quot;
    },
    &quot;serviceaccount&quot;: {
      &quot;name&quot;: &quot;mycontroller&quot;,
      &quot;uid&quot;: &quot;2f0ab840-064c-4168-b9b2-932c361e13d6&quot;
    },
    &quot;warnafter&quot;: 1673811775
  },
  &quot;nbf&quot;: 1673808168,
  &quot;sub&quot;: &quot;system:serviceaccount:kube-system:mycontroller&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到 token 中除了 service account 的信息，还有 pod 的信息。这样 token 的有效期是由 pod 的生命周期以及 nbf, exp 来确定了。nbf 代表 &lt;code&gt;Not valid before&lt;/code&gt;，exp 代表 &lt;code&gt;Expiration time&lt;/code&gt;，都是使用 unix time 来保存的。并且在 pod 删除后，token 就自动失效了。同时鉴权还是使用 service account 进行。&lt;/p&gt;
</content:encoded><category>k8s</category><category>k8s</category><category>service account</category><author>joyme123</author></item><item><title>OpenFlow 的流表匹配规则</title><link>https://www.myway5.com/blog/openflow-match-rule/</link><guid isPermaLink="true">https://www.myway5.com/blog/openflow-match-rule/</guid><description>OpenFlow 里有一个重要的概念—流表(FlowTable)，通过 Flow Table，我们可以制定交换机处理流量的行为。因此，理解流表匹配规则，是理解 OpenFlow 的重要一环。</description><pubDate>Mon, 09 Jan 2023 14:16:06 GMT</pubDate><content:encoded>&lt;p&gt;OpenFlow 里有一个重要的概念—流表(FlowTable)，通过 Flow Table，我们可以制定交换机处理流量的行为。因此，理解流表匹配规则，是理解 OpenFlow 的重要一环。 OpenvSwitch(下文称为OVS) 是一个开源的软件交换机的实现，同时也是支持 OpenFlow 的，因此下文也会通过 OVS 来说明流表是如何匹配的。&lt;/p&gt;
&lt;h2&gt;流表匹配规则&lt;/h2&gt;
&lt;h3&gt;流表项&lt;/h3&gt;
&lt;p&gt;在一个 OpenFlow 的网络中，每一个支持 OpenFlow 的交换机都必须包含至少 1 个流表(table 0)，这个流表里会包含 0 或多个流表项。这些流表项描述了流量的匹配规则，计数器以及如果针对这些流量做出动作。 一个流表项通常由以下元素组成&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Match Fileds&lt;/td&gt;
&lt;td&gt;用来匹配数据包&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Priority&lt;/td&gt;
&lt;td&gt;匹配流表项的优先级。如果一个数据包被多个流表项匹配到，则会根据优先级进行选择&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Counters&lt;/td&gt;
&lt;td&gt;当数据包被匹配成功时会更新该字段，主要用来流量统计&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instructions&lt;/td&gt;
&lt;td&gt;用来修改 Actions 或 流水线处理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeouts&lt;/td&gt;
&lt;td&gt;流表项的超时时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cookie&lt;/td&gt;
&lt;td&gt;一些不透明的数据值。控制器可以使用它来过滤流量统计，流量修改和流量删除。在处理数据包时不使用这些数据值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;流水线处理&lt;/h3&gt;
&lt;p&gt;流表在处理时，就像流水线一下，每个 table 都是一个处理阶段。在每一个 table 里，数据包都可能被：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;丢弃。&lt;/li&gt;
&lt;li&gt;转发给下一个 table&lt;/li&gt;
&lt;li&gt;发送给 controller&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2023/01/openflow-flow.webp&quot; alt=&quot;/uploads/wp/2023/01/openflow-flow.webp&quot; /&gt; 如上图所示，数据包通过 port 进入交换机后，首先会匹配 table 0 中的流表项。如果流表项匹配到了，则会根据该流表项设定的 actions 进行操作。如果未匹配到，则会被丢弃。 数据包在不同的 table 中流转时，只能按照升序进行，也就是说可以从 table 0 → table10，但是不能从 table10 → table0。并且需要注意的是，在流表之间跳转需要使用 goto_table 或 resubmit 语句来将数据包 copy 一份转发到其他的流表中处理。如果没有指定跳转动作，是不会继续在其他 table 中进行匹配的。 &lt;img src=&quot;/uploads/wp/2023/01/openflow-flow-det.webp&quot; alt=&quot;/uploads/wp/2023/01/openflow-flow-det.webp&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;OVS 操作演示&lt;/h2&gt;
&lt;p&gt;为了加深对 OpenFlow 的理解，可以使用 OVS 提供的一个例子进行练习。这个例子中，使用 OVS 的流表实现了交换机二层 Mac 地址的学习，以及 VLAN 的支持。&lt;/p&gt;
&lt;h3&gt;环境准备&lt;/h3&gt;
&lt;p&gt;和 ovs 官网不同的是，这里我使用 mininet 快速创建一个实验环境。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo mn --topo=single,4 --mac --controller=none
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里创建了一个名为 s1 的交换机和 4 台连接到交换机上的主机 h1, h2, h3, h4，通过 port1, port2, port3, port4 连接。port1 trunk 所有的 vlan，port2 access vlan 20, port3, port3 access vlan 30。 &lt;img src=&quot;/uploads/wp/2023/01/ovs.drawio-1.png&quot; alt=&quot;/uploads/wp/2023/01/ovs.drawio-1.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;table 0: 准入控制&lt;/h3&gt;
&lt;p&gt;table 0 是数据包进入交换机的起始位置。我们在这一步去禁止某些数据包的进入。比如：以多播源地址的数据包是非法的，所以我们可以在这里丢弃掉。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl add-flow s1 &quot;table=0, dl_src=01:00:00:00:00:00/01:00:00:00:00:00, actions=drop&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;交换机也不应该转发 IEEE 802.1D Spanning Tree Protocol(STP) 数据包，所以我们在可以通过流表项来丢弃掉。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl add-flow s1 &quot;table=0, dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0, actions=drop&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于其他合法的数据包，我们都提交到流水线的下一阶段(table1) 中去处理。这里我们使用最低的优先级来做兜底&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl add-flow s1 &quot;table=0, priority=0, actions=resubmit(,1)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;测试 table 0&lt;/h3&gt;
&lt;p&gt;这里因为有一些数据包不方便构造去做实际的测试，所以使用 &lt;code&gt;ofproto/trace&lt;/code&gt; 去模拟数据库的匹配。 &lt;strong&gt;例子1&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=01:80:c2:00:00:05
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会出现以下输出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Flow: in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=01:80:c2:00:00:05,dl_type=0x0000

bridge(&quot;s1&quot;)
------------
 0. dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0, priority 32768
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到以下关键信息：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据包匹配到了 &lt;code&gt;dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0, priority 32768&lt;/code&gt; ,根据 action 需要被 drop 掉&lt;/li&gt;
&lt;li&gt;Final flow 为 unchanged ，说明数据包本身没有被修改。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;例子2&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=01:80:c2:00:00:10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;出现以下输出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Flow: in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=01:80:c2:00:00:10,dl_type=0x0000

bridge(&quot;s1&quot;)
------------
 0. priority 0
    resubmit(,1)
 1. No match.
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=01:80:c2:00:00:10/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出数据包被 resubmit 到 table 1 了，但是因为 table 1 中没有任何流表项，所以被 drop 了。&lt;/p&gt;
&lt;h3&gt;table 1: VLAN 进入数据包处理&lt;/h3&gt;
&lt;p&gt;进入 table 1 的数据包，都是在 table 0 中被校验有效的数据包。table 1 被设计用来校验数据包的 VLAN，这个校验是基于数据包进入交换机经过的 Port 配置。同时我们也会在这些进入 access port 的数据包 header 里添加 VLAN tag，来保证后续的处理都可以基于这些 VLAN tag 进行。 首先我们添加一个默认 drop 的规则&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl add-flow s1 &quot;table=1, priority=0, actions=drop&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 trunk port 1，可以接收任意数据包，无论数据包是否有 VLAN header 或者这个 VLAN tag 是多少。所以可以添加一个流表项，将进入 port1 的所有数据包 resubmit 到 table 2 中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl add-flow s1 &quot;table=1, priority=99, in_port=1, actions=resubmit(,2)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 access port 上，只接收没有 VLAN header 的数据包，然后对数据包打上 VLAN tag，然后再提交到下一阶段(table2) 去处理。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl add-flow s1 &quot;table=1, priority=99, in_port=2, vlan_tci=0, actions=mod_vlan_vid:20, resubmit(,2)&quot;
sh ovs-ofctl add-flow s1 &quot;table=1, priority=99, in_port=3, vlan_tci=0, actions=mod_vlan_vid:30, resubmit(,2)&quot;
sh ovs-ofctl add-flow s1 &quot;table=1, priority=99, in_port=4, vlan_tci=0, actions=mod_vlan_vid:30, resubmit(,2)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;测试 table 1&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;例子1：数据包进入 trunk port&lt;/strong&gt; 数据包进入 trunk port 1&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=1,vlan_tci=5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到以下输出，数据包在 table 0 中 resubmit 到 table 1，再到 table 2 后没有规则，被默认丢弃&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Flow: in_port=1,vlan_tci=0x0005,vlan_tci1=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,dl_type=0x0000

bridge(&quot;s1&quot;)
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. No match.
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=00:00:00:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;例子2：有效的数据包进入 access port&lt;/strong&gt; 一个没有 802.1Q header 的数据包进入 port 2&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到以下输出，数据包在 table 0 中 resubmit 到 table 1，再到 table 2 后没有规则，被默认丢弃i&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Flow: in_port=2,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,dl_type=0x0000

bridge(&quot;s1&quot;)
------------
 0. priority 0
    resubmit(,1)
 1. in_port=2,vlan_tci=0x0000, priority 99
    mod_vlan_vid:20
    resubmit(,2)
 2. No match.
    drop

Final flow: in_port=2,dl_vlan=20,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=2,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=00:00:00:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;例子3: 无效的数据包进入 access port&lt;/strong&gt; 一个有 802.1Q header 的数据包进入 port 2&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=2,vlan_tci=5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到以下输出，在 table 1 中被 drop 了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Flow: in_port=2,vlan_tci=0x0005,vlan_tci1=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,dl_type=0x0000

bridge(&quot;s1&quot;)
------------
 0. priority 0
    resubmit(,1)
 1. priority 0
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=2,vlan_tci=0x0005,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=00:00:00:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;table 2: 进入 port 后学习 MAC+VLAN&lt;/h3&gt;
&lt;p&gt;table 2允许我们实现的交换机学习数据包的 source mac。只需要一个流表项&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl add-flow s1 &quot;table=2 actions=learn(table=10, NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[], load:NXM_OF_IN_PORT[]-&amp;gt;NXM_NX_REG0[0..15]),resubmit(,3)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;learn 这个 action 会基于正在处理的数据包，动态的修改流表。对于 learn 的几个字段说明如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;table=10

    Modify flow table 10.  This will be the MAC learning table.

NXM_OF_VLAN_TCI[0..11]

    Make the flow that we add to flow table 10 match the same VLAN
    ID that the packet we&apos;re currently processing contains.  This
    effectively scopes the MAC learning entry to a single VLAN,
    which is the ordinary behavior for a VLAN-aware switch.

NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[]

    Make the flow that we add to flow table 10 match, as Ethernet
    destination, the Ethernet source address of the packet we&apos;re
    currently processing.

load:NXM_OF_IN_PORT[]-&amp;gt;NXM_NX_REG0[0..15]

    Whereas the preceding parts specify fields for the new flow to
    match, this specifies an action for the flow to take when it
    matches.  The action is for the flow to load the ingress port
    number of the current packet into register 0 (a special field
    that is an Open vSwitch extension to OpenFlow).
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;测试 table 2&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;例子1&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=1,vlan_tci=20,dl_src=50:00:00:00:00:01 -generate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到以下输出。上面的命令中使用了 &lt;code&gt;-generate&lt;/code&gt;，是为了让数据包真实的在 OVS 中生效，不指定的话，OVS 不会真实的生成 table 10 中的流表项。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Flow: in_port=1,vlan_tci=0x0014,vlan_tci1=0x0000,dl_src=50:00:00:00:00:01,dl_dst=00:00:00:00:00:00,dl_type=0x0000

bridge(&quot;s1&quot;)
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]-&amp;gt;NXM_NX_REG0[0..15])
     -&amp;gt; table=10 vlan_tci=0x0014/0x0fff,dl_dst=50:00:00:00:00:01 priority=32768 actions=load:0x1-&amp;gt;NXM_NX_REG0[0..15]
    resubmit(,3)
 3. No match.
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,vlan_tci=0x0014/0x1fff,dl_src=50:00:00:00:00:01,dl_dst=00:00:00:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 table 10 中确认是否有新的流表项生成&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl dump-flows s1 table=10

cookie=0x0, duration=142.170s, table=10, n_packets=0, n_bytes=0, vlan_tci=0x0014/0x0fff,dl_dst=50:00:00:00:00:01 actions=load:0x1-&amp;gt;NXM_NX_REG0[0..15]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;例子2&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=2,dl_src=50:00:00:00:00:01 -generate

cookie=0x0, duration=193.493s, table=10, n_packets=0, n_bytes=0, vlan_tci=0x0014/0x0fff,dl_dst=50:00:00:00:00:01 actions=load:0x2-&amp;gt;NXM_NX_REG0[0..15]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，在例子 1 中 table 10 中，在 port 1 上学习到了 mac 地址 &lt;code&gt;50:00:00:00:00:01&lt;/code&gt;，现在 port 2 中也出现了该 mac 地址，所以这个 mac 地址更新到了 port 2 上。&lt;/p&gt;
&lt;h3&gt;table 3: 查找目标 Port&lt;/h3&gt;
&lt;p&gt;这个 table 实现了如何通过 MAC 和 VLAN 查找到目标的 output port。通过以下流表项来实现查找&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl add-flow s1 &quot;table=3 priority=50 actions=resubmit(,10), resubmit(,4)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个流表项首先将数据包提交到 table 10 中。table 10 中存储了学习到的 mac 地址。如果这个 mac 地址没有被学习过，则 table 10 中不会被匹配。那么就会被 resubmit 到 table 4 中继续处理。&lt;/p&gt;
&lt;h3&gt;测试 table 3&lt;/h3&gt;
&lt;p&gt;下面的命令会让 OVS 学习到 port 1上 VLAN 20 的 mac 地址 f0:00:00:00:00:01&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=1,dl_vlan=20,dl_src=f0:00:00:00:00:01,dl_dst=90:00:00:00:00:01 -generate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到以下输出，数据包在 table 10 中没有匹配到，所以被 resubmit 到 table 4.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Flow: in_port=1,dl_vlan=20,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=f0:00:00:00:00:01,dl_dst=90:00:00:00:00:01,dl_type=0x0000

bridge(&quot;s1&quot;)
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]-&amp;gt;NXM_NX_REG0[0..15])
     -&amp;gt; table=10 vlan_tci=0x0014/0x0fff,dl_dst=f0:00:00:00:00:01 priority=32768 actions=load:0x1-&amp;gt;NXM_NX_REG0[0..15]
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. No match.
            drop
    resubmit(,4)
 4. No match.
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_vlan=20,dl_src=f0:00:00:00:00:01,dl_dst=90:00:00:00:00:01,dl_type=0x0000
Datapath actions: drop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过以下两种方式验证 port 1 上学习到的 mac 地址： 方法一：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl dump-flows s1 table=10

cookie=0x0, duration=107.451s, table=10, n_packets=0, n_bytes=0, vlan_tci=0x0014/0x0fff,dl_dst=f0:00:00:00:00:01 actions=load:0x1-&amp;gt;NXM_NX_REG0[0..15]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法二：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=2,dl_src=90:00:00:00:00:01,dl_dst=f0:00:00:00:00:01 -generate

Flow: in_port=2,vlan_tci=0x0000,dl_src=90:00:00:00:00:01,dl_dst=f0:00:00:00:00:01,dl_type=0x0000

bridge(&quot;s1&quot;)
------------
 0. priority 0
    resubmit(,1)
 1. in_port=2,vlan_tci=0x0000, priority 99
    mod_vlan_vid:20
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]-&amp;gt;NXM_NX_REG0[0..15])
     -&amp;gt; table=10 vlan_tci=0x0014/0x0fff,dl_dst=90:00:00:00:00:01 priority=32768 actions=load:0x2-&amp;gt;NXM_NX_REG0[0..15]
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. vlan_tci=0x0014/0x0fff,dl_dst=f0:00:00:00:00:01, priority 32768
            load:0x1-&amp;gt;NXM_NX_REG0[0..15]
    resubmit(,4)
 4. No match.
    drop

Final flow: reg0=0x1,in_port=2,dl_vlan=20,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=90:00:00:00:00:01,dl_dst=f0:00:00:00:00:01,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=2,dl_src=90:00:00:00:00:01,dl_dst=f0:00:00:00:00:01,dl_type=0x0000
Datapath actions: drop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到在 table 10 中匹配到了流表项，学习到了 &lt;code&gt;load:0x1-&amp;gt;NXM_NX_REG0[0..15]&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;table 4: 数据包输出处理&lt;/h3&gt;
&lt;p&gt;在 table 4 中，我们知道 register 0 包含了需要的 output port，如果该 output port 是 0，则说明需要将数据包 flood。我们也知道数据包的 VLAN 在它的 802.1Q header 上。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl add-flow s1 &quot;table=4 reg0=1 actions=1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于要 output 的 port，还需要把 VLAN header 移除掉。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl add-flow s1 &quot;table=4 reg0=2 actions=strip_vlan,2&quot;
sh ovs-ofctl add-flow s1 &quot;table=4 reg0=3 actions=strip_vlan,3&quot;
sh ovs-ofctl add-flow s1 &quot;table=4 reg0=4 actions=strip_vlan,4&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flood 广播或多播包&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-ofctl add-flow s1 &quot;table=4 reg0=0 priority=99 dl_vlan=20 actions=1,strip_vlan,2&quot;
sh ovs-ofctl add-flow s1 &quot;table=4 reg0=0 priority=99 dl_vlan=30 actions=1,strip_vlan,3,4&quot;
sh ovs-ofctl add-flow s1 &quot;table=4 reg0=0 priority=50            actions=1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;测试 table 4&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;例子1：广播，多播以及未知目的地址&lt;/strong&gt; 测试在 port 1 上进入广播包，VLAN 是 30&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=ff:ff:ff:ff:ff:ff,dl_vlan=30

Flow: in_port=1,dl_vlan=30,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=00:00:00:00:00:00,dl_dst=ff:ff:ff:ff:ff:ff,dl_type=0x0000

bridge(&quot;s1&quot;)
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]-&amp;gt;NXM_NX_REG0[0..15])
     &amp;gt;&amp;gt; suppressing side effects, so learn action ignored
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. No match.
            drop
    resubmit(,4)
 4. reg0=0,dl_vlan=30, priority 99
    output:1
     &amp;gt;&amp;gt; skipping output to input port
    strip_vlan
    output:3
    output:4

Final flow: in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=ff:ff:ff:ff:ff:ff,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=1,dl_vlan=30,dl_vlan_pcp=0,dl_src=00:00:00:00:00:00,dl_dst=ff:ff:ff:ff:ff:ff,dl_type=0x0000
Datapath actions: pop_vlan,4,3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到 &lt;code&gt;Datapath actions: pop_vlan,4,3&lt;/code&gt;，最终数据包被移除 vlan，从 port3，4出去了。 而下面的广播包都会被 drop，因为 VLAN 必须属于 input port&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=ff:ff:ff:ff:ff:ff
sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=ff:ff:ff:ff:ff:ff,dl_vlan=55
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;例子2： MAC 学习&lt;/strong&gt; VLAN 30， port 1 学习 MAC 地址：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=1,dl_vlan=30,dl_src=10:00:00:00:00:01,dl_dst=20:00:00:00:00:01 -generate

Flow: in_port=1,dl_vlan=30,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=10:00:00:00:00:01,dl_dst=20:00:00:00:00:01,dl_type=0x0000

bridge(&quot;s1&quot;)
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]-&amp;gt;NXM_NX_REG0[0..15])
     -&amp;gt; table=10 vlan_tci=0x001e/0x0fff,dl_dst=10:00:00:00:00:01 priority=32768 actions=load:0x1-&amp;gt;NXM_NX_REG0[0..15]
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. No match.
            drop
    resubmit(,4)
 4. reg0=0,dl_vlan=30, priority 99
    output:1
     &amp;gt;&amp;gt; skipping output to input port
    strip_vlan
    output:3
    output:4

Final flow: in_port=1,vlan_tci=0x0000,dl_src=10:00:00:00:00:01,dl_dst=20:00:00:00:00:01,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=1,dl_vlan=30,dl_vlan_pcp=0,dl_src=10:00:00:00:00:01,dl_dst=20:00:00:00:00:01,dl_type=0x0000
Datapath actions: pop_vlan,4,3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为目的地址是未知的，所以数据包被 flood 到 port3，port4 上。然后我们再次测试 MAC 地址是否学习到了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh ovs-appctl ofproto/trace s1 in_port=4,dl_src=20:00:00:00:00:01,dl_dst=10:00:00:00:00:01 -generate

Flow: in_port=4,vlan_tci=0x0000,dl_src=20:00:00:00:00:01,dl_dst=10:00:00:00:00:01,dl_type=0x0000

bridge(&quot;s1&quot;)
------------
 0. priority 0
    resubmit(,1)
 1. in_port=4,vlan_tci=0x0000, priority 99
    mod_vlan_vid:30
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]-&amp;gt;NXM_NX_REG0[0..15])
     -&amp;gt; table=10 vlan_tci=0x001e/0x0fff,dl_dst=20:00:00:00:00:01 priority=32768 actions=load:0x4-&amp;gt;NXM_NX_REG0[0..15]
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. vlan_tci=0x001e/0x0fff,dl_dst=10:00:00:00:00:01, priority 32768
            load:0x1-&amp;gt;NXM_NX_REG0[0..15]
    resubmit(,4)
 4. reg0=0x1, priority 32768
    output:1

Final flow: reg0=0x1,in_port=4,dl_vlan=30,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=20:00:00:00:00:01,dl_dst=10:00:00:00:00:01,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=4,dl_src=20:00:00:00:00:01,dl_dst=10:00:00:00:00:01,dl_type=0x0000
Datapath actions: push_vlan(vid=30,pcp=0),2
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>网络</category><category>ovs</category><author>joyme123</author></item><item><title>linux 网络问题排查手册</title><link>https://www.myway5.com/blog/linux-network-analysis/</link><guid isPermaLink="true">https://www.myway5.com/blog/linux-network-analysis/</guid><description>这里主要记录一些在实际排查网络问题过程中，觉得非常好用的工具或方法。大多数和 k8s 的网络问题相关 1\. iptables 处理规则流程图</description><pubDate>Sat, 10 Dec 2022 04:11:02 GMT</pubDate><content:encoded>&lt;p&gt;这里主要记录一些在实际排查网络问题过程中，觉得非常好用的工具或方法。大多数和 k8s 的网络问题相关&lt;/p&gt;
&lt;h2&gt;1. iptables 处理规则流程图&lt;/h2&gt;
&lt;p&gt;出处：&lt;a href=&quot;https://www.zsythink.net/archives/1199&quot; rel=&quot;noopener&quot;&gt;https://www.zsythink.net/archives/1199&lt;/a&gt; &lt;img src=&quot;/uploads/wp/2022/12/screenshot-20221210-113850.png&quot; alt=&quot;/uploads/wp/2022/12/screenshot-20221210-113850.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2. 使用 xtables-monitor 追踪数据包在 iptables 规则中的流向&lt;/h2&gt;
&lt;p&gt;这种场景常用在宿主机上有比较复杂的 iptables 规则，导致在排查问题时比较难发现。比如 k8s 中 pod 访问 svc ip 不通时。 首先在数据包必经的地方添加 TRACE，比如 raw 表的 PREROUTING 就是个好地方。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;iptables -t raw -A PREROUTING -p tcp --dport 5432 -j TRACE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加 TRACE 之后，就可以追踪该数据包后续的流向了。通过 trace 的信息，可以看到数据库的流入流出网卡，mark 值，匹配的 iptables 规则等等。非常容易帮我们判断出问题&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;xtables-monitor --trace
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 使用 conntrack 查看连接的生命周期&lt;/h2&gt;
&lt;p&gt;conntrack 是 linux 网络协议栈提供的能力，顾名思义，conntrack 提供的是连接追踪的能力。这里的连接并不是 tcp 的连接，而且传输层的 5 元组(源IP，源端口，目的IP，目的端口，协议)来唯一确定的连接。像 NAT 之类的功能，都是基于 conntrack 之上的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;# 比如这条命令就可以看 10.233.101.170 这个 pod 访问 svc ip 建立的连接请。
conntrack -p tcp -s 10.233.101.170 -L -o ktimestamp
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. no route to host 怎么解决&lt;/h2&gt;
&lt;p&gt;这个问题相信大多数人都遇到过，之所以要单独写在这里，是因为遇到过一些小众场景非常难排查。 大多数情况下，这个问题可以通过检查下面几项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查机器上的路由表，是否有通往目的 IP 的路由。&lt;/li&gt;
&lt;li&gt;确认网关配置是否正确，网关路由是否正常。&lt;/li&gt;
&lt;li&gt;抓包检查，这里我们可以选择抓 ARP 包。如：tcpdump -i any arp and host xx.xxx.xx.xx -ne。这种方式看似没什么用，但是非常好用，特别是在网络配置非常复杂(多网卡，多网络平面的 k8s 集群）时。有的时候很难准确判断出出去的数据包命中了什么样的路由。这种情况下，我们可以通过 arp 请求的网关 IP，来判断出路由是哪条，且网关是否正常连通。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;5. 路由判断理解是否正确&lt;/h2&gt;
&lt;p&gt;通过第一点中提供的图，我们可以发现，路由判断只在两个地方发生：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;PREROUTING 之后&lt;/li&gt;
&lt;li&gt;OUTPUT 之前&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;也就是说，当数据包通过了这两个地方之后，路由就已经确定了。在这之后做任何数据包的修改都不会再影响路由判断。 实际场景：使用 ip rule 可以创建一些策略路由，比如来自于 10.100.0.0/24 的数据包，mark 值为 0x06 时走什么路由策略。当在 POSTROUTING 链上做 SNAT，set mark 的时候，是不会影响路由的。&lt;/p&gt;
&lt;h2&gt;6. iptables 与 LVS&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2022/12/nf-lvs.png&quot; alt=&quot;/uploads/wp/2022/12/nf-lvs.png&quot; /&gt; 正常情况下，LVS nat 模式的流量不能做 SNAT，因为不会经过 POSTROUTING 链。需要增加下的配置来让 LVS 数据包经过 POSTROUTING&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;net.ipv4.vs.conntrack=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;iptables 在 PREROUTING 阶段做的 mark，在 LVS NAT 后仍然有效。&lt;/p&gt;
&lt;h2&gt;7. 容器网络不通解决思路&lt;/h2&gt;
&lt;p&gt;这里的容器网络仅指 docker/nerdctl 部署的容器网络，k8s 情况会更复杂，也要结合 CNI 去看，这里不讨论。 实际使用中，常见的使用方式是部署容器时使用端口(以 443 为例)映射，将宿主机的 port 映射到容器的 port。一般的底层实现都是使用 iptables DNAT，将发到 443 端口的流量 DNAT 到容器的 ip:port。这样再根据本机的路由，将流量通过 docker0/nerdctl1 这样的网桥进行转发即可。 根据以上的背景，一般我们可以检查下面几个地方：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;宿主机的 ip_forward 是否打开：&lt;code&gt;sysctl -a | grep ip_forward&lt;/code&gt;。需要确认值为 1，这时候宿主机才会将不属于自己的流量进行 FORWARD。&lt;/li&gt;
&lt;li&gt;宿主机的 bridge-nf-call-iptables 是否打开：&lt;code&gt;sysctl -a | grep bridge&lt;/code&gt;。需要确认值为 1，这时候 bridge 设备上的流量就会经过 iptables conntrack。&lt;/li&gt;
&lt;li&gt;检查 DNAT 规则。docker/nerdctl 一般都会在 iptables NAT 表上写这些规则。&lt;code&gt;iptables -t nat -L PREROUTING&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;检查 FILTER 表。实际使用中遇到过在宿主机上同时使用 docker 和 nerdctl 的用户，docker 默认将 FILTER 表 FORWARD 链改为 DROP Policy，也就是不匹配 FORWARD 下规则的流量都会丢弃。而 nerdctl 则是默认 ACCEPT。这样 nerdctl 启动的容器网络就会不通。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>网络</category><author>joyme123</author></item><item><title>通过 metrics-server 获取的 NodeMetrics 为何会不准确</title><link>https://www.myway5.com/blog/1271/</link><guid isPermaLink="true">https://www.myway5.com/blog/1271/</guid><description>在使用 metrics-server 的 NodeMetrics 获取 node 的 CPU 使用量时，会稍微大于 node 的 CPU 核心数，导致计算剩余可用的 CPU 时出现了负数。此时 node 的 cpu 是 100% 满载，但是理论上无论如何也不会超过 CPU 总量。</description><pubDate>Sun, 06 Nov 2022 06:05:44 GMT</pubDate><content:encoded>&lt;h2&gt;背景描述&lt;/h2&gt;
&lt;p&gt;在使用 metrics-server 的 NodeMetrics 获取 node 的 CPU 使用量时，会稍微大于 node 的 CPU 核心数，导致计算剩余可用的 CPU 时出现了负数。此时 node 的 cpu 是 100% 满载，但是理论上无论如何也不会超过 CPU 总量。&lt;/p&gt;
&lt;h2&gt;问题分析&lt;/h2&gt;
&lt;p&gt;通过 metrics-server 获取 NodeMetrics 的链路如下：metrics-server → kubelet 10250 端口→ cadvisor。所以问题的本质还是在于 cadvisor 如何统计 CPU 使用量。 通过分析 cadvisor 的代码可以知道，cadvisor 会读取 cgroup 根目录的 &lt;code&gt;cpuacct.usage&lt;/code&gt; 中的值，来获取当前累积使用的 CPU 时间。如下图代码所示。 &lt;img src=&quot;/uploads/wp/2022/11/cadvisor1.png&quot; alt=&quot;cadvisor1&quot; /&gt; &lt;code&gt;cpuacct.usage&lt;/code&gt; 的描述如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reports the total CPU time (in nanoseconds) consumed by all tasks in this cgroup (including tasks lower in the hierarchy).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;cadvisor 会定期（每秒）对 node 进行采样，保存到 CpuUsage 中。 &lt;img src=&quot;/uploads/wp/2022/11/cadvisor2.png&quot; alt=&quot;cadvisor2&quot; /&gt; 当 metrics-server 需要获取当前的 CPU 使用量时，cadvisor 会统计最近 60s 内（即60个采样数据）的累积 CPU 使用时间，并除以总采样时间(ns)，得到 CPU 使用量。 &lt;img src=&quot;/uploads/wp/2022/11/cadvisor3.png&quot; alt=&quot;cadvisor3&quot; /&gt; 因为 CPU 使用量的时间单位是精确到纳秒(ns)的，因此难免在计算上会有一定的误差，所以当 CPU 满载时出现统计出来的数据会稍大于 CPU 核心数也是正常情况了。&lt;/p&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>IPv6 学习笔记</title><link>https://www.myway5.com/blog/ipv6/</link><guid isPermaLink="true">https://www.myway5.com/blog/ipv6/</guid><description>IPv6 全称是 Internet Protocol Version 6，不过虽然是叫 Version 6，事实上是网络层协议的第二代标准协议。其出现主要是为了解决 IPv4 在实际应用场景中存在的一些缺陷。 与 IPv4 的优缺点对比 摘抄自：华为 IPv6 技术白皮书</description><pubDate>Sun, 20 Feb 2022 14:04:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 概述&lt;/h2&gt;
&lt;p&gt;IPv6 全称是 Internet Protocol Version 6，不过虽然是叫 Version 6，事实上是网络层协议的第二代标准协议。其出现主要是为了解决 IPv4 在实际应用场景中存在的一些缺陷。&lt;/p&gt;
&lt;h2&gt;与 IPv4 的优缺点对比&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;摘抄自：华为 IPv6 技术白皮书&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;IPv4缺陷&lt;/th&gt;
&lt;th&gt;IPv6优势&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;地址空间&lt;/td&gt;
&lt;td&gt;IPv4 地址只有32位，因此总共可表示的地址在 43 亿左右。另外由于历史原因，IP 地址的分配也非常不均衡：美国占全球地址 空间的一半左右，而欧洲则相对匮乏;亚太地区则更加匮乏。IPv4 中用来解决地址短缺的方法有：CIDR 和 NAT。不过这两种方案都有其本身的缺点。&lt;/td&gt;
&lt;td&gt;IPv6 有 128 位。理论上总共可以支持 43亿x43亿x43亿x43亿的地址。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;报文格式&lt;/td&gt;
&lt;td&gt;IPv4报头包含可选字段Options，内 容涉及Security、Timestamp、 Record route等，这些Options可以将 IPv4报头长度从20字节扩充到60字 节。携带这些Options的IPv4报文在 转发过程中往往需要中间路由转发 设备进行软件处理，对于性能是个 很大的消耗，因此实际中也很少使 用。&lt;/td&gt;
&lt;td&gt;IPv6和IPv4相比，去除了IHL、 Identifier、Flag、Fragment Offset、Header Checksum、 Option、Paddiing域，只增加了流 标签域，因此IPv6报文头的处理 较IPv4更为简化，提高了处理效 率。另外，IPv6为了更好支持各 种选项处理，提出了扩展头的概 念，新增选项时不必修改现有结 构，理论上可以无限扩展，体现 了优异的灵活性。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自动配置和重新编制&lt;/td&gt;
&lt;td&gt;由于IPv4地址只有32比特，并且地 址分配不均衡，导致在网络扩容或 重新部署时，经常需要重新分配IP 地址，因此需要能够进行自动配置 和重新编址，以减少维护工作量。 目前IPv4的自动配置和重新编址机 制主要依靠DHCP协议。&lt;/td&gt;
&lt;td&gt;IPv6协议内置支持通过地址自动 配置方式使主机自动发现网络并 获取IPv6地址，大大提高了内部 网络的可管理性。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;路由聚合&lt;/td&gt;
&lt;td&gt;由于IPv4发展初期的分配规划问 题，造成许多IPv4地址分配不连 续，不能有效聚合路由。日益庞大 的路由表耗用大量内存，对设备成 本和转发效率产生影响，这一问题 促使设备制造商不断升级其产品， 以提高路由寻址和转发性能。&lt;/td&gt;
&lt;td&gt;巨大的地址空间使得IPv6可以方 便的进行层次化网络部署。层次 化的网络结构可以方便的进行路 由聚合，提高了路由转发效率。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;端对端安全&lt;/td&gt;
&lt;td&gt;IPv4协议制定时并没有仔细针对安全性进行设计，因此固有的框架结构并不能支持端到端的安全。&lt;/td&gt;
&lt;td&gt;IPv6中，网络层支持IPSec的认证 和加密，支持端到端的安全。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QoS&lt;/td&gt;
&lt;td&gt;随着网络会议、网络电话、网络电 视迅速普及与使用，客户要求有更 好的QoS来保障这些音视频实时转 发。IPv4并没有专门的手段对QoS 进行支持。&lt;/td&gt;
&lt;td&gt;IPv6新增了流标记域，提供QoS 保证。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;支持移动特性&lt;/td&gt;
&lt;td&gt;随着Internet的发展，移动IPv4出现 了一些问题，比如:三角路由，源 地址过滤等。&lt;/td&gt;
&lt;td&gt;IPv6协议规定必须支持移动特 性。和移动IPv4相比，移动IPv6 使用邻居发现功能可直接实现外 地网络的发现并得到转交地址， 而不必使用外地代理。同时，利 用路由扩展头和目的地址扩展头 移动节点和对等节点之间可以直 接通信，解决了移动IPv4的三角 路由、源地址过滤问题，移动通 信处理效率更高且对应用层透 明。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;IPv6 地址&lt;/h2&gt;
&lt;h3&gt;表示方法&lt;/h3&gt;
&lt;p&gt;IPv6 总共有 128 位，通过分为8组，每组 16 位，由 4 个十六进制数表示。每组之间由冒号分隔。如：&lt;code&gt;FC00:0000:130F:0000:0000:09C0:876A:130B&lt;/code&gt;。为了方便书写，提供了一些压缩后的写法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以省略前缀0。所以这个地址还可以写成：&lt;code&gt;FC00:0:130F:0:0:9C0:876A:130B&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;地址中包含的连续两个或多个均为0的组，可以用双冒号&quot;::&quot;代替。所以进一步缩写成：&lt;code&gt;FC00:0:130F::9C0:876A:130B&lt;/code&gt;。不过需要注意的是，一个 IPv6 地址中只能有一个 &quot;::&quot;，因为如果有多个的话，就无法辨别出每个 &quot;::&quot; 代表几组 0。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;地址结构&lt;/h3&gt;
&lt;p&gt;类似 IPv4 的设计，一个 IPv6 地址也是由两部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;网络前缀：n 位，相当于 IPv4 的网络号。&lt;/li&gt;
&lt;li&gt;接口标识：128-n位，相当于 IPv4 地址中的主机号。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;地址分类&lt;/h3&gt;
&lt;p&gt;IPv6 地址分为单播地址，任播地址，组播地址。相比于 IPv4，取消了广播地址，以更丰富的组播地址代替，同时增加了任播地址。&lt;/p&gt;
&lt;h4&gt;单播地址&lt;/h4&gt;
&lt;p&gt;单播地址用来表示一个节点的一个网络接口的地址。有以下几种单播地址：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;未指定地址&lt;/td&gt;
&lt;td&gt;指 &lt;code&gt;::/128&lt;/code&gt;。该地址表示讴歌接口或者节点还没有 IP 地址。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;环回地址&lt;/td&gt;
&lt;td&gt;指&lt;code&gt;::1/128&lt;/code&gt;。与 IPv4 中的 &lt;code&gt;127.0.0.1&lt;/code&gt; 作用相同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;全球单播地址&lt;/td&gt;
&lt;td&gt;类似于 IPv4 中的单播地址。由 &lt;code&gt;全球路由前缀(Global routing prefix，至少48位)+子网ID(Subnet ID)+接口标识(Interface ID)&lt;/code&gt;组成。全球路由前缀由提供商指定给一个组织机构，因此也可以起到聚合路由的作用。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;链路本地地址&lt;/td&gt;
&lt;td&gt;链路本地地址是 IPv6 中的应用范围受限制的地址类型，只能在连接到同一本地链 路的节点之间使用。它使用了特定的本地链路前缀FE80::/10(最高10位值为 1111111010)，同时将接口标识添加在后面作为地址的低64比特。当一个节点启动IPv6协议栈时，启动时节点的每个接口会自动配置一个链路本地 地址(其固定的前缀+EUI-64规则形成的接口标识)。在 IPv4 中，链路本地地址为 169.254.0.0/16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;唯一本地地址&lt;/td&gt;
&lt;td&gt;唯一本地地址是另一种应用范围受限的地址，它仅能在一个站点内使用。由于本地站点地址的废除(RFC3879)，唯一本地地址被用来代替本地站点地址。唯一本地地址的作用类似于IPv4中的私网地址，任何没有申请到提供商分配的全 球单播地址的组织机构都可以使用唯一本地地址。唯一本地地址只能在本地网络内部被路由转发而不会在全球网络中被路由转发。唯一本地地址的固定前缀为&lt;code&gt;FC00::/7&lt;/code&gt;,二进制表示为 &lt;code&gt;1111 110&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;任播地址&lt;/h4&gt;
&lt;p&gt;任播地址一般用来表示一组节点上的接口，当数据包发往任播地址时，中间路由设备会将数据包发往最近的一个节点上的接口。所以可以看出，任播地址是被设计用来给多个主机或者节点提供相同服务时提供冗余功能和负载均衡功能的。不过目前实际应用中，任播地址只能分配给路由设备，并不能应用于主机等设备。并且任播地址不能作为 IPv6 报文的源地址。 任播地址并没有单独的地址空间，和单播地址使用相同的地址空间。&lt;/p&gt;
&lt;h4&gt;组播地址&lt;/h4&gt;
&lt;p&gt;IPv6的组播与IPv4相同，用来标识一组接口，一般这些接口属于不同的节点。一个节点 可能属于0到多个组播组。发往组播地址的报文被组播地址标识的所有接口接收。例如 组播地址FF02::1表示链路本地范围的所有节点，组播地址FF02::2表示链路本地范围的 所有路由器。 一个IPv6组播地址由前缀，标志(Flag)字段、范围(Scope)字段以及组播组ID (Global ID)4个部分组成:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;前缀:IPv6组播地址的前缀是FF00::/8。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标志字段(Flag):长度4bit，目前只使用了最后一个比特(前三位必须置0)， 当该位值为0时，表示当前的组播地址是由IANA所分配的一个永久分配地址;当 该值为1时，表示当前的组播地址是一个临时组播地址(非永久分配地址)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;范围字段&lt;/strong&gt;(Scope):长度4bit，用来限制组播数据流在网络中发送的范围，该字 段取值和含义的对应关系如图&lt;strong&gt;1-5&lt;/strong&gt;所示。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;组播组ID(Group ID):长度112bit，用以标识组播组。目前，RFC2373并没有将 所有的112位都定义成组标识，而是建议仅使用该112位的最低32位作为组播组 ID，将剩余的80位都置0。这样每个组播组ID都映射到一个唯一的以太网组播 MAC地址(RFC2464)。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2022/02/image-20220220175332438.png&quot; alt=&quot;image-20220220175332438&quot; /&gt; 被请求节点组播地址通过节点的单播或任播地址生成。当一个节点具有了单播或任播地址，就会对应生成一个被请求节点组播地址，并且加入这个组播组。一个单播地址或任播地址对应一个被请求节点组播地址。该地址主要用于邻居发现机制和地址重复检测功能。 IPv6中没有广播地址，也不使用ARP。但是仍然需要从IP地址解析到MAC地址的 功能。在IPv6中，这个功能通过邻居请求NS(Neighbor Solicitation)报文完成。 当一个节点需要解析某个IPv6地址对应的MAC地址时，会发送NS报文，该报文目的IP就是需要解析的IPv6地址对应的被请求节点组播地址;只有具有该组播地 址的节点会检查处理。 被请求节点组播地址由前缀FF02::1:FF00:0/104和单播地址的最后24位组成。&lt;/p&gt;
&lt;h2&gt;IPv6 报文格式&lt;/h2&gt;
&lt;p&gt;IPv6 除了在大小上做了变动，也针对 IPv4 报文格式在实际应用场景中的设计不合理之处做了优化。IPv6 报文主要由三部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IPv6 基本报头：8个字段，固定为 40 字节。&lt;/li&gt;
&lt;li&gt;IPv6 扩展报头：扩展报头是链式结构的，理论上可无限扩展&lt;/li&gt;
&lt;li&gt;上层协议数据单元：一般由上层协议报头和它的有效载荷构成，有效载荷可以是一个 ICMPv6 报文、一个 TCP 报文或一个 UDP 报文。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个 IPv6 的基本报头格式如下： &lt;img src=&quot;/uploads/wp/2022/02/image-20220220193727771.png&quot; alt=&quot;image-20220220193727771&quot; /&gt; 这些字段的解释如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Version:版本号，长度为4bit。对于IPv6，该值为6。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Traffic Class:流类别，长度为8bit。等同于IPv4中的TOS字段，表示IPv6数据报的 类或优先级，主要应用于QoS。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Flow Label:流标签，长度为20bit。IPv6中的新增字段，用于区分实时流量，不同 的流标签+源地址可以唯一确定一条数据流，中间网络设备可以根据这些信息更加 高效率的区分数据流。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Payload Length:有效载荷长度，长度为16bit。有效载荷是指紧跟IPv6报头的数据 报的其它部分(即扩展报头和上层协议数据单元)。该字段只能表示最大长度为 65535字节的有效载荷。如果有效载荷的长度超过这个值，该字段会置0，而有效 载荷的长度用逐跳选项扩展报头中的超大有效载荷选项来表示。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Next Header:下一个报头，长度为8bit。该字段定义紧跟在IPv6报头后面的第一个 扩展报头(如果存在)的类型，或者上层协议数据单元中的协议类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hop Limit:跳数限制，长度为8bit。该字段类似于IPv4中的Time to Live字段，它 定义了IP数据报所能经过的最大跳数。每经过一个设备，该数值减去1，当该字段 的值为0时，数据报将被丢弃。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Source Address:源地址，长度为128bit。表示发送方的地址。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Destination Address:目的地址，长度为128bit。表示接收方的地址。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过上述描述可以知道，IPv6 的基本报头相比于 IPv4 的报头做了简化，去除了IHL、identifiers、Flags、Fragment Offset、Header Checksum、 Options、Paddiing域，只增了流标签域。这样的设计可以提升路由设备对数据的处理性能。 在IPv4中，IPv4报头包含可选字段Options，内容涉及security、Timestamp、Record route 等，这些Options可以将IPv4报头长度从20字节扩充到60字节。在转发过程中，处理携带这些Options的IPv4报文会占用设备很大的资源，因此实际中也很少使用。 IPv6将这些Options从IPv6基本报头中剥离，放到了扩展报头中，扩展报头被置于IPv6 报头和上层协议数据单元之间。一个IPv6报文可以包含0个、1个或多个扩展报头，仅 当需要设备或目的节点做某些特殊处理时，才由发送方添加一个或多个扩展头。与 IPv4不同，IPv6扩展头长度任意，不受40字节限制，这样便于日后扩充新增选项，这一特征加上选项的处理方式使得IPv6选项能得以真正的利用。但是为了提高处理选项头 和传输层协议的性能，扩展报头总是8字节长度的整数倍。 当使用多个扩展报头时，前面报头的Next Header字段指明下一个扩展报头的类型，这 样就形成了链状的报头列表。目前，RFC 2460中定义了6个IPv6扩展头:逐跳选项报头、目的选项报头、路由报头、分段报头、认证报头、封装安全净载报头。 &lt;img src=&quot;/uploads/wp/2022/02/image-20220220194519027.png&quot; alt=&quot;image-20220220194519027&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;ICMPv6&lt;/h2&gt;
&lt;p&gt;ICMPv6(Internet Control Message Protocol for the IPv6)是IPv6的基础协议之一。 在IPv4中，Internet控制报文协议ICMP(Internet Control Message Protocol)向源节点报 告关于向目的地传输IP数据包过程中的错误和信息。它为诊断、信息和管理目的定义 了一些消息，如:目的不可达、数据包超长、超时、回应请求和回应应答等。在IPv6 中，ICMPv6除了提供ICMPv4常用的功能之外，还是其它一些功能的基础，如邻接点 发现、无状态地址配置(包括重复地址检测)、PMTU发现等。 ICMPv6的协议类型号(即IPv6报文中的Next Header字段的值)为58。 &lt;img src=&quot;/uploads/wp/2022/02/image-20220220201051653.png&quot; alt=&quot;image-20220220201051653&quot; /&gt; 报文中字段解释如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Type:表明消息的类型，0至127表示差错报文类型，128至255表示消息报文类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Code:表示此消息类型细分的类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Checksum:表示ICMPv6报文的校验和。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;邻居发现&lt;/h2&gt;
&lt;p&gt;邻居发现协议NDP(Neighbor Discovery Protocol)是IPv6协议体系中一个重要的基础协 议。邻居发现协议替代了IPv4的ARP(Address Resolution Protocol)和ICMP路由器发现 (Router Discovery)，它定义了使用ICMPv6报文实现地址解析，跟踪邻居状态，重复 地址检测，路由器发现以及重定向等功能。&lt;/p&gt;
&lt;h3&gt;地址解析&lt;/h3&gt;
&lt;p&gt;邻居发现协议 NDP(Neighbor Discovery Protocol) 是基于 ICMPv6 的一个三层协议，用来取代 IPv4 的 ARP 协议。其以太网协议类型为 0x86DD。地址解析过程中使用了两种 ICMPv6 报文：邻居请求报文 NS(Neighbor Solicitation) 和邻居通告报文 NA(Neighbor Advertisement)。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NS 报文：Type 字段值为 135，Code 字段值为 0，在地址解析中的作用类似于 IPv4 中的 ARP 请求报文。&lt;/li&gt;
&lt;li&gt;NA 报文：Type 字段值为 136，Code 字段值为0，在地址解析中的作用类似于 IPv4 中的 ARP 响应报文。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2022/02/image-20220220202534018.png&quot; alt=&quot;image-20220220202534018&quot; /&gt; Host A在向Host B发送报文之前它必须要解析出Host B的链路层地址，所以首先Host A 会发送一个NS报文，其中源地址为Host A的IPv6地址，目的地址为Host B的被请求节 点组播地址，需要解析的目标IP为Host B的IPv6地址，这就表示Host A想要知道Host B 的链路层地址。同时需要指出的是，在NS报文的Options字段中还携带了Host A的链路 层地址。 当Host B接收到了NS报文之后，就会回应NA报文，其中源地址为Host B的IPv6地址， 目的地址为Host A的IPv6地址(使用NS报文中的Host A的链路层地址进行单播)，Host B的链路层地址被放在Options字段中。这样就完成了一个地址解析的过程。&lt;/p&gt;
&lt;h3&gt;跟踪邻居状态&lt;/h3&gt;
&lt;p&gt;通过邻居或到达邻居的通信，会因各种原因而中断，包括硬件故障、接口卡的热插入 等。如果目的地失效，则恢复是不可能的，通信失败;如果路径失效，则恢复是可能 的。 因此节点需要维护一张邻居表，每个邻居都有相应的状态，状态之间可以迁移。 RFC2461中定义了5种邻居状态，分别是:未完成(Incomplete)、可达 (Reachable)、陈旧(Stale)、延迟(Delay)、探查(Probe) &lt;img src=&quot;/uploads/wp/2022/02/image-20220220202903445.png&quot; alt=&quot;image-20220220202903445&quot; /&gt; 下面以A、B两个邻居节点之间相互通信过程中A节点的邻居状态变化为例(假设A、B 之前从未通信)，说明邻居状态迁移的过程。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A先发送NS报文，并生成缓存条目，此时，邻居状态为Incomplete。&lt;/li&gt;
&lt;li&gt;若B回复NA报文，则邻居状态由Incomplete变为Reachable，否则固定时间后邻居状态由Incomplete变为Empty，即删除表项。&lt;/li&gt;
&lt;li&gt;经过邻居可达时间，邻居状态由Reachable变为Stale，即不确定邻居节点的可达性。&lt;/li&gt;
&lt;li&gt;如果在Reachable状态，A收到B的非请求NA报文，且报文中携带的B的链路层地址和表项中不同，则邻居状态马上变为Stale。&lt;/li&gt;
&lt;li&gt;在STALE状态到达老化时间后进入Delay状态。&lt;/li&gt;
&lt;li&gt;在经过一段固定时间(5秒)后，邻居状态由Delay变为Probe，其间若有NA应答， 则邻居状态由Delay变为Reachable。&lt;/li&gt;
&lt;li&gt;在Probe状态，A每隔一定时间间隔(1秒)发送单播NS，发送固定次数(3次) 后，有应答则邻居状态变为Reachable，否则邻居状态变为Empty，即删除表项。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;重复地址检测&lt;/h3&gt;
&lt;p&gt;重复地址检测DAD(Duplicate Address Detect)是在接口使用某个IPv6单播地址之前进 行的，主要是为了探测是否有其它的节点使用了该地址。尤其是在地址自动配置的时 候，进行DAD检测是很必要的。 一个IPv6单播地址在分配给一个接口之后且通过重复 地址检测之前称为试验地址(Tentative Address)。此时该接口不能使用这个试验地址 进行单播通信，但是仍然会加入两个组播组:ALL-NODES组播组和试验地址所对应的 Solicited-Node组播组。 IPv6重复地址检测技术和IPv4中的免费ARP类似:节点向试验地址所对应的Solicited- Node组播组发送NS报文。NS报文中目标地址即为该试验地址。如果收到某个其他站点 回应的NA报文，就证明该地址已被网络上使用，节点将不能使用该试验地址通讯。 &lt;img src=&quot;/uploads/wp/2022/02/image-20220220203031376.png&quot; alt=&quot;image-20220220203031376&quot; /&gt; Host A的IPv6地址FC00::1为新配置地址，即FC00::1为Host A的试验地址。Host A向 FC00::1的Solicited-Node组播组发送一个以FC00::1为请求的目标地址的NS报文进行重 复地址检测，由于FC00::1并未正式指定，所以NS报文的源地址为未指定地址。当Host B收到该NS报文后，有两种处理方法:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果Host B发现FC00::1是自身的一个试验地址，则Host B放弃使用这个地址作为 接口地址，并且不会发送NA报文。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果Host B发现FC00::1是一个已经正常使用的地址，Host B会向FF02::1发送一个 NA报文，该消息中会包含FC00::1。这样，Host A收到这个消息后就会发现自身的 试验地址是重复的。Host A上该试验地址不生效，被标识为duplicated状态。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;路由器发现&lt;/h3&gt;
&lt;p&gt;路由器发现功能用来发现与本地链路相连的设备，并获取与地址自动配置相关的前缀和其他配置参数。 在IPv6中，IPv6地址可以支持无状态的自动配置，即主机通过某种机制获取网络前缀信 息，然后主机自己生成地址的接口标识部分。路由器发现功能是IPv6地址自动配置功 能的基础，主要通过以下两种报文实现:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;路由器通告RA(Router Advertisement)报文:每台设备为了让二层网络上的主机 和设备知道自己的存在，定时都会组播发送RA报文，RA报文中会带有网络前缀 信息，及其他一些标志位信息。RA报文的Type字段值为134。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;路由器请求RS(Router Solicitation)报文:很多情况下主机接入网络后希望尽快 获取网络前缀进行通信，此时主机可以立刻发送RS报文，网络上的设备将回应RA 报文。RS报文的Tpye字段值为133。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;重定向&lt;/h3&gt;
&lt;p&gt;当网关设备发现报文从其它网关设备转发更好，它就会发送重定向报文告知报文的发 送者，让报文发送者选择另一个网关设备。重定向报文也承载在ICMPv6报文中，其 Type字段值为137，报文中会携带更好的路径下一跳地址和需要重定向转发的报文的目 的地址等信息。 &lt;img src=&quot;/uploads/wp/2022/02/image-20220220203223504.png&quot; alt=&quot;image-20220220203223504&quot; /&gt; Host A需要和Host B通信，Host A的默认网关设备是Switch A，当Host A发送报文给 Host B时报文会被送到Switch A。Switch A接收到Host A发送的报文以后会发现实际上 Host A直接发送给Switch B更好，它将发送一个重定向报文给主机A，其中报文中更好 的路径下一跳地址为Switch B，Destination Address为Host B。Host A接收到了重定向报 文之后，会在默认路由表中添加一个主机路由，以后发往Host B的报文就直接发送给 Switch B。 当设备收到一个报文后，只有在如下情况下，设备会向报文发送者发送重定向报文:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;报文的目的地址不是一个组播地址。&lt;/li&gt;
&lt;li&gt;报文并非通过路由转发给设备。&lt;/li&gt;
&lt;li&gt;经过路由计算后，路由的下一跳出接口是接收报文的接口。&lt;/li&gt;
&lt;li&gt;设备发现报文的最佳下一跳IP地址和报文的源IP地址处于同一网段。&lt;/li&gt;
&lt;li&gt;设备检查报文的源地址，发现自身的邻居表项中有用该地址作为全球单播地址或链路本地地址的邻居存在。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Path MTU&lt;/h2&gt;
&lt;p&gt;在IPv4中，报文如果过大，必须要分片进行发送，所以在每个节点发送报文之前，设备都会根据发送接口的最大传输单元MTU(Maximum Transmission Unit)来对报文进 行分片。但是在IPv6中，为了减少中间转发设备的处理压力，中间转发设备不对IPv6报文进行分片，报文的分片将在源节点进行。当中间转发设备的接口收到一个报文后， 如果发现报文长度比转发接口的MTU值大，则会将其丢弃;同时将转发接口的MTU值 通过ICMPv6报文的“Packet Too Big”消息发给源端主机，源端主机以该值重新发送 IPv6报文，这样带来了额外流量开销。PMTU发现协议可以动态发现整条传输路径上各 链路的MTU值，减少由于重传带来的额外流量开销。 PMTU协议是通过ICMPv6的Packet Too Big报文来完成的。首先源节点假设PMTU就是 其出接口的MTU，发出一个试探性的报文，当转发路径上存在一个小于当前假设的 PMTU时，转发设备就会向源节点发送Packet Too Big报文，并且携带自己的MTU值， 此后源节点将PMTU的假设值更改为新收到的MTU值继续发送报文。如此反复，直到 报文到达目的地之后，源节点就能知道到达目的地的PMTU了。 &lt;img src=&quot;/uploads/wp/2022/02/image-20220220203415347.png&quot; alt=&quot;image-20220220203415347&quot; /&gt; 整条传输路径需要通过4条链路，每条链路的MTU分别是1500、1500、1400、1300，当 源节点发送一个分片报文的时候，首先按照PMTU为1500进行分片并发送分片报文，当 到达MTU为1400的出接口时，设备返回Packet Too Big错误，同时携带MTU值为1400的 信息。源节点接收到之后会将报文重新按照PMTU为1400进行分片并再次发送一个分片 报文，当分片报文到达MTU值为1300的出接口时，同样返回Packet Too Big错误，携带 MTU值为1300的信息。之后源节点重新按照PMTU为1300进行分片并发送分片报文， 最终到达目的地，这样就找到了该路径的PMTU。&lt;/p&gt;
&lt;h2&gt;Linux IPv6&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;条目&lt;/th&gt;
&lt;th&gt;ipv4&lt;/th&gt;
&lt;th&gt;ipv6&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sysctl 配置项&lt;/td&gt;
&lt;td&gt;net.ipv4.conf&lt;/td&gt;
&lt;td&gt;net.ipv6.conf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ip 地址&lt;/td&gt;
&lt;td&gt;通过 &lt;code&gt;ip a&lt;/code&gt;查看时可以看到 inet 后面的就是 ipv4 地址。&lt;/td&gt;
&lt;td&gt;通过 &lt;code&gt;ip a&lt;/code&gt;查看时可以看到 inet6 后面的就是 ipv6 地址。一般会有多个，scope global 的是全局唯一单播地址或唯一本地地址(fc或fd开头)，scope link 是链路本地地址(fe80 开头)。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;抓包&lt;/td&gt;
&lt;td&gt;tcpdump icmp/ tcpdump ip&lt;/td&gt;
&lt;td&gt;tcpdump icmp6 / tcpdump ip6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ping&lt;/td&gt;
&lt;td&gt;ping&lt;/td&gt;
&lt;td&gt;ping6 或 ping -6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;traceroute6&lt;/td&gt;
&lt;td&gt;traceroute&lt;/td&gt;
&lt;td&gt;traceroute6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;邻居地址解析&lt;/td&gt;
&lt;td&gt;arping&lt;/td&gt;
&lt;td&gt;ndisc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;路由表&lt;/td&gt;
&lt;td&gt;ip r&lt;/td&gt;
&lt;td&gt;ip -6 r&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;邻居地址表&lt;/td&gt;
&lt;td&gt;ip neigh 或 arp -n&lt;/td&gt;
&lt;td&gt;ip -6 neigh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS 解析&lt;/td&gt;
&lt;td&gt;dig&lt;/td&gt;
&lt;td&gt;dig -6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Kubernetes 的 IPv4/IPv6 双栈&lt;/h2&gt;
&lt;p&gt;IPv4/IPv6 双栈是由 IPv4 向 IPv6 过渡阶段的一种解决方案，双栈即一个网络接口同时拥有 IPv4 和 IPv6 的地址，这样在和远端通信时，如果远端支持 IPv6，就使用 IPv6 进行通信，否则也可以使用 IPv4 进行通信。Kubernetes 在 1.20 后开始支持双栈。当然，除了对 Kubernetes 版本有要求外，CNI 插件也必须支持双栈才行。 要在 Kubernetes 中开启双栈，需要做以下配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;kube-apiserver:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--service-cluster-ip-range=&amp;lt;IPv4 CIDR&amp;gt;,&amp;lt;IPv6 CIDR&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;kube-controller-manager:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--cluster-cidr=&amp;lt;IPv4 CIDR&amp;gt;,&amp;lt;IPv6 CIDR&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--service-cluster-ip-range=&amp;lt;IPv4 CIDR&amp;gt;,&amp;lt;IPv6 CIDR&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--node-cidr-mask-size-ipv4|--node-cidr-mask-size-ipv6&lt;/code&gt; 对于 IPv4 默认为 /24，对于 IPv6 默认为 /64&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;kube-proxy:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--cluster-cidr=&amp;lt;IPv4 CIDR&amp;gt;,&amp;lt;IPv6 CIDR&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;IPv6 地址速查&lt;/h2&gt;
&lt;p&gt;平常接触 IPv4 地址较多，因此一眼就大概知道某个地址代表什么含义，但是 IPv6 中往往比较难分辨，这里提供一个表格供对照参考。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;地址类型&lt;/th&gt;
&lt;th&gt;IPv4&lt;/th&gt;
&lt;th&gt;IPv6&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;环回地址&lt;/td&gt;
&lt;td&gt;127.0.0.1&lt;/td&gt;
&lt;td&gt;::1/128&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;私网地址&lt;/td&gt;
&lt;td&gt;10.0.0.0 – 10.255.255.255， 172.16.0.0 – 172.31.255.255，192.168.0.0 – 192.168.255.255&lt;/td&gt;
&lt;td&gt;前缀FC00::/7（1111 110），范围：FC~FD。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;链路本地地址&lt;/td&gt;
&lt;td&gt;169.254.0.0/16&lt;/td&gt;
&lt;td&gt;fe80::/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;组播地址&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;被请求节点组播地址由前缀FF02::1:FF00:0/104和单播地址的最后24位组成。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;广播地址&lt;/td&gt;
&lt;td&gt;广播地址使用该网络范围内的最大地址。 即主机部分的各比特位全部为 1 的地址。在网络 10.1.1.0/24 中，其广播地址是 10.1.1.255。&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;华为 《IPv6 技术白皮书》。本文大多数内容都是参考或摘抄自该白皮书。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>网络协议</category><author>joyme123</author></item><item><title>机械硬盘的性能评估</title><link>https://www.myway5.com/blog/hdd-performance/</link><guid isPermaLink="true">https://www.myway5.com/blog/hdd-performance/</guid><description>从个人 PC 到数据中心，机械硬盘都扮演着不可或缺的角色。从性能、存储容量等方面来考虑，机械硬盘一直都是一个不错的选择。因此，了解机械硬盘的性能评估方式也很有必要。 机械硬盘的组成结构</description><pubDate>Fri, 11 Feb 2022 14:17:38 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;从个人 PC 到数据中心，机械硬盘都扮演着不可或缺的角色。从性能、存储容量等方面来考虑，机械硬盘一直都是一个不错的选择。因此，了解机械硬盘的性能评估方式也很有必要。&lt;/p&gt;
&lt;h2&gt;机械硬盘的组成结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2022/02/screenshot-20220209-233208.png&quot; alt=&quot;&quot; /&gt; 从物理视角来看，主要组件如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;盘片(Platter): 一个机械硬盘一般由多个盘片组成，每个盘片都有两面，每一面都可以存储数据。&lt;/li&gt;
&lt;li&gt;转轴(Spindle): 转轴会连接到一个电机上，驱动盘片的转动。常见的转速有：5400 rpm, 7200 rpm, 10000 rpm 和 15000 rpm。&lt;/li&gt;
&lt;li&gt;读写磁头(Read/Write Head): 每个盘片的面都有一个对应的读写磁头，负责在该盘面上进行读写。&lt;/li&gt;
&lt;li&gt;机械臂杆(Actuator Arm): 磁头连接到机械臂杆上，所有磁头在不同的磁道上移动时是同步的。&lt;/li&gt;
&lt;li&gt;驱动控制主板：上面包括了微信处理器，内存，电路以及一些固件。这些固件负责控制转轴电机的电源，电机的速度。同时也控制了硬盘和主机的通信。此外，通过移动磁头，以及在不同磁头间的切换来控制硬盘的读写操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从逻辑视角来看：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;磁道：盘片的每一面上都有多个磁道，每个磁道都是一个同心圆。&lt;/li&gt;
&lt;li&gt;扇区：每个磁道被划分成多个扇区。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;详细信息可参考：&lt;a href=&quot;https://www.jianshu.com/p/cf100e39ccdf&quot; rel=&quot;noopener&quot;&gt;https://www.jianshu.com/p/cf100e39ccdf&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;性能评估维度&lt;/h2&gt;
&lt;h3&gt;寻道时间(Seek Time)&lt;/h3&gt;
&lt;p&gt;机械硬盘在读写数据时，首先需要将磁头移动到指定的磁道上。这个时间为 Seek Time。一般情况下，机械硬盘的厂商会提供以下几个场景的 Seek Time 参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Full Stroke:&lt;/strong&gt; 这个时间用来描述磁头从最里面的磁道移动到最外面的磁道所需要的时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Average:&lt;/strong&gt; 从随机的磁道移动到另一个磁道所需的时间。一般是 1/3 的 Full Stroke 时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Track-to-Track:&lt;/strong&gt; 在相邻的磁道之间移动磁头所需要的时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在的机械硬盘 Average 时间一般在 3~15ms 左右。&lt;/p&gt;
&lt;h3&gt;旋转延迟(Rotational Latency)&lt;/h3&gt;
&lt;p&gt;在将磁头移动到指定磁道后，还需要转动磁盘盘片，将磁头指到特定的扇区以供读写。这个时间为 Rotational Latency，和硬盘的转速紧密相关。一般情况下，Average Rotational Latency 为 Full Rotational Latency 的一半。 以 5400 rpm 转速的硬盘为例，每分钟转动 5400 转，即 Full Rotational Latency 为 60*1000/5400 = 11.11ms。那么 Average Rotational Latency 就是 5.5 ms 左右。&lt;/p&gt;
&lt;h3&gt;数据传输速率(Data Transfer Rate)&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2022/02/image-20220211001141721.png&quot; alt=&quot;image-20220211001141721&quot; /&gt; 如上图所示，机械硬盘的数据传输速率有两个检测点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;外部数据传输速率(External transfer rate): 这个是从硬盘外写入到硬盘内 Buffer 区域的速度。&lt;/li&gt;
&lt;li&gt;内部数据传输速率(Internal transfer rate): 这个是从硬盘内 Buffer 通过磁头写入到盘片中的速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一般来说，External transfer rate 都要远大于 Internal transfer rate。&lt;/p&gt;
&lt;h2&gt;IOPS&lt;/h2&gt;
&lt;p&gt;从上面总结到的三个维度，我们可以知道，一次 I/O 的时间为： T(s) = T + L + X 其中，T 为平均的寻道时间，L 为平均旋转延迟，X 为数据传输时间。对于一块 7200rpm，平均寻道时间为 5ms，内部数据传输速率为 40MB/s 的机械硬盘来说。每次大小为 32KB 的 I/O 需要的时间为： T(s) = 5ms + (60*1000ms/7200)/2 + 32KB/40MB*1000ms = 5ms + 4.17ms + 0.78ms = 9.95 ms IOPS 描述的是每秒的 I/O 次数，那么可以得出该硬盘的 IOPS 为：1000/9.95 = 100.5 IOPS。&lt;/p&gt;
&lt;h2&gt;硬盘 I/O 控制器的利用率&lt;/h2&gt;
&lt;p&gt;除了上述硬盘本身的性能参数，我们还可以从实际使用时磁盘 I/O 控制器的利用率来评估。我们可以将硬盘当作黑盒，只有以下两个组件构成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;队列：在 I/O 请求被处理之前，都被存放在队列中等待。&lt;/li&gt;
&lt;li&gt;硬盘 I/O 控制器：控制器负责从队列中取出 I/O 请求并处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2022/02/screenshot-20220211-220424.png&quot; alt=&quot;disk-io-rate&quot; /&gt; 如上图所示，应用产生的 I/O 请求先到达 I/O 队列中，由 I/O 控制器取出并处理。如果该队列的长度持续增加，那么每个 I/O 的平均响应时间也是持续增加的。可以得出： 平均响应时间 = T(s)/(1-利用率)。 根据该公式可以得出下图： &lt;img src=&quot;/uploads/wp/2022/02/screenshot-20220211-221211.png&quot; alt=&quot;graph&quot; /&gt; 平均响应时间的增长并不是线性的，当利用率越高，增长会越快。整个增长的拐点大概在 70% 处。所以一般情况下，我们要保证我们的应用使用的磁盘利用率在 70% 左右，才能保证一个较好的性能。&lt;/p&gt;
</content:encoded><category>存储</category><author>joyme123</author></item><item><title>calico IPIP 分析</title><link>https://www.myway5.com/blog/calico-ipip/</link><guid isPermaLink="true">https://www.myway5.com/blog/calico-ipip/</guid><description>当集群中所有的主机都在同一个二层时，calico cni 可以仅靠路由，使得所有的 Pod 网络互通。但是纯二层的环境在很多场景下都不一定能满足，因此当主机之间仅3层互通时，就可以使用 calico IPIP(全称 IP in IP) 模式。</description><pubDate>Tue, 20 Jul 2021 13:42:55 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;当集群中所有的主机都在同一个二层时，calico cni 可以仅靠路由，使得所有的 Pod 网络互通。但是纯二层的环境在很多场景下都不一定能满足，因此当主机之间仅3层互通时，就可以使用 calico IPIP(全称 IP in IP) 模式。 IP in IP 是一种 IP 隧道协议，其核心技术点就是发送方将一个 IP 数据包封装到另一个 IP 数据包之中发送，接受方收到后，从外层 IP 数据包中解析出内部的 IP 数据包进行处理。常用在 VPN 等技术中，用来打通两个内网环境。&lt;/p&gt;
&lt;h2&gt;calico IPIP 流量分析&lt;/h2&gt;
&lt;p&gt;之前的文章&lt;a href=&quot;https://www.myway5.com/index.php/2021/07/19/proxy-arp-in-calico/&quot; rel=&quot;noopener&quot;&gt;proxy_arp在calico中的妙用&lt;/a&gt;简单讲了 calico 是如何通过路由打通不同主机上的 Pod 网络的，其实这个方案有一个前提，就是不同的主机之间需要二层互通。当网络环境满足不了时，就可以通过使用路由 + IPIP 的方式来打通网络。 这里可以通过一个简单的实验来验证一下该方案。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# node.sh
ip netns add n1
ip link add veth1 type veth peer name veth2
ip link set veth2 netns n1
ip netns exec n1 ip link set veth2 up
ip netns exec n1 ip route add 169.254.1.1 dev veth2 scope link
ip netns exec n1 ip route add default via 169.254.1.1
ip netns exec n1 ip addr add 172.19.1.10/24 dev veth2
ip link set veth1 up
ip route add 172.19.1.10 dev veth1 # 这个路由必须有
ip netns exec n1 ip route del 172.19.1.0/24 dev veth2 proto kernel scope link src 172.19.1.10
echo 1 &amp;gt; /proc/sys/net/ipv4/conf/veth1/proxy_arp
echo 1 &amp;gt; /proc/sys/net/ipv4/ip_forward
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的脚本是用来创建一个虚拟的 Pod 的，可以在不同的主机上执行一下，这里要记得修改一下 IP 地址，来保证两个 Pod 的 IP 不同。 之后在宿主机上创建 IP 隧道。也是两台主机都要执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ip tunnel add mode ipip
ip link set tunl0 up
ip route add 172.19.1.0/24 via 192.168.105.135 dev tunl0 proto bird onlink
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里在创建 IP 隧道时，并没有指定隧道对端的地址，因为在实际的集群中，1对1的隧道是没使用场景的。而是使用路由告诉这个隧道的对端地址。这时候在 netns n1 内就可以 ping 通对端的 IP 了。 流程图如下 &lt;img src=&quot;/uploads/wp/2021/07/calico-ipip.jpg&quot; alt=&quot;calico ipip&quot; /&gt;&lt;/p&gt;
</content:encoded><category>linux</category><category>k8s</category><category>网络</category><author>joyme123</author></item><item><title>proxy_arp在calico中的妙用</title><link>https://www.myway5.com/blog/proxy-arp-in-calico/</link><guid isPermaLink="true">https://www.myway5.com/blog/proxy-arp-in-calico/</guid><description>proxy\arp 是网卡的一个配置，在开启后，该网卡会使用自己的 MAC 地址应答非自身 IP 的 ARP Request。常见的用途就是当两台主机的 IP 在同一个网段内，二层却不通，就可以使用额外的一台主机作为 proxy，将这台主机的网卡开启 proxy\arp，来作为…</description><pubDate>Mon, 19 Jul 2021 15:39:34 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;proxy_arp 是网卡的一个配置，在开启后，该网卡会使用自己的 MAC 地址应答非自身 IP 的 ARP Request。常见的用途就是当两台主机的 IP 在同一个网段内，二层却不通，就可以使用额外的一台主机作为 proxy，将这台主机的网卡开启 proxy_arp，来作为中间代理打通网络。如下图所示： &lt;img src=&quot;/uploads/wp/2021/07/ether-arp-proxy.png&quot; alt=&quot;img&quot; /&gt; 开启网卡的 proxy_arp 也很简单：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo 1 &amp;gt; /proc/sys/net/ipv4/conf/veth1/proxy_arp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;calico 是一个使用路由方案打通网络的网络插件，在作为 k8s cni 时，其也使用了 proxy_arp，作为打通路由的一个环节。在了解 calico 如何使用 proxy_arp 之前，我们先看一下 flannel 的 host-gw 是如何使用路由打通 pod 网络的。&lt;/p&gt;
&lt;h2&gt;flannel host-gw 路由方案&lt;/h2&gt;
&lt;p&gt;两台二层互通的主机上的 pod，如果要通过路由来互相访问，常见的方式是类似于 flannel 的 host-gw 模式。其流量路径如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每台主机上都有一个 bridge，pod 通过 veth pair 接入到 bridge 上。&lt;/li&gt;
&lt;li&gt;pod 将 bridge 的 ip 作为网关。这样 pod 访问其他网段的 IP 时，流量就会到达 bridge 上。&lt;/li&gt;
&lt;li&gt;流量到达 bridge 后，就可以根据宿主机上的路由表转发到对端主机。&lt;/li&gt;
&lt;li&gt;对端主机也会根据路由表，将流量从 bridge 转发到 pod 内。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2020/10/flannel-hostgw.png&quot; alt=&quot;flannel-host-gw&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;calico 的路由方案&lt;/h2&gt;
&lt;p&gt;相比于 flannel host-gw 模式，calico 采用了更巧妙的方法，省掉了 bridge。 其 veth pair 的一端在 Pod 内，设置为 pod 的 IP，另一端在宿主机中，没有设置 IP，也没有接入 bridge，但是设置了 proxy_arp=1。 pod 内有以下的路由表：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;default via 169.254.1.1 dev veth2 
169.254.1.1 dev veth2 scope link 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;169.254.0.0/16 是一个特殊的 IP 段，只会在主机内出现。不过这里这个 IP 并不重要，只是为了防止冲突才选择了这个特殊值。当 Pod 要访问其他 IP 时，如果该 IP 在同一个网段，那就需要获取该 IP 的 MAC 地址。如果不在一个网段，那么根据路由表，就要获取网关的 IP 地址。所以无论如何，arp 请求都会到达下图中的 veth1。 因为 veth1 设置了 proxy_arp=1，所以就会返回自己的 MAC 地址，然后 Pod 的流量就发到了主机的网络协议栈。到达网络协议栈之后，就和 flannel host-gw 一样，被转发到对端的主机上。 流量到达对端主机后，和 flannel host-gw 不一样的是，主机上直接设置了 pod 的路由：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;172.19.2.10 dev veth1 scope link
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是直接从 veth1 发到 pod 内。 &lt;img src=&quot;/uploads/wp/2021/07/proxy_arp.jpg&quot; alt=&quot;proxy_arp&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://linux-ip.net/html/ether-arp-proxy.html&quot; rel=&quot;noopener&quot;&gt;2.2. Proxy ARP&lt;/a&gt; &lt;a href=&quot;https://cloud.tencent.com/developer/article/1495301&quot; rel=&quot;noopener&quot;&gt;戳穿 Calico 的谎言&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>linux</category><category>k8s</category><category>网络</category><author>joyme123</author></item><item><title>linux 网络数据包接收流程（一）</title><link>https://www.myway5.com/blog/linux-network-packet-receive-1/</link><guid isPermaLink="true">https://www.myway5.com/blog/linux-network-packet-receive-1/</guid><description>Linux 作为最流行的服务器操作系统，其提供的网络能力也是经过了各种各样场景的考验。因此如果经常和 linux server 打交道的话，了解 linux 的数据包处理流程也是很有必要的。</description><pubDate>Thu, 15 Jul 2021 12:34:58 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;Linux 作为最流行的服务器操作系统，其提供的网络能力也是经过了各种各样场景的考验。因此如果经常和 linux server 打交道的话，了解 linux 的数据包处理流程也是很有必要的。 网络数据包的接收处理可以分成两个部分，一是从物理网卡进入到达 linux 内核的网络协议栈，二是经网络协议栈处理后交给上层应用或者转发出去。本篇文档主要说明第一部分，并且不会去深入细节点（因为我也不太熟）。&lt;/p&gt;
&lt;h2&gt;重要概念和数据结构&lt;/h2&gt;
&lt;p&gt;在说明网络数据包的处理流程之前，有必要提前讲一下一些相关的概念，因为这些概念决定了后面的内容是否能够理解。 &lt;strong&gt;&lt;em&gt;硬中断&lt;/em&gt;&lt;/strong&gt; 硬中断是由硬件在发生某些事件后发出的，称为中断请求（IRQ)，CPU 会响应硬中断，并执行对应的 IRQ Handler。对于网卡来说，在有网络流量进入后，网卡会通过硬中断通知 CPU 有网络流量进来了，CPU 会调用对应网卡驱动中的处理函数。 硬中断在处理期间，是屏蔽外部中断的，所以硬中断的处理时间要尽可能的短。 &lt;strong&gt;&lt;em&gt;软中断&lt;/em&gt;&lt;/strong&gt; 软中断是由软件执行指令发出的，因为硬中断的特点不能处理耗时的任务，所以软中断往往用来替代硬中断来处理耗时任务。 比如网络流量的处理，网卡在发出硬中断通知 CPU 处理后，这次硬中断的处理方法中又会触发软中断，由软中断接着去处理网络流量数据。 &lt;strong&gt;&lt;em&gt;网卡驱动&lt;/em&gt;&lt;/strong&gt; 驱动是打通硬件和操作系统的通道，linux 通过网卡驱动，可以支持不同厂商，不同型号，不同特性的网卡。网卡驱动主要负责将从网卡中进来的流量解析并转换成 sk_buff，交给内核协议栈。 &lt;strong&gt;&lt;em&gt;DMA&lt;/em&gt;&lt;/strong&gt; DMA是一种无需CPU的参与就可以让外设和系统内存之间进行双向数据传输的硬件机制。网卡会通过 DMA 直接将网络流量数据存储到一块提前申请好的内存区域中。 &lt;strong&gt;&lt;em&gt;NAPI&lt;/em&gt;&lt;/strong&gt; 全称 New API，因为没有更好的名字，所以就直接用 NAPI 了。这是用于支持高速网卡处理网络数据包的一种机制。非 NAPI 往往是只依靠硬中断的方式让 CPU 来处理数据包，NAPI 引入了硬中断+轮询的方式，有效的缓解了硬中断带来的性能问题。 &lt;strong&gt;&lt;em&gt;sk_buff&lt;/em&gt;&lt;/strong&gt; sk_buff 是一个非常大而通用的 struct，可以用来表示2,3,4层的数据包。它被分成两个部分：head 和 data。 head 部分有单独的字段表示不同层的网络头：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;transport_header：用来表示传输层（4层）的 header，包括 tcp, udp, icmp 等协议头&lt;/li&gt;
&lt;li&gt;network_header：用来表示网络层（3层）的 header，包括 ip, ipv6, arp 等协议头&lt;/li&gt;
&lt;li&gt;mac_header：用来表示链路层（2层）的 header。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当数据包进入网络协议栈之前，需要先被转换成 sk_buff。&lt;/p&gt;
&lt;h2&gt;流程梳理&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;数据包进入触发硬中断&lt;/em&gt;&lt;/strong&gt; &lt;img src=&quot;/uploads/wp/2021/07/%E6%B5%81%E9%87%8F%E8%BF%9B%E5%85%A5%E5%88%B0%E7%A1%AC%E4%BB%B6%E4%B8%AD%E6%96%AD.jpg&quot; alt=&quot;流量进入到硬件中断&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;数据包进入网卡设备&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;网卡设备通过 DMA 直接写入的内存中。如果写不下就直接 drop 掉&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;网卡产生硬中断&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CPU 收到硬中断后，会直接提前注册好的该硬中断的 handler。这个 handler 是写在网卡驱动中的一个方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IRQ handler 禁用网卡的 IRQ。这是后面处理内存中的数据包是采用的 poll 模式。也就是说 cpu 会自己去内存中轮询数据包，直到一定时间/数量，或者全部处理完之后。这段时间内就不需要网卡通过硬中断来通知 CPU 了，并且硬中断会打断 CPU 的工作，带来一定的性能问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;网卡驱动产生软中断。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;软中断触发数据包的处理&lt;/em&gt;&lt;/strong&gt; &lt;img src=&quot;/uploads/wp/2021/07/%E8%BD%AF%E4%B8%AD%E6%96%AD%E8%A7%A6%E5%8F%91%E6%95%B0%E6%8D%AE%E5%8C%85%E7%9A%84%E5%A4%84%E7%90%86.jpg&quot; alt=&quot;软中断触发数据包的处理&quot; /&gt; 这里为了方便表述，使用目前最常用的 NAPI 的处理流程进行说明。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在系统启动时，net_dev_init 方法中注册了 NET_RX_SOFTIRQ 对应的 handler 是 net_rx_action。上面触发软中断的方式是 __raise_softirq_irqoff(NET_RX_SOFTIRQ)。所以开始执行 net_rx_action&lt;/li&gt;
&lt;li&gt;net_rx_action 会从 poll_list 链表中获取第一个 poll，使用 napi_poll 轮询内存中的数据包。napi_poll 调用到网卡驱动提供的 poll 方法&lt;/li&gt;
&lt;li&gt;poll 方法中从内存中取出数据包&lt;/li&gt;
&lt;li&gt;网卡驱动调用 napi_gro_receive 来处理数据包&lt;/li&gt;
&lt;li&gt;napi gro 会合并多个 skb 数据包，比如一个 IP 包会被分成多个 frame 这种。那么如果在接收的时候，在到达协议栈之前直接合并，会有一定的性能提升。这里最终会调用到 gro_normal_list 来批量处理 skb。&lt;/li&gt;
&lt;li&gt;最终调用到 netif_receive_skb_list_internal，从 napi.rx_list 上处理 sk_buff 链表。&lt;/li&gt;
&lt;li&gt;如果开启了 RPS，会根据 skb 的 hash 值找到对应的 cpu，将 skb 存储到该 cpu 上的 backlog 队列。backlog 队列是一种用软件方式将数据包处理负载均衡到多个 cpu 上的一种方法。&lt;/li&gt;
&lt;li&gt;最终都会调用到 __netif_receive_skb_core。&lt;/li&gt;
&lt;li&gt;如果有 AF_PACKET 的 socket，还会拷贝一份给它（tcpdump 的实现原理）。&lt;/li&gt;
&lt;li&gt;最后递交给内核协议栈&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://cxd2014.github.io/2017/10/15/linux-napi/&quot; rel=&quot;noopener&quot;&gt;Linux协议栈--NAPI机制&lt;/a&gt; &lt;a href=&quot;https://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data/#receive-packet-steering-rps&quot; rel=&quot;noopener&quot;&gt;Monitoring and Tuning the Linux Networking Stack: Receiving Data&lt;/a&gt; &lt;a href=&quot;https://abcdxyzk.github.io/blog/2015/04/18/kernel-net-gro/&quot; rel=&quot;noopener&quot;&gt;linux kernel 网络协议栈之GRO(Generic receive offload)&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>linux</category><category>网络</category><author>joyme123</author></item><item><title>kube-proxy iptables 流量处理流程</title><link>https://www.myway5.com/blog/kube-proxy-iptables/</link><guid isPermaLink="true">https://www.myway5.com/blog/kube-proxy-iptables/</guid><description>kube-proxy 在 iptables 模式下，主要是通过使用 iptables 提供从 service 到 pod 的访问。主要作用在两个表上： NAT：访问 service 时，需要 DNAT 到 pod IP 上 Filter: 对流量做过滤，比如如果一个 servi…</description><pubDate>Tue, 13 Jul 2021 13:29:25 GMT</pubDate><content:encoded>&lt;p&gt;kube-proxy 在 iptables 模式下，主要是通过使用 iptables 提供从 service 到 pod 的访问。主要作用在两个表上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NAT：访问 service 时，需要 DNAT 到 pod IP 上&lt;/li&gt;
&lt;li&gt;Filter: 对流量做过滤，比如如果一个 service 没有 endpoints，就直接 REJECT 掉访问 cluster IP 的流量等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NAT 主要作用在三个关键点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PREROUTING: 在这里为进入 node 流量进行处理，如果是访问 service，则选择一个后端 pod DNAT，并在流量上做标记&lt;/li&gt;
&lt;li&gt;OUTPUT: 在这里为从本机进程出来的流量进行处理，如果是访问 service，则选择一个后端 pod DNAT，并在流量上做标记。&lt;/li&gt;
&lt;li&gt;POSTROUTING: 为做了标记的流量做 MASQUERADE，MASQUIERADE 可以理解为加强版的 SNAT，会自动根据出去的网卡选择 src IP。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Filter 主要作用在三个点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;INPUT: 发往本机的流量&lt;/li&gt;
&lt;li&gt;FORWARD: 转发到其他 host 的流量&lt;/li&gt;
&lt;li&gt;OUTPUT: 从本机进程出去的流量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;分析 kube-proxy iptables 时，主要就是从上述的几个点去看，iptables 规则本身比较枯燥，没有太多可说的。下面是整理的 kube-proxy 使用 iptables 的流量处理流程。可以用来作参考。 &lt;img src=&quot;/uploads/wp/2021/07/iptables-flow.jpg&quot; alt=&quot;kube-proxy-iptables&quot; /&gt;&lt;/p&gt;
</content:encoded><category>linux</category><category>k8s</category><author>joyme123</author></item><item><title>cgroup cpu子系统</title><link>https://www.myway5.com/blog/cgroup-cpu-subsystem/</link><guid isPermaLink="true">https://www.myway5.com/blog/cgroup-cpu-subsystem/</guid><description>cgroup 全名是 control groups，在 linux 上负责对进程的一系列资源进行管控。比如 CPU，Memory，Huge Pages 等。cgroup 下通过子系统(subsystem)来划分模块，每种资源都通过一个子系统来实现。</description><pubDate>Tue, 15 Jun 2021 05:54:24 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;cgroup 全名是 control groups，在 linux 上负责对进程的一系列资源进行管控。比如 CPU，Memory，Huge Pages 等。cgroup 下通过子系统(subsystem)来划分模块，每种资源都通过一个子系统来实现。 cgroup 通过文件系统的方式对外提供调用，并可以用层级的方式进行组合。这种层级通过文件系统目录的方式进行呈现。比如在 cgroup cpu 目录下创建子目录，就相当于在根 cpu cgroup 下创建了一个子 cgroup。并且子 cgroup 会继承父 cgroup 的限制。 cgroup 目前有两个版本：v1 和 v2，并且两个版本的设计差异较大。但是理念类似，因此即使版本不同，也可以一样来理解。下面会以 cgroup v1 cpu 子系统进行讲解。&lt;/p&gt;
&lt;h2&gt;cpu 子系统的使用&lt;/h2&gt;
&lt;p&gt;cgroup 描述起来一直是一个比较抽象的概念。下面用一个简单的例子来帮助认识 cgroup 是如何工作的。 首先在机器上启动一个 stress 进程，分配一个 cpu，然后查看该进程 cpu 占用情况：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ stress -c 1

$ pidstat -p 480164 1
Linux 4.14.81.bm.26-amd64 (n251-254-159)    06/01/2021  _x86_64_    (8 CPU)
02:36:56 PM   UID       PID    %usr %system  %guest    %CPU   CPU  Command
02:36:57 PM  1001    480164  100.00    0.00    0.00  100.00     6  stress
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，stress 进程已经占用了 1 cpu。现在我们创建一个名叫 stress 的 cgroup 来限制 cpu：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ cd /sys/fs/cgroup/cpu

$ mkdir stress &amp;amp;&amp;amp; cd stress

# 将 pid 写入到 cgroup.procs 中，就等同于将这个进程移到该 cgroup 中
$ echo 480164 &amp;gt; cgroup.procs

$ echo 100000 &amp;gt; cpu.cfs_period_us

$ echo 50000 &amp;gt; cpu.cfs_quota_us

# 再看看当前的 CPU 占用
$ pidstat -p 480164 1
Linux 4.14.81.bm.26-amd64 (n251-254-159)    06/04/2021  _x86_64_    (8 CPU)

05:17:49 AM   UID       PID    %usr %system  %guest    %CPU   CPU  Command
05:17:50 AM  1001   480164   50.00    0.00    0.00   50.00     6  stress
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述操作通过配置 &lt;code&gt;cpu.cfs_period_us&lt;/code&gt; 和 &lt;code&gt;cpu.cfs_quota_us&lt;/code&gt; 参数达到了限制进程使用 CPU 的目的。 cgroup 还提供了一个 &lt;code&gt;cpu.shares&lt;/code&gt; 参数，当 CPU 资源繁忙时，这个参数可以配置进程使用 CPU 的权重。下面我们在 cpu 为 1 的虚拟机演示。 在 cgroup 下创建两个子 cgroup 来展示这个参数的效果。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ cd /sys/fs/cgroup/cpu,cpuacct
$ mkdir stress1 &amp;amp;&amp;amp; cd stress1
$ stress -c 1
$ echo 3475127 &amp;gt; cgroup.procs
$ echo 1024 &amp;gt; cpu.shares
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时 PID 3475127 的 stress 进程 CPU 占用率接近 100%。在新的终端中执行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ mkdir stress2 &amp;amp;&amp;amp; cd stress2
$ stress -c 1
$ echo 3479833 &amp;gt; cgroup.procs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时两个 stress 进程的 CPU 占用大致相等，接近 50%。因为 stress2 cgroup 中没有设置 cpu.shares，所以取默认值为 1024。现在设置 stress2 cgroup 的 cpu.shares 参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ echo 512 &amp;gt; cpu.shares

# 使用 top 查看
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM
  3475127 root      20   0    7948     96      0 R  65.1   0.0
  3479833 root      20   0    7948     92      0 R  32.2   0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;stress1 中的进程 CPU 占用率大概是 stress2 中的两倍。这是因为 stress1 中 cpu.shares 的值是 stress2 中的两倍。当然上述情况必须在 CPU 资源不够时，cpu.shares 才会起作用。如果这是一个 2 cpu 的虚拟机，那么 stress1 和 stress2 都会占用 100%。&lt;/p&gt;
&lt;h2&gt;参数说明&lt;/h2&gt;
&lt;p&gt;上述出现了一些 cpu 的参数，这里统一解释一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;cpu.cfs_period_us: 重新分配 CPU 资源的时间周期长度，单位是 us。cfs 是 linux 进程调度器的一种，全称为&lt;strong&gt;完全公平调度器&lt;/strong&gt;。因此这个参数只针对使用 cfs 调度的进程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cpu.cfs_quota_us: 进程在设置的时间周期长度内，可以使用的 CPU 时间上限。结合 cpu.cfs_period_us 就可以限制一个进程可以使用的总 CPU 时间了。计算方式为 &lt;code&gt;(cpu.cfs_quota_us / cpu.cfs_period_us)*count(cpu)&lt;/code&gt;。这个参数只针对使用 cfs 调度的进程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cpu.shares: 这个参数只有在 CPU 资源忙时才生效，它可以用来设置进程使用的 CPU 权重。上面的例子中，虚拟机只有 1 CPU，进程 1,2 都会占用一个 CPU，因此根据设置进程 1 的 cpu.shares 为 1024，进程 2 的 cpu.shares 为 512，就可以将 2/3 的 cpu 分配给进程 1，1/3 的 cpu 分配给进程 2 了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了上述例子中的几个参数，cgroup cpu 子系统还提供了以下的参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cpu.rt_period_us: 重新分配 CPU 资源的时间周期长度。 针对使用了实时调度器的进程&lt;/li&gt;
&lt;li&gt;cpu.rt_runtime_us: 进程在设置的时间周期长度内，可以使用的 CPU 时间上限。这个和上面说的 cfs 的两个参数类似。&lt;/li&gt;
&lt;li&gt;cpu.nr_periods: 这是一个统计参数。用来表示已经过去的 cpu 周期数（使用 cpu.cfs_period_us 来指定)&lt;/li&gt;
&lt;li&gt;cpu.nr_throttled: cgroup 中进程被限制的次数（因为这些进程用完了分配的 cpu 时间）。&lt;/li&gt;
&lt;li&gt;cpu.throttled_time: cgroup 中进程被限制的总时间（单位是 ns）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/72754729&quot; rel=&quot;noopener&quot;&gt;Linux进程调度：完全公平调度器CFS&lt;/a&gt; &lt;a href=&quot;https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/resource_management_guide/sec-cpu&quot; rel=&quot;noopener&quot;&gt;redhat cfs cpu&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>linux</category><category>容器技术</category><author>joyme123</author></item><item><title>containerd CRI 简要分析</title><link>https://www.myway5.com/blog/containerd-cri/</link><guid isPermaLink="true">https://www.myway5.com/blog/containerd-cri/</guid><description>Containerd 在 release1.5 之后内置了 cri。通过暴露 CRIService 供 kubelet 调用。CRI 的封装并不复杂，都是利用了 containerd 本身的功能模块。</description><pubDate>Tue, 01 Jun 2021 05:41:06 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;Containerd 在 release1.5 之后内置了 cri。通过暴露 CRIService 供 kubelet 调用。CRI 的封装并不复杂，都是利用了 containerd 本身的功能模块。通过 CRI 管理 pod 主要分为三个模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sandbox 的管理：RunPodSandbox、StopPodSandbox、RemovePodSandbox、PodSandboxStatus、ListPodSandbox&lt;/li&gt;
&lt;li&gt;Container 的管理：CreateContainer、StartContainer、StopContainer、RemoveContainer、ListContainers、UpdateContainerResources、ContainerStats、ListContainerStats、UpdateRuntimeConfig、Status&lt;/li&gt;
&lt;li&gt;容器其他方面的管理：ReopenContainerLog、ExecSync、Exec、Attach、PortForward&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Sandbox 的管理&lt;/h2&gt;
&lt;p&gt;Sandbox 和 Container 在实现上类似，通过启动一个特殊的 pause 容器来创建一个沙箱环境。通常情况下，这个沙箱环境具有和主机隔离的 pid，uts，mount，network，ipc，user。 以 RunPodSandbox 为例，containerd 会执行以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Containerd CRI 在 RunPodSandbox 时，会先保证 sandbox 的镜像是否存在。如果不存在则会进行 pull 操作。&lt;/li&gt;
&lt;li&gt;创建 pod network namespace，并调用 CNI&lt;/li&gt;
&lt;li&gt;使用 container task 来创建容器&lt;/li&gt;
&lt;li&gt;启动 container task，也就是执行二进制文件 pause&lt;/li&gt;
&lt;li&gt;更新 sandbox 的状态，包括 pid 置为 task pid，state 置为 ready 以及更新创建时间。&lt;/li&gt;
&lt;li&gt;在新的 goroutine 中启动 sandbox exit monitoring。这样就可以在 sandbox 进程退出时，执行清理工作。然后更新 sandbox 的状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以 StopPodSandbox 为例，containerd 会执行以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 containerStore 来 list 出所有 container，并使用 container.SandboxID 过滤出属于当期 sandbox 的 container&lt;/li&gt;
&lt;li&gt;依次停止 sandbox 下的 container&lt;/li&gt;
&lt;li&gt;清理 sandbox 的文件，比如 unmount dev shm&lt;/li&gt;
&lt;li&gt;如果 sandbox container(pause) 的状态是 Ready 或 Unknown，使用 SiGKILL 终止 sandbox container&lt;/li&gt;
&lt;li&gt;调用 cni 清理 network namespace，然后移除。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Container 管理&lt;/h2&gt;
&lt;p&gt;在 sandbox 创建好之后，kubelet 就可以通过 CRI 来创建 container 了。CreateContainer 的逻辑如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取 sandbox 的信息，后面创建的容器需要使用和 sandbox 同样的 runtime，namespace。&lt;/li&gt;
&lt;li&gt;获取 container mounts，包括 &lt;code&gt;/etc/hosts&lt;/code&gt;，&lt;code&gt;/etc/resolv.con&lt;/code&gt;，&lt;code&gt;dev shm&lt;/code&gt;。然后设置&lt;/li&gt;
&lt;li&gt;设置 container log path。&lt;/li&gt;
&lt;li&gt;设置 container io，包括 stdin, stdout, stderr, terminal。&lt;/li&gt;
&lt;li&gt;在 container store(metadata) 中创建容器记录。此时 container 处于 CREATED 状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;创建好 container 后，调用 StartContainer 即可运行容器。步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;更新 container 状态为 Running，防止重复 start。&lt;/li&gt;
&lt;li&gt;设置 container stdout，stderr 到 log 文件上。&lt;/li&gt;
&lt;li&gt;启动 task 来运行 container 进程&lt;/li&gt;
&lt;li&gt;更新 container status 的 Pid 和 StartedAt&lt;/li&gt;
&lt;li&gt;在新的 goroutine 中启动 sandbox exit monitoring 来监控 container 的进程状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;StopContainer 的步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果 container 设置了 stop timeout，使用 task.Kill 发送 SIGTERM 信号。然后等待 timeout 时间&lt;/li&gt;
&lt;li&gt;使用 task.Kill 发送 SIGKILL 信号。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;RemoveConrtainer 的步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果当前 container 处于 RUNNING 或者 UNKNOWN 状态，则使用 timeout 为 0 的 stopContainer 来强制停止容器。&lt;/li&gt;
&lt;li&gt;设置 container 的状态为 removing。&lt;/li&gt;
&lt;li&gt;从 store 中删除 container，checkpoint 以及一些缓存信息。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;容器管理&lt;/h2&gt;
&lt;p&gt;Containerd CRI 除了实现了 sandbox 和 container 的生命周期管理，也提供了对容器其他方面的管理。比如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 kubelet 对 logfile rotate 之后，调用 ReopenContainerLog 来将 container log 输出到新的日志文件中&lt;/li&gt;
&lt;li&gt;通过 GRPC 提供 GetAttach，返回 attach http endpoint 和 token，供 kubelet 通过 http stream 连接到 process 的 stdin，stdout 和 stderr 上。&lt;/li&gt;
&lt;li&gt;通过 GRPC 提供 GetExec，返回 exec http endpoint 和 token，供 kubelet 通过 http stream 在容器命名空间内执行命令。&lt;/li&gt;
&lt;li&gt;通过 GRPC 提供 GetPortforward，返回 portforward http endpoint 和 token。kubelet 通过 http stream 连接上后，使用 netns 在 sandbox network namespace 下 dial 容器内的端口，使用 io.Copy 转发输入输出流。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>容器技术</category><author>joyme123</author></item><item><title>containerd storage模块分析</title><link>https://www.myway5.com/blog/containerd-storage/</link><guid isPermaLink="true">https://www.myway5.com/blog/containerd-storage/</guid><description>containerd 的 storage 模块负责镜像的存储，容器 rootfs 的创建等工作。其主要包括三个子模块： content: content 会在本地目录下保存镜像的内容。包括镜像的 manifest，config，以及镜像的层。</description><pubDate>Mon, 24 May 2021 06:31:52 GMT</pubDate><content:encoded>&lt;h2&gt;一、概述&lt;/h2&gt;
&lt;p&gt;containerd 的 storage 模块负责镜像的存储，容器 rootfs 的创建等工作。其主要包括三个子模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;content: content 会在本地目录下保存镜像的内容。包括镜像的 manifest，config，以及镜像的层。每个层都是一个文件，格式是 tar+gzip，名称为层的 sha256sum 值。content 中存储的层都是不可变的。也就是使用的时候，并不会改变这里面的任何文件。&lt;/li&gt;
&lt;li&gt;snapshot: snapshot 对容器运行时的层做了抽象。分为三种类型：Commited，Active，View。其中 Active 和 View 类似，不过前者可读写，后者只读。Active 和 View 类型的 snapshot 就是我们观察到的文件系统，一般是最上层。Commited 和另外两个相反，对用户不可见，作为 Active 或 View 的 parent 使用。&lt;/li&gt;
&lt;li&gt;diff: diff 的主要功能有两个：Compare 和 Apply。Compare 负责计算 lower 和 upper 挂载的差异，然后使用 tar 打包差异生成新的镜像层。Apply 负责将镜像层挂载到文件系统上，生成容器运行时需要的 rootfs。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2021/05/architecture.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;二、content 如何工作的&lt;/h2&gt;
&lt;p&gt;content 通过 GRPC 对外提供了以下接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// ContentServer is the server API for Content service.
type ContentServer interface {
    Info(context.Context, *InfoRequest) (*InfoResponse, error)
    Update(context.Context, *UpdateRequest) (*UpdateResponse, error)
    List(*ListContentRequest, Content_ListServer) error
    Delete(context.Context, *DeleteContentRequest) (*types.Empty, error)
    Read(*ReadContentRequest, Content_ReadServer) error
    Status(context.Context, *StatusRequest) (*StatusResponse, error)
    ListStatuses(context.Context, *ListStatusesRequest) (*ListStatusesResponse, error)
    Write(Content_WriteServer) error
    Abort(context.Context, *AbortRequest) (*types.Empty, error)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;提供了 local 和 proxy 的实现，其中 proxy 是通过 GRPC 将具体实现解耦合，因此这里并不讨论，主要关注 local 的实现方式。local 的实现中，将以上接口再分为 4 个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manager: 提供了对 content 的查询，更新和删除操作&lt;/li&gt;
&lt;li&gt;Provider：提供了对指定 content 内容的读取&lt;/li&gt;
&lt;li&gt;IngestManager：提供了对 ingest 的状态查询和终止操作。&lt;/li&gt;
&lt;li&gt;Ingester：提供了对 ingest 的写入操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Store combines the methods of content-oriented interfaces into a set that
// are commonly provided by complete implementations.
type Store interface {
    Manager
    Provider
    IngestManager
    Ingester
}

// Manager provides methods for inspecting, listing and removing content.
type Manager interface {
    Info(ctx context.Context, dgst digest.Digest) (Info, error)
    Update(ctx context.Context, info Info, fieldpaths ...string) (Info, error)
    Walk(ctx context.Context, fn WalkFunc, filters ...string) error
    Delete(ctx context.Context, dgst digest.Digest) error
}

// Provider provides a reader interface for specific content
type Provider interface {
    ReaderAt(ctx context.Context, desc ocispec.Descriptor) (ReaderAt, error)
}

// IngestManager provides methods for managing ingests.
type IngestManager interface {
    Status(ctx context.Context, ref string) (Status, error)
    ListStatuses(ctx context.Context, filters ...string) ([]Status, error)
    Abort(ctx context.Context, ref string) error
}

// Ingester writes content
type Ingester interface {
    Writer(ctx context.Context, opts ...WriterOpt) (Writer, error)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了防止理解上有歧义，这里对一些术语做一些详细的解释&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;content: content 中存储的最小单位可以是镜像的 manifest，config，或者是镜像的一层，通常还包含该层的大小，创建/修改时间，labels 等等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;digest: 在 content 模块，digest 指的是镜像层的 sha256sum 值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ingest: 因为文件系统在写入文件时，是无法保证原子性的。所以一般的解决方案是是先写入中间文件，然后通过 rename 调用，把中间文件改成要写入的目标文件。ingest 就是这个中间文件集的统称，一个 ingest 对应一个 content。ingest 包含这几个文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;data: content 的数据&lt;/li&gt;
&lt;li&gt;ref: 根据 ref 找到 target location&lt;/li&gt;
&lt;li&gt;startedat: 开始时间&lt;/li&gt;
&lt;li&gt;updatedat: 更新时间&lt;/li&gt;
&lt;li&gt;total: content 的总大小&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;content 的使用其实已经很底层了，所以这里不准备按照 content 提供的接口进行分析，而是通过 &lt;code&gt;ctr image pull docker.io/library/nginx:latest&lt;/code&gt; 来说明 content 的工作原理。这条命令在 &lt;code&gt;cmd/ctr/commands/images/pull&lt;/code&gt; 下。主要执行了以下几行代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;client, ctx, cancel, err := commands.NewClient(context)
ctx, done, err := client.WithLease(ctx)
config, err := content.NewFetchConfig(ctx, context)
img, err := content.Fetch(ctx, client, ref, config)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;commands.NewClient(context)&lt;/code&gt;会初始化 ctr 到 containerd 的连接参数。比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;timeout: ctr 连接 containerd 的超时时间，默认 10s&lt;/li&gt;
&lt;li&gt;defaultns: containerd 使用 namespace 进行租户隔离。默认值为 default&lt;/li&gt;
&lt;li&gt;address: containerd 的地址，默认值是 &lt;code&gt;/run/containerd/containerd.sock&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;runtime: 默认是 &lt;code&gt;io.containerd.runc.v2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;platform: 指的是操作系统，CPU 架构 等&lt;/li&gt;
&lt;li&gt;还要一些 GRPC 连接数据等等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;client.WithLease(ctx)&lt;/code&gt;会在 metadata 中记录该操作，当该操作结束后，也会从 metadata 中删除。 &lt;code&gt;content.NewFetchConfig(ctx, context)&lt;/code&gt; 用来初始化这次拉取镜像的配置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实例化 resolver，resolver 负责从远端 pull 到本地。containerd 使用的是 &lt;code&gt;remotes/docker&lt;/code&gt;，应该是从 docker 那部分拿过来的代码。&lt;/li&gt;
&lt;li&gt;配置 platforms&lt;/li&gt;
&lt;li&gt;配置 &lt;code&gt;max-concurrent-downloads&lt;/code&gt;，这个参数会限制同时下载的并发&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;content.Fetch(ctx, client, ref, config)&lt;/code&gt; 负责调用上一步实例化出的 client，根据 ref(镜像地址) 和 fetch config 来拉取远端镜像，然后使用 content 的接口存储到本地。 下面主要就 Fetch 展开分析。这里可以先了解一下，Fetch 会做以下的工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置 opts(RemoteOpt 数组)，&lt;code&gt;type RemoteOpt func(*Client, *RemoteContext) error&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;应用到 image 上的 labels&lt;/li&gt;
&lt;li&gt;设置上面实例化的 resolver&lt;/li&gt;
&lt;li&gt;设置 BaseHandlers，BaseHandlers 在 dispatch 时调用&lt;/li&gt;
&lt;li&gt;设置 AllMetadata，AllMetadata 会下载所有的 manifest 和已知的配置文件&lt;/li&gt;
&lt;li&gt;设置 Platforms&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;使用 resolver 来将我们提供的镜像名(ref) 解析成 name 和 descriptor
&lt;ul&gt;
&lt;li&gt;如果镜像名的格式为: &lt;code&gt;docker.io/library/nginx:latest&lt;/code&gt;，则会发送 Head请求到 &lt;code&gt;https://registry-1.docker.io/v2/library/nginx/manifests/latest&lt;/code&gt;，从响应头中获取 &lt;code&gt;docker-content-digest&lt;/code&gt; 的值。如果不存在，还会再发同样的请求，使用 GET 方法来获取 manifest，从 manifest 中获取 digest。最终会获取 digest，mediaType 和 size 三个值。&lt;/li&gt;
&lt;li&gt;如果镜像名的格式为：&lt;code&gt;docker.io/library/nginx@sha256:df13abe416e37eb3db...&lt;/code&gt;，则@ 后面提供的是 digest 值。此时就使用 &lt;code&gt;https://registry-1.docker.io/v2/library/nginx/manifests/sha256:df13abe416e37eb3db...&lt;/code&gt;来实现&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;现在得到了 image digest，此时调用 &lt;code&gt;images.Dispatch(ctx, handler, limiter, desc)&lt;/code&gt;方法。这个 Dispatch 方法是个递归调用
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;先通过 image digest，调用 dockerFetcher 和 content，将 image manifests 列表信息 store 到 content blobs 中。并找到符合当前 platform 的 descriptor。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再通过上一步获取的 digest，调用 dockerFetcher 和 content，获取该 platform 的 manifest，这次的内容中会包含 config 和 layers&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据 config 的 digest，调用 dockerFetcher 和 content，获取 config 的内容并存储&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据 layers 数组中每个 layer 的 digest，调用 dockerFetcher 和 content，获取 layer 内容并存储。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;调用 createNewImage 在 metadata 中创建 image 记录。记录中存储了 image 的 digest 和 lables。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一步获取的 manifests 内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;manifests&quot;: [
        {
            &quot;digest&quot;: &quot;sha256:eba373a0620f68ffdc3f217041ad25ef084475b8feb35b992574cd83698e9e3c&quot;,
            &quot;mediaType&quot;: &quot;application/vnd.docker.distribution.manifest.v2+json&quot;,
            &quot;platform&quot;: {
                &quot;architecture&quot;: &quot;amd64&quot;,
                &quot;os&quot;: &quot;linux&quot;
            },
            &quot;size&quot;: 1570
        }
    ],
    &quot;mediaType&quot;: &quot;application/vnd.docker.distribution.manifest.list.v2+json&quot;,
    &quot;schemaVersion&quot;: 2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二步获取的 manifest 如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;schemaVersion&quot;: 2,
    &quot;mediaType&quot;: &quot;application/vnd.docker.distribution.manifest.v2+json&quot;,
    &quot;config&quot;: {
        &quot;mediaType&quot;: &quot;application/vnd.docker.container.image.v1+json&quot;,
        &quot;size&quot;: 7736,
        &quot;digest&quot;: &quot;sha256:f0b8a9a541369db503ff3b9d4fa6de561b300f7363920c2bff4577c6c24c5cf6&quot;
    },
    &quot;layers&quot;: [
        {
            &quot;mediaType&quot;: &quot;application/vnd.docker.image.rootfs.diff.tar.gzip&quot;,
            &quot;size&quot;: 27145915,
            &quot;digest&quot;: &quot;sha256:69692152171afee1fd341febc390747cfca2ff302f2881d8b394e786af605696&quot;
        },
        {
            &quot;mediaType&quot;: &quot;application/vnd.docker.image.rootfs.diff.tar.gzip&quot;,
            &quot;size&quot;: 26576310,
            &quot;digest&quot;: &quot;sha256:49f7d34d62c18a321b727d5c05120130f72d1e6b8cd0f1cec9a4cca3eee0815c&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三步获取的 config 如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;architecture&quot;: &quot;amd64&quot;,
    &quot;config&quot;: {
        &quot;Hostname&quot;: &quot;&quot;,
        &quot;Domainname&quot;: &quot;&quot;,
        &quot;User&quot;: &quot;&quot;,
        &quot;AttachStdin&quot;: false,
        &quot;AttachStdout&quot;: false,
        &quot;AttachStderr&quot;: false,
        &quot;ExposedPorts&quot;: {
            &quot;80/tcp&quot;: {}
        },
        &quot;Tty&quot;: false,
        &quot;OpenStdin&quot;: false,
        &quot;StdinOnce&quot;: false,
        &quot;Env&quot;: [
            &quot;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&quot;,
            &quot;NGINX_VERSION=1.19.10&quot;,
            &quot;NJS_VERSION=0.5.3&quot;,
            &quot;PKG_RELEASE=1~buster&quot;
        ],
        &quot;Cmd&quot;: [
            &quot;nginx&quot;,
            &quot;-g&quot;,
            &quot;daemon off;&quot;
        ],
        &quot;Image&quot;: &quot;sha256:f46ebb94fdef867c7f07f0b9c458ebe0ca97191f9fd6f91fd918ef71702cd755&quot;,
        &quot;Volumes&quot;: null,
        &quot;WorkingDir&quot;: &quot;&quot;,
        &quot;Entrypoint&quot;: [
            &quot;/docker-entrypoint.sh&quot;
        ],
        &quot;OnBuild&quot;: null,
        &quot;Labels&quot;: {
            &quot;maintainer&quot;: &quot;NGINX Docker Maintainers &amp;lt;docker-maint@nginx.com&amp;gt;&quot;
        },
        &quot;StopSignal&quot;: &quot;SIGQUIT&quot;
    },
    &quot;container&quot;: &quot;b728dbd6862a960807b78a68f3d1d6697d954ed2b53d05b1b4c440f4aa8574a3&quot;,
    &quot;container_config&quot;: {
      ...
    },
    &quot;created&quot;: &quot;2021-05-12T08:40:31.711670345Z&quot;,
    &quot;docker_version&quot;: &quot;19.03.12&quot;,
    &quot;history&quot;: [
        {
            &quot;created&quot;: &quot;2021-05-12T01:21:22.128649612Z&quot;,
            &quot;created_by&quot;: &quot;/bin/sh -c #(nop) ADD file:7362e0e50f30ff45463ea38bb265cb8f6b7cd422eb2d09de7384efa0b59614be in / &quot;
        }
    ],
    &quot;os&quot;: &quot;linux&quot;,
    &quot;rootfs&quot;: {
        &quot;type&quot;: &quot;layers&quot;,
        &quot;diff_ids&quot;: [
            &quot;sha256:02c055ef67f5904019f43a41ea5f099996d8e7633749b6e606c400526b2c4b33&quot;,
            &quot;sha256:431f409d4c5a8f79640000705665407ff22d73e043472cb1521faa6d83afc5e8&quot;,
            &quot;sha256:4b8db2d7f35aa38ac283036f2c7a453ebfdcc8d7e83a2bf3b55bf8847f8fafaf&quot;,
            &quot;sha256:c9732df61184e9e8d08f96c6966190c59f507d8f57ea057a4610f145c59e9bc4&quot;,
            &quot;sha256:eeb14ff930d4c2c04ece429112c16a536985f0cba6b13fdb52b00853107ab9c4&quot;,
            &quot;sha256:f0f30197ccf95e395bbf4efd65ec94b9219516ae5cafe989df4cf220eb1d6dfa&quot;
        ]
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第四步获取的就是每个 layer 的二进制数据了。 通过上面的分析可以知道，containerd 本身并没有实现镜像的 pull，但是通过暴露 storage 中的 content 和 matadata 中 image 接口，可以在调用方实现 image pull，并将数据按照 containerd 的要求进行存储，相当于 containerd 只提供了 image 存储的实现。总结一下流程如下： &lt;img src=&quot;/uploads/wp/2021/05/containerd.jpg&quot; alt=&quot;containerd&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;三、snapshot 如何工作的&lt;/h2&gt;
&lt;p&gt;存储在 content 中的镜像层是不可变的，通常其存储格式也是没法直接使用的，常见的格式为 tar-gzip。为了使用 content 中存储的镜像层，containerd 抽象出了 snapshot，每个镜像层都会生成对应的 snapshot。 snapshot 有三种类型：committed，active 和 view。在启动容器前，镜像的每一层都会被创建成 committed snapshot，committed 表示该镜像层不可变。最后再创建出一层 active snapshot，这一层是可读写的。 下方展示了一个 nginx 镜像被 run 起来后生成的 snapshot。snapshot 之间是有 parent 关系的。第1层的 parent 为空。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# ctr snapshot ls
KEY                PARENT                       KIND
nginx              sha256:60f61ee7da08          Active
sha256:02c055ef                                 Committed
sha256:5c3e94c8    sha256:adda6567aeaa          Committed
sha256:60f61ee7    sha256:affa58c5a9d1          Committed
sha256:6b1533d4    sha256:5c3e94c8305f          Committed
sha256:adda6567    sha256:02c055ef67f5          Committed
sha256:affa58c5    sha256:6b1533d42f38          Committed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2021/05/snapshots.jpg&quot; alt=&quot;snapshots_of_nginx&quot; /&gt; 下面针对执行 &lt;code&gt;ctr run docker.io/library/nginx:latest nginx&lt;/code&gt; 来说明，不过 ctr run 还会涉及到很多 runtime 相关的内容，这里为了简单不做叙述。 当执行 ctr run 之后，会根据提供的 image 名，创建出多个 snapshot。主要的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 从 metadata 中查询 image 的信息
i, err := client.ImageService().Get(ctx, ref)
// 根据 image 信息初始化 image 实例
image = containerd.NewImage(client, i)
// 这个 image 是否 unpacked
unpacked, err := image.IsUnpacked(ctx, snapshotter)
if !unpacked {
  // unpack 镜像
  if err := image.Unpack(ctx, snapshotter); err != nil {
    return nil, err
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;IsUnpacked&lt;/code&gt; 的实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (i *image) IsUnpacked(ctx context.Context, snapshotterName string) (bool, error) {
    // 获取 snapshotter 实例，默认是 overlayfs
  sn, err := i.client.getSnapshotter(ctx, snapshotterName)
    if err != nil {
        return false, err
    }
  // 获取 content store 实例
    cs := i.client.ContentStore()
  // 这里是通过读取 image manifest，获取到 image layers 的 digest，也就是 diffs
    diffs, err := i.i.RootFS(ctx, cs, i.platform)
    if err != nil {
        return false, err
    }

  // 通过 diffs 计算出最上层的 chainID
    chainID := identity.ChainID(diffs)
  // 因为 snapshot 的名字就是 chainID，这里通过判断最上层的 snapshot 的 chainID 是否存在
  // 就可以知道这个 image 是否 unpack 了
    _, err = sn.Stat(ctx, chainID.String())
    if err == nil {
        return true, nil
    } else if !errdefs.IsNotFound(err) {
        return false, err
    }

    return false, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;chainID 的计算方式参考之前的文章：&lt;a href=&quot;https://www.myway5.com/index.php/2021/05/18/%e5%ae%b9%e5%99%a8%e9%95%9c%e5%83%8f%e6%98%af%e5%a6%82%e4%bd%95%e5%b7%a5%e4%bd%9c%e7%9a%84/&quot; rel=&quot;noopener&quot;&gt;chainID 计算方式&lt;/a&gt; &lt;code&gt;Unpack&lt;/code&gt; 的实现如下，为了展示方便，代码有删减：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...UnpackOpt) error {
    // 获取镜像的 manifest
  manifest, err := i.getManifest(ctx, i.platform)
  // 通过 manifest，获取 layers
  layers, err := i.getLayers(ctx, i.platform, manifest)

  // 默认是 overlayfs
  snapshotterName, err = i.client.resolveSnapshotterName(ctx, snapshotterName)
  // 获取 snapshotter 实例
  sn, err := i.client.getSnapshotter(ctx, snapshotterName)

  for _, layer := range layers {
    // apply layer，这里是 snapshot 的工作重点
    unpacked, err = rootfs.ApplyLayerWithOpts(ctx, layer, chain, sn, a, config.SnapshotOpts, config.ApplyOpts)
        // chainID 的计算需要之前的 diffID，所以这里报错了每一层的 digest。
    chain = append(chain, layer.Diff.Digest)
  }
  // 最上层的 snapshot 就是 rootfs，可以提供给 runc 使用。
  rootfs := identity.ChainID(chain).String()
  return err
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Unpack&lt;/code&gt; 的过程，就是对每一层 apply layer 的过程。apply 一个 layer 的实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func applyLayers(ctx context.Context, layers []Layer, chain []digest.Digest, sn snapshots.Snapshotter, a diff.Applier, opts []snapshots.Opt, applyOpts []diff.ApplyOpt) error {
    for {
        key = fmt.Sprintf(snapshots.UnpackKeyFormat, uniquePart(), chainID)
        // prepare 会创建出一个 active snapshot
        mounts, err = sn.Prepare(ctx, key, parent.String(), opts...)
        break
    }
    // 使用 diff，将这一层应用到 prepare 的 layer 上
    diff, err = a.Apply(ctx, layer.Blob, mounts, applyOpts...)
    // Commit 会在 metadata 中将这个 snapshot 标记为 committed。
  // 对于 device mapper 设备，还会额外的使这个 snapshot 挂载不可见。
    if err = sn.Commit(ctx, chainID.String(), key, opts...); err != nil {
        err = errors.Wrapf(err, &quot;failed to commit snapshot %s&quot;, key)
        return err
    }

    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上就是 snapshotter 通过 image layers 创建出 snapshots 的过程。不过这上面创建的都是 committed snapshot。所以在这之后还会单独在这之上创建出一个 active snapshot 供容器读写。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// WithNewSnapshot allocates a new snapshot to be used by the container as the
// root filesystem in read-write mode
func WithNewSnapshot(id string, i Image, opts ...snapshots.Opt) NewContainerOpts {
    return func(ctx context.Context, client *Client, c *containers.Container) error {
        diffIDs, err := i.RootFS(ctx)
        if err != nil {
            return err
        }

        parent := identity.ChainID(diffIDs).String()
        c.Snapshotter, err = client.resolveSnapshotterName(ctx, c.Snapshotter)
        if err != nil {
            return err
        }
        s, err := client.getSnapshotter(ctx, c.Snapshotter)
        if err != nil {
            return err
        }
        if _, err := s.Prepare(ctx, id, parent, opts...); err != nil {
            return err
        }
        c.SnapshotKey = id
        c.Image = i.Name()
        return nil
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、diff 如何工作的&lt;/h2&gt;
&lt;p&gt;在上面对 content 和 snapshot 进行一些分析后，已经清楚了镜像的层是如何存储的，以及使用镜像是什么样的一个过程。但这其中还有两个细节没有说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个 image layer 如何被 mount 成一个 snapshot。&lt;/li&gt;
&lt;li&gt;一个 snapshot 如何被压缩成一个 image layer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里就是 diff 子模块的作用了。diff 对外提供了两个接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Diff: 负责将 snapshot 打包成 image layer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Apply: 负责将 image layer 生成 snapshot 挂载&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在说明 Diff 之前，需要先提一下 OCI 中 image spec 中的一个例子。假设现在有两个文件夹 &lt;code&gt;rootfs-c9d-v1/&lt;/code&gt; and &lt;code&gt;rootfs-c9d-v1.s1/&lt;/code&gt;。对其进行字典序的递归比较，发现的变动如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Added:      /etc/my-app.d/
Added:      /etc/my-app.d/default.cfg
Modified:   /bin/my-app-tools
Deleted:    /etc/my-app-config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么使用 OCI 的规范打包出来就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./etc/my-app.d/
./etc/my-app.d/default.cfg
./bin/my-app-tools
./etc/.wh.my-app-config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除的文件使用 .wh. 前缀来表示。 那么 Diff 的时候，主要就是对 snapshot 和其 parent 做比较，比较时使用字典序来 walk dir。然后生成的 tar 包中，对 added 和 modified 文件，只需打包最新的即可。对 deleted 文件，生成 .wh.* 来代替。 在 Apply 的时候，如果是 .wh. 前缀的文件，就根据所使用文件系统的特点来生成，比如 overlayfs 中使用 whiteout 文件来表示删除。非 .wh. 文件原样输出即可。&lt;/p&gt;
</content:encoded><category>容器技术</category><author>joyme123</author></item><item><title>containerd的启动流程</title><link>https://www.myway5.com/blog/containerd-startup/</link><guid isPermaLink="true">https://www.myway5.com/blog/containerd-startup/</guid><description>整体架构图如下： 使用 github.com/urfave/cli启动，有 command configCommand: 和 containerd 配置相关 publishCommand：event 相关 ociHook: 提供了 preStart, preStop 等 con…</description><pubDate>Thu, 20 May 2021 14:36:55 GMT</pubDate><content:encoded>&lt;p&gt;整体架构图如下： &lt;img src=&quot;/uploads/wp/2021/05/architecture.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;github.com/urfave/cli&lt;/code&gt;启动，有 command
&lt;ul&gt;
&lt;li&gt;configCommand: 和 containerd 配置相关&lt;/li&gt;
&lt;li&gt;publishCommand：event 相关&lt;/li&gt;
&lt;li&gt;ociHook: 提供了 preStart, preStop 等 container hook&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;未执行子 command，则执行默认的 action
&lt;ul&gt;
&lt;li&gt;加载配置文件&lt;/li&gt;
&lt;li&gt;创建顶层文件夹:&lt;/li&gt;
&lt;li&gt;root = &quot;/var/lib/containerd&quot;&lt;/li&gt;
&lt;li&gt;state = &quot;/run/containerd&quot;&lt;/li&gt;
&lt;li&gt;创建 /var/lib/containerd/tmpmounts&lt;/li&gt;
&lt;li&gt;清理 tmpmounts 下的临时挂载点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;创建和初始化 containerd server
&lt;ul&gt;
&lt;li&gt;将配置设置的 server 进程上&lt;/li&gt;
&lt;li&gt;如果设置了 OOMScore，则应用到进程上。OOMScore 越低，系统内存不足时越不会被 kill&lt;/li&gt;
&lt;li&gt;如果设置了 containerd 的 Cgroup path。则会将自己的进程加入到 cgroup 下。这里还会判断使用 cgroup v1 还是 v2。&lt;/li&gt;
&lt;li&gt;设置一系列超时参数&lt;/li&gt;
&lt;li&gt;加载 plugin，containerd 通过 plugin 来划分模块。&lt;/li&gt;
&lt;li&gt;通过设置的 plugin 目录（默认在 /var/lib/containerd/plugins）来加载。go1.8 之后就不支持了。&lt;/li&gt;
&lt;li&gt;注册 content plugin：containred 架构中 storage 部分的 content。负责镜像的存储&lt;/li&gt;
&lt;li&gt;注册 metadata plugin：使用的是 bolt 这个嵌入式的 key/value 数据库。依赖 content 和 snapshot plugin。&lt;/li&gt;
&lt;li&gt;注册 proxy plugin: proxy plugin 支持 content, snapshot 两种类型。相当于起了一个 GRPC 服务，替换掉内置的 content, snapshot plugin。&lt;/li&gt;
&lt;li&gt;还有很多 plugin 是在包的 init 方法中注册的
&lt;ul&gt;
&lt;li&gt;大量 snapshot 的插件: aufs, btrfs, devmapper, native, overlayfs, zfs&lt;/li&gt;
&lt;li&gt;diff 插件: walking&lt;/li&gt;
&lt;li&gt;GC 插件&lt;/li&gt;
&lt;li&gt;大量 service 插件：introspection，containers，content，diff，images，leases，namespaces，snapshots，tasks&lt;/li&gt;
&lt;li&gt;runtime 插件：linux，task&lt;/li&gt;
&lt;li&gt;monitoring 插件：cgroups&lt;/li&gt;
&lt;li&gt;internal 插件：restart，opt&lt;/li&gt;
&lt;li&gt;GRPC 插件：containers，content，diff，events，healthcheck，images，leases，namespaces，snapshots，tasks，version，introspection&lt;/li&gt;
&lt;li&gt;CRI 插件：实现 CRI，提供给 kubelet 调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;diff 模块注册 stream processor，支持两种&lt;/li&gt;
&lt;li&gt;application/vnd.oci.image.layer.v1.tar+encrypted&lt;/li&gt;
&lt;li&gt;application/vnd.oci.image.layer.v1.tar+gzip+encrypted&lt;/li&gt;
&lt;li&gt;启动 TTRPC server：/run/containerd/containerd.sock.ttrpc&lt;/li&gt;
&lt;li&gt;启动 GRPC server：/run/containerd/containerd.sock&lt;/li&gt;
&lt;li&gt;如果开启了 TCP，还会将 GRPC server 监听到 tcp 上。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时，containerd 已经可以对外提供服务了。下面对上述提到的一些概念或模块做个简单的解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;service: GRPC 服务依赖于对应的 service。service 是则用来封装内部的实现。&lt;/li&gt;
&lt;li&gt;TTRPC: TTRPC 是为低内存环境做的优化，通过淘汰&lt;code&gt;net/http&lt;/code&gt;, &lt;code&gt;net/http2&lt;/code&gt; 和 &lt;code&gt;grpc&lt;/code&gt; 包，实现了更轻量的 framing protocol，实现了更小的二进制文件以及更少的常驻内存使用。&lt;/li&gt;
&lt;li&gt;storage 模块：包含 content, snapshot 和 diff 三个子模块。
&lt;ul&gt;
&lt;li&gt;content: content 是负责整个镜像的存储流程的。&lt;/li&gt;
&lt;li&gt;snapshot：为了保证镜像层的数据不可变，所以在运行容器时会从 layer 中创建 snapshot。&lt;/li&gt;
&lt;li&gt;diff：diff 模块实现了两个功能： diff 和 apply。diff 用来比较上层和下层之间的差异，然后按照 OCI 规范对该层打包。apply 用来根据底层的联合文件系统，对某一层进行挂载。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;metadata 模块: 包含 images 和 containers 两个子模板。
&lt;ul&gt;
&lt;li&gt;images: 存储镜像相关的元数据。&lt;/li&gt;
&lt;li&gt;containers: 存储容器相关的元数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;tasks 模块：一个 container 的运行被抽象成一个 task&lt;/li&gt;
&lt;li&gt;event 模块：提供了事件的发布订阅功能。第三方可以通过 event 获取 containerd 中的事件，比如镜像的创建，更新，容器的创建，删除等等。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>容器技术</category><author>joyme123</author></item><item><title>容器镜像是如何工作的</title><link>https://www.myway5.com/blog/container-image/</link><guid isPermaLink="true">https://www.myway5.com/blog/container-image/</guid><description>容器技术近些年的发展非常迅速，其本身使用的技术并非多么高深，但是带来的生产力提升是业界公认的。容器技术的主要特点就是其资源限制和环境隔离，linux 容器主要使用 cgroup 和 namespace 技术，但如果要更深入的理解容器技术，还需要了解一下容器镜像是什么。 二、初探</description><pubDate>Tue, 18 May 2021 12:46:11 GMT</pubDate><content:encoded>&lt;h2&gt;一、概述&lt;/h2&gt;
&lt;p&gt;容器技术近些年的发展非常迅速，其本身使用的技术并非多么高深，但是带来的生产力提升是业界公认的。容器技术的主要特点就是其资源限制和环境隔离，linux 容器主要使用 cgroup 和 namespace 技术，但如果要更深入的理解容器技术，还需要了解一下容器镜像是什么。&lt;/p&gt;
&lt;h2&gt;二、初探&lt;/h2&gt;
&lt;p&gt;docker 可以说是容器技术的代表了，下面会以 docker 为例。docker 支持 pull, build, push 镜像。 Pull 操作如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  docker pull ubuntu:20.04
20.04: Pulling from library/ubuntu
345e3491a907: Pull complete   # 镜像共有三层
57671312ef6f: Pull complete
5e9250ddb7d0: Pull complete
Digest: sha256:cf31af331f38d1d7158470e095b132acd126a7180a54f263d386da88eb681d93 # 镜像的摘要
Status: Downloaded newer image for ubuntu:20.04
docker.io/library/ubuntu:20.04 # 镜像地址
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Build 镜像时，需要提供 Dockerfile，Dockerfile 如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;FROM ubuntu:20.04
RUN touch hello
CMD [&apos;sh&apos;, &apos;-c&apos;, &apos;echo hello ubuntu&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后开始构建我们自己的镜像&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  docker build -t joyme/ubuntu-hello:0.1 .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM ubuntu:20.04
 ---&amp;gt; 7e0aa2d69a15
Step 2/3 : RUN touch hello
 ---&amp;gt; Running in 5e2fcef8c28b
Removing intermediate container 5e2fcef8c28b
 ---&amp;gt; 4bacc0d61995
Step 3/3 : CMD [&apos;sh&apos;, &apos;-c&apos;, &apos;echo hello ubuntu&apos;]
 ---&amp;gt; Running in 802e795e28c4
Removing intermediate container 802e795e28c4
 ---&amp;gt; ff588b7779d8
Successfully built ff588b7779d8
Successfully tagged joyme/ubuntu-hello:0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;push 我们自己的镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  docker push joyme/ubuntu-hello:0.1
The push refers to repository [docker.io/joyme/ubuntu-hello]
59c67359ad17: Pushed
2f140462f3bc: Mounted from library/ubuntu # 下面三层都是来自于 ubuntu:20.04
63c99163f472: Mounted from library/ubuntu
ccdbb80308cc: Mounted from library/ubuntu
0.1: digest: sha256:0a1a5857cade488bfc60c7f5d2be2c7c5eee7f90edc1950c4c32214fada31a7d size: 1149
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们也可以把镜像保存成 tar 包，然后加载。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  docker save -o ubuntu-hello.tar joyme/ubuntu-hello:0.1
➜  ls
Dockerfile  ubuntu-hello.tar
➜  tar xvf ubuntu-hello.tar
1392a7609ae8d845eba5fbe95e266a6b104d55b30262a284c960583f91307420/
1392a7609ae8d845eba5fbe95e266a6b104d55b30262a284c960583f91307420/VERSION
1392a7609ae8d845eba5fbe95e266a6b104d55b30262a284c960583f91307420/json
1392a7609ae8d845eba5fbe95e266a6b104d55b30262a284c960583f91307420/layer.tar
15cbe1c29902a1020a4a47c835a82f0416f1896f02fac942fdd35d326c63fa22/
15cbe1c29902a1020a4a47c835a82f0416f1896f02fac942fdd35d326c63fa22/VERSION
15cbe1c29902a1020a4a47c835a82f0416f1896f02fac942fdd35d326c63fa22/json
15cbe1c29902a1020a4a47c835a82f0416f1896f02fac942fdd35d326c63fa22/layer.tar
2a8749a3d9080a156a4381fea67ce47a478fb31d6acaa20e19fda1ba0c8f20e7/
2a8749a3d9080a156a4381fea67ce47a478fb31d6acaa20e19fda1ba0c8f20e7/VERSION
2a8749a3d9080a156a4381fea67ce47a478fb31d6acaa20e19fda1ba0c8f20e7/json
2a8749a3d9080a156a4381fea67ce47a478fb31d6acaa20e19fda1ba0c8f20e7/layer.tar
6d56becb66b184f78b25f61dc91f68fcfce4baeecb3a8dcb21ada2306091aab7/
6d56becb66b184f78b25f61dc91f68fcfce4baeecb3a8dcb21ada2306091aab7/VERSION
6d56becb66b184f78b25f61dc91f68fcfce4baeecb3a8dcb21ada2306091aab7/json
6d56becb66b184f78b25f61dc91f68fcfce4baeecb3a8dcb21ada2306091aab7/layer.tar
ff588b7779d8b10861c566538b885c379d633d059fc067c5ec6e1ab026427075.json
manifest.json
repositories
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现，镜像是分层的，比如上面的 ubuntu:20.04 镜像总共有三层，我们基于 ubuntu:20.04 制作的 ubuntu-hello:0.1 镜像是 4 层，层信息记录在 manifest.json 文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;➜  cat manifest.json | jq
[
  {
    &quot;Config&quot;: &quot;ff588b7779d8b10861c566538b885c379d633d059fc067c5ec6e1ab026427075.json&quot;,
    &quot;RepoTags&quot;: [
      &quot;joyme/ubuntu-hello:0.1&quot;
    ],
    &quot;Layers&quot;: [
      &quot;15cbe1c29902a1020a4a47c835a82f0416f1896f02fac942fdd35d326c63fa22/layer.tar&quot;,
      &quot;6d56becb66b184f78b25f61dc91f68fcfce4baeecb3a8dcb21ada2306091aab7/layer.tar&quot;,
      &quot;1392a7609ae8d845eba5fbe95e266a6b104d55b30262a284c960583f91307420/layer.tar&quot;,
      &quot;2a8749a3d9080a156a4381fea67ce47a478fb31d6acaa20e19fda1ba0c8f20e7/layer.tar&quot;
    ]
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2a8749a3 是我们刚刚创建的最后一层，也就是 touch hello 生成的新文件。而最后一行 CMD 由于并不会改变文件信息，因此不会单独的存储成一层，而是记录在和 touch hello 同一层中的 json 配置文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;    &quot;Cmd&quot;: [
      &quot;/bin/sh&quot;,
      &quot;-c&quot;,
      &quot;#(nop) &quot;,
      &quot;CMD [\&quot;/bin/sh\&quot; \&quot;-c\&quot; \&quot;[&apos;sh&apos;, &apos;-c&apos;, &apos;echo hello ubuntu&apos;]\&quot;]&quot;
    ],
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、镜像的本地存储&lt;/h2&gt;
&lt;p&gt;当使用 docker pull 镜像到本地后，镜像的存储位置一般都在 &lt;code&gt;/var/lib/docker/image/&amp;lt;storage_driver&amp;gt;&lt;/code&gt; 下。因为要考虑到镜像层的复用等等场景， pull 下来的镜像肯定不会是一个压缩包。镜像存储时基本要满足以下几个场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存储当期机器上所有的镜像索引。&lt;/li&gt;
&lt;li&gt;存储镜像到层的映射，这样使用镜像时可以找到层的信息。&lt;/li&gt;
&lt;li&gt;按照层来存储，这样在 pull 镜像时，如果有重复的层，可以避免多次拉取。&lt;/li&gt;
&lt;li&gt;需要知道一个层被哪些镜像引用。这样在删除镜像时，可以确定这个镜像的某个层能不能被删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;镜像存储的目录分布如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;├── distribution
│   ├── diffid-by-digest
│   └── v2metadata-by-diffid
├── imagedb
│   ├── content
│   └── metadata
├── layerdb
│   ├── mounts
│   ├── sha256
│   └── tmp
└── repositories.json
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;repositories.json: 存了的镜像的 repo 信息以及镜像信息。&lt;/li&gt;
&lt;li&gt;distribution 下存储了和镜像分发相关的信息。
&lt;ul&gt;
&lt;li&gt;diffid-by-digest：存储了 digest 到 diffid 的映射，digest 用来在拉取层的时候，对比远端的层本地是否有&lt;/li&gt;
&lt;li&gt;v2metadata-by-diffid 存储了 diffid 到层的元数据信息。元数据中记录了层的 digest，repo 等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;imagedb 下存储了和镜像相关的信息。
&lt;ul&gt;
&lt;li&gt;content 下存储的是镜像的配置信息。也是符合 OCI 的规范的。可以参考：&lt;a href=&quot;https://github.com/opencontainers/image-spec/blob/master/config.md&quot; rel=&quot;noopener&quot;&gt;https://github.com/opencontainers/image-spec/blob/master/config.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;metadata 存储了镜像之间的 parent 信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;layerdb 下存储了和镜像层相关的信息
&lt;ul&gt;
&lt;li&gt;mounts: 存储了层的的挂载信息，包括 init-id, mount-id, parent&lt;/li&gt;
&lt;li&gt;sha256: 存储了层的信息，根据层的 sha256 划分目录&lt;/li&gt;
&lt;li&gt;tmp:&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在可以知道，镜像的 repo, name, tag, id 等信息，可以通过 repositories.json 知道，使用的镜像的时候，提供 image+tag 找到 id，或者直接提供 id 也可以。比如上面的 ubuntu-hello 的 id 为 &lt;code&gt;ff588b7779d8...&lt;/code&gt;，然后通过这个 id，在 &lt;code&gt;/var/lib/docker/image/overlay2/imagedb/content/sha256&lt;/code&gt;下就可以找到名为这个 id 的文件了，里面存储了镜像的具体信息。这个 id 就是这个文件的 sha256sum 后的值。比如 &lt;code&gt;ubuntu-hello&lt;/code&gt;的 content 就存储了以下信息（有一定的精简）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;architecture&quot;: &quot;amd64&quot;,
  &quot;config&quot;: {
    &quot;Env&quot;: [
      &quot;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&quot;
    ],
    &quot;Cmd&quot;: [
      &quot;/bin/sh&quot;,
      &quot;-c&quot;,
      &quot;[&apos;sh&apos;, &apos;-c&apos;, &apos;echo hello ubuntu&apos;]&quot;
    ],
    &quot;Image&quot;: &quot;sha256:4bacc0d6199516a20039bc06ddaa7247a7553a39ae700ade279bd6ce78cd0a61&quot;
  },
  &quot;container&quot;: &quot;802e795e28c4afced8fb6e6e703f0efeeaa6fa07a36c4400e242c4cd65e4bff5&quot;,
  &quot;history&quot;: [
    {
      &quot;created&quot;: &quot;2021-05-15T10:32:23.665502338Z&quot;,
      &quot;created_by&quot;: &quot;/bin/sh -c touch hello&quot;
    },
    {
      &quot;created&quot;: &quot;2021-05-15T10:32:23.925742465Z&quot;,
      &quot;created_by&quot;: &quot;/bin/sh -c #(nop)  CMD [\&quot;/bin/sh\&quot; \&quot;-c\&quot; \&quot;[&apos;sh&apos;, &apos;-c&apos;, &apos;echo hello ubuntu&apos;]\&quot;]&quot;,
      &quot;empty_layer&quot;: true
    }
  ],
  &quot;os&quot;: &quot;linux&quot;,
  &quot;rootfs&quot;: {
    &quot;type&quot;: &quot;layers&quot;,
    &quot;diff_ids&quot;: [
      &quot;sha256:ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439&quot;,
      &quot;sha256:63c99163f47292f80f9d24c5b475751dbad6dc795596e935c5c7f1c73dc08107&quot;,
      &quot;sha256:2f140462f3bcf8cf3752461e27dfd4b3531f266fa10cda716166bd3a78a19103&quot;,
      &quot;sha256:59c67359ad1702b424dcf3deefdf137e92ef13c13bec5b878b08fe66683a78f7&quot;
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们现在重点关注 &lt;code&gt;rootfs&lt;/code&gt; 字段下的 diff_ids，这里存储了该镜像的每一层的 diff_id。接下来就可以通过这些 id 找到这些层的信息了。 层的信息存储在 &lt;code&gt;/var/lib/docker/image/overlay2/layerdb/sha256&lt;/code&gt; 下，目录名是这一层的 chain_id。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;chain_id 的计算中，用到了所有祖先 layer 的信息，从而能保证根据 chain_id 得到的 rootfs 是唯一的。比如我在debian和ubuntu的image基础上都添加了一个同样的文件，那么commit之后新增加的这两个layer具有相同的内容，相同的diff_id，但由于他们的父layer不一样，所以他们的chain_id会不一样，从而根据chainid能找到唯一的rootfs。 chain_id 的计算方式如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ChainID(L₀) =  DiffID(L₀)
ChainID(L₀|...|Lₙ₋₁|Lₙ) = Digest(ChainID(L₀|...|Lₙ₋₁) + &quot; &quot; + DiffID(Lₙ))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如：第一层的 diff_id 是 ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439，第一层的 chain_id 是 ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439，第二层的 diff_id 是 63c99163f47292f80f9d24c5b475751dbad6dc795596e935c5c7f1c73dc08107，那么第二层的 chain_id 可以用下面的方法计算： echo -n &quot;sha256:ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439 sha256:63c99163f47292f80f9d24c5b475751dbad6dc795596e935c5c7f1c73dc08107&quot; | sha256sum 也就是：8d8dceacec7085abcab1f93ac1128765bc6cf0caac334c821e01546bd96eb741&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;8d8dceacec7085abcab1f93ac1128765bc6cf0caac334c821e01546bd96eb741&lt;/code&gt;目录下的文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cache-id  diff  parent  size  tar-split.json.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;cache-id 是该层存储目录的名字。比如该 cache-id 的内容为 &lt;code&gt;52cb2ec8a0ac5a2418c568896fc079cc29a0da7d7b6b0c4b740d13241581d6e8&lt;/code&gt;，对应的位置是 &lt;code&gt;/var/lib/docker/overlay2/52cb2ec8a0ac5a2418c568896fc079cc29a0da7d7b6b0c4b740d13241581d6e8&lt;/code&gt;，这里才是这一层文件的实际位置。cache-id 是随机生成的。&lt;/li&gt;
&lt;li&gt;diff: 存储的是 diff_id，这里就是 &lt;code&gt;ccdbb80308cc5ef43b605ac28fac29c6a597f89f5a169bbedbb8dec29c987439&lt;/code&gt;。diff_id 是 layer.tar 的 sha256sum 的值。&lt;/li&gt;
&lt;li&gt;parent: 记录了上一层的 chain_id。&lt;/li&gt;
&lt;li&gt;size: 这一层的大小。&lt;/li&gt;
&lt;li&gt;tar-split.json.gz：&lt;a href=&quot;https://github.com/vbatts/tar-split&quot; rel=&quot;noopener&quot;&gt;vbatts/tar-split&lt;/a&gt; 这个库生成的文件，主要为了 image pull 之后，重新推送可能出现的 layer checksum 不一致的问题。具体可见:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/moby/moby/pull/14067&quot; rel=&quot;noopener&quot;&gt;https://github.com/moby/moby/pull/14067&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/moby/moby/issues/14018&quot; rel=&quot;noopener&quot;&gt;https://github.com/moby/moby/issues/14018&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/distribution/distribution/issues/634&quot; rel=&quot;noopener&quot;&gt;https://github.com/distribution/distribution/issues/634&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;四、镜像的使用&lt;/h2&gt;
&lt;p&gt;根据上面的探索可以知道，提供 image+tag 或者 image_id，就可以找到关于这个镜像的所有信息。那么镜像提供的这些文件是如何被使用的呢？最简单的做法就是，将所有层的信息合并成 rootfs，提供给 runc 使用就可以了。 那么如何把这些层的信息高效的组合成 rootfs ？答案就是联合文件系统（UnionFS）。联合文件系统是一种分层、轻量级并且高性能的文件系统，它支持对文件系统的修改作为一次提交来一层层的叠加，同时可以将不同目录挂载到同一个虚拟文件系统下。 这里我们还是拿 docker 来说，docker 目前支持的联合文件系统有：overlay2，aufs，fuse-overlayfs，devicemapper，btrfs，zfs，vfs。目前推荐的是 overlay2 ，下面会使用 overlay2 来讲解容器是如何使用镜像的。 &lt;img src=&quot;/uploads/wp/2021/05/overlay_constructs.jpg&quot; alt=&quot;overlayfs&quot; /&gt; 如上图所示，overlay2 主要有三个目录&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lowerdir 对应了 image layers，图上只花了一层，但实际上是支持多层的，lowerdir 是只读的，也就是说 image layers 中所有的文件都不会被修改。&lt;/li&gt;
&lt;li&gt;upperdir 对应了 container layer，这个是我们运行容器时创建出来的。upperdir 是支持读写的&lt;/li&gt;
&lt;li&gt;merged 对应了 container mount，这一层是容器内的文件系统视图。也就是说，会把 image layer 和 container layer 在逻辑上合并起来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;overlay2 其实还有一个目录 workdir，workdir 是为了在某些场景下，为了保证操作的原子性而设计的。具体的可以参考：&lt;a href=&quot;https://blog.csdn.net/luckyapple1028/article/details/78075358&quot; rel=&quot;noopener&quot;&gt;深入理解overlayfs（二）：使用与原理分析&lt;/a&gt; 这里我们举几个例子帮助理解。我们通过 image 运行容器后，此时 image layer 如上图所示，但 container layer 是空的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在容器内删除 file2，只会在 container layer 中创建一个名为 file2 的 whiteout 文件，image layer 的 file2 不会被真正的删除。容器内 file2 不可见。&lt;/li&gt;
&lt;li&gt;在容器内重新创建 file2，只会在 container layer 创建 file2。&lt;/li&gt;
&lt;li&gt;在容器内修改 file1，会触发 copy-up，将 image layer 的 file1 拷贝到 container layer 中并修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可见，在 container layer 的文件操作比普通的文件系统操作消耗更大，特别是修改 image layer 的大文件会触发复制。 我们可以使用 docker inspect 来看一下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;GraphDriver&quot;: {
  &quot;Data&quot;: {
    &quot;LowerDir&quot;: &quot;/data00/docker/overlay2/379f2fd25bc3e317b5f767ea1bf061174013c0aa80a9454c6f01d47722dbd270-init/diff:/data00/docker/overlay2/a80f1234038ffa4720876215a77c16fd42d67fd0f110d9bd984dca73ea03b49b/diff:/data00/docker/overlay2/2aac2c4faa0be785d9d0faa5127f635bbc4dc65eb635bc442da4e03b46f05814/diff:/data00/docker/overlay2/afd88f520ed7fde849d8b7b136261348cd165bed4a6796382188529e08426685/diff:/data00/docker/overlay2/081774340b851380f09ad2d0286d8cfad388bf6738673ee089ed06efbed295c0/diff:/data00/docker/overlay2/651cf1972f63c01679beac3705e1d984d7dcae01ed4e6ae6c76bd7326b97d1c0/diff:/data00/docker/overlay2/2fc2f5bf53a5aaaec137666c6988573b3c3887a63eb5594ccbcd07995d0d3e5f/diff:/data00/docker/overlay2/0d323f1368ced5d62a56e7dd447db7cc4032b598a23473d31d5e812d2fed2376/diff:/data00/docker/overlay2/d07016559096eb4a59d4fe1e97ed7b26a492156f764542ff15ccbf53624ff7d2/diff:/data00/docker/overlay2/db070da7fe3eb36cd5a70fbcd5d72fed192d2d25d27cd5d9f7cfa26fb9de7bd0/diff:/data00/docker/overlay2/cabe60077f68da04a6d460cac3ef339bde6919fc267d808fd52950bbc5f4891d/diff:/data00/docker/overlay2/f1368e926209187e11312e4b805c9b1d912ac64776eada90e5115b245f5ab54a/diff&quot;,
    &quot;MergedDir&quot;: &quot;/data00/docker/overlay2/379f2fd25bc3e317b5f767ea1bf061174013c0aa80a9454c6f01d47722dbd270/merged&quot;,
    &quot;UpperDir&quot;: &quot;/data00/docker/overlay2/379f2fd25bc3e317b5f767ea1bf061174013c0aa80a9454c6f01d47722dbd270/diff&quot;,
    &quot;WorkDir&quot;: &quot;/data00/docker/overlay2/379f2fd25bc3e317b5f767ea1bf061174013c0aa80a9454c6f01d47722dbd270/work&quot;
  },
  &quot;Name&quot;: &quot;overlay2&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;了解了上述的知识后，我们就可以对 docker commit 的机制做一些猜想了：应该就是将 upperdir 作为新的 lowerdir 进行挂载。 参考： &lt;a href=&quot;https://blog.csdn.net/luckyapple1028/article/details/78075358&quot; rel=&quot;noopener&quot;&gt;深入理解overlayfs（二）：使用与原理分析&lt;/a&gt; &lt;a href=&quot;https://github.com/opencontainers/distribution-spec/blob/main/spec.md#appendix&quot; rel=&quot;noopener&quot;&gt;Open Container Initiative Distribution Specification&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>容器技术</category><author>joyme123</author></item><item><title>读懂 go 的 panic 信息</title><link>https://www.myway5.com/blog/go-panic/</link><guid isPermaLink="true">https://www.myway5.com/blog/go-panic/</guid><description>作为一个经常写 go 的程序员，肯定时不时会看到 go 的 panic 信息。一般我们都可以根据信息轻松定位到出错的代码行数，但也因为这个原因，往往忽视了其他的一些信息。这篇文章主要来分析，go 程序 panic 时输出的错误信息到底如何理解？ 例子分析</description><pubDate>Mon, 22 Mar 2021 16:36:52 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;作为一个经常写 go 的程序员，肯定时不时会看到 go 的 panic 信息。一般我们都可以根据信息轻松定位到出错的代码行数，但也因为这个原因，往往忽视了其他的一些信息。这篇文章主要来分析，go 程序 panic 时输出的错误信息到底如何理解？&lt;/p&gt;
&lt;h2&gt;例子分析&lt;/h2&gt;
&lt;p&gt;下面我们先用一个非常简单的例子来说明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;fmt&quot;
)

type Person struct {
    name string
    age  int
}

func (person *Person) say(words []string) (ret int) {
    for i := range words {
        ret++
        fmt.Printf(&quot;%s say: %s&quot;, person.name, words[i])
    }
    return
}

func main() {
    var person *Person
    words := []string{
        &quot;hello&quot;,
        &quot;world&quot;,
    }
    person.say(words)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子一运行就会 panic，信息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x493373]

goroutine 1 [running]:
main.(*Person).say(0x0, 0xc000072f58, 0x2, 0x2, 0x0)
    /home/jiangpengfei.jiangpf/projects/panic_demo/main.go:15 +0x43
main.main()
    /home/jiangpengfei.jiangpf/projects/panic_demo/main.go:26 +0x7d
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;panic: runtime error: invalid memory address or nil pointer dereference&lt;/code&gt;。这句话表面了这是一个 panic 异常，并且可能原因是无效的内存地址或空指针的解引用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x493373]&lt;/code&gt;。&lt;code&gt;SIGSEGV&lt;/code&gt; 当一个进程执行了一个无效的内存引用，或发生段错误时发送给它的信号。&lt;code&gt;0x1&lt;/code&gt;对应的是&lt;code&gt;SEGV_MAPERR&lt;/code&gt;，也就是指&lt;code&gt;地址未找到对象&lt;/code&gt;。&lt;code&gt;addr&lt;/code&gt;是&lt;code&gt;0x8&lt;/code&gt;，这并不是一个有效的内存地址。&lt;code&gt;pc&lt;/code&gt;是程序计数器，用来指向下一条指令存放的位置。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;goroutine 1 [running]&lt;/code&gt;。&lt;code&gt;1&lt;/code&gt;是 goroutine 的 ID。&lt;code&gt;running&lt;/code&gt;代表该协程在发生异常时的状态。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;main.(*Person).say(0x0, 0xc000072f58, 0x2, 0x2, 0x0)&lt;/code&gt;。&lt;code&gt;main&lt;/code&gt;是 package，&lt;code&gt;(*Person)&lt;/code&gt;是类型，&lt;code&gt;say&lt;/code&gt;是方法，我们可以看到，say 的调用共有 5 的参数。但代码中，say 的调用其实只有 1 个参数。其实呢，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第 1 参数是 receiver，这里就是 &lt;code&gt;*Person&lt;/code&gt;。值为 &lt;code&gt;0x0&lt;/code&gt;说明它是一个空指针。&lt;/li&gt;
&lt;li&gt;第2~4个参数是 &lt;code&gt;[]string&lt;/code&gt;，我们都知道，slice 这个数据结构是有三个字段组成：pointer, len, cap。pointer(0xc000072f58) 是指向实际内存地址的指针，len(0x2) 是长度，cap(0x2) 是容量。&lt;/li&gt;
&lt;li&gt;第 5 个参数是返回值。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;/home/jiangpengfei.jiangpf/projects/panic_demo/main.go:14 +0x43&lt;/code&gt;。这里标出了出错的代码位置以及行号。&lt;code&gt;+0x43&lt;/code&gt; 又代表什么含义呢？说实话我没有搜索到相关的信息。但是找到了如下的 go runtime 的代码[go/src/runtime/traceback.go:439]。frame 代表当前栈帧，f 代表当前函数，entry 是该函数的 start pc。因此可以知道 &lt;code&gt;+0x43&lt;/code&gt; 代表的是栈内的 pc 偏移量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;if frame.pc &amp;gt; f.entry {
print(&quot; +&quot;, hex(frame.pc-f.entry))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;至此，一个简单的 panic 例子分析完毕。但是这里我们还有两个没有说明白的事情。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;panic 时的参数到底如何分析，比如上面说到 slice 类型的参数在 panic 时，会输出 pointer, len, cap 这三个信息。其实也就是，&lt;strong&gt;panic 中会输出 go 类型的内存布局信息&lt;/strong&gt;。那么对于其他的类型又是什么样子的呢？&lt;/li&gt;
&lt;li&gt;panic 时，go runtime 是如何做到收集并打印出上述的所有信息呢？&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;go 类型的内存布局&lt;/h2&gt;
&lt;p&gt;这一部分主要参考：&lt;a href=&quot;https://research.swtch.com/godata&quot; rel=&quot;noopener&quot;&gt;Go Data Structures&lt;/a&gt; 和 &lt;a href=&quot;https://research.swtch.com/interfaces&quot; rel=&quot;noopener&quot;&gt;Go Data Structures: Interfaces&lt;/a&gt;。大家也可以直接看这两篇文章。&lt;/p&gt;
&lt;h3&gt;基本类型&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2021/03/godata1.png&quot; alt=&quot;basic type&quot; /&gt; 基本类型的内存布局很好理解，不多阐述。&lt;/p&gt;
&lt;h3&gt;结构体和指针&lt;/h3&gt;
&lt;p&gt;结构体的内存布局是按照成员变量的顺序来的。当然，还会有一些内存对齐的优化，不过不属于本篇文章的范围。 比如下面的 Point 结构体。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Point struct { X, Y int }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其内存布局如下：。 &lt;img src=&quot;/uploads/wp/2021/03/godata1a.png&quot; alt=&quot;struct and pointer&quot; /&gt; 对于成员变量非基本类型，内存布局如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Rect1 struct { Min, Max Point }
type Rect2 struct { Min, Max *Point }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2021/03/godata1b_1.png&quot; alt=&quot;struct and pointer&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;字符串&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2021/03/godata2.png&quot; alt=&quot;string&quot; /&gt; 字符串主要由：pointer 和 len 组成，其中 pointer 指向 byte 数组的内存首地址。同时，go 中的 string 是不可变的，因此多个字符串共享同一块内存区域是安全的。&lt;/p&gt;
&lt;h3&gt;切片&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2021/03/godata3.png&quot; alt=&quot;slice&quot; /&gt; slice 也是引用到数组的一块内存地址上，由三个字段组成：pointer, len 和 cap&lt;/p&gt;
&lt;h3&gt;interface&lt;/h3&gt;
&lt;p&gt;(下面是基于 32 位机器而言)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Stringer interface {
    String() string
}

func ToString(any interface{}) string {
    if v, ok := any.(Stringer); ok {
        return v.String()
    }
    switch v := any.(type) {
    case int:
        return strconv.Itoa(v)
    case float:
        return strconv.Ftoa(v, &apos;g&apos;, -1)
    }
    return &quot;???&quot;
}
type Binary uint64

func (i Binary) String() string {
    return strconv.Uitob64(i.Get(), 2)
}

func (i Binary) Get() uint64 {
    return uint64(i)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面定义了 &lt;code&gt;Stringer&lt;/code&gt; 类型，&lt;code&gt;Binary&lt;/code&gt; 实现了 &lt;code&gt;Stringer&lt;/code&gt; 类型。 &lt;img src=&quot;/uploads/wp/2021/03/gointer1.png&quot; alt=&quot;&quot; /&gt; 那么，图中 b 的内存布局如下。将 b 做类型转换成 s 后，内存布局如下： &lt;img src=&quot;/uploads/wp/2021/03/gointer2.png&quot; alt=&quot;&quot; /&gt; 这里，tab 指向了一个 itable&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;itable 中的 type 指向了底层类型(Binary)，因此 s.tab-&amp;gt;type 就可以获取该 interface 的类型。&lt;/li&gt;
&lt;li&gt;itable 的 func 数组中只保存实现了 Stringer 的方法指针。所以 Get() 并不会保存在这里。&lt;/li&gt;
&lt;li&gt;在调用 s.String() 时，等同于调用 &lt;code&gt;s.tab-&amp;gt;func[0](s.data)&lt;/code&gt;。将 s.data 作为第一个参数，传进函数调用中。这里还需要注意一点，因为函数调用传进去的是 s.data，而 s.data 的类型是 *Binary。因此 func[0] 是 &lt;code&gt;(*Binary).String&lt;/code&gt; 而不是 &lt;code&gt;(Binary).String&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;data 是一个指针，指向了 Binary(200)。需要注意的是，这里并不是指向了原始的 b，而是 b 的拷贝。 当然，并不是所有情况都如此，比如说，如果将 b 转换成一个没有方法的 interface，这里就可以对内存做一下优化。 &lt;img src=&quot;/uploads/wp/2021/03/gointer3.png&quot; alt=&quot;&quot; /&gt; 此时，因为没有 func 列表，所以就不要单独为 itable 在堆上分配一块内存了。any.type 直接指向一个类型就可以了。同样的，如果 data 正好是 32位（和机器的寻址大小一致），那么也可以通过直接将值保存在 data 中来进行优化。 &lt;img src=&quot;/uploads/wp/2021/03/gointer4.png&quot; alt=&quot;&quot; /&gt; 下面是两种优化都可以享受的情况： &lt;img src=&quot;/uploads/wp/2021/03/gointer5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参数更多的例子分析&lt;/h2&gt;
&lt;p&gt;在了解到上述的 go 类型的内存布局之后，下面看一个更多参数的函数调用 panic。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

//go:noinline
func test1(a int, b []int, c string, d [2]int64) error {
    panic(&quot;test1&quot;)
}

func main() {
    a := 0
    b := make([]int, 3, 7)
    b[0], b[1], b[2] = 1, 2, 3
    c := &quot;c&quot;
    d := [2]int64{5, 6}

    test1(a, b, c, d)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，这里使用了 &lt;code&gt;//go:noinline&lt;/code&gt; 来防止 go 编译时对函数进行内联，这样 panic 后就会丢失参数信息。panic 信息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;main.test1(0x0, 0xc00003a740, 0x3, 0x7, 0x475848, 0x1, 0x5, 0x6, 0x4046eb, 0xc000058000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解释如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;main.test1(0x0, // a 的值
           0xc00003a710, // b 指向的内存地址
           0x3, // b 的 len
           0x7, // b 的 cap
           0x475a48, // c 指向的内存地址
           0x1, // c 的长度
           0x5, // d[0]
           0x6, // d[1]
           0x4046eb, // 
           0xc000058000) // error 的值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们希望看到没有优化过的 error 值。可以使用下面的方式来运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;go run -gcflags &apos;-N -l&apos; main.go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的话，最后两个参数就都是 &lt;code&gt;0x0&lt;/code&gt;。再来一个和结构体相关的例子:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

type speaker interface {
    say()
}

type person struct {
    name string
    age  int
}

func (p person) say() {
}

//go:noinline
func test2(p1 person, p2 *person, p3 speaker) error {
    panic(&quot;test2&quot;)
    return nil
}

func main() {
    p1 := person{&quot;p&quot;, 11}
    p2 := new(person)
    p3 := speaker(p1)

    test2(p1, p2, p3)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;go run -gcflags &apos;-N -l&apos; main.go&lt;/code&gt; 运行后 panic 信息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;main.test2(0x475a48, 0x1, 0xb, 0xc00003a748, 0x487200, 0xc00003a730, 0x0, 0x0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解释如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;main.test2(0x475a49, // person.name 指向的内存地址
           0x1, // person.name 的长度
           0xb, // person.age 的值
           0xc00003a748, // p2 的指针值
           0x487200, // p3 指向的 itable 中的 type
           0xc00003a730, // p3 指向的 p1 的拷贝的内存地址
           0x0, // error 的类型
           0x0) // error 的值
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;go panic 后的执行过程&lt;/h2&gt;
&lt;p&gt;goroutine 上发生 panic 后，会进入函数退出阶段。以手动调用 &lt;code&gt;panic&lt;/code&gt; 为例。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先，go runtime 会将该 panic 放到 goroutine 的 panic 链表首。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;var p _panic
p.arg = e       // arg 是 panic 的参数
p.link = gp._panic // link 指向更早的 panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&amp;amp;p))) // gp 是当前 goroutine 协程
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;接着，会依次执行 goroutine 上的 defer。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;for {
    d := gp._defer
    if d == nil {
        break
    }
   ...
   ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果该 defer 之前已经执行过了。则直接忽略该 defer&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// If defer was started by earlier panic or Goexit (and, since we&apos;re back here, that triggered a new panic),
// take defer off list. The earlier panic or Goexit will not continue running.
if d.started {
 if d._panic != nil {
   d._panic.aborted = true
 }
 d._panic = nil
 d.fn = nil
 gp._defer = d.link
 freedefer(d)
 continue
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行 defer 对应的函数调用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 defer 函数中遇到了 recover()，还会执行以下代码。当然，这里只会检查 recover() 调用是否有效&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;p != nil。当前确实发生 panic 了。&lt;/li&gt;
&lt;li&gt;!p.recovered。当前的 panic 没有恢复&lt;/li&gt;
&lt;li&gt;argp uintptr(p.argp)。argp 是调用者的参数指针，p.argp 是 defer 的参数。&lt;/li&gt;
&lt;li&gt;如果上述条件之一不满足，就返回 nil，表示这个 recover() 是无效的。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func gorecover(argp uintptr) interface{} {
// Must be in a function running as part of a deferred call during the panic.
// Must be called from the topmost function of the call
// (the function used in the defer statement).
// p.argp is the argument pointer of that topmost deferred function call.
// Compare against argp reported by caller.
// If they match, the caller is the one who can recover.
gp := getg()
p := gp._panic
if p != nil &amp;amp;&amp;amp; !p.recovered &amp;amp;&amp;amp; argp == uintptr(p.argp) {
    p.recovered = true
    return p.arg
}
return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上一步虽然是 recover() 调用，但并没有 recover 的逻辑，只是给当前 panic 标记了 recovered=true。所以可以执行到下面这个判断。通过 &lt;code&gt;mcall(recovery)&lt;/code&gt; 来执行真正的恢复逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;if p.recovered {
        atomic.Xadd(&amp;amp;runningPanicDefers, -1)

        gp._panic = p.link
        // Aborted panics are marked but remain on the g.panic list.
        // Remove them from the list.
        for gp._panic != nil &amp;amp;&amp;amp; gp._panic.aborted {
            gp._panic = gp._panic.link
        }
        if gp._panic == nil { // must be done with signal
            gp.sig = 0
        }
        // Pass information about recovering frame to recovery.
        gp.sigcode0 = uintptr(sp)
        gp.sigcode1 = pc
        mcall(recovery)
        throw(&quot;recovery failed&quot;) // mcall should not return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;恢复实现如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Unwind the stack after a deferred function calls recover
// after a panic. Then arrange to continue running as though
// the caller of the deferred function returned normally.
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1

// d&apos;s arguments need to be in the stack.
if sp != 0 &amp;amp;&amp;amp; (sp &amp;lt; gp.stack.lo || gp.stack.hi &amp;lt; sp) {
    print(&quot;recover: &quot;, hex(sp), &quot; not in [&quot;, hex(gp.stack.lo), &quot;, &quot;, hex(gp.stack.hi), &quot;]\n&quot;)
    throw(&quot;bad recovery&quot;)
}

// Make the deferproc for this d return again,
// this time returning 1.  The calling function will
// jump to the standard return epilogue.
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
gogo(&amp;amp;gp.sched)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果没有恢复逻辑的话，就执行到输出异常信息的地方了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// ran out of deferred calls - old-school panic now
// Because it is unsafe to call arbitrary user code after freezing
// the world, we call preprintpanics to invoke all necessary Error
// and String methods to prepare the panic strings before startpanic.
preprintpanics(gp._panic)

fatalpanic(gp._panic) // should not return
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;preprintpanics&lt;/code&gt; 负责准备要打印的信息。即如果 panic 参数是 error，就从 v.Error() 中获取错误信息。如果参数实现了 stringer，则调用 String() 来获取字符串信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Call all Error and String methods before freezing the world.
// Used when crashing with panicking.
func preprintpanics(p *_panic) {
defer func() {
    if recover() != nil {
        throw(&quot;panic while printing panic value&quot;)
    }
}()
for p != nil {
    switch v := p.arg.(type) {
    case error:
        p.arg = v.Error()
    case stringer:
        p.arg = v.String()
    }
    p = p.link
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fatalpanic&lt;/code&gt; 开始最后的异常信息输出。首先递归调用 &lt;code&gt;printpanics&lt;/code&gt; 来打印 panic 的参数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Print all currently active panics. Used when crashing.
// Should only be called after preprintpanics.
func printpanics(p *_panic) {
    if p.link != nil {
        printpanics(p.link)
        print(&quot;\t&quot;)
    }
    print(&quot;panic: &quot;)
    printany(p.arg)
    if p.recovered {
        print(&quot; [recovered]&quot;)
    }
    print(&quot;\n&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后调用 &lt;code&gt;dopanic_m&lt;/code&gt; 来打印异常调用栈，然后 &lt;code&gt;exit(2)&lt;/code&gt;退出&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>go</category><author>joyme123</author></item><item><title>一个kube config 管理工具-kubecm</title><link>https://www.myway5.com/blog/kubecm-intro/</link><guid isPermaLink="true">https://www.myway5.com/blog/kubecm-intro/</guid><description>kubecm 全称是 kube config manager，主要用来管理 kube config 文件的。使用场景是在我们做 k8s 相关的开发时，有的时候会存在各种各样的集群环境，这时无论是从集群中获取 kubeconfig 文件，还是做 kubeconfig 文件的切换，…</description><pubDate>Thu, 11 Mar 2021 16:28:54 GMT</pubDate><content:encoded>&lt;h2&gt;kubecm&lt;/h2&gt;
&lt;p&gt;kubecm 全称是 kube config manager，主要用来管理 kube config 文件的。使用场景是在我们做 k8s 相关的开发时，有的时候会存在各种各样的集群环境，这时无论是从集群中获取 kubeconfig 文件，还是做 kubeconfig 文件的切换，都是非常麻烦的一件事情。 kubecm 可以方便的帮助你从多个途径导入 kubeconfig 文件并管理起来。你也可以使用 kubecm 来快速切换 kubeconfig。 项目在 github 上：&lt;a href=&quot;https://github.com/joyme123/kubecm&quot; rel=&quot;noopener&quot;&gt;https://github.com/joyme123/kubecm&lt;/a&gt; &lt;img src=&quot;https://raw.githubusercontent.com/joyme123/kubecm/master/resource/term.gif&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;go get github.com/joyme123/kubecm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者在这里找到二进制文件下载：&lt;a href=&quot;https://github.com/joyme123/kubecm/releases&quot; rel=&quot;noopener&quot;&gt;https://github.com/joyme123/kubecm/releases&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;列出所有的配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubecm list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导致配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 从本地文件系统中导入
kubecm import -n dev_129_cluster -l /tmp/configs/config_dev_182_cluster

# 通过带 password 的 ssh 导入。
kubecm import dev_0_101_cluster --from=ssh://root@192.168.0.101:/etc/kubernetes/kubectl.kubeconfig  -p mypassword

# 通过带证书的 ssh 导入，默认读取 $HOME/.ssh/id_rsa
kubecm import dev_0_102_cluster --from=ssh://root@192.168.0.102:/etc/kubernetes/kubectl.kubeconfig 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubecm use -n dev_129_cluster
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重命名配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubecm rename -n dev_129_cluster -t dev_cluster
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubecm remove -n dev_129_cluster
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>flannel 的多种 backend 实现分析</title><link>https://www.myway5.com/blog/flannel-multi-backend-implementation/</link><guid isPermaLink="true">https://www.myway5.com/blog/flannel-multi-backend-implementation/</guid><description>flannel 是一个较简单的网络插件，其支持多种网络方案，可以使用 etcd 或者 k8s 作为存储，来实现 docker 或者 k8s 的网络。其支持的网络方案 backend 有: hostgw udp vxlan ipip ipsec</description><pubDate>Wed, 23 Dec 2020 08:51:42 GMT</pubDate><content:encoded>&lt;h2&gt;一、概述&lt;/h2&gt;
&lt;p&gt;flannel 是一个较简单的网络插件，其支持多种网络方案，可以使用 etcd 或者 k8s 作为存储，来实现 docker 或者 k8s 的网络。其支持的网络方案 backend 有:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hostgw&lt;/li&gt;
&lt;li&gt;udp&lt;/li&gt;
&lt;li&gt;vxlan&lt;/li&gt;
&lt;li&gt;ipip&lt;/li&gt;
&lt;li&gt;ipsec&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时也支持了多家云厂商的网络环境：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AliVPC&lt;/li&gt;
&lt;li&gt;AWS VPC&lt;/li&gt;
&lt;li&gt;GCE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面会简单介绍 flannel 的工作原理，并主要就标准环境下的网络方案 backend 做分析。&lt;/p&gt;
&lt;h2&gt;二、flannel 网络方案&lt;/h2&gt;
&lt;p&gt;flannel 的部署文件中包含一个 configmap，其中包含 flannel cni 的配置文件，以及 flannel 需要的 cluster-cidr 和使用的 backend 配置。flannel 通过 daemonset 调度到每个节点上。flannel 的 pod 有一个 init 容器，负责将 configmap 中的 cni-conf.json 复制到宿主机上的 &lt;code&gt;/etc/cni/net.d/10-flannel.conflist&lt;/code&gt;。之后 flanneld 启动，其拥有 &lt;code&gt;NET_ADMIN&lt;/code&gt; 和&lt;code&gt;NET_RAW&lt;/code&gt; 的 capabilities。 这里需要注意的是，如果你的默认路由对应的网卡不是 node 使用的网卡（比如使用 vagrant 部署 k8s 时，虚拟机的 eth0 是默认的 nat 网卡，但是不用在 k8s 集群中），应该使用 &lt;code&gt;--iface=eth1&lt;/code&gt; 来指定使用的网卡。 flannel 会根据 cluster-cidr 来为每个 node 分配单独的子网，这样就能保证不同 node 上的 pod ip 不会冲突。然后根据配置文件中不同的 backend 来注册网络。下面就开始简单分析不同 backend 的工作原理。其中&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;IPIP 和 VXLAN 类似，不过 VXLAN 封装的是二层的帧，IPIP 封装的是 IP 包。这里不做分析。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;UDP 使用的很少，也不做分析。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IPSEC 关注的是通信安全方面。不是这里关注的重点。不做分析。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了方便后续的描述，这里先列举出整个集群的概况: cluster cidr: 172.10.0.0/16 master: 192.168.33.101，子网是 172.10.100.0/24 node1: 192.168.33.102，子网是 172.10.0.0/24 node2: 192.168.33.103，子网是 172.10.1.0/24&lt;/p&gt;
&lt;h3&gt;2.1 host-gw&lt;/h3&gt;
&lt;p&gt;host-gw 是最简单的 backend，所有的 pod 都会被接入到虚拟网桥 cni0 上，然后它通过监听 subnet 的更新，来动态的更新 host 上的路由表。通过路由来实现不同 node 上的 pod 间通信以及 node 和 pod 间的通信。如下图所示： &lt;img src=&quot;/uploads/wp/2020/10/flannel-hostgw.png&quot; alt=&quot;flannel-hostgw&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Node1 上的 pod A(172.10.0.134) 和 node2 上的 pod B(172.10.1.3) 通信时，A 根据 namespace 下的路由规则&lt;code&gt;default via 172.10.0.1 dev eth0&lt;/code&gt;将流量发往网关 cni0 到达宿主机。&lt;/li&gt;
&lt;li&gt;根据宿主机路由规则 &lt;code&gt;172.10.1.0/24 via 192.168.33.103 dev eth1&lt;/code&gt; ，通过网卡 eth1 发往 192.168.33.103 这个网关，而这个网关正好是 node2 的 eth1 网卡 ip。&lt;/li&gt;
&lt;li&gt;node2 此时扮演网关的角色，根据路由规则 &lt;code&gt;172.10.1.0/24 dev cni0 proto kernel scope link src 172.10.1.1&lt;/code&gt;， 通过 cni0 发送。使用 arp 找到目标 ip 对应的 mac 地址。将二层的目标 mac 地址替换成 pod B 的 mac 地址。将二层的源 mac 地址替换成 cni0 的 mac 地址。&lt;/li&gt;
&lt;li&gt;cni0 是个 bridge 设备。根据 mac 表来将流量从对应端口发送到 pod B 中。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为通信过程的第 2 步需要将其他 node 作为网关，因此 hostgw 需要所有 node 二层互通。&lt;/p&gt;
&lt;h3&gt;2.2 VXLAN&lt;/h3&gt;
&lt;p&gt;相比于 host-gw 必须要二层互通。VXLAN 是个 overlay 的网络实现，只需三层互通即可。在 flannel 的实现中，并没有使用 VXLAN 的全部能力，仅仅用它来做二层包的封装和解封装。其整个流程图如下： &lt;img src=&quot;/uploads/wp/2020/12/flannel-vxlan-1.png&quot; alt=&quot;flannel-vxlan&quot; /&gt; 可以发现，相比于 host-gw，增加了 flannel.1 这个设备。这个 flannel 的进程在启动的时候创建的。同时它还会监听所有的子网，每个节点加入网络中，都会创建一个属于自己的子网。flannel 进程在监听到新的子网创建时，会在当前节点创建以下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一条路由：&lt;code&gt;172.10.0.0/24 via 172.10.0.0 dev flannel.1&lt;/code&gt;。172.10.0.0 的 IP 是其他节点的 flannel.1 地址。&lt;/li&gt;
&lt;li&gt;一条 ARP: &lt;code&gt;172.10.0.0 ether ee:9a:f8:a5:3c:02 CM flannel.1&lt;/code&gt;。在包通过路由发出去前，需要知道 172.10.0.0 的二层地址。这时就会匹配这条 ARP 记录。&lt;/li&gt;
&lt;li&gt;一条 FDB：&lt;code&gt;ee:9a:f8:a5:3c:02 dev flannel.1 dst 192.168.33.102 self permanent&lt;/code&gt;。这条 FDB 记录会匹配二层的转发路径。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了更好的理解 flannel 的 vxlan 实现，我们按照图中的步骤一步步分析。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Pod B (172.10.1.3) 向 Pod A (172.10.0.134) 发送数据。因为 Pod A 和 Pod B 的 IP 不在一个子网，因此走默认路由表，发向 &lt;code&gt;172.10.1.1&lt;/code&gt;。这个地址是 cni0 的地址，因此可以直接发过去。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IP 包到达 cni0 网桥后，根据主机路由表 &lt;code&gt;172.10.0.0/24 via 172.10.0.0 dev flannel.1&lt;/code&gt;，下一跳是 172.10.0.0，通过 flannel.1 发送。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此时需要知道 172.10.0.0 的 mac 地址，因此检查主机的 arp 表。发现 &lt;code&gt;172.10.0.0 ether ee:9a:f8:a5:3c:02 CM flannel.1&lt;/code&gt;，因此要发送的帧如下： &lt;img src=&quot;/uploads/wp/2020/12/ethernet-frame.png&quot; alt=&quot;ethernet-frame&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;二层帧的转发需要查找主机的 fdb 表。这里匹配到 &lt;code&gt;ee:9a:f8:a5:3c:02 dev flannel.1 dst 192.168.33.102 self permanent&lt;/code&gt;。封装成 vxlan 的包从 eth1 发出去。发出去的包如下： &lt;img src=&quot;/uploads/wp/2020/12/vxlan.png&quot; alt=&quot;vxlan&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对端的 eth1 网络收到包，发现是 vxlan，于是会对包解封装。二层地址是 flannel.1 设备的 mac 地址。因此发到 flannel.1 上。 &lt;img src=&quot;/uploads/wp/2020/12/ethernet-frame.png&quot; alt=&quot;ethernet-frame&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此时三层目标地址是 172.10.0.134，因此匹配主机的路由表 &lt;code&gt;172.10.0.0/24 dev cni0 proto kernel scope link src 172.10.0.1&lt;/code&gt;。这个路由表没有写在上图中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cni0 和我们的 pod 是二层互通的。因此将包发给 pod。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pod 收到包。三层的来源地址是 172.10.1.3，二层的来源地址是 cni0 的 mac 地址。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以通过以下命令行，模拟整个流程。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# host1
br0_ip=&quot;10.20.1.1&quot;
vtep_ip=&quot;10.20.1.0/32&quot;
endpoint_ip=&quot;10.20.1.4/24&quot;
sudo ip link add name br0 type bridge forward_delay 1500 hello_time 200 max_age 2000 vlan_protocol 802.1Q
sudo ip addr add $br0_ip/24 dev br0
sudo ip link add name vtep0 type vxlan id 1 dev ens33 srcport 0 0 dstport 4789 nolearning proxy ageing 300
sudo ip addr add $vtep_ip dev vtep0
sudo ip link add name veth0 type veth peer name veth1
sudo ip netns add n1
sudo ip link set veth1 netns n1
sudo ip link set veth0 master br0
sudo ip netns exec n1 ip addr add $endpoint_ip dev veth1
sudo ip netns exec n1 ip link set veth1 up
sudo ip netns exec n1 ip route add default via $br0_ip dev veth1
sudo ip link set veth0 up
sudo ip link set br0 up
sudo ip link set vtep0 up

# host2
br0_ip=&quot;10.20.2.1&quot;
vtep_ip=&quot;10.20.2.0/32&quot;
endpoint_ip=&quot;10.20.2.4/24&quot;
sudo ip link add name br0 type bridge forward_delay 1500 hello_time 200 max_age 2000 vlan_protocol 802.1Q
sudo ip addr add $br0_ip/24 dev br0
sudo ip link add name vtep0 type vxlan id 1 dev ens33 srcport 0 0 dstport 4789 nolearning proxy ageing 300
sudo ip addr add $vtep_ip dev vtep0
sudo ip link add name veth0 type veth peer name veth1
sudo ip netns add n1
sudo ip link set veth1 netns n1
sudo ip link set veth0 master br0
sudo ip netns exec n1 ip addr add $endpoint_ip dev veth1
sudo ip netns exec n1 ip link set veth1 up
sudo ip netns exec n1 ip route add default via $br0_ip dev veth1
sudo ip link set veth0 up
sudo ip link set br0 up
sudo ip link set vtep0 up


# host1
host2_vtep_mac=&quot;f2:a4:1f:4e:5c:51&quot;
host2_vtep_ip=&quot;10.20.2.0&quot;
subnet_mask=&quot;24&quot;
host2_ip=&quot;192.168.105.167&quot;
# one route
sudo ip route add $host2_vtep_ip/$subnet_mask via $host2_vtep_ip dev vtep0 onlink
# one arp
sudo arp -i vtep0 -s $host2_vtep_ip $host2_vtep_mac
# one fdb
sudo bridge fdb add $host2_vtep_mac dev vtep0 dst $host2_ip

# host2
host1_vtep_mac=&quot;be:ae:0d:f3:da:77&quot;
host1_vtep_ip=&quot;10.20.1.0&quot;
subnet_mask=&quot;24&quot;
host1_ip=&quot;192.168.105.166&quot;
# one route
sudo ip route add $host1_vtep_ip/$subnet_mask via $host1_vtep_ip dev vtep0 onlink
# one arp
sudo arp -i vtep0 -s $host1_vtep_ip $host1_vtep_mac
# one fdb
sudo bridge fdb add $host1_vtep_mac dev vtep0 dst $host1_ip

# host1 and host2
echo &quot;1&quot; &amp;gt; /proc/sys/net/ipv4/ip_forward

# host1
ip netns exec n1 ping 10.20.2.4
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>k8s</category><category>网络</category><author>joyme123</author></item><item><title>apiserver 处理请求的过程</title><link>https://www.myway5.com/blog/apiserver-process-request/</link><guid isPermaLink="true">https://www.myway5.com/blog/apiserver-process-request/</guid><description>k8s 的 apiserver 作为所有组件通信的枢纽，其重要性不言而喻。apiserver 可以对外提供基于 HTTP 的服务，那么一个请求从发出到处理，具体要经过哪些步骤呢？下面会根据代码将整个过程简单的叙述一遍，让大家可以对这个过程由大概的印象。</description><pubDate>Thu, 22 Oct 2020 15:44:17 GMT</pubDate><content:encoded>&lt;h2&gt;1. 概述&lt;/h2&gt;
&lt;p&gt;k8s 的 apiserver 作为所有组件通信的枢纽，其重要性不言而喻。apiserver 可以对外提供基于 HTTP 的服务，那么一个请求从发出到处理，具体要经过哪些步骤呢？下面会根据代码将整个过程简单的叙述一遍，让大家可以对这个过程由大概的印象。 因为 apiserver 的代码结构并不简单，因此会尽量少的贴代码。以下分析基于 k8s 1.18&lt;/p&gt;
&lt;h2&gt;2. 请求的处理链&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 构建请求的处理链
func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
   handler := genericapifilters.WithAuthorization(apiHandler, c.Authorization.Authorizer, c.Serializer)
   if c.FlowControl != nil {
      handler = genericfilters.WithPriorityAndFairness(handler, c.LongRunningFunc, c.FlowControl)
   } else {
      handler = genericfilters.WithMaxInFlightLimit(handler, c.MaxRequestsInFlight, c.MaxMutatingRequestsInFlight, c.LongRunningFunc)
   }
   handler = genericapifilters.WithImpersonation(handler, c.Authorization.Authorizer, c.Serializer)
   handler = genericapifilters.WithAudit(handler, c.AuditBackend, c.AuditPolicyChecker, c.LongRunningFunc)
   failedHandler := genericapifilters.Unauthorized(c.Serializer, c.Authentication.SupportsBasicAuth)
   failedHandler = genericapifilters.WithFailedAuthenticationAudit(failedHandler, c.AuditBackend, c.AuditPolicyChecker)
   handler = genericapifilters.WithAuthentication(handler, c.Authentication.Authenticator, failedHandler, c.Authentication.APIAudiences)
   handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, &quot;true&quot;)
   handler = genericfilters.WithTimeoutForNonLongRunningRequests(handler, c.LongRunningFunc, c.RequestTimeout)
   handler = genericfilters.WithWaitGroup(handler, c.LongRunningFunc, c.HandlerChainWaitGroup)
   handler = genericapifilters.WithRequestInfo(handler, c.RequestInfoResolver)
   if c.SecureServing != nil &amp;amp;&amp;amp; !c.SecureServing.DisableHTTP2 &amp;amp;&amp;amp; c.GoawayChance &amp;gt; 0 {
      handler = genericfilters.WithProbabilisticGoaway(handler, c.GoawayChance)
   }
   handler = genericfilters.WithPanicRecovery(handler)
   return handler
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个请求的处理链是从后向前执行的。因此请求经过的 handler 为:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PanicRecovery&lt;/li&gt;
&lt;li&gt;ProbabilisticGoaway&lt;/li&gt;
&lt;li&gt;RequestInfo&lt;/li&gt;
&lt;li&gt;WaitGroup&lt;/li&gt;
&lt;li&gt;TimeoutForNonLongRunningRequests&lt;/li&gt;
&lt;li&gt;CORS&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;failedHandler: FailedAuthenticationAudit&lt;/li&gt;
&lt;li&gt;failedHandler: Unauthorized&lt;/li&gt;
&lt;li&gt;Audit&lt;/li&gt;
&lt;li&gt;Impersonation&lt;/li&gt;
&lt;li&gt;PriorityAndFairness / MaxInFlightLimit&lt;/li&gt;
&lt;li&gt;Authorization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;之后传递到 director，由 director 分到 gorestfulContainer 或 nonGoRestfulMux。gorestfulContainer 是 apiserver 主要部分。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;director := director{
   name:               name,
   goRestfulContainer: gorestfulContainer,
   nonGoRestfulMux:    nonGoRestfulMux,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PanicRecovery&lt;/h3&gt;
&lt;p&gt;runtime.HandleCrash 防止 panic，并打了日志记录 panic 的请求详情&lt;/p&gt;
&lt;h3&gt;ProbabilisticGoaway&lt;/h3&gt;
&lt;p&gt;因为 client 和 apiserver 是使用 http2 长连接的。这样即使 apiserver 有负载均衡，部分 client 的请求也会一直命中到同一个 apiserver 上。goaway 会配置一个很小的几率，在 apiserver 收到请求后响应 GOWAY 给 client，这样 client 就会新建一个 tcp 连接负载均衡到不同的 apiserver 上。这个几率的取值范围是 0~0.02 相关的 PR：&lt;a href=&quot;https://github.com/kubernetes/kubernetes/pull/88567&quot; rel=&quot;noopener&quot;&gt;https://github.com/kubernetes/kubernetes/pull/88567&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;RequestInfo&lt;/h3&gt;
&lt;p&gt;RequestInfo 会根据 HTTP 请求进行解析处理。得到以下的信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// RequestInfo holds information parsed from the http.Request
type RequestInfo struct {
    // IsResourceRequest indicates whether or not the request is for an API resource or subresource
    IsResourceRequest bool
    // Path is the URL path of the request
    Path string
    // Verb is the kube verb associated with the request for API requests, not the http verb.  This includes things like list and watch.
    // for non-resource requests, this is the lowercase http verb
    Verb string

    APIPrefix  string
    APIGroup   string
    APIVersion string
    Namespace  string
    // Resource is the name of the resource being requested.  This is not the kind.  For example: pods
    Resource string
    // Subresource is the name of the subresource being requested.  This is a different resource, scoped to the parent resource, but it may have a different kind.
    // For instance, /pods has the resource &quot;pods&quot; and the kind &quot;Pod&quot;, while /pods/foo/status has the resource &quot;pods&quot;, the sub resource &quot;status&quot;, and the kind &quot;Pod&quot;
    // (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource &quot;pods&quot;, subresource &quot;binding&quot;, and kind &quot;Binding&quot;.
    Subresource string
    // Name is empty for some verbs, but if the request directly indicates a name (not in body content) then this field is filled in.
    Name string
    // Parts are the path parts for the request, always starting with /{resource}/{name}
    Parts []string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;WaitGroup&lt;/h3&gt;
&lt;p&gt;waitgroup 用来处理短连接退出的。 如何判断是不是一个长连接呢？这里是通过请求的动作或者 subresource 来判断的。watch 和 proxy 这两个动作是在 requestinfo 上通过请求的 path 来判断的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;serverConfig.LongRunningFunc = filters.BasicLongRunningRequestCheck(
  sets.NewString(&quot;watch&quot;, &quot;proxy&quot;),
  sets.NewString(&quot;attach&quot;, &quot;exec&quot;, &quot;proxy&quot;, &quot;log&quot;, &quot;portforward&quot;),
)

// BasicLongRunningRequestCheck returns true if the given request has one of the specified verbs or one of the specified subresources, or is a profiler request.
func BasicLongRunningRequestCheck(longRunningVerbs, longRunningSubresources sets.String) apirequest.LongRunningRequestCheck {
    return func(r *http.Request, requestInfo *apirequest.RequestInfo) bool {
        if longRunningVerbs.Has(requestInfo.Verb) {
            return true
        }
        if requestInfo.IsResourceRequest &amp;amp;&amp;amp; longRunningSubresources.Has(requestInfo.Subresource) {
            return true
        }
        if !requestInfo.IsResourceRequest &amp;amp;&amp;amp; strings.HasPrefix(requestInfo.Path, &quot;/debug/pprof/&quot;) {
            return true
        }
        return false
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样之后的 handler 全部退出后，这个 waitgroup 的 handler 才会 done。这样就能实现优雅退出了。&lt;/p&gt;
&lt;h3&gt;TimeoutForNonLongRunningRequests&lt;/h3&gt;
&lt;p&gt;对于非长连接的请求，使用 ctx 的 cancel 来在超时后取消请求。&lt;/p&gt;
&lt;h3&gt;CORS&lt;/h3&gt;
&lt;p&gt;设置一些跨域的响应头&lt;/p&gt;
&lt;h3&gt;Authentication&lt;/h3&gt;
&lt;p&gt;开始认证用户。认证成功会从请求中移除 &lt;code&gt;Authorization&lt;/code&gt;。然后将请求交给下一个 handler，否则将请求交给下一个 failed handler。 处理的方式有很多中。包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Requestheader，负责从请求中取出 X-Remote-User，X-Remote-Group，X-Remote-Extra&lt;/li&gt;
&lt;li&gt;X509 证书校验，&lt;/li&gt;
&lt;li&gt;BearerToken&lt;/li&gt;
&lt;li&gt;WebSocket&lt;/li&gt;
&lt;li&gt;Anonymous: 在允许匿名的情况下&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有一部分是以插件的形式提供了认证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;bootstrap token&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Basic auth&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;password&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OIDC&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Webhook&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果有一个认证成功的话，就认为认证成功。并且如果用户是 &lt;code&gt;system:anonymous&lt;/code&gt; 或 用户组中包含 &lt;code&gt;system:unauthenticated&lt;/code&gt; 和 &lt;code&gt;system:authenticated&lt;/code&gt;。就直接返回，否则修改用户信息并返回：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;r.User = &amp;amp;user.DefaultInfo{
        Name:   r.User.GetName(),
        UID:    r.User.GetUID(),
        Groups: append(r.User.GetGroups(), user.AllAuthenticated),
        Extra:  r.User.GetExtra(),
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意到，user 现在已经属于 &lt;code&gt;system:authenticated&lt;/code&gt;。也就是认证过了。&lt;/p&gt;
&lt;h3&gt;FailedAuthenticationAudit&lt;/h3&gt;
&lt;p&gt;这个只会在认证失败后才会执行。主要是提供了审计的功能。&lt;/p&gt;
&lt;h3&gt;Unauthorized&lt;/h3&gt;
&lt;p&gt;未授权的处理，在 FailedAuthenticationAudit 之后调用&lt;/p&gt;
&lt;h3&gt;Audit&lt;/h3&gt;
&lt;p&gt;提供请求的审计功能&lt;/p&gt;
&lt;h3&gt;Impersonation&lt;/h3&gt;
&lt;p&gt;impersonation 是一个将当前用户扮演为另外一个用户的特性，这个特性有助于管理员来测试不同用户的权限是否配置正确等等。取得 header 的 key 是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Impersonate-User：用户&lt;/li&gt;
&lt;li&gt;Impersonate-Group：组&lt;/li&gt;
&lt;li&gt;Impersonate-Extra-：额外信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用户分为 service account 和 user。根据格式区分，service account 的格式是 namespace/name，否则就是当作 user 对待。 Service account 最终的格式是： system:serviceaccount:namespace:name&lt;/p&gt;
&lt;h3&gt;PriorityAndFairness / MaxInFlightLimit&lt;/h3&gt;
&lt;p&gt;如果设置了流控，就使用 PriorityAndFairness，否则使用 MaxInFlightLimit。 PriorityAndFairness：会对请求做优先级的排序。同优先级的请求会有公平性相关的控制。 MaxInFlightLimit：在给定时间内进行中不可变请求的最大数量。当超过该值时，服务将拒绝所有请求。0 值表示没有限制。（默认值 400） 参考资料：&lt;a href=&quot;https://kubernetes.io/zh/docs/concepts/cluster-administration/flow-control/&quot; rel=&quot;noopener&quot;&gt;https://kubernetes.io/zh/docs/concepts/cluster-administration/flow-control/&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Authorization&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// AttributesRecord implements Attributes interface.
type AttributesRecord struct {
   User            user.Info
   Verb            string
   Namespace       string
   APIGroup        string
   APIVersion      string
   Resource        string
   Subresource     string
   Name            string
   ResourceRequest bool
   Path            string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;鉴权的时候会从 context 中取出上面这个结构体需要的信息，然后进行认证。支持的认证方式有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Always allow&lt;/li&gt;
&lt;li&gt;Always deny&lt;/li&gt;
&lt;li&gt;Path: 允许部分路径总是可以被访问&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他的一些常用的认证方式主要是通过插件提供：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;li&gt;RBAC&lt;/li&gt;
&lt;li&gt;Node&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中 Node 专门为 kubelet 设计的，节点鉴权器允许 kubelet 执行 API 操作。包括： 读取操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;services&lt;/li&gt;
&lt;li&gt;endpoints&lt;/li&gt;
&lt;li&gt;nodes&lt;/li&gt;
&lt;li&gt;pods&lt;/li&gt;
&lt;li&gt;secrets、configmaps、pvcs 以及绑定到 kubelet 节点的与 pod 相关的持久卷&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;写入操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点和节点状态（启用 &lt;code&gt;NodeRestriction&lt;/code&gt; 准入插件以限制 kubelet 只能修改自己的节点）&lt;/li&gt;
&lt;li&gt;Pod 和 Pod 状态 (启用 &lt;code&gt;NodeRestriction&lt;/code&gt; 准入插件以限制 kubelet 只能修改绑定到自身的 Pod)&lt;/li&gt;
&lt;li&gt;事件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;鉴权相关操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于基于 TLS 的启动引导过程时使用的 certificationsigningrequests API 的读/写权限&lt;/li&gt;
&lt;li&gt;为委派的身份验证/授权检查创建 tokenreviews 和 subjectaccessreviews 的能力&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在将来的版本中，节点鉴权器可能会添加或删除权限，以确保 kubelet 具有正确操作所需的最小权限集。 为了获得节点鉴权器的授权，kubelet 必须使用一个凭证以表示它在 &lt;code&gt;system:nodes&lt;/code&gt; 组中，用户名为 &lt;code&gt;system:node:&amp;lt;nodeName&amp;gt;&lt;/code&gt;。 上述的组名和用户名格式要与 &lt;a href=&quot;https://kubernetes.io/zh/docs/reference/command-line-tools-reference/kubelet-tls-bootstrapping/&quot; rel=&quot;noopener&quot;&gt;kubelet TLS 启动引导&lt;/a&gt;过程中为每个 kubelet 创建的标识相匹配。&lt;/p&gt;
&lt;h3&gt;director&lt;/h3&gt;
&lt;p&gt;director 的 ServeHTTP 方法定义如下，也就是会根据定义的 webservice 匹配规则进行转发。否则就调用 nonGoRestfulMux 进行处理。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (d director) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    path := req.URL.Path

    // check to see if our webservices want to claim this path
    for _, ws := range d.goRestfulContainer.RegisteredWebServices() { q 
        switch {
        case ws.RootPath() == &quot;/apis&quot;:
            // if we are exactly /apis or /apis/, then we need special handling in loop.
            // normally these are passed to the nonGoRestfulMux, but if discovery is enabled, it will go directly.
            // We can&apos;t rely on a prefix match since /apis matches everything (see the big comment on Director above)
            if path == &quot;/apis&quot; || path == &quot;/apis/&quot; {
                klog.V(5).Infof(&quot;%v: %v %q satisfied by gorestful with webservice %v&quot;, d.name, req.Method, path, ws.RootPath())
                // don&apos;t use servemux here because gorestful servemuxes get messed up when removing webservices
                // TODO fix gorestful, remove TPRs, or stop using gorestful
                d.goRestfulContainer.Dispatch(w, req)
                return
            }

        case strings.HasPrefix(path, ws.RootPath()):
            // ensure an exact match or a path boundary match
            if len(path) == len(ws.RootPath()) || path[len(ws.RootPath())] == &apos;/&apos; {
                klog.V(5).Infof(&quot;%v: %v %q satisfied by gorestful with webservice %v&quot;, d.name, req.Method, path, ws.RootPath())
                // don&apos;t use servemux here because gorestful servemuxes get messed up when removing webservices
                // TODO fix gorestful, remove TPRs, or stop using gorestful
                d.goRestfulContainer.Dispatch(w, req)
                return
            }
        }
    }

    // if we didn&apos;t find a match, then we just skip gorestful altogether
    klog.V(5).Infof(&quot;%v: %v %q satisfied by nonGoRestful&quot;, d.name, req.Method, path)
    d.nonGoRestfulMux.ServeHTTP(w, req)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;admission webhook&lt;/h3&gt;
&lt;p&gt;在请求真正被处理前，还差最后一步，就是我们的 admission webhook。admission 的调用是在具体的 REST 的处理代码中，在 create, update 和 delete 时，会先调用 mutate，然后再调用 validating。k8s 本身就内置了很多的 admission，以插件的形式提供，具体如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AlwaysAdmit&lt;/li&gt;
&lt;li&gt;AlwaysPullImages&lt;/li&gt;
&lt;li&gt;LimitPodHardAntiAffinityTopology&lt;/li&gt;
&lt;li&gt;CertificateApproval/CertificateSigning/CertificateSubjectRestriction&lt;/li&gt;
&lt;li&gt;DefaultIngressClass&lt;/li&gt;
&lt;li&gt;DefaultTolerationSeconds&lt;/li&gt;
&lt;li&gt;ExtendedResourceToleration&lt;/li&gt;
&lt;li&gt;OwnerReferencesPermissionEnforcement&lt;/li&gt;
&lt;li&gt;ImagePolicyWebhook&lt;/li&gt;
&lt;li&gt;LimitRanger&lt;/li&gt;
&lt;li&gt;NamespaceAutoProvision&lt;/li&gt;
&lt;li&gt;NamespaceExists&lt;/li&gt;
&lt;li&gt;NodeRestriction&lt;/li&gt;
&lt;li&gt;TaintNodesByCondition&lt;/li&gt;
&lt;li&gt;PodNodeSelector&lt;/li&gt;
&lt;li&gt;PodPreset&lt;/li&gt;
&lt;li&gt;PodTolerationRestriction&lt;/li&gt;
&lt;li&gt;Priority&lt;/li&gt;
&lt;li&gt;ResourceQuota&lt;/li&gt;
&lt;li&gt;RuntimeClass&lt;/li&gt;
&lt;li&gt;PodSecurityPolicy&lt;/li&gt;
&lt;li&gt;SecurityContextDeny&lt;/li&gt;
&lt;li&gt;ServiceAccount&lt;/li&gt;
&lt;li&gt;PersistentVolumeLabel&lt;/li&gt;
&lt;li&gt;PersistentVolumeClaimResize&lt;/li&gt;
&lt;li&gt;DefaultStorageClass&lt;/li&gt;
&lt;li&gt;StorageObjectInUseProtection&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 如何阅读 apiserver 的相关代码&lt;/h2&gt;
&lt;p&gt;我看的是仓库是 &lt;a href=&quot;https://github.com/kubernetes/kubernetes&quot; rel=&quot;noopener&quot;&gt;https://github.com/kubernetes/kubernetes&lt;/a&gt;。apiserver 的代码主要分散在以下几个位置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cmd/kube-apiserver: apiserver main 函数入口。主要封装了很多的启动参数。&lt;/li&gt;
&lt;li&gt;pkg/kubeapiserver: 提供了 kube-apiserver 和 federation-apiserve 共用的代码，但是不属于 generic API server。&lt;/li&gt;
&lt;li&gt;plugin/pkg: 这下面都是和认证，鉴权以及准入控制相关的插件代码&lt;/li&gt;
&lt;li&gt;staging/src/apiserver: 这里面是 apiserver 的核心代码。其下面的 pkg/server 是服务的启动入口。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>kubernetes 中的认证和授权</title><link>https://www.myway5.com/blog/kubernetes-authn-authz/</link><guid isPermaLink="true">https://www.myway5.com/blog/kubernetes-authn-authz/</guid><description>kubernetes 中有两种用户类型：服务账户（service account)和 普通用户(user)。这两种用户类型对应了两种使用场景。 服务账户提供给在集群中运行的 pod，当这些 pod 要和 apiserver 通信时，就是使用 serviceaccount 来认证…</description><pubDate>Sun, 18 Oct 2020 18:08:26 GMT</pubDate><content:encoded>&lt;h2&gt;一、概述&lt;/h2&gt;
&lt;p&gt;kubernetes 中有两种用户类型：**服务账户（service account)**和 &lt;strong&gt;普通用户(user)&lt;/strong&gt;。这两种用户类型对应了两种使用场景。 服务账户提供给在集群中运行的 pod，当这些 pod 要和 apiserver 通信时，就是使用 serviceaccount 来认证和授权。服务账户是存储在 k8s 集群中的，基于 RBAC，可以和角色进行绑定，从而拥有特定资源的特定权限。 普通用户是非 pod 的场景下用来做认证和授权。比如像 k8s 的一些关键组件: scheduler, kubelet 和 controller manager，包括使用 kubectl 和 k8s 集群做交互。&lt;/p&gt;
&lt;h2&gt;二、服务账户&lt;/h2&gt;
&lt;h3&gt;2.1 自动化&lt;/h3&gt;
&lt;p&gt;即使我们不在 namespace 下创建服务账户，也不为 pod 绑定任何的服务账户，pod 的 serviceAccount 字段也会被设置为 default。任何 namespace 下都会有这样的服务账户。我们可以查看这个名为 default 的服务账户，它会对应一个 secret。secret 中记录了 ca.crt，namespace 和 token 这三个值。 这整个过程由三个组件完成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务账户准入控制器（Service account admission controller）&lt;/li&gt;
&lt;li&gt;Token 控制器（Token controller）&lt;/li&gt;
&lt;li&gt;服务账户控制器（Service account controller）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，服务账户控制器负责在每个 namespace 下维护默认的服务账户。这样当新的 namespace 被创建后，就会自动创建一个 default 服务账户。即使删除了该服务账户，也会立刻被重新自动创建出来。 token 控制器负责以下几项工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检测服务账户的创建，并且创建相应的 Secret 以支持 API 访问。&lt;/li&gt;
&lt;li&gt;检测服务账户的删除，并且删除所有相应的服务账户 Token Secret。&lt;/li&gt;
&lt;li&gt;检测 Secret 的增加，保证相应的服务账户存在，如有需要，为 Secret 增加 token。&lt;/li&gt;
&lt;li&gt;检测 Secret 的删除，如有需要，从相应的服务账户中移除引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至于为什么需要为服务账户创建 token 并生成 secret，将在后面提到。 现在我们知道了服务账号和其 token 的自动化过程。那服务账户准入控制器又是什么作用呢？我们将目光投到 pod 创建的过程中，大多数时候我们都不会为 pod 指定服务账户。那么 pod 在创建成功后，关联的 default 服务账户是怎么回事呢？ 这里就是服务账户准入控制器的作用了。它通过 admission controller 的机制来对 pod 进行修改。在 pod 被创建或更新时，会执行以下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果该 pod 没有 &lt;code&gt;ServiceAccount&lt;/code&gt; 设置，将其 &lt;code&gt;ServiceAccount&lt;/code&gt; 设为 &lt;code&gt;default&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保证 pod 所关联的 &lt;code&gt;ServiceAccount&lt;/code&gt; 存在，否则拒绝该 pod。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 pod 不包含 &lt;code&gt;ImagePullSecrets&lt;/code&gt; 设置，那么 将 &lt;code&gt;ServiceAccount&lt;/code&gt; 中的 &lt;code&gt;ImagePullSecrets&lt;/code&gt; 信息添加到 pod 中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将一个包含用于 API 访问的 token 的 &lt;code&gt;volume&lt;/code&gt; 添加到 pod 中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将挂载于 &lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount&lt;/code&gt; 的 &lt;code&gt;volumeSource&lt;/code&gt; 添加到 pod 下的每个容器中。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 认证&lt;/h3&gt;
&lt;p&gt;假设我们的 pod 已经设置好了服务账户，现在要和 apiserver 通信，那么 apiserver 是怎么认证这个服务账户是有效的呢？ 我们在上面提到，token 控制器会为服务账户创建 secret，secret 中的 token 就是服务账户的校验信息，具体来说是通过 JWT 来认证的。这个 token 中会存储服务账户所在的命名空间，名称，UID 和 secret 的名称等信息。如果你对 token 内容感兴趣的话，可以将 token 值用 base64 解码，粘贴到&lt;a href=&quot;https://jwt.io/#debugger-io&quot; rel=&quot;noopener&quot;&gt;这里&lt;/a&gt;看看 token 中的内容。然后也可以将私钥和公钥粘贴上去验证签名是否正确。具体内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;{
  &quot;iss&quot;: &quot;kubernetes/serviceaccount&quot;,
  &quot;kubernetes.io/serviceaccount/namespace&quot;: &quot;default&quot;,
  &quot;kubernetes.io/serviceaccount/secret.name&quot;: &quot;default-token-894m5&quot;,
  &quot;kubernetes.io/serviceaccount/service-account.name&quot;: &quot;default&quot;,
  &quot;kubernetes.io/serviceaccount/service-account.uid&quot;: &quot;df5a8a9c-14d4-44c7-a55f-0100f51fc848&quot;,
  &quot;sub&quot;: &quot;system:serviceaccount:default:default&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 token 在生成的过程中，使用了服务账户专属的私钥进行签名，这个私钥是在 contrller-manager 启动时，通过&lt;code&gt;--service-account-private-key-file&lt;/code&gt;传进去的。同样的，为了在 apiserver 中对这个 token 进行校验，需要在 apiserver 启动时通过参数&lt;code&gt;--service-account-key-file&lt;/code&gt;传入对应的公钥。&lt;/p&gt;
&lt;h3&gt;2.3 授权&lt;/h3&gt;
&lt;p&gt;认证的问题解决了，那么 apiserver 怎么知道该请求的服务账户是否有权限操作当前的资源呢？在 k8s 中，常用的授权策略就是 RBAC 了。我们通过将服务账户和角色关联，就可以让服务账户有指定资源的相关操作权限了。&lt;/p&gt;
&lt;h2&gt;三、普通用户&lt;/h2&gt;
&lt;h3&gt;3.1 认证&lt;/h3&gt;
&lt;p&gt;不同于服务账户的是，k8s 本身并不存储普通用户的任何信息，那么 apiserver 是如何认证普通用户的呢？ 在创建 k8s 集群时，一般都会有一个根证书负责签发集群中所需的其他证书。那么可以认为，如果一个普通用户可以提供由根证书签发的证书，他就是一个合法的用户。其中，证书的 common name 就是用户名，orgnization 是用户组。比如我们本地集群上的 controller-manager 的证书信息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;$ cfssl certinfo -cert controller-manager.pem
{
  &quot;subject&quot;: {
    &quot;common_name&quot;: &quot;system:kube-controller-manager&quot;,
    &quot;names&quot;: [
      &quot;system:kube-controller-manager&quot;
    ]
  },
  &quot;issuer&quot;: {
    &quot;common_name&quot;: &quot;kubernetes&quot;,
    &quot;names&quot;: [
      &quot;kubernetes&quot;
    ]
  },
  &quot;serial_number&quot;: &quot;7884702304157281003&quot;,
  &quot;not_before&quot;: &quot;2020-10-09T05:51:52Z&quot;,
  &quot;not_after&quot;: &quot;2021-10-09T05:51:54Z&quot;,
  &quot;sigalg&quot;: &quot;SHA256WithRSA&quot;,
  &quot;authority_key_id&quot;: &quot;11:F5:D7:48:AE:2E:7F:59:DD:4C:C4:A8:97:D2:C0:21:98:C6:3A:A7&quot;,
  &quot;subject_key_id&quot;: &quot;&quot;,
  &quot;pem&quot;: &quot;-----BEGIN CERTIFICATE-----\nMIIDCDCCAfCgAwIBAgIIbWwW+H7/quswDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE\nAxMKa3ViZXJuZXRlczAeFw0yMDEwMDkwNTUxNTJaFw0yMTEwMDkwNTUxNTRaMCkx\nJzAlBgNVBAMTHnN5c3RlbTprdWJlLWNvbnRyb2xsZXItbWFuYWdlcjCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMPCkXDAttbJHnoLuhGFPr/28ag8NoI5\nY0e00uv3ltyHlakfCeOV48eBgpMka3BdUxFOTHI5wtumlU3iymdDvTnKkLc75v6p\nQ0Hfx0DYz8ykDcHQ04hIsgyXecaHl+hfy90bYAbF8V43MjA0X2VmIyLxS6wXgeM6\n8d/jc1X8Ggpw53ow7L4XiCMlXDPwzLlVUThYHRN+PA5EdADZHAzgXjsyg379/ori\nbS/NZtmizzfHGWugrfwBGPL187mp1xN1tyjR+7obtsQYpgZ0Emz74fWNlike2I69\ntlBDWYC5ddsbHtDu4h/H5guwFtZ3+VVLogyw3CntPvoV840Ro5jxmtMCAwEAAaNI\nMEYwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMB8GA1UdIwQY\nMBaAFBH110iuLn9Z3UzEqJfSwCGYxjqnMA0GCSqGSIb3DQEBCwUAA4IBAQBvUxh0\n+TDJn19qJPWXu5MGrRs1Efn+KCgSVMDcak9MfnG3kzCZ94SKw5PRYGQ6fzuUsgwT\nkbGJ3o4PR/BkZ9R2UUHa2prydQTHN+Qb/DuF3kVYTRbWxTN3br8Tp1uqiQVOLPe0\nrfRelwVR6y39O5Wc3VQCnQKM/ih4k2LKGwinq2sO7HN6pjwoKfapaOb050vrGOTu\n5RmX+SWs7CeWzITjC3sLfFyP/lh8zK7TINOKRx1/QBHlCnX4wnsXpOIe4Jf4ol1b\nKKGcicAcSrj252oOIxspAW8a7vX4DjVGRTSneQen5wbHeZbkeMyuvAVs2a73x94d\nfTH4K9+zxCLAVZFs\n-----END CERTIFICATE-----\n&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，subject 是证书申请人的信息，issuer 是签发人的信息。这里可以知道，controller-manager 使用的是 &lt;code&gt;system:kube-controller-manager&lt;/code&gt; 这个用户。&lt;/p&gt;
&lt;h3&gt;3.2 授权&lt;/h3&gt;
&lt;p&gt;跟服务账户的授权一样，普通用户也可以通过 RBAC 的机制来绑定角色，然后拥有某些资源的某些权限。比如 controller-manager 的 ClusterRoleBinding 信息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl get clusterrolebinding  system:kube-controller-manager -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: &quot;true&quot;
  creationTimestamp: &quot;2020-09-22T12:33:24Z&quot;
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: system:kube-controller-manager
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:kube-controller-manager
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: system:kube-controller-manager
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是绑定到了 &lt;code&gt;system:kube-controller-manager&lt;/code&gt; 这个集群角色，如果你感兴趣的话，还可以继续看这个角色绑定了哪些资源极其操作权限。&lt;/p&gt;
&lt;h3&gt;3.3 实践&lt;/h3&gt;
&lt;p&gt;因为普通用户需要我们自己为其签发证书，然后授权。下面用一个简单的例子来走一遍。 首先创建一个证书签发请求的 json 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;CN&quot;: &quot;jiang&quot;,
    &quot;key&quot;: {
        &quot;algo&quot;: &quot;ecdsa&quot;,
        &quot;size&quot;: 256
    },
    &quot;names&quot;: [
        {
            &quot;O&quot;: &quot;dev&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CN 是 common name 的简写。也就是我们要设置的用户名。O 是 orgnization 的简写，可以理解为用户组。接下来用 k8s 的根证书签发，需要 ca.crt 和 ca.key。这两个文件可以在 master 节点上的 &lt;code&gt;/etc/kubernetes/certs&lt;/code&gt;或其他地方找到。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;cfssl gencert -ca ca.crt -ca-key=ca.key jiang.json | cfssljson -bare jiang
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会生成三个新的文件:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jiang-key.pem：私钥&lt;/li&gt;
&lt;li&gt;jiang.pem：证书&lt;/li&gt;
&lt;li&gt;jiang.csr: 证书签发请求文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面我们用 jiang 的私钥和证书生成 kubeconfig 中的用户：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;kubectl config set-credentials jiang --client-key=./jiang-key.pem --client-certificate=./jiang.pem --embed-certs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来生成新的 context，指定 k8s 集群要使用的用户：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;kubectl config set-context k8s-jiang --user=jiang --cluster=k8s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为 jiang 这个用户创建角色和绑定。这里只允许 jiang 这个用户读取 default namespace 下的 pod。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
    name: pod-reader
    namespace: default
rules:
- apiGroups: [&quot;&quot;]
  resources: [&quot;pods&quot;]
  verbs: [&quot;get&quot;, &quot;watch&quot;, &quot;list&quot;]

---

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
    name: read-pods
    namespace: default
subjects:
- kind: User
  name: jiang
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就完成了对 jiang 这个用户的认证和授权了。使用下面命令切换到这个用户：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;kubectl config use-context k8s-jiang
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>kubernetes 中的垃圾回收机制</title><link>https://www.myway5.com/blog/kubernetes-gc/</link><guid isPermaLink="true">https://www.myway5.com/blog/kubernetes-gc/</guid><description>一个运行中的 kubernetes 集群，存储了非常多的相互关联的资源，比如我们常用的 deployment，replicaset 和 pod，就是一组有关联的资源。我们在创建 deployment 时，相关的控制器就会自动创建出 replicaset，之后 replicase…</description><pubDate>Sun, 20 Sep 2020 15:01:39 GMT</pubDate><content:encoded>&lt;h2&gt;一、概述&lt;/h2&gt;
&lt;p&gt;一个运行中的 kubernetes 集群，存储了非常多的相互关联的资源，比如我们常用的 deployment，replicaset 和 pod，就是一组有关联的资源。我们在创建 deployment 时，相关的控制器就会自动创建出 replicaset，之后 replicaset 的控制器又会创建出 pod 来运行我们部署的服务。那么同样的，我们肯定也希望在删除 deployment 之后，会自动删除 replicaset 和 pod。这个机制就叫做垃圾回收（下面简称 GC）。 在早期的版本中，GC 是由客户端实现的，比如使用 &lt;code&gt;kubectl delete deployment nginx&lt;/code&gt; 这样的命令，kubectl 会删除 pod 和 replicaset。但是这种方式增加了客户端的实现复杂度，不利于统一管理。因此提出了在服务端实现 GC 的需求。实现 GC 有三个主要目标，我们在之后分析的时候，也主要是围绕这三个主要目标进行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在服务端支持级联删除&lt;/li&gt;
&lt;li&gt;中心化级联删除的逻辑，而不是分布在各个组件内&lt;/li&gt;
&lt;li&gt;可以选择不删除被依赖的资源。如只删除 deployment，但是保留 replicaset 和 pod&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;kubernetes 的 GC 是在 controller manager 中，作为一个单独的 controller 来实现的。它通过 discovery client 来动态发现并监听集群中所有支持 &lt;code&gt;delete&lt;/code&gt;,&lt;code&gt;list&lt;/code&gt;和&lt;code&gt;watch&lt;/code&gt;的资源。然后构造资源之间的关系图来记录资源之间的依赖关系。&lt;/p&gt;
&lt;h2&gt;二、预备知识&lt;/h2&gt;
&lt;p&gt;为了更好的阐述 kubernetes 的 GC 机制，这里先将一些 k8s 基本知识做一些阐述。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;finalizer: finalizer 可以翻译为终结期。是一种用来保证资源在被删除之前，能够有机会做一些清理工作的机制。&lt;/li&gt;
&lt;li&gt;kubernetes 的删除传播策略有三种：
&lt;ol&gt;
&lt;li&gt;Orphan. 这种策略下，会保留被依赖的资源。如只删除 deployment，但是保留 replicaset 和 pod。&lt;/li&gt;
&lt;li&gt;Background. 从 etcd 中删除资源，被依赖的资源由 GC 机制来删除。&lt;/li&gt;
&lt;li&gt;Foreground. apiserver 不会删除该资源。而是在它的 finalizer 中添加 foregroundDeletion，并且设置当前的删除时间戳。然后 GC 会先从 etcd 中删除有 &lt;code&gt;ownerReference.blockOwnerDeletion=true&lt;/code&gt; 的被依赖资源。最后再删除当前资源。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;UID。k8s 中的每个资源都有一个唯一的 UID。这个 UID 在整个集群的生命周期中，对于每一个资源来说都是唯一的。所有在标记资源的依赖关系时，需要使用 UID。&lt;/li&gt;
&lt;li&gt;ownerReferences。每个资源的 metadata 中都会有这个字段，它是一个数组，用来表示该资源的 owner 有哪些。每当 owner 资源被删除，就会从这个数组中移除。当所有的 owner 都被删除后，GC 就会回收该资源。&lt;/li&gt;
&lt;li&gt;Dependents。如果一组资源 G 的 ownerReference 指向某个具体的资源 A。那个 A 的 dependents 就是 G&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三、垃圾回收的实现机制&lt;/h2&gt;
&lt;p&gt;kubernetes 的 GC 主要由两部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GraphBuilder 主要用来使用 monitors 监听 apiserver 上的所有资源，通过将所有资源的事件插入到 graphChanges 队列中，然后调用 &lt;code&gt;processGraphChanges&lt;/code&gt; 方法，从队列中依次取出元素，构建资源之间的依赖关系。并根据情况插入到 attemptToDelete 或 attemptToOrphan 队列中。&lt;/li&gt;
&lt;li&gt;GarbageCollector 负责从 attemptToDelete 和 attemptToOrphan 队列中取出资源，然后通过一系列负责的过程，判断是否能删除，并进行相关的处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，对于垃圾回收实现机制的分析，主要从这两部分进行。&lt;/p&gt;
&lt;h3&gt;3.1 graph builder 的实现&lt;/h3&gt;
&lt;p&gt;graph builder 可以看做是集群资源状态的维护者。其本身并不会通过 apiserver 修改任何的资源。其定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// GraphBuilder 处理 informers 提供的事件，更新 uidToNode，使用 LRU 缓存依赖资源，并将
// 资源送入到 attemptToDelete 和 attemptToOrphan 队列
type GraphBuilder struct {
    restMapper meta.RESTMapper

  // 每个 monitor 都会 list/watches 一个资源，结果会被导入到 dependencyGraphBuilder 中·
    monitors    monitors
    monitorLock sync.RWMutex

    informersStarted &amp;lt;-chan struct{}
    stopCh &amp;lt;-chan struct{}
    running bool

    metadataClient metadata.Interface
  // monitors 是该队列的生产者，graphBuilder 根据这些改变来修改内存中的 graph
    graphChanges workqueue.RateLimitingInterface
  // 资源 uid 对应到 graph 中的 node
    uidToNode *concurrentUIDToNode
  // GraphBuilder 是 attemptToDelete 和 attemptToOrphan 的生产者，GC 是消费者。
    attemptToDelete workqueue.RateLimitingInterface
    attemptToOrphan workqueue.RateLimitingInterface
  // GraphBuilder 和 GC 共享 absentOwnerCache. 目前已知的不存在的对象会被添加到缓存中
    absentOwnerCache *UIDCache
    sharedInformers  controller.InformerFactory
    ignoredResources map[schema.GroupResource]struct{}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;组成 graph 的 node 定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 单线程的 GraphBuilder.processGraphChanges() 是 nodes 的唯一 writer。多线程的 GarbageCollector.attemptToDeleteItem() 读取 nodes。
type node struct {
    identity objectReference
    dependentsLock sync.RWMutex
    // dependents 是当前 node 的依赖资源。比如当前 node 是 replicaset，那么这里面保存的应该就是多个 pod
    dependents map[*node]struct{}
    // this is set by processGraphChanges() if the object has non-nil DeletionTimestamp
    // and has the FinalizerDeleteDependents.
    deletingDependents     bool
    deletingDependentsLock sync.RWMutex
    // this records if the object&apos;s deletionTimestamp is non-nil.
    beingDeleted     bool
    beingDeletedLock sync.RWMutex
    // this records if the object was constructed virtually and never observed via informer event
    virtual     bool
    virtualLock sync.RWMutex
    // when processing an Update event, we need to compare the updated
    // ownerReferences with the owners recorded in the graph.
    owners []metav1.OwnerReference
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GraphBuilder 会和 apiserver 同步 monitors，然后为每种资源创建一个 monitor，通过 informer 同步资源的状态。所有的资源都会直接进入 graphChanges 队列。然后在 processGraphChanges 方法中统一处理。 &lt;strong&gt;对于 Add 和 Update 事件：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果当前资源不存在 graph 中，就会实例化出一个 Node 对象，加入到 graph 中。然后将该 node 加入到其 owners 的 dependents 数组中。 这里有一个细节，就是有可能出现一种情况，当前 node 所代表的资源通过 informer 被同步到本地缓存中，但是其 owner 还没有被同步过来。这样更新 owners 的 dependents 就会有遗漏。因此每个 node 都有一个 virtual 字段，在 owner 还没有被同步时，实例化一个虚拟的 owner node 加入到 graph 中。并且将这个虚拟 node 添加到 attemptToDelete 队列中，由之后的 GC 处理。如果这个虚拟 node 在之后被 processGraphChanges 发现了，就会调用 markObserved() 将 virtual 置为 false。&lt;/li&gt;
&lt;li&gt;如果已经存在了，那么就要比对新旧资源的 ownerReferences 的变化情况。这里会计算出 added, removed 和 changed。ownerReferences 的变化可能会带来以下要处理的情况。
&lt;ul&gt;
&lt;li&gt;之前提到 Foreground 的删除，ownerReference 带有 blockOwnerDeletion=true 的资源会 block 的 owner 的删除。那么这里因为 ownerReferences 的变化，需要做以下两点：&lt;/li&gt;
&lt;li&gt;对于 removed 的 ownerReference，如果 blockOwnerDeletion 为 true。就说明当前不允许再 block 该 node owner 的删除。因此将 owner 放到 attemptToDelete 队列中，等待 GC 的处理。&lt;/li&gt;
&lt;li&gt;对于更新的 ownerReference，如果之前 blockOwnerDeletion 为 true，现在为 false，那么也要加入到 attemptToDelete 队列。&lt;/li&gt;
&lt;li&gt;对于 added 和 removed，都需要更新对应的 owner node 的 dependents。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;无论是 Add 还是 Update 事件，都会调用 &lt;code&gt;processTransitions&lt;/code&gt; 方法，
&lt;ul&gt;
&lt;li&gt;如果 old object 没有被删除或者没有 orphan finalizer，但是 new object 被删除了或者有 orphan finalizer，就会将该节点插入到 attemptToOrphan 队列。&lt;/li&gt;
&lt;li&gt;如果 old object 没有被删除或者没有 foregroundDeletion finalizer，但是 new object 被删除了或者有 foregroundDeletion finalizer，就会将该节点的 dependents 都插入到 attemptToDelete 队列，再将节点插入到 attemptToDelete 队列。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;对于删除事件&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;会从当前的 graph 中移除该 node。起始就是从 uidToNode 中删除该 node，然后更新所有的 owner 的 dependents。&lt;/li&gt;
&lt;li&gt;如果当前 node 的 dependents 大于 0，就将当前 node 添加到 absentOwnerCache 中。&lt;/li&gt;
&lt;li&gt;将该 node 的 dependents 将入到 attemptToDelete 队列中（垃圾回收）。&lt;/li&gt;
&lt;li&gt;最后，从该 node 中找到处于 deletingDependents=true 状态的 owner，也插入到 attemptToDelete 队列中。这里是为了让 GC 检查该 owner 是不是所有的 dependents 都被删除了，如果是，就将该 owner 也删除（这里 owner 处于 deletingDependents，说明使用了 foregroundDeletion，因此需要先删除 dependents，再删除 owner）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此可以知道，以下状态的资源会被插入到 attemptToDelete 队列中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;finalizers 中有 foregroundDelete&lt;/li&gt;
&lt;li&gt;owner 的 finalizers 中有 foregroundDelete&lt;/li&gt;
&lt;li&gt;owner 资源被删除&lt;/li&gt;
&lt;li&gt;Dependents 中有资源被删除，并且当前状态还不是正在删除 deletingDependents&lt;/li&gt;
&lt;li&gt;owner 处于 deletingDependents&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以下状态的资源会被插入到 attemptToOrphan 队列中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;finalizers 中有 orphan&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 GarbageCollector 的实现&lt;/h3&gt;
&lt;p&gt;在 3.1 中提到，GC 会消费 GraphBuilder 的 attemptToDelete 和 attemptToOrphan 队列，来执行 delete 或 orphan 操作。因此我们这里主要关心，什么样的资源可以被 GC delete 或者 orphan。&lt;/p&gt;
&lt;h4&gt;3.2.1 attemptToDeleteItem&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;对于 DeletionTimestamp 不为空，并且不处于删除 dependents 的资源。直接跳过处理流程。&lt;/li&gt;
&lt;li&gt;如果资源处于 deletingDependents 状态，则统计 &lt;code&gt;blockOwnerDeletion=true&lt;/code&gt;的 dependents 个数。
&lt;ul&gt;
&lt;li&gt;如果为 0，说明当前资源可以删除了，则移除 foregroundDeletion 这个 finalizer 即可。&lt;/li&gt;
&lt;li&gt;否则将 dependents 插入到 attemptToDelete 队列中&lt;/li&gt;
&lt;li&gt;之后会退出这个循环&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对资源的 ownerReferences 进行分类
&lt;ul&gt;
&lt;li&gt;Dangling: owner 对应的资源实际已经不存在了。&lt;/li&gt;
&lt;li&gt;waitingForDependentsDeletion: owner 的 DeletionTimeStamp 不为空，但是有 foregroundDeletion，所以正在等待 dependents 删除&lt;/li&gt;
&lt;li&gt;solid: owner 存在，并且不是 waitingForDependentsDeletion&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果 solid 不为空，那么当前资源就不能被 GC，因此只需要通过 patch 来移除 dangling 和 waitingForDependentsDeletion 的 ownerReferences&lt;/li&gt;
&lt;li&gt;如果 waitingForDependentsDeletion 不为空并且当前资源的 dependents 不为空。这个判断用来处理循环依赖的异常情况，因为当前资源并不处于删除状态且有 dependents，其 owner 又在等待该 item 的删除，说明这里有一个循环依赖。解决办法就是通过 patch 去更改该资源的 blockOwnerDeletion 为 false。&lt;/li&gt;
&lt;li&gt;如果上面两种情况都不是。就会根据当前资源的 finalizer 来删除资源
&lt;ul&gt;
&lt;li&gt;orphan&lt;/li&gt;
&lt;li&gt;foreground&lt;/li&gt;
&lt;li&gt;Background&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此可以得出，以下状态的资源会被 GC 调用删除请求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;资源处于 deletingDependents 状态，且其没有 dependents 的 blockOwnerDeletion 为 true。先移除 foregroundDeletion finalizer，然后删除&lt;/li&gt;
&lt;li&gt;资源的 owner 和 dependents 都有 blockOwnerDeletion。如果 dependents 处于 deletingDependents 状态。为了防止存在循环依赖，会先把 owner 的 unblock。然后使用 foreground 来删除当前资源。&lt;/li&gt;
&lt;li&gt;资源没有 solid 的 owner，那么这个资源就是应该被级联删除的资源。所以根据该资源的 finalizer 来删除。默认使用 background 的方式删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3.2.2 attemptToOrphan&lt;/h4&gt;
&lt;p&gt;orphan 是防止某些情况下资源被 GC 回收的方式。attemptToOrphan 的逻辑要简短一些，如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;移除 dependents 对当前资源 ownerReferences&lt;/li&gt;
&lt;li&gt;移除该资源的 orphan finalizer （这个更新事件会被 GraphBuilder 获取到，然后该资源符合进入 attemptToDelete 队列的条件。之后再由 GC 的处理，最终会被删除。）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;根据以上流程，附上自己整理的一个整体的 GC 流程图 &lt;img src=&quot;/uploads/wp/2020/09/k8s-garbage-collection.png&quot; alt=&quot;k8s-gc&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/garbage-collection.md#orphaning-the-descendants-with-orphan-finalizer&quot; rel=&quot;noopener&quot;&gt;garbage collection&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>k8s</category><category>k8s</category><category>gc</category><author>joyme123</author></item><item><title>k8s 中删除 namespace 时发生了什么</title><link>https://www.myway5.com/blog/k8s-namespace-deletion/</link><guid isPermaLink="true">https://www.myway5.com/blog/k8s-namespace-deletion/</guid><description>namespace 是 kubernetes 中一个比较重要的概念，是对一组资源和对象的抽象，也常用来作不同用户的隔离。namespace 下有很多资源，比如我们常用的 deployment, pods, service, ingress, configmap 等等。</description><pubDate>Fri, 04 Sep 2020 13:43:20 GMT</pubDate><content:encoded>&lt;h2&gt;一、概述&lt;/h2&gt;
&lt;p&gt;namespace 是 kubernetes 中一个比较重要的概念，是对一组资源和对象的抽象，也常用来作不同用户的隔离。namespace 下有很多资源，比如我们常用的 deployment, pods, service, ingress, configmap 等等。 当然本篇文章的重点在于删除 namespace 时发生了什么？一个很典型的场景是在终端中执行 &lt;code&gt;kubectl delete ns test&lt;/code&gt; 时，我们会观察到，在执行命令后，test 命名空间会立刻进入 terminating 状态，在几秒钟之后，才会被真正删除。即使 test 命名空间中没有任何资源。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;NAME              STATUS   AGE
default           Active   2d2h
docker            Active   2d2h
kube-node-lease   Active   2d2h
kube-public       Active   2d2h
kube-system       Active   2d2h
test              Active   4s
test              Terminating   18s
test              Terminating   23s
test              Terminating   23s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，我们在下面会探究以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;api-server 如何处理 namespace 的删除请求&lt;/li&gt;
&lt;li&gt;删除 namespace 时如何处理其中的资源&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、api server 如何处理 namespace 删除请求&lt;/h2&gt;
&lt;p&gt;和其他资源不同，namespace 在删除时，需要先清空 namespace 下资源。因此 namespace 有两种状态，即 active 和 terminating。当 namespace 处于 terminating 时，说明其下的资源还没有被确认删除干净。因此，api-server 在收到 namespace 的删除请求时，并不会立刻将其从 etcd 中删除，而是先检查 metadata.DeletionTimestamp 是否为空，如果为空，则是先将 metadata.DeletionTimestamp 置为当前时间，然后将 status.Phase 置为 terminating。如果 metadata.DeletionTimestamp 不为空，还要再判断 spec.Finalizers 是否为空。如果为空，才会真正的删除该 namespace。 这样的处理方式，就保证了在 spec.Finalizers 不为空时，namespace 不会被删除。那么 finalizer 是在什么时候添加的呢？具体的作用是怎么体现的？&lt;/p&gt;
&lt;h2&gt;三、finalizer 机制&lt;/h2&gt;
&lt;p&gt;namespace 的 finalizer 其实在创建的时候就已经添加上去了。处理逻辑可见以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// PrepareForCreate clears fields that are not allowed to be set by end users on creation.
func (namespaceStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
    // on create, status is active
    namespace := obj.(*api.Namespace)
    namespace.Status = api.NamespaceStatus{
        Phase: api.NamespaceActive,
    }
    // on create, we require the kubernetes value
    // we cannot use this in defaults conversion because we let it get removed over life of object
    hasKubeFinalizer := false
    for i := range namespace.Spec.Finalizers {
        if namespace.Spec.Finalizers[i] == api.FinalizerKubernetes {
            hasKubeFinalizer = true
            break
        }
    }
    if !hasKubeFinalizer {
        if len(namespace.Spec.Finalizers) == 0 {
            namespace.Spec.Finalizers = []api.FinalizerName{api.FinalizerKubernetes}
        } else {
            namespace.Spec.Finalizers = append(namespace.Spec.Finalizers, api.FinalizerKubernetes)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在删除时 namespace 变更到 terminating 状态，namespace controller 就开始发挥作用了。namespace controller 属于 controller manager，其会监听 namespace 的 add 和 update 事件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;    // configure the namespace informer event handlers
    namespaceInformer.Informer().AddEventHandlerWithResyncPeriod(
        cache.ResourceEventHandlerFuncs{
            AddFunc: func(obj interface{}) {
                namespace := obj.(*v1.Namespace)
                namespaceController.enqueueNamespace(namespace)
            },
            UpdateFunc: func(oldObj, newObj interface{}) {
                namespace := newObj.(*v1.Namespace)
                namespaceController.enqueueNamespace(namespace)
            },
        },
        resyncPeriod,
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且会使用 workqueue 来保存每一个 namespace 的变化事件。然后统统触发 &lt;code&gt;nm.namespacedResourcesDeleter.Delete(namespace.Name)&lt;/code&gt;。当然，如果 namespace 不存在或者 namespace.DeletionTimestamp 为空，则会退出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;    namespace, err := d.nsClient.Get(context.TODO(), nsName, metav1.GetOptions{})
    if err != nil {
        if errors.IsNotFound(err) {
            return nil
        }
        return err
    }
    if namespace.DeletionTimestamp == nil {
        return nil
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则无论如何都会先将 namespace 的 phase 先置为 terminating。这也就是说，如果一个 namespace 已经处于 terminating 了，你就无法通过仅仅修改该 phase 来改变该 namespace 的状态。我之前在遇到过 namespace 一直处于 terminating 时，手动修改了 phase 为 active，但是 namespace 会立刻变为 terminating，原因大概就是如此了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// updateNamespaceStatusFunc will verify that the status of the namespace is correct
func (d *namespacedResourcesDeleter) updateNamespaceStatusFunc(namespace *v1.Namespace) (*v1.Namespace, error) {
    if namespace.DeletionTimestamp.IsZero() || namespace.Status.Phase == v1.NamespaceTerminating {
        return namespace, nil
    }
    newNamespace := v1.Namespace{}
    newNamespace.ObjectMeta = namespace.ObjectMeta
    newNamespace.Status = *namespace.Status.DeepCopy()
    newNamespace.Status.Phase = v1.NamespaceTerminating
    return d.nsClient.UpdateStatus(context.TODO(), &amp;amp;newNamespace, metav1.UpdateOptions{})
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后就开始尝试清空该 namespace 下的所有内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;    // there may still be content for us to remove
    estimate, err := d.deleteAllContent(namespace)
    if err != nil {
        return err
    }
    if estimate &amp;gt; 0 {
        return &amp;amp;ResourcesRemainingError{estimate}
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、DiscoveryInterface 的工作机制&lt;/h2&gt;
&lt;p&gt;现在我们面临的一个问题就是如何清理该 namespace 下的所有资源呢？平时如果我们要删除一个 pod，我们可以调用 client-go 提供的 PodInterface 接口来删除，其实就是 RESTful 的 HTTP DELETE 动作的封装。但是现在因为我们不知道 namespace 下有哪些资源，所以就没有办法直接调用删除的接口。 所以 client-go 还提供了一个 DiscoveryInterface，顾名思义，DicoveryInterface 可以用来发现集群中的 API groups，versions, resources。在取得集群中所有的接口资源列表口，我们就可以对这些资源进行查询和删除了。DicoveryInterface 接口如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// DiscoveryInterface holds the methods that discover server-supported API groups,
// versions and resources.
type DiscoveryInterface interface {
    RESTClient() restclient.Interface
    ServerGroupsInterface
    ServerResourcesInterface
    ServerVersionInterface
    OpenAPISchemaInterface
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 ServerGroupInterface 提供了获取集群中所有接口组的能力，具体的函数签名如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;    // ServerGroups returns the supported groups, with information like supported versions and the
    // preferred version.
    ServerGroups() (*metav1.APIGroupList, error)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ServerVersionInterface 可以用来获取服务的版本信息，具体的函数签名如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;    // ServerVersion retrieves and parses the server&apos;s version (git version).
    ServerVersion() (*version.Info, error)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们需要关注的是 ServerResourcesInterface 这个接口&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// ServerResourcesInterface has methods for obtaining supported resources on the API server
type ServerResourcesInterface interface {
    // ServerResourcesForGroupVersion returns the supported resources for a group and version.
    ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error)
    // ServerResources returns the supported resources for all groups and versions.
    //
    // The returned resource list might be non-nil with partial results even in the case of
    // non-nil error.
    //
    // Deprecated: use ServerGroupsAndResources instead.
    ServerResources() ([]*metav1.APIResourceList, error)
    // ServerResources returns the supported groups and resources for all groups and versions.
    //
    // The returned group and resource lists might be non-nil with partial results even in the
    // case of non-nil error.
    ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error)
    // ServerPreferredResources returns the supported resources with the version preferred by the
    // server.
    //
    // The returned group and resource lists might be non-nil with partial results even in the
    // case of non-nil error.
    ServerPreferredResources() ([]*metav1.APIResourceList, error)
    // ServerPreferredNamespacedResources returns the supported namespaced resources with the
    // version preferred by the server.
    //
    // The returned resource list might be non-nil with partial results even in the case of
    // non-nil error.
    ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们可以用 ServerPreferredNamespacedResources 来获取所有属于 namespace 的资源列表。然后过滤出支持 DELETE 的资源。最后获取这些资源的 GroupVersionResources（简称 GVR ）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;    resources, err := d.discoverResourcesFn()
    if err != nil {
        // discovery errors are not fatal.  We often have some set of resources we can operate against even if we don&apos;t have a complete list
        errs = append(errs, err)
        conditionUpdater.ProcessDiscoverResourcesErr(err)
    }
    // TODO(sttts): get rid of opCache and pass the verbs (especially &quot;deletecollection&quot;) down into the deleter
    deletableResources := discovery.FilteredBy(discovery.SupportsAllVerbs{Verbs: []string{&quot;delete&quot;}}, resources)
    groupVersionResources, err := discovery.GroupVersionResources(deletableResources)
    if err != nil {
        // discovery errors are not fatal.  We often have some set of resources we can operate against even if we don&apos;t have a complete list
        errs = append(errs, err)
        conditionUpdater.ProcessGroupVersionErr(err)
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后遍历这些 GVR 进行删除：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;    for gvr := range groupVersionResources {
        gvrDeletionMetadata, err := d.deleteAllContentForGroupVersionResource(gvr, namespace, namespaceDeletedAt)
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;五、为什么 namespace 会长时间处于 terminating 状态&lt;/h2&gt;
&lt;p&gt;要探究 namespace 长时间处于 terminating 状态的原因，我们先看下面一段很短的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;    // there may still be content for us to remove
    estimate, err := d.deleteAllContent(namespace)
    if err != nil {
        return err
    }
    if estimate &amp;gt; 0 {
        return &amp;amp;ResourcesRemainingError{estimate}
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在删除命名空间下所有资源的时候，如果返回了错误，或者预估删除完所有资源的时间大于 0 的话，就会继续处于 terminating 状态。比如说 pod 会有一个 terminationGracePeriodSeconds，那么在删除 pod 的时候就可能要等待这个周期过去。但是这也造成不了什么问题，我们常常遇到的头疼问题是，namespace 一直无法删除。简单来说，就是 namespace 下肯定还有资源没法删除，可能性有以下几种。 &lt;strong&gt;部分资源有 admission 阻止了删除&lt;/strong&gt;，因为所有的删除请求都要先进过 admission webhook，那么可能因为 admission 的原因导致部分资源无法直接删除。 &lt;strong&gt;apiservice 出问题了&lt;/strong&gt;。这个问题我们可以通过 &lt;code&gt;kubectl get apiservice&lt;/code&gt; 来确认，在 AVAILABLE 一列中，如果有 false 的话，我们就要去检查这个 apiservice 无法使用的原因了。因为 apiservice 出了问题，就会导致这个 apiservice 下的资源无法通过 HTTP 请求去查询或操作，那么自然无法确认是否还有这部分资源遗留，也就无法彻底删除了。 最后，关于 namespace 无法删除的解决方案，网上给出的方案往往是通过置空 namespace 的 spec.finalizers 来做，但是这是治标不治本的方法。因为如果 namespace 无法删除，就一定说明你的集群中存在缺陷或问题，还是要找出真正的原因才是解决之道。你也可以尝试这个工具找出问题所在：&lt;a href=&quot;https://github.com/thyarles/knsk&quot; rel=&quot;noopener&quot;&gt;https://github.com/thyarles/knsk&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>k8s</category><category>controller的实现</category><category>namespace</category><category>controller</category><author>joyme123</author></item><item><title>informer 的基础知识</title><link>https://www.myway5.com/blog/informer/</link><guid isPermaLink="true">https://www.myway5.com/blog/informer/</guid><description>informer 是 client-go 提供的一个工具，主要是用来在 api-server 和程序之间同步指定的资源，并作为本地缓存，比如 Pod, Deployment 等等。</description><pubDate>Thu, 16 Jul 2020 14:55:13 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;informer 是 &lt;a href=&quot;https://github.com/kubernetes/client-go&quot; rel=&quot;noopener&quot;&gt;client-go&lt;/a&gt; 提供的一个工具，主要是用来在 &lt;code&gt;api-server&lt;/code&gt; 和程序之间同步指定的资源，并作为本地缓存，比如 &lt;code&gt;Pod&lt;/code&gt;, &lt;code&gt;Deployment&lt;/code&gt; 等等。 我们都知道，kubernetes 中有很多个 controller 在运行，来保证它们关注的资源处于符合期望的状态。比如 &lt;code&gt;ReplicasSet&lt;/code&gt;，会保证该 &lt;code&gt;ReplicaSet&lt;/code&gt; 有期望的副本数一直运行。这是通过一个不会终止的循环，不断监控当前集群的状态，然后调整的期望的状态。如下代码所示:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;for {
    current := getCurrentState()
    desired := getDesiredState()
    reconcile(current, desired)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为这样的需求，所以 controller 需要不停的获取集群中一些资源的状态，然后调整到期望的状态。如果我们是通过网络不停的查询集群状态，将是一个性能很差的方案。为了性能，可以使用缓存，来将指定的资源保存在本地，只要我们及时的更新缓存，就不需要通过网络向集群查询了。 这就是 informer 出现的原因。它通过 &lt;code&gt;List&amp;amp;Watch&lt;/code&gt; 来实时同步 api-server 中的资源，然后将资源分成三种事件来触发不同的处理。这三种事件是: &lt;code&gt;Add&lt;/code&gt;, &lt;code&gt;Update&lt;/code&gt; 和 &lt;code&gt;Delete&lt;/code&gt;。同时它还提供了一个抽象的 &lt;code&gt;Store&lt;/code&gt; 来提供本地缓存的查询。 最后，它还可以配合 &lt;code&gt;workqueue&lt;/code&gt; 来实现本地的重试等等。informer 是一个非常强大的工具，在我们做 kubernetes 上 controller 的开发时必不可少，但是因为 controller 的编写本身就是一件比较复杂的工作，我们必须要对 informer 本身，以及其周边的工具有清晰的理解，才能写出质量更好的代码。&lt;/p&gt;
&lt;h2&gt;工作流程&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2020/07/informer-1.png&quot; alt=&quot;informer&quot; /&gt; 这里先放上一张图来做参考。一般我们在使用 informer 时，会使用如下的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// filterd
lw := cache.ListWatch{
    ListFunc: func(options metav1.ListOptions) (object runtime.Object, err error) {
        return k8sCli.CoreV1().Pods(metav1.NamespaceAll).List(context.TODO(), options)
    },
    WatchFunc: func(options metav1.ListOptions) (w watch.Interface, err error) {
        return k8sCli.CoreV1().Pods(metav1.NamespaceAll).Watch(context.TODO(), options)
    },
}
// indexerInformer, shareInformer
store, ctrl := cache.NewInformer(&amp;amp;lw, &amp;amp;v1.Pod{}, 0, cache.ResourceEventHandlerFuncs{
    AddFunc:    handleAddPod,
    UpdateFunc: handleUpdatePod,
    DeleteFunc: handleDeletePod,
})
stopChan := signals.SetupSignalHandler()
go ctrl.Run(stopChan)
if sync := cache.WaitForCacheSync(stopChan, ctrl.HasSynced); !sync {
    log.Println(&quot;not sync&quot;)
}
log.Println(&quot;synchronized finish&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，我们定义了 ListWatch 的方法，informer 会用 List 方法来获取所有的 Pod 资源，然后使用 Watch 来监听之后 Pod 资源的更新。 之后我们实例化了一个 informer。第二个参数是资源类型。第三个参数是重新同步的周期，0为不同步，否则会在每个周期开始时重新 List 所有的资源。第四个参数是 &lt;code&gt;ResourceEventHandlerFuncs&lt;/code&gt;，这里的 &lt;code&gt;AddFunc&lt;/code&gt;, &lt;code&gt;UpdateFunc&lt;/code&gt;,&lt;code&gt;DeleteFunc&lt;/code&gt; 是本地缓存在增加，更新和删除时触发的事件。 &lt;code&gt;NewInformer&lt;/code&gt; 返回了 store 和 ctrl 两个值，store 就是 pod 的本地缓存，我们可以通过查询 store 来代替直接向 api-server 查询。这个返回的 store 实现了如下的接口:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Store interface {
    Add(obj interface{}) error
    Update(obj interface{}) error
    Delete(obj interface{}) error
    List() []interface{}
    ListKeys() []string
    Get(obj interface{}) (item interface{}, exists bool, err error)
    GetByKey(key string) (item interface{}, exists bool, err error)

    // Replace will delete the contents of the store, using instead the
    // given list. Store takes ownership of the list, you should not reference
    // it after calling this function.
    Replace([]interface{}, string) error
    Resync() error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 controller 中，我们一般使用 &lt;code&gt;List*&lt;/code&gt;, &lt;code&gt;Get*&lt;/code&gt; 方法，可以用来查询本地的缓存。同时不要使用其他的方法，这会导致一些不可预知的问题。 另外一个返回值 ctrl，实现了如下的接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Controller interface {
    Run(stopCh &amp;lt;-chan struct{})
    HasSynced() bool
    LastSyncResourceVersion() string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个接口很简单，&lt;code&gt;Run&lt;/code&gt; 用来开始启动同步，&lt;code&gt;stopCh&lt;/code&gt; 用来随时停止同步，&lt;code&gt;HasSynced&lt;/code&gt; 用来判断同步是否完成。&lt;code&gt;LastSyncResourceVersion&lt;/code&gt; 用来获取最新同步的资源 version。 &lt;code&gt;cache.WaitForCacheSync&lt;/code&gt; 用来等待同步完成。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;关于 informer 的基本使用就先介绍这么多。后面会对 informer 中涉及的代码进行详细的分析，包括 &lt;code&gt;List&amp;amp;Watch&lt;/code&gt; 的机制、&lt;code&gt;DeltaFIFO&lt;/code&gt; 的实现、本地缓存(Store) 的实现等等。&lt;/p&gt;
</content:encoded><category>controller的实现</category><author>joyme123</author></item><item><title>从 iptables 看 k8s service 的实现机制</title><link>https://www.myway5.com/blog/iptables-k8s-service/</link><guid isPermaLink="true">https://www.myway5.com/blog/iptables-k8s-service/</guid><description>k8s service 可以看做是多个 Pod 的负载均衡。有以下几种 service: LoadBalancer ClusterIP NodePort ExternalName</description><pubDate>Fri, 12 Jun 2020 12:50:48 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;k8s service 可以看做是多个 Pod 的负载均衡。有以下几种 service:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LoadBalancer&lt;/li&gt;
&lt;li&gt;ClusterIP&lt;/li&gt;
&lt;li&gt;NodePort&lt;/li&gt;
&lt;li&gt;ExternalName&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 service 的演进中，从最初的 userspace 的方案变成 iptables 和 ipvs 的方案，其中，ipvs 主要是解决了 iptables 的性能问题。这篇文章主要分析 iptables 如何实现 service 的负载均衡。&lt;/p&gt;
&lt;h2&gt;ClusterIP&lt;/h2&gt;
&lt;p&gt;ClusterIP 是提供在集群中访问 Service 的方案，通常每个 Service 都会分配一个 VIP，然后为多个 Pod 提供负载均衡。这里我们创建两个副本的 nginx 部署，以及一个 nginx service。具体信息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ kubectl get endpoints nginx
NAME    ENDPOINTS                     AGE
nginx   172.17.0.4:80,172.17.0.5:80   65m

$ kubectl get service nginx
NAME    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
nginx   ClusterIP   10.111.67.225   &amp;lt;none&amp;gt;        80/TCP    65m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在集群中访问 &lt;code&gt;nginx.default.svc.cluster.local&lt;/code&gt; 时，DNS 会将这个地址解析到 Service 的 IP 上，也就是 &lt;code&gt;10.111.67.225&lt;/code&gt;。下面我们看看 iptables 是如何将访问这个地址的流量转到真实的 Pod 上的。 首先看一下 nat 表上的 OUTPUT 链:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ iptables -t nat -nL OUTPUT
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */
DOCKER     all  --  0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一条规则会匹配所有的流量，然后跳到 &lt;code&gt;KUBE-SERVICES&lt;/code&gt; 这条链上。我们看一下 &lt;code&gt;KUBE-SERVICES&lt;/code&gt; 的具体内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ iptables -t nat -nL KUBE-SERVICES
Chain KUBE-SERVICES (2 references)
target     prot opt source               destination
KUBE-SVC-NPX46M4PTMTKRN6Y  tcp  --  0.0.0.0/0            10.96.0.1            /* default/kubernetes:https cluster IP */ tcp dpt:443
KUBE-SVC-P4Q3KNUAWJVP4ILH  tcp  --  0.0.0.0/0            10.111.67.225        /* default/nginx:http cluster IP */ tcp dpt:80
KUBE-SVC-TCOU7JCQXEZGVUNU  udp  --  0.0.0.0/0            10.96.0.10           /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
KUBE-SVC-ERIFXISQEP7F7OF4  tcp  --  0.0.0.0/0            10.96.0.10           /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
KUBE-SVC-JD5MR3NA4I4DYORP  tcp  --  0.0.0.0/0            10.96.0.10           /* kube-system/kube-dns:metrics cluster IP */ tcp dpt:9153
KUBE-NODEPORTS  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里前面的 &lt;code&gt;KUBE-SVC-*&lt;/code&gt; 都是根据 destination， protocol 和目的端口号来匹配的，根据我们的 service 地址和端口号以及协议，可以定位到 &lt;code&gt;KUBE-SVC-P4Q3KNUAWJVP4ILH&lt;/code&gt; 这条规则可以匹配，然后跳到这条链上。我们接着看这条链定义了什么：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ iptables -t nat -nL KUBE-SVC-P4Q3KNUAWJVP4ILH
Chain KUBE-SVC-P4Q3KNUAWJVP4ILH (1 references)
target     prot opt source               destination
KUBE-SEP-GL7IUDQTUTXSADHR  all  --  0.0.0.0/0            0.0.0.0/0            /* default/nginx:http */ statistic mode random probability 0.50000000000
KUBE-SEP-VMO3WCKZND6ZICDD  all  --  0.0.0.0/0            0.0.0.0/0            /* default/nginx:http */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有两条规则，根据第一条规则后面的内容，我们可以知道这就是使用 iptables 实现负载均衡的地方了。第一条规则有 50% 的匹配几率。如果匹配到了其中一条，就会跳到另外一个链上。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ iptables -t nat -nL KUBE-SEP-GL7IUDQTUTXSADHR
Chain KUBE-SEP-GL7IUDQTUTXSADHR (1 references)
target     prot opt source               destination
KUBE-MARK-MASQ  all  --  172.17.0.4           0.0.0.0/0            /* default/nginx:http */
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            /* default/nginx:http */ tcp to:172.17.0.4:80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中第一条规则的 source 是 Pod 的 IP，在访问 Service 时目前还不会匹配，于是我们看第二条规则，将目的 IP 和 Port 改写成 172.17.0.4:80，也就是我们的 Pod IP，这样流量就经过负载均衡指向了我们的 Pod了。&lt;/p&gt;
&lt;h2&gt;NodePort&lt;/h2&gt;
&lt;p&gt;我们将上面的 Service 改成 NodePort&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nginx        NodePort    10.111.67.225   &amp;lt;none&amp;gt;        80:30000/TCP   34h
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后查询机器上的 30000 端口。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ ss -lp | grep 30000
tcp               LISTEN              0                    0                                                                                            0.0.0.0:30000                                                 0.0.0.0:*                  users:((&quot;kube-proxy&quot;,pid=4006,fd=8))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到, &lt;code&gt;kube-proxy&lt;/code&gt; 监听了 30000 端口，同时我们看 nat 表上的 &lt;code&gt;PREROUTING&lt;/code&gt; 链。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再看 &lt;code&gt;KUBE-SERVICES&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;KUBE-SVC-TCOU7JCQXEZGVUNU  udp  --  0.0.0.0/0            10.96.0.10           /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
KUBE-SVC-ERIFXISQEP7F7OF4  tcp  --  0.0.0.0/0            10.96.0.10           /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
KUBE-SVC-JD5MR3NA4I4DYORP  tcp  --  0.0.0.0/0            10.96.0.10           /* kube-system/kube-dns:metrics cluster IP */ tcp dpt:9153
KUBE-SVC-NPX46M4PTMTKRN6Y  tcp  --  0.0.0.0/0            10.96.0.1            /* default/kubernetes:https cluster IP */ tcp dpt:443
KUBE-SVC-P4Q3KNUAWJVP4ILH  tcp  --  0.0.0.0/0            10.111.67.225        /* default/nginx:http cluster IP */ tcp dpt:80
KUBE-NODEPORTS  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后一条 &lt;code&gt;KUBE-NODEPORTS&lt;/code&gt; 可以匹配到，这里有个匹配条件，那就是 &lt;code&gt;ADDRTYPE match dst-type LOCAL&lt;/code&gt;。注意这里的 &lt;code&gt;LOCAL&lt;/code&gt; 指的是本机网卡上存在的地址，也就是这条数据是发到本机，那么就能匹配。 &lt;code&gt;KUBE-NODEPORTS&lt;/code&gt; 的规则如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;KUBE-MARK-MASQ  tcp  --  0.0.0.0/0            0.0.0.0/0            /* default/nginx:http */ tcp dpt:30000
KUBE-SVC-P4Q3KNUAWJVP4ILH  tcp  --  0.0.0.0/0            0.0.0.0/0            /* default/nginx:http */ tcp dpt:30000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一条规则是替换源地址为本机出口的网卡地址。第二条规则如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;KUBE-SEP-F3MS6OIYSABTYGOY  all  --  0.0.0.0/0            0.0.0.0/0            /* default/nginx:http */ statistic mode random probability 0.50000000000
KUBE-SEP-VMO3WCKZND6ZICDD  all  --  0.0.0.0/0            0.0.0.0/0            /* default/nginx:http */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们在 &lt;code&gt;ClusterIP&lt;/code&gt; 中就分析了实现方法，因此这里忽略。&lt;/p&gt;
&lt;h2&gt;LoadBalancer&lt;/h2&gt;
&lt;p&gt;LoadBalancer 本身不是由 Kubernetes 提供的，其原理说起来也不难，我们先创建一个 LoadBalancer 的 Service 看看：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nginx        LoadBalancer   10.111.67.225   &amp;lt;pending&amp;gt;     80:32014/TCP   34h
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里因为我的本地集群没有 LoadBalancer，所以一直处于 Pending 状态。但是我们可以看到，这里还有一个 &lt;code&gt;80:32014&lt;/code&gt;。和上面的 NodePort 输出一致。也就是说创建 LoadBalancer 时，会在 Pod 所在的机器上开启 NodePort，然后由外部的 LoadBalancer 将负载均衡过的流量带到机器的指定的 NodePort 上。&lt;/p&gt;
&lt;h2&gt;一些有意思的参数&lt;/h2&gt;
&lt;p&gt;这里顺便多提几个有意思的Service 参数 &lt;code&gt;externalTrafficPolicy&lt;/code&gt;：可选值有 &lt;code&gt;Local&lt;/code&gt; 和 &lt;code&gt;Cluster&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Local: 流量只会被导向本机的 Pod，这样就少一次包的转发，提高性能。但是缺点是如果容易导致负载不均衡。&lt;/li&gt;
&lt;li&gt;Cluster: 在集群范围内转发流量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果能保证 Pod 均匀的分布在不同的节点上，那么外部的 LoadBalancer 配合 Local 的 externalTrafficPolicy 可以带来更好的性能。 &lt;code&gt;sessionAffinity&lt;/code&gt;: 会话亲和性，可以设置为 ClientIP，来达到将同一个 IP 的会话转发到相同的 Pod 上。其也是通过 iptables 实现的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;KUBE-SEP-Q7ZFI57LOFFPF3HN  all  --  0.0.0.0/0            0.0.0.0/0            /* test/nginx-session-affinity:http */ recent: CHECK seconds: 10800 reap name: KUBE-SEP-Q7ZFI57LOFFPF3HN side: source mask: 255.255.255.255
KUBE-SEP-LWUZWBNY6M3CYJ2M  all  --  0.0.0.0/0            0.0.0.0/0            /* test/nginx-session-affinity:http */ recent: CHECK seconds: 10800 reap name: KUBE-SEP-LWUZWBNY6M3CYJ2M side: source mask: 255.255.255.255
KUBE-SEP-Q7ZFI57LOFFPF3HN  all  --  0.0.0.0/0            0.0.0.0/0            /* test/nginx-session-affinity:http */ statistic mode random probability 0.50000000000
KUBE-SEP-LWUZWBNY6M3CYJ2M  all  --  0.0.0.0/0            0.0.0.0/0            /* test/nginx-session-affinity:http */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 iptables 的前两条规则就是在做 iptables 的检查。&lt;/p&gt;
</content:encoded><category>k8s</category><category>k8s</category><category>service</category><author>joyme123</author></item><item><title>ARP 协议笔记</title><link>https://www.myway5.com/blog/arp/</link><guid isPermaLink="true">https://www.myway5.com/blog/arp/</guid><description>在具体学习 ARP(Address Resolution Protocol) 协议之前，我们应该先了解 ARP 协议的使用场景。大多数人对 ARP 协议可能和我一样，都有一个大概的印象。比如它是在已知 IP 地址的情况下，用来查找 MAC 地址的协议。</description><pubDate>Sat, 30 May 2020 17:50:25 GMT</pubDate><content:encoded>&lt;h2&gt;ARP 协议是什么&lt;/h2&gt;
&lt;p&gt;在具体学习 ARP(Address Resolution Protocol) 协议之前，我们应该先了解 ARP 协议的使用场景。大多数人对 ARP 协议可能和我一样，都有一个大概的印象。比如它是在已知 IP 地址的情况下，用来查找 MAC 地址的协议。这里也试着将 wikipedia 上的定义翻译过来，给出一个较为全面准确的定义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ARP 协议是一种通信协议，用来发现网络层地址（比如 IPv4地址）关联的链路层地址（通常是 MAC 地址）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以下的内容都来自于 wikipedia： &lt;a href=&quot;https://en.wikipedia.org/wiki/Address%5C_Resolution%5C_Protocol&quot; rel=&quot;noopener&quot;&gt;https://en.wikipedia.org/wiki/Address\_Resolution\_Protocol&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;ARP 报文&lt;/h2&gt;
&lt;p&gt;ARP 协议使用一种格式来表示地址解析的请求或响应。ARP 消息的大小取决于链路层或者网络层地址的大小。报文头指明了每一层使用的网络类型以及地址的大小。报文头以 operation code(op) 结束，code 为 1 时表示请求，为 2 时表示响应。报文内容部分由四个地址组成，分别为发送者的硬件地址（Sender hardware address，简称 SHA）、发送者的网络层地址（Sender protocol address，简称 SPA）、目标的硬件地址（Target hardware address，简称 THA）、目标的网络层地址（Target protocol address，简称 TPA）。 &lt;img src=&quot;/uploads/wp/2020/05/%E6%88%AA%E5%B1%8F2020-05-30-%E4%B8%8B%E5%8D%889.58.32.png&quot; alt=&quot;arp package&quot; /&gt; 上图是以 IPv4 为例。这种情况下，SHA 和 THA 的大小为 48bit，SPA 和 TPA 的大小的 32bit。报文头的大小固定是 8 个字节。在 IPv4 的情况下就是总共有 28 个字节。下面，也以 IPv4 为例分别对 ARP 报文的每个字段进行解释。 1～2，Hardware type（HTYPE）： 指明链路层协议类型，以太网是1。 3～4，Protocol type （PTYPE）：指明网络层协议类型。对于 IPv4 来说，值是 0x0800。 5，Hardware address length（HLEN)：硬件地址的长度。以太网地址的长度是6。 6，Protocol length（PLEN）：网络层地址的长度。IPv4 地址长度是 4。 7～8，Operation（OP）：指明发送方执行的操作，1是请求，2是响应。 到此，ARP 的报文头结束。 9～14，Sender hardware address（SHA）：发送方的 MAC 地址。在 ARP 请求中，它代表的是发送请求方的地址。在 ARP 响应中，它代表的是这次 ARP 请求查找的主机地址。 15～18，Sender protocol address（SPA）：发送方的网络层地址。 19～24，Target hardware address（THA）：接收方的 MAC 地址。在 ARP 请求中，这个字段是被忽略的。在 ARP 响应中，这个字段用来表示 ARP 请求源主机的地址。 25～28，Target protocol address（TPA）：接收方的网络层地址。 ARP 的以太网帧类型是 0x0806。&lt;/p&gt;
&lt;h2&gt;例子&lt;/h2&gt;
&lt;p&gt;在一个办公室中的两台电脑 c1（192.168.1.100） 和 c2（192.168.1.101） ，在局域网内通过以太网接口和交换机连接，中间没有网关和路由器。 下面我通过 linux 的网桥和 network namespace 来模拟这一场景:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 准备交换机
ip link add name switch type bridge
# 准备一根网线，一头连接电脑c1，一头连接交换机
ip link add name veth_c10 type veth peer name veth_c11
# 准备一根网线，一头连接电脑c1，一头连接交换机
ip link add name veth_c20 type veth peer name veth_c21
# 准备电脑c1
ip netns add c1
# 准备电脑c2
ip netns add c2
# 将网线插入 c1
ip link set veth_c11 netns c1
# 将网线插入 c2
ip link set veth_c21 netns c2
# 将两根网线都插到交换机
ip link set veth_c10 master switch
ip link set veth_c20 master switch
# 启动交换机
ip link set switch up
# 启动c1
ip link set veth_c10 up
# 启动c2
ip link set veth_c20 up
# 为c1和c2分配ip
ip netns exec c1 ip addr add 192.168.1.100/24 dev veth_c11
ip netns exec c2 ip addr add 192.168.1.101/24 dev veth_c21
ip netns exec c1 ip link set veth_c11 up
ip netns exec c2 ip link set veth_c21 up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;环境准备好了之后，c1 想要跟 c2 通信，此时 c1 需要知道 c2 的 MAC 地址。首先它会查找本地是否有缓存的 ARP 表。因为我们的环境刚刚创建好，所以肯定是没有缓存的，那么这个时候，c1 就会发送 ARP 请求，来查找 c2 的 MAC 地址。为了看到 c1 和 c2 之间的所有通信，我们可以用 tcpdump 或 wireshark 来抓交换机上的包，我这里为了展示的更清晰，采用 wireshark 来抓包。 从 c1 向 c2 发送一次 ping。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ip netns exec c1 ping -c 1 192.168.1.101
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;wireshark 抓包截图如下： &lt;img src=&quot;/uploads/wp/2020/05/%E6%88%AA%E5%B1%8F2020-05-30-%E4%B8%8B%E5%8D%8811.46.40.png&quot; alt=&quot;wireshark&quot; /&gt; 第一条是 ARP 请求。它是封装在以太网帧中的。 &lt;img src=&quot;/uploads/wp/2020/05/%E6%88%AA%E5%B1%8F2020-05-30-%E4%B8%8B%E5%8D%8811.50.29.png&quot; alt=&quot;arp request&quot; /&gt; 以太网帧的广播地址是 ff:ff:ff:ff:ff:ff，源地址是 2e:ee:58:76:59:fc。类型是 ARP。ARP 请求中因为不知道目标的 MAC 地址，所以是 00:00:00:00:00:00。十六进制表示如下： &lt;img src=&quot;/uploads/wp/2020/05/%E6%88%AA%E5%B1%8F2020-05-30-%E4%B8%8B%E5%8D%8811.53.59.png&quot; alt=&quot;arp request hex&quot; /&gt;。 ARP 响应报文如下： &lt;img src=&quot;/uploads/wp/2020/05/%E6%88%AA%E5%B1%8F2020-05-30-%E4%B8%8B%E5%8D%8811.55.28.png&quot; alt=&quot;arp reply&quot; /&gt;。通过这个响应我们也能知道 c1 的 MAC 地址是 2e:ee:58:76:59:fc，c2 的 MAC 地址是 76:cb:15:06:92:87。这个时候，我们也可以看一下 arp 表的情况。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip netns exec c1 arp -a
? (192.168.1.101) at 76:cb:15:06:92:87 [ether] on veth_c11
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ARP 探针（ARP probe）&lt;/h2&gt;
&lt;p&gt;ARP 探针是一种 SPA 全为 0 的请求。在使用一个 IPv4 地址之前，实现了这个规范的主机必须检查这个地址是否已经在使用了。就是通过这样一个请求来检查的。 为什么要 SPA 全为 0 呢？这是为了防止如果存在冲突，这个请求可能会污染其他主机的 arp 表。&lt;/p&gt;
&lt;h2&gt;ARP 通告（ARP announcements）&lt;/h2&gt;
&lt;p&gt;ARP 可以用来作为一种简单的通告协议。当发送方的 IP 地址或者 MAC 地址发生改变后，用来更新其他主机的 MAC 表映射。ARP 通告请求在 target 字段上包含了 SPA 的值（TPA=SPA），THA 为 0，然后广播出去。因为 TPA 为自己的网络层地址，所以不会有其他主机的 ARP 响应。但是其他主机都会收到发送方的 MAC 地址和 IP 地址，那么就可以更新自己的缓存。&lt;/p&gt;
&lt;h2&gt;ARP 欺骗（ARP spoofing）和 代理 ARP（proxy ARP）&lt;/h2&gt;
&lt;p&gt;ARP 欺骗很好理解，就是让 ARP 请求的发送方收到错误的 ARP 响应。比如现在我们有三台电脑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;c1: 192.168.1.100（2e:ee:58:76:59:fc）&lt;/li&gt;
&lt;li&gt;c2: 192.168.1.101（76:cb:15:06:92:87）&lt;/li&gt;
&lt;li&gt;c3: 192.168.1.102（12:07:6b:be:20:d2）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;c1 想给 c2 发送数据，在 c1 发 ARP 请求的时候，我们将 ARP 响应中 c2 的 MAC 地址改为 c3 的 MAC地址。然后 c1 的数据就都会发给 c3 了，但是 c1 仍然认为自己在和 c2 通信，这就是 ARP 欺骗了。 代理 ARP 和 ARP 欺骗很像，只是目的不太一样。代理 ARP 的使用场景一般是两台主机不在同一个二层网内，这样通过代理 ARP 的方式来做流量转发。&lt;/p&gt;
</content:encoded><category>网络协议</category><category>arp</category><author>joyme123</author></item><item><title>kubernetes 的 taints 和 tolerations 的理解和实践</title><link>https://www.myway5.com/blog/kubernetes-taints-and-tolerations/</link><guid isPermaLink="true">https://www.myway5.com/blog/kubernetes-taints-and-tolerations/</guid><description>taints 和 tolerations 是一个比较好理解的概念，taints 可以翻译为污点，给 node 打上 taints，就可以用来驱逐 pod，并防止 pod 调度到该节点上。就像是某个人有了一个坏习惯（taints），那么其他人的就会远离这个人。</description><pubDate>Sun, 24 May 2020 09:31:08 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;taints 和 tolerations 是一个比较好理解的概念，taints 可以翻译为污点，给 node 打上 taints，就可以用来驱逐 pod，并防止 pod 调度到该节点上。就像是某个人有了一个坏习惯（taints），那么其他人的就会远离这个人。但是有些人可以容忍别人的坏习惯，那么就会不受影响。就像 pod 拥有了 tolerations，就可以免疫节点上对应的 taints。 taints 的官方说明为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The node this Taint is attached to has the &quot;effect&quot; on any pod that does not tolerate the Taint.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，taint 会为所有不能忍受该 taint 的 pod 添加副作用（不调度，偏好不调度，不执行） 如果要让某些 pod 免疫这些 taints，可以使用 tolerations。&lt;/p&gt;
&lt;h2&gt;taints 的使用&lt;/h2&gt;
&lt;p&gt;在使用 taints 的时候很简单，我们只需要指定节点 taints 的 key 和 value 即可。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ kubectl taint nodes minikube onlyNginxPod=true:NoSchedule
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，onlyNginxPod 是 taints 的 key，true 是 value，NoSchedule 是 effect。另外还有 PreferNoScheduler 和 NoExecute。这里顺便总结一下这三种 effect 的区别:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NoSchedule: 表示不要将 Pod 向该节点调度。如果 Pod 已经调度到该节点了，则不受影响。&lt;/li&gt;
&lt;li&gt;PreferNoScheduler: 表示尽量不要往该节点调度，但是如果没有其他选择，还是会将 Pod 调度到该节点。&lt;/li&gt;
&lt;li&gt;NoExecute: Pod 不仅不能往上调度，所有已经运行在该节点上的 Pod 将会被驱逐。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个时候，我们尝试创建一个普通的 pod，看看调度情况。 &lt;em&gt;pod.yaml&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: Pod
metadata:
    name: nginx
    namespace: default
    labels:
        app: nginx
spec:
    containers:
    - name: nginx
      image: nginx:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ kubectl apply -f pod-nginx.yaml 
$ kubectl describe pods nginx

Warning  FailedScheduling  &amp;lt;unknown&amp;gt;  default-scheduler  0/1 nodes are available: 1 node(s) had taint {onlyNginxPod: true}, that the pod didn&apos;t tolerate.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会出现上面的警告信息。表示因为 pod 没有容忍该 taint，所以没有办法调度上去。 我们可以用以下语句来删除 taint&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ kubectl taint node minikube onlyNginxPod=true:NoSchedule-
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;tolerations 的使用&lt;/h2&gt;
&lt;p&gt;某些情况下，我们仍然希望 Pod 可以调度到有 taint 的节点上，这时候就可以为 Pod 指定 tolerations。比如将上面的 Pod 改写成如下： &lt;em&gt;pod.yaml&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: Pod
metadata:
    name: nginx
    namespace: default
    labels:
        app: nginx
spec:
    containers:
    - name: nginx
      image: nginx:latest
    tolerations:
      - key: onlyNginxPod
        operator: Equal
        value: &quot;true&quot;
        effect: NoSchedule
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后创建这个 Pod, ···yaml $ kubectl apply -f pod.yaml $ kubectl get pods NAME READY STATUS RESTARTS AGE nginx 0/1 ContainerCreating 0 4s ··· 说明该 pod 调度成功，tolerations 生效了。tolerations 会匹配 key 和 effect，只有一样的时候才会生效。tolerations 的 operator 字段除了 &lt;code&gt;Equal&lt;/code&gt; 之外，还有 &lt;code&gt;Exists&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Equal: 要求 value 也相同。&lt;/li&gt;
&lt;li&gt;Exists: 不需要设置 value 字段。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;kubernetes 中使用 taints 和 tolerations 的常用场景&lt;/h2&gt;
&lt;p&gt;taints 和 tolerations 不仅仅是提供给用户使用的特性，kubernetes 本身也大量使用了 taints 和 tolerations。比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;node.kubernetes.io/not-ready&lt;/code&gt;: Node 还没准备好，对应的 NodeCondition 的 &lt;code&gt;Ready&lt;/code&gt; 为 &lt;code&gt;False&lt;/code&gt;。比如在创建集群时，还没有安装 CNI 的话，节点就会有该 taint。对应的 effect 为 NoSchedule。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;node.kubernetes.io/unreachable&lt;/code&gt;: node controller 无法连接到 Node，此时 NodeCondition 的 &lt;code&gt;Ready&lt;/code&gt; 为 &lt;code&gt;Unknown&lt;/code&gt;。对应的 effect 为 NoSchedule。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;node.kubernetes.io/out-of-disk&lt;/code&gt;: 磁盘用尽。对应的 effect 为 NoSchedule。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;node.kubernetes.io/memory-pressure&lt;/code&gt;: 节点有内存的压力。对应的 effect 为 NoSchedule。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;node.kubernetes.io/disk-pressure&lt;/code&gt;: 节点有磁盘压力。和四个参数有关：nodefs.available, nodefs.inodesFree, imagefs.available, imagefs.inodesFree。nodefs 是用来存储卷和 daemon 日志的，imagefs 是容器运行时用来存储镜像和容器可写层的。当这些值到达某个阈值，就会出现 disk-pressure。对应的 effect 为 NoSchedule。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;node.kubernetes.io/network-unavailable&lt;/code&gt;: 节点的网络还不可用。对应的 effect 为 NoSchedule。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;node.kubernetes.io/unschedulable&lt;/code&gt;: 节点是不可调度的。对应的 effect 为 NoSchedule。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;node.kubernetes.io/pid-pressure&lt;/code&gt;: 节点上的进程太多。对应的 effect 为 NoSchedule。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>virtualbox 的几种网络模式</title><link>https://www.myway5.com/blog/virtualbox-network/</link><guid isPermaLink="true">https://www.myway5.com/blog/virtualbox-network/</guid><description>NAT 模式下，虚拟机连通外部网络类似于我们使用路由器上网。也就是说，虚拟机内部可以访问外部网络，外部网络无法直接连接虚拟机，但是可以通过端口转发的方式实现。 我们使用 virtualbox 启动一个 NAT 网络模式的虚拟机。查看它的网络接口：</description><pubDate>Tue, 21 Apr 2020 09:26:11 GMT</pubDate><content:encoded>&lt;h2&gt;1. NAT&lt;/h2&gt;
&lt;p&gt;NAT 模式下，虚拟机连通外部网络类似于我们使用路由器上网。也就是说，虚拟机内部可以访问外部网络，外部网络无法直接连接虚拟机，但是可以通过端口转发的方式实现。 我们使用 virtualbox 启动一个 NAT 网络模式的虚拟机。查看它的网络接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ ip link

1: lo: &amp;lt;LOOPBACK,UP,LOWER_UP&amp;gt; mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:8a:fe:e6 brd ff:ff:ff:ff:ff:ff
3: eth1: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:8e:f8:c6 brd ff:ff:ff:ff:ff:ff
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再看一下路由表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ip route

default via 10.0.2.2 dev eth0 proto dhcp metric 100 
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100 
192.168.88.0/24 dev eth1 proto kernel scope link src 192.168.88.101 metric 101
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认的路由规则是通过 &lt;code&gt;10.0.2.2&lt;/code&gt; 出去，这里 &lt;code&gt;10.0.2.2&lt;/code&gt; 这个设备就相当于路由器的地址。并且虚拟机的 &lt;code&gt;eth0&lt;/code&gt; 的地址 10.0.2.15 是通过 dhcp 来获得的。NAT 模式下的工作机制如下图： &lt;img src=&quot;/uploads/wp/2020/04/nat.png&quot; alt=&quot;nat&quot; /&gt; 当虚拟机启动时，它会使用 DHCP 来获取一个 IP 地址。VirtualBox 会处理这个 DHCP 请求，并且告诉虚拟机它分配到的 IP 地址和网关地址。在这种模式下，每个虚拟机都会分配相同的 IP 地址(10.0.2.15)，因为每个虚拟机都认为它们实在自己的隔离网络内。当它们通过网关(10.0.2.2)发送数据包时，VirtualBox重写这些包，让它们看起来是来自宿主机，而不是来自于虚拟机。 NAT 网络的特点如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虚拟机位于私有 LAN 中。&lt;/li&gt;
&lt;li&gt;VirtualBox 扮演一个 DHCP 服务。&lt;/li&gt;
&lt;li&gt;VirtualBox NAT 引擎来做地址转换。&lt;/li&gt;
&lt;li&gt;目标服务看到的流量是来自于 VirtualBox 宿主机。&lt;/li&gt;
&lt;li&gt;宿主机和虚拟机都不需要配置。&lt;/li&gt;
&lt;li&gt;虚拟机作为客户端时是非常合适的。&lt;/li&gt;
&lt;li&gt;虚拟机作为服务端不合适&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bridged Networking&lt;/h2&gt;
&lt;p&gt;桥接网络给我的第一印象就是和 linux 上的 bridge。在这种网络模式下，虚拟机和宿主机在网络拓扑中是平等的，宿主机上会有一个虚拟的 NIC 桥接到物理 NIC 上。关于这个 bridge 的实现，VirtualBox 在宿主机上使用了设备驱动来从物理网络适配器上过滤数据。因此这个驱动被称为 &lt;code&gt;net filter&lt;/code&gt;。这使得 VirtualBox 可以从物理网络上拦截数据以及注入数据，就像是用软件实现了一个网络接口一样。 如下图所示： &lt;img src=&quot;/uploads/wp/2020/04/bridged.png&quot; alt=&quot;bridged&quot; /&gt; bridged networking 的特点如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VirtualBox 负责桥接到主机网络（这也是在 linux 宿主机上并不能看到上面所谓的 bridge 的原因）&lt;/li&gt;
&lt;li&gt;对于客户端或服务端的虚拟机都很友好&lt;/li&gt;
&lt;li&gt;会消耗所处网络内的 IP 地址&lt;/li&gt;
&lt;li&gt;可能需要对虚拟机进行配置&lt;/li&gt;
&lt;li&gt;生产环境的最佳选择&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Internal Networking&lt;/h2&gt;
&lt;p&gt;Internal Networking 和 bridged networking 类似，可以和外部的网络通信。但是这里的外部网络仅指可以在同一宿主机上的相同的内网的其他虚拟机。如下图所示： &lt;img src=&quot;/uploads/wp/2020/04/internal.png&quot; alt=&quot;internal&quot; /&gt; 我们可以通过命令行创建一个 DHCP 服务，网络的名称是 &lt;code&gt;intnet&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ vboxmanage dhcpserver add -netname intnet --ip 10.10.0.1 --netmask 255.255.0.0 --lowerip 10.10.10.1 --upperip 10.10.10.255 --enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 VirtualBox 中创建虚拟机时加入这个网络即可。这个网络中的所有虚拟机都是和外界隔离的，包括宿主机。 Internal Networking 的特点是： - 虚拟机可以看到其他在同一个网络内的虚拟机 - 宿主机看不到内部网络 - 网络需要手动配置 - 即使宿主机没有网络也可以工作 - 可以和 Bridged 网络一起使用 - 适合多层解决方案&lt;/p&gt;
&lt;h2&gt;Host-Only Networking&lt;/h2&gt;
&lt;p&gt;Host-Only Networking 和 Internal Networking 是相似的，你可以指定虚拟机位于的网络，比如说：&lt;code&gt;vboxnet0&lt;/code&gt;。所有在 &lt;code&gt;vboxnet0&lt;/code&gt; 上的虚拟机都可以看见彼此，此外宿主机也可以看见这些虚拟机。当然，其他外部的机器没有办法看到这个网络上的虚拟机，因此取名为 &quot;Host-only&quot;。 其网络拓扑图如下： &lt;img src=&quot;/uploads/wp/2020/04/host_only.png.jpeg&quot; alt=&quot;host_only.png.jpeg&quot; /&gt; Host-Only 网络的特点如下： - VirtualBox 为虚拟机和宿主机创建私有的内部网络 - 宿主机上可以看到新的软件 NIC - VirtualBox 提供了 DHCP 服务 - 虚拟机 无法访问外部互联网 - 即使宿主机失去连接，虚拟机依然正常工作 - 适合开发的场景&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.virtualbox.org/manual/ch06.html&quot; rel=&quot;noopener&quot;&gt;Chapter 6. Virtual Networking&lt;/a&gt; &lt;a href=&quot;https://blogs.oracle.com/scoter/networking-in-virtualbox-v2&quot; rel=&quot;noopener&quot;&gt;Oracle VM VirtualBox: Networking options and how-to manage them&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>网络</category><category>virtualbox</category><author>joyme123</author></item><item><title>[Gaia Scheduler] gpu-manager 的虚拟化 gpu 分配流程</title><link>https://www.myway5.com/blog/gaia-scheduler-gpu-manager/</link><guid isPermaLink="true">https://www.myway5.com/blog/gaia-scheduler-gpu-manager/</guid><description>在之前的一篇文章主要是分析了 gpu-manager 的启动流程。关于 gpu-manager 应该会有一系列的文章，一是觉得这是一个很有价值的项目，二是为这个项目花了好几天去看代码，想通过写文章的方式对内容进行梳理。</description><pubDate>Wed, 08 Apr 2020 03:09:11 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;在之前的一篇文章主要是分析了 &lt;a href=&quot;https://www.myway5.com/index.php/2020/04/01/gpu-manager-%e5%90%af%e5%8a%a8%e6%b5%81%e7%a8%8b%e5%88%86%e6%9e%90/&quot; rel=&quot;noopener&quot;&gt;gpu-manager 的启动流程&lt;/a&gt;。关于 gpu-manager 应该会有一系列的文章，一是觉得这是一个很有价值的项目，二是为这个项目花了好几天去看代码，想通过写文章的方式对内容进行梳理。 这篇文章主要分析 gpu-manager 的虚拟 gpu 分配原理，我认为将虚拟 gpu 分配给容器主要有两个重点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gpu-manager 作为 device plugin 的工作流程&lt;/li&gt;
&lt;li&gt;虚拟 gpu 分配的最优方案，分配需要保证最少碎片，同时性能最好&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;从 pod 调度到虚拟 gpu 分配&lt;/h2&gt;
&lt;p&gt;这一部分会涉及到 &lt;code&gt;device plugin&lt;/code&gt; 的工作机制，因此不熟悉的话可以看一下之前的一篇文章：&lt;a href=&quot;https://www.myway5.com/index.php/2020/03/24/kubernetes%e5%bc%80%e5%8f%91%e7%9f%a5%e8%af%86-device-plugin%e7%9a%84%e5%ae%9e%e7%8e%b0/&quot; rel=&quot;noopener&quot;&gt;Kubernetes开发知识–device-plugin的实现&lt;/a&gt;。下面附上一张这篇文章中 &lt;code&gt;device plugin&lt;/code&gt; 的工作时序图： &lt;img src=&quot;/uploads/wp/2020/03/device-plugins.svg&quot; alt=&quot;device plugin&quot; /&gt; 在之前的启动流程分析文章中，说到 gpu-manager 向 kubelet 注册。在这之后， gpu-manager 就正式作为一个 device plugin 来工作了。这个时候，我们可以创建如下的 pod:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: Pod
metadata:
  name: tf-training-example-10
  namespace: test
  labels:
    name: tf-training-example
spec:
  restartPolicy: Never
  containers:
  - name: tf-training-example
    image: joyme/tf_training_example:1.5
    resources:
      requests:
        tencent.com/vcuda-core: 20
        tencent.com/vcuda-memory: 15
      limits:
        tencent.com/vcuda-core: 20
        tencent.com/vcuda-memory: 15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个创建 pod 的请求会到达 kubernetes 的 API Server，然后由 kube-scheduler 进行调度。kube-scheduler 的调度经过预选和优选两个阶段，确定了最佳的目标节点。这时候 kubelet 就上场了。因为我们的 pod 中的容器请求了 &lt;code&gt;vcuda-core&lt;/code&gt; 和 &lt;code&gt;vcuda-memory&lt;/code&gt; 这两个资源，但是 kubelet 并没有能力去给容器分配这些资源，于是它就找是谁注册了这些资源类型，然后发现是 vcore 和 vmemory 这两个服务注册的，于是使用 grpc 和 &lt;code&gt;/var/lib/kubelet/device-plugins/vcore.sock&lt;/code&gt; 以及 &lt;code&gt;/var/lib/kubelet/device-plugins/vmemory.sock&lt;/code&gt; 通过 unix socket 通讯。 vcore 和 vmemory 是两种资源，因此这里其实相当于注册了两个 device plugin。 对于 vcuda-memory，kubelet 调用的 &lt;code&gt;Allocate&lt;/code&gt; 方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;/** device plugin interface */
func (vr *vmemoryResourceServer) Allocate(ctx context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
    glog.V(2).Infof(&quot;%+v allocation request for vmemory&quot;, reqs)
    fakeData := make([]*pluginapi.ContainerAllocateResponse, 0)
    fakeData = append(fakeData, &amp;amp;pluginapi.ContainerAllocateResponse{})

    return &amp;amp;pluginapi.AllocateResponse{
        ContainerResponses: fakeData,
    }, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里其实并没有做任何实际分配操作，我们可以认为 vcuda-core 和 vcuda-memory 必然是同时申请分配的，因此我们只需要处理二者之一即可。 对于 vcuda-core，kubelet 会调用的 &lt;code&gt;Allocate&lt;/code&gt; 方法代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (vr *vcoreResourceServer) Allocate(ctx context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
    glog.V(2).Infof(&quot;%+v allocation request for vcore&quot;, reqs)
    return vr.mgr.Allocate(ctx, reqs)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终会走到 &lt;code&gt;pkg/services/allocator/nvidia/allocator.go&lt;/code&gt; 的 Allcate 方法中。下面就来到这篇文章最复杂的部分了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (ta *NvidiaTopoAllocator) Allocate(_ context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们先看一下函数原型，&lt;code&gt;reqs *pluginapi.AllocateRequest&lt;/code&gt; 这个参数是分配请求，然后返回了一个分配响应 &lt;code&gt;*pluginapi.AllocateResponse&lt;/code&gt;。这里看一下 &lt;code&gt;AllocateRequest&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// - Allocate is expected to be called during pod creation since allocation
//   failures for any container would result in pod startup failure.
// - Allocate allows kubelet to exposes additional artifacts in a pod&apos;s
//   environment as directed by the plugin.
// - Allocate allows Device Plugin to run device specific operations on
//   the Devices requested
type AllocateRequest struct {
    ContainerRequests []*ContainerAllocateRequest `protobuf:&quot;bytes,1,rep,name=container_requests,json=containerRequests&quot; json:&quot;container_requests,omitempty&quot;`
}

type ContainerAllocateRequest struct {
    DevicesIDs []string `protobuf:&quot;bytes,1,rep,name=devicesIDs&quot; json:&quot;devicesIDs,omitempty&quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很明显，请求里包含了每个容器需要的设备数组。同时通过 &lt;code&gt;AllocateRequest&lt;/code&gt; 上的注释可以得出以下信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Allocate 是在 pod 创建时被调用的，因此任何容器分配失败都会造成pod启动失败。&lt;/li&gt;
&lt;li&gt;Allocate 允许 kubelet 在 pod 环境中引入更多的 artifacts，这部分工作由我们的 device plugin 主导。对于 gpu manager 来说就是，覆盖容器内的 LD_LIBRARY_PATH，挂载 cuda 库文件等等。&lt;/li&gt;
&lt;li&gt;Allocate 允许 device plugin 在设备上运行特定的操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后再来看一下 &lt;code&gt;AllocateResponse&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// AllocateResponse includes the artifacts that needs to be injected into
// a container for accessing &apos;deviceIDs&apos; that were mentioned as part of
// &apos;AllocateRequest&apos;.
// Failure Handling:
// if Kubelet sends an allocation request for dev1 and dev2.
// Allocation on dev1 succeeds but allocation on dev2 fails.
// The Device plugin should send a ListAndWatch update and fail the
// Allocation request
type AllocateResponse struct {
    ContainerResponses []*ContainerAllocateResponse `protobuf:&quot;bytes,1,rep,name=container_responses,json=containerResponses&quot; json:&quot;container_responses,omitempty&quot;`
}

type ContainerAllocateResponse struct {
    // List of environment variable to be set in the container to access one of more devices.
    Envs map[string]string `protobuf:&quot;bytes,1,rep,name=envs&quot; json:&quot;envs,omitempty&quot; protobuf_key:&quot;bytes,1,opt,name=key,proto3&quot; protobuf_val:&quot;bytes,2,opt,name=value,proto3&quot;`
    // Mounts for the container.
    Mounts []*Mount `protobuf:&quot;bytes,2,rep,name=mounts&quot; json:&quot;mounts,omitempty&quot;`
    // Devices for the container.
    Devices []*DeviceSpec `protobuf:&quot;bytes,3,rep,name=devices&quot; json:&quot;devices,omitempty&quot;`
    // Container annotations to pass to the container runtime
    Annotations map[string]string `protobuf:&quot;bytes,4,rep,name=annotations&quot; json:&quot;annotations,omitempty&quot; protobuf_key:&quot;bytes,1,opt,name=key,proto3&quot; protobuf_val:&quot;bytes,2,opt,name=value,proto3&quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们又可以看到一些关键信息，&lt;code&gt;AllocateResponse&lt;/code&gt; 为每个容器返回了 &lt;code&gt;ContainerAllocateResponse&lt;/code&gt;，包括容器的环境变量，容器的挂载，容器的设备信息，容器的 annotations 信息。其中，容器的设备信息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// DeviceSpec specifies a host device to mount into a container.
type DeviceSpec struct {
    // Path of the device within the container.
    ContainerPath string `protobuf:&quot;bytes,1,opt,name=container_path,json=containerPath,proto3&quot; json:&quot;container_path,omitempty&quot;`
    // Path of the device on the host.
    HostPath string `protobuf:&quot;bytes,2,opt,name=host_path,json=hostPath,proto3&quot; json:&quot;host_path,omitempty&quot;`
    // Cgroups permissions of the device, candidates are one or more of
    // * r - allows container to read from the specified device.
    // * w - allows container to write to the specified device.
    // * m - allows container to create device files that do not yet exist.
    Permissions string `protobuf:&quot;bytes,3,opt,name=permissions,proto3&quot; json:&quot;permissions,omitempty&quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即在容器中挂载设备需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设备相对于容器的地址&lt;/li&gt;
&lt;li&gt;设备在宿主机上的地址&lt;/li&gt;
&lt;li&gt;设备的 Cgroups 信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时候我们再来重新思考 gpu-manager 的 gpu 虚拟化原理。如果你看过腾讯关于 Gaia Scheduler 的论文，就会知道 gpu-manager 需要做以下工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为容器挂载 cuda 相关的库，包括 vcuda-control 这个项目的拦截库&lt;/li&gt;
&lt;li&gt;通过覆盖容器中的 LD_LIBRARY_PATH 来将 cuda 调用指向 libcuda-control.so 这个库，这个库里面对显存和计算 api 做了拦截。&lt;/li&gt;
&lt;li&gt;为容器挂载 vcuda.sock，在容器调用特定的 cuda api 时，会触发 grpc 调用，通过 vcuda.sock 和 virtual manager 通信，virtual manager 下发容器配置。这样拦截库就知道自己应该怎么限制容器了。这里留一个问题 A，为什么要大费周章的通过 grpc，直接挂载容器配置文件可行吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些 gpu-manager 要做的工作都是 device plugin 的 Allocate 调用提供的能力。所以 gpu-manager 需要在 Allocate 期间完成这么多的工作。这也是这部分比较复杂的原因。下面我们带着这些信息去看代码，会更容易懂一些。下面的代码都是来自于 &lt;code&gt;pkg/services/allocator/nvidia/allocator.go&lt;/code&gt; 中的 Allocate 方法，但是因为很长，所以我会截取出来分析。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// k8s send allocate request for one container at a time
req := reqs.ContainerRequests[0]
resps := &amp;amp;pluginapi.AllocateResponse{}
reqCount = uint(len(req.DevicesIDs))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这部分取了 Allocate 中的第一个 ContainerRequest，通过注释知道，k8s 一次只为一个容器发送分配请求。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;    if ta.unfinishedPod != nil {

    } else {

    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来有一个对 &lt;code&gt;unfinishedPod&lt;/code&gt; 的判断，因为 k8s 一次请求只针对一个容器，因此这里的 &lt;code&gt;unfinishedPod&lt;/code&gt; 指的是只分配了部分容器，还有其他容器没有分配的 pod。这里我们需要仔细思考一下，使用 &lt;code&gt;unfinishedPod&lt;/code&gt; 的目的是什么？看到这里我有两个猜测：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;因为 k8s 一次请求只针对一个容器，所以为了优先分配完一个 pod，就需要标记 &lt;code&gt;unfinishedPod&lt;/code&gt; 了。但是仔细想想，因为 Allocate 的请求和响应中都没有容器的信息，所以本次请求分配的容器是由 kubelet 决定的。device plugin 并没有能力改变容器的分配顺序，这个想法是错的。&lt;/li&gt;
&lt;li&gt;为了性能考虑。因为 gpu-manager 有两个 device plugin：vmemory 和 vcore。但是之前说到 vmemory 的分配没有做任何工作。所以我们不得不在分配 vcore 的时候，把 vmemory 的分配工作也做了。可是我怎么知道当前正在给哪个容器分配资源？那我也更不知道分配多少 vmemory 了。但是天无绝人之路啊，我可以遍历当前节点上的所有 pod，然后挑出需要 gpu 资源的 pod。然后再从这些 pod 中挑出符合这次请求的容器。这里如果使用 &lt;code&gt;unfinishedPod&lt;/code&gt; 就避免了重复的大规模查找操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么，假设现在还有一个未完成的 pod，会执行下面的代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 候选pod
candidatePod = ta.unfinishedPod
// 从已分配的pod中查找
cache := ta.allocatedPod.GetCache(string(candidatePod.UID))
if cache == nil {
    msg := fmt.Sprintf(&quot;failed to find pod %s in cache&quot;, candidatePod.UID)
    glog.Infof(msg)
    return nil, fmt.Errorf(msg)
}
for i, c := range candidatePod.Spec.Containers {
    if _, ok := cache[c.Name]; ok {
        continue
    }

    if !utils.IsGPURequiredContainer(&amp;amp;c) {
        continue
    }

    if reqCount != utils.GetGPUResourceOfContainer(&amp;amp;candidatePod.Spec.Containers[i], types.VCoreAnnotation) {
        msg := fmt.Sprintf(&quot;allocation request mismatch for pod %s, reqs %v&quot;, candidatePod.UID, reqs)
        glog.Infof(msg)
        return nil, fmt.Errorf(msg)
    }
    // 候选的容器（应该就是待分配资源的容器）
    candidateContainer = &amp;amp;candidatePod.Spec.Containers[i]
    found = true
    break
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这段代码遍历这个 pod 的容器列表，然后和缓存中的容器对比，如果没有分配并且需要 gpu 资源，并且容器请求的资源量和当前的分配请求一致，就认定这个容器是我们接下来要为之分配的候选人了。这里我们又有一个问题 B，如果一个 Pod 中有多个 vcore 请求一致，但是 vmemory 不同的容器，这里只通过 vcore 的请求量来判断，可以保证这个分配请求和我们的候选容器能对的上吗？这个问题我们可以产生如下的猜测：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;AllocateRequest 是按照 Pod 中的容器顺序来的，这样我们在做 reqCount 对比的时候，因为顺序一致就能保证请求和候选容器是对应关系了。那么，AllocateRequest 是按照 Pod 中容器顺序来的吗？这是一个新的问题 C。&lt;/li&gt;
&lt;li&gt;其实请求和候选容器不对应也没关系，因为容器中进行 cuda 调用拦截的时候，才会请求 virtual manager，拿到容器的资源限制配置信息。只要这个环节能保证容器和其请求的资源量对应上，就不会有任何问题？这也是我们的问题 E：cuda 调用拦截的时候，如何保证容器和配置的对应关系。这也和问题 A 相呼应，如果这个猜测成立，那就是为什么问题 A 中要大费周折的使用 grpc 调用下发配置，而不是直接把配置信息挂载或写到容器的变量中。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来我们继续看，如果没有未完成的容器，就执行以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 获取候选的pod,候选的pod是当前节点上的需要GPU,没有分配并且不应该删除的pod
pods, err := getCandidatePods(ta.k8sClient, ta.config.Hostname)
if err != nil {
    msg := fmt.Sprintf(&quot;Failed to find candidate pods due to %v&quot;, err)
    glog.Infof(msg)
    return nil, fmt.Errorf(msg)
}

for _, pod := range pods {
    if found {
        break
    }
    for i, c := range pod.Spec.Containers {
        if !utils.IsGPURequiredContainer(&amp;amp;c) {
            continue
        }
        podCache := ta.allocatedPod.GetCache(string(pod.UID))
        if podCache != nil {
            if _, ok := podCache[c.Name]; ok {
                glog.Infof(&quot;container %s of pod %s has been allocate, continue to next&quot;, c.Name, pod.UID)
                continue
            }
        }
        if utils.GetGPUResourceOfContainer(&amp;amp;pod.Spec.Containers[i], types.VCoreAnnotation) == reqCount {
            glog.Infof(&quot;Found candidate Pod %s(%s) with device count %d&quot;, pod.UID, c.Name, reqCount)
            candidatePod = pod
            candidateContainer = &amp;amp;pod.Spec.Containers[i]
            found = true
            break
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和上面的不同之处，就是在获取候选 pod 这里。获取候选 pod 的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;    candidatePods := []*v1.Pod{}
    allPods, err := getPodsOnNode(client, hostname, string(v1.PodPending))

    for _, pod := range allPods {
        current := pod
        if utils.IsGPURequiredPod(¤t) &amp;amp;&amp;amp; !utils.IsGPUAssignedPod(¤t) &amp;amp;&amp;amp; !utils.ShouldDelete(¤t) {
            candidatePods = append(candidatePods, ¤t)
        }
    }

    return OrderPodsdByPredicateTime(candidatePods), nil
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先是获取节点上的所有 pod，然后从节点上的 pod 中选取需要 GPU，并且没有分配 GPU，并且不应该删除的 pod。最后得到一个候选 pod 列表。最后对这个列表根据时间排序。这样就可以拿到最先被调度的 pod 了。这里其实也默认了一个前提，最先调度的 pod 会最先发出分配请求。这里还有一个需要注意的地方，排序依据的时间有两个选择：预选时间或创建时间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;    if predicateTimeStr, ok := pod.ObjectMeta.Annotations[types.PredicateTimeAnnotation]; ok {
        u64, err := strconv.ParseUint(predicateTimeStr, 10, 64)
        if err != nil {
            glog.Warningf(&quot;Failed to parse predicate Timestamp %s due to %v&quot;, predicateTimeStr, err)
        } else {
            predicateTime = u64
        }
    } else {
        // If predicate time not found, use createionTimestamp instead
        predicateTime = uint64(pod.ObjectMeta.CreationTimestamp.UnixNano())
    }

    return predicateTime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，预选时间并不是 kube-scheduler 添加的，而是和 gpu-manager 配合使用的 gpu-admission 这个项目。如果没有预选时间，就会使用 pod 的创建时间。这也就是说，我们不使用 gpu-admission 这个项目，也可以正常使用 gpu-manager。其实这里还有一个问题 D，我怎么保证挑出来的容器就是这次分配请求的呢？这个问题还要留在后面的分析中。 现在我们拿到了候选容器，就需要进行真正的分配工作了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// get vmemory info from container spec
vmemory := utils.GetGPUResourceOfContainer(candidateContainer, types.VMemoryAnnotation)
for i := 0; i &amp;lt; int(vmemory); i++ {
    req.DevicesIDs = append(req.DevicesIDs, types.VMemoryAnnotation)
}

resp, err := ta.allocateOne(candidatePod, candidateContainer, req)
if err != nil {
    glog.Errorf(err.Error())
    return nil, err
}
resps.ContainerResponses = append(resps.ContainerResponses, resp)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码中，我们拿到容器的 vmemory 信息。因为 vmemory 是根据数量划分的。1 个 vmemory 相当于 256M 的 memory，也就是一个 deviceID。这里请求多少的 vmemory，就存多少个 deviceID。然后调用 &lt;code&gt;allocateOne&lt;/code&gt; 为单个容器进行真正的分配工作。下面我们开始分析 &lt;code&gt;allocateOne&lt;/code&gt; 的分配逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;var (
    nodes                       []*nvtree.NvidiaNode
    needCores, needMemoryBlocks int64
    predicateMissed             bool
    allocated                   bool
)

// 是否是 gpu 预选 pod
predicateMissed = !utils.IsGPUPredicatedPod(pod)
// 单节点的总内存
singleNodeMemory := int64(ta.tree.Leaves()[0].Meta.TotalMemory)
for _, v := range req.DevicesIDs {
    if strings.HasPrefix(v, types.VCoreAnnotation) {
        // 请求 core
        needCores++
    } else if strings.HasPrefix(v, types.VMemoryAnnotation) {
        // 请求 memory
        needMemoryBlocks++
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先就是根据 deviceID 来计算需要多少 core 和 memory。接下来会调用 &lt;code&gt;ta.recycle()&lt;/code&gt; 回收资源。回收的逻辑如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (ta *NvidiaTopoAllocator) recycle() {
    activePods := watchdog.GetActivePods()

    lastActivePodUids := sets.NewString()
    activePodUids := sets.NewString()
    for _, uid := range ta.allocatedPod.Pods() {
        lastActivePodUids.Insert(uid)
    }
    for uid := range activePods {
        activePodUids.Insert(uid)
    }

    // difference 出来的就是已经运行结束的pod，可以回收分配的gpu资源
    podsToBeRemoved := lastActivePodUids.Difference(activePodUids)

    glog.V(5).Infof(&quot;Pods to be removed: %v&quot;, podsToBeRemoved.List())

    // 释放资源
    ta.freeGPU(podsToBeRemoved.List())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对已分配的 pod 和 正在运行的 pod 集合取差集，差集就是分配了资源但是已经停止运行的 pod 。然后对这部分 pod 释放 GPU 资源。具体的释放逻辑放在后面分析。现在继续向下看，这里我们直接跳到尝试分配资源的逻辑上。分配 gpu 资源分为三种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果需要的核心数大于 100，也就是说超过一个物理 GPU，就使用 link 评估器来选出 GPU 节点&lt;/li&gt;
&lt;li&gt;如果正好是一个 100 核心，则使用 fragment 评估器&lt;/li&gt;
&lt;li&gt;如果小于 100 核心，则使用 share 评估器。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;情况 1 的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;eval, ok := ta.evaluators[&quot;link&quot;]
if !ok {
    return nil, fmt.Errorf(&quot;can not find evaluator link&quot;)
}
if needCores%nvtree.HundredCore &amp;gt; 0 {
    return nil, fmt.Errorf(&quot;cores are greater than %d, must be multiple of %d&quot;, nvtree.HundredCore, nvtree.HundredCore)
}
nodes = eval.Evaluate(needCores, 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意到这里还要求请求的核心数必须是 100 的整数，也就是说必须是整数个物理 GPU，你不能请求 1.5 个 物理 GPU 这种。 情况 2 的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;eval, ok := ta.evaluators[&quot;fragment&quot;]
if !ok {
    return nil, fmt.Errorf(&quot;can not find evaluator fragment&quot;)
}
nodes = eval.Evaluate(needCores, 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;情况 3 的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// EnableShare 是在启动时指定的参数，代表是否允许多个容器共享一个gpu
if !ta.config.EnableShare {
    return nil, fmt.Errorf(&quot;share mode is not enabled&quot;)
}
if needCores == 0 || needMemory == 0 {
    return nil, fmt.Errorf(&quot;that cores or memory is zero is not permitted in share mode&quot;)
}

// evaluate in share mode
shareMode = true
// 使用 share 评估
eval, ok := ta.evaluators[&quot;share&quot;]
if !ok {
    return nil, fmt.Errorf(&quot;can not find evaluator share&quot;)
}
// 评估出来的合适的 nvidia gpu 节点
nodes = eval.Evaluate(needCores, needMemory)
if len(nodes) == 0 {
    if shareMode &amp;amp;&amp;amp; needMemory &amp;gt; singleNodeMemory {
        return nil, fmt.Errorf(&quot;request memory %d is larger than %d&quot;, needMemory, singleNodeMemory)
    }

    return nil, fmt.Errorf(&quot;no free node&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在评估出来节点之后，会先判断这个这个 pod 是否真的经过预选阶段？判断方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func IsGPUPredicatedPod(pod *v1.Pod) (predicated bool) {
    glog.V(4).Infof(&quot;Determine if the pod %s needs GPU resource&quot;, pod.Name)
    var ok bool

    // Check if pod request for GPU resource
    if GetGPUResourceOfPod(pod, types.VCoreAnnotation) &amp;lt;= 0 || GetGPUResourceOfPod(pod, types.VMemoryAnnotation) &amp;lt;= 0 {
        glog.V(4).Infof(&quot;Pod %s in namespace %s does not Request for GPU resource&quot;,
            pod.Name,
            pod.Namespace)
        return predicated
    }

    // Check if pod already has predicate time
    // tencent.com/predicate-time 是 gpu-admission 中添加的。
    if _, ok = pod.ObjectMeta.Annotations[types.PredicateTimeAnnotation]; !ok {
        glog.V(4).Infof(&quot;No predicate time for pod %s in namespace %s&quot;,
            pod.Name,
            pod.Namespace)
        return predicated
    }

    // Check if pod has already been assigned
    if assigned, ok := pod.ObjectMeta.Annotations[types.GPUAssigned]; !ok {
        glog.V(4).Infof(&quot;No assigned flag for pod %s in namespace %s&quot;,
            pod.Name,
            pod.Namespace)
        return predicated
    } else if assigned == &quot;true&quot; {
        glog.V(4).Infof(&quot;pod %s in namespace %s has already been assigned&quot;,
            pod.Name,
            pod.Namespace)
        return predicated
    }
    predicated = true
    return predicated
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;共有三个要求才算经过了预选：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;resource 字段请求了 vcore 和 vgpu，并且大于 0。&lt;/li&gt;
&lt;li&gt;必须有 &lt;code&gt;tencent.com/predicate-time&lt;/code&gt; 字段。这点要求必须经过 gpu-admission 的预选阶段。&lt;/li&gt;
&lt;li&gt;没有被分配 gpu 资源&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果经过预选的话，就需要执行以下的逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// get predicate node by annotation
containerIndex, err := utils.GetContainerIndexByName(pod, container.Name)
if err != nil {
    return nil, err
}
var devStr string
if idxStr, ok := pod.ObjectMeta.Annotations[types.PredicateGPUIndexPrefix+strconv.Itoa(containerIndex)]; ok {
    if _, err := strconv.Atoi(idxStr); err != nil {
        return nil, fmt.Errorf(&quot;predicate idx %s invalid for pod %s &quot;, idxStr, pod.UID)
    }
    devStr = types.NvidiaDevicePrefix + idxStr
    if !utils.IsValidGPUPath(devStr) {
        return nil, fmt.Errorf(&quot;predicate idx %s invalid&quot;, devStr)
    }
} else {
    return nil, fmt.Errorf(&quot;failed to find predicate idx for pod %s&quot;, pod.UID)
}

predicateNode := ta.tree.Query(devStr)
if predicateNode == nil {
    return nil, fmt.Errorf(&quot;failed to get predicate node %s&quot;, devStr)
}

// check if we choose the same node as scheduler
if predicateNode.MinorName() != nodes[0].MinorName() {
    return nil, fmt.Errorf(&quot;Nvidia node mismatch for pod %s(%s), pick up:%s  predicate: %s&quot;,
        pod.Name, container.Name, nodes[0].MinorName(), predicateNode.MinorName())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，经过预选阶段的 Pod 都会根据容器的顺序在 &lt;code&gt;Annotations&lt;/code&gt; 为该容器写上配置信息。这说明在 &lt;code&gt;gpu-admission&lt;/code&gt; 这个项目中会为容器分配 gpu 设备。最后还要检查一下在 &lt;code&gt;gpu-manager&lt;/code&gt; 中分配的 gpu 设备和 &lt;code&gt;gpu-admission&lt;/code&gt; 中是否一致，不一致的话也会返回分配失败。 现在我们已经知道要为当前请求的容器分配哪个 gpu 设备，以及分配的资源数量。这样就可以构建 &lt;code&gt;ContainerAllocateResponse&lt;/code&gt; 了。先把已分配的设备放到响应中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;    for _, n := range nodes {
        name := n.MinorName()
        glog.V(2).Infof(&quot;Allocate %s for %s(%s), Meta (%d:%d)&quot;, name, pod.UID, container.Name, n.Meta.ID, n.Meta.MinorID)

        ctntResp.Annotations[types.VCoreAnnotation] = fmt.Sprintf(&quot;%d&quot;, needCores)
        ctntResp.Annotations[types.VMemoryAnnotation] = fmt.Sprintf(&quot;%d&quot;, needMemory)

        ctntResp.Devices = append(ctntResp.Devices, &amp;amp;pluginapi.DeviceSpec{
            ContainerPath: name,
            HostPath:      name,
            Permissions:   &quot;rwm&quot;,
        })
        deviceList = append(deviceList, n.Meta.UUID)

        if !allocated {
            // 在 gpu tree 中标记设备已占用
            ta.tree.MarkOccupied(n, needCores, needMemory)
        }
        allocatedDevices.Insert(name)
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更改响应的 &lt;code&gt;Annotations&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;ctntResp.Annotations[types.VDeviceAnnotation] = vDeviceAnnotationStr(nodes)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查 pod 的所有容器是否都完成了分配，并把新的分配信息写入到 checkpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;unfinished := false
for _, c := range pod.Spec.Containers {
    if !utils.IsGPURequiredContainer(&amp;amp;c) {
        continue
    }
    podCache := ta.allocatedPod.GetCache(string(pod.UID))
    if podCache != nil {
        if _, ok := podCache[c.Name]; !ok {
            unfinished = true
            break
        }
    }
}
if unfinished {
    ta.unfinishedPod = pod
} else {
    ta.unfinishedPod = nil
}
ta.writeCheckpoint()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在响应中为容器添加 &lt;code&gt;/dev/nvidiactl&lt;/code&gt; 和 &lt;code&gt;/dev/nvidia-uvm&lt;/code&gt;，如果配置了 extraConfig，还会把里面要默认添加的设备加进去：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Append control device
ctntResp.Devices = append(ctntResp.Devices, &amp;amp;pluginapi.DeviceSpec{
    ContainerPath: types.NvidiaCtlDevice,
    HostPath:      types.NvidiaCtlDevice,
    Permissions:   &quot;rwm&quot;,
})

ctntResp.Devices = append(ctntResp.Devices, &amp;amp;pluginapi.DeviceSpec{
    ContainerPath: types.NvidiaUVMDevice,
    HostPath:      types.NvidiaUVMDevice,
    Permissions:   &quot;rwm&quot;,
})

// Append default device
if cfg, found := ta.extraConfig[&quot;default&quot;]; found {
    for _, dev := range cfg.Devices {
        ctntResp.Devices = append(ctntResp.Devices, &amp;amp;pluginapi.DeviceSpec{
            ContainerPath: dev,
            HostPath:      dev,
            Permissions:   &quot;rwm&quot;,
        })
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，响应中的设备信息已经处理结束，接下来处理容器中的环境变量，gpu manager 需要通过修改 &lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt; 来劫持程序对 cuda 的调用，然后通过 &lt;code&gt;NVIDIA_VISIBLE_DEVICES&lt;/code&gt; 来让挂载的设备可见。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// LD_LIBRARY_PATH
ctntResp.Envs[&quot;LD_LIBRARY_PATH&quot;] = &quot;/usr/local/nvidia/lib64&quot;
for _, env := range container.Env {
    if env.Name == &quot;compat32&quot; &amp;amp;&amp;amp; strings.ToLower(env.Value) == &quot;true&quot; {
        ctntResp.Envs[&quot;LD_LIBRARY_PATH&quot;] = &quot;/usr/local/nvidia/lib&quot;
    }
}

// NVIDIA_VISIBLE_DEVICES
ctntResp.Envs[&quot;NVIDIA_VISIBLE_DEVICES&quot;] = strings.Join(deviceList, &quot;,&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着根据是否处于 &lt;code&gt;shareMode&lt;/code&gt;，也就是单个 gpu 能否被共享来挂载不同的 host 目录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;if shareMode {
    // nvidia 是劫持的库，用在shareMode这种情况
    ctntResp.Mounts = append(ctntResp.Mounts, &amp;amp;pluginapi.Mount{
        ContainerPath: &quot;/usr/local/nvidia&quot;,
        HostPath:      types.DriverLibraryPath,
        ReadOnly:      true,
    })
} else {
    // 非shareMode用正常的库即可
    ctntResp.Mounts = append(ctntResp.Mounts, &amp;amp;pluginapi.Mount{
        ContainerPath: &quot;/usr/local/nvidia&quot;,
        HostPath:      types.DriverOriginLibraryPath,
        ReadOnly:      true,
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;shareMode&lt;/code&gt; 下，会挂载的 host 目录是 &lt;code&gt;/etc/gpu-manager/vdriver/nvidia&lt;/code&gt;，这里面是被劫持的库。否则挂载 &lt;code&gt;/etc/gpu-manager/vdriver/origin&lt;/code&gt;，里面是原始的 CUDA 库。 紧接着，将 host 上的 &lt;code&gt;/etc/gpu-manager/vm/{podUID}&lt;/code&gt; 挂载到容器中，这个是为了容器内可以通过 &lt;code&gt;vcuda.sock&lt;/code&gt; 和 &lt;code&gt;virtual-manager&lt;/code&gt; 通信。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 将host上的/etc/gpu-manager/vm/podUID挂载进去(vcuda.sock)，这个目录是在PreStartContainer期间由VirtualManager创建的
ctntResp.Mounts = append(ctntResp.Mounts, &amp;amp;pluginapi.Mount{
    ContainerPath: types.VCUDA_MOUNTPOINT,
    HostPath:      filepath.Join(ta.config.VirtualManagerPath, string(pod.UID)),
    ReadOnly:      true,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果当前请求的容器所属 Pod 没有经过 &lt;code&gt;gpu-admission&lt;/code&gt;，还会被放到一个处理队列中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;if predicateMissed {
    ar := &amp;amp;allocateResult{
        pod:     pod,
        result:  PREDICATE_MISSING,
        resChan: make(chan struct{}),
    }

    // 这个 queue 的处理是在virtualmanager里面的process方法
    ta.queue.AddRateLimited(ar)
    &amp;lt;-ar.resChan
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个队列会在 &lt;code&gt;pkg/service/allocator/nvidia/allocator.go&lt;/code&gt; 的 &lt;code&gt;proccessResult&lt;/code&gt; 中处理。 这样，kubelet 调用 &lt;code&gt;Allocate&lt;/code&gt; 方法就结束了。这里再来回顾一下上面遗留的问题和相关逻辑： 问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;问题 A: 为什么要大费周章的通过 grpc，直接挂载容器配置文件可行吗？ 总结： 这一点在上面的阅读中可以发现，这时候容器本身对自己应该限制多少的 gpu 资源调用并不知道。这个问题得和 B/C/D 问题结合来看。因为做　Allocate 调用时，kubelet 并没有告知此时在为哪个容器请求配置。因此只能根据请求的资源量以及 Pod 的 predicateTime 或 createTime 来判断。这个是无法保证一定准确的，因此此时容器的具体资源配置也无法确定。可能这就是要通过 grpc 而不是挂载容器配置文件的原因吧。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;问题 B: 如果一个 Pod 中有多个 vcore 请求一致，但是 vmemory 不同的容器，这里只通过 vcore 的请求量来判断，可以保证这个分配请求和我们的候选容器能对的上吗？ 总结：问题 B 是在 unfinisedPod 中查找当前请求的容器。只要能保证 unfinishedPod 是正确的（问题 D 说明不能保证），那么就可以保证容器是对的上的（问题 C 保证了这个结论）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;问题 C: AllocateRequest 是按照 Pod 中的容器顺序来的? 总结：对于这个问题，最好的回答方式是去看 &lt;code&gt;kubelet&lt;/code&gt; 的源代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;for _, container := range pod.Spec.Containers {
    if err := m.allocateContainerResources(pod, &amp;amp;container, devicesToReuse); err != nil {
        return err
    }
    m.podDevices.removeContainerAllocatedResources(string(pod.UID), container.Name, devicesToReuse)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这边做 &lt;code&gt;Allocate&lt;/code&gt; 的时候，是顺序遍历 Pod 中的容器，因此这个问题的答案是肯定的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;问题 D: 遍历当前节点上的所有 pod，然后挑出需要 gpu 资源的 pod，根据 &lt;code&gt;predicatedTime&lt;/code&gt; 或 &lt;code&gt;createTime&lt;/code&gt; 排序。然后再从这些 pod 中， 按顺序挑出符合这次请求的容器，怎么保证挑出来的容器就是这次分配请求的呢？ 总结：我觉得回答这个问题，需要确定两个大前提，一是 Pod 从创建到发起 Allocate 的过程，都是顺序的。这样就能保证当调用 Allocate 对应的 Pod 永远是尚未分配到资源的第一个。二是在一个 Pod 中，为每个容器 Allocate 时，也是顺序的，这一点在问题 &lt;code&gt;C&lt;/code&gt; 中得到确认。 但是实际上，第一个前提是不能保证的，在 Pod &lt;code&gt;bind&lt;/code&gt; 到节点时，这个是并发执行的。因此可以得出一个结论：在这个阶段无法保证 Allocate 请求和我们的候选容器是对应关系。关于这一点我也提了个 issue：&lt;a href=&quot;https://github.com/tkestack/gpu-manager/issues/17&quot; rel=&quot;noopener&quot;&gt;a question about Allocate for a container?&lt;/a&gt;。官方也给了回答，因为这个原因 gpu manager 有时候会报 &lt;code&gt;UnexpectedAdmissionError&lt;/code&gt; 错误。 所以根据问题 4，我们还要使用 &lt;code&gt;gpu-admission&lt;/code&gt; 这个项目，来保证该阶段的正确性，具体机制还得等到看 &lt;code&gt;gpu-admission&lt;/code&gt; 的时候才能知道了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实以上四个问题都是因为 kubelet 的 Allocate 请求不会带上正在分配的容器。所以需要一系列的查找方式来确定具体的容器。 因为篇幅问题，关于 gpu 的最佳分配策略会作为下一篇文章的内容。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.infoq.cn/article/or7CRphTDlX1IVhsFNgk&quot; rel=&quot;noopener&quot;&gt;从零开始入门 K8s：调度器的调度流程和算法介绍&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>kubernetes 的挂载传播(mount propagation)机制</title><link>https://www.myway5.com/blog/kubernetes-mount-propagation/</link><guid isPermaLink="true">https://www.myway5.com/blog/kubernetes-mount-propagation/</guid><description>今天在看 kubectl-debug 这个项目的时候，看到其部署文件的 volumeMounuts 中使用了一个 mountPropagation 字段，因为不清楚这个字段的作用，就做了一下了解。</description><pubDate>Sun, 05 Apr 2020 14:39:15 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;今天在看 &lt;a href=&quot;https://github.com/aylei/kubectl-debug&quot; rel=&quot;noopener&quot;&gt;kubectl-debug&lt;/a&gt; 这个项目的时候，看到其部署文件的 volumeMounuts 中使用了一个 mountPropagation 字段，因为不清楚这个字段的作用，就做了一下了解。mount propagation 背后的东西还是很多的，因此整理了这篇文章，顺便梳理一下知识点。 kubernetes 的 mount propagation 翻译成中文就是挂载传播。挂载传播提供了共享卷挂载的能力，它允许在同一个 Pod，甚至同一个节点内，在多个容器之间共享卷的挂载。&lt;/p&gt;
&lt;h2&gt;kubernetes 的挂载传播&lt;/h2&gt;
&lt;p&gt;卷的挂载传播由 Container.volumeMounts 的 mountPropagation 字段控制。它的值有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;None&lt;/code&gt;: 这种卷挂载将不会收到任何后续由 host 创建的在这个卷上或其子目录上的挂载。同样的，由容器创建的挂载在 host 上也是不可见的。这是默认的模式。这个其实很好理解，就是容器内和 host 的后续挂载完全隔离。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;HostToContainer&lt;/code&gt;: 这种卷挂载将会收到之后所有的由 host 创建在该卷上或其子目录上的挂载。换句话说，如果 host 在卷挂载内挂载的任何内容，在容器中都是可见的。同样，如果任何具有 &lt;code&gt;Bidirectional&lt;/code&gt; 的 Pod 挂载传播到该卷挂载上，具有 &lt;code&gt;HostToContainer&lt;/code&gt; 的挂载传播都可以看见。整个挂载传播的流程如下：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2020/04/excalidraw-202033110352-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Bidirectional&lt;/code&gt;: 这种挂载机制和 &lt;code&gt;HostToContainer&lt;/code&gt; 类似。此外，任何在容器中创建的挂载都会传播到 host，然后传播到使用相同卷的所有 Pod 的所有容器。注意：Bidirectional 挂载传播是很危险的。可能会危害到 host 的操作系统。因此只有特权容器在允许使用它。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在了解了这几种挂载传播之后，我们可以做一些实验来验证一下，首先验证的是 &lt;code&gt;None&lt;/code&gt; 的挂载传播类型，我们创建一个 nginx 的Pod:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: Pod
metadata:
    name: mount-a
    namespace: default
    label:
      app: mount
spec:
    containers:
    - name: main
      image: nginx:latest
      volumeMounts:
      - name: testmount
        mountPath: /home
        mountPropagation: None
    volumes:
    - name: testmount
      hostPath:
        path: /mnt/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们分别向 host 的 &lt;code&gt;/mnt&lt;/code&gt; 和容器的 &lt;code&gt;/home&lt;/code&gt; 下挂载目录并查看容器和 host 的情况： 容器中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ kubectl exec -it mount-a sh
$ cd /home
$ ls 
sda1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;host 上：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ cd /mnt
$ ls 
sda1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 host 上创建挂载：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ mkdir /mnt/none
$ sudo mount --bind /var /mnt/none
$ ls none
cache  empty  lib  lock  log  run  spool  tmp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候，我们再看容器中的文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ ls none
# 无输出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这说明 host 上在该卷下的挂载并不会改变容器中的文件。接下来我们可以在容器中按照上面的方案来验证容器中的挂载也不会影响 host 中的目录视图。这里就不展示了。接下来看一下 &lt;code&gt;HostToContainer&lt;/code&gt; 的挂载传播，我们将上面的 Pod 的 &lt;code&gt;mountPropagation&lt;/code&gt; 字段改成 &lt;code&gt;HostToContainer&lt;/code&gt;，然后先取消 host 上的挂载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo umoint /mnt/none
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重新创建 Pod，和上面一样，在 host 上创建挂载，查看容器中的挂载情况：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ ls none
cache  empty  lib  lock  log  run  spool  tmp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;host 上的挂载因为 &lt;code&gt;HostToContainer&lt;/code&gt; 机制传播到了容器中。我们继续看最后一种 &lt;code&gt;Bidirectional&lt;/code&gt; 机制。这次我们要创建两个 Pod: mount-a, mount-b，并把 &lt;code&gt;mountPropagation&lt;/code&gt; 字段改成 &lt;code&gt;Bidirectional&lt;/code&gt;。注意，因为 &lt;code&gt;Bidirectional&lt;/code&gt; 是危险的，所以只有特权容器才可以使用。因此这里还需要把容器改成特权模式，最后在 mount-a 中的容器执行挂载，验证挂载是否传播到 host 和 mount-b 的容器中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: Pod
metadata:
    name: mount-a
    namespace: default
    labels:
      app: mount
spec:
    containers:
    - name: main
      image: nginx:latest
      securityContext:
        privileged: true
      volumeMounts:
      - name: testmount
        mountPath: /home
        mountPropagation: Bidirectional
    volumes:
    - name: testmount
      hostPath:
        path: /mnt/
---
apiVersion: v1
kind: Pod
metadata:
    name: mount-b
    namespace: default
    labels:
      app: mount
spec:
    containers:
    - name: main
      image: nginx:latest
      securityContext:
        privileged: true
      volumeMounts:
      - name: testmount
        mountPath: /home
        mountPropagation: Bidirectional
    volumes:
    - name: testmount
      hostPath:
        path: /mnt/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后进入 mount-a，创建挂载：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ kubectl exec -it mount-a sh
$ su
$ mount --bind /var /home/none
$ ls /home/none
backups  lib    lock  mail  run    tmp
cache    local  log   opt   spool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候查看 host 下的 /mnt/none:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ ls /mnt/none
backups  lib    lock  mail  run    tmp
cache    local  log   opt   spool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现，容器中的挂载传播到了 host 上。这时候再查看 mount-b 中的容器。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ ls /home/none
backups  lib    lock  mail  run    tmp
cache    local  log   opt   spool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;挂载也传播到了 mount-b 的容器中。&lt;/p&gt;
&lt;h2&gt;linux mount 的几种类型&lt;/h2&gt;
&lt;p&gt;上面分析了 kubernetes 的挂载传播机制，在 linux mount 中，也有类似的概念。mount 分为下面几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;shared mount： 相当于上面所说的 &lt;code&gt;Bidirectional&lt;/code&gt; 的挂载传播&lt;/li&gt;
&lt;li&gt;slave mount： 每个 slave mount 都有一个 shared master mount，挂载传播只能从 master -&amp;gt; slave，等同于上面的 &lt;code&gt;HostToContainer&lt;/code&gt;， host 是 master，container 是 slave。&lt;/li&gt;
&lt;li&gt;private mount： 很明显，private 就是相当于 &lt;code&gt;None&lt;/code&gt;，挂载不会向任何一方传播。&lt;/li&gt;
&lt;li&gt;unbindable mount：unbindable mount 其实就是 unbindable private mount，也就是不允许使用 &lt;code&gt;--bind&lt;/code&gt; 的挂载。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;mount namespace 的机制&lt;/h2&gt;
&lt;p&gt;kubernetes 的挂载传播不是其本身实现的，也不是 docker 之类的容器运行时提供的。这是由容器化技术的基础：linux namespace 提供的，linux namespace 当前共有 6 种:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cgroup namespace: 隔离 cgroup 根目录&lt;/li&gt;
&lt;li&gt;pid namespace: 隔离进程 id&lt;/li&gt;
&lt;li&gt;ipc namespace: 隔离 System V IPC, POSIX message queues&lt;/li&gt;
&lt;li&gt;uts namespace: 隔离 Hostname 和 NIS domain name&lt;/li&gt;
&lt;li&gt;user namespace: 隔离用户和用户组 ID&lt;/li&gt;
&lt;li&gt;mount namespace: 隔离挂载点&lt;/li&gt;
&lt;li&gt;network namespace: 隔离网络设备，网络栈，端口等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，mount namespace 是这篇文章的重点。我们可以通过 &lt;code&gt;clone&lt;/code&gt; 调用来看看 mount namespace 的使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#define _GNU_SOURCE
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;sys/wait.h&amp;gt;
#include &amp;lt;sys/mount.h&amp;gt;
#include &amp;lt;sched.h&amp;gt;
#include &amp;lt;signal.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

#define STACK_SIZE (1024*1024)
static char container_stack[STACK_SIZE];

char* const container_args[] = {
    &quot;/bin/bash&quot;,
    NULL
};

int container_main(void* arg)
{
    printf(&quot;Container [%5d] - inside the container!\n&quot;, getpid());
    mount(&quot;none&quot;, &quot;/&quot;, NULL, MS_REC|MS_PRIVATE, NULL);
    execv(container_args[0], container_args);
    printf(&quot;Something&apos;s wrong!\n&quot;);
    return 1;
}
int main()
{
    printf(&quot;Parent [%5d] - start a container!\n&quot;, getpid());
    /* 启用Mount Namespace - 增加CLONE_NEWNS参数 */
    int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD, NULL);
    waitpid(container_pid, NULL, 0);
    printf(&quot;Parent - container stopped!\n&quot;);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译运行:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ gcc main.c -o mount
$ sudo ./mount
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后尝试挂载，来验证挂载 &lt;code&gt;MS_PRIVATE&lt;/code&gt; 的挂载传播问题。&lt;code&gt;MS_PRIVATE&lt;/code&gt; 下 namespace 内和 host 应该是隔离的。&lt;code&gt;MS_PRIVATE&lt;/code&gt; 还可以替换成 &lt;code&gt;MS_UNBINDABLE&lt;/code&gt;， &lt;code&gt;MS_SLAVE&lt;/code&gt;，&lt;code&gt;MS_SHARED&lt;/code&gt;。 关于更多的 namespace 的资料，建议看这两篇文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://coolshell.cn/articles/17010.html&quot; rel=&quot;noopener&quot;&gt;DOCKER基础技术：LINUX NAMESPACE（上）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://coolshell.cn/articles/17029.html&quot; rel=&quot;noopener&quot;&gt;DOCKER基础技术：LINUX NAMESPACE（下）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://man7.org/linux/man-pages/man7/mount_namespaces.7.html&quot; rel=&quot;noopener&quot;&gt;MOUNT_NAMESPACES&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://man7.org/linux/man-pages/man7/namespaces.7.html&quot; rel=&quot;noopener&quot;&gt;NAMESPACES&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt&quot; rel=&quot;noopener&quot;&gt;Shared Subtrees&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://coolshell.cn/articles/17010.html&quot; rel=&quot;noopener&quot;&gt;DOCKER基础技术：LINUX NAMESPACE（上）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://coolshell.cn/articles/17029.html&quot; rel=&quot;noopener&quot;&gt;DOCKER基础技术：LINUX NAMESPACE（下）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://feichashao.com/kernel-namespace-implementation/&quot; rel=&quot;noopener&quot;&gt;Namespace 在 Kernel 里是怎么实现的？以 mount namespace 为例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/storage/volumes/#mount-propagation&quot; rel=&quot;noopener&quot;&gt;Mount propagation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>[Gaia Scheduler] gpu-manager 启动流程分析</title><link>https://www.myway5.com/blog/gpu-manager-startup/</link><guid isPermaLink="true">https://www.myway5.com/blog/gpu-manager-startup/</guid><description>Gaia scheduler 是腾讯开源的在 Kubernetes 集群中做 GPU 虚拟化的方案，实现了为容器分配虚拟化 GPU 资源并加以限制，它的最大的优势就是不需要特殊的硬件支持，并且性能损耗很小。</description><pubDate>Wed, 01 Apr 2020 05:58:26 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;Gaia scheduler 是腾讯开源的在 Kubernetes 集群中做 GPU 虚拟化的方案，实现了为容器分配虚拟化 GPU 资源并加以限制，它的最大的优势就是不需要特殊的硬件支持，并且性能损耗很小。关于它的论文，地址在这里：&lt;a href=&quot;https://ieeexplore.ieee.org/document/8672301&quot; rel=&quot;noopener&quot;&gt;Gaia Scheduler: A Kubernetes-Based Scheduler Framework&lt;/a&gt;。如果想要理解这个项目，强烈建议先读这篇论文。 Gaia Scheduler 可以分为 4 个组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;GPU Manager: 作为 device plugin 向 kubelet 注册。共注册了两个设备，包括 vcore 和 vmemory，支持两种计算资源：&lt;code&gt;tencent.com/vcuda-core&lt;/code&gt; 和 &lt;code&gt;tencent.com/vcuda-memory&lt;/code&gt;，分别用来做 GPU 计算资源和 GPU 内存资源的请求和限制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GPU Scheduler: 这里的 scheduler 并不是 kubernetes 的调度器，是 GPU Manager 在收到 kubelet 的 Allocate 调用后，它需求将设备挂载给容器。为了实现最佳的 GPU 挂载，就有这样一个专门的 Scheduler 来根据节点上当前的 GPU 拓扑和资源占用情况进行调度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;vGPU Manager: vGPU Manager 是具体负责管理容器的组件，包括监控容器状态，传递配置，和容器内的vGPU Library通信，以及在容器死亡后进行回收操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;vGPU Library: vGPU Library 虽然相关的代码量不多，但它是 Gaia Scheduler 最重要的部分。因为它是实现 GPU 虚拟化的核心。通过覆盖容器中的 LD_LIBRARY_PATH 以及自定义了 &lt;code&gt;libcuda-control.so&lt;/code&gt; 实现对 CUDA API 的拦截。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Gaia Scheduler 主要由三个项目组成: &lt;a href=&quot;https://github.com/tkestack/gpu-manager&quot; rel=&quot;noopener&quot;&gt;gpu-manager&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/tkestack/vcuda-controller&quot; rel=&quot;noopener&quot;&gt;vcuda-controller&lt;/a&gt;，&lt;a href=&quot;https://github.com/tkestack/gpu-admission&quot; rel=&quot;noopener&quot;&gt;gpu-admission&lt;/a&gt;。但是这里的 gpu-manager 是 Gaia Scheduler 的主要实现，包含了上述的 4 个组件，vcuda-controller 就是 vGPU Library，已经被打包到了 gpu-manager 这个项目中。gpu-manager 需要配合 gpu-admission 项目来完成 GPU Scheduler 的工作。不要因此产生误解。下文中我们主要就 gpu-manager 这个项目进行分析。&lt;/p&gt;
&lt;h2&gt;启动流程分析&lt;/h2&gt;
&lt;p&gt;gpu-manager 本身主要作为 kubernetes 的 device plugin 来实现的，定义了两种设备: &lt;code&gt;vcuda-core&lt;/code&gt; 和 &lt;code&gt;vcuda-memory&lt;/code&gt;，我们的应用通过 pod 的资源字段进行申请，然后 kube-scheduler 会根据节点上的资源状态进行调度。因此，你最好还需要了解 kubernetes 的 device plugin 的开发知识。关于 device plugin 的开发，可以看之前的一篇文章：&lt;a href=&quot;https://www.myway5.com/index.php/2020/03/24/kubernetes-device-plugin/&quot; rel=&quot;noopener&quot;&gt;Kubernetes开发知识--device-plugin的实现&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;启动参数&lt;/h3&gt;
&lt;p&gt;分析一个项目从启动参数开始，可以帮助我们快速了解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;driver: 这个是 GPU 的驱动，当前的默认值是 nvidia，很显然该项目可以扩展支持其他类型的 GPU。&lt;/li&gt;
&lt;li&gt;extra-config: 额外的配置，这个参数暂时看不出来有什么特别&lt;/li&gt;
&lt;li&gt;volume-config: 这里的 volume 指的是一些动态链接库和可执行文件的位置。也就是 gpu-manager 需要拦截调用的一些库&lt;/li&gt;
&lt;li&gt;docker-endpoint: 用来挂载到容器中和 docker 做通信的，默认位置是 &lt;code&gt;unix:////var/run/docker.sock&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;query-port: 统计信息服务的查询接口&lt;/li&gt;
&lt;li&gt;query-port: 统计信息服务的监听地址&lt;/li&gt;
&lt;li&gt;kubeconfig: 用来授权的配置文件&lt;/li&gt;
&lt;li&gt;standalone: 暂时还不清楚的参数&lt;/li&gt;
&lt;li&gt;sample-period: gpu-manager 会查询 gpu 设备的使用情况，这个参数用来设定采样周期&lt;/li&gt;
&lt;li&gt;node-labels: 给节点自动打标签&lt;/li&gt;
&lt;li&gt;hostname-override: gpu-manager 在运行时，只关注自己节点上的 pod，这主要是通过 hostname 来辨认的&lt;/li&gt;
&lt;li&gt;virtual-manager-path: gpu-manager 会为所有需要虚拟 gpu 资源的 pod 创建唯一的文件夹，文件夹的路径就在这个地址下。&lt;/li&gt;
&lt;li&gt;device-plugin-path: kubernetes 默认的 device plugin 的目录地址&lt;/li&gt;
&lt;li&gt;checkpoint-path: gpu-manager 会产生 checkpoint 来当缓存用&lt;/li&gt;
&lt;li&gt;share-mode: gpu-manager 最大的特点就是将一个物理 gpu 分成多个虚拟 gpu，也就是共享模式&lt;/li&gt;
&lt;li&gt;allocation-check-period: 检查分配了虚拟 gpu 资源的 pod 的状态，及时回收资源&lt;/li&gt;
&lt;li&gt;incluster-mode: 是否在集群内运行&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;服务启动&lt;/h3&gt;
&lt;p&gt;gpu-manager 推荐的部署方案是通过 kubernetes 的 daemonset，然后配置 node selector 调度到指定的节点上。然后 gpu-manager 就开始在指定节点上启动了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;srv := server.NewManager(cfg)
go srv.Run()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里，我们需要看一下这个 srv 的具体实现，首先是它的结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type managerImpl struct {
    config *config.Config

    allocator      allocFactory.GPUTopoService     // gpu 容器调度分配
    displayer      *display.Display                // gpu 使用情况可视化服务
    virtualManager *vitrual_manager.VirtualManager // 负责管理 vgpu

    bundleServer map[string]ResourceServer
    srv          *grpc.Server
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;config 包含了我们上面的所有参数，就不进去细看了。 allocator 负责在容器调度到节点上后，为其分配具体的设备资源。allocator 实现了探测节点上的 gpu 拓扑架构，然后以最佳性能，最少碎片为目的使用最优的方案进行资源分配。 displayer 是将 gpu 的使用情况输出，方便我们查看。 virtualManager 负责 vgpu 分配后的管理工作。 bundleServer 包含 vcore，vmemory，我们上面提到这两种资源以 device plugin 的方式进行注册，因此他们需要启动 grpc server。 srv: 将 gpu display server 注册到这个 grpc server 中。 接下来，我们就可以分析 &lt;code&gt;srv.Run()&lt;/code&gt; 方法具体执行了哪些内容。为了先对整个流程有个大概的印象，我将内容整理成以下条目：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启动 volumeManager，将节点上和 nvidia gpu (包括cuda) 的所有可执行文件和库移动到 /etc/gpu-manager/vdriver 中。并且将关键的库替换成 vcuda-control，实现 cuda 调用的拦截。&lt;/li&gt;
&lt;li&gt;watchdog 创建 pod 缓存并监控 pod，之后所有关于 pod 的操作都来源于这里。&lt;/li&gt;
&lt;li&gt;watchdog 给节点打上标签&lt;/li&gt;
&lt;li&gt;启动 virtualManager&lt;/li&gt;
&lt;li&gt;gpu 拓扑结构感知。&lt;/li&gt;
&lt;li&gt;初始化资源分配器&lt;/li&gt;
&lt;li&gt;设置 vcuda, vmemory, display 的 grpc 服务&lt;/li&gt;
&lt;li&gt;启动 metrics 的 http 服务，主要是提供给 prometheus&lt;/li&gt;
&lt;li&gt;启动 vcuda，vmemory 的 grpc 服务&lt;/li&gt;
&lt;li&gt;启动 display 的 grpc 服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来，我们具体来分析每一步是如何做的。当然，这里只会挑一些重点的部分。&lt;/p&gt;
&lt;h4&gt;volumeManager 的启动&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (vm *VolumeManager) Run() (err error) {
    // ldcache 是动态链接库的缓存信息
    cache, err := ldcache.Open()
    defer func() {
        if e := cache.Close(); err == nil {
            err = e
        }
    }()
    vols := make(VolumeMap)
    for _, cfg := range vm.Config {
        vol := &amp;amp;Volume{
            Path: path.Join(cfg.BasePath, cfg.Name),
        }

        if cfg.Name == &quot;nvidia&quot; {
            // nvidia 库的位置
            types.DriverLibraryPath = filepath.Join(cfg.BasePath, cfg.Name)
        } else {
            // origin 库的位置
            types.DriverOriginLibraryPath = filepath.Join(cfg.BasePath, cfg.Name)
        }

        for t, c := range cfg.Components {
            switch t {
            case &quot;binaries&quot;:
                // 调用 which 来查找可执行文件的位置
                bins, err := which(c...)
                // 将实际位置存起来
                vol.dirs = append(vol.dirs, volumeDir{binDir, bins})
            case &quot;libraries&quot;:
                // 是库的话，就从 ldcache 里面去找
                libs32, libs64 := cache.Lookup(c...)
                // 将 library 位置存起来
                vol.dirs = append(vol.dirs, volumeDir{lib32Dir, libs32}, volumeDir{lib64Dir, libs64})
            }
            vols[cfg.Name] = vol
        }
    }
    // 找到了需要的库位置之后，做 mirror 处理
    if err := vm.mirror(vols); err != nil {
        return err
    }
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码的前半部分都是在查找指定的动态链接库和可执行文件，这些文件是在 volume.conf 这个配置文件中指定的，通过参数传进来。查找动态链接库时，使用的是 ldcache，查找可执行文件时，使用了系统的 &lt;code&gt;which&lt;/code&gt; 指令。找到之后会将其所在位置记录下来。接着就是对找到的库做 &lt;code&gt;mirror&lt;/code&gt; 处理。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (vm *VolumeManager) mirror(vols VolumeMap) error {
    // nvidia 和 origin
    for driver, vol := range vols {
        if exist, _ := vol.exist(); !exist {
            // 这里的path是/etc/gpu-manager/vdriver下面
            if err := os.MkdirAll(vol.Path, 0755); err != nil {
                return err
            }
        }
        for _, d := range vol.dirs {
            vpath := path.Join(vol.Path, d.name)
            // 创建 bin lib lib64
            if err := os.MkdirAll(vpath, 0755); err != nil {
                return err
            }

            // For each file matching the volume components (blacklist excluded), create a hardlink/copy
            // of it inside the volume directory. We also need to create soname symlinks similar to what
            // ldconfig does since our volume will only show up at runtime.
            for _, f := range d.files {
                glog.V(2).Infof(&quot;Mirror %s to %s&quot;, f, vpath)
                if err := vm.mirrorFiles(driver, vpath, f); err != nil {
                    return err
                }

                if strings.HasPrefix(path.Base(f), &quot;libcuda.so&quot;) {
                    driverStr := strings.SplitN(strings.TrimPrefix(path.Base(f), &quot;libcuda.so.&quot;), &quot;.&quot;, 2)
                    types.DriverVersionMajor, _ = strconv.Atoi(driverStr[0]) // 驱动版本号
                    types.DriverVersionMinor, _ = strconv.Atoi(driverStr[1])
                    glog.V(2).Infof(&quot;Driver version: %d.%d&quot;, types.DriverVersionMajor, types.DriverVersionMinor)
                }

                if strings.HasPrefix(path.Base(f), &quot;libcuda-control.so&quot;) {
                    vm.cudaControlFile = f
                }
            }
        }
    }

    vCudaFileFn := func(soFile string) error {
        if err := os.Remove(soFile); err != nil {
            if !os.IsNotExist(err) {
                return err
            }
        }
        if err := clone(vm.cudaControlFile, soFile); err != nil {
            return err
        }

        glog.V(2).Infof(&quot;Vcuda %s to %s&quot;, vm.cudaControlFile, soFile)

        l := strings.TrimRight(soFile, &quot;.0123456789&quot;)
        if err := os.Remove(l); err != nil {
            if !os.IsNotExist(err) {
                return err
            }
        }
        if err := clone(vm.cudaControlFile, l); err != nil {
            return err
        }
        glog.V(2).Infof(&quot;Vcuda %s to %s&quot;, vm.cudaControlFile, l)
        return nil
    }

    if vm.share &amp;amp;&amp;amp; len(vm.cudaControlFile) &amp;gt; 0 {
        if len(vm.cudaSoname) &amp;gt; 0 {
            for _, f := range vm.cudaSoname {
                if err := vCudaFileFn(f); err != nil {
                    return err
                }
            }
        }

        if len(vm.mlSoName) &amp;gt; 0 {
            for _, f := range vm.mlSoName {
                if err := vCudaFileFn(f); err != nil {
                    return err
                }
            }
        }
    }

    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码先会对所有上面查找到的库或可执行文件调用 &lt;code&gt;mirrorFiles&lt;/code&gt;，但是记录下来了 &lt;code&gt;libcuda.so&lt;/code&gt; 的版本号和 &lt;code&gt;libcuda-control.so&lt;/code&gt; 的位置。注意，这个 &lt;code&gt;libcuda-control&lt;/code&gt; 就是 &lt;code&gt;vcuda-control&lt;/code&gt; 项目生成的用来拦截 &lt;code&gt;cuda&lt;/code&gt; 调用的库。 然后将 &lt;code&gt;cudaControlFile&lt;/code&gt; clone到所有 &lt;code&gt;cudaSoname&lt;/code&gt; 和 &lt;code&gt;mlSoName&lt;/code&gt; 中库的位置。这个 clone 方法会先尝试硬链接过去，如果失败就直接复制过去。这里的 &lt;code&gt;cudaControlFile&lt;/code&gt; 就是我们上面所说的 &lt;code&gt;libcuda-control.so&lt;/code&gt; 啦。&lt;code&gt;cudaSoname&lt;/code&gt; 和 &lt;code&gt;mlSoName&lt;/code&gt; 包含了所有需要被拦截调用的库。这样子就实现了拦截所有的 &lt;code&gt;cuda&lt;/code&gt; 调用。下面我们在看一下 &lt;code&gt;mirrorFiles&lt;/code&gt; 这个方法就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// driver 是配置文件中的 &quot;nvidia&quot; 或 &quot;origin&quot;
// vpath 是要 mirror 到的位置，在 /etc/gpu-manager/vdriver 下面
func (vm *VolumeManager) mirrorFiles(driver, vpath string, file string) error {
    // In computing, the Executable and Linkable Format (ELF, formerly named Extensible Linking Format), is a common standard file format for executable files, object code, shared libraries, and core dumps
    obj, err := elf.Open(file)
    defer obj.Close()

    // 黑名单机制，具体用处还不清楚，跟 nvidia 的驱动相关
    ok, err := blacklisted(file, obj)
    if ok {
        return nil
    }
    l := path.Join(vpath, path.Base(file))
    // 不管有没有，先尝试把 gpu-manager 里面的移除
    if err := removeFile(l); err != nil {
        return err
    }
    // clone 优先硬连接，其次是复制文件到指定位置
    if err := clone(file, l); err != nil {
        return err
    }
    // 从 elf 中获取当前库的 soname
    soname, err := obj.DynString(elf.DT_SONAME)
    if len(soname) &amp;gt; 0 {
        // 将获取到 soname 组成路径
        l = path.Join(vpath, soname[0])
        // 如果文件和它的soname不一致（是否可以认为这个文件是软链接过去的）
        if err := linkIfNotSameName(path.Base(file), l); err != nil &amp;amp;&amp;amp; !os.IsExist(err) {
            return err
        }

        // XXX Many applications (wrongly) assume that libcuda.so exists (e.g. with dlopen)
        // Hardcode the libcuda symlink for the time being.
        if strings.Contains(driver, &quot;nvidia&quot;) {
            // 这里为什么要移除 libcuda.so 和 libnvidia-ml.so 的软链接
            // 因为gpu调用会涉及到这两个库，这两个库会软链接到真实的库上。移除后替换成拦截的库
            // Remove libcuda symbol link
            if vm.share &amp;amp;&amp;amp; driver == &quot;nvidia&quot; &amp;amp;&amp;amp; strings.HasPrefix(soname[0], &quot;libcuda.so&quot;) {
                os.Remove(l)
                vm.cudaSoname[l] = l
            }

            // Remove libnvidia-ml symbol link
            if vm.share &amp;amp;&amp;amp; driver == &quot;nvidia&quot; &amp;amp;&amp;amp; strings.HasPrefix(soname[0], &quot;libnvidia-ml.so&quot;) {
                os.Remove(l)
                vm.mlSoName[l] = l
            }

            // XXX GLVND requires this symlink for indirect GLX support
            // It won&apos;t be needed once we have an indirect GLX vendor neutral library.
            if strings.HasPrefix(soname[0], &quot;libGLX_nvidia&quot;) {
                l = strings.Replace(l, &quot;GLX_nvidia&quot;, &quot;GLX_indirect&quot;, 1)
                if err := linkIfNotSameName(path.Base(file), l); err != nil &amp;amp;&amp;amp; !os.IsExist(err) {
                    return err
                }
            }
        }
    }

    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码中，先使用 &lt;code&gt;blacklisted&lt;/code&gt; 排除一些不需要处理的库，然后尝试将库或可执行文件 clone 到我们的 &lt;code&gt;/etc/gpu-manager/vdriver&lt;/code&gt; 下面。&lt;code&gt;/etc/gpu-manager/vdriver&lt;/code&gt; 下面有两个文件夹，一个是 &lt;code&gt;nvidia&lt;/code&gt;，保存了已经被我们拦截的库，一个是 &lt;code&gt;origin&lt;/code&gt;，这里面是原始的未处理的库。同时，还将 libcuda.so 和 libnvidia-ml.so 移除了，这样就调用不到真实的库了，转而在之后用我们拦截的库来替换这几个文件。 至此，volumeManager 分析结束。&lt;/p&gt;
&lt;h4&gt;gpu 拓扑结构感知&lt;/h4&gt;
&lt;p&gt;关于 gpu 拓扑结构这一块，主要是为了在之后做资源分配时选择最优方案用的。腾讯也有分享过这一块的资料(&lt;a href=&quot;http://dl.zhangluya.com/Qcon/qconbj2019/%E8%85%BE%E8%AE%AF%E5%9F%BA%E4%BA%8E%20Kubernetes%20%E7%9A%84%E4%BC%81%E4%B8%9A%E7%BA%A7%E5%AE%B9%E5%99%A8%E4%BA%91%E5%AE%9E%E8%B7%B5-%E7%BD%97%E9%9F%A9%E6%A2%85.pdf&quot; rel=&quot;noopener&quot;&gt;腾讯基于 Kubernetes 的企业级容器云实践&lt;/a&gt;): &lt;img src=&quot;/uploads/wp/2020/04/Screenshot-from-2020-04-01-13-18-35.png&quot; alt=&quot;gpu 拓扑结构&quot; /&gt; 这里不影响我们理解整个工作机制，所以先不分析。&lt;/p&gt;
&lt;h4&gt;初始化资源分配器&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 分配器，根据driver调用相应的分配器
initAllocator := allocFactory.NewFuncForName(m.config.Driver)
if initAllocator == nil {
    return fmt.Errorf(&quot;can not find allocator for %s&quot;, m.config.Driver)
}

m.allocator = initAllocator(m.config, tree, client)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 initAllocator 对应的方法是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;//NewNvidiaTopoAllocator returns a new NvidiaTopoAllocator
func NewNvidiaTopoAllocator(config *config.Config, tree device.GPUTree, k8sClient kubernetes.Interface) allocator.GPUTopoService {
    runtimeRequestTimeout := metav1.Duration{Duration: 2 * time.Minute}
    imagePullProgressDeadline := metav1.Duration{Duration: 1 * time.Minute}
    dockerClientConfig := &amp;amp;dockershim.ClientConfig{
        DockerEndpoint:            config.DockerEndpoint,
        RuntimeRequestTimeout:     runtimeRequestTimeout.Duration,
        ImagePullProgressDeadline: imagePullProgressDeadline.Duration,
    }

    _tree, _ := tree.(*nvtree.NvidiaTree)
    cm, err := checkpoint.NewManager(config.CheckpointPath, checkpointFileName)
    if err != nil {
        glog.Fatalf(&quot;Failed to create checkpoint manager due to %s&quot;, err.Error())
    }
    alloc := &amp;amp;NvidiaTopoAllocator{
        tree:              _tree,
        config:            config,
        evaluators:        make(map[string]Evaluator),
        dockerClient:      dockershim.NewDockerClientFromConfig(dockerClientConfig),
        allocatedPod:      cache.NewAllocateCache(),
        k8sClient:         k8sClient,
        queue:             workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
        stopChan:          make(chan struct{}),
        checkpointManager: cm,
    }

    // Load kernel module if it&apos;s not loaded
    alloc.loadModule()

    // Initialize evaluator
    alloc.initEvaluator(_tree)

    // Read extra config if it&apos;s given
    alloc.loadExtraConfig(config.ExtraConfigPath)

    // Process allocation results in another goroutine
    go wait.Until(alloc.runProcessResult, time.Second, alloc.stopChan)

    // Recover
    alloc.recoverInUsed()

    // Check allocation in another goroutine periodically
    go alloc.checkAllocationPeriodically(alloc.stopChan)

    return alloc
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;allocator 调用 &lt;code&gt;loadModule()&lt;/code&gt; 来启用 nvidia 的内核模块。 调用 &lt;code&gt;initEvaluator(_tree)&lt;/code&gt; 来初始化评估器，这里的 &lt;code&gt;_tree&lt;/code&gt; 就是感知到的 gpu 拓扑结构。 调用 &lt;code&gt;loadExtraConfig(config.ExtraConfigPath)&lt;/code&gt; 来加载启动时传入的额外参数配置文件。 &lt;code&gt;go wait.Until(alloc.runProcessResult, time.Second, alloc.stopChan)&lt;/code&gt; 创建了新的协程来处理分配结果。 &lt;code&gt;recoverInUsed()&lt;/code&gt; 是恢复 gpu 分配结果。比如在 gpu-manager 重启之后，之前的 gpu 分配结果都丢失了，但是节点上还有大量的容器正在占用 gpu，这个方法会通过查找节点上存活的容器，通过 docker endpoint， 调用 &lt;code&gt;InspectContainer&lt;/code&gt; 获取容器中占用的 device id，然后标记该设备和容器之间的占用关系。 &lt;code&gt;go alloc.checkAllocationPeriodically(alloc.stopChan)&lt;/code&gt; 创建新的协程来周期性的检查资源分配情况。如果是 Failed 和 Pending 状态的容器，就根据错误信息检查是否应该删除它们，然后如果这些 pod 的控制器是 deployment 类似的，就尝试删除它们，这样控制器会重新创建这些 pod 进行调度，让这些 pod 恢复到正常运行状态。&lt;/p&gt;
&lt;h4&gt;启动各种服务&lt;/h4&gt;
&lt;p&gt;vcuda，vmemory 的 grpc 服务是 device plugin 的机制。metrics service 是提供给 prometheus 调用的，以监控该节点的相关信息。display 服务会打印 gpu 拓扑结构的相关信息。&lt;/p&gt;
&lt;h3&gt;Device plugin 的注册&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2020/03/device-plugins.svg&quot; alt=&quot;Device plugin&quot; /&gt; 这张图是 device plugin 注册的时序图。gpu-manager 的注册方法是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (m *managerImpl) RegisterToKubelet() error {
    socketFile := filepath.Join(m.config.DevicePluginPath, types.KubeletSocket)
    dialOptions := []grpc.DialOption{grpc.WithInsecure(), grpc.WithDialer(utils.UnixDial), grpc.WithBlock(), grpc.WithTimeout(time.Second * 5)}

    conn, err := grpc.Dial(socketFile, dialOptions...)
    if err != nil {
        return err
    }
    defer conn.Close()

    client := pluginapi.NewRegistrationClient(conn)

    for _, srv := range m.bundleServer {
        req := &amp;amp;pluginapi.RegisterRequest{
            Version:      pluginapi.Version,
            Endpoint:     path.Base(srv.SocketName()),
            ResourceName: srv.ResourceName(),
            Options:      &amp;amp;pluginapi.DevicePluginOptions{PreStartRequired: true},
        }

        glog.V(2).Infof(&quot;Register to kubelet with endpoint %s&quot;, req.Endpoint)
        _, err = client.Register(context.Background(), req)
        if err != nil {
            return err
        }
    }

    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里分别注册了 vcuda 和 vmemory。vcuda 和 vmemory 的 Allocate 方法都指向了同一个方法，写在了 &lt;code&gt;service/allocator/nvidia/allocator.go&lt;/code&gt; 中。 至此，gpu-manager 的启动流程结束。接下来的 gpu-manager 的职责就是等待 kubelet 通过 grpc 的调用，在容器调度到节点的时候进行资源设备的分配，必要目录的挂载等工作了。具体的可以见下一篇文章 最后，提供一个简单的脑图帮助理解： &lt;img src=&quot;/uploads/wp/2020/04/Screenshot-from-2020-04-01-13-57-48.png&quot; alt=&quot;gpu-manager-arch&quot; /&gt;&lt;/p&gt;
</content:encoded><category>k8s</category><category>k8s</category><category>gpu-manager</category><category>gaia scheduler</category><category>gpu 虚拟化</category><author>joyme123</author></item><item><title>MySQL 事务隔离性探究</title><link>https://www.myway5.com/blog/mysql-transaction-isolation/</link><guid isPermaLink="true">https://www.myway5.com/blog/mysql-transaction-isolation/</guid><description>MySQL 的事务提供了 ACID 四个特性，其中隔离性是较复杂的一个特性。SQL 标准定义了四种隔离级别，每一种隔离级别都规定了事务中修改对于其他事务的可见性。一般来说，较低的隔离通常可以带来更高的并发。</description><pubDate>Mon, 30 Mar 2020 16:41:30 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;MySQL 的事务提供了 ACID 四个特性，其中隔离性是较复杂的一个特性。SQL 标准定义了四种隔离级别，每一种隔离级别都规定了事务中修改对于其他事务的可见性。一般来说，较低的隔离通常可以带来更高的并发。四种隔离级别分别是： 未提交读（READ UNCOMMITTED)，已提交读（READ COMMITTED)/不可重复读(NONREPEATABLE READ)，可重复读(REPEATABLE READ)，可串行化(SERIALIZABLE)。下面的说明仅对 InnoDB 引擎保证准确。&lt;/p&gt;
&lt;h2&gt;四种隔离级别&lt;/h2&gt;
&lt;p&gt;关于四种隔离级别，在《高性能 MySQL&amp;gt; 中已经有了很好的阐述，这里简单地陈述出来。&lt;/p&gt;
&lt;h3&gt;未提交读&lt;/h3&gt;
&lt;p&gt;在未提交读级别，事务中的修改，即使没有提交，对于其他事务也都是可见的。事务可以读取未提交的数据，这也称为脏读（Dirty Read）。很明显，未提交读等同于未做任何的事务隔离，因此是最低的隔离级别。一般情况下，也没有什么可用的场景。&lt;/p&gt;
&lt;h3&gt;已提交读&lt;/h3&gt;
&lt;p&gt;已提交读是相对于未提交读来说的，主要是实现了在事务中未提交的修改，对于其他事务是不可见的。但是这种隔离级别会造成一个问题，在一次事务中多次读取会出现不一样的结果，所以也称为不可重复读。想要更轻松的理解不可重复读，可以看下面的例子： &lt;img src=&quot;/uploads/wp/2020/03/Screenshot-from-2020-03-31-00-44-42.png&quot; alt=&quot;iso1&quot; /&gt; 事务A期间，事务B提交了一次更新，这会导致事务A中两次查询 id 为 1 的数据，第一次 score 是 89，第二次 score 是 29。我们也可以用 sql 语句来验证一下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mysql&amp;gt; show create table score;

+-------+--------------------------------------------------------------------+
| Table | Create Table                                                       |
+-------+--------------------------------------------------------------------+
| score | CREATE TABLE `score` (                                             |
|       |   `id` int(11) NOT NULL AUTO_INCREMENT,                            |
|       |   `name` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,          |
|       |   `score` int(11) NOT NULL,                                        |
|       |   PRIMARY KEY (`id`)                                               |
|       | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci |
+-------+--------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;终端 A :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mysql&amp;gt; set autocommit=0;
mysql&amp;gt; set session transaction isolation level read committed;
mysql&amp;gt; begin;
mysql&amp;gt; select * from score where id = 1;

+----+------+-------+
| id | name | score |
+----+------+-------+
| 1  | Bob  | 89    |
+----+------+-------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;终端 B :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mysql&amp;gt; set autocommit=0;
mysql&amp;gt; set session transaction isolation level read committed;
mysql&amp;gt; begin;
mysql&amp;gt; update score set score=29 where id = 1;
mysql&amp;gt; commit;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;终端 A :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mysql&amp;gt; select * from score where id = 1;

+----+------+-------+
| id | name | score |
+----+------+-------+
| 1  | Bob  | 29    |
+----+------+-------+

mysql&amp;gt; commit;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;可重复读&lt;/h3&gt;
&lt;p&gt;可重复读是 MySQL 的默认隔离界别，保证了在同一个事务中多次读取同样的记录结果是一致的。但是在理论上，可重复读隔离级别还是无法解决另外一个幻读（Phantom Read）的问题。所谓幻读，指的是当某个事务在读取某个范围内的记录时，会产生换行（Phantom Row）。InnoDB 通过多版本控制（MVCC，Multiversion Concurreny Control）解决了幻读的问题。但是对于可重复读，有一个让我比较不确定的场景: &lt;img src=&quot;/uploads/wp/2020/03/Screenshot-from-2020-03-31-00-46-17.png&quot; alt=&quot;iso2&quot; /&gt; 按道理说，在事务 A 内 score 应该是一致的，事务 B 提交的 score 是不可见的，也就是 89，所以 id 为 1 的数据应该是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;+----+------+-------+
| id | name | score |
+----+------+-------+
| 1  | Alice| 29    |
+----+------+-------+

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事实真的如此吗？我们用 MySQL 验证一下： 终端 A:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mysql&amp;gt; set session transaction isolation level repeatable read;
mysql&amp;gt; begin;
mysql&amp;gt; select * from score where id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;终端 B:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mysql&amp;gt; set session transaction isolation level repeatable read;
mysql&amp;gt; begin;
mysql&amp;gt; update score set score=29 where id = 1;
mysql&amp;gt; commit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;终端 A:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mysql&amp;gt; update score set name=&apos;Alice&apos; where score=89 and id = 1;
mysql&amp;gt; commit;
mysql&amp;gt; select * from score where id = 1; 

+----+------+-------+
| id | name | score |
+----+------+-------+
| 1  | Bob  | 29    |
+----+------+-------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现我的想法是错的。所以如何正确理解这里所说的可重复读呢？应该是仅指在 SELECT 的时候，因为 MySQL 事务的可重复读隔离级别下，会使用 MVCC，此时 SELECT 只查找版本早于当前事务版本的数据行，所以保证了一次事务内的可重复读。而 INSERT、DELETE、UPDATE 均会使用当前系统版本号的最新数据。&lt;/p&gt;
&lt;h3&gt;可串行化&lt;/h3&gt;
&lt;p&gt;可串行化是最高的隔离级别。它通过强制事务串行执行，避免了前面说的幻读问题。简单来说，SERIALIZABLE 会在读取的每一行数据都加锁，所以可能导致大量的超时和锁争用问题。实际应用中也很少用到这个隔离级别。&lt;/p&gt;
</content:encoded><category>数据库</category><category>mysql</category><category>acid</category><category>isolation</category><author>joyme123</author></item><item><title>Kubernetes开发知识--device-plugin的实现</title><link>https://www.myway5.com/blog/kubernetes-device-plugin/</link><guid isPermaLink="true">https://www.myway5.com/blog/kubernetes-device-plugin/</guid><description>什么是 device plugin</description><pubDate>Tue, 24 Mar 2020 09:21:15 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 device plugin&lt;/h2&gt;
&lt;p&gt;Kubernetes 作为一个自动化容器编排系统，在调度 pod 的时候会根据容器需要的资源进行节点的选择，节点的选择会分为预选和优选阶段。预选阶段会根据所有节点上剩余的资源量与 pod 需要的资源量进行对比，选出能够满足需求的节点。通常情况下，这里的资源都会包括 CPU 和 Memory，就像下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;resources:
  requests:
    memory: &quot;64Mi&quot;
    cpu: &quot;250m&quot;
  limits:
    memory: &quot;128Mi&quot;
    cpu: &quot;500m&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是现在我们有了新的需求，我们有一个 tensorflow 的模型需要借助 tensorflow serving 来部署，同时我们希望采用 GPU 部署的方案以加快模型的在线推算速度。这样我们就需要一个 GPU 的资源并借助 Kubernetes 的调度器将容器调度到有空余 GPU 资源的方案。 在 1.11 版本之前的 Kubernetes 中，提供了 &lt;code&gt;alpha.kubernetes.io/nvidia-gpu&lt;/code&gt; 的资源名称来帮助我们根据 GPU 资源调度。但是这也带来了一些问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kubernetes 需要维护 NVIDIA GPU 相关的代码，增加了维护成本。&lt;/li&gt;
&lt;li&gt;NVIDIA GPU 方面的专家不一定熟悉 Kubernetes，这不符合让最擅长的人做最擅长的事的原则。&lt;/li&gt;
&lt;li&gt;除了 NVIDIA GPU，还会有其他的计算资源需要支持。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，Kubernetes 在 1.8 版本引入了 device plugin 机制，将第三方的计算资源通过插件的方式引入 Kubernetes，并且由第三方厂商自行维护。Kubernetes 社区的活瞬间就轻松了，第三方厂商也开心了。 通过以上的说明，可以总结出 device plugin 主要用来解耦第三方计算资源和 kubernetes 系统，将第三方的计算资源通过插件的方式引入 Kubernetes。当然 cpu 和 memory 除外，毕竟谁还能少了 CPU 和 Memory 呢。&lt;/p&gt;
&lt;h2&gt;device plugin 能做什么&lt;/h2&gt;
&lt;p&gt;目前一些常用的 device plugin 有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nvidia 提供的 GPU 插件：&lt;a href=&quot;https://github.com/NVIDIA/k8s-device-plugin&quot; rel=&quot;noopener&quot;&gt;NVIDIA device plugin for Kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AMD 提供的 GPU 插件：&lt;a href=&quot;https://github.com/RadeonOpenCompute/k8s-device-plugin&quot; rel=&quot;noopener&quot;&gt;RadeonOpenCompute/k8s-device-plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;高性能低延迟 RDMA 卡插件：&lt;a href=&quot;https://github.com/hustcat/k8s-rdma-device-plugin.git&quot; rel=&quot;noopener&quot;&gt;RDMA device plugin for Kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;低延迟 Solarflare 万兆网卡驱动：&lt;a href=&quot;https://github.com/vikaschoudhary16/sfc-device-plugin&quot; rel=&quot;noopener&quot;&gt;Solarflare Device Plugin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我了解的还有腾讯的 Gaia Scheduler，通过 device plugin 实现的 GPU 虚拟化方案。如果 NVIDIA 的 GPU 方案还不够适合你，可以看看腾讯的这个方案：&lt;a href=&quot;https://github.com/tkestack/gpu-manager&quot; rel=&quot;noopener&quot;&gt;tkestack/gpu-manager&lt;/a&gt; 我觉得腾讯的 GPU 虚拟化方案是最能说明 device plugin 使用场景的例子，通过定义 &lt;code&gt;tencent.com/vcuda-core&lt;/code&gt; 和 &lt;code&gt;tencent.com/vcuda-memory&lt;/code&gt; 这两个计算资源，来将一个物理 GPU 划分成多个虚拟 GPU 进行调度，这样可以实现一个 GPU 上部署多个 tensorflow serving。你不需要购买特殊的硬件或者修改任何 Kubernetes 的代码，就有了 GPU 虚拟化的能力。 下面我们开个脑洞，现在我们有了一种叫做 &lt;code&gt;cola&lt;/code&gt; 的计算资源，可以提高程序的 IO 能力。但是 cola 的资源有限，只分配给特定的容器使用。这时候我们就可以通过实现自己的 device plugin 来满足这个需求。我们的计算资源名就叫做： &lt;code&gt;myway5.com/cola&lt;/code&gt;，在下面一小节做具体的实现。&lt;/p&gt;
&lt;h2&gt;device plugin 的实现方案&lt;/h2&gt;
&lt;p&gt;device plugin 的工作原理其实不复杂。主要有以下步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先 device plugin 可以通过手动或 daemonset 部署到需要的节点上。&lt;/li&gt;
&lt;li&gt;为了让 Kubernetes 发现 device plugin，需要向 kubelet 的 unix socket。 进行注册，注册的信息包括 device plugin 的 unix socket，API Version，ResourceName。&lt;/li&gt;
&lt;li&gt;kubelet 通过 grpc 向 device plugin 调用 ListAndWatch， 获取当前节点上的资源。&lt;/li&gt;
&lt;li&gt;kubelet 向 api server 更新节点状态来通知资源变更。&lt;/li&gt;
&lt;li&gt;用户创建 pod，请求资源并调度到节点上后，kubelet 调用 device plugin 的 Allocate 进行资源分配。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;时序图如下: &lt;img src=&quot;/uploads/wp/2020/03/device-plugins.svg&quot; alt=&quot;device plugins&quot; /&gt; 在 device plugin 的实现中，最关键的两个要实现的方法是 &lt;code&gt;ListAndWatch&lt;/code&gt; 和 &lt;code&gt;Allocate&lt;/code&gt;。除此之外，还要注意监控 kubelet 的重启，一般是使用 &lt;code&gt;fsnotify&lt;/code&gt; 类似的库监控 kubelet.sock 的重新创建事件。如果重新创建了，则认为 kubelet 是重启了，我们需要重新向 kubelet 注册 device plugin。&lt;/p&gt;
&lt;h3&gt;ListAndWatch&lt;/h3&gt;
&lt;p&gt;我们上面定义的 &lt;code&gt;myway5.com/cola&lt;/code&gt; 资源用 &lt;code&gt;/etc/colas&lt;/code&gt; 下的文件代表。每一个文件代表一个可用的资源。因此实现 &lt;code&gt;ListAndWatch&lt;/code&gt; 就是查找该文件夹下的文件，然后添加到设备列表发送给 kubelet，之后调用 &lt;code&gt;fsnotify&lt;/code&gt; 去监控文件的 &lt;code&gt;CREATE&lt;/code&gt; 和 &lt;code&gt;REMOVE&lt;/code&gt; 事件。每次设备列表发生变更都重新向 kubelet 发送更新过的设备列表。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// ListAndWatch returns a stream of List of Devices
// Whenever a Device state change or a Device disappears, ListAndWatch
// returns the new list
func (s *ColaServer) ListAndWatch(e *pluginapi.Empty, srv pluginapi.DevicePlugin_ListAndWatchServer) error {
    log.Infoln(&quot;ListAndWatch called&quot;)
    devs := make([]*pluginapi.Device, len(s.devices))

    i := 0
    for _, dev := range s.devices {
        devs[i] = dev
        i++
    }

    err := srv.Send(&amp;amp;pluginapi.ListAndWatchResponse{Devices: devs})
    if err != nil {
        log.Errorf(&quot;ListAndWatch send device error: %v&quot;, err)
        return err
    }

    // 更新 device list
    for {
        log.Infoln(&quot;waiting for device change&quot;)
        select {
        case &amp;lt;-s.notify:
            log.Infoln(&quot;开始更新device list, 设备数:&quot;, len(s.devices))
            devs := make([]*pluginapi.Device, len(s.devices))

            i := 0
            for _, dev := range s.devices {
                devs[i] = dev
                i++
            }

            srv.Send(&amp;amp;pluginapi.ListAndWatchResponse{Devices: devs})
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Allocate&lt;/h3&gt;
&lt;p&gt;在用户创建的 Pod 请求资源时，Kubernetes 的调度器会进行调度，并通过 kubelet 向 device plugin 发出 Allocate 调用，这一步的调用主要是为了让 device plugin 为容器调度资源。 在调度成功后向 kubelet 返回调度结果即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Allocate is called during container creation so that the Device
// Plugin can run device specific operations and instruct Kubelet
// of the steps to make the Device available in the container
func (s *ColaServer) Allocate(ctx context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
    log.Infoln(&quot;Allocate called&quot;)
    resps := &amp;amp;pluginapi.AllocateResponse{}
    for _, req := range reqs.ContainerRequests {
        log.Infof(&quot;received request: %v&quot;, strings.Join(req.DevicesIDs, &quot;,&quot;))
        resp := pluginapi.ContainerAllocateResponse{
            Envs: map[string]string{
                &quot;COLA_DEVICES&quot;: strings.Join(req.DevicesIDs, &quot;,&quot;),
            },
        }
        resps.ContainerResponses = append(resps.ContainerResponses, &amp;amp;resp)
    }
    return resps, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;p&gt;device plugin 可以手动部署到机器上，也可以通过 Daemonset 进行部署。这里当然是 Daemonset 进行部署了。部署的时候有几个注意事项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要挂载 hostPath，其中 &lt;code&gt;/var/lib/kubelet/device-plugins&lt;/code&gt; 是必须的。这个文件夹下有 &lt;code&gt;kubelet.sock&lt;/code&gt;，以及我们也需要将 device plugin 的 unix socket 文件存在这里。使得 kubelet 可以和我们的应用通信。&lt;/li&gt;
&lt;li&gt;为 device plugin 的 Pod 设置调度优先级别，通常设置成 &lt;code&gt;priorityClassName: &quot;system-node-critical&quot;&lt;/code&gt;。这样可以保证不会因为节点利用率过高被逐出。&lt;/li&gt;
&lt;li&gt;如果资源设备不是每台机器都有，建议使用 &lt;code&gt;nodeSelector&lt;/code&gt; 将 device plugin 调度到指定的机器上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;device plugin 的开发源代码可以参考上面的 &lt;code&gt;cola&lt;/code&gt; 例子：&lt;a href=&quot;https://github.com/joyme123/cola-device-plugin&quot; rel=&quot;noopener&quot;&gt;cola device plugin&lt;/a&gt; 部署结束之后，可以查看一下节点的资源情况：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ kubectl describe nodes test
Capacity:
 cpu:                2
 ephemeral-storage:  17784752Ki
 hugepages-2Mi:      0
 memory:             1986740Ki
 myway5.com/cola:    2
 pods:               110
Allocatable:
 cpu:                2
 ephemeral-storage:  17784752Ki
 hugepages-2Mi:      0
 memory:             1986740Ki
 myway5.com/cola:    2
 pods:               110
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个 pod，请求 &lt;code&gt;myway5.com/cola&lt;/code&gt; 资源：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ kubectl apply -f e2e/pod-with-cola.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后查看一下 cola pod 的日志来了解设备发现和调度情况：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ kubectl -n kube-system logs cola-thtm9 
time=&quot;2020-03-24T08:16:53Z&quot; level=info msg=&quot;cola device plugin starting&quot;
time=&quot;2020-03-24T08:16:53Z&quot; level=info msg=&quot;find device &apos;cocacola&apos;&quot;
time=&quot;2020-03-24T08:16:53Z&quot; level=info msg=&quot;find device &apos;peisicola&apos;&quot;
time=&quot;2020-03-24T08:16:53Z&quot; level=info msg=&quot;watching devices&quot;
time=&quot;2020-03-24T08:16:53Z&quot; level=info msg=&quot;start GPPC server for &apos;myway5.com/cola&apos;&quot;
time=&quot;2020-03-24T08:16:53Z&quot; level=info msg=&quot;Register to kubelet with endpoint cola.sock&quot;
time=&quot;2020-03-24T08:16:53Z&quot; level=info msg=&quot;register to kubelet successfully&quot;
time=&quot;2020-03-24T08:16:53Z&quot; level=info msg=&quot;ListAndWatch called&quot;
time=&quot;2020-03-24T08:16:53Z&quot; level=info msg=&quot;waiting for device change&quot;
time=&quot;2020-03-24T08:17:10Z&quot; level=info msg=&quot;Allocate called&quot;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>Kubernetes 开发知识--Kubernetes 准入控制与 admission webhook 的使用</title><link>https://www.myway5.com/blog/kubernetes-admission/</link><guid isPermaLink="true">https://www.myway5.com/blog/kubernetes-admission/</guid><description>我们都知道 Kubernetes 的最核心组件就是它的 API Server，所有资源的创建、更新和删除都是通过 API Server 进行的。我们可以通过 HTTP 请求或者 kubectl 这样的客户端来和 APIServer 通信，在我们操作对象的请求到达 API Ser…</description><pubDate>Thu, 19 Mar 2020 15:43:17 GMT</pubDate><content:encoded>&lt;h2&gt;一、什么是准入控制&lt;/h2&gt;
&lt;p&gt;我们都知道 Kubernetes 的最核心组件就是它的 API Server，所有资源的创建、更新和删除都是通过 API Server 进行的。我们可以通过 HTTP 请求或者 kubectl 这样的客户端来和 APIServer 通信，在我们操作对象的请求到达 API Server 之前，会先到达准入控制器这里。准入控制器可以执行“验证”和“变更”操作。因此我们可以认为有两种准入控制器：&lt;code&gt;变更（mutating）准入控制器&lt;/code&gt;和&lt;code&gt;验证（validating）准入控制器&lt;/code&gt;。 准入控制过程也同样分为两个阶段。第一阶段，运行变更准入控制器，对对象进行修改操作；第二阶段，运行验证准入控制器。如果任何一个阶段的任何控制器拒绝了该请求，则整个请求将立即被拒绝，并向终端用户返回一个错误。 我们可以通过下面的图来直观的感受一下： &lt;img src=&quot;/uploads/wp/2020/03/webhooks.png&quot; alt=&quot;Kubernetes 开发知识--Kubernetes 准入控制与 admission webhook 的使用&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;二、准入控制能做什么&lt;/h2&gt;
&lt;p&gt;准入控制存在的目的就是提高 Kubernetes 架构的灵活性。因此 Kubernetes 提供了一些准入控制器，并且允许你打开或关闭一部分，同时你也可以自定义准入控制器，来完成你自己的一些特殊需求。可以自定义的准入控制器也分为两种：一个叫 &lt;code&gt;MutatingAdmissionWebhook&lt;/code&gt;，一个叫 &lt;code&gt;ValidatingAdmissionWebhook&lt;/code&gt;，其实也是我们上面说的&lt;code&gt;变更准入控制器&lt;/code&gt;和&lt;code&gt;验证准入控制器&lt;/code&gt;。 Kubernetes 提供了一些常用的准入控制器，下面举几个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AlwaysPullImages： 该准入控制器会修改每一个新创建的 Pod 的镜像拉取策略为 Always。这样在多租户集群里，用户就能保证自己的私有镜像只会在有凭证的情况下使用，而不会出现因为机器上缓存了镜像而被其他用户直接使用的情况。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NamespaceLifecycle：该准入控制器禁止在一个正在被终止的 &lt;code&gt;Namespace&lt;/code&gt; 中创建新对象，并且确保使用不存在的 &lt;code&gt;Namespace&lt;/code&gt; 的请求被拒绝。该准入控制器还会禁止删除三个系统保留的命名空间，即 &lt;code&gt;default&lt;/code&gt;、&lt;code&gt;kube-system&lt;/code&gt; 和 &lt;code&gt;kube-public&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了 Kubernetes 提供的这些准入控制器，我们可以编写 &lt;code&gt;MutatingAdmissionWebhook&lt;/code&gt; 和 &lt;code&gt;ValidatingAdmissionWebhook&lt;/code&gt;。比如我之前有写一个叫做 &lt;code&gt;[lazykube](https://github.com/joyme123/lazykube)&lt;/code&gt; 的 &lt;code&gt;MutatingAdmissionWebhook&lt;/code&gt;，它的作用是对每一个创建的 Pod 的镜像源都进行校验，如果是 &lt;code&gt;docker hub&lt;/code&gt;, &lt;code&gt;gcr.io&lt;/code&gt;, &lt;code&gt;quay.io&lt;/code&gt; 等地址的镜像，就修改成国内的代理源，这样就可以自动解决镜像需要翻墙下载的问题了。&lt;/p&gt;
&lt;h2&gt;三、如何使用 admission webhook 编写一个准入控制器&lt;/h2&gt;
&lt;p&gt;使用 admission webhook 编写一个准入控制器其实很简单，它就和你日常写 web server 一样:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Start 启动服务
func (whsrv *WebhookServer) Start() error {
    mux := http.NewServeMux()
    mux.HandleFunc(&quot;/mutate&quot;, whsrv.serve)
    whsrv.server.Handler = mux

    if err := whsrv.server.ListenAndServeTLS(&quot;&quot;, &quot;&quot;); err != nil {
        return fmt.Errorf(&quot;Failed to listen and serve webhook server: %v&quot;, err)
    }

    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在收到一个 &lt;code&gt;AdmissionReview&lt;/code&gt; 请求的时候，将其中的 &lt;code&gt;AdmissionRequest&lt;/code&gt; 对象携带的资源对象取出来进行校验或变更：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;var body []byte
if r.Body != nil {
    if data, err := ioutil.ReadAll(r.Body); err == nil {
        body = data
    }
}

if len(body) == 0 {
    log.Error(&quot;empty body&quot;)
    http.Error(w, &quot;empty body&quot;, http.StatusBadRequest)
    return
}

// verify the content type is accurate
contentType := r.Header.Get(&quot;Content-Type&quot;)
if contentType != &quot;application/json&quot; {
    log.Errorf(&quot;Content-Type=%s, expect application/json&quot;, contentType)
    http.Error(w, &quot;invalid Content-Type, expect `application/json`&quot;, http.StatusUnsupportedMediaType)
    return
}

var admissionResponse *v1beta1.AdmissionResponse
ar := v1beta1.AdmissionReview{}
if _, _, err := deserializer.Decode(body, nil, &amp;amp;ar); err != nil {
    log.Errorf(&quot;Can&apos;t decode body: %v&quot;, err)
    admissionResponse = &amp;amp;v1beta1.AdmissionResponse{
        Result: &amp;amp;metav1.Status{
            Message: err.Error(),
        },
    }
} else {
    admissionResponse = whsrv.mutate(&amp;amp;ar)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后生成 &lt;code&gt;AdmissionResponse&lt;/code&gt;, 通过 &lt;code&gt;AdmissionReview&lt;/code&gt; 进行响应即可：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;admissionReview := v1beta1.AdmissionReview{}
if admissionResponse != nil {
    admissionReview.Response = admissionResponse
    if ar.Request != nil {
        admissionReview.Response.UID = ar.Request.UID
    }
}

resp, err := json.Marshal(admissionReview)
if err != nil {
    log.Errorf(&quot;Can&apos;t encode response: %v&quot;, err)
    http.Error(w, fmt.Sprintf(&quot;could not encode response: %v&quot;, err), http.StatusInternalServerError)
}
log.Infoln(&quot;Ready to write response ...&quot;)
if _, err := w.Write(resp); err != nil {
    log.Errorf(&quot;Can&apos;t write response: %v&quot;, err)
    http.Error(w, fmt.Sprintf(&quot;could not write response: %v&quot;, err), http.StatusInternalServerError)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的代码可以参考上面的 lazykube 项目。 这样我们写好之后就可以开始部署了。我们需要告诉 Kubernetes 我们的 admission webhook 的地址，以及我们要操作的资源对象，因此我们创建一个 &lt;code&gt;MutatingWebhookConfiguration&lt;/code&gt; 或者 &lt;code&gt;ValidatingWebhookConfiguration&lt;/code&gt; 对象，比如 lazykube 就是对创建 Pod 进行变更操作：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  name: lazykube-webhook-cfg
  namespace: kube-system
  labels:
    app: lazykube
webhooks:
  - name: lazykube.myway5.com
    clientConfig:
      service:
        name: lazykube-webhook-svc
        namespace: kube-system
        path: &quot;/mutate&quot;
      caBundle: ××××××××
    rules:
      - operations: [ &quot;CREATE&quot; ]
        apiGroups: [&quot;&quot;]
        apiVersions: [&quot;v1&quot;]
        resources: [&quot;pods&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后为了让 Kubernetes 通过 HTTPS 进行通信，并信任我们的证书，我们需要让 Kubernetes 集群为我们的签发证书。具体的方法可以参考这里：&lt;a href=&quot;https://www.myway5.com/index.php/2020/03/18/kubernetes-%e9%9b%86%e7%be%a4%e4%b8%ad%e7%9a%84%e8%af%81%e4%b9%a6%e7%ad%be%e5%8f%91/&quot; rel=&quot;noopener&quot;&gt;kubernetes 集群中的证书签发&lt;/a&gt;。最后使用 Deployment 部署我们的服务即可。&lt;/p&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>kubernetes 集群中的证书签发</title><link>https://www.myway5.com/blog/kubernetes-certification/</link><guid isPermaLink="true">https://www.myway5.com/blog/kubernetes-certification/</guid><description>在我们使用 kubernetes 的 admission webhook 机制实现一些集群资源认证、修改的方案时，会涉及到集群内部的 https 通信。这就涉及到我们的服务需要配置证书，并且要让 kubernetes 的组件信任该证书。</description><pubDate>Wed, 18 Mar 2020 03:10:01 GMT</pubDate><content:encoded>&lt;h2&gt;场景&lt;/h2&gt;
&lt;p&gt;在我们使用 kubernetes 的 &lt;code&gt;admission webhook&lt;/code&gt; 机制实现一些集群资源认证、修改的方案时，会涉及到集群内部的 https 通信。这就涉及到我们的服务需要配置证书，并且要让 kubernetes 的组件信任该证书。我们都知道 kubernetes 集群中所有的证书都是由一个自定义的 CA 签发的，并且 kubernetes 集群都信任该 CA，因此基于该原理，使用 kubernetes 提供的 &lt;code&gt;CertificateSigningRequest&lt;/code&gt; 来为我们的证书签名即可。&lt;/p&gt;
&lt;h2&gt;具体流程&lt;/h2&gt;
&lt;p&gt;kubernetes 官方的文档上提供了比较详细的说明，文档地址在这里：&lt;a href=&quot;https://kubernetes.io/zh/docs/tasks/tls/managing-tls-in-a-cluster/&quot; rel=&quot;noopener&quot;&gt;管理集群中的 TLS 认证&lt;/a&gt; 大致流程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 cfssl 为我们的 service 地址创建证书&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;CertificateSigningRequest&lt;/code&gt; 请求 kubernetes 的 CA 来为该证书签名&lt;/li&gt;
&lt;li&gt;管理员通过 &lt;code&gt;kubectl certificate approve&lt;/code&gt; 来批准请求&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;kubectl get csr&lt;/code&gt; 获取签名后的证书，并使用到我们的服务中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然这个过程还不够自动化，我们可以使用一些很好的脚本来帮助我们完成这个工作:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joyme123/lazykube/blob/master/deployment/webhook-create-signed-cert.sh&quot; rel=&quot;noopener&quot;&gt;webhook-create-signed-cert.sh&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个脚本使用 openssl 来创建公私钥，创建 &lt;code&gt;CertificateSigningRequest&lt;/code&gt; 请求对公钥签名，然后自动批准并将私钥和签名后的公钥存到 secret 中。使用方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;./webhook-create-signed-cert.sh \
    --service lazykube-webhook-svc \
    --secret lazykube-webhook-certs \
    --namespace kube-system
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joyme123/lazykube/blob/master/deployment/webhook-patch-ca-bundle.sh&quot; rel=&quot;noopener&quot;&gt;webhook-patch-ca-bundle.sh&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/joyme123/lazykube/blob/master/deployment/mutatingwebhook.yaml&quot; rel=&quot;noopener&quot;&gt;mutatingwebhook.yaml&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个脚本其实跟证书签发没有太大关系，但是如果你使用 &lt;code&gt;mutatingwebhook&lt;/code&gt; 的话正好可以使用它，同理 &lt;code&gt;validatingwebhook&lt;/code&gt; 也是如此&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;cat mutatingwebhook.yaml | \
    ./webhook-patch-ca-bundle.sh &amp;gt; \
    mutatingwebhook-ca-bundle.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;一些问题&lt;/h2&gt;
&lt;p&gt;我在使用上述脚本的时候，发现使用 rancher 创建的 kubernetes 集群有问题。这是因为 rancher 创建的 kubernetes 集群没有默认开启 &lt;code&gt;kubernetes controller manager&lt;/code&gt; 的签名选项。具体的选项可以参考这里：&lt;a href=&quot;https://kubernetes.io/zh/docs/tasks/tls/managing-tls-in-a-cluster/#%E7%BB%99%E9%9B%86%E7%BE%A4%E7%AE%A1%E7%90%86%E5%91%98%E7%9A%84%E4%B8%80%E4%B8%AA%E5%BB%BA%E8%AE%AE&quot; rel=&quot;noopener&quot;&gt;给集群管理员的一个建议&lt;/a&gt; 修改的方案就是打开该选项，rancher 中可以在界面上编辑集群的 yaml 文件，加上以下参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  kube-controller: 
    extra_args: 
      cluster-signing-cert-file: &quot;/etc/kubernetes/ssl/kube-ca.pem&quot;
      cluster-signing-key-file: &quot;/etc/kubernetes/ssl/kube-ca-key.pem&quot;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>Golang 中的错误处理建议</title><link>https://www.myway5.com/blog/golang-error/</link><guid isPermaLink="true">https://www.myway5.com/blog/golang-error/</guid><description>Golang 的错误处理一直是一个比较讨论比较多的话。我刚接触 Golang 的时候也看过关于错误处理的一些文档，但是并没有放在心上。在我使用 Golang 一段时间之后，我觉得我可能无法忽略这个问题。因此，这篇文章主要是为了整理一些在 Golang 中常用的错误处理技巧和原则。</description><pubDate>Tue, 03 Mar 2020 09:15:00 GMT</pubDate><content:encoded>&lt;h2&gt;一、概述&lt;/h2&gt;
&lt;p&gt;Golang 的错误处理一直是一个比较讨论比较多的话。我刚接触 Golang 的时候也看过关于错误处理的一些文档，但是并没有放在心上。在我使用 Golang 一段时间之后，我觉得我可能无法忽略这个问题。因此，这篇文章主要是为了整理一些在 Golang 中常用的错误处理技巧和原则。&lt;/p&gt;
&lt;h2&gt;二、错误处理的技巧和原则&lt;/h2&gt;
&lt;h3&gt;2.1 使用封装来避免重复的错误判断&lt;/h3&gt;
&lt;p&gt;在 Golang 的项目中，最多的一句代码肯定是 &lt;code&gt;if err != nil&lt;/code&gt;。Golang 将错误作为返回值，因此你不得不处理这些错误。但是有的时候，错误处理的判断可能会占据你的代码的一半篇幅，这使得代码看起来乱糟糟的。在官方的博客中有一个这样的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
// and so on
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是的，你没有看错，这里其实就是调用了 3 行 &lt;code&gt;fd.Write&lt;/code&gt;，但是你不得不写上 9 错误判断。因此官方的博客中也给出了一个比较优雅的处理方案：将 &lt;code&gt;io.Writer&lt;/code&gt; 再封装一层。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type errWriter struct {
    w   io.Writer
    err error
}

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

ew := &amp;amp;errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
    return ew.err
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在看上去就好多了，&lt;code&gt;write(buf []byte)&lt;/code&gt; 方法在内部判断了错误值，来避免在外面多次的错误判断。当然，这种写法可能也有它的弊端，比如你没有办法知道出错在哪一行调用。大多数情况下，你只需要检查错误，然后进行处理而已。因此这种技巧还是很有用的。Golang 的标准库也有很多类似的技巧。比如&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// and so on
if b.Flush() != nil {
    return b.Flush()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;b.Write&lt;/code&gt; 是有错误值返回的，这只是为了符合 &lt;code&gt;io.Writer&lt;/code&gt; 接口。你可以在调用 &lt;code&gt;b.Flush()&lt;/code&gt; 的时候再进行错误值的判断。&lt;/p&gt;
&lt;h3&gt;2.2 Golang 1.13 前的错误处理&lt;/h3&gt;
&lt;h4&gt;检验错误&lt;/h4&gt;
&lt;p&gt;大多数情况下，我们只需要对错误进行简单的判断即可。因为我们不需要对错误做其他的处理，只需要保证代码逻辑正确执行即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;if err != nil {
    // something went wrong
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但有的时候我们需要根据错误类型进行不同的处理，比如网络连接未连接/断开导致的错误，我们应该在判断是未连接/断开时，进行重连操作。 在涉及到错误类型的判断时，我们通常有两种方法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将错误和已知的值进行比对&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;var ErrNotFound = errors.New(&quot;not found&quot;)

if err == ErrNotFound {
    // something wasn&apos;t found
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;判断错误的具体类型&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type NotFoundError struct {
    Name string
}

func (e *NotFoundError) Error() string { return e.Name + &quot;: not found&quot; }

if e, ok := err.(*NotFoundError); ok {
    // e.Name wasn&apos;t found
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;添加信息&lt;/h4&gt;
&lt;p&gt;当一个错误在经过多层的调用栈向上返回时，我们通常会在这个错误上添加一些额外的信息，以帮助开发人员判断错误出现时程序运行到了哪里，发生了什么。最简单的方式是，使用之前的错误信息构造新的错误：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;if err != nil {
    return fmt.Errorf(&quot;decompress %v: %v&quot;, name, err)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;fmt.Errorf&lt;/code&gt; 只保留了上一个错误的文本，丢弃了其他所有的信息。如果我们想保留上一个错误的所有信息，我们可以使用下面的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type QueryError struct {
    Query string
    Err   error
}

if e, ok := err.(*QueryError); ok &amp;amp;&amp;amp; e.Err == ErrPermission {
    // query failed because of a permission problem
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 Golang 1.13 中的错误处理&lt;/h3&gt;
&lt;p&gt;Golang 1.13 中，如果一个错误包含了另一个错误，则可以通过实现 &lt;code&gt;Unwrap()&lt;/code&gt; 方法来返回底层的错误。如果 &lt;code&gt;e1.Unwrap()&lt;/code&gt; 返回了 e2，我们就可以说 e1 包含了 e2。&lt;/p&gt;
&lt;h4&gt;使用 Is 和 As 来检验错误&lt;/h4&gt;
&lt;p&gt;在 2.2 中提到了错误信息的常见处理方式，在 Golang 1.13 中，标准库中添加了几个方法来帮助我们更快速的完成以上的工作。当前前提是，你的自定义 Error 正确的实现了 &lt;code&gt;Unwrap()&lt;/code&gt; 方法 &lt;code&gt;errors.Is&lt;/code&gt; 用来将一个错误和一个值进行对比：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Similar to:
//   if err == ErrNotFound { … }
if errors.Is(err, ErrNotFound) {
    // something wasn&apos;t found
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;errors.As&lt;/code&gt; 用来判断一个错误是否是一个特定的类型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Similar to:
//   if e, ok := err.(*QueryError); ok { … }
var e *QueryError
if errors.As(err, &amp;amp;e) {
    // err is a *QueryError, and e is set to the error&apos;s value
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当操作一个包装了的错误时，&lt;code&gt;Is&lt;/code&gt; 和 &lt;code&gt;As&lt;/code&gt; 会考虑错误链上所有的错误。一个完整的例子如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type ErrorA struct {
    Msg string
}

func (e *ErrorA) Error() string {
    return e.Msg
}

type ErrorB struct {
    Msg string
    Err *ErrorA
}

func (e *ErrorB) Error() string {
    return e.Msg + e.Err.Msg
}

func (e *ErrorB) Unwrap() error {
    return e.Err
}

func main() {
    a := &amp;amp;ErrorA{&quot;error a&quot;}

    b := &amp;amp;ErrorB{&quot;error b&quot;, a}

    if errors.Is(b, a) {
        log.Println(&quot;error b is a&quot;)
    }

    var tmpa *ErrorA
    if errors.As(b, &amp;amp;tmpa) {
        log.Println(&quot;error b as ErrorA&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error b is a
error b as ErrorA
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用 %w 来包装错误&lt;/h4&gt;
&lt;p&gt;Go 1.13 中增加了 %w，当 %w 出现时，由 &lt;code&gt;fmt.Errorf&lt;/code&gt; 返回的错误，将会有 &lt;code&gt;Unwrap&lt;/code&gt; 方法，返回的是 %w 对应的值。下面是一个简单的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type ErrorA struct {
    Msg string
}

func (e *ErrorA) Error() string {
    return e.Msg
}

func main() {
    a := &amp;amp;ErrorA{&quot;error a&quot;}

    b := fmt.Errorf(&quot;new error: %w&quot;, a)

    if errors.Is(b, a) {
        fmt.Println(&quot;error b is a&quot;)
    }

    var tmpa *ErrorA
    if errors.As(b, &amp;amp;tmpa) {
        fmt.Println(&quot;error b as ErrorA&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error b is a
error b as ErrorA
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;是否需要对错误进行包装&lt;/h4&gt;
&lt;p&gt;当你向一个 error 中添加额外的上下文信息时，要么使用 &lt;code&gt;fmt.Errorf&lt;/code&gt;，要么实现一个自定义的错误类型，这是你就要决定这个新的错误是否应该包装原始的错误信息。这是一个没有标准答案的问题，它取决于新错误创建的上下文。 包装一个错误是为了将它暴露给调用者。这样调用者就可以根据不同的原始错误作出不同的处理，比如 &lt;code&gt;os.Open(file)&lt;/code&gt; 会返回文件不存在这种具体的错误， 这样调用者就可以通过创建文件来让代码可以正确往下执行。 当我们不想暴露实现细节时就不要包装错误。因为暴露一个具备细节的错误，就意味的调用者和我们的代码产生了耦合。这也违反了抽象的原则。&lt;/p&gt;
&lt;h2&gt;三、参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.golang.org/errors-are-values&quot; rel=&quot;noopener&quot;&gt;Errors are values&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.golang.org/go1.13-errors&quot; rel=&quot;noopener&quot;&gt;Working with Errors in Go 1.13&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>go</category><author>joyme123</author></item><item><title>go 调度器的实现</title><link>https://www.myway5.com/blog/go-scheduler/</link><guid isPermaLink="true">https://www.myway5.com/blog/go-scheduler/</guid><description>goroutine 是 go 语言的特色之一，在 go 语言中，你可以轻易的使用 go 关键字来创建一个协程运行一段代码，协程在使用上和我们常说的线程相似，但是在 go 中，协程的实现并非是直接使用线程。</description><pubDate>Sat, 01 Feb 2020 06:33:58 GMT</pubDate><content:encoded>&lt;h2&gt;一、概述&lt;/h2&gt;
&lt;p&gt;goroutine 是 go 语言的特色之一，在 go 语言中，你可以轻易的使用 &lt;code&gt;go&lt;/code&gt; 关键字来创建一个协程运行一段代码，协程在使用上和我们常说的线程相似，但是在 go 中，协程的实现并非是直接使用线程。原因有二：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;线程的实现由操作系统决定，它附带了太多额外的特性，比如线程有它自己的信号掩码，线程能够被赋予 CPU affinity 功能，线程能够被添加到 Cgroup 中，线程所使用的资源也可以被查询到，线程的栈空间大小默认是 2M 等等。这些在 goroutine 中不需要的特性带来了额外的性能开销。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;除了上面的性能问题，在 go 语言的编程模型中，操作系统无法作出最佳的决策。比如在运行一次垃圾收集时，go 的垃圾收集器要求所有线程都被停止，并且内存处于一致性状态（关于内存一致性，可以参考这篇文章：&lt;a href=&quot;http://blog.chinaunix.net/uid-25909722-id-3016122.html&quot; rel=&quot;noopener&quot;&gt;内存一致性模型&lt;/a&gt;)。这个涉及到要等待全部运行时线程（running threads）到达一个点（point），我们事先知道在这个点内存是一致的。当在一个随机点上调度了多个线程，你不得不等待他们中的大多数到达一致性状态。go 调度程序可以决定仅在知道内存一致的点上进行调度，这样在任何时刻，当前没有被调度的线程是处于内存一致状态的，这意味着当我们要为垃圾收集停止线程运行时，我们只需要等待那些 CPU 核心上运行的线程。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，go 语言实现一个自己的调度器，它可以创建更轻量的线程，也就是 goroutine，同时，调度算法需要更智能，以提升大量并发下的性能。 在讨论 go 的调度器之前，我们还要讨论一下常见的线程模型，具体的内容可以参考这篇文章：&lt;a href=&quot;https://www.jianshu.com/p/5a4fc2729c17&quot; rel=&quot;noopener&quot;&gt;内核线程与用户线程的一点小总结&lt;/a&gt;. 我们根据线程的调度实现，将线程分为内核线程和用户线程。其中，内核线程是由操作系统调度，而用户线程是由用户自己实现的调度器进行调度。根据用户线程和内核线程的关系分为下面几种线程模型： &lt;strong&gt;1:1模型&lt;/strong&gt; 1:1 模型很简单，就是将一个用户线程映射或绑定到一个内核线程上，但是这会导致线程的上下文切换会很慢。 &lt;strong&gt;N:1模型&lt;/strong&gt; N:1 模型中会将多个用户线程映射或绑定到一个内核线程上，这样多个用户线程之间的上下文切换会很快，但是缺点是没法利用多核的优势。 &lt;strong&gt;M:N模型&lt;/strong&gt; 1:1 模型 和 N:1 模型都有它们各自的缺点，因此 go 调度器中使用了 M:N 模型，既能保证利用到多核的优势，也能保证更快的上下文切换。这也是我们接下来要讨论的问题。&lt;/p&gt;
&lt;h2&gt;二、go 1.0 的调度器实现方案&lt;/h2&gt;
&lt;p&gt;go 1.0 的调度器实现方案其实并不是这篇文章的关注点，因为它的生命周期太短。之所以要拿出来单独说，是为了更好的理解 go 1.1 中调度器实现方案解决的问题以及带来的性能提升。 为了更好的阐述这中间的机制，我们使用符号 M 来表示内核线程，使用符号 G 来表示 goroutine。在这个版本中，只有一个全局的 goroutine 队列，所有的内核线程都要从这个队列中取出 和放回goroutine。下图是一个包含两个内核线程，只有一个全局队列的例子。 &lt;img src=&quot;/uploads/wp/2020/01/1-1.png&quot; alt=&quot;go scheduler 1.0&quot; /&gt; 只有一个全局队列，导致无法保证一个 goroutine 会在同一个内核线程上调度。因此，一个 goroutine 会在不同的内核线程上调度，导致上下文切换较慢，非常影响性能。下面是一个阻塞通道的例子，用来说明这个问题： goroutine G7 阻塞在 channel 上，等待接收一个消息。一旦接收到这个消息，G7 就被放到全局队列中。 &lt;img src=&quot;/uploads/wp/2020/01/2.png&quot; alt=&quot;go scheduler 1.0&quot; /&gt; G7 被放到队列尾后，队列首的 GX 得到了被执行的机会，因此它被调度上第一个 M 上执行。此时，G8 也阻塞在 channel 上。 &lt;img src=&quot;/uploads/wp/2020/01/3.png&quot; alt=&quot;go scheduler 1.0&quot; /&gt; G7 重新得到了执行的机会，但是因为第一个 M 正在执行 GX，因此 G7 只能调度到第二个 M 上执行。这时候就发生了跨内核线程的上下文切换。 &lt;img src=&quot;/uploads/wp/2020/01/4.png&quot; alt=&quot;go scheduler 1.0&quot; /&gt; 只有一个全局队列还带来了另外一个问题，因为从队列中获取 goroutine 必须要加锁，导致锁的争用非常频繁。尤其是在大量 goroutine 被调度的情况下，对性能的影响也会非常明显。 另外在 &lt;a href=&quot;https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit#&quot; rel=&quot;noopener&quot;&gt;Scalable Go Scheduler Design Doc&lt;/a&gt; 这篇文章中还提到了另外两个问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;所有的 M 都关联了内存缓存（mcache）和其他的缓存（栈空间），但实际上只有正在运行的 go 代码的 M 才需要 mcache（阻塞在系统调用的 M 不需要 mcache）。运行 go 代码的 M 和系统调用阻塞的 M 比例大概在 1:100，这就导致了大量的资源消耗（每个 mcache 会占用到 2M）以及 poor data locality（poor data locality找不到好的翻译，意思大概是内存缓存命中会很少，导致内存缓存无效，可参考这里：&lt;a href=&quot;https://www.quora.com/What-does-it-mean-that-hash-sets-have-poor-data-locality&quot; rel=&quot;noopener&quot;&gt;What does it mean that hash sets have poor data locality?&lt;/a&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;激进的线程阻塞/解阻塞。因为系统调用导致工作线程经常被阻塞和解阻塞，这增加了很多的负担。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面就是4个关于 go 1.0 中调度器的问题所在。&lt;code&gt;Dmitry Vyukov&lt;/code&gt; 因此提出了新的调度算法，并在 go 1.1 中发布。&lt;/p&gt;
&lt;h2&gt;三、go 1.1 之后的调度器实现方案&lt;/h2&gt;
&lt;p&gt;针对在 1.0 中调度器实现的问题，go 1.1 在 M, G 的基础上，引入了新的角色 P（processor）。以下简单列出新的调度算法的改进点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新角色 P 代替了 M 的一部分功能，M 的 mcache 现在属于 P 了，并且 P 的数量等于 GOMAXPROCS。M 要执行 G 的时候，就被调度绑定一个 P，然后去执行 G，这样对内存的占用就大大减少。&lt;/li&gt;
&lt;li&gt;每个 P 都有自己的 goroutine 队列 runq，新的 G 就放到自己的 runq 上，满了之后再放到全局的 runq，优先执行自己的 runq。这样的设计大大减少了锁的争用，并且可以保证尽量少的在多个核心上传递 G。&lt;/li&gt;
&lt;li&gt;当 G 执行网络操作和锁切换时，G 和 M 分离，M 通过调度执行新的 G。这样就可以保证用户在 G 中执行网络操作时不用考虑阻塞线程的问题。&lt;/li&gt;
&lt;li&gt;当 M 因为执行系统调用阻塞或 cgo 运行一段时间后，sysmon 协程会将 P 和 M分离，由其他的 M 来结合 P 进行调度。这样 M 就不用因为阻塞而占用不必要的资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面是一个比较易懂的图例： &lt;img src=&quot;/uploads/wp/2020/02/our-cast.jpg&quot; alt=&quot;cast&quot; /&gt; M 是内核线程，P 是调度的上下文，G 是 goroutine。 &lt;img src=&quot;/uploads/wp/2020/02/in-motion.jpg&quot; alt=&quot;in-motion&quot; /&gt; 这里可以看到。M 绑定一个 P 来执行 G，灰色部分代表待执行的、只属于 P 的 goroutine 队列。 &lt;img src=&quot;/uploads/wp/2020/02/syscall.jpg&quot; alt=&quot;syscall&quot; /&gt; M0 线程因为执行 syscall 导致阻塞，因此调度算法将其和 P 分离，防止其占用 P 的资源，然后将 P 分给 M1 来执行剩下的 goroutine。 &lt;img src=&quot;/uploads/wp/2020/02/steal.jpg&quot; alt=&quot;steal&quot; /&gt; 第二个 P 中的 goroutine 已经执行结束。因此从第一个 P 中“偷来”一部分的 goroutine 执行。 下面是一个比较详细的 GPM 模型示意图： &lt;img src=&quot;/uploads/wp/2020/02/Picture1.png&quot; alt=&quot;gpm&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;四、附加的参考资料&lt;/h2&gt;
&lt;p&gt;下面的一些图片是从腾讯技术团队分享的 PPT 中截取的，PPT 地址：&lt;a href=&quot;https://github.com/yifhao/share&quot; rel=&quot;noopener&quot;&gt;深入浅出Golang Runtime&lt;/a&gt; &lt;img src=&quot;/uploads/wp/2020/02/go-runtime-changelog.png&quot; alt=&quot;go-runtime&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;五、参考文档&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://morsmachine.dk/go-scheduler&quot; rel=&quot;noopener&quot;&gt;The Go scheduler - Morsing&apos;s blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.jianshu.com/p/5a4fc2729c17&quot; rel=&quot;noopener&quot;&gt;内核线程与用户线程的一点小总结 《程序员的自我修养》·笔记&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/a-journey-with-go/go-concurrency-scheduler-affinity-3b678f490488&quot; rel=&quot;noopener&quot;&gt;Go: Concurrency &amp;amp; Scheduler Affinity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit#&quot; rel=&quot;noopener&quot;&gt;Scalable Go Scheduler Design Doc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/20862617&quot; rel=&quot;noopener&quot;&gt;Golang 的 goroutine 是如何实现的？&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>go</category><author>joyme123</author></item><item><title>mock 测试和 gomock 的使用</title><link>https://www.myway5.com/blog/gomock/</link><guid isPermaLink="true">https://www.myway5.com/blog/gomock/</guid><description>在平常做单元测试中，常常会依赖外部的系统，这导致单元测试很难写。比如业务系统中有一个用户信息更新的函数 UpdateUserInfo，如果对该函数做单元测试，则需要连接数据库，建立测试所需的基础数据，然后执行测试，最后清除测试导致的数据更新。</description><pubDate>Thu, 23 Jan 2020 13:04:16 GMT</pubDate><content:encoded>&lt;h2&gt;mock 测试是什么&lt;/h2&gt;
&lt;p&gt;在平常做单元测试中，常常会依赖外部的系统，这导致单元测试很难写。比如业务系统中有一个用户信息更新的函数 &lt;code&gt;UpdateUserInfo&lt;/code&gt;，如果对该函数做单元测试，则需要连接数据库，建立测试所需的基础数据，然后执行测试，最后清除测试导致的数据更新。这导致单元测试的成本很高，并且难以维护。 这时候，mock 测试就可以发挥它的作用了。我们将对数据库的操作做成假的，也就是 mock 出一个假的数据库操作对象，然后注入到我们的业务逻辑中使用，然后就可以对业务逻辑进行测试。 看了描述可能还是有点糊涂，下面会用一个例子来说明&lt;/p&gt;
&lt;h2&gt;一个 mock 测试的例子&lt;/h2&gt;
&lt;p&gt;这个例子是一个简单的用户登录，其中，&lt;code&gt;UserDBI&lt;/code&gt; 是用户表操作的接口，其实现是&lt;code&gt;UserDB&lt;/code&gt;，我们的业务层有 &lt;code&gt;UserService&lt;/code&gt;，实现了 &lt;code&gt;Login&lt;/code&gt; 方法，我们现在要做的就是对 &lt;code&gt;Login&lt;/code&gt; 这里的业务逻辑进行单元测试。项目结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── db
│   └── userdb.go
├── go.mod
├── go.sum
├── mocks
└── service
    ├── user.go
    └── user_test.go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UserDBI&lt;/code&gt; 的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type UserDBI interface {
    Get(name string, password string) (*User, error)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UserDB&lt;/code&gt; 的相关代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type UserDB struct {
    db *sql.DB
}

func NewUserDB(user string, password string, host string, port int, db string) (UserDBI, error) {
    dsn := fmt.Sprintf(&quot;%s:%s@tcp(%s:%d)/%s&quot;, user, password, host, port, db)

    var userDB UserDB
    var err error

    userDB.db, err = sql.Open(&quot;mysql&quot;, dsn)

    if err != nil {
        return nil, err
    }

    return &amp;amp;userDB, nil
}

// Get 根据 UserID 获取用户资料
func (udb *UserDB) Get(name string, password string) (*User, error) {
    s := &quot;SELECT * FROM user WHERE name = ? AND password = ?&quot;
    stmt, err := udb.db.Prepare(s)
    if err != nil {
        return nil, err
    }

    defer stmt.Close()

    var user User
    err = stmt.QueryRow(name, password).Scan(&amp;amp;user)
    if err != nil {
        return nil, err
    }

    return &amp;amp;user, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Login&lt;/code&gt; 的逻辑如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type UserService struct {
    db db.UserDBI
}

// NewUserService 实例化用户服务
func NewUserService(db db.UserDBI) *UserService {
    var userService UserService
    userService.db = db

    return &amp;amp;userService
}

// Login 登录
func (userService *UserService) Login(name, password string) (*db.User, error) {
    user, err := userService.db.Get(name, password)
    if err != nil {
        log.Println(err)
        return nil, err
    }

    return user, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以知道，通过 &lt;code&gt;NewUserService&lt;/code&gt; 可以实例化出 UserService 对象，然后调用 &lt;code&gt;Login&lt;/code&gt; 即可实现登录逻辑，但是在 &lt;code&gt;Login&lt;/code&gt; 中调用了 UserDB 的 &lt;code&gt;Get&lt;/code&gt; 方法，而 &lt;code&gt;Get&lt;/code&gt; 方法又会从实际的数据库中去查询。这就是我们这个例子的测试难点：有没有办法不依赖实际的数据库去完成单元测试呢？ 这里我们的 &lt;code&gt;NewUserService&lt;/code&gt; 的参数是 &lt;code&gt;UserDBI&lt;/code&gt; 这个接口，在实际的代码运行中，我们是将 &lt;code&gt;UserDB&lt;/code&gt; 的实例化对象传进去的，但是在测试的时候，我们完全可以传入一个不操作数据库的假的对象，这个对象只需要实现了 &lt;code&gt;UserDBI&lt;/code&gt; 的接口即可。因此我们创建了一个 &lt;code&gt;FakeUserDB&lt;/code&gt;，这个 &lt;code&gt;FakeUserDB&lt;/code&gt; 就是我们 mock 出来的内容了。这个 &lt;code&gt;FakeUserDB&lt;/code&gt; 非常简单，因为它什么也不包含。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type FakeUserDB struct {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，这个 &lt;code&gt;FakeUserDB&lt;/code&gt; 实现了 &lt;code&gt;Get&lt;/code&gt; 方法，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (db *FakeUserDB) Get(name string, password string) (*User, error) {
    if name == &quot;user&quot; &amp;amp;&amp;amp; password == &quot;123456&quot; {
        return &amp;amp;User{ID: 1, Name: &quot;user&quot;, Password: &quot;123456&quot;, Age: 20, Gender: &quot;male&quot;}, nil
    } else {
        return nil, errors.New(&quot;no such user&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 Get 方法中既可以返回正常情况，又可以返回错误的情况，完全满足我们的测试需求。这样，我们就完成 mock 测试的一大半内容了，接下来我们来实际写单元测试即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestUserLoginWithFakeDB(t *testing.T) {

    testcases := []struct {
        Name        string
        Password    string
        ExpectUser  *db.User
        ExpectError bool
    }{
        {&quot;user&quot;, &quot;123456&quot;, &amp;amp;db.User{1, &quot;user&quot;, &quot;123456&quot;, 20, &quot;male&quot;}, false},
        {&quot;user2&quot;, &quot;123456&quot;, nil, true},
    }

    var fakeUserDB db.FakeUserDB
    userService := NewUserService(&amp;amp;fakeUserDB)
    for i, testcase := range testcases {

        user, err := userService.Login(testcase.Name, testcase.Password)

        if testcase.ExpectError {
            assert.Error(t, err, &quot;login error:&quot;, i)
        } else {
            assert.NoError(t, err, &quot;login error:&quot;, i)
        }

        assert.Equal(t, testcase.ExpectUser, user, &quot;user doesn&apos;t equal&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行单元测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ go test github.com/joyme123/gomock-examples/service
ok      github.com/joyme123/gomock-examples/service     0.002s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，我们在测试时使用了 FakeUserDB，这样就彻底摆脱了数据库，并且这里的单元测试考虑了登录成功和登录失败的方式。 但是手写 &lt;code&gt;FakeUserDB&lt;/code&gt; 同样也有点工作量，这个例子为了简洁所以体现不出来。考虑当 &lt;code&gt;UserDBI&lt;/code&gt; 这个接口的方法很多的时候，我们需要额外手写的代码量立马就多了起来。还好 go 官方就提供了 &lt;code&gt;gomock&lt;/code&gt; 这个工具，来帮我们更好的完成单元测试的工作。&lt;/p&gt;
&lt;h2&gt;gomock 的使用&lt;/h2&gt;
&lt;p&gt;gomock 的官方仓库地址是：&lt;a href=&quot;https://github.com/golang/mock.git&quot; rel=&quot;noopener&quot;&gt;https://github.com/golang/mock.git&lt;/a&gt;。gomock 并不复杂，其主要的工作是将我们刚刚的 &lt;code&gt;FakeUserDB&lt;/code&gt; 由手动编写变成自动生成。因此我会用刚刚的例子加上 gomock 再做一遍示范。&lt;/p&gt;
&lt;h3&gt;gomock 的安装&lt;/h3&gt;
&lt;p&gt;执行以下命令即可安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GO111MODULE=on go get github.com/golang/mock/mockgen@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;mockgen 会安装在你的 $GOPATH 下的 bin 目录中。&lt;/p&gt;
&lt;h3&gt;gomock 生成代码&lt;/h3&gt;
&lt;p&gt;在上面的例子中，我们用 &lt;code&gt;FakeUserDB&lt;/code&gt; 实现了 &lt;code&gt;UserDBI&lt;/code&gt; 这个接口，这里同样也是使用 mockgen 这个程序生成实现 &lt;code&gt;UserDBI&lt;/code&gt; 的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir mocks
mockgen -package=mocks -destination=mocks/userdb_mock.go github.com/joyme123/gomock-examples/db UserDBI
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 mocks 下生成的文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/joyme123/gomock-examples/db (interfaces: UserDBI)

// Package mocks is a generated GoMock package.
package mocks

import (
    gomock &quot;github.com/golang/mock/gomock&quot;
    db &quot;github.com/joyme123/gomock-examples/db&quot;
    reflect &quot;reflect&quot;
)

// MockUserDBI is a mock of UserDBI interface
type MockUserDBI struct {
    ctrl     *gomock.Controller
    recorder *MockUserDBIMockRecorder
}

// MockUserDBIMockRecorder is the mock recorder for MockUserDBI
type MockUserDBIMockRecorder struct {
    mock *MockUserDBI
}

// NewMockUserDBI creates a new mock instance
func NewMockUserDBI(ctrl *gomock.Controller) *MockUserDBI {
    mock := &amp;amp;MockUserDBI{ctrl: ctrl}
    mock.recorder = &amp;amp;MockUserDBIMockRecorder{mock}
    return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockUserDBI) EXPECT() *MockUserDBIMockRecorder {
    return m.recorder
}

// Get mocks base method
func (m *MockUserDBI) Get(arg0, arg1 string) (*db.User, error) {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, &quot;Get&quot;, arg0, arg1)
    ret0, _ := ret[0].(*db.User)
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// Get indicates an expected call of Get
func (mr *MockUserDBIMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, &quot;Get&quot;, reflect.TypeOf((*MockUserDBI)(nil).Get), arg0, arg1)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;执行测试&lt;/h3&gt;
&lt;p&gt;代码生成结束之后，我们开始写单元测试了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestUserLoginWithGoMock(t *testing.T) {
    testcases := []struct {
        Name        string
        Password    string
        MockUser    *db.User
        MockErr     error
        ExpectUser  *db.User
        ExpectError bool
    }{
        {&quot;user&quot;, &quot;123456&quot;, &amp;amp;db.User{1, &quot;user&quot;, &quot;123456&quot;, 20, &quot;male&quot;}, nil, &amp;amp;db.User{1, &quot;user&quot;, &quot;123456&quot;, 20, &quot;male&quot;}, false},
        {&quot;user2&quot;, &quot;123456&quot;, nil, errors.New(&quot;&quot;), nil, true},
    }

    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    userDB := mocks.NewMockUserDBI(ctrl)

    for i, testcase := range testcases {
        userDB.EXPECT().Get(testcase.Name, testcase.Password).Return(testcase.MockUser, testcase.MockErr)
        userService := NewUserService(userDB)
        user, err := userService.Login(testcase.Name, testcase.Password)

        if testcase.ExpectError {
            assert.Error(t, err, &quot;login error:&quot;, i)
        } else {
            assert.NoError(t, err, &quot;login error:&quot;, i)
        }

        assert.Equal(t, testcase.ExpectUser, user, &quot;user doesn&apos;t equal&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在测试用例中增加了两个字段：MockUser, MockErr，这就是我们 Mock 出来的数据，通过 &lt;code&gt;userDB := mocks.NewMockUserDBI(ctrl)&lt;/code&gt; 实例化 mock 出来的 userDB，这里的 userDB 等价于上一个例子中的 &lt;code&gt;fakeUserDB&lt;/code&gt;，然后调用 &lt;code&gt;userDB.EXPECT().Get(testcase.Name, testcase.Password).Return(testcase.MockUser, testcase.MockErr)&lt;/code&gt; 这句话，来输入我们想输入的参数，产生我们想要的输出即可。这样在 &lt;code&gt;Login&lt;/code&gt; 函数执行时会自动产生我们刚刚设定的 Mock 数据，完成单元测试的需求。 如果传参的时候，对参数不确定，可以使用 &lt;code&gt;gomock.Any()&lt;/code&gt; 来代替，如果希望多次调用该方法仍然返回相同的结果，可以使用 &lt;code&gt;.AnyTimes()&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;mock 测试在实现上的重点是将外部依赖实现成可替换的，例子中使用了 &lt;code&gt;UserDBI&lt;/code&gt; 这个接口来抽象出用户表的操作，然后使用参数的方式来实现 &lt;code&gt;UserService&lt;/code&gt; 的实例化。接口和使用参数来实例化（也就是不要把外部依赖写死）缺一不可。只要注意到这一点就可以写出方便 mock 测试的代码。&lt;/p&gt;
</content:encoded><category>go</category><category>gomock</category><category>unit test</category><author>joyme123</author></item><item><title>容器中程序的信号捕捉</title><link>https://www.myway5.com/blog/container-signal/</link><guid isPermaLink="true">https://www.myway5.com/blog/container-signal/</guid><description>项目中使用了 argo 在 kubernetes 集群中做工作流的调度。argo 提供了工作流的停止功能，其原理大致是检查正在运行的 Pod，向该 Pod 中的 wait 容器发送 USR2 信号，wait 容器收到 USR2 信号后，在主机上的调用 docker kill -…</description><pubDate>Fri, 17 Jan 2020 16:27:52 GMT</pubDate><content:encoded>&lt;h2&gt;一、问题描述&lt;/h2&gt;
&lt;p&gt;项目中使用了 argo 在 kubernetes 集群中做工作流的调度。argo 提供了工作流的停止功能，其原理大致是检查正在运行的 Pod，向该 Pod 中的 wait 容器发送 USR2 信号，wait 容器收到 USR2 信号后，在主机上的调用 &lt;code&gt;docker kill --signal TERM main_container_id&lt;/code&gt; 来停止我们的程序容器, 如果 10s 后容器还未停止，则发送 SIGKILL 来强制终止。但是我在实现 argo 工作流中调度 &lt;code&gt;tfjob&lt;/code&gt; 时出现了一些问题。 &lt;img src=&quot;/uploads/wp/2020/01/argo_scheduler_tfjob.png&quot; alt=&quot;argo_scheduler_tfjob&quot; /&gt; 在argo停止工作流时，正在运行的 step2 中的 manager 监听了 TERM 信号，以便在工作流停止时同步停止 tfjob。但是事实情况却是 manager 退出了，但是没有收到任何的 TERM 信号。&lt;/p&gt;
&lt;h2&gt;二、问题剖析&lt;/h2&gt;
&lt;p&gt;检查这个问题的第一步是弄清楚 &lt;code&gt;docker kill&lt;/code&gt; 背后发生了什么，官网的资料中有以下的描述：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: ENTRYPOINT and CMD in the shell form run as a subcommand of /bin/sh -c, which does not pass signals. This means that the executable is not the container’s PID 1 and does not receive Unix signals.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当我们用 &lt;code&gt;sh&lt;/code&gt; 执行一段 shell script 时，在 shell script 中的可执行文件的 PID 不是1，并且 sh 也不会帮忙转发 TERM 信号，导致我们的可执行文件无法接收到终止信号，并执行清理逻辑。 我们的 manager 确实是用了一段 shell script 来启动的，可能就是因为这个原因导致无法收到 TERM 信号。&lt;/p&gt;
&lt;h2&gt;三、问题复现&lt;/h2&gt;
&lt;p&gt;我写了一段很简单的 go 程序，监听了 TERM 信号，然后打印一段文字。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;log&quot;
    &quot;os&quot;
    &quot;os/signal&quot;
    &quot;syscall&quot;
)

func main() {
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT)

    s, ok := &amp;lt;-sigs
    if !ok {
        log.Println(&quot;信号接收出错&quot;)
        os.Exit(1)
    }

    log.Println(&quot;收到信号:&quot;, s.String())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我的 Dockerfile 如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM alpine:latest
LABEL maintainr=&quot;jiangpengfei &amp;lt;jiangpengfei12@gmail.com&amp;gt;&quot;

COPY main /usr/bin/main
COPY run.sh /usr/bin/run.sh
RUN chmod +x /usr/bin/main &amp;amp;&amp;amp; chmod +x /usr/bin/run.sh

CMD [&quot;sh&quot;, &quot;-c&quot;, &quot;/usr/bin/run.sh&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;run.sh 如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/bin/sh
/usr/bin/main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行这个容器后，查看容器内的进程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PID   USER     TIME  COMMAND
    1 root      0:00 {busybox} ash /usr/bin/run.sh
    6 root      0:00 /usr/bin/main
   12 root      0:00 sh
   17 root      0:00 ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现，&lt;code&gt;run.sh&lt;/code&gt; 是 PID 为1, &lt;code&gt;main&lt;/code&gt; 程序是6。此时我们使用 &lt;code&gt;docker kill --signal TERM main_container_id&lt;/code&gt; 来停止容器，发现确实是没有反应的。因为 TERM 信号会发送给 PID 为 1 的进程。同时也因为 sh 不响应 TERM 信号，也不会转发该信号给子进程，所以容器也不会退出。如果我们使用 &lt;code&gt;docker stop&lt;/code&gt; 退出的话，会发现很慢，这是因为 &lt;code&gt;docker stop&lt;/code&gt; 会尝试先用 TERM 信号来终止进程，一段时间后发现没有退出的话再使用 KILL 信号。&lt;/p&gt;
&lt;h2&gt;四、解决方案&lt;/h2&gt;
&lt;p&gt;这个问题的解决方案有很多，要么让我们的程序进程成为 PID 1，要么让 PID 为 1 的进程转发这个 TERM 信号给我们的子进程。 &lt;strong&gt;方法一: 在 shell script 中使用 exec&lt;/strong&gt; 将我们的 &lt;code&gt;run.sh&lt;/code&gt; 改成如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh
exec /usr/bin/main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再查看容器内的进程列表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PID   USER     TIME  COMMAND
    1 root      0:00 /usr/bin/main
   11 root      0:00 sh
   16 root      0:00 ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现，&lt;code&gt;main&lt;/code&gt; 进程的PID 是 1, 我们使用 &lt;code&gt;docker kill --signal TERM main_container_id&lt;/code&gt; 来杀死进程，出现如下打印语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2020/01/17 23:46:24 收到信号: terminated
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见，&lt;code&gt;exec&lt;/code&gt; 可以让我们的 main 进程成为 PID 为 1, 关于 exec 的作用描述如下:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The exec() family of functions replaces the current process image with a new process image.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;即使用新进程的镜像替换当前进程的镜像数据，可以理解为exec系统调用并没有创建新的进程，只是替换了原来进程上下文的内容。原进程的代码段，数据段，堆栈段被新的进程所代替。这样我们的 main 进程就顺利成章的替换了 sh 进程成为 PID 为 1 的进程了。 &lt;strong&gt;方法二: 直接使用 main 作为镜像入口&lt;/strong&gt; 这是最简单的方法了，但是很多时候会有限制，因为我们希望在 shell script 中写一些逻辑来调用程序。 &lt;strong&gt;方法三: 借助第三方程序&lt;/strong&gt; 一些第三方的程序专门提供了这样的作用，以它们作为启动的入口，这些第三方程序会 watch 所有它产生的子进程，在这些子进程退出后自动退出，并且在其收到 TERM 信号后发送给子进程。 这里我们用 &lt;a href=&quot;https://github.com/insidewhy/smell-baron&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;smell-baron&lt;/code&gt;&lt;/a&gt; 这个应用作为例子 修改 Dockerfile:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM alpine:latest
LABEL maintainr=&quot;jiangpengfei &amp;lt;jiangpengfei12@gmail.com&amp;gt;&quot;

COPY main /usr/bin/main
COPY run.sh /usr/bin/run.sh
RUN chmod +x /usr/bin/main &amp;amp;&amp;amp; chmod +x /usr/bin/run.sh
RUN wget -O /usr/bin/smell-baron https://github.com/insidewhy/smell-baron/releases/download/v0.4.2/smell-baron.musl &amp;amp;&amp;amp; chmod +x /usr/bin/smell-baron

CMD [&quot;/usr/bin/smell-baron&quot;, &quot;/usr/bin/run.sh&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看容器内的进程:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PID   USER     TIME  COMMAND
    1 root      0:00 /usr/bin/smell-baron /usr/bin/run.sh
    6 root      0:00 /usr/bin/main
   14 root      0:00 sh
   19 root      0:00 ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;docker kill&lt;/code&gt; 发现 main 收到了 TERM 信号。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1.Multiple commands can be run, smell-baron will exit when all the watched processes have exited. 2.Whether a spawned process is watched can be configured. 3.smell-baron can be told to signal all child processes on termination, this allows it to cleanly deal with processes that spawn a subprocess in a different process group then fail to clean it up on exit.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><category>linux</category><category>容器技术</category><category>signal</category><author>joyme123</author></item><item><title>kubernetes存储--FlexVolume</title><link>https://www.myway5.com/blog/kubernetes-flexvolume/</link><guid isPermaLink="true">https://www.myway5.com/blog/kubernetes-flexvolume/</guid><description>kubernetes 使用 volume 来满足它的存储需求，它支持很多的存储系统，比如 nfs、 glusterfs、cephfs等等，但是这些存储的实现方式有一个问题，就是它们的实现代码都必须合并到 Kubernetes 的代码中（称为 in-tree），这为 kubern…</description><pubDate>Thu, 16 Jan 2020 13:06:52 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;kubernetes 使用 volume 来满足它的存储需求，它支持很多的存储系统，比如 nfs、 glusterfs、cephfs等等，但是这些存储的实现方式有一个问题，就是它们的实现代码都必须合并到 Kubernetes 的代码中（称为 in-tree），这为 kubernetes 社区带来了维护上的成本。因此，kubernetes 提出了两种 out-of-tree 的方案: FlexVolume 和 csi。通过这两种方案实现的存储功能不必合并到 kubernetes 的代码仓库，由存储系统的供应商单独维护。 FlexVolume 是这篇文章主要关注的点，FlexVolume 自 1.2 版本开始就支持了。它使用基于 exec 的模型来与驱动程序对接。用户必须在每个节点（有些情况下包括主节点）上的预定义卷插件路径中安装 FlexVolume 驱动程序的可执行文件。当需要挂载 volume 的时候，由 kubelet 执行挂载命令来挂载即可。&lt;/p&gt;
&lt;h2&gt;基于 nfs 实现 FlexVolume&lt;/h2&gt;
&lt;p&gt;在探究 FlexVolume 的实现原理之前，我们可以先看一下官方提供的&lt;a href=&quot;https://github.com/kubernetes/examples/tree/master/staging/volumes/flexvolume&quot; rel=&quot;noopener&quot;&gt;基于 nfs 的例子&lt;/a&gt;。 注: 我这里是用 &lt;code&gt;minikube&lt;/code&gt; 启动的本地 kubernetes 集群。 为了部署基于 nfs 实现的 FlexVolume，我们首先将目录下的 nfs 复制到 &lt;code&gt;deploy&lt;/code&gt; 文件夹下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ cp nfs deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后将 &lt;code&gt;deploy/deploy.sh&lt;/code&gt; 中的 &lt;code&gt;dummy&lt;/code&gt; 修改成 &lt;code&gt;nfs&lt;/code&gt;，表示我们使用的插件脚本是 &lt;code&gt;nfs&lt;/code&gt; 这个可执行文件。 接着在 &lt;code&gt;deploy&lt;/code&gt; 文件夹下构建 docker 镜像，这里要修改 &lt;code&gt;Dockerfile&lt;/code&gt;，将 nfs &lt;code&gt;COPY&lt;/code&gt; 到镜像中。然后执行下面的命令（镜像标签需要修改成你自己的）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ docker build -t joyme/nfs-flexvolume:1.0 .
$ docker push joyme/nfs-flexvolume:1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;镜像构建并推送完成之后，我们就开始部署了。因为 FlexVolume 要求将驱动文件放在指定的目录下，最粗暴的方式就是手动将文件 scp 到集群的每个节点上。这里为了方便，我们还可以使用 kubernetes 的 &lt;code&gt;Daemenset&lt;/code&gt;，然后使用 hostPath 将文件放到主机之上。我们修改 &lt;code&gt;deploy&lt;/code&gt; 文件夹下的 &lt;code&gt;ds.yaml&lt;/code&gt; 这个部署文件。将我们刚刚推送的镜像填进去。然后执行以下命令进行部署。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl apply -f ds.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有个地方要注意， 默认的插件安装地址是 &lt;code&gt;/usr/libexec/kubernetes/kubelet-plugins/volume/exec/&lt;/code&gt;, 但是 kubelet 的参数 &lt;code&gt;--volume-plugin-dir&lt;/code&gt; 和 controller manager 的参数 &lt;code&gt;--flex-volume-plugin-dir&lt;/code&gt; 都可以修改这个值，如果你启动这些组件是指定了这些参数，那就需要修改 &lt;code&gt;ds.yaml&lt;/code&gt; 中的路径。 在集群中部署完成之后，我们可以到某个节点上检查一下&lt;code&gt;/usr/libexec/kubernetes/kubelet-plugins/volume/exec/&lt;/code&gt;是否存在我们刚刚部署的文件。 最后我们创建一个 nginx，挂载一个 FlexVolume。在创建之前，我们需要先启动一个 nfs server，这里为了方便，可以使用容器启动一个。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ docker run -d --privileged --restart=always \
-v /tmp:/dws_nas_scratch \
-e NFS_EXPORT_DIR_1=/dws_nas_scratch \
-e NFS_EXPORT_DOMAIN_1=\* \
-e NFS_EXPORT_OPTIONS_1=ro,insecure,no_subtree_check,no_root_squash,fsid=1 \
-p 111:111 -p 111:111/udp \
-p 2049:2049 -p 2049:2049/udp \
-p 32765:32765 -p 32765:32765/udp \
-p 32766:32766 -p 32766:32766/udp \
-p 32767:32767 -p 32767:32767/udp \
fuzzle/docker-nfs-server:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用官方提供的 &lt;code&gt;nginx-nfs.yaml&lt;/code&gt; 文件，然后把其中的 server 地址修改一下，使用以下命令创建:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ kubectl apply -f nginx-nfs.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：如果出现错误，可以检查 node 上是否安装了 &lt;code&gt;jq&lt;/code&gt;, &lt;code&gt;nfs-common&lt;/code&gt; 等必要的依赖包。&lt;/p&gt;
&lt;h2&gt;实现原理&lt;/h2&gt;
&lt;p&gt;在完成上面例子的过程中，关于 FlexVolume 的大多数问题都比较好解答了。我们来看一下 &lt;code&gt;nfs&lt;/code&gt; 的实现代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;usage() {
    err &quot;Invalid usage. Usage: &quot;
    err &quot;\t$0 init&quot;
    err &quot;\t$0 mount &amp;lt;mount dir&amp;gt; &amp;lt;json params&amp;gt;&quot;
    err &quot;\t$0 unmount &amp;lt;mount dir&amp;gt;&quot;
    exit 1
}

err() {
    echo -ne $* 1&amp;gt;&amp;amp;2
}

log() {
    echo -ne $* &amp;gt;&amp;amp;1
}

ismounted() {
    MOUNT=`findmnt -n ${MNTPATH} 2&amp;gt;/dev/null | cut -d&apos; &apos; -f1`
    if [ &quot;${MOUNT}&quot; == &quot;${MNTPATH}&quot; ]; then
        echo &quot;1&quot;
    else
        echo &quot;0&quot;
    fi
}

domount() {
    MNTPATH=$1

    NFS_SERVER=$(echo $2 | jq -r &apos;.server&apos;)
    SHARE=$(echo $2 | jq -r &apos;.share&apos;)

    if [ $(ismounted) -eq 1 ] ; then
        log &apos;{&quot;status&quot;: &quot;Success&quot;}&apos;
        exit 0
    fi

    mkdir -p ${MNTPATH} &amp;amp;&amp;gt; /dev/null

    mount -t nfs ${NFS_SERVER}:/${SHARE} ${MNTPATH} &amp;amp;&amp;gt; /dev/null
    if [ $? -ne 0 ]; then
        err &quot;{ \&quot;status\&quot;: \&quot;Failure\&quot;, \&quot;message\&quot;: \&quot;Failed to mount ${NFS_SERVER}:${SHARE} at ${MNTPATH}\&quot;}&quot;
        exit 1
    fi
    log &apos;{&quot;status&quot;: &quot;Success&quot;}&apos;
    exit 0
}

unmount() {
    MNTPATH=$1
    if [ $(ismounted) -eq 0 ] ; then
        log &apos;{&quot;status&quot;: &quot;Success&quot;}&apos;
        exit 0
    fi

    umount ${MNTPATH} &amp;amp;&amp;gt; /dev/null
    if [ $? -ne 0 ]; then
        err &quot;{ \&quot;status\&quot;: \&quot;Failed\&quot;, \&quot;message\&quot;: \&quot;Failed to unmount volume at ${MNTPATH}\&quot;}&quot;
        exit 1
    fi

    log &apos;{&quot;status&quot;: &quot;Success&quot;}&apos;
    exit 0
}

op=$1

if ! command -v jq &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
    err &quot;{ \&quot;status\&quot;: \&quot;Failure\&quot;, \&quot;message\&quot;: \&quot;&apos;jq&apos; binary not found. Please install jq package before using this driver\&quot;}&quot;
    exit 1
fi

if [ &quot;$op&quot; = &quot;init&quot; ]; then
    log &apos;{&quot;status&quot;: &quot;Success&quot;, &quot;capabilities&quot;: {&quot;attach&quot;: false}}&apos;
    exit 0
fi

if [ $# -lt 2 ]; then
    usage
fi

shift

case &quot;$op&quot; in
    mount)
        domount $*
        ;;
    unmount)
        unmount $*
        ;;
    *)
        log &apos;{&quot;status&quot;: &quot;Not supported&quot;}&apos;
        exit 0
esac

exit 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实就是一段 shell 脚本，支持三个命令: init、mount、unmount。当我们在集群中为某个 pod 挂载 FlexVolume时，该 pod 所在节点的 kubelet 会调用其指定的插件脚本执行 mount 命令，然后挂载给 pod 使用。当然了，FlexVolume 还支持更复杂的插件。这个可以看官方的文档: &lt;a href=&quot;https://github.com/kubernetes/community/blob/master/contributors/devel/sig-storage/flexvolume.md&quot; rel=&quot;noopener&quot;&gt;flexvolume&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;部署方案&lt;/h2&gt;
&lt;p&gt;关于如何部署 FlexVolume 的插件，其实在例子中也有提到，这里可以总结一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;手动部署到每个节点的指定目录下，比如我们刚刚部署的 nfs ，其实际路径是: &lt;code&gt;/usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs&lt;/code&gt;。其中 &lt;code&gt;/usr/libexec/kubernetes/kubelet-plugins/volume/exec&lt;/code&gt; 是默认路径，也可以通过 kubelet 的参数 &lt;code&gt;--volume-plugin-dir&lt;/code&gt; 和 controller manager 的参数 &lt;code&gt;--flex-volume-plugin-dir&lt;/code&gt; 来指定。&lt;code&gt;k8s~nfs&lt;/code&gt; 这个路径中，&lt;code&gt;k8s&lt;/code&gt; 是供应商， &lt;code&gt;nfs&lt;/code&gt; 是驱动名称，在使用的时候可以这样指定: `driver: &quot;k8s/nfs&quot;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 kubernetes 的 deamonset 配合 hostPath 来部署，因为 daemonset 会在每个节点上都启动 pod，然后通过 hostPath 将插件放在指定的位置即可。kubernetes 集群中 master 节点可能被设置成不允许调度。这种情况下 daemonset 默认不调度到 master 节点上，可以使用 tolerations 来解决这个问题. 具体可参考: &lt;a href=&quot;https://stackoverflow.com/questions/48495263/scheduler-is-not-scheduling-pod-for-daemonset-in-master-node&quot; rel=&quot;noopener&quot;&gt;Scheduler is not scheduling Pod for DaemonSet in Master node&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其实除了 kubelet 要调用插件之外，controller-manager 也要调用。比如执行 &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;attach&lt;/code&gt;, &lt;code&gt;detach&lt;/code&gt;, &lt;code&gt;waitforattach&lt;/code&gt;, &lt;code&gt;isattached&lt;/code&gt; 等命令。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>k8s</category><category>k8s</category><category>flexvolume</category><author>joyme123</author></item><item><title>argo的输入输出源代码分析</title><link>https://www.myway5.com/blog/argo-input-output/</link><guid isPermaLink="true">https://www.myway5.com/blog/argo-input-output/</guid><description>argo是一个工作流的调度引擎，支持 Steps 和 DAG 这两种工作流。 Steps: 是按照步骤，从前往后的工作流调度方案。工作流中的每一步都只依赖上一步的结果 DAG: 全称是 directed acyclic graph，译为有向无环图。</description><pubDate>Fri, 10 Jan 2020 13:52:39 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;argo是一个工作流的调度引擎，支持 Steps 和 DAG 这两种工作流。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Steps: 是按照步骤，从前往后的工作流调度方案。工作流中的每一步都只依赖上一步的结果&lt;/li&gt;
&lt;li&gt;DAG: 全称是 directed acyclic graph，译为有向无环图。与 Steps 的区别在于每一步可能依赖之前的多步输出，但是不会循环依赖（也就是无环）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不论是在什么类型的工作流上，argo都抽象出了两种输入输出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;parameters: 通常情况下都是字符串，该字符串可以来源于标准输出，也可以来源于文件的内容&lt;/li&gt;
&lt;li&gt;artifacts: 可以理解成文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;输入输出是连接整个工作流的核心。每一步都可以看作是一次函数调用。那么在argo中，它是如何实现在多步之间输入输出的传输呢？下面会通过源代码进行分析。 在看代码之前，可以看一个 argo 的工作流中的一个pod，为了查看更方便，我删除一些不需要关注的字段:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl -n workflow describe pods custom-workflow-111-2fw2f-2639432629

Name:           custom-workflow-111-2fw2f-2639432629
Namespace:      workflow
Labels:         pipeline.starx.com/nodeID=743
                workflows.argoproj.io/completed=true
                workflows.argoproj.io/workflow=custom-workflow-111-2fw2f
Annotations:    cni.projectcalico.org/podIP: 10.42.0.83/32
                workflows.argoproj.io/node-name: custom-workflow-111-2fw2f.yolov3-evaluate-743
                workflows.argoproj.io/outputs:
                  {&quot;result&quot;:...
                workflows.argoproj.io/template:
                  {&quot;name&quot;:&quot;yolov3-evaluate-743&quot;,&quot;inputs&quot;:{&quot;parameters&quot;:[{&quot;name&quot;:&quot;userParam&quot;,&quot;value&quot;:&quot;eyJTY29yZVRocmVzaG9sZCI6MC41LCJJb3VfVGhyZXNob2xkIjowLjQ...
Controlled By:  Workflow/custom-workflow-111-2fw2f
Init Containers:
  init:
    Image:         argoproj/argoexec:v2.3.0
    Command:
      argoexec
      init
    Environment:
      ARGO_POD_NAME:  custom-workflow-111-2fw2f-2639432629 (v1:metadata.name)
    Mounts:
      /argo/inputs/artifacts from input-artifacts (rw)
      /argo/podmetadata from podmetadata (rw)
      /argo/staging from argo-staging (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-lfk5b (ro)
Containers:
  wait:
    Image:         argoproj/argoexec:v2.3.0
    Command:
      argoexec
      wait
    Environment:
      ARGO_POD_NAME:  custom-workflow-111-2fw2f-2639432629 (v1:metadata.name)
    Mounts:
      /argo/podmetadata from podmetadata (rw)
      /mainctrfs/argo/staging from argo-staging (rw)
      /mainctrfs/tmp/artifacts/artifact-input0 from input-artifacts (rw,path=&quot;artifact0&quot;)
      /mainctrfs/tmp/artifacts/artifact-input1 from input-artifacts (rw,path=&quot;artifact1&quot;)
      /var/run/docker.sock from docker-sock (ro)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-lfk5b (ro)
  main:
    Image:         registry.cn-shanghai.aliyuncs.com/xinhuodev/wt:0.4
    Command:
      sh
    Args:
      /argo/staging/script
    Mounts:
      /argo/staging from argo-staging (rw)
      /tmp/artifacts/artifact-input0 from input-artifacts (rw,path=&quot;artifact0&quot;)
      /tmp/artifacts/artifact-input1 from input-artifacts (rw,path=&quot;artifact1&quot;)
Volumes:
  podmetadata:
    Type:  DownwardAPI (a volume populated by information about the pod)
    Items:
      metadata.annotations -&amp;gt; annotations
  docker-sock:
    Type:          HostPath (bare host directory volume)
    Path:          /var/run/docker.sock
    HostPathType:  Socket
  input-artifacts:
    Type:       EmptyDir (a temporary directory that shares a pod&apos;s lifetime)
    Medium:     
    SizeLimit:  &amp;lt;unset&amp;gt;
  argo-staging:
    Type:       EmptyDir (a temporary directory that shares a pod&apos;s lifetime)
    Medium:     
    SizeLimit:  &amp;lt;unset&amp;gt;
  default-token-lfk5b:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-lfk5b
    Optional:    false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们需要关注的信息有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pod 的 Annotations&lt;/li&gt;
&lt;li&gt;Init Containers 启动的初始化容器&lt;/li&gt;
&lt;li&gt;Containers 中的 wait 容器和 main 容器&lt;/li&gt;
&lt;li&gt;Pod 的 Volumes 和每个容器的 Mounts&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Init 容器&lt;/h2&gt;
&lt;p&gt;argo 创建的 Pod 的初始化容器执行了 &lt;code&gt;argoexec init&lt;/code&gt; 命令，从名字上可以猜测出，这个容器负责初始化 Pod 中的环境，比如获取来上一步的输入等等，对应的代码是 &lt;code&gt;cmd/argoexec/commands/init.go&lt;/code&gt;， 我们的分析也从这里开始。在执行 &lt;code&gt;argo exec init&lt;/code&gt;之后，第一个调用的函数应该是&lt;code&gt;loadArtifacts()&lt;/code&gt;。这个方法中做了三件事: &lt;code&gt;initExecutor()&lt;/code&gt;、&lt;code&gt;wfExecutor.StageFiles()&lt;/code&gt;、&lt;code&gt;wfExecutor.LoadArtifacts()&lt;/code&gt; &lt;strong&gt;initExecutor&lt;/strong&gt;: initExecutor 的代码如下（删除了不重要的代码）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func initExecutor() *executor.WorkflowExecutor {
    tmpl, err := executor.LoadTemplate(podAnnotationsPath)

    var cre executor.ContainerRuntimeExecutor
    switch os.Getenv(common.EnvVarContainerRuntimeExecutor) {
    case common.ContainerRuntimeExecutorK8sAPI:
        cre, err = k8sapi.NewK8sAPIExecutor(clientset, config, podName, namespace)
    case common.ContainerRuntimeExecutorKubelet:
        cre, err = kubelet.NewKubeletExecutor()
    case common.ContainerRuntimeExecutorPNS:
        cre, err = pns.NewPNSExecutor(clientset, podName, namespace, tmpl.Outputs.HasOutputs())
    default:
        cre, err = docker.NewDockerExecutor()
    }

    wfExecutor := executor.NewExecutor(clientset, podName, namespace, podAnnotationsPath, cre, *tmpl)
    yamlBytes, _ := json.Marshal(&amp;amp;wfExecutor.Template)
    return &amp;amp;wfExecutor
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 &lt;code&gt;podAnnotationsPath&lt;/code&gt;加载模板，这个模板其实就是 Argo 中单步的执行模板，默认情况下它的值是 &lt;code&gt;/argo/podmetadata/annotations&lt;/code&gt;，这正好是 &lt;code&gt;init&lt;/code&gt; 容器的挂载，而这个挂载对应的卷是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; podmetadata:
    Type:  DownwardAPI (a volume populated by information about the pod)
    Items:
      metadata.annotations -&amp;gt; annotations
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;DownwardAPI&lt;/code&gt; 也解释一下，它是一种 volume 的类型，可以将 Pod 和 Container 的字段通过挂载文件的方式提供给容器内的进程方案。那么这里就是将 Pod 的 Annotations 字段通过上面的路径提供给 init 容器，init 容器根据其中的 template 获取该 Pod 的输入输出。 接下来判断根据容器运行时进行判断，这里我们只考虑 docker 作为容器运行时的情况。最后调用&lt;code&gt;NewExecutor&lt;/code&gt;实例化了一个 &lt;code&gt;wfExecutor&lt;/code&gt; &lt;strong&gt;StageFiles()&lt;/strong&gt; 源代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (we *WorkflowExecutor) StageFiles() error {
    var filePath string
    var body []byte
    switch we.Template.GetType() {
    case wfv1.TemplateTypeScript:
        log.Infof(&quot;Loading script source to %s&quot;, common.ExecutorScriptSourcePath)
        filePath = common.ExecutorScriptSourcePath
        body = []byte(we.Template.Script.Source)
    case wfv1.TemplateTypeResource:
        log.Infof(&quot;Loading manifest to %s&quot;, common.ExecutorResourceManifestPath)
        filePath = common.ExecutorResourceManifestPath
        body = []byte(we.Template.Resource.Manifest)
    default:
        return nil
    }
    err := ioutil.WriteFile(filePath, body, 0644)
    if err != nil {
        return errors.InternalWrapError(err)
    }
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;职责很简单，根据 template 的类型，写入到不同的文件中，比如 script 就写入到 &lt;code&gt;/argo/staging/script&lt;/code&gt;。这就是我们在 main 容器中执行的脚本了。 &lt;strong&gt;LoadArtifacts&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// LoadArtifacts loads artifacts from location to a container path
func (we *WorkflowExecutor) LoadArtifacts() error {
    for _, art := range we.Template.Inputs.Artifacts {
        artDriver, err := we.InitDriver(art)

        var artPath string
        mnt := common.FindOverlappingVolume(&amp;amp;we.Template, art.Path)
        if mnt == nil {
            artPath = path.Join(common.ExecutorArtifactBaseDir, art.Name)
        } else {
            // If we get here, it means the input artifact path overlaps with an user specified
            // volumeMount in the container. Because we also implement input artifacts as volume
            // mounts, we need to load the artifact into the user specified volume mount,
            // as opposed to the `input-artifacts` volume that is an implementation detail
            // unbeknownst to the user.
            log.Infof(&quot;Specified artifact path %s overlaps with volume mount at %s. Extracting to volume mount&quot;, art.Path, mnt.MountPath)
            artPath = path.Join(common.ExecutorMainFilesystemDir, art.Path)
        }

        // The artifact is downloaded to a temporary location, after which we determine if
        // the file is a tarball or not. If it is, it is first extracted then renamed to
        // the desired location. If not, it is simply renamed to the location.
        tempArtPath := artPath + &quot;.tmp&quot;
        err = artDriver.Load(&amp;amp;art, tempArtPath)
        if err != nil {
            return err
        }
        if isTarball(tempArtPath) {
            err = untar(tempArtPath, artPath)
            _ = os.Remove(tempArtPath)
        } else {
            err = os.Rename(tempArtPath, artPath)
        }

        if art.Mode != nil {
            err = os.Chmod(artPath, os.FileMode(*art.Mode))
        }
    }
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;InitDriver&lt;/code&gt;是初始化 Artifacts 的驱动。Argo 支持多种类型的存储系统，在 v2.3.0 这个版本支持: s3, http, git, artifactory, hdfs, raw。 &lt;code&gt;FindOverlappingVolume&lt;/code&gt; 是检查 artifacts 的路径和用户挂载的路径是否有重合。如果有，则返回深度最深的路径，如果没有，则返回 nil。如果返回 nil, 则使用 &lt;code&gt;/argo/inputs/artifacts&lt;/code&gt; 作为 artifacts 的基础路径。否则使用 &lt;code&gt;/mainctrfs&lt;/code&gt; 作为路径。 下面就是下载文件，解压文件并修改权限了。 注意在这里，init、wait和main容器都挂载了&lt;code&gt;input-artifacts&lt;/code&gt;和&lt;code&gt;argo-staging&lt;/code&gt;，并且 init 将输入和script放在了这两个卷中，所以其他几个卷都可以共享这些文件。&lt;/p&gt;
&lt;h2&gt;wait 容器&lt;/h2&gt;
&lt;p&gt;wait容器的职责有以下几点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;等待 main 容器结束&lt;/li&gt;
&lt;li&gt;杀死 sidecar&lt;/li&gt;
&lt;li&gt;保存日志&lt;/li&gt;
&lt;li&gt;保存 parameters&lt;/li&gt;
&lt;li&gt;保存 artifacts&lt;/li&gt;
&lt;li&gt;获取脚本的输出流&lt;/li&gt;
&lt;li&gt;将输出放在 Annotations 上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面我们看这些功能点的实现： &lt;strong&gt;等待 main 容器结束&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Wait is the sidecar container logic which waits for the main container to complete.
// Also monitors for updates in the pod annotations which may change (e.g. terminate)
// Upon completion, kills any sidecars after it finishes.
func (we *WorkflowExecutor) Wait() error {
    // WaitInit() 是初始化操作，只有 PSN 需要
    err := we.RuntimeExecutor.WaitInit()
    if err != nil {
        return err
    }
    log.Infof(&quot;Waiting on main container&quot;)
    // waitMainContainerStart的主要原理是周期轮询Pod中的所有容器，检查main容器的ContainerID字段
    // 不为空说明启动了
    mainContainerID, err := we.waitMainContainerStart()
    if err != nil {
        return err
    }
    log.Infof(&quot;main container started with container ID: %s&quot;, mainContainerID)
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // monitorAnnotations是因为pod的annotations会更改
    annotationUpdatesCh := we.monitorAnnotations(ctx)
    // 超时会杀死
    go we.monitorDeadline(ctx, annotationUpdatesCh)

    // 这里是直接用ContainerRuntime去等待容器结束的，比如docker,直接调用docker wait
    err = we.RuntimeExecutor.Wait(mainContainerID)
    if err != nil {
        return err
    }
    log.Infof(&quot;Main container completed&quot;)
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;杀死 sidecar&lt;/strong&gt; main 容器运行结束后，wait 容器会负责杀死其他容器（这个让我发现了之前用 sidecar 做 main 容器运行结束后的清理工作一直无效的原因)。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// KillSidecars kills any sidecars to the main container
func (we *WorkflowExecutor) KillSidecars() error {
    if len(we.Template.Sidecars) == 0 {
        log.Infof(&quot;No sidecars&quot;)
        return nil
    }
    log.Infof(&quot;Killing sidecars&quot;)
    pod, err := we.getPod()
    if err != nil {
        return err
    }
    sidecarIDs := make([]string, 0)
    // 遍历pod中的容器，排除main和wait,然后调用runtime来杀死容器
    for _, ctrStatus := range pod.Status.ContainerStatuses {
        if ctrStatus.Name == common.MainContainerName || ctrStatus.Name == common.WaitContainerName {
            continue
        }
        if ctrStatus.State.Terminated != nil {
            continue
        }
        containerID := containerID(ctrStatus.ContainerID)
        log.Infof(&quot;Killing sidecar %s (%s)&quot;, ctrStatus.Name, containerID)
        sidecarIDs = append(sidecarIDs, containerID)
    }
    if len(sidecarIDs) == 0 {
        return nil
    }
    return we.RuntimeExecutor.Kill(sidecarIDs)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;保存日志&lt;/strong&gt; argo 是支持将 main 容器中的日志持久化并保存到指定的地方的(s3, hdfs, Artifactory)。这在 argo 的文档上好像没有提到过。这一部分的逻辑比较简单，就是通过 ContainerRuntime 获取获取容器中的输出流，然后存成文件，通过 argo 中的 storage driver 保存下来。 &lt;strong&gt;保存 parameters&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// SaveParameters will save the content in the specified file path as output parameter value
func (we *WorkflowExecutor) SaveParameters() error {
    if len(we.Template.Outputs.Parameters) == 0 {
        log.Infof(&quot;No output parameters&quot;)
        return nil
    }
    log.Infof(&quot;Saving output parameters&quot;)
    mainCtrID, err := we.GetMainContainerID()
    if err != nil {
        return err
    }

    // 遍历模板参数
    for i, param := range we.Template.Outputs.Parameters {
        log.Infof(&quot;Saving path output parameter: %s&quot;, param.Name)
        // Determine the file path of where to find the parameter
        if param.ValueFrom == nil || param.ValueFrom.Path == &quot;&quot; {
            continue
        }

        var output string
        if we.isBaseImagePath(param.ValueFrom.Path) {
            log.Infof(&quot;Copying %s from base image layer&quot;, param.ValueFrom.Path)
            // 容器内，通过 runtime 获取
            output, err = we.RuntimeExecutor.GetFileContents(mainCtrID, param.ValueFrom.Path)
            if err != nil {
                return err
            }
        } else {
            log.Infof(&quot;Copying %s from from volume mount&quot;, param.ValueFrom.Path)
            mountedPath := filepath.Join(common.ExecutorMainFilesystemDir, param.ValueFrom.Path)
            // 容器的挂载卷，直接获取
            out, err := ioutil.ReadFile(mountedPath)
            if err != nil {
                return err
            }
            output = string(out)
        }

        outputLen := len(output)
        // Trims off a single newline for user convenience
        if outputLen &amp;gt; 0 &amp;amp;&amp;amp; output[outputLen-1] == &apos;\n&apos; {
            output = output[0 : outputLen-1]
        }
        // 保存下来
        we.Template.Outputs.Parameters[i].Value = &amp;amp;output
        log.Infof(&quot;Successfully saved output parameter: %s&quot;, param.Name)
    }
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;保存 artifacts&lt;/strong&gt; 保存 artifacts 和 保存 parameters 的操作是一样的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// SaveArtifacts uploads artifacts to the archive location
func (we *WorkflowExecutor) SaveArtifacts() error {
    if len(we.Template.Outputs.Artifacts) == 0 {
        log.Infof(&quot;No output artifacts&quot;)
        return nil
    }
    log.Infof(&quot;Saving output artifacts&quot;)
    mainCtrID, err := we.GetMainContainerID()
    if err != nil {
        return err
    }

    err = os.MkdirAll(tempOutArtDir, os.ModePerm)
    if err != nil {
        return errors.InternalWrapError(err)
    }

    for i, art := range we.Template.Outputs.Artifacts {
        err := we.saveArtifact(mainCtrID, &amp;amp;art)
        if err != nil {
            return err
        }
        we.Template.Outputs.Artifacts[i] = art
    }
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;获取脚本的输出流&lt;/strong&gt; 直接调用 runtime 去获取 main 容器的输出流，然后保存到 template.outputs 中&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (we *WorkflowExecutor) CaptureScriptResult() error {
    if we.Template.Script == nil {
        return nil
    }
    log.Infof(&quot;Capturing script output&quot;)
    mainContainerID, err := we.GetMainContainerID()
    if err != nil {
        return err
    }
    reader, err := we.RuntimeExecutor.GetOutputStream(mainContainerID, false)
    if err != nil {
        return err
    }
    defer func() { _ = reader.Close() }()
    bytes, err := ioutil.ReadAll(reader)
    if err != nil {
        return errors.InternalWrapError(err)
    }
    out := string(bytes)
    // Trims off a single newline for user convenience
    outputLen := len(out)
    if outputLen &amp;gt; 0 &amp;amp;&amp;amp; out[outputLen-1] == &apos;\n&apos; {
        out = out[0 : outputLen-1]
    }
    we.Template.Outputs.Result = &amp;amp;out
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;将输出放在 Annotations 上&lt;/strong&gt; 将 outputs 存在 pod 的 annotations 上。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (we *WorkflowExecutor) AnnotateOutputs(logArt *wfv1.Artifact) error {
    outputs := we.Template.Outputs.DeepCopy()
    if logArt != nil {
        outputs.Artifacts = append(outputs.Artifacts, *logArt)
    }

    if !outputs.HasOutputs() {
        return nil
    }
    log.Infof(&quot;Annotating pod with output&quot;)
    outputBytes, err := json.Marshal(outputs)
    if err != nil {
        return errors.InternalWrapError(err)
    }
    return we.AddAnnotation(common.AnnotationKeyOutputs, string(outputBytes))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;init 容器做了 pod 的初始化，包括存储 script，下载 artifacts等等，这样我们的 main 容器就不用关心输入的来源，只需要在指定地方使用即可。wait 容器负责监控 main 容器的生命周期，在 main 容器中的主要逻辑运行结束之后，负责将输出部分读取，持久化，这样 main 容器就不用操心如何将该步产生的结果传到后面的步骤上的问题。&lt;/p&gt;
</content:encoded><category>k8s</category><category>argo</category><category>workflow</category><category>k8s</category><author>joyme123</author></item><item><title>VXLAN网络基础</title><link>https://www.myway5.com/blog/vxlan/</link><guid isPermaLink="true">https://www.myway5.com/blog/vxlan/</guid><description>VXLAN 全称 Virtual eXtensible Local Area Network, 是一种基于三层网络构建虚拟的二层网络的方案。它使用 UDP 封装二层的数据帧，实现了 overlay 网络。所有处于 overlay 网络中的设备均感觉不到底层和传统网络的差别。</description><pubDate>Thu, 02 Jan 2020 15:58:20 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;VXLAN 全称 Virtual eXtensible Local Area Network, 是一种基于三层网络构建虚拟的二层网络的方案。它使用 UDP 封装二层的数据帧，实现了 overlay 网络。所有处于 overlay 网络中的设备均感觉不到底层和传统网络的差别。&lt;/p&gt;
&lt;h2&gt;相关知识点&lt;/h2&gt;
&lt;h3&gt;OSI七层网络模型&lt;/h3&gt;
&lt;p&gt;OSI 的七层网络模型从下到上依次是: 物理层，数据链路层，网络层，传输层、会话层、表示层、应用层 我们在简介中提到的二层、三层都是这七层中的，二层是数据链路层，它主要抽象了根据mac地址来传输数据帧这一过程。三层是网络层，典型的是ipv4, ipv6这样的网络协议，根据 ip 地址来传输 ip 数据报。 注意虽然 VXLAN 是基于 UDP 封装了数据帧，但是我们一般说它是基于三层而不是四层。因为在这里我们关注的是数据的是如何传输到指定地址的，而不是如何封装的。&lt;/p&gt;
&lt;h3&gt;overlay 的含义&lt;/h3&gt;
&lt;p&gt;overlay 字面含义就是上层的，还有一个对应的词，也就是underlay。结合在一起就好理解了， VXLAN 是 overlay 网络，说的是它实现的二层（数据链路层）是 overlay 的，这二层是基于三层（网络层）的 underlay 网络。&lt;/p&gt;
&lt;h3&gt;单播和多播&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;下面的定义来源于维基百科&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单播: 英文 unicast, 是指数据包在计算机网络的传输中，目的地址为单一目标的一种传输方式。它是现今网络应用最为广泛，通常所使用的网络协议或服务大多采用单播传输，例如一切基于TCP的协议。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多播（组播）: 英文 multicast，是指把信息同时传递给一组目的地址。它使用的策略是最高效的，因为消息在每条网络链路上只需传递一次，且只有在链路分叉的时候，消息才会被复制。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据帧&lt;/h3&gt;
&lt;p&gt;以常见的 EthernetII 帧为例，其帧格式如下： &lt;img src=&quot;/uploads/wp/2019/12/ethernetII_frame.png&quot; alt=&quot;ethernetII frame&quot; /&gt; D.MAC: 6byte，目标 MAC 地址 S.MAC: 6byte, 来源 MAC 地址 Type: 2byte, 0x0800是 IP 类型，0x0806 是 ARP 类型 Data: 数据 FCS: 为了进行差错检验而添加的冗余码。 以下是我用 wireshark 抓的 arp 帧: &lt;img src=&quot;/uploads/wp/2019/12/arp_in_wireshark.png&quot; alt=&quot;arp in wireshark&quot; /&gt; 我在笔记本(192.168.31.243)上 ping 了 &lt;code&gt;192.168.31.133&lt;/code&gt; 这个地址，因为我的笔记本不知道 192.168.31.133 的mac地址，因此使用 arp 帧来查找目的mac地址。&lt;/p&gt;
&lt;h3&gt;VLAN&lt;/h3&gt;
&lt;p&gt;VLAN(Virtual Local Area Network) 和本文介绍的 VXLAN 从名称上看就很相似，中文名称叫做虚拟局域网，它们的作用也是一样的，可以用来划分子网。下面采用维基百科上关于&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E8%99%9A%E6%8B%9F%E5%B1%80%E5%9F%9F%E7%BD%91&quot; rel=&quot;noopener&quot;&gt;虚拟局域网&lt;/a&gt;的介绍。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;虚拟区域网络（Virtual Local Area Network或简写VLAN, V-LAN）是一种建构于局域网交换技术（LAN Switch）的网络管理的技术，网管人员可以借此透过控制交换机有效分派出入局域网的报文到正确的出入端口，达到对不同实体局域网中的设备进行逻辑分群（Grouping）管理，并降低局域网内大量数据流通时，因无用报文过多导致壅塞的问题，以及提升局域网的信息安全保障。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但是 VLAN 是基于二层的方案，它会在数据帧头部添加4个字节的 VLAN Tag，其中 12bit 用来标识不同的二层网络，这样总共是 4000 多个。其次 VLAN 会使用 MAC 地址表来记录 VLAN ID、 MAC 和 Port 这三者之间的关系，因此一旦网络中主机数量多起来，会导致 MAC 地址表占用很大的内存。 关于 VLAN 和 VXLAN的区别，可以参考这篇文章: &lt;a href=&quot;https://zhuanlan.zhihu.com/p/36165475&quot; rel=&quot;noopener&quot;&gt;VXLAN vs VLAN&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;VXLAN 协议&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2020/01/vxlan-protocol.jpg&quot; alt=&quot;vxlan protocol&quot; /&gt; 上图从整体上来看，是一个 UDP 的报文，在 UDP 的数据部分的前8位是 VXLAN Header，表明这个 UDP 封装的是 VXLAN 的数据帧，后面则是原始的2层数据帧了。在 VXLAN Header中，有下面几个字段:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VXLAN RRRR1RRR: VXLAN 的标记位&lt;/li&gt;
&lt;li&gt;Reserved: 保留位&lt;/li&gt;
&lt;li&gt;VNID: 24位的 VNI 字段&lt;/li&gt;
&lt;li&gt;Reserved: 保留字段&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;VXLAN 的实现原理&lt;/h2&gt;
&lt;p&gt;VXLAN 将以太网数据帧封装在 UDP 内，进而在三层网络传输。VXLAN 数据的封装和解封发生在 VTEP(VXLAN Tunnel EndPoint)。VTEP 是 VXLAN 网络的边缘设备。同时每个 VXLAN 网络都有唯一的 VNI(VXLAN Network Identifier) 标识，这样在一个物理网络上可以构建多个 VXLAN 虚拟网络，满足多租户的要求。下图是 VXLAN 的网络架构示意图。 &lt;img src=&quot;/uploads/wp/2020/01/vxlan.png&quot; alt=&quot;vxlan&quot; /&gt; 这里面有两个比较重要的概念:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;VTEP: VTEP 和传统交换机类似，也是基于 MAC 地址表工作，是 VXLAN 网络的边缘设备，用来对 VXLAN 报文封包和解包。VTEP 可以是网络设备（比如交换机），也可以是一台机器（比如虚拟化集群中的宿主机）。在 VTEP 中，可以认为有两个表: 一个是 VLAN 和 VXLAN 的对应关系表；另一个是 MAC 地址表，里面包含了很多 MAC 地址，VXLAN ID 和远端 VTEP IP 地址的对应关系。 VTEP 收到下面主机的网络数据帧时，会先根据 VLAN 查第一个表获取对应的 VXLAN ID，之后根据 VXLAN ID和目的 MAC 地址，查 MAC 地址表获取远端 VTEP 的 IP 地址。最后， VTEP 会剥离VLAN Tag，按照 VXLAN 格式封装数据帧，发往远端的 VTEP。远端的 VTEP 收到该数据后进行解包，根据 MAC 地址将数据帧发往其所连接的主机。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;VNI: VNI 是每个VXLAN的标识，也就是上面说的 VXLAN ID，共24位，那么就可以表示 2^24=16777216 个 VXLAN 网络。每个 VXLAN ID 对应一个租户，那么理论上可以支撑千万级别的租户。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2020/01/vxlan-vtep.jpg&quot; alt=&quot;vxlan-vtep&quot; /&gt; &lt;em&gt;图例: VXLAN VTEP&lt;/em&gt; 这里又引出另外一个问题， VXLAN 中的一台主机在只知道 ip 的情况下，如何获取对方的 MAC 地址。在传统网络中，ARP 请求是用来解决这个问题的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VXLAN 网络中，主机发出的 ARP 请求会被 VTEP(1) 接收到，VTEP(1) 发现虚拟机目的 MAC 为广播地址，封装上 VXLAN 协议头部之后，发送给多播组，支持多播的底层网络设备会把报文发送给组内的所有成员&lt;/li&gt;
&lt;li&gt;VTEP(2) 接收到 VXLAN 封装的 ARP 请求，去掉 VXLAN 头部，并通过报文学习到发送方 &amp;lt;虚拟机MAC-VNI-VTEP IP&amp;gt;这个对应关系，并把原来的 ARP 报文广播给主机。&lt;/li&gt;
&lt;li&gt;主机接受到 ARP 请求报文，如果 ARP 报文请求的是自己的 MAC 地址，就返回 ARP 应答&lt;/li&gt;
&lt;li&gt;VTEP(2) 此时已经知道发送方的虚拟机 MAC 和 VTEP 信息，把 ARP 应答添加上 VXLAN 头部之后通过单播发送出去&lt;/li&gt;
&lt;li&gt;VTEP(1)接收到报文，并学习到报文中的的对应关系，记录下来。然后 VTEP 进行解包，知道内部的 IP 和 MAC 地址，并转发给虚拟机。&lt;/li&gt;
&lt;li&gt;虚拟机拿到 ARP 应答报文，就知道了对方 IP 对应的 MAC 地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这次多播之后，两台虚拟机之间的通信就可以通过单播了。VTEP 在这中间担任了一个代理的角色，使得虚拟机之间可以透明的进行网络通信。这和 nginx 担任反向代理的角色有点类似。同时我们可以发现，在一个大规模的 VXLAN 网络中，多播会是一件很消耗性能的事。&lt;/p&gt;
&lt;h2&gt;资料地址&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/36165475&quot; rel=&quot;noopener&quot;&gt;VXLAN vs VLAN&lt;/a&gt; &lt;a href=&quot;https://zhuanlan.zhihu.com/p/37171463&quot; rel=&quot;noopener&quot;&gt;VXLAN in OpenStack Neutron&lt;/a&gt; &lt;a href=&quot;https://cizixs.com/2017/09/25/vxlan-protocol-introduction/&quot; rel=&quot;noopener&quot;&gt;VXLAN 协议原理简介&lt;/a&gt; &lt;a href=&quot;https://cizixs.com/2017/09/28/linux-vxlan/&quot; rel=&quot;noopener&quot;&gt;linux 上实现vxlan网络&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>网络</category><author>joyme123</author></item><item><title>linux ip 命令的使用</title><link>https://www.myway5.com/blog/linux-ip-command/</link><guid isPermaLink="true">https://www.myway5.com/blog/linux-ip-command/</guid><description>linux 下的 ip 命令是一个很强大的工具，在这之前，我通常只会使用 ifconfig 命令来查看本机网络接口和 ip 地址等等。或者 netstat 命令查看端口占用等等。</description><pubDate>Sun, 29 Dec 2019 08:26:23 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;linux 下的 &lt;code&gt;ip&lt;/code&gt; 命令是一个很强大的工具，在这之前，我通常只会使用 &lt;code&gt;ifconfig&lt;/code&gt; 命令来查看本机网络接口和 ip 地址等等。或者 &lt;code&gt;netstat&lt;/code&gt; 命令查看端口占用等等。&lt;code&gt;ip&lt;/code&gt; 命令属于 &lt;code&gt;iproute2&lt;/code&gt; 套件中的一个命令，关于 &lt;code&gt;iproute2&lt;/code&gt; 和 linux &lt;code&gt;net-tools&lt;/code&gt; 中的命令对比如下（图片来源:&lt;a href=&quot;https://linux.cn/article-3144-1.html&quot; rel=&quot;noopener&quot;&gt;https://linux.cn/article-3144-1.html&lt;/a&gt;)： &lt;img src=&quot;/uploads/wp/2019/12/nettools_vs_iproute2.png&quot; alt=&quot;net-tools vs iproute2&quot; /&gt; 可以看出，除了部分 &lt;code&gt;netstat&lt;/code&gt; 命令用 &lt;code&gt;ss&lt;/code&gt; 来替代，其它都可以用 &lt;code&gt;ip&lt;/code&gt; 命令替代。并且，&lt;code&gt;iproute2&lt;/code&gt; 已经是大多数 linux 发行版默认安装了，而 &lt;code&gt;net-tools&lt;/code&gt; 则需要另外安装。 &lt;code&gt;ip&lt;/code&gt; 命令可以分为下面几个模块:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;网卡设备相关: &lt;code&gt;ip link&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;网卡地址相关: &lt;code&gt;ip addr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;路由表相关: &lt;code&gt;ip route&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;arp 相关: &lt;code&gt;ip neigh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面会列出一些常用的操作，最好在虚拟机中操作，防止影响个人机器。&lt;/p&gt;
&lt;h2&gt;ip link&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;查看 ip link 的帮助&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ip link help
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;查看网络接口&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ip link list

1: lo: &amp;lt;LOOPBACK,UP,LOWER_UP&amp;gt; mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:8a:fe:e6 brd ff:ff:ff:ff:ff:ff
3: eth1: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:15:ee:5c brd ff:ff:ff:ff:ff:ff
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里显示了三个网络接口，&lt;code&gt;lo&lt;/code&gt;代表的本机的回环网卡，&lt;code&gt;eth0&lt;/code&gt; 和 &lt;code&gt;eth1&lt;/code&gt; 分别是两个网卡 &lt;strong&gt;添加网络接口&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip link add link eth0 mydev type bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里添加了一个网桥，连接在 eth0 上。使用 &lt;code&gt;ip link list&lt;/code&gt; 查看可以发现多了下面一个设备&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;6: mydev: &amp;lt;BROADCAST,MULTICAST&amp;gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 5e:0c:36:7b:ce:0d brd ff:ff:ff:ff:ff:ff

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;删除网络接口&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip link delete link dev mydev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关闭网络接口&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip link set eth1 down
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;打开网络接口&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip link set eht1 up
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ip addr&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;查看帮助&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ip addr help
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;查看网络地址&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ip addr list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;查看某一个网络接口的地址&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ip addr show eth1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;添加 ip 地址&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip addr add 192.168.31.131/24 dev eth1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看 eth1 的地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ip addr show eth1

3: eth1: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:15:ee:5c brd ff:ff:ff:ff:ff:ff
    inet 192.168.31.77/24 brd 192.168.31.255 scope global noprefixroute dynamic eth1
       valid_lft 42769sec preferred_lft 42769sec
    inet 192.168.31.131/24 scope global secondary eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe15:ee5c/64 scope link 
       valid_lft forever preferred_lft forever
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们也可以 ping 一下这个地址:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ping 192.168.31.131

PING 192.168.31.131 (192.168.31.131) 56(84) bytes of data.
64 bytes from 192.168.31.131: icmp_seq=1 ttl=64 time=0.109 ms
64 bytes from 192.168.31.131: icmp_seq=2 ttl=64 time=0.155 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;删除 ip 地址&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip addr del 192.168.31.131/24 dev eth1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;改变设备地址的配置&lt;/strong&gt; 这里有一篇很好的文章: &lt;a href=&quot;https://serverfault.com/questions/476926/understanding-ip-addr-change-and-ip-addr-replace-commands&quot; rel=&quot;noopener&quot;&gt;understanding ip addr change and ip addr replace commands&lt;/a&gt; 为了演示的方便，我添加了一个网卡设备&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip link add link eth0 name dummy0 type dummy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为它分配地址:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip addr add 192.168.31.132/24 dummy0
$ ip addr show dummy0

5: dummy0: &amp;lt;BROADCAST,NOARP&amp;gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 9e:dc:6e:0b:70:99 brd ff:ff:ff:ff:ff:ff
    inet 192.168.31.132/24 scope global dummy0
       valid_lft forever preferred_lft forever
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想要修改 &lt;code&gt;valid_lft&lt;/code&gt; 和 &lt;code&gt;preferred_lft&lt;/code&gt; 配置，可以使用 &lt;code&gt;ip change&lt;/code&gt;命令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip addr change 192.168.31.132 dev dummy0 preferred_lft 300 valid_lft 300
$ ip addr show dummpy0

5: dummy0: &amp;lt;BROADCAST,NOARP&amp;gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 9e:dc:6e:0b:70:99 brd ff:ff:ff:ff:ff:ff
    inet 192.168.31.132/24 scope global dynamic dummy0
       valid_lft 299sec preferred_lft 299sec
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ip route&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;查看帮助&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ip route help
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;查看路由&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ip route list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;添加路由&lt;/strong&gt; 添加一条普通的路由&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip route add 39.156.0.0/16 via 192.168.31.133 dev dummy0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加默认路由&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip route add default via 192.168.31.133 dev dummy0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;删除路由&lt;/strong&gt; 删除默认路由&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip route del default via 192.168.31.133 dev dummy0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除普通路由&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo ip route del 39.156.0.0/16 via 192.168.31.133 dev dummy0 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**查看一个 ip 地址的路由包来源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ip route get 39.156.69.79

39.156.69.79 via 10.0.2.2 dev eth0 src 10.0.2.15 
    cache 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ip neigh&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;查看帮助&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ip neigh help
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;查看同一个网络的邻居设备&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ip neigh show

192.168.31.1 dev eth1 lladdr 34:ce:00:2e:88:b9 STALE
10.0.2.2 dev eth0 lladdr 52:54:00:12:35:02 REACHABLE
10.0.2.3 dev eth0 lladdr 52:54:00:12:35:03 STALE
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>linux</category><category>网络</category><author>joyme123</author></item><item><title>Kubernetes Pod 解析</title><link>https://www.myway5.com/blog/kubernetes-pod/</link><guid isPermaLink="true">https://www.myway5.com/blog/kubernetes-pod/</guid><description>在 Kubernetes 中， Pod 是一个非常重要的概念，它由一个或多个容器组成，这些容器共享存储、网络、进程空间，以及可以使用进程间通信。 Pod 是集群中最小的调度单位，如果把 Kubernetes 集群比作操作系统，那么 Pod 则是一个进程。</description><pubDate>Thu, 12 Dec 2019 06:18:12 GMT</pubDate><content:encoded>&lt;h2&gt;pod 基础概念&lt;/h2&gt;
&lt;p&gt;在 Kubernetes 中， Pod 是一个非常重要的概念，它由一个或多个容器组成，这些容器共享存储、网络、进程空间，以及可以使用进程间通信。 Pod 是集群中最小的调度单位，如果把 Kubernetes 集群比作操作系统，那么 Pod 则是一个进程。一个 Pod 被创建出来之后，它会被调度到集群中的某一个节点上开始运行，Pod 中的 Container 都会在该节点上启动。 Pod 是短暂的，就跟进程一样，在被创建之后可能会随时被终止。但是 Kubernetes 会根据需求来的及时的重新创建一个 Pod，所以单独从 Pod 的层面来说，它应该是一个无状态的应用。 集群中的每个 Pod 都会有唯一的 ID (UID)，这跟进程的进程号是唯一的一样。&lt;/p&gt;
&lt;h2&gt;共享命名空间&lt;/h2&gt;
&lt;p&gt;这里以 docker 在 linux 下的实现为例，docker 主要使用了 linux namespace 做的资源隔离。在 pod 中，所有的 docker 容器都可以共享同一个 network, ipc, pid命名空间，并且可以通过挂载同一个卷的方式来共享文件系统。需要注意的是，默认情况下只有 network 这个命名空间是开启的，其他的需要通过 &lt;code&gt;shareProcessNamespace&lt;/code&gt;、 &lt;code&gt;SYS_PTRACE&lt;/code&gt; 和 &lt;code&gt;emptyDir&lt;/code&gt; 等字段来开启。 为了说明，可以在 kubernetes 集群中创建下面这个 pod&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  shareProcessNamespace: true
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
      - mountPath: /cache
        name: cache-volume
  - name: shell
    image: busybox
    volumeMounts:
      - mountPath: /cache
        name: cache-volume
    securityContext:
      capabilities:
        add:
        - SYS_PTRACE
    stdin: true
    tty: true
  volumes:
  - name: cache-volume
    emptyDir: {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面创建的 Pod 有两个容器，一个是 nginx，另一个是 shell。我们使用以下命令进入到 shell 容器中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl exec -it nginx -c shell sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;network&lt;/h3&gt;
&lt;p&gt;为了验证同一个 Pod 下 network 是共享的，可以使用以下命令验证&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wget localhost
Connecting to localhost (127.0.0.1:80)
saving to &apos;index.html&apos;
index.html           100% |******************************************|   612  0:00:00 ETA
&apos;index.html&apos; saved
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很明显，这里的 localhost 指向了 nginx 容器。&lt;/p&gt;
&lt;h3&gt;pid&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$ ps -el
PID   USER     TIME  COMMAND
    1 root      0:00 /pause
    6 root      0:00 nginx: master process nginx -g daemon off;
   11 101       0:00 nginx: worker process
   12 root      0:00 sh
   20 root      0:00 sh
   26 root      0:00 ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 shell 容器中查看进程可以看到 /pause 和 nginx 等进程。因为共享了 pid 命名空间，所以可以看到其他容器的进程。这里的 pause 是一个很特殊的进程，在后文章会单独解释。&lt;/p&gt;
&lt;h3&gt;ipc&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$ kill -9 11
$ ps -el
PID   USER     TIME  COMMAND
    1 root      0:00 /pause
    6 root      0:00 nginx: master process nginx -g daemon off;
   12 root      0:00 sh
   20 root      0:00 sh
   29 101       0:00 nginx: worker process
   30 root      0:00 ps -el
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着上面的命令，我们杀死了 nginx 的 worker 进程，nginx master 进程又重启了 worker，重启后的 worker PID 是 29。可以在 shell 容器中使用信号杀死 nginx 中的进程，说明 IPC 命名空间是共享的。&lt;/p&gt;
&lt;h3&gt;shared volume&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$ cd cache
$ touch test
$ kubectl exec -it nginx -c nginx sh
$ ls /cache
test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在 shell 容器中 cache 文件夹下创建了文件 test, 在 nginx 容器中也能看到，说明两个容器可以共享文件系统的某些目录。&lt;/p&gt;
&lt;h2&gt;容器探针&lt;/h2&gt;
&lt;p&gt;之所以特地提到容器探针是因为容器探针是一个非常好的检查服务是否正确运行的方式。 TODO: 几种探针的使用场景和 探针是由 kubelet 周期性对容器执行的诊断措施。kubernetes 提供了三种方式:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ExecAction: 在容器中执行命令，如果命令的 exit code 是 0 则代表成功。&lt;/li&gt;
&lt;li&gt;TCPSocketAction: 在容器的ip和端口上执行 tcp 连接检查，如果端口是打开的则表明诊断成功。&lt;/li&gt;
&lt;li&gt;HTTPGetAction: 在容器的ip和制定端口和路径上执行，如果返回的状态码大于等于 200 ,小于400就表明成功。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;直到 kubernetes v1.16 止，共有三种探针可以使用，分别是 livenessProbe, readinessProbe, startupProbe.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;livenessProbe&lt;/code&gt;: 检查容器是否在运行，如果 liveness 探针失败，kubelet 会杀死这个容器，这个容器会遵循它的重启策略。如果一个容器没有提供 liveness 探针，默认状态是 &lt;code&gt;Success&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;readinessProbe&lt;/code&gt;: 表明容器是否准备好接收请求了。如果 readiness 探针失败，endpoints 控制器会从符合这个 Pod 的所有 service 的 endpoint 列表中移除该 Pod。默认状态是 &lt;code&gt;Failure&lt;/code&gt;。如果容器没有提供 readiness 探针，默认状态就是 &lt;code&gt;Success&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;startupProbe&lt;/code&gt;: 表明容器中的应用是否启动完成。如果提供了 startup 探针，其他的探针都被禁用直到 startup 探针成功。如果 startup 探针失败，kuberlet 杀死容器，容器会遵循它的重启策略。是否容器没有提供 startup 探针，默认状态是 &lt;code&gt;Success&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;init container&lt;/h2&gt;
&lt;p&gt;我们都知道 Pod 可以有多个容器，其中 init 容器是比较特殊的一个，它由 spec.initContainers 指定，与普通容器不同的是，只有在 init 容器运行完成之后，Kubernetes 才会初始化 Pod 和运行应用容器。 实际应用中，init 容器的职责基本上都是和它名字描述的一样，用来做初始化用。比如在 argo 这个工作流调度应用中，它会为每个调度的 Pod 初始化一个 init 容器，用来载入该步骤需要使用的文件资源等等。&lt;/p&gt;
&lt;h2&gt;pause container&lt;/h2&gt;
&lt;p&gt;在上面提到了 pid namespace 共享中，有一个 PID 为 1 的 Pause 进程，这就是现在提到的 pause container，pause container 对 kubernetes 用户是不感知的。但是我们在 kubernetes 节点上使用 &lt;code&gt;docker ps&lt;/code&gt;来查看，会发现很多的 pause 容器。pause 容器的作用主要有两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 pod 中作为 linux namespace 共享的基础容器&lt;/li&gt;
&lt;li&gt;在 PID namespace 共享的前提下，作为每个 pod 中的PID 1，然后回收僵尸进程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了研究pause的作用，可以在电脑上执行以下的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --ipc=shareable --name pause -p 8080:80 warrior/pause-amd64:3.0

docker run -d --name nginx -v /home/jiang/projects/testk8s/nginx.conf:/etc/nginx/nginx.conf --net=container:pause --ipc=container:pause --pid=container:pause nginx

docker run -d --name ghost --net=container:pause --ipc=container:pause --pid=container:pause ghost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nginx.conf 如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error_log stderr;
events {worker_connections 1024;}
http {
    access_log /dev/stdout combined;
    server {
        listen 80 default_server;
        server_name example.com;
        location / {
            proxy_pass http://127.0.0.1:2368;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们首先启动了一个 pause 容器，并且开始了 ipc 的共享。然后又启动了 nginx 和 ghost 容器，并且这两个容器都加入了 pause 的network、ipc和pid命名空间。 在浏览器中打开地址: &lt;code&gt;http://localhost:8080&lt;/code&gt;, 发现打开了 ghost 博客网页。我们在容器 pause 中开启的 8080 端口，然后经过 nginx 容器代理到了 ghost 容器。我们的应用容器 pause 容器完成了命名空间的共享。 我们再来看一下 pause 的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

#include &amp;lt;signal.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;sys/wait.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

static void sigdown(int signo) {
  psignal(signo, &quot;Shutting down, got signal&quot;);
  exit(0);
}

static void sigreap(int signo) {
  while (waitpid(-1, NULL, WNOHANG) &amp;gt; 0);
}

int main() {
  if (getpid() != 1)
    /* Not an error because pause sees use outside of infra containers. */
    fprintf(stderr, &quot;Warning: pause should be the first process\n&quot;);

  if (sigaction(SIGINT, &amp;amp;(struct sigaction){.sa_handler = sigdown}, NULL) &amp;lt; 0)
    return 1;
  if (sigaction(SIGTERM, &amp;amp;(struct sigaction){.sa_handler = sigdown}, NULL) &amp;lt; 0)
    return 2;
  if (sigaction(SIGCHLD, &amp;amp;(struct sigaction){.sa_handler = sigreap,
                                             .sa_flags = SA_NOCLDSTOP},
                NULL) &amp;lt; 0)
    return 3;

  for (;;)
    pause();
  fprintf(stderr, &quot;Error: infinite loop terminated\n&quot;);
  return 42;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码在监听了三个信号量，在 &lt;code&gt;SIGINT&lt;/code&gt; 和　&lt;code&gt;SIGTERM&lt;/code&gt;　时调用 &lt;code&gt;sigdown()&lt;/code&gt; 来退出。在接收到　&lt;code&gt;SIGCHLD&lt;/code&gt; 信号时使用 &lt;code&gt;waitpid&lt;/code&gt;，因为 pause 进程的 PID 是１，所以所有的僵尸进程都会被挂到 pause 进程之下，因此 waitpid 可以回收僵尸进程。&lt;/p&gt;
&lt;h2&gt;multi containers design pattern&lt;/h2&gt;
&lt;p&gt;在大多数情况下，Pod 往往只有一个容器，因为一个 Pod 的职责是唯一的。但是同样的，也有一些值得借鉴的多容器设计模式。 常用的模式有三种: sidecar, adapter, ambassador, 下图是常见的三种设计模式图，图片来源于网络: &lt;img src=&quot;/uploads/wp/2019/12/multi-container-pod-design.png&quot; alt=&quot;multi container pod design&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;sidecar 模式&lt;/h3&gt;
&lt;p&gt;在 sidecar 模式中，通常有一个主要的容器A--比如我们的 web 应用，然后有另外一个重要的容器B，负责处理 A 容器的一些功能，但是 B 容器又不是必须的。这个 B 容器我们通常称它为 sidecar 容器。 常见的 sidecar 容器有 日志，同步服务，监控等职责。当应用容器不在运行时，日志容器的运行是没有意义的，所以我们通常会创建一个 Pod 包含主要的容器和一个 sidecar 容器，来协同工作。这样的好处就是减少应用容器的功能需求，将通用的功能交给 sidecar 容器去执行，而又不会侵入应用容器。&lt;/p&gt;
&lt;h3&gt;adapter 模式&lt;/h3&gt;
&lt;p&gt;adapter 模式就是程序设计中常用的适配器模式，负责将应用容器中一些不兼容的功能调整成兼容的格式。比如一个大型系统中有很多小的系统，每个系统输出的日志格式都不同。而我们的统一监控系统只接受一种日志格式。这时候就可以使用 adapter 模式在 Pod 的加入一个负责适配的容器，将各种格式的日志调整成相同的统一发送给日志系统。&lt;/p&gt;
&lt;h3&gt;ambassador 模式&lt;/h3&gt;
&lt;p&gt;ambassador 模式常用来将应用容器连接到容器之外的网络。比如数据库，我们的应用容器只负责连接 localhost 的地址，然后由 ambassador 容器判断当前的环境，将应用容器的数据库请求代理到不同的数据库上。这样，我们在开发环境，测试环境，生产环境都只需要一套配置。&lt;/p&gt;
&lt;h2&gt;pod lifecycle&lt;/h2&gt;
&lt;p&gt;pod 的状态包含一个 phase 属性，这个属性用来描述当前 pod 的状态，可能的值有:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pending: Pod 已经被 Kubernetes 系统接受，但是有一个或多个容器镜像尚未创建。等待时间包括调度 Pod 的时间和通过网络下载镜像的时间。&lt;/li&gt;
&lt;li&gt;Running: Pod 已经绑定到一个节点上，Pod 中所有的容器都已经被创建，至少有一个容器正在运行，或者正处于启动或重启状态。&lt;/li&gt;
&lt;li&gt;Succeeded: Pod中的所有容器都被成功终止，并且不会再重启。&lt;/li&gt;
&lt;li&gt;Failed: Pod中所有容器都已经终止，并且至少有一个容器是因为失败终止。也就是说，容器以非0状态退出或被系统终止。&lt;/li&gt;
&lt;li&gt;Unknown: 因为某些原因无法取得 Pod 的状态，通过是因为与 Pod 所在的主机通信失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下图是一个 Pod 的生命周期状态，图片来源于网络： &lt;img src=&quot;/uploads/wp/2019/12/kubernetes-pod-life-cycle.jpg&quot; alt=&quot;kubernetes-pod-life-cycle&quot; /&gt; 在 Pod 的整个生命周期中，我们可以通过容器的生命周期钩子来在某些阶段处理一些工作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;PostStart: 当容器被创建的时候，这个钩子会立刻执行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PreStop: 当容器退出时执行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在两种钩子触发时我们可以选择调用脚本执行还是发送HTTP请求。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/pods/pod/&quot; rel=&quot;noopener&quot;&gt;Pods-Kubernetes&lt;/a&gt; &lt;a href=&quot;https://jimmysong.io/kubernetes-handbook/concepts/pod-state-and-lifecycle.html&quot; rel=&quot;noopener&quot;&gt;Pod状态与生命周期管理&lt;/a&gt; &lt;a href=&quot;https://www.ianlewis.org/en/almighty-pause-container&quot; rel=&quot;noopener&quot;&gt;The Almighty Pause Container&lt;/a&gt; &lt;a href=&quot;https://kubernetes.io/blog/2015/06/the-distributed-system-toolkit-patterns/&quot; rel=&quot;noopener&quot;&gt;The Distributed System Toolkit: Patterns for Composite Containers&lt;/a&gt; &lt;a href=&quot;https://kubernetes.io/blog/2016/06/container-design-patterns/&quot; rel=&quot;noopener&quot;&gt;Container Design Patterns&lt;/a&gt; &lt;a href=&quot;https://matthewpalmer.net/kubernetes-app-developer/articles/multi-container-pod-design-patterns.html&quot; rel=&quot;noopener&quot;&gt;Multi-Container Pod Design Patterns in Kubernetes&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>tensorflow-serving 在k8s中的模型部署方案</title><link>https://www.myway5.com/blog/tensorflow-serving/</link><guid isPermaLink="true">https://www.myway5.com/blog/tensorflow-serving/</guid><description>tensorflow-serving是一个tensorflow模型部署的方案，其在设计时，就考虑了非常灵活的设计，比如： 支持不同的文件系统，并且易扩展 将模型发现、加载、使用和卸载和模型生命周期的管理，以及对外提供服务解耦合，因此非常容易扩展它的模型发现方式，以及同样可以支持…</description><pubDate>Mon, 09 Dec 2019 06:02:54 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;tensorflow-serving是一个tensorflow模型部署的方案，其在设计时，就考虑了非常灵活的设计，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持不同的文件系统，并且易扩展&lt;/li&gt;
&lt;li&gt;将模型发现、加载、使用和卸载和模型生命周期的管理，以及对外提供服务解耦合，因此非常容易扩展它的模型发现方式，以及同样可以支持其他框架下模型的整合。&lt;/li&gt;
&lt;li&gt;整个服务是无状态的，因此方便在k8s上进行部署&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下图是 tensorflow serving 的整体架构: &lt;img src=&quot;/uploads/wp/2019/12/serving_architecture.svg&quot; alt=&quot;tensorflow serving architecture&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;模型加载方式&lt;/h2&gt;
&lt;p&gt;tensorflow-serving支持从不同的地方，以不同的方式去加载模型。比如我们可以直接在启动tensorflow-serving时加上模型的地址，也可以提供模型配置文件来启动服务。 启动时加上参数:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tensorflow_model_server --port=9000 --rest_api_port=8500 --model_name=resnet --model_base_path=/home/jiang/data/yolov3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从配置文件中加载模型: /etc/config/models.config&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model_config_list {
    config {
        name: &apos;fashion&apos;
        base_path: &apos;s3://models/fashion/&apos;
        model_platform: &apos;tensorflow&apos;
    }
    config {
        name: &apos;resnet&apos;
        base_path: &apos;s3://models/resnet/&apos;
        model_platform: &apos;tensorflow&apos;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行以下命令来加载&lt;code&gt;fashion&lt;/code&gt;和&lt;code&gt;resnet&lt;/code&gt;两个模型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tensorflow_model_server --port=9000&quot;, &quot;--rest_api_port=8500&quot;, &quot;--model_config_file=/etc/config/models.config&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;模型存储系统&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tensorflow-serving&lt;/code&gt;的另一个特点就是支持从不同类型的存储系统中加载模型。比如本地的文件系统、s3、hdfs等等 &lt;strong&gt;从本地文件系统中加载&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tensorflow_model_server --port=9000 --rest_api_port=8500 --model_name=resnet --model_base_path=/home/jiang/data/yolov3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;从s3加载&lt;/strong&gt; 从s3（兼容s3的对象存储系统都可以）中加载模型，需要配置一些环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export AWS_ACCESS_KEY_ID=&amp;lt;key id&amp;gt;
export AWS_SECRET_ACCESS_KEY=&amp;lt;key&amp;gt;
export S3_ENDPOINT=minio-service.minio:9000
export S3_USE_HTTPS=0
export S3_VERIFY_SSL=0
export AWS_REGION=us-west-1
export S3_REGION=us-west-1
export AWS_LOG_LEVEL=3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后通过以下命令启动服务即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tensorflow_model_server --port=9000 --rest_api_port=8500 --model_name=resnet --model_base_path=s3://models/resnet/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;从hdfs中加载&lt;/strong&gt; 从hdfs中加载需要设置以下的环境变量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;JAVA_HOME&lt;/code&gt;: Java 的安装路径&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;HADOOP_HDFS_HOME&lt;/code&gt;: HDFS 的安装路径，如果在LD_LIBRARY_PATH中设置了 &lt;code&gt;libhdfs.so&lt;/code&gt; 的路径，那么这个环境变量可以不要。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt;: 引入 &lt;code&gt;libjvm.so&lt;/code&gt; 的路径。如果你的 HADOOP 发行版在 &lt;code&gt;${HADOOP_HDFS_HOME}/lib/native&lt;/code&gt; 这个目录下没有包含 &lt;code&gt;libhdfs.so&lt;/code&gt;，也需要引入它。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:${JAVA_HOME}/jre/lib/amd64/server
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CLASSPATH&lt;/code&gt;: 注意仅仅是设置 &lt;code&gt;CLASSPATH&lt;/code&gt; 环境变量是不行的，需要用以下的方式使用:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;CLASSPATH=$(${HADOOP_HDFS_HOME}/bin/hadoop classpath --glob) tensorflow_model_server --port=9000 --rest_api_port=8500 --model_name=yolov3 --model_base_path=hdfs://worknode2:9000/pipeline/models/yolov3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;在k8s中部署&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;s3&lt;/strong&gt; tensorflow-serving 官方提供了docker镜像，因此使用 s3 的方式加载模型部署是很简单的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: tfserving-deployment
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: tfserving
    spec:
      containers:
      - name: serving-container
        image: tensorflow/serving:1.14.0  
        ports:
        - containerPort: 8500
        - containerPort: 9000
        env:
        - name: AWS_ACCESS_KEY_ID
          value: J5WW5NKKV7AE9S0WZCM1
        - name: AWS_SECRET_ACCESS_KEY
          value: TbG0Y6nnUV8nQNLL9n4B3u3UPMMCJvqs2COx3and
        - name: S3_ENDPOINT
          value: minio-service.minio:9000
        - name: S3_USE_HTTPS
          value: &quot;0&quot;
        - name: S3_VERIFY_SSL
          value: &quot;0&quot;
        - name: AWS_REGION
          value: us-west-1
        - name: S3_REGION
          value: us-west-1
        - name: AWS_LOG_LEVEL
          value: &quot;3&quot;
        command: [&quot;/usr/bin/tensorflow_model_server&quot;]
        args: [&quot;--port=9000&quot;, &quot;--rest_api_port=8500&quot;, &quot;--model_name=resnet&quot;, &quot;--model_base_path=s3://models/resnet/&quot;]

---
apiVersion: v1
kind: Service
metadata:
  labels:
    run: tf-service 
  name: tf-service
spec:
  ports:
  - name: rest-api-port
    port: 8500
    targetPort: 8500
  - name: grpc-port
    port: 9000
    targetPort: 9000
  selector:
    app: tfserving 
  type: NodePort
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;hdfs&lt;/strong&gt; 在官方提供的 docker 镜像中，并没有打包 hdfs 的环境，因此我们需要自己构建一个镜像: Dockerfile 所在目录如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hdfs_dockerfile
├── Dockerfile
└── hadoop-2.10.0
    ├── bin
    ├── etc
    ├── include
    ├── lib
    ├── libexec
    ├── LICENSE.txt
    ├── logs
    ├── NOTICE.txt
    ├── README.txt
    ├── sbin
    └── share
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dockerfile 如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM tensorflow/serving:1.14.0

RUN apt update &amp;amp;&amp;amp; apt install -y openjdk-8-jre

COPY hadoop-2.10.0 /root/hadoop

ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64/
ENV HADOOP_HDFS_HOME /root/hadoop
ENV LD_LIBRARY_PATH ${LD_LIBRARY_PATH}:${JAVA_HOME}/jre/lib/amd64/server

RUN echo &apos;#!/bin/bash \n\n\
CLASSPATH=$(${HADOOP_HDFS_HOME}/bin/hadoop classpath --glob) tensorflow_model_server --port=8500 --rest_api_port=9000 \
--model_name=${MODEL_NAME} --model_base_path=${MODEL_BASE_PATH}/${MODEL_NAME} \
&quot;$@&quot;&apos; &amp;gt; /usr/bin/tf_serving_entrypoint.sh \
&amp;amp;&amp;amp; chmod +x /usr/bin/tf_serving_entrypoint.sh

EXPOSE 8500
EXPOSE 9000
ENTRYPOINT [&quot;/usr/bin/tf_serving_entrypoint.sh&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进行构建:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build -t tensorflow_serving:1.14-hadoop-2.10.0 .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -p 9000:9000 --name tensorflow-serving -e MODEL_NAME=yolov3 -e MODEL_BASE_PATH=hdfs://192.168.50.166:9000/pipeline/models -t tensorflow_serving:1.14-hadoop-2.10.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样将上面的部署文件稍微修改一下即可使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: tfserving-deployment
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: tfserving
    spec:
      containers:
      - name: serving-container
        image: joyme/tensorflow_serving:1.14-hadoop-2.10.0
        ports:
        - containerPort: 8500
        - containerPort: 9000
        env:
        - name: MODEL_NAME
          value: yolov3
        - name: MODEL_BASE_PATH
          value: hdfs://192.168.50.166:9000/pipeline/models

---
apiVersion: v1
kind: Service
metadata:
  labels:
    run: tf-service 
  name: tf-service
spec:
  ports:
  - name: rest-api-port
    port: 8500
    targetPort: 8500
  - name: grpc-port
    port: 9000
    targetPort: 9000
  selector:
    app: tfserving 
  type: NodePort
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;模型调用&lt;/h2&gt;
&lt;p&gt;tensorflow-serving 支持两种方式调用模型进行预测: GRPC 和 RESTful api GRPC的方式如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from __future__ import print_function

import grpc
import requests
import tensorflow as tf

from tensorflow_serving.apis import predict_pb2
from tensorflow_serving.apis import prediction_service_pb2_grpc

IMAGE_URL = &apos;https://tensorflow.org/images/blogs/serving/cat.jpg&apos;

tf.app.flags.DEFINE_string(&apos;server&apos;, &apos;192.168.50.201:30806&apos;, &apos;PredictionService host:port&apos;)
tf.app.flags.DEFINE_string(&apos;image&apos;, &apos;&apos;, &apos;path to image in jpeg format&apos;)
FLAGS = tf.app.flags.FLAGS

def main(_):
    if FLAGS.image:
        with open(FLAGS.image, &apos;rb&apos;) as f:
            data = f.read()
    else:
        dl_request = requests.get(IMAGE_URL, stream=True)
        dl_request.raise_for_status()
        data = dl_request.content

    channel = grpc.insecure_channel(FLAGS.server)
    stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)

    # Send request
    request = predict_pb2.PredictRequest()
    request.model_spec.name = &apos;resnet&apos;
    request.model_spec.signature_name = &apos;serving_default&apos;
    request.inputs[&apos;image_bytes&apos;].CopyFrom(
            tf.contrib.util.make_tensor_proto(data, shape=[1]))

    result = stub.Predict(request, 10.0) # 10 secs timeout
    print(result)

if __name__ == &apos;__main__&apos;:
    tf.app.run()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RESTful API的方式如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests
import json
import base64

with open(&quot;cat.jpg&quot;, &quot;rb&quot;) as image_file:
    encoded_string = base64.b64encode(image_file.read())

headers = {&quot;content-type&quot;: &quot;application/json&quot;}
body = {
        &quot;instances&quot;: [
            {&apos;b64&apos;: encoded_string}
           ]
        }
r = requests.post(&apos;http://192.168.50.201:32063/v1/models/resnet:predict&apos;, data = json.dumps(body), headers = headers)

print(r.text)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>k8s</category><category>tensorflow</category><author>joyme123</author></item><item><title>容器标准化</title><link>https://www.myway5.com/blog/container/</link><guid isPermaLink="true">https://www.myway5.com/blog/container/</guid><description>我认为容器标准化可以分为两个角度去讲： 一个是容器的使用和镜像的格式需要规范，这叫做OCI(open container initiative)，也就是说，不同技术实现的容器，都可以使用同一种方式运行，同一个镜像也可以在不同的容器技术上运行。</description><pubDate>Mon, 04 Nov 2019 05:33:27 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;我认为容器标准化可以分为两个角度去讲： 一个是容器的使用和镜像的格式需要规范，这叫做OCI(open container initiative)，也就是说，不同技术实现的容器，都可以使用同一种方式运行，同一个镜像也可以在不同的容器技术上运行。 另外一个就是因为Kubernetes的流行，Kubernetes推出了一个CRI(container runtime interface)的接口规范，凡是直接或间接实现了这个接口规范的容器都可以作为Kubernetes的默认容器运行时。 OCI和CRI的制定也意味着容器技术迎来了高速发展。&lt;/p&gt;
&lt;h2&gt;CRI: container runtime interface&lt;/h2&gt;
&lt;p&gt;CRI是kubernetes推出的容器运行时接口，有了CRI，不论各种容器化技术是如何实现的，都可以用一个共同的接口对外提供服务。CRI中定义了容器和镜像的接口的接口，基于&lt;code&gt;gRPC&lt;/code&gt;调用。具体的可以查看&lt;a href=&quot;https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1alpha2/api.proto&quot; rel=&quot;noopener&quot;&gt;api.proto&lt;/a&gt;。下面简单的列一下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Runtime service defines the public APIs for remote container runtimes
service RuntimeService {
    // Version returns the runtime name, runtime version, and runtime API version.
    rpc Version(VersionRequest) returns (VersionResponse) {}

    // RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure
    // the sandbox is in the ready state on success.
    rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
    // StopPodSandbox stops any running process that is part of the sandbox and
    // reclaims network resources (e.g., IP addresses) allocated to the sandbox.
    // If there are any running containers in the sandbox, they must be forcibly
    // terminated.
    // This call is idempotent, and must not return an error if all relevant
    // resources have already been reclaimed. kubelet will call StopPodSandbox
    // at least once before calling RemovePodSandbox. It will also attempt to
    // reclaim resources eagerly, as soon as a sandbox is not needed. Hence,
    // multiple StopPodSandbox calls are expected.
    rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
    // RemovePodSandbox removes the sandbox. If there are any running containers
    // in the sandbox, they must be forcibly terminated and removed.
    // This call is idempotent, and must not return an error if the sandbox has
    // already been removed.
    rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
    // PodSandboxStatus returns the status of the PodSandbox. If the PodSandbox is not
    // present, returns an error.
    rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}
    // ListPodSandbox returns a list of PodSandboxes.
    rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}

    // CreateContainer creates a new container in specified PodSandbox
    rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
    // StartContainer starts the container.
    rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
    // StopContainer stops a running container with a grace period (i.e., timeout).
    // This call is idempotent, and must not return an error if the container has
    // already been stopped.
    // TODO: what must the runtime do after the grace period is reached?
    rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
    // RemoveContainer removes the container. If the container is running, the
    // container must be forcibly removed.
    // This call is idempotent, and must not return an error if the container has
    // already been removed.
    rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}
    // ListContainers lists all containers by filters.
    rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
    // ContainerStatus returns status of the container. If the container is not
    // present, returns an error.
    rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}
    // UpdateContainerResources updates ContainerConfig of the container.
    rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {}
    // ReopenContainerLog asks runtime to reopen the stdout/stderr log file
    // for the container. This is often called after the log file has been
    // rotated. If the container is not running, container runtime can choose
    // to either create a new log file and return nil, or return an error.
    // Once it returns error, new container log file MUST NOT be created.
    rpc ReopenContainerLog(ReopenContainerLogRequest) returns (ReopenContainerLogResponse) {}

    // ExecSync runs a command in a container synchronously.
    rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {}
    // Exec prepares a streaming endpoint to execute a command in the container.
    rpc Exec(ExecRequest) returns (ExecResponse) {}
    // Attach prepares a streaming endpoint to attach to a running container.
    rpc Attach(AttachRequest) returns (AttachResponse) {}
    // PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
    rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {}

    // ContainerStats returns stats of the container. If the container does not
    // exist, the call returns an error.
    rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse) {}
    // ListContainerStats returns stats of all running containers.
    rpc ListContainerStats(ListContainerStatsRequest) returns (ListContainerStatsResponse) {}

    // UpdateRuntimeConfig updates the runtime configuration based on the given request.
    rpc UpdateRuntimeConfig(UpdateRuntimeConfigRequest) returns (UpdateRuntimeConfigResponse) {}

    // Status returns the status of the runtime.
    rpc Status(StatusRequest) returns (StatusResponse) {}
}

// ImageService defines the public APIs for managing images.
service ImageService {
    // ListImages lists existing images.
    rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {}
    // ImageStatus returns the status of the image. If the image is not
    // present, returns a response with ImageStatusResponse.Image set to
    // nil.
    rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {}
    // PullImage pulls an image with authentication config.
    rpc PullImage(PullImageRequest) returns (PullImageResponse) {}
    // RemoveImage removes the image.
    // This call is idempotent, and must not return an error if the image has
    // already been removed.
    rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {}
    // ImageFSInfo returns information of the filesystem that is used to store images.
    rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse) {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;共包含了两个服务: - RuntimeService：容器和Sandbox运行时管理。 - ImageService：提供了从镜像仓库拉取、查看、和移除镜像的RPC。 再看一下CRI的架构图： &lt;img src=&quot;/uploads/wp/2019/10/cri-architecture.png&quot; alt=&quot;cri architecture&quot; /&gt; 在kubernetes中，CRI扮演了kubelet和container runtime的通信桥梁。也因为CRI的存在，container runtime和kubelet解耦，就有了多种选择，比如: docker、 CRI-O、containerd、frakti等等。&lt;/p&gt;
&lt;h2&gt;OCI: open container initiative&lt;/h2&gt;
&lt;p&gt;这个是由docker和其他的公司推动的容器标准，为了围绕容器格式和运行时制定一个开放的工业化标准，目前主要有两个标准文档：容器运行时标准 （runtime spec）和 容器镜像标准（image spec）。这两个协议通过 OCI runtime filesytem bundle 的标准格式连接在一起，OCI 镜像可以通过工具转换成 bundle，然后 OCI 容器引擎能够识别这个 bundle 来运行容器 &lt;img src=&quot;/uploads/wp/2019/10/oci.png&quot; alt=&quot;oci&quot; /&gt; 下面引用一下其他博客的文字（&lt;a href=&quot;https://www.jianshu.com/p/62e71584d1cb&quot; rel=&quot;noopener&quot;&gt;https://www.jianshu.com/p/62e71584d1cb&lt;/a&gt;）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;设计考量&lt;/strong&gt; 操作标准化：容器的标准化操作包括使用标准容器创建、启动、停止容器，使用标准文件系统工具复制和创建容器快照，使用标准化网络工具进行下载和上传。 内容无关：内容无关指不管针对的具体容器内容是什么，容器标准操作执行后都能产生同样的效果。如容器可以用同样的方式上传、启动，不管是PHP应用还是MySQL数据库服务。 基础设施无关：无论是个人的笔记本电脑还是AWS S3，亦或是OpenStack，或者其它基础设施，都应该对支持容器的各项操作。 为自动化量身定制：制定容器统一标准，是的操作内容无关化、平台无关化的根本目的之一，就是为了可以使容器操作全平台自动化。 工业级交付：制定容器标准一大目标，就是使软件分发可以达到工业级交付成为现实 &lt;strong&gt;image spec（容器标准包）&lt;/strong&gt; OCI 容器镜像主要包括几块内容： 文件系统：以 layer 保存的文件系统，每个 layer 保存了和上层之间变化的部分，layer 应该保存哪些文件，怎么表示增加、修改和删除的文件等 config 文件：保存了文件系统的层级信息（每个层级的 hash 值，以及历史信息），以及容器运行时需要的一些信息（比如环境变量、工作目录、命令参数、mount 列表），指定了镜像在某个特定平台和系统的配置。比较接近我们使用 docker inspect 看到的内容 manifest 文件：镜像的 config 文件索引，有哪些 layer，额外的 annotation 信息，manifest 文件中保存了很多和当前平台有关的信息 index 文件：可选的文件，指向不同平台的 manifest 文件，这个文件能保证一个镜像可以跨平台使用，每个平台拥有不同的 manifest 文件，使用 index 作为索引 &lt;strong&gt;runtime spec（容器运行时和生命周期）&lt;/strong&gt; 容器标准格式也要求容器把自身运行时的状态持久化到磁盘中，这样便于外部的其它工具对此信息使用和演绎。该运行时状态以JSON格式编码存储。推荐把运行时状态的JSON文件存储在临时文件系统中以便系统重启后会自动移除。 基于Linux内核的操作系统，该信息应该统一地存储在/run/opencontainer/containers目录，该目录结构下以容器ID命名的文件夹（/run/opencontainer/containers//state.json）中存放容器的状态信息并实时更新。有了这样默认的容器状态信息存储位置以后，外部的应用程序就可以在系统上简便地找到所有运行着的容器了。 state.json文件中包含的具体信息需要有： 版本信息：存放OCI标准的具体版本号。 容器ID：通常是一个哈希值，也可以是一个易读的字符串。在state.json文件中加入容器ID是为了便于之前提到的运行时hooks只需载入state.json就- - 可以定位到容器，然后检测state.json，发现文件不见了就认为容器关停，再执行相应预定义的脚本操作。 PID：容器中运行的首个进程在宿主机上的进程号。 容器文件目录：存放容器rootfs及相应配置的目录。外部程序只需读取state.json就可以定位到宿主机上的容器文件目录。 容器创建：创建包括文件系统、namespaces、cgroups、用户权限在内的各项内容。 容器进程的启动：运行容器进程，进程的可执行文件定义在的config.json中，args项。 容器暂停：容器实际上作为进程可以被外部程序关停（kill），然后容器标准规范应该包含对容器暂停信号的捕获，并做相应资源回收的处理，避免孤儿进程的出现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;CRI和OCI的对比&lt;/h2&gt;
&lt;p&gt;OCI是容器技术的开放性标准，而CRI是Kubernetes为了更方便的支持不同的容器技术，而推出的接口标准，与CRI类似的还有CNI和CSI，分别是网络和存储的接口。 可以看一下这张图: &lt;img src=&quot;/uploads/wp/2019/10/kubelet-cri-runtime.png&quot; alt=&quot;kubelet cri runtime&quot; /&gt; kubelet有了CRI的接口，可以通过cri-containerd和containerd通信，也可以通过docker-shim和docker通信。&lt;strong&gt;注意这里的cri-containerd在containerd v1.2的时候就已经不再使用了，因为containerd本身就支持了CRI的规范。&lt;/strong&gt; 同时kubernetes还孵化了cri-o这个项目，cri-o直接打通了cri和oci。runc和kata都是oci的具体实现。 所以，用一句话理解：实现了CRI就可以保证被kubernetes使用，实现了OCI就可以在各种设备上无差别的使用各种镜像。&lt;/p&gt;
&lt;h2&gt;docker、containerd和runc&lt;/h2&gt;
&lt;p&gt;containerd从docker中分出来的一部分。containerd是负责管理容器生命周期的常驻进程，而runc则是真正负责容器运行的部分。可以通过以下的图来看三者之间的关系： &lt;img src=&quot;/uploads/wp/2019/10/docker-containerd-runc.jpg&quot; alt=&quot;docker-containerd-runc&quot; /&gt; containerd会调用多个runc实例来管理多个容器。docker engine则是提供接口给用户使用。&lt;/p&gt;
&lt;h2&gt;kubernetes当前支持的CRI后端&lt;/h2&gt;
&lt;h3&gt;containerd&lt;/h3&gt;
&lt;p&gt;containerd的地址：&lt;a href=&quot;https://github.com/containerd/containerd&quot; rel=&quot;noopener&quot;&gt;https://github.com/containerd/containerd&lt;/a&gt; 先用官网的图片来看一下containerd的架构： &lt;img src=&quot;/uploads/wp/2019/10/containerd-architecture.png&quot; alt=&quot;containerd architecture&quot; /&gt; containerd处于os和clients之间，它使用CRI API提供给Kubelet调用，使用containerd API提供给containerd client调用，使用Metrics API提供给Prometheus监控数据。然后有一层&lt;code&gt;containerd Service Interfaces&lt;/code&gt;提供给上层api使用。注意到其中还有一个&lt;code&gt;container-shim&lt;/code&gt;打通了&lt;code&gt;Runtime manager&lt;/code&gt;和&lt;code&gt;OCI runtime&lt;/code&gt;的具体实现，比如&lt;code&gt;runc&lt;/code&gt;、&lt;code&gt;runhcs&lt;/code&gt;、&lt;code&gt;kata&lt;/code&gt;。 containerd实现了以下的特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OCI Image规范的支持&lt;/li&gt;
&lt;li&gt;OCI Runtime规范的支持(通过runc等)&lt;/li&gt;
&lt;li&gt;Image的上传和下载&lt;/li&gt;
&lt;li&gt;容器运行时和生命周期的支持&lt;/li&gt;
&lt;li&gt;创建、修改和删除网络&lt;/li&gt;
&lt;li&gt;管理网络命名空间以及将容器加入到现有的网络命名空间&lt;/li&gt;
&lt;li&gt;全部镜像的CAS存储的多租户模式支持&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;cri-o&lt;/h3&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/cri-o/cri-o&quot; rel=&quot;noopener&quot;&gt;https://github.com/cri-o/cri-o&lt;/a&gt; &lt;img src=&quot;/uploads/wp/2019/11/crio-architecture.png&quot; alt=&quot;cri-o&quot; /&gt; cri-o项目是Kubernetes CRI接口的实现，同时可以兼容OCI标准的容器运行时。这样的能力就使得它可以作为Docker的轻量级的容器运行时的替代方案，使得Kubernetes可以接入符合OCI标准的所有容器运行时，同时也减少了容器开发者们的额外工作量（只需实现OCI标准即可）。&lt;/p&gt;
&lt;h3&gt;frakti&lt;/h3&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/kubernetes/frakti&quot; rel=&quot;noopener&quot;&gt;https://github.com/kubernetes/frakti&lt;/a&gt; &lt;img src=&quot;/uploads/wp/2019/11/frakti.png&quot; alt=&quot;frakti&quot; /&gt; frakti是Kubernetes官方推出的一个容器运行时，但是不同于docker这样的利于linux namespace的技术，它是基于虚拟化技术的容器，因此可以带来更好的环境隔离以及独享的内核。&lt;/p&gt;
&lt;h3&gt;rkt&lt;/h3&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/rkt/rkt/&quot; rel=&quot;noopener&quot;&gt;https://github.com/rkt/rkt/&lt;/a&gt; &lt;img src=&quot;/uploads/wp/2019/11/rkt-vs-docker-process-model.png&quot; alt=&quot;rkt-vs-docker-process-model&quot; /&gt; rkt是coreos推出的和Docker抗衡的容器产品，不同于现在的Docker往更大更全的方向，不仅仅是容器功能，更集成了Swarm这样的集群方案，rkt注重的是作为运行在linux系统上的容器组件。上图可以看出Docker的架构要更加的复杂。&lt;/p&gt;
&lt;h3&gt;docker&lt;/h3&gt;
&lt;p&gt;官网地址: &lt;a href=&quot;https://docker.com&quot; rel=&quot;noopener&quot;&gt;https://docker.com&lt;/a&gt; docker作为Kubernetes的默认容器运行时，其本身在容器领域也占据了绝对的领导地位。&lt;/p&gt;
&lt;h2&gt;实现了OCI，可以通过cri-o接入kubernetes的项目&lt;/h2&gt;
&lt;h3&gt;runc&lt;/h3&gt;
&lt;p&gt;项目地址: &lt;a href=&quot;https://github.com/opencontainers/runc&quot; rel=&quot;noopener&quot;&gt;https://github.com/opencontainers/runc&lt;/a&gt; opencontainers组织推出了OCI的规范，同时也开发了runc作为OCI规范的实现。runc是docker贡献出来的容器运行时，runc不仅是containerd的默认运行时，同时也可以接入到cri-o中。&lt;/p&gt;
&lt;h3&gt;Clear Containers&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/clearcontainers/runtime&quot; rel=&quot;noopener&quot;&gt;https://github.com/clearcontainers/runtime&lt;/a&gt;，项目已经不在维护，推荐迁移到Kata Containers&lt;/p&gt;
&lt;h3&gt;Kata Containers&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kata-containers/runtime&quot; rel=&quot;noopener&quot;&gt;https://github.com/kata-containers/runtime&lt;/a&gt; Kata Containers和runc这种技术栈是不同的。runc使用的是linux namespace和cgroup来做环境隔离和资源限制，缺点在于使用的仍然是宿主机的内核，这样一旦受到了内核层的影响，会扩散到所有的容器。而Kata Containers使用的是虚拟化的技术，它实际上是一个虚拟机，但是可以像容器那样使用。 Kata Containers是2017年12月启动的项目，结合了Intel Clear Containers和 Hyper.sh RunV的优点，支持不同的主流架构，除x86_64外，还支持AMD64, ARM, IBM p-series and IBM z-series。 下图是kata Containers和传统容器技术的对比: &lt;img src=&quot;/uploads/wp/2019/11/katacontainers_traditionalvskata_diagram.jpg&quot; alt=&quot;katacontainers_traditionalvskata_diagram&quot; /&gt; 主要特点如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;/strong&gt;: 使用专用内核，提供了网络、IO和内存的独立，在虚拟化VT扩展的基础上利用硬件强制隔离&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能&lt;/strong&gt;: 提供与标准Linux容器一致的性能；提高隔离度，而无需增加标准虚拟机的性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兼容性&lt;/strong&gt;: 支持行业标准，包括OCI容器格式，Kubernetes CRI接口以及旧版虚拟化技术。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简单&lt;/strong&gt;: 消除了在完整的虚拟机内部嵌套容器的要求；标准接口使插入和入门变得容易&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下图是Kata Containers的架构: &lt;img src=&quot;/uploads/wp/2019/11/katacontainers_architecture_diagram.jpg&quot; alt=&quot;katacontainers_architecture_diagram&quot; /&gt; Kubernetes可以通过Hypervisor VSOCK Socket和容器交互。&lt;/p&gt;
&lt;h3&gt;gVisor&lt;/h3&gt;
&lt;p&gt;gVisor提供的是一个沙箱容器环境，可以说是传统容器技术和虚拟机容器技术的折中。它使用Go编写了一个可以作为普通非特权进程运行的内核，这个内核实现了大多数的系统调用。所以相比于namespace和cgroup实现的容器，它可以屏蔽掉容器内应用程序的内核调用。相比于虚拟机实现的容器，它更轻量级（作为系统的一个进程运行）。 &lt;img src=&quot;/uploads/wp/2019/11/gvisor.jpg&quot; alt=&quot;gvisor&quot; /&gt;&lt;/p&gt;
</content:encoded><category>k8s</category><category>容器技术</category><category>oci</category><category>cri</category><author>joyme123</author></item><item><title>理解kubernetes service</title><link>https://www.myway5.com/blog/kubernetes-service/</link><guid isPermaLink="true">https://www.myway5.com/blog/kubernetes-service/</guid><description>这篇文章不是关于如何使用kubernetes中的service，而是尝试整理我自己对service的看法，然后加深对service的理解。那么，我是从哪几个角度去看待service呢？</description><pubDate>Wed, 30 Oct 2019 08:00:58 GMT</pubDate><content:encoded>&lt;h2&gt;理解service的角度&lt;/h2&gt;
&lt;p&gt;这篇文章不是关于如何使用kubernetes中的service，而是尝试整理我自己对service的看法，然后加深对service的理解。那么，我是从哪几个角度去看待service呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;service是服务的稳定性保证&lt;/li&gt;
&lt;li&gt;service是集群中的load balance&lt;/li&gt;
&lt;li&gt;通过无selector的service去理解VIP(虚拟ip)&lt;/li&gt;
&lt;li&gt;service的设计，和不同实现方式的性能&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;service是服务的稳定性保证&lt;/h2&gt;
&lt;p&gt;在k8s集群中，无状态的pod副本是可以随时删除、随时创建的，并且重新创建的pod不再保留旧的pod的任何信息，包括ip地址。在这样的情况下，前端应用如何使用后端的这些pod来提供服务就成了问题，因此k8s实现了service这样一个抽象的概念。对于有selector的service，它在被创建的时候会自动创建endpoint资源，这个endpoint中包含了所有的pod的ip和端口，并且在之后的pod的删除、创建中，这个endpoint中会立即更新相关pod的ip和端口信息。同时，service的ip地址是永远固定的，service和endpoint是一一对应的关系。这样，如果前端应用通过固定的service ip来访问pod提供的服务，那么就可以在endpoint中找到一个可用的pod的ip和端口，然后通过一些操作（这个在后面会整理）将数据包转发到指定的pod上即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 你可以通过kubectl查看service和endpoint来加深理解

$ kubectl -n h2o describe svc h2o

Name:              h2o
Namespace:         h2o
Labels:            app=h2o
Annotations:       kubectl.kubernetes.io/last-applied-configuration:
                     {&quot;apiVersion&quot;:&quot;v1&quot;,&quot;kind&quot;:&quot;Service&quot;,&quot;metadata&quot;:{&quot;annotations&quot;:{},&quot;labels&quot;:{&quot;app&quot;:&quot;h2o&quot;},&quot;name&quot;:&quot;h2o&quot;,&quot;namespace&quot;:&quot;h2o&quot;},&quot;spec&quot;:{&quot;clusterIP...
Selector:          app=h2o
Type:              ClusterIP
IP:                None
Port:              web  54321/TCP
TargetPort:        54321/TCP
Endpoints:         10.42.1.33:54321,10.42.2.139:54321
Session Affinity:  None
Events:            &amp;lt;none&amp;gt;


$ kubectl -n h2o get endpoints h2o
NAME   ENDPOINTS                            AGE
h2o    10.42.1.33:54321,10.42.2.139:54321   4h45m

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;service通过ip地址的固定来保证服务的稳定性。那为啥service就是可以固定不变的呢？这是因为service本身就是一个抽象的概念啊，它不是一个正在运行的进程，只是一条数据，也正因为如此，它的ip地址和端口号也是不存在的，这些都是存储在etcd中的一条数据。那么k8s是如何通过这样一个虚假的ip和端口将请求转发到真实存在的pod中呢？这就是后面要说的内容了。&lt;/p&gt;
&lt;h2&gt;service是集群中的load balance&lt;/h2&gt;
&lt;p&gt;在上一节说到，一个service会对应一个endpoint，这个endpoint中会保存所有当前匹配到的pod的ip和端口号。那么现在有一个http请求过来了，发现endpoint中有三个待选的pod，那么我们使用一定的方式比较公平的选择出一个pod，就轻松的达到了负载均衡的效果。 &lt;img src=&quot;/uploads/wp/2019/10/service-loadbalance.png&quot; alt=&quot;service load balance&quot; /&gt; 那么k8s中，load balance的策略是什么样的呢？因为不同的service实现方式使用的方法不同，这个内容会在后面整理。&lt;/p&gt;
&lt;h2&gt;通过无selector的service去理解VIP(虚拟ip)&lt;/h2&gt;
&lt;p&gt;在前面的内容中，service一直和endpoint、pod关联在一起，那么如果我们的service没有selector，就不会创建endpoint了，也不会关联pod。前面也提到了service是一个抽象的概念，其拥有的ip和port都是假的。其实这个叫做VIP(virtual ip)。那么，如何通过无selector的service来理解VIP呢？ 在k8s中创建无selector service的时候，不会自动创建关联的endpoint，更不会去匹配pod了。但是这样的service仍然是拥有ip和port的。我们可以尝试一下： svc-without-selector.yaml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  ports:
    - protocol: TCP
      port: 8081
      targetPort: 8081
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f svc-without-selector.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看一下这个svc的详情:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl describe svc my-service

Name:              my-service
Namespace:         default
Labels:            &amp;lt;none&amp;gt;
Annotations:       kubectl.kubernetes.io/last-applied-configuration:
                     {&quot;apiVersion&quot;:&quot;v1&quot;,&quot;kind&quot;:&quot;Service&quot;,&quot;metadata&quot;:{&quot;annotations&quot;:{},&quot;name&quot;:&quot;my-service&quot;,&quot;namespace&quot;:&quot;default&quot;},&quot;spec&quot;:{&quot;ports&quot;:[{&quot;port&quot;:8081,...
Selector:          &amp;lt;none&amp;gt;
Type:              ClusterIP
IP:                10.43.12.208
Port:              &amp;lt;unset&amp;gt;  8081/TCP
TargetPort:        8081/TCP
Endpoints:         &amp;lt;none&amp;gt;
Session Affinity:  None
Events:            &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了拥有ip和端口号，就什么都没有了。这就是说service为什么就是一条数据的原因，10.43.12.208也就是一个VIP。 对于无selector的service还有一个用处，就是让集群内部的应用可以稳定的访问到集群外部的服务。因为service是稳定的，那么集群内部都可以访问这个service，然后让这个service将请求转发到集群外。 这里我们可以手动创建一个endpoint，这个endpoint包含了集群外的两个http服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Endpoints
metadata:
  name: my-service
subsets:
  - addresses:
      - ip: 192.168.50.99
      - ip: 192.168.50.201
    ports:
      - port: 8081
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们先检查一下service，发现endpoints已经更新了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl describe svc my-service

Name:              my-service
Namespace:         default
Labels:            &amp;lt;none&amp;gt;
Annotations:       kubectl.kubernetes.io/last-applied-configuration:
                     {&quot;apiVersion&quot;:&quot;v1&quot;,&quot;kind&quot;:&quot;Service&quot;,&quot;metadata&quot;:{&quot;annotations&quot;:{},&quot;name&quot;:&quot;my-service&quot;,&quot;namespace&quot;:&quot;default&quot;},&quot;spec&quot;:{&quot;ports&quot;:[{&quot;port&quot;:8081,...
Selector:          &amp;lt;none&amp;gt;
Type:              ClusterIP
IP:                10.43.12.208
Port:              &amp;lt;unset&amp;gt;  8081/TCP
TargetPort:        8081/TCP
Endpoints:         192.168.50.201:8081,192.168.50.99:8081
Session Affinity:  None
Events:            &amp;lt;none&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在集群内部访问一下(使用kubectl exec到一个pod上):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wget my-service:8081 -q -O out | cat out
server 2
$ wget my-service:8081 -q -O out | cat out
server 1
$ wget my-service:8081 -q -O out | cat out
server 2
$ wget my-service:8081 -q -O out | cat out
server 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;service的设计，和不同实现方式的性能&lt;/h2&gt;
&lt;p&gt;service的设计是以提高性能为前提不断的演进的，这里是关于Service的设计讨论: &lt;a href=&quot;https://github.com/kubernetes/kubernetes/issues/1107&quot; rel=&quot;noopener&quot;&gt;DESIGN: Services v2&lt;/a&gt;。感兴趣的还可以看看k8s-release-v1.0的时候对service的描述: &lt;a href=&quot;https://github.com/kubernetes/kubernetes/blob/release-1.0/docs/user-guide/services.md&quot; rel=&quot;noopener&quot;&gt;Service&lt;/a&gt; service的设计中有4个角色: Pod、 Service、Ambassador、Portal&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pod: k8s集群中的最小调度单位，包含一个或多个容器&lt;/li&gt;
&lt;li&gt;Service: 一组pod的集合，由标签选择器来关联&lt;/li&gt;
&lt;li&gt;Ambassador: 中文翻译是&lt;code&gt;大使&lt;/code&gt;，是一段可执行的逻辑，它负责实现客户端访问Service，然后将请求转发到一个对应的Pod上。这个Ambassador可以是一个云服务商的服务，也可以是一个单独的pod(比如haproxy)，或者是每个节点都有的共享进程(kube-proxy)。&lt;/li&gt;
&lt;li&gt;Portal: 固定的ip:port对，客户端只要访问这个Portal，请求自然会被转发到Ambassador上，客户端不需要理解Ambassador的具体实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最初的设计中是有三种方案， &lt;strong&gt;方案一&lt;/strong&gt;: 每个服务一个ip，共享的&lt;code&gt;Ambassador&lt;/code&gt;。这个ip就是上面说的&lt;code&gt;Portal&lt;/code&gt; ip。将服务以及ip、端口广播给所有的&lt;code&gt;kube-proxy&lt;/code&gt;实例。&lt;code&gt;kube-proxy&lt;/code&gt;设置好iptables来“窃取”所有到&lt;code&gt;Portal(ip,port)&lt;/code&gt;的请求，然后将这个请求转发到自己的某个端口上。这里&lt;code&gt;kube-proxy&lt;/code&gt;扮演的是&lt;code&gt;Ambassador&lt;/code&gt;角色，它会使用&lt;code&gt;round-robin&lt;/code&gt;的方法来把请求均衡的分发到Service后面的Pod上。这个方案里，有以下的优点和缺点： &lt;strong&gt;优点：&lt;/strong&gt; - 不会有端口冲突 - Service的ip和port都是固定的，方便做DNS A (forward) 和 PTR (reverse)和 SRV 记录。 - iptables可以放在&lt;code&gt;root namespace&lt;/code&gt;，即使pods重启了也不需要更新iptables(这是因为iptables是负责将到service ip:port的流量转发到kube-proxy的一个端口上即可)。 - 不需要在pod上预先声明需要的Service。 &lt;strong&gt;缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;kube-proxy是多租户的(需要为所有的service做流量转发)&lt;/li&gt;
&lt;li&gt;从kube-proxy转发的流量的源ip不是真实的源ip，&lt;/li&gt;
&lt;li&gt;需要为portal预留虚拟ip空间&lt;/li&gt;
&lt;li&gt;需要master跟踪和检查所有的portal ip&lt;/li&gt;
&lt;li&gt;当service数量级上千后可扩展性不高&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;方案二&lt;/strong&gt;： 每个服务一个ip，私有的&lt;code&gt;Ambassador&lt;/code&gt;。对每个pod来说，都有一个_私有_的的ambassador，这要求pod需要先声明它们想先访问那个服务（否则的话，对于集群中的每次Service的添加和删除，都需要&lt;code&gt;kubelet&lt;/code&gt;或其他的root-namespace、true-root的用户代理变动到每个pod的namespace下。[iptables规则需要root用户])，这样才能在pod的命名空间下建立iptables规则。 &lt;strong&gt;优点:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不会有端口冲突&lt;/li&gt;
&lt;li&gt;Service的ip和port都是固定的，方便做DNS A (forward) 和 PTR (reverse)和 SRV 记录。&lt;/li&gt;
&lt;li&gt;代理不是多租户的&lt;/li&gt;
&lt;li&gt;从kube-proxy转发的流量的源ip是真实的源ip，&lt;/li&gt;
&lt;li&gt;容易从方案一迁移&lt;/li&gt;
&lt;li&gt;需要pod预先声明服务（结构良好)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iptables是配置在pod的namespace下，但是pod的命名空间重启了就必须重新运行一次&lt;/li&gt;
&lt;li&gt;需要为portal预留虚拟ip空间&lt;/li&gt;
&lt;li&gt;需要master跟踪和检查所有的portal ip&lt;/li&gt;
&lt;li&gt;需要pod预先声明服务（目前还没实现）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;方案三&lt;/strong&gt;：localhost的portal，私有的ambassador 不同于给service分配ip，而是使用本地的端口号作为portal。 介绍完这三种方案后，就可以引入service最终的演进了: userspace-&amp;gt;iptables-&amp;gt;ipvs。 这里先放一张iptables的工作流程图，方便理解： &lt;img src=&quot;/uploads/wp/2019/10/iptables%E7%BB%93%E6%9E%84%E5%9B%BE.png&quot; alt=&quot;iptables&quot; /&gt; &lt;strong&gt;userspace模式&lt;/strong&gt; 这里的userspace就是方案一的实现，在k8s 1.0的发布中正式启用。userspace的工作原理图如下： &lt;img src=&quot;/uploads/wp/2019/10/services-userspace-overview.svg&quot; alt=&quot;userspace service overview&quot; /&gt; 这种模式，kube-proxy 会监视 Kubernetes master 对 Service 对象和 Endpoints 对象的添加和移除。 对每个 Service，它会在本地 Node 上打开一个端口（随机选择）。 任何连接到“代理端口”的请求，都会被代理到 Service 的backend Pods 中的某个上面（如 Endpoints 所报告的一样）。 使用哪个 backend Pod，是 kube-proxy 基于 SessionAffinity 来确定的。 最后，它安装 iptables 规则，捕获到达该 Service 的 clusterIP（是虚拟 IP）和 Port 的请求，并重定向到代理端口，代理端口再代理请求到 backend Pod。默认情况下，用户空间模式下的kube-proxy通过&lt;code&gt;round-robin&lt;/code&gt;选择后端。 这里有一个问题在于，client访问service的clusterIP时，iptables会把流量转发到kube-proxy的某个端口上，这样的话，每次转发都有一个&lt;code&gt;内核态&lt;/code&gt;到&lt;code&gt;用户态&lt;/code&gt;的转换。 &lt;strong&gt;iptables模式&lt;/strong&gt; &lt;img src=&quot;/uploads/wp/2019/10/services-iptables-overview.svg&quot; alt=&quot;iptables service overview&quot; /&gt; 这种模式，kube-proxy 会监视 Kubernetes 控制节点对 Service 对象和 Endpoints 对象的添加和移除。 对每个 Service，它会安装 iptables 规则，从而捕获到达该 Service 的 clusterIP 和端口的请求，进而将请求重定向到 Service 的一组 backend 中的某个上面。 对于每个 Endpoints 对象，它也会安装 iptables 规则，这个规则会选择一个 backend 组合。 默认的策略是，kube-proxy 在 iptables 模式下随机选择一个 backend。类似于这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iptables -t nat -A PREROUTING -p tcp -d 15.45.23.67 --dport 80 -j DNAT --to-destination 192.168.1.1-192.168.1.10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 iptables 处理流量具有较低的系统开销，因为流量由 Linux netfilter 处理，而无需在用户空间和内核空间之间切换。 这种方法也可能更可靠。 如果 kube-proxy 在 iptable s模式下运行，并且所选的第一个 Pod 没有响应，则连接失败。 这与用户空间模式不同：在这种情况下，kube-proxy 将检测到与第一个 Pod 的连接已失败，并会自动使用其他后端 Pod 重试。 您可以使用 Pod readiness 探测器 验证后端 Pod 可以正常工作，以便 iptables 模式下的 kube-proxy 仅看到测试正常的后端。 这样做意味着您避免将流量通过 kube-proxy 发送到已知已失败的Pod。 &lt;strong&gt;ipvs模式&lt;/strong&gt; ipvs是在Kubernetes v1.11正式可用的。ipvs也是依赖于iptables的，但是它的性能更高。 &lt;img src=&quot;/uploads/wp/2019/10/services-ipvs-overview.svg&quot; alt=&quot;ipvs service overview&quot; /&gt; 在ipvs模式下，kube-proxy监视Kubernetes服务和端点，调用netlink接口相应地创建IPVS规则，并定期将IPVS规则与Kubernetes服务和端点同步。该控制循环可确保IPVS状态与所需状态匹配。访问服务时，IPVS　将流量定向到后端Pod之一。 IPVS代理模式基于类似于iptables模式的netfilter挂钩函数，但是使用哈希表作为基础数据结构，并且在内核空间中工作。 这意味着，与iptables模式下的 kube-proxy 相比，IPVS 模式下的 kube-proxy 重定向通信的延迟要短，并且在同步代理规则时具有更好的性能。与其他代理模式相比，IPVS 模式还支持更高的网络流量吞吐量。 IPVS提供了更多选项来平衡后端Pod的流量。 这些是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rr: round-robin&lt;/li&gt;
&lt;li&gt;lc: least connection (smallest number of open connections)&lt;/li&gt;
&lt;li&gt;dh: destination hashing&lt;/li&gt;
&lt;li&gt;sh: source hashing&lt;/li&gt;
&lt;li&gt;sed: shortest expected delay&lt;/li&gt;
&lt;li&gt;nq: never queue&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意： 要在IPVS模式下运行kube-proxy，必须在启动kube-proxy之前使IPVS Linux在节点上可用。 当 kube-proxy 以 IPVS 代理模式启动时，它将验证 IPVS 内核模块是否可用。 如果未检测到 IPVS 内核模块，则 kube-proxy 将退回到以 iptables 代理模式运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;ipvs在同步规则、网络带宽、cpu/内存消耗上都明显优于iptables，关于具体的性能数据可以看这篇文章: &lt;a href=&quot;https://zhuanlan.zhihu.com/p/37230013&quot; rel=&quot;noopener&quot;&gt;华为云在 K8S 大规模场景下的 Service 性能优化实践&lt;/a&gt;。ipvs的详细介绍可以看这篇文章:&lt;a href=&quot;https://www.qikqiak.com/post/how-to-use-ipvs-in-kubernetes/&quot; rel=&quot;noopener&quot;&gt;ipvs 基本介绍&lt;/a&gt;。ipvs和iptables的对比:&lt;a href=&quot;https://blog.fleeto.us/post/iptables-or-ipvs/&quot; rel=&quot;noopener&quot;&gt;kube-proxy 模式对比：iptables 还是 IPVS？&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>打赏</title><link>https://www.myway5.com/blog/e6-89-93-e8-b5-8f/</link><guid isPermaLink="true">https://www.myway5.com/blog/e6-89-93-e8-b5-8f/</guid><description>感谢打赏～ 微信付款码 支付宝付款码</description><pubDate>Fri, 25 Oct 2019 16:12:58 GMT</pubDate><content:encoded>&lt;p&gt;感谢打赏～ &lt;strong&gt;微信付款码&lt;/strong&gt; &lt;img src=&quot;/uploads/wp/2019/10/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20191026001037-e1572020085992.jpg&quot; alt=&quot;微信付款码&quot; /&gt; &lt;strong&gt;支付宝付款码&lt;/strong&gt; &lt;img src=&quot;/uploads/wp/2019/10/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20191026001029-e1572020036110.jpg&quot; alt=&quot;支付宝付款码&quot; /&gt;&lt;/p&gt;
</content:encoded><author>joyme123</author></item><item><title>rook ceph的rgw崩溃问题排查</title><link>https://www.myway5.com/blog/rook-ceph-rgw-crash/</link><guid isPermaLink="true">https://www.myway5.com/blog/rook-ceph-rgw-crash/</guid><description>在开发可视化机器学习平台时，集成的FastRCNN实验一直跑不到结束就会出错。有时候是在下载基础模型以及代码包时出错，有时候在train结束后向predict传递artifacts出错。 过程</description><pubDate>Fri, 25 Oct 2019 07:54:31 GMT</pubDate><content:encoded>&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;在开发可视化机器学习平台时，集成的FastRCNN实验一直跑不到结束就会出错。有时候是在下载基础模型以及代码包时出错，有时候在train结束后向predict传递artifacts出错。&lt;/p&gt;
&lt;h2&gt;过程&lt;/h2&gt;
&lt;p&gt;首先这个问题出现在局域网内，处于开发环境，因此ceph没有做高可用的部署。其次，ceph是用rook这个项目部署在k8s集群中的。 最后，在使用argo做机器学习的资源调度时，会出现大的数据资源下载和转移出现错误。具体表现为：大量数据下载会出现&lt;code&gt;connectiion refused&lt;/code&gt;,日志如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2019-10-25 02:35:24 (20.1 MB/s) - Connection closed at byte 528482304. Retrying.
--2019-10-25 02:35:25--  (try: 2)  http://rook-ceph-rgw-my-store.rook-ceph/workflow-storage/tho6wHm0UmZeZYbfWBv5lkOZ576838763
Connecting to rook-ceph-rgw-my-store.rook-ceph (rook-ceph-rgw-my-store.rook-ceph)|10.43.126.166|:80... failed: Connection refused.
Resolving rook-ceph-rgw-my-store.rook-ceph (rook-ceph-rgw-my-store.rook-ceph)... 10.43.126.166
Connecting to rook-ceph-rgw-my-store.rook-ceph (rook-ceph-rgw-my-store.rook-ceph)|10.43.126.166|:80... failed: Connection refused.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大量数据上传时也会中断，导致argo无法调用下一步： 日志如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NAME            custom-workflow-43-6rhlb.api-train-faster-1699
TYPE            Pod
PHASE           Error
MESSAGE         failed to save outputs: timed out waiting for the condition
START TIME      2019-10-24T06:15:56Z
END TIME        2019-10-24T06:30:34Z
DURATION        14:38 min
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里可能是网络问题，argo的问题或者是ceph的问题。但是当数据量不大的时候不会出现错误。因此检查ceph是否正常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~ » kubectl -n rook-ceph get pods                                                
NAME                                           READY   STATUS      RESTARTS   AGE
csi-cephfsplugin-964zm                         3/3     Running     27         46d
csi-cephfsplugin-dxnbg                         3/3     Running     12         46d
csi-cephfsplugin-provisioner-b66d48bc8-fglq9   4/4     Running     0          12d
csi-cephfsplugin-provisioner-b66d48bc8-x67pd   4/4     Running     0          12d
csi-rbdplugin-5fs2x                            3/3     Running     27         46d
csi-rbdplugin-bddlt                            3/3     Running     12         46d
csi-rbdplugin-provisioner-95dd85d6-7kc4c       5/5     Running     0          12d
csi-rbdplugin-provisioner-95dd85d6-mpjtj       5/5     Running     0          12d
rook-ceph-agent-fs4xq                          1/1     Running     9          46d
rook-ceph-agent-wx6r4                          1/1     Running     4          46d
rook-ceph-mds-myfs-a-774974c8c4-xt2ls          1/1     Running     0          12d
rook-ceph-mds-myfs-b-748d7d7f7d-wftt5          1/1     Running     0          12d
rook-ceph-mgr-a-5f54d44c98-57qcb               1/1     Running     0          12d
rook-ceph-mon-a-6f9fbfc99d-lmb6c               1/1     Running     0          17d
rook-ceph-operator-6f556bcbff-glvt6            1/1     Running     0          12d
rook-ceph-osd-0-7c489dc87b-wkt7x               1/1     Running     0          17d
rook-ceph-osd-1-86cc67cc45-25h4q               1/1     Running     0          12d
rook-ceph-osd-prepare-worknode1-xnmpw          0/1     Completed   0          12d
rook-ceph-rgw-my-store-a-66b7d8cc9d-vrhkm      1/1     Running     65         12d
rook-ceph-tools-5f5dc75fd5-52jbj               1/1     Running     0          12d
rook-discover-g95ws                            1/1     Running     6          46d
rook-discover-vs5fs                            1/1     Running     13         46d

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现ceph rgw重启了65次，这个肯定是不正常的。查看ceph rgw的日志:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl -n rook-ceph logs -p rook-ceph-rgw-my-store-a-66b7d8cc9d-vrhkm

# 截取了一部分日志
debug 2019-10-25 02:35:13.473 7f4880b98700  1 ====== starting new request req=0x55aa678c48e0 =====
debug 2019-10-25 02:35:14.549 7f4880b98700  0 ERROR: client_io-&amp;gt;complete_request() returned Broken pipe
debug 2019-10-25 02:35:14.549 7f4880b98700  1 ====== req done req=0x55aa678c48e0 op status=0 http_status=200 latency=1.076s ======
debug 2019-10-25 02:35:19.949 7f48e345d700  1 ====== starting new request req=0x55aa54a488e0 =====
debug 2019-10-25 02:35:19.949 7f48e345d700  1 ====== req done req=0x55aa54a488e0 op status=0 http_status=404 latency=0s ======

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在我的理解中&lt;code&gt;broken pipe&lt;/code&gt;一般出现在向已关闭的连接中写入数据时，会出现这个问题。但是通过日志可以发现，出现&lt;code&gt;broken pipe&lt;/code&gt;的错误之后，rgw仍然是在处理请求的，但是部分请求的&lt;code&gt;latency&lt;/code&gt;很高。因此这里的&lt;code&gt;broken pipe&lt;/code&gt;是表示着rgw开始出现一些异常情况，但不是pod重启的直接原因。 真正导致rgw被杀死的原因是因为rgw进程收到了&lt;code&gt;sigterm&lt;/code&gt;信号，然后进程被杀死。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;debug 2019-10-25 02:35:24.525 7f494c52f700 -1 received  signal: Terminated from Kernel ( Could be generated by pthread_kill(), raise(), abort(), alarm() ) UID: 0
debug 2019-10-25 02:35:24.525 7f494c52f700  1 handle_sigterm
debug 2019-10-25 02:35:24.525 7f494c52f700  1 handle_sigterm set alarm for 120
debug 2019-10-25 02:35:24.525 7f4962116780 -1 shutting down
debug 2019-10-25 02:35:24.629 7f488cbb0700  0 iterate_obj() failed with -9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;kubectl describe&lt;/code&gt;查看pod的event:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Events:
  Type     Reason     Age                  From                Message
  ----     ------     ----                 ----                -------
  Normal   Killing    15m (x65 over 12d)   kubelet, worknode1  Container rgw failed liveness probe, will be restarted
  Warning  Unhealthy  15m (x249 over 12d)  kubelet, worknode1  Liveness probe failed: Get http://10.42.2.44:80/: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
  Normal   Pulled     15m (x66 over 12d)   kubelet, worknode1  Container image &quot;ceph/ceph:v14.2.2-20190826&quot; already present on machine
  Normal   Created    15m (x66 over 12d)   kubelet, worknode1  Created container rgw
  Normal   Started    15m (x66 over 12d)   kubelet, worknode1  Started container rgw

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里才是真正的重启原因，kubelet检查pod是否存活，但是请求超时了。意味pods出现的故障，因此杀死了pod并重启。 pod的liveness设置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Liveness:       http-get http://:80/ delay=10s timeout=1s period=10s #success=1 #failure=3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;k8s的liveness机制是检查pod中应用程序存活状态并在出错后自动重启的一种机制。提供了三种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在容器内执行命令，如果执行成功，则表示容器是存活并且健康的。否则就重启容器使得应用程序恢复正常。&lt;/li&gt;
&lt;li&gt;使用http请求检查，如果返回的状态码是200则表示正常，否则表示失败。&lt;/li&gt;
&lt;li&gt;使用tcp连接检查，如果kubelet可以打开指定端口的socket连接，则表示正常，否则表示失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这个场景下出现&lt;code&gt;Warning Unhealthy 15m (x249 over 12d) kubelet, worknode1 Liveness probe failed: Get http://10.42.2.44:80/: net/http: request canceled (Client.Timeout exceeded while awaiting headers)&lt;/code&gt;，表示kubelet使用http get检查pod的80端口，但是这个请求却超时了。因此杀死了容器并重启，导致大文件(700MB以上)上传/下载失败。 这里kubelet检查的是&lt;code&gt;http://10.42.2.44:80&lt;/code&gt;这个地址，我们回过头看一下argo那边的报错信息，&lt;code&gt;Connecting to rook-ceph-rgw-my-store.rook-ceph (rook-ceph-rgw-my-store.rook-ceph)|10.43.126.166|:80... failed: Connection refused.&lt;/code&gt;。都是80端口，当然这里千万不能被ip地址误导了，&lt;code&gt;10.42.2.44&lt;/code&gt;是pod的ip地址，&lt;code&gt;10.43.126.166&lt;/code&gt;是service的ip地址，我们可以验证一下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~ » kubectl -n rook-ceph get svc                                                 
NAME                              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
rook-ceph-rgw-my-store            ClusterIP   10.43.126.166   &amp;lt;none&amp;gt;        80/TCP              46d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再结合之前&lt;code&gt;debug 2019-10-25 02:35:14.549 7f4880b98700 1 ====== req done req=0x55aa678c48e0 op status=0 http_status=200 latency=1.076s ======&lt;/code&gt;这条日志，latency已经超过了1s，而kubelet的liveness超时时间是1s。 现在基本可以得出以下异常流程:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;因为某些原因，导致rgw出现broken pipe的出错，并且部分请求的lantency时间大大提高。&lt;/li&gt;
&lt;li&gt;kubelet周期性的对rgw做liveness的检查，并且检查的http就是rgw的80端口，这个端口因为上面的原因导致lantency超过了1s，而liveness检查的timeout只有1s。因此kubelet认为该pod不健康，选择重启。&lt;/li&gt;
&lt;li&gt;kubelet向rgw发送了&lt;code&gt;sigterm&lt;/code&gt;信号，rgw关闭进程，pod重启。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;猜测可能造成这个问题的原因:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ceph所在机器的性能不够，导致响应请求出现问题。&lt;/li&gt;
&lt;li&gt;局域网的网络问题，因为内部的最高带宽只有10MB/s，但是局域网内的设备很多，网络这部分导致了瓶颈。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关于机器性能的问题，我认为是可以排除的，因为机器性能本身很好，并且开发环境几乎没有请求量，接下来就是验证是因为网络问题导致瓶颈，造成部分接口延迟过高被杀死。 为了验证这个猜想，假设这里有三台机器A,B,C，组成了一个k8s集群，ceph是部署在k8s之上的。在A之上，我用dd命令产生一个40G的大文件，然后在B之上使用wget下载。然后在C上面观察ping的延迟是否上升。 A:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ dd if=/dev/zero of=test bs=1M count=0 seek=40000

$ python -m SimpleHTTPServer 8999
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;B:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wget http://192.168.50.37:8999/test

--2019-10-25 14:18:30--  http://192.168.50.37:8999/test
正在连接 192.168.50.37:8999... 已连接。
已发出 HTTP 请求，正在等待回应... 200 OK
长度： 41943040000 (39G) [application/octet-stream]
正在保存至: “test”

test      3%[==&amp;gt;            ]   1.54G  11.1MB/s    剩余 73m 5s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ping 192.168.50.37
PING 192.168.50.37 (192.168.50.37) 56(84) bytes of data.
64 bytes from 192.168.50.37: icmp_seq=1 ttl=64 time=0.384 ms
64 bytes from 192.168.50.37: icmp_seq=2 ttl=64 time=0.373 ms
64 bytes from 192.168.50.37: icmp_seq=3 ttl=64 time=0.336 ms
64 bytes from 192.168.50.37: icmp_seq=4 ttl=64 time=4.90 ms
64 bytes from 192.168.50.37: icmp_seq=5 ttl=64 time=1.18 ms
64 bytes from 192.168.50.37: icmp_seq=6 ttl=64 time=7.74 ms
64 bytes from 192.168.50.37: icmp_seq=7 ttl=64 time=3.51 ms
64 bytes from 192.168.50.37: icmp_seq=8 ttl=64 time=6.66 ms
64 bytes from 192.168.50.37: icmp_seq=9 ttl=64 time=6.31 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;网络延迟增加的还是很明显的。 这时候使用argo开始一个新的机器学习的实验，但是这个实验的数据量较小，在之前的使用中都没有问题。结果确实出现了问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2019-10-25 06:21:04 (8.10 MB/s) - Connection closed at byte 136314880. Retrying.
--2019-10-25 06:21:05--  (try: 2)  http://rook-ceph-rgw-my-store.rook-ceph/workflow-storage/maC8Om6Y3QbSJxSTT9x82rp8908272441
Connecting to rook-ceph-rgw-my-store.rook-ceph (rook-ceph-rgw-my-store.rook-ceph)|10.43.126.166|:80... failed: Connection refused.
Resolving rook-ceph-rgw-my-store.rook-ceph (rook-ceph-rgw-my-store.rook-ceph)... 10.43.126.166
Connecting to rook-ceph-rgw-my-store.rook-ceph (rook-ceph-rgw-my-store.rook-ceph)|10.43.126.166|:80... failed: Connection refused.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么为了有对照实验，将下载关闭，重新做这个机器学习的实验，结果正常。&lt;/p&gt;
&lt;h2&gt;解决方法&lt;/h2&gt;
&lt;p&gt;因为缺少了对ceph这块源代码的研究，上面的结论并不一定正确。但是可以大概得出如何解决，可以先尝试将liveness检测的timeout时间增加。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl -n rook-ceph edit deployment rook-ceph-rgw-my-store-a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把liveness的timeout时间调成5s，这样就解决了这个问题。&lt;/p&gt;
</content:encoded><category>k8s</category><category>分布式存储</category><author>joyme123</author></item><item><title>理解go context</title><link>https://www.myway5.com/blog/go-context/</link><guid isPermaLink="true">https://www.myway5.com/blog/go-context/</guid><description>在我刚接触context包时，我是有一点迷惑的。因为在其他的编程语言中很少有接触到context包类似的用法。比如在js绘制canvas中的context，也只是作为保留上下文操作来用的。</description><pubDate>Thu, 10 Oct 2019 07:55:26 GMT</pubDate><content:encoded>&lt;h2&gt;理解context&lt;/h2&gt;
&lt;p&gt;在我刚接触context包时，我是有一点迷惑的。因为在其他的编程语言中很少有接触到context包类似的用法。比如在js绘制canvas中的context，也只是作为保留上下文操作来用的。在go语言的context包中，同样也可以当成上下文来理解，但是在看待context提供的能力时，要从以下两点来理解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;context提供了一种管理多个goroutine的机制。&lt;/li&gt;
&lt;li&gt;context最终形成了一种树形结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在深入之前，让我们回忆一下多线程/进程模型中，主线程/进程是如何管理子线程/进程的。如果子线程/进程又派生了其他的线程/进程呢？这一定是一个头疼的问题。 在go语言中，协程也面临了同样的问题。因此官方在go1.7版本中引入了context包。那么context提供了什么样的能力来管理协程呢？先看一个&lt;code&gt;withCancel&lt;/code&gt;的简单的例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func watch(ctx context.Context) {
    for {
        select {
        case &amp;lt;-ctx.Done():
            log.Println(&quot;退出&quot;)
            return

        default:
            log.Println(&quot;执行逻辑&quot;)
            time.Sleep(2 * time.Second)
        }
    }
}

func withCancel() {
    ctx, cancel := context.WithCancel(context.Background())

    go watch(ctx)
    go watch(ctx)
    go watch(ctx)

    time.Sleep(6 * time.Second)
    fmt.Println(&quot;可以了，通知子协程停止&quot;)
    cancel()
    //为了检测子协程是否停止，如果没有输出，就表示停止了
    time.Sleep(5 * time.Second)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用withCancel的输出如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2019/09/30 00:18:48 执行逻辑
2019/09/30 00:18:48 执行逻辑
2019/09/30 00:18:48 执行逻辑
2019/09/30 00:18:50 执行逻辑
2019/09/30 00:18:50 执行逻辑
2019/09/30 00:18:50 执行逻辑
2019/09/30 00:18:52 执行逻辑
2019/09/30 00:18:52 执行逻辑
2019/09/30 00:18:52 执行逻辑
可以了，通知子协程停止
2019/09/30 00:18:54 退出
2019/09/30 00:18:54 退出
2019/09/30 00:18:54 退出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，我们通过&lt;code&gt;context.WithCancel&lt;/code&gt;方法生成了一个ctx和一个cancel，然后主动调用cancel就可以通过所有的子协程退出了。在&lt;code&gt;watch&lt;/code&gt;方法的实现中，我们是通过&lt;code&gt;select&lt;/code&gt;机制来实现的，一旦context的&lt;code&gt;Done()&lt;/code&gt;方法有值，就会调用return退出，否则的话就执行&lt;code&gt;default&lt;/code&gt;中我们的业务逻辑。 这样我们就可以随时通知所有的子协程退出了。在上面说到，context最终形成了一种树形结构，是因为在子协程中也可以继续使用新的协程，这样就形成了一个树形的调用了。 &lt;img src=&quot;/uploads/wp/2019/09/context%E5%8D%8F%E7%A8%8B.png&quot; alt=&quot;协程的树形结构&quot; /&gt; 我们在1中使用cancel方法，就可以向下传播，在2~10号协程中全部退出。 简单的了解&lt;code&gt;context&lt;/code&gt;包的使用后，可以看一下&lt;code&gt;context.Context&lt;/code&gt;这个接口，为了简洁，我删除了源代码中的注释。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() &amp;lt;-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Context&lt;/code&gt;接口总共提供了4个方法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Deadline()&lt;/code&gt;用来获取当前context的取消时间，第二个返回值&lt;code&gt;ok&lt;/code&gt;等于false的时候，表示没有设置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Done()&lt;/code&gt;方法返回了一个&lt;code&gt;chan&lt;/code&gt;，当chan中读取到值的时候，表示父context已经发起了取消的请求，那么当前协程开始做相关的清理工作然后退出。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Err()&lt;/code&gt;返回context的取消原因&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Value()&lt;/code&gt;方法用来通过一个key获取当前Context上与之对应的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;理解Context的树形结构&lt;/h2&gt;
&lt;p&gt;go中大量的库都使用了context机制，比如&lt;code&gt;database/sql&lt;/code&gt;库，&lt;code&gt;net/http&lt;/code&gt;库等等，因为这些库都支持了context，使得我们在程序中很容易通过context来管理所有新建的协程，而不用自己实现复杂的机制来管理。一旦我们需要取消，只需要在root context调用cancel方法即可。&lt;/p&gt;
&lt;h2&gt;一些基本使用&lt;/h2&gt;
&lt;p&gt;在上面的例子中，我们使用了withCancel来实例化一个可以手动取消的context。context包中同样提供了一些其他的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;WithDeadline&lt;/code&gt;可以设置截止时间。会到达指定时间时自动取消。当然也可以调用&lt;code&gt;CancelFunc&lt;/code&gt;来手动取消。 &lt;code&gt;WithTimeout&lt;/code&gt;可以设置在一段时间后自动取消，和&lt;code&gt;WithDeadline&lt;/code&gt;类似。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.flysnow.org/2017/05/12/go-in-action-go-context.html&quot; rel=&quot;noopener&quot;&gt;Go语言实战笔记(二十)&lt;/a&gt; &lt;a href=&quot;https://juejin.im/post/5a6873fef265da3e317e55b6&quot; rel=&quot;noopener&quot;&gt;Golang Context深入理解&lt;/a&gt; &lt;a href=&quot;https://blog.golang.org/context&quot; rel=&quot;noopener&quot;&gt;Go Concurrency Patterns: context&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>go</category><category>go</category><category>context</category><author>joyme123</author></item><item><title>etcd分布式锁的实现方式</title><link>https://www.myway5.com/blog/etcd-distribute-lock/</link><guid isPermaLink="true">https://www.myway5.com/blog/etcd-distribute-lock/</guid><description>在etcd的clientv3包中，实现了分布式锁。使用起来和mutex是类似的，为了了解其中的工作机制，这里简要的做一下总结。 二、使用方式</description><pubDate>Wed, 09 Oct 2019 11:32:03 GMT</pubDate><content:encoded>&lt;h2&gt;一、概述&lt;/h2&gt;
&lt;p&gt;在etcd的clientv3包中，实现了分布式锁。使用起来和&lt;code&gt;mutex&lt;/code&gt;是类似的，为了了解其中的工作机制，这里简要的做一下总结。&lt;/p&gt;
&lt;h2&gt;二、使用方式&lt;/h2&gt;
&lt;p&gt;etcd分布式锁的实现在&lt;code&gt;go.etcd.io/etcd/clientv3/concurrency&lt;/code&gt;包中，主要提供了以下几个方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;func NewMutex(s *Session, pfx string) *Mutex， 用来新建一个mutex&lt;/li&gt;
&lt;li&gt;func (m *Mutex) Lock(ctx context.Context) error，它会阻塞直到拿到了锁，并且支持通过context来取消获取锁。&lt;/li&gt;
&lt;li&gt;func (m *Mutex) Unlock(ctx context.Context) error，解锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此在使用etcd提供的分布式锁式非常简单，通常就是实例化一个mutex，然后尝试抢占锁，之后进行业务处理，最后解锁即可。 一个简单的例子如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;context&quot;
    &quot;github.com/coreos/etcd/clientv3&quot;
    &quot;github.com/coreos/etcd/clientv3/concurrency&quot;
    &quot;log&quot;
    &quot;sync&quot;
    &quot;time&quot;
)

var n = 0

// 使用worker模拟锁的抢占
func worker(key string) error {
    endpoints := []string{&quot;127.0.0.1:2379&quot;}

    cfg := clientv3.Config{
        Endpoints:            endpoints,
        DialTimeout:          3 * time.Second,
    }

    cli, err := clientv3.New(cfg)
    if err != nil {
        log.Println(&quot;new cli error:&quot;, err)
        return err
    }

    sess, err := concurrency.NewSession(cli)
    if err != nil {
        return err
    }

    m := concurrency.NewMutex(sess, &quot;/&quot;+key)

    err = m.Lock(context.TODO())
    if err != nil {
        log.Println(&quot;lock error:&quot;, err)
        return err
    }

    defer func() {
        err = m.Unlock(context.TODO())
        if err != nil {
            log.Println(&quot;unlock error:&quot;, err)
        }
    }()

    log.Println(&quot;get lock: &quot;, n)
    n++
    time.Sleep(time.Second) // 模拟执行代码


    return nil
}

func main() {
    var wg sync.WaitGroup
    wg.Add(3)
    go func() {
        defer wg.Done()
        err := worker(&quot;lockname&quot;)
        if err != nil {
            log.Println(err)
        }
    }()


    go func() {
        defer wg.Done()
        err := worker(&quot;lockname&quot;)
        if err != nil {
            log.Println(err)
        }
    }()

    go func() {
        defer wg.Done()
        err := worker(&quot;lockname&quot;)
        if err != nil {
            log.Println(err)
        }
    }()

    wg.Wait()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、实现机制&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Lock()&lt;/code&gt;函数的实现很简单。这里可以贴出来看一下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Lock locks the mutex with a cancelable context. If the context is canceled
// while trying to acquire the lock, the mutex tries to clean its stale lock entry.
func (m *Mutex) Lock(ctx context.Context) error {
    s := m.s
    client := m.s.Client()

    m.myKey = fmt.Sprintf(&quot;%s%x&quot;, m.pfx, s.Lease())
    cmp := v3.Compare(v3.CreateRevision(m.myKey), &quot;=&quot;, 0)
    // put self in lock waiters via myKey; oldest waiter holds lock
    put := v3.OpPut(m.myKey, &quot;&quot;, v3.WithLease(s.Lease()))
    // reuse key in case this session already holds the lock
    get := v3.OpGet(m.myKey)
    // fetch current holder to complete uncontended path with only one RPC
    getOwner := v3.OpGet(m.pfx, v3.WithFirstCreate()...)
    resp, err := client.Txn(ctx).If(cmp).Then(put, getOwner).Else(get, getOwner).Commit()
    if err != nil {
        return err
    }
    m.myRev = resp.Header.Revision
    if !resp.Succeeded {
        m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
    }
    // if no key on prefix / the minimum rev is key, already hold the lock
    ownerKey := resp.Responses[1].GetResponseRange().Kvs
    if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
        m.hdr = resp.Header
        return nil
    }

    // wait for deletion revisions prior to myKey
    hdr, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
    // release lock key if wait failed
    if werr != nil {
        m.Unlock(client.Ctx())
    } else {
        m.hdr = hdr
    }
    return werr
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先通过一个事务来尝试加锁，这个事务主要包含了4个操作: &lt;code&gt;cmp&lt;/code&gt;、&lt;code&gt;put&lt;/code&gt;、&lt;code&gt;get&lt;/code&gt;、&lt;code&gt;getOwner&lt;/code&gt;。需要注意的是，key是由&lt;code&gt;pfx&lt;/code&gt;和&lt;code&gt;Lease()&lt;/code&gt;组成的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cmp: 比较加锁的key的修订版本是否是0。如果是0就代表这个锁不存在。&lt;/li&gt;
&lt;li&gt;put: 向加锁的key中存储一个空值，这个操作就是一个加锁的操作，但是这把锁是有超时时间的，超时的时间是session的默认时长。超时是为了防止锁没有被正常释放导致死锁。&lt;/li&gt;
&lt;li&gt;get: get就是通过key来查询&lt;/li&gt;
&lt;li&gt;getOwner: 注意这里是用&lt;code&gt;m.pfx&lt;/code&gt;来查询的，并且带了查询参数&lt;code&gt;WithFirstCreate()&lt;/code&gt;。使用&lt;code&gt;pfx&lt;/code&gt;来查询是因为其他的session也会用同样的&lt;code&gt;pfx&lt;/code&gt;来尝试加锁，并且因为每个LeaseID都不同，所以第一次肯定会&lt;code&gt;put&lt;/code&gt;成功。但是只有最早使用这个&lt;code&gt;pfx&lt;/code&gt;的&lt;code&gt;session&lt;/code&gt;才是持有锁的，所以这个getOwner的含义就是这样的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来才是通过判断来检查是否持有锁&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;m.myRev = resp.Header.Revision
if !resp.Succeeded {
    m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
}
// if no key on prefix / the minimum rev is key, already hold the lock
ownerKey := resp.Responses[1].GetResponseRange().Kvs
if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
    m.hdr = resp.Header
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;m.myRev&lt;/code&gt;是当前的版本号，&lt;code&gt;resp.Succeeded&lt;/code&gt;是&lt;code&gt;cmp&lt;/code&gt;为true时值为true，否则是false。这里的判断表明当同一个session非第一次尝试加锁，当前的版本号应该取这个key的最新的版本号。 下面是取得锁的持有者的key。如果当前没有人持有这把锁，那么默认当前会话获得了锁。或者锁持有者的版本号和当前的版本号一致， 那么当前的会话就是锁的持有者。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// wait for deletion revisions prior to myKey
hdr, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
// release lock key if wait failed
if werr != nil {
    m.Unlock(client.Ctx())
} else {
    m.hdr = hdr
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这段代码就很好理解了，因为走到这里说明没有获取到锁，那么这里等待锁的删除。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// waitDeletes efficiently waits until all keys matching the prefix and no greater
// than the create revision.
func waitDeletes(ctx context.Context, client *v3.Client, pfx string, maxCreateRev int64) (*pb.ResponseHeader, error) {
    getOpts := append(v3.WithLastCreate(), v3.WithMaxCreateRev(maxCreateRev))
    for {
        resp, err := client.Get(ctx, pfx, getOpts...)
        if err != nil {
            return nil, err
        }
        if len(resp.Kvs) == 0 {
            return resp.Header, nil
        }
        lastKey := string(resp.Kvs[0].Key)
        if err = waitDelete(ctx, client, lastKey, resp.Header.Revision); err != nil {
            return nil, err
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;waitDeletes&lt;/code&gt;方法的实现也很简单，但是需要注意的是，这里的&lt;code&gt;getOpts&lt;/code&gt;只会获取比当前会话版本号更低的key，然后去监控最新的key的删除。等这个key删除了，自己也就拿到锁了。 这种分布式锁的实现和我一开始的预想是不同的。它不存在锁的竞争，不存在重复的尝试加锁的操作。而是通过使用统一的前缀&lt;code&gt;pfx&lt;/code&gt;来put，然后根据各自的版本号来排队获取锁。效率非常的高。 &lt;img src=&quot;/uploads/wp/2019/10/etcd%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81.png&quot; alt=&quot;etcd 分布式锁&quot; /&gt; 如图所示，共有4个session来加锁，那么根据revision来排队，获取锁的顺序为session2 -&amp;gt; session3 -&amp;gt; session1 -&amp;gt; session4。 当然，这里为什么可以通过revision来判定获取锁的顺序，就需要更深入的了解etcd的内部机制以及raft协议了。&lt;/p&gt;
</content:encoded><category>分布式系统</category><category>etcd</category><category>etcd 分布式锁</category><author>joyme123</author></item><item><title>分布式文件上传方案</title><link>https://www.myway5.com/blog/file-upload-in-distributed-system/</link><guid isPermaLink="true">https://www.myway5.com/blog/file-upload-in-distributed-system/</guid><description>\-## 一、背景 考虑可扩展性，后台的服务肯定是要能够支持任意的扩展的，这样才能在业务量增长时通过增加机器的方式来应对。这对后台服务提出了一个要求，必须处理好分布式环境和单机环境的不同带来的问题。</description><pubDate>Fri, 27 Sep 2019 02:10:55 GMT</pubDate><content:encoded>&lt;p&gt;-## 一、背景 考虑可扩展性，后台的服务肯定是要能够支持任意的扩展的，这样才能在业务量增长时通过增加机器的方式来应对。这对后台服务提出了一个要求，必须处理好分布式环境和单机环境的不同带来的问题。比如：在文件的分片上传这一场景下，应该负载均衡的问题，一个文件的多个分片请求会分布到不同的服务器上，这导致在将多个分片合并成完整文件时出现问题，而单机情况下则完全不会有这样的问题。&lt;/p&gt;
&lt;h2&gt;二、难点&lt;/h2&gt;
&lt;p&gt;这个问题的解决方案有很多种，但是需要根据实际情况尽量选择简洁、易部署和维护的方案进行，并且不能丢掉分布式系统的优点。比如网上的有的方案是使用单独的文件上传服务器，但是这就变成了单机服务了。也有使用NFS挂载的方案，即所有的服务器挂载一个相同的NFS目录，所有上传相关的文件都存放在挂载的目录下，这不仅给运维带来了麻烦，为了保证NFS的高可用，也带来了额外的运维成本。&lt;/p&gt;
&lt;h2&gt;三、解决方案&lt;/h2&gt;
&lt;h3&gt;3.1 借助负载均衡&lt;/h3&gt;
&lt;p&gt;借助负载均衡的方案很简单，这是和应用无关的一种方法。即通过负载均衡这一层，将同一个文件的不同分片的请求全部导向到同一个服务器上。比如负载均衡这一块使用的是nginx, 通过&lt;code&gt;url hash&lt;/code&gt;的方式来完成，可以使用如下的配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;upstream backend {
    server 0.0.0.0:8080;
    server 0.0.0.0:8081;
    server 0.0.0.0:8082;
    hash $request_uri;
}
server {
    location / {
        proxy_pass http://backend;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后写一个简单的服务来验证一下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func post(resp http.ResponseWriter, req *http.Request) {
    v := req.PostFormValue(&quot;key&quot;)
    identity := req.PostFormValue(&quot;identity&quot;)
    log.Printf(&quot;identity: %v, value: %v\n&quot;, identity, v)

    resp.WriteHeader(http.StatusOK)
    _, err := resp.Write([]byte(&quot;ok&quot;))
    if err != nil {
        log.Println(&quot;error:&quot;, err)
    }
}

func main() {
    var addr string
    flag.StringVar(&amp;amp;addr, &quot;addr&quot;, &quot;0.0.0.0:8080&quot;, &quot;http listen addr:port&quot;)
    flag.Parse()

    http.HandleFunc(&quot;/Post&quot;, post)
    log.Println(&quot;listen &quot;, addr)
    err := http.ListenAndServe(addr, nil)
    if err != nil {
        log.Fatal(err)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们启动三个服务:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./godemo -addr 0.0.0.0:8080
./godemo -addr 0.0.0.0:8081
./godemo -addr 0.0.0.0:8082
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用curl来post数据过来，请求的地址类似于: &lt;code&gt;http://0.0.0.0/Post?123456&lt;/code&gt;。&lt;code&gt;?&lt;/code&gt;后面可以当成是文件的唯一标识码，比如文件和用户id的组合的md5信息等，这样对于同一个用户上传的同一个文件的不同分片请求，通过url哈希都会得到同样的结果，这样就会被转发到同一个后端服务器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl http://0.0.0.0/Post\?123456 -d &quot;key=value&amp;amp;identity=123456&quot; -X POST
curl http://0.0.0.0/Post\?123456 -d &quot;key=value&amp;amp;identity=123456&quot; -X POST
curl http://0.0.0.0/Post\?123456 -d &quot;key=value&amp;amp;identity=123456&quot; -X POST
curl http://0.0.0.0/Post\?789abc -d &quot;key=vvvvv&amp;amp;identity=789abc&quot; -X POST
curl http://0.0.0.0/Post\?789abc -d &quot;key=vvvvv&amp;amp;identity=789abc&quot; -X POST
curl http://0.0.0.0/Post\?789abc -d &quot;key=vvvvv&amp;amp;identity=789abc&quot; -X POST
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;端口8080的服务收到了三条请求:
2019/09/26 13:58:52 identity: 123456, value: value
2019/09/26 13:58:56 identity: 123456, value: value
2019/09/26 13:59:03 identity: 123456, value: value

端口8081的服务收到了三条请求:
2019/09/26 13:59:40 identity: 789abc, value: vvvvv
2019/09/26 13:59:43 identity: 789abc, value: vvvvv
2019/09/26 13:59:44 identity: 789abc, value: vvvvv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在k8s集群中部署，使用&lt;code&gt;nginx-ingress&lt;/code&gt;的话可以使用以下的部署方案：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
    name: pipeline-ingress
    namespace: default
    annotations:
      nginx.ingress.kubernetes.io/proxy-body-size: &quot;50m&quot;
      nginx.ingress.kubernetes.io/upstream-hash-by: &quot;$request_uri&quot;
spec:
    rules:
        - host: pipeline.dev.com
          http:
              paths:
                  - path: / 
                    backend:
                        serviceName: pipeline-service
                        servicePort: 8888

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然这种方式需要注意你的每个请求的&lt;code&gt;URI&lt;/code&gt;都需要加上额外的参数（比如用户的userId的md5)，这样才能均匀的分布到不同的服务器上。&lt;/p&gt;
&lt;h2&gt;3.2 后台程序自动proxy请求&lt;/h2&gt;
&lt;p&gt;这个方案的思路是集群中的每个服务实例都有单独的标识，文件分片在第一次上传时会返回给它一个该请求所属服务器的identity，之后所有的请求会带上这个identity，之后收到请求的服务器会检查这个identity是不是属于自己，如果不属于自己，则把这个请求转发给所属的服务器。 这个方案要求每台服务器都知道其他所有服务器的identity和地址。这里我们可以使用&lt;code&gt;etcd&lt;/code&gt;这样的分布式数据库来存储。每台服务器在启动时都向etcd里注册自己的identity和address，之后服务器转发的时候都向etcd里面查找对应的address即可。 下面是一个示例的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
    &quot;context&quot;
    &quot;encoding/json&quot;
    &quot;flag&quot;
    &quot;io/ioutil&quot;
    &quot;log&quot;
    &quot;net/http&quot;
    &quot;go.etcd.io/etcd/clientv3&quot;
    &quot;net/url&quot;
    &quot;time&quot;
)

type Server struct {
    Etcd string
    Addr string
    Identify string
}

type ResponseObj struct {
    Identity string `json:&quot;identity,omitempty&quot;`
    Value string `json:&quot;value&quot;`
}

func getEtcdKV(etcd string) (clientv3.KV, error) {
    cfg := clientv3.Config{
        Endpoints:               []string{etcd},
        // set timeout per request to fail fast when the target endpoint is unavailable
        DialTimeout: time.Second,
    }

    cli, err := clientv3.New(cfg)

    if err != nil {
        return nil, err
    }

    return clientv3.NewKV(cli), nil
}

func httpProxy(anotherServer string, body map[string]string) (*http.Response, error) {
    formData := url.Values{}

    for k, v := range body {
        formData.Set(k ,v)
    }

    return http.PostForm(anotherServer, formData)
}

func (s *Server) Register() error {
    cli, err := getEtcdKV(s.Etcd)
    if err != nil {
        return err
    }

    ctx, cancel := context.WithTimeout(context.Background(), time.Duration(1)*time.Second)
    _, err = cli.Put(ctx, s.Identify, &quot;http://&quot; + s.Addr)
    cancel()
    if err != nil {
        return err
    }

    return nil
}

func (s *Server) Post(resp http.ResponseWriter, req *http.Request) {
    v := req.PostFormValue(&quot;key&quot;)
    identity := req.PostFormValue(&quot;identity&quot;)
    log.Printf(&quot;identity: %v, value: %v\n&quot;, identity, v)

    var data ResponseObj

    if identity != &quot;&quot; &amp;amp;&amp;amp; identity != s.Identify {
        // proxy
        cli, err := getEtcdKV(s.Etcd)
        if err != nil {
            log.Println(&quot;get kv client error&quot;, err)
            resp.WriteHeader(http.StatusInternalServerError)
            resp.Write([]byte(&quot;error&quot;))
            return
        }

        etcdResp, err := cli.Get(context.Background(), identity)
        if err != nil {
            log.Println(&quot;get value error: &quot;, err)
            resp.WriteHeader(http.StatusInternalServerError)
            resp.Write([]byte(&quot;error&quot;))
            return
        }

        if len(etcdResp.Kvs) == 0 {
            log.Println(&quot;没有值&quot;)
            resp.WriteHeader(http.StatusInternalServerError)
            resp.Write([]byte(&quot;error&quot;))
            return
        }

        proxyAddr := string(etcdResp.Kvs[0].Value)

        log.Println(&quot;proxy to: &quot;, proxyAddr)

        text := map[string]string {
            &quot;key&quot;: v,
            &quot;identity&quot;: identity,
        }

        proxyResp, err := httpProxy(proxyAddr+&quot;/Post&quot;, text)
        if err != nil || proxyResp.Body == nil {
            log.Println(&quot;http proxy error: &quot;, err)
            resp.WriteHeader(http.StatusInternalServerError)
            resp.Write([]byte(&quot;error&quot;))
            return
        }

        bodyData, err := ioutil.ReadAll(proxyResp.Body)
        if err != nil {
            log.Println(&quot;read proxy body error: &quot;, err)
            resp.WriteHeader(http.StatusInternalServerError)
            resp.Write([]byte(&quot;error&quot;))
            return
        }

        err = json.Unmarshal(bodyData, &amp;amp;data)
        if err != nil {
            log.Println(&quot;json unmarshal error: &quot;, err)
            resp.WriteHeader(http.StatusInternalServerError)
            resp.Write([]byte(&quot;error&quot;))
            return
        }
    } else {
        data.Identity = s.Identify
        data.Value = v
    }

    text, err := json.Marshal(data)
    if err != nil {
        log.Println(&quot;marshal json error: &quot;, err)
        resp.WriteHeader(http.StatusInternalServerError)
        return
    }

    resp.WriteHeader(http.StatusOK)
    _, err = resp.Write(text)
    if err != nil {
        log.Println(&quot;error:&quot;, err)
    }
}

func main() {
    var addr string
    var identity string
    var etcd string
    flag.StringVar(&amp;amp;addr, &quot;addr&quot;, &quot;0.0.0.0:8080&quot;, &quot;http listen addr:port&quot;)
    flag.StringVar(&amp;amp;identity, &quot;identity&quot;, &quot;&quot;, &quot;identify the server&quot;)
    flag.StringVar(&amp;amp;etcd, &quot;etcd&quot;, &quot;http://127.0.0.1:2379&quot;, &quot;etcd server url&quot;)
    flag.Parse()

    if identity == &quot;&quot; {
        log.Fatal(&quot;identity不可为空&quot;)
    }

    var server = &amp;amp;Server{
        Addr:     addr,
        Identify: identity,
        Etcd: etcd,
    }

    err := server.Register()
    if err != nil {
        log.Fatal(&quot;register server error, check your etcd server: &quot;, err)
    }

    http.HandleFunc(&quot;/Post&quot;, server.Post)
    log.Println(&quot;listen &quot;, addr)
    err = http.ListenAndServe(addr, nil)
    if err != nil {
        log.Fatal(err)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3.3 基于S3存储的方案&lt;/h2&gt;
&lt;p&gt;兼容S3的存储系统有一个对外的接口叫做: &lt;code&gt;ComposeObject&lt;/code&gt;。这个接口可以将多个文件合并成一个文件存储到指定位置。如果我们的底层存储是基于兼容S3的系统，那么就可以利用这个接口轻松的实现。 方案的步骤可以描述成以下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;前端实现将文件分片，然后上传&lt;/li&gt;
&lt;li&gt;上传的请求会因为前端负载均衡的原因分布到不同的服务器，每个上传请求都要带上这个文件、用户id、随机字符串组合的md5值，服务器收到请求后，只管将分片上传到以md5值为名的目录下，一旦上传成功，使用etcd或redis这样作为分片计数器+1.&lt;/li&gt;
&lt;li&gt;分片总数等于总分片数时调用&lt;code&gt;ComposeObject&lt;/code&gt;组合所有分片存储到指定位置即可。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>分布式系统</category><author>joyme123</author></item><item><title>ceph架构研究</title><link>https://www.myway5.com/blog/ceph/</link><guid isPermaLink="true">https://www.myway5.com/blog/ceph/</guid><description>这篇文章的绝大多数内容都是官网原文的翻译：Ceph Architecture ceph是什么</description><pubDate>Tue, 24 Sep 2019 03:06:52 GMT</pubDate><content:encoded>&lt;p&gt;这篇文章的绝大多数内容都是官网原文的翻译：&lt;a href=&quot;https://docs.ceph.com/docs/master/architecture/&quot; rel=&quot;noopener&quot;&gt;Ceph Architecture&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;ceph是什么&lt;/h2&gt;
&lt;p&gt;ceph是一个分布式的文件存储系统，它提供了以下三种存储能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;块设备存储(block device)&lt;/li&gt;
&lt;li&gt;文件系统存储(filesystem)&lt;/li&gt;
&lt;li&gt;对象存储(object storage)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此如果你需要多种存储方案的话，ceph是一个非常好的选择。这几种存储方案的差别是什么呢？ &lt;strong&gt;块设备存储&lt;/strong&gt;是将底层的存储能力以逻辑硬盘的方式暴露给主机使用。主机只感知到单块物理硬盘的挂载，底层的复杂机制都被屏蔽 &lt;strong&gt;对象存储&lt;/strong&gt;是将文件抽象成对象，然后通过网络接口(比如s3的api)来对文件进行操作(put,get,delete等)。 &lt;strong&gt;文件系统存储&lt;/strong&gt;是像FTP，NFS这种，可以将ceph的存储能力当成普通的文件系统来使用，也支持cd, ls这样的文件系统命令。 &lt;img src=&quot;/uploads/wp/2019/09/ceph-filesystem.png&quot; alt=&quot;ceph filesystem&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;ceph 架构一览&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2019/09/ceph-arch.png&quot; alt=&quot;ceph arch&quot; /&gt; 通过上图可以看出，ceph的底层核心是&lt;code&gt;RADOS&lt;/code&gt;，然后通过&lt;code&gt;LIBRADOS&lt;/code&gt;和&lt;code&gt;CEPHFS&lt;/code&gt;以及基于LIBRADOS的&lt;code&gt;RADOSGW&lt;/code&gt;, &lt;code&gt;RBD&lt;/code&gt;对外提供存储功能。&lt;/p&gt;
&lt;h2&gt;ceph 存储集群&lt;/h2&gt;
&lt;p&gt;ceph基于&lt;code&gt;RADOS&lt;/code&gt;提供了无限的可扩展的存储集群。关于RADOS的知识可以看这篇论文： &lt;a href=&quot;/uploads/wp/2016/08/weil-rados-pdsw07.pdf&quot; rel=&quot;noopener&quot;&gt;RADOS - A Scalable, Reliable Storage Service for Petabyte-scale Storage Clusters&lt;/a&gt; 一个Ceph存储集群包括两类常驻服务:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ceph Monitor&lt;/li&gt;
&lt;li&gt;Ceph OSD Daemon&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2019/09/ceph-daemons.png&quot; alt=&quot;ceph daemons&quot; /&gt; Ceph Monitor维护了一个关于集群信息映射的主副本。Ceph monitors集群保证了在一个monitor服务停止时的高可用性。存储集群客户端从Ceph Monitor获取一份集群信息映射的拷贝。 Ceph OSD服务负责检查自身的状态以及其他OSD的状态，然后汇报给monitors。 存储集群客户端和每一个Ceph OSD服务使用&lt;code&gt;CRUSH&lt;/code&gt;算法来高效的计算数据位置的信息，而不是依赖于一个中心化的查找表。librados是一个很重要的角色，它提供了Ceph的高层特性，一系列的服务接口都是建设在librados上层的。&lt;/p&gt;
&lt;h3&gt;存储数据&lt;/h3&gt;
&lt;p&gt;Ceph存储集群从Ceph客户端接受数据，这里的客户端可以是Ceph块设备存储，对象存储，文件系统或者是基于librados的自定义实现。librados将数据作为一个对象存储。每一个对象都在文件系统中对应一个文件，这些文件存储在对象存储设备上。Ceph OSD服务在存储磁盘上处理这些读写操作。 &lt;img src=&quot;/uploads/wp/2019/09/osd_handle_rw_op.png&quot; alt=&quot;osd handle read/write operations&quot; /&gt; Ceph OSD服务在一个扁平的命名空间中(没有目录结构)存储所有的数据,每个数据都作为一个对象。一个对象有唯一标识符，二进制数据，以及有一系列键值对的元数据。这完全取决于Ceph客户端的实现。举例来说，CephFS使用元数据来存储文件属性，比如文件拥有者，创建日期，最后修改日期等等。 &lt;img src=&quot;/uploads/wp/2019/09/object.png&quot; alt=&quot;ceph object&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意: 对象ID是在整个集群中唯一的，而不仅仅是本地文件系统。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;扩展性和高可用&lt;/h3&gt;
&lt;p&gt;在传统架构中，客户端和一个中心化的组件(比如gateway, broker, API, facade等)通信。这个中心化组件在一个复杂系统中扮演了一个单点的入口。这在性能和扩展性上都导致了局限性————单点失败。（比如，如果中心组件宕机，整个集群都不可用）。 Ceph消灭了这个中心化的入口，让客户端直接和Ceph OSD服务通信。Ceph OSD服务在其他Ceph节点上创建对象副本来保证数据安全和高可用。Ceph同样使用一个monitor的集群来保证高可用。为了消灭中心化，Ceph使用了一个叫做&lt;code&gt;CRUSH&lt;/code&gt;的算法。&lt;/p&gt;
&lt;h3&gt;CRUSH介绍&lt;/h3&gt;
&lt;p&gt;Ceph客户端和Ceph OSD服务都使用&lt;code&gt;CRUSH&lt;/code&gt;算法来高效的计算对象存储位置信息，而不是必须取决于一个中心化的查找表。相比于之前的方案，&lt;code&gt;CRUSH&lt;/code&gt;提供了一个更好的数据管理机制，通过清晰的将工作分布到集群中的所有客户端和OSD服务，可以达到很好的扩展性。&lt;code&gt;CRUSH&lt;/code&gt;使用智能的数据复制来保证灵活性，这更适用于超大规模的存储。&lt;/p&gt;
&lt;h3&gt;集群映射表&lt;/h3&gt;
&lt;p&gt;Ceph依赖于Ceph客户端和OSD服务，它们包含了5个映射表。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;监控映射表&lt;/strong&gt;: 包含集群的fsid， 位置, 名字地址以及每个monitor的端口。它同样记录了当前的代数，映射表的创建时间，最后一次修改时间。可以使用&lt;code&gt;ceph mon dump&lt;/code&gt;来查看一个监控表&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OSD映射表&lt;/strong&gt;: 包含集群的fsid, 这个映射表的创建时间和最后修改时间，pools的列表，副本数量，PG数量，OSD的列表以及他们的状态。使用&lt;code&gt;ceph osd dump&lt;/code&gt;来查看osd表。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PG映射表&lt;/strong&gt;: 包含PG版本号, 时间戳，最新的OSD映射表的代数，全比率，每一个放置组的详情，比如PG ID, Up Set, Acting Set,以及PG的状态(比如active + clean)，每一个pool的数据使用统计信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CRUSH映射表&lt;/strong&gt;: 包含存储设备的列表，失败域的层级（比如device,host,rack,row,room等)，存储数据时的遍历层级的规则。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MDS映射表&lt;/strong&gt;: 包含当前的MDS映射表的代数， 映射表的创建时间，最后修改时间。它同样包含存储元数据的池，元数据服务器的列表，那些元数据服务器是up和in的。执行&lt;code&gt;ceph fs dump&lt;/code&gt;来查看MDS映射表。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每一个映射表都包含它的操作状态改变的可迭代历史。Ceph Monitors维护一个集群映射表的主拷贝，包含了集群成员，状态，改变，以及Ceph存储集群的全局健康状态。&lt;/p&gt;
&lt;h3&gt;高可用的监控服务&lt;/h3&gt;
&lt;p&gt;在Ceph客户端可以读或写数据前，它们都必须和一个Ceph监控服务通信来获取最新的集群映射表的拷贝。一个Ceph存储集群可以只有一个Monitor角色。当然，这会导致单点失败。 为了增加可靠性和错误容忍，Ceph支持监控服务的集群。在一个监控服务的集群中，延迟和其他错误会导致一个或多个监控服务落后于集群的当前状态。因此，Ceph必须在各个监控服务实例之间就集群状态达成一致。Ceph总是相信大多数的监控服务。Paxos算法被用来就当前集群状态达成共识。&lt;/p&gt;
&lt;h3&gt;高可用的认证&lt;/h3&gt;
&lt;p&gt;为了鉴别用户以及防止中间人攻击，Ceph提供了&lt;code&gt;cephx&lt;/code&gt;认证系统来认证用户和后台服务。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;cephx协议不致力于传输过程中数据加密(比如SSL/TLS)或者是其他的加密。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Cephx使用共享的秘钥来认证，这意味着客户端和监控集群都有一份客户端秘钥的拷贝。认证协议需要双方都能证明给对方自己拥有这个秘钥的拷贝，但是又不能把秘钥原文说出来。这提供了两端的验证，意味着集群确信用户有这个秘钥，用户也确信集群有这个秘钥。 Ceph的一个关键的可扩展的特性是避免对Ceph object store的实现有中心化接口，这意味着Ceph客户端必须能够和OSD直接通信。为了保护数据，Ceph提供了&lt;code&gt;cephx&lt;/code&gt;认证系统。&lt;code&gt;cephx&lt;/code&gt;协议的机制和&lt;code&gt;Kerberos&lt;/code&gt;类似。 用户调用Ceph客户端来和监控服务器通信。和Kerberos不同的是，每一个监控服务都可以验证用户以及分发私钥，所以在使用&lt;code&gt;cephx&lt;/code&gt;时不再有单点失败或瓶颈了。监控服务返回一个认证数据结构，类似于Kerberos ticket，包含获取Ceph服务的会话秘钥。会话秘钥被用户永久秘钥加密过，因此只有用户可以向Ceph监控服务请求服务。之后客户端使用会话秘钥来请求它想要的服务，监控服务向客户端提供一个ticket，这个ticket可以向OSD认证客户端来处理数据。 Ceph Monitors和OSD共享秘钥，因此客户端可以使用监控服务提供的ticket和任意的OSD或元数据服务器通信。和Kerberos一样，cephx ticket会过期，因此攻击者不能使用过期的ticket或会话秘钥来获取服务。 为了使用cephx，管理员必须首先设置用户。在下图中，client.admin这个用户从命令行中调用&lt;code&gt;ceph auth get-or-create-key&lt;/code&gt;来生成用户名和秘钥。Ceph的认证系统生成用户名和秘钥，存储一份拷贝给所有的监控服务，然后将用户秘钥传送给client.admin用户。这意味着客户端和监控服务共享同一个秘钥。 &lt;img src=&quot;/uploads/wp/2019/09/request-create-user.png&quot; alt=&quot;request create user&quot; /&gt; 为了通过监控服务的认证，客户端把用户名发送给监控服务，监控服务生成一个会话钥匙，使用秘钥附加上用户名来加密。之后，监控服务把加密后的ticket返回给客户端。客户端使用共享的秘钥解密，获取到会话钥匙。会话钥匙为当前的会话提供认证。客户端之后请求由这个会话钥匙签名的ticket。监控服务生成ticket，使用用户秘钥加密并返回给客户端。客户端解密ticket，使用它来签名给OSD和元数据服务器的请求。 &lt;img src=&quot;/uploads/wp/2019/09/client-to-osd.png&quot; alt=&quot;client to osd&quot; /&gt; cephx协议认证每个在客户端机器和Ceph服务器之间的会话。每一个在客户端和服务器之间发送的信息，都会接连通过初始化认证，使用ticket签名，这个签名可以由监控服务，OSD和元数据服务器用他们共享的秘钥来验证。 &lt;img src=&quot;/uploads/wp/2019/09/complete-process.png&quot; alt=&quot;complete process&quot; /&gt; 由这种认证提供的保护是在Ceph客户端和Ceph服务端之间的。在Ceph客户端之外这个认证是不管用的。如果用户通过远程连接到Ceph客户端上，Ceph的认证不会应用到这个远程连接上。&lt;/p&gt;
&lt;h3&gt;智能的后台服务造就了超大规模的可扩展性&lt;/h3&gt;
&lt;p&gt;在很多集群架构中，集群成员的主要目的是通过一个中心化的接口知道哪些节点可以访问。然后中心化的接口通过二次转发来提供服务，这回导致在pb级别向eb级别扩展时有很大的瓶颈。 Ceph消灭了这个瓶颈：Ceph的OSD服务和Ceph客户端是集群感知的。比如Ceph客户端、Ceph OSD服务都知道集群中的其他Ceph OSD服务。这使得Ceph OSD服务可以直接和其他Ceph OSD服务以及Ceph监控服务通信。另外，它使得Ceph客户端可以直接和Ceph OSD服务直接通信。 这种方法也最大化的利用了节点的CPU和RAM, 带来了以下几个主要的好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;OSD 为客户端直接提供服务：因为任何网络设备都有最大并发连接的瓶颈，一个中心化的系统在高扩展的情况下会出现一个很低的物理瓶颈。通过让Ceph客户端直接和Ceph OSD服务通信, Ceph同时增加了性能和系统容量，并且解决了单点失败的问题。Ceph客户端可以维护一个和Ceph OSD服务的会话，而不是一个中心化的系统，只要它们需要这样做。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OSD成员和状态：Ceph OSD服务加入到集群中并且汇报它们的状态。在最低级别上，Ceph OSD服务状态是up或者down的，代表着它是否能够处理Ceph客户端的请求。如果Ceph OSD服务是down的，这意味着Ceph OSD服务的异常。如果Ceph OSD服务不在运行(crash了)，Ceph OSD服务就不能通知Ceph监控服务它停止运行了。OSD服务周期性的给Ceph监控服务发送消息，如果Ceph监控服务在约定的周期内没有收到OSD服务的消息，它就把这个OSD服务标记为down。这种机制叫做failsafe(failsafe可以理解可以故障的自动保护装置，当OSD出现故障会自动将其排除从而避免引发更大的故障)。当然，OSD服务也会查看它相邻的OSD服务是否停止，然后汇报给Ceph监控服务。这保证了Ceph监控服务是一个轻量级的进程（脏活累活都被OSD服务做了）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据清理：作为维护数据一致性和清洁度的一部分，Ceph OSD服务可以在PG(placement group, 放置组)中清理对象。Ceph OSD服务可以将一个PG中的对象和存储在另一个OSD服务中的副本进行元数据的对比。清理操作(通常是每天)会找出bug或文件系统的错误。Ceph OSD服务同样会执行更深层次的清理，通过对对象执行按位的对比。深层次的清理(通常是每周)会找到硬盘上坏的扇区。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据复制：像Ceph客户端一样，Ceph OSD服务使用CRUSH算法，但是Ceph OSD服务使用它来计算对象的副本应该存储在哪里（或者是重新负载均衡）。在一个典型的写操作场景下，一个客户端使用CRUSH算法来计算对象存储在哪里，将这个对象映射到pool和PG中，然后查看CRUSH映射表来鉴别PG组的主OSD服务 客户端将对象写入到主OSD服务的PG组中。之后，主OSD携带着他自己的CRUSH映射表的拷贝存储到第二和第三个OSD服务中，这是为了多副本的备份。一旦它确认数据存储成功就返回给客户端。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2019/09/osd-write-for-replication.png&quot; alt=&quot;osd write for replication&quot; /&gt; 因为有这样的数据复制的能力，Ceph OSD服务使得Ceph客户端不需要负责这件事，同时也能保证高的数据可用性和数据安全。&lt;/p&gt;
&lt;h2&gt;动态集群管理&lt;/h2&gt;
&lt;p&gt;在高可用和可扩展性章节，我们解释了Ceph如何使用CRUSH、集群感知、智能服务扩展以及维护高可用。Ceph的关键设计就是自治、自我恢复、智能Ceph OSD服务。下面我们更深入的探究一下CRUSH如何在现代化的云存储架构中发挥作用，包括存储数据、集群的重新负载均衡以及动态的从错误中恢复。&lt;/p&gt;
&lt;h3&gt;关于pool&lt;/h3&gt;
&lt;p&gt;Ceph存储系统支持一个叫做&lt;code&gt;pool&lt;/code&gt;的概念，这是存储对象的逻辑分区。 Ceph客户端从Ceph监控服务中获取集群映射表，然后向pool中写入对象。pool的大小或副本数量、CRUSH的规则以及PG的数量决定了Ceph如何存储数据。 &lt;img src=&quot;/uploads/wp/2019/09/how-crush-work.png&quot; alt=&quot;how crush work&quot; /&gt; pool至少会设置以下参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象的拥有者/访问权限&lt;/li&gt;
&lt;li&gt;PG的数量&lt;/li&gt;
&lt;li&gt;使用的CRUSH规则&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;映射PG到OSD&lt;/h3&gt;
&lt;p&gt;每一个pool都有一系列的PG。CRUSH动态的将PG映射到OSD中。当Ceph客户端存储对象，CRUSH将会映射每一个对象到一个PG中。 将对象映射到PG创建了一个中间层，这个中间层位于Ceph OSD服务和Ceph客户端中间。在动态的存储对象时，Ceph存储系统必须能够增长（或收缩）以及重新负载均衡。如果Ceph客户端知道哪个Ceph OSD服务拥有哪一个对象，这就导致Ceph客户端和Ceph OSD服务紧耦合了。相反的，CRUSH算法将每一个对象映射到PG，然后将每一个PG映射到一个或多个OSD服务。这个中间层允许在有新的OSD服务以及新的底层OSD设备上线时，Ceph可以动态重新负载均衡。下面的图表演示了CRUSH如何映射对象到PG，然后映射PG到OSD。 &lt;img src=&quot;/uploads/wp/2019/09/crush-mapping.png&quot; alt=&quot;crush mapping&quot; /&gt; 客户端在有集群映射表的拷贝和CRUSH算法的情况下，可以准确的计算出在读取或写入对象时使用哪一个OSD。&lt;/p&gt;
&lt;h3&gt;计算PG ID&lt;/h3&gt;
&lt;p&gt;当一个Ceph客户端绑定到一个Ceph监控服务时，它会获取集群映射表的最新拷贝。有了这个集群映射表，客户端知道所有的监控服务，OSD，元数据服务。然而，它并不知道任何关于对象的位置信息。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对象位置需要计算&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;客户端唯一需要的输入是对象ID和pool。这很简单：Ceph在命名的pool中存储数据。当客户端存储一个命名的对象时，它使用对象名、一个哈希码、pool中PG的数量和pool名来计算PG。Ceph客户端使用以下步骤来计算PG ID。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1.客户端输入pool名和对象ID(比如: pool=&quot;liverpool&quot;, object-id=&quot;john&quot;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2.Ceph获取对象ID，然后做一下哈希操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3.Ceph计算哈希值除PG数量的余数，(比如58)得到了PG ID&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;4.Ceph获取给定的pool名的pool ID(比如&quot;liverpool&quot;=4)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;5.Ceph将pool ID放在PG ID之前(比如4.58)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在一个忙碌的会话中，计算对象位置要比执行对象位置查询快很多。CRUSH算法允许客户端计算对象应该存在哪里，这使得客户端可以直接和主OSD服务通信，存储或获取对象。&lt;/p&gt;
&lt;h3&gt;peering and sets&lt;/h3&gt;
&lt;p&gt;在之前的章节中，我们提到Ceph OSD服务检查彼此的心跳，然后汇报给Ceph监控服务。Ceph OSD做的另外一件事叫做&quot;窥探(peering)&quot;，这会让所有存储PG的OSD对PG中的对象状态达成一致。事实上，Ceph OSD服务也会汇报&quot;peering&quot;的失败给Ceph的监控服务。Peering的问题通常会自我解决。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对状态达成一致并不意味着PG有最新的内容&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Ceph存储集群被设置成一个对象至少有两个副本(比如, size=2)，这是数据安全的最小要求。为了高可用，Ceph存储集群应该存储多于两个的副本(比如size=3并且最小size=2)。这样它就能在维护数据安全的时候以一个降级状态继续运行。&lt;/p&gt;
&lt;h3&gt;重新负载均衡&lt;/h3&gt;
&lt;p&gt;当你添加一个Ceph OSD服务到集群中，集群映射表会随之更新。根据之前的计算PG ID的方法，这会改变集群映射表。于是，它改变了对象的位置，因为它改变了计算的一个输入参数。下面的图标描述了重新负载均衡的过程。有一些但不是所有的PG会从已存在的OSD（OSD1和OSD2)中迁移到新的OSD(OSD3)中（这个过程虽然看似很简单粗暴，但其实对大型系统影响很小）。即使在重新负载均衡的过程中，CRUSH仍然是稳定可靠的。大多数PG保留它们原有的配置，每一个OSD获取到更多的可用容量，所以在重新负载均衡之后新的OSD没有负载峰值。 &lt;img src=&quot;/uploads/wp/2019/09/ceph-rebalance.png&quot; alt=&quot;ceph rebalance&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;数据一致性&lt;/h3&gt;
&lt;p&gt;作为维护数据一致性和清洁度的一部分，Ceph OSD同样可以在PG中清理对象。关于OSD如何维持各个副本的对象一致性，可以参考上面的数据清理。&lt;/p&gt;
&lt;h3&gt;纠删码(erasure coding)&lt;/h3&gt;
&lt;p&gt;一个纠删码pool将每个对象分成K+M个块。数据被分为K个数据块和M个编码块。这个pool被配置成K+M的大小，因此每个块都可以存在一个OSD上。块的排序作为对象的属性存储起来。 举例来说，一个使用5个OSD(K+M=5)的纠删码pool可以容忍2块数据的丢失(M=2)。&lt;/p&gt;
&lt;h3&gt;读写编码块&lt;/h3&gt;
&lt;p&gt;当一个叫做NYAN的对象包含&quot;ABCDEFGHI&quot;的内容被写入到pool中，擦除编码功能将内容分成3个数据块：第一部分是ABC，第二部分是DEF，第三部分是GHI。如果内容长度不是K的整数倍，会被填充内容。这个功能同样创建两个编码块：第4个是YXY，第5个是QGC。每一个块都被存储到一个OSD中。这些块以同样的名字(NYAN)以对象的形式存储在不同的OSD上，块的创建顺序必须保留，存储对象的属性中(shard_t)，作为名字的额外补充。块1包含ABC，存储在OSD5中，块4包含YXY存储在OSD3中。 &lt;img src=&quot;/uploads/wp/2019/09/erasure-conding.png&quot; alt=&quot;erasure conding&quot; /&gt; 当对象NYAN从纠删码pool中读取时，解码功能读取三个块: 块1包含ABC，块3包含GHI，块4包含YXY。然后它重建了对象的原始内容ABCDEFGHI。这个解码功能被通知块2和块5是缺失的（它们被称作&apos;擦除&apos;）。块5不会被读取是因为OSD4从集群中退出了。一旦3个块被读取解码功能就会被调用，而此时OSD2因为最慢所以根本没有被考虑进来。 &lt;img src=&quot;/uploads/wp/2019/09/erasure-decode.png&quot; alt=&quot;erasure decode&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;中断完整的写入&lt;/h3&gt;
&lt;p&gt;在一个纠删码pool中，主OSD接受所有的写操作。它负责编码内容为K+M块，然后将它们发送给其他的OSD。它同样负责维护PG日志的版本号。 在下面的图表中，一个纠删码的PG被创建成K=2 M=1，它支持3个OSD，两个存储K，一个存储M，分别称为OSD1、OSD2、OSD3。一个对象被编码以及存储在OSD中，块D1v1(数据块序号1，版本1)在OSD1上，块D2v1在OSD2上，C1v1(编码块序号1,版本1)在OSD3上。每一个OSD上的PG日志都是唯一的(1,1 是epoch 1. version 1) &lt;img src=&quot;/uploads/wp/2019/09/interrupted-full-writes.png&quot; alt=&quot;interrupted-fill-writes&quot; /&gt; OSD1是主服务，接受客户端的完整的写入，这意味着写入的数据要完整的替换对象而不是覆盖一部分。版本2(v2)的对象被创建来覆盖版本1(v1)。OSD1将数据编码到3块: D1v2在OSD1上，D2v2在OSD2上，C1v2在OSD3上。每一个块都被发送到目标OSD上。当OSD接受到消息要写入数据块时，它同样创建一个新的PG日志来反映这个改变。举例来说，只要OSD3存储了C1V2，它添加1,2到日志中。因为OSD是异步工作的，当其他块已经存储好了（比如C1v1和D1v1)一些数据块可能还在处理（比如D2v2)。 &lt;img src=&quot;/uploads/wp/2019/09/write-replace.png&quot; alt=&quot;write replace&quot; /&gt; 如果一切顺利，数据块都会被存储，日志中的&lt;code&gt;last_complete&lt;/code&gt;指针会从1,1移动到1,2 &lt;img src=&quot;/uploads/wp/2019/09/full-write-success.png&quot; alt=&quot;full write success&quot; /&gt; 最后，之前的版本都可以被移除了: OSD1上的D1v1，OSD2上的D2v1, OSD3上的C1v1 &lt;img src=&quot;/uploads/wp/2019/09/remove-previous-version.png&quot; alt=&quot;remove previous version&quot; /&gt; 但是如果发生了异常，OSD1在D2v2仍然写入的时候停止了，对象的版本2只是部分写入：OSD3有一个块但是不足以恢复。它丢失了两个块: D1v2和D2v2,但是纠删码参数K=2,M=1要求至少2个块是可用的，才能恢复第三个块。OSD4成为新的主服务，找到last_complete日志是1,1，这应该是最新的日志了。 &lt;img src=&quot;/uploads/wp/2019/09/osd4-online.png&quot; alt=&quot;osd4 online&quot; /&gt; 日志1,2被发现在OSD3上，这和在OSD4上存储的最新版本1,1不一样、因此1,2被取消，C1v2块被移除。D1v1块被解码功能重建出来存在OSD4上。 &lt;img src=&quot;/uploads/wp/2019/09/rebuild-on-osd4.png&quot; alt=&quot;rebuild-on-osd4&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;缓存分层&lt;/h3&gt;
&lt;p&gt;缓存层提供给Ceph客户端更好的IO性能，因为一部分数据是存储在缓存中的。缓存层包括创建一个快速/昂贵的存储设备的pool，被配置成缓存层，便宜的存储设备被当成存储层使用。Ceph objecter负责处理将对象放在哪里，tiering代理决定什么时候将对象从缓存中写入到后面的存储层。因此缓存层和背后的存储层对Ceph客户端是完全透明的。 &lt;img src=&quot;/uploads/wp/2019/09/cache-tiering.png&quot; alt=&quot;cache tiering&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;ceph协议&lt;/h2&gt;
&lt;p&gt;Ceph客户端使用本地的协议来和Ceph存储集群通信。Ceph将它的功能打包到librados库中，因此你可以创建自己的Ceph客户端。下面的图表描述了这种基础架构。 &lt;img src=&quot;/uploads/wp/2019/09/basic-arch.png&quot; alt=&quot;basic architecture&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;本地协议和librados&lt;/h3&gt;
&lt;p&gt;现在的应用程序需要一个支持异步通信的简单的对象存储接口。Ceph存储集群提供了这样的一个功能。接口提供了直接的，并行的对象访问功能。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pool操作&lt;/li&gt;
&lt;li&gt;快照和写时复制克隆&lt;/li&gt;
&lt;li&gt;读写对象-创建和移除-整个对象或字节范围-追加或者截短&lt;/li&gt;
&lt;li&gt;创建/设置/获取/移除 XATTRS&lt;/li&gt;
&lt;li&gt;创建/设置/获取/移除 键值对&lt;/li&gt;
&lt;li&gt;复合操作和二次ack语义&lt;/li&gt;
&lt;li&gt;对象类&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;对象监视和通知&lt;/h3&gt;
&lt;p&gt;客户端可以注册一个对对象的持久关注，保持一个和主OSD的会话打开。客户端可以发送通知消息和一些负载信息给所有的监视者，然后所有监视客户端都会收到通知。这使得客户端可以使用任何对象作为同步通信通道。 &lt;img src=&quot;/uploads/wp/2019/09/watch-notify.png&quot; alt=&quot;watch and notify&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;数据分片&lt;/h3&gt;
&lt;p&gt;存储设备都会有吞吐量的限制，这对性能和可扩展性都有很大的影响。大多数存储系统都支持在多个存储设备上分别存储数据的一部分片段，这会增加吞吐量和性能。最常见的就是RAID了，Ceph的分片和RAID 0类似。 Ceph提供了三类客户端: Ceph块设备、Ceph文件系统、Ceph对象存储。Ceph客户端负责转换数据给用户（一个块设备镜像，RESTful对象，CephFS文件系统目录）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;提示: Ceph存储的对象不会被分片。Ceph对象存储，Ceph块设备,Ceph文件系统将它们的数据分片存储成多个Ceph存储系统对象。Ceph客户端通过librados写入数据必须执行分片(以及并行io)才能获取分片的优点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最简单的Ceph分片格式包含一个对象的分片。Ceph客户端向存储对象写入分片单元，直到对象达到了它的最大容量，然后创建一个新的对象来存储多出来的数据分片。这种最简单的方式对于小的块设备镜像、s3或swift对象以及CephFS文件是足够的。当然，这种简单的方式没有最大化利用Ceph分布式存储数据的优点，串行的方式也没有提升性能。下面的图标描述了这种形式的分片: &lt;img src=&quot;/uploads/wp/2019/09/simplest-strip.png&quot; alt=&quot;simplest strip&quot; /&gt; 如果你想要更大的镜像尺寸，更大的S3或Swift对象，或者更大的CephFS目录，你应该考虑通过将数据分片分布到一个对象集合中的多个对象上来提升读写性能。并行操作会显著的提升写入性能。因为对象会被映射到不同的PG中，然后被映射到不同的OSD中，每一个写入操作都会以最大的写入速度来并行操作。单硬盘的写入会因为柱头的移动和设备的最大带宽（100M/s）而受到限制(比如每次寻道都要6ms)。 通过将写入分布到多个对象上，Ceph可以减少每个硬盘的寻道时间，然后将多个硬盘的吞吐量结合以达到更快的写入（或读取）速度。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：分片是独立于对象复制的。因为CRUSH在OSD中复制对象，分片也会自动被复制。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在下图中，客户端数据被分片到一个对象集合中（对象集合1），这个集合包含4个对象，第一个分片单元是位于object 0 的 strip unit0，第4个分片单元是位于object3的strip unit3。当写如第4个分片后，客户端判断对象集合是否满了。如果对象集合没有满，客户端会从第一个对象继续写入分片。如果对象集合满了，客户端重新创建一个对象集合(object set2)。然后开始写入第一个分片(strip unit16)。 &lt;img src=&quot;/uploads/wp/2019/09/strip-to-multiple-object.png&quot; alt=&quot;strip to multiple object&quot; /&gt; 下面是三个重要的变量决定了Ceph如何对数据分片：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对象大小： Ceph存储系统中的对象有一个配置的最大容量（比如2MB, 4MB)。这个对象大小应该足够大，以容纳很多个分片单元。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分片宽度：分片有一个配置的单元大小（比如64kb)。Ceph客户端分割数据成同样大小的分片单元，除了最后一个。分片宽度应该是对象大小的一小部分，这样一个对象就可以包含多个分片单元了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分片数量：Ceph客户端在指定分片数量的对象上写入一连串的分片单元。这些指定数量的对象叫做一个对象集合。在Ceph客户端写完对象集合中的最后一个对象后，它又开始从对象集合中的第一个对象开始写入。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;重要：在投入到生产环境之前应该测试分片配置的性能。写入数据之后就不能修改这些配置了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一旦Ceph客户端将数据分片，然后将分片单元映射到对象中，Ceph的CRUSH算法将对象映射到PG中，然后PG被映射到Ceph OSD服务中。&lt;/p&gt;
&lt;h2&gt;Ceph客户端&lt;/h2&gt;
&lt;p&gt;Ceph客户端包含一系列的服务接口，包括:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;块设备：块设备(RBD)服务提供可变大小的、精简配置的块设备，块设备支持快照和复制。Ceph在整个集群中将块设备分片来提高性能。Ceph支持内核对象(KO)和QEMU的管理程序直接使用librbd——避免虚拟系统的内核对象过载。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对象存储：对象存储(RGW)服务提供RESTful API，和Amazon S3以及OpenStack Swift兼容。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件系统：文件系统(CephFS)服务提供了POSIX兼容的文件系统，可以使用&lt;code&gt;mount&lt;/code&gt;这样的命令，或者作为一个用户空间的文件系统。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ceph可以运行OSD, MDS, 监控服务的额外实例，这些会提高可扩展性和高可用性。下面的图从高层架构上描述了这个情况： &lt;img src=&quot;/uploads/wp/2019/09/high-level-arch.png&quot; alt=&quot;high level architecture&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Ceph对象存储&lt;/h3&gt;
&lt;p&gt;Ceph对象存储服务叫做radosgw，是一个FastCGi程序，提供了RESTful HTTP API来存储对象和元数据。它是在Ceph存储集群的顶层，拥有自己的数据格式，维护自己的用户数据库，认证和访问控制。RADOS入口使用统一的命名空间，这意味着你既可以使用OpenStack Swift兼容的API，也可以使用Amazon S3兼容的API。举例来说，你可以使用S3的API写入数据，使用Swift的API读取数据。&lt;/p&gt;
&lt;h3&gt;Ceph块设备&lt;/h3&gt;
&lt;p&gt;Ceph块设备将一个块设备镜像分片到多个Ceph存储集群的对象上，每一个对象都会映射到不同的PG，PG也会分布到不同的ceph-osd服务上。 精简配置的，可生成快照的Ceph块设备是虚拟化和云计算的吸引点。在虚拟机的场景下，人们可以在QEMU/KVM中使用rbd网络存储驱动来部署ceph块设备，主机使用librbd来给客户提供块设备服务。需要云计算栈使用libvirt来集成管理程序。你可以在QUME中使用精简配置的Ceph块设备，使用libvirt来支持OpenStack、CloudStack或其他解决方案。 因为我们目前不提供librbd支持其他的管理程序，你也同样可以使用Ceph块设备内核对象来提供一个块设备给一个客户端。其他的虚拟化技术，比如Xen可以访问Ceph块设备的内核对象。这可以使用命令行工具rbd完成。&lt;/p&gt;
&lt;h3&gt;Ceph文件系统&lt;/h3&gt;
&lt;p&gt;Ceph文件系统(CephFS)提供了POSIX兼容的文件系统，处于基于对象的Ceph存储集群之上。 &lt;img src=&quot;/uploads/wp/2019/09/ceph-filesystem.png&quot; alt=&quot;ceph filesystem&quot; /&gt; Ceph文件系统服务包括Ceph元数据服务(MDS)。MDS的目的是存储所有的文件系统元数据（目录，文件拥有者，访问模式等等），元数据都是存储在内存中的。原因是MDS是的一般操作(ls, cd)会对Ceph OSD服务造成巨大的压力。因此将元数据和数据本身分离开是提供高性能服务的必要条件。&lt;/p&gt;
&lt;h2&gt;一些不错的文章&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/58888246&quot; rel=&quot;noopener&quot;&gt;https://zhuanlan.zhihu.com/p/58888246&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>分布式存储</category><category>ceph</category><category>分布式存储</category><author>joyme123</author></item><item><title>cfssl生成证书并部署</title><link>https://www.myway5.com/blog/cfssl/</link><guid isPermaLink="true">https://www.myway5.com/blog/cfssl/</guid><description>cfssl是基于go的，因此需要安装go</description><pubDate>Tue, 16 Jul 2019 11:06:24 GMT</pubDate><content:encoded>&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;cfssl是基于go的，因此需要安装go&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go get -u github.com/cloudflare/cfssl/cmd/cfssl
go get -u github.com/cloudflare/cfssl/cmd/cfssljson
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后如果不能直接使用&lt;code&gt;cfssl&lt;/code&gt;请检查&lt;code&gt;$GOPATH/bin&lt;/code&gt;是否加入到&lt;code&gt;PATH&lt;/code&gt;环境变量中。 执行&lt;code&gt;cfssl&lt;/code&gt;查看一下cfssl提供的命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Usage:
Available commands:
        sign
        serve
        gencsr
        ocspsign
        ocspserve
        revoke
        certinfo
        version
        crl
        gencert
        ocsprefresh
        scan
        info
        print-defaults
        bundle
        genkey
        gencrl
        ocspdump
        selfsign
Top-level flags:
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建CA&lt;/h2&gt;
&lt;p&gt;CA的全称是Certificate Authority，也叫证书授权中心。CA的作用是作为一个权威的、被信任的第三方机构，提供管理和签发证书。在使用https访问一个网站时，为了证明这个网站是可信任的，那么就需要使用CA颁发的证书来证明自己。 因为在内网环境中搭建docker-registry，必然不可能用到互联网上的CA机构，因此我们需要自己扮演这个角色。首先创建文件夹&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir ca
cfssl print-defaults config &amp;gt; ca/ca-config.json
cfssl print-defaults csr &amp;gt; ca/ca-csr.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后修改ca-config.json&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;signing&quot;: {
        &quot;default&quot;: {
            &quot;expiry&quot;: &quot;2540400h&quot;
        },
        &quot;profiles&quot;: {
            &quot;www&quot;: {
                &quot;expiry&quot;: &quot;2540400h&quot;,
                &quot;usages&quot;: [
                    &quot;signing&quot;,
                    &quot;key encipherment&quot;,
                    &quot;server auth&quot;
                ]
            },
            &quot;client&quot;: {
                &quot;expiry&quot;: &quot;2540400h&quot;,
                &quot;usages&quot;: [
                    &quot;signing&quot;,
                    &quot;key encipherment&quot;,
                    &quot;client auth&quot;
                ]
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成ca&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cfssl gencert -initca ca/ca-csr.json | cfssljson -bare ca/ca -
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;生成服务器证书&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;mkdir server
cfssl print-defaults csr &amp;gt; server/server.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改server.json&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;CN&quot;: &quot;docker-registry&quot;,
    &quot;hosts&quot;: [
        &quot;docker-registry.k8s&quot;,
        &quot;www.docker-registry.k8s&quot;
    ],
    &quot;key&quot;: {
        &quot;algo&quot;: &quot;ecdsa&quot;,
        &quot;size&quot;: 256
    },
    &quot;names&quot;: [
        {
            &quot;C&quot;: &quot;CN&quot;,
            &quot;ST&quot;: &quot;SH&quot;,
            &quot;L&quot;: &quot;Shanghai&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;签发证书&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cfssl gencert -ca=ca/ca.pem -ca-key=ca/ca-key.pem -config=ca/ca-config.json -profile=www server/server.json | cfssljson -bare server/server
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;上面两步得到了ca.pem, server-key.pem, server.pem。ca.pem是ca的证书，server-key.pem是服务器证书的私钥，server.pem是服务器证书 为了在k8s中使用，我们需要先创建默认的tls secret&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl -n docker-registry create secret tls docker-registry-tls-cert --key=server/server-key.pem --cert=server/server.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;为电脑导入ca证书&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir /usr/share/ca-certificates/extra
sudo cp ca.pem /usr/share/ca-certificates/extra/ca.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行命令更新证书&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg-reconfigure ca-certificates
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用&lt;code&gt;curl&lt;/code&gt;查看ca根证书是否安装成功&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -v https://docker-registry.k8s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显示一下内容表示成功&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: C=CN; ST=SH; L=SH; CN=DR
*  start date: Jun 26 06:37:00 2019 GMT
*  expire date: Apr 17 06:37:00 2309 GMT
*  subjectAltName: host &quot;docker-registry.k8s&quot; matched cert&apos;s &quot;docker-registry.k8s&quot;
*  issuer: C=US; ST=CA; L=San Francisco; CN=example.net
*  SSL certificate verify ok.
&amp;gt; GET / HTTP/1.1
&amp;gt; Host: docker-registry.k8s
&amp;gt; User-Agent: curl/7.64.0
&amp;gt; Accept: */*
&amp;gt; 
&amp;lt; HTTP/1.1 200 OK
&amp;lt; Server: nginx/1.15.9
&amp;lt; Date: Wed, 26 Jun 2019 11:40:04 GMT
&amp;lt; Content-Length: 0
&amp;lt; Connection: keep-alive
&amp;lt; Cache-Control: no-cache
&amp;lt; 
* Connection #0 to host docker-registry.k8s left intact
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有一点需要注意的，我的电脑上安装了anaconda的环境，因此curl是在anaconda下的curl，它会默认加载&lt;code&gt;/home/username/anaconda3/certs/cacert.pem&lt;/code&gt;这个。因此我需要将&lt;code&gt;/etc/ssl/certs/ca-certificates.crt&lt;/code&gt;覆盖掉这个文件才能默认加载。或者也可以使用&lt;code&gt;--cacert&lt;/code&gt;指定根目录文件的位置。 或者可以参考这篇详细的说明： &lt;a href=&quot;https://jite.eu/2019/2/6/ca-with-cfssl/#&quot; rel=&quot;noopener&quot;&gt;https://jite.eu/2019/2/6/ca-with-cfssl/#&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;chrome下的相关设置&lt;/h2&gt;
&lt;p&gt;即使在我将根证书设置好并且验证成功，但是chrome仍然会有不安全的标识，这是因为需要为chrome单独导入根证书，设置的路径为: 设置 -&amp;gt; 高级 -&amp;gt; HTTPS相关设置。&lt;/p&gt;
</content:encoded><category>k8s</category><author>joyme123</author></item><item><title>go处理多态的JSON</title><link>https://www.myway5.com/blog/go-json/</link><guid isPermaLink="true">https://www.myway5.com/blog/go-json/</guid><description>最近使用go在对h2o的REST API进行封装时，发现了h2o的JSON返回值中有Polymorphic类型的字段。Polymorphic可以翻译为多态。一个多态的类型可以理解为既可能是float型，也可能是string类型等等。</description><pubDate>Tue, 09 Jul 2019 08:45:34 GMT</pubDate><content:encoded>&lt;p&gt;最近使用go在对h2o的REST API进行封装时，发现了h2o的JSON返回值中有&lt;code&gt;Polymorphic&lt;/code&gt;类型的字段。&lt;code&gt;Polymorphic&lt;/code&gt;可以翻译为多态。一个多态的类型可以理解为既可能是float型，也可能是string类型等等。&lt;/p&gt;
&lt;p&gt;在我的理解中，一个JSON中每个字段的类型都必须是确定的，在动态语言比如php, js这种，处理这种不确定的类型很方便。但是在go这种静态语言中，一个不确定的类型导致在解析中总是会出现类似于这样子的错误: &lt;code&gt;json: cannot unmarshal string into Go struct field Foo.Value of type int&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;使用interface{}处理多态的问题&lt;/h2&gt;
&lt;p&gt;在go中，interface{}是一个空接口，那么所有类型都可以看做实现了这个空接口，因此interface{}可以接收任何类型的值。那么如果一个json既可能是&lt;code&gt;{&quot;mean&quot;: 123}&lt;/code&gt;, 又可能是&lt;code&gt;{&quot;mean&quot;: &quot;NaN&quot;}&lt;/code&gt;，就可以使用以下结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Prediction struct {
    Mean interface{} `json:&quot;mean&quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是在使用Mean这个变量的时候，就比较麻烦了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;t2 := `{&quot;mean&quot;: &quot;NaN&quot;}`

var p2 Prediction
err = json.Unmarshal([]byte(t2), &amp;amp;p2)

if err != nil {
    log.Println(err)
} else {
    if mean, ok := p2.Mean.(string); ok {
        log.Printf(&quot;p2 mean: %s \n&quot;, mean)
    } else if n, ok := p2.Mean.(int); ok {
        mean = strconv.Itoa(n)
        log.Printf(&quot;p2 mean: %s \n&quot;, mean)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实现Unmarshaler和Marshaler接口&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type Marshaler interface {
        MarshalJSON() ([]byte, error)
}

type Unmarshaler interface {
        UnmarshalJSON([]byte) error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在使用&lt;code&gt;json.Unmarshal&lt;/code&gt;或&lt;code&gt;json.Marshal&lt;/code&gt;时，会自动调用对应变量类型的&lt;code&gt;UnmarshalJSON&lt;/code&gt;和&lt;code&gt;MarshalJSON&lt;/code&gt;方法。这样就可以定义一个&lt;code&gt;FlexString&lt;/code&gt;类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type FlexString string
type Prediction struct {
    Mean FlexString `json:&quot;mean&quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后实现*FlexString的UnmarshalJSON方法和FlexString的MarshalJSON方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (fs *FlexString) UnmarshalJSON(b []byte) error {
    if b[0] == &apos;&quot;&apos; {
        return json.Unmarshal(b, (*string)(fs))
    }
    var n float64
    if err := json.Unmarshal(b, &amp;amp;n); err != nil {
        return err
    }

    s := strconv.FormatFloat(n, &apos;g&apos;, 15, 64)

    *fs = FlexString(s)
    return nil
}

func (fs FlexString) MarshalJSON() ([]byte, error) {
    s := string(fs)
    n, err := strconv.ParseFloat(s, 64)
    if err != nil {
        return json.Marshal(s)
    }

    if math.IsNaN(n) {
        return json.Marshal(s)
    }

    return json.Marshal(n)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样在使用的时候，就可以正常的对多态的&lt;code&gt;Mean&lt;/code&gt;进行json解码以及编码了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    t1 := `{&quot;mean&quot;: 123.1212}`
    var p1 Prediction
    err := json.Unmarshal([]byte(t1), &amp;amp;p1)
    if err != nil {
        log.Println(err)
    } else {
        log.Println(&quot;p1 mean value:&quot;, p1.Mean)
    }

    res1, err := json.Marshal(p1)
    if err != nil {
        log.Println(&quot;res1 error:&quot;, err)
    } else {
        log.Println(&quot;res1 :&quot;, string(res1))
    }

    t2 := `{&quot;mean&quot;: &quot;NaN&quot;}`
    var p2 Prediction
    err = json.Unmarshal([]byte(t2), &amp;amp;p2)
    if err != nil {
        log.Println(err)
    } else {
        log.Println(&quot;p2 mean value:&quot;, p2.Mean)
    }

    res2, err := json.Marshal(p2)
    if err != nil {
        log.Println(&quot;res2 error:&quot;, err)
    } else {
        log.Println(&quot;res2 :&quot;, string(res2))
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2019/07/09 16:38:42 p1 mean value: 123.1212
2019/07/09 16:38:42 res1 : {&quot;mean&quot;:123.1212}
2019/07/09 16:38:42 p2 mean value: NaN
2019/07/09 16:38:42 res2 : {&quot;mean&quot;:&quot;NaN&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;123.1212在解码再编码之后，仍然是float类型，NaN仍然是string类型。这里有一个注意事项，就是&lt;code&gt;n, err := strconv.ParseFloat(s, 64)&lt;/code&gt;这个方法，如果s是NaN的字符串并不会出错，而是将n变成一个&lt;code&gt;NaN&lt;/code&gt;，因此需要使用math.IsNaN进行检查。&lt;/p&gt;
</content:encoded><category>go</category><category>go</category><category>UnmarshalJSON</category><category>MarshalJSON</category><category>json</category><author>joyme123</author></item><item><title>基于phpx的php 扩展调试</title><link>https://www.myway5.com/blog/phpx/</link><guid isPermaLink="true">https://www.myway5.com/blog/phpx/</guid><description>用c写php的扩展不是一件轻松的事情，调试起来更是麻烦。普通的php扩展调试网上可以找到资料。我这里因为要用到c++，因此用了phpx来降低php扩展开发的门槛。</description><pubDate>Sat, 15 Jun 2019 08:06:09 GMT</pubDate><content:encoded>&lt;p&gt;用c写php的扩展不是一件轻松的事情，调试起来更是麻烦。普通的php扩展调试网上可以找到资料。我这里因为要用到c++，因此用了phpx来降低php扩展开发的门槛。&lt;/p&gt;
&lt;p&gt;phpx项目地址：&lt;a href=&quot;https://github.com/swoole/phpx&quot; rel=&quot;noopener&quot;&gt;https://github.com/swoole/phpx&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;准备环境&lt;/h2&gt;
&lt;p&gt;ubuntu16.04, gcc, git, wget&lt;/p&gt;
&lt;p&gt;首先从php的官方网站下载需要的php版本的源代码： &lt;a href=&quot;https://php.net/releases/&quot; rel=&quot;noopener&quot;&gt;https://php.net/releases/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我下载的是php-7.0版本的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://www.php.net/distributions/php-7.0.33.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载完成后，解压进入目录，安装一些必要的依赖库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install libxml2-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;./configure --enable-debug
make -j 4
sudo make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我的扩展使用了phpx这个库，所有phpx的编译也需要开启debug模式，编辑cmakelist.txt加入 &lt;code&gt;SET(CMAKE_BUILD_TYPE &quot;Debug&quot;)&lt;/code&gt;。然后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cmake .
make -j 4
sudo make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：在编译php扩展的时候也要开启调试模式。&lt;/p&gt;
&lt;h2&gt;使用gdb调试&lt;/h2&gt;
&lt;p&gt;因为php+phpx这个组合编译出来的扩展没有办法像普通的gdb调试一样，通过行号来打断点调试。因此需要根据函数名来打断点。&lt;/p&gt;
&lt;p&gt;比如我的代码中有一个关键的方法名叫做input， 因此可以&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nm /usr/local/lib/php/extensions/debug-non-zts-20151012/php_dv.so | grep input
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到以下的查找结果&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;000000000003a99e T _Z8Dv_inputRN3php6ObjectERNS_4ArgsERNS_7VariantE&lt;br /&gt;
00000000000491f8 T _ZN11DataAdaptor5inputESt6vectorIP6PersonSaIS2_EE&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;_ZN11DataAdaptor5inputESt6vectorIP6PersonSaIS2_EE&lt;/code&gt;这个就是可以打断点的函数名啦。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gdb php
break _ZN11DataAdaptor5inputESt6vectorIP6PersonSaIS2_EE
run paintAll.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就可以看到断点打在了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Breakpoint 1, DataAdaptor::input (this=0x1259de0, 
    allPerson=std::vector of length 2622, capacity 2622 = {...}) at ../DataAdaptor.cc:195
195    void DataAdaptor::input(vector allPerson) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以使用layout分割窗口，大致结果如下：&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;/uploads/wp/2019/06/深度截图_选择区域_20190615155555-1.png&quot; alt=&quot;gdb调试窗口&quot; /&gt;&lt;/figure&gt;
&lt;h2&gt;coredump信息的查看&lt;/h2&gt;
&lt;p&gt;php扩展出错后，也可以生成coredump文件。需要修改limits.conf中的core文件大小，比如设置成unlimited。生成core文件后，执行&lt;code&gt;gdb php core&lt;/code&gt;, 然后使用bt即可打印出方法的调用栈，从而分析问题出现的原因。&lt;/p&gt;
</content:encoded><category>php</category><category>php</category><category>gdb</category><category>phpx</category><category>调试</category><author>joyme123</author></item><item><title>如何在go中优雅的热升级服务</title><link>https://www.myway5.com/blog/go-hot-upgrade/</link><guid isPermaLink="true">https://www.myway5.com/blog/go-hot-upgrade/</guid><description>在日常业务中，服务会经常升级，但是因为某些原因不希望断开和客户端的连接。因此就需要服务的热升级技术。 在研究这个问题之前，可以先看一下nginx是如何做到不间断服务热重启的。 将新的nginx可执行文件替换掉旧的可执行文件。</description><pubDate>Sun, 02 Jun 2019 07:34:52 GMT</pubDate><content:encoded>&lt;h2&gt;一、概述&lt;/h2&gt;
&lt;p&gt;在日常业务中，服务会经常升级，但是因为某些原因不希望断开和客户端的连接。因此就需要服务的热升级技术。 在研究这个问题之前，可以先看一下nginx是如何做到不间断服务热重启的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;将新的nginx可执行文件替换掉旧的可执行文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;向&lt;code&gt;master&lt;/code&gt;进程发送&lt;code&gt;USR2&lt;/code&gt;信号，&lt;code&gt;master&lt;/code&gt;进程在接收到信号后会将pid文件命名为&lt;code&gt;.oldbin&lt;/code&gt;后缀。之后启动新的可执行文件，并启动新的&lt;code&gt;worker&lt;/code&gt;进程。这个时候会有两个&lt;code&gt;master进程&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;向第一个&lt;code&gt;master&lt;/code&gt;进程发送&lt;code&gt;WINCH&lt;/code&gt;信号。第一个master进程会通知旧的&lt;code&gt;worker&lt;/code&gt;进程优雅（处理完当前请求）地退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果此时新的可执行文件有问题。可以做以下措施：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;向旧的&lt;code&gt;master&lt;/code&gt;进程发送&lt;code&gt;HUP&lt;/code&gt;信号，旧的&lt;code&gt;master&lt;/code&gt;进程会启动新的&lt;code&gt;worker&lt;/code&gt;进程，并且不会重新读取配置文件。然后会向新的&lt;code&gt;master&lt;/code&gt;进程发送&lt;code&gt;QUIT&lt;/code&gt;信号来要求其退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发送&lt;code&gt;TERM&lt;/code&gt;信号到新的master进程，新的master进程和其派生的&lt;code&gt;worker&lt;/code&gt;进程都会立刻退出。旧的&lt;code&gt;master&lt;/code&gt;进程会自动启动新的&lt;code&gt;worker&lt;/code&gt;进程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果升级成功，&lt;code&gt;QUIT&lt;/code&gt;信号会发送给旧的&lt;code&gt;master&lt;/code&gt;进程，并退出。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;完整的文档可以看: &lt;a href=&quot;http://nginx.org/en/docs/control.html#upgrade&quot; rel=&quot;noopener&quot;&gt;http://nginx.org/en/docs/control.html#upgrade&lt;/a&gt; 在go中处理这个问题也是这个思路。&lt;/p&gt;
&lt;h2&gt;二、具体实现&lt;/h2&gt;
&lt;h3&gt;2.1 定义服务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type Server struct {
l         net.Listener  // 监听端口
conns     map[int]net.Conn  // 当前服务的所有连接
rw        sync.RWMutex      // 读写锁，用来保证conns在并发情况下的正常工作
idLock    sync.Mutex        // 锁，用来保证idCursor在并发情况下的递增没有问题
idCursor  int               // 用来标记当前连接的id
isChild   bool // 是否是子进程
status    int  // 当前服务的状态
relaxTime int  // 在退出时允许协程处理请求的时间
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 处理信号&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func (s *Server) handleSignal() {
sc := make(chan os.Signal)

signal.Notify(sc, syscall.SIGHUP, syscall.SIGTERM)

for {
sig := &amp;lt;-sc

switch sig {
case syscall.SIGHUP:
log.Println(&quot;signal sighup&quot;)
// reload
go func() {
s.fork()
}()
case syscall.SIGTERM:
log.Println(&quot;signal sigterm&quot;)
// stop
s.shutdown()
}
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里只处理了两个信号，&lt;code&gt;HUP&lt;/code&gt;表示要热升级服务，此时会fork一个新的服务。&lt;code&gt;TERM&lt;/code&gt;表示要终止服务。&lt;/p&gt;
&lt;h3&gt;2.3 如何fork新的服务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func (s *Server) fork() (err error) {

log.Println(&quot;start forking&quot;)
serverLock.Lock()
defer serverLock.Unlock()

if isForked {
return errors.New(&quot;Another process already forked. Ignoring this one&quot;)
}

isForked = true

files := make([]*os.File, 1+len(s.conns))
files[0], err = s.l.(*net.TCPListener).File() // 将监听带入到子进程中
if err != nil {
log.Println(err)
return
}

i := 1
for _, conn := range s.conns {
files[i], err = conn.(*net.TCPConn).File()

if err != nil {
log.Println(err)
return
}

i++
}

env := append(os.Environ(), CHILD_PROCESS+&quot;=1&quot;)
env = append(env, fmt.Sprintf(&quot;%s=%s&quot;, SERVER_CONN, strconv.Itoa(len(s.conns))))

path := os.Args[0] // 当前可执行程序的路径
var args []string
if len(os.Args) &amp;gt; 1 {
args = os.Args[1:]
}

cmd := exec.Command(path, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.ExtraFiles = files
cmd.Env = env

err = cmd.Start()
if err != nil {
log.Println(err)
return
}

return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里会将监听的文件描述符以及所有连接的文件描述符都带到新的服务中。这里只需要在新的服务中重新使用这些文件描述符即可保证不断开连接。&lt;/p&gt;
&lt;h3&gt;2.4 服务的启动流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func (s *Server) Start(addr string) {
var err error

log.Printf(&quot;pid: %v \n&quot;, os.Getpid())

s.setState(StateInit)

if s.isChild {
log.Println(&quot;进入子进程&quot;)
// 通知父进程停止
ppid := os.Getppid()

err := syscall.Kill(ppid, syscall.SIGTERM)

if err != nil {
log.Fatal(err)
}

// 子进程， 重新监听之前的连接
connN, err := strconv.Atoi(os.Getenv(SERVER_CONN))
if err != nil {
log.Fatal(err)
}

for i := 0; i &amp;lt; connN; i++ {
f := os.NewFile(uintptr(4+i), &quot;&quot;)
c, err := net.FileConn(f)
if err != nil {
log.Print(err)
} else {
id := s.add(c)
go s.handleConn(c, id)
}
}
}

s.l, err = s.getListener(addr)
if err != nil {
log.Fatal(err)
}
defer s.l.Close()

log.Println(&quot;listen on &quot;, addr)

go s.handleSignal()

s.setState(StateRunning)

for {
log.Println(&quot;start accept&quot;)
conn, err := s.l.Accept()
if err != nil {
log.Fatal(err)
return
}

log.Println(&quot;accept new conn&quot;)

id := s.add(conn)
go s.handleConn(conn, id)
}

}

func (s *Server) getListener(addr string) (l net.Listener, err error) {
if s.isChild {
f := os.NewFile(3, &quot;&quot;)
l, err = net.FileListener(f)
return
}

l, err = net.Listen(&quot;tcp&quot;, addr)

return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动时，会判断是否是fork出的新的进程。如果是，则继承从父进程传递过来的文件描述符，并重新监听或作为连接处理。 完整的代码参考github: &lt;a href=&quot;https://github.com/joyme123/graceful%5C_restart%5C_server%5C_in%5C_golang%5C_demo&quot; rel=&quot;noopener&quot;&gt;https://github.com/joyme123/graceful\_restart\_server\_in\_golang\_demo&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>go</category><category>go</category><category>hot upgrade</category><author>joyme123</author></item><item><title>go WebAssembly初体验</title><link>https://www.myway5.com/blog/go-webassembly/</link><guid isPermaLink="true">https://www.myway5.com/blog/go-webassembly/</guid><description>WebAssembly是一门新的浏览器技术。可以取代一部分js的角色，并且从性能上来说，要比js好很多。WebAssembly目前还处于早期的发展阶段，仍然不够成熟。go语言对WebAssembly的支持也是在1.11版本上刚刚加入。</description><pubDate>Thu, 13 Dec 2018 13:10:07 GMT</pubDate><content:encoded>&lt;p&gt;WebAssembly是一门新的浏览器技术。可以取代一部分js的角色，并且从性能上来说，要比js好很多。WebAssembly目前还处于早期的发展阶段，仍然不够成熟。go语言对WebAssembly的支持也是在1.11版本上刚刚加入。虽然不能投入生产环境，但是可以用来做一些很有意思的事情。 官方WebAssembly的文档：&lt;a href=&quot;https://github.com/golang/go/wiki/WebAssembly&quot; rel=&quot;noopener&quot;&gt;https://github.com/golang/go/wiki/WebAssembly&lt;/a&gt;。详细的介绍可以看官方的文档。&lt;/p&gt;
&lt;h2&gt;一个go WebAssembly的例子&lt;/h2&gt;
&lt;p&gt;main.go&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
    fmt.Println(&quot;hello WebAssembly&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译这个go文件: &lt;code&gt;GOOS=js GOARCH=wasm go build -o main.wasm main.go&lt;/code&gt; index.html&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Go wasm&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;script src=&quot;wasm_exec.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
        const go = new Go();
        WebAssembly.instantiateStreaming(
            fetch(&quot;main.wasm&quot;),go.importObject).then((result) =&amp;gt; {
            go.run(result.instance)
        });

    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里还需要一个&lt;code&gt;wasm_exec.js&lt;/code&gt;。这个文件是官方提供的，可以理解为是go编译出来的二进制文件和js间的连接桥梁。这个时候打开浏览器，可以看到控制台下有：&lt;code&gt;hello WebAssembly&lt;/code&gt;。 需要说明的是，在go里面的所有STDOUT，都会在浏览器的控制台打印出来。&lt;/p&gt;
&lt;h2&gt;wasm的初始化过程&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;WebAssembly.instantiateStreaming(fetch(&quot;main.wasm&quot;),go.importObject)&lt;/code&gt;。浏览器执行这样一段代码来加载&lt;code&gt;main.wasm&lt;/code&gt;，并返回一个Promise对象。在加载完毕之后，调用&lt;code&gt;go.run(result.instance)&lt;/code&gt;来执行wasm中的代码。 加载一个wasm文件就是这样的简单。&lt;/p&gt;
&lt;h2&gt;go WebAssembly如何和js交互&lt;/h2&gt;
&lt;p&gt;上面的例子非常简单。但是WebAssembly技术绝非这样简单。我们可以在go中轻松的调用js中的方法。比如下面这句话：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;js.Global().Get(&quot;document&quot;).Call(&quot;getElementById&quot;, &quot;maxCubes&quot;).Set(&quot;value&quot;, 256)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相当于js中的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.getElementById(&quot;maxCubes).setAttribute(&apos;value&apos;, 256)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的，我们也可以在go中定义js方法，这样就可以在js中直接调用了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;api.onMemInitCb = js.NewCallback(func(args []js.Value) {
        length := args[0].Int()
        api.console.Call(&quot;log&quot;, &quot;length&quot;, length) // 调用js的console.log(&quot;length&quot;, length)
        api.inBuf = make([]uint8, length)
        // 拿到这个slice的SliceHeader
        hdr := (*reflect.SliceHeader)(unsafe.Pointer(&amp;amp;api.inBuf))
        ptr := uintptr(unsafe.Pointer(hdr.Data))

        api.console.Call(&quot;log&quot;, &quot;ptr:&quot;, ptr)
        js.Global().Call(&quot;gotMem&quot;, ptr)

        fmt.Println(&quot;初始化Mem成功&quot;)
    })

js.Global().Set(&quot;initMem&quot;, api.onMemInitCb)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，我们就可以在js中直接使用&lt;code&gt;initMem&lt;/code&gt;方法了。这样子，就相当于打通了go和js之间的通道，使得go WebAssembly几乎无所不能。&lt;/p&gt;
&lt;h2&gt;go和js之间如何通过内存传值。&lt;/h2&gt;
&lt;p&gt;这个部分是我在看go WebAssembly部分时最关注的。因为大多数时候，我们传参都不仅仅是&lt;code&gt;256&lt;/code&gt;这样的字面值。比如我们在做图片处理的时候，浏览器加载图片后传给wasm去处理，这种时候传递的肯定是一个指向一段内存的指针。 go代码中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;api.onMemInitCb = js.NewCallback(func(args []js.Value) {
    length := args[0].Int()
    api.console.Call(&quot;log&quot;, &quot;length&quot;, length) // 调用js的console.log(&quot;length&quot;, length)
    api.inBuf = make([]uint8, length)
    // 拿到这个slice的SliceHeader
    hdr := (*reflect.SliceHeader)(unsafe.Pointer(&amp;amp;api.inBuf))
    ptr := uintptr(unsafe.Pointer(hdr.Data))

    api.console.Call(&quot;log&quot;, &quot;ptr:&quot;, ptr)
    js.Global().Call(&quot;gotMem&quot;, ptr)

    fmt.Println(&quot;初始化Mem成功&quot;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;js代码中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function gotMem(pointer) {

    console.log(&quot;pointer&quot;, pointer)

    memoryBytes.set(bytes, pointer);
    // Now the image can be loaded from the slice.

    console.log(&quot;load image&quot;)
    loadImage();
}

......

let reader = new FileReader();
reader.onload = (ev) =&amp;gt; {
    bytes = new Uint8Array(ev.target.result);
    initMem(bytes.length);
    let blob = new Blob([bytes], {&apos;type&apos;: imageType});
    document.getElementById(&quot;sourceImg&quot;).src = URL.createObjectURL(blob);
};
imageType = this.files[0].type;
reader.readAsArrayBuffer(this.files[0]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面有两段代码，js部分代码是在加载一张图片，然后转换为&lt;code&gt;Uint8Array&lt;/code&gt;数组bytes，然后调用&lt;code&gt;initMem&lt;/code&gt;方法，传递一个数组bytes的长度作为参数。而&lt;code&gt;initMem&lt;/code&gt;是在go代码中定义的，&lt;code&gt;initMem&lt;/code&gt;负责调用&lt;code&gt;make([]uint8, length)&lt;/code&gt;去初始化需要的内存。然后通过一系列的转换获得申请的内存区域的指针&lt;code&gt;ptr&lt;/code&gt;。然后调用js的&lt;code&gt;gotMem&lt;/code&gt;将这个&lt;code&gt;ptr&lt;/code&gt;传递给js代码。在&lt;code&gt;gotMem&lt;/code&gt;中，&lt;code&gt;memoryBytes.set(bytes, pointer)&lt;/code&gt;这句代码初始化了这块内存。 这里值得指出的是，&lt;code&gt;memoryBytes&lt;/code&gt;是一段在wasm初始化时申请的内存，保存在一个&lt;code&gt;Uint8Array&lt;/code&gt;数组中。如果我们打印出来的话，可以发现这段内存有1G。然后我们的go代码中调用&lt;code&gt;make([]uint8, length)&lt;/code&gt;申请一块内存时，其实是在这段内存中申请的。比如说申请的区域为&lt;code&gt;201883648~(201883648+107003)&lt;/code&gt;。我们只要在js中向&lt;code&gt;memoryBytes&lt;/code&gt;的数组中给这块区域赋值，就把值传递给go的对象了。 再考虑一个问题。1G的初始化内存是不是太大了？这个内存是在编译go代码时由编译工具指定的。但是如果我们使用浏览器（比如chrome)的任务管理器查看这个窗口的占用内存时就会发现，实际占用并不会这么大。 在我的理解中（并不一定正确），这和C语言的&lt;code&gt;malloc&lt;/code&gt;类似。&lt;code&gt;malloc&lt;/code&gt;可以申请大于物理内存的虚拟内存，但是只要你不实际占用这么大内存，是不会有问题的。所以虽然go WebAssembly打印出来有1G的初始化内存，但是如果不是真的会使用，是不会占用这么大物理内存的。 关于初始化内存过大的问题，可以参考这个issues:&lt;a href=&quot;https://github.com/golang/go/issues/27462&quot; rel=&quot;noopener&quot;&gt;cmd/compile: wasm code causes out of memory error on Chrome and Firefox for Android&lt;/a&gt; 一些关于WebAssembly内存的设计：&lt;a href=&quot;https://github.com/WebAssembly/design/blob/master/FutureFeatures.md#finer-grained-control-over-memory&quot; rel=&quot;noopener&quot;&gt;Finer-grained control over memory&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;一个用go WebAssembly实现中位切分法处理图片的例子&lt;/h2&gt;
&lt;p&gt;关于中切分法可以参考我之前的这篇文章：&lt;a href=&quot;https://www.myway5.com/index.php/2018/12/06/%E4%B8%AD%E4%BD%8D%E5%88%87%E5%88%86%E6%B3%95%E9%A2%9C%E8%89%B2%E9%87%8F%E5%8C%96/&quot; rel=&quot;noopener&quot;&gt;中位切分法颜色量化&lt;/a&gt; 可以在这个地址进行预览: &lt;a href=&quot;https://joyme123.github.io/WebAssembly-MedianCut/&quot; rel=&quot;noopener&quot;&gt;预览地址&lt;/a&gt; 在浏览器中运行的效果(这里只展示了FastMap的效果，BestMap会好很多）： &lt;img src=&quot;/uploads/wp/2018/12/go_wsam.png&quot; alt=&quot;fastmap&quot; /&gt; 代码的github地址是:&lt;a href=&quot;https://github.com/joyme123/WebAssembly-MedianCut&quot; rel=&quot;noopener&quot;&gt;WebAssembly-MedianCut&lt;/a&gt; 里面大多数的代码都是用的我之前的代码。可见go WebAssembly还可以非常舒服的复用之前的代码。&lt;/p&gt;
</content:encoded><category>go</category><category>go</category><category>WebAssembly</category><author>joyme123</author></item><item><title>中位切分法颜色量化</title><link>https://www.myway5.com/blog/color/</link><guid isPermaLink="true">https://www.myway5.com/blog/color/</guid><description>首先举两个例子。 有一种视频接口叫VGA，这种视频接口有一个最大的缺点，就是同一时刻无法显示超过256种颜色。而对于一张true-color的图片，R、G、B都是有1个byte标示，因此一张true-color的图片可能有2^24种颜色。这远远超过了VGA可以同时展示的颜色数量。</description><pubDate>Wed, 05 Dec 2018 16:12:33 GMT</pubDate><content:encoded>&lt;p&gt;首先举两个例子。 有一种视频接口叫VGA，这种视频接口有一个最大的缺点，就是同一时刻无法显示超过256种颜色。而对于一张true-color的图片，R、G、B都是有1个byte标示，因此一张true-color的图片可能有2^24种颜色。这远远超过了VGA可以同时展示的颜色数量。 我们都知道有一种图片格式叫PNG( &lt;a href=&quot;https://www.myway5.com/index.php/2017/11/10/png%E6%A0%BC%E5%BC%8F%E5%88%86%E6%9E%90%E4%B8%8E%E5%8E%8B%E7%BC%A9%E5%8E%9F%E7%90%86/&quot; rel=&quot;noopener&quot;&gt;PNG格式分析与压缩原理&lt;/a&gt;)。它的编码方案中，有一种使用&lt;code&gt;调色板&lt;/code&gt;的编码方案。调色板上最多有256种颜色。这样每一种颜色都可以用一个byte的索引值代替。如果将一副超过256种颜色的png图使用调色板的编码方式，那么就可以明显的减少图片的体积。 那么如何将2^24种颜色用256种颜色表示呢？或者说，如何将m种颜色使用n种颜色来代替表示(m&amp;gt;n)。这就是这篇文章主要讨论的问题。 首先，如下图所示，RGB可以映射到三维空间中，R代表X轴，G代表Y轴，B代表Z轴。这样，任意一个RGB的组合都可以在空间中以一个点表示。 &lt;img src=&quot;/uploads/wp/2018/11/rgb-cube.gif&quot; alt=&quot;rgb-cube&quot; /&gt; 对于一个24-bit的图片来说，通常来说颜色空间是连续的，因为颜色之间的最小差异是几乎不可能察觉的。现在，这个连续的颜色空间要被映射到256种离散的颜色上。将一个连续的变量映射到离散的集合上，就叫做&lt;code&gt;量化&lt;/code&gt;。 有了上面这些说明后，现在有一张颜色很多的图片。图片上所有的点都被映射到一个空间中的立方体上。因为图片上点的分布一般不是均匀的，那么就可能有很多个点的聚集块。一般来说，聚集的越密，代表这些点的颜色越接近，那么如果我们将这些聚集块的点用聚集块中心点的颜色来表示，这样就可以将很多接近的颜色用一种颜色来代替。其实，这个过程跟聚类是很相似的。只不过一般的聚类算法都是无监督的机器学习，效率要较差。而&lt;code&gt;中位切分法&lt;/code&gt;则可以用很快的速度完成这样的聚类。&lt;/p&gt;
&lt;h2&gt;中位切分法&lt;/h2&gt;
&lt;p&gt;中位切分法用简短的话描述：将一个图片对应的RGB立方体切割成目标数量的紧凑的RGB立方体，然后用立方体的质心值代表立方体内所有点的值。重复这个过程直到得到想要的颜色数量。 这里假设我们要获取256个颜色。详细的流程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将整张图片转换成一个RGB立方体&lt;/li&gt;
&lt;li&gt;找到立方体的最长边，从中位数的地方开始切割。得到两个包含相同数量点的立方体。&lt;/li&gt;
&lt;li&gt;对分割成的立方体重复上一步的切割过程直到得到256个立方体。&lt;/li&gt;
&lt;li&gt;256个立方体的质心就是要计算的256个颜色值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用一个例子来说明（为了简单，这里没有B颜色空间）,这里有6种颜色，共14个点。想要得到4中颜色输出。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始情况&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;th&gt;(r,g)-coordinates&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;C0&lt;/td&gt;
&lt;td&gt;(20,40)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C1&lt;/td&gt;
&lt;td&gt;(40,20)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C2&lt;/td&gt;
&lt;td&gt;(5,60)&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C3&lt;/td&gt;
&lt;td&gt;(50,80)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C4&lt;/td&gt;
&lt;td&gt;(60,30)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C5&lt;/td&gt;
&lt;td&gt;(80,50)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;3次切割后的情况&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cube&lt;/th&gt;
&lt;th&gt;HistPtr.lower&lt;/th&gt;
&lt;th&gt;HistPtr.upper&lt;/th&gt;
&lt;th&gt;Colors Enclosed&lt;/th&gt;
&lt;th&gt;Cube Centroid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;C0&lt;/td&gt;
&lt;td&gt;(20,40)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;C2&lt;/td&gt;
&lt;td&gt;(5,60)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;C1,C4&lt;/td&gt;
&lt;td&gt;(46.7,23.3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;C5,C3&lt;/td&gt;
&lt;td&gt;(65,65)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;具体流程图如下： 第一次：最长边是Ｒ,中位数是（20 + 40) / 2 = 30。从30处切割。得到收缩后的矩形。左边的矩形为A1,右边为B1。 第二次: 切割B1的R边，得到A2,B2 第三次: 切割A1的R边，得到A3,B3 这个时候我们已经有4个矩形了。 这里我们有两个颜色映射方式。第一种是&lt;code&gt;快速映射&lt;/code&gt;，以矩形的质心作为映射后的颜色值。第二种是&lt;code&gt;最佳映射&lt;/code&gt;，得到和其他点的距离和最短的点作为映射后的颜色值。 &lt;img src=&quot;/uploads/wp/2018/11/9409a5f2.gif&quot; alt=&quot;切割过程&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;中位切分法的实现&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;递归方式。在描述的时候，我们RGB块描述成递归的方式。如下&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Split(Cube){
  if (ncubes == 4) return;
  find longest axis of Cube;
  cut Cube at median to form CubeA, CubeB;
  Split(CubeA);
  Split(CubeB);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这种递归切割的方式是有问题的。结果如下图 &lt;img src=&quot;/uploads/wp/2018/11/9409a5f3.gif&quot; alt=&quot;递归切割&quot; /&gt; 在递归情况下，B1将一直不会被处理。 2.为了解决1中的问题。我们可以设定一个最大深度(level)。比如我们要得到4个输出颜色。那么log 2 4=2。最大深度应该是2。当切割到最大深度时，我们就不再往下切割，转而切割那些还未被处理的更大的区域。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;maxlevel = 2;
Split(Cube,level){
  if (ncubes == 4) return;
  if (Cube&apos;s level == maxlevel) return;
  find longest axis of Cube;
  cut Cube at median to form CubeA, CubeB;
  Split(CubeA, level+1);
  Split(CubeB, level+1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;但是这样又会有新的问题出现。如果一个立方体中只有一种颜色（实际上此时只有一个点)，不能再往下切割。因此我们需要一种方案去解决这个问题。这里可以维持一个包含所有立方体的数组，然后根据优先级排序切割。优先级可以按照level从小到大排序，但是所有颜色数量为1的立方体都会被忽略。这样每次切割前对这个数组做一次排序，取出优先级最高的立方体进行切割。直到切割出指定数量的立方体或者所有的立方体颜色都为1。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;build initial cube from histogram;
set initial cube&apos;s level to 0;
insert initial cube in list of cubes;
ncubes = 1;
while (ncubes &amp;lt; maxcubes){
  search for Cube with smallest level;
  find the longest axis of Cube;
  find the median along this axis;
  cut Cube at median to form CubeA, CubeB;
  set CubeA&apos;s level = Cube&apos;s level + 1;
  set CubeB&apos;s level = Cube&apos;s level + 1;
  insert CubeA in Cube&apos;s slot;
  add CubeB to end of list of cubes;
  ncubes = ncubes + 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实践&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用中位切分法提取图片的主题色&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用中位切分法压缩图片&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;中位切分法的实现的go语言版本:&lt;a href=&quot;https://github.com/joyme123/MedianCut&quot; rel=&quot;noopener&quot;&gt;joyme123/MedianCut&lt;/a&gt;。&lt;a href=&quot;https://github.com/joyme123/MedianCut/blob/master/asset/preview.md&quot; rel=&quot;noopener&quot;&gt;点击这里进行效果预览&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考文献&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://collaboration.cmc.ec.gc.ca/science/rpn/biblio/ddj/Website/articles/DDJ/1994/9409/9409e/9409e.htm&quot; rel=&quot;noopener&quot;&gt;Median-Cut Color Quantization&lt;/a&gt; &lt;a href=&quot;https://cloud.tencent.com/developer/article/1132389&quot; rel=&quot;noopener&quot;&gt;前端图片主题色提取&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>图像相关</category><category>go</category><category>中位切分</category><category>颜色量化</category><author>joyme123</author></item><item><title>virtualbox 网络桥接</title><link>https://www.myway5.com/blog/virtualbox-network-bridge/</link><guid isPermaLink="true">https://www.myway5.com/blog/virtualbox-network-bridge/</guid><description>virtualbox的默认方式是NAT，用宿主机对虚拟机做端口转发。在组建本地的集群环境时，使用这种方式是不行的。可以使用桥接网卡的方式，使得虚拟机分配到一个宿主机局域网内的ip地址。 首先，修改/etc/network/interfaces文件。 这个文件原来的内容如下：</description><pubDate>Mon, 03 Dec 2018 05:36:04 GMT</pubDate><content:encoded>&lt;p&gt;virtualbox的默认方式是NAT，用宿主机对虚拟机做端口转发。在组建本地的集群环境时，使用这种方式是不行的。可以使用桥接网卡的方式，使得虚拟机分配到一个宿主机局域网内的ip地址。 首先，修改&lt;code&gt;/etc/network/interfaces&lt;/code&gt;文件。 这个文件原来的内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto enp0s3
iface enp0s3 inet dhcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改成如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto enp0s3
#iface enp0s3 inet dhcp
iface enp0s3 inet static
address 192.168.0.100
gateway 192.168.0.1
netmask 255.255.255.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里的&lt;code&gt;address&lt;/code&gt;和&lt;code&gt;gateway&lt;/code&gt;的网段要和宿主机网段一致。比如我的宿主机ip为&lt;code&gt;192.168.0.8&lt;/code&gt;。 注意这里还要修改一下dns，不然会出现无法解析域名的问题，编辑/etc/resolvconf/resolv.conf.d/base 添加一下的nameserver:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nameserver 8.8.8.8
nameserver 1.1.1.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行&lt;code&gt;sudo resolvconf -u&lt;/code&gt;使dns的配置生效。 然后修改虚拟机的设置，在VirtualBox的菜单栏中：设备-&amp;gt;网络-&amp;gt;网络-&amp;gt;连接方式：桥接网卡。 然后重启网络 &lt;code&gt;sudo /etc/init.d/networking restart&lt;/code&gt; 如果网络还是有问题，可以尝试重启虚拟机。 之后在终端之中输入&lt;code&gt;ifconfig&lt;/code&gt;，网络信息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enp0s3    Link encap:Ethernet  HWaddr 08:00:27:f1:6d:fd  
          inet addr:192.168.0.100  Bcast:192.168.0.255  Mask:255.255.255.0
          inet6 addr: fe80::a00:27ff:fef1:6dfd/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:304 errors:0 dropped:0 overruns:0 frame:0
          TX packets:173 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:28254 (28.2 KB)  TX bytes:28063 (28.0 KB)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虚拟机的ip已经变成192.168.0.100。使用&lt;code&gt;ping 192.168.0.8&lt;/code&gt;检查和宿主机的通信是否正常。&lt;/p&gt;
</content:encoded><category>架构设计</category><category>工作</category><category>计算机</category><category>virtualbox</category><author>joyme123</author></item><item><title>TLS1.2 RFC5426中一些术语解释</title><link>https://www.myway5.com/blog/tls1-2-rfc5426/</link><guid isPermaLink="true">https://www.myway5.com/blog/tls1-2-rfc5426/</guid><description>最近想为我的cats服务器加上https的支持。因为最近有一点点的忙，这个项目已经很久没有提交新的代码了。之所以没有用一些开源的库去做，因为这个项目的目的就是锻炼我的代码能力，以及英文RFC的阅读能力。但是在看RFC5426时遇到了一些挫折，里面有大量的密码学上的专业词汇。</description><pubDate>Sat, 03 Nov 2018 03:24:01 GMT</pubDate><content:encoded>&lt;p&gt;最近想为我的&lt;a href=&quot;https://github.com/joyme123/cats&quot; rel=&quot;noopener&quot;&gt;cats&lt;/a&gt;服务器加上https的支持。因为最近有一点点的忙，这个项目已经很久没有提交新的代码了。之所以没有用一些开源的库去做，因为这个项目的目的就是锻炼我的代码能力，以及英文RFC的阅读能力。但是在看RFC5426时遇到了一些挫折，里面有大量的密码学上的专业词汇。因此买了一本《图解密码技术》，这里将RFC5426中的专业词汇和概念单独拿出来做一次笔记。 PRF( pseudorandom function ) algorithm: 伪随机函数算法。随机数有三类性质：1.随机性。2.不可预测性。3.不可重现性。这三个性质要求越来越严格。满足1，称为弱伪随机数，满足1、2，称为强伪随机数，满足1、2、3，称为真伪随机数。在密码学的体系中，要求至少达到强伪随机数才能保证安全。 public key encryption：公开密钥加密（英语：Public-key cryptography），也称为非对称加密（英语：asymmetric cryptography），是密码学的一种算法，它需要两个密钥，一个是公开密钥，另一个是私有密钥；一个用作加密的时候，另一个则用作解密。使用其中一个密钥把明文加密后所得的密文，只能用相对应的另一个密钥才能解密得到原本的明文；甚至连最初用来加密的密钥也不能用作解密。由于加密和解密需要两个不同的密钥，故被称为非对称加密；不同于加密和解密都使用同一个密钥的对称加密。虽然两个密钥在数学上相关，但如果知道了其中一个，并不能凭此计算出另外一个；因此其中一个可以公开，称为公钥，任意向外发布；不公开的密钥为私钥，必须由用户自行严格秘密保管，绝不透过任何途径向任何人提供，也不会透露给要通信的另一方，即使他被信任。参考链接: &lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%85%AC%E5%BC%80%E5%AF%86%E9%92%A5%E5%8A%A0%E5%AF%86&quot; rel=&quot;noopener&quot;&gt;公开密钥加密&lt;/a&gt; RSA: 名称不是什么缩写,而是发明人首字母的结合。是一种非对称加密的方法。它的速度比起DES等对称加密算法要慢的多。因为非对称加密中,有一个公钥和一个私钥,那么如果分配公钥则是一个问题。如果通过网络传输公钥,则可能被中间人替换掉公钥进行攻击.因此一般的做法是&lt;strong&gt;用可靠的第三方机构签发证书&lt;/strong&gt;来防止这样的攻击。参考链接：&lt;a href=&quot;https://zh.wikipedia.org/wiki/RSA%E5%8A%A0%E5%AF%86%E6%BC%94%E7%AE%97%E6%B3%95&quot; rel=&quot;noopener&quot;&gt;RSA加密演算法&lt;/a&gt; MAC (Message Authentication Code) algorithm: 消息验证码的算法.是经过特定算法后产生的一小段信息，检查某段消息的完整性，以及作身份验证 。它可以用来检查在消息传递过程中，其内容是否被更改过，不管更改的原因是来自意外或是蓄意攻击。同时可以作为消息来源的身份验证，确认消息的来源。在我的理解中，MAC是一种与密钥相关联的单向散列函数。参考链接：&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E8%A8%8A%E6%81%AF%E9%91%91%E5%88%A5%E7%A2%BC&quot; rel=&quot;noopener&quot;&gt;消息认证码&lt;/a&gt; HMAC (Keyed-Hashing for Message Authentication): 它通过一个标准算法，在计算哈希的过程中，把key混入计算过程中。其实就是常用的加salt的方式,使得相同的原值生成不同的哈希值。HMAC是用来生成MAC值的。参考链接：&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E9%87%91%E9%91%B0%E9%9B%9C%E6%B9%8A%E8%A8%8A%E6%81%AF%E9%91%91%E5%88%A5%E7%A2%BC&quot; rel=&quot;noopener&quot;&gt;密钥散列消息认证码&lt;/a&gt; DSA (digital signing algorithm)：DSA是一种更高级的验证方式。一般用于数字签名和认证。DSA 不单单只有公钥、私钥，还有数字签名。私钥加密生成数字签名，公钥验证数据及签名。在DSA数字签名和认证中，发送者使用自己的私钥对文件或消息进行签名，接受者收到消息后使用发送者的公钥来验证签名的真实性。如果数据和签名不匹配则认为验证失败！数字签名，不仅能验证数据的完整性，真实性，还能“对第三方证明”和“防止否认”。参考链接：&lt;a href=&quot;https://blog.csdn.net/Trustauth/article/details/80049597&quot; rel=&quot;noopener&quot;&gt;常见的加密算法之DSA 算法&lt;/a&gt; CBC (Cipher Block Chaining)：分组密码的一种工作模式，允许使用同一个分组密码密钥对多于一块的数据进行加密，并保证其安全性。其他的工作模式还有：ECB,PCBC,CFB,OFB,CTR等。参考链接:&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%88%86%E7%BB%84%E5%AF%86%E7%A0%81%E5%B7%A5%E4%BD%9C%E6%A8%A1%E5%BC%8F&quot; rel=&quot;noopener&quot;&gt;分组密码工作模式&lt;/a&gt; SHA256, SHA1, MD5: 常见的几种信息摘要算法（有时候也称为哈希算法，单向散列函数等）。 SSL (Secure Socket Layer)：安全套接字层 TLS (Transport Layer Security Protocol)：安全传输层协议，TLS的后续工作是在SSL的基础上进行的。 stream cipher encryption：在密码学中，流密码（英语：Stream cipher），又译为流加密、数据流加密，是一种对称加密算法，加密和解密双方使用相同伪随机加密数据流（pseudo-random stream）作为密钥，明文数据每次与密钥数据流顺次对应加密，得到密文数据流。实践中数据通常是一个位（bit）并用异或（xor）操作加密。参考链接: &lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%B5%81%E5%AF%86%E7%A0%81&quot; rel=&quot;noopener&quot;&gt;流密码&lt;/a&gt; block cipher encryption：在密码学中，分组加密（英语：Block cipher），又称分块加密或块密码，是一种对称密钥算法。它将明文分成多个等长的模块（block），使用确定的算法和对称密钥对每组分别加密解密。分组加密是极其重要的加密协议组成，其中典型的如DES和AES作为美国政府核定的标准加密算法，应用领域从电子邮件加密到银行交易转帐，非常广泛。参考链接: &lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%88%86%E7%BB%84%E5%AF%86%E7%A0%81&quot; rel=&quot;noopener&quot;&gt;分组加密&lt;/a&gt; authenticated encryption with additional data (AEAD) encryption：认证加密（英语：Authenticated encryption，AE）和用于关联数据的认证加密（authenticated encryption with associated data，AEAD，AE的变种）是一种能够同时保证数据的保密性、 完整性和真实性的一种加密模式。参考链接: &lt;a href=&quot;https://zh.wikipedia.org/wiki/%E8%AE%A4%E8%AF%81%E5%8A%A0%E5%AF%86&quot; rel=&quot;noopener&quot;&gt;用于关联数据的认证加密&lt;/a&gt; compression algorithm：数据压缩算法 master secret：主密码用来生成对称密码的秘钥，消息认证码的秘钥以及对称密码CBC模式所使用的初始化向量(IV)&lt;/p&gt;
</content:encoded><category>网络协议</category><category>TLS1.2</category><category>RFC5426</category><author>joyme123</author></item><item><title>rabbitmq遇到的一次tcp半打开的问题</title><link>https://www.myway5.com/blog/rabbitmq-tcp-halfopen/</link><guid isPermaLink="true">https://www.myway5.com/blog/rabbitmq-tcp-halfopen/</guid><description>业务中有一个测试服务器，里面运行了好几个任务队列。但是自从将任务队列从redis迁移到rabbitmq上后，一直会在运行一段时间后停止运行。一般这种情况下，要么是进程退出了，要么是连接断开了。但是检查后发现，进程是正常运行的，并且通过netstat发现，连接也一直存在。</description><pubDate>Tue, 30 Oct 2018 07:08:30 GMT</pubDate><content:encoded>&lt;h2&gt;问题描述&lt;/h2&gt;
&lt;p&gt;业务中有一个测试服务器，里面运行了好几个任务队列。但是自从将任务队列从redis迁移到rabbitmq上后，一直会在运行一段时间后停止运行。一般这种情况下，要么是进程退出了，要么是连接断开了。但是检查后发现，进程是正常运行的，并且通过netstat发现，连接也一直存在。如果是连接断开，我的代码中也做了断线重连的机制。&lt;/p&gt;
&lt;h2&gt;问题排查&lt;/h2&gt;
&lt;p&gt;一开始以为是&lt;code&gt;php-amqplib&lt;/code&gt;这个库的问题，就去找它的issue，看看有没有类似的问题。这里没有找到类似的bug。 然后去复现了一个最小的demo，这个问题一直都是出现在运行相当长的时间之后。于是决定对测试服务器上已经出现问题的代码进行抓包。新push的任务，任务队列的进程就是接收不到，但是netstat结果，任务队列到rabbit服务的连接又确实是存在的。 没有办法就去随便翻翻《UNIX网络编程》的TCP章节，然后想到TCP的&lt;code&gt;全双工&lt;/code&gt;特性。全双工特性就是说**在一个给定的连接上应用可以在任何时候在进出两个方向上既发送数据又接收数据。建立一个全双工连接后，需要的话可以把它转换成一个单工连接。**于是用netstat检查rabbit服务的连接，发现是没有到任务队列的连接的。所以问题就是出现在这里。**但是仔细思考一下，这里的问题并不能用&lt;code&gt;全双工&lt;/code&gt;特性去解释，因为&lt;code&gt;全双工&lt;/code&gt;转成&lt;code&gt;单工&lt;/code&gt;是tcp的一个特性，但是在这个问题中，应该是一个异常情况。&lt;code&gt;全双工&lt;/code&gt;是需要双端协商的，而我这里的问题应该是： **服务器关闭了连接，但是任务队列却没有收到关闭的Fin报文，**很多时候被称为&lt;code&gt;半打开&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;问题解决&lt;/h2&gt;
&lt;p&gt;解决这个问题很简单，启用&lt;code&gt;php-amqplib&lt;/code&gt;的心跳包机制即可。&lt;/p&gt;
&lt;h2&gt;更多的思考&lt;/h2&gt;
&lt;p&gt;1.半连接，半打开，半关闭（以下用A,B代表连接的两端） &lt;strong&gt;半连接&lt;/strong&gt;:出现在tcp的三次握手阶段。A发送syn，B响应ack,syn后，此时处于半连接状态。如果A不发送ack，B将会一直为这个半连接分配一段内存空间。因此可以使用这个特点对B进行&lt;strong&gt;半连接攻击&lt;/strong&gt; &lt;strong&gt;半打开(half-open)&lt;/strong&gt;:A断开连接但是却没有发送Fin报文，导致B不知道。在维基百科上半打开和半连接是相同的。 &lt;strong&gt;半关闭&lt;/strong&gt;:在关闭的4次挥手阶段，A端发送Fin,B端ack但是不发送Fin。 半连接、半关闭都是正常出现的情况。半打开则是不正常的状态，**一个Unix进程无论自愿地（调用exit或是从main函数中返回）还是非自愿地（收到一个终止本进程的信号）终止时，所有打开的描述符都被关闭，这也导致仍然打开的任何TCP连接上也发出一个FIN。**也就是说，只有当服务器断电等这种非正常关闭的情况下才会出现半连接，否则对端都应该收到Fin报文，然后关闭连接。 1.什么情况导致了半打开？ 服务器断电这类情况肯定是没有出现的，所以一定是其他地方有问题导致了这个情况。因为这个问题只在测试服务器上出现，生产服务器上并没有。所以有点难推测。 2.双向连接(bidirectional)和全双工(full-duplex) 在我的理解中，双向连接指的是A端确认了到B端的连接，B端也确认了到A端的连接。全双工则指可以同时发送和接收，互不干扰。 3.心跳机制是如何避免这种情况的？ 心跳机制一般都是隔一段时间主动发送一个消息给对端来确认连接是否存活。如果连接丢失，则必然不会收到对端的响应。这样在响应超时后重新发起连接即可。 其实tcp也有一个keep-alive机制。与心跳包作用类似，但是一是检查的周期长，二是一旦启用，机器上所有的连接都会启用这个机制，导致资源浪费。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/TCP_half-open&quot; rel=&quot;noopener&quot;&gt;TCP half-open&lt;/a&gt; &lt;a href=&quot;https://blog.csdn.net/guowenyan001/article/details/11765749&quot; rel=&quot;noopener&quot;&gt;半连接、半打开、半关闭&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>工作</category><category>rabbitmq</category><category>tcp半打开</category><author>joyme123</author></item><item><title>go 内存模型</title><link>https://www.myway5.com/blog/go-memory/</link><guid isPermaLink="true">https://www.myway5.com/blog/go-memory/</guid><description>go的内存模型旨在说明：一个协程中对变量v的写入产生的值可以保证被另一个协程中的对变量v的读取观察到。 Happens Before</description><pubDate>Tue, 04 Sep 2018 13:51:40 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;go的内存模型旨在说明：一个协程中对变量v的写入产生的值可以保证被另一个协程中的对变量v的读取观察到。&lt;/p&gt;
&lt;h2&gt;Happens Before&lt;/h2&gt;
&lt;p&gt;在一个协程内，读写操作必须按照程序指定的顺序进行。在一个协程内，编译器和处理器可能对读写操作重写排序，但是这个排序的前提是：在&lt;code&gt;当前协程&lt;/code&gt;内，不会改变程序的执行行为。但是这个重新排序是不保证其他协程观测到执行顺序是不改变的。比如在协程1中&lt;code&gt;a=1;b=2&lt;/code&gt;，但在其他协程的感知中，可能b比a先更新值。 我们这里定义&lt;code&gt;Happens Before(在...之前发生)&lt;/code&gt;，如果事件e1在事件e2&lt;code&gt;之前发生&lt;/code&gt;，那么我们就可以说e2在e1之后发生。如果e1既不在e2之前发生，也不在e2之后发生。那么e1和e2就是同时发生的（并发）。 在一个协程内，&lt;code&gt;Happens Before&lt;/code&gt;的顺序就是程序表达的那样。 如果下面两点可以保证，就说明对变量v的读取r&lt;code&gt;允许&lt;/code&gt;观察到对变量v的写入w：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;r不是在w之前发生&lt;/li&gt;
&lt;li&gt;在w之后并且r之前没有其他的对v的写入w&apos;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了保证变量v的读取r观察到v的特定写入w，并且保证w是唯一允许被r观察到的。也就是说，r保证能观察到w。需要做到下面两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;w在r之前发生&lt;/li&gt;
&lt;li&gt;其他的对v的写入w&apos;要么发生在w之前，要么发生在w之后&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面两点比上面两点要求更为严格。它保证了没有其他的写入w&apos;和w、r同时发生。 在一个协程内，因为没有并发，所以这两种定义是一致的：读取r可以观察到写入w对变量v最近一次的写入。但是当多个协程同时访问同一个共享变量时，就必须使用同步事件来建立&lt;code&gt;Happens Before&lt;/code&gt;语义来保证读取r可以观察到指定的写入w。 在内存模型中，对变量v以0值初始化是一次写入。 对于大于单机器字节的读取和写入，可以看做是对多个单机器字节的乱序操作。&lt;/p&gt;
&lt;h2&gt;同步&lt;/h2&gt;
&lt;h3&gt;初始化&lt;/h3&gt;
&lt;p&gt;程序初始化是在单协程内运行的，但是这个协程可能创建其他的协程。它们是并发的。 如果包p引入了包q，则q的初始化函数会在p的初始化函数之前运行。 main.main函数在所有的初始化函数之后运行。&lt;/p&gt;
&lt;h3&gt;协程创建&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;go&lt;/code&gt;关键字创建协程发生在协程运行之前。&lt;/p&gt;
&lt;h3&gt;协程销毁&lt;/h3&gt;
&lt;p&gt;协程的销毁不保证在程序中的任何事件发生之前。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a string

func hello() {
    go func() { a = &quot;hello&quot; }()
    print(a)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个赋值没有跟随任何同步事件，所以它不保证被其他协程观察到。事实上，激进的编译器会删除整个go语句。 如果需要，可以使用同步原语比如&lt;code&gt;锁&lt;/code&gt;或&lt;code&gt;管道通信&lt;/code&gt;来建立一个相关的执行顺序。&lt;/p&gt;
&lt;h3&gt;管道通信&lt;/h3&gt;
&lt;p&gt;在go的协程中，管道通信是非常重要的一个同步方法。通常发送方和接受方在两个不同的协程中，利用发送和接收这两个有序的动作来进行同步。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1.在有缓冲的管道中，发送一定发生在接收完成前。(A send on a channel happens before the corresponding receive from that channel completes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var c = make(chan int, 10)
var a string

func f() {
    a = &quot;hello, world&quot;
    c &amp;lt;- 0
}

func main() {
    go f()
    &amp;lt;-c
    print(a)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;a = &quot;hello, world&quot;&lt;/code&gt;一定在&lt;code&gt;c&amp;lt;-0&lt;/code&gt;之前发生，&lt;code&gt;c&amp;lt;-0&lt;/code&gt;一定在&lt;code&gt;&amp;lt;-c&lt;/code&gt;之前发生，&lt;code&gt;&amp;lt;-c&lt;/code&gt;一定在&lt;code&gt;print(a)&lt;/code&gt;之前发生。这样就能保证&lt;code&gt;a = &quot;hello, world&quot;&lt;/code&gt;在&lt;code&gt;print(a)&lt;/code&gt;之前发生。则保证可以打印出&lt;code&gt;hello, world&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2.管道的关闭一定发生在从管道中接收值之前。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此上面的例子将&lt;code&gt;&amp;lt;-c&lt;/code&gt;替换成&lt;code&gt;close(c)&lt;/code&gt;也是可以的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;3.在无缓冲管道中，接收一定发生在发送完成前。(The closing of a channel happens before a receive that returns a zero value because the channel is closed.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如下面的例子将发送和接收语句互换了位置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var c = make(chan int)
var a string

func f() {
    a = &quot;hello, world&quot;
    &amp;lt;-c
}
func main() {
    go f()
    c &amp;lt;- 0
    print(a)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果上述例子中管道是有缓冲的(e.g., c = make(chan int, 1)) ，就无法保证一定能打印出&lt;code&gt;hello,world&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;4.在容量为C的管道中，第k个接收发生在k+C个发送完成之前。(The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;第4点推广了第一点的规则。这里其实有一点绕，举个例子：&lt;code&gt;第1个接收发生在1+C个发送完成之前&lt;/code&gt;。首先思考：第一个接收能否保证在0+C个发送完成之前？答案是不能。因为管道有C个容量的缓冲，C个发送语句发送完成前，完全可以不调用接收语句。那&lt;code&gt;第1个接收发生在1+C个发送完成之前&lt;/code&gt;如何保证，我们知道，当管道缓冲满了之后，就无法向管道中发送，发送语句会阻塞。因此必须在接收之后发送语句才能继续执行。 第四点规则使得&lt;code&gt;计数信号量可以由缓冲管道建模&lt;/code&gt;：管道中的数量对应了当前并发量，管道的容量对应了最大并发量。发送语句占用一个信用量，接收语句释放一个信号量。这是限制并发量的一个惯用手段。 下面的例子限制了最大并发量为3：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var limit = make(chan int, 3)

func main() {
    for _, w := range work {
        go func(w func()) {
            limit &amp;lt;- 1
            w()
            &amp;lt;-limit
        }(w)
    }
    select{}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的，我们也可以用lock和once来实现&lt;code&gt;happens before&lt;/code&gt;语义&lt;/p&gt;
&lt;h2&gt;不正确的同步方式&lt;/h2&gt;
&lt;p&gt;即使读r可以观察到同时发生的写w的值，但这并不意味这在r之后发生的读r&apos;可以观察到在w之前发生的写w&apos;。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a, b int

func f() {
    a = 1
    b = 2
}

func g() {
    print(b)
    print(a)
}

func main() {
    go f()
    g()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段程序可能打印出2、0 这种现象使得一些常见的方式失效。比如&lt;code&gt;双重锁定检查&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a string
var done bool

func setup() {
    a = &quot;hello, world&quot;
    done = true
}

func doprint() {
    if !done {
        once.Do(setup)
    }
    print(a)
}

func twoprint() {
    go doprint()
    go doprint()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段程序不能保证&lt;code&gt;print(a)&lt;/code&gt;时能够观察到a的值一定是&lt;code&gt;hello, world&lt;/code&gt;。 同样的，还有一种循环等待的写法也可能有问题，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a string
var done bool

func setup() {
    a = &quot;hello, world&quot;
    done = true
}

func main() {
    go setup()
    for !done {
    }
    print(a)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段程序也不保证&lt;code&gt;print(a)&lt;/code&gt;一定能打印出内容，甚至更坏的情况下无法观察到&lt;code&gt;done&lt;/code&gt;发生 了改变，因此程序会死循环下去。 还有一种衍生版本的写法也会有问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type T struct {
    msg string
}

var g *T

func setup() {
    t := new(T)
    t.msg = &quot;hello, world&quot;
    g = t
}

func main() {
    go setup()
    for g == nil {
    }
    print(g.msg)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即使&lt;code&gt;main&lt;/code&gt;协程观察到了g被赋值，也不一定能观察到&lt;code&gt;g.msg&lt;/code&gt;有值。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://golang.org/ref/mem&quot; rel=&quot;noopener&quot;&gt;The Go Memory Model（原文）&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>go</category><category>go内存模型</category><author>joyme123</author></item><item><title>用c写php扩展的笔记</title><link>https://www.myway5.com/blog/php-c-extesions/</link><guid isPermaLink="true">https://www.myway5.com/blog/php-c-extesions/</guid><description>1.使用php-src中ext文件夹中的ext\_skel生成项目框架 2.编辑config.m4,将其中三句话前面的dnl删除，改成下面这样。</description><pubDate>Thu, 23 Aug 2018 06:22:39 GMT</pubDate><content:encoded>&lt;h2&gt;编写php扩展的步骤:&lt;/h2&gt;
&lt;p&gt;1.使用php-src中ext文件夹中的ext_skel生成项目框架 2.编辑config.m4,将其中三句话前面的dnl删除，改成下面这样。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PHP_ARG_WITH(md2pic, for md2pic support,
Make sure that the comment is aligned:
[  --with-md2pic             Include md2pic support])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.执行phpize 4.执行./configure 5.使用&lt;code&gt;make&lt;/code&gt;编译 6.使用&lt;code&gt;make install&lt;/code&gt;安装扩展 7.将扩展加入php.ini中 8.使用&lt;code&gt;php -m&lt;/code&gt;检查扩展是否正常加载&lt;/p&gt;
&lt;h2&gt;关于config.m4&lt;/h2&gt;
&lt;p&gt;config.m4相当于一个构建系统，在php扩展的开发中，我的理解就是它可以用来配置lib，include，flags等编译时的属性以及其他的一些功能。这里给出一个配置了其他的lib和include信息的config.m4文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dnl $Id$
dnl config.m4 for extension md2pic

dnl Comments in this file start with the string &apos;dnl&apos;.
dnl Remove where necessary. This file will not work
dnl without editing.

dnl If your extension references something external, use with:

PHP_ARG_WITH(md2pic, for md2pic support,
Make sure that the comment is aligned:
[  --with-md2pic             Include md2pic support])

dnl Otherwise use enable:

dnl PHP_ARG_ENABLE(md2pic, whether to enable md2pic support,
dnl Make sure that the comment is aligned:
dnl [  --enable-md2pic           Enable md2pic support])

if test &quot;$PHP_MD2PIC&quot; != &quot;no&quot;; then
  dnl Write more examples of tests here...

  dnl # --with-md2pic -&amp;gt; check with-path
  dnl SEARCH_PATH=&quot;/usr/local /usr&quot;     # you might want to change this
  dnl SEARCH_FOR=&quot;/include/md2pic.h&quot;  # you most likely want to change this
  dnl if test -r $PHP_MD2PIC/$SEARCH_FOR; then # path given as parameter
  dnl   MD2PIC_DIR=$PHP_MD2PIC
  dnl else # search default path list
  dnl   AC_MSG_CHECKING([for md2pic files in default path])
  dnl   for i in $SEARCH_PATH ; do
  dnl     if test -r $i/$SEARCH_FOR; then
  dnl       MD2PIC_DIR=$i
  dnl       AC_MSG_RESULT(found in $i)
  dnl     fi
  dnl   done
  dnl fi
  dnl
  dnl if test -z &quot;$MD2PIC_DIR&quot;; then
  dnl   AC_MSG_RESULT([not found])
  dnl   AC_MSG_ERROR([Please reinstall the md2pic distribution])
  dnl fi

  dnl # --with-md2pic -&amp;gt; add include path

  PHP_ADD_INCLUDE(src/libMultiMarkdown/include)

  LIBNAME=gd # you may want to change this
  LIBSYMBOL=gdImageCreate # you most likely want to change this 

  PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,
  [
    PHP_ADD_LIBRARY_WITH_PATH(gd,&quot;/usr/lib&quot;, MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(curl,&quot;/usr/lib&quot;, MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(png,&quot;/usr/lib&quot;, MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(z,&quot;/usr/lib&quot;, MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(jpeg,&quot;/usr/lib&quot;, MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(freetype,&quot;/usr/lib&quot;, MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(m,&quot;/usr/lib&quot;, MD2PIC_SHARED_LIBADD)
    AC_DEFINE(HAVE_MD2PICLIB,1,[ ])
  ],[
    AC_MSG_ERROR([wrong md2pic lib version or lib not found])
  ],[

  ])


  dnl
  PHP_SUBST(MD2PIC_SHARED_LIBADD)
  PHP_NEW_EXTENSION(md2pic, [md2pic.c \
  src/libMultiMarkdown/aho-corasick.c \
  src/libMultiMarkdown/beamer.c \
  src/libMultiMarkdown/char.c \
  src/libMultiMarkdown/critic_markup.c \
  src/libMultiMarkdown/d_string.c \
  src/libMultiMarkdown/epub.c \
  src/libMultiMarkdown/file.c \
  src/libMultiMarkdown/html.c \
  src/libMultiMarkdown/latex.c \
  src/libMultiMarkdown/lexer.c \
  src/libMultiMarkdown/memoir.c \
  src/libMultiMarkdown/miniz.c \
  src/libMultiMarkdown/mmd.c \
  src/libMultiMarkdown/object_pool.c \
  src/libMultiMarkdown/opendocument-content.c \
  src/libMultiMarkdown/opendocument.c \
  src/libMultiMarkdown/scanners.c \
  src/libMultiMarkdown/stack.c \
  src/libMultiMarkdown/textbundle.c \
  src/libMultiMarkdown/token_pairs.c \
  src/libMultiMarkdown/token.c \
  src/libMultiMarkdown/transclude.c \
  src/libMultiMarkdown/rng.c \
  src/libMultiMarkdown/uuid.c \
  src/libMultiMarkdown/writer.c \
  src/libMultiMarkdown/zip.c \
  src/libMultiMarkdown/parser.c \
  src/libMultiMarkdown/pic.c], $ext_shared,, [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 ] )
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编写php扩展的资料&lt;/h2&gt;
&lt;p&gt;我这里主要参考的是 &lt;a href=&quot;https://github.com/pangudashu/php7-internal&quot; rel=&quot;noopener&quot;&gt;php内核剖析&lt;/a&gt;这本书。 php的扩展其实也可以用c++开发。这里有一个很好的项目&lt;a href=&quot;https://github.com/swoole/phpx&quot; rel=&quot;noopener&quot;&gt;php-x&lt;/a&gt;，并且开发扩展也要容易很多。&lt;/p&gt;
</content:encoded><category>linux</category><category>php</category><category>php_extentsion</category><author>joyme123</author></item><item><title>HTTP协议中的缓存控制</title><link>https://www.myway5.com/blog/http-cache-control/</link><guid isPermaLink="true">https://www.myway5.com/blog/http-cache-control/</guid><description>HTTP协议中有以下的头部字段和缓存相关（很多内容都是复制的MDN的文档）</description><pubDate>Mon, 13 Aug 2018 15:33:04 GMT</pubDate><content:encoded>&lt;h2&gt;一、总览&lt;/h2&gt;
&lt;p&gt;HTTP协议中有以下的头部字段和缓存相关（很多内容都是复制的MDN的文档）&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段名&lt;/th&gt;
&lt;th&gt;请求头包含&lt;/th&gt;
&lt;th&gt;响应头包含&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;出现的协议版本&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cache-Control&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是否缓存、缓存时间、缓存验证等&lt;/td&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pragma&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;只有一种用法：Pragma: no-cache。在响应头中没有规定&lt;/td&gt;
&lt;td&gt;HTTP/1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vary&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;它决定了对于未来的一个请求头，应该用一个缓存的回复(response)还是向源服务器请求一个新的回复&lt;/td&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If-Match&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;在请求方法为 GET 和 HEAD 的情况下，服务器仅在请求的资源满足此首部列出的 ETag 之一时才会返回资源。而对于 PUT 或其他非安全方法来说，只有在满足条件的情况下才可以将资源上传&lt;/td&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If-None-Match&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;对于 GET 和 HEAD 请求方法来说，当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候，服务器端会才返回所请求的资源，响应码为 200 。对于其他方法来说，当且仅当最终确认没有已存在的资源的 ETag 属性值与这个首部中所列出的相匹配的时候，才会对请求进行相应的处理。&lt;/td&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If-Modified-Since&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回，状态码为200。如果请求的资源从那时起未经修改，那么返回一个不带有消息主体的304响应，而在 Last-Modified 首部中会带有上次修改时间。不同于If-Unmodified-Since, If-Modified-Since 只可以用在 GET 或 HEAD 请求中。&lt;/td&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If-Unmodified-Since&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;只有当资源在指定的时间之后没有进行过修改的情况下，服务器才会返回请求的资源，或是接受 POST 或其他 non-safe 方法的请求。如果所请求的资源在指定的时间之后发生了修改，那么会返回 412 (Precondition Failed) 错误。&lt;/td&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ETag&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;TagHTTP响应头是资源的特定版本的标识符。这可以让缓存更高效，并节省带宽，因为如果内容没有改变，Web服务器不需要发送完整的响应。而如果内容发生了变化，使用ETag有助于防止资源的同时更新相互覆盖（“空中碰撞”）&lt;/td&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expires&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;Expires 响应头包含日期/时间， 即在此时候之后，响应过期&lt;/td&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last-Modified&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;包含源头服务器认定的资源做出修改的日期及时间。 它通常被用作一个验证器来判断接收到的或者存储的资源是否彼此一致。由于精确度比 ETag 要低，所以这是一个备用机制。包含有 If-Modified-Since 或 If-Unmodified-Since 首部的条件请求会使用这个字段。&lt;/td&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;消息生成的时间&lt;/td&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If-Range&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;If-Range HTTP 请求头字段用来使得 Range 头字段在一定条件下起作用：当字段值中的条件得到满足时，Range 头字段才会起作用，同时服务器回复206 部分内容状态码，以及Range 头字段请求的相应部分；如果字段值中的条件没有得到满足，服务器将会返回 200 OK 状态码，并返回完整的请求资源。&lt;/td&gt;
&lt;td&gt;HTTP/1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;二、详细说明&lt;/h2&gt;
&lt;p&gt;一眼看上去，缓存相关的字段确实有很多。但是实际上，稍微理一理思路即可。 上面所有的字段都在围绕着3个点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.是否要缓存&lt;/li&gt;
&lt;li&gt;2.缓存多久&lt;/li&gt;
&lt;li&gt;3.缓存是否有效&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.1 是否要缓存&lt;/h3&gt;
&lt;p&gt;一个HTTP的客户端（包括浏览器，以及CDN等缓存代理）如何知道当前的请求是否要缓存呢？ 在&lt;code&gt;Cache-Control&lt;/code&gt;中，有下列取值来决定是否缓存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public:表明响应可以被任何对象（包括：发送请求的客户端，代理服务器，等等）缓存。
private:表明响应只能被单个用户缓存，不能作为共享缓存（即代理服务器不能缓存它）,可以缓存响应内容。
no-store:缓存不应存储有关客户端请求或服务器响应的任何内容。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 缓存多久&lt;/h3&gt;
&lt;p&gt;源服务器上的内容可能随时发生变化，那么如何知道什么时候去检查缓存是否更新了呢？HTTP协议中有以下字段规定了一个缓存的有效期。 &lt;strong&gt;Cache-Control&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;max-age={seconds}：设置缓存存储的最大周期，超过这个时间缓存被认为过期(单位秒)。与Expires相反，时间是相对于请求的时间。
s-maxage={seconds}：覆盖max-age 或者 Expires 头，但是仅适用于共享缓存(比如各个代理)，并且私有缓存中它被忽略。
max-stale[={seconds}]：表明客户端愿意接收一个已经过期的资源。 可选的设置一个时间(单位秒)，表示响应不能超过的过时时间。
min-fresh={seconds}：表示客户端希望在指定的时间内获取最新的响应。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expire&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Expires: {http-date}: 表示在http-date之后，这个缓存就过期了。如果http-date是一个无效的时间值，则代表已过期。如果在Cache-Control响应头设置了 &quot;max-age&quot; 或者 &quot;s-max-age&quot; 指令，那么 Expires 头会被忽略。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Date和Last-Modified&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;如果在`Cache-Control`和`Expire`都没有返回的情况下，也可以通过`Date`头和`Last-Modified`头去计算缓存的有效期。缓存的寿命就等于头里面Date的值减去Last-Modified的值除以10。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 缓存是否有效&lt;/h3&gt;
&lt;p&gt;源服务器上的内容可能随时发生变化, 那么HTTP客户端如果知道自己缓存的内容是否有效呢？这里要分几种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务器的响应中有&lt;code&gt;Cache-Control:must-revalidate&lt;/code&gt;头：当前缓存在有效时间内，此时缓存默认就是有效的。当前缓存过了有效时间，则会向服务器验证缓存是否过期。如果服务返回304，则代表缓存没有过期。&lt;/li&gt;
&lt;li&gt;服务器的响应中有&lt;code&gt;Cache-Control: no-cache&lt;/code&gt;或&lt;code&gt;Pragma:no-cache&lt;/code&gt;头：则表示每一次都要从服务器验证缓存是否过期。no-cache的优先级是要大于Pragma的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注：no-cache和must-revalidate的区别&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在RFC7234中说到： &quot;must-revalidate&quot; 响应头指令表示一旦该响应过期，这个缓存在向源服务器成功验证之前禁止使用。在任何情况下，一个缓存都必须遵循&quot;must-revalidate&quot;指令；特殊情况下，如果源服务器无法连接，必须生成504(Getway Timeout)响应。 &quot;no-cache&quot;响应头指令表示缓存在向源服务器成功验证之前禁止使用（注：不论缓存是否过期）。如果&quot;no-cache&quot;指令指明了一或多个字段，缓存可以被用来响应之后的请求。但是，在没有和源服务器进行验证的情况下，任何&quot;no-cache&quot;中列出的字段都禁止在之后的响应中被发送。这使得源服务器可以阻止某些字段被重复使用，但是仍然可以缓存响应的其他部分。 &quot;no-cache&quot;中的字段不仅仅限于http1.1协议中列举出来的字段。字段名是大小写不敏感的。使用双引号包围。 因此个人认为，在某些时候max-age=0;must-revalidate 可以等同于 no-cache。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么这个验证机制是什么样的？也分几种情况&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据文件指纹&lt;code&gt;ETag&lt;/code&gt;：在服务器返回了一个文件的&lt;code&gt;ETag&lt;/code&gt;的情况下，HTTP客户端可以根据&lt;code&gt;If-None-Match&lt;/code&gt;或&lt;code&gt;If-Match&lt;/code&gt;来向服务器验证当前缓存是否过期。&lt;/li&gt;
&lt;li&gt;根据文件修改时间：在服务器返回了&lt;code&gt;Last-Modified&lt;/code&gt;的情况下，HTTP客户端可以根据&lt;code&gt;If-Unmodified-Since&lt;/code&gt;或&lt;code&gt;If-Modified-Since&lt;/code&gt;来向服务器验证当前缓存是否过期。&lt;/li&gt;
&lt;li&gt;一个比较特殊的&lt;code&gt;If-Range&lt;/code&gt;：&lt;code&gt;If-Range&lt;/code&gt;通常出现在分段请求当中，用来分段请求的资源主体是否发生了变化。它的值既可以是etag，也可以是GMT时间戳。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注：因为Last-Modified精确到秒，在精确度上比ETag低，所以应该以ETag为主。 2.4 关于vary字段 上面说了三个点，但是没有涉及到vary字段。vary和缓存并不是直接相关的。取一段MDN的说明：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Vary 是一个HTTP响应头部信息，它决定了对于未来的一个请求头，应该用一个缓存的回复(response)还是向源服务器请求一个新的回复。它被服务器用来表明在 content negotiation algorithm（内容协商算法）中选择一个资源代表的时候应该使用哪些头部信息（headers）. 在响应状态码为 304 Not Modified 的响应中，也要设置 Vary 首部，而且要与相应的 200 OK 响应设置得一模一样。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;举个例子，如果服务器返回的网页是分手机版和电脑版的，一般我们会根据user-agent来判断浏览器是手机浏览器还是电脑上的浏览器。假设有一个中间代理的请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;手机用户1请求index.html----------&amp;gt;中间代理------------&amp;gt;源服务器
电脑用户1请求index.html----------&amp;gt;中间代理------------&amp;gt;源服务器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在电脑用户1请求index.html时，中间代理会向原服务器请求还是直接使用本地缓存的副本呢？ 如果原服务器在第一次请求时响应头中有&lt;code&gt;vary:user-agent&lt;/code&gt;则会重新请求。因为两次请求的user-agent是不同的，因此缓存不能被重复使用。但是如果没有指定则使用本地缓存作为响应。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;参考文档&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://imweb.io/topic/5795dcb6fb312541492eda8c&quot; rel=&quot;noopener&quot;&gt;HTTP缓存控制小结&lt;/a&gt; &lt;a href=&quot;https://imququ.com/post/vary-header-in-http.html&quot; rel=&quot;noopener&quot;&gt;HTTP 协议中 Vary 的一些研究&lt;/a&gt; &lt;a href=&quot;http://www.cnblogs.com/chyingp/p/no-cache-vs-must-revalidate.html&quot; rel=&quot;noopener&quot;&gt;http://www.cnblogs.com/chyingp/p/no-cache-vs-must-revalidate.html&lt;/a&gt; &lt;a href=&quot;https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=zh-cn&quot; rel=&quot;noopener&quot;&gt;HTTP缓存&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching_FAQ&quot; rel=&quot;noopener&quot;&gt;HTTP Caching | MDN&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control&quot; rel=&quot;noopener&quot;&gt;Cache-Control | MDN&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Pragma&quot; rel=&quot;noopener&quot;&gt;Pragma | MDN&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Vary&quot; rel=&quot;noopener&quot;&gt;Vary | MDN&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/If-Match&quot; rel=&quot;noopener&quot;&gt;If-Match | MDN&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/If-None-Match&quot; rel=&quot;noopener&quot;&gt;If-None-Match | MDN&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/If-Modified-Since&quot; rel=&quot;noopener&quot;&gt;If-Modified-Since | MDN&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/If-Unmodified-Since&quot; rel=&quot;noopener&quot;&gt;If-Unmodified-Since | MDN&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/ETag&quot; rel=&quot;noopener&quot;&gt;ETag | MDN&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Expires&quot; rel=&quot;noopener&quot;&gt;Expires | MDN&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Last-Modified&quot; rel=&quot;noopener&quot;&gt;Last-Modified | MDN&lt;/a&gt; &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/If-Range&quot; rel=&quot;noopener&quot;&gt;If-Range | MDN&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>网络协议</category><category>HTTP协议</category><category>缓存控制</category><author>joyme123</author></item><item><title>从php-fpm解析FastCGI协议</title><link>https://www.myway5.com/blog/php-fpm-fastcgi/</link><guid isPermaLink="true">https://www.myway5.com/blog/php-fpm-fastcgi/</guid><description>这是一篇类似于开发笔记的文章，从php-fpm与nginx的tcp请求中，去理解FastCGI协议，因此不会详细的阐述FastCGI协议到底是什么样的。 从一段抓包说起</description><pubDate>Tue, 07 Aug 2018 08:21:35 GMT</pubDate><content:encoded>&lt;p&gt;这是一篇类似于开发笔记的文章，从php-fpm与nginx的tcp请求中，去理解FastCGI协议，因此不会详细的阐述FastCGI协议到底是什么样的。&lt;/p&gt;
&lt;h2&gt;从一段抓包说起&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&quot;No.&quot;,&quot;Time&quot;,&quot;Source&quot;,&quot;Destination&quot;,&quot;Protocol&quot;,&quot;Length&quot;,&quot;Info&quot;
&quot;431&quot;,&quot;22.975896289&quot;,&quot;127.0.0.1&quot;,&quot;127.0.0.1&quot;,&quot;TCP&quot;,&quot;76&quot;,&quot;55928  &amp;gt;  9000 [SYN] Seq=0 Win=43690 Len=0 MSS=65495 SACK_PERM=1 TSval=1732184618 TSecr=0 WS=128&quot;
&quot;432&quot;,&quot;22.975910047&quot;,&quot;127.0.0.1&quot;,&quot;127.0.0.1&quot;,&quot;TCP&quot;,&quot;76&quot;,&quot;9000  &amp;gt;  55928 [SYN, ACK] Seq=0 Ack=1 Win=43690 Len=0 MSS=65495 SACK_PERM=1 TSval=1732184618 TSecr=1732184618 WS=128&quot;
&quot;433&quot;,&quot;22.975920352&quot;,&quot;127.0.0.1&quot;,&quot;127.0.0.1&quot;,&quot;TCP&quot;,&quot;68&quot;,&quot;55928  &amp;gt;  9000 [ACK] Seq=1 Ack=1 Win=43776 Len=0 TSval=1732184618 TSecr=1732184618&quot;
&quot;434&quot;,&quot;22.975948796&quot;,&quot;127.0.0.1&quot;,&quot;127.0.0.1&quot;,&quot;TCP&quot;,&quot;1356&quot;,&quot;55928  &amp;gt;  9000 [PSH, ACK] Seq=1 Ack=1 Win=43776 Len=1288 TSval=1732184618 TSecr=1732184618&quot;
&quot;435&quot;,&quot;22.975953739&quot;,&quot;127.0.0.1&quot;,&quot;127.0.0.1&quot;,&quot;TCP&quot;,&quot;68&quot;,&quot;9000  &amp;gt;  55928 [ACK] Seq=1 Ack=1289 Win=174720 Len=0 TSval=1732184618 TSecr=1732184618&quot;
&quot;452&quot;,&quot;23.068068706&quot;,&quot;127.0.0.1&quot;,&quot;127.0.0.1&quot;,&quot;TCP&quot;,&quot;660&quot;,&quot;9000  &amp;gt;  55928 [PSH, ACK] Seq=1 Ack=1289 Win=174720 Len=592 TSval=1732184710 TSecr=1732184618&quot;
&quot;453&quot;,&quot;23.068076923&quot;,&quot;127.0.0.1&quot;,&quot;127.0.0.1&quot;,&quot;TCP&quot;,&quot;68&quot;,&quot;55928  &amp;gt;  9000 [ACK] Seq=1289 Ack=593 Win=44928 Len=0 TSval=1732184710 TSecr=1732184710&quot;
&quot;454&quot;,&quot;23.068097717&quot;,&quot;127.0.0.1&quot;,&quot;127.0.0.1&quot;,&quot;TCP&quot;,&quot;68&quot;,&quot;9000  &amp;gt;  55928 [FIN, ACK] Seq=593 Ack=1289 Win=174720 Len=0 TSval=1732184710 TSecr=1732184710&quot;
&quot;455&quot;,&quot;23.068153021&quot;,&quot;127.0.0.1&quot;,&quot;127.0.0.1&quot;,&quot;TCP&quot;,&quot;68&quot;,&quot;55928  &amp;gt;  9000 [FIN, ACK] Seq=1289 Ack=594 Win=44928 Len=0 TSval=1732184710 TSecr=1732184710&quot;
&quot;456&quot;,&quot;23.068163150&quot;,&quot;127.0.0.1&quot;,&quot;127.0.0.1&quot;,&quot;TCP&quot;,&quot;68&quot;,&quot;9000  &amp;gt;  55928 [ACK] Seq=594 Ack=1290 Win=174720 Len=0 TSval=1732184710 TSecr=1732184710&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了抓这段包，需要将php-fpm中的监听地址改成tcp socket。注意:tcp socket的性能远远低于unix socket。 可以看到，这里面除了tcp的握手和断开以及应答部分，有&lt;code&gt;PSH&lt;/code&gt;标志的是FastCGI的具体协议内容。可以看到nginx给php-fpm发送了一段数据，之后php-fpm进行响应。FastCGI协议就是这样简单的使用tcp协议，使得Web Server可以转发HTTP请求到FastCGI应用程序上，具体的协议内容可以参考&lt;a href=&quot;https://www.myway5.com/index.php/2018/07/19/fastcgi-%E8%A7%84%E8%8C%83%E4%B8%AD%E6%96%87%E7%BF%BB%E8%AF%91/&quot; rel=&quot;noopener&quot;&gt;FastCGI规范中文翻译&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;php-fpm在单次请求结束后，会主动断开连接，而在FastCGI协议中，明确说明单次连接是可以复用的。&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/43280573/whether-the-connection-between-php-fpm-and-nginx-by-fast-cgi-are-persistent-kee&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/43280573/whether-the-connection-between-php-fpm-and-nginx-by-fast-cgi-are-persistent-kee&lt;/a&gt; 这个链接中有关于nginx和php-fpm连接释放的相关说明。 web server 可以将关闭权限委托给php-fpm,这样php-fpm在每次请求结束后就会关闭。 将关闭权限委托给php-fpm的好处就是不会因为连接的占用导致子进程不释放。但是不断的建立和断开连接也会影响性能。&lt;/p&gt;
&lt;h2&gt;当前php-fpm和nginx的主动断开连接是否会影响性能&lt;/h2&gt;
&lt;p&gt;会影响性能，但是并不推荐保持连接。 如果希望php-fpm不主动关闭连接，可以使用以下设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Syntax: fastcgi_keep_conn on | off;
Default:    
fastcgi_keep_conn off;
Context:    http, server, location
This directive appeared in version 1.1.4.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记得在upstream中使用keepalive选项&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;upstream backend {
    server 127.0.0.1:9000
    keepalive 20
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是缺点也很明显，如果用户请求一直和nginx保持连接，那么nginx也不会释放该与php-fpm的连接。这样会一直占用php-fpm的子进程不释放。当达到php-fpm的最大子进程时，就会拒绝其他的请求。 同时需要注意的是，如果nginx和php-fpm都在本地，不断的重新建立连接的影响是很小的。因此并不推荐将fastcgi_keep_conn选项打开。 这里是一些压测数据(pm.max_children 设置为20,这里只使用20个并发)： 测试命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ab -k -n 100000 -c 20 http://localhost/php/index.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在主动断开FastCGI连接的情况下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Server Software:        nginx/1.13.3
Server Hostname:        localhost
Server Port:            80

Document Path:          /php/index.php
Document Length:        60 bytes

Concurrency Level:      20
Time taken for tests:   28.334 seconds
Complete requests:      100000
Failed requests:        0
Keep-Alive requests:    0
Total transferred:      22900000 bytes
HTML transferred:       6000000 bytes
Requests per second:    3529.39 [#/sec] (mean)
Time per request:       5.667 [ms] (mean)
Time per request:       0.283 [ms] (mean, across all concurrent requests)
Transfer rate:          789.29 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0      14
Processing:     1    5   2.4      5     212
Waiting:        0    5   2.4      5     212
Total:          1    6   2.5      5     212

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在不断开连接的情况下： 测试一直没法正常完成，部分请求会超时。 &lt;strong&gt;因此FastCGI是没有必要保持连接的，这会大大降低并发度。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;benchmark 压测,请求直接超时退出&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ab -k -c 100 -n 10000 &quot;http://localhost/php/index.php&quot;&lt;/code&gt; php-fpm有一个子进程数量的限制，在并发过高时，没有办法为每一个请求分配一个子进程，导致请求一直在等待，直至超时退出。&lt;/p&gt;
&lt;h2&gt;unix socket和tcp socket的区别&lt;/h2&gt;
&lt;p&gt;unix socket相对于tcp socket来说，性能会提升很多。 unix socket虽然也有个socket，但是和网络一点关系都没有。unix socket是进程间的通信。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;unix socket不需要经过网络协议栈，不需要打包拆包、计算校验和、维护序号和应答等，只是将应用层数据从一个进程拷贝到另一个进程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;实现FastCGI协议时，tcp连接中读到EOF代表了什么&lt;/h2&gt;
&lt;p&gt;在写代码过程中，tcp连接读到了EOF。从表面上来看，是读到了流的结束。但也意味着对端至少关闭了写通道。这是因为php-fpm读到了它无法理解的请求，因此直接关闭了连接。&lt;/p&gt;
&lt;h2&gt;在开发过程中，遇到了php-fpm进程不释放的问题&lt;/h2&gt;
&lt;p&gt;在BeginRequestRecord中，将flags置为1，这样与fastcgi的连接会一直保持。但是我在tcp连接中读到EOF时，却没有释放这个连接。因此这个连接会占用一个php-fpm子进程不会释放。只要手动释放这个连接即可，或者将flags设为0。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://xiaoxia.org/2009/10/05/fastcgi-protocol-analysis/&quot; rel=&quot;noopener&quot;&gt;FastCGI协议分析&lt;/a&gt; &lt;a href=&quot;https://blog.jjonline.cn/linux/218.html&quot; rel=&quot;noopener&quot;&gt;Nginx支持PHP的PATHINFO模式配置分析&lt;/a&gt; &lt;a href=&quot;https://blog.csdn.net/guxch/article/details/7041052&quot; rel=&quot;noopener&quot;&gt;Linux下的IPC－UNIX Domain Socket&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>php</category><category>php-fpm</category><category>fastcgi</category><author>joyme123</author></item><item><title>FastCGI 规范中文翻译</title><link>https://www.myway5.com/blog/fastcgi/</link><guid isPermaLink="true">https://www.myway5.com/blog/fastcgi/</guid><description>原文地址：https://fastcgi-archives.github.io/FastCGI\Specification.html 1.简介 2.初始处理状态 2.1 参数列表 2.2 文件描述符 2.3 环境变量 2.4 其他状态 3.协议基础 3.1 符号 3.</description><pubDate>Thu, 19 Jul 2018 14:29:46 GMT</pubDate><content:encoded>&lt;p&gt;原文地址：&lt;a href=&quot;https://fastcgi-archives.github.io/FastCGI%5C_Specification.html&quot; rel=&quot;noopener&quot;&gt;https://fastcgi-archives.github.io/FastCGI\_Specification.html&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#1&quot; title=&quot;简介&quot; rel=&quot;noopener&quot;&gt;1.简介&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2&quot; rel=&quot;noopener&quot;&gt;2.初始处理状态&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#2.1&quot; rel=&quot;noopener&quot;&gt;2.1 参数列表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2.2&quot; rel=&quot;noopener&quot;&gt;2.2 文件描述符&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2.3&quot; rel=&quot;noopener&quot;&gt;2.3 环境变量&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2.4&quot; rel=&quot;noopener&quot;&gt;2.4 其他状态&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3&quot; rel=&quot;noopener&quot;&gt;3.协议基础&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#3.1&quot; rel=&quot;noopener&quot;&gt;3.1 符号&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3.2&quot; rel=&quot;noopener&quot;&gt;3.2 接受传输连接&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3.3&quot; rel=&quot;noopener&quot;&gt;3.3 记录&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3.4&quot; rel=&quot;noopener&quot;&gt;3.4 键值对&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3.5&quot; rel=&quot;noopener&quot;&gt;3.5 关闭传输连接&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4&quot; rel=&quot;noopener&quot;&gt;4.管理记录类型&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#4.1&quot; rel=&quot;noopener&quot;&gt;4.1 FCGI_GET_VALUES, FCGI_GET_VALUES_RESULT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4.2&quot; rel=&quot;noopener&quot;&gt;4.2 FCGI_UNKNOWN_TYPE&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5&quot; rel=&quot;noopener&quot;&gt;5.应用程序的记录类型&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#5.1&quot; rel=&quot;noopener&quot;&gt;5.1 FCGI_BEGIN_REQUEST, FCGI_GET_VALUES_RESULT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5.2&quot; rel=&quot;noopener&quot;&gt;5.2 键值对流：FCGI_PARAMS, FCGI_RESULTS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5.3&quot; rel=&quot;noopener&quot;&gt;5.3 字节流：FCGI_STDIN, FCGI_DATA, FCGI_STDOUT, FCGI_STDERR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5.4&quot; rel=&quot;noopener&quot;&gt;5.4 FCGI_ABORT_REQUEST&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5.5&quot; rel=&quot;noopener&quot;&gt;5.5 FCGI_END_REQUEST&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6&quot; rel=&quot;noopener&quot;&gt;6.角色&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#6.1&quot; rel=&quot;noopener&quot;&gt;6.1 角色协议&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6.2&quot; rel=&quot;noopener&quot;&gt;6.2 响应器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6.3&quot; rel=&quot;noopener&quot;&gt;6.3 授权器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6.4&quot; rel=&quot;noopener&quot;&gt;6.4 过滤器&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#7&quot; rel=&quot;noopener&quot;&gt;7.错误&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#8&quot; rel=&quot;noopener&quot;&gt;8.类型和常量&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#9&quot; rel=&quot;noopener&quot;&gt;9.参考文献&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#A&quot; rel=&quot;noopener&quot;&gt;A.表：记录类型的属性&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#B&quot; rel=&quot;noopener&quot;&gt;B. 典型的协议消息流&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.简介&lt;/h2&gt;
&lt;p&gt;FastCGI 是一种对 CGI 的开放扩展，在不改变 Web 服务的前提下，为所有的网络应用程序提供了很高的性能。 这个规范的目的很小：从应用程序角度来看，指定了一个 FastCGI 应用程序和一个支持 FaseCGI的 Web 服务之间的接口。许多 Web 服务的特性和 FastCGI相关，例如，应用程序管理工具，与Web服务器接口的应用程序无关，此处不再赘述。 这个规范适用于Unix（更确切的说，适用于支持 Berkeley Sockets 的 POSIX 系统）。规范的大部分是一个简单的通信协议，它独立于字节序，并将扩展到其他系统。 我们将通过比较 FastCGI 和常规的 CGI/1.1 的 Unix 实现来介绍它。 FastCGI 是被设计用于支持常驻内存的应用程序进程，例如，应用程序服务。常规的 CGI/1.1 的 Unix 实现的主要不同之处在于，CGI 会创建一个应用程序进程，响应一个请求之后就会退出。 FastCGI进程的初始状态比CGI / 1.1进程的初始状态更简洁，因为 FastCGI 进程在初始化时没有开始与任何事物连接。它没有常规的打开标准输入(stdin)、输出(stdout)和错误(stderr)流，并且它不会通过环境变量接受大量信息。在一个 FastCGI 进程中，关键的初始状态是监听一个 socket，这个socket会接收来自 Web 服务器的连接。 一个 FastCGI 进程在它监听的 socket 上接收一个连接时，进程会执行一个简单的协议去接收和发送数据。这个协议主要有两个目的。第一，在多个独立的 FastCGI 请求中，这个协议复用一个传输连接。这支持那些使用了事件驱动或多线程编程技术来处理并发请求的应用程序。第二，对于每一个请求，这个协议在每个传输方向上都提供了多个独立的数据流。这样，例如，stdout 和 stderr 数据都通过单个传输连接从应用程序传递到Web服务器，而不是像 CGI/1.1 那样需要单独的管道。 一个 FastCGI 应用程序扮演了明确定义的角色之一。我们最熟悉的是响应器角色，应用程序从一个 HTTP 请求中接收所有的信息，之后生成一个 HTTP 响应；这正是 CGI/1.1 程序所扮演的角色。第二个角色是认证器，应用程序从一个 HTTP 请求中接收所有的信息，之后生成一个认证通过/不通过的决定。第三个角色是过滤器，应用程序从一个 HTTP 请求中接收所有的信息，加上一个 Web 服务器中存储的额外的文件数据流，然后生成一个“过滤的”版本的数据流作为 HTTP 响应。这个框架是可扩展的，因此更多的 FastCGI 角色可以在以后定义。 在本说明书的其余部分中，术语“ FastCGI 应用程序”，“应用程序进程”或“应用程序服务器”在不会引起混淆的情况下缩写为“应用程序”&lt;/p&gt;
&lt;h2&gt;2.初始处理状态&lt;/h2&gt;
&lt;h3&gt;2.1 参数列表&lt;/h3&gt;
&lt;p&gt;默认情况下，Web 服务器创建一个包含单个元素的参数列表，应用程序的名字会被当作可执行文件路径名的最后一部分。Web 服务器可能提供了一种方法来指明一个不同的应用程序名称，或者一个更详细的参数列表。 注意，由 Web 服务器执行的文件可能是一个解释性脚本（一个文本文件，以#!开头），这种情况下，应用程序参数的构建如在execve联机帮助页中所述那样。&lt;/p&gt;
&lt;h3&gt;2.2 文件描述符&lt;/h3&gt;
&lt;p&gt;Web 服务器在应用程序开始执行时打开单个文件描述符FCGI_LISTENSOCK_FILENO。这个描述符指向由 Web 服务器创建的监听的socket。 FCGI_LISTENSOCK_FILENO 等价于 STDIN_FILENO 。标准的描述符 STDOUT_FILENO 和 STDERR_FILENO 在应用程序开始执行时被关闭。判断一个应用程序是被 CGI 还是 FastCGI 调用的可靠方法是：调用 getpeername(FCGI_LISTENSOCK_FILENO)，返回 -1 并将errno设置为ENOTCONN的就是 FastCGI 程序。 Web服务器选择可靠的传输，Unix流管道（AF_UNIX）或TCP/IP（AF_INET），隐含在FCGI_LISTENSOCK_FILENO套接字的内部状态中。&lt;/p&gt;
&lt;h3&gt;2.3 环境变量&lt;/h3&gt;
&lt;p&gt;Web 服务器可以使用环境变量去传递参数给应用程序。这个规范定义了一个这样的变量：FCGI_WEB_SERVER_ADDRS。我们期待随着规范的演变，会有更多的变量会被传递。Web 服务器可以提供一种方法去绑定其他的环境变量，比如 PATH 变量。&lt;/p&gt;
&lt;h3&gt;2.4 其他状态&lt;/h3&gt;
&lt;p&gt;Web 服务器可以提供一种方法去指明一个应用程序的初始处理状态的其他部分，比如优先级，用户 ID，用户组 ID，根目录，以及进程的工作目录。&lt;/p&gt;
&lt;h2&gt;3.协议基础&lt;/h2&gt;
&lt;h3&gt;3.1 符号&lt;/h3&gt;
&lt;p&gt;我们使用 C 语言符号去定义协议信息的格式。所有的结构体元素都使用 unsigned char 类型定义，并安排使ISO C编译器以常规方式将它们排列，没有填充。在结构体中，第一个字节会被第一个传输，第二个会被第二个传输，以此类推。 我们使用两个公约来概括我们的定义。 第一，当两个相邻的结构体组件名称相同时，除了后缀&quot;B1&quot;和&quot;B0&quot;，这意味着这两个组件可以被视为单个数字，计算为B1&amp;lt;&amp;lt;8 + B0。 第二，我们扩展 C 的结构体，允许以下的形式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct {
    unsigned char mumbleLengthB1;
    unsigned char mumbleLengthB0;
    ... /* other stuff */
    unsigned char mumbleData[mumbleLength];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这代表着一个变长的结构体，它的长度是由前面的组件的值决定的。&lt;/p&gt;
&lt;h3&gt;3.2 接受传输连接&lt;/h3&gt;
&lt;p&gt;一个 FastCGI 应用程序在由文件描述符 FCGI_LISTENSOCK_FILENO 引用的 socket 上调用 accept() 去接收一个新的传输连接。如果 accept() 成功了，FCGI_WEB_SERVER_ADDRS 环境变量被绑定，应用程序立即执行以下的特殊操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FCGI_WEB_SERVER_ADDRS: 这个值是 Web 服务器的有效的 ip 地址列表。&lt;/li&gt;
&lt;li&gt;如果 FCGI_WEB_SERVER_ADDRS 绑定了，应用程序检查新连接的对等 IP 地址是否在列表中。如果检查失败了（包括连接没有使用 TCP/IP 这种可能性），应用程序关掉连接来响应。
&lt;ul&gt;
&lt;li&gt;FCGI_WEB_SERVER_ADDRS 是由英文逗号分割的 IP 地址列表。每一个 IP 地址是由点号分割的4个0~255内的数字组成。例如：FCGI_WEB_SERVER_ADDRS=199.170.183.28,199.170.183.71 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应用程序可以接收多个并发传输连接，但是它不一定需要这样做。&lt;/p&gt;
&lt;h3&gt;3.3 记录&lt;/h3&gt;
&lt;p&gt;应用程序使用一个简单的协议从 Web 服务器获取请求并执行。协议的具体内容视应用程序的角色而定，但是一般来说，Web 服务器首先发送参数和其他数据到应用程序，之后应用程序发送结果数据给 Web 服务器，最终应用程序告诉 Web 服务器请求处理已经结束。 所有通过传输连接的数据都是在 FastCGI 记录(records)里的。FastCGI 记录完成两件事。第一，记录在多个独立的请求之间复用传输连接。这种复用支持使用事件驱动模型或多线程技术来处理并发请求的应用程序。第二，在同一个请求中，记录提供了在不同方向上多个独立的数据流。这样，stdout 和 stderr 可以使用同一个传输连接来传输，而不是需要不同的连接。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        typedef struct {
            unsigned char version;
            unsigned char type;
            unsigned char requestIdB1;
            unsigned char requestIdB0;
            unsigned char contentLengthB1;
            unsigned char contentLengthB0;
            unsigned char paddingLength;
            unsigned char reserved;
            unsigned char contentData[contentLength];
            unsigned char paddingData[paddingLength];
        } FCGI_Record;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个 FastCGI 记录包含一个定长的前缀，以及变长的内容和填充字节。一条记录包含7个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;版本号（version）:指定 FastCGI 协议的版本号。这个规范文档的版本号是 FCGI_VERSION_1。&lt;/li&gt;
&lt;li&gt;类型（type）：指定这条记录的类型。例如，记录的功能函数。具体的记录类型和功能函数在之后的章节有详细介绍。&lt;/li&gt;
&lt;li&gt;请求ID（requestId）：指定这条记录属于哪个 FastCGI 请求。&lt;/li&gt;
&lt;li&gt;内容长度（contentLength）：在contentData部分存储的字节数。&lt;/li&gt;
&lt;li&gt;填充长度（paddingLength）：在paddingData部分存储的字节数。&lt;/li&gt;
&lt;li&gt;内容数据（contentData）：在0到65535字节之间的数据，根据记录类型进行解释。&lt;/li&gt;
&lt;li&gt;填充数据（paddingData）：0到255个字节的数据，被忽略。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们使用宽松的C struct初始化语法来指定常量FastCGI记录。我们省略了版本号部分，忽略填充部分，并将requestId视为一个数字。因此 &lt;code&gt;{FCGI_END_REQUEST, 1, {FCGI_REQUEST_COMPLETE,0}&lt;/code&gt; 是一个 &lt;code&gt;type == FCGI_END_REQUEST, requestId == 1, and contentData == {FCGI_REQUEST_COMPLETE,0}&lt;/code&gt; 的记录。&lt;/p&gt;
&lt;h4&gt;Padding&lt;/h4&gt;
&lt;p&gt;协议允许发送者填充发送的记录，然后要求接收者解释 paddingLength，跳过 paddingData。Padding 允许发送者保持数据对齐，达到更高效的数据处理。使用X窗口系统协议的经验显示了这种对齐的性能优势。 我们推荐记录的长度是8字节的整数倍。一个 FastCGI 的固定长度部分正好是8个字节。&lt;/p&gt;
&lt;h4&gt;处理请求ID&lt;/h4&gt;
&lt;p&gt;Web服务器重用 FastCGI 的请求ID；在一个给定的传输连接上，应用程序追踪每个请求 ID 的当前状态。当应用程序收到一条记录{FCGI_BEGIN_REQUEST, R, …}，一个请求 ID R 置为活跃状态。当应用程序发送一条记录 {FCGI_END_REQUEST, R, …} 给 Web 服务器时，请求 ID R置为非活跃状态。 当请求 ID R 是非活跃的，应用程序会忽略所有的 requestId R 的记录，除了如上所述的 FCGI_BEGIN_REQUEST 记录。 Web 服务器试图保持 FastCGI 请求 ID 是一个很小的数字。这样应用程序就可以使用一个很短的数组来追踪请求 ID 的状态，而不是一个长的数组或是一个哈希表。应用程序可以选择在同一时间仅仅接收一条请求。这样应用程序可以简单的根据当前连接请求 ID 来检查 requestId。&lt;/p&gt;
&lt;h4&gt;记录类型&lt;/h4&gt;
&lt;p&gt;有两种阐述 FastCGI 记录类型的方法。 第一个区别是管理记录和应用程序记录。管理记录包含非特定于任何 Web 服务器请求的信息，例如有关应用程序的协议功能的信息。应用程序记录包含有关requestId组件标识的特定请求的信息。 第二个区别是离散记录和流记录。离散记录本身包含有意义的数据单元。流记录是流的一部分，例如，一系列的0或更多的非空记录（length != 0），之后紧跟着一个空记录（length 0）。流记录的 contentData 部分是一连串的字节组成。这个字节序列就是流的值。因此流的值是独立于它包含多少条记录，以及它的字节在非空记录中如何划分。 这两点解释是不相关的。在当前版本的 FastCGI 协议定义的记录类型中，所有的管理记录类型都是离散的记录类型，几乎所有的应用程序记录类型都是流记录类型。但是有三个应用程序记录类型是离散的，也不能保证在之后的版本中，一个管理记录类型是流式的。&lt;/p&gt;
&lt;h3&gt;3.4 键值对&lt;/h3&gt;
&lt;p&gt;在这些角色中，FastCGI 应用程序需要读写边长值的不同数字。因此采用一个标准格式去编码一个键值对是有用的。 FastCGI 发送的键值对格式：键长，值长，键，值。小于等于127字节可以用一个字节编码，大于127字节的用4个字节编码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
    unsigned char nameLengthB0;  /* nameLengthB0  &amp;gt;&amp;gt; 7 == 0 */
    unsigned char valueLengthB0; /* valueLengthB0 &amp;gt;&amp;gt; 7 == 0 */
    unsigned char nameData[nameLength];
    unsigned char valueData[valueLength];
} FCGI_NameValuePair11;

typedef struct {
    unsigned char nameLengthB0;  /* nameLengthB0  &amp;gt;&amp;gt; 7 == 0 */
    unsigned char valueLengthB3; /* valueLengthB3 &amp;gt;&amp;gt; 7 == 1 */
    unsigned char valueLengthB2;
    unsigned char valueLengthB1;
    unsigned char valueLengthB0;
    unsigned char nameData[nameLength];
    unsigned char valueData[valueLength
                    ((B3 &amp;amp; 0x7f) &amp;lt;&amp;lt; 24) + (B2 &amp;lt;&amp;lt; 16) + (B1 &amp;lt;&amp;lt; 8) + B0];
} FCGI_NameValuePair14;

typedef struct {
    unsigned char nameLengthB3;  /* nameLengthB3  &amp;gt;&amp;gt; 7 == 1 */
    unsigned char nameLengthB2;
    unsigned char nameLengthB1;
    unsigned char nameLengthB0;
    unsigned char valueLengthB0; /* valueLengthB0 &amp;gt;&amp;gt; 7 == 0 */
    unsigned char nameData[nameLength
                    ((B3 &amp;amp; 0x7f) &amp;lt;&amp;lt; 24) + (B2 &amp;lt;&amp;lt; 16) + (B1 &amp;lt;&amp;lt; 8) + B0];
    unsigned char valueData[valueLength];
} FCGI_NameValuePair41;

typedef struct {
    unsigned char nameLengthB3;  /* nameLengthB3  &amp;gt;&amp;gt; 7 == 1 */
    unsigned char nameLengthB2;
    unsigned char nameLengthB1;
    unsigned char nameLengthB0;
    unsigned char valueLengthB3; /* valueLengthB3 &amp;gt;&amp;gt; 7 == 1 */
    unsigned char valueLengthB2;
    unsigned char valueLengthB1;
    unsigned char valueLengthB0;
    unsigned char nameData[nameLength
                    ((B3 &amp;amp; 0x7f) &amp;lt;&amp;lt; 24) + (B2 &amp;lt;&amp;lt; 16) + (B1 &amp;lt;&amp;lt; 8) + B0];
    unsigned char valueData[valueLength
                    ((B3 &amp;amp; 0x7f) &amp;lt;&amp;lt; 24) + (B2 &amp;lt;&amp;lt; 16) + (B1 &amp;lt;&amp;lt; 8) + B0];
} FCGI_NameValuePair44;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个字节的高位表示长度的编码。高位是0表示一个字节编码，高位是1表示4个字节编码。 这样的键值对格式允许发送方发送二进制数据，使得接受者可以立即分配正确大小的存储空间，即使是很大的值。&lt;/p&gt;
&lt;h3&gt;3.5 关闭传输连接&lt;/h3&gt;
&lt;p&gt;Web 服务器控制传输连接的生命周期。Web 服务器可以在没有活跃请求时关闭连接。或者 Web 服务器可以将关闭权限委托给应用程序（请参阅FCGI_BEGIN_REQUEST)。在这种情况下，应用程序在指定的请求之后关闭连接。 这种灵活设计可以包容不同的应用程序风格。简单的应用程序一次只处理一个请求，每个请求都会建立一个连接。更复杂的应用将会处理并发请求，一个和多个传输连接，会长时间保持传输连接。 通过在完成写入响应时关闭传输连接，简单的应用程序可以显着提升性能。Web服务器需要控制长期连接的连接生存期。 当应用程序关闭连接或发现连接已关闭时，应用程序将启动新连接。&lt;/p&gt;
&lt;h2&gt;4.管理记录类型&lt;/h2&gt;
&lt;h3&gt;4.1 FCGI_GET_VALUES, FCGI_GET_VALUES_RESULT&lt;/h3&gt;
&lt;p&gt;Web 服务器可以查询应用程序中的特定变量。服务器通常会在应用程序启动时执行查询，以便自动化系统配置的某些方面。 应用程序接受一个查询，比如{FCGI_GET_VALUES, 0, …}。FCGI_GET_VALUES 记录的 contentData 部分包含一系列的具有空值的键值对。 应用程序通过发送一个带有值的记录{FCGI_GET_VALUES_RESULT, 0, …}来响应。如果应用程序不理解在查询中的某个变量名，它会从响应中忽略该名称。FCGI_GET_VALUES 被设计成允许一个开放结束集合的变量。初始集合变量提供信息去帮助服务器操作应用，以及连接管理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FCGI_MAX_CONNS：应用程序接收的并发传输连接的最大值。比如，1或10。&lt;/li&gt;
&lt;li&gt;FCGI_MAX_REQS：应用程序接收的并发请求的最大值。比如1或50。&lt;/li&gt;
&lt;li&gt;FCGI_MPXS_CONNS：如果应用程序不复用连接，这个值是0（例如，一个请求一个连接）。否则是1。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 FCGI_UNKNOWN_TYPE&lt;/h3&gt;
&lt;p&gt;管理记录类型集可能会在此协议的未来版本中增长。为了提供这种演变，该协议包括 FCGI_UNKNOWN_TYPE 管理记录。当应用程序收到其类型T不理解的管理记录时，应用程序将使用{FCGI_UNKNOWN_TYPE，0，{T}}进行响应。 FCGI_UNKNOWN_TYPE记录的contentData部分具有以下形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
    unsigned char type;    
    unsigned char reserved[7];
} FCGI_UnknownTypeBody;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类型组件是无法识别的管理记录的类型。&lt;/p&gt;
&lt;h2&gt;5.应用的记录类型&lt;/h2&gt;
&lt;h3&gt;5.1 FCGI_BEGIN_REQUEST, FCGI_GET_VALUES_RESULT&lt;/h3&gt;
&lt;p&gt;Web服务发送一个 FCGI_BEGIN_REQUEST 记录来开始一个请求。 一个 FCGI_BEGIN_REQUEST 记录的 contentData 部分有以下形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
    unsigned char roleB1;
    unsigned char roleB0;
    unsigned char flags;
    unsigned char reserved[5];
} FCGI_BeginRequestBody;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;角色组件设置Web服务器期望应用程序扮演的角色。当前定义的角色是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FCGI_RESPONDER&lt;/li&gt;
&lt;li&gt;FCGI_AUTHORIZER&lt;/li&gt;
&lt;li&gt;FCGI_FILTER&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;角色定义具体在第六章描述。 flags部分包含一个控制连接关闭的位：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;flags &amp;amp; FCGI_KEEP_CONN: 如果是0，应用程序在响应请求后关闭连接。如果不是0，应用程序在响应请求后不关闭连接；Web 服务器保持对连接的管理权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 键值对流：FCGI_PARAMS, FCGI_RESULTS&lt;/h3&gt;
&lt;h4&gt;FCGI_PARAMS&lt;/h4&gt;
&lt;p&gt;是一种流记录类型，用于从Web服务器向应用程序发送键值对。名称 - 值对一个接一个地沿着流向下发送，没有指定的顺序。&lt;/p&gt;
&lt;h3&gt;5.3 字节流：FCGI_STDIN, FCGI_DATA, FCGI_STDOUT, FCGI_STDERR&lt;/h3&gt;
&lt;h4&gt;FCGI_STDIN&lt;/h4&gt;
&lt;p&gt;是一种流记录类型，用于从Web服务器向应用程序发送任意数据。 FCGI_DATA是第二个流记录类型，用于向应用程序发送其他数据。 FCGI_STDOUT和FCGI_STDERR是流记录类型，用于分别从应用程序向Web服务器发送任意数据和错误数据。&lt;/p&gt;
&lt;h3&gt;5.4 FCGI_ABORT_REQUEST&lt;/h3&gt;
&lt;p&gt;Web服务器发送 FCGI_ABORT_REQUEST 记录以中止请求。收到{FCGI_ABORT_REQUEST，R}后，应用程序会尽快响应{FCGI_END_REQUEST，R，{FCGI_REQUEST_COMPLETE，appStatus}}。这确实是来自应用程序的响应，而不是来自FastCGI库的低级别确认。 当HTTP客户端关闭其传输连接而来自客户端FastCGI请求正运行到一半时，Web服务器将中止FastCGI请求。这种情况似乎不太可能，大多数FastCGI请求的响应时间都很短，如果客户端速度很慢，Web服务器会提供输出缓冲。但FastCGI应用程序可能与其他系统通信有延迟或正执行服务器推送。 当Web服务器未通过传输连接复用请求时，Web服务器可以通过关闭请求的传输连接来中止请求。但是对于多路复用的请求，关闭传输连接会导致中止连接上的所有请求，这是一种令人遗憾的结果。&lt;/p&gt;
&lt;h3&gt;5.5 FCGI_END_REQUEST&lt;/h3&gt;
&lt;p&gt;应用程序发送FCGI_END_REQUEST记录以终止请求，既可能因为应用程序已处理请求，也可能应用程序已拒绝该请求。 FCGI_END_REQUEST记录的contentData部分具有以下形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
    unsigned char appStatusB3;
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
    unsigned char protocolStatus;
    unsigned char reserved[3];
} FCGI_EndRequestBody;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;appStatus组件是应用程序级状态代码。每个角色都在文档上记录了它对appStatus的使用。 protocolStatus组件是协议级状态代码;可能的protocolStatus值是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FCGI_REQUEST_COMPLETE：正常的请求结束。&lt;/li&gt;
&lt;li&gt;FCGI_CANT_MPX_CONN：拒绝新请求。当Web服务器通过一个连接将并发请求发送到旨在每个连接一次处理一个请求的应用程序时，就会发生这种情况。&lt;/li&gt;
&lt;li&gt;FCGI_OVERLOADED：拒绝新请求。当应用程序耗尽某些资源时会发生这种情况，例如：数据库连接。&lt;/li&gt;
&lt;li&gt;FCGI_UNKNOWN_ROLE：拒绝新请求。当Web服务器指定了应用程序未知的角色时，会发生这种情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6.角色&lt;/h2&gt;
&lt;h3&gt;6.1 角色协议&lt;/h3&gt;
&lt;p&gt;角色协议仅包括具有应用程序记录类型的记录。它们都使用流传输几乎所有的数据。 为了使协议可靠并简化应用程序编程，角色协议被设计使用&lt;code&gt;几乎连续的编组（nearly sequential marshalling.）&lt;/code&gt;。具有&lt;code&gt;严格连续编组（strictly sequential marshalling）&lt;/code&gt;的协议中，应用程序接收其第一个输入，然后是第二个输入，等等。直接所有数据接受完成。类似地，应用程序发送它的第一个输出，然后发送它的第二个输出，直到它发送它们全部。输入不相互交错，输出不相互交错。 &lt;code&gt;连续编组&lt;/code&gt;规则对某些FastCGI角色限制太多。因为 CGI 程序没有时间上的限制，可以同时使用 stdout和stderr。因此角色协议使用FCGI_STDOUT 和FCGI_STDERR来允许这两个流交错。 所有角色协议都使用FCGI_STDERR流，就像在传统应用程序编程中使用stderr一样：以可理解的方式报告应用程序级错误。使用FCGI_STDERR流始终是可选的。如果应用程序没有要报告的错误，它将不发送FCGI_STDERR记录或一个零长度FCGI_STDERR记录。 当角色协议要求传输FCGI_STDERR以外的流时，即使流是空的，也总是传输至少一个流类型的记录 再次为了可靠的协议和简化的应用程序编程，角色协议被设计成&lt;code&gt;几乎连续的编组（nearly sequential marshalling.）&lt;/code&gt;。在真正的请求-响应协议中，应用程序在发送其第一个输出记录之前接收其所有输入记录。请求-响应协议不允许流水线操作。 请求-响应规则对某些FastCGI角色限制太多;毕竟，在开始写stdout之前，CGI程序不限制读取所有stdin。因此一些角色协议允许这种特定的可能性。首先，应用程序接收除最终流输入之外的所有输入。当应用程序开始接收最终流输入时，它可以开始写入其输出。 当角色协议使用FCGI_PARAMS传输文本值时，例如CGI程序从环境变量中获取的值，值的长度不包括终止空字节，且值本身不包含空字节。需要提供environ（7）格式键值对的应用程序必须在键和值之间插入等号，并在值后附加空字节。 角色协议不支持CGI的非解析头功能。FastCGI应用程序使用 Status 和Location CGI头设置响应状态。&lt;/p&gt;
&lt;h3&gt;6.2 响应器&lt;/h3&gt;
&lt;p&gt;一个响应器角色的FastCGI应用程序与CGI / 1.1程序具有相同的目的：它接收与HTTP请求关联的所有信息并生成HTTP响应。 下面将解释响应器如何模拟CGI/1.1的每个元素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;响应器应用程序通过FCGI_PARAMS从Web服务器接收CGI/1.1环境变量。&lt;/li&gt;
&lt;li&gt;接下来，响应器应用程序通过FCGI_STDIN从Web服务器接收CGI/1.1 stdin数据。在接收流结束指示之前，应用程序从该流接收最多CONTENT_LENGTH个字节。 （仅当HTTP客户端无法提供它们时，应用程序才会收到少于CONTENT_LENGTH个字节，例如因为客户端崩溃了。）&lt;/li&gt;
&lt;li&gt;响应器应用程序通过FCGI_STDOUT将CGI/1.1 stdout数据发送到Web服务器，通过FCGI_STDERR将CGI/1.1 stderr数据发送到Web服务器。应用程序同时发送这些，而不是一个接一个地发送。应用程序必须在开始写入FCGI_STDOUT和FCGI_STDERR之前，完成读取FCGI_PARAMS。但它无需在开始写入这两个流之前，结束读取FCGI_STDIN。&lt;/li&gt;
&lt;li&gt;发送所有stdout和stderr数据后，响应器应用程序发送FCGI_END_REQUEST记录。应用程序将protocolStatus部分设置为FCGI_REQUEST_COMPLETE，将appStatus组件设置状态代码后，CGI程序通过exit系统调用返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;响应者执行更新，例如实现POST方法时，应将FCGI_STDIN上接收的字节数与CONTENT_LENGTH进行比较，如果两个数字不相等则中止更新。&lt;/p&gt;
&lt;h3&gt;6.3 授权器&lt;/h3&gt;
&lt;p&gt;授权器FastCGI应用程序接收与HTTP请求相关的所有信息，并生成授权/未授权的决策。在授权决策的情况下，授权者还可以将键值对与HTTP请求相关联;在做出未经授权的决定时，授权器会向HTTP客户端发送完整的响应。 由于CGI / 1.1定义了一种表示与HTTP请求相关的信息的完美方法，因此授权器使用相同的表示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;授权器应用程序通过FCGI_PARAMS流从Web服务器接收HTTP请求信息，与响应器的格式相同。Web服务器不发送CONTENT_LENGTH，PATH_INFO，PATH_TRANSLATED和SCRIPT_NAME头。&lt;/li&gt;
&lt;li&gt;授权器应用程序以与Responder相同的方式发送stdout和stderr数据。CGI/1.1响应状态指明了请求的 处置方式。如果应用程序发送状态200(OK)，则Web服务器允许访问。根据其配置，Web服务器可以继续进行其他访问检查，包括对其他授权器的请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;授权器应用程序的200响应可能包括名称以Variable-为前缀的标头。这些头将应用程序中的键值对传递给Web服务器。例如，响应头：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Variable-AUTH_METHOD: database lookup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用名称AUTH-METHOD传输值“database lookup”。服务器将这些键值对与HTTP请求相关联，并将它们包含在处理HTTP请求时执行的后续CGI或FastCGI请求中。当应用程序提供200响应时，服务器会忽略名称不带Variable-前缀的响应头，并忽略任何响应内容。 对于除“200”（OK）以外的授权器响应状态值，Web服务器拒绝访问并将响应状态，标头和内容发送回HTTP客户端。&lt;/p&gt;
&lt;h3&gt;6.4 过滤器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;过滤器FastCGI应用程序接收与HTTP请求相关的所有信息，以及来自存储在Web服务器上的文件的额外数据流，并生成数据流的“过滤”版本作为HTTP响应。

过滤器的功能类似于将数据文件作为参数的响应器程序。区别在于使用过滤器，数据文件和过滤器本身都可以使用Web服务器的访问控制机制进行访问控制，将数据文件名称作为参数的响应程序必须对数据文件执行自己的访问控制检查。

过滤器采取的步骤类似于响应者的步骤。服务器首先向Filter提供环境变量，然后是标准输入（通常是POST数据），最后是数据文件输入：

- 与响应器一样，过滤器应用程序通过FCGI_PARAMS从Web服务器接收键值对。过滤器应用程序接收两个专属的变量：FCGI_DATA_LAST_MOD和FCGI_DATA_LENGTH。
- 接下来，过滤器应用程序通过FCGI_STDIN从Web服务器接收CGI/1.1 stdin数据。在接收流结束指示之前，应用程序从该流接收最多CONTENT_LENGTH个字节。（仅当HTTP客户端无法提供它们时，应用程序才会收到少于CONTENT_LENGTH个字节，例如因为客户端崩溃了。）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;- 接下来，过滤器应用程序通过FCGI_DATA从Web服务器接收文件数据。该文件的最后修改时间（表示为1970年1月1日UTC以来的整数秒）为FCGI_DATA_LAST_MOD;应用程序可以查阅此变量并从缓存中进行响应而无需读取文件数据。在接收流结束指示之前，应用程序从该流中读取最多FCGI_DATA_LENGTH个字节。 - 响应器应用程序通过FCGI_STDOUT将CGI/1.1 stdout数据发送到Web服务器，通过FCGI_STDERR将CGI/1.1 stderr数据发送到Web服务器。应用程序同时发送这些，而不是一个接一个地发送。应用程序必须在开始写入FCGI_STDOUT和FCGI_STDERR之前，完成读取FCGI_PARAMS。但它无需在开始写入这两个流之前，结束读取FCGI_DATA。 - 发送所有stdout和stderr数据后，响应器应用程序发送FCGI_END_REQUEST记录。应用程序将protocolStatus部分设置为FCGI_REQUEST_COMPLETE，将appStatus组件设置状态代码后，CGI程序通过exit系统调用返回。 过滤器应将FCGI_STDIN上接收的字节数与CONTENT_LENGTH和FCGI_DATA上的FCGI_DATA_LENGTH进行比较。如果数字不匹配且过滤器是一次查询，过滤器响应应提供数据丢失的指示。如果数字不匹配且过滤器是一次更新，则过滤器应中止更新。&lt;/p&gt;
&lt;h2&gt;7.错误&lt;/h2&gt;
&lt;p&gt;FastCGI应用程序以零状态退出，表示它是故意终止的，例如为了执行原始形式的垃圾收集。FastCGI应用程序以非零状态退出，表示它崩溃了。Web服务器或其他应用程序管理器如何响应以零或非零状态退出的应用程序超出了本规范的范围。 Web服务器可以通过发送SIGTERM来请求FastCGI应用程序退出。如果应用程序忽略SIGTERM，则Web服务器可以使用SIGKILL。 astCGI应用程序使用FCGI_STDERR流和FCGI_END_REQUEST记录的appStatus部分报告应用程序级错误。在许多情况下，将通过FCGI_STDOUT流直接向用户报告错误。 Unix上，应用程序向syslog报告较低级别的错误，包括FastCGI协议错误和FastCGI环境变量中的语法错误。根据错误的严重程度，应用程序可以继续或以非零状态退出。&lt;/p&gt;
&lt;h2&gt;8.类型和常量&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/*
 * Listening socket file number
 */
#define FCGI_LISTENSOCK_FILENO 0

typedef struct {
    unsigned char version;
    unsigned char type;
    unsigned char requestIdB1;
    unsigned char requestIdB0;
    unsigned char contentLengthB1;
    unsigned char contentLengthB0;
    unsigned char paddingLength;
    unsigned char reserved;
} FCGI_Header;

/*
 * Number of bytes in a FCGI_Header.  Future versions of the protocol
 * will not reduce this number.
 */
#define FCGI_HEADER_LEN  8

/*
 * Value for version component of FCGI_Header
 */
#define FCGI_VERSION_1           1

/*
 * Values for type component of FCGI_Header
 */
#define FCGI_BEGIN_REQUEST       1
#define FCGI_ABORT_REQUEST       2
#define FCGI_END_REQUEST         3
#define FCGI_PARAMS              4
#define FCGI_STDIN               5
#define FCGI_STDOUT              6
#define FCGI_STDERR              7
#define FCGI_DATA                8
#define FCGI_GET_VALUES          9
#define FCGI_GET_VALUES_RESULT  10
#define FCGI_UNKNOWN_TYPE       11
#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)

/*
 * Value for requestId component of FCGI_Header
 */
#define FCGI_NULL_REQUEST_ID     0

typedef struct {
    unsigned char roleB1;
    unsigned char roleB0;
    unsigned char flags;
    unsigned char reserved[5];
} FCGI_BeginRequestBody;

typedef struct {
    FCGI_Header header;
    FCGI_BeginRequestBody body;
} FCGI_BeginRequestRecord;

/*
 * Mask for flags component of FCGI_BeginRequestBody
 */
#define FCGI_KEEP_CONN  1

/*
 * Values for role component of FCGI_BeginRequestBody
 */
#define FCGI_RESPONDER  1
#define FCGI_AUTHORIZER 2
#define FCGI_FILTER     3

typedef struct {
    unsigned char appStatusB3;
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
    unsigned char protocolStatus;
    unsigned char reserved[3];
} FCGI_EndRequestBody;

typedef struct {
    FCGI_Header header;
    FCGI_EndRequestBody body;
} FCGI_EndRequestRecord;

/*
 * Values for protocolStatus component of FCGI_EndRequestBody
 */
#define FCGI_REQUEST_COMPLETE 0
#define FCGI_CANT_MPX_CONN    1
#define FCGI_OVERLOADED       2
#define FCGI_UNKNOWN_ROLE     3

/*
 * Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT records
 */
#define FCGI_MAX_CONNS  &quot;FCGI_MAX_CONNS&quot;
#define FCGI_MAX_REQS   &quot;FCGI_MAX_REQS&quot;
#define FCGI_MPXS_CONNS &quot;FCGI_MPXS_CONNS&quot;

typedef struct {
    unsigned char type;    
    unsigned char reserved[7];
} FCGI_UnknownTypeBody;

typedef struct {
    FCGI_Header header;
    FCGI_UnknownTypeBody body;
} FCGI_UnknownTypeRecord;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;9.参考文献&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.w3.org/CGI/&quot; rel=&quot;noopener&quot;&gt;The WWW Common Gateway Interface at W3C&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;A.表：记录类型的属性&lt;/h2&gt;
&lt;p&gt;下表列出了所有记录类型，并指出了每种记录的属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WS-&amp;gt;App: 此类记录只能由Web服务器发送到应用程序。其他类型的记录只能由应用程序发送到Web服务器。&lt;/li&gt;
&lt;li&gt;management: 此类型的记录包含不是特定于Web服务器请求的信息，并使用的的请求ID。其他类型的记录包含特定于请求的信息，不能使用空的请求ID。&lt;/li&gt;
&lt;li&gt;stream: 此类型的记录形成一个流，由具有空contentData的记录终止。其他类型的记录是离散的;每个都带有一个有意义的数据单元。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;                               WS-&amp;gt;App   management  stream

        FCGI_GET_VALUES           x          x
        FCGI_GET_VALUES_RESULT               x
        FCGI_UNKNOWN_TYPE                    x

        FCGI_BEGIN_REQUEST        x
        FCGI_ABORT_REQUEST        x
        FCGI_END_REQUEST
        FCGI_PARAMS               x                    x
        FCGI_STDIN                x                    x
        FCGI_DATA                 x                    x
        FCGI_STDOUT                                    x 
        FCGI_STDERR                                    x     
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;B. 典型的协议消息流&lt;/h2&gt;
&lt;p&gt;示例的其他符号约定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;流记录的contentData（FCGI_PARAMS，FCGI_STDIN，FCGI_STDOUT和FCGI_STDERR）表示为字符串。以“...”结尾的字符串太长而无法显示，因此仅显示前缀。&lt;/li&gt;
&lt;li&gt;发送到Web服务器的消息相对于从Web服务器接收的消息缩进。&lt;/li&gt;
&lt;li&gt;消息按应用程序所经历的时间顺序显示。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;一个没有stdin数据的简单请求，以及一个成功的响应：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;(&lt;strong&gt;注:&lt;/strong&gt;\013\016这里是8进制编码。这里所有的记录都省略了Version和PaddingData,因此类似于FCGI_BEGIN_REQUEST代表Type，1代表RequestId)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, &quot;\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... &quot;}
{FCGI_PARAMS,          1, &quot;&quot;}
{FCGI_STDIN,           1, &quot;&quot;}

    {FCGI_STDOUT,      1, &quot;Content-type: text/html\r\n\r\n&amp;lt;html&amp;gt;\n&amp;lt;head&amp;gt; ... &quot;}
    {FCGI_STDOUT,      1, &quot;&quot;}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.与示例1类似，但这次使用stdin上的数据。 Web服务器选择使用比以前更多的FCGI_PARAMS记录发送参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, &quot;\013\002SERVER_PORT80\013\016SER&quot;}
{FCGI_PARAMS,          1, &quot;VER_ADDR199.170.183.42 ... &quot;}
{FCGI_PARAMS,          1, &quot;&quot;}
{FCGI_STDIN,           1, &quot;quantity=100&amp;amp;item=3047936&quot;}
{FCGI_STDIN,           1, &quot;&quot;}

    {FCGI_STDOUT,      1, &quot;Content-type: text/html\r\n\r\n&amp;lt;html&amp;gt;\n&amp;lt;head&amp;gt; ... &quot;}
    {FCGI_STDOUT,      1, &quot;&quot;}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.与示例1类似，但这次应用程序检测到错误。应用程序将消息记录到stderr，将页面返回给客户端，并将非零退出状态返回给Web服务器。应用程序选择使用更多FCGI_STDOUT记录发送页面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, &quot;\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... &quot;}
{FCGI_PARAMS,          1, &quot;&quot;}
{FCGI_STDIN,           1, &quot;&quot;}

    {FCGI_STDOUT,      1, &quot;Content-type: text/html\r\n\r\n&amp;lt;ht&quot;}
    {FCGI_STDERR,      1, &quot;config error: missing SI_UID\n&quot;}
    {FCGI_STDOUT,      1, &quot;ml&amp;gt;\n&amp;lt;head&amp;gt; ... &quot;}
    {FCGI_STDOUT,      1, &quot;&quot;}
    {FCGI_STDERR,      1, &quot;&quot;}
    {FCGI_END_REQUEST, 1, {938, FCGI_REQUEST_COMPLETE}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4.示例1的两个实例，复用到单个连接上。第一个请求比第二个请求更难，因此应用程序不按顺序完成请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, FCGI_KEEP_CONN}}
{FCGI_PARAMS,          1, &quot;\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... &quot;}
{FCGI_PARAMS,          1, &quot;&quot;}
{FCGI_BEGIN_REQUEST,   2, {FCGI_RESPONDER, FCGI_KEEP_CONN}}
{FCGI_PARAMS,          2, &quot;\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... &quot;}
{FCGI_STDIN,           1, &quot;&quot;}

    {FCGI_STDOUT,      1, &quot;Content-type: text/html\r\n\r\n&quot;}

{FCGI_PARAMS,          2, &quot;&quot;}
{FCGI_STDIN,           2, &quot;&quot;}

    {FCGI_STDOUT,      2, &quot;Content-type: text/html\r\n\r\n&amp;lt;html&amp;gt;\n&amp;lt;head&amp;gt; ... &quot;}
    {FCGI_STDOUT,      2, &quot;&quot;}
    {FCGI_END_REQUEST, 2, {0, FCGI_REQUEST_COMPLETE}}
    {FCGI_STDOUT,      1, &quot;&amp;lt;html&amp;gt;\n&amp;lt;head&amp;gt; ... &quot;}
    {FCGI_STDOUT,      1, &quot;&quot;}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>FastCGI协议</category><author>joyme123</author></item><item><title>redis使用总结</title><link>https://www.myway5.com/blog/redis/</link><guid isPermaLink="true">https://www.myway5.com/blog/redis/</guid><description>项目中存在多种redis的使用场景 1.1 场景一：缓存(key,value)</description><pubDate>Tue, 17 Jul 2018 08:37:32 GMT</pubDate><content:encoded>&lt;h2&gt;1.redis的使用场景&lt;/h2&gt;
&lt;p&gt;项目中存在多种redis的使用场景&lt;/p&gt;
&lt;h3&gt;1.1 场景一：缓存(key,value)&lt;/h3&gt;
&lt;p&gt;缓存是一个非常常见的场景，在项目中，可以将mysql中的一部分数据查询的结果缓存到redis中，以此来获取更快的查询速度。 以用户信息缓存为例，我的做法如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.设定用户信息缓存的规则，比如userinfo128代表用户id为128的用户信息缓存。&lt;/li&gt;
&lt;li&gt;2.在query语句执行时，先检查userinfo128是否存在，如果存在，直接取出结果返回，否则执行sql语句进行查询，并将查询结果缓存。&lt;/li&gt;
&lt;li&gt;3.在update和delete语句执行时，删除userinfo128的缓存，这样下一次query时就会自动更新缓存了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 timeline(sorted set)&lt;/h3&gt;
&lt;p&gt;timeline非常经典的场景就是微博，朋友圈这种，每个用户都能在自己的timeline上获取到按时间排序的其他用户发布的动态。这里有以前总结过的一篇文章：&lt;a href=&quot;https://www.myway5.com/index.php/2017/06/29/timeline-design/&quot; rel=&quot;noopener&quot;&gt;朋友圈式的TIMELINE设计方案&lt;/a&gt;。 其实这个 timeline 我一开始的实现是用 list 去做的，但是用list会存在一个问题：因为推送动态可能同时发生，导致不是严格的按照时间排序。&lt;/p&gt;
&lt;h3&gt;1.3 推送用户集合(set)&lt;/h3&gt;
&lt;p&gt;这个功能其实就相当于维持一个用户的粉丝列表。这个列表是一个会经常发生变化的集合，并且在项目中是根据用户的关系链计算出来的，单次的查询会消耗很多的时间，因此做成一个集合，在需要推送动态之类的内容时，直接从redis的集合中查询，会节约很多的时间。&lt;/p&gt;
&lt;h3&gt;1.4 任务队列(list)&lt;/h3&gt;
&lt;p&gt;整个项目中很多的操作都是异步的，比如发短信，发邮件，推送用户动态等等，使用redis作为任务队列是很简单的，使用它的list结构，然后使用lpush/rpop对，一边push进任务，另一边有一个单独的后台进程pop出任务进行执行。 当然lpush/rpop并不是很好的一个选择，更好的选择是lpush/brpop,使用阻塞版本的pop指令，可以减少很多不必要的轮询。 在使用这样的任务队列时，还需要考虑到一个问题，如果在取出一个任务时进程崩溃，那么这个任务就彻底的丢失了。因此还可以使用 rpoplpush 或者阻塞版本的 brpoplpush ，取出一个任务的同时备份到另一个队列。如果执行成功的话就再lrem掉这个备份即可。关于队列的更详细的使用在第7大点有更详细的说明。 当然， Redis其实并不推荐作为任务队列的实现，如果需要的话，可以尝试使用Redis作者的另一个项目:disque，或者是kafka。  &lt;/p&gt;
&lt;h3&gt;1.5 计数器(hash)&lt;/h3&gt;
&lt;p&gt;计数器我认为也算是 redis 一个常用的功能了，我认为原因有以下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.很多场景下的计数功能都是一个非常高频的操作，使用 redis 会拥有极高的性能。&lt;/li&gt;
&lt;li&gt;2.redis支持原子性的自增(incre)操作，不用担心CAS(check and set)的问题。&lt;/li&gt;
&lt;li&gt;3.传统数据库，如mysql，如果是MyISAM，单次的更新会带来表锁，如果是InnoDB,则带来行锁，影响并发度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;计数器的使用很简单，直接对某个key做 incr 操作，或者对某个 hash 的 key 做 hincrby 操作即可。  &lt;/p&gt;
&lt;h2&gt;2.php在使用redis时，多个数据库切换的困扰&lt;/h2&gt;
&lt;p&gt;项目中使用的是phpredis这个扩展，在使用pconnect保持redis长连接时，所有对redis的操作会共用同一个redis连接。这就导致：多个进程同时使用一个redis连接，并且多个进程使用的数据库不同时导致错误。比如下方的操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 进程1做以下操作
$redis-&amp;gt;select(0);
$redis-&amp;gt;set(&quot;key1&quot;, &quot;val1&quot;);

// 进程2做以下操作
$redis-&amp;gt;select(1);
$redis-set(&quot;key2&quot;, &quot;val2&quot;);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是在redis的server端，所做的操作可能如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select(0)
select(1)
set(&quot;key2&quot;, &quot;val2&quot;)
set(&quot;key1&quot;, &quot;val1&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就导致key1的存储错误 所以我必须在所有这样的操作中，使用MULTI/EXEC对去解决这个问题。以上代码变成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 进程1做以下操作
$redis-&amp;gt;multi();
$redis-&amp;gt;select(0);
$redis-&amp;gt;set(&quot;key1&quot;, &quot;val1&quot;);
$result-&amp;gt;exec();

// 进程2做以下操作
$redis-&amp;gt;multi();
$redis-&amp;gt;select(1);
$redis-set(&quot;key2&quot;, &quot;val2&quot;);
$result-&amp;gt;exec();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事实上，使用 redis 时，同时使用多个数据库并不推荐。因为在 redis 集群中是不支持 select 命令的。&lt;/p&gt;
&lt;h2&gt;3.redis多个数据库之间的切换，对性能有影响吗？&lt;/h2&gt;
&lt;p&gt;在探讨这个问题之前，摘录官网上对 select 命令的说明：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Since the currently selected database is a property of the connection, clients should track the currently selected database and re-select it on reconnection. While there is no command in order to query the selected database in the current connection, the CLIENT LIST output shows, for each client, the currently selected database.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大致意思可以翻译为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;因为当前选中的数据库是连接的一个属性，每个客户端连接都跟踪记录了当前选中的数据库，在重新连接时会重新选择数据库。虽然没有命令是为了查询当前连接选中的数据库，但是 CLIENT LIST 的输出会显示，每个客户端当前选中的是哪个数据库。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此 select 操作只是修改了当前连接的属性。&lt;/p&gt;
&lt;h2&gt;4.redis 有 16 个数据库，目的是什么，最佳的使用方式是什么？为什么 redis 集群不支持 select？&lt;/h2&gt;
&lt;p&gt;同样的摘录一段官网的介绍&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Redis different selectable databases are a form of namespacing: all the databases are anyway persisted together in the same RDB / AOF file. However different databases can have keys having the same name, and there are commands available like FLUSHDB, SWAPDB or RANDOMKEY that work on specific databases.   In practical terms, Redis databases should mainly used in order to, if needed, separate different keys belonging to the same application, and not in order to use a single Redis instance for multiple unrelated applications. When using Redis Cluster, the SELECT command cannot be used, since Redis Cluster only supports database zero. In the case of Redis Cluster, having multiple databases would be useless, and a worthless source of complexity, because anyway commands operating atomically on a single database would not be possible with the Redis Cluster design and goals.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大意如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Redis 多个不同的可选择的数据库是命名空间的一个表现形式：所有的数据库都会在同一个 RDB/AOF 文件中进行持久化。当然不同的数据库可以拥有同样的名字的键，同样的也有一些类似 FLUSHDB, SWAPDB 或 RANDOMKEY 这样的命名专门在数据库上工作的。 实际上，Redis 数据库应该主要用来分离属于一个应用的不同的键，而不是为了使用一个单独的 Redis 实例服务于多个不相关的应用。 当使用 Redis 集群时，SELECT 命名就不能使用了，因为 Redis 集群仅仅支持数据库0。在 Redis 集群的案例中，拥有多个数据库是无用的，是一种毫无价值的复杂性的来源，因为 Redis 集群的设计和目标是不可能支持 SELECT 命令的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里解释了为什么 Redis 被设计为有多个数据库，是为了分离同一个应用中不同的键而设计的，但是不能在多个不相关的应用中使用同一个 Redis 实例。并且还要注意在 Redis 集群中无法使用 SELECT ，因此在项目中还是不用为好。&lt;/p&gt;
&lt;h2&gt;5.redis是单进程的，如何理解？&lt;/h2&gt;
&lt;p&gt;我在第一次看到这句话时，是很不理解的。对于这种应用，不可能只有一个进程在工作啊。但是在深入了解之后，明白了这里的单进程指的是：处理 Redis 命令是单进程的。 也就是说，同一时间，在并发和并行的层面上来说，都只有一个 Redis 命令被执行。这样设计的理由我的理解有以下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis是内存数据库，所有的操作耗时都视CPU的运行速度而定，IO不可能是瓶颈，并行/并发处理带来的意义不大。&lt;/li&gt;
&lt;li&gt;并行/并发会增加应用的复杂度&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6.redis中使用队列的问题&lt;/h2&gt;
&lt;p&gt;在1.4小节中我提到了使用Redis作为任务队列的场景。在使用时遇到了程序运行一段时间之后，无法使用brpop获取数据的问题，并且这个程序的连接依然是存活的。 于是我用 CLIENT LIST 查看当前的连接客户端。发现服务器中有大量连接，但是很多连接的 idle 特别长，明显是很久以前的连接，这些连接我可以肯定是已经断开的。经过检查之后，发现 Redis.conf 中的 tcp-keepalive 项我设置为0了，设置为0就不会检查连接是否存活，从而导致连接一直存在。以前将 tcp-keepalive 设置为60， 那这跟 brpop 无法从 Redis 中获取数据有什么关系呢？&lt;strong&gt;以下是个人的猜想时间。&lt;/strong&gt; 这要从Redis的block模型说起。Redis的网络连接是epoll模型的，所以是一个异步的io，肯定不会block一个连接。那么Redis server为了实现这样的block操作，会维持一个内部的哈希表，这个哈希表保存了哪个key上阻塞了哪些客户端。如下图所示： &lt;img src=&quot;/uploads/wp/2018/07/%E6%B7%B1%E5%BA%A6%E6%88%AA%E5%9B%BE_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20180717160632.png&quot; alt=&quot;此处输入图片的描述&quot; /&gt; 如果此时list key1中被push进了一个值，key1就被置为ready状态，然后从链表头部取出client2，将值传给它。可能在我自己的测试中，有大量的已经断开连接客户端阻塞在key1上，但是因为tcp-keepalive为0，没有被及时清除。导致以上的结果。（目前的水平只能这么解释了，虽然还有很多地方说不通）&lt;/p&gt;
</content:encoded><category>php</category><category>redis</category><category>redis</category><category>php</category><category>任务队列</category><author>joyme123</author></item><item><title>ABNF格式说明</title><link>https://www.myway5.com/blog/abnf/</link><guid isPermaLink="true">https://www.myway5.com/blog/abnf/</guid><description>ABNF全称是Augmented Backus-Naur Form，广泛用于很多的互联网文档说明中。主要作用就是以简洁的字符串来描述某些规范。使用了ABNF的标准说明有:电子邮件的标准说明\[RFC733\]和之后的\[RFC822\]，HTTP1.</description><pubDate>Thu, 17 May 2018 06:12:46 GMT</pubDate><content:encoded>&lt;h2&gt;一、简介&lt;/h2&gt;
&lt;p&gt;ABNF全称是Augmented Backus-Naur Form，广泛用于很多的互联网文档说明中。主要作用就是以简洁的字符串来描述某些规范。使用了ABNF的标准说明有:电子邮件的标准说明[RFC733]和之后的[RFC822]，HTTP1.1协议的[RFC7230]。因此，要想阅读这些文档，必须了解ABNF的格式。ABNF在RFC5234中进行了详细的说明。&lt;/p&gt;
&lt;h2&gt;二、规则定义&lt;/h2&gt;
&lt;h3&gt;2.1 规则命名&lt;/h3&gt;
&lt;p&gt;ABNF中规则的命名是大小写不敏感的，由字母开头，后面跟上字母、数字或连字符&lt;/p&gt;
&lt;h3&gt;2.2 规则格式&lt;/h3&gt;
&lt;p&gt;一个规则是如下格式定义的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name = elements crlf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;name指的是规则名，elements是一个或多个规则名，或者是终端字符，crlf也就是我们常说的\r\n&lt;/p&gt;
&lt;h3&gt;2.3 终端值&lt;/h3&gt;
&lt;p&gt;一个规则被解释成一个字符串。每个字符都是一个非负的数字（比如ASCII码中a对应十进制的97）。终端值就是这些数字。目前定义了以下几种进制:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;b     = binary          ;二进制
d     = decimal         ;十进制
x     = hexadecimal     ;十六进制
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CR = %d13
CR = %x0D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&quot;.&quot;号来分割字符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CRLF = %d13.10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 额外的编码&lt;/h3&gt;
&lt;p&gt;根据编码不同，所显示的值可能也不同。比如7-bit的US-ASCII和16-bit的unicode编码，结果是截然不同的。目前7-bit的US-ASCII编码是最常用的。&lt;/p&gt;
&lt;h2&gt;三、 运算符&lt;/h2&gt;
&lt;h3&gt;3.1 连接: &lt;code&gt;Rule1 Rule2&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;连接的意思就是值一个规则可能由其他规则连接而成。比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;foo = %x61 ; a
bar = %x62 ; b
mumble = foo bar foo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此规则mumnle = aba&lt;/p&gt;
&lt;h3&gt;3.2 选择: &lt;code&gt;Rule1 / Rule2&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;选择就是多选一的意思。比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rule = foo / bar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么rule是foo或者bar都接受的&lt;/p&gt;
&lt;h3&gt;3.3 扩展的选择: &lt;code&gt;Rule1 =/ Rule2&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ruleset = rule1 / rule2
ruleset =/ rule3
ruleset =/ rule4 / rule5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么ruleset最终为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ruleset = rule1 / rule2 / rule3 / rule4 / rule5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4 范围选择: &lt;code&gt;%c##-##&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;DIGIT = %x30-39
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等价于&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DIGIT = &quot;0&quot; / &quot;1&quot; / &quot;2&quot; / &quot;3&quot; / &quot;4&quot; / &quot;5&quot; / &quot;6&quot; / &quot;7&quot; / &quot;8&quot; / &quot;9&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.5 序列组: &lt;code&gt;(Rule1 Rule2)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;序列组主要是为了阅读上的方便&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;elem (foo / bar) blat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等价于&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(elem foo blat) or (elem bar blat)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;elem foo / bar blat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等价于&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(elem foo) or (bar blat)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.6 变量重复: &lt;code&gt;*Rule&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;完整的格式为：&lt;code&gt;&amp;lt;a&amp;gt;*&amp;lt;b&amp;gt;element&lt;/code&gt; &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;和&lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt;是可选的数字值，代表最少a个，最多b个 因此：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*&amp;lt;element&amp;gt; 0到任意多个
1*&amp;lt;element&amp;gt; 至少1个
3*3&amp;lt;element&amp;gt; 只能是3个
1*2&amp;lt;element&amp;gt; 1到2个
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.7 指定的重复: &lt;code&gt;nRule&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;n&amp;lt;element&amp;gt;等价于n*n&amp;lt;element&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.8 可选的序列: &lt;code&gt;[Rule]&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;[Rule]代表这个规则可有可无。因此[foo bar]等价于*1[foo bar]&lt;/p&gt;
&lt;h3&gt;3.9 注释: &lt;code&gt;;Comment&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;;&lt;/code&gt;来表示注释&lt;/p&gt;
&lt;h3&gt;3.10 运算符优先级&lt;/h3&gt;
&lt;p&gt;运算符优先级从上往下排序如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;规则名, 单值, 终端值
注释
范围取值
重复
组, 可选
连接
选择
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、使用ABNF定义ABNF&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;rulelist = 1*( rule / (*c-wsp c-nl) )

rule = rulename defined-as elements c-nl
; continues if next line starts
; with white space

rulename = ALPHA *(ALPHA / DIGIT / &quot;-&quot;)

defined-as = *c-wsp (&quot;=&quot; / &quot;=/&quot;) *c-wsp
; basic rules definition and
; incremental alternatives

elements = alternation *c-wsp

c-wsp = WSP / (c-nl WSP)

c-nl = comment / CRLF
; comment or newline

comment = &quot;;&quot; *(WSP / VCHAR) CRLF

alternation = concatenation
*(*c-wsp &quot;/&quot; *c-wsp concatenation)

concatenation = repetition *(1*c-wsp repetition)

repetition = [repeat] element

repeat = 1*DIGIT / (*DIGIT &quot;*&quot; *DIGIT)

element = rulename / group / option /
char-val / num-val / prose-val

group = &quot;(&quot; *c-wsp alternation *c-wsp &quot;)&quot;

option = &quot;[&quot; *c-wsp alternation *c-wsp &quot;]&quot;

char-val = DQUOTE *(%x20-21 / %x23-7E) DQUOTE
; quoted string of SP and VCHAR
; without DQUOTE

num-val = &quot;%&quot; (bin-val / dec-val / hex-val)

bin-val = &quot;b&quot; 1*BIT
[ 1*(&quot;.&quot; 1*BIT) / (&quot;-&quot; 1*BIT) ]
; series of concatenated bit values
; or single ONEOF range

dec-val = &quot;d&quot; 1*DIGIT
[ 1*(&quot;.&quot; 1*DIGIT) / (&quot;-&quot; 1*DIGIT) ]

hex-val = &quot;x&quot; 1*HEXDIG
[ 1*(&quot;.&quot; 1*HEXDIG) / (&quot;-&quot; 1*HEXDIG) ]

prose-val = &quot;&amp;lt;&quot; *(%x20-3D / %x3F-7E) &quot;&amp;gt;&quot;
; bracketed string of SP and VCHAR
; without angles
; prose description, to be used as
; last resort
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;附录：核心规则&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;ALPHA = %x41-5A / %x61-7A ; A-Z / a-z

BIT = &quot;0&quot; / &quot;1&quot;

CHAR = %x01-7F
; any 7-bit US-ASCII character,
; excluding NUL

CR = %x0D
; carriage return

CRLF = CR LF
; Internet standard newline

CTL = %x00-1F / %x7F
; controls

DIGIT = %x30-39
; 0-9

DQUOTE = %x22
; &quot; (Double Quote)

HEXDIG = DIGIT / &quot;A&quot; / &quot;B&quot; / &quot;C&quot; / &quot;D&quot; / &quot;E&quot; / &quot;F&quot;

HTAB = %x09
; horizontal tab

LF = %x0A
; linefeed

LWSP = *(WSP / CRLF WSP)
; Use of this linear-white-space rule
; permits lines containing only white
; space that are no longer legal in
; mail headers and have caused
; interoperability problems in other
; contexts.
; Do not use when defining mail
; headers and use with caution in
; other contexts.

OCTET = %x00-FF
; 8 bits of data

SP = %x20

VCHAR = %x21-7E
; visible (printing) characters

WSP = SP / HTAB
; white space
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RFC5234地址：&lt;a href=&quot;https://tools.ietf.org/html/rfc5234&quot; rel=&quot;noopener&quot;&gt;https://tools.ietf.org/html/rfc5234&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>工作</category><category>计算机</category><category>ABNF</category><category>RFC5234</category><author>joyme123</author></item><item><title>Canvas性能优化小结</title><link>https://www.myway5.com/blog/canvas-perf/</link><guid isPermaLink="true">https://www.myway5.com/blog/canvas-perf/</guid><description>H5中引入了对canvas的支持，使得网页的表达能力更加丰富了。程序员可以通过canvas来绘制复杂的图形，甚至是游戏。因为工作中的需求，需要使用canvas在网页上绘制家谱。具体的算法可以看之前的一篇总结：树的可视化以及家谱绘制的算法 。</description><pubDate>Thu, 26 Apr 2018 05:42:48 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;H5中引入了对canvas的支持，使得网页的表达能力更加丰富了。程序员可以通过canvas来绘制复杂的图形，甚至是游戏。因为工作中的需求，需要使用canvas在网页上绘制家谱。具体的算法可以看之前的一篇总结：&lt;a href=&quot;https://www.myway5.com/index.php/2017/07/20/%E6%A0%91%E7%9A%84%E5%8F%AF%E8%A7%86%E5%8C%96%E4%BB%A5%E5%8F%8A%E5%AE%B6%E8%B0%B1%E7%BB%98%E5%88%B6%E7%9A%84%E7%AE%97%E6%B3%95/&quot; rel=&quot;noopener&quot;&gt;树的可视化以及家谱绘制的算法&lt;/a&gt; 。当家谱中的数据量变大之后，整个绘制过程会变的很卡（800左右人物的家谱1000次绘制居然需要25s左右）。但是在做完优化后，1000次绘制只需要0.15s左右。 &lt;img src=&quot;/uploads/wp/2018/04/%E6%B7%B1%E5%BA%A6%E6%88%AA%E5%9B%BE_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20180426122444.png&quot; alt=&quot;家谱示例&quot; /&gt; 目前的家谱绘制共有两部分。一个是家谱的主体，用户可以通过鼠标、滚轮去任意的浏览；一个是左上角的小地图功能，帮助用户了解到当前浏览的位置，小地图中的有色方块就是用户整个屏幕显示的内容。&lt;/p&gt;
&lt;h2&gt;优化措施一：缓存&lt;/h2&gt;
&lt;p&gt;缓存是在绝大多数系统中经常用到的提升性能的方法，典型的以空间换时间。在canvas中当然也可以使用缓存。在家谱的每一次绘制中，都要遍历所有的人物，然后计算他们的位置大小，然后绘制到界面上，然后还要遍历所有的路径再次进行绘制。而使用缓存的目的就是避免这一部分的计算。因此，在任何复杂计算后的绘制，都能使用缓存来提升性能。 在canvas中有这样一个方法，&lt;code&gt;CanvasRenderingContext2D.drawImage(image, dx, dy)&lt;/code&gt;，&lt;code&gt;image&lt;/code&gt;参数代表要绘制的图片源，不仅仅可以是一个image对象，还可以是一个canvas对象。所以如果我们将一个复杂的图形作为canvas对象缓存起来，然后直接使用&lt;code&gt;drawImage&lt;/code&gt;方法去绘制到画面上，就能减少很多的重复计算，来提升性能。 例如以下的伪代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function paintPerson() {
    // paint 
}

function paintFamilyTree() {
    for(var i = 0; i &amp;lt; 10000; i++) {
        paintPerson()
    }
}

// 用户移动鼠标就需要重绘族谱
dom.addEventListener(&quot;mousemove&quot;, function() {
    paintFamilyTree()
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户每一次移动鼠标，都需要10000次的循环去画。所以我们使用下面的方法，使得只需要计算一次&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function paintPerson() {
    // paint 
}

function paintFamilyTree() {
    for(var i = 0; i &amp;lt; 10000; i++) {
        paintPerson()
    }
}

var canvasObj = cache(paintFamilyTree) // 缓存这次绘制的结果。

dom.addEventListener(&quot;mousemove&quot;, function() {
    ctx.drawImage(canvasObj,0,0)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么如何去缓存这个 canvas 对象呢？可以使用&lt;code&gt;document.createElement(&apos;canvas&apos;)&lt;/code&gt;来创建一个不在页面上显示的 canvas 对象，一般也称它为&lt;code&gt;离屏画布&lt;/code&gt;，这里用变量 offScreenCanvas 来称呼。然后将绘制的图形画在这个 canvas 上，之后调用 &lt;code&gt;ctx.drawImage(offScreenCanvas,0,0)&lt;/code&gt; 即可。 在创建这个离屏画布的时候，我们也要设置合适的宽高，这样也有助于提升性能。 可以参考这个例子来感受一下性能差距： &lt;a href=&quot;https://joyme123.github.io/study-code/offscreen_canvas/index.html&quot; rel=&quot;noopener&quot;&gt;离屏画布缓存来提升页面性能&lt;/a&gt; 例子代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
        &amp;lt;title&amp;gt;离屏canvas实例&amp;lt;/title&amp;gt;
        &amp;lt;style&amp;gt;
            .main-wrapper {
                width: 800px;
                margin: 10px auto;
            }
        &amp;lt;/style&amp;gt;
    &amp;lt;/head&amp;gt;

    &amp;lt;body onload=&quot;init()&quot;&amp;gt;
        &amp;lt;div class=&quot;main-wrapper&quot;&amp;gt;
            &amp;lt;canvas width=&quot;800&quot; height=&quot;600&quot; style=&quot;border: 1px solid #ccc&quot; id=&quot;canvas&quot;&amp;gt;
                你的浏览器不支持canvas
            &amp;lt;/canvas&amp;gt;
            &amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;
            &amp;lt;div id=&quot;result&quot;&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;br&amp;gt;
            渲染次数:&amp;lt;input type=&quot;number&quot; id=&quot;times&quot; value=&quot;1000&quot;&amp;gt;,共耗时(ms)&amp;lt;span id=&quot;time_used&quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;br&amp;gt;
            &amp;lt;button onclick=&quot;doTest(false,false)&quot;&amp;gt;不使用离屏canvas&amp;lt;/button&amp;gt;
            &amp;lt;button onclick=&quot;doTest(true,false)&quot;&amp;gt;使用离屏canvas&amp;lt;/button&amp;gt;
            &amp;lt;button onclick=&quot;doTest(true,true)&quot;&amp;gt;使用离屏canvas并设置正确的高度&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;script&amp;gt;
            /**
             * 离屏缓存，num为缓存canvas的数量
             */
            var OffScreenCache = function (num) {
                this.canvases = [];
                for (i = 0; i &amp;lt; num; i++) {
                    this.canvases.push(document.createElement(&quot;canvas&quot;));
                }
            }
            OffScreenCache.prototype = {
                pop: function() {
                    return this.canvases.pop();
                },
                push: function(canvas) {
                    this.canvases.push(canvas);
                },
                destroy: function() {
                    this.canvases = null;
                }
            }
            var Ball = function (color) {
                this.radius = 50;
                this.lineWidth = 4;
                this.cache = null;
                this.color = color;
            }
            Ball.prototype = {
                paint: function(ctx, x, y) {
                    ctx.save();
                    ctx.lineWidth = this.lineWidth;
                    ctx.strokeStyle = this.color;
                    for (i = 1; i &amp;lt; this.radius; i+= this.lineWidth) {
                        ctx.beginPath();
                        ctx.arc(x, y, i, 0, Math.PI*2,true); // 绘制
                        ctx.stroke();
                    }
                    ctx.restore();
                },
                useCache: function (cacheCanvas, autoSet) {
                    if (autoSet) {
                        cacheCanvas.width = this.radius * 2;
                        cacheCanvas.height = this.radius * 2;
                    }
                    cacheCtx = cacheCanvas.getContext(&apos;2d&apos;)
                    this.paint(cacheCtx, cacheCanvas.width / 2, cacheCanvas.height / 2)
                    this.cache = cacheCanvas
                }
            }
            /******************** 下面是执行代码 **************************/
            var g_canvas, g_ctx;
            var g_width = 800;
            var g_height = 600;
            function init() {
                g_canvas = document.getElementById(&quot;canvas&quot;);
                g_ctx = g_canvas.getContext(&apos;2d&apos;);
            }
            function getRandomPos() {
                var x = Math.random() * g_width
                var y = Math.random() * g_height
                return {x:x, y:y}
            }
            function showResult(ballCount) {
                var dom = document.getElementById(&quot;result&quot;);
                dom.innerHTML = &quot;&quot;;
                var str = &quot;&quot;;
                for (var key of Object.keys(ballCount)) {
                    str += &quot;&amp;lt;span&amp;gt;&quot; + key + &quot;ball：&quot; + ballCount[key] + &quot;&amp;lt;/span&amp;gt;    &quot;
                }
                dom.innerHTML = str;
            }
            function doTest(useCache, autoSet) {
                g_ctx.clearRect(0, 0, g_width, g_height)
                var startTime = Date.now();
                var times = document.getElementById(&quot;times&quot;).value
                var colorArray = [&apos;red&apos;, &apos;blue&apos;, &apos;green&apos;, &apos;black&apos;, &apos;pink&apos;]   // 共绘制5种颜色
                var ballCount = {};
                colorArray.forEach(function(color) {
                    ballCount[color] = 0;
                })
                if (useCache) {
                    var colorBall = [];
                    var cacheCanvases = new OffScreenCache(colorArray.length);
                    for (var i = 0; i &amp;lt; colorArray.length; i++) {
                        var ball = new Ball(colorArray[i]);
                        var cacheCanvas = cacheCanvases.pop();
                        ball.useCache(cacheCanvas, autoSet)
                        colorBall.push(ball)
                    }
                    for (var i = 0; i &amp;lt; times; i++) {
                        ball = colorBall[i % 5]
                        ballCount[ball.color]++;
                        var pos = getRandomPos()
                        g_ctx.drawImage(ball.cache, pos.x, pos.y)       // 开始画
                    }
                } else {
                    for (var i = 0; i &amp;lt; times; i++) {
                        var color = colorArray[i % 5];
                        var ball = new Ball(color)
                        var pos = getRandomPos()
                        ball.paint(g_ctx, pos.x, pos.y)               // 开始画
                        ballCount[color]++;
                    }
                }
                var endTime = Date.now();
                document.getElementById(&quot;time_used&quot;).innerText = endTime - startTime;
                showResult(ballCount)
            }
        &amp;lt;/script&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;优化措施二：分层&lt;/h2&gt;
&lt;p&gt;在上述的族谱图片中，左上角的小地图其实是有两个canvas重叠而成。底层的canvas绘制族谱的缩略图，上层的canvas绘制小方框。这样在小方框移动时，只需清空上层canvas的局部画布重绘小方框即可，不需要重绘底层的缩略图。&lt;/p&gt;
</content:encoded><category>工作</category><category>前端</category><category>html5</category><category>canvas</category><author>joyme123</author></item><item><title>接口请求速率（接口防刷）限制方案</title><link>https://www.myway5.com/blog/flowcontrol/</link><guid isPermaLink="true">https://www.myway5.com/blog/flowcontrol/</guid><description>当前业务中接口防刷的场景有: 1.短信/邮件接口。这个接口不做好防刷策略，损失是很大的。 2.涉及到安全问题的接口调用。比如注册接口，登录接口等等。过于频繁的请求可能代表着用户的批量注册，用户密码的暴力破解（需要考虑到可能是用户忘记密码了)等等。 2.要求 1.</description><pubDate>Mon, 16 Apr 2018 09:10:05 GMT</pubDate><content:encoded>&lt;h2&gt;1.场景&lt;/h2&gt;
&lt;p&gt;当前业务中接口防刷的场景有:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.短信/邮件接口。这个接口不做好防刷策略，损失是很大的。&lt;/li&gt;
&lt;li&gt;2.涉及到安全问题的接口调用。比如注册接口，登录接口等等。过于频繁的请求可能代表着用户的批量注册，用户密码的暴力破解（需要考虑到可能是用户忘记密码了)等等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.要求&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;1.与当前业务系统解耦合&lt;/li&gt;
&lt;li&gt;2.可以很方便的对不同的接口设置不同的请求频率限制&lt;/li&gt;
&lt;li&gt;3.具有较好的性能。在大并发的情况下，需要有正确的结果&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.方案提出&lt;/h2&gt;
&lt;h3&gt;3.1 token bucket（令牌桶）&lt;/h3&gt;
&lt;p&gt;token bucket算法的如下图： &lt;img src=&quot;/uploads/wp/2018/04/token-bucket.jpg&quot; alt=&quot;token_bucket&quot; /&gt; 描述如下: 1.假设有一个桶，不断的向桶中投放token，并且我们也可以从桶中取出token。 2.投放token的速率是恒定的r1，桶满了token数量则不会再增长。 3.取出的速率是随机的r2。如果桶是空的，则无法取出token。 4.所有的请求都必须从桶中取出token才是有效的。否则该请求会被拒绝。 举个例子，假设桶的容量是10，每秒放2个token进去。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果现在每秒2个请求，那么所有这样的请求都是没有问题的，都会拿到token。&lt;/li&gt;
&lt;li&gt;如果现在每秒4个请求,10 + 2x = 4x。也就是x=5秒后请求就没法及时拿到token，被丢弃。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结出以下特点: - 如果请求速率过快，到一段时间后会受到限制 - 允许突发请求&lt;/p&gt;
&lt;h3&gt;3.2 redis数据库&lt;/h3&gt;
&lt;p&gt;使用redis数据库完全是因为方便以及速度。只要注意这其中存在的CAS问题即可&lt;/p&gt;
&lt;h2&gt;4.具体实现&lt;/h2&gt;
&lt;p&gt;首先，为了解决redis中检查和存储数据存在的CAS问题，我们需要使用lua脚本。因此，对于token bucket的主要实现是用lua脚本实现,然后通过php调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;local function mysplit(inputstr, sep)
    if sep == nil then
            sep = &quot;%s&quot;
    end
    local t={} ; 
    local i=1
    for str in string.gmatch(inputstr, &quot;([^&quot;..sep..&quot;]+)&quot;) do
            t[i] = str
            i = i + 1
    end
    return t
end

local res = redis.pcall(&apos;get&apos;,KEYS[1])

if (res == false) then
    -- bucket不存在，初始化为capacity - 1
    local str = string.format(&quot;%d %d&quot;,ARGV[1] - 1,ARGV[2])  
    return redis.pcall(&apos;set&apos;,KEYS[1],str)
end

local arr_str = mysplit(res, &quot; &quot;)

local num = arr_str[1]
local timestamp = arr_str[2]

-- 根据时间戳来计算num，这里一秒2个令牌
num = num + (ARGV[2] - timestamp) * ARGV[3]
-- 不超过10个令牌
if num &amp;gt; 10 then
    num = 10
end

if (tonumber(num) &amp;gt; 0) then
    -- 拿到token并减一
    local str = string.format(&quot;%d %d&quot;,num - 1,ARGV[2])
    return redis.pcall(&apos;set&apos;, KEYS[1], str)
else 
    -- 没有拿到token
    return false
end
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

$sha = &apos;2ff7ad2d1b49da8430e5adc8675e&apos;;

/**
 * 初始化lua脚本
 */
function initScript($redis) {
    global $sha;
    //不存在脚本，load进去
    $script = file_get_contents(&apos;check_and_set.lua&apos;);
    $sha = $redis-&amp;gt;script(&apos;load&apos;, $script);
    if($redis-&amp;gt;getLastError() !== NULL){
        echo &quot;出错了：&quot;.$redis-&amp;gt;getLastError().&apos;\n&apos;;
    }
    echo &quot;初始化的脚本sha:&quot;.$sha.PHP_EOL;
}

function getTokenFromBucket($redis, $bucket) {
    global $sha;
    $capacity = 10;
    $time = time();
    $inputRate = 2;     //一秒2个token
    $params = array($bucket, $capacity, $time, $inputRate);
    $result = $redis-&amp;gt;evalSha($sha, $params, 1);
    if($redis-&amp;gt;getLastError() !== NULL){
        echo &quot;出错了：&quot;.$redis-&amp;gt;getLastError().&apos;\n&apos;;
    }
    return $result;
}

$redis = new Redis();
$redis-&amp;gt;pconnect(&quot;127.0.0.1&quot;);

initScript($redis);

$start = microtime(true) * 1000;

while(true) {
    $result =  getTokenFromBucket($redis, &apos;bucket1&apos;);
    if (!$result) {
        break;
    } else {
        echo time().&quot;--拿到令牌&quot;.PHP_EOL;
        usleep(250000);    //每秒请求4次,10 + 2x = 4x, x = 5。5秒左右无法拿到令牌
    }
}

$end = microtime(true) * 1000;

echo &quot;共耗时：&quot;.($end - $start).&quot;毫秒&quot;.PHP_EOL.PHP_EOL;

$start = microtime(true) * 1000;

while(true) {
    $result =  getTokenFromBucket($redis, &apos;bucket2&apos;);
    if (!$result) {
        break;
    } else {
        echo &quot;拿到令牌&quot;.PHP_EOL;
        usleep(500000);    //每秒请求2次，永远可以拿到令牌
    }
}

$end = microtime(true) * 1000;

echo &quot;共耗时：&quot;.($end - $start).&quot;毫秒&quot;.PHP_EOL.PHP_EOL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码中主要部分概括如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查redis中是否存在桶&lt;code&gt;bucket1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果不存在，那么默认的容量是10，将bucket1设为&lt;code&gt;9 timestamp&lt;/code&gt;(timestamp为当前的时间戳)。返回true&lt;/li&gt;
&lt;li&gt;如果存在，则获取该bucket1的值，解析出实际token数量为num,以及上一次存储时时间戳为pre。如果rate为token的生成速率，那么现在的token数量应该是:num = num + (time() - pre) * rate。num最大为默认容量10。如果num大于0，则减一后重新设置，返回true。否则返回false。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码的运行结果如下： &lt;img src=&quot;/uploads/wp/2018/04/%E6%B7%B1%E5%BA%A6%E6%88%AA%E5%9B%BE_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20180416170607.png&quot; alt=&quot;运行结果演示&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;5.扩展知识&lt;/h2&gt;
&lt;p&gt;与token bucket相似的算法还有leaky bucket（漏桶）。leaky bucket 就像是 token bucket的镜像一样。leaky bucket 一般用于Traffic shaping和Traffic policing等问题，与下面的两张图对应。 &lt;img src=&quot;/uploads/wp/2018/04/Leaky_bucket_as_a_meter-shaping.jpeg&quot; alt=&quot;leaky bucket&quot; /&gt; &lt;img src=&quot;/uploads/wp/2018/04/Leaky_bucket_as_a_meter-policing.jpeg&quot; alt=&quot;leaky bucket&quot; /&gt; 可以描述如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.有一个桶，其上方水龙头以变化的速率r1向里面滴水，桶底的水龙头则以恒定的速率向下漏水&lt;/li&gt;
&lt;li&gt;2.水桶装满后，上方水龙头的滴水就会溢出&lt;/li&gt;
&lt;li&gt;3.水桶空后，下方水龙头则不会滴水&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里举一个异步任务处理（任务队列的容量是有限的）的例子。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上方水龙头相当于任务投递系统，下方水龙头相当于任务处理系统。&lt;/li&gt;
&lt;li&gt;任务投递的速率是变化的，因为不同时间段的系统负载可能不同。&lt;/li&gt;
&lt;li&gt;任务处理系统的速率是恒定的，因为机器的处理能力是恒定的。&lt;/li&gt;
&lt;li&gt;当任务投递的速率大于处理速率，填充满任务队列后，后面的任务都会被丢弃。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看出leaky bucket可以将不规则的抖动的请求序列变成规则的平滑的请求序列。leaky bucket通常有两个版本：作为一个评判的仪器(meter)或者是生成一个合法请求队列(queue)。 虽然在使用中leaky bucket和token bucket是不同的，但实际上他们是同一种思路。leaky bucket的变化的输入相当于token bucket的变化的输出，leaky bucket的恒定的输出相当于token bucket的恒定的输出。&lt;/p&gt;
&lt;h2&gt;参考文献&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Token_bucket&quot; rel=&quot;noopener&quot;&gt;token bucket&lt;/a&gt; &lt;a href=&quot;https://en.wikipedia.org/wiki/Leaky_bucket&quot; rel=&quot;noopener&quot;&gt;leaky bucket&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>工作</category><category>redis</category><category>接口防刷</category><category>token_bucket</category><author>joyme123</author></item><item><title>redis中的事务与锁</title><link>https://www.myway5.com/blog/redis-transaction-and-lock/</link><guid isPermaLink="true">https://www.myway5.com/blog/redis-transaction-and-lock/</guid><description>这篇文章是我在查找如何对redis中的值做原子操作时的一系列笔记，虽然最初的目的只是研究有哪些方式可以实现事务(transaction)操作，但是后来的延伸很多，所以我认为有必要做一些笔记防止忘记。 1.redis中的事务</description><pubDate>Wed, 11 Apr 2018 13:40:01 GMT</pubDate><content:encoded>&lt;p&gt;这篇文章是我在查找如何对redis中的值做原子操作时的一系列笔记，虽然最初的目的只是研究有哪些方式可以实现事务(transaction)操作，但是后来的延伸很多，所以我认为有必要做一些笔记防止忘记。&lt;/p&gt;
&lt;h2&gt;1.redis中的事务&lt;/h2&gt;
&lt;p&gt;redis中的事务其实并不满足数据库事务的四个特性：原子性（Atomicity）、一致性（Consistency）、隔离型（Isolation）、持久性（Durability），简称ACID。redis只能在一致性和隔离性上提供保证，而原子性和持久性是无法保证的。因为redis的事务操作如果中断，无法回滚，满足不了原子性，并且redis的持久化策略有很多中，比如在纯内存模式下是无法持久化数据库更改的，满足不了持久性。 所以下面针对与redis事务的讨论都是有局限性的。仅仅是指对redis数据进行操作时，不会受到其他客户端的干扰。举例来说：客户端A读取keyA，修改keyA中间，不会受到客户端B的影响。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //客户端A执行以下代码
    //获取user1的性别，如果性别是男，头像设置为男性角色
    //否则头像设置为女性角色
    $gender = $redis-&amp;gt;get(&apos;user1_gender&apos;);
    if($gender === &apos;male&apos;){
        $redis-&amp;gt;set(&apos;user1_photo&apos;,&apos;male.png&apos;);
    }else{
        $redis-&amp;gt;set(&apos;user1_photo&apos;,&apos;female.png&apos;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    //客户端B执行以下代码
    //更改user1的性别，如果是男则改为女，如果是女则改为男
    //并根据性别设置头像
    $gender = $redis-&amp;gt;get(&apos;user1_gender&apos;);
    if($gender === &apos;male&apos;){
        $redis-&amp;gt;set(&apos;user1_gender&apos;,&apos;female&apos;);
        $redis-&amp;gt;set(&apos;user1_photo&apos;,&apos;female.png&apos;);
    }else{
        $redis-&amp;gt;set(&apos;user1_gender&apos;,&apos;male&apos;);
        $redis-&amp;gt;set(&apos;user1_photo&apos;,&apos;male.png&apos;);
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果客户端A、B同时只有一个执行，那么性别和头像一定是对应的。但是A、B同时执行，并且执行顺序如下: &lt;img src=&quot;/uploads/wp/2018/03/%E6%B7%B1%E5%BA%A6%E6%88%AA%E5%9B%BE_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20180308151942.png&quot; alt=&quot;时序图&quot; /&gt; 那么执行结果就是user1性别为女，但头像为男性角色。 所以针对于这种情况，我们需要一定的措施去预防。 redis的事务实现方式有多种。据我所知有三种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.&lt;code&gt;MULTI&lt;/code&gt;/&lt;code&gt;EXEC&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;2.&lt;code&gt;WATCH&lt;/code&gt;/&lt;code&gt;UNWATCH&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;3.lua脚本&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.1 &lt;code&gt;MULTI&lt;/code&gt;/&lt;code&gt;EXEC&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MULTI&lt;/code&gt;/&lt;code&gt;EXEC&lt;/code&gt;是成对出现的指令。大家都知道，redis执行指令是单线程的，也就是说所有指令都处于一个队列，一个一个执行。&lt;code&gt;MULTI&lt;/code&gt;/&lt;code&gt;EXEC&lt;/code&gt;可以保证所有的指令都会被同时放松到redis中，中间不会掺杂来自其他客户端的指令。 但是这个针对于上述情况并不适用。因为我们需要在GET到数据之后，才能做下面的更新操作。&lt;/p&gt;
&lt;h3&gt;1.2 &lt;code&gt;WATCH&lt;/code&gt;/&lt;code&gt;UNWATCH&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;在redis中我们可以使用watch来监视一个值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis&amp;gt; WATCH name
OK

redis&amp;gt; MULTI
OK

redis&amp;gt; SET name peter
QUEUED

redis&amp;gt; EXEC
(nil)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如这段代码，在我们WATCH name后，如果另外一个客户端修改了name的值，那么这个客户端再次修改name则无法成功。这其实就是乐观锁的一种实现。它保证了代码的执行结果不会错乱。用一开始的例子来说，就是不会出现性别和头像不对应的情况。 1.3 lua脚本 redis中可以执行lua脚本来完成事务操作。lua脚本和MULTI/EXEC很像，也就是说在lua脚本执行的过程中，redis是不执行其他客户端的指令的。 但是lua脚本的不同之处在于，你可以使用GET操作来获取数据并判断，再执行后来的操作。&lt;/p&gt;
&lt;h2&gt;2.在redis操作中使用锁&lt;/h2&gt;
&lt;p&gt;在多线程环境中，为了防止资源出现race condition，需要借助锁来互斥的访问资源。在这里也是一样的，user1_gender就是我们要互斥访问的资源。 还是上述那个例子，如果我们使用悲观锁，只有获得锁的客户端才能读取和修改user1的的值，也可以很好的解决这个问题。 伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$can_lock = lock(&apos;user1_gender&apos;); //得到锁
if($can_lock){
    do_something();
    $release_lock(&apos;user1_gender&apos;);  //释放锁
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不同于多线程环境下的是，我们这里的锁的范围是针对于不同的客户端。因此没法使用基于系统的、或者基于语言的锁，而是得使用分布式的锁。这样的分布式锁我们同样可以借助于redis来实现。 整个分布式锁的实现可以概括为以下几个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;获得锁。得到要锁的资源的唯一hash：&lt;code&gt;lockname&lt;/code&gt;,以及一个随机字符串：&lt;code&gt;identifier&lt;/code&gt;，设置expire：20s,这个expire就是锁的有效期，在有效期后锁会自动释放，防止出现死锁。在redis中使用setnx(lockname,identifier,expire)。这句代码的意思是:如果redis中不存在lockname，则存入lockname，值为identifier，过期时间是expire。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;第一步我们就得到了一个锁，这一步我们开始执行获得锁之后的代码&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;释放锁。我们根据lockname来从redis中查找。如果get(lockname) identifier，则表示我们仍然持有这把锁，使用delete(lockname)来释放锁。如果不等于，说明我们已经不持有这把锁了，则什么也不做。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么lock函数可以用以下代码来描述:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function lock($lockname,$identifier,$expire = 20){
    $acquire_timeout = 10;      //花费10秒去获得锁，否则就放弃
    $end = time() + $acquire_timeout
    while (time() &amp;lt; $end){
        $can_lock = $redis-&amp;gt;setnx($lockname,$identifier,$expire);
        if($can_lock){
            return true;
        }
        sleep(0.1);
    }

    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    function release_lock($lockname,$identifier){
        $redis-&amp;gt;watch($lockname);
        try{
            if($redis-&amp;gt;get($lockname) === $identifier){
                //锁仍然持有，释放锁
                $redis-&amp;gt;delete($lockname);
                return true;
            }

            $redis-&amp;gt;unwatch();
        }catch(Exception $e){

        }

        return false;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样我们就很轻松的得到了借助于redis实现的分布式锁。但是这样的实现方式依然是有问题的。 问题1：如果在某个客户端获得锁后，redis主服务器宕机了，那么即使我们使用了主从备份，从属服务器被提升为主服务器，因为redis备份是异步的原因，这里的锁是没法及时同步到从属服务器的。 问题2：如果一个客户端在获得锁后，执行的操作超过了锁的有效期，锁被自动释放了。那么后续的操作是没法受到锁的保护的。 问题2的解决方案可以是watch，在获取锁后，可以立刻watch资源，然后再执行余下操作。 问题1的解决方案则是接下来要介绍的redlock算法&lt;/p&gt;
&lt;h2&gt;3.redlock&lt;/h2&gt;
&lt;p&gt;redlock算法是Redis的作者antirez提出来的。 可以被概述为以下几个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1.获取当前时间（毫秒数）:start_time。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2.按顺序获得N个Redis节点的锁（使用相同的key和identifier，并设置初始有效时间:init_validity_time）。在获得每个redis结点的锁的时候，都要设置一个timeout参数，这个timeout要远小于锁的自动释放时间。例如：如果锁的自动释放时间是10s，timeout应该为~5-55ms（还得视网络情况决定）。这样可以防止在获取锁时，节点宕机，导致耗时过长锁被释放了。如果获取锁失败则立刻获取下一个redis节点的锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3.client计算为了获取锁花了多长时间：used_time = current_time - start_time。当且仅当client可以获取大多数实例的时候（至少N / 2 + 1个)，所花费的时间小于锁的有效时间，才认为获得了锁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;4.如果获得了锁，重新计算锁的有效时间:validity_time = init_validity_time - used_time&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;5.如果锁获取失败(无法获取N/2 + 1个节点的锁，或者有效时间validity_time是负数），则释放所有实例的锁（即使获得锁的时候失败了，这主要是考虑到有的时候锁获得成功了，但是告知客户端时网络异常）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这很好的解决了上述的问题1，通过多个redis节点了来保证分布式锁服务的可靠性。&lt;/p&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://redis.io/topics/distlock%20%E2%80%9CDistributed%20locks%20with%20Redis%E2%80%9D&quot; rel=&quot;noopener&quot;&gt;Distributed locks with Redis&lt;/a&gt; &lt;a href=&quot;https://mp.weixin.qq.com/s/JTsJCDuasgIJ0j95K8Ay8w&quot; rel=&quot;noopener&quot;&gt;基于Redis的分布式锁到底安全吗（上）？&lt;/a&gt; &lt;a href=&quot;https://mp.weixin.qq.com/s/4CUe7OpM6y1kQRK8TOC_qQ&quot; rel=&quot;noopener&quot;&gt;基于Redis的分布式锁到底安全吗（下）？&lt;/a&gt; &lt;a href=&quot;http://redisbook.readthedocs.io/en/latest/feature/transaction.html&quot; rel=&quot;noopener&quot;&gt;redis事务&lt;/a&gt; &lt;a href=&quot;https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-2-distributed-locking/6-2-3-building-a-lock-in-redis/&quot; rel=&quot;noopener&quot;&gt;6.2.3 Building a lock in Redis&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>php</category><category>redis</category><category>redis</category><category>transaction</category><category>lock</category><category>distributed-lock</category><author>joyme123</author></item><item><title>收藏</title><link>https://www.myway5.com/blog/collections/</link><guid isPermaLink="true">https://www.myway5.com/blog/collections/</guid><description>这里会放一些我觉得很好的博客、文章、资源等等。 The Knuth-Morris-Pratt Algorithm in my own words：这是一篇讲KMP的文章，非常详细易懂。KMP算法的next数组怎么求:https://blog.csdn.</description><pubDate>Fri, 30 Mar 2018 03:10:02 GMT</pubDate><content:encoded>&lt;p&gt;这里会放一些我觉得很好的博客、文章、资源等等。 &lt;a href=&quot;http://jakeboxer.com/blog/2009/12/13/the-knuth-morris-pratt-algorithm-in-my-own-words/&quot; title=&quot;The Knuth-Morris-Pratt Algorithm in my own words&quot; rel=&quot;noopener&quot;&gt;The Knuth-Morris-Pratt Algorithm in my own words&lt;/a&gt;：这是一篇讲KMP的文章，非常详细易懂。KMP算法的next数组怎么求:&lt;a href=&quot;https://blog.csdn.net/qq%5C_30974369/article/details/74276186&quot; rel=&quot;noopener&quot;&gt;https://blog.csdn.net/qq\_30974369/article/details/74276186&lt;/a&gt; &lt;a href=&quot;https://legacy.gitbook.com/book/yar999/gopl-zh/details&quot; rel=&quot;noopener&quot;&gt;Go语言圣经（中文版）&lt;/a&gt;: go语言的教程，内容很详细，比较有深度。 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/HTTP&quot; rel=&quot;noopener&quot;&gt;Http|MDN&lt;/a&gt;: MDN一篇讲Http协议的文章。从基本概念，到缓存、Cookie、访问控制等方面进行详细的阐述。非常值得一看。 &lt;a href=&quot;https://blog.csdn.net/monkey_d_meng/article/details/6647488&quot; rel=&quot;noopener&quot;&gt;树形结构的数据库表Schema设计&lt;/a&gt;：一篇讲了在关系数据库中如何存储树形结构的博文。基于左右值的存储方式有一种b树的感觉。但是使用场景应该是较小的树形结构，不然在修改树的结构时效率很差。 &lt;a href=&quot;https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md&quot; rel=&quot;noopener&quot;&gt;Raft一致性算法论文译文&lt;/a&gt;，&lt;a href=&quot;https://www.youtube.com/watch?v=YbZ3zDzDnrw&quot; rel=&quot;noopener&quot;&gt;raft的视频&lt;/a&gt;，&lt;a href=&quot;https://zhuanlan.zhihu.com/p/27970722&quot; rel=&quot;noopener&quot;&gt;知乎上的一篇讲述&lt;/a&gt;,&lt;a href=&quot;https://raft.github.io/&quot; rel=&quot;noopener&quot;&gt;raft官网&lt;/a&gt;。&lt;a href=&quot;http://thesecretlivesofdata.com/raft/&quot; rel=&quot;noopener&quot;&gt;图解raft&lt;/a&gt; &lt;a href=&quot;http://www.infoq.com/cn/articles/a-million-go-routines-but-only-1000-java-threads&quot; rel=&quot;noopener&quot;&gt;为什么能有上百万个Goroutines，却只能有上千个Java线程？&lt;/a&gt;：通过对比来了解go的协程。 &lt;a href=&quot;https://www.youtube.com/watch?v=f6kdp27TYZs&quot; rel=&quot;noopener&quot;&gt;Google I/O 2012 - Go Concurrency Patterns&lt;/a&gt;：如何在go中更好的使用它的并发模型。可以结合这篇PPT：&lt;a href=&quot;https://talks.golang.org/2012/concurrency.slide#1&quot; rel=&quot;noopener&quot;&gt;https://talks.golang.org/2012/concurrency.slide#1&lt;/a&gt; &lt;a href=&quot;http://davmac.org/davpage/linux/async-io.html&quot; rel=&quot;noopener&quot;&gt;什么是异步io&lt;/a&gt;: 非常详细的讲解了linux中的异步IO的概念，使用 从&lt;a href=&quot;https://medium.com/google-cloud/understanding-kubernetes-networking-pods-7117dd28727&quot; rel=&quot;noopener&quot;&gt;Pod&lt;/a&gt;,&lt;a href=&quot;https://medium.com/google-cloud/understanding-kubernetes-networking-services-f0cb48e4cc82&quot; rel=&quot;noopener&quot;&gt;Service&lt;/a&gt;,&lt;a href=&quot;https://medium.com/google-cloud/understanding-kubernetes-networking-ingress-1bc341c84078&quot; rel=&quot;noopener&quot;&gt;Ingress&lt;/a&gt;三个层面讲解k8s的网络 &lt;a href=&quot;http://www.zsythink.net/archives/category/%E8%BF%90%E7%BB%B4%E7%9B%B8%E5%85%B3/iptables/&quot; rel=&quot;noopener&quot;&gt;iptables&lt;/a&gt; iptables系列，非常详细易懂的讲解 &lt;a href=&quot;https://ruslanspivak.com/lsbasi-part1/&quot; rel=&quot;noopener&quot;&gt;Let’s Build A Simple Interpreter.&lt;/a&gt; 从 0 构建一个解释性语言。&lt;/p&gt;
</content:encoded><author>joyme123</author></item><item><title>系统权限设计中RBAC模型的使用</title><link>https://www.myway5.com/blog/rbac/</link><guid isPermaLink="true">https://www.myway5.com/blog/rbac/</guid><description>RBAC，全称是Role-Based-Access-Control，可以译为基于角色的权限控制，是一种应用非常广泛的权限控制模型。 什么是角色(Role)</description><pubDate>Thu, 29 Mar 2018 04:35:11 GMT</pubDate><content:encoded>&lt;h2&gt;什么是RBAC&lt;/h2&gt;
&lt;p&gt;RBAC，全称是Role-Based-Access-Control，可以译为&lt;code&gt;基于角色的权限控制&lt;/code&gt;，是一种应用非常广泛的权限控制模型。&lt;/p&gt;
&lt;h2&gt;什么是角色(Role)&lt;/h2&gt;
&lt;p&gt;角色不是一个用户实体，而是代表了&lt;code&gt;一组行为能力或责任的命名实体&lt;/code&gt;，以我们常用的QQ群为例，有以下角色：超级管理员、群主、管理员、普通成员、非群成员。 每个角色都有不同的权限:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;超级管理员可以管理任何的QQ群，但是一般只能禁言、删除群，不能管理具体的某个群成员，也不能在群里聊天；&lt;/li&gt;
&lt;li&gt;群主可以管理自己的QQ群，可以在群里聊天；&lt;/li&gt;
&lt;li&gt;管理员可以管理自己的QQ群，可以聊天，但是不能解散群，也不能任命或撤销其他管理员；&lt;/li&gt;
&lt;li&gt;普通成员则没有管理权限，只有在群里聊天的权限&lt;/li&gt;
&lt;li&gt;非群成员无管理权限，也无法在群里聊天。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以可以知道，每个角色都对应着一组行为能力或责任。&lt;/p&gt;
&lt;h2&gt;隐式的基于角色的权限控制&lt;/h2&gt;
&lt;p&gt;对于QQ群的例子来说。如果我们要做一个删除某个群成员的动作，伪代码可能如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if( user.hasRole(&apos;Group Owner&apos;) || user.hasRole(&apos;Group Manager&apos;) ){
    // delete the Group Member
} else {
    // Permission Denied
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么现在，如果腾讯的权限策略发生了改变，超级管理员也可以删除某个群的群成员来防止某些不当言论的传播，那么代码就要改为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if( user.hasRole(&apos;Group Owner&apos;) || user.hasRole(&apos;Group Manager&apos;) || user.hasRole(&apos;Super Manager&apos;) ){
    // delete the Group Member
} else {
    // Permission Denied
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这种权限的管理方式，就可以称为隐式的基于角色的权限控制。因为&lt;code&gt;Group Owner&lt;/code&gt;、&lt;code&gt;Group Manager&lt;/code&gt;、&lt;code&gt;Super Manager&lt;/code&gt;并不能显式的表达出：它们的角色具有删除群成员的权限。没有任何的代码显式的定义了这些角色的权限。我们只能从代码中隐式的推测出：这三个角色拥有删除群成员的权限。所以程序员们使用if/else语句来反映这些假设。&lt;/p&gt;
&lt;h2&gt;显式的基于角色的权限控制&lt;/h2&gt;
&lt;p&gt;了解了&lt;code&gt;隐式的基于角色的权限控制&lt;/code&gt;，那么我们就可以知道，&lt;code&gt;显式的基于角色的权限控制&lt;/code&gt;要有能力直接表达出：当前用户有权限去删除群成员。这样我们的代码可以调整如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if( user.isPermitted(&apos;GroupMember:delete:478&apos;) ){
    // delete the Group Member
} else {
    // Permission Denied
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样从代码中可以看到，如果当前用户被允许删除ID为478的群成员，那么就去删除，否则报权限不足的错误。至于isPermitted()中如何去判断权限的，可能依然是回到了哪些角色有哪些权限的问题。但是不同之处在于，应对上面的需求变更问题时，我们只需要更改user的isPermitted的判断规则，而不用去更改散布在代码中各个地方的if语句。可以做到以最小的变更来应对复杂的需求变化。&lt;/p&gt;
&lt;h2&gt;隐式 vs 显式&lt;/h2&gt;
&lt;p&gt;隐式和显式在我看来，其内在的权限控制仍然是一样的，都是基于角色在做权限判断。但对外的抽象则是截然不同的：隐式侧重于某个用户是否有某些角色，显式则直接将问题聚焦于某个用户是否有对某个资源的某个操作的权限。这在写代码中给程序员带来的影响是不一样的。截取一段项目中的代码来佐证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//检查操作的权限
if(
    !$familyDB-&amp;gt;isUserForFamily($familyId,$userId)&amp;amp;&amp;amp;
    !$familyDB-&amp;gt;isAdminForFamily($familyId,$userId)&amp;amp;&amp;amp;
    !$familyDB-&amp;gt;isOriginatorForFamily($familyId,$userId)
){
    Util::printResult( $GLOBALS[&apos;ERROR_PERMISSION&apos;], &quot;操作权限错误&quot;);
    exit;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码判断了用户是否对家族有读取权限。因为我们是&lt;code&gt;隐式的基于角色的权限控制&lt;/code&gt;，很直观的想法就是：家族成员、家族管理员、家族创始人这三个角色都有对家族的读取权限。所以这里判断了三个权限。事实上，家族创始人的判断是多余的，因为家族创始人肯定属于家族成员。但是真正在写代码的时候，很有可能考虑不到这个问题。 但如果是&lt;code&gt;显式的基于角色的权限控制&lt;/code&gt;，这个if语句就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//检查操作的权限
if(
    !$familyDB-&amp;gt;isUserHasReadPermission($familyId,$userId))
){
    Util::printResult( $GLOBALS[&apos;ERROR_PERMISSION&apos;], &quot;操作权限错误&quot;);
    exit;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序员写代码时就不会做出多余的判断。可以得出，显式的抽象表达能力是明显更强的。&lt;/p&gt;
&lt;h2&gt;新的RBAC：Resource-Based-Access-Control&lt;/h2&gt;
&lt;p&gt;通过对比&lt;code&gt;隐式&lt;/code&gt;和&lt;code&gt;显式&lt;/code&gt;的区别，我们知道&lt;code&gt;显式&lt;/code&gt;是直接检查某个&lt;code&gt;用户&lt;/code&gt;对某个&lt;code&gt;资源（Resource）&lt;/code&gt;是否有某个&lt;code&gt;权限&lt;/code&gt;。所以不如直接抛弃&lt;code&gt;角色（Role）&lt;/code&gt;的概念，将&lt;code&gt;资源&lt;/code&gt;这个概念引入。这样就有了&lt;code&gt;基于资源的权限控制（Resource-Based-Access-Control）&lt;/code&gt;。&lt;/p&gt;
</content:encoded><category>架构设计</category><category>工作</category><category>权限控制</category><category>RBAC</category><category>Role-Based-Access-Control</category><category>Resource-Based-Access-Control</category><author>joyme123</author></item><item><title>xdebug的简易使用教程</title><link>https://www.myway5.com/blog/xdebug/</link><guid isPermaLink="true">https://www.myway5.com/blog/xdebug/</guid><description>1.ubuntu下的安装 通过 pecl 安装 pecl install xdebug</description><pubDate>Fri, 16 Mar 2018 08:02:53 GMT</pubDate><content:encoded>&lt;h2&gt;1.ubuntu下的安装&lt;/h2&gt;
&lt;h3&gt;通过 pecl 安装&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;pecl install xdebug&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后将xdebug.so加入php.ini中，注意如果使用的是fpm，则需要加入到fpm下的php.ini,同理cli环境下则需要向cli下的php.ini添加&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;zend_extension=&quot;/usr/local/php/modules/xdebug.so&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意:xdebug是zend的拓展，不需要添加&lt;code&gt;extension=xdebug.so&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;通过编译安装&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;git clone git://github.com/xdebug/xdebug.git cd xdebug phpize ./configure --enable-xdebug make make install&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2.配置php使用xdebug&lt;/h2&gt;
&lt;p&gt;xdebug有许多特性。&lt;/p&gt;
&lt;h3&gt;2.1 通过设置来影响var_dump()&lt;/h3&gt;
&lt;p&gt;影响var_dump()的属性有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;xdebug.var_display_max_children,&lt;/li&gt;
&lt;li&gt;xdebug.var_display_max_data&lt;/li&gt;
&lt;li&gt;xdebug.var_display_max_depth&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这三个属性的值都是数字类型的。会影响var_dump()函数显示的变量的内容长度和深度。 可以在php.ini做以下设置：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;xdebug.var_display_max_depth = 2 xdebug.var_display_max_data = 8 xdebug.var_display_max_children = 3&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;具体的值可以自己手动调整 另外还有&lt;code&gt;xdebug.cli_color&lt;/code&gt;和&lt;code&gt;xdebug.overload_var_dump&lt;/code&gt;会影响到显示的效果&lt;/p&gt;
&lt;h3&gt;2.2 堆栈跟踪&lt;/h3&gt;
&lt;p&gt;演示脚本如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
//这个脚本会超时
function foo( $a ) {
    for ($i = 1; $i &amp;lt; $a[&apos;foo&apos;]; $i++) {
        if ($i == 500000) xdebug_break();
    }
}

set_time_limit(1);
$c = new stdClass;
$c-&amp;gt;bar = 100;
$a = array(
    42 =&amp;gt; false, &apos;foo&apos; =&amp;gt; 9121240000000000,
    $c, new stdClass, fopen( &apos;/etc/passwd&apos;, &apos;r&apos; )
);
foo( $a );
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用设置&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;xdebug.collect_params = 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;结果如下: &lt;img src=&quot;/uploads/wp/2018/03/%E6%B7%B1%E5%BA%A6%E6%88%AA%E5%9B%BE_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20180316153016.png&quot; alt=&quot;params为1&quot; /&gt; 修改一下设置：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;xdebug.collect_params = 3&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2018/03/%E6%B7%B1%E5%BA%A6%E6%88%AA%E5%9B%BE_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20180316153516.png&quot; alt=&quot;params为3&quot; /&gt; 可以看到浏览器中的报错信息更多了，体现在foo()函数中的参数数量 同样的，我们还可以设置&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;xdebug.dump_globals = On xdebug.dump.SERVER = &apos;REQUEST_URI&apos;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这样则可以展示一些超全局变量。这里指定了请求的URI &lt;img src=&quot;/uploads/wp/2018/03/%E6%B7%B1%E5%BA%A6%E6%88%AA%E5%9B%BE_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20180316153905.png&quot; alt=&quot;超全局变量&quot; /&gt; 还可以设置&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;xdebug.show_local_vars = On&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;来展示程序运行期间的本地变量 &lt;img src=&quot;/uploads/wp/2018/03/%E6%B7%B1%E5%BA%A6%E6%88%AA%E5%9B%BE_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20180316154049.png&quot; alt=&quot;本地变量&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.3 函数跟踪&lt;/h3&gt;
&lt;p&gt;使用xdebug可以记录所有的函数调用。 测试脚本如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

ini_set(&apos;xdebug.trace_format&apos;,&apos;0&apos;);

xdebug_start_trace();
$str = &quot;Xdebug&quot;;
function ret_ord( $c )
{
    return ord( $c );
}

foreach ( str_split( $str ) as $char )
{
    echo $char, &quot;: &quot;, ret_ord( $char ), &quot;\n&quot;;
}
xdebug_stop_trace();
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用的设置如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;;代码跟踪日志文件位置,注意要先新建这个/tmp/php_traces/fpm目录，并设置777
xdebug.auto_trace = Off
xdebug.trace_output_dir = /tmp/php_traces/fpm
;代码跟踪日志文件格式 
xdebug.trace_output_name = trace.%c.%p
;trace中显示函数的参数值，这个很有用，待会细说
xdebug.collect_params = 3
xdebug.collect_includes = On
xdebug.collect_return = On
xdebug.show_mem_delta = On
xdebug.var_display_max_depth = 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下: &lt;img src=&quot;/uploads/wp/2018/03/%E6%B7%B1%E5%BA%A6%E6%88%AA%E5%9B%BE_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20180316154843.png&quot; alt=&quot;此处输入图片的描述&quot; /&gt; 通过调整&lt;code&gt;xdebug.trace_format&lt;/code&gt;的值可以更改记录的格式。0是人类可读，1是机器可读，2是html&lt;/p&gt;
&lt;h3&gt;2.4远程调试&lt;/h3&gt;
&lt;p&gt;这里xdebug中非常好用的一个功能。通过设置，我们可以在IDE中单步调试。下面我会使用vscode来演示一遍。&lt;/p&gt;
&lt;h4&gt;2.4.1 环境准备&lt;/h4&gt;
&lt;p&gt;1.首先我们需要为vscode安装xdebug的插件。 2.配置好调试环境 &lt;img src=&quot;/uploads/wp/2018/03/%E6%B7%B1%E5%BA%A6%E6%88%AA%E5%9B%BE_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20180316155714.png&quot; alt=&quot;此处输入图片的描述&quot; /&gt; 这一步是在vscode左侧调试栏新增配置，然后选择php即可。 3.打上断点，启动调试 &lt;img src=&quot;/uploads/wp/2018/03/%E6%B7%B1%E5%BA%A6%E6%88%AA%E5%9B%BE_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20180316160013.png&quot; alt=&quot;此处输入图片的描述&quot; /&gt; 4.在浏览器中访问这个页面即可 &lt;img src=&quot;/uploads/wp/2018/03/%E6%B7%B1%E5%BA%A6%E6%88%AA%E5%9B%BE_%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20180316160056.png&quot; alt=&quot;此处输入图片的描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://xdebug.org/docs/all#default&quot; rel=&quot;noopener&quot;&gt;https://xdebug.org/docs/all#default&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>php</category><author>joyme123</author></item><item><title>ubuntu服务器部署ipv6访问</title><link>https://www.myway5.com/blog/ubuntu-ipv6/</link><guid isPermaLink="true">https://www.myway5.com/blog/ubuntu-ipv6/</guid><description>整个部署过程分为： 1.启用服务器的ipv6支持 2.申请ipv6通道 3.开启nginx的ipv6地址监听 1.ubuntu上开启ipv6支持，需要修改几个地方</description><pubDate>Fri, 09 Feb 2018 10:10:59 GMT</pubDate><content:encoded>&lt;p&gt;整个部署过程分为： 1.启用服务器的ipv6支持 2.申请ipv6通道 3.开启nginx的ipv6地址监听&lt;/p&gt;
&lt;h2&gt;1.ubuntu上开启ipv6支持，需要修改几个地方&lt;/h2&gt;
&lt;p&gt;1.开启ipv6支持，/etc/sysctl.conf 确保有以下设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;net.ipv6.conf.all.disable_ipv6 = 0
net.ipv6.conf.default.disable_ipv6 = 0
net.ipv6.conf.lo.disable_ipv6 = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置完毕后使用&lt;code&gt;sysctl -p&lt;/code&gt;使设置生效 2.设置服务器的ipv6 dns服务器,/etc/network/interfaces 添加以下设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dns-nameserver 2001:4860:4860::8888
dns-nameserver 2001:4860:4860::8844
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置完毕后使用&lt;code&gt;sudo resolvconf -u&lt;/code&gt;使设置生效 这样能够保证服务器在连接ipv6地址时有合适的dns服务器 测试:ping6 ipv6.google.com 如果能够ping通则表示已经设置成功&lt;/p&gt;
&lt;h2&gt;2.申请ipv6通道&lt;/h2&gt;
&lt;p&gt;因为我的服务器是没有分配ipv6地址的，所以需要去&lt;code&gt;http://tunnelbroker.net&lt;/code&gt;申请ipv6的通道。 1.注册账号 2.邮箱激活 3.登录 4.create regular tunnel 5.输入你的服务器的ipv4地址，创建通道 6.在example configurations处选择操作系统，获取配置 7.在 /etc/network/interfaces中拷贝6中的配置 8.&lt;code&gt;sudo resolvconf -u&lt;/code&gt;使配置生效 9.通过ifconfig查看是否设置成功，如果出现了ipv6的地址则表示成功&lt;/p&gt;
&lt;h2&gt;3.开启nginx的ipv6地址监听&lt;/h2&gt;
&lt;p&gt;在所有的站点配置文件中加上:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;listen [::]:80
listen [::]:443
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分别监听80端口和443端口(如果有https) 重启nginx即可。 通过&lt;code&gt;http://ipv6-test.com/validate.php&lt;/code&gt;测试域名能否在ipv6下访问成功。如果是阿里云，那么有可能在&lt;code&gt;IPv6 DNS server&lt;/code&gt;这一项上失败。因为阿里云的DNS服务器没有IPv6 DNS server。这种情况下，如果用户只有ipv6的环境，则会因为无法访问DNS服务器而失败&lt;/p&gt;
</content:encoded><category>linux</category><category>linux</category><category>ipv6</category><author>joyme123</author></item><item><title>ubuntu下使用GDB调试segment fault错误</title><link>https://www.myway5.com/blog/ubuntu-gdb-debug-segmentfault/</link><guid isPermaLink="true">https://www.myway5.com/blog/ubuntu-gdb-debug-segmentfault/</guid><description>注意：下面的使用仅在ubuntu实验过。 1.打开ubuntu的core dump，这样程序出错后会生成corefile以供我们分析</description><pubDate>Tue, 09 Jan 2018 02:29:49 GMT</pubDate><content:encoded>&lt;p&gt;注意：下面的使用仅在ubuntu实验过。 1.打开ubuntu的core dump，这样程序出错后会生成corefile以供我们分析&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;/tmp/corefile/core.%e.%p.%t&apos; | sudo tee /proc/sys/kernel/core_pattern
ulimit -c unlimited
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后创建/tmp/corefile文件夹&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir /tmp/corefile/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.运行程序，如果出现&lt;code&gt;段错误（核心已转储）&lt;/code&gt;，则说明corefile已经生成了。检查/tmp/corefile下是否有新生成的错误文件。 如果没有，执行&lt;code&gt;cat /proc/sys/kernel/core_pattern&lt;/code&gt;，检查&lt;code&gt;/tmp/corefile/core.%e.%p.%t&lt;/code&gt;是否写入了 3.使用gdb调试错误&lt;/p&gt;
</content:encoded><category>linux</category><author>joyme123</author></item><item><title>tcpdump分析php curl</title><link>https://www.myway5.com/blog/tcpdump-php-curl-2/</link><guid isPermaLink="true">https://www.myway5.com/blog/tcpdump-php-curl-2/</guid><description>所用工具 tcpdump:linux下分析网络的一个好用的终端工具 psysh:php的一个终端执行工具，可以很方便的在终端执行php代码 curl:php的curl库</description><pubDate>Tue, 12 Dec 2017 04:38:11 GMT</pubDate><content:encoded>&lt;h2&gt;所用工具&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;tcpdump:linux下分析网络的一个好用的终端工具&lt;/li&gt;
&lt;li&gt;psysh:php的一个终端执行工具，可以很方便的在终端执行php代码&lt;/li&gt;
&lt;li&gt;curl:php的curl库&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;tcpdump报文中Flags代表报文类型: S (SYN), F (FIN), P (PUSH), R (RST), U (URG), W (ECN CWR), E (ECN-Echo) or &apos;.&apos; (ACK), or &apos;none&apos; if no flags are set.&lt;/p&gt;
&lt;h2&gt;分析背景&lt;/h2&gt;
&lt;p&gt;最近在了解swoole，因为公司的技术栈是php，所以之前用的java那一套在普通的php开发上完全用不了。为了让php有更好的性能，更广泛的使用场景，找了找相关资料，了解到swoole这个库。决定使用之后，项目中有个地方需要使用到http连接池，所以需要使用curl，但是curl虽然强大，但是参数实在是太多了，里面的实现细节也只能查看c的源代码才知道。所以只能曲线救国，通过tcpdump了解curl在http请求上的工作机制 tcpdump的使用方法是:&lt;/p&gt;
&lt;h2&gt;案例分析&lt;/h2&gt;
&lt;h3&gt;案例一&lt;/h3&gt;
&lt;p&gt;只执行一次exec，等待一段时间，强制关闭终端进程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$context = curl_init();
curl_setopt($context,CURLOPT_URL,&quot;http://www.izuqun.com&quot;);
curl_exec($context);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整个过程的报文如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    `192.168.1.107.44664 &amp;gt; 116.62.25.128.http`: Flags [S], cksum 0x4bdf (correct), seq 3687066847, win 29200, options [mss 1460,sackOK,TS val 5931640 ecr 0,nop,wscale 7], length 0
16:28:54.014280 IP (tos 0x20, ttl 52, id 0, offset 0, flags [DF], proto TCP (6), length 60)

    `116.62.25.128.http &amp;gt; 192.168.1.107.44664`: Flags [S.], cksum 0xbcb5 (correct), seq 4174849842, ack 3687066848, win 28960, options [mss 1460,sackOK,TS val 3844377305 ecr 5931640,nop,wscale 7], length 0
16:28:54.014304 IP (tos 0x0, ttl 64, id 44712, offset 0, flags [DF], proto TCP (6), length 52)

    `192.168.1.107.44664 &amp;gt; 116.62.25.128.http`: Flags [.], cksum 0x5bba (correct), ack 1, win 229, options [nop,nop,TS val 5931643 ecr 3844377305], length 0
16:28:54.014334 IP (tos 0x0, ttl 64, id 44713, offset 0, flags [DF], proto TCP (6), length 105)

    `192.168.1.107.44664 &amp;gt; 116.62.25.128.http`: Flags [P.], cksum 0xee60 (correct), seq 1:54, ack 1, win 229, options [nop,nop,TS val 5931643 ecr 3844377305], length 53: HTTP, length: 53
        GET / HTTP/1.1
        Host: www.izuqun.com
        Accept: *\* 
16:28:54.023036 IP (tos 0x20, ttl 52, id 44520, offset 0, flags [DF], proto TCP (6), length 52)

    `116.62.25.128.http &amp;gt; 192.168.1.107.44664`: Flags [.], cksum 0x5b85 (correct), ack 54, win 227, options [nop,nop,TS val 3844377307 ecr 5931643], length 0
16:28:54.023061 IP (tos 0x20, ttl 52, id 44521, offset 0, flags [DF], proto TCP (6), length 314)
    `116.62.25.128.http &amp;gt; 192.168.1.107.44664`: Flags [P.], cksum 0xba4d (correct), seq 1:263, ack 54, win 227, options [nop,nop,TS val 3844377307 ecr 5931643], length 262: HTTP, length: 262
        HTTP/1.1 200 OK
        Server: nginx/1.12.2
        Date: Mon, 11 Dec 2017 08:28:54 GMT
        Content-Type: text/html
        Content-Length: 1243
        Last-Modified: Mon, 11 Dec 2017 02:35:33 GMT
        Connection: keep-alive
        Vary: Accept-Encoding
        ETag: &quot;5a2deef5-4db&quot;
        Accept-Ranges: bytes
16:28:54.023076 IP (tos 0x0, ttl 64, id 44714, offset 0, flags [DF], proto TCP (6), length 52)

    `192.168.1.107.44664 &amp;gt; 116.62.25.128.http`: Flags [.], cksum 0x5a73 (correct), ack 263, win 237, options [nop,nop,TS val 5931645 ecr 3844377307], length 0
16:28:54.024214 IP (tos 0x20, ttl 52, id 44522, offset 0, flags [DF], proto TCP (6), length 1295)

    `116.62.25.128.http &amp;gt; 192.168.1.107.44664`: Flags [P.], cksum 0x870b (correct), seq 263:1506, ack 54, win 227, options [nop,nop,TS val 3844377307 ecr 5931643], length 1243: HTTP
16:28:54.024236 IP (tos 0x0, ttl 64, id 44715, offset 0, flags [DF], proto TCP (6), length 52)

    `192.168.1.107.44664 &amp;gt; 116.62.25.128.http`: Flags [.], cksum 0x5585 (correct), ack 1506, win 256, options [nop,nop,TS val 5931645 ecr 3844377307], length 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1~3报文：tcp的三次握手， 4~5报文：curl发起的get请求、服务器的ack回应 6~9报文：服务器对get请求的返回、我的机器的内核的ack，因为报文长度是超出了最大的限制，所以分成两次发送 稍等一会儿，又有新的报文&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    `116.62.25.128.http &amp;gt; 192.168.1.107.44664`: Flags [F.], cksum 0x161d (correct), seq 1506, ack 54, win 227, options [nop,nop,TS val 3844393567 ecr 5931645], length 0
16:29:59.097967 IP (tos 0x0, ttl 64, id 44716, offset 0, flags [DF], proto TCP (6), length 52)
    `192.168.1.107.44664 &amp;gt; 116.62.25.128.http`: Flags [.], cksum 0xd672 (correct), ack 1507, win 256, options [nop,nop,TS val 5947914 ecr 3844393567], length 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是服务器发起的tcp连接关闭的报文，我的机器响应了，但是没有主动再次发送Fin 主动关闭psysh进程，出现大量报文&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    192.168.1.107.44664 &amp;gt; 116.62.25.128.http: Flags [F.], cksum 0x8765 (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5968150 ecr 3844393567], length 0
16:31:20.253999 IP (tos 0x0, ttl 64, id 44718, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 &amp;gt; 116.62.25.128.http: Flags [F.], cksum 0x8730 (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5968203 ecr 3844393567], length 0
16:31:20.465940 IP (tos 0x0, ttl 64, id 44719, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 &amp;gt; 116.62.25.128.http: Flags [F.], cksum 0x86fb (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5968256 ecr 3844393567], length 0
16:31:20.890008 IP (tos 0x0, ttl 64, id 44720, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 &amp;gt; 116.62.25.128.http: Flags [F.], cksum 0x8691 (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5968362 ecr 3844393567], length 0
16:31:21.737935 IP (tos 0x0, ttl 64, id 44721, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 &amp;gt; 116.62.25.128.http: Flags [F.], cksum 0x85bd (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5968574 ecr 3844393567], length 0
16:31:23.437992 IP (tos 0x0, ttl 64, id 44722, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 &amp;gt; 116.62.25.128.http: Flags [F.], cksum 0x8414 (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5968999 ecr 3844393567], length 0
16:31:26.833967 IP (tos 0x0, ttl 64, id 44723, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 &amp;gt; 116.62.25.128.http: Flags [F.], cksum 0x80c3 (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5969848 ecr 3844393567], length 0
16:31:33.633977 IP (tos 0x0, ttl 64, id 44724, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 &amp;gt; 116.62.25.128.http: Flags [F.], cksum 0x7a1f (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5971548 ecr 3844393567], length 0
16:31:47.218007 IP (tos 0x0, ttl 64, id 44725, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 &amp;gt; 116.62.25.128.http: Flags [F.], cksum 0x6cdb (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5974944 ecr 3844393567], length 0
16:32:14.417998 IP (tos 0x0, ttl 64, id 44726, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44664 &amp;gt; 116.62.25.128.http: Flags [F.], cksum 0x524b (correct), seq 54, ack 1507, win 256, options [nop,nop,TS val 5981744 ecr 3844393567], length 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些报文都是我的机器主动发送的关闭报文，但是服务器也不再响应&lt;/p&gt;
&lt;h3&gt;案例二&lt;/h3&gt;
&lt;p&gt;完整的使用curl_init和curl_close，显式关闭连接&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$context = curl_init();
curl_setopt($context,CURLOPT_URL,&quot;http://www.izuqun.com&quot;);
curl_exec($context);
curl_close($context);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报文如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    192.168.1.107.44684 &amp;gt; 116.62.25.128.http: Flags [S], cksum 0x469f (correct), seq 2378041097, win 29200, options [mss 1460,sackOK,TS val 6028159 ecr 0,nop,wscale 7], length 0
16:35:20.090006 IP (tos 0x20, ttl 52, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    116.62.25.128.http &amp;gt; 192.168.1.107.44684: Flags [S.], cksum 0xd6a7 (correct), seq 3548165201, ack 2378041098, win 28960, options [mss 1460,sackOK,TS val 3844473825 ecr 6028159,nop,wscale 7], length 0
16:35:20.090050 IP (tos 0x0, ttl 64, id 64631, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44684 &amp;gt; 116.62.25.128.http: Flags [.], cksum 0x75ac (correct), ack 1, win 229, options [nop,nop,TS val 6028162 ecr 3844473825], length 0
16:35:20.090105 IP (tos 0x0, ttl 64, id 64632, offset 0, flags [DF], proto TCP (6), length 105)
    192.168.1.107.44684 &amp;gt; 116.62.25.128.http: Flags [P.], cksum 0x0853 (correct), seq 1:54, ack 1, win 229, options [nop,nop,TS val 6028162 ecr 3844473825], length 53: HTTP, length: 53
        GET / HTTP/1.1
        Host: www.izuqun.com
        Accept: */*

16:35:20.099626 IP (tos 0x20, ttl 52, id 47030, offset 0, flags [DF], proto TCP (6), length 52)
    116.62.25.128.http &amp;gt; 192.168.1.107.44684: Flags [.], cksum 0x7576 (correct), ack 54, win 227, options [nop,nop,TS val 3844473828 ecr 6028162], length 0
16:35:20.099655 IP (tos 0x20, ttl 52, id 47031, offset 0, flags [DF], proto TCP (6), length 314)
    116.62.25.128.http &amp;gt; 192.168.1.107.44684: Flags [P.], cksum 0xda41 (correct), seq 1:263, ack 54, win 227, options [nop,nop,TS val 3844473828 ecr 6028162], length 262: HTTP, length: 262
        HTTP/1.1 200 OK
        Server: nginx/1.12.2
        Date: Mon, 11 Dec 2017 08:35:20 GMT
        Content-Type: text/html
        Content-Length: 1243
        Last-Modified: Mon, 11 Dec 2017 02:35:33 GMT
        Connection: keep-alive
        Vary: Accept-Encoding
        ETag: &quot;5a2deef5-4db&quot;
        Accept-Ranges: bytes

16:35:20.099670 IP (tos 0x0, ttl 64, id 64633, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44684 &amp;gt; 116.62.25.128.http: Flags [.], cksum 0x7464 (correct), ack 263, win 237, options [nop,nop,TS val 6028164 ecr 3844473828], length 0
16:35:20.099680 IP (tos 0x20, ttl 52, id 47032, offset 0, flags [DF], proto TCP (6), length 1295)
    116.62.25.128.http &amp;gt; 192.168.1.107.44684: Flags [P.], cksum 0xa0fc (correct), seq 263:1506, ack 54, win 227, options [nop,nop,TS val 3844473828 ecr 6028162], length 1243: HTTP
16:35:20.099686 IP (tos 0x0, ttl 64, id 64634, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44684 &amp;gt; 116.62.25.128.http: Flags [.], cksum 0x6f76 (correct), ack 1506, win 256, options [nop,nop,TS val 6028164 ecr 3844473828], length 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这边的报文一开始和案例一完全一致，但是在我使用curl_close()显式关闭curl后，并没有发送任何关闭tcp连接的报文。查了一下资料，在php7.0中，如果$context任何有全局引用，即使使用curl_close()显式关闭，也不会关闭连接。&lt;/p&gt;
&lt;h3&gt;案例3&lt;/h3&gt;
&lt;p&gt;完整的使用curl_init和curl_close，显式关闭连接，并且多次执行exec&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;16:38:26.590098 IP (tos 0x0, ttl 64, id 18826, offset 0, flags [DF], proto TCP (6), length 60)
    192.168.1.107.44714 &amp;gt; 116.62.25.128.http: Flags [S], cksum 0x520f (correct), seq 3672832041, win 29200, options [mss 1460,sackOK,TS val 6074787 ecr 0,nop,wscale 7], length 0
16:38:26.604823 IP (tos 0x20, ttl 52, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    116.62.25.128.http &amp;gt; 192.168.1.107.44714: Flags [S.], cksum 0xf795 (correct), seq 3884635295, ack 3672832042, win 28960, options [mss 1460,sackOK,TS val 3844520454 ecr 6074787,nop,wscale 7], length 0
16:38:26.604870 IP (tos 0x0, ttl 64, id 18827, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44714 &amp;gt; 116.62.25.128.http: Flags [.], cksum 0x969a (correct), ack 1, win 229, options [nop,nop,TS val 6074790 ecr 3844520454], length 0
16:38:26.604904 IP (tos 0x0, ttl 64, id 18828, offset 0, flags [DF], proto TCP (6), length 105)
    192.168.1.107.44714 &amp;gt; 116.62.25.128.http: Flags [P.], cksum 0x2941 (correct), seq 1:54, ack 1, win 229, options [nop,nop,TS val 6074790 ecr 3844520454], length 53: HTTP, length: 53
        GET / HTTP/1.1
        Host: www.izuqun.com
        Accept: */*

16:38:26.616651 IP (tos 0x20, ttl 52, id 28329, offset 0, flags [DF], proto TCP (6), length 52)
    116.62.25.128.http &amp;gt; 192.168.1.107.44714: Flags [.], cksum 0x9664 (correct), ack 54, win 227, options [nop,nop,TS val 3844520457 ecr 6074790], length 0
16:38:26.616674 IP (tos 0x20, ttl 52, id 28330, offset 0, flags [DF], proto TCP (6), length 314)
    116.62.25.128.http &amp;gt; 192.168.1.107.44714: Flags [P.], cksum 0xf829 (correct), seq 1:263, ack 54, win 227, options [nop,nop,TS val 3844520457 ecr 6074790], length 262: HTTP, length: 262
        HTTP/1.1 200 OK
        Server: nginx/1.12.2
        Date: Mon, 11 Dec 2017 08:38:26 GMT
        Content-Type: text/html
        Content-Length: 1243
        Last-Modified: Mon, 11 Dec 2017 02:35:33 GMT
        Connection: keep-alive
        Vary: Accept-Encoding
        ETag: &quot;5a2deef5-4db&quot;
        Accept-Ranges: bytes

16:38:26.616687 IP (tos 0x0, ttl 64, id 18829, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44714 &amp;gt; 116.62.25.128.http: Flags [.], cksum 0x9551 (correct), ack 263, win 237, options [nop,nop,TS val 6074793 ecr 3844520457], length 0
16:38:26.616701 IP (tos 0x20, ttl 52, id 28331, offset 0, flags [DF], proto TCP (6), length 1295)
    116.62.25.128.http &amp;gt; 192.168.1.107.44714: Flags [P.], cksum 0xc1ea (correct), seq 263:1506, ack 54, win 227, options [nop,nop,TS val 3844520457 ecr 6074790], length 1243: HTTP
16:38:26.616708 IP (tos 0x0, ttl 64, id 18830, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44714 &amp;gt; 116.62.25.128.http: Flags [.], cksum 0x9063 (correct), ack 1506, win 256, options [nop,nop,TS val 6074793 ecr 3844520457], length 0
16:38:28.241841 IP (tos 0x0, ttl 64, id 18831, offset 0, flags [DF], proto TCP (6), length 105)
    192.168.1.107.44714 &amp;gt; 116.62.25.128.http: Flags [P.], cksum 0x2174 (correct), seq 54:107, ack 1506, win 256, options [nop,nop,TS val 6075199 ecr 3844520457], length 53: HTTP, length: 53
        GET / HTTP/1.1
        Host: www.izuqun.com
        Accept: */*

16:38:28.253636 IP (tos 0x20, ttl 52, id 28332, offset 0, flags [DF], proto TCP (6), length 314)
    116.62.25.128.http &amp;gt; 192.168.1.107.44714: Flags [P.], cksum 0xeede (correct), seq 1506:1768, ack 107, win 227, options [nop,nop,TS val 3844520867 ecr 6075199], length 262: HTTP, length: 262
        HTTP/1.1 200 OK
        Server: nginx/1.12.2
        Date: Mon, 11 Dec 2017 08:38:28 GMT
        Content-Type: text/html
        Content-Length: 1243
        Last-Modified: Mon, 11 Dec 2017 02:35:33 GMT
        Connection: keep-alive
        Vary: Accept-Encoding
        ETag: &quot;5a2deef5-4db&quot;
        Accept-Ranges: bytes

16:38:28.253689 IP (tos 0x0, ttl 64, id 18832, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44714 &amp;gt; 116.62.25.128.http: Flags [.], cksum 0x8be1 (correct), ack 1768, win 276, options [nop,nop,TS val 6075202 ecr 3844520867], length 0
16:38:28.253704 IP (tos 0x20, ttl 52, id 28333, offset 0, flags [DF], proto TCP (6), length 1295)
    116.62.25.128.http &amp;gt; 192.168.1.107.44714: Flags [P.], cksum 0xb8a1 (correct), seq 1768:3011, ack 107, win 227, options [nop,nop,TS val 3844520867 ecr 6075199], length 1243: HTTP
16:38:28.253713 IP (tos 0x0, ttl 64, id 18833, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.107.44714 &amp;gt; 116.62.25.128.http: Flags [.], cksum 0x86f3 (correct), ack 3011, win 295, options [nop,nop,TS val 6075202 ecr 3844520867], length 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多次curl_exec()会复用之前的连接，但是curl_close()依然是不能关闭连接的。但当我使用unset($context)时，观察到了以下报文：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    192.168.1.107.49938 &amp;gt; 116.62.25.128.http: Flags [F.], cksum 0x8407 (correct), seq 54, ack 1506, win 256, options [nop,nop,TS val 274723 ecr 3860448314], length 0                                                                                                      
10:20:58.408173 IP (tos 0x20, ttl 52, id 25537, offset 0, flags [DF], proto TCP (6), length 52)                                      
    116.62.25.128.http &amp;gt; 192.168.1.107.49938: Flags [F.], cksum 0x5ca5 (correct), seq 1506, ack 55, win 227, options [nop,nop,TS val 3860458424 ecr 274723], length 0                                                                                                      
10:20:58.408218 IP (tos 0x0, ttl 64, id 34603, offset 0, flags [DF], proto TCP (6), length 52)                                       
    192.168.1.107.49938 &amp;gt; 116.62.25.128.http: Flags [.], cksum 0x5c85 (correct), ack 1507, win 256, options [nop,nop,TS val 274726 ecr 3860458424], length 0 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我的机器主动关闭连接发出Fin，服务器ack并发送Fin,我的机器也ack。两边顺利的完成了连接的关闭。 所以当前可以得出以下几点: 1.&lt;code&gt;$context = curl_init()&lt;/code&gt;, &lt;code&gt;$context&lt;/code&gt;是可以复用的，复用$context后，多次执行curl_exec()可以避免tcp的3次握手和4次断开的过程。 2.curl_close()在上面的试验中，并不能正常的关闭连接，这是因为在psysh的执行环境中，&lt;code&gt;$context&lt;/code&gt;一直是在当前的作用域，会一直保持着变量的引用。而php7.0中，&lt;code&gt;$context&lt;/code&gt;如果保持引用，curl_close则不会关闭连接，而unset($context)则会关闭。 3.服务器主动关闭连接后发出Fin，我的机器虽然响应了ack，但是没有主动发送Fin，这会不会导致服务器大量的fin_wait2？ 第三点是很重要的，如果不解决会大量占用服务器资源。 所以再做一个实验，实验过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我的机器初始化大量的curl_init()，然后不释放，等待服务器关闭。使用netstat查看服务器是否会出现大量的fin_wait2。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用&lt;code&gt;netstat -an|awk &apos;/tcp/ {print $6}&apos;|sort|uniq -c&lt;/code&gt;可以快速查看各种tcp连接状态的统计。 初始情况为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;20 CLOSE_WAIT
63 ESTABLISHED                                                                              
10 LISTEN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;psysh中执行以下脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$contextArr = array();
for($i = 0; $i &amp;lt; 1000; $i++){
    $contextArr[$i] = curl_init();
    curl_setopt($contextArr[$i],CURLOPT_URL,&quot;http://www.izuqun.com&quot;);
    curl_exec($contextArr[$i]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;脚本执行完后，服务器状态变化如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;20 CLOSE_WAIT
1058 ESTABLISHED
5 FIN_WAIT2
10 LISTEN
2 TIME_WAIT
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;20 CLOSE_WAIT
58 ESTABLISHED
1001 FIN_WAIT2
10 LISTEN

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;20 CLOSE_WAIT
63 ESTABLISHED
10 LISTEN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，中间有1000个FIN_WAIT2状态，说明我们的猜想是正确的。&lt;/p&gt;
</content:encoded><category>编程相关</category><author>joyme123</author></item><item><title>Swoole Server架构分析</title><link>https://www.myway5.com/blog/swoole/</link><guid isPermaLink="true">https://www.myway5.com/blog/swoole/</guid><description>首页这里引用一下swoole的官方介绍: swoole:面向生产环境的 PHP 异步网络通信引擎 使 PHP 开发人员可以编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP，WebSocket 服务。</description><pubDate>Fri, 08 Dec 2017 06:44:28 GMT</pubDate><content:encoded>&lt;h2&gt;一.简介&lt;/h2&gt;
&lt;p&gt;首页这里引用一下swoole的官方介绍:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;swoole:面向生产环境的 PHP 异步网络通信引擎 使 PHP 开发人员可以编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP，WebSocket 服务。Swoole 可以广泛应用于互联网、移动通信、企业软件、云计算、网络游戏、物联网（IOT）、车联网、智能家居等领域。 使用 PHP + Swoole 作为网络通信框架，可以使企业 IT 研发团队的效率大大提升，更加专注于开发创新产品。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过上述的介绍,我们是可以得出几点信息的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.swoole可以投入生产环境&lt;/li&gt;
&lt;li&gt;2.使用php编写&lt;/li&gt;
&lt;li&gt;3.异步网络通信引擎,支持大量的网络协议,并且具有很高的网络性能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;swoole server与传统的php运行模式是完全不同的,它是常驻内存的,省去了大量的php脚本的初始化。&lt;/p&gt;
&lt;h2&gt;二.swoole server的是怎么运行的&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/uploads/wp/2017/12/process.jpg&quot; alt=&quot;swoole进程/线程模型&quot; /&gt; 这是一张官方的swoole运行时进程/线程模型。&lt;/p&gt;
&lt;h3&gt;1.Master进程&lt;/h3&gt;
&lt;p&gt;Master进程是一个多线程模型,其中包括Master线程,Reactor线程组,心跳检测线程,UDP收包线程。 以http server为例,Master线程负责监听(listen)端口,然后接受(accept)新的连接,然后将这个连接分配给一个Reactor线程,由这个Reactor线程监听此连接,一旦此连接可读时,读取数据,解析协议,然后将请求投递到worker进程中去执行。 Master进程是使用select/poll进行IO事件循环的,这是因为Master进程中的文件描述符只有几个(listenfd等),Reactor线程使用的是epoll,因为Reactor线程中会监听大量连接的可读事件,使用epoll可以支持大量的文件描述符。&lt;/p&gt;
&lt;h3&gt;2.Manager进程&lt;/h3&gt;
&lt;p&gt;Manager进程是专门用来管理Worker进程组和Task进程组的。它会Fork出指定数量的Worker进程和Task进程，并且有以下职能:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;子进程结束运行时，manager进程负责回收此子进程，避免成为僵尸进程。并创建新的子进程&lt;/li&gt;
&lt;li&gt;服务器关闭时，manager进程将发送信号给所有子进程，通知子进程关闭服务&lt;/li&gt;
&lt;li&gt;服务器reload时，manager进程会逐个关闭/重启子进程&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.Worker进程和Task进程&lt;/h3&gt;
&lt;p&gt;Worker进程接收Reactor线程投递过来的数据，执行php代码，然后生成数据交给Reactor线程，由Reactor线程通过tcp将数据返回给客户端。（如果是UDP，Worker进程直接将数据发送给客户端）。 Worker进程中执行的php代码和我们平时写php是一样的，它等同于php-fpm。但是众所周知，php-fpm下，php在处理异步操作时是很无力的，swoole提供的Task进程可以很好的解决这个问题。Worker进程可以将一些异步任务投递给Task进程，然后直接返回，处理其他的由Reactor线程投递过来的事件。 Task进程以完全同步阻塞的方式运行，一个Task进程在执行任务期间，是不接受从Worker进程投递的任务的，当Task进程执行完任务后，会异步地通知worker进程告诉它此任务已经完成。 所以介绍完上述的一些概念后，再引用一张官方的swoole执行流程图。 &lt;img src=&quot;/uploads/wp/2017/12/swoole.jpg&quot; alt=&quot;swoole运行流程图&quot; /&gt; 这里需要注意，在文档上说的是：Workder进程组和Task进程组是由Manager进程Fork出来的，但是流程图上画的是在启动服务器时Fork出主进程和Worker进程组以及Tasker进程组。&lt;/p&gt;
&lt;h2&gt;三、使用swoole和传统php开发的优缺点&lt;/h2&gt;
&lt;p&gt;在说这个话题之前，需要先了解一下CGI,FASTCGI。 1.CGI CGI的全称是Common Gateway Interface,通用网关接口，它使得任何一个拥有标准输入输出的程序拥有提供web server的能力。假设我们写了一个Hello World的c++程序,这个程序接受输入{text},输出{text},Hello World。 以nginx作为接受http请求为例，nginx接受一个http请求，Fork出一个进程，将http请求带来的text参数作为输入，执行完hello world程序，将输出{text},Hello World作为输出，销毁这个Fork出来的进程，由nginx返回给客户端。 这种方式虽然简单，但是要不断的Fork进程，销毁进程。 2.FASTCGI FASTCGI，顾名思义，它是CGI的改进版，是一个常驻型的CGI服务。我们常用的php-fpm就是这种模式运行的，php-fpm负责Forl多个进程，每个进程中都运行了php的解释器。可以在终端下看一下php-fpm的进程： &lt;img src=&quot;/uploads/wp/2017/12/Screenshot_20171208_140949-1.png&quot; alt=&quot;php-fpm进程&quot; /&gt; 一个php-fpm主进程,pid是1263,Fork出了3个子进程。在nginx+php-fpm的组合中，nginx负责接受http请求，将请求封装好交给php-fpm，php-fpm将请求按照一定的规则交给一个子进程去执行，这个子进程中的php解释器加载php代码运行。也是因为这个原因，传统的php只能作为web server。 然后我们发现，nginx+php-fpm的组合和我们Reactor+Worker子进程的运行方式非常相似。 3.swoole的运行方式 这里以swoole作为http server为例（传统php几乎都是作为web服务）。 首先swoole是实现了http server的，也就是说不需要nginx作为http服务器了，当然swoole并不是为了取代nginx，实际上swoole当前实现的http server功能有限，比如说只支持Get和Post，所有往往swoole前面还要运行一个nginx来作为前端代理服务器。 其次，swoole是内存常驻的。和php-fpm的常驻服务不同，php-fpm中常驻的是php的解释器，这个解释器会重复加载php代码，初始化环境，而swoole只在启动的时候加载，这样一来，性能就自然而然的提高了。这一点可以在开发中很明显的体现出来，php-fpm下，修改的php代码会即时生效，而使用swoole则需要重启swoole的server才能使代码生效。 通过上面的一些说明，就可以很明显的得出swoole和传统php开发的优缺点了。 swoole server优点： - swoole性能更高 - 可以做为tcp,udp服务器 - 在高io高并发的服务器要求下，swoole的运行模式是完全可以胜任的 swoole server缺点： - 更难上手。这要求开发人员对于多进程的运行模式有更清晰的认识 - 更容易内存泄露。在处理全局变量，静态变量的时候一定要小心，这种不会被GC清理的变量会存在整个生命周期中，如果没有正确的处理，很容易消耗完所有的内存。而以往的php-fpm下，php代码执行完内存就会被完全释放。 - 无法做密集计算。当然这一点是php甚至是所有动态语言都存在的问题。写在这里是因为防止误导读者以为使用swoole后，php可以用来做密集计算。&lt;/p&gt;
</content:encoded><category>linux</category><category>php</category><author>joyme123</author></item><item><title>png格式分析与压缩原理</title><link>https://www.myway5.com/blog/png/</link><guid isPermaLink="true">https://www.myway5.com/blog/png/</guid><description>png图片格式采用了一种很灵活的编码设计，支持多种编码方式来展示一张图片：灰度图片、索引彩色图像、真彩色图像、带α通道数据的灰度图像、带α通道数据的真彩色图像。因为png的编码方式灵活，使得其压缩的发挥空间非常大。 2.几种编码方式的说明</description><pubDate>Fri, 10 Nov 2017 07:54:10 GMT</pubDate><content:encoded>&lt;h2&gt;1.简介&lt;/h2&gt;
&lt;p&gt;　png图片格式采用了一种很灵活的编码设计，支持多种编码方式来展示一张图片：灰度图片、索引彩色图像、真彩色图像、带α通道数据的灰度图像、带α通道数据的真彩色图像。因为png的编码方式灵活，使得其压缩的发挥空间非常大。&lt;/p&gt;
&lt;h2&gt;2.几种编码方式的说明&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;注意：这里的一个通道并不等同于1个字节或2个字节，因为png IDAT块的编码单位是bit而不是byte，具体一个通道占用的位数是靠bit depth来决定的&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;a.灰度图片&lt;/h3&gt;
&lt;p&gt;灰度图片一般是用来生成我们常见的黑白照片，&lt;code&gt;bit depth可选值为1、 2、 4、 8、16&lt;/code&gt;，其一个像素点用一个通道来表示（0~255）或(0~65535)，因此这种图片的大小往往非常小&lt;/p&gt;
&lt;h3&gt;b.索引彩色图像&lt;/h3&gt;
&lt;p&gt;索引彩色图像很有意思，它可以和真彩色图像一样拥有表达彩色图片的能力，但是一张索引彩色图像上最多只有256种颜色，每种颜色用三个字节(RGB,0~255)表示。&lt;code&gt;它的bit depth可选值为1、 2、4、8&lt;/code&gt;，它有一个调色板区域，用来记录索引颜色的值，然后像素点数据区域用一个通道(0~255)来表示调色板区域记录的颜色索引。所以对于色彩不多的图片来说，采用索引彩色来编码是很适合的。&lt;/p&gt;
&lt;h3&gt;c.真彩色图像&lt;/h3&gt;
&lt;p&gt;真彩色图像最好理解，&lt;code&gt;它的bit depth为8或16&lt;/code&gt;，它的每个像素点都用3个字节(3个通道）来表示，分别为R(0~255),G(0~255),B(0~255)，可以表达256_256_256=16777216种颜色，或者使用6个字节（3个通道）来表示，分别为R(0~65535),G(0~65535),B(0~65535)，可以表达65535_65535_65535种颜色。&lt;/p&gt;
&lt;h3&gt;d.带α通道数据的灰度图像&lt;/h3&gt;
&lt;p&gt;就是在灰度图片上增加了α通道，共2个通道,支持透明(0~255)或者(0~65535)，&lt;code&gt;但是它的bit depth为8或16&lt;/code&gt;，因此这种编码方式一个像素点是2个字节或4个字节&lt;/p&gt;
&lt;h3&gt;e.带α通道数据的真彩色图像&lt;/h3&gt;
&lt;p&gt;就是在真彩色图像上增加了α通道,同四个通道，支持透明(0~255)或者(0~65535)，&lt;code&gt;它的bit depth为8或16&lt;/code&gt;，因此这种编码方式一个像素点是4个字节或8个字节&lt;/p&gt;
&lt;h2&gt;3.png图片具体编码说明&lt;/h2&gt;
&lt;p&gt;png图片的编码一般如下:header,chunk,chunk,chunk....chunk。 &lt;code&gt;header&lt;/code&gt;代表png图片的头，png文件头一般都是由固定的8个字节组成 &lt;code&gt;89 50 4E 47 OD 0A 1A 0A&lt;/code&gt; 图片软件可以通过这8个字节来判断这个文件是不是png格式。 &lt;code&gt;chunk&lt;/code&gt;代表png图片的数据块，数据块是有不同的类型的，记录了不同的信息。一张png图片可能包含多个数据块。 数据块的类型如下（关键部分高亮显示）: &lt;strong&gt;PNG文件格式中的数据块（表1.1）&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;数据块符号&lt;/th&gt;
&lt;th&gt;数据块名称&lt;/th&gt;
&lt;th&gt;多数据块&lt;/th&gt;
&lt;th&gt;可选否&lt;/th&gt;
&lt;th&gt;位置限制&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IHDR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文件头数据块&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;第一块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cHRM&lt;/td&gt;
&lt;td&gt;基色和白色点数据块&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;在PLTE和IDAT之前&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gAMA&lt;/td&gt;
&lt;td&gt;图像γ数据块&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;在PLTE和IDAT之前&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sBIT&lt;/td&gt;
&lt;td&gt;样本有效位数据块&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;在PLTE和IDAT之前&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PLTE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;调色板数据块&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;在IDAT之前&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bKGD&lt;/td&gt;
&lt;td&gt;背景颜色数据块&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;在PLTE之后IDAT之前&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hIST&lt;/td&gt;
&lt;td&gt;图像直方图数据块&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;在PLTE之后IDAT之前&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tRNS&lt;/td&gt;
&lt;td&gt;图像透明数据块&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;在PLTE之后IDAT之前&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;oFFs&lt;/td&gt;
&lt;td&gt;(专用公共数据块)&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;在IDAT之前&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pHYs&lt;/td&gt;
&lt;td&gt;物理像素尺寸数据块&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;在IDAT之前&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sCAL&lt;/td&gt;
&lt;td&gt;(专用公共数据块)&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;在IDAT之前&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IDAT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;图像数据块&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;与其他IDAT连续&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tIME&lt;/td&gt;
&lt;td&gt;图像最后修改时间数据块&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;无限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tEXt&lt;/td&gt;
&lt;td&gt;文本信息数据块&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;无限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zTXt&lt;/td&gt;
&lt;td&gt;压缩文本数据块&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;无限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fRAc&lt;/td&gt;
&lt;td&gt;(专用公共数据块)&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;无限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gIFg&lt;/td&gt;
&lt;td&gt;(专用公共数据块)&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;无限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gIFt&lt;/td&gt;
&lt;td&gt;(专用公共数据块)&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;无限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gIFx&lt;/td&gt;
&lt;td&gt;(专用公共数据块)&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;无限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IEND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;图像结束数据&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;最后一个数据块&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;png 文件中，每个数据块由4个部分组成 length | type(name) | data | CRC, 说明如下 length: 4 bytes， data的长度，不包括type和CRC type: 4 bytes, ASCII码([A-Z,a-z]) CRC: 4bytes CRC(cyclic redundancy check)域中的值是对Chunk Type Code域和Chunk Data域中的数据进行计算得到的。CRC具体算法定义在ISO 3309和ITU-T V.42中，其值按下面的CRC码生成多项式进行计算： &lt;code&gt;x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1&lt;/code&gt; 除了高亮的关键数据块，还有下面比较有意思的数据块 &lt;strong&gt;tRNS&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;tRNS contains transparency information. For indexed images, it stores alpha channel values for one or more palette entries. For truecolor and grayscale images, it stores a single pixel value that is to be regarded as fully transparent.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;tRNS包含透明度信息， 1.在索引图片中，它可以保存一或多个调色板像素对的alpha通道值。用来指明哪些像素需要透明，透明度是多少。然后剩下的像素透明度都被默认成255，完全不透明。 2.在灰度图片中，包含单个灰度级别值，格式如下： Gray: 2 bytes, range 0 .. (2^bitdepth)-1 被指定的灰度值都会被当成是透明的，其他灰度值则完全不透明（如果bit depth小于16，则取最低有效位，其他位为0） 3.在真彩色图片中，包含单个RGB颜色值，格式如下： Red: 2 bytes, range 0 .. (2^bitdepth)-1 Green: 2 bytes, range 0 .. (2^bitdepth)-1 Blue: 2 bytes, range 0 .. (2^bitdepth)-1 被指定的颜色值都会被当成是透明的，其他颜色值则完全不透明（如果bit depth小于16，则取最低有效位，其他位为0） 在带alpha通道的图片中，tRNS是被禁止使用的。&lt;/p&gt;
&lt;h3&gt;a.IHDR数据块&lt;/h3&gt;
&lt;p&gt;IHDR数据块存储了png图像的基本信息，一个图像中只能有一个IHDR数据块，并且也是png图像的第一个数据块。共有13个字节。记录的信息如下： &lt;strong&gt;表1.2&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;域的名称&lt;/th&gt;
&lt;th&gt;字节数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Width&lt;/td&gt;
&lt;td&gt;4 bytes&lt;/td&gt;
&lt;td&gt;图像宽度，以像素为单位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Height&lt;/td&gt;
&lt;td&gt;4 bytes&lt;/td&gt;
&lt;td&gt;图像高度，以像素为单位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bit depth&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;图像深度.&lt;code&gt;索引彩色图像： 1，2，4或8 ,&lt;/code&gt; &lt;code&gt;灰度图像： 1，2，4，8或16&lt;/code&gt; &lt;code&gt;真彩色图像： 8或16&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ColorType&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;颜色类型. &lt;code&gt;[0]：灰度图像, 1，2，4，8或16&lt;/code&gt; &lt;code&gt;[2]：真彩色图像，8或16&lt;/code&gt; &lt;code&gt;[3]：索引彩色图像，1，2，4或8&lt;/code&gt; &lt;code&gt;[4]：带α通道数据的灰度图像，8或16&lt;/code&gt; &lt;code&gt;[6]：带α通道数据的真彩色图像，8或16&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression method&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;压缩方法(LZ77派生算法)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter method&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;滤波器方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interlace method&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;隔行扫描方法. &lt;code&gt;0：非隔行扫描&lt;/code&gt; &lt;code&gt;1： Adam7(由Adam M. Costello开发的7遍隔行扫描方法)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;从这个表中可以得出，一张png图片最大的宽高（4294967300 × 4294967300） 这里的Bit depth一开始很难理解，引用维基百科两段话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Pixels in PNG images are numbers that may be either indices of sample data in the palette or the sample data itself. The palette is a separate table contained in the PLTE chunk. Sample data for a single pixel consists of a tuple of between one and four numbers. Whether the pixel data represents palette indices or explicit sample values, the numbers are referred to as channels and every number in the image is encoded with an identical format. The permitted formats encode each number as an unsigned integral value using a fixed number of bits, referred to in the PNG specification as the bit depth. Notice that this is not the same as color depth, which is commonly used to refer to the total number of bits in each pixel, not each channel. The permitted bit depths are summarized in the table along with the total number of bits used for each pixel. The number of channels depends on whether the image is grayscale or color and whether it has an alpha channel. PNG allows the following combinations of channels, called the color type.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大意是：png图片的像素(Pixels)是调色板中样本数据的索引或者样本数据其本身，调色板是在PLTE数据块中的。单个像素的样本数据包含1~4个数字的元组。像素数据表示调色板索引或者明确的样本值，每个数字代表每个通道，每个数字都有唯一可识别的编码。 允许编码每个数字的格式是一个使用固定位数的unsigned整数值，在PNG中称为bit depth(位深)。位深和color depth(颜色深度)不同，颜色深度通常代表一个像素的总位数，不是每个通道的总位数。 通道数取决于这个图片是否是灰度图还是有alpha通道的色彩图 &lt;strong&gt;因此可以知道bit depth是一个通道的位数，bit depth在索引彩色图片和灰度图中使用，使得图片的一个像素点的大小可以小于一个字节&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;b.PLTE数据块（调色板数据块）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;a.PLTE数据块在索引彩色图像中&lt;code&gt;必须存在&lt;/code&gt;，这里记录了整张图片所有的颜色，最多为256个。这个是bit depth规定的，由表1.2可知，索引彩色图像的bit depth为1,2,4,8，共为1、4、16、256种颜色。每种颜色是3个字节（RGB)，所以PLTE数据块的length肯定是3的倍数，否则就是非法的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;b.PLTE数据块在真彩色图像或带α通道数据的真彩色图像中是&lt;code&gt;可选的&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;c.PLTE数据块在灰度图像或带α通道数据的灰度图像中是&lt;code&gt;不能存在的&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;c.IDAT数据块&lt;/h3&gt;
&lt;p&gt;这部分的数据块存放的就是图像的一个个像素（使用压缩算法之后生成的像素），一张图像中可以存在多个IDAT数据块，这种编码方式虽然稍微增加了图像的大小，但是可以使得PNG图像以一种流的方式生成。例如在浏览器中打开一张非常大的PNG图像，如果网络很慢，我们可以看到图片是从上到下一点点打开的，这是因为即使整张图片没有加载完，仍然可以显示局部内容。 IDAT数据块是最重要的一部分，这里的数据会被&lt;code&gt;隔行扫描方法(Interlace method)&lt;/code&gt;、&lt;code&gt;滤波器（filtering）&lt;/code&gt;和&lt;code&gt;压缩算法(compression)&lt;/code&gt;处理。这三个部分会在下面详细介绍。 并且要注意的是，IDAT数据块存储的数据并不是以字节为最小单位，在灰度图片和索引彩色图片中，一个字节可能会包含多个像素点，由bit depth指定。&lt;/p&gt;
&lt;h3&gt;d.IENT数据块&lt;/h3&gt;
&lt;p&gt;这段数据块标记PNG文件或者数据流已经结束，并且必须要放在文件的尾部。 正常情况下， png 文件的结尾为如下12个字符： 00 00 00 00 49 45 4E 44 AE 42 60 82 由于数据块结构的定义，IEND数据块的长度总是0（00 00 00 00，除非人为加入信息），数据标识总是IEND（49 45 4E 44），因此，CRC码也总是AE 42 60 82&lt;/p&gt;
&lt;h2&gt;4.隔行扫描方法&lt;/h2&gt;
&lt;p&gt;隔行扫描方法是为了图片被更先进的展示出来。在图片传输过程中，可以让图片的展示有淡入的效果。虽然平均下来稍微增加了存储的大小，但是给带来用户更快的显示效果。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;a.这个方法取值为0时，代表像素点是从左到右顺序存储，扫描线扫描时从上到下即可。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;b.这个方法取值为1时，被称为Adam7算法 Adam7分为7个扫描步骤，可见下图： &lt;img src=&quot;/uploads/wp/2017/11/2017-11-10-15-18-37%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;5.过滤器&lt;/h2&gt;
&lt;p&gt;过滤器中只有一种过滤方法，但是有多种过滤类型。过滤方法并不会影响数据的大小，也不会影响丢失任何信息，过滤的目的只有一个，为压缩方法提供更好压缩的数据，IDAT数据中的每一行第一个字节定义了过滤类型，过滤类型的介绍如下:&lt;/p&gt;
&lt;h3&gt;a.过滤类型0：None&lt;/h3&gt;
&lt;p&gt;也就是不做任何过滤&lt;/p&gt;
&lt;h3&gt;b. 过滤类型1：Sub&lt;/h3&gt;
&lt;p&gt;记录当前像素和左边像素的差值。左边起第一个像素是标准值，不做任何过滤&lt;/p&gt;
&lt;h3&gt;c. 过滤类型2：Up&lt;/h3&gt;
&lt;p&gt;记录X - B的值，即当前像素和上边像素点差值。如果当前行是第1行，则当前行数标准值，不做任何过滤。&lt;/p&gt;
&lt;h3&gt;d.过滤类型3：Average&lt;/h3&gt;
&lt;p&gt;记录当前像素与左边像素和上边像素的平均值的差值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果当前行数第一行：做特殊的Sub过滤，左边起第一个像素是标准值，不做任何过滤。其他像素记录该像素与左边像素的二分之一的值的差值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果当前行数不是第一行：左边起第一个像素记录该像素与上边像素的二分之一的值的差值，其他像素做正常的Average过滤。&lt;/p&gt;
&lt;h3&gt;e.过滤类型4：Paeth&lt;/h3&gt;
&lt;p&gt;记录X - Pr的值，这种过滤方式比较复杂，Pr的计算方式（伪代码）如下：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;p = a + b - c
pa = abs(p - a)
pb = abs(p - b)
pc = abs(p - c)
if pa &amp;lt;= pb and pa &amp;lt;= pc then Pr = a
else if pb &amp;lt;= pc then Pr = b
else Pr = c
return Pr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果当前行数第一行：做Sub过滤。 如果当前行数不是第一行：左边起第一个像素记录该像素与上边像素的差值，其他像素做正常的Peath过滤。&lt;/p&gt;
&lt;h2&gt;6.压缩算法&lt;/h2&gt;
&lt;p&gt;压缩算法当前只定义了一种，值为0，是使用一个滑动窗口的deflate/inflate压缩，这个算法可以调用zlib库来实现&lt;/p&gt;
&lt;h2&gt;7.关于压缩&lt;/h2&gt;
&lt;p&gt;对于一张图片，我们可以使用以上知识来完成一次无损压缩,以及去除所有的辅助数据块。但是很多场景下，常规的无损压缩并不能带来很高的压缩比，这个时候可以考虑使用有损压缩，大概的思路就是将总体颜色数减少到256以下（使用相近的颜色来代替），然后使用索引色彩来编码整张图片，带来很高的压缩比&lt;/p&gt;
&lt;h2&gt;实际操作验证&lt;/h2&gt;
&lt;p&gt;一级压缩图片（958.8k）：/uploads/wp/2017/11/output_1.png 九级压缩图片（857.0k）：/uploads/wp/2017/11/output_9.png 有损压缩图片（273.0k）：/uploads/wp/2017/11/test_tiny.png 原图（1.5m）：/uploads/wp/2017/11/test.png) 使用vim打开原图: &lt;img src=&quot;/uploads/wp/2017/11/2017-11-10-16-41-50%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt; &lt;code&gt;8950 4e47 0d0a 1a0a&lt;/code&gt;是png固定的头, &lt;code&gt;0000 000d&lt;/code&gt;是iHDR数据块的长度，为13，&lt;code&gt;4948 4452&lt;/code&gt;是数据块的type,为IHDR，之后紧跟着是data， &lt;code&gt;0000 02bc&lt;/code&gt;是图片的宽度,&lt;code&gt;0000 03a5&lt;/code&gt;是高度，&lt;code&gt;08&lt;/code&gt;是Bit depth，也就是一个通道是8位,&lt;code&gt;06&lt;/code&gt;是color type,这里表示图片是真彩色，&lt;code&gt;00&lt;/code&gt;是压缩方法，png中目前只有一种，也就是LZ77派生的算法，&lt;code&gt;00&lt;/code&gt;是滤波器方法，表示不使用，&lt;code&gt;00&lt;/code&gt;是隔行扫描方法，代表不扫描。&lt;code&gt;8f 1434 a4&lt;/code&gt;是四个字节的CRC校验码。 &lt;code&gt;00 0000 01&lt;/code&gt;是sRGB数据块的长度（???)，为1，&lt;code&gt;73 5247 42&lt;/code&gt;是type,为sRGB，&lt;code&gt;00&lt;/code&gt;是存储的一个字节数据，&lt;code&gt;aece 1ce9&lt;/code&gt;是循环校验码 之后还有一个iDoT数据块，再紧接着才是IDAT数据块，下面就不分析了。 再打开9级压缩的图片 &lt;img src=&quot;/uploads/wp/2017/11/2017-11-10-17-00-36%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt; 简单看一下，sRGB和iDOT数据块已经不存在了,IDAT数据块的长度发生了改变，其实就是被压缩了。这种压缩是无损的，9级压缩和1级压缩相差十几k的大小，但是压缩的越小，耗时也越长。 最后去看在tinypng上进行有损压缩的图片: &lt;img src=&quot;/uploads/wp/2017/11/2017-11-10-17-04-41%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt; 它多了一个PLTE字段，意味着这张图变成了索引彩色图片，索引彩色图片最关键的一点就是总颜色数不能超过256。如果这张图片的总颜色数没有256个，那么甚至可以说这次图片的压缩是无损的。但是一旦颜色数超过256，就需要将多个相近的颜色处理成一个颜色，也就是有损的压缩&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[1] png文件结构分析:&lt;a href=&quot;http://www.360doc.com/content/11/0428/12/1016783%5C_112894280.shtml&quot; rel=&quot;noopener&quot;&gt;http://www.360doc.com/content/11/0428/12/1016783\_112894280.shtml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[2] php imagecreatefrom* 系列函数之 png：&lt;a href=&quot;http://drops.xmd5.com/static/drops/tips-16034.html&quot; rel=&quot;noopener&quot;&gt;http://drops.xmd5.com/static/drops/tips-16034.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[3] Portable Network Graphics: &lt;a href=&quot;https://en.wikipedia.org/wiki/Portable%5C_Network%5C_Graphics&quot; rel=&quot;noopener&quot;&gt;https://en.wikipedia.org/wiki/Portable\_Network\_Graphics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[4] png的故事：获取图片信息和像素内容: &lt;a href=&quot;https://www.qcloud.com/community/article/864088&quot; rel=&quot;noopener&quot;&gt;https://www.qcloud.com/community/article/864088&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[5] &lt;a href=&quot;https://www.w3.org/TR/2003/REC-PNG-20031110/#figure48&quot; rel=&quot;noopener&quot;&gt;https://www.w3.org/TR/2003/REC-PNG-20031110/#figure48&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>工作</category><category>图像相关</category><author>joyme123</author></item><item><title>leveldb源代码阅读（四）- table cache的实现</title><link>https://www.myway5.com/blog/leveldb-4-table-cache/</link><guid isPermaLink="true">https://www.myway5.com/blog/leveldb-4-table-cache/</guid><description>缓存在整个计算机体系中，占据着举足轻重的地位，往往被用于提升软件的运行速度。在计算机系统中，最典型的当属CPU高速缓存了，CPU高速缓存是介于CPU寄存器和内存之间，CPU向内存中请求数据时，会先检查CPU高速缓存中是否存在数据，如果不存在，则会将内存中的数据放入高速缓存中，再…</description><pubDate>Sun, 20 Aug 2017 03:31:39 GMT</pubDate><content:encoded>&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;缓存在整个计算机体系中，占据着举足轻重的地位，往往被用于提升软件的运行速度。在计算机系统中，最典型的当属CPU高速缓存了，CPU高速缓存是介于CPU寄存器和内存之间，CPU向内存中请求数据时，会先检查CPU高速缓存中是否存在数据，如果不存在，则会将内存中的数据放入高速缓存中，再将高速缓存中的数据读入CPU。这个过程中，缓存之所以能够大大的提升系统速度，是因为程序在运行的时候对内存的访问具有局部性的特点，这种局部性我理解为程序在运行时对某一块的内存请求会非常频繁，而这一块内存在第一次请求之后就会被缓存，所以会大大提升之后的数据读取速度。&lt;em&gt;所以，缓存设计的是否合理有效，在于缓存的命中率高不高。&lt;/em&gt; 在leveldb中，为了提升对数据的检索速度，也设计了缓存，对应的代码在db/table_cache.h和db/table_cache.cc中，这个table cache的实现主要借助于Cache类，关于Cache类，是可以用户自定义实现的，但leveldb也有一个内置的Cache的实现，文件是util/cache.cc。主要是LRU（最近最少使用）算法，原理是“如果一个数据最近被使用，那么将来被使用的概率同样也很大”。同样也实现了一个HashTable，leveldb中提供的数据显示在g++ 4.4.3下比built-in的哈希表性能稍高，大概5%左右。&lt;/p&gt;
&lt;h2&gt;二、概览&lt;/h2&gt;
&lt;h3&gt;2.1 hash table数据结构&lt;/h3&gt;
&lt;p&gt;哈希表是一个很常见的结构了，存储的是key-value结构，一个key-value对常被称作entry，它最大的特点是查找快。在网上找了一张hash table的图 &lt;img src=&quot;/uploads/wp/2017/07/450px-Hash_table_5_0_1_1_1_1_1_LL.svg_.png&quot; alt=&quot;hash table&quot; /&gt; 首先它是一个长为len的数组，每个数组中的元素都是一个链表。如图所示，john Smith和Sandra Dee都被hash到152这个地方，所以在152这个地方使用链表来存储这两个entry.查找的时候Sandra Dee的时候，先找到152这个地方，在遍历链表，直到找到key是Sandra的entry。&lt;/p&gt;
&lt;h3&gt;2.2 LRU算法&lt;/h3&gt;
&lt;p&gt;在leveldb中,LRU算法的实现是使用两个双向环形链表，一个链表(in-use)存储当前正在使用的数据，另一个链表(lru)按照访问时间先后顺序存储缓存数据，每个数据都可以在in-use和lru之间切换。当我们需要使用LRU算法来淘汰数据时，只需要在lru上淘汰排序靠后的数据即可。&lt;/p&gt;
&lt;h3&gt;2.3 分片LRU缓存&lt;/h3&gt;
&lt;p&gt;分片LRU缓存很简单，其实就是同时创建多个LRU缓存对象，然后使用hash将特定的缓存数据放置到相应的LRU缓存对象中。这个方式可以避免一个LRU缓存中存储过多的数据。&lt;/p&gt;
&lt;h2&gt;三、详细分析&lt;/h2&gt;
&lt;h3&gt;3.1 Hash Table&lt;/h3&gt;
&lt;p&gt;leveldb中实现了HashTable，使用的是最经典的数组+链表的实现。通过hash值定位到数组中的某一处，然后在这一处的链表上遍历查找。 HashTable的长度是哈希算法效率的最大影响因素，如果存在较多的hash冲突，则在链表上遍历查找的时间花费会很长。所以这个长度的选择是很重要的，比如在Java中的HashMap,实现中有一个负载因子0.7，如果数组上已经有值的数量超过总长度的0.7就会对整个哈希表resize。在leveldb中，这个resize的阈值是当前所有Insert进来的元素个数超过了数组的长度length，就会进行resize。这里简单的分析一下最重要的查找和resize过程。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哈希表查找过程&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;  // Return a pointer to slot that points to a cache entry that
  // matches key/hash.  If there is no such cache entry, return a
  // pointer to the trailing slot in the corresponding linked list.
  LRUHandle** FindPointer(const Slice&amp;amp; key, uint32_t hash) {
    LRUHandle** ptr = &amp;amp;list_[hash &amp;amp; (length_ - 1)];     //这里hash &amp;amp; (length_ - 1)会定位到小于length的位置,ptr就是hash到的位置
    while (*ptr != NULL &amp;amp;&amp;amp;
           ((*ptr)-&amp;gt;hash != hash || key != (*ptr)-&amp;gt;key())) {
           //hash到特定位置后，如果当前位置的hash和当前hash不一样，或者key不一样，并且指针也不为空，则继续向下找，直到找到
      ptr = &amp;amp;(*ptr)-&amp;gt;next_hash;
    }
    return ptr;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;哈希表resize的过程&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;   void Resize() {
    uint32_t new_length = 4;        //哈希表的长度从4开始，逐倍增长
    while (new_length &amp;lt; elems_) {
      new_length *= 2;
    }
    LRUHandle** new_list = new LRUHandle*[new_length];
    memset(new_list, 0, sizeof(new_list[0]) * new_length);      //申请新的哈希表的所需的空间
    uint32_t count = 0;
    for (uint32_t i = 0; i &amp;lt; length_; i++) {    //将旧的哈希表的数据重新计算复制到新的哈希表中
      LRUHandle* h = list_[i];
      while (h != NULL) {
        LRUHandle* next = h-&amp;gt;next_hash;
        uint32_t hash = h-&amp;gt;hash;
        LRUHandle** ptr = &amp;amp;new_list[hash &amp;amp; (new_length - 1)];
        h-&amp;gt;next_hash = *ptr;
        *ptr = h;
        h = next;
        count++;
      }
    }
    assert(elems_ == count);
    delete[] list_; //删除旧的哈希表
    list_ = new_list;
    length_ = new_length;
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 LRUCache的实现&lt;/h3&gt;
&lt;p&gt;这里的LRUCache的私有成员变量包括以下几个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // Initialized before use.
  size_t capacity_;         //LRUCache的大小。

  // mutex_ protects the following state.
  mutable port::Mutex mutex_;       //互斥变量，用来同步访问
  size_t usage_;                    //LRUCache已经使用的大小

  // Dummy head of LRU list.
  // lru.prev is newest entry, lru.next is oldest entry.
  // Entries have refs==1 and in_cache==true.
  LRUHandle lru_;                   //LRU链表的头，lru.prev代表新的节点，lru.next代表旧的节点，节点的refs == 1,in_cache == true

  // Dummy head of in-use list.
  // Entries are in use by clients, and have refs &amp;gt;= 2 and in_cache==true.
  LRUHandle in_use_;                //正在使用的节点链表的头，refs &amp;gt;= 2，in_cache == true

  HandleTable table_;               //哈希表，用来在缓存中实现快速查找
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LRU的构造函数，直接构造出空的环形链表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LRUCache::LRUCache()
    : usage_(0) {
  // Make empty circular linked lists.
  lru_.next = &amp;amp;lru_;
  lru_.prev = &amp;amp;lru_;
  in_use_.next = &amp;amp;in_use_;
  in_use_.prev = &amp;amp;in_use_;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LRU中节点的引用和解引用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//增加引用时，如果在缓存中，则移动到in_use中
void LRUCache::Ref(LRUHandle* e) {
  if (e-&amp;gt;refs == 1 &amp;amp;&amp;amp; e-&amp;gt;in_cache) {  // If on lru_ list, move to in_use_ list.
    LRU_Remove(e);
    LRU_Append(&amp;amp;in_use_, e);
  }
  e-&amp;gt;refs++;
}

//解引用时会出现两种情况。1.节点不再需要，使用deleter来删除节点 2.不再被使用，移动到缓存
void LRUCache::Unref(LRUHandle* e) {
  assert(e-&amp;gt;refs &amp;gt; 0);
  e-&amp;gt;refs--;
  if (e-&amp;gt;refs == 0) { // Deallocate.
    assert(!e-&amp;gt;in_cache);
    (*e-&amp;gt;deleter)(e-&amp;gt;key(), e-&amp;gt;value);
    free(e);
  } else if (e-&amp;gt;in_cache &amp;amp;&amp;amp; e-&amp;gt;refs == 1) {  // No longer in use; move to lru_ list.
    LRU_Remove(e);
    LRU_Append(&amp;amp;lru_, e);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缓存的插入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cache::Handle* LRUCache::Insert(
    const Slice&amp;amp; key, uint32_t hash, void* value, size_t charge,
    void (*deleter)(const Slice&amp;amp; key, void* value)) {
  MutexLock l(&amp;amp;mutex_);

  LRUHandle* e = reinterpret_cast&amp;lt;LRUHandle*&amp;gt;(
      malloc(sizeof(LRUHandle)-1 + key.size()));    //这个地方申请内存需要注意，在LRUHandle中，key_data只有1个字节，其实是整个key的开头一个字节，所以申请的空间实际上是包含整个key的
  e-&amp;gt;value = value;
  e-&amp;gt;deleter = deleter;
  e-&amp;gt;charge = charge;
  e-&amp;gt;key_length = key.size();
  e-&amp;gt;hash = hash;
  e-&amp;gt;in_cache = false;
  e-&amp;gt;refs = 1;  // for the returned handle.
  memcpy(e-&amp;gt;key_data, key.data(), key.size());

  if (capacity_ &amp;gt; 0) {      //如果capacity大于0，也就是需要进行缓存
    e-&amp;gt;refs++;  // for the cache&apos;s reference.
    e-&amp;gt;in_cache = true;
    LRU_Append(&amp;amp;in_use_, e);
    usage_ += charge;
    FinishErase(table_.Insert(e));
  } // else don&apos;t cache.  (Tests use capacity_==0 to turn off caching.)

    //当使用的内存大于容量时，则要移除旧的缓存，直到缓存小于指定的容量
  while (usage_ &amp;gt; capacity_ &amp;amp;&amp;amp; lru_.next != &amp;amp;lru_) {
    LRUHandle* old = lru_.next;
    assert(old-&amp;gt;refs == 1);
    bool erased = FinishErase(table_.Remove(old-&amp;gt;key(), old-&amp;gt;hash));
    if (!erased) {  // to avoid unused variable when compiled NDEBUG
      assert(erased);
    }
  }

  return reinterpret_cast&amp;lt;Cache::Handle*&amp;gt;(e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缓存的查找&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cache::Handle* LRUCache::Lookup(const Slice&amp;amp; key, uint32_t hash) {
  MutexLock l(&amp;amp;mutex_);
  LRUHandle* e = table_.Lookup(key, hash);      //直接借用哈希表完成缓存的快速查找
  if (e != NULL) {
    Ref(e);
  }
  return reinterpret_cast&amp;lt;Cache::Handle*&amp;gt;(e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 分片缓存的实现(ShardedLRUCache)&lt;/h3&gt;
&lt;p&gt;分片缓存的实现其实就是借助上面的LRUCaChe，只不过同时拥有多个LRUCache，然后特定的缓存数据会被缓存到相应的LRUCache中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static const int kNumShardBits = 4;                     //可以理解为缓存片数量的容量因子(这里是4个bits，所以共有16个缓存片）
static const int kNumShards = 1 &amp;lt;&amp;lt; kNumShardBits;       //一共有多少个缓存片

//通过Shard函数可以将任意一个hash分配到16个缓存片中的任意一个)
static uint32_t Shard(uint32_t hash) {
    return hash &amp;gt;&amp;gt; (32 - kNumShardBits);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有的缓存存储和读取都会通过上面的Shard函数来找到特定的缓存片，从而实现了分片缓存。&lt;/p&gt;
&lt;h2&gt;五、总结&lt;/h2&gt;
&lt;p&gt;在这个部分，400行左右的代码就实现了哈希表，LRU缓存，分片LRU缓存。了解到LRU缓存的具体实现方式，特别是LRU中Ref和UnRef的实现非常精炼，很好的解决了一个数据在LRU缓存中被引用时脱离缓存，不使用时进入LRU缓存的功能。同时将LRU缓存简单的封装，就可以实现分片LRU缓存。&lt;/p&gt;
</content:encoded><category>levelDB源码阅读</category><category>leveldb</category><category>cache</category><category>lru</category><category>hash</category><category>shardedLRUCache</category><author>joyme123</author></item><item><title>ubuntu上使用chrome进行手机页面调试的方案</title><link>https://www.myway5.com/blog/ubuntu-chrome-debug-mobile/</link><guid isPermaLink="true">https://www.myway5.com/blog/ubuntu-chrome-debug-mobile/</guid><description>虽然大多数时候我们都可以使用chrome的开发者工具，通过模拟手机来调试手机页面，但是对于一些特殊的动作是无法在电脑上做的，比如说手机的多点触控操作，在页面上模拟双指缩放时没法使用鼠标完成，因此需要通过手机来进行真机调试，但是调试的同时，我们希望可以看到调试信息，希望可以实时修…</description><pubDate>Mon, 31 Jul 2017 09:34:42 GMT</pubDate><content:encoded>&lt;p&gt;虽然大多数时候我们都可以使用chrome的开发者工具，通过模拟手机来调试手机页面，但是对于一些特殊的动作是无法在电脑上做的，比如说手机的多点触控操作，在页面上模拟双指缩放时没法使用鼠标完成，因此需要通过手机来进行真机调试，但是调试的同时，我们希望可以看到调试信息，希望可以实时修改查看。这个时候就可以借助chrome的远程设备使用手机chrome打开页面，在电脑chrome中调试。具体的调试过程在 &lt;a href=&quot;https://developers.google.com/web/tools/chrome-devtools/remote-debugging/?utm_source=dcc&amp;amp;utm_medium=redirect&amp;amp;utm_campaign=2016q3&quot; rel=&quot;noopener&quot;&gt;远程调试 Android 设备使用入门&lt;/a&gt;可以看到。下面只是记录ubuntu下使用该方案遇到的问题。&lt;/p&gt;
&lt;h2&gt;一.打开远程调试界面&lt;/h2&gt;
&lt;p&gt;打开chrome的控制台，点击右上角的&lt;code&gt;列表按钮&lt;/code&gt;，找到&lt;code&gt;more tool&lt;/code&gt;-&amp;gt;&lt;code&gt;remote device&lt;/code&gt;即可。&lt;/p&gt;
&lt;h2&gt;二.找不到已连接的手机&lt;/h2&gt;
&lt;p&gt;这个在windows上一般比较容易，因为基本上各种安全软件都会自动帮助用户连上，在Ubuntu下我们需要使用adb来连接手机。使用&lt;code&gt;adb start-server&lt;/code&gt;来开启服务监听手机的连接。如果出现权限错误，可以&lt;code&gt;adb kill-server&lt;/code&gt;，再&lt;code&gt;adb start-server&lt;/code&gt;。只要手机上弹出连接确认框，即说明已经连上了。&lt;/p&gt;
&lt;h2&gt;三.Inspect的页面空白&lt;/h2&gt;
&lt;p&gt;这个问题困扰了我很久，因为我在windows上是没有问题的，后来查了一下，是因为chrome需要翻墙才行，在windows上我使用的是shadowsocks的pac方案，linux上则是自己建立的翻墙规则，导致有遗漏。所以如果出现Inspect页面空白，可以尝试&lt;code&gt;全局翻墙&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;四.如果在手机上访问开发机器上的服务&lt;/h2&gt;
&lt;p&gt;这个问题有这种解决方案。 1.如果手机和电脑在同一个局域网内，可以直接访问电脑的ip地址即可。但是http服务监听的地址不能只是localhost，应该是电脑的ip地址或者0.0.0.0 2.使用远程设备的端口转发功能，设置如下图 &lt;img src=&quot;/uploads/wp/2017/07/2017-07-31-17-26-36%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE.png&quot; alt=&quot;端口转发设置&quot; /&gt; 成功的界面如下 &lt;img src=&quot;/uploads/wp/2017/07/2017-07-31-17-28-48%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE.png&quot; alt=&quot;端口转发设置成功&quot; /&gt; 这样，我在手机上访问localhost:4200,相当于在电脑上访问localhost:4200,在手机上访问localhost:1025，相当于在电脑上访问localhost:80。需要注意的是，端口转发的左侧输入框端口号必须大于1024，这是因为小于等于1024的端口号是被划分出来特殊使用的。&lt;/p&gt;
</content:encoded><category>前端</category><author>joyme123</author></item><item><title>树的可视化以及家谱绘制的算法</title><link>https://www.myway5.com/blog/tree-visual/</link><guid isPermaLink="true">https://www.myway5.com/blog/tree-visual/</guid><description>树的可视化意思就是将一颗树（数据结构的树）用图形展现出来。树的可视化有很多种用途，比如说很常见的组织结构图就是树的可视化的应用。家谱也可以算是一颗树，但是与树的可视化稍微不同的是，会有配偶这个角色，同时一个家谱可能并不是从一个根节点向下（可能存在某个家族向上追溯到某一代，之前的…</description><pubDate>Thu, 20 Jul 2017 09:37:28 GMT</pubDate><content:encoded>&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;树的可视化意思就是将一颗树（数据结构的树）用图形展现出来。树的可视化有很多种用途，比如说很常见的组织结构图就是树的可视化的应用。家谱也可以算是一颗树，但是与树的可视化稍微不同的是，会有配偶这个角色，同时一个家谱可能并不是从一个根节点向下（可能存在某个家族向上追溯到某一代，之前的资料已经完全丢失了，那么这个家谱的起源应该是这一代人，而不是具体到某一个人）。&lt;/p&gt;
&lt;h2&gt;二、难点&lt;/h2&gt;
&lt;p&gt;在树的可视化过程中，难点就在于如何确定每一个节点的位置（X,Y），Y坐标相对来说是比较好确定的，可以根据它在一颗树中的第几层来确定Y的值；X的值既受到同一层其他的节点位置的影响，同时也受到其子代位置的影响。因为其子代可能会和其兄弟节点的子代交叉。所以这篇文章主要讲解的是每个点的位置的计算。 家谱的绘制和树的可视化也有前言中的一些不同之处，因为也不能完全的将树的可视化算法生搬硬套。 下面我将一步步的讲解整个树的可视化算法的过程，以及如何将这个算法应用到家谱的绘制上，并给出具体的实现代码（因为是网页上有这个需求，所以代码是js写的）。算法的主要提出者是John Q. Walker II，这里有他对这个算法的相关讲解。&lt;a href=&quot;http://www.drdobbs.com/positioning-nodes-for-general-trees/184402320?pgno=4&quot; rel=&quot;noopener&quot;&gt;POSITIONING NODES FOR GENERAL TREES&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;三、算法介绍&lt;/h2&gt;
&lt;p&gt;图1： &lt;img src=&quot;/uploads/wp/2017/07/fig1.gif&quot; alt=&quot;示意图&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1、节点属性定义&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var Node = function(key,image,description){
    this.key = key;
    this.parent = null;         //父辈
    this.offspring = null;      //指向最左边的子辈
    this.leftSibling = null;    //左兄弟
    this.rightSibling = null;   //右兄弟
    this.prelim = 0;            //x坐标的预定义
    this.modifier = 0;          //调整的大小
    this.level = 0;
    this.width = 60;            //每个节点的最终宽度，如果存在配偶这个宽度是会发生改变的
    this.height = 80;           
    this.nodeWidth = 60;        //每个节点的基本宽度，不会变
    this.nodeHeight = 80;
    this.x = 0;
    this.y = 0;
    this.spouses = [];
    this.image = image||&quot;assets/post-photo.jpg&quot;;
    this.description = description || &quot;&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里首先介绍几个重要的属性： &lt;code&gt;parent,offspring,leftSibling,rightSibling&lt;/code&gt;：分别是当前节点的父节点指针、最左边的孩子节点的指针、左兄弟、右兄弟。以上图为例，M点的parent是N,offspring是H,leftSibling是G,rightSibling是null。 &lt;code&gt;prelim&lt;/code&gt;：当前点的X的预定义位置。如同在难点中所说，X的位置受到很多因素的影响，所以需要多次计算。prelim只是预定义的位置，不是最终的位置。 &lt;code&gt;modifier&lt;/code&gt;：调整值，不过这个调整值并不是说当前点的X还要调整多少，而是当前点的后代的X还要调整的大小。 &lt;code&gt;level&lt;/code&gt;：当前点所处的层级。 &lt;code&gt;width、height、nodeWidth、nodeHeight&lt;/code&gt;：这些参数用来表示节点的宽和高，width和nodeWidth的区别在于，nodeWidth是画出来的节点的宽度，是一个基本属性，不会改变，width则表示这个点最终会占用的宽度。在树的可视化算法中，只需要nodeWidth和nodeHeight，width和height是专门为家谱的绘制增加的。 &lt;code&gt;x&lt;/code&gt;：当前点的X，如何确定X的大小呢？以M为例，M.X = M.prelim + N.modifier + O.modifier。 &lt;code&gt;y&lt;/code&gt;：当前点的y,以M为例: M.y = M.level * (每层的间隔 + M.height); &lt;code&gt;spouses&lt;/code&gt;:这个是专门为家谱的绘制准备的。表示当前点的配偶，一个点的配偶可以有多个，所以使用数组。&lt;/p&gt;
&lt;h3&gt;2、算法的大概流程&lt;/h3&gt;
&lt;p&gt;算法大概可以划分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.倒序遍历整颗树，初次确定每个点的prelim和modifier。&lt;/li&gt;
&lt;li&gt;2.从最底层逐级向上检查是否存在交叉，如果存在交叉则调整（这一步和我参考的算法不同，它是从上向下检查，但是我在使用中发现，如果存在子树的子树交叉的情况，那么在高层做了调整之后，这里再做一次调整就可能出现再次重叠的情况，所以改用从底层向上检查）。 (勘误：这个地方不对，应该还是从上向下遍历，这样从上方调整后，即使下方还有重叠，也可以再次检测并调整)&lt;/li&gt;
&lt;li&gt;3.最后确定根节点的位置&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、算法详细介绍&lt;/h3&gt;
&lt;p&gt;这里我们先定义几个常量:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SiblingSeparation = 4,   //兄弟节点之间的间隔
SubtreeSeparation = 6,   //子树之间的间隔
width = nodeWidth = 2   //节点的宽度
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是以图1所示的树为例，这里先做一遍倒序遍历，初步确定每一个节点的位置： A：因为A的左节点是null,所以&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A.prelim = 0;
A.modifier = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;B:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;B.prelim = 0;
B.modifier = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C：因为C有左节点B，所以C要在B的基础上做向右的偏移，偏移量为B的prelim加上B的宽度加上间隔大小(这个地方的处理和John Q. Walker II的算法不同，是为了家谱的显示做的更改)。所以&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C.prelim = B.prelim + B.width + SiblingSeparation = 0 + 2 + 4 = 6;
C.modifier = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;D：因为D是B、C的父节点，同时是A的右兄弟，所以D的位置由A确定后，为了保证D应该在B和C中间，应该对B、C做调整，但是我们并不会再回头对B、C做处理，而是计算D的modifier，以此来表示D的后代需要调整的位置。并且D.modifer应该等于D.prelim - (B.prelim + C.prelim)/2。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D.prelim = A.prelim + A.width +  SiblingSeparation= 0 + 2 + 4 = 6;
D.modifier = D.prelim - (B.prelim + C.prelim) / 2 = 6 - (0 + 6) / 2 = 3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;E：因为E没有左兄弟，所以直接通过A、D来确定位置即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;E.prelim = (A.prelim + D.prelim) / 2 = (0 + 6) / 2  = 3;
E.modifier = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;F：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;F.prelim = E.prelim + E.width + SiblingSeparation = 3 + 2 + 4 = 9;
F.modifier = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;G：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;G.prelim = 0;
G.modifier = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;H：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;H.prelim = 0;
H.modifier = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;I.prelim = H.prelim + H.width + SiblingSeparation = 0 + 2 + 4 = 6;
I.modifier = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;J：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;J.prelim = I.prelim + I.width + SiblingSeparation = 6 + 2 + 4 = 12;
J.modifier = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;K：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;K.prelim = J.prelim + J.width + SiblingSeparation = 12 + 2 + 4 = 18;
K.modifier = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;L：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;L.prelim = K.prelim + k.width + SiblingSeparation = 18 + 2 + 4 = 24;
L.modifier = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;M：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;M.prelim = G.prelim + G.width + SiblingSeparation = 6;
M.modifer = M.prelim - (H.prelim + L.prelim) / 2 = -6；
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;N:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;N.prelim = F.prelim + F.width + SiblingSeparation = 9 + 2 + 4 = 15;
N.modifier = N.prelim - (G.prelim + M.prelim) / 2 =  12;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;走到这里我们就不再向上走了，因为根节点的位置肯定是有E和N来最终确定。所以这个时候应该来调整整颗树，检查是否存在节点位置重叠的情况。检查是否重叠是一个递归，所以使用递归检查会降低很多复杂度。但是这里我们走一遍过程，并非是严格按照代码执行过程。 我们先检查最底层，也就是这颗树的第4层： 检查C和H是否有重叠（当然是最右和最左做对比了）： C.X = D.modifier + E.modifier + C.prelim = 3 + 0 + 6 = 9; H.X = M.modifier + N.modifier + H.prelim = -6 + 12 + 0 = 6; 所以H在C的左边3处，显然是重叠了的。所以需要将H所在的子树全部向右移&lt;code&gt;offset = SubtreeSeparation - (H.X-C.X) = 9&lt;/code&gt;;我们一直向上找，直到C和H的祖先是兄弟时，也就是E和N，修改N.prelim、N.modifier就代表将N和其子树全体右移。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;N.prelim = N.prelim + offset = 15 + 9 = 24;
N.modifier = N.modifier + offset = 12 + 9 = 21;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候E和N之间距离变得更大了，但是E和F的距离与F和N的距离是不同的，为了让树显示的好看一点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;F.prelim = F.prelim + offset / 2 = 9 + 9 / 2 = 13.5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;F的子树也应该调整一下（虽然这里并没有）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;F.modifier = F.modifier + offset / 2 = 0 + 9 / 2 = 4.5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再往上看，发现已经没有重叠的了。 这时候确定根节点O的位置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;O.prelim = (E.prelim + N.prelim) / 2 = (3 + 24) / 2 = 13.5;
O.modifier = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;四、如何将树的可视化算法应该到家谱的绘制上。&lt;/h3&gt;
&lt;p&gt;在之前已经介绍过家谱和树的结构有稍许不同，所以我们尽量将家谱做一些处理，使其变成标准的树结构。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.配偶问题。 将某个节点的配偶不当成一个节点看待，而是这个节点的一部分，所以我们只需在有配偶的时候改变节点的width就可以了。&lt;/li&gt;
&lt;li&gt;2.不只一个根节点的问题 既然有不只一个根节点存在，那么我们就自己定义一个虚拟的节点作为根节点，将第一层所有的点的parent都指向这个根节点即可。这样就是一个非常标准的树结构了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;五、源代码&lt;/h3&gt;
&lt;p&gt;源代码放在了github上，地址为&lt;a href=&quot;https://github.com/joyme123/candraw&quot; rel=&quot;noopener&quot;&gt;https://github.com/joyme123/candraw&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>工作</category><category>树的可视化</category><category>族谱</category><author>joyme123</author></item><item><title>leveldb源代码阅读（三）-memtable的实现</title><link>https://www.myway5.com/blog/leveldb-3-memtable/</link><guid isPermaLink="true">https://www.myway5.com/blog/leveldb-3-memtable/</guid><description>在之前的文章中提到，leveldb所有的记录都是由一个个log文件逐渐转变来的，与存储在磁盘上的log文件对应的就是内存中的memtable。memtable和log文件存储的内容是一致的。 二、SkipList</description><pubDate>Mon, 17 Jul 2017 15:49:02 GMT</pubDate><content:encoded>&lt;h2&gt;一、序言&lt;/h2&gt;
&lt;p&gt;在之前的文章中提到，leveldb所有的记录都是由一个个log文件逐渐转变来的，与存储在磁盘上的log文件对应的就是内存中的memtable。memtable和log文件存储的内容是一致的。&lt;/p&gt;
&lt;h2&gt;二、SkipList&lt;/h2&gt;
&lt;p&gt;memtable中的键值对是通过SkipList这种数据结构存储的，SkipList相对于普通的List来说,普通的List查找时间复杂度为O(n)，而SkipList可以达到O(log(n)),同时，SkipList相对与红黑树，在多线程上，需要锁住的数据量更小，性能更优。 &lt;img src=&quot;/uploads/wp/2017/07/SkipList1-1024x146.png&quot; alt=&quot;SkipList示意图&quot; /&gt; 比如上图就是一个SkipList结构，如果我们需要搜索45这个点，我们首先在最高层(第二层)找到30,然后发现下一个点是57，已经大于45了，于是来到下一层(第一层)从30开始,向后找，就能找到45了，在这个过程中，我们跳过了很多个点。因此这种数据结构得名SkipList。&lt;/p&gt;
&lt;h3&gt;2.1 SkipList的基本信息&lt;/h3&gt;
&lt;p&gt;SkipList是memtable中用到的最主要的数据结构。 SkipList的线程安全特点：写操作需要外部的同步，比如信号量这种。读操作需要保证在读的时候SkipList不会被销毁。除此之外，读操作没有任何的内部锁或者同步。 SkipList的基本原则： （1）分配节点在SkipList销毁之前不会被删除。因为在代码中我们没有任何的删除节点的操作。 （2）一个节点的内容，除了next/prev指针,在这个节点被存储到SkipList之后其他数据都是不变的。只有Insert()会修改list的内容，初始化一个节点并使用release-stores去将这个节点存放到一个或多个list中需要谨慎对待。&lt;/p&gt;
&lt;h3&gt;2.2 SkipList的成员函数&lt;/h3&gt;
&lt;p&gt;SkipList中，公共的成员函数只有两个 void Insert(const Key&amp;amp; key); bool Contains(const Key&amp;amp; key) const; 顾名思义，Insert用来向SkipList中插入key，Contains用来检查SkipList中是否包含某个key。 私有的成员函数如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  Node* NewNode(const Key&amp;amp; key, int height);
  int RandomHeight();
  bool Equal(const Key&amp;amp; a, const Key&amp;amp; b) const { return (compare_(a, b) == 0); }

  // Return true if key is greater than the data stored in &quot;n&quot;
  bool KeyIsAfterNode(const Key&amp;amp; key, Node* n) const;

  // Return the earliest node that comes at or after key.
  // Return NULL if there is no such node.
  //
  // If prev is non-NULL, fills prev[level] with pointer to previous
  // node at &quot;level&quot; for every level in [0..max_height_-1].
  Node* FindGreaterOrEqual(const Key&amp;amp; key, Node** prev) const;

  // Return the latest node with a key &amp;lt; key.
  // Return head_ if there is no such node.
  Node* FindLessThan(const Key&amp;amp; key) const;

  // Return the last node in the list.
  // Return head_ if list is empty.
  Node* FindLast() const;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.3 SkipList的节点Node实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// Implementation details follow
template&amp;lt;typename Key, class Comparator&amp;gt;
struct SkipList&amp;lt;Key,Comparator&amp;gt;::Node {
  explicit Node(const Key&amp;amp; k) : key(k) { }

  Key const key;

  // Accessors/mutators for links.  Wrapped in methods so we can
  // add the appropriate barriers as necessary.
  Node* Next(int n) {
    assert(n &amp;gt;= 0);
    // Use an &apos;acquire load&apos; so that we observe a fully initialized
    // version of the returned Node.
    return reinterpret_cast&amp;lt;Node*&amp;gt;(next_[n].Acquire_Load());
  }
  void SetNext(int n, Node* x) {
    assert(n &amp;gt;= 0);
    // Use a &apos;release store&apos; so that anybody who reads through this
    // pointer observes a fully initialized version of the inserted node.
    next_[n].Release_Store(x);
  }

  // No-barrier variants that can be safely used in a few locations.
  Node* NoBarrier_Next(int n) {
    assert(n &amp;gt;= 0);
    return reinterpret_cast&amp;lt;Node*&amp;gt;(next_[n].NoBarrier_Load());
  }
  void NoBarrier_SetNext(int n, Node* x) {
    assert(n &amp;gt;= 0);
    next_[n].NoBarrier_Store(x);
  }

 private:
  // Array of length equal to the node height.  next_[0] is lowest level link.
  port::AtomicPointer next_[1];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意到Node实现了两个版本的Next和SetNext，带有NoBarrier_的是多进程不安全的，因为在少数情况下会用到，因此做了额外的实现。另外，&lt;code&gt;可以注意这里只实现了Next()方法，并没有Prev()指向上一个节点，可以知道SkipList并不是一个双向链表&lt;/code&gt;，但是SkipList的迭代器是支持Prev()方法的，所以这是一个值得注意的地方。 Node节点的创建是通过NewNode()方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Key, class Comparator&amp;gt;
typename SkipList&amp;lt;Key,Comparator&amp;gt;::Node*
SkipList&amp;lt;Key,Comparator&amp;gt;::NewNode(const Key&amp;amp; key, int height) {
  char* mem = arena_-&amp;gt;AllocateAligned(
      sizeof(Node) + sizeof(port::AtomicPointer) * (height - 1));
  return new (mem) Node(key);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NewNode中使用了arena来分配对齐的内存，分配的内存大小是&lt;code&gt;sizeof(Node)+sizeof(port::AtomicPointer) * (height - 1)&lt;/code&gt;。这里可以注意到申请的空间不仅仅是一个Node的大小，还包括sizeof(port::AtomicPointer) * (height - 1)，这是因为SkipList是一个多层的结构，所以如果当前Node是3层，那么就需要3个Next指针，在Node的代码中，默认只申请了一个Next指针空间，所以需要额外的多申请(height - 1)个AtomicPointer的空间。&lt;/p&gt;
&lt;h3&gt;2.4 SkipList的迭代器(Iterator)&lt;/h3&gt;
&lt;p&gt;SkipList实现了自己的迭代器，迭代器的声明如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Iterator {
   public:
    // Initialize an iterator over the specified list.
    // The returned iterator is not valid.
    explicit Iterator(const SkipList* list);

    // 如果迭代器位于一个有效的节点上，就会返回true.
    bool Valid() const;

    // 返回当前位置的key.
    // REQUIRES: Valid()
    const Key&amp;amp; key() const;

    // 前进到下一个位置
    // REQUIRES: Valid()
    void Next();

    // 后退到上一个位置
    // REQUIRES: Valid()
    void Prev();

    // 移到第一个key &amp;gt;= target的entry
    void Seek(const Key&amp;amp; target);

    // 移到这个list的第一个entry
    // 如果list非空，迭代器的最终状态是Valid()
    void SeekToFirst();

    // 移到这个list的最后一个entry
    // 如果list非空，迭代器的最终状态是Valid()
    void SeekToLast();

   private:
    const SkipList* list_; 
    Node* node_;
    // Intentionally copyable
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中Prev()的实现并不是通过prev指针，因为上文中已经介绍了Node只有next指针。Prev()是通过FindLessThan来实现，找到小于指定Node的最后一个值。&lt;code&gt;这里有一个问题，为什么不使用prev指针，难道不会大大的降低时间复杂度吗？&lt;/code&gt;看一下FindLessThan的具体实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Key, class Comparator&amp;gt;
typename SkipList&amp;lt;Key,Comparator&amp;gt;::Node*
SkipList&amp;lt;Key,Comparator&amp;gt;::FindLessThan(const Key&amp;amp; key) const {
  Node* x = head_;
  int level = GetMaxHeight() - 1;
  while (true) {
    assert(x == head_ || compare_(x-&amp;gt;key, key) &amp;lt; 0);
    Node* next = x-&amp;gt;Next(level);
    if (next == NULL || compare_(next-&amp;gt;key, key) &amp;gt;= 0) {
      if (level == 0) {
        return x;
      } else {
        // Switch to next list
        level--;
      }
    } else {
      x = next;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实就是SkipList查找过程的实现。时间复杂度应该是O(log(n)).相比于prev指针的话，在空间占用上是少一点的。具体为什么这样实现还得继续往下看。 SkipList还以同样的方式实现了&lt;code&gt;FindGreaterOrEqual&lt;/code&gt;和&lt;code&gt;FindLast&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Key, class Comparator&amp;gt;
typename SkipList&amp;lt;Key,Comparator&amp;gt;::Node* SkipList&amp;lt;Key,Comparator&amp;gt;::FindGreaterOrEqual(const Key&amp;amp; key, Node** prev)
    const {
  Node* x = head_;
  int level = GetMaxHeight() - 1;
  while (true) {
    Node* next = x-&amp;gt;Next(level);
    if (KeyIsAfterNode(key, next)) {
      // Keep searching in this list
      x = next;
    } else {
      if (prev != NULL) prev[level] = x;
      if (level == 0) {
        return next;
      } else {
        // Switch to next list
        level--;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;FindGreaterOrEqual&lt;/code&gt;的实现还是比较有意思的，在返回比指定key大于或等于的节点的同时，还可以选择性的将该节点的prev指针保存到Node** prev中，返回给用户。其实这也是Iterator中Prev()的实现不采用prev指针的原因，因为有了FindGreaterOrEqual这样的实现，SkipList的内部实现中根本不需要Prev()来将指针向前移动，同时又大大的节约了内存。 下面看一下SkipList的Insert的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void SkipList&amp;lt;Key,Comparator&amp;gt;::Insert(const Key&amp;amp; key) {
  // TODO(opt): We can use a barrier-free variant of FindGreaterOrEqual()
  // here since Insert() is externally synchronized.
  Node* prev[kMaxHeight];
  Node* x = FindGreaterOrEqual(key, prev);

  // Our data structure does not allow duplicate insertion
  assert(x == NULL || !Equal(key, x-&amp;gt;key));

  int height = RandomHeight();
  if (height &amp;gt; GetMaxHeight()) {
    for (int i = GetMaxHeight(); i &amp;lt; height; i++) {
      prev[i] = head_;
    }
    //fprintf(stderr, &quot;Change height from %d to %d\n&quot;, max_height_, height);

    // It is ok to mutate max_height_ without any synchronization
    // with concurrent readers.  A concurrent reader that observes
    // the new value of max_height_ will see either the old value of
    // new level pointers from head_ (NULL), or a new value set in
    // the loop below.  In the former case the reader will
    // immediately drop to the next level since NULL sorts after all
    // keys.  In the latter case the reader will use the new node.
    max_height_.NoBarrier_Store(reinterpret_cast&amp;lt;void*&amp;gt;(height));
  }

  x = NewNode(key, height);
  for (int i = 0; i &amp;lt; height; i++) {
    // NoBarrier_SetNext() suffices since we will add a barrier when
    // we publish a pointer to &quot;x&quot; in prev[i].
    x-&amp;gt;NoBarrier_SetNext(i, prev[i]-&amp;gt;NoBarrier_Next(i));
    prev[i]-&amp;gt;SetNext(i, x);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，Insert方法中没有做任何的多线程同步，所以需要调用者在外部进行写入同步(防止多个线程同步写入)。 插入的过程是先找到需要插入的位置，然后调用RandomHeight()来随机得到当前节点的高度，之后向普通链表一样执行插入操作。 注意到这里有一个多线程读写的问题。也就是当我们改变max_height_的值的时候，并没有改变head_指针。这种情况下有两种可能,一是读到了新的max_height_，但是head_指针还没有更改,这时候指针会立刻下降到下一级，SkipList的多级只是为了提高查找速度，所以在这里并没有其他的副作用；二是正常的情况，不作讨论。&lt;/p&gt;
&lt;h2&gt;2.Memtable&lt;/h2&gt;
&lt;p&gt;Memtable的实现主要就是依赖于SkipList，分析了SKipList之后，Memtable的插入，查找操作都是SkipList的插入和查找。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // Increase reference count.
  void Ref() { ++refs_; }

  // Drop reference count.  Delete if no more references exist.
  void Unref() {
    --refs_;
    assert(refs_ &amp;gt;= 0);
    if (refs_ &amp;lt;= 0) {
      delete this;
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Memtable的实现使用了类似于计数器引用的机制，不过这是手动实现的，所以需要用户在使用Memtable时调用Ref()来增加引用计数，引用结束后调用Unref来减少引用计数，当引用计数为0时则销毁自己。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Memtable的设计还是很让人佩服的，至少对我来说是这样。了解到了SkipList这样的数据结构，也了解到内存屏障这种多进程情况下代码执行乱序的问题，还有内存对齐的必要、内存对齐的实现。同时也了解了一些C++编写程序的独特风格（我写的C++一直像Java，反而少了一点C++的美）&lt;/p&gt;
</content:encoded><category>c++</category><category>levelDB源码阅读</category><category>leveldb</category><category>memtable</category><category>skiplist</category><author>joyme123</author></item><item><title>leveldb源码阅读（二）—— Varint和Arena的实现</title><link>https://www.myway5.com/blog/leveldb-varint-arena/</link><guid isPermaLink="true">https://www.myway5.com/blog/leveldb-varint-arena/</guid><description>Varint是在leveldb中广泛使用的一种变长的整数类型，Varint其实和unicode的实现非常相似，并且是little-endian。如果当前字节的最高位是1，则表示后面的字节也属于这个整数，如果最高位是0，则表示该整数结束了。</description><pubDate>Mon, 17 Jul 2017 15:45:28 GMT</pubDate><content:encoded>&lt;h2&gt;一、Varint&lt;/h2&gt;
&lt;p&gt;Varint是在leveldb中广泛使用的一种变长的整数类型，Varint其实和unicode的实现非常相似，并且是little-endian。如果当前字节的最高位是1，则表示后面的字节也属于这个整数，如果最高位是0，则表示该整数结束了。所以，一个无符号4个字节的整型数，使用Varint可能以1,2,3,4,5个字节来表示。 比如130:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;无符号整型表示起来为：00000000 00000000 00000000 10000010
使用Varint表示为：10000010 00000001             #这里注意Varint是little-endian
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为牺牲了每个字节的最高位作为标志位,所以遇到大的数可能需要5个字节。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;某个无符号整型：10000001 00000001 00000001 00000001
使用Varint表示：10000001 10000010 10000100 10001000 00001000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是平均下来，Varint是绝对会节省大量的存储空间的。 那么leveldb中Varint是如何编码和解码的呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char* EncodeVarint32(char* dst, uint32_t v) {
  // Operate on characters as unsigneds
  unsigned char* ptr = reinterpret_cast&amp;lt;unsigned char*&amp;gt;(dst);
  static const int B = 128; //10000000
  if (v &amp;lt; (1&amp;lt;&amp;lt;7)) {  v &amp;lt; 2^7
    *(ptr++) = v;
  } else if (v &amp;lt; (1&amp;lt;&amp;lt;14)) { v &amp;lt; 2^14
    *(ptr++) = v | B;
    *(ptr++) = v&amp;gt;&amp;gt;7;
  } else if (v &amp;lt; (1&amp;lt;&amp;lt;21)) { //v &amp;lt; 2^21
    *(ptr++) = v | B;
    *(ptr++) = (v&amp;gt;&amp;gt;7) | B;
    *(ptr++) = v&amp;gt;&amp;gt;14;
  } else if (v &amp;lt; (1&amp;lt;&amp;lt;28)) {
    *(ptr++) = v | B;
    *(ptr++) = (v&amp;gt;&amp;gt;7) | B;
    *(ptr++) = (v&amp;gt;&amp;gt;14) | B;
    *(ptr++) = v&amp;gt;&amp;gt;21;
  } else {
    *(ptr++) = v | B;
    *(ptr++) = (v&amp;gt;&amp;gt;7) | B;
    *(ptr++) = (v&amp;gt;&amp;gt;14) | B;
    *(ptr++) = (v&amp;gt;&amp;gt;21) | B;
    *(ptr++) = v&amp;gt;&amp;gt;28;
  }
  return reinterpret_cast&amp;lt;char*&amp;gt;(ptr);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这一段对uint32_t的变量进行Varint32编码的例子，编码的结果保存在dst里，返回的ptr是dst的最后一个字节的指针。使用if-else对1、2、3、4、5个字节的情况分别做了处理。主要就是移位和或运算符的使用。跟着代码走一遍就能理解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const char* GetVarint32PtrFallback(const char* p,
                                   const char* limit,
                                   uint32_t* value) {
  uint32_t result = 0;
  for (uint32_t shift = 0; shift &amp;lt;= 28 &amp;amp;&amp;amp; p &amp;lt; limit; shift += 7) {
    uint32_t byte = *(reinterpret_cast&amp;lt;const unsigned char*&amp;gt;(p));
    p++;
    if (byte &amp;amp; 128) {
      // More bytes are present
      result |= ((byte &amp;amp; 127) &amp;lt;&amp;lt; shift);
    } else {
      result |= (byte &amp;lt;&amp;lt; shift);
      *value = result;
      return reinterpret_cast&amp;lt;const char*&amp;gt;(p);
    }
  }
  return NULL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;inline const char* GetVarint32Ptr(const char* p,
                                  const char* limit,
                                  uint32_t* value) {
  if (p &amp;lt; limit) {
    uint32_t result = *(reinterpret_cast&amp;lt;const unsigned char*&amp;gt;(p));
    if ((result &amp;amp; 128) == 0) {
      //如果低8位是0xxxxxxx,little-endian,也就是小于128
      *value = result;    //直接赋值
      return p + 1;
    }
  }
  return GetVarint32PtrFallback(p, limit, value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面GetVarint32Ptr是解码Varint32，当值大于128时，则调用GetVarint32PtrFallback。在则调用GetVarint32PtrFallback中，利用循环一个字节一个字节的取出，最终将value指针指向结果result。&lt;/p&gt;
&lt;h2&gt;二、Arena&lt;/h2&gt;
&lt;p&gt;Arena是leveldb中管理内存分配的类。所有的内存分配都通过Arena申请，可以根据申请的内存大小使用不同的内存分配策略，也可以避免过多的内存碎片问题，并且在内存释放时统一使用Arena来释放，方便管理。 Arena类的实现并不复杂。首先看一下成员变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // Allocation state
  char* alloc_ptr_;                 //指向当前块中剩余的内存起点
  size_t alloc_bytes_remaining_;    //当前块中剩余的内存

  // Array of new[] allocated memory blocks
  std::vector&amp;lt;char*&amp;gt; blocks_;       //用来保存所有new出来的char数组，释放内存时也会使用到

  // Total memory usage of the arena.
  port::AtomicPointer memory_usage_;    //通过Arena申请的内存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就是public的成员函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Return a pointer to a newly allocated memory block of &quot;bytes&quot; bytes.
  char* Allocate(size_t bytes);

  // Allocate memory with the normal alignment guarantees provided by malloc
  char* AllocateAligned(size_t bytes);

  // Returns an estimate of the total memory usage of data allocated
  // by the arena.
  size_t MemoryUsage() const {
    return reinterpret_cast&amp;lt;uintptr_t&amp;gt;(memory_usage_.NoBarrier_Load());
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;功能上很简单，Allocate用来申请指定大小的内存，AllocateAligned用来申请保证内存对齐的内存空间，MemoryUsage用来获取内存使用情况。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我对内存对齐的理解：现在假定内存对齐的最小单位是8个字节，那么如果你只申请14个字节，内存对齐会多给你分配2个字节凑成16个字节。内存对齐的好处就是访问速度的提升，在CPU一次读取8个字节的情况下。在没有内存对齐的情况下，你要读取第6~12个字节，就需要读取两次，第一次0~7个字节，通过移位取出6~7,再读取第8~15个字节，取出8~12。这样就需要读取两次。而如果在内存对齐的情况下，你需要读取的6~12个字节应该被放在8~15个字节中。只需读取一次即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;私有的成员函数有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char* AllocateFallback(size_t bytes);
char* AllocateNewBlock(size_t block_bytes);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两个函数提供了具体的内存分配的实现。 具体分析这5个成员函数。 1.Allocate(size_t bytes)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline char* Arena::Allocate(size_t bytes) {
  // The semantics of what to return are a bit messy if we allow
  // 0-byte allocations, so we disallow them here (we don&apos;t need
  // them for our internal use).
  assert(bytes &amp;gt; 0);
  if (bytes &amp;lt;= alloc_bytes_remaining_) {
    char* result = alloc_ptr_;
    alloc_ptr_ += bytes;
    alloc_bytes_remaining_ -= bytes;
    return result;
  }
  return AllocateFallback(bytes);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Allocate是一个内联函数。内联函数的好处在于编译时会直接在调用处展开，对于程序执行速度有一定提升，但是也会导致程序大小变大。 &lt;code&gt;assert(bytes &amp;gt; 0);&lt;/code&gt;是一个断言语句，断言语句可以在编译时由编译器去除，但是在调试的时候可以帮助程序员发现问题所在。这里用来保证没有出现0个字节分配的情况。 后面的判断语句用来判断当前块的剩余空间是否够分配，如果够则分配。如果不够则交给AllocateFallback()处理。返回的是分配的空间起点。 2.AllocateAligned(size_t bytes)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char* Arena::AllocateAligned(size_t bytes) {
  const int align = (sizeof(void*) &amp;gt; 8) ? sizeof(void*) : 8;
  assert((align &amp;amp; (align-1)) == 0);   // Pointer size should be a power of 2
  size_t current_mod = reinterpret_cast&amp;lt;uintptr_t&amp;gt;(alloc_ptr_) &amp;amp; (align-1);
  size_t slop = (current_mod == 0 ? 0 : align - current_mod);
  size_t needed = bytes + slop;
  char* result;
  if (needed &amp;lt;= alloc_bytes_remaining_) {
    result = alloc_ptr_ + slop;
    alloc_ptr_ += needed;
    alloc_bytes_remaining_ -= needed;
  } else {
    // AllocateFallback always returned aligned memory
    result = AllocateFallback(bytes);
  }
  assert((reinterpret_cast&amp;lt;uintptr_t&amp;gt;(result) &amp;amp; (align-1)) == 0);
  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AllocateAligned(size_t bytes)用来做内存对齐的分配。 &lt;code&gt;const int align = (sizeof(void*) &amp;gt; 8) ? sizeof(void*) : 8;&lt;/code&gt;用来获取分配的最小单位，如果指针大小大于8，则取指针大小，否则使用8作为最小的对齐单位。 &lt;code&gt;assert((align &amp;amp; (align-1)) == 0);&lt;/code&gt;这是一个很有意思的判断，可以保证只有2的n次方才能满足。 &lt;code&gt;size_t current_mod = reinterpret_cast&amp;lt;uintptr_t&amp;gt;(alloc_ptr_) &amp;amp; (align-1);&lt;/code&gt;current_mod是余数，比如当前指针地址是15，align是8,那么余数为7,这个余数就是alloc_prt_&amp;amp;(align-1)的值。 &lt;code&gt;size_t slop = (current_mod == 0 ? 0 : align - current_mod);&lt;/code&gt;因为当前指针可能不是align的整数倍,slop即代表当前指针需要偏移的大小，来保证是align的整数倍。 &lt;code&gt;size_t needed = bytes + slop;&lt;/code&gt; needed代表需要分配的空间。 之后的判断则和Allocate()函数做法相同了。需要注意的是返回的result指针是align的整数倍，这样就能保证以最少的内存读取次数来获取想要的数据。 3.AllocateFallback(size_t bytes)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char* Arena::AllocateFallback(size_t bytes) {
  if (bytes &amp;gt; kBlockSize / 4) {
    //如果需要分配的bytes大于块大小的1/4，则单独在新的块中分配，以此避免浪费过多的剩下的空间（这样能保证浪费的空间小与kBlockSize / 4)
    char* result = AllocateNewBlock(bytes);
    return result;
  }

  // 重新开辟新的块空间进行分配，上一个块剩下的空间都浪费了。
  alloc_ptr_ = AllocateNewBlock(kBlockSize);
  alloc_bytes_remaining_ = kBlockSize;

  char* result = alloc_ptr_;
  alloc_ptr_ += bytes;
  alloc_bytes_remaining_ -= bytes;
  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面两个成员函数在当前块分配空间不足时，都将分配任务交给AllocateFallback处理。 4.AllocateNewBlock(size_t block_bytes)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char* Arena::AllocateNewBlock(size_t block_bytes) {
  char* result = new char[block_bytes];
  blocks_.push_back(result);
  memory_usage_.NoBarrier_Store(
      reinterpret_cast&amp;lt;void*&amp;gt;(MemoryUsage() + block_bytes + sizeof(char*)));
  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用new char来申请一个新的块，并将块的起始指针存入blocks_中。更新memory_usage的大小。 5.MemoryUsage() ``` size_t MemoryUsage() const { return reinterpret_cast&amp;lt;uintptr_t&amp;gt;(memory_usage_.NoBarrier_Load()); } ``` 获取memory_usage_中存储的使用空间数据。&lt;/p&gt;
&lt;h2&gt;三、Memeory Barrier（内存屏障）&lt;/h2&gt;
&lt;p&gt;memory_usage_的类型是AtomicPointer,这是一个原子类型，针对不同平台有不同的实现。可以用来线程安全的存储和获取值。 AtomicPointer类实现了几个函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NoBarrier_Load();
NoBarrier_Store(void* v)
Acquire_Load();
Release_Store(void* v)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里面涉及到一个Memory Barrier（内存屏障,Memory fence）的概念。 引用一个wikipedia的例子：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Processor #1:&lt;/em&gt; while (f 0); // Memory fence required here print x; &lt;em&gt;Processor #2:&lt;/em&gt; x = 42; // Memory fence required here f = 1;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一个两核心的CPU按照上述例子执行。可能会出现很多种情况： 虽然我们希望的是打印“42”。但是如果核心#2存储操作乱序执行(out-of-order)，就有可能f在x之前被更新了。这样打印语句就可能打印出“0”。同样的，核心#1读取操作可能也乱序执行，x就有可能在f被判断之前读取了，打印语句可能打印出一个无法预期的值。对于大多数程序来说，上面的这些情况都是不可以接受的。使用memory barrier可以避免out-of-order的问题。 简而言之就是，在多CPU共享内存空间的情况下，两条语句的执行顺序已经无法保证了。需要memory barrier来保证执行顺序的正确。 当然，单CPU是不会出现这个问题的。 现在再来解释上面的4个函数的作用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NoBarrier_Load();           //不使用内存屏障的读取
NoBarrier_Store(void* v)    //不使用内存屏障的存储
Acquire_Load();             //使用内存屏障的读取
Release_Store(void* v)      //使用内存屏障的存储
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>c++</category><category>levelDB源码阅读</category><category>leveldb</category><category>Varint</category><category>Arena</category><author>joyme123</author></item><item><title>leveldb源码阅读（一）-log读取和写入</title><link>https://www.myway5.com/blog/leveldb-log/</link><guid isPermaLink="true">https://www.myway5.com/blog/leveldb-log/</guid><description>leveldb源码阅读（一）-log读取和写入</description><pubDate>Mon, 17 Jul 2017 15:44:07 GMT</pubDate><content:encoded>&lt;h1&gt;leveldb源码阅读（一）-log读取和写入&lt;/h1&gt;
&lt;p&gt;标签（空格分隔）： leveldb log&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、 引言&lt;/h2&gt;
&lt;p&gt;leveldb中，所有对数据库的操作都会记录到log中，当log文件达到预定义的大小后，就会被转换成数据表(sstable)，所以log文件的生成和读取算是leveldb核心部分的第一步。 这篇文章涉及到的leveldb的源代码文件有:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;db/log_format.h
db/log_reader.h
db/log_reader.cc
db/log_writer.h
db/log_writer.cc
leveldb/slice.h
leveldb/status.h
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、日志存储格式——log_format.h&lt;/h2&gt;
&lt;p&gt;在之前的文章中，了解到log是由一个个&lt;code&gt;block&lt;/code&gt;组成，每个block是&lt;code&gt;32KB&lt;/code&gt;的大小。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;block := record* trailer?
record :=
  checksum: uint32     // crc32c of type and data[] ; little-endian
  length: uint16       // little-endian
  type: uint8          // One of FULL, FIRST, MIDDLE, LAST
  data: uint8[length]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个&lt;code&gt;block&lt;/code&gt;中存储的是一条条的&lt;code&gt;记录&lt;/code&gt;,如果一个&lt;code&gt;block&lt;/code&gt;的末尾最后剩下小于等于6bytes的空间，就会使用一个&lt;code&gt;trailer&lt;/code&gt;填充而不是用新的记录填充，这个&lt;code&gt;trailer&lt;/code&gt;都是由0字节组成。一条记录包括一个crc32c生成的校验码checksum、记录的长度length、记录的类型type、数据。 如果一条记录跨越了多个&lt;code&gt;block&lt;/code&gt;,就会被分段，分段类型有3种——First、Middle、Last 关于这部分的实现是在log_format.h中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum RecordType {
  //0是保留字，用来表示预分配的文件
  kZeroType = 0,

  kFullType = 1,

  // 一条记录被分段的3种类型
  kFirstType = 2,
  kMiddleType = 3,
  kLastType = 4
};
//定义了最大的记录类型值
static const int kMaxRecordType = kLastType;
//定义了最大的`block`大小
static const int kBlockSize = 32768;

// 记录头是  checksum (4 bytes), length (2 bytes), type (1 byte).总共7bytes
static const int kHeaderSize = 4 + 2 + 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、日志的写入&lt;/h2&gt;
&lt;p&gt;在log_writer代码中，实现了Writer类，最主要的就是一个&lt;code&gt;AddRecord(const Slice&amp;amp; slice)&lt;/code&gt;。 Writer类有两个构造函数，对于只传了一个参数——WritableFile指针的构造函数，使用了explicit显示构造，这样可以防止隐式转换。另外将&lt;code&gt;Writer(const Writer&amp;amp;)&lt;/code&gt;和&lt;code&gt;void operator=(const Writer&amp;amp;)&lt;/code&gt;放在private域，防止拷贝赋值。 Writer有三个成员变量 &lt;code&gt;WritableFile* dest_&lt;/code&gt;用来保存可写的文件，&lt;code&gt;int block_offset_&lt;/code&gt;用来保存当前块的偏移量, &lt;code&gt;uint32_t type_crc_[kMaxRecordType + 1]&lt;/code&gt;用来保存5种记录类型的crc32c计算结果。这里会提前计算好保存进这个数组来减少存储时计算的负载。 接下来就是最重要的&lt;code&gt;AddRecord(const Slice&amp;amp; slice)&lt;/code&gt;了。Slice是leveldb中用来保存key或者value的值的，包含一个指针，一个长度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Status Writer::AddRecord(const Slice&amp;amp; slice) {
  const char* ptr = slice.data();   //取出数据的指针
  size_t left = slice.size();       //取出数据的长度

  // 如果有必要的话，对记录分段，并保存.
  // 注意如果slice是空的，我们也会迭代一次，保存一个长度为0的记录
  Status s;     //用来保存这次操作的结果
  bool begin = true;    //是否是一条记录的开始
  do {
    //计算出当前块剩下的空间
    const int leftover = kBlockSize - block_offset_;
    //使用assert进行断言，这边是一定会&amp;gt;=0的，assert这里只是为了帮助调试程序，实际使用时在编译时通过参数去除assert语句
    assert(leftover &amp;gt;= 0);

    //如果剩下的空间已经不足一条记录的header了
    if (leftover &amp;lt; kHeaderSize) {
      // 切换到新的块
      if (leftover &amp;gt; 0) {
        // 如果剩下的空间&amp;gt;0，需要使用\x00填充
        assert(kHeaderSize == 7);
        dest_-&amp;gt;Append(Slice(&quot;\x00\x00\x00\x00\x00\x00&quot;, leftover));
      }
      //新的块偏移量是0
      block_offset_ = 0;
    }

    // 不变的道理：我们绝对不会让剩下的空间&amp;lt;kHeaderSize.
    assert(kBlockSize - block_offset_ - kHeaderSize &amp;gt;= 0);

    //当前还可用的空间
    const size_t avail = kBlockSize - block_offset_ - kHeaderSize;

    //记录片段的大小，取可用的空间大小和记录的大小的最小值
    const size_t fragment_length = (left &amp;lt; avail) ? left : avail;

    //当前的记录类型
    RecordType type;

    //如果记录的大小和片段的长度是一样的，表示到达了记录的末尾为true，否则false
    const bool end = (left == fragment_length);

    //记录类型的判断
    if (begin &amp;amp;&amp;amp; end) {
      type = kFullType;
    } else if (begin) {
      type = kFirstType;
    } else if (end) {
      type = kLastType;
    } else {
      type = kMiddleType;
    }

    //写入到物理存储中，
    s = EmitPhysicalRecord(type, ptr, fragment_length);
    ptr += fragment_length;     //数据指针右移一个片段长度
    left -= fragment_length;    //数据的长度减去一个片段的长度
    begin = false;              //不再是开始了
  } while (s.ok() &amp;amp;&amp;amp; left &amp;gt; 0); //如果没有出错，并且数据还剩就再循环

  return s;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AddRecord(const Slice&amp;amp; slice)&lt;/code&gt;调用了一个私有成员函数&lt;code&gt;EmitPhysicalRecord(RecordType t, const char* ptr, size_t n)&lt;/code&gt;,看一看这个将记录写入持久化存储是如何实现的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Status Writer::EmitPhysicalRecord(RecordType t, const char* ptr, size_t n) {
  assert(n &amp;lt;= 0xffff);  // n必须可以用2KB表示，因为一条记录的length是uint16大小的，little-endian
  assert(block_offset_ + kHeaderSize + n &amp;lt;= kBlockSize);

  // 对记录的header进行格式化
  char buf[kHeaderSize];
  //取低位字节
  buf[4] = static_cast&amp;lt;char&amp;gt;(n &amp;amp; 0xff);
  //取高位字节
  buf[5] = static_cast&amp;lt;char&amp;gt;(n &amp;gt;&amp;gt; 8);
  //一位的记录类型
  buf[6] = static_cast&amp;lt;char&amp;gt;(t);

  // 计算记录类型的crc值和有效载荷
  uint32_t crc = crc32c::Extend(type_crc_[t], ptr, n);
  crc = crc32c::Mask(crc);                 // Adjust for storage
  //填充4位的校验值
  EncodeFixed32(buf, crc);

  // 将记录的header写入
  Status s = dest_-&amp;gt;Append(Slice(buf, kHeaderSize));
  if (s.ok()) {
    //将记录的数据写入
    s = dest_-&amp;gt;Append(Slice(ptr, n));
    if (s.ok()) {
      //flush一下
      s = dest_-&amp;gt;Flush();
    }
  }

  //块的偏移量右移header和数据的长度和
  block_offset_ += kHeaderSize + n;
  return s;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码分析其实就是log部分逻辑上的实现，将一条记录格式化成设计好的格式。这里有一个疑问就是&lt;code&gt;dest_-&amp;gt;Append(Slice&amp;amp; slice)&lt;/code&gt;和&lt;code&gt;dest_-&amp;gt;Flush()&lt;/code&gt;是如何工作的。所以继续追踪下去，到&lt;code&gt;include/leveldb/env.h&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// A file abstraction for sequential writing.  The implementation
// must provide buffering since callers may append small fragments
// at a time to the file.
class WritableFile {
 public:
  WritableFile() { }
  virtual ~WritableFile();

  virtual Status Append(const Slice&amp;amp; data) = 0;
  virtual Status Close() = 0;
  virtual Status Flush() = 0;
  virtual Status Sync() = 0;

 private:
  // No copying allowed
  WritableFile(const WritableFile&amp;amp;);
  void operator=(const WritableFile&amp;amp;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这部分是WritableFile的抽象部分，Append、Close、Flush、Sync都是纯虚函数，具体实现必须由用户自己去做。并且实现时必须提供缓冲区，这样调用者可以append小的数据段到文件中。 实际上leveldb中也有默认(posix)的实现。在&lt;code&gt;util/env/env_posix.cc&lt;/code&gt;中。目前只看Append和Flush具体实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  virtual Status Append(const Slice&amp;amp; data) {
    size_t r = fwrite_unlocked(data.data(), 1, data.size(), file_);
    if (r != data.size()) {
      return IOError(filename_, errno);
    }
    return Status::OK();
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;  virtual Status Flush() {
    if (fflush_unlocked(file_) != 0) {
      return IOError(filename_, errno);
    }
    return Status::OK();
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;fwrite_unlocked和fflush_unlocked都是define的fwrite和fflush方法。 所以这里的设计是很好的案例。通过将WritableFile抽象化，提供给用户自己来实现，从而很轻松的就可以在多平台间移植。&lt;/p&gt;
&lt;h2&gt;四、日志的读取&lt;/h2&gt;
&lt;p&gt;在log_reader中，实现了Reader类，用来读取日志文件。 Reader的构造函数定义如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Reader(SequentialFile* file, Reporter* reporter, bool checksum,
         uint64_t initial_offset);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;file是代表要读取的文件，reporter是用来报告日志文件中发现错误的，checksum是当前文件的校验码，initial_offset是当前文件中开始读取的物理位置。 Reader类中私有的成员变量和函数需要注意的有:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  SequentialFile* const file_;      //要读取的文件
  Reporter* const reporter_;        //报告错误的对象
  bool const checksum_;             //校验值
  char* const backing_store_;       //备份存储
  Slice buffer_;                    //缓冲
  bool eof_;   // Last Read() indicated EOF by returning &amp;lt; kBlockSize

  // Offset of the last record returned by ReadRecord.
  uint64_t last_record_offset_;         //最后一个Record的偏移量
  // Offset of the first location past the end of buffer_.
  uint64_t end_of_buffer_offset_;       //

  // Offset at which to start looking for the first record to return
  uint64_t const initial_offset_;   //第一个记录的初始偏移量

  // True if we are resynchronizing after a seek (initial_offset_ &amp;gt; 0). In
  // particular, a run of kMiddleType and kLastType records can be silently
  // skipped in this mode
  bool resyncing_;      //在一次查找后我们再同步则是True。特别的，kMiddleType和kLastType会被跳过   

  // Extend record types with the following special values
  enum {
    kEof = kMaxRecordType + 1,
    // 无效的记录值，可能有以下情况
    // * 具有无效的CRC值(checksum不正确)(ReadPhysicalRecord reports a drop)
    // * 长度为0的记录 (No drop is reported)
    // * The record is below constructor&apos;s initial_offset (No drop is reported)
    kBadRecord = kMaxRecordType + 2
  };

  bool SkipToInitialBlock();        //跳过直到initial_offset的位置,成功则返回true
  unsigned int ReadPhysicalRecord(Slice* result);   //读取Record，返回记录类型，或者kEof、kBadRecord

  void ReportCorruption(uint64_t bytes, const char* reason);    //汇报丢弃的字节，以及原因
  void ReportDrop(uint64_t bytes, const Status&amp;amp; reason);        //汇报丢弃的字节，以及原因
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些成员函数中着重看一下&lt;code&gt;ReadPhysicalRecord&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsigned int Reader::ReadPhysicalRecord(Slice* result) {
  while (true) {
    if (buffer_.size() &amp;lt; kHeaderSize) {
      if (!eof_) {
        // Last read was a full read, so this is a trailer to skip
        buffer_.clear();
        Status status = file_-&amp;gt;Read(kBlockSize, &amp;amp;buffer_, backing_store_);
        end_of_buffer_offset_ += buffer_.size();
        if (!status.ok()) {
          buffer_.clear();
          ReportDrop(kBlockSize, status);
          eof_ = true;
          return kEof;
        } else if (buffer_.size() &amp;lt; kBlockSize) {
          eof_ = true;
        }
        continue;
      } else {
        // Note that if buffer_ is non-empty, we have a truncated header at the
        // end of the file, which can be caused by the writer crashing in the
        // middle of writing the header. Instead of considering this an error,
        // just report EOF.
        buffer_.clear();
        return kEof;
      }
    }

    // Parse the header
    const char* header = buffer_.data();
    const uint32_t a = static_cast&amp;lt;uint32_t&amp;gt;(header[4]) &amp;amp; 0xff;
    const uint32_t b = static_cast&amp;lt;uint32_t&amp;gt;(header[5]) &amp;amp; 0xff;
    const unsigned int type = header[6];
    const uint32_t length = a | (b &amp;lt;&amp;lt; 8);
    if (kHeaderSize + length &amp;gt; buffer_.size()) {
      size_t drop_size = buffer_.size();
      buffer_.clear();
      if (!eof_) {
        ReportCorruption(drop_size, &quot;bad record length&quot;);
        return kBadRecord;
      }
      // If the end of the file has been reached without reading |length| bytes
      // of payload, assume the writer died in the middle of writing the record.
      // Don&apos;t report a corruption.
      return kEof;
    }

    if (type == kZeroType &amp;amp;&amp;amp; length == 0) {
      // Skip zero length record without reporting any drops since
      // such records are produced by the mmap based writing code in
      // env_posix.cc that preallocates file regions.
      buffer_.clear();
      return kBadRecord;
    }

    // Check crc
    if (checksum_) {
      uint32_t expected_crc = crc32c::Unmask(DecodeFixed32(header));
      uint32_t actual_crc = crc32c::Value(header + 6, 1 + length);
      if (actual_crc != expected_crc) {
        // Drop the rest of the buffer since &quot;length&quot; itself may have
        // been corrupted and if we trust it, we could find some
        // fragment of a real log record that just happens to look
        // like a valid log record.
        size_t drop_size = buffer_.size();
        buffer_.clear();
        ReportCorruption(drop_size, &quot;checksum mismatch&quot;);
        return kBadRecord;
      }
    }

    buffer_.remove_prefix(kHeaderSize + length);

    // Skip physical record that started before initial_offset_
    if (end_of_buffer_offset_ - buffer_.size() - kHeaderSize - length &amp;lt;
        initial_offset_) {
      result-&amp;gt;clear();
      return kBadRecord;
    }

    *result = Slice(header + kHeaderSize, length);
    return type;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reader类中公共的成员函数需要注意的有: &lt;code&gt;bool ReadRecord(Slice* record, std::string* scratch);&lt;/code&gt; &lt;code&gt;uint64_t LastRecordOffset();&lt;/code&gt; ReadRecord用来读取下一个要读取的Record，如果读取成功，返回true,存储在record中，如果读到了结尾没有一个完整的Record，则存储在scratch中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool Reader::ReadRecord(Slice* record, std::string* scratch) {
  if (last_record_offset_ &amp;lt; initial_offset_) {
    if (!SkipToInitialBlock()) {    //跳到初始的偏移位置
      return false;
    }
  }

  scratch-&amp;gt;clear();
  record-&amp;gt;clear();
  bool in_fragmented_record = false;
  // Record offset of the logical record that we&apos;re reading
  // 0 is a dummy value to make compilers happy
  uint64_t prospective_record_offset = 0;

  Slice fragment;
  while (true) {
    const unsigned int record_type = ReadPhysicalRecord(&amp;amp;fragment);

    // ReadPhysicalRecord may have only had an empty trailer remaining in its
    // internal buffer. Calculate the offset of the next physical record now
    // that it has returned, properly accounting for its header size.
    uint64_t physical_record_offset =
        end_of_buffer_offset_ - buffer_.size() - kHeaderSize - fragment.size();

    if (resyncing_) {
      if (record_type == kMiddleType) {
        continue;
      } else if (record_type == kLastType) {
        resyncing_ = false;
        continue;
      } else {
        resyncing_ = false;
      }
    }

    switch (record_type) {
      case kFullType:
        if (in_fragmented_record) {
          // Handle bug in earlier versions of log::Writer where
          // it could emit an empty kFirstType record at the tail end
          // of a block followed by a kFullType or kFirstType record
          // at the beginning of the next block.
          if (scratch-&amp;gt;empty()) {
            in_fragmented_record = false;
          } else {
            ReportCorruption(scratch-&amp;gt;size(), &quot;partial record without end(1)&quot;);
          }
        }
        prospective_record_offset = physical_record_offset;
        scratch-&amp;gt;clear();
        *record = fragment;
        last_record_offset_ = prospective_record_offset;
        return true;

      case kFirstType:
        if (in_fragmented_record) {
          // Handle bug in earlier versions of log::Writer where
          // it could emit an empty kFirstType record at the tail end
          // of a block followed by a kFullType or kFirstType record
          // at the beginning of the next block.
          if (scratch-&amp;gt;empty()) {
            in_fragmented_record = false;
          } else {
            ReportCorruption(scratch-&amp;gt;size(), &quot;partial record without end(2)&quot;);
          }
        }
        prospective_record_offset = physical_record_offset;
        scratch-&amp;gt;assign(fragment.data(), fragment.size());
        in_fragmented_record = true;
        break;

      case kMiddleType:
        if (!in_fragmented_record) {
          ReportCorruption(fragment.size(),
                           &quot;missing start of fragmented record(1)&quot;);
        } else {
          scratch-&amp;gt;append(fragment.data(), fragment.size());
        }
        break;

      case kLastType:
        if (!in_fragmented_record) {
          ReportCorruption(fragment.size(),
                           &quot;missing start of fragmented record(2)&quot;);
        } else {
          scratch-&amp;gt;append(fragment.data(), fragment.size());
          *record = Slice(*scratch);
          last_record_offset_ = prospective_record_offset;
          return true;
        }
        break;

      case kEof:
        if (in_fragmented_record) {
          // This can be caused by the writer dying immediately after
          // writing a physical record but before completing the next; don&apos;t
          // treat it as a corruption, just ignore the entire logical record.
          scratch-&amp;gt;clear();
        }
        return false;

      case kBadRecord:
        if (in_fragmented_record) {
          ReportCorruption(scratch-&amp;gt;size(), &quot;error in middle of record&quot;);
          in_fragmented_record = false;
          scratch-&amp;gt;clear();
        }
        break;

      default: {
        char buf[40];
        snprintf(buf, sizeof(buf), &quot;unknown record type %u&quot;, record_type);
        ReportCorruption(
            (fragment.size() + (in_fragmented_record ? scratch-&amp;gt;size() : 0)),
            buf);
        in_fragmented_record = false;
        scratch-&amp;gt;clear();
        break;
      }
    }
  }
  return false;
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>c++</category><category>levelDB源码阅读</category><category>leveldb</category><category>log</category><author>joyme123</author></item><item><title>leveldb文档翻译(3)-leveldb的日志格式和表格式</title><link>https://www.myway5.com/blog/leveldb-log-table/</link><guid isPermaLink="true">https://www.myway5.com/blog/leveldb-log-table/</guid><description>日志文件的内容是一个32KB大小的块的序列。唯一可能的意外是日志文件结尾可能包含一个不完整的块。 每一个块由以下记录组成</description><pubDate>Mon, 17 Jul 2017 15:43:03 GMT</pubDate><content:encoded>&lt;h2&gt;日志文件的格式&lt;/h2&gt;
&lt;p&gt;日志文件的内容是一个32KB大小的块的序列。唯一可能的意外是日志文件结尾可能包含一个不完整的块。 每一个块由以下记录组成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;block := record* trailer?
record :=
  checksum: uint32     // crc32c of type and data[] ; little-endian
  length: uint16       // little-endian
  type: uint8          // One of FULL, FIRST, MIDDLE, LAST
  data: uint8[length]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（注：crc32c是一种检验算法） 一条记录绝不会在块的最后6个字节内开始（因为剩下的6个字节已经不够大，checksum,length,type总共需要7个字节）。任何留下的字节形成trailer(拖车?),这部分必须完全是0字节，读取的时候必须跳过。 注：如果当前块恰好有7个字节留下来，一个新的非0长度的记录被增加，写入必须用一个FIRST记录（包含0字节的用户数据，FIRST用来表示这是一条记录的开始部分）去填充块的最后7个字节，之后将所有的用户数据写在后来的块中。 更多的类型可能在以后会加入进来。一些读取程序可能会跳过它们不认识的记录类型，其他读取程序可能会报告有些数据被跳过了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FULL == 1
FIRST == 2
MIDDLE == 3
LAST == 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FULL记录包含一个完整的用户记录的内容 FIRST，MIDDLE，LAST是用来表示用户记录被分成多个片段（尤其是块的边界）。FIRST是用户记录的第一个片段，LAST是用户记录的最后一个片段，MIDDLE是所有用户数据的内部片段。 例如:有以下的用户数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A: length 1000
B: length 97270
C: length 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;将会在第一块中存储为FULL记录。 &lt;strong&gt;B&lt;/strong&gt;将会划分成3段：第一段占用第一块剩下的部分，第二段占用整个第二块，第三段占用第三块的前面部分。这将会在第三块中留下6个字节的空间，被留作空白当成trailer。 &lt;strong&gt;C&lt;/strong&gt;将会在第四块中存储为一个FULL记录。&lt;/p&gt;
&lt;h3&gt;这种记录格式的优点&lt;/h3&gt;
&lt;p&gt;1.我们不需要任何的同步的启发式算法-直接跳到下一个块的边界并且扫描。如果有一个错误发生，跳到下一块。作为一个边界效益，当一个日志文件的内容的一部分被作为一个记录嵌入到另外一个日志文件中，我们不会变的困惑。 2.在近似边界上分割（比如mapreduce）是简单的：找到下一个块的边界，跳过记录直到我们遇到一个FULL或FIRST记录。 3.对于大的记录我们不需要额外的缓冲区。&lt;/p&gt;
&lt;h3&gt;对比这个记录格式的缺点&lt;/h3&gt;
&lt;p&gt;1.没有对微小的记录的包装。这可能通过添加新的记录类型来修复，因此它只是当前实现的短处，不是这种格式的必须。 2.没有压缩。同样的，这也可以通过添加新的记录格式来修复。&lt;/p&gt;
&lt;h2&gt;表文件的格式&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;beginning_of_file&amp;gt;
[data block 1]
[data block 2]
...
[data block N]
[meta block 1]
...
[meta block K]
[metaindex block]
[index block]
[Footer]        (fixed size; starts at file_size - sizeof(Footer))
&amp;lt;end_of_file&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个文件包含内部的指针。每一个指针都被称作BlockHandle，包含下面的信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;offset:   varint64
size:     varint64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考&lt;a href=&quot;https://developers.google.com/protocol-buffers/docs/encoding#varints&quot; rel=&quot;noopener&quot;&gt;varints&lt;/a&gt;,里面有varint64格式的解释。 1.键值对序列是有序存储的，被分成多个数据块。这些数据块从文件开头一个接着一个。每一个数据块被&lt;code&gt;block_builder.cc&lt;/code&gt;中的代码格式化，之后有选择的进行压缩。 2.在数据块之后我们存储一束元块。支持的元块类型在下面描述了。更多的元块类型可能在以后会加进来。每一个元块类型也是用&lt;code&gt;block_builder.cc&lt;/code&gt;格式化，然后之后被有选择的压缩。 3.一个“元索引”块。它包含每一个其他的元块的入口，入口的键是这个元块的名字，值是一个BlockHandle指向元块。 4.一个“索引”块。这个块包含每个数据块的入口，键是一个大于等于当前数据块的最后一个键的字符串，在连续的数据块的第一个键之前。值是这个数据块的BlockHandle。 5.在文件的最后面是一个填充满长度的脚部，包含了元索引块和索引块的BlockHandle，还有一个魔术数字。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    metaindex_handle: char[p];     // Block handle for metaindex
    index_handle:     char[q];     // Block handle for index
    padding:          char[40-p-q];// zeroed bytes to make fixed length
                                   // (40==2*BlockHandle::kMaxEncodedLength)
    magic:            fixed64;     // == 0xdb4775248b80fb57 (little-endian)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;“filter”元块&lt;/h3&gt;
&lt;p&gt;在数据库打开时如果 &lt;code&gt;FilterPolicy&lt;/code&gt;被指定，每个表都会有一个filter块。“元索引”块包含一个入口，将&lt;code&gt;filter.&amp;lt;N&amp;gt;&lt;/code&gt;指向了 BlockHandle，对于filter块，&lt;code&gt;&amp;lt;N&amp;gt;&lt;/code&gt;是filter 策略的 &lt;code&gt;Name()&lt;/code&gt; 方法返回的。 filter块存储了一系列的filters，filter i包含了&lt;code&gt;FilterPolicy::CreateFilter()&lt;/code&gt;在所有存储在文件偏移落入到范围[ i_base ... (i+1)_base-1 ]的块中键的输出。 当前，&quot;base&quot;是2KB，对这个例子来说，如果块X和Y开始在范围 &lt;code&gt;[ 0KB .. 2KB-1 ]&lt;/code&gt;,所有的在X和Y之间的键将会通过调用&lt;code&gt;FilterPolicy::CreateFilter()&lt;/code&gt;被转换成一个filter，得到的filter将会被存储成当前filter块的第一个filter。 filter block是以下格式的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[filter 0]
[filter 1]
[filter 2]
...
[filter N-1]

[offset of filter 0]                  : 4 bytes
[offset of filter 1]                  : 4 bytes
[offset of filter 2]                  : 4 bytes
...
[offset of filter N-1]                : 4 bytes

[offset of beginning of offset array] : 4 bytes
lg(base)                              : 1 byte
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;filter块的最后的偏移数组允许高效地mapping一个数据块偏移到相应的filter。&lt;/p&gt;
&lt;h2&gt;&quot;统计&quot;元块&lt;/h2&gt;
&lt;p&gt;这个元块包含一束统计。键是统计项的名字。值包含着统计内容。 TODO(预览):记录了以下的统计&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data size
index size
key size (uncompressed)
value size (uncompressed)
number of entries
number of data blocks
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>c++</category><category>levelDB源码阅读</category><category>leveldb</category><author>joyme123</author></item><item><title>leveldb文档翻译(2)-leveldb的实现</title><link>https://www.myway5.com/blog/leveldb/</link><guid isPermaLink="true">https://www.myway5.com/blog/leveldb/</guid><description>这部分是来自于leveldb的impl.md文档，详细介绍了leveldb实现方面的设计。 1、文件</description><pubDate>Mon, 17 Jul 2017 15:41:57 GMT</pubDate><content:encoded>&lt;p&gt;这部分是来自于leveldb的impl.md文档，详细介绍了leveldb实现方面的设计。&lt;/p&gt;
&lt;h2&gt;1、文件&lt;/h2&gt;
&lt;p&gt;leveldb其实是Bigtable的简单的实现。Bigtable论文地址:&lt;a href=&quot;http://research.google.com/archive/bigtable.html&quot; rel=&quot;noopener&quot;&gt;http://research.google.com/archive/bigtable.html&lt;/a&gt;。当然，文件的组织方法和bigtable有一些不同之处，下面会逐个介绍。&lt;/p&gt;
&lt;h3&gt;1.1 日志文件&lt;/h3&gt;
&lt;p&gt;一个日志文件(*.log)存储一系列的最近的更新。每一个更新都会添加到当前日志文件的最后。当所有日志文件达到了预定义的大小（默认是4M），它会转换成一个有序的表（参见下面介绍），一个新的日志文件被创建提供给之后的更新。 当前日志文件的副本是保存在一个内存结构中（称为&lt;code&gt;memtable&lt;/code&gt;）。每一次读都会向这个&lt;code&gt;memtable&lt;/code&gt;中查询，所以读操作其实还是映射到所有记录下来的更新中。&lt;/p&gt;
&lt;h2&gt;2.有序表&lt;/h2&gt;
&lt;p&gt;一个有序表(*.ldb)存储了一系列的由键排序的键值对。每个键值对要么键对应着值，要么是键对应着一个删除标记。（删除标记保存在旧的排序表中存储着过时的值）。 排序表的集合是由一系列的等级(level)组织的。排序表如果是由一个日志文件生成，就会被置为一个特别的&lt;strong&gt;young&lt;/strong&gt;level(也被称为level-0)。当young文件的数量超过了某个特定的阈值（当前是4），所有的young level文件和所有键范围重叠的level-1文件被合并在一起产生一系列的新的level-1文件（我们每2MB的数据创建一个新的level-1文件） 属于young level的文件可能自己相互包含重叠的键，但属于其他等级的文件有唯一的不重叠的键范围。level-L(L &amp;gt;= 1)，当level-L所有的文件大小超过了(10^L)MB（例如,level-1是10MB,level2是100MB），在level-L中取一个文件，和所有在level-(L+1)中与之重叠的文件，合并成level-(L+1)的新的文件集合。这些合并使得新的更新逐渐从young-level的文件转移到最大的等级,这个过程只需要批量的读写操作即可完成(注：最小的时间复杂度)&lt;/p&gt;
&lt;h3&gt;2.1 Manifest&lt;/h3&gt;
&lt;p&gt;一个MANIFEST文件中个列举了由各个level组成的有序表的集合,包含了相应的键范围，以及其他重要的元数据。任何时候数据库被打开，一个新的MANIFEST文件（文件名中有一个新的数字代表）被创建。MANIFEST文件和log文件的格式相同，任何发生的改变都会被添加到这个文件中（比如文件添加或者移除）。&lt;/p&gt;
&lt;h3&gt;2.2 Current&lt;/h3&gt;
&lt;p&gt;CURRENT是一个简单的文本文件，包含了最新的MANIFEST文件的名字。&lt;/p&gt;
&lt;h3&gt;2.3 信息日志（Info logs）&lt;/h3&gt;
&lt;p&gt;信息消息被打印到LOG和LOG.old文件中&lt;/p&gt;
&lt;h3&gt;2.4 其他&lt;/h3&gt;
&lt;p&gt;其他文件用来记录一些杂项。（LOCK，*.dbtmp)&lt;/p&gt;
&lt;h2&gt;3.Level 0&lt;/h2&gt;
&lt;p&gt;当一个日志文件增长到一个特定的值（默认1MB）： 创建一个新的memtable和log文件，并将未来的更新放在这里。 在后台的操作： 将上一个memtable的内容写入到sstable 丢弃上一个memtable 删除旧的日志文件和旧的memtable 将新的sstable添加到young(level 0) level。&lt;/p&gt;
&lt;h2&gt;4.压实（不是对数据进行压缩，只是将数据整合到一起，并且在这个过程中会去除重复键以及删除的键）&lt;/h2&gt;
&lt;p&gt;当level L超出了文件大小的限制，我们在另外一个后台线程中压实它。这个压实操作选择level L中的一个文件以及来自下一个level L+1的所有与之重叠的文件。注意：如果一个level-L文件只和一个level-(L+1)文件部分重叠，这个文件的所有部分都被作为压实的输入，然后在压实之后被丢弃。例外：因为level-0是特别的（level-0的文件可能彼此重叠），我们特殊对待从level-0到level-1的压实：当这些level-0文件彼此重叠时，一个level-0的压实会选择多个level-0的文件。 一个压实操作合并选择的文件的内容，产生一系列的level-(L+1)文件。在当前的输出文件超过目标文件大小(2MB)时，我们切换到一个产生的新的level-(L+1)文件。在当前输出文件的键范围增长到足够大，以致于和超过10个level-(L+2)的文件产生重叠时，我们同样切换到产生的新的输出文件。最后一条准则保证了之后的一次level-(L+1)文件压实不会从level-(L+2)中选择太多的数据。 旧的文件被丢弃，新的文件被加入到服务状态。 特定level的压实通过键空间的压实。详细来说，对于每个level L，我们记住最后一次level L的压实的结尾的键。下一次level L的压实将会选择在这个键之后的第一个文件（如果没有这个文件，就从键空间的起点环绕。 压实会丢弃覆盖的值，如果没有更大的数字level包含一个范围重叠当前键的文件，也会丢弃删除标记。&lt;/p&gt;
&lt;h3&gt;4.1 时间复杂度&lt;/h3&gt;
&lt;p&gt;level-0的压实将会从level-0中读取最多4个1MB的文件,在最坏情况下读取所有的level-1文件(10MB)。这样一来，我们将会读取14MB,写入14MB。 对于其他等级的压实，我们将会从level L中选择一个2MB的文件。最坏的情况下，将会从level L+1中重叠大概12个文件（10个文件是因为level-(L+1)是level-L的10倍大，其他在两边的两个文件是因为level L的文件范围通常和level-(L+1)的文件范围是不对齐的）。压实因此会读取26MB，写入26MB。假设磁盘IO速度为100MB/s，最差的压实大概消耗0.5s 如果我们减慢后台写入，只取全速100MB/s的10%，一次压实可能耗时5s。如果用户的写入速度为10MB/s，我们可能创建许多level-0的文件（大概5*10MB总共50MB）。这将明显的增加读取消耗，因为每次读取合并了更多的文件增加了负载。 解决方案1：为了减轻这个问题，我们可能希望当level-0文件数很大的时候，增加日志切换的阈值。虽然缺点是这个阈值越大，需要越多的内存来保存相应的memtable。 解决方案2:我们可能希望当level-0的文件数变多时，人为增加写入速度。 解决方案3：我们致力于降低大量文件合并的成本。也许大多数level-0文件在缓存中都有未压缩的块，我们仅仅需要担心O(N)复杂度的合并迭代。&lt;/p&gt;
&lt;h3&gt;4.2 文件的数量&lt;/h3&gt;
&lt;p&gt;除了总是生成2MB的文件，我们也可以为更高的等级生成更大的文件，以此减少文件的总数，虽然代价是更多突发的压缩。当然，我们也可以将文件集合分片放在不同的文件夹中。 在2011年2月4日，在ex3文件系统上做了一次测试，该测试显示，在很多文件的目录中做100K次文件打开的平均用时如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Files in directory&lt;/th&gt;
&lt;th&gt;Microseconds to open a file&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;因此，在现代文件系统中，也许连分片也是不需要的。&lt;/p&gt;
&lt;h2&gt;5.恢复&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;读取CURRENT去找到最后一次提交的MANIFEST文件名&lt;/li&gt;
&lt;li&gt;读取该MANIFEST文件&lt;/li&gt;
&lt;li&gt;清除过期的文件&lt;/li&gt;
&lt;li&gt;我们可以在这里打开所有的sstables，但是懒加载可能会更好。&lt;/li&gt;
&lt;li&gt;将log块转换成新的level-0 sstable&lt;/li&gt;
&lt;li&gt;开始将恢复的序列导向到新的日志文件的新的写入流。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6.文件的垃圾回收&lt;/h2&gt;
&lt;p&gt;每次恢复的最后以及每次压缩的最后&lt;code&gt;DeleteObsoleteFiles()&lt;/code&gt;会被调用。它会找出数据库中所有的文件的名字。然后删除所有的不是当前日志文件的日志文件。再删除所有的不涉及到某些level和不是一个活跃的压缩输出的表文件。&lt;/p&gt;
</content:encoded><category>c++</category><category>levelDB源码阅读</category><category>leveldb</category><author>joyme123</author></item><item><title>leveldb文档翻译(1)-leveldb概览</title><link>https://www.myway5.com/blog/leveldb-intro/</link><guid isPermaLink="true">https://www.myway5.com/blog/leveldb-intro/</guid><description>这是leveldb源代码包中提供的 readme.md，index.md 的部分翻译文档，不是逐字逐句翻译，只是把自己感兴趣的部分翻译出来了，作为看代码之前的准备工作。 1.公共接口</description><pubDate>Mon, 17 Jul 2017 15:24:30 GMT</pubDate><content:encoded>&lt;p&gt;这是leveldb源代码包中提供的 readme.md，index.md 的部分翻译文档，不是逐字逐句翻译，只是把自己感兴趣的部分翻译出来了，作为看代码之前的准备工作。&lt;/p&gt;
&lt;h2&gt;1.公共接口&lt;/h2&gt;
&lt;p&gt;include文件夹下面的是公共接口，所有使用者应该从这里的头文件直接调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;include/db.h:数据库的主接口:从这里开始&lt;/li&gt;
&lt;li&gt;include/options.h:控制整个数据的行为，也控制数据库的读写，可以看做是配置文件。&lt;/li&gt;
&lt;li&gt;include/comparator.h:用户自定义的比较函数的抽象。如果想要使用基于字节的方法对键大小进行比较，可以使用默认的比较器，用户也可以自己实现比较器来按自己的想法对存储进行排序。&lt;/li&gt;
&lt;li&gt;include/iterator.h:遍历数据的接口。可以从DB对象中获取这样一个迭代器。&lt;/li&gt;
&lt;li&gt;include/write_batch.h:对数据库原子的进行多个更新操作的接口。&lt;/li&gt;
&lt;li&gt;include/slice.h:一个简单的模块，用来维护一个指针和长度，来表示一个字节数组。&lt;/li&gt;
&lt;li&gt;include/status.h:用来汇报成功或是其他各种各样的错误。&lt;/li&gt;
&lt;li&gt;include/env.h:操作系统环境的抽象。这个接口的posix实现是在util/env_posix.cc中&lt;/li&gt;
&lt;li&gt;include/table.h,include/table_builder.h:低层次的模块，绝大多数客户端不会使用这个接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.leveldb基本使用&lt;/h2&gt;
&lt;p&gt;leveldb提供持久化的键值存储。键和值是任意的字节数组。键值对的存储是按照键的顺序存储的，并且这个顺序是按照用户自定义的排序函数来的。&lt;/p&gt;
&lt;h3&gt;2.1 打开一个数组库&lt;/h3&gt;
&lt;p&gt;leveldb存储是使用文件系统中的文件夹。所有数据库内容都会存储在这个文件夹里。下面是一个打开数据库的例子，并且如果数据库不存在会自动创建:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;cassert&amp;gt;
#include &quot;leveldb/db.h&quot;

leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options, &quot;/tmp/testdb&quot;, &amp;amp;db);
assert(status.ok());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望在数据库已经存在时报错，在&lt;code&gt;leveldb::DB::Open&lt;/code&gt;前调用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;options.error_if_exists = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 状态&lt;/h3&gt;
&lt;p&gt;上面的代码中使用了leveldb::Status类型，leveldb中大多数函数都会返回这个值。你可以检查这个值是不是&lt;code&gt;ok&lt;/code&gt;,或者打印出相关的错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leveldb::Status s = ....;
if (!s.ok()) cerr &amp;lt;&amp;lt; s.ToString() &amp;lt;&amp;lt; endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 关闭数据库&lt;/h3&gt;
&lt;p&gt;如果数据库操作结束后，删除数据库对象即可关闭&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.... open the db as described above ...
.... do something with db ....
delete db;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 读写&lt;/h3&gt;
&lt;p&gt;数据库提供了Put、Delete、Get方法来修改/查询数据库。例如，下面的代码用来将key1的值移动到key2中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::string value;
leveldb::Status s = db-&amp;gt;Get(leveldb::ReadOptions(), key1, &amp;amp;value);
if (s.ok()) s = db-&amp;gt;Put(leveldb::WriteOptions(), key2, value);
if (s.ok()) s = db-&amp;gt;Delete(leveldb::WriteOptions(), key1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.5 原子更新&lt;/h3&gt;
&lt;p&gt;上面的例子中，如果在Put完key2的值，Delete key1的值前，进程崩溃了，同一个值就会被存储在多个key下。使用&lt;code&gt;WriteBatch&lt;/code&gt;类可以原子的执行多个更新操作:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;leveldb/write_batch.h&quot;
....
std::string value;
leveldb::Status s = db-&amp;gt;Get(leveldb::ReadOptions(), key1, &amp;amp;value);
if (s.ok()) {
  leveldb::WriteBatch batch;
  batch.Delete(key1);
  batch.Put(key2, value);
  s = db-&amp;gt;Write(leveldb::WriteOptions(), &amp;amp;batch);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;WriteBatch&lt;/code&gt;存储了一系列的对数据库的编辑操作，这些操作会被顺序执行。注意到我们先执行了&lt;code&gt;Delete&lt;/code&gt;,再执行&lt;code&gt;Put&lt;/code&gt;,这样当key1和key2相同时，我们不会错误的删除所有的值。 &lt;code&gt;WirteBatch&lt;/code&gt;不仅仅有原子操作的特点，还可以用来加速大量的更新操作。原理是将大量的单独的变化放入同一个批操作中。&lt;/p&gt;
&lt;h3&gt;2.6 同步写入&lt;/h3&gt;
&lt;p&gt;默认情况下,对leveldb的写入都是异步的:它在将进程中的写操作提交给操作系统后返回。数据从操作系统内存转移到持久化存储这个过程是异步的（也就是和这里不会等持久化之后才返回）。使用同步写入，可以让某些特定的写入等到数据被写入持久化存储后才返回。（在Posix系统中，这个通过调用 &lt;code&gt;fsync(....)&lt;/code&gt; 或 &lt;code&gt;fdatasync(....)&lt;/code&gt; 或 &lt;code&gt;msync(...., MS_SYNC)&lt;/code&gt; 来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leveldb::WriteOptions write_options;
write_options.sync = true;
db-&amp;gt;Put(write_options, ....);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;异步写入通常情况下是千百倍的快于同步写入。异步写入的缺点是如果机器崩溃可能会造成最后的一些更新丢失。注意仅仅是写进程的崩溃不会造成任何数据丢失，即使没有使用同步写。因为写进程只负责将写操作提交给操作系统，提交完就意味着写入已经结束（即使这个时候没有真正的写入到持久化存储中）。 异步写大多数时候都能被安全使用。比如，当载入大量数据到数据库中，如果出现崩溃你可以重新开始这个载入操作。也可以使用混合写入模式，也就是每多少个异步写之后使用一次同步写。如果崩溃了，可以从上一次同步写开始（同步写会产生一个标记，所以可以从上一次同步写开始）。 &lt;code&gt;WriteBatch&lt;/code&gt;对于异步写提供了多个选择。多个更新操作也被放在同一个批操作中，然后使用一个同步写被一起执行（&lt;code&gt;write_options.sync&lt;/code&gt;设置为true）。同步写产生的额外性能消耗被均摊在所有的写操作上。&lt;/p&gt;
&lt;h3&gt;2.7 并发&lt;/h3&gt;
&lt;p&gt;数据库在同一时间只能由一个进程打开。leveldb使用了操作系统的锁来防止被错误使用。在一个单进程中，同一个&lt;code&gt;leveldb::DB&lt;/code&gt;对象可以被多个并发线程安全的共享。不同的线程之间在对同一个数据库对象写入或者遍历或者调用Get时，不需要额外的同步操作（leveldb的实现会自动做这个必要的同步操作。然而其他对象(比如Iterator和&lt;code&gt;WriteBatch&lt;/code&gt;)可能需要额外的同步。如果两个线程共享了这样的对象，它们必须使用它们自己的锁协议来保护对象的使用。更多的细节可以在公共头文件中看到。&lt;/p&gt;
&lt;h3&gt;2.8 迭代器(Iteration)&lt;/h3&gt;
&lt;p&gt;下面的例子展示了如何打印一个数据库中所有的键值对。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leveldb::Iterator* it = db-&amp;gt;NewIterator(leveldb::ReadOptions());
for (it-&amp;gt;SeekToFirst(); it-&amp;gt;Valid(); it-&amp;gt;Next()) {
  cout &amp;lt;&amp;lt; it-&amp;gt;key().ToString() &amp;lt;&amp;lt; &quot;: &quot;  &amp;lt;&amp;lt; it-&amp;gt;value().ToString() &amp;lt;&amp;lt; endl;
}
assert(it-&amp;gt;status().ok());  // Check for any errors found during the scan
delete it;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面的例子展示了如何显示一个特定范围的数据 [start,limit):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (it-&amp;gt;Seek(start);
   it-&amp;gt;Valid() &amp;amp;&amp;amp; it-&amp;gt;key().ToString() &amp;lt; limit;
   it-&amp;gt;Next()) {
  ....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你也可以倒序遍历所有的数据（倒序比正序可能要慢)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (it-&amp;gt;SeekToLast(); it-&amp;gt;Valid(); it-&amp;gt;Prev()) {
  ....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.9 快照(Snapshots)&lt;/h3&gt;
&lt;p&gt;快照提供了键值存储所有状态一致的只读视图。&lt;code&gt;ReadOptions::snapshot&lt;/code&gt;可能是non-NULL的值，来表示一个读操作应该执行在特定的DB状态版本上。如果 &lt;code&gt;ReadOptions::snapshot&lt;/code&gt;是NULL,读操作可能执行在当前状态的隐式快照上。 快照的创建:&lt;code&gt;DB::GetSnapshot()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leveldb::ReadOptions options;
options.snapshot = db-&amp;gt;GetSnapshot();
.... apply some updates to db ....
leveldb::Iterator* iter = db-&amp;gt;NewIterator(options);
.... read using iter to view the state when the snapshot was created ....
delete iter;
db-&amp;gt;ReleaseSnapshot(options.snapshot);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当一个快照不需要的时候，使用&lt;code&gt;DB::ReleaseSnapshot&lt;/code&gt;接口来释放。&lt;/p&gt;
&lt;h3&gt;2.10 片（Slice）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;*Slice不知道翻译成什么*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;it-&amp;gt;key()&lt;/code&gt;和&lt;code&gt;it-&amp;gt;value()&lt;/code&gt;的返回值都调用了&lt;code&gt;leveldb::Slice&lt;/code&gt;类型的实例。Slice是一个简单的结构：包含字节数组的长度和指针。返回一个Slice比返回&lt;code&gt;std::string&lt;/code&gt;是一个更轻量划算的选择,因为我们不需要复制潜在的很大的键和值（返回的是指针和长度）。额外的，leveldb的方法不返回null-terminated（\0结尾）的 C-style 字符串，因为leveldb的键和值是允许存储&lt;code&gt;&apos;\0&apos;&lt;/code&gt;字节的。 Slice和C++字符串以及C-style字符串之间可以互相转换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leveldb::Slice s1 = &quot;hello&quot;;

std::string str(&quot;world&quot;);
leveldb::Slice s2 = str;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;std::string str = s1.ToString();
assert(str == std::string(&quot;hello&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用Slice时要小心，因为Slice取决于调用者来保证Slice中存储的数据的生命周期。下面就是一段有问题的代码的示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leveldb::Slice slice;
if (....) {
  std::string str = ....;
  slice = str;
}
Use(slice);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当执行到if外面的时候，str被销毁，slice中的存储也就消失了(注:这是因为slice中存储的是指针和长度)。&lt;/p&gt;
&lt;h3&gt;2.11 比较器（Comparators）&lt;/h3&gt;
&lt;p&gt;前面的例子使用的都是默认的排序函数来对键做排序，是对字节做字典排序的。当打开数据库的时候，可以使用自定义的比较器来排序。例如，假设每个数据的键都由两个数字组成，我们应该使用第一个数字来排序，使用第二个数字来打破联系。首先，定义一个合适的&lt;code&gt;leveldb::Comparator&lt;/code&gt;的子类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TwoPartComparator : public leveldb::Comparator {
 public:
  // Three-way comparison function:
  //   if a &amp;lt; b: negative result
  //   if a &amp;gt; b: positive result
  //   else: zero result
  int Compare(const leveldb::Slice&amp;amp; a, const leveldb::Slice&amp;amp; b) const {
    int a1, a2, b1, b2;
    ParseKey(a, &amp;amp;a1, &amp;amp;a2);
    ParseKey(b, &amp;amp;b1, &amp;amp;b2);
    if (a1 &amp;lt; b1) return -1;
    if (a1 &amp;gt; b1) return +1;
    if (a2 &amp;lt; b2) return -1;
    if (a2 &amp;gt; b2) return +1;
    return 0;
  }

  // Ignore the following methods for now:
  const char* Name() const { return &quot;TwoPartComparator&quot;; }
  void FindShortestSeparator(std::string*, const leveldb::Slice&amp;amp;) const {}
  void FindShortSuccessor(std::string*) const {}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在使用这个比较器来打开数据库:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TwoPartComparator cmp;
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
options.comparator = &amp;amp;cmp;
leveldb::Status status = leveldb::DB::Open(options, &quot;/tmp/testdb&quot;, &amp;amp;db);
....
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.11.1 向后兼容&lt;/h4&gt;
&lt;p&gt;比较器的Name()方法的返回值在数据库被创建时就已经联系起来了，在之后的每次数据库打开时都会被检查。如果name改变了，&lt;code&gt;leveldb::DB::Open&lt;/code&gt;调用时会失败。因此，当且仅当新的键格式和比较函数和已经存在的数据库不兼容时，才会改变name的值，这时不得不丢弃所有已经存在的数据。 当然你也可以通过一些提前的计划来逐步的拓展你的键格式。例如，你可以存储一个版本号在每一个键的末尾（一个字节就够用了）。当你想要改变到新的键格式（例如添加第三个部分到键上） - (a) 保持同样的比较器名字 - (b) 增加新的键的版本号 - (c) 改变比较函数，使用键的版本号来决定如何解释这些键&lt;/p&gt;
&lt;h3&gt;2.12 性能&lt;/h3&gt;
&lt;p&gt;性能可以通过改变配置文件来调节 &lt;code&gt;include/leveldb/options.h&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;2.13 块大小&lt;/h3&gt;
&lt;p&gt;leveldb将相邻的键合成一组存储在同一个块中，这样的块是从持久化存储和内存间转移的最小单元。默认的块大小是大约4096个未压缩的字节。如果应用程序总是在数据库中做大量的扫描，可以考虑适当增加这个值的大小。应用程序做大量的很小的值的点读取，可以考虑使用小一点的块大小（如果这些设置确实提升了性能）。如果块大小小于1kilobyte,或者大于几个megabytes是没有太多好处的。注意对于更大的块大小使用压缩会更有效率。&lt;/p&gt;
&lt;h3&gt;2.14 压缩&lt;/h3&gt;
&lt;p&gt;每一个数据块在写入持久化存储前会单独的被压缩，压缩是默认的，因为默认的压缩函数是非常快的。对于不可压缩的数据会自动禁用。在极少数的例子中，程序可能会希望完全禁用压缩，仅仅当性能测试显示性能提升了才推荐这么做。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leveldb::Options options;
options.compression = leveldb::kNoCompression;
.... leveldb::DB::Open(options, name, ....) .....
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.15 缓存&lt;/h3&gt;
&lt;p&gt;数据库的内容被存储在文件系统的文件集合中。每一个文件存储了一系列的压缩的数据块。如果options.cache是non-NULL的，经常使用的未压缩的数据块内容将会被缓存。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;leveldb/cache.h&quot;

leveldb::Options options;
options.cache = leveldb::NewLRUCache(100 * 1048576);  // 100MB cache
leveldb::DB* db;
leveldb::DB::Open(options, name, &amp;amp;db);
.... use the db ....
delete db
delete options.cache;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意缓存保存了未压缩的数据，因此根据程序级的数据量来设置缓存的大小，使用压缩的数据不会有任何的减少。（压缩的数据块的存储留给操作系统缓冲区缓存，或者客户端的其他自定义环境变量来实现） 当执行大量的读取操作时，应用程序最好禁用缓存，这样大量的数据读取不会导致缓存内容大量被改变。per-iterator选项用来实现这个:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leveldb::ReadOptions options;
options.fill_cache = false;
leveldb::Iterator* it = db-&amp;gt;NewIterator(options);
for (it-&amp;gt;SeekToFirst(); it-&amp;gt;Valid(); it-&amp;gt;Next()) {
  ....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.15.1 键层（Key Layout）&lt;/h4&gt;
&lt;p&gt;注意硬盘中转移和缓存的单元是一个数据块。相邻的键（根据数据库排序顺序）将被放在同一个数据块中。因此应用程序通过将所有的键依次相邻的排放，将不常访问的值内容（通过key来索引）放在另外一个区域可以提升程序性能。 举例来说，假设我们正在在leveldb上实现一个简单的文件系统，下面的入口类型我们可能希望存储。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;filename -&amp;gt; permission-bits, length, list of file_block_ids
file_block_id -&amp;gt; data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可能想要设置filename的键前缀为一个字母(&apos;/&apos;)，&lt;code&gt;file_block_id&lt;/code&gt;的键前缀为另一个字母(&apos;0&apos;)，因此扫描这些元数据不会强制我们取出和缓存大量的文件内容。&lt;/p&gt;
&lt;h3&gt;2.16 过滤器&lt;/h3&gt;
&lt;p&gt;因为leveldb的数据在磁盘上有结构地存放，一个简单的&lt;code&gt;Get()&lt;/code&gt;可能调用多个从磁盘的读取。选项 &lt;code&gt;FilterPolicy&lt;/code&gt;机制可以被用来减少磁盘读取的次数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leveldb::Options options;
options.filter_policy = NewBloomFilterPolicy(10);
leveldb::DB* db;
leveldb::DB::Open(options, &quot;/tmp/testdb&quot;, &amp;amp;db);
.... use the database ....
delete db;
delete options.filter_policy;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码将一个基于过滤策略的布隆过滤器(Bloom filter)和数据库联系起来。布隆过滤器的基础过滤依赖于保存每个键的一些位的数据在内存中（在这个例子中每个键有10个bit)。这个过滤器将会减少Get()带来的不必要的磁盘读取的次数大概为100。增加保存的每个键的bit数会更明显的减少读取次数，但是这是在更多的内存消耗的代价上得来的（注：典型的以空间换时间）。我们建议应用程序工作时还剩很多的内存，并且会做大量的随机读取时，设置一个过滤策略。 (注：布隆过滤器是一个针对于大数据量的去重算法，它会通过多个hash将一个字节数组映射到一个很大的bit向量中的k个位置，通过检查一个字节数组hash后的k个位置对应的bit向量是否都为1，如果不是则这个字节数组没有出现过，否则出现过（会有少量的误报）。使用在这里，是为了在读取前检查这个键是否存在，如果不存在则避免了以此全数据库查询，从而大大的提升的性能） 如果你使用一个自定义的比较器，你应该保证你正在使用的过滤策略是和你比较器兼容的。例如，想象一个比较器比较键时忽略了末尾的空间，&lt;code&gt;NewBloomFilterPolicy&lt;/code&gt;不能和这样的比较器一起使用。相反的，应用程序需要提供一个自定义的过滤策略同样忽略这些末尾的空间。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class CustomFilterPolicy : public leveldb::FilterPolicy {
 private:
  FilterPolicy* builtin_policy_;

 public:
  CustomFilterPolicy() : builtin_policy_(NewBloomFilterPolicy(10)) {}
  ~CustomFilterPolicy() { delete builtin_policy_; }

  const char* Name() const { return &quot;IgnoreTrailingSpacesFilter&quot;; }

  void CreateFilter(const Slice* keys, int n, std::string* dst) const {
    // Use builtin bloom filter code after removing trailing spaces
    std::vector&amp;lt;Slice&amp;gt; trimmed(n);
    for (int i = 0; i &amp;lt; n; i++) {
      trimmed[i] = RemoveTrailingSpaces(keys[i]);
    }
    return builtin_policy_-&amp;gt;CreateFilter(&amp;amp;trimmed[i], n, dst);
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有一些应用程序可能提供其他过滤策略不使用布隆过滤器，而是使用其他的机制来处理键的集合。细节看&lt;code&gt;leveldb/filter_policy.h&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;2.17 校验（Checksums)&lt;/h3&gt;
&lt;p&gt;leveldb会校验所有存储在文件系统中的数据。这里有两个不相关的控制部分提供积极的校验： &lt;code&gt;ReadOptions::verify_checksums&lt;/code&gt;可被设置为true，强制校验从文件系统一次读取的所有数据。默认情况是不会做这个验证的。 &lt;code&gt;Options::paranoid_checks&lt;/code&gt;在打开数据库之前可被设置为true，来使得数据库一旦检查到内部错误立马报错。这取决于数据库的哪一部分出错，当数据库被打开时还是其他后来的操作时，这个错误会被警告。默认情况下，偏执检查是关闭的，因此即使有时候持久化存储已经出错了，数据库依然可以被使用。 如果一个数据库出错了（可能它在偏执检查开启时不能打开）， &lt;code&gt;leveldb::RepairDB&lt;/code&gt;方法会尽可能的修复数据库的数据。&lt;/p&gt;
&lt;h3&gt;2.18 近似大小&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;GetApproximateSizes&lt;/code&gt;方法可以用来通过一个或多个键范围来获取文件系统已用的空间的近似字节数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leveldb::Range ranges[2];
ranges[0] = leveldb::Range(&quot;a&quot;, &quot;c&quot;);
ranges[1] = leveldb::Range(&quot;x&quot;, &quot;z&quot;);
uint64_t sizes[2];
leveldb::Status s = db-&amp;gt;GetApproximateSizes(ranges, 2, sizes);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前面的代码调用将会设置&lt;code&gt;sizes[0]&lt;/code&gt;为键范围在[a..c)之间的数据使用的文件系统存储空间的大小，设置&lt;code&gt;sizes[1]&lt;/code&gt;为键范围在[x..z)之间的数据使用的文件系统存储空间的大小。&lt;/p&gt;
&lt;h3&gt;2.19 环境&lt;/h3&gt;
&lt;p&gt;所有的文件操作（以及其他的系统调用）都在&lt;code&gt;leveldb:Env&lt;/code&gt;中实现了。复杂的客户端可能希望提供它们自己的Env的实现来获得更好的控制。例如，一个应用程序可能会在文件IO中引入人为延迟，限制leveldb对系统其他活动的影响。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SlowEnv : public leveldb::Env {
  .... implementation of the Env interface ....
};

SlowEnv env;
leveldb::Options options;
options.env = &amp;amp;env;
Status s = leveldb::DB::Open(options, ....);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.20 移植(Porting)&lt;/h3&gt;
&lt;p&gt;leveldb可能通过提供被&lt;code&gt;leveldb/port/port.h&lt;/code&gt;导出的类型/方法/函数/的平台相关的实现，来移植到新的平台。详细内容参考&lt;code&gt;leveldb/port/port_example.h&lt;/code&gt; 另外的，新的平台可能需要新的默认&lt;code&gt;leveldb::Env&lt;/code&gt;的实现。详细内容参考&lt;code&gt;leveldb/util/env_posix.h&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;2.21 其他信息&lt;/h3&gt;
&lt;p&gt;其他信息可以参考其他的文档文件 1. impl.md 2. table_format.md 3. log_format.md&lt;/p&gt;
</content:encoded><category>c++</category><category>levelDB源码阅读</category><category>leveldb</category><author>joyme123</author></item><item><title>基于Jenkins的自动部署方案</title><link>https://www.myway5.com/blog/auto-deploy-based-jenkins/</link><guid isPermaLink="true">https://www.myway5.com/blog/auto-deploy-based-jenkins/</guid><description>在一家小公司工作，负责开发一个项目，前台用的angular2，后台是php和c++。每天下班之前都要将当天的代码部署的线上的开发服务器上，常常也需要更新到生产服务器（项目并没有正式运行，但是老板要用项目去拉投资）。手动部署代码总有一种浪费时间的感觉（因为下班时间到了啊！！！）。</description><pubDate>Tue, 04 Jul 2017 08:30:03 GMT</pubDate><content:encoded>&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;在一家小公司工作，负责开发一个项目，前台用的angular2，后台是php和c++。每天下班之前都要将当天的代码部署的线上的开发服务器上，常常也需要更新到生产服务器（项目并没有正式运行，但是老板要用项目去拉投资）。手动部署代码总有一种浪费时间的感觉（因为下班时间到了啊！！！）。使用Jenkins+shell脚本完成自动部署就可以避免这种情况了。感谢&lt;a href=&quot;http://kawabangga.com&quot; rel=&quot;noopener&quot;&gt;赖同学&lt;/a&gt;的指导&lt;/p&gt;
&lt;h2&gt;二、流程介绍&lt;/h2&gt;
&lt;p&gt;公司内网搭建了git服务器，线上的开发用的服务器部署在阿里云，但是这台服务器的配置实在是太低了，完全没法胜任在线编译部署的需求。所以我要做的是将代码提交到git服务器后，使用Jenkins从git的dev分支拉取代码，构建部署计划（手动或者使用钩子都行）。&lt;/p&gt;
&lt;h2&gt;三、Jenkins的使用&lt;/h2&gt;
&lt;p&gt;第一步:部署Jenkins，这一步很简单，直接从Jenkins的官网上wget Jenkins的war包，然后java -jar jenkins.war即可。可以配合supervisor来实现自动启动和重启。 第二步：访问http://ip:8080进行配置，第一次配置的密码是java -jar jenkins.war时，在控制台上输出的密文。然后新建项目，然后选择&lt;strong&gt;构建一个多配置项目&lt;/strong&gt; 第二步：进入到一个配置界面，填一下项目名称，其他的都不用管，直接来到源代码管理。我用的是Git服务器，所以选择git,然后填入仓库地址，&lt;img src=&quot;/uploads/wp/2017/07/2017-07-04-16-16-15%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE.png&quot; alt=&quot;配置项目&quot; /&gt;仓库地址可以是HTTP协议，也可以是SSH协议，然后记得在Credentials字段填入认证信息,HTTP协议可以是用户名和密码，SSH的话可以是秘钥。选择的是dev分支。之后在填写构建信息。&lt;img src=&quot;/uploads/wp/2017/07/2017-07-04-16-19-44%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE.png&quot; alt=&quot;构建信息&quot; /&gt;构建命令主要就是执行一个脚本 第三步：编写自动部署的脚本，主要就是编译，然后发送到远程服务器，这里用了scp来传输文件，非常好用。 脚本很简单，具体如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/bin/bash
## 自动部署脚本

cd homepage/mobile
cnpm install
ng build -prod -aot -env=prod
scp -r dist/* root@120.26.103.174:/alidata/www/m_family
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里的scp传输文件到远程服务器的前提是，远程服务器上.ssh文件夹下的authorized_keys中有Jenkins所在服务器的公钥，这样才能不需要密码通过认证。&lt;/p&gt;
</content:encoded><category>工作</category><category>Jenkins</category><category>自动部署</category><author>joyme123</author></item><item><title>朋友圈式的Timeline设计方案</title><link>https://www.myway5.com/blog/timeline-design/</link><guid isPermaLink="true">https://www.myway5.com/blog/timeline-design/</guid><description>几乎每个人都会发朋友圈、微博，一个看似简单的发布功能，实际在背后是经过精细设计的组织架构。这里有一篇关于新浪微博的架构设计的演讲，主要是讲了通过redis+缓存的使用，来实现发微博的实时性和高效性。 二、Timeline</description><pubDate>Thu, 29 Jun 2017 07:07:49 GMT</pubDate><content:encoded>&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;几乎每个人都会发朋友圈、微博，一个看似简单的发布功能，实际在背后是经过精细设计的组织架构。&lt;a href=&quot;http://www.infoq.com/cn/presentations/ywh-build-high-performance-weibo&quot; rel=&quot;noopener&quot;&gt;这里有一篇关于新浪微博的架构设计的演讲&lt;/a&gt;，主要是讲了通过redis+缓存的使用，来实现发微博的实时性和高效性。&lt;/p&gt;
&lt;h2&gt;二、Timeline&lt;/h2&gt;
&lt;p&gt;目前来说，大多数发朋友圈、发微博这种架构的设计都是Timeline的方式。以微博为例，每个用户都有一个自己的Timeline，用户查看微博时，只从自己的Timeline上获取数据。而如果我们是使用普通的sql查询，那么查询可能就是&lt;code&gt;select weibo from table where table.userId in (select userId from guanzhu where guanzhu.userId = &apos;me&apos;)&lt;/code&gt;。也就是先查询我所关注的人，再查询所有我关注的人发的微博，然后以时间排序返回。而且因为数据的变化会非常的快，根本没有办法去做缓存，并且刷微博又是一个非常高频的动作，因此普通的sql查询必然无法满足。 而Timeline方案，其实就是以空间换时间的一种方案，假设现在姚晨（1000万粉丝）发布了一条微博，在姚晨的视角里，她已经发布完成了，但是实际上此时并不是她的所有粉丝都能立刻看到这条微博。这个发布动作做了异步处理，此时正在向关注她的微博的粉丝的Timeline上推送（当然微博也不可能真的向她所有粉丝推送，可能会去除一些长期不上线的用户以及僵尸粉）。 因此Timeline方案最复杂的地方就在于发微博这个环节而不是刷新微博这个环节了。发微博相对于刷新微博是非常低频次的，并且只要自己能实时看到就行，并不要求其他人都能实时看到，大大降低了复杂度。&lt;/p&gt;
&lt;h2&gt;三、使用redis配合完成Timeline的设计。&lt;/h2&gt;
&lt;p&gt;redis是一款内存数据库，但是它的数据也可以持久化的磁盘上。因为是内存数据库，它最擅长的就是高频次的读取和写入。并且redis支持多种数据结构，其中list数据结构非常符合Timeline的需求。 在redis中，可以创建命名的list（系统中现在使用了zset去保存timeline，比list更优），并且可以对list做push、pop、range操作。比如id为28的用户，他的timeline可以是名为u28的list,然后使用push来向用户的Timeline增加新的推文、使用range来获取指定范围内的推文。 下面的php代码演示了如何从Timeline上获取推文信息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;/**
 * 获取一页时间轴上的推文id
 * @param $userId 用户id
 * @param $pageIndex 页码
 * @param $pageSize  页大小
 * @return array 一页postId
 */
public function getTimeline($userId,$pageIndex,$pageSize){
    $end = (1 - $pageIndex) * $pageSize - 1;
    $start = $end - $pageSize + 1;

    $result = $this-&amp;gt;redis-&amp;gt;lRange($this-&amp;gt;getUserTimelineKey($userId),$start,$end);

    return $result;
}

/**
 * 获取用户时间轴上推文的总数
 * @param $userId 用户id
 * @return int 总数
 */
public function getTimelinePostCount($userId){
    return $this-&amp;gt;redis-&amp;gt;lSize($this-&amp;gt;getUserTimelineKey($userId));
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、php如何异步推送到其他用户的Timeline&lt;/h2&gt;
&lt;p&gt;php的机制决定了它无法完成异步操作（一个php文件执行完成之后线程会直接被销毁），因此这里同样是采用了Redis当做任务队列来解耦合，实现异步操作。一个用户发布推文之后，使用下面的&lt;code&gt;setPostTask($pushUserIds,$postId,$userId)&lt;/code&gt;，将发布推文放到任务队列中然后直接返回。在这里，对用户自己的Timeline做了特殊处理，发推文之后立马给自己的timeline同步推送，其他用户的timeline是异步推送。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;        /**
         * 设置推送任务
         * @param $pushUserIds 要推送的userId
         * @param $postId      推文内容
         * @return mix 任务推送状态
         */
        public function setPostTask($pushUserIds,$postId,$userId){

            //为了用户第一时间看到自己发的，这里特殊处理用户自己发的推文
            $this-&amp;gt;redis-&amp;gt;rPush($this-&amp;gt;getUserTimelineKey($userId),$postId);

            $userIds = array();
            foreach($pushUserIds as $value){
                array_push($userIds,$value[&apos;userId&apos;]);
            }
            $data[&apos;u&apos;] = $userIds;
            $data[&apos;p&apos;] = $postId;
            $str = \json_encode($data);
            return $this-&amp;gt;redis-&amp;gt;rPush($GLOBALS[&apos;redis_post&apos;],$str);
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于此同时，一个以cli模式运行的php脚本从任务队列中顺序获取所有的推送任务，向其他用户的Timeline上推送。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;/**
 * 获取推送推文的任务
 * @return mix 推送结果
 */
public function getPostTask(){
    return $this-&amp;gt;redis-&amp;gt;lPop($GLOBALS[&apos;redis_post&apos;]);
}

/**
 * 将推文id推送到所有用户的timeline中
 * @TODO 现在是同步的方式
 * @param $pushUserIds 要推送的用户id
 * @param $postId 推文id
 * @return boolean  true操作成功
 */
public function pushToTimeline($pushUserIds,$postId){
    foreach($pushUserIds as $value){
        $result = $this-&amp;gt;redis-&amp;gt;rPush($this-&amp;gt;getUserTimelineKey($value),$postId);
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;cli模式运行的php脚本代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php
/**
 * 从Redis队列中轮询要执行的任务，并执行推送
 *
 */
require_once(&apos;vendor/autoload.php&apos;);
use DB\PostDB;

$postDB = new PostDB();
$postContent = &quot;&quot;;
while(true){

    while( ($postContent = $postDB-&amp;gt;getPostTask()) != FALSE ){
        $post = json_decode($postContent,true);
        var_dump($post[&apos;u&apos;]);
        $postDB-&amp;gt;pushToTimeline($post[&apos;u&apos;],$post[&apos;p&apos;]);
    }

    sleep(1);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即使这个推送会花费一些时间，但是没有任何用户的体验受到了影响，发布推文的用户timeline因为是特殊处理，所以他能立马看到自己发的推文，其他用户虽然无法立刻看到，但是其他用户对“立刻看到”的需求几乎没有（“立刻看到”指的是刚发布的一瞬间，绝大多数人对于延迟几秒看到根本无所谓）。并且这个架构可以很轻松的横向扩展。&lt;/p&gt;
&lt;h2&gt;五、一些其他问题&lt;/h2&gt;
&lt;h3&gt;5.1 内存很贵，尽量压缩&lt;/h3&gt;
&lt;p&gt;redis是内存数据库，内存相对于磁盘的价格要贵上很多倍，因此存在redis中的内容要尽量的小。比如存的是Json,那么Json的字段要尽量的小（直接使用1,2,3,4,5这种也完全可以，即使损失了可读性），同时Json也可以考虑改成谷歌的Protocol Buffers等等。&lt;/p&gt;
&lt;h3&gt;5.2 timeline里应该存什么&lt;/h3&gt;
&lt;p&gt;使用Redis存储了每个用户的Timeline，在我的实现中，timeline中存储的是每篇推文的id，然后获取到id之后再去mysql数据库中查询，但是这样的设计虽然大大降低了查询的复杂度，但是并没有降低mysql的IO负荷，如果把推文内容直接存储到Timeline中呢？这样的话如果用户要删除一篇推文，如何对应的删除其他用户的timeline中的推文？这个地方一直是我无法解决的疑惑。 &lt;a href=&quot;https://redislabs.com/ebook/part-2-core-concepts/chapter-8-building-a-simple-social-network/&quot; rel=&quot;noopener&quot;&gt;这里有一篇关于twitter使用redis解决timeline的方案&lt;/a&gt;，这篇方案中使用的不是list来保存用户的post，而是zset，zset中每条数据由推文的id和时间戳组成，通过时间来排序。仔细考虑一下，list因为是根据推送时间来排序的，可能出现后发的推文出现在靠前的位置（当然我认为是可以通过隐藏描述显示来解决这个问题，因为错位几秒并不影响）。 所以理论上来说，上面那篇文章介绍的方案和我的方案是一致的，只是我把推文的源放在mysql上，twitter直接是存储在redis中来获取更快的速度。&lt;/p&gt;
</content:encoded><category>架构设计</category><category>架构</category><category>redis</category><category>timeline</category><author>joyme123</author></item><item><title>c++11实现的跨平台定时器</title><link>https://www.myway5.com/blog/cpp11-implement-cross-platform-timer/</link><guid isPermaLink="true">https://www.myway5.com/blog/cpp11-implement-cross-platform-timer/</guid><description>在一个比较大的系统当中，很多地方都需要定时功能，比如某些存在内存中的数据，需要定时的做持久化（实时持久化可能带来很大的性能问题），如果这个系统要求跨平台。那么实现一个跨平台的定时器就很有必要了。</description><pubDate>Fri, 19 May 2017 06:14:10 GMT</pubDate><content:encoded>&lt;h2&gt;一、引言&lt;/h2&gt;
&lt;p&gt;在一个比较大的系统当中，很多地方都需要定时功能，比如某些存在内存中的数据，需要定时的做持久化（实时持久化可能带来很大的性能问题），如果这个系统要求跨平台。那么实现一个跨平台的定时器就很有必要了。 c++11的标准中，实现了多线程,头文件为&lt;code&gt;&amp;lt;thread&amp;gt;&lt;/code&gt;,也有了系统时间相关的库，头文件为&lt;code&gt;&amp;lt;chrono&amp;gt;&lt;/code&gt;,这使得使用c++11实现跨平台的定时器有了可能。在这之前，多线程一般都是使用操作系统相关的库。&lt;/p&gt;
&lt;h2&gt;二、 定时器的基本功能&lt;/h2&gt;
&lt;p&gt;在定时器的实现之前，需要事先计划好一个定时器的基本功能。大概的功能如下： 1.添加一个定时事件 2.删除一个指定的定时事件 3.定时事件支持只执行一次或者重复执行 4.定时器启动指令 这个部分的内容在&lt;a href=&quot;https://www.ibm.com/developerworks/cn/linux/l-cn-timers/index.html&quot; rel=&quot;noopener&quot;&gt;Linux 下定时器的实现方式分析&lt;/a&gt;中有很详细的介绍&lt;/p&gt;
&lt;h2&gt;三、 数据结构知识预备&lt;/h2&gt;
&lt;p&gt;在实现定时器中，需要有一个容器去存储所有的定时事件，比如说最容易想到的数组，如果我们用数组保存定时事件，那么在找出当前需要执行的定时事件的伪代码就是&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;    for(i = 0 ; i &amp;lt; events_array_size; ++i){
        if(events[i].time &amp;lt;= current_time){
            exec_in_child_thread(events);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里就是遍历所有的事件，将已经到了执行时间的事件放到子线程中去执行。 在这里有一个可以优化效率问题：注意到每一次查询是否有事件到了执行时间，都会遍历整个数组，也就是O(n)的复杂度，并且查询周期也是非常短的，所以会有非常大的性能问题。因此，我们可以将这里的数组变成有序的，每一次只需查询第一个事件，如果还没到执行时间，那么之后的事件就不用遍历了。这样平均下来就是O(1)的时间复杂度。考虑到我们在插入，删除时始终要保证数组的有序，可以采用最小堆。也就是堆顶始终是最先到执行时间的一项。&lt;/p&gt;
&lt;h2&gt;四、c++11多线程的知识预备&lt;/h2&gt;
&lt;h3&gt;4.1 join()还是detach()&lt;/h3&gt;
&lt;p&gt;c++11中创建一个线程很容易&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;thread&amp;gt;

void function_1(){
    std::cout &amp;lt;&amp;lt; &quot;hello world&quot; &amp;lt;&amp;lt; std::endl;
    while(true){}
}

int main(){
    std::thread t1(function_1);
    t1.join();      //加入运行

    std::cout &amp;lt;&amp;lt; &quot;hahah&quot; &amp;lt;&amp;lt; std::endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码就是创建了一个t1的线程，来执行function_1函数。注意到我们这里使用了t1.join(),字面上的意思是将子线程加入的主线程来执行，这样导致的结果是主线程会在t1.join()这里停止，直到t1执行结束后再往下执行。在这里function_1中存在死循环，所以hahah永远不会被输出。而如果使用t1.detach()的话，即将子线程从主线程分离出去，那么主线程就会一直向下执行，在这里就会执行到return 0,然后主线程销毁，detach出去的子线程可能就没有任何显示的机会了（查过资料说detach出去子线程的行为的undefined的）。 在这个定时器的实现中，当然不能使用join(),因为会阻塞程序的正常运行。&lt;/p&gt;
&lt;h3&gt;4.2 如何使得定时器的使用是线程安全的。&lt;/h3&gt;
&lt;p&gt;在一个程序中，可能很多地方会使用到定时器去触发某个动作，但是为了效率，肯定不会创建多个定时器实例，因此定时器的实现应该是单例的。因为需要单例，因此定时器的创建，定时器中定时事件的添加和删除都是线程安全的（因为同一时间可能会有多个线程操作定时器）。 4.2.1 线程安全的单例模式 单例模式的实现方法有很多种，因为能力问题，不对这一点多做叙述。可以看这一篇&lt;a href=&quot;http://www.zkt.name/dan-li-mo-shi-singleton-ji-c-shi-xian/&quot; rel=&quot;noopener&quot;&gt;单例模式(Singleton)及其C++实现&lt;/a&gt;。 我这里使用的是使用static来创建局部可见的全局变量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;static Timer* getInstance(std::chrono::milliseconds tick){
    static Timer timer(tick);
    return &amp;amp;timer;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4.2.2 使用锁保证定时事件的添加和删除是线程安全的。 在往定时器中添加和删除定时事件时，就是在做最小堆的添加和删除。这个中间线程不安全的现象为:以添加为例，往最小堆数组末尾加入一个数据，然后再逐层向上调整。如果在这个调整的过程中，另外一个线程执行了删除操作，两个调整过程就可能同时发生，造成不可预知的错误，使得最小堆并不有序。 所以添加和删除操作应该是互斥的，只有其中一个操作执行结束，另一个操作才可以开始。c++中可以使用信号量mutex和锁locker轻松的完成互斥的需求。示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;mutex&amp;gt;
#include &amp;lt;fstream&amp;gt;

class LofFile{
    public:
        LofFile(){
            f.open(&quot;log.txt&quot;);
        }

        void shared_print(std::string id,int value){
            std::lock(m_mutex,m_mutex2);
            std::lock_guard&amp;lt;std::mutex&amp;gt; locker(m_mutex,std::adopt_lock);
            std::lock_guard&amp;lt;std::mutex&amp;gt; locker2(m_mutex2,std::adopt_lock);
            std::cout &amp;lt;&amp;lt; &quot;from&quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &quot;:&quot; &amp;lt;&amp;lt; value &amp;lt;&amp;lt; std::endl;
        }

        void shared_print2(std::string id,int value){
            std::lock(m_mutex,m_mutex2);
            std::lock_guard&amp;lt;std::mutex&amp;gt; locker2(m_mutex2,std::adopt_lock);      //lock_guard是为了防止语句执行中出现异常，锁不被释放。这样做可以保证在退出代码块后解锁
            std::lock_guard&amp;lt;std::mutex&amp;gt; locker(m_mutex,std::adopt_lock);
            std::cout &amp;lt;&amp;lt; &quot;from&quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &quot;:&quot; &amp;lt;&amp;lt; value &amp;lt;&amp;lt; std::endl;
        }

    protected:
    private:
        std::mutex m_mutex;     //使用信号量来解决资源竞争
        std::mutex m_mutex2;
        std::ofstream f;

};

void function_1(LofFile&amp;amp; log){
    for(int i = 0; i &amp;gt; -100; i--){
        log.shared_print(&quot;From t1:&quot;,i);
    }
};

int main(){
    LofFile log;
    std::thread t1(function_1,std::ref(log));//线程开始运行
    for(int i = 0; i &amp;lt; 100; i++){
        log.shared_print2(&quot;From main&quot;,i);
    }

    t1.join();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;五、总结与实现&lt;/h2&gt;
&lt;p&gt;分析到这里，一个定时器的基本要素已经具备了。 1.用来存储定时事件，并且添加和删除操作都是线程安全的最小堆 2.可以多线程实现定时事件的执行从而不阻塞主线程。 下面贴出所有的实现代码：&lt;/p&gt;
&lt;h3&gt;5.1 最小堆的实现。&lt;/h3&gt;
&lt;p&gt;SortedHeap.hpp&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * 排序堆的实现
 * 堆使用的是完全二叉树的结构，使用数组（数组从零开始）保存，那么最后一个非叶子节点为(n - 1) / 2
 * 堆的构建一直都是在有序的基础上的，那么每次调整只需比较i和(i - 1) / 2的元素，依次上推
 * 支持任意类的排序
 * 当前还不支持多线程环境下的使用
 * author:jiangpengfie
 * date:2017-05-09
 */
#ifndef SORTEDHEAP_H
#define SORTEDHEAP_H
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;functional&amp;gt;
#include &amp;lt;memory&amp;gt;
#include &amp;lt;mutex&amp;gt;
#include &amp;lt;condition_variable&amp;gt;
#include &quot;src/core/util/util.h&quot;


template&amp;lt;class T&amp;gt;
class SortedHeap{
    private:
        struct HeapNode{
            unsigned int id;
            T obj;
            HeapNode(unsigned int id,T t):obj(t){
                this-&amp;gt;id = id;
            }
        };
        std::vector&amp;lt;HeapNode&amp;gt; heap;
        unsigned int autoIncrementId;
        std::function&amp;lt;bool(T&amp;amp; ,T&amp;amp;)&amp;gt; cmp;    //比较函数，实现选择构造最大堆还是最小堆
        std::mutex mu1;                
        std::mutex mu2;                

        /**
         * 插入节点后调整堆中不符合的节点
         */
        void adjustAfterInsert();

        /**
         * pop出堆顶元素后调整堆中不符合的节点
         */
        void adjustAfterPopTop();

        /**
         * 删除节点后调整堆中不符合的节点
         * @param i 删除的节点id
         */
        void adjustAfterDelete(int id);         

        void swap(HeapNode&amp;amp; t1,HeapNode&amp;amp; t2);

        void deleteNodeByPos(const unsigned int pos);
    public:
        /**
         * 构造函数
         * @param cmp 用来比较
         */
        SortedHeap(std::function&amp;lt;bool(T&amp;amp;,T&amp;amp;)&amp;gt; cmp);
        /**
         * 插入节点
         * @param node 插入的节点
         */
        unsigned int insertNode(T&amp;amp; node);
        /**
         * 删除节点，时间复杂度为O(n)
         * @param id  要删除的节点id
         */
        void deleteNode(unsigned int id);

        /**
         * pop最小的节点
         * @return T* 返回的最顶部的节点指针
         */
        std::unique_ptr&amp;lt;T&amp;gt; popTopNode();

        /**
         * 获取最顶部的节点
         * @return T 最顶部的节点指针
         */
        std::unique_ptr&amp;lt;T&amp;gt; getTopNode();

        /**
         * 删除顶部的节点
         *
         */
        void deleteTopNode();
};

template&amp;lt;typename T&amp;gt;
SortedHeap&amp;lt;T&amp;gt;::SortedHeap(std::function&amp;lt;bool(T&amp;amp;,T&amp;amp;)&amp;gt; cmp){
    this-&amp;gt;cmp = cmp;
    this-&amp;gt;autoIncrementId = 0;
}

template&amp;lt;typename T&amp;gt;
void SortedHeap&amp;lt;T&amp;gt;::swap(HeapNode&amp;amp; t1,HeapNode&amp;amp; t2){
    HeapNode tmp = t1;
    t1 = t2;
    t2 = tmp;
}

template&amp;lt;typename T&amp;gt;
void SortedHeap&amp;lt;T&amp;gt;::adjustAfterInsert(){
    int last = this-&amp;gt;heap.size() - 1;
    int flag = true;
    //从插入的节点位置开始向上调整
    while(last &amp;gt; 0 &amp;amp;&amp;amp; flag){
        if(this-&amp;gt;cmp(this-&amp;gt;heap[last].obj,this-&amp;gt;heap[(last - 1) / 2].obj)){
            this-&amp;gt;swap(this-&amp;gt;heap[(last - 1) / 2],this-&amp;gt;heap[last]);
        }else{
            //不需要调整了
            flag = false;
        }
        last = (last - 1) / 2;
    }
}

template&amp;lt;typename T&amp;gt;
void SortedHeap&amp;lt;T&amp;gt;::adjustAfterDelete(int pos){
    //从pos位置开始向下调整
    int last = this-&amp;gt;heap.size() - 1;
    if(last == 0)
        return;     //最后一个不需要调整
    bool flag = true;   //标记是否需要调整
    while(pos &amp;lt;= (last - 1) / 2 &amp;amp;&amp;amp; flag){
        //一直调整到最后一个非叶子结点
        int topNum = 0;     //记录最小的结点编号

          //(pos + 1) * 2 - 1是左孩子，pos是父
        if(this-&amp;gt;cmp(this-&amp;gt;heap[(pos + 1) * 2 - 1].obj,this-&amp;gt;heap[pos].obj)){
            topNum = (pos + 1) * 2 - 1;
        }else{
            topNum = pos;
        }

        if((pos + 1) * 2 &amp;lt;= last){
            //如果存在右结点
            if(this-&amp;gt;cmp(this-&amp;gt;heap[(pos + 1) * 2].obj,this-&amp;gt;heap[topNum].obj)){
                topNum = (pos + 1) * 2;
            }
        }

        //看看topNum是不是自己
        if(pos == topNum){
            //是自己就不用调整了
            flag = false;
        }else{
            //交换
            this-&amp;gt;swap(this-&amp;gt;heap[pos],this-&amp;gt;heap[topNum]);
        }
        pos = topNum;
    }
}


template&amp;lt;typename T&amp;gt;
void SortedHeap&amp;lt;T&amp;gt;::deleteNodeByPos(const unsigned int pos){
    unsigned int last = this-&amp;gt;heap.size() - 1;
    if(pos &amp;gt; last){
        return;
    }
    std::lock(mu1,mu2);             //上锁
    std::lock_guard&amp;lt;std::mutex&amp;gt; locker1(mu1,std::adopt_lock);
    std::lock_guard&amp;lt;std::mutex&amp;gt; locker2(mu2,std::adopt_lock);
    //与最后一个交换
    swap(this-&amp;gt;heap[pos],this-&amp;gt;heap[last]);
    //删除最后一个
    this-&amp;gt;heap.pop_back();      

    this-&amp;gt;adjustAfterDelete(pos);
}



template&amp;lt;typename T&amp;gt;
unsigned int SortedHeap&amp;lt;T&amp;gt;::insertNode(T&amp;amp; node){
    HeapNode hNode(this-&amp;gt;autoIncrementId++,node);
    std::lock(mu1,mu2);             //上锁
    std::lock_guard&amp;lt;std::mutex&amp;gt; locker1(mu1,std::adopt_lock);
    std::lock_guard&amp;lt;std::mutex&amp;gt; locker2(mu2,std::adopt_lock);
    this-&amp;gt;heap.push_back(hNode);     //先将node放在最后一位
    if(this-&amp;gt;heap.size() != 1){
        //如果大小不等于1，则在新增节点后调整
        this-&amp;gt;adjustAfterInsert();
    }
    return this-&amp;gt;autoIncrementId - 1;
}


template&amp;lt;typename T&amp;gt;
void SortedHeap&amp;lt;T&amp;gt;::deleteNode(unsigned int id){
    for(unsigned int i = 0; i &amp;lt; this-&amp;gt;heap.size(); i++){
        if(heap[i].id == id){
            //找到了id
            this-&amp;gt;deleteNodeByPos(i);
            break;
        }
    }


}

template&amp;lt;typename T&amp;gt;
std::unique_ptr&amp;lt;T&amp;gt; SortedHeap&amp;lt;T&amp;gt;::popTopNode(){
    if(this-&amp;gt;heap.size() != 0){
        std::unique_ptr&amp;lt;T&amp;gt; top(new T(this-&amp;gt;heap[0].obj));
        this-&amp;gt;deleteNodeByPos(0);
        return top;
    }else{
        std::unique_ptr&amp;lt;T&amp;gt; p = nullptr;
        return p;
    }
}

template&amp;lt;typename T&amp;gt;
std::unique_ptr&amp;lt;T&amp;gt; SortedHeap&amp;lt;T&amp;gt;::getTopNode(){
    if(this-&amp;gt;heap.size() != 0){
        std::unique_ptr&amp;lt;T&amp;gt; top(new T(this-&amp;gt;heap[0].obj));
        return top;
    }else{
        std::unique_ptr&amp;lt;T&amp;gt; p = nullptr;
        return p;
    }
}

template&amp;lt;typename T&amp;gt;
void SortedHeap&amp;lt;T&amp;gt;::deleteTopNode(){
   if(this-&amp;gt;heap.size() != 0){
        this-&amp;gt;deleteNodeByPos(0);
    }
}

#endif



&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2 定时器的实现&lt;/h3&gt;
&lt;p&gt;Timer.h&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * 定时器的实现
 * 支持int setTimer(T interval,function action):设置一个定时器，指定间隔interval和回调函数action,返回定时器id
 * 支持void deleteTimer(int timerId):删除一个定时器
 * 数据结构:最小堆模型，按照定时器触发的时间排序
 * author:jiangpengfei
 * date:2017-05-09
 */
#ifndef TIMER_H
#define TIMER_H
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;chrono&amp;gt;
#include &amp;lt;functional&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;memory&amp;gt;
#include &quot;SortedHeap.hpp&quot;

class Timer{
    private:
        std::chrono::milliseconds tick;
        double timeline;     //当前时间线,long double的字节数为12
        bool isStart;        //标志当前定时器的启动状态
        struct SchedulerEvent{
          unsigned int id;                   //定时事件的唯一标示id
          double interval;                   //事件的触发间隔，在重复事件中会用到这个属性
          double deadline;                   //定时事件的触发时间
          std::function&amp;lt;void()&amp;gt; action;      //触发的事件
          bool isRepeat;                     //是否是重复执行事件
          SchedulerEvent( double interval, double timeline,std::function&amp;lt;void()&amp;gt; action,bool isRepeat){
              this-&amp;gt;interval = interval;
              this-&amp;gt;deadline = interval + timeline;
              this-&amp;gt;action = action;
              this-&amp;gt;isRepeat = isRepeat;
          }
        };

        SortedHeap&amp;lt;SchedulerEvent&amp;gt; eventQueue;

        /**
         * 执行到达期限的定时器
         */
        void loopForExecute();

        //私有的构造函数
        Timer(std::chrono::milliseconds tick):eventQueue(
            [](SchedulerEvent&amp;amp; a,SchedulerEvent&amp;amp; b){
                return a.deadline &amp;lt; b.deadline;
            }
        ){
            this-&amp;gt;timeline = 0;
            this-&amp;gt;tick = tick;
            this-&amp;gt;isStart = false;
        }

    public:

        //单例模式
        static Timer* getInstance(std::chrono::milliseconds tick){
            static Timer timer(tick);
            return &amp;amp;timer;
        }

        /**
         * 设置定时器
         * @param interval 定时间隔
         * @param action 定时执行的动作
         * @param isRepeat 是否重复执行,默认不重复执行
         * @return unsigned int 定时器的id,可以根据这个id执行删除操作
         */
        unsigned int addEvent(double interval,std::function&amp;lt;void()&amp;gt; action,bool isRepeat = false);

        /**
         * 删除定时器
         * @param timerId 定时器id
         *
         */
        void deleteEvent(unsigned int timerId);

        /**
         * 同步执行启动定时器
         */
         void syncStart();

         /**
         * 异步执行启动定时器
         */
         void asyncStart();

};


#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Timer.cpp&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &quot;src/core/util/Timer.h&quot;

unsigned int Timer::addEvent(double interval,std::function&amp;lt;void()&amp;gt; action,bool isRepeat){
    SchedulerEvent event(interval,this-&amp;gt;timeline,action,isRepeat);
    return this-&amp;gt;eventQueue.insertNode(event);
}

void Timer::deleteEvent(unsigned int timerId){
    this-&amp;gt;eventQueue.deleteNode(timerId);
}

void Timer::loopForExecute(){
    std::unique_ptr&amp;lt;SchedulerEvent&amp;gt; top = this-&amp;gt;eventQueue.getTopNode();
    while(top != nullptr &amp;amp;&amp;amp; top-&amp;gt;deadline &amp;lt;= this-&amp;gt;timeline){
        //如果已经到了执行的时间,新开一个子线程执行任务
        std::thread t(top-&amp;gt;action);
        t.detach();    //子线程分离

        if(top-&amp;gt;isRepeat){
            //如果是重复事件,则重新添加
            this-&amp;gt;addEvent(top-&amp;gt;interval,top-&amp;gt;action,top-&amp;gt;isRepeat);
        }

        //从堆中删除
        this-&amp;gt;eventQueue.deleteTopNode();
        top = this-&amp;gt;eventQueue.getTopNode();
    }
    //执行一次后等待一个周期
    std::this_thread::sleep_for(this-&amp;gt;tick);
    //周期增1
    this-&amp;gt;timeline++;
}

void Timer::asyncStart(){
    if(!this-&amp;gt;isStart){
        std::thread daemon_thread(&amp;amp;Timer::syncStart,this);
        daemon_thread.detach();     //从当前主线程分离
    }
}

void Timer::syncStart(){
    if(!this-&amp;gt;isStart){
        while(1)
            this-&amp;gt;loopForExecute();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试执行的代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;chrono&amp;gt;
#include &amp;lt;ctime&amp;gt;
#include &amp;lt;iomanip&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;functional&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;memory&amp;gt;
#include &amp;lt;fstream&amp;gt;
#include &quot;src/core/util/Timer.h&quot;

void myprint(std::string msg){
    std::ofstream of(&quot;timer.txt&quot;, std::ios::app);
    std::thread::id this_id = std::this_thread::get_id();
    auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
    of &amp;lt;&amp;lt; &quot;From Thread &quot; &amp;lt;&amp;lt; this_id &amp;lt;&amp;lt; &quot;at time &quot; &amp;lt;&amp;lt; std::put_time(std::localtime(&amp;amp;t), &quot;%Y-%m-%d %H.%M.%S&quot;) &amp;lt;&amp;lt; &quot;:&quot; &amp;lt;&amp;lt; msg &amp;lt;&amp;lt; std::endl;
}

int main(){
    std::chrono::milliseconds tick(2000);       //1000毫秒作为一个周期
    Timer* timer = Timer::getInstance(tick);
    std::function&amp;lt;void()&amp;gt; f1 = std::bind(myprint,&quot;第一个加入,10tick后执行&quot;);
    std::function&amp;lt;void()&amp;gt; f2 = std::bind(myprint,&quot;第二个加入，被删除不执行&quot;);
    std::function&amp;lt;void()&amp;gt; f3 = std::bind(myprint,&quot;第三个加入，每5tick重复执行&quot;);
    std::function&amp;lt;void()&amp;gt; f4 = std::bind(myprint,&quot;第四个加入，5tick后执行&quot;);
    std::function&amp;lt;void()&amp;gt; f5 = std::bind(myprint,&quot;第五个加入，5tick后执行&quot;);
    std::function&amp;lt;void()&amp;gt; f6 = std::bind(myprint,&quot;第六个加入，5tick后执行&quot;);
    std::function&amp;lt;void()&amp;gt; f7 = std::bind(myprint,&quot;第七个加入，5tick后执行&quot;);
    std::function&amp;lt;void()&amp;gt; f8 = std::bind(myprint,&quot;第八个加入，5tick后执行&quot;);
    std::function&amp;lt;void()&amp;gt; f9 = std::bind(myprint,&quot;第九个加入，5tick后执行&quot;);
    std::function&amp;lt;void()&amp;gt; f10 = std::bind(myprint,&quot;第十个加入，5tick后执行&quot;);
    std::function&amp;lt;void()&amp;gt; f11 = std::bind(myprint,&quot;第十一个加入，15tick后执行&quot;);
    std::function&amp;lt;void()&amp;gt; f12 = std::bind(myprint,&quot;第十二个在执行后加入，20tick+5s后执行&quot;);

    timer-&amp;gt;addEvent(10,f1);
    int id = timer-&amp;gt;addEvent(11,f2);
    timer-&amp;gt;addEvent(5,f3,true);
    timer-&amp;gt;addEvent(5,f4);
    timer-&amp;gt;addEvent(5,f5);
    timer-&amp;gt;addEvent(5,f6);   
    timer-&amp;gt;addEvent(5,f7);
    timer-&amp;gt;addEvent(5,f8);
    timer-&amp;gt;addEvent(5,f9);
    timer-&amp;gt;addEvent(5,f10);
    timer-&amp;gt;addEvent(15,f11);

    timer-&amp;gt;deleteEvent(id);

    myprint(&quot;线程开始启动,每tick是2秒&quot;);

    //异步执行，程序退出后计时器也会终止，因此在下面使用while循环保证程序不会退出
    timer-&amp;gt;asyncStart();
    //timer-&amp;gt;syncStart();


    //休眠5秒钟
    std::this_thread::sleep_for(std::chrono::seconds(5));   
    //应该在大概20*tick+5秒后执行,
    //TODO 执行后加入的定时器不对
    timer-&amp;gt;addEvent(20,f12);

    getchar();

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>c++</category><category>c++11</category><category>多线程</category><category>定时器</category><author>joyme123</author></item><item><title>stream和buffer的概念解析</title><link>https://www.myway5.com/blog/stream-vs-buffer/</link><guid isPermaLink="true">https://www.myway5.com/blog/stream-vs-buffer/</guid><description>buffer:内存中一块确定的临时存储区域。 stream:一段不确定长度的数据序列，可以认为stream就是I/O中input部分的FIFO实现，为了方便理解，可以用键盘输入作为输入来举例，从键盘中敲入的字符组成一段stream,先敲入的字符先被读取，也就是FIFO,但是读取…</description><pubDate>Sat, 29 Apr 2017 16:31:07 GMT</pubDate><content:encoded>&lt;h2&gt;1.概念解析&lt;/h2&gt;
&lt;p&gt;buffer:内存中一块确定的临时存储区域。 stream:一段不确定长度的数据序列，可以认为stream就是I/O中input部分的FIFO实现，为了方便理解，可以用键盘输入作为输入来举例，从键盘中敲入的字符组成一段stream,先敲入的字符先被读取，也就是FIFO,但是读取的过程中，你并不知道总共要读取多少，也就是长度的不确定性。&lt;/p&gt;
&lt;h2&gt;2.具体使用时的区别与联系&lt;/h2&gt;
&lt;p&gt;在使用 stream 的过程中，我们遵循这样一个基本的过程，以从文件中读取为例: 1.程序向 stream 发起请求，要读取一个字节。 2.stream 在磁盘上定为到请求的字节，并发送给程序。 3.程序得到这个字节后，在进行请求，重复到第一步 这个样子就有一个很严重的效率问题，读取一个字节需要在程序和stream之间来回一次，那么1000个字节就是1000次。怎么解决这个问题呢？ 这时候就到 buffer 出场了，buffer 根据上面的概念,他是内存中一块确定的临时存储区域。如果使用一个 1000 字节的 buffer ,那么程序和 stream 的关系就变成: 1.程序向stream发起请求，要求stream将buffer填满。 2.stream向buffer中填充字节，要么填满1000个字节为止，要么stream到达结尾为止。 3.程序从buffer中一次性获取1000字节的数据 很明显，使用buffer的好处在于减少的请求IO的次数，也大大提升了效率&lt;/p&gt;
</content:encoded><category>编程相关</category><author>joyme123</author></item><item><title>LINUX下修改php.ini配置报错输出</title><link>https://www.myway5.com/blog/linux-edit-php-ini-error/</link><guid isPermaLink="true">https://www.myway5.com/blog/linux-edit-php-ini-error/</guid><description>首先是找到php.ini文件 输入 find / -name php.ini 总共有两个结果 /etc/php5/cli/php.ini /etc/php5/apache2/php.ini cli/php.</description><pubDate>Wed, 19 Apr 2017 09:49:21 GMT</pubDate><content:encoded>&lt;p&gt;首先是找到php.ini文件 输入 find / -name php.ini 总共有两个结果 /etc/php5/cli/php.ini /etc/php5/apache2/php.ini cli/php.ini指的是在控制台环境下运行php脚本使用的配置文件 apache2/php.ini是apache2环境下运行php脚本使用的配置文件 如果你还不确定是用哪一个，可以新建一个php代码 使用 echo phpinfo(); 来输出php信息，其中有一项是加载的php.ini路径。我的就是apache2/php.ini 然后编辑apache2/php.ini，我这里是要开启他的报错，不然如果代码中有错，浏览器访问就会直接报500错误 找到display_errors这一行，去除前面的; display_errors=On 配置错误级别 error_reporting=E_ALL &amp;amp; ~E_NOTICE 修改完成后要使php.ini配置生效，网上普遍的说法是重启apache服务生效 service apache2 restart 代码出错的地方仍然报500错误 准备去看apache的日志文件，同样使用find / -name error.log找到日志，由于日志太多，就把之前的error.log删掉，重启apache，浏览器打开出错页面，然后查看error.log。发现日志中是记录了代码出错的信息的。确定就是没有开启报错，导致500错误。 修改apache2.conf，在/etc/apache2下面。 添加 php_flag display_errors on php_value error_reporting 2039 重启apache service apache2 restart 浏览器打开错误页面，成功报错！&lt;/p&gt;
</content:encoded><category>linux</category><author>joyme123</author></item><item><title>linux下一些好用的指令记录</title><link>https://www.myway5.com/blog/linux-command/</link><guid isPermaLink="true">https://www.myway5.com/blog/linux-command/</guid><description>查找当前目录下包含指定字符串的文件 grep -rn &quot;hello,world!&quot; \ : 表示当前目录所有文件，也可以是某个文件名 -r 是递归查找 -n 是显示行号 -R 查找所有文件包含子目录 -i 忽略大小写</description><pubDate>Wed, 19 Apr 2017 09:48:11 GMT</pubDate><content:encoded>&lt;p&gt;查找当前目录下包含指定字符串的文件 grep -rn &quot;hello,world!&quot; *&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;: 表示当前目录所有文件，也可以是某个文件名 -r 是递归查找 -n 是显示行号 -R 查找所有文件包含子目录 -i 忽略大小写&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;mysql数据库相关操作 创建用户 mysql&amp;gt; insert into mysql.user(Host,User,Password) values(&quot;localhost&quot;,&quot;phplamp&quot;,password(&quot;1234&quot;)); 授权&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;grant all privileges on phplampDB.* to phplamp@localhost identified by &apos;1234&apos;; 删除用户 mysql&amp;gt;DELETE FROM user WHERE User=&quot;phplamp&quot; and Host=&quot;localhost&quot;; mysql&amp;gt;flush privileges; 修改密码 mysql&amp;gt;update mysql.user set password=password(&apos;新密码&apos;) where User=&quot;phplamp&quot; and Host=&quot;localhost&quot;; mysql&amp;gt;flush privileges; 刷新系统权限表 mysql&amp;gt;flush privileges;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;cinnamon桌面图标重复 gsettings set org.gnome.desktop.background show-desktop-icons false&lt;/p&gt;
</content:encoded><category>linux</category><author>joyme123</author></item><item><title>java反射机制以及在android开发中的应用</title><link>https://www.myway5.com/blog/java-reflect-application/</link><guid isPermaLink="true">https://www.myway5.com/blog/java-reflect-application/</guid><description>java中的反射机制：只要给定类的名字就可以得到所有类的信息。因为这个类的名字是可以在代码运行时动态指定的，所以利用java的反射机制比通过new的方式要灵活的多 在java中，通过new的方式创建的对象称为静态加载（编译时加载类),通过反射机制创建对象称为动态加载（运行时加载…</description><pubDate>Wed, 19 Apr 2017 09:47:31 GMT</pubDate><content:encoded>&lt;p&gt;java中的反射机制：只要给定类的名字就可以得到所有类的信息。因为这个类的名字是可以在代码运行时动态指定的，所以利用java的反射机制比通过new的方式要灵活的多 在java中，通过new的方式创建的对象称为静态加载（编译时加载类),通过反射机制创建对象称为动态加载（运行时加载类) java反射机制的使用方式 这里有个Human类如下 package com.myway5; public class Human { private String name; private int age; public String word;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Human() {
    // TODO Auto-generated constructor stub
}

public Human(String name) {
    this.name = name;
}

public Human(String name, int age) {
    this.name = name;
    this.age = age;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}
public void speak(String word) {
     System.out.println(word);
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;} 我们使用反射机制获取Human类所有的信息 package com.myway5; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; public class Client { public static void main(String[] args) { // 获取类名等信息 Class class1 = Human.class; System.out.println(class1.getName()); System.out.println(class1.getSimpleName());&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    System.out.println(&quot;***********构造方法**********&quot;);

    Constructor[] cs = class1.getDeclaredConstructors();
    for (Constructor constructor : cs) {
        System.out.print(constructor.getName() + &quot;(&quot;);
        Class[] paramsType = constructor.getParameterTypes();
        for (Class class2 : paramsType) {
            System.out.print(class2.getName() + &quot;,&quot;);
        }
        System.out.println(&quot;)&quot;);
    }

    System.out.println(&quot;**********公共成员变量************&quot;);
    Field[] fields = class1.getFields();
    for (Field field : fields) {
        System.out.println(field.getType().getName() + &quot;:&quot; + field.getName());
    }

    System.out.println(&quot;**********公共方法**************&quot;);
    Method[] methods = class1.getMethods();
    for (Method method : methods) {
        System.out.print(method.getName() + &quot;(&quot;);
        Class[] params = method.getParameterTypes();
        for (Class class3 : params) {
            System.out.print(class3.getName() + &quot;,&quot;);
        }
        System.out.println(method.getReturnType().getName() + &quot;)&quot;);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;} 输出结果如下： com.myway5.Human Human ***********构造方法********** com.myway5.Human(java.lang.String,int,) com.myway5.Human(java.lang.String,) com.myway5.Human() **********公共成员变量************ java.lang.String:word **********公共方法************** getName(java.lang.String) setName(java.lang.String,void) getAge(int) setAge(int,void) wait(long,int,void) wait(long,void) wait(void) equals(java.lang.Object,boolean) toString(java.lang.String) hashCode(int) getClass(java.lang.Class) notify(void) notifyAll(void)&lt;br /&gt;
可以看到，所有公共的成员变量，方法都可以通过这个方式获取到，那么怎么调用其中的方法呢 public static void useMethod() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException, NoSuchMethodException, SecurityException { Class class1 = Human.class; Human man = (Human) class1.newInstance();//通过newInstance创建实例 Method method = class1.getMethod(&quot;speak&quot;, String.class); Object object = method.invoke(man, new Object[] { &quot;hello reflect&quot; }); } android开发中的使用后续更新 继上文更新： android开发中常常会使用java的反射机制来更改一些系统底层无法更改的代码逻辑。比如说在AlertDialog的使用中，通过自带的setPositiveButton或者setNegativeButton时，一旦点击按钮都会退出dialog，有时候我们不希望他退出，比如用户登录时登录失败再次登录。那么一种解决办法 就是通过java的反射机制。 package com.myway5.java_reflect; import android.app.AlertDialog; import android.content.DialogInterface; import android.os.Handler; import android.os.Message; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import java.lang.ref.WeakReference; import java.lang.reflect.Field; public class MainActivity extends AppCompatActivity{&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    AlertDialog.Builder builder=new AlertDialog.Builder(this);

    builder.setMessage(&quot;Hello Dialog&quot;)
             .setTitle(&quot;对话框&quot;)
                .setView(getLayoutInflater().inflate(R.layout.dialog_signin,null));
    builder.setPositiveButton(&quot;确定&quot;, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {

        }
    });
    builder.setNegativeButton(&quot;取消&quot;, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {

        }
    });
    AlertDialog dialog=builder.create();
    try {
        Field field = dialog.getClass().getDeclaredField(&quot;mAlert&quot;);
        field.setAccessible(true);
        Object obj=field.get(dialog);
        field=obj.getClass().getDeclaredField(&quot;mHandler&quot;);
        field.setAccessible(true);
        field.set(obj,new ButtonHandler(dialog));
    }catch (Exception e){
        e.printStackTrace();
    }
    dialog.show();
}
private static final class ButtonHandler extends Handler {
    // Button clicks have Message.what as the BUTTON{1,2,3} constant
    private static final int MSG_DISMISS_DIALOG = 1;

    private WeakReference&amp;lt;DialogInterface&amp;gt; mDialog;

    public ButtonHandler(DialogInterface dialog) {
        mDialog = new WeakReference&amp;lt;DialogInterface&amp;gt;(dialog);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {

            case DialogInterface.BUTTON_POSITIVE:
            case DialogInterface.BUTTON_NEGATIVE:
            case DialogInterface.BUTTON_NEUTRAL:
                ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what);
                break;

            case MSG_DISMISS_DIALOG:
                //这里是点击后的逻辑，因为注释掉了dismiss(),dialog不会退出了
                //((DialogInterface) msg.obj).dismiss();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;} 其中 try { Field field = dialog.getClass().getDeclaredField(&quot;mAlert&quot;); field.setAccessible(true); Object obj=field.get(dialog); field=obj.getClass().getDeclaredField(&quot;mHandler&quot;); field.setAccessible(true); field.set(obj,new ButtonHandler(dialog)); }catch (Exception e){ e.printStackTrace(); } 这个部分就是通过反射获取AlertDialog的私有变量mAlert,然后获取mAlert的成员变量mHandler，将这个Handler设置成我们自己的ButtonHandler，这样就解决了点击后退出的问题 代码地址：&lt;a href=&quot;https://github.com/joyme123/java%5C_reflect&quot; rel=&quot;noopener&quot;&gt;https://github.com/joyme123/java\_reflect&lt;/a&gt; 参考文章:&lt;a href=&quot;http://www.oschina.net/question/163910%5C_27112&quot; rel=&quot;noopener&quot;&gt;http://www.oschina.net/question/163910\_27112&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>java</category><author>joyme123</author></item><item><title>android studio导入项目出现Gradle version 1.10 is required. Current version is 2.0的解决办法</title><link>https://www.myway5.com/blog/android-studio-gradle-version-1-10-is-required-current-version-is-2-0/</link><guid isPermaLink="true">https://www.myway5.com/blog/android-studio-gradle-version-1-10-is-required-current-version-is-2-0/</guid><description>1.首先在项目下找到gradlew.bat这个文件，单击运行。它会下载Gradle 1.10 。等待任务完成 2.在项目的报错的控制台中选择setting这个选项。然后设置本地的Gradle。我的是默认放在 C:\\Users\\joyme.</description><pubDate>Wed, 19 Apr 2017 09:46:58 GMT</pubDate><content:encoded>&lt;p&gt;1.首先在项目下找到gradlew.bat这个文件，单击运行。它会下载Gradle 1.10 。等待任务完成 2.在项目的报错的控制台中选择setting这个选项。然后设置本地的Gradle。我的是默认放在 C:\Users\joyme.gradle\wrapper\dists\gradle-1.10-all\bif7dlrky2i400uf9zsz2c0my\gradle-1.10&lt;/p&gt;
</content:encoded><category>问题</category><author>joyme123</author></item><item><title>vitamio使用出现找不到文件的解决方法</title><link>https://www.myway5.com/blog/vitamio-can-not-find-file/</link><guid isPermaLink="true">https://www.myway5.com/blog/vitamio-can-not-find-file/</guid><description>首先我遇到这个问题肯定不是一般的找不到文件的错误。使用vitamio的VideoView的时候，出现一部分视频会找不到文件。 分析很久问题出现的原因，最后得出是因为这部分视频名都是URLEncode过的，可能是这个原因导致字符串在解析的过程中出现问题。</description><pubDate>Wed, 19 Apr 2017 09:45:59 GMT</pubDate><content:encoded>&lt;p&gt;首先我遇到这个问题肯定不是一般的找不到文件的错误。使用vitamio的VideoView的时候，出现一部分视频会找不到文件。 分析很久问题出现的原因，最后得出是因为这部分视频名都是URLEncode过的，可能是这个原因导致字符串在解析的过程中出现问题。然后跟踪代码到VideoView的setVideoPath处，发现他的实现如下 public void setVideoPath(String path) { setVideoURI(Uri.parse(path)); } 很明显这里就是导致错误所在，所以解决办法就是将string 类型的path先编码一下 videoView.setVideoPath(URLEncoder.encode(path, &quot;UTF-8&quot;));&lt;/p&gt;
</content:encoded><category>问题</category><author>joyme123</author></item><item><title>java常量池</title><link>https://www.myway5.com/blog/java-constant-pool/</link><guid isPermaLink="true">https://www.myway5.com/blog/java-constant-pool/</guid><description>package 测试常量; public class Test { public static void main(String\[\] args){ String s1 = &quot;hello world&quot;; String s2 = &quot;hello world&quot;; System.</description><pubDate>Wed, 19 Apr 2017 09:44:49 GMT</pubDate><content:encoded>&lt;p&gt;package 测试常量; public class Test { public static void main(String[] args){ String s1 = &quot;hello world&quot;; String s2 = &quot;hello world&quot;; System.out.println(s1==s2); String s4 = new String(&quot;你好&quot;); String s3 = &quot;你好&quot;; System.out.println(s3==s4); Integer i1 = 10; Integer i2 = 10; System.out.println(i1 == i2); Integer i3 = 2000; Integer i4 = 2000; System.out.println(i3 == i4); Integer i5 = new Integer(10); Integer i6 = new Integer(10); System.out.println(i5 == i6); int i7 = 2000; int i8 = 2000; System.out.println(i7 == i8); Double f1 = new Double(1.01); Double f2 = new Double(1.01); System.out.println(f1 == f2); Double f3 = 1000.01; Double f4 = 1000.01; System.out.println(f3 == f4); double f5 = 1000.01; double f6 = 1000.01; System.out.println(f5 == f6); } }&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;输出结果是 true false true false false true false false true&lt;/p&gt;
</content:encoded><category>java</category><author>joyme123</author></item><item><title>ubuntu下linux创建桌面启动器以及菜单栏点击失效的解决方法</title><link>https://www.myway5.com/blog/ubuntu-linux-quickstart/</link><guid isPermaLink="true">https://www.myway5.com/blog/ubuntu-linux-quickstart/</guid><description>创建桌面启动器 进入/usr/share/applications vi eclipse.desktop 输入一下内容 \[Desktop Entry\] Encoding=UTF-8 Name=eclipse Comment=Eclipse IDE Exec=/home/ji…</description><pubDate>Wed, 19 Apr 2017 09:44:07 GMT</pubDate><content:encoded>&lt;p&gt;创建桌面启动器 进入/usr/share/applications vi eclipse.desktop 输入一下内容 [Desktop Entry] Encoding=UTF-8 Name=eclipse Comment=Eclipse IDE Exec=/home/jiang/eclipse/eclipse Icon=/home/jiang/eclipse/icon.xpm Terminal=false StartupNotify=true Type=Application Categories=Application;Development; Exec=env UBUNTU_MENUPROXY=0 /home/jiang/eclipse/eclipse 保存后，将这个文件复制到桌面文件夹 cp /usr/share/applications/eclipse.desktop /home/jiang/桌面 对于eclipse打不开菜单的情况，可以修改eclipase文件夹下的eclipse.ini为 -startup plugins/org.eclipse.equinox.launcher_1.3.100.v20150511-1540.jar --launcher.GTK_version 2 --launcher.library plugins/org.eclipse.equinox.launcher.gtk.linux.x86_64_1.1.300.v20150602-1417 -product org.eclipse.epp.package.jee.product --launcher.defaultAction openFile -showsplash org.eclipse.platform --launcher.XXMaxPermSize 256m --launcher.defaultAction openFile --launcher.appendVmargs -vmargs -Dosgi.requiredJavaVersion=1.7 -XX:MaxPermSize=256m -Xms256m -Xmx2048m ———————————— --launcher.GTK_version 2 这一部分是新增的部分&lt;/p&gt;
</content:encoded><category>linux</category><author>joyme123</author></item><item><title>linux下编译安装mysql-connector-cpp</title><link>https://www.myway5.com/blog/linux-complier-mysql-connector-cpp/</link><guid isPermaLink="true">https://www.myway5.com/blog/linux-complier-mysql-connector-cpp/</guid><description>首先在github上下载mysql-connector-cpp git clone https://github.com/mysql/mysql-connector-cpp.git git上面有两个分支，2.0版本是支持mysql作为文档存储的接口，1.</description><pubDate>Wed, 19 Apr 2017 09:43:19 GMT</pubDate><content:encoded>&lt;p&gt;首先在github上下载mysql-connector-cpp git clone &lt;a href=&quot;https://github.com/mysql/mysql-connector-cpp.git&quot; rel=&quot;noopener&quot;&gt;https://github.com/mysql/mysql-connector-cpp.git&lt;/a&gt; git上面有两个分支，2.0版本是支持mysql作为文档存储的接口，1.1版本是支持正常的关系数据库存储接口 首先我们编译安装2.0版本，进入代码目录执行 cmake . cmake --build . --config CCC cmake --build . --target install --config CCC 到这里已经安装完毕. 然后到testapp下编译测试样例 cmake . 提示出错，WITH-CONCPP没有设置，我试了编辑/etc/profile文件，设置这个变量为/usr/local/mysql/connector-c++-2.0/,但是仍然提示没有设置，于是我将CMakeLists中的 set(WITH_CONCPP $ENV{WITH_CONCPP} CACHE PATH &quot;MySQL Connector/C++ 2.0 install location&quot;) 改为 set(WITH_CONCPP &quot;/usr/local/mysql/connector-c++-2.0/&quot; CACHE PATH &quot;MySQL Connector/C++ 2.0 install location&quot;) 继续cmake . 提示出错，在/usr/local/mysql/connector-c++-2.0/lib64下找不到依赖库 那么将目录下生成的 libmysqlcppconn2.so libmysqlcppconn2.so.1 libmysqlcppconn2.so.1.0 拷贝到/usr/local/mysql/connector-c++-2.0/lib64中，没有的创建一个即可。 cmake . make 在run文件夹下面生成了两个可执行文件 devapi_test xapi_test 这两个文件的区别在于，xapi_test实现了c的接口，是通过mysql的一个叫做x plugin的插件连接mysql的，而devapi则是使用了c++的接口。所以如果使用xapi接口的话，mysql安装时必须安装x plugin 接下来编译安装1.1版本，先将当前的改动都commit了，然后 git checkout 1.1， cmake . 如果出错。 报缺少boost库错误，可以 sudo apt-get install libboost-all-dev 报缺少mysql.h等错误，可以通过sudo apt-get install libmysql++-dev 然后make sudo make install 然后将生成的动态库拷贝的/usr/local/lib下 sudo cp libmysqlcppconn.so* /usr/local/lib/&lt;/p&gt;
</content:encoded><category>linux</category><author>joyme123</author></item><item><title>c++中的中文字符分割</title><link>https://www.myway5.com/blog/c-plus-plus-chinese-split/</link><guid isPermaLink="true">https://www.myway5.com/blog/c-plus-plus-chinese-split/</guid><description>在utf-8编码的前提下，一个字符可能占用的空间为1~4个char，由于这种不确定的字符长度导致在utf-8编码的string中，无法直接使用substr进行字符串分割。</description><pubDate>Wed, 19 Apr 2017 09:42:41 GMT</pubDate><content:encoded>&lt;p&gt;在utf-8编码的前提下，一个字符可能占用的空间为1~4个char，由于这种不确定的字符长度导致在utf-8编码的string中，无法直接使用substr进行字符串分割。 解决的方案的关键在于利用utf-8编码的特点，utf-8的第一个字符的前缀连续为1的个数代表这个”字“占用的字符数量，当只有一个字符时，第一个字符的第一位为0。具体的说明可以参考这篇文章。&lt;a href=&quot;http://www.ruanyifeng.com/blog/2007/10/ascii%5C_unicode%5C_and%5C_utf-8.html&quot; rel=&quot;noopener&quot;&gt;http://www.ruanyifeng.com/blog/2007/10/ascii\_unicode\_and\_utf-8.html&lt;/a&gt; 这里主要说明怎么利用代码解决这个问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int getUtf8CharLength(char c){
    int len = 0;
    if(c &amp;gt; 0){              //利用计算机中数字的存储特点，第一位为1为负，第一位为0为正，很容易判断
        return len + 1;     //第一位为0时为特殊情况，需要加1
    }
    while(c &amp;lt; 0){
        len++;
        c = c &amp;lt;&amp;lt; 1;
    }
    return len;
}

std::string substrWithChinese(std::string str,unsigned int start,unsigned int length){
    unsigned int i = 0;             //标识前进的几个“字符”
    unsigned int cursor = 0;        //标识前进了几个“字”
    unsigned int save = 0;          //保存的字符标识
    unsigned int len = str.length();
    char* c = new char[len + 1];
    while(str[i] != &apos;\0&apos;&amp;amp;&amp;amp;length &amp;gt; 0){
        unsigned int l = getUtf8CharLength(str[i]); //获取字符长度
        if(cursor &amp;gt;= start){
            unsigned int m = l;
            while(m--){
                c[save++] = str[i++];
            }
            length--;
        }else{
            i+=l;
        }
        cursor++;
    }
    c[i] = &apos;\0&apos;;
    return std::string(c);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>c++</category><author>joyme123</author></item><item><title>ubuntu eclipse mars.2Bug修复</title><link>https://www.myway5.com/blog/ubuntu-eclipse-mars-2-bug-fix/</link><guid isPermaLink="true">https://www.myway5.com/blog/ubuntu-eclipse-mars-2-bug-fix/</guid><description>ubuntu eclipse mars.2Bug的bug具体表现为图标异常，菜单无法正常点击切换等等，问题是因为GTK方面，将eclipse的启动方式eclipse.desktop改为一下代码</description><pubDate>Wed, 19 Apr 2017 09:41:37 GMT</pubDate><content:encoded>&lt;p&gt;ubuntu eclipse mars.2Bug的bug具体表现为图标异常，菜单无法正常点击切换等等，问题是因为GTK方面，将eclipse的启动方式eclipse.desktop改为一下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   [Desktop Entry]
   Name=eclipse
   Comment=develop java app
   Exec=env SWT_GTK3=0 /home/jiang/software/eclipse/eclipse
   Icon=/home/jiang/software/eclipse/icon.xpm
   Terminal=false
   Type=Application
   Categories=devtool
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>问题</category><author>joyme123</author></item><item><title>网站登录系统设计</title><link>https://www.myway5.com/blog/web-login-system-design/</link><guid isPermaLink="true">https://www.myway5.com/blog/web-login-system-design/</guid><description>一、前言 一个网站的登录系统可以算作是一个网站的大门，如何让这个大门足够的安全，并且又不用太复杂是一个很考验经验的工作。作为一个没有经验的新手，简单记下一点自己在做网站登录系统设计时做法。</description><pubDate>Wed, 19 Apr 2017 09:40:45 GMT</pubDate><content:encoded>&lt;p&gt;一、前言 一个网站的登录系统可以算作是一个网站的大门，如何让这个大门足够的安全，并且又不用太复杂是一个很考验经验的工作。作为一个没有经验的新手，简单记下一点自己在做网站登录系统设计时做法。 二、前提 首先假设我们设计的网站是一个单服务器的应用，并且他有多种登录方式（邮箱，手机，用户名），以及多种登录途径（web，app）。 三、正文 1.会话状态 因为是单服务器的应用，所以不用担心多台服务器之间的会话状态转移的问题，所以为了简单，我们采用的是cookie方式，这样既不用在用户关闭浏览器后再次访问就要重新登录，也可以通过设定cookie的失效时间，来让浏览器自动在一定时间后让用户重新登录，保护用户设备丢失带来的风险。 2.登录方式 考虑到需要支持邮箱，手机或者是用户名的登录，也就是用户随意输入其中的一种以及密码都要可以登录，所以要对用户名有一定的限制，首先用户名不能重复，并且用户不能是手机或者邮箱，因此要对用户名做一些简单的限制——用户名不能为纯数字，也不能包含@符号。 3.密码存储 密码的存储是一定要足够安全的。因此对密码的存储一定要够安全。我采用的是密码+salt的方式，使用sha-256加密。 4.身份验证 身份验证上，我在cookie中填入用户id和token连接而成的字符串（称为id_token)，例如24|dashdiuasdhgiuagsduigaiusdg这种形式，|符号前面是Id，后面是token，token是登录时随机生成的，在登录时会讲登录记录插入到数据库中，这条记录包括用户id，登录名(用户名，邮箱或手机中的任一种)、登录途径(web还是app)、登录时生成的token、登录时间等等，然后之后用户提交请求时都会验证cookie,将cookie中的id和token截取出来，与最新的一条该用户id的登录成功的记录的token对比，如果一致，则表示身份是对的。这种做法可以很好的防止伪造cookie。对于app来说，将这个cookie保存下来就可以一直作为登录凭证。如果遇到手机丢失，想使手机上的登录失效，重新用新手机登录一次即可。 5.多端登录的冲突解决 在4.身份验证中说到，登录的身份验证是根据最新的该用户的登录记录来判断的，所以如果用户在app上登录了，再在网站上登录则会使app的登录失效，为了解决这个问题，可以在登录时附带上登录方式的字段，那么就可以解决web端和app端的冲突问题。&lt;/p&gt;
</content:encoded><category>架构设计</category><author>joyme123</author></item><item><title>linux下使用crontab进行网站内容和数据库定期备份</title><link>https://www.myway5.com/blog/linux-crontab-backup/</link><guid isPermaLink="true">https://www.myway5.com/blog/linux-crontab-backup/</guid><description>crontab有几个常用的命令 crontab -l 列举crontab的任务 crontab -e 编辑crontab的任务 crontab -r 删除crontab的任务 crontab -h crontab的帮助 crontab -i 删除crontab前进行提示 cro…</description><pubDate>Wed, 19 Apr 2017 09:39:52 GMT</pubDate><content:encoded>&lt;p&gt;crontab有几个常用的命令 crontab -l 列举crontab的任务 crontab -e 编辑crontab的任务 crontab -r 删除crontab的任务 crontab -h crontab的帮助 crontab -i 删除crontab前进行提示 crontab -e进行编辑任务时会使用nano进行编辑，编辑命令如下示例。 分 时 每月的第几天 每周的周几 命令 前5个参数可以使用通配符 *&lt;/p&gt;
&lt;h1&gt;m h dom mon dow command&lt;/h1&gt;
&lt;h1&gt;设置每两分钟扫描一次，清理过期记录&lt;/h1&gt;
&lt;p&gt;*/2 * * * * /usr/bin/php /var/www/poker/scan.php &amp;gt;&amp;gt; /$&lt;/p&gt;
&lt;h1&gt;设置每周二23:29分备份一次&lt;/h1&gt;
&lt;p&gt;59 23 * * 2 /bin/bash /home/back.sh 59 23 * * 2 /bin/bash /home/back.sh就是我用来定期备份的指令，主要就是配合一个shell脚本 #!/bin/bash cd /home mv backup/* oldbackup/ echo &quot;old backup file has been moved to oldbackup folder&quot;; cd /home/backup Now=&lt;code&gt;date &quot;+%Y-%m-%d-%H-%M-%S&quot;&lt;/code&gt; File_emlog=&quot;backup-emlog-&quot;${Now}&quot;.sql&quot; mysqldump -u me -ppassword emlog &amp;gt; $File_emlog File_java=backup-java-$Now.sql mysqldump -u me -ppassword java &amp;gt; $File_java File_plan=backup-plan-$Now.sql mysqldump -u me -ppassword plan &amp;gt; $File_plan File_sms=backup-sms-$Now.sql mysqldump -u me -ppassword sms &amp;gt; $File_sms File_tu=backup-tu-$Now.sql mysqldump -u me -ppassword tu &amp;gt; $File_tu File_wordpress=backup-wordpress-$Now.sql mysqldump -u me -ppassword wordpress &amp;gt; $File_wordpress echo &quot;your databases backup successfully completed&quot;; #数据文件到这里备份完毕 #下面备份网站的文件 tar -czf /home/backup/back_www.tar.gz /var/www&lt;/p&gt;
</content:encoded><category>linux</category><author>joyme123</author></item><item><title>JavaEE post Base64的图片丢失数据解决</title><link>https://www.myway5.com/blog/javaee-post-base64-data-lost/</link><guid isPermaLink="true">https://www.myway5.com/blog/javaee-post-base64-data-lost/</guid><description>项目中有个提交Base64编码的图片到服务器，服务器端转换成图片的二进制流存放到数据库中。在实际使用中发现，保存到数据库中图片文件有损坏。表现为前一部分二进制数据相同，到了后面就开始不同了。 一开始的步骤为:</description><pubDate>Wed, 19 Apr 2017 09:37:55 GMT</pubDate><content:encoded>&lt;p&gt;项目中有个提交Base64编码的图片到服务器，服务器端转换成图片的二进制流存放到数据库中。在实际使用中发现，保存到数据库中图片文件有损坏。表现为前一部分二进制数据相同，到了后面就开始不同了。 一开始的步骤为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;st=&amp;gt;start: 准备Base64数据
op1=&amp;gt;operation: Post准备好的Base64数据
op2=&amp;gt;operation: 将数据并转换为byte[]
op3=&amp;gt;operation: 存储到数据库
e=&amp;gt;end

st-&amp;gt;op1-&amp;gt;op2-&amp;gt;op3-&amp;gt;end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了找出问题，将post这个步骤省去，并将数据直接存成图片以便观察&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;st=&amp;gt;start: 准备Base64数据
op2=&amp;gt;operation: 将数据并转换为byte[]
op3=&amp;gt;operation: 存储成图片
e=&amp;gt;end

st-&amp;gt;op2-&amp;gt;op3-&amp;gt;end
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;这一步保存的文件的大小是9.0k&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;st=&amp;gt;start: 准备Base64数据
op1=&amp;gt;operation: Post准备好的Base64数据
op2=&amp;gt;operation: 将数据并转换为byte[]
op3=&amp;gt;operation: 存储成图片
e=&amp;gt;end

st-&amp;gt;op1-&amp;gt;op2-&amp;gt;op3-&amp;gt;end
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;这一步保存的文件的大小是8.9k&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面两步对比可以说明数据在传输的时候出现了丢失的情况，于是我猜测是不是tomcat对post方式做了限制，发现不是。查询资料发现，Base64位数据传输的时候中间的+号容易发生丢失。解决方法是将post过来的数据先UrlEncode一下即可 发生这个问题的没有找到明确的可靠的解释，其中一片文章解释为javascript将+当做字符串连接符号处理导致丢失。&lt;/p&gt;
</content:encoded><category>java</category><author>joyme123</author></item><item><title>Java虚拟机学习记录(九)——类文件结构(上)</title><link>https://www.myway5.com/blog/java-study-lesson-9/</link><guid isPermaLink="true">https://www.myway5.com/blog/java-study-lesson-9/</guid><description>##一、前言 在Java开发中，可以通过javac将.java的源代码编译为.class的类文件。之前一直以为，只有java语言可以编译为.class。但是在前些天的学习中，了解到不仅仅是Java语言可以编译成.</description><pubDate>Wed, 19 Apr 2017 09:36:58 GMT</pubDate><content:encoded>&lt;p&gt;##一、前言 在Java开发中，可以通过javac将.java的源代码编译为.class的类文件。之前一直以为，只有java语言可以编译为.class。但是在前些天的学习中，了解到不仅仅是Java语言可以编译成.class文件然后运行在Java虚拟机上，Clojure、Groovy、JRuby、Jython、Scala等语言都可以运行在Java虚拟机上。觉得这真是太神奇了。今天这一章的内容刚好可以解释这些。 ##二、Java虚拟机的无关系特点 一般都说Java是平台无关的，因为Java是运行在Java虚拟机上的，而Java虚拟机是开发成各个平台通用的。那么同时，Java虚拟机其实也是语言无关的，也就是说，Java虚拟机并不要求特定的语言。只要该语言可以被编译生成符合标准的class(类)文件，那么就是可以运行在Java虚拟机上了。那么，类文件的结构是什么样的呢？ ##三、类文件的基本知识 ##1.基本单位 类文件是以8位字节为基础单位的二进制流，没有任何分割符。当遇到需要占用8位以上的数据时，则会按高位在前的方式分割成若干个8位字节进行存储。 ###2.存储数据的数据格式:无符号数和表。 无符号数属于基本数据类型。以u1,u2,u4,u8来分别代表1个字节，2个字节，4个字节，8个字节的无符号数，无符号数可以用来描述数字、索引引用、数量值或者安装UTF-8编码构成的字符串值。 表是由多个无符号数或者其他表作为数据项构成的复合数据类型，所有表都习惯性以_info结尾。 ##三、类文件的结构 ###1.魔数和版本号 class文件的前四个字节称为魔数(Magic Number)，用来描述文件的格式，class文件的魔数是0xCAFEBABE。第五六个字节描述次版本号(Minor Version),第七八个字节描述主版本号。 ###2.常量池 紧接着主版本号之后是常量池的入口。由于常量池的常量数量是变化的，所以在常量池的入口有一个u2类型（第9,10位)的数据，代表常量池容量计数值。不过这个计数是从1开始的，所以如果这个值是22，则代表有21个常量。 常量池中主要有两种类型:字面量(Literal)和符号引用(Symbolic References)。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;a.字面量: 接近java语言的常量的概念，如文本字符串，声明为final的常量值等 b.符号引用:1.类和接口的全限定名(Fully Qualified Name) 2.字段的名称和描述符(Descriptor) 3.方法的名称和描述符&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;java代码在编译时不会像c/c++一样进行&quot;连接&quot;，这样在编译生成的class文件中不会保存各个方法、字段的最终内存布局信息，而是在运行的时候进行动态连接。也就是从常量池中获得方法、字段对应的符号引用，再在类创建或运行时解析翻译到具体的内存地址之中。 常量池中每一个常量都是一个表，在JDK1.7前共有11中常量，在JDK1.7中为了更好的支持动态语言调用，又额外增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。 这14个表的共同之处在于，表开始的第一项是一个u1类型的标志位(tag),代表当前这个常量属于哪种常量类型。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;标志&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Utf8_info&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;UTF-8编码的字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Integer_info&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;整型字面量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Float_info&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;浮点型字面量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Long_info&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;长整型字面量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Double_info&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;双精度浮点型字面量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Class_info&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;类或接口的符号引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_String_info&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;字符串类型字面量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Fieldref_info&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;字段的符号引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_Methodref_info&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;类中方法的符号引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_InterfaceMathodref_info&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;接口中方法的符号引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_NameAndType_info&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;字段或方法的部分符号引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_MethodHandle_info&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;表示方法句柄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_MethodType_info&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;表示方法类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONSTANT_InvokeDynamic_info&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;表示一个动态方法调用点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;###3.访问标志 在常量池结束之后，紧接着的是访问标志，用来描述一些类和接口的访问信息，包括这个Class是类还是接口，是否定义为public,是否是abstract如果是类的话，是否是final。 ###4.类索引，父类索引与接口索引集合 访问标志之后是类索引，父类索引与接口索引集合。类索引(this_class)和父类索引(super_class)都是一个u2类型的数据，而接口索引集合(interfaces)是一组u2类型的数据集合(对应java语言中的单继承和多接口实现), ###5.字段表集合 再之后是字段表集合。字段表(Field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量，但不包括在方法内部声明的局部变量。一个字段的描述包括:字段的作用域(public,private,protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符，是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型，数组，对象)、字段名称。 ###6.方法表集合 字段表之后是方法表集合。很显然，对方法的描述和对字段的描述是很像的。volatile和transient不能描述方法，但是syncronized、native、strictfp和abstract是方法独有的。&lt;/p&gt;
</content:encoded><category>java虚拟机学习</category><author>joyme123</author></item><item><title>Java虚拟机学习记录(八) —— 虚拟机性能监控与故障处理工具</title><link>https://www.myway5.com/blog/jvm-study-lesson-8/</link><guid isPermaLink="true">https://www.myway5.com/blog/jvm-study-lesson-8/</guid><description>我觉得Java的强大之处在于它有非常完善的生态环境，从开发工具到分析处理工具。使用JDK中提供的工具可以在遇到程序故障时快速定位故障发生的原因并进行调优。 二、JDK命令行工具 a、jps(JVM Process Status):虚拟机进程状况工具</description><pubDate>Wed, 19 Apr 2017 09:36:08 GMT</pubDate><content:encoded>&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;我觉得Java的强大之处在于它有非常完善的生态环境，从开发工具到分析处理工具。使用JDK中提供的工具可以在遇到程序故障时快速定位故障发生的原因并进行调优。&lt;/p&gt;
&lt;h2&gt;二、JDK命令行工具&lt;/h2&gt;
&lt;h3&gt;a、jps(JVM Process Status):虚拟机进程状况工具&lt;/h3&gt;
&lt;p&gt;jps可以用来列出正在运行的虚拟机进程，并显示虚拟机执行主类（Main class,main函数所在的类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;jps命令格式: jps [option] [hostid] 使用范例: jiang@jiang-HP-ENVY-Notebook:~$ jps -l 6084 /home/jiang/eclipse//plugins/org.eclipse.equinox.launcher_1.3.200.v20160318-1642.jar 6152 sun.tools.jps.Jps jps可以通过RMI协议查询开启了RMI服务的远程虚拟机进程状态，hostid为RMI注册表中注册的主机名。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选项&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-q&lt;/td&gt;
&lt;td&gt;只输出LVMID,省略主类的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-m&lt;/td&gt;
&lt;td&gt;输出虚拟机进程启动时传递给主类main()函数的参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-l&lt;/td&gt;
&lt;td&gt;输出主类的全名，如果进程执行的是Jar包，输出Jar路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-v&lt;/td&gt;
&lt;td&gt;输出虚拟机进程启动时JVM参数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;b、jstat(JVM Statistics Monitoring Tool): 虚拟机统计信息监视工具&lt;/h3&gt;
&lt;p&gt;jstat是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、JIT编译等运行数据。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;jstat命令格式为: jstat [option vmid [interval [s|ms] [count]] ] 如果VMID是本地进程，和LVMID是一样的，如果是远程虚拟机进程，那VMID的格式是: [protocol:][//]lvmid[@hostname[:port]/servername] 参数inerval和count代表查询间隔和次数，如果省略则表示只查询一次。 使用范例: jstat -gc 2764 250 20 代表每250ms查询一次进程2764垃圾收集状况，一共查询20次&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选项&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-class&lt;/td&gt;
&lt;td&gt;监视类装载，卸载数量，总空间以及类装载所耗费的时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-gc&lt;/td&gt;
&lt;td&gt;监视Java堆状况，包括Eden区，两个survivor区，老年代，永久代等的容量、已用空间、GC时间合计等信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-gccapacity&lt;/td&gt;
&lt;td&gt;监视内容与-gc基本相同，但输出主要关注Java堆各个区域使用到的最大、最小空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-gcutil&lt;/td&gt;
&lt;td&gt;监视内容与-gc基本相同，但输出主要关注已使用空间占总空间的百分比&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-gccause&lt;/td&gt;
&lt;td&gt;与-gcutil功能一样，但会额外输出导致上一次GC的原因&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-gcnew&lt;/td&gt;
&lt;td&gt;监视新生代GC状况&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-gcnewcapacity&lt;/td&gt;
&lt;td&gt;监视新生代使用到的最大、最小空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-gcold&lt;/td&gt;
&lt;td&gt;监视老年代GC状况&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-gcoldcapacity&lt;/td&gt;
&lt;td&gt;监视老年代使用到的最大、最小空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-gcpermcapacity&lt;/td&gt;
&lt;td&gt;输出永久代使用到的最大、最小空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-compiler&lt;/td&gt;
&lt;td&gt;输出JIT编译器编译过的方法、耗时等信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-printcompilation&lt;/td&gt;
&lt;td&gt;输出已被JIT编译的方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded><category>java虚拟机学习</category><author>joyme123</author></item><item><title>Java虚拟机学习记录(七)——内存分配与回收策略</title><link>https://www.myway5.com/blog/jvm-study-lesson-7/</link><guid isPermaLink="true">https://www.myway5.com/blog/jvm-study-lesson-7/</guid><description>Java的内存分配，从全局来看，就是堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接的栈上分配，对象的分配主要在新生代的Eden区上，如果开启了本地线程分配缓冲，将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中(大的对象直接进入老年代）。</description><pubDate>Wed, 19 Apr 2017 09:35:20 GMT</pubDate><content:encoded>&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;Java的内存分配，从全局来看，就是堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接的栈上分配，对象的分配主要在新生代的Eden区上，如果开启了本地线程分配缓冲，将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中(大的对象直接进入老年代）。&lt;/p&gt;
&lt;h2&gt;二、对象优先在Eden分配&lt;/h2&gt;
&lt;p&gt;Java堆的新生代中，被分为Eden和两个survivor。大多数情况下，对象在Eden区优先分配，当Eden区没有足够的空间进行分配时，虚拟机将会发起一起Minor GC 新生代GC(Minor GC):指发生在新生代的垃圾回收动作。 老年代GC(Major GC/Full GC):指发生在老年代的GC。&lt;/p&gt;
&lt;h2&gt;三、大对象直接进入老年代&lt;/h2&gt;
&lt;p&gt;大对象指的是要占用大量连续内存空间的Java对象，最典型的大对象就是那种很长的字符串以及数组。大对象的内存分配堆虚拟机来说是一件很难的事，因为往往会因为找不到这样的连续的内存空间而不得不提前触发一次Full GC。因此，在写程序中要尽量避免这样大对象，尤其是生命周期很短的大对象。大对象的分配一般会直接进入老年代。（这里可以认为JVM是很不支持生命周期很短的大对象的创建的)。&lt;/p&gt;
&lt;h2&gt;四、长期存活的对象将进入老年代&lt;/h2&gt;
&lt;p&gt;虚拟机为每个对象定义了一个年龄计数器，在一次GC后仍然存活的对象它的年龄就会+1，如果对象在Eden出生并且经过第一次Minor GC仍然存活并且Survivor能够容纳就会被移到Survivor(其实就是标记-复制法)。当它的年龄达到一定的程度(默认为15岁)，就会被移入老年代。可以通过-XX:MaxTenuringThreshold来设定这个年龄阈值。&lt;/p&gt;
&lt;h2&gt;五、动态年龄判定&lt;/h2&gt;
&lt;p&gt;为了更好地适应不同程序的内存情况，虚拟机并不是永远都要求对象的年龄必须达到了MaxTenuringThreshold才能进入老年代，如果在Survivor区中相同年龄所有对象的大小总和大于Survivor空间的一半，年龄大于或等于该年龄的对象就可以直接进入老年代(节省了一半以上的Survivor内存)。&lt;/p&gt;
&lt;h2&gt;六、空间分配担保&lt;/h2&gt;
&lt;p&gt;在新生代的Minor GC是采用的标记——复制法,所以一次Minor GC后会将存活的对象移到另一个空闲的Survivor区中，但是没有人可以保证一次GC后存活的对象是Survivor能够容纳的，那么就需要老年代的进行空间分配担保，以防止在容纳不下的情况下有后备的解决方案。所以过程是: Minor GC发生之前:检查老年代的连续空间是否能够容纳下新生代的所有对象，如果可以，则可以确保Minor GC是正常的。如果不可以，虚拟机就会检查HandlePromotionFailure设置值是否允许担保失败。如果允许，那么就会检查老年代最大可用连续空间是否大于历次进入老年代对象的平均大小，如果大于，则尝试一次Minor GC；如果小于，或者不允许担保失败，就要进行一次Full GC（所以可以理解为Full GC是为了给Minor GC腾出空间，所以Full GC之后往往跟随着一次Minor GC)。&lt;/p&gt;
</content:encoded><category>java虚拟机学习</category><author>joyme123</author></item><item><title>Java虚拟机学习记录(六)——HotSpot算法实现</title><link>https://www.myway5.com/blog/jvm-study-lesson-6/</link><guid isPermaLink="true">https://www.myway5.com/blog/jvm-study-lesson-6/</guid><description>在JVM运行的过程中，垃圾回收是性能提升的重中之重，垃圾回收的前提是准确判定哪些对象是可以回收的，在前面的学习中说到，Java的大多数虚拟机都是通过可达性分析算法来判定对象能否回收。那么如何去找这些根节点(GC ROOTS)的位置也是一个要解决的问题。</description><pubDate>Wed, 19 Apr 2017 09:34:36 GMT</pubDate><content:encoded>&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;在JVM运行的过程中，&lt;strong&gt;垃圾回收&lt;/strong&gt;是性能提升的重中之重，垃圾回收的前提是准确判定哪些对象是可以回收的，在前面的学习中说到，Java的大多数虚拟机都是通过可达性分析算法来判定对象能否回收。那么如何去找这些**根节点(GC ROOTS)**的位置也是一个要解决的问题。&lt;/p&gt;
&lt;h2&gt;二、HotSpot枚举根节点(GC Roots)&lt;/h2&gt;
&lt;p&gt;根节点主要在全局性的引用(常量和类的静态属性)和执行上下文中(例如栈帧的本地变量表中),因为这些区域的占用内存往往很大，不可能去逐个检索，因为这个操作太耗时了。 同样的，可达性分析的时间严格要求还体现在GC停顿上，GC停顿就是指在可达性分析期间所有的Java执行线程得全部停下来，等分析完成之后再开始重新运行，如果不停顿，就可能导致分析期间，引用关系还在不断的改变。很显然，这个停顿时间必须短不然用户体验极差。Sun把这个停顿叫做Stop the world。 HotSpot采用一种叫OopMap的数据结构来记录所有的执行上下文和全局引用位置，这样就可以直接得到所有的GC Roots的地址了。 但是在Java程序运行过程中，有很多指令会导致对象的引用关系发生变化，如果每个变化都要写入到OopMap中，那样就需要维护一个很大的OopMap数据结构，会占用大量的空间。所以在Jvm中有一个**安全点(SafePoint)&lt;strong&gt;的概念。HotSpot只在安全点处记录了这些信息，然后开始GC。安全点的选定不能太少造成GC等待时间过长，也不能频繁GC增加运行负荷。 如何让所有线程跑到安全点时停止下来，有两种方案可以选择，&lt;strong&gt;抢先式中断(Preemptive Suspension)&lt;strong&gt;和&lt;/strong&gt;主动式中断(Voluntary Suspension)&lt;/strong&gt;。 其中抢先式中断不需要线程的执行代码主动配合，在GC发生时，首先让所有线程中断，如果发现线程中断的地方不在安全点上，就恢复线程，让它跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。 主动式中断的思想是当GC需要中断线程的时候，不直接对线程操作，仅仅简单地设置一个标志，各个线程去主动轮询这个标志，发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的，另外加上创建对象需要分配内存的地方。 但是安全点只能很好的解决运行中的程序，对于&quot;不运行&quot;的程序也就也就无法进入安全点，也就无法进行GC。这里的不运行指的是线程没有分配到CPU时间，典型的例子就是线程处于Sleep状态或者Blocked状态，这时候线程是无法响应JVM的中断请求的，&quot;走&quot;安全的地方去中断挂起,JVM显然也不太可能等待线程重新被分配CPU时间。这种情况需要借助&lt;/strong&gt;安全区域（Safe Region)**来解决。 安全区域指的是在一段代码中，引用关系不会发生变化。在这个区域任意开GC都是安全的。我们也可以把安全区域当成是安全点的扩展。 当线程执行到安全区域中时，首先标识自己进入了安全区域。在这段时间内JVM发起GC就不用管安全区域里的线程了。但是在安全区域内的线程重新获得CPU时间要离开Safe Region时，要检查系统是否已经完成了根节点枚举。完成了才能离开否则就要等待完成。 ##三 HotSpot垃圾收集器的实现&lt;/p&gt;
&lt;h3&gt;a.Serial收集器&lt;/h3&gt;
&lt;p&gt;这是一款最基本，发展历史最悠久的收集器。这个收集器是一个单线程收集器，并且在收集垃圾时，必须暂停其他所有工作线程。适用于作为Client模式下的虚拟机。&lt;/p&gt;
&lt;h3&gt;b.ParNew收集器&lt;/h3&gt;
&lt;p&gt;其实就是Serial收集器的多线程版本。在单CPU的环境中，性能比不上Serial收集器，但是多CPU的时候性能是要好过Serial收集器的。&lt;/p&gt;
&lt;h3&gt;c.parallel Scavenge收集器&lt;/h3&gt;
&lt;p&gt;新生代收集器，复制算法，这个收集器与其他收集器的区别在于，Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。吞吐量=运行用户代码所花费的时间/（运行用户代码时间+垃圾收集时间)。而其他收集则是关注减少GC停顿的时间。&lt;/p&gt;
&lt;h3&gt;d.Serial Old收集器&lt;/h3&gt;
&lt;p&gt;是Serial收集器的老年代版本，使用标记整理算法。也是给Client模式下的虚拟机使用。在server模式下，还有两大用途:1.在JDK1.5之前和Parallel Scavenge配合使用; 2作为CMS收集器的后备预案，在并发收集发生Concurrent Mode Failure失败时使用。&lt;/p&gt;
&lt;h3&gt;e.Parallell Old收集器&lt;/h3&gt;
&lt;p&gt;是Parallel Scavenge收集器的老年代版本，使用多线程和标记-整理算法。配合paraller Scavenge使用。&lt;/p&gt;
&lt;h3&gt;f.CMS收集器&lt;/h3&gt;
&lt;p&gt;Concurrent Mark Sweep。以获取最短时间停顿为目标，基于“标记-清除”算法。包括四个步骤: 1.初始标记 2.并发标记 3.重新标记 4.并发清除 他有一下几个缺点: 1.对CPU资源敏感，对性能影响大 2.无法清理浮动垃圾 3.容易产生内存碎片，触发Full GC。&lt;/p&gt;
&lt;h3&gt;g.G1收集器&lt;/h3&gt;
&lt;p&gt;是一款面向服务器的垃圾收集器。有以下特点: 1.并行和并发 2.分代收集 3.空间整合 4.可预测的停顿&lt;/p&gt;
</content:encoded><category>java虚拟机学习</category><author>joyme123</author></item><item><title>Java虚拟机学习记录(五)-垃圾收集器</title><link>https://www.myway5.com/blog/jvm-study-lesson-5/</link><guid isPermaLink="true">https://www.myway5.com/blog/jvm-study-lesson-5/</guid><description>众所周知，Java与C的一个显著的区别在于c需要手动的去管理内存，而Java几乎不需要去做这样的处理。原因在于Java的虚拟机有一套自己的内存回收策略。 在Java虚拟机运行过程中，虚拟机栈、程序计数器、本地方法栈随着线程的生命周期的变化而变化，因此这一部分的内存是不需要额外的…</description><pubDate>Wed, 19 Apr 2017 09:33:52 GMT</pubDate><content:encoded>&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;众所周知，Java与C的一个显著的区别在于c需要手动的去管理内存，而Java几乎不需要去做这样的处理。原因在于Java的虚拟机有一套自己的内存回收策略。 在Java虚拟机运行过程中，虚拟机栈、程序计数器、本地方法栈随着线程的生命周期的变化而变化，因此这一部分的内存是不需要额外的去回收。但是对于Java堆来说，几乎所有的对象的创建（这里之所以说几乎，是因为随着JIT编译器、对象逃逸分析和栈上分配的技术发展，部分对象不需要在堆中分配内存）都是在堆中进行，如果不作内存回收，很快就会被占满内存，那么怎么样去分析得到那些对象已经不再需要则是问题的关键。**所以，要实现垃圾回收，先要判断对象是否已死，然后再对已死对象执行回收算法。**下面记录几种常见的在垃圾回收中的算法&lt;/p&gt;
&lt;h2&gt;二、对象是否已死的判定——引用计数法&lt;/h2&gt;
&lt;p&gt;这种方法原理很简单，就是说每个对象增加一个引用就给他的计数器加1，当引用失效时(这里的引用失效，我觉得是说当程序的执行离开了对象的作用域，那么这个引用就算是失效了)，计数器就减一。当计数器再次变为0时，这个对象就判定它已经可以回收了。 但是这种方法也有一个严重的问题，当ObjectA.instance = ObjectB;ObjectB.instance = ObjectA时，这里两个对象互相引用着导致引用计数一直不为0。因此在JVM的主流实现中，都不采用这种方法。据说python的GC算法就是引用计数加上辅助算法完成的。&lt;/p&gt;
&lt;h2&gt;三、对象是否已死的判定——可达性分析算法&lt;/h2&gt;
&lt;p&gt;可达性算法是借助树的数据结构的一个算法，通过判定一个对象是否可以通过树的根到达来确定其是否需要回收。引用一张来自网上的图片&lt;a href=&quot;http://blog.csdn.net/derrantcm/article/details/44409835&quot; rel=&quot;noopener&quot;&gt;图片引用地址&lt;/a&gt;。 &lt;img src=&quot;/uploads/wp/2017/08/%E5%8F%AF%E8%BE%BE%E6%80%A7%E5%88%86%E6%9E%90.png&quot; alt=&quot;可达性分析&quot; /&gt; 在这里，虽然object 8,object 9,object 10,object 11,object 12都互相持有引用，但是因为从GC Roots中无法到达，所以可以判定为可回收对象。很好的解决了引用计数法的弊端。 在Java中，可作为GC Roots的对象包括： 虚拟机栈（栈帧中的本地变量表)中的引用对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 本地方法栈中JNI引用的对象&lt;/p&gt;
&lt;h2&gt;四、垃圾回收算法——标记-清除算法&lt;/h2&gt;
&lt;p&gt;标记-清除算法共有两个阶段。标记和清除 标记是将内存空间扫描一遍，对所有可回收的对象做标记。清除是对内存空间再做一遍扫描，清除可回收的对象。这种方法有两个问题：一是要做两遍扫描，效率不高，二是会产生大量的内存碎片(回收的对象随机分布造成)，当分配一个很大的对象时很有可能找不到这样的连续空间而提前触发一次垃圾回收。因此这种算法大多数的JVM的实现都不使用。&lt;/p&gt;
&lt;h2&gt;五、垃圾回收算法——复制算法&lt;/h2&gt;
&lt;p&gt;复制算法最大的特点是将内存空间分为大小相同的两部分。当开始垃圾回收时，只需将存活的对象移到另一块没有使用的内存空间，然后将使用的一边全部回收，这样的做法简单高效，但是对内存的浪费实在是太大了。就像是你买了8G的内存条只能使用4G，你肯定是不愿意的。 但是实际上，现在的商业虚拟机都采用这种算法的优化版本来回收新生代。因为在新生代中（Java堆会分为&lt;strong&gt;新生代&lt;/strong&gt;[Young Generation]和&lt;strong&gt;年老代&lt;/strong&gt;[Old Generation]）中的对象绝大多数都是可回收的，那么实际上每次做垃回收时，新生代中存活的对象并不多。所以并不需要按照1:1进行空间分割，一般情况下，会将新生代分为一块较大的Eden空间和两块较小的Survivor空间，每次使用Eden空间和一块Survivor空间，当进行垃圾回收时，将这两块空间中的存活对象移到另一块空闲的Survivor空间。然后将对象全部清除。这样只有10%的内存会被浪费。 经历了几次垃圾回收都依然存活的对象会被放置到年老代中，因此年老代中的对象都是不容易被回收的。 因为没有办法保证每次的垃圾回收过程存活的对象都不超过10%，所以当Survivor空间不够用时，需要依赖其他内存(这里指年老代）进行分配担保(Handle Promotion)。&lt;/p&gt;
&lt;h2&gt;六、垃圾回收算法——标记-整理法&lt;/h2&gt;
&lt;p&gt;上面的复制算法很明显不适用于年老代，因为年老代中的对象特点是存活率大。标记-整理算法与标记-清除算法类似，但是它不是直接对对象进行清除，而是将存活的对象向一端移动，然后直接清理掉端边界意外的内存。引用一张来自网络上的图片&lt;a href=&quot;https://my.oschina.net/winHerson/blog/114391&quot; rel=&quot;noopener&quot;&gt;图片引用地址&lt;/a&gt; &lt;img src=&quot;/uploads/wp/2017/08/%E5%A4%8D%E5%88%B6%E6%B8%85%E7%90%86-1.jpg&quot; alt=&quot;此处输入图片的描述&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;七、扩展一下引用的知识&lt;/h2&gt;
&lt;p&gt;在四、五的判定对象是否已死中，均涉及到了引用。在上面的表述中，似乎只有引用和死亡两种状态。但是事实上，Java规定了四种引用状态来帮助Java程序获得更好的性能。怎么去理解这个，一般来说，当Java虚拟机的内存足够时，有的对象虽然已经不需要了，但是完全没有必要将它丢弃，只有在进行垃圾回收后内存仍然不足时才将这些对象回收。这样就增加了这些对象的复用。免去了下一次使用这些对象又要重新创建的问题。很多系统的缓存功能都符合这样的应用场景。 在JDK1.2后，Java扩充了引用的概念，分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。 &lt;strong&gt;a.强引用&lt;/strong&gt; Object obj = new Object()这种就是强引用，只要这个引用存在，无论如何JVM都不会回收这些对象 &lt;strong&gt;b.软引用&lt;/strong&gt; 软引用用来描述一些还有用但并非必需的对象。用软引用的对象在系统进行过垃圾回收仍然内存不足时才会进行回收。在JDK1.2之后，提供了SoftReference类来实现软引用。 &lt;strong&gt;c.弱引用&lt;/strong&gt; 弱引用也是用来描述非必需的对象，但是强度比软引用还弱一点。在下一次GC时必定会回收。 &lt;strong&gt;d.虚引用&lt;/strong&gt; 虚引用对对象的生存时间构不成影响，就和没有引用与之关联一样，所以任何时候的GC都会将其回收。对一个对象设置虚引用的唯一目的就是这个对象被回收时会收到一个系统通知。在JDK1.2以后，提供了PhantomReference类来实现虚引用。 用代码来检验一下软引用、弱引用和虚引用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package test;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;

public class TestReference {
    public static boolean run = true;
    public static void main(String[] args){
        WeakReference&amp;lt;String&amp;gt; weakReference = new WeakReference&amp;lt;String&amp;gt;(new String(&quot;weak Reference&quot;));
        SoftReference&amp;lt;String&amp;gt; softReference = new SoftReference&amp;lt;String&amp;gt;(new String(&quot;soft Reference&quot;));
        final ReferenceQueue&amp;lt;String&amp;gt; queue = new ReferenceQueue&amp;lt;String&amp;gt;();
        new Thread(new Runnable() {

            @Override
            public void run() {
                while(run){
                    Object object = queue.poll();
                    if(object != null){
                        try {
                            Field referent = Reference.class.getDeclaredField(&quot;referent&quot;);
                            referent.setAccessible(true);
                            Object result = referent.get(object);
                            System.out.println(&quot;即将回收&quot;+result.getClass()+(String)result);
                        } catch (NoSuchFieldException | SecurityException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        } catch (IllegalArgumentException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }

            }
        }).start();
        PhantomReference&amp;lt;String&amp;gt; phantomReference = new PhantomReference&amp;lt;String&amp;gt;(new String(&quot;phantom Reference&quot;), queue);

        System.out.println(weakReference.get());
        System.out.println(softReference.get());
        System.out.println(phantomReference.get());
        System.out.println(&quot;*****************下面开始GC******************&quot;);
        System.gc();        //System.gc只是建议系统进行垃圾回收，并不是立刻执行
        System.out.println(weakReference.get());
        System.out.println(softReference.get());
        System.out.println(phantomReference.get());
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这段代码的输出为&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;weak Reference soft Reference null *****************下面开始GC****************** null soft Reference null 即将回收class java.lang.Stringphantom Reference&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以看到，软引用在gc之后仍然是存在的，而弱引用gc之后变成null了，虚引用一直为null。并且我们通过新开一个线程来检测虚引用被回收的通知。所以正确的使用soft Reference和weak Reference可以实现缓存和防止内存泄露，而虚引用一般用来实现细粒度的内存控制。比如下面代码实现一个缓存。程序在确认原来的对象要被回收之后，才申请内存创建新的缓存。&lt;a href=&quot;http://blog.csdn.net/imzoer/article/details/8044900&quot; rel=&quot;noopener&quot;&gt;Java幽灵引用的作用&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;八、总结&lt;/h2&gt;
&lt;p&gt;可以看出，不论是对java堆中内存空间进行分代，还是对引用进行四种类型的划分，都是为了解决在java程序运行过程中因为存在各种各样的对象，单一的垃圾回收算法没有办法高效的进行。事实上垃圾回收算法再不断的变化，每一种当前存在的垃圾回收算法都有它适应的运行环境。因此在什么时候使用什么样的垃圾回收算法，对于程序的性能来说也是至关重要的。&lt;/p&gt;
</content:encoded><category>java虚拟机学习</category><author>joyme123</author></item><item><title>Java虚拟机学习记录(四)-对象的内存布局和访问定位</title><link>https://www.myway5.com/blog/jvm-study-lesson-4/</link><guid isPermaLink="true">https://www.myway5.com/blog/jvm-study-lesson-4/</guid><description>jvm创建对象的过程分为类加载检查，分配对象空间，初始化类空间，设置对象信息，对象初始化。那么在分配对象空间时是如何分配的，怎么保证能够定位到对象的内存位置。 二、对象的内存布局</description><pubDate>Wed, 19 Apr 2017 09:32:59 GMT</pubDate><content:encoded>&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;jvm创建对象的过程分为类加载检查，分配对象空间，初始化类空间，设置对象信息，对象初始化。那么在分配对象空间时是如何分配的，怎么保证能够定位到对象的内存位置。&lt;/p&gt;
&lt;h2&gt;二、对象的内存布局&lt;/h2&gt;
&lt;p&gt;对象在内存中的布局可以分为三部分：对象头（Object Header),实例数据（Instance Data)和对齐填充(Padding)。&lt;/p&gt;
&lt;h3&gt;a.对象头&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;对象头有两部分，一部分是用于存储对象自身的运行时数据，如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
对象头的另外一部分是类型指针，即对象指向他的类元数据的指针(表示这个对象是哪个类实例化出来的)。并不是所有的虚拟机实现都必须在对象数据上保留类元数据的指针。因为查找对象的类元数据信息并不一定要经过对象本身(通过句柄)。另外，如果对象是一个Java数组，那在对象头中还必须要有一块用于记录数组长度的数据，因为虚拟机可以通过普通Java对象的类元数据信息确定Java对象大小，但是数据的类元数据中却无法确定数据的大小。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;b.实例数据&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;存储对象中的各种类型的字段内容以及普通对象的指针(oops,Ordinary Object Pointers)。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;c.对象填充&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;不是必然存在，没有特别意义，作为占位符存在。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、对象的访问定位&lt;/h2&gt;
&lt;p&gt;建立对象是为了使用对象，对象的访问是通过栈上的reference数据来操作堆上的具体对象。reference是java虚拟机规范的一个指向对象的引用，但并没有规定如何去具体实现，一般来说，有两种实现方式:句柄和直接指针。&lt;/p&gt;
&lt;h3&gt;a.句柄&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;采用句柄方式会在Java堆中开辟出一块句柄池空间，Java栈中的本地变量表中存放着指向句柄池中某一个句柄的reference，然后句柄保存有指向某个实例的指针和指向方法区的对象类元数据。
reference----&amp;gt;句柄------&amp;gt;对象和类元数据，共三次指针定位
这种方式的优点是GC清理垃圾时会移动对象地址，栈中的reference不需要改变只需要改变句柄中指向对象的指针。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;b.直接指针访问&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;reference----&amp;gt;对象实例数据(对象实例数据的对象头包含类元指针)------&amp;gt;类元数据，共两次指针定位
这种方式的优点是速度更快，节省了一次指针定位的时间。Sun HotSpot采用的就是这种对象的访问定位方式。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;c.注意&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;在JDK1.8中，已经完全移除了方法区，类元数据的存储放在了本地内存中，这样就不会再收到方法区大小的限制。
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>java虚拟机学习</category><author>joyme123</author></item><item><title>Java虚拟机学习记录(三)-对象创建的过程</title><link>https://www.myway5.com/blog/jvm-study-lesson-3/</link><guid isPermaLink="true">https://www.myway5.com/blog/jvm-study-lesson-3/</guid><description>在Java程序运行时几乎每时每刻都有对象在被创建出来，从语言层面上看，只是new了一个对象，但是在虚拟机中这个对象的创建过程时比较复杂的（这里的对象仅适用于普通的Java对象，不包括数据和Class对象）。我把这其中的步骤总结为下面几步 1.</description><pubDate>Wed, 19 Apr 2017 09:32:18 GMT</pubDate><content:encoded>&lt;p&gt;在Java程序运行时几乎每时每刻都有对象在被创建出来，从语言层面上看，只是new了一个对象，但是在虚拟机中这个对象的创建过程时比较复杂的（这里的对象仅适用于普通的Java对象，不包括数据和Class对象）。我把这其中的步骤总结为下面几步 &lt;strong&gt;1.类加载检查&lt;/strong&gt; 当虚拟机接受到new指令时，首先去查常量池中能否定位到这个类的符号引用，并且检查这个符号引用的类是否已经被加载、解析和初始化过。如果没有那就必须执行类的加载过程。简单来说，就是虚拟机中有没有这个类的信息，如果没有就得去加载。 &lt;strong&gt;2.为对象分配内存&lt;/strong&gt; 对象需要的内存在类加载完成之后就已经完全确定了，为对象分配内存的任务等同于在Java堆上划分出一块确定大小的内存。 这个划分内存的动作有两种情况。 &lt;strong&gt;如果Java堆中的内存时规整的&lt;/strong&gt;，使用过的放一边，未使用的放另一边，中间用一个指针作为分界点的指示器。那么分配内存的动作就是将指针向未使用的那一边移动所需要的内存大小。这种分配方式叫做&lt;strong&gt;指针碰撞&lt;/strong&gt;(Bump the Pointer)。 &lt;strong&gt;如果Java堆中的内存时不规整的&lt;/strong&gt;,这种情况很明显不适用于指针碰撞，一般这种情况下Java虚拟机会维持一张表来记录内存的使用情况，哪些内存使用了，哪些内存时空闲的都会记录好，需要分配内存时在表中查找到合适的内存区域分配，然后更新这张表即可。这张表被称为&lt;strong&gt;空闲列表&lt;/strong&gt;(Free List) 使用指针碰撞还是空闲列表由Java堆是否规整决定，而Java堆是否规整则由Java虚拟机的GC算法是否带有压缩整理功能决定。 在并发环境下，简单的修改指针指向的内存位置并不安全，因为A对象分配内存的时候，指针还没有移动的同时，B对象也要开始分配内存，因此使用的还是未发生改变的指针。解决方案有两种，一种是对分配内存空间的动作作同步处理————实际上虚拟机采用CAS配上失败重试的方式来保证操作的原子性；另一种是把内存分配动作按照线程划分在不同的空间之中进行，即每个线程在Java堆中预先分配一小块内存，称为本地线程分配缓冲（Thread Local Allocation Buffer,TLAB)。哪个线程分配内存，就在那个线程的TLAB上分配，只有TLAB分配完并分配新的TLAB时，才需要同步锁定。 &lt;strong&gt;3.内存初始化&lt;/strong&gt; 在分配完内存后，虚拟机需要将分配到的内存空间初始化为0(不包括对象头)，如果使用TLAB，那么在TLAB分配时就可以完成这一步骤。这一步骤保证了对象中的字段不初始化就能直接使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package test;

public class NoInitInt {
    private int noInitInteger;

    public static void main(String[] args){
        int i = 0;
        System.out.println(i);
    }

    public int getNoInitInteger() {
        return noInitInteger;
    }

    public void setNoInitInteger(int noInitInteger) {
        NoInitInt noInitInt = new NoInitInt();
        System.out.println(noInitInt.getNoInitInteger());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上程序所示，输出为0 &lt;strong&gt;4.对象设置&lt;/strong&gt; Java虚拟机需要设置对象是哪个类的实例，如果才能找到类的元数据信息，对象的哈希码，对象的GC分代年龄等信息。这些信息被存放在对象的对象头中(Object Header) &lt;strong&gt;5.初始化&lt;/strong&gt; 经过上面的步骤，从Java虚拟机的角度一个新的对象已经生成了，但是从Java程序员的角度，这个对象还差一步，就是对象的初始化&lt;/p&gt;
</content:encoded><category>java虚拟机学习</category><author>joyme123</author></item><item><title>Java虚拟机学习记录(二)-运行时数据区域</title><link>https://www.myway5.com/blog/jvm-study-lesson-2/</link><guid isPermaLink="true">https://www.myway5.com/blog/jvm-study-lesson-2/</guid><description>Java虚拟机在执行Java程序的过程中，会把他管理的内存划分为多个不同的数据区域，这些区域各自有各自的用途，以及创建和销毁的时间，有的区域随虚拟机进程的启动而存在，有的区域依赖用户线程的启动和结束而建立和销毁。 Java虚拟机所管理的内存将会包括以下几个运行时数据区域。 1.</description><pubDate>Wed, 19 Apr 2017 09:31:25 GMT</pubDate><content:encoded>&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;Java虚拟机在执行Java程序的过程中，会把他管理的内存划分为多个不同的数据区域，这些区域各自有各自的用途，以及创建和销毁的时间，有的区域随虚拟机进程的启动而存在，有的区域依赖用户线程的启动和结束而建立和销毁。 Java虚拟机所管理的内存将会包括以下几个运行时数据区域。 1.方法区（线程共享）,JDK1.7中已经开始改变，JDK1.8中被&lt;strong&gt;元空间&lt;/strong&gt;替代 2.堆（线程共享） 3.虚拟机栈（线程隔离） 4.本地方法栈（线程隔离） 5.程序计数器（线程隔离） Java虚拟机运行时涉及到的另外的内存区域 1.直接内存&lt;/p&gt;
&lt;h2&gt;二、方法区（Method Area）&lt;/h2&gt;
&lt;p&gt;这是一个线程共享的数据区域，用来保存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机将方法区描述为堆的一个逻辑部分（在HotSpot中具体表现为在方法区的内存管理和堆中的内存管理是一套方案，当然这里的一套方案并不是说他们是一模一样的），但是方法区有一个别名叫做Non-Heap(非堆),应该是为了和堆作区别(Heap)。 在HotSpot虚拟机中，方法区又被很多人叫做“永久代”（Permanent Generation),本质上两者并不等价，仅仅是HotSpot团队将GC分代收集扩展至方法区，也就是使用永久代来实现方法区。这样就和Java堆使用了同样的GC分代收集策略，不必重新实现一套管理策略。对于其他虚拟机（如BEA JRockit、IBM J9等）来说是不存在永久代的。 然而在实际应用中，使用永久代来实现方法区并不是一个好的选择，更容易遇到内存溢出的问题。在JDK1.7中，已经将字符串常量池从永久代移出了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;字符串常量池的移出&lt;/strong&gt;:JDK1.7 被转移到Java堆中(Java Heap)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Java虚拟机规范堆方法区的限制宽松，方法区不需要连续的内存，也可以不实现垃圾收集。事实上，垃圾收集的频率在这个区域是很小的，但是并不是所有在此的数据真的是“永久”的，这个区域的垃圾回收目标主要是针对常量池的回收和对类型的卸载。这个区域的回收成绩难以令人满意。当方法区无法满足内存分配的需求时，会抛出OutOfMemoryError的错误。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于&lt;strong&gt;类型卸载&lt;/strong&gt;，我理解为在Java虚拟机运行时，会将类信息加载到方法区，当某些类不会用到的时候(unreachable)，就会从方法区中卸载这个类以节省内存),具体可以看下面的文章 1.&lt;a href=&quot;http://www.blogjava.net/zhuxing/archive/2016/06/14/220841.html#430896&quot; rel=&quot;noopener&quot;&gt;Java类加载原理解析&lt;/a&gt; 2.&lt;a href=&quot;http://www.blogjava.net/zhuxing/archive/2008/07/24/217285.html&quot; rel=&quot;noopener&quot;&gt;Java虚拟机类型卸载和类型更新解析&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在方法区中有一块叫做运行时常量池(Runtime Constant Pool)的区域，Class文件中除了有类的版本、字段、方法、接口等描述信息外，还有一项信息常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用，这部分在类加载到方法区后进入运行时常量池。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;字面量(literal):&lt;/strong&gt; 我的理解为字面量指的几种基本类型。int,boolean,char,float,double,string,long,byte,null 其中float和double统称为floating-point literal,当你创建一个浮点数时，默认的是double类型。这也是为什么float a = 0.1;是错误的，你得float a = (float)0.1; 具体字面量的内容可以参考这里:&lt;a href=&quot;http://www.javatutorialprograms.com/2013/01/java-literals.html&quot; rel=&quot;noopener&quot;&gt;Java Literals&lt;/a&gt; &lt;strong&gt;符号引用:&lt;/strong&gt; 符号引用是一个字符串，它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法。这样，对于其他类的符号引用必须给出类的全名。对于其他类的字段，必须给出类名、字段名以及字段描述符。对于其他类的方法的引用必须给出类名、方法名以及方法的描述符。 &lt;a href=&quot;http://blog.csdn.net/imzoer/article/details/8086255&quot; rel=&quot;noopener&quot;&gt;JVM中的直接引用和符号引用&lt;/a&gt; 关于常量池中的一些细节可以看这里的对比 &lt;a href=&quot;http://myway5.com/?post=43&quot; rel=&quot;noopener&quot;&gt;Java常量池&lt;/a&gt; &lt;strong&gt;方法区的变迁&lt;/strong&gt;: 1、JDK1.2 ~ JDK6 在 JDK1.2 ~ JDK6 的实现中，HotSpot 使用永久代实现方法区；HotSpot 使用 GC 分代实现方法区带来了很大便利； 2、JDK7 由于 GC 分代技术的影响，使之许多优秀的内存调试工具无法在 Oracle HotSpot之上运行，必须单独处理；并且 Oracle 同时收购了 BEA 和 Sun 公司，同时拥有 JRockit 和 HotSpot，在将 JRockit 许多优秀特性移植到 HotSpot 时由于 GC 分代技术遇到了种种困难，所以从 JDK7 开始 Oracle HotSpot 开始移除永久代。 JDK7中符号表被移动到 Native Heap中，字符串常量和类引用被移动到 Java Heap中。 3、JDK8 在 JDK8 中，永久代已完全被&lt;strong&gt;元空间&lt;/strong&gt;(Meatspace)所取代。 引用:&lt;a href=&quot;https://mritd.me/2016/03/22/Java-%E5%86%85%E5%AD%98%E4%B9%8B%E6%96%B9%E6%B3%95%E5%8C%BA%E5%92%8C%E8%BF%90%E8%A1%8C%E6%97%B6%E5%B8%B8%E9%87%8F%E6%B1%A0/#jdk7&quot; rel=&quot;noopener&quot;&gt;Java 内存之方法区和运行时常量池&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;##三、堆（Java Heap) 和方法区一样，这也是一个线程共享的数据区域。在java虚拟机执行Java程序时，它占据了大多数的内存，几乎所有的对象的存储都是在这个区域。为什么说几乎呢？因为随着JIT编译器的发展和逃逸分析技术的逐渐成熟，栈上分配，标量替换优化技术将会导致一系列微妙的变化发生。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;**JIT编译器:**及时编译器（Just-In-Time compiler) **逃逸分析技术:**全部变量赋值，方法返回值，实例引用传递三种情况会发生指针逃逸，如果在方法内新建对象，并且这个对象没有离开过这个方法，则这个对象没有必要在堆中分配内存，直接在Java虚拟机栈中分配内存，省去了在堆中分配内存堆GC造成的压力。 &lt;a href=&quot;http://www.iteye.com/topic/473355&quot; rel=&quot;noopener&quot;&gt;什么是逃逸分析(Escape Analysis)?&lt;/a&gt; **栈上分配:**将对象在栈上分配内存 **标量替换优化技术:**Java中的原始类型无法再分解，可以看作标量（scalar）；指向对象的引用也是标量；而对象本身则是聚合量（aggregate），可以包含任意个数的标量。如果把一个Java对象拆散，将其成员变量恢复为分散的变量，这就叫做标量替换。拆散后的变量便可以被单独分析与优化，可以各自分别在活动记录（栈帧或寄存器）上分配空间；原本的对象就无需整体分配空间了。 &lt;a href=&quot;http://rednaxelafx.iteye.com/blog/659108&quot; rel=&quot;noopener&quot;&gt;HotSpot 17.0-b12的逃逸分析/标量替换的一个演示&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Java堆是垃圾收集器管理的主要区域，又成&quot;GC堆&quot;(Garbage Collected Heap)。 从内存回收的角度来看，现在收集器基本都采用分代收集算法。所以Java堆可以细分为：新生代和老年代，再细分一点，可以分为Eden空间J，From Survivor空间，to Survivor空间 从内存分配的角度来看，Java堆可能划分出多个线程私有的分配缓冲区（Thread Local Allocation Buffer,TLAB) Java堆的内存不要求在空间上是连续的，只要在逻辑上连续即可。&lt;/p&gt;
&lt;h2&gt;四、虚拟机栈（Java Virtual Machine Stack）&lt;/h2&gt;
&lt;p&gt;线程私有，和线程的生命周期相同。虚拟机栈描述的是Java方法执行的内存模型：每个方法在执行的同时，都会创建一个栈帧（Stack Frame），用于存储局部变量表，操作数栈，动态链接，方法出口等信息。每一个方法从调用到执行完成的过程，就对应着一个栈帧从虚拟机栈入栈到出栈的过程。 局部变量表中存放了编译区可知的各种基本数据类型（boolean,byte,char,int,float,double,long,short)、对象引用(reference，它不等同于对象本身，可能是一个指向对象起始地址的引用指针，也可能是指向一个代表对象的句柄和其他与此对象相关的位置) 64位的double和long会占据两个局部变量空间(slot),其余数据类型只占一个。局部变量表所需的空间在编译期间分配完成，当进入一个方法时，这个方法需要在帧中分配多少局部变量空间完全时确定的。 在Java虚拟机中，对这个区域规定了两中异常情况。1.如果线程请求的栈深度大于虚拟机所允许的深度，将抛出StackOverflowError;如果虚拟机栈可以动态扩展，在扩展时无法申请到足够的内存，则会抛出OutOfMemoryError异常。&lt;/p&gt;
&lt;h2&gt;五、本地方法栈（Native Method Stack）&lt;/h2&gt;
&lt;p&gt;跟虚拟机栈类似,不过是用来存储本地方法的。&lt;/p&gt;
&lt;h2&gt;六、程序计数器（Program Counter Register）&lt;/h2&gt;
&lt;p&gt;线程私有，这是一块较小的内存空间，可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令，分支，循环，跳转，异常处理，线程恢复等基础功能都依赖这个。 每个线程都需要一个独立的程序计数器，保证各条线程执行中不会混乱。 如果当前执行的时Java方法，这个计数器记录的正是当前正在执行的虚拟机字节码指令的位置，如果执行的时Native方法，这个计数器为空。此区域时唯一一个在Java虚拟机规范中没规定任何OutOfMemoryError的区域。&lt;/p&gt;
&lt;h2&gt;七、直接内存（Direct Memory）&lt;/h2&gt;
&lt;p&gt;在JDK1.4中加入了NIO(New Input/Output)类，引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式，它可以使用Native函数库直接分配堆外内存，然后通过一个存储在Java堆中的DirectByteBuffer对象作为这个内存的引用进行操作。这样能在一些场景中显著提高性能，因为避免了频繁的在Java堆中和Native堆中复制数据。&lt;/p&gt;
</content:encoded><category>java虚拟机学习</category><author>joyme123</author></item><item><title>Java虚拟机学习记录（一）-Java技术体系</title><link>https://www.myway5.com/blog/jvm-study-lesson-1/</link><guid isPermaLink="true">https://www.myway5.com/blog/jvm-study-lesson-1/</guid><description>##一、java技术体系概览 JDK(Java Development Kit) Java程序设计语言 Java虚拟机 Java API类库</description><pubDate>Wed, 19 Apr 2017 09:30:04 GMT</pubDate><content:encoded>&lt;p&gt;##一、java技术体系概览 JDK(Java Development Kit)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java程序设计语言&lt;/li&gt;
&lt;li&gt;Java虚拟机&lt;/li&gt;
&lt;li&gt;Java API类库&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JDK是用于支持Java程序开发的最小环境。 JRE(Java Runtime Environment)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java SE API子集&lt;/li&gt;
&lt;li&gt;Java虚拟机&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JRE是支持Java程序运行的标准环境&lt;/p&gt;
&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Java语言&lt;/td&gt;&lt;th&gt;Java Language&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;工具及工具API&lt;/td&gt;&lt;td&gt;Java&lt;/td&gt;&lt;td&gt;Javac&lt;/td&gt;&lt;td&gt;JavaDoc&lt;/td&gt;&lt;td&gt;Jar&lt;/td&gt;&lt;td&gt;Javap&lt;/td&gt;&lt;td&gt;Monitor&lt;/td&gt;&lt;td&gt;JPDA&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;JConsole&lt;/td&gt;&lt;td&gt;Java VisualVM&lt;/td&gt;&lt;td&gt;JavaDB&lt;/td&gt;&lt;td&gt;security&lt;/td&gt;&lt;td&gt;Internationalization&lt;/td&gt;&lt;td&gt;JMC&lt;/td&gt;&lt;td&gt;RMI&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;IDL&lt;/td&gt;&lt;td&gt;Deploy&lt;/td&gt;&lt;td&gt;JFR&lt;/td&gt;&lt;td&gt;Troubleshoot&lt;/td&gt;&lt;td&gt;Scripting&lt;/td&gt;&lt;td&gt;JVMTI&lt;/td&gt;&lt;td&gt;Web Services&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;程序部署和发布&lt;/td&gt;&lt;td&gt;Java Web Start&lt;/td&gt;&lt;td&gt;Applet/Java Plug-in&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;用户界面工具集&lt;/td&gt;&lt;td&gt;JavaFX&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Swing&lt;/td&gt;&lt;td&gt;Java 2D&lt;/td&gt;&lt;td&gt;AWT&lt;/td&gt;&lt;td&gt;Accessbility&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Drag and Drop&lt;/td&gt;&lt;td&gt;Input Methods&lt;/td&gt;&lt;td&gt;Image I/O&lt;/td&gt;&lt;td&gt;Print Service&lt;/td&gt;&lt;td&gt;Sound&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;集成库&lt;/td&gt;&lt;td&gt;IDL&lt;/td&gt;&lt;td&gt;JDBC&lt;/td&gt;&lt;td&gt;JNDI&lt;/td&gt;&lt;td&gt;RMI&lt;/td&gt;&lt;td&gt;RMI-IIOP&lt;/td&gt;&lt;td&gt;Scripting&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;其他基础库&lt;/td&gt;&lt;td&gt;Beans&lt;/td&gt;&lt;td&gt;Int&apos;l Support&lt;/td&gt;&lt;td&gt;Input/Output&lt;/td&gt;&lt;td&gt;JMX&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;JNI&lt;/td&gt;&lt;td&gt;NetWorking&lt;/td&gt;&lt;td&gt;Override Mechanism&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Security&lt;/td&gt;&lt;td&gt;Serialization&lt;/td&gt;&lt;td&gt;Extension Mechanism&lt;/td&gt;&lt;td&gt;XML JAXP Mechanism&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;语言和工具基础库&lt;/td&gt;&lt;td&gt;Math&lt;/td&gt;&lt;td&gt;Collections&lt;/td&gt;&lt;td&gt;Concurrency Utilities&lt;/td&gt;&lt;td&gt;JAR&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Logging&lt;/td&gt;&lt;td&gt;Management&lt;/td&gt;&lt;td&gt;Preferences API&lt;/td&gt;&lt;td&gt;Ref Objects&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Reflection&lt;/td&gt;&lt;td&gt;Regular Expressions&lt;/td&gt;&lt;td&gt;Versioning&lt;/td&gt;&lt;td&gt;Zip&lt;/td&gt;&lt;td&gt;Instrumentation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Java虚拟机&lt;/td&gt;&lt;td&gt;Java HotSpot Client and Server VM&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;这张表网上有很多版本，我以《深入理解java虚拟机》为参考，添加了JMC和JFR。可能在这划分中会有重叠部分，仅做参考。 其中&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;JDK: Java语言，工具及工具API，程序部署发布，用户界面工具集，集成库，其他基础库，语言和工具基础库，Java虚拟机&lt;/li&gt;
&lt;li&gt;JRE: 程序部署发布，用户界面工具集，集成库，其他基础库，语言和工具基础库，Java虚拟机&lt;/li&gt;
&lt;li&gt;Java SE API: 用户界面工具集，集成库，其他基础库，语言和工具基础库，Java虚拟机&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;##二. Java工具和工具API（对应了jdk中bin目录下的工具） ###自己目前使用过的有： - java : 用来运行jar。对应bin目录下的java。 - javac: java的编译工具。对应bin目录下的javac。 - JConsole : 配置好环境的情况下，可以在控制台输入jconsole打开,jconsole可以配合JMX(其他基础库)，达到对运行中的java程序进行监控甚至动态修改变量值的作用。具体的使用可以参考这一篇&lt;a href=&quot;https://my.oschina.net/xpbug/blog/221547&quot; rel=&quot;noopener&quot;&gt;JMX整理&lt;/a&gt;,对应bin目录下的jconsole。 - security : - keytool： 这个工具可以生成key和certificate，具体使用可以参考这一篇&lt;a href=&quot;http://www.bkjia.com/Javabc/951431.html&quot; rel=&quot;noopener&quot;&gt;Java Security：keytool工具使用说明，securitykeytool&lt;/a&gt; 。对应bin目录下的keytool工具。 - jarsigner: jar密匙签名工具 - kinit: 主要用于获取或缓存Kerberos协议的票据授权票据。 - klist: 允许用户查看本地凭据缓存和密钥表中的条目(用于Kerberos协议)。 - ktab: Kerberos密钥表管理工具，允许用户管理存储于本地密钥表中的主要名称和服务密钥。 - policytool: 策略工具，用于管理用户策略文件(.java.policy)。 ###尚未使用过的有 - javaDoc : 使用 javdoc 编译 .java 源文件时，它会读出 .java 源文件中的文档注释，并按照一定的规则与 Java 源程序一起进行编译，生成文档。对应bin目录下的javadoc。 - jar : JAR（Java Archive，Java 归档文件），是java 开发工具中的一个工具，位于JDK的安装目录的bin目录下。它是一个打包工具，有点类似winrar压缩工具，虽然一般是用来打包.class文件，但是实际上其它文件也是可以打包的。对应bin目录下的jar。 - Javap : java反编译工具。对应bin目录下的javap。 - JPDA : Java Platform Debugger Architecture(JPDA:Java平台调试架构),Java虚拟机后端和调试平台前端组成 - 1.Java虚拟机提供了Java调试的功能 - 2.调试平台通过调试交互协议向Java虚拟机请求服务以对在虚拟机中运行的程序进行调试 - jdb: Java调试工具(Java Debugger)，主要用于对Java应用进行断点调试。 - Java VisualVM : java性能分析工具，对应bin目录下的jvisualvm。 - JavaDB : java的数据库。 - Int&apos;l:可能指的是internationalization,即国际化。这里很不确定，在bin目录下有一个native2ascii，本地编码到ASCII编码的转换器(Native-to-ASCII Converter)，用于&quot;任意受支持的字符编码&quot;和与之对应的&quot;ASCII编码和(或)Unicode转义&quot;之间的相互转换。 - JFR: Java飞行记录(好奇怪的翻译，Java Flight Recordings)。&lt;a href=&quot;http://coderbee.net/index.php/jvm/20150406/1188&quot; rel=&quot;noopener&quot;&gt;Java Flight Recordings (JFR) — Java 飞行记录器 – part 1&lt;/a&gt; - JMC: Java任务控制工具(java Mission Control)。对应bin目录下的jmc - RMI: Java远程方法调用(Remote Method Invocation,之前用过类似与thrift这种跨语言的远程调用框架，原理是socket通信)。对应bin目录下的 - java-rmi: Java远程方法调用(Java Remote Method Invocation)工具，主要用于在客户机上调用远程服务器上的对象。 - rmic: Java RMI 编译器，为使用JRMP或IIOP协议的远程对象生成stub、skeleton、和tie类，也用于生成OMG IDL。 - rmid: Java RMI 激活系统守护进程，rmid启动激活系统守护进程，允许在虚拟机中注册或激活对象。 - rmiregistry: Java 远程对象注册表，用于在当前主机的指定端口上创建并启动一个远程对象注册表。 - jstatd: jstatd(VM jstatd Daemon)工具是一个RMI服务器应用，用于监测HotSpot JVM的创建和终止，并提供一个接口，允许远程监测工具附加到运行于本地主机的JVM上。 - serialver: 序列版本命令，用于生成并返回serialVersionUID。 - IDL: 与RMI类似，是面向对象的远程调用，但不同的是他是跨语言的。对应bin目录下的 - idlj: IDL转Java编译器(IDL-to-Java Compiler)，用于为指定的IDL文件生成Java绑定。IDL意即接口定义语言(Interface Definition Language)。 - servertool: Java IDL 服务器工具，用于注册、取消注册、启动和终止持久化的服务器。 - tnameserv: Java IDL瞬时命名服务。 - orbd: 对象请求代理守护进程(Object Request Broker Daemon)，它使客户端能够透明地定位和调用位于CORBA环境的服务器上的持久对象。 - deploy: - javafxpackager: JavaFX包装器，用于执行与封装或签名JavaFX应用有关的任务。 - pack200: JAR文件打包压缩工具，它可以利用Java类特有的结构，对普通JAR文件进行高效压缩，以便于能够更快地进行网络传输。 - unpack200: JAR文件解压工具，将一个由pack200打包的文件解压提取为JAR文件。 - Monitor: 我这里理解为一系列的java运行监视工具 - JPS：JVM进程状态工具(JVM Process Status Tool)，用于显示目标系统上的HotSpot JVM的Java进程信息。对应bin目录下的jps。 - JSTAT: JVM统计监测工具(JVM Statistics Monitoring Tool)，主要用于监测并显示JVM的性能统计信息。对应bin目录下的jstat。 - Troubleshoot: java的一系列错误定位工具 - JINFO: Java配置信息工具(Java Configuration Information)，用于打印指定Java进程、核心文件或远程调试服务器的配置信息。对应bin目录下的jinfo。 - JStack: Java堆栈跟踪工具，主要用于打印指定Java进程、核心文件或远程调试服务器的Java线程的堆栈跟踪信息。 - JMAP: ava内存映射工具(Java Memory Map)，主要用于打印指定Java进程、核心文件或远程调试服务器的共享对象内存映射或堆内存细节。。对应bin目录下的jmap。 - jhat: Heap Dump Browser, 根据dump文件进行分析，可以在浏览器中查看。 - jsadebugd: Java可用性代理调试守护进程(Java Serviceability Agent Debug Daemon)，主要用于附加到指定的Java进程、核心文件，或充当一个调试服务器。 - Scripting: - jrunscript: Java命令行脚本外壳工具(command line script shell)，主要用于解释执行javascript、groovy、ruby等脚本语言。 - JVMTI: Java 虚拟机工具接口(Java Virtual Machine Toolkit Interface),用于替代在先前的 JDK 版本中作为试验功能存在的 Java 虚拟机剖析接口（Java Virtual Machine Profiling Interface，JVMPI）和 Java 虚拟机调试接口（Java Virtual Machine Debugging Interface，JVMDI）。通过 JVMTI 接口可以创建代理程序（Agent）以监视和控制 Java 应用程序，包括剖析、调试、监控、分析线程等等。 - Web Services: - wsgen: 当从 Java 代码启动时，wsgen 命令行工具将生成 Java API for XML Web Services (JAX-WS) 应用程序所必需的工件。 - wsimport: wsimport 命令行工具用于处理现有 Web Service 描述语言 (WSDL) 文件，并生成开发 Java API for XML-Based Web Services (JAX-WS) Web Service 应用程序所必需的工件。 - schemagen: XML schema生成器，用于生成XML schema文件。 - xjc: 主要用于根据XML schema文件生成对应的Java类。 ##程序部署和发布 - Java Web Start:允许用户直接从网络上运行基于java技术的应用。共有三种运行方式，无论哪一种，都会连上网络运行 - 1.从网页上点击链接。 - 2.从桌面图标或者开始菜单。 - 3.从java缓存视图中。 - Applet/Java Plug-in) applet小程序 ##用户界面工具集 - JavaFx: 2007年首次推出,具体资料可以看这些。&lt;a href=&quot;http://developer.51cto.com/art/200904/117824.htm&quot; rel=&quot;noopener&quot;&gt;JavaFX对Java开发者到底意味着什么&lt;/a&gt;&lt;a href=&quot;http://docs.oracle.com/javase/8/javafx/get-started-tutorial/jfx-overview.htm#JFXST784&quot; rel=&quot;noopener&quot;&gt;JavaFX: Getting Started with JavaFX&lt;/a&gt; - Swing: 是所谓的Lightweight组件，不是通过native方法来实现的，所以Swing的窗口风格更多样化。但是,Swing里面也有heaveyweight组件。比如JWindow，Dialog,JFrame。Swing由纯Java写成，可移植性好，外观在不同平台上相同。所以Swing部件称为轻量级组件， Swing是由纯JAVA CODE所写的，因此SWING解决了JAVA因窗口类而无法跨平台的问题，使窗口功能也具有跨平台与延展性的特性，而且SWING不需占有太多系统资源，因此称为轻量级组件 - Java 2D:包括Graphics，Graphics2D接口。&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/2d/&quot; rel=&quot;noopener&quot;&gt;Trail: 2D Graphics&lt;/a&gt; - AWT:调用系统的native接口绘制，所以风格和系统相关。由于不同 操作系统 的图形库所提供的功能是不一样的，在一个平台上存在的功能在另外一个平台上则可能不存在。为了实现Java语言所宣称的&quot;一次编译，到处运行&quot;的概念，AWT 不得不通过牺牲功能来实现其平台无关性，也就是说，AWT 所提供的图形功能是各种通用型操作系统所提供的图形功能的交集。由于AWT 是依靠本地方法来实现其功能的，我们通常把AWT控件称为重量级控件。 - Accessbility: 无障碍使用，针对残疾人士。&lt;a href=&quot;http://docs.oracle.com/javase/7/docs/technotes/guides/access/#programmers_guides&quot; rel=&quot;noopener&quot;&gt;Java Accessibility Guide&lt;/a&gt; - Drag and Drop: 实现拖放功能。&lt;a href=&quot;http://docs.oracle.com/javase/tutorial/uiswing/dnd/index.html&quot; rel=&quot;noopener&quot;&gt;Lesson: Drag and Drop and Data Transfer&lt;/a&gt; - Input Methods:用户输入 - Image I/O:图片的输入输出 - Print Service:打印服务 - Sound:声音 ##三. 集成库 - IDL: Java IDL技术添加CORBA(Common Object Request Broker Architecture)到Java平台，提供了标准的互操性和连通性。Java IDL使得分布式，web的java应用能够使用OMG(Object Management Group)定义的行业标准的IDL(Interface Definition Language)语言和IIOP(Internet Inter-ORB Protocol) 协议透明的调用远程服务。 - JDBC: Java数据库连接API(Java Database Connectivity API)。 - JNDI: Java 命名与目录接口(Java Naming and Directory Interface)，所有与系统外部的资源的引用，都可以通过JNDI定义和引用。通俗点理解我觉得应该就是将配置用xml保存。&lt;a href=&quot;http://blog.csdn.net/zhaosg198312/article/details/3979435&quot; rel=&quot;noopener&quot;&gt;JNDI 是什么&lt;/a&gt; - RMI: 远程方法调用API(Remote Method Invocation)。 - RMI-IIOP:通过IIOP(Internet Inter-ORB Protocol)协议的远程方法调用（RMI)。 - Scripting:能让java应用运行js引擎。 ##四. 其他基础库 - Beans: Java Beans是Java中一种特殊的类，可以将多个对象封装到一个对象（bean）中。特点是 - 可序列化 - 提供无参构造器 - 提供getter方法和setter方法访问对象的属性。 名称中的“Bean”是用于Java的可重用软件组件的惯用叫法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    public class PersonBean implements java.io.Serializable {

    /**
     * name 属性(注意大小写)
     */
    private String name = null;

    private boolean deceased = false;

    /** 无参构造器(没有参数) */
    public PersonBean() {
    }

    /**
     * name 属性的Getter方法
     */
    public String getName() {
        return name;
    }

    /**
     * name 属性的Setter方法
     * @param value
     */
    public void setName(final String value) {
        name = value;
    }

    /**
     * deceased 属性的Getter方法
     * 布尔型属性的Getter方法的不同形式(这里使用了is而非get)
     */
    public boolean isDeceased() {
        return deceased;
    }

    /**
     * deceased 属性的Setter方法
     * @param value
     */
    public void setDeceased(final boolean value) {
        deceased = value;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Internationalization Support: 国际化支持。特点如下：
&lt;ul&gt;
&lt;li&gt;额外的本土化数据和任何地方运行结果相同&lt;/li&gt;
&lt;li&gt;文字信息存储在代码之外，动态加载(方便程序的翻译)&lt;/li&gt;
&lt;li&gt;不需要重新编译就能支持新的语言&lt;/li&gt;
&lt;li&gt;基于文化的数据，比如日期，货币，呈现方式&lt;/li&gt;
&lt;li&gt;可以快速本土化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Input/Output: 输入/输出
&lt;ul&gt;
&lt;li&gt;通过数据流、序列化、文件系统的输入输出&lt;/li&gt;
&lt;li&gt;字符集、解码、编码、byte到unicode直接的转换&lt;/li&gt;
&lt;li&gt;文件、文件属性、文件系统&lt;/li&gt;
&lt;li&gt;提供建立使用异步的或可复用的、无阻塞的输入输出的服务的API&lt;/li&gt;
&lt;li&gt;java.io (description) - Supports system input and output, and object serialization. to the file system&lt;/li&gt;
&lt;li&gt;java.nio (description) - Defines buffers for bulk memory operations. Buffers may be allocated in direct memory for high performance.&lt;/li&gt;
&lt;li&gt;java.nio.channels (description) - Defines channels, an abstraction for devices capable of performing I/O operations; defines selectors for multiplexed, non-blocking I/O&lt;/li&gt;
&lt;li&gt;java.nio.channels.spi (description) - Provides implementations for channels&lt;/li&gt;
&lt;li&gt;java.nio.file - Defines interfaces and classes to access files and file systems.&lt;/li&gt;
&lt;li&gt;java.nio.file.attribute - Defines interfaces and classes for accessing file system attributes.&lt;/li&gt;
&lt;li&gt;java.nio.file.spi - Defines classes for creating a file system implementation.&lt;/li&gt;
&lt;li&gt;java.nio.charset (description) - Defines charsets, decoders, and encoders, for translating between bytes and Unicode characters&lt;/li&gt;
&lt;li&gt;java.nio.charset.spi (description) - Provides implementations for charsets&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JMX: Java管理扩展(Java Management Extensions)。提供接口结合jconsole工具使用，可以实时查看java程序运行时的变量值，以及修改他们。&lt;/li&gt;
&lt;li&gt;JNI: 提供java对c的支持，可以调用c的方法。&lt;/li&gt;
&lt;li&gt;NetWorking: 提供使用URLS,URIS的类，socket类提供连接服务，安全功能。&lt;/li&gt;
&lt;li&gt;Override Mechanism: 重载机制。&lt;/li&gt;
&lt;li&gt;Security: 提供安全相关功能的API，例如可配置的访问控制，电子签名，认证和签名，加密，安全网络交互。&lt;/li&gt;
&lt;li&gt;Serialization: 序列化，可以将对象(Object)序列化变成二进制流，然后从二进制流中读取出对象。&lt;/li&gt;
&lt;li&gt;Extension Mechanism: 扩展机制。在我的理解中，是指java的包机制，类加载机制，包括从URL中获取。
&lt;ul&gt;
&lt;li&gt;java.lang.ClassLoader&lt;/li&gt;
&lt;li&gt;java.lang.Package&lt;/li&gt;
&lt;li&gt;java.lang.Thread&lt;/li&gt;
&lt;li&gt;java.net.JarURLConnection&lt;/li&gt;
&lt;li&gt;java.net.URLClassLoader&lt;/li&gt;
&lt;li&gt;java.security.SecureClassLoader&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;XML JAXP Mechanism:提供丰富的API集用来处理XML文档和数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;##五. 语言和工具基础库 - Math: 提供和数学计算相关的方法 - Collections: - 1.分为两类 - 最基本的一类是 java.util.Collection，有以下的后代 - java.util.Set - java.util.SortedSet - java.util.NavigableSet - java.util.Queue - java.util.concurrent.BlockingQueue - java.util.concurrent.TransferQueue - java.util.Deque - java.util.concurrent.BlockingDeque - 另外的一类是 java.util.Map，Map并不是真正的Collections，但是他们有着和Collections类似的接口，所以可以像Collections一样操作。 - java.util.SortedMap - java.util.NavigableMap - java.util.concurrent.ConcurrentMap - java.util.concurrent.ConcurrentNavigableMap&lt;br /&gt;
- 2.许多Collections的修改操作都是可选的，当尝试使用没有实现的修改操作时会抛出UnsupportedOperationException，接口的实现必须明确哪些方法是支持的。 - 如果Collections不可修改,则是unmodifiable，如果可以修改，则是modifiable - 如果Collections一成不变，则是immutable，如果是变化的，则是mutable - 如果Lists的大小(Size)是不变的，则是fixed-size，否则则是variable-size. - 如果Lists支持快速的索引访问(时间为常量)，则是Random Access,不支持则是sequential access。RandomAccess标记接口意味着Lists支持Random Access。 - 3.有一些元素的存储是有限制的（比如Map的key--value) - 是一个特定的类型 - 不能为null - obey some arbitrary predicate - 4.接口表格&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    |Interface|HashTable|Resizable Array|Balanced Tree|Linked List|Hash Table + Linked List|
    |:----:|
    |Set      |HashSet  |               |TreeSet      |           |LinkedHashSet           |
    |List     |         | ArrayList     |             |LinkedList |                        |
    |Deque    |         | ArrayDeque    |             |LinkedList |                        |
    |Map      |HashMap  |               |TreeMap      |           |LinkedHashMap           |

- 5.AbstractCollection, AbstractSet, AbstractList, AbstractSequentialList and AbstractMap类已经提供了基本功能的实现
- 6.Concurrent Collection(提供多线程下的Collections)
    - 接口
        - BlockingQueue
        - TransferQueue
        - BlockingDeque
        - ConcurrentMap
        - ConcurrentNavigableMap
    类
        - LinkedBlockingQueue
        - ArrayBlockingQueue
        - PriorityBlockingQueue
        - DelayQueue
        - SynchronousQueue
        - LinkedBlockingDeque
        - LinkedTransferQueue
        - CopyOnWriteArrayList
        - CopyOnWriteArraySet
        - ConcurrentSkipListSet
        - ConcurrentHashMap
        - ConcurrentSkipListMap
- 7.额外注意事项
   -  HashTable在官方的Collections中并没有任何提及，通过查看HashTable的源代码，发现它是继承自Dictionary，实现了Map的接口。而HashMap则是Map的实现，继承的AbstractMap也是Map的实现
    - HashTable和HashMap有以下的区别:
        - HashMap =&amp;gt; 不同步、可空键值、效率高、containsKey/containsValue
        - Hashtable =&amp;gt; 同步、非空键值、效率略低、contains/containsKey/containsValue

8.这一部分的内容越整理越多，还是另开一篇去记录。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;- Concurrency Utilities: 提供了一系列支持并发的接口和类 - java.util.concurrent - java.util.concurrent.atomic:一系列的原子性类，比如AtomicInteger这样的，不需要额外的同步操作就可以支持并线程并发 - java.util.concurrent.locks:提供了多种锁机制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JAR: 提供格式化读写JAR文件的类&lt;/li&gt;
&lt;li&gt;Logging: 提供日志功能&lt;/li&gt;
&lt;li&gt;Management: 提供一系列标准的接口(JMX)用来管理资源，例如应用、设备、服务、Java虚拟机。关于JMX的内容上面以及整理过了。&lt;/li&gt;
&lt;li&gt;Preferences API: 提供存储，读取用户和系统配置的方法。&lt;/li&gt;
&lt;li&gt;Ref(Reference Objects): 提供了和垃圾收集器相关的接口，程序可以使用引用对象来获取一个对象的引用，这样做在之后的垃圾回收中仍然可能会将这个引用对象回收。程序也可以被设计成当垃圾收集器认为一个给定的对象的可达性（gc（垃圾回收）的可达性算法，这个在后续的垃圾回收算法中会整理出来）改变时被通知。因此，引用对象在设计简单的缓存时是很有用的，因为在低内存的情况下它会被自动回收，内存充裕时，因为对象保持了引用，所以不会被垃圾回收器回收。&lt;/li&gt;
&lt;li&gt;Objects: 万物皆对象，你有吗。&lt;/li&gt;
&lt;li&gt;Reflection: Java反射，也是很有用的一个库。spring的IOC就是Java反射的使用 。&lt;/li&gt;
&lt;li&gt;Regular Expressions:正则表达式，也是很常用的库。&lt;/li&gt;
&lt;li&gt;Versioning:可以让java程序在运行时被识别它的需求的运行环境的版本号。&lt;/li&gt;
&lt;li&gt;Zip: 指的应该是Java Archive (JAR) Files，可以将多个文件压缩成一个的技术。&lt;/li&gt;
&lt;li&gt;Instrumentation: 和Sound相关&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;##六. Java虚拟机 - HotSpot Client and Server VM: 现在我们最多接触到的应该就是HotSpot虚拟机了，但是在java最初发布到现在，出现过许多或经典或优秀或有特色的虚拟机实现。 - 1.Sun Classic/Exact VM - 2.Sun HotSpot VM - 3.Sun Mobile-Embedded VM/Meta-Circular VM - 4.BEA JRockit/IBM J9 VM - 5.Azul VM/BEA Liquid VM - 6.Apache Harmony/Google Android Dalvik VM - 7.Microsoft JVM及其他 ##七.参考文章 1. &lt;a href=&quot;http://www.softown.cn/post/168.html&quot; rel=&quot;noopener&quot;&gt;JDK自带工具一览表&lt;/a&gt; 2. &lt;a href=&quot;http://wzktravel.github.io/2015/08/06/java-monitor/&quot; rel=&quot;noopener&quot;&gt;java监控工具(jps,jstat,jstack,jmap,jvisualvm等)&lt;/a&gt; 3. &lt;a href=&quot;http://docs.oracle.com/javase/7/docs/technotes/tools/&quot; rel=&quot;noopener&quot;&gt;JDK Tools and Utilities&lt;/a&gt; 4. &lt;a href=&quot;http://docs.oracle.com/javase/7/docs/technotes/guides/javaws/developersguide/overview.html#jws&quot; rel=&quot;noopener&quot;&gt;Java Web Start Technology&lt;/a&gt; 5. &lt;a href=&quot;http://docs.oracle.com/javase/7/docs/technotes/guides/&quot; rel=&quot;noopener&quot;&gt;Java™ Platform Overview&lt;/a&gt; 6. &lt;a href=&quot;http://docs.oracle.com/javase/7/docs/technotes/guides/jndi/index.html&quot; rel=&quot;noopener&quot;&gt;Java™ Naming and Directory Interface (JNDI)&lt;/a&gt; 7. &lt;a href=&quot;http://docs.oracle.com/javase/7/docs/technotes/guides/intl/index.html&quot; rel=&quot;noopener&quot;&gt;Java™ Internationalization Support&lt;/a&gt; 8. &lt;a href=&quot;http://docs.oracle.com/javase/7/docs/technotes/guides/io/index.html&quot; rel=&quot;noopener&quot;&gt;Java™ I/O, NIO, and NIO.2&lt;/a&gt; 9. &lt;a href=&quot;http://docs.oracle.com/javase/7/docs/technotes/guides/extensions/index.html&quot; rel=&quot;noopener&quot;&gt;The Extension Mechanism&lt;/a&gt; 10. &lt;a href=&quot;https://docs.oracle.com/javase/7/docs/technotes/guides/collections/overview.html&quot; rel=&quot;noopener&quot;&gt;Collections Framework Overview&lt;/a&gt; 11. &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/&quot; rel=&quot;noopener&quot;&gt;Java Concurrency Utilities&lt;/a&gt; 12. 《深入理解Java虚拟机第一章》&lt;/p&gt;
</content:encoded><category>java虚拟机学习</category><author>joyme123</author></item><item><title>c++和Java的对象内存异同</title><link>https://www.myway5.com/blog/object-memory-difference-between-java-and-c-plus-plus/</link><guid isPermaLink="true">https://www.myway5.com/blog/object-memory-difference-between-java-and-c-plus-plus/</guid><description>##前言 好久都没有写博客了。经过一段时间的实习，收获上感觉并不是很大，还好自己也有看一些东西。因为毕业设计做的是c++方面的编程，算是初探c++的内容。这里简单记录自己对c++和java的对象内存区别，谈谈c++为什么性能要比java高。</description><pubDate>Wed, 19 Apr 2017 09:27:42 GMT</pubDate><content:encoded>&lt;p&gt;##前言 好久都没有写博客了。经过一段时间的实习，收获上感觉并不是很大，还好自己也有看一些东西。因为毕业设计做的是c++方面的编程，算是初探c++的内容。这里简单记录自己对c++和java的对象内存区别，谈谈c++为什么性能要比java高。想到哪里就记到哪里，因为很多东西自己也要查证才能确定。 ##1.栈和堆 栈和堆在数据结构上是两种不同的结构，栈的特点是先进后出，而堆则可以看做是一大堆数据的集合。在c++和Java的内存中，也有堆和栈的概念。 首先理解一下，内存中栈中的每一个元素都被称为栈帧，每个栈帧中存储一个方法(function)中的用到的变量内存。程序执行时，就是一个栈中的每一帧被读取执行的过程。堆则是主要用来存储对象。那么在c++和java中到底有什么不同呢? ```c++ object* processObject(int param){ object obj; obj.param = param; return &amp;amp;obj; } int main(){ object* obj = processObject(10); obj-&amp;gt;do_something(); //1 } &lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;br /&gt;1处是会出现一个空指针错误的。而如果这是一段java的代码则完全没有问题。 在c++和java中，一个栈帧被执行完，这部分内存就直接被释放了。而在c++中，所有通过声明产生的对象都是在栈上开辟内存空间，所以函数执行完后这个对象也就被释放了，那么返回出来就是一个空指针了。只有通过new生成的对象才会在堆中申请空间，因此通过new出来的对象都需要在不用时手动释放内存，不然就会内存泄露。而在java中，对象是在堆中生成的(JDK7中的字符串是在常量池中，int等字面值在限定范围内也会在常量池中申请内存),所有方法内的对象都是一个指向堆中相应对象的指针（注意是指针而不是引用，很多人认为java中向方法传递参数是引用传递，但其实是值传递，只不过这个值是指向对象的指针）。而在java中不需要手动释放内存则是因为Java拥有GC机制(Garbage collect,垃圾回收)。这个在之后再谈。 ##2 java的强弱引用和c++的智能指针 java的强弱引用和c++的智能指针都是在希望可以更好的进行内存管理的前提下出现的，java的强弱引用可以帮助GC机制进行粒度更细的内存回收，而c++的智能指针则是让没有GC机制的c++有了一定能力的自动释放内存的能力。 ###a. java的强弱引用 java的引用共有四种,分别为强引用（Strong Reference）、软引用（Soft Reference）、弱引用（Weak Reference）、虚引用（Phantom Reference）。 之所以定义出这么多引用，是希望在GC发生时，可以更灵活的进行对象的销毁。 强引用就是通过new出来的对象，强引用表示的都是必需的对象，这是无论如何都不会被回收的。 而软引用则表示有用但非必需的对象。当系统内存不够将要发生内存溢出异常的时候，会将软引用的对象列入回收范围，进行二次回收。只有这次回收仍然内存不足才会抛出内存溢出异常。JDK1.2之后提供了SoftReference类来实现这个。 弱引用也用来表示非必需的对象，不同的是，这个引用并不能帮助对象躲过任何的GC，也就是说无论如何，发生GC时这个对象都会被回收，弱引用的唯一作用就是用来取得一个对象实例。JDK1.2之后提供了WeakReference类来实现这个。 综合上述，虚引用的存在也比较清晰了。它不仅不能帮助对象躲过GC,甚至不能取得对象的实例。唯一存在的用处就是当其引用的对象被GC回收时会收到一个系统的通知。JDK1.2之后使用PhantomReference类来实现。 ###b. C++的智能指针 相比于Java的GC机制以及为GC服务的各种引用，C++的智能指针则稍显简单。但是这个看似简单的c++的智能指针，对于c++来说意义也许并不是那么低。在我的理解中，c++之所以实用，因为相对于c，c++有着丰富高效的STL库和被大多数开发者接受的OOP。对于一个巨大的项目来说，OOP往往可以更好的帮助系统模块化，降低耦合，而STL库则是开发者进行高效工作的基础。相对于Java,单单从c++没有GC机制，就意味着它有着更高的性能，另外Java的虚拟机也是影响性能的一个方面。可也是因为c++没有GC机制，对于一个多人合作的巨大的项目来说，内存泄露则是面临的首要问题。举个很低端的例子 ```c++ class Example{ Mysql* getMysqlConnect(){ Mysql* mysql = new Mysql; return mysql; } }; 一个项目中所有的mysql对象指针都通过上面的getMysqlConnect()获取,看似没有问题，但是没有人敢保证一个项目中所有使用这个方法的人都会在使用完mysql对象后，手动将其释放。因此这里需要一个shared_ptr或者unique_ptr去包装一下指针，这样调用函数的人就不需要在外部释放对象内存，也就避免了内存泄露的问题。当然在使用shared_ptr时也一定要注意，因为可能会出现循环引用的问题，循环引用则会导致对象内存一直不被释放。 当然了，上面的例子只是为了说明这个例子而列举出来的，事实上可以直接返回对象而不是指针。 &lt;code&gt;c++ class Example{ Mysql getMysqlConnect(){ Mysql mysql; return mysql; } };&lt;/code&gt; 这里我一开始以为是会将对象复制一遍返回出来，但事实上在c++11中有了移动语义，即这种在拷贝语义和移动语义中，会优先使用移动语义来将mysql对象从方法中移出来，而不是拷贝mysql对象返回，并把方法中的对象删除。 ##3 c++如何尽量避免内存泄露 后天再写啦&lt;p&gt;&lt;/p&gt;
&lt;/code&gt;&lt;/pre&gt;</content:encoded><category>c++</category><category>java</category><author>joyme123</author></item><item><title>linux下c/c++的内存泄漏分析</title><link>https://www.myway5.com/blog/linux-c-or-c-plus-plus-memory-leak-analysis/</link><guid isPermaLink="true">https://www.myway5.com/blog/linux-c-or-c-plus-plus-memory-leak-analysis/</guid><description>使用valgrind进行内存分析 简介</description><pubDate>Wed, 19 Apr 2017 09:25:30 GMT</pubDate><content:encoded>&lt;p&gt;使用valgrind进行内存分析&lt;/p&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;官网地址:&lt;a href=&quot;http://valgrind.org&quot; rel=&quot;noopener&quot;&gt;http://valgrind.org&lt;/a&gt; 主要提供以下工具：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Memcheck&lt;/strong&gt; 是一个内存错误的检测工具，帮助你的程序，尤其是c/c++程序出现更少内存的问题。 &lt;strong&gt;Cachegrind&lt;/strong&gt; 是一个缓存和分支预测探查工具，帮助你的程序运行的更快。 &lt;strong&gt;Callgrind&lt;/strong&gt; 也是一个和缓存相关的调用图工具，和Cachegrind有一部分重叠，但也生成一些Cachegrind不提供的信息。 &lt;strong&gt;Helgrind&lt;/strong&gt; 是一个线程错误的检测工具，在多线程场景下能派上用场。 &lt;strong&gt;DRD&lt;/strong&gt; 也是一个线程错误的检测工具，与Helgrind功能一样，但是使用了不同的分析技术，可能会发现不同的问题。 &lt;strong&gt;Massif&lt;/strong&gt; 是一个堆分析工具，帮助程序使用更少的内存。 &lt;strong&gt;DHAT&lt;/strong&gt; 是不同与Massif的堆分析工具，帮助你理解块的生命周期, 块的利用率, 以及layout的低效。 &lt;strong&gt;SGcheck&lt;/strong&gt; 是一个实验性的工具，帮助检测栈的超支和全局数组。是Memcheck的功能方面的补充：可以检测出Memcheck无法发现的问题，反之亦然。 &lt;strong&gt;BBV&lt;/strong&gt; 是一个实验SimPoint基本块向量生成器。对于进行计算机体系结构研究和开发的人来说，这是有用的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里记录的只是Memcheck的使用，其他的使用可以参考上述的官网的网址。&lt;/p&gt;
&lt;h2&gt;ubuntu下的安装&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;sudo apt-get install valgrind&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用方法&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;valgrind [valgrind-options] your-prog [your-prog-options]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如,对ls -l进行分析:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;valgrind --tool=memcheck ls -l&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;valgrind的默认工具就是memcheck，所以使用memcheck工具时可以省略--tool参数&lt;/p&gt;
&lt;h2&gt;注意事项:&lt;/h2&gt;
&lt;p&gt;1、valgrind工具会减慢程序的运行速度 2、程序编译时需要开启-g，帮助valgrind可以更精准的定位到错误 3、程序编译时最好关闭优化，否则可能产生不正确的未初始化错误信息，以及遗漏未初始化错误。 4、程序编译时最好使用-Wall，帮助valgrind在高优化等级的程序中精准识别一些甚至是全部的问题&lt;/p&gt;
&lt;h2&gt;具体使用说明&lt;/h2&gt;
&lt;p&gt;Valgrind会记录下一些注释，文本流，具体的错误报告以及其他的重要的事件。类似与以下的格式: ==12345== some-message-from-Valgrind &lt;strong&gt;12345&lt;/strong&gt;代表进程Id,这个格式方便区分程序输出和Valgrind的注释输出，以及区分多个进程的输出。Valgrind只会输出最重要的信息，如果需要一些次要的信息，可以使用-v参数。 你可以使用三种方式去导出这些错误 1、默认情况:会直接在控制台打印出来 2、使用文件记录，这个时候需要使用参数--log-file=filename,filename代表存储的文件名 3、通过socket发送:使用参数--log-socket=192.168.0.1:12345，不加端口号会使用默认的1500端口，Valgrind提供了一个叫Valgrind-listener的工具去监听这个网络流。&lt;/p&gt;
&lt;h2&gt;读懂memcheck工具产生的错误信息&lt;/h2&gt;
&lt;h3&gt;1.非法读/非法写的错误(Illegal read/Illegal write errors)&lt;/h3&gt;
&lt;p&gt;例如: Invalid read of size 4 at 0x40F6BBCC: (within /usr/lib/libpng.so.2.1.0.9) by 0x40F6B804: (within /usr/lib/libpng.so.2.1.0.9) by 0x40B07FF4: read_png_image(QImageIO *) (kernel/qpngio.cpp:326) by 0x40AC751B: QImageIO::read() (kernel/qimage.cpp:3621) Address 0xBFFFF0E0 is not stack&apos;d, malloc&apos;d or free&apos;d 出现这个错误是因为程序读或写了Valgrind认为不应该读写的内存区域&lt;/p&gt;
&lt;h3&gt;2.使用了为初始化的值&lt;/h3&gt;
&lt;p&gt;例如: Conditional jump or move depends on uninitialised value(s) at 0x402DFA94: _IO_vfprintf (_itoa.h:49) by 0x402E8476: _IO_printf (printf.c:36) by 0x8048472: main (tests/manuel1.c:8) 这样一段错误可能就是由以下的代码产生&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
int x;
printf (&quot;x = %d\n&quot;, x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Valgrind会跟踪变量x,直到x被使用时才会报错。在这里x被传入了printf,进而进入_IO_printf，但是这些都不会报错，只有当x被传递到_IO_vfprintf,_IO_vfprintf开始检查x是否可以被转换为ASCII码时才报错。 未初始化值一般有两种情况:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、局部变量没有被初始化，就像上面一样。&lt;/li&gt;
&lt;li&gt;2、The contents of heap blocks (allocated with malloc, new, or a similar function) before you (or a constructor) write something there.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了找到未初始化变量一开始的位置，可以使用--track-origins=yes参数。当然这会减慢Valgrind的使用速度。&lt;/p&gt;
&lt;h3&gt;3.在系统调用中使用了未初始化或者不可寻址的值&lt;/h3&gt;
&lt;p&gt;Valgrind会检查所有系统调用的参数，一般有以下3类:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、检查所有直接调用的参数，即使已经初始化了。&lt;/li&gt;
&lt;li&gt;2、如果系统调用需要你的程序申请的缓冲区，Valgrind会检查所有的缓冲区内容，看它是否可寻址，内容是否初始化了。&lt;/li&gt;
&lt;li&gt;3、如果系统调用需要写入用户提供的缓冲，Valgrind会检查是否可寻址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面是两个使用了无效参数的系统调用的例子:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include
#include
int main( void )
{
char* arr = malloc(10);
int* arr2 = malloc(sizeof(int));
write( 1 /* stdout */, arr, 10 );
exit(arr2[0]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到这样的错误信息: Syscall param write(buf) points to uninitialised byte(s) at 0x25A48723: __write_nocancel (in /lib/tls/libc-2.3.3.so) by 0x259AFAD3: __libc_start_main (in /lib/tls/libc-2.3.3.so) by 0x8048348: (within /auto/homes/njn25/grind/head4/a.out) Address 0x25AB8028 is 0 bytes inside a block of size 10 alloc&apos;d at 0x259852B0: malloc (vg_replace_malloc.c:130) by 0x80483F1: main (a.c:5) Syscall param exit(error_code) contains uninitialised byte(s) at 0x25A21B44: __GI__exit (in /lib/tls/libc-2.3.3.so) by 0x8048426: main (a.c:8) write（a）和exit（b）都是错误的，a从堆中向标准输出中写入了未初始化的arr。b向exit传递了为初始化的值。注意a的错误在于arr指向的内存区域，而b的错误直接是arr2[0]。&lt;/p&gt;
&lt;h3&gt;4.非法的释放(Illegal frees)&lt;/h3&gt;
&lt;p&gt;例如: Invalid free() at 0x4004FFDF: free (vg_clientmalloc.c:577) by 0x80484C7: main (tests/doublefree.c:10) Address 0x3807F7B4 is 0 bytes inside a block of size 177 free&apos;d at 0x4004FFDF: free (vg_clientmalloc.c:577) by 0x80484C7: main (tests/doublefree.c:10) 这个例子中，一块区域被free了两次，所以出现Illegal frees的错误。&lt;/p&gt;
&lt;h3&gt;5.使用不合适的释放函数去释放堆区域的内存&lt;/h3&gt;
&lt;p&gt;例如: Mismatched free() / delete / delete [] at 0x40043249: free (vg_clientfuncs.c:171) by 0x4102BB4E: QGArray::~QGArray(void) (tools/qgarray.cpp:149) by 0x4C261C41: PptDoc::~PptDoc(void) (include/qmemarray.h:60) by 0x4C261F0E: PptXml::~PptXml(void) (pptxml.cc:44) Address 0x4BB292A8 is 0 bytes inside a block of size 64 alloc&apos;d at 0x4004318C: operator new[](unsigned int) (vg_clientfuncs.c:152) by 0x4C21BC15: KLaola::readSBStream(int) const (klaola.cc:314) by 0x4C21C155: KLaola::stream(KLaola::OLENode const *) (klaola.cc:416) by 0x4C21788F: OLEFilter::convert(QCString const &amp;amp;) (olefilter.cc:272) 这个错误是因为使用new[]开辟内存空间，却使用了free去释放内存。 使用malloc, calloc, realloc, valloc or memalign,必须使用free释放内存。 使用new, 必须使用delete释放内存。 使用new[],必须使用delete[]释放内存。&lt;/p&gt;
&lt;h3&gt;6.源内存区域和目标内存区域重叠&lt;/h3&gt;
&lt;p&gt;memcpy, strcpy, strncpy, strcat, strncat这些函数可以从源内存区域复制内容到目标内存区域。这两块区域是不可以重叠的。POSIX标准规定这种行为是未定义的。 例如 ==27492== Source and destination overlap in memcpy(0xbffff294, 0xbffff280, 21) ==27492== at 0x40026CDC: memcpy (mc_replace_strmem.c:71) ==27492== by 0x804865A: main (overlap.c:40)&lt;/p&gt;
&lt;h3&gt;7.Fishy argument values&lt;/h3&gt;
&lt;p&gt;所以的内存分配函数都指定了分配的内存大小，这个大小必定为正数，或者一般情况下不会极度的大。例如在64位的机器上，不会申请分配2^63大小的内存。这种为负数的或者过于大的参数被成为Fishy argument。 例如: ==32233== Argument &apos;size&apos; of function malloc has a fishy (possibly negative) value: -3 ==32233== at 0x4C2CFA7: malloc (vg_replace_malloc.c:298) ==32233== by 0x400555: foo (fishy.c:15) ==32233== by 0x400583: main (fishy.c:23)&lt;/p&gt;
&lt;h3&gt;8.内存泄漏检测&lt;/h3&gt;
&lt;p&gt;Valgrind会跟踪所有由malloc或new申请的内存，所以当程序退出时,Valgrind知道哪些内存没有被主动释放。 如果--leak-check参数设置得当，对于每一个未被释放的内存块，Valgrind判断从root-set中的指针是否能到达这些内存块。root-set包含（a）普通的所有线程使用的寄存器，（b）初始化的, 对齐的, 指针大小的数据块，包括栈。一个数据块有两种方式可到达，第一种是“start-pointer”，即指针在数据块的开头;第二种是“interior-pointer”，即指针在数据块的中间。“interior-pointer”有多种方式出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;指针一开始是“start-pointer”，被程序有意或无意的移动到中间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可能只是巧合。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;std::string中的char的指针。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有些代码分配块内存，使用前8个去存储作为64位的数。例如sqlite3MemMalloc就是这样做的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可能是一个c++对象（具有析构函数）数组的指针，由new[]来分配内存。这种情况下，有些编译器存储一个“magic cookie”，包含数组长度存储在分配的块开头。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可能是一个多重继承产生的c++对象的内部部分的指针。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在使用了启发式（heuristics）的情况下，stdstring, length64, newarray and multipleinheritance情况下的“interior-pointer”会被当成“start-pointer”对待。 考虑下面这九种情况： Pointer chain AAA Leak Case BBB Leak Case&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;(1) RRR ------------&amp;gt; BBB DR (2) RRR ---&amp;gt; AAA ---&amp;gt; BBB DR IR (3) RRR BBB DL (4) RRR AAA ---&amp;gt; BBB DL IL (5) RRR ------?-----&amp;gt; BBB (y)DR, (n)DL (6) RRR ---&amp;gt; AAA -?-&amp;gt; BBB DR (y)IR, (n)DL (7) RRR -?-&amp;gt; AAA ---&amp;gt; BBB (y)DR, (n)DL (y)IR, (n)IL (8) RRR -?-&amp;gt; AAA -?-&amp;gt; BBB (y)DR, (n)DL (y,y)IR, (n,y)IL, (_,n)DL (9) RRR AAA -?-&amp;gt; BBB DL (y)IL, (n)DL Pointer chain legend: - RRR: a root set node or DR block(一个root set或者直接可达的块） - AAA, BBB: heap blocks（堆块） - ---&amp;gt;: a start-pointer （头指针） - -?-&amp;gt;: an interior-pointer （内部指针） Leak Case legend: - DR: Directly reachable （直接可达） - IR: Indirectly reachable （不直接可达） - DL: Directly lost （直接丢失） - IL: Indirectly lost （不直接丢失） - (y)XY: it&apos;s XY if the interior-pointer is a real pointer （内部指针是一个真实的指针） - (n)XY: it&apos;s XY if the interior-pointer is not a real pointer （内部指针不是一个真实的指针） - (_)XY: it&apos;s XY in either case （任意一个情况） 任意一种情况都可以被归为上述9种情况之一，Valgrind合并其中一些情况，得出4种可能&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&quot;Still reachable（依然可达）&quot;。 这包含情况 1 和 2 (for the BBB blocks) 。 一个内存块的头指针的或者头指针的链被发现，程序员至少在原理上释放了这块内存在程序退出之前。这是一个非常普遍并且不算是一个问题，Valgrind默认不报告这个问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;Definitely lost（绝对丢失）&quot;。 这包含情况3 (for the BBB blocks) 。这意味着这个数据块没有指针可达。数据块被归为丢失，因为程序员在程序结束时不能主动释放它，原因是没有指针指向这块内存。 这可能是在较早之前丢失了指向内存区域的指针。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;Indirectly lost（非直接丢失）&quot;。这包含情况4和9 (for the BBB blocks)。这意味着数据块丢失不是因为没有指针指向它，而是因为所有的指向数据块的指针自己丢失了。 举例来说，如果你有一个二叉树，根节点丢失，所有的他的子节点都变成非直接丢失。因为根节点的直接丢失问题被解决，子节点的非直接丢失问就会消失。Valgrind默认不报告这个问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;Possibly lost（可能丢失）&quot;。 这包含情况5、6、7、8 (for the BBB blocks) 。 这意味着一个或多个数据块指针被发现，但是至少一个指针是内部指针。这可能只是一个内存中的随机值，刚好指向一个数据块，所以你不需要考虑这个情况除非你知道你的代码中出现了内部指针。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面是一个内存泄漏的总结的例子 LEAK SUMMARY: definitely lost: 48 bytes in 3 blocks. indirectly lost: 32 bytes in 2 blocks. possibly lost: 96 bytes in 6 blocks. still reachable: 64 bytes in 4 blocks. suppressed: 0 bytes in 0 blocks. 如果开启的启发式的选项，类似于以下输出 LEAK SUMMARY: definitely lost: 4 bytes in 1 blocks indirectly lost: 0 bytes in 0 blocks possibly lost: 0 bytes in 0 blocks still reachable: 95 bytes in 6 blocks of which reachable via heuristic: stdstring : 56 bytes in 2 blocks length64 : 16 bytes in 1 blocks newarray : 7 bytes in 1 blocks multipleinheritance: 8 bytes in 1 blocks suppressed: 0 bytes in 0 blocks 如果 --leak-check=full 被指定, Memcheck 会给出每一个绝对丢失或可能丢失块的详细情况，包括他们在哪里被分配。它不能告诉你何时、如何、为何指向一个泄露内存块的指针丢失了;这个需要自己解决。通常，你需要保证在程序退出时，你的程序没有任何的绝对丢失或者可能丢失的内存块。 例如 8 bytes in 1 blocks are definitely lost in loss record 1 of 14 at 0x........: malloc (vg_replace_malloc.c:...) by 0x........: mk (leak-tree.c:11) by 0x........: main (leak-tree.c:39) 88 (8 direct, 80 indirect) bytes in 1 blocks are definitely lost in loss record 13 of 14 at 0x........: malloc (vg_replace_malloc.c:...) by 0x........: mk (leak-tree.c:11) by 0x........: main (leak-tree.c:25) 第一条信息描述了一种简单的情况，一个8byte的内存块绝对丢失了。第二种情况描述了另外一个8byte内存块绝对丢失;不同在于第二种情况会引起在另外内存块中的更多的80bytes内存非直接丢失了。loss number没有任何特殊的含义。这个loss number可以在Valgrind gdbserver中用来列出泄漏内存块的地址，或者给出更多的信息关于为何一个内存块仍然可达。 当 --leak-check=full 被指定时，选项--show-leak-kinds= 控制显示的泄漏类型。 有下面几种类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单独指定一或多个： definite indirect possible reachable。&lt;/li&gt;
&lt;li&gt;all代表所有。&lt;/li&gt;
&lt;li&gt;none 代表空集合。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如使用 --show-leak-kinds=definite,possible 来只显示绝对或者可能的内存丢失。&lt;/p&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;p&gt;在调试php时,因为php自己实现了内存管理机制,所有使用valgrind时,会检测出很多php上的内存泄露,我们可以通过&lt;code&gt;export USE_ZEND_ALLOC=0&lt;/code&gt;来让php直接向内存申请内存,这样有助于发现问题&lt;/p&gt;
</content:encoded><category>linux</category><category>c++</category><author>joyme123</author></item><item><title>unable to make backup link of `./usr/bin/chattr&apos; before installing new version: Operation not permitted</title><link>https://www.myway5.com/blog/unable-to-make-backup-link-of-usrbinchattr-before-installing-new-version-operation-not-permitted/</link><guid isPermaLink="true">https://www.myway5.com/blog/unable-to-make-backup-link-of-usrbinchattr-before-installing-new-version-operation-not-permitted/</guid><description>在公司服务器上使用apt-get upgrade遇到这个问题。通过查阅资料，发现问题的关键在于，chattr需要升级但是chatter无法被删除。 使用:</description><pubDate>Wed, 19 Apr 2017 09:24:14 GMT</pubDate><content:encoded>&lt;p&gt;在公司服务器上使用apt-get upgrade遇到这个问题。通过查阅资料，发现问题的关键在于，chattr需要升级但是chatter无法被删除。 使用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lsattr /usr/bin/chattr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现chattr的属性包括i和a,i代表immutable,不可更改，a代表append only,只能增加。这样问题就清楚了，chattr不可更改导致无法升级，至于出现这个情况的原因也不清楚。于是使用chattr更改自己的i和a属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chattr -i /usr/bin/chattr
chattr -a /usr/bin/chattr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有任何效果，而且提示我chattr的用法。出现这种提示的原因往往都是用错了指令，我反复确认指令都没有错。 于是在本地机器上验证chattr的属性，发现是没有i和a属性的，而且上述指令也可以正常工作。 实在没有办法，使用sftp将本地的chattr传到服务器上，命名为chattr_new，再用传上去的chattr_new更改chattr的属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chattr_new -i /usr/bin/chattr
chattr_new -a /usr/bin/chattr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在执行apt-get upgrade，没有任何报错。 注: chattr是用来防止误删操作的，即使是root用户，在chattr为文件添加了i属性后，root用户也无法删除。 lsattr则是用来查看文件的这方面的属性的。&lt;/p&gt;
</content:encoded><category>linux</category><category>问题</category><author>joyme123</author></item><item><title>Ubuntu下Docker安装遇到的问题记录</title><link>https://www.myway5.com/blog/ubuntu-docker-problem/</link><guid isPermaLink="true">https://www.myway5.com/blog/ubuntu-docker-problem/</guid><description>Ubuntu下安装docker的几点问题记录 1.要注意系统的内核和版本是否支持，内核最低要求为3.10，版本最低为12.04，这两点必须同时满足。 使用uname -a查看系统内核 Linux jiang-PC 4.4.</description><pubDate>Wed, 19 Apr 2017 09:20:48 GMT</pubDate><content:encoded>&lt;p&gt;Ubuntu下安装docker的几点问题记录 1.要注意系统的内核和版本是否支持，内核最低要求为3.10，版本最低为12.04，这两点必须同时满足。 使用&lt;code&gt;uname -a&lt;/code&gt;查看系统内核 &lt;code&gt;Linux jiang-PC 4.4.0-72-generic #93-Ubuntu SMP Fri Mar 31 14:07:41 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux&lt;/code&gt; 这里4.4.0-72则是内核的版本号 使用&lt;code&gt;cat /etc/issue&lt;/code&gt;查看系统版本号 &lt;code&gt;Ubuntu 16.04 LTS \n \l&lt;/code&gt; 2.注意查看docker的日志记录 使用service docker start(或者systemctl docker start)启动docker时，没有任何输出提示，但是往后执行可能就发现docker没有正常启动，但是又不知道问题出在哪里。/var/log/upstart/docker.log记录了docker启动日志。 3.AppArmor的问题 AppArmor enabled on system but the docker-default profile could not be loaded。出现这个问题时，使用 &lt;code&gt;apt-get install apparmor&lt;/code&gt;即可解决 附一段webserver的Dockerfile：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 这是服务器环境的Docker,会安装好php,nginx等环境，并且进行配置
# author:jiangpengfei
# date: 2017-04-19

FROM ubuntu:16.04
RUN apt-get update \
    &amp;amp;&amp;amp; DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common \
    &amp;amp;&amp;amp; DEBIAN_FRONTEND=noninteractive apt-add-repository -y ppa:nginx/stable \
    &amp;amp;&amp;amp; DEBIAN_FRONTEND=noninteractive apt-get update \
    &amp;amp;&amp;amp; DEBIAN_FRONTEND=noninteractive apt-get install -y nginx
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y php7.0
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y php-redis
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y php-mysql
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y php-imagick \
    &amp;amp;&amp;amp; apt-get autoremove \
    &amp;amp;&amp;amp; apt-get autoclean
# 上面是从ubuntu的源中安装必要组件，下面开始进行配置
RUN mkdir /var/www/family \
    &amp;amp;&amp;amp; mkdir -p /var/log/nginx/access/ \
    &amp;amp;&amp;amp; touch /var/log/nginx/access/default.log

COPY family /etc/nginx/sites-enabled 
COPY start.sh /usr/local/bin
EXPOSE 9090 81
CMD [&quot;start.sh&quot;]
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>linux</category><category>问题</category><author>joyme123</author></item></channel></rss>