<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>zy::o</title>
        <link>https://zyi.io/zh</link>
        <atom:link href="https://zyi.io/zh/feed.xml" rel="self" type="application/rss+xml" />
        <language>zh</language>
        <lastBuildDate>Sat, 11 Oct 2025 00:00:00 +0000</lastBuildDate>
        <description>永远年轻，永远热泪盈眶.</description>
        <image>
            <url>https://zyi.io/media/site/f551bac59e-1762605728/zyiio-logo.contrast.round.495x495.png</url>
            <title>zy::o</title>
            <link>https://zyi.io/zh</link>
        </image>

        <item>
            <title>Zynq 工程基础 - Vitis Unified IDE 实用参考</title>
            <link>https://zyi.io/zh/corpus/note/zynq-project-basic-vitis-unified-ide</link>
            <guid isPermaLink="false">corpus/note/zynq-project-basic-vitis-unified-ide</guid>
            <pubDate>Sat, 11 Oct 2025 00:00:00 +0000</pubDate>
            <description>

            <![CDATA[<p>本文持续更新中...<br />
另外因为最近在做的项目有对 Zynq 的应用需求，再加上我对这个结合了 <strong>多核 Arm+FPGA</strong> 的 Soc 有着浓厚的兴趣，因此准备开启一个专题，为自己记录备忘的同时为其他同学提供参考，欢迎关注。</p>
<hr />
<p>本文主要从实际工程开发入手，从多个细节角度记录 Xilinx 在淘汰基于 Eclipse/Java 的 Vitis Classic 后推出的基于 Eclipse Theia 的全新 IDE 的开发流程。</p>
<h2>基本概念</h2>
<p>Vitis 有几个关键地概念需要了解，以便于对整个开发流程有一个深入的认识。</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/7149a4ab84-1762607158/vitis-workflow-brief.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/7149a4ab84-1762607158/vitis-workflow-brief.png" width="400"></a></figure>
<ul>
<li>Platform（平台）<ul>
<li>目标平台（或称平台）是由硬件组件（XSA）和软件组件（域/BSP、FSBL 之类的启动组件等）组合而成。存储库内的平台不可编辑。工作空间内的平台可编辑，称为平台组件。</li>
</ul>
</li>
<li>System Project（系统工程）<ul>
<li>同时运行的一个或多个 Application。若只有一个 Application 时，系统工程不是必须的。</li>
</ul>
</li>
<li>Domain（域）<ul>
<li>域即板级支持包（BSP）或操作系统（OS），其中包含软件驱动程序集合，可在其中构建自己的应用。</li>
</ul>
</li>
<li>Application（应用程序）<ul>
<li>即在 PS 端处理器上运行的软件，最终编译和生成二进制输出文件（ELF）。</li>
</ul>
</li>
</ul>
<p>Platform 和 Application；Vitis 将硬件平台与应用程序工程分隔开，可方便地将不同的硬件平台与应用程序工程组合而无需对各组合创建各自独立的工程，以尽可能地减少冗杂的开发流程。</p>
<h2>从 Vivado 导出硬件并创建 Vitis 软件项目</h2>
<h4>导出硬件并创建 Platform</h4>
<p>Extensible Vitis Platform -&gt; File/Export Platform<br />
不使用该选项，否则会在 Vitis/Flow 中找不到 Run/Debug 的功能。<br />
（因为 Extensible Vitis Platform 不会自动创建包含 Run/Debug 命令的 launch.json 配置文件）</p>
<p>Extensible Vitis Platform (Deselected) -&gt; File/Export Hardware<br />
导出硬件描述文件 xsa，用于在 Vitis 中创建 Platform 时定义硬件细节使用</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/3053f84cec-1762607158/vitis-flow-full.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/3053f84cec-1762607158/vitis-flow-full.png" width="400"></a></figure>
<p>创建 Platform 时步骤包含 Domain 的自动创建，根据需要选择 standalone/freertos/linux 以及绑定的内核。</p>
<h4>创建 Application</h4>
<p>一般使用 Template 来扩展，方便项目框架的快速搭建。</p>
<p>注意：部分 Template 需要特殊的 3rd-party 库（例如 lwip），需要在 Domain 下的 Board Support Package 中找到并启用对应库、根据 Template 的需要配置后，Regenerate BSP 并在 Flow 中 Build Platform 后再从 Template 页面中创建 Application:</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/0863b917ef-1762607158/vitis-bsp.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/0863b917ef-1762607158/vitis-bsp.png"></a></figure>
<h4>更新与切换 Platform</h4>
<p>若在 Vivado 端对 PL 的硬件部分做过修改，需重新综合实现生成 bitstream 并导出硬件至 xsa 文件后，重新加载 xsa 文件到 Platform：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/a099825ef3-1762607158/vitis-switch-re-read-xsa.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/a099825ef3-1762607158/vitis-switch-re-read-xsa.png" width="600"></a></figure>
<p>另外，Vitis 支持方便地对 Platform 进行切换。<br />
例如你想在多个支持同一套固件地不同 Platform 间进行测试，可在 Application Component Settings 下进行 Platform 地切换。</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/f382e499de-1762607158/vitis-app-settings.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/f382e499de-1762607158/vitis-app-settings.png"></a></figure>
<p>注意:</p>
<ol>
<li>给 Application 切换 Platform 时，由于没有经过从 Template 创建 Application 的流程，因此不会进行 BSP 依赖的检查，所以需要手动保证 Template 的依赖库以及配置的正确性。</li>
<li>若切换 Platform 后没有生效，可以尝试 重启 Vitis 大法（</li>
<li>最近及附近版本（2025.1）在导出时建议使用同一个 xsa 文件名称（可以选择备份原 xsa 文件后覆盖导出），因为 Vitis 目前有一个切换不同名称 xsa 时自动从仓库中删除平台的 bug，而且还添加不上，非常浪费时间</li>
</ol>
<h4>CMake 集成</h4>
<p>Vitis CMake 项目集成的逻辑是：IDE 负责维护 UserConfig.cmake 文件，这个文件主要是以 UI 界面的形式辅助生成一堆以 <code>USER_</code> 开头的用户定义变量和编译与链接器参数，常用的例如 <code>USER_COMPILE_DEFINITIONS</code> <code>USER_INCLUDE_DIRECTORIES</code> 和 <code>USER_COMPILE_SOURCES</code>，以及添加库常用的 <code>-l</code> 参数：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/b4f12ee57c-1765098714/vitis-add-library.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/b4f12ee57c-1765098714/vitis-add-library.png" title="" width="600"></a></figure>
<p>这些变量最后都由 <code>CMakeLists.txt</code> 文件读取并应用在编译和链接上。<br />
每次新建源文件后都需要手动添加文件到这些变量中略显麻烦，可以在 <code>CMakeLists.txt</code> 中添加 <code>aux_source_directory</code> 指令来批量添加同一路径下的所有源文件；例如若需将 <code>main.c</code> 同一路径下的所有源文件添加到 sources 中，可把</p>
<pre><code>list (APPEND _sources ${USER_COMPILE_SOURCES})</code></pre>
<p>改为</p>
<pre><code>aux_source_directory(. DIR_SRCS)
list (APPEND _sources ${USER_COMPILE_SOURCES} ${DIR_SRCS})</code></pre>
<p>即可。</p>
<p>P.S. 不要在 <code>UserConfig.cmake</code> 文件中实现，每次从 Vitis 中删除源文件时这个文件会自动更新并把更改覆盖掉。</p>
<h4>编译</h4>
<p>默认情况下，在 Flow 下点击 Build 进行编译时会弹出对话框确认是否在编译 Application 前自动编译 Platform BSP。建议选择手动分别编译（因为 Platform BSP 没有更改时也会 Clean build dir 然后重新进行编译，着实浪费时间）。<br />
如果想在之后更改这项配置，可通过 <code>ctrl+,</code> 快捷键打开设置，找到 Vitis/Application - Platform Build Dependency：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/9f5c2c2109-1762607158/vitis-settings-platform-build-dependency.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/9f5c2c2109-1762607158/vitis-settings-platform-build-dependency.png" width="400"></a></figure>
<p>P.S. 项目的路径名称不能过长，否则编译时 obj 文件不能正确地生成；下面是一个实际的例子：</p>
<pre><code>CMake Warning in libsrc/freertos10_xilinx/src/CMakeLists.txt:
   The object file directory
     C:/Users/usrlibzy/Documents/Xilinx/ModSpotFlow/ModSpotFlow.vitis_test/TestHardware/ps7_cortexa9_0/freertos_ps7_cortexa9_0/bsp/libsrc/build_configs/gen_bsp/libsrc/freertos10_xilinx/src/CMakeFiles/freertos.dir/./
   has 210 characters.  The maximum full path to an object file is 250
   characters (see CMAKE_OBJECT_PATH_MAX).  Object file
     Source/portable/GCC/ARM_CA9/portASM.S.obj
   cannot be safely placed under this directory.  The build may not work
   correctly.</code></pre>
<p>由于上述 obj 文件没有正确生成，在构建 BSP 时便会失败：</p>
<pre><code>[ERROR] C:/Users/usrlibzy/Documents/Xilinx/ModSpotFlow/ModSpotFlow.vitis_test/TestHardware/ps7_cortexa9_0/freertos_ps7_cortexa9_0/bsp/libsrc/freertos10_xilinx/src/Source/portable/GCC/ARM_CA9/port_asm_vectors.S:144: fatal error: opening dependency file libsrc\freertos10_xilinx\src\CMakeFiles\freertos.dir\Source\portable\GCC\ARM_CA9\port_asm_vectors.S.obj.d: No such file or directory</code></pre>
<p><del>Warning 中也给了提示，给环境变量 CMAKE_OBJECT_PATH_MAX 赋个更高的值。或者参考 <a href="https://adaptivesupport.amd.com/s/question/0D54U000091FhV1SAK/vitis-unified-20242-works-deep-into-the-file-system?language=zh_CN">这个回答</a> 将 Windows API 的 MAX-PATH 限制设置更大一些。</del><br />
不用浪费时间咯，实际测试检验上述方法根本没用（至少对 CMake.exe 是无效的）。</p>
<p>最终我使用系统自带的 <strong>虚拟驱动器映射</strong> 功能（<code>subst</code> 命令）来规避这个问题。<br />
例如，使用下面这个命令将整个 Xilinx 工程映射至一个虚拟驱动器上：</p>
<pre><code>subst W: C:\Users\usrlibzy\Documents\Xilinx\VirtualSource\</code></pre>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/a07384908d-1762607158/vitis-fix-max-path-subst.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/a07384908d-1762607158/vitis-fix-max-path-subst.png" title=""></a></figure>
<p>然后重新使用 Vitis 打开其中的 Workspace <code>C:\Users\usrlibzy\Documents\Xilinx\VirtualSource\VirtualSource.vitis</code>，现在映射到虚拟驱动器路径则是 <code>W:\VirtualSource.vitis</code>。<br />
注意不要直接映射 Vitis 项目所在的文件夹，Vitis Unfied IDE 不能直接打开根目录作为 Workspace。<br />
后续如果需要移除这个虚拟驱动器，可以使用 <code>subst /d W:</code> 命令实现。</p>
<h4>程序下载与PS/PL联合调试</h4>
<p>默认情况下，Application 配置的 <code>launch.json</code> 只是配置了 PS 端程序（.elf）的下载以及处理器的复位。若需要进行 PS 与 PL 的联调，可编辑该配置文件，手动添加最新硬件的 bitstream 文件，然后勾选 Program Device 即可：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/b1994af873-1762607158/vitis-launch-json-config.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/zynq-project-basic-vitis-unified-ide/b1994af873-1762607158/vitis-launch-json-config.png"></a></figure>
<p>配置界面的最下方贴心地提示了这个 launch 配置的运行流程。大致概括如下：</p>
<ol>
<li>复位整个系统，清除 PL 端逻辑</li>
<li>使用配置的 bitstream 文件来编程 PL 端逻辑</li>
<li>加载并运行 ps7_init tcl 脚本来复位 PS 处理器</li>
<li>加载配置的 elf 文件到 PS 处理器并运行</li>
</ol>
<p>注意检查用于初始化 ps 的 tcl 脚本应为 <code>ps7_init.tcl</code>；当工程中存在 IP 核时，Vitis 会选错为 IP 核中的脚本。</p>
<p>如果在 PL 端添加了 ILA 来观察波形，可运行上述的 launch 流程配置好 PS/PL 后，再回到 Vivado 打开 Hardware Manager/Logic Analyzer 来访问 ILA。</p>
<h2>参考</h2>
<ol>
<li><a href="https://docs.amd.com/r/zh-CN/ug1400-vitis-embedded/" rel="noreferrer" target="_blank">Xilinx 官方文档 UG1400</a></li>
<li><a href="https://adaptivesupport.amd.com/s/question/0D52E00007GTGeCSAX/how-can-i-use-ila-if-i-program-my-board-with-vitis?language=en_US" rel="noreferrer" target="_blank">How can I use ILA if I program my board with Vitis?</a></li>
</ol>]]>

            </description>
        </item>
        <item>
            <title>k3s 集群维护笔记 - 2</title>
            <link>https://zyi.io/zh/corpus/note/k3s-cluster-op-notes-2</link>
            <guid isPermaLink="false">corpus/note/k3s-cluster-op-notes-2</guid>
            <pubDate>Sun, 22 Jun 2025 00:00:00 +0000</pubDate>
            <description>

            <![CDATA[<p>未完待续，目前正在折腾多线宽带叠加/分流，尽请期待完稿日。😊</p>
<hr />
<p>本篇继续来记录 k3s 集群的基础设施维护。<br />
接 <a href="https://zyi.io/zh/corpus/note/dormitory-corner-micro-dc">上篇文章</a> 完成了对宿舍设施的升级后，准备集中解决下几个痛点；总结起来就是（还是）网络问题。</p>
<h2>集群网络问题</h2>
<h4>域控制器 与 主/从 DNS</h4>
<p>DNS 作为集群中的基础，其稳定性保证各项服务的质量，因此需要一个良好易实施与维护的方案。</p>
<p>在这之前，我所有的域名都在 Cloudflare 上配置解析，其中也包括内网中的部分。在实际使用时就会发现这是不太方便的做法：将内网的服务地址往外暴露不仅配置麻烦，而且没有必要（暂且不说明安全方面也会有一定的隐患）。<br />
因为现在有了 NAS，为了方便 SMB 共享的用户验证，我搭建了 Windows Server 用做 Active Directory 服务器的同时，发现域控本身就是在 DNS 基础上实现的，于是就顺便将域控的 DNS 用作集群的内部 DNS。当然也可以不使用 Windows Server 的 DNS 实现，而选择广泛使用的 <code>bind9</code> 作为替代；具体的做法就不在这里展开了。（<del>熟悉嘛，直接抄的上篇文章</del>😝）</p>
<p>首先打开 DNS 管理器，将要配置为次级 DNS 的服务器解析记录（A/AAAA）添加到主 DNS 上，然后右键正向查找区域中的区域名称（例如我的域名为 <code>plam.toay.io</code>），在属性中的名称服务器选项卡中添加上次级 DNS 即可：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-2/efea2b4cf6-1762605720/configuring-secondary-dns-for-dc.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-2/efea2b4cf6-1762605720/configuring-secondary-dns-for-dc.png"></a></figure>
<p>然后在 <strong>区域传送</strong> 选项卡中允许向次级 DNS 服务器传送解析的副本：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-2/be090fc778-1770624965/configuring-dns-domain-transfer.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-2/be090fc778-1770624965/configuring-dns-domain-transfer.png" title=""></a></figure>
<p>接着在将要用做次级 DNS 的 Linux 服务器上安装 <code>Bind9</code>：</p>
<pre><code>apt install bind9 bind9utils bind9-doc</code></pre>
<p>编辑配置文件 <code>/etc/bind/named.conf.options</code>；先在 <code>options</code> 块中配置 <code>listen-on</code> 指定监听的 host：</p>
<pre><code>options {
    //...

    listen-on {
        100.120.4.9;
    };
}</code></pre>
<p>后在文件末尾添加区域配置：</p>
<pre><code>zone "plam.toay.io" { // Zone Domain
    type slave;
    masters {
        100.120.32.67; // primary dns 1
        100.120.32.68; // primary dns 2
    };
    allow-query { any; };
    allow-notify {
        100.120.32.67; // primary dns 1
        100.120.32.68; // primary dns 2
    };
    notify no;
    file "/var/lib/bind/slaves/plam.toay.io.hosts"; // slave hosts file
};</code></pre>
<p><code>systemctl restart bind9</code> 重启 bind9 服务，就可以在日志中找到 zone transfer 的记录了。这时可使用 <code>nslookup</code> 尝试一下能否正常解析区域内的名称。</p>
<p>P.S. 1: 此时你可能会发现只有区域内的名称可以解析，因为目前的操作仅配置了 bind9 作为次级 DNS。如果要让它解析区域外的名称，还要额外配置转发。<br />
在同一个配置文件中的 <code>options</code> 块里添加转发器：</p>
<pre><code>options {
    //...

    forwarders {
        8.8.8.8;
        8.8.4.4;
    };
    recursion yes;
    allow-recursion { any; };
    allow-query { any; };
}</code></pre>
<p>转发作为默认配置，在具有显式配置的区域中不会生效，因此不会影响次级 DNS 的正常解析。</p>
<p>P.S. 2: 这里有一个小提示：当你在使用 Tailscale 的 exit-node 时，DNS 的解析是在 <strong>出口节点</strong> 上进行的。因此若使用 exit-node 的机器出现域名解析异常，可考虑从出口节点开始问题的排查。</p>
<p>如果你在 Tailscale 控制台中配置了下面的 DNS 自动分流的功能，这个问题可能就不会出现：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-2/1ad8385bd8-1762605720/configuring-dns-for-tailscale.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-2/1ad8385bd8-1762605720/configuring-dns-for-tailscale.png"></a></figure>
<p>Tailscale 的客户端会在解析域名时自动将属于内网的部分发送至你配置的内网 DNS 上进行解析（因此叫做 <code>Split DNS</code>），你不再需要为要访问内网服务的设备手动配置内网 DNS 了。</p>
<p>P.S. 3: 最后一个建议，对于大陆部署的服务器应当将域控 DNS 的上级配置为 <strong>可信 DNS</strong> （例如自建 DNS）上，避免部分机器没能使用到 Tailscale DNS 时遇到的域名污染问题。<br />
当然，这个配置也是非常简单的啦：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-2/61e32f98ac-1762605720/configuring-dns-forwarder-for-dc.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-2/61e32f98ac-1762605720/configuring-dns-forwarder-for-dc.png"></a></figure>
<h4>PVE - Intel 网卡 大流量假死问题</h4>
<p>PVE 总的来说非常稳定，之前的 PN61 连续 7*24 运行四五个月没有丝毫一点问题。<br />
但是在最近更新最新系统后，在遇到大流量情况时会出现 intel 网卡假死的现象，具体来说就是物理机的网络完全断开，将网线拔下重插后立刻恢复；在 PVE 系统日志中可以找到以下的记录：</p>
<pre><code>Jun 18 14:58:08 cubelets.plam.toay kernel: e1000e 0000:00:1f.6 eno1: Detected Hardware Unit Hang:
  TDH                  &lt;1d&gt;
  TDT                  &lt;7a&gt;
  next_to_use          &lt;7a&gt;
  next_to_clean        &lt;1d&gt;
buffer_info[next_to_clean]:
  time_stamp           &lt;1326d01db&gt;
  next_to_watch        &lt;1e&gt;
  jiffies              &lt;138cb45c0&gt;
  next_to_watch.status &lt;0&gt;
MAC Status             &lt;40080083&gt;
PHY Status             &lt;796d&gt;
PHY 1000BASE-T Status  &lt;3c00&gt;
PHY Extended Status    &lt;3000&gt;
PCI Status             &lt;10&gt;</code></pre>
<p><del>网上搜了下，在 PVE 的官方论坛上找到了 <a href="https://forum.proxmox.com/threads/e1000e-eno1-detected-hardware-unit-hang.59928/page-2" rel="noreferrer" target="_blank">解决方法</a>：关闭</del> <code>~~rx/tx checksumming~~</code><br />
这个方法在生效一段时间后，</p>
<p>先使用 <code>ethtool -k eno1</code> 命令审查开启的选项，可以使用 grep 查找 checksumming 相关的配置：</p>
<pre><code> ~  ethtool -k eno1
Features for eno1:
rx-checksumming: on
tx-checksumming: on
        tx-checksum-ipv4: off [fixed]
        tx-checksum-ip-generic: on
        tx-checksum-ipv6: off [fixed]
        tx-checksum-fcoe-crc: off [fixed]
        tx-checksum-sctp: off [fixed]
scatter-gather: on
        tx-scatter-gather: on
        tx-scatter-gather-fraglist: off [fixed]

...</code></pre>
<p>确认开启了校验后，可以使用下面的命令来关闭校验：</p>
<pre><code>~ ethtool -K eno1 tx off rx off
Actual changes:
tx-checksum-ip-generic: off
tx-tcp-segmentation: off [not requested]
tx-tcp6-segmentation: off [not requested]
rx-checksum: off</code></pre>
<p>配置是立刻生效的，在这之后便不会在大流量时挂机了。然而配置在重启后会失效，因此需要额外的配置以在启动时应用。</p>
<p>根据官方论坛上 <a href="https://forum.proxmox.com/threads/intel-nic-e1000e-hardware-unit-hang.106001/page-2" rel="noreferrer" target="_blank">另一条帖子</a> 的记录，可以编辑 <code>/etc/network/interfaces</code> 文件，在 <code>iface eno1 inet manual</code> 后添加一条 post up 命令 <code>post-up /sbin/ethtool -K eno1 tx off rx off</code> 即可。</p>
<h2>参考链接</h2>
<ol>
<li><a href="https://www.reddit.com/r/sysadmin/comments/pls8zf/is_it_possible_to_connect_a_linux_bind_dns_server/">https://www.reddit.com/r/sysadmin/comments/pls8zf/is_it_possible_to_connect_a_linux_bind_dns_server/</a></li>
<li><a href="https://vswitchzero.com/2021/02/24/linux-and-bind9-as-a-dns-secondary-for-active-directory/">https://vswitchzero.com/2021/02/24/linux-and-bind9-as-a-dns-secondary-for-active-directory/</a></li>
</ol>]]>

            </description>
        </item>
        <item>
            <title>宿舍角落里的&#8220;数据中心&#8221;</title>
            <link>https://zyi.io/zh/corpus/note/dormitory-corner-micro-dc</link>
            <guid isPermaLink="false">corpus/note/dormitory-corner-micro-dc</guid>
            <pubDate>Wed, 04 Jun 2025 00:00:00 +0000</pubDate>
            <description>

            <![CDATA[<h2>（并不）简要的介绍</h2>
<p>从去年夏天开始，寝室里一直运行着一台 华硕 pn61s 小主机，上面用 PVE 作为 Hypervisor 跑着几个虚拟机。<br />
这个机器是由在京东二手买的带 八代移动端 i5 的准系统，外加闲鱼入的低价 16g*2 ddr4、拆机西数黑盘 500g hdd 和一块西数蓝盘 1t m2 组成的。特点就是非常小巧，115mm x 115mm x 49mm 的体积在哪都能找个地方容纳。这里还有 <a href="https://x.zyi.io/@usr/111952369154640502" rel="noreferrer" target="_blank">刚到手时的记录</a>，那时是挺高兴的（算是当时用过的最高的配置的服务器了，能不高兴嘛）。后面由于气温越来越高，于是去淘宝找了个散热支架将它包围起来，<a href="https://x.zyi.io/@usr/113107453173916087" rel="noreferrer" target="_blank">就像这样</a>。</p>
<p>pn61s 的主要用途是跑从云上搬下来的自建 GitLab（那就是 git.toay 啦，话说 Java 真得巨占内存，憋再继续浪费我资源呐），以及用做 k3s 跨云集群的一个 control plane 节点，顺便还可以用 Pydio 搭个简易的 WebDAV 网盘利用一下剩余的硬盘空间。</p>
<p>在除了供电故障外稳定运行一年多得时间后，pn61s 却在上个月中旬的一次寝室停电后突然就罢工了。<br />
于是出差时得空闲时间就在调研，打算搞台算力强些的 <strong>桌面级 u</strong> 小主机用做主力服务器。最后在闲鱼上收了一台 ThinkStation P340 Tiny 准系统，然后去淘宝买了块散片，以及到京东把内存拉满。<br />
将从 pn61s 上取下的固态直接装上并一次性点亮后，<del>要不干脆就直接升级成数据中心吧，</del> 抱着 <strong>一步到位能用服役年限拉低平均成本</strong> 的心态顺便搞了台 <strong>MikroTik RB5009UG</strong>，一台 <strong>TerraMaster D5 Hybrid 硬盘柜</strong> 和一块 <strong>8T 的酷狼 3.5 SATA</strong>。。。<br />
最后趁 618 打折再准备了一台 <strong>APC BK650 UPS</strong>，避免寝室突然断电硬关机造成的硬盘与数据丢失问题，以及一个 100 出头就能入手的机柜，然后就把所有的设备整理在角落里了：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/68296e8158-1762605720/rack-shot.jpeg"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/68296e8158-1762605720/rack-shot.jpeg"></a></figure>
<p>P.S.1 看得出来我非常喜欢联想的工业设计。除了完全贴合我审美的简约外观外，p340 tiny 的散热效果真的逆天。刚开始在等待机架时放在桌边测试了几天，一米外基本上听不见一点声音；而在虚拟机开满的情况下能听到的声音很低沉，像是呼吸一般，根本谈不上是“噪声”。放在一起对比之下，pn61s 不加外部散热时不仅有个频率更高些、明显给人带来厌烦感的噪声，严重的积热使我怀疑它是否能承受长时间的开机。</p>
<p>P.S.2 pn61s 现在已经复工了，大概只是纽扣电池没电导致断电后 BIOS 配置丢失+内存重新 training，同时华硕默认的 Windows 安全启动不能正常地拉起 GRUB 进行 PVE 的引导，但在当时几次三番长达十几分钟的黑屏后掉电的经历让我误以为低价的自组装内存已经准备就此退役le ^_^"）</p>
<p>那就组成集群吧！</p>
<h2>配置与组成</h2>
<p>简单整理一下，现在集群的配置大概是这样的：</p>
<table>
<thead>
<tr>
<th>物件</th>
<th>配置</th>
<th>参考价格与备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>准系统</td>
<td>P340 Tiny</td>
<td>890 闲鱼全新</td>
</tr>
<tr>
<td>230W电源</td>
<td>联想原装</td>
<td>270 淘宝</td>
</tr>
<tr>
<td>cpu</td>
<td>10代桌面端 i5 10600</td>
<td>680 淘宝散片</td>
</tr>
<tr>
<td>内存</td>
<td>英睿达 DDR4 32G * 2</td>
<td>366+415 京东</td>
</tr>
<tr>
<td>m2</td>
<td>西数蓝盘 sn580 1t</td>
<td>600 直接从 pn61s 搬过来</td>
</tr>
<tr>
<td>sata</td>
<td>西数黑盘 wd5000 500g</td>
<td>50 淘宝拆机，也是直接从 pn61s 搬过来</td>
</tr>
<tr>
<td>硬盘柜</td>
<td>铁威马 D5 Hybrid</td>
<td>800 淘宝</td>
</tr>
<tr>
<td>3.5 sata</td>
<td>希捷酷狼 8T</td>
<td>1400 京东</td>
</tr>
<tr>
<td>Homelab 有线路由</td>
<td>MikroTik RB5009UG</td>
<td>1200 淘宝 带机架固定器</td>
</tr>
<tr>
<td>机柜</td>
<td>12u</td>
<td>100 淘宝 600mm x 550mm x 400mm</td>
</tr>
<tr>
<td>UPS</td>
<td>APC BK650</td>
<td>440 淘宝</td>
</tr>
</tbody>
</table>
<p>上面的大概就是一个 All in One 的组合，去年的 pn61s 就不算在这里面了。</p>
<p>话说硬盘真贵啊，硬盘柜和硬盘加起来基本上就是 p340 无盘系统的价格了，况且我小主机所有配件选的还是成色非常好的。。。<br />
但怎么说呢我觉得还是挺值得的：首先是有了相机后对存储的要求突增了许多；另外科研上的各种资料项目工程文件越来越多，除了网盘外还是得有一个离线的备份方案。</p>
<h2>基础设施</h2>
<p>画了张图，能够清楚地描述整个小 dc 的组成；其他只是与 dc 相关而并不隶属的部分我就简单概括一下咯：（话说 draw.io 挺好用的）</p>
<p><br /></p>
<picture>
  <source srcset="/corpus/note/dormitory-corner-micro-dc/plam.net.drawio.dark.png" media="(prefers-color-scheme: dark)">
  <img src="/corpus/note/dormitory-corner-micro-dc/plam.net.drawio.light.png" width="500" class="aligncenter">
</source></picture>
<p><br /></p>
<p>其中部分系统服务的编排，我认为是能保证集群稳定运行的比较好的实践，接下来我简要地说明下，供大家参考。</p>
<h4>PVE</h4>
<p>即 Proxmox Virtual Environment，是在 Debian 的下游开发的虚拟化平台。PVE 的好处是非常稳定，24*7 连续运行时基本上宿主系统不会出现什么大问题，非常可靠。相比另一个常用的虚拟化平台 VMWare ESXi 更多的驱动和更完善的折腾记录和参考。自带的集群管理功能可以很方便地集中管理，或者在 host 之间迁移数据，而其它各项功能就跟 VMWare Workstation 在 Windows 上的操作一样（或者说虚拟化平台之间的功能都差不太多），就不再具体说明了。</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/438d8b894b-1762605720/insulator-plam.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/438d8b894b-1762605720/insulator-plam.png"></a></figure>
<h4>DNS</h4>
<p>DNS 作为集群网络中的基础设施，其稳定性保证几乎所有服务的质量，因此需要一个良好易实施与维护的方案。</p>
<p>在这之前，我所有的域名都在 Cloudflare 上配置解析，其中也包括内网中的部分。在实际使用时就会发现这是不太方便的做法：将内网的服务地址往外暴露不仅配置麻烦，而且没有必要（暂且不说明安全方面也会有一定的隐患）。<br />
因为现在有了 NAS，为了方便 SMB 共享的用户验证，我搭建了 Windows Server 用做 Active Directory 服务器的同时，发现域控本身就是在 DNS 基础上实现的，于是就顺便将域控的 DNS 用作集群的内部 DNS。当然你也可以不使用 Windows Server 的 DNS 实现，而选择广泛使用的 <code>bind9</code> 作为替代。</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/c855773508-1762605720/plam.net.dns.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/c855773508-1762605720/plam.net.dns.png"></a></figure>
<p>使用 Windows Server 在同一个域中搭建多个 DNS 服务器时域配置会自动同步，因此可以轻松地实现高可用。<br />
对于 DNS 这种需要保证稳定性的服务，建议再搭建几个次级 DNS 放在公网的云服务器上（但只开放内网访问）。<code>bind9</code> 能够很方便的完成次级 DNS 的配置，将域中的主 DNS 记录完整地同步过来。</p>
<p>待 DNS 部署完成后，可在 Tailscale 控制台中开启 <code>Split DNS</code> 功能，Tailscale 的客户端会在解析域名时自动将属于内网的部分发送至你配置的内网 DNS 上进行解析：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/ad9cd7ea33-1762605720/configuring-dns-for-tailscale.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/ad9cd7ea33-1762605720/configuring-dns-for-tailscale.png"></a></figure>
<p>这算是 Tailscale 一大便利功能，因为配置完成之后，你不再需要为要访问内网服务的设备手动配置内网 DNS 了。</p>
<h4>网络代理</h4>
<p>集群中的代理主要分为两种：一种是给集群中所有的机器进行镜像拉取、软件包脚本下载等使用的内部代理，配置 http proxy 即可；另一种是给自己的设备使用的外部代理，具体的角色设定为旁路由。</p>
<p>在 <a href="https://zyi.io/zh/corpus/note/k3s-cluster-op-notes-1" target="blank">之前的文章</a> 有介绍过我解决大陆服务器网络问题的策略：不使用 Tailscale exit-node，而是在多个外网节点搭建简单的 http-proxy （http proxy over wireguard ¿） 后通过 openwrt+openclash 中继的方式实现 failover/load balancing。</p>
<h4>流量入口与负载均衡</h4>
<p>如架构图所示，虽然所有的虚拟机都通过网桥直接连接在路由器上，但访问应用程序的流量均通过固定的 Load Balancer 进入。</p>
<p>这样做的好处是很明显的：</p>
<ol>
<li>不会再出现 <code>main-server:9100, sidecar:9090, nas:8080</code> 之类乱七八糟的访问地址，转而由 <code>prometheus.plam.toay.io, insight.plam.toay.io, cells.attic.plam.toay.io</code> 替代并开启 https，让自己看起来更像是 Saas 提供商。😝</li>
<li>统一管理所有的域名和证书；这一点通过 <strong>通配符解析</strong> 和 acme.sh 就可以完美实现。</li>
<li>仅将服务通过指定的一两台机器暴露出去，大大减少可能的安全问题。访问日志、流量统计、WAF、统一缓存什么的很轻松就能部署。</li>
<li>负载均衡，特别是对于使用 k3s 部署的微服务，load balancing+traefik ingress+动态缩扩容非常方便。</li>
</ol>
<p>这个任务 Nginx 可以轻松承担。<br />
另外，若想把服务暴露给公网，可以再在带宽稍高点的云服务器上部署一个 Nginx 来负责外网流量的导入，就像我在 <code>git.toay.io</code> 上做的那样。</p>
<h4>状态监测</h4>
<p>状态监测（包括 <strong>Metrics</strong> 和 <strong>Logging</strong>）是维护集群稳定的必要方式以及出现问题后排查与解决的参考来源。</p>
<ul>
<li>Grafana: 可视化 Web 应用</li>
<li>Prometheus: 从 *_exporter 采集指标数据</li>
<li>Loki: 日志数据采集</li>
<li>*_exporter: 指标数据收集与统一 expose</li>
<li>promtail: 收集与提交日志数据至 Loki</li>
</ul>
<p>这里推荐一个用于 MikroTik 设备数据与日志收集的工具 <a href="https://github.com/akpw/mktxp-stack" rel="noreferrer" target="_blank">mktxp</a>，可收集各种运行时产生的数据用于分析：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/cd7330bd6e-1762605720/insight-plam-mktxp.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/cd7330bd6e-1762605720/insight-plam-mktxp.png"></a></figure>
<h4>UPS</h4>
<p>对于寝室这种可能会突然断电的环境，如果你想让你的设备在役时间更长久一些，UPS 可以说是必须准备的物件了。<br />
APC 的 UPS 貌似全平台都有各种官方的/开源的上位机控制软件的支持；在 Linux 下官方的上位机守护程序 <code>apcupsd</code> 直接用 <code>apt</code> 安装即可。</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/9fd20dbd29-1762605720/apcaccess.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/9fd20dbd29-1762605720/apcaccess.png"></a></figure>
<p>虽然 BK650 通过 USB 仅直接连接到一台物理机上，但是这台机器上运行的 <code>apcupsd</code> 可以开启一个 <strong>Network Information Server</strong> 作为 Master 给其他主机的 <code>apcupsd</code> 作为 Slave 来使用，可以实现 UPS 在电量即将耗尽之前将所有的物理机关机，那就非常方便了。</p>
<h4>k3s+rancher</h4>
<p>k3s 用于除了 NAS 相关、以及部分重要的基础设施之外服务的编排，部署在云服务器上的 Rancher 则作为远程可视化管理工具，可以方便的跟自动化工具联合。</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/f223aeee9b-1762605720/k3s-rancher-ns.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/f223aeee9b-1762605720/k3s-rancher-ns.png"></a></figure>
<p>虽然这个集群主要是为 k3s 构建的，但是我的博客已经花了不少篇幅介绍他们，这里也不再赘述了；更多内容可以通过下面的专栏链接来了解。</p>
<p>基础设施的介绍差不多就到这了；这里只是简要地介绍我的方案。如果想要了解更详细的部署步骤和实际遇到问题的细节说明，可以移步专栏 <a href="https://zyi.io/zh/search?tag=k3s-operation" rel="noreferrer" target="_blank">k3s 集群维护笔记</a>。接下来就介绍下我部署的各种有意思的服务啦。😊</p>
<h2>各种有意思的服务</h2>
<p>目前两个小主机上加起来跑了十个左右的虚拟机，主要是软路由、反向代理、k3s 集群节点、NAS、Linux 开发环境，以及用于远程桌面、做域控和 DNS 的 Windows Server。</p>
<h4>Git.Toay</h4>
<p>Git.Toay 应该是 PlamNet 中寿命最长的一项了，也是截止目前使用的最多的一项；我将我绝大多数暂不公开的项目存在其中，有时也会从 GitHub 上 clone 一些有意思的项目下来做存档。GitLab 自带 CI/CD 工具和容器镜像库功能，现在也在探索中，之后会将其与 Rancher 集成，尝试构建部署全流程的自动化。</p>
<p>为了保证数据安全（针对于数据丢失的风险），特别为它启用了完善的备份措施：每天凌晨由 GitLab 自动创建备份到 p340 tiny 内置 sata 机械盘中，然后每周从机械盘增量备份至 NAS 中。<br />
现在打算整理一下 Dropbox，按重要性筛选将较低的大文件下载到 NAS 中，然后在 NAS 中部署 rclone，每周将备份文件夹中的资料推到 Dropbox 备份；大概是我能/需要做到的备份计划之极限了吧？</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/51a009e101-1762605720/git-plam.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/51a009e101-1762605720/git-plam.png"></a></figure>
<h4>NAS</h4>
<p>对于 NAS 的实现，我的方案是：系统方面选择使用 OpenZFS 文件系统的 TrueNAS，并将硬盘盒直通到 TrueNAS 虚拟机中。</p>
<p>虚拟机里运行的服务主要有以下几类：</p>
<ol>
<li>SMB 服务器</li>
<li>作为网盘使用的 Pydio / Nextcloud</li>
<li>作为文件同步备份用的 Resilio Sync / Syncthing</li>
<li>作为网络相册的 Photoprism</li>
<li>作为辅助工具/基础设施的 n8n/nginx proxy manager</li>
</ol>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/5cd4501e17-1762605720/attic-plam.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/5cd4501e17-1762605720/attic-plam.png"></a></figure>
<h4>Gallery.Attic</h4>
<p>网络相册是我认为购买硬盘/硬盘盒凑 NAS 后效益最大的一项功能。在之前，我整理照片的流程是在工作站上直接使用 Lightroom CC 导入 SD 卡内的照片到存在移动硬盘里的库中。由于我习惯拍照时都是自动保存 DNG 原片，因此每个快门都会产生一百多兆的数据；时间一长，我的 1T 小移动硬盘就招架不住了。</p>
<p>而现在，我的流程转为：将相机连接到小主机后，通过 Windows Server 远程桌面使用 Lightroom CC 将 DNG 原片导入到 TrueNAS 的 SMB 网络驱动器中，而只是保存预览和修改描述文件的库则通过 Resilio Sync 进行几大设备间的同步。另外，照片保存到 NAS 中的同时，Photoprism 也会自动导入并生成索引以供快速搜索与查看。因此我可以在空闲的时候将 Gallery.Attic 打开，将自认为尚可的照片挑选出来，一并导出到博客中来，而不是就尘封在移动硬盘中，直到自己都忘记曾拍过什么照片。</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/0dcda24615-1762605720/gallery-attic-plam.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/0dcda24615-1762605720/gallery-attic-plam.png"></a></figure>
<h4>SMB 服务器</h4>
<p>除此之外，SMB 服务器实现了“无线大容量硬盘”的功能。<br />
备份数据什么的就不用说了，将其挂在到 Windows Server 上还可以用来 24h 挂机下载。下载完成后不需要拷贝到电脑上观看，能轻松跑满千兆的网络可以直接使用流传输；这个算是附带效益吧。</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/76a27bd237-1762605720/tumbleweed-rdp-alipan-download.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/dormitory-corner-micro-dc/76a27bd237-1762605720/tumbleweed-rdp-alipan-download.png"></a></figure>
<h4>iSCSI/NFS 服务器</h4>
<p>NAS 能提供的最高级的服务可能就是 <strong>为 k3s 集群提供存储支撑</strong> 了。有了 iSCSI/NFS 这两种 k3s/rancher 原生支持的选项，可以完美地实现 compute 与 storage 资源的分离，体验私有云效果的同时还能将资源分配利用率最大化。（简单举个例子，将价格低稳定性较差些但还有使用价值的硬盘用于虚拟机启动盘的存储，就算挂掉后数据仍在 NAS 中，换块差不多的硬盘用模板重新启动就可以了）</p>
<p>由于之前 k3s 的存储一直使用的 bind-mount，这种存储方式现在还在探索中。在我实践出经验后将会把经历记录在 <strong>k3s 维护</strong> 专栏中。</p>
<h2>Bonus</h2>
<p>感谢看到最后 (｡•ᴗ-)_<br />
既然看起来已经像模像样了，要不简单地回顾一下折腾史吧。</p>
<p>最早从线上（云服务器）折腾到线上线下双管齐下时当回溯到高一的暑假了。<del>其实这么多年过去了博客上还保留着那时的记录。</del> 当初的想法和现在也相差无几（要不把 06 年的 <strong>惠普畅游人</strong> 拿来搭个家庭媒体系统吧，大概），而在 17 年时 <strong>家用 NAS</strong> 应该是非常小众的。后来因为维护麻烦、噪声高利用率低等诸多原因，再加上长期不在家回校后根本用不上只得关机，于是断断续续累计运行总时不到一年就没再使用了。现在回想起来，Linux 的入门实践大概都是在那台古董上进行的。</p>
<p>后来接触到树莓派这种单板机，在宿舍角落挂几个 Home Assistant/OpenClash/Mqtt 容器什么的，顺便用 GPIO 与串口控制用单片机做成的不带任何界面的“隐形门锁”，就这么玩到本科毕业。然而在去年，在价格媲美 x86 而性能堪比 riscv 的 raspberry pi 5 出现后，我大概是放弃树莓派了，等几年后知识储备到位时会研究下自己的 Arm 板子。</p>
<p>弃坑之后，本文的内容便开始了。</p>]]>

            </description>
        </item>
        <item>
            <title>Windows 端 ROSless ORB-SLAM3 的移植</title>
            <link>https://zyi.io/zh/corpus/note/porting-rosless-orb-slam3-to-windows</link>
            <guid isPermaLink="false">corpus/note/porting-rosless-orb-slam3-to-windows</guid>
            <pubDate>Mon, 24 Feb 2025 00:00:00 +0000</pubDate>
            <description>

            <![CDATA[<p>最近项目上有对放射源进行三维扫描与辐射场建模的需求，而必要的能谱分析软件是基于 Windows 端开发的，故需要在 Win 端实现 SLAM 建图。目前实际应用中 ORB-SLAM 为效果最好的 <strong>基于特征点的视觉 SLAM 系统</strong>，而其 v3 版本相较于 v2 版本增加了使用 IMU 与视觉里程计融合的功能并提高了运算速度，因此便选择使用 <strong>ORB-SLAM3</strong> 实现功能，同时准备把其基于 Linux gcc 平台实现的库移植到 Win MSVC 平台上。</p>
<p>GitHub 上可以搜到一些移植完成的项目，基本上都是 v2 版本，极少数的 ORB-SLAM3 使用的都是 v0.x rc 版本。而且就算这样也都没有一个移植过程的记录，展示下 <strong>进行摄像头在线联调</strong> 的结果。是没调出来嘛还是不愿意分享呢，这我就不知道了🙁。</p>
<p>考虑到项目有稳定与安全性方面的要求，我最终选择 <strong>ORB-SLAM3 使用最新版本、所有依赖库均使用官方 Release</strong> 的方式来构建项目。构建完成的项目会放到 GitHub 上，但还是建议过一遍本文的流程，了解移植时在什么地方做了什么修改有什么坑，触类旁通的同时还能便于以后在 ORB-SLAM3 库更新版本时及时跟上。</p>
<h2>移植前准备</h2>
<h4>工具链</h4>
<p>构建工具链大版本一致就不会有什么问题，但我还是把我的版本放在这里：</p>
<ul>
<li><strong>CMake</strong> v3.31.4</li>
<li><strong>MSVC 2022</strong> v14.43.34808 on Windows 11</li>
</ul>
<h4>依赖库与驱动</h4>
<p>ORB-SLAM3 有着众多的依赖包。除了 Repo 里 Thirdparty 文件夹中自带的魔改过的 <strong>DBoW2、g2o 和 Sophus</strong> 外，其它的均需要手动下载编译。这里先列个清单，附上我在编译时实际使用的参考版本及下载地址：</p>
<ul>
<li><strong><a href="https://github.com/UZ-SLAMLab/ORB_SLAM3" rel="noreferrer" target="_blank">ORB-SLAM3</a></strong> <a href="https://github.com/UZ-SLAMLab/ORB_SLAM3/releases/tag/v1.0-release" rel="noreferrer" target="_blank">v1.0</a></li>
<li><strong><a href="https://opencv.org" rel="noreferrer" target="_blank">OpenCV</a></strong> <a href="https://github.com/opencv/opencv/releases/4.10.0" rel="noreferrer" target="_blank">v4.10.0</a></li>
<li><strong><a href="https://www.boost.org" rel="noreferrer" target="_blank">boost</a></strong> <a href="https://www.boost.org/users/history/version_1_87_0.html" rel="noreferrer" target="_blank">v1.87.0</a></li>
<li><strong><a href="https://eigen.tuxfamily.org" rel="noreferrer" target="_blank">eigen</a></strong> <a href="https://gitlab.com/libeigen/eigen/-/releases/3.4.0" rel="noreferrer" target="_blank">v3.4.0</a></li>
<li><strong><a href="https://github.com/stevenlovegrove/Pangolin" rel="noreferrer" target="_blank">Pangolin</a></strong> <a href="https://github.com/stevenlovegrove/Pangolin/releases/tag/v0.6" rel="noreferrer" target="_blank">v0.6</a></li>
<li><strong><a href="https://slproweb.com/products/Win32OpenSSL.html" rel="noreferrer" target="_blank">Win64OpenSSL</a></strong> <a href="https://slproweb.com/download/Win64OpenSSL_Light-3_4_1.exe" rel="noreferrer" target="_blank">v3.4.1 x64</a></li>
<li><strong><a href="https://structure.io/openni" rel="noreferrer" target="_blank">OpenNI2</a></strong> <a href="https://s3.amazonaws.com/com.occipital.openni/OpenNI-Windows-x64-2.2.0.33.zip" rel="noreferrer" target="_blank">v1.02.2.0.33 Beta x64</a></li>
</ul>
<h4>示例数据集</h4>
<p>由于我使用的是 Orbbec Astra 2，这是一个奥比中光推出的 <strong>带 IMU 的单目结构光深度相机</strong>，因此选择的示例数据为 RGB-D 类型。<br />
这是 RGB-D 数据集下载地址：<a href="https://cvg.cit.tum.de/data/datasets/rgbd-dataset/download" rel="noreferrer" target="_blank">cvg.cit.tum.de/data/datasets/rgbd-dataset/download</a>。由于 ORB-SLAM3 Repo 中的 Examples 文件夹自带有 associations 文件（关联 RGB 与 Depth 图片的 txt 文件），因此不用再下载 associate.py 脚本来生成。</p>
<p>其它类型（单目、双目等）可在这篇知乎专栏中找到：<a href="https://zhuanlan.zhihu.com/p/625605417">https://zhuanlan.zhihu.com/p/625605417</a>。</p>
<h2>ORB-SLAM3 库 构建流程</h2>
<h4>unistd.h</h4>
<p><code>unistd.h</code> 文件在 Unix 类系统中的作用类似于 Windows 中的 <code>Windows.h</code> 文件。从 Linux 移植到 Windows 平台的程序大多需要手动创建一个，避免编译时出现 <strong>无法打开源文件</strong> 的错误。</p>
<p>在 msvc <code>include</code> 文件夹（...\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.43.34808\include）中添加一个 <code>unistd.h</code> 文件：</p>
<pre><code class="language-c">/** This file is part of the Mingw32 package.
unistd.h maps (roughly) to io.h
*/
#ifndef _UNISTD_H
#define _UNISTD_H
#include &lt;io.h&gt;
#include &lt;process.h&gt;
#endif /* _UNISTD_H */
</code></pre>
<p>注意⚠️：文件末尾需<strong>留有一行空行</strong>，否则编译时会报 <code>Unexpected end of file</code> 错误。</p>
<p>P.S. MSVC 工具链升级更新后 Visual Studio 会在原版本工具链的文件夹下保留该文件，需要手动移动至新版本工具链的 <code>include</code> 目录中。</p>
<h4>OpenCV 安装</h4>
<p>直接下载后解压安装到 <code>Thirdparty</code> 子文件夹即可。</p>
<ul>
<li>OpenCV_DIR 目录为 <code>.\build</code></li>
</ul>
<h4>Win32OpenSSL 安装</h4>
<p>ORB-SLAM3 使用到了 md5 相关函数。如果缺少该库，在其编译时会出现 <code>无法打开包括文件: “openssl/md5.h”: No such file or directory</code> 的错误。</p>
<p>直接下载后安装到 <code>C:\Program Files</code> 即可。</p>
<ul>
<li>include 目录为 <code>.\include</code></li>
<li>lib 目录为 <code>.\lib\VC\x64\MT</code></li>
</ul>
<h4>OpenNI2 安装</h4>
<p>OpenNI2 是 Pangolin 和 ORB-SLAM3 的依赖，因其包含驱动，故直接下载安装到 <code>C:\Program Files</code> 即可。</p>
<h4>Boost 编译</h4>
<p>v1.87.0 版本的 Boost 可直接双击 <code>bootstrap.bat</code> 脚本即可生成 Boost 专用的构建工具 <code>b2.exe</code>（Boost-Build）。<br />
然后使用下面的命令来生成 debug/release 版本的动态与静态库：</p>
<pre><code>.\b2.exe stage --stagedir=".\stage" link=static runtime-link=shared runtime-link=static threading=multi debug release</code></pre>
<ul>
<li>Boost_DIR 目录为 <code>.\stage\lib</code></li>
</ul>
<p>若在链接时不能指定正确的链接库，则会在构建 ORB-SLAM3 和 Demo 时报大量的未定义符号错误；因此这里要介绍一下 Boost 编译得到的链接库命名规则：</p>
<pre><code>libboost_serialization-vc143-mt-sgd-x64-1_87.lib
---  b   -------------   d   --  f  ---  h
 a -----       c       ----- e  ---  g  ----</code></pre>
<ul>
<li><strong>a:</strong> 静态库以 lib 开头，动态库则无此前缀</li>
<li><strong>b:</strong> boost 库前缀</li>
<li><strong>c:</strong> 库子类名，serialization 库</li>
<li><strong>d:</strong> 编译器版本，visual c++ 14.3</li>
<li><strong>e:</strong> mt=multi-threaded，单线程则没有此段；可使用 <code>BOOST_LIB_THREAD_OP</code> 参数指定</li>
<li><strong>f:</strong> s=static 静态链接，gd=debug 构建类型为调试； 可使用 <code>BOOST_LIB_RT_OPT</code> 参数指定</li>
<li><strong>g:</strong> 平台架构名 x86；可使用 <code>BOOST_LIB_ARCH_AND_MODEL_OPT</code> 参数指定</li>
<li><strong>h:</strong> Boost 版本 v1.87；可使用 <code>BOOST_LIB_VERSION</code> 参数指定</li>
</ul>
<h4>DBoW2 编译</h4>
<p>从这个依赖开始，下面的都需要使用 CMake GUI 来进行项目的生成和编译了。<br />
步骤大致都相同，这里先详细列出，后面基本相同的地方便不再赘述：</p>
<ol>
<li>在待编译项目的根目录下创建一个 <code>build</code> 文件夹</li>
<li>打开 CMake GUI，<code>Where is the source code</code> 填写 <code>CMakeLists.txt</code> 所在位置（都是根目录），<code>Where to build the binaries</code> 填写刚创建的 <code>build</code> 文件夹位置</li>
<li>点击 Configure 后，在弹出的对话框中选择使用的 MSVC 编译器版本，多次点击 Configure 和填写 CMakeLists 中空缺的变量值（大多都是类似 OpenCV / Boost 等依赖库所在的位置），DBoW2 需要填写 OpenCV CMake 文件所在路径：</li>
</ol>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/porting-rosless-orb-slam3-to-windows/10cbab7759-1762605720/dbow2-cmake-gui-params-opencv.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/porting-rosless-orb-slam3-to-windows/10cbab7759-1762605720/dbow2-cmake-gui-params-opencv.png" width="400"></a></figure>
<ol start="4">
<li>填写完依赖路径之后，填写 <code>CMAKE_INSTALL_PREFIX</code> 为 <code>.\build\install</code>，以避免部分未指定输出路径变量的库直接将构建产物安装在系统路径 <code>program files</code> 下而混淆可能存在的其它版本：</li>
</ol>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/porting-rosless-orb-slam3-to-windows/d8145d6f47-1762605720/dbow2-cmake-gui-params-cmake-install-prefix.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/porting-rosless-orb-slam3-to-windows/d8145d6f47-1762605720/dbow2-cmake-gui-params-cmake-install-prefix.png" width="400"></a></figure>
<ol start="6">
<li>再次点击 Configure 确认不再有错误后，点击 Generate 以检查并生成项目。若在生成时出现 <code>Invalid escape sequence \U</code> 的错误，可将路径中的反斜杠替换为斜杠，例如：<code>C:\</code> =&gt; <code>C:/</code></li>
<li>成功生成项目后点击 Open Project 以在 Visual Studio 中打开</li>
<li>根据需要的构建类型，更改顶部菜单栏选择为 Debug/Release（建议一次性地将 <strong>Debug/Release</strong> 版本的 <strong>静态/动态链接库</strong> 构建好）</li>
<li>右键项目名(DBoW2)-&gt;属性-&gt;VC++目录 添加需要的依赖项：</li>
</ol>
<ul>
<li>包含目录：添加依赖项的 include 路径；如 DBoW2 需要添加 boost 库的 include 路径为 <code>..\..\..\Thirdparty\boost;</code></li>
<li>库目录：添加依赖项的 lib 路径；如 DBoW2 需要添加 boost 库的 lib 路径为 <code>..\..\..\Thirdparty\boost\stage\lib;</code></li>
</ul>
<ol start="10">
<li>由于 <strong>除了 ORB-SLAM3 本体外</strong> 编译的几个依赖库库都使用 <strong>静态链接</strong> 的方式生成（Linux GCC 移植到 Windows MSVC 时绝大部分库缺少 <strong>动态链接库导出符号需要的显式声明</strong>，后编译 ORB-SLAM3 时有详细介绍），因此需要在项目属性中配置：</li>
</ol>
<ul>
<li>常规-&gt;配置类型 选择 <code>静态库(.lib)</code></li>
<li>高级-&gt;目标文件扩展名 更改为 <code>.lib</code></li>
<li>根据需要选择 MSVC 运行库版本为 静态链接（MT/MTd） 或 动态链接 （MD/MDd），<strong>整个项目</strong> 需保持一致；C/C++-&gt;代码生成-&gt;运行库 若构建类型为 <code>Debug</code> 则选择 <code>多线程调试(/MTd)</code> 或  <code>多线程调试 DLL(/MDd)</code>，若为 <code>Release</code> 则选择 <code>多线程(/MT)</code> 或 <code>多线程 DLL(/MD)</code></li>
</ul>
<ol start="10">
<li>右键 ALL_BUILD-&gt;生成</li>
</ol>
<p>需要填写的 CMake 变量值：</p>
<ul>
<li>OpenCV_DIR: <code>C:\Users\usrlibzy\Documents\Visual Studio 2022\Projects\ORB_SLAM3\Thirdparty\opencv\build</code></li>
</ul>
<p>由于是使用 MSVC 编译，gcc 标准库中的 <code>stdint-gcc.h</code> 头文件自然是找不到的，但由于里面的类型由 MSVC 的其它头文件定义过，因此可以直接注释掉 <code>Thirdparty\DBoW2\DBoW2\FORB.cpp</code> 文件中的 <strong>include</strong> 引用：</p>
<pre><code class="language-c">//--//#include &lt;stdint-gcc.h&gt;</code></pre>
<p>最终构建产物位于 <code>Thirdparty\DBoW2\lib</code>。</p>
<h4>Eigen3 生成</h4>
<p>作为 g2o 的依赖的 Eigen3，其依赖 Boost 库只需在 CMake 配置时提供其 <code>include</code> 路径即可：</p>
<ul>
<li><strong>Boost_INCLUDE_DIR</strong>: <code>../../boost</code><br />
然后打开工程，右键 INSTALL 生成即可。此生成只包含头文件与 CMake 文件供 g2o 编译使用，不产生实际二进制文件。</li>
</ul>
<p>Eigen3_DIR 目录为 <code>.\build\artifacts\share\eigen3\cmake</code>。</p>
<h4>g2o 编译</h4>
<p>g2o 在使用 CMake GUI 生成项目时需配置其依赖 Eigen3 的 <code>include</code> 路径参数 <code>G2O_EIGEN3_INCLUDE</code> 为 <code>../eigen/build/artifacts/include/eigen3</code>。</p>
<p>g2o 的 <code>CMakeLists.txt</code> 中警告层级 <code>-W</code> 参数没有值，在 gcc 下为默认打印所有警告，而 MSVC 下必须显示赋值，因此手动去掉或修改为 <code>-W1</code> 即可；而其后的优化等级 <code>-O3</code> 以及平台优化 <code>-march=native</code> 均为 gcc/clang 特有参数，此处可直接删去以使用默认即可：</p>
<pre><code>#-- Explicitly set Warning param
%(AdditionalOptions) -W1</code></pre>
<p>平台优化选项 <code>-march=native</code> 将在下一篇文章点云地图重建中引入 <strong>PCL 库</strong> 时介绍，此处只需先<strong>不使能</strong>其对应于 MSVC 的替代，即项目属性-&gt;C/C++-&gt;代码生成-&gt;启用增强指令集 <strong>高级矢量扩展2</strong> <code>/arch:AVX2</code>。</p>
<p>项目中有以下几个文件的 <code>unordered_map</code> 和 <code>shared_ptr</code> 使用的还是 <strong>较高版本编译器已移除的</strong> <code>std::tr1</code> 命名空间，可以选择手动将 <code>std::tr1::unordered_map</code> 和 <code>std::tr1::shared_ptr</code> 分别替换为 <code>std::unordered_map</code> 和 <code>std::shared_ptr</code>：</p>
<ul>
<li>core/hyper_graph.h</li>
<li>core/estimate_propagator.h</li>
<li>core/marginal_covariance_cholesky.h</li>
<li>core/robust_kernel.h</li>
<li>core/sparse_block_matrix_ccs.h</li>
</ul>
<p>项目属性-&gt;C/C++-&gt;预处理器-&gt;预处理器定义 中需要添加一项 <code>WINDOWS;</code> （等价于在所有源文件中添加 <code>#define WINDOWS</code> 宏定义），否则会出现 <code>vasprintf 未定义符号</code> 的问题。</p>
<p>最终构建产物位于 <code>Thirdparty\g2o\build\Release</code> 和 <code>Thirdparty\g2o\build\Debug</code>。</p>
<h4>Pangolin 编译</h4>
<p>Pangolin 建议严格使用 v0.6 版本（release date 与 ORB-SLAM3 v1.0 最近）。</p>
<p>Pangolin 在使用 CMake GUI 生成项目时需配置其依赖 Eigen3 的 <code>include</code> 路径参数 <code>EIGEN_INCLUDE_DIR</code> 为 <code>C:/.../Thirdparty/eigen/build/artifacts/include/eigen3</code>。<br />
注意：Pangolin 的 <code>EIGEN_INCLUDE_DIR</code> 与 <code>CMAKE_INSTALL_PREFIX</code> 参数均需使用绝对路径。</p>
<p>Pangolin v0.6 存在一个 <a href="https://github.com/stevenlovegrove/Pangolin/issues/609" rel="noreferrer" target="_blank">已知问题 #609</a>，在编译过程中会出现大量类似于 <em>没有可用于扩展的参数包</em>、<em>使用类模板需要模板参数列表</em> 的错误。<br />
解决方法为，打开 <code>include\mpark\variant.hpp</code> 文件：</p>
<pre><code class="language-c">#if defined(__cpp_constexpr) &amp;&amp; __cpp_constexpr &gt;= 201304
//-- Commenting out line for Known-Issue: https://github.com/stevenlovegrove/Pangolin/issues/609#issuecomment-645653656
//--//#define MPARK_CPP14_CONSTEXPR
#endif</code></pre>
<p>将中间的 <code>define MPARK_CPP14_CONSTEXPR</code> 行注释掉即可。<br />
然后即可右键 ALL_BUILD-&gt;生成 以构建 Pangolin 依赖的静态库。生成结束后，可右键 INSTALL-&gt;生成 pangolin.lib。</p>
<p>注意⚠️：右键 INSTALL-&gt;生成 Pangolin.lib 时，生成的 Debug/Release 版本的 <code>pangolin.lib</code> 以及 <code>jpeg.lib</code> 名称相同会相互覆盖。建议先配置构建版本为 Debug，将这两个库的文件名末尾添加代表 <strong>debug</strong> 的字符 <code>d</code> 后，再切换为 <code>Release</code> 生成。最终当前目录下的所有库应该都是 Debug/Release 成对的。</p>
<p>最终构建产物位于 <code>Thirdparty\Pangolin\build\artifacts</code>。</p>
<h4>ORB-SLAM3 编译</h4>
<p>先对 <code>CMakeLists.txt</code> 进行修改。</p>
<p>第 #27 行，由于 MSVC 跳过 C++11 直接支持的 C++14，因此这里注释掉 FATAL_ERROR：</p>
<pre><code># Check C++11 or C++0x support
include(CheckCXXCompilerFlag)
CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)
CHECK_CXX_COMPILER_FLAG("-std=c++0x" COMPILER_SUPPORTS_CXX0X)
if(COMPILER_SUPPORTS_CXX11)
···
#-- Commenting out for compiling with MSVC C++14 support
#--#else()
#--#   message(FATAL_ERROR "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.")
endif()</code></pre>
<p>找到刚才 Thirdparty 中编译出的静态链接库位置，然后编辑 <code>CMakeLists.txt</code> 第 #221 行处的 <code>target_link_libraries</code>：</p>
<pre><code>target_link_libraries(${PROJECT_NAME}
${OpenCV_LIBS}
${EIGEN3_LIBS}
${Pangolin_LIBRARIES}
#-- Update thirdparty dependencies static libs
${PROJECT_SOURCE_DIR}/Thirdparty/DBoW2/lib/Release/DBoW2.lib
${PROJECT_SOURCE_DIR}/Thirdparty/g2o/build/Release/g2o.lib
)</code></pre>
<p>另外，由于 C++ 实现的不同以及平台更换后标准库间的差异，需要对 <strong>ORB-SLAM3 库项目</strong> 源码进行修改：</p>
<p><strong>MSVC 特性：</strong></p>
<p>打开 <code>MapPoint.cc</code> 文件，找到第 #371 行:</p>
<pre><code class="language-cpp">//-- Replacing float 2d-array with vector for MSVC not supporting dynamic-length array.
//--//float Distances[N][N];
vector&lt;vector&lt;float&gt; &gt; Distances(N, vector&lt;float&gt;(N));</code></pre>
<p>将 <code>float Distances[N][N];</code> 替换为 <code>vector&lt;vector&lt;float&gt; &gt; Distances(N, vector&lt;float&gt;(N));</code>。</p>
<p>以及同一个文件第 #388 行:</p>
<pre><code class="language-cpp">//--//vector&lt;int&gt; vDists(Distances[i],Distances[i]+N);
vector&lt;int&gt; vDists(Distances[i].begin(), Distances[i].end());</code></pre>
<p>打开 <code>Tracking.cc</code> 文件，找到第 #3544 行:</p>
<pre><code class="language-cpp">    //-- Modofied to fix vector iterators incompatible. Source: https://www.cnblogs.com/jiangym1998/p/13673145.html
    // Include also some not-already-included keyframes that are neighbors to already-included keyframes
    //--// for(vector&lt;KeyFrame*&gt;::const_iterator itKF=mvpLocalKeyFrames.begin(), itEndKF=mvpLocalKeyFrames.end(); itKF!=itEndKF; itKF++)
    for(int i=0; i&lt;mvpLocalKeyFrames.size(); i++)
    {
        // Limit the number of keyframes
        if(mvpLocalKeyFrames.size()&gt;80) // 80
            break;

        //-- Modofied to fix vector iterators incompatible
        //--//KeyFrame* pKF = *itKF;
        KeyFrame* pKF = mvpLocalKeyFrames[i];</code></pre>
<p>将 <code>for(vector&lt;KeyFrame*&gt;::const_iterator itKF=mvpLocalKeyFrames.begin(), itEndKF=mvpLocalKeyFrames.end(); itKF!=itEndKF; itKF++)</code> 替换为 <code>for(int i=0; i&lt;mvpLocalKeyFrames.size(); i++)</code>，以及 <code>KeyFrame* pKF = *itKF;</code> 替换为 <code>KeyFrame* pKF = mvpLocalKeyFrames[i];</code>。</p>
<p><strong>标准库符号缺失：</strong><br />
编译时会出现大量 <code>“usleep”: 找不到标识符</code> 的问题，可使用 <a href="https://stackoverflow.com/questions/5801813/c-usleep-is-obsolete-workarounds-for-windows-mingw" rel="noreferrer" target="_blank">StackOverflow</a> 上一个回答中的一个 <code>usleep</code> 实现。<br />
新建文件 <code>__port.cc</code>，添加函数定义：</p>
<pre><code class="language-cpp">void usleep(__int64 usec) 
{ 
    HANDLE timer; 
    LARGE_INTEGER ft; 

    ft.QuadPart = -(10*usec); // Convert to 100 nanosecond interval, negative value indicates relative time

    timer = CreateWaitableTimer(NULL, TRUE, NULL); 
    SetWaitableTimer(timer, &amp;ft, 0, NULL, NULL, 0); 
    WaitForSingleObject(timer, INFINITE); 
    CloseHandle(timer); 
}</code></pre>
<p>新建文件 <code>__port.h</code>，添加函数签名：</p>
<pre><code class="language-cpp">//-- Added usleep implementation. Source: http://stackoverflow.com/questions/5801813/c-usleep-is-obsolete-workarounds-for-windows-mingw
void usleep(__int64 usec);</code></pre>
<p>然后再在下面各文件中 <code>include __port.h</code>：</p>
<ul>
<li>Viewer.cc</li>
<li>Tracking.cc</li>
<li>System.cc</li>
<li>LocalMapping.cc</li>
<li>LoopClosing.cc</li>
<li>MapPoint.cc</li>
<li>Atlas.cc</li>
</ul>
<p>与前面相同的原因，需要在 <code>ORBmatcher.cc</code> 文件中直接注释掉 <code>#include &lt;stdint-gcc.h&gt;</code>。</p>
<p>项目属性-&gt;C/C++-&gt;预处理器-&gt;预处理器定义 中需要添加一项 <code>COMPILEDWITHC11;</code> （等价于在所有源文件中添加 <code>#define COMPILEDWITHC11</code> 宏定义），否则会出现 <code>"monotonic_clock": 不是 "std::chrono" 的成员</code> 及类似的编译期错误。<br />
最后就可以右键 ORB_SLAM3 项目-&gt;生成。可能会看到许多的 Warning，但最终是能够构建成功的。<br />
如果在链接时报错 <code>无法解析的外部符号 MD5_Init</code> 等 MD5 相关错误，则链接器未找到 OpenSSL 库，可手动添加 <code>OpenSSL-Win64\lib\VC\x64\MT\libcrypto.lib </code>。</p>
<p>注意⚠️：ORB-SLAM3 需要编译为 <strong>与 OpenCV 同类型 的链接方式</strong>。由于 OpenCV 项目较大，静态链接出的 .lib 文件巨大，因此选择直接安装动态链接的 OpenCV 库。如果 ORB-SLAM3 在这里选择使用静态的方式来链接，能够正常运行 TUM 数据集，但 Port 到摄像头时便会出现 <code>Debug Assertion Failed! Expression: __acrt_first_block == header</code> 的问题：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/porting-rosless-orb-slam3-to-windows/c17675101a-1762605720/clion-debug-mal-link-type-assert-failure.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/porting-rosless-orb-slam3-to-windows/c17675101a-1762605720/clion-debug-mal-link-type-assert-failure.png"></a></figure>
<p><a href="https://stackoverflow.com/questions/35310117/debug-assertion-failed-expression-acrt-first-block-header/35311928#35311928" rel="noreferrer" target="_blank">StackOverflow</a> 上有对此问题的详细解释，大概可以总结为：给动态链接库和静态链接库的分配的是不同的堆内存，因此可能出现 alloc 与 dealloc <strong>并非同一堆内存</strong> 的问题。</p>
<p><strong>导出符号：</strong><br />
由于 MSVC 对于 <strong>动态链接库的生成</strong> 需要通过 <code>__declspec(dllexport)</code> 声明 或使用 <strong>def</strong> 文件枚举 的方式 <strong>显式指定导出符号</strong>，才能正确生成包含这些符号的 DLL。因此还需要最后一步：将 <strong>需要在自己的项目中引用的符号（即 ORB-SLAM3 的接口）</strong> 使用宏定义的方式声明。<br />
在上一步创建的 <code>__port.h</code> 文件中添加宏定义：</p>
<pre><code class="language-cpp">//-- Macro Declarations for exporting symbols
#ifdef ORB_SLAM3_BUILD
#define ORB_SLAM3_API __declspec(dllexport)
#else
#define ORB_SLAM3_API __declspec(dllimport)
#endif</code></pre>
<p>然后在 <code>System.h</code> 文件中找到 <strong>System 类</strong>，在所有需要调用的函数前面添加 <code>ORB_SLAM3_API</code>，例如</p>
<pre><code class="language-cpp">ORB_SLAM3_API Sophus::SE3f TrackRGBD(const cv::Mat &amp;im, const cv::Mat &amp;depthmap, const double &amp;timestamp, const vector&lt;IMU::Point&gt;&amp; vImuMeas = vector&lt;IMU::Point&gt;(), string filename="");</code></pre>
<p>最后在 项目属性-&gt;C/C++-&gt;预处理器-&gt;预处理器定义 中添加一项 <code>ORB_SLAM3_BUILD;</code> 即可。</p>
<p>最终构建产物位于 <code>.\build\Release</code> 和 <code>.\build\Debug</code>。</p>
<h2>Demo 构建流程</h2>
<p>最后准备编译 RGB-D Example 来验证移植结果：</p>
<ol>
<li>同样地，需要右键 rgbd_tum 项目-&gt;属性-&gt;C/C++-&gt;预处理器-&gt;预处理器定义 中添加一项 <code>COMPILEDWITHC11;</code>（等价于在所有源文件中添加 <code>#define COMPILEDWITHC11</code> 宏定义），否则会出现 <code>"monotonic_clock": 不是 "std::chrono" 的成员</code> 及类似的编译期错误</li>
<li>链接器-&gt;高级-&gt;导入库 删除所有项</li>
<li>尝试右键项目-&gt;生成，若出现找不到链接库的错误时，可再打开项目属性，在链接器-&gt;输入-&gt;附加依赖项中添加即可</li>
</ol>
<p>P.S. 若在链接时出现 <code>error LNK2038: 检测到“RuntimeLibrary”的不匹配项</code> 问题时，先查看是否更改 项目属性-&gt;C/C++-&gt;代码生成-&gt;运行库 是否一致（在本例中为 多线程 /MT），然后查看 报错的动态运行时链接库所对应的静态运行时链接库是否在 链接器-&gt;输入-&gt;附加依赖项 中出现。若未出现，应手动添加。</p>
<p>构建完成后，可打开 <code>Examples/RGB-D/Release</code> 文件夹，将生成的文件连同 ORB-SLAM3 自带的 TUM1 配置文件、associations 文件夹和下载的数据集，以及 OpenCV 动态链接库 复制到单独的文件夹中，然后右键打开 Terminal，执行命令：</p>
<pre><code>.\rgbd_tum.exe ..\..\Vocabularies\ORBvoc.txt ..\..\Vocabularies\ORBvoc.txt .\tum\TUM1.yaml ..\..\Datasets\rgbd_dataset_freiburg1_room\ ..\..\Datasets\associations\fr1_room.txt</code></pre>
<p>等待几秒 Vocabulary 的加载后，demo 大概是能成功运行起来咯：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/porting-rosless-orb-slam3-to-windows/5ac14353ec-1762605720/rgbd_tum_demo_fr1_room.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/porting-rosless-orb-slam3-to-windows/5ac14353ec-1762605720/rgbd_tum_demo_fr1_room.png"></a></figure>
<p>完全按照文章流程的步骤，是可以正常加载 Debug 符号进行调试的：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/porting-rosless-orb-slam3-to-windows/8897da4660-1762605720/clion-debug-dense-map.jpeg"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/porting-rosless-orb-slam3-to-windows/8897da4660-1762605720/clion-debug-dense-map.jpeg"></a></figure>
<p>Happy debugging😛</p>
<h2>后记</h2>
<p><del>后面还会有一篇来详细记录下密集点云建图的实现</del><br />
（因为搜到了几篇很有意思的论文所以）打算开一个专题，专门记录三维建图算法的<del>学习</del>折腾。今天先这样啦😊</p>
<h2>一些有用的参考链接</h2>
<ol>
<li><a href="https://github.com/UZ-SLAMLab/ORB_SLAM3/pull/53" rel="noreferrer" target="_blank">github.com/UZ-SLAMLab/ORB_SLAM3/pull/53</a></li>
<li><a href="https://github.com/lydieusang/orbslam3-windows" rel="noreferrer" target="_blank">github.com/lydieusang/orbslam3-windows</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/625605417" rel="noreferrer" target="_blank">zhuanlan.zhihu.com/p/625605417</a></li>
<li><a href="https://www.cnblogs.com/oloroso/p/8574936.html" rel="noreferrer" target="_blank">cnblogs.com/oloroso/p/8574936.html</a></li>
<li><a href="https://blog.csdn.net/xp178171640/article/details/102371279" rel="noreferrer" target="_blank">blog.csdn.net/xp178171640/article/details/102371279</a></li>
<li><a href="https://learn.microsoft.com/zh-cn/cpp/build/determining-which-exporting-method-to-use?view=msvc-170#pros-and-cons-of-using-def-files" rel="noreferrer" target="_blank">learn.microsoft.com/zh-cn/cpp/build/determining-which-exporting-method-to-use</a></li>
<li><a href="https://learn.microsoft.com/en-us/cpp/build/exporting-from-a-dll-using-declspec-dllexport?view=msvc-170" rel="noreferrer" target="_blank">learn.microsoft.com/en-us/cpp/build/exporting-from-a-dll-using-declspec-dllexport</a></li>
<li><a href="https://www.cnblogs.com/jiangym1998/p/13673145.html" rel="noreferrer" target="_blank">cnblogs.com/jiangym1998/p/13673145.html</a></li>
</ol>]]>

            </description>
        </item>
        <item>
            <title>k3s 集群维护笔记 - 1</title>
            <link>https://zyi.io/zh/corpus/note/k3s-cluster-op-notes-1</link>
            <guid isPermaLink="false">corpus/note/k3s-cluster-op-notes-1</guid>
            <pubDate>Sat, 04 Jan 2025 00:00:00 +0000</pubDate>
            <description>

            <![CDATA[<p>本文 Galera 集群部分还在更新中，敬请等待完稿日 :)</p>
<hr />
<p>拥有一个能稳定运行的 k3s/k8s 集群可能并不太容易（所以这只会是一个系列中的一篇😝）。我将在这个系列中记录我爬坑的过程，既为我<del>之后重建x</del>，也为可能存在的读者作一定的参考。<br />
对于文章中的内容，如果有错误，或是你有更好的方案，欢迎在文末提出。😊</p>
<h2>网络</h2>
<p>网络可以说是集群维护中最头痛的一件事了，特别是在大陆部署服务器时：国内服务器带宽小且贵，多数 registry、repository、api 不是太慢就是 connection reset、dns 污染。总之，部署在大陆的服务器有一个离不开的一个问题，如何让外网流量正常地进出？</p>
<p>之前我的方案是 <strong>使用 openwrt 作为软路由</strong> 的方式透明代理所有流量，但因为 clash 分流配置复杂存在内存泄漏需要定时重启、软路由会增加 Tailscale 直连难度等问题，最后现在选用 <strong>自建 dns + http/socks proxy via Tailscale</strong> 的方式，完美解决了长期的痛点。</p>
<h4>DNS</h4>
<p>目前采用的方案是在国内服务器集群内多节点部署 <a href="https://github.com/AdguardTeam/AdGuardHome" rel="noreferrer" target="_blank">AdGuard-Home</a> （一般三个就足够了，而且用着还算稳定可靠），并指向 <strong>可信上游 dns</strong> （可在大陆外再搭一台只做上游，或仅使用 doh 然后把缓存拉满，运营商的那些统统丢掉），systemd-resolved 会自动 Failover，那在每台机器上都填完整就好了。</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-1/c09396ca8c-1762605720/adguard-home.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-1/c09396ca8c-1762605720/adguard-home.png"></a></figure>
<h4>镜像拉取/临时终端代理方案</h4>
<p>要在 docker 拉取时应用代理，可编辑 <code>/etc/docker/daemon.json</code> 文件来配置：</p>
<pre><code>{
  "proxies": {
    "no-proxy": "*.local, localhost, 127.0.0.0/8, 10.24.0.0/16, 100.64.0.0/10, 10.42.0.0/16",
    "http-proxy": "http://&lt;user&gt;:&lt;password&gt;@&lt;proxy-address&gt;:&lt;http-port&gt;",
    "https-proxy": "http://&lt;user&gt;:&lt;password&gt;@&lt;proxy-address&gt;:&lt;http-port&gt;"
  }
}</code></pre>
<p>而对于平常终端中使用时，可以添加一个简单的 bash 命令：</p>
<pre><code>use_proxy () {
        export HTTP_PROXY=http://&lt;user&gt;:&lt;password&gt;@&lt;proxy-address&gt;:&lt;http-port&gt;
        export HTTPS_PROXY=http://&lt;user&gt;:&lt;password&gt;@&lt;proxy-address&gt;:&lt;http-port&gt;
        export ALL_PROXY=socks5://&lt;user&gt;:&lt;password&gt;@&lt;proxy-address&gt;:&lt;socks5-port&gt;
}</code></pre>
<p>然后在需要时（例如进行软件包安装，或者调用远程脚本等）键入 <code>use_proxy</code> 并回车即可。</p>
<h4>容器流量代理方案</h4>
<p>现在得到正确的 ip 已有了可靠的保证，接下来就该部署代理了。 <br />
目前采用的方案是在 <em>线路稍好</em> 的国外服务器上部署一个 <strong>只 bind Tailnet 地址的 v2ray http/socks5 proxy</strong>（如果国外节点多的话可以用 nginx 做负载均衡），然后向需要用到代理的主机、容器什么的添加环境变量就好了。</p>
<p>为 k3s 容器添加环境变量最方便的方式为：单独将代理参数存入一个 configmap 中： </p>
<pre><code>apiVersion: v1 
data: 
  ALL_PROXY: socks5://&lt;user&gt;:&lt;password&gt;@&lt;proxy-address&gt;:&lt;socks5-port&gt;

  HTTP_PROXY: http://&lt;user&gt;:&lt;password&gt;@&lt;proxy-address&gt;:&lt;http-port&gt;

  HTTPS_PROXY: http://&lt;user&gt;:&lt;password&gt;@&lt;proxy-address&gt;:&lt;http-port&gt;

  NO_PROXY: &gt;- 
    .local, *.local, localhost, 127.0.0.0/8, 100.64.0.0/10, 10.42.0.0/16, 10.43.0.0/16

  all_proxy: socks5://&lt;user&gt;:&lt;password&gt;@&lt;proxy-address&gt;:&lt;socks5-port&gt;

  http_proxy: http://&lt;user&gt;:&lt;password&gt;@&lt;proxy-address&gt;:&lt;http-port&gt;

  https_proxy: http://&lt;user&gt;:&lt;password&gt;@&lt;proxy-address&gt;:&lt;http-port&gt;

  no_proxy: &gt;- 
    .local, *.local, localhost, 127.0.0.0/8, 100.64.0.0/10, 10.42.0.0/16, 10.43.0.0/16

kind: ConfigMap </code></pre>
<p>其中 NO_PROXY/no_proxy 将使下列目标绕过代理：</p>
<ul>
<li><strong>.local, *.local</strong>: 集群域名</li>
<li><strong>localhost, 127.0.0.0/8</strong>: lo</li>
<li><strong>100.64.0.0/10</strong>: Tailnet</li>
<li><strong>10.42.0.0/16, 10.43.0.0/16</strong>: k3s 集群 pod cidr 和 service cidr</li>
</ul>
<p>然后在创建 deployment 时引用这个 configmap 即可。</p>
<p>注意：</p>
<ul>
<li>配置代理的环境变量共8条，最好一条都不少（<a href="https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/" rel="noreferrer" target="_blank">不同的程序对大小写有不一样的要求&iquest;</a>）</li>
<li>使用代理参数后，所有没在 NO_PROXY/no_proxy 中出现的目标流量将经过代理；因此按照上述的配置后，容器内的服务需要使用完整的集群内域名以保证命中规则，例如 service <code>prometheus.prometheus</code> 应该使用 <code>prometheus.prometheus.svc.cluster.local</code></li>
<li>大多数情况下可通过进入容器使用 <strong>curl -vvv</strong> 来判断是否走了代理，如下所示：</li>
</ul>
<pre><code>grafana-57968c8758-s9b52:/usr/share/grafana$ curl prometheus.prometheus.svc.cluster.local:9090 -vvv
07:55:46.701660 [0-x] == Info: [READ] client_reset, clear readers
07:55:46.701800 [0-x] == Info: Uses proxy env variable no_proxy == '.local, *.local, localhost, 127.0.0.0/8, 10.24.0.0/16, 100.64.0.0/10, 10.42.0.0/16, 10.43.0.0/16'
07:55:46.900827 [0-0] == Info: Host prometheus.prometheus.svc.cluster.local:9090 was resolved.
07:55:46.900978 [0-0] == Info: IPv6: (none)
07:55:46.901066 [0-0] == Info: IPv4: 10.43.110.251
07:55:46.901166 [0-0] == Info: [SETUP] added
07:55:46.901292 [0-0] == Info:   Trying 10.43.110.251:9090...
07:55:46.901590 [0-0] == Info: Connected to prometheus.prometheus.svc.cluster.local (10.43.110.251) port 9090
07:55:46.901808 [0-0] == Info: using HTTP/1.x
07:55:46.902026 [0-0] =&gt; Send header, 108 bytes (0x6c)</code></pre>
<h2>k3s datastore</h2>
<p><a href="https://zyi.io/zh/corpus/note/build-extensible-cross-cloud-k3s-cluster-using-tailscale#:~:text=%E7%94%B1%E4%BA%8E%20datastore%20%E4%BD%9C%E4%B8%BA%20k3s%20%E9%9B%86%E7%BE%A4%E7%9A%84%E6%A0%B8%E5%BF%83%EF%BC%8C%E8%BF%99%E9%87%8C%E7%89%B9%E5%9C%B0%E6%A0%87%E6%98%8E%E5%87%A0%E4%B8%AA%E8%A6%81%E7%82%B9%EF%BC%8C%E5%85%B6%E5%85%B3%E7%B3%BB%E7%9D%80%E6%90%AD%E5%BB%BA%E7%9A%84%E9%9B%86%E7%BE%A4%E6%98%AF%E5%90%A6%E8%83%BD%E7%A8%B3%E5%AE%9A%E4%BD%BF%E7%94%A8%EF%BC%88%E8%80%8C%E4%B8%8D%E6%98%AF%E6%B5%AA%E8%B4%B9%E5%A4%A7%E9%87%8F%E6%B2%A1%E5%BF%85%E8%A6%81%E7%9A%84%E6%97%B6%E9%97%B4%E6%9D%A5%E6%8A%98%E8%85%BE%20%E2%9A%A0%EF%B8%8F" rel="noreferrer" target="_blank">之前</a> 有强调过 k3s datastore 的重要性，然而当预算不足时只能在两个代价之间找平衡点。我给出的折中方案为 <strong>MariaDB Galera 集群</strong>；为了让数据库能够稳定高可用，这里将给出一些数据库集群的使用 techniques 供参考。</p>
<h4>负载均衡 &amp; Failover</h4>
<p>首先，应使用互联延迟低、运行稳定的机器作数据库节点；然后使用 TCP Loadbalancer 来实现 Failover，可以固定频繁读写节点，有效地减少数据库冲突发生可能性的同时保证单个数据库节点宕机时能平滑切换，从而达到数据库高可用的目的。</p>
<p>Nginx <a href="https://docs.nginx.com/nginx/admin-guide/load-balancer/tcp-udp-load-balancer/" rel="noreferrer" target="_blank">只需要几行</a> 即可方便地配置为 TCP Loadbalancer，但貌似 free 版本（非 plus 版）需要手动编译带 <code>--with-stream</code> 参数来添加 <strong>ngx_stream_core_module 模块</strong> 以支持四层负载均衡。随后我又经过了一番广泛的搜索，<a href="https://serverfault.com/questions/1011369/any-reason-to-not-use-nginx-free-to-load-balance-exchange-2019" rel="noreferrer" target="_blank">最后</a> 发现了性能更高的 <strong>HAProxy</strong>。</p>
<h4>1:2 分裂 crash recovery</h4>
<p>接下来我将参考 <a href="https://galeracluster.com/documentation/html_docs_remove-wsrep-new-cluster/documentation/crash-recovery.html" rel="noreferrer" target="_blank">官方文档</a>，记录 <strong>典型三节点全主数据库集群</strong> 遇到一些常见情形时的 crash recovery 流程。</p>
<p>集群分裂大致是由网络掉线或电源中断造成：前者相较于后者，其掉线后可能还在继续写事务，即可能产生冲突。因此集群分裂后恢复时最主要的步骤是 <strong>确认拥有最新状态的节点</strong>，使用其作为 <strong>集群恢复的 bootstrap 节点</strong> 以便于其它节点能通过 <strong>Incremental State Transfer</strong> 来 <strong>以增量形式</strong> 同步状态。<br />
最新状态序列号（recovery position state sequence）可通过 <code>sudo -u mysql mysqld --wsrep-recover</code> 命令来得到。</p>
<p>例如，通过在节点一执行上述命令，得到其 recovery position 为 <strong>3864673</strong>：</p>
<pre><code> ~  sudo -u mysql mysqld --wsrep-recover                                                                                                
2025-01-04  5:03:47 0 [Note] Starting MariaDB 10.11.8-MariaDB-0ubuntu0.24.04.1 source revision 3a069644682e336e445039e48baae9693f9a08ee as process 1272794
2025-01-04  5:03:47 0 [Note] InnoDB: Compressed tables use zlib 1.3
2025-01-04  5:03:47 0 [Note] InnoDB: Number of transaction pools: 1
2025-01-04  5:03:47 0 [Note] InnoDB: Using crc32 + pclmulqdq instructions
2025-01-04  5:03:47 0 [Note] InnoDB: Using liburing
2025-01-04  5:03:47 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB
2025-01-04  5:03:47 0 [Note] InnoDB: Completed initialization of buffer pool
2025-01-04  5:03:47 0 [Note] InnoDB: File system buffers for log disabled (block size=512 bytes)
2025-01-04  5:03:47 0 [Note] InnoDB: End of log at LSN=8179900673
2025-01-04  5:03:47 0 [Note] InnoDB: 128 rollback segments are active.
2025-01-04  5:03:47 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ...
2025-01-04  5:03:47 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB.
2025-01-04  5:03:47 0 [Note] InnoDB: log sequence number 8179900673; transaction id 7569239
2025-01-04  5:03:47 0 [Warning] InnoDB: Skipping buffer pool dump/restore during wsrep recovery.
2025-01-04  5:03:47 0 [Note] Plugin 'FEEDBACK' is disabled.
2025-01-04  5:03:47 0 [Warning] You need to use --log-bin to make --expire-logs-days or --binlog-expire-logs-seconds work.
2025-01-04  5:03:47 0 [Note] Server socket created on IP: '100.120.32.65'.
2025-01-04  5:03:47 0 [Note] WSREP: Recovered position: 93114f49-c453-11ef-b5ed-5b52f2d6eaa1:3864673</code></pre>
<p>节点二与节点一相同，而节点三为 <strong>3694624</strong>：</p>
<pre><code> ~  sudo -u mysql mysqld --wsrep-recover                                                                                                     
2025-01-04 13:04:45 0 [Note] Starting MariaDB 10.11.8-MariaDB-0ubuntu0.24.04.1 source revision 3a069644682e336e445039e48baae9693f9a08ee as process 249409
2025-01-04 13:04:45 0 [Note] InnoDB: Compressed tables use zlib 1.3
2025-01-04 13:04:45 0 [Note] InnoDB: Number of transaction pools: 1
2025-01-04 13:04:45 0 [Note] InnoDB: Using crc32 + pclmulqdq instructions
2025-01-04 13:04:45 0 [Note] InnoDB: Using liburing
2025-01-04 13:04:45 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB
2025-01-04 13:04:45 0 [Note] InnoDB: Completed initialization of buffer pool
2025-01-04 13:04:45 0 [Note] InnoDB: File system buffers for log disabled (block size=512 bytes)
2025-01-04 13:04:45 0 [Note] InnoDB: End of log at LSN=7281719277
2025-01-04 13:04:45 0 [Note] InnoDB: 128 rollback segments are active.
2025-01-04 13:04:45 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ...
2025-01-04 13:04:45 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB.
2025-01-04 13:04:45 0 [Note] InnoDB: log sequence number 7281719277; transaction id 6622581
2025-01-04 13:04:45 0 [Warning] InnoDB: Skipping buffer pool dump/restore during wsrep recovery.
2025-01-04 13:04:45 0 [Note] Plugin 'FEEDBACK' is disabled.
2025-01-04 13:04:45 0 [Warning] You need to use --log-bin to make --expire-logs-days or --binlog-expire-logs-seconds work.
2025-01-04 13:04:45 0 [Note] Server socket created on IP: '100.120.3.9'.
2025-01-04 13:04:45 0 [Note] WSREP: Recovered position: 93114f49-c453-11ef-b5ed-5b52f2d6eaa1:3694624</code></pre>
<p>由于状态序列号 3694624 &lt; 3864673，即节点三的状态没跟得上节点一与二（因为上述事例是因为节点三断电导致）。此时即可编辑 bootstrap 节点（即节点一或二）上的 <code>/var/lib/mysql/grastate.dat</code> 文件，将原本为 <code>0</code> 的 <code>safe_to_bootstrap</code> 改为 <code>1</code>，然后执行 <code>galera_new_cluster</code> 来重置集群。</p>
<p>注意：节点一和二脱离节点三，需要在重置集群前先 <code>sudo mysql</code> 后执行 <code>SET GLOBAL wsrep_provider_options='pc.bootstrap=true';</code> 来 <strong>手动使能 primary component</strong>。</p>
<h2>Monitoring</h2>
<p>最后，为了能保证集群的稳定，<del>在宕机之前</del>做好监控与预警是有必要的。<br />
以前使用过 <strong>ELK stack</strong>，虽然用起来非常舒服，但由于 elasticsearch 是做全文索引，再加上它还是用 memory hogger 写的，集群 <strong>内外存的资源开销</strong> 都过于庞大。因此最后换成了最早尝试使用的 <strong>prometheus stack</strong>。</p>
<p>先给几张实际截图看看。</p>
<p><strong>Grafana logs:</strong></p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-1/f23d263365-1762605720/grafana-logs.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-1/f23d263365-1762605720/grafana-logs.png"></a></figure>
<p><strong>Grafana node metrics:</strong></p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-1/43951eb403-1762605720/grafana-node-metrics.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/k3s-cluster-op-notes-1/43951eb403-1762605720/grafana-node-metrics.png"></a></figure>
<p>prometheus stack 的主要组成为:</p>
<ul>
<li><strong>Grafana</strong>: 可视化 Web 应用</li>
<li><strong>Prometheus</strong>: 从 *_exporter 采集指标数据</li>
<li><strong>Loki</strong>: 日志数据采集</li>
<li><strong>*_exporter</strong>: 指标数据收集与统一 expose</li>
<li><strong>promtail</strong>: 收集与提交日志数据至 Loki</li>
</ul>
<p>这个 stack 中只有 Grafana、Prometheus 和 Loki 集中部署，后两者部署在所有需要收集数据的机器上，类似于 elastic-agent；总体来说非常好安装啦。</p>
<p>对于 node_exporter 和 promtail 这两个 agent 来说，有个部署的小技巧：若要在集群内中的所有节点上均部署，可以在编写 deployment 时指定 <strong>pod 副本数为 节点数</strong>、pod 调度中指定 <strong>反亲和性拓扑键为 kubernetes.io/hostname</strong>，然后在每个节点下初始化完各自的配置文件，最后 <code>kubectl apply -f</code>，k3s 便会自动在每个节点上都部署一个 pod。下面是 deployment.yaml 中的一个示例片段：</p>
<pre><code>spec:
  replicas: 2
  strategy:
    type: Recreate
  template:
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector: {}
              topologyKey: kubernetes.io/hostname    </code></pre>
<p>值得注意的一点是，对于 promtail 来说，它是主动推送日志数据至 loki，因此不需要额外的配置。而对于 node_exporter，它需要使用 host network 而不是 service 来暴露服务，否则从不同节点端口采集到的都是相同的、来自任意 pod 的数据。</p>]]>

            </description>
        </item>
        <item>
            <title>使用 Tailscale 轻松搭建易扩展的跨云 k3s 集群</title>
            <link>https://zyi.io/zh/corpus/note/build-extensible-cross-cloud-k3s-cluster-using-tailscale</link>
            <guid isPermaLink="false">corpus/note/build-extensible-cross-cloud-k3s-cluster-using-tailscale</guid>
            <pubDate>Mon, 25 Nov 2024 00:00:00 +0000</pubDate>
            <description>

            <![CDATA[<p>上一篇<del>两年前的</del>文章 <a href="https://zyi.io/zh/corpus/note/setup-k3s-rancher-cluster">记 容器编排工具 k3s + Rancher 环境的搭建</a> 记录了 <strong>外置 mysql 数据库、运行在 Azure 云上的 k3s 集群</strong>，以及方便的集群管理工具 Rancher 的安装与配置过程。</p>
<p>对于家中（寝室？学校/实验室？）有自建 NAS 、树莓派/服务器等 Homelab 设施，以及在多个云服务商部署有云服务器这种更为常见场景的我来说，为了设法充分利用这些资源，我在这之后又探索了各种能够实现分布式高可用 k3s 集群的搭建。<br />
对于这种应用场景下的集群，为了保证稳定和可靠性，要反复斟酌的主要有这几个点：</p>
<ol>
<li>如何为处于不同网络环境下的机器完成互联？<br />
即使是分布在不同的运营商，让拥有公网 IP 的云服务器之间互联也很容易（例如用 WireGuard 组网，甚至不组网直接使用 IPv6 亦可）。但如何将他们和 Homelab 里没有公网 IP 的机器互联呢？</li>
<li>在可能出现延迟突升甚至机器掉线的情况下，使用 k3s 作为集群服务部署工具，该选取哪种 datastore 方案（嵌入式 SQLite / 嵌入式 Etcd / 外置 Etcd 集群 / 外置 Mysql Postgres）呢？</li>
</ol>
<p>最终经过多次反复试错和尝试，我的部署方案停留在 <strong>使用 自建 MariaDB Galera 集群的多 Server 节点高可用部署</strong> 和 <strong>通过 Tailscale NAT 穿透 建立跨云内网实现跨云 k3s 集群</strong>。本文在 k3s 跨云的各种坑中摸爬滚打后，给出如下的能够较完美运行的配置方案供大家参考。<br />
那么我们就开始吧☺️</p>
<h2>WireGuard 与 Tailscale</h2>
<p>在正式的部署流程前，先给大家简要介绍下 WireGuard VPN 和 Tailscale 这个强大的组网工具。</p>
<p>WireGuard 是一个用于替代 OpenVPN、L2TP/IPsec 这类传统 VPN 的高性能、现代化且安全的 VPN 协议（官网是这么描述的），它与这些传统 VPN 的主要区别有：</p>
<ul>
<li>WireGuard 在保证核心功能 <strong>“加密隧道”</strong> 实现的同时切割掉了许多不必要的功能，因此它相当的简洁（代码只有几千行），还有着非常简单的配置步骤</li>
<li>WireGuard 可以运行在内核态，避免了与用户态之间来回切换，因此速度非常快，且可以支持很高的带宽</li>
<li>WireGuard 没有 Server/Client 之分，<strong>连线的两端地位是对等的</strong>，因此非常适合用来进行各种拓扑组网</li>
</ul>
<p><a href="https://zhuanlan.zhihu.com/p/404402933" rel="noreferrer" target="_blank">知乎</a> 上有一篇文章详细介绍了 WireGuard 是如何对传统 VPN 进行精简化的；后面我也会看看源码学习学习，毕竟我对能简洁优雅地实现核心功能的东西相当感兴趣。</p>
<p><a href="https://tailscale.com">Tailscale</a> 是一个基于 Wireguard 实现的 mesh 网络构建器。它大致是通过一个集中式 control plane 记录与分发不同节点之间的 wireguard 连接配置，以及一些灵活的打洞黑科技来实现的。<br />
对于大多数用户来说，Tailscale 的主要功能即通过搭建 mesh vpn，将处于不同网络的设备连接在同一个网络下。</p>
<h2>使用 Tailscale 为集群跨云互联组建虚拟内网</h2>
<p>首先要在这里说明的一点是：k3s 在较新版本中自带有 Tailscale 支持（详见 <a href="https://docs.k3s.io/networking/distributed-multicloud" rel="noreferrer" target="_blank">Distributed hybrid or multicloud cluster</a>），但是并不建议使用：</p>
<ul>
<li>对于每个节点，用于 k3s 网络基础设施维护的 <strong>flannel</strong> 会创建一个 <strong>cidr 子网</strong>。<br />
比如说节点 A 下的所有 pod 会分配 10.42.0.0/16 的地址，节点 B 下的所有 pod 会分配 10.42.1.0/16 的地址。<br />
Tailscale 需要 <strong>为每个节点创建一个 Subnet Router</strong> 来完成 <strong>跨节点间 pod 网络的互联</strong> （例如 10.42.0.31 &lt;-&gt; 10.42.1.56）。免费版只有一个 router 的额度，<strong>自建 Headscale</strong> 的话稳定性可能得不到较好的保障，因此不考虑自建。</li>
<li>这个方案需要让 k3s 接管该节点的 Tailscale 的运行，虽然可以传自定义参数进去，倘若在集群运行时对 Tailscale 进行配置的话，可能玩着玩着集群就炸了😊</li>
</ul>
<p>其次还要再提醒一下，本文的方案是 <strong>在每个节点上都跑 Tailscale</strong> 来组网，但在经过一段时间的运行后发现，就算所有设备均始终处在校园网这个大内网中，<strong>Tailscale 也并不是随时都能打通直连的喔</strong>，特别是在 <strong>类似于 site2site 拓扑</strong> 的网络架构时，有较小几率出现一边只有一台机器能够直连而其他机器走代理的现象，V 站上也存在这样的讨论（可以搜搜，地址嚒我不记得了）。这种情况就建议在本城开个 derp 做个最低延迟的中转咯；要不就 site2site，参考官方文档 <a href="https://tailscale.com/kb/1214/site-to-site#select-a-subnet-router" rel="noreferrer" target="_blank">Site-to-site networking</a>。当然，这个也需要两个或以上的 <strong>Subnet Router</strong>。</p>
<h4>网络基础设施配置</h4>
<p>网络基础设施的准备主要分为两个方面：搭建分布式网络，以及大陆用户绕不开的完整互联网访问。</p>
<p>先使用下面的命令在各个节点上安装好 Tailscale：</p>
<pre><code class="language-bash">curl -fsSL https://tailscale.com/install.sh | sh</code></pre>
<p>然后执行 <code>tailscale up</code>，点击提示中的链接登录即可。<br />
集群中所有节点均安装完成后，可以进入 <a href="https://login.tailscale.com/admin/machines" rel="noreferrer" target="_blank">Tailscale Admin Console</a> 为节点配置各自的 IP。</p>
<p>P.S. 这里出现了第一个坑，注意 <strong>不要使用 Exit Node</strong>，这将会导致 <strong>flannel 10.42.0.0/16</strong> 所有的包都丢被到出口节点上，就算加上 <code>--exit-node-allow-lan-access</code> flag 也行不通（因为 <strong>10.42.0.0/16</strong> 并不在 Tailscale 定义的 Lan 段内）。</p>
<p>由于跑的许多服务都需要例如 Google API 等服务的支持，我之前在大陆内单台服务器的做法都是直接使用出口节点将包转发到境外机器上。</p>
<p>对于现在的部署方案，这种做法就行不通咯。截止目前，我暂时还没有找到能够正确部署这个路由规则的方法（只要这句话还在本文当中，就说明我还没能有时间来研究😭）。此外，Tailscale 打洞的难度会随着两个 peer 间 hop 数量的增加而增加，因此使用 openwrt 软路由 + openclash 透明代理的方式不仅会为 Tailscale 添堵，而且会让机器对软路由的正常工作状态产生强依赖性，似乎也不太可取。</p>
<p>最后，我似乎已经找到目前来说最佳的替代方法：在境外机器上部署 dns 服务器作中继杜绝 <strong>dns 污染</strong>，以及 <strong>仅对内网地址开放 http 或 socks proxy</strong> 供境内服务器代理流量。我在 plamnet 中使用的是 AdGuard Home + v2ray 多机器负载均衡，能够非常稳定地提供基础的网络服务。</p>
<h2>使用自建外置 MariaDB Galera 集群为 k3s 提供 datastore</h2>
<p><a href="https://galeracluster.com/" rel="noreferrer" target="_blank">Galera</a> 是 Codership 公司为 Mysql 开发的 <strong>多主集群插件</strong>。<strong>MariaDB</strong> 在 10.4 版本后提供其官方支持，因此我便计划从 Mysql 迁移至 MariaDB。</p>
<p>通过网上资料的查找，国内使用 Galera 方案的人好像有点少啊。<br />
你也许会疑惑，etcd 作为 kubernetes 原生 datastore 方案，那为什么不直接在 k3s 中使用呢？<del>（当然是因为懒，学不动啦</del></p>
<p>因为对于我这种非专业运维来说，Mysql 已经用了很多年了，比 Etcd 熟悉得多，挂了能更迅速找到问题并及时解决；就算解决不了，Mysql 数据库的备份恢复易如反掌，能在较短时间恢复集群。<br />
其次，新搭好的这个 <strong>Mysql(MariaDB) 集群</strong> 还可以用来存其他生产应用的数据，为它们提供数据库 <strong>高可用+多地备份</strong> 的特性，而这正是我从中学时代就想要得到的方案。</p>
<p>由于 datastore 作为 k3s 集群的核心，这里特地标明几个要点，其关系着搭建的集群是否能稳定使用（而不是浪费大量没必要的时间来折腾 ⚠️ ：</p>
<ol>
<li>经过网上大量博客、笔记和论坛资料的调研，我得到的结论是 <strong>k3s + 由云服务商提供的 MySQL 服务</strong> 能够最有效的避免集群宕机。</li>
<li>Galera 集群搭建的主要目的为数据库备份，其次为高可用性。由于分布式数据库集群容易出现冲突的问题，严重的话可能会使整个数据库不可用，因此我的方案 <em>不是在所有 k3s master 节点上安装 MariaDB</em>，而是 <strong>选取几个网络延迟低、运行稳定的服务器安装 + 使用 TCP 负载均衡器做 Failover</strong> 来固定频繁读写的节点，从而尽量避免冲突问题的发生。</li>
<li>Galera 集群至少需要三个节点；若只有两个节点时需要额外的配置才能保证其中一个节点掉线时集群还能正常运行，因此这里以三个节点为例。</li>
</ol>
<h4>数据库迁移</h4>
<p>P.S. 这个环节非必须。</p>
<p>如果你跟我一样准备从 <strong>单节点 Mysql</strong> 迁移至 <strong>MariaDB Galera 集群</strong>，虽然很简单，但有几点提示可能会有些许帮助：</p>
<p>数据迁移前务必在 k3s 集群的 <strong>所有节点</strong> 上停止 <strong>k3s/k3s-agent</strong>。<br />
然后可以准备导出数据库了。我习惯在集群中使用 phpmyadmin，在浏览器中就能很方便地导出下载单个数据库至 sql 文件并导入。<br />
当然，如果你没有 phpmyadmin，也可直接使用 mysqldump 工具进行导出。</p>
<p>要注意的一点是，如果你是从 Mysql-5 迁移至 MariaDB-10 (对应 Mysql-8 的 API)，因为版本兼容问题，在集群安装配置完成后，数据库导入前需要打开导出文件作如下更改：</p>
<ul>
<li><code>utf8mb4_0900_ai_ci</code> 替换为 <code>utf8_general_ci</code></li>
<li><code>utf8mb4</code> 替换为 <code>utf8</code></li>
</ul>
<h4>MariaDB 安装与集群配置</h4>
<p>以下的命令均在 root 权限下执行。</p>
<p>安装前确保本地 Mysql 服务器已关闭：</p>
<pre><code class="language-bash">systemctl stop mysql
systemctl disable mysql</code></pre>
<p>在所有节点上执行：</p>
<pre><code class="language-bash">apt install mariadb-server mariadb-client mariadb-backup galera-4 </code></pre>
<p>打开 <code>/etc/mysql/mariadb.conf.d/</code> 文件夹，根据下面叙述仿照着更改 <code>60-galera.cnf</code> 配置文件：</p>
<pre><code>[galera]
# Mandatory settings
wsrep_on                 = ON
wsrep_provider           = /usr/lib/galera/libgalera_smm.so
wsrep_cluster_name       = "carton.plam"
wsrep_cluster_address    = gcomm://100.120.32.65,100.120.3.9,100.120.16.65
binlog_format            = row
default_storage_engine   = InnoDB
innodb_autoinc_lock_mode = 2

# Allow server to accept connections on all interfaces.
bind-address = 100.120.32.65

# Optional settings
#wsrep_slave_threads = 1
#innodb_flush_log_at_trx_commit = 0
wsrep_node_name          = coord.plam.toay
wsrep_node_address       = 100.120.32.65</code></pre>
<p>简要描述几个重要的：</p>
<ul>
<li><strong>wsrep_provider</strong> wsrep 为 Galera 4 核心组件，确保这个 provider 动态链接库存在</li>
<li><strong>wsrep_cluster_address</strong> 在这里补充上 <strong>MariaDB Galera 集群</strong> 中所有节点的 Tailnet IP 地址</li>
<li><strong>bind-address</strong> 仅在 Tailnet 地址监听连接</li>
</ul>
<p>所有节点均配置完成存盘退出后，在其中一个节点上执行 <code>galera_new_cluster</code> 来初始化集群。<br />
MariaDB 会自动启动，等待命令成功退出后可 <code>mysql -u root</code> 通过 SQL 查询状态来检查集群节点数量：</p>
<pre><code class="language-mysql">MariaDB [(none)]&gt; SHOW STATUS LIKE 'wsrep_cluster_size';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| wsrep_cluster_size | 1     |
+--------------------+-------+
1 row in set (0.001 sec)</code></pre>
<p>集群节点数为 1，表明集群已创建成功，且当前节点已成功加入。<br />
此时再在其他节点启动 MariaDB 即可自动加入集群：</p>
<pre><code>systemctl enable mariadb
systemctl restart mariadb</code></pre>
<p>然后再次通过 SQL 查询状态，可观察到与节点数相同的正确集群大小。至此，datastore 已为 k3s 准备完成。</p>
<h2>部署 k3s 至集群</h2>
<p>配置好网络和 datastore 后，可以正式开始 k3s 的安装了。<br />
由于 datastore 只是从 Mysql 切换至同类的 MariaDB，故 k3s 的安装配置与上篇文章几乎完全一致，故这里不再给出详细安装流程。</p>
<p>需要留意的一点是，现在集群使用 Tailscale 作为其网络基础，因此需要在 <strong>配置并初始化集群中所有的节点后</strong> 在每个节点 <strong>（包括 k3s 和 k3s-agent）</strong> 的启动参数中添加一项 <code>--flannel-iface tailscale0</code> 来指定 flannel 的接口。<br />
初始化集群前不要添加这个参数，会导致 <code>Waiting for CRD helmchartconfigs.helm.cattle.io to become available</code> 从而无法正常初始化集群。</p>
<h4>连接集群至 Rancher 实现远程可视化管理</h4>
<p>P.S. 同样地，这个环节非必须。</p>
<p>如果你和我一样习惯使用 Rancher 来管理 kubernetes 集群，可以从 Rancher Web 界面添加导入集群，然后复制粘贴命令到 control-plane 节点终端中执行，然后回到 Rancher 等待集群状态 Ready。</p>
<p>在此过程中可通过 <code>sudo kubectl get pod -n cattle-system</code> 命令查看 <strong>cattle cluster agent</strong> 的状态。若 pod 反复重启，然后在日志中遇到 pod 内 dns 不能正常解析的错误，可以尝试修改 deployment 的 <code>dnsPolicy</code> 为 <code>ClusterFirst</code>：</p>
<p>修改前：</p>
<pre><code>NAME                                    READY   STATUS    RESTARTS      AGE
cattle-cluster-agent-569cc46d9c-66pdb   1/1     Running   0             83s
cattle-cluster-agent-569cc46d9c-pkljj   0/1     Error     2 (40s ago)   81s
rancher-webhook-5b5665c649-rsx9s        1/1     Running   0             4h30m</code></pre>
<p>修改后：</p>
<pre><code>NAME                                    READY   STATUS    RESTARTS   AGE
cattle-cluster-agent-57ccdb69b4-9q7bl   1/1     Running   0          13s
cattle-cluster-agent-57ccdb69b4-p882r   1/1     Running   0          15s
rancher-webhook-5b5665c649-rsx9s        1/1     Running   0          4h35m</code></pre>
<p>All Done.</p>
<p>等待几分钟，然后到 rancher 上查看集群状态，接下来就能进行应用和服务的部署啦。</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/build-extensible-cross-cloud-k3s-cluster-using-tailscale/efc6e40fb5-1762605720/rancher-nodes.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/build-extensible-cross-cloud-k3s-cluster-using-tailscale/efc6e40fb5-1762605720/rancher-nodes.png"></a></figure>
<p>你也可以尝试使一个节点掉线，观察 k3s 能否正常地将服务转移至其他节点，毕竟这是我们搭建集群想要实现的最主要功能。</p>
<p>例如，实验室每天晚上都会断电，因此我一般会将 worker 部署到位于实验室主机中的虚拟机节点（threadpool.plam.toay）上。</p>
<p>当断电时，k3s 会自动对节点进行污点标记（node.kubernetes.io/unreachable=:NoSchedule 和 node.kubernetes.io/unreachable=:NoExecute）：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/build-extensible-cross-cloud-k3s-cluster-using-tailscale/3bddf97379-1762605720/rancher-nodes-with-taints.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/build-extensible-cross-cloud-k3s-cluster-using-tailscale/3bddf97379-1762605720/rancher-nodes-with-taints.png"></a></figure>
<p>以及 pod 部署转移（threadpool.plam.toay =&gt; coordinator.plam.toay）：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/build-extensible-cross-cloud-k3s-cluster-using-tailscale/0d0af487ca-1762605720/rancher-deploy-transfer-1.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/build-extensible-cross-cloud-k3s-cluster-using-tailscale/0d0af487ca-1762605720/rancher-deploy-transfer-1.png"></a></figure>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/note/build-extensible-cross-cloud-k3s-cluster-using-tailscale/4c5c1e5201-1762605720/rancher-deploy-transfer-2.png"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/note/build-extensible-cross-cloud-k3s-cluster-using-tailscale/4c5c1e5201-1762605720/rancher-deploy-transfer-2.png"></a></figure>
<p>从而保证了服务的高可用。</p>
<p>关于后续对 k3s 集群维护的相关内容，我特意新建了一个专栏 <a href="https://zyi.io/zh/search?tag=k3s-operation" rel="noreferrer" target="_blank">k3s 集群维护笔记</a> 来记录，欢迎关注😊。</p>
<h2>Bonus</h2>
<p>最后，Tailscale 实现了一些非常好用的便捷功能，这里简单做个摘要：</p>
<h4>Taildrop</h4>
<p>使用 Taildrop 可以非常方便地实现 Tailnet 中文件的传递，从此可以不再使用 SFTP 咯😋。</p>
<p>从 Linux 端发送：</p>
<pre><code class="language-bash">tailscale file cp &lt;src-file-loc&gt; &lt;dst-device&gt;:</code></pre>
<p>然后从 Linux 端接收：</p>
<pre><code class="language-bash">tailscale file get &lt;dst-dir&gt;</code></pre>
<h4>Subnet Router</h4>
<p>免费版仅有的一个 Subnet Router 也可以利用起来，通过一行命令实现从你的 Tailnet 访问 LAN 网中的所有设备：</p>
<pre><code class="language-bash">sudo tailscale up advertise-routes=10.24.1.1/24</code></pre>
<h4>使用 Exit Node</h4>
<p>可以使用 Exit Node 来代理全部流量，实现一键接入完整互联网的功能：</p>
<p>Server 节点：</p>
<pre><code class="language-bash">sudo tailscale up --advertise-exit-node</code></pre>
<p>Client 节点：</p>
<pre><code class="language-bash">sudo tailscale up --exit-node=&lt;exit-node-ip&gt;</code></pre>
<p>目前 Tailscale 貌似还没打算实现复杂的分流功能😭。<br />
若要跳过 <strong>局域网流量（仅 LAN 而已）</strong> 的代理，可添加 <code>--exit-node-allow-lan-access=true</code> 参数来实现。</p>
<h2>一些非常值得参考的链接</h2>
<ol>
<li><a href="https://tailscale.com/blog/2021-05-life-of-a-packet" rel="noreferrer" target="_blank">The long wondrous life of a Tailscale packet</a></li>
<li><a href="https://just4coding.com/2021/11/03/flannel/" rel="noreferrer" target="_blank">Kubernetes flannel网络分析</a></li>
<li><a href="https://github.com/rancher/rancher/issues/16454#issuecomment-786725616" rel="noreferrer" target="_blank">cattle-cluster-agent pod内不能解析 解决方法 - Github issue</a></li>
<li><a href="https://mp.weixin.qq.com/s/woXtO9u-YOqpNnBtQb5tqg" rel="noreferrer" target="_blank">如何设置一个生产级别的高可用etcd集群</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/404402933" rel="noreferrer" target="_blank">WireGuard到底好在哪？</a></li>
</ol>]]>

            </description>
        </item>
        <item>
            <title>为 Sony Walkman 升级国际版固件</title>
            <link>https://zyi.io/zh/corpus/chronicle/flashing-intl-ver-firmware-for-sony-walkman</link>
            <guid isPermaLink="false">corpus/chronicle/flashing-intl-ver-firmware-for-sony-walkman</guid>
            <pubDate>Tue, 26 Mar 2024 00:00:00 +0000</pubDate>
            <description>

            <![CDATA[<h4>前言</h4>
<p>因为要使用到 <strong>Apple Music/YouTube Music</strong> 流媒体应用需要 Google Play 框架，所以要给国行版播放器刷上国际版的系统。<br />
之前在 YouTube 上的一个视频（Channel @杰哥Jason_Vlog）了解到给 NW-ZX505 国行版本刷国际版系统的方法，成功地通过下载系统更新固件包，使用系统自带的“系统更新”应用我的 NW-A105 上升级为国际版系统，后面也在对下载地址作了小调整后也成功地升级了我的 NW-Z706。今天发现系统更新到版本2.0了看看有什么优化没有，但不知道为什么，原视频好像随频道一起不见了，于是在这里记录一下以方便后面有需要的人找到正确的方法。</p>
<h4>安装包下载</h4>
<p>首先是找到正确的下载地址得到响应的系统更新包：先通过 <a href="https://www.sony-asia.com/electronics/support/digital-music-players-nw-nwz-zx-series/nw-zx707/downloads" rel="noreferrer" target="_blank">Sony Support 官网</a> 找到最新的固件版本号，然后通过下面的链接拼接规则得到（猜到）正确的下载地址。</p>
<p>对于 NW-Z500 和 NW-A100 系列播放器：</p>
<pre><code>https://info.update.sony.net/PA001/&lt;NW-Z500 或 NW-A100&gt;Series_&lt;地区代码&gt;/contents/&lt;版本自增序号&gt;/NW-A100_&lt;地区代码&gt;_V&lt;5位版本编号&gt;_NW_WM_FW.UPG</code></pre>
<p>如 V4.06.00(V4_06_00)、日版(0000)系统的下载地址：</p>
<pre><code>https://info.update.sony.net/PA001/NW-A100Series_0000/contents/0015/NW-A100_0000_V4_06_00_NW_WM_FW.UPG</code></pre>
<p>对于 NW-ZX700 系列播放器，升级包的名称有了小变动（增加了 <strong>Series</strong> 字符串）：</p>
<pre><code>https://info.update.sony.net/PA001/NW-ZX700Series_&lt;地区代码&gt;/contents/&lt;版本自增序号&gt;/NW-ZX700Series_&lt;地区代码&gt;_V&lt;5位版本编号&gt;_NW_WM_FW.UPG</code></pre>
<p>如 V2.01.00(V2_01_00)、日版(0000)系统的下载地址：</p>
<pre><code>https://info.update.sony.net/PA001/NW-ZX700Series_0000/contents/0004/NW-ZX700Series_0000_V2_01_00_NW_WM_FW.UPG</code></pre>
<p>上面这个规则是 YouTube 频道 @杰哥Jason_Vlog 发现的，大概是通过官方升级时抓包改 URI 猜出来的😊。<br />
对于黑砖金砖来说规律应该是差不多的，但我没有就没尝试过，可以自己试一下😋。</p>
<p>P.S. 这里分享一个Dropbox链接，里面有我使用过后备份的一些版本的日区固件升级安装包：<br />
<a href="https://www.dropbox.com/scl/fo/lcfe8sl08ugqjhm4aezsl/AJHT0Ng98jDTiGxuRacZsjI?rlkey=u31kb5fo1atynyndmu8tat7po&amp;st=lrmlw78b&amp;dl=0">https://www.dropbox.com/scl/fo/lcfe8sl08ugqjhm4aezsl/AJHT0Ng98jDTiGxuRacZsjI?rlkey=u31kb5fo1atynyndmu8tat7po&amp;st=lrmlw78b&amp;dl=0</a></p>
<h4>使用“系统更新”应用升级</h4>
<p>将下载好的安装包放在 Android 系统根目录下，然后进入 <strong>设置-&gt;系统-&gt;系统更新</strong>，检测到根目录下的安装包后点击 <strong>开始更新</strong> 即可。</p>
<p>一个需要注意的是：若是新机首次使用，建议在更新完最新版本系统，即成功从国行系统升级至国际版系统后，再进行一次 <strong>恢复出厂设置</strong> 的操作，以清除可能造成冲突的国行版系统残余的系统组件（例如我的 NWZ706 在升级成国际版后 Gboard 始终闪退，最终在恢复出厂设置后得以解决）：<strong>设置-&gt;系统-&gt;重置选项-&gt;清除所有数据（恢复出厂设置）</strong>；需要注意的是，刷成国际版后恢复出厂设置时的开机初始化需要通过 WiFi 设置代理服务器或软路由的方式配置网络以登录 Google 账号。</p>
<p>另外，将播放器刷成国际版后，日后所有的更新均需要通过上述的方式手动下载正确的更新包进行升级，<strong>不可使用自带的“系统更新”应用</strong>，否则又会给更新回国行版系统（据说是通过序列号来判断区域的，这个貌似改不了咯）。</p>]]>

            </description>
        </item>
        <item>
            <title>Ubuntu 常用磁盘操作</title>
            <link>https://zyi.io/zh/corpus/note/ubuntu-common-disk-operations</link>
            <guid isPermaLink="false">corpus/note/ubuntu-common-disk-operations</guid>
            <pubDate>Tue, 05 Mar 2024 00:00:00 +0000</pubDate>
            <description>

            <![CDATA[<h2>初始化新磁盘</h2>
<p>先使用 <code>fdisk -l</code> 命令查看所有检测到的磁盘与分区，找到新添加磁盘的路径：</p>
<pre><code>Disk /dev/sdb: 256 GiB, 274877906944 bytes, 536870912 sectors
Disk model: QEMU HARDDISK
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes</code></pre>
<p>对新添加的磁盘进行分区：</p>
<pre><code> ~  sudo fdisk /dev/sdb                                                                                                                                         

Welcome to fdisk (util-linux 2.37.2).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Device does not contain a recognized partition table.
Created a new DOS disklabel with disk identifier 0x08c3917d.

Command (m for help): n
Partition type
   p   primary (0 primary, 0 extended, 4 free)
   e   extended (container for logical partitions)
Select (default p): p
Partition number (1-4, default 1): 1
First sector (2048-268435455, default 2048):
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-268435455, default 268435455):

Created a new partition 1 of type 'Linux' and of size 128 GiB.

Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.</code></pre>
<p>再次查看所有磁盘与分区：</p>
<pre><code>Disk /dev/sdb: 128 GiB, 137438953472 bytes, 268435456 sectors
Disk model: QEMU HARDDISK
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x08c3917d

Device     Boot Start       End   Sectors  Size Id Type
/dev/sdb1        2048 268435455 268433408  128G 83 Linux</code></pre>
<p>格式化分区：</p>
<pre><code>~  sudo mkfs -t ext4 /dev/sdb1
mke2fs 1.46.5 (30-Dec-2021)
Discarding device blocks: done
Creating filesystem with 8388352 4k blocks and 2097152 inodes
Filesystem UUID: 9eb32519-8f2b-4296-be89-c0e7589410b1
Superblock backups stored on blocks:
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
        4096000, 7962624

Allocating group tables: done
Writing inode tables: done
Creating journal (32768 blocks): done
Writing superblocks and filesystem accounting information: done</code></pre>
<p>挂载：</p>
<pre><code>sudo mkdir /data
sudo mount /dev/sdb1 /data</code></pre>
<p>查看空间占用：</p>
<pre><code> ~  sudo df -Th                                                                                                                                                 
Filesystem                        Type   Size  Used Avail Use% Mounted on
tmpfs                             tmpfs  197M  1.2M  196M   1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv ext4    61G  5.9G   53G  11% /
tmpfs                             tmpfs  982M     0  982M   0% /dev/shm
tmpfs                             tmpfs  5.0M     0  5.0M   0% /run/lock
/dev/sda2                         ext4   2.0G  251M  1.6G  14% /boot
tmpfs                             tmpfs  197M  4.0K  197M   1% /run/user/1000
/dev/sdb1                         ext4   126G   24K  120G   1% /data</code></pre>
<p>添加至 <code>/etc/fstab</code> 以实现开机自动挂载：<br />
使用 <code>blkid</code> 或 <code>ls -l /dev/disk/by-uuid/</code> 命令得到分区的 <strong>uuid</strong>，然后向 <code>/etc/fstab</code> 文件末尾追加一行：</p>
<pre><code>UUID=10fd13d0-5d0f-42cc-9c14-331858bbcf01 /data ext4 defaults 0 2</code></pre>
<p>最后重启系统。</p><h2>附加磁盘扩容</h2>
<p>使用 <code>df -Th</code> 查看当前磁盘分区容量、挂载信息：</p>
<pre><code>/dev/sdc1                         ext4     126G   90G   30G  76% /data</code></pre>
<p>首先通过 hypervisor （这里我使用的是 pve，其他虚拟机软件同理）直接配置扩展磁盘大小。pve 可以方便地进行在线容量扩展，不需要进行关机或重新挂载磁盘的操作。</p>
<p>进入系统，输入 <code>fdisk -l</code> 查看所有磁盘及分区，可以观察到硬盘有分区表大小不匹配的警告：</p>
<pre><code>GPT PMBR size mismatch (268435455 != 536870911) will be corrected by write.
The backup GPT table is not on the end of the device.</code></pre>
<p>先使用 <code>parted -l</code> 解决分区表不匹配问题：</p>
<pre><code>Warning: Not all of the space available to /dev/sdc appears to be used, you can
fix the GPT to use all of the space (an extra 268435456 blocks) or continue with
the current setting?
Fix/Ignore? Fix
Model: QEMU QEMU HARDDISK (scsi)
Disk /dev/sdc: 275GB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags:

Number  Start   End    Size   File system  Name  Flags
 1      1049kB  137GB  137GB  ext4</code></pre>
<p>接下来的操作就很简单了，直接使用 <code>cfdisk /dev/sdc</code> 命令进行 resize，然后使用 <code>resize2fs /dev/sdc1</code> 命令同步系统容量到内核。</p>
<p>再次使用 <code>df -Th</code> 查看当前磁盘分区容量：</p>
<pre><code> /dev/sdc1                         ext4     252G   90G  151G  38% /data</code></pre>
<p>磁盘的容量已成功扩大并应用。</p><h2>主分区磁盘扩容</h2>
<p>类似地，先使用 <code>cfdisk</code> resize 磁盘并更新分区表。</p>
<p>然后，若直接 <code>resize2fs</code>:</p>
<pre><code> ~  sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv                                                                                                     
resize2fs 1.46.5 (30-Dec-2021)
The filesystem is already 7863296 (4k) blocks long.  Nothing to do!</code></pre>
<p>先使用 <code>pvresize</code> 命令 resize，再使用 <code>lvresize</code> 扩大文件系统：</p>
<pre><code> ~  sudo pvresize /dev/sda3                                                                                                                                     
  Physical volume "/dev/sda3" changed
  1 physical volume(s) resized or updated / 0 physical volume(s) not resized
 ~  sudo lvresize -l  +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv                                                                                               
  Size of logical volume ubuntu-vg/ubuntu-lv changed from &lt;30.00 GiB (7679 extents) to &lt;62.00 GiB (15871 extents).
  Logical volume ubuntu-vg/ubuntu-lv successfully resized.</code></pre>
<p>再次使用 <code>fdisk -l</code> 命令查看，则观察到分区由：</p>
<pre><code>Disk /dev/mapper/ubuntu--vg-ubuntu--lv: 62 GiB, 66567798784 bytes, 130015232 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes</code></pre>
<p>变为：</p>
<pre><code>Disk /dev/mapper/ubuntu--vg-ubuntu--lv: 254 GiB, 272726228992 bytes, 532668416 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes</code></pre>
<p>最后使用 <code>resize2fs</code> 应用更改：</p>
<pre><code> ~  sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv                                                                                                            
resize2fs 1.46.5 (30-Dec-2021)
Filesystem at /dev/mapper/ubuntu--vg-ubuntu--lv is mounted on /; on-line resizing required
old_desc_blocks = 4, new_desc_blocks = 8
The filesystem on /dev/mapper/ubuntu--vg-ubuntu--lv is now 16251904 (4k) blocks long.</code></pre>
<p>=&gt;</p>
<pre><code> ~  sudo df -Th                                                                                                                                                 ok | twikor@sodium
Filesystem                        Type   Size  Used Avail Use% Mounted on
tmpfs                             tmpfs  197M  1.2M  196M   1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv ext4    61G  5.9G   53G  11% /
tmpfs                             tmpfs  982M     0  982M   0% /dev/shm
tmpfs                             tmpfs  5.0M     0  5.0M   0% /run/lock
/dev/sda2                         ext4   2.0G  251M  1.6G  14% /boot
tmpfs                             tmpfs  197M  4.0K  197M   1% /run/user/1000</code></pre><h4>参考链接</h4>
<ol>
<li><a href="https://www.reddit.com/r/Proxmox/comments/12ubr84/ive_increased_a_vms_partition_size_but_the_lvm/">https://www.reddit.com/r/Proxmox/comments/12ubr84/ive_increased_a_vms_partition_size_but_the_lvm/</a></li>
<li><a href="http://linux.51yip.com/">http://linux.51yip.com/</a></li>
</ol>]]>

            </description>
        </item>
        <item>
            <title>峨眉雪景图鉴</title>
            <link>https://zyi.io/zh/corpus/chronicle/mount-emei-gallery</link>
            <guid isPermaLink="false">corpus/chronicle/mount-emei-gallery</guid>
            <pubDate>Tue, 14 Mar 2023 00:00:00 +0000</pubDate>
            <description>

            <![CDATA[<link rel="stylesheet" href="https://zyi.io/assets/plugins/pannellum/pannellum.css"/>
<script type="text/javascript" src="https://zyi.io/assets/plugins/pannellum/pannellum.js"></script>
<p>本文永久短链接：<a href="https://zyi.io/143-1" rel="noreferrer" target="_blank">zyi.io/143-1</a><br />
经过了3年疫情的阻拦，2023/03/11，143终于策划了第一次的集体活动。</p>
<h2>雷洞坪</h2>
<div id="panorama_dji_fly_pano_leidongping" style="height: 600px;"></div>
<script>
pannellum.viewer('panorama_dji_fly_pano_leidongping', {
    "type": "equirectangular",
    "panorama": "https://zyi.io/corpus/chronicle/mount-emei-gallery/dji_fly_pano_leidongping.jpg",
    "preview": "https://zyi.io/corpus/chronicle/mount-emei-gallery/dji_fly_pano_leidongping_preview.png",
    "autoLoad": false,
    "showZoomCtrl": true,
    "mouseZoom": "fullscreenonly"
});
</script>
<h2>金顶</h2>
<h4>缆车上的日出</h4>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/chronicle/mount-emei-gallery/dfcd95fe94-1762605723/img_3287_on_cableway_to_jinding.jpeg"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/chronicle/mount-emei-gallery/dfcd95fe94-1762605723/img_3287_on_cableway_to_jinding.jpeg"></a></figure>
<h4>明与暗的境界线</h4>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/chronicle/mount-emei-gallery/34f536ce4c-1762605723/dji_fly_photo_jinding_center.jpg"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/chronicle/mount-emei-gallery/34f536ce4c-1762605723/dji_fly_photo_jinding_center.jpg"></a></figure>
<h4>寺院屋檐上柔滑的雪</h4>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/chronicle/mount-emei-gallery/c9958bcfac-1762605723/dji_fly_photo_jinding_silky_snow_over_the_roof.jpg"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/chronicle/mount-emei-gallery/c9958bcfac-1762605723/dji_fly_photo_jinding_silky_snow_over_the_roof.jpg"></a></figure>
<h4>金顶全景</h4>
<div id="panorama_dji_fly_pano_jinding" style="height: 600px;"></div>
<script>
pannellum.viewer('panorama_dji_fly_pano_jinding', {
    "type": "equirectangular",
    "panorama": "https://zyi.io/corpus/chronicle/mount-emei-gallery/dji_fly_pano_jinding.jpg",
    "preview": "https://zyi.io/corpus/chronicle/mount-emei-gallery/dji_fly_pano_jinding_preview.jpeg",
    "autoLoad": false,
    "showZoomCtrl": true,
    "mouseZoom": "fullscreenonly"
});
</script>
<h4>金顶中心全景</h4>
<div id="panorama_dji_fly_pano_jinding_center" style="height: 600px;"></div>
<script>
pannellum.viewer('panorama_dji_fly_pano_jinding_center', {
    "type": "equirectangular",
    "panorama": "https://zyi.io/corpus/chronicle/mount-emei-gallery/dji_fly_pano_jinding_center.jpg",
    "preview": "https://zyi.io/corpus/chronicle/mount-emei-gallery/dji_fly_pano_jinding_center_preview.jpeg",
    "autoLoad": false,
    "showZoomCtrl": true,
    "mouseZoom": "fullscreenonly"
});
</script>
<h4>金顶索道全景</h4>
<div id="panorama_dji_fly_pano_jinding_cableway_station" style="height: 600px;"></div>
<script>
pannellum.viewer('panorama_dji_fly_pano_jinding_cableway_station', {
    "type": "equirectangular",
    "panorama": "https://zyi.io/corpus/chronicle/mount-emei-gallery/dji_fly_pano_jinding_cableway_station.jpg",
    "preview": "https://zyi.io/corpus/chronicle/mount-emei-gallery/dji_fly_pano_jinding_cableway_station_preview.jpeg",
    "autoLoad": false,
    "showZoomCtrl": true,
    "mouseZoom": "fullscreenonly"
});
</script>
<p>未完待续。。。</p>]]>

            </description>
        </item>
        <item>
            <title>开源 手柄套件 BunoStick</title>
            <link>https://zyi.io/zh/corpus/project/bunostick</link>
            <guid isPermaLink="false">corpus/project/bunostick</guid>
            <pubDate>Sun, 14 Aug 2022 00:00:00 +0000</pubDate>
            <description>

            <![CDATA[<p>BunoStick 这个名字是由 Bluetooth + Arduino + JoyStick 合起来的。<br />
明明是 Arduino，为什么最终选择使用 STM32 做主控呢？<del>还不是因为懒，这个项目从刚开始学 Arduino 时就在准备了，结果硬是拖到32都结束了才想起来还有这个项目一直鸽着。</del></p>
<p>先来张图展示一下最终产品吧：</p>
<figure><a class="fancybox" href="https://zyi.io/media/pages/corpus/project/bunostick/c7e4b2ecd3-1706951566/bunostick.jpeg"><img alt="" class="aligncenter" src="https://zyi.io/media/pages/corpus/project/bunostick/c7e4b2ecd3-1706951566/bunostick.jpeg"></a></figure>
<p>在嘉立创上打的板，分上下两层，板之间用排针连接。上层板放欧姆龙的大按键，并将调试线引出。下层板放电池和稳压芯片、OLED、十轴陀螺仪、摇杆、蓝牙模块和 STM32 核心板。<br />
顺便一提，摇杆也有两种：一种是方形限位的摇杆，一般用来做航模遥控的油门；一种是圆形限位的，游戏机用的大多都是这种摇杆。淘宝上搜 XBox 手柄替换摇杆就可搜出一大堆，便宜而且手感非常好，不知比 Switch Joy-Con 上的摇杆舒服到哪里去。</p>
<p>你可能要问了，为什么不直接搞个 F103 上去？其实最开始做这个项目的时候还留有了小游戏机的需求的，一直想要复刻几个远古时期的老游戏练练手，就直接上的F411的芯片。而最近 STM32F411 的芯片价格炒上天了，立创商城上每片要卖一两百，这怎么承担得起。。。<del>（还不是因为没钱）</del>要知道这块 WeAct 工作室的核心板才 42 左右（现在已经绝版了，官方淘宝只有国产 GD32 的核心板了，幸好我还留有一块全新的做备用😊）。</p>
<p>不过做成模块化的话也有个好处，这个核心板的引脚排布与 Bluepill 的排布大致相同；若要考虑直接使用 Bluepill 做主控的话直接替换也行。</p>]]>

            </description>
        </item>
    </channel>
</rss>
