<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Hai</title>
    <description>脚踏实地一步一步学习吧。记录一些学习上的事情。</description>
    <link>https://uoryon.github.io/</link>
    <atom:link href="https://uoryon.github.io/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>2022-12-30</pubDate>
    <lastBuildDate>Fri, 30 Dec 2022 15:56:56 +0800</lastBuildDate>
    <generator>Jekyll v3.9.2</generator>
    
      <item>
        <title>在宿主机上如何查询容器内进程的链接</title>
        <description>&lt;p&gt;最近在做流媒体相关的一些服务，它通过 k3s 部署在盒子上，我们的服务偶尔会出现拉流资源被拉爆的情况，需要定位一下是否存在资源泄漏的情况。由于基本流媒体服务是端口复用的，我们则选择 lsof -i :&lt;port_num&gt; 来做这样的事情，但是在宿主机上看不到相关的信息，一定得进容器去看，但是部署的容器一般不带 lsof 等网络工具，每次定位问题需要 apt update &amp;amp;&amp;amp; apt install，还是挺麻烦的。于是深入了解了一下，应该如何做？
&lt;!--more--&gt;&lt;/port_num&gt;&lt;/p&gt;

&lt;h2 id=&quot;lsof--i-&quot;&gt;lsof -i :&lt;port_num&gt;&lt;/port_num&gt;&lt;/h2&gt;

&lt;p&gt;这是最直观的工具，可以看到针对这个 port 有打开哪些资源，但是在我们现在这个环境下失效了，什么都没有输出。但是系统又是 work 的，信息到底隐藏到哪里了呢？&lt;/p&gt;

&lt;h2 id=&quot;procnettcp&quot;&gt;/proc/net/tcp&lt;/h2&gt;

&lt;p&gt;linux proc 伪文件系统提供了很多有意思的功能，例如这个文件就记录着系统里目前运行着的 tcp 链接，那么我们尝试读取这个文件，由于这个文件记录的数据是 16 进制的，进行 grep 时，我们需要转换一下，我这里的端口是 9500，转换后是 251C。
关于 tcp 文件的细节，可以参考 https://www.kernel.org/doc/Documentation/networking/proc_net_tcp.txt。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ cat /proc/net/tcp | grep 251c
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/leakage_link/screenshot-20221230-151735.png&quot; alt=&quot;data&quot; /&gt;
返回是空白的，什么数据都没有，只能再尝试别的方法了。&lt;/p&gt;

&lt;h2 id=&quot;proc&quot;&gt;/proc/&lt;pid&gt;&lt;/pid&gt;&lt;/h2&gt;
&lt;p&gt;系统里什么都看不到，只能继续去对应的进程的空间里看看了，fd 是最常用的 ，我们能看到很多数据，一些 socket，一些 file，但是并无法知道 socket 到底是什么，是一个 unix socket 还是别的什么，也看不到端口。
&lt;img src=&quot;/assets/leakage_link/screenshot-20221230-152323.png&quot; alt=&quot;data2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;那么 /proc/&lt;pid&gt;/net/tcp 呢？
![data3](/assets/leakage_link/screenshot-20221230-152505.png)&lt;/pid&gt;&lt;/p&gt;

&lt;p&gt;哈，果然在这里，能看到打开的 251C 的链接了，通过这里我们能够看到真实 work 的数据了。&lt;/p&gt;

&lt;h2 id=&quot;why&quot;&gt;WHY&lt;/h2&gt;

&lt;p&gt;为什么会这样？&lt;/p&gt;

&lt;p&gt;本身问题也比较简单，要搞明白这个问题，需要了解这些网络相关的东西，做容器的同学应该都听说过 iptables 和 linux namespaces。后者为每个容器划分独立的网络空间，而 iptables 提供转发的功能。这里我们不深入讨论这两个系统，了解即可。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/proc/net/tcp&lt;/code&gt; 的问题是，如果是在 namespace 里打开的链接，在这里就不会体现了。进程在 net namespace 中监听端口，iptables 将收到的网络数据进行转发。&lt;/p&gt;

&lt;p&gt;既然知道是 namespace，一切都简单了，我们有 nsenter 工具，以及对应的 namespace&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ nsenter --net /proc/&amp;lt;pid&amp;gt;/ns/net -- lsof -i :9500
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src=&quot;/assets/leakage_link/screenshot-20221230-154813.png&quot; alt=&quot;data4&quot; /&gt;&lt;/p&gt;

&lt;p&gt;至此，完美解决问题，不需要再登进容器 apt update &amp;amp;&amp;amp; apt install lsof 了。&lt;/p&gt;

&lt;p&gt;建议大家去熟悉 proc 各个功能，在监控不足的环境下定位问题，有奇效。&lt;/p&gt;
</description>
        <pubDate>2022-12-30</pubDate>
        <link>https://uoryon.github.io/articles/2022/12/30/%E5%9C%A8%E5%AE%BF%E4%B8%BB%E6%9C%BA%E4%B8%8A%E5%A6%82%E4%BD%95%E6%9F%A5%E8%AF%A2%E5%AE%B9%E5%99%A8%E5%86%85%E8%BF%9B%E7%A8%8B%E7%9A%84%E9%93%BE%E6%8E%A5.html</link>
        <guid isPermaLink="true">https://uoryon.github.io/articles/2022/12/30/%E5%9C%A8%E5%AE%BF%E4%B8%BB%E6%9C%BA%E4%B8%8A%E5%A6%82%E4%BD%95%E6%9F%A5%E8%AF%A2%E5%AE%B9%E5%99%A8%E5%86%85%E8%BF%9B%E7%A8%8B%E7%9A%84%E9%93%BE%E6%8E%A5.html</guid>
        
        <category>linux</category>
        
        <category>namespace</category>
        
        
        <category>linux</category>
        
      </item>
    
      <item>
        <title>有关文件系统的学习</title>
        <description>&lt;p&gt;文件系统有很多种，是广义的一个概念，例如 HDFS、GFS 这种分布式文件系统，以及 ZFS、ext4 这种只专注于宿主机上管理的文件系统。这里我们讨论的是兼容 posix 接口的文件系统。这样的文件系统可以直接使用 linux 的 mount umount 进行挂载，挂载点内是树状的结构，可以使用我们常见的 open，write 等接口进行交互，接口内部的实现则由文件系统自己定义，也就是说文件系统内部有可能是基于 block device、内存、web service 等等来实现的。
&lt;!--more--&gt;&lt;/p&gt;
&lt;h3 id=&quot;基础知识介绍&quot;&gt;基础知识介绍&lt;/h3&gt;

&lt;h4 id=&quot;vfs&quot;&gt;VFS&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/assets/filesystem/The_Linux_Storage_Stack_Diagram.svg.png&quot; alt=&quot;The_Linux_Storage_Stack_Diagram.svg&quot; /&gt;
一图胜千言，从图中可以看到 VFS 作为衔接用户程序和块设备之间的一个系统，对用户屏蔽块设备、硬件细节，使用统一接口（open、write）等对下层文件系统进行访问。&lt;/p&gt;

&lt;h4 id=&quot;fuse&quot;&gt;FUSE&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/assets/filesystem/fuse.png&quot; alt=&quot;fuse&quot; /&gt;
Filesystem in userspace，它能允许我们在用户空间实现文件系统，而且实现起来非常简单，一句话描述则是基于 /dev/fuse 通信协议的一套服务。 它定义了一组与 VFS 抽象的调用类似的消息。
用户程序与 VFS 通信，VFS 查询到访问的文件系统是由 FUSE 管理的，那么会向 /dev/fuse 发送指令，表明用户需要 readdir、read 或者 write 之类的操作，此时我们实现的 fuse 程序，可以从 /dev/fuse 里拿到消息，从而进行下一步处理即可。&lt;/p&gt;

&lt;h4 id=&quot;难点&quot;&gt;难点&lt;/h4&gt;
&lt;p&gt;事情总不是那么美好，要完整的实现 posix 接口，完完全全和本地文件系统一样的话，还是需要不少的代价的。&lt;/p&gt;

&lt;h5 id=&quot;元数据&quot;&gt;元数据&lt;/h5&gt;
&lt;p&gt;ls 是一个非常好用的指令，但是在 peerfs （千亿级文件数量）上的实现必然是阉割的，因为不可能将所有的文件（inode）在一台机器上创建完毕，这会耗费极大的内存，只能按需创建，有一些特定领域系统为了省略文件系统元数据大小，会将一些小文件合并在一个文件中，也会直接把 posix 的兼容给阉割掉，例如 HDFS，按照服务定义的交互的方式交互在全局上可以更优。&lt;/p&gt;

&lt;h5 id=&quot;性能&quot;&gt;性能&lt;/h5&gt;
&lt;p&gt;用户态文件系统相比内核态文件系统，在目前还是有差距的。纵然用户态文件系统相对于内核态文件系统有很多优点：&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;开发简单，迭代快&lt;/li&gt;
  &lt;li&gt;实现 stackable 的文件系统&lt;/li&gt;
  &lt;li&gt;Bug 不会 crash 整个系统
