标签归档:linux

解决 Linux Wayland 环境下 Joplin 使用 fcitx5 中文输入漏字问题

在 Linux + Wayland 环境下使用 Joplin 时,如果配合 fcitx5 输入中文,可能会遇到输入漏字的问题。

下面这份配置可作为参考:

➜  Downloads cat /usr/share/applications/joplin-desktop.desktop
[Desktop Entry]
Name=Joplin
Comment=Joplin for Desktop
-Exec=/usr/bin/joplin-desktop --gtk-version=3 --ozone-platform=x11
+Exec=/usr/bin/joplin-desktop --gtk-version=3 --ozone-platform-hint=auto --enable-wayland-ime
Terminal=false
Icon=joplin-desktop
StartupWMClass=@joplin/app-desktop
Type=Application
Categories=Office;
MimeType=x-scheme-handler/joplin;
SingleMainWindow=true

使用 --ozone-platform=x11 可以解决 Joplin 全局菜单不显示的问题,但中文输入时可能会出现漏字。

改用 --ozone-platform-hint=auto --enable-wayland-ime 后,可以解决 fcitx5 中文输入漏字的问题;不过相应地,Joplin 全局菜单可能又无法正常显示。

实测二者不可两全,暂无找到很好的方法。建议先采用第一行配置,显示出来菜单做好配置后,改用第二行配置,确保后期中文输入体现。

输入漏字真的很恼火,如有两全解决方案欢迎讨论。

Refs

【翻译】这篇博客在 Ubuntu 16.04 上跑了 10 年,我把它迁移到了 FreeBSD

前言

文章来源:CrociDB

原文作者:CrociDB(Antonio Vivace)

原文标题:This blog ran on Ubuntu 16.04 for 10 years. I migrated it to FreeBSD

原文发布时间:2026-05-21

原文链接:https://crocidb.com/post/this-blog-ran-on-ubuntu-16-04-for-10-years-i-migrated-it-to-freebsd/

本文为全文翻译,译文由 AI 辅助整理,若有理解偏差欢迎指正。


这篇博客已经在一台 DigitalOcean VPS 上运行了十多年。那是一台托管在纽约、运行着 Ubuntu 16.04 LTS 的机器。而这个 LTS 版本至少已经有 5 年不再受支持了。是时候做些改变了。经过一番权衡,我把它迁移到了一台 Hetzner 的虚拟机上:配置比我旧的 Ubuntu 机器强得多,价格还不到以前的一半,而且机房就在我所在国家的另一端。不仅如此,我还顺便挑战了一下自己,把整套栈迁到了 FreeBSD。这篇文章会有点长,但如果你愿意读下去,会看到一个关于 FreeBSD JailsBastille,以及一些有意思的网站负载测试结果的故事。

动机

如果你了解 Ubuntu 的发布和支持周期(我自己其实也不算熟),就会知道一旦某个版本结束支持,apt 对应的软件仓库也就没了,你再也拿不到更新。运行这样一套过时系统会带来不少问题,其中最显而易见的一点就是:你的服务器不再那么安全了。互联网上肯定有各种机器人,专门扫描存在漏洞的节点,然后想办法塞进去恶意内容。幸运的是(我想大概算幸运),我这里一直没出过事。反正机器里也没什么特别重要的东西可偷。但我记得很久以前,我还有一个 WordPress 博客,也是跑在一台老旧 VPS 上,结果某天文章正文里突然被插满了可疑的赌场和赌博链接。

我原本就有一台 Hetzner VPS,当作远程开发机来用,可以随时 SSH 上去,考虑到价格,它一直都很可靠。所以我决定先从对比配置开始。下面这台就是原来跑我的博客和其它网站的 droplet:

(原文此处有旧 DigitalOcean 服务器截图)

它有 2GB 内存、1 个 vCPU、50GB 磁盘、每月 2TB 流量,运行的是 Ubuntu 16.04 x64。机房位于纽约,这大概也是它比较贵的原因:我每个月要为它支付 13 美元。

Hetzner 是一家老牌欧洲 IT 公司,在德国有大型数据中心,而我也住在德国。它最便宜的一款服务器,只要 3.56 欧元,配置就已经比我原来那台更好了:

(原文此处有 Hetzner 入门套餐截图)

内存和 CPU 都翻倍,存储稍微少一点,但月流量多了十倍。不过我最后还是选了一台更“豪华”的配置,每月不到 6 欧元:

(原文此处有更高配套餐截图)

对我的这些站点来说,这配置甚至有点过头了,不过为什么不呢?

旧的部署方式

我那套旧环境托管的不只是这个博客,还有几个别的网站。它们都不算热门;在所有站点里,这个博客流量算最高的了,但每个月也不过几千次页面浏览。除了偶尔有几篇文章在 Hacker News 上爆一下,平时根本没多少流量。说到底,这台机器基本就是在提供静态网站服务,没有复杂 CGI,也没有什么自定义后端代码在跑。整套栈很简单:所有内容都通过 nginx/1.10.3 以静态方式提供。我基本上就是在 /etc/nginx/sites-available 里给每个站点放一份配置文件。像静态站点生成器、LaTeX 套件之类的附加程序(比如这个博客就是用 Hugo 生成的)则通过 aptsnap 安装。事实上,我更新博客的流程一直是这样:

  • 在本地写文章
  • 提交并推送到仓库
  • SSH 登录服务器
  • 拉取仓库更新
  • 运行 hugo

这台 VPS 刚开始用的那些年里,我还会拿它做一些测试和编程实验。所以机器里积累了很多已经不再使用的老旧软件,显得很臃肿。但它就是能跑,而且跑得还挺好。

(原文此处有 Linux 4.4 截图)

它甚至好到什么程度呢?我关机前,它的 uptime 达到了 1491 天,也就是差不多连续 4 年没中断过。

为什么选 FreeBSD

