tag:tonyhan.dev,2014:/feed
Tony Han
2020-04-18T21:40:41-07:00
Tony Han
http://tonyhan.dev
[email protected]
Svbtle.com
tag:tonyhan.dev,2014:Post/what-is-ssd-trim-and-why-use-it
2020-04-18T21:40:41-07:00
2020-04-18T21:40:41-07:00
What's SSD TRIM and why use it?
<p>最近我们的一个 Mongo cluster 总是在周末的时候有大量的 IO 操作,并严重影响了 Mongo 的性能。然后发现问题出现的时候总是会执行一个 cron job – fstrim:</p>
<pre><code class="prettyprint">$ cat /etc/crontab | grep weekly
47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
$ cat /etc/cron.weekly/fstrim
#!/bin/sh
# trim all mounted file systems which support it
/sbin/fstrim --all || true
</code></pre>
<p>这是在 Ubuntu 16.04 上默认开启的(注意在 18.04 上变成了 systemd 来驱动,而不是 crontab),从 <a href="proxy.php?url=https://man7.org/linux/man-pages/man8/fstrim.8.html" rel="nofollow">man</a> 可以看到 fstrim 是用来清除文件系统上,特别是 SSD 不用的 block 的,并可以延长 SSD 的寿命。</p>
<p>通过 iostat 可以看到,fstrim 运行的时候,Mongo 的 IO write 数量增长了几千倍:</p>
<pre><code class="prettyprint">$ sudo iostat 3
Linux 4.4.0-1105-aws (mongo-03) 04/13/2020 _x86_64_ (64 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
3.27 0.00 0.58 0.18 0.00 95.97
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
loop0 0.00 0.00 0.00 0 0
xvda 0.00 0.00 0.00 0 0
xvdd 0.00 0.00 0.00 0 0
xvdb 0.00 0.00 0.00 0 0
nvme1n1 231.67 2488.00 560.00 7464 1680
nvme0n1 245.33 2478.67 561.33 7436 1684
nvme2n1 241.33 2634.67 637.33 7904 1912
nvme5n1 243.67 2472.00 617.33 7416 1852
nvme3n1 238.33 2493.33 525.33 7480 1576
nvme4n1 235.67 2457.33 529.33 7372 1588
nvme6n1 227.67 2248.00 640.00 6744 1920
nvme7n1 228.67 2198.67 557.33 6596 1672
md0 1931.33 19478.67 4628.00 58436 13884
avg-cpu: %user %nice %system %iowait %steal %idle
3.39 0.00 0.49 0.14 0.00 95.98
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
loop0 0.00 0.00 0.00 0 0
xvda 0.33 0.00 2.67 0 8
xvdd 0.00 0.00 0.00 0 0
xvdb 0.00 0.00 0.00 0 0
nvme1n1 251.33 2685.33 544.00 8056 1632
nvme0n1 256.67 2518.67 715.00 7556 2145
nvme2n1 243.33 2600.00 548.00 7800 1644
nvme5n1 256.00 2454.67 570.83 7364 1712
nvme3n1 238.00 2630.67 544.00 7892 1632
nvme4n1 218.33 2296.00 545.33 6888 1636
nvme6n1 256.67 2670.67 640.33 8012 1921
nvme7n1 270.33 2545.33 732.00 7636 2196
md0 2027.67 20408.00 4839.50 61224 14518
# after fstrim is called
avg-cpu: %user %nice %system %iowait %steal %idle
1.63 0.00 1.34 9.80 0.00 87.23
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
loop0 0.00 0.00 0.00 0 0
xvda 2.67 0.00 17.33 0 52
xvdd 0.00 0.00 0.00 0 0
xvdb 3.33 0.00 369.67 0 1109
nvme1n1 910.67 360.00 7540296.00 1080 22620888
nvme0n1 887.67 330.67 7400742.67 992 22202228
nvme2n1 892.33 336.00 7392044.00 1008 22176132
nvme5n1 890.00 293.33 7397830.67 880 22193492
nvme3n1 932.33 377.33 7653155.50 1132 22959466
nvme4n1 956.00 352.00 7917478.67 1056 23752436
nvme6n1 889.67 298.67 7421049.33 896 22263148
nvme7n1 892.67 293.33 7395156.00 880 22185468
md0 157869.00 2641.33 80663394.50 7924 241990183
avg-cpu: %user %nice %system %iowait %steal %idle
0.49 0.00 0.63 4.84 0.00 94.04
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
loop0 0.00 0.00 0.00 0 0
xvda 0.33 1.33 0.00 4 0
xvdd 0.00 0.00 0.00 0 0
xvdb 0.00 0.00 0.00 0 0
nvme1n1 997.00 17.33 8663381.33 52 25990144
nvme0n1 998.33 0.00 8689493.33 0 26068480
nvme2n1 1000.00 2.67 8686761.33 8 26060284
nvme5n1 999.67 6.67 8675158.67 20 26025476
nvme3n1 999.33 4.00 8678128.50 12 26034385
nvme4n1 923.00 8.00 8007872.00 24 24023616
nvme6n1 999.67 6.67 8683810.67 20 26051432
nvme7n1 997.67 5.33 8680789.33 16 26042368
md0 78771.67 96.00 40321945.83 288 120965837
</code></pre>
<p>另外,还发现我们挂载磁盘的时候,已经通过 <code class="prettyprint">discard</code> 启用了 TRIM:</p>
<pre><code class="prettyprint">$ findmnt -O discard
TARGET SOURCE FSTYPE OPTIONS
/ /dev/xvda1 ext4 rw,relatime,discard,data=ordered
└─/mnt/data /dev/md0 xfs rw,noatime,attr2,discard,nobarrier,inode64,sunit=1024,swidth=8192,noquota
</code></pre>
<p>磁盘的 <code class="prettyprint">discard</code> option 叫做 “Continuous TRIM” 或者 “Online discard",而 fstrim 也叫做 "Periodic TRIM” 或 “Batch discard"。</p>
<p>从 <a href="proxy.php?url=https://wiki.archlinux.org/index.php/Solid_state_drive" rel="nofollow">https://wiki.archlinux.org/index.php/Solid_state_drive</a> and <a href="proxy.php?url=https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/storage_administration_guide/ch02s04" rel="nofollow">https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/storage_administration_guide/ch02s04</a> 看来,应该使用 TRIM,并且 periodic TRIM 更好。</p>
<p>fstrim 的 mannul 也说</p>
<blockquote>
<p>Running fstrim frequently, or even using mount -o discard, might negatively affect the lifetime of poor-quality SSD devices. For most desktop and server systems the sufficient trimming frequency is once a week.</p>
</blockquote>
<p>本质上这两种 TRIM 做的事情都一样,只是频次不太一样。而因为 TRIM 有不小的副作用,所以运行过于频繁,副作用也更大。</p>
<p>那 TRIM 是什么呢?又是为什么会影响 SSD 寿命呢?</p>
<h2 id="how-ssd-write-works_2">How SSD write works <a class="head_anchor" href="proxy.php?url=#how-ssd-write-works_2" rel="nofollow">#</a>
</h2>
<p>SSD 在写数据的时候和 HHD 不同的一点是,SSD 不能直接重写一块区域的数据,而是需要先 erase,才能重新写。读写是按 page,比如 4KB,而 erase 是以 block 为单位,一个 block 一般有上百个 page,并且一块 SSD 只能够被擦除有限次数(比如10000次)。SSD 在重写数据的时候,一般会写到新的 page,在删除数据的时候,只是在 OS 做了标记,SSD 并不会实际删除数据(HHD 这点其实也是一样),所以当 SSD 空间不够的时候,OS 就可能会需要重写在已经被删除的地方,所以就需要 erase。而 erase 可能是一个比较“重”的操作,会影响性能。所以随着 SSD 空闲的空间越来越少,性能就会越差。</p>
<p>举个例子来说明一下 SSD 写数据和擦除的过程。我们假设一个 page 是 4KB,一个 block 是5个 page,并且当前 SSD 只剩下1个 block。然后先写入了一个 4K(1 page)的文件 A,再写入了一个8K(2 page)的文件B,随后又删除了文件A,注意这时 SSD 并没有把第一个 page 删除。最后我们写了一个 12KB(3 page) 的文件C,OS 因为知道第一个 page 已经被删除,所以会告诉 SSD 来擦除这个 page。SSD 因为要按 block 来擦除,所以要先把当前的数据放到 cache 中,然后删掉第一个 page,再把文件 C 写到空余的3个 page 中。所以为了写文件C 的12KB,我们需要先读12KB,再写20KB,由于产生了写放大,所以比平时慢不少。</p>
<p>所以如果我们有更多的空间,就能避免擦除,来提高写性能。这也是很多 SSD 一开始做格式化的时候,会留一部分空间不用。</p>
<h2 id="trim_2">TRIM <a class="head_anchor" href="proxy.php?url=#trim_2" rel="nofollow">#</a>
</h2>
<p>OS 可以通过 TRIM 命令来告诉 SSD 哪些数据已经被删除了,这样 SSD 就可以在 <a href="proxy.php?url=https://en.wikipedia.org/wiki/Write_amplification#BG-GC" rel="nofollow">GC</a> 时做优化,从而减少擦除的次数,并且减少在写数据时的擦除,从而提高写性能。</p>
<p>可以看到即使是 TRIM 也不能完全消除 SSD 擦除的操作,只是有所改善。</p>
<p>而 batch TRIM 更好的原因是在 TRIM 的早期实现中是 non-queued 的,会等当前操作完全,然后触发 TRIM,再恢复正常响应,所以性能比较差。所以在低峰时来做 batch TRIM,可以减少对系统的影响。SATA 3.1 后已经改成了 ququed trim,从而可以减少对性能的影响。</p>
<p>但从各方面的资料看来,比如 fstrim 的 manual,以及 <a href="proxy.php?url=https://wiki.archlinux.org/index.php/Solid_state_drive/NVMe#Discards" rel="nofollow">https://wiki.archlinux.org/index.php/Solid_state_drive/NVMe#Discards</a> 里提到 Intel 对于 discard 的建议(原因是 Linux Kernel 的 TRIM 实现比较慢),在低峰的时候,周期性地用 batch trim 似乎都是更好的选择,特别是在你不清楚你的设备时候支持 queued trim 的情况下。</p>
<p>当然,如果 fstrim 对性能影响也比较大,比如你的应用没有一个低峰的时刻可以用来跑 fstrim,那可以先调整 fstrim 的参数,来减少影响。实在不行,可以在测试没问题的前提下使用 Online discard,但千万不要同时使用。</p>
tag:tonyhan.dev,2014:Post/d-state-high-load-to-erlang-memory-tuning
2019-08-20T17:11:29-07:00
2019-08-20T17:11:29-07:00
从 D-state 造成的 high load 到 Erlang 内存分配调优
<p>在 Tubi,我们有个电影/电视剧的 metadata 服务,叫 Content,是一个 Elixir(Erlang) 写的服务。当请求 Tubi 的时候,这个服务会负责把需要的 metadata,比如标题、描述、图片等返回给客户端,是一个比较关键的服务。最近线上因为客户端发送了过多的请求,导致服务器的 (normalized) load 很快地从 0.5 升高到 1.3,同时 latency 升高,最终通过扩容来解决。请求量增加导致 load 升高很正常,但不正常的是请求量只增加了30%,但 load 却增加了 160%,所以想搞清楚为什么。</p>
<h2 id="_2">重现线上问题 <a class="head_anchor" href="proxy.php?url=#_2" rel="nofollow">#</a>
</h2>
<p>能够在 staging 环境复现,问题就等于解决了一半。我先在 staging 把相关的两个服务升级到了和 production 一样的机型,然后用 <a href="proxy.php?url=https://github.com/tsenart/vegeta" rel="nofollow">Vegeta</a> 来产生一定的 load,用 Vegeta 的原因是可以产生固定 QPS 的 load,因为 staging 机器比 production 数量少,所以我按比例发送了 400 QPS 的请求。另外,因为 Content 支持一个 gRPC 接口请求多个 content 的数据,所以我也尽量让每个请求和 production 环境的平均值相同。</p>
<p>我先来模拟出问题前的情况</p>
<pre><code class="prettyprint">cat targets.txt | ./vegeta attack -rate=400 -max-body=0 > results.bin # -max-body=0 is used to avoid large logs
</code></pre>
<p>通过监控可以看到 load 达到了 0.5,p99 latency 差不多是 75ms,Erlang 的 run queue 也在正常水平,和我们线上的正常情况比较接近。</p>
<p>然后我逐步增加了 load,到 520 QPS</p>
<pre><code class="prettyprint">cat targets.txt | ./vegeta attack -rate=520 -max-body=0 > results.bin
</code></pre>
<p>然后发现 load 很快到了 1.3,latency 也到了 2s,Erlang 的 run queue 非常高,基本上还原了线上的问题。然后就可以来 debug 了。</p>
<h2 id="_2">问题调查 <a class="head_anchor" href="proxy.php?url=#_2" rel="nofollow">#</a>
</h2>
<p>通过 htop,可以看到有很多 D 状态的进程。</p>
<p><a href="proxy.php?url=https://svbtleusercontent.com/2jD7pVyba4V1d4Ed9fcH4P0xspap.png" rel="nofollow"><img src="proxy.php?url=https://svbtleusercontent.com/2jD7pVyba4V1d4Ed9fcH4P0xspap_small.png" alt="Screen_Shot_2019-08-11_at_10_33_16_PM.png"></a></p>
<p>我们知道 <a href="proxy.php?url=https://en.wikipedia.org/wiki/Load_(computing)" rel="nofollow">Linux load</a> 包括</p>
<ol>
<li>Running tasks</li>
<li>Runnable tasks(waiting for CPU)</li>
<li>Tasks in uninterruptible sleep state. See <a href="proxy.php?url=http://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html" rel="nofollow">http://www.brendangregg.com/blog/2017-08-08/linux-load-averages.html</a> for details.</li>
</ol>
<p>所以 load 才会那么高,D 状态一般是因为磁盘 IO 或者锁导致,为了弄明白到底是什么原因,我们需要更深入研究。</p>
<p>我用 <a href="proxy.php?url=https://github.com/iovisor/bcc/blob/master/tools/offcputime.py" rel="nofollow">offcputime</a> 来查看 Kernel Space 的调用栈(Kernel 需要 >= 4.9),并且只保留 state 2(TASK_UNINTERRUPTIBLE) ,然后再生成火焰图</p>
<pre><code class="prettyprint">sudo /usr/sbin/offcputime-bpfcc -K --state 2 -f 10 > out.stacks
awk '{ print $1, $2 / 1000 }' out.stacks | flamegraph.pl --color=io --countname=ms > out.offcpu.svg
</code></pre>
<p><a href="proxy.php?url=https://svbtleusercontent.com/sZskNs4c22EhgkjJkA9y450xspap.png" rel="nofollow"><img src="proxy.php?url=https://svbtleusercontent.com/sZskNs4c22EhgkjJkA9y450xspap_small.png" alt="Screen Shot 2019-08-09 at 9.58.18 AM.png"></a></p>
<p><a href="proxy.php?url=https://svbtleusercontent.com/hoHvgPEeoxa7tAMcqTBmaP0xspap.png" rel="nofollow"><img src="proxy.php?url=https://svbtleusercontent.com/hoHvgPEeoxa7tAMcqTBmaP0xspap_small.png" alt="Screen Shot 2019-08-09 at 9.58.24 AM.png"></a></p>
<p>我们可以看到有很多 <a href="proxy.php?url=https://elixir.bootlin.com/linux/v4.15/source/kernel/locking/rwsem-xadd.c#L284" rel="nofollow">rwsem_down_read_failed</a> 的调用,而这就是 Linux Kernel 里加锁以及 TASK_UNINTERRUPTIBLE 被设置的地方,并且从火焰图可以看到加锁的之前发送了 page fault。</p>
<p>另外,从 vmstat 也可以验证,并没有很多 IO 操作</p>
<pre><code class="prettyprint">$ vmstat -w 1
procs -----------------------memory---------------------- ---swap-- -----io---- -system-- --------cpu--------
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 9552352 217752 19212732 0 0 0 25 6 3 38 4 58 0 0
8 0 0 9881040 217752 19212860 0 0 0 1492 43269 60876 53 6 41 0 0
9 0 0 9799896 217752 19213052 0 0 0 0 46717 62012 57 7 36 0 0
16 0 0 9711712 217752 19213244 0 0 0 0 49103 63563 59 8 33 0 0
22 0 0 9772740 217752 19213372 0 0 0 0 48386 63826 56 8 36 0 0
6 0 0 9821224 217752 19213544 0 0 0 0 48229 64044 57 7 36 0 0
4 0 0 9932152 217752 19213688 0 0 0 1840 52131 66037 59 7 34 0 0
5 0 0 9750640 217752 19213880 0 0 0 0 55170 68330 61 9 31 0 0
</code></pre>
<p>于是我们可以确定,D 状态是由于 page fault 之后的锁造成的,并不是 IO 操作。但 page fault 正常情况下应该不会产生严重的问题,但这里是为什么造成了大量的 D 状态进程,已经 page fault 又是怎么产生的?</p>
<p>通过 <code class="prettyprint">sar</code> 我们可以看到几乎所有的 page fault 都是 <a href="proxy.php?url=https://en.wikipedia.org/wiki/Page_fault#Minor" rel="nofollow">minor page faults</a> ,并且数量非常多(20~30万/s)。minor page fault 通常不会有大问题,除非同时有大量的 page fault,就像我们现在这种情况。</p>
<pre><code class="prettyprint">sar -B 1
Linux 4.15.0-1044-aws (content-03) 08/11/19 _x86_64_ (16 CPU)
15:07:31 pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
15:07:32 0.00 0.00 258816.00 0.00 316014.00 0.00 0.00 0.00 0.00
15:07:33 0.00 0.00 303459.00 0.00 294384.00 0.00 0.00 0.00 0.00
15:07:34 0.00 0.00 259817.00 0.00 330940.00 0.00 0.00 0.00 0.00
15:07:35 0.00 0.00 228191.00 0.00 272419.00 0.00 0.00 0.00 0.00
15:07:36 0.00 4276.00 303600.00 0.00 286122.00 0.00 0.00 0.00 0.00
15:07:37 0.00 32.00 248255.00 0.00 343716.00 0.00 0.00 0.00 0.00
15:07:38 0.00 20.00 236296.00 0.00 309811.00 0.00 0.00 0.00 0.00
15:07:39 0.00 0.00 295435.00 0.00 296189.00 0.00 0.00 0.00 0.00
15:07:40 0.00 0.00 279576.00 0.00 305584.00 0.00 0.00 0.00 0.00
15:07:41 0.00 0.00 220455.00 0.00 323671.00 0.00 0.00 0.00 0.00
15:07:42 0.00 0.00 356954.00 0.00 227642.00 0.00 0.00 0.00 0.00
15:07:43 0.00 0.00 237139.00 0.00 423679.00 0.00 0.00 0.00 0.00
15:07:44 0.00 0.00 274000.00 0.00 303181.00 0.00 0.00 0.00 0.00
15:07:45 0.00 0.00 271769.00 0.00 317360.00 0.00 0.00 0.00 0.00
15:07:46 0.00 0.00 282215.00 0.00 295938.00 0.00 0.00 0.00 0.00
15:07:47 0.00 0.00 252556.00 0.00 317603.00 0.00 0.00 0.00 0.00
15:07:48 0.00 0.00 249663.00 0.00 280742.00 0.00 0.00 0.00 0.00
15:07:49 0.00 0.00 295803.00 0.00 304895.00 0.00 0.00 0.00 0.00
</code></pre>
<p>为什么我们会有这么多 page fault?可以用 <code class="prettyprint">perf</code> 来看一下原因:</p>
<pre><code class="prettyprint">sudo perf record -e page-faults -ag --call-graph dwarf # --call-graph dwarf is needed, otherwise we can't have symbols
sudo perf report -g graph --no-children
</code></pre>
<p><a href="proxy.php?url=https://svbtleusercontent.com/txBdXcm3M8Uw4eL7jtSYBH0xspap.png" rel="nofollow"><img src="proxy.php?url=https://svbtleusercontent.com/txBdXcm3M8Uw4eL7jtSYBH0xspap_small.png" alt="Screen Shot 2019-08-10 at 2.50.07 PM.png"></a></p>
<p>我们看到 page fault 都在 Erlang 进程中,尽管有些函数不能被很好地显示,我们还是可以看到一些有用的信息。比如 <code class="prettyprint">erts_garbage_collect_nobump</code> 告诉我们 page fault 在 GC 执行的时候产生。我没有找到很直接的资料关于 Erlang GC 的时候产生 page fault 的问题,但我们可以从 page fault 产生原因和 Erlang 内存分配的工作原理来揣测。</p>
<p>Erlang 的 <a href="proxy.php?url=https://erlang.org/doc/man/erts_alloc.html" rel="nofollow">mseg_alloc</a> 会通过 <a href="proxy.php?url=https://en.wikipedia.org/wiki/Mmap" rel="nofollow">mmap</a> 来分配内存,刚刚分配后,mmap 并不会使用的物理内存,而是等到这块内存需要被实际用到的时候再通过 page fault 来通知 Kernel 去进行实际分配,我写了个简单的 <a href="proxy.php?url=https://gist.github.com/tony612/f5c6d227a3c0d4eec43ee933b5f192a3" rel="nofollow">C 代码</a> 来验证这点。也就是说只要程序需要像 OS 请求新的内存,就可能会产生 page fault。</p>
<p>Erlang 会对每个轻量级进程(actor)单独执行 GC,在 <a href="proxy.php?url=https://www.erlang-solutions.com/blog/erlang-garbage-collector.html" rel="nofollow">GC</a> 运行的时候,Erlang 会创建一块新的内存,把老的数据拷过去,如果 Erlang 本身请求的内存不够了,就会像操作系统请求新的内存。所以就可以解释为什么 GC 的时候会产生 page fault 了。</p>
<h2 id="erlang_2">Erlang 参数调优 <a class="head_anchor" href="proxy.php?url=#erlang_2" rel="nofollow">#</a>
</h2>
<p>那怎么来判断程序的内存分配是不是有问题,以及有没有需要调整的参数呢?</p>
<p>我用了 <a href="proxy.php?url=http://ferd.github.io/recon/recon_alloc.html" rel="nofollow">recon_alloc</a> 来看内存分配的情况:</p>
<pre><code class="prettyprint">iex> :recon_alloc.sbcs_to_mbcs(:current)
[
{{:eheap_alloc, 0}, 1.7865168539325842}, # 1
{{:eheap_alloc, 11}, 0.04578313253012048},
{{:eheap_alloc, 6}, 0.04429530201342282},
{{:eheap_alloc, 3}, 0.02880658436213992},
{{:eheap_alloc, 16}, 0.02786377708978328},
{{:eheap_alloc, 2}, 0.026066350710900472},
...
iex> :recon_alloc.cache_hit_rates
[
{{:instance, 0},
[hit_rate: 0.6419778480913936, hits: 28810063, calls: 44877036]}, # 2
{{:instance, 16},
[hit_rate: 0.8994651408511145, hits: 5292434, calls: 5883979]},
{{:instance, 3},
[hit_rate: 0.8989566664247531, hits: 5239595, calls: 5828529]},
# I use this instead of average_block_sizes for getting block and carrier data for only {:eheap_alloc, 0}
> IO.inspect :recon_alloc.allocators, limit: :infinity
...
{{:eheap_alloc, 0},
[
...
mbcs: [
{:blocks, 89, 89, 127}, # 3
{:blocks_size, 704544, 704544, 846400}, # 4
{:carriers, 3, 3, 3},
{:mseg_alloc_carriers, 2},
{:sys_alloc_carriers, 1},
{:carriers_size, 1441792, 1441792, 1441792},
{:mseg_alloc_carriers_size, 1310720},
{:sys_alloc_carriers_size, 131072}
],
sbcs: [
{:blocks, 93, 278, 390}, # 6
{:blocks_size, 359408608, 1038591216, 1445304432}, # 7
{:carriers, 93, 278, 390},
{:mseg_alloc_carriers, 93},
{:sys_alloc_carriers, 0},
{:carriers_size, 369188864, 1069051904, 1508335616},
{:mseg_alloc_carriers_size, 369188864},
{:sys_alloc_carriers_size, 0}
]
]},
</code></pre>
<p>这些和 Erlang 的分配器非常相关,可以参考下列资料来了解:<br>
<a href="proxy.php?url=http://erlang.org/doc/man/erts_alloc.html" rel="nofollow">http://erlang.org/doc/man/erts_alloc.html</a><br>
<a href="proxy.php?url=http://www.erlang-factory.com/static/upload/media/139454517145429lukaslarsson.pdf" rel="nofollow">http://www.erlang-factory.com/static/upload/media/139454517145429lukaslarsson.pdf</a> ( <a href="proxy.php?url=https://www.youtube.com/watch?v=nuCYL0X-8f4&list=LLp0Rd1Jd2VpkBxoC3nAO0Uw&index=2&t=0s" rel="nofollow">video</a> )<br>
<a href="proxy.php?url=https://blog.heroku.com/logplex-down-the-rabbit-hole" rel="nofollow">https://blog.heroku.com/logplex-down-the-rabbit-hole</a></p>
<p>简单来说一下 sbcs(single block carriers) 和 mbcs(multiblock carriers),这是 Erlang 来分配内存的两种方式。当要分配的内存大于 sbct(single block carrier threshold) 时,会产生一个新的 sbcs,否则会通过 mbcs 来找到可以分配的空位。mbcs 更好,因为他们可以被重复使用,而不需要想操作系统要更多内存。但是对于 sbcs,Erlang 会调用 mmap/malloc 来分配内存。所以尽可能使用 mbcs 会使得性能更好,但如果我们有一些很大的数据,sbcs 会被使用,所以我们需要调整 sbct 和 lmbcs(Largest multiblock carrier size) 来更多地使用 mbcs。</p>
<p>现在让我们看一下上边的数据:<br>
#1: sbcs/mbcs 应该保持在比较低的值,比如其他都是 <0.1,>=1 已经很大了,代表我们用了太多的 sbcs<br>
#2: 0.8 以上是比较好的。我们的 cache_hit_rates 比较低,意味着经常需要分配内存<br>
#3 + #4 + #6 + #7: sbcs 比 mbcs 多,并且 sbcs 平均大小是 is 359408608/93=3,864,608(3.6M)<br>
#5: 我们现在的 sbct 是 524288 bytes(默认值),所以我们有很多 sbcs,我们需要增大到 3.6M,并调整 smbcs 和 lmbcs</p>
<p>我们对 Erlang 的参数做了调整:</p>
<pre><code class="prettyprint">$ cat vm.args
+MHsbct 4096 # Unit: KB. 4096*1024 is defualt value 524288 * 8
+MHsmbcs 2048 # Unit: KB. 2048*1024 is default value 262144 * 8
+MHlmbcs 40960 # Unit: KB. 40960*1024 is default value 5242880 * 8
</code></pre>
<p>然后再测一次,QPS可以达到 650,并且各种系统指标都在正常范围,也没有 D 状态的进程了,minor page fault 还是有,但只有 50000/s,相比之前已经少了很多。</p>
<p>也就是说在对 Erlang 内存分配的参数调优后,我们能处理的 QPS 增长了 40% 左右。</p>
<h2 id="_2">总结 <a class="head_anchor" href="proxy.php?url=#_2" rel="nofollow">#</a>
</h2>
<p>通过对这个问题的抽丝剥茧,我们可以看到解决问题就像推理一样,需要环环相扣,最终当都扣上的时候,问题也就解决了。</p>
<p>另外,在查找资料的时候还看到 Linux team 在研究移除 page fault handler 中的锁,如果这个能够实现的话,可以预见,在很多场景下,性能都能够得到提升。<br>
<a href="proxy.php?url=https://lwn.net/Articles/730531/" rel="nofollow">https://lwn.net/Articles/730531/</a><br>
<a href="proxy.php?url=https://lwn.net/Articles/754739/" rel="nofollow">https://lwn.net/Articles/754739/</a></p>
tag:tonyhan.dev,2014:Post/elixir-grpc-the-road-to-production
2019-08-02T06:58:11-07:00
2019-08-02T06:58:11-07:00
Elixir + gRPC: the road to production
<p>原文发表在 Tubi 的 Medium 账号上,点击标题查看原文。</p>
tag:tonyhan.dev,2014:Post/k8s-pod-unhealthy-debug
2018-05-02T05:29:22-07:00
2018-05-02T05:29:22-07:00
k8s pod unhealthy 后不能正常重启 debug
<p>之前遇到一个问题,当一个 pod 从 healthy 到 unhealthy 后,经常不能够正常启动。</p>
<p>比如 init delay 是 0,健康检查 period 是 10s,unhealthy threshold 是 3,那么应该在20s内启动即可,但实际发现不到 20s 就会被 terminate。通过观察 kubelet 的 log 发现,Liveness 的 probe 出现了2次就会被 kill:</p>
<pre><code class="prettyprint">Apr 19 02:28:32 ip-xxx kubelet[2449]: I0419 02:28:32.999475 2449 prober.go:111] Liveness probe for "xxx(21431e82-42fa-11e8-a986-02683fc2eb42):backend-xxx" failed (failure): Get http://ip:8080/status: dial tcp ip:8080: getsockopt: connection refused
Apr 19 02:28:44 ip-xxx kubelet[2449]: I0419 02:28:44.005553 2449 prober.go:111] Liveness probe for "xxx(21431e82-42fa-11e8-a986-02683fc2eb42):xxx" failed (failure): Get http://ip:8080/status: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
Apr 19 02:28:44 ip-xxx kubelet[2449]: I0419 02:28:44.310592 2449 kuberuntime_manager.go:550] Container "xxx" ({"docker" "5bea0c1cb416247a6e91649049e79d2214be9ef18d34e17948cc663e7003291c"}) of pod xxx(21431e82-42fa-11e8-a986-02683fc2eb42): Container failed liveness probe.. Container will be killed and recreated.
</code></pre>
<p>最终通过阅读 kubernetes 的代码发现,是因为 prober.worker 的 <code class="prettyprint">resultRun</code> 在一次失败后,被 reset 为一个错误的值(应该为 0,但实际为 1),导致新的 pod 起来后,resultRun 已经是 1 了,那么当 liveness 失败 n-1 次就会把 pod kill 掉了。修复在 <a href="proxy.php?url=https://github.com/kubernetes/kubernetes/pull/62853" rel="nofollow">https://github.com/kubernetes/kubernetes/pull/62853</a></p>
<h2 id="probe_2">probe 工作流程 <a class="head_anchor" href="proxy.php?url=#probe_2" rel="nofollow">#</a>
</h2><h3 id="_3">初始化 <a class="head_anchor" href="proxy.php?url=#_3" rel="nofollow">#</a>
</h3>
<p>在 kubelet 初始化时,会<a href="proxy.php?url=https://github.com/kubernetes/kubernetes/blob/16c3167040f1d0a739f66ea83c0ee1167887eb4e/pkg/kubelet/kubelet.go#L754" rel="nofollow">初始化</a> probeManager,在 kubelet 启动时,会在 <a href="proxy.php?url=https://github.com/kubernetes/kubernetes/blob/16c3167040f1d0a739f66ea83c0ee1167887eb4e/pkg/kubelet/kubelet.go#L1694" rel="nofollow">syncLoop -> syncLoopIteration</a> 中处理 <code class="prettyprint">kubetypes.PodUpdate</code> 这个 channel 的消息,当收到 <code class="prettyprint">kubetypes.ADD</code> 的事件(也就是是 pod 增加)时,会调用 <code class="prettyprint">kubelet.HandlePodAdditions</code>,其中包含了 <code class="prettyprint">kl.probeManager.AddPod(pod)</code>。probeManager 就是之前初始化的那个,它会根据 pod 的定义来创建 readiness 或者 liveness <a href="proxy.php?url=https://github.com/kubernetes/kubernetes/blob/c0c5566c25aa1e118d77e1f1bce7489cd53b9016/pkg/kubelet/prober/prober_manager.go#L168" rel="nofollow">worker</a>,每个 container 对应一个 worker,然后就会在一个新的 goroutine 里运行这个 worker。</p>
<p>在 <code class="prettyprint">prober.worker</code> 初始化时,会根据 readiness 或 liveness 设置不同的 initialValue(对应于文档中,readiness 一开始是 failure,liveness 一开始是 success),在 <code class="prettyprint">worker.run()</code> 中,会先 sleep probe period 内的随机值(比如周期是 10s,会 sleep 0-10s),然后按 period 进入 ticker 的 loop 中,也就是 <code class="prettyprint">worker.doProbe()</code>,并根据 doProbe 的返回值判断是否结束(false 为结束循环)。</p>
<h3 id="doprobe_3">doProbe <a class="head_anchor" href="proxy.php?url=#doprobe_3" rel="nofollow">#</a>
</h3>
<p>doProbe 有几个判断:</p>
<ul>
<li>pod 不存在(没被创建/被删除),return true</li>
<li>pod 在 terminated 状态,return false</li>
<li>container 没有被创建或者被删除,return true</li>
<li>container 的实际 id 和 worker 被保存的 id 不同,表示 pod 内的 container 被重启了,这时需要通知 resultsManager container 的状态发生变化,并设置 <code class="prettyprint">onHold</code> 是 false。</li>
<li>
<code class="prettyprint">onHold</code> 是 true,return true。是为了暂时暂停 probe(一般是在有新 container 时)</li>
<li>container 的 StartedAt 早于 InitialDelaySeconds 的时间,return true</li>
</ul>
<p>如果上边的判断都没有 return,就开始实际的 probe(TCP, HTTP 等),如果 <code class="prettyprint">lastResult</code> 和当前 <code class="prettyprint">result</code> 相等,会把 <code class="prettyprint">resultRun</code> +1,表示连续 <code class="prettyprint">resultRun</code> 次得到成功或失败的 <code class="prettyprint">result</code>,否则就设置 <code class="prettyprint">lastResult</code>,并把 <code class="prettyprint">resultRun</code> 置为 1。如果 resultRun 小于 Threshold,则 return true。否则就通知 resultsManager。</p>
<p>如果 worker 是 liveness,且 <code class="prettyprint">result</code> 是 <code class="prettyprint">Failure</code>,则会设置 <code class="prettyprint">onHold</code> 是 <code class="prettyprint">true</code>,因为 container 会重启,所以需要暂停 probe。并 reset <code class="prettyprint">resultRun</code> 为 <code class="prettyprint">1</code>(这里应该为 0,也就是我们碰到的 bug)。</p>
<p>当 pod 删除时,prober_manager 会调用 <code class="prettyprint">RemovePod</code> 来进行回收,比如停止 worker。</p>
<h3 id="probe-container_3">probe 的结果如何能让 container 重启 <a class="head_anchor" href="proxy.php?url=#probe-container_3" rel="nofollow">#</a>
</h3>
<p>resultsManager 的作用是用来通知 container 的事件变化,比如 kubelet 的 livenessManager。</p>
<p>在 kubelet 的 <code class="prettyprint">syncLoopIteration</code> 中会对 <code class="prettyprint">updates</code> 新的事件(比如 probe worker 发的事件)调用 <code class="prettyprint">HandlePodSyncs</code>,最终调到 <code class="prettyprint">kl.podWorkers.UpdatePod</code>,在 <code class="prettyprint">podWorkers.UpdatePod</code> 中调用 <code class="prettyprint">podWorkers.managePodLoop</code>,然后是 <code class="prettyprint">syncPodFn</code>。而 syncPodFn 是在哪里初始化的呢? </p>
<p>是在 kubelet 初始化时<a href="proxy.php?url=https://github.com/kubernetes/kubernetes/blob/16c3167040f1d0a739f66ea83c0ee1167887eb4e/pkg/kubelet/kubelet.go#L798" rel="nofollow">设置</a> 的 <code class="prettyprint">klet.syncPod</code>。于是调用 <code class="prettyprint">syncPodFn</code> 就是调用 kubelet 的 <code class="prettyprint">syncPod</code>,然后会调用 <code class="prettyprint">kl.containerRuntime.SyncPod</code>,也就是 kubeGenericRuntimeManager(在 kubelet 初始化时会<a href="proxy.php?url=https://github.com/kubernetes/kubernetes/blob/16c3167040f1d0a739f66ea83c0ee1167887eb4e/pkg/kubelet/kubelet.go#L639" rel="nofollow">创建</a>的) 的 syncPod,在 syncPod 的 computePodActions 中,通过 <code class="prettyprint">m.livenessManager.Get(containerStatus.ID)</code> 就得到了 container 的状态。</p>
tag:tonyhan.dev,2014:Post/kubernetes-debug
2017-05-04T03:36:11-07:00
2017-05-04T03:36:11-07:00
Kubernetes 源码解读 - 由一次 debug 学到的
<p>在最近公司办的第一次 Open L 活动中,我们分享了为什么我们要用 Kubernetes,其中吸引我们的一方面就是 autoscaling,它能够根据 CPU 等指标动态调整 pod 的个数,以此提高机器的利用率。</p>
<p>但最近却发现它并不能按预期正常地工作,<code class="prettyprint">deployment</code> 的 Horizontal Pod Autoscaler(HPA) 显示的 CPU 并不能反应实际情况,所以也就不能正常地对 pod 的数量进行调整。查了 log 又 Google 一番后,我们并没有发现根本原因和解决办法,试了一些可能的方案,比如升级 docker,也没有解决,于是我们决定借助 k8s 的相关源码来弄清问题的根由,也方便之后再出现其他问题时能够更快地解决。</p>
<p>(本文基于 Kubernetes v1.4.6 e569a27d02 和 heapster v1.2.0 eea8c965)</p>
<p>先看一下 HPA 的工作原理:<br>
<a href="proxy.php?url=https://svbtleusercontent.com/hb8ewgc8yswlkw.jpg" rel="nofollow"><img src="proxy.php?url=https://svbtleusercontent.com/hb8ewgc8yswlkw_small.jpg" alt="Horizontal_Pod_Autoscaling___Kubernetes.jpg"></a></p>
<p>HPA 会被 controller manager 启动,不断从 heapster 抓取所有 pod 的 metrics,计算出平均值,再控制 deployment 伸缩。我们的问题很明显出在计算平均值的那前半段。</p>
<h2 id="hpa_2">HPA 启动和运行 <a class="head_anchor" href="proxy.php?url=#hpa_2" rel="nofollow">#</a>
</h2>
<p>Kubernetes 那么多代码,从哪看起呢? 我们先查看了 controller manager 的 log,发现了关键的几行</p>
<pre><code class="prettyprint">W0429 12:42:11.182073 5 horizontal.go:105] Failed to reconcile neo-staging-web: failed to compute desired number of replicas based on CPU utilization for Deployment/backend/neo-staging-web: failed to get CPU utilization: failed to get CPU consumption and request: metrics obtained for 0/2 of pods (sample missing pod: backend/neo-staging-web-v201-dqxj5)
</code></pre>
<p>这个 log 是在 <code class="prettyprint">k8s/controller/podautoscaler/horizontal.go:105</code> 被打印的,逆着调用链,我们发现程序入口在 <code class="prettyprint">kube-controller-manager/controller-manager.go:53</code>,接着在 <code class="prettyprint">cmd/kube-controller-manager/app/controllermanager.go:121</code> 这个 <code class="prettyprint">Run</code> 函数中启动了多个 controller,其中就包括 podautoscaler pkg 的 <code class="prettyprint">HorizontalController</code>(cmd/kube-controller-manager/app/controllermanager.go:337)。当它被调用 <code class="prettyprint">.Run(wait.NeverStop)</code> 的时候,就会创建一个 loop 每隔一段时间从 apiserver 获取 autoscaler 列表,再对每个 autoscaler 做处理。这个处理的过程(pkg/controller/podautoscaler/horizontal.go:258)包括获取相关监控数据、计算需要的 pod 数量和调用 apiserver 的 API 来更新 autoscaler 的 desired pod 数量。</p>
<p>获取监控数据的逻辑入口在这里 pkg/controller/podautoscaler/metrics/metrics_client.go:104。其中两个关键函数 GetCpuConsumptionAndRequestInMillis 和 getCpuUtilizationForPods 函数简化后是</p>
<pre><code class="prettyprint lang-go">func (h *HeapsterMetricsClient) GetCpuConsumptionAndRequestInMillis(...) (...) {
podList, err := h.client.Core().Pods(namespace).
List(api.ListOptions{LabelSelector: selector})
// ... pods 必须是 running 并且有设置 requested CPU
requestAvg := requestSum / int64(len(podNames))
consumption, timestamp, err := h.getCpuUtilizationForPods(namespace, selector, podNames)
return consumption, requestAvg, len(podNames), timestamp, nil
}
func (h *HeapsterMetricsClient) getCpuUtilizationForPods(namespace, selector, podNames) (int64, time.Time, error) {
metricPath := fmt.Sprintf("/apis/metrics/v1alpha1/namespaces/%s/pods", namespace)
params := map[string]string{"labelSelector": selector.String()}
resultRaw, err := h.client.Core().Services(h.heapsterNamespace).
ProxyGet(h.heapsterScheme, h.heapsterService, h.heapsterPort, metricPath, params).
DoRaw()
metrics := metrics_api.PodMetricsList{}
err = json.Unmarshal(resultRaw, &metrics)
if len(metrics.Items) != len(podNames) {
missing := // 找出没有 metrics 的 pod
hint := ""
if len(missing) > 0 {
hint = fmt.Sprintf(" (sample missing pod: %s/%s)", namespace, missing[0])
}
return 0, time.Time{}, fmt.Errorf("metrics obtained for %d/%d of pods%s", len(metrics.Items), len(podNames), hint)
}
}
</code></pre>
<p>先通过 apiserver 得到 pod list,经过简单的判断和过滤,再调用 heapster 的 API 得到 metrics,只有返回的 metrics 和 pod 一致才会继续进行。从错误日志可以判断,问题就出在 heapster 返回的 metrics 少了。</p>
<h2 id="heapster-api-pod-metrics_2">heapster API 返回 pod metrics <a class="head_anchor" href="proxy.php?url=#heapster-api-pod-metrics_2" rel="nofollow">#</a>
</h2>
<p><a href="proxy.php?url=https://github.com/kubernetes/heapster" rel="nofollow">heapster</a> 是一个单独的项目,在 k8s 中,heapster 通常以 pod 的形式被部署在 k8s 集群中,它从 k8s 的每个节点那里收集数据,经过处理之后再写到一些存储里。它也提供 API 来得到数据,比如刚刚看到的 <code class="prettyprint">/apis/metrics/v1alpha1/namespaces/:namespace/pods</code>。</p>
<p>由 build 的脚本或者直接搜索代码,很容易知道 heapster 是在 metrics/heapster.go:63 启动的,这里先初始化了 source(用来得到监控数据,比如 k8s)和 sink(用来保存数据,比如 influxdb),启动了用来收集和处理数据的 manager,最后启动了 HTTP server。先从上边的 API 调用入口看起 metrics/apis/metrics/handlers.go:188,这里获取了 pods 列表,然后从 MetricsSink 中取到相应的 metrics。MetricsSink 就是其中一种 sink,它比较简单,只是把长期(15m)和短期(140s)的数据放在 struct 里,metrics 的实际获取是异步进行的,这里先按下不表,先来看是哪里出的问题。从 log 中我们看到了如下的错误:</p>
<pre><code class="prettyprint">No metrics for pod backend/neo-staging-web-v201-dqxj5
</code></pre>
<p>但这块代码错误处理写得并不是很好,没有把更进一步的原因打到 log 里,所以我们很难知道是因为 MetricsSink 里没有数据,还是数据不正确。这里我们为了更快地定位问题,自己改了一下代码,在这个函数 metrics/apis/metrics/handlers.go:260 GetLatestDataBatch() 判空以及 parseResourceList error 判断的地方加了 log,把自己修改后的代码重新 build image 并运行。结论是因为 metrics 里没有 “cpu/usage_rate”(CPU usage on all cores in millicores)。因为是数据的问题,所以只看这段代码是不够的,我们需要知道 MetricsSink 的数据是如何被写入的。</p>
<h2 id="heapster-kubernetes-metrics_2">heapster 从 Kubernetes 获取 metrics <a class="head_anchor" href="proxy.php?url=#heapster-kubernetes-metrics_2" rel="nofollow">#</a>
</h2>
<p>heapster 的 main 函数里先创建了 sourceManager、dataProcessors 和 sinkManager,再用它们创建了一个总的 manager(metrics/manager/manager.go:64),并运行它。这个运行的过程,其实就是创建了一个 goroutine,异步地不断进行数据从抓取到保存的整个过程。</p>
<p>简化后的代码如下:</p>
<pre><code class="prettyprint lang-go">// metrics/manager/manager.go:110
func (rm *realManager) housekeep(start, end time.Time) {
select {
go func(rm *realManager) {
data := rm.source.ScrapeMetrics(start, end)
for _, p := range rm.processors {
newData, err := process(p, data)
data = newData
}
rm.sink.ExportData(data)
}(rm)
}
// metrics/sources/manager.go:78
func (this *sourceManager) ScrapeMetrics(start, end time.Time) *DataBatch {
sources := this.metricsSourceProvider.GetMetricsSources()
response := DataBatch{
MetricSets: map[string]*MetricSet{},
}
for _, source := range sources {
metrics := s.ScrapeMetrics(start, end)
for key, value := range metrics.MetricSets {
response.MetricSets[key] = value
}
}
return &response
}
// metrics/processors/rate_calculator.go:32
func (this *RateCalculator) Process(batch *core.DataBatch) (*core.DataBatch, error) {
if this.previousBatch == nil {
this.previousBatch = batch
return batch, nil
}
for key, newMs := range batch.MetricSets {
if oldMs, found := this.previousBatch.MetricSets[key]; found {
if !newMs.CreateTime.Equal(oldMs.CreateTime) {
glog.V(4).Infof("Skipping rates for %s - different create time new:%v old:%v", key, newMs.CreateTime, oldMs.CreateTime)
// Create time for container must be the same.
continue
}
for metricName, targetMetric := range this.rateMetricsMapping {
// 通过新旧 metrics 来计算 rate,并修改 newMs
}
}
}
this.previousBatch = batch
return batch, nil
}
</code></pre>
<p>这里会先得到所有的 sources,也就是每个 k8s cluster node 的 kubelet,然后调用 kubelet 的 “/stats/container/” 接口来得到 <code class="prettyprint">[]cadvisor.ContainerInfo</code>,它包含所有 container 的信息以及对应的 metrics。再用得到的数据来计算一些 rate metrics,比如前边提到过的 CpuUsageRate,也就是一段时间内的 CPU usage。但在计算之前做了一些判断,比如会比较前后两个 DataBatch 的 container 的创建时间。通过把 log 级别调到 4,我们发现这里会输出很多这样错误日志:</p>
<pre><code class="prettyprint">rate_calculator.go:51] Skipping rates for namespace:backend/pod:neo-staging-web-v201-ee4c3 - different create time new:2017-05-04 14:26:19.639205791 +0000 UTC old:2017-05-04 14:25:19.591801877 +0000 UTC
</code></pre>
<p>所以基本可以确定,前边发现没有 CpuUsageRate 的原因是在这里跳过了计算。那为什么要比较 container 的创建时间呢?通过 git blame,我们在这个 issue <a href="proxy.php?url=https://github.com/kubernetes/heapster/issues/988" rel="nofollow">https://github.com/kubernetes/heapster/issues/988</a> 找到了答案,原来是为了处理 container 重启的情况。而实际上,container 并没有重启,但为什么 container 的创建时间会一直不一致呢?是从 kubelet 中获取的数据本身就有问题还是经过 heapster 的处理后造成的问题?</p>
<p>我们可以直接在 node 上边调用 kubelet 的 API 来看结果有什么问题,其中不太重要的信息被我略去了</p>
<pre><code class="prettyprint">$ curl -X "POST" -d '{"containerName":"/","subcontainers":true,"num_stats":1}' http://localhost:10255/stats/container
{
"/system.slice/docker.service/docker/cf372e95fb3ed22b8613576164867f273eca157c0d3c4112af196993b0b8125f": {
"id": "cf372e95fb3ed22b8613576164867f273eca157c0d3c4112af196993b0b8125f",
"name": "/system.slice/docker.service/docker/cf372e95fb3ed22b8613576164867f273eca157c0d3c4112af196993b0b8125f",
"spec": {
"creation_time": "2017-05-04T15:30:22.688799748Z",
"labels": {
"io.kubernetes.container.name": "neo-web",
"io.kubernetes.pod.name": "neo-staging-web-v201-dqxj5",
"io.kubernetes.pod.namespace": "backend"
}
}
},
"/docker/cf372e95fb3ed22b8613576164867f273eca157c0d3c4112af196993b0b8125f": {
"id": "cf372e95fb3ed22b8613576164867f273eca157c0d3c4112af196993b0b8125f",
"name": "/docker/cf372e95fb3ed22b8613576164867f273eca157c0d3c4112af196993b0b8125f",
"spec": {
"creation_time": "2017-05-04T15:30:43.173457599Z",
"labels": {
"io.kubernetes.container.name": "neo-web",
"io.kubernetes.pod.name": "neo-staging-web-v201-dqxj5",
"io.kubernetes.pod.namespace": "backend"
}
}
}
}
</code></pre>
<p>MetricSet 的 key 是这三个 labels 拼起来的,container 的创建时间是 <code class="prettyprint">spec.creation_time</code>。可以发现,我们有两份 container、pod 完全一样但 id 和创建时间却又细微差别的数据。所以我们基本可以确定 kubelet 返回的数据有问题。</p>
tag:tonyhan.dev,2014:Post/my-elixir-2016
2017-01-02T03:26:17-08:00
2017-01-02T03:26:17-08:00
我的 Elixir 2016
<p>回顾自己 2016 年的技术方面,Elixir 应该最值得一说了。这一年,写了一些、参与了一些、看到了一些、想了一些,正好在这个时间点记录下来,算是对此的总结,也或许能从我的视角看到 Elixir 的一些发展。</p>
<h2 id="_2">开源项目 <a class="head_anchor" href="proxy.php?url=#_2" rel="nofollow">#</a>
</h2>
<p>翻看今年的 <a href="proxy.php?url=https://github.com/tony612" rel="nofollow">GitHub</a>,虽然也就 400 多个提交,不算多,但却是一直坚持在业余时间写代码的结果,主要就是 Elixir。除了代码量的收获,所有 Elixir 相关项目总共到达 200 多个 star,也算是对自己的一种鼓励。</p>
<h3 id="exchat_3">ExChat <a class="head_anchor" href="proxy.php?url=#exchat_3" rel="nofollow">#</a>
</h3>
<p>上半年还在继续开发 <a href="proxy.php?url=https://github.com/tony612/exchat" rel="nofollow">ExChat</a> 项目,基本的群聊和私聊功能已经实现,但因为不擅长前端等各种原因,后来进展一直很慢,现在已经停止开发了。之后打算还是稍微维护一下,比如保持 Phoenix 版本更新,以及可以运行的状态,但目前没有增加新功能的计划了。除非有前端/客户端的同学感兴趣,让我只负责后端,倒是可以考虑重拾起来。</p>
<p><a href="proxy.php?url=http://tony612.com/after-liulishuo-hack-week" rel="nofollow">7 月份公司办了 Hack Week 活动</a>,现在看来算是一个转折点,ExChat 从那之后就没再动过。当时我们用 Elixir 做后端,还拿了二等奖。</p>
<h3 id="grpcelixir_3">grpc-elixir <a class="head_anchor" href="proxy.php?url=#grpcelixir_3" rel="nofollow">#</a>
</h3>
<p>从那之后,我开始想,要用 Elixir 做点什么。继续做 ExChat 还是再拿 Phoenix 做个什么应用? 结果是可能对 Phoenix 越来越了解,但一方面,我并不希望 Phoenix 变成 Elixir 的 Rails,另一方面,做这种项目,业务逻辑会占很多时间,以我当前的精力恐怕是很难在业余时间维护好的。那就造轮子吧,什么轮子呢?写个数据库啥的,以我的水平暂时怕是写不出来的,就整个简单的吧。碰巧 Hack Week 的项目需要用 <a href="proxy.php?url=http://www.grpc.io/" rel="nofollow">gRPC</a>,当时因为没有 Elixir 的库,只能勉强用 Ruby 搭了个 proxy,把一个 gRPC 服务转成了 HTTP。于是就想着要做个 gRPC 的 Elixir 实现,毕竟随着微服务越来越流行,加上 Google 的推广,gRPC 应该会被越来越多的使用,如果 Elixir 没有这样的轮子的话,岂不是很被动。</p>
<p>于是我剩下几个月就一直在折腾 <a href="proxy.php?url=https://github.com/tony612/grpc-elixir" rel="nofollow">grpc-elixir</a>,直到现在。一开始思路不太对,导致走了很多弯路,从九月份开始终于算是找到了方向。之后虽然有时会在一些问题上卡个几天,但整体还算比较顺利,到现在已经实现了 client 和 server 的基本功能(四种方式的调用),有了能跟 grpc-go 互相调用的 examples,Authentication 也快搞定了。接下来除了一些细节的处理,把 Benchmarking 做完,就比较可用了。</p>
<p>写 grpc-elixir 的过程还是有不少收获的。最大的感受是,Elixir 虽然年轻,但工具链简直完胜 Erlang,想想 Erlang 发展这么多年,OTP 做的是好,但工具链实在不怎么样,火不起来不是没有道理啊。而且我能感受到,Elixir 的生态已经在慢慢地超过 Erlang 的了,首先 Erlang 的库 Elixir 都能很方便地使用,另外我注意到一些高质量的库已经先考虑 Elixir 而不是 Erlang 了。其实我在刚开始写 grpc-elixir 的时候就想过要不要也支持 Erlang,毕竟有些逻辑应该是可以公用的,但考虑到效率问题,还是决定先用 Elixir 写了。</p>
<p>说到效率,这应该算是另一个收获。Elixir 在 <a href="proxy.php?url=http://elixir-lang.org/blog/2013/08/08/elixir-design-goals/" rel="nofollow">productivity 这一设计目标</a> 上确实做的不错,没有太多冗余代码,就能很容易地实现需求,而且写出的代码很简洁、直观,利用 pattern match 对于二进制数据处理也非常方便。<a href="proxy.php?url=http://elixir-lang.org/getting-started/meta/macros.html" rel="nofollow">宏</a>也是一个很好的工具,既能使得 API 很友好, 又能减少很多工作,对于 gRPC 这个需要有“生成代码”功能的项目来说实在很合适。当然,宏确实不是很好写,所以最好想清楚是不是一定需要使用宏,就算用也要尽量少用,毕竟像 Ecto 这种大面积使用宏的项目是 José 自己在写啊,真心学不来。</p>
<h3 id="the-zen-of-elixir_3">The Zen of Elixir <a class="head_anchor" href="proxy.php?url=#the-zen-of-elixir_3" rel="nofollow">#</a>
</h3>
<p>上个月整理了 <a href="proxy.php?url=https://github.com/tony612/the-zen-of-elixir" rel="nofollow">The Zen of Elixir</a>,用来收集一些“必看”的、“最好”的 Elixir 资料。因为一直觉得 Elixir 入门不算难,但要想学好还是需要多花点功夫的,而且并不是把语法、概念、文档记熟就够了,有些思想靠自己可能还是比较难体会到的。这些思想甚至不仅仅限于 Erlang/Elixir 方面,比如 José 自己就写过几篇很好的文章,我在很多时候即使不写 Elixir 都会想到,也经常会在 <a href="proxy.php?url=https://elixir-slackin.herokuapp.com/" rel="nofollow">Elixir slack</a> 上看到被不同的人贴出来。而这些好的资料可能会被淹没在一大堆文章、视频中,所以才创建了这个项目,希望能够让其他人更容易发现,也方便自己时不时能回顾一下。</p>
<h2 id="_2">社区 <a class="head_anchor" href="proxy.php?url=#_2" rel="nofollow">#</a>
</h2><h3 id="elixir-shanghai_3">Elixir Shanghai <a class="head_anchor" href="proxy.php?url=#elixir-shanghai_3" rel="nofollow">#</a>
</h3>
<p>很多技术人其实并不怎么喜欢参与社区,特别是线下的活动,我也并不热衷于参加各种活动,还是喜欢自己多研究技术、写写代码。只是有时觉得定期的交流还是很有必要的,每个人可以学到更多,对整个社区都会有帮助,社区氛围好了也会促进语言的发展,然后又对每个人产生帮助,这是一个正向的循环。年初就看到北京那边在办 Elixir 的 meetup,让我羡慕了好久,到了年中的时候就在想为什么不在上海这边也组织 meetup 呢,反正这事情谁做都是要做啊。于是就创建了 <a href="proxy.php?url=https://www.meetup.com/Elixir-Shanghai/" rel="nofollow">Elixir Shanghai meetup</a>,然后找公司借了场地,想办法联系各种小伙伴,找演讲者,终于办了第一次 meetup。到现在一共办了 4 次,并且形成了每两个月最后一个周六的周期,每次平均会有10个人左右。因为社区还比较小,不管是演讲者还是找参加者,其实都不是那么容易,但只要有人肯来分享相关的内容,有感兴趣的人肯来参加,就已经是不错的开始了。</p>
<p>搞线下活动最有意思的就是认识各种各样的人了,既有之前比较熟的 Ruby 社区的,也有完全不认识,只是因为喜欢 Elixir 而凑在一起的,之前可能是 Erlang、Python、Go、Java 等后端背景,甚至是前端、客户端的同学。也让我感到办这些活动是很有意义的,至少我自己觉得每次大家聚在一起,听演讲者讲各种有意思的主题以及其他同学精彩的讨论,都会受益匪浅,然后鼓励我保持不断地学习,再争取分享给大家。</p>
<p>当然也学到了很多,除了直接从其他人那里学到的,因为怕主题不够,我经常只能自己充数,迫使自己一直要有新的东西拿出来分享,就怕每次讲的太水被大家嫌弃了。</p>
<h3 id="elixir-china_3">Elixir China <a class="head_anchor" href="proxy.php?url=#elixir-china_3" rel="nofollow">#</a>
</h3>
<p>之前一直觉得,Ruby China 的成功不可复制,<a href="proxy.php?url=http://elixir-cn.com/" rel="nofollow">Elixir China</a> 应该很难像做得像 Ruby China 一样好,特别是现在社区比较小,语言流行度也不够,大家还不如潜心干点正事的好。近几个月突然态度开放了很多,有些东西,有比没有的好,即使做不成 Ruby China 那样的,但只要能沉淀一些内容,对其他 Elixir 学习者、开发者都是好事。虽然一个人的力量微不足道,产出的内容有限,但能贡献一点是一点咯,总会有人看到,会对别人有帮助的。所以最近还是经常有逛一下论坛,看看其他人的帖子,给个评论,有时会发发帖子。之前想发个分享,直接就发了,现在经常会选择先发到 Elixir China,再发到社交网络。</p>
<p>除了这些,最近还经常给 Elixir China 的代码提 issue 或者 PR,自己有空就 fix 一些 bug 或者加一些功能,既有了写代码的机会,又能给社区做点贡献。虽然我不觉得现在功能的完善是 Elixir China 当前最需要的(内容应该更重要),但还是能够改善用户体验,让大家更乐意去用。比如我在用的时候就发现不能直接上传图片、分享到微信的 title 不是帖子标题等可以改进的地方,都会有想要完善的冲动,于是就尝试去修复了。</p>
<p>话说回来,为什么会产生这些心理变化,我想应该是在运营 Elixir Shanghai 的原因,深知社区的运营靠一个人是搞不定的,还是需要大家的参与,而这里的“运营”基本等同于“参与”,任何人发一些不错的帖子或者是添几行简单的代码都是在“运营”这个社区,都是对社区很有帮助的。</p>
<h2 id="_2">生态 <a class="head_anchor" href="proxy.php?url=#_2" rel="nofollow">#</a>
</h2>
<p>我们常说,一个语言本身好不好可能并不会成为我们是否使用它的决定性因素,很多时候还要看它的生态。那 Elixir 现在的生态发展的如何呢?我不敢说看到了全部,但还是看到了很多有意思的事情。</p>
<p>应该大部分的 Elixir 开发者都会订阅(几乎算官方的) <a href="proxy.php?url=http://plataformatec.com.br/elixir-radar/" rel="nofollow">Elixir Radar</a>,它从 2015 年二月份到现在,几乎每周一期,已经有 79 期了,一开始只是些文章,然后又加进去了免费的招聘信息、各地的 Meetup 和 conf 信息。开始时招聘一般只有三四个,现在已经有几十个,实在太多放不下,他们干脆做了个<a href="proxy.php?url=http://plataformatec.com.br/elixir-radar/jobs" rel="nofollow">招聘版块</a>。每次有 meetup 活动的时候,我都会联系他们,把 meetup 信息放上去,最近的一次居然没有了,后来收到他们的一封邮件说,Meetup 列表实在太长了,让大家还是直接去 <a href="proxy.php?url=https://www.meetup.com/topics/elixir/" rel="nofollow">Elixir Meetup</a> 看吧,真是哭笑不得,让人感叹 Elixir 社区发展速度之快。</p>
<p>之前 Elixir 有两个 mailing list,一个是 <a href="proxy.php?url=https://groups.google.com/forum/#!forum/elixir-lang-core" rel="nofollow">elixir-lang-core</a> 用于讨论 Elixir 核心的一些开发,一个是 <a href="proxy.php?url=https://groups.google.com/forum/#!forum/elixir-lang-talk" rel="nofollow">elixir-lang-talk</a> 用于讨论问题和其他讨论。而现在 elixir-lang-talk 的 mailing list 已经放到 <a href="proxy.php?url=https://elixirforum.com/" rel="nofollow">elixirforum.com</a> 的 <a href="proxy.php?url=https://elixirforum.com/c/elixir-chat" rel="nofollow">Elixir Chat</a> 分类了,还是挺惊讶于 Elixir 团队这个决定的,毕竟印象中,很多语言核心团队的开发者都很喜欢用 mailing list,但其实比较一下就能发现之前 mailing list 每个帖子只有几十个浏览量,现在基本都是几百甚至上千,说明大家还是很喜欢和能够接受这种形式,毕竟体验更好嘛。另外,Elixir 团队成员也经常在这个论坛中发帖或者回复,<a href="proxy.php?url=https://elixirforum.com/c/elixir-news" rel="nofollow">Elixir News</a> 更是已经成为了官方发布一些新消息的地方。</p>
<p>Elixir 语言本身则从 1.2 到了 1.4-rc,有关注过最近几次 Elixir Conf 上 José 的演讲的同学可能注意到,Elixir 除了语言层面的改动以及关注开发者的体验,现在核心团队非常关注如何更好地用 Elixir 来解决实际的问题,比如 <a href="proxy.php?url=https://github.com/elixir-lang/gen_stage" rel="nofollow">GenStaging</a> 关注的是<a href="proxy.php?url=https://www.youtube.com/watch?v=srtMWzyqdp8&list=PLE7tQUdRKcyYoiEKWny0Jj72iu564bVFD&index=15" rel="nofollow">如何更好的处理数据</a>, <a href="proxy.php?url=https://github.com/elixir-lang/elixir/blob/v1.4/CHANGELOG.md#registry" rel="nofollow">Registry</a> 则是从 Phoenix 代码中提取出来,让大家方便地使用。</p>
<p>其他的还有很多变化,比如 Elixir API 文档现在跟其他所有的 hex package 文档都<a href="proxy.php?url=https://hexdocs.pm/elixir/" rel="nofollow">放在一起了</a>,<a href="proxy.php?url=https://github.com/doomspork/elixir-companies" rel="nofollow">使用 Elixir 的大大小小的公司</a>也在变多,出现了<a href="proxy.php?url=https://elixirforum.com/t/elixir-events-in-2017/2998" rel="nofollow">各种各样的 conf</a>,<a href="proxy.php?url=https://github.com/h4cc/awesome-elixir" rel="nofollow">awesome-elixir</a> 的列表变得越来越长,ThoughtWorks 的<a href="proxy.php?url=https://www.thoughtworks.com/radar/languages-and-frameworks" rel="nofollow">技术雷达</a>将 Elixir 和 Phoenix 都列为 TRIAL。</p>
<p>而在中国,参加 Meetup 的人慢慢变多,而且每次都有新的面孔。在线上,不管是 <a href="proxy.php?url=https://elixir-slackin.herokuapp.com/" rel="nofollow">Slack</a> 的 #china channel,还是 QQ 或微信群,大家经常会有一些讨论。而且身边就有一些同学已经在生产中使用,以及写一些开源项目,造各种轮子了。</p>
<h2 id="elixir-2017_2">Elixir 2017 <a class="head_anchor" href="proxy.php?url=#elixir-2017_2" rel="nofollow">#</a>
</h2>
<p>2017 年这些又会有什么变化呢?不知道,但应该会变得更好。</p>
<p>有些人说将来 Elixir 会变得很流行,有些人会问将来到底什么时候会来。我当然也希望 Elixir 会真的变得很流行,而且越快越好,但不管那一天是哪一天,甚至会不会到来,我还是会继续保持关注和学习,毕竟这是门值得学习的语言,毕竟我可以愉快地以高的效率写下高效率运行的代码。</p>
tag:tonyhan.dev,2014:Post/using-env-in-elixir-config
2016-12-22T08:56:53-08:00
2016-12-22T08:56:53-08:00
在 Elixir config 中使用 ENV 的一点技巧
<p>看到了 Erlang Solution 的<a href="proxy.php?url=https://www.erlang-solutions.com/blog/elixir-module-attributes-alchemy-101-part-1.html" rel="nofollow">这篇文章</a>,想到了这个话题。这篇文章虽然内容不多,但还是挺有用的。文中提到的编译时和运行时的区别,也是刚学 Elixir 搞不太清的问题。</p>
<p>而 Elixir 的 config 也并不是简单的启动前执行的代码,特别是在部署时。</p>
<p>有时我们可能会想在 config 里使用 ENV,比如:</p>
<pre><code class="prettyprint lang-elixir">config :my_app, api_key: System.get_env("API_KEY")
</code></pre>
<p>但直接这样写到 config 中是不行的。在部署时,比如通过 distillery 运行 <code class="prettyprint">mix release</code> ,config 就会变成 sys.config,System.get_env(“API_KEY”) 已经被计算了,等运行的时候就不能动态得到 ENV 的实际值了。</p>
<p>有种做法就是像上边那篇文章中最后提到的方式,把 ENV 的获取逻辑放到函数里去做,这样就变成了运行时才会执行了:</p>
<pre><code class="prettyprint lang-elixir"># config.exs
config :my_app, api_key: {:env, "API_KEY"}
# my_app.ex
def api_key do
get_env(Application.get_env(:my_app, :api_key))
end
def get_env({:env, key}), do: System.get_env(key)
</code></pre>
<p>Phoenix 的 config 支持从 ENV 中获取 port 就是这样处理的:</p>
<pre><code class="prettyprint"># https://github.com/phoenixframework/phoenix/blob/996a83a27d8ccdc7e0e3bdda9c21d537b19b2002/installer/templates/new/config/prod.exs#L15
config :<%= app_name %>, <%= app_module %>.Endpoint,
http: [:inet6, port: {:system, "PORT"}]
# https://github.com/phoenixframework/phoenix/blob/2295ba7440221871b64c9535dec404c7d53589eb/lib/phoenix/endpoint/handler.ex#L57
defp to_port({:system, env_var}), do: to_port(System.get_env(env_var))
</code></pre>
tag:tonyhan.dev,2014:Post/after-liulishuo-hack-week
2016-07-10T07:30:45-07:00
2016-07-10T07:30:45-07:00
写在流利说 Hack Week 之后
<p>这周公司内部搞了为期一周的 Hackathon 活动,有十几支队伍参赛,很是热闹。</p>
<p>Hackathon 对我而言并不陌生,从大学起已经参加过三次 24 小时的,参与举办过一次 48 小时的。几乎每次参加 Hackathon 都会让我见识到很多牛人,也让我受到很大影响。</p>
<p>刚开始听说 Hackathon 就觉得很神奇、很让人热血沸腾,要在 24 小时完成构思、组队、开发、准备演示等全部的工作。粗想一下会觉得这怎么可能,但又让人按捺不住想去试一试的欲望。</p>
<p>第一次参加的景象还历历在目,它并不像现在很多的 Hackathon,有各种赞助商、高大上的场地,只是几个大学生发起的、面向大学生的,大概有二十个人参加,所有人都挤在一个可能还不到一百平米的住房里,待了一天时间,几乎都没怎么睡,就为做出一些东西。记得有几个师兄做了一个 web 上提交代码,然后返回编译结果的网站,对于当时基本不懂 web 开发的我,觉得这真是酷毙了。而我和另外一个朋友则用 C++ 写了一个 Windows 上类似泡泡堂的游戏。</p>
<p>除此之外,Hackathon 很有趣的另一点在于,因为很可能要和一些不认识的人组队,而大家的技术栈也不尽相同,所以经常需要快速学一个技术,然后马上做出点东西。虽然在时间本来就很紧张的几十个小时内还要去学个新技术,但因为目标明确,也倒挺有意思,而且经常有其他人带,所以很快就能上手。</p>
<p>从 Hackathon 也能看出技术的演进和趋势,开始是 Web,然后移动开发,现在则是各种硬件、人工智能,大家使用的编程语言也在不断变化。</p>
<p>公司的 Hack Week 跟以往的 Hackathon 差别其实很大。最明显的是时间长了,提前就可以想 idea、组队,最后有一周时间开发。这意味着不用拼命熬夜了,毕竟是持久战,但似乎也少了一些紧张感。想法可以更完整、成熟、目标性也更强,但也容易“想得太多”,而且少了一些说干就干的快感。因为是在公司内,大家都很熟悉,所以合作也更容易、分工也会比较明确,不过可能也少了更多思想的碰撞,少了一些向外部学习的机会。有更多的时间可以把作品做完整,但同时要求也会变得更高。</p>
<p>虽说如此,但因为每个队创意不同、组成不同、风格不同、时间不同(因为有时还是要处理公司的事情),所以大家的目标可能也不完全一样。</p>
<p>我们队,算是比较稳的风格,一开始就确定要做完,而且要做真正可用的产品。所以把需求分为三类:对展现产品没有帮助的、可做可不做的,就不做;基本的功能,或者是虽然有点难度,但能体现出价值、乐趣的,一定要做;对改善用户体验有帮助,但比较耗时、不确定因素较多的,最后根据时间来决定做不做。</p>
<p>整个过程中还算比较顺利,开始两天比较紧张地完成了基本功能,然后在此基础上完成剩下的部分,最后有一天左右主要用来修 bug、测试和准备 demo。</p>
<p>如果一直都照计划按部就班地完成,那估计会挺没意思的,于是就发生了一些计划外的有趣的事情。本来待定的 push 功能,中途因为自己用起来觉得实在太不舒服,决定改成必做的。也砍掉了一些开始讨论要做的功能,或者是进行了改造。最后两天,还做了简单的宣传主页,而且开放给公司内测,有意思的是,倒数第二天要结束的时候还根据用户反馈加了修改头像和昵称的功能。push 和小红点的通知,看似是小功能,其实花了蛮多精力去开发和调试,不过也蛮爽的,因为增强了可用性。</p>
<p>而我自己的成长,除了一些产品方面、甚至是推广方面的,技术方面也有不少。虽然对主要开发语言 Elixir 算比较熟悉了,但真正地部署到生产环境,给客户端接入、用户来用,应该还算是第一次(heroku 的 demo 就不算了)。这就意味着,所有生产环境可能会遇到的问题都要在一个新的技术下去想办法解决,从基本的如何自动化部署、配置管理,到简单的查看 log、修改数据,再到一些问题的调试,每一个都是新的挑战。</p>
<p>再说说最后的 demo。因为产品完成度还算不错,所以还算是比较顺利,把特点基本都展示出来了。虽然没有拿到大家很想要的第一,不过第二也还算不错,而且仔细想想,确实可以做的更好。</p>
<p>另外,不管是数据、人工智能、社交,甚至是纯技术等方面,都有一些很有创意和创造力的作品出现,大家都很有才啊。</p>
<p>期待下次这样的活动 :)</p>
<p>点后边这个链接可以看到这次 Hack Week 的官方总结 <a href="proxy.php?url=https://mp.weixin.qq.com/s?__biz=MzI0NjIzNDkwOA==&mid=2247483761&idx=1&sn=f04f38826bc494510084a86368797b6d&scene=1&srcid=0710NejBg9AWXjF5VhvOyC1G&from=singlemessage&isappinstalled=0&key=77421cf58af4a6539f67e4c99caadd72277d806045cf7278ce6f8181f660f4dfed1c96710003693310aa922dbcfe9390&ascene=1&uin=MjYwOTgwNDU%3D&devicetype=iPhone+OS9.3.2&version=16031610&nettype=WIFI&fontScale=100&pass_ticket=6MXg0%2FlIAaXaXIUthERC4JOVYKP2XW2TbutyJXi5i5w%3D" rel="nofollow">Never Stop Hacking!</a></p>
tag:tonyhan.dev,2014:Post/activerecord-vs-ecto
2016-05-13T21:02:39-07:00
2016-05-13T21:02:39-07:00
ActiveRecord 和 Ecto 的比较
<p><a href="proxy.php?url=http://guides.rubyonrails.org/active_record_basics.html" rel="nofollow">ActiveRecord</a> 是 <a href="proxy.php?url=http://rubyonrails.org/" rel="nofollow">Ruby on Rails</a> 的 Model 层,是一个 ORM(Object-relational mapping)。<a href="proxy.php?url=https://github.com/elixir-lang/ecto" rel="nofollow">Ecto</a> 是 <a href="proxy.php?url=http://elixir-lang.org/" rel="nofollow">Elixir</a> 实现的一个库,类似于 ORM。不管是不是 ORM,二者本质上都是在各自的语言层面,对于<strong>数据库操作提供了抽象</strong>,能让我们更方便地和数据库交互,而不是直接通过 SQL 的方式,并且对表中的数据做了映射,从而方便进行后续逻辑的处理。</p>
<p>这篇文章并不打算来争个孰优孰劣,很多时候对比的作用更是加深对于事物的认识。(当然不代表我本人没有倾向,只是希望大家能够尽量保持客观)</p>
<h2 id="_2">定义映射关系 <a class="head_anchor" href="proxy.php?url=#_2" rel="nofollow">#</a>
</h2>
<p>我们一般会以表为单位来操作数据库,二者也对表这个概念做了映射。假定我们有一个 <code class="prettyprint">users</code> 的表,那么它们的定义(这篇文章的代码示范大多会使用这个模型)如下:</p>
<pre><code class="prettyprint lang-ruby"># ActiveRecord
class User < ActiveRecord::Base
end
</code></pre>
<pre><code class="prettyprint lang-elixir"># Ecto
defmodule User do
use Ecto.Schema
schema "users" do
field :email, :string
end
end
</code></pre>
<p>可以看到,ActiveRecord 更“智能”,通过继承了 <code class="prettyprint">ActiveRecord::Base</code> 这个类,并且利用表名的<strong>转换约定</strong>(User 对应复数形式的表名 users)来完成映射。而 Ecto 中则是通过 DSL 定义了 schema,很显然,表名以及字段的指定都是<strong>显式指定</strong>的。</p>
<p>从代码上看,ActiveRecord 更简洁,但只看这个代码却不知道表名、字段等信息,需要通过查看 db schema 的定义来做进一步了解,而 Ecto 定义比较繁琐,但 schema 结构一目了然。</p>
<h2 id="_2">数据存放和使用 <a class="head_anchor" href="proxy.php?url=#_2" rel="nofollow">#</a>
</h2>
<p>让我们先跳过数据库操作,直接到数据被取出来之后的部分。我们来定义一个操作——把 email <code class="prettyprint">@</code> 前的部分提取出作为 name:</p>
<pre><code class="prettyprint lang-ruby"># ActiveRecord
class User < ActiveRecord::Base
def name
email.split('@').first
end
end
irb> user = User.first
=> #<User id: 1, email: "[email protected]">
irb> user.name
=> "foo"
</code></pre>
<pre><code class="prettyprint lang-elixir"># Ecto
defmodule User do
def name(user)
user.email |> String.split("@") |> List.first
end
end
iex> user = Ecto.Repo.get_by User, id: 1
%User{id: 1, email: "foo.example.com"}
iex> User.name(user)
"foo"
</code></pre>
<p>与其说是 ActiveRecord 和 Ecto 的比较,不如说是 Ruby 和 Elixir,甚至是面向对象语言和函数式语言的比较。ActiveRecord 的数据被存放到一个对象中,这个对象不光有<strong>数据</strong>,还有在类的定义中被赋予的<strong>行为</strong>,使用起来非常方便。Ecto 的<strong>数据和行为是分开的</strong>,数据用 <a href="proxy.php?url=http://elixir-lang.org/getting-started/structs.html" rel="nofollow">Struct</a>(类似于 C 语言中的 struct),行为则是通过函数,使用起来需要写的代码更多。</p>
<p>关于面向对象语言和函数式语言的比较网上已经有很多了,这里我仅仅从测试的角度来做进一步的对比:</p>
<pre><code class="prettyprint lang-ruby"># ActiveRecord
user = User.new(email: "[email protected]") # prepare data
assert user.name == "foo"
</code></pre>
<pre><code class="prettyprint lang-elixir"># Ecto
user = %User{email: "[email protected]"} # prepare data
assert User.name(user) == "foo"
</code></pre>
<p>乍一看,它们可能没有什么区别,但在准备数据的第一行却完全是两种做法。因为 <code class="prettyprint">User</code> 继承自 <code class="prettyprint">ActiveRecord::Base</code>,它会帮我们用传入的 attributes 来进行初始化操作,当然我们也可以在 <code class="prettyprint">User</code> 里自定义一些初始化行为。而 Ecto 中则只是用了 Elixir 的 Struct 来构造我们需要的数据,而没有任何行为。</p>
<p>前者更<strong>灵活、强大</strong>,比如我们可以在初始化时给一些字段赋上默认值。但这种灵活也伴随着风险与不可靠,当我们在调用 <code class="prettyprint">User.new</code> 时,其实我们不能确定得到的那个对象的 email 是否就是我们传入的,因为初始化代码里可以随意改变默认行为,并可能产生其他不必要的副作用。正是因为这种不确定,使得我们的测试其实没有看上去那么容易写。</p>
<p>而后者完全<strong>没有副作用</strong>,我们不需要担心得到的结果会跟预期的不一致,测试从而更加可靠。当然损失了一些灵活性,但很难说这到底是好还是坏,可能要在不同的场景下才能判断,也取决于不同人的喜好。</p>
<p><em>在进入下一部分前,其实前边的比较还没有结束,ActiveRecord 和 Ecto 都对关联关系做了抽象,其中也会体现出这些区别,但留到之后关联的部分再讲。</em></p>
<h2 id="_2">数据库查询 <a class="head_anchor" href="proxy.php?url=#_2" rel="nofollow">#</a>
</h2>
<p>我们来做一个很简单的查询操作——取出 id 为 2 的用户:</p>
<pre><code class="prettyprint lang-ruby"># ActiveRecord
user = User.where(id: 2).first
</code></pre>
<pre><code class="prettyprint lang-elixir"># ecto
user = Ecto.Repo.one(from u in User, where: u.id == 2)
</code></pre>
<p>在 ActiveRecord 中,只涉及到 <code class="prettyprint">User</code> 这一个 class 就可以完成全部的查询。而 Ecto 则涉及到 <a href="proxy.php?url=https://hexdocs.pm/ecto/Ecto.Repo.html" rel="nofollow"><code class="prettyprint">Ecto.Repo</code></a> 和 <a href="proxy.php?url=https://hexdocs.pm/ecto/Ecto.Query.html" rel="nofollow"><code class="prettyprint">Ecto.Query</code></a>(<code class="prettyprint">from</code> 是从 <code class="prettyprint">Ecto.Query</code> 引入的宏定义) 和 <code class="prettyprint">User</code>。</p>
<p>ActiveRecord 的一个类就完成了 Ecto 三个 Module 才完成的工作——指定要查询的表、设定查询条件和实际向数据库的查询,我们调用的时候只需要知道 <code class="prettyprint">User</code> 这一个,非常方便。</p>
<p><code class="prettyprint">Ecto.Repo</code> 是对于一个数据库的映射,可以说,一个 Repo 就是对于一个数据库的连接,可以是多个类型的数据库,比如 MySQL、PostgreSQL 甚至是 MongoDB,也可以是一个类型的多个数据库。可以看到,相比于 ActiveRecord 这样所有 <strong>DB 操作耦合</strong>在一个 class 的做法,Ecto 则显得更加灵活,因为查询条件和实际的查询操作、schema 定义和数据库连接是<strong>分开</strong>的。</p>
<p>可能很多人会觉得,还是 ActiveRecord 舒服,开始我也是这么觉得的,既然可以这么方便,为什么要弄得这么复杂呢?直到我遇到了更复杂的场景,比如一个项目里需要有<strong>多个不同数据库</strong>连接,甚至是同一个 Model 需要连接多个数据库,或者读写分离的需求。这时,对于 ActiveRecord,我想到的唯一解决方案就是——Google,因为从来没这样用过啊,而 schema 和查询又是耦合的,所以我知道只能通过对 ActiveRecord 的定制才能达到目的,而搜索到的解决方案靠谱吗?不确定,因为毕竟不是 ActiveRecord 擅长的应用场景。但对于 Ecto,自然就支持了,根本不用多想。</p>
<p>不要说你不会遇到这种情况,当项目越来越复杂时就自然会遇到了。当然,如果出发点就是 Demo 性质或者是小项目的话,那 ActiveRecord 是再合适不过了。</p>
<p>换个角度想,Ecto 真的复杂吗?看上去似乎是代码多了,每次实际查询都需要显式执行,而 ActiveRecord 则是当你调用特定方法时就会触发查询。但就像函数式语言一样,语法上的一些繁琐,反而带来了代码上的简洁。</p>
<p>当然,Ecto 也不是完美,在有些场景,ActiveRecord 更有优势,比如当需要把一个已经存在的项目的 Model 完全换为另一个数据库时,ActiveRecord 中可能就是把一个 Model 的连接改一下就行了,而 Ecto 似乎比较难以全局修改。</p>
<h3 id="_3">查询语法 <a class="head_anchor" href="proxy.php?url=#_3" rel="nofollow">#</a>
</h3>
<p>除此之外,二者的查询语法也各有千秋。ActiveRecord 定义了一系列比较语义化的方法,比如 <code class="prettyprint">where</code>, <code class="prettyprint">order</code>, <code class="prettyprint">group</code>, <code class="prettyprint">joins</code>, <code class="prettyprint">select</code> 等,通过不断调用就能得到结果。而 Ecto 则是定义了一套类似于 <a href="proxy.php?url=https://en.wikipedia.org/wiki/Language_Integrated_Query" rel="nofollow">LINQ</a> 的 DSL,能让我们<strong>像写 SQL 一样</strong>来写查询代码。</p>
<p>刚接触 ActiveRecord 的时候,觉得可以不写 SQL 实在是太爽了,甚至到现在也一直觉得 ActiveRecord 写起来很容易,就像 Ruby 语言一样优雅。但有时难免会碰到一些<strong>复杂的查询</strong>,比如涉及到 join,group,这时 ActiveRecord 写起来反而不是那么容易,很可能很容易就想出了 SQL,但还是不会写 ActiveRecord 风格的代码。因为对于复杂的查询,代码到 SQL 的转换可能不那么显而易见,最终只能通过 Google 来找到答案或者是直接用 string 来写 SQL。</p>
<p>Ecto 是另外一种优雅,从代码到 SQL 的转变可以说是直接对应起来的,知道了 SQL 基本就知道了代码怎么写,对于复杂查询可能更容易。比如文档里的这个例子,并不是很复杂,但已经可以说明问题:</p>
<pre><code class="prettyprint lang-elixir">from(p in Post,
group_by: p.category,
select: {p.category, count(p.id)})
</code></pre>
<h2 id="_2">数据写操作 <a class="head_anchor" href="proxy.php?url=#_2" rel="nofollow">#</a>
</h2>
<p>还是先来看一个例子——插入一条数据:</p>
<pre><code class="prettyprint lang-ruby"># ActiveRecord
class User < ActiveRecord::Base
validates_presence_of :email
end
irb> User.create!(email: "[email protected]")
</code></pre>
<pre><code class="prettyprint lang-elixir"># Ecto
defmodule User do
import Ecto.Changeset
def changeset(user, params \\ %{}) do
user
|> cast(params, ~w(email))
|> validate_required([:email])
end
end
iex> changeset = User.changeset(%User{}, %{email: "[email protected]"})
iex> Ecto.Repo.insert(changeset)
</code></pre>
<h3 id="_3">数据验证 <a class="head_anchor" href="proxy.php?url=#_3" rel="nofollow">#</a>
</h3>
<p>数据写操作其实与查询类似,ActiveRecord 全都通过 <code class="prettyprint">User</code> 这个 class 完成插入,而 Ecto 则需要通过 <code class="prettyprint">User</code> 和之前见过的 <code class="prettyprint">Ecto.Repo</code> 来完成,数据组装和实际写入是分开的。这里更关注的是写操作之外的,也就是数据验证等额外的操作,比如这里验证了 email 必须存在。ActiveRecord 是通过在类定义中调用方法来定义<strong>全局的 validations</strong>,当调用 <code class="prettyprint">create</code> 或 <code class="prettyprint">update</code> 等方法时就会自动调用验证。而 Ecto 则是通过这个新的 module <a href="proxy.php?url=https://hexdocs.pm/ecto/Ecto.Changeset.html" rel="nofollow"><code class="prettyprint">Ecto.Changeset</code></a> 来进行数据验证等处理。</p>
<p>对于 ActiveRecord,因为定义是全局的,所以调用写操作时不需要去关心验证的逻辑,缺点就是灵活性会受到限制,比如可能你需要在不同的场景下做不同的验证逻辑,像邮箱注册、手机注册、游客、第三方注册,因为是全局的约束,就使得所有的逻辑混在一起,错综复杂。</p>
<p>而 Ecto.Changeset 的思路是,每一个 changeset 就是<strong>一条验证的流程</strong>,比如你可以定义 <code class="prettyprint">email_signup_changeset</code>、<code class="prettyprint">phone_signup_changeset</code>、<code class="prettyprint">guest_changeset</code>、<code class="prettyprint">oauth_changeset</code>,他们互相不受影响,整个逻辑很清晰。而且 changeset 可以互相组合,比如定义一个公共的 <code class="prettyprint">changeset</code> 作为所有 changeset 的基础。当然,缺点就是调用的时候必须要显示指定一个 changeset,甚至可以不通过 changeset,代码上会相对比较麻烦。</p>
<h3 id="_3">回调 <a class="head_anchor" href="proxy.php?url=#_3" rel="nofollow">#</a>
</h3>
<p>ActiveRecord 中可以定义在写操作整个流程中各个关键点的回调逻辑,比如在写入之前构造一些字段,或是写入完之后做一些缓存、数据库的更新。</p>
<p>而 Ecto 2.0 之后就没有 callback 了,其实这是必然的,因为按 Ecto 的思路,schema 和数据库操作是分开的,那就无法在 schema 中定义各种回调了。另外就是,你真的需要回调吗?全局的回调不止带来了方便,也可能会引入了一些问题,因为这些自动触发的回调对开发者而言是隐藏的,加一行回调很简单,但当你加了越来越多的回调时,代码也就失控了。关于 Ecto 的 callback,可以看 José 写的<a href="proxy.php?url=http://blog.plataformatec.com.br/2015/12/ecto-v1-1-released-and-ecto-v2-0-plans/" rel="nofollow">这篇文章</a>。</p>
<h2 id="_2">关联关系 <a class="head_anchor" href="proxy.php?url=#_2" rel="nofollow">#</a>
</h2>
<p>我们不会只有一个表,很多时候数据库的操作需要涉及到多个表以及他们之间的关系,ActiveRecord 和 Ecto 也都对此做了抽象,比如 one-one、one-many、many-to-many。</p>
<p>我们在 User 的基础上加入 posts 这个表(id, title, user_id)来做说明。二者的定义都大同小异:</p>
<pre><code class="prettyprint lang-ruby"># ActiveRecord
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
</code></pre>
<pre><code class="prettyprint lang-elixir"># Ecto
defmodule User do
schema "users" do
has_many :posts, Post
end
end
defmodule Post do
schema "posts" do
field :title, :string
belongs_to :user, User
end
end
</code></pre>
<p>但从使用开始就产生了区别:</p>
<pre><code class="prettyprint lang-ruby"># ActiveRecord
irb> user = User.first
=> #<User id: 1, email: "[email protected]">
irb> user.posts # 发生了数据库查询
=> [#<Post id: 1, title: "Post 1", user_id: 1>,
#<Post id: 2, title: "Post 2", user_id: 1>]
</code></pre>
<pre><code class="prettyprint lang-elixir"># Ecto
iex> user = Ecto.Repo.get_by User, id: 1
%User{id: 1, email: "foo.example.com", posts: #Ecto.Association.NotLoaded<association :posts is not loaded>}
iex> user.posts
#Ecto.Association.NotLoaded<association :posts is not loaded>
iex> user = Ecto.Repo.preload(user, :posts) # 发生了数据库查询
iex> user.posts
[%Post{id: 1, title: "Post 1", user_id: 1},
%Post{id: 2, title: "Post 2", user_id: 2}]
</code></pre>
<p>可以看到,ActiveRecord 依旧延续自己的风格,<code class="prettyprint">user.posts</code> 这个方法调用就<strong>自动做了数据库查询</strong>。</p>
<p>而对于 Ecto,<code class="prettyprint">user.posts</code> 不是方法调用,只是取了 Struct 的一个值,它在 user 被取出来后就存在于 struct 中,它本身又是一个 Struct <code class="prettyprint">Ecto.Association.NotLoaded</code>。正如这个名字暗示,posts 还<strong>没有被从数据库中加载</strong>出来,一直到我们显示通过 <code class="prettyprint">preload</code> 调用之后。</p>
<p>Ecto 这样做的目的是什么呢? 或许我们可以看看 ActiveRecord 这种做法有什么不好,数据库查询就像方法调用一样简单,所以在你不经意的时候,就产生了数据库查询,会进一步拖慢我们的程序。而 Rails 中经常发生的 n+1 的查询问题,真的是开发者能力不够,总是忘记这个性能问题吗?并不完全是,当你在 view 里随便调用一个方法就做了查询时,其实很多时候你是比较难意识到的,可以说 ActiveRecord 的这种方便,使得代码更容易产生性能问题。</p>
<p>而 Ecto 从一开始就试图去减少这种问题,当一个 <code class="prettyprint">Ecto.Association.NotLoaded</code> 被使用时会直接报错,Ecto 通过<strong>强制、显式</strong>的关联查询,让开发者更能意识到代码产生的影响。当然你也可以在 view 的循环体内去通过 <code class="prettyprint">Repo.preload</code> 来查询,但这时你应该是知道你在做什么的。好的框架或者库可以帮你减少错误的发送,但却不能完全避免。</p>
<h2 id="_2">总结 <a class="head_anchor" href="proxy.php?url=#_2" rel="nofollow">#</a>
</h2>
<p>ActiveRecord 和 Ecto 很像,甚至 Ecto 从 ActiveRecord 借鉴了很多,但通过比较后,大家应该可以发现,二者其实是对于同一问题的两种风格迥异的解决方案。ActiveRecord <strong>简便、强大</strong>,帮你做了很多事情,但缺点也是帮你做了一些可能不该做的事情。Ecto 因为在 ActiveRecord 之后才产生,所以除了借鉴,还在 ActiveRecord 做的不够好的地方做了改善,更<strong>透明</strong>、更有<strong>约束</strong>力,<strong>松耦合</strong>,但有些地方相对更繁琐。</p>
<p>欢迎大家交流、指正,可能我的一些理解还不到位,所以有失偏颇。</p>
<p><em>(想了解 Ecto 或者 Elixir 的可以关注 Elixir Shanghai meetup <a href="proxy.php?url=http://www.meetup.com/Elixir-Shanghai/" rel="nofollow">http://www.meetup.com/Elixir-Shanghai/</a> 来一起交流学习)</em></p>
tag:tonyhan.dev,2014:Post/does-3-percent-users-matter
2016-03-21T08:22:50-07:00
2016-03-21T08:22:50-07:00
Does 3% users matter?
<p>好久没写博客,这几个月来的第一篇,居然不是技术的,实在是有愧于自己程序猿这个身份。无奈技术的博大精深,积累一篇高质量的文章实在是不容易,写的太烂也不敢发出来,怕被同行嗤笑。反倒因为不是做产品,才敢来聊一聊产品,虽然这雾中花,也不是我这俗人可以洞见的,但不论优劣,起码不至于会被骂得太惨。</p>
<p>这开头扯的差不多满意了,可以开始正文了。</p>
<p>早上开了一个会,提到了一个产品功能设计的一个依据是,3% 的用户如何如何,其它比例的如何如何。当时听了,觉得挺合理的,现在 PM 用数据把程序猿的嘴堵的严严的,有进步啊。但之后算了一下,按流利说的官方数字( <a href="proxy.php?url=http://liulishuo.com/about" rel="nofollow">http://liulishuo.com/about</a> )——25,000,000 用户——来算的话,这 3% 有 750,000 个用户。</p>
<p>在这里,我意识到第一件事——思维惯性有多么可怕。3% 这个数字看起来不算很多,但当它放到不同上下文中去时,所代表的分量却不尽相同。即便是 1% 的比例,也可能影响着数以万计的用户。一个产品量级越大,相同的比例所影响的用户也就越多。当然,量级小的产品,比例的分量可能反而更重。所以比例这个数据能起到多少作用,真的是一个很模糊的问题。</p>
<p>随后,我又意识到另外一个问题——这个数据足以支撑一个决策吗?这些数据只是某几类用户的不同占比,但这些就足够了吗?如果这 3% 的用户日活都很不错呢?如果这 3% 的用户都是很优质的用户呢?印象中好像没有听到有关于这方面的数据,但如果假设成立的话,那是不是就意味着,这类用户反而需要我们投入更多的精力、花更多的心思呢?</p>
<p>这里引申出来的一个问题是,现在很流行 data-driven,但似乎并不是拿出了数据就可以用来 driven 了,起码得保证数据是可靠的、全面的吧。感觉很可能发生的事情是,有了一个想法,就想拿数据验证一下,然后也确实找了一些数据,也确实能够验证那个想法之后,就认为这是可行的,但有可能那些数据只是冰山一角。结果产品死了之后,还一定要说,我明明是 data-driven 的啊。就好像那句不知道是谁说的话一样,“人们只相信自己愿意相信的”。</p>
<p>好像一说起产品的问题,任何理论都有反面。即便是数据全部都没问题,就一定能证明决策的正确性吗?好像也不见得。重新回到标题,如果数据靠谱、全面,3% 的用户真的重要或者不重要吗?或者有时数据真的只能作为参考,还是得跟着心走,不能怂。</p>
<p>不行了,实在编不下去了,再往下恐怕自己都看不懂自己写的东西了。还是留点精神晚点看苹果发布会好了,虽然帮主不在,但毕竟苹果有些事儿,在我了解的范围内,做的还真是厚道,比如最近为了用户隐私和 FBI 撕逼那件事情。</p>