但是性能在某些场景下会差很多。从图中可以看到有三次数据流在用户态和内核态之间跳转，这里会有大量的内存拷贝，在现有 FUSE 的优化下，大文件读取与使用普通文件系统差别不大，但是有些场景下性能会降低 83%，CPU 的相对使用率也会提升 31%。更多验证细节可以看这里。
不过 FUSE 还在进行着优化。&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;相关优化&quot;&gt;相关优化&lt;/h4&gt;
&lt;p&gt;基本还是围绕两个点展开：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;减少内存拷贝&lt;/li&gt;
  &lt;li&gt;减少磁盘 io&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;splice&quot;&gt;Splice&lt;/h5&gt;
&lt;p&gt;splice 是操作系统提供的功能，可以支持将一个文件拷贝到另外一个文件里，而不需要拷贝到用户态来。这在跨文件系统（设备）进行数据复制时带来的优势是巨大的。
VFS 是 stackable 的，也就是说一个文件系统 A 可以基于文件系统 B 实现，数据类似栈一样传递，这样的实现可以做一些去重或者压缩，类似文件系统界的网关。&lt;/p&gt;

&lt;h5 id=&quot;write-back-cache&quot;&gt;Write back cache&lt;/h5&gt;
&lt;p&gt;写 cache 的方式有两种，一种是 write back，一种是 write through。
Write through 和我们平时的使用方式比较类似，数据从分级存储，一层一层往后写，直到最后的存储存储完毕。例如数据服务接收到数据，在内存中缓存并写入磁盘保存，优点是数据一致性强，断电后数据也有保存。
Write back 则有一些不一样，数据先写第一层缓存，直到第一层缓存写满，内存需要换出时，根据策略（最少访问，最老创建等），将需要的数据写入后一级缓存，这带来的优势是吞吐量的提升。因为通常我们后一级的存储，响应时间是比前一层慢的，例如 L1 cache 相比 main memory，memory 相比 disk。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/filesystem/latency.png&quot; alt=&quot;latency&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;reference&quot;&gt;Reference&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;https://uoryon.github.io/articles/2021/04/27/haystack-%E5%AD%A6%E4%B9%A0.html&lt;/li&gt;
  &lt;li&gt;https://en.wikipedia.org/wiki/Virtual_file_system&lt;/li&gt;
  &lt;li&gt;https://en.wikipedia.org/wiki/Filesystem_in_Userspace&lt;/li&gt;
  &lt;li&gt;https://www.usenix.org/system/files/conference/fast17/fast17-vangoor.pdf&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>2022-12-30</pubDate>
        <link>https://uoryon.github.io/articles/2021/04/29/%E6%9C%89%E5%85%B3%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E7%9A%84%E5%AD%A6%E4%B9%A0.html</link>
        <guid isPermaLink="true">https://uoryon.github.io/articles/2021/04/29/%E6%9C%89%E5%85%B3%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E7%9A%84%E5%AD%A6%E4%B9%A0.html</guid>
        
        
      </item>
    
      <item>
        <title>Haystack 学习</title>
        <description>&lt;p&gt;最近在涉猎存储相关的一些知识，要补习的东西还是比较多，网络、存储、IO。其中之前没接触过的东西还蛮多，正好听分享的时候，有听到对象存储相关的东西，说是借鉴了一些 Haystack 的设计，那也顺便学习一下。&lt;/p&gt;

&lt;!--more--&gt;

&lt;h3 id=&quot;haystack&quot;&gt;Haystack&lt;/h3&gt;
&lt;p&gt;Haystack 是 facebook 实现的基于图片的对象存储。主要是三个组件&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Haystack Directory&lt;/li&gt;
  &lt;li&gt;Haystack Cache&lt;/li&gt;
  &lt;li&gt;Haystack Store&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/haystack/serving_a_photo.png&quot; alt=&quot;Serving a photo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在讲流程之前，需要额外提的一点是系统中有一个 Logical Volume 的概念，这个概念封装了 Physical Volume，这可以将多个 Physical Volume 映射到一个 Logical Volume 内，这个操作是为了做冗余，也就是 High Availablity（HA）。真实数据实际存在多个备份，防止坏盘，也可以提供读取负载均衡的功能。&lt;/p&gt;

&lt;p&gt;接下来就可以进入流程的环节了，用户通过 Haystack Directory 请求图片对象的访问 URL。Directory 会返回 CDN 地址，或者 Haystack Cache 的地址，让用户去访问。这里的 Cache 其实也是类似 CDN 的作用，为了区别外部 CDN，这样设计是为了减少自己对外部 CDN 的依赖，同时自己的 Cache 服务，能做更多针对性的优化。同时对于 CDN 来说，Haystack Cache 也像它的源站缓存。CDN miss 后，会向 Haystack Cache 调用，当 Haystack Cache 也 miss 时，数据会落到 Haystack Store 上，实际的物理存储介质里。&lt;/p&gt;

&lt;p&gt;（上传的过程先略过）&lt;/p&gt;

&lt;h4 id=&quot;haystack-directory&quot;&gt;Haystack Directory&lt;/h4&gt;
&lt;p&gt;它主要做四件事情：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;logical volume 到 physical volume 的映射&lt;/li&gt;
  &lt;li&gt;不同 physical volume 的 load balance&lt;/li&gt;
  &lt;li&gt;选择流量到 CDN 上还是 Cache 上&lt;/li&gt;
  &lt;li&gt;标记 volume 的 read-only 属性&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些都比较好理解。read only 功能是当卷满了或者需要运维的时候做的操作，使得 SRE 们能够维护一下集群。标记是以机器为粒度的。&lt;/p&gt;

&lt;h4 id=&quot;haystack-cache&quot;&gt;Haystack Cache&lt;/h4&gt;
&lt;p&gt;缓存系统，相似于 CDN。它是特殊设计过的，它只缓存满足两个条件的文件：&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;直接来自用户的请求，而不是 CDN 的请求&lt;/li&gt;
  &lt;li&gt;来自于可写集群的图片&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;文章中提到有两个原因，促使这样的决定。对于第一点，在内部集群缓存 CDN 来的请求是低效的，CDN 自身就有一个比较大的池子，而一次回源后，CDN 就应当有 Cache 了，短期内不应该再次回源，那么对于 Cache 来说，缓存一次的话，后面不会有访问，那么就白白浪费了内存。
而第二点来说，这是基于冷热数据来分析的，用户通常会访问刚刚上传完的图片，这个也在时间点的最前面，很符合直觉。&lt;/p&gt;

&lt;h4 id=&quot;haystack-store&quot;&gt;Haystack Store&lt;/h4&gt;
&lt;p&gt;这一部分是主要和 disk 交互得比较多。最核心的设计是，获取文件名、offset、大小不需要任何磁盘操作。这类数据称为 filesystem metadata。这些 metadata 为了避免 disk 相关的操作，都是存在 main memory 中的。Haystack 在此之上还做了一些优化，可以估测出图片在数量上是非常大的，那么元数据/文件大小就会变得低，比较浪费 memory，存储效率低，很直接的优化就是将多个图片文件合并成一个大文件。（索引相关的也略过。）磁盘操作的话，顺序写这些都是基础操作了。
选择了 XFS 来作为文件系统，这个 FS 对预创建文件、碎片迁移效率很高，以及对元数据的内存使用很小。&lt;/p&gt;

&lt;h3 id=&quot;重点总结&quot;&gt;重点总结&lt;/h3&gt;
&lt;p&gt;分布式系统必备的技能就不重复了。
看到的核心思想是尽可能的省略磁盘操作，这个操作会极大的影响延迟和吞吐量，这对对象存储系统来说是很重要的。
于是把很多数据放到了内存中，下一步，就是优化内存的使用。
Cache 的缓存策略也很有意思，算一种启发，这个之前工作有遇到类似的问题，但是没有这么系统的整理和认识。&lt;/p&gt;

&lt;h3 id=&quot;reference&quot;&gt;Reference&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Beaver.pdf&quot;&gt;Finding a needle in Haystack: Facebook’s photo storage&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>2022-12-30</pubDate>
        <link>https://uoryon.github.io/articles/2021/04/27/haystack-%E5%AD%A6%E4%B9%A0.html</link>
        <guid isPermaLink="true">https://uoryon.github.io/articles/2021/04/27/haystack-%E5%AD%A6%E4%B9%A0.html</guid>
        
        
      </item>
    
      <item>
        <title>数独游玩感受</title>
        <description>&lt;p&gt;最近沉迷了数独一段时间，有一些感受，记录一下下。&lt;/p&gt;

&lt;p&gt;首先，数独是一个游戏，每当找到一个新的数字的时候，带来的都是爽感，反馈特别强，而且我也很享受这种简单的逻辑游戏。&lt;/p&gt;

&lt;p&gt;其次，越难的数独，越有意思。我喜欢去尝试困难，这个困难能带给我学习的动力。&lt;/p&gt;

