<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Posts on codedump notes</title>
    <link>https://www.codedump.info/zh/post/</link>
    <description>Recent content in Posts on codedump notes</description>
    <generator>Hugo</generator>
    <language>zh</language>
    <lastBuildDate>Sun, 12 Apr 2026 09:59:41 +0800</lastBuildDate>
    <atom:link href="https://www.codedump.info/zh/post/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Rockraft：基于 OpenRaft 与 RocksDB 的强一致 KV 存储框架</title>
      <link>https://www.codedump.info/zh/post/20260412-rockraft/</link>
      <pubDate>Sun, 12 Apr 2026 09:59:41 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20260412-rockraft/</guid>
      <description>&lt;h2 class=&#34;heading&#34; id=&#34;动机&#34;&gt;&#xA;  动机&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8a%a8%e6%9c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Redis协议已经成为事实意义上的key-value存储协议标准。除了官方的Redis实现以外，我们看到还有各种兼容Redis协议的实现：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/valkey-io/valkey&#34;&gt;Valkey&lt;/a&gt;：Linux基金会官方fork的Redis 7.2分支，采用BSD许可证，是Redis更换SSPL许可证后社区推出的真正开源替代方案，完全兼容Redis协议和持久化机制。&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/dragonflydb/dragonfly&#34;&gt;Dragonfly&lt;/a&gt;：追求极致性能的现代多线程内存数据库，可提供相比Redis高达25倍的吞吐量和更低的尾延迟，但采用BSL许可证（4年后转Apache 2.0）。&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/snapchat/keydb&#34;&gt;KeyDB&lt;/a&gt;：Snapchat收购维护的Redis多线程分支，在100%兼容Redis API的基础上增加了主动复制（Active Replication）和Flash存储扩展能力，但更新已放缓。&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/apache/kvrocks&#34;&gt;Kvrocks&lt;/a&gt;：Apache顶级项目，基于RocksDB实现数据持久化到磁盘的分布式KV存储，支持数十TB级数据且成本仅为内存方案的1/5-1/10，适合大容量低成本场景。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;但是，上面的任何一个实现，都没有在分布式系统的强一致性上走得更远，在这个维度，它们依然采用了Redis原生实现的最终一致性。&lt;/p&gt;&#xA;&lt;p&gt;最开始，我创建&lt;a href=&#34;https://github.com/lichuang/coredb&#34;&gt;coredb&lt;/a&gt;项目，是为了利用raft共识算法，实现一个满足强一致性且兼容redis协议的服务。我知道一定有人会有疑问：大家使用Redis类的系统，是为了缓存数据，在这类型的项目里，一般都会选择&lt;a href=&#34;https://en.wikipedia.org/wiki/CAP_theorem&#34;&gt;CAP&lt;/a&gt;中的AP，把可用性放在第一位，而非一致性。&lt;/p&gt;&#xA;&lt;p&gt;但是，回到文章最开始的结论：Redis协议已经成为事实意义上的key-value存储协议标准。如果在这个大前提下，它的生态价值不应仅仅局限于传统的内存缓存。通过为其引入强一致性的持久化存储，我们可以赋予它全新的生命力和应用场景——正如 HTTP 协议从早期的网页传输协议，最终演变为无处不在的通信基石一样。&lt;/p&gt;&#xA;&lt;p&gt;最开始，我构建的项目只有coredb，这是一个采用Raft算法+rocksdb的强一致且兼容Redis协议的服务，也就是说，可以继续使用redis客户端访问这个服务，但是它满足强一致性：只要数据写入时返回成功，意味着至少在集群中的半数以上节点写入成功。&lt;/p&gt;&#xA;&lt;p&gt;在开发过程中我意识到，“Raft + RocksDB” 的架构组合具有极高的通用价值。考虑到许多开发者可能也需要这样一套可靠的底层基座，去构建他们自己专属的强一致性存储系统，我便将这部分核心逻辑进行了解耦，单独抽离出了 &lt;a href=&#34;https://github.com/lichuang/rockraft&#34;&gt;Rockraft&lt;/a&gt; 这个基础框架项目。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;设计与实现&#34;&gt;&#xA;  设计与实现&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%ae%be%e8%ae%a1%e4%b8%8e%e5%ae%9e%e7%8e%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Rockraft采用Rust开发，这是我目前最喜欢的系统编程语言：类型安全且内存安全，这两个特性是我最喜欢的Rust语言特性，有了这两个特性在编程时会更加放心。目前Rust语言的常见Raft实现有以下两个：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/tikv/raft-rs&#34;&gt;raft-rs&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Rust生态中最成熟、生产验证最充分的Raft实现，被近千个生产环境采用。它源自etcd的Go实现移植，但完全用Rust重写，保证了线程安全和内存安全。&lt;/p&gt;&#xA;&lt;p&gt;架构特点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;核心共识模块：仅提供纯共识算法核心（Raft状态机），不包含日志存储、网络传输或状态机实现&lt;/li&gt;&#xA;&lt;li&gt;高度可定制：需自行实现Storage Trait（日志存储）和RaftMessage传输层，灵活性极高&lt;/li&gt;&#xA;&lt;li&gt;多Raft支持：TiKV基于此实现了Multi-Raft架构，支持海量Region分片&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;功能完整性：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;✅ Leader选举、日志复制、成员变更（Joint Consensus）&lt;/li&gt;&#xA;&lt;li&gt;✅ PreVote机制避免网络分区干扰&lt;/li&gt;&#xA;&lt;li&gt;✅ Leader Lease读取优化&lt;/li&gt;&#xA;&lt;li&gt;✅ Snapshot快照传输&lt;/li&gt;&#xA;&lt;li&gt;✅ CheckQuorum检查机制&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;raft-rs的生产用户包括：&lt;a href=&#34;https://github.com/tikv/tikv&#34;&gt;TiKV&lt;/a&gt;（分布式事务KV数据库）&lt;/p&gt;&#xA;&lt;p&gt;注意：该库已进入维护模式，新功能开发放缓，建议新项目考虑OpenRaft&lt;/p&gt;&#xA;&lt;p&gt;2、&lt;a href=&#34;https://github.com/databendlabs/openraft&#34;&gt;OpenRaft&lt;/a&gt; 🚀 现代化异步架构&lt;/p&gt;&#xA;&lt;p&gt;设计理念：完全异步事件驱动，不依赖定时tick，消息批处理优化高吞吐。&lt;/p&gt;&#xA;&lt;p&gt;核心亮点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;事件驱动架构：基于Raft事件而非轮询，避免空转，大幅提升资源利用率&lt;/li&gt;&#xA;&lt;li&gt;统一API：单一Raft类型，通过RaftLogStorage、RaftStateMachine、RaftNetwork三个Trait扩展存储和网络层&lt;/li&gt;&#xA;&lt;li&gt;完善的成员变更：采用更通用的Joint Consensus，支持任意成员变更（单次可增删多节点），而非单步变更&lt;/li&gt;&#xA;&lt;li&gt;内置可观测性：集成tracing日志和分布式追踪，支持编译时调整日志级别&lt;/li&gt;&#xA;&lt;li&gt;手动控制：支持手动触发选举(trigger_elect)、快照(trigger_snapshot)、日志清理(purge_log)，便于运维&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;功能特性：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;✅ 线性一致性读取(ensure_linearizable)&lt;/li&gt;&#xA;&lt;li&gt;✅ Learner(Non-voter)角色支持&lt;/li&gt;&#xA;&lt;li&gt;✅ 动态心跳/选举开关控制&lt;/li&gt;&#xA;&lt;li&gt;⛔️ 不支持单步成员变更（设计取舍，倾向更安全的Joint Consensus）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;它的生产用户：&lt;a href=&#34;https://github.com/databendlabs/databend&#34;&gt;Databend&lt;/a&gt;（云原生数仓）、&lt;a href=&#34;https://github.com/cnosdb/cnosdb&#34;&gt;CnosDB&lt;/a&gt;（时序数据库）、&lt;a href=&#34;https://github.com/robustmq/robustmq&#34;&gt;RobustMQ&lt;/a&gt;（云原生消息队列）。&lt;/p&gt;</description>
    </item>
    <item>
      <title>写代码与做菜</title>
      <link>https://www.codedump.info/zh/post/20250227-cook/</link>
      <pubDate>Thu, 27 Feb 2025 11:15:30 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20250227-cook/</guid>
      <description>&lt;p&gt;过去的几年里，由于长期在家远程办公，多了很多自己做菜的机会，厨艺也精进了不少。目前一些普通的家常菜，即便我之前没有做过，但是可以根据网上的视频教程就能还原大体的风味出来。究其原因，我想是写代码中模块化的思想，用在了做菜上。&lt;/p&gt;&#xA;&lt;p&gt;不妨把做菜大体分为三个阶段：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;准备阶段：包括一些预处理，如改刀、腌制、去腥等等；&lt;/li&gt;&#xA;&lt;li&gt;烹饪阶段：使用日常说的煎炒烹炸等手段对材料进行烹制，包括煮、红烧、煎、炒、清蒸等等；&lt;/li&gt;&#xA;&lt;li&gt;收尾阶段：烹饪阶段完毕之后，还要对菜进行一些收尾工作，例如收汁、清蒸鱼时的泼油等等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;做菜三阶段&#34; src=&#34;https://www.codedump.info/media/imgs/20250227-cook/%E5%81%9A%E8%8F%9C%E4%B8%89%E9%98%B6%E6%AE%B5.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 做菜三阶段 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;按照写代码模块化的思想，当把这些阶段划分成具体的模块之后，模块之间进行排列组合之后，就可以做成不同的菜，举例来说，红烧鱼和清蒸鱼是两种不同的料理鱼的方式，但是却有着相同的预处理阶段：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;准备阶段：鱼去腥、鱼背花刀方便入味&lt;/li&gt;&#xA;&lt;li&gt;烹饪阶段：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;清蒸鱼：清蒸&lt;/li&gt;&#xA;&lt;li&gt;红烧鱼：调入酱料红烧&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;收尾阶段：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;清蒸鱼：倒掉蒸鱼容器中的水（太腥），切葱丝放于鱼身上，烧热油淋上；&lt;/li&gt;&#xA;&lt;li&gt;红烧鱼：大火收汁。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;两种不同的鱼料理&#34; src=&#34;https://www.codedump.info/media/imgs/20250227-cook/%E4%B8%A4%E7%A7%8D%E4%B8%8D%E5%90%8C%E7%9A%84%E9%B1%BC%E6%96%99%E7%90%86.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 两种不同的鱼料理 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;从这里可以看出，不论哪种做鱼的方式，预处理阶段是大体一致的。用程序设计的话来说：这个模块是可以复用的。也就是说，一旦学会了如何预处理一条要清蒸的鱼，换做其它方式料理的鱼，也可以使用同样的办法类似处理。&lt;/p&gt;&#xA;&lt;p&gt;类似的，其它阶段也有类似可以复用的模块：例如在我看来，红烧鱼和红烧肉中烹饪的红烧阶段，就大体一致，都是要调入一些酱料对食物进行烹煮，只不过由于食材的不同，选料不同以及烹饪时长不同。所以当学会如何红烧鱼以后，稍微了解也能够学会如何红烧大肉。&lt;/p&gt;&#xA;&lt;p&gt;可见，对菜的烹饪进行阶段划分之后，可复用的模块多了，就能在不同的菜里排列组合不同的模块，这就能组合出不同的菜来。&lt;/p&gt;&#xA;&lt;p&gt;所以，现在看各种教做菜的视频时，可以有意的这样来看：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;这道菜的三阶段分别有哪些？哪些我还没有掌握，哪些又是和之前类似的流程，可以稍加改动就能复用；&lt;/li&gt;&#xA;&lt;li&gt;菜的调料有哪些，比例如何？&lt;/li&gt;&#xA;&lt;li&gt;菜的火候要求到什么程度？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以看到，从阶段划分以及模块复用的思想再来看菜的制作，就会清晰很多。&lt;/p&gt;&#xA;&lt;p&gt;除此以外，在具体的模块实现时，还能参考面向对象的思想。例如，红烧肉中有一道工序是“给食物上色”，做法有：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;加糖用油或者水来炒制糖色；&lt;/li&gt;&#xA;&lt;li&gt;使用老抽上色；&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;用面向对象的思想来说，“给食物上色”是一个接口，有以上两种不同的实现。以我而言，我总是把握不住炒糖色的火候，所以同样的接口我选择了对我而言更容易实现的老抽上色。这并不影响大局，因为程序设计里又有另一句话：完成比完美更重要。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;食物上色&#34; src=&#34;https://www.codedump.info/media/imgs/20250227-cook/%E9%A3%9F%E7%89%A9%E4%B8%8A%E8%89%B2.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 食物上色 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;类似的，把做菜分为多道工序以后，如果某些工序并不熟练，可以尝试交给别人，或者使用只需要简单加工的半成品。例如，如果不会宰杀鸡鸭，那么有以下几种选择：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;可以将这道工序外包给摊贩，让他们帮忙；&lt;/li&gt;&#xA;&lt;li&gt;或者买袋装的冰鲜鸡，味道稍差一些。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下这幅图里，就以吃披萨为例，来解释各种AAS（As A Service）的区别，本质上把不同的工序外包出去，就是使用了某种服务：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;pizza as a service&#34; src=&#34;https://www.codedump.info/media/imgs/20250227-cook/pizza.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; pizza as a service &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;做一个简单的总结：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;熟练掌握做菜中经常用到的“模块”，红烧、清蒸、腌肉等，因为这些模块会在多个菜品中被复用；&lt;/li&gt;&#xA;&lt;li&gt;将一道菜划分为几个不同的阶段，方便模块复用，这样就能组合起以前掌握的模块，来完成还不会的菜；&lt;/li&gt;&#xA;&lt;li&gt;将自己不熟练的模块，外包出去，寻找合适的服务替代；&lt;/li&gt;&#xA;&lt;li&gt;完成比完美重要，能够独立借助不同的服务来完成一道菜，就可以慢慢累积在这方面的自信心。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这几年在做菜上的投入，也让我有了另外的领悟。在中文里，“火候”这个词真的很妙，“火”强调了要对食物做功，“候”说明了要耐心等待时间。似乎在英文里找不到关于“火候”一词非常信达雅的翻译，如果直译过去是“Fire Control”，但是这一个翻译丢掉了中文语境里时间的部分。另一个词“功夫”也是，英语里“KongFu”只有武功的含义，而在中文里并不单指“武功”，在现在更多说的是在某件事情上投入时间。&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第24期）：sqlite并发读写的演进之路</title>
      <link>https://www.codedump.info/zh/post/20220904-weekly-24/</link>
      <pubDate>Sun, 04 Sep 2022 09:10:31 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220904-weekly-24/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本文梳理sqlite并发读写方案的演进之路。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;sqlite并发读写的演进之路&#34;&gt;&#xA;  sqlite并发读写的演进之路&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#sqlite%e5%b9%b6%e5%8f%91%e8%af%bb%e5%86%99%e7%9a%84%e6%bc%94%e8%bf%9b%e4%b9%8b%e8%b7%af&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;概论&#34;&gt;&#xA;  概论&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%ae%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;sqlite底层的存储基于B-tree，B-Tree对底层存储的基本读写单位是页面，而每个页面都由全局唯一的页面编号与之对应，一般来说页面编号从1开始递增。&lt;/p&gt;&#xA;&lt;p&gt;类B-Tree的存储引擎修改数据的流程如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;b-tree&#34; src=&#34;https://www.codedump.info/media/imgs/20220904-weekly-24/b-tree.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; b-tree &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;从上图中，需要区分B-Tree类的存储引擎几个核心的模块：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;B-Tree算法模块：从页面管理器中读取页面到内存，进行逻辑的修改，修改完毕之后标记该页面为脏页面，这样页面管理器就知道哪些页面被修改，后续需要进行落盘。&lt;/li&gt;&#xA;&lt;li&gt;页面管理器：负责向B-Tree算法模块提供根据页面编号读、写页面的接口。&lt;/li&gt;&#xA;&lt;li&gt;数据库文件：这其实不是一个模块，泛指在磁盘上的数据库相关文件，任何的修改最终都要落到数据库文件。在sqlite中，数据库文件是单一文件，在其他存储引擎里可能是一组相关的文件。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;最上层的B-Tree算法模块，在进行写事务的时候，是首先向页面管理器发起读页面到内存中的请求，注意到B-Tree模块并不会直接跟数据库文件打交道，而是经过页面管理器模块（下面会展开说），修改了页面之后标记为“脏页面”，页面管理器最终负责将脏页面落盘到数据库文件中。&lt;/p&gt;&#xA;&lt;p&gt;现在来谈谈“页面管理器”模块的具体工作，也有的实现称为“缓存管理器（buffer manager）”。这个模块负责：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;在内存中管理页面，这涉及到两部分内容：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如果页面当前不在内存中，需要根据页面编号到磁盘上加载页面。&lt;/li&gt;&#xA;&lt;li&gt;页面也并不是每一次读写时都要到磁盘上加载，有些时候页面已经在缓存中存在了，这种情况下不需要到磁盘上加载页面数据。于是，“页面管理器”模块还需要负责维护这些内存中的页面缓存，何时淘汰这些页面、淘汰哪些内存中的页面、何时真正从磁盘上加载，都是这个模块的工作。&lt;/li&gt;&#xA;&lt;li&gt;对外部而言（这里的外部更多的是B-Tree算法模块），其实不需要也看不到页面缓存的细节，页面管理器对外提供根据页面编号读、写页面接口即可。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;错误的恢复、事务的管理。比如：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一次事务要修改N个页面，修改到中间的时候，进程崩溃了，这时候重新启动时需要恢复到这个事务之前的数据成功启动，即需要提供回滚事务的功能。&lt;/li&gt;&#xA;&lt;li&gt;同样的一个事务要修改N个页面，在事务还未提交的时候，如果事务级别不是read uncommitted， 那么前面的修改效果不能被其他事务可见，这也是页面管理器需要做的事情，毕竟它对外提供了读、写页面的接口，同一个页面编号的页面什么时候的内容可见都由它来决定。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;有了这些基础的了解，我们来看看sqlite在并发读写方面的演进之路。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;journal&#34;&gt;&#xA;  Journal&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#journal&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;最早的页面管理器实现是基于Journal文件的，这个文件存储页面在修改之前的内容：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;journal&#34; src=&#34;https://www.codedump.info/media/imgs/20220904-weekly-24/journal.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; journal &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;可以看到的是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Journal文件存储了一个事务所要修改的页面在修改之前的内容，这个定义有点拗口，姑且称为“旧页面内容”。&lt;/li&gt;&#xA;&lt;li&gt;每次一个事务提交之后，意味着这个事务所有队页面的修改都已经落到了数据库文件中，这时候Journal文件里保存的旧页面内容就不再需要了，可以被删除了。&lt;/li&gt;&#xA;&lt;li&gt;由于每次事务修改都要落盘到数据库文件，这些落盘操作涉及到多次磁盘寻道，即一次事务多次随机磁盘寻道，这样代价其实是很大的。&lt;/li&gt;&#xA;&lt;li&gt;当需要事务回滚的功能时，页面管理器就可以从Journal文件中读出来旧页面内容覆盖回去。&lt;/li&gt;&#xA;&lt;li&gt;虽然这个算法很简单，但是缺陷也明显：它没有任何的读写并发支持。每次开始一个写事务，从开始写事务，到这个写事务提交完成的过程中间，其他的读写事务都不能开始，可以说是“一写全卡住”。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;wal&#34;&gt;&#xA;  WAL&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#wal&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;从上面的分析可以看出，以Journal文件的机制，每次写事务：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;需要把内容修改全部落盘到数据库文件才能算完成。&lt;/li&gt;&#xA;&lt;li&gt;这个过程中间，不能同时存在其他并发的读、写操作。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从sqlite3.7.0版本开始（&lt;a href=&#34;https://www.sqlite.org/releaselog/3_7_0.html&#34;&gt;SQLite Release 3.7.0 On 2010-07-21&lt;/a&gt;），sqlite引入了更常见的WAL机制来解决页面的读写并发问题，WAL的原理如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;wal&#34; src=&#34;https://www.codedump.info/media/imgs/20220904-weekly-24/wal.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; wal &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;WAL机制中，事务对页面的修改：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;并没有马上落到数据库文件里，而是首先写入WAL文件中。这样有两个好处：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;WAL文件是append-only的文件，在文件结尾处添加新内容，对写磁盘文件这种操作而言是更快的，因为少了很多磁盘寻道的流程。&lt;/li&gt;&#xA;&lt;li&gt;由于事务的修改并没有马上落盘到数据库文件，所以就并不可见，后续如果需要回滚事务的修改也更容易：不要这个事务修改的那部分WAL内容即可。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;由于修改有时候还未落盘，需要维护一个wal中页面的索引，用于根据页面编号定位到WAL中的页面。由于wal索引可以控制哪些wal文件内容“可见”，于是就能控制未提交的事务修改对读操作并不可见了。&lt;/li&gt;&#xA;&lt;li&gt;WAL文件不能一直增长下去，需要定期把WAL文件中已经提交的事务修改内容落盘到数据库文件，这个流程被称为“checkpoint”。在“checkpoint”之后，wal索引就可以修改了。虽然checkpoint过程将WAL文件中的内容落盘到数据库文件，仍然是针对数据库文件的随机写流程，有很多磁盘寻道操作，但是由于一次checkpoint累计了多次写事务一次性落盘，代价小了一些。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;有了WAL之后，读写并发有了一些改善：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;虽然同一时间仍然只能有一个写事务在进行，但是读事务同时存在多个。其核心原因是因为修改并没有马上直接落盘到数据库文件中，这样修改的可见性就可以由wal索引来控制，即：写事务尽管写，读事务尽管读，只要控制这些写事务的修改不在wal索引中可见即可。&lt;/li&gt;&#xA;&lt;li&gt;WAL虽然支持“一写多读”，而不是Journal文件那样的“一写全卡住”，但是还有一个问题没有解决：在做checkpoint操作的时候，连写事务也不能进行了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;两个可能的优化方案&#34;&gt;&#xA;  两个可能的优化方案&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%a4%e4%b8%aa%e5%8f%af%e8%83%bd%e7%9a%84%e4%bc%98%e5%8c%96%e6%96%b9%e6%a1%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;以下介绍sqlite目前在讨论的两个优化方案，之所以说是“可能”，因为看这部分代码还并没有合并到主干中，目前暂时还在分支里，参见：https://github.com/sqlite/sqlite/tree/begin-concurrent-pnu-wal2。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;wal-2&#34;&gt;&#xA;  WAL-2&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#wal-2&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;为了解决“checkpoint时无法进行写事务”的痛点，sqlite目前在尝试新的WAL-2机制。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;wal-2&#34; src=&#34;https://www.codedump.info/media/imgs/20220904-weekly-24/wal-2.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; wal-2 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;引入WAL-2之后，同时有两个WAL文件，这样可以：checkpoint其中一个WAL文件时，继续写另一个WAL文件，下一次再进行checkpoint时进行切换，这样checkpoint就不会阻塞住写操作。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;begin-concurrent&#34;&gt;&#xA;  BEGIN CONCURRENT&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#begin-concurrent&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;目前的WAL机制，都只能支持同一时间一个写事务，&lt;code&gt;BEGIN CONCURRENT&lt;/code&gt;机制可以实现多个写并发，这篇&lt;a href=&#34;https://www.sqlite.org/cgi/src/doc/begin-concurrent/doc/begin_concurrent.md&#34;&gt;SQLite: Begin Concurrent&lt;/a&gt;文档中，大概描述了一下这个优化的思路：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第23期）：图解Blink-Tree：B&#43;Tree的一种并发优化结构和算法</title>
      <link>https://www.codedump.info/zh/post/20220807-weekly-23/</link>
      <pubDate>Sun, 07 Aug 2022 10:33:38 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220807-weekly-23/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：&lt;a href=&#34;https://www.csd.uoc.gr/~hy460/pdf/p650-lehman.pdf&#34;&gt;《Efficient Locking for Concurrent Operations on B-Trees 》&lt;/a&gt;论文中提出了一种称为“Blink-Tree”的数据结构，这个数据结构提供了B+Tree并发访问的一些优化方式，本文对这篇论文进行解读。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概论&#34;&gt;&#xA;  概论&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%ae%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;由于Blink-Tree本质上是B+Tree的一种优化，所以要理解它首先要对B+Tree有一些了解，在这以前介绍过B+Tree，就不在这里阐述了，可以参考：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;我们来看如果同时存在两个读写操作并发访问一颗B+Tree，会出现什么问题，见下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;b&amp;#43;tree-1&#34; src=&#34;https://www.codedump.info/media/imgs/20220807-weekly-23/b&amp;#43;tree-1.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; b+tree-1 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;进程P1查询数据15，而进程P2写入数据9，当P2写入数据完毕时，树结构变成了下图这样：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;b&amp;#43;tree-2&#34; src=&#34;https://www.codedump.info/media/imgs/20220807-weekly-23/b&amp;#43;tree-2.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; b+tree-2 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;由于原先的叶子节点要满足B+Tree的性质，所以分成了两个叶子节点，而这时P1进程对此并没有感知，还停留在旧的节点上，于是就导致了查询数据15失败。&lt;/p&gt;&#xA;&lt;p&gt;一种最直观的优化方式是读、写的时候加全局锁，但是这样做的效率不高。Blink-Tree就是为了高效解决这类并发访问问题引入的一种结构和算法。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;数据结构&#34;&gt;&#xA;  数据结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;Blink-Tree本质上还是一颗B+Tree，即数据存储在叶子节点上的B-Tree。&lt;/p&gt;&#xA;&lt;p&gt;对于一颗&lt;code&gt;k-degree&lt;/code&gt;的Blink-Tree而言，它有如下的性质：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;所有叶子节点是同一高度的，即从根节点到每个叶子节点都是同一长度。（Each path from the root to any leaf has the same length, h.）&lt;/li&gt;&#xA;&lt;li&gt;对于每个内部节点而言，除非是根节点，否则都至少有&lt;code&gt;k+1&lt;/code&gt;子节点。（Each node except the root and the leaves has at least k + 1 sons.）&lt;/li&gt;&#xA;&lt;li&gt;根节点要么是叶子节点，否则至少有两个子节点。（The root is a leaf or has at least two sons.）&lt;/li&gt;&#xA;&lt;li&gt;内部节点最多有&lt;code&gt;2k+1&lt;/code&gt;个子节点（Each node has at most 2k + 1 sons），结合上面的内容即内部节点的子节点数量在&lt;code&gt;[k+1,2k+1]&lt;/code&gt;之间。&lt;/li&gt;&#xA;&lt;li&gt;数据都存储在叶子节点上。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以看到，上面的性质和B+Tree很相似，在此基础上Blink-Tree还增加了以下数据：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第22期）：图解一致性模型</title>
      <link>https://www.codedump.info/zh/post/20220710-weekly-22/</link>
      <pubDate>Sun, 10 Jul 2022 14:41:24 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220710-weekly-22/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本文使用大量的图例，同时没有难懂的公式，意图解释清楚一致性模型要解决什么问题，以及三种一致性模型：顺序一致性、线性一致性、因果一致性。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;图解一致性模型&#34;&gt;&#xA;  图解一致性模型&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9b%be%e8%a7%a3%e4%b8%80%e8%87%b4%e6%80%a7%e6%a8%a1%e5%9e%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;解决什么问题&#34;&gt;&#xA;  解决什么问题？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%a7%a3%e5%86%b3%e4%bb%80%e4%b9%88%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;分布式系统要保证系统的可用性，就需要对数据提供一定的冗余度：一份数据，要存储在多个服务器上，才能认为保存成功，至于这里要保存的冗余数，有&lt;code&gt;Majority&lt;/code&gt;和&lt;code&gt;Quorum&lt;/code&gt;之说，可以参考之前的文章：&lt;a href=&#34;https://www.codedump.info/post/20220528-weekly-17/&#34;&gt;周刊（第17期）：Read-Write Quorum System及在Raft中的实践&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;p&gt;同一份数据保存在多个机器上提供冗余度，也被称为&lt;code&gt;副本(replica)策略&lt;/code&gt;，这个做法带来下面的好处：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;容错性：即便分布式系统中几台机器不能工作，系统还能照常对外提供服务。&lt;/li&gt;&#xA;&lt;li&gt;提升吞吐量：既然同一份数据存储在多个机器上，对该数据的请求（至少是读请求）能够分担到多个副本上，这样整个系统可以线性扩容增加更多的机器以应付请求量的增加。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;同时，副本策略也有自己需要解决的问题，其中最重要的问题就是一致性问题：在系统中的一个机器写入的数据，是否在系统中其他机器看来也是一样的？&lt;/p&gt;&#xA;&lt;p&gt;很显然，即便在一切都正常工作的条件下，在系统中的一个机器成功写入了数据，因为广播这个修改到系统中的其他机器还需要时间，那么系统的其他机器看到这个修改的结果也还是需要时间的。换言之，中间的这个&lt;code&gt;时间差&lt;/code&gt;可能出现短暂的数据不一致的情况。&lt;/p&gt;&#xA;&lt;p&gt;可以看到，由于这个&lt;code&gt;时间差&lt;/code&gt;的客观存在，并不存在一个&lt;code&gt;绝对&lt;/code&gt;意义上的数据一致性。换言之，&lt;code&gt;数据一致性&lt;/code&gt;有其实现的严格范围，越严格的数据一致，要付出的成本、代价就越大。&lt;/p&gt;&#xA;&lt;p&gt;为了解决一致性问题，需要首先定义一致性模型，在维基的页面上，&lt;code&gt;一致性模型（Consistency model）&lt;/code&gt;的定义如下：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;In computer science, a consistency model specifies a contract between the programmer and a system, wherein the system guarantees that if the programmer follows the rules for operations on memory, memory will be consistent and the results of reading, writing, or updating memory will be predictable.&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;我们举一个日常生活中常见的问题来解释&lt;code&gt;一致性模型&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;wechat&#34; src=&#34;https://www.codedump.info/media/imgs/20220710-weekly-22/wechat.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; wechat &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第21期）：Lamport时钟介绍</title>
      <link>https://www.codedump.info/zh/post/20220703-weekly-21/</link>
      <pubDate>Sun, 03 Jul 2022 10:59:09 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220703-weekly-21/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：在分布式系统中，由于有多个机器（进程）在一起协调工作，于是如何定义分布式系统中事件的先后顺序就成了难题，本文介绍论文 &lt;a href=&#34;https://lamport.azurewebsites.net/pubs/time-clocks.pdf&#34;&gt;《Time, Clocks, and the Ordering of Events in a Distributed System》&lt;/a&gt;中提到的Lamport时钟。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;lamport时钟介绍&#34;&gt;&#xA;  Lamport时钟介绍&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#lamport%e6%97%b6%e9%92%9f%e4%bb%8b%e7%bb%8d&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;概论&#34;&gt;&#xA;  概论&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%ae%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在分布式系统中，由于有多个机器（进程）在一起协调工作，于是如何定义分布式系统中事件的先后顺序就成了难题，本文介绍论文 &lt;a href=&#34;https://lamport.azurewebsites.net/pubs/time-clocks.pdf&#34;&gt;《Time, Clocks, and the Ordering of Events in a Distributed System》&lt;/a&gt;中提到的Lamport时钟。&lt;/p&gt;&#xA;&lt;p&gt;内容以如下的顺序展开：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;物理时钟的问题在哪里？（解决了什么问题）&lt;/li&gt;&#xA;&lt;li&gt;全序和偏序关系。（数学基础）&lt;/li&gt;&#xA;&lt;li&gt;Lamport时钟的原理介绍、&lt;code&gt;happen-before&lt;/code&gt;关系介绍。（原理介绍）&lt;/li&gt;&#xA;&lt;li&gt;分布式一致性的基础。（更远的影响）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;物理时钟的问题&#34;&gt;&#xA;  物理时钟的问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%89%a9%e7%90%86%e6%97%b6%e9%92%9f%e7%9a%84%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;分布式系统中定义一个事件的先后顺序是一个难点，下意识的第一反应是：给每个事件加上一个物理的时间戳，不就可以比较不同事件的时间戳来决定其顺序了吗？&lt;/p&gt;&#xA;&lt;p&gt;这样做的问题在于：在分布式系统中，由多个机器组合起来协调工作，而每个机器上的物理时间也不尽相同，所以“物理时间戳”本质上是一个机器属性，并不一定系统中所有机器都满足同一个时间度量。&lt;/p&gt;&#xA;&lt;p&gt;以下图为例：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;&#34; src=&#34;https://sookocheff.com/post/time/lamport-clock/assets/clock-adjustment-error.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;（引用自&lt;a href=&#34;https://sookocheff.com/post/time/lamport-clock/&#34;&gt;Lamport Clocks - Kevin Sookocheff&lt;/a&gt;）&lt;/p&gt;&#xA;&lt;p&gt;上图中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;server A在发出事件A时，打上了本机的时间戳1点。&lt;/li&gt;&#xA;&lt;li&gt;同理，server B给事件B打上了本机的时间戳12:59。&lt;/li&gt;&#xA;&lt;li&gt;可以看到这两个事件都以本地时间为准，当观察者进程收到这两个事件的时候，先后顺序与事件上所带的时间戳并不一致：先收到了时间戳更大的事件A。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这个例子说明：在分布式系统中，以“物理时间”来衡量事件的先后顺序，并不可行。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;全序和偏序&#34;&gt;&#xA;  全序和偏序&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%85%a8%e5%ba%8f%e5%92%8c%e5%81%8f%e5%ba%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在继续讲解之前，还需要了解两个数学上的概念：全序（total ordering）和偏序（partial ordering）关系。&lt;/p&gt;&#xA;&lt;p&gt;我们首先来定义集合上的几种关系，对一个集合${\displaystyle X}$中的${\displaystyle a,b}$和${\displaystyle c}$ 而言，有以下这些关系：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;自反性：∀a∈S，有a≤a。&lt;/li&gt;&#xA;&lt;li&gt;反对称性：若 $ {\displaystyle a\leq b}$且$ {\displaystyle b\leq a} $ 则 $ {\displaystyle a=b} $。&lt;/li&gt;&#xA;&lt;li&gt;传递性：若${\displaystyle a\leq b} $ 且 $ {\displaystyle b\leq c} $ 则 $ {\displaystyle a\leq c} $。&lt;/li&gt;&#xA;&lt;li&gt;完全性：$ {\displaystyle a\leq b} $ 或 $ {\displaystyle b\leq a} $。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;有了这几种关系之后，就可以看看全序和偏序关系分别满足以上的哪些关系了：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第20期）：Rust并发安全相关的几个概念(下)</title>
      <link>https://www.codedump.info/zh/post/20220625-weekly-20/</link>
      <pubDate>Sat, 25 Jun 2022 10:00:49 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220625-weekly-20/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本文介绍Rust并发安全相关的几个概念：&lt;code&gt;Send&lt;/code&gt;、&lt;code&gt;Sync&lt;/code&gt;、&lt;code&gt;Arc&lt;/code&gt;，&lt;code&gt;Mutex&lt;/code&gt;、&lt;code&gt;RwLock&lt;/code&gt;等之间的联系。这是其中的下篇，主要介绍&lt;code&gt;Arc&lt;/code&gt;，&lt;code&gt;Mutex&lt;/code&gt;、&lt;code&gt;RwLock&lt;/code&gt;这几个线程安全相关的类型。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;rust并发安全相关的几个概念下&#34;&gt;&#xA;  Rust并发安全相关的几个概念（下）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#rust%e5%b9%b6%e5%8f%91%e5%ae%89%e5%85%a8%e7%9b%b8%e5%85%b3%e7%9a%84%e5%87%a0%e4%b8%aa%e6%a6%82%e5%bf%b5%e4%b8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在&lt;a href=&#34;https://www.codedump.info/post/20220619-weekly-19/&#34;&gt;上一节&lt;/a&gt;中，讲解了&lt;code&gt;Send&lt;/code&gt;和&lt;code&gt;Sync&lt;/code&gt;这两个线程安全相关的&lt;code&gt;trait&lt;/code&gt;，在此基础上展开其它相关类型的讲解。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;rc&#34;&gt;&#xA;  Rc&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#rc&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;Rc&lt;/code&gt;是&lt;code&gt;Reference Counted（引用计数）&lt;/code&gt;的简写，在Rust中，这个数据结构用于实现单线程安全的对指针的引用计数。之所以这个数据结构只是单线程安全，是因为在定义中显式声明了并不实现&lt;code&gt;Send&lt;/code&gt;和&lt;code&gt;Sync&lt;/code&gt;这两个&lt;code&gt;trait&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;#[stable(feature = &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;rust1&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;, since = &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;1.0.0&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;)]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;impl&lt;/span&gt;&amp;lt;T: ?&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Sized&lt;/span&gt;&amp;gt; !marker::&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Send&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;for&lt;/span&gt; Rc&amp;lt;T&amp;gt; {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;#[stable(feature = &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;rust1&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;, since = &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;1.0.0&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;)]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;impl&lt;/span&gt;&amp;lt;T: ?&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Sized&lt;/span&gt;&amp;gt; !marker::&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Sync&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;for&lt;/span&gt; Rc&amp;lt;T&amp;gt; {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;个中原因，是因为&lt;code&gt;Rc&lt;/code&gt;内部的实现中，使用了非原子的引用计数（non-atomic reference counting），因此就不能满足线程安全的条件了。如果要在多线程中使用引用计数，就要使用&lt;code&gt;Arc&lt;/code&gt;这个类型：&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;arc&#34;&gt;&#xA;  Arc&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#arc&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;与&lt;code&gt;Rc&lt;/code&gt;不同的是，&lt;code&gt;Arc&lt;/code&gt;内部使用了原子操作来实现其引用计数，因此&lt;code&gt;Arc&lt;/code&gt;是&lt;code&gt;Atomically Reference Counted（原子引用计数）&lt;/code&gt;的简写，能被使用在多线程环境中，缺陷是原子操作的性能消耗会更大一些。&lt;/p&gt;&#xA;&lt;p&gt;虽然&lt;code&gt;Arc&lt;/code&gt;能被用在多线程环境中，并不意味着&lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;/code&gt;天然就实现了&lt;code&gt;Send&lt;/code&gt;和&lt;code&gt;Sync&lt;/code&gt;，来看看这两部分的声明：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;#[stable(feature = &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;rust1&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;, since = &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;1.0.0&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;)]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;unsafe&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;impl&lt;/span&gt;&amp;lt;T: ?&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Sized&lt;/span&gt; + &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Sync&lt;/span&gt; + &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Send&lt;/span&gt;&amp;gt; &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Send&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;for&lt;/span&gt; Arc&amp;lt;T&amp;gt; {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;#[stable(feature = &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;rust1&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;, since = &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;1.0.0&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;)]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;unsafe&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;impl&lt;/span&gt;&amp;lt;T: ?&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Sized&lt;/span&gt; + &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Sync&lt;/span&gt; + &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Send&lt;/span&gt;&amp;gt; &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Sync&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;for&lt;/span&gt; Arc&amp;lt;T&amp;gt; {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从声明可以看出：一个&lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;/code&gt;类型，当且仅当包裹（wrap）的类型&lt;code&gt;T&lt;/code&gt;满足&lt;code&gt;Sync&lt;/code&gt;和&lt;code&gt;Send&lt;/code&gt;时才能被认为是满足&lt;code&gt;Send&lt;/code&gt;和&lt;code&gt;Sync&lt;/code&gt;的类型。来做一个实验：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;#![feature(negative_impls)]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;use&lt;/span&gt; std::sync::Arc;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;#[derive(Debug)]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Foo&lt;/span&gt; {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;impl&lt;/span&gt; !&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;Send&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;for&lt;/span&gt; Foo {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;main&lt;/span&gt;() {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;let&lt;/span&gt; foo = Arc::new(Foo {});&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    std::thread::spawn(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;move&lt;/span&gt; || {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        dbg!(foo);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在以上的代码中，由于在第8行显示声明了&lt;code&gt;Foo&lt;/code&gt;这个类型不满足&lt;code&gt;Sync&lt;/code&gt;，所以这段代码编译不过，报错信息如下：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第19期）：Rust并发安全相关的几个概念(上)</title>
      <link>https://www.codedump.info/zh/post/20220619-weekly-19/</link>
      <pubDate>Sun, 19 Jun 2022 10:42:40 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220619-weekly-19/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本文介绍Rust并发安全相关的几个概念：&lt;code&gt;Send&lt;/code&gt;、&lt;code&gt;Sync&lt;/code&gt;、&lt;code&gt;Arc&lt;/code&gt;，&lt;code&gt;Mutex&lt;/code&gt;、&lt;code&gt;RwLock&lt;/code&gt;等之间的联系。这是其中的上篇，主要介绍&lt;code&gt;Send&lt;/code&gt;、&lt;code&gt;Sync&lt;/code&gt;这两个&lt;code&gt;trait&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;rust并发安全相关的几个概念上&#34;&gt;&#xA;  Rust并发安全相关的几个概念（上）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#rust%e5%b9%b6%e5%8f%91%e5%ae%89%e5%85%a8%e7%9b%b8%e5%85%b3%e7%9a%84%e5%87%a0%e4%b8%aa%e6%a6%82%e5%bf%b5%e4%b8%8a&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;rust的所有权概念&#34;&gt;&#xA;  Rust的所有权概念&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#rust%e7%9a%84%e6%89%80%e6%9c%89%e6%9d%83%e6%a6%82%e5%bf%b5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在展开介绍并发相关的几个概念之前，有必要先了解一下Rust的所有权概念，Rust对值（value）的所有权有明确的限制：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一个值只能有一个owner。&lt;/li&gt;&#xA;&lt;li&gt;可以同时存在同一个值的多个共享的非可变引用（immutable reference）。&lt;/li&gt;&#xA;&lt;li&gt;但是只能存在一个值的可变引用（mutable reference）。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;比如下面这段代码，user在创建线程之后，被移动（move）到两个不同的线程中：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;fn main() {&#xA;    let user = User { name: &amp;#34;drogus&amp;#34;.to_string() };&#xA;&#xA;    let t1 = spawn(move || {&#xA;        println!(&amp;#34;Hello from the first thread {}&amp;#34;, user.name);&#xA;    });&#xA;&#xA;    let t2 = spawn(move || {&#xA;        println!(&amp;#34;Hello from the second thread {}&amp;#34;, user.name);&#xA;    });&#xA;&#xA;    t1.join().unwrap();&#xA;    t2.join().unwrap();&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;由于&lt;code&gt;一个值只能有一个owner&lt;/code&gt;，所以编译器报错，报错信息如下：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;error[E0382]: use of moved value: `user.name`&#xA;  --&amp;gt; src/main.rs:15:20&#xA;   |&#xA;11 |     let t1 = spawn(move || {&#xA;   |                    ------- value moved into closure here&#xA;12 |         println!(&amp;#34;Hello from the first thread {}&amp;#34;, user.name);&#xA;   |                                                    --------- variable moved due to use in closure&#xA;...&#xA;15 |     let t2 = spawn(move || {&#xA;   |                    ^^^^^^^ value used here after move&#xA;16 |         println!(&amp;#34;Hello from the second thread {}&amp;#34;, user.name);&#xA;   |                                                    --------- use occurs due to use in closure&#xA;   |&#xA;   = note: move occurs because `user.name` has type `String`, which does not implement the `Copy` trait&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;h2 class=&#34;heading&#34; id=&#34;send和sync的约束作用&#34;&gt;&#xA;  Send和Sync的约束作用&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#send%e5%92%8csync%e7%9a%84%e7%ba%a6%e6%9d%9f%e4%bd%9c%e7%94%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;于是，如果一个类型会被多个线程所使用，是需要明确说明其共享属性的。&lt;code&gt;Send&lt;/code&gt;和&lt;code&gt;Sync&lt;/code&gt;这两个&lt;code&gt;trait&lt;/code&gt;作用就在于此，注意到这两个&lt;code&gt;trait&lt;/code&gt;都是&lt;code&gt;std::marker&lt;/code&gt;，实现这两个&lt;code&gt;trait&lt;/code&gt;并不需要对应实现什么方法，可以理解为这两个&lt;code&gt;trait&lt;/code&gt;是类型的&lt;code&gt;约束&lt;/code&gt;，编译器通过这些&lt;code&gt;约束&lt;/code&gt;在编译时对类型进行检查。到目前为止，暂时不展开对两个概念的理解，先来看看两者是如何在类型检查中起&lt;code&gt;约束&lt;/code&gt;作用的。&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第18期）：网状的思考，线性的写作</title>
      <link>https://www.codedump.info/zh/post/20220612-weekly-18/</link>
      <pubDate>Sun, 12 Jun 2022 10:19:47 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220612-weekly-18/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本文介绍我理解的“卡片式笔记法”，以及我的笔记实践、工具等。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;网状的思考线性的写作&#34;&gt;&#xA;  网状的思考，线性的写作&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%bd%91%e7%8a%b6%e7%9a%84%e6%80%9d%e8%80%83%e7%ba%bf%e6%80%a7%e7%9a%84%e5%86%99%e4%bd%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;现实世界中的思考&#34;&gt;&#xA;  现实世界中的思考&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%8e%b0%e5%ae%9e%e4%b8%96%e7%95%8c%e4%b8%ad%e7%9a%84%e6%80%9d%e8%80%83&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;我们的大脑，每时每刻都在进行着一些思考：走在路上、做饭、看书看电影时，等等。&lt;/p&gt;&#xA;&lt;p&gt;而在物理的时空上，肉身在任意时刻只能身处在一个物理意义上的空间里，在时间上也只能处于一个时间点上。&lt;/p&gt;&#xA;&lt;p&gt;可见，思考产生的念头，和时空的限制对比起来，是发散的、不确定的。如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;time-thinking&#34; src=&#34;https://www.codedump.info/media/imgs/20220612-weekly-18/time-thinking.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; time-thinking &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;很多时候，思考并不是凭空而生，可能是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;前几天有过一次对某问题的思考。&lt;/li&gt;&#xA;&lt;li&gt;过了几天，想起来这个问题，又有了另外的考虑和补充。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;当需要记录下来这些想法为笔记时，单篇笔记本身是线型的，只能算是一个一维的创作，这是因为从物理角度来理解一篇“笔记”的话，它在物理上只能存储在一个地方（比如只能是一个文件）。如果按照“一维”的视角，去记录写作的想法，可能就会这样做：某天产生了一个想法，把它记录下来；隔几天有了对这个想法的补充，要么找到前几天的想法记录的地方，继续在这上面补充，也或者另开一篇再记录一次今天的想法。如果这样做，就会得到类似上图那样的效果：不同时空的思考之间，缺少了联系。&lt;/p&gt;&#xA;&lt;p&gt;从这个意义上来说，用只有一个物理维度的“笔记”，来试图存储维度不限的想法、念头，并不适合。这种做法相当于把多维度的想法、念头给降维了。什么时候更适用于这类“降维”的写作行为？我认为是在思考已经很清楚，能够把想法、念头整合在一篇完整的文章时才适合，这时候这篇输出的文章是经过整理、且有完整的观点的。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;article&#34; src=&#34;https://www.codedump.info/media/imgs/20220612-weekly-18/article.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; article &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在更多的时候人是在不同的时空进行不同的思考，这时候就需要另外的工具来整合这些念头，“卡片式笔记法”就适用于这种场景。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;卡片式笔记法&#34;&gt;&#xA;  卡片式笔记法&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8d%a1%e7%89%87%e5%bc%8f%e7%ac%94%e8%ae%b0%e6%b3%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;“卡片式笔记法”中的“卡片”，对应的是前述场景中不同时空下产生的想法、念头。与传统意义上的“笔记”不同的是，“卡片式笔记法”中的记录粒度更细，可以任意想法就能记录在它所谓的“卡片”上。同时，在每个“卡片”赋予一个逻辑上的“地址”，这个&lt;code&gt;逻辑地址&lt;/code&gt;类似于编程中的IP地址、超链接等概念。当有了另外的和这个想法有关联的其他想法时，可以再创建另外的卡片，不同的卡片之间通过&lt;code&gt;逻辑地址&lt;/code&gt;进行关联。同时，为了更好的查找同类的想法，可以使用&lt;code&gt;tag&lt;/code&gt;等方式打上标签，便于搜索、归类。&lt;/p&gt;&#xA;&lt;p&gt;下图是一个典型的“卡片笔记”组成示意图：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Unique Identity：这篇笔记的唯一ID，也就是上述的&lt;code&gt;逻辑地址&lt;/code&gt;，其它笔记可以通过这个唯一的&lt;code&gt;逻辑地址&lt;/code&gt;和这篇笔记发生关联。&lt;/li&gt;&#xA;&lt;li&gt;Tags：这篇笔记的标签。笔记的“物理地址”只能有一个（比如存储在哪个目录的哪个文件里），但是逻辑上可以位于多个标签下，在标签这个维度上可以无限制。这样，多个相同标签的笔记就能发生联系。&lt;/li&gt;&#xA;&lt;li&gt;Links：与这篇笔记相关联的、之前写过的笔记的&lt;code&gt;逻辑地址&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;complete-zettel&#34; src=&#34;https://www.codedump.info/media/imgs/20220612-weekly-18/complete-zettel.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; complete-zettel &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;（出自&lt;a href=&#34;https://zettelkasten.de/introduction/zh/&#34;&gt;卢曼卡片盒笔记法介绍 (Introduction to the Zettelkasten Method) • Zettelkasten Method&lt;/a&gt;）&lt;/p&gt;&#xA;&lt;p&gt;可以看到，“卡片式笔记”与传统笔记相比：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;笔记可以更细粒度，只记录某个时刻的某个具体念头即可，更强调笔记的原子化（Atomicity）。当产生了新的念头之后，并不需要去补充到之前的笔记中，因为笔记是&lt;code&gt;原子化&lt;/code&gt;的，只记录一个瞬间的想法，有了新的念头之后，只需要新建笔记与之前的笔记产生关联。&lt;/li&gt;&#xA;&lt;li&gt;笔记有唯一的&lt;code&gt;逻辑地址&lt;/code&gt;，可以打上不同的tags。&lt;/li&gt;&#xA;&lt;li&gt;笔记与笔记之间，可以通过&lt;code&gt;逻辑地址&lt;/code&gt;、&lt;code&gt;tag&lt;/code&gt;来发生关联。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;有了“卡片式笔记”之后，之前在单一时空维度的想法念头，可能就演变成这样的了：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;connected-thinking&#34; src=&#34;https://www.codedump.info/media/imgs/20220612-weekly-18/connected-thinking.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; connected-thinking &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;可以看到，在“卡片式笔记”的视角下，对某个事情有了思考之后，针对这件事情的思考可能发生在不同的时空里，想法和想法之间互相联系、互为补充，不需要再把它们局限、降维仅仅记录在单篇物理意义上的“笔记”里了。在上图中，时间点C产生的想法C，和想法B、A产生了关联，于是想法C加上对这两条笔记的链接，这样并不需要回头对原有的笔记A、B进行补充，只需要新增笔记C，并且增加链接即可。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;我的笔记实践&#34;&gt;&#xA;  我的笔记实践&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%88%91%e7%9a%84%e7%ac%94%e8%ae%b0%e5%ae%9e%e8%b7%b5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;最后聊一聊我自己怎么记录笔记的，我的笔记分为两个类型：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;碎片化的想法。&lt;/li&gt;&#xA;&lt;li&gt;相对正式的记录。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;碎片化的想法&#34;&gt;&#xA;  碎片化的想法&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%a2%8e%e7%89%87%e5%8c%96%e7%9a%84%e6%83%b3%e6%b3%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;一类是前述提到的一些碎片化的想法，这部分记录在&lt;code&gt;flomo&lt;/code&gt;上。&lt;/p&gt;&#xA;&lt;p&gt;在&lt;code&gt;flomo&lt;/code&gt;上，可以对笔记打标签、还可以通过它所谓的&lt;a href=&#34;https://help.flomoapp.com/advance/thread.html&#34;&gt;🔗 批注连接&lt;/a&gt; 功能给之前的想法做补充，其实这里的&lt;code&gt;批注&lt;/code&gt;就是前面提到的在不同的笔记之间发生关联。&lt;/p&gt;&#xA;&lt;p&gt;使用&lt;code&gt;flomo&lt;/code&gt;记录这些零星想法的好处是：心智负担很低，随手就能记录，不需要在乎格式、是否美观等等，一个工具使用起来的心智负担越低，越能提升做这类事情的频率。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;相对正式的记录&#34;&gt;&#xA;  相对正式的记录&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%9b%b8%e5%af%b9%e6%ad%a3%e5%bc%8f%e7%9a%84%e8%ae%b0%e5%bd%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;另一类是相对更正式的一些记录，比如每天的日志、阅读某些内容之后的笔记等，这部分使用Markdown方式记录在本地的文件中，同时还按照前面的规则，给需要打标签分类的记录打上标签，在需要笔记之间发生关联的时候，切换到&lt;code&gt;obsidian &lt;/code&gt;下面使用&lt;code&gt;[[]]&lt;/code&gt;的方式关联笔记。对&lt;code&gt;obsidian&lt;/code&gt;使用感兴趣的可以看参考资料中推荐的&lt;code&gt;Obsidian&lt;/code&gt;的使用介绍文章。&lt;/p&gt;&#xA;&lt;p&gt;在这篇演示&lt;code&gt;Obsidian&lt;/code&gt;双链效果的文章&lt;a href=&#34;https://publish.obsidian.md/chinesehelp/01&amp;#43;2021%E6%96%B0%E6%95%99%E7%A8%8B/%E5%8F%8C%E9%93%BE%E8%BD%AF%E4%BB%B6&#34;&gt;双链软件 - Obsidian中文教程&lt;/a&gt;中，演示了&lt;code&gt;Obsidian&lt;/code&gt;下双链的效果。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;why-not-logseq&#34;&gt;&#xA;  Why not Logseq？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#why-not-logseq&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;除了&lt;code&gt;Obdisian&lt;/code&gt;之外，国人的作品&lt;code&gt;Logseq&lt;/code&gt;也是一款很好的笔记软件，但是我并没有把它做为自己的主力软件。之前尝试用&lt;code&gt;Logseq&lt;/code&gt;好几次，并不是很习惯，我回想起来，可能更多的原因是：&lt;code&gt;Logseq&lt;/code&gt;对于使用者来说，更淡化了本地文件的存在，而我做为一个更倾向于自托管Markdown本地文件来记录笔记的人，更希望能够清楚知道我的每个笔记存储在哪个位置。所以现在，我的主力Markdown编辑器是&lt;code&gt;Typora&lt;/code&gt;，只有在需要查看双链接、tags的时候才用&lt;code&gt;Obsidian&lt;/code&gt;。&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第17期）：Read-Write Quorum System及在Raft中的实践</title>
      <link>https://www.codedump.info/zh/post/20220528-weekly-17/</link>
      <pubDate>Sat, 28 May 2022 16:16:57 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220528-weekly-17/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：在Paxos、Raft这类一致性算法的描述里，经常会看到&lt;code&gt;Majority&lt;/code&gt;、&lt;code&gt;Quorum&lt;/code&gt;这两个词，在以前我以为都是表达“半数以上”的含义，最近才发现两者有不小的区别。本文介绍这两者的区别，以及在Raft中实践中的问题。有了&lt;code&gt;Quorum&lt;/code&gt;的视角，能更好得理解一致性算法。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;read-write-quorum-system&#34;&gt;&#xA;  Read-Write Quorum System&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#read-write-quorum-system&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;首先来在数学上给出&lt;code&gt;Read-Write Quorum System&lt;/code&gt;的定义。&lt;/p&gt;&#xA;&lt;p&gt;一个&lt;code&gt;Read-Write Quorum System（读写法定系统）&lt;/code&gt;是两个集合组成的元组，即&lt;code&gt;Q=(R,W)&lt;/code&gt;，其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;集合&lt;code&gt;R&lt;/code&gt;被称为&lt;code&gt;Read Quorum（读法定集合）&lt;/code&gt;，即可以认为读操作都是读的集合&lt;code&gt;R&lt;/code&gt;中的元素；&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;集合&lt;code&gt;W&lt;/code&gt;被称为&lt;code&gt;Write Quorum（写法定集合）&lt;/code&gt;，即可以认为写操作都是写入到集合&lt;code&gt;W&lt;/code&gt;中的元素。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;$r∈R,  w∈W,r∩w≠0 $，即任从读集合中取一个成员r，以及任从写集合中取一个成员w，这两个集合一定有交集。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;都知道在分布式系统中，一个写入操作要达成一致，读写操作一定要有一定的冗余度，即：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;写入多份数据成功才能认为写入成功，&lt;/li&gt;&#xA;&lt;li&gt;从多个节点读到同一份数据才认为读取成功。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在&lt;code&gt;Majority&lt;/code&gt;系统中，这个冗余度就是系统内半数以上节点。因为根据&lt;a href=&#34;https://baike.baidu.com/item/%E6%8A%BD%E5%B1%89%E5%8E%9F%E7%90%86/233776&#34;&gt;抽屉原理&lt;/a&gt;，当写入到至少半数以上节点时，读操作与写操作一定有重合的节点。&lt;/p&gt;&#xA;&lt;p&gt;但是在一个&lt;code&gt;Read-Write Quorum System&lt;/code&gt;中，这个条件变的更宽泛了，在这类系统中，只需要满足以下条件即可认为读写成功：&lt;/p&gt;&#xA;&lt;p&gt;$r∈R,  w∈W,r∩w≠0 $&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;用直观的大白话来说：在&lt;code&gt;Read-Write Quorum System&lt;/code&gt;中，只要读、写集合中的任意元素有重合即可。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;我们来详细看看&lt;code&gt;Majority&lt;/code&gt;和&lt;code&gt;Read-Write Quorum System&lt;/code&gt;这两个系统的区别在哪里。&lt;/p&gt;&#xA;&lt;p&gt;首先，&lt;code&gt;Majority&lt;/code&gt;系统并没有区分读、写两类不同的集合，因为在它的视角里，读和写操作都要到半数以上节点才能达到一致。但是在&lt;code&gt;Read-Write Quorum System&lt;/code&gt;系统里，是严格区分了读、写集合的，尽管可能在很多时候，这两类集合是一样的。&lt;/p&gt;&#xA;&lt;p&gt;再次，有了前面严格区分的读、写集合之后，以这个视角来看分布式系统中，一个数据达成一致的大前提是“读、写操作一定有重合的节点”，这样就能保证：写入一个数据到写集合中，最终会被读集合读到。在&lt;code&gt;Majority&lt;/code&gt;系统里，读、写集合都必须是半数以上节点的要求当然能够满足这个条件，但是这个条件太&lt;code&gt;强&lt;/code&gt;了。如果只考虑&lt;code&gt;读、写集合有重合&lt;/code&gt;这个条件，是可以适当放宽而且还不影响系统的一致性的。&lt;/p&gt;&#xA;&lt;p&gt;从以上的讨论，可以得到下面的结论：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;分布式系统中，只要读、写集合有重合，就能保证数据的一致性了。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;Majority&lt;/code&gt;系统是对上述条件的一个强实现，但是存在比这个实现更弱一些的实现，同样能保证数据的一致性。&lt;/li&gt;&#xA;&lt;li&gt;以&lt;code&gt;Read-Write Quorum System&lt;/code&gt;的定义和视角来看，&lt;code&gt;Majority&lt;/code&gt;系统相当于在这两方面强化了&lt;code&gt;Read-Write Quorum System&lt;/code&gt;系统的要求：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;读、写集合完全一样，&lt;/li&gt;&#xA;&lt;li&gt;且都是半数以上节点集合的&lt;code&gt;Read-Write Quorum System&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;即可以认为&lt;code&gt;Majority&lt;/code&gt;系统，只是&lt;code&gt;Read-Write Quorum System&lt;/code&gt;的一个子集。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;quorum&#34; src=&#34;https://www.codedump.info/media/imgs/20220528-weekly-17/quorum.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; quorum &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;讲了这么多，来看一个非&lt;code&gt;Majoiry&lt;/code&gt;的 &lt;code&gt;Read-Write Quorum System&lt;/code&gt;，下面的集合&lt;code&gt;{a,b,c,d,e,f}&lt;/code&gt;组成的网格（grid）被划分成了横竖两个读、写集合：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;grid&#34; src=&#34;https://www.codedump.info/media/imgs/20220528-weekly-17/grid.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; grid &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中，定义了一个&lt;code&gt;Read-Write Quorum System&lt;/code&gt;，&lt;code&gt;Q={{abc}∪{def},{ab}∪{bc}∪{ac}}&lt;/code&gt;，其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;读集合为&lt;code&gt;{abc}∪{def}&lt;/code&gt;，即横着的两个集合&lt;code&gt;{abc}&lt;/code&gt;和&lt;code&gt;{def}&lt;/code&gt;组成了读集合。&lt;/li&gt;&#xA;&lt;li&gt;写集合为&lt;code&gt;{ad}∪{be}∪{cf}&lt;/code&gt;，即竖着的三个集合&lt;code&gt;{ad}&lt;/code&gt;、&lt;code&gt;{be}&lt;/code&gt;、&lt;code&gt;{cf}&lt;/code&gt;组成了写集合。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;显然这个划分是能够满足前面的条件：$r∈R,  w∈W,r∩w≠0 $ 的，因为任选一个读集合中的集合如&lt;code&gt;{abc}&lt;/code&gt;，写集合中任选的一个集合如&lt;code&gt;{ad}&lt;/code&gt;，这两个集合中的元素都会有重合。&lt;/p&gt;&#xA;&lt;p&gt;假设是这样构成的一个分布式系统，那么写操作只需要写入写集合中的任意一个集合即可认为成功，可以看到一个写集合最小可以只有两个节点构成，这个数量是小于&lt;code&gt;Majority&lt;/code&gt;的。&lt;/p&gt;&#xA;&lt;p&gt;有了对&lt;code&gt;Read-Write Quorum System&lt;/code&gt;系统及与&lt;code&gt;Majority&lt;/code&gt;的区分和联系，以这个视角来看看raft的成员变更算法。&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第16期）：图解ARIES论文（下）</title>
      <link>https://www.codedump.info/zh/post/20220521-weekly-16/</link>
      <pubDate>Sat, 21 May 2022 11:46:44 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220521-weekly-16/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：ARIES(Algorithm for Recovery and Isolation Exploiting Semantics的简称）是论文&lt;a href=&#34;https://cs.stanford.edu/people/chrismre/cs345/rl/aries.pdf&#34;&gt;《ARIES: A Transaction Recovery Method Supporting Fine-Franularity Locking and Partial Rollbacks Using Write-Ahead Logging》&lt;/a&gt;中提到的一种存储引擎中数据恢复的算法。这篇论文可以说是存储引擎数据恢复领域必读的一篇论文，这两期的周刊就是对这篇论文的图解，这是其中的下篇。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;图解aries论文下&#34;&gt;&#xA;  图解ARIES论文（下）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9b%be%e8%a7%a3aries%e8%ae%ba%e6%96%87%e4%b8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;前情回顾&#34;&gt;&#xA;  前情回顾&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%89%8d%e6%83%85%e5%9b%9e%e9%a1%be&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在&lt;a href=&#34;https://www.codedump.info/post/20220514-weekly-15/&#34;&gt;周刊（第15期）：图解ARIES论文（上）&lt;/a&gt;中，讨论了存储引擎面临的问题，如果存储引擎宕机重启，将要进行以下两个操作：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;撤销（Undo）：未完成或者由于各种原因发生回滚（rollback）、中断（abort）的事务，其修改需要被撤销，即回滚为事务之前的旧值。&lt;/li&gt;&#xA;&lt;li&gt;重做（Redo）：已经提交的事务，其修改操作的效果需要体现为新值。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;为了这两个操作，存储引擎就需要回答这两个问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;“是否允许未提交事务的修改在持久化存储上生效”（Whether the DBMS allows an uncommitted txn to overwrite the most recent committed value of an object in non-volatile storage），被称为&lt;code&gt;Steal policy&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;一个事务在提交之前是否需要将所有修改同步到持久化存储上（Whether the DBMS requires that all updates made by a txn are reflected on non-volatile storage before the txn is allowed to commit.），称为&lt;code&gt;force policy&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;两个问题合并起来一共有四种组合：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第15期）：图解ARIES论文（上）</title>
      <link>https://www.codedump.info/zh/post/20220514-weekly-15/</link>
      <pubDate>Sat, 14 May 2022 06:33:26 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220514-weekly-15/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：ARIES(Algorithm for Recovery and Isolation Exploiting Semantics的简称）是论文&lt;a href=&#34;https://cs.stanford.edu/people/chrismre/cs345/rl/aries.pdf&#34;&gt;《ARIES: A Transaction Recovery Method Supporting Fine-Franularity Locking and Partial Rollbacks Using Write-Ahead Logging》&lt;/a&gt;中提到的一种存储引擎中数据恢复的算法。这篇论文可以说是存储引擎数据恢复领域必读的一篇论文，这两期的周刊就是对这篇论文的图解，这是其中的上篇。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;图解aries论文上&#34;&gt;&#xA;  图解ARIES论文（上）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9b%be%e8%a7%a3aries%e8%ae%ba%e6%96%87%e4%b8%8a&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在展开解释ARIES算法原理之前，需要对&lt;a href=&#34;https://www.codedump.info/post/20220410-weekly-12/&#34;&gt;Page oriented类存储引擎&lt;/a&gt;的日志系统有一定的了解，才能继续解释基于这个日志系统之上做的恢复算法。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;问题&#34;&gt;&#xA;  问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在一个存储系统中，出错是非常常见的情况的，这就涉及到出错了之后系统恢复时还需要能继续工作，即数据不能发生破坏导致整个系统跑不起来。&lt;/p&gt;&#xA;&lt;p&gt;于是，当系统出错需要重启恢复时，就涉及到以下两个动作：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;撤销（Undo）：未完成或者由于各种原因发生回滚（rollback）、中断（abort）的事务，其修改需要被撤销，即回滚为事务之前的旧值。&lt;/li&gt;&#xA;&lt;li&gt;重做（Redo）：已经提交的事务，其修改操作的效果需要体现为新值。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;来看下图中提出的问题：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;bufferpool&#34; src=&#34;https://www.codedump.info/media/imgs/20220514-weekly-15/bufferpool.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; bufferpool &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;存在事务T1和T2在同时执行：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;事务T1：修改A值为3，但是在事务还未提交前，事务T2开始执行。&lt;/li&gt;&#xA;&lt;li&gt;事务T2：修改B值为8，并且成功提交。&lt;/li&gt;&#xA;&lt;li&gt;事务T1终止：在事务T2成功提交之后，事务T1终止。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这个事务调度的执行顺序引发了以下几个问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;回滚未提交的事务T1需要做什么？&lt;/li&gt;&#xA;&lt;li&gt;对于未提交的事务T1，是否允许其修改操作在持久化存储上生效（即将A修改为3）？&lt;/li&gt;&#xA;&lt;li&gt;在磁盘的数据库文件中，已成功提交的事务T2，其修改操作是否应该立即落盘（即从buffer pool中同步修改的内容到硬盘）。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;第一个问题当前暂且放到一边，来看后面两个问题。&lt;/p&gt;&#xA;&lt;p&gt;“是否允许未提交事务的修改在持久化存储上生效”（Whether the DBMS allows an uncommitted txn to overwrite the most recent committed value of an object in non-volatile storage），被称为&lt;code&gt;Steal policy&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;steal：允许未提交事务的修改持久化存储上生效。&lt;/li&gt;&#xA;&lt;li&gt;no steal：反之。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;一个事务在提交之前是否需要将所有修改同步到持久化存储上（Whether the DBMS requires that all updates made by a txn are reflected on non-volatile storage before the txn is allowed to commit.），也有两种策略：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第14期）：重读Raft论文中的集群成员变更算法（二）：实践篇</title>
      <link>https://www.codedump.info/zh/post/20220507-weekly-14/</link>
      <pubDate>Sat, 07 May 2022 17:57:08 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220507-weekly-14/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：以前阅读Raft大论文的时候，对“集群变更”这部分内容似懂非懂。于是最近又重读了大论文这部分的内容，以下是重读时做的一些记录。这部分内容打算分为两篇文章，上篇讲解成员变更流程的理论基础，下篇讲解实践中存在的问题。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;重读raft论文中的集群成员变更算法二实践篇&#34;&gt;&#xA;  重读Raft论文中的集群成员变更算法（二）：实践篇&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%87%8d%e8%af%bbraft%e8%ae%ba%e6%96%87%e4%b8%ad%e7%9a%84%e9%9b%86%e7%be%a4%e6%88%90%e5%91%98%e5%8f%98%e6%9b%b4%e7%ae%97%e6%b3%95%e4%ba%8c%e5%ae%9e%e8%b7%b5%e7%af%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;单步成员变更存在的问题&#34;&gt;&#xA;  单步成员变更存在的问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8d%95%e6%ad%a5%e6%88%90%e5%91%98%e5%8f%98%e6%9b%b4%e5%ad%98%e5%9c%a8%e7%9a%84%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;正确性问题&#34;&gt;&#xA;  正确性问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%ad%a3%e7%a1%ae%e6%80%a7%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;单步变更成员时，可能出现正确性问题。如下面的例子所示，最开始时，系统的成员是&lt;code&gt;{a,b,c,d}&lt;/code&gt;这四个节点的集合，要将节点&lt;code&gt;u&lt;/code&gt;和&lt;code&gt;v&lt;/code&gt;加入集群，按照单步变更成员的做法，依次会经历：&lt;code&gt;{a,b,c,d}&lt;/code&gt;-&amp;gt;&lt;code&gt;{a,b,c,d,u}&lt;/code&gt;-&amp;gt;&lt;code&gt;{a,b,c,d,u,v}&lt;/code&gt;的变化，每次将一个节点加入到集群里。&lt;/p&gt;&#xA;&lt;p&gt;上面的步骤看起来很美好，但是考虑下面的例子，在变更过程中leader节点发生了变化的情况：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;C₀ = {a, b, c, d}&#xA;Cᵤ = C₁ ∪ {u}&#xA;Cᵥ = C₁ ∪ {v}&#xA;&#xA;Lᵢ: Leader in term `i`&#xA;Fᵢ: Follower in term `i`&#xA;☒ : crash&#xA;&#xA;    |&#xA; u  |         Cᵤ                  F₂  Cᵤ&#xA;--- | ----------------------------------&#xA; a  | C₀  L₀  Cᵤ  ☒               L₂  Cᵤ&#xA; b  | C₀  F₀          F₁          F₂  Cᵤ&#xA; c  | C₀  F₀          F₁  Cᵥ          Cᵤ&#xA; d  | C₀              L₁  Cᵥ  ☒       Cᵤ&#xA;--- | ----------------------------------&#xA; v  |                     Cᵥ                  time&#xA;    +--------------------------------------------&amp;gt;&#xA;          t₁  t₂  t₃  t₄  t₅  t₆  t₇  t₈&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;（引用自&lt;a href=&#34;https://blog.openacid.com/distributed/raft-bug/&#34;&gt;TiDB 在 Raft 成员变更上踩的坑 - OpenACID Blog&lt;/a&gt;）&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第13期）：重读Raft论文中的集群成员变更算法（一）：理论篇</title>
      <link>https://www.codedump.info/zh/post/20220417-weekly-13/</link>
      <pubDate>Sun, 17 Apr 2022 15:16:30 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220417-weekly-13/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：以前阅读Raft大论文的时候，对“集群变更”这部分内容似懂非懂。于是最近又重读了大论文这部分的内容，以下是重读时做的一些记录。这部分内容打算分为两篇文章，上篇讲解成员变更流程的理论基础，下篇讲解实践中存在的问题。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;重读raft论文中的集群成员变更算法一理论篇&#34;&gt;&#xA;  重读Raft论文中的集群成员变更算法（一）：理论篇&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%87%8d%e8%af%bbraft%e8%ae%ba%e6%96%87%e4%b8%ad%e7%9a%84%e9%9b%86%e7%be%a4%e6%88%90%e5%91%98%e5%8f%98%e6%9b%b4%e7%ae%97%e6%b3%95%e4%b8%80%e7%90%86%e8%ae%ba%e7%af%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;“集群成员变更（cluster membership change）”意指一个集群内节点的增、删操作，这在一个分布式系统中是必不可少的操作，因为并不能保证一个集群的所有节点都一直能工作的很好。Raft大论文《&lt;a href=&#34;https://web.stanford.edu/~ouster/cgi-bin/papers/OngaroPhD.pdf&#34;&gt;Consensus: Bridging Theory and Practice&lt;/a&gt;》中有专门的一节来讲解这部分内容。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;安全性&#34;&gt;&#xA;  安全性&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%89%e5%85%a8%e6%80%a7&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;首先，Raft算法中要求所有操作都需要保证安全性（safety），即：任何时候都不能在集群中同时存在两个leader节点。“集群成员变更”算法也必须保证安全性这个大前提不能被破坏，于是论文中阐述了为什么直接变更多个节点是不被允许的：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;4.2&#34; src=&#34;https://www.codedump.info/media/imgs/20220417-weekly-13/4.2.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 4.2 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图的图示中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;旧集群有1、2、3这三个节点，而需要将这个三节点的集群新增节点4、5变更到5节点集群去。&lt;/li&gt;&#xA;&lt;li&gt;如果直接如图中这样变更，由于每个节点的时间窗口并不一致，可能就会出现这种情况：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;在某一时刻，节点1、2还使用的是旧集群（只含有{1,2,3}）的成员配置，而3、4、5已经是新集群（含有{1,2,3,4,5}）的成员配置了。&lt;/li&gt;&#xA;&lt;li&gt;这样就可能出现还使用旧集群节点配置的1、2选出了一个leader，以及已经使用了新集群配置的节点3、4、5选出了另一个leader的情况，于是违反了上面阐述的“安全性”要求。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要说明的是，在上面这个错误的示例中，是由于有两类行为同时出现才导致的错误：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一次性变更多个节点。在例子中，就是一次性把4、5两个节点加入到集群中。&lt;/li&gt;&#xA;&lt;li&gt;直接（directly）变更。直接变更由于集群中不同节点的步子不一样，而不一样的节点如果出现了两个不同的集群，那么就可能导致选出两个不同的leader。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;cluster-membership-change&#34; src=&#34;https://www.codedump.info/media/imgs/20220417-weekly-13/cluster-membership-change.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; cluster-membership-change &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;于是，由于这两个错误操作是一起发生才会导致错误，论文中给出了两种方案：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;要么一次性严格限制只变更一个节点。&lt;/li&gt;&#xA;&lt;li&gt;如果实在想一次变更多个节点，那就不能直接变更，需要经过一个中间状态的过渡之后才能完成同时变更多个节点的操作。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下分别来阐述这两种不同的实现。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;一次变更单个节点&#34;&gt;&#xA;  一次变更单个节点&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%80%e6%ac%a1%e5%8f%98%e6%9b%b4%e5%8d%95%e4%b8%aa%e8%8a%82%e7%82%b9&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;如果限制每次只变更一个节点，那么就能保证“新、旧集合的quorum集合是有重合的”，由于有重合，这样就能保证新旧两个集群的集合不会选出不同的leader，就能间接保证安全性。&lt;/p&gt;&#xA;&lt;p&gt;论文中以下面几个例子来说明这样操作的正确性：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;4.3&#34; src=&#34;https://www.codedump.info/media/imgs/20220417-weekly-13/4.3.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 4.3 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;这几个图，是在两个维度上做示范的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;增、删操作。&lt;/li&gt;&#xA;&lt;li&gt;原集群节点数量是奇数还是偶数。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;两个维度的组合一共就是上面的4中情况，但是无论哪一种情况，由于都保证了“新、旧集合的quorum集合是有重合的”这个条件，于是不会选出不一样的leader来。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;一次变更多个节点&#34;&gt;&#xA;  一次变更多个节点&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%80%e6%ac%a1%e5%8f%98%e6%9b%b4%e5%a4%9a%e4%b8%aa%e8%8a%82%e7%82%b9&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;从上面的例子中可以看到：只要能保证一次只变更一个节点，是可以直接（directly）变更的。即：无需中间状态，直接从A集合变更到A+1集合，因为这两个集合的quorum肯定有重合。&lt;/p&gt;&#xA;&lt;p&gt;但是，在一次需要变更多个节点的情况下，就不能这样直接变更，因为会出现最开始示例的那样同时选出两个leader的情况。于是，为了解决这个问题，需要引入一个中间状态：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;假设原先的集群节点集合为C_Old，新的集群节点集合为C_New，那么首先变更配置到{C_Old,C_New}，也就是新旧集群节点集合的并集。&lt;/li&gt;&#xA;&lt;li&gt;上面这次变更提交之后，再向集群变更配置到C_New。在这次变更提交之后，那些不在C_New节点集合中的节点，收到这个变更时，自动下线退出集群。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以证明：上面两个步骤中，都不会出现“同时存在两个leader”的情况。&lt;/p&gt;&#xA;&lt;p&gt;从本质上来说，这种变更算法，属于一种两阶段的成员变更算法，Raft大论文中称之为“Joint Consensus（联合共识）”算法。下图中演示了Joint Consensus算法这两个阶段的流程：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;4.8&#34; src=&#34;https://www.codedump.info/media/imgs/20220417-weekly-13/4.8.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 4.8 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;failover&#34;&gt;&#xA;  Failover&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#failover&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;我们来看看Joint Consensus算法，在变更过程中如果出错，是如何failover选出新leader的。&lt;/p&gt;&#xA;&lt;p&gt;第一阶段，这时候选出来的leader只有可能有两种情况，还是旧的C_Old节点集合，或者已经收到了{C_Old,C_New}节点集合：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;只有C_Old节点集合的节点：由于这时候这个leader并没有第一阶段提交的{C_Old,C_New}节点集合变更，因此那些已有{C_Old,C_New}节点集合的follower这部分的日志将被截断，成员变更失败，回退回C_Old集合。&lt;/li&gt;&#xA;&lt;li&gt;有{C_Old,C_New}节点集合的节点：这意味这个leader已经有第一阶段提交的{C_Old,C_New}节点集合变更，可以继续将未完成的成员变更流程走完。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;类似的，也可以去推导一下在第二阶段出现leader宕机时，选出来的leader只可能具备两种情况，但是这两种情况都不可能选出多个leader。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;集群变更何时生效&#34;&gt;&#xA;  集群变更何时生效？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%9b%86%e7%be%a4%e5%8f%98%e6%9b%b4%e4%bd%95%e6%97%b6%e7%94%9f%e6%95%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;以上讲解完毕两种不同的集群变更方式，下面来聊一聊集群变更何时生效。&lt;/p&gt;&#xA;&lt;p&gt;在Raft、Paxos这类状态机模型的一致性算法中，将任何变更操作都认为是一个命令（Command），命令的处理流程是这样的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;状态机收到命令，首先在自己本地将命令持久化。&lt;/li&gt;&#xA;&lt;li&gt;然后广播给集群中的其他节点。&lt;/li&gt;&#xA;&lt;li&gt;当收到集群半数以上节点的应答时，认为命令是可以被提交（commit）的，于是可以生效将这些已经被提交的日志传给应用层的状态机使用了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以上流程可以看到：一条命令，只有在“提交（commit）”之后才能“生效（apply）”。&lt;/p&gt;&#xA;&lt;p&gt;在Raft中，“成员变更”这个操作，也是一类命令，即：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第12期）：Page oriented类存储引擎里可能同时存在多种结构</title>
      <link>https://www.codedump.info/zh/post/20220410-weekly-12/</link>
      <pubDate>Sun, 10 Apr 2022 11:38:16 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220410-weekly-12/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本期聊一聊Page oriented类存储引擎内的数据结构组织。在满足“向磁盘读写的基本单位是物理页面”这个大前提下，这类存储引擎的可能同时存在多种结构。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;page-oriented类存储引擎里可能同时存在多种树形结构&#34;&gt;&#xA;  page oriented类存储引擎里可能同时存在多种树形结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#page-oriented%e7%b1%bb%e5%ad%98%e5%82%a8%e5%bc%95%e6%93%8e%e9%87%8c%e5%8f%af%e8%83%bd%e5%90%8c%e6%97%b6%e5%ad%98%e5%9c%a8%e5%a4%9a%e7%a7%8d%e6%a0%91%e5%bd%a2%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;存储引擎的分类&#34;&gt;&#xA;  存储引擎的分类&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ad%98%e5%82%a8%e5%bc%95%e6%93%8e%e7%9a%84%e5%88%86%e7%b1%bb&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;目前接触到的存储引擎，以向磁盘读写方式来分类的话，大体可以分为两类：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;LSM-Tree结构。&lt;/li&gt;&#xA;&lt;li&gt;Page oriented类。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;LSM-Tree是“Log-Structured Merge-Tree”的简称，这类存储引擎写入一条数据的流程大体如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;向内存以及WAL日志中写入完成，即可认为写入成功。&lt;/li&gt;&#xA;&lt;li&gt;内存中的数据写满之后，将落盘到所谓的sstable中。&lt;/li&gt;&#xA;&lt;li&gt;sstable分为多层，随着写入进行，不同层次的sstable数据将进行合并。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;LSM&#34; src=&#34;https://www.codedump.info/media/imgs/20220410-weekly-12/LSM.jpeg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; LSM &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;（图片引用自&lt;a href=&#34;https://zhuanlan.zhihu.com/p/181498475&#34;&gt;LSM树详解 - 知乎&lt;/a&gt;)&lt;/p&gt;&#xA;&lt;p&gt;从上面简单的写入LSM的流程可以看到：无论是写入内存还是磁盘，这类存储引擎在写入新数据时（不是合并sstable流程），磁盘操作的单位是一条记录。而一条记录的长度，是不定长的。&lt;/p&gt;&#xA;&lt;p&gt;与LSM-Tree类的结构不同的是，Page oriented类的存储引擎，向磁盘发起读写操作的基本单位是页面（page），一个页面通常的大小是2的次方，最小一般是1024字节，比如sqlite的存储，其页面大小为4K（可以修改编译选项配置页面大小）。&lt;/p&gt;&#xA;&lt;p&gt;以一个物理页面为读写磁盘的基本单位，这也是这一类存储引擎之所以被称为”Page oriented类存储引擎“的原因。本文重点是介绍Page oriented类存储引擎的结构。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;page-oriented存储引擎的结构&#34;&gt;&#xA;  Page oriented存储引擎的结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#page-oriented%e5%ad%98%e5%82%a8%e5%bc%95%e6%93%8e%e7%9a%84%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;还是以之前介绍过的sqlite的架构图来开头：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;btree架构&#34; src=&#34;https://www.codedump.info/media/imgs/20211217-sqlite-btree-0/btree-arch.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; btree架构 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;这个架构由下往上依次是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;系统层：提供不同系统级API的封装，比如文件读写、加解锁操作等。&lt;/li&gt;&#xA;&lt;li&gt;物理页面管理层：提供物理页面读写、缓存等功能。&lt;/li&gt;&#xA;&lt;li&gt;树形结构的实现：根据具体的树形算法，组织物理页面之间的逻辑关系（比如父子页面、兄弟页面），以及单个物理页面之内的数据的组织。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这里的重点是页面管理层和树形结构的实现这两部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;物理页面管理相当于是磁盘文件的”原材料供应商“，负责对它的客户也就是各种不同结构的实现提供物理页面这一”原材料“的读写、缓存管理，而它对这些材料被客户拿去做成了什么，一无所知。&lt;/li&gt;&#xA;&lt;li&gt;树形结构的实现，从页面管理器拿到了”物理页面“这个原材料之后，可以按照自己的算法和数据结构任意塑造成任何合理的结构。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;数据库文件的物理页面组织和逻辑页面结构&#34; src=&#34;https://www.codedump.info/media/imgs/20220201-sqlite-btree-5-btree/database-file.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 数据库文件的物理页面组织和逻辑页面结构 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;可以看到，Page oriented存储引擎，在满足“向磁盘读写的基本单位是物理页面”这个大前提下，这类存储引擎的可能同时存在多种结构：可能只有B-Tree，也可能只有B+Tree。还有另一种情况是：这类存储引擎内部同时存在多种结构。&lt;/p&gt;&#xA;&lt;p&gt;以sqlite为例，内部其实就存在两种结构：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;存储索引的index tree：结构为B-Tree，键为表索引，值为这一行数据的&lt;code&gt;rowid&lt;/code&gt;，其中&lt;code&gt;rowid&lt;/code&gt;为隐藏列，创建数据表时自动生成，这一列是自增整数。&lt;/li&gt;&#xA;&lt;li&gt;存储数据的table tree：结构为B+Tree，键为&lt;code&gt;rowid&lt;/code&gt;，值为一行数据。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这两类存储引擎，由于同属于“Page oriented类存储引擎”，因此可以共用同一个物理页面管理器。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;数据库文件的rowid全量数据表和索引表&#34; src=&#34;https://www.codedump.info/media/imgs/20220201-sqlite-btree-5-btree/btree-rowid.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 数据库文件的rowid全量数据表和索引表 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;下面，以sqlite中的一个表为例来解释上面这个流程。&lt;/p&gt;&#xA;&lt;p&gt;首先，创建一个表以及索引：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;// &lt;span style=&#34;&#34;&gt;创建数据库&lt;/span&gt;COMPANY&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;TABLE&lt;/span&gt; COMPANY(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ID             &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;INT&lt;/span&gt;      &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;NULL&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   NAME           &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;TEXT&lt;/span&gt;    &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;NULL&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   AGE            &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;INT&lt;/span&gt;     &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;NULL&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;// &lt;span style=&#34;&#34;&gt;创建索引&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;INDEX&lt;/span&gt; id_index &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ON&lt;/span&gt; COMPANY (id);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上面这个建表以及创建索引之后，对应的在这个数据文件中就有了两个树形结构：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第11期）：mmap适用于存储引擎吗？</title>
      <link>https://www.codedump.info/zh/post/20220327-weekly-11/</link>
      <pubDate>Sun, 27 Mar 2022 15:06:38 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220327-weekly-11/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本期聊一聊mmap技术在存储引擎中的应用。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;mmap适用于存储引擎吗&#34;&gt;&#xA;  mmap适用于存储引擎吗？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#mmap%e9%80%82%e7%94%a8%e4%ba%8e%e5%ad%98%e5%82%a8%e5%bc%95%e6%93%8e%e5%90%97&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;想写这篇文章，主要源于两篇文章（论文）中的对mmap在存储引擎中使用的两种截然不同的观点讨论：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;反方（mmap不应该用于存储引擎）：&lt;a href=&#34;https://db.cs.cmu.edu/mmap-cidr2022/&#34;&gt;Are You Sure You Want to Use MMAP in Your Database Management System? (CIDR 2022)&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;正方（mmap可以用于存储引擎）：&lt;a href=&#34;https://ayende.com/blog/196161-C/re-are-you-sure-you-want-to-use-mmap-in-your-database-management-system&#34;&gt;re: Are You Sure You Want to Use MMAP in Your Database Management System? - Ayende @ Rahien&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;由于刚好看过这两种方式的btree存储引擎：sqlite的btree实现以及boltdb，所以可以结合我的认知来聊一聊这个问题。这两个存储引擎的实现都已经整理成了系列博客，这两个系列的第一篇分别是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200625-boltdb-1/&#34;&gt;boltdb 1.3.0实现分析（一） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;先来看看一个存储引擎实现时的大体分层，以sqlite为例分为三层：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;btree架构&#34; src=&#34;https://www.codedump.info/media/imgs/20211217-sqlite-btree-0/btree-arch.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; btree架构 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;自下而上，这三个层次分别是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;os层：封装系统级API实现文件的读写等操作。&lt;/li&gt;&#xA;&lt;li&gt;页面管理层：提供以页面为单位的读、写、加载、缓存等操作。&lt;/li&gt;&#xA;&lt;li&gt;btree实现：btree以物理页面为单位向下一层的页面管理层来读写页面，而物理页面内部的逻辑组织（比如父子关系），以及页面内的数据组织（比如一个页面中管理的数据）由这一层负责。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以这样来简单区别理解“页面管理”模块和btree模块的功能：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;页面管理：顾名思义，页面管理模块的最基本单位是”页面“，页面的读、写、缓存、落盘、恢复、回滚等，都由页面模块负责。上一层依赖页面管理模块的btree模块，不需要关心一个页面何时缓存、何时落盘等细节。即：&lt;strong&gt;页面模块负责页面的物理级别的操作&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;btree：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;负责按照btree算法，来组织页面，即负责的是页面之间逻辑关系维护。&lt;/li&gt;&#xA;&lt;li&gt;除此以外，一个页面内部的数据的物理、逻辑组织，也是btree模块来负责的。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;即：&lt;strong&gt;btree负责维护页面间的逻辑关系，以及一个页面内数据的组织。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;以页面物理、逻辑关系的维护看模块划分&#34; src=&#34;https://www.codedump.info/media/imgs/20211217-sqlite-btree-0/page-module.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 以页面物理、逻辑关系的维护看模块划分 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在数据库文件中，通常按照页面为单位来划分文件，比如sqlite一般是4KB大小为一个物理页面，所以一个数据库文件可以看做是一个大的“物理页面数组”，这样的话每个物理页面都有一个对应的编号（从1开始），这个编号通常简称为PID（page id）。&lt;/p&gt;&#xA;&lt;p&gt;从上面的功能划分可以看到，“页面管理器（也被称为“buffer pool）”的功能是非常复杂的，这里列举几个最关键的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;读页面：上层的btree要读一个数据库文件中的页面时，通常传入一个PID，由页面管理器去加载这个页面的数据。而页面数据并不是每次都会到数据库文件中一次磁盘IO读出来，也很可能在内存中，此时就不需要读磁盘操作了。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;写页面：当一个页面被修改后，就被称为“脏页面（dirty page）”，需要落盘；但并不是每一次修改了一个页面的内容之后就马上落盘，其原因在于：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一次写事务可能修改了不止一个页面，需要以事务为单位去落盘脏页面。&lt;/li&gt;&#xA;&lt;li&gt;即便是落盘脏页面，由于涉及到写磁盘操作，所以还会用其他方式减少写磁盘的次数。比如sqlite的wal备份文件机制中，脏页面的内容是首先写入wal文件的，由于写wal文件是一次append操作而不是随机写，所以效率会更高，如果一个脏页面的内容被写入wal文件的话，那么这部分页面内容是不急于马上写入数据库文件的。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;缓存页面：由于页面缓存的功能，所以还需要一个页面缓存管理的功能，主要负责：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第10期）：“忘记目标，专注于体系”</title>
      <link>https://www.codedump.info/zh/post/20220319-weekly-10/</link>
      <pubDate>Sat, 19 Mar 2022 13:50:46 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220319-weekly-10/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本期聊一聊《掌控习惯》这本书里提到的养成习惯的方法论。我读下来一个最深的感受是：越不需要“坚持”就能做下去的事情，才越能长久做下去。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;忘记目标专注于体系&#34;&gt;&#xA;  “忘记目标，专注于体系”&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%bf%98%e8%ae%b0%e7%9b%ae%e6%a0%87%e4%b8%93%e6%b3%a8%e4%ba%8e%e4%bd%93%e7%b3%bb&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;“忘记目标，专注于体系（Forget About Setting Goals, Focus on the system Instead）”是出自《&lt;a href=&#34;https://jamesclear.com/atomic-habits&#34;&gt;Atomic Habits&lt;/a&gt;》（中文名&lt;a href=&#34;https://book.douban.com/subject/34326931//&#34;&gt;《掌控习惯 》&lt;/a&gt;）一书的一句话，个人认为这是本书最重要的一个观点。&lt;/p&gt;&#xA;&lt;p&gt;要做成一件事情，只是依靠简单的设定目标，是远远不够的，更重要的是建立一套体系，促进事情的良性循环，最终让事情发生；反之，如果一件不好的事情，经常重蹈覆辙，其根本原因也是从未改变导致这种状况一再发生的体系。如果没有建立一套体系，所做的一切只是治标不治本。&lt;/p&gt;&#xA;&lt;p&gt;这里说的“体系”，在作者的话语中就是“习惯”，作者列出了培养良好习惯的四个步骤（提示、渴求、反应和奖励）：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;1、提示：让它显而易见。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;2、渴求：让它有吸引力。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;3、反应：让它简便易行。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;4、奖励：让它令人愉悦。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;作者将这四个步骤，总结在&lt;code&gt;习惯循环（habit-loop）&lt;/code&gt;里，如下图：&lt;/p&gt;&#xA;&lt;div align=&#34;center&#34;&gt;&#xA; &lt;img src=&#34;https://www.codedump.info/media/imgs/20220319-weekly-10/habit-cycle.png&#34; width=&#34;400&#34; height=&#34;200&#34; alt=&#34;习惯循环&#34; align=center /&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;这四个步骤缺一不可，任何一个环节做的不够的话，就难以形成习惯：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;消除提示，习惯形成循环永远也不会开始。&lt;/li&gt;&#xA;&lt;li&gt;降低渴求，就不会有足够的动力去行动。&lt;/li&gt;&#xA;&lt;li&gt;让行动变得困难，将无法付诸实施。&lt;/li&gt;&#xA;&lt;li&gt;如果奖励不能满足欲望，那么未来就没有理由再这样做了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;没有前三步，就不会有任何行为发生。没有这四个步骤，任何一种行为都不会得以重复。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;1提示让它显而易见&#34;&gt;&#xA;  1、提示：让它显而易见&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#1%e6%8f%90%e7%a4%ba%e8%ae%a9%e5%ae%83%e6%98%be%e8%80%8c%e6%98%93%e8%a7%81&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在日常行为中，“原动力经常被高估，而环境的作用往往被低估”，比如经常会认为树立一个远大的目标，坚持做下去就好；而现实的情况是，行为是环境中人的函数：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;B（行为）=f（函数）[P（人），E（环境）]&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;即有怎样的行为，除了和人有关系之外，还和环境有重要的关系。&lt;/p&gt;&#xA;&lt;p&gt;环境函数中，常见的两个提示是时间和地点。比如&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;同样摆放在超市的东西，更容易被接触到、更显眼的商品，被购买的概率会更高。&lt;/li&gt;&#xA;&lt;li&gt;据书中列举的越战士兵的统计显示，在离开越南战场之后，毒瘾戒除率大大提高，因为生活环境发生了彻底的改变。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这一定理给的提示是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;假如不需要经常自我克制的话，做起来就会更容易。所以，没错，毅力、勇气和意志力是取得成功的要素，但是增强这些品质的途径不是期望你自己成为一个自律的人，而是创造一个有纪律的环境。&lt;/li&gt;&#xA;&lt;li&gt;消除坏习惯的最实用的方法之一是避免接触引起它的提示，比如要少玩手机就想办法远离手机；反过来，想养成好习惯比如多喝水，就把水杯放在易接触的位置。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;2渴求让它有吸引力&#34;&gt;&#xA;  2、渴求：让它有吸引力&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#2%e6%b8%b4%e6%b1%82%e8%ae%a9%e5%ae%83%e6%9c%89%e5%90%b8%e5%bc%95%e5%8a%9b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;习惯是多巴胺驱动的反馈回路。每一种极可能形成习惯的行为——吃垃圾食品、玩电子游戏、浏览社交媒体——都与较高浓度的多巴胺有关。每当你预测一个机会会有回报时，你体内的多巴胺浓度就会随着这种预期飙升。当你获得奖励时，大脑中激活的奖励系统，与你期待奖励时激活的系统是同一个。这就是对一种体验的期待往往比体验本身，更令人感到愉悦的原因之一。&lt;/p&gt;&#xA;&lt;p&gt;这就引出了“喜好绑定”的原理：把你需要做的事与愿意做的事绑定。比如想培养健身的习惯，那么可以让自己在健身的时候同时看喜欢看的视频做为奖励。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;3反应让它简便易行&#34;&gt;&#xA;  3、反应：让它简便易行&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#3%e5%8f%8d%e5%ba%94%e8%ae%a9%e5%ae%83%e7%ae%80%e4%be%bf%e6%98%93%e8%a1%8c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;《精益求精》一书里举过这样的例子：“日本公司强调为人所知的‘精益生产’理念，坚持不懈地努力寻求从生产流程中去除各种浪费，直至重新设计工作环境，使得工人们的身体不必转来转去，从而避免为拿工具而浪费时间。结果是日本工厂比美国工厂效率更高，产品更可靠。“&lt;/p&gt;&#xA;&lt;p&gt;作者将这样的策略称为”因减而加“：寻找生产线上的每一个阻力点，予以清除。这样减少了无用功，反过来就增加了效率。&lt;/p&gt;&#xA;&lt;p&gt;与之类似的，有”两分钟法则”：当你开始培养一种新习惯时，它所用时间不应超过两分钟。这样的策略也有另一个原因：它们强化着你想要建立的身份。如果你连续五天现身健身房，哪怕只在那里停留两分钟，你就是在为你的新身份投赞同票。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;4奖励让它令人愉悦&#34;&gt;&#xA;  4、奖励：让它令人愉悦&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#4%e5%a5%96%e5%8a%b1%e8%ae%a9%e5%ae%83%e4%bb%a4%e4%ba%ba%e6%84%89%e6%82%a6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;保持习惯的关键是要有成就感，哪怕只是细微的感受。成就感是一个信号，它表明你的习惯有了回报，你为此付出的努力是值得的。比如，把要做的事情列成一个todo列表，完成一件划掉一项，看到todo上的事情全部划掉就是一种“奖励”。&lt;/p&gt;&#xA;&lt;p&gt;取得进步令人满意，借助于视觉量度，如移动曲别针、发夹或弹珠，你能清晰地看到自己的进步。这样做的结果是，它们强化着你的行为，并为任何活动增加一些即时满足感。视觉量度有多种形式：食物日志、健身日志、打孔忠诚卡、软件下载进度条，甚至书籍中的页码等等。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;作者做的总结表格&#34;&gt;&#xA;  作者做的总结表格&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%bd%9c%e8%80%85%e5%81%9a%e7%9a%84%e6%80%bb%e7%bb%93%e8%a1%a8%e6%a0%bc&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;作者将根据以上四个步骤如何养成好习惯以及戒除坏习惯的方法，总结在下面的表格里。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;怎样养成好习惯&#34;&gt;&#xA;  怎样养成好习惯&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%80%8e%e6%a0%b7%e5%85%bb%e6%88%90%e5%a5%bd%e4%b9%a0%e6%83%af&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;div align=&#34;center&#34;&gt;&#xA; &lt;img src=&#34;https://www.codedump.info/media/imgs/20220319-weekly-10/habit-table.png&#34; width=&#34;400&#34; height=&#34;200&#34; alt=&#34;养成好习惯&#34; align=center /&gt;&#xA;&lt;/div&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;怎样戒除坏习惯&#34;&gt;&#xA;  怎样戒除坏习惯&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%80%8e%e6%a0%b7%e6%88%92%e9%99%a4%e5%9d%8f%e4%b9%a0%e6%83%af&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;div align=&#34;center&#34;&gt;&#xA; &lt;img src=&#34;https://www.codedump.info/media/imgs/20220319-weekly-10/drop-habit-table.png&#34; width=&#34;400&#34; height=&#34;200&#34; alt=&#34;戒掉坏习惯&#34; align=center /&gt;&#xA;&lt;/div&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;我的实践&#34;&gt;&#xA;  我的实践&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%88%91%e7%9a%84%e5%ae%9e%e8%b7%b5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;为了减少睡觉前看手机，以及睡醒之后看手机的时间，我的做法是睡觉前把手机放在远离床头的地方。（避免坏习惯就让它难以被接触）。&lt;/li&gt;&#xA;&lt;li&gt;跑步时使用&lt;a href=&#34;https://github.com/yihong0618/running_page&#34;&gt;running_page&lt;/a&gt;这个项目来记录、展示我的跑步数据，一目了然也会带来更多的成就感。（奖励习惯）。&lt;/li&gt;&#xA;&lt;li&gt;年纪越大，越发现需要“坚持”去做的事情，很难做得长久。因为一提到需要“坚持”，很大原因是因为其中有阻碍、没有正反馈等等，这些都可以在上面四个步骤里找到答案。越不需要“坚持”就能做下去的事情，才越能长久做下去。所以，以后再遇到事情发现做不下去时，第一反应不应该再是什么意志力不够等原因，而是要从上面几个步骤里找到是哪里出了问题。是不够显而易见？不够有吸引力？不够简便？奖励不够？不够愉悦？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;其他推荐&#34;&gt;&#xA;  其他推荐&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%85%b6%e4%bb%96%e6%8e%a8%e8%8d%90&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;有关sre和devops的两篇文章&#34;&gt;&#xA;  有关SRE和Devops的两篇文章&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%89%e5%85%b3sre%e5%92%8cdevops%e7%9a%84%e4%b8%a4%e7%af%87%e6%96%87%e7%ab%a0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;推荐&lt;a href=&#34;https://twitter.com/laixintao&#34;&gt;@laixintao&lt;/a&gt;有关SRE和Devops的两篇文章：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第9期）：Mozilla rr使用简介</title>
      <link>https://www.codedump.info/zh/post/20220313-weekly-9/</link>
      <pubDate>Sun, 13 Mar 2022 11:20:59 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220313-weekly-9/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：在之前的&lt;a href=&#34;https://www.codedump.info/post/20220227-weekly-7/&#34;&gt;周刊（第7期）：一个C系程序员的Rust初体验&lt;/a&gt;中，简单提到过Mozilla rr这款调试工具，由于这个工具并不是太为人所知，所以本文对该工具做一个简介。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;mozilla-rr使用简介&#34;&gt;&#xA;  Mozilla rr使用简介&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#mozilla-rr%e4%bd%bf%e7%94%a8%e7%ae%80%e4%bb%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://rr-project.org/&#34;&gt;rr&lt;/a&gt;是由Mozilla出品的一款调试工具，用官网的话来说：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;rr aspires to be your primary C/C++ debugging tool for Linux, replacing — well, enhancing — gdb. You record a failure once, then debug the recording, deterministically, as many times as you want. The same execution is replayed every time.&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;即它的特点是：可以记录下来程序运行时的上下文环境，包括线程、堆栈、寄存器等等，这样的好处有两个：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;“deterministically”：很多问题问题的产生，都与特定的环境相关，如：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;线程调度执行的顺序，先执行A线程再B线程，以及反之，可能得到的是不同的结果。&lt;/li&gt;&#xA;&lt;li&gt;环境参数，如输入不同的参数，尤其一些边界条件的触发就跟输入不同的参数有关。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;replay：记录下来程序执行的环境之后，rr除了支持gdb方式的调试之后，还能利用环境来不停的重放程序，甚至反向来执行程序。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下对&lt;code&gt;rr&lt;/code&gt;的使用做一些简单的介绍。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;deterministically&#34;&gt;&#xA;  deterministically&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#deterministically&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;以下面一个最简单的多线程程序来解释何为&lt;code&gt;deterministically&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;#include&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;&amp;lt;pthread.h&amp;gt;&lt;/span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;#include&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt; * &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;doPrint&lt;/span&gt;(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt; *arg)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;NULL&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;main&lt;/span&gt;() {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;pthread_t&lt;/span&gt; pid;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;pthread_create&lt;/span&gt;(&amp;amp;pid, &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;NULL&lt;/span&gt;, doPrint, &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;NULL&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;printf&lt;/span&gt;(&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;pid = %lu&lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;&lt;/span&gt;, pid);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;return&lt;/span&gt; 0;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个程序很简单：创建一个线程之后，打印线程的pid。&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第8期）：技术配图的一些心得</title>
      <link>https://www.codedump.info/zh/post/20220304-weekly-8/</link>
      <pubDate>Fri, 04 Mar 2022 22:10:11 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220304-weekly-8/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：写过不少技术文章，以及给不少技术思路手绘示例配图之后，在这方面有了一些心得，本文权当个人的一些的总结，抛砖引玉。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;技术配图的一些心得&#34;&gt;&#xA;  技术配图的一些心得&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%8a%80%e6%9c%af%e9%85%8d%e5%9b%be%e7%9a%84%e4%b8%80%e4%ba%9b%e5%bf%83%e5%be%97&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;我觉得我们理工科出身的，对于可以量化的事情，总是很容易根据量化差异来做出判断，比如一个程序性能优化之后，能比优化之前快出多少，都能有一个量化的数字来说明。&lt;/p&gt;&#xA;&lt;p&gt;但是对于那些不能量化的东西，就很难说出具体好在哪里了。&lt;/p&gt;&#xA;&lt;p&gt;本文主题要讨论的“技术配图”就属于这种很难量化的领域，很难有一个标准来量化说明两幅图之间差别在哪里。我也是画了很多图，以及看了别人的很多配图之后，才慢慢有一些心得，本文权当个人的一些的总结，抛砖引玉。&lt;/p&gt;&#xA;&lt;p&gt;本文并不是一个画图工具的对比说明，尽管现在各种绘图工具已经很多，也各有自己的优缺点以及个人喜好，但是在这里并不讨论具体工具的使用，会把更多的文字放在配图的一些注意事项上。但是，也总有人问我文章的配图使用什么工具做的，在这里再回答一次：&lt;a href=&#34;https://www.omnigroup.com/omnigraffle&#34;&gt;OmniGraffle&lt;/a&gt;，一款目前仅有Mac版本的工具软件。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;一图胜千言&#34;&gt;&#xA;  一图胜千言&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%80%e5%9b%be%e8%83%9c%e5%8d%83%e8%a8%80&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在开始交代具体的配图注意事项之前，有必要先说说配图的重要性。&lt;/p&gt;&#xA;&lt;p&gt;绘图，某种程度也是辅助自己思考某个技术点的手段之一，以我个人的体会来说，有时候讲不清楚一个技术点的时候，就手绘图出来，比朴素的文字更容易说明问题。其中的原因，有可能是：图片可以有多维的信息，而文字通常只有一维，遇到文字表达能力不太好的人，这仅有的一维能力可能还不好发挥出来。&lt;/p&gt;&#xA;&lt;p&gt;所以，在交代技术细节、沟通交流的时候，尽量多画图。反向的，图画多了，也自然慢慢会找到感觉，如何更好的通过图示表达思路。&lt;/p&gt;&#xA;&lt;p&gt;顺便一提，还有比朴素的文字表达更差的技术沟通方式，就是简单粗暴的贴一大段代码上去。这种做法，其实更多时候是没有对作者的思路有太多个人的整理，想偷懒的方式，最后回头再看写过的文字，可能连自己都看不懂了。&lt;/p&gt;&#xA;&lt;p&gt;个人的一个体会：如果产出某些输出的时候，能假设自己未来就是这些输出的读者、维护者，那么输出起来会更“友善”一些。比如写的代码、文章、甚至于提交代码时候的信息，如果能考虑是写给未来的自己看的，会更清晰、尽可能留下更多的信息。我最开始要在文章里大量配图，也是为了将来自己回看的时候能看懂。&lt;/p&gt;&#xA;&lt;p&gt;扯远了，总之，尽可能多画图来表达技术思路。&lt;/p&gt;&#xA;&lt;p&gt;下面开始正题，以下会以简单的几个原则及示例来说明。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;区分联系组合&#34;&gt;&#xA;  区分、联系、组合&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8c%ba%e5%88%86%e8%81%94%e7%b3%bb%e7%bb%84%e5%90%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;配图中，应该尽量将不同的模块、组件等区分开来，“区分”的方式有很多，常见的有：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;使用不同的颜色。&lt;/li&gt;&#xA;&lt;li&gt;使用不同的形状。&lt;/li&gt;&#xA;&lt;li&gt;使用箭头、曲线等表示数据的走向、趋势。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;等等，所有的这些手段，概括起来就是尽量在图中，将不同的元素区分开来，“有区分”意味着至少有一个维度的不同，这样能给读者更加清晰的感觉。可以结合下面的例子来理解区分、联系和组合的绘图表达。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;分组&#34;&gt;&#xA;  分组&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%88%86%e7%bb%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;一个模块里，可能由多个组件构成，可以把这些组件分组到一个更大的模块中。&lt;/p&gt;&#xA;&lt;p&gt;分组是非常常见的一种手段，这里多举几个例子。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;cpu&#34; src=&#34;https://www.codedump.info/media/imgs/20220304-weekly-8/cpu.jpeg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; cpu &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，每个CPU Core中有L1、L2缓存，于是把这些组件合并在一起放在Core组件中，周围使用一个正方形包裹起来，同时这个正方形左上角有一个&lt;code&gt;Core&lt;/code&gt;的说明文字，这样一目了然：Core模块，由L1、L2缓存构成。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;meituan&#34; src=&#34;https://www.codedump.info/media/imgs/20220304-weekly-8/raft-log.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; meituan &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图出自Raft论文，整体上划分为了Client、Server这两大部分。而每个Server又有以下三部分组成：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一致性算法模块。&lt;/li&gt;&#xA;&lt;li&gt;状态机。&lt;/li&gt;&#xA;&lt;li&gt;持久化的日志。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;所以，图示中将这三部分合在一起放在同一个矩形里，表示一个Server有这三个组件。&lt;/p&gt;&#xA;&lt;p&gt;另外还需注意的是，一般这种分组中外围的矩形，有这样的讲究：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一般使用斜面矩形，即四个角是圆角的矩形，这样圆润一些的边角看起来会更舒服一些，如上图。&lt;/li&gt;&#xA;&lt;li&gt;如果这个组合，是一种逻辑上的组合，那么线的形状一般用虚线；否则就一般用的实线。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在分组时，有时候可以将相同类型的模块层叠起来，这样会更加简洁，如下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;raft&#34; src=&#34;https://www.codedump.info/media/imgs/20220304-weekly-8/raft.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; raft &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图是出自Raft论文中的状态机模型，其中想要表达的一个点是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;有多个client向server发起请求。&lt;/li&gt;&#xA;&lt;li&gt;server要达成一致，需要将日志在server之间同步。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;但是上图中，并没有把这些同类型的组件分开表达，而是巧妙的使用层叠的方式，简洁得表达了有多个client、多个server的情况。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;趋势&#34;&gt;&#xA;  趋势&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%b6%8b%e5%8a%bf&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;如果不同的组件之间，有不同的趋势，可以在图中使用类似箭头这样的符号表达出来。&lt;/p&gt;&#xA;&lt;p&gt;下图是描述不同层次存储的访问速度，于是用了两个方式来表达访问速度的变化趋势：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;左边的箭头表达速度和成本的变化。&lt;/li&gt;&#xA;&lt;li&gt;不同大小的多边形表达了这些存储空间的变化：越往上访问速度越快，但是对应的存储空间也更小。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;cache&#34; src=&#34;https://www.codedump.info/media/imgs/20220304-weekly-8/cache.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; cache &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;再比如，下图中，是说明sqlite中btree页面的数据组织的。其中的两部分内容，&lt;code&gt;Cell地址数组&lt;/code&gt;以及&lt;code&gt;Cell内容区&lt;/code&gt;为变长大小，前者从地址低位向高位生长，后者反之，于是在图中，就用箭头示例出地址的高低位区别，以及两者的增长方向：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;页面内数组的组织&#34; src=&#34;https://www.codedump.info/media/imgs/20220201-sqlite-btree-5-btree/page-format.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 页面内数组的组织 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;（出自&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;）&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;联系&#34;&gt;&#xA;  联系&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%81%94%e7%b3%bb&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;用箭头等表示数据、状态等的走向，或者模块之间的联系。&lt;/p&gt;&#xA;&lt;p&gt;这在涉及：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;状态切换。&lt;/li&gt;&#xA;&lt;li&gt;数据流向。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;等场景下是非常常见的手段，比如经典的TCP状态机切换：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第7期）：一个C系程序员的Rust初体验</title>
      <link>https://www.codedump.info/zh/post/20220227-weekly-7/</link>
      <pubDate>Sun, 27 Feb 2022 11:25:33 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220227-weekly-7/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：在工作里使用Rust已经有两个多月的时间了，谈谈我做为一名多年的C系（C、C++）程序员，对Rust的初体验。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;一个c系程序员的rust初体验&#34;&gt;&#xA;  一个C系程序员的Rust初体验&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%80%e4%b8%aac%e7%b3%bb%e7%a8%8b%e5%ba%8f%e5%91%98%e7%9a%84rust%e5%88%9d%e4%bd%93%e9%aa%8c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;最近由于工作的原因，使用上了Rust语言，在此之前我有多年的C、C++编码经验（以下将C、C++简称C系语言）。&lt;/p&gt;&#xA;&lt;p&gt;使用C系语言编码时，最经常面对的问题就是内存问题，诸如：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;野指针（Wild Pointer）：使用了不可知的指针变量，如已经被释放、未初始化、随机，等等。&lt;/li&gt;&#xA;&lt;li&gt;内存地址由于访问越界等原因被覆盖（overflow），这不但是可能出错的问题，还有可能成为程序的内存漏洞被利用。&lt;/li&gt;&#xA;&lt;li&gt;内存分配后未回收。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;连Chrome的报告都指出，Chrome中大约70%的安全漏洞都是内存问题，见：&lt;a href=&#34;https://www.chromium.org/Home/chromium-security/memory-safety/&#34;&gt;Memory safety&lt;/a&gt;。（不仅如此，微软的文章也显示在微软的产品中70%的安全漏洞也是内存问题，见：&lt;a href=&#34;https://www.zdnet.com/article/microsoft-70-percent-of-all-security-bugs-are-memory-safety-issues/&#34;&gt;Microsoft: 70 percent of all security bugs are memory safety issues | ZDNet&lt;/a&gt;）&lt;/p&gt;&#xA;&lt;p&gt;C系语言发展到今天，已经有不少可以用于内存问题检测的利器了，其中最好用的莫过于&lt;a href=&#34;https://en.wikipedia.org/wiki/AddressSanitizer&#34;&gt;AddressSanitizer&lt;/a&gt;，它的原理是在编译时给程序加上一些信息，一旦发生内存越界访问、野指针等错误都会自动检测出来。&lt;/p&gt;&#xA;&lt;p&gt;但是即便有这些工具，内存问题也不好解决，其核心的原因在于：这些问题绝大部分都是运行时（Runtime）问题，即要在程序跑到特定场景的时候才会暴露出来，诸如上面提到的AddressSanitizer就是这样。&lt;/p&gt;&#xA;&lt;p&gt;都知道解决问题的第一步是能复现问题，而如果一个问题是运行时问题，这就意味着：复现问题可能会是一件很麻烦的事情，有时候还可能到生产环境去复现。&lt;/p&gt;&#xA;&lt;p&gt;以我之前经历的一个Bug来看这类工作的复杂度，见&lt;a href=&#34;https://www.codedump.info/post/20190413-problem-fix/&#34;&gt;线上存储服务崩溃问题分析记录 - codedump的网络日志&lt;/a&gt;，这是一个很典型的发生在生产环境上由于内存错误导致的崩溃问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;不好复现，因为跟特定的请求相关，还跟线程的调度有关；&lt;/li&gt;&#xA;&lt;li&gt;本质是由于使用了被释放的内存导致的错误。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这个线上问题，记得当时花了一周时间来复现问题解决。&lt;/p&gt;&#xA;&lt;p&gt;换言之，如果一个问题要等到运行时才能发现，那么可以预见的是：一旦出现问题，要复现问题可能要花费大量的精力，以及需要很多经验才行。如果一个问题还是在特定场景，或者用户现场才出现的，那就更麻烦了，C系程序员以往一般都是这样来保存“现场”：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;出现崩溃的时候保存core文件来查看调用堆栈、变量等信息。&lt;/li&gt;&#xA;&lt;li&gt;发明了各种复制流量重放的工具，比如&lt;a href=&#34;https://github.com/session-replay-tools/tcpcopy&#34;&gt;tcpcopy&lt;/a&gt;等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;总而言之，运行时问题一旦出现是很麻烦的，而解决这类问题的时间是难以预期的。&lt;/p&gt;&#xA;&lt;p&gt;Rust给这类内存问题的解决提供了另一个解决思路：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一个内存地址同时只能被一个变量使用。&lt;/li&gt;&#xA;&lt;li&gt;不能使用未初始化的变量。&lt;/li&gt;&#xA;&lt;li&gt;&amp;hellip;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;简而言之，凡是可能出现内存错误的地方，都在语言的语法层面给予禁止，换来的就是更多的编译时间，因为要做这么多检查嘛，而需要更多的编译时间反过来就需要更好的硬件。我想这也是Rust到了最近几年才开始慢慢流行开来的原因之一，毕竟即便是现在，一些大型的Rust项目普通的机器编译起来也还是很耗时。&lt;/p&gt;&#xA;&lt;p&gt;“编译时间（compile time）”是一个可以预期的固定时间，能通过增加硬件性能（比如买更好的机器来写Rust）来解决；而“运行时问题”一旦出现，查找起来的时间、精力、场景（比如出现在用户现场、几百万次才能重现一次等）不确定性可就很高了。&lt;/p&gt;&#xA;&lt;p&gt;两者权衡，我选择解决“编译时间”问题。而且，在我意识到有这样的工具能够在编译期解决大部分内存问题时，反过来再看使用C系语言的项目，几乎可以预期的是：只要代码和复杂度上了一定规模，那么这类项目都要花上相当的一段时间才能稳定下来。原因在于：类似内存问题这样的运行时问题，是需要场景去积累，才能暴露出来的，而场景的积累，就需要很多的小白鼠和运行时间了。&lt;/p&gt;&#xA;&lt;p&gt;总结一下我的观点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;C系语言最多的问题就是各类内存问题，而这些问题大多是运行时问题。即便现在已经有了各种工具，解决其运行时问题也很困难。&lt;/li&gt;&#xA;&lt;li&gt;Rust解决这类问题的思路，是在语法层面禁止一切可能出现内存问题的操作，换来的代价就是更多的编译时间。&lt;/li&gt;&#xA;&lt;li&gt;解决可预期的“编译时间”和难预期的“运行时问题”，我选择前者。人生苦短，浪费时间在解决运行时的各种内存问题太不值当了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;番外篇&#34;&gt;&#xA;  番外篇&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%95%aa%e5%a4%96%e7%af%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;rr&#34;&gt;&#xA;  rr&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#rr&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://rr-project.org/&#34;&gt;rr: lightweight recording &amp;amp; deterministic debugging&lt;/a&gt;也是出自Mozilla的另一款调试C系程序的利器，&lt;code&gt;rr&lt;/code&gt;是&lt;code&gt;Record and Replay&lt;/code&gt;的简称，目的还是为了解决各种运行时问题，由于运行时问题中存在着各种不确定的因素，包括：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;变量值。&lt;/li&gt;&#xA;&lt;li&gt;进程、线程环境，比如不同的线程调度顺序可能导致了不同的结果。&lt;/li&gt;&#xA;&lt;li&gt;输入不同的数据，能得到不同的结果。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;于是，&lt;code&gt;rr&lt;/code&gt;要解决的核心问题，就是让一个程序在运行时有一个固定的环境，它可以抓取程序运行的环境保存下来。这样在出现问题之后，就能使用它可以记录下来程序运行时的环境，不停的重放来调试解决问题。&lt;/p&gt;&#xA;&lt;p&gt;但是，即便是这样，&lt;code&gt;rr&lt;/code&gt;可能更适合于明确知道问题的情况下去抓取环境，不可能在线上直接打开这个工具。所以又回到前面的结论了：调试运行时问题可能面对的困难，包括场景、时间、用户现场等等不确定因素。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;rr&lt;/code&gt;和&lt;code&gt;Rust&lt;/code&gt;一样，都出自Mozilla，我想不是偶然的。Mozilla和chrome等一样，都是使用C++编码的超大型项目，而这里一定遇到了各种运行时问题，不止于内存问题，所以才要使用各种工具来辅助解决这类问题。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;吃上硬件升级的红利了吗&#34;&gt;&#xA;  吃上硬件升级的红利了吗？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%90%83%e4%b8%8a%e7%a1%ac%e4%bb%b6%e5%8d%87%e7%ba%a7%e7%9a%84%e7%ba%a2%e5%88%a9%e4%ba%86%e5%90%97&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;前面提到过，Rust目前较大的问题是编译时间过长，这可能是导致它最近几年才开始逐渐流行开来的原因。其实反过来说，在硬件升级之后，应该能尽量利用上硬件，在编译期尽量多检查出错误来，减少运行时发现问题的数量。这样，才能吃上硬件升级的红利，利用硬件来减少自己的犯错。&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第6期）：《sqlite 3.36 btree实现解析》番外篇</title>
      <link>https://www.codedump.info/zh/post/20220220-weekly-6/</link>
      <pubDate>Sun, 20 Feb 2022 10:53:41 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220220-weekly-6/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：从2021年9月份开始要探索生产级btree存储引擎的实现，到2022年2月整理完毕发布《sqlite 3.36 btree实现解析》的系列文章，我花费了小半年的时间，本期会聊聊整个过程下来我的一些想法。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;sqlite-336-btree实现解析番外篇&#34;&gt;&#xA;  《sqlite 3.36 btree实现解析》番外篇&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#sqlite-336-btree%e5%ae%9e%e7%8e%b0%e8%a7%a3%e6%9e%90%e7%95%aa%e5%a4%96%e7%af%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;时间回到2021年9月份。彼时，因为工作的关系，要研究一下生产级btree存储引擎的实现，在此之前我大体对btree、b+tree的数据结构和算法有个了解，见：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;但是，一个生产级的产品，对比教科书的示范型代码，还是有很大的区别的，具体来说，我当时不明白以下这些生产级存储引擎的问题如何解决：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如何存储变长的数据？&lt;/li&gt;&#xA;&lt;li&gt;如何存储数据大小超过一个物理页面的数据？&lt;/li&gt;&#xA;&lt;li&gt;如何利用被回收的空间？&lt;/li&gt;&#xA;&lt;li&gt;如何处理崩溃恢复？&lt;/li&gt;&#xA;&lt;li&gt;读写并发如何处理？&lt;/li&gt;&#xA;&lt;li&gt;&amp;hellip;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;为了解答这些疑问，先后去翻阅InnodDB、WiredTiger、sqlite的文档，但是这些项目代码量都太大了，以我当时的程度，无法马上找到很具体的解答。&lt;/p&gt;&#xA;&lt;p&gt;事情的突破在从网上查找文章时看到的这一篇文章：&lt;a href=&#34;https://dzone.com/articles/database-btree-indexing-in-sqlite&#34;&gt;How Database B-Tree Indexing Works - DZone Database&lt;/a&gt;，这是一篇解释btree工作原理的文章，这篇文章同时还列出了一个项目：&lt;a href=&#34;https://github.com/madushadhanushka/simple-sqlite&#34;&gt;madushadhanushka/simple-sqlite: Code reading for sqlite backend&lt;/a&gt;，这个项目的作者，将sqlite2.5版本中btree的实现，单独抽取出来形成了一个独立的KV库，可以编译通过使用。&lt;/p&gt;&#xA;&lt;p&gt;看到这个项目的时候，我的感觉就是如获至宝，因为虽然只有几千行的代码量，但是解答了很多上面提到的疑问，“麻雀虽小五脏俱全”，我花了几天的时间整体阅读了解了原理，这个项目给我打开了研究生产级btree存储引擎的突破口。&lt;/p&gt;&#xA;&lt;p&gt;在这以后，考虑到2.5版本的sqlite已经是2002年的作品，距离现在时间太久了，还想接着了解后面做了那些改进，又接着阅读了3.6.10版本的实现，找这个版本的原因，是因为这是sqlite官方在github上同步的第一个版本，那时候仍然步子不敢迈得太大。&lt;/p&gt;&#xA;&lt;p&gt;又花了一个多月把这个版本的btree实现了解以后，我了解到在这之后的版本里，sqlite做了另一个重大的更新：在页面管理部分引入了WAL机制，加上前面两个版本阅读下来累积的信心，就接着找当时还是最新的3.36版本的实现来阅读，这又花了一个多月的时间。&lt;/p&gt;&#xA;&lt;p&gt;这以后，就是逐步将整理的笔记写成文档了，后续的事情不表，都在这几篇文档里。&lt;/p&gt;&#xA;&lt;p&gt;回头看这整个流程，我自己的感受是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;“问题驱动”可能是效率更高的学习方式，带着问题出发、找到自己疑问的答案，能更快的学习某个知识。&lt;/li&gt;&#xA;&lt;li&gt;生产级的实现和教科书的区别很大，后者更多的是讲解原理，而生产级实现考虑更多的是各种实际生产中的边际情况。如果只了解原理，而不去具体做实现，对事情的理解最后只能浮于表面。&lt;/li&gt;&#xA;&lt;li&gt;找到那个精简实现是这个过程里的“突破口”，原因在于：如果一上来看的很成熟的版本，而且你在这个领域积累的不深，那么很可能会导致丢失了很多“上下文（context）”情景，给阅读、理解带来很大困难。下次再遇到类似的问题，我会按照这次的经验，先尝试回退到之前的更简单的版本，看看在那里能不能跟上作者的思路，攻克简单的实现之后，再尝试最新的版本。&lt;/li&gt;&#xA;&lt;li&gt;除了数据库领域以外，有一些别的领域，在教学的时候会让学生参与实现一个简单的项目。这类型的项目虽然简单，但是五脏俱全，能够让学生了解这个领域的概貌，我把这种流程称为“破解神秘感”。如我最开始提到的那些疑问，如果在这之前做过数据库相关的作业，应该会有个大体的想法。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;这篇番外篇的番外篇&#34;&gt;&#xA;  这篇番外篇的番外篇&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%bf%99%e7%af%87%e7%95%aa%e5%a4%96%e7%af%87%e7%9a%84%e7%95%aa%e5%a4%96%e7%af%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;sqlite的注释&#34;&gt;&#xA;  sqlite的注释&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#sqlite%e7%9a%84%e6%b3%a8%e9%87%8a&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;除了这些以外，sqlite的代码风格也很好，尤其是注释写的非常详尽。&lt;/p&gt;&#xA;&lt;p&gt;有一种说法，“好的代码都是自解释的，无需多做注释”。我对这句话有一些不太一样的看法，因为即便再好的代码，如果只看代码的话，对整个的架构、结构很难了解。这一点sqlite就做的很好，在代码中会写上类似文档一样的注释来解释结构，比如有这么一段解释btree内部结构的注释文档：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;** This file implements an &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;external&lt;/span&gt; (disk-based) database using BTrees.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;** For a detailed discussion of BTrees, refer to&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;**&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;**     Donald E. Knuth, THE ART OF COMPUTER PROGRAMMING, Volume 3:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;**     &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;Sorting And Searching&amp;#34;&lt;/span&gt;, pages 473-480. Addison-Wesley&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;**     Publishing Company, Reading, Massachusetts.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;**&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;** The basic idea is that each page of the file contains N database&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;** entries and N+1 pointers to subpages.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;**&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;**   ----------------------------------------------------------------&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;**   |  &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Ptr&lt;/span&gt;(0) | &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Key&lt;/span&gt;(0) | &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Ptr&lt;/span&gt;(1) | &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Key&lt;/span&gt;(1) | ... | &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Key&lt;/span&gt;(N-1) | &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Ptr&lt;/span&gt;(N) |&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;**   ----------------------------------------------------------------&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;**&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;** All of the keys on the page that &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Ptr&lt;/span&gt;(0) points to have values less&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;** than &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Key&lt;/span&gt;(0).  All of the keys on page &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Ptr&lt;/span&gt;(1) and its subpages have&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;** values greater than &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Key&lt;/span&gt;(0) and less than &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Key&lt;/span&gt;(1).  All of the keys&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;** on &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Ptr&lt;/span&gt;(N) and its subpages have values greater than &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Key&lt;/span&gt;(N-1).  And&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;** so forth.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;*/&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果不写这些注释，读者想要理解作者的思路的话，仅凭代码是很困难的。&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第5期）：从存储模型聊一聊时序数据库的应用场景</title>
      <link>https://www.codedump.info/zh/post/20220211-weekly-5/</link>
      <pubDate>Fri, 11 Feb 2022 22:27:57 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220211-weekly-5/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本期介绍时序数据库的存储模型，只有理解了时序数据的存储模型，才能更好的了解时序数据库的优缺点以及其适用场景。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;从存储模型聊一聊时序数据库的应用场景&#34;&gt;&#xA;  从存储模型聊一聊时序数据库的应用场景&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%bb%8e%e5%ad%98%e5%82%a8%e6%a8%a1%e5%9e%8b%e8%81%8a%e4%b8%80%e8%81%8a%e6%97%b6%e5%ba%8f%e6%95%b0%e6%8d%ae%e5%ba%93%e7%9a%84%e5%ba%94%e7%94%a8%e5%9c%ba%e6%99%af&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;想写本文，是因为看到了知乎上的一篇文章：&lt;a href=&#34;https://zhuanlan.zhihu.com/p/453556881&#34;&gt;投资数据库领域：2021年总结（NoSQL、图、时序） - 知乎&lt;/a&gt;，里面谈到了时序数据库：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;但缺陷是实际的市场空间较小。跟通用型数据库，尤其是OLAP数据库相比，时序数据库最大的差异点在于对于时间维度建立了独特的索引与优化，而其他所谓schemaless等特性在OLAP数据库上都能做到，不存在技术障碍。这也就是为什么其实在公司做时序场景的数据库选型的时候会直接将时序数据库与一些OLAP数据库（比如ClickHouse）做比较。如果要把时序数据库往更宽的场景发展，那就是想好如何与那么多的通用型数据库做竞争了。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;由于之前有过短暂一段时间的时序数据库从业经历，所以想从我的理解聊聊时序数据库的应用场景。&lt;/p&gt;&#xA;&lt;p&gt;要了解应用场景，需要首先对时序数据库的存储模型有个大概的了解，在下文中我尽量不涉及到太艰深的技术术语来描述我的理解。由于我从业时序数据库的时间并不长，所以有可能理解会有偏差。&lt;/p&gt;&#xA;&lt;p&gt;何谓“时序数据（time-series data）”？就我个人粗浅的理解，就是任何一定会带上时间戳（timestamp）维度的数据。日常生活里，在微博、微信等社交媒体的发现就可以理解时序数据，因为它们肯定都有一个发言时间，所以有时候会把个人看到的微博等称为“时间线（timeline）”。对应到工业领域，比如一个电表每小时上报的用电量也是时序数据，比如服务器监控时每隔15分钟采集的性能数据也是时序数据。&lt;/p&gt;&#xA;&lt;p&gt;由于时序数据天然有“时间“这个维度，为了更好的优化其写入性能，通常专门存储时序数据的存储引擎会按照时间分块、按列来存储数据，如下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;数据库文件的物理页面组织和逻辑页面结构&#34; src=&#34;https://www.codedump.info/media/imgs/20220211-weekly-5/time-series-model.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 数据库文件的物理页面组织和逻辑页面结构 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，演示用的数据格式有三列：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;时间戳。&lt;/li&gt;&#xA;&lt;li&gt;A指标。&lt;/li&gt;&#xA;&lt;li&gt;B指标。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;通常，时序数据库存储时，会按照时间来划分块（block）：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;块的大小固定。&lt;/li&gt;&#xA;&lt;li&gt;在同一个块时间区的数据，会存储到同一个块中。&lt;/li&gt;&#xA;&lt;li&gt;而块内部，将除了时间维度之外的其他的列，将其中相同列的数据存储在一起。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这样做的好处是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;由于时序数据的特点，写入的数据也是在时间上连续的，因此通常写入的时候按照上面的设计就能落在同一个块中。&lt;/li&gt;&#xA;&lt;li&gt;不同行但是同一列的数据，都是相同类型的，将相同类型的数据紧邻放在一起，更容易进行压缩。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;换言之，这样做换来的好处是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;在时序数据的写入场景下，写入速度很快。&lt;/li&gt;&#xA;&lt;li&gt;由于同类型数据放在一起，压缩性能也很好。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这些都是相对于传统BTree类存储引擎而言的，因为这类型的数据写入更像append操作，这是必然会更快的。&lt;/p&gt;&#xA;&lt;p&gt;但是注意到没有，这样存储数据之后，最大的问题是：查询时只有时间这个维度做了索引，而除去时间维度之外的其他列都没有做索引。&lt;/p&gt;&#xA;&lt;p&gt;这样导致的问题是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;任何查询都要带上时间参数才能管用。比如：“请查询过去一个小时里哪五分钟的CPU最高”这样的查询是可以的，但是更多其他的查询是不知道时间维度，或者说查询者就是不知道具体时间才想来查询的，比如“我是什么时候达成了累计跑步100公里成就的？”这类探索型、且没有时间维度的查询。&lt;/li&gt;&#xA;&lt;li&gt;即便是带上了时间维度的查询可行，由于没有对其他维度做索引，所以查询时的处理，更多的是按照时间维度查询出数据、再进行聚合计算，比如上面的“请查询过去一个小时里哪五分钟的CPU最高”这个查询，只能先把过去一小时的CPU数据全部查出来，然后逐个计算才能算出哪5分钟的CPU最高了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;总结下来：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;时序数据库根据时序数据的特点设计和优化了时序数据库的存储模型，对比传统的关系数据库存储模型来说，优势是写入速度快、压缩比高。&lt;/li&gt;&#xA;&lt;li&gt;但这样的存储只有时间这个维度，换言之由于没有其他维度的索引数据，导致对不带有时间维度或者时间跨度大的查询支持的不够友好。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;回到最开始引用的文章，了解了时序数据库的存储特点，也就能解释为何作者认为纯粹的时序数据库场景不大了。&lt;/p&gt;&#xA;&lt;p&gt;好像大部分时候，事情也是这样的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;在一个维度优化到极致，可能其他维度就做的不够好，不存在各个维度都能做得很好的产品，因为不同维度之间也会彼此有制约，更多时候要看使用者自己的场景取舍，并不存在适用于一切场景的产品。&lt;/li&gt;&#xA;&lt;li&gt;所谓”优势“，在换了上下文和场景之后，也可能会变成劣势。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;相关推荐&#34;&gt;&#xA;  相关推荐&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%9b%b8%e5%85%b3%e6%8e%a8%e8%8d%90&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;writing-a-time-series-database-from-scratch--fabian-reinartz&#34;&gt;&#xA;  &lt;a href=&#34;https://fabxc.org/tsdb/&#34;&gt;Writing a Time Series Database from Scratch | Fabian Reinartz&lt;/a&gt;&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#writing-a-time-series-database-from-scratch--fabian-reinartz&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://prometheus.io/&#34;&gt;Prometheus&lt;/a&gt;项目的核心开发者写的文章，介绍了如何从头实现一个时序数据库的存储引擎，可做为上面文章更深入的补充。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;cmu在2017年时组织的时序数据库系列讲座&#34;&gt;&#xA;  CMU在2017年时组织的时序数据库系列讲座&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#cmu%e5%9c%a82017%e5%b9%b4%e6%97%b6%e7%bb%84%e7%bb%87%e7%9a%84%e6%97%b6%e5%ba%8f%e6%95%b0%e6%8d%ae%e5%ba%93%e7%b3%bb%e5%88%97%e8%ae%b2%e5%ba%a7&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;邀请了一系列业内时序数据库的从业人员来分享，见：&lt;a href=&#34;https://db.cs.cmu.edu/seminar2017/&#34;&gt;Time Series Database Lectures – Fall 2017&lt;/a&gt;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;阿里云数据库内核月报分类整理&#34;&gt;&#xA;  &lt;a href=&#34;https://github.com/tangwz/db-monthly&#34;&gt;阿里云数据库内核月报分类整理&lt;/a&gt;&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%98%bf%e9%87%8c%e4%ba%91%e6%95%b0%e6%8d%ae%e5%ba%93%e5%86%85%e6%a0%b8%e6%9c%88%e6%8a%a5%e5%88%86%e7%b1%bb%e6%95%b4%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;阿里云数据库内核团队的月报提供了很多学习数据库技术的资料、文章，这个github将每个月的月报进行了分类整理，推荐收藏。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;how-does-a-database-work--lets-build-a-simple-database&#34;&gt;&#xA;  &lt;a href=&#34;https://cstack.github.io/db_tutorial/&#34;&gt;How Does a Database Work? | Let’s Build a Simple Database&lt;/a&gt;&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#how-does-a-database-work--lets-build-a-simple-database&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;自己动手打造一个简单的数据库，应该有很多地方参考了&lt;a href=&#34;https://sqlite.org/index.html&#34;&gt;SQLite&lt;/a&gt;的实现，本博客也深度解析了sqlite的btree实现，见：&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;。&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第4期）：为什么我还在看中国足球</title>
      <link>https://www.codedump.info/zh/post/20220204-weekly-4/</link>
      <pubDate>Fri, 04 Feb 2022 21:55:23 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220204-weekly-4/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：虎年大年初一的晚上，一场脆败发生在世界杯亚洲区预选赛中国客场对越南队的比赛上。如今，“你居然还在看中国男足”，仿佛已成一句骂人的质问。本期从我角度来谈谈，我眼中的中国足球，以及说说我为什么还一直在关注这个领域。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;我为什么还在看中国足球&#34;&gt;&#xA;  我为什么还在看中国足球&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%88%91%e4%b8%ba%e4%bb%80%e4%b9%88%e8%bf%98%e5%9c%a8%e7%9c%8b%e4%b8%ad%e5%9b%bd%e8%b6%b3%e7%90%83&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;我从94年开始看球，中国足球绝大部分的重要比赛都看了：94年亚运会决赛输给乌兹别克斯坦、97年大连金州被伊朗逆转、2002年世界杯出线&amp;hellip;太多了，数不过来，算是从我开始看球之后就一直有关注中国足球。&lt;/p&gt;&#xA;&lt;p&gt;在看国足比赛二十多年之后，慢慢地从一个参与者、评论者的角色，切换到了近似于第三方视角的观察者角色。切换到这个视角之后，让我能从里面各种情绪里抽离出来，当然高兴的时候也会像个普通球迷那样欢乐，比如2017年世界杯预选赛击败韩国这样的比赛。&lt;/p&gt;&#xA;&lt;p&gt;我国虽然在奥运会上取得了看似很好的成绩，金牌数总是保持前列，但是有一说一，并不算是体育大国，更别提强国了。&lt;/p&gt;&#xA;&lt;p&gt;只看我们占优势、能取得好成绩的项目，大多有这样的特点：小众、冷门，这样的特点直接导致这样的项目，实际是商业化程度很低的领域。这样的领域，国外参与的人不会太多，也因此可以继续沿用以前我们擅长的打法：集中力量办大事，换到体育这个领域，就是所谓的“举国体制”。&lt;/p&gt;&#xA;&lt;p&gt;这样做的好处是，能用较少的资源拿到不错的效果，因为大部分人只关注金银牌这些数字，并不关心你怎么拿到的。这个策略，用知乎上一个回答的话来说叫“田忌赛马”，见：&lt;a href=&#34;https://www.zhihu.com/question/414037344/answer/1713991758&#34;&gt;为什么中国的其他运动项目那么强，到了男足这里就不行呢？&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;在商业化、职业化很好的体育项目，比如足球、篮球、网球等等领域，我们的成绩就不这么好了，李娜、姚明、刘翔是少数在这些领域拿得出手的世界级运动员。（后面会专门谈谈女足）&lt;/p&gt;&#xA;&lt;p&gt;一言以蔽之：举国体制从目前的成绩来看，并不适合职业化、商业化很好的体育项目。&lt;/p&gt;&#xA;&lt;p&gt;“足球是体育工业化的集大成者”（见&lt;a href=&#34;https://www.zhihu.com/question/310636566/answer/1720809481&#34;&gt;(为什么整个中国都知道中国足球的问题，为什么还是没有办法解决？ - 知乎&lt;/a&gt;），所以它不像其他领域那样，需要长期的积累和基础。&lt;/p&gt;&#xA;&lt;p&gt;所以，男足的存在，在我看来更多像是一个“大型的社会实验”，我在这个实验中，看到不同的政策、体制、行政干预、市场行为等对这个运动的影响，看到各方参与者、评论者、媒体的所作所为，从中能看到某些我们社会的缩影。&lt;/p&gt;&#xA;&lt;p&gt;男足也是“客观规律”的具象化代言人（见&lt;a href=&#34;https://weibo.com/1573046985/Ldz5zvYdY&#34;&gt;中国男足挺好的，理直&amp;hellip; - @祝佳音的微博 - 微博&lt;/a&gt;）：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;男足不跟你讲这些虚的，不按科学规律办事就是不行。给钱诱惑也不行，立规矩骂人也不行，做思想工作说服不行，临时加班加练也不行。富贵不能淫威武不能屈，不行就是不行。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;相对其他很多领域，足球的成绩更透明、公开，足球运动一直在提醒我要尊重“客观规律”。&lt;/p&gt;&#xA;&lt;p&gt;足球在中国，属于“参与度很低，但是关注度高”的体育项目。在这样的领域：看起来中超火爆的时候现场能有几万球迷现场观赛，看起来花了很多钱、投入了很多人力，但是只要没有按照足球的“客观规律”办事，成绩马上就能打脸，公开透明。&lt;/p&gt;&#xA;&lt;p&gt;规则透明、结果公开，等等这些都是我一直还在关注这个“大型社会实验”的原因。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;番外&#34;&gt;&#xA;  番外&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%95%aa%e5%a4%96&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;番外篇里，试图简单科普关于中国足球相关的几个常见问题。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;14亿人里为什么就选不出11个能踢球的人&#34;&gt;&#xA;  “14亿人里为什么就选不出11个能踢球的人？”&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#14%e4%ba%bf%e4%ba%ba%e9%87%8c%e4%b8%ba%e4%bb%80%e4%b9%88%e5%b0%b1%e9%80%89%e4%b8%8d%e5%87%ba11%e4%b8%aa%e8%83%bd%e8%b8%a2%e7%90%83%e7%9a%84%e4%ba%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;这是最常见的问题了，用类比的方式试图回答一下，这就好比问：“这么大一块沙漠为什么就种不出几棵树来？”。显然很多人并没有意识到，我国在足球从业人员领域属于“沙漠”，只是看起来热闹，仅此而已。&lt;/p&gt;&#xA;&lt;p&gt;我们的选材不是从14亿里选，而是足球注册球员里面选（中国好像只有几万足球从业人口），这个基数对比足球发达国家差的很多。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;怎么看待女足的成绩比男足好这么多&#34;&gt;&#xA;  怎么看待女足的成绩比男足好这么多？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%80%8e%e4%b9%88%e7%9c%8b%e5%be%85%e5%a5%b3%e8%b6%b3%e7%9a%84%e6%88%90%e7%bb%a9%e6%af%94%e7%94%b7%e8%b6%b3%e5%a5%bd%e8%bf%99%e4%b9%88%e5%a4%9a&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;女足有过比男足更光辉的历史：奥运会银牌、世界杯亚军。但是需要认识到，这些成绩的取得，已经年代久远，距离现在有20多年了。&lt;/p&gt;&#xA;&lt;p&gt;取得这些成绩很大的原因是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;由于宗教、妇女地位等原因，参与女足运动的国家并不多。&lt;/li&gt;&#xA;&lt;li&gt;当时职业化的女足并不多。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;比如在亚洲，男足要打世界杯预选赛、亚洲杯决赛这样的比赛，都要先进行一轮预选赛，因为有50多个国家参与；而女足对应的比赛，则无需预选赛，因为参与的队伍实在不多，比如一大批相对落后的国家派不出女足，比如阿拉伯国家也不让女足参赛（这次女足亚洲杯伊朗倒是参赛了）。&lt;/p&gt;&#xA;&lt;p&gt;另外，说到女足职业化。随着欧美女足职业联赛的发展，她们的水平提高了很多，此消彼长，这就是后来北京奥运会、东京奥运会大比分输球，以及世界排名一路滑到19名的原因。&lt;/p&gt;&#xA;&lt;p&gt;做一个可能不正确的类比：10个人参加的比赛里，取得第3名的成绩，确实比50个人参加的比赛里取得第10名，看起来好看一些。&lt;/p&gt;&#xA;&lt;p&gt;但是，即便抛开职业化、商业化、参赛队伍基数等因素，有一说一，女足的精气神确实比男足要高出一大截来，尤其在春节密集得看了几场男足、女足的比赛对比就更明显了。&lt;/p&gt;&#xA;&lt;p&gt;豆瓣上有人整理了本届女足亚洲杯现役国家队成员的介绍：&lt;a href=&#34;https://www.douban.com/group/topic/258215115/?_dtcc=1&amp;amp;_i=4158490DTua3Gc&#34;&gt;中国女足现役国家队队员介绍（亚洲杯来了）&lt;/a&gt;&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;是否一定要搞好足球&#34;&gt;&#xA;  是否一定要“搞好足球”？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%98%af%e5%90%a6%e4%b8%80%e5%ae%9a%e8%a6%81%e6%90%9e%e5%a5%bd%e8%b6%b3%e7%90%83&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;这个问题见仁见智，我不认为一定有“搞好足球”的必要，毕竟比这个事情重要的事情还有很多，“足球”也并不能代表一个国家的综合国力。&lt;/p&gt;&#xA;&lt;p&gt;我比较同意知乎这个回答里的几段话：&lt;a href=&#34;https://www.zhihu.com/question/310636566/answer/1830192531&#34;&gt;为什么整个中国都知道中国足球的问题，为什么还是没有办法解决？ - 知乎&lt;/a&gt;&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;因为和整个国家要解决的问题来比，中国足球不重要。&lt;/p&gt;&#xA;&lt;p&gt;&amp;hellip;&lt;/p&gt;&#xA;&lt;p&gt;有基建重要吗？有国防重要吗？有教育重要吗？有医疗重要吗？有扶贫攻坚重要吗？有抗击疫情重要吗？……三百六十行，至少得有三百行排在竞技体育前面好不好？&lt;/p&gt;&#xA;&lt;p&gt;中国人喜欢足球吗？中国人只喜欢看足球，而且是可有可无的那种喜欢。真喜欢踢，投身足球事业的不会只有这么点。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;但是不同意回答里的这句话：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;中国足球的问题不是没有办法解决，要解决中国足球的问题，集国家力量有一万种办法解决。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;毕竟我们还从来没有解决过，实事求是。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;总结&#34;&gt;&#xA;  总结&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%80%bb%e7%bb%93&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;总结一下我的观点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;足球属于商业化、职业化程度很高的竞技体育项目，“举国体制”从目前来看不适用于这样的领域。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;足球在中国属于“参与度低、关注度高”的项目，看着很热闹，实际真正参与、从业的人并不多。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;因为早期我们只关注奥运会的金银牌，所以集中力量发展冷门项目更容易出成绩。大部分人只关注简单的数字、金银牌，不会关注背后的难易程度，“10个人参加的比赛里，取得第3名的成绩，确实比50个人参加的比赛里取得第10名，看起来好看一些。”&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;我并不认为，有一定要“搞好足球”的必要性，至少现在没有，因为还有更重要的事情需要做。&lt;/p&gt;</description>
    </item>
    <item>
      <title>sqlite3.36版本 btree实现（五）- Btree的实现</title>
      <link>https://www.codedump.info/zh/post/20220201-sqlite-btree-5-btree/</link>
      <pubDate>Tue, 01 Feb 2022 15:55:40 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220201-sqlite-btree-5-btree/</guid>
      <description>&lt;p&gt;《sqlite3.36版本 btree实现》系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-1-pagecache/&#34;&gt;sqlite3.36版本 btree实现（一）- 管理页面缓存 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211222-sqlite-btree-3-journal/&#34;&gt;sqlite3.36版本 btree实现（三）- journal文件备份机制 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220106-sqlite-btree-4-wal/&#34;&gt;sqlite3.36版本 btree实现（四）- WAL的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面的内容里，详细介绍了页面管理器部分的内容，回顾一下页面管理器和Btree模块的分工：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;页面管理器：提供页面级别的物理管理，如缓存、读取、写入、页面备份等。&lt;/li&gt;&#xA;&lt;li&gt;Btree：根据btree数据结构提供页面在逻辑上的组织，以及单个页面内的划分。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;还记得最开始，研究生产级别btree实现时的几个疑问：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;数据库教科书中，演示btree算法时，使用的都是定长的简单数据。实际应用中，存储的数据都是变长的，那么应该如何存储变长的数据呢？&lt;/li&gt;&#xA;&lt;li&gt;如果一行数据的大小，超过了一个物理页面的大小，又该如何处理？&lt;/li&gt;&#xA;&lt;li&gt;删除一行数据之后，它留下的空间如何回收利用？而回收利用时，不可避免的会出现碎片的问题，比如原先10字节的数据被回收，用来存储9字节的数据，多出来的1字节数据就被浪费了，碎片问题应该如何解决？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这些问题，都与“一个物理页面内数据如何组织”这个核心问题息息相关，带着这些问题展开btree实现的讨论。&lt;/p&gt;&#xA;&lt;p&gt;在下文中，不会讨论btree算法的细节，这部分不熟悉的，可以回看之前的文章或者教科书：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;物理页面的数据组织&#34;&gt;&#xA;  物理页面的数据组织&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%89%a9%e7%90%86%e9%a1%b5%e9%9d%a2%e7%9a%84%e6%95%b0%e6%8d%ae%e7%bb%84%e7%bb%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;数据表的逻辑组织和页面类型&#34;&gt;&#xA;  数据表的逻辑组织和页面类型&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e8%a1%a8%e7%9a%84%e9%80%bb%e8%be%91%e7%bb%84%e7%bb%87%e5%92%8c%e9%a1%b5%e9%9d%a2%e7%b1%bb%e5%9e%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在展开具体的格式讨论之前，有必要先了解一下数据库文件的大体结构，已经不同的页面类型。&lt;/p&gt;&#xA;&lt;p&gt;sqlite中所谓的&lt;code&gt;数据库文件&lt;/code&gt;是单一文件，按照物理页面（2的次方）的大小来划分为多个页面。其中，每个表在数据库文件中是一棵btree的结构来组织，而不同类型的btree还区分了不同的页面。&lt;/p&gt;&#xA;&lt;p&gt;比如下图中，将平面的数据库文件，按照颜色划分成存储两个表的btree：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;数据库文件的物理页面组织和逻辑页面结构&#34; src=&#34;https://www.codedump.info/media/imgs/20220201-sqlite-btree-5-btree/database-file.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 数据库文件的物理页面组织和逻辑页面结构 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;上半部分表示，在物理的组织上，一个数据库文件以一个物理页面为基本单位来存储。&lt;/li&gt;&#xA;&lt;li&gt;下半部分表示，在逻辑的组织上，不同的表都有自己的btree树形结构，这是物理页面在逻辑上的组织方式。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因为每个表都有自己的btree树形结构，如果每个表都有一个对应的根页面编号，比如图中的两个表，对应的树形结构中，根节点所在的页面分别是1和2。&lt;/p&gt;&#xA;&lt;p&gt;接着来看不同的页面类型，以及存储上的差异。&lt;/p&gt;&#xA;&lt;p&gt;以一个例子来说明，创建以下的数据库，插入数据，以及索引：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;// 创建数据库COMPANY&#xA;CREATE TABLE COMPANY(&#xA;   ID             INT      NOT NULL,&#xA;   NAME           TEXT    NOT NULL,&#xA;   AGE            INT     NOT NULL,&#xA;   ADDRESS        CHAR(50),&#xA;   SALARY         REAL&#xA;);&#xA;&#xA;// 创建索引&#xA;CREATE INDEX id_index ON COMPANY (id);&#xA;&#xA;// 插入2条数据&#xA;INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (1, &amp;#39;Paul&amp;#39;, 32, &amp;#39;California&amp;#39;, 20000.00 );&#xA;INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY)&#xA;VALUES (2, &amp;#39;Allen&amp;#39;, 25, &amp;#39;Texas&amp;#39;, 15000.00 );&#xA;&#xA;// 查询数据&#xA;sqlite&amp;gt; select * from COMPANY;&#xA;1|Paul|32|California|20000.0&#xA;2|Allen|25|Texas|15000.0&#xA;&#xA;// 查询rowid和数据&#xA;sqlite&amp;gt; select rowid,* from COMPANY;&#xA;1|1|Paul|32|California|20000.0&#xA;2|2|Allen|25|Texas|15000.0&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在上面的流程里：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第3期）：一个前游戏开发者眼中的游戏后端技术</title>
      <link>https://www.codedump.info/zh/post/20220129-weekly-3/</link>
      <pubDate>Sat, 29 Jan 2022 14:21:20 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220129-weekly-3/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：在我之前的工作里，因为各种原因，断续在游戏行业里有过总共大概四年左右的从业时间，今天想从我的视角聊聊游戏行业后端开发相关的技术，供那些想在这个行业从业，尤其是后端开发从业人员一些参考。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;一个前游戏开发者眼中的游戏后端技术&#34;&gt;&#xA;  一个前游戏开发者眼中的游戏后端技术&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%80%e4%b8%aa%e5%89%8d%e6%b8%b8%e6%88%8f%e5%bc%80%e5%8f%91%e8%80%85%e7%9c%bc%e4%b8%ad%e7%9a%84%e6%b8%b8%e6%88%8f%e5%90%8e%e7%ab%af%e6%8a%80%e6%9c%af&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;由于我从业的方向都是后端开发，所以这里仅谈论游戏开发后端的技术。&lt;/p&gt;&#xA;&lt;p&gt;游戏本质是个内容行业，所以游戏开发时相当一部分工作内容，就是不停的更新新的内容出来给玩家消费。这些内容包括但不限于：新的英雄、技能、玩法，等等。&lt;/p&gt;&#xA;&lt;p&gt;而要支撑这些玩法，相当一部分是策划的配表数据，即：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;程序员把玩法的框架、逻辑搭建好，留出读玩法数据的接口来。&lt;/li&gt;&#xA;&lt;li&gt;玩法数据由负责该玩法的策划来配表实现。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这其实就是很朴素的“表驱动”的编程方式。&lt;/p&gt;&#xA;&lt;p&gt;因为这个原因，所以游戏服务器启动时，要加载相当多的数据，主要有：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;玩家的数据，包括账号、角色、帮派、金钱等数据。&lt;/li&gt;&#xA;&lt;li&gt;玩法相关的策划配表数据。比如一个场景的坐标位置、NPC的坐标位置、任务，等等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;由于需要在启动的时候要加载很多数据，游戏服务器才能完成初始化，所以“编译型”语言在游戏开发里并不适用来编写游戏玩法逻辑，试想下面的开发场景：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;策划提出了新的玩法需求。&lt;/li&gt;&#xA;&lt;li&gt;开发使用如C++这样的编译型语言编码实现玩法。&lt;/li&gt;&#xA;&lt;li&gt;编译新修改的代码：这里面有包括了编译、修改编译不通过时候的报错，等等。&lt;/li&gt;&#xA;&lt;li&gt;停止服务器，重启服务器来验证玩法的逻辑。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以看到，姑且不论其他因素，单是每次验证代码修改时需要停服、重启服务器的流程，而重启时又要加载一堆数据才能完成初始化，这个过程就严重影响开发效率。&lt;/p&gt;&#xA;&lt;p&gt;换言之，“编译型”语言并不适合于用来编码在游戏开发里需要经常变更的玩法逻辑。&lt;/p&gt;&#xA;&lt;p&gt;于是，一种新的开发架构出现了，见下图。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;游戏后端的架构分层&#34; src=&#34;https://www.codedump.info/media/imgs/20220129-weekly-3/game-server-arch.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 游戏后端的架构分层 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在图中，将游戏服务器架构分为了两层：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;引擎层：这部分由C++编码，实现了游戏开发中与具体逻辑关系不大、且不太会变更的部分，如网络数据收发、数据库访问，等等。&lt;/li&gt;&#xA;&lt;li&gt;脚本层：这部分由Python、Lua这样的脚本语言实现，主要就是各种玩法。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;采用这样的架构最主要的优点，就是解决前面提到的开发效率问题。由于Python、Lua这样的脚本语言，支持热更新，即“不需要重启进程也能更新最新的代码”，这样开发模式就变成了：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;策划提出了新的玩法需求。&lt;/li&gt;&#xA;&lt;li&gt;开发使用如Python、Lua这样的脚本型语言编码实现玩法。&lt;/li&gt;&#xA;&lt;li&gt;热更新脚本代码，调试玩法。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以看到，由于支持“热更新”，省去了停服、重启、加载数据的步骤，一下子开发效率就提升了很多。&lt;/p&gt;&#xA;&lt;p&gt;多说一句，“热更新”还有一个优点：假如线上出问题时，总不可能停服下来修复，热更新不需要重启就能更新最新代码的特点在这里又发挥了作用。&lt;/p&gt;&#xA;&lt;p&gt;从这个开发架构里，也可以看到游戏服务器人员的构成：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;引擎层只有少数人能够去维护，要求稳定、高效。&lt;/li&gt;&#xA;&lt;li&gt;绝大部分人，都在脚本层用脚本语言来写各种玩法逻辑，类似于web开发中的CRUD。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这个架构已经很久没变化了，而且每个公司都有自己一套（甚至几套）维护得较为稳定的服务器引擎，这会有一个问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;技术演进慢。十几年前的架构到现在基本没变。&lt;/li&gt;&#xA;&lt;li&gt;由于演进慢，而且对稳定性的要求高，实际上新人也很难找到机会在引擎层发挥作用，引擎层的代码通常也不会让脚本层工作的人看到，这样就进入不到这一层的开发，而且引擎层绝大部分的问题已经被前人解决了，用现在的话来说，这部分技术“固化”了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;为了证实我的观点，我找来了去年刚出版的&lt;a href=&#34;https://book.douban.com/subject/35669430/&#34;&gt;《腾讯游戏开发精粹Ⅱ 》&lt;/a&gt;这本书，来看看书里关于游戏服务器都有哪些内容。全书一共21章，与游戏服务器相关的只有三章：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;部分Ⅴ 服务端架构和技术&lt;/p&gt;&#xA;&lt;p&gt;第15章 面向游戏的高性能服务网格TbusppMesh 304&lt;/p&gt;&#xA;&lt;p&gt;15.1　TbusppMesh摘要 304&lt;/p&gt;&#xA;&lt;p&gt;15.2　TbusppMesh数据通信 305&lt;/p&gt;&#xA;&lt;p&gt;15.3　TbusppMesh组网策略 309&lt;/p&gt;&#xA;&lt;p&gt;15.4　TbusppMesh有状态服务 315&lt;/p&gt;&#xA;&lt;p&gt;15.5　总结 321&lt;/p&gt;&#xA;&lt;p&gt;第16章 游戏配置系统设计 322&lt;/p&gt;&#xA;&lt;p&gt;16.1　游戏配置系统概述 322&lt;/p&gt;&#xA;&lt;p&gt;16.2　游戏配置简介 322&lt;/p&gt;&#xA;&lt;p&gt;16.3　游戏配置系统 323&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第2期）：从笔记软件谈被体制化</title>
      <link>https://www.codedump.info/zh/post/20220123-weekly-2/</link>
      <pubDate>Sun, 23 Jan 2022 15:38:31 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220123-weekly-2/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;从笔记软件谈被体制化&#34;&gt;&#xA;  从笔记软件谈被体制化&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%bb%8e%e7%ac%94%e8%ae%b0%e8%bd%af%e4%bb%b6%e8%b0%88%e8%a2%ab%e4%bd%93%e5%88%b6%e5%8c%96&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前两周，一则收购消息，在偌大的中文互联网上，几乎没有掀起任何的讨论：&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://mp.weixin.qq.com/s/luImz3NFwN5zmNHsqvjmDg&#34;&gt;为知笔记并入 ONES，WizNote X 迎来新的征程&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;我是为知笔记的老用户了，从 2011 年就开始使用为知笔记，即便是现在不怎么使用的情况下，也已经把 VIP 会员续费到了 2024 年。在我看来，为知笔记是一个气质与众不同的互联网产品：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;在互联网上几乎没看到这款产品主动来宣传自己，都是靠用户的口碑传播，最开始我也是通过用户的介绍知道这款产品的。&lt;/li&gt;&#xA;&lt;li&gt;是最早支持 Markdown 的笔记本软件，这在我最开始了解 MD 并且开始用这个格式来记录笔记时起了很大的帮助。&lt;/li&gt;&#xA;&lt;li&gt;价格良心。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;所以，当看到为知笔记被收购的新闻，心情还是有些复杂的。我大体知道一些互联网产品的玩法，知道为知笔记应该生存的不太好，被收购了也许对团队是个好事。&lt;/p&gt;&#xA;&lt;p&gt;于是，我开始回想起来，到底是什么样的契机，让我开始慢慢抛弃了传统的笔记软件，以及我现在都用什么方式记录笔记。&lt;/p&gt;&#xA;&lt;p&gt;在 Evernote 刚出来的时候，多端可用、可以收藏文章、记录自己的笔记等等，我也大概是那时候开始记录笔记的，然后就是后来使用网易云笔记，再到主力使用为知笔记。&lt;/p&gt;&#xA;&lt;p&gt;让我逐渐意识到不应该把笔记托管到笔记软件上，有那么几个原因：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;笔记软件之间竞争激烈，可能你从 A 家换到 B 家的产品时，数据的迁移是个大问题，有时候就不得不丢掉一部分难迁移的数据。比如把数据从 Evernote 迁移出来就很麻烦，还好我之前放在这里的笔记也不太用得上了。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Markdown 格式开始流行以后。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;我大概是从2015年开始用MD格式开始写笔记的，这种格式马上就让我爱上了记录笔记：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;格式对比LaTex来说太简单了，只有常见的几种格式，易学易写。&lt;/li&gt;&#xA;&lt;li&gt;纯文本格式，这意味着用任何文本编辑器就能打开。&lt;/li&gt;&#xA;&lt;li&gt;搭配好用的编辑器，能获得”所见即所得“的体验。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;由此，我开始反思之前对笔记本软件的使用：以往都将数据的格式和具体的软件绑定了，这是后面一系列问题（迁移难、文本编辑器打不开）的核心原因。&lt;/p&gt;&#xA;&lt;p&gt;这样，我慢慢意识到，对记录笔记工具这件事情，应该是这样的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;格式通用，且是纯文本格式。&lt;/li&gt;&#xA;&lt;li&gt;由于上面这个限制条件，于是就能将格式和写作软件解除绑定，我能在用不惯一款写作工具的时候，毫不费力的用另一款支持这个通用格式的软件继续写我的笔记。&lt;/li&gt;&#xA;&lt;li&gt;而为知笔记之类的笔记软件，即便后来支持了MD格式，也存储为自己的私有格式，导致了导出数据麻烦。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;我后来也是这样实践的：本地建一个仓库，在里面写MD格式文件，搭配顺手的MD编辑器，需要存储的时候将这个仓库同步到云端。&lt;/p&gt;&#xA;&lt;p&gt;现在看来，能让这个写笔记方式发生很大变化，有这么几个契机：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;MD格式开始流行。&lt;/li&gt;&#xA;&lt;li&gt;由于这个格式开始流行，雨后春笋般出来了很多相关编辑器。用现在的话来说，MD编辑器这个领域开始卷了起来，这样做为用户就有了很多选择，依赖关系反转。&lt;/li&gt;&#xA;&lt;li&gt;微软收购Github之后，创建私人仓库变得轻而易举。（有钱，真的能让用户为所欲为。）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;”笔记格式，应该和笔记软件解绑“，这样我就不会被绑定在一个具体的软件里面了，就不会被一个具体的软件所”体制化“。我怀疑这类想法，很大程度上受到最喜欢的《肖申克的救赎》这部电影的影响：电影里在监狱里生活了几十年的老布，已经被监狱”体制化“，这导致他出去监狱没多久就适应不了不被体制化的生活，选择了上吊自杀。&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;These walls are funny.First you hate them,then you get used to them;Enough time passes,gets so you depend on them.That&amp;rsquo;s institutionalized.&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊(第1期)：开刊，数字化生活数据</title>
      <link>https://www.codedump.info/zh/post/20220116-weekly-1/</link>
      <pubDate>Sun, 16 Jan 2022 10:00:54 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220116-weekly-1/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;为什么会写周刊&#34;&gt;&#xA;  为什么会写周刊&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%ba%e4%bb%80%e4%b9%88%e4%bc%9a%e5%86%99%e5%91%a8%e5%88%8a&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;我为写周刊这个事情思考了小一阵子。&lt;/p&gt;&#xA;&lt;p&gt;列举了好几个优点和缺点。比如，优点有能以输出来倒逼自己的输入，缺点有我的积累是否能够足以支撑我每周写一点东西，以及这种有明确时间限制的输出，是否会给我带来心智负担，等等。再比如，以往的博客内容都是很“硬”的技术内容，周刊不可能也不会每周都更新硬技术文章，会不会导致读者群有点分裂。&lt;/p&gt;&#xA;&lt;p&gt;思考到最后，我还是决定写周刊。&lt;/p&gt;&#xA;&lt;p&gt;从去年开始远程工作，大部分时间都在家里。由于我又很宅，网购也足够的发达，这又让我更宅了。然而，“物理”意义上的活动范围变小，并不意味着“逻辑”上能触达的范围也变小了，实际情况是，由于把通勤时间节省下来，我有更多时间看书、跑步了。所以，我需要一种方式，能输出我看到的一些东西，来与外界发生交流。&lt;/p&gt;&#xA;&lt;p&gt;我在微博上看到了这么两句话，也反向让我坚定了这么做的意义。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;（&lt;a href=&#34;https://weibo.com/1244214071/LagQ8fzm0&#34;&gt;宋一松SYS的微博&lt;/a&gt;）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;社交媒体在我看来最大的价值：它是最开放的peer review system&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://weibo.com/5339148412/L6LhDE7Zu&#34;&gt;硅谷王川的微博&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;让读书产生好处的最简单办法是，一旦有灵感和想法之后，马上写出来，公开发布在社交媒体上，即使不成熟也没关系。写的过程也是自己深度思考的一个步骤，外人的有价值评论可帮你不断推敲，或给你带来新的线索，积累多了自然会出深刻的洞见。一个人孤立封闭的傻读写笔记，很难迅速提高思考深度。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;周刊于我的意义，就是能定期把自己想到、看到的事情都公开出去，反向的让自己定期整理、输出，这是我突破“物理”界限和人交流的手段之一。内容将会以自己的一些想法、业界的动态、推荐、读书、影视等为准，与我写的其他技术文章相比，不会这么“硬”。&lt;/p&gt;&#xA;&lt;p&gt;另外，写周刊还有一个好处，之前有很多想法、念头会散落在各种社交平台这样的新媒体上，实际对整理、汇总、索引等也不好，把念头汇之于文章，用这种更老式的传播方式可以更好得记录下来。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;数字化生活数据&#34;&gt;&#xA;  数字化生活数据&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e5%ad%97%e5%8c%96%e7%94%9f%e6%b4%bb%e6%95%b0%e6%8d%ae&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;既然谈到了周刊做为个人对外输出的一个手段，我就想起最近几年一直在做的事情：数字化各种生活数据。&lt;/p&gt;&#xA;&lt;p&gt;做过一阵子时序数据库的开发，有些人当时并不太了解什么叫做“时序数据”，于是我当时想了一下生活里的类比，其实“时序数据”就好比我们的“流水账”：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;9:00：起床，洗漱。&lt;/li&gt;&#xA;&lt;li&gt;9:30：工作。&lt;/li&gt;&#xA;&lt;li&gt;12:00：午餐。&lt;/li&gt;&#xA;&lt;li&gt;&amp;hellip;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;所谓“数字化生活数据”，也就是按照时间顺序把这些记录下来。“时序数据”，说白了就是时间强相关的数据，以时间为第一维度来存储、查询的数据。&lt;/p&gt;&#xA;&lt;p&gt;“数字化各种生活数据”，这么做的好处有什么？在我看来，有这些好处：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;形成上面的“时序数据”之后，可以方便进行聚合、统计、查询。&lt;/li&gt;&#xA;&lt;li&gt;定期还能依赖于各种工具来进行汇总、回顾等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;比如，我每天都在用的&lt;code&gt;DayOne&lt;/code&gt; app，就有一个“每年今日”的功能，提醒我以往的这一天我都记录了什么，这就是基于这些时序数据的汇总；再比如，每到年底各种app都会自动给用户汇总生成这一年的用户行为统计数据，告诉你最喜欢的歌、和你爱好最匹配的人，等等。&lt;/p&gt;&#xA;&lt;p&gt;这些功能，都依赖于你之前上报过的“时序数据”。&lt;/p&gt;&#xA;&lt;p&gt;今天要推荐的两个相关的开源项目，作者都是&lt;a href=&#34;https://github.com/yihong0618&#34;&gt;yihong0618 (yihong)&lt;/a&gt;，他也是“数字化生活数据”的提倡者，可以在小宇宙里收听他的访问：&lt;a href=&#34;https://www.xiaoyuzhoufm.com/episode/619896e8138b51cbd78f3912&#34;&gt;S01E03 专访 YiHong，自学成为流行开源项目作者的点滴 - 开源面对面 | 小宇宙 - 听播客，上小宇宙&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;第一个项目是&lt;a href=&#34;https://github.com/yihong0618/running_page&#34;&gt;yihong0618/running_page: Make your own running home page&lt;/a&gt;，可以抓取主流的几个跑步app数据，生成好看的跑步数据展示页面：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;&#34; src=&#34;https://user-images.githubusercontent.com/15976103/98808834-c02f1d80-2457-11eb-9a7c-70e91faa5e30.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;第二个项目是&lt;a href=&#34;https://github.com/yihong0618/GitHubPoster&#34;&gt;yihong0618/GitHubPoster: Make everything a GitHub svg poster and Skyline!&lt;/a&gt;，可以将在各种app上（twitter、多邻国、扇贝等）上报的数据可视化：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;&#34; src=&#34;https://github.com/yihong0618/GitHubPoster/raw/main/examples/summary_2021.svg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;&#34; src=&#34;https://github.com/yihong0618/GitHubPoster/raw/main/examples/strava_circular.svg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;可视化展示，会给人很直观的反馈。人的行为如果能得到即时的反馈，某种程度上会有正向作用。以我来说，去年9月份开始跑步，也是fork了这个项目每天生成跑步的可视化数据到我的网站，时不时会看一看数据，知道自己都做了哪些努力，潜移默化的会让我有一些成就感。&lt;/p&gt;&#xA;&lt;p&gt;一个想法：未来会不会有一天，可以拿到一个人所有的数字化数据，在这个人去世之后以这些数据重塑回一个“一样”的人？&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;推荐&#34;&gt;&#xA;  推荐&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%8e%a8%e8%8d%90&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;算法可视化交互动图&#34;&gt;&#xA;  算法可视化交互动图&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ae%97%e6%b3%95%e5%8f%af%e8%a7%86%e5%8c%96%e4%ba%a4%e4%ba%92%e5%8a%a8%e5%9b%be&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;既然这一期讲到了数字化数据之后方便交互演示，就接着推荐旧金山大学制作的系列算法可视化交互动图，包括常见的堆、栈、队列等。学习算法数据结构的时候，如果能图示化的展现其变化过程，理解起来就会更顺畅，在学习B+Tree算法的时候，我就用过这里的演示来理解流程。&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://www.cs.usfca.edu/~galles/visualization/Algorithms.html&#34;&gt;Data Structure Visualization&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>sqlite3.36版本 btree实现（四）- WAL的实现</title>
      <link>https://www.codedump.info/zh/post/20220106-sqlite-btree-4-wal/</link>
      <pubDate>Thu, 06 Jan 2022 21:48:18 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220106-sqlite-btree-4-wal/</guid>
      <description>&lt;p&gt;《sqlite3.36版本 btree实现》系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-1-pagecache/&#34;&gt;sqlite3.36版本 btree实现（一）- 管理页面缓存 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211222-sqlite-btree-3-journal/&#34;&gt;sqlite3.36版本 btree实现（三）- journal文件备份机制 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220106-sqlite-btree-4-wal/&#34;&gt;sqlite3.36版本 btree实现（四）- WAL的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面两节，分别讲解了sqlite中写入事务时的并发控制框架，以及journal备份文件的实现机制。&lt;/p&gt;&#xA;&lt;p&gt;回忆一下journal备份文件的实现：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;每次一个新的写事务开始之前，要首先写journal文件的文件头。&lt;/li&gt;&#xA;&lt;li&gt;写事务过程中，如果修改了哪个页面，在修改之前需要首先将这个页面的内容写入到journal文件中。&lt;/li&gt;&#xA;&lt;li&gt;写事务完成后，在同步所有缓存中被修改的页面到数据库文件之前，要首先将journal文件中的所有修改同步到磁盘，然后再修改数据库文件。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以看到，journal备份的整个流程都较为原始，性能不高，所以在sqlite 3.7.0版本（&lt;a href=&#34;https://www.sqlite.org/releaselog/3_7_0.html&#34;&gt;SQLite Release 3.7.0 On 2010-07-21&lt;/a&gt;，2010-07-21）中，引入了另一种备份机制：WAL（Write Ahead Log）。&lt;/p&gt;&#xA;&lt;p&gt;本节首先介绍WAL的实现原理，然后再展开其具体的实现。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;wal工作原理&#34;&gt;&#xA;  WAL工作原理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#wal%e5%b7%a5%e4%bd%9c%e5%8e%9f%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;从前面journal的实现中可以看到，写入journal文件中的内容，是待修改页面修改之前的内容，而WAL则相反：被修改的页面内容首先写入到WAL中。&lt;/p&gt;&#xA;&lt;p&gt;用sqlite官网的文字来说，WAL文件的定义是这样的：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;The write-ahead log or &amp;ldquo;wal&amp;rdquo; file is a roll-forward journal that records transactions that have been committed but not yet applied to the main database.&lt;/p&gt;</description>
    </item>
    <item>
      <title>etcd 3.5版本的joint consensus实现解析</title>
      <link>https://www.codedump.info/zh/post/20220101-etcd3.5-joint-consensus/</link>
      <pubDate>Sat, 01 Jan 2022 15:02:50 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220101-etcd3.5-joint-consensus/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在以前的etcd实现中，“集群节点变更”这一功能，仅支持每次变更一个节点，最新的etcd已经能支持一次变更多个节点配置的功能了。本文将就这部分的实现进行解析。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;原理&#34;&gt;&#xA;  原理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8e%9f%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;Raft论文《CONSENSUS: BRIDGING THEORY AND PRACTICE》的第四章”集群成员变更“中，支持两种集群变更方式：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;每次变更单节点，即“One Server Config Change”。&lt;/li&gt;&#xA;&lt;li&gt;多节点联合共识，即“Joint Consensus”。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;本文先就这两种实现方式进行原理上的讲解。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;集群节点变更的问题&#34;&gt;&#xA;  集群节点变更的问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%9b%86%e7%be%a4%e8%8a%82%e7%82%b9%e5%8f%98%e6%9b%b4%e7%9a%84%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;要保证Raft协议的安全性，就是要保证任意时刻，集群中只有唯一的&lt;code&gt;leader&lt;/code&gt;节点。如果不加限制条件，那么动态向当前运行集群增删节点的操作，有可能会导致存在多个&lt;code&gt;leader&lt;/code&gt;的情况。如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;集群节点变更问题&#34; src=&#34;https://www.codedump.info/media/imgs/20220101-etcd3.5-joint-Consensus/multi-server.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 集群节点变更问题 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;图中有两种颜色的配置，绿色表示旧的集群配置（&lt;code&gt;C_old&lt;/code&gt;），蓝色表示新的集群配置（&lt;code&gt;C_new&lt;/code&gt;），如果不加任何限制，直接将配置启用，由于不同的集群节点之间，存在时间差，那么可能出现这样的情况：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Server{1,2}：当前都使用旧的集群配置，所以可能选出server1为集群的leader。&lt;/li&gt;&#xA;&lt;li&gt;Server{3,4,5}：当前都使用新的集群配置，可能选出server3为集群的leader。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;由上图可以看到：如果不加任何限制，直接应用新的集群配置，由于时间差的原因，可能导致集群中出现两个不同leader的情况。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;单节点成员变更one-server-confchange&#34;&gt;&#xA;  单节点成员变更（One Server ConfChange）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8d%95%e8%8a%82%e7%82%b9%e6%88%90%e5%91%98%e5%8f%98%e6%9b%b4one-server-confchange&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;“单节点成员变更”，意指每次只添加或删除一个节点，这样就能保证集群的安全性，不会在同一时间出现多个&lt;code&gt;leader&lt;/code&gt;的情况。之所以能有这个保证，是因为每次变更一个节点，那么新旧两种配置的半数节点（majorrity）肯定存在交集。以下图来说明：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;单节点成员变更&#34; src=&#34;https://www.codedump.info/media/imgs/20220101-etcd3.5-joint-Consensus/one-server-confchange.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 单节点成员变更 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图演示了向偶数或奇数的集群增删一个节点的所有可能情况。不论哪种情况，新旧配置都有交集，在每个任期只能投出一张票的情况下，是不会出现多&lt;code&gt;leader&lt;/code&gt;的情况的。&lt;/p&gt;&#xA;&lt;p&gt;有了上面的理论基础，下面来看&lt;code&gt;单节点集群变更&lt;/code&gt;的全流程，当下发集群节点变更配置时，新的配置会以一种特殊的日志方式进行提交，即：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;普通日志：半数通过，提交成功时，会传给应用层的状态机。&lt;/li&gt;&#xA;&lt;li&gt;配置变更类日志：半数通过，提交成功时，集群节点将以新的集群配置生效。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;其流程如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;将集群配置变更数据，序列化为日志数据，需要将日志类型标记为&lt;code&gt;集群配置变更&lt;/code&gt;类的日志，提交给&lt;code&gt;leader&lt;/code&gt;节点。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;leader&lt;/code&gt;节点收到日志后，需要存储该日志的索引为&lt;code&gt;未完成的集群配置变更索引&lt;/code&gt;，像其它正常日志一样处理：先写本地的日志，再广播给集群的其他节点，半数应答则认为日志达成一致可以提交了。如果提交了这类日志，可以将前面保存的&lt;code&gt;未完成的集群配置变更索引&lt;/code&gt;置为空了。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;集群配置变更&lt;/code&gt;日志提交之后，对照新旧的集群变更数据，该添加到集群的添加到集群，该删除的节点停机。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要注意的是，同一时间只能有唯一一个&lt;code&gt;集群变更类日志&lt;/code&gt;存在，怎么保证这一点？就算是在&lt;code&gt;leader&lt;/code&gt;收到该类型日志时，判断&lt;code&gt;未完成的集群配置变更索引&lt;/code&gt;是否为空。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;多节点联合共识joint-consensus&#34;&gt;&#xA;  多节点联合共识（Joint Consensus）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%9a%e8%8a%82%e7%82%b9%e8%81%94%e5%90%88%e5%85%b1%e8%af%86joint-consensus&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;除了上面的单节点变更，有时候还需要一次提交多个节点的变更。但是按照前面的描述，如果一次提交多个节点，很可能会导致集群的安全性被破坏，即同时出现多个&lt;code&gt;leader&lt;/code&gt;的情况。因此，一次提交多节点时，就需要走&lt;code&gt;联合共识&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;所谓的&lt;code&gt;联合共识&lt;/code&gt;，就是将新旧配置的节点一起做为一个节点集合，只有该节点集合达成半数一致，才能认为日志可以提交，由于新旧两个集合做了合并，那么就不会出现多&lt;code&gt;leader&lt;/code&gt;的情况了。具体流程如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;leader&lt;/code&gt;收到成员变更请求，新集群节点集合为&lt;code&gt;C_new&lt;/code&gt;，当前集群节点集合为&lt;code&gt;C_old&lt;/code&gt;，此时首先会以新旧节点集合的交集&lt;code&gt;C_{old,new}&lt;/code&gt;做为一个&lt;code&gt;集群配置变更&lt;/code&gt;类的日志，走正常的日志提交流程。注意，这时候的日志，需要提交到&lt;code&gt;C_{old,new}&lt;/code&gt;中的所有节点。&lt;/li&gt;&#xA;&lt;li&gt;当&lt;code&gt;C_{old,new}&lt;/code&gt;集群变更日志提交之后，&lt;code&gt;leader&lt;/code&gt;节点再马上创建一个只有&lt;code&gt;C_new&lt;/code&gt;节点集合的&lt;code&gt;集群配置变更&lt;/code&gt;类日志，再次走正常的日志提交流程。这时候的日志，只需要提交到&lt;code&gt;C_new&lt;/code&gt;中的所有节点。&lt;/li&gt;&#xA;&lt;li&gt;当&lt;code&gt;C_new&lt;/code&gt;日志被提交之后，集群的配置就能切换到&lt;code&gt;C_new&lt;/code&gt;对应的新集群配置下了。而不在&lt;code&gt;C_new&lt;/code&gt;配置内的节点，将被移除。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以看到，&lt;code&gt;多节点联合共识&lt;/code&gt;的提交流程分为了两次提交：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;先提交新旧集合的交集&lt;code&gt;C_{old,new}&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;再提交新节点集合&lt;code&gt;C_new&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下图来说明，这几个阶段中，集群的安全性都得到了保证：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;多节点联合共识&#34; src=&#34;https://www.codedump.info/media/imgs/20220101-etcd3.5-joint-Consensus/Joint-Consensus.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 多节点联合共识 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;code&gt;C_{old,new}&lt;/code&gt;配置提交之前：在做个阶段，集群中的节点，要么处于&lt;code&gt;C_old&lt;/code&gt;配置下，要么处于&lt;code&gt;C_new,old&lt;/code&gt;配置之下。此时，如果集群的&lt;code&gt;leader&lt;/code&gt;节点宕机，那么将会基于&lt;code&gt;C_old&lt;/code&gt;或者&lt;code&gt;C_new,old&lt;/code&gt;配置来选出新的&lt;code&gt;leader&lt;/code&gt;，而不会仅仅基于&lt;code&gt;C_new&lt;/code&gt;，因此不会选出不同的&lt;code&gt;leader&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;C_{old,new}&lt;/code&gt;配置提交之后，&lt;code&gt;C_new&lt;/code&gt;下发之前：如果这时候&lt;code&gt;leader&lt;/code&gt;宕机，只会基于&lt;code&gt;C_{old,new}&lt;/code&gt;的配置选出&lt;code&gt;leader&lt;/code&gt;，因此也不会选出不同的&lt;code&gt;leader&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;C_new&lt;/code&gt;下发但还未提交时：如果这时候&lt;code&gt;leader&lt;/code&gt;宕机，只会基于&lt;code&gt;C_{old,new}&lt;/code&gt;或者&lt;code&gt;C_new&lt;/code&gt;的配置选出&lt;code&gt;leader&lt;/code&gt;，同时也不再会发给仅仅在&lt;code&gt;C_old&lt;/code&gt;中的节点了，所以无论是哪个配置，都需要得到&lt;code&gt;C_new&lt;/code&gt;的半数同意，因此不会选出不同的&lt;code&gt;leader&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;C_new&lt;/code&gt;提交之后：此时集群中只有一种配置了，安全性得到了保证。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;实现&#34;&gt;&#xA;  实现&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%9e%e7%8e%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;了解了原理之后，可以来具体看etcd 3.5中这部分的实现了。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;learner&#34;&gt;&#xA;  learner&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#learner&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;首先需要了解&lt;code&gt;learner&lt;/code&gt;这个概念，在Raft中，这类型节点有以下特点：&lt;/p&gt;</description>
    </item>
    <item>
      <title>sqlite3.36版本 btree实现（三）- journal文件备份机制</title>
      <link>https://www.codedump.info/zh/post/20211222-sqlite-btree-3-journal/</link>
      <pubDate>Wed, 22 Dec 2021 19:15:31 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20211222-sqlite-btree-3-journal/</guid>
      <description>&lt;p&gt;《sqlite3.36版本 btree实现》系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-1-pagecache/&#34;&gt;sqlite3.36版本 btree实现（一）- 管理页面缓存 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211222-sqlite-btree-3-journal/&#34;&gt;sqlite3.36版本 btree实现（三）- journal文件备份机制 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220106-sqlite-btree-4-wal/&#34;&gt;sqlite3.36版本 btree实现（四）- WAL的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在上一节中（&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架&lt;/a&gt;），已经讲解了sqlite中的并发控制机制，里面会涉及到一个“备份页面”的模块：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;备份所有在一个事务中会修改到的页面。&lt;/li&gt;&#xA;&lt;li&gt;出错时回滚页面内容。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;里面也提到，有两种备份文件的机制：journal文件，以及WAL文件。今天首先讲解journal文件的实现，它的效率会更低一些，也正是因为这个原因后续推出了更优的WAL机制。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;相关命令&#34;&gt;&#xA;  相关命令&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%9b%b8%e5%85%b3%e5%91%bd%e4%bb%a4&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;sqlite中，可以使用&lt;code&gt;PRAGMA journal_mode&lt;/code&gt;来修改备份文件机制，包括以下几种：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;delete：默认模式。在该模式下，在事务结束时，备份文件将被删除。&lt;/li&gt;&#xA;&lt;li&gt;truncate：日志文件被截断为零字节长度。&lt;/li&gt;&#xA;&lt;li&gt;persist：日志文件被留在原地，但头部被重写，表明日志不再有效。&lt;/li&gt;&#xA;&lt;li&gt;memory：日志记录保留在内存中，而不是磁盘上。&lt;/li&gt;&#xA;&lt;li&gt;off：不保留任何备份记录。&lt;/li&gt;&#xA;&lt;li&gt;wal：采用wal形式的备份文件。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;其中，前面三种delete、truncate、persist都是使用journal文件来实现的备份，区别在于事务结束之后的对备份文件的处理罢了。&lt;/p&gt;&#xA;&lt;p&gt;本节首先讲解journal文件，下一节讲解wal备份文件。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;journal文件格式&#34;&gt;&#xA;  journal文件格式&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#journal%e6%96%87%e4%bb%b6%e6%a0%bc%e5%bc%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;journal文件的文件名规则是：与同目录的数据库文件同名，但是多了字符串“-journal”为后缀。比如数据库文件是“test.db”，那么对应的journal文件名为“test.db-journal”。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;文件头&#34;&gt;&#xA;  文件头&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%96%87%e4%bb%b6%e5%a4%b4&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;偏移量&lt;/th&gt;&#xA;          &lt;th&gt;大小&lt;/th&gt;&#xA;          &lt;th&gt;描述&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;0&lt;/td&gt;&#xA;          &lt;td&gt;8&lt;/td&gt;&#xA;          &lt;td&gt;文件头的magic number: 0xd9, 0xd5, 0x05, 0xf9, 0x20, 0xa1, 0x63, 0xd7&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;8&lt;/td&gt;&#xA;          &lt;td&gt;4&lt;/td&gt;&#xA;          &lt;td&gt;journal文件中的页面数量，如果为-1表示一直到journal文件尾&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;12&lt;/td&gt;&#xA;          &lt;td&gt;4&lt;/td&gt;&#xA;          &lt;td&gt;每次计算校验值时算出来的随机数&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;16&lt;/td&gt;&#xA;          &lt;td&gt;4&lt;/td&gt;&#xA;          &lt;td&gt;在开始备份前数据库文件的页面数量&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;20&lt;/td&gt;&#xA;          &lt;td&gt;4&lt;/td&gt;&#xA;          &lt;td&gt;磁盘扇区大小&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;24&lt;/td&gt;&#xA;          &lt;td&gt;4&lt;/td&gt;&#xA;          &lt;td&gt;journal文件中的页面大小&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这里大部分的字段都自解释了，不必多做解释，唯一需要注意的是随机数，因为这是用来后续校验备份页面的字段，这将在后面结合流程来说明。&lt;/p&gt;</description>
    </item>
    <item>
      <title>sqlite3.36版本 btree实现（二）- 并发控制框架</title>
      <link>https://www.codedump.info/zh/post/20211218-sqlite-btree-2-concurrency-control/</link>
      <pubDate>Sat, 18 Dec 2021 15:25:05 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20211218-sqlite-btree-2-concurrency-control/</guid>
      <description>&lt;p&gt;《sqlite3.36版本 btree实现》系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-1-pagecache/&#34;&gt;sqlite3.36版本 btree实现（一）- 管理页面缓存 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211222-sqlite-btree-3-journal/&#34;&gt;sqlite3.36版本 btree实现（三）- journal文件备份机制 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220106-sqlite-btree-4-wal/&#34;&gt;sqlite3.36版本 btree实现（四）- WAL的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;按照之前起步阶段对sqlite btree整体架构的分析，“页面管理模块”分为以下几个子模块：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;页面缓存管理。&lt;/li&gt;&#xA;&lt;li&gt;页面备份，又分为以下两种实现：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;journal文件。&lt;/li&gt;&#xA;&lt;li&gt;WAL文件。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;页面管理模块。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;前面一节讲完了“页面缓存管理”的实现，按照自下往上的顺序，就应该到“页面备份”了。“页面备份”核心的工作是：在真正修改页面内容之前，将还未修改的页面内容备份，这样一旦系统在事务过程中宕机崩溃，就可以用这部分内容回滚还未落盘的事务修改，让系统回到一个正确的状态。&lt;/p&gt;&#xA;&lt;p&gt;“页面备份”有两种实现方式，在早期使用的journal文件，这种方式性能不高；在3.7版本之后，sqlite引入了WAL文件来保存页面内容，这样做的效率更高。&lt;/p&gt;&#xA;&lt;p&gt;本节就讲解这部分内容，在对这部分内容有一个总体的了解之后，继续讲解页面备份的总体流程。后面的章节再具体分析journal以及WAL的实现。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;写事务的流程&#34;&gt;&#xA;  写事务的流程&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%86%99%e4%ba%8b%e5%8a%a1%e7%9a%84%e6%b5%81%e7%a8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;（以下流程分析，按照sqlite官网中的文档&lt;a href=&#34;https://sqlite.org/atomiccommit.html&#34;&gt;Atomic Commit In SQLite&lt;/a&gt;进行讲解，图例也全部引用自官网。）&lt;/p&gt;&#xA;&lt;p&gt;sqlite的写事务，分为以下几个流程：&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;1初始化阶段initial-state&#34;&gt;&#xA;  1、初始化阶段（Initial State）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#1%e5%88%9d%e5%a7%8b%e5%8c%96%e9%98%b6%e6%ae%b5initial-state&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;初始化&#34; src=&#34;https://www.codedump.info/media/imgs/20211218-sqlite-btree-2-concurrency-control/commit-0.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 初始化 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图中，从右到左即是系统的磁盘、操作系统缓冲区、用户空间三部分，其中磁盘和操作系统缓冲区有划分为多块的空间，每一块在sqlite里被称为一个&lt;code&gt;sector&lt;/code&gt;，蓝色部分表示是修改之前的数据。&lt;/p&gt;&#xA;&lt;p&gt;这是系统初始时的样子。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;2拿到读锁acquiring-a-read-lock&#34;&gt;&#xA;  2、拿到读锁（Acquiring A Read Lock）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#2%e6%8b%bf%e5%88%b0%e8%af%bb%e9%94%81acquiring-a-read-lock&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;拿到读锁&#34; src=&#34;https://www.codedump.info/media/imgs/20211218-sqlite-btree-2-concurrency-control/commit-1.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 拿到读锁 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在开始进行写操作之前，sqlite必须先把待修改的页面加载内存中（这就是上一节“页面缓存管理器”做的事情），后续的修改其实也是首先修改这部分加载到内存中的页面内容，因为可能一次提交会修改同一个页面中的多处内容，最后才把页面内容落盘。&lt;/p&gt;&#xA;&lt;p&gt;所以，这一步所要做的，是首先拿到数据库文件的读锁（shared lock），需要说明的是，这个读锁是数据库级别的锁。同一时间，系统中可以存在多个读锁，但是只要系统中还存在读锁，就不再允许分配出新的写锁（write lock）。&lt;/p&gt;</description>
    </item>
    <item>
      <title>sqlite3.36版本 btree实现（一）- 管理页面缓存</title>
      <link>https://www.codedump.info/zh/post/20211217-sqlite-btree-1-pagecache/</link>
      <pubDate>Fri, 17 Dec 2021 14:22:06 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20211217-sqlite-btree-1-pagecache/</guid>
      <description>&lt;p&gt;《sqlite3.36版本 btree实现》系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-1-pagecache/&#34;&gt;sqlite3.36版本 btree实现（一）- 管理页面缓存 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211222-sqlite-btree-3-journal/&#34;&gt;sqlite3.36版本 btree实现（三）- journal文件备份机制 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220106-sqlite-btree-4-wal/&#34;&gt;sqlite3.36版本 btree实现（四）- WAL的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;&lt;code&gt;页面管理&lt;/code&gt;模块中，很重要的一个功能是缓存页面的内容在内存中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;读页面：如果页面已经在内存，就不需要到文件中读出页面内容。&lt;/li&gt;&#xA;&lt;li&gt;写页面：如果页面已经在内存，那么对页面的修改就只需要修改页面在内存中的数据即可，被修改了但是还没有落盘的页面，被称为“脏页面（dirty page）“。这样，多次对某个页面的修改，可能最后只需要一次落盘即可。当然，对页面的修改，如果在还没有落盘之前，系统就崩溃了，这种情况下应该如何处理，这就是“崩溃恢复”模块做的事情了。本节中，将专注在“页面缓存”这个子模块的实现。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;既然要将页面缓存在内存中，就会涉及到几个功能：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如何知道哪些页面已经被缓存在内存里了？&lt;/li&gt;&#xA;&lt;li&gt;缓存在内存中的页面如何组织管理？&lt;/li&gt;&#xA;&lt;li&gt;缓存页面使用的内存不够用时，应该如何处理？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;我们首先来了解一下“页面缓存”模块的总体划分：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;页面缓存功能的模块划分&#34; src=&#34;https://www.codedump.info/media/imgs/20211217-sqlite-btree-1-pagecache/pagecache.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 页面缓存功能的模块划分 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;按照上图的划分，页面缓存模块分为以下几部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;页面缓存管理器：实现了页面缓存的总体算法流程，以及提供对外的接口，但是具体到“页面缓存算法”的实现，则有赖于下面这个可用户定制的&lt;code&gt;sqlite3_pcache_methods2&lt;/code&gt;。这部分功能在代码&lt;code&gt;pcache.c&lt;/code&gt;中。&lt;/li&gt;&#xA;&lt;li&gt;页面缓存算法：用户可自己定制，只要实现&lt;code&gt;sqlite3_pcache_methods2&lt;/code&gt;结构体中的接口即可。系统中的默认实现，在文件&lt;code&gt;pcache1.c&lt;/code&gt;中。&lt;/li&gt;&#xA;&lt;li&gt;除此以外，还需要快速根据页面编号就能知道哪些页面已经被缓存的功能，这部分sqlite使用位图数据结构来实现，在文件&lt;code&gt;bitvec.c&lt;/code&gt;中。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;页面缓存管理器，核心功能就是维护脏页面链表，缓存页面的管理，诸如根据页面编号查找页面、淘汰页面算法等，都由“页面缓存算法”来维护。可以这样来简单的理解上面的功能划分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;“页面缓存管理器”：定义了管理页面缓存的接口、总体流程，维护管理目前在用的脏页面。&lt;/li&gt;&#xA;&lt;li&gt;“页面缓存算法”：维护其它不在使用但还在内存中的页面，负责其淘汰、回收等实现。由“sqlite3_pcache_methods2”结构体实现，用户可以定制自己实现的“sqlite3_pcache_methods2”，系统也提供默认的实现。当内存不足以分配时，需要淘汰不常用的页面，这时候需要使用“页面缓存管理器”注册的回调函数来淘汰页面。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;页面缓存两个模块的功能划分&#34; src=&#34;https://www.codedump.info/media/imgs/20211217-sqlite-btree-1-pagecache/pagecache_module.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 页面缓存两个模块的功能划分 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;简而言之，如果把当前在内存中的页面划分为以下两类，那么：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;当前在使用的页面：即与页面编号对应的页面，由“页面缓存管理器”维护。&lt;/li&gt;&#xA;&lt;li&gt;当前还未使用、但也在内存中的页面：即随时准备拿出来存储从磁盘中读出来的数据的页面，由“页面缓存算法”维护，比如淘汰、回收、复用等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;两种页面的划分&#34; src=&#34;https://www.codedump.info/media/imgs/20211217-sqlite-btree-1-pagecache/page_cache_memory.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 两种页面的划分 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;下面，就开始“页面缓存”这几部分功能的具体讲解。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;管理页面&#34;&gt;&#xA;  管理页面&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ae%a1%e7%90%86%e9%a1%b5%e9%9d%a2&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;页面相关的数据数据结构&#34;&gt;&#xA;  页面相关的数据数据结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%a1%b5%e9%9d%a2%e7%9b%b8%e5%85%b3%e7%9a%84%e6%95%b0%e6%8d%ae%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;首先来看页面相关的数据结构，sqlite中使用&lt;code&gt;PgHdr&lt;/code&gt;结构体来在内存中描述一个页面：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;/*&#xA;** Every page in the cache is controlled by an instance of the following&#xA;** structure.&#xA;*/&#xA;struct PgHdr {&#xA;  sqlite3_pcache_page *pPage;    /* Pcache object page handle */&#xA;  void *pData;                   /* Page data */&#xA;  void *pExtra;                  /* Extra content */&#xA;  PCache *pCache;                /* PRIVATE: Cache that owns this page */&#xA;  PgHdr *pDirty;                 /* Transient list of dirty sorted by pgno */&#xA;  Pager *pPager;                 /* The pager this page is part of */&#xA;  Pgno pgno;                     /* Page number for this page */&#xA;#ifdef SQLITE_CHECK_PAGES&#xA;  u32 pageHash;                  /* Hash of page content */&#xA;#endif&#xA;  u16 flags;                     /* PGHDR flags defined below */&#xA;&#xA;  /**********************************************************************&#xA;  ** Elements above, except pCache, are public.  All that follow are &#xA;  ** private to pcache.c and should not be accessed by other modules.&#xA;  ** pCache is grouped with the public elements for efficiency.&#xA;  */&#xA;  i16 nRef;                      /* Number of users of this page */&#xA;  PgHdr *pDirtyNext;             /* Next element in list of dirty pages */&#xA;  PgHdr *pDirtyPrev;             /* Previous element in list of dirty pages */&#xA;                          /* NB: pDirtyNext and pDirtyPrev are undefined if the&#xA;                          ** PgHdr object is not dirty */&#xA;};&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中的信息，大部分在注释中已经自解释：&lt;/p&gt;</description>
    </item>
    <item>
      <title>sqlite3.36版本 btree实现（零）- 起步及概述</title>
      <link>https://www.codedump.info/zh/post/20211217-sqlite-btree-0/</link>
      <pubDate>Fri, 17 Dec 2021 10:19:05 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20211217-sqlite-btree-0/</guid>
      <description>&lt;p&gt;《sqlite3.36版本 btree实现》系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-1-pagecache/&#34;&gt;sqlite3.36版本 btree实现（一）- 管理页面缓存 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211222-sqlite-btree-3-journal/&#34;&gt;sqlite3.36版本 btree实现（三）- journal文件备份机制 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220106-sqlite-btree-4-wal/&#34;&gt;sqlite3.36版本 btree实现（四）- WAL的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;起步&#34;&gt;&#xA;  起步&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%b5%b7%e6%ad%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在去年大体把btree以及b+tree算法流程研究了之后，我写了两篇博客：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;（鉴于b+tree只是btree的一个特例，下面描述将仅使用“btree”，不再严格区分两者。）&lt;/p&gt;&#xA;&lt;p&gt;但是，这两篇文章仅仅只是让我懂得了最基本的原理。懂得原理，只是能做出toy级别的实现，拿btree类的存储引擎来说，要做到生产级产品，至少还有以下几个问题我当时不知道怎么做的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如何处理不同大小的数据的存储？&lt;/li&gt;&#xA;&lt;li&gt;删除一个数据之后，如何复用其留下的空间？&lt;/li&gt;&#xA;&lt;li&gt;错误、崩溃恢复怎么做？&lt;/li&gt;&#xA;&lt;li&gt;跟磁盘文件是如何交互的？&lt;/li&gt;&#xA;&lt;li&gt;页面缓存模块如何实现？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;等等等等，还有太多我还没有弄清楚的实现细节。&lt;/p&gt;&#xA;&lt;p&gt;（我甚至还在微博上发问，得到了两个质量很高的回答，见本文最后的&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/#%E5%BD%A9%E8%9B%8B&#34;&gt;彩蛋部分&lt;/a&gt;。）&lt;/p&gt;&#xA;&lt;p&gt;对LSM类存储引擎有了解的人都知道，Leveldb这个项目在LSM领域属于入门级别的生产级实现，即这个领域最精简、但是又能放心在某些要求不高的场景下用于生产的项目。在这之后，我一直在找那种btree领域的“leveldb”，很遗憾一直都没有找到，我分别看了目前WiredTiger、innodb、sqlite的对应实现，都太复杂了，看不下去。&lt;/p&gt;&#xA;&lt;p&gt;直到有一天，无意间发现了这个项目：&lt;a href=&#34;https://github.com/madushadhanushka/simple-sqlite&#34;&gt;madushadhanushka/simple-sqlite: Code reading for sqlite backend&lt;/a&gt;，看介绍，作者把sqlite2.5里b-tree相关的部分代码抽取出来了，我编译运行了一下用例都能正常跑，代码量不过几千行，我只花了几天就看完了。&lt;/p&gt;&#xA;&lt;p&gt;虽然按照&lt;a href=&#34;https://www.sqlite.org/changes.html&#34;&gt;Release History Of SQLite&lt;/a&gt;上的记载，sqlite 2.5版本是2002年的版本了，但是这个版本还是某种程度回答了我在上面的疑问。&lt;/p&gt;&#xA;&lt;p&gt;趁热打铁，我又找来更新一些的sqlite 3.6.10代码继续看这部分的实现，这次花了更多的时间才看完，但是又增强了我的信心。由于这个版本的sqlite，还未实现btree的wal，还只是用了journal文件来做崩溃恢复（无论wal还是journal，都会在后面文章展开详细讨论），所以在有足够的信心之后，我接下来又继续看当时（2021.10月份）最新的sqlite 3.36版本的实现，这部分的实现对比3.6.10来说，在btree部分最大的变化就是多了wal的实现，在已经清楚3.6.10的前提下，再增加了解这部分的实现，也并不是什么难事了。&lt;/p&gt;&#xA;&lt;p&gt;以上，简单描述了我探索一个生产级btree实现的初过程，btree类存储引擎的实现博大精深，更复杂者还有很多（WiredTiger、innodb、tokudb&amp;hellip;），但是无疑从低版本sqlite开始的探索流程，终于让我打开了走上这条路的一扇大门。&lt;/p&gt;&#xA;&lt;p&gt;本系列文章就sqlite 3.36版本的btree实现展开描述，希望对那些和我一样对“生产级btree类存储引擎实现”有好奇心的人有一点帮助。&lt;/p&gt;&#xA;&lt;p&gt;当然，如果你还是觉得吃力，可以先从&lt;a href=&#34;https://github.com/madushadhanushka/simple-sqlite&#34;&gt;madushadhanushka/simple-sqlite: Code reading for sqlite backend&lt;/a&gt;这里看起。这里并不建议对btree原理没有了解的人直接上手sqlite的实现，如果需要了解原理请参考相关文章或者我上面给出的我写的两篇博客。这系列文章中，将不再对btree原理做过多描述，将假设读者已经了解这部分内容。&lt;/p&gt;</description>
    </item>
    <item>
      <title>为什么Raft协议不能提交之前任期的日志？</title>
      <link>https://www.codedump.info/zh/post/20211011-raft-propose-prev-term/</link>
      <pubDate>Mon, 11 Oct 2021 23:14:01 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20211011-raft-propose-prev-term/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在Raft大论文中3.6.2中，有一个细节“不允许提交之前任期的日志”，之前看了几次都理解的不够准确，把这部分内容展开阐述一下。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;问题&#34;&gt;&#xA;  问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;还是先从论文的图例开始解释，如下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;论文截图&#34; src=&#34;https://www.codedump.info/media/imgs/20211011-raft-propose-prev-term/propose-prev-term.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 论文截图 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;需要特别说明的是，图例中演示的是**“如果允许提交之前任期的日志，将导致什么问题”**，这是大前提，这个前提条件后面会反复强调。&lt;/p&gt;&#xA;&lt;p&gt;有了这个前提，下面展开图中的步骤讨论：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;(a) ：S1 是leader，将黄色的日志2同步到了S2，然后S1崩溃。&lt;/li&gt;&#xA;&lt;li&gt;(b) ：S5 在任期 3 里通过 S3、S4 和自己的选票赢得选举，将蓝色日志3存储到本地，然后崩溃了。&lt;/li&gt;&#xA;&lt;li&gt;\(c\)：S1重新启动，选举成功。注意在这时，&lt;strong&gt;如果允许“提交之前任期的日志”&lt;/strong&gt;，将首先开始同步过往任期的日志，即将S1上的本地黄色的日志2同步到了S3。这时黄色的节点2已经同步到了集群多数节点，然后S1写了一条新日志4，然后S1又崩溃了。&lt;/li&gt;&#xA;&lt;li&gt;接下来，就可能出现两种不同的情况：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;（d1）：S5重新当选，&lt;strong&gt;如果允许“提交之前任期的日志”&lt;/strong&gt;，就开始同步往期日志，将本地的蓝色日志3同步到所有的节点。结果已经被同步到半数以上节点的黄色日志2被覆盖了。这说明，如果允许“提交之前任期的日志”，会可能出现即便已经同步到半数以上节点的日志被覆盖，这是不允许的。&lt;/li&gt;&#xA;&lt;li&gt;（d2）：反之，如果在崩溃之前，S1不去同步往期的日志，而是首先同步自己任期内的日志4到所有节点，就不会导致黄色日志2被覆盖。因为leader同步日志的流程中，会通过不断的向后重试的方式，将日志同步到其他所有follower，只要日志4被复制成功，在它之前的日志2就会被复制成功。（d2）是想说明：不能直接提交过往任期的日志，即便已经被多数通过，但是可以先同步一条自己任内的日志，如果这条日志通过，就能带着前面的日志一起通过，这是（c）和（d2）两个图的区别。图（c）中，S1先去提交过往任期的日志2，图（d2）中，S1先去提交自己任内的日志4。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;再次强调，这里图示想演示的是**“如果允许提交之前任期的日志，将导致什么问题”**。&lt;/p&gt;&#xA;&lt;p&gt;我们可以看到的是，如果允许这么做，那么：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;\(c\)中，S1恢复之后，又再次提交在任期2中的黄色日志2。但是，从后面可以看到，即便这个之前任期中的黄色日志2，提交到大部分节点，如果允许“提交之前任期的日志”，仍然存在被覆盖的可能性，因为：&lt;/li&gt;&#xA;&lt;li&gt;(d1)中，S5恢复之后，也会提交在自己本地上保存的之前任期3的蓝色日志，这会导致覆盖了前面已经到半数以上节点的黄色日志2。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;所以，“如果允许提交之前任期的日志”，即如同\(c\)和(d1)演示的那样：重新当选之后，马上提交自己本地保存的、之前任期的日志，就会&lt;strong&gt;可能导致即便已经同步到半数以上节点的日志，被覆盖的情况&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;而“已同步到半数以上节点的日志”，一定在新当选leader上（否则这个节点不可能成为新leader）且达成了一致可提交，即不允许被覆盖。&lt;/p&gt;&#xA;&lt;p&gt;这就是矛盾的地方，即允许“提交之前任期的日志”，最终导致了违反协议规则的情况。&lt;/p&gt;&#xA;&lt;p&gt;那么，如何确保新当选的leader节点，其本地的未提交日志被正确提交呢？图(d2)展示了正常的情况：即当选之后，不要首先提交本地已有的黄色日志2，而是首先提交一条新日志4，如果这条新日志被提交成功，那么按照Raft日志的匹配规则（log matching property）：日志4如果能提交，它前面的日志也提交了。&lt;/p&gt;&#xA;&lt;p&gt;可是，新的问题又出现了，如果在(d2)中，S1重新当选之后，客户端写入没有这条新的日志4，那么前面的日志2是不是永远无法提交了？为了解决这个问题，raft要求每个leader新当选之后，马上写入一条只有任期号和索引、而没有内容的所谓“no-op”日志，以这条日志来驱动在它之前的日志达成一致。&lt;/p&gt;&#xA;&lt;p&gt;这就是论文中这部分内容想要表达的。这部分内容之所以比较难理解，是因为经常忽略了这个图示展示的是错误的情况，允许“提交之前任期的日志”可能导致的问题。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;其他疑问&#34;&gt;&#xA;  其他疑问&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%85%b6%e4%bb%96%e7%96%91%e9%97%ae&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;和-有什么区别&#34;&gt;&#xA;  \(c\)和\(d2\) 有什么区别？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%92%8c-%e6%9c%89%e4%bb%80%e4%b9%88%e5%8c%ba%e5%88%ab&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;看起来，\(c\)和\(d2\)一样，S1当选后都提交了日志1、2、4，那么两者的区别在哪里？&lt;/p&gt;&#xA;&lt;p&gt;虽然两个场景中，提交的日志都是一样的，但是日志达成一致的顺序并不一致：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;\(c\)：S1成为leader之后，先提交过往任期、本地的日志2，再提交日志4。这就是“提交之前任期日志”的情况。&lt;/li&gt;&#xA;&lt;li&gt;\(d2\)：S1成为leader之后，先提交本次任期的日志4，如果日志4能提交成功，那么它前面的日志2就能提交成功了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;关于\(d2\)的这个场景，有可能又存在着下一个疑问：&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;如何理解中本任期的日志4提交成功那么它前面的日志2也能提交成功了&#34;&gt;&#xA;  如何理解\(d2\)中，“本任期的日志4提交成功，那么它前面的日志2也能提交成功了”？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a6%82%e4%bd%95%e7%90%86%e8%a7%a3%e4%b8%ad%e6%9c%ac%e4%bb%bb%e6%9c%9f%e7%9a%84%e6%97%a5%e5%bf%974%e6%8f%90%e4%ba%a4%e6%88%90%e5%8a%9f%e9%82%a3%e4%b9%88%e5%ae%83%e5%89%8d%e9%9d%a2%e7%9a%84%e6%97%a5%e5%bf%972%e4%b9%9f%e8%83%bd%e6%8f%90%e4%ba%a4%e6%88%90%e5%8a%9f%e4%ba%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;这是由raft日志的&lt;code&gt;Log Matching Property&lt;/code&gt;决定的:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;If two entries in different logs have the same index and term, then they store the same command.&#xA;If two entries in different logs have the same index and term, then the logs are identical in all preceding entries.&lt;/li&gt;&#xA;&lt;li&gt;If two entries in different logs have the same index and term, then the logs are identical in all preceding entries.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;第一条性质，说明的是在不同节点上的已提交的日志，如果任期号、索引一样，那么它们的内容肯定一样。这是由leader节点的安全性和leader上的日志只能添加不能覆盖来保证的，这样leader就永远不会在同一个任期，创建两个相同索引的日志。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Memcached的存储原理解析（续）</title>
      <link>https://www.codedump.info/zh/post/20210812-memcached/</link>
      <pubDate>Thu, 12 Aug 2021 08:40:04 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20210812-memcached/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在前面的&lt;a href=&#34;https://www.codedump.info/post/20210701-memcached/&#34;&gt;Memcached的存储原理解析&lt;/a&gt;一文中，简单分析了memcached的存储原理，但是最近在照搬memcached的实现原理到项目中时，发现前面的梳理还不够细致，有一些细节没有谈及，因此重新整理一篇文章。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;slab&#34;&gt;&#xA;  slab&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#slab&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;memcached是根据slab为基础单位来管理空闲空间的。slab的大体原理如下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;slabclass的分级存储&#34; src=&#34;https://www.codedump.info/media/imgs/20210812-memcached/slabclass.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; slabclass的分级存储 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;slabs.c中定义了类型为&lt;code&gt;slabclass_t&lt;/code&gt;、大小为&lt;code&gt;MAX_NUMBER_OF_SLAB_CLASSES&lt;/code&gt;的数组&lt;code&gt;slabclass&lt;/code&gt;，用于分级存储。&lt;/p&gt;&#xA;&lt;p&gt;数组中的每个&lt;code&gt;slabclass_t&lt;/code&gt;元素，其能分配出去的内存大小递增，由如下的规则决定：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;每个数组可分配的内存大小都要8字节对齐（&lt;code&gt;CHUNK_ALIGN_BYTES&lt;/code&gt;）,这个大小保存在&lt;code&gt;slabclass_t&lt;/code&gt;的&lt;code&gt;size&lt;/code&gt;成员中。&lt;/li&gt;&#xA;&lt;li&gt;数组的第一个&lt;code&gt;slabclass_t&lt;/code&gt;元素的可分配内存大小为&lt;code&gt;sizeof(item) + settings.chunk_size&lt;/code&gt;。这之后的&lt;code&gt;slabclass_t&lt;/code&gt;可分配内存大小，都在上一个的元素的基础上放大&lt;code&gt;factor&lt;/code&gt;倍，同时还要8字节对齐。&lt;/li&gt;&#xA;&lt;li&gt;每次分配一个页面的大小由配置项&lt;code&gt;settings.slab_page_size&lt;/code&gt;来决定，因此每一个&lt;code&gt;slabclass_t&lt;/code&gt;元素的一个页面能容纳的&lt;code&gt;item&lt;/code&gt;数量为&lt;code&gt;settings.slab_page_size / slabclass[i].size&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以上图为例，假设第一级存储的item大小不超过56字节，每个slab之间的增长因子是1.2，那么下一个slab存储的item内存大小就是56*1.2=72字节。&lt;/p&gt;&#xA;&lt;p&gt;在当前还有空闲可用内存的情况下，每一次分配新的空间，都是以page（page=1MB）为单位的，然后再根据该slab的item大小划分为多个空闲可用item。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;slabclass_t&lt;/code&gt;类型中最重要的是以下两个成员：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;slab_list：保存已经分配出去的page数组，分配一个page的内存之后，需要将page根据该slab的size划分成多个空闲的item，挂载到下面提到的slots链表中。当最后需要回收分配出去的内存时，直接遍历slab_list中的成员回收内存即可。&lt;/li&gt;&#xA;&lt;li&gt;slots：保存空闲item链表。空闲item来源有两部分，一部分是从page中分配但是还未使用的item，还有一部分是曾经被使用后来释放回来的item，上图中使用了不同的颜色进行了区分。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;当需要分配一块大小的内存时，首先需要根据其大小，计算出该尺寸最终对应到上面的哪个元素，这个数组索引在Memcached中被称为&lt;code&gt;clsid&lt;/code&gt;，这个计算索引的过程参见函数&lt;code&gt;slabs_clsid&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;比如：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;slabclass[0].size = 56，fator参数为1.2，那么slabclass[1].size = (56 * 1.25)向上对齐8位 = 72，以此类推。&lt;/li&gt;&#xA;&lt;li&gt;假设需要分配的内存大小为60，就会去找&lt;code&gt;slabclass_t.size &amp;gt;= 60&lt;/code&gt;的第一个slabclass，在这个例子中返回的&lt;code&gt;clsid&lt;/code&gt;是1，也就是&lt;code&gt;slabclass[1]&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;内存分配时根据大小向上取满足条件的第一个slab的做法，优点在于方便了内存的分配管理，缺陷是会浪费掉部分空间，比如上面的例子中，将大小为72的slab用于60的内存，那么12字节的空间就被浪费掉了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从上面可以看到，&lt;code&gt;slabclass_t&lt;/code&gt;用于管理空闲内存，当需要分配新item时，会依次做如下的检查：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如果&lt;code&gt;slots&lt;/code&gt;链表中还有空闲item，直接摘下来使用；&lt;/li&gt;&#xA;&lt;li&gt;否则，如果当前还没有达到内存分配的阈值，就分配一个新的page出来，将page按照该slab的大小划分为多个item，这些新分配出来的item都挂载到&lt;code&gt;slots&lt;/code&gt;链表中。&lt;/li&gt;&#xA;&lt;li&gt;如果以上两步都不满足了，说明当前已经没有可用的内存和空闲item，需要进行淘汰了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;讲到item的淘汰，就涉及到下面的LRU算法了。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;lru算法&#34;&gt;&#xA;  LRU算法&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#lru%e7%ae%97%e6%b3%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;旧的lru算法及其问题&#34;&gt;&#xA;  旧的LRU算法及其问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%97%a7%e7%9a%84lru%e7%ae%97%e6%b3%95%e5%8f%8a%e5%85%b6%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;以往的LRU算法，基本做法都是这样的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;创建一个LRU链表，每次新加入的元素都放在链表头。&lt;/li&gt;&#xA;&lt;li&gt;如果元素被访问了一次，同样从当前链表中摘除放到链表头。&lt;/li&gt;&#xA;&lt;li&gt;需要淘汰元素时，从链表尾开始找可以淘汰的元素出来淘汰。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这个算法有如下几个问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;元素被访问一次就会被放到LRU链表的头部，这样即便这个元素可以被淘汰，也会需要很久才会淘汰掉这个元素。&lt;/li&gt;&#xA;&lt;li&gt;由于上面的原因，从链表尾部开始找可以淘汰的元素时，实际可能访问到的是一些虽然不常被访问，但是还没到淘汰时间（即有效时间TTL还未过期）的数据，这样会一直沿着链表往前找很久才能找到适合淘汰的元素。由于这个查找被淘汰元素的过程是需要加锁保护的，加锁时间一长影响了系统的并发。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;经典的LRU链表实现&#34; src=&#34;https://www.codedump.info/media/imgs/20210701-memcached/old-lru-list.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 经典的LRU链表实现 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;综上，经典的LRU链表问题的核心在于：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;只需要一次被访问就能让元素远离被淘汰的地方。&lt;/li&gt;&#xA;&lt;li&gt;以及如何高效定位到更可能被淘汰的元素。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从Memcached 1.5版本开始，引入了所谓的分段LRU算法（Segmented LRU）来解决这些问题。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;改进的分段lru算法segmented-lru&#34;&gt;&#xA;  改进的分段LRU算法（Segmented LRU）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%94%b9%e8%bf%9b%e7%9a%84%e5%88%86%e6%ae%b5lru%e7%ae%97%e6%b3%95segmented-lru&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;分段LRU算法中将LRU链表根据&lt;code&gt;活跃度&lt;/code&gt;分成了三类：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;HOT_LRU：存储热数据的LRU链表。&lt;/li&gt;&#xA;&lt;li&gt;WARM_LRU：存储温数据（即活跃度不如热数据）的LRU链表。&lt;/li&gt;&#xA;&lt;li&gt;COLD_LRU：存储冷数据的LRU链表。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要说明的是：热（参数&lt;code&gt;settings.hot_lru_pct&lt;/code&gt;）和暖（参数&lt;code&gt;settings.warm_lru_pct&lt;/code&gt;）数据的占总体内存的比例有限制，而冷数据则无限。&lt;/p&gt;</description>
    </item>
    <item>
      <title>选择的维度</title>
      <link>https://www.codedump.info/zh/post/20210803-choice-dimension/</link>
      <pubDate>Tue, 03 Aug 2021 12:10:01 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20210803-choice-dimension/</guid>
      <description>&lt;p&gt;工作这些年之后，我将一份工作中满意度的指标大体划分到三个维度里面：钱、事情和人。一份工作，如果有两个维度都能较好的满足自己标准的，就算是一份不错的工作了；反之，只有一个维度满意，这时毫不犹豫就应该换一份工作；三个维度都能很好的工作，可遇而不可求。&lt;/p&gt;&#xA;&lt;p&gt;虽然工作满意度指标可以划分为这三个维度，但是具体到个人身上，每个维度的权重又不尽相同。比如有的人家里条件不错，可能就不会把钱这个维度看的太过重要；另外，每个人对每个维度的满意程度标准又是不一样的。这些，都需要具体情况具体分析，但是将你工作中最在意的部分划分到这三个维度来反问自己对工作的满意程度，是一切的开始。&lt;/p&gt;&#xA;&lt;p&gt;我在年初换了一份工作，本来还有过一些犹豫和迟疑，但是当我把这份工作的各种指标映射到这三个维度时，发现事情和人这两个维度都不能让我满意，这时候，“换工作”这个决定就清晰和坚决起来。&lt;/p&gt;&#xA;&lt;p&gt;由此我还想到的是，在我们做一些选择的时候，往往考虑的维度过多，顾此失彼、没有重点。我的建议也是将维度缩小，只考虑最重要的三个（最多不超过五个）维度，按照上面的算法进行维度的排序和权重计算，以此来指导我们做出选择。&lt;/p&gt;&#xA;&lt;p&gt;要牢记的是：一个选择的维度很多，常人如我们不可能什么都想要都能要，所以要有个取舍只选择对我们来说最重要的维度；即便是最重要的维度，也不可能面面俱到，需要根据自身的情况进行估算，最终做出符合自身条件利益最大化的选择。&lt;/p&gt;&#xA;&lt;p&gt;我最近帮一个朋友的孩子做高考志愿选择。一个高考志愿在我看来，涉及的最重要的三个维度是：学校、城市、专业。小朋友想学计算机，但是分数比较尴尬，勉强能够得着211，如果要稳进211就需要选择一般的学校、城市，甚至放弃专业（服从调剂）。在了解了最近几年广东高考的分数之后，给他填报的志愿是：武汉理工大学计算机专业（不服从调剂）、南京邮电大学。南邮虽不属于211，但是胜在专业较强，南京也是不错的城市。最终，如我意料的那样，武汉理工因为不服从调剂未被录取，录取进入南邮的电子信息工程专业。&lt;/p&gt;&#xA;&lt;p&gt;以上，就是我应用前面划分选择维度、计算最满意维度组合这套方法论的理论和例子。&lt;/p&gt;&#xA;&lt;p&gt;无独有偶，系统设计里，也有所谓的&amp;quot;CAP不可能三角（&lt;a href=&#34;https://zh.wikipedia.org/wiki/CAP%E5%AE%9A%E7%90%86&#34;&gt;CAP定理 - 维基百科，自由的百科全书&lt;/a&gt;）&amp;quot;：即系统设计时，只能在一致性（Consistency）、可用性（Availability）、分区容忍性（Partition tolerance）选择两者。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;CAP不可能三角&#34; src=&#34;https://www.codedump.info/media/imgs/20210803-choice-dimension/Visualization-of-CAP-theorem.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; CAP不可能三角 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Memcached的存储原理解析</title>
      <link>https://www.codedump.info/zh/post/20210701-memcached/</link>
      <pubDate>Thu, 01 Jul 2021 14:00:22 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20210701-memcached/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;最近工作上的需要，需要做一个LRU形式管理内存的分配器，首先想到的就是Memcached这个项目。早些年粗略的看过一些，有个大体的了解，这一次看下来发现其LRU算法做了不少的改动。&lt;/p&gt;&#xA;&lt;p&gt;本文解析Memcached内存管理这部分的内容，基于Memcached 1.6.9版本。&lt;/p&gt;&#xA;&lt;p&gt;Memcached将单个KV数据的存储，都放在&lt;code&gt;item&lt;/code&gt;这个结构体中，每个&lt;code&gt;item&lt;/code&gt;数据同时存在于这几个数据结构之中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;slabclass_t：以分级存储机制来提供内存的数据结构（下面展开详细讨论slabclass）。&lt;/li&gt;&#xA;&lt;li&gt;链表：当&lt;code&gt;item&lt;/code&gt;被使用时，存储在LRU链表中（下面详细讨论LRU链表）；当&lt;code&gt;item&lt;/code&gt;被释放之后，空闲的&lt;code&gt;item&lt;/code&gt;形成一个链表以备再次使用。&lt;/li&gt;&#xA;&lt;li&gt;hash表：用于根据键值查找数据的数据结构。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;hash表自不必多说，Memcached中将&lt;code&gt;item&lt;/code&gt;组织成一个名为&lt;code&gt;primary_hashtable&lt;/code&gt;的hash数组，根据键值查找元素时，首先计算出键值的hash值，再到对应的数组元素中遍历查找数据。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;slabclass_t&lt;/code&gt;结构体以分级的方式分配内存给&lt;code&gt;item&lt;/code&gt;，这样做有以下几个好处：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;统一了内存的管理，避免了内存的碎片化。&lt;/li&gt;&#xA;&lt;li&gt;分配、释放内存时都能到对应的slab中。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;slabclass_t&#34;&gt;&#xA;  slabclass_t&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#slabclass_t&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;定义&#34;&gt;&#xA;  定义&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%9a%e4%b9%89&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;slabs.c中定义了类型为&lt;code&gt;slabclass_t&lt;/code&gt;、大小为&lt;code&gt;MAX_NUMBER_OF_SLAB_CLASSES&lt;/code&gt;的数组&lt;code&gt;slabclass&lt;/code&gt;，用于分级存储。&lt;/p&gt;&#xA;&lt;p&gt;数组中的每个&lt;code&gt;slabclass_t&lt;/code&gt;元素，其能分配出去的内存大小递增，由如下的规则决定：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;每个数组可分配的内存大小都要8字节对齐（&lt;code&gt;CHUNK_ALIGN_BYTES&lt;/code&gt;）,这个大小保存在&lt;code&gt;slabclass_t&lt;/code&gt;的&lt;code&gt;size&lt;/code&gt;成员中。&lt;/li&gt;&#xA;&lt;li&gt;数组的第一个&lt;code&gt;slabclass_t&lt;/code&gt;元素的可分配内存大小为&lt;code&gt;sizeof(item) + settings.chunk_size&lt;/code&gt;。这之后的&lt;code&gt;slabclass_t&lt;/code&gt;可分配内存大小，都在上一个的元素的基础上放大&lt;code&gt;factor&lt;/code&gt;倍，同时还要8字节对齐。&lt;/li&gt;&#xA;&lt;li&gt;每次分配一个页面的大小由配置项&lt;code&gt;settings.slab_page_size&lt;/code&gt;来决定，因此每一个&lt;code&gt;slabclass_t&lt;/code&gt;元素的一个页面能容纳的&lt;code&gt;item&lt;/code&gt;数量为&lt;code&gt;settings.slab_page_size / slabclass[i].size&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;slabclass的分级存储&#34; src=&#34;https://www.codedump.info/media/imgs/20210701-memcached/slabclass.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; slabclass的分级存储 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;当需要分配一块大小的内存时，首先需要根据其大小，计算出该尺寸最终对应到上面的哪个元素，这个数组索引在Memcached中被称为&lt;code&gt;clsid&lt;/code&gt;，这个计算索引的过程参见函数&lt;code&gt;slabs_clsid&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;比如：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;slabclass[0].size = 56，fator参数为1.2，那么slabclass[1].size = (56 * 1.25)向上对齐8位 = 72，以此类推。&lt;/li&gt;&#xA;&lt;li&gt;假设需要分配的内存大小为60，就会去找&lt;code&gt;slabclass_t.size &amp;gt;= 60&lt;/code&gt;的第一个slabclass，在这个例子中返回的&lt;code&gt;clsid&lt;/code&gt;是1，也就是&lt;code&gt;slabclass[1]&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;内存分配时根据大小向上取满足条件的第一个slab的做法，优点在于方便了内存的分配管理，缺陷是会浪费掉部分空间，比如上面的例子中，将大小为72的slab用于60的内存，那么12字节的空间就被浪费掉了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;每一个slab中，需要维持两类空间：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;按照页面大小来分配的一整页空间，每个页面又按照该slab的大小划分成了多个不同的chunk。&lt;/li&gt;&#xA;&lt;li&gt;管理使用已被释放的item。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在&lt;code&gt;slabclass_t&lt;/code&gt;结构体中，以下几个成员用来维护该class的内存信息：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;slab_list：保存页面的数组，其大小保存在&lt;code&gt;slabs&lt;/code&gt;成员中。&lt;/li&gt;&#xA;&lt;li&gt;sl_curr：可用的&lt;code&gt;item&lt;/code&gt;数量。&lt;/li&gt;&#xA;&lt;li&gt;slots：保存在该&lt;code&gt;slabclass_t&lt;/code&gt;中空闲&lt;code&gt;item&lt;/code&gt;的链表头。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;slabclass结构体示意图&#34; src=&#34;https://www.codedump.info/media/imgs/20210701-memcached/slabclass-structure.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; slabclass结构体示意图 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;即：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;在Memcached的这一套内存管理体系中，一个页面被称为一个&lt;code&gt;slab&lt;/code&gt;，其大小为&lt;code&gt;settings.slab_page_size&lt;/code&gt;；页面中可以分割成多个&lt;code&gt;slot&lt;/code&gt;用来分配内存，一个&lt;code&gt;slot&lt;/code&gt;的大小由该&lt;code&gt;slabclass&lt;/code&gt;的初始大小及&lt;code&gt;factor&lt;/code&gt;来决定，但是需要向上补齐为8位对齐的大小。&lt;/li&gt;&#xA;&lt;li&gt;一个&lt;code&gt;slabclass&lt;/code&gt;中，有预分配好的页面数组，也有被回收的元素组成的空闲slot链表，分配元素时优先从空闲链表中分配（见函数&lt;code&gt;do_slabs_alloc&lt;/code&gt;）。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;内存分配&#34;&gt;&#xA;  内存分配&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%86%85%e5%ad%98%e5%88%86%e9%85%8d&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;既然Memcached是一个LRU形式的内存分配器，所以其内存是有限制的，系统中定义了如下几个全局变量来保存当前系统的内存分配信息：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;static size_t mem_limit：内存分配的上限。&lt;/li&gt;&#xA;&lt;li&gt;static size_t mem_malloced：当前分配的内存大小。&lt;/li&gt;&#xA;&lt;li&gt;static void *mem_base：保存内存的起始地址。&lt;/li&gt;&#xA;&lt;li&gt;static void *mem_current：保存内存分配的当前地址。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在初始化时，系统首先会根据&lt;code&gt;mem_limit&lt;/code&gt;分配一大块内存出来。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Etcd Raft库的日志存储</title>
      <link>https://www.codedump.info/zh/post/20210628-etcd-wal/</link>
      <pubDate>Mon, 28 Jun 2021 17:01:53 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20210628-etcd-wal/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;之前看etcd raft实现的时候，由于wal以及日志的落盘存储部分，没有放在raft模块中，对这部分没有扣的特别细致。而且，以前我的观点认为etcd raft把WAL这部分留给了上层的应用去实现，自身通过&lt;code&gt;Ready&lt;/code&gt;结构体来通知应用层落盘的数据，这个观点也有失偏颇，etcd只是没有把这部分代码放在raft模块中，属于代码组织的范畴问题，并不是需要应用层自己来实现。&lt;/p&gt;&#xA;&lt;p&gt;于是，决定专门写一篇文章把这部分内容给讲解一下，主要涉及以下内容：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;日志（包括快照）文件的格式。&lt;/li&gt;&#xA;&lt;li&gt;日志（包括快照）内容的落盘、恢复。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以前的系列文章可以在下面的链接中找到，本文不打算过多重复原理性的内容：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20210515-raft/&#34;&gt;Etcd Raft库的工程化实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;wal及快照文件格式&#34;&gt;&#xA;  WAL及快照文件格式&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#wal%e5%8f%8a%e5%bf%ab%e7%85%a7%e6%96%87%e4%bb%b6%e6%a0%bc%e5%bc%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;首先来讲解这两种文件的格式，了解了格式才能继续展开下面的讲述。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;wal文件格式&#34;&gt;&#xA;  WAL文件格式&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#wal%e6%96%87%e4%bb%b6%e6%a0%bc%e5%bc%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;wal文件的文件名格式为：seq-index.wal（见函数&lt;code&gt;walName&lt;/code&gt;）。其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;seq：序列号，从0开始递增。&lt;/li&gt;&#xA;&lt;li&gt;index：该wal文件存储的第一条日志数据的索引。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因此，如果将一个目录下的所有wal文件按照名称排序之后，给定一个日志索引，很快就能知道该索引的日志落在哪个wal文件之中的。&lt;/p&gt;&#xA;&lt;p&gt;WAL文件中每条记录的格式如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-proto&#34; data-lang=&#34;proto&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;message&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Record&lt;/span&gt; {&lt;span style=&#34;&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;&lt;/span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;optional&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;int64&lt;/span&gt; type  = 1 [(gogoproto.nullable) = &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;false&lt;/span&gt;];&lt;span style=&#34;&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;&lt;/span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;optional&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint32&lt;/span&gt; crc  = 2 [(gogoproto.nullable) = &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;false&lt;/span&gt;];&lt;span style=&#34;&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;&lt;/span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;optional&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;bytes&lt;/span&gt; data  = 3;&lt;span style=&#34;&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;&lt;/span&gt;}&lt;span style=&#34;&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#xA;&lt;li&gt;type：记录的类型，下面解释。&lt;/li&gt;&#xA;&lt;li&gt;crc：后面data部分数据的crc32校验值。&lt;/li&gt;&#xA;&lt;li&gt;data：数据部分，根据类型的不同有不同格式的数据。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;记录数据的类型如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;const&lt;/span&gt; (&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 以下是WAL存放的数据类型&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 元数据&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;metadataType &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;int64&lt;/span&gt; = &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;iota&lt;/span&gt; + 1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 日志数据&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;entryType&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 状态数据&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;stateType&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 校验初始值&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;crcType&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 快照数据&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;snapshotType&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;下面展开解释。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Etcd Raft库的工程化实现</title>
      <link>https://www.codedump.info/zh/post/20210515-raft/</link>
      <pubDate>Sat, 15 May 2021 13:52:08 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20210515-raft/</guid>
      <description>&lt;p&gt;最近回顾前几年写的Raft、etcd raft的实现文章，以及重新阅读Raft论文、etcd raft代码，发现之前有些理解不够准确、深刻，但是不打算在原文上做修正，于是写这篇补充的文章做一些另外角度的解释，以前的系列文章可以在下面的链接中找到，本文不打算过多重复原理性的内容：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在开始展开讨论前，先介绍这个Raft论文中的示意图，我认为能理解这幅图才能对一致性算法有个全貌的了解：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;Etcd Raft与应用层的交互&#34; src=&#34;https://www.codedump.info/media/imgs/20210515-raft/statemachine.jpeg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; Etcd Raft与应用层的交互 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;图中分为两种进程：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;server进程：server进程中运行着一致性算法模块、持久化保存的日志、以及按照日志提交的顺序来进行顺序操作的状态机。&lt;/li&gt;&#xA;&lt;li&gt;client进程：用于向server提交日志的进程。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要说明的是，两种进程都用叠加的矩形来表示，意指系统中这两类进程不止一个。&lt;/p&gt;&#xA;&lt;p&gt;一个日志要被正确的提交，图中划分了几步：&lt;/p&gt;&#xA;&lt;p&gt;1、client进程提交数据到server进程，server进程将收到的日志数据灌入一致性模块。&lt;/p&gt;&#xA;&lt;p&gt;2、一致性模块将日志写入本地WAL，然后同步给集群中其他server进程。&lt;/p&gt;&#xA;&lt;p&gt;3、多个节点对某条日志达成一致之后，将修改本地的提交日志索引（commit index）；落盘后的日志按照顺序灌入状态机，只要保证所有server进程上的日志顺序，那么最后状态机的状态肯定就是一致的了。&lt;/p&gt;&#xA;&lt;p&gt;4、灌入状态机之后，server进程可以应答客户端。&lt;/p&gt;&#xA;&lt;p&gt;所以，本质上，一个使用了一致性算法的库，划分了成了两个不同的模块：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一致性算法库，这里泛指Raft、Paxos、Zab等一致性协议。这类一致性算法库主要做如下的事情：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;用户输入库中日志（log），由库根据各自的算法来检测日志的正确性，并且通知上层的应用层。&#xA;&lt;ul&gt;&#xA;&lt;li&gt;输入到库中的日志维护和管理，算法库中需要知道哪些日志提交、提交成功、以及上层的应用层已经applied过的。当发生错误的时候，某些日志还会进行回滚（rollback）操作。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;日志的网络收发，这部分属于可选功能。有一些库，比如braft把这个事情也揽过来自己做了，优点是使用者不需要关注这部分功能，缺点是braft和它自带的网络库brpc耦合的很紧密，不可能拆开来使用；另一些raft实现，比如这里重点提到etcd raft实现，并不自己完成网络数据收发的工作，而是通知应用层，由应用层自己实现。&lt;/li&gt;&#xA;&lt;li&gt;日志的持久化存储：这部分也属于可选功能。前面说过，一致性算法库中维护了未达成一致的日志缓冲区，达成一致的日志才通知应用层，因此在这里不同的算法库又有了分歧，braft也是自己完成了日志持久化的工作，etcd raft则是将这部分工作交给了应用层。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;应用层：即工作在一致性算法之上的库使用者，这个就比上图中的“状态机”：只有达成一致并且落盘的数据才灌入应用层，只要保证灌入应用层的日志顺序一致那么最后的状态就是一致的。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;总体来看，一个一致性算法库有以下必选和可选功能：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;输入日志进行处理的算法（必选）。&lt;/li&gt;&#xA;&lt;li&gt;日志的维护和管理（必选）。&lt;/li&gt;&#xA;&lt;li&gt;日志（包括快照）数据的网络收发（可选）。&lt;/li&gt;&#xA;&lt;li&gt;日志（包括快照）的持久化存储（可选）。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要特别说明的是，即便是后面两个工作是可选的，但是可选还是必选的区别在于，这部分工作是一致性算法库自己完成，还是由算法库通知给上面的应用层去完成，并不代表这部分工作可以完全不做。&lt;/p&gt;&#xA;&lt;p&gt;在下表中列列举了etcd raft和braft在这几个特性之间的区别：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;功能&lt;/th&gt;&#xA;          &lt;th&gt;etcd raft&lt;/th&gt;&#xA;          &lt;th&gt;braft&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;raft一致性算法&lt;/td&gt;&#xA;          &lt;td&gt;实现&lt;/td&gt;&#xA;          &lt;td&gt;实现&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;日志的维护和管理&lt;/td&gt;&#xA;          &lt;td&gt;实现&lt;/td&gt;&#xA;          &lt;td&gt;实现&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;日志数据的网络收发&lt;/td&gt;&#xA;          &lt;td&gt;交由应用层&lt;/td&gt;&#xA;          &lt;td&gt;自己实现&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;日志数据的持久化存储&lt;/td&gt;&#xA;          &lt;td&gt;交由应用层&lt;/td&gt;&#xA;          &lt;td&gt;自己实现&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;优缺点&lt;/td&gt;&#xA;          &lt;td&gt;松耦合，易于验证、测试；需要应用者做更多的事情&lt;/td&gt;&#xA;          &lt;td&gt;与其rpc库紧耦合，难拆分；应用层做的事情不多，易于用来做服务&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;两种实现各有自己的优缺点，braft类实现更适合提供一个需要集成raft的服务时，可以直接用来实现服务；etcd raft类的实现，由于与网络、存储层耦合不紧密，易于进行测试，更适合拿来做为库使用。&lt;/p&gt;&#xA;&lt;p&gt;如果把前面的一致性算法的几个特性做一个抽象，我认为一致性算法库本质上就是一个“维护操作日志的算法库，只要大家都按照相同的顺序将日志灌入应用层”就好，其工作原理大体如下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;一致性算法的本质&#34; src=&#34;https://www.codedump.info/media/imgs/20210515-raft/co-algo.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 一致性算法的本质 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如果把问题抽象成这样的话，那么本质上，所谓的“一致性算法库”跟一个经常看到的tcp、kcp甚至是一个应用层的协议栈也就没有什么区别了：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;大家都要维护一个数据区：只有确认过正确的，才会抛给上一层。以TCP协议算法来说，比如发送但未确认的数据由协议栈的缓冲区维护，如果超时还未等到对端的确认，将发起超时重传等，这些都是每种协议算法的具体细节，但是本质上这些协议都要维护一个未确认数据的缓冲区。一致性算法在数据的维护上会更复杂一些，一是参与确认的节点不止通信的C/S两端，需要集群中半数以上节点的确认；同时，在未确认之前日志需要首先落盘，在提交成功之后再抛给应用层。&lt;/li&gt;&#xA;&lt;li&gt;只要保证所有参与的节点，都以相同的数据灌入日志给应用层，那么得到的结果将最终一致。&lt;/li&gt;&#xA;&lt;li&gt;确认的流程是可以pipeline异步化的，提交日志的进程并不需要一直等待日志被提交成功，而是提交之后等待。不妨以下面的流程来做解释：&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;流水线异步化的日志提交流程&#34; src=&#34;https://www.codedump.info/media/imgs/20210515-raft/pipeline.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 流水线异步化的日志提交流程 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;</description>
    </item>
    <item>
      <title>KCP 1.4源码分析</title>
      <link>https://www.codedump.info/zh/post/20201105-kcp/</link>
      <pubDate>Thu, 05 Nov 2020 22:08:40 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20201105-kcp/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/skywind3000/kcp&#34;&gt;KCP&lt;/a&gt;是基于UDP协议之上的ARQ协议实现。TCP虽然使用的更广泛，但是在某些实时性要求更高的领域（如实时音视频、即时在线游戏等），会更倾向于使用基于UDP的可靠传输协议。&lt;/p&gt;&#xA;&lt;p&gt;在项目的官网上，对KCP是这么介绍的：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;KCP是一个快速可靠协议，能以比 TCP 浪费 10%-20% 的带宽的代价，换取平均延迟降低 30%-40%，且最大延迟降低三倍的传输效果。纯算法实现，并不负责底层协议（如UDP）的收发，需要使用者自己定义下层数据包的发送方式，以 callback的方式提供给 KCP。 连时钟都需要外部传递进来，内部不会有任何一次系统调用。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;UDP并不是一个可靠的传输协议，如果数据没有发送成功并不会自动重传，KCP基于UDP协议之上实现了自己的ARQ协议，所以在继续介绍KCP协议之前，先大体了解一下ARQ协议。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;arq的两种模式&#34;&gt;&#xA;  ARQ的两种模式&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#arq%e7%9a%84%e4%b8%a4%e7%a7%8d%e6%a8%a1%e5%bc%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;KCP在UDP之上，自己实现了可靠性的算法，即给UDP加上了诸如超时重传、流量控制等机制，这些都是为了保证ARQ协议的运作。&lt;/p&gt;&#xA;&lt;p&gt;ARQ协议(Automatic Repeat-reQuest)，即自动重传请求，是传输层的错误纠正协议之一，它通过使用确认和超时两个机制，在不可靠的网络上实现可靠的信息传输。&lt;/p&gt;&#xA;&lt;p&gt;ARQ的实现通常有如下两种模式。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;停等arq协议stop-and-wait&#34;&gt;&#xA;  停等ARQ协议（stop-and-wait）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%81%9c%e7%ad%89arq%e5%8d%8f%e8%ae%aestop-and-wait&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;停等ARQ协议，意味着每个数据在发送出去之后，在没有收到对端的接收回复之前，将一直等待下去，而不会继续发送新的数据包。如果超时还未收到应答，就会自动重传数据包，以保证数据的可靠性。&lt;/p&gt;&#xA;&lt;p&gt;下图是两种情况下停等协议的示意图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;stop-and-wait&#34; src=&#34;https://www.codedump.info/media/imgs/20201105-kcp/stop-and-wait.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; stop-and-wait &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;上图：正常不出错情况下运行的停等协议，消息2必须在发送方收到了消息1的对端确认回复之后才能发送出去。&lt;/li&gt;&#xA;&lt;li&gt;下图：出错情况下运行的停等协议，发送方发现消息1超时还未收到应答，就触发了针对消息1的重传机制。在这之前消息2都不会被发出去。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;协议栈如何确认这个“超时时间”呢？答案是根据数据往返时间动态估算出来的RTO（Retransmission TimeOut，重传超时时间）时间来确认的。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;连续arq协议continuous-arq&#34;&gt;&#xA;  连续ARQ协议（Continuous ARQ）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%bf%9e%e7%bb%adarq%e5%8d%8f%e8%ae%aecontinuous-arq&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;可以看到，停等协议的机制是“一应一答”式的，对带宽的利用率不高，传输效率不高。&lt;/p&gt;&#xA;&lt;p&gt;连续ARQ协议，可以一次性发送多个数据，而不必像停等协议那样需要等待上一个数据包的确认回复才能继续发送数据。&lt;/p&gt;&#xA;&lt;p&gt;在使用连续ARQ协议的时候，接收方也并不会针对每一个收到的数据包进行确认应答，而只需应答确认最大的那个数据包，这时就认为在此之前的数据包都收到了。&lt;/p&gt;&#xA;&lt;p&gt;这种模式称为“UNA（unacknowledge，即第一个未应答数据包的序列号，小于该序列号的数据包都已经确认被接收到）”模式，与之对应的是，停等协议是ACK模式。&lt;/p&gt;&#xA;&lt;p&gt;然而，即便是可以一次发送多个数据包，也不意味着可以不受控制的发送数据，因为还要受到几种流量窗口的限制，这部分被称为“流量控制”。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;拥塞窗口&#34;&gt;&#xA;  拥塞窗口&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%8b%a5%e5%a1%9e%e7%aa%97%e5%8f%a3&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;拥塞窗口更多是对网络上经过的网络设备总体网络情况的一个预估。因为在真正发送数据时，并不清楚这时候的网络情况，因此启动时拥塞窗口会有一个初始值，然后根据以下几种算法进行动态的调整：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;慢启动：在启动时让拥塞窗口缓慢扩张。&lt;/li&gt;&#xA;&lt;li&gt;退半避让：在发生网络拥堵时让拥塞窗口大小减半。&lt;/li&gt;&#xA;&lt;li&gt;快重传：在网络恢复时尽快的将数据发送出去。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;滑动窗口&#34;&gt;&#xA;  滑动窗口&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%bb%91%e5%8a%a8%e7%aa%97%e5%8f%a3&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;拥塞窗口是对外部网络情况的一种动态的检测，而滑动窗口则是进程本身接收缓冲区的控制，滑动窗口就是接收方用来通知发送方本方接收缓冲区大小的。由于一个网络进程分为协议层和应用层，如果协议层接收数据很快，但是应用层消费数据很慢，这个滑动窗口就会缩小，通过这种方式来通知对端放缓数据的发送，因为接收方已经忙不过来了。&lt;/p&gt;&#xA;&lt;p&gt;KCP作为一个ARQ协议，内部就是要实现对以上这些机制的支持。&lt;/p&gt;&#xA;&lt;p&gt;如果对TCP协议的实现有一些了解，可以看到上述的对端确认回复、超时重传、拥塞窗口、滑动窗口等概念，在TCP中就有，KCP自己实现的ARQ机制，与TCP对比起来有如下的不同点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;在TCP中，超时之后的RTO时间直接翻倍（即RTO&lt;em&gt;2），而在KCP启用了快速模式之后，RTO的超时时间是&lt;/em&gt;1.5，避免RTO时间的快速增长。&lt;/li&gt;&#xA;&lt;li&gt;TCP协议在丢包时会直接重传丢的那个包之后的所有数据包，KCP只会选择性的重传真正丢失的数据包。&lt;/li&gt;&#xA;&lt;li&gt;TCP为了充分利用带宽，会延迟发送ACK应答对端，这样会导致计算出来的RTT时间过大，KCP的ACK是否延迟发送则可以调节。&lt;/li&gt;&#xA;&lt;li&gt;KCP 正常模式同 TCP 一样使用公平退让法则，即发送窗口大小由：发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的小数据时，可选择通过配置跳过后两步，仅用前两项来控制发送频率。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;本文基于KCP 1.4版本对其实现做分析。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;术语概念&#34;&gt;&#xA;  术语概念&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%af%e8%af%ad%e6%a6%82%e5%bf%b5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在展开讨论之前，首先介绍相关的术语概念。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;ARQ：Automatic Repeat-reQuest，自动重传请求协议。KCP就是其中一种ARQ协议的实现。&lt;/li&gt;&#xA;&lt;li&gt;MTU：Maximum Transmission Unit，最大传输单元，链路层规定的每一帧最大长度，通常为1500字节。&lt;/li&gt;&#xA;&lt;li&gt;MSS：Maximum Segment Size，最大分段大小。通常为MTU-协议头大小。&lt;/li&gt;&#xA;&lt;li&gt;RTT：Round-Trip Time，数据往返时间，即发出消息到接收到对端消息应答之间的时间差。&lt;/li&gt;&#xA;&lt;li&gt;RTO：Retransmission TimeOut，重传超时时间，根据收集到的RTT时间估算。&lt;/li&gt;&#xA;&lt;li&gt;rwnd：Receive Window，接收窗口大小，接收端通过该数据通知发送端本方接收窗口大小。&lt;/li&gt;&#xA;&lt;li&gt;cwnd：Congestion Window，拥塞窗口大小，影响发送方发送数据大小。&lt;/li&gt;&#xA;&lt;li&gt;ack：acknowledge，接收端接收到一个数据包之后，通过应答该数据包序列号来通知发送端接收成功。&lt;/li&gt;&#xA;&lt;li&gt;una：unacknowledge，即第一个未应答数据包的序列号，小于该序列号的数据包都已经确认被接收到。&lt;/li&gt;&#xA;&lt;li&gt;ssthresh：Slow Start threshold，慢启动阈值，用于在发生拥塞的情况下控制窗口的增长速度。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;数据结构&#34;&gt;&#xA;  数据结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;报文定义&#34;&gt;&#xA;  报文定义&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%8a%a5%e6%96%87%e5%ae%9a%e4%b9%89&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;每个KCP数据报文，其定义如下，注释中描述了每个字段的含义：&lt;/p&gt;</description>
    </item>
    <item>
      <title>boltdb 1.3.0实现分析（四）</title>
      <link>https://www.codedump.info/zh/post/20200726-boltdb-4/</link>
      <pubDate>Sun, 26 Jul 2020 17:55:11 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200726-boltdb-4/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文基于boltdb 1.3.0对其实现进行分析。boltdb是etcd系统存储数据使用的KV嵌入式DB，使用Go编码实现，内部是一个B+树结构体。关于etcd、raft协议以及B+树，可以参考之前的文章：&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;本文的写作，主要参考了&lt;a href=&#34;https://www.jianshu.com/p/b86a69892990&#34;&gt;《区块的持久化之BoltDB》系列文章&lt;/a&gt;以及&lt;a href=&#34;https://youjiali1995.github.io/storage/boltdb&#34;&gt;boltdb 源码分析&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;前面的章节中，分别讲解了boltdb的页面结构、Bucket结构以及事务相关的逻辑，最后一节讲解boltdb如何实现MVCC。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;mvcc概述&#34;&gt;&#xA;  MVCC概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#mvcc%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;数据库的&lt;code&gt;ACID&lt;/code&gt;特性中，&lt;code&gt;Isolation&lt;/code&gt;即隔离性是一个较难实现的特性。&lt;/p&gt;&#xA;&lt;p&gt;一个数据库被修改时，在这次事务提交之前，不希望其他事务操作读到修改的结果。一种常见的办法就是加锁，但是锁的粒度如果很大，就会影响数据库的并发性能，即在写操作完成之前不能进行其他操作。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;MVCC（Multiversion concurrency control，多版本并发控制）&lt;/code&gt;是解决这个问题的一种方式。它的做法是：保存数据库中的多个版本，修改的是一个版本，而同时进行的读操作读取到的数据是旧版本的数据，这样即便读到了旧的数据也不影响，只要不是写操作中间的数据就好。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;readCommitted&#34; src=&#34;https://www.codedump.info/media/imgs/20200726-boltdb-4/readCommitted.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; readCommitted &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图所示，事务A修改了x的值为2，在这个事务提交之前，读事务B读取到的还是修改之前的值1，因为存在有两个该数据的不同版本，并且并没有因为有写操作同时存在而必须等待写操作完成才能进行读操作。在事务A提交之后，才能读到新的值2。（但是这个图里还有另外的问题，即同一个读事务的过程中，前后读到了同一个数据两次不同的值，这叫“不可重复读”，这就是另外一个问题了不在这里展开讨论。）&lt;/p&gt;&#xA;&lt;p&gt;本节讲解boltdb如何实现&lt;code&gt;MVCC&lt;/code&gt;操作，在开始讨论之前先看看boltdb如何管理数据库文件的。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;映射文件的使用&#34;&gt;&#xA;  映射文件的使用&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%98%a0%e5%b0%84%e6%96%87%e4%bb%b6%e7%9a%84%e4%bd%bf%e7%94%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;boltdb通过&lt;code&gt;mmap&lt;/code&gt;系统调用将数据库文件映射到内存中，64位体系下一个进程的虚拟内存空间有128TB，足够映射一个文件了。在把磁盘文件映射到内存之后，对磁盘文件的读写可以直接使用读写内存的操作，由操作系统内核来决定什么时候将哪部分的虚拟内存换入、换出物理内存。&lt;/p&gt;&#xA;&lt;p&gt;看上去这个好像方便了很多，其实并不建议在存储引擎中使用内存映射这样“偷懒”的技术。原因在于：操作系统内核除了知道什么时候换入换出物理内存之外，对数据库的其他操作一无所知，并不能很精准的控制其行为。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;db-mmap&#34; src=&#34;https://www.codedump.info/media/imgs/20200726-boltdb-4/db-mmap.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; db-mmap &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;对数据库进行内存映射的操作在函数&lt;code&gt;db.mmap&lt;/code&gt;函数中进行，大体流程如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;func&lt;/span&gt; (db *DB) &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;mmap&lt;/span&gt;(minsz &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;int&lt;/span&gt;) &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;error&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;db.mmaplock.&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Lock&lt;/span&gt;()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;defer&lt;/span&gt; db.mmaplock.&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Unlock&lt;/span&gt;()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 计算至少要多大的文件大小才能满足minsz需求&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// Memory-map the data file as a byte slice.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;if&lt;/span&gt; err := &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;mmap&lt;/span&gt;(db, size); err != &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;nil&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;return&lt;/span&gt; err&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个函数只有在以下两种情况会被调用到：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;db初始化时，即初次加载db文件到内存映射中。&lt;/li&gt;&#xA;&lt;li&gt;当前文件不够大，需要进行扩容时，即在&lt;code&gt;db.allocate&lt;/code&gt;中分配新页面而当前页面不足需要扩充文件大小时。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;后者也被称为&lt;code&gt;remmap&lt;/code&gt;操作，即以新的大小重新映射文件进行内存中。为了避免每次增加了文件大小都需要重新进行文件内存映射操作，实际上boltdb是对文件大小做了&lt;code&gt;over allocate&lt;/code&gt;操作，具体的计算新文件大小的逻辑在函数&lt;code&gt;DB.mmapSize&lt;/code&gt;函数中实现的，这里不做展开了。&lt;/p&gt;</description>
    </item>
    <item>
      <title>boltdb 1.3.0实现分析（三）</title>
      <link>https://www.codedump.info/zh/post/20200725-boltdb-3/</link>
      <pubDate>Sat, 25 Jul 2020 11:26:33 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200725-boltdb-3/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文基于boltdb 1.3.0对其实现进行分析。boltdb是etcd系统存储数据使用的KV嵌入式DB，使用Go编码实现，内部是一个B+树结构体。关于etcd、raft协议以及B+树，可以参考之前的文章：&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;本文的写作，主要参考了&lt;a href=&#34;https://www.jianshu.com/p/b86a69892990&#34;&gt;《区块的持久化之BoltDB》系列文章&lt;/a&gt;以及&lt;a href=&#34;https://youjiali1995.github.io/storage/boltdb&#34;&gt;boltdb 源码分析&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;在前面的文章里，分别介绍了boltdb的几种页面格式、Bucket以及Cursor结构，本文介绍boltdb的事务（Transaction）。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;boltdb支持事务的&lt;code&gt;ACID&lt;/code&gt;特性，使用&lt;code&gt;MVCC&lt;/code&gt;来做并发控制，同时可以执行一个写事务和多个读事务：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;原子性（Atomicity）：未提交的写事务操作都在内存中。在提交写事务的时候，按照B+树数据、freelist、meta元数据的顺序写入文件。在meta元信息写入之前，都可以进行回滚（rollback）操作，只有meta元信息写入成功才能认为写操作执行成功。&lt;/li&gt;&#xA;&lt;li&gt;隔离性（Isolation）：每个读事务开始的时候获得一个版本号，读事务涉及到的页面不会被同时进行的写事务所覆盖；而每次写事务都会更新一个版本号。&lt;/li&gt;&#xA;&lt;li&gt;持久性（Durability）：写事务在提交的时候，会将这次写操作修改的数据（dirty page）分配新的页面，写入文件持久化。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;本节首先讲解boltdb的事务基本实现，下一节讲解boltdb事务如何实现&lt;code&gt;MVCC&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;事务初始化&#34;&gt;&#xA;  事务初始化&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%ba%8b%e5%8a%a1%e5%88%9d%e5%a7%8b%e5%8c%96&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;boltdb中，任何一次读写操作，都有一个事务与之对应。这时候首先会调用&lt;code&gt;DB.Begin&lt;/code&gt;函数返回一个事务，而传入的参数会根据情况分别创建写和读事务：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;func&lt;/span&gt; (db *DB) &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Begin&lt;/span&gt;(writable &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;bool&lt;/span&gt;) (*Tx, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;error&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;if&lt;/span&gt; writable {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;return&lt;/span&gt; db.&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;beginRWTx&lt;/span&gt;()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;return&lt;/span&gt; db.&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;beginTx&lt;/span&gt;()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到，根据是否是写事务，会分别调用&lt;code&gt;beginRWTx&lt;/code&gt;和&lt;code&gt;beginTx&lt;/code&gt;来创建读写事务和只读事务。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;DB&lt;/code&gt;结构体中，仅有一个写事务成员，而读事务则可以同时存在多个，因此同一个时间只能有一个写事务：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; DB &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;rwtx     *Tx&#x9;&#x9;&#x9;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 同一时间只能有一个未完成的写事务&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;txs      []*Tx&#x9;&#x9;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 保存未完成的读事务的，读事务可以有多个，写事务一个时间只能有一个，就在rwtx里面&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}  &#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;beginTx&#34; src=&#34;https://www.codedump.info/media/imgs/20200725-boltdb-3/beginTx.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; beginTx &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;了解了在&lt;code&gt;DB&lt;/code&gt;中如何使用事务，下面来看事务结构体的定义：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; Tx &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;writable       &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;bool&lt;/span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 是否写事务&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;managed        &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;bool&lt;/span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;//&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;db             *DB&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 对应的db&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;meta           *meta&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 对应的meta数据指针&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;root           Bucket&#x9;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;pages          &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;map&lt;/span&gt;[pgid]*page&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 涉及到的page&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;stats          TxStats&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;commitHandlers []&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;func&lt;/span&gt;()&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// commit回调函数数组&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;WriteFlag &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;int&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;成员释义如下：&lt;/p&gt;</description>
    </item>
    <item>
      <title>boltdb 1.3.0实现分析（二）</title>
      <link>https://www.codedump.info/zh/post/20200711-boltdb-2/</link>
      <pubDate>Sat, 11 Jul 2020 09:33:06 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200711-boltdb-2/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文基于boltdb 1.3.0对其实现进行分析。boltdb是etcd系统存储数据使用的KV嵌入式DB，使用Go编码实现，内部是一个B+树结构体。关于etcd、raft协议以及B+树，可以参考之前的文章：&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;本文的写作，主要参考了&lt;a href=&#34;https://www.jianshu.com/p/b86a69892990&#34;&gt;《区块的持久化之BoltDB》系列文章&lt;/a&gt;以及&lt;a href=&#34;https://youjiali1995.github.io/storage/boltdb&#34;&gt;boltdb 源码分析&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;在&lt;a href=&#34;https://www.codedump.info/post/20200625-boltdb-1/&#34;&gt;上一节&lt;/a&gt;里面，系统的介绍了Boltdb中几种类型页面的格式，有了这些基础，本节开始介绍boltdb中的Bucket结构。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;bucket&#34;&gt;&#xA;  Bucket&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#bucket&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在上一节中，Bucket类比于mysql中的table，在boltdb中，&lt;code&gt;meta&lt;/code&gt;页面中有一个成员&lt;code&gt;bucket&lt;/code&gt;，其存储了整个数据库根bucket的信息，而一个数据库中存储的其他table的信息，则作为子bucket存储到Bucket中。这几个数据结构的关系如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; DB &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;meta0    *meta&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;meta1    *meta  &#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; meta &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;root     bucket&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 根bucket的信息&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; Bucket &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;*bucket&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  buckets  &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;string&lt;/span&gt;]*Bucket &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 存储子bucket的对应关系&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; bucket &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 根节点的page id&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;root pgid &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// page id of the bucket&amp;#39;s root-level page&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 单调递增的序列号&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;sequence &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint64&lt;/span&gt; &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// monotonically incrementing, used by NextSequence()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在&lt;code&gt;bucket&lt;/code&gt;数据结构中，两个成员的作用是：&lt;/p&gt;</description>
    </item>
    <item>
      <title>boltdb 1.3.0实现分析（一）</title>
      <link>https://www.codedump.info/zh/post/20200625-boltdb-1/</link>
      <pubDate>Thu, 25 Jun 2020 21:43:17 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200625-boltdb-1/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文基于boltdb 1.3.0对其实现进行分析。boltdb是etcd系统存储数据使用的KV嵌入式DB，使用Go编码实现，内部是一个B+树结构体。关于etcd、raft协议以及B+树，可以参考之前的文章：&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;本文的写作，主要参考了&lt;a href=&#34;https://www.jianshu.com/p/b86a69892990&#34;&gt;《区块的持久化之BoltDB》系列文章&lt;/a&gt;以及&lt;a href=&#34;https://youjiali1995.github.io/storage/boltdb&#34;&gt;boltdb 源码分析&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;boltdb是etcd项目使用的kv存储引擎，代码量不大，不算测试用例的话仅有几千行代码量，是入门存储引擎不错的参考项目。&lt;/p&gt;&#xA;&lt;p&gt;boltdb中与mysql这类的关系数据库相对应的概念列举如下：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;boltdb&lt;/th&gt;&#xA;          &lt;th&gt;mysql&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;db&lt;/td&gt;&#xA;          &lt;td&gt;database&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;bucket&lt;/td&gt;&#xA;          &lt;td&gt;table&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;即：在boltdb中，db代表一个数据库，对应一个db文件；而一个数据库中可能有多个表，对应的概念就是boltdb中的bucket。&lt;/p&gt;&#xA;&lt;p&gt;另外，对B+树有了解的都知道，B+树中为了减少磁盘读写次数，每次读写都是以页为单位的，对应到boltdb中用&lt;code&gt;page&lt;/code&gt;数据结构表示，&lt;code&gt;page&lt;/code&gt;只是描述磁盘上一个页面的数据结构，当一个页面读取到内存中时，就使用&lt;code&gt;node&lt;/code&gt;结构体来描述。另外，既然落地到磁盘的单位是页，就需要有数据结构来管理页面的分配，这部分使用&lt;code&gt;freelist&lt;/code&gt;这个数据结构来管理。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;page-struct&#34; src=&#34;https://www.codedump.info/media/imgs/20200625-boltdb-1/page-struct.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; page-struct &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;以下，首先展开对页面相关核心数据结构的分析。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;数据库文件的磁盘布局和页面&#34;&gt;&#xA;  数据库文件的磁盘布局和页面&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e5%ba%93%e6%96%87%e4%bb%b6%e7%9a%84%e7%a3%81%e7%9b%98%e5%b8%83%e5%b1%80%e5%92%8c%e9%a1%b5%e9%9d%a2&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面提到过，boltdb中以页面为单位来进行磁盘的读写操作，一个页面的大小一般而言与操作系统的页面一致，即4K大小。在boltdb中，分为以下几种类型的页面：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;存储meta元数据的页面。&lt;/li&gt;&#xA;&lt;li&gt;存储freelist，即管理页面数据的页面。&lt;/li&gt;&#xA;&lt;li&gt;Branch页面，存储B+树索引节点，也就是内部节点的页面。&lt;/li&gt;&#xA;&lt;li&gt;Leaf页面，存储B+树数据节点，也就是叶子节点的页面。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;boltdb代码中定义页面类型如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;const&lt;/span&gt; (&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;branchPageFlag   = 0x01&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;leafPageFlag     = 0x02&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;metaPageFlag     = 0x04&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;freelistPageFlag = 0x10&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这四种页面，在boltdb的数据库文件的布局大体如下：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;boltdb-layout&#34; src=&#34;https://www.codedump.info/media/imgs/20200625-boltdb-1/boltdb-layout.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; boltdb-layout &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;从上图中可以看出：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;最开始的两个页面是两个meta页面，至于为什么是两个，后面再展开讨论。&lt;/li&gt;&#xA;&lt;li&gt;紧跟着的一个页面是freelist页面。&lt;/li&gt;&#xA;&lt;li&gt;从上面可知，数据库文件中最开始的三个页面存的都是管理信息，此后数据数据型的branch以及leaf页面了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;接下来就这几种页面具体的结构展开说明，不过在此之前还是首先来看看&lt;code&gt;page&lt;/code&gt;结构体，它用于表示一个磁盘页面的数据结构。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;page结构体&#34;&gt;&#xA;  page结构体&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#page%e7%bb%93%e6%9e%84%e4%bd%93&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;page&lt;/code&gt;结构体的定义如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; pgid &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint64&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; page &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    id       pgid&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    flags    &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint16&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    count    &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint16&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    overflow &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint32&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ptr      &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uintptr&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其中：&lt;/p&gt;</description>
    </item>
    <item>
      <title>《面向应用开发者的系统指南》CPU篇之Linux系统平均负载</title>
      <link>https://www.codedump.info/zh/post/20200620-sgfap-loadavg/</link>
      <pubDate>Sat, 20 Jun 2020 12:19:08 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200620-sgfap-loadavg/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文是《面向应用开发者的系统指南》文档其中的一篇，完整的目录见&lt;a href=&#34;https://www.codedump.info/post/20200501-system-guide-for-application-programmer/&#34;&gt;《面向应用开发者的系统指南》导论&lt;/a&gt;。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;Linux中可以使用uptime、top等命令来查看系统的平均负载情况，比如：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ uptime&#xA; 10:54:37 up 29 days,  1:35,  2 users,  load average: 0.81, 0.65, 0.64&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中的&lt;code&gt;load average: 0.81, 0.65, 0.64&lt;/code&gt;数据，给出了系统在最近1分钟、5分钟、15分钟的系统平均负载情况。&lt;/p&gt;&#xA;&lt;p&gt;这一节讲解系统平均负载这个数据的来源，内容包括以下几方面：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;系统平均负载值来源于哪里？&lt;/li&gt;&#xA;&lt;li&gt;平均负载包括了哪些指标？&lt;/li&gt;&#xA;&lt;li&gt;内核是如何计算平均负载值的？&lt;/li&gt;&#xA;&lt;li&gt;平均负载的意义是什么？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;平均负载值的来源&#34;&gt;&#xA;  平均负载值的来源&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%b9%b3%e5%9d%87%e8%b4%9f%e8%bd%bd%e5%80%bc%e7%9a%84%e6%9d%a5%e6%ba%90&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;通过uptime命令可以看到系统最近1分钟、5分钟以及15分钟的平均负载值，所以要知道这个值的来源，最简单的方式就是了解uptime命令是从哪里获取到这些数据的，一方面可以看uptime命令的代码实现，但是直觉告诉我们一般这类命令都是通过读取/proc文件系统来获取系统的一些指标，所以更简单的方式是strace一下uptime命令，看看都去读取了哪些/proc文件系统的文件，果然看到了如下一行：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;openat(AT_FDCWD, &amp;#34;/proc/loadavg&amp;#34;, O_RDONLY) = 4&#xA;lseek(4, 0, SEEK_SET)                   = 0&#xA;read(4, &amp;#34;0.42 0.20 0.07 3/137 1322\n&amp;#34;, 8191) = 26&#xA;fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;可以看到读取了&lt;code&gt;/proc/loadavg&lt;/code&gt;文件，通过&lt;code&gt;man proc&lt;/code&gt;命令来看看关于这个文件的说明：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;/proc/loadavg&#xA;&#x9;&#x9;The first three fields in this file are load average figures giving the number of jobs in the run queue (state R) or  wait‐&#xA;&#x9;&#x9;ing  for  disk  I/O  (state  D) averaged over 1, 5, and 15 minutes.  They are the same as the load average numbers given by&#xA;&#x9;&#x9;uptime(1) and other programs.  The fourth field consists of two numbers separated by a slash (/).  The first  of  these  is&#xA;&#x9;&#x9;the  number of currently runnable kernel scheduling entities (processes, threads).  The value after the slash is the number&#xA;&#x9;&#x9;of kernel scheduling entities that currently exist on the system.  The fifth field is the PID of the process that was  most&#xA;&#x9;&#x9;recently created on the system.&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;以上文档中说明了，系统负载的统计数据源，包括：&lt;/p&gt;</description>
    </item>
    <item>
      <title>B树、B&#43;树索引算法原理（下）</title>
      <link>https://www.codedump.info/zh/post/20200615-btree-2/</link>
      <pubDate>Mon, 15 Jun 2020 22:44:21 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200615-btree-2/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;这一段时间由于在阅读boltdb代码的缘故，找机会学习了B树及B+树的算法原理，这个系列会花两个篇幅分别介绍这两种数据结构的实现，其用于数据库索引中的基本原理。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;在&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;上一篇文章&lt;/a&gt;中，介绍了数据库索引的简单概念，以及B树的结构及核心算法，这一篇将继续介绍B树的变形B+树。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;b树的定义及性质&#34;&gt;&#xA;  B+树的定义及性质&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#b%e6%a0%91%e7%9a%84%e5%ae%9a%e4%b9%89%e5%8f%8a%e6%80%a7%e8%b4%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;B+树之于B树，最大的不同在于：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;B树的数据可以存储在内部节点上，也可以存储在叶子节点上。&lt;/li&gt;&#xA;&lt;li&gt;而在B+树中，内部节点上仅存放数据的索引，数据只存储在叶子节点上。在内部节点中的键值，被称为“索引”，由于是数据索引，因此可能出现同一个键值，既出现在内部节点，也出现在叶子节点中的情况。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;内部节点的“索引”，应该满足以下条件：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;大于左边子树的最大键值；&lt;/li&gt;&#xA;&lt;li&gt;小于等于右边子树的最小键值。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;同时，B+树为了方便范围查询，叶子节点之间还用指针串联起来。&lt;/p&gt;&#xA;&lt;p&gt;以下是一颗B+树的典型结构：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;b&amp;#43;tree&#34; src=&#34;https://www.codedump.info/media/imgs/20200615-btree-2/b&amp;#43;tree.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; b+tree &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;由于采用了这样的结构，B+树对比B树有以下优点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;索引节点上由于只有索引而没有数据，所以索引节点上能存储比B树更多的索引，这样树的高度就会更矮。按照我们上一篇中介绍数据库索引的内容，这种面向磁盘的数据结构，树的高度越矮，磁盘寻道的次数就会越少。&lt;/li&gt;&#xA;&lt;li&gt;因为数据都集中在叶子节点了，而所有叶子节点的高度相同，那么可以在叶子节点中增加前后指针，指向同一个父节点的相邻兄弟节点，给范围查询提供遍历。比如这样的SQL语句：&lt;code&gt;select * from tbl where t &amp;gt; 10&lt;/code&gt;，如果使用B+树存储数据的话，可以首先定位到数据为10的节点，再沿着它的next指针一路找到所有在该叶子节点右边的叶子节点数据返回。而如果使用B树结构，由于数据既可以存储在内部节点也可以存储在叶子节点，范围查询可想而知是很繁琐的。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;核心算法&#34;&gt;&#xA;  核心算法&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a0%b8%e5%bf%83%e7%ae%97%e6%b3%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;插入算法&#34;&gt;&#xA;  插入算法&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%8f%92%e5%85%a5%e7%ae%97%e6%b3%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;B+树的插入算法与B树的很相近，都是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;首先判断待插入数据节点是否已经溢出，如果是就首先拆分成两个节点，然后再插入数据。&lt;/li&gt;&#xA;&lt;li&gt;由于内部节点上的数据是索引，所以在插入完成之后调整父节点指针。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;比如在下图的B+树中，向这里插入新的数据&lt;code&gt;10&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;slide01b&#34; src=&#34;https://www.codedump.info/media/imgs/20200615-btree-2/slide01b.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; slide01b &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;由于插入节点&lt;code&gt;[7,11]&lt;/code&gt;在插入之后并没有溢出，所以可以直接变成&lt;code&gt;[7,10,11]&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;slide01c&#34; src=&#34;https://www.codedump.info/media/imgs/20200615-btree-2/slide01c.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; slide01c &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;而如下图的B+树中，插入数据&lt;code&gt;4&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;slide02b&#34; src=&#34;https://www.codedump.info/media/imgs/20200615-btree-2/slide02b.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; slide02b &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;由于所在节点&lt;code&gt;[2,3,5]&lt;/code&gt;在插入之后数据溢出，因此需要分裂为两个新的节点，同时调整父节点的索引数据：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;slide02g&#34; src=&#34;https://www.codedump.info/media/imgs/20200615-btree-2/slide02g.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; slide02g &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;[2,3,4,5]&lt;/code&gt;分裂成了&lt;code&gt;[2,3]&lt;/code&gt;和&lt;code&gt;[4,5]&lt;/code&gt;，因此需要在这两个节点之间新增一个索引值，这个值应该满足：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;大于左子树的最大值。&lt;/li&gt;&#xA;&lt;li&gt;小于等于右子树的最小值。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;综上，需要在父节点中新增索引&lt;code&gt;4&lt;/code&gt;和两个指向新节点的指针。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;删除算法&#34;&gt;&#xA;  删除算法&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%88%a0%e9%99%a4%e7%ae%97%e6%b3%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;B+树的删除算法，与B树类似，分为以下几步：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;首先查询到键值所在的叶子节点，删除该叶子节点的数据。&lt;/li&gt;&#xA;&lt;li&gt;如果删除叶子节点之后的数据数量，满足B+树的平衡条件，则直接返回不用往下走了。&lt;/li&gt;&#xA;&lt;li&gt;否则，就需要做平衡操作：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如果该叶子节点的左右兄弟节点的数据量可以借用，就借用过来满足平衡条件。&lt;/li&gt;&#xA;&lt;li&gt;否则，就只能与相邻的兄弟节点合并成一个新的子节点了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;在上面平衡操作中，如果是进行了合并操作，就需要向上修正父节点的指针：删除被合并节点的键值以及指针。由于做了删除操作，可能父节点也会不平衡，那么就按照前面的步骤也对父节点进行重新平衡操作，这样一直到某个节点平衡为止。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;下面结合&lt;a href=&#34;http://www.mathcs.emory.edu/~cheung/Courses/554/Syllabus/3-index/B-tree=delete1.html&#34;&gt;B-tree=delete1&lt;/a&gt;、&lt;a href=&#34;http://www.mathcs.emory.edu/~cheung/Courses/554/Syllabus/3-index/B-tree=delete2.html&#34;&gt;B-tree=delete2&lt;/a&gt; 的图示对删除算法展开具体的分析。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;从叶子节点中删除数据&#34;&gt;&#xA;  从叶子节点中删除数据&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%bb%8e%e5%8f%b6%e5%ad%90%e8%8a%82%e7%82%b9%e4%b8%ad%e5%88%a0%e9%99%a4%e6%95%b0%e6%8d%ae&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;从叶子节点中删除数据分为三种情况：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;删除之后的数据量足够，不需要进行重平衡操作；&lt;/li&gt;&#xA;&lt;li&gt;删除之后的数据量不够，但是可以从兄弟节点那里借用数据，重新达到平衡；&lt;/li&gt;&#xA;&lt;li&gt;删除之后的数据量不够，兄弟节点的数据也不够，那么需要合并成一个新的节点，同时在父节点中删除索引和指针。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下针对后面两种需要做重平衡的操作展开分析。&lt;/p&gt;&#xA;&lt;h4 class=&#34;heading&#34; id=&#34;借用兄弟节点数据进行重平衡操作&#34;&gt;&#xA;  借用兄弟节点数据进行重平衡操作&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%80%9f%e7%94%a8%e5%85%84%e5%bc%9f%e8%8a%82%e7%82%b9%e6%95%b0%e6%8d%ae%e8%bf%9b%e8%a1%8c%e9%87%8d%e5%b9%b3%e8%a1%a1%e6%93%8d%e4%bd%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h4&gt;&#xA;&lt;p&gt;在下图中，从叶子节点中删除数据之后，只剩下数据&lt;code&gt;[11]&lt;/code&gt;：&lt;/p&gt;</description>
    </item>
    <item>
      <title>B树、B&#43;树索引算法原理（上）</title>
      <link>https://www.codedump.info/zh/post/20200609-btree-1/</link>
      <pubDate>Tue, 09 Jun 2020 18:40:46 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200609-btree-1/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;这一段时间由于在阅读boltdb代码的缘故，找机会学习了B树及B+树的算法原理，这个系列会花两个篇幅分别介绍这两种数据结构的实现，其用于数据库索引中的基本原理。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;b树数据库索引原理&#34;&gt;&#xA;  B树数据库索引原理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#b%e6%a0%91%e6%95%b0%e6%8d%ae%e5%ba%93%e7%b4%a2%e5%bc%95%e5%8e%9f%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在一堆数据中查找一个数据时，常用的数据结构有二叉查找树（binary search tree，简称BST）、哈希桶等。以BST为例，常见的实现有AVT、红黑树等，由于这类型的树是平衡的，每次比较操作都会去掉当前数据量一半的数据，因此查找的时间复杂度为&lt;code&gt;O(log2n)&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;bst-example&#34; src=&#34;https://www.codedump.info/media/imgs/20200609-btree-1/bst-example.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; bst-example &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;但是这类型数据结构的问题在于，由于每个节点只能容纳一个数据，导致树的高度很高，逻辑上挨着的节点数据可能离的很远。如果是在内存中操作数据的话，这样问题并不大。&lt;/p&gt;&#xA;&lt;p&gt;考虑在磁盘中存储数据的情况，与内存相比，读写磁盘有以下不同点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;读写磁盘的速度相比内存读写慢很多。&lt;/li&gt;&#xA;&lt;li&gt;因为上面的原因，因此每次读写磁盘的单位要比读写内存的最小单位大很多。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因为读写磁盘的这个特点，因此对应的数据结构应该尽量的满足“局部性原理”：“当一个数据被用到时，其附近的数据也通常会马上被使用”，为了满足局部性原理，应该：&lt;strong&gt;将逻辑上相邻的数据在物理上也尽量存储在一起&lt;/strong&gt;。这样才能减少读写磁盘的数量。&lt;/p&gt;&#xA;&lt;p&gt;所以，对比起一个节点只能存储一个数据的BST类数据结构来，要求这种数据结构在形状上更“胖”、更加“扁平”，即：每个节点能容纳更多的数据，这样就能降低树的高度，同时让逻辑上相邻的数据都能尽量的存储在物理上也相邻的硬盘空间上，减少磁盘读写。&lt;/p&gt;&#xA;&lt;p&gt;以下图为例：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;disk-ds&#34; src=&#34;https://www.codedump.info/media/imgs/20200609-btree-1/disk-ds.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; disk-ds &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;图中从根节点出发，查找数据14的过程中，经过的第二个节点中有键值&lt;code&gt;[3,7,13]&lt;/code&gt;，这三个值在“逻辑”上是相邻的，如果它们在磁盘上的存储也能做到在“物理”上相邻，那么只需要一次读操作就能把这个节点的数据从磁盘上加载到内存中进行数据比较，这样整个查找过程就只需要两次磁盘读操作。&lt;/p&gt;&#xA;&lt;p&gt;在这里，一个节点越“胖”，意味着扇出（fanout）越大，同时高度越低，这两个性质决定了：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;高扇出：邻近键值的数据局部性更好。&lt;/li&gt;&#xA;&lt;li&gt;低高度：遍历期间的寻道次数更少。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以证明，查找数据的次数（searchnum）与degree、以及数据总量有以下关系：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;btree-num&#34; src=&#34;https://www.codedump.info/media/imgs/20200609-btree-1/btree-num.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; btree-num &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;B树和B+树就是两种利用磁盘局部性原理进行优化的树结构，B+树基于B树做了一些改进，这里首先将介绍B树的原理。本系列将用两篇文章讲解这两种数据结构的原理，并且提供Python实现代码。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;b树的定义及性质&#34;&gt;&#xA;  B树的定义及性质&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#b%e6%a0%91%e7%9a%84%e5%ae%9a%e4%b9%89%e5%8f%8a%e6%80%a7%e8%b4%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在B树中，分为两种节点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;内部节点（internal node）：存储了数据以及指向其子节点的指针。&lt;/li&gt;&#xA;&lt;li&gt;叶子节点（leaf node）：与内部节点不同的是，叶子节点只存储数据，并没有子节点。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;一个数据，既可能存在内部节点上，也可能存在叶子节点上，这一点是与后面讲到的B+树最大的不同，后者只会将数据存储在叶子节点上。&lt;/p&gt;&#xA;&lt;p&gt;创建B树时，需要输入一个degree参数（以下简写为t），该参数决定了每个节点上数据量的多少，即节点的“胖”、“瘦”程度，而节点的胖瘦程度又会影响整棵树的高度，因为越胖的节点树高度就会越矮。&lt;/p&gt;&#xA;&lt;p&gt;为了维持B树的平衡性，需要满足以下的属性：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;在每个节点上的键值，以递增顺序排列，即&lt;code&gt;node.keys[i] &amp;lt;= node.keys[i+1]&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;在一个键值左边的子树，其键值大于该键值右边子树的所有键值，即&lt;code&gt;node.keys[i] &amp;gt; max(node.child[i]的所有键值)&lt;/code&gt;；同时，在一个键值右边的子树，其键值的最小值都不小于该键值，即&lt;code&gt;node.keys[i] &amp;lt;= min(node.child[i + 1]的所有键值)&lt;/code&gt;。具体情况可以在下面的图中进行说明。&lt;/li&gt;&#xA;&lt;li&gt;在内部节点中，指向子节点的指针数量总是存储数据节点的数量+1，即：&lt;code&gt;num(node.child) = num(node.keys) + 1&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;所有叶子节点的高度一致。&lt;/li&gt;&#xA;&lt;li&gt;无论是内部节点还是叶子节点，其存储的键值数量在&lt;code&gt;[t-1,2t-1]&lt;/code&gt;之间，如果数量不满足此条件，需要做重平衡操作。如果少于&lt;code&gt;t-1&lt;/code&gt;，需要借用或合并数据；反之，如果数据量大于&lt;code&gt;2t-1&lt;/code&gt;，则需要分裂成两个节点。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;我们来看下面的图示，该图中的B树，t参数的值为2（&lt;strong&gt;需要特别说明的是，一棵树中每个存储数据的地方，应该既有键值（key）也有数据（value），本文中为了简单起见，存储的数据只有键值。&lt;/strong&gt;）：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;btree-example&#34; src=&#34;https://www.codedump.info/media/imgs/20200609-btree-1/btree-example.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; btree-example &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;由于&lt;code&gt;t=2&lt;/code&gt;，所有所有节点的键值数量在&lt;code&gt;[1,3]&lt;/code&gt;之间。&lt;/li&gt;&#xA;&lt;li&gt;所有叶子节点的高度相同。&lt;/li&gt;&#xA;&lt;li&gt;以左边的内部节点为例，其第一个键值为3，即该节点的&lt;code&gt;keys[0]=3&lt;/code&gt;，而该键值的左边子树的键值为&lt;code&gt;[1,2]&lt;/code&gt;，都小于3，即&lt;code&gt;keys[0]&amp;gt;max(child[0]的所有键值)&lt;/code&gt;；而其右边子树的键值为&lt;code&gt;[4,5,6]&lt;/code&gt;，都不小于3，即&lt;code&gt;keys[0]&amp;lt;=min(child[1]的所有键值)&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;b树算法原理&#34;&gt;&#xA;  B树算法原理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#b%e6%a0%91%e7%ae%97%e6%b3%95%e5%8e%9f%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;了解了B树的性质，下面讨论B树中的两个核心操作：插入及删除。这两个操作的核心，都是在操作如果破坏了B树的平衡性之后，进行重新平衡以满足B树的性质。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;插入数据&#34;&gt;&#xA;  插入数据&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%8f%92%e5%85%a5%e6%95%b0%e6%8d%ae&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;向B树中插入一个数据，可能会导致节点的数据变满，即不满足上面提到的节点数据数量在&lt;code&gt;[t,2t-1]&lt;/code&gt;这个性质。此时需要对节点进行分裂节点操作：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;将数据变满（即节点数据量为&lt;code&gt;2t&lt;/code&gt;）的节点，分为左右两个数据量分别为&lt;code&gt;t-1&lt;/code&gt;的节点，同时将中间的数据提升到父节点的合适位置上。&lt;/li&gt;&#xA;&lt;li&gt;如果父节点由于新增了这个被提升的数据导致了变满，就继续上面的分裂节点操作。&lt;/li&gt;&#xA;&lt;li&gt;沿着树向上一直执行该操作，直到不再变满为止。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;分裂操作的示意图如下：&lt;/p&gt;</description>
    </item>
    <item>
      <title>如何阅读一份源代码？（2020年版）</title>
      <link>https://www.codedump.info/zh/post/20200605-how-to-read-code/</link>
      <pubDate>Fri, 05 Jun 2020 00:14:14 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200605-how-to-read-code/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;我在2019年写过一份文档 &lt;a href=&#34;https://www.codedump.info/post/20190324-how-to-read-code/&#34;&gt;《如何阅读一份源代码？》&lt;/a&gt;，现在回头来看，这份文档还有改进的空间，于是我在此基础上又重新整理了一份。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;阅读源代码的能力算是程序员的一种底层基础能力之一，这个能力之所以重要，原因在于：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;不可避免的需要阅读或者接手他人的项目。比如调研一个开源项目，比如接手一个其他人的项目。&lt;/li&gt;&#xA;&lt;li&gt;阅读优秀的项目源码是学习他人优秀经验的重要途径之一，这一点我自己深有体会。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;读代码与写代码是两个不太一样的技能，原因在于“&lt;a href=&#34;https://www.zhihu.com/question/21820752/answer/19427754&#34;&gt;写代码是在表达自己,读代码是在理解别人&lt;/a&gt;”。因为面对的项目多，项目的作者有各自的风格，理解起来需要花费不少的精力。&lt;/p&gt;&#xA;&lt;p&gt;我从业这些年泛读、精读过的项目源码不算少了，陆陆续续的也写了一些代码分析的文章，本文中就简单总结一下我的方法。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;先跑起来&#34;&gt;&#xA;  先跑起来&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%85%88%e8%b7%91%e8%b5%b7%e6%9d%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;开始阅读一份项目源码的第一步，是先让这个项目能够通过你自己编译通过并且顺利跑起来。这一点尤其重要。&lt;/p&gt;&#xA;&lt;p&gt;有的项目比较复杂，依赖的组件多，搭建起一个调试环境并不容易，所以并不见得是所有项目都能顺利的跑起来。如果能自己编译跑起来，那么后面讲到的情景分析、加上调试代码、调试等等才有展开的基础。&lt;/p&gt;&#xA;&lt;p&gt;就我的经验而言，一个项目代码，是否能顺利的搭建调试环境，效率大不一样。&lt;/p&gt;&#xA;&lt;p&gt;跑起来之后，又要尽量的精简自己的环境，减少调试过程中的干扰信息。比如，Nginx使用多进程的方式处理请求，为了调试跟踪Nginx的行为，我经常把worker数量设置为1个，这样调试的时候就知道待跟踪的是哪个进程了。&lt;/p&gt;&#xA;&lt;p&gt;再比如，很多项目默认是会带上编译优化选项或者去掉调试信息的，这样在调试的时候可能会有困扰，这时候我会修改makefile编译成&lt;code&gt;-O0 -g&lt;/code&gt;，即编译生成带上调试信息且不进行优化的版本。&lt;/p&gt;&#xA;&lt;p&gt;总而言之，跑起来之后的调试效率能提升很多，而在跑起来的前提之下又要尽量精简环境排除干扰的因素。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;明确自己的目的&#34;&gt;&#xA;  明确自己的目的&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%98%8e%e7%a1%ae%e8%87%aa%e5%b7%b1%e7%9a%84%e7%9b%ae%e7%9a%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;尽管阅读项目源码很重要，但是并不见得所有项目都需要从头到尾看的清清楚楚。在开始展开阅读之前，需要明确自己的目的：是需要了解其中一个模块的实现，还是需要了解这个框架的大体结构，还是需要具体熟悉其中的一个算法的实现，等等。&lt;/p&gt;&#xA;&lt;p&gt;比如，很多人看Nginx的代码，而这个项目有很多模块，包括基础的核心模块（epoll、网络收发、内存池等）和扩展具体某个功能的模块，并不是所有这些模块都需要了解的非常清楚，我在阅读Nginx代码的过程中，主要涉及了以下方面：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;了解Nginx核心的基础流程以及数据结构。&lt;/li&gt;&#xA;&lt;li&gt;了解Nginx如何实现一个模块。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;有了这些对这个项目大体的了解，剩下的就是遇到具体的问题查看具体的代码实现了。&lt;/p&gt;&#xA;&lt;p&gt;总而言之，并不建议毫无目的的就开始展开一个项目的代码阅读，无头苍蝇式的乱看只会消耗自己的时间和热情。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;区分主线和支线剧情&#34;&gt;&#xA;  区分主线和支线剧情&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8c%ba%e5%88%86%e4%b8%bb%e7%ba%bf%e5%92%8c%e6%94%af%e7%ba%bf%e5%89%a7%e6%83%85&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;有了前面明确的阅读目的，就能在阅读过程中区分开主线和支线剧情了。比如：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;想了解一个业务逻辑的实现流程，在某个函数中使用一个字典来保存数据，在这里，“字典这个数据结构是如何实现的”就属于支线剧情，并不需要深究其实现。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在这一原则的指导下，对于支线剧情的代码，比如一个不需要了解其实现的类，读者只需要了解其对外接口，了解这些接口的入口、出口参数以及作用，把这部分当成一个“黑盒”即可。&lt;/p&gt;&#xA;&lt;p&gt;顺便一提的是，早年间看到一种C++的写法，头文件中只有一个类的对外接口声明，将实现通过内部的impl类转移到C++文件中，比如：&lt;/p&gt;&#xA;&lt;p&gt;头文件：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// test.h&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;class Test {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;public:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;fun&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;private:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  class Impl;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Impl *impl_;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;C++文件：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt; Test::&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;fun&lt;/span&gt;() {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  impl_-&amp;gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;fun&lt;/span&gt;()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;class Test::Impl {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;public:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;fun&lt;/span&gt;() {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 具体的实现&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样的写法，让头文件清爽了很多：头文件中没有与实现相关的私有成员、私有函数，只有对外暴露的接口，使用者一目了然就能知道这个类对外提供的功能。&lt;/p&gt;</description>
    </item>
    <item>
      <title>《面向应用开发者的系统指南》CPU篇之软中断</title>
      <link>https://www.codedump.info/zh/post/20200522-sgfap-softirq/</link>
      <pubDate>Fri, 22 May 2020 21:52:58 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200522-sgfap-softirq/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文是《面向应用开发者的系统指南》文档其中的一篇，完整的目录见&lt;a href=&#34;https://www.codedump.info/post/20200501-system-guide-for-application-programmer/&#34;&gt;《面向应用开发者的系统指南》导论&lt;/a&gt;。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;中断（interrupt）通常被定义为一个事件，该事件改变处理器执行的指令顺序。中断分为同步和异步两种：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;同步中断在指令执行时由CPU控制单元产生，之所以称为同步，是因为只有在一条指令终止执行后CPU才发生中断。&lt;/li&gt;&#xA;&lt;li&gt;异步中断是由其他硬件设备依照CPU时钟信号随机产生的。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在Intel的处理器手册中，将同步中断称为“异常（exception）”，异步中断称为“中断”。&lt;/p&gt;&#xA;&lt;p&gt;异常通常由程序的错误产生，或者是由内核必须处理的异常条件产生的。比如程序中有除零异常，比如进程运行过程中产生的“缺页异常（pagefault）”等，都属于异常。&lt;/p&gt;&#xA;&lt;p&gt;而中断是由定时器和I/O设备产生的，比如用户的一次按键、网卡收到数据，都会产生中断。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;interrupt-type&#34; src=&#34;https://www.codedump.info/media/imgs/20200522-sgfap-softirq/interrupt-type.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; interrupt-type &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;处理器一旦收到中断，就必须打断当前的执行，转而去执行中断处理函数。中断处理函数，本身有一些缺陷：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;不能在进程上下文中执行，因此不能阻塞。&lt;/li&gt;&#xA;&lt;li&gt;中断处理程序会打断程序执行，为了避免这个打断的流程停止时间过长，所以应该执行的越短越好。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因为以上的原因，Linux内核将中断的处理分为了上下两部分，其中上半部就是前面提到的中断处理函数，这部分能够最快的响应中断，并且做一些中断后必须要做的事情，而一些可以在中断处理函数后继续执行的操作，则可以放在下半部中。&lt;/p&gt;&#xA;&lt;p&gt;以网卡接收到数据来举例，网卡通过中断告诉内核有数据可以接收，此时内核就会到网卡的中断处理程序中执行一些网卡硬件的必要设置，而对应的下半部就是处理网卡收到的数据，因为处理网卡数据没有必要在中断处理函数里面马上执行。&lt;/p&gt;&#xA;&lt;p&gt;两者的主要区别在于：中断不能被相同类型的中断打断，而下半部依然可以被中断打断；中断对于时间非常敏感，而下半部基本上都是一些可以延迟的工作。由于二者的这种区别，所以对于一个工作是放在上半部还是放在下半部去执行，可以参考下面4条：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如果一个任务对时间非常敏感，将其放在中断处理程序中执行。&lt;/li&gt;&#xA;&lt;li&gt;如果一个任务和硬件相关，将其放在中断处理程序中执行。&lt;/li&gt;&#xA;&lt;li&gt;如果一个任务要保证不被其他中断（特别是相同的中断）打断，将其放在中断处理程序中执行。&lt;/li&gt;&#xA;&lt;li&gt;其他所有任务，考虑放在下半部去执行。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;有写内核任务需要延后执行，因此才有的下半部，进而实现了三种实现下半部的方法。这就是本文要讨论的软中断、tasklet和工作队列。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;软中断&#34;&gt;&#xA;  软中断&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%bd%af%e4%b8%ad%e6%96%ad&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;软中断作为下半部机制的代表，是随着SMP（share memory processor）的出现应运而生的，它也是tasklet实现的基础（tasklet实际上只是在软中断的基础上添加了一定的机制）。软中断一般是“可延迟函数”的总称，有时候也包括了tasklet（请读者在遇到的时候根据上下文推断是否包含tasklet）。它的出现就是因为要满足上面所提出的上半部和下半部的区别，使得对时间不敏感的任务延后执行，而且可以在多个CPU上并行执行，使得总的系统效率可以更高。它的特性包括：&lt;/p&gt;&#xA;&lt;p&gt;产生后并不是马上可以执行，必须要等待内核的调度才能执行。软中断不能被自己打断(即单个cpu上软中断不能嵌套执行)，只能被硬件中断打断（上半部）。&#xA;可以并发运行在多个CPU上（即使同一类型的也可以）。所以软中断必须设计为可重入的函数（允许多个CPU同时操作），因此也需要使用自旋锁来保其数据结构。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;数据结构&#34;&gt;&#xA;  数据结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;软中断由系统在启动的时候注册到内核中，由一个全局数组来维护软中断：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; softirq_action&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt;&#x9;(*action)(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; softirq_action *);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到，本质上结构体softirq_action存储的是函数指针而已，软中断有以下类型：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;enum&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;HI_SOFTIRQ=0,     &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 处理高优先级的tasklet&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&#x9;TIMER_SOFTIRQ,    &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 定时器的下半部&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&#x9;NET_TX_SOFTIRQ,   &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 网卡发送数据包&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&#x9;NET_RX_SOFTIRQ,   &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 网卡接收数据包&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&#x9;BLOCK_SOFTIRQ,    &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// BLOCK装置     &#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&#x9;IRQ_POLL_SOFTIRQ, &#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;TASKLET_SOFTIRQ,  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 处理常规的tasklet&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&#x9;SCHED_SOFTIRQ,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;HRTIMER_SOFTIRQ, &#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;RCU_SOFTIRQ,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;NR_SOFTIRQS&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;系统提供了open_softirq函数用于各个需要使用到软中断的系统注册对应的软中断处理函数。&lt;/p&gt;</description>
    </item>
    <item>
      <title>《面向应用开发者的系统指南》CPU篇之系统调用</title>
      <link>https://www.codedump.info/zh/post/20200516-sgfap-syscall/</link>
      <pubDate>Sat, 16 May 2020 16:31:03 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200516-sgfap-syscall/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文是《面向应用开发者的系统指南》文档其中的一篇，完整的目录见&lt;a href=&#34;https://www.codedump.info/post/20200501-system-guide-for-application-programmer/&#34;&gt;《面向应用开发者的系统指南》导论&lt;/a&gt;。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;应用程序需要使用内核提供出来的一些功能，才能完成相应的操作，这个由内核提供出来给用户态程序调用的接口，就是“系统调用（system call）”。比如打开文件时需要调用&lt;code&gt;open&lt;/code&gt;系统调用，写文件时需要调用&lt;code&gt;write&lt;/code&gt;系统调用，等等。&lt;/p&gt;&#xA;&lt;p&gt;本节将简单描述Linux在X86下系统调用的工作原理，接着描述如何追踪用户层进程的系统调用。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;系统调用原理&#34;&gt;&#xA;  系统调用原理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%b3%bb%e7%bb%9f%e8%b0%83%e7%94%a8%e5%8e%9f%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;传统系统调用legacy-system-calls&#34;&gt;&#xA;  传统系统调用（Legacy system calls）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%bc%a0%e7%bb%9f%e7%b3%bb%e7%bb%9f%e8%b0%83%e7%94%a8legacy-system-calls&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在这里先讨论系统调用的传统实现方式，在这里需要解决以下几个问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;用户态怎么触发系统调用？&lt;/li&gt;&#xA;&lt;li&gt;用户态怎么传递参数给系统调用？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;内核预留了一个特殊的软中断号 128 (0x80)，用户空间程序使用它可以进入内核执行系统调用，在内核中定义了宏&lt;code&gt;IA32_SYSCALL_VECTOR&lt;/code&gt;与之对应：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// arch/x86/include/asm/irq_vectors.h&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;#define IA32_SYSCALL_VECTOR&#x9;&#x9;0x80&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;触发给软中断时会调用到汇编编写的函数&lt;code&gt;&#x9;entry_INT80_32&lt;/code&gt;中：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// arch/x86/kernel/idt.h&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;SYSG&lt;/span&gt;(IA32_SYSCALL_VECTOR,&#x9;entry_INT80_32),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;entry_INT80_32&lt;/code&gt;函数在&lt;code&gt;arch/x86/entry/entry_32.S&lt;/code&gt;中实现，其最终会走到&lt;code&gt;do_int80_syscall_32&lt;/code&gt;函数中调用系统调用。&lt;/p&gt;&#xA;&lt;p&gt;以上解决了第一个问题，即用户态通过触发软中断&lt;code&gt;int 0x80&lt;/code&gt;来调用系统调用的，接下来的问题是，内核如何知道调用的是哪个系统调用，以及怎么解决给系统调用传递参数的问题。&lt;/p&gt;&#xA;&lt;p&gt;在函数&lt;code&gt;entry_INT80_32&lt;/code&gt;的注释中，看到有如下的描述：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// arch/x86/entry/entry_32.S&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;/*&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt; * Arguments:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt; * eax  system call number&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt; * ebx  arg1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt; * ecx  arg2&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt; * edx  arg3&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt; * esi  arg4&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt; * edi  arg5&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt; * ebp  arg6&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt; */&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可见，寄存器&lt;code&gt;eax&lt;/code&gt;中存放的是系统调用编号，接下来的几个寄存器分别存放传递进来的参数。&lt;/p&gt;</description>
    </item>
    <item>
      <title>《面向应用开发者的系统指南》CPU篇之使用systemtap分析进程的行为</title>
      <link>https://www.codedump.info/zh/post/20200503-sgfap-process-systemtap/</link>
      <pubDate>Sun, 03 May 2020 14:32:57 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200503-sgfap-process-systemtap/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文是《面向应用开发者的系统指南》文档其中的一篇，完整的目录见&lt;a href=&#34;https://www.codedump.info/post/20200501-system-guide-for-application-programmer/&#34;&gt;《面向应用开发者的系统指南》导论&lt;/a&gt;。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;以上描述进程的创建、执行、调度器的工作原理，有了这些准备之后，可以使用systemtap在系统中埋点进行一些跟踪，以便理解进程的行为。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;分析进程对cpu的占用&#34;&gt;&#xA;  分析进程对CPU的占用&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%88%86%e6%9e%90%e8%bf%9b%e7%a8%8b%e5%af%b9cpu%e7%9a%84%e5%8d%a0%e7%94%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;简单回顾一下前面进程调度相关的内容：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;内核中使用就绪队列来维护当前所有处于可运行状态的进程，可运行状态不包括等待IO、休眠等状态的进程。&lt;/li&gt;&#xA;&lt;li&gt;进程调度器负责从就绪队列中选择处于可运行状态的进程来执行。&lt;/li&gt;&#xA;&lt;li&gt;而所有不处于可运行状态的进程，并不占用CPU资源，这些进程都等待被相关的事件比如网络IO唤醒，唤醒之后的进程更改状态为可运行状态，同时加入到就绪队列中，然后才能被调度器算法选择执行。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因此，一个进程的整个生命周期中，虽然看上去进程一直存在，但是并不是所有时候都占用CPU资源。根据CPU占用资源与否，或者说当前是否在运行，分为&lt;code&gt;on cpu&lt;/code&gt;和&lt;code&gt;off cpu&lt;/code&gt;状态：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;onoffcpu&#34; src=&#34;https://www.codedump.info/media/imgs/20200503-sgfap-process-systemtap/onoffcpu.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; onoffcpu &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中就一个进程执行的时间线做了简单的阶段划分，其中省略掉了进程被创建出来和最后退出时的情况，仅列出占用CPU资源状态的切换。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;进程占用CPU获得执行权的时候，称为&lt;code&gt;on cpu&lt;/code&gt;时间。&lt;/li&gt;&#xA;&lt;li&gt;进程因为各种原因（被其他进程抢占、自己调用了sleep系统调用主动进入睡眠状态、等待网络IO等）被剥夺了执行权的时候，首先会调用&lt;code&gt;deactivate_task&lt;/code&gt;函数从就绪队列中删除，接下来调用&lt;code&gt;context_switch&lt;/code&gt;函数进行进程的上下文切换，这个时候旧的进程失去CPU的执行权，此时正式进入&lt;code&gt;off cpu&lt;/code&gt;时间中。&lt;/li&gt;&#xA;&lt;li&gt;在此之后，进程由于各种原因被唤醒，唤醒之后首先会被再次调用&lt;code&gt;activate_task&lt;/code&gt;函数加入到就绪队列中，进入就绪队列的进程也并不是马上就能够获得执行权的，是由进程调度算法来决定哪一个在就绪队列中的进程来执行。这段时间又可以分为两个部分：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;进程被切换出去直到重新进入就绪队列，这部分时间内进程等待被唤醒。&lt;/li&gt;&#xA;&lt;li&gt;进入就绪队列到被调度器选中执行，这部分时间内进程等待被调度执行。&lt;/li&gt;&#xA;&lt;li&gt;以上两部分时间的总和，加起来就是进程休眠的时间，即处于&lt;code&gt;off cpu&lt;/code&gt;状态的时间。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从这里看出来，一个进程虽然看上去一直存在，但并不是所有时间都在执行，跟进一个程序的运行时间时，需要区分其&lt;code&gt;on&lt;/code&gt;和&lt;code&gt;off&lt;/code&gt; cpu的时间，如果off的时间过长，那需要看看是什么原因导致了进程一直没有被唤醒执行。&lt;/p&gt;&#xA;&lt;p&gt;另外需要注意的是，进程处于就绪状态，并不一定就是在运行，有可能还在就绪队列中等待被调度执行；但是反之则不然，一个占用CPU在执行的进程，其状态一定是就绪状态。即：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;进程处于就绪状态的时间 = 进程在就绪队列的时间 + 进程在执行的时间&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;关于&lt;code&gt;off cpu&lt;/code&gt;这一概念，&lt;a href=&#34;http://www.brendangregg.com/offcpuanalysis.html&#34;&gt;Off-CPU Analysis&lt;/a&gt;一文中有更多的讲述。&lt;/p&gt;&#xA;&lt;p&gt;有了上面对&lt;code&gt;on cpu&lt;/code&gt;和&lt;code&gt;off cpu&lt;/code&gt;的介绍，下面来看看使用systemtap如何跟踪这些状态以及所处的时间。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;off-cpu&#34;&gt;&#xA;  off CPU&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#off-cpu&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;systemtap中自带的tapset中，有一个scheduler.stp文件，里面定义了与调度器相关的一些probe。&lt;/p&gt;&#xA;&lt;p&gt;其中跟踪&lt;code&gt;off cpu&lt;/code&gt;的probe是&lt;code&gt;scheduler.cpu_off&lt;/code&gt; ：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;probe scheduler.cpu_off =&#xA;&#x9;kernel.trace(&amp;#34;sched_switch&amp;#34;) !,&#xA;&#x9;kernel.function(&amp;#34;context_switch&amp;#34;)&#xA;{&#xA;    name = &amp;#34;cpu_off&amp;#34;&#xA;    task_prev = $prev&#xA;    task_next = $next&#xA;    idle = __is_idle()&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;结合代码和最开始的示意图，可以知道该probe事件是针对内核trace事件&lt;code&gt;sched_switch&lt;/code&gt;以及内核函数&lt;code&gt;context_switch&lt;/code&gt;的封装，这两个事件都在进程上下文切换时触发。&lt;/p&gt;&#xA;&lt;p&gt;在该probe事件中，能获取到的参数是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;task_prev：保存切换之前的进程&lt;code&gt;task_struct&lt;/code&gt;结构体。&lt;/li&gt;&#xA;&lt;li&gt;task_next：保存切换之后的进程&lt;code&gt;task_struct&lt;/code&gt;结构体。&lt;/li&gt;&#xA;&lt;li&gt;idle：表示当前CPU是否空闲。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因为这个probe事件记录了进程切换前后的信息，因此可以用来完成类似记录系统切换最多的进程跟踪的功能：&lt;/p&gt;</description>
    </item>
    <item>
      <title>《面向应用开发者的系统指南》CPU篇之进程调度</title>
      <link>https://www.codedump.info/zh/post/20200503-sgfap-process-schedule/</link>
      <pubDate>Sun, 03 May 2020 09:53:34 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200503-sgfap-process-schedule/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文是《面向应用开发者的系统指南》文档其中的一篇，完整的目录见&lt;a href=&#34;https://www.codedump.info/post/20200501-system-guide-for-application-programmer/&#34;&gt;《面向应用开发者的系统指南》导论&lt;/a&gt;。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;一种资源，如果本身数量有限，需要多个资源需求方来使用的情况下，就涉及到&lt;code&gt;资源调度&lt;/code&gt;的问题。在内核中，CPU就是一种有限的资源，同时在系统中处于运行状态的进程数量有很多，此时就需要设计出一种方法，尽可能的保证这种资源被公平的分配到进程中间。&lt;/p&gt;&#xA;&lt;p&gt;Linux内核中的进程调度，涉及到以下几个重要概念：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;核心调度器：核心调度器可以认为是内核中进程调度模块，对外提供了周期性调度（定时触发）以及主调度器两个接口。&lt;/li&gt;&#xA;&lt;li&gt;就绪队列：所有当前运行的进程都在这个队列中维护，需要选择出下一个执行的进程也从这个队列中选举。&lt;/li&gt;&#xA;&lt;li&gt;调度优先级：给予不同的进程不同的优先级，这样分配到的时间就不一样。&lt;/li&gt;&#xA;&lt;li&gt;调度算法：不同类型的进程使用不同的调度算法来选择执行进程。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下来简单阐述这几个组件如何一起作用完成进程调度的工作。&lt;/p&gt;&#xA;&lt;p&gt;每个CPU维护自己的就绪队列，就绪队列由结构体&lt;code&gt;rq&lt;/code&gt;来表示，队列中的每个元素都是前面提到的描述进程信息的结构体&lt;code&gt;task_struct&lt;/code&gt;。这里需要注意的是，虽然称之为“队列”，内部的实现中，根据不同的调度算法，使用了不同的数据结构来保存进程，比如CFS调度器使用了红黑树来保存进程，这一点在后面展开阐述，目前为止，暂且认为就绪队列是一个维护CPU所有当前就绪进程的容器。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;runqueue&#34; src=&#34;https://www.codedump.info/media/imgs/20200503-sgfap-process-schedule/runqueue.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; runqueue &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;不同的调度器算法，无论内部如何实现，其最终都是从就绪队列中选择下一个可执行的进程来运行。&#xA;在这个版本的内核中一共实现了如下几种调度器算法，它们统一由结构体&lt;code&gt;sched_class&lt;/code&gt;来表示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;sched_class&#34; src=&#34;https://www.codedump.info/media/imgs/20200503-sgfap-process-schedule/sched_class.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; sched_class &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;调度器&lt;/th&gt;&#xA;          &lt;th&gt;描述&lt;/th&gt;&#xA;          &lt;th&gt;对应调度策略&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;dl_sched_class&lt;/td&gt;&#xA;          &lt;td&gt;deadline调度器&lt;/td&gt;&#xA;          &lt;td&gt;SCHED_DEADLINE&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;rt_sched_class&lt;/td&gt;&#xA;          &lt;td&gt;实时调度器&lt;/td&gt;&#xA;          &lt;td&gt;SCHED_FIFO、SCHED_RR&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;fair_sched_class&lt;/td&gt;&#xA;          &lt;td&gt;完全公平调度器&lt;/td&gt;&#xA;          &lt;td&gt;SCHED_NORMAL、SCHED_BATCH&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;idle_sched_class&lt;/td&gt;&#xA;          &lt;td&gt;idle调度器&lt;/td&gt;&#xA;          &lt;td&gt;SCHED_IDLE&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;以上列举了进程的几种调度器及对应的调度策略，其优先级依次递减。在下面的内容中，将详细介绍完全公平调度器（Completely Fair Scheduler，简称CFS），因为这是最普遍的进程调度器。&lt;/p&gt;&#xA;&lt;p&gt;从以上的介绍可以看到，内核的调度器负责维护就绪队列，即提供了调度进程所需的数据来源；而不同的调度器算法则根据自己的实现来从就绪队列中选择进程来执行，那么选择的依据又是什么？答案是进程的优先级。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;schedule&#34; src=&#34;https://www.codedump.info/media/imgs/20200503-sgfap-process-schedule/schedule.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; schedule &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;以上简单阐述了Linux进程调度中涉及到的四个最重要的要素，下面将展开讨论。&lt;/p&gt;&#xA;&lt;p&gt;首先将介绍进程的优先级，通过这个值如何计算得到进程的权重，进一步得到&lt;code&gt;CFS&lt;/code&gt;调度器算法中所需的虚拟运行时间。&lt;/p&gt;&#xA;&lt;p&gt;紧接着介绍与进程调度相关的数据结构，以及内核中进程调度的核心调度器的实现。&lt;/p&gt;&#xA;&lt;p&gt;最后就是详细展开&lt;code&gt;CFS&lt;/code&gt;调度器内部的实现。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;优先级权重和虚拟运行时间&#34;&gt;&#xA;  优先级、权重和虚拟运行时间&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%bc%98%e5%85%88%e7%ba%a7%e6%9d%83%e9%87%8d%e5%92%8c%e8%99%9a%e6%8b%9f%e8%bf%90%e8%a1%8c%e6%97%b6%e9%97%b4&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;优先级&#34;&gt;&#xA;  优先级&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%bc%98%e5%85%88%e7%ba%a7&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Linux通过nice命令设置进程的静态优先级，进程的nice值在[-20,19]之间，值越小优先级越高。而内核本身，选择范围[0,139]在内部表示优先级，同样是数值越低优先级越高：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;nice&#34; src=&#34;https://www.codedump.info/media/imgs/20200503-sgfap-process-schedule/nice.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; nice &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;对于普通的进程，可以认为优先级不会发生变化，而实时进程则不然：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// kernel/sched/core.c&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;effective_prio&lt;/span&gt;(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; task_struct *p)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;p-&amp;gt;normal_prio = &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;normal_prio&lt;/span&gt;(p);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 如果不是实时进程，返回前面normal_prio的计算结果&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;if&lt;/span&gt; (!&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;rt_prio&lt;/span&gt;(p-&amp;gt;prio))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;return&lt;/span&gt; p-&amp;gt;normal_prio;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;return&lt;/span&gt; p-&amp;gt;prio;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;由于在这里不讨论实时进程，仅讨论普通进程，因此可以认为进程优先级就是静态不变的。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;cpu时间权重&#34;&gt;&#xA;  CPU时间权重&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#cpu%e6%97%b6%e9%97%b4%e6%9d%83%e9%87%8d&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;CFS调度器的设计理念，就是能够实现理想、精确的多任务CPU进程调度。与以往的调度器不同的是，CFS调度器没有时间片的概念，使用的是分配CPU时间的比例。通过进程的优先级，就可以计算出来一个进程在就绪队列中所占时间的权重了。&lt;/p&gt;</description>
    </item>
    <item>
      <title>《面向应用开发者的系统指南》CPU篇之进程</title>
      <link>https://www.codedump.info/zh/post/20200502-sgfap-process/</link>
      <pubDate>Sat, 02 May 2020 14:41:22 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200502-sgfap-process/</guid>
      <description>&lt;p&gt;本文是《面向应用开发者的系统指南》文档其中的一篇，完整的目录见&lt;a href=&#34;https://www.codedump.info/post/20200501-system-guide-for-application-programmer/&#34;&gt;《面向应用开发者的系统指南》导论&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概论&#34;&gt;&#xA;  概论&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%ae%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;进程即处于执行状态的程序。操作系统执行进程时，大体经历了以下步骤：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;为进程分配空间及其他相关资源。&lt;/li&gt;&#xA;&lt;li&gt;加载进程可执行程序到内存中。&lt;/li&gt;&#xA;&lt;li&gt;由调度器选择进程来占用CPU资源执行。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从上面的描述可以看到，进程并不是仅仅只有可执行程序二进制文件就可以运行起来，还需要执行时所需要的资源（内存、CPU等）、进程执行时需要的其他共享库等。&lt;/p&gt;&#xA;&lt;p&gt;在现代操作系统中，进程提供了针对两种资源的虚拟机制：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;虚拟处理器。&lt;/li&gt;&#xA;&lt;li&gt;虚拟内存。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;虽然在同一时间有多个进程在执行，即分享处理器资源，但是虚拟处理器提供了一种假象：让这些进程认为自己都在独占处理器执行，这里涉及到进程调度部分的内容，在后面进程调度篇再展开讨论。&lt;/p&gt;&#xA;&lt;p&gt;同时，进程使用的内存实际上虚拟内存，虚拟内存机制使进程以为自己拥有整个4G空间（32位处理器下）而不必关心其他进程的内存空间，这部分内容在内存篇中讲解。&lt;/p&gt;&#xA;&lt;p&gt;程序本身并不是进程，进程是在执行的程序以及相关资源的总称。&lt;/p&gt;&#xA;&lt;p&gt;本篇从进程开始讲起，涉及内核管理进程的数据结构、与进程创建和执行相关的系统调用、进程的状态。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;数据结构&#34;&gt;&#xA;  数据结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;Linux内核使用&lt;code&gt;task_struct&lt;/code&gt;结构体来描述一个进程的所有信息。考虑这个结构体的内容太多，这里并不打算全部列举出来，到讲解需要涉及到的时候才提出其中的某些成员详细说明，这里只列出最关键的几个成员：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;task_struct&#34; src=&#34;https://www.codedump.info/media/imgs/20200502-sgfap-process/task_struct.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; task_struct &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;state：用于表示进程的状态，下面将展开讨论。&lt;/li&gt;&#xA;&lt;li&gt;pid：每个进程都有一个pid与之对应。&lt;/li&gt;&#xA;&lt;li&gt;mm：类型为&lt;code&gt;mm_struct&lt;/code&gt;，用于表示进程的内存地址信息，后面内存部分将展开讨论。&lt;/li&gt;&#xA;&lt;li&gt;fs：类型为&lt;code&gt;fs_struct&lt;/code&gt;，用于表示文件系统信息，后面IO部分将展开讨论。&lt;/li&gt;&#xA;&lt;li&gt;files：类型为&lt;code&gt;files_struct&lt;/code&gt;，用于表示进程打开文件的信息，后面IO部分将展开讨论。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;进程的状态&#34;&gt;&#xA;  进程的状态&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%bf%9b%e7%a8%8b%e7%9a%84%e7%8a%b6%e6%80%81&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;task_struct&lt;/code&gt;中的&lt;code&gt;state&lt;/code&gt;成员，用于表示当前进程的状态，进程的状态必然处于以下五种状态之一：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;TASK_RUNNING：进程是可执行的（Runnable），表示进程要么正在执行，要么正要准备执行（已经就绪），等待cpu时间片的调度。&lt;/li&gt;&#xA;&lt;li&gt;TASK_INTERRUPTIBLE：进程因为等待一些条件而被挂起（阻塞）而所处的状态。这些条件主要包括：硬中断、资源、一些信号等，一旦等待的条件成立，进程就会从该状态（阻塞）迅速转化成为就绪状态TASK_RUNNING。&lt;/li&gt;&#xA;&lt;li&gt;TASK_UNINTERRUPTIBLE：此进程状态类似于&lt;code&gt;TASK_INTERRUPTIBLE&lt;/code&gt;，只是它不会处理信号。中断处于这种状态的进程是不合适的，因为它可能正在完成某些重要的任务。 当它所等待的事件发生时，进程将被显式的唤醒呼叫唤醒。&lt;/li&gt;&#xA;&lt;li&gt;TASK_TRACED：正被调试程序等其它进程监控时，进程将进入这种状态。&lt;/li&gt;&#xA;&lt;li&gt;TASK_STOPPED：进程被停止执行，当进程接收到SIGSTOP、SIGTTIN、SIGTSTP或者SIGTTOU信号之后就会进入该状态。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这几个状态之间，转换关系如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;taskstate&#34; src=&#34;https://www.codedump.info/media/imgs/20200502-sgfap-process/taskstate.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; taskstate &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上面的状态转换图中，休眠状态（SLEEPING）包括了&lt;code&gt;TASK_INTERRUPTIBLE&lt;/code&gt;和&lt;code&gt;TASK_UNINTERRUPTIBLE&lt;/code&gt;，并没有做区分；另外，按照前面的讲述，&lt;code&gt;TASK_RUNNING&lt;/code&gt;状态区分了就绪以及在运行状态，由于这两者都是&lt;code&gt;TASK_RUNNING&lt;/code&gt;状态，所以分到了同一组里，又因为需要细化这两者之间的状态，内部也有状态之间的迁移。&lt;/p&gt;&#xA;&lt;p&gt;根据上面的状态转换图，进程状态的转换有这几种情况：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;正在运行的进程，由于需要等待某些事件（比如网络IO、磁盘IO等），进入休眠状态。&lt;/li&gt;&#xA;&lt;li&gt;正在运行的进程，由于时间片用完或者被其他更高优先级的进程抢占等因素，虽然还是就绪状态，但是被剥夺了执行权，进入就绪队列等待下一次被唤醒执行。&lt;/li&gt;&#xA;&lt;li&gt;处于休眠状态的进程，由于等待的事件满足被唤醒，进入就绪队列中等待被调度运行。&lt;/li&gt;&#xA;&lt;li&gt;处于就绪队列中的进程，被调度器分配CPU时间调度执行。&lt;/li&gt;&#xA;&lt;li&gt;在运行的进程退出。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;除了上面几种状态以外，还有僵尸（zombie）状态（内核使用&lt;code&gt;EXIT_ZOMBIE&lt;/code&gt;宏表示），用于表示进程已经不再执行，等待被回收的状态。&lt;/p&gt;&#xA;&lt;p&gt;在使用&lt;code&gt;ps aux&lt;/code&gt;命令时，可以查询到系统中进程所处的状态，与上面描述的内核中进程状态一一对应：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;S：休眠状态（sleeping），对应&lt;code&gt;TASK_INTERRUPTIBLE&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;R：等待运行（runable）对应&lt;code&gt;TASK_RUNNING&lt;/code&gt;，进程处于运行或就绪状态。&lt;/li&gt;&#xA;&lt;li&gt;I：空闲状态（idle）。&lt;/li&gt;&#xA;&lt;li&gt;Z：僵尸状态（zombie），对应&lt;code&gt;EXIT_ZOMBIE&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;T：暂停或跟踪状态（Traced），对应&lt;code&gt;TASK_TRACED&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;D: 不可中断的睡眠状态，对应&lt;code&gt;TASK_UNINTERRUPTIBLE&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在这里，需要再次强调的是，进程处于&lt;code&gt;Runnable&lt;/code&gt;状态时，并不代表就在执行，而是处于就绪可执行状态，由调度器最终决定进程执行。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;进程的创建&#34;&gt;&#xA;  进程的创建&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%bf%9b%e7%a8%8b%e7%9a%84%e5%88%9b%e5%bb%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;Unix系统将进程的执行放在两个不同的函数中执行：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;fork：fork函数拷贝父进程来创建一个子进程，fork函数调用后会分别在父子进程中各返回一次，区别在于：父进程中的返回值是所创建的子进程的进程pid，而子进程则是返回0表示创建成功。&lt;/li&gt;&#xA;&lt;li&gt;exec函数组：在fork调用返回后，子进程就创建完成了，如果需要运行一个与父进程不同的可执行文件，就通过&lt;code&gt;exec&lt;/code&gt;函数组来完成这个工作。如果不调用&lt;code&gt;exec&lt;/code&gt;，那么也就意味着父子进程运行的是同一份可执行文件代码。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;其他操作系统，有一些把以上两步合在一个函数中完成，即在同一个函数中既完成子进程的创建，也完成子进程的执行，Unix系统将以上两步分开成两个步骤，为shell执行程序提供了方便，因为shell可以在&lt;code&gt;fork&lt;/code&gt;创建进程之后，调用&lt;code&gt;exec&lt;/code&gt;来执行程序之前改变子进程的一些行为。比如让shell方便的实现类似重定向（redirect）的功能：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;wc test.txt &amp;gt; stat&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在上面的脚本中，希望将&lt;code&gt;wc&lt;/code&gt;命令的输出结果重定向到文件&lt;code&gt;stat&lt;/code&gt;中。shell在&lt;code&gt;fork&lt;/code&gt;创建了子进程之后，在&lt;code&gt;exec&lt;/code&gt;执行之前，关闭该子进程的标准输出，然后打开文件&lt;code&gt;stat&lt;/code&gt;，这样打开的文件就获得了刚刚关闭的标准输出的fd，执行&lt;code&gt;wc&lt;/code&gt;命令的子进程结果就写入到了文件&lt;code&gt;stat&lt;/code&gt;中。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;写时复制机制&#34;&gt;&#xA;  写时复制机制&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%86%99%e6%97%b6%e5%a4%8d%e5%88%b6%e6%9c%ba%e5%88%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;前面提到过，fork函数为子进程创建一个父进程地址空间的副本，复制属于父进程的页面。然而，考虑到许多子进程在创建之后立即调用系统调用exec函数组来执行另外的程序代码，父进程地址空间的复制可能没有必要。&lt;/p&gt;</description>
    </item>
    <item>
      <title>《面向应用开发者的系统指南》导论</title>
      <link>https://www.codedump.info/zh/post/20200501-system-guide-for-application-programmer/</link>
      <pubDate>Fri, 01 May 2020 21:43:43 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200501-system-guide-for-application-programmer/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;导论&#34;&gt;&#xA;  导论&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%af%bc%e8%ae%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;我想整理这份文档很久了。&lt;/p&gt;&#xA;&lt;p&gt;做为一个主要工作在应用层、用户态的开发者，我看了不少的所谓“内核资料”。我身边不少与我相似工作背景的人，也在不停的看“内核”、写各种内核代码分析。另一方面，在遇到很多系统方面的知识时，并没有太多能够系统解答这部分内容的地方，比如如下几个问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;free&lt;/code&gt;命令中的&lt;code&gt;buffer&lt;/code&gt;和&lt;code&gt;cache&lt;/code&gt;分别是什么？&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;uptime&lt;/code&gt;命令中显示的数据来源是什么？&lt;/li&gt;&#xA;&lt;li&gt;用户态进程的CPU时间，都由哪些部分组成？&lt;/li&gt;&#xA;&lt;li&gt;&amp;hellip;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这就是这两个维度层面的断档：内核资料大部分是写给在内核态工作的人看的，并没有从用户态的角度去解释一些系统相关的概念，导致了用户态开发者看各种内核文档时云里雾里，最后并没有给自己理解和解决系统问题带来太多的帮助。&lt;/p&gt;&#xA;&lt;p&gt;所以，我想从应用开发者的角度，抽出系统中最重要的那些概念，结合一些不那么复杂的内核代码解读（毕竟复杂的我也不会）、相关命令指标的数据来源（比如前面的&lt;code&gt;free&lt;/code&gt;命令）、&lt;code&gt;systemtap&lt;/code&gt;脚本等等手段，帮助工作在Linux上的应用开发者来更好的理解系统。&lt;/p&gt;&#xA;&lt;p&gt;如果打一个可能不是很恰当的比方，内核文档在我看来就是写给数学系的《数学分析》，但是工科学生并不需要了解的过于深入，他们需要的是一本面向工科生、实际解决他们遇到的问题的《高等数学》即可。&lt;/p&gt;&#xA;&lt;p&gt;这份文档就想能够写一本给应用开发者的“《高等数学》”，最后完成时，不一定能尽善尽美，但是希望能打开一扇窗户：应用开发者不应该在过多的内核细节中深入，而是应该从自己遇到的问题出发，带着问题抽丝剥茧在内核中去掉不重要的细节，寻找自己问题的答案。&lt;/p&gt;&#xA;&lt;p&gt;这份文档将逐渐更新（希望最后不会太监），将主要分为CPU篇、内存篇、IO篇，本来还应该有个网络篇，不过我还是先完成前面这个小目标再继续吧。&lt;/p&gt;&#xA;&lt;p&gt;本文档基于Ubuntu16、内核版本4.15进行讲解，但是其中大部分的概念、内容应该都不会随着版本有太大的变动。&lt;/p&gt;&#xA;&lt;p&gt;今后的文档，将更新到下面这个目录中。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;目录&#34;&gt;&#xA;  目录&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%9b%ae%e5%bd%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;基础篇&#34;&gt;&#xA;  基础篇&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e7%a1%80%e7%af%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;因为后续会使用&lt;code&gt;systemtap&lt;/code&gt;脚本来理解内核的一些行为，所以&lt;code&gt;systemtap&lt;/code&gt;的相关的原理和使用放在基础篇中&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200128-systemtap-by-example/&#34;&gt;通过实例快速入门Systemtap&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200218-linux-traceevent/&#34;&gt;Systemtap中内核trace事件的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;cpu篇&#34;&gt;&#xA;  CPU篇&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#cpu%e7%af%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200502-sgfap-process/&#34;&gt;进程&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200503-sgfap-process-schedule/&#34;&gt;进程调度&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200503-sgfap-process-systemtap/&#34;&gt;使用systemtap分析进程的行为&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200516-sgfap-syscall/&#34;&gt;系统调用&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200522-sgfap-softirq/&#34;&gt;软中断&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;内存篇&#34;&gt;&#xA;  内存篇&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%86%85%e5%ad%98%e7%af%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;io篇&#34;&gt;&#xA;  IO篇&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#io%e7%af%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;</description>
    </item>
    <item>
      <title>Systemtap中内核trace事件的实现</title>
      <link>https://www.codedump.info/zh/post/20200218-linux-traceevent/</link>
      <pubDate>Tue, 18 Feb 2020 17:37:01 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200218-linux-traceevent/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;内核中定义了一系列的trace point，这些trace point在特定的内核函数中被触发调用时被记录，而对应到systemtap中就是&lt;code&gt;kernel.trace&lt;/code&gt;类型的probe事件，可以使用命令来查看系统所有的trace point：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ sudo stap -L &amp;#39;kernel.trace(&amp;#34;*&amp;#34;)&amp;#39; | more&#xA;kernel.trace(&amp;#34;9p:9p_client_req&amp;#34;) $clnt:struct p9_client* $type:int8_t $tag:int&#xA;kernel.trace(&amp;#34;9p:9p_client_res&amp;#34;) $clnt:struct p9_client* $type:int8_t $tag:int $err:int&#xA;kernel.trace(&amp;#34;9p:9p_protocol_dump&amp;#34;) $clnt:struct p9_client* $pdu:struct p9_fcall*&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;换言之，通过systemtap能够对这些已经静态注册的内核调用记录点进行监控、跟踪。&lt;/p&gt;&#xA;&lt;p&gt;以下来解释trace point在内核的实现以及与systemtap相关的内容。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;数据结构&#34;&gt;&#xA;  数据结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;内核通过&lt;code&gt;DECLARE_TRACE&lt;/code&gt;来声明一个trace point：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;DECLARE_TRACE(subsys_eventname,&#xA;&#x9;TP_PROTO(int firstarg, struct task_struct *p),&#xA;&#x9;TP_ARGS(firstarg, p));&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在这里：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;subsys_eventname是定义trace事件的唯一字符串，又能拆解成两部分：subsys就是子系统的名称，而eventname是事件名称。比如下面将作为实例的&lt;code&gt;softirq_entry&lt;/code&gt;，就定义了一个在&lt;code&gt;softirq&lt;/code&gt;子系统中的&lt;code&gt;entry&lt;/code&gt;事件。&lt;/li&gt;&#xA;&lt;li&gt;TP_PROTO(int firstarg, struct task_struct *p)：定义了传入trace函数的参数原型。&lt;/li&gt;&#xA;&lt;li&gt;TP_ARGS(firstarg, p)：定义了参数名称，其类型与TP_PROTO中的类型一一对应。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这个宏的定义如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// include/linux/tracepoint.h&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;#define DECLARE_TRACE(name, proto, args)&#x9;&#x9;&#x9;&#x9;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;&#x9;__DECLARE_TRACE(name, PARAMS(proto), PARAMS(args),&#x9;&#x9;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;&#x9;&#x9;&#x9;cpu_online(raw_smp_processor_id()),&#x9;&#x9;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;&#x9;&#x9;&#x9;PARAMS(void *__data, proto),&#x9;&#x9;&#x9;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;&#x9;&#x9;&#x9;PARAMS(__data, args))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其中的宏&lt;code&gt;__DECLARE_TRACE&lt;/code&gt;定义如下：&lt;/p&gt;</description>
    </item>
    <item>
      <title>通过实例快速入门Systemtap</title>
      <link>https://www.codedump.info/zh/post/20200128-systemtap-by-example/</link>
      <pubDate>Tue, 28 Jan 2020 11:56:56 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200128-systemtap-by-example/</guid>
      <description>&lt;p&gt;我这段时间好好学习了一下Systemtap相关的使用，这篇文章算是学习过程中总结的一些笔记，我另外在github上创建了一个&lt;a href=&#34;https://github.com/lichuang/awesome-systemtap-cn&#34;&gt;awesome-systemtap-cn&lt;/a&gt;项目，收集systemtap相关的优秀学习资源，欢迎提供其他更好的参考资料。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;systemtap是一款“动态跟踪（dynamic tracing）”工具，为什么需要这类工具？打一个比方，这类工具就好比医生的听诊器，病人就好比是在运行的系统，很多时候查看一些问题需要在系统在运行的时候来观察，这时候就需要这类动态跟踪工具。与之对应的是，类似gdb这样的调试工具，其工作原理是让进程在某些断点暂停下来，查看进程的行为，这种技术称为“静态调试”。&lt;/p&gt;&#xA;&lt;p&gt;关于动态跟踪技术，推荐阅读&lt;a href=&#34;https://openresty.org/posts/dynamic-tracing/&#34;&gt;《动态追踪技术漫谈》&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;Smileytap.svg&#34; src=&#34;https://www.codedump.info/media/imgs/20200128-systemtap-by-example/Smileytap.svg_.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; systemtap &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;本文旨在通过实例，快速解释systemtap脚本语言的最常见用法和语法。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;工作原理&#34;&gt;&#xA;  工作原理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%b7%a5%e4%bd%9c%e5%8e%9f%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;如下图，systemtap使用.stp脚本语言，由命令行&lt;code&gt;stap&lt;/code&gt;编译生成对应的内核模块，动态放入内核中执行：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;systemtap&#34; src=&#34;https://www.codedump.info/media/imgs/20200128-systemtap-by-example/systemtap.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; systemtap &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;stap 流程从将脚本转换成解析树开始 (pass 1)。&lt;/li&gt;&#xA;&lt;li&gt;然后使用细化（elaboration）步骤 (pass 2) 中关于当前运行的内核的符号信息解析符号。&lt;/li&gt;&#xA;&lt;li&gt;接下来，转换流程将解析树转换成 C 源代码 (pass 3) 并使用解析后的信息和 tapset 脚本（SystemTap 定义的库，包含有用的功能）。&lt;/li&gt;&#xA;&lt;li&gt;stap 的最后步骤是构造使用本地内核模块构建进程的内核模块 (pass 4)。&lt;/li&gt;&#xA;&lt;li&gt;有了可用的内核模块之后，stap 完成了自己的任务，并将控制权交给其他两个实用程序 SystemTap：staprun 和 stapio。这两个实用程序协调工作，负责将模块安装到内核中并将输出发送到 stdout (pass 5)。如果在 shell 中按组合键 Ctrl-C 或脚本退出，将执行清除进程，这将导致卸载模块并退出所有相关的实用程序。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;stap命令行参数&#34;&gt;&#xA;  stap命令行参数&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#stap%e5%91%bd%e4%bb%a4%e8%a1%8c%e5%8f%82%e6%95%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;-x-pid&#34;&gt;&#xA;  -x PID&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#-x-pid&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;-x用于传递PID参数给systemtap脚本，这样在脚本内部可以通过target()函数拿到这个传递进来的参数：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;// $ sudo stap x-param.stp -x 10&#xA;// 输出：pid:10&#xA;probe begin&#xA;{&#xA;  printf(&amp;#34;pid:%d\n&amp;#34;, target())&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;h2 class=&#34;heading&#34; id=&#34;-t-seconds&#34;&gt;&#xA;  -T seconds&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#-t-seconds&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;-T 参数后面可以带上秒数，这样脚本在这个时间之后自动退出，这样可以设置脚本执行的时间。&lt;/p&gt;</description>
    </item>
    <item>
      <title>C&#43;&#43;11中的内存模型下篇 - C&#43;&#43;11支持的几种内存模型</title>
      <link>https://www.codedump.info/zh/post/20191214-cxx11-memory-model-2/</link>
      <pubDate>Sat, 14 Dec 2019 22:41:22 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20191214-cxx11-memory-model-2/</guid>
      <description>&lt;p&gt;在本系列的上篇，介绍了内存模型的基本概念，接下来看C++11中支持的几种内存模型。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;几种关系术语&#34;&gt;&#xA;  几种关系术语&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%87%a0%e7%a7%8d%e5%85%b3%e7%b3%bb%e6%9c%af%e8%af%ad&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在接着继续解释之前，先了解一下几种关系术语。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;sequenced-before&#34;&gt;&#xA;  sequenced-before&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#sequenced-before&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;sequenced-before用于表示&lt;strong&gt;单线程&lt;/strong&gt;之间，两个操作上的先后顺序，这个顺序是非对称、可以进行传递的关系。&lt;/p&gt;&#xA;&lt;p&gt;它不仅仅表示两个操作之间的先后顺序，还表示了操作结果之间的可见性关系。两个操作A和操作B，如果有A sequenced-before B，除了表示操作A的顺序在B之前，还表示了操作A的结果操作B可见。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;happens-before&#34;&gt;&#xA;  happens-before&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#happens-before&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;与sequenced-before不同的是，happens-before关系表示的&lt;strong&gt;不同线程&lt;/strong&gt;之间的操作先后顺序，同样的也是非对称、可传递的关系。&lt;/p&gt;&#xA;&lt;p&gt;如果A happens-before B，则A的内存状态将在B操作执行之前就可见。在上一篇文章中，某些情况下一个写操作只是简单的写入内存就返回了，其他核心上的操作不一定能马上见到操作的结果，这样的关系是不满足happens-before的。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;synchronizes-with&#34;&gt;&#xA;  synchronizes-with&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#synchronizes-with&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;synchronizes-with关系强调的是变量被修改之后的传播关系（propagate），即如果一个线程修改某变量的之后的结果能被其它线程可见，那么就是满足synchronizes-with关系的。&lt;/p&gt;&#xA;&lt;p&gt;显然，满足synchronizes-with关系的操作一定满足happens-before关系了。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;c11中支持的内存模型&#34;&gt;&#xA;  C++11中支持的内存模型&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#c11%e4%b8%ad%e6%94%af%e6%8c%81%e7%9a%84%e5%86%85%e5%ad%98%e6%a8%a1%e5%9e%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;从C++11开始，就支持以下几种内存模型：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;enum memory_order {&#xA;    memory_order_relaxed,&#xA;    memory_order_consume,&#xA;    memory_order_acquire,&#xA;    memory_order_release,&#xA;    memory_order_acq_rel,&#xA;    memory_order_seq_cst&#xA;};&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;与内存模型相关的枚举类型有以上六种，但是其实分为四类，如下图所示，其中对一致性的要求逐渐减弱，以下来分别讲解。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;c&amp;#43;&amp;#43;model&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-2/c&amp;#43;&amp;#43;model.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; c++model &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;memory_order_seq_cst&#34;&gt;&#xA;  memory_order_seq_cst&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#memory_order_seq_cst&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;这是默认的内存模型，即上篇文章中分析过的顺序一致性内存模型，由于在上篇中的相关概念已经做过详细的介绍，这里就不再阐述了。仅列出引用自《C++  Concurrency In Action》的示例代码。&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;#include &amp;lt;atomic&amp;gt;&#xA;#include &amp;lt;thread&amp;gt;&#xA;#include &amp;lt;assert.h&amp;gt;&#xA;&#xA;std::atomic&amp;lt;bool&amp;gt; x,y;&#xA;std::atomic&amp;lt;int&amp;gt; z;&#xA;&#xA;void write_x()&#xA;{&#xA;    x.store(true,std::memory_order_seq_cst);&#xA;}&#xA;&#xA;void write_y()&#xA;{&#xA;    y.store(true,std::memory_order_seq_cst);&#xA;}&#xA;&#xA;void read_x_then_y()&#xA;{&#xA;    while(!x.load(std::memory_order_seq_cst));&#xA;    if(y.load(std::memory_order_seq_cst))&#xA;        ++z;&#xA;}&#xA;&#xA;void read_y_then_x()&#xA;{&#xA;    while(!y.load(std::memory_order_seq_cst));&#xA;    if(x.load(std::memory_order_seq_cst))&#xA;        ++z;&#xA;}&#xA;&#xA;int main()&#xA;{&#xA;    x=false;&#xA;    y=false;&#xA;    z=0;&#xA;    std::thread a(write_x);&#xA;    std::thread b(write_y);&#xA;    std::thread c(read_x_then_y);&#xA;    std::thread d(read_y_then_x);&#xA;    a.join();&#xA;    b.join();&#xA;    c.join();&#xA;    d.join();&#xA;    assert(z.load()!=0);&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;由于采用了顺序一致性模型，因此最后的断言不可能发生，即在程序结束时不可能出现z为0的情况。&lt;/p&gt;</description>
    </item>
    <item>
      <title>C&#43;&#43;11中的内存模型上篇 - 内存模型基础</title>
      <link>https://www.codedump.info/zh/post/20191214-cxx11-memory-model-1/</link>
      <pubDate>Sat, 14 Dec 2019 10:10:15 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20191214-cxx11-memory-model-1/</guid>
      <description>&lt;p&gt;前段时间花了些精力研究C++11引入的内存模型相关的操作，于是把相关的知识都学习了一下，将这个学习过程整理为两篇文档，这是第一篇，主要分析内存模型的一些基础概念，第二篇展开讨论C++11相关的操作。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;cpu架构的演进&#34;&gt;&#xA;  CPU架构的演进&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#cpu%e6%9e%b6%e6%9e%84%e7%9a%84%e6%bc%94%e8%bf%9b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;早期的CPU，CPU之间能共享访问的只有内存，此时的结构大体如图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;memory&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-1/memory.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; memory &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;随着硬件技术的发展，内存的访问已经跟不上CPU的执行速度，此时内存反而变成了瓶颈。为了加速读写速度，每个CPU也都有自己内部才能访问的缓存，结构变成了这样：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;multicore&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-1/multicore.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; multicore &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;有多个CPU处理器，每个CPU处理器内部又有多个核心。&lt;/li&gt;&#xA;&lt;li&gt;存在只能被一个CPU核心访问的L1 cache。&lt;/li&gt;&#xA;&lt;li&gt;存在只能被一个CPU处理器的多个核心访问的L2 cache。&lt;/li&gt;&#xA;&lt;li&gt;存在能被所有CPU处理器都能访问到的L3 cache以及内存。&lt;/li&gt;&#xA;&lt;li&gt;L1 cache、L2 cache、L3 cache的容量空间依次变大，但是访问速度依次变慢。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;当CPU结构发生变化，增加了只能由内部才能访问的缓存之后，一些在旧架构上不会出现的问题，在新的架构上就会出现。而本篇的主角内存模型（memory model），其作用就是规定了各种不同的访问共享内存的方式，不同的内存模型，既需要编译器的支持，也需要硬件CPU的支持。&lt;/p&gt;&#xA;&lt;p&gt;我们从一个最简单的多线程访问变量问题谈起。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;简单的多线程访问数据问题&#34;&gt;&#xA;  简单的多线程访问数据问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ae%80%e5%8d%95%e7%9a%84%e5%a4%9a%e7%ba%bf%e7%a8%8b%e8%ae%bf%e9%97%ae%e6%95%b0%e6%8d%ae%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;假设在程序执行之前，A=B=0，有两个线程同时分别执行如下的代码：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;线程1&lt;/th&gt;&#xA;          &lt;th&gt;线程2&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;1. A=1&lt;/td&gt;&#xA;          &lt;td&gt;3. B=2&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2. print(B)&lt;/td&gt;&#xA;          &lt;td&gt;4. print(A)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;问上述程序的执行结果如何？&lt;/p&gt;&#xA;&lt;p&gt;这个问题是一个简单的排列组合问题，其结果有：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;2（先选择A或B输出）* 2（输出修改前还是之后的结果）* 1（前面第一步选择了一个变量之后，现在只能选剩下的变量）* 2（输出修改前还是之后的结果） = 8&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;其可能的结果包括：(0,0)、(1,0)、(0,2)、(1,2)、(0,1)、(2,0)、(2,1)。（这里只有7个结果，是因为有两个(0,0)，所以少了一个）。&lt;/p&gt;&#xA;&lt;p&gt;由于多个线程交替执行，可能有以下几种结果，下面来分别解析。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;两个线程依次执行&#34;&gt;&#xA;  两个线程依次执行&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%a4%e4%b8%aa%e7%ba%bf%e7%a8%8b%e4%be%9d%e6%ac%a1%e6%89%a7%e8%a1%8c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;最简单的情况，就是这两个线程依次执行，即一个线程执行完毕之后再执行另一个线程的指令，这种情况下有两种可能：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;1-&amp;gt;2-&amp;gt;3-&amp;gt;4&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这种情况先执行完毕线程1，再执行线程2，最后输出的结果是(0,1)。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;sc1&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-1/sc1.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; sc1 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;3-&amp;gt;4-&amp;gt;1-&amp;gt;2&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这种情况先执行完毕线程2，再执行线程1，最后输出的结果是(0,2)。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;sc2&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-1/sc2.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; sc2 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;两个线程交替执行&#34;&gt;&#xA;  两个线程交替执行&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%a4%e4%b8%aa%e7%ba%bf%e7%a8%8b%e4%ba%a4%e6%9b%bf%e6%89%a7%e8%a1%8c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;这样情况下，先执行的可能是线程1或者线程2，来看线程1先执行的情况。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;1-&amp;gt;3-&amp;gt;2-&amp;gt;4&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这种情况下的输出是（2,1）。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;sc3&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-1/sc3.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; sc3 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;1-&amp;gt;3-&amp;gt;4-&amp;gt;2&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这种情况下的输出是（1,2）。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;sc4&#34; src=&#34;https://www.codedump.info/media/imgs/20191214-cxx11-memory-model-1/sc4.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; sc4 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;以上是第一条指令先执行线程1执行的情况，同样地也有先执行线程2指令的情况（3-1-&amp;gt;4-&amp;gt;2和3-&amp;gt;1-&amp;gt;2-4），这里不再列出，有兴趣的读者可以自行画图理解。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;不可能出现的情况&#34;&gt;&#xA;  不可能出现的情况&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%8d%e5%8f%af%e8%83%bd%e5%87%ba%e7%8e%b0%e7%9a%84%e6%83%85%e5%86%b5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;除了以上的情况之外，还有一种可能是输出(0,0)，但是这种输出在一般情况下不可能出现（我们接下来会解释什么情况下可能出现），下面来做解释。&lt;/p&gt;</description>
    </item>
    <item>
      <title>对比脚本型和编译型游戏服务器的热更新方案</title>
      <link>https://www.codedump.info/zh/post/20191206-gameserver-hot-refresh/</link>
      <pubDate>Fri, 06 Dec 2019 22:40:49 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20191206-gameserver-hot-refresh/</guid>
      <description>&lt;p&gt;本文对比游戏服务器中C++搭配脚本语言（Lua、Python）以及纯编译型语言（C++、Golang）来进行开发时，进行线上服务器热更新的方案。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;游戏开发模式&#34;&gt;&#xA;  游戏开发模式&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%b8%b8%e6%88%8f%e5%bc%80%e5%8f%91%e6%a8%a1%e5%bc%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在开始下文之前，有必要简单描述一下游戏服务与web服务的区别。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;长连接-vs-短连接&#34;&gt;&#xA;  长连接 VS 短连接&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%95%bf%e8%bf%9e%e6%8e%a5-vs-%e7%9f%ad%e8%bf%9e%e6%8e%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;游戏服务对外与客户端之间的链接多是长连接形式，而web服务多是短连接。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;有状态服务-vs-无状态服务&#34;&gt;&#xA;  有状态服务 VS 无状态服务&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%89%e7%8a%b6%e6%80%81%e6%9c%8d%e5%8a%a1-vs-%e6%97%a0%e7%8a%b6%e6%80%81%e6%9c%8d%e5%8a%a1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;游戏服务内，需要维持着玩家的状态数据，如玩家属性、位置等，web请求多是无状态服务。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;启动时间&#34;&gt;&#xA;  启动时间&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%90%af%e5%8a%a8%e6%97%b6%e9%97%b4&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;由于前面提到的游戏服务是有状态服务，因此游戏服务器启动的时候，需要从持久化存储中将数据加载到内存中，这意味着游戏服务器的启动时间会很长，一般一次需要几分钟，web服务器相对轻量很多，因为需要访问的持久化数据在另外的存储服务器上。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;开发周期&#34;&gt;&#xA;  开发周期&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%bc%80%e5%8f%91%e5%91%a8%e6%9c%9f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;游戏服务的开发周期短，有一些游戏一周就需要进行一次维护，这意味着在这一周内策划（对应互联网中的产品经理）提出的需求都要完成上线。&lt;/p&gt;&#xA;&lt;p&gt;从以上对比可以看到，游戏业务的特点是更新频繁，而启动一个服务器的时间又比较长。在进行开发的过程中，如果使用纯编译型语言进行开发，那么流程就是如下所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;cpp-dev&#34; src=&#34;https://www.codedump.info/media/imgs/20191206-gameserver-hot-refresh/cpp-dev.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; cpp-dev &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;可以看到，上面是一个比较长的开发功能流程，而如果还考虑到开发周期短这个特点，显然是不能匹配游戏开发这种业务的特征的，此时就需要“热更新”功能才能提高开发效率。&lt;/p&gt;&#xA;&lt;p&gt;以下就脚本语言与编译型语言如何实现“热更新”展开讨论。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;c搭配脚本语言&#34;&gt;&#xA;  C++搭配脚本语言&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#c%e6%90%ad%e9%85%8d%e8%84%9a%e6%9c%ac%e8%af%ad%e8%a8%80&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;这种方案是笔者见过的方案，其一般的做法是：C++来实现底层的框架（网络、与数据库通信等），接收到数据包之后，将数据传递给脚本层，由脚本来处理具体的业务逻辑。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;script-level&#34; src=&#34;https://www.codedump.info/media/imgs/20191206-gameserver-hot-refresh/script-level.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; script-level &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;这种也是软件设计中常见的分层方案：底层的模块为上层的模块服务，同时底层模块也变动的较少。&lt;/p&gt;&#xA;&lt;p&gt;由于嵌入到进程里面的脚本语言引擎，本质上是将脚本语言代码翻译成内存中的Opcode来执行，因此这类型游戏服务器实现“热更新”方案很简单：将新的脚本同步到服务器上，然后给服务器发出一个信号，重新读取脚本代码到内存中即可。&lt;/p&gt;&#xA;&lt;p&gt;有了这个架构之后，原先的开发模式就变成了下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;script-dev&#34; src=&#34;https://www.codedump.info/media/imgs/20191206-gameserver-hot-refresh/script-dev.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; script-dev &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;可以看到，前面编译型语言中编译和重启服务器这两部最消耗时间的步骤，变成了热更新脚本，这样就不需要重启服务器来验证功能，开发效率提高了很多。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;编译型语言实现热更新&#34;&gt;&#xA;  编译型语言实现热更新&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%bc%96%e8%af%91%e5%9e%8b%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0%e7%83%ad%e6%9b%b4%e6%96%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;从上面的分析可以看到，因为编译型语言存在需要重启服务器的步骤，导致了以下两个问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;客户端连接需要断开，因为游戏服务是长连接。&lt;/li&gt;&#xA;&lt;li&gt;重启服务器时需要耗费大量的时间将持久化存储的数据加载到内存中，这样启停过程中的客户端请求就会丢失。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;下面依次看看如何解决这两个问题。&lt;/p&gt;&#xA;&lt;p&gt;维护客户端连接，可以再引入一个网关组件，由网关来维护连接，这样服务器重启流程中客户端对内部游戏服务器的启停并无感知。&lt;/p&gt;&#xA;&lt;p&gt;为了在启动新版本服务器的过程中继续服务客户端请求，并且新版本服务器上线之后能接着当前的玩家属性继续操作，可以考虑将数据存入共享内存中，这样即便进程退出共享内存还存在。这样做的思路是“代码与数据分离”。&lt;/p&gt;&#xA;&lt;p&gt;如果要实现这个方案，又要做到以下两点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;设计一套面向共享内存的数据结构，至少应该能支持常见的链表、数组、字典等类型。&lt;/li&gt;&#xA;&lt;li&gt;数据结构的设计需要考虑可扩展性以及前后兼容性，因为可能出现两个前后版本中，有一些字段不存在或者有一些字段新增的情况。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;有了以上的介绍，下图中就是为了支持热更新的编译型语言的架构方案：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;cpp-hotrefresh&#34; src=&#34;https://www.codedump.info/media/imgs/20191206-gameserver-hot-refresh/cpp-hotrefresh.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; cpp-hotrefresh &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;网关负责维护与客户端的连接，同时也知道当前访问的是哪个游戏服务器。当新版本服务器启动完毕之后，向网关发送一个指令，让网关在收到这个指令之后的所有客户端请求，都转发到新的游戏服务器上，这样就完成了一个看似没有重启的“热更新”。&lt;/li&gt;&#xA;&lt;li&gt;数据保存在共享内存中，这样即使在启动新版本服务器的时候也能继续服务客户端的请求。另外需要注意的是，启动的时候服务器需要判断一下是否已经有一个进程存在，如果存在进程且有共享内存数据的情况下，不需要再从持久化存储中加载数据到内存中。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;方案对比&#34;&gt;&#xA;  方案对比&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%96%b9%e6%a1%88%e5%af%b9%e6%af%94&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;以下来对比一下两种技术方案的优缺点。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;特性&lt;/th&gt;&#xA;          &lt;th&gt;脚本型游戏服务器&lt;/th&gt;&#xA;          &lt;th&gt;编译型游戏服务器&lt;/th&gt;&#xA;          &lt;th&gt;备注&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;开发效率&lt;/td&gt;&#xA;          &lt;td&gt;高&lt;/td&gt;&#xA;          &lt;td&gt;低&lt;/td&gt;&#xA;          &lt;td&gt;脚本语言没有编译步骤&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;性能&lt;/td&gt;&#xA;          &lt;td&gt;低&lt;/td&gt;&#xA;          &lt;td&gt;高&lt;/td&gt;&#xA;          &lt;td&gt;脚本语言执行性能不如编译型语言&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;架构难度&lt;/td&gt;&#xA;          &lt;td&gt;低&lt;/td&gt;&#xA;          &lt;td&gt;高&lt;/td&gt;&#xA;          &lt;td&gt;编译型语言为了实现热更新，需要解决：网关维护连接，代码数据分离，数据存入共享内存等，而脚本型语言只需要实现热更新脚本即可&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;重构难度&lt;/td&gt;&#xA;          &lt;td&gt;高&lt;/td&gt;&#xA;          &lt;td&gt;低&lt;/td&gt;&#xA;          &lt;td&gt;“脚本语言一时爽，代码重构火葬场”&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;</description>
    </item>
    <item>
      <title>IM服务器设计-如何解决消息的乱序</title>
      <link>https://www.codedump.info/zh/post/20191013-im-msg-out-of-order/</link>
      <pubDate>Sun, 13 Oct 2019 10:59:16 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20191013-im-msg-out-of-order/</guid>
      <description>&lt;p&gt;IM消息需要面对的另一个难题：如何保证收到的消息不乱序。下面先展开看看要解决这个难题有哪些障碍。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;消息乱序的原因&#34;&gt;&#xA;  消息乱序的原因&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%b6%88%e6%81%af%e4%b9%b1%e5%ba%8f%e7%9a%84%e5%8e%9f%e5%9b%a0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;时间难以保证&#34;&gt;&#xA;  时间难以保证&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%97%b6%e9%97%b4%e9%9a%be%e4%bb%a5%e4%bf%9d%e8%af%81&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;既然谈到“顺序”，就必然有一个衡量的标准，然而无论是使用客户端时间还是服务器时间都难以作为这个标准来衡量消息的先后顺序。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;msg&#34; src=&#34;https://www.codedump.info/media/imgs/20191013-im-msg-out-of-order/msg.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; msg &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图中，一个IM系统在多个客户端，在不同的接入网关进行接入，进而又在不同的逻辑处理服务器上进行处理，不论是客户端本身，还是服务器（网络、逻辑服务器），各自机器上的时间都不相同，因此无法以机器本地的时间来作为衡量消息顺序的标准。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;网络顺序无法保证&#34;&gt;&#xA;  网络顺序无法保证&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%bd%91%e7%bb%9c%e9%a1%ba%e5%ba%8f%e6%97%a0%e6%b3%95%e4%bf%9d%e8%af%81&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;考虑到只有一个客户端连接上一个网关的场景，即使在这样的场景中，消息的先后顺序也因为网络的因素难以得到保证。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;msg-network&#34; src=&#34;https://www.codedump.info/media/imgs/20191013-im-msg-out-of-order/msg-network.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; msg-network &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图中，网关试图向客户端依次发送消息1、2这两条消息，可能出现下面的问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;网关向客户端发送消息1，此时客户端的网络状况不好，导致该消息可能会丢失或者重传。&lt;/li&gt;&#xA;&lt;li&gt;网关没有等待消息1的发送结果，继续发送了消息2，而此时客户端的网络状况变好，这条消息比消息1更快的被客户端收到。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以上的场景，可能会有人想到一种处理模式：网关只有在客户端应答收到了消息1之后再继续发送消息2，这样就不会出现网络原因导致的消息乱序问题了。然而这样的话，消息相当于串行发送了，效率并不高。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;多线程因素导致的乱序&#34;&gt;&#xA;  多线程因素导致的乱序&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%9a%e7%ba%bf%e7%a8%8b%e5%9b%a0%e7%b4%a0%e5%af%bc%e8%87%b4%e7%9a%84%e4%b9%b1%e5%ba%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;客户端、服务器都可能存在多个发送、接收线程，这也是导致消息乱序的原因之一。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;解决策略&#34;&gt;&#xA;  解决策略&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%a7%a3%e5%86%b3%e7%ad%96%e7%95%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面分析了消息乱序的几个成因，下面就逐个分析都应该怎么解决。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;消息序列号&#34;&gt;&#xA;  消息序列号&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%b6%88%e6%81%af%e5%ba%8f%e5%88%97%e5%8f%b7&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;前面提过的第一个问题：消息的时序标准问题，无法以客户端或者服务器本地的时间来作为衡量的标准，此时可以引入一个产生递增ID的组件，由这一组件来统一生成递增、不回退的消息序列号用于衡量消息的先后顺序。&lt;/p&gt;&#xA;&lt;p&gt;然而这里还有可以细化讨论的部分：这个组件生成的ID，是否需要全局唯一？即不论单聊、群聊都需要保证生成出来的序列号唯一。&lt;/p&gt;&#xA;&lt;p&gt;这个全局唯一性不是必要的，原因在于不同的聊天，能保证消息在自己的频道唯一、递增即可。有了这个前提，这个组件生成ID的流程大体如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;处理该聊天的逻辑服务器ID。&lt;/li&gt;&#xA;&lt;li&gt;每个聊天频道（单聊、群聊）有自己一个独立的频道ID。&lt;/li&gt;&#xA;&lt;li&gt;每个频道内部，保证能够产生一个递增、不回退的序列号。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这样，消息序列号实际上由三部分部分组成：逻辑服务器ID-频道ID-频道内的消息序列号。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;群聊消息的处理&#34;&gt;&#xA;  群聊消息的处理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%be%a4%e8%81%8a%e6%b6%88%e6%81%af%e7%9a%84%e5%a4%84%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;有了前面的消息序列号，已经解决了第一个问题：消息的时序标准问题。然而这样还不足够，考虑到下图中的群聊场景：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;group-msg&#34; src=&#34;https://www.codedump.info/media/imgs/20191013-im-msg-out-of-order/group-msg.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; group-msg &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;两个客户端依次发出消息A和消息B。&lt;/li&gt;&#xA;&lt;li&gt;在两个不同的处理群聊消息的服务器中，由于种种原因，反倒是消息B比消息A先到。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从上面可以看出，群聊消息乱序的原因在于：同一个群聊的消息，最后被分派到了两个不同的逻辑服务器上处理。&lt;/p&gt;&#xA;&lt;p&gt;还是继续沿用上面生成消息序列号的思路：如果是同一个聊天频道的消息，就放在一起处理。因此可以变成下图中的处理方式：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;group-msg-2&#34; src=&#34;https://www.codedump.info/media/imgs/20191013-im-msg-out-of-order/group-msg-2.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; group-msg-2 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，根据群聊消息的群ID来选择逻辑服务器，这样同一个群的消息都能落在同一个服务器中来处理了。&lt;/p&gt;&#xA;&lt;p&gt;可以看到，这里并不需要使用一个“分布式唯一递增ID”这样的组件来产生ID，因为这里的问题简化成了：只需要该消息序列号在所在的逻辑服务器处理的聊天频道中唯一且递增就可以了。问题的重新分析和定义，让这个处理变得简单了很多。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;网络乱序的处理&#34;&gt;&#xA;  网络乱序的处理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%bd%91%e7%bb%9c%e4%b9%b1%e5%ba%8f%e7%9a%84%e5%a4%84%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;接着处理由于网络原因导致的乱序，TCP协议中也有类似处理网络乱序的手段，简单来说：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;TCP协议栈中有缓冲区缓存收到的数据。&lt;/li&gt;&#xA;&lt;li&gt;发送端使用序列号ACK来确认接收端收到的数据，比如1、2、3三个序列号的数据，如果先接收到1，此时发送端会收到ACK 1的消息，但是在这之后如果消息3先于消息2被接收端收到，此时发送端仍然会ACK消息1，表示消息3这条消息是乱序的。&lt;/li&gt;&#xA;&lt;li&gt;有了缓冲区和确认序列号，就知道哪些数据可以由协议栈提供给应用层。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;tcp-stack&#34; src=&#34;https://www.codedump.info/media/imgs/20191013-im-msg-out-of-order/tcp-stack.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; tcp-stack &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;接收方TCP协议栈中依次存有消息1和3，而消息2还未接收到。&lt;/li&gt;&#xA;&lt;li&gt;消息1被发送方确认，此时消息1可以提供给应用层。&lt;/li&gt;&#xA;&lt;li&gt;由于消息2没有接收到，因此消息3是乱序消息，不能提供给应用层。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从中得到的启发是：收发队列是可以有发送者来掌控的，发送者知道消息的顺序，虽然不能保证消息收发的前后顺序，但是由于引入了缓冲区，只有被确认的消息才可以被消费，这样可以通过发送者的ACK确认，来保证消息的顺序消费。&lt;/p&gt;&#xA;&lt;p&gt;以上的思路，可以沿用到网络乱序消息的处理中。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;最终方案&#34;&gt;&#xA;  最终方案&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%80%e7%bb%88%e6%96%b9%e6%a1%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;综合以上的分析，消息乱序问题可以使用下面的方式来解决。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;客户端消息缓存队列&#34;&gt;&#xA;  客户端消息缓存队列&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%a2%e6%88%b7%e7%ab%af%e6%b6%88%e6%81%af%e7%bc%93%e5%ad%98%e9%98%9f%e5%88%97&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;客户端内部，维持一个缓存消息的队列，每个消息都有对应的消息序列号，收到消息之后需要与网关进行确认，以此确认这条消息是否是按序接收的消息，只有这样的消息才能提供给应用层消费。&lt;/p&gt;</description>
    </item>
    <item>
      <title>IM服务器设计-网关接入层</title>
      <link>https://www.codedump.info/zh/post/20190818-im-msg-gate/</link>
      <pubDate>Sun, 18 Aug 2019 16:55:17 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190818-im-msg-gate/</guid>
      <description>&lt;p&gt;IM服务系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20190608-im-design-base/&#34;&gt;IM服务器设计-基础&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20190608-im-msg-storage/&#34;&gt;IM服务器设计-消息存储&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;网关接入层负责维护与客户端之间的长连接，由于它是唯一一个与客户端进行直接通信的服务入口，维护着大量的客户端连接，其设计原则应该满足：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;安全&lt;/li&gt;&#xA;&lt;li&gt;稳定&lt;/li&gt;&#xA;&lt;li&gt;快速&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;具体来说，需要考虑不少的问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;用什么数据结构保存与客户端的连接？&lt;/li&gt;&#xA;&lt;li&gt;如何清除死链？&lt;/li&gt;&#xA;&lt;li&gt;在网关宕机的情况下如何容错？&lt;/li&gt;&#xA;&lt;li&gt;服务如何降级？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下具体展开。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;基础设计&#34;&gt;&#xA;  基础设计&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e7%a1%80%e8%ae%be%e8%ae%a1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;简而言之，网关内部维护着一个map，其中保存着客户端相关的ID与对应连接的映射关系。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;map&#34; src=&#34;https://www.codedump.info/media/imgs/20190818-im-msg-gate/map.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; map &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;内部服务需要应答客户端时，经历如下步骤：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;到redis中查询路由信息，即客户端连接到了哪个网关，将消息发送给该网关。&lt;/li&gt;&#xA;&lt;li&gt;网关服务在上面的map中找到对应的客户端连接，将消息发送给客户端。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;死链的处理&#34;&gt;&#xA;  死链的处理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%ad%bb%e9%93%be%e7%9a%84%e5%a4%84%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;由于网关上维护着大量的客户端连接，需要通过收发心跳报的方式检查死链，具体做法是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;网关针对每个连上的连接，都创建一个定时器。&lt;/li&gt;&#xA;&lt;li&gt;网关跟客户端的每次交互之后，网关都对应的更新一下该客户端的心跳时间为当前时间。&lt;/li&gt;&#xA;&lt;li&gt;客户端内部同样也维护一个定时器，每次定时器超时时，判断当前是否已经有一段时间没有跟网关通信了，此时将发出心跳消息进行保活。&lt;/li&gt;&#xA;&lt;li&gt;当该每个定时器到期时，检查客户端的心跳时间距离当前时间已经超过一个阈值了，那么将认为该客户端已经失连，将清除掉该连接。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要注意的是，客户端的定时器应该小于网关层给每个连接加上的定时器。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;keepalive&#34; src=&#34;https://www.codedump.info/media/imgs/20190818-im-msg-gate/keepalive.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; keepalive &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;容错设计&#34;&gt;&#xA;  容错设计&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%b9%e9%94%99%e8%ae%be%e8%ae%a1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;网关有可能宕机，此时要考虑到这种情况下的容错处理。&lt;/p&gt;&#xA;&lt;p&gt;这里的原则有两条：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;客户端一旦发现前面连接的网关宕机，将尝试重连。&lt;/li&gt;&#xA;&lt;li&gt;内部服务要通过网关层应答给客户端的消息，一旦发现由于网关宕机而无法发出，将直接丢弃，由客户端重新尝试重连。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下来详细解释一下这两个原则。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;客户端重连&#34;&gt;&#xA;  客户端重连&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%a2%e6%88%b7%e7%ab%af%e9%87%8d%e8%bf%9e&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;客户端内部维护着一个发出消息的消息队列，仅在收到服务器的处理应答之后才可以从其中清除相应的消息。注意，这里每个客户端的消息ID需要做到严格递增。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;messagequeue&#34; src=&#34;https://www.codedump.info/media/imgs/20190818-im-msg-gate/messagequeue.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; messagequeue &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;比如，上图中发出但是未收到应答的消息有三条，消息ID依次递增，分别是100、101、102。此时如果收到服务器应答消息101已经被确认处理，那么在这个序号之前的消息100以及101都可以被认为已经被服务器正常接收并且处理完毕，此时可以从消息队列中删除掉序号101之前的消息了。&lt;/p&gt;&#xA;&lt;p&gt;反之，客户端同时还维护另外一个定时器，一段时间没有收到连接的网关消息时，将向网关发出心跳消息，如果仍然没有回复则认为网关出现异常，将重新走正常的登录流程尝试选择另外一台网关登录。重连之后，将重新发送消息队列中已经存在的消息。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;重连策略&#34;&gt;&#xA;  重连策略&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%87%8d%e8%bf%9e%e7%ad%96%e7%95%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;当一台网关出现问题需要客户端进行重连时，还需要考虑到不要因为重连问题导致了其他网关服务器也受影响，产生雪崩效应，此时还需要考虑以下几点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;打散重连时间：需要进行重连的客户端，在一个时间范围内选择一个随机的时间，这样将这些客户端的重连时间打散，不至于一下子都连接上来。&lt;/li&gt;&#xA;&lt;li&gt;指数退避：一次重连不上时，客户端还需要再次尝试进行多次重连，然而重连的时间需要像TCP协议那样在阻塞恢复时做指数退避，即第一次重连时间是1秒后，第二次2秒后，第三次4秒后，等等。这个策略也是为了避免由于重连导致的服务雪崩。&lt;/li&gt;&#xA;&lt;li&gt;服务器保护：上面两条是客户端的重连策略，然而服务器自身也需要进行保护，当服务器判断自己当前的负载到一定程度时，将拒绝客户端的连接请求。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;内部服务丢弃应答消息&#34;&gt;&#xA;  内部服务丢弃应答消息&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%86%85%e9%83%a8%e6%9c%8d%e5%8a%a1%e4%b8%a2%e5%bc%83%e5%ba%94%e7%ad%94%e6%b6%88%e6%81%af&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;同样的，内部服务也只是通过网关层与客户端进行通信，当处理了一些消息之后需要应答客户端，此时发现对应的网关已经宕机，那么应该丢弃掉这些应答消息，等待客户端重连之后重新将前面没有收到应答的消息发出来。&lt;/p&gt;&#xA;&lt;p&gt;如果是这个处理原则的话，对应的就需要服务器的逻辑中做到“幂等性（idempotent）”了，即同一个操作，一次请求与多次请求的结果是一样的。比如，逻辑服务器可以通过客户端的消息ID来判断这条消息之前是否已经被处理过，如果是的话可以直接忽略处理应答处理即可。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;服务保证&#34;&gt;&#xA;  服务保证&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%8d%e5%8a%a1%e4%bf%9d%e8%af%81&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;每个网关服务器可以容纳的长连接总数是固定的，到了一定程度系统资源就消耗的差不多了，应答的延迟也提高了。所以，网关层还需要考虑到服务的可用性。&lt;/p&gt;&#xA;&lt;p&gt;比如，可以向管理网关的服务器上报如下数据：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;当前维护的连接数量。&lt;/li&gt;&#xA;&lt;li&gt;当前应答延迟指标，90%的延迟到多少，99%的应答延迟到多少，等等。&lt;/li&gt;&#xA;&lt;li&gt;当前系统资源的消耗情况，比如CPU占用、内存占用等等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这样，可以有依据来判断该网关是否还能继续接收新的连接，如果不能接收连接可以返回一批当前可用的其他网关服务列表给客户端重新发起连接，同时将当前不可用的网关从返回给客户端的网关列表中删除，这样下次就不会再来这个网关进行连接。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;qos&#34; src=&#34;https://www.codedump.info/media/imgs/20190818-im-msg-gate/qos.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; qos &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图中，有如下步骤：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;网关都向网关管理服务上报自己当前的服务状态，管理服务发现网关A已经接近服务极限，此时将通知网关A此时不能再接收新的连接，同时还告知当前可用的网关B和C地址。&lt;/li&gt;&#xA;&lt;li&gt;客户端向网关A发起请求，此时网关A拒绝该连接请求，并且返回网关B和C的服务列表给客户端。&lt;/li&gt;&#xA;&lt;li&gt;客户端选择网关C进行连接。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以看到，这实际上是“服务降级”的一种做法。&lt;/p&gt;</description>
    </item>
    <item>
      <title>glog C&#43;&#43;版本代码分析</title>
      <link>https://www.codedump.info/zh/post/20190729-glog/</link>
      <pubDate>Mon, 29 Jul 2019 11:43:56 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190729-glog/</guid>
      <description>&lt;p&gt;本文基于glog CPP版本的0.4.0版本，对glog的实现机制做一些简单的分析记录。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;要记录一条日志，分为两个阶段：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;首先生成日志。&lt;/li&gt;&#xA;&lt;li&gt;然后将日志内容输出到相应的设备，如标准输出、文件等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下就分两部分展开对glog的分析。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;日志的生成&#34;&gt;&#xA;  日志的生成&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%97%a5%e5%bf%97%e7%9a%84%e7%94%9f%e6%88%90&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;一般有两种生成日志数据的方式：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;类printf的方式，将需要输入的数据格式化。&lt;/li&gt;&#xA;&lt;li&gt;类C++ stream流的方式，提供出来operator &amp;laquo;操作符供输入数据。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;前者的好处在于可以对输入的数据格式进行严格检查，不匹配的情况下编译器会进行告警。缺点则是不够灵活。&#xA;后者的好处是灵活，除了用了进行一般的日志输入，还可以写出类似&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;CHECK_IF(某条件不成立) &amp;lt;&amp;lt; 输出日志&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;的操作。&lt;/p&gt;&#xA;&lt;p&gt;glog中选择了第二种方式。&lt;/p&gt;&#xA;&lt;p&gt;首先来看glog对外暴露的用于日志输入的接口。其对应的宏是：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;#define LOG(severity) COMPACT_GOOGLE_LOG_ ## severity.stream()&#xA;&#xA;#define COMPACT_GOOGLE_LOG_INFO google::LogMessage( \&#xA;    __FILE__, __LINE__)&#xA;#define COMPACT_GOOGLE_LOG_WARNING google::LogMessage( \&#xA;    __FILE__, __LINE__, google::GLOG_WARNING)&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;从中可以看到glog中每一条日志，都对应一个LogMessage的类，然后将返回其中的stream()对象输入日志数据。&lt;/p&gt;&#xA;&lt;p&gt;每个LogMessage内部有一个名为LogMessageData的成员，用于保存这些数据，其中比较重要的成员有以下几个：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;char message_text_[LogMessage::kMaxLogMessageLen+1]; // 用户存储日志的固定长度数组，大小为30KB。&#xA;LogStream stream_; // 用于接收用户日志的C++stream，构造时传入上面的message_text_来构造，所以实际写数据会到message_text_中。&#xA;void (LogMessage::*send_method_)(); // 用户最终发送日志数据的函数指针。&#xA;timestamp_、tm_time_：保存日志时间相关的成员。&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;实际根据日志创建出一个LogMessage对象时，会根据不同类型的日志，传入不同的send_method函数指针，而每个不同的LogMessage构造时都会去调用其内部的Init函数完成LogMessageData的构造，Init函数主要做的事情有：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;存储send_method函数指针。&lt;/li&gt;&#xA;&lt;li&gt;获取当前的系统时间，存放到相应的成员中。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;而LOG之类的宏，实际返回的就是LogMessageData的stream指针，待到一切的输入完毕，这一条日志对应的LogMessage就会被析构，其析构函数内又会调用成员函数Flush，这个函数最终完成将日志输出的操作：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;void LogMessage::Flush() {&#xA;  // ...&#xA;  {&#xA;    MutexLock l(&amp;amp;log_mutex);&#xA;    (this-&amp;gt;*(data_-&amp;gt;send_method_))();&#xA;    ++num_messages_[static_cast&amp;lt;int&amp;gt;(data_-&amp;gt;severity_)];&#xA;  }&#xA;  // ...&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;有了以上的准备，实际回头来看一个日志的输入&lt;/p&gt;</description>
    </item>
    <item>
      <title>服务调用的演进历史</title>
      <link>https://www.codedump.info/zh/post/20190629-service-history/</link>
      <pubDate>Sat, 29 Jun 2019 12:47:07 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190629-service-history/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;这是2019年给组内分享时整理的一篇服务调用演进历史的科普文。写作本文的时候，我自己最大的感受是：如果能清楚理解演化历史中的一些原则和思路，就会发现现在的变化并不新鲜。它们不是今天才有，也不会止于今天的演化。在技术大发展的今天，更多的关注本质才能让我们不至于在变化中失去方向。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;这个题目稍微有点大，纯粹是一篇科普文，将我所了解到的解决“服务调用”相关的技术演进历史简述一下，本文专注于演化过程中每一步的为什么（Why）和是什么（What）上面，尽量不在技术细节（How）上面做太多深入。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;服务的三要素&#34;&gt;&#xA;  服务的三要素&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%8d%e5%8a%a1%e7%9a%84%e4%b8%89%e8%a6%81%e7%b4%a0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;一般而言，一个网络服务包括以下的三个要素：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;地址：调用方根据地址访问到网络接口。地址包括以下要素：IP地址、服务端口、服务协议（TCP、UDP，etc）。&lt;/li&gt;&#xA;&lt;li&gt;协议格式：协议格式指的是该协议都有哪些字段，由接口提供者与协议调用者协商之后确定下来。&lt;/li&gt;&#xA;&lt;li&gt;协议名称：或者叫协议类型，因为在同一个服务监听端口上面，可能同时提供多种接口服务于调用方，这时候需要协议类型（名称）来区分不同的网络接口。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要说明在服务地址中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;IP地址提供了在互联网上找到这台机器的凭证。&lt;/li&gt;&#xA;&lt;li&gt;协议以及服务端口提供了在这台机器上找到提供服务的进程的凭证。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;service-address&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/service-address.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; service address &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;这都属于TCPIP协议栈的知识点，不在这里深入详述。&lt;/p&gt;&#xA;&lt;p&gt;下图中，以最简单的一个HTTP请求，来拆解请求URL中的服务要素：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;http-request&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/http-request.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; http-request &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;http：指明使用的是哪种应用层协议，同类型的还有“https”、“ftp”等。&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.abc.com&#34;&gt;www.abc.com&lt;/a&gt;：域名地址，最终会由DNS域名解析服务器解析成数字的IP地址。&lt;/li&gt;&#xA;&lt;li&gt;8080：前面解析成数字化的IP地址之后，就可以访问到具体提供服务的机器上，但是上面提供服务的进程可能有很多，这时候就需要端口号来告诉协议栈到底是访问哪个进程提供的服务了。&lt;/li&gt;&#xA;&lt;li&gt;hello：该服务进程中，可能提供多个接口供访问，所以需要接口名+协议（即前面的http）告诉进程访问哪个协议的哪个接口。&lt;/li&gt;&#xA;&lt;li&gt;msg=world：不同的接口，需要的参数不同，最后跟上的查询参数（query param）告诉服务请求该接口服务时传入的参数。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以上，简单的把网络服务的几个要素进行了描述。&lt;/p&gt;&#xA;&lt;p&gt;这里还需要对涉及到服务相关的一些名词做解释。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;服务实例：服务对应的IP地址加端口的简称。需要访问服务的时候，需要先寻址知道该服务每个运行实例的地址加端口，然后才能建立连接进行访问。&lt;/li&gt;&#xA;&lt;li&gt;服务注册：某个服务实例宣称自己提供了哪些服务，即某个IP地址+端口都提供了哪些服务接口。&lt;/li&gt;&#xA;&lt;li&gt;服务发现：调用方通过某种方式找到服务提供方，即知道服务运行的IP地址加端口。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;基于ip地址的调用&#34;&gt;&#xA;  基于IP地址的调用&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8eip%e5%9c%b0%e5%9d%80%e7%9a%84%e8%b0%83%e7%94%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;最初的网络服务，通过原始的IP地址暴露给调用者。这种方式有以下的问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;IP地址是难于记忆并且无意义的。&lt;/li&gt;&#xA;&lt;li&gt;另外，从上面的服务三要素可以看到，IP地址其实是一个很底层的概念，直接对应了一台机器上的一个网络接口，如果直接使用IP地址进行寻址，更换机器就变的很麻烦。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;“尽量不使用过于底层的概念来提供服务”，是这个演化流程中的重要原则，好比在今天已经很少能够看到直接用汇编语言编写代码的场景了，取而代之的，就是越来越多的抽象，本文中就展现了服务调用这一领域在这个过程中的演进流程。&lt;/p&gt;&#xA;&lt;p&gt;在现在除非是测试阶段，否则已经不能直接以IP地址的形式将服务提供出去了。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;域名系统&#34;&gt;&#xA;  域名系统&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%9f%e5%90%8d%e7%b3%bb%e7%bb%9f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面的IP地址是给主机做为路由器寻址的数字型标识，并不好记忆。此时产生了域名系统，与单纯提供IP地址相比，域名系统由于使用有意义的域名来标识服务，所以更容易记忆。另外，还可以更改域名所对应的IP地址，这为变换机器提供了便利。有了域名之后，调用方需要访问某个网络服务时，首先到域名地址服务中，根据DNS协议将域名解析为相应的IP地址，再根据返回的IP地址来访问服务。&lt;/p&gt;&#xA;&lt;p&gt;从这里可以看到，由于多了一步到域名地址服务查询映射IP地址的流程，所以多了一步解析，为了减少这一步带来的影响，调用方会缓存解析之后的结果，在一段时间内不过期，这样就省去了这一步查询的代价。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;协议的接收与解析&#34;&gt;&#xA;  协议的接收与解析&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8d%8f%e8%ae%ae%e7%9a%84%e6%8e%a5%e6%94%b6%e4%b8%8e%e8%a7%a3%e6%9e%90&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;以上通过域名系统，已经解决了服务IP地址难以记忆的问题，下面来看协议格式解析方面的演进。&lt;/p&gt;&#xA;&lt;p&gt;一般而言，一个网络协议包括两部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;协议包头：这里存储协议的元信息（meta infomation），其中可能会包括协议类型、报体长度、协议格式等。需要说明的是，包头一般为固定大小，或者有明确的边界（如HTTP协议中的\r\n结束符），否则无法知道包头何时结束。&lt;/li&gt;&#xA;&lt;li&gt;协议包体：具体的协议内容。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;无论是HTTP协议，又或者是自定义的二进制网络协议，大体都由这两部分组成。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;protocol-format&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/protocol-format.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; protocol format &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;由于很多时候不能一口气接收完毕客户端的协议数据，因此在接收协议数据时，一般采用状态机来做协议数据的接收：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;protocol-statemachine&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/protocol-statemachine.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; protocol statemachine &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;接收完毕了网络数据，在协议解析方面却长期停滞不前。一个协议，有多个字段（field），而这些不同的字段有不同的类型，简单的raw类型（如整型、字符串）还好说，但是遇到复杂的类型如字典、数组等就比较麻烦。&lt;/p&gt;&#xA;&lt;p&gt;当时常见的手段有以下几种：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;使用json或者xml这样的数据格式。好处是可视性强，表达起上面的复杂类型也方便，缺陷是容易被破解，传输过去的数据较大。&lt;/li&gt;&#xA;&lt;li&gt;自定义二进制协议。每个公司做大了，在这一块难免有几个类似的轮子。笔者见过比较典型的是所谓的TLV格式（Type-Length-Value），自定义二进制格式最大的问题出现在协议联调与协商的时候，由于可视性比较弱，有可能这边少了一个字段那边多了一个字段，给联调流程带来麻烦。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;上面的问题一直到Google的Protocol Buffer（以下简称PB）出现之后才得到很大的改善。PB出现之后，也有很多类似的技术出现，如Thrift、MsgPack等，不在这里阐述，将这一类技术都以PB来描述。&lt;/p&gt;&#xA;&lt;p&gt;与前面的两种手段相比，PB具有以下的优点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;使用proto格式文件来定义协议格式，proto文件是一个典型的DSL（domain-specific language）文件，文件中描述了协议的具体格式，每个字段都是什么类型，哪些是可选字段哪些是必选字段。有了proto文件之后，C\S两端是通过这个文件来进行协议的沟通交流的，而不是具体的技术细节。&lt;/li&gt;&#xA;&lt;li&gt;PB能通过proto文件生成各种语言对应的序列化反序列化代码，给跨语言调用提供了方便。&lt;/li&gt;&#xA;&lt;li&gt;PB自己能够对特定类型进行数据压缩，减少数据大小。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;pb&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/pb.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; pb &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;服务网关&#34;&gt;&#xA;  服务网关&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%8d%e5%8a%a1%e7%bd%91%e5%85%b3&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;有了前面的演化之后，写一个简单的单机服务器已经不难。然而，当随着访问量的增大，一台机器已经不足以支撑所有的请求，此时就需要横向扩展多加一些业务服务器。&lt;/p&gt;</description>
    </item>
    <item>
      <title>IM服务器设计-消息存储</title>
      <link>https://www.codedump.info/zh/post/20190608-im-msg-storage/</link>
      <pubDate>Sat, 08 Jun 2019 20:18:47 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190608-im-msg-storage/</guid>
      <description>&lt;p&gt;这部分专门讲述IM消息存储的设计。消息存储的难度在于，要考虑以下的场景：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;离线消息存储。即发送消息时对方不在线该怎么处理。&lt;/li&gt;&#xA;&lt;li&gt;单聊、群聊消息。&lt;/li&gt;&#xA;&lt;li&gt;随着用户量越来越大，应该以后如何扩展。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;读扩散-vs-写扩散&#34;&gt;&#xA;  读扩散 VS 写扩散&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%af%bb%e6%89%a9%e6%95%a3-vs-%e5%86%99%e6%89%a9%e6%95%a3&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;消息同步模型中，有写扩散和读扩散这两种模型。在开始讨论之前需要先了解两个相关的概念：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;收件箱（inbox）：该用户收到的消息。&lt;/li&gt;&#xA;&lt;li&gt;发件箱（outbox）：该用户发出的消息。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;写扩散push&#34;&gt;&#xA;  写扩散（push）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%86%99%e6%89%a9%e6%95%a3push&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;写扩散就是经常说的push模式，即每个消息都直接发送到该用户的收件箱中。其优缺点如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;优点：读优化，用户每次只需要去读取自己收件箱中的消息即可。&lt;/li&gt;&#xA;&lt;li&gt;缺点：写很重，如果这个消息是一条群消息，那么一个群成员发送出去的消息将拷贝到所有其余群成员的收件箱中。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-msg-push&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-msg-storage/im-msg-push.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im msg push &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;读扩散pull&#34;&gt;&#xA;  读扩散（pull）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%af%bb%e6%89%a9%e6%95%a3pull&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;读扩散就是pull模式，用户每次到消息发送者的发件箱去拉取消息，优缺点如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;优点：写优化，每次发送的消息只需要写到一个地方，由收件者自己去拉取消息即可。&lt;/li&gt;&#xA;&lt;li&gt;缺点：读操作很重，假设一个用户有一千个好友，重新登录时需要拉取这些好友所有的离线消息。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-msg-pull&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-msg-storage/im-msg-pull.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im msg pull &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;最终选择的是以pull模式为主的模式，理由在于：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;IM业务属于『写多读少』类型的业务，如果使用push模式，将造成消息的大量冗余。&lt;/li&gt;&#xA;&lt;li&gt;pull模式读操作较重的缺陷可以通过其他方式来优化解决。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;下面来看具体的设计。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;表设计&#34;&gt;&#xA;  表设计&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%a1%a8%e8%ae%be%e8%ae%a1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在数据库设计中，仅使用一个发送消息表来存储消息的具体内容，而另外有一个消息接收表用来存储消息的ID信息而不是具体内容，这样用户查询消息时，大体流程如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;首先拉取接收消息表中的信息。&lt;/li&gt;&#xA;&lt;li&gt;根据接收消息表中的ID以及发送者ID信息到发送信息表来具体查询消息。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-msg&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-msg-storage/im-msg.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im msg &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;用户发送消息表&#34;&gt;&#xA;  用户发送消息表&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%94%a8%e6%88%b7%e5%8f%91%e9%80%81%e6%b6%88%e6%81%af%e8%a1%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;无论是单聊还是群聊消息，都使用这个表来存储发送出去的消息。&lt;/p&gt;&#xA;&lt;p&gt;im_message_send（msg_id,msg_from,msg_to,msg_seq,msg_content,send_time,msg_type）&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;msg_id：消息ID。&lt;/li&gt;&#xA;&lt;li&gt;msg_from：消息发送者UID。&lt;/li&gt;&#xA;&lt;li&gt;msg_to：消息接收者。如果是单聊消息那么就是用户UID，如果是群聊消息就是群ID。&lt;/li&gt;&#xA;&lt;li&gt;msg_seq：客户端发送消息时带上的序列号，主要用于消息排重以及通知客户端消息发送成功之用。&lt;/li&gt;&#xA;&lt;li&gt;msg_content：消息内容。&lt;/li&gt;&#xA;&lt;li&gt;send_time：消息发送时间。&lt;/li&gt;&#xA;&lt;li&gt;msg_type：消息类型，如单聊、群聊消息等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;用户接收消息表&#34;&gt;&#xA;  用户接收消息表&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%94%a8%e6%88%b7%e6%8e%a5%e6%94%b6%e6%b6%88%e6%81%af%e8%a1%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;im_message_recieve（id,msg_from,msg_to,msg_id,flag）&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;id：这个表的ID，自增。&lt;/li&gt;&#xA;&lt;li&gt;msg_from：消息发送者ID。&lt;/li&gt;&#xA;&lt;li&gt;msg_to：消息接收者ID。&lt;/li&gt;&#xA;&lt;li&gt;msg_id：消息ID，对应发送消息表中的ID。&lt;/li&gt;&#xA;&lt;li&gt;flag：标志位，表示该消息是否已读。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;接收消息表的信息并没有很多，因为主体部分如消息内容、发送消息时间等都在发送消息表中。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-msg-table&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-msg-storage/im-msg-table.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im msg table &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;分库分表及访问策略&#34;&gt;&#xA;  分库分表及访问策略&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%88%86%e5%ba%93%e5%88%86%e8%a1%a8%e5%8f%8a%e8%ae%bf%e9%97%ae%e7%ad%96%e7%95%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;发送消息表，根据msg_from字段做为分库分表的依据，而接收消息表则使用msg_to字段做为分库分表的依据。&lt;/p&gt;</description>
    </item>
    <item>
      <title>IM服务器设计-基础</title>
      <link>https://www.codedump.info/zh/post/20190608-im-design-base/</link>
      <pubDate>Sat, 08 Jun 2019 11:09:10 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190608-im-design-base/</guid>
      <description>&lt;p&gt;IM做为非常经典的服务器系统，其设计时候的考量具备代表性，所以这一次花几个篇幅讨论其相关设计。&lt;/p&gt;&#xA;&lt;p&gt;主要内容相当部分参考了 &lt;a href=&#34;http://www.52im.net/thread-812-1-1.html&#34;&gt;一套海量在线用户的移动端IM架构设计实践分享&lt;/a&gt;一文，在此之上补充了更好的消息存储设计以及集群设计。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;整体架构&#34;&gt;&#xA;  整体架构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b4%e4%bd%93%e6%9e%b6%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-arch&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-design-base/im-arch.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im arch &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;以上架构图中，分为几个部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;客户端：支持IOS、Android系统。&lt;/li&gt;&#xA;&lt;li&gt;接入层：负责维护与客户端之间的长连接。&lt;/li&gt;&#xA;&lt;li&gt;逻辑层：负责IM系统中各逻辑功能的实现。&lt;/li&gt;&#xA;&lt;li&gt;存储层：存储IM系统相关的数据，主要包括Redis缓存系统（用于保存用户状态及路由数据）、消息数据。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;上图中几部分的交互如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;客户端通过gate接入IM服务器。在这里，客户端与gate之间保持TCP长连接，客户端使用DNS查询域名返回最近的gate地址进行连接。&lt;/li&gt;&#xA;&lt;li&gt;Gate的作用：保持与客户端之间的长连接，将请求数据转发给后面的逻辑服务LogicServer。LogicServer最上面是一个消息路由服务Router，根据请求的类型转发到后面具体的逻辑服务器。其中c代表客户端，s代表服务器，g代表群组，因此比如c2c服务就是处理客户端之间消息的服务器，而auth服务是处理客户端登录请求的服务器。&lt;/li&gt;&#xA;&lt;li&gt;逻辑类服务器与存储层服务打交道，其中：redis用于存储用户在线状态、用户路由数据（用户路由数据就是指用户在哪个gate服务上维护长连接），而DB用于存储用户的消息数据，这部分留待下一部分讲解。&lt;/li&gt;&#xA;&lt;li&gt;以上的接入层、逻辑层由于本身不存储状态，因此都可以进行横向扩展。看似Gate维护着长连接，但是即使一个Gate宕机，客户端检测到之后可以重新发起请求接入另一台Gate服务器。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;数据存储&#34;&gt;&#xA;  数据存储&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e5%ad%98%e5%82%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;路由数据：存放在Redis中，格式为（UID,客户端在哪个gate登录）。&lt;/li&gt;&#xA;&lt;li&gt;消息数据：存储在DB中，部分也会缓存在缓存中方便查询，这部分做为下一部分文章的重点来讲解，不在这部分展开讨论。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;核心交互流程&#34;&gt;&#xA;  核心交互流程&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a0%b8%e5%bf%83%e4%ba%a4%e4%ba%92%e6%b5%81%e7%a8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;统一登录系统&#34;&gt;&#xA;  统一登录系统&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%bb%9f%e4%b8%80%e7%99%bb%e5%bd%95%e7%b3%bb%e7%bb%9f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;登录授权auth&#34;&gt;&#xA;  登录授权（auth）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%99%bb%e5%bd%95%e6%8e%88%e6%9d%83auth&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-login&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-design-base/im-login.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im login &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;客户端通过统一登录系统验证登录密码等。&lt;/li&gt;&#xA;&lt;li&gt;SSO验证客户端用户名密码之后，生成登录token并返回给客户端。&lt;/li&gt;&#xA;&lt;li&gt;客户端使用UID和返回的token向gate发起授权验证请求。&lt;/li&gt;&#xA;&lt;li&gt;gate同步调用logic server的验证接口。&lt;/li&gt;&#xA;&lt;li&gt;logic server请求SSO系统验证token合法性。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;SSO向auth系统返回验证token结果。&lt;/li&gt;&#xA;&lt;li&gt;如果验证成功，auth系统在redis中存储客户端的路由信息，即客户端在哪个gate上登录。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;ol start=&#34;6&#34;&gt;&#xA;&lt;li&gt;auth系统向gate返回验证登录结果。&lt;/li&gt;&#xA;&lt;li&gt;gate向客户端返回授权结果。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;登出logout&#34;&gt;&#xA;  登出（logout）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%99%bb%e5%87%balogout&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-logout&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-design-base/im-logout.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im logout &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;客户端向gate发出logout请求。&lt;/li&gt;&#xA;&lt;li&gt;gate设置客户端UID对应的peer无效，然后应答客户端登出成功。&lt;/li&gt;&#xA;&lt;li&gt;gate向logic server发出登录请求。&lt;/li&gt;&#xA;&lt;li&gt;处理该类请求的c2s服务器，清除redis中的客户端路由信息。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;踢人kickout&#34;&gt;&#xA;  踢人（kickout）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%b8%a2%e4%ba%bakickout&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;用户请求授权时，可能在另一个设备（同类型设备，比如一台苹果手机登录时发现一台安卓手机也在登录这个账号）开着软件处于登录状态。这种情况需要系统将那个设备踢下线。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-kickout&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-design-base/im-kickout.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im kickout &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;新的客户端登陆流程同上面的登陆认证流程，只不过在auth模块完成认证之后，会做如下的操作：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;根据UID到redis中查询路由数据，如果不存在说明前面没有登陆过，那么就像登陆流程一样返回即可。&lt;/li&gt;&#xA;&lt;li&gt;否则说明前面已经有其他设备登陆了，将向前面的gate发送踢人请求，然后保存新的路由信息到redis中。&lt;/li&gt;&#xA;&lt;li&gt;gate接收到踢人请求，踢掉客户端之后断掉与客户端的连接。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;客户端上报消息c2s消息&#34;&gt;&#xA;  客户端上报消息（c2s消息）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%a2%e6%88%b7%e7%ab%af%e4%b8%8a%e6%8a%a5%e6%b6%88%e6%81%afc2s%e6%b6%88%e6%81%af&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;im-c2smsg&#34; src=&#34;https://www.codedump.info/media/imgs/20190608-im-design-base/im-c2smsg.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; im c2s msg &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;</description>
    </item>
    <item>
      <title>OpenResty Lua Stream实现分析</title>
      <link>https://www.codedump.info/zh/post/20190501-lua-stream/</link>
      <pubDate>Wed, 01 May 2019 15:03:45 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190501-lua-stream/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;OpenResty（以下简称OR）是Lua应用的典范，其最大的亮点在于，使用Lua协程搭配上异步非阻塞的IO，这样开发者可以使用同步方式来编写代码，而底层IO调度、唤醒等操作留给C编写的引擎层。&lt;/p&gt;&#xA;&lt;p&gt;实际上，使用类协程的技术，让异步操作同步化，已经有很多相关的技术了，比如腾讯的libco、百度的brpc都是自己在C层面实现了类协程的机制，不过这一类技术用的最广泛的还是OR。市面上分析OR内部实现的文章并不算很多，所以这段时间研究了一下OR的实现。&lt;/p&gt;&#xA;&lt;p&gt;OR内部，其实是分7层HTTP的ngx_lua模块，以及四层TCP的lua_stream实现，两者在很多部分都很相近，以下分析以4层的lua_stream来解释，对应的版本是openresty-1.13.6.1和ngx_stream_lua-0.0.3的实现。&lt;/p&gt;&#xA;&lt;p&gt;既然OR在这里选择了使用协程来将用户的异步操作同步化，那么实际上内部其实实现了一个简易版本的操作系统内核的“CPU调度”，其中一个一个的协程就是CPU调度单位，因此在这里分为几部分来分析：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;维护协程的数据结构。&lt;/li&gt;&#xA;&lt;li&gt;创建新协程的时候如何进行初始化？&lt;/li&gt;&#xA;&lt;li&gt;协程调度算法？&lt;/li&gt;&#xA;&lt;li&gt;如何将异步操作同步化？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在这里，先列举出来OR中与“调度”相关的核心数据结构和函数：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;调度相关核心组件&lt;/th&gt;&#xA;          &lt;th&gt;数据结构或函数&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;调度单元&lt;/td&gt;&#xA;          &lt;td&gt;Lua协程（lua_State）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;保存协程信息&lt;/td&gt;&#xA;          &lt;td&gt;ngx_stream_lua_co_ctx_t&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;当前调度协程信息&lt;/td&gt;&#xA;          &lt;td&gt;ngx_stream_lua_ctx_t.cur_co_ctx成员，指向一个ngx_stream_lua_co_ctx_t类型指针&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;调度函数&lt;/td&gt;&#xA;          &lt;td&gt;ngx_stream_lua_run_thread&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;协程的维护&#34;&gt;&#xA;  协程的维护&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8d%8f%e7%a8%8b%e7%9a%84%e7%bb%b4%e6%8a%a4&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;OR中有以下两种场景能够创建出来一个协程：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一个tcp请求自动对应一个协程。这种场景用户不能控制，即默认就是这么实现的，当收到一个TCP请求默认创建出来一个协程与之绑定。&lt;/li&gt;&#xA;&lt;li&gt;Lua代码内部显示调用thread.spawn函数创建一个用户线程时。与前者不同，这种场景就是用户可以自己控制的。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;lua stream内部，协程相关的数据结构存储在ngx_stream_lua_co_ctx_t中，既然OR里面使用协程来模拟用户线程，不难想象这个数据结构内部应该有以下的成员：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;维护协程内部栈关系的数据。由于OR采用了Lua协程，这部分当然就是留给Lua协程来处理了。&lt;/li&gt;&#xA;&lt;li&gt;保存协程状态的数据。&lt;/li&gt;&#xA;&lt;li&gt;维护协程之间关系的数据，比如父子协程、僵尸子协程，等等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;下面简单的看一下其成员：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;void *data：存储用户相关数据。&lt;/li&gt;&#xA;&lt;li&gt;lua_State *co：存储Lua协程指针。&lt;/li&gt;&#xA;&lt;li&gt;ngx_stream_lua_co_ctx_t *parent_co_ctx：存储父协程指针。&lt;/li&gt;&#xA;&lt;li&gt;ngx_stream_lua_posted_thread_t *zombie_child_threads：将该协程管理的僵尸子进程放在这个队列中。&lt;/li&gt;&#xA;&lt;li&gt;int co_ref：在Lua的registry表中对应该协程指针的引用值。&lt;/li&gt;&#xA;&lt;li&gt;unsigned waited_by_parent：为1的情况下表示该协程的父协程在等待该协程的退出。&lt;/li&gt;&#xA;&lt;li&gt;unsigned co_status：当前协程状态。&lt;/li&gt;&#xA;&lt;li&gt;unsigned is_uthread：为1的情况下表示该协程是用户线程，即上面提到的场景2创建出来的协程。&lt;/li&gt;&#xA;&lt;li&gt;unsigned thread_spawn_yielded：为1的情况下表示当前协程是由于创建了用户线程（前面的场景2）才让出的执行权。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;ngx_stream_lua_co_ctx_t&#34; src=&#34;https://www.codedump.info/media/imgs/20190501-lua-stream/ngx_stream_lua_co_ctx_t.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; ngx_stream_lua_co_ctx_t &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;另外，还有一个全局变量ngx_stream_lua_ctx_t，其中的cur_co_ctx指针指向当前被调度执行的ngx_stream_lua_co_ctx_t指针。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;协程的初始化&#34;&gt;&#xA;  协程的初始化&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8d%8f%e7%a8%8b%e7%9a%84%e5%88%9d%e5%a7%8b%e5%8c%96&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;上一部分提到了创建协程的两种场景，这里就来分析这两种场景下面协程的初始化。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;新建立连接的协程&#34;&gt;&#xA;  新建立连接的协程&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%96%b0%e5%bb%ba%e7%ab%8b%e8%bf%9e%e6%8e%a5%e7%9a%84%e5%8d%8f%e7%a8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;OR通过在nginx配置文件中填写&amp;quot;content_by_lua_block&amp;quot;等，来配置新建一个连接时对应的Lua脚本，这种场景下OR会默认创建出来一个Lua协程来执行这段脚本代码。&lt;/p&gt;&#xA;&lt;p&gt;对应创建Lua协程的代码在函数ngx_stream_lua_new_thread中，下面来分析这个函数的流程。&lt;/p&gt;&#xA;&lt;p&gt;OR中需要在Registry表中存储每个创建出来的Lua协程的reference，这个存储协程的表在Registry表中对应的key是全局变量ngx_stream_lua_coroutines_key的指针，因此下面这段代码就是从Registry表中查询这个表返回到栈顶：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;lua_pushlightuserdata&lt;/span&gt;(L, &amp;amp;ngx_stream_lua_coroutines_key);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;lua_rawget&lt;/span&gt;(L, LUA_REGISTRYINDEX);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;接着下来就是创建了一个新的协程，同时初始化其全局表：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 创建Lua协程&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;co = &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;lua_newthread&lt;/span&gt;(L);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 创建该协程的全局表&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;ngx_stream_lua_create_new_globals_table&lt;/span&gt;(co, 0, 0);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 再创建一个新表&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;lua_createtable&lt;/span&gt;(co, 0, 1);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 拿到全局表&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;ngx_stream_lua_get_globals_table&lt;/span&gt;(co);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 全局表的__index指向新创建的表&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;lua_setfield&lt;/span&gt;(co, -2, &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;__index&amp;#34;&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 全局表的meta table指向新创建的表&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;lua_setmetatable&lt;/span&gt;(co, -2);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// set 全局表回去&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;ngx_stream_lua_set_globals_table&lt;/span&gt;(co);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从上面的代码可以看出，新创建的协程，其全局表目前是一个空表。&lt;/p&gt;</description>
    </item>
    <item>
      <title>redis高可用原理</title>
      <link>https://www.codedump.info/zh/post/20190409-redis-sentinel/</link>
      <pubDate>Sun, 21 Apr 2019 22:52:16 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190409-redis-sentinel/</guid>
      <description>&lt;p&gt;redis中为了实现高可用（High Availability，简称HA），采用了如下两个方式：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;主从复制数据。&lt;/li&gt;&#xA;&lt;li&gt;采用哨兵监控数据节点的运行情况，一旦主节点出现问题由从节点顶上继续进行服务。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;主从复制&#34;&gt;&#xA;  主从复制&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%bb%e4%bb%8e%e5%a4%8d%e5%88%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;redis中主从节点复制数据有全量复制和部分复制之分。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;旧版本全量复制功能的实现&#34;&gt;&#xA;  旧版本全量复制功能的实现&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%97%a7%e7%89%88%e6%9c%ac%e5%85%a8%e9%87%8f%e5%a4%8d%e5%88%b6%e5%8a%9f%e8%83%bd%e7%9a%84%e5%ae%9e%e7%8e%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;全量复制使用snyc命令来实现，其流程是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;从服务器向主服务器发送sync命令。&lt;/li&gt;&#xA;&lt;li&gt;主服务器在收到sync命令之后，调用bgsave命令生成最新的rdb文件，将这个文件同步给从服务器，这样从服务器载入这个rdb文件之后，状态就会和主服务器执行bgsave命令时候的一致。&lt;/li&gt;&#xA;&lt;li&gt;主服务器将保存在命令缓冲区中的写命令同步给从服务器，从服务器执行这些命令，这样从服务器的状态就跟主服务器当前状态一致了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;sync&#34; src=&#34;https://www.codedump.info/media/imgs/20190409-redis-sentinel/sync.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; sync &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;旧版本全量复制功能，其最大的问题是从服务器断线重连时，即便在从服务器上已经有一部分数据了，也需要进行全量复制，这样做的效率很低，于是新版本的redis在这部分做了改进。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;新版本全量复制功能的实现&#34;&gt;&#xA;  新版本全量复制功能的实现&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%96%b0%e7%89%88%e6%9c%ac%e5%85%a8%e9%87%8f%e5%a4%8d%e5%88%b6%e5%8a%9f%e8%83%bd%e7%9a%84%e5%ae%9e%e7%8e%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;新版本redis使用psync命令来代替sync命令，该命令既可以实现完整全同步也可以实现部分同步。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;复制偏移量&#34;&gt;&#xA;  复制偏移量&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%8d%e5%88%b6%e5%81%8f%e7%a7%bb%e9%87%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;执行复制的双方，主从服务器，分别会维护一个复制偏移量：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;主服务器每次向从服务器同步了N字节数据之后，将修改自己的复制偏移量+N。&lt;/li&gt;&#xA;&lt;li&gt;从服务器每次从主服务器同步了N字节数据之后，将修改自己的复制偏移量+N。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;复制积压缓冲区&#34;&gt;&#xA;  复制积压缓冲区&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%8d%e5%88%b6%e7%a7%af%e5%8e%8b%e7%bc%93%e5%86%b2%e5%8c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;主服务器内部维护了一个固定长度的先进先出队列做为复制积压缓冲区，其默认大小为1MB。&lt;/p&gt;&#xA;&lt;p&gt;在主服务器进行命令传播时，不仅会将写命令同步到从服务器，还会将写命令写入复制积压缓冲区。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;replication-backlog&#34; src=&#34;https://www.codedump.info/media/imgs/20190409-redis-sentinel/replication-backlog.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; replication-backlog &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;服务器运行id&#34;&gt;&#xA;  服务器运行ID&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9c%8d%e5%8a%a1%e5%99%a8%e8%bf%90%e8%a1%8cid&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;每个redis服务器，都有其运行ID，运行ID由服务器在启动时自动生成，主服务器会将自己的运行ID发送给从服务器，而从服务器会将主服务器的运行ID保存起来。&lt;/p&gt;&#xA;&lt;p&gt;从服务器redis断线重连之后进行同步时，就是根据运行ID来判断同步的进度：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如果从服务器上面保存的主服务器运行ID与当前主服务器运行ID一致，则认为这一次断线重连连接的是之前复制的主服务器，主服务器可以继续尝试部分同步操作。&lt;/li&gt;&#xA;&lt;li&gt;否则，如果前后两次主服务器运行ID不相同，则认为是完成全同步流程。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;psync命令流程&#34;&gt;&#xA;  psync命令流程&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#psync%e5%91%bd%e4%bb%a4%e6%b5%81%e7%a8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;有了前面的准备，下面开始分析psync命令的流程：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如果从服务器之前没有复制过任何主服务器，或者之前执行过slaveof no one命令，那么从服务器就会向主服务器发送psync ? -1命令，请求主服务器进行数据的全量同步。&lt;/li&gt;&#xA;&lt;li&gt;否则，如果前面从服务器已经同步过部分数据，那么从服务器向主服务器发送psync &amp;lt;runid&amp;gt; &amp;lt;offset&amp;gt;命令，其中runid是上一次主服务器的运行id，offset是当前从服务器的复制偏移量。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;前面两种情况主服务器收到psync命令之后，会出现以下三种可能：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;主服务器返回+fullresync &amp;lt;runid&amp;gt; &amp;lt;offset&amp;gt;回复，表示主服务器要求与从服务器进行完整的数据全量同步操作。其中，runid是当前主服务器运行id，而offset是当前主服务器的复制偏移量。&lt;/li&gt;&#xA;&lt;li&gt;如果主服务器应答+continue，那么表示主服务器与从服务器进行部分数据同步操作，将从服务器缺失的数据同步过来即可。&lt;/li&gt;&#xA;&lt;li&gt;如果主服务器应答-err，那么表示主服务器版本低于2.8，识别不了psync命令，此时从服务器将向主服务器发送sync命令，执行完整的全量数据同步。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;psync&#34; src=&#34;https://www.codedump.info/media/imgs/20190409-redis-sentinel/psync.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; psync &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;哨兵机制概述&#34;&gt;&#xA;  哨兵机制概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%93%a8%e5%85%b5%e6%9c%ba%e5%88%b6%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;redis使用哨兵机制来实现高可用(HA)，其大概工作原理是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;redis使用一组哨兵（sentinel）节点来监控主从redis服务的可用性。&lt;/li&gt;&#xA;&lt;li&gt;一旦发现redis主节点失效，将选举出一个哨兵节点作为领导者（leader）。&lt;/li&gt;&#xA;&lt;li&gt;哨兵领导者再从剩余的从redis节点中选出一个redis节点作为新的主redis节点对外服务。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以上将redis节点分为两类：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;哨兵节点（sentinel）：负责监控节点的运行情况。&lt;/li&gt;&#xA;&lt;li&gt;数据节点：即正常服务客户端请求的redis节点，有主从之分。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以上是大体的流程，这个流程需要解决以下几个问题：&lt;/p&gt;</description>
    </item>
    <item>
      <title>《数据密集型应用系统设计》第九章《一致性与共识》笔记</title>
      <link>https://www.codedump.info/zh/post/20190406-ddia-chapter09-consistency-and-consensus/</link>
      <pubDate>Thu, 18 Apr 2019 08:40:34 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190406-ddia-chapter09-consistency-and-consensus/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;一致性保证&#34;&gt;&#xA;  一致性保证&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%80%e8%87%b4%e6%80%a7%e4%bf%9d%e8%af%81&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;最终一致性（eventual consistency）：如果停止更新数据，等待一段时间（时间长度未知），则最终所有读请求将返回相同的内容。&lt;/p&gt;&#xA;&lt;p&gt;然而最终一致性是一种非常弱的一致性保证，因为无法知道何时（when）系统会收敛。而在收敛之前，读请求都可能返回任何值。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;可线性化linearizability&#34;&gt;&#xA;  可线性化（Linearizability）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8f%af%e7%ba%bf%e6%80%a7%e5%8c%96linearizability&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;可线性化（Lineariazability），也被称为原子一致性（atomic consistency）、强一致性（strong consistency），其基本的思想是让一个系统看起来好像只有一个数据副本，且所有的操作都是原子的。有了这个保证，应用程序不需要再关系系统内部有多少个副本。&lt;/p&gt;&#xA;&lt;p&gt;在一个可线性化的系统中，一旦客户端成功提交写请求，所有客户端的读请求一定能看到刚刚写入的值。这一保证让客户端认为只有一个副本，这样任何一次读取都能读到最新的值，而不是过期的数据。&lt;/p&gt;&#xA;&lt;p&gt;下图来解释在一个非线性化的系统中，可能出现什么问题。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;9-1&#34; src=&#34;https://www.codedump.info/media/imgs/20190406-ddia-chapter09-consistency-and-consensus/9-1.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 9-1 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，alice和bob同时等待2014年世界杯决赛的结果。在宣布最终比分之后，alice看到了最终的结果，然后将此结果告诉了bob，bob马上在自己的手机上刷新想看最新的结果，但是却返回了过期的数据，显示当前比赛还在进行中。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;如何实现可线性化&#34;&gt;&#xA;  如何实现可线性化？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a6%82%e4%bd%95%e5%ae%9e%e7%8e%b0%e5%8f%af%e7%ba%bf%e6%80%a7%e5%8c%96&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;前面只是简单介绍了可线性化的思想：使系统看起来只有一个数据副本。为了更好的理解可线性化，看下面的图示例子。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;9-2&#34; src=&#34;https://www.codedump.info/media/imgs/20190406-ddia-chapter09-consistency-and-consensus/9-2.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 9-2 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中，分为两种操作：针对某个值进行read和write操作。&lt;/p&gt;&#xA;&lt;p&gt;客户端A的第一次和最后一次read操作，分别返回0和1，这没有问题，因为在这两次操作中间有客户端C的write操作将数据x更新为了1。&lt;/p&gt;&#xA;&lt;p&gt;但是，在写操作还在进行的时候，如果读操作返回的值会来回的跳变，即某次读请求返回的是旧值，而某一次又返回的是新值，这对于一个可线性化系统而言是不可接受的。&lt;/p&gt;&#xA;&lt;p&gt;为此，需要加入一个约束条件，如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;9-3&#34; src=&#34;https://www.codedump.info/media/imgs/20190406-ddia-chapter09-consistency-and-consensus/9-3.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 9-3 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中，箭头表示时序依赖关系。即先有客户端A的第二次read(x)操作，再有客户端B的第二次read(x)操作。客户端A的第二次读请求返回了x的新值1，而客户端B在这次读请求之后也去读x的值，此时应该返回的也是新值1。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;即：在一个可线性化的系统中，有一个很重要的约束条件，在写操作开始和结束之间必然存在一个时间段，此时读到x的值会在旧值与新值之间跳变。但是，如果某个客户端的读请求返回了新值，那么即使这时写操作还未真正完成，后续的所有读请求也应该返回新值。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;以下的例子进一步解释可线性化的操作，除了读写之外又引入另一种操作：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;cas(x, old, new)：表示一次原子的比较-设置操作（compare-and-set，简称CAS），如果此时x的值为old，则原子设置这个值为new；否则保留原有值不变，这个操作的返回值表示这次x原有的值是否为old，即设置操作是否发生。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;9-4&#34; src=&#34;https://www.codedump.info/media/imgs/20190406-ddia-chapter09-consistency-and-consensus/9-4.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 9-4 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中的每个操作都有一个竖线，表示可能的执行时间点。可线性化要求，连接这些标记的竖线，必须总是按时间（即从左到右）向前移动，而不能向后移动。因此，一旦新值被写入或读取，所有后续的值读到的都是新值，直到被覆盖。&lt;/p&gt;&#xA;&lt;p&gt;在上图中，有一些细节需要注意：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;客户端B首先read(x)，接下来客户端D write(x,0)，然后客户端A在write(x,1)，而最终返回给客户端B的值是1（客户端A写入的值）。这个结果是可能的，这意味着数据库执行的顺序是：先处理客户端D的写请求，然后是A的写入操作，最后才是B的读请求。虽然这个顺序并不是上面请求的顺序，但是考虑到请求有网络延迟的情况，比如可能B的请求延迟很大，导致在两次写请求之后才打到数据库，因此只能返回最后A写入的值。&lt;/li&gt;&#xA;&lt;li&gt;客户端A在收到写请求的应答之前，B就收到了新的值1，这表明写入成功。这也是可能的，这并不意味着B的读请求在A的写请求之前发生，只是意味着由于网络延迟等原因导致A稍后才收到响应。&lt;/li&gt;&#xA;&lt;li&gt;客户端的最后一次读取不满足线性化。因为在此之前，A已经读到了由C进行cas(x,2,4)操作设置的新值4，B的最后一次读请求在A读取到4之后，因此B不能读到旧值2了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;注意可线性化（Lineariazability）和可串行化（Serializability）的区别：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;可串行化：可串行化是事务的隔离属性，其中每个事务可以读写多个对象。用来确保事务执行的结果与串行执行的结果完全相同，即使串行执行的顺序可能与事务实际执行顺序不同。&lt;/li&gt;&#xA;&lt;li&gt;可线性化：可线性化是读写寄存器（单个对象）的最新值保证，并不要求将操作组合到事务中，因此无法避免写倾斜等问题。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;数据库可以同时支持可串行化与可线性化，这种组合又被称为严格的可串行化或者强的单副本可串行化（strong one-copy Serializability）。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;线性化的依赖条件&#34;&gt;&#xA;  线性化的依赖条件&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ba%bf%e6%80%a7%e5%8c%96%e7%9a%84%e4%be%9d%e8%b5%96%e6%9d%a1%e4%bb%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;实现线性化系统&#34;&gt;&#xA;  实现线性化系统&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%9e%e7%8e%b0%e7%ba%bf%e6%80%a7%e5%8c%96%e7%b3%bb%e7%bb%9f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;由于线性化本质上意味着“表现的好像只有一个数据副本，其上的操作都是原子操作”。最简单的方案就是只用一个数据副本，但是这样无法容错。&lt;/p&gt;&#xA;&lt;p&gt;系统容错最常见的方法是采用复制机制，回顾一下之前的多种复制方案：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;主从复制（部分支持可线性化）：主从复制系统中，只有主节点写入数据，而从节点保存副本数据。如果从主节点或者同步更新的从节点读取数据，则可以满足线性化。&lt;/li&gt;&#xA;&lt;li&gt;共识算法（可线性化）。&lt;/li&gt;&#xA;&lt;li&gt;多主复制（不可线性化）：用于同时在多个节点上执行写入操作，并将数据异步复制到其他节点，因此可能产生写入冲突。&lt;/li&gt;&#xA;&lt;li&gt;无主复制（可能不可线性化）：对于无主节点复制的系统，依赖于具体的quorum配置，以及如何定义强一致性，可能并不能保证线性化。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;线性化与quorum&#34;&gt;&#xA;  线性化与quorum&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ba%bf%e6%80%a7%e5%8c%96%e4%b8%8equorum&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;9-6&#34; src=&#34;https://www.codedump.info/media/imgs/20190406-ddia-chapter09-consistency-and-consensus/9-6.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 9-6 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，x的初始值为0，写客户端向所有三副本（n=3，w=3）写入更新x为1。而客户端A从两个节点（r=2）读数据，其中一个节点返回1，而客户端B则从两个节点都得到了0。&lt;/p&gt;&#xA;&lt;p&gt;显然这是违反线性化要求的：客户端B在客户端A之后读取数据，但是仍然得到了旧值。&lt;/p&gt;&#xA;&lt;p&gt;总而言之，最安全的假定是类似Dynamo风格的无主复制系统无法保证线性化。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;线性化的代价&#34;&gt;&#xA;  线性化的代价&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ba%bf%e6%80%a7%e5%8c%96%e7%9a%84%e4%bb%a3%e4%bb%b7&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;cap理论&#34;&gt;&#xA;  CAP理论&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#cap%e7%90%86%e8%ae%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;在一个数据中心内部，主要存在不可靠的网络，就可能会违背线性化的风险，需要做出权衡考虑：&lt;/p&gt;</description>
    </item>
    <item>
      <title>《数据密集型应用系统设计》第八章《分布式系统的挑战》笔记</title>
      <link>https://www.codedump.info/zh/post/20190405-ddia-chapter08-the-trouble-with-distributed-system/</link>
      <pubDate>Tue, 16 Apr 2019 21:03:16 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190405-ddia-chapter08-the-trouble-with-distributed-system/</guid>
      <description>&lt;p&gt;本章描述分布式系统中可能出现的各种问题。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;故障与部分失效&#34;&gt;&#xA;  故障与部分失效&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%85%e9%9a%9c%e4%b8%8e%e9%83%a8%e5%88%86%e5%a4%b1%e6%95%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;单机上的程序，以一种确定性的方式运行：要么工作，要么出错。&lt;/p&gt;&#xA;&lt;p&gt;然而涉及到多台节点时，会出现系统的一部分正常，一部分异常的情况，称为“部分故障（partial failure）”。&lt;/p&gt;&#xA;&lt;p&gt;正是由于这种不确定性和部分失效大大提高了分布式系统的复杂性。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;不可靠的网络&#34;&gt;&#xA;  不可靠的网络&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%8d%e5%8f%af%e9%9d%a0%e7%9a%84%e7%bd%91%e7%bb%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;分布式系统中的多个节点以网络进行通信，但是网络并不保证什么时候到达以及是否一定到达。等待响应的过程中，很多事情可能出错：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;请求可能丢失。&lt;/li&gt;&#xA;&lt;li&gt;请求在某个队列里等待，无法马上发送。&lt;/li&gt;&#xA;&lt;li&gt;远程节点因为崩溃、宕机等原因已经失效。&lt;/li&gt;&#xA;&lt;li&gt;远程节点因为某些原因暂时无法响应。&lt;/li&gt;&#xA;&lt;li&gt;远程节点接收并且处理了请求，但是回复却丢失了。&lt;/li&gt;&#xA;&lt;li&gt;远程节点已经完成了请求，但是回复被延迟了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;8-1&#34; src=&#34;https://www.codedump.info/media/imgs/20190405-ddia-chapter08-the-trouble-with-distributed-system/8-1.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 8-1 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中，请求没有得到响应，但是无法区分是因为什么原因，可能有：请求丢失、远程节点关闭、响应丢失等情况。&lt;/p&gt;&#xA;&lt;p&gt;从以上可以知道，异步网络中的消息没有得到响应，但是无法判断具体的原因。&lt;/p&gt;&#xA;&lt;p&gt;处理这种问题通常采用超时机制：在等待一段时间之后，如果没有收到回复则选择放弃，并且认为响应不会到达。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;检测网络故障&#34;&gt;&#xA;  检测网络故障&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a3%80%e6%b5%8b%e7%bd%91%e7%bb%9c%e6%95%85%e9%9a%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;如果超时是检测网络故障的唯一可行方法，那么这个超时时间应该如何选择？&lt;/p&gt;&#xA;&lt;p&gt;太小：出现误判的情况。太大：意味着要很长时间才能宣布节点失效了。&lt;/p&gt;&#xA;&lt;p&gt;假设有一个虚拟的系统，网络可以保证数据报在一个最大延迟范围内：要么在时间d内交付完成，要么丢失。此外，非故障节点在时间r内完成请求的处理。此时，就可以确定成功的请求总是在2d+r时间内完成，因此这个时间是一个理想超时时间。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;同步网络和异步网络&#34;&gt;&#xA;  同步网络和异步网络&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%90%8c%e6%ad%a5%e7%bd%91%e7%bb%9c%e5%92%8c%e5%bc%82%e6%ad%a5%e7%bd%91%e7%bb%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;既然同步网络可以在规定的延迟时间内完成数据的发送，且不会丢失数据包，那么为什么分布式系统没有选择同步网络，在硬件层面就解决网络问题？&lt;/p&gt;&#xA;&lt;p&gt;原因在于，固定电话网络中的电路与TCP连接存在很大的不同：电路方式总是预留固定带宽，在电路建立之后其他人无法使用；而TCP连接的数据包则会尝试使用所有可用的网络带宽。TCP可以传送任意大小可变的数据块，会尽力在最短时间内完成数据传送。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;不可靠的时钟&#34;&gt;&#xA;  不可靠的时钟&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%8d%e5%8f%af%e9%9d%a0%e7%9a%84%e6%97%b6%e9%92%9f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;很多操作依赖时间，但是时间也是靠不住的，本节就是说这部分的内容。&lt;/p&gt;&#xA;&lt;p&gt;计算机的时钟分为两种，墙上时钟（time-of-day clock）和单调时钟（monotonic clock），但是两者在使用上是有区别的。&lt;/p&gt;&#xA;&lt;p&gt;墙上时钟根据某个日历（也称为墙上时间，wall-clock time）返回当前的日期和时间。比如Linux的系统调用clock_gettime(CLOCK_REALTIME)返回自1970年1月1日以来的秒数和毫秒数。&lt;/p&gt;&#xA;&lt;p&gt;单调时钟更适合用于测试持续时间段（时间间隔），Linux的系统调用clock_gettime(CLOCK_MONITONIC)返回的就是单调时钟。单调时钟的名字源于它们总是保证向前走而不会出现回拨现象。&lt;/p&gt;&#xA;&lt;p&gt;可以在一个时间点读取单调时钟的值，完成某项工作然后再次检查时钟，时钟之间的插值就是两次检查的时间间隔。&lt;/p&gt;&#xA;&lt;p&gt;但是，单调时钟的绝对值没有任何意义。&lt;/p&gt;&#xA;&lt;p&gt;单调时钟不需要同步，而墙上时钟需要根据NTP服务器或外部时间源做调整。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;依赖时钟的同步&#34;&gt;&#xA;  依赖时钟的同步&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%be%9d%e8%b5%96%e6%97%b6%e9%92%9f%e7%9a%84%e5%90%8c%e6%ad%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;某些操作强依赖时钟的同步，这里往往容易出现问题，这一节就是列举这些问题。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;时间戳与事件顺序&#34;&gt;&#xA;  时间戳与事件顺序&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%97%b6%e9%97%b4%e6%88%b3%e4%b8%8e%e4%ba%8b%e4%bb%b6%e9%a1%ba%e5%ba%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;一个常见的功能：跨节点的事件排序，如果高度依赖时钟计时，就存在一定的技术风险。比如，两个客户端同时写入数据库，谁先到达，哪个操作是最新的？&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;8-3&#34; src=&#34;https://www.codedump.info/media/imgs/20190405-ddia-chapter08-the-trouble-with-distributed-system/8-3.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 8-3 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，客户端A写入x=1的时间是42.004秒，而客户端B写入x+=1即x=2虽然在后面发生但是时间是42.003秒。节点2在收到这两个事件时，会根据时间戳错误的认为x=1是最新的值，丢弃了x=2的值。&lt;/p&gt;&#xA;&lt;p&gt;这种冲突解决方式称为“最后写入获胜（Last Write Win）”，但是这样保持“最新”值并丢弃其他值的做法，由于“最新”的定义强依赖于墙上时钟，则会引入偏差。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;时钟的置信区间&#34;&gt;&#xA;  时钟的置信区间&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%97%b6%e9%92%9f%e7%9a%84%e7%bd%ae%e4%bf%a1%e5%8c%ba%e9%97%b4&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;不应该把墙上时间视为一个精确的时间点，而更应该被视为带有置信区间的时间范围。比如，系统有95%的置信度认为目前时间在[10.3,10.5]秒之间。&lt;/p&gt;&#xA;&lt;p&gt;比如Google Spanner中的TrueTime API，在查询当前时间时，会得到两个值：[不早于，不晚于]分别代表误差的最大偏差范围。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;进程暂停&#34;&gt;&#xA;  进程暂停&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%bf%9b%e7%a8%8b%e6%9a%82%e5%81%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;另外一个分布式系统中危险使用时钟的例子：假设数据库每个分区只有一个主节点，只有主节点可以接收写入，那么其它节点该如何确信该节点没有被宣告失效，可以继续安全写入呢？&lt;/p&gt;</description>
    </item>
    <item>
      <title>线上存储服务崩溃问题分析记录</title>
      <link>https://www.codedump.info/zh/post/20190413-problem-fix/</link>
      <pubDate>Sat, 13 Apr 2019 12:04:59 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190413-problem-fix/</guid>
      <description>&lt;p&gt;注：本文为重新发布2017-06-17所写博客，以下为正文部分。&lt;/p&gt;&#xA;&lt;p&gt;上周我们的存储服务在某个线上项目频繁出现崩溃，花了几天的时间来查找解决该问题。在这里，将这个过程做一下记录。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;加入调试信息&#34;&gt;&#xA;  加入调试信息&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8a%a0%e5%85%a5%e8%b0%83%e8%af%95%e4%bf%a1%e6%81%af&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;由于问题在线上发生，较难重现，首先想到的是能不能加上更多的信息，在问题出现时提供更多的解决思路。&lt;/p&gt;&#xA;&lt;p&gt;首先，我们的代码里，在捕获到进程退出的信号比如SIGABRT、SIGSEGV、SIGILL等信号时，会打印出主线程的堆栈，用于帮助我们发现问题。&lt;/p&gt;&#xA;&lt;p&gt;但是在崩溃的几次情况中，打印出来的信息并不足以帮助我们解决问题，因为打印的崩溃堆栈只有主线程，猜测是不是在辅助线程中发生的异常，于是采取了两个策略：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;ulimit命令打开线上一台服务器的coredump，当再次有崩溃发生时有core文件产生，能够帮助发现问题。&lt;/li&gt;&#xA;&lt;li&gt;加入了一些代码，用于在崩溃的时候同时也打印出所有辅助线程的堆栈信息。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在做这两部分工作之后，再次发生崩溃的情况下，辅助线程的堆栈并无异常，core文件由于数据错乱也看不出来啥有用的信息来。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;复现问题&#34;&gt;&#xA;  复现问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%8d%e7%8e%b0%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;由于第一步工作受挫，接下来我的思路就在考虑怎么能在开发环境下复现这个问题。&lt;/p&gt;&#xA;&lt;p&gt;我们的存储服务在其他项目上已经上线了有一段时间了，但是并没有出现类似的问题。那么，出现问题的项目，与其他已经上线的服务有啥不同，这里也许是一个突破口。&lt;/p&gt;&#xA;&lt;p&gt;经过咨询业务方，该业务的特点是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;单条数据大：有的数据可能有几KB，而之前的项目都只有几百字节。&lt;/li&gt;&#xA;&lt;li&gt;读请求并发大，而其他业务是写请求远大于读请求。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;由于我们的存储服务兼容memcached协议，出现问题时也是以memcached协议进行访问的，所以此时我的考虑是找一个memcached压测工具，模拟前面的数据和请求特点来做模拟压测。&lt;/p&gt;&#xA;&lt;p&gt;最后选择的是twitter出品的工具twemperf，其特点是可以指定写入缓存的数据范围，同时还可以指定请求的频率。&lt;/p&gt;&#xA;&lt;p&gt;有了这个工具，首先尝试了往存储中写入大量数据量分布在4KB~10KB的数据，此时没有发现服务有core的情况出现。&#xA;然后，尝试构造大量的读请求，果然出现了core情况，重试了几次，都能稳定的重现问题了。&lt;/p&gt;&#xA;&lt;p&gt;有了能稳定重现问题的办法，总算给问题的解决打开了一个口子。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;首次尝试&#34;&gt;&#xA;  首次尝试&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%a6%96%e6%ac%a1%e5%b0%9d%e8%af%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;此时，可以正式的在代码中查找问题的原因了。&lt;/p&gt;&#xA;&lt;p&gt;来大概说明一下该存储服务的架构：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;主线程负责接收客户端请求，并且进行解析。&lt;/li&gt;&#xA;&lt;li&gt;如果是读请求，将分派给读请求处理线程，由这个线程与存储引擎库进行交互，查询数据。此时该线程数量配置为2。&lt;/li&gt;&#xA;&lt;li&gt;存储引擎库负责存储落地到磁盘的数据，类似leveldb，只不过这部分是我们自己写的存储引擎。&lt;/li&gt;&#xA;&lt;li&gt;在读线程从存储引擎中查询数据返回后，将把数据返回给主线程，由主线程负责应答客户端。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;server&#34; src=&#34;https://www.codedump.info/media/imgs/20190413-problem-fix/server.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; server &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在这几步中，第1和第4步是在主线程中进行的，第2和第3步是在读存储引擎线程中进行的。在这个过程中，如果同一个客户端有多个读请求，那么只有按照这四步在处理完毕一个读请求之后，才会继续从该客户端中取出下一个请求进行处理。&lt;/p&gt;&#xA;&lt;p&gt;在几次重现问题的过程中，发现出错的都是在第2步和第4步中，该请求客户端的数据结构某些成员出现了错乱，即要访问的指针地址已经无效了，导致的错误。&lt;/p&gt;&#xA;&lt;p&gt;指针无效，一般来说有两种可能性：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;被无效地址覆盖了这个指针。&lt;/li&gt;&#xA;&lt;li&gt;指针已经被释放的情况下继续使用。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;当时尝试把一些错误的指针地址打印出来，发现有几次都是是字符串“pcm*”的16进制表示，当时在想这个特殊的字符串到底是什么，百思不得其解的时候，一位曾经使用过mcperf工具的同事，想起来mcperf做压测时的key就是&amp;quot;mcp&amp;quot;开头的，而因为是小端方式，所以如果使用这个类型的字符串，去覆盖指针，那么就变成了&amp;quot;pcm&amp;quot;。我们很快验证了这个说法，mcperf确实是以这个为前缀来写入数据的。&lt;/p&gt;&#xA;&lt;p&gt;此时，猜测问题的原因在于：当读存储引擎线程去访问存储引擎时，某些错误导致从存储引擎读出来的数据，将客户端请求数据写乱，从而导致了崩溃。&lt;/p&gt;&#xA;&lt;p&gt;由于同时有两个读存储引擎的线程，猜测这里是不是因为多线程访问出了问题，导致的错误呢？&lt;/p&gt;&#xA;&lt;p&gt;为了验证这个问题，最简单的办法就是将线程数量改成1，重新用mcperf试了几次，确实没有再次出现问题。此时已经是周五，我们缓了一口气，打算以此修改暂时上测试环境利用周末的时间观察一下情况。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;柳暗花明&#34;&gt;&#xA;  柳暗花明&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%9f%b3%e6%9a%97%e8%8a%b1%e6%98%8e&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面提到过，猜测问题出现的原因，是多线程访问存储引擎时将某个数据写错乱了，导致其中的指针无效。&lt;/p&gt;&#xA;&lt;p&gt;clang和gcc 4.8有对应的编译参数，可以用来检测内存错误的写操作，即Address Sanitizer工具。为了兼容线上比较老的系统，之前我们的服务都是在gcc 4.1的环境下进行编译的，为了使用这个工具，首先需要折腾到满足gcc版本号大于4.8的系统上进行编译。&lt;/p&gt;&#xA;&lt;p&gt;然而，在折腾编译并且运行后，同样使用mcperf的情况下，并不能看到有内存错误覆盖写的提示，我尝试了多次都没有看到。难道是工具没有起作用？&lt;/p&gt;&#xA;&lt;p&gt;为了验证该工具的作用，我简单在出错代码的前面加入了一段肯定有问题的代码，比如：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;char a[100] = {&amp;#39;0&amp;#39;};&#xA;a[100] = &amp;#39;1&amp;#39;;&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;而在加入这段有问题的代码之后再次运行，就能看到编译器对这段代码的提示。可见，Address Sanitizer工具是起作用的。那么，前面的过程中没有看到问题，只能说明一个问题：并没有内存错误写的情况发生。&lt;/p&gt;&#xA;&lt;p&gt;此时想到另一个可能，就是有没有可能是多线程在没有保护的情况下访问了某段数据导致的问题？&lt;/p&gt;&#xA;&lt;p&gt;gcc同样也有类似的工具来检查这类错误，即Thread Sanitizer工具。然而，在给项目Makefile加入该编译参数后，程序一运行就退出了，根本看不出什么有用的信息来。&lt;/p&gt;&#xA;&lt;p&gt;此时想到的另一个工具是valgrind。大多数时候，valgrind只是用来做内存泄露检测的，其实它也可以用来做线程数据竞争的检查，使用参数 &amp;ndash;tool=helgrind 即可。使用valgrind之后，打印出来疑似有问题的代码如下：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;valgrind&#34; src=&#34;https://www.codedump.info/media/imgs/20190413-problem-fix/valgrind.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; valgrind &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;到了这里，猜测问题的原因就是由于多线程访问数据导致的错误。&lt;/p&gt;&#xA;&lt;p&gt;因为有多个处理读请求数据的线程，首先猜测的是不是某些错误的处理，导致了可以在同一时间多个线程都操作该请求客户端的数据。但是通过review代码，发现这部分处理是没有问题的，另外在访问存储引擎查询数据时，入口处也确实进行了加锁的操作。&lt;/p&gt;</description>
    </item>
    <item>
      <title>《数据密集型应用系统设计》第七章《事务》笔记</title>
      <link>https://www.codedump.info/zh/post/20190403-ddia-chapter07-transaction/</link>
      <pubDate>Wed, 03 Apr 2019 22:33:58 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190403-ddia-chapter07-transaction/</guid>
      <description>&lt;p&gt;事务提供了一种机制，应用程序可以把一组读和写操作放在一个逻辑单元里，所有在一个事务的读和写操作会被视为一个操作：要么全部失败，要么全部成功，因此应用程序不需要担心部分失败（partial failure）问题，可以安全的重试。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;深入理解事务&#34;&gt;&#xA;  深入理解事务&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%b7%b1%e5%85%a5%e7%90%86%e8%a7%a3%e4%ba%8b%e5%8a%a1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;事务提供的安全性保证即所谓的&lt;code&gt;ACID&lt;/code&gt;，它包括以下四个要求：&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;acid&#34;&gt;&#xA;  ACID&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#acid&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;原子性atomicity&#34;&gt;&#xA;  原子性（Atomicity）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8e%9f%e5%ad%90%e6%80%a7atomicity&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;A（Atomicity，原子性）：在一个事务中的所有操作，要么全部成功，要么全部失败，不存在部分成功或者部分失败的情况。在出错时中断事务，前面成功的操作都会被丢弃。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;一致性consistency&#34;&gt;&#xA;  一致性（consistency）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%80%e8%87%b4%e6%80%a7consistency&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;C（Consistency，一致性）：对数据有特定的预期状态，任何数据修改必须满足这些状态约束，比如针对一个账号，账号上的款项必须保持平衡。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;隔离性isolation&#34;&gt;&#xA;  隔离性（isolation）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%9a%94%e7%a6%bb%e6%80%a7isolation&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;I（Isolation，隔离性）：并发执行的多个事务，不会相互影响。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;7-1&#34; src=&#34;https://www.codedump.info/media/imgs/20190403-ddia-chapter07-transaction/7-1.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 7-1 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&#xA;如上图中所示，两个客户端同时增加数据库的计时器，由于没有做好隔离，导致最终的结果是43而不是正确的44。&lt;/p&gt;&#xA;&lt;p&gt;ACID语义中的隔离性意味着并发执行的多个事务相互隔离，不能交叉运行。经典的数据库教材将隔离性定义为可串行化（serializability），这就意味着可以假装它是数据库上运行的唯一事务。&lt;/p&gt;&#xA;&lt;p&gt;然而实践中，由于性能问题很少使用串行化隔离。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;持久性durability&#34;&gt;&#xA;  持久性（Durability）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%8c%81%e4%b9%85%e6%80%a7durability&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;D（Durability，持久性）：一旦事务提交，数据将被持久化存储起来。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;弱隔离级别&#34;&gt;&#xA;  弱隔离级别&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%bc%b1%e9%9a%94%e7%a6%bb%e7%ba%a7%e5%88%ab&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;可串行化的隔离会影响性能，而很多业务不愿意牺牲性能，因而倾向于使用更弱的隔离级别。&lt;/p&gt;&#xA;&lt;p&gt;以下介绍几个常见的弱隔离级别（非串行化）。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;读提交read-committed&#34;&gt;&#xA;  读提交（read committed）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%af%bb%e6%8f%90%e4%ba%a4read-committed&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;读提交是最基本的事务级别，提供两个保证：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;读数据库时，只能读到被提交成功的数据（不会读到脏数据）。&lt;/li&gt;&#xA;&lt;li&gt;写数据库时，只会覆盖已被提交成功的数据（不会脏写）。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;防止脏读&#34;&gt;&#xA;  防止脏读&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%98%b2%e6%ad%a2%e8%84%8f%e8%af%bb&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;如果一个事务被中断或者没有提交成功，而另一个事务能读取到这部分没有提交成功的数据，这就是“脏读”。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;7-4&#34; src=&#34;https://www.codedump.info/media/imgs/20190403-ddia-chapter07-transaction/7-4.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 7-4 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图，用户2仅在用户1的事务提交成功之后，才能读取到这次事务修改的新值x=3。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;防止脏写&#34;&gt;&#xA;  防止脏写&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%98%b2%e6%ad%a2%e8%84%8f%e5%86%99&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;如果先前写入的数据是尚未提交事务的一部分，而被另一个事务的写操作覆盖了，这就是脏写。通常防止脏写的办法是推迟第二个写请求，等到前面的事务操作提交。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;7-5&#34; src=&#34;https://www.codedump.info/media/imgs/20190403-ddia-chapter07-transaction/7-5.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 7-5 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图，alice和bob两人试图购买同一辆车。购买时需要两次数据库写入：网站需要更新买主为新买家，而同时发票也需要随之更新。&#xA;但是在上图中，车主被改成了bob，但是发票上面写的却是alice。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;实现读提交&#34;&gt;&#xA;  实现读提交&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%9e%e7%8e%b0%e8%af%bb%e6%8f%90%e4%ba%a4&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;实现防脏写：数据库通常使用行级锁来防止脏写，事务想修改某个对象，必须首先获得该对象的锁，直到事务结束。&lt;/p&gt;&#xA;&lt;p&gt;实现防脏读：也可以使用前面的防脏写来实现防脏读，但是这样代价太大了。一般的方式是保存这个值的两个版本，事务没有提交之前返回旧的值，提交之后才返回新的值。&lt;/p&gt;&#xA;&lt;p&gt;然而，读锁在实际中并不可行，原因在于运行时间较长的事务导致了许多只读事务等待太长的时间。&lt;/p&gt;&#xA;&lt;p&gt;因此，大部分数据库使用7-4中的方式来防止脏读：对于每个待更新的对象，数据库都会维护其旧值和当前持有锁事务将要设置的新值两个版本。在事务提交之前返回的是旧值；仅当事务提交之后，才会切换到新的值。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;快照隔离级别snapshot-isolation和重复读&#34;&gt;&#xA;  快照隔离级别（Snapshot isolation）和重复读&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%bf%ab%e7%85%a7%e9%9a%94%e7%a6%bb%e7%ba%a7%e5%88%absnapshot-isolation%e5%92%8c%e9%87%8d%e5%a4%8d%e8%af%bb&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;尽管上面的读提交已经能解决一部分问题，但是还是有一些问题不能解决的，如下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;7-6&#34; src=&#34;https://www.codedump.info/media/imgs/20190403-ddia-chapter07-transaction/7-6.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 7-6 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，alice有两个账号，但是如果alice在转账过程中去查看账户，会发现少了100美元。&lt;/p&gt;&#xA;&lt;p&gt;原因在于：alice对两个账户的两次读操作是同一个事务，而在这两次读操作之间，还有两次写操作，在这两次写操作完成之后才进行的第二次读操作，这样读出来的数据就不一致了。&lt;/p&gt;&#xA;&lt;p&gt;这种异常现象称为”不可重复读取（nonrepeatable read）“或者”读倾斜（read skew）“问题。&lt;/p&gt;</description>
    </item>
    <item>
      <title>《数据密集型应用系统设计》第六章数据分区笔记</title>
      <link>https://www.codedump.info/zh/post/20181124-ddia-chapter06-partitioning/</link>
      <pubDate>Tue, 02 Apr 2019 22:17:24 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20181124-ddia-chapter06-partitioning/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;键值数据的分区&#34;&gt;&#xA;  键值数据的分区&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%94%ae%e5%80%bc%e6%95%b0%e6%8d%ae%e7%9a%84%e5%88%86%e5%8c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;基于关键字区间的分区&#34;&gt;&#xA;  基于关键字区间的分区&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8e%e5%85%b3%e9%94%ae%e5%ad%97%e5%8c%ba%e9%97%b4%e7%9a%84%e5%88%86%e5%8c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;给每个分区分配一段连续的关键字或者关键字区间（以最小值和最大值来指示），从关键字区间的上下限可以确定哪个分区包含这些关键字。&lt;/p&gt;&#xA;&lt;p&gt;关键字的区间段不一定要均匀分布，这是因为数据本身可能就不是均匀的。比如，某些分区包含以A和B开头字母的键，而某些分区包含了T、U、V、X、Y和Z开始的单词。&lt;/p&gt;&#xA;&lt;p&gt;基于关键字的区间分区的缺点是某些访问模式会导致热点（hot spot）。比如关键字是时间戳，分区对应一个时间范围，那么可能会出现所有的写入操作都集中在同一个分区（比如当天的分区），而其他分区始终处于空闲状态。&lt;/p&gt;&#xA;&lt;p&gt;为了避免类似的问题，需要使用时间戳以外的其他内容作为关键字的第一项。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;基于关键字hash值分区&#34;&gt;&#xA;  基于关键字Hash值分区&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8e%e5%85%b3%e9%94%ae%e5%ad%97hash%e5%80%bc%e5%88%86%e5%8c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;基于关键字Hash值分区，可以解决上面提到的数据倾斜和热点问题，但是丧失了良好的区间查询特性。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;负载倾斜和热点&#34;&gt;&#xA;  负载倾斜和热点&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%b4%9f%e8%bd%bd%e5%80%be%e6%96%9c%e5%92%8c%e7%83%ad%e7%82%b9&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;基于关键字Hash值分区的办法，可以减轻数据热点问题，但是不能完全避免这类问题。一种常见的极端场景是，社交网络上某个名人有几百万的粉丝，当其发布一些热点事件时可能会引起访问风暴。此时，Hash起不到任何分流的作用。&lt;/p&gt;&#xA;&lt;p&gt;大部分系统解决不了这个问题，只能通过应用层来解决这类问题。比如某个关键字被确认是热点，一个简单的技术就是在关键字的开头或结尾处添加随机数，这样将访问分配到不同的分区上。但是随之而来的问题就是，之后的任何读取都需要额外的工作，必须将这些分区上的读取数据进行合并。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;分区与二级索引&#34;&gt;&#xA;  分区与二级索引&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%88%86%e5%8c%ba%e4%b8%8e%e4%ba%8c%e7%ba%a7%e7%b4%a2%e5%bc%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;键值类数据库的分区相对还简单一些，但是如果涉及到二级索引就变得复杂了。二级索引主要的挑战在于：它们不能规整的映射到分区中。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;基于文档分区的二级索引&#34;&gt;&#xA;  基于文档分区的二级索引&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8e%e6%96%87%e6%a1%a3%e5%88%86%e5%8c%ba%e7%9a%84%e4%ba%8c%e7%ba%a7%e7%b4%a2%e5%bc%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;figure 6-4&#34; src=&#34;https://www.codedump.info/media/imgs/20181124-ddia-chapter06-partitioning/figure6-4.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; figure 6-4 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，数据根据ID 进行分区，但是实际查询的时候，还可以按照颜色和厂商进行过滤，所以每个分区上面还创建了颜色和厂商的索引。每次往分区中写入新数据时，自动创建这些二级索引。&lt;/p&gt;&#xA;&lt;p&gt;在这种索引方式中，每个分区完全独立。各自维护自己的二级索引。因此文档索引也成为本地索引，而不是全局索引。&lt;/p&gt;&#xA;&lt;p&gt;但是读取的时候，需要查询所有的分区数据然后进行合并才返回给客户端，这种叫分散/聚集（scatter/gather）。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;基于词条的二级索引&#34;&gt;&#xA;  基于词条的二级索引&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8e%e8%af%8d%e6%9d%a1%e7%9a%84%e4%ba%8c%e7%ba%a7%e7%b4%a2%e5%bc%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;可以对所有的数据构建全局索引，而不是每个分区维护自己的本地索引。而且吧，为了避免成为瓶颈，不能将全局索引放在一个节点上，否则又破坏了分区均衡的目标，因此全局索引数据也需要进行分区。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;figure 6-5&#34; src=&#34;https://www.codedump.info/media/imgs/20181124-ddia-chapter06-partitioning/figure6-5.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; figure 6-5 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，所有数据分区中的颜色进行了分区，比如从a到r开始的颜色放在了分区0中，从s到z的颜色放在了分区1中，类似的，厂商索引也被分区。这种索引方式成为词条分区（term-partitioned）。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;优点：读取高效，不需要采用scatter/gather方式对所有分区都进行查询；&lt;/li&gt;&#xA;&lt;li&gt;缺点：写入速度慢并且非常复杂，主要是因为单个文档需要更新的时候，里面可能涉及多个二级索引，而二级索引又放在不同的节点上。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在实践中，对全局二级索引数据的更新一般都是异步进行的。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;分区再平衡rebalancing-partitions&#34;&gt;&#xA;  分区再平衡（Rebalancing Partitions）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%88%86%e5%8c%ba%e5%86%8d%e5%b9%b3%e8%a1%a1rebalancing-partitions&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;实际中，数据会发生某些变化，这时候需要将数据和请求从一个节点转移到另一个节点。这样的一个迁移负载的过程称为再平衡（rebalance）。&lt;/p&gt;&#xA;&lt;p&gt;分区再平衡至少需要满足：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;平衡之后，负载、数据存储、读写请求能够在集群范围内更均匀分布。&lt;/li&gt;&#xA;&lt;li&gt;再平衡过程中，数据库可以继续处理客户端的读写请求。&lt;/li&gt;&#xA;&lt;li&gt;避免不必要的负载迁移。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;下面谈各种再平衡策略。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;为什么不能用取模&#34;&gt;&#xA;  为什么不能用取模？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%ba%e4%bb%80%e4%b9%88%e4%b8%8d%e8%83%bd%e7%94%a8%e5%8f%96%e6%a8%a1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;对节点数进行取模的方式，最大的问题在于如果节点的数据发生了变化，会导致很多关键字从现有的节点迁移到另一个节点。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;固定数量的分区&#34;&gt;&#xA;  固定数量的分区&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9b%ba%e5%ae%9a%e6%95%b0%e9%87%8f%e7%9a%84%e5%88%86%e5%8c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;创建远超实际节点数的分区数，然后给每个节点分配多个分区。比如只有10个节点的集群，划分了1000个逻辑分区。&lt;/p&gt;&#xA;&lt;p&gt;如果集群中添加了一个新节点，该新节点就可以从每个现有节点上匀走几个分区，直到分区再次达到全局平衡。&lt;/p&gt;&#xA;&lt;p&gt;这个方式的优点在于，关键字与逻辑分区的映射关系一开始就固定下来了，节点数量的变更只是改变了逻辑分区分布在哪些节点上。节点间迁移分区数据需要时间，这个过程中，就分区依然可以处理客户端的读写请求。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;figure 6-6&#34; src=&#34;https://www.codedump.info/media/imgs/20181124-ddia-chapter06-partitioning/figure6-6.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; figure 6-6 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;动态分区&#34;&gt;&#xA;  动态分区&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8a%a8%e6%80%81%e5%88%86%e5%8c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;按节点比例分区&#34;&gt;&#xA;  按节点比例分区&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%8c%89%e8%8a%82%e7%82%b9%e6%af%94%e4%be%8b%e5%88%86%e5%8c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;自动与手动再平衡操作&#34;&gt;&#xA;  自动与手动再平衡操作&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%87%aa%e5%8a%a8%e4%b8%8e%e6%89%8b%e5%8a%a8%e5%86%8d%e5%b9%b3%e8%a1%a1%e6%93%8d%e4%bd%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;请求路由&#34;&gt;&#xA;  请求路由&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%af%b7%e6%b1%82%e8%b7%af%e7%94%b1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;当客户端需要发起请求时，如果知道应该连接哪个节点？如果发生了分区再平衡，分区与节点的对应关系发生了变化。&lt;/p&gt;</description>
    </item>
    <item>
      <title>《数据密集型应用系统设计》第五章数据复制笔记</title>
      <link>https://www.codedump.info/zh/post/20181118-ddia-chapter05-replication/</link>
      <pubDate>Mon, 01 Apr 2019 18:19:22 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20181118-ddia-chapter05-replication/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;主从复制&#34;&gt;&#xA;  主从复制&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%bb%e4%bb%8e%e5%a4%8d%e5%88%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;集群中有一个主节点，写操作都必须经过主节点完成，读操作主从节点都可以处理。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;figure 5-1&#34; src=&#34;https://www.codedump.info/media/imgs/20181118-ddia-chapter05-replication/figure5-1.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; figure 5-1 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;同步复制和异步复制&#34;&gt;&#xA;  同步复制和异步复制&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%90%8c%e6%ad%a5%e5%a4%8d%e5%88%b6%e5%92%8c%e5%bc%82%e6%ad%a5%e5%a4%8d%e5%88%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;同步复制&#34;&gt;&#xA;  同步复制&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%90%8c%e6%ad%a5%e5%a4%8d%e5%88%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;数据在副本上落盘才返回。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;优点：保证在副本上的数据是最新数据。&lt;/li&gt;&#xA;&lt;li&gt;缺点：延迟高，响应慢。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;异步复制&#34;&gt;&#xA;  异步复制&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%bc%82%e6%ad%a5%e5%a4%8d%e5%88%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;数据不保证在副本上落盘。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;优点：延迟低&lt;/li&gt;&#xA;&lt;li&gt;不能保证在副本上的数据最新。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;不能把集群中所有节点设置为同步节点，因为这样的话任何一个节点的停滞都会导致整个集群的不可用。像Paxos、Raft算法，都要求集群中大多数节点返回就可以了。部分同步、部分异步的集群配置成为半同步（semi-sync）的集群配置。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;新增新的从节点&#34;&gt;&#xA;  新增新的从节点&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%96%b0%e5%a2%9e%e6%96%b0%e7%9a%84%e4%bb%8e%e8%8a%82%e7%82%b9&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;主节点生成快照数据&lt;/li&gt;&#xA;&lt;li&gt;主节点将快照数据发送到从节点。&lt;/li&gt;&#xA;&lt;li&gt;从节点请求主节点快照数据之后的数据。&lt;/li&gt;&#xA;&lt;li&gt;重复上面三步直到从节点追上主节点的进度。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;处理节点失效&#34;&gt;&#xA;  处理节点失效&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%84%e7%90%86%e8%8a%82%e7%82%b9%e5%a4%b1%e6%95%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;从节点失效&#34;&gt;&#xA;  从节点失效&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%bb%8e%e8%8a%82%e7%82%b9%e5%a4%b1%e6%95%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;从节点崩溃恢复之后按照前面新增新的从节点的步骤来追上主节点的数据进度。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;主节点失效&#34;&gt;&#xA;  主节点失效&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%bb%e8%8a%82%e7%82%b9%e5%a4%b1%e6%95%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;主节点失败时需要提升某个从节点为新的主节点，同时需要通知客户端新的主节点。&lt;/p&gt;&#xA;&lt;p&gt;自动切换主节点的步骤通常如下：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;确认主节点失效。大部分系统采用基于超时的机制，主从节点直接发送心跳消息，主节点在某个时间内都没有响应，则认为主节点已经失效。&lt;/li&gt;&#xA;&lt;li&gt;选举新的主节点。通过选举的方式（超过半数以上的从节点达成共识）来选举新的主节点，新的主节点是与旧的主节点数据差异最小的一个，最小化数据丢失的风险。&lt;/li&gt;&#xA;&lt;li&gt;重新配置使新的主节点上线。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;除了以上步骤之外，还有以下问题需要考虑：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;如果使用异步复制机制，而且在失效之前，新的主节点并没有收到旧的主节点的所有数据，那么在旧的主节点重新上线之后，未完成复制的数据将被丢弃。&lt;/li&gt;&#xA;&lt;li&gt;&lt;/li&gt;&#xA;&lt;li&gt;可能会出现集群同时存在两个主节点的情况，也就是所谓的脑裂（split brain）现象，此时两个主节点都认为自己是主节点并且都能接收客户端的写数据请求，会导致数据丢失或者破坏。&lt;/li&gt;&#xA;&lt;li&gt;如何设置合理的超时时间来判断主节点失效？如果太大意味着总体恢复时间长，如果太小意味着某些情况下可能主节点并未失效但是被误判为失效了，比如网络峰值导致延迟高等原因，这样会导致很多不必要的主节点切换。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;上述的问题，包括节点失效、网络不可靠、副本一致性、持久性、可用性与延迟之间的各种细微的权衡，正是分布式系统核心的基本问题。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;复制日志的实现&#34;&gt;&#xA;  复制日志的实现&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%8d%e5%88%b6%e6%97%a5%e5%bf%97%e7%9a%84%e5%ae%9e%e7%8e%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;基于语句的复制&#34;&gt;&#xA;  基于语句的复制&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8e%e8%af%ad%e5%8f%a5%e7%9a%84%e5%a4%8d%e5%88%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;主节点记录所执行的每个写请求并将该语句做为日志发送给从节点。但是有些场景并不适合这么做，比如：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;调用任何非确定函数的语句，比如NOW()获得当前时间，RAND()返回一个随机数。&lt;/li&gt;&#xA;&lt;li&gt;语句中使用了自增列，或者依赖于当前数据库的数据。&lt;/li&gt;&#xA;&lt;li&gt;有副作用的语句，在每个副本上面执行的效果不一样。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;基于预写日志wal&#34;&gt;&#xA;  基于预写日志(WAL)&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8e%e9%a2%84%e5%86%99%e6%97%a5%e5%bf%97wal&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;将对数据库的操作写入日志，传送到从节点上然后执行，得到与主节点相同的数据副本。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;基于行的逻辑日志复制&#34;&gt;&#xA;  基于行的逻辑日志复制&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8e%e8%a1%8c%e7%9a%84%e9%80%bb%e8%be%91%e6%97%a5%e5%bf%97%e5%a4%8d%e5%88%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;所谓的逻辑日志，就是复制与存储引擎采用不同的日志格式，这样复制与存储逻辑剥离，这种日志称为逻辑日志，与物理存储引擎的数据区分开。由于逻辑日志与存储引擎逻辑上解耦，因此可以更好的向后兼容，也更好的能被外部程序解析。&lt;/p&gt;</description>
    </item>
    <item>
      <title>如何阅读一份源代码？</title>
      <link>https://www.codedump.info/zh/post/20190324-how-to-read-code/</link>
      <pubDate>Sun, 24 Mar 2019 09:41:21 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190324-how-to-read-code/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;我在本文的基础上做了一些补充和改进，见&lt;a href=&#34;https://www.codedump.info/post/20200605-how-to-read-code-v2020/&#34;&gt;《如何阅读一份源代码？（2020年版）》&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;阅读源代码的能力算是程序员的一种底层基础能力之一，这个能力之所以重要，原因在于：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;不可避免的需要阅读或者接手他人的项目。比如调研一个开源项目，比如接手一个其他人的项目。&lt;/li&gt;&#xA;&lt;li&gt;阅读优秀的项目源码是学习他人优秀经验的重要途径之一，这一点我自己深有体会。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;然而，读代码比写代码还是更难一些，原因在于“&lt;a href=&#34;https://www.zhihu.com/question/21820752/answer/19427754&#34;&gt;写代码是在表达自己,读代码是在理解别人&lt;/a&gt;”。因为面对的项目多，项目的作者有各自的风格，理解起来需要花费不少的精力。&lt;/p&gt;&#xA;&lt;p&gt;我从业这些年泛读、精读过的项目源码不算少了，陆陆续续的也写了一些代码分析的文章，本文中就简单总结一下我的方法。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;先跑起来&#34;&gt;&#xA;  先跑起来&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%85%88%e8%b7%91%e8%b5%b7%e6%9d%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;开始阅读一份项目源码的第一步，是先让这个项目能够通过你自己编译通过并且顺利跑起来。这一点尤其重要。&lt;/p&gt;&#xA;&lt;p&gt;有的项目比较复杂，依赖的组件多，搭建起一个调试环境并不容易，所以并不见得是所有项目都能顺利的跑起来。如果能自己编译跑起来，那么后面讲到的情景分析、加上调试代码、调试等等才有展开的基础。&lt;/p&gt;&#xA;&lt;p&gt;就我的经验而言，一个项目代码，是否能顺利的搭建调试环境，效率大不一样。&lt;/p&gt;&#xA;&lt;p&gt;跑起来之后，又要尽量的精简自己的环境，减少调试过程中的干扰信息。比如，Nginx使用多进程的方式处理请求，为了调试跟踪Nginx的行为，我经常把worker数量设置为1个，这样调试的时候就知道待跟踪的是哪个进程了。&lt;/p&gt;&#xA;&lt;p&gt;总而言之，跑起来之后的调试效率能提升很多，而在跑起来的前提之下又要尽量精简环境。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;调试手段&#34;&gt;&#xA;  调试手段&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%b0%83%e8%af%95%e6%89%8b%e6%ae%b5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;调试手段，大体分为以下两种：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;加调试语句。为了做到这一点，你需要先了解项目如何加调试日志，可能需要修改项目的日志级别支持输出一些在调试级别的日志，等等。&lt;/li&gt;&#xA;&lt;li&gt;断点调试。并不是所有项目代码，跑起来之后都自带调试信息能够断点调试的。所以在自己的调试环境里需要先确定这一点。比如一些C相关的项目，基本都是&amp;quot;./configure &amp;amp; make&amp;quot;来编译，但是makefile中的编译flags使用了O2之类的优化选项，此时需要自己先手动修改成&amp;quot;-O0 -g&amp;quot;，即编译生成的二进制中不优化且带上调试信息。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;总之，在能够搭建自己的调试环境之后，还需要想办法确定一下如何加上调试日志以及断点调试。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;使用顺手的工具&#34;&gt;&#xA;  使用顺手的工具&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%bd%bf%e7%94%a8%e9%a1%ba%e6%89%8b%e7%9a%84%e5%b7%a5%e5%85%b7&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;好的工具会让你事半功倍，这一点应该很多人都同意。&lt;/p&gt;&#xA;&lt;p&gt;我阅读Go代码的时候，喜欢使用IDEA，这个IDE工具可以完美的做到以下几点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;符号的定位、跳转、查找符号被引用的地方。&lt;/li&gt;&#xA;&lt;li&gt;左边能够展开一个源码文件中的所有符号。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;反之，很多人推崇的VSCode，我几次尝试使用用来阅读Go和C类代码，都觉得不够顺手，查找符号能力不行、也没有地方可以看到一个文件中出现的符号。&lt;/p&gt;&#xA;&lt;p&gt;C\C++类的代码，在尝试各种工具之后，还是使用Vim+Ctags+Cscope来写C、C++代码。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;情景分析&#34;&gt;&#xA;  情景分析&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%83%85%e6%99%af%e5%88%86%e6%9e%90&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;假如有了前面的基础，已经能够让项目顺利在自己的调试环境跑起来了，那么就可以对项目代码进行情景分析了。&lt;/p&gt;&#xA;&lt;p&gt;所谓的“情景分析”，我的理解就是自己构造一些情景，然后通过加断点、调试语句等分析在这些场景下的行为。&lt;/p&gt;&#xA;&lt;p&gt;以我自己为例，在写&lt;a href=&#34;https://book.douban.com/subject/27108476/&#34;&gt;《Lua设计与实现》&lt;/a&gt;时，讲解到Lua虚拟机指令的解释和执行过程中，需要针对每个指令做分析，此时用的就是情景分析的方法。我会模拟出来使用该指令的Lua脚本代码，然后在程序里断点调试这些场景下的行为。&lt;/p&gt;&#xA;&lt;p&gt;我惯用的做法，是在某个重要的入口函数上面加上断点，然后构造触发场景的调试代码，当代码在断点处停下，通过查看堆栈、变量值等等来观察代码的行为。&lt;/p&gt;&#xA;&lt;p&gt;例如，Lua解释器代码中中，生成Opcode最终都会调用函数luaK_code，那么我就在这个函数上面加上断点，然后构造我想要调试的场景，只要在断点处中断，我通过函数堆栈就能看到完整的调用流程：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(lldb) bt&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;* &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;thread&lt;/span&gt; &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;1: tid = 0xb1dd2, 0x00000001000071b0 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;luaK_code, queue = &lt;span style=&#34;&#34;&gt;&amp;#39;&lt;/span&gt;com.apple.main-&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;thread&lt;/span&gt;&lt;span style=&#34;&#34;&gt;&amp;#39;&lt;/span&gt;, stop reason = breakpoint 1.1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;* frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;0: 0x00000001000071b0 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;luaK_code&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;1: 0x000000010000753e lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;discharge2reg + 238&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;2: 0x000000010000588f lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;exp2reg + 31&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;3: 0x000000010000f15b lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;statement + 3131&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;4: 0x000000010000e0b6 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;luaY_parser + 182&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;5: 0x0000000100009de9 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;f_parser + 89&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;6: 0x0000000100008ba5 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;luaD_rawrunprotected + 85&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;7: 0x0000000100009bf4 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;luaD_pcall + 68&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;8: 0x0000000100009d65 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;luaD_protectedparser + 69&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;9: 0x00000001000047e1 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;lua_load + 65&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;10: 0x0000000100018071 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;luaL_loadfile + 433&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;11: 0x0000000100000eb9 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;pmain + 1545&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;12: 0x00000001000090cd lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;luaD_precall + 589&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;13: 0x00000001000098c1 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;luaD_call + 81&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;14: 0x0000000100008ba5 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;luaD_rawrunprotected + 85&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;15: 0x0000000100009bf4 lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;luaD_pcall + 68&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;16: 0x00000001000046fb lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;lua_cpcall + 43&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;17: 0x00000001000007af lua&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;main + 63&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;frame &lt;span style=&#34;&#34;&gt;#&lt;/span&gt;18: 0x00007fff6468708d libdyld.dylib&lt;span style=&#34;&#34;&gt;`&lt;/span&gt;start + 1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;情景分析的好处在于：不会在一个项目中大海捞针似的查找，而是能够把问题缩小到一个范围内展开来理解。&lt;/p&gt;</description>
    </item>
    <item>
      <title>TCP协议笔记</title>
      <link>https://www.codedump.info/zh/post/20190227-tcp/</link>
      <pubDate>Wed, 27 Feb 2019 21:01:43 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190227-tcp/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;tcpip&#34; src=&#34;https://www.codedump.info/media/imgs/20190227-tcp/tcpip.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; tcpip &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;应用层：通常也称为“七层”，这是大部分服务器工作的层次，如HTTP 服务器等，位于应用层上的信息分组成为报文（message）。识别不同应用层的信息是通过端口号，即不同的端口号提供不同的服务。&lt;/li&gt;&#xA;&lt;li&gt;传输层：通常也称为“四层”，TCP、UDP协议工作在这一层，位于这一层的分组称为报文段（segment）。&lt;/li&gt;&#xA;&lt;li&gt;网络层：通常也称为“三层”，负责将数据包（datagram）从一台主机移动到另一台主机。&lt;/li&gt;&#xA;&lt;li&gt;接口层：通常也称为“二层”，链路层分组称为帧（frame）。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;tcp协议格式&#34;&gt;&#xA;  TCP协议格式&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#tcp%e5%8d%8f%e8%ae%ae%e6%a0%bc%e5%bc%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;tcp-header&#34; src=&#34;https://www.codedump.info/media/imgs/20190227-tcp/tcp-header.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; tcp-header &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;端口号：tcp使用端口号来标记目标和源端口，tcp头中并没有ip地址信息，根据前面的tcp/ip模型，ip地址这是三层做的事情。&lt;/li&gt;&#xA;&lt;li&gt;序号（Sequence Number）：用于对tcp字节流进行编号，以解决网络包乱序问题。&lt;/li&gt;&#xA;&lt;li&gt;确认号（Acknowledgement Number）：用于确认接收到的报文段序号，用来解决丢包问题。&lt;/li&gt;&#xA;&lt;li&gt;窗口：用于通知对端接收窗口大小，用于解决流控问题。&lt;/li&gt;&#xA;&lt;li&gt;TCP标志位，用于控制TCP协议状态机的，包括以下几个：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;ACK：只有这个标志位置位时，前面的确认号字段才有效。&lt;/li&gt;&#xA;&lt;li&gt;SYN：在连接建立时用来同步序号。当 SYN=1，ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接，则响应报文中 SYN=1，ACK=1。&lt;/li&gt;&#xA;&lt;li&gt;FIN：用来释放一个连接，当 FIN=1 时，表示此报文段的发送方的数据已发送完毕，并要求释放连接。&lt;/li&gt;&#xA;&lt;li&gt;RST：重置连接，比如向一个不存在监听服务的端口发请求时，就会收到RST包。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;TCP选项：这部分可选，不属于TCP头部必然存在的部分。&#xA;&lt;ul&gt;&#xA;&lt;li&gt;MSS（Maximum Segment Size，最大报文长度）：MSS选项用于在TCP连接建立时，收发双方协商通信时每一个报文段所能承载的最大数据长度。为了达到最佳的传输效能，TCP协议在建立连接的时候通常要协商双方的MSS值，这个值TCP协议在实现的时候往往用MTU值代替（需要减去IP数据包包头的大小20Bytes和TCP数据段的包头20Bytes）所以一般MSS值1460。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;tcp连接的建立和终止&#34;&gt;&#xA;  TCP连接的建立和终止&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#tcp%e8%bf%9e%e6%8e%a5%e7%9a%84%e5%bb%ba%e7%ab%8b%e5%92%8c%e7%bb%88%e6%ad%a2&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;连接建立&#34;&gt;&#xA;  连接建立&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%bf%9e%e6%8e%a5%e5%bb%ba%e7%ab%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;tcp-connect&#34; src=&#34;https://www.codedump.info/media/imgs/20190227-tcp/tcp-connect.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; tcp-connect &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;以上图说明建立TCP连接的过程，其中左边的A为客户端，右边的B为服务器：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;B调用listen系统命令，进入监听状态，等待客户端的连接。&lt;/li&gt;&#xA;&lt;li&gt;A向B发送连接请求报文，其中TCP标志位里SYN=1，ACK=0，选择一个初始的序号x。&lt;/li&gt;&#xA;&lt;li&gt;B收到请求报文，向 A 发送连接确认报文，SYN=1，ACK=1，确认号为 x+1，同时也选择一个初始的序号 y。&lt;/li&gt;&#xA;&lt;li&gt;A 收到 B 的连接确认报文后，还要向 B 发出确认，确认号为 y+1，序号为 x+1。&lt;/li&gt;&#xA;&lt;li&gt;B 收到 A 的确认后，连接建立。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以上就是TCP建立连接的三次握手过程，以上流程还需要补充的是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;对于建链接的3次握手，主要是要初始化Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的Sequence Number（缩写为ISN：Inital Sequence Number）——所以叫SYN，全称Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号，以保证应用层接收到的数据不会因为网络上的传输的问题而乱序（TCP会用这个序号来拼接数据）。&lt;/li&gt;&#xA;&lt;li&gt;第三次握手是为了防止失效的连接请求到达服务器，让服务器错误打开连接。客户端发送的连接请求如果在网络中滞留，那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后，就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器，如果不进行三次握手，那么服务器就会打开两个连接。如果有第三次握手，客户端会忽略服务器之后发送的对滞留连接请求的连接确认，不进行第三次握手，因此就不会再次打开连接。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;backlog参数与syn-flood攻击&#34;&gt;&#xA;  backlog参数与SYN Flood攻击&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#backlog%e5%8f%82%e6%95%b0%e4%b8%8esyn-flood%e6%94%bb%e5%87%bb&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;listen系统调用中，会传入一个backlog参数，man文档对其的解释是：&lt;/p&gt;</description>
    </item>
    <item>
      <title>Leveldb代码阅读笔记</title>
      <link>https://www.codedump.info/zh/post/20190215-leveldb/</link>
      <pubDate>Fri, 15 Feb 2019 08:52:47 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190215-leveldb/</guid>
      <description>&lt;p&gt;本文基于leveldb 1.9.0代码。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;整体架构&#34;&gt;&#xA;  整体架构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b4%e4%bd%93%e6%9e%b6%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;leveldb&#34; src=&#34;https://www.codedump.info/media/imgs/20190215-leveldb/leveldb.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; leveldb &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图，leveldb的数据存储在内存以及磁盘上，其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;memtable：存储在内存中的数据，使用skiplist实现。&lt;/li&gt;&#xA;&lt;li&gt;immutable memtable：与memtable一样，只不过这个memtable不能再进行修改，会将其中的数据落盘到level 0的sstable中。&lt;/li&gt;&#xA;&lt;li&gt;多层sstable：leveldb使用多个层次来存储sstable文件，这些文件分布在磁盘上，这些文件都是根据键值有序排列的，其中0级的sstable的键值可能会重叠，而level 1及以上的sstable文件不会重叠。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在上面这个存储层次中，越靠上的数据越新，即同一个键值如果同时存在于memtable和immutable memtable中，则以memtable中的为准。&lt;/p&gt;&#xA;&lt;p&gt;另外，图中还使用箭头来表示了合并数据的走向，即：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;memtable -&amp;gt; immutable memtable -&amp;gt; level 0 sstable -&amp;gt; level 1 sstable -&amp;gt; ... -&amp;gt; level N sstable&lt;span style=&#34;&#34;&gt;。&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;以下将针对这几部分展开讨论。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;log文件&#34;&gt;&#xA;  Log文件&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#log%e6%96%87%e4%bb%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;写入数据的时候，最开始会写入到log文件中，由于是顺序写入文件，所以写入速度很快，可以马上返回。&lt;/p&gt;&#xA;&lt;p&gt;来看Log文件的结构：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一个Log文件由多个Block组成，每个Block大小为32KB。&lt;/li&gt;&#xA;&lt;li&gt;一个Block内部又有多个Record组成，Record分为四种类型：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Full：一个Record占满了整个Block存储空间。&lt;/li&gt;&#xA;&lt;li&gt;First：一个Block的第一个Record。&lt;/li&gt;&#xA;&lt;li&gt;Last：一个Block的最后一个Record。&lt;/li&gt;&#xA;&lt;li&gt;Middle：其余的都是Middle类型的Record。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;Record的结构如下：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Header部分&#xA;&lt;ul&gt;&#xA;&lt;li&gt;32位长度的CRC Checksum：存储这个Record的数据校验值，用于检测Record合法性。&lt;/li&gt;&#xA;&lt;li&gt;16位长度的Length：存储数据部分长度。&lt;/li&gt;&#xA;&lt;li&gt;8位长度的Type：存储Record类型，就是上面说的四种类型。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;数据部分&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;log-file&#34; src=&#34;https://www.codedump.info/media/imgs/20190215-leveldb/log-file.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; log-file &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;memtable&#34;&gt;&#xA;  memtable&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#memtable&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;memtable用于存储在内存中还未落盘到sstable中的数据，这部分使用跳表（skiplist）做为底层的数据结构，这里先简单描述一下跳表的工作原理。&lt;/p&gt;&#xA;&lt;p&gt;如果数据存放在一个普通的有序链表中，那么查找数据的时间复杂度就是O(n)。跳表的设计思想在于：链表中的每个元素，都有多个层次，查找某一个元素时，遍历该链表的时候，根据层次来跳过（skip）中间某些明显不满足需求的元素，以达到加快查找速度的目的，如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;skiplist&#34; src=&#34;https://www.codedump.info/media/imgs/20190215-leveldb/skiplist.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; skiplist &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在以上这个跳表中，查找元素6的流程，大体如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;构建一个每个链表元素最多有5个元素的跳表。&lt;/li&gt;&#xA;&lt;li&gt;由于6大于链表的第一个元素1，因此如果存在必然在1之后的元素中，因此进入元素1的指针数组中，从上往下查找元素4：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;第一层：指向的指针为Nil空指针，不满足需求，继续往下查找；&lt;/li&gt;&#xA;&lt;li&gt;第二层：指向的指针保存的数据为4，小于待查找的元素4，因此如果元素6存在也必然在4之后，因此指针跳转到元素4所在的位置，继续从上往下开始查找。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;到了元素4所在的指针数组，开始从上往下继续查找：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;第一层：指向的指针保存的数据为6，查找完毕。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从上面的分析过程中可以看到：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;跳表是一种以牺牲更多的存储空间换取查找速度，即“空间换时间”的数据结构。&lt;/li&gt;&#xA;&lt;li&gt;跳表的每一层也都是一个有序链表。&lt;/li&gt;&#xA;&lt;li&gt;如果一个元素出现在第i层的链表中，那么也必然会在第i层以下的链表中出现。&lt;/li&gt;&#xA;&lt;li&gt;链表的每个节点中，垂直方向的数组存储的数据都是一样的，水平方向的指针指向链表的下一个元素。&lt;/li&gt;&#xA;&lt;li&gt;最底层的链表包含所有元素，也就是说，在最底层数据结构退化为一个普通的有序链表。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;sstable文件&#34;&gt;&#xA;  sstable文件&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#sstable%e6%96%87%e4%bb%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;大体结构&#34;&gt;&#xA;  大体结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%a7%e4%bd%93%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;首先来看sstable文件的整体结构，如下图：&lt;/p&gt;</description>
    </item>
    <item>
      <title>Nginx源码阅读笔记-内存池的设计</title>
      <link>https://www.codedump.info/zh/post/20190214-nginx-memory-pool/</link>
      <pubDate>Thu, 14 Feb 2019 14:48:24 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190214-nginx-memory-pool/</guid>
      <description>&lt;p&gt;nginx中所有请求都单独对应一个内存池，在这个请求的过程中，所有涉及到内存分配的地方，都到该请求相关的内存池中处理，而中间不会去释放回收内存，内存池的生命周期与请求一样，请求完毕则直接回收内存。这样的好处在于：统一分配和统一释放，降低了内存泄露问题的出现。&lt;/p&gt;&#xA;&lt;p&gt;nginx的内存池设计的比较简单了，一个内存池中分为两个部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;超过max大小的内存分配，走大块内存分配，这部分内存管理由ngx_pool_large_t结构体负责。&lt;/li&gt;&#xA;&lt;li&gt;否则就是在ngx_pool_t遍历符合要求的ngx_pool_t结构体，找到符合要求大小的pool直接返回，否则就申请一块新的内存pool。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;ngx_pool_data_t&#34;&gt;&#xA;  ngx_pool_data_t&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#ngx_pool_data_t&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;先来看结构体ngx_pool_data_t，它存储每个ngx_pool_t结构体的meta元数据：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;u_char *last：指向分配空间的可用空间。&lt;/li&gt;&#xA;&lt;li&gt;u_char *end：指向分配空间的最后位置。&lt;/li&gt;&#xA;&lt;li&gt;ngx_pool_t *next：指向下一个ngx_pool_t指针。&lt;/li&gt;&#xA;&lt;li&gt;ngx_uint_t failed：存储本ngx_pool_t结构体分配失败次数。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;ngx_pool_data_t&#34; src=&#34;https://www.codedump.info/media/imgs/20190214-nginx-memory-pool/ngx_pool_data_t.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; ngx_pool_data_t &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;failed成员的引入是为了避免某个pool虽然还有可用的空间，但是由于空间很小了所以经常性的分配空间失败，当累计失败的次数达到某个阈值时，下一次再次查找内存就直接跳过这个pool，而去寻找内存池链表中的下一个pool。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;ngx_pool_large_t&#34;&gt;&#xA;  ngx_pool_large_t&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#ngx_pool_large_t&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;ngx_pool_large_t结构体用于保存大内存块，这一块就比较简单粗暴了，直接分配一块大内存来使用。另外，多个大内存块之间也是以链表形式来组织数据。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 管理超大空间的结构体&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; ngx_pool_large_s {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 指向下一个指针&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_pool_large_t&lt;/span&gt;     *next;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 直接指向内存区域的指针&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt;                 *alloc;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 class=&#34;heading&#34; id=&#34;ngx_pool_t&#34;&gt;&#xA;  ngx_pool_t&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#ngx_pool_t&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;再来看ngx_pool_t结构体，该数据结构用于表示一个内存池，内存池内部以链表形式来组织数据。如下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;ngx_pool_t&#34; src=&#34;https://www.codedump.info/media/imgs/20190214-nginx-memory-pool/ngx_pool_t.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; ngx_pool_t &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;需要说明的是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;内存池内部以链表形式组织起来，完成这个工作的就是前面的ngx_pool_data_t的next成员。&lt;/li&gt;&#xA;&lt;li&gt;current指针，用于表示当前该内存池在使用的pool指针。除了内存池链表的头结点之外，内存池链表其他节点的该指针无效。之所以需要这个指针，就是前面提到的，在某个内存池多次失效的情况下，下一次直接跳过该内存池查找空间，current指针保存当前在内存池链表的哪一个内存池上面查找空间。&lt;/li&gt;&#xA;&lt;li&gt;large指针，指向ngx_pool_large_t结构体，管理大块内存。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;有了以上数据结构的了解，从内存池分配内存的流程就很简单了：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;memory-pool&#34; src=&#34;https://www.codedump.info/media/imgs/20190214-nginx-memory-pool/memory-pool.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; memory-pool &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Nginx源码阅读笔记-处理HTTP请求</title>
      <link>https://www.codedump.info/zh/post/20190213-nginx-process-http-request/</link>
      <pubDate>Wed, 13 Feb 2019 09:09:19 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190213-nginx-process-http-request/</guid>
      <description>&lt;p&gt;前面分析了nginx&lt;a href=&#34;https://www.codedump.info/post/20190131-nginx-read-http-request/&#34;&gt;如何读取一个HTTP请求&lt;/a&gt;、&lt;a href=&#34;https://www.codedump.info/post/20190212-nginx-http-config/&#34;&gt;如何查询到HTTP对应的配置&lt;/a&gt;，本节分析如何处理HTTP请求。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;处理http请求的11个阶段&#34;&gt;&#xA;  处理HTTP请求的11个阶段&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%84%e7%90%86http%e8%af%b7%e6%b1%82%e7%9a%8411%e4%b8%aa%e9%98%b6%e6%ae%b5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;nginx将处理HTTP请求划分为了11个阶段，原因在于nginx是一个重度模块化的系统，划分为不同阶段以后，不同的模块可以根据自己的需求在相应的模块中添加自己的处理函数。&lt;/p&gt;&#xA;&lt;p&gt;简单看看这11个模块的定义：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;typedef&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;enum&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 在接收到完整的HTTP头部后处理的HTTP阶段&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  NGX_HTTP_POST_READ_PHASE = 0,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 在将请求的URI与location表达式匹配前，修改请求的&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// URI（所谓的重定向）是一个独立的HTTP阶段&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  NGX_HTTP_SERVER_REWRITE_PHASE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 根据请求的URL寻找匹配的location表达式，这个阶段&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 只能由ngx_http_core_module模块实现，不建议其他HTTP&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 模块模块重新定义这一阶段的行为&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  NGX_HTTP_FIND_CONFIG_PHASE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 在NGX_HTTP_FIND_CONFIG_PHASE阶段寻找到匹配的location&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 之后再修改请求的URI&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  NGX_HTTP_REWRITE_PHASE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 这一阶段用于在rewrite重写URL后，防止错误的nginx配置导致&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 死循环（递归地修改URI），因此，这一阶段仅由ngx_http_core_module&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 模块处理。目前，控制死循环的方法就是看rewrite次数，超过一定阈值&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 就认为出现了死循环，返回500&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  NGX_HTTP_POST_REWRITE_PHASE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 表示在处理NGX_HTTP_ACCESS_PHASE阶段决定请求的访问权限前，HTTP模块可以介入的处理阶段&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  NGX_HTTP_PREACCESS_PHASE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 这个阶段用于让HTTP模块判断是否允许这个请求访问Nginx服务器&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  NGX_HTTP_ACCESS_PHASE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 在NGX_HTTP_ACCESS_PHASE阶段中，当HTTP模块的handler处理函数返回不允许访问的错误码时（&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 实际就是NGX_HTTP_FORBIDDEN或者NGX_HTTP_UNAUTHORIZED），这里将负责向用户发送拒绝服务的&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 错误响应，因此这个阶段实际上用于给NGX_HTTP_ACCESS_PHASE阶段收尾&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  NGX_HTTP_POST_ACCESS_PHASE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 这个阶段完全为try_files配置项而设立的，当HTTP访问静态文件资源时，try_files配置项可以&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 使这个请求顺序地访问多个静态文件资源，如果某一次访问失败，则继续访问try_files中指定的&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 下一个静态资源。&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  NGX_HTTP_PRECONTENT_PHASE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 用于处理HTTP请求内容的阶段，这是大部分HTTP模块最愿意介入的阶段&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  NGX_HTTP_CONTENT_PHASE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 处理完请求记录日志的阶段。&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  NGX_HTTP_LOG_PHASE&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} ngx_http_phases;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这11个阶段里，有一些是可以由模块开发者插入自己的处理函数，有一些只能使用nginx的http框架的实现。另外，每个阶段并不是一定只能有一个处理函数，有的可以提供多个处理函数，在同一个阶段中顺序被调用。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Nginx源码阅读笔记-查询HTTP配置流程</title>
      <link>https://www.codedump.info/zh/post/20190212-nginx-http-config/</link>
      <pubDate>Tue, 12 Feb 2019 09:54:25 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190212-nginx-http-config/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面已经分析过&lt;a href=&#34;https://www.codedump.info/post/20190103-nginx-config-parse/#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99&#34;&gt;nginx解析配置文件的整体流程&lt;/a&gt;，接下来看查询HTTP配置的流程。&lt;/p&gt;&#xA;&lt;p&gt;HTTP属于nginx的core顶层模块，下面又包括了三部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;main部分配置：即在HTTP块但是又不在任何server、location块中的配置，如下图中的sendfile配置指令。&lt;/li&gt;&#xA;&lt;li&gt;server块：在server块内部的配置。&lt;/li&gt;&#xA;&lt;li&gt;location块：在location块内部分配置。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;http_config&#34; src=&#34;https://www.codedump.info/media/imgs/20190212-nginx-http-config/http_config.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; http_config &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;解析HTTP模块的入口函数是ngx_http_block，这一点可以从http指令相关的配置看出：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{ &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;ngx_string&lt;/span&gt;(&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;http),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ngx_http_block,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  0,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  0,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;NULL&lt;/span&gt; }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在这个解析函数的开始，就创建了ngx_http_conf_ctx_t结构体，所以看的出来这个结构体是HTTP模块的第一级配置，它的定义如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;typedef&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt;        **main_conf;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt;        **srv_conf;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt;        **loc_conf;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_http_conf_ctx_t&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;下面列举出来这几部分相关的函数以及数据结构：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;块&lt;/th&gt;&#xA;          &lt;th&gt;入口函数&lt;/th&gt;&#xA;          &lt;th&gt;数据结构&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;http&lt;/td&gt;&#xA;          &lt;td&gt;ngx_http_block&lt;/td&gt;&#xA;          &lt;td&gt;ngx_http_conf_ctx_t&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;main&lt;/td&gt;&#xA;          &lt;td&gt;&lt;/td&gt;&#xA;          &lt;td&gt;ngx_http_core_main_conf_t&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;server&lt;/td&gt;&#xA;          &lt;td&gt;ngx_http_core_server&lt;/td&gt;&#xA;          &lt;td&gt;ngx_http_core_srv_conf_t&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;location&lt;/td&gt;&#xA;          &lt;td&gt;ngx_http_core_location&lt;/td&gt;&#xA;          &lt;td&gt;ngx_http_core_loc_conf_t&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;ngx_http_module&#34; src=&#34;https://www.codedump.info/media/imgs/20190212-nginx-http-config/ngx_http_module.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; ngx_http_module &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;另外，由于HTTP块内的一些配置，作用域可以在多种块中，因此需要涉及到合并配置的流程，即：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如果子作用域某配置项在解析过程中未被赋值，则将父作用域的 相同的配置项值拷贝至此配置项里；&lt;/li&gt;&#xA;&lt;li&gt;如果子作用域配置项在解析过程中被赋值了，则保留原 样；如果子作用域配置项和父作用域配置项都没有被初始化，则填入代码中预设的默认值。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;相关的合并配置函数列举如下：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;块&lt;/th&gt;&#xA;          &lt;th&gt;合并函数&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;server&lt;/td&gt;&#xA;          &lt;td&gt;ngx_http_merge_servers&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;location&lt;/td&gt;&#xA;          &lt;td&gt;ngx_http_merge_locations&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;以下具体看看一次HTTP请求如何查找到相关HTTP配置的流程，分为两步：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;根据Host查找server块&lt;/li&gt;&#xA;&lt;li&gt;根据URI查找location块&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;根据host查找server块流程&#34;&gt;&#xA;  根据Host查找server块流程&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a0%b9%e6%8d%aehost%e6%9f%a5%e6%89%beserver%e5%9d%97%e6%b5%81%e7%a8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面分析&lt;a href=&#34;https://www.codedump.info/post/20190131-nginx-read-http-request/&#34;&gt;nginx接收HTTP请求流程&lt;/a&gt;中分析到，nginx在接收HTTP请求流程中，将调用ngx_http_process_request_headers函数来处理请求头。&lt;/p&gt;&#xA;&lt;p&gt;nginx使用一个ngx_http_header_t结构体，定义了哪些请求头需要进行特定的函数回调处理，函数ngx_http_process_request_headers会根据这个表来查询接收到的请求头都需要哪些回调函数来处理：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_http_header_t&lt;/span&gt;  ngx_http_headers_in[] = {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  { &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;ngx_string&lt;/span&gt;(&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;Host&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;offsetof&lt;/span&gt;(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_http_headers_in_t&lt;/span&gt;, host),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ngx_http_process_host },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  { &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;ngx_string&lt;/span&gt;(&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;Connection&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;offsetof&lt;/span&gt;(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_http_headers_in_t&lt;/span&gt;, connection),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ngx_http_process_connection },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ....&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到，针对Host这个header，会调用ngx_http_process_host函数，这个函数最终会调用ngx_http_set_virtual_server函数来根据Host头确定对应的server块。&lt;/p&gt;</description>
    </item>
    <item>
      <title>zeromq所谓的“无锁消息队列”</title>
      <link>https://www.codedump.info/zh/post/20190209-zeromq-lockfree-queue/</link>
      <pubDate>Sat, 09 Feb 2019 20:10:13 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190209-zeromq-lockfree-queue/</guid>
      <description>&lt;p&gt;本文基于zeromq 4.3.0版本，分析其无锁消息队列的实现。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;zeromq这个网络库，有以下几个亮点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;从以往的面向TCP流的网络开发，变成了面向消息的开发。应用层关注的是什么类型的消息，库本身解决网络收发、断线重连等问题。&lt;/li&gt;&#xA;&lt;li&gt;将这些消息的传输模式封装成几个模式，应用开发者只需要关注自己的业务符合什么模式，采用搭积木的方式就能构建起应用服务。&lt;/li&gt;&#xA;&lt;li&gt;内部实现无锁消息队列用于对象间通信，类似actor模式。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;基本架构&#34;&gt;&#xA;  基本架构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e6%9c%ac%e6%9e%b6%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;zeromq内部运行着多个io线程，每个io线程内部有以下两个核心组件：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;poller：即针对epoll、select等事件轮询器的封装。&lt;/li&gt;&#xA;&lt;li&gt;mailbox：负责接收消息的消息邮箱。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以简单理解IO线程做的事情是：内部通过一个poller，监听着各种事件，其中包括针对IO线程的mailbox的消息，以及绑定在该IO线程上的IO对象的消息。&lt;/p&gt;&#xA;&lt;p&gt;即这是一个per-thread-per-loop的线程设计，线程之间的通信通过消息邮箱来进行。&lt;/p&gt;&#xA;&lt;p&gt;除了io线程之外，io对象也有mailbox，即如果想与某个IO对象通信也是通过该mailbox进行。由于消息邮箱是zeromq中的重要组成部分，下面将专门分析zeromq是如何实现的。&lt;/p&gt;&#xA;&lt;p&gt;所有需要收发消息的对象都继承自object_t：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;class &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;object_t&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;public:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;object_t&lt;/span&gt; (zmq::&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ctx_t&lt;/span&gt; *ctx_, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint32_t&lt;/span&gt; tid_);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;process_command&lt;/span&gt; (zmq::&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;command_t&lt;/span&gt; &amp;amp;cmd_);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;private:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  zmq::&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ctx_t&lt;/span&gt; *ctx;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;//  Context provides access to the global state.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint32_t&lt;/span&gt; tid;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;//  Thread ID of the thread the object belongs to.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;send_command&lt;/span&gt; (&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;command_t&lt;/span&gt; &amp;amp;cmd_);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;而IO对象之间的命令通过command_t结构体来定义：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;command_t&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;//  Object to process the command.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  zmq::&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;object_t&lt;/span&gt; *destination;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;enum&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;type_t&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ...&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  } type;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;union&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ...&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  } args;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到，zeromq实现对象间相互通信依赖于mailbox，本文重点在分析其无锁队列的实现上。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Nginx源码阅读笔记-接收HTTP请求流程</title>
      <link>https://www.codedump.info/zh/post/20190131-nginx-read-http-request/</link>
      <pubDate>Fri, 01 Feb 2019 17:50:37 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190131-nginx-read-http-request/</guid>
      <description>&lt;p&gt;前面已经描述过nginx的事件模块了，接下来具体分析nginx如何接收一个HTTP请求，下一部分接着解析nginx解析HTTP请求的流程。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;协议状态机编程模式&#34;&gt;&#xA;  协议状态机编程模式&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8d%8f%e8%ae%ae%e7%8a%b6%e6%80%81%e6%9c%ba%e7%bc%96%e7%a8%8b%e6%a8%a1%e5%bc%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;TCP协议是一种流协议（stream protocol），这意味着数据是以字节流形式给数据接收者的，一次网络接收不一定能接收完毕，需要上面的应用层根据自己协议的情况来解析处理。它的数据没有边界，需要应用层自己根据协议来判断边界的存在。&lt;/p&gt;&#xA;&lt;p&gt;如果两次请求，分开为几次接收，但是某次接收的数据中，有跨两次请求的数据，这就是所谓的“粘包(sticky-package)”问题。如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;sticky-package-problem&#34; src=&#34;https://www.codedump.info/media/imgs/20190131-nginx-read-http-request/sticky-package-problem.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; sticky-package-problem &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;结合epoll之类的事件派发器来设计一个TCP协议的服务器时，因为并不能确保每一次接收数据，都能完整的接收到协议所需的所有数据。因此一般而言，写一个高性能服务器的协议解析部分，会以状态机的方式来实现，即定义了协议数据的每个部分，如下伪代码所示：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 定义协议头数据&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;typedef&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;header_t&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 协议版本号&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;int&lt;/span&gt; version;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 定义body部分大小&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;int&lt;/span&gt; size;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;header_t&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 定义协议数据&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;typedef&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;protocol_t&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;header_t&lt;/span&gt; header;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;char&lt;/span&gt; body[0];&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} protocol;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 定义接收数据的状态机类型&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;enum&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;state_t&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  RECV_HEADER,        &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 接收包头&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  RECV_BODY,          &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 接收包体&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  PROCESS_PROTOCOL,   &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 处理协议&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  SEND_RESPONSE       &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 发送回复&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 处理请求的状态机&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;statemachine&lt;/span&gt;() {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;switch&lt;/span&gt; (state) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;case&lt;/span&gt; RECV_HEADER:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 接收协议包头数据&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 接收完毕之后，切换state到RECV_BODY&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;case&lt;/span&gt; RECV_BODY:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 接收协议包体数据&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 接收完毕之后，切换state到PROCESS_PROTOCOL&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;case&lt;/span&gt; PROCESS_PROTOCOL:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 处理协议&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 处理完毕之后，切换state到SEND_RESPONSE&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;case&lt;/span&gt; SEND_RESPONSE:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 发送应答&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;&lt;/span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如上面的伪代码所示，接收一个请求之后，会初始化一个变量state用于保存当前协议处理的状态类型，假如第一次接收数据时还不能接收完毕协议的数据，就将接收fd重新放入到事件派发器中，下一次被唤醒之后再根据当前的状态继续接收数据进行处理。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Nginx源码阅读笔记-事件处理模块</title>
      <link>https://www.codedump.info/zh/post/20190131-nginx-event/</link>
      <pubDate>Thu, 31 Jan 2019 21:12:01 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190131-nginx-event/</guid>
      <description>&lt;p&gt;大概做高性能服务器的，都绕不开事件处理模块来，一般一个事件模块，会分为以下几部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如何定义一个描述事件的数据结构。&lt;/li&gt;&#xA;&lt;li&gt;如何在事件模块中支持定时器。&lt;/li&gt;&#xA;&lt;li&gt;如果需要支持多平台，事件模块需要考虑如何统一以及区分各平台的具体实现。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;下面就这三部分展开Nginx事件处理模块的分析。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;ngx_event_t&#34;&gt;&#xA;  ngx_event_t&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#ngx_event_t&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;描述事件的数据结构，一般至少需要以下几部分数据：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;用于保存用户相关的数据。&lt;/li&gt;&#xA;&lt;li&gt;用于保存事件触发之后的回调函数。&lt;/li&gt;&#xA;&lt;li&gt;用于表示事件状态、类型的数据。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;nginx中，描述事件采用的数据结构是ngx_event_t中，其内部成员就是按照前面的三部分来划分了。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;void *data：事件相关的数据。&lt;/li&gt;&#xA;&lt;li&gt;ngx_event_handler_pt handler：事件被触发时的回调函数。&lt;/li&gt;&#xA;&lt;li&gt;第三类数据，ngx_event_t中划分的比较仔细：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;unsigned write:1：可写标志位&lt;/li&gt;&#xA;&lt;li&gt;unsigned active:1：活跃标志位&lt;/li&gt;&#xA;&lt;li&gt;unsigned disabled:1：禁用标志位&lt;/li&gt;&#xA;&lt;li&gt;unsigned eof:1：为1表示字节流已经结束&lt;/li&gt;&#xA;&lt;li&gt;unsigned error:1：处理事件出错&lt;/li&gt;&#xA;&lt;li&gt;unsigned timedout:1：事件超时&lt;/li&gt;&#xA;&lt;li&gt;unsigned timer_set:1：为1表示这是一个超时事件&lt;/li&gt;&#xA;&lt;li&gt;unsigned deferred_accept:1：为1表示需要延迟接收TCP连接&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;除了以上三部分，还有其他一些重要的数据：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;ngx_rbtree_node_t timer：红黑树节点，用于实现定时器的，下面讨论定时器再展开。&lt;/li&gt;&#xA;&lt;li&gt;ngx_queue_t queue：延迟队列，如果事件不在轮询循环中直接处理，而是之后被处理，就放在这个队列中。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;总体来看，event这个结构体为了涵盖所有可能的事件，做的大而全，不只是用来描述一般的IO事件，还包括了定时器事件，还包括了接收连接相关的数据。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;定时器的实现&#34;&gt;&#xA;  定时器的实现&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%9a%e6%97%b6%e5%99%a8%e7%9a%84%e5%ae%9e%e7%8e%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;Nginx内部使用红黑树来实现定时器，目的在于能够快速的查询到哪些定时器超时了。不同的事件结构中，这部分实现采用的数据结构不一样，libevent、libuv采用的是最小堆，redis比较挫，这部分采用的是链表。&lt;/p&gt;&#xA;&lt;p&gt;在一个事件循环中，因为既要考虑到一般的IO事件，又要考虑到定时器事件，所以都会以一个最近被触发的定时器来做为查询IO事件被触发的时间，即以下的伪代码：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;查询最近将被触发的定时器超时时间返回&lt;/span&gt;t&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;将&lt;/span&gt;t做为epoll_wait之类的查询IO事件的超时时间&lt;span style=&#34;&#34;&gt;，即最长等待&lt;/span&gt;t时间看有没有IO事件被触发&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;遍历定时器，查询已经超时的定时器进行回调处理&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从这里可以看出，“迅速查询到距离当前最近被触发的定时器时间”以及“迅速查询到当前哪些定时器超时”，是这个定时器模块速度的关键。&lt;/p&gt;&#xA;&lt;p&gt;由于红黑树、最小堆这种平衡数据结构，每次查询都排除掉当前一半的元素，可以做到时间复杂度O(logn)，所以就常用来实现定时器了。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;事件模块的实现&#34;&gt;&#xA;  事件模块的实现&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%ba%8b%e4%bb%b6%e6%a8%a1%e5%9d%97%e7%9a%84%e5%ae%9e%e7%8e%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;由于nginx需要跑在多个平台下面，而不同平台使用的事件机制又不一样，比如linux是epoll，bsd是kqueue等，需要实现事件模块的时候，既需要统一事件模块的共性部分，又需要区分不同平台的差异部分。&lt;/p&gt;&#xA;&lt;p&gt;这看上去又是一个面向对象的设计问题了：基类负责实现共性的部分，子类具体再来实现各平台相关的部分。&lt;/p&gt;&#xA;&lt;p&gt;前面&lt;a href=&#34;http://localhost:1313/post/20190123-libuv/#%E6%80%BB%E7%BB%93&#34;&gt;分析libuv&lt;/a&gt;的时候提到过，libuv多使用宏来模拟C++中的继承，不是很认可这个代码风格，来看看nginx类似场景的实现。&lt;/p&gt;&#xA;&lt;p&gt;nginx中，将事件相关的操作函数统一放在结构体ngx_event_actions_t中，可以把这部分类比于子类需要实现的函数接口：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;typedef&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt;  (*add)(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_event_t&lt;/span&gt; *ev, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt; event, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_uint_t&lt;/span&gt; flags);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt;  (*del)(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_event_t&lt;/span&gt; *ev, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt; event, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_uint_t&lt;/span&gt; flags);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt;  (*enable)(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_event_t&lt;/span&gt; *ev, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt; event, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_uint_t&lt;/span&gt; flags);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt;  (*disable)(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_event_t&lt;/span&gt; *ev, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt; event, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_uint_t&lt;/span&gt; flags);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt;  (*add_conn)(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_connection_t&lt;/span&gt; *c);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt;  (*del_conn)(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_connection_t&lt;/span&gt; *c, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_uint_t&lt;/span&gt; flags);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt;  (*notify)(ngx_event_handler_pt handler);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt;  (*process_events)(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_cycle_t&lt;/span&gt; *cycle, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_msec_t&lt;/span&gt; timer,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_uint_t&lt;/span&gt; flags);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_int_t&lt;/span&gt;  (*init)(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_cycle_t&lt;/span&gt; *cycle, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_msec_t&lt;/span&gt; timer);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt;       (*done)(&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_cycle_t&lt;/span&gt; *cycle);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ngx_event_actions_t&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;前面在分析到nginx如何解析配置的时候提到过，nginx中的配置是分层次的，event模块做为一个顶层的core模块，内部又有子模块，而这里的事件模块就是event模块中的子模块：&lt;/p&gt;</description>
    </item>
    <item>
      <title>Nginx源码阅读笔记-Master Woker进程模型</title>
      <link>https://www.codedump.info/zh/post/20190131-nginx-master-worker/</link>
      <pubDate>Thu, 31 Jan 2019 09:53:57 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190131-nginx-master-worker/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;master进程&#34;&gt;&#xA;  master进程&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#master%e8%bf%9b%e7%a8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;Nginx采用的模型是master-worker模型，即：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;由master进程负责创建worker进程，以及监控worker进程的情况，如需要更新配置的情况下发消息给worker进程重新加载配置等。&lt;/li&gt;&#xA;&lt;li&gt;master进程负责具体网络事件。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;将里面核心的流程和函数抽取出来，如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;master-worker&#34; src=&#34;https://www.codedump.info/media/imgs/20190131-nginx-master-worker/master-worker.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; master-worker &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;master进程的主循环在函数ngx_master_process_cycle中，主要负责：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;调用ngx_start_worker_processes函数创建worker子进程。&lt;/li&gt;&#xA;&lt;li&gt;监控各种信号量的变化做处理，比如需要停止进程、重新加载配置等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;master进程最终会调用函数ngx_spawn_process函数来创建出worker子进程：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;使用共享内存创建出用于master-worker进程之间通信的channel。&lt;/li&gt;&#xA;&lt;li&gt;fork出子进程之后，进入worker进程的主函数ngx_worker_process_cycle。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下列举出几个相关的信号量：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;信号&lt;/th&gt;&#xA;          &lt;th&gt;对应全局变量&lt;/th&gt;&#xA;          &lt;th&gt;处理&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;QUIT&lt;/td&gt;&#xA;          &lt;td&gt;ngx_quit&lt;/td&gt;&#xA;          &lt;td&gt;优雅关闭整个Nginx服务&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;TERM或者INT&lt;/td&gt;&#xA;          &lt;td&gt;ngx_terminate&lt;/td&gt;&#xA;          &lt;td&gt;强制关闭Nginx服务&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;USR1&lt;/td&gt;&#xA;          &lt;td&gt;ngx_reopen&lt;/td&gt;&#xA;          &lt;td&gt;重新打开文件&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;WINCH&lt;/td&gt;&#xA;          &lt;td&gt;ngx_noaccept&lt;/td&gt;&#xA;          &lt;td&gt;所有worker进程不再接受新的连接，相当于给子进程发送QUIT信号&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;USR2&lt;/td&gt;&#xA;          &lt;td&gt;ngx_change_binary&lt;/td&gt;&#xA;          &lt;td&gt;平滑升级到新的Nginx二进制文件&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;HUP&lt;/td&gt;&#xA;          &lt;td&gt;ng_reconfigure&lt;/td&gt;&#xA;          &lt;td&gt;重新加载配置文件&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;CHLD&lt;/td&gt;&#xA;          &lt;td&gt;ngx_reap&lt;/td&gt;&#xA;          &lt;td&gt;需要监控所有子进程&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;worker进程&#34;&gt;&#xA;  worker进程&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#worker%e8%bf%9b%e7%a8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;worker进程的函数入口在ngx_worker_process_cycle中，其主要做的工作分为两部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;调用ngx_process_events_and_timers处理IO事件以及定时器事件。&lt;/li&gt;&#xA;&lt;li&gt;判断ngx_terminate、ngx_quit、ngx_reopen这几个变量是否被置位来做相应的处理。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;下面主要谈网络IO事件的处理，即ngx_process_events_and_timers函数。&lt;/p&gt;&#xA;&lt;p&gt;先来介绍几个与接收连接相关的全局变量：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;ngx_use_accept_mutex：由配置项accept_mutex配置，表示是否需要使用accept锁，只有抢到该锁的worker才允许接收新的连接。&lt;/li&gt;&#xA;&lt;li&gt;ngx_accept_mutex_delay：由配置项accept_mutex_delay配置，在开启accept_mutex的情况下，一个worker进程在抢不到accept锁的情况下，最长多少时间才重新接收新的连接。&lt;/li&gt;&#xA;&lt;li&gt;ngx_accept_disabled：值为ngx_cycle-&amp;gt;connection_n / 8 - ngx_cycle-&amp;gt;free_connection_n，可以看到当链接数量到nginx.conf中配置的worker_connections的7/8以上时，这个变量ngx_accept_disabled为正数，此时不会接收新的连接，直到该值小于等于0为止。&lt;/li&gt;&#xA;&lt;li&gt;ngx_accept_mutex_held：表示是否抢到了accept锁，只有抢到的才能接收新连接。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;具体来看看ngx_process_events_and_timers函数中与接收连接相关的逻辑，伪代码如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;如果开启了&lt;/span&gt;accept_mutex配置&lt;span style=&#34;&#34;&gt;：&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;&#34;&gt;如果当前&lt;/span&gt;ngx_accept_disabled大于0&lt;span style=&#34;&#34;&gt;，表示不能接收新的连接，直接返回。&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;&#34;&gt;否则尝试获取&lt;/span&gt;accept mutex&lt;span style=&#34;&#34;&gt;。&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;&#34;&gt;如果获取&lt;/span&gt;accept mutex锁成功&lt;span style=&#34;&#34;&gt;：&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;&#34;&gt;将调用事件轮询函数的标志位加上&lt;/span&gt;NGX_POST_EVENTS标志&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;&#34;&gt;如果获取失败：&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;&#34;&gt;调用事件轮询函数的事件参数不得超过&lt;/span&gt;ngx_accept_mutex_delay值&lt;span style=&#34;&#34;&gt;。&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;&#34;&gt;调用&lt;/span&gt;ngx_process_events函数处理轮询事件&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;&#34;&gt;调用&lt;/span&gt;ngx_event_process_posted(cycle, &amp;amp;ngx_posted_accept_events)&lt;span style=&#34;&#34;&gt;函数处理&lt;/span&gt;accept事件&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;&#34;&gt;如果前面拿到了&lt;/span&gt;accept mutex锁&lt;span style=&#34;&#34;&gt;，则释放这个锁，好让其他&lt;/span&gt;worker也有机会接收新的连接&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;&#34;&gt;调用&lt;/span&gt;ngx_event_expire_timers处理定时器事件&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;&#34;&gt;调用&lt;/span&gt;ngx_event_process_posted(cycle, &amp;amp;ngx_posted_events);&lt;span style=&#34;&#34;&gt;函数处理除了&lt;/span&gt;accept事件以外的其他post事件&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在ngx_process_events处理函数中，当传入的flags有NGX_POST_EVENTS标志时，意味着并不马上在这个函数中调用事件的回调函数进行处理，而是放在一个队列中，回头在后面的ngx_event_process_posted函数中再进行处理。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Libuv代码简单分析</title>
      <link>https://www.codedump.info/zh/post/20190123-libuv/</link>
      <pubDate>Wed, 23 Jan 2019 08:43:49 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190123-libuv/</guid>
      <description>&lt;p&gt;本文基于libuv 1.x版本进行简单的分析。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;数据结构&#34;&gt;&#xA;  数据结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;uv__io_t&#34;&gt;&#xA;  uv__io_t&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#uv__io_t&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;uv__io_t用来表示一个IO事件。&lt;/p&gt;&#xA;&lt;p&gt;其成员包括：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;成员&lt;/th&gt;&#xA;          &lt;th&gt;说明&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;uv__io_cb cb&lt;/td&gt;&#xA;          &lt;td&gt;IO事件被触发的回调函数&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;void* pending_queue[2]&lt;/td&gt;&#xA;          &lt;td&gt;pending队列&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;void* watcher_queue[2]&lt;/td&gt;&#xA;          &lt;td&gt;watcher队列&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;unsigned int pevents&lt;/td&gt;&#xA;          &lt;td&gt;pending的事件mask，等待下一次被添加到事件中&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;unsigned int events&lt;/td&gt;&#xA;          &lt;td&gt;当前的事件mask&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;int fd&lt;/td&gt;&#xA;          &lt;td&gt;事件fd&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;queue&#34;&gt;&#xA;  queue&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#queue&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;libuv的queue实现比较奇葩，一个queue里面的元素会有两个指针，一个指向队列前一个成员，一个指向队列下一个成员，在这里不做阐述，看到类似：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;void&lt;/span&gt;* watcher_queue[2]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样子定义了有两个void*指针的数组知道这是一个队列就好了。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;uv_timer_t&#34;&gt;&#xA;  uv_timer_t&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#uv_timer_t&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;定义定时器的结构体，libuv中使用最小堆来维护定时器。&lt;/p&gt;&#xA;&lt;p&gt;一般而言，都是首先从这个最小堆数据结构中获得距离当前最近的定时器，然后拿到它的超时时间，以该超时时间做为下一次loop事件循环的时间，某些情况下会无视这个值，比如存在idle handler的情况下，此时会以0做为超时时间。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;uv_handle_t及其子类&#34;&gt;&#xA;  uv_handle_t及其子类&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#uv_handle_t%e5%8f%8a%e5%85%b6%e5%ad%90%e7%b1%bb&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;uv_handle_t是libuv中所有handler的基类，libuv中实现继承的手段也比较奇葩：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;类成员定义放在宏里。&lt;/li&gt;&#xA;&lt;li&gt;继承自某个基类的子类按照继承顺序依次放它前面基类的宏。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;比如uv_tcp_t继承自uv_stream_t，而后者又继承自uv_handle_t，三者的定义如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; uv_handle_s {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  UV_HANDLE_FIELDS&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; uv_stream_s {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  UV_HANDLE_FIELDS&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  UV_STREAM_FIELDS&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;struct&lt;/span&gt; uv_tcp_s {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  UV_HANDLE_FIELDS&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  UV_STREAM_FIELDS&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  UV_TCP_PRIVATE_FIELDS&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;代码中可以看到：&lt;/p&gt;</description>
    </item>
    <item>
      <title>Nginx源码阅读笔记-配置解析流程</title>
      <link>https://www.codedump.info/zh/post/20190103-nginx-config-parse/</link>
      <pubDate>Thu, 03 Jan 2019 08:41:44 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190103-nginx-config-parse/</guid>
      <description>&lt;p&gt;本系列文章基于openresty-1.13.6.1版本的代码做的笔记，其对应的nginx源码版本是nginx-1.13.6。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;模块与配置值解析相关数据结构&#34;&gt;&#xA;  模块与配置值解析相关数据结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a8%a1%e5%9d%97%e4%b8%8e%e9%85%8d%e7%bd%ae%e5%80%bc%e8%a7%a3%e6%9e%90%e7%9b%b8%e5%85%b3%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;整个Nginx是以模块的方式来组织的，即使是核心的组件如epoll之类的，最终也是以模块的方式注册到nginx中的。所以先了解整个nginx模块的结构很有必要。&lt;/p&gt;&#xA;&lt;p&gt;与模块相关的核心数据结构有以下这几个。&lt;/p&gt;&#xA;&lt;p&gt;ngx_module_t结构体用于定义nginx模块相关的数据结构，其中包括几个核心的数据：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;void *ctx：用于存储每个模块相关的context数据。&lt;/li&gt;&#xA;&lt;li&gt;ngx_command_t *commands：用于存储与该模块相关的配置命令解析数据。所谓的配置命令，就是对应的nginx配置文件中的语句，如”event“、”include“等，每个配置语句最终一定有一个相关的ngx_command_t数据与之对应，负责解析这个命令。&lt;/li&gt;&#xA;&lt;li&gt;ngx_uint_t type：用于保存模块的类型，目前包括NGX_HTTP_MODULE，NGX_CORE_MODULE，NGX_CONF_MOULE，NGX_EVENT_MODULE，NGX_MAIL_MODULE这几种类型。&lt;/li&gt;&#xA;&lt;li&gt;一组回调函数：用于在解析配置的时候进行回调。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;而上面的ngx_command_t结构体又有以下成员：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;ngx_str_t name：配置文件里对应的配置项名称。如前面提到的nginx配置文件中的”event“、”include“等。&lt;/li&gt;&#xA;&lt;li&gt;ngx_uint_t type：配置项类型，这里会存储如该配置项应该出现在什么位置（http块、server块、location块等），以及配置项参数数量，以便于解析过程中进行合法性的判断。&#xA;&lt;ul&gt;&#xA;&lt;li&gt;命令的作用域，即该命令能够出现在什么位置（http块、server块、location块等），这些与作用域相关的类型有NGX_MAIN_CONF、NGX_EVENT_CONF、NGX_HTTP_MAIN_CONF、NGX_HTTP_SRV_CONF、 NGX_HTTP_LOC_CONF、NGX_MAIL_MAIN_CONF、NGX_MAIL_SRV_CONF 等。&lt;/li&gt;&#xA;&lt;li&gt;命令能够接受的参数数量，如NGX_CONF_NOARGS、NGX_CONF_TAKE1等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;ngx_uint_t offset：该配置命令所要修改的配置项在该模块配置结构体中的偏移量。&lt;/li&gt;&#xA;&lt;li&gt;ngx_uint_t conf：该配置在子模块配置项中的索引。&lt;/li&gt;&#xA;&lt;li&gt;回调函数set：在解析到配置项的时候进行回调。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;下图中给出一个简单的nginx配置文件的作用域示意图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;ngx-conf-scope&#34; src=&#34;https://www.codedump.info/media/imgs/20190103-nginx-config-parse/ngx-conf-scope.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; ngx-conf-scope &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;有了以上两个核心数据结构，可以知道每个nginx模块注册时的方式：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;定义一组与本模块相关的ngx_command_t，用于定义本模块相关的配置项信息。&lt;/li&gt;&#xA;&lt;li&gt;定义一个与本模块相关的数据结构，注册为ngx_module_t的ctx指针，用于保存本模块相关的数据结构。&lt;/li&gt;&#xA;&lt;li&gt;最后，将上面的数据放到ngx_module_t中，nginx解析配置的时候会自动回调对应的处理函数了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以epoll模块来说，其ngx_module_t结构体是如下组织的。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;epoll-module&#34; src=&#34;https://www.codedump.info/media/imgs/20190103-nginx-config-parse/epoll-module.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; epoll-module &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;根据上面的图示，不难想象，nginx在配置解析的时候是如何解析epoll相关的配置的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;首先解析到event模块，也就是nginx配置文件中的event{}部分，此时会把对用的context指针指向ngx_epoll_module_ctx，开始进行event模块的解析工作。&lt;/li&gt;&#xA;&lt;li&gt;如果在event块中遇到名为“epoll_events”或者“worker_aio_requests”开始的配置，那么就知道是上面ngx_epoll_commands数组中定义的配置命令，nginx首先会根据这里定义的type来分析其出现的位置（是否出现在event块）以及参数数量（NGX_CONF_TAKE1）是否正确，都检测通过之后，才会调用ngx_command_t中set回调函数进行配置解析。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;解析配置流程&#34;&gt;&#xA;  解析配置流程&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%a7%a3%e6%9e%90%e9%85%8d%e7%bd%ae%e6%b5%81%e7%a8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;相关核心函数&#34;&gt;&#xA;  相关核心函数&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%9b%b8%e5%85%b3%e6%a0%b8%e5%bf%83%e5%87%bd%e6%95%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;上面分析到ngx_module_t结构体的时候曾经提到过，当前的type有如下类型：NGX_HTTP_MODULE，NGX_CORE_MODULE，NGX_CONF_MOULE，NGX_EVENT_MODULE，NGX_MAIL_MODULE。&lt;/p&gt;&#xA;&lt;p&gt;实际上，nginx的模块类型是一个树形结构，最顶层的是NGX_CORE_MODULE，它下面的子类型是NGX_HTTP_MODULE、NGX_EVENT_MODULE等。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;module-tree&#34; src=&#34;https://www.codedump.info/media/imgs/20190103-nginx-config-parse/module-tree.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; module-tree &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如果一个结构体是使用树形结构来进行组织的，那么首先会想到的是“递归算法”。&lt;/p&gt;&#xA;&lt;p&gt;实际上nginx配置解析，确实也是以递归的方式来进行解析的。仍然是以前面的epoll模块解析为例来说明这个流程：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;首先解析类型为NGX_CORE_MODULE的顶层模块，由于event模块（即event{}配置块）也是NGX_CORE_MODULE，因此解析到event{}块时会进入event模块相关的配置解析中。&lt;/li&gt;&#xA;&lt;li&gt;下面开始进入NGX_EVENT_MODULE这个二层模块的解析中，当遇到epoll相关的指令时，进入epoll模块的解析。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;上面可以看到，既然是递归方式来解析的，那么意味着解析配置使用的解析函数是可以被递归调用的，在nginx中这个会被递归调用的核心函数是core/ngx_conf_file.c中的ngx_conf_parse函数。&lt;/p&gt;&#xA;&lt;p&gt;在ngx_conf_parse中，会根据传入的filename来区分几种情况：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;filename不为空：说明是首次调用该函数，此时会打开filename指定的文件名开始解析。&lt;/li&gt;&#xA;&lt;li&gt;cf-&amp;gt;conf_file-&amp;gt;file.fd != NGX_INVALID_FILE：说明此时是被递归调用的情况，用于分析一个{}block的内容。&lt;/li&gt;&#xA;&lt;li&gt;如果以上情况都不是，说明也是被递归调用的情况，而这时是分析一个参数的情况。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;另外，既然ngx_conf_parse会被递归调用，每次传入的参数就都是一个类型的，被递归调用的时候就需要相应的做修改。&lt;/p&gt;&#xA;&lt;p&gt;ngx_conf_parse的函数原型如下：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;char*&#xA;ngx_conf_parse(ngx_conf_t *cf, ngx_str_t *filename);&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;可以看到其中有两个入参，其中之一的filename前面已经介绍过了，下面来介绍ngx_conf_t结构体。这个结构体可以认为是解析配置文件过程中为了保存数据的中间数据结构，其重要的几个成员是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;ngx_uint_t module_type：模块类型，即前面提到的NGX_HTTP_MODULE，NGX_CORE_MODULE，NGX_CONF_MOULE，NGX_EVENT_MODULE，NGX_MAIL_MODULE。&lt;/li&gt;&#xA;&lt;li&gt;ngx_uint_t cmd_type：命令类型，表示指令的作用域。有NGX_MAIN_CONF、NGX_EVENT_CONF、NGX_HTTP_MAIN_CONF、NGX_HTTP_SRV_CONF、 NGX_HTTP_LOC_CONF、NGX_MAIL_MAIN_CONF、NGX_MAIL_SRV_CONF 等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因此，在每次递归调用ngx_conf_parse函数之前，调用方都要相应的设置ngx_conf_t结构体这两个成员，好让ngx_conf_parse函数知道当前解析的是哪个模块、在哪个配置作用域中。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Etcd存储的实现</title>
      <link>https://www.codedump.info/zh/post/20181125-etcd-server/</link>
      <pubDate>Sun, 25 Nov 2018 15:13:28 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20181125-etcd-server/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;本文是博客解析raft算法及etcd raft库实现的系列三篇文章之一，之所以详细结合etcd实现解析raft算法原理及实现，因为etcd的raft实现是最接近论文本身的，结合论文原理一起阅读十分酸爽。这个系列文章的索引如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20210515-raft/&#34;&gt;Etcd Raft库的工程化实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;另外，我个人还针对etcd 3.1.10版本的raft相关代码实现做了一些代码的注释笔记，地址在此：&lt;a href=&#34;https://github.com/lichuang/etcd-3.1.10-codedump&#34;&gt;etcd-3.1.10-codedump&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概览&#34;&gt;&#xA;  概览&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%a7%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在前面已经分析了Raft算法原理、etcd raft库的实现，接着就可以看etcd如何使用raft实现存储服务的了。&lt;/p&gt;&#xA;&lt;p&gt;以下的分析主要针对etcd V3版本的实现。&lt;/p&gt;&#xA;&lt;p&gt;下图中展示了etcd如何处理一个客户端请求的涉及到的模块和流程。图中淡紫色的矩形表示etcd，它包括如下几个模块：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;etcd server：对外接收客户端的请求，对应etcd代码中的etcdserver目录，其中还有一个raft.go的模块与etcd-raft库进行通信。etcdserver中与存储相关的模块是applierV3，这里封装了V3版本的数据存储，WAL（write ahead log），用于写数据日志，etcd启动时会根据这部分内容进行恢复。&lt;/li&gt;&#xA;&lt;li&gt;etcd raft：etcd的raft库，前面的文章已经具体分析过这部分代码。除了与本节点的etcd server通信之外，还与集群中的其他etcd server进行交互做一致性数据同步的工作（在图中集群中其他etcd服务用橙色的椭圆表示）。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;etcd server&#34; src=&#34;https://www.codedump.info/media/imgs/20181125-etcd-server/etcd-server.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; etcd server &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中，一个请求与一个etcd集群交互的主要流程分为两大部分：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;写数据到某个etcd server中。&lt;/li&gt;&#xA;&lt;li&gt;该etcd server与集群中的其他etcd节点进行交互，当确保数据已经被存储之后应答客户端。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;请求流程划分为了以下的子步骤：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;1.1：etcd server收到客户端请求。&lt;/li&gt;&#xA;&lt;li&gt;1.2：etcd server将请求发送给本模块中的raft.go，这里负责与etcd raft模块进行通信。&lt;/li&gt;&#xA;&lt;li&gt;1.3：raft.go将数据封装成raft日志的形式提交给raft模块。&lt;/li&gt;&#xA;&lt;li&gt;1.4：raft模块会首先保存到raftLog的unstable存储部分。&lt;/li&gt;&#xA;&lt;li&gt;1.5：raft模块通过raft协议与集群中其他etcd节点进行交互。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;注意在以上流程中，假设这里写入数据的etcd是leader节点，因为在raft协议中，如果提交数据到非leader节点的话需要路由到etcd leader节点去。&lt;/p&gt;&#xA;&lt;p&gt;而应答步骤如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;2.1：集群中其他节点向leader节点应答接收这条日志数据。&lt;/li&gt;&#xA;&lt;li&gt;2.2：当超过集群半数以上节点应答接收这条日志数据时，etcd raft通过Ready结构体通知etcd server中的raft该日志数据已经commit。&lt;/li&gt;&#xA;&lt;li&gt;2.3：raft.go收到Ready数据将首先将这条日志写入到WAL模块中。&lt;/li&gt;&#xA;&lt;li&gt;2.4：通知最上层的etcd server该日志已经commit。&lt;/li&gt;&#xA;&lt;li&gt;2.5：etcd server调用applierV3模块将日志写入持久化存储中。&lt;/li&gt;&#xA;&lt;li&gt;2.6：etcd server应答客户端该数据写入成功。&lt;/li&gt;&#xA;&lt;li&gt;2.7：最后etcd server调用etcd raft，修改其raftLog模块的数据，将这条日志写入到raftLog的storage中。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从上面的流程可以看到&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;etcd raft模块在应答某条日志数据已经commit之后，是首先写入到WAL模块中的，因为这个模块只是添加一条日志，所以速度会很快，即使在后面applierV3写入失败，重启的时候也可以根据WAL模块中的日志数据进行恢复。&lt;/li&gt;&#xA;&lt;li&gt;etcd raft中的raftLog，按照前面文章的分析，其中的数据是保存到内存中的，重启即失效，上层应用真实的数据是持久化保存到WAL和applierV3中的。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下就来分析etcd server与这部分相关的几个模块。&lt;/p&gt;</description>
    </item>
    <item>
      <title>etcd Raft库解析</title>
      <link>https://www.codedump.info/zh/post/20180922-etcd-raft/</link>
      <pubDate>Sat, 22 Sep 2018 11:01:02 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20180922-etcd-raft/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;本文是博客解析raft算法及etcd raft库实现的系列三篇文章之一，之所以详细结合etcd实现解析raft算法原理及实现，因为etcd的raft实现是最接近论文本身的，结合论文原理一起阅读十分酸爽。这个系列文章的索引如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20210515-raft/&#34;&gt;Etcd Raft库的工程化实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;另外，我个人还针对etcd 3.1.10版本的raft相关代码实现做了一些代码的注释笔记，地址在此：&lt;a href=&#34;https://github.com/lichuang/etcd-3.1.10-codedump&#34;&gt;etcd-3.1.10-codedump&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;序言&#34;&gt;&#xA;  序言&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ba%8f%e8%a8%80&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;今年初开始学习了解Raft协议，论文读下来之后还是决定结合一个成熟的代码进行更深的理解。etcd做为一个非常成熟的作品，其Raft库实现也非常精妙，屏蔽了网络、存储等模块，提供接口由上层应用者来实现。&lt;/p&gt;&#xA;&lt;p&gt;本篇文章解析etcd的Raft库实现，基于etcd 3.1.10版本。etcd的Raft库，位于其代码目录的Raft中。&lt;/p&gt;&#xA;&lt;p&gt;我自己也单独将3.1.10的代码拉出了一个专门添加了我阅读代码注释的版本，目前Raft这部分基本都做了注释，见：&#xA;&lt;a href=&#34;https://github.com/lichuang/etcd-3.1.10-codedump&#34;&gt;https://github.com/lichuang/etcd-3.1.10-codedump&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;以下在介绍的时候，可能会混用中文和英文术语，这里先列举出来：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;英文&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: right&#34;&gt;中文&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Term&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;选举任期，每次选举之后递增1&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Vote&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;选举投票(的ID)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Entry&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;Raft算法的日志数据条目&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;candidate&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;候选人&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;leader&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;领导者&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;follower&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;跟随者&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;commit&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;提交&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;propose&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;提议&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;输入及输出&#34;&gt;&#xA;  输入及输出&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%be%93%e5%85%a5%e5%8f%8a%e8%be%93%e5%87%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;既然做为一个库使用，就有其确定的输入和输出接口，先来了解这部分再进行后续的展开讨论。&lt;/p&gt;&#xA;&lt;p&gt;作为一个一致性算法的库，不难想象使用的一般场景是这样的：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;应用层接收到新的写入数据请求，向该算法库写入一个数据。&lt;/li&gt;&#xA;&lt;li&gt;算法库返回是否写入成功。&lt;/li&gt;&#xA;&lt;li&gt;应用层根据写入结果进行下一步的操作。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;然而，Raft库却相对而言更复杂一些，因为还有以下的问题存在：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;写入的数据，可能是集群状态变更的数据，Raft库在执行写入这类数据之后，需要返回新的状态给应用层。&lt;/li&gt;&#xA;&lt;li&gt;Raft库中的数据不可能一直以日志的形式存在，这样会导致数据越来越大，所以有可能被压缩成快照（snapshot）的数据形式，这种情况下也需要返回这部分快照数据。&lt;/li&gt;&#xA;&lt;li&gt;由于etcd的Raft库不包括持久化数据存储相关的模块，而是由应用层自己来做实现，所以也需要返回在某次写入成功之后，哪些数据可以进行持久化保存了。&lt;/li&gt;&#xA;&lt;li&gt;同样的，etcd的Raft库也不自己实现网络传输，所以同样需要返回哪些数据需要进行网络传输给集群中的其他节点。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;以上的这些，集中在raft/node.go的Ready结构体中，其包括以下成员：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;成员名称&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: right&#34;&gt;类型&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;作用&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;SoftState&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;SoftState&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;软状态，软状态易变且不需要保存在WAL日志中的状态数据，包括：集群leader、节点的当前状态&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;HardState&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;HardState&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;硬状态，与软状态相反，需要写入持久化存储中，包括：节点当前Term、Vote、Commit&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;ReadStates&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;[]ReadStates&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;用于读一致性的数据，后续会详细介绍&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Entries&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;[]pb.Entry&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;在向其他集群发送消息之前需要先写入持久化存储的日志数据&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Snapshot&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;pb.Snapshot&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;需要写入持久化存储中的快照数据&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;CommittedEntries&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;[]pb.Entry&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;需要输入到状态机中的数据，这些数据之前已经被保存到持久化存储中了&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Messages&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;[]pb.Message&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;在entries被写入持久化存储中以后，需要发送出去的数据&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;以上的成员说明，最开始看不一定能理解其含义和用法，不过在后续会慢慢展开讨论。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Raft算法原理</title>
      <link>https://www.codedump.info/zh/post/20180921-raft/</link>
      <pubDate>Fri, 21 Sep 2018 20:15:32 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20180921-raft/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;本文是博客解析raft算法及etcd raft库实现的系列三篇文章之一，之所以详细结合etcd实现解析raft算法原理及实现，因为etcd的raft实现是最接近论文本身的，结合论文原理一起阅读十分酸爽。这个系列文章的索引如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20210515-raft/&#34;&gt;Etcd Raft库的工程化实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;另外，我个人还针对etcd 3.1.10版本的raft相关代码实现做了一些代码的注释笔记，地址在此：&lt;a href=&#34;https://github.com/lichuang/etcd-3.1.10-codedump&#34;&gt;etcd-3.1.10-codedump&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;简介&#34;&gt;&#xA;  简介&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ae%80%e4%bb%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;关于Raft算法，有两篇经典的论文，一篇是《In search of an Understandable Consensus Algorithm》，这是作者最开始讲述Raft算法原理的论文，但是这篇论文太简单了，很多算法的细节没有涉及到。更详细的论文是《Consensus: Bridging Theory and Practice》，除了包括第一篇论文的内容以外，还加上了很多细节的描述。在我阅读完etcd raft算法库的实现之后，发现这个库的代码基本就是按照后一篇论文来写的，甚至有部分测试用例的注释里也写明了是针对这篇论文的某一个小节的情况做验证。&lt;/p&gt;&#xA;&lt;p&gt;这篇文章做为我后续分析etcd raft算法的前导文章，将结合后一篇论文加上一些自己的演绎和理解来讲解Raft算法的原理。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;算法的基本流程&#34;&gt;&#xA;  算法的基本流程&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ae%97%e6%b3%95%e7%9a%84%e5%9f%ba%e6%9c%ac%e6%b5%81%e7%a8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;raft算法概述&#34;&gt;&#xA;  Raft算法概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#raft%e7%ae%97%e6%b3%95%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Raft算法由leader节点来处理一致性问题。leader节点接收来自客户端的请求日志数据，然后同步到集群中其它节点进行复制，当日志已经同步到超过半数以上节点的时候，leader节点再通知集群中其它节点哪些日志已经被复制成功，可以提交到raft状态机中执行。&lt;/p&gt;&#xA;&lt;p&gt;通过以上方式，Raft算法将要解决的一致性问题分为了以下几个子问题。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;leader选举：集群中必须存在一个leader节点。&lt;/li&gt;&#xA;&lt;li&gt;日志复制：leader节点接收来自客户端的请求然后将这些请求序列化成日志数据再同步到集群中其它节点。&lt;/li&gt;&#xA;&lt;li&gt;安全性：如果某个节点已经将一条提交过的数据输入raft状态机执行了，那么其它节点不可能再将相同索引&#xA;的另一条日志数据输入到raft状态机中执行。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Raft算法需要一直保持的几个属性。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;选举安全性（Election Safety）：在一个任期内只能存在最多一个leader节点。&lt;/li&gt;&#xA;&lt;li&gt;Leader节点上的日志为只添加（Leader Append-Only）：leader节点永远不会删除或者覆盖本节点上面的日志数据，leader节点上写日志的操作只可能是添加操作。&lt;/li&gt;&#xA;&lt;li&gt;日志匹配性（Log Matching）：如果两个节点上的日志，在日志的某个索引上的日志数据其对应的任期号相同，那么在两个节点在这条日志之前的日志数据完全匹配。&lt;/li&gt;&#xA;&lt;li&gt;leader完备性（Leader Completeness）：如果一条日志在某个任期被提交，那么这条日志数据在leader节点上更高任期号的日志数据中都存在。&lt;/li&gt;&#xA;&lt;li&gt;状态机安全性（State Machine Safety）：如果某个节点已经将一条提交过的数据输入raft状态机执行了，那么其它节点不可能再将相同索引的另一条日志数据输入到raft状态机中执行。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;raft-propertities&#34; src=&#34;https://www.codedump.info/media/imgs/20180921-raft/raft-propertities.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; raft-propertities &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;raft算法基础&#34;&gt;&#xA;  Raft算法基础&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#raft%e7%ae%97%e6%b3%95%e5%9f%ba%e7%a1%80&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在Raft算法中，一个集群里面的所有节点有以下三种状态：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Leader：领导者，一个集群里只能存在一个Leader。&lt;/li&gt;&#xA;&lt;li&gt;Follower：跟随者，follower是被动的，一个客户端的修改数据请求如果发送到Follower上面时，会首先由Follower重定向到Leader上，&lt;/li&gt;&#xA;&lt;li&gt;Candidate：参与者，一个节点切换到这个状态时，将开始进行一次新的选举。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;每一次开始一次新的选举时，称为一个“任期”。每个任期都有一个对应的整数与之关联，称为“任期号”，任期号用单词“Term”表示，这个值是一个严格递增的整数值。&lt;/p&gt;&#xA;&lt;p&gt;节点的状态切换状态机如下图所示。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;raft states&#34; src=&#34;https://www.codedump.info/media/imgs/20180921-raft/raft-states.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; raft states &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中标记了状态切换的6种路径，下面做一个简单介绍，后续都会展开来详细讨论。&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;start up：起始状态，节点刚启动的时候自动进入的是follower状态。&lt;/li&gt;&#xA;&lt;li&gt;times out, starts election：follower在启动之后，将开启一个选举超时的定时器，当这个定时器到期时，将切换到candidate状态发起选举。&lt;/li&gt;&#xA;&lt;li&gt;times out, new election：进入candidate 状态之后就开始进行选举，但是如果在下一次选举超时到来之前，都还没有选出一个新的leade，那么还会保持在candidate状态重新开始一次新的选举。&lt;/li&gt;&#xA;&lt;li&gt;receives votes from majority of servers：当candidate状态的节点，收到了超过半数的节点选票，那么将切换状态成为新的leader。&lt;/li&gt;&#xA;&lt;li&gt;discovers current leader or new term：candidate状态的节点，如果收到了来自leader的消息，或者更高任期号的消息，都表示已经有leader了，将切换回到follower状态。&lt;/li&gt;&#xA;&lt;li&gt;discovers server with higher term：leader状态下如果收到来自更高任期号的消息，将切换到follower状态。这种情况大多数发生在有网络分区的状态下。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;如果一个candidate在一次选举中赢得leader，那么这个节点将在这个任期中担任leader的角色。但并不是每个任期号都一定对应有一个leader的，比如上面的情况3中，可能在选举超时到来之前都没有产生一个新的leader，那么此时将递增任期号开始一次新的选举。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
