<?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>nicolasyang&#39;s blog</title>
    <link>https://blog.nicolasyang.me/</link>
    <description>Recent content on nicolasyang&#39;s blog</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>zh-hans</language>
    <copyright>nicolasyang. All rights reserved.</copyright>
    <lastBuildDate>Tue, 09 Sep 2025 20:22:18 +0800</lastBuildDate>
    
        <atom:link href="https://blog.nicolasyang.me/index.xml" rel="self" type="application/rss+xml" />
    
    
    
        <item>
        <title>避免 soft RAID 会在硬盘故障时卡住：启用硬盘的 ERC 功能</title>
        <link>https://blog.nicolasyang.me/posts/why-soft-raid-hangs-on-disk-failure-hdd-erc/</link>
        <pubDate>Tue, 09 Sep 2025 20:22:18 +0800</pubDate>
        
        <guid>https://blog.nicolasyang.me/posts/why-soft-raid-hangs-on-disk-failure-hdd-erc/</guid>
        <description>nicolasyang&#39;s blog https://blog.nicolasyang.me/posts/why-soft-raid-hangs-on-disk-failure-hdd-erc/ -&lt;p&gt;之前我的 &lt;a href=&#34;https://blog.nicolasyang.me/posts/pve-zfs-mirrored-root-recovery-from-disk-failure&#34;&gt;PVE 出现硬盘故障的时候&lt;/a&gt;，整个母机都阻塞住了，本来用于高可用的 ZFS mirror, 并没有启动预期中的作用。这里的原因是什么呢？我们来看一段日志：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[    3.867520] ZFS: Loaded module v2.1.11-pve1, ZFS pool version 5000, ZFS filesystem version 5
[   35.998221] ata2.00: exception Emask 0x0 SAct 0x700 SErr 0x0 action 0x6 frozen
[   36.011820] ata2.00: failed command: WRITE FPDMA QUEUED
[   36.018667] ata2.00: cmd 61/10:40:10:0a:20/00:00:00:00:00/40 tag 8 ncq dma 8192 out
                        res 40/00:01:00:00:00/00:00:00:00:00/00 Emask 0x4 (timeout)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;看起来这个写操作花了超过 30 秒才返回失败。可以想象，如果某个进程在运行过程中也需要访问故障硬盘上的数据，就需要等待很久直到超时返回。由于 linux 中硬盘 I/O 是阻塞的，即使有 ZFS mirror, 也要等到故障的硬盘返回失败，才能去尝试读写下一个硬盘。 Linux 也没有可以中断一个已经启动的硬盘 I/O 操作的机制，软件是没办法控制超时时间的，具体会阻塞多久，完全取决于硬盘在什么时候返回错误。&lt;/p&gt;
&lt;h1 id=&#34;硬盘的存储和读取机制&#34;&gt;硬盘的存储和读取机制&lt;/h1&gt;
&lt;p&gt;至于硬盘在出现故障时为什么要阻塞很久，而不是立刻返回错误，这要从硬盘存储和读取数据的方式说起。&lt;/p&gt;
&lt;p&gt;现代硬盘的数据密度非常大，读取时，磁头上产生的信号十分微弱，读取过程其实是一个复杂的信号检测问题。在硬盘变旧，或者受到了什么外来损伤时，磁盘上的数据不一定会立刻消失，而可能是信号变弱。为此，硬盘有前向纠错 (FEC) 机制&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;，在信号检测中产生少量误码时，可以解码出正确的数据。在信号弱到能否解码的临界点时，随机噪音就会成为决定解码是否成功的因素。因此，硬盘会在解码失败时反复尝试读取，试图解码出正确的数据。&lt;/p&gt;
&lt;h1 id=&#34;与-raid-的冲突和解决方案&#34;&gt;与 RAID 的冲突和解决方案&lt;/h1&gt;
&lt;p&gt;FEC 和重试是很好的机制。在桌面平台使用的时候，有时旧硬盘出现故障的最初现象，就是读写变慢。但这个机制和 RAID 的设计却是冲突的。 RAID 是一种高可用机制，我们希望在硬盘出故障的时候系统还能继续运行，我们的应用服务不会中断。为此，我们需要出现故障的时候，尽快返回错误，让 RAID 控制器或者操作系统 (soft RAID 场景下) 切换到没有故障的硬盘上继续工作。&lt;/p&gt;
&lt;p&gt;为此，硬盘提供了 Error Recovery Control 功能，让 RAID 控制器或者操作系统，可以指定读写操作的超时时间，避免硬盘长时间阻塞在重试过程中。一般硬件 RAID 卡会自动使用这个功能，但如果用的是各种 soft RAID 方案，比如 mdraid, btrfs, ZFS 等，就需要通过软件来启用这个功能。&lt;/p&gt;
&lt;h1 id=&#34;配置-error-recovery-control&#34;&gt;配置 Error Recovery Control&lt;/h1&gt;
&lt;p&gt;Error Recovery Contorl 功能通过 SMART Command Transport (SCT) 命令控制。我们可以用 smartctl 或者 smartd 来控制这个功能。&lt;/p&gt;
&lt;p&gt;首先需要确认硬盘是否支持这个功能， &lt;code&gt;smartctl -c /dev/sda&lt;/code&gt; 可以列出设备支持的 SMART 能力。如果有显示 “SCT Error Recovery Control supported.”, 那就是支持这个功能。&lt;/p&gt;
&lt;p&gt;在支持这个功能的情况下，执行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;smartctl -l scterc /dev/sda&lt;/code&gt; 可以显示当前的配置。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;smartctl -l scterc,&amp;lt;READTIME&amp;gt;,&amp;lt;WRITETIME&amp;gt; /dev/sda&lt;/code&gt; 可以设置设置读写超时时间，单位为分秒。如 &lt;code&gt;scterc,70,70&lt;/code&gt; 会把超时时间设置为 7 秒。7 秒也是 RAID 场景下推荐的超时设置。&lt;/p&gt;
&lt;p&gt;注意：大部分硬盘支持的最小设置是 6.5 秒。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;smartctl -l scterc,&amp;lt;READTIME&amp;gt;,&amp;lt;WRITETIME&amp;gt;,p /dev/sda&lt;/code&gt; 可以把超时时间持久化保存到硬盘上，以后每次通电时都会应用这个设置。&lt;/p&gt;
&lt;p&gt;注意：不是所有支持 SCT Error Recovery Control 的硬盘都能持久化保存设置。我手上的两款硬盘， HGST HC320 支持， 另一款 WD 红盘就不支持。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果硬盘不支持持久化设置，或者你不希望把设置持久化到硬盘上，也可以在 smartd.conf 中使用 &lt;code&gt;-l scterc,&amp;lt;READTIME&amp;gt;,&amp;lt;WRITETIME&amp;gt;&lt;/code&gt; 来让 smartd 在启动时去设置指定的硬盘。&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;HGST. Iterative Detection Read Channel Technology in Hard Disk Drives. 2008-11. &lt;a href=&#34;https://web.archive.org/web/20181010031401/https://www.hgst.com/sites/default/files/resources/IDRC_WP_final.pdf&#34;&gt;https://web.archive.org/web/20181010031401/https://www.hgst.com/sites/default/files/resources/IDRC_WP_final.pdf&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
- https://blog.nicolasyang.me/posts/why-soft-raid-hangs-on-disk-failure-hdd-erc/ - nicolasyang. All rights reserved.</description>
        </item>
    
    
    
        <item>
        <title>启用 smartd 的定期 self-test 和通知服务</title>
        <link>https://blog.nicolasyang.me/posts/smartd-scheduled-self-test-and-notification/</link>
        <pubDate>Sun, 17 Aug 2025 15:34:56 +0800</pubDate>
        
        <guid>https://blog.nicolasyang.me/posts/smartd-scheduled-self-test-and-notification/</guid>
        <description>nicolasyang&#39;s blog https://blog.nicolasyang.me/posts/smartd-scheduled-self-test-and-notification/ -&lt;h1 id=&#34;smartdconf-配置格式&#34;&gt;&lt;code&gt;smartd.conf&lt;/code&gt; 配置格式&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;smartd.conf&lt;/code&gt; 的格式是每行一个设备；每行开头是设备名，后面跟着用于该设备的参数。如：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;/dev/sda -n standby -m root -M exec /usr/share/smartmontools/smartd-runner
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中，设备名可以用 &lt;code&gt;DEVICESCAN&lt;/code&gt; 代替。 &lt;code&gt;DEVICESCAN&lt;/code&gt; 会扫描未被单独配置的所有硬盘，并把本行指定的参数应用到这些硬盘上。另外，&lt;code&gt;DEVICESCAN&lt;/code&gt; 应该是最后一行有效配置，后面的配置都会被 smartd 忽略。&lt;/p&gt;
&lt;p&gt;我现在不需要对每块硬盘单独配置，所以只需要一行 &lt;code&gt;DEVICESCAN -d removable&lt;/code&gt;. 这里的 &lt;code&gt;-d removable&lt;/code&gt; 让 smartd 在硬盘消失时不会报错。&lt;/p&gt;
&lt;p&gt;其它的参数中， &lt;code&gt;-n standby&lt;/code&gt; 用于忽略 standby 状态（停转）的磁盘（我们不希望为了读取 SMART 状态把停转的硬盘唤醒起来）； &lt;code&gt;-m root&lt;/code&gt; 指定通知邮件的收件人为 root; &lt;code&gt;-M exec /usr/...&lt;/code&gt; 指定了用于发送邮件的命令。&lt;/p&gt;
&lt;h1 id=&#34;打开定期-short-self-test&#34;&gt;打开定期 short self-test&lt;/h1&gt;
&lt;p&gt;SMART short self-test 是硬盘自带的 self test, 具体的实现细节是由硬盘固件决定的。但根据规范，一般包含以下部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对电路部分的测试：包括缓存 RAM, 读写电路。&lt;/li&gt;
&lt;li&gt;寻道测试：检测能不能正确寻道。&lt;/li&gt;
&lt;li&gt;局部读取测试：检测能不能完成数据读取。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;定期运行 short self-test 有助于我们及时发现故障的硬盘。&lt;/p&gt;
&lt;p&gt;在 smartd.conf 中， self-test 由 &lt;code&gt;-s&lt;/code&gt; 开关控制。具体的格式为：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;-s T/MM/DD/d/HH
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T&lt;/code&gt; 是测试类型，S 是 short, L 是 long.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MM/DD/d/HH&lt;/code&gt; 是月、日、星期几、小时。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这串参数是正则格式，所以如果我们要每周测试一次，就可以写成：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;S/../../1/00
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中 &lt;code&gt;.&lt;/code&gt; 在正则里面是匹配任意字符，把它放在 MM/DD 字段，意为匹配任意月份和日期，&lt;code&gt;1&lt;/code&gt; 是星期一， &lt;code&gt;00&lt;/code&gt; 是 0 点；也就是在每周一 0 点运行 short self test.&lt;/p&gt;
&lt;h1 id=&#34;使用-http-api-发送通知&#34;&gt;使用 HTTP API 发送通知&lt;/h1&gt;
&lt;p&gt;默认的通知是基于邮件的，需要配置 MTA, 很复杂。不过 smartd 提供了插件机制来自定义通知。&lt;/p&gt;
&lt;p&gt;smartd 的插件机制会在收件人 &lt;code&gt;-m&lt;/code&gt; 参数设置为 &lt;code&gt;@&amp;lt;plugin&amp;gt;&lt;/code&gt; 时，去调用 &lt;code&gt;smartd_warning.d/&amp;lt;plugin&amp;gt;&lt;/code&gt; 命令来发送通知，而不会去调用邮件。 &lt;code&gt;smartd_warning.d/&lt;/code&gt; 目录的路径在不同发行版上可能不一样。在 debian 上，完整路径是 &lt;code&gt;/etc/smartmontools/smartd_warning.d/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;我这里用 pushover 来发送通知，使用的脚本是：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;#!/bin/sh
curl -X POST https://api.pushover.net/1/messages.json -d token=&amp;lt;token&amp;gt; -d user=&amp;lt;receiver_user_id&amp;gt; -d title=&amp;#34;SMART ${SMARTD_FAILTYPE}: ${SMARTD_DEVICE}&amp;#34; -d message=&amp;#34;${SMARTD_MESSAGE}&amp;#34; &amp;lt; /dev/null &amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;放置在 &lt;code&gt;smartd_warning.d/pushover&lt;/code&gt;. 对应的 &lt;code&gt;-m&lt;/code&gt; 参数为 &lt;code&gt;-m @pushover&lt;/code&gt;. 当然，不要忘记给 &lt;code&gt;smartd_warning.d/pushover&lt;/code&gt; 文件添加 x 权限。&lt;/p&gt;
&lt;p&gt;完整的 &lt;code&gt;smartd.conf&lt;/code&gt; 配置为：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;DEVICESCAN -d removable -n standby -m @pushover -M exec /usr/share/smartmontools/smartd-runner -s S/../../1/00
&lt;/code&gt;&lt;/pre&gt;&lt;h1 id=&#34;测试通知&#34;&gt;测试通知&lt;/h1&gt;
&lt;p&gt;为了检测通知能不能正确发送出去，我们需要测试一下。 smartd 提供了 &lt;code&gt;-M test&lt;/code&gt; 参数来在启动的时候发送一条测试通知。把配置改成：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;DEVICESCAN -d removable -n standby -m @pushover -M exec /usr/share/smartmontools/smartd-runner -M test -s S/../../1/00
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;重启 smartd.service, 就应该能收到标题为 &lt;code&gt;SMART EmailTest: /dev/sda&lt;/code&gt; 的通知。测试完成后，可以把 &lt;code&gt;-M test&lt;/code&gt; 删掉。&lt;/p&gt;
- https://blog.nicolasyang.me/posts/smartd-scheduled-self-test-and-notification/ - nicolasyang. All rights reserved.</description>
        </item>
    
    
    
        <item>
        <title>PVE zfs mirror 磁盘故障恢复</title>
        <link>https://blog.nicolasyang.me/posts/pve-zfs-mirrored-root-recovery-from-disk-failure/</link>
        <pubDate>Thu, 14 Aug 2025 19:18:48 +0800</pubDate>
        
        <guid>https://blog.nicolasyang.me/posts/pve-zfs-mirrored-root-recovery-from-disk-failure/</guid>
        <description>nicolasyang&#39;s blog https://blog.nicolasyang.me/posts/pve-zfs-mirrored-root-recovery-from-disk-failure/ -&lt;p&gt;两天前的凌晨，正在玩手机的我发现家里断网了。唤醒待机中的电脑，发现没有获取到 IPv4 地址。手动配上 IP 地址后打开路由器的管理界面，没发现什么问题，就尝试重启 dnsmasq, 但没有起作用。于是我决定重启整个 openwrt 路由器。&lt;/p&gt;