&lt;p&gt;再次，数独的游戏需要循序渐进，效率较低的部分，是直接去挑战高难度的数独，但没有去复盘每一局数独，很多可以提高的地方就放过了。&lt;/p&gt;

&lt;p&gt;最后，很多东西都是可以通过刻意训练来获得提升的，也要找对方法，最后有幸在微信阅读上，看到了适合我的教材，稍微学习了一下，基本最高难度的数独，20分钟能做完了，不会像之前卡住，解不出来。而且还只是随意看了，没有完整的看。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;希望能从数独这个游戏上积累到的东西，扩善到生活当中去。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;先挑战难的&lt;/li&gt;
  &lt;li&gt;失败后去针对性训练&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;如此反复，高效率的针对性学习。不过这种适合有固定解的问题，能够很快的找到答案，但如果是开放题，这个时候是没有标准答案了，就不适用这种方法。目前能够想到的还是多试，多尝试总没有错。&lt;/p&gt;
</description>
        <pubDate>2022-12-30</pubDate>
        <link>https://uoryon.github.io/articles/2021/01/13/%E6%95%B0%E7%8B%AC%E6%B8%B8%E7%8E%A9%E6%84%9F%E5%8F%97.html</link>
        <guid isPermaLink="true">https://uoryon.github.io/articles/2021/01/13/%E6%95%B0%E7%8B%AC%E6%B8%B8%E7%8E%A9%E6%84%9F%E5%8F%97.html</guid>
        
        <category>game</category>
        
        
      </item>
    
      <item>
        <title>InnoDB 锁学习记录</title>
        <description>&lt;p&gt;早在学校里就学习了数据库相关的东西，但是具体到细节的实现，了解得还是比较少，仅以这篇文章来回顾一下 InnoDB 中使用的一些锁，最后会和事务一块联系起来，做完整的一个介绍，彻底的理解 InnoDB 的锁与事务实现。
&lt;!--more--&gt;&lt;/p&gt;

&lt;p&gt;要实现大规模、负载高以及高可用的数据库应用，或者 mysql 调优，了解清楚 InnoDB 的锁机制和 InnoDB 的事务模型非常重要。&lt;/p&gt;

&lt;h4 id=&quot;shared-and-exclusive-locks&quot;&gt;Shared and Exclusive Locks&lt;/h4&gt;
&lt;p&gt;共享（S）锁和排他（X）锁&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;共享锁，允许持有该锁的事务，读取这一行&lt;/li&gt;
  &lt;li&gt;排他锁，允许持有该锁的事务，删除或更新这一行&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果事务 T1 持有行 r 的共享锁，那么新事务 T2 申请这一行的 S 锁能立马得到，结果是 T1 和 T2 都持有这个 S 锁。而如果 T2 申请这一行的 X 锁，那么就不能立马得到它。
如果事务 T1 持有行 r 的 X 锁，事务 T2 拿这一行的任意锁，都不会立马得到响应，会被锁住，直到 T1 释放这个 X 锁。
这个和我们程序里并发控制的锁还是挺相似的，类似读写锁。&lt;/p&gt;

&lt;h4 id=&quot;intention-locks&quot;&gt;Intention Locks&lt;/h4&gt;
&lt;p&gt;为了满足 innodb 的表级锁与行级锁共存，那么实现了 Intention Locks。
Intention locks 是表级别的锁，表明之后这个事务会对这张表的某些行做操作，同时也有 IS 和 IX 两种。是上面基础锁的扩展。
例如当用户更新 user 表的行 r 时，会对 r 设置 X 锁，并对表设置 IX 锁。这样，另外一个事务锁定表的话，就会冲突，需要等待 IX 的释放。
具体锁的冲突如下 &lt;img src=&quot;/assets/innodb-lock/conflict_lock.png&quot; alt=&quot;conflict_lock&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;record-locks&quot;&gt;Record Locks&lt;/h4&gt;
&lt;p&gt;记录锁，锁在索引的记录上，例如 select c1 from t where c1 = 10 for update; 那么所有插入、更新、删除 c1 值为 10 的事务，都会被锁住。
如果加上范围的话，大胆拆测是在索引树上，对子节点进行锁定。后续查看锁定过程。&lt;/p&gt;

&lt;h4 id=&quot;gap-locks&quot;&gt;Gap Locks&lt;/h4&gt;
&lt;p&gt;gap lock 会锁住索引区间。
gap lock 是性能和并发间的取舍，有一些隔离级别需要，有一些不需要。
一个 gap 可能是单个索引值，多个索引值，甚至是空的。使用唯一索引去查找的话，不需要 gap lock。例如下面这一句。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SELECT * FROM child WHERE id = 100
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;如果 id 是 uk，那么就只会使用 record lock，而和 100 之前的数据是没有关系的。而 id 不是索引，或者也不是 uk 的话，这个语句会锁住之前的间隙。&lt;/p&gt;

&lt;p&gt;间隙锁之间是会冲突的，”允许冲突的间隙锁的原因是，如果从索引中清除记录，则必须合并由不同事务保留在记录上的间隙锁。”对这一句体会还是比较浅显，了解得不多。
这是一个后面的todo&lt;/p&gt;

&lt;h4 id=&quot;next-key-locks&quot;&gt;Next-Key Locks&lt;/h4&gt;
&lt;p&gt;next-key lock 是 index record 上 record lock 和 index record 之前的 gap lock 的结合。
当执行搜索或者扫描表索引的时候，innodb 使用 next-key lock，X 或者 S，这个会阻止别的事务影响这个键以及键之前的数据。
例如：
如果索引中有的值是 10，11，13 和 20。那么生成的锁区间是&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;innodb 默认使用可重复读的隔离级别，用来阻止幻读。&lt;/p&gt;

&lt;h3 id=&quot;insert-intention-locks&quot;&gt;Insert Intention Locks&lt;/h3&gt;
&lt;p&gt;先跳过吧。&lt;/p&gt;

&lt;h3 id=&quot;auto_inc-locks&quot;&gt;AUTO_INC locks&lt;/h3&gt;
&lt;p&gt;特殊的表锁，保证插入后获得的 key 是连续的。&lt;/p&gt;

&lt;h4 id=&quot;spatial-indexes&quot;&gt;Spatial Indexes&lt;/h4&gt;
&lt;p&gt;先跳过吧。&lt;/p&gt;

&lt;h4 id=&quot;后记&quot;&gt;后记&lt;/h4&gt;
&lt;p&gt;为什么这里一点 sql 都没有，因为我意识到，如果要用 sql 解释清楚这些锁，应该需要对事务也做更好的理解，毕竟锁是一个工具，最终还是服务于事务，到时候会弄清楚。这一篇文章只是起点，可能只是翻译，也没有说得很明白，但是后面会结合更实际的例子，将这部分的知识给沉淀下来，争取介绍得更充分一些。&lt;/p&gt;

&lt;h3 id=&quot;参考&quot;&gt;参考&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html&quot;&gt;innodb-locking&lt;/a&gt; 基本上都是这篇文章&lt;/p&gt;
</description>
        <pubDate>2022-12-30</pubDate>
        <link>https://uoryon.github.io/articles/2020/12/18/innodb-%E9%94%81%E4%BB%8B%E7%BB%8D.html</link>
        <guid isPermaLink="true">https://uoryon.github.io/articles/2020/12/18/innodb-%E9%94%81%E4%BB%8B%E7%BB%8D.html</guid>
        
        <category>database</category>
        
        <category>InnoDB</category>
        
        
      </item>
    
      <item>
        <title>解密列存 parquet</title>
        <description>&lt;p&gt;在做数据分析的时候，相对于传统关系型数据库，我们更倾向于计算列之间的关系。在使用传统关系型数据库时，基于此的设计，我们会扫描很多我们并不关心的列，这导致了查询效率的低下，大部分数据库 io 比较低效。因此目前出现了列式存储。&lt;a href=&quot;http://parquet.apache.org/&quot;&gt;Apache Parquet&lt;/a&gt; 是一个列式存储的文件格式。从这里入手，提升对列存的理解。当我还没看的时候，我还是很疑惑的，跟大家一样，向 hdfs 还是 lsmt 的设计，为了提升写入性能，都是使用 append 进行操作，而 append 如何将行式的数据转换成列式的数据呢？&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;parquet&quot;&gt;Parquet&lt;/h2&gt;
&lt;p&gt;在深入 parquet 之前，我们需要先了解一下后面需要学习的术语。&lt;/p&gt;

&lt;h3 id=&quot;术语&quot;&gt;术语&lt;/h3&gt;
&lt;h4 id=&quot;block&quot;&gt;Block&lt;/h4&gt;
&lt;p&gt;Block（hdfs block）：这是指 hdfs 的一个 block，parquet 运行在 hadoop 生态之中，文件的格式需要很好的与这些特征契合起来。
因为我之前也不知道这是什么，所以我们先来学习一下 Block 是啥。
Block 是 hdfs 中的最小的存储单元，使得其能将大文件切分为多个小文件，实现大文件的存储。并可以对多个小文件做合适的 replication，实现错误容忍（精髓）以及HA。
例如我们现在有一个文件一共是 518 MB，而设置的 hdfs block size 是 128 MB，那么它会由 5 个 block（128MB + 128MB + 128MB + 128MB + 6MB）来组成。所以我们处理数据的时候需要考虑同一行数据被写在两个不同的 block 上的情况，这里就不展开了（hdfs 细节还不是很清楚）。&lt;/p&gt;