说实话,我的主要动机之一就是想亲手折腾点不一样的东西。我最近读了很多、也看了很多关于 BSD 家族的内容,而我之前也曾短暂用过 FreeBSD,所以我觉得这是一个把它放到真实生产场景里试一试的好机会。FreeBSD 常常因为它的一体化设计、安全性以及 Jails 而被人称赞。后面我会详细展开。

我不想装得好像自己一开始就完全知道该怎么做,但当我读到 Jails 时,我立刻就知道自己想怎么搭了。Jails 是一种虚拟化/容器化机制,已经存在于 FreeBSD 里超过 25 年了,比 Docker 诞生还早得多。乍一看,它实现的效果和你对 Docker 容器的预期很像:在系统里面隔离出一个“小系统”,你可以在里面运行一些拿不到宿主机权限的东西。但在我看来,Docker 以及其它容器方案更适合“打包程序”——它们强调短生命周期和不可变;容器内处理的数据来自外部,但容器内部本身最好始终保持一致。而 Jails 更像真正的子系统,几乎可以把它看作迷你虚拟机,只不过底层仍然共享同一个内核。

除此之外,FreeBSD 的文件系统 ZFS 对服务器场景也确实非常好用。如果你来自 Linux 世界,大概听说过 Btrfs,它是近几年越来越多发行版开始采用的新文件系统。它和 ZFS 有一些相似点,比如数据完整性和快照。不过 ZFS 大概比 Btrfs 要成熟得多。如果我能频繁地给系统做快照,就不需要依赖 VPS 提供商的快照或备份服务,而那些通常都要额外付费。

我的想法是:每个网站一个 Jail,里面装上构建这个网站所需的工具(比如博客需要 Hugo),再在 Jail 里运行一个 nginx 实例来提供服务;另外再单独放一个主 Web 服务器 Jail,通过反向代理把外部世界和这些站点连起来。这样一来,哪怕某个 Jail 被攻破了,我也可以把它直接销毁,再建一个新的。后面我会更具体地介绍配置过程。

Hetzner VPS

先给所有喜欢看系统信息的人一点细节:

(原文此处有 fastfetch 截图)

搭建过程

我会快速讲一下自己是如何把这台服务器搭起来的,以及其中最关键的几个点。平时我会记开发日志,所以这次也是边做边记。不过不要把这篇文章当作教程,因为我可能漏掉了一些步骤。更准确地说,这是一篇“在 Hetzner 上用 FreeBSD 搭 Web 服务器是什么感觉”的记录。

安装 FreeBSD

Hetzner 在创建虚拟机时会提供一些镜像,但选择其实非常有限:

(原文此处有 “No BSD” 截图)

不过我在 FreeBSD 官方 YouTube 频道 上看到了一份讲如何在 Hetzner 上安装 FreeBSD 的指南。顺带一提,这个频道非常推荐。

关键在于:Hetzner 其实提供了 FreeBSD 镜像,只是藏得比较深,需要多点几步。它是以 ISO 镜像的形式提供的。所以在创建虚拟机、选择操作系统镜像的时候,你先随便选一个,反正稍后都要把它抹掉。创建完虚拟机后,只需要去控制台里的 ISO Images 标签页,挂载你想要的镜像即可:

(原文此处有 FreeBSD ISO 镜像截图)

我选的是 14.3。然后重启,基本上就按安装器一路走下去。印象里我没有改什么默认选项,当时是跟着官方 FreeBSD 频道里的安装视频一步步做的。很快,系统就装好并跑起来了。

Bastille

前面我已经提了好几次 Jails。但我用的不只是 Jails,还用了 Bastille,这是一个用来管理 Jails 的系统。原因很简单:手动创建 Jail 这件事比我想象中复杂,而且每新建一个 Jail,都有一堆独立步骤需要自己处理。Bastille 把这些都简化了,很多事情只差一条 bastille 命令。比如 bastille list 可以列出系统里的所有 Jail,bastille create 用来创建新 Jail,bastille console 则可以直接打开某个 Jail 的 shell,等等。

安装并启用它(几乎)就像 官方文档 里说的这么简单:

pkg install bastille
sysrc bastille_enable="YES"

当然也有别的 Jail 管理器,我只是选了一个名字最酷的。

整体架构

整套思路是:先有一个运行 Caddy 的 Jail,负责对外提供所有站点,并处理域名和 SSL 证书;然后每个站点再单独有自己的 Jail,里面安装各自构建和服务所需要的软件。这个“服务器 Jail”会把所有流量反向代理到对应的站点 Jail。所以第一步就是配置一个内部虚拟网卡。你可以把这套结构理解成:一组跑在同一个宿主机里的小型虚拟机网络。

配置虚拟网络接口的方式如下:

sudo sysrc cloned_interfaces+="lo1"
sudo sysrc ifconfig_lo1_name="bastille0"
sudo service netif cloneup
sudo sysrc ifconfig_bastille0="inet 10.0.0.1 netmask 255.255.255.0"

这几条命令本质上就是克隆一个回环接口,把它命名为 bastille0,再给它分配网络参数。所有 Jail 只会跑在这张网络接口上。至于 Caddy 所在的 Jail,因为它需要监听外部请求,就必须具备访问外网的能力。为此,我们要用 PF(Packet Filter,也就是 FreeBSD 的 防火墙)配置一条出网规则。

我们需要编辑 /etc/pf.conf

# the main interface `vtnet0` and the virtual one we created `bastille0`
ext_if = "vtnet0"
int_if = "bastille0"
vpn_if = "tailscale1"

# skip internal traffic
set skip on $int_if
set skip on $vpn_if

# allow outbound access to the internet from the jails
nat on $ext_if from 10.0.0.0/24 to any -> ($ext_if)

# redirect HTTP/HTTPS traffic from the web to the server jail (10.0.0.5)
rdr pass on $ext_if proto tcp from any to any port {80, 443} -> 10.0.0.5

# block everything else
block all

# allows outbound traffic from host
pass out quick on $ext_if keep state