&lt;p&gt;然而路由器并没有完成重启，反而，现在连 luci 管理界面都挂了。尝试 ssh 登录路由器，以及 web/ssh 登录 PVEW 母机（我的 openwrt 是运行在 PVE 上的虚拟机），都没有成功，只能去 reset PVE 母机。&lt;/p&gt;
&lt;p&gt;重启后网络恢复了，但是检查 PVE 母机发现，两块硬盘组成的 zfs mirror degraded 了。 sdb 彻底失效，连 smart 数据都读不出来；同时 sda 的 smart 数据里有 pending sectors 和 offline uncorrected sectors. 当时我就觉得两块盘同时故障，可能恢复不了了。于是决定买两块盘替换掉故障硬盘，同时尽量拯救数据。不过事已至此，深夜就不要搞变更操作了，先睡觉，其它都第二天再说吧。&lt;/p&gt;
&lt;h1 id=&#34;故障止损&#34;&gt;故障止损&lt;/h1&gt;
&lt;p&gt;首先，随着这台 PVE 母机自动启动的只有至关重要的 openwrt 路由器，其它的虚拟机都是手动启动的。目前这个状态下我就没有去启动其它虚拟机了。&lt;/p&gt;
&lt;h2 id=&#34;zed-notification&#34;&gt;ZED Notification&lt;/h2&gt;
&lt;p&gt;ZFS 是有一个 zed 服务监听 zfs 相关的事件并发送通知的。默认应该是通过邮件发送，但是我没有配置 MTA (邮件还是太复杂了)。现在我决定要赶快把它配上。&lt;/p&gt;
&lt;p&gt;看了一下 zed 的配置文件，很好，它是自带 pushover 支持的。我本来就在使用 pushover 的服务，这里就直接配上 pushover 的 token, 重启 zed.service 即可。&lt;/p&gt;
&lt;h2 id=&#34;数据备份&#34;&gt;数据备份&lt;/h2&gt;
&lt;p&gt;第二天下单了两块新硬盘 + UAS 硬盘盒，同时开始准备备份数据。&lt;/p&gt;
&lt;p&gt;故障的这两块硬盘其实是 SMR 盘，所以我一开始就没把重要数据放在上面。上面的数据其实就是 PVE 系统、一个 openwrt 虚拟机、一个运行 ubnt controller 的 LXC. 另外还有几台虚拟机跑在这个母机上，但存储是通过 iSCSI 挂载在 NAS 上的。&lt;/p&gt;
&lt;p&gt;查了一下 PVE 的备份命令 vzdump 的手册，发现它可以把备份写到 stdout. 那么最好的备份方式就是通过 ssh vzdump 把备份直接保存在我的台式机上，避免再往已经出问题的硬盘上写入大量数据。备份命令为：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;ssh root@&amp;lt;pve-host&amp;gt; vzdump &amp;lt;vmid&amp;gt; --compress zstd --stdout &amp;gt; &amp;lt;vmid&amp;gt;.vma.zst
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Openwrt 虚拟机的磁盘只有十几 MB, 很快完成了。 运行 ubnt controller 的 LXC 更大一点，不过也顺利完成了备份，没有触发 I/O 错误。&lt;/p&gt;
&lt;h1 id=&#34;替换故障硬盘&#34;&gt;替换故障硬盘&lt;/h1&gt;
&lt;p&gt;收到硬盘后，首先关机用新硬盘换掉失效的 sdb. 开机后首先需要对硬盘进行分区。&lt;/p&gt;
&lt;h2 id=&#34;创建分区&#34;&gt;创建分区&lt;/h2&gt;
&lt;p&gt;一开始我准备用  &lt;code&gt;sfdisk --dump &amp;lt;old&amp;gt; | sfdisk &amp;lt;new&amp;gt;&lt;/code&gt; 直接把旧硬盘的分区表复制过去。但我买的新硬盘比旧硬盘大，复制分区表过去后发现没办法把分区扩大到整个硬盘的大小。（可能是 GPT 分区表里面还记录了整个磁盘的大小？）于是只能手动把分区抄过去。&lt;/p&gt;
&lt;p&gt;PVE 安装的时候会在硬盘上创建 3 个分区： BIOS BOOT, EFI SYSTEM PARITION, 和根分区。其中 BIOS BOOT 分区不是 4K 对齐的。这让我用分区工具手动创建的时候很不好操作。所以我就不创建 BIOS BOOT 分区了，反正我不需要 legacy boot.&lt;/p&gt;
&lt;h2 id=&#34;替换-zpool-设备&#34;&gt;替换 zpool 设备&lt;/h2&gt;
&lt;p&gt;执行 &lt;code&gt;zpool status rpool&lt;/code&gt; 命令（其中 rpool 是这个 zpool 的名称），会显示当前的状态：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;# zpool status rpool 
  pool: rpool
 state: DEGRADED