&lt;h4 id=&quot;file&quot;&gt;File&lt;/h4&gt;
&lt;p&gt;一个 hdfs 文件，必定有 metadata。并不必须要数据。&lt;/p&gt;

&lt;h4 id=&quot;row-group&quot;&gt;Row Group&lt;/h4&gt;
&lt;p&gt;将我们的数据水平上的一个逻辑分区，按 mysql 来理解就是一些数据行，这些行组成一个 row group。这个 group 中包含数据集中每一列的 chunk（Column Chunk）。&lt;/p&gt;

&lt;h4 id=&quot;column-chunk&quot;&gt;Column Chunk&lt;/h4&gt;
&lt;p&gt;特定列的一大块数据。它存在于特定的一个 row group 中，并在物理上认为是连续的。&lt;/p&gt;

&lt;h4 id=&quot;page-column-chunk-中&quot;&gt;Page (column chunk 中)&lt;/h4&gt;
&lt;p&gt;column chunk 被划分为 pages。一个 page 认为是最小不可分割单元（就压缩和编码而言）。会有很多种 page type 在一个 column chunk 中交错存储。&lt;/p&gt;

&lt;p&gt;一个文件会由一个或者多个 row group 组成，每个 row group 对其中一列只会存在一个 column chunk。column chunk 中含有多个 pages。&lt;/p&gt;

&lt;h3 id=&quot;细节&quot;&gt;细节&lt;/h3&gt;
&lt;p&gt;基于以上的一些认知后，我们可以开始 Parquet 的细节描述了。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;4-byte magic number &quot;PAR1&quot;
&amp;lt;Column 1 Chunk 1 + Column Metadata&amp;gt;
&amp;lt;Column 2 Chunk 1 + Column Metadata&amp;gt;
...
&amp;lt;Column N Chunk 1 + Column Metadata&amp;gt;
&amp;lt;Column 1 Chunk 2 + Column Metadata&amp;gt;
&amp;lt;Column 2 Chunk 2 + Column Metadata&amp;gt;
...
&amp;lt;Column N Chunk 2 + Column Metadata&amp;gt;
...
&amp;lt;Column 1 Chunk M + Column Metadata&amp;gt;
&amp;lt;Column 2 Chunk M + Column Metadata&amp;gt;
...
&amp;lt;Column N Chunk M + Column Metadata&amp;gt;
File Metadata
4-byte length in bytes of file metadata
4-byte magic number &quot;PAR1&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;上面这是从官网上摘抄下来的例子，一共有 M 个 row group，数据集中一共有 N 列。每个 row group 之后有一项 column 的 metadata。
在文件的末尾，有记录上 File Metadata，这个 file metada 中记录着每个 column metadata 的位置。通过这个信息，可以提取出关心的列。&lt;/p&gt;

&lt;p&gt;这里附上两个图，都来自 Apache Parquet 官网的 document 里。就比较清晰数据结构了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/parquet/FileLayout.gif&quot; alt=&quot;FileLayout.gif&quot; /&gt;&lt;/p&gt;

&lt;p&gt;数据结构&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/parquet/FileFormat.gif&quot; alt=&quot;FileFormat.gif&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;深入&quot;&gt;深入&lt;/h3&gt;
&lt;p&gt;以上的讲解都是概念性的，有点难理解，我一开始也没理解，到底什么是列存。
接下来我们从实际的例子中来解密 parquet。&lt;/p&gt;

&lt;p&gt;parquet-cpp 原先是有自己的项目，后来迁移至 arrow 中了。我们用 arrow 的代码来学习过程。
代码使用的 commit 是 c29462c9&lt;/p&gt;

&lt;h4 id=&quot;写入&quot;&gt;写入&lt;/h4&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;// source code: https://github.com/apache/arrow/blob/master/cpp/examples/parquet/low-level-api

// 首先，书写 parquet 文件，需要先定义好对应的 parquet schema。这个 schema 用来描述我们的数据，数据有哪些列，每一列是什么样的类型。
std::shared_ptr&amp;lt;GroupNode&amp;gt; schema = SetupSchema();

// 设定写入的选项，压缩算法。
parquet::WriterProperties::Builder builder;
builder.compression(parquet::Compression::SNAPPY);
std::shared_ptr&amp;lt;parquet::WriterProperties&amp;gt; props = builder.build();

// 用定义好的 schema 和属性，创建写文件的对象实例
std::shared_ptr&amp;lt;parquet::ParquetFileWriter&amp;gt; file_writer =
    parquet::ParquetFileWriter::Open(out_file, schema, props);

// 对这个文件新增 row group，注意是 buffered 的，因为 row group 的 size，可以由我们指定，而这个参数之后会影响到读取的效率。在后文有一些讨论，现在就不深入了。
parquet::RowGroupWriter* rg_writer = file_writer-&amp;gt;AppendBufferedRowGroup();

// 后续针对每一行数据，进行对应 column 的写入。这个由于代码封装好了，使用起来也比较方便，就不细说了。想看具体如何使用的，可以去看看 c++ 的代码。
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;读取&quot;&gt;读取&lt;/h4&gt;
&lt;p&gt;文件结构的解析，我们在读取这一部分来阐述。而其中又以读 metadata 更为重要，那么解析一下读 metadata 的过程。&lt;/p&gt;

&lt;p&gt;// parquet/file_reader.cc:158&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;void ParseMetaData() {
  int64_t file_size = -1;
  PARQUET_THROW_NOT_OK(source_-&amp;gt;GetSize(&amp;amp;file_size));

  if (file_size == 0) {
    throw ParquetException(&quot;Invalid Parquet file size is 0 bytes&quot;);
  } else if (file_size &amp;lt; kFooterSize) {
    std::stringstream ss;
    ss &amp;lt;&amp;lt; &quot;Invalid Parquet file size is &quot; &amp;lt;&amp;lt; file_size
       &amp;lt;&amp;lt; &quot; bytes, smaller than standard file footer (&quot; &amp;lt;&amp;lt; kFooterSize &amp;lt;&amp;lt; &quot; bytes)&quot;;
    throw ParquetException(ss.str());
  }

  std::shared_ptr&amp;lt;Buffer&amp;gt; footer_buffer;
  int64_t footer_read_size = std::min(file_size, kDefaultFooterReadSize);
  PARQUET_THROW_NOT_OK(
      source_-&amp;gt;ReadAt(file_size - footer_read_size, footer_read_size, &amp;amp;footer_buffer));

  // Check if all bytes are read. Check if last 4 bytes read have the magic bits
  if (footer_buffer-&amp;gt;size() != footer_read_size ||
      memcmp(footer_buffer-&amp;gt;data() + footer_read_size - 4, kParquetMagic, 4) != 0) {
    throw ParquetException(&quot;Invalid parquet file. Corrupt footer.&quot;);
  }

  uint32_t metadata_len = *reinterpret_cast&amp;lt;const uint32_t*&amp;gt;(
      reinterpret_cast&amp;lt;const uint8_t*&amp;gt;(footer_buffer-&amp;gt;data()) + footer_read_size -
      kFooterSize);
  int64_t metadata_start = file_size - kFooterSize - metadata_len;
  if (kFooterSize + metadata_len &amp;gt; file_size) {
    throw ParquetException(
        &quot;Invalid parquet file. File is less than &quot;
        &quot;file metadata size.&quot;);
  }

  std::shared_ptr&amp;lt;Buffer&amp;gt; metadata_buffer;
  // Check if the footer_buffer contains the entire metadata
  if (footer_read_size &amp;gt;= (metadata_len + kFooterSize)) {
    metadata_buffer = SliceBuffer(
        footer_buffer, footer_read_size - metadata_len - kFooterSize, metadata_len);
  } else {
    PARQUET_THROW_NOT_OK(
        source_-&amp;gt;ReadAt(metadata_start, metadata_len, &amp;amp;metadata_buffer));
    if (metadata_buffer-&amp;gt;size() != metadata_len) {
      throw ParquetException(&quot;Invalid parquet file. Could not read metadata bytes.&quot;);
    }
  }
  file_metadata_ = FileMetaData::Make(metadata_buffer-&amp;gt;data(), &amp;amp;metadata_len);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;就是我们的主体了，从这里开始进行 metadata 的分析。跟上文中的例子一样，首先读文件末尾 4-byte 的 magic number，如果不是 “PAR1” 的话，会直接抛出错误，认为文件损坏了。再继续读出 metadata 的size，通过 size 解析出 metadata 的内容，有了这些 raw data，我们便可以将其序列化成 Metadata 了。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;// Metadata
/**
 * Description for file metadata
 */
