<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Wonderland</title>
  <icon>https://github.com/waynexia.png</icon>
  
  <link href="https://waynexia.github.io/atom.xml" rel="self"/>
  
  <link href="https://waynexia.github.io/"/>
  <updated>2026-01-14T13:19:51.701Z</updated>
  <id>https://waynexia.github.io/</id>
  
  <author>
    <name>Wayne</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>2025 的记录</title>
    <link href="https://waynexia.github.io/2026/01/2025-summary/"/>
    <id>https://waynexia.github.io/2026/01/2025-summary/</id>
    <published>2026-01-13T22:10:53.000Z</published>
    <updated>2026-01-14T13:19:51.701Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><em>读一卷书，行万里路</em></p></blockquote><h1 id="冬"><a href="#冬" class="headerlink" title="冬"></a>冬</h1><h2 id="映画"><a href="#映画" class="headerlink" title="映画"></a>映画</h2><pre><code>名侦探柯南 百万美元的五棱星奇异博士2好东西失控玩家美国队长 美丽新世界</code></pre><h2 id="番剧"><a href="#番剧" class="headerlink" title="番剧"></a>番剧</h2><pre><code>神是中学生Ave MujicaS 级贝西摩斯药屋少女的呢喃 S2天久鹰央的推理病理表香格里拉边境 S2 下半金牌得主青之驱魔师 S5 终夜篇</code></pre><h2 id="书"><a href="#书" class="headerlink" title="书"></a>书</h2><pre><code>第七重解答 保罗 霍尔特彩虹牙刷 早坂吝卡拉马佐夫兄弟 陀思妥耶夫斯基图书馆之谜 青崎有吾</code></pre><h1 id="春"><a href="#春" class="headerlink" title="春"></a>春</h1><h2 id="映画-1"><a href="#映画-1" class="headerlink" title="映画"></a>映画</h2><pre><code>碟中谍8 最终清算哆啦A梦 大雄的绘画奇遇记</code></pre><h2 id="番剧-1"><a href="#番剧-1" class="headerlink" title="番剧"></a>番剧</h2><pre><code>300年史莱姆 S2小市民 S2时光流逝饭菜依然美味记忆缝线夏日口袋末日后酒店Lazarus黑镜s6</code></pre><h2 id="书-1"><a href="#书-1" class="headerlink" title="书"></a>书</h2><pre><code>水族馆之谜 青崎有吾美德的不幸 萨德围城 钱锺书麦卡托如是说 麻耶雄嵩</code></pre><h1 id="夏"><a href="#夏" class="headerlink" title="夏"></a>夏</h1><h2 id="映画-2"><a href="#映画-2" class="headerlink" title="映画"></a>映画</h2><pre><code>柯南 独眼的残像</code></pre><h2 id="番剧-2"><a href="#番剧-2" class="headerlink" title="番剧"></a>番剧</h2><pre><code>彻夜之歌 S2转生七王子的魔法全解 S2青春猪头少年不会梦到圣诞服女郎胆大党 S2小城日常末日后酒店乱马1/2the pitt</code></pre><h2 id="书-2"><a href="#书-2" class="headerlink" title="书"></a>书</h2><pre><code>禅与摩托车维修艺术 罗伯特 波西格金阁寺 三岛由纪夫海伯利安 丹 西蒙斯海伯利安的陨落</code></pre><h1 id="秋"><a href="#秋" class="headerlink" title="秋"></a>秋</h1><h2 id="映画-3"><a href="#映画-3" class="headerlink" title="映画"></a>映画</h2><pre><code>创战神极速追杀 1/2/3/4/外传利刃出鞘 王者归来惊天魔盗团3</code></pre><h2 id="番剧-3"><a href="#番剧-3" class="headerlink" title="番剧"></a>番剧</h2><pre><code>琉璃的宝石基地 S1 S2 S3同乐者</code></pre><h2 id="书-3"><a href="#书-3" class="headerlink" title="书"></a>书</h2><pre><code>占星术杀人魔法 岛田庄司母语之外的旅行 多和田叶子</code></pre>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;读一卷书，行万里路&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&quot;冬&quot;&gt;&lt;a href=&quot;#冬&quot; class=&quot;headerlink&quot; title=&quot;冬&quot;&gt;&lt;/a&gt;冬&lt;/h1&gt;&lt;h2 id=&quot;映画&quot;&gt;&lt;a href=&quot;#映画</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>VictoriaLogs Source Reading</title>
    <link href="https://waynexia.github.io/2025/01/victorialogs-source-reading/"/>
    <id>https://waynexia.github.io/2025/01/victorialogs-source-reading/</id>
    <published>2025-01-06T21:20:17.000Z</published>
    <updated>2026-01-14T13:19:51.714Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Overview"><a href="#Overview" class="headerlink" title="Overview"></a>Overview</h1><p>VictoriaLogs is a new and prominent player in the log management sector. Developed by the same team behind VictoriaMetrics, the code is not built on top of VictoriaMetrics. Compared to its competitors, VictoriaLogs is more lightweight: one of the key differentiators for VictoriaLogs is its architectural design and usage. It can be used as a standalone log management system or as a command line tool similar to <code>grep</code>. On the query interface, it has proposed a new DSL named <code>LogsQL</code> which generally follows a pipe style.</p><p>This project is relatively “young”, and a major part of the codebase is from an “initial” <a target="_blank" rel="noopener" href="https://github.com/VictoriaMetrics/VictoriaMetrics/commit/87b66db47da23cb9fe800ce16efa6bdac365f5a8">patch</a>. The team is and likely will be mainly focusing on the storage (database) part akin to VictoriaMetrics. At the time of writing, there are two ways to visualize the logs: Grafana data source <a target="_blank" rel="noopener" href="https://github.com/VictoriaMetrics/victorialogs-datasource/">plugin</a> (not available in the plugin store) and a built-in UI. It plans to support cluster mode in early 2025.</p><p><em>Fun fact: VictoriaLogs seems to be proposed by a community contributor. Here is the <a target="_blank" rel="noopener" href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/816">original list</a> (back to Oct. 2020)</em></p><p>This blog is based on commit <code>e0b2c1c4f5b68070c8945c587e17d6e9415c48b5</code>, Tue Dec 24.</p><h2 id="Code-line-count"><a href="#Code-line-count" class="headerlink" title="Code line count"></a>Code line count</h2><p>Before diving in, let’s have a quick look at the repository. Both VictoriaLogs and VictoriaMetrics, as well as other related projects like agent or alert services, are placed in one main <a target="_blank" rel="noopener" href="https://github.com/VictoriaMetrics/VictoriaMetrics/">repository</a>.</p><p>Pie charts of the code line count:</p><div class="heti heti--columns-2">    <figure class="figure-image">      <img src="line-of-repo.svg" alt="Line count of the repository" width="100%" height="100% loading="lazy" />      <figcaption>Line count of the repository</figcaption>    </figure>      <figure class="figure-image">      <img src="line-of-logstorage.svg" alt="Line count of logstorage package" width="100%" height="100% loading="lazy" />      <figcaption>Line count of logstorage package</figcaption>    </figure>  </div><p>From the above charts, we can see that the logstorage package is nearly 1/4 of this project. To clarify, the logstorage package doesn’t reuse much code from VictoriaMetrics; basic components like <code>block</code>, <code>indexdb</code>, etc., are duplicated with slight differences. This, on the other hand, shows that the project is relatively independent and concise.</p><p>The <code>logstorage</code> package above is the major implementation of VictoriaLogs, including its query language, storage, index, etc. The second figure on the right side is a rough breakdown of <code>logstorage</code>. It’s composed of a few key components: <code>pipe</code>, <code>filter</code>, <code>stats</code>, <code>block</code>, <code>parser</code>, and so on. We will cover them in the following sections.</p><p>Another interesting finding is that in either the whole project level or in <code>logstorage</code>, the test code is about 1/2 of the normal logic. For example, inside <code>logstorage</code>, there are 33620 lines of test code among 73861 lines in total.</p><p>These two figures can give us a rough idea of the project structure and what might be the key components. Now let’s dive into the code implementation.</p><h1 id="Key-concepts"><a href="#Key-concepts" class="headerlink" title="Key concepts"></a>Key concepts</h1><p>Firstly, there are some key concepts and their relations in VictoriaLogs (referred to as <code>logstorage</code> later on) that I came across many times. Some of them are exposed to the user, while others are internal:</p><ul><li>Data Model:<ul><li><strong>Stream</strong>: High-level representation of a group of log data (like a stream). Stream is used to group log entries logically.</li><li><strong>Block</strong>: Data representation within streams. A stream is divided into blocks for efficient storage and retrieval.</li><li><strong>Field</strong>: A field in log entries, like the column of data. Fields can be further divided into “Message Field”, “Time Field” and “Stream Field”.</li></ul></li></ul>    <figure class="figure-image">      <img src="data-model.svg" alt="Abstract data model of VictoriaLogs" width="70%" height="100% loading="lazy" />      <figcaption>Abstract data model of VictoriaLogs</figcaption>    </figure>  <div class="heti heti--columns-2"><ul><li>Storage:<ul><li><strong>Partition</strong>: Data is partitioned by time range. Each partition is one day (<code>getPartitionForDay</code>). First level of organizing data (and index).</li><li><strong>DataDB</strong>: Stores log data in blocks. Works like an LSM tree.</li><li><strong>Part</strong>: Has <code>InMemoryPart</code> and <code>DiskPart</code>. <code>InMemoryPart</code> is like memtable and will be flushed to <code>DiskPart</code> when necessary.</li><li><strong>IndexDB</strong>: Manages indexing of log data. Based on <code>mergeset</code>.</li></ul></li></ul><p>Those “words” can be found in the file system. The following is an example file structure of VictoriaLogs’ data directory ➡️:</p><ul><li>Query:<ul><li><strong>Search</strong>: A query to logstorage is called “search”.</li><li><strong>Tokenizer</strong>: Tokenize log entries for efficient searching and indexing.</li><li><strong>Filter</strong>: Various filters (e.g., time range, string match, regex) are used to narrow down log entries.</li><li><strong>Pipe</strong>: Advanced processing after filtering.</li><li><strong>BloomFilter</strong>: Helps quickly check the existence of log entries to accelerate queries.</li></ul></li></ul><figure class="highlight plaintext"><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></pre></td><td class="code"><pre><code class="hljs plaintext">❯ ll -T victoria-logs-data<br><br>Size Name<br>   - victoria-logs-data<br>   0 ├── flock.lock<br>   - └── partitions<br>   -     └── 20250113<br>   -         ├── datadb<br>   -         │   ├── 181A232DC5D34F7D<br> 48M         │   │   ├── bloom.bin0<br> 48M         │   │   ├── bloom.bin1<br>  54         │   │   ├── column_names.bin<br>736k         │   │   ├── columns_header.bin<br> 78k         │   │   ├── columns_header_index.bin<br>136k         │   │   ├── index.bin<br>   0         │   │   ├── message_bloom.bin<br>   0         │   │   ├── message_values.bin<br> 250         │   │   ├── metadata.json<br> 110         │   │   ├── metaindex.bin<br> 21M         │   │   ├── timestamps.bin<br>110M         │   │   ├── values.bin0<br>437M         │   │   └── values.bin1<br> 210         │   └── parts.json<br>   -         └── indexdb<br>   -             ├── 181A232DC5CDBAB4<br>  52             │   ├── index.bin<br>  26             │   ├── items.bin<br>   8             │   ├── lens.bin<br> 163             │   ├── metadata.json<br>  51             │   └── metaindex.bin<br>  20             └── parts.json<br></code></pre></td></tr></table></figure></div><p>A LogsQL example:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">_time:5m log.level:error -app:(buggy_app OR foobar) | delete host, app | stats count() errors<br></code></pre></td></tr></table></figure><ul><li>Utilities:<ul><li><strong>ChunkedAllocator</strong>: Allocates memory in chunks to reduce overhead.</li><li><strong>Encoding</strong>: Handles encoding and decoding of log data.</li><li><strong>Hasher</strong>: Provides hashing functions for log entries.</li><li><strong>Parser</strong>: Parses log entries and queries.</li></ul></li></ul><h1 id="Storage"><a href="#Storage" class="headerlink" title="Storage"></a>Storage</h1><p>In the storage part, we’ll cover how the ingest request is handled, how the data is stored, and how to maintain related structures.</p><h2 id="Ingest"><a href="#Ingest" class="headerlink" title="Ingest"></a>Ingest</h2><p>The incoming log data is parsed to identify the tenant, stream tags, timestamp, etc. The log data is represented as the following:</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></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// LogRows holds a set of rows needed for Storage.MustAddRows</span><br><span class="hljs-comment">//</span><br><span class="hljs-comment">// LogRows must be obtained via GetLogRows()</span><br><span class="hljs-keyword">type</span> LogRows <span class="hljs-keyword">struct</span> &#123;<br>streamIDs []streamID<br>timestamps []<span class="hljs-keyword">int64</span><br>    <span class="hljs-comment">// Field is key-value pairs</span><br>rows [][]Field<br><br>    <span class="hljs-comment">// streamFields contains names for stream fields</span><br>streamFields <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">struct</span>&#123;&#125;<br><span class="hljs-comment">// ignoreFields contains names for log fields, which must be skipped during data ingestion</span><br>ignoreFields <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">struct</span>&#123;&#125;<br><span class="hljs-comment">// extraFields contains extra fields to add to all the logs at MustAdd().</span><br>extraFields []Field<br><span class="hljs-comment">// extraStreamFields contains extraFields, which must be treated as stream fields.</span><br>extraStreamFields []Field<br><br>    <span class="hljs-comment">// ...omitted</span><br>&#125;<br></code></pre></td></tr></table></figure><p><code>streamID</code> is a unique identifier for a stream, and can be used to locate the stream in the indexdb or in filter by user. <code>streamID</code> is generated by hashing the stream tags and tenant id in <code>MustAdd</code> method. Technically, <code>streamID</code> can be generated outside of the VictoriaLogs.</p><p>Log rows are batched into an in-memory part that can’t be searched. Each <code>LogRows</code> will be converted to an <code>inmemoryPart</code>. Once the batch is large enough, or enough time has passed, a <code>block</code> is created and written to disk (<code>mustWriteTo</code>). <code>blockStreamWriter</code> is responsible for this, along with its associated index structures (<code>blockHeader</code>, etc.) and bloom filters.</p><p><code>indexdb</code> is updated to make sure every <code>streamID</code> is registered in the indexdb (via <code>mustRegisterStream</code>) so subsequent queries can locate the new data.</p><p>Periodically, these in-memory parts are merged and flushed to long-term storage for efficient querying. This is done by <code>datadb</code>.</p>    <figure class="figure-image">      <img src="ingest.svg" alt="Ingest process in VictoriaLogs" width="70%" height="100% loading="lazy" />      <figcaption>Ingest process in VictoriaLogs</figcaption>    </figure>  <p>Above is the rough process of handling ingest requests. On the protocol side, VictoriaLogs supports mainstream protocols like Grafana Loki, jsonline, Elasticsearch, OpenTelemetry, syslog, etc. Agents (log collector) like Fluent Bit, Logstash, Fluentd, and Vector can be used to send logs to VictoriaLogs.</p><h2 id="Storage-Block"><a href="#Storage-Block" class="headerlink" title="Storage - Block"></a>Storage - Block</h2><p><code>block</code> is the physical abstraction of log data. There is another “block” in <code>storage</code> which is similar to this one (as well as <code>indexdb</code>), but they are different structures. Like <code>block</code> in <code>logstorage</code> is used to store string log data, and the <code>indexdb</code> in <code>storage</code> doesn’t have a concept of “stream”. The hierarchy of related concepts is like this: <code>block</code> &lt;- <code>part</code> &lt;- <code>partition</code>.</p><p>Some basic information about <code>block</code>, many of them are inherited from <code>part</code>:</p><ul><li>Log entries from the same stream are packed together into the same blocks.</li><li>Each block contains columns covering unique field names across logs (horizontally partitioned).</li><li>Smaller blocks are automatically merged into bigger blocks in the background.</li><li><strong>Blocks exceeding size limits are split into multiple smaller blocks</strong>.</li><li>Partition: data is partitioned by time range.</li></ul><p>A block (in-memory representation) is simply defined as:</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></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// block represents a block of log entries.</span><br><span class="hljs-keyword">type</span> block <span class="hljs-keyword">struct</span> &#123;<br>    <span class="hljs-comment">// timestamps contains timestamps for log entries.</span><br>    timestamps []<span class="hljs-keyword">int64</span><br>    <span class="hljs-comment">// columns contains values for fields seen in log entries.</span><br>    columns []column<br>    <span class="hljs-comment">// constColumns contains fields with constant values across all the block entries.</span><br>    constColumns []Field<br>&#125;<br></code></pre></td></tr></table></figure><p>On generating a block, the log entries are encoded and stored in file-system. In this step, some “encoding” is applied:</p><ul><li>Data will be encoded using some specific types (integer, float, ipv4, timestamp). This is dynamically probed from log content.</li><li>String encoding is similar to Arrow BytesArray: a number array to record length, and a byte array to store the string content.</li><li>General compression like zstd is also applied on the marshaled binary data.</li></ul><p>Other structures like bloom filter, and index are also generated together and stored in the same directory.</p><h2 id="Storage-IndexDB"><a href="#Storage-IndexDB" class="headerlink" title="Storage - IndexDB"></a>Storage - IndexDB</h2><p>Victorialogs maintains an LSM (merge set) for stream metadata. Provides accurate point get and filter on stream id (akin to time-series).</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(idb *indexdb)</span> <span class="hljs-title">searchStreamIDs</span><span class="hljs-params">(tenantIDs []TenantID, sf *StreamFilter)</span> []<span class="hljs-title">streamID</span></span> &#123;&#125;<br></code></pre></td></tr></table></figure><p><code>StreamFilter</code> is a simple filter condition on tags (<code>_stream:&#123;...&#125;</code>).</p><!-- todo: some illustrate --><h2 id="Storage-Cache"><a href="#Storage-Cache" class="headerlink" title="Storage - Cache"></a>Storage - Cache</h2><!-- todo: less code and add one figure --><p><code>logstorage</code> maintains a very simple cache implementation to store <code>streamID</code>, and supports <code>filterStream</code> for quick access. <code>InMemoryParts</code> can also be treated as cache, but it’s more like a buffer for data from disk and we won’t cover them here.</p><p>The <code>cache</code> struct is a double-buffering map defined as follows:</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></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> cache <span class="hljs-keyword">struct</span> &#123;<br>    curr atomic.Pointer[sync.Map]<br>    prev atomic.Pointer[sync.Map]<br>    stopCh <span class="hljs-keyword">chan</span> <span class="hljs-keyword">struct</span>&#123;&#125;<br>    wg     sync.WaitGroup<br>&#125;<br></code></pre></td></tr></table></figure><ul><li><code>curr</code> &amp; <code>prev</code>: Atomic pointers to the cache (<code>sync.Map</code>).</li><li><code>stopCh</code> &amp; <code>wg</code>: for graceful shutdown (yes this cache is not just a pure “algorithm”)</li></ul><p>The system will periodically clean the cache to swap the current and previous caches. The <code>clean</code> method simply swaps the current and previous caches. I’m very curious about the performance of this implementation compared to the traditional LRU cache. I can almost assert it would be faster, but wondering by how much. I recall a <a target="_blank" rel="noopener" href="https://github.com/waynexia/texn/blob/master/src/dropmap.rs">similar thing</a> which is written for simplicity and performance.</p><p>On querying, it will first try to find in the <code>curr</code> cache, then the <code>prev</code> cache. And finally <code>indexdb</code> if the cache misses.</p><p>This <code>cache</code> is a small piece among caches. There are many other in-place caches in the system, like metadata of a block, bloom filter to evaluating filters etc. Those are also simply in-memory maps.</p><h1 id="Query"><a href="#Query" class="headerlink" title="Query"></a>Query</h1><div class="heti heti--columns-2"><p>At a high level, its capabilities can be grouped into <em>filters</em> (ways to locate logs of interest) and <em>pipes</em> (ways to transform or process the results).</p><p><strong>Filters</strong>: These are building blocks for narrowing down log messages based on specific criteria (e.g., time ranges, word matches, field values). Filters include time filters, word/phrase/prefix filters, stream filters, range filters, etc.</p><p><strong>Pipes</strong>: Once filters have selected some logs, pipes let you transform or process these results. Some examples include:</p><ul><li><code>sort by (_time)</code> to reorder logs chronologically,</li><li><code>limit &lt;N&gt;</code> to retrieve only the top N results,</li><li><code>stats</code> to compute aggregate metrics like counts over the filtered logs.</li></ul>    <figure class="figure-image">      <img src="query.svg" alt="General query process" width="100%" height="100% loading="lazy" />      <figcaption>General query process</figcaption>    </figure>  </div><h2 id="Query-Filter"><a href="#Query-Filter" class="headerlink" title="Query - Filter"></a>Query - Filter</h2><p>There are various filter mechanisms to efficiently query and filter log entries.</p>    <figure class="figure-image">      <img src="filter.svg" alt="Filter in VictoriaLogs" width="70%" height="100% loading="lazy" />      <figcaption>Filter in VictoriaLogs</figcaption>    </figure>  <p>The interface is defined as follows:</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></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// filter must implement filtering for log entries.</span><br><span class="hljs-keyword">type</span> filter <span class="hljs-keyword">interface</span> &#123;<br>    <span class="hljs-comment">// String returns string representation of the filter</span><br>    String() <span class="hljs-keyword">string</span><br>    <span class="hljs-comment">// udpdateNeededFields must update neededFields with fields needed for the filter</span><br>    updateNeededFields(neededFields fieldsSet)<br>    <span class="hljs-comment">// applyToBlockSearch must update bm according to the filter applied to the given bs block</span><br>    applyToBlockSearch(bs *blockSearch, bm *bitmap)<br>    <span class="hljs-comment">// applyToBlockResult must update bm according to the filter applied to the given br block</span><br>    applyToBlockResult(br *blockResult, bm *bitmap)<br>&#125;<br></code></pre></td></tr></table></figure><p><code>applyToBlockSearch</code> and <code>applyToBlockResult</code> have similar logic. On querying, filters are applied to stored data via <code>applyToBlockSearch</code>, which will generate an initial filter result. Then, if there is any pipe operation, <code>applyToBlockResult</code> may be called (based on the implementation) to do a post-filtering.</p><p>Filters work by first evaluating at block level with bloom filter to omit blocks quickly, then examining each log entries in one block and set the result to bitmap, which is used to indicate whether a log entry is matched or not.</p><h2 id="Query-Pipes"><a href="#Query-Pipes" class="headerlink" title="Query - Pipes"></a>Query - Pipes</h2><p>In LogsQL, an example using pipes is as follows:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">_time:5m error | sort by (_time) desc | limit 10 | stats count()<br></code></pre></td></tr></table></figure><p>Some key takeaways about Pipe:</p><ul><li>Run after all filters (except the <code>filter</code> pipe which can perform post-filtering)</li><li>Push mode in execution<ul><li>Filter “pushes” result to pipe processors, at block level</li><li>Pipe processors “push” their intermediate result to the next pipe</li><li>Collect all data at the final collector.</li></ul></li><li>Interface<ul><li>Interfaces of <code>pipe</code> and <code>pipeProcessor</code> are very simple, and can be simplified to some kinds of function pointer.</li></ul></li></ul><p>Among various <code>pipe</code>s, the <code>stat</code> pipe is an exception in the aspect of complexity. It can be treated as the third major concept in log search, with the <code>pipe</code> type. It is some kind of “UDAF” for computing log entries. Common computation like <code>min()</code>, <code>count()</code>, <code>uniq()</code> (unique) or <code>quantile()</code> are supported out-of-the-box.</p><p>Both <code>pipe</code> and <code>stat</code> are easy to extend. A major part of query features will be implemented in these forms. Log queries usually don’t need complex computation or relation like in generic SQL, so this framework could cover a great part of the requirements.</p><h1 id="Misc"><a href="#Misc" class="headerlink" title="Misc"></a>Misc</h1><h2 id="JSON"><a href="#JSON" class="headerlink" title="JSON"></a>JSON</h2><ul><li>VictoriaLogs automatically transforms multi-level (nested) JSON into single-level JSON during data ingestion through a systematic flattening process<a target="_blank" rel="noopener" href="https://docs.victoriametrics.com/victorialogs/keyconcepts/">1</a>.</li><li>Nested dictionaries are flattened by joining keys with a dot (<code>.</code>) character. (e.g.: <code>host.os.version</code>)</li><li>Arrays, numbers, and boolean values are converted to strings.</li></ul><h2 id="Pool-anywhere"><a href="#Pool-anywhere" class="headerlink" title="Pool anywhere"></a>Pool anywhere</h2><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></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> tokenizer <span class="hljs-keyword">struct</span> &#123;<br>    m <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">struct</span>&#123;&#125;<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getTokenizer</span><span class="hljs-params">()</span> *<span class="hljs-title">tokenizer</span></span> &#123;<br>    v := tokenizerPool.Get()<br>    <span class="hljs-keyword">if</span> v == <span class="hljs-literal">nil</span> &#123;<br>        <span class="hljs-keyword">return</span> &amp;tokenizer&#123;<br>            m: <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">struct</span>&#123;&#125;),<br>        &#125;<br>    &#125;<br>    <span class="hljs-keyword">return</span> v.(*tokenizer)<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">putTokenizer</span><span class="hljs-params">(t *tokenizer)</span></span> &#123;<br>    t.reset()<br>    tokenizerPool.Put(t)<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="Arena"><a href="#Arena" class="headerlink" title="Arena"></a>Arena</h2><p><code>arena</code> is used for efficient memory allocation, are cached to avoid the overhead of repeatedly creating and destroying these objects. In the major source of small allocation (query processing), <code>arena</code> is used among 13 pipe processors (30+ in total) and other misc structs like reader, intermediate result, codec, etc.</p><!-- todo: worker# Product Level Review## Great method is simple## Searching with a better `grep`## Compare with other log management systems --><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li>CTO’s overview slides about VL in 2023.05: <a target="_blank" rel="noopener" href="https://www.slideshare.net/slideshow/victorialogs-previewpdf/257033587">https://www.slideshare.net/slideshow/victorialogs-previewpdf/257033587</a></li><li>Repository: VictoriaMetrics’ <a target="_blank" rel="noopener" href="https://github.com/VictoriaMetrics/VictoriaMetrics">main repository</a> and the <a target="_blank" rel="noopener" href="https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/lib/logstorage">logstorage dir</a></li><li>Key Concepts chapter from VictoriaLogs docs: <a target="_blank" rel="noopener" href="https://docs.victoriametrics.com/victorialogs/keyconcepts/">https://docs.victoriametrics.com/victorialogs/keyconcepts/</a></li><li>LogsQL reference: <a target="_blank" rel="noopener" href="https://docs.victoriametrics.com/victorialogs/logsql/">https://docs.victoriametrics.com/victorialogs/logsql/</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;Overview&quot;&gt;&lt;a href=&quot;#Overview&quot; class=&quot;headerlink&quot; title=&quot;Overview&quot;&gt;&lt;/a&gt;Overview&lt;/h1&gt;&lt;p&gt;VictoriaLogs is a new and prominent player in </summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>2024 的记录</title>
    <link href="https://waynexia.github.io/2024/12/2024-summary/"/>
    <id>https://waynexia.github.io/2024/12/2024-summary/</id>
    <published>2024-12-31T21:00:04.000Z</published>
    <updated>2026-01-14T13:19:51.701Z</updated>
    
    <content type="html"><![CDATA[<h1 id="冬"><a href="#冬" class="headerlink" title="冬"></a>冬</h1><h2 id="映画"><a href="#映画" class="headerlink" title="映画"></a>映画</h2><p>　　名侦探柯南 黑铁的鱼影<br>　　坚如磐石<br>　　周处除三害<br>　　沙丘 2</p><h2 id="番剧"><a href="#番剧" class="headerlink" title="番剧"></a>番剧</h2><p>　　想要成为魔法少女<br>　　pluto<br>　　迷宫饭<br>　　公主大人拷问时间到了<br>　　香格里拉 frontier<br>　　不死不幸<br>　　我内心糟糕的念头 第二季<br>　　青之驱魔师 岛根启明神社篇<br>　　葬送的芙莉莲<br>　　药物少女的呢喃<br>　　魔都精兵的奴隶<br>　　Severance<br>　　vivant<br>　　宝可梦礼宾部<br>　　三体 Netflix 版</p><h2 id="书"><a href="#书" class="headerlink" title="书"></a>书</h2><p>　　红莲馆杀人事件 阿津川辰海<br>　　过于喧嚣的孤独 赫拉巴尔</p><h1 id="春"><a href="#春" class="headerlink" title="春"></a>春</h1><h2 id="映画-1"><a href="#映画-1" class="headerlink" title="映画"></a>映画</h2><p>　　告白 中岛哲也<br>　　哆啦A梦：大雄的地球交响乐</p><h2 id="番剧-1"><a href="#番剧-1" class="headerlink" title="番剧"></a>番剧</h2><p>　　辐射<br>　　interspecies reviewers<br>　　时光巡逻队 S1<br>　　このすば S3<br>　　七王子的魔法学习之路<br>　　终末电车去哪里<br>　　转生史莱姆 S3<br>　　Girls Band Cry<br>　　迷宫饭 下半<br>　　狼与香辛料</p><h2 id="书-1"><a href="#书-1" class="headerlink" title="书"></a>书</h2><p>　　名侦探的牺牲 白井智之<br>　　小小人 杰西·安德鲁斯<br>　　霍乱时期的爱情 马尔克斯<br>　　月球城市 安迪·威尔<br>　　负零 广濑正</p><h1 id="夏"><a href="#夏" class="headerlink" title="夏"></a>夏</h1><h2 id="映画-2"><a href="#映画-2" class="headerlink" title="映画"></a>映画</h2><p>　　月球坠落<br>　　异型 夺命舰</p><h2 id="番剧-2"><a href="#番剧-2" class="headerlink" title="番剧"></a>番剧</h2><p>　　无神世界的神明活动<br>　　拉面赤猫<br>　　<del>你我战 S2</del><br>　　我推的孩子 S2<br>　　狼与香辛料<br>　　<del>鹿乃子乃子乃子虎视眈眈</del><br>　　小市民系列<br>　　地下城中的人<br>　　Stranger Thing S1<br>　　Loki S1<br>　　败犬女主太多啦<br>　　黑色止血钳 S2</p><h2 id="书-2"><a href="#书-2" class="headerlink" title="书"></a>书</h2><p>　　托斯卡纳艳阳下 弗朗西斯·梅斯<br>　　黑牢城 米泽穗信<br>　　量子窃贼 哈努·拉贾涅米</p><h1 id="秋"><a href="#秋" class="headerlink" title="秋"></a>秋</h1><h2 id="映画-3"><a href="#映画-3" class="headerlink" title="映画"></a>映画</h2><p>　　the substance<br>　　超能一家人</p><h2 id="番剧-3"><a href="#番剧-3" class="headerlink" title="番剧"></a>番剧</h2><p>　　Re0 第三季水门都市<br>　　<del>地。关于地球的运动</del><br>　　胆大党<br>　　悲喜渔生<br>　　魔法光能股份有限公司<br>　　擅长逃跑的殿下<br>　　香格里拉边境 S2<br>　　青之驱魔师 S4 雪之尽头篇<br>　　无彩限的怪灵世界<br>　　汉尼拔 S1</p><h2 id="书-3"><a href="#书-3" class="headerlink" title="书"></a>书</h2><p>　　底层的珍珠 赫拉巴尔<br>　　古都 川端康成<br>　　第欧根尼变奏曲 陈浩基<br>　　万物皆计算 Wolfram</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;冬&quot;&gt;&lt;a href=&quot;#冬&quot; class=&quot;headerlink&quot; title=&quot;冬&quot;&gt;&lt;/a&gt;冬&lt;/h1&gt;&lt;h2 id=&quot;映画&quot;&gt;&lt;a href=&quot;#映画&quot; class=&quot;headerlink&quot; title=&quot;映画&quot;&gt;&lt;/a&gt;映画&lt;/h2&gt;&lt;p&gt;　　名侦探柯南</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Talk on CMUDB Seminar 2024</title>
    <link href="https://waynexia.github.io/2024/12/database-building-block/"/>
    <id>https://waynexia.github.io/2024/12/database-building-block/</id>
    <published>2024-12-11T22:54:12.000Z</published>
    <updated>2026-01-14T13:19:51.707Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Implement-Integrate-amp-Extend-a-Query-Engine-in-GreptimeDB"><a href="#Implement-Integrate-amp-Extend-a-Query-Engine-in-GreptimeDB" class="headerlink" title="Implement, Integrate &amp; Extend a Query Engine in GreptimeDB"></a>Implement, Integrate &amp; Extend a Query Engine in GreptimeDB</h1><p>This is a talk on the CMUDB Seminar 2024.</p><p>Slides: <a target="_blank" rel="noopener" href="https://pub-424f6ec11df445209bab160eb5dcab38.r2.dev/GreptimeDB%20Query%20Engine.pdf">Download Link</a></p><p>YouTube Recording: <a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=VLAvZw0ZEwI&list=PLSE8ODhjZXjZc2AdXq_Lc1JS62R48UX2L&index=10">Here</a></p><p>Project Page: <a target="_blank" rel="noopener" href="https://db.cs.cmu.edu/seminar2024/">Project</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;Implement-Integrate-amp-Extend-a-Query-Engine-in-GreptimeDB&quot;&gt;&lt;a href=&quot;#Implement-Integrate-amp-Extend-a-Query-Engine-in-GreptimeDB&quot; </summary>
      
    
    
    
    
    <category term="Slides" scheme="https://waynexia.github.io/tags/Slides/"/>
    
  </entry>
  
  <entry>
    <title>Talk on DataFusion Meetup 2024</title>
    <link href="https://waynexia.github.io/2024/07/datafusion-meetup-2024/"/>
    <id>https://waynexia.github.io/2024/07/datafusion-meetup-2024/</id>
    <published>2024-07-19T22:54:12.000Z</published>
    <updated>2026-01-14T13:19:51.707Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Build-a-Distributed-Execution-Engine-in-GreptimeDB-with-Apache-DataFusion"><a href="#Build-a-Distributed-Execution-Engine-in-GreptimeDB-with-Apache-DataFusion" class="headerlink" title="Build a Distributed Execution Engine in GreptimeDB with Apache DataFusion"></a>Build a Distributed Execution Engine in GreptimeDB with Apache DataFusion</h1><p>This is a talk on the DataFusion Meetup China 2024.</p><p>Slides: <a target="_blank" rel="noopener" href="https://pub-424f6ec11df445209bab160eb5dcab38.r2.dev/Distributed%20Execution%20Engine.pdf">Download Link</a></p><p>Related Discussion: <a target="_blank" rel="noopener" href="https://github.com/apache/datafusion/discussions/10341#discussioncomment-10110273">GitHub Discussion</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;Build-a-Distributed-Execution-Engine-in-GreptimeDB-with-Apache-DataFusion&quot;&gt;&lt;a href=&quot;#Build-a-Distributed-Execution-Engine-in-Greptim</summary>
      
    
    
    
    
    <category term="Slides" scheme="https://waynexia.github.io/tags/Slides/"/>
    
  </entry>
  
  <entry>
    <title>How error occurs in GreptimeDB</title>
    <link href="https://waynexia.github.io/2024/01/rust-stack-error/"/>
    <id>https://waynexia.github.io/2024/01/rust-stack-error/</id>
    <published>2024-01-22T00:53:48.000Z</published>
    <updated>2026-01-14T13:19:51.709Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>TL;DR:</p><p>This post discusses the practice of Rust error handling topic in GreptimeDB. Including how to build a cheaper yet more accurate error stack to replace system backtrace, how to organize errors in large project and how to print errors in different schemes to log and end users. And shares possibly future work in the end.</p><p>An example of error in GreptimeDB looks like:</p><figure class="highlight plaintext"><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><code class="hljs plaintext">0: Foo error, at src/common/catalog/src/error.rs:80:10<br>1: Bar error, at src/common/function/src/error.rs:90:10<br>2: Root cause, invalid table name, at src/common/catalog/src/error.rs:100:10<br></code></pre></td></tr></table></figure></blockquote><h1 id="Introduce"><a href="#Introduce" class="headerlink" title="Introduce"></a>Introduce</h1><h2 id="What-Error-is-in-Rust"><a href="#What-Error-is-in-Rust" class="headerlink" title="What Error is in Rust"></a>What <code>Error</code> is in Rust</h2><!-- toc:- What `Error` is in Rust. --><p>In Rust, functions that might fail usually return a special enum <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/result/enum.Result.html#variant.Err"><code>Result&lt;T, E&gt;</code></a>, where <code>E</code> usually implements the trait <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/error/trait.Error.html"><code>std::error::Error</code></a>. This is the fundamental of error handling.</p><p>This blog shares our experience on how to organize variant types of <code>Error</code> in a relatively complex system like GreptimeDB, which is composed of multiple components with their own <code>Error</code> definition. From how an error is defined to how to present an error to log or end-user.</p><!-- todo: add a doc here? --><h2 id="Ecosystem-at-present"><a href="#Ecosystem-at-present" class="headerlink" title="Ecosystem at present"></a>Ecosystem at present</h2><!-- toc:- Two widely used crates about error: `thiserror` and `anyhow`.- How those two are used. Define customized error type.- Introduce `snafu`, and its characteristics. --><p>There are some <code>Error</code> structs in std that implement <code>std::error::Error</code>, like <code>std::io::Error</code> or <code>std::fmt::Error</code>. But we would usually define one for our project, as either we want to express our own error info or we may face multiple errors at the same time.</p><p>Given the <code>std::error::Error</code> trait is not complicated, it’s easy to implement manually for the customized error type. However you usually won’t want to do so. When error variants increase, it might be hard to work with flooding template code. Nowadays, there are some widely used utility crates to help play around with customized error types, like <a target="_blank" rel="noopener" href="https://docs.rs/thiserror/latest/thiserror/"><code>thiserror</code></a> and <a target="_blank" rel="noopener" href="https://docs.rs/anyhow/latest/anyhow/"><code>anyhow</code></a>, from the same author. Said that, use <code>thiserror</code> for library project and <code>anyhow</code> for binary project. This rule suits a major number of cases.</p><p>But for projects like GreptimeDB, where we divide the entire workspace into several individual sub-crates, we need to define an error type for each crate while keeping a streamlined combination. Neither <code>thiserror</code> nor <code>anyhow</code> can achieve this easily.</p><p>Here we choose another crate <a target="_blank" rel="noopener" href="https://docs.rs/snafu/latest/snafu/"><code>snafu</code></a> to instrument our error system. It’s like a combination of <code>thiserror</code> and <code>anyhow</code>. <code>thiserror</code> provides a convenient macro to define customized error type, with display, source and some context fields. And <code>anyhow</code> gives a <code>Context</code> trait that can easily transform from one underlying error into another with a new context.</p><p><code>thiserror</code> mainly implements the <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/convert/trait.From.html"><code>std::convert::From</code></a> trait for your error type, so that you can easily use <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/convert/trait.From.html"><code>?</code></a> to propagate the error you receive. Consequently, this also means you cannot define two error variants that come from the same source. Considering you are performing some I/O operations, you won’t know whether an error is generated in write or read. This is also an important reason we don’t use <code>thiserror</code> – the context is blurred in type.</p><h1 id="Stacking-Error"><a href="#Stacking-Error" class="headerlink" title="Stacking Error"></a>Stacking Error</h1><h2 id="What-we-want-from-error"><a href="#What-we-want-from-error" class="headerlink" title="What we want from error"></a>What we want from error</h2><!-- toc:- In the real world, knowing what the error is is not enough. --><p>In the real world, only knowing what an error is is far not enough. Imagine we are building a little protocol component in GreptimeDB (or any other project, just for example), It reads messages from the Internet, decodes, performs some operations and then sends it. We may encounter errors from several aspects:</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Error</span></span>&#123;<br>    ReadSocket(hyper::Error),<br>    DecodeMessage(serde_json::Error),<br>    Operation(GreptimeError),<br>    EncodeMessage(serde_json::Error),<br>    WriteSocket(hyper::Error),<br>&#125;<br></code></pre></td></tr></table></figure><p>When an error occurs, it might look like <code>DecodeMessage(serde_json: invalid character at 1)</code>. But this component might have 10 places that decode the message! How can we figure out in which step we see the invalid content?</p><p>So despite the error itself telling what has happened, if we want to have a clue on where this error occurs and if we should pay attention to it, we need the error to carry more information. For comparison, here is an example of an error log you might see from GreptimeDB.</p><figure class="highlight plaintext"><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><code class="hljs plaintext">Failed to handle protocol<br>0: Failed to handle incoming content, query: blabla, at src/protocol/handler.rs:89:22<br>1: Failed to reading next message at queue 5 of 10, at src/protocol/loop.rs:254:14<br>2: Failed to decode `01010001001010001` to ProtocolHeader, at src/protocol/codec.rs:90:14<br>3: serde_json(invalid character at position 1)<br></code></pre></td></tr></table></figure><p>A good error is not only about how it’s constructed, but more importantly is what will human see from it. We call it <strong>Stack Error</strong>. It should be intuitive and you must have seen a similar format elsewhere like backtrace.</p><p>From this log, it’s easy to know the entire thing with full context, from the user-facing behavior to the root cause. Plus the exact line and col number of where each error is propagated. And even gain a tiny report of this error: “in the query “blabla”, the fifth package’s header is corrupted”. It’s likely to be an invalid user input and we may not need to handle it from the server side.</p><p>This example shows the critical information that an error should contain (and contains in GreptimeDB):</p><ul><li>The root cause. Tells what’s happening.</li><li>The full context stack. For debugging or figuring out where this occurs.</li><li>What happens from the user’s perspective. Decide whether we need to expose this to user.</li></ul><p>The first root cause is often clear in many cases, like the <code>DecodeMessage</code> example before. Only if the library or function we used implements their error type correctly. But sometimes it isn’t quite helpful if we only have the root cause. Here is another <a target="_blank" rel="noopener" href="https://github.com/delta-incubator/delta-kernel-rs/pull/151">example</a> from DataBricks.</p><p>In the following sections, we will focus on the next two points and explain how it’s achieved. So hopefully you can achieve the same experience as in GreptimeDB.</p><h2 id="System-Backtrace"><a href="#System-Backtrace" class="headerlink" title="System Backtrace"></a>System Backtrace</h2><!-- toc:- System Backtrace comes to help- But backtrace has many problems --><p>So, now you have the root cause (<code>DecodeMessage(serde_json: invalid character at 1)</code>) but it’s not clear at which step this error occurs. Is decoding the header or body?</p><p>A natural thought is to capture the backtrace. In a causal where <code>.unwrap()</code> is the first choice, the backtrace will show up when error occurs (of course this is a bad practice). It will give you a complete call stack along with the line number. Then inspect the source code stack by stack, after skipping lots of unrelated system stacks, runtime stacks and std stacks, you finally get to the code you write.</p><p>Nowadays many libraries also provide the ability to capture backtrace on an <code>Error</code> is constructed. Regardless of whether the system backtrace can provide what we truly want, it’s very costly on either CPU (<a target="_blank" rel="noopener" href="https://github.com/GreptimeTeam/greptimedb/pull/1261">#1261</a>) and memory (<a target="_blank" rel="noopener" href="https://github.com/GreptimeTeam/greptimedb/pull/1273">#1273</a>). Capturing a backtrace will slow down your program as it needs to walk through the call stack and translate the pointer. Then to be able to translate the stack pointer we will need to include a large <code>debuginfo</code> in our binary. In GreptimeDB this means increasing the binary size by &gt; 700MB (4x size compared to 170MB without debuginfo). And there will be many noises in the captured system backtrace because the system can’t distinguish whether the code comes from the standard library, a third-party async runtime or our codebase.</p><p>There is another difference between the system backtrace and the proposed stack error. System backtrace tells us how to get to the position where the error occurs. While the stack error shows how the error is propagated. Sometimes there are different things. This difference comes from how the two things are implemented.</p><h2 id="Virtual-User-Stack"><a href="#Virtual-User-Stack" class="headerlink" title="Virtual User Stack"></a>Virtual User Stack</h2><!-- toc:- Propose the virtual user stack, how does it look like- The principle, and how it's added to errors --><p>Now let’s introduce the virtual user stack. The word “virtual” means the contrast of the system stack. Means it’s defined and constructed fully on user code. Look closer into the previous example:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">1: Failed to reading next message at queue 5 of 10, at src/protocol/loop.rs:254:14<br></code></pre></td></tr></table></figure><p>A stack layer is composed of 3 parts. The first number represents its position in the entire stack. Then follows the description of this layer, which is the <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/fmt/trait.Display.html"><code>std::fmt::Display</code></a> implementation of that error. The last is corresponding code location. Rust provides <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/macro.file.html"><code>file!</code></a>, <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/macro.line.html"><code>line!</code></a> and <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/macro.column.html"><code>column!</code></a> macros to help get that information.</p><p>In practice, we utilize <a target="_blank" rel="noopener" href="https://docs.rs/snafu/0.8.2/snafu/struct.Location.html"><code>snafu::Location</code></a> to gather the code location. So each location points to where the error is constructed. Through this chain we know how this error is generated and propagated to the uppermost.</p><p>So here is what it looks like all together:</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-meta">#[derive(Snafu)]</span><br><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Error</span></span> &#123;<br>    <span class="hljs-meta">#[snafu(display(<span class="hljs-meta-string">&quot;General catalog error: &quot;</span>))]</span> <span class="hljs-comment">// &lt;-- the `Display` impl deriv</span><br>    Catalog &#123;<br>        location: Location, <span class="hljs-comment">// &lt;-- the `location`</span><br>        source: catalog::error::Error, <span class="hljs-comment">// &lt;-- inner cause</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>Then we implemented a proc-macro <a target="_blank" rel="noopener" href="https://greptimedb.rs/common_macro/attr.stack_trace_debug.html"><code>stack_trace_debug</code></a> to scrape necessary information from <code>Error</code>‘s definition and generate the implementation of the related trait <a target="_blank" rel="noopener" href="https://greptimedb.rs/common_error/ext/trait.StackError.html"><code>StackError</code></a>, which provides useful methods to access and print the error:</p><figure class="highlight rust"><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></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">trait</span> <span class="hljs-title">StackError</span></span>: std::error::Error &#123;<br>    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">debug_fmt</span></span>(&amp;<span class="hljs-keyword">self</span>, layer: <span class="hljs-built_in">usize</span>, buf: &amp;<span class="hljs-keyword">mut</span> <span class="hljs-built_in">Vec</span>&lt;<span class="hljs-built_in">String</span>&gt;);<br><br>    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">next</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;&amp;<span class="hljs-keyword">dyn</span> StackError&gt;;<br><br>    <span class="hljs-comment">// Provided method</span><br>    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">last</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; &amp;<span class="hljs-keyword">dyn</span> StackError<br>    <span class="hljs-keyword">where</span> <span class="hljs-keyword">Self</span>: <span class="hljs-built_in">Sized</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>By the way, we have added <code>Location</code> and <code>display</code> to all errors in GreptimeDB. This is the hard work behind the methodology.</p><h2 id="Macro-Details"><a href="#Macro-Details" class="headerlink" title="Macro Details"></a>Macro Details</h2><p>Error is a singly linked list, like an onion from outer to inner. So we can capture an error at the outermost and walk through it.</p><p>One tricky thing we did here is about how to distinguish internal and external errors. Internal errors all implement the same trait <a target="_blank" rel="noopener" href="https://greptimedb.rs/common_error/ext/trait.ErrorExt.html"><code>ErrorExt</code></a> which can be used as a marker. But depending on this requires a <code>downcast</code> every time. We avoid this extra <code>downcast</code> call by simply giving a different name to them and detect in our macro. As shown below, we name all external errors <code>error</code> and all internal errors <code>source</code>. Then return <code>None</code> on implementing <a target="_blank" rel="noopener" href="https://greptimedb.rs/common_error/ext/trait.StackError.html#tymethod.next"><code>StackError::next</code></a> method if we find an <code>error</code>, or <code>Some(source)</code> if we read <code>source</code>.</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-meta">#[derive(Snafu)]</span><br><span class="hljs-meta">#[stack_trace_debug]</span><br><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Error</span></span> &#123;<br>    <span class="hljs-meta">#[snafu(display(<span class="hljs-meta-string">&quot;Failed to deserialize value&quot;</span>))]</span><br>    ValueDeserialize &#123;<br>        <span class="hljs-meta">#[snafu(source)]</span><br>        error: serde_json::error::Error, <span class="hljs-comment">// external source</span><br>        location: Location,<br>    &#125;,<br><br>    <span class="hljs-meta">#[snafu(display(<span class="hljs-meta-string">&quot;Table engine not found: &#123;&#125;&quot;</span>, engine_name))]</span><br>    TableEngineNotFound &#123;<br>        engine_name: <span class="hljs-built_in">String</span>,<br>        location: Location,<br>        source: table::error::Error, <span class="hljs-comment">// internal source</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>The method <a target="_blank" rel="noopener" href="https://greptimedb.rs/common_error/ext/trait.StackError.html#tymethod.debug_fmt"><code>StackError::debug_fmt</code></a> is used to render the error stack. It would be called recursively in the generated code. Each layer of error will write its own debug message to the mutable <code>buf</code>. The content will contain error description captured from <code>#[snafu(display)]</code> attribute, the variant arm type like <code>TableEngineNotFound</code> and the location from the enumeration.</p><p>Given we already defined our error types in that way, adopting stack error doesn’t require too much work, only adding the attribute macro <code>#[stack_trace_debug]</code> to every error type would be enough.</p><h2 id="Present-Error-to-End-User"><a href="#Present-Error-to-End-User" class="headerlink" title="Present Error to End User"></a>Present Error to End User</h2><p>So far we have done the most things. Here is the last piece about how to present the error to your user.</p><!-- toc:- How error shows to users- Distinguishing different errors, walk through the error chain --><p>Unlike the developer of your system, a user may not care about the line number and even the stack. But what information is helpful to end users?</p><p>This topic is very subjective. Our experience is that the leaf (or the innermost) error’s message might be useful as it is closer to what really goes wrong. The message can be further divided into two parts: internal and external, where the internal error is those defined in our codebase and the external is from dependencies, like <code>serde_json</code> from the previous example. And the root (or the outermost) error’s category is more accurate as it comes from where the error is thrown to the user. This can be achieved easily with previous <a target="_blank" rel="noopener" href="https://greptimedb.rs/common_error/ext/trait.StackError.html#tymethod.next"><code>StackError::next</code></a> and <a target="_blank" rel="noopener" href="https://greptimedb.rs/common_error/ext/trait.StackError.html#method.last"><code>StackError::last</code></a>. Or you can customize the format you want with those methods.</p><p>Combine them, here is the schema of our error that the user would see eventually:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">KIND - REASON ([EXTERNAL CAUSE])<br></code></pre></td></tr></table></figure><p>For example:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">Unexpected Message - Failed to decode `01010001001010001` to ProtocolHeader (serde_json(invalid character at position 1))<br></code></pre></td></tr></table></figure><!-- But our error here has carried many extra information than the standard library API defined: the source error, the location and the display. Moreover, we have to distinguish internal error (defined in GreptimeDB) and external error (defined elsewhere). --><h2 id="Cost"><a href="#Cost" class="headerlink" title="Cost?"></a>Cost?</h2><p>The virtual stack is sweety so far. And is cheaper and more accurate than the system backtrace. How much does it cost? </p><p>As for runtime overhead, it only requires some string format for the per-level reason and location.</p><p>And it’s even better on binary size. In GreptimeDB’s binary, the debug symbols occupied ~ 700MB, as a comparison the <code>strip</code>-ed binary size is around 170MB, with <code>.rodata</code> section size <code>016a2225</code> (~ 22.6M), the <code>.text</code> section occupies <code>06ad7511</code> (~ 106.8M).</p><p>Removing all <code>Location</code> reduces the <code>.rodata</code> size to <code>0169b225</code> and the overall binary size to 170MB. while removing all <code>#[snafu(display)]</code> reduces the <code>.rodata</code> size to <code>01690225</code> (~ 22.5M) and the overall binary size to 170MB. Hence this stack error mechanism’s overhead to binary size is very low (~ 100K).</p><h1 id="Conclusion-and-Future-Work"><a href="#Conclusion-and-Future-Work" class="headerlink" title="Conclusion and Future Work"></a>Conclusion and Future Work</h1><!-- toc:- In the last, the test about binary size.- We may consider making it more generic using the new std API --><p>In this post, we present how to implement a proc-macro <a target="_blank" rel="noopener" href="https://greptimedb.rs/common_macro/attr.stack_trace_debug.html"><code>stack_trace_debug</code></a> and use it to assemble a low-overhead and powerful stack error message. It also provides a convenient way to walk through the error chain, to help render the error in different schema for different purposes.</p><p>This macro is only adopted in GreptimeDB now, we are attempting to make it more generic for different projects. A wide adoption of this pattern can also make it even more powerful by bringing more third-party stacks and detailed reasons.</p><p>An unstable API <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/error/trait.Error.html#method.provide"><code>provide</code></a> in std <code>Error</code> allows getting a field in a struct. It’s an option we can consider for refactoring our stack-trace utils.</p><p><em>Discussion on hackernews: <a target="_blank" rel="noopener" href="https://news.ycombinator.com/item?id=42457515">https://news.ycombinator.com/item?id=42457515</a></em></p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;TL;DR:&lt;/p&gt;
&lt;p&gt;This post discusses the practice of Rust error handling topic in GreptimeDB. Including how to build a cheaper </summary>
      
    
    
    
    
    <category term="Rust" scheme="https://waynexia.github.io/tags/Rust/"/>
    
  </entry>
  
  <entry>
    <title>雪国</title>
    <link href="https://waynexia.github.io/2024/01/yukiguni/"/>
    <id>https://waynexia.github.io/2024/01/yukiguni/</id>
    <published>2024-01-10T21:25:18.000Z</published>
    <updated>2026-01-14T13:19:51.714Z</updated>
    
    <content type="html"><![CDATA[<p>2024 年元旦出游的一些照片，题图是结冰的三笠市<a target="_blank" rel="noopener" href="https://www.city.mikasa.hokkaido.jp/sightseeing/detail/00000039.html">桂沢湖</a>。</p><h1 id="千岁-Chitose"><a href="#千岁-Chitose" class="headerlink" title="千岁 Chitose"></a><ruby>千岁 <rt>Chitose</rt></ruby></h1><div class="heti heti--columns-2">    <figure class="figure-image">      <img src="horizon.avif" alt="山形上空" width="100%" height="100% loading="lazy" />      <figcaption>山形上空</figcaption>    </figure>      <figure class="figure-image">      <img src="solar-settlement.avif" alt="太阳能电站" width="100%" height="100% loading="lazy" />      <figcaption>太阳能电站</figcaption>    </figure>  </div><p>往北飞了大概只有一半的时候就能够看到雪覆盖的地面了。在本州山形的时候还只出现在山顶，到了北海道的上空就已经连道路都是一片白茫茫了。在飞机上还担心会不会因为积雪导致开车打滑，到了地面发现这个担心是完全不多余的。在北海道的路上体验到了大概是比之前加起来还多的 ABS 介入。<del>怀疑有些是运转士桑故意的</del></p><p>这次乘坐的航班飞得很高，到了三万九千英尺，大早上十点不到的也能看见月亮了。降落的时候飞过了几片太阳能发电站，这是其中一个。后面还在在北海道地区看到过很多次成片的太阳能版，大多数都和这个一样被雪覆盖了。</p><hr>    <figure class="figure-image">      <img src="chitose.avif" alt="新千岁空港" width="100%" height="100% loading="lazy" />      <figcaption>新千岁空港</figcaption>    </figure>  <p>我们乘坐的飞机没有靠桥，滑了半天之后被转运巴士接走，下机之后看到的滑行道上盖满了一层薄雪。也是这样才能够不用隔着玻璃就能拍到新千岁特色的弧形航站楼，楼上面并没有贴大字，那架撞毁的飞机就是从这里飞往东京的。</p><h1 id="札幌-Sapporo"><a href="#札幌-Sapporo" class="headerlink" title="札幌 Sapporo"></a><ruby>札幌 <rt>Sapporo</rt></ruby></h1>    <figure class="figure-image">      <img src="sapporo-park.avif" alt="路边停车场" width="100%" height="100% loading="lazy" />      <figcaption>路边停车场</figcaption>    </figure>  <p>札幌市周围的一个停车场，刚刚进入雪国，还非常激动。第一次下车的时候堆了一个<a target="_blank" rel="noopener" href="https://x.com/wayne17229928/status/1742026983419330974">雪人</a>，雪非常松软，随便滚几下就能搓出一个好大的雪球。失策的是这次带的手套并不防水，没有玩多久就开始有点湿了，没能堆出很理想的雪人。</p><p>在这里的雪其实还不算大，干道路面上也有人清理得差不多了。拍下这张图的时候还不知道这里的雪还算薄的了。</p><hr>    <figure class="figure-image">      <img src="moiwa.avif" alt="藻岩山" width="100%" height="100% loading="lazy" />      <figcaption>藻岩山</figcaption>    </figure>  <blockquote><p>藻岩山在阿伊努语中被称为“inkar-us-pe”（インカルシペ），意为登上山顶监视四周的地方，曾是阿伊努族的圣山。</p></blockquote><div class="heti heti--columns-2">    <figure class="figure-image">      <img src="moiwa-ticket.avif" alt="" width="50%" height="100% loading="lazy" />      <figcaption></figcaption>    </figure>  <p>这是到了札幌和北海道之后去的第一个（收门票）的景点，在札幌市的西南角。山其实只高五百多米，但是上山分了两段缆车。这张图片是第一段缆车的半路拍的。</p><p>图片右下角是靠近山脚的一大片墓园，以及在墓园里面的东本愿寺，从地图上粗估起来占地大概有七八万平方米。左边是平和塔，正好是我拿到的缆车票上面印的图案。</p><p>当天天气不是太好，正好在这种还不够高的地方能够看到比较远的札幌的景色。与其他城市比起来，札幌显得非常的规整，除了中间的北海道大学之外，基本所有的路都是沿着直线走的。在 20 世纪开拓府进行规划的时候参考了京都，所以都是非常相似的棋盘状布局，就连道路的命名也一样是按方向加数字来表示的。</p></div><!-- 图：在山上看到的一辆小车头 --><hr>    <figure class="figure-image">      <img src="moiwa-temple.avif" alt="山顶的一间小寺庙" width="100%" height="100% loading="lazy" />      <figcaption>山顶的一间小寺庙</figcaption>    </figure>  <p>山顶有一间观音寺，藻岩山其实一直到山顶都是有车道的，不过积雪非常厚，在山脚下完全没有意识到能开车上去。也没有看到其他的辙迹，估计也有可能雪太大不让上山吧。</p><hr>    <figure class="figure-image">      <img src="asahiyama-park.avif" alt="旭山纪念公园" width="100%" height="100% loading="lazy" />      <figcaption>旭山纪念公园</figcaption>    </figure>  <div class="heti heti--columns-2"><p>明明还在藻岩山上，离旭山有百来公里，不知道为什么以旭山命名。这是在北海道见到的第一个被大雪封闭的公园，后面几天还要看到数不清的放假的景点。</p><p>前面是一个下坡，最底下是公园里的喷泉广场。两个都被雪埋住了，看起来只是有梯度的雪坡。这里地势也比较高，能够看到札幌的天际线。</p>    <figure class="figure-image">      <img src="asahiyama-park-2.avif" alt="" width="70%" height="100% loading="lazy" />      <figcaption></figcaption>    </figure>  </div><h1 id="支笏湖-Shikotsuko"><a href="#支笏湖-Shikotsuko" class="headerlink" title="支笏湖 Shikotsuko"></a><ruby>支笏湖 <rt>Shikotsuko</rt></ruby></h1>    <figure class="figure-image">      <img src="shikotsuko.avif" alt="支笏湖" width="100%" height="100% loading="lazy" />      <figcaption>支笏湖</figcaption>    </figure>  <p>本来准备去的是另一个火山湖洞爷湖，因为太远了换成了顺路的支笏湖，这里也是日本最北的不冻火山湖。</p><p>与洞爷湖不同的是，支笏湖并不是一个非常规整的圆型。在支笏湖之后三四万年南方的风不死岳和北面的惠庭岳先后喷发，改变了支笏湖的地形。这张照片是站在惠庭岳的脚下向支笏湖方向拍的，远处的是风不死岳。它东面（图左边）有一个直的缓坡，像是用尺子画出来的一样。从地图上看到是苔之回廊，看起来也是一个很有趣的景点。</p><p>这天的天气非常好，能看到蓝天了。拍照的这个小平地周围也有些人路过停下来拍照，钓鱼和冬泳的也能见到，也许是个本地人常来的景点，风景确实很不错。</p><div class="heti heti--columns-2">    <figure class="figure-image">      <img src="tenboudai.avif" alt="" width="100%" height="100% loading="lazy" />      <figcaption></figcaption>    </figure>  <p>沿着湖走的另一边有一个小聚落，看着就几间屋子但甚至有一所小学。这次时间不凑巧，没有赶上每年的冰涛祭，温泉和休假村之类的地方也没有开门。去旁边野鸟之森的小瞭望台上看了看，能够同时看到风不死岳和惠庭岳，上面还有块牌子说远远能听到狼嚎。</p><p>去瞭望台的时候没有看到前面的人整理出来的路，直接从新雪上走了过去。这边的雪都比较厚，但是非常松软，走起来非常舒服。</p></div><h1 id="苫小牧-Tomakomai"><a href="#苫小牧-Tomakomai" class="headerlink" title="苫小牧 Tomakomai"></a><ruby>苫小牧 <rt>Tomakomai</rt></ruby></h1>    <figure class="figure-image">      <img src="tomakomai-bridge.avif" alt="苫小牧的天桥" width="100%" height="100% loading="lazy" />      <figcaption>苫小牧的天桥</figcaption>    </figure>  <p>苫小牧是北海道西南面的城市，形状狭长非常有特色，像是把智利横过来一样。是一个工业发达的城市，特别是印刷业。也是《<a target="_blank" rel="noopener" href="https://bgm.tv/subject/137722">只有我不存在的城市</a>》里的舞台。不过无论是科学馆，小学还是绿之丘都没有开放。从封山告示牌上看，绿之丘一年中有半年的时间都是封闭的。</p><p>在从绿之丘掉头的时候拍到了这个天桥，也算是巡到礼了。</p><hr>    <figure class="figure-image">      <img src="tomakomai-seaside.avif" alt="苫小牧的海边" width="100%" height="100% loading="lazy" />      <figcaption>苫小牧的海边</figcaption>    </figure>  <p>到了苫小牧这边天上的云又多了起来了，天色有些暗。苫小牧的海边连着太平洋，从这个角度作射线也许能一直到新西兰。</p><p>这里的雪的边界应该是被涨潮上来的海水冲出来的。海边有一块用消波块围起来的地方，告示牌上说这是一个“故乡的海”的维护工程，这里在地图上也被标注为“故乡海岸”。</p><hr>    <figure class="figure-image">      <img src="tetrapod.avif" alt="两种消波块" width="100%" height="100% loading="lazy" />      <figcaption>两种消波块</figcaption>    </figure>  <p>在这里还看到了另一种不常见的消波块，像是正四面体的框架。之前见到的基本都是远处那种四根圆柱连在一起的形状，正四面体的还是第一次看见，在这个海边也只有这一角有用。从腐蚀程度上看应该用了有一段时间了，不知道是不是有什么特殊的用处。</p><h1 id="登别-Noboribetsu"><a href="#登别-Noboribetsu" class="headerlink" title="登别 Noboribetsu"></a><ruby>登别 <rt>Noboribetsu</rt></ruby></h1><div class="heti heti--columns-2">    <figure class="figure-image">      <img src="jigokudani-1.avif" alt="地狱谷 (a)" width="90%" height="100% loading="lazy" />      <figcaption>地狱谷 (a)</figcaption>    </figure>      <figure class="figure-image">      <img src="jigokudani-2.avif" alt="地狱谷 (b)" width="100%" height="100% loading="lazy" />      <figcaption>地狱谷 (b)</figcaption>    </figure>  </div><blockquote><p>登别名称源自阿伊努语的“nupur-pet”（深色的河）或“nupki-pet”（混浊的河）</p></blockquote><p>登别地狱谷。这里的地形和环境让我想到海拉鲁的鼓隆城：裸露的山脉，热气腾腾的地表。在冬天有积雪覆盖的时候，地表温度的差异一目了然，旁边不远就是活火山“俱多乐“。不过可惜的是这里随着水汽一起冒出来的还有很多含硫气体，越是富含水汽的地方气味越是难以接受，到了游客步道的尽头更是无法呼吸，现场看到了一组人在轮流互相拍照，只觉得忍耐力是望尘莫及。地狱谷名字的由来也有这浓郁的硫元素的不小贡献，这么想来鼓隆城的居住环境其实比想象中的要更加恶劣？</p><p>附近有非常多的温泉酒店，就地取材地狱谷的温泉，应该是有专门的工序来处理这让人生厌的气味吧，除掉这些这里的水是矿物质含量很丰富的天然热水，做温泉应该是相当合适的。</p><p>周围最近一次的火山活动被确认在两百年前，2015年的时候气象厅对俱多乐火山发出了<a target="_blank" rel="noopener" href="https://www.jma.go.jp/jma/press/1509/18d/150918kuttara.pdf">喷火警戒</a>，感觉好像还是有点危险的样子。</p><hr>    <figure class="figure-image">      <img src="jigokudani-3.avif" alt="地狱谷周围" width="100%" height="100% loading="lazy" />      <figcaption>地狱谷周围</figcaption>    </figure>  <p>在裂隙周围植被其实还是不少的，积雪看起来也只比其他地方略薄一些。在步道上好几次差点滑倒，去往大汤沼的路甚至被雪封住了上不去。也许是因为丰富的高温水汽，这里的气候也与别处有所不同。这个亭子周围能见到被雪盖住的草地，掉光了叶子的树和叶子只是稍稍变黄的灌木。</p><h1 id="薄野-Susukino"><a href="#薄野-Susukino" class="headerlink" title="薄野 Susukino"></a><ruby>薄野 <rt>Susukino</rt></ruby></h1>    <figure class="figure-image">      <img src="susukino-street.avif" alt="薄野的一个路口" width="70%" height="100% loading="lazy" />      <figcaption>薄野的一个路口</figcaption>    </figure>  <p>这里其实还在札幌，是札幌最繁华的一个区域：</p><blockquote><p>薄野是日本北海道札幌市中央区著名的红灯区，与东京新宿的歌舞伎町及九州福冈的中洲被列为“日本三大红灯区”（日本3大歓楽街），亚洲一些旅行团亦多称薄野为“薄野不夜天”。</p></blockquote><p>虽然是这么说，但不止一次地产生“这里真的是繁华地带吗？”的疑问。除了两三条有大商场或者灯饰的街之外，好像与普通地区的夜晚也没有什么区别。当然与薄野周边比的话人确实是多一点，可能是北海道本身的人比较少的缘故吧。啊，转了几圈下来糟糕的广告牌倒是不少，红灯区不假。</p><hr>    <figure class="figure-image">      <img src="susukino-border.avif" alt="薄野边界" width="70%" height="100% loading="lazy" />      <figcaption>薄野边界</figcaption>    </figure>  <p>薄野中心的十字路口不会拍，不过散步到薄野边界的时候看到了很有趣的景象。“狸小路”是薄野一条有名的商店街，但是是只有外国游客的一块地方，就是那种经典的专门划一块地方给游客买东西或拍照等等的步行街。</p><p>根据薄野观光协会划定的范围，薄野东西范围是西二丁目至西六丁目。而狸小路是一丁目到七丁目，最东和最西的一段超出了薄野的范围。从六丁目走出来的时候发现两边的差异实在是太明显了：薄野部分还有闪亮的拱形招牌，而对面就非常潦草地挂一个牌子；六丁目里挂着白色的灯管，七丁目就只剩普通的路灯，也不见有开门的店家了。</p><h1 id="小樽-Otaru"><a href="#小樽-Otaru" class="headerlink" title="小樽 Otaru"></a><ruby>小樽 <rt>Otaru</rt></ruby></h1><!-- 图：朝里站的海边----------------------------------------------------------------------- -->    <figure class="figure-image">      <img src="otaru-unga.avif" alt="运河" width="100%" height="100% loading="lazy" />      <figcaption>运河</figcaption>    </figure>  <blockquote><p>小樽的地名源自于阿伊努语的“ota-ru-nay”（侵蚀沙滩的河川/沙滩上有痕迹的河川）</p></blockquote><p>这次没去到小樽名字里的这条河，只沿着与海岸线平行的运河走了走。这里早先是港口城市，见到俄文的频率也比之前高了不少。运河边上建了许多仓库，不过只见到短途小游船在运河里面开。</p><p>运河边上旅游开发程度很高，有非常多的适合拍照的门面，和摆满了玻璃工艺品的工房。现在小樽是一个旅游城市，招牌商业是玻璃工艺和寿司海产。在运河边上走着好像只是在逛一个主题商场，让人觉得似是商业化得太多而有些丢掉了原本的味道了。</p><hr><p>图：小樽的玻璃</p><p>对于玻璃的描述，《雪国》里将玻璃形容成一面通往虚幻世界的镜子，从表面反射的光线和透过玻璃折射进来的实象叠在一起，让人分不清眼中所见之物究竟为何。而在小樽见到的玻璃工艺品，大多以有色的为主，偏向用玻璃来模仿其他的小物件，让玻璃本身来构成装饰。</p><p>还有一种看见比较多的形式，是将玻璃吹成一个空心的球。最开始是实用的捕捞工具，能够浮在水面上。现在在每年的雪灯祭的时候也会在运河上铺上很多这种玻璃浮灯，当然这回也没有赶上。</p><hr>    <figure class="figure-image">      <img src="asari.avif" alt="朝里站" width="100%" height="100% loading="lazy" />      <figcaption>朝里站</figcaption>    </figure>  <p>这里本来应该只是函馆本线上的一个普通小站，wiki 上说 2012 年的时候日均上客量只有不到三百人，并且是一个无人车站。也许是最近才开始因为周围不错的景色变为火热的打卡地。到这里的时候感觉全是人，说的话也完全能听懂，就像是回到了池袋一样。还见到了几组拍婚纱照的外国游客，和做安全劝阻的的工作人员。我们没有进站，在外面不能同时把铁路和海边拍到一起。</p><hr>    <figure class="figure-image">      <img src="yuobin.avif" alt="朝里站的邮筒" width="100%" height="100% loading="lazy" />      <figcaption>朝里站的邮筒</figcaption>    </figure>  <p>在铁道旁边也有一排房子，还有电话亭和邮筒📮。在还寄信的年代用过几次邮筒，出门散步能遇见好几个，后来到现在连邮局旁边也见不着邮筒了。但是这里的邮筒看上去还很新的样子，甚至没有积雪。</p><p>因为来朝里的人非常多，旁边的这些住户应该也是不胜其烦。有的还用警戒线把院子围了起来，再用多语言写了警示牌。离“景点”太近也是一件很麻烦的事情。而且对于本地人来说这里不过是普通的海边车站罢了。</p><hr>    <figure class="figure-image">      <img src="otaru-downtown.avif" alt="本地人的城区" width="100%" height="100% loading="lazy" />      <figcaption>本地人的城区</figcaption>    </figure>  <p>从运河区走出来的时候，拿了一份游览地图，非常复古地根据纸质地图转了起来。小樽大部分游客都集中在运河周围的商业区中，来到地势稍高一点的普通城区就是另一番景象了，人一下子少了很多，看起来也都是本地人。这一天应该已经是工作日了，周围的商店看起来还有很多没开门，看来做本地人生意并不需要这么早返工。</p><p>中间还路过了一条商业街，和薄野的狸小路也完全是两番景象，老旧发黄还稍微有点漏水的顶棚，和看起来完全是店家个人兴趣的商店。周围还有各种只能看见告示牌的公园，那份地图看起来是给夏季游览设计的。</p><hr>    <figure class="figure-image">      <img src="otaru-lighthouse.avif" alt="灯塔" width="70%" height="100% loading="lazy" />      <figcaption>灯塔</figcaption>    </figure>  <p>祝津的展望台和灯塔，远处的陆地应该是札幌。</p><p>这里在小樽市区再往西一些的位置，是一个视角非常不错的海岬，由于方位不合适没能看到日落。</p><hr>    <figure class="figure-image">      <img src="abandoned-rail.avif" alt="废弃的铁道" width="100%" height="100% loading="lazy" />      <figcaption>废弃的铁道</figcaption>    </figure>  <p>City walk 的时候偶然看到雪面上有两条印子，走进了才发现是一段废弃的铁路。周围的踏切设施也都拆掉了，不仔细看可能都注意不到这里曾经是开火车的地方。</p><p>回来之后同伙查到这个是<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/zh-cn/%E6%89%8B%E5%AE%AE%E7%B7%9A">手宫线</a>，早在1880年（光绪六年）就开通了，连接小樽和札幌，是北海道的第一条铁路。到1972年因货运量减少停运废线至今。</p><hr>    <figure class="figure-image">      <img src="orion.avif" alt="猎户座" width="100%" height="100% loading="lazy" />      <figcaption>猎户座</figcaption>    </figure>  <p><a target="_blank" rel="noopener" href="https://zh.wikipedia.org/zh-cn/%E7%9F%B3%E7%8B%A9%E7%81%A3">石狩湾</a>冬天的星空，中间的星座是<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/zh-hans/%E7%8D%B5%E6%88%B6%E5%BA%A7">猎户座</a>，看得非常清楚。偏上一点最亮的那颗是木星。</p><p>顺带一提当时的时间是下午六点差一分。本来想去石狩湾看看石狩川入海口，路到了一半出现了一面车过不去的雪墙，下车休息的时候偶然看到的天空。即使不远处就有主城区（图中右边）和一些光污染，这也是最近看的最清楚的一次夜空了。</p><h1 id="旭川-Asahikawa"><a href="#旭川-Asahikawa" class="headerlink" title="旭川 Asahikawa"></a><ruby>旭川 <rt>Asahikawa</rt></ruby></h1><div class="heti heti--columns-2"><p>在旭川住的酒店顶楼是一个露天大浴场，第一次亲自泡。里面的水温非常热，从下水之后心率就开始一路飙升，到后面出来的时候已经有些晕晕乎乎的了。</p><p>池子虽然分了几个区域有，水温的区别，但我觉得是好烫超烫和非常烫的档位。最后已经没有信心走进桑拿房了，感觉很有可能会被抬出来，睁眼看见陌生的天花板。</p>    <figure class="figure-image">      <img src="daiyokujou.avif" alt="露天大浴场" width="100%" height="100% loading="lazy" />      <figcaption>露天大浴场</figcaption>    </figure>  </div><hr>    <figure class="figure-image">      <img src="asahikawa-penguin-2.avif" alt="" width="100%" height="100% loading="lazy" />      <figcaption></figcaption>    </figure>      <figure class="figure-image">      <img src="asahikawa-penguin.avif" alt="旭川动物园的企鹅" width="100%" height="100% loading="lazy" />      <figcaption>旭川动物园的企鹅</figcaption>    </figure>  <p>旭川动物园的第一个节目，冬季限定的企鹅散步。能够从非常近的距离看到企鹅列队走过。</p><p>下面是企鹅的馆，营业结束的企鹅就在这里休息，有几只的动作看起来好像有些奇怪？脖子是不是过于灵活了。仔细看了下它们的手（翅膀？）和身体连接的地方，好像只有部分套了彩色的环。也许这里的饲养员有特殊的技巧，能够不借助额外的标记物就能认出来吧。</p><hr>    <figure class="figure-image">      <img src="asahikawa-lamb.avif" alt="旭川动物园的小羔羊" width="100%" height="100% loading="lazy" />      <figcaption>旭川动物园的小羔羊</figcaption>    </figure>  <p>像这种比较常见的小动物都没有太多的围栏，能够怼脸拍。回来翻照片的时候才发现原来羊的瞳孔是长方形的。完全不知道它在看哪里。</p><hr>    <figure class="figure-image">      <img src="asahikawa-bear.avif" alt="旭川动物园的熊" width="70%" height="100% loading="lazy" />      <figcaption>旭川动物园的熊</figcaption>    </figure>  <p>年纪相仿的棕熊，很小的时候就在这个动物园了。这里很多的动物的馆都有露天的部分，不过愿意在外面活动的非常少。当时好像正好是表演还是进食时间，饲养员一边给大家讲解一边用食物逗着熊走来走去。</p><hr>    <figure class="figure-image">      <img src="asahikawa-street.avif" alt="旭川城市一角" width="100%" height="100% loading="lazy" />      <figcaption>旭川城市一角</figcaption>    </figure>  <p>从高处拍到的一个路口。在旭川待的时间不长，没有转很多的地方。这里的纬度大概和吉林长春差不多，但是景色应该差别很大。</p><hr>    <figure class="figure-image">      <img src="nhk-tower.avif" alt="旭川 NHK 电波塔" width="70%" height="100% loading="lazy" />      <figcaption>旭川 NHK 电波塔</figcaption>    </figure>  <h1 id="北海道-Hokkaido"><a href="#北海道-Hokkaido" class="headerlink" title="北海道 Hokkaidō"></a><ruby>北海道 <rt>Hokkaidō</rt></ruby></h1><p>其他路上拍到的一些地方。</p>    <figure class="figure-image">      <img src="trees.avif" alt="富良野" width="100%" height="100% loading="lazy" />      <figcaption>富良野</figcaption>    </figure>  <p>富良野附近有四个叫富良野的地方，除了这里之外还有上富良野町，中富良野町和南富良野町。从明治时代开始这里的行政区划就老是变化，现在划分的几个地方互相之间也没有直接连接，看起来是一个小城市群。</p><p>另外这个名字也很有意思：</p><blockquote><p>该市的名称源自阿伊努语的“hura-nu-i”（发臭的地方）或“hura-nuy”（发臭的火焰），一般认为指的是十胜岳的硫磺喷气。</p></blockquote><hr>    <figure class="figure-image">      <img src="shin-yuubari.avif" alt="新夕张站" width="100%" height="100% loading="lazy" />      <figcaption>新夕张站</figcaption>    </figure>  <blockquote><p>市名源于阿伊努语的“yu-paro”（矿泉涌出的地方）</p></blockquote><p>夕张是上个世纪的一个资源城市，六十年前人口还有十一万，现在已经只剩六千余了，是日本人口第二少的城市。因为周围煤炭资源逐渐减少，城市也慢慢萎缩下来。老夕张站已经废止了，这里的新夕张站看上去也没有很好。车站楼外壳也全部生了铁锈，连 JR 的标志都没有。洗手间还在运营，但工作人员三点钟就下班了（羡慕！）。有点好奇在这里上班的人一天的行程。</p><hr><div class="heti heti--columns-2">    <figure class="figure-image">      <img src="asahikawa-yuki.avif" alt="路边的雪" width="100%" height="100% loading="lazy" />      <figcaption>路边的雪</figcaption>    </figure>      <figure class="figure-image">      <img src="ooyuki.avif" alt="三段滝公园" width="100%" height="100% loading="lazy" />      <figcaption>三段滝公园</figcaption>    </figure>  </div><p>两张比较超出认知的雪。左边是早上出门时拍到的旭川路边堆到了两人高的雪堆；另一个是路上看到的一个公园的化妆室。非常好理解为什么北海道是人口密度最少的区域了，如果要在这种情况下定居着实得好好考虑下。而且对于公共设施来说这么多的雪维护费用也不小，所以有很多地方索性直接关门还来的比较好。</p><hr>    <figure class="figure-image">      <img src="tokio.avif" alt="江户川" width="70%" height="100% loading="lazy" />      <figcaption>江户川</figcaption>    </figure>  <p>因为是从北海道起飞的，所以也算北海道。</p><p>拍得比较模糊。这是夜晚的江户川，右边能看到天空树。这次想在天上看到两个东西其一就是东京夜景，另一个富士山因为航路问题没怎么看明白。</p><h1 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h1><p>手机在新千岁机场落地的时候就开始出问题了，开始是磁场传感器失灵，然后每天坏一两个元件，最后一天甚至没信号了。庆幸的是摄像头和屏幕一直好到最后，还能当个门锁用。回来一看发现按了几千次快门，花了一周的业余时间才基本整理完。大部分图都非常糊，所以还是不要放大看了（</p><p>另外，这次的标题从《<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/zh-cn/%E9%9B%AA%E5%9B%BD">雪国</a>》借来，但是内容和地点都与小说无关。川端康成笔下的雪国在元旦经历了能登地震，希望能够早日恢复。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;2024 年元旦出游的一些照片，题图是结冰的三笠市&lt;a target=&quot;_blank&quot; rel=&quot;noopener&quot; href=&quot;https://www.city.mikasa.hokkaido.jp/sightseeing/detail/00000039.html&quot;&gt;桂沢</summary>
      
    
    
    
    
    <category term="Experience" scheme="https://waynexia.github.io/tags/Experience/"/>
    
  </entry>
  
  <entry>
    <title>2023 的记录</title>
    <link href="https://waynexia.github.io/2023/12/2023-summary/"/>
    <id>https://waynexia.github.io/2023/12/2023-summary/</id>
    <published>2023-12-06T09:54:12.000Z</published>
    <updated>2026-01-14T13:19:51.701Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><em>What I am composed of</em></p></blockquote><h1 id="冬"><a href="#冬" class="headerlink" title="冬"></a>冬</h1><p>　　　　<em>数据丢失</em> 一开始没有记</p><p>　　　　其主之声 莱姆<br>　　　　魔眼之匣迷案 今村昌弘　　</p><h1 id="春"><a href="#春" class="headerlink" title="春"></a>春</h1><h2 id="番剧"><a href="#番剧" class="headerlink" title="番剧"></a>番剧</h2><p>　　　　放学后失眠的你<br>　　　　地狱乐<br>　　　　天国大魔境<br>　　　　总之就是非常可爱 第二季<br>　　　　我推的孩子<br>　　　　为美好的世界献上爆炎<br>　　　　三月的狮子 第一季 第二季<br>　　　　我心中糟糕的念头</p><h2 id="映画"><a href="#映画" class="headerlink" title="映画"></a>映画</h2><p>　　　　辉夜大小姐想让我告白 初吻不会结束<br>　　　　铃芽之旅<br>　　　　平家物语 犬王<br>　　　　超级玛丽欧兄弟大电影<br>　　　　名侦探柯南 万圣节的新娘<br>　　　　哆啦A梦 大雄与天空的理想乡<br>　　　　消失的她</p><h2 id="书"><a href="#书" class="headerlink" title="书"></a>书</h2><p>　　　　呼吸 特德姜<br>　　　　透明人潜入密室 阿津川辰海<br>　　　　夜晚的潜水艇 陈春成<br>　　　　凶人馆迷案 今村昌弘<br>　　　　索拉里斯星 莱姆</p><h1 id="夏"><a href="#夏" class="headerlink" title="夏"></a>夏</h1><h2 id="番剧-1"><a href="#番剧-1" class="headerlink" title="番剧"></a>番剧</h2><p>　　　　fate strange fake OVA<br>　　　　不死少女<br>　　　　无职转生 第二季<br>　　　　莱莎的炼金工房<br>　　　　能干的猫今天也忧郁<br>　　　　偶像大师 U149<br>　　　　夏日重现<br>　　　　slow loop<br>　　　　k-on!<br>　　　　do it yourself!!</p><h2 id="映画-1"><a href="#映画-1" class="headerlink" title="映画"></a>映画</h2><p>　　　　碟中谍7 上<br>　　　　春宵苦短，少女前进吧<br>　　　　芭比<br>　　　　奥本海默</p><h2 id="书-1"><a href="#书-1" class="headerlink" title="书"></a>书</h2><p>　　　　将饮茶 杨绛<br>　　　　树上的男爵 卡尔维诺<br>　　　　喜鹊谋杀案 安东尼 霍洛沃兹<br>　　　　莫失莫忘 石黑一雄<br>　　　　小岛经济学 希夫　　　　</p><h1 id="秋"><a href="#秋" class="headerlink" title="秋"></a>秋</h1><h2 id="番剧-2"><a href="#番剧-2" class="headerlink" title="番剧"></a>番剧</h2><p>　　　　葬送的芙莉莲<br>　　　　福星小子<br>　　　　香格里拉 边境<br>　　　　不死不幸<br>　　　　间谍过家家 season 2<br>　　　　星灵感应<br>　　　　家里蹲吸血鬼的苦闷<br>　　　　药屋少女的呢喃<br>　　　　MyGO<br>　　　　史莱姆OVA　　　　</p><h2 id="映画-2"><a href="#映画-2" class="headerlink" title="映画"></a>映画</h2><p>　　　　k-on<br>　　　　布达佩斯大饭店<br>　　　　坠楼死亡的剖析　　　　</p><h2 id="书-2"><a href="#书-2" class="headerlink" title="书"></a>书</h2><p>　　　　玻璃之塔杀人事件 知念实希人<br>　　　　徳米安 黑塞<br>　　　　雪国 川端康成<br>　　　　献给阿尔吉侬的花束 丹尼尔 凯斯<br>　　　　象首 白井智之<br>　　　　城冢翡翠倒叙集  向沢沙呼</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What I am composed of&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&quot;冬&quot;&gt;&lt;a href=&quot;#冬&quot; class=&quot;headerlink&quot; title=&quot;冬&quot;&gt;&lt;/a&gt;冬&lt;/h1&gt;&lt;p&gt;　　　　&lt;em&gt;数</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Querying Prometheus</title>
    <link href="https://waynexia.github.io/2023/11/querying-prometheus/"/>
    <id>https://waynexia.github.io/2023/11/querying-prometheus/</id>
    <published>2023-11-23T16:13:33.000Z</published>
    <updated>2026-01-14T13:19:51.708Z</updated>
    
    <content type="html"><![CDATA[<p>Explain PromQL in simple SQL that you are familiar with.</p><blockquote><p>This blog is work-in-progress. But you can still read and discuss it with me.</p></blockquote><p>Planned content:</p><ul><li>select data</li><li>operations</li><li>format in storage</li><li>joins, group and set operation</li><li>get distributed</li><li>extension on types and operations</li><li>counter, histogram and summary</li></ul><h1 id="Select-data"><a href="#Select-data" class="headerlink" title="Select data"></a>Select data</h1><p>Prometheus collects data in an unreliable way. The data process pipeline has considered and brings lots of “special logic” to make those unreliable data look reasonable and intuitive. But as the price, that logic might not be straightforward as a traditional SQL-based database. This section includes lookback,  null-handling, offset, interval.</p><p>Notice that those mechanisms always take effort together. To keep the explanation dry, other cases are ignored when explaining one (E.g., “offset” section assumes there is no “lookback”).</p><h2 id="offset"><a href="#offset" class="headerlink" title="offset"></a>offset</h2><p>This is the simplest one. Offset is used to “bias” the data’s timestamp – every data point in Prometheus has a related timestamp, as well as the query itself. In SQL, the table (Prometheus names it “metric”) looks just like this:</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><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> metric (<br>ts, <span class="hljs-type">timestamp</span>(<span class="hljs-number">3</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">NULL</span>,<br>    v, <span class="hljs-keyword">double</span>,<br>    tag_1 string,<br>    tag_2 string,<br>    ...<br>    <span class="hljs-keyword">PRIMARY</span> KEY (tag_1, tag_2, ...),<br>);<br></code></pre></td></tr></table></figure><p>The <code>offset</code> provides a way to <strong>push backward</strong> (toward an earlier time) the time range of querying:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">SELECT</span> ... <span class="hljs-keyword">FROM</span> metric <span class="hljs-keyword">WHERE</span> ts <span class="hljs-operator">&gt;=</span> (<span class="hljs-keyword">START</span> <span class="hljs-operator">-</span> <span class="hljs-keyword">OFFSET</span>) <span class="hljs-keyword">and</span> ts <span class="hljs-operator">&lt;</span> (<span class="hljs-keyword">END</span> <span class="hljs-operator">-</span> <span class="hljs-keyword">OFFSET</span>);<br></code></pre></td></tr></table></figure><p>For example, if we query data between <code>2023-11-14T18:00:00Z</code> and <code>2023-11-14T22:00:00Z</code>, and with “1h” offset, the equal SQL would be something like below:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">SELECT</span> ... <span class="hljs-keyword">FROM</span> metric <span class="hljs-keyword">WHERE</span> ts <span class="hljs-operator">&gt;=</span> <span class="hljs-string">&#x27;2023-11-14T17:00:00Z&#x27;</span> <span class="hljs-keyword">AND</span> ts <span class="hljs-operator">&lt;</span> <span class="hljs-string">&#x27;2023-11-14T21:00:00Z&#x27;</span><br></code></pre></td></tr></table></figure><p>Another point is the offset can be negative. In this case, you are just “minus a negative”</p><h2 id="interval"><a href="#interval" class="headerlink" title="interval"></a>interval</h2><p>You might be wondering why the results from Prometheus step are aligned to the given resolution but not the data collection interval. Here is an example result part I get with <code>7s</code> resolution:</p><figure class="highlight plaintext"><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><code class="hljs plaintext">[   ...,<br>    [1700635385,&quot;1&quot;],<br>    [1700635392,&quot;1&quot;],<br>    [1700635399,&quot;1&quot;],<br>    [1700635406,&quot;1&quot;],<br>    [1700635413,&quot;1&quot;],<br>    ...<br>]<br></code></pre></td></tr></table></figure><p>That’s because Prometheus doesn’t generate timestamp directly from the data’s timestamp, but in a reversed way, where the timestamp in the result is determined first, then finds values suited for that timestamp and feeds them into calculate operators. The <code>interval</code> is one of the key parameters for determining the timestamps in result. It works very simply – if we use a <code>for</code> loop to simulate this behavior, it would be</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">for</span> (ts = start; ts &lt; end; ts += interval) &#123;&#125;<br></code></pre></td></tr></table></figure><h2 id="lookback"><a href="#lookback" class="headerlink" title="lookback"></a>lookback</h2><p>Also, unlike SQL where filter filters the “exact” what you require, PromQL will take its liberty to find data out the range of your query and present them to you. This behavior is called “lookback”, looking at a larger range and fetching data for calculation.<br>For example, if the current timestamp we are calculating is <code>1700000000</code> (<code>2023-11-14T22:13:20Z</code>), and lookback delta is ‘5m’ (300s). The filter is not <code>WHERE ts = 1700000000</code>, but this:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">SELECT</span> ... <span class="hljs-keyword">FROM</span> metric <span class="hljs-keyword">WHERE</span> ts <span class="hljs-operator">&gt;=</span> <span class="hljs-number">1699999700</span> <span class="hljs-keyword">AND</span> ts <span class="hljs-operator">&lt;=</span> <span class="hljs-number">1700000000</span>;<br></code></pre></td></tr></table></figure><p>That is, Prometheus will take up to 5 minutes of data into consideration when calculating one data point.</p><h2 id="null-handling"><a href="#null-handling" class="headerlink" title="null handling"></a>null handling</h2><p>TODO</p><h2 id="don’t-repeat"><a href="#don’t-repeat" class="headerlink" title="don’t repeat"></a>don’t repeat</h2><p>Though Prometheus will try its best to find data, but it won’t grab a repeated dataset for calculation, where all the points are the same, i.e., it won’t repeat on one point to fill absent timestamps.</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Explain PromQL in simple SQL that you are familiar with.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This blog is work-in-progress. But you can still read and di</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>原子之心</title>
    <link href="https://waynexia.github.io/2023/08/atomic-heart/"/>
    <id>https://waynexia.github.io/2023/08/atomic-heart/</id>
    <published>2023-08-30T22:54:53.000Z</published>
    <updated>2026-01-14T13:19:51.705Z</updated>
    
    <content type="html"><![CDATA[<p>从概念图和 PV 开始就溢出屏幕的复古科幻的味道让人非常心动，而且首发 XGP 完全没有门槛。</p><p>对我来说，科幻的背景设定到五十年前和五十年后完全没有太大的区别，毕竟都算是“遥不可及”的时代了。而且虽然俄式游戏接触得不多，但是古老的科幻故事听了不少，这个带入起来是非常熟练了。</p><p>过去式科幻的醍醐味就是又老又新，有些是时代本身的限制，有些是故意做旧的效果。原子之心这次的做旧我觉得体验起来很满意，比波士顿的狗还灵活的机器人，脸上是面具一样的表情；空天浮岛上建的是老式步道和小红瓦房；激光感应的锁锁住的电梯里挂着的是转盘电话等等。此外，美术设置非常优秀——这也是最突出的亮点。而且不只是美术本身，作为大背景设定的两极争霸和红色风格也让人觉得非常复古。加上一开始就能见到的一堆奇观、超大雕像、超大人像海报和没有感情的复读机人等等，再一连想到游戏刚发售的时候的那段小学生景区介绍，只能说太艺术了。</p><p><img src="./statue.jpeg" alt="statue"></p><hr><p>虽然美术音乐设定等等都和上乘，但是作为游戏本身的 game play 部分还是让人有点头疼（字面）。虽然是在一个很不错的场景里，但是没过多久就丧失了探索的欲望。在路上满是怪物，如果不是也马上就是了。敌人超远的索敌范围，联动和增殖机制，使得在户外的时候永远别想停下来。而与此相对的是较低的收益和较差的攻击体验，苦苦战斗不如直接逃跑。还有随处可见的有形围墙，以及不可见的 bug 形成的空气陷阱，不沿着主路走马上就会教你后悔。</p><p>这一套流程下来，在户外直接奔向下一个目标点变成了最优解。路边的小屋？没看见，里面指定一堆不知道什么东西等着你，搜刮完大概率也就几颗子弹，而且子弹本身就用不完。后来去云了些开发花边，听说有许多之前早期阶段展示过的怪物都被移除了，原因是担心影响探索体验。看了下移掉的那几个更是逆天，哼着小曲开着车给你来一套 QTE 的鸨式和天花板上的倒挂惊吓魔盒铁蜘蛛。只能说改了但没完全改。</p><p>只需要跑路还不是最痛苦的，游戏里向前方移动和向侧/后方移动的速度是不一样的，显示的视野也不是很大，玩着玩着就容易头晕，个人体验是只能撑半个小时。我猜可能是为了省掉前进的时候需要一直按着跑步键所加的设定吧，但是不说在需要拐弯的时候会很自然地左右摇杆一起打，平常走路看到个箱子什么的也会动摇杆，这时候突然慢下来就跟踩了什么东西一样，手感稀烂。完美地模拟了晕车的感觉：你不知道师傅什么时候油门什么时候刹车，只有你的前庭系统和眼睛在脑子里面打架。</p><p>地图设计感觉也能再提升一下，地图上还放了巨量的只有一截的死路，很容易走着走着忘记刚刚想要去看的地方是哪里，纯纯的反向引导。有些npc的位置也很奇怪，有放到存档点外面的，有三四个储物箱连续放一起的，还有一开始有一条往接下来走的路放到了安全区内，来来回回绕了半天才发现。</p><p>除了美术剧情，战斗也是占比比较大的一部分内容，提供了三四套技能，三种元素和一堆不同动作的武器能够使用，而且还有非常先进的无痛洗点科技鼓励尝试各种组合，构筑体验不用纠结了。不过等到构筑完上手实操的时候会发现，游戏不带视角锁定的，别说远程武器的瞄准，光是想知道敌人的方位就费老大劲了。打大型boss或者混战的时候基本就是全程在转圈圈（莱因哈特.gif）。</p><p><img src="./game-over.jpeg" alt="game-over"></p><blockquote><p>不过每次死亡都有一个小动画看（</p></blockquote><p>可能鼠标操作还相对好一点，但是手柄玩家操作起来真的怀疑staff自己打不打。怪物随便一个动作就冲出了视野，它要是高兴你的右摇杆根本放不开。打了几场之后专门出去把闪避改到手柄背键，不然完全就是站桩活靶子。</p><p>另外怪物的种类相对来说还算可以，前面还说删了好几个。但是实际游玩起来还是觉得非常重复和枯燥。不仅是刷新得太密集了，而且喜欢滥用也是一个原因，有事没事总要给你放点怪在旁边生怕你闲着。最受不了的一次是还在电梯里面过剧情，突然电梯到了，门打开冲过来一个小胡子给我就是一拳，等我打完刚刚手套说了什么完全不记得了。</p><p>解谜要素也是类似，初见流程走下来已经熟练地掌握了三种苏联高科技电子锁的开锁方法，这种完全重复，又不怎么需要动脑子的内容在关键的地方锁一两下就算了，有的地方大路上走得好好的也要给你来把锁卡一下实在是意义不明。其他的场景谜题也直接进行一个重复的使用，试验场和外面剧情遇到的谜题（在我打过的里面）是完全一样的，给人为了放谜题而放谜题的感觉。主角语音自己也吐槽很多次，看起来是故意的。</p><p>最后程序质量也有点问题， 没有征兆地闪退了好几回，还有基本全程存在的跳对话时候的花屏等等。对手柄操作模式的适配也很敷衍，在这里发表一个爆论：凡是手柄模式下还出现自由光标ui的一律扣分，<ruby>必须使用光标<rt>Atomic Heart</rt></ruby>的直接0分。</p><p><img src="./twins.jpeg" alt="twins"></p><blockquote><p>图文无关，只是想放一张双生舞伶在这里</p></blockquote><hr><p>故事设定的王道（？）苏联不特色Communism背景，以及这个大集体<u title="加上围绕这个背景展开的人物动机、关系和冲突根源。人人平等，只是有些人更加平等">简直是</u><u title="——赵鼎新">教科书</u>般的刻板印象：</p><blockquote><p>采用这一方法的学者一般不会对其理论中的人的行为模式做出详细说明。……主义理论并未对人的行为模式做出详细界定，……主义理论中，生产方式和与之相适应的阶级关系被看作历史发展的唯一主线。</p></blockquote><p>首发中文的游戏现在越来越多了，但是首发中文语音，而且还是这么优秀的配音质量和完全融入的本地化效果现在也很少见到（甚至把本土游戏算上也是这样）。最出圈的就是红色魅魔小冰箱，在客厅外放确实有些羞耻。场景素材里面所有（我遇到的）的文字都做了翻译，准心对准就会显示出来，有些场景还直接将素材做成了中文的。对一个十小时流程的游戏来说文本量应该算偏多的那一档了，联动最近星空偷跑出来的zh-MS，简直是云泥之别。文本汉化还能靠用爱发电，但是优秀的配音现在还只有厂商能提供，现在的科技更多的也只是在音色层面的转换，融入感强还得靠人声，上一次有这种配音体验是很久之前了。卷这些不比画质数毛、光追阴影来得更拟真？</p><p>配乐在几场关键战斗和剧情都挺有代入感的，可能跟本来乐库丰富也有关系吧，跟多还都是符合设定的时代和背景的。当然也没有完全局限在苏联音乐里面，在柳树培育间那首无人声的魔笛也让人印象比较深刻。流程中还有一个场景就是设定在歌剧院里面，上演着机器人芭蕾舞的表演。用很有创意的方式一边展示美术设计，一边推进剧情背景，截图键全程高强度上班。芭蕾和歌剧都看得比较少，但是觉得这里用机器人展示的东西很棒，可惜的就是没有完整的一幕，而都是几个零散动作的表演。</p><p><img src="./projection.jpeg" alt="projection"></p><p>剧情推进方面还有几段画风突变的、有点意识流的过场。角色断片之后突然出现在一个奇妙的地方，周围几个声音一直对你讲着听不懂的谜语。可能是路上偷懒没有看到相关的剧情提示，在操作的时候多少是有点云里雾里的。不过可能也不太影响，这几段的操作本身也比较弱，基本只要走路就行了，也许反而更加贴近角色当时的感觉。</p><p>最后决战篇对留下来的伏笔进行了回收，包括这几段过场也给了解释（不过还是不知道那只猫猫到底是什么？）。但是！这个结局实在是太绷不住了，最后二十分钟的流程跟我的小学作文一模一样，一看字数差不多了就直接来个唐突结尾。最后决战前会给你两个选项，类似选边的。里面一个是冲进去打几架，然后输输输，另一个直接 <em>“我不干了，我要去海滩度假”</em> 然后开始结束播片。当时心情是非常震撼的，怀疑自己到底看了个啥。</p><hr><p><em>我不写了，我要去睡觉</em></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;从概念图和 PV 开始就溢出屏幕的复古科幻的味道让人非常心动，而且首发 XGP 完全没有门槛。&lt;/p&gt;
&lt;p&gt;对我来说，科幻的背景设定到五十年前和五十年后完全没有太大的区别，毕竟都算是“遥不可及”的时代了。而且虽然俄式游戏接触得不多，但是古老的科幻故事听了不少，这个带入起来</summary>
      
    
    
    
    
    <category term="Experience" scheme="https://waynexia.github.io/tags/Experience/"/>
    
    <category term="Game" scheme="https://waynexia.github.io/tags/Game/"/>
    
  </entry>
  
  <entry>
    <title>看不见的控制流</title>
    <link href="https://waynexia.github.io/2022/12/async-cancellation-zh/"/>
    <id>https://waynexia.github.io/2022/12/async-cancellation-zh/</id>
    <published>2022-12-04T16:06:20.000Z</published>
    <updated>2026-01-14T13:19:51.705Z</updated>
    
    <content type="html"><![CDATA[<h1 id="The-Problem"><a href="#The-Problem" class="headerlink" title="The Problem"></a>The Problem</h1><p>这篇博客会讲一个在<a target="_blank" rel="noopener" href="https://github.com/GreptimeTeam/greptimedb">GreptimeDB</a>中遇到的“奇怪”的<a target="_blank" rel="noopener" href="https://github.com/GreptimeTeam/greptimedb/issues/350">问题</a>。先剧透一下是关于”async cancellation”的。</p><p>先描述一个简化的场景，我们在一个长时间运行的测试中发现了元信息损坏的问题，有一个应该单调递增的序列号重复了。它的更新逻辑非常简单：从一个原子变量中读取当前值，将新值写入文件里然后更新这个原子变量。整个流程都是串行化的（<code>file</code>是一个独占引用）。</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">update_metadata</span></span>(file: &amp;<span class="hljs-keyword">mut</span> File, counter: AtomicU64) -&gt; <span class="hljs-built_in">Result</span>&lt;()&gt; &#123;<br>    <span class="hljs-keyword">let</span> next_number = counter.load(Ordering::Relaxed) + <span class="hljs-number">1</span>;<br>    persist_number(file, number).<span class="hljs-keyword">await</span>?;<br>    counter.fetch_add(<span class="hljs-number">1</span>, Ordering::Relaxed);<br>&#125;<br></code></pre></td></tr></table></figure><p>出于一些原因，我们没有在这里使用<code>fetch_add</code>（即使它可以用，并且如果用了就没这回儿事了🤪）。举例来说，当这个流程在中间出现错误的时候我们不希望更新内存中的计数器，比如<code>persist_number()</code>写入文件时失败就会从<code>?</code>提前返回。我们清楚地知道这里有些函数会失败，并且在失败的时候会提早结束执行来传播错误，所以编码的时候有注意这些问题。</p><p>但是到了<code>.await</code>这里事情就变得奇妙了起来，因为async cancellation带来了一个隐藏的控制流。</p><h1 id="Async-Cancellation"><a href="#Async-Cancellation" class="headerlink" title="Async Cancellation"></a>Async Cancellation</h1><h2 id="async-task-and-runtime"><a href="#async-task-and-runtime" class="headerlink" title="async task and runtime"></a>async task and runtime</h2><p>如果这时候你已经猜到了是谁干的好事，可以跳过这一章节。或者让我从一些伪代码开始解释在”await point”那里到底发生了什么，以及runtime是如何参与其中的。首先是<code>poll_future</code>，来自定义在<code>Future</code>的<a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/future/trait.Future.html#tymethod.poll"><code>poll</code></a>方法。我们写的异步方法都会被转化成类似这样子的一个匿名的<code>Future</code>实现。</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">poll_future</span></span>() -&gt; FutureOutput &#123;<br>    <span class="hljs-keyword">match</span> status_of_the_task &#123;<br>        Ready(output) =&gt; &#123;<br>            <span class="hljs-comment">// the task is finished, and we have it output.</span><br>            <span class="hljs-comment">// some logic</span><br>            <span class="hljs-keyword">return</span> our_output;<br>        &#125;,<br>        Pending =&gt; &#123;<br>            <span class="hljs-comment">// it is not ready, we don&#x27;t have the output.</span><br>            <span class="hljs-comment">// thus we cannot make progress and need to wait</span><br>            <span class="hljs-keyword">return</span> Pending;<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><code>async</code>块通常包含其他的异步方法，比如<code>update_metadata</code>和<code>persist_number</code>。这里把<code>persist_number</code>称为<code>update_metadata</code>的子异步任务。每个<code>.await</code>都会被展开成类似<code>poll_future</code>的东西，等待子任务的结果并继续执行。在这个例子中就是等待<code>persist_number</code>的结果返回<code>Ready</code>再更新计数器，否则不更新。</p><p>第二段伪代码是一个简化的runtime，它负责轮询(poll)异步任务直到它们完成（不过考虑到接下来要说的，“直到……完成”可能不是一个合适的表述）。在GreptimeDB中使用<a target="_blank" rel="noopener" href="https://docs.rs/tokio/latest/tokio/"><code>tokio</code></a>作为runtime。现在的异步runtime可能有很多特性和功能，但是最基础的一个就是轮询这些任务。</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">runtime</span></span>(&amp;<span class="hljs-keyword">self</span>) &#123;<br>    <span class="hljs-keyword">loop</span> &#123;<br>        <span class="hljs-keyword">let</span> future_tasks: <span class="hljs-built_in">Vec</span>&lt;Task&gt; = <span class="hljs-keyword">self</span>.get_tasks();<br>        <span class="hljs-keyword">for</span> task <span class="hljs-keyword">in</span> tasks &#123;<br>            <span class="hljs-keyword">match</span> task.poll_future()&#123;<br>                Ready(output) =&gt; &#123;<br>                    <span class="hljs-comment">// this task is finished. wake it with the result</span><br>                    task.wake(output);<br>                &#125;,<br>                Pending =&gt; &#123;<br>                    <span class="hljs-comment">// this task needs some time to run. poll it later</span><br>                    <span class="hljs-keyword">self</span>.poll_later(task);<br>                &#125;<br>            &#125;<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>这就是一个非常简化的future和runtime的模型，就是把上面的两个方法结合起来。从某种方面来说，它们不过就是一个循环（真实的runtime非常复杂，这里为了内容集中省略了很多东西）。需要强调的是，每个<code>.await</code>都代表着一个或者多个函数调用(调用到<code>poll()</code>或者说是<code>poll_future()</code>的)。这就是标题所谓的“隐藏的控制流”，以及cancellation发生的地方。</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">run</span></span>() &#123;<br>    <span class="hljs-keyword">loop</span> &#123;<br>        <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> Ready(result) = task.poll() &#123;<br>            <span class="hljs-keyword">break</span> result;<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>弄明白原理并不复杂，但是（对我来说）能够想到并不自然。在把其他问题都排除掉之后我一直盯着和第一个示例里面差不多长的这几行，知道问题就发生在这里，在这个<code>.await</code>上。也不知道是太多次成功的异步函数调用麻痹了注意还是我的心智模型中没有把这两点联系起来，本垃圾花了一整晚来怀疑人生。</p><h2 id="cancellation"><a href="#cancellation" class="headerlink" title="cancellation"></a>cancellation</h2><p>目前为止的内容是问题复盘的标准流程。我们接下来想展开讨论一下cancellation，它是与runtime的行为相关的。虽然rust中的很多runtime都有类似的行为，但是这不是一个必须的特性，比如我的这个<a target="_blank" rel="noopener" href="https://github.com/waynexia/texn">玩具runtime</a>就不支持cancellation。我会以tokio为例，因为这是这个问题发生的地方。其他的runtime可能也是类似的。</p><p>在tokio中，可以使用<a target="_blank" rel="noopener" href="https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html#method.abort"><code>JoinHandle::abort()</code></a>来取消一个task。task结构中有一个“cancel marker bit”来跟踪一个任务是否被取消了。如果它发现一个task被取消了，就会停止执行这个task。(代码在<a target="_blank" rel="noopener" href="https://github.com/tokio-rs/tokio/blob/00bf5ee8a855c28324fa4dff3abf11ba9f562a85/tokio/src/runtime/task/state.rs#L283-L291">这里</a>）</p><figure class="highlight rust"><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></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">// If the task is running, we mark it as cancelled. The thread</span><br><span class="hljs-comment">// running the task will notice the cancelled bit when it</span><br><span class="hljs-comment">// stops polling and it will kill the task.</span><br><span class="hljs-comment">//</span><br><span class="hljs-comment">// The set_notified() call is not strictly necessary but it will</span><br><span class="hljs-comment">// in some cases let a wake_by_ref call return without having</span><br><span class="hljs-comment">// to perform a compare_exchange.</span><br>snapshot.set_notified();<br>snapshot.set_cancelled();<br></code></pre></td></tr></table></figure><p>async cancellation背后的逻辑也很简单，就是runtime放弃了继续轮询你的task，就和<code>?</code>差不多。某种程度上可能更棘手一点，因为我们不能像<code>Err</code>那样处理这个cancellation。不过这代表我们需要考虑每一个<code>.await</code>都有可能随时被cancel掉吗？这也太麻烦了。以本文的这个metadata更新的情况为例，如果把cancel纳入考虑范围，我们需要检查文件是否和内存中的状态一致，如果不一致就要回滚持久化的改动等等等等🫠 坏消息是，在某些方面答案是肯定的，runtime可以对你的future做任何事情。不过好在大多数情况下它们都还是很遵守规矩的。</p><h1 id="Runtime-Behavior"><a href="#Runtime-Behavior" class="headerlink" title="Runtime Behavior"></a>Runtime Behavior</h1><p>这一小节打算讨论一下我所期望的runtime的行为，以及有哪些是我们现在已经可以用得上的。</p><h2 id="marker-trait"><a href="#marker-trait" class="headerlink" title="marker trait"></a>marker trait</h2><p>首先自然希望runtime不要无条件地取消我的task，而是尝试通过类型系统来变得更友好，比如借助类似<code>CancelSafe</code>的marker trait 。对于cancellation safety这个词，tokio在它的<a target="_blank" rel="noopener" href="https://docs.rs/tokio/latest/tokio/macro.select.html#cancellation-safety">文档</a>中有提到：</p><blockquote><p>To determine whether your own methods are cancellation safe, look for the location of uses of  <code>.await</code> . This is because when an asynchronous method is cancelled, that always happens at an  <code>.await</code> . If your function behaves correctly even if it is restarted while waiting at an  <code>.await</code> , then it is cancellation safe.  </p></blockquote><p>简单来说就是用来描述一个task是否可以安全地被取消掉。这肯定是一个async task的属性之一。在上面的链接中tokio维护了一个很长的列表，列出了哪些是安全的以及哪些是不安全的。看起来这和<a href="jhttps://doc.rust-lang.org/std/panic/trait.UnwindSafe.html"><code>UnwindSafe</code></a>这个marker trait很像。两者都是描述“这种控制流程并不总是被预料到的”，并且“有可能导致一些微妙的bug”的这样一种属性。</p><p>如果有这样一个<code>CancelSafe</code>的trait，我们有途径可以告诉runtime我们的异步任务是否可以安全地被取消掉，同时也是一种方式让用户承诺“cancelling”这个控制流程是被仔细处理过的。如果发现没有实现这个trait，那就意味着我们不希望这个task被取消掉，简单而清晰。以<code>timeout()</code>为例：</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-comment">/// The marker trait</span><br><span class="hljs-class"><span class="hljs-keyword">trait</span> <span class="hljs-title">CancelSafe</span></span> &#123;&#125;<br><br><span class="hljs-comment">/// Only cancellable task can be timeout-ed</span><br><span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">timeout</span></span>&lt;F&gt;(duration: Duration, future: F) -&gt; Timeout&lt;F&gt; <span class="hljs-keyword">where</span><br>    F: Future + CancelSafe<br>&#123;&#125;<br></code></pre></td></tr></table></figure><h2 id="volunteer-cancel"><a href="#volunteer-cancel" class="headerlink" title="volunteer cancel"></a>volunteer cancel</h2><p>另一个方式是让任务自愿地取消。就像Kotlin中的<a target="_blank" rel="noopener" href="https://kotlinlang.org/docs/cancellation-and-timeouts.html#cancellation-is-cooperative">cooperative cancellation</a>一样，它有一个<a target="_blank" rel="noopener" href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/is-active.html"><code>isActive</code></a>方法来检查一个task是否被取消掉。这只是一个检测方法，是否要取消完全取决于task本身。下面是Kotlin文档中的一个例子，cooperative cancellation发生在第5行。这种方式把“隐藏的控制流程”放在了桌面上，让我们能以一种更自然地来考虑和处理calcellation，就像<code>Option</code>或<code>Result</code>一样。</p><figure class="highlight kotlin"><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><code class="hljs kotlin"><span class="hljs-keyword">val</span> startTime = System.currentTimeMillis()<br><span class="hljs-keyword">val</span> job = launch(Dispatchers.Default) &#123;<br>    <span class="hljs-keyword">var</span> nextPrintTime = startTime<br>    <span class="hljs-keyword">var</span> i = <span class="hljs-number">0</span><br>    <span class="hljs-keyword">while</span> (isActive) &#123; <span class="hljs-comment">// cancellable computation loop</span><br>        <span class="hljs-comment">// print a message twice a second</span><br>        <span class="hljs-keyword">if</span> (System.currentTimeMillis() &gt;= nextPrintTime) &#123;<br>            println(<span class="hljs-string">&quot;job: I&#x27;m sleeping <span class="hljs-subst">$&#123;i++&#125;</span> ...&quot;</span>)<br>            nextPrintTime += <span class="hljs-number">500L</span><br>        &#125;<br>    &#125;<br>&#125;<br>delay(<span class="hljs-number">1300L</span>) <span class="hljs-comment">// delay a bit</span><br>println(<span class="hljs-string">&quot;main: I&#x27;m tired of waiting!&quot;</span>)<br>job.cancelAndJoin() <span class="hljs-comment">// cancels the job and waits for its completion</span><br>println(<span class="hljs-string">&quot;main: Now I can quit.&quot;</span>)<br></code></pre></td></tr></table></figure><p>并且我认为这也不难实现，Tokio现在已经有了<a target="_blank" rel="noopener" href="https://github.com/tokio-rs/tokio/blob/00bf5ee8a855c28324fa4dff3abf11ba9f562a85/tokio/src/runtime/task/state.rs#L41"><code>Cancelled</code> bit</a>和<a target="_blank" rel="noopener" href="https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html"><code>CancellationToken</code></a>。虽然看起来和期望的还有点不一样。最后还需要runtime把cancellation的权利交给task。否则情况可能没有什么大的不同。</p><h2 id="explicit-detach"><a href="#explicit-detach" class="headerlink" title="explicit detach"></a>explicit detach</h2><p>现在是否有手段能防止task被取消呢？在tokio中我们可以通过drop <code>JoinHandle</code>来“detach”一个任务到后台。一个detach task意味着没有前台的handle来控制这个任务，从某种意义上来说也就使得其他人不能在外面套一层<code>timeout</code>或<code>select</code>，从而间接地使它不会被取消执行。并且开头提到的问题就是通过这种方式解决的。</p><blockquote><p>A  <code>JoinHandle</code>  <em>detaches</em> the associated task when it is dropped, which means that there is no longer any handle to the task, and no way to  <code>join</code> on it.  </p></blockquote><p>不过虽然有办法能够实现这个功能，我在想是否像<a target="_blank" rel="noopener" href="https://docs.rs/glommio/0.7.0/glommio/struct.Task.html#method.detach"><code>glommio&#39;s</code></a>一样有一个显式的<code>detach</code>方法，类似一个不返回<code>JoinHandle</code>的<code>spawn</code>方法会更好。但这些都是琐碎的事情，一个runtime通常不会完全没有理由就取消一个task，并且在大多数情况下都是出于用户的要求，只不过有时候可能没有注意到，就像<code>select</code>中的那些“未选中的分支”或者<code>tonic</code>中请求处理的逻辑那样。所以如果我们确定一个task是不能被取消的话，显式地detach可能能预防某些悲剧的发生。</p><h1 id="Back-To-The-Problem"><a href="#Back-To-The-Problem" class="headerlink" title="Back To The Problem"></a>Back To The Problem</h1><p>目前为止所有问题都清晰了，让我们开始修复这个bug吧！首先，为什么我们的future会被取消呢？通过函数调用链路很容易就能发现整个处理过程都是在<code>tonic</code>的请求执行逻辑中就地执行的，而对于一个网络请求来说有一个超时行为是很常见的。解决方案也很简单，就是将服务器处理逻辑detach到另一个runtime中，从而防止它被取消。只需要<a target="_blank" rel="noopener" href="https://github.com/GreptimeTeam/greptimedb/pull/376/files#diff-9756dcef86f5ba1d60e01e41bf73c65f72039f9aaa057ffd03f3fc2f7dadfbd0R46-R54">几行代码</a>就能完成。</p><figure class="highlight diff"><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><code class="hljs diff"><span class="hljs-meta">@@ -30,12 +40,24 @@</span> impl BatchHandler &#123;<br>         &#125;<br>         batch_resp.admins.push(admin_resp);<br><br><span class="hljs-deletion">-        for db_req in batch_req.databases &#123;</span><br><span class="hljs-deletion">-            for obj_expr in db_req.exprs &#123;</span><br><span class="hljs-deletion">-                let object_resp = self.query_handler.do_query(obj_expr).await?;</span><br><span class="hljs-deletion">-                db_resp.results.push(object_resp);</span><br><span class="hljs-addition">+        let (tx, rx) = oneshot::channel();</span><br><span class="hljs-addition">+        let query_handler = self.query_handler.clone();</span><br><span class="hljs-addition">+        let _ = self.runtime.spawn(async move &#123;</span><br><span class="hljs-addition">+            // execute request in another runtime to prevent the execution from being cancelled unexpected by tonic runtime.</span><br><span class="hljs-addition">+            let mut result = vec![];</span><br><span class="hljs-addition">+            for db_req in batch_req.databases &#123;</span><br><span class="hljs-addition">+                for obj_expr in db_req.exprs &#123;</span><br><span class="hljs-addition">+                    let object_resp = query_handler.do_query(obj_expr).await;</span><br><span class="hljs-addition">+</span><br><span class="hljs-addition">+                    result.push(object_resp);</span><br><span class="hljs-addition">+                &#125;</span><br>             &#125;<br></code></pre></td></tr></table></figure><p>现在一切正常了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;The-Problem&quot;&gt;&lt;a href=&quot;#The-Problem&quot; class=&quot;headerlink&quot; title=&quot;The Problem&quot;&gt;&lt;/a&gt;The Problem&lt;/h1&gt;&lt;p&gt;这篇博客会讲一个在&lt;a target=&quot;_blank&quot; rel=&quot;n</summary>
      
    
    
    
    
    <category term="Rust" scheme="https://waynexia.github.io/tags/Rust/"/>
    
  </entry>
  
  <entry>
    <title>The Hidden Control Flow</title>
    <link href="https://waynexia.github.io/2022/12/async-cancellation/"/>
    <id>https://waynexia.github.io/2022/12/async-cancellation/</id>
    <published>2022-12-04T16:06:19.000Z</published>
    <updated>2026-01-14T13:19:51.705Z</updated>
    
    <content type="html"><![CDATA[<h1 id="The-Problem"><a href="#The-Problem" class="headerlink" title="The Problem"></a>The Problem</h1><p>This post is talking about a “weird” <a target="_blank" rel="noopener" href="https://github.com/GreptimeTeam/greptimedb/issues/350">problem</a> we encountered in <a target="_blank" rel="noopener" href="https://github.com/GreptimeTeam/greptimedb">GreptimeDB</a>. And, a little spoiler, it’s about the “async cancellation”.</p><p>Let’s first describe the (simplified) scenario. We observed metadata corruption in a long-run test. A series number is duplicated, but it should be increased monotonously. The update logic is very straightforward – load value from an in-memory atomic counter, persist the new series number to file, and then update the in-memory counter. The entire procedure is serialized (<code>file</code> is a mutable reference):</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">update_metadata</span></span>(file: &amp;<span class="hljs-keyword">mut</span> File, counter: AtomicU64) -&gt; <span class="hljs-built_in">Result</span>&lt;()&gt; &#123;<br>    <span class="hljs-keyword">let</span> next_number = counter.load(Ordering::Relaxed) + <span class="hljs-number">1</span>;<br>    persist_number(file, number).<span class="hljs-keyword">await</span>?;<br>    counter.fetch_add(<span class="hljs-number">1</span>, Ordering::Relaxed);<br>&#125;<br></code></pre></td></tr></table></figure><p>For some reason, we are not using <code>fetch_add</code> here, though it does work, and if we’ve done so, then the story won’t happen 🤪 For example, we don’t want to update the in-memory counter when this procedure fails halfway, like operation <code>persist_number()</code> cannot write to the file. Here the sweet sugar <code>?</code> stands for such a situation. We know clearly that this function call may fail, and if it fails the caller returns early to propagate the error. So we will handle it carefully with that in mind.</p><p>But things become tricky with <code>.await</code>, the hidden control flow comes due to async cancellation.</p><h1 id="Async-Cancellation"><a href="#Async-Cancellation" class="headerlink" title="Async Cancellation"></a>Async Cancellation</h1><h2 id="async-task-and-runtime"><a href="#async-task-and-runtime" class="headerlink" title="async task and runtime"></a>async task and runtime</h2><p>If you have figured out the shape of the “criminal”, you may want to skip this section. Instead, I’ll start with some pseudocode to show what happens in the “await point”, and how it interacts with the runtime. First is <code>poll_future</code>, it comes from the <code>Future</code>‘s <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/future/trait.Future.html#tymethod.poll"><code>poll</code></a> function, as every <code>async fn</code>‘s we write will be desugared to an anonymous <code>Future</code> implementation.</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">poll_future</span></span>() -&gt; FutureOutput &#123;<br>    <span class="hljs-keyword">match</span> status_of_the_task &#123;<br>        Ready(output) =&gt; &#123;<br>            <span class="hljs-comment">// the task is finished, and we have it output.</span><br>            <span class="hljs-comment">// some logic</span><br>            <span class="hljs-keyword">return</span> our_output;<br>        &#125;,<br>        Pending =&gt; &#123;<br>            <span class="hljs-comment">// it is not ready, we don&#x27;t have the output.</span><br>            <span class="hljs-comment">// thus we cannot make progress and need to wait</span><br>            <span class="hljs-keyword">return</span> Pending;<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><code>async</code> block usually contains other async functions, like <code>update_metadata</code> and <code>persist_number</code>. Say <code>persist_number</code> is a sub-async-task of <code>update_metadata</code>. Each <code>.await</code> point will be expanded to something like <code>poll_future</code> – <code>await</code>ing the subtask’s output and make progress when the subtask is ready. Here we need to wait <code>persist_number</code>‘s task returns <code>Ready</code> before we update the counter, otherwise we cannot do it.</p><p>And the second one is a (toy) runtime, which is in response to poll futures delivered to it. In GreptimeDB we use <a target="_blank" rel="noopener" href="https://docs.rs/tokio/latest/tokio/"><code>tokio</code></a> as our runtime. An async runtime may have tons of features and logic, but the most basic one is to poll: as the name tells, keep running unfinished tasks until they finish (but consider the things I’m going to write later, the “until” might not be a proper word).</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">runtime</span></span>(&amp;<span class="hljs-keyword">self</span>) &#123;<br>    <span class="hljs-keyword">loop</span> &#123;<br>        <span class="hljs-keyword">let</span> future_tasks: <span class="hljs-built_in">Vec</span>&lt;Task&gt; = <span class="hljs-keyword">self</span>.get_tasks();<br>        <span class="hljs-keyword">for</span> task <span class="hljs-keyword">in</span> tasks &#123;<br>            <span class="hljs-keyword">match</span> task.poll_future()&#123;<br>                Ready(output) =&gt; &#123;<br>                    <span class="hljs-comment">// this task is finished. wake it with the result</span><br>                    task.wake(output);<br>                &#125;,<br>                Pending =&gt; &#123;<br>                    <span class="hljs-comment">// this task needs some time to run. poll it later</span><br>                    <span class="hljs-keyword">self</span>.poll_later(task);<br>                &#125;<br>            &#125;<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>That is it, a very minimalist model of future and runtime. Combining these two functions, you will find that in some aspects, it is just a loop (again, I’ve omitted lots of details to keep tight on the topic; the real world is way more complex). I want to stress the thing that each <code>.await</code> imply one or more function calls (call to <code>poll()</code> or <code>poll_future()</code>). This is the “hidden control flow” in the title and the place cancellation takes effort.</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">run</span></span>() &#123;<br>    <span class="hljs-keyword">loop</span> &#123;<br>        <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> Ready(result) = task.poll() &#123;<br>            <span class="hljs-keyword">break</span> result;<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>Know it is not hard, but thinking about it is not easy (at least for me). I’ve stared at these lines for minutes after I narrow the scope down to as simple as the first code snippet. I know the problem is definitely in the <code>.await</code>. But don’t know whether too many successful async calls have numbed me or my mental model hasn’t linked these two points. The bulky garbage, me, spent a whole sleepless night doubting life and the world.</p><h2 id="cancellation"><a href="#cancellation" class="headerlink" title="cancellation"></a>cancellation</h2><p>So far is the standard part. We will then talk about cancellation, which is runtime-dependent. Though many runtimes in rust have similar behavior, this is not a required feature, i.e., a runtime can not support cancellation at all like <a target="_blank" rel="noopener" href="https://github.com/waynexia/texn">this toy</a>. And I’ll take tokio as an example because the story happens there. Other runtimes may be similar.</p><p>In tokio, one can use <a target="_blank" rel="noopener" href="https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html#method.abort"><code>JoinHandle::abort()</code></a> to cancel a task. Tasks have a “cancel marker bit” tracks whether it’s cancelled. And if the runtime finds a task is cancelled, it will kill that task (code from <a target="_blank" rel="noopener" href="https://github.com/tokio-rs/tokio/blob/00bf5ee8a855c28324fa4dff3abf11ba9f562a85/tokio/src/runtime/task/state.rs#L283-L291">here</a>):</p><figure class="highlight rust"><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></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">// If the task is running, we mark it as cancelled. The thread</span><br><span class="hljs-comment">// running the task will notice the cancelled bit when it</span><br><span class="hljs-comment">// stops polling and it will kill the task.</span><br><span class="hljs-comment">//</span><br><span class="hljs-comment">// The set_notified() call is not strictly necessary but it will</span><br><span class="hljs-comment">// in some cases let a wake_by_ref call return without having</span><br><span class="hljs-comment">// to perform a compare_exchange.</span><br>snapshot.set_notified();<br>snapshot.set_cancelled();<br></code></pre></td></tr></table></figure><p>The theory behind async cancellation is also elementary. It’s just the runtime gives up to keep polling your task when it’s not yet finished, just like <code>?</code> or even tougher because we cannot catch this cancellation like <code>Err</code>. But does it means that we need to take care of every single <code>.await</code>? It would be very annoying. Take this metadata updating as an example. If we have to consider this, we need to check if the file is consistent with the memory state and revert the persisted change if found inconsistency. Well… 🫠 In some aspects, yes. The runtime can literally do anything to your future. But the good thing is that most of them are disciplined.</p><h1 id="Runtime-Behavior"><a href="#Runtime-Behavior" class="headerlink" title="Runtime Behavior"></a>Runtime Behavior</h1><p>This section will discuss what I would expect from a runtime and what we can get for now.</p><h2 id="marker-trait"><a href="#marker-trait" class="headerlink" title="marker trait"></a>marker trait</h2><p>I want the runtime not to cancel my task unconditionally and turn to the type system for help. This is wondering if there is a marker trait like <code>CancelSafe</code>. For the word cancellation safety, tokio has said about it in its <a target="_blank" rel="noopener" href="https://docs.rs/tokio/latest/tokio/macro.select.html#cancellation-safety">documentation</a>:</p><blockquote><p>To determine whether your own methods are cancellation safe, look for the location of uses of  <code>.await</code> . This is because when an asynchronous method is cancelled, that always happens at an  <code>.await</code> . If your function behaves correctly even if it is restarted while waiting at an  <code>.await</code> , then it is cancellation safe.  </p></blockquote><p>That is, whether a task is safe to be cancelled. This is definitely an “attribute” of an async task. You can find that tokio has a long list of what is safe and what isn’t in the library from the above link. And, in some ways, I think it’s just like the <a href="jhttps://doc.rust-lang.org/std/panic/trait.UnwindSafe.html"><code>UnwindSafe</code></a> marker. Both are “<em>this sort of control flow is not always anticipated</em>“ and “<em>has the possibility of causing subtle bugs</em>“.</p><p>With such a <code>CancelSafe</code> trait, we can tell the runtime if our spawned future is ok to be cancelled, and we promise the “cancelling” control flow is carefully handled. And if without this, means we don’t want the task to be cancelled. Simple and clear. This is also an approach for the runtimes to require their users (like you and me) to check if their tasks are able to be cancelled. Take <code>timeout()</code> as an example:</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-comment">/// The marker trait</span><br><span class="hljs-class"><span class="hljs-keyword">trait</span> <span class="hljs-title">CancelSafe</span></span> &#123;&#125;<br><br><span class="hljs-comment">/// Only cancellable task can be timeout-ed</span><br><span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">timeout</span></span>&lt;F&gt;(duration: Duration, future: F) -&gt; Timeout&lt;F&gt; <span class="hljs-keyword">where</span><br>    F: Future + CancelSafe<br>&#123;&#125;<br></code></pre></td></tr></table></figure><h2 id="volunteer-cancel"><a href="#volunteer-cancel" class="headerlink" title="volunteer cancel"></a>volunteer cancel</h2><p>Another approach is to cancel voluntarily. Like the <a target="_blank" rel="noopener" href="https://kotlinlang.org/docs/cancellation-and-timeouts.html#cancellation-is-cooperative">cooperative cancellation in Kotlin</a>, it has an <a target="_blank" rel="noopener" href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/is-active.html"><code>isActive</code></a> method for a task to check if it is cancelled. And this is only a tester method, to cancel or not is fully dependent on the task itself. I paste an example from Kotlin’s document below, the “cooperative cancellation” happens in line 5. This way brings the “hidden control flow” on the table and makes it more natural to consider and handle the cancellation just like <code>Option</code> or <code>Result</code>.</p><figure class="highlight kotlin"><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><code class="hljs kotlin"><span class="hljs-keyword">val</span> startTime = System.currentTimeMillis()<br><span class="hljs-keyword">val</span> job = launch(Dispatchers.Default) &#123;<br>    <span class="hljs-keyword">var</span> nextPrintTime = startTime<br>    <span class="hljs-keyword">var</span> i = <span class="hljs-number">0</span><br>    <span class="hljs-keyword">while</span> (isActive) &#123; <span class="hljs-comment">// cancellable computation loop</span><br>        <span class="hljs-comment">// print a message twice a second</span><br>        <span class="hljs-keyword">if</span> (System.currentTimeMillis() &gt;= nextPrintTime) &#123;<br>            println(<span class="hljs-string">&quot;job: I&#x27;m sleeping <span class="hljs-subst">$&#123;i++&#125;</span> ...&quot;</span>)<br>            nextPrintTime += <span class="hljs-number">500L</span><br>        &#125;<br>    &#125;<br>&#125;<br>delay(<span class="hljs-number">1300L</span>) <span class="hljs-comment">// delay a bit</span><br>println(<span class="hljs-string">&quot;main: I&#x27;m tired of waiting!&quot;</span>)<br>job.cancelAndJoin() <span class="hljs-comment">// cancels the job and waits for its completion</span><br>println(<span class="hljs-string">&quot;main: Now I can quit.&quot;</span>)<br></code></pre></td></tr></table></figure><p>And this is not hard to achieve in my opinion. Tokio already has the <a target="_blank" rel="noopener" href="https://github.com/tokio-rs/tokio/blob/00bf5ee8a855c28324fa4dff3abf11ba9f562a85/tokio/src/runtime/task/state.rs#L41"><code>Cancelled</code> bit</a> and <a target="_blank" rel="noopener" href="https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html"><code>CancellationToken</code></a>. But they look a bit different than what I describe. And after all of these, we need runtime to give the cancellation right back to our task. Or the situation might not have big difference.</p><h2 id="explicit-detach"><a href="#explicit-detach" class="headerlink" title="explicit detach"></a>explicit detach</h2><p>Can we force the runtime not to cancel our tasks at present? In tokio we can “detach” a task to the background by dropping the <code>JoinHandle</code>. A detached task means there is no foreground handle to the spawned task, and in some aspect, others cannot wrap a <code>timeout</code> or <code>select</code> over it, making it un-cancellable. And the problem in the very beginning is solved in this way.</p><blockquote><p>A  <code>JoinHandle</code>  <em>detaches</em> the associated task when it is dropped, which means that there is no longer any handle to the task, and no way to  <code>join</code> on it.  </p></blockquote><p>Though there is the functionality, I would wonder if it’s better to have an explicit <code>detach</code> method like <a target="_blank" rel="noopener" href="https://docs.rs/glommio/0.7.0/glommio/struct.Task.html#method.detach"><code>glommio&#39;s</code></a>, or even a <code>detach</code> method in the runtime like <code>spawn</code>, which doesn’t return the <code>JoinHandle</code>. But these are trifles. A runtime usually won’t cancel a task for no reason, and in most cases, it’s required by the users. But sometimes you haven’t noticed that, like those “unselect branches” in <code>select</code>, or the logic in <code>tonic</code>‘s request handler. And if we are sure that a task is ready for cancellation, explicit detach may prevent it from tragedy sometime.</p><h1 id="Back-To-The-Problem"><a href="#Back-To-The-Problem" class="headerlink" title="Back To The Problem"></a>Back To The Problem</h1><p>So far everything is clear. Let’s start to wipe out this bug! First is, why our future is cancelled? Through the function call graph we can easily find the entire process procedure is executed in-place in <code>tonic</code>‘s request licensing runtime, and it’s common for an internet request to have a timeout behavior. And the solution is also simple, just detaching the server processing logic into another runtime to prevent it from cancelled with the request. Only <a target="_blank" rel="noopener" href="https://github.com/GreptimeTeam/greptimedb/pull/376/files#diff-9756dcef86f5ba1d60e01e41bf73c65f72039f9aaa057ffd03f3fc2f7dadfbd0R46-R54">a few lines</a>:</p><figure class="highlight diff"><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><code class="hljs diff"><span class="hljs-meta">@@ -30,12 +40,24 @@</span> impl BatchHandler &#123;<br>         &#125;<br>         batch_resp.admins.push(admin_resp);<br><br><span class="hljs-deletion">-        for db_req in batch_req.databases &#123;</span><br><span class="hljs-deletion">-            for obj_expr in db_req.exprs &#123;</span><br><span class="hljs-deletion">-                let object_resp = self.query_handler.do_query(obj_expr).await?;</span><br><span class="hljs-deletion">-                db_resp.results.push(object_resp);</span><br><span class="hljs-addition">+        let (tx, rx) = oneshot::channel();</span><br><span class="hljs-addition">+        let query_handler = self.query_handler.clone();</span><br><span class="hljs-addition">+        let _ = self.runtime.spawn(async move &#123;</span><br><span class="hljs-addition">+            // execute request in another runtime to prevent the execution from being cancelled unexpected by tonic runtime.</span><br><span class="hljs-addition">+            let mut result = vec![];</span><br><span class="hljs-addition">+            for db_req in batch_req.databases &#123;</span><br><span class="hljs-addition">+                for obj_expr in db_req.exprs &#123;</span><br><span class="hljs-addition">+                    let object_resp = query_handler.do_query(obj_expr).await;</span><br><span class="hljs-addition">+</span><br><span class="hljs-addition">+                    result.push(object_resp);</span><br><span class="hljs-addition">+                &#125;</span><br>             &#125;<br></code></pre></td></tr></table></figure><p>Now all fine.</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;The-Problem&quot;&gt;&lt;a href=&quot;#The-Problem&quot; class=&quot;headerlink&quot; title=&quot;The Problem&quot;&gt;&lt;/a&gt;The Problem&lt;/h1&gt;&lt;p&gt;This post is talking about a “weir</summary>
      
    
    
    
    
    <category term="Rust" scheme="https://waynexia.github.io/tags/Rust/"/>
    
  </entry>
  
  <entry>
    <title>Paper Reading: CURP protocol</title>
    <link href="https://waynexia.github.io/2022/09/curp-notes/"/>
    <id>https://waynexia.github.io/2022/09/curp-notes/</id>
    <published>2022-09-18T17:58:33.000Z</published>
    <updated>2026-01-14T13:19:51.707Z</updated>
    
    <content type="html"><![CDATA[<p>论文名字是《Exploiting Commutativity For Practical Fast Replication》</p><p>CURP全称Consistent Unordered Replication Protocol，主要是利用操作之间的相关性（Commutative）来实现局部乱序提交的协议。这里Commutative指的是不同操作记录之间是否可以交换执行顺序而不影响最终结果，比如<code>x=1</code>、<code>y=2</code>和<code>x=3</code>三条记录中<code>y=2</code>与其他两条都是无关的，满足Commutative可以乱序执行，而<code>x=1</code>与<code>x=3</code>之间有关系，需要一个确定的执行顺序。</p><p>相比于其他的复制协议，CURP的特点是对于满足Commutative的记录可以以1 RTT的延时完成提交。</p><h1 id="集群角色"><a href="#集群角色" class="headerlink" title="集群角色"></a>集群角色</h1><p>除了常见的<code>Client</code>，<code>Leader</code>，<code>Backup</code>之外CRUP中还引入了一个新的<code>Witness</code>角色。简单概括一下这些角色：</p><ul><li><code>Client</code><ul><li>与<code>Witness</code>和<code>Leader</code>交互，对<code>Witness</code>的通信是广播的形式(同时与所有<code>Witness</code>通信)  </li><li>自己也有一定的状态，主要是关于操作的执行情况的。</li></ul></li><li><code>Witness</code>  <ul><li>CURP协议定义的角色，存在复数个实例，且实例之间对等  </li><li>有存储能力来临时存储近期的操作记录，但不需要维护状态机（操作结果），就是一个WAL</li><li>需要感知具体的操作内容并判断操作之间是否有可交换性(commutative)  </li><li>实例间独立运作及决策，也独立于<code>Leader</code>/<code>Backup</code>。</li></ul></li><li><code>Leader</code><ul><li>集群主节点，有其他主备协议中常见的<code>Leader</code>的能力(维护状态机，答复读写请求，数据同步等)  </li><li>同样需要感知具体操作并判断可交换性</li><li>可以在把操作同步给<code>Backup</code>之前答复请求</li><li>论文中叫<code>Master</code></li></ul></li><li><code>Backup</code><ul><li>普通的backup角色</li></ul></li></ul><p>在论文的正文部分中<code>Witness</code>都是作为一个独立的部署实例来讨论的，不过附录里也提到实践中也没必要将<code>Witness</code>单独部署，而是跟着<code>Leader</code>/<code>Backup</code>一起运作，只作为逻辑上的一个独立单元。所以一些RPC请求实际上是可以合并（比如<code>Client</code>同时请求<code>Leader</code>和与其在一起的<code>Witness</code>）或者是本地IPC（比如<code>Leader</code>/<code>Backup</code>请求对应的<code>Witness</code>）的。因此对于实际使用的时候来说<code>Witness</code>更像是CURP引入的新机制的一个概括抽象。</p><h1 id="基本操作"><a href="#基本操作" class="headerlink" title="基本操作"></a>基本操作</h1><p>协议的具体内容可以从几个基本操作来说明</p><h2 id="更新"><a href="#更新" class="headerlink" title="更新"></a>更新</h2><p>由<code>Client</code>发起，<code>Client</code>同时对<code>Leader</code>及<strong>所有</strong><code>Witnesses</code>发出请求，根据这个请求与之前的请求是否无关(可交换，commutative)有两种情况：  </p><ul><li>如果无关，那么<code>Leader</code>和<strong>所有</strong><code>Witnesses</code>都会accept这个请求。<code>Client</code>在收到<strong>全部</strong>的accept答复之后可以当这个请求已完成。</li><li>如果<code>Leader</code>或任意一个 <code>Witness</code> 认为有关(not commutative)，<code>Client</code>都需要要求<code>Leader</code>进行一次sync，将状态与<code>Backups</code>进行同步后答复<code>Client</code>。<code>Client</code>可以依据这个sync请求的答复将请求完成，这种情况下<code>Witness</code>的答复没有影响。</li></ul><p>也就是说，一次操作从<code>Client</code>发起到完成一共有三个状态：<code>Proposed</code>，<code>Unsynced</code>和<code>Synced</code>。<code>Client</code>会对除了<code>Backup</code>之外的所有节点发起请求，在上面的第一种情况下成为<code>Unsynced</code>，此时<code>Client</code>已经可以认为该操作完成，<code>Leader</code>也可以把操作应用到状态机上。<code>Leader</code>应用<code>Unsynced</code>操作的过程叫做“推测执行（speculative execution）”。<code>Unsynced</code>的操作会在一下次收到sync请求时往<code>Backups</code>进行同步，同步完成的操作称为<code>Synced</code>。而在上面的第二种情况下操作会跳过<code>Unsynced</code>这一步。</p><p>但是从<code>Leader</code>视角看一个操作只要<code>Leader</code>认为是commutative的它就会应用到状态机，但这个操作可能会被<code>Witness</code>拒绝（认为是not commutative）。这时候需要依赖<code>Client</code>来发起sync请求，要求<code>Leader</code>与<code>Backups</code>进行同步。</p><h2 id="Leader-故障恢复"><a href="#Leader-故障恢复" class="headerlink" title="Leader 故障恢复"></a>Leader 故障恢复</h2><p>新 <code>Leader</code> 的产生/恢复有两步，首先同步 <code>Backup</code> 的状态，然后随便挑一个 <code>Witness</code> 来重放没同步到 <code>Backup</code> 中的操作。</p><p>同步<code>Backup</code>状态就获得了所有<code>Synced</code>的操作，而从上面的更新流程可以知道<code>Unsynced</code>的操作存在于<strong>全部</strong>的<code>Witnesses</code>中，所以这里随便挑一个 <code>Witness</code> 都有全部所需的信息。另外<code>Unsynced</code>操作在不同的<code>Witnesses</code>上所提交的顺序可能不一样，所以CURP要求只有满足commutative可以乱序执行的操作才可以推测执行。</p><p>另外还需要解决<code>Backup</code>和<code>Witness</code>可能存在重复记录的问题，如果将已经是<code>Synced</code>的操作再进行重放是会导致状态错误的。因此CURP需要保证每个操作只能执行一次，简单来说就是让<code>Client</code>给每个操作加上一个<code>RPC ID</code>。</p><blockquote><p>To avoid duplicate executions of the requests that are already replicated to backups, CURP relies on exactly-once semantics provided by RIFL, which detects already executed client requests and avoids their re-execution</p></blockquote><p>当然如果是一个 <code>Backup</code> 成为新 <code>Leader</code> 就可以跳过第一步。</p><h2 id="读取"><a href="#读取" class="headerlink" title="读取"></a>读取</h2><p><code>Client</code> 可以直接从 <code>Leader</code> 读，不过需要这次读操作也与其他 <code>Unsynced</code> 的操作无关，否则需要先 sync 一下，因为对同一个内容的读和写操作是无法乱序执行的。而在<code>Leader</code>视角看，推测执行的操作不一定在重启/变更Leader之后还存在（在<code>Leader</code>应用到状态机后，但是被<code>Witnesses</code>记录下来之前发生crash），所以读推测执行的内容也需要检查commutative。所以这种读操作与更新操作的区别只有(1)不涉及 <code>Witness</code> (2)不会影响 <code>Leader</code> 的状态机，因此不需要被记录。</p><p><code>Client</code> 也可以从 <code>Backup</code> 读，不过读之前需要先向一个 <code>Witness</code> 询问这个操作是否 commutative。只有在得到肯定答复的情况下才可以读 <code>Backup</code>，否则还是只能从 <code>Leader</code> 读。前面要求一个操作被记录到所有 <code>Witnesses</code> 也考虑到了这里能够随意请求 <code>Witness</code> 来询问 commutative。</p><p>同时因为<code>Backups</code>之间的状态可能因为同步进度不同而出现差异，为了从任意一个 <code>Backup</code> 都能读到同样的结果，<code>Backups</code> 之间还需要感知互相的同步状态(即一个操作是否被所有的 <code>Backups</code> 同步完成)。</p><blockquote><p>even if a value is replicated to some of backups, the value may get lost if the master crashes and a new master recovers from a backup that didn’t receive the new value. A simple solution for this problem is that backups don’t allow reading values that are not yet fully replicated to all backups.</p></blockquote><h2 id="GC"><a href="#GC" class="headerlink" title="GC"></a>GC</h2><p>这里讨论<code>Witness&#39; records</code>和<code>RPC ID</code>两个资源的回收。</p><p>从前面可知，<code>Witness</code> 只有在新记录与现在所有记录都无关的情况下才能 accept。所以为了提高成功率 <code>Witness</code> 必须尽快丢掉不必要的数据(被 <code>Leader</code> 同步到了 <code>Backups</code> 上，即synced的数据)。在 CURP 中 <code>Leader</code> 会给 <code>Witness</code> 发 GC 指令，告诉 <code>Witness</code> 哪些操作已经完成了同步并可以丢弃。</p><p>在 <code>Witness GC</code> 和更之前的故障恢复环节都需要对操作记录进行标识，CPUP 使用的是 <code>RIFL</code> 中的 <code>RPC ID</code> 来完成。因为这个 ID 是 Client 分配的，所以无法使用累进确认的方式而必须对每个 ID 都进行记录。此外 CURP 还对 <code>RIFL</code> 做了一些改动，在论文的 <em>附录§C.1</em> 节有展开，主要是为了适配 Witness 提供的 Recovery。</p><h1 id="进阶操作"><a href="#进阶操作" class="headerlink" title="进阶操作"></a>进阶操作</h1><p>从更新操作的步骤可以看到 <code>Client</code> 向 <code>Witnesses</code> 写数据以及 <code>Leader</code> 向 <code>Backups</code> 同步状态都是有全同步的要求的，即要求所有的请求对象都完成才能算完成，而后面的故障恢复和读取操作也都用到了这一点。</p><p>这样操作在网络分区或者单节点处理慢之类情况下的可用性和性能是比较让人担心的。论文在附录还展开了另一种方法，可以让 <code>Client</code> 向 <code>Witnesses</code> 写数据时只得到<em>超大多数(superquorum)</em> <code>Witnesses</code> 的答复就行。这里“超大多数”的计算方式是 $$superquorum=f+\lceil f/2 \rceil + 1$$，其中<code>f</code>是能容忍多少节点挂掉的数量。这里比传统多数派还多了一个“少数派的大多数”的原因是为了故障恢复时剩下的 <code>Witnesses</code> 之间还能保持多数派。</p><blockquote><p>The reason why CURP needs a superquorum instead of a simple majority is to ensure commutativity of replays from witnesses during recovery</p></blockquote><p>而相应的，<code>Leader</code>在恢复的时候以及<code>Client</code>在读<code>Backup</code>之前查询的时候都需要收集多数派<code>Witnesses</code>的结果。其实就是牺牲了一些读时的便利性来优化写的操作。</p><p>此外 <code>Leader</code> 向 <code>Backups</code> 同步数据也是类似，只要同步到多数派上，在恢复的时候选最新的。在 <code>Client</code> 读 <code>Backup</code> 的时候也选多数派和最新任期，类似 Raft 的那一套操作。</p><h1 id="一些证明"><a href="#一些证明" class="headerlink" title="一些证明"></a>一些证明</h1><p>论文在 <em>附录§A</em> 里面简单证明了一下 <code>Durability</code>, <code>Consistency</code> 和 <code>Linearizability</code> 几个关键特性，不过都是基于基本操作的版本（全同步写）。这里也洗一下稿。</p><p>先回顾一下几个操作时的规则：</p><ul><li><code>Client</code> 只有两种情况下可以完成一个操作：该操作被<strong>所有</strong> <code>Witnesses</code> 记录 <em>或</em> 该操作被<strong>所有</strong> <code>Backups</code> 持久化(即被 <code>Leader</code> 接收)<blockquote><p>from §3.2.1, a client only completes an update operation if (1) it is recorded in all f witnesses or (2) it is replicated to f backups.</p></blockquote></li><li>一个未同步(unsynced)的操作必须与所有其他未同步的操作无关(commutative)<blockquote><p>a completed unsynced operation must be individually commutative with all preceding operations that are not synced yet.</p></blockquote></li><li>如果 <code>Leader</code> 要答复一个不可交换(not commutative)的操作，需要先与 <code>Backup</code> 进行一次同步(sync)<blockquote><p>a master must sync before responding if the current operation is not commutative with any other existing (preceding) unsynced operations.</p></blockquote></li></ul><h2 id="Durability"><a href="#Durability" class="headerlink" title="Durability"></a>Durability</h2><p>所有的操作至少会存在于所有 <code>Backups</code> (synced) 或所有 <code>Witnesses</code> (unsynced) 上，所以恢复的时候找一个 <code>Backup</code> 加上一个 <code>Witness</code> 就能拿到所有的信息。</p><h2 id="Consistency"><a href="#Consistency" class="headerlink" title="Consistency"></a>Consistency</h2><p>synced 的操作存在在 Backup 上，并通过 ID 的方式防止重复执行。unsynced 的操作存在在 Witness 上，并且可以以任意顺序重放。</p><p>不过论文好像有一个内容没讲，就是 <code>Witness</code> 上记录的操作不一定最终会执行（比如<code>Client</code>发起sync之前发生crash），所以 <code>Witness</code> 是需要有机制去确认一个记录下来的操作是有效的。我觉得可以让 <code>Leader</code> 定期给 <code>Witness</code> 同步这个信息（比如将这个也作为 sync 的一步），同时 <code>Witness</code> 在恢复期间不提供未被确认的操作。</p><h2 id="Linearizability"><a href="#Linearizability" class="headerlink" title="Linearizability"></a>Linearizability</h2><p>还是从前面的规则推导。如果一个写入在只被部分 <code>Witness</code> 记录到的时候就crash，重启之后的 <code>Leader</code> 可能不会有这个信息，但是这时候 <code>Client</code> 也不会认为这个操作已完成，因为没收到所有 <code>Witness</code> 的答复。对于<code>Client</code>认为已经完成的操作再去读的时候 <code>Leader</code> 能保证线性一致。读 <code>Backup</code> 的流程论文的图4也稍微涉及到了线性一致的几种情况。</p><p>不过这里只是文字论述，也没有什么并法内容，作为证明感觉还是有点单薄，但是也想不到什么反面例子……</p><h1 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h1><p>这篇文章省略了论文中的一些内容，比如一些实现上的经验细节，分区的处理等等。看下来觉得基本思想还好理解，论文说这是一个通用的优化手段，但是怎么变成一个真正的优化感觉还是要在实现的时候多做些微操……</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;论文名字是《Exploiting Commutativity For Practical Fast Replication》&lt;/p&gt;
&lt;p&gt;CURP全称Consistent Unordered Replication Protocol，主要是利用操作之间的相关性（Commu</summary>
      
    
    
    
    
    <category term="Paper Reading" scheme="https://waynexia.github.io/tags/Paper-Reading/"/>
    
  </entry>
  
  <entry>
    <title>CeresDB 的单线程模型实践 (Rust China Conf 2022)</title>
    <link href="https://waynexia.github.io/2022/06/tpc-practice/"/>
    <id>https://waynexia.github.io/2022/06/tpc-practice/</id>
    <published>2022-06-16T12:46:10.000Z</published>
    <updated>2026-01-14T13:19:51.714Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>这篇是从 Rust China Conf 2022 的 talk 整理来的（校对听写转录稿好麻烦……</p></blockquote><p><strong>（还没校对完）</strong></p><p>介绍一下单线程模式两个在生产中的例子，<a target="_blank" rel="noopener" href="https://github.com/waynexia/helixdb">HelixDB</a> 是一个Thread Per Core模型的KV存储，<a target="_blank" rel="noopener" href="https://github.com/ceresdb/ceresdb">CeresDB</a> 中也有部分逻辑在单线程模式下工作。</p><h1 id="Examples"><a href="#Examples" class="headerlink" title="Examples"></a>Examples</h1><div class="heti heti--annotation">首先看一下CeresDB中的一个例子，在CeresDB中所有的写请求都由Write Worker组件来进行实际处理，上层的Insert之类的请求最后会转化为对数据的修改请求，并交给Write Worker执行，比如说插入数据或者更改表结构等等。同时也有一些后台处理归档的工作，比如说<ruby>压实<rt>compact</rt></ruby>或者<ruby>刷写<rt>flush</rt></ruby>。</div><p>对以一张表来说，写操作本身就有互斥的属性，所以我们以表为单位，将每张表对应到一个Write Worker，一个Write Worker上可能会负责多个表，是一个一对多的关系，这张表的所有写操作都由对应的Worker来完成。默认是按照核数来初始化Write Worker，每个Write Worker之间的负载和资源都进行了隔离。包括Worker本身会独占一个线程，以及Worker所负责到的那些表涉及到的资源在逻辑上也是不共享的。Write Worker的主逻辑实际上就是不断地从一个channel中接收任务并执行。</p><p>这样的操作能带来两个好处，首先对于每张表来说，后台的写入操作是串行化的，所以做一些简单的逻辑就能够避免写写冲突的问题，能够简化状态管理的代码。同时Write Worker将表以及表后所隐藏的资源都预先分配并独占，可以去减少资源竞争的情况。</p><p>当然这个完全串行化是一个比较简单的场景，实际上还会有一些复杂的逻辑，比如说一些不关心写入顺序的操作就可以detach到后台来执行，忽略它与其他前台操作之间的顺序等。</p><p>刚刚所说在目前我们大多数情况底下，虽然在逻辑上做了区分，但底下还是同一个物理资源，比如说同一块磁盘、同一块网卡之类的。在逻辑上独占，独占的可能是一个文件路径或者是一个SQL的链接，通过这个抽象能够减少上层资源互斥的逻辑，并且在物理资源拓展的时候能够很方便的Scale out，比如说简单的加磁盘、加网卡就行，因为已经做了逻辑资源的隔离，所以能够比较方便地去进行拓展。</p><p>Image</p><p>这是单线程模型在CeresDB中的一个应用场景，作为一个通用的手段，我简单总结了几点它能够带来的好处：</p><ul><li>首先在这种模式下，所有的状态只会在一个线程内被访问和修改，在编码的时候可以简化状态管理的逻辑。资源也是一样，各个资源在逻辑层面独立，能够减少资源的竞争开销。</li><li>充分利用了Rust的协程特性，其实就是利用async/await来减少线程的上下文切换。可能之前是有一堆线程，并且线程数量是大于核数。操作系统就会对我们的线程进行调度，分时调度到不同的核上来执行。我们现在如果把所有的任务都已经预先分配到线程里面，就可以将线程的切换简化为一个协程的切换。并且由更理解具体使用情况的我们自己来代替操作系统进行调度。</li><li>如果能够再进一步将这些工作线程与核心绑定起来，形成Thread Per Core的模式，还可以进一步提高CPU的亲和性以及Cache的命中率等。</li></ul><p>Image</p><h1 id="Pros-amp-Cons"><a href="#Pros-amp-Cons" class="headerlink" title="Pros. &amp; Cons."></a>Pros. &amp; Cons.</h1><p>不过需要注意的是，这里所提到的各种方法都有适用场景，接下来详细讨论一下关于这种方式具体的优势和局限性。</p><h2 id="It-gives"><a href="#It-gives" class="headerlink" title="It gives"></a>It gives</h2><p>接下来详细介绍一下它能带来的几点优势，首先是调度模型方面，从之前提的由操作系统进行的抢占式调度，变为了各个协程之间进行的协作式调度。</p><p>来对比一下，左边是一个抢占式调度的一种常见的情况。产生这种情况其实就是我们把若干个任务同时放进一个线程去执行，可能这些任务之间是会涉及到同一个状态或者逻辑资源，但是这些任务是互斥的。</p><p>比如说这里有两个线程同时在执行两个任务，而这两个任务都是涉及到同一个资源，假设两个任务分别叫x和y。它们可能会被调度到一个核上来执行，在这里可能先执行第一个线程，执行了两句，然后操作系统把第二个线程调度到这个核上，把第一个线程调度走，第二个线程就执行了两句，这样交替执行。从一个线程来说，它感知到的是自己是一直在执行的，但是在写完y=1然后去读y的时候，却会发现y变了。所以在这种模式下其实是需要一个数据同步的，可能是锁或者原子变量之类的手段，来保护好状态和变更。</p><div class="heti heti--annotation">而在右边，这两个任务会被分配到同一个线程内。即涉及到同一个资源的会被分配到一起。如果两个任务运行在同一个线程，就算这个线程本身被操作系统调度走，对任务来说它（任务）也是一直运行的。因为整个线程被调度走，这个线程上的其他任务也无法执行。这时候虽然操作系统还是能够对线程进行调度，但逻辑上的调度权实际上就是由<ruby>执行器<rt>executor</rt></ruby>任务之间来进行一个协作式调度。而有这个基础之后还能够进一步地限制操作系统的调度，尽可能多地把调度权转移到应用层来。</div><p>而协作式调度则是由各个任务主动交出执行权，比如说这里如果Task1不想被调度走，那它可以选择不交出自己的执行权，而把自己四条命令全部执行完，然后再由Task2来执行，这样就能够有一个确定性的执行模式。Task1能够确定自己在写y和读y之间，这个y是不会发生变化的。这样能够减少简化我们的状态管理，不需要去锁上一个临界区来确保变量或者资源地独占访问之类的，因为这一点已经在我们模型层面已经保障。另外也能对系统的延时性能有一定提升。</p><p>Image</p><p>在这个条件下，更进一步可以想到其实不再需要原子操作了，或者换句话说来说，所有的操作都是原子的，可以不受限于硬件的限制，对任意多的数据进行“原子”操作，因为只要不主动交出执行权，那这个操作就可以一直以一种确定的顺序执行。</p><p>这一点带来最直观的变化就是我们<ruby>工具结构<rt>utils</rt></ruby>的变化，比如说常用的<code>Arc</code>(Atomic Reference Count)可能就变成<code>Rc</code>(primitive Reference Count)，就是不需要原子操作的引用计数。同时还有一类大量运用了原子操作的lock free这种结构，也不需要掉头发去写这种东西了，用普通的单线程结构就能够完成。还能够保证这个结构、这个状态在同一时刻不会有其他人来访问。</p><p>不过也不是说完全能够去掉lock free的结构或者是lock，毕竟状态之间以及任务之间还是需要进行状态同步的，完全抹掉所有的共享状态是非常困难的，在两个实践中都是选择系统的一部分来实现这个模型，算是一个工程上的取舍。</p><p>Image</p><p>除了不需要原子操作，还有一个特征就是没有<code>Send</code>这个auto trait，可以看到在标准库里面是显示的给<code>Rc</code>实现了一个<code>!Send</code>。</p><p>在接下来讲之前，先回忆一下Rust中关于<code>Send</code>的定义，简单来说就是用来表示一个结构能否安全地被多个线程所持有。这个trait非常常见，大部分地方都能见到，或者自己定义自己的trait的时候，也给它加上了<code>Send</code>，加上<code>Sync</code>，或者加上<code>&#39;static</code>这种，先加上再说的这些auto trait，以及遇事不决，用<code>Arc&lt;Mutex&lt;T&gt;&gt;</code>来堵住编译器关于生命周期、Send之类的报错的做法。</p><p>而且目前很多的基础设施都是在<code>Send</code>这一条件被满足的情况下来实现的，比如可能我们看看自己的代码，可能大部分都要求了<code>Send</code>，或者是像pub fn spawn这个方法也是对spawn进行了future以及future的结果，也要求了一个<code>Send</code>。</p><p>但是在我们现在所讨论的这个模型中，可能<code>Send</code>就不是那么常见了，因为我们大部分的工作都是在一个线程中完成，没有这样一个结构或者是一个任务发送到多个线程的需求，自然就不需要<code>Send</code>这个约束，因为我们在一开始就不会在多个线程中共享它们。</p><p>那么少了这个约束之后，不仅是常用的工具类可能会发生变化，还有他们背后所隐含的编程习惯可能也会不同。</p><p>Image</p><p>最后一点就是获取可见性的开销会减少。比如之前可能常见的是用<code>Mutex</code>或者<code>RwLock</code>，需要处理多个资源可能被同时访问的情况。但在这里可以从结构上避免这种情况的出现，只需要去获得语义上的一个内部可变性就行了。理论上是可以把所有额外的运行时检查都去掉，同时还能够保证代码的安全。</p><p>另外就是所有的工作负载从开始到完成都在同一个worker thread中，这个worker thread包含所有的上下文，从这一点出发来方便地实现任务调度和资源控制等功能。</p><p>Image</p><p>前面这么多虽然，最后还有一个但是：虽然同时只会有一个任务在进行，不存在并发状态的修改，但是还是不能够完全丢掉锁，因为在不同的任务之间还是需要同步状态。为了性能通常会选择将任务进行穿插进行，比如说任务A在等IO的时候可以交出所有权，它去后台等IO，让任务B的计算先开始。这个时候我们可能有些操作才执行到了一半，所以要通过锁的机制来告诉别的任务，这个资源现在不能够被进行访问/修改。</p><p>不过同样是锁，单线程下的锁会稍微简单一些，不需要条件变量之类的手段，比如说最朴素的就是自旋锁（现在你也许是知道你在<a target="_blank" rel="noopener" href="https://www.realworldtech.com/forum/?threadid=189711&curpostid=189723">干什么</a>的）。不过最好还是和Runtime相结合来干涉Runtime的调度（的确能在user-land能做到这些！）。</p><p>另外一点需要注意的就是这个工作线程中不能够有任何的blocking行为，虽然在平常的异步中也是需要注意的一点。但是同样是执行blocking操作，在一共只有一个线程的情况下把这个线程block住带来的后果会比平常更麻烦一些。多线程的情况下其他的任务可以被调度到别的线程上去执行，但是在这里这个线程关联的所有任务都将无法进行。</p><p>那是不是这种情况下就完全不能进行阻塞式的IO了呢？如果是纯异步的IO当然是没有问题的。如果不是的话，一个常见的办法就是开一个或者多个线程来专门执行这些blocking的操作，主线程把这些操作移交出去（某种程度上和模拟异步IO差不多）来保证自己仍然是异步的，考虑到环境与环境不能一概而论，在只能使用阻塞式IO的时候这个手段就是必要的。</p><p>Image</p><h2 id="It-takes"><a href="#It-takes" class="headerlink" title="It takes"></a>It takes</h2><p>好处讲了这么多，那么代价是什么呢？</p><p>这里列了两点我认为比较重要的代价，一个是传染性，另外一个是做强制分片的需求。首先说传染性，这里指的是Send这个trait的传染性，我刚刚所说整个系统变为单线程的模型改动会比较大，但是如果只改一部分的话也会有问题。(todo: mesos)</p><p>我们先来回忆一下Rust中是怎么处理auto trait的，这里就以Send为例，如果一个结构所有的field都是Send的，编译器就会自动给这个结构也推导成Send。反过来如果这个结构中有一个或者是多个field，它是Unsend的，就它没有实现Send，那编译器就会把这个Unsend推导到这个结构上。如果一个结构中任何一个地方是Unsend，那这个结构就会被推导为Unsend，这个结构也可以是Rust自动生成的future，通过async语法来生成的future，它本身也是一个匿名的结构体。</p><p>如果是这样，Unsend可能就会随着这种函数调用扩散到整个系统。但这显然是不可接受的，因为这就强制要求我们把整个系统改造成够接纳Unsend。这种时候为了防止把Unsend扩散得到处都是，我们可以让两部分通过channel来交流，把之前的显示函数调用包装成一个个task或者是request，就类似于模拟Rpc的感觉，通过这种方法来构建一个Unsend Boundary，把两部分分离开，从而避免这个问题。</p><p>Image</p><p>我们CeresDB中的Write，就刚才说的Write，也是通过上层所封装好的Write request来执行操作，上层是把自己所收到的写请求包装成一个request，然后通过channel发送给底下对应的Worker，然后这个Worker执行完再把结果发送回去，这样就避免了一个显示函数的调用，也就避免了这个类型扩散到其他的部分，而是只把它局限在我们的Worker中。</p><p>Image</p><p>另外一个问题就是强制分区，因为我们希望各个线程之间尽量减少交流来减少额外开销，所以能够最好就是预先将工作负载和资源都进行划分，比如CeresDB是按照表来进行partition，可能别的系统还按照ID或 Key range之类的。对一些系统来说可能分区是比较简单的，但是对于另外一部分系统，它可能就比较难以找到一个合适的分区方式，那我们这种单线程的编程模型或者是Thread Per Core不太适合。</p><p>Image</p><p>分区也要注意粒度的问题，最好能够保证每个分区之间不会相差太多，或者是每个分区元素的力度也不会相差太大，涉及到分区的系统通常都会遇到分区负载不均的情况，所以为了能够灵活地调度，能够根据负荷来动态地调整partition，就要求每个分区的单元不会太大。</p><p>另外，还要求本身能够执行这个Repartition，这个其实大部分是工程上的问题，对于线程本身来说，它所代表的是一个计算资源，是无状态的，所以可以很方便地进行划分，能很方便地把某条指令在这个线程或者这个核上执行，或者``把它调度到另外一个线程或者另外一个核上执行，这个是比较方便的。</p><p>但是一个任务通常背后还对应了一些状态和资源，这些相较于任务本身是更难以调度的，特别是如果涉及到持久化的状态，比如把什么东西写到磁盘上，那我们这个下在这个磁盘，另外一个partition在另外一个磁盘，这个时候可能会要有一个比较复杂的逻辑。同时上层的分组路由也要保证路由的正确性。</p><h1 id="How-to"><a href="#How-to" class="headerlink" title="How to"></a>How to</h1><p>那么最后假如场景也很适合，想体验一下它带来船新体验和性能提升，那要怎么样开始开发呢？First of这个要求纯异步的代码，所以离不开<code>async</code>/<code>.await</code>。</p><p>先看一下最重要的Runtime，目前一些常用的Runtime都有提供不要求<code>Send</code>的接口，比如说tokio和futures，都有spawn local的方法，可以看到和刚刚的spawn的区别就是我们这里函数签名上不再要求Send这个bound了。</p><p>另外也有一些专门为Thread Per Core模型设计的运行时，比如说glommio和monoio，不过这两个除了Thread Per Core之外还绑定了io_uring作为IO接口，虽然非常合理，因为我们本身就是最好是能够和异步IO相结合使用，但是也让它没有那么灵活，因为作为一个还比较新的系统特性，可能很多存量的服务器系统版本还没有升级，可能就会需要用到我们刚刚提到的那些手段去做一个适配。</p><p>在我们的实践中，也有遇到这个问题，为了能够兼容非阻塞的IO接口，需要用到前面提到Blocking的线程方式，可能还要再做一些hack来适配，把它迁移到常用的Runtime上。</p><p>还有标准库中的一些工具类也能够派上用场，比如用TLS代替一些全局变量，它们也属于被分开的“状态”。在刚刚提到我们使用tokio来模拟Thread Per Core Runtime的时候，也有使用到TLS，就是把每个线程拥有一个正常的tokio Runtime，然后把它放在TLS里面来模拟一个Thread Per Core Runtime。</p><p>此外还有Cell类，主要就是用来获取内部可变性的，用来代替锁，在无序竞争的场景下一个比较开销更小的内部可变性的获取。</p><p>不过这里RefCell其实还涉及到一个动态运行时的检查，就是它会检查我这个是否有其他的也同时持有这个可变性，有的话它就会导致panic。事实上在这个模型中，就像刚才提到的一样，是能够在设计的时候就完全规避掉这个运行时的检查的，所以我们理论上RefCell还可以更进一步提供一个保证。</p><p>除了这些，还有一些平时的异步编码的注意事项也会变得很重要，比如前面提到的Blocking操作，或者Clippy中的一些Lint。比如Clippy中这个Lint，我们可能会用RefCell来大量代替锁，所以需要注意await的时机，以这个fn函数为例，比如我们在第一行获取了x的可变引用，但是在第二行把它await走，这个await相当于交出了我们这个函数的执行权，这个时候是能够把其他的任务调度过来的。如果有第二个fn函数在这里进入了，它在尝试去获取x的可变引用，这个时候就相当于一个可变引用被两个函数所持有，这里就会导致一个panic。所以我们需要注意await的时机，await其实就是隐式地会交出执行权。</p><p>除了这些，还有很多平常异步编码的时候那些注意事项，在这里可能就是会变得更加重要，因为违背这些事项，可能会带来一些更加严重的后果。就比如说刚刚的Blocking，或者是这里的await holding refcell。</p><p>虽然目前已经有很多基础的方式能够让我们相对完整地实现这一个特性，或者说这一个模型，但是我们还是可以从生态或者是基础库的角度做得更好。比如说在设计的时候就考虑能够保证独占访问这一条件的基础类，比如说从channel到Runtime等等，还能够更进一步地压榨性能，以及现在可能到处都要求的Send，那是否在设计的时候我们再加上Send这个trait bound的时候，想一想这里是否真的需要。</p><p>如果是一些简单的场景在目前的生态中已经能够基于TPC模型构建一个基本完整的应用了，但是想要轻松上手以及最大地发挥优势还需要一些发展时间。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;这篇是从 Rust China Conf 2022 的 talk 整理来的（校对听写转录稿好麻烦……&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;（还没校对完）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;介绍一下单线程模式两个在生产中的例子，&lt;</summary>
      
    
    
    
    
    <category term="Rust" scheme="https://waynexia.github.io/tags/Rust/"/>
    
  </entry>
  
  <entry>
    <title>reMarkable2 使用体验</title>
    <link href="https://waynexia.github.io/2022/05/remarkable2/"/>
    <id>https://waynexia.github.io/2022/05/remarkable2/</id>
    <published>2022-05-04T01:37:44.000Z</published>
    <updated>2026-01-14T13:19:51.708Z</updated>
    
    <content type="html"><![CDATA[<p><em>题图来自<a href="remarkable.com">remarkable.com</a></em></p><h1 id="又一次广告投放"><a href="#又一次广告投放" class="headerlink" title="又一次广告投放"></a>又一次广告投放</h1><p>知道reMarkable2是在某天刷推的时候看见了他们投放的广告，看了两天之后就剁手了，非常冲动。之前怀疑广告是不是真的那么有用，最后没想到电视购物的竟是我自己。</p><p>一直觉得看PDF很不爽。手上的设备有太小的kindle，太重的surface和没感觉的电脑屏幕。一直有点想再整一个，但是也一直没特别想下手。dpt-rp1看起来确实也很不错，不一定要墨水屏所以pad也在考虑范围。这种情况下可能接下来应该说的是reMarkable2为什么不一样，可惜的是我也不知道为什么。它的几个亮点比如薄和手写体验对我都只算“有了更好，没有也无所谓”的范畴，只能说是广告投放的成功吧。</p><h1 id="槽点"><a href="#槽点" class="headerlink" title="槽点"></a>槽点</h1><p>因为入手的时候对它的期望更偏向于一个阅读器，开箱用了几天就有好多想吐槽的地方……</p><ul><li><p>翻盖死贵但是功能非常少。<br>我觉得这个盖子就是一个带两块磁铁的壳，完全不能算是电子配件。机身侧面留有几个金属触点，但是这个壳对应的位置居然什么都没有。而且壳本身封面有磁吸，但是机器不能响应翻盖动作……让按键焦虑的人非常难受。</p></li><li><p>没有背光灯。<br>两代都没有，不知道有什么特别的原因。在我的桌面环境上只有一个屏幕灯，所以当晚上靠在椅子上不开额外光源基本不能看。经过了一个阴天，室内光线稍微暗一点就要开顶灯使用了。</p></li><li><p>屏幕分辨率低。<br>这个主推的是像真纸一样的书写体验，但是谁的纸能够……写出锯齿？设备第一次开机的第一个环节就是让你拿笔体验一下，结果我一划拉出来一条锯齿，当场蚌埠住了。</p></li><li><p>软件（固件）体验很差，这个分两方面。</p><ul><li>首先我觉得这个系统（v2.12.3）的基本功能是缺失的，比如笔居然不支持调节压感灵敏度，导致我在习惯的力道下只有一种笔触能使用;而且你甚至不能在PDF上选择文字，such a REMARKABLE tablet。</li><li>以及不那么基本的功能也是缺失的（对于不想每个月再多掏58.88 hkd且不想折腾用户来说）。邮件传输、三方存储和屏幕分享这些功能都被<em>remarkable.com</em>列为了付费订阅内容。</li></ul></li></ul><h1 id="还不错的地方"><a href="#还不错的地方" class="headerlink" title="还不错的地方"></a>还不错的地方</h1><p>主要使用场景是拿来看PDF，比起我的voyage确实大了不少，屏幕小是voyage看不来的唯一因素。但是实际拿A4纸比了下才发现rm2也只仅仅比A5尺寸稍宽一些，但是裁掉页面边缘的留白之后也能够很舒服地展示一页纸的内容了，当然字还是变小了一些。</p>    <figure class="figure-image">      <img src="compare.jpg" alt="和kindle voyage以及A5纸的对比。基本只比A5宽几毫米" width="100%" height="100% loading="lazy" />      <figcaption>和kindle voyage以及A5纸的对比。基本只比A5宽几毫米</figcaption>    </figure>  <p>在这之前想要在kindle上看纸舒服一点，尝试过各种方式试图重排PDF来适应kindle，也许简单地裁一下加上横屏就能让体验提升不少。</p><p>拿着笔的时候会自然地想写写画画。写得太丑就不展示了，画就像下面这样，非常艺术。仔细研究了一下为什么在板子上面写字就这么难看，最后得出的结论是板子的问题<del>，真的</del>。因为屏幕有一定厚度，笔尖接触到的位置和设备画出笔触的位置之间存在一个垂直上的距离。如果眼睛看板子的时候与板平面法线存在一个角度，就会在视觉上带来一个笔尖和笔触的偏差量，感觉最明显的是画画的时候你很难让两条线完美地连接起来，因为你觉得你应该落笔的地方并不是带电碳粉出来的位置。甚至在Reddit上看到一个advice，如果你是右撇子的话可以尝试将设备的偏好设置调到左撇子，这样系统对笔触进行的修正能够减轻这个偏移现象。</p>    <figure class="figure-image">      <img src="doraemon.png" alt="画" width="100%" height="100% loading="lazy" />      <figcaption>画</figcaption>    </figure>  <p>而我的对于为什么写字会变丑的结论是，在我写字的时候手会尝试去弥补这个偏差量从而导致写出来的东西变形<del>（逐渐远离小标题）</del>。</p><p>因为画画技能实在太差，不太好说真拿这个当画板会是什么体验，不过还是要提一下用了一段时间感觉到的这个设备在软件方面难得做了的一件提升体验的功能。在用笔编辑的时候可以使用图层功能，比如上面的Doraemon和Nobita就是先画好眼眶，再拉到另一个图层去涂眼珠子，可以防止用橡皮擦的时候把其他已经画好的地方擦掉。</p><p>不过在导出到别的设备上的时候会发现笔迹有些奇怪，有点喷粉颜料的感觉。就是从瓶子里面喷出来是粉末，拿火加热一下就会融化成颜料镀膜的那种。在设备上看起来只是比较浅的笔迹，导出来之后会变得奇怪地圆润起来。</p><h1 id="Workflow"><a href="#Workflow" class="headerlink" title="Workflow"></a>Workflow</h1><p>如果是看纸的话就边看边写写画画，不过由于手写识别差不多就是只有英文，所以得要赛博誊写一下。从白纸开始的草稿就当作是写在纸上了，最后还是拿画图板重新画一次（当然手绘的东西也太难看而不能直接拿出来用）。拿这个写写画画的时候体验还行，不同的事情单独开不一样笔记本，可以避免过了一段时间望着一堆纸头痛，现在桌面基本可以不用放a4纸了。</p><p>也许因为connect是他们后面加的功能，所以在21年10月之前买的可以获得无限的free connect权限。但是他认证购买信息的方式非常原始，是让你提供订单邮件的订单号他们来人工确认，于是找当时购买的店家要了个老订单通过了验证（店家直接给了我一个两年前的订单，邮件上面写的是第四批货……）。而connect解锁的功能我觉得比较有用的就几个：文件同步、邮件发送以及三方存储集成。如果没有connect订阅的话超过五十天没有打开的文件就永远不会同步，这显然是有些影响使用了;而邮件发送和三方存储都能够比较方便地导入导出阅读材料和涂鸦。不过如果完全没有connect的话也能通过usb和电脑连接来有线传输，所以备份起来也还行。</p><h1 id="reHackable"><a href="#reHackable" class="headerlink" title="reHackable"></a>reHackable</h1><p>reMarkable的系统是一个叫做<code>Codex</code>的linux系统，通过usb连接上就会开放一个remote terminal给你，可能是比较开放所以有许多三方创作的各种工具来增强使用体验，<a target="_blank" rel="noopener" href="https://github.com/reHackable/awesome-reMarkable">Awesome reMarkable</a>是一个awesome列表，可以找到许多有趣的东西。比如拿来拉小清单的<a target="_blank" rel="noopener" href="https://recalendar.me/">recalender</a>，以及看起来可以完全自建connect服务的<a target="_blank" rel="noopener" href="https://github.com/ddvk/rmfakecloud">rmfakecloud</a>，甚至还有一个包管理器<a target="_blank" rel="noopener" href="https://toltec-dev.org/">Toltec</a>。活跃的hack带来了跟官方克扣feature完全不一样的体验，虽然没有实际用上几个但是看着很舒服……</p><h1 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h1><p>最后还是想强调一下看PDF的需求体验最好也最便宜的方案还是打印出来。<del>唯一只有电子阅读器能够带来的是和阅读无关的花钱的快感</del></p><p>最近也在商场闲逛的时候体验了一些其他品牌的墨水屏，感觉reMarkable的价格还是太没优势了。用差不多一半的钱就能买到差不多的书写体验以及也许会是加分项的网络和同步体验，如果再让我选一次可能还是会重新纠结一下的。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;em&gt;题图来自&lt;a href=&quot;remarkable.com&quot;&gt;remarkable.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h1 id=&quot;又一次广告投放&quot;&gt;&lt;a href=&quot;#又一次广告投放&quot; class=&quot;headerlink&quot; title=&quot;又一次广告投放&quot;&gt;&lt;/a&gt;又</summary>
      
    
    
    
    
    <category term="Experience" scheme="https://waynexia.github.io/tags/Experience/"/>
    
    <category term="Device" scheme="https://waynexia.github.io/tags/Device/"/>
    
  </entry>
  
  <entry>
    <title>深海迷航：冰点之下</title>
    <link href="https://waynexia.github.io/2022/03/subnautica-below-zero/"/>
    <id>https://waynexia.github.io/2022/03/subnautica-below-zero/</id>
    <published>2022-03-11T17:31:45.000Z</published>
    <updated>2026-01-14T13:19:51.711Z</updated>
    
    <content type="html"><![CDATA[<p>这是系列第二作。但是从一些之前的资料来看一开始冰点之下是作为第一部的 DLC 的身份面世的，不过我在 xbox 商店看到的时候它已经和《深海迷航》分开成两个条目了，也许是做着做着就拿出来单独售卖了吧。不过可能是这个原因，冰点之下大部分的元素基本和之前一模一样，画面和操作都是熟悉的味道<del>（没有说偷懒的意思）</del>。</p><p>这个系列简单来应该算以探索海洋为主题的生存游戏。两次都是在一片被海洋覆盖的星球上（背景里面好像是同一颗星球），从坠落时的一个小逃生舱开始，在探索周围环境的过程中获取生存物资与解锁科技，并通过不断地扩大探索范围来推进剧情。觉得这个系列最有特点的地方就是再海洋为主场景的设定下带来的立体探索的体验。</p><p>这一次故事发生在另一张地图，不过前作其实没有推完的我并不记得具体的地图是什么样子，真正让我感到地图换了的是场景元素。前作从逃生舱出来之后在周围转悠了很久才进行第一次远行，还被辐射警告给吓了回来。然而这作不仅没有辐射警告，甚至还能够徒手开铀矿。不过这是后面的内容，冰点之下第一幕是从冰川开始的，在我的印象中这是前作没有的要素。</p>    <figure class="figure-image">      <img src="map.png" alt="本作地图，白色是新增的陆上冰川部分，不过一大半都是去不了的" width="30%" height="100% loading="lazy" />      <figcaption>本作地图，白色是新增的陆上冰川部分，不过一大半都是去不了的</figcaption>    </figure>  <p>第一作进度应该不到三分之一，甚至都没见到利维坦。当时是在宿舍里面，深夜两点中我还在收集物资。屏幕里外都是一个人，内心一直有萦绕不散恐惧感。我记得那时的日记好像写了“这个游戏玩出了深海恐惧症”之类的话，再玩了几个小时就顶不住弃了。这个现象在这一次还是没有好转，当自己又一次被丢到海里，周围只有三四立方米的安全空间时，这种熟悉的感觉又回来了。由于地图场景非常复杂，在全身贯注地贴着海底移动寻找物资的时候留意氧气表就已经很紧张了，经常无暇留意周围潜藏着的其他生物。在前期还没有载具装备能稍微增加一些安全感的时候好几次被突如其来的袭击吓到。而就算有了载具之后面对未知生物时仍然难以克服恐惧，总是小心翼翼地避免和它们接触，想去的地方有奇怪生物在徘徊时经常等到肚子饿。这时候想想祖先走出了丛林真是太好了，能经常把基本的生存当作一件理所当然的事情真实太好了。</p><p>而伴着这种感觉在海洋中游弋的过程也许是游戏里最让我喜欢的地方。和在游泳馆游泳不同，游戏画面并没有渲染密铺的小瓷砖或者被波纹打散到地上的光线，而是通过立体的生态环境来给人在水中的感觉。而且是一片非常广阔，地面崎岖，没有同类但有许多其他物种的水。</p>    <figure class="figure-image">      <img src="environment.png" alt="前期的迷路点" width="100%" height="100% loading="lazy" />      <figcaption>前期的迷路点</figcaption>    </figure>  <p>然而在写本篇的时候觉得这次又要弃掉了。找实况录像参考了下，这次剧情应该是基本快推完了，不过连着推了几个晚上觉得脑袋已经不行了。到了后期基本上所有地方都能够去，找东西环节就变得非常麻烦。海洋的部分随着深度下潜地形变得越来越绕，就算给了信标也要去找攻略才知道怎么走，而陆地部分一言不合就来极端天气把能见度降到伸手不见五指，还有重新回来的强大引力也直接把机动能力摘掉了一个纵轴。迷路致死的次数越来越多，以至于现在一回想起来找路时在天旋地转的屏幕都会头晕。当然这个路痴可能是我的问题，就算我在这里查攻略看路线的次数已经比 XB2 还多了（</p><p>这一次大型怪物的掉 san 程度已经比前作好了很多，不过还是不太能适应。对我来说海洋主题的冒险大概要 ABZU 那种程度的才会觉得舒服吧。</p>    <figure class="figure-image">      <img src="fish.png" alt="已经和蔼多了的生物" width="50%" height="100% loading="lazy" />      <figcaption>已经和蔼多了的生物</figcaption>    </figure>  <p>对于新加的冰原场景并不是很喜欢，丢掉了立体探索的特色，和其他的作品没有什么区别了，而且就算是在同一个作品里面也觉得冰原的生物不如海洋有意思（可能各种奇奇怪怪的陆地生物平时看得也不少了）。还有陆地上的载具真是太催吐了，不知道是不是打开方式有问题，总之造出来之后就没碰过几回，宁愿走路都不想开。除此之外还有些其他影响体验的细节，感觉和其他几个接触了一段时间的以 steam alpha 模式开发的作品类似，厂家<del>小作坊</del>做了新奇的核心玩法，但是不知道如何更进一步地完善并继续挖掘丰富的内容。最后给我的感觉就是开始很新奇很上头，但是玩着完着就总是感觉差了些东西。</p><p>最后总结的话就是<del>抛掉不好的体验来说体验还是可以的</del>留一个在海中冒险的模糊印象是不错的，但是不太好的游玩体验让我不太想继续操作下去完成后面的剧情。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;这是系列第二作。但是从一些之前的资料来看一开始冰点之下是作为第一部的 DLC 的身份面世的，不过我在 xbox 商店看到的时候它已经和《深海迷航》分开成两个条目了，也许是做着做着就拿出来单独售卖了吧。不过可能是这个原因，冰点之下大部分的元素基本和之前一模一样，画面和操作都是</summary>
      
    
    
    
    
    <category term="Experience" scheme="https://waynexia.github.io/tags/Experience/"/>
    
    <category term="Game" scheme="https://waynexia.github.io/tags/Game/"/>
    
  </entry>
  
  <entry>
    <title>杂技：被 CGO 玩转</title>
    <link href="https://waynexia.github.io/2021/12/embed-rust-lib-into-go/"/>
    <id>https://waynexia.github.io/2021/12/embed-rust-lib-into-go/</id>
    <published>2021-12-28T19:58:27.000Z</published>
    <updated>2026-01-14T13:19:51.707Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>故事发生在蚂蚁一个内部项目上，这个项目有 Go 和 Rust 两个部分，其中 Rust 库作为一个存储组件被 Go 的业务部分依赖着。出于种种原因，两个部分需要作为同一个进程来运行，中间有一层 C FFI 接口作为理想与现实的桥梁。大概是这个样子的</p><p><img src="/covers/embed-rust-lib-into-go.png" alt="img"></p><p>Rust 部分首先将使用到的 Library 的接口和结构使用另一个小小的 <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/reference/linkage.html">cdylib</a> shim 项目封装一下，并通过这个项目生成 Rust 库的编译产物、一个动态链接对象和一份 C 的接口头文件。再基于 C header 写一个 Go 的 SDK 给上层使用。单独左边的 Go 或者右边的 Rust 项目都不够刺激，连在一起就很得劲了。这里主要集中在中间那一团麻花部分上，也是 too young too simple 掉了很多头发的地方。</p><p>大概会从线程、内存和信号三个方面讲几个事故。</p><h1 id="线程"><a href="#线程" class="headerlink" title="线程"></a>线程</h1><p>刚开始的时候 shim 和 SDK 都非常简单，SDK 每个请求都通过 CGO 走到 shim，shim 在进行一些结构的转换后调用 library 来处理请求，等待请求完成之后把结果返回给 SDK。比较符合直觉的流程，但是在测试的时候出现了问题，整个程序的行为都不太正常。当时刚开始连调，在这之前 component 和 library/shim 都只单独进行了测试，当然单独测试的时候大家都是一切正常，所以连调起来场景就变成了…</p><p><img src="1.png" alt="img"></p><div class="heti heti--annotation">虽然很快就怀疑到了 SDK 和 shim 部分，但是也不清楚问题到底出在哪里。直到翻日志的时候发现了一条原本只应该在启动的时候输出一次的日志出现了复数次，才意识到不对劲。（又）出于一些原因，shim 和 library 中都存在有 <a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Thread-local_storage">TLS</a>(<ruby> thread local storage<rt>线程局部存储</rt></ruby>)结构，比如在 Rust std 中的一种<a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/macro.thread_local.html">实现</a>。它会在每个线程第一次使用这个结构的时候进行初始化，之后同一个线程都会一直访问到这个对象，而不同的线程之间访问到的是不同的对象。而那条重复出现的日志就是一个 TLS 结构在初始化时输出的，也就是说 TLS 不符合预期地被重复初始化了。</div><p>先看下 Go 在进行 CGO 调用的时候会发生什么。Go runtime 提供的线程与系统线程不是一一对应，在进行非 Go 的调用时，会把当前 goroutine 放在一个系统线程上，使用这个系统线程完成后续的调用流程。所以如果不做特殊处理的话对于 shim 来说每次请求都有可能发生在一个新的线程上，而使得之前保存在 TLS 中的状态失效。并且更加严重的是，shim 的 TLS 中包括了一些 library 的线程句柄，导致 library 的部分状态也出现了混乱，最终整个进程的行为都变得很奇怪。</p><div class="heti heti--annotation">为了解决这个问题，我们需要把 shim 和 library 的线程固定住，shim 接到请求后通过<ruby>线程信息传递<rt>channel</rt></ruby>的方式与 library 交互，包括发送请求与接收结果：</div><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Handle</span></span> &#123;<br>    runtime: Runtime <span class="hljs-comment">// this was TLS</span><br>&#125;<br><br><span class="hljs-keyword">impl</span> Handle &#123;<br>    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">new</span></span>() -&gt; <span class="hljs-keyword">Self</span>&#123;<br>        <span class="hljs-keyword">let</span> worker_thread = std::thread::spawn(|| &#123;&#125;); <span class="hljs-comment">// main thread, channels inside</span><br>        <span class="hljs-keyword">let</span> runtime = Runtime::new(); <span class="hljs-comment">// callback threads</span><br>        <span class="hljs-keyword">Self</span> &#123; runtime &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>同时来都来了，也顺便将 library 异步的接口暴露到了上层，由原来一次 CGO 完成整个请求的方式变成了一次 CGO 提交请求，结果通过后续另一次 C 到 Go 的调用异步地返回，也能够防止大请求把 Go runtime spawn 出来的 CGO 线程阻塞太长时间。现在整个的交互流程看起来大概是这样的：</p><p><img src="2.png" alt="img"></p><p>不过这只是很基础的方案，“变电器” CGO 的原理很简单，但也存在许多优化空间，能够在解决问题的同时降低损耗率。这里是找到的一些操作（<a target="_blank" rel="noopener" href="https://mp.weixin.qq.com/s/PdeLX4loFaXr4E74hsrHCw">1</a>,<a target="_blank" rel="noopener" href="https://about.sourcegraph.com/go/gophercon-2018-adventures-in-CGO-performance/">2</a>,<a target="_blank" rel="noopener" href="https://www.cockroachlabs.com/blog/the-cost-and-complexity-of-CGO/">3</a>），当然大部分都还没做（</p><h1 id="内存"><a href="#内存" class="headerlink" title="内存"></a>内存</h1><p>除此之外，为了通过 C 的接口来传递数据，需要对数据结构进行一些修改，这一部分主要在 shim 和 SDK 进行。简单来说就是把所有需要传递的类型都能够用 C 表示出来，在<a target="_blank" rel="noopener" href="http://jakegoulding.com/rust-ffi-omnibus/">这里</a> 有一些例子，或者是像这样：</p><figure class="highlight rust"><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><code class="hljs rust"><span class="hljs-meta">#[repr(C)]</span><br><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Bytes</span></span> &#123;<br>    ptr: *<span class="hljs-keyword">mut</span> libc::c_void,<br>    len: libc::size_t,<br>&#125;<br><br><span class="hljs-keyword">crate</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">make_bytes</span></span>(<span class="hljs-keyword">mut</span> bytes: <span class="hljs-built_in">Vec</span>&lt;<span class="hljs-built_in">u8</span>&gt;) -&gt; <span class="hljs-keyword">Self</span> &#123;<br>    bytes.shrink_to_fit();<br>    <span class="hljs-keyword">let</span> (ptr, len, cap) = bytes.into_raw_parts();<br>    <span class="hljs-built_in">debug_assert_eq!</span>(len, cap);<br>    Bytes &#123;<br>        ptr: <span class="hljs-keyword">unsafe</span> &#123; mem::transmute::&lt;*<span class="hljs-keyword">mut</span> <span class="hljs-built_in">u8</span>, *<span class="hljs-keyword">mut</span> libc::c_void&gt;(ptr) &#125;,<br>        len,<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>Go 与 Rust 在远离 FFI 边界的地方（一般来说）都能够很好地处理内存问题，这些非法的指针和地址基本上都是没能好好处理另一方丢过来的东西造成的。在当时为了减少工作量已经通过大量的拷贝减少了许多使用裸指针的地方，剩下的主要集中在两处，即接收对方传递过来的数据以及通知对方回收这一块数据的内存。这种时候除开遵守老生常谈的“<span class="heti-em"><strong>谁分配谁释放</strong></span>”原则外，剩下的基本也只能现场<del>抓头发后悔</del>定位了。</p><p>最先的入手点就是发生错误时所打出来的堆栈，按照经验跟着上面的错误信息后的第一条 Go routine backtrace 通常是发生问题的地方（应该……？）。可以先顺着调用栈检查一下代码中是否有不正确的指针使用。不过这个堆栈中只会存在 Go 这一部分的信息。当问题出现在 Go 之外的时候日志会变得非常迷惑，基本只能够知道是哪一个 CGO 函数值得怀疑。这一部分就和普通的内存问题排查差不多，列举几个常用的步骤：</p><h2 id="打印日志"><a href="#打印日志" class="headerlink" title="打印日志"></a>打印日志</h2><p>如果问题比较好复现的话，可以在路径上多增加一些日志输出，把值得怀疑的指针地址以及它们解引用之后的内容打印出来，有时能够观察到一个指针是如何一步步走向非法的。注意解引用时最好放到另一行日志中，毕竟每一个解引用操作都是在非法边缘蹦迪，如果这个指针在解引用的时候出错可能会带着其他有用的日志输出一起消失。</p><h2 id="借助工具"><a href="#借助工具" class="headerlink" title="借助工具"></a>借助工具</h2><p>也可以通过一些工具来监测内存的使用情况，比如各种 <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/beta/unstable-book/compiler-flags/sanitizer.html">sanitizer</a> 和 <a target="_blank" rel="noopener" href="https://valgrind.org/">valgrind</a> 等，有的时候不是所有的内存问题都会导致程序挂掉，一些非法的内存操作有可能被忽略，比如数组稍微越界一点点，不小心 free 多次或忘记 free 等。这些工具能够及时地发现这些问题。不过它们都或多或少会带来一些性能影响，所以也不是所有情况都适用。</p><figure class="highlight bash"><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><code class="hljs bash"><span class="hljs-comment"># valgrind</span><br>$ cargo build<br>$ valgrind --leak-check=full --show-leak-kinds=all --tool=memcheck $(BIN)<br><br><span class="hljs-comment"># address sanitizer</span><br>$ <span class="hljs-built_in">export</span> RUSTFLAGS=-Zsanitizer=address RUSTDOCFLAGS=-Zsanitizer=address<br>$ cargo run -Zbuild-std --target x86_64-unknown-linux-gnu<br></code></pre></td></tr></table></figure><h2 id="特殊值"><a href="#特殊值" class="headerlink" title="特殊值"></a>特殊值</h2><p>有些环境会在内存操作前后把那一块内存设置上特殊值来表示这块内存的状态，使它能方便地被观察到。如 jemalloc 的 <code>--enable-fill</code> <a target="_blank" rel="noopener" href="https://github.com/jemalloc/jemalloc/wiki/Use-Case%3A-Find-a-memory-corruption-bug">参数</a> 或 MariaDB 的 <code>TRASH_ALLOC()</code> <a target="_blank" rel="noopener" href="https://github.com/MariaDB/server/blob/c9fcea14e9e1f34a97451706eac51276c85bbea7/include/my_valgrind.h?_pjax=%23js-repo-pjax-container,%20div%5Bitemtype=%22http://schema.org/SoftwareSourceCode%22%5D%20main,%20%5Bdata-pjax-container%5D#L97-L100">宏</a>，会把回收的内存填上一个特殊值，如果看到这个值的话就知道是发生了 use after free。</p><h2 id="缩小范围"><a href="#缩小范围" class="headerlink" title="缩小范围"></a>缩小范围</h2><p>分为两个方面的缩小。如果条件允许的话也可以通过注释掉一些代码来缩小排查范围，比如先不进行 free 操作或者暂停部分路径进行观察；以及缩短调用路径，把各个组件单独拿出来 mock 测试。</p><p>还有一些其他的小地方，比如观察指针是否对齐，出错的指针与周围指针的范围等。比如这里 Go 与 Rust 运行在同一个进程内，能够观察出 Go 出来的指针和 Rust 出来的指针在两个地址段上，指针本身的值有时候也能够说明一些信息。不过说归说，真写出来的内存问题都是表面上千篇一律，背地里各有千秋。当程序内存出错时可能会带来各种千奇百怪的表现，搞多了就能够对不同的问题进行分类并案。毕竟一般来说全然无计可施的情况比较少，而通常是有许多手段但不知道哪一个才能得到信息。每个技能都会有施法时间消耗，万一被 invalid pointer 纠缠太久导致做梦都是panic就很痛苦了😇。</p><p>在制作 shim，binding 和它们的 c demo 的时候通过一些 C 重新体验了一下<span class="heti-em">文明的进步</span>。不过现在回过头想想应该还有更先进的方法来避免一些问题的，有的时候的确是自己作死。</p><h1 id="信号"><a href="#信号" class="headerlink" title="信号"></a>信号</h1><p>这是一个花了比较久时间的问题，噩梦从一次 SIGSEGV 开始（这个本质上也是一个内存问题）。</p><figure class="highlight plaintext"><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><code class="hljs plain">fatal error: unexpected signal during runtime execution<br>[signal SIGSEGV: segmentation violation code=0x2 addr=0x18589091b1 pc=0x7f9205f814ba]<br>runtime stack:<br>runtime.throw(&#123;0xace954, 0xefc4e055&#125;)<br>        /home/go/src/runtime/panic.go:1198 +0x71 fp=0x7f91dce8afe8 sp=0x7f91dce8afb8 pc=0x448451<br>runtime.sigpanic()<br>        /home/go/src/runtime/signal_unix.go:719 +0x396 fp=0x7f91dce8b038 sp=0x7f91dce8afe8 pc=0x45fdf6<br></code></pre></td></tr></table></figure><p>这里从复现就不顺利，原始场景大概需要半天到一天才能出现，同时也有其他未解决的问题混杂在一起，并且存在许多不同的系统环境和流量，各方面来说都非常地狱。</p><p>在花了一些时间把其他的因素都排除掉之后，终于能够开始看 SIGSEGV 产生的 core dump 文件。但是通过 gdb 来看 core 的时候，都基本上是这样的</p><figure class="highlight plaintext"><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><code class="hljs plain">Thread  (Thread 0x7f7c45fef700 (LWP 47715)):<br>#0  0x0000000000895b93 in runtime.futex ()<br>#1  0x0000000000860a20 in runtime.futexsleep ()<br>#2  0x0000000001dea010 in runtime.sched ()<br>#3  0x0000000000000080 in ?? ()<br>#4  0x00007f7c45feccb0 in ?? ()<br>#5  0x0000000000000000 in ?? ()<br>Thread  (Thread 0x7f7c457ee700 (LWP 47716)):<br>#0  0x0000000000895b93 in runtime.futex ()<br>#1  0x00000000008609ab in runtime.futexsleep ()<br>#2  0x000000c00011e840 in ?? ()<br>#3  0x0000000000000080 in ?? ()<br>#4  0x0000000000000000 in ?? ()<br>Thread  (Thread 0x7f7c4468c700 (LWP 52927)):<br>#0  0x00007f7c49301489 in syscall () from /lib64/libc.so.6<br>#1  0x00007f7c49834da9 in futex_wait (self=0x7f7c4468a640, ts=...) at parking_lot_core-0.8.5/src/thread_parker/linux.rs:112<br></code></pre></td></tr></table></figure><p>问号的那些很好猜是 Go 的 m:n 线程来的，但是其他非 Go 的线程却都停在 syscall 上。一下子完全不知道到底该 blame 谁，为了查看下 Go 里面发生了什么事情，也用过 <a target="_blank" rel="noopener" href="https://github.com/go-delve/delve">delve</a> 来看看 Go 的堆栈，用法和 gdb 类似。这是一个有些痛苦的过程，dump 出了几千个 goroutine 的 backtrace，而且花了些时间简单看了下栈顶之后发现基本都是业务 goroutine，没有什么好怀疑的。</p><figure class="highlight plaintext"><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><code class="hljs plain">(dlv)   Goroutine 1 - User: main.go:207 main.main (0xfa6f0e) [chan receive (nil chan) 451190h16m34.324364485s]<br>  Goroutine 2 - User: /root/go/src/runtime/proc.go:367 runtime.gopark (0x7f8076) [force gc (idle) 451190h16m34.619109482s]<br>  Goroutine 3 - User: /root/go/src/runtime/proc.go:367 runtime.gopark (0x7f8076) [GC sweep wait]<br>  Goroutine 4 - User: /root/go/src/runtime/proc.go:367 runtime.gopark (0x7f8076) [GC scavenge wait]<br>  Goroutine 5 - User: /root/go/src/runtime/proc.go:367 runtime.gopark (0x7f8076) [finalizer wait 451190h14m6.860088484s]<br>* Goroutine 17 - User: /root/go/src/runtime/sys_linux_amd64.s:165 runtime.raise (0x82acc1) (thread 71636) [GC assist marking]<br>  Goroutine 47 - User: /root/go/src/runtime/proc.go:367 runtime.gopark (0x7f8076) [select 451190h16m34.324667846s]<br>  Goroutine 48 - User: /root/go/src/runtime/sigqueue.go:169 os/signal.signal_recv (0x825998) (thread 67111)<br></code></pre></td></tr></table></figure><p>唯一的信息就是看到了 SIGSEGV 是从哪里出来的（Goroutine 17）</p><figure class="highlight plaintext"><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></pre></td><td class="code"><pre><code class="hljs plain">runtime.throw(&#123;0xace954, 0xefc4e055&#125;)<br>        /home/go/src/runtime/panic.go:1198 +0x71 fp=0x7f91dce8afe8 sp=0x7f91dce8afb8 pc=0x448451<br>runtime.sigpanic()<br>        /home/go/src/runtime/signal_unix.go:719 +0x396 fp=0x7f91dce8b038 sp=0x7f91dce8afe8 pc=0x45fdf6<br></code></pre></td></tr></table></figure><p>看着有点眼熟，就是经常在错误信息里面出现的第一行 stack。这个 goroutine 只有上面两层。只能说这个信息虽然有用，但基本没用。</p><p>又经过了一些时间的挣扎，最后怀疑上面看到的 core dump 并不是实际上的第一手堆栈。通过把整个进程挂在 gdb 里面运行，终于能够拿到非法操作实际上出现的位置，让 gdb 在信号第一次抛出的时候先于 Go runtime 捕获到它。现在回看起来想说：如果早知道，堆栈信号会被 Go runtime 转手…</p><p><img src="3.png" alt="img"></p><p>在能够看到第一案发现场之后，后续的流程就比较普通了，而且根因是个自己挖的弱智坑。</p><h1 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h1><p>分享我很喜欢的一句话：</p><blockquote><p>君子不立于花活之下。</p></blockquote><p>如果再让我选一次，打死也不会表演这个杂技。特别是最后发现一通操作猛如虎，一看QPS只有5 😇。还不如乖乖弄一些靠谱的东西（虽然被毒打之前也觉得 CGO 不过如此……）。</p><!-- # 参考 -->]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;故事发生在蚂蚁一个内部项目上，这个项目有 Go 和 Rust 两个部分，其中 Rust 库作为一个存储组件被 Go 的业务部分依赖着。出于种</summary>
      
    
    
    
    
    <category term="Rust" scheme="https://waynexia.github.io/tags/Rust/"/>
    
  </entry>
  
  <entry>
    <title>天穗之咲稻姬</title>
    <link href="https://waynexia.github.io/2021/02/sakuna-rice-and-ruin/"/>
    <id>https://waynexia.github.io/2021/02/sakuna-rice-and-ruin/</id>
    <published>2021-02-24T15:49:28.000Z</published>
    <updated>2026-01-14T13:19:51.709Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>最后放staff名单的时候的片尾曲。另外一个唱得不太正经的版本也很有意思，变奏在许多场景里面都出现过。可能是歌比较慢，打出staff之后躺在床上听着听着睡着了。</p></blockquote><div class="aplayer-box" data-name='ヤナト田植唄' data-artist='大嶋啓之 / 朝倉さや' data-url='https://music.163.com/song/media/outer/url?id=1813023950.mp3' data-cover='https://p1.music.126.net/RtdA4v9i3JESvxWC_LVLeQ==/109951165639529932.jpg' ></div><hr><p>种田，战斗和故事融合得不错的游戏（<strong>包含剧透</strong>）</p><p>虽然说种田怎么想都是重复度很高，很枯燥的东西，但是在目前经历的十个春秋中并没有觉得重复到令人犯困，反而觉得非常有趣。操作虽然都是固定的–耕地插秧配肥料收割出米，但是中间有许多事情来调节，比如说据求，探索，一些随机取得的物品，不同的目标属性所要求的完全不同的种植方法等。不过可惜最后还是没种明白地，几个属性的影响因素也没弄清楚，一些病发作的时候也不知道怎么解决，总的来说应该只知道了基本过程这种非常皮毛的东西。作为种田模拟器来说应该还有很大一部分要素没体验到，比如最后面新修的水磨坊之类的。但是丰收的快乐还是很好体验到了<del>（能结出穗也很厉害了）</del>。</p><p>据说日本农林水产省的网站在游戏发售之后访问量爆增，是因为种田系统太硬核，玩家直接开始参考现实的种田攻略了。我也因为水稻的疾病一直治不好翻攻略翻到了农业网站，但是马上就看不懂被劝退出来了，我觉得还是找针对性的攻略比较适合我……</p><p>操作方面有些地方不是很顺手，一是羽衣只有八个方向，可能是针对键盘设计的操作吧，在前期没有习惯的时候有些台子跳得真是血压升高…另一个是跟着玩家移动和自动调整视角，在一般时候还行，比如爬家前面那段山坡就可以只用左摇杆完成，但是到了插秧的时候就变成了噩梦，用摇杆的你永远对不齐方向，只能上垃圾十字键来勉强用用，而且发现歪了也很难微调。</p>    <figure class="figure-image">      <img src="2021021717524300-3E76408CDEF3BAC51A41DB325F1EE97E.jpg" alt="决战前的祭典" width="70%" height="100% loading="lazy" />      <figcaption>决战前的祭典</figcaption>    </figure>  <p>战斗难度对我这个手残玩家来说感觉很友好，而且比较自由的地方就是可以自己随便调节战斗和务农的比例，打不过了就不打了，回去种种田吃饱饱就能碾压。关键道具也比较少卡，而且最后打boss的武器还是直接送的，大概在祭典之前的一年左右应该就没什么东西需要再去刻意收集了。如果是种田爱好者，在剧情结束之后还有一个三百层的天返宫可以当end game，能继续种田-吃饭-打架的完整流程。虽然出现得比较突兀，不是很清楚它的背景（也可能是我漏了）。不过我几十层就滚蛋了（</p><p>战斗方面的重复感到后面就有些出现了。游戏的怪物种类设计得比较少，前后期地图的难度变化基本只是通过调整怪物强度来实现的。但是对比塞尔达更大的地图，怪物种类也没有非常多。种类少可能并不是直接的原因，出战斗是下副本的形式，以及一些材料收集和怪物与战斗基本绑定应该也是很重要的原因。</p><hr><p>故事方面的梗概在最开始的intro章节就讲述的差不多了，神仙闯了祸被赶到荒郊野岭和凡人一起种田。这个鬼岛日之穗是sakuna父母之前认识的地方，好像也是sakuna出生的地方（有这个吗，好像记错了）？。主场景就是上面那个山头，一个小房子和一块田。因为大家基本上都不是什么正经人（字面），有趣的日常不少。不过毕竟是种田游戏，剧情什么的哪有田重要（）心环和火山爆发两个小高潮的感觉挺不错，但是当时满脑子都是水稻，没有很带入进去。</p><p>关于sakuna的父母去哪里了在最终战之前居然一直没说，甚至在心环公主那一次回御柱都的时候我都还以为父母还在，然后自己瞎猜了一堆奇怪的设定。最后差点忘了还有许多伏笔好像没回收，是谁在操纵鬼族呢，做羽衣和实现结的愿望的是谁，农花的来历是什么等等，但是觉得这个种田系统已经很完善，如果说要出续集的话也想不到会怎么操作，就当这些是留白吧。</p>    <figure class="figure-image">      <img src="2021021719023200-3E76408CDEF3BAC51A41DB325F1EE97E.jpg" alt="最后见一面" width="70%" height="100% loading="lazy" />      <figcaption>最后见一面</figcaption>    </figure>  <p>年后开始玩，断断续续玩了十个小时左右之后在初五初六关在房间里面肝了两天打通主线，现在还是在想种田的事情。甚至有点想去做做农活，还好是在冬天（</p><p>RPG的属性要素通过种田吃饭体现出来的，一日三餐都有属性影响。头两三年过着一言不合就只靠喝水度过一天的日子，到了后面田地和盐多起来之后都懒得每天翻库存，只每晚把猎到的食物处理一下，完全吃不完（经典穷的时候不舍得用消耗品，库存足了懒得翻出来x）。在打完之后听说到了一个邪道方法，可以通过堆肥来延长食物的保质期。这场景想想就很不得了，可惜从来没有过把堆肥的东西拿去吃的想法，白节省了很多东西（每次去堆肥的时候都想吐槽厕所居然没有门</p><!-- 重新回到山头上面，除掉了作恶的大龙，终于和父母见面并告别，和御柱都的神仙们有了更多的往来，和伙伴们一起开垦更多的田地，等着某一天大桥再次出现。 -->    <figure class="figure-image">      <img src="2021021719094300-3E76408CDEF3BAC51A41DB325F1EE97E.jpg" alt="主题曲和大家的人设都很棒，谁不想开着神仙去种田呢" width="70%" height="100% loading="lazy" />      <figcaption>主题曲和大家的人设都很棒，谁不想开着神仙去种田呢</figcaption>    </figure>  <p>在最后打倒大家大龙愉快地走在回家的路上的时候，想到她们明年还是要接着种田，我明天要开始上班。就突然觉得工作真是…😭</p><hr><p>看着ed中几个人走在田间小路上，在想应该有很多类似古事记里面的故事暗喻没有get到吧？毕竟那些神祇的名字里面也有很多穗啊稻啊什么的。临结局的时候一直在猜之后会怎么安排，还是非常出乎意料的。本来以为只有圣女会回到凡间，没想到金太和饲丸也一起下去了。除了sakuna和结之外的其他人都没有猜对：</p><div class="heti heti--vertical"><blockquote><p>之后<strong>米鲁缇</strong>回到凡间，编写了多本书籍并出版于世。还成立了弗罗摩斯新教派，带给人们全新的观点和价值观。（没找到对应的宗教）</p><p><strong>饲丸</strong>在凡间成为了一位温柔的农夫，由于他能对别人的感受产生深刻共鸣，所以村子里的人有事都会找他商量。</p><p><strong>田右卫门</strong>还是一样在日之惠勤奋种稻，虽然总有点过头，但也因为他懂礼法规则，所以又渐渐获认可称为丰穰神。</p><p>回到凡间的<strong>金太</strong>成了驰名远近的传说锻造师，听说他还成了挺会喝酒的酒豪。</p><p>与金太分开后，<strong>结</strong>成为一位美丽的纺织女神。诸神听闻了她的消息后，络绎不绝地造访日之惠向她求婚，但每次都被佐久名赶回去。www</p><p><strong>魂爷</strong>最近倒是常常发呆，或许是因为佐久名的成长让他了无牵挂了吧。不管怎么说，身为人臣的他似乎过得挺幸福。</p><p>而<strong>心环</strong>于公于私都得到神灵树尊的重用，每天都忙碌且充实。之后却因某件事在都城和另一名发明神起了争端，不过这又是另一个故事了。</p><p><strong>佐久名</strong>则实至名归，成了雅那特最伟大的丰穰神。据说在往后数百年，她的稻子为诸神所爱，将丰穰带给凡间。</p>    <figure class="figure-image">      <img src="2021021719085300-3E76408CDEF3BAC51A41DB325F1EE97E.jpg" alt="" width="100%" height="20% loading="lazy" />      <figcaption></figcaption>    </figure>  </blockquote></div>]]></content>
    
    
      
      
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;最后放staff名单的时候的片尾曲。另外一个唱得不太正经的版本也很有意思，变奏在许多场景里面都出现过。可能是歌比较慢，打出staff之后躺在床上听着听着睡着了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;aplayer-box&quot; </summary>
      
    
    
    
    
    <category term="Experience" scheme="https://waynexia.github.io/tags/Experience/"/>
    
    <category term="Game" scheme="https://waynexia.github.io/tags/Game/"/>
    
  </entry>
  
  <entry>
    <title>Paper Reading: Adaptive Radix Tree</title>
    <link href="https://waynexia.github.io/2020/07/adaptive-radix-tree/"/>
    <id>https://waynexia.github.io/2020/07/adaptive-radix-tree/</id>
    <published>2020-07-13T21:45:12.000Z</published>
    <updated>2026-01-14T13:19:51.701Z</updated>
    
    <content type="html"><![CDATA[<h1 id="什么是-ART"><a href="#什么是-ART" class="headerlink" title="什么是 ART ?"></a>什么是 ART ?</h1><p>ART 是一个能提供高效点的增删查改，以及范围查询的索引结构。是一个有序的映射。名称中的 radix 表示这是一颗用基数来构建的结构，内部数据是有序存储的，所以有高效范围查询的能力。相比于其他常见的基数结构，ART 的空间利用率更高。这得益于它 adaptive 的特性，即能够根据空间使用情况动态地调节节点大小。</p>    <figure class="figure-image">      <img src="./1.png" alt="Compare to HashTable" width="50%" height="100% loading="lazy" />      <figcaption>Compare to HashTable</figcaption>    </figure>  <h1 id="基数树"><a href="#基数树" class="headerlink" title="基数树"></a>基数树</h1><p>是trie树的一种。这种数据结构所储存的信息并不在某个节点上，而是由一条由根到叶节点的路径所表示（当用作映射的时候通常为键的信息储存在路径上，值的信息存于对应的叶节点）。</p>    <figure class="figure-image">      <img src="2.png" alt="An ART" width="50%" height="100% loading="lazy" />      <figcaption>An ART</figcaption>    </figure>  <h1 id="Adaptive-Node"><a href="#Adaptive-Node" class="headerlink" title="Adaptive Node"></a>Adaptive Node</h1><p>ART 使用能动态调节大小（自适应 adaptive）的节点作为内部树节点，能够提高内存使用率。DBMS 中常见的有序索引数据结构B-tree或者传统的radix tree因为每个节点的大小是固定的，因此存在空间放大的问题。类似于磁盘或内存页表的大小，节点的大小通常需要取舍，节点太大的话浪费很严重，节点太小路径又很长，影响读写效率。</p>    <figure class="figure-image">      <img src="3.png" alt="" width="70%" height="100% loading="lazy" />      <figcaption></figcaption>    </figure>  <p>虽然理论上说能够使用动态数组作为内部节点，但是这样会涉及到频繁的内存分配释放。为了解决这个问题，ART将节点的大小固定为了四种。分别为4、16、48以及256。并且每个节点根据大小，在具体的结构以及行为上会有细微不同</p><h1 id="四种节点的具体实现"><a href="#四种节点的具体实现" class="headerlink" title="四种节点的具体实现"></a>四种节点的具体实现</h1><p>首先节点类型的变化仅发生在上溢或者下溢时，即可以/需要使用另一种类型的节点来代替。需要注意由于前缀树的特性，一个节点的最大宽度是有限的，因此并不会横向分裂成两个节点。</p><h2 id="Node4-与-Node16"><a href="#Node4-与-Node16" class="headerlink" title="Node4 与 Node16"></a>Node4 与 Node16</h2>    <figure class="figure-image">      <img src="4.png" alt="Node4 and Node16" width="40%" height="100% loading="lazy" />      <figcaption>Node4 and Node16</figcaption>    </figure>  <p>Node4 与 Node16 在结构上面是类似的，都是由一对数量相等的数组组成，其中一个存放键，另一个存放指针。</p><h2 id="Node48"><a href="#Node48" class="headerlink" title="Node48"></a>Node48</h2>    <figure class="figure-image">      <img src="5.png" alt="Node48" width="40%" height="100% loading="lazy" />      <figcaption>Node48</figcaption>    </figure>  <p>Node48 与前两种一样也是由两个数组组成，不过存储键的数组有256的容量，可以直接通过下标进行索引。</p><h2 id="Node256"><a href="#Node256" class="headerlink" title="Node256"></a>Node256</h2>    <figure class="figure-image">      <img src="6.png" alt="Node256" width="40%" height="100% loading="lazy" />      <figcaption>Node256</figcaption>    </figure>  <p>Node256与Node48相比其实就是第二个数组的大小也来到了256，键与值有满射的关系，可以省略一次索引。</p><h1 id="路径压缩"><a href="#路径压缩" class="headerlink" title="路径压缩"></a>路径压缩</h1><p>文中将路径压缩分成了两种情况，分别是 path compression 和 lazy expansion。 path compression 指将只有一个 key 的节点往下合并，直到需要区分多个子节点为止。lazy expansion 表示把一个节点路径上没有分叉的部分折叠起来。</p>    <figure class="figure-image">      <img src="7.png" alt="Two ways of path compression" width="70%" height="100% loading="lazy" />      <figcaption>Two ways of path compression</figcaption>    </figure>  <p>既然路径被压缩，那么在查找的时候也需要在比较 key 时进行相应的处理。文中将处理分为了两类。悲观的方式是节点储存一个长度不定的前缀值，在每次下降地时候使用这个值进行比较。乐观地方式是节点只储存被压缩地前缀地长度，在每次下降过程中直接跳过这个长度的 key 的处理，并在最后达到叶子节点时再比较 key 是否相等。文中的节点将这两种方式组合起来，在下降的时候按照前缀长度进行动态处理。</p><h1 id="节点-header"><a href="#节点-header" class="headerlink" title="节点 header"></a>节点 header</h1><p>一个节点除了自己的数据域之外，还包含了一些元信息。例如节点的类型，实际容量，压缩的路径（前缀和 / 或前缀的长度）等。在文中，节点有八字节的前缀区域以及四字节的前缀长度域，即上一段提到的悲观与乐观相结合的处理方法。</p><h1 id="读写操作"><a href="#读写操作" class="headerlink" title="读写操作"></a>读写操作</h1><h2 id="查询"><a href="#查询" class="headerlink" title="查询"></a>查询</h2><p>对于 Node4 采用遍历的方法，Node16 使用 SIMD 或者二分来比较当前的 key，Node48 与 Node256 分别只需进行两次 / 一次索引就能对比到前缀进行下降。<br>一个一般的查询流程为找到对应的节点进行下降或返回完成查找，对于不存在的键或者节点返回空。另外由于 path compression 的存在，可能一次下降会完成多个 bytes 的比较。<br>而 range scan 可以使用类似深搜的数次查询实现。</p><h2 id="插入-删除"><a href="#插入-删除" class="headerlink" title="插入 / 删除"></a>插入 / 删除</h2><p>一般一个 key 的插入或删除可能出现几种特殊情况。发生上溢/下溢时需要进行节点的替换来完成扩/缩容；如果一次扩缩容触发了路径压缩，那么会有内部节点的新增或删除，以及节点元信息里前缀数据的调整。<br>如果一次扩容的节点是 lazy expansion 的，或者缩容导致节点的实际容量变为1，那么这条路径的长度会发生变化（以插入为例，使用一个新节点替换当前的叶子节点，这个新节点有两个叶子节点，分别为之前的以及新增的，节点的前缀也会变成两个叶子节点的 key 的重合部分）；如果新插入节点的 key 会影响一个节点的前缀，那么也会在该节点上面新增一个 inner 节点，并相应地调节前缀；其他的情况只需要进行简单的插入删除就行了。可以看到，在上述几种情况中，一次增删过程中会受到影响的节点最多仅有两个。</p><h1 id="数据表示"><a href="#数据表示" class="headerlink" title="数据表示"></a>数据表示</h1><p>为了实现数据有序排列，需要能将数据类型表示成二进制可比较（binary comparable）的形式。<br>无符号整型天然满足要求，只是需要注意大小端。有符号整型需要做一次异或操作来处理补码表示法。浮点数稍微复杂一些，按照 [ 正，负 ] x [ 规格化、非规格化、NaN、∞、0 ] 分为互不重叠的十类，然后重新给 rank。对于字符串有 Unicode 定义的比较算法。</p><h1 id="应用"><a href="#应用" class="headerlink" title="应用"></a>应用</h1><p>在本文的 benchmark 中可以看到，当数据的 key 分布比较密集时，ART 有着很优秀的表现。而且除了 range scan 范围读之外，ART 的 bulk load 范围写也有很大的发掘空间。</p><h1 id="并发"><a href="#并发" class="headerlink" title="并发"></a>并发</h1><p>文章介绍了两种并发方案，分别是 Optimistic Lock Coupling 和 Read-Optimized Write EXclusion (ROWEX)。它们的性能很接近。</p>    <figure class="figure-image">      <img src="8.png" alt="Comparison of serval concurrency schemas" width="70%" height="100% loading="lazy" />      <figcaption>Comparison of serval concurrency schemas</figcaption>    </figure>  <h2 id="Optimistic-Lock-Coupling"><a href="#Optimistic-Lock-Coupling" class="headerlink" title="Optimistic Lock Coupling"></a>Optimistic Lock Coupling</h2><p>是在传统 Lock Coupling 方案上的一种优化。Lock Coupling 的重点在于一个操作同时最多持有两把锁，是线程同步 B-Tree 的常见做法。并且在 ART 的背景下，一次修改最多只会影响到两个节点，非常适合使用 Lock Coupling 来实现并发。但是 Lock Coupling 因为每次取得一把锁都会修改节点的内容，所以对缓存很不友好，导致其效率低下。在这里采用一种 Optimistic 的做法，即假设冲突很少出现，每次只在读完之后检测版本信息来判断有没有读到过时信息，而非预先上锁来确保互斥。如果检测到了过期信息则进行重试，为了防止饿死可以在若干次重试失败之后进行加锁。</p><h2 id="ROWEX"><a href="#ROWEX" class="headerlink" title="ROWEX"></a>ROWEX</h2><p>则稍微复杂一点，它要求 writer 通过原子操作来保证 reader 不会出现脏读。这个方法也使用到了锁，不过仅针对 writer 提供互斥性，而读操作是 wait-free 并且与 Optimistic Lock Coupling 不同，是保证成功的。</p><h1 id="Bench"><a href="#Bench" class="headerlink" title="Bench"></a>Bench</h1><p>在<a target="_blank" rel="noopener" href="https://dbis-informatik.uibk.ac.at/sites/default/files/2018-06/hot-height-optimized.pdf">HOT (SIGMOD 2018)</a> 的 appendix A 找到一个比较全面的 Cross Validation。TL; DR. 在数据密集查询比扫描多，写操作比重较多的情况下 ART 有很优秀的表现，纯扫表性能并不比 B-Tree 好。大量密集数据点查表现可以超过 HashTable，不过性能会受到数据分布的影响。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;什么是-ART&quot;&gt;&lt;a href=&quot;#什么是-ART&quot; class=&quot;headerlink&quot; title=&quot;什么是 ART ?&quot;&gt;&lt;/a&gt;什么是 ART ?&lt;/h1&gt;&lt;p&gt;ART 是一个能提供高效点的增删查改，以及范围查询的索引结构。是一个有序的映射。名称中的 r</summary>
      
    
    
    
    
    <category term="Paper Reading" scheme="https://waynexia.github.io/tags/Paper-Reading/"/>
    
  </entry>
  
</feed>