status: One or more devices could not be used because the label is missing or
        invalid.  Sufficient replicas exist for the pool to continue
        functioning in a degraded state.
action: Replace the device using &amp;#39;zpool replace&amp;#39;.
   see: https://openzfs.github.io/openzfs-docs/msg/ZFS-8000-4J
  scan: resilvered 1.21G in 00:15:09 with 0 errors on Sat Jul  8 17:18:50 2023
config:

        NAME                                       STATE     READ WRITE CKSUM
        rpool                                      DEGRADED     0     0     0
          mirror-0                                 DEGRADED     0     0     0
            ata-ST***********************H9-part3  ONLINE       0     0     0
            52***************90                    UNAVAIL      0     0     0  was /dev/disk/by-id/ata-ST***********************ZX-part3
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中， &lt;code&gt;UNAVAIL&lt;/code&gt; 的是我们要替换的设备。替换的命令是：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;zpool replace rpool 52***************90 /dev/disk/by-id/ata-HGST_HU********************YM-part3
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中，&lt;code&gt;52***************90&lt;/code&gt; 是前面 &lt;code&gt;zpool status&lt;/code&gt; 命令显示的 NAME, &lt;code&gt;/dev/disk/by-id...&lt;/code&gt; 是新的设备分区。执行后 zfs 会开始在后台 resilver, 即重建 mirror. 可以执行 &lt;code&gt;zpool status&lt;/code&gt; 命令查看进度。我这里需要复制 29 GB 的数据，即使是 SMR 盘，也不会花很长时间。&lt;/p&gt;
&lt;p&gt;不久后手机上收到了 pushover 的推送通知，resilver 顺利完成了。还好 sda 的 pending sectors 和 offline uncorrected sectors 没有影响到数据。&lt;/p&gt;
&lt;h2 id=&#34;重建引导&#34;&gt;重建引导&lt;/h2&gt;
&lt;p&gt;为了达到整个系统的高可用，除了根分区的 ZFS 是 mirror 的，PVE 还需要在两个硬盘上都建立 ESP, 安装上 systemd-boot、内核、initramfs, 并注册到 UEFI 启动项中。之前分区的时候已经创建了 ESP, 现在只要用 PVE 自带的工具就可以一键完成。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;proxmox-boot-tool format /dev/sdb2
proxmox-boot-tool init /dev/sdb2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;执行 init 的过程中会提示有磁盘 does not exist, 这是被替换掉的旧设备。根据提示去编辑 &lt;code&gt;/etc/kernel/proxmox-boot-uuids&lt;/code&gt;, 删除其中的旧设备。&lt;/p&gt;
&lt;p&gt;执行完后用 &lt;code&gt;efibootmgr -v&lt;/code&gt; 检查一下，发现旧的硬盘还在里面。执行 &lt;code&gt;efibootmgr --delete-bootnum -b &amp;lt;num&amp;gt;&lt;/code&gt; 删掉对应的旧启动项。&lt;/p&gt;
&lt;h2 id=&#34;替换-sda&#34;&gt;替换 sda&lt;/h2&gt;
&lt;p&gt;sda 有 pending sectors 和 offline uncorrected sectors, 也是处于非常不健康的状态，所以也要替换掉它。替换的过程和前面替换 sdb 完全一致。&lt;/p&gt;
&lt;h2 id=&#34;扩大文件系统&#34;&gt;扩大文件系统&lt;/h2&gt;
&lt;p&gt;这次换的新硬盘比旧硬盘大，所以还需要扩大文件系统。执行命令 &lt;code&gt;zpool set autoexpand=on rpool&lt;/code&gt; 打开 &lt;code&gt;autoexpand&lt;/code&gt;. 但是好像没有触发 expand. 试了一下 &lt;code&gt;zpool reopen rpool&lt;/code&gt; 也没效果。最后用&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;zpool offline rpool ata-HGST_HU********************YM-part3
zpool online rpool ata-HGST_HU********************YM-part3
zpool offline rpool ata-HGST_HU********************5M-part3
zpool online rpool ata-HGST_HU********************5M-part3
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;给两块硬盘分别切换 offline/online, 触发了 expand.&lt;/p&gt;
&lt;p&gt;Expand 完成后执行 &lt;code&gt;zpool set autoexpand=off rpool&lt;/code&gt; 关掉 &lt;code&gt;autoexpand&lt;/code&gt;.&lt;/p&gt;
&lt;h1 id=&#34;处理旧硬盘&#34;&gt;处理旧硬盘&lt;/h1&gt;
&lt;p&gt;两块旧硬盘，一块还能访问的，插到 UAS 硬盘盒里去执行 secure erase. 另一块彻底失效的，拆掉螺丝打开外壳，毁掉盘面。附上硬盘照片。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.nicolasyang.me/images/opened-hard-disk-case.jpg&#34; alt=&#34;opened hard disk case&#34;&gt;&lt;/p&gt;
&lt;p&gt;可以看到这是一块单盘双面的硬盘。另外有意思的是，以前我以为磁头是用弹簧固定在磁头架上的，只要断电就会回弹。但实际上这块硬盘的磁头拨到盘面上以后，是不会自动回弹的，所以断电紧急停靠大概是依赖电容存储的能量来驱动电机收回磁头。&lt;/p&gt;
&lt;p&gt;磁盘表面也真的是光滑到极致了，在划破磁盘表面之前，我去转动轴承，都看不出来盘面有没有跟着旋转。&lt;/p&gt;
&lt;h1 id=&#34;故障分析&#34;&gt;故障分析&lt;/h1&gt;
&lt;p&gt;首先，这里根因肯定在于 SMR 硬盘。我知道 SMR 不靠谱，但没想到这么不靠谱。根据 resilver 时的统计信息，我这个 zfs 上面就只有 29 GB 数据，会持续产生写入的只有 ubnt controller 使用的 mangodb. SMART 数据显示总共有 400 TB 的写入量，我很怀疑这个数据不准确。这两块硬盘在我这样的轻度负载下，运行了两年多，还没满 3 年保修期就坏了。&lt;/p&gt;
&lt;p&gt;其次，我没有配置 zed 故障告警是问题很大的。那天凌晨当机可能已经不是故障的第一现场了。如果我能早点发现问题，可能就不会像现在这样一块硬盘失效，另一块硬盘告警这么吓人了。但是现在要配置告警通知也很麻烦，一般普遍支持的方式都是邮件告警。但现在商业邮箱注册基本上都要验证手机号，我又不想和自己共用主力邮箱（担心数据安全），要自己搭建 MTA 那就更加复杂。还好现在 zed 支持 pushover 之类基于 HTTP API 的通知服务。&lt;/p&gt;
- https://blog.nicolasyang.me/posts/pve-zfs-mirrored-root-recovery-from-disk-failure/ - nicolasyang. All rights reserved.</description>
        </item>
    
    
    
        <item>
        <title>使用 nginx 作为 sniproxy</title>
        <link>https://blog.nicolasyang.me/posts/nginx-as-sniproxy/</link>
        <pubDate>Sun, 03 Aug 2025 16:12:49 +0800</pubDate>
        
        <guid>https://blog.nicolasyang.me/posts/nginx-as-sniproxy/</guid>
        <description>nicolasyang&#39;s blog https://blog.nicolasyang.me/posts/nginx-as-sniproxy/ -&lt;p&gt;把多个 HTTPS 服务部署在同一个 IP 上时，需要把传入连接根据 SNI 转发到不同的服务上。 Nginx 可以在不解密 TLS 的情况下，进行 SNI 代理。&lt;/p&gt;