struct FileMetaData {
  /** Version of this file **/
  1: required i32 version

  /** Parquet schema for this file.  This schema contains metadata for all the columns.
   * The schema is represented as a tree with a single root.  The nodes of the tree
   * are flattened to a list by doing a depth-first traversal.
   * The column metadata contains the path in the schema for that column which can be
   * used to map columns to nodes in the schema.
   * The first element is the root **/
  2: required list&amp;lt;SchemaElement&amp;gt; schema;

  /** Number of rows in this file **/
  3: required i64 num_rows

  /** Row groups in this file **/
  4: required list&amp;lt;RowGroup&amp;gt; row_groups

  /** Optional key/value metadata **/
  5: optional list&amp;lt;KeyValue&amp;gt; key_value_metadata

  /** String for application that wrote this file.  This should be in the format
   * &amp;lt;Application&amp;gt; version &amp;lt;App Version&amp;gt; (build &amp;lt;App Build Hash&amp;gt;).
   * e.g. impala version 1.0 (build 6cf94d29b2b7115df4de2c06e2ab4326d721eb55)
   **/
  6: optional string created_by

  /**
   * Sort order used for the min_value and max_value fields of each column in
   * this file. Each sort order corresponds to one column, determined by its
   * position in the list, matching the position of the column in the schema.
   *
   * Without column_orders, the meaning of the min_value and max_value fields is
   * undefined. To ensure well-defined behaviour, if min_value and max_value are
   * written to a Parquet file, column_orders must be written as well.
   *
   * The obsolete min and max fields are always sorted by signed comparison
   * regardless of column_orders.
   */
  7: optional list&amp;lt;ColumnOrder&amp;gt; column_orders;

  /**
   * Encryption algorithm. This field is set only in encrypted files
   * with plaintext footer. Files with encrypted footer store algorithm id
   * in FileCryptoMetaData structure.
   */
  8: optional EncryptionAlgorithm encryption_algorithm

  /**
   * Retrieval metadata of key used for signing the footer.
   * Used only in encrypted files with plaintext footer.
   */
  9: optional binary footer_signing_key_metadata
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;后续可以继续深入去看了，其实就是上面 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FileLayout.gif&lt;/code&gt; 那幅图。&lt;/p&gt;

&lt;p&gt;除了使用源代码来学习，我们还可以使用官方提供的工具 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;parquet-tools&lt;/code&gt; 来分析 parquet 文件。&lt;/p&gt;

&lt;p&gt;parquet-tool 有如下命令，命令的作用也很直白，就不展开介绍了。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cat
head
meta
schema
dump
merge
rowcount
size
column-index
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;下下来，本地使用一下即可。可以看到很多有用的信息，包括压缩率，Schema 等信息。
这里列部分上面我们写入的文件的 meta 信息，作为参考。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;file:          file:/xxx/arrow/cpp/examples/parquet/low-level-api/parquet_cpp_example2.parquet
creator:       parquet-cpp version 1.5.1-SNAPSHOT

file schema:   schema
--------------------------------------------------------------------------------
boolean_field: REQUIRED BOOLEAN R:0 D:0
int32_field:   REQUIRED INT32 L:TIME(MILLIS,true) R:0 D:0
int64_field:   REPEATED INT64 R:1 D:1
int96_field:   REQUIRED INT96 R:0 D:0
float_field:   REQUIRED FLOAT R:0 D:0
double_field:  REQUIRED DOUBLE R:0 D:0
ba_field:      OPTIONAL BINARY R:0 D:1
flba_field:    REQUIRED FIXED_LEN_BYTE_ARRAY R:0 D:0

row group 1:   RC:427502 TS:15645725 OFFSET:4
--------------------------------------------------------------------------------
boolean_field:  BOOLEAN SNAPPY DO:4 FPO:4 SZ:2550/53476/20.97 VC:427502 ENC:RLE,PLAIN ST:[min: false, max: true, num_nulls: 0]
int32_field:    INT32 SNAPPY DO:2615 FPO:1051257 SZ:2300579/2300478/1.00 VC:427502 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[min: 00:00:00.000+0000, max: 00:07:07.501+0000, num_nulls: 0]
int64_field:    INT64 SNAPPY DO:2303276 FPO:2827873 SZ:3706509/7227980/1.95 VC:855004 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[min: 0, max: 855003, num_nulls: 0]
int96_field:    INT96 SNAPPY DO:6009885 FPO:6621698 SZ:3179187/5316039/1.67 VC:427502 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[no stats for this column]
float_field:    FLOAT SNAPPY DO:9189128 FPO:10237777 SZ:2300592/2300478/1.00 VC:427502 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[min: -0.0, max: 470251.12, num_nulls: 0]
double_field:   DOUBLE SNAPPY DO:11489804 FPO:12529844 SZ:3690680/3699099/1.00 VC:427502 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[min: -0.0, max: 475001.1063611, num_nulls: 0]
ba_field:       BINARY SNAPPY DO:15180585 FPO:15244729 SZ:441270/608056/1.38 VC:427502 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[min: 0x70617271756574003030, max: 0x70617271756574FF3938, num_nulls: 213751]
flba_field:     FIXED_LEN_BYTE_ARRAY SNAPPY DO:15621935 FPO:15622985 SZ:24358/430982/17.69 VC:427502 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[min: 0x00000000000000000000, max: 0xFFFFFFFFFFFFFFFFFFFF, num_nulls: 0]

row group 2:   RC:427314 TS:15648394 OFFSET:15646373
--------------------------------------------------------------------------------
boolean_field:  BOOLEAN SNAPPY DO:15646373 FPO:15646373 SZ:2550/53453/20.96 VC:427314 ENC:RLE,PLAIN ST:[min: false, max: true, num_nulls: 0]
int32_field:    INT32 SNAPPY DO:15648992 FPO:16697641 SZ:2299840/2299726/1.00 VC:427314 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[min: 00:07:07.502+0000, max: 00:14:14.815+0000, num_nulls: 0]
int64_field:    INT64 SNAPPY DO:17948916 FPO:18473500 SZ:3705028/7224924/1.95 VC:854628 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[min: 855004, max: 1709631, num_nulls: 0]
int96_field:    INT96 SNAPPY DO:21654044 FPO:22265872 SZ:3177890/5313783/1.67 VC:427314 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[no stats for this column]
float_field:    FLOAT SNAPPY DO:24831990 FPO:25880639 SZ:2299840/2299726/1.00 VC:427314 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[min: 470252.22, max: 940296.5, num_nulls: 0]
double_field:   DOUBLE SNAPPY DO:27131914 FPO:28180563 SZ:3697785/3697596/1.00 VC:427314 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[min: 475002.2174722, max: 949794.4349465, num_nulls: 0]
ba_field:       BINARY SNAPPY DO:30829800 FPO:30893940 SZ:441108/607877/1.38 VC:427314 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[min: 0x70617271756574003030, max: 0x70617271756574FF3938, num_nulls: 213657]
flba_field:     FIXED_LEN_BYTE_ARRAY SNAPPY DO:31270988 FPO:31272038 SZ:24353/430797/17.69 VC:427314 ENC:RLE,PLAIN_DICTIONARY,PLAIN ST:[min: 0x00000000000000000000, max: 0xFFFFFFFFFFFFFFFFFFFF, num_nulls: 0]

row group 3:   RC:427302 TS:15648549 OFFSET:31295421
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;row-group-size-的选择&quot;&gt;row group size 的选择&lt;/h4&gt;
&lt;p&gt;row group size，这个指标会影响到我们的读取。因为 parquet 是 hadoop 生态下的产物，那么各项配置与 hadoop 契合起来，会有更大的威力。有如上文提到的 block，将每个 row group 的大小，设置为 block 的大小，则可以很好的提升 MR 程序的效率。至于 hdfs block 大小的调优，笔者刚接触这一块领域的知识，还没有详细的结论和见解，后续会有文章详细的阐述。&lt;/p&gt;

&lt;h3 id=&quot;结论&quot;&gt;结论&lt;/h3&gt;
&lt;p&gt;那么这下我们就搞清楚了，parquet 将很多行数据划分进不同的 row group 中去，在 row group 里，把每一列数据进行顺序编码，是连续的，这样分析这里列的时候就是顺序的了，可以极大的利用好系统 io，提升读取性能。&lt;/p&gt;

&lt;h2 id=&quot;reference&quot;&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://parquet.apache.org/documentation/latest/&quot;&gt;Apache Parquet Document&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/apache/arrow&quot;&gt;Arrow&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://hadoop.apache.org/docs/r1.2.1/hdfs_design.html&quot;&gt;hdfs design&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/apache/parquet-mr/tree/master/parquet-tools&quot;&gt;parquet-tools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>2022-12-30</pubDate>
        <link>https://uoryon.github.io/articles/2019/07/02/%E8%A7%A3%E5%AF%86%E5%88%97%E5%AD%98-parquet.html</link>
        <guid isPermaLink="true">https://uoryon.github.io/articles/2019/07/02/%E8%A7%A3%E5%AF%86%E5%88%97%E5%AD%98-parquet.html</guid>
        
        
        <category>data</category>
        
