<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>瞎扯</title>
  
  <subtitle>瞎**扯</subtitle>
  <link href="http://nearsyh.me/atom.xml" rel="self"/>
  
  <link href="http://nearsyh.me/"/>
  <updated>2023-04-13T09:29:48.794Z</updated>
  <id>http://nearsyh.me/</id>
  
  <author>
    <name>Near</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Fly.IO Challenge 6 - Totally-Available Transactions</title>
    <link href="http://nearsyh.me/2023/04/13/2023-04-13-FlyIO-Challenge-Txn/"/>
    <id>http://nearsyh.me/2023/04/13/2023-04-13-FlyIO-Challenge-Txn/</id>
    <published>2023-04-12T16:00:00.000Z</published>
    <updated>2023-04-13T09:29:48.794Z</updated>
    
    <content type="html"><![CDATA[<p>最近完成了 <a href="https://fly.io/dist-sys">fly.io 推出的和分布式系统有关的一些挑战</a>，感觉挺有意思的。这篇文章分享一下最后一道题的思路。</p><h2 id="whats-the-problem"><a href="#whats-the-problem" class="headerlink" title="题目"></a><a href="#whats-the-problem">题目</a></h2><p>最后一道题的原文在<a href="https://fly.io/dist-sys/6a/">这里</a>。它要求我们实现一个支持 transaction, totally-available, read committed 的分布式 K/V 数据库。</p><h2 id="version-1"><a href="#version-1" class="headerlink" title="第一个版本的思路"></a><a href="#version-1">第一个版本的思路</a></h2><p>我的思路其实有点偷懒。因为这个挑战的框架提供了一些支持 linearizability 的中心化 KV 数据库（但是不支持 transaction），所以我就基于这个数据库实现了一个 WAL。每一次提交事务就只需要把事务里的所有写操作 append 到这个数据库里就行。对于读操作，我们可以通过 apply WAL 里面所有（可见的）写操作，来获取实际的数据。为了避免每次都 apply 全部的 WAL，每个 node 还会维护一个 snapshot。</p><p>这个版本已经可以在 QPS 比较低的情况下通过 part a 了。但是一旦 QPS 变高，最终的 WAL 会变得非常长，导致测试框架出问题。第二个版本解决了这个问题。</p><h2 id="version-2"><a href="#version-2" class="headerlink" title="第二个版本的思路"></a><a href="#version-2">第二个版本的思路</a></h2><p>既然问题是 WAL 太长，那我们定期把不需要的 WAL 清理掉不久好了。显然，如果一个 WAL entry 包含在所有 node 的 snapshot 中，我们就不再需要这个 entry 了。</p><p>为了判断 WAL entry 是否在所有 node 的 snapshot 中，在这个中心化数据库中我们为每一个 node 维护一个 watermark，watermark 之前的 entry 都已经包含在了这个 node 的 snapshot 中。通过计算这些 watermark 里面的最小值，我们可以知道哪些 WAL entry 可以被删除了。</p><h2 id="analysis"><a href="#analysis" class="headerlink" title="分析"></a><a href="#analysis">分析</a></h2><h3 id="transaction"><a href="#transaction" class="headerlink" title="Transaction"></a><a href="#transaction">Transaction</a></h3><p>为了保证事务的原子性，每个 WAL 的 entry 包含了一个事物所有的写操作。这样，当我们在内存里 apply 一个 WAL entry 的时候，就可以保证这个 entry 里面的所有写操作都会被 apply。这样，我们就可以保证事务的原子性。</p><h3 id="totally-availablility"><a href="#totally-availablility" class="headerlink" title="Totally Availablility"></a><a href="#totally-availablility">Totally Availablility</a></h3><p>这个系列的<a href="https://fly.io/dist-sys/2/">第二个挑战</a>解释了什么是 totally available： 即使出现了网络问题（network partition），服务还是能正常工作。按照我的理解，这里说的网络问题是指 node 之间无法通讯，但是 node 还是可以访问框架提供的 KV 数据库。显然，因为上面描述的思路不需要 node 之间进行通讯，所以它是 totally available 的。</p><h3 id="read-committed"><a href="#read-committed" class="headerlink" title="Read Committed"></a><a href="#read-committed">Read Committed</a></h3><p>因为只要插入 WAL 我们就认为一个 transaction 已经 commit 了，所以我们只能读到 committed 的修改。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近完成了 &lt;a href=&quot;https://fly.io/dist-sys&quot;&gt;fly.io 推出的和分布式系统有关的一些挑战&lt;/a&gt;，感觉挺有意思的。这篇文章分享一下最后一道题的思路。&lt;/p&gt;
&lt;h2 id=&quot;whats-the-problem&quot;&gt;&lt;a href=&quot;#wh</summary>
      
    
    
    
    <category term="技术" scheme="http://nearsyh.me/categories/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="分布式系统" scheme="http://nearsyh.me/categories/%E6%8A%80%E6%9C%AF/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="技术" scheme="http://nearsyh.me/tags/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="分布式系统" scheme="http://nearsyh.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    
  </entry>
  
  <entry>
    <title>小记</title>
    <link href="http://nearsyh.me/2022/10/13/2022-10-13-Random/"/>
    <id>http://nearsyh.me/2022/10/13/2022-10-13-Random/</id>
    <published>2022-10-12T16:00:00.000Z</published>
    <updated>2022-10-13T12:04:27.809Z</updated>
    
    <content type="html"><![CDATA[<p>昨天听日谈公园第 483 期，嘉宾是马伯庸。他们聊到说，在历史上大事件发生的时候，其实很多身在其中的普通人都意识不到。其中一个例子是卡夫卡日记里，有这样一段：“德国向俄国宣战了。今天下午有游泳课。” 这场战争现在被称为第一次世界大战。也许现在我们也处在一场巨变之中而不自知。当然这种“不知道”也许是一种好事。在 2019 年，我可能不会想知道 2022 年有很多人会被封在家里 3 个月，有人会因为回家奔丧写检查。从前我幻想要是自己穿越回古代，一定会因为观念的巨大不和而生气。其实现在好像也差不多。</p><p>早上听了日谈公园之后，当天就在通勤地铁上把马伯庸的《长安的荔枝》看完了，真是有种酣畅淋漓的感觉（可能映射到了什么吧…）。然后晚上就下单了他的新书《大医 - 破晓篇》，这个周末应该就能看完，可惜下半部分《日出篇》不知道还要都能多久。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;昨天听日谈公园第 483 期，嘉宾是马伯庸。他们聊到说，在历史上大事件发生的时候，其实很多身在其中的普通人都意识不到。其中一个例子是卡夫卡日记里，有这样一段：“德国向俄国宣战了。今天下午有游泳课。” 这场战争现在被称为第一次世界大战。也许现在我们也处在一场巨变之中而不自知。</summary>
      
    
    
    
    <category term="随笔" scheme="http://nearsyh.me/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="随笔" scheme="http://nearsyh.me/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>Uniswap 的工作原理</title>
    <link href="http://nearsyh.me/2022/01/23/2022-01-23-Uniswap/"/>
    <id>http://nearsyh.me/2022/01/23/2022-01-23-Uniswap/</id>
    <published>2022-01-22T16:00:00.000Z</published>
    <updated>2022-10-13T12:05:04.323Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://uniswap.org/">Uniswap</a> 是最近几年很火的代币交易协议。和币安，Coinbase 这种中心化交易所不同，Uniswap 提供了一种去中心化的代币间交易方式，背后有着简单但又非常有趣的数学原理。</p><h2 id="how-exchange-works"><a href="#how-exchange-works" class="headerlink" title="中心化交易所如何运作"></a><a href="#how-exchange-works">中心化交易所如何运作</a></h2><p>首先，我们来想象一下，一场交易是如何达成的。比如说，A 想用 100 元价格买一袋大米。他把这个意愿告诉了想要卖大米的人。这个时候，如果有另一个人也愿意把自己的大米用 100 元卖出去，那么交易就可以成立了。为了让交易更加容易达成，我们需要创造一个平台，让想要买卖大米的人把他们能接受的价格广播出去。现实中就有这样的平台，比如咸鱼，淘宝等等。</p><p>买卖大米之类的物品，淘宝咸鱼就可以了。但是如果要买卖股票，加密货币这种类型的资产，我们就需要更加高效的方式。这主要是因为两个原因：</p><ol><li>同一种资产，在同一个时间点，每一份是同质的。比如都是比特币，对于交易者来说，只要价格一样，A 的和 B 的没有区别。</li><li>这种类型的资产价格变化很快，不能再用这种人工的匹配方式。</li></ol><p>为了解决这个问题，就有了现在常见的*中心化交易所 (Centralized Exchange or CEX)*。所有想要买卖某种资产的人把自己想要的价格发布到交易所，交易所自动把买卖双方按照价格匹配起来。比如下图在交易平台非常常见。绿色表示买方的出价，红色表示卖房的要价。红绿两遍是不会有重合的，因为一旦有重合，系统就会匹配买方卖方达成交易，从图上移除。<br><img src="https://upload.wikimedia.org/wikipedia/commons/1/14/Order_book_depth_chart.gif" alt="order_book_depth_chart"></p><p>这种将买卖双发匹配起来的系统，也叫做 <em>Market Maker</em>。</p><h3 id="how-ce-increase-liquidity"><a href="#how-ce-increase-liquidity" class="headerlink" title="如何提高 Liquidity"></a><a href="#how-ce-increase-liquidity">如何提高 Liquidity</a></h3><p><em>Liquidity</em> 表示市场的流动性。具体来说，就是匹配买卖双方的难易程度。如果一个市场的 liquidity 很低，交易就很难达成。如果交易一直很难达成，交易者就不愿意来到这个市场交易，liquidity 就会更低。这样就成了一个负反馈。</p><p>因此，交易所会努力地提高自己的 liquidity。对于中心化交易所，它们增加 liquidity 的方式，就是依赖于职业交易者或者金融机构。这些人通过给出各种不同报价的方式与市场上的其他交易者交易，通过差价赚取收益，同时也增加了交易所的 liquidity。</p><h2 id="how-uniswap-works"><a href="#how-uniswap-works" class="headerlink" title="Uniswap 如何运作"></a><a href="#how-uniswap-works">Uniswap 如何运作</a></h2><p>上面介绍了传统中心化交易所的简单原理，接下来我们来看看 Uniswap 是如何工作的。Uniswap 经历了三个版本。虽然每个版本相对于之前都有一些新特性，但是它的核心思路在 V1 就奠定了。接下来，我们先介绍一下去中性化交易所的基本逻辑，然后再着重介绍 Uniswap 的工作原理。</p><h3 id="essentials-of-dex"><a href="#essentials-of-dex" class="headerlink" title="去中心化交易所的要素"></a><a href="#essentials-of-dex">去中心化交易所的要素</a></h3><p><em>去中心化交易所 (Decentralized Exchange or DEX)</em> 和传统的中心化交易所不同，不存在一个 owner，任何人都可以进入进行交易。我感觉这也是它为什么会收到加密货币狂热者的拥护。然而作为一个交易所，它还是需要保证两部分功能：</p><ol><li><strong>Market Maker</strong>: 由于是去中心化的，我们不再有一个平台来做 market maker。DEX 需要实现某种 *Automated Market Maker (AMM)*。</li><li><strong>Liquidity</strong>: 要让交易顺利进行，DEX 也需要保证有充足的 liquidity。</li></ol><p>一般来说，DEX 会通过经济收益（一般是交易费用）来吸引人们将自己持有的资产 deposit 到 DEX 中作为 <em>liquidity pool</em>。AMM 让交易者可以和 liquidity pool 直接并且随时做交易。</p><h3 id="uniswap-v1"><a href="#uniswap-v1" class="headerlink" title="Uniswap: x * y = k 模型"></a><a href="#uniswap-v1">Uniswap: x * y = k 模型</a></h3><p>$x * y = k$ 模型可以说是 Uniswap 的核心逻辑，从 V1 就开始使用了。之后的版本也都基于这个模型。这个模型的逻辑很简单，就是要保证 liquidity pool 中两种资产的数量乘积是一个常数。我们接下来用 ETH 和 USDT 这两种币之间的交易作为例子。为了理解起来更容易，以下都假设没有交易费用。</p><p>首先，需要有人愿意把自己持有的 ETH 和 USDT 按照特定的比例存到 Uniswap 的 liquidity pool 中（这个特定的比例就是 liquidiy pool 中当前 ETH 和 USDT 的比例）。每一个存入资产到 liquidity pool 的人，都可以获得 liquidity token。拥有 token 的数量占总数的比重决定了这个人能获得百分之多少的交易费用。</p><p>一旦我们有了这个 liquidity pool，交易者就可以进行交易了。比如说现在 pool 中有 100 个 ETH 和 200K 个 USDT，两者数额的乘积是 20M。我想用把 1 ETH 换成 USDT，那么首先我的这个 ETH 会进入这个 pool 中。此时 pool 里就会有 101 个 ETH，为了满足乘积为常数的条件，USDT 的数额应该约等于 198019.8。那么原本 200K 个 USDT 多出来的那部分（1980.2 USDT）就会成为我买到的 USDT。</p><p>可以看到，ETH 和 USDT 的交易价格和 liquidity pool 中两种资产数额的比值有关。<br>$$Price = \frac{\Delta USDT}{\Delta ETH} = \frac{USDT}{ETH + \Delta ETH}$$</p><h4 id="faq-v1-1"><a href="#faq-v1-1" class="headerlink" title="FAQ 1: 交易之后比例不是变了吗"></a><a href="#faq-v1-1">FAQ 1: 交易之后比例不是变了吗</a></h4><p>确实，交易之后两种资产的数额比例发生了变化，价格当然也变了。就拿上面的例子来说，交易完之后，ETH 和 USDT 的比值变大了。这个时候如果有人再用 1 个 ETH 买 USDT，获得的 USDT 的数额就会变小。这个问题有个专门的词叫 <em>slippage</em>。</p><p>这个价格的“不正确”虽然是个问题，但是在实际交易中几乎不会出现。还是拿上面这个例子来说，在我交易完之后，虽然 ETH 买 USDT 亏了，但是反过来却可以用更少的 USDT 去买 ETH。 因此一定会人来套利，因此价格会一直不断的围绕着市价波动。</p><p>另外，如果 liquidity pool 很大，其实小额的交易对价格的影响也很小。</p><h4 id="faq-v1-2"><a href="#faq-v1-2" class="headerlink" title="FAQ 2: 如果有人存入或者提取 liquidity，x * y 不就不是常数了吗"></a><a href="#faq-v1-2">FAQ 2: 如果有人存入或者提取 liquidity，x * y 不就不是常数了吗</a></h4><p>是的。$x * y$ 这个不变量在存入和提取 liquidity 的时候不需要保持不变。</p><h4 id="faq-v1-3"><a href="#faq-v1-3" class="headerlink" title="FAQ 3: 为什么存入或提取 liquidity 要按照比例存入两种资产"></a><a href="#faq-v1-3">FAQ 3: 为什么存入或提取 liquidity 要按照比例存入两种资产</a></h4><p>这是因为我们要保证这两种操作不会影响资产的价格。</p><h4 id="faq-v1-4"><a href="#faq-v1-4" class="headerlink" title="FAQ 4: 第一次存入 liquidity 怎么决定比例"></a><a href="#faq-v1-4">FAQ 4: 第一次存入 liquidity 怎么决定比例</a></h4><p>Uniswap 使用了一种非常巧妙的方式。它不强求第一次存入的比例，只是定义了第一次存入能获得的 liquidity token 是两种资产数额乘积的根号值。这样，如果存入者想要用最少的资金获得最多的 token，就需要按照市价对应的比例存入，我们来简单证明一下。比如说 1 个 ETH 值 400 USDT，那么为了获得 20 个 token，需要存入 $x(ETH) + \frac{400}{x}(USDT)$。把 ETH 代换成 400 USDT 就可以得到，为了获得 20 个 token，需要付出等价于 $400x + \frac{400}{x}$ USDT 的资产。显然当 $x = 1$ 时，这个值最小。也就是说，liquidity provider 会收到利益的驱使，用合适的比例存入。</p><p>当然，如果有人非要瞎鸡儿存，也会有套利的人让这个比例恢复正确。</p><h2 id="summary"><a href="#summary" class="headerlink" title="总结"></a><a href="#summary">总结</a></h2><p>Uniswap 的基本原理大概就是这样。一个简单的公式，再加上一点基本的博弈论，就能构建出一个非常实用的交易所。不过吧，我还是很难理解为什么不使用中心化交易所来兑换。在写这篇博客的时候，Uniswap 的平均 gas 数量大约是 140K，一个 gas 算 90 Gwei 的话，一次 swap 单是 gas fee 就要 0.013 ETH。即使在币圈大跌的今天，也要 30 多美金。这还没有算上千分之三的 fee。如果从自己的钱包转到交易所，兑换之后再转回来，总共费用可能也就几美金。这么对比下来，还是 CEX 真香警告吧？你觉得呢？</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;a href=&quot;https://uniswap.org/&quot;&gt;Uniswap&lt;/a&gt; 是最近几年很火的代币交易协议。和币安，Coinbase 这种中心化交易所不同，Uniswap 提供了一种去中心化的代币间交易方式，背后有着简单但又非常有趣的数学原理。&lt;/p&gt;
&lt;h2 id</summary>
      
    
    
    
    <category term="数学" scheme="http://nearsyh.me/categories/%E6%95%B0%E5%AD%A6/"/>
    
    <category term="技术" scheme="http://nearsyh.me/categories/%E6%95%B0%E5%AD%A6/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="Crypto" scheme="http://nearsyh.me/categories/%E6%95%B0%E5%AD%A6/%E6%8A%80%E6%9C%AF/Crypto/"/>
    
    
    <category term="技术" scheme="http://nearsyh.me/tags/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="数学" scheme="http://nearsyh.me/tags/%E6%95%B0%E5%AD%A6/"/>
    
    <category term="Crypto" scheme="http://nearsyh.me/tags/Crypto/"/>
    
  </entry>
  
  <entry>
    <title>怎么推导出 Y Combinator</title>
    <link href="http://nearsyh.me/2021/09/19/2021-09-19-y-combinator/"/>
    <id>http://nearsyh.me/2021/09/19/2021-09-19-y-combinator/</id>
    <published>2021-09-18T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.530Z</updated>
    
    <content type="html"><![CDATA[<p>Y 组合子 (Y Combinator) 可以帮助我们在 lambda 演算中实现递归函数。它的形式很简单，想要背下来也很容易。它的形式又很复杂，让你不明白它为什么长这样。</p><h2 id="why-combinator"><a href="#why-combinator" class="headerlink" title="递归函数不是很好实现吗"></a><a href="#why-combinator">递归函数不是很好实现吗</a></h2><p>在很多编程语言中，递归函数很好实现。比如大家喜闻乐见的 <code>Java</code>，要用递归的方式实现阶乘可以写成这样：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">fact</span><span class="params">(<span class="keyword">int</span> n)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> n == <span class="number">0</span> ? <span class="number">1</span> : n * fact(n - <span class="number">1</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然而，在 lambda 演算中，所有的函数都是匿名函数。因此，我们没有办法在函数<code>fact</code>中调用它自己。当然，我们可以通过对函数本身做一些改动来实现递归。但是这个改动可能并不那么通用，可读性也会受到影响。而 Y 组合子，则提供了一种通用的方法，可以让一个按照某种(容易 follow 的)规则实现的函数递归起来。</p><h2 id="how"><a href="#how" class="headerlink" title="再一次发现 Y 组合子"></a><a href="#how">再一次发现 Y 组合子</a></h2><p>Y 组合子的定义如下：</p><p>$$\lambda f.(\lambda x. f \space (x \space x)) (\lambda x. f \space (x \space x))$$</p><p>如果你把它 apply 到一个函数 g 上，你会发现 $$ (Y g) = (g \space (Y g)) $$</p><p>这也就意味着它实现了递归。只看它的定义，你可能很难理解为什么它可以做到这一点，以及发现它的人脑洞得有多大。我们今天就来一步一步的，再一次发现 Y 组合子。</p><h3 id="attempt-1"><a href="#attempt-1" class="headerlink" title="把自己当作函数的参数"></a><a href="#attempt-1">把自己当作函数的参数</a></h3><p>我们不是没法在自己的函数体内引用自己吗？那我们就把自己当作参数传进去。我们还是用阶乘举例子。首先我们用不正规的 lambda 演算定义 $f$：$$ \lambda n. n = 0\space?\space1 : n * (f \space n) $$。之所以说不正规，除了语法之外，主要是因为 $f$ 的函数体内引用了 $f$。</p><p>现在，我们给 $f$ 加一个参数的到 $f’$：</p><p>$$ f’ = \lambda s. \lambda n. n = 0\space?\space1 : n * ((s \space s) \space n)$$</p><p>这样 $ (f’ \space f’) $ 就等价于原本的 $f$ 了。</p><h3 id="attempt-2"><a href="#attempt-2" class="headerlink" title="把实际的逻辑提取出来"></a><a href="#attempt-2">把实际的逻辑提取出来</a></h3><p>现在，我们实现了递归。但是这种方式不够直观。我们在实际递归调用的时候，需要用 $(s \space s)$ 这种略显奇怪的形式。现在我们先做一件事，把实际的逻辑提取出来。</p><p>$$ f’ = \lambda s. \lambda n. ((\lambda g. \lambda m. m = 0\space?\space1 : m * (g \space m)) \space (s \space s)) \space n) $$</p><p>这个改动的核心在于把实际的逻辑抽出来了。有了上面这种形式，我们再把实际逻辑当作参数（我们姑且用 $fr$ 表示上面新加的 $\lambda g. \lambda m. m = 0\space?\space1 : m * (g \space m)$ ），就可以得到：</p><p>$$ f’’ = \lambda h. \lambda s. \lambda n. ((h \space (s \space s)) \space n) = \lambda h. \lambda s. h \space (s \space s)$$</p><p>$$ f’ = (f’’ fr) $$</p><p>因为 $(f’ \space f’) = f$，所以我们得到：</p><p>$$ ((f’’ \space fr) (f’’ \space fr)) = f $$</p><p>此时 $f’’$ 和我们想实现的阶乘已经是独立的了，等式左边只有 $fr$ 和阶乘有关。我们再把 $fr$ 当作参数抽取出来，等式左边就变成了：</p><p>$$ \lambda g. ((f’’ g) (f’’ g)) = \lambda g. (\lambda s. g \space (s \space s)) (\lambda s. g \space (s \space s)) = Y $$ </p><p>有了这个万能的 Y 之后，我们定义阶乘的方式就变成了上面提到的 $fr$，也就是：</p><p>$$ \lambda g. \lambda m. m = 0\space?\space1 : m * (g \space m) $$</p><p>我相信任何能用 Java 实现阶乘的人，都可以轻松地写出并理解上面的代码。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Y 组合子 (Y Combinator) 可以帮助我们在 lambda 演算中实现递归函数。它的形式很简单，想要背下来也很容易。它的形式又很复杂，让你不明白它为什么长这样。&lt;/p&gt;
&lt;h2 id=&quot;why-combinator&quot;&gt;&lt;a href=&quot;#why-combinat</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Continuation 从入门到放弃</title>
    <link href="http://nearsyh.me/2021/09/13/2021-09-13-Continuation/"/>
    <id>http://nearsyh.me/2021/09/13/2021-09-13-Continuation/</id>
    <published>2021-09-12T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.530Z</updated>
    
    <content type="html"><![CDATA[<p>最近又捡起了一本关于 List 的书，之前只看了一点就放弃了。当时我在读到 continuation 的时候，整个人都有点懵逼了。刚刚又重新读到这部分，终于好像来感觉了。</p><h2 id="what-is-continuation"><a href="#what-is-continuation" class="headerlink" title="什么是 Continuation"></a><a href="#what-is-continuation">什么是 Continuation</a></h2><p>简单来说，某个位置的 <code>continuation</code> 可以理解成在这个位置要做的运算。比如在 <code>(+ 1 2)</code> 这个例子里，不严谨地说，对于 <code>2</code> 来说，它所处的 <code>continuation</code> 就是把它和 1 相加。如果我们有办法把这个 <code>continuation</code> 抽取并保存下来，我们就可以把它运用(apply)到其他值上来执行相同的运算。</p><h2 id="how-to-extract-continuation"><a href="#how-to-extract-continuation" class="headerlink" title="如何获取 Continuation"></a><a href="#how-to-extract-continuation">如何获取 Continuation</a></h2><p>那么现在问题来了，我们要怎么获得一个位置的 <code>continuation</code> 呢？接下来的这几句话有点绕，我们一句句看。</p><ul><li>Scheme 本身提供了 <code>call/cc</code> 函数。它接受一个函数作为参数。我们姑且叫这个函数为 <code>cc-consumer</code>。<code>cc</code> 是 current continuation 的缩写。</li><li>这个函数 <code>cc-consumer</code> 就和它的名字一样，也接受一个参数，这个参数就是当前的 <code>continuation</code>，也就是 <code>cc</code>。</li><li><code>cc</code> 就像上面说的，可以被理解成某个位置接下来要做的运算。这也意味着，<code>cc</code> 也是一个函数，它接受一个参数作为这个位置上的值。它一旦被调用，就会回到它对应的位置，并且把它的参数放在这个位置，然后进行接下来的操作。</li></ul><p>说了这么多，其实也很难有个直观的了解。让我们用伪代码来描述一下这些函数和参数的类型。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">T call/cc(ContinuationConsumer cc_consumer);</span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> ContinuationConsumer = function&lt;T(Continuation)&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> Continuation = function&lt;<span class="keyword">void</span>(T)&gt;;</span><br></pre></td></tr></table></figure><p>接下来，我们来看一个简单的例子。这个例子使用了 <code>call/cc</code> 来实现最开始我们提到的 <code>add1</code> 这个操作。你可以使用 Chez Scheme 来执行下面的代码。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">define</span></span> add1 <span class="literal">#f</span>)</span><br><span class="line"></span><br><span class="line">(<span class="name"><span class="builtin-name">+</span></span></span><br><span class="line">  <span class="number">1</span></span><br><span class="line">  (<span class="name"><span class="builtin-name">call/cc</span></span></span><br><span class="line">    (<span class="name"><span class="builtin-name">lambda</span></span> (cc)</span><br><span class="line">            (<span class="name"><span class="builtin-name">set!</span></span> add1 cc)</span><br><span class="line">            <span class="number">0</span></span><br><span class="line">    )</span><br><span class="line">  )</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">(<span class="name">add1</span> <span class="number">10</span>) <span class="comment">; =&gt; 11</span></span><br><span class="line">(<span class="name">add1</span> <span class="number">20</span>) <span class="comment">; =&gt; 21</span></span><br></pre></td></tr></table></figure><ul><li>在上面代码的第四行，我们通过调用 <code>call/cc</code> 来获取当前的 <code>continuation</code>。</li><li><code>call/cc</code> 接受一个用户自定义的函数作为参数，并且会把当前的 continuation 传给这个函数作为它的参数。简单来说，<code>call/cc</code> 接受一个回调函数，并把 <code>continuation</code> 传给回调函数。</li><li>在回调函数中，我们把 <code>continuation</code> 保存到了 <code>add1</code> 这个变量中，之后我们就可以使用它来访问这个 <code>continuation</code>。</li></ul><p>在上面代码的最后两行，我们通过 <code>add1</code> 来使用保存下来的 <code>continuation</code>。当我们使用它的时候，程序会把传给它的参数，放到 <code>call/cc</code> 对应的位置上然后继续执行。显然，<code>(add1 10)</code> 就会执行 <code>(+ 1 10) =&gt; 11</code>。</p><h2 id="why-continuation"><a href="#why-continuation" class="headerlink" title="为什么需要 Continuation"></a><a href="#why-continuation">为什么需要 Continuation</a></h2><p>我们废了半天劲理解了 <code>continuation</code> 是什么（甚至可能没有理解…)，但是问题来了。为什么我们需要它呢？<code>continuation</code> 可以帮我们做很多事，比如提前返回。提前返回听上去好像很普通（比如 Java 里一个 return 就好了），但是 Lisp 其实没有很简单的办法。有了 <code>continuation</code>，我们就可以实现这一点。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">define</span></span> (<span class="name">find</span> lst elem)</span><br><span class="line">  (<span class="name"><span class="builtin-name">call/cc</span></span> (<span class="name"><span class="builtin-name">lambda</span></span> (return)</span><br><span class="line">    (<span class="name"><span class="builtin-name">for-each</span></span></span><br><span class="line">      (<span class="name"><span class="builtin-name">lambda</span></span> (e)</span><br><span class="line">              (<span class="name"><span class="builtin-name">if</span></span> (<span class="name"><span class="builtin-name">eq?</span></span> elem e)</span><br><span class="line">                  (<span class="name">return</span> <span class="symbol">&#x27;found</span>)</span><br><span class="line">                  <span class="symbol">&#x27;nil</span>))</span><br><span class="line">      lst)</span><br><span class="line">    (<span class="name">return</span> <span class="symbol">&#x27;not-found</span>)</span><br><span class="line">    )))</span><br></pre></td></tr></table></figure><p>因为 <code>call/cc</code> 的调用在最外层，所以这个位置的 <code>continuation</code> 就是什么也不做，直接返回。而这个 <code>continuation</code> 被传递给参数 <code>return</code>，这样当我们想返回时，调用 <code>return</code> 即可。</p><h2 id="generator"><a href="#generator" class="headerlink" title="更复杂的例子: Generator"></a><a href="#generator">更复杂的例子: Generator</a></h2><p>上面的例子比较简单，我们来看一个比较复杂的例子，generator。我们想实现一个函数，每次调用它都会依次返回给定 list 中的元素。这样的函数其实也可以通过闭包来实现，但是我们今天试试用 <code>call/cc</code> 来实现它。</p><h3 id="attempt-1"><a href="#attempt-1" class="headerlink" title="第一次尝试"></a><a href="#attempt-1">第一次尝试</a></h3><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">define</span></span> (<span class="name">new-generator</span> lst)</span><br><span class="line">  (<span class="name"><span class="builtin-name">define</span></span> (<span class="name">cc-consumer</span> return)</span><br><span class="line">    (<span class="name"><span class="builtin-name">for-each</span></span></span><br><span class="line">      (<span class="name"><span class="builtin-name">lambda</span></span> (e)</span><br><span class="line">        (<span class="name">return</span> e))</span><br><span class="line">      lst))</span><br><span class="line">  (<span class="name"><span class="builtin-name">define</span></span> (<span class="name">generator</span>)</span><br><span class="line">    (<span class="name"><span class="builtin-name">call/cc</span></span> cc-consumer))</span><br><span class="line">  generator)</span><br><span class="line"></span><br><span class="line">(<span class="name"><span class="builtin-name">define</span></span> generator (<span class="name">new-generator</span> &#x27;(<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>)))</span><br></pre></td></tr></table></figure><p>先别急着说”第一次尝试”就这么复杂。上面大部分的代码其实都是模版代码。</p><p>先看第一行，因为我们想要实现一个可以根据指定 list 创建 generator 的函数，所以很自然的，我们定义一个接受 lst 作为参数的 <code>new-generator</code>。这个函数应该返回一个 generator，也是一个函数。所以在 7～9 行，我们定义一个函数，并且返回它。</p><p>然后我们再看第8行。显然今天的主角是 <code>call/cc</code>，所以我们这里怎么也得用一下它对吧。我们定义了 <code>cc-consumer</code> 这个函数作为 <code>call/cc</code> 的参数。</p><p>最后我们看第一个版本的核心部分，第2～6行。在 <code>cc-consumer</code> 中，我们遍历了传入的 <code>lst</code>，每次都调用 <code>return</code> 返回每个元素。</p><p>显然这次尝试是失败的。每次调用 <code>generator</code> 都只会返回列表中的第一个元素。这是因为我们目前只通过 <code>continuation</code> 实现了提前返回的功能。我们还需要实现在上一次返回的位置接着执行下去。</p><h3 id="attempt-2"><a href="#attempt-2" class="headerlink" title="第二次尝试"></a><a href="#attempt-2">第二次尝试</a></h3><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">define</span></span> (<span class="name">new-generator</span> lst)</span><br><span class="line"> (<span class="name"><span class="builtin-name">define</span></span> (<span class="name">cc-consumer</span> return)</span><br><span class="line">  (<span class="name"><span class="builtin-name">for-each</span></span></span><br><span class="line">   (<span class="name"><span class="builtin-name">lambda</span></span> (e)</span><br><span class="line">    (<span class="name"><span class="builtin-name">call/cc</span></span> (<span class="name"><span class="builtin-name">lambda</span></span> (resume)             <span class="comment">; new</span></span><br><span class="line">              (<span class="name"><span class="builtin-name">set!</span></span> cc-consumer resume)   <span class="comment">; new</span></span><br><span class="line">              (<span class="name">return</span> e))))               <span class="comment">; new</span></span><br><span class="line">   lst))</span><br><span class="line"> (<span class="name"><span class="builtin-name">define</span></span> (<span class="name">generator</span>)</span><br><span class="line">  (<span class="name"><span class="builtin-name">call/cc</span></span> cc-consumer))</span><br><span class="line"> generator)</span><br><span class="line"></span><br><span class="line">(<span class="name"><span class="builtin-name">define</span></span> generator (<span class="name">new-generator</span> &#x27;(<span class="number">1</span> <span class="number">2</span> <span class="number">3</span>)))</span><br></pre></td></tr></table></figure><p>第二次尝试，我们改动了三行。因为我们想让每次调用 <code>generator</code> 都会回到上次返回的地方，因为我们希望能够把上次返回的位置的 <code>continuation</code> 保存下来。因此，我们把原本第一个版本调用 <code>return</code> 的地方，改成了调用 <code>call/cc</code>。然后在第6行，我们把这个位置的 <code>continuation</code> 赋值给 <code>cc-consumer</code>。这样，当我们再次调用 <code>generator</code> 的时候，我们会回到第6行，然后执行下一次循环。</p><p>用这个版本，我们的 <code>generator</code> 运行的很好。每次调用都正确的返回了下一个元素。但是这个版本其实有一个严重的 bug。为了暴露出这个 bug，我们现在换一下需求，不再直接返回元素，而是做一些改动。如果是第奇数个元素，就直接返回；否则就返回它的相反数。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">define</span></span> (<span class="name">new-generator</span> lst)</span><br><span class="line"> (<span class="name"><span class="builtin-name">define</span></span> (<span class="name">cc-consumer</span> return)</span><br><span class="line">  (<span class="name"><span class="builtin-name">for-each</span></span></span><br><span class="line">   (<span class="name"><span class="builtin-name">lambda</span></span> (e)</span><br><span class="line">    (<span class="name"><span class="builtin-name">call/cc</span></span> (<span class="name"><span class="builtin-name">lambda</span></span> (resume)</span><br><span class="line">              (<span class="name"><span class="builtin-name">set!</span></span> cc-consumer resume)</span><br><span class="line">              (<span class="name">return</span> e))))</span><br><span class="line">   lst))</span><br><span class="line"></span><br><span class="line"> (<span class="name"><span class="builtin-name">let</span></span> ((<span class="name">index</span> <span class="number">0</span>))</span><br><span class="line">  (<span class="name"><span class="builtin-name">define</span></span> (<span class="name">generator</span>)</span><br><span class="line">       (<span class="name"><span class="builtin-name">set!</span></span> index (<span class="name"><span class="builtin-name">+</span></span> index <span class="number">1</span>))</span><br><span class="line">       (<span class="name"><span class="builtin-name">if</span></span> (<span class="name"><span class="builtin-name">eq?</span></span> (<span class="name">mod</span> times <span class="number">2</span>) <span class="number">0</span>)</span><br><span class="line">           (<span class="name"><span class="builtin-name">-</span></span> <span class="number">0</span> (<span class="name"><span class="builtin-name">call/cc</span></span> control-state))</span><br><span class="line">           (<span class="name"><span class="builtin-name">call/cc</span></span> control-state)))</span><br><span class="line">  generator))</span><br></pre></td></tr></table></figure><p>上面的版本尝试实现新的需求。它的主要改动在于我们维护一个 <code>index</code> 来判断当前是第几个元素，然后决定是否取反。这个改动的目的主要是要构造出两个不同的 <code>continuation</code>，也就是第14和15行。我们通过多次调用 <code>generator</code> 发现，每次都返回了元素本身，不管它是第几个元素。之所以这样，就是因为 <code>cc-consumer</code> 的参数 <code>return</code> 没有被更新过，永远都是用第一次调用 <code>cc-consumer</code> 时的 continuation，也就是不取反的情况。</p><h3 id="final-attempt"><a href="#final-attempt" class="headerlink" title="最后的版本"></a><a href="#final-attempt">最后的版本</a></h3><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">(<span class="name"><span class="builtin-name">define</span></span> (<span class="name">new-generator</span> lst)</span><br><span class="line"> (<span class="name"><span class="builtin-name">define</span></span> (<span class="name">cc-consumer</span> return)</span><br><span class="line">  (<span class="name"><span class="builtin-name">for-each</span></span></span><br><span class="line">   (<span class="name"><span class="builtin-name">lambda</span></span> (e)</span><br><span class="line">    (<span class="name"><span class="builtin-name">set!</span></span> return                                 <span class="comment">; new</span></span><br><span class="line">          (<span class="name"><span class="builtin-name">call/cc</span></span> (<span class="name"><span class="builtin-name">lambda</span></span> (resume)</span><br><span class="line">                     (<span class="name"><span class="builtin-name">set!</span></span> cc-consumer resume)</span><br><span class="line">                     (<span class="name">return</span> e))))</span><br><span class="line">    )                                            <span class="comment">; new</span></span><br><span class="line">   lst))</span><br><span class="line"></span><br><span class="line"> (<span class="name"><span class="builtin-name">let</span></span> ((<span class="name">index</span> <span class="number">0</span>))</span><br><span class="line">  (<span class="name"><span class="builtin-name">define</span></span> (<span class="name">generator</span>)</span><br><span class="line">       (<span class="name"><span class="builtin-name">set!</span></span> index (<span class="name"><span class="builtin-name">+</span></span> index <span class="number">1</span>))</span><br><span class="line">       (<span class="name"><span class="builtin-name">if</span></span> (<span class="name"><span class="builtin-name">eq?</span></span> (<span class="name">mod</span> times <span class="number">2</span>) <span class="number">0</span>)</span><br><span class="line">           (<span class="name"><span class="builtin-name">-</span></span> <span class="number">0</span> (<span class="name"><span class="builtin-name">call/cc</span></span> control-state))</span><br><span class="line">           (<span class="name"><span class="builtin-name">call/cc</span></span> control-state)))</span><br><span class="line">  generator))</span><br></pre></td></tr></table></figure><p>为了解决上面发现的问题，我们需要更新 <code>return</code> 的值。</p><p>当我们第一次调用 <code>generator</code> 的时候，因为 <code>index</code> 是 1，所以第 17 行会被执行，这是，<code>return</code> 的值会是第 17 行对应的 <code>continuation</code>。当这次调用完成之后，<code>cc-consumer</code> 已经被更新成 <code>resume</code> 了，也就是第6行对应的 <code>continuation</code>。这时 <code>return</code> 还没有更新，我们又回到第 17 行，然后 <code>generator</code> 返回，第一次执行结束。</p><p>当我们再一次调用 <code>generator</code> 的时候，我们会触发第16行的 <code>call/cc</code>，而这次的参数是更新后的 <code>cc-consumer</code>，也就是 <code>resume</code>。这次调用会让我们回到第6行。这时，第6行的返回值，也就是第16行的 <code>continuation</code>。它会被赋值给 <code>return</code>。赋值完之后，我们开始第二次循环，这次，我们会返回第二个元素。注意，因为 <code>return</code> 是第16行的 <code>continuation</code>，我们会正确地执行取反的操作。</p><h2 id="summary"><a href="#summary" class="headerlink" title="总结"></a><a href="#summary">总结</a></h2><p>我个人觉得 <code>continuation</code> 很绕，很难解释地清楚。很多的文章都直接把一个完成的例子抛出来，就算读者理解了这么写能够 work，但是还是不知道为什么要这么写。我把 <code>generator</code> 这个例子拆成了三个阶段，来解释每一部分的代码写成那样的原因，希望能够为你理解 continuation 做一点微小的贡献。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近又捡起了一本关于 List 的书，之前只看了一点就放弃了。当时我在读到 continuation 的时候，整个人都有点懵逼了。刚刚又重新读到这部分，终于好像来感觉了。&lt;/p&gt;
&lt;h2 id=&quot;what-is-continuation&quot;&gt;&lt;a href=&quot;#what-is</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Dapr 学习笔记 3 - Runtime</title>
    <link href="http://nearsyh.me/2021/03/07/2021-03-07-Dapr-Runtime/"/>
    <id>http://nearsyh.me/2021/03/07/2021-03-07-Dapr-Runtime/</id>
    <published>2021-03-06T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.530Z</updated>
    
    <content type="html"><![CDATA[<p>我感觉 runtime 应该是 dapr 的核心。从 runtime 的代码入手，我们可以更快的把握 dapr 的整体思路。</p><h2 id="initialize-runtime"><a href="#initialize-runtime" class="headerlink" title="runtime 的初始化"></a><a href="#initialize-runtime">runtime 的初始化</a></h2><p>说实话我感觉 dapr 的核心逻辑并不是特别复杂。初始化 runtime 无非就是读取 components 配置，然后根据配置初始化所有的组件。通过阅读 <code>pkg/runtime/runtime.go</code> 的 <code>Run</code> 函数，就可以验证我们的想法。<code>Run</code> 和 <code>initRuntime</code> 的主干如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(a *DaprRuntime)</span> <span class="title">Run</span><span class="params">(opts ...Option)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">    <span class="comment">// 创建 options</span></span><br><span class="line">    <span class="keyword">var</span> o runtimeOpts</span><br><span class="line">    <span class="keyword">for</span> _, opt := <span class="keyword">range</span> opts &#123;</span><br><span class="line">        opt(&amp;o)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 初始化 runtime</span></span><br><span class="line">    err := a.initRuntime(&amp;o)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(a *DaprRuntime)</span> <span class="title">initRuntime</span><span class="params">(opts *runtimeOpts)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">    <span class="comment">// 注册支持的各种类型的 component</span></span><br><span class="line">    a.xxxRegistry.Register(opts.xxx...)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 读取 components 配置，加载 components</span></span><br><span class="line">    <span class="comment">// go routine 负责不断地处理 pendingComponents 这个 channel 中的数据</span></span><br><span class="line">    <span class="keyword">go</span> a.processComponents()</span><br><span class="line">    <span class="comment">// loadComponents 则是读取 opts 中指定的配置文件，将要加载的 component </span></span><br><span class="line">    <span class="comment">// 写入 pendingComponents</span></span><br><span class="line">err = a.loadComponents(opts)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动 http 和 grpc server</span></span><br><span class="line">    a.startGRPCAPIServer(...)</span><br><span class="line">    a.startHTTPServer(...)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 到这里 dapr 自己需要做的初始化工作都已经完成了，</span></span><br><span class="line">    <span class="comment">// 接下来是和实际的应用程序做初始化。</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 等应用程序 ready，通过不断和 app 建立 tcp 连接实现</span></span><br><span class="line">    a.blockUntilAppIsReady()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 和应用程序建立连接</span></span><br><span class="line">    a.createAppChannel()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动 actor</span></span><br><span class="line">    a.initActors()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 开始 subscribing，开始从 binding reading</span></span><br><span class="line">    a.startSubscribing()</span><br><span class="line">a.startReadingFromBindings()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="registry-factory"><a href="#registry-factory" class="headerlink" title="Registry - 工厂集合"></a><a href="#registry-factory">Registry - 工厂集合</a></h2><p>Registry 为 component 提供了各种实现类型的注册。每一种 component 的每一个实现，都需要提供工厂方法。<code>daprd</code> 在创建 runtime 的时候，会将所有支持的 component 实现都以参数的形式传入。在上面描述的 <code>initRuntime</code> 方法中，又会把这些传入的实现注册到 registry 中。在我们处理 component 的时候，会根据配置中指定的类型，寻找对应的工厂方法并加以调用。下面，我们用 state store 为例子，看看具体的调用链路：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 在 initRuntime 中被调用，处理 pendingComponents </span></span><br><span class="line"><span class="comment">// 中待处理的 component 配置。</span></span><br><span class="line">processComponents() &#123;</span><br><span class="line">    <span class="comment">// -&gt; processComponentAndDependents() </span></span><br><span class="line">    <span class="comment">// -&gt; doProcessOneComponent()</span></span><br><span class="line">    <span class="keyword">switch</span> category &#123;</span><br><span class="line">    <span class="keyword">case</span> stateComponent:</span><br><span class="line">        <span class="keyword">return</span> a.initState(comp)</span><br><span class="line">    <span class="comment">// 其他类型</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">initState() &#123;</span><br><span class="line">    <span class="comment">// 调用 registry 的 create 方法。</span></span><br><span class="line">    <span class="comment">// 它的实现就是从内存中维护的 map 中获取对应的工厂方法，然后调用它</span></span><br><span class="line">    store, err := a.stateStoreRegistry.Create(s.Spec.Type, s.Spec.Version) &#123;</span><br><span class="line">       <span class="keyword">if</span> method, ok := s.stateStores[name]; ok &#123;</span><br><span class="line"><span class="keyword">return</span> method(), <span class="literal">nil</span></span><br><span class="line">   &#125; </span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 用 component 配置中的参数初始化</span></span><br><span class="line">    store.Init(...)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="subscribing"><a href="#subscribing" class="headerlink" title="Subscribing - 屏蔽消息实现"></a><a href="#subscribing">Subscribing - 屏蔽消息实现</a></h2><p>在 <code>initRuntime</code> 的最后，它调用了 <code>startSubscribing</code> 和 <code>startReadingFromBindings</code>。这两个我个人觉得有点类似。binding 更多的是为了建立和外部（i.e. 不被 dapr 所管理）服务通讯通道，例如读或者写一个外部部署的 Kafka。如果这个 Kafka 已经包含在了 component 配置中，我们则应该使用 subscribing。</p><p><code>startSubscribing</code> 主要做的事情就是遍历所有注册了的 <code>pubsub</code>，对于每一个订阅的 topic，就启动一个 go routine。这个 go routine 会不断地将收到的消息通过 RPC 调用，转发给应用程序。应用程序只需要提供一个回调接口即可。这样设计的一个好处就是，应用程序完全不需要考虑底层的实现到底是 pull 还是 push 模式，dapr 会自动使用对应的模式获取消息进行转发。</p><h2 id="summary"><a href="#summary" class="headerlink" title="总结"></a><a href="#summary">总结</a></h2><p>runtime 的结构非常简单。它的主要职责就是根据配置来创建各种 component，从而构建完整的运行时环境。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;我感觉 runtime 应该是 dapr 的核心。从 runtime 的代码入手，我们可以更快的把握 dapr 的整体思路。&lt;/p&gt;
&lt;h2 id=&quot;initialize-runtime&quot;&gt;&lt;a href=&quot;#initialize-runtime&quot; class=&quot;heade</summary>
      
    
    
    
    <category term="技术" scheme="http://nearsyh.me/categories/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="分布式" scheme="http://nearsyh.me/categories/%E6%8A%80%E6%9C%AF/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
    
    <category term="技术" scheme="http://nearsyh.me/tags/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="Dapr" scheme="http://nearsyh.me/tags/Dapr/"/>
    
    <category term="分布式" scheme="http://nearsyh.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>Dapr 学习笔记 2 - Code Map</title>
    <link href="http://nearsyh.me/2021/03/06/2021-03-06-Dapr-Code-Map/"/>
    <id>http://nearsyh.me/2021/03/06/2021-03-06-Dapr-Code-Map/</id>
    <published>2021-03-05T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.529Z</updated>
    
    <content type="html"><![CDATA[<p>在进一步理解 Dapr，我们先来大概看一下它的 code base，梳理一下它的代码地图，从而对它的结构有个 high-level 的理解。</p><h2 id="directory-structure"><a href="#directory-structure" class="headerlink" title="目录结构"></a><a href="#directory-structure">目录结构</a></h2><p>Dapr 的代码包含了几个主要的顶层目录：</p><ol><li><code>cmd</code>: 包含了几个主要的可执行程序。</li><li><code>dapr</code>: 包含了 dapr 使用到的 protobuf 的定义。这些 protobuf 主要是用于 RPC 使用的数据结构定义。</li><li><code>pkg</code>: 包含了 dapr 的核心代码。</li></ol><h2 id="executables"><a href="#executables" class="headerlink" title="cmd - 可执行程序"></a><a href="#executables">cmd - 可执行程序</a></h2><p>Dapr 包含了若干个可执行程序，它们的代码在 <code>cmd</code> 这个路径下。其中最重要的就是下面 <code>daprd</code>。它就是 dapr 提供的运行时环境，也就是实际的 sidecar。</p><p>除此之外，还有以下几个不是特别核心的：</p><ol><li><code>placement</code>：用于在“放置” <code>actor</code>，比如在适合的 pod 上运行 actor。</li><li><code>injector</code> 和 <code>operator</code>：这是 dapr 在 K8s 环境中部署是使用的。</li><li><code>sentry</code>：TLS 的 CA。dapr 的 sidecar 之间的 TLS 通讯会使用到它。</li></ol><h3 id="daprd-vs-dapr"><a href="#daprd-vs-dapr" class="headerlink" title="daprd v.s. dapr"></a><a href="#daprd-vs-dapr">daprd v.s. dapr</a></h3><p>在看 dapr 文档的时候，相信大家也都使用了 <code>dapr</code> 这个命令行工具。要注意它和 <code>daprd</code> 是有区别的，并且它的代码在单独的 <a href="https://www.github.com/dapr/cli">repo</a> 中。<br><code>daprd</code> 是实际的运行时环境。而 <code>dapr</code> 类似一个 devops 工具。在我们执行 <code>dapr run ...</code> 的时候，它会同时执行 <code>daprd</code> 和我们指定的应用程序。</p><h2 id="code-code"><a href="#code-code" class="headerlink" title="pkg - 核心代码"></a><a href="#code-code">pkg - 核心代码</a></h2><p>这个目录包含了 dapr 的核心代码。其中 runtime 是核心中的核心。其他目录则包含了 dapr 的各个组件的代码。</p><h2 id="summary"><a href="#summary" class="headerlink" title="总结"></a><a href="#summary">总结</a></h2><p>Dapr 的代码结构还是比较清晰的。之后我会从 runtime 开始，研究一下 dapr 大概的实现原理。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在进一步理解 Dapr，我们先来大概看一下它的 code base，梳理一下它的代码地图，从而对它的结构有个 high-level 的理解。&lt;/p&gt;
&lt;h2 id=&quot;directory-structure&quot;&gt;&lt;a href=&quot;#directory-structure&quot; cl</summary>
      
    
    
    
    <category term="技术" scheme="http://nearsyh.me/categories/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="分布式" scheme="http://nearsyh.me/categories/%E6%8A%80%E6%9C%AF/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
    
    <category term="技术" scheme="http://nearsyh.me/tags/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="Dapr" scheme="http://nearsyh.me/tags/Dapr/"/>
    
    <category term="分布式" scheme="http://nearsyh.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>Dapr 学习笔记 1 - 初探</title>
    <link href="http://nearsyh.me/2021/02/26/2021-02-26-Dapr-First-Look/"/>
    <id>http://nearsyh.me/2021/02/26/2021-02-26-Dapr-First-Look/</id>
    <published>2021-02-25T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.529Z</updated>
    
    <content type="html"><![CDATA[<p>最近在 HN 上看到了一个项目 Dapr 发布了 1.0 版本。我花了一点时间读了一下它的文档，觉得挺有趣的，所以用这篇博客来简单介绍一下它以及我对它的第一印象。</p><h2 id="what-is-dapr"><a href="#what-is-dapr" class="headerlink" title="Dapr 是什么"></a><a href="#what-is-dapr">Dapr 是什么</a></h2><p>Dapr  (<strong>D</strong>istributed <strong>Ap</strong>plication <strong>R</strong>untime) 是一个运行时环境，以 sidecar 的形式和实际的应用程序一起执行。它让开发者可以专注于业务逻辑的开发。而分布式服务的其他常用组件（状态存储，pub/sub）则包含在运行时中，并且可以通过配置文件来一定的个性化。</p><h2 id="dapr-core-ideas"><a href="#dapr-core-ideas" class="headerlink" title="Dapr 的核心思想"></a><a href="#dapr-core-ideas">Dapr 的核心思想</a></h2><p>想象一个普通的服务。它为了实现业务逻辑，经常会依赖</p><ol><li>数据库</li><li>消息队列</li></ol><p>有经验的开发人员往往会通过封装的方式，根据核心业务来定义接口以隐藏实际的依赖选择（比如数据库选用了 MySQL，消息队列选用 Kafka）。业务逻辑在进程中调用这些接口，来实际使用这些依赖。</p><p>Dapr 进一步地推广了这个思路。Dapr 也对这些常见依赖进行抽象，只不过使用了 RPC 的方式，让不同的语言和进程都可以使用。我这样描述可能不够清晰，但是下面这张图一定能让你明白。</p><p><img src="https://i.imgur.com/SEiR2uK.jpg" alt="Dapr"></p><h3 id="dapr-abstractions"><a href="#dapr-abstractions" class="headerlink" title="抽象"></a><a href="#dapr-abstractions">抽象</a></h3><p>为了帮助业务开发，Dapr 提供了很多种抽象，包括以下几种：</p><ol><li>statestore：用于持久化，提供了键值存储的抽象，支持常见的数据库，redis，等等。我不确定 dapr 怎么对复杂的查询进行支持。</li><li>pub/sub：提供了 publishing 和 subscribing events 的抽象，支持 Kafka，RocketMQ 等等。</li><li>binding：提供了对外部系统相应的抽象。比如一个 input binding 可以隐藏 subscribe kafka 这一细节，开发者只需要实现一个 API 来处理 input。Dapr 会负责调用这个 API。</li></ol><h3 id="dapr-service-invocation"><a href="#dapr-service-invocation" class="headerlink" title="服务调用"></a><a href="#dapr-service-invocation">服务调用</a></h3><p>除了以上一种抽象，在分布式应用这种特定场景下，多个服务之间往往会有互相调用。随着这种互相调用越来越复杂，流量越来越大，我们往往需要服务发现，负载均衡等等。为此 Dapr 要求服务间的互相调用也需要通过 sidecar 提供的 API 执行。sidecar 根据请求数据，将其转发到对应的服务进程。</p><h3 id="observability"><a href="#observability" class="headerlink" title="可观察性"></a><a href="#observability">可观察性</a></h3><p>由于服务之间的调用都需要通过 sidecar，所以实现可观察性非常自然，比如分布式 trace，接口调用监控等等。</p><h2 id="summary"><a href="#summary" class="headerlink" title="总结"></a><a href="#summary">总结</a></h2><p>Dapr 的思想并不复杂，甚至让我觉得非常自然。它看上去似乎确实能够让业务开发简单很多，但是有一点我不太确定。虽然 dapr 对各种底层组件做了抽象，在应用中我们是不是也应该将 dapr 提供的抽象视作实现细节，比如将 dapr 提供的接口和 mysql 一视同仁。否则业务和底层细节就有了耦合。</p><p>按照目前我的理解，我感觉我们的业务逻辑还是应该和 dapr 隔离开，不要让 dapr 侵入到我们的代码中。在下一篇博客里（如果有的话），我可以试着用一个具体的例子来说明我的想法。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近在 HN 上看到了一个项目 Dapr 发布了 1.0 版本。我花了一点时间读了一下它的文档，觉得挺有趣的，所以用这篇博客来简单介绍一下它以及我对它的第一印象。&lt;/p&gt;
&lt;h2 id=&quot;what-is-dapr&quot;&gt;&lt;a href=&quot;#what-is-dapr&quot; class</summary>
      
    
    
    
    <category term="技术" scheme="http://nearsyh.me/categories/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="分布式" scheme="http://nearsyh.me/categories/%E6%8A%80%E6%9C%AF/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
    
    <category term="技术" scheme="http://nearsyh.me/tags/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="Dapr" scheme="http://nearsyh.me/tags/Dapr/"/>
    
    <category term="分布式" scheme="http://nearsyh.me/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>Linux 内核内存管理</title>
    <link href="http://nearsyh.me/2020/12/27/2020-12-27-Linux-Kernel-Memory/"/>
    <id>http://nearsyh.me/2020/12/27/2020-12-27-Linux-Kernel-Memory/</id>
    <published>2020-12-26T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.529Z</updated>
    
    <content type="html"><![CDATA[<p>今天读完了 Understanding the Linux Kernel 的第八章，主要描述了 Linux 内核如何为自己分配动态内存。这一章涉及了很多很复杂的内容，组织形式对我来说也不太友好，有种盲人摸象的感觉。直到全部看完，才有了一个相对比较完整的理解。这篇博客希望能让我之后再回来看的时候，减少我自己的认知负担。这篇博客主要分为以下几个部分：</p><ol><li><a href="#address-space">内核态地址空间结构</a></li><li><a href="#physical-page-frame">物理内存页的分配</a></li><li><a href="#virtual-memory-mapping">虚拟地址映射</a></li></ol><p>这里声明一下，以下的内容都是基于 Linux 2.6 在 80x86 架构上的实现。</p><h2 id="address-space"><a href="#address-space" class="headerlink" title="内核态地址空间结构"></a><a href="#address-space">内核态地址空间结构</a></h2><p>大家都知道，操作系统本身的目的，是为了给上层的应用提供各种资源的抽象。对于内存，操作系统使用了虚拟地址来为各个应用提供了安全易用的使用接口。而内核态地址空间，是一个进程只有在内核态时才可以使用的虚拟地址空间。Linux 把大于等于 0xC0000000 (3GB) 的这一片虚拟地址空间分配给了内核态。这一片空间中的每一部分，不是生来平等的，而是分成了几个部分，每一个部分都用不同的用途。这些部分按照虚拟地址从小到大排序依次是：</p><ol><li>3GB ~ 3GB + 896M (<a href="https://elixir.bootlin.com/linux/v2.6.11/source/mm/memory.c#L79"><code>high_memory</code></a>): 这一部分的虚拟地址是直接映射到了 0~896M 这一片物理内存。这个映射是固定不变的（虚拟地址减去 3G 就是物理地址）。这部分包含了 _DMA_（可以用于 direct memory access） 和 _normal_。这之后的部分在 Linux 中被称为 _high memory_。</li><li><code>high_memory</code> ~ <code>VMALLOC_START</code>: 这一部分是留空的，用来保证内存访问安全。如果有代码一不小心访问了这部分内存，会被捕捉到。</li><li><code>VMALLOC_START</code> ~ <code>VMALLOC_END</code>: 这一部分是 _vmalloc area_，主要用来映射非连续内存。</li><li><code>VMALLOC_END</code> ~ <code>PKMAP_BASE</code>: 这一部分也是留空，用来保证访问安全的。</li><li><code>PKMAP_BASE</code> ~ <code>FIXADDR_START</code>: 这一部分是用于永久映射的，详细的讨论见下文。</li><li><code>FIXADDR_START</code> ~ 4GB: 这一部分是用于固定映射的。固定映射是为了一些指定的用途建立的到物理内存的映射，比如 APIC（高级可编程中断控制器）。</li></ol><p><img src="https://i.imgur.com/Rkk2Way.png" alt="memory_space"></p><h2 id="memory-allocation-general"><a href="#memory-allocation-general" class="headerlink" title="内存分配的一般流程"></a><a href="#memory-allocation-general">内存分配的一般流程</a></h2><p>不管内核怎么为自己分配内存，都依赖于实际的物理内存页的分配。除此之外，内核还需要分配内存来存放这个分配的描述符，以及创建从虚拟地址到新的物理内存的映射。所以，一般的流程如下：</p><ul><li>为元数据分配内存 (注意，这一步也是一个内存分配的过程)</li><li>实际的物理内存分配</li><li>更新分配的元数据</li><li>分配虚拟地址空间，更新页表</li></ul><p>这上面 4 个步骤的顺序，在不同的情况下会有一些不同，并且在某些情况下，只需要某几个步骤。但是大体的思路是这样的。接下来我们来看看其中重要步骤的实现。</p><h2 id="physical-page-frame"><a href="#physical-page-frame" class="headerlink" title="物理内存页的分配"></a><a href="#physical-page-frame">物理内存页的分配</a></h2><p>内核将物理内存抽象为 _page frame_（页框）。每一个分配获得的 page frame 都对应到了一个页描述符（<a href="https://elixir.bootlin.com/linux/v2.6.11/source/include/linux/mm.h#L223"><code>struct page</code></a>）。这些页描述符都保存在了 <a href="https://elixir.bootlin.com/linux/v2.6.11/source/mm/memory.c#L65"><code>mem_map</code></a> 这个数组中。</p><p>分配物理页框的入口函数是 <code>alloc_pages</code>，它也有一些变种，比如 <code>__get_free_pages</code>。这里要注意的一点是，<code>alloc_pages</code> 返回的物理页框，都是连续的。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// gfp 是 get free page 的缩写。</span></span><br><span class="line"><span class="comment">// gfp_mask 是分配内存时需要的一些参数，下面再详细解释</span></span><br><span class="line"><span class="comment">// order: 表示需要 2**order 个 page frame。</span></span><br><span class="line"><span class="function">page* <span class="title">alloc_pages</span><span class="params">(<span class="keyword">unsigned</span> <span class="keyword">int</span> gfp_mask, <span class="keyword">unsigned</span> <span class="keyword">int</span> order)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// alloc_pages_node -&gt; __alloc_pages</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">page* <span class="title">alloc_pages</span><span class="params">(<span class="keyword">unsigned</span> <span class="keyword">int</span> gfp_mask)</span> </span>&#123; <span class="keyword">return</span> alloc_pages(gfp_masks, <span class="number">0</span>); &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// __get_free_pages 的主要区别是它的返回值是虚拟地址</span></span><br><span class="line"><span class="keyword">unsigned</span> <span class="keyword">long</span> __get_free_pages(<span class="keyword">unsigned</span> <span class="keyword">int</span> gfp_mask, <span class="keyword">unsigned</span> <span class="keyword">int</span> order) &#123;</span><br><span class="line">  <span class="keyword">return</span> page_address(alloc_pages(gfp_mask, order));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">unsigned</span> <span class="keyword">long</span> __get_free_pages(<span class="keyword">unsigned</span> <span class="keyword">int</span> gfp_mask) &#123;</span><br><span class="line">  <span class="keyword">return</span> __get_free_pages(gfp_mask, <span class="number">0</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>接下来我们具体看看 <code>__alloc_pages</code> 的实现。</p><h3 id="zoned-page-frame-allocator"><a href="#zoned-page-frame-allocator" class="headerlink" title="Zoned Page Frame Allocator"></a><a href="#zoned-page-frame-allocator">Zoned Page Frame Allocator</a></h3><p><code>__alloc_pages</code> 的实现被称为 zoned page frame allocator (以下简称 zoned allocator)。它的特点是将物理内存分为若干个 zone，对应的数据结构是 <a href="https://elixir.bootlin.com/linux/v2.6.11/source/include/linux/mmzone.h#L110"><code>struct zone</code></a>。它在分配内存的时候，会根据参数（<code>gfpmask</code>）从合适的 zone 中去分配内存。在 Linux 2.6 中，内核将物理内存分为了三部分：DMA，normal 和 high memory。这三个部分恰好就对应了内核态地址空间的分配。DMA 和 normal 对应了 3GB 到 3GB + 876M 这一段的虚拟地址。</p><p>当 zoned allocator 找到一个合适的区可以分配内存的时候，它会使用 buddy system 算法，来从这个 zone 所对应的物理内存中挑选出合适的部分。</p><h3 id="buddy-system"><a href="#buddy-system" class="headerlink" title="Buddy System"></a><a href="#buddy-system">Buddy System</a></h3><p>Buddy system 需要解决的问题是 <code>external fragmentation</code>。回忆一下，上面的 <code>alloc_pages</code> 返回的是连续的物理页框。如果我们在处理内存分配请求的时候过于随意，就会导致有很多零碎的物理页框。这些页框可能分散各处，即使总量很多，也没法满足连续的物理内存的需求。</p><p>Buddy system 解决这个问题的方式，就是将连续的物理内存分类。对于每个内存分配请求，我们根据请求的大小寻找合适的分类进行分配。具体来说，Buddy system 将连续内存按照 2 的幂分成若干个列表。比如，第一个列表包含了若干个长度为 1 个页框的连续物理页，第二个则包含了若干长度为 2 个页框的连续物理页。当我们收到一个需要 4 个页框的内存分配请求时，我们从 4 对应的列表开始搜索，一旦找到一个非空列表，就从列表中取一个元素。如果这个列表中的元素对应的物理内存空间比请求的大小大，我们会把这段空间拆分成多个 2 个幂。比如，如果我们使用长度为 16 的列表来满足大小为 4 的请求，我们会把 16 拆成 8 + 4 + 4。出去需要被分配的 4 之外，其他长度为 8 和 4 的物理内存，会插入到对应的列表中，满足之后的内存分配。当我们释放内存的时候，我们也会使用类似的算法，把相邻的物理内存合并。</p><p>Buddy system 的入口函数是 [<code>__rmqueue(struct zone *zone, unsigned int order)</code>]，它会从 <code>zone-&gt;free_area</code> 这个数组的第 order 个元素开始，需要合适的内存块。</p><h2 id="physical-memory"><a href="#physical-memory" class="headerlink" title="物理内存的分配"></a><a href="#physical-memory">物理内存的分配</a></h2><p>上面介绍了部分，是以物理页框为内存分配的单位。然而，在实际的使用中，我们往往不会直接时候一个页框。相反，我们往往是根据我们需要的数据结构，来申请内存。为了处理这种请求，Linux 内核实现了 Slab Allocator。</p><p>Slab Allocator 使用了两种抽象：Cache 和 Slab。一个 Cache（<code>struct kmem_cache_t</code>） 用来处理固定大小的内存分配请求，它包含了多个 Slab（<code>struct slab</code>），每个 Slab 对应到了若干个连续的物理页框，是实际的物理内存来源。</p><p>Slab Allocator 是基于 zoned allocator 实现的，它的入口函数是 <a href="https://elixir.bootlin.com/linux/v2.6.11/source/include/linux/slab.h#L64"><code>kmem_cache_alloc</code></a>，简单的伪代码实现如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// kmem_cache_t 指明的对应的 cache。</span></span><br><span class="line"><span class="comment">// 这里不需要指定大小，因为每个 cache 能分配的大小是固定的。</span></span><br><span class="line"><span class="function"><span class="keyword">void</span>* <span class="title">kmem_cache_alloc</span><span class="params">(<span class="keyword">kmem_cache_t</span> *cachep, <span class="keyword">int</span> flags)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 这里的 ac 是 cache 中每个 cpu 对应的本地缓存，目的是为了减少 contention</span></span><br><span class="line">  <span class="comment">// 分配的时候只会从 array_cache 中取第一个空闲的。</span></span><br><span class="line">  <span class="comment">//</span></span><br><span class="line">  <span class="comment">// cache_alloc_refill 做的事情是把 cache 拥有的 slab 中的内存放到 array_cache 中。</span></span><br><span class="line">  <span class="class"><span class="keyword">struct</span> <span class="title">array_cache</span> *<span class="title">ac</span> =</span> cachep-&gt;<span class="built_in">array</span>[smp_processor_id()];</span><br><span class="line">  <span class="keyword">if</span> (ac-&gt;avail) &#123;</span><br><span class="line">    <span class="keyword">return</span> ((<span class="keyword">void</span>**)(ac + <span class="number">1</span>)[--ac-&gt;avail]);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> cache_alloc_refill(cachep, flags);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这个方法是为了 cache 分配 slab。</span></span><br><span class="line"><span class="comment">// 它会调用 alloc_pages，也就是 zoned allocator。</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">cache_grow</span><span class="params">(<span class="keyword">kmem_cache_t</span> * cachep, <span class="keyword">int</span> flags, <span class="keyword">int</span> nodeid)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// kmem_getpages -&gt; alloc_pages</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="virtual-memory-mapping"><a href="#virtual-memory-mapping" class="headerlink" title="虚拟地址映射"></a><a href="#virtual-memory-mapping">虚拟地址映射</a></h2><p>内存分配的另一个重要部分，就是建立虚拟地址的映射。只有有了映射，内核代码才可以访问内存。对于不同的内存，建立映射的方式也不尽相同。具体分为以下几类：</p><ol><li>非 high memory 的虚拟地址映射到的物理内存就是 0~896M。这个映射在初始化页表的时候，已经配置好了，不需要再额外配置。</li><li>对于 high memory 的虚拟地址部分，它的映射分为三种：<ol><li>永久内核映射，它将 <code>PKMAP_BASE</code> ~ <code>FIXADDR_START</code> 的虚拟地址映射到物理内存。它的入口函数是 <code>kmap</code>。</li><li>临时内核映射，它将固定映射中的 FIX_KMAP_BEGIN 到 FIX_KMAP_END 这部分映射到物理内存。它的入口函数是 <code>kmap_atomic</code>。</li><li>非连续内存管理，它将 <code>VMALLOC_START</code> ~ <code>VMALLOC_END</code> 的虚拟地址映射到物理内存。它的入口函数是 <code>map_vm_area</code>。</li></ol></li></ol><p>这三种映射方式有不同的特性，所以会使用在不同的场景。</p><h3 id="permanent"><a href="#permanent" class="headerlink" title="永久内核映射"></a><a href="#permanent">永久内核映射</a></h3><p>永久内核映射之所以被称为永久，是因为，除非有明确的 release，否则这个映射一直存在。由于这部分的虚拟地址空间是有限的，所以建立这种类型的映射可能会 block，也就意味着它不能在中断处理函数中使用。</p><h3 id="temporary"><a href="#temporary" class="headerlink" title="临时内核映射"></a><a href="#temporary">临时内核映射</a></h3><p>临时内核映射的特点是它本身没有修改的保护，需要内核代码编写者自己保证不会有不同的内核控制路径同时使用一个虚拟地址映射到不同的物理内存。这也是为什么它不会阻塞的原因。</p><h3 id="vmalloc"><a href="#vmalloc" class="headerlink" title="非连续内存管理"></a><a href="#vmalloc">非连续内存管理</a></h3><p>非连续内存管理比较特殊，它在处理打断内存分配请求的时候，不会分配连续的物理页。Linux 在实现它的时候，将 <code>VMALLOC_START</code> ~ <code>VMALLOC_END</code> 这段虚拟地址分成了若干个非连续内存区（描述符的数据结构是 <code>struct vm_struct</code>）。每一个内存分配都会生成一个 <code>vm_struct</code>，它的入口函数是 <code>vmalloc</code>。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span>* <span class="title">vmalloc</span><span class="params">(<span class="keyword">unsigned</span> <span class="keyword">long</span> size)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 分配一个空闲的 vm area。</span></span><br><span class="line">  <span class="class"><span class="keyword">struct</span> <span class="title">vm_struct</span> *<span class="title">area</span> =</span> get_vm_area(size, VM_ALLOC&gt;);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 分配页描述符需要的内存</span></span><br><span class="line">  <span class="comment">// kmalloc 底层调用的是 slab allocator</span></span><br><span class="line">  area-&gt;pages = kmalloc(array_size, GFP_KERNEL);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 分配若干个物理页</span></span><br><span class="line">  <span class="comment">// 注意这里多次调用 alloc_page，分配的就是多个彼此不连续的物理页</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; area-&gt;nr_pages; i ++) &#123;</span><br><span class="line">    area-&gt;pages[i] = alloc_page(...);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 建立虚拟地址映射</span></span><br><span class="line">  <span class="comment">// 它会更新页表</span></span><br><span class="line">  map_vm_area(area, area-&gt;pages);</span><br><span class="line">  <span class="keyword">return</span> area-&gt;addr;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个函数的实现，恰好就对应了 <a href="#memory-allocation-general">内存分配的一般流程</a> 的几个步骤。</p><h2 id="summary"><a href="#summary" class="headerlink" title="总结"></a><a href="#summary">总结</a></h2><p>Linux 内核的内存管理的实现，实际上有明确的分层。最上层是 <code>vmalloc</code>，它依赖于 <code>slab allocator</code>。<code>slab allocator</code> 又依赖于 <code>zoned allocator</code>。而 <code>zoned allocator</code> 又依赖于 <code>buddy system</code>。在物理内存分配之外，虚拟地址的映射又分成了一种形式。从这个角度来看，内核的内存管理似乎就更清晰了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;今天读完了 Understanding the Linux Kernel 的第八章，主要描述了 Linux 内核如何为自己分配动态内存。这一章涉及了很多很复杂的内容，组织形式对我来说也不太友好，有种盲人摸象的感觉。直到全部看完，才有了一个相对比较完整的理解。这篇博客希望能让</summary>
      
    
    
    
    <category term="技术" scheme="http://nearsyh.me/categories/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="Linux" scheme="http://nearsyh.me/categories/%E6%8A%80%E6%9C%AF/Linux/"/>
    
    
    <category term="Linux" scheme="http://nearsyh.me/tags/Linux/"/>
    
    <category term="UtLK" scheme="http://nearsyh.me/tags/UtLK/"/>
    
    <category term="技术" scheme="http://nearsyh.me/tags/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="底层" scheme="http://nearsyh.me/tags/%E5%BA%95%E5%B1%82/"/>
    
  </entry>
  
  <entry>
    <title>Hexo 主题的折腾记录</title>
    <link href="http://nearsyh.me/2020/12/26/2020-12-26-Hexo-Theme-Tweaking/"/>
    <id>http://nearsyh.me/2020/12/26/2020-12-26-Hexo-Theme-Tweaking/</id>
    <published>2020-12-25T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.529Z</updated>
    
    <content type="html"><![CDATA[<p>昨天晚上折腾了一下这个 blog 的主题，主要包含了一下几个更新。</p><ol><li><a href="#tags">增加了标签展示</a></li><li><a href="#toc">增加了目录展示</a></li><li><a href="#comments">增加了评论功能</a></li></ol><p>这篇博客主要记录一下如何实现和一点心得。</p><h2 id="origin"><a href="#origin" class="headerlink" title="主题的原型 Apollo"></a><a href="#origin">主题的原型 Apollo</a></h2><p>这个主题的前身是 <a href="https://github.com/pinggod/hexo-theme-apollo">apollo</a> 主题。我特别喜欢它的简介，以及模仿 Vue 官方网站的样式，所以这个博客最开始的版本就直接使用了这个主题（只有一些非常简单的修改）。</p><p>在那个时候，我对 Hexo 也并没有什么了解，只是单纯的照着教程使用而已，没有办法对主题本身做比较明显的改动。</p><h2 id="hexo-learning"><a href="#hexo-learning" class="headerlink" title="糙快猛地学习 Hexo"></a><a href="#hexo-learning">糙快猛地学习 Hexo</a></h2><p>最近相对整个 blog 做些读者体验方面的改进。主要的动因是我的同事在搜索 Spanner 的文章的时候搜到了我的博客。一旦有了实实在在的读者，我的整个心态就有点飘了，准备添加一下之前我就觉得缺少的特性：标签和目录。为此，我就迅速地读了一下 Hexo 的文档。</p><p>Hexo 的文档说实话有点太简单了，而且样例代码都是基于 ejs 的。因为 apollo 本身是用 jade 实现的，而我又没有用过，所以还是遇到了一些麻烦，不过最后都通过看 Hexo 以及别的主题的代码搞定了。下面就讲讲两个更新的实现方式。</p><h2 id="tags"><a href="#tags" class="headerlink" title="标签展示"></a><a href="#tags">标签展示</a></h2><p>展示标签其实挺简单的。Hexo 提供了 <code>list_tags</code> 这个函数，并且提供了一定的自定义空间，所以非常简单。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; 因为我只想在文章页展示，所以判断了一下现在的位置是否是 post</span><br><span class="line">if is_post()</span><br><span class="line">    &#x2F;&#x2F; 在每个 tag 前面加一个 #</span><br><span class="line">    - var transform &#x3D; function(str) &#123; return &#39;#&#39; + str; &#125;</span><br><span class="line">    - var config &#x3D; &#123;class: &#39;post-tag&#39;, show_count: false, style: false, separator: &#39; &#39;, transform: transform&#125;</span><br><span class="line">    .post-tags</span><br><span class="line">        !&#x3D; list_tags(item.tags, config)</span><br></pre></td></tr></table></figure><p>这里我遇到的一个问题就是，我不知道 pug 怎么直接在 object 里面使用 lambda，只好把代码拆成了几行。不过这样可读性似乎也好一点。</p><h2 id="toc"><a href="#toc" class="headerlink" title="目录功能"></a><a href="#toc">目录功能</a></h2><p>添加目录比较简单，Hexo 也提供了 <code>toc</code> 这个函数。然而，为了达到现在的效果，着实花了我一点时间。当然，对于有前端经验的朋友们来说，应该比较简单，我就不赘述了。这里我主要想说说怎么实现滚动的时候，目录会高亮当前展示的部分对应的目录项。</p><p>网上有挺多实现方式的，我挑选了 Bootstrap。Bootstrap 提供了 scrollspy 的功能，虽然有文档，但是文档中有几个属性的名字写错了，也花了我一点时间。下面是实现的几个主要部分：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; 为 body 加上以下几个属性。</span><br><span class="line">&#x2F;&#x2F; data-bs-target 的值必须是目录元素的 id</span><br><span class="line">body.container(data-bs-spy&#x3D;&quot;scroll&quot; data-bs-target&#x3D;&quot;#toc-nav&quot; data-bs-offset&#x3D;0)</span><br><span class="line">  &#x2F;&#x2F; ...</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; 这是目录的定义。注意它的 class 必须是 navbar。</span><br><span class="line">nav#toc-nav.navbar</span><br><span class="line">  &#x2F;&#x2F; 这里的 class 必须是 nav。</span><br><span class="line">  !&#x3D; toc(item.content, &#123; list_number: false, class: &#39;nav&#39; &#125;)</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; 也别忘了 include bootstrap 的 js 文件。</span><br><span class="line">&#x2F;&#x2F; 也可以自己 host。</span><br><span class="line">script(src&#x3D;&quot;&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;npm&#x2F;bootstrap@5.0.0-beta1&#x2F;dist&#x2F;js&#x2F;bootstrap.min.js&quot;)</span><br></pre></td></tr></table></figure><h3 id="chinese-support"><a href="#chinese-support" class="headerlink" title="中文的支持"></a><a href="#chinese-support">中文的支持</a></h3><p>如果你的 blog 和我一样，也是使用中文，那么只有上面的代码，是没法实现自动高亮的。这个问题的原因是 <code>toc</code> 是实现在指定链接的时候，会调用 <a href="https://github.com/hexojs/hexo/blob/5967369c1bd6fb05879527433080609a07d84110/lib/plugins/helper/toc.js#L29"><code>encodeURL</code></a>。然而，Bootstrap 会假设这个链接和文章中每个段落标题的 id 是相同的。这个 id 是由 <code>hexo-renderer-marked</code> 解析 markdown 生成 html 的时候自动生成的，默认就是这个段落的标题。所以，如果你使用的英文作为标题，<code>encodeURL</code> 之后可能还是一样的，Bootstrap 就能关联起来。</p><p>为了解决这个问题，我看了一下 <code>hexo-renderer-marked</code> 的文档，发现它支持自定义每个段落的 id。你需要在 <code>_config.yml</code> 中先启用 <code>anchorAlias</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">marked:</span></span><br><span class="line">  <span class="attr">headerIds:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">anchorAlias:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>然后在 Markdown 中，将原本的 <code># header</code> 改成 <code># [header](#header-id)</code>。这样就能解决这个问题了。</p><h2 id="comments"><a href="#comments" class="headerlink" title="评论功能"></a><a href="#comments">评论功能</a></h2><p>评论功能的实现就很容易了，apollo 本身就支持。如果使用 disqus 的话，只要先去 disqus 上注册一个网站，然后把注册的网站名写进配置就可以了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;昨天晚上折腾了一下这个 blog 的主题，主要包含了一下几个更新。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#tags&quot;&gt;增加了标签展示&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#toc&quot;&gt;增加了目录展示&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#comment</summary>
      
    
    
    
    
    <category term="技术" scheme="http://nearsyh.me/tags/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="Hexo" scheme="http://nearsyh.me/tags/Hexo/"/>
    
    <category term="Blog" scheme="http://nearsyh.me/tags/Blog/"/>
    
  </entry>
  
  <entry>
    <title>Linux 如何实现定时调度任务</title>
    <link href="http://nearsyh.me/2020/12/18/2020-12-18-Timer-In-Linux/"/>
    <id>http://nearsyh.me/2020/12/18/2020-12-18-Timer-In-Linux/</id>
    <published>2020-12-17T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.529Z</updated>
    
    <content type="html"><![CDATA[<p>最近工作上遇到了一些和定时调度相关的问题。在 Google，类似这样的问题，往往已经有人帮我们造好轮子了。但是，作为正经程序员，我们还是会忍不住思考，如果我们自己做，会怎么做呢？已有的实现，又有什么特殊之处。今天这篇文章，我们来一起看一看 Linux 怎么解决这个问题的。</p><p>首先，我们先来试试分解这个问题。</p><ol><li>数据结构: 我们需要设计一个数据结构来维护所有的任务，它需要支持快速的插入操作，和获取到期任务的操作。</li><li>任务执行：我们需要一个高效的手段，在到了指定的时间<strong>之后</strong>，执行已经到期的任务。</li></ol><p>接下来，会尝试从这两个方面来分析 Linux 的实现。Linux 提供了多种执行定时任务的方式，我们这里主要来分析一下 <code>timer_list</code>。</p><h2 id="data-structure"><a href="#data-structure" class="headerlink" title="数据结构"></a><a href="#data-structure">数据结构</a></h2><p>在 Linux 代码中，每一个定时任务都对应到一个 <code>timer_list</code>。下面是这个数据结构的定义：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">timer_list</span> &#123;</span></span><br><span class="line">    <span class="comment">// 侵入式的双向链表，Linux 代码中很常见</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">list_head</span> <span class="title">entry</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 什么时候到期，单位是 tick。</span></span><br><span class="line">    <span class="comment">// tick 可以理解成 CPU 的时钟周期。到底多久不影响理解。</span></span><br><span class="line"><span class="keyword">unsigned</span> <span class="keyword">long</span> expires;</span><br><span class="line"></span><br><span class="line"><span class="keyword">spinlock_t</span> lock;</span><br><span class="line"><span class="keyword">unsigned</span> <span class="keyword">long</span> magic;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 到期之后要执行的函数</span></span><br><span class="line"><span class="keyword">void</span> (*function)(<span class="keyword">unsigned</span> <span class="keyword">long</span>);</span><br><span class="line">    <span class="comment">// 函数的参数</span></span><br><span class="line"><span class="keyword">unsigned</span> <span class="keyword">long</span> data;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">tvec_t_base_s</span> *<span class="title">base</span>;</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>相信这个数据结构的定义，大家并不会觉得有什么惊讶的部分（除了这个名字…）。我们接下来看 Linux 怎么组织这些定时任务。</p><p>Linux 使用了 <code>tvec_t_base_s</code> 来维护（每个 CPU 对应的）所有的定时任务。它的定义如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">tvec_t_base_s</span> &#123;</span></span><br><span class="line"><span class="keyword">spinlock_t</span> lock;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 目前已经处理到的时间点</span></span><br><span class="line">    <span class="comment">// 这个时间点之前应该到期的所有任务都已经处理完了。</span></span><br><span class="line"><span class="keyword">unsigned</span> <span class="keyword">long</span> timer_jiffies;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 当前正在执行的任务</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">timer_list</span> *<span class="title">running_timer</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 下面的几个字段是这个数据结构的核心。</span></span><br><span class="line">    <span class="comment">// 每个字段可以理解成一个使用链表来解决冲突的哈希表。</span></span><br><span class="line">    <span class="comment">// 哈希表的 key 是定时任务的 expires - current_time 的值。</span></span><br><span class="line">    <span class="comment">// 哈希表的 value 是定时任务本身。</span></span><br><span class="line">    <span class="comment">// 每个字段的 hash 函数不同。</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 只包含 [0, 256) 个 tick 之后过期的任务</span></span><br><span class="line">    <span class="comment">// hash function: id -&gt; id.</span></span><br><span class="line"><span class="keyword">tvec_root_t</span> tv1;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 只包含 [256, 64 * 256) 个 tick 之后过期的任务</span></span><br><span class="line">    <span class="comment">// hash function: id -&gt; id / 256</span></span><br><span class="line"><span class="keyword">tvec_t</span> tv2;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 只包含 [64 * 256, 64 ^ 2 * 256) 个 tick 之后过期的任务</span></span><br><span class="line">    <span class="comment">// hash function: id -&gt; id / 256 / 64</span></span><br><span class="line"><span class="keyword">tvec_t</span> tv3;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 只包含 [64 ^2 * 256, 64 ^ 3 * 256) 个 tick 之后过期的任务</span></span><br><span class="line">    <span class="comment">// hash function: id -&gt; id / 256 / 64 ^ 2</span></span><br><span class="line"><span class="keyword">tvec_t</span> tv4;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 包含更未来的任务</span></span><br><span class="line"><span class="keyword">tvec_t</span> tv5;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个数据结构，某种程度上来看，和时间轮非常接近。随着时间的前进，当 <code>tv1</code> 中的任务完全处理完的时候，<code>tv2</code> 中的最小 slot 中的所有任务（tick 跨度是 256）会填补到 <code>tv1</code> 中。<code>tv2</code> 中的任务完成了之后，也会从 <code>tv3</code> 中获取任务，以此类推下去。这样的数据结构，可以支持快速的插入和查找的操作。</p><h2 id="task-execution"><a href="#task-execution" class="headerlink" title="任务执行"></a><a href="#task-execution">任务执行</a></h2><p>Linux 执行定时任务的策略，某种程度上有点像 cron job。对于每个时钟中断（每个 tick），Linux 定义的中断处理函数都会更新当前时间，同时会触发一个软中断（TIMER_SOFTIRQ）。这个软中断的处理函数会查看 <code>tvec_t_base_s.timer_jiffies</code> 和当前时间的差距，然后处理所有到期的任务。之所以要使用软中断，是因为我们要保证每个硬件中断（这里的时钟中断）的处理函数执行的时间足够短，否则会阻塞其他的中断处理，降低系统的吞吐量。具体的代码的调用路径见下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 时钟中断处理函数</span></span><br><span class="line">timer_interrupt() &#123;</span><br><span class="line">    do_timer_interrupt() &#123;</span><br><span class="line">        do_timer_interrupt_hook() &#123;</span><br><span class="line">            update_process_times() &#123;</span><br><span class="line">                run_local_timers() &#123;</span><br><span class="line">                    <span class="comment">// 触发软中断</span></span><br><span class="line">                    raise_softirq(TIMER_SOFTIRQ);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 软中断处理函数</span></span><br><span class="line">run_timer_softirq() &#123;</span><br><span class="line">    <span class="comment">// 核心逻辑</span></span><br><span class="line">    __run_timers();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近工作上遇到了一些和定时调度相关的问题。在 Google，类似这样的问题，往往已经有人帮我们造好轮子了。但是，作为正经程序员，我们还是会忍不住思考，如果我们自己做，会怎么做呢？已有的实现，又有什么特殊之处。今天这篇文章，我们来一起看一看 Linux 怎么解决这个问题的。&lt;</summary>
      
    
    
    
    <category term="技术" scheme="http://nearsyh.me/categories/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="Linux" scheme="http://nearsyh.me/categories/%E6%8A%80%E6%9C%AF/Linux/"/>
    
    
    <category term="Linux" scheme="http://nearsyh.me/tags/Linux/"/>
    
    <category term="UtLK" scheme="http://nearsyh.me/tags/UtLK/"/>
    
    <category term="技术" scheme="http://nearsyh.me/tags/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="底层" scheme="http://nearsyh.me/tags/%E5%BA%95%E5%B1%82/"/>
    
  </entry>
  
  <entry>
    <title>快照隔离在一些分布式系统中的实现 (1) - Omid1</title>
    <link href="http://nearsyh.me/2020/07/12/2020-07-12-Snapshot-Isolation-Omid-1/"/>
    <id>http://nearsyh.me/2020/07/12/2020-07-12-Snapshot-Isolation-Omid-1/</id>
    <published>2020-07-11T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.529Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>系列文章:</p><p><a href="https://blogs.nearsyh.me/2020/07/07/2020-07-07-Snapshot-Isolation-Introduction/">快照隔离在一些分布式系统中的实现 (1) - 什么是快照隔离</a><br><a href="https://blogs.nearsyh.me/2020/07/12/2020-07-12-Snapshot-Isolation-Omid-1/">快照隔离在一些分布式系统中的实现 (2) - Omid1</a></p></blockquote><p>Omid1 发表于论文 <a href="https://ieeexplore.ieee.org/document/6816691">Omid: Lock-free transactional support for distributed data stores</a>。论文作者在之后的另一篇论文中对它的设计做了一些改进，并将 Omid 原本的设计称为 Omid1。为了方便，接下来我都会用 Omid 来指代 Omid1.</p><h2 id="Omid-是什么"><a href="#Omid-是什么" class="headerlink" title="Omid 是什么"></a>Omid 是什么</h2><p>根据论文的描述，Omid 是一个用来为已有的数据存储系统支持事务的工具，并且事务是实现是 lock-free 的。这里有几个关键词：</p><ol><li>工具</li><li>事务</li><li>Lock Free</li></ol><p>用更直白的话说，Omid 是一个外挂/插件。它可以和已有的数据存储服务（例如论文中使用的 HBase）组合，实现事务的支持。基于已有的存储服务是一个很有意思的设计选择。之前也有一些论文使用了相同的思路，比如<a href="https://blogs.nearsyh.me/2019/06/15/2019-06-14-Incremental-Prcoessing/">Precolator</a>就使用了Bigtable。使用已有的系统可以节省很多开发和运维成本。同时这些系统也已经经受了时间的考验，在性能上都没什么问题。</p><p>既然这个系列是关于快照隔离，很明显，Omid 支持的事务也选择了支持快照隔离等级。接下来，我们一起来看一看 Omid 是怎么支持快照隔离的。</p><h2 id="Omid-的设计思路"><a href="#Omid-的设计思路" class="headerlink" title="Omid 的设计思路"></a>Omid 的设计思路</h2><p>在这个系列的<a href="https://blogs.nearsyh.me/2020/07/07/2020-07-07-Snapshot-Isolation-Introduction/">第一篇文章</a>里，我们已经介绍了快照隔离的一般实现思路。没有读过也没关系，这里我们再重复一下这个设计思路：</p><ol><li>数据存储层会维护每个数据的多个版本。</li><li>事务开始前会分配一个开始时间戳 *T<sub>start</sub>*。</li><li>事务会记录自己修改的所有数据集合。</li><li>事务提交之前先会分配一个提交时间戳 *T<sub>commit</sub>*。</li><li>分配完时间戳之后会检查是否有事务和自己的修改数据集合有交集，并且该事务的提交时间戳在 <em>(T<sub>start</sub>, T<sub>commit</sub>)</em> 之间。</li></ol><p>简而言之，根据这个实现思路，快照隔离的实现往往需要两个东西：</p><ol><li>数据多版本</li><li>单调递增的(逻辑)时间戳</li></ol><p>在 Omid 的设计中，数据多版本是通过 HBase 实现的。而单调递增的时间戳则是通过一个中心化的服务实现的，论文中将它称为 transaction status oracle，以下简称 TSO。这个服务除了分配时间戳之外，还会用来判断事务是否提交。</p><h2 id="Omid-的实现简介"><a href="#Omid-的实现简介" class="headerlink" title="Omid 的实现简介"></a>Omid 的实现简介</h2><p>其实看到这里，Omid 的大概实现相信已经比较清楚了。我们分别通过 Omid 使用的数据结构，和读写操作的伪代码来了解它的实现原理。</p><h3 id="数据结构"><a href="#数据结构" class="headerlink" title="数据结构"></a>数据结构</h3><p>为了满足快照隔离的实现，Omid 分别在 HBase 和 TSO 中维护了不同的数据。</p><p>HBase 中主要保存了数据的多个版本。对于每一个 key，HBase 本身就会保存这个 key 对应的多个历史版本数据，并且每个版本的数据都有对应的时间戳。</p><p>TSO 中则维护了两张表，分别是 <code>Commit</code> 和 <code>Last Commit Timestamp</code>。<code>Commit</code> 表维护了从事务 ID 到它提交时间戳的映射。由于事务开始时间的时间戳是唯一的，Omid 直接使用了开始时间戳作为事务 ID。<code>Last Commit Timestamp</code> 表维护了从每一个 Key 到最近修改它并且提交成功的事务的 *T<sub>commit</sub>*。</p><h3 id="伪代码"><a href="#伪代码" class="headerlink" title="伪代码"></a>伪代码</h3><p>实现的关键部分主要是<em>事务的开始</em>，<em>事务的提交</em>，已经<em>数据的读写</em>。我们分别来看一下这几个操作的逻辑。</p><h4 id="事务的开始"><a href="#事务的开始" class="headerlink" title="事务的开始"></a>事务的开始</h4><p>客户端会对 TSO 发起一个 RPC 调用，获得一个时间戳。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Implemented by TSO</span></span><br><span class="line"><span class="function">txn_timestamp <span class="title">start</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">auto</span> t_start = assign_timestamp();</span><br><span class="line">  <span class="keyword">return</span> t_start;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="事务的提交"><a href="#事务的提交" class="headerlink" title="事务的提交"></a>事务的提交</h4><p>客户端会之前获得的事务开始时间戳 <em>T<sub>start</sub></em> 以及修改的 key 的集合发送给 TSO。TSO 判断事务是否能够提交。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Implemented by TSO</span></span><br><span class="line"><span class="function">txn_commit_result <span class="title">commit</span><span class="params">(txn_timstamp t_start, <span class="built_in">set</span> keys)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">auto</span> key : keys) &#123;</span><br><span class="line">        <span class="keyword">if</span> (last_commit(key) &gt; t_start) &#123;</span><br><span class="line">            <span class="comment">// 因为提交时间戳一定比 last_commit 大，所以只需要判断 last_commit 是否大于 t_start</span></span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">abort</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">auto</span> t_commit = assign_timestamp();</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">auto</span> key : keys) &#123;</span><br><span class="line">        last_commit(key) = t_commit</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> commit;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="数据的读写"><a href="#数据的读写" class="headerlink" title="数据的读写"></a>数据的读写</h3><p>数据的写相对比较简单，完全在客户端实现。客户端在 HBase 中将 <code>(key, t_start)</code> 的值设置为对应的 <code>value</code> 即可。注意这个写操作是在事务提交之前做的，这也就意味着 HBase 中存储的值并不一定提交成功，在读取的时候需要通过 TSO 做判断。</p><p>数据的读比较复杂，下面是它的伪代码。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">value <span class="title">read</span><span class="params">(key key, txn_timstamp t_start)</span> </span>&#123;</span><br><span class="line">    txn_timestamp end = t_start;</span><br><span class="line">    <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">        <span class="built_in">list</span>&lt;value&gt; values = read_n_values_before(key, <span class="number">10</span>, end);</span><br><span class="line">        <span class="keyword">if</span> (values.empty()) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">auto</span> value : values) &#123;</span><br><span class="line">            <span class="keyword">if</span> (in_snapshot(value, t_start)) &#123;</span><br><span class="line">                <span class="keyword">return</span> value;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        update end to the earliest timestamp of values.</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 需要访问 TSO</span></span><br><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">in_snapshot</span><span class="params">(value value, txn_timstamp t_start)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 写下这个 value 的事务的开始时间戳</span></span><br><span class="line">    txn_timestamp value_start_timestamp = value.txn_start_timestamp();</span><br><span class="line">    txn_timestamp value_commit_timstamp = commit_table(value_start_timstamp);</span><br><span class="line">    <span class="keyword">return</span> value_commit_timstamp != <span class="literal">nullptr</span> <span class="comment">// 意味着写这个值的事务提交成功了</span></span><br><span class="line">        &amp;&amp; value_commit_timstamp &lt; t_start; <span class="comment">// 意味着写这个值的事务在当前事务之前提交</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="问题和-Omid-的解决方案"><a href="#问题和-Omid-的解决方案" class="headerlink" title="问题和 Omid 的解决方案"></a>问题和 Omid 的解决方案</h2><p>上面的实现思路虽然很简单，但是在性能上会有一些问题。</p><ol><li>HBase 中没有提交的事务写入的数据会额外占据空间，同时老版本的数据也会。</li><li><code>commit</code>表会随着运行时间不断增大。</li><li><code>last commit</code>表和 key 的数量成正比，TSO 作为单节点无法在内存中存储全部。</li><li><code>in_snapshot</code>需要访问 TSO，一个事务会访问很多次，造成 TSO 压力过大。</li><li>TSO 的稳定性。</li></ol><p>为了解决这些问题，Omid 在实现上做了一些优化。我们接下来一一分析。</p><h3 id="HBase-中的数据"><a href="#HBase-中的数据" class="headerlink" title="HBase 中的数据"></a>HBase 中的数据</h3><p>对于没有提交成功的事务的数据，客户端在收到提交失败的响应之后，可以自行删除刚刚写入的数据。与此同时，Omid 也会定期执行清理工作，通过判断一个 <code>cell</code> 的版本号是否在 <code>commit</code> 表中有对应的数据来决定是否需要清理。</p><p>对于老版本的数据，我们只需要保证当前还未执行完的事务能够访问他们需要读取的数据版本即可。TSO 通过维护一个 watermark 来记录所有分配但是还没有提交的事务即可。对于那些很久都没有提交的事务（有可能是 Client 进程挂了），TSO 也可以通过一个 TTL 来忽略，如果对应的 Client 又浪子回头提交事务，TSO 直接拒绝事务即可，不影响正确性。</p><h3 id="commit-表的规模控制"><a href="#commit-表的规模控制" class="headerlink" title="commit 表的规模控制"></a><code>commit</code> 表的规模控制</h3><p>和 HBase 数据规模控制的思路类似，<code>commit</code> 表可以在不影响正确性的情况下，丢弃很老的 entry。与此同时，TSO 会维护一个 <em>T<sub>max</sub>*。这个 *T<sub>max</sub></em> 是所有丢弃的 entry 中提交时间戳的最大值，同时对事务提交代码做这样的改动：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Implemented by TSO</span></span><br><span class="line"><span class="function">txn_commit_result <span class="title">commit</span><span class="params">(txn_timstamp t_start, <span class="built_in">set</span> keys)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// t_start 太老了</span></span><br><span class="line">    <span class="keyword">if</span> (t_max &gt; t_start) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">abort</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 和之前一样</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样的改动可能会有一些 false positive，但是概率不会太大，我们可以通过修改 <code>commit</code> 表的最大大小来控制这个概率。注意，即使错误的 abort 一个事务，也是不会影响正确性的。</p><p>做了这个优化之后，虽然 <code>commit</code> 方法可以正常工作，但是 <code>in_snapshot</code> 方法的正确性会受到影响，因为通过查看表中是否有某个版本来决定事务是否提交会出错。为此，TSO 又维护了一个被 abort 的事务 ID 列表，并对 <code>in_snapshot</code> 方法做如下修改：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 需要访问 TSO</span></span><br><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">in_snapshot</span><span class="params">(value value, txn_timstamp t_start)</span> </span>&#123;</span><br><span class="line">    txn_timestamp value_start_timestamp = value.txn_start_timestamp();</span><br><span class="line">    txn_timestamp value_commit_timstamp = commit_table(value_start_timstamp);</span><br><span class="line">    <span class="keyword">if</span> (value_commit_timstamp != <span class="literal">nullptr</span>) &#123; <span class="comment">// 意味着写这个值的事务提交成功了</span></span><br><span class="line">        <span class="comment">// 意味着写这个值的事务在当前事务之前提交</span></span><br><span class="line">        <span class="keyword">return</span> value_commit_timstamp &lt; t_start;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 如果 commit 表中没有，可能是因为数据被 recycle 了，需要单独判断</span></span><br><span class="line">    <span class="keyword">if</span> (t_max &lt; value_start_timestamp) &#123;</span><br><span class="line">        <span class="comment">// t_start &gt; t_max 说明不是因为 recycle 导致找不到</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (value_start_timestamp in abort_list) &#123;</span><br><span class="line">        <span class="comment">// 在 abort 集合中，说明没有提交成功</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为了控制 abort 列表的大小，我们在清空 HBase 中无效数据的时候，也会顺便清理。</p><h3 id="last-commit-表的规模控制"><a href="#last-commit-表的规模控制" class="headerlink" title="last commit 表的规模控制"></a><code>last commit</code> 表的规模控制</h3><p>为了控制 <code>last commit</code> 表的规模，<code>Omid</code> 使用 HBase 的 key 的 hash 值作为自己的 key，通过牺牲精度来换取空间。同时，如果某一个 entry 的时间戳如果小于 *T<sub>max</sub>*，我们也可以删除它。</p><h3 id="TSO-in-snapshot-的调用频率"><a href="#TSO-in-snapshot-的调用频率" class="headerlink" title="TSO in_snapshot 的调用频率"></a>TSO <code>in_snapshot</code> 的调用频率</h3><p>之所以 <code>in_snapshot</code> 需要访问 TSO 是因为需要访问 <code>commit</code> 表。为了解决这个问题，Omid 利用 <code>commit</code> 表实际上是 <strong>append only</strong> 的特性，提出了一个巧妙的解决方案，将 <code>commit</code> 表的数据同步给客户端，以减少网络 I/O。而将 <code>commit</code> 表的数据同步给客户端的操作，是在客户端调用 TSO 开始一个事务的时候执行的，注意到 TSO 只需要将返回给客户端的时间戳之前的 <code>commit</code> 表的数据同步到客户端，就可以满足这个事务执行过程中 <code>in_snapshot</code> 的需求了。</p><h3 id="TSO-的稳定性"><a href="#TSO-的稳定性" class="headerlink" title="TSO 的稳定性"></a>TSO 的稳定性</h3><p>Omid 通过使用 replica 来保证 TSO 的稳定性。当 TSO 挂了的时候，一个 replica 成为新的 TSO 需要恢复</p><ol><li><code>commit</code> 表</li><li>abort 列表</li><li><em>T<sub>max</sub></em></li><li><code>last commit</code> 表</li></ol><p>对于前三者，Omid 使用 WAL 保证数据的持久化。Replica 在恢复是会重放 WAL 保证数据完整性。<br>至于 <code>last commit</code> 表，由于只用来检测写冲突，我们可以通过无脑 abort 所有比 replica 上位之后第一个事务还要早的事务，来避免恢复这个表。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;系列文章:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blogs.nearsyh.me/2020/07/07/2020-07-07-Snapshot-Isolation-Introduction/&quot;&gt;快照隔离在一些分布式系统中的实现 (1) </summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>快照隔离在一些分布式系统中的实现 (1) - 什么是快照隔离</title>
    <link href="http://nearsyh.me/2020/07/07/2020-07-07-Snapshot-Isolation-Introduction/"/>
    <id>http://nearsyh.me/2020/07/07/2020-07-07-Snapshot-Isolation-Introduction/</id>
    <published>2020-07-06T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.529Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>系列文章:</p><p><a href="https://blogs.nearsyh.me/2020/07/07/2020-07-07-Snapshot-Isolation-Introduction/">快照隔离在一些分布式系统中的实现 (1) - 什么是快照隔离</a><br><a href="https://blogs.nearsyh.me/2020/07/12/2020-07-12-Snapshot-Isolation-Omid-1/">快照隔离在一些分布式系统中的实现 (2) - Omid1</a></p></blockquote><p>最近在读一篇描述快照隔离的文章的时候，我发现自己已经差不多忘了之前读的论文里提到的各种分布式系统是怎么实现快照隔离的了。我不得不又重温了一下那些论文，但是这次要写几篇博客来总结一下。</p><h2 id="老生常谈的-ACID"><a href="#老生常谈的-ACID" class="headerlink" title="老生常谈的 ACID"></a>老生常谈的 ACID</h2><p>ACID 可以说是计算机专业学生的必学概念，同时也是互联网公司面试的常客。讨论快照隔离自然也没法绕开 ACID。ACID 是以下四个概念的缩写。</p><ol><li><strong>A</strong>tomic：原子性。一个事务的所有写操作要么全部完成（i.e. Commit)，要么全部失败（i.e. Abort）。<strong>注意</strong>这里的原子性和并发编程中的原子性的区别。并发编程中的原子性是指一个过程是“不可分割的”，其他线程无法看到这个过程进行到一半的状态（但是如果机器突然断电了，那么这个过程可能只完成了一部分）。</li><li><strong>C</strong>onsistent：一致性。这是一个业务概念，表示数据库的状态永远都是合法的。这个一致性，是通过 ACID 中的其他属性保证的。</li><li><strong>I</strong>solation：隔离性。若干个并发事务不应该互相影响，它们如果都提交成功，那么数据库的状态应该和他们以某种顺序依次执行后的状态相同，这是狭义上的隔离性，也被称作是 Serializable Isolation。在实际业务中，我们往往不需要这么强的隔离性保证。通过放松隔离性要求，我们往往可以获得更高的并发量，从而获得更高的吞吐量。</li><li><strong>D</strong>urable：持久性。每一个提交成功的事务包含的所有写操作都必须持久化的保存下来，即使数据库进程突然终止。</li></ol><p>上面的四个概念中，A，C，D 并没有什么可以变化的地方。而剩下的 I 则有很多变种。在介绍 I 的变种之前，我们需要先讨论这些 I 的变种到底是为什么被发明出来。</p><h2 id="并发事务可能带来的问题"><a href="#并发事务可能带来的问题" class="headerlink" title="并发事务可能带来的问题"></a>并发事务可能带来的问题</h2><p>如果没有了强隔离性的保证，并发事务可能会带来各种各样的问题。我们用教科书级的例子来描述各种可能出错的情况。假设我们有一张表 Accounts，它的 Schema 长这样：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> Accounts (</span><br><span class="line">  UserId <span class="built_in">varchar</span>(<span class="number">255</span>),</span><br><span class="line">  <span class="keyword">Type</span> <span class="built_in">varchar</span>(<span class="number">255</span>),</span><br><span class="line">  Balance <span class="built_in">int</span>,</span><br><span class="line">  PRIMARY <span class="keyword">KEY</span> (UserId, <span class="keyword">Type</span>)</span><br><span class="line">)</span><br></pre></td></tr></table></figure><h3 id="脏读-Dirty-Read"><a href="#脏读-Dirty-Read" class="headerlink" title="脏读 Dirty Read"></a>脏读 Dirty Read</h3><p>Dirty Read 是指当两个并发事务 Txn1 和 Txn2 执行时，Txn1 读取到了 Txn2 还没有提交的改动，不管 Txn2 之后实际是提交了还是没有提交。考虑这个例子，当用户 1 执行转账任务 (Txn 2) 的同时查询自己的账户余额 (Txn 1)，他会发现自己的余额总数不对。这是因为 Txn 1 读取了一个进行中的事务修改后的值。</p><p><img src="https://i.imgur.com/0GGs6rM.jpg" alt="Dirty Read"></p><h3 id="脏写-Dirty-Write"><a href="#脏写-Dirty-Write" class="headerlink" title="脏写 Dirty Write"></a>脏写 Dirty Write</h3><p>Dirty Write 是指当两个并发事务 Txn1 和 Txn 2 执行时，一个事务的写操作覆盖了另一个事务的未提交的写操作，导致两个事务都提交之后，数据库的状态不满足一致性。比如下面这里例子，当两个修改文件数据和元数据的事务并发执行后，元数据中的最后修改人，和文件的最新内容不匹配。</p><p><img src="https://i.imgur.com/3gLnczD.jpg" alt="Dirty Write"></p><h3 id="写丢失-Lost-Update"><a href="#写丢失-Lost-Update" class="headerlink" title="写丢失 Lost Update"></a>写丢失 Lost Update</h3><p>Lost Update 和 Dirty Write 类似，也是两个并发的写操作造成的 conflict。但是 Lost Update 有些不同，它没有覆盖未提交的写操作。比如下面这个例子，用户 1 的公司同时给用户 1 的账户发了奖金和工资，然而这两个事务由于出现了写丢失，导致用户 1 最终只收到了其中的一笔钱。</p><p><img src="https://i.imgur.com/TWSli2F.jpg" alt="Lost Update"></p><h3 id="不可重复读-Unrepeatable-Read-Fuzzy-Read-Read-Skew"><a href="#不可重复读-Unrepeatable-Read-Fuzzy-Read-Read-Skew" class="headerlink" title="不可重复读 Unrepeatable Read / Fuzzy Read / Read Skew"></a>不可重复读 Unrepeatable Read / Fuzzy Read / Read Skew</h3><p>Unrepeatable Read 是指一个事务 Txn1 中读取的数据被另一个并发事务 Txn2 修改了。在 Txn2 提交之后，Txn1 之前读取的数据已经失效，如果再重新读一次就会读到不同的值。然而，Txn1 往往不会重新读一次，而是会读取其他的被 Txn2 修改的数据。这样，Txn1 会同时读到 Txn2 提交前后两个不同版本的部分数据，从而破坏一直性。比如下面这个例子，在用户 1 执行转账的同时，他查询余额总数会得到不正确的结果。注意和 Dirty Read 的区别，Txn1 每次读取的数据都是已提交的数据。</p><p><img src="https://i.imgur.com/8Bosl9c.jpg" alt="Unrepeatable Read"></p><h3 id="写偏斜-Write-Skew"><a href="#写偏斜-Write-Skew" class="headerlink" title="写偏斜 Write Skew"></a>写偏斜 Write Skew</h3><p>Write Skew 是指两个事务并发读取一个数据集之后，同时修改不相干的两部分数据，造成的数据库不一致的问题。这个描述有些抽象，我们看下图的这个(可能并不自然的)例子。用户 1 同时刷了信用卡和申请贷款触发了两个事务。这两个事务都读取了该用户的余额总额，发现总额足够 100 之后，各自在 credit 和 loan 两个账户下扣除了 100 元。可以看到，Write Skew 归根结底，也是两个并发的写操作造成的 conflict。</p><p><img src="https://i.imgur.com/BIRpkN0.png" alt="Write Skew"></p><h2 id="如何解决上述问题"><a href="#如何解决上述问题" class="headerlink" title="如何解决上述问题"></a>如何解决上述问题</h2><p>上面的这些问题都是用于多个事务的并发执行导致的。为了解决这些问题，我们需要使用适当的隔离等级来约束事务的执行顺序/策略。例如，如果我们用最傻的办法，即全局锁，来实现所有事务都依次执行，我们就达成了 Serializable Isolation，显然上面的这些问题都不会发生。如果我们只需要解决其中的部分问题，我们可以使用更弱的隔离等级。下面会一一介绍几种常见的隔离等级，它们能解决的问题，以及它们的实现思路。注意，不同的数据库系统中，同一个名称的隔离等级表达的含义可能不同，大家使用的时候需要去读一下对应的文档。</p><h3 id="Read-Committed"><a href="#Read-Committed" class="headerlink" title="Read Committed"></a>Read Committed</h3><p>Read Committed 解决了 Dirty Read 和 Dirty Write 这两个问题。其中 Dirty Read 这个问题实在太严重了，几乎所有的（如果不是全部的）隔离等级都保证不会读取到未提交的数据。Read Committed 做的事情就和它的名字一样，保证读操作只能读取到提交了的数据。</p><p>常见的实现 Read Committed 的方式是通过读写锁。当写一个数据的时候，为数据上写锁（排他锁）。当读取一个数据的时候，为数据上读锁（共享锁）。这样，任何一个事务在读取的一个未提交的事务修改的数据时，会阻塞直到该事务提交。当然，这样简单的实现方式性能并不太理想，尤其是在一个事务需要用户同意时，会长时间的占用一个写锁，从而阻塞其他事务。一个比较简单的优化是，在事务上写锁之前，记录下该数据的当前值。其他的事务可以直接读取记录下的值，避免被写操作阻塞。</p><h3 id="Snapshot-Isolation"><a href="#Snapshot-Isolation" class="headerlink" title="Snapshot Isolation"></a>Snapshot Isolation</h3><p>Snapshot Isolation 解决了 Unrepeatable Read 和 Lost Update 的问题。它的思路是在一个事务执行过程中，数据库为它展示一个数据库在某个时间点的快照，这个快照包含了这个时间点之前所有提交的事务的执行结果。这样事务读取到的数据一定是一致的，Unrepeatable Read 的问题也就不存在了。除此之外，为了解决 Lost Update 的问题，每一个事务在提交之前，会检查自己修改的数据是否在提交之前被其他已经提交的事务修改了。如果已经被修改了，当前事务就必须被 Abort。</p><p>Snapshot Isolation 的常见实现是使用 MVCC，为同一份数据维护多个版本。当一个事务开始时为它分配一个时间戳 <em>T<sub>start</sub>*，这个事务的所有读取操作只会读这个时间戳之前的版本的数据，以此来达到快照的效果。同时，每一个事务会维护它修改的数据集合。在提交之前会分配一个时间戳 *T<sub>commit</sub>*，然后判断所有修改集合中的数据是否被提交时间在 *(T<sub>start</sub>, T<sub>commit</sub>)</em> 区间内的事务修改。如果没有才可以提交。注意这里的时间戳是单调递增的逻辑时间戳。</p><h3 id="Serializable"><a href="#Serializable" class="headerlink" title="Serializable"></a>Serializable</h3><p>Serializable 解决了所有的问题，因为它在观察者眼中，和所有事务依次执行是等价了。这是最强的隔离等级，所有的事务都互相“隔离”了。</p><p>Serializable 有多种实现方式。除了使用全局锁来事实上依次执行之外，实际使用的实现方式往往有：</p><ol><li>Two-Phase Lock，通过锁的形式保证。除了读写锁之外，还需要对索引范围，甚至整张表上锁。它的实现非常复杂，性能很差，并且非常容易出现死锁。</li><li>Serializable Snapshot Isolation，和 Snapshot Isolation 类似。但是除了像 Snapshot Isolation 一样检查两个写操作的冲突，还会检查读操作和写操作之间的冲突（维护读取的数据集合，如果集合中有数据在提交前被其他事务修改，也需要 Abort 事务）。</li></ol><h2 id="快照隔离"><a href="#快照隔离" class="headerlink" title="快照隔离"></a>快照隔离</h2><p>上面已经介绍了快照隔离的大概实现思路，这里再总结一下：</p><ol><li>数据存储层会维护每个数据的多个版本。</li><li>事务开始前会分配一个开始时间戳 *T<sub>start</sub>*。</li><li>事务会记录自己修改的所有数据集合。</li><li>事务提交之前先会分配一个提交时间戳 *T<sub>commit</sub>*。</li><li>分配完时间戳之后会检查是否有事务和自己的修改数据集合有交集，并且该事务的提交时间戳在 <em>(T<sub>start</sub>, T<sub>commit</sub>)</em> 之间。</li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;系列文章:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blogs.nearsyh.me/2020/07/07/2020-07-07-Snapshot-Isolation-Introduction/&quot;&gt;快照隔离在一些分布式系统中的实现 (1) </summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>实现简单的&quot;纤程&quot;</title>
    <link href="http://nearsyh.me/2020/06/22/2020-06-26-Cooperative-Threads/"/>
    <id>http://nearsyh.me/2020/06/22/2020-06-26-Cooperative-Threads/</id>
    <published>2020-06-21T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.528Z</updated>
    
    <content type="html"><![CDATA[<p>好长时间没有更新博客了。我最近读了 <a href="https://brennan.io/2020/05/24/userspace-cooperative-multitasking/">Implementing simple cooperative threads in C</a> 这篇文章。它说的 cooperative threads，实际上就是每一个“线程”或者说控制流，可以主动的让出 CPU 的使用权，来达成某种意义上的“合作”。这样的行为似乎很接近纤程/用户态线程，所以我在这里姑且翻译成了“纤程”。读完之后，我用 C++ 又重新实现了一下。很久没有写 C++ 了，写得磕磕绊绊的，源代码在<a href="https://github.com/nearsyh/continuation_blog/tree/master/cooperative_threads">这里</a>。</p><h2 id="接口"><a href="#接口" class="headerlink" title="接口"></a>接口</h2><p>这篇文章描述的实现思路很简单，它尝试用一个数据结构去描述一个任务执行的上下文，并使用一个队列来维护没有完成的任务。任务以及调度器的接口如下：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TaskHolder</span> &#123;</span></span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line">  <span class="comment">// 实际要执行的任务代码</span></span><br><span class="line">  <span class="keyword">void</span> (*_task)(Scheduler* scheduler);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">  TaskHolder(<span class="keyword">void</span> (*task)(Scheduler* scheduler));</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 实际触发任务执行的代码</span></span><br><span class="line">  <span class="function"><span class="keyword">virtual</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">(Scheduler* scheduler)</span> </span>&#123; _task(scheduler); &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Scheduler</span> &#123;</span></span><br><span class="line"> <span class="keyword">protected</span>:</span><br><span class="line">  <span class="comment">// 获取正在执行的任务</span></span><br><span class="line">  <span class="function"><span class="keyword">virtual</span> TaskHolder* <span class="title">get_current_task</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 设置正在执行的任务</span></span><br><span class="line">  <span class="function"><span class="keyword">virtual</span> <span class="keyword">void</span> <span class="title">set_current_task</span><span class="params">(TaskHolder* task_holder)</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 将当前任务移除队列</span></span><br><span class="line">  <span class="function"><span class="keyword">virtual</span> <span class="keyword">void</span> <span class="title">exit_current_task</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">  <span class="comment">// 添加任务</span></span><br><span class="line">  <span class="function"><span class="keyword">virtual</span> <span class="keyword">void</span> <span class="title">add_task</span><span class="params">(<span class="keyword">void</span> (*task)(Scheduler* scheduler))</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 任务主动让出 CPU, 由任务代码调用.</span></span><br><span class="line">  <span class="function"><span class="keyword">virtual</span> <span class="keyword">void</span> <span class="title">yield</span><span class="params">()</span> </span>= <span class="number">0</span>;</span><br><span class="line">    </span><br><span class="line">  <span class="comment">// 调度器开始执行任务</span></span><br><span class="line">  <span class="function"><span class="keyword">virtual</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>= <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>只看上面的接口可能有点抽象，下面给出一个实际的使用例子:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">scheduler::SequentialScheduler scheduler&#123;&#125;;</span><br><span class="line"><span class="comment">// 添加两个任务</span></span><br><span class="line">scheduler.add_task([](scheduler::Scheduler *scheduler) &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; <span class="number">2</span>; i++) &#123;</span><br><span class="line">        <span class="built_in">std</span>::<span class="built_in">cout</span> &lt;&lt; <span class="string">&quot;Task 1: &quot;</span> &lt;&lt; i &lt;&lt; <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line">        <span class="comment">// 让出 CPU 使用权</span></span><br><span class="line">        scheduler-&gt;yield();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line">scheduler.add_task([](scheduler::Scheduler *scheduler) &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; <span class="number">4</span>; i++) &#123;</span><br><span class="line">        <span class="built_in">std</span>::<span class="built_in">cout</span> &lt;&lt; <span class="string">&quot;Task 2: &quot;</span> &lt;&lt; i &lt;&lt; <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line">        <span class="comment">// 让出 CPU 使用权</span></span><br><span class="line">        scheduler-&gt;yield();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line"><span class="comment">// 开始执行任务, 当所有任务执行完毕后, run方法返回</span></span><br><span class="line">scheduler.run();</span><br><span class="line"><span class="built_in">std</span>::<span class="built_in">cout</span> &lt;&lt; <span class="string">&quot;Finish&quot;</span> &lt;&lt; <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br></pre></td></tr></table></figure><p>上述代码的输出是:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Task <span class="number">1</span>: <span class="number">0</span></span><br><span class="line">Task <span class="number">2</span>: <span class="number">0</span></span><br><span class="line">Task <span class="number">1</span>: <span class="number">1</span></span><br><span class="line">Task <span class="number">2</span>: <span class="number">1</span></span><br><span class="line">Task <span class="number">2</span>: <span class="number">2</span></span><br><span class="line">Task <span class="number">2</span>: <span class="number">3</span></span><br><span class="line">Finish</span><br></pre></td></tr></table></figure><h2 id="实现原理"><a href="#实现原理" class="headerlink" title="实现原理"></a>实现原理</h2><p>和原文的实现一样，我的实现包含了两个核心部分:</p><ol><li>使用<code>setjmp</code> 和 <code>longjmp</code>来实现控制流的转移，保留上下文，即所有寄存器的值。</li><li>自行维护每个任务的栈内存，保留栈空间。</li></ol><h3 id="setjmp-和-longjmp"><a href="#setjmp-和-longjmp" class="headerlink" title="setjmp 和 longjmp"></a><code>setjmp</code> 和 <code>longjmp</code></h3><p><code>setjmp</code> 和 <code>longjmp</code> 提供了类似汇编中 <code>jmp</code> 指令的功能. <code>setjmp</code>可以将当前指令的内存地址存储一个数据结构中（<code>jmp_buf</code>)，而 <code>longjmp</code> 可以返回 <code>jmp_buf</code> 指定的地址继续执行。下面我们看一个例子。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">jmp_buf target;</span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> value = setjmp(target);</span><br><span class="line"><span class="keyword">if</span> (value) &#123;</span><br><span class="line">    <span class="built_in">std</span>::<span class="built_in">cout</span> &lt;&lt; <span class="string">&quot;Someone jumps here! With a value &quot;</span> &lt;&lt; value &lt;&lt; <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="built_in">std</span>::<span class="built_in">cout</span> &lt;&lt; <span class="string">&quot;Set up a place for jumping&quot;</span> &lt;&lt; <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">longjmp(target, <span class="number">100</span>);</span><br></pre></td></tr></table></figure><p>上面这段代码会进入一个死循环，在输出一次 <strong>Set up a place for jumping</strong> 后会不断地输出 <strong>Someone jumps here! With a value 100</strong>。之所以有这样行为的原因主要是：</p><ol><li><code>longjmp</code> 会跳回 <code>setjmp</code> 发生的位置重新执行。</li><li><code>setjmp</code> 实际调用会返回 0，如果由 <code>longjmp</code> 调用则会返回 <code>longjmp</code> 的第二个参数值。</li></ol><h3 id="手动维护栈内存"><a href="#手动维护栈内存" class="headerlink" title="手动维护栈内存"></a>手动维护栈内存</h3><p>大家的知道，在执行代码的过程中，为了保证每个线程才能互不干扰的执行自己的逻辑，它们需要有独立的栈空间，操作系统会保证这一点。在我们现在的实现中，每个任务也需要有独立的栈内存。没有了操作系统的帮助，我们需要自己手动分配内存，并将其指定为栈内存。核心的实现代码如下：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 分配栈内存</span></span><br><span class="line">task-&gt;_stack_bottom = <span class="built_in">malloc</span>(stack_size);</span><br><span class="line"><span class="comment">// 计算栈顶地址</span></span><br><span class="line">task-&gt;_stack_top = task-&gt;_stack_bottom + stack_size;</span><br><span class="line"><span class="comment">// 使用汇编将 rsp 寄存器的值设置为我们分配的栈顶地址</span></span><br><span class="line"><span class="function"><span class="keyword">asm</span> <span class="title">volatile</span><span class="params">(<span class="string">&quot;mov %[rs], %%rsp \n&quot;</span> : [ rs ] <span class="string">&quot;+r&quot;</span>(task-&gt;_stack_top)::)</span></span></span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>实现代码见 <a href="https://github.com/nearsyh/continuation_blog/tree/master/cooperative_threads">Github</a>，这里就不再赘述了。</p><h3 id="可能的扩展"><a href="#可能的扩展" class="headerlink" title="可能的扩展"></a>可能的扩展</h3><p>Github 上给出了 <code>SequentialScheduler</code> 的实现，是一个单线程的版本。我感觉可以比较简单的扩展成可并发的版本。有兴趣的朋友可以试一下。</p><h3 id="为什么用纤程这个词"><a href="#为什么用纤程这个词" class="headerlink" title="为什么用纤程这个词"></a>为什么用纤程这个词</h3><p>我最开始在纤程和协程中犹豫了一下。</p><p>根据我个人的理解，协程之间应该有显式的控制流切换。而在我们的实现中，只有 task 和 <code>Scheduler</code> 之间存在控制流切换，因此我觉得协程不太恰当。</p><p>我们的实现，某种程度上似乎很像 event loop。最大的区别在于，我们并不需要一个事件来决定哪一个任务是可调度的。我的实现中，每一个任务都是可以调度的。</p><p>如果我们扩展成了可并发的版本，也许使用纤程就比较恰当了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;好长时间没有更新博客了。我最近读了 &lt;a href=&quot;https://brennan.io/2020/05/24/userspace-cooperative-multitasking/&quot;&gt;Implementing simple cooperative threads in </summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Vert.x 源码阅读 (4) - Context</title>
    <link href="http://nearsyh.me/2020/03/23/2020-03-23-Vertx-4-Context/"/>
    <id>http://nearsyh.me/2020/03/23/2020-03-23-Vertx-4-Context/</id>
    <published>2020-03-22T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.528Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><a href="https://blogs.nearsyh.me/2020/03/09/2020-03-09-Vertx-1-Future-Promise/">Vert.x 源码阅读 (1) - Future 和 Promise</a></p><p><a href="https://blogs.nearsyh.me/2020/03/10/2020-03-10-Vertx-2-Stream/">Vert.x 源码阅读 (2) - Stream</a></p><p><a href="https://blogs.nearsyh.me/2020/03/12/2020-03-12-Vertx-3-EventBus/">Vert.x 源码阅读 (3) - EventBus</a></p><p><a href="https://blogs.nearsyh.me/2020/03/23/2020-03-23-Vertx-4-Context/">Vert.x 源码阅读 (4) - Context</a></p></blockquote><p>这是 Vert.x 项目源码阅读笔记的第四篇，主要记录一下 Vertx 中的核心 <code>Context</code>。<code>Context</code> 贯穿了整个 Vertx 的代码，它主要用来表示一个任务执行时的上下文环境。</p><p>一个服务实例往往需要同时处理大量的请求，而这些请求相互独立，拥有不同的上下文。在使用 Servlet 的年代，由于每个线程同时只处理一个请求，我们使用一个简单的 <code>ThreadLocal</code> 变量就可以满足需求。然而，在使用 EventLoop 的时候，由于一个线程会同时处理多个请求，我们需要显式地管理和切换上下文。<code>Context</code> 是 Vertx 中上下文的抽象。</p><h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>在详细介绍 <code>Context</code> 之前，我们先介绍几个名词：</p><ul><li><code>Handler</code> 是一个可执行的对象，类似 <code>Runnable</code>。</li><li>线程，就不多说了。</li><li><em>Execution</em> 是指 <code>Handler</code> 的一次调用。<code>Handler</code>可以被多次调用。</li></ul><p><code>Context</code> 是</p><blockquote><p>The execution context of a Handler execution.</p></blockquote><p>简单来说，就是 <code>Handler</code> 的一次调用从开始到结束时，它使用的上下文信息。例如，一个 REST 请求执行过程中的 HEADER 信息。</p><p>在 Vertx 中，Context 和线程的关系简单来说，可以总结成一下几点</p><ol><li>一个线程在不同的时间，会执行不同的 <code>Handler</code>，因此和它相关联的 <code>context</code> (通过 <code>VertxThread::context</code>) 获取会发生变化。即**一个线程会对应到多个 <code>context</code>**。</li><li>一个 <code>Context</code> <strong>往往只对应到一个线程</strong>，但是并不强制。</li></ol><p>Context 的继承关系如下图：</p><p><img src="https://i.imgur.com/A6Rs1Ub.png" alt="Context"></p><p>根据上图，我们知道在 Vertx 主要有 <code>EventLoopContext</code> 和 <code>WorkerContext</code> 这两类 Context。这两类 Context，前一类对应到 <code>EventLoop</code> 线程，后一类对应到 <code>Worker</code> 线程。它们两个的区别主要在于，在执行/调度一个任务时（调用一个 <code>Handler</code> 时），到底使用哪一个线程。</p><h2 id="Context"><a href="#Context" class="headerlink" title="Context"></a><code>Context</code></h2><p><code>Context</code> 是一个接口，它主要包含了一下几类方法：</p><ol><li>获取当前执行线程的一些基本信息，例如 <code>isOnWorkerThread</code>。</li><li>获取自身属性，例如 <code>isEventLoopContext</code>。</li><li>读取或更新上下文信息，例如 <code>get</code>，<code>put</code>。</li><li>执行任务，例如 <code>runOnContext</code> ，<code>executeBlocking</code>。</li></ol><p>前三类方法都相对比较好理解，我们主要看看 <code>Context</code> 提供的执行和调度任务的方法。</p><h3 id="执行和调度任务"><a href="#执行和调度任务" class="headerlink" title="执行和调度任务"></a>执行和调度任务</h3><p><code>Context</code> 主要提供了四种类型的调度方式：</p><table><thead><tr><th>方式名称</th><th>执行线程</th><th>上下文</th></tr></thead><tbody><tr><td><code>execute</code></td><td>自己对应的线程</td><td>自己</td></tr><tr><td><code>schedule</code></td><td>自己对应的线程</td><td>线程执行任务时关联的 <code>context</code></td></tr><tr><td><code>emit</code></td><td>调用 <code>emit</code> 方法的线程</td><td>自己</td></tr><tr><td><code>dispatch</code></td><td>自己对应的线程</td><td>自己</td></tr></tbody></table><p>上表描述了各个类型的执行方法使用的线程和上下文，注意 <code>dispatch</code> 和 <code>execute</code> 的语义是一样的，它们的区别在于，</p><ul><li><code>execute</code> 将任务加入<strong>自己对应的线程</strong>的执行队列</li><li> <code>dispatch</code> 会判断当前线程是不是自己对应的线程，如果是的话直接执行。否则和 <code>execute</code> 一样。</li></ul><p>上面的描述可能比较抽象，下面用伪代码来简单实现这几种方式：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 通过保存和恢复现场，来实现在执行任务过程中，用自身作为上下文</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">emit</span><span class="params">(Runnable task)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 获取当前线程使用的 context，并将当前线程关联的 context 设置为自己</span></span><br><span class="line">  ContextInternal prev = emitBegin();</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    handler.run();</span><br><span class="line">  &#125; <span class="keyword">catch</span> (Throwable t) &#123;</span><br><span class="line">    reportException(t);</span><br><span class="line">  &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    <span class="comment">// 恢复当前线程使用的 context</span></span><br><span class="line">    emitEnd(prev);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在自身关联的线程上，执行任务。这个任务被 emit 包裹，保证执行是会使用自身做上下文。</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">execute</span><span class="params">(Runnable task)</span> </span>&#123;</span><br><span class="line">  getAssociatedThread().execute(() -&gt; emit(task));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在自身关联的线程上，执行任务。但是任务执行时的上下文没有单独指定。</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">schedule</span><span class="params">(T argument, Handler&lt;T&gt; task)</span> </span>&#123;</span><br><span class="line">  Thread thread = getAssociatedThread();</span><br><span class="line">  <span class="keyword">if</span> (thread.isCurrentThread()) &#123;</span><br><span class="line">    task.handle(argument);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    thread.execute(() -&gt; task.handle(argument));</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">dispatch</span><span class="params">(T argument, Handler&lt;T&gt; task)</span> </span>&#123;</span><br><span class="line">  schedule(v -&gt; emit(argument, task));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="执行阻塞代码"><a href="#执行阻塞代码" class="headerlink" title="执行阻塞代码"></a>执行阻塞代码</h2><p>在使用 <code>EventLoop</code> 时，我们不应该阻塞事件循环。这是因为事件循环的线程数很少，一旦阻塞了，新的请求都会被阻塞。当我们需要执行阻塞代码是，我们往往会使用另外的线程来执行。<code>Context</code> 提供了 <code>executeBlocking</code> 系列方法，来满足这个需求。它的实现伪代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> &lt;T&gt; <span class="function">Future&lt;T&gt; <span class="title">executeBlocking</span><span class="params">(Handler&lt;Promise&lt;T&gt;&gt; blockingCode, TaskQueue queue)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// Worker Pool 就是我们所说的非事件循环线程</span></span><br><span class="line">  <span class="keyword">return</span> executeBlocking(<span class="keyword">this</span>, blockingCode, workerPool, queue);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> &lt;T&gt; <span class="function">Future&lt;T&gt; <span class="title">executeBlocking</span><span class="params">(ContextInternal context, </span></span></span><br><span class="line"><span class="function"><span class="params">                                     Handler&lt;Promise&lt;T&gt;&gt; blockingCode,</span></span></span><br><span class="line"><span class="function"><span class="params">                                     WorkerPool workerPool, TaskQueue queue)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 创建一对 Promise 和 Future</span></span><br><span class="line">  <span class="comment">// Promise 会传个 blockingCode. BlockingCode 在完成后应该调用 promise 的 complete 方法</span></span><br><span class="line">  <span class="comment">// 调用者使用 Future 来监听 blockingCode 是否完成</span></span><br><span class="line">  Promise&lt;T&gt; promise = context.promise();</span><br><span class="line">  Future&lt;T&gt; fut = promise.future();</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 构造一个任务</span></span><br><span class="line">  Runnable command = () -&gt; &#123;</span><br><span class="line">    <span class="comment">// 使用 emit 方法，保证会使用 context 作为上下文</span></span><br><span class="line">    context.emit(promise, f -&gt; &#123;</span><br><span class="line">      blockingCode.handle(promise);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 使用 workerPool 和 queue 执行任务</span></span><br><span class="line">  queue.execute(command, exec);</span><br><span class="line">  <span class="keyword">return</span> fut;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>看到上面的代码，我们可以发现，指定的阻塞任务</p><ol><li>执行在 Worker 线程上</li><li>使用了当前的 (EventLoop) 的 Context</li></ol><p>这样实现看似有点奇怪，实际上满足了等效于在事件循环上”执行“阻塞代码的要求。这也是为什么 <code>Context</code> 会提供单独的 <code>isEventLoopContext/isWorkerContext</code> 和 <code>isOnEventLoopThread/isOnWorkerThread</code>方法。</p><h3 id="阻塞时长监控"><a href="#阻塞时长监控" class="headerlink" title="阻塞时长监控"></a>阻塞时长监控</h3><p>虽然我们允许使用 <code>Context</code> 执行阻塞代码，阻塞的时间仍然不能太久。Vertx 通过 <code>BlockedThreadChecker</code> 来监控线程的阻塞时间。</p><p><code>BlockedThreadChecker</code> 的实现比较简单。它通过一个后台线程，定期的检查每个线程当前执行的任务的开始时间离现在有多久。如果超过了最大时间，就打印一条日志。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://blogs.nearsyh.me/2020/03/09/2020-03-09-Vertx-1-Future-Promise/&quot;&gt;Vert.x 源码阅读 (1) - Future 和 Promise&lt;/a&gt;&lt;/p&gt;
</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>The Effective Engineer 读后整理</title>
    <link href="http://nearsyh.me/2020/03/14/2020-03-14-The-Effective-Engineer/"/>
    <id>http://nearsyh.me/2020/03/14/2020-03-14-The-Effective-Engineer/</id>
    <published>2020-03-13T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.528Z</updated>
    
    <content type="html"><![CDATA[<p><a href="http://www.effectiveengineer.com/">The Effective Engineer</a> 是 Edmond Lau 写的一本，关于如何让工程师更加高效的书。从前我对这类书，是有点排斥的，总觉得有成功学的味道，内容会比较空洞。但是因为在很多地方都看到有人推荐，同时自己也觉得个人效率到了一个瓶颈，所以就决定给它一个机会。读完之后，感觉没白花我这几个小时。这里简单整理一下我觉得对自己最有启发的一些观点。</p><h2 id="Leverage"><a href="#Leverage" class="headerlink" title="Leverage"></a>Leverage</h2><p>整本书最核心的观点，就是永远以 Leverage 为导向。Leverage = 产出 / 耗时，也就是单位时间内的产出。提高 Leverage 自然就会让自己更加高效，而方法也很自然，有三种：</p><ol><li>提高产出</li><li>减少耗时</li><li>换一个 Leverage 更高的任务来做</li></ol><p>这三个方法从数学层面很容易理解，但是执行起来并不容易。这本书就是围绕了这三个方面来展开的。接下来讲几个比较细节的点。</p><h2 id="优化学习"><a href="#优化学习" class="headerlink" title="优化学习"></a>优化学习</h2><p>学习的效果，和复利（利滚利）很相似。它是一个指数函数。曲线前期的斜率很小，但是到了后期就会 <a href="https://www.youtube.com/watch?v=y3pkgaboWFw">“火箭发射，轰隆隆隆”</a>。要注意的是，前期的一点点提高，对后期会有巨大的影响。作者也提到了他当时在 Google 花了大量时间阅读内部的代码和文档，看到这，我就痛心疾首，后悔自己在 Google 的时候怎么没有好好利用这么好的学习资源。</p><h2 id="任务管理"><a href="#任务管理" class="headerlink" title="任务管理"></a>任务管理</h2><p>作者使用 {“重要”，”不重要”} x {“紧急”，”不紧急”} 来区分所有的任务，并强调了<strong>重要但不紧急</strong>的任务。这种 classify 方式之前就听过很多次了，但是都没有深入了解，看了这本书才明白这么做的意义。我个人性格上的一个缺陷是没法忍受有任务没做完（用我爸的话来说，就是没有大将风度），而这些任务往往大部分都是紧急但不重要的。被这些任务占据了大量时间，导致自己推迟了很多重要但不紧急的，对个人成长有帮助的任务。而这些被推迟的任务实际上 Leverage 更高（见优化学习）。</p><h2 id="尽早建立-Feedback-Loop"><a href="#尽早建立-Feedback-Loop" class="headerlink" title="尽早建立 Feedback Loop"></a>尽早建立 Feedback Loop</h2><p>建立 Feedback Loop 本质上也是为了提高 Leverage。通过获取同事，上级的 Feedback，及时修正自己对 Leverage 的判断。这样我们可以一直去选择 Leverage 更高的任务，提高效率。</p><p>而由这一点出发，我们可以发现，有更多的执行准则，例如：</p><ol><li>尽早做原型，MVP，来获取 Feedback</li><li>开发时多做 instrument，多收集 Metric，多加测试</li><li>提高迭代速度，尽快上线，A/B 测试</li><li>先做风险最大的部分</li><li>警惕单人项目，多获取 Code Review（这是我个人的一大问题，开发太快，担心老让人 Review 会拖慢自己的开发进度，但是实际上磨刀不误砍柴工）</li></ol><h2 id="帮助周围的人成功"><a href="#帮助周围的人成功" class="headerlink" title="帮助周围的人成功"></a>帮助周围的人成功</h2><p>个人事业成功的秘诀是帮助周围的人成功。这一点是我之前没有意识到的，但是读了这本书之后深以为然。一个人的力量终归优先。让周围的人成长，将紧急但不重要的任务分发出去，让自己可以去做 Leverage 更高的事情。你可能觉得这么做是不是有点自私？其实，我们要意识到任务是否”紧急但是不重要”是一个主观标准。例如，对一个 senior 的工程师来说不重要的任务，对于 junior 的工程师也许是重要的，因为他们需要更多的训练来累积经验。</p><p>从这一点出发，我们也可以发现更多的原则：</p><ol><li>做好新入职员工的 onboard。他们越早适应，就可以有越块的成长（回忆一下学习曲线）。今天自己花几个小时帮助新同事更快熟悉，换来他们日后几年的贡献，孰轻孰重一目了然。</li><li>重视 sharing code ownership。这一点虽然有点反直觉，毕竟成为团队内唯一熟悉某个服务的人，这种感觉好像很好。但是仔细想一想，就会发现不分享 ownership 会导致两个问题：<ol><li>对公司来说，你是一个单点故障</li><li>对个人来说，因为你是唯一熟悉这个服务的人，所以大量紧急但不重要的 bug fix 需要你来做。你就没时间去做 leverage 更高的事情了。</li></ol></li></ol><h2 id="重视工具和自动化"><a href="#重视工具和自动化" class="headerlink" title="重视工具和自动化"></a>重视工具和自动化</h2><p>这也是老生常谈了。开发工具会暂时的花费时间，但是之后可以大大提供效率。我们在实操的时候要注意：</p><ol><li>重视工具的推广，用的人多，impact 才越大。而提高使用率，则需要尽早建立 Feedback。</li><li>渐进式开发，最开始不要想着一步到位，而是一点一点的优化工作流。这样的好处也是为了尽早获得 Feedback，同时也平衡了和其他任务的冲突。</li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;a href=&quot;http://www.effectiveengineer.com/&quot;&gt;The Effective Engineer&lt;/a&gt; 是 Edmond Lau 写的一本，关于如何让工程师更加高效的书。从前我对这类书，是有点排斥的，总觉得有成功学的味道，内容会比较空洞</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Vert.x 源码阅读 (3) - EventBus</title>
    <link href="http://nearsyh.me/2020/03/12/2020-03-12-Vertx-3-EventBus/"/>
    <id>http://nearsyh.me/2020/03/12/2020-03-12-Vertx-3-EventBus/</id>
    <published>2020-03-11T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.528Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><a href="https://blogs.nearsyh.me/2020/03/09/2020-03-09-Vertx-1-Future-Promise/">Vert.x 源码阅读 (1) - Future 和 Promise</a></p><p><a href="https://blogs.nearsyh.me/2020/03/10/2020-03-10-Vertx-2-Stream">Vert.x 源码阅读 (2) - Stream</a></p><p><a href="https://blogs.nearsyh.me/2020/03/12/2020-03-12-Vertx-3-EventBus/">Vert.x 源码阅读 (3) - EventBus</a></p><p><a href="https://blogs.nearsyh.me/2020/03/23/2020-03-23-Vertx-4-Context/">Vert.x 源码阅读 (4) - Context</a></p></blockquote><p>这是 Vert.x 项目源码阅读笔记的第三篇，主要记录一下 <code>EventBus</code> 相关的代码。<code>EventBus</code> 是一个轻量级的分布式消息系统，让 Vert.x 的服务中各个组件之间可以以一种低耦合的方式交互。代码在<a href="https://github.com/eclipse-vertx/vert.x/tree/master/src/main/java/io/vertx/core/eventbus">这个目录</a>下，主要包含了 <code>EventBus</code>，<code>MessageConsumer</code>，<code>MessageProducer</code>，以及 <code>DeliveryContext</code>。</p><h2 id="消息总线-EventBus"><a href="#消息总线-EventBus" class="headerlink" title="消息总线 EventBus"></a>消息总线 <code>EventBus</code></h2><p><code>EventBus</code> 提供了两类接口：</p><ol><li>通讯相关的接口，例如 <code>send</code>, <code>publish</code> 等等。</li><li>创建更加高级的抽象的接口，例如 <code>consumer</code>，<code>producer</code>。</li></ol><p><code>EventBus</code> 只提供的 <em>best-effort</em> 的投递保证，它提供了三种通讯模式：</p><table><thead><tr><th>模式名称</th><th>特征</th><th>方法</th></tr></thead><tbody><tr><td>Pub-Sub</td><td>异步单向，一对多</td><td><code>publish</code></td></tr><tr><td>P2P</td><td>不期待响应，一对一</td><td><code>send</code></td></tr><tr><td>Request-Response （以下简称 RR）</td><td>期待响应，一对一</td><td><code>request</code></td></tr></tbody></table><p>其中 P2P 和 RR 模式非常类似，区别只在于 RR 会指定一个 <code>replyHandler</code>（实现上会在投递的消息中指定一个 <code>replyAddress</code>，之后详细介绍）。</p><h3 id="Message-类"><a href="#Message-类" class="headerlink" title="Message 类"></a><code>Message</code> 类</h3><p><code>Message</code> 是实际发送的消息，它实际上是对网络请求的封装。它包含了消息的发送地址，接收地址等等，这里不详细介绍了。</p><h2 id="MessageConsumer"><a href="#MessageConsumer" class="headerlink" title="MessageConsumer"></a>MessageConsumer</h2><p><code>MessageConsumer</code> （逻辑上很自然地）扩展了 <code>ReadStream&lt;Message&lt;T&gt;&gt;</code>接口，是对消息处理做的抽象。它是线程安全的，但是如果能只在单一线程（也就是之后会讲到的事件循环线程）上被使用的话，性能会更好一点。这是因为它的并发控制使用了 <code>synchronize</code> 关键字，而 JVM 在没有竞争的情况下会退化到使用偏向锁，减少同步开销。</p><h3 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h3><p><code>MessageConsumer</code> 本身是一个接口，它的实现是 <code>MessageConsumerImpl</code>。我们这里先看一下它的核心方法之一，<code>doReceive(Message&lt;T&gt; message)</code>。它是新消息进入时的入口方法，核心代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">doReceive</span><span class="params">(Message&lt;T&gt; message)</span> </span>&#123;</span><br><span class="line">  Handler&lt;Message&lt;T&gt;&gt; theHandler;</span><br><span class="line">  <span class="keyword">synchronized</span> (<span class="keyword">this</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (demand == <span class="number">0L</span>) &#123;</span><br><span class="line">      <span class="comment">// 如果没有需求</span></span><br><span class="line">      <span class="comment">// 回忆一下之前提到的 Stream 的背压处理</span></span><br><span class="line">      <span class="keyword">if</span> (pending.size() &lt; MaxBuffer) &#123;</span><br><span class="line">        pending.add(message);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 超过最大缓冲大小的话就丢弃</span></span><br><span class="line">        discardHandler.handle(message);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (pending.size() &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="comment">// 从之前 pending 中最早的消息开始处理</span></span><br><span class="line">        pending.add(message);</span><br><span class="line">        message = pending.poll();</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="comment">// handler 是 mututable 的</span></span><br><span class="line">      <span class="comment">// copy 一下 handler 的引用，在同步块外实际调用 handler</span></span><br><span class="line">      <span class="comment">// 很典型的并发编程模式</span></span><br><span class="line">      theHandler = handler;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 将消息发送给 handler.</span></span><br><span class="line">  deliver(theHandler, message);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从这个方法我们看出来 <code>MessageConsumer</code> 的运作原理。</p><p>这里还有一个比较有意思的地方是 <code>deliver</code> 方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">deliver</span><span class="params">(Handler&lt;Message&lt;T&gt;&gt; theHandler, Message&lt;T&gt; message)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 给 Producer 回血</span></span><br><span class="line">  String creditsAddress = message.headers().get(CREDIT_ADDRESS_HEADER_NAME);</span><br><span class="line">  <span class="keyword">if</span> (creditsAddress != <span class="keyword">null</span>) &#123;</span><br><span class="line">    eventBus.send(creditsAddress, <span class="number">1</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 实际将消息发送给 handler.</span></span><br><span class="line">  dispatch(theHandler, message, context.duplicate());</span><br><span class="line">  checkNextTick();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个方法的前四行代码发送了 <code>1</code> 这条消息给 <code>creditsAddress</code>，实际上是给 <code>MessageProducer</code> 监听的一个”队列”发送 <code>credit</code>，<code>MessageProducer</code> 收到这个 <code>credit</code> 就会给自己”回一滴血”，表示自己能够多一个可以发消息的额度。</p><h2 id="MessageProducer"><a href="#MessageProducer" class="headerlink" title="MessageProducer"></a>MessageProducer</h2><p><code>MessageProducer</code> （在逻辑上很自然地）扩展了 <code>WriteStream&lt;T&gt;</code> 接口，是对消息发送做的抽象。</p><h3 id="实现-1"><a href="#实现-1" class="headerlink" title="实现"></a>实现</h3><p><code>MessageProducerImpl</code> 是 <code>MessageProducer</code> 的实现。它有两个核心方法，分别是 <code>write</code> 和 <code>doReceiveCredit</code>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">write</span><span class="params">(T data, Handler&lt;AsyncResult&lt;Void&gt;&gt; handler)</span> </span>&#123;</span><br><span class="line">  Promise&lt;Void&gt; promise = createPromise();</span><br><span class="line">  promise.future().setHandler(handler);</span><br><span class="line">  write(data, promise);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">write</span><span class="params">(T data, Promise&lt;Void&gt; handler)</span> </span>&#123;</span><br><span class="line">  MessageImpl msg = createMessage();</span><br><span class="line">  OutboundDeliveryContext&lt;T&gt; sendCtx = createContext();</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 只有在 P2P 和 RR 模式下，send 才是 true。</span></span><br><span class="line">  <span class="comment">// 这也很合理。在 Pub-Sub 模式下，Producer 不会管下游&quot;死活&quot;，下游甚至都可能没有任何接受者。</span></span><br><span class="line">  <span class="comment">// 而在 P2P 和 RR 模式下，我们要考虑下游，实现背压。</span></span><br><span class="line">  <span class="keyword">if</span> (send) &#123;</span><br><span class="line">    <span class="keyword">synchronized</span> (<span class="keyword">this</span>) &#123;</span><br><span class="line">      <span class="comment">// credits 有点类似令牌桶</span></span><br><span class="line">      <span class="keyword">if</span> (credits &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        credits--;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 没有额度就先 pending。</span></span><br><span class="line">        <span class="comment">// 回忆一下 WriteStream 和 ReadStream 的流量控制机制。</span></span><br><span class="line">        <span class="comment">// WriteStream::writeQueueFull 的实现就是判断 credits == 0</span></span><br><span class="line">        pending.add(sendCtx);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  bus.sendOrPubInternal(msg, options, <span class="keyword">null</span>, handler);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//============================================</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">doReceiveCredit</span><span class="params">(<span class="keyword">int</span> credit)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 回复发送额度</span></span><br><span class="line">  credits += credit;</span><br><span class="line">  <span class="keyword">while</span> (credits &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="comment">// 看看有没有 pending 的内容可以发的。</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 看看是不是从超负荷恢复过来了</span></span><br><span class="line">  <span class="comment">// 是的话，要调用 drainHandler</span></span><br><span class="line">  checkDrained();</span><br><span class="line">&#125;</span><br><span class="line">   </span><br></pre></td></tr></table></figure><p>最开始我比较困惑的一点是，通过网络来恢复额度，总感觉不是很稳，要是网络挂了，就没法回血来允许接着发消息。但是仔细想想，要是网络挂了，发消息也没有意义了。</p><h2 id="DeliveryContext"><a href="#DeliveryContext" class="headerlink" title="DeliveryContext"></a>DeliveryContext</h2><p><code>DeliveryContext</code> 封装了一个要发送的消息，同时提供了一些控制方法。它最核心的方法就是 <code>next()</code>。一个 <code>DeliveryContext</code> 会包含多个 <em>interceptor</em>，而 <code>next</code> 会遍历这些 interceptor，一个个调用，直到全部调用完之后，再实际发送。这个类似于责任链模式，在很多框架中都使用了，例如 Spring Boot，Netty。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://blogs.nearsyh.me/2020/03/09/2020-03-09-Vertx-1-Future-Promise/&quot;&gt;Vert.x 源码阅读 (1) - Future 和 Promise&lt;/a&gt;&lt;/p&gt;
</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Vert.x 源码阅读 (2) - Stream</title>
    <link href="http://nearsyh.me/2020/03/10/2020-03-10-Vertx-2-Stream/"/>
    <id>http://nearsyh.me/2020/03/10/2020-03-10-Vertx-2-Stream/</id>
    <published>2020-03-09T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.527Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><a href="https://blogs.nearsyh.me/2020/03/09/2020-03-09-Vertx-1-Future-Promise/">Vert.x 源码阅读 (1) - Future 和 Promise</a></p><p><a href="https://blogs.nearsyh.me/2020/03/10/2020-03-10-Vertx-2-Stream">Vert.x 源码阅读 (2) - Stream</a></p><p><a href="https://blogs.nearsyh.me/2020/03/12/2020-03-12-Vertx-3-EventBus/">Vert.x 源码阅读 (3) - EventBus</a></p><p><a href="https://blogs.nearsyh.me/2020/03/23/2020-03-23-Vertx-4-Context/">Vert.x 源码阅读 (4) - Context</a></p></blockquote><p>这是 Vert.x 项目源码阅读笔记的第二篇，主要记录一下 <code>Stream</code> 这个抽象。<code>Stream</code> 是 <code>Vertx</code> 中消息传递使用的底层抽象。主要代码在<a href="https://github.com/eclipse-vertx/vert.x/tree/master/src/main/java/io/vertx/core/streams">这个目录</a>下。其中两个接口比较重要，分别是 <code>ReadStream</code> 和 <code>WriteStream</code>。这两个接口都扩展了 <code>StreamBase</code> 这个接口。从名字就能看出来，这两个接口分别对应了读取和写入，我们可以不严谨地类比到 C++ 里的 <code>std::cin</code> 和 <code>std::cout</code>。</p><h2 id="ReadStream"><a href="#ReadStream" class="headerlink" title="ReadStream"></a><code>ReadStream</code></h2><h3 id="响应式"><a href="#响应式" class="headerlink" title="响应式"></a>响应式</h3><p><code>ReadStream</code> 提供的接口是响应式的。使用者可以设置数据的回调方法，一旦有数据过来，回调就会被调用。这个和 <code>Reactive</code> 编程方式提供的抽象有点相似。</p><h3 id="读取模式"><a href="#读取模式" class="headerlink" title="读取模式"></a>读取模式</h3><p><code>ReadStream</code> 提供了两种读取模式，它们分别是:</p><ol><li><code>Flowing</code></li><li><code>Fetching</code></li></ol><p>这两种模式可以类比为 Push 和 Pull 模式。<code>ReadStream</code> 通过 <code>pause</code>， <code>resume</code>，还有 <code>fetch</code> 三个方法来切换读取模式。<code>pause</code> 方法会进入 <code>Fetching</code> 模式，而 <code>resume</code> 方法会进入 <code>Flowing</code> 模式。<code>fetch</code> 方法则是用来在 <code>Fetch</code> 模式下指定需要读取多少元素。</p><p>下面是简化后的 <code>ReadStream</code> 接口的一个实现 <code>MessageConsumerImpl</code> 的代码。主要省略了边界条件处理。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">MessageConsumer</span>&lt;<span class="title">T</span>&gt; <span class="keyword">extends</span> <span class="title">ReadStream</span>&lt;<span class="title">T</span>&gt; </span>&#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MessageConsumerImpl</span>&lt;<span class="title">T</span>&gt; <span class="keyword">implements</span> <span class="title">MessageConsumer</span>&lt;<span class="title">T</span>&gt; </span>&#123;</span><br><span class="line">  <span class="comment">// 这个字段用来表示想要读取多少元素。如果是 Long.MAX_VALUE，则表示正处在 Flowing 模式下。</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">long</span> demand = Long.MAX_VALUE;</span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> MessageConsumer&lt;T&gt; <span class="title">pause</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    demand = <span class="number">0L</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> MessageConsumer&lt;T&gt; <span class="title">resume</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> fetch(Long.MAX_VALUE);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> MessageConsumer&lt;T&gt; <span class="title">fetch</span><span class="params">(<span class="keyword">long</span> amount)</span> </span>&#123;</span><br><span class="line">    demand += amount;</span><br><span class="line">    <span class="keyword">if</span> (demand &gt; <span class="number">0L</span>) &#123;</span><br><span class="line">      <span class="comment">// 异步 consume 数据</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="WriteStream"><a href="#WriteStream" class="headerlink" title="WriteStream"></a><code>WriteStream</code></h2><p><code>WriteStream</code> 提供了异步的写入数据的接口，这个接口比较自然，所以不做太多讨论。下面我们主要看看它提供的流量控制的接口。</p><h3 id="流量控制-Flow-Control"><a href="#流量控制-Flow-Control" class="headerlink" title="流量控制 Flow Control"></a>流量控制 Flow Control</h3><p><code>WriteStream</code> 提供的和流量控制的相关接口主要有三个</p><ol><li><code>setWriteQueueMaxSize(int maxSize)</code> 设置了最大的 Write Queue 的大小。</li><li><code>boolean writeQueueFull()</code> 返回当前是否 Write Queue 已写满。</li><li><code>drainHandler(Handler&lt;Void&gt; handler)</code> 用来指定当 Write Queue 从 Full 的状态恢复的时候，应该执行什么逻辑。</li></ol><p>有了这三个方法，我们就能在一定程度上保证流量控制了。下面是一个简单的例子，假设我们想从一个 <code>ReadStream</code> 读取数据，然后写到 <code>WriteStream</code> 中。为了避免 <code>WriteStream</code> 的下游承受的住，我们可以这么写：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 我们想把数据从 readStream 读出来，然后转移到 writeStream 中。</span></span><br><span class="line">ReadStream&lt;T&gt; readStream = ...;</span><br><span class="line">WriteStream&lt;T&gt; writeStream = ...;</span><br><span class="line"></span><br><span class="line">readStream.handler(data -&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (writeStream.writeQueueFull()) &#123;</span><br><span class="line">    <span class="comment">// 发现 Queue 写满了，就暂停一下</span></span><br><span class="line">    readStream.pause();</span><br><span class="line">    <span class="comment">// 在 Queue 恢复了之后，自动 resume</span></span><br><span class="line">    writeStream.drainHandler(any -&gt; &#123;</span><br><span class="line">      readStream.resume();</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    writeStream.write(data);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>Vert.x 提供了 <code>Pump</code> 和 <code>Pipe</code> 两个接口，它们的实现和上面的代码类似。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://blogs.nearsyh.me/2020/03/09/2020-03-09-Vertx-1-Future-Promise/&quot;&gt;Vert.x 源码阅读 (1) - Future 和 Promise&lt;/a&gt;&lt;/p&gt;
</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Vert.x 源码阅读 (1) - Future 和 Promise</title>
    <link href="http://nearsyh.me/2020/03/09/2020-03-09-Vertx-1-Future-Promise/"/>
    <id>http://nearsyh.me/2020/03/09/2020-03-09-Vertx-1-Future-Promise/</id>
    <published>2020-03-08T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.527Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><a href="https://blogs.nearsyh.me/2020/03/09/2020-03-09-Vertx-1-Future-Promise/">Vert.x 源码阅读 (1) - Future 和 Promise</a></p><p><a href="https://blogs.nearsyh.me/2020/03/10/2020-03-10-Vertx-2-Stream/">Vert.x 源码阅读 (2) - Stream</a></p><p><a href="https://blogs.nearsyh.me/2020/03/12/2020-03-12-Vertx-3-EventBus/">Vert.x 源码阅读 (3) - EventBus</a></p><p><a href="https://blogs.nearsyh.me/2020/03/23/2020-03-23-Vertx-4-Context/">Vert.x 源码阅读 (4) - Context</a></p></blockquote><p>这是 Vert.x 项目源码阅读笔记的第一篇, 主要讲一下最基础的 <code>Future</code> 和 <code>Promise</code> 的概念。<code>Future</code> 和 <code>Promise</code> 实际上是异步编程中非常基础的两个抽象，大部分程序员想必都非常熟悉，我这里主要是整理为主。</p><h2 id="Vert-x-中的-Future-和-Promise"><a href="#Vert-x-中的-Future-和-Promise" class="headerlink" title="Vert.x 中的 Future 和 Promise"></a>Vert.x 中的 <code>Future</code> 和 <code>Promise</code></h2><h3 id="AsyncResult-接口"><a href="#AsyncResult-接口" class="headerlink" title="AsyncResult 接口"></a><code>AsyncResult</code> 接口</h3><p>Vert.x 中的 <code>Future</code> 扩展了 <code>AsyncResult</code>。<code>AsyncResult</code> 有点像一个 <code>Monad</code>，它包装了一个结果，或者是一个错误。注意 <code>AsyncResult</code> 这个接口并不包含结果还没有准备就绪的状态，这个状态是由 <code>Future</code> 暴露出来的。</p><h3 id="Future-接口"><a href="#Future-接口" class="headerlink" title="Future 接口"></a><code>Future</code> 接口</h3><p><code>Future</code> 逻辑上表达了一个异步计算的结果。它扩展了 <code>AsyncResult</code> 接口，主要提供了两类方法：</p><ol><li>判断是否已经完成：<code>isComplete</code>。</li><li>注册回调方法，例如 <code>onComplete</code>。</li></ol><p>等待结果的一方直接面对的，应该是一个 <code>Future</code> 对象。这个对象对于结果使用方来说，是只读的。使用方可以不断的 <code>poll</code> 它直到计算完成，结果 ready。同时使用也可用基于这个 <code>Future</code> 创建一个新的 <code>Future</code> 对象。</p><h3 id="Promise-接口"><a href="#Promise-接口" class="headerlink" title="Promise 接口"></a><code>Promise</code> 接口</h3><p><code>Promise</code> 有点难解释，我觉得我们可以把它理解成 <code>Future</code> 的可写的另一端。实际完成异步计算的结果提供方，在干完活之后，将结果写入这个 <code>Promise</code>。<code>Promise</code> 会负责通知自己对应的 <code>Future</code> 结果已经准备就绪了。</p><h2 id="Golang-中的-Channel"><a href="#Golang-中的-Channel" class="headerlink" title="Golang 中的 Channel"></a>Golang 中的 <code>Channel</code></h2><p>我感觉 Golang 中的 <code>channel</code> 是一个有点相似的概念。<code>Future</code> 和 <code>Promise</code> 是 <code>channel</code> 的两端。这也意味着，我们可以用 <code>Future</code> 和 <code>Promise</code> 来做线程间同步，虽然我们不应该怎么做，而应该根据你的情况使用更加适合的，专门用于同步的抽象。</p><h2 id="一般的使用场景"><a href="#一般的使用场景" class="headerlink" title="一般的使用场景"></a>一般的使用场景</h2><p>在一般情况下，<code>Promise</code> 和 <code>Future</code> 的使用往往符合这样的模式。我们假设存在 A 和 B 两个实体，A 需要 B 执行一个任务，然后将结果告诉自己。为了达到这个目的，一般会经过一下几个步骤：</p><ol><li>A 触发整个流程，往往是通过调用 B 提供的某个接口，这个接口一般都返回一个 <code>Future</code> 对象。</li><li>B 的接口中创建了一对 <code>Promise</code> 和 <code>Future</code>。<code>Future</code> 会被返回给 A。</li><li>A 获得返回的 <code>Future</code> 之后一般会在这个 <code>Future</code> 上注册回调函数，表示收到结果之后要做什么。</li><li>B 在结果计算出来之后调用 <code>Promise::complete</code> 方法，而这个 <code>complete</code> 方法会调用 A 在 <code>Future</code> 上注册的函数。</li></ol><h2 id="样例"><a href="#样例" class="headerlink" title="样例"></a>样例</h2><p>为了加深理解，这里给出几段代码，用来品品这两个抽象的使用方式。为了方便理解，这里去掉了和 <code>context</code> 相关的代码。</p><h3 id="Future-的-map-方法"><a href="#Future-的-map-方法" class="headerlink" title="Future 的 map 方法"></a><code>Future</code> 的 <code>map</code> 方法</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Future&lt;O&gt; <span class="title">map</span><span class="params">(Function&lt;I, O&gt; transformer)</span> </span>&#123;</span><br><span class="line">  Promise&lt;O&gt; ret = Promise.promise();</span><br><span class="line">  <span class="keyword">this</span>.setHandler(asyncResult -&gt; &#123;</span><br><span class="line">    <span class="keyword">if</span> (asyncResult.succeed()) &#123;</span><br><span class="line">      ret.complete(transformer.apply(asyncResult.result()));</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      ret.fail(asyncResult.cause())</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="keyword">return</span> ret.future();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过 <code>map</code> 的代码，我们可以清楚地发现，<code>Promise</code> 和 <code>Future</code> 非常类似于一个 <code>channel</code>。我们总是留住一根管子的一端，把另一端交给下游。自己干完活就对着管子吼一声。</p><h3 id="CompositeFuture-的实现"><a href="#CompositeFuture-的实现" class="headerlink" title="CompositeFuture 的实现"></a><code>CompositeFuture</code> 的实现</h3><p>这里为了方便理解做了一点简化，实际上 <code>CompositeFuture</code> 是一个接口，真正的实现在 <code>CompositeFutureImpl</code> 中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CompositeFuture</span> <span class="keyword">implements</span> <span class="title">Future</span>&lt;<span class="title">CompositeFuture</span>&gt; </span>&#123;</span><br><span class="line">  <span class="comment">// 自己包含的子 Future 的数量</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">int</span> count;</span><br><span class="line">  <span class="comment">// 用来表示自己所有子 Future 完成的 Promise</span></span><br><span class="line">  <span class="keyword">private</span> Promise promise = Promise.promise();</span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> CompositFuture <span class="title">all</span><span class="params">(List&lt;Future&lt;?&gt;&gt; futures)</span> </span>&#123;</span><br><span class="line">    CompositFutureImpl composit = <span class="keyword">new</span> CompositFutureImpl(futures);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (Future&lt;?&gt; future : futures) &#123;</span><br><span class="line">      future.setHandler(result -&gt; &#123;</span><br><span class="line">        <span class="keyword">if</span> (result.succeed()) &#123;</span><br><span class="line">          composit.count ++;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (count == composit.len) &#123;</span><br><span class="line">          composit.succeed();</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>CompositFuture</code> 的实现思路也很简单，就是在每一个子 <code>Future</code> 上注册一个回调来通知自己一个子 <code>Future</code> 已经完成。如果所有的子 <code>Future</code> 都完成了，就可以把自己标记成完成。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://blogs.nearsyh.me/2020/03/09/2020-03-09-Vertx-1-Future-Promise/&quot;&gt;Vert.x 源码阅读 (1) - Future 和 Promise&lt;/a&gt;&lt;/p&gt;
</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>OPED-11 Physalia - 分散力量办大事, 实现元数据强一致且高可用</title>
    <link href="http://nearsyh.me/2020/03/08/2020-03-08-Physalia/"/>
    <id>http://nearsyh.me/2020/03/08/2020-03-08-Physalia/</id>
    <published>2020-03-07T16:00:00.000Z</published>
    <updated>2022-01-23T11:52:47.527Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>OPED 是 (我自己发起的) <strong>O</strong>ne <strong>P</strong>aper <strong>E</strong>ach <strong>D</strong>ay 挑战, 即<del>每天</del>(偶尔)读一篇 Paper, 领域不限.</p><p>这是 OPED 挑战的第十一篇, 论文为 <a href="https://assets.amazon.science/c4/11/de2606884b63bf4d95190a3c2390/millions-of-tiny-databases.pdf">Millions of Tiny Databases</a></p></blockquote><p>今天介绍的这一篇论文是 AWS 发表的. 它描述了 AWS 为了保证 EBS (大概可以翻译成云盘?) 正确地做 replication 和 fail over 而实现的一个兼顾 C 和 A 的系统.</p><h2 id="名字的由来"><a href="#名字的由来" class="headerlink" title="名字的由来"></a>名字的由来</h2><p><a href="https://en.wikipedia.org/wiki/Portuguese_man_o'_war">Physalia</a> 中文名叫僧帽水母。</p><blockquote><p>虽然僧帽水母像<a href="https://zh.wikipedia.org/wiki/%E6%B0%B4%E6%AF%8D">水母</a>，但其实是一个包含<a href="https://zh.wikipedia.org/w/index.php?title=%E6%B0%B4%E8%9E%85%E9%AB%94&action=edit&redlink=1">水螅体</a>及<a href="https://zh.wikipedia.org/w/index.php?title=%E6%B0%B4%E6%AF%8D%E9%AB%94&action=edit&redlink=1">水母体</a>的群落。</p></blockquote><p>今天介绍的 Physalia 这个系统，某种意义上来说和 Physalia 这个生物有点相似.</p><p>另外, Physalia, 又被称为 Man-Of-War. 虽然我知道没有什么关系，但我还是不由自主地联想到了人民战争的汪洋大海，以及集中力量办大事的优越性。</p><h2 id="要解决的问题"><a href="#要解决的问题" class="headerlink" title="要解决的问题"></a>要解决的问题</h2><p>熟悉 AWS 的朋友想必都知道，EBS 为 EC2 提供了块设备 (block device)，EC2 可以像挂载普通硬盘一样挂载它。然而，EBS 在实现上并不是一个真的本地硬盘，而是通过网络访问的。为了不让使用者操心这些，同时也是为了保证高可用，EBS 会做 Replication 和自动的 Fail Over，保证一个 EBS 节点挂了或者网络分区了，EC2 仍然可以不受影响。</p><p>为了做到这一点，我们就需要有一种强一致的方式，获取一个 EBS 到底有哪些备份，以及到底哪一个备份是当前的 <strong>Primary</strong>。这个问题在分布式场景中很常见，我们往往是通过某个实现了共识算法的组件来解决这个问题，例如使用 <em>etcd</em>，<em>zookeeper</em>等等。然而这种系统的一个问题是，如果出现了网络分区，分区中较小的一块中的所有节点，都无法工作了（i.e. 这是一个 CP 系统）。</p><p>Physalia 的目的，就是实现一个<strong>配置服务</strong>，在保证强一致的情况下，尽可能保证高可用。</p><h2 id="核心思路"><a href="#核心思路" class="headerlink" title="核心思路"></a>核心思路</h2><h3 id="核心概念"><a href="#核心概念" class="headerlink" title="核心概念"></a>核心概念</h3><p>为了解决上面提出的问题，Physalia 提出了两个核心概念:</p><ol><li>Blast Radius: 一个 Failure 波及的用户数量。像 <em>zookeeper</em> 这种服务，Blast Radius 就相对比较大。</li><li>Correlation of Failure: 我们经常听到<em>两地三中心</em>这种说法，它的意思是在两个空间上分割的地点部署三个数据中心。这样做的目的是为了提高数据中心出现问题这种事件的独立性。</li></ol><h3 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h3><p>Physalia 解决问题的核心思路就是</p><p><strong>利用对网络拓扑的理解，构造更细粒度的 Paxos Group，每一个 Group 只负责处理一小部分节点，从而减少 Blast Radius</strong></p><h3 id="具体做法"><a href="#具体做法" class="headerlink" title="具体做法"></a>具体做法</h3><p>Physalia 将一个配置服务中的所有节点（Colony），分成若干个 Cell。每一个 Cell 包含了 7 个 Node。每一个 Cell 构成了一个 Paxos Group，并且 Cell 之间都完全独立，不互相依赖。每一个 Cell 都只负责一小部分 EBS 的配置，保证了如果一个 Cell 中的网络不通，或者节点挂了，只会有一小部分 EBS 收到影响，从而减小了 Blast Radius。</p><h4 id="Cell-和-EBS-的分配"><a href="#Cell-和-EBS-的分配" class="headerlink" title="Cell 和 EBS 的分配"></a>Cell 和 EBS 的分配</h4><p>上面的思路已经比较明确了，需要注意的一点是怎么来决定哪一个 Cell 负责保存哪一个 EBS 的配置信息。这里就需要 Physalia 对网络拓扑有了解，保证每一个 Cell 和 EBS 的距离不会很远，否则”路上”任一一段网络出了问题，就会波及更多的节点，做不到减小 Blast Radius。</p><h4 id="Reconfiguration"><a href="#Reconfiguration" class="headerlink" title="Reconfiguration"></a>Reconfiguration</h4><p>除了 EBS 和负责它的 Cell 之间的距离不能太大之外，Cell 和使用它的客户（EC2）也不能太远。因此 Physalia 中包含了一个 <em>control-plane</em> 不断的优化 Cell 的放置，优化网络距离。由于每一个 Cell 中保存的数据（只保存了它负责的少量的 EBS 的配置数据）很小，数据迁移的开销也很小。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>我感觉 Physalia 的思路就和我标题了说的一样，分散力量办大事。它将一个大问题拆解成了<strong>独立</strong>的小问题，然后再<strong>独立</strong>地解决这些小问题。它的思路很有意思，但是这样做的代价就是让整个系统变得更加复杂。尤其是在引入了 Cell 的动态分配之后，整个系统的可预测性似乎也受到了影响，要是出了什么问题，感觉很难查错。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;OPED 是 (我自己发起的) &lt;strong&gt;O&lt;/strong&gt;ne &lt;strong&gt;P&lt;/strong&gt;aper &lt;strong&gt;E&lt;/strong&gt;ach &lt;strong&gt;D&lt;/strong&gt;ay 挑战, 即&lt;del&gt;每天&lt;/del&gt;(偶尔</summary>
      
    
    
    
    
  </entry>
  
</feed>