正如配置里的注释所写,这段规则主要做了几件事:把 bastille0tailscale1 上的内部流量跳过处理;允许 Jail 和宿主机出网;再把所有 80443(HTTP/HTTPS)流量重定向到内部 IP 10.0.0.5,也就是我们的 Caddy 服务器。

然后启用 PF:

sysrc pf_enable="YES"
service pf start
sysrc gateway_enable="YES"

至此,网络层的基础就完成了。接下来开始创建 Caddy 服务器 Jail。

创建第一个 Jail:Caddy Server

我的旧服务器一直在跑老朋友 nginx。除了早年短暂用过一点 Apache,我人生的大多数时间都在用 nginx。不过 nginx 有个烦人的点,而 Caddy 正好能漂亮地解决:SSL 证书。以前在旧服务器上,我得隔三差五跑一遍 certbot 去续域名证书,而且续签忘掉的次数比我愿意承认的要多。Caddy 会自动处理这些事情。

要创建一个新 Jail,先得为当前要使用的 FreeBSD 版本做 bootstrap,在我的场景里是 14.3-RELEASE

bastille bootstrap 14.3-RELEASE

然后创建真正的 Jail:

bastille create caddy 14.3-RELEASE 10.0.0.5 bastille0
bastille start caddy

这里我们创建了一个名为 caddy 的 Jail,使用 14.3-RELEASE,IP 为 10.0.0.5,挂在 bastille0 接口上。

检查 Jail 状态:

# bastille list
 JID  Name      Boot  Prio  State  Type   IP Address  Published Ports  Release       Tags
 3    caddy     on    99    Up     thin   10.0.0.5    -                14.3-RELEASE  -

这样它就已经跑起来了!要记住,Jails 并不是像 Docker 容器那样追求 ephemeral 的东西。你不会先把容器完全定义好,然后指望它永远都保持一模一样。这里的感觉更接近虚拟机。你随时都可以通过下面的命令进到它的 shell:

# bastille console caddy

[caddy]:
root@caddy:~ #

配置 Caddy Server

既然我们已经在 caddy 这个 Jail 里了,那只需要把 Caddy 装上:

pkg install caddy
sysrc caddy_enable="YES"
service caddy start

Caddy 的所有配置都放在 Jail 里的 /usr/local/etc/caddy/Caddyfile。如果我们想在宿主机上也能直接访问这份配置文件,就需要把宿主机上的一个目录挂载到 Jail 里。甚至还可以用只读模式挂载,这样 Jail 自己就无法修改配置。大概像这样:

bastille mount caddy /usr/local/etc/my-caddy-config /usr/local/etc/caddy nullfs ro 0 0

好了,服务器差不多配完了,但我们还没有一个真正的站点。所以先建一个站点 Jail,然后再回来配置 Caddy。

第一个站点:es.cro.to

疫情期间,巴西总统选择把 Covid-19 当作感冒来对待,没有采取任何紧急封锁政策。他不断强调国家不能停摆,承担不起代价,哪怕有人因此死掉也无所谓。这完全无视科学,甚至无视常识。历史已经证明他有多错。对此我很不爽,于是我想做一点“抗议”。

所以我做了一个小页面 es.cro.to。它在葡萄牙语里字面上是“阴囊”的意思,但更常被用来形容一个人是个 混蛋烂人,或者别的类似意思。我买下了 cro.to 这个域名(现在回头看,这域名其实挺酷的,英文里听起来也不错),然后加了一个 es 子域名。它刚好把这个词按字典式的音节拆开,于是我就在页面里放上了这个词的词义和对应生物的图片:

(原文此处有 escroto 页面截图)

它当然也是跑在我的旧服务器上的,而且在封锁那段时间还获得了不小的流量。我决定先从它开始迁,因为它本质上只是一个带着一张压得很惨照片的单页站点,而且现在也没那么重要了。

这个站点和我其它站一样,也是一个 Git 仓库。所以我准备把所有网站都放在宿主机的 /usr/local/www 下,然后把各个站点对应的目录以只读方式挂载到对应 Jail 里。于是,当我把这个站点放到 /usr/local/www/escroto 后,就开始用 Bastille 创建 Jail。这次我直接用了一个用于 nginx 的 bastille 模板:www/nginx。模板其实就是预装一些软件的小脚本。其实不用也行,但既然有,为什么不用呢?

bastille bootstrap https://github.com/bastillebsd/templates
bastille create escroto 14.3-RELEASE 10.0.0.11 bastille0
bastille template escroto www/nginx

我给它分配的 IP 是 10.0.0.11,等会会用到。此时,这个 Jail 里的 nginx 已经在提供默认页面了。它的站点目录位于 /usr/local/www/nginx(注意,FreeBSD 几乎什么都放在 /usr/local 下)。

接着,我把宿主机上的站点目录挂到 Jail 里:

bastille mount escroto /usr/local/www/escroto /usr/local/www/escroto nullfs ro 0 0