&lt;h1 id=&#34;思路&#34;&gt;思路&lt;/h1&gt;
&lt;p&gt;由于我们不想在 nginx 里解密 TLS 流量，只是单纯进行 TCP 连接转发，所以这里需要用到的是 &lt;a href=&#34;https://nginx.org/en/docs/stream/ngx_stream_proxy_module.html&#34;&gt;nginx_stream_proxy_module&lt;/a&gt;. 为了能够根据 SNI 转发到不同后端，需要 &lt;a href=&#34;https://nginx.org/en/docs/stream/ngx_stream_ssl_preread_module.html&#34;&gt;ngx_stream_ssl_preread_module&lt;/a&gt; 来解析 TLS 协议头里的 SNI 字段。&lt;/p&gt;
&lt;h1 id=&#34;配置&#34;&gt;配置&lt;/h1&gt;
&lt;h2 id=&#34;模块安装和激活&#34;&gt;模块安装和激活&lt;/h2&gt;
&lt;p&gt;发行版打包的 nginx 可能是分模块、动态加载，并且拆包的。以 debian 为例，在 debian 12 里需要安装 &lt;code&gt;nginx-full&lt;/code&gt; 包，并且在 &lt;code&gt;nginx.conf&lt;/code&gt; 开头加入&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;load_module /usr/lib/nginx/modules/ngx_stream_module.so;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;才能用上四层转发功能。&lt;/p&gt;
&lt;h2 id=&#34;转发配置&#34;&gt;转发配置&lt;/h2&gt;
&lt;p&gt;首先，这里所有的内容都是属于 stream 模块的配置，需要写在 &lt;code&gt;stream {}&lt;/code&gt; 块里面。这里的关键配置包含两个部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;server&lt;/code&gt; 块中指定代理监听的地址、端口，打开 &lt;code&gt;ssl_preread&lt;/code&gt; 功能，并把转发目标指定为一个变量 &lt;code&gt;$upstream&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;map&lt;/code&gt; 块，根据 stream 模块预读提供的 &lt;code&gt;$ssl_pread_server_name&lt;/code&gt; 变量, 设置 &lt;code&gt;$upstream&lt;/code&gt; 的值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;stream {
    map $ssl_preread_server_name $upstream {
        foo.example.com  [::1]:2001;
        bar.example.com  unix:/run/bar/bar.socket;
        default unix:/nonexist;
    }

    server {
      listen        443;
      listen        [::]:443;
      proxy_pass    $upstream;
      ssl_preread   on;

      proxy_connect_timeout 15s;
      proxy_half_close      on;
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在这里，我们设置了两个转发的主机名，转发目标可以是 IP:port, 也可以是 unix domain socket. 并且在最后的 default 目标设置成了一个不存在的 unix domain socket, 让它能在收到无效的主机名时关闭连接。（网络上各种随机的扫描很多的）&lt;/p&gt;
&lt;p&gt;其余的参数可以按需添加，如 &lt;code&gt;proxy_half_close on&lt;/code&gt; 让这个代理对应用层看起来更加透明。&lt;/p&gt;
&lt;h1 id=&#34;完整配置&#34;&gt;完整配置&lt;/h1&gt;
&lt;p&gt;发行版的 nginx 默认配置可能很复杂。因为我们只用 sni proxy 功能，里面大部分都是我们不需要的。因此可以单独写一份最小配置来启动 nginx. Stream 转发功能是很简单的，我们需要关注的参数基本上就只有以下 3 个：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;worker_processes auto;        # worker 进程数， auto 为根据 CPU 数自动配置
worker_cpu_affinity auto;     # worker 的 CPU 亲和性，自动配置就可以了

events {
  worker_connections 1024;    # 每个 worker 的并发连接数，根据你的工作负载，和服务器性能选择一个合适的
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;合并在一起就是：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;load_module /usr/lib/nginx/modules/ngx_stream_module.so;

worker_processes auto;
worker_cpu_affinity auto;

events {
  worker_connections 1024;
}

stream {
    ...
}
&lt;/code&gt;&lt;/pre&gt;- https://blog.nicolasyang.me/posts/nginx-as-sniproxy/ - nicolasyang. All rights reserved.</description>
        </item>
    
    
    
        <item>
        <title>32 位二进制是如何跑在 64 位操作系统上的</title>
        <link>https://blog.nicolasyang.me/posts/how-x86-app-runs-on-64-bit-os/</link>
        <pubDate>Sat, 24 May 2025 17:22:45 +0800</pubDate>
        
        <guid>https://blog.nicolasyang.me/posts/how-x86-app-runs-on-64-bit-os/</guid>
        <description>nicolasyang&#39;s blog https://blog.nicolasyang.me/posts/how-x86-app-runs-on-64-bit-os/ -&lt;p&gt;现在电脑上的 CPU 和操作系统都是 64 位的了，x86 架构扩展到 64 位带来了很多好处，比如：更大的地址空间让你可以直接 mmap 一个几百 GB 的文件；更多的寄存器让内存访问减少，应用性能更好。但还是有很多应用程序是 32 位的，特别是 windows 上那些闭源软件，即使一直在更新，也还是 32 位的。那么问题来了： 32 位的 x86 二进制是怎么在 64 位 CPU 和操作系统上运行的呢？实际上，操作系统和 CPU 分别在软件和硬件上都做了不少工作。&lt;/p&gt;
&lt;h1 id=&#34;操作系统的接口兼容&#34;&gt;操作系统的接口兼容&lt;/h1&gt;
&lt;p&gt;Windows 上的兼容层是 WoW64, 它的主要功能包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;分配进程的虚拟地址空间时，只使用 32 位地址能寻址的范围。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在应用调用 windows API 时，做 32 位和 64 位之间的结构体双向翻译。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而 Linux 上的做法是在内核里保留了原来的 32 位系统调用 (&lt;a href=&#34;https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_32.tbl&#34;&gt;syscall_32.tbl&lt;/a&gt;), 那么 userland 里：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;32 位的静态连接 ELF 直接加载在 32 位以下的地址空间，继续使用原来的系统调用，和以前在 32 位内核上运行没什么区别&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;动态连接的 ELF 根据 .interp 里面指定的 ld.so 加载。 32 位的 ELF 指定的是 32 位的 ld.so, 还是和以前一样。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&#34;cpu-的兼容&#34;&gt;CPU 的兼容&lt;/h1&gt;
&lt;p&gt;首先需要明确一点， x86 的机器码和 x86-64 是不兼容的。不兼容的地方包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一些 x86 上的不常用指令被移除了&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;一些机器码被修改了用途 &lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如：机器码序列 &lt;code&gt;48 ff c0&lt;/code&gt; 中的第一个字节 &lt;code&gt;48&lt;/code&gt; 就是被修改了用途的机器码。在 x86 上， &lt;code&gt;48&lt;/code&gt; 是 &lt;code&gt;inc %eax&lt;/code&gt;, 整个序列解码为&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-asm&#34; data-lang=&#34;asm&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;inc&lt;/span&gt; %eax
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;dec&lt;/span&gt; %eax
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;而在 x86-64 上， &lt;code&gt;48&lt;/code&gt; 改成了 REX Prefix, 用来修饰后面 &lt;code&gt;ff c0&lt;/code&gt; 的操作数长度，整个序列解码为&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-asm6502&#34; data-lang=&#34;asm6502&#34;&gt;inc %rax
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;另外，在指令没有用 REX Prefix 修饰操作数长度时，默认的操作数长度也是不一样的。&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;因此， CPU 必须明确地知道当前执行的机器码是 x86 的还是 x86-64 的。实际上，这是通过 code segment descriptor 中的 一个 flag 来确定的。执行 x86-64 代码的模式称为 64-bit submode; 执行 x86 代码的模式称为 compatibility submode. 操作系统在初始化时就会在 GDT 中准备好 x86 和 x86-64 两个版本的 code segment descriptors, 根据需要进行切换。这里的切换时机在 wow64 和 linux 有区别。&lt;/p&gt;
&lt;p&gt;WOW64 的兼容层在 userland, 32 位应用调用系统 API 时，在兼容层里完成模式切换，这大概需要用到 long jmp 或 long call 指令来切换 CS 寄存器。而 linux 运行 32 位应用时整个 userland 都是 32 位的，模式切换是通过在 sysenter 的时候切换到内核的 CS 寄存器来完成的。&lt;/p&gt;
&lt;h1 id=&#34;总结&#34;&gt;总结&lt;/h1&gt;
&lt;p&gt;在 64 位操作系统上运行 32 位二进制是需要软硬件两方面的支持的。通过 CS 寄存器选择 long mode 中的 compatibility submod, x86-64 CPU 才能执行 x86 的机器码。同时软件方面需要操作系统提供在 32 位的应用和 64 位的系统 API 之间的调用机制。&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;2.5.10 Invalid Instructions, &lt;a href=&#34;https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/24593.pdf&#34;&gt;AMD64 Architecture Programmer’s Manual Volume 2: System Programming&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;2.5.12 Reassigned Opcodes, &lt;a href=&#34;https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/24593.pdf&#34;&gt;AMD64 Architecture Programmer’s Manual Volume 2: System Programming&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;Table 1-3. Address-Size Overrides, &lt;a href=&#34;https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/24594.pdf&#34;&gt;AMD64 Architecture Programmer’s Manual Volume 2: Volume 3: General-Purpose and System Instructions&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
- https://blog.nicolasyang.me/posts/how-x86-app-runs-on-64-bit-os/ - nicolasyang. All rights reserved.</description>
        </item>
    
    
    
        <item>
        <title>修复 Gnome terminal 里 Solarized Dark 配色看不到临时文件的问题</title>
        <link>https://blog.nicolasyang.me/posts/fix-solarized-palette-in-gnome-termianl/</link>
        <pubDate>Sun, 03 Nov 2024 21:58:06 +0800</pubDate>
        
        <guid>https://blog.nicolasyang.me/posts/fix-solarized-palette-in-gnome-termianl/</guid>
        <description>nicolasyang&#39;s blog https://blog.nicolasyang.me/posts/fix-solarized-palette-in-gnome-termianl/ -&lt;p&gt;在 gnome terminal 里用 solarized dark 配色，一直有个问题，就是 ls 列出 .tmp 结尾的临时文件时，文字颜色和背景色是一样的，根本看不到。由于从 ls 输出到终端显示出颜色这个路径里有太多组件和配置要排查，这个问题又影响不大，我就一直放着没管。最近终于决定要解决这个问题了。&lt;/p&gt;
&lt;h1 id=&#34;终端控制色彩显示的原理&#34;&gt;终端控制色彩显示的原理&lt;/h1&gt;
&lt;p&gt;CLI 应用通过 ANSI escape code&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; 来控制终端的行为。这个可以理解为类似 HTML 中的各种 &lt;code&gt;&amp;lt;foo&amp;gt;bar&amp;lt;/foo&amp;gt;&lt;/code&gt; 标记。其中，大部分复杂的功能都是通过 CSI&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt; 来使用的。 CSI 里面，Select Graphic Rendition (SGR) 序列，用来控制显示格式。&lt;/p&gt;
&lt;p&gt;CSI 就是 ESC 字符后面接 &lt;code&gt;[&lt;/code&gt; 字符，后面再接各种命令和参数。用转义写法就是 &lt;code&gt;\033[&amp;lt;cmd_and_args&amp;gt;&lt;/code&gt;. SGR 是 &lt;code&gt;&amp;lt;CSI&amp;gt;&amp;lt;args&amp;gt;m&lt;/code&gt;, 写成转义序列就是 &lt;code&gt;\033[&amp;lt;args&amp;gt;m&lt;/code&gt;. 控制终端输出的字符格式，就通过在这里指定不同的 &lt;code&gt;&amp;lt;args&amp;gt;&lt;/code&gt; 来实现。可以控制的功能包括加粗、闪烁、下划线，以及字符颜色，背景颜色等。&lt;/p&gt;
&lt;p&gt;其中， &lt;code&gt;&amp;lt;CSI&amp;gt;0m&lt;/code&gt;, 即 &lt;code&gt;\033[0m&lt;/code&gt; 是恢复基本格式。所以在终端应用里常见的用法，就是 &lt;code&gt;\033[&amp;lt;args&amp;gt;m&lt;/code&gt; 和 &lt;code&gt;\033[0m&lt;/code&gt; 包住一段需要改变格式的问题，就像一对 HTML 标记一样。&lt;/p&gt;
&lt;h1 id=&#34;问题排查&#34;&gt;问题排查&lt;/h1&gt;
&lt;p&gt;那现在可以开始来排查是谁把临时文件的颜色设置成和背景色一样了。首先我怀疑的是 oh-my-zsh 的主题。根据 &lt;code&gt;man ls&lt;/code&gt; 的说明， ls 是根据环境变量 ‵LS_COLORS&lt;code&gt;来确定各种类型文件的显示颜色的，那就在&lt;/code&gt;.oh-my-zsh&lt;code&gt;目录里 grep 各种&lt;/code&gt;LS_COLORS&lt;code&gt;, &lt;/code&gt;tmp&lt;code&gt; 之类的关键字，没有发现。&lt;/code&gt;man ls&lt;code&gt;里又说&lt;/code&gt;LS_COLORS&lt;code&gt;一般用&lt;/code&gt;dircolors` 命令来生成，但这个也没 grep 到。那就要怀疑是不是默认的配置就有问题了。&lt;/p&gt;
&lt;p&gt;执行一下 &lt;code&gt;dircolors -b&lt;/code&gt; 命令，可以看到输出和我的 &lt;code&gt;env&lt;/code&gt; 里的内容一致，都有 &lt;code&gt;*.tmp=00;90&lt;/code&gt; 这一段。那就 ls 这边都是默认的配置，没有问题。出问题的地方是在终端模拟器那边了。自己用命令 &lt;code&gt;printf &#39;Normal text. \033[90mInvisible text\033[0m.\n&#39;&lt;/code&gt; 试一下，也能复现同样的问题。&lt;/p&gt;
&lt;p&gt;查&lt;a href=&#34;https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit&#34;&gt;维基百科上的色表&lt;/a&gt;, 90 对应的是 16 色（也就是 4 bit 颜色）里面的第 9 个颜色，亮灰色。看来这个颜色被设置成和背景色一样了。看一下 gnome terminal 的 solarized 调色板，这第 9 个颜色正好就是和背景色一样啊。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.nicolasyang.me/images/gnome-terminal-solarized-palette.png&#34; alt=&#34;gnome-terminal-solarized-palette&#34;&gt;&lt;/p&gt;
&lt;p&gt;试着改了一下这个颜色，之前 print 出来的 &lt;em&gt;Invisible text&lt;/em&gt; 就可以看到了。&lt;/p&gt;
&lt;p&gt;那看来这个锅要 gnome terminal 背了？&lt;/p&gt;
&lt;p&gt;找了一下 gnome terminal 的数据文件和 gconf 配置，都没有找到相关配置，看来这些内置调色板是写在源码里了。找了一下源码，发现在这里： &lt;a href=&#34;https://github.com/GNOME/gnome-terminal/blob/d092dc2af7fb29b38a2962ece8cb377a0cd50f07/src/terminal-profile-editor.cc#L298&#34;&gt;https://github.com/GNOME/gnome-terminal/blob/d092dc2af7fb29b38a2962ece8cb377a0cd50f07/src/terminal-profile-editor.cc#L298&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;调色板里的这个颜色和背景色都被设置成了 solarized 里的 base03. 继续去看 solarized&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt; 的&lt;a href=&#34;https://github.com/altercation/solarized#the-values&#34;&gt;定义&lt;/a&gt;，发现它只定义了 8 种不同深浅的灰色，和 8 种 accent colors. 看来 gnome terminal 的 16 色调色板是自己扩展的？&lt;/p&gt;
&lt;p&gt;继续去找 gnome termianl 的 bug reports, 发现这个 &lt;a href=&#34;https://gitlab.gnome.org/GNOME/gnome-terminal/-/issues/7564&#34;&gt;9 年前的 report&lt;/a&gt;. 里面说问题是 &lt;a href=&#34;https://github.com/altercation/solarized/issues/220&#34;&gt;solarized 上游&lt;/a&gt; 的。最后发现 solarized 上游在给 xterm 定义调色板的时候就出问题了： &lt;a href=&#34;https://github.com/altercation/solarized/blob/62f656a02f93c5190a8753159e34b385588d5ff3/xresources/solarized#L64&#34;&gt;https://github.com/altercation/solarized/blob/62f656a02f93c5190a8753159e34b385588d5ff3/xresources/solarized#L64&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;看来这个问题的根源是 solarized 设计的时候只定义了 8 种 accent color, 但终端需要 16 色调色板，扩展的时候就出了问题。 Solarized 上游基本上已经是放弃维护了， gnome terminal 也不愿意管这个事情，只能自己改一下配置来解决问题了。&lt;/p&gt;
&lt;h1 id=&#34;修复&#34;&gt;修复&lt;/h1&gt;
&lt;p&gt;按照 gnome terminal 的 bug report 里的建议，自己把调色板里的那个颜色调亮一点。可以用这个网站： &lt;a href=&#34;https://colorizer.org&#34;&gt;https://colorizer.org&lt;/a&gt;, 把调色板里原来的 sRGB 色彩值 &lt;code&gt;#002B36&lt;/code&gt; 转换为 Lab, 再把 L 分量调高。最后我转成了 &lt;code&gt;#274C57&lt;/code&gt;，一个色调和背景色相同，亮度稍微高一点的色彩。&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/ANSI_escape_code&#34;&gt;https://en.wikipedia.org/wiki/ANSI_escape_code&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands&#34;&gt;https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;&lt;a href=&#34;https://ethanschoonover.com/solarized/&#34;&gt;https://ethanschoonover.com/solarized/&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
- https://blog.nicolasyang.me/posts/fix-solarized-palette-in-gnome-termianl/ - nicolasyang. All rights reserved.</description>
        </item>
    
    
    
        <item>
        <title>Rootless podman 容器的网络隔离</title>
        <link>https://blog.nicolasyang.me/posts/network-isolation-for-rootless-podman/</link>
        <pubDate>Thu, 10 Oct 2024 20:05:51 +0800</pubDate>
        
        <guid>https://blog.nicolasyang.me/posts/network-isolation-for-rootless-podman/</guid>
        <description>nicolasyang&#39;s blog https://blog.nicolasyang.me/posts/network-isolation-for-rootless-podman/ -&lt;p&gt;有时候在容器中测试服务，需要服务本身不能访问外部网络，但主机要能请求容器内的服务。在以前的 rootful 容器中，一般是用一个单独的 bridge 网络来实现的。但在 rootless 容器中， bridge 网络是用不了的，只能通过 pasta 或者 slirp4netns 的参数来限制容器对外部网络的访问。&lt;/p&gt;
&lt;p&gt;根据 &lt;a href=&#34;https://passt.top/passt/about/&#34;&gt;pasta&lt;/a&gt; 的文档，可以使用 &lt;code&gt;-o&lt;/code&gt;/&lt;code&gt;--outbound&lt;/code&gt; 来绑定 outbound socket 的地址，那么只要把 socket bind 到 lo 上，包就不会发到外部网络了。再根据 &lt;a href=&#34;https://docs.podman.io/en/v5.2.2/markdown/podman-run.1.html#network-mode-net&#34;&gt;podman-run(1)&lt;/a&gt; 给 pasta 传参的语法，就可以写成&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;podman run --rm -ti --network pasta:-o,127.0.0.1 docker.io/library/archlinux /bin/bash
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;试了一下，发现泄漏的 IPv6 流量和 DNS 流量，进一步改成：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;podman run --rm -ti --network pasta:--ipv4-only,-o,127.0.0.1,-D,none docker.io/library/archlinux /bin/bash
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--ipv4-only&lt;/code&gt; 禁用 IPv6&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o,127.0.0.1&lt;/code&gt; 把 socket bind 到 lo 接口，使流量无法离开本机&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-D,none&lt;/code&gt; 停止提供 DNS 服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样看起来就比较完善了。&lt;/p&gt;
&lt;p&gt;不过注意，这不阻止容器内的服务访问容器外的本机服务。达到的效果是：本机上容器内外能互通，容器内和本机以外的网络不互通，适合在容器里调试网络服务的场景。&lt;/p&gt;
- https://blog.nicolasyang.me/posts/network-isolation-for-rootless-podman/ - nicolasyang. All rights reserved.</description>
        </item>
    
    
    
        <item>
        <title>调试 Linux 设备电源管理，解决笔记本发热和续航问题</title>
        <link>https://blog.nicolasyang.me/posts/debug-linux-laptop-power-management/</link>
        <pubDate>Sat, 10 Sep 2022 15:52:28 +0800</pubDate>
        
        <guid>https://blog.nicolasyang.me/posts/debug-linux-laptop-power-management/</guid>
        <description>nicolasyang&#39;s blog https://blog.nicolasyang.me/posts/debug-linux-laptop-power-management/ -&lt;p&gt;长期以来，在笔记本上装 Linux 都会面临发热和电池续航下降（相比于在同一台笔记本上装 Windows）的问题。这个主要是驱动的电源管理不完善，很多设备的省电功能没有启动造成的。不过最近随着主要的厂商（Intel, AMD, Qualcomm 等）拥抱开源，主动编写开源驱动，大部分问题都得到了解决。但由于还是有 bug 存在，并且电源管理的 bug 很容易造成整个系统 hang up, 所以发行版一般都把策略配置得比较保守。现在一般在接入电源是都会停用大部分电源管理功能，使用电池供电时才会启用。这样插电运行的时候就还是发热和耗电很厉害；而如果电源管理有 bug, 就会出现插电运行一切正常，不插电就 hang up 的现象。&lt;/p&gt;
&lt;p&gt;前段时间在一台 ThinkPad p15 gen2 上装了 Linux, 就遇到了这个问题，断断续续调了很久，才解决问题。感觉这里的调试方法有参考意义，这里记录一下调试问题的过程。&lt;/p&gt;
&lt;h1 id=&#34;背景&#34;&gt;背景&lt;/h1&gt;
&lt;p&gt;ThinkPad p15 gen2 是一个使用 Intel Core CPU 和 Nvidia Quadro 专业显卡的图形工作站。作为图形工作站，它的显卡接口连接方式和一般的笔记本不同。一般的笔记本是核显连接显示输出，独显只能做计算渲染，渲染结果拷贝回核显再进行输出。现在大多数的显卡切换方案，也是按照这个设计来的。但这台笔记本是 intel 核显连接内置显示屏，nvidia 独显连接外置显示屏。这个配置很难配好能适应各种情况的切换方案，由于我主要是连接外置显示器工作的，就在 UEFI 里面把核显禁用了，只用独显。只有在长期出差，没有外置显示器的情况下，才会使用核显。&lt;/p&gt;
&lt;p&gt;除了显卡的问题，另外一个更严重的问题就是，这台机器在不接电源的时候，就会在开机进入桌面环境后不久就 hang 住，连关机都关不了，只能按 alt + sysrq + reisub 重启。由于这个机器的网卡声卡什么全都是 intel 的，就只有显卡和 NVMe 硬盘是第三方品牌，我就直接怀疑这两个设备有问题，把它们的电源管理禁用。但问题并没有解决，最后只好把全部 PCIe 设备的电源管理关掉，才没有问题。不过这样就完全没有续航了，2 个小时就耗尽电池。&lt;/p&gt;
&lt;p&gt;在这期间，我还遇到了 type-c 供电驱动程序的 bug, 表现也是系统 hang up. 多个问题交织到一起更加增大了调试难度。&lt;/p&gt;
&lt;h2 id=&#34;x86-电源管理概况&#34;&gt;x86 电源管理概况&lt;/h2&gt;
&lt;p&gt;x86 平台上的电源管理大概分为 3 部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CPU 电源管理：包括 freqency scaling, C-state 这些&lt;/li&gt;
&lt;li&gt;平台电源管理：包括待机、休眠、关机、唤醒、电池充放电管理这些&lt;/li&gt;
&lt;li&gt;外设设备的电源管理：目前的 x86 上的外设设备主要就分为 PCIe 和 USB 两大类&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中，CPU 就只有 Intel AMD 两家，并且文档资料齐全，厂商也支持开源驱动开发，电源管理支持得非常好。平台这块则是 firmware 通过 ACPI 向系统提供接口。这里台式机一般没有问题，但笔记本由于多了合盖检测、背光、电池这些，更加复杂，出问题的可能性比较大，不过老牌厂商，如 ThinkPad, Dell 这些，都经过了内核驱动和 ACPI 固件的长期磨合，一般也问题不大。外设设备中，USB 的规范比较具体，比如 HID 键盘鼠标、摄像头、U 盘这些都有通用协议和驱动（比如通用的 Mass Storage 驱动就可以适用于所有品牌的 U 盘），也不容易出问题。&lt;strong&gt;问题最大的就是 PCIe 设备&lt;/strong&gt;，这类设备都是显卡、网卡这类的复杂设备，固件和驱动都很复杂，如果有 bug, 就很容易造成内核错误。&lt;/p&gt;
&lt;h2 id=&#34;pcie-设备电源管理&#34;&gt;PCIe 设备电源管理&lt;/h2&gt;
&lt;p&gt;PCIe 设备的电源管理有统一的规范和通信协议。一个设备的电源状态分为 D0, D1, D2, D3 4 个状态。其中 D0 是完全启动正常工作，D3 是最深的省电状态。这两个状态是所有设备都要支持的，D1 和 D2 是可选的。CPU 可以通过写入设备的配置寄存器来改变电源状态。PCIe 设备在进入省电状态时，会关闭一部分电路，缓存、寄存器之类的状态信息可能会丢失。这样，在唤醒设备时，就需要重新初始化这部分状态。这需要驱动的密切配合，如果这个过程有 bug, 设备就会进入一个非预期的状态，驱动无法正常和设备通信，就会造成 hang up 之类的问题。&lt;/p&gt;
&lt;h2 id=&#34;linux-下的配置&#34;&gt;Linux 下的配置&lt;/h2&gt;
&lt;h3 id=&#34;内核&#34;&gt;内核&lt;/h3&gt;
&lt;p&gt;Linux 下，每个 PCIe 的电源管理 &lt;code&gt;RUNTIME_PM&lt;/code&gt; 可以通过 sysfs 中的开关 &lt;code&gt;/sys/bus/pci/devices/&amp;lt;address&amp;gt;/power/control&lt;/code&gt; 单独开启或关闭。开关的值可以是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;on&lt;/code&gt;: 设备保持开启状态，即禁用电源管理&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto&lt;/code&gt;: 自动控制，即启用电源管理&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;用户空间&#34;&gt;用户空间&lt;/h3&gt;
&lt;p&gt;有个 &lt;code&gt;tlp&lt;/code&gt; 命令可以根据 &lt;code&gt;/etc/tlp.conf&lt;/code&gt; 中的配置来调整 sysfs 中的各个开关。在多数发行版中，这个命令会通过 udev 规则来触发，在启动过程中、插拔电源的情况下执行。在多数发行版的默认配置中，tlp 会在电池运行时打开更多省电功能。&lt;/p&gt;
&lt;p&gt;另外有个命令是 Intel 开发的 &lt;code&gt;powertop&lt;/code&gt;. 这是一个 TUI 应用，可以监控 CPU 的电源、外设状态，以及调整 sysfs 中的电源管理开关。&lt;/p&gt;
&lt;p&gt;tlp 适合保存长期、持久化的配置，而 powertop 适合在调试过程中来使用。&lt;/p&gt;
&lt;h1 id=&#34;调试过程&#34;&gt;调试过程&lt;/h1&gt;
&lt;p&gt;这台笔记本最严重的问题就是拔掉电源、电池供电时，会随机 hang 住。例如，不插电源开机，一般是可以正常进入桌面环境，但进入桌面环境后，尝试启动一个应用，如 firefox, 就会启动不了。这个时候其它部分看起来都是正常的，但在终端里尝试 kill 掉 firefox, 也会卡住。尝试执行 &lt;code&gt;sudo journcalctl -k&lt;/code&gt; 或 &lt;code&gt;dmesg&lt;/code&gt; 来检查日志也会卡住。甚至执行 &lt;code&gt;sudo reboot&lt;/code&gt; 也会卡住。&lt;/p&gt;
&lt;p&gt;这种已经启动的程序运行正常，新程序无法启动的问题，让我地第一时间怀疑问题是硬盘 I/O 相关的，于是第一时间通过 tlp 禁用了 NVMe 的电源管理，但是没有效果。考虑到 N 卡是闭源驱动，也禁用了电源管理，也没有效果。&lt;/p&gt;
&lt;p&gt;这个时候我还不太了解 powertop 这个工具，就开始对比 tlp 的配置在电源供电和电池供电的状态下有哪些区别。最明显的区别就是，插电状态的默认 PCIe 电源管理是 &lt;code&gt;on&lt;/code&gt;, 电池状态是 &lt;code&gt;auto&lt;/code&gt;. 这里我把电池状态也改成 &lt;code&gt;on&lt;/code&gt; 后，问题基本解决。&lt;/p&gt;
&lt;p&gt;但这只能算是个能用的方案。在没有开启设备省电的情况下，机器发热比较厉害，并且电池续航只有大概 2.5 小时。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么说是基本解决呢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为这里还穿插了一个 type-c 供电驱动的 bug, 这个 bug 和 PCIe 电源管理的 bug 混在一起，很难调试。但根据最后的结论，这里关闭 PCIe runtime power management 后，就没有因为 PCIe 设备导致 hang up 的问题了。&lt;/p&gt;
&lt;p&gt;这个 type-c bug, 会导致 kernel oops. 在过了一个多月，这个 kernel oops 被修复以后，我才重新找时间去定位 PCIe 的问题。&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;到了这一步，我能想到的就是通过二分法去查找因为问题的设备，即每次禁用一半设备的电源管理，看看问题还是否复线。但看到除了 NVMe 和显卡，其它设备全都是 Intel 的，我都怀疑是否有多个设备同时有问题，用 tlp 来配置又还是比较麻烦的，这个问题就脱了很久。&lt;/p&gt;
&lt;p&gt;直到两个月后，有几天比较闲，我尝试去 powertop 这个工具里去看一下。这就发现 powertop 可以通过 TUI 列出所有外设并 toggle 每个设备的电源管理开关。于是我决定在插电运行，默认关闭所有 PCIe 设备电源管理的情况下，一个个打开电源管理开关，看看什么时候会出故障。&lt;/p&gt;
&lt;p&gt;于是一个终端运行 &lt;code&gt;dmesg&lt;/code&gt; 看日志，另一个终端开 powertop. 首先把我没有用的设备打开，看了一下，好像完全没用的就只有有线网卡。按下 Enter 打开电源管理，系统立刻表现出异常，过了十几秒中，耳机里的音乐也停了。所以这就找到问题了？&lt;/p&gt;
&lt;p&gt;重启电脑，在 tlp 里面把有线网卡的电源管理禁用，尝试一下拔掉电源，好像没有出现任何问题。再继续使用电脑，多开几个新应用，也都没有问题。所以引起问题的竟然是一个 Intel 的，我根本没有使用的有线网卡？？？因为有线网卡这种设备，在我用过的环境中，从来没有出现过问题；Intel 的设备，也是从来没有出现过问题。&lt;/p&gt;
&lt;h1 id=&#34;后续配置&#34;&gt;后续配置&lt;/h1&gt;
&lt;p&gt;把有线网卡加入 tlp 黑名单后，在电池模式下开启省电，大概有 5.5 小时的续航，续航时间翻了一倍。后面我顺便把插电状态下的 PCIe runtime power management 也打开了。虽然插电时没有电池续航的问题，但打开电源管理能让空闲时的 CPU 温度降低大约 7 度。&lt;/p&gt;
&lt;p&gt;想到这个有线网卡我也用不上，后面我干脆在 UEFI 里面把网卡禁用了。&lt;/p&gt;
&lt;h1 id=&#34;总结&#34;&gt;总结&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;如果一个问题只在不插电的时候出现，那么它大概率和省电配置有关，这时应该去配置 tlp.conf.&lt;/li&gt;
&lt;li&gt;Intel 的驱动也可能出问题，不能完全信任。&lt;/li&gt;
&lt;li&gt;发行版默认在插电状态下会禁用一下省电配置，如果想要省电/静音/冷却，应该去修改 tlp.conf 把它打开。（包括台式机）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;powertop&lt;/code&gt; 是个调试省电配置的好工具。&lt;/li&gt;
&lt;/ol&gt;
- https://blog.nicolasyang.me/posts/debug-linux-laptop-power-management/ - nicolasyang. All rights reserved.</description>
        </item>
    
    
    
        <item>
        <title>Firefox 添加自定义搜索引擎</title>
        <link>https://blog.nicolasyang.me/posts/add-custom-search-to-firefox/</link>
        <pubDate>Mon, 08 Aug 2022 20:41:34 +0800</pubDate>
        
        <guid>https://blog.nicolasyang.me/posts/add-custom-search-to-firefox/</guid>
        <description>nicolasyang&#39;s blog https://blog.nicolasyang.me/posts/add-custom-search-to-firefox/ -&lt;p&gt;在很久以前，Firefox 是可以手工写 pattern (如 &lt;code&gt;https://www.google.com/search?q=%s&lt;/code&gt;) 来添加自定义搜索引擎的。但不知道哪个版本开始，就砍掉了这个入口，只支持通过 opensearch 元数据来添加了。&lt;/p&gt;
&lt;p&gt;最近急需根据 bug id 跳转到 bug tracker 页面的功能（因为 bugzilla 自带的搜索很慢，直接搜 bug id 也要等好久），想要看看这个自定义搜索还能不能搞。搜索一番发现还是能搞的，只是配置方式不直观，很难找到。&lt;/p&gt;
&lt;p&gt;这里的原理是 firefox 的书签可以带参数和关键字。我们可以添加一个 bug 页面的书签，比如：&lt;code&gt;http://bugzilla.example.com/show_bug.cgi?id=12345&lt;/code&gt;, 然后编辑这个书签，把 &lt;code&gt;12345&lt;/code&gt; 改成 &lt;code&gt;%s&lt;/code&gt;, 再配置一个关键字，比如 &lt;code&gt;bug&lt;/code&gt;. 后面就可以在地址栏输入 &lt;code&gt;bug&amp;lt;TAB&amp;gt;56789&lt;/code&gt; 来直接跳转到那个 bug 了。&lt;/p&gt;
&lt;p&gt;类似的方法也可以用来配置其它各种没有 opensearch 元数据的搜索引擎。&lt;/p&gt;
- https://blog.nicolasyang.me/posts/add-custom-search-to-firefox/ - nicolasyang. All rights reserved.</description>
        </item>
    
    
    
        <item>
        <title>Redis 中的原子操作：事务与 Lua 脚本</title>
        <link>https://blog.nicolasyang.me/posts/atomicity-in-redis-transaction-and-lua/</link>
        <pubDate>Sat, 13 Nov 2021 17:41:19 +0800</pubDate>
        
        <guid>https://blog.nicolasyang.me/posts/atomicity-in-redis-transaction-and-lua/</guid>
        <description>nicolasyang&#39;s blog https://blog.nicolasyang.me/posts/atomicity-in-redis-transaction-and-lua/ -&lt;h1 id=&#34;讨论-redis-事务时我们关注的是&#34;&gt;讨论 Redis 事务时，我们关注的是&lt;/h1&gt;
&lt;p&gt;讨论 RDBMS 时，事务是需要 ACID 的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Atomicity: 原子性，事务是不可分割的单元&lt;/li&gt;
&lt;li&gt;Consistency: 一致性，包括
&lt;ul&gt;
&lt;li&gt;提交成功的事务产生的效果要能被后续的所有事务读取到&lt;/li&gt;
&lt;li&gt;不能破坏数据库约束&lt;/li&gt;
&lt;li&gt;提交成功的事务里，所有操作都要成功执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Isolation: 隔离性，多个并发事务不能互相影响&lt;/li&gt;
&lt;li&gt;Durability: 持久性，提交成功的事务不能丢失&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但对于 redis 来说，情况有些不同。首先是 durability 是不用想了，redis 肯定做不到。 Isolation, redis 没有并发，肯定没问题，也不需要考虑。我们只要考虑 atomicity 和 consistency.&lt;/p&gt;
&lt;h2 id=&#34;atomicity&#34;&gt;Atomicity&lt;/h2&gt;
&lt;p&gt;原子性是一个很基本的要求。即使只是把 redis 当缓存来使用&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-redis&#34; data-lang=&#34;redis&#34;&gt;HSET key1 field1 value2 field2 value2
EXPIRE key1 60
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们也不希望这两个操作中间被中断，&lt;code&gt;EXPIRE&lt;/code&gt; 要是没执行成功，&lt;code&gt;key1&lt;/code&gt; 占用的内存可能就永远没人去释放了。&lt;/p&gt;
&lt;h2 id=&#34;consistency&#34;&gt;Consistency&lt;/h2&gt;
&lt;p&gt;一致性的要求比较复杂，但 redis 是但线程的，又没有约束，所以前两点可以忽略。要关注的就是提交成功的事务里，所有操作是否能保证成功执行了。&lt;/p&gt;
&lt;p&gt;Redis 的设计是：不行。例如，还是去 &lt;code&gt;HSET&lt;/code&gt; 一个 key1, 但执行的时候，发现 key1 已经存在，但并不是 hash 类型，那这个命令就会执行失败。这如果是传统 RDBMS, 那整个事务都没办法提交，事务中的全部操作都不生效。但 redis 会忽略掉这个错误继续执行后面的操作，并能成功提交事务。&lt;/p&gt;
&lt;p&gt;所以 Redis 不能满足一致性的要求，我们必须在写程序的时候自己保证，提交的命令都是不会出错的（或者出了错也没有什么影响的）。&lt;/p&gt;
&lt;h2 id=&#34;小结&#34;&gt;小结&lt;/h2&gt;
&lt;p&gt;Redis 的事务，可以保证 atomicity 和 isolation, 但不能保证 consistency 和 durability. 并且，由于单线程的设计，原子操作一定是隔离的。所以我们后面就集中来看 aomticity 这点了。&lt;/p&gt;
&lt;h1 id=&#34;redis-中实现原子操作的-3-种方式&#34;&gt;Redis 中实现原子操作的 3 种方式&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;单个命令是原子的&lt;/p&gt;
&lt;p&gt;如 &lt;code&gt;HSET key1 field1 value1 field2 value2&lt;/code&gt;, 能保证 &lt;code&gt;field1&lt;/code&gt; 和 &lt;code&gt;field2&lt;/code&gt; 一起设置上&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过事务命令&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-redis&#34; data-lang=&#34;redis&#34;&gt;MULTI
HSET key1 field1 value2 field2 value2
EXPIRE key1 60
EXEC
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Redis 服务器缓存 &lt;code&gt;MULTI&lt;/code&gt; 后的命令，直到 &lt;code&gt;EXEC&lt;/code&gt; 再一起执行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过 lua 脚本&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; ret &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;hset&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;], &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;field1&amp;#39;&lt;/span&gt;, ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;], &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;field2&amp;#39;&lt;/span&gt;, ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;expire&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;], ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ret;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-redis&#34; data-lang=&#34;redis&#34;&gt;EVAL &amp;lt;脚本&amp;gt; 1 key1 value1 value2 60
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;EVAL&lt;/code&gt; 是单独的一条命令，自然整个脚本的执行都是原子的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id=&#34;transaction-与-lua-脚本对比&#34;&gt;Transaction 与 Lua 脚本对比&lt;/h1&gt;
&lt;p&gt;首先，可以看出，简单的操作，transaction 是要比 lua 简洁的。但 lua 能实现更加复杂的逻辑，例如：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; x &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;get&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;set&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;], x &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个操作用 MULTI-EXEC 是实现不了的，因为操作是原子的，在 &lt;code&gt;EXEC&lt;/code&gt; 执行之前，前面的命令都无法执行，无法返回结果，自然也就无法计算 x + 1 等于什么。&lt;/p&gt;
&lt;p&gt;如果一定要用 transaction, 这个操作可以用 WATCH-MULTI-EXEC 来做，但 optimsitic locking 可能会失败，需要反复重试，这里就是 lua 更简单高效了。&lt;/p&gt;
&lt;p&gt;所以，我们可以在简单的命令里用 MULTI-EXEC, 复杂的逻辑用 lua 脚本。当然，用 MULTI-EXEC 时要配合 pipeline 使用，否则每发送一条命令都等待响应的 RTT 会严重影响性能。&lt;/p&gt;
&lt;h2 id=&#34;关于-pipeline&#34;&gt;关于 Pipeline&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;只使用 pipeline 不能保证原子性！！！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在协议上，pipeline 纯粹就是一个针对 RTT 的优化，可以批量发送命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;没有 pipeline 时，每个命令都要等待响应&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.nicolasyang.me/images/redis-no-pipeline-sequence.svg&#34; alt=&#34;without pipeline&#34;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有 pipeline 时，可以发送多个命令后一起等待&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.nicolasyang.me/images/redis-with-pipeline-sequence.svg&#34; alt=&#34;with pipeline&#34;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种方式并不能保证 command 1 和 command 2 是原子的，当有多个 client 同时和 server 通信时，完全可以变成：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.nicolasyang.me/images/redis-pipeline-not-atomic-sequence.svg&#34; alt=&#34;pipeline is not atomic&#34;&gt;&lt;/p&gt;
&lt;p&gt;这里，对于 client A 来说，它确实使用了 pipeline 操作，连续发送两个命令，然后一起等待响应；但 server 却把两个命令分开处理了。&lt;/p&gt;
&lt;p&gt;这种情况在开发的时候可能不容易观察到。如果一个 pipeline 中的数据比较少（比如小于一个 TCP 包的大小），在客户端 redis 库、客户端内核的缓冲下，可能就是这个 pipeline 的命令都在一个 TCP 包中发了出去；server 也一次性地从内核缓冲区把整个 pipeline 请求读出来处理了。这种情况下，看起来就是原子的。&lt;/p&gt;
&lt;p&gt;但如果一个 pipeline 的数据比较多，大小超过了 TCP 包的大小，那发送出去的多个包 server 就不一定能一次性收到了。可以说在负载比较高的时候，必定会出问题的。&lt;/p&gt;
&lt;h1 id=&#34;redis-cluster&#34;&gt;Redis Cluster&lt;/h1&gt;
&lt;p&gt;在生产环境中一般会使用 redis cluster. Redis cluster 没有分布式事务，这会对我们能使用的原子操作产生限制。&lt;/p&gt;
&lt;p&gt;简单地看一下 redis cluster 是如何实现的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;redis 对 key 做 hash, 把 key 分配到 16384 个 hash slot 中，然后再把 hash slot 和集群节点绑定到一起&lt;/li&gt;
&lt;li&gt;hash slot 和节点的绑定关系可以通过通过命令改变，这可以用来从集群中增删节点，平衡数据。改变绑定关系会导致数据迁移。数据迁移不是原子的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么 redis cluster 的原子操作要根据 key 的异同分 3 种情况来看：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;操作中所有的 keys 都相同&lt;/li&gt;
&lt;li&gt;操作中所有的 keys 都属于同一个 hash slot&lt;/li&gt;
&lt;li&gt;操作中的 keys 属于多个 hash slot&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中 3 是不用看了，无法支持原子操作。而属于同一个 hash slot 时，要分两种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;集群处于稳定状态：这时同一个 hash slot 的 keys 都在同一个节点上，可以实现原子操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这个 hash slot 正在执行数据迁移：这时操作中的 keys 可能不在同一个节点上，操作可能产生部分失败的结果&lt;/p&gt;
&lt;p&gt;迁移过程中，一个 slot 的一部分 keys 在源节点上，另一个部分 keys 在目的节点上。Client 根据 slot 映射，向源节点发送请求；这时如果操作的 key 在目的节点上，server 就会返回 ASK 重定向，操作失败。&lt;/p&gt;
&lt;p&gt;这时如果操作的是都是同一个 key, 那么它要么在源节点上，要么在目的节点上，所有操作要么全部成功，要么全部返回重定向。全部返回重定向时，整个事务都没有产生任何效果，这时 atomicity 和 consistency 是可以保证的。&lt;/p&gt;
&lt;p&gt;但如果操作的不是同一个 key, 那就要可能部分 keys 操作成功，另一部分返回了重定向。只有一部分命令成功，这时 consistency 就破坏了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以，如果要保证 atomicity 和  consistency, &lt;strong&gt;redis cluster 的一个事务只能操作一个 key.&lt;/strong&gt;&lt;/p&gt;
&lt;h1 id=&#34;总结&#34;&gt;总结&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;单个命令是原子的&lt;/li&gt;
&lt;li&gt;MULTI-EXEC 事务命令，和 lua 脚本都是原子的&lt;/li&gt;
&lt;li&gt;MULTI-EXEC 要配合 pipeline 使用，否则性能会比 lua 脚本差&lt;/li&gt;
&lt;li&gt;WATCH-MULTI-EXEC 不如 lua 脚本&lt;/li&gt;
&lt;li&gt;Redis cluster 上，不管用 MULTI-EXEC 还是 lua 脚本，只有操作单个 key 的事务才能保证 consistency. 如果操作了多个 key, 可能会因为数据迁移，产生部分失败的结果&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;后记： &lt;a href=&#34;https://redis.io/topics/transactions#redis-scripting-and-transactions&#34;&gt;redis 文档里说&lt;/a&gt;，lua script 一般比 multi-exec 更快、更简单&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A Redis script is transactional by definition, so everything you can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.&lt;/p&gt;&lt;/blockquote&gt;
- https://blog.nicolasyang.me/posts/atomicity-in-redis-transaction-and-lua/ - nicolasyang. All rights reserved.</description>
        </item>
    
    
  </channel>
</rss> 