      </item>
    
      <item>
        <title>数据预测学习记录（一）</title>
        <description>&lt;p&gt;最近学习了一些数据预测相关的知识，随着预测服务的上线，开始做一轮记录，总结一下最近学习到的与数据预测相关的知识，
也介绍一下做预测目前用到的一些工具。前面也说一下，我处理的数据是时序数据。&lt;/p&gt;

&lt;!--more--&gt;

&lt;h3 id=&quot;time-series&quot;&gt;Time series&lt;/h3&gt;
&lt;p&gt;我现在处理的数据都是时序数据(Time series)，其特点是跟时间相关，一个时间点，有一个值。
举个例子就用我们互联网服务的带宽来描述好了，通常 CDN 厂商提供的最小粒度是 5 分钟，值的单位是 bps。
那么我们的数据就会长这样&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;2015-01-02T13:00:00 2345&lt;/li&gt;
  &lt;li&gt;2015-01-02T13:05:00 4095&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;时间序列还是挺规律的，每天服务的高峰低峰，每周也有高峰低峰，每日与每日，每周与每周，其中有一些相关性，
这让我们能够使用一些手段来预测这个值之后的变化。&lt;/p&gt;

&lt;h3 id=&quot;预测&quot;&gt;预测&lt;/h3&gt;
&lt;p&gt;关于预测，经过最近的学习，也改变了一些观点。刚开始接手的时候，想到，需要把各种因素给找出来，拟合出一个方程式，
有几个因素，就几元的公式，然后进行计算，就能得到那个点的值。比如带宽的因素，当时能想到的，dau，用户平均访问时长，
文件的大小，等等，元素找出来，再进行多项式拟合，找到合适的参数和阶数，然后就能用这些数据进行预测了。
了解调研过后发现，并不是这样的了。&lt;/p&gt;

&lt;p&gt;先从最基本的说起，什么是可以预测的，什么是不能预测的呢。
抛硬币。这是一个很普通的例子了，统计学中都会出现的东西，很显然，这个东西是无法预测的，也就是说，
你不能按照前一次抛的硬币是正面还是反面，推测出下一次硬币是正面或者反面。因为无论怎么抛，下一次出现正面反面的概率都是相等的，
除非这不是一个标准的硬币。而当一个数据，它们有时间上的相互关系，并且有一个模式，我们就能预测它。如同一年的降雨量，
一个城市是有规律的。&lt;/p&gt;

&lt;h4 id=&quot;预测的基本步骤&quot;&gt;预测的基本步骤&lt;/h4&gt;
&lt;ol&gt;
  &lt;li&gt;定义问题&lt;/li&gt;
  &lt;li&gt;获取信息&lt;/li&gt;
  &lt;li&gt;初步分析数据&lt;/li&gt;
  &lt;li&gt;选择模型和测试模型&lt;/li&gt;
  &lt;li&gt;检验模型。&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;预测的方式&quot;&gt;预测的方式&lt;/h4&gt;
&lt;h5 id=&quot;通过预测变量&quot;&gt;通过预测变量&lt;/h5&gt;

&lt;p&gt;这种就比较传统了，跟上面说的一样，类似的公式是：&lt;/p&gt;

&lt;p&gt;ExpectedValue = f(currentValue, factor1, factor2, factor3, factor4…)&lt;/p&gt;

&lt;p&gt;需要找到对应的预测变量，并要剔除掉关系不大的预测变量。这种叫做解释性模型，挺好用的，
因为这些因素很好的说明了值是如何做出来的，模型是如何工作。&lt;/p&gt;

&lt;h5 id=&quot;通过时序数据&quot;&gt;通过时序数据&lt;/h5&gt;

&lt;p&gt;ExpectedValue(t) = f(value(t-1), value(t-2), value(t-3), …)&lt;/p&gt;

&lt;p&gt;这就是从时序数据自身来推断后续的数据了。&lt;/p&gt;

&lt;h5 id=&quot;混合模型&quot;&gt;混合模型&lt;/h5&gt;

&lt;p&gt;将上面两种结合起来做。&lt;/p&gt;

&lt;p&gt;然而，我们通常是使用时序数据来做预测，而不是其他两种方式。有几点理由：&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;系统可能是无法理解的，或者说我们很难找出得到这种变化的因素间的关系&lt;/li&gt;
  &lt;li&gt;有必要知道或者预测在未来预测变量的值是多少，这个就很困难了。预测依赖预测。&lt;/li&gt;
  &lt;li&gt;我们更重要的是要知道这个数据是什么，而不是为什么数据是这样。&lt;/li&gt;
  &lt;li&gt;时序数列模型预测，能给出更为精确的值。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;实践&quot;&gt;实践&lt;/h3&gt;

&lt;p&gt;针对上面这几点，我们开始通过时序数据做预测。慢慢说一些预测的方式。&lt;/p&gt;

&lt;h4 id=&quot;一些简单的&quot;&gt;一些简单的&lt;/h4&gt;
&lt;h5 id=&quot;平均法&quot;&gt;平均法&lt;/h5&gt;
&lt;p&gt;前 n 个数据取均值，作为预测的数据。这种对于有趋势的数据就没什么太大的作用&lt;/p&gt;

&lt;h5 id=&quot;naive-法&quot;&gt;Naive 法&lt;/h5&gt;
&lt;p&gt;用最新的点当作预测的&lt;/p&gt;

&lt;h5 id=&quot;seasonal-naive-法&quot;&gt;Seasonal naive 法&lt;/h5&gt;
&lt;p&gt;按照数据的特征，取上一个周期的点作为预测值。&lt;/p&gt;

&lt;h5 id=&quot;drift-法&quot;&gt;Drift 法&lt;/h5&gt;
&lt;p&gt;将上一个点，加上之前变化的均值作为预测值。&lt;/p&gt;

&lt;h4 id=&quot;稍微复杂一些的&quot;&gt;稍微复杂一些的&lt;/h4&gt;
&lt;h5 id=&quot;移动平均&quot;&gt;移动平均&lt;/h5&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Moving_average&quot;&gt;MovingAverage&lt;/a&gt;&lt;/p&gt;
&lt;h5 id=&quot;指数平滑&quot;&gt;指数平滑&lt;/h5&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Exponential_smoothing&quot;&gt;Exponential smoothing&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;新的方式&quot;&gt;新的方式&lt;/h4&gt;
&lt;p&gt;通过深度学习，神经网络进行数据预测&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;lstm&lt;/li&gt;
  &lt;li&gt;seq2seq&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;数据分解&quot;&gt;数据分解&lt;/h4&gt;
&lt;p&gt;时间数据，那么就天然的有一些季节性的特征。通常会分成三个部分。&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Trend&lt;/li&gt;
  &lt;li&gt;Seasonal&lt;/li&gt;
  &lt;li&gt;Cyclic&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;一些概念&quot;&gt;一些概念&lt;/h4&gt;
&lt;h5 id=&quot;强弱平稳&quot;&gt;强/弱平稳&lt;/h5&gt;
&lt;p&gt;通常我们用一些模型类似 ARIMA 做时序数列的预测，需要其数据是平稳的。平稳简单的说就是数据的变化不会存在趋势。
这个平稳性是有数学定义的。
要求数据平稳，也就是说，我们希望其中的某些性质，不会随着时间的变化而变化。也就是这个数据最本质的特征也延续到未来，
相反，如果这个数据的性质在未来不会发生，对我们的预测工作就没有意义了。
强平稳很难达到，所以我们一般都是弱平稳。
这个数据指标，可以只用 acf 来计算出来。即自己与自己的相关变量。&lt;/p&gt;

&lt;p&gt;很自然我们的数据肯定是有趋势的。我们可以针对这份数据进行变换，得到平稳的数据。&lt;/p&gt;

&lt;h5 id=&quot;差分&quot;&gt;差分&lt;/h5&gt;
&lt;p&gt;差分，diff，也可以理解为 delta。例如一个数据，从 1 慢慢的涨到 10，step 是 10。那么一阶差分，就是 1 的序列。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

diff(1): [1, 1, 1, 1, 1, 1, 1, 1, 1]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;这个就是我们上面提到的一个特征（平稳）。每次都 + 1 的这个特征，会延续到未来，我们就能利用这个数据判断出，
下一个预测值很大的概率上是 11。&lt;/p&gt;

&lt;h3 id=&quot;我现在用的一些方法&quot;&gt;我现在用的一些方法。&lt;/h3&gt;

&lt;h4 id=&quot;arima&quot;&gt;ARIMA&lt;/h4&gt;
&lt;p&gt;这个可以从我上一篇文章看到，不过有一些不一样，经过我的试验。我发现我的数据按照周来划分，得到的结果要比按天划分的，
要好得多。&lt;/p&gt;

&lt;h4 id=&quot;prophet&quot;&gt;Prophet&lt;/h4&gt;
&lt;p&gt;这是 facebook 开源的工具，&lt;a href=&quot;http://facebook.github.io/prophet/&quot;&gt;prophet&lt;/a&gt;，
挺好用的。一开始的时候我把这个工具应用在预测变量上，再用个预测变量计算出预测值。
数据不是很好，因为当时还没理解直接用时序数据预测的方式。直接用来预测，发现预测的曲线比较平滑，一些极值的点判断不好，
还得多看看吧。&lt;/p&gt;

&lt;h4 id=&quot;lstm&quot;&gt;LSTM&lt;/h4&gt;
&lt;h4 id=&quot;seq2seq&quot;&gt;seq2seq&lt;/h4&gt;
</description>
        <pubDate>2022-12-30</pubDate>
        <link>https://uoryon.github.io/articles/2018/11/30/%E6%95%B0%E6%8D%AE%E9%A2%84%E6%B5%8B%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%951.html</link>
        <guid isPermaLink="true">https://uoryon.github.io/articles/2018/11/30/%E6%95%B0%E6%8D%AE%E9%A2%84%E6%B5%8B%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%951.html</guid>
        
        
        <category>data</category>
        