这样一来,我在 Jail 内就能访问 /usr/local/www/escroto 这个站点目录了。因为它是个 Git 仓库,里面带有 .git 之类不该被直接提供出去的文件,所以我写了一个 deploy.sh 脚本,把 /usr/local/www/escroto/* 复制到 /usr/local/www/nginx/*,再删掉 .git 目录:

rm -fr /usr/local/www/nginx/*
cp -R /usr/local/www/escroto/* /usr/local/www/nginx/
rm -fr /usr/local/www/nginx/.git

这样以后每次要部署这个站点的新版本,只需要登录宿主机,更新仓库,然后执行这个脚本:

cd /usr/local/www/escroto
git pull
bastille cmd escroto /root/deploy.sh

后来我发现这个思路挺好,于是也给其它站点都放了一份 /root/deploy.sh

让 Caddy 把域名指向这个 Jail

回到 Caddy 配置:

cro.to {
        redir https://es.cro.to{uri} permanent
}

es.cro.to {
        reverse_proxy 10.0.0.11
}

配置非常简单:主域名重定向到 es.cro.to,再由后者反向代理到 10.0.0.11,也就是 escroto Jail。

配置好 DNS 记录后,这个站点就上线了。

部署这个博客

这个博客本身是用 Hugo 写的,仓库放在 GitHub 上。所以我把它克隆到了 /usr/local/www/blog,然后像 escroto 一样创建了一个新的 Jail:

bastille create blog 14.3-RELEASE 10.0.0.12 bastille0
bastille template blog www/nginx
bastille mount blog /usr/local/www/blog /usr/local/www/blog nullfs ro 0 0

然后在这个 Jail 里安装 Hugo:

bastille pkg blog update
bastille pkg blog install gohugo

部署脚本也很类似:

rm -fr /usr/local/www/nginx/*
cd /usr/local/www/blog
hugo -d /usr/local/www/nginx

在正式把站点迁离旧服务器前,我想先做一些基准测试,所以先把博客绑定到了一个旧域名上:

crocidb.cro.to {
        reverse_proxy 10.0.0.12
}

就这么简单,博客已经在新服务器上跑起来了。

给我的服务器做基准测试

在正式更新 DNS 记录前,我想确认这台新服务器到底能不能扛住流量。理论上看它当然行,但万一我哪里配错了呢?于是我开始琢磨,怎么比较旧服务器上的 crocidb.com 和新服务器上的 crocidb.cro.to

一开始我先想清楚,到底该测什么。测延迟没什么意义,因为我知道,对大多数访问我博客的人来说,新服务器的延迟本来就会稍微更长一些。这个站点的大部分流量通常来自北美,其次是欧洲,再然后是南美。不过那点差距其实不至于影响体验。所以更值得测的是:服务能力和高负载表现。

市面上有一些免费的在线工具,可以做比较通用的测试,比如 GTMetrixPingdomWebPageTest。但老实说,这两台服务器在这些工具里的差异几乎只体现在延迟上,其它方面基本差不多。所以我决定往更深一点挖。

我发现了 wrkhey 这两个 HTTP 压测工具。它们本质上就是对网站发起成千上万的并发请求,然后收集诸如请求延迟、错误响应、每秒传输量等信息。比如,我在另一台 Hetzner VPS 上跑了下面这个 wrk 命令:

wrk -t4 -c100 -d30s --latency https://crocidb.com/

这表示让 wrk4 个线程、维持 100 个并发连接、持续压测 30 秒。这个数字其实也不算特别离谱。结果如下:

Running 30s test @ https://crocidb.com/
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    89.63ms   18.31ms 189.09ms   95.29%
    Req/Sec   211.74     38.44   313.00     87.69%
Requests/sec:    833.41
Transfer/sec:      8.29MB

而对 crocidb.cro.to 测出来是:

Running 30s test @ https://crocidb.cro.to/
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     6.75ms    3.59ms  51.98ms   74.41%
    Req/Sec     3.08k   260.11     4.34k    81.83%
Requests/sec:  12260.10
Transfer/sec:    130.80MB

旧服务器每秒能处理 833 个请求,而新服务器则达到 12,260。平均延迟则是 89ms6ms。但这当然不公平:压测机器和新服务器在同一个数据中心。所以我想,要不试试挂 VPN,从不同地区去测?

我用的是 Proton VPN,于是写了一个小脚本,从一组地点里选一个,连上 VPN,再跑 wrk。结果非常平淡:旧服务器平均大概 300 req/s,新服务器大概 800 req/s。测试地点如下:

LOCATIONS=(
    "US//New York|US - New York"
    "US//Los Angeles|US - Los Angeles"
    "BR//São Paulo|BR - São Paulo"
    "GB//London|GB - London"
    "DE//Frankfurt|DE - Frankfurt"
    "FR//Paris|FR - Paris"
    "SE//Stockholm|SE - Stockholm"
    "CH//Zurich|CH - Zurich"
    "IN//Mumbai|IN - Mumbai"
    "JP//Tokyo|JP - Tokyo"
    "KR//Seoul|KR - Seoul"
    "SG//Singapore|SG - Singapore"
    "AU//Sydney|AU - Sydney"
    "ZA//Johannesburg|ZA - Johannesburg"
    "AR//Buenos Aires|AR - Buenos Aires"
)

于是我放弃了“用终端用户 VPN 视角来测”的思路,转而采用一种更硬核的办法:直接在世界各地的数据中心创建真正的 VPS 节点来跑测试。

Vultr VPS 基准测试

我需要一家不同于 DigitalOcean 和 Hetzner 的 VPS 提供商,以免底层基础设施带来额外优势。除了这两家,我唯一用过的 VPS 服务商就是 Vultr。于是我决定在不同地区各开一台机器跑测试。因为整个过程是手工完成的,所以最后只选了四个地区:伦敦圣保罗硅谷东京。我在这些地区各创建了一台最便宜的 Fedora 虚拟机。

经过几轮测试后,我发现 hey 更符合我的需求。于是我的流程就变成:SSH 登录测试机,执行脚本,然后把结果拷出来。第一个测试点是圣保罗:

dnf install -y tmux vim htop
wget https://storage.googleapis.com/hey-releases/hey_linux_amd64
chmod +x hey_linux_amd64
./hey_linux_amd64 -n 1000000 -c 10000 -t 10 -z 5m -h2 https://crocidb.com/ > crocidb.com.log
./hey_linux_amd64 -n 1000000 -c 10000 -t 10 -z 5m -h2 https://crocidb.cro.to/ > crocidb.cro.to.log

这会下载 hey,然后以一种非常夸张的配置运行:总请求数 1M,并发 10k,超时 10s(也就是任意单个请求超过 10 秒就算失败),总时长 5m

第一次跑的时候,我很快就发现新 FreeBSD 服务器确实有个问题:它很早就开始失败,因为它根本扛不住 1 万并发连接。查了很多资料后,我发现可以用 netstat -Lan 查看 socket 队列大小,而我的队列值全都是 128。继续追下来才发现,原来 kern.ipc.somaxconn 的默认值就是这个数字。于是我把它调高了:

sysctl kern.ipc.somaxconn=16384

差不多十分钟后,日志跑完了,我开始看结果:

(原文此处有圣保罗 VPS 上 hey 输出对比图)

左边是旧服务器,右边是新服务器,差异简直夸张。

虽然两台机器都返回了不少错误,但 FreeBSD 这边至少完成了预期的 1M 请求,而 Ubuntu 那边连 20k 都没撑住!这个差距实在是太大了

基准结果分析

(原文此处有成功率图表)

旧服务器 crocidb.com 只能完成极少一部分请求,这一点真的很惊人。实际上,它只完成了大约 7% 的请求,而 FreeBSD 服务器完成了 94%。东京地区里新服务器的成功率略微低一点,但我觉得还不至于值得担心。

从每秒请求数来看,新服务器最少也有 3 倍提升,最高甚至能到 11 倍。

(原文此处有 Requests per second 图表)

这里的差距看起来更夸张,而我猜测东京那边的延迟可能确实更高一些。

(原文此处有延迟百分位图表)

如果看延迟百分位(例如 p50 表示 50% 的请求都比这个值更快返回),会发现两台服务器呈现出很不一样的曲线形态。新服务器直到大约 90% 之前都保持比较 线性 的增长,所以表现更可预测;而旧服务器的增长曲线则明显更不稳定。

这说明,即使在高负载下,全球范围内 90% 的访问者在尝试加载我博客首页时,仍然能在 3.5 秒 内拿到内容。我觉得这已经相当不错了。

我没有再继续深挖东京地区的问题,不过目前我也没那么担心。看 hey 的请求阶段分解(也就是它会拆分 DNS、连接、等待响应、读取响应等耗时),确实能看出日本方向的流量更慢一些:

(原文此处有请求阶段耗时分解图)

不过这些数据在我看来又有点怪。首先,第二个域名的 DNS 拨号和查询时间低得离谱,也许是因为它是个 CNAME 记录?其次,resp wait(基本可以理解成首字节时间)和 resp read(传输时间)在这里高得有点异常。但这也可能是因为它统计的只是成功请求:也就是说,旧服务器可能一开始响应很快,但很快就把新请求都拒掉了。

结论

我并不认为这个差距是因为我选的这套栈本身就有多神奇。更有可能的原因是:旧 Ubuntu 系统本身就有一些配置问题,再加上我选的 Hetzner VPS 有 4 个 CPU 核,而旧 DigitalOcean 机器只有 1 个,自然能同时处理更多请求。我也不觉得这在实际意义上真的有多重要,因为这些服务器几乎不可能真的遇到这么大的并发量。也许跑一遍 WebPageTest 这样的 Web 基准工具就已经够了。

真正切换

虽然这些基准测试还留下了很多未解答的问题,但我对结果已经很满意了。所以我直接更新了 DNS 记录。现在,这个博客已经正式跑在那台新机器上了。

说到底,经过很多个小时的实验、折腾、构建、推倒重来之后,我意识到:搭一台用 FreeBSD 托管网站的机器,其实并没有我想象中那么复杂。满足我需求的 Web 托管服务其实也有不少,我本来也完全可以用别的方案。或者装一个 Proxmox,用图形化仪表盘来管理容器和系统。甚至也可以试试它在 FreeBSD 世界里的对应物 Sylve。但我还是很喜欢自己这次走过的这条路,因为整个过程让我学到了很多。

其中几个最主要的收获是:

  • 那台 Ubuntu 服务器真的非常皮实。十年来,它一直把我这些站点的负载扛得很好。最后四年甚至一次都没重启过。而这一切并不需要多复杂的配置。
  • 配置 FreeBSD 比我原本想象的容易。我很喜欢把系统配置集中在一个地方管理的感觉,而且它的在线文档也真的非常好。
  • 想自己搭一台托管博客的机器,往往需要大量网络知识,而这些远远超出了一个游戏开发者通常具备的范围。
  • 学习另一套系统真的很有趣。也许下次我会试试 OpenBSD 或 NetBSD。

不过说到底,这一切也没什么意义,因为我的大部分流量现在反正都来自各种 AI 抓取器……

Refs

修复 Joplin on KDE 菜单栏显示问题

在 KDE 桌面下默认使用全局菜单显示应用程序的菜单栏,但是唯独 Joplin 无法显示。

最后在这里找到了解决方案,下面简单记录:

sudo vim /usr/share/applications/joplin-desktop.desktop 
--Exec=/usr/bin/joplin-desktop
++Exec=/usr/bin/joplin-desktop --gtk-version=3 --ozone-platform=x11

启动程序增加这两个参数即可解决。

Refs

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=21yjpwt8mhhc0

Copy Fail:Linux 内核 2017 年至今的高危漏洞(附临时缓解方案) | CVE-2026-31431

最近爆出一个 Linux 内核存在近10年的漏洞,随便找了一台最近在用的机器试了一下,直接成功提权:

~$ whoami
songtianlun
~$ python3 test.py 
# whoami
root

临时解决方案如下:

由于 SSH/OpenSSL 等安全基建库几乎都在使用自行维护的用户态加密库, 所以 AF_ALG 可以直接禁用, 为临时缓解措施 (仅供参考):

rmmod algif_aead 2>/dev/null || true
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif.conf

再尝试就不行了:

~$ python3 test.py Traceback (most recent call last):
  File "/home/songtianlun/test.py", line 9, in 
<module>
    while i<len(e):c(f,i,e[i:i+4]);i+=4
  File "/home/songtianlun/test.py", line 5, in c
    a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
FileNotFoundError: [Errno 2] No such file or directory

影响面甚广,赶快检查一下自己的服务器。

Refs

在 K3s 节点上安装并使用 nerdctl

适用场景:K3s 默认不附带 nerdctl,但其内置的 containerd 与 nerdctl 完全兼容。本教程讲解如何在 K3s 节点上以最小代价安装 nerdctl,并正确指向 K3s 的 containerd socket,无需重复安装 containerd 或 CNI。

一、背景与原理

工具 说明
ctr containerd 内置调试工具,与 Docker CLI 不兼容,功能有限
crictl CRI 调试工具,K3s 自带,面向 Kubernetes 运维
nerdctl Docker 兼容 CLI,支持 run/build/compose推荐日常使用

K3s 的 containerd socket 路径为 /run/k3s/containerd/containerd.sock,而非标准路径 /run/containerd/containerd.sock。只需在配置中指向该路径,nerdctl 即可接管 K3s 容器管理。

K3s 已自带 CNI 插件(flannel/calico 等),查看 K3s 节点已有的 Pod 和镜像无需额外 CNI。若需要 nerdctl run 启动独立容器并连接网络,则需要补充安装 CNI 插件(见第四节)。

二、安装 nerdctl(仅二进制)

K3s 节点已有 containerd,只需下载 nerdctl 的精简包(不含 containerd/CNI,体积小)。

2.1 下载二进制

# 查询最新版本(或手动前往 https://github.com/containerd/nerdctl/releases 查看)
NERDCTL_VERSION=$(curl -s https://api.github.com/repos/containerd/nerdctl/releases/latest \
  | grep tag_name | cut -d '"' -f4 | tr -d 'v')

echo "最新版本: ${NERDCTL_VERSION}"

# 下载精简包(仅 nerdctl 二进制)
curl -LO "https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz"

ARM64 节点(如树莓派、ARM 服务器)将 amd64 替换为 arm64

curl -LO "https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-arm64.tar.gz"

2.2 解压并安装

# 解压到 /usr/local/bin
sudo tar Cxzvf /usr/local/bin nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz nerdctl

# 验证安装
nerdctl --version

三、配置 nerdctl 指向 K3s containerd

nerdctl 默认连接 /run/containerd/containerd.sock,在 K3s 节点上需要修改为 K3s 专用路径。

3.1 创建配置文件

sudo mkdir -p /etc/nerdctl

sudo tee /etc/nerdctl/nerdctl.toml > /dev/null <<EOF
# nerdctl 全局配置,适配 K3s 节点
address        = "/run/k3s/containerd/containerd.sock"
namespace      = "k8s.io"
EOF

说明

  • address:K3s containerd 的 socket 路径
  • namespace:K3s 所有容器和镜像均存储在 k8s.io 命名空间下

3.2 验证连接

# 列出 K3s 命名空间下的所有容器(等同于 kubectl get pods 的容器视角)
sudo nerdctl ps -a

# 列出镜像
sudo nerdctl images

如果能看到 K3s 系统 Pod(如 coredns、traefik 等),说明配置成功。

四、安装 CNI 插件(按需,用于 nerdctl run)

如果只需要查看 K3s 已有容器和镜像,可跳过此节。

只有当你需要用 nerdctl run 启动独立容器(即非 Kubernetes 管理的容器)时,才需要 CNI 插件。K3s 自带的 CNI 仅供 Kubernetes 使用,nerdctl 的独立容器网络需要单独配置。

4.1 下载官方 CNI 插件

CNI_VERSION=$(curl -s https://api.github.com/repos/containernetworking/plugins/releases/latest \
  | grep tag_name | cut -d '"' -f4)

curl -LO "https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/cni-plugins-linux-amd64-${CNI_VERSION}.tgz"

# 安装到标准路径
sudo mkdir -p /opt/cni/bin
sudo tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-${CNI_VERSION}.tgz

4.2 创建默认网络配置

sudo mkdir -p /etc/cni/net.d

sudo tee /etc/cni/net.d/10-nerdctl-bridge.conflist > /dev/null <<EOF
{
  "cniVersion": "1.0.0",
  "name": "nerdctl-bridge",
  "plugins": [
    {
      "type": "bridge",
      "bridge": "nerdctl0",
      "isGateway": true,
      "ipMasq": true,
      "ipam": {
        "type": "host-local",
        "ranges": [
          [{"subnet": "10.88.0.0/16"}]
        ],
        "routes": [{"dst": "0.0.0.0/0"}]
      }
    },
    {
      "type": "portmap",
      "capabilities": {"portMappings": true}
    },
    {
      "type": "firewall"
    }
  ]
}
EOF

注意:此桥接网络(10.88.0.0/16)仅供 nerdctl 管理的独立容器使用,不会影响 K3s 自身网络。

4.3 验证独立容器运行

# 注意:启动独立容器时需使用默认命名空间(不加 --namespace k8s.io)
# 或在 nerdctl.toml 中临时切换,推荐直接在命令行覆盖:
sudo nerdctl --namespace default run -d --name test-nginx -p 8080:80 nginx:alpine

# 确认运行
sudo nerdctl --namespace default ps
curl http://localhost:8080

五、常用命令速查

所有命令均需 sudo(或将当前用户加入 containerd 相关权限组)。

查看 K3s 容器和镜像

# 列出所有容器(K3s 管理)
sudo nerdctl ps -a

# 列出镜像
sudo nerdctl images

# 查看容器日志
sudo nerdctl logs <容器ID或名称>

# 进入容器终端
sudo nerdctl exec -it <容器ID或名称> sh

镜像管理

# 拉取镜像(拉取后可直接被 K3s Pod 使用)
sudo nerdctl pull nginx:alpine

# 查看镜像详情
sudo nerdctl inspect <镜像ID>

# 删除镜像
sudo nerdctl rmi <镜像ID>

# 从 tar 包导入镜像(常用于离线环境)
sudo nerdctl load < image.tar

# 导出镜像为 tar 包
sudo nerdctl save nginx:alpine -o nginx.tar

构建镜像(需安装 BuildKit,见第六节)

sudo nerdctl build -t myapp:v1 /path/to/dockerfile-dir

配合 kubectl 使用本地镜像

# 构建并打 tag 到 k8s.io 命名空间
sudo nerdctl --namespace k8s.io build -t myapp:local .

# 然后在 Pod spec 中指定 imagePullPolicy: Never 即可使用本地镜像
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: myapp
spec:
  containers:
  - name: myapp
    image: myapp:local
    imagePullPolicy: Never
EOF

六、可选:安装 BuildKit(支持 nerdctl build)

nerdctl 构建镜像需要 BuildKit daemon。

BUILDKIT_VERSION=$(curl -s https://api.github.com/repos/moby/buildkit/releases/latest \
  | grep tag_name | cut -d '"' -f4)

curl -LO "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/buildkit-${BUILDKIT_VERSION}.linux-amd64.tar.gz"

sudo tar Cxzvf /usr/local buildkit-${BUILDKIT_VERSION}.linux-amd64.tar.gz

# 创建 systemd 服务
sudo tee /etc/systemd/system/buildkit.service > /dev/null <<EOF
[Unit]
Description=BuildKit
After=network.target containerd.service

[Service]
ExecStart=/usr/local/bin/buildkitd \
  --addr unix:///run/buildkit/buildkitd.sock \
  --containerd-worker-addr /run/k3s/containerd/containerd.sock
Restart=always

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now buildkit

Refs

KDE Plasma6 禁用全局菜单,恢复正常应用菜单

前情提要

不知道从什么时候开始,KDE Plasma 默认启用类似 macOS 的全局应用菜单。

即应用窗口标题栏下方不显示菜单,而是移动到顶部菜单栏中“全局菜单”小组件中。

但问题是,Linux 桌面生态生态复杂,X11 Wayland Qt GTK 等等技术太过复杂,很难保证常用软件都能够正常显示全局菜单。

比如我最近在使用 Joplin ,就发现除了菜单栏根本找不到任何入口。

于是搜索了一番后,终于找到了关闭全局菜单,恢复正常的应用菜单的方法。

恢复方法

第一步:移除“全局菜单组件”

Edit Mode > Add or Manage Widgets > Global Menu > Remove all instances (button in the top right corner of the widget)

进入编辑模式,添加或管理组件,删除“全局菜单”小组间

image

第二步:移除应用菜单

实测完成第一步即可,第二步按照自己实际情况决定是否要做。

Settings > Colors & Themes > Window Decorations > Configure Titlebar Buttons…

There remove the Application Menu (“Hamburger”) button from your titlebars.

进入设置,颜色与主题,窗口装饰元素,配置菜单栏按钮,将“应用菜单”按钮移除,点击应用,即可。

image

第三步:重启应用

此时应该就能看到应用菜单了,如果看不到再重启一下即可。

image

Refs

磁盘使用分析工具对比:du vs ncdu vs gdu vs dust

Claude Sonnet 4.5 协助编写。

在日常的系统管理和磁盘空间清理工作中,我们经常需要分析磁盘使用情况。本文将对比四个常用的磁盘使用分析工具:传统的 du、经典的交互式工具 ncdu、现代化的 gdudust

工具简介

du (Disk Usage)

du 是 Unix/Linux 系统自带的经典磁盘使用分析工具,已经存在了几十年。它是最基础、最通用的选择。

ncdu (NCurses Disk Usage)

ncdu 是基于 ncurses 库的磁盘使用分析工具,提供了简洁的交互式文本界面。它是最早流行的交互式磁盘分析工具之一。

gdu (Go Disk Usage)

gdu 是用 Go 语言编写的现代化磁盘分析工具,提供了交互式界面和更快的扫描速度。

dust (du + rust = dust)

dust 是用 Rust 编写的磁盘使用分析工具,以更直观的可视化输出为特色。

功能对比

特性 du ncdu gdu dust
交互式界面
扫描速度 中等 中等
可视化输出 基础 中等 强大 优秀
系统自带
内存占用
删除文件功能
编程语言 C C Go Rust
易用性

使用示例

du 基本用法

# 显示当前目录大小
du -sh

# 显示所有子目录大小并排序
du -h --max-depth=1 | sort -hr

# 显示最大的10个目录
du -h | sort -rh | head -10

优点:

  • 系统自带,无需安装
  • 稳定可靠,脚本友好
  • 广泛的兼容性

缺点:

  • 速度较慢
  • 输出不够直观
  • 缺少交互功能

ncdu 基本用法

# 安装
# Debian/Ubuntu
sudo apt install ncdu

# macOS
brew install ncdu

# RHEL/CentOS
sudo yum install ncdu

# 分析当前目录
ncdu

# 分析指定目录
ncdu /path/to/directory

# 扫描时排除某些目录
ncdu --exclude /path/to/exclude

# 导出结果到文件(可在其他机器上查看)
ncdu -o result.json
ncdu -f result.json  # 读取导出的文件

交互式操作:

  • ↑↓j/k: 上下移动
  • Enter: 进入目录
  • : 返回上级目录
  • d: 删除选中的文件/目录
  • g: 显示百分比/图形条
  • n: 按名称排序
  • s: 按大小排序
  • q: 退出

优点:

  • 成熟稳定,广泛使用
  • 交互式界面简洁清晰
  • 可以直接删除文件
  • 支持导出和导入扫描结果
  • 内存占用合理
  • 在大多数发行版仓库中可用

缺点:

  • 扫描速度比 gdu 慢
  • 界面相对传统,不如 gdu 美观
  • 大型目录扫描时需要等待

gdu 基本用法

# 安装
# macOS
brew install gdu

# Linux
curl -L https://github.com/dundee/gdu/releases/latest/download/gdu_linux_amd64.tgz | tar xz
sudo mv gdu /usr/local/bin/

# 分析当前目录
gdu

# 分析指定目录
gdu /path/to/directory

# 非交互模式
gdu -n /path/to/directory

优点:

  • 扫描速度极快
  • 交互式 TUI 界面,可以用键盘导航
  • 可以直接在界面中删除文件
  • 支持彩色输出
  • 可以显示进度条

缺点:

  • 需要单独安装
  • 交互模式在某些脚本场景下不适用

dust 基本用法

# 安装
# macOS
brew install dust

# Linux
cargo install du-dust

# 基本使用
dust

# 分析指定目录
dust /path/to/directory

# 显示更多层级
dust -d 3

# 只显示目录
dust -t

优点:

  • 树状图可视化输出,非常直观
  • 彩色条形图显示占用比例
  • 输出清晰易读
  • 速度较快
  • 默认排序输出

缺点:

  • 需要单独安装
  • 没有交互式界面
  • 相对 du 功能较新,可能有兼容性问题

实际使用场景推荐

选择 du 的场景

  • 在生产服务器上进行快速检查
  • 编写自动化脚本
  • 需要最大兼容性
  • 系统资源受限

选择 ncdu 的场景

  • 需要交互式浏览但服务器上没有 gdu
  • 偏好传统稳定的工具
  • 需要导出扫描结果到其他机器分析
  • 在资源受限的系统上需要交互功能
  • 系统包管理器中已有 ncdu

选择 gdu 的场景

  • 需要深入分析大型目录结构
  • 需要最快的扫描速度
  • 追求现代化的交互体验
  • 在个人工作站上使用
  • 经常处理超大目录

选择 dust 的场景

  • 需要快速浏览目录大小
  • 偏好可视化输出
  • 想要更现代化的工具体验
  • 不需要交互式操作
  • 需要快速生成报告

性能对比

在一个包含 50GB 数据、约 100,000 个文件的目录上测试:

  • du: ~8 秒
  • ncdu: ~5 秒(扫描阶段)
  • gdu: ~2 秒
  • dust: ~4 秒

注:实际性能取决于硬件配置、文件系统类型和文件数量。ncdu 的优势在于扫描后的交互浏览非常流畅。

总结

四个工具各有千秋:

  • du 是经典之选,适合脚本和生产环境
  • ncdu 是稳定可靠的交互式工具,兼具易用性和可用性
  • gdu 是性能之王,提供强大的交互功能和最快速度
  • dust 是可视化专家,输出最为直观

工具演进历史

这四个工具代表了磁盘分析工具的演进过程:

  1. du (1970s): 命令行时代的基础工具
  2. ncdu (2007): 加入交互式界面,提升用户体验
  3. gdu (2020): 现代编程语言带来的性能提升
  4. dust (2018): 注重可视化和用户友好度

我的推荐

对于日常使用,我的建议是:

  1. 保留 du 用于脚本和快速检查
  2. 安装 ncdu 作为通用的交互式工具(服务器友好)
  3. 安装 gdu 用于深入的磁盘分析(个人工作站)
  4. 安装 dust 用于快速浏览和可视化

如果只能选一个额外工具:

  • 服务器环境: 选 ncdu(稳定、轻量、可靠)
  • 个人电脑: 选 gdu(快速、现代、强大)
  • 快速查看: 选 dust(直观、美观、高效)

根据具体需求选择合适的工具,甚至可以将它们组合使用,发挥各自的优势。

参考链接

彻底解决阿里云和 tailscale 冲突

如果你在一台阿里云服务器安装并启动了 tailscale,大概率会出现阿里云服务器无法上网的问题,根本原因为阿里云服务器默认DNS与tailscale网段产生冲突。

由于阿里云和 tailscale 都使用了 100.64.0.0/10 这个网段。100.64.0.0/10RFC 6598 中被保留为 运营商级 NAT (Carrier-Grade NAT) 地址段,用于 ISP 做 NAT 时避免与内网冲突。Tailscale 把它当成“只允许来自 Tailscale 接口的地址段”是符合规范的。但阿里云在 VPC 内把 100.100.2.136100.100.2.138 作为内网 DNS 服务地址,初衷是: 地址在公网不可路由,避免外泄; 与经典网络互通时不会冲突。

由于动机不同,目标不同,造成二者冲突。

目前比较流行的方法,是关闭 tailscale 的 iptables 规则生成,但这是不安全的。浏览各种解决方案后我认为脚本轮询的方案最可靠。

脚本轮训解决

/usr/local/bin/fix-ts-dns.sh

#!/bin/bash
while true; do
  if ! iptables -C ts-input -s 100.100.2.136/32 -j ACCEPT 2>/dev/null; then
    iptables -I ts-input 1 -s 100.100.2.136/32 -j ACCEPT
  fi
  sleep 30
done

/etc/systemd/system/fix-ts-dns.service

[Unit]
Description=Keep Aliyun DNS whitelist in Tailscale chain
After=tailscaled.service

[Service]
Type=simple
ExecStart=/usr/local/bin/fix-ts-dns.sh
Restart=always

[Install]
WantedBy=multi-user.target
chmod +x /usr/local/bin/fix-ts-dns.sh
systemctl enable --now fix-ts-dns.service

脚本轮询,检查到存在 tailscale 的规则表且不存在白名单时,自动插入一条放通阿里云网段的规则。

解决方案来源:https://www.xugj520.cn/archives/aliyun-cgnat-tailscale-conflict.html

Refs

SSH 通过跳板机连接

TL;DR

有两种方式可以实现通过跳板机直接连接目标服务器 SSH.

# ProxyJump(推荐方式)OpenSSH >= 7.3
ssh -J user@jump-server.dealiaxy.com:10023 user@target.dealiaxy.com
# 在这条命令中,-J 后面指定了跳板机的地址(user@jump-server.dealiaxy.com)和端口(10023)。SSH 会先与跳板机建立连接,然后通过跳板机转发流量到目标服务器 target.dealiaxy.com。整个过程只需要一次登录操作,极大简化了访问流程。
# ProxyCommand
ssh -o "ProxyCommand ssh -W %h:%p user@jump-server.dealiaxy.com -p 10023" user@target.dealiaxy.com
# 在这个命令中,-o "ProxyCommand" 选项指定了一个自定义的命令来通过跳板机进行连接。具体地,ssh -W %h:%p 会将目标主机(%h)和端口(%p)转发给跳板机,然后通过跳板机建立与目标主机的连接。

Refs