      </item>
    
      <item>
        <title>时序数据预测及异常检测</title>
        <description>&lt;p&gt;最近在做数据相关的事情。把我们线上的一个指标拉了下来。因为我们用到的都是第三方的服务，数据都维护在第三方。一个一个三方去看，还是比较麻烦的，也不好进行比对所以我们就全拉了回来，方便一起统计观看，同时也可以对这些数据利用起来，包括调度。&lt;/p&gt;

&lt;p&gt;对这些数据的利用，最基本的是监控，其次需要针对这些数据做预测，其中一个点是需要做成本管理，预估之类的工作，第二点是要做异常检测。&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;异常检测也算是在探索之中吧，博主之前完全没有搞过，好多统计学的知识学校里也没有
学得很好。之后会有陆陆续续的文章，权当学习记录。&lt;/p&gt;

&lt;p&gt;互联网的数据，特别是业务数据，特征性还是非常明显的。例如一个社交应用的访问量，一天之中，从早上起床开始，慢慢变多，直到中午的时候，迎来一个午高峰，随后到下午的时候，数据又渐渐的下降，晚饭后慢慢爬升至晚高峰，等到开始睡觉了，又慢慢降低下来。这是一天之内的变化。一周的变化也很明显，周末比工作日高。可以看到，我们的数据的季节性非常明显。调研到，现在有针对时序数据分解的方法&lt;a href=&quot;https://en.wikipedia.org/wiki/Decomposition_of_time_series&quot;&gt;Decomposition_of_time_series&lt;/a&gt;，于是采用了这种方式针对我们的数据进行了处理及预测，搭配 arima 数据的预测以及报警。&lt;/p&gt;

&lt;p&gt;我们使用 python 来操作，还是比较轻松的。&lt;/p&gt;

&lt;h4 id=&quot;1-准备数据&quot;&gt;1. 准备数据&lt;/h4&gt;
&lt;p&gt;我们的数据，现在全部收集在 InfluxDB 里面。找出来画个图:
&lt;img src=&quot;/assets/timeseries/stl_arima/2w-data.png&quot; alt=&quot;2w-data&quot; /&gt;
我们之后就基于这个数据来操作，做数据预测，以及异常检测。&lt;/p&gt;

&lt;h4 id=&quot;2-将数据进行-stl-分解&quot;&gt;2. 将数据进行 stl 分解&lt;/h4&gt;
&lt;p&gt;stl 会将我们的数据分为趋势分量，季节分量和余量。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;import pandas
from statsmodels.tsa.seasonal import seasonal_decompose
import matplotlib.pyplot as plt

decomposition = seasonal_decompose(pandas.Series([x[1] for x in data]),
  freq=288, two_sided=False) # 数据粒度是 5 分钟，一天的数据是 288 个点
decomposition.plot()
plt.show()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src=&quot;/assets/timeseries/stl_arima/2w-data-stl.png&quot; alt=&quot;2w-data-stl&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Observed 是我们现在的数据。&lt;/p&gt;

&lt;p&gt;Seasonal 是分解出来的季节分量，可以看到分解出来的数据，与我们之前描述的数据相当相似。&lt;/p&gt;

&lt;p&gt;Trend 就是趋势分量了，看到高峰是我们的周末，低峰是平时。我们针对这一项数据进行后续数据的预测。&lt;/p&gt;

&lt;p&gt;Residual 是季节的余量。通过余量的范围来制定我们数据的区间范围。&lt;/p&gt;

&lt;h4 id=&quot;3-通过-acfpacf-确认我们的-arima-模型&quot;&gt;3. 通过 acf，pacf 确认我们的 ARIMA 模型&lt;/h4&gt;
&lt;p&gt;我们的数据刚开始并不是若平稳的，需要将数据变成平稳的，那么就需要做差分。我的数据做了二次差分之后，变得平稳。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;import matplotlib.pyplot as plt
import statsmodels.api as sm

diff1 = trend.diff()
diff2 = diff1.diff()

diff2 = diff2.dropna() # 做了差分后，之前的数据会变成 na。需要 drop 掉才能画图

fig = plt.figure(figsize=(12,8))
ax1 = fig.add_subplot(211)
fig = sm.graphics.tsa.plot_acf(diff2, lags=40, ax=ax1)
ax2 = fig.add_subplot(212)
fig = sm.graphics.tsa.plot_pacf(diff2, lags=40, ax=ax2)
plt.show()
确认模型为 (7, 2, 3)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;图这里就不展示了。&lt;/p&gt;

&lt;h4 id=&quot;4-针对余项确认置信区间&quot;&gt;4. 针对余项确认置信区间&lt;/h4&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;d = residual.describe()
delta = d['75%'] - d['25%'] # 置信区间
low_error = d['25%'] - 1 * delta
high_error = d['75%'] + 1 * delta
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;5-按照这个数据来画图&quot;&gt;5. 按照这个数据来画图&lt;/h4&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;model = ARIMA(trend.dropna(), (7, 2, 3)).fit(disp=-1, method='css')
new_data = model.forecast(288)[0] # 预测之后 288 个点（一天的数据）
season_data = decomposition.seasonal

values = []
low_conf_values = []
high_conf_values = []

for i , trend_part in enumerate(new_data):
  season_part = season_data[season_data.index % 288 == i].mean()
  predict = trend_part + season_part
  low_bound = trend_part + season_part + low_error
  high_bound = trend_part + season_part + high_error
  values.append(predict)
  low_conf_values.append(low_bound)
  high_conf_values.append(high_bound)

plt.plot(values, color='green')
plt.plot(low_conf_values, color='red')
plt.plot(high_conf_values, color='blue')

plt.plot(real_values, color='pink')
plt.show()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src=&quot;/assets/timeseries/stl_arima/predict_value.png&quot; alt=&quot;predict_value&quot; /&gt;&lt;/p&gt;

&lt;p&gt;可以看到还是挺靠谱的。
最后通过计算均方根误差，得到误差大约 5 %。还是可以的。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;目前对时序数据分析概念也不是很深，基本都是参考网上前人的经验在做，很多地方多不会，原理也不是很清楚，这篇文章就当一个试水，之后会带来技术向，讲原理，提升感受的文章。&lt;/p&gt;

&lt;p&gt;参考：
&lt;a href=&quot;https://zhuanlan.zhihu.com/p/33695908&quot;&gt;用Python预测「周期性时间序列」的正确姿势&lt;/a&gt; 基本都是按照这篇博文来做的。&lt;/p&gt;
</description>
        <pubDate>2022-12-30</pubDate>
        <link>https://uoryon.github.io/articles/2018/11/21/%E6%97%B6%E5%BA%8F%E6%95%B0%E6%8D%AE%E9%A2%84%E6%B5%8B%E5%8F%8A%E5%BC%82%E5%B8%B8%E6%A3%80%E6%B5%8B.html</link>
        <guid isPermaLink="true">https://uoryon.github.io/articles/2018/11/21/%E6%97%B6%E5%BA%8F%E6%95%B0%E6%8D%AE%E9%A2%84%E6%B5%8B%E5%8F%8A%E5%BC%82%E5%B8%B8%E6%A3%80%E6%B5%8B.html</guid>
        
        
        <category>timeseries</category>
        
      </item>
    
      <item>
        <title>Golang sql 的小 feature</title>
        <description>&lt;p&gt;最近都是在写 cms 后端，跟之前写容器调度系统，区别还是超级大的，面向的东西不一样了。发现 cms 后端，也就是 web
后端，还是蛮考察人的抽象能力的，可能比不上系统应用那样的性能以及效率。但是为了开发效率，以及抽象，还是很考察人的代码功底的。但是也没说意味着要牺牲性能。&lt;/p&gt;

&lt;!--more--&gt;

&lt;h3 id=&quot;db&quot;&gt;db&lt;/h3&gt;
&lt;p&gt;写 web 后端，很大几率都会与 db 交互，使用 sql。那么就会使用到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;database/sql&lt;/code&gt; 这个包。可能很多开发者都倾向于使用一些 ORM。ORM 当然会省很多力气。但是掌握一些神奇的技巧的话，可以变得很优雅。
比如，我们要写一个表，举例子通常都是&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;CREATE TABLE `Person` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` TEXT NOT NULL,
  `email` TEXT NOT NULL,
  PRIMARY KEY (`id`)
)Engine=InnoDB DEFAULT CHARSET=utf8mb4;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;相当简单的样子，如果只是这种程度，我们直接定义一个&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;type Person {
  ID    uint64 `db:&quot;id&quot;`
  Name  string `db:&quot;name&quot;`
  Email string `db:&quot;email&quot;`
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;即可，直接用 ORM 的一些 Get 方法，就能很轻松的处理了。不过我们不会止步于此，我们向更复杂的结构前进。倘若数据表，发生了这样的改进：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ALTER TABLE `Person` ADD COLUMN `permission` TEXT NOT NULL;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;permission&lt;/code&gt;中，存储的不是普通的字符串，而是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json&lt;/code&gt;。那么我们的 struct 应该如何做？变成这种吗？&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;type Person {
  ID         uint64 `db:&quot;id&quot;`
  Name       string `db:&quot;name&quot;`
  Email      string `db:&quot;email&quot;`
  Permission string `db:&quot;permission&quot;`
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;然后每次输出的时候，再对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;permission&lt;/code&gt; 做&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json.Unmarshal&lt;/code&gt;操作吗？&lt;/p&gt;

&lt;p&gt;显然不应该这样，这样复用性也不好，多加了一个复杂字段，就要如此改动，显然不可行呀。会多出很多用于数据处理的代码。很棒的是，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;golang&lt;/code&gt;有一些神奇的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interface&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&quot;scanner&quot;&gt;Scanner&lt;/h3&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;// Scanner is an interface used by Scan.
type Scanner interface {
	// Scan assigns a value from a database driver.
	//
	// The src value will be of one of the following types:
	//
	//    int64
	//    float64
	//    bool
	//    []byte
	//    string
	//    time.Time
	//    nil - for NULL values
	//
	// An error should be returned if the value cannot be stored
	// without loss of information.
	Scan(src interface{}) error
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;这个就是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Scanner&lt;/code&gt;接口了。需要实现一个 Scan。这个接口是用于从 db 读出到内存中用的，基础的数据结构，都能直接从 db 中读出来，那么复杂的结构，我们只用实现 Scan 接口，就可以工作了。
按上面的结构举例子，我们可以实现一个结构。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;type Permission struct {
  Read  bool
  Write bool
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;然后我们再针对这个结构实现 Scanner 接口即可。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;func (p *Permission) Scan(src interface{}) error {
  var data []byte
  switch src.(type) {
  case string:
    data = []byte(src.(string))
  case []byte:
    data = src.([]byte)
  default:
    return fmt.Errorf(&quot;Incompatible type for Permission val: %v&quot;, src)
  }

  err := json.Unmarshal(data, p)
  if err != nil {
    return fmt.Errorf(&quot;Permission Scan Unmarshal data: %v err: %v&quot;, data, err)
  }
  return nil
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;代码很简单。&lt;/p&gt;

&lt;h3 id=&quot;valuer&quot;&gt;Valuer&lt;/h3&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;// Valuer is the interface providing the Value method.
//
// Types implementing Valuer interface are able to convert
// themselves to a driver Value.
type Valuer interface {
	// Value returns a driver Value.
	Value() (Value, error)
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;就是实现与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Scanner&lt;/code&gt; 相反的过程，这样理解应该没什么问题。
这样，我们能够将内存中的对象，转化为数据库中存储的格式。
针对我们的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Permission&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;func (p Permission) Value() (driver.Value, error) {
  b, err := json.Marshal(p)
  if err != nil {
    return nil, fmt.Errorf(&quot;Marshal %v err: %v&quot;, p, err)
  }
  return string(b), err
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样就可以了。&lt;/p&gt;

&lt;h3 id=&quot;一些坑&quot;&gt;一些坑&lt;/h3&gt;
&lt;p&gt;json 和 go 结合的话，其实有一些坑。&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;默认类型
假设承接 json 的对象结构是，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;map[string]interface{}&lt;/code&gt;，那么可要注意 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interface{}&lt;/code&gt; 了。如果原先的值是数字，那么读回来后，这个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interface{}&lt;/code&gt; 的 type 是
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float64&lt;/code&gt;。这个时候，我们选择使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;encoding/gob&lt;/code&gt;就好。这个序列化的时候是保存类型的。（此处的考虑未考虑性能，只考虑可行，因为具体的性能我也没做过测试。）&lt;/li&gt;
&lt;/ol&gt;
</description>
        <pubDate>2022-12-30</pubDate>
        <link>https://uoryon.github.io/articles/2017/11/03/go-cms.html</link>
        <guid isPermaLink="true">https://uoryon.github.io/articles/2017/11/03/go-cms.html</guid>
        
        
        <category>golang</category>
        
      </item>
    
      <item>
        <title>Golang interface</title>
        <description>&lt;p&gt;Golang 的 interface(接口) 是一个很神奇的东西，定义为：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;接口类型是由一组方法定义的集合&lt;/li&gt;
  &lt;li&gt;接口类型的值可以存放实现这些方法的任何值&lt;/li&gt;
&lt;/ul&gt;

&lt;!--more--&gt;

&lt;p&gt;也就是说，只要一个值实现了这个接口的方法，那么他就是这个接口。最简单的例子是一个空的接口&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interface{}&lt;/code&gt;。因为这个接口是空的，
那么所有的类型，都可以认为是实现了这个接口，那么他就是这个接口。所以函数&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;func foo(a interface{}) {
  return
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;是可以接受任何类型的值作为参数的。
定义接口的语法如下：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;type fooInterface interface{
  Foo() string
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样，我们就算定义了一个接口，这个接口需要有方法&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Foo() string&lt;/code&gt;。我们只用定义一个 struct&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;type foo struct{}

func (f foo) Foo() string {
  return &quot;foo&quot;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;这样，这个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foo&lt;/code&gt; 就算实现了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fooInterface&lt;/code&gt; 这个接口。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;func(a fooInterface) {
  fmt.Println(&quot;ok&quot;)
}(&amp;amp;foo{}) // &quot;ok&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;接口在很多地方都可以应用，比如抽象一个数据库数据对象，或者写一个功能的多种算法实现。都是非常简便的方法。&lt;/p&gt;

&lt;h2 id=&quot;接口反推类型&quot;&gt;接口反推类型&lt;/h2&gt;
&lt;p&gt;有的时候我们需要通过接口来反推类型。举个例子，比如用 yaml 写了一个文件。你需要解析它，读取数据。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;	func Unmarshal(in []byte, out interface{}) (err error)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Unmarshal 的接口长这样，out 就是你要传递进去的参数，通常，你可以直接定义一个接口体，传进去，就像 yaml 给出的例子。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;type T struct {
    F int `yaml:&quot;a,omitempty&quot;`
    B int
}
var t T
yaml.Unmarshal([]byte(&quot;a: 1\nb: 2&quot;), &amp;amp;t)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;但是有时候，数据比较多样化，结构顶部下来的时候，就可以使用接口 + type assertion 的方式来做了。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;var t interface{}
yaml.Unmarshal([]byte(&quot;a: 1\nb: 2&quot;), &amp;amp;t)
fmt.Println(t) // map[a:1 b:2]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们可以看到，打印出来了一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;map[a:1 b:2]&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;k := t.(map[string]interface{})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;上面这种就是 type assertion 了，就得到了一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;map[string]interface{}&lt;/code&gt; 类型的数据了。需要注意的是，如果类型不匹配的话，
程序是会 panic 的。为了避免 panic&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;k, ok := t.(map[string]interface{})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;加上一个 ok 就可以了。通过判断 ok 的值，可以判断是否成功。&lt;/p&gt;

&lt;h2 id=&quot;embedded--interface&quot;&gt;embedded + interface&lt;/h2&gt;
&lt;p&gt;golang 的 struct 的 field 可以是 embeded 的。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;type bar struct {
  foo
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;通过 embedded 引入 struct 的成员比较有意思，如果 embedded 实现了一个借口，那么这个 struct 同样实现了这个接口。这个特性也比较有意思。
灵活使用的话，可以省很多事情。&lt;/p&gt;

&lt;hr /&gt;
&lt;p&gt;介绍就这样了，这一周写 cms 用到的一些特性。很久没写上层的业务，发现上层的业务还是蛮考验代码功底的。&lt;/p&gt;
</description>
        <pubDate>2022-12-30</pubDate>
        <link>https://uoryon.github.io/articles/2017/10/13/go-interface.html</link>
        <guid isPermaLink="true">https://uoryon.github.io/articles/2017/10/13/go-interface.html</guid>
        
        
        <category>golang</category>
        
      </item>
    
  </channel>
</rss>
