Linux 桌面系统故障排查指南(六) - 系统关机与电源管理

AI 创作声明:本系列文章由笔者借助 ChatGPT, Kimi K2, 豆包和 Cursor 等 AI 工具创作,有很大篇幅的内容完全由 AI 在我的指导下生成。如有错误,还请指正。 前言 系统关机看似简单,但背后涉及了繁杂的资源清理和状态管理过程。当你点击关机按钮,系统却卡在那里不动,或者出现各种奇怪的错误信息时,理解关机流程和故障排查方法就显得尤为重要。 除了关机,Linux 还提供了休眠和挂起两种重要的电源管理功能,它们可以让系统快速进入低功耗状态,同时保持工作状态,是日常使用中非常实用的功能。 作为这个系列的最后一篇文章,本文将探讨系统关机的完整流程,以及休眠和挂起功能的配置与故障排查,从优雅关闭到强制关机,从服务停止到资源清理,从电源管理到状态恢复,全面了解系统的电源管理机制。 系统关机流程 1.1 关机流程概览 systemd 管理的关机过程分为四个主要阶段,每个阶段都有明确的目标和顺序,确保数据完整性和系统稳定性。 关机阶段: 用户会话清理阶段(约 1-5 秒): 通知所有用户会话即将关机 优雅关闭用户应用程序 回收用户设备权限 系统服务停止阶段(约 2-10 秒): 按依赖关系逆向停止系统服务 卸载文件系统(除根文件系统外) 网络服务断开连接 内核资源释放阶段(约 1-3 秒): 同步所有文件系统到磁盘 卸载根文件系统为只读 终止所有剩余进程 硬件关机阶段(约 1-2 秒): 通过 ACPI 发送关机信号 固件接管系统控制权 所有硬件设备断电 1.2 用户会话清理 当用户发起关机时,systemd 首先处理用户会话的清理工作,确保用户数据得到妥善保存。 会话清理流程: bash # systemd 发送关机信号 systemctl start shutdown.target # 用户会话收到终止信号 loginctl terminate-session <session_id> # 用户服务停止 systemctl --user stop graphical-session.target 关键操作: 会话通知:通过 D-Bus 向桌面环境发送关机信号 应用关闭:等待应用保存未保存的数据 权限回收:logind 回收分配给用户的设备访问权限 服务停止:用户 systemd 实例停止所有用户服务 监控用户会话清理: bash # 查看会话状态变化 journalctl -b | grep -E "(session|Session)" # 用户服务停止日志 journalctl --user -b | grep -E "(Stopping|Stopped)" # 设备权限回收 journalctl -u systemd-logind -b | grep -i "device" 1.3 系统服务停止 用户会话清理完成后,systemd 开始按依赖关系的逆向顺序停止系统服务。 服务停止顺序: 图形服务:合成器、显示管理器 网络服务:网络管理器、DNS 解析器 存储服务:磁盘管理、LVM 基础服务:日志、设备管理 关键服务处理: bash # 查看关机时的服务停止顺序 systemd-analyze critical-chain shutdown.target # 监控服务停止状态 watch -n 1 'systemctl list-units --state=deactivating' # 检查服务停止日志 journalctl -b -1 | grep -E "(Stopping|Stopped)" | tail -20 文件系统卸载: bash # 查看挂载点卸载情况 mount | grep -v "on / type" # 文件系统同步状态 sync echo 3 > /proc/sys/vm/drop_caches # 检查卸载错误 journalctl -b -1 | grep -i "unmount\|busy" 1.4 内核资源释放 当所有用户空间服务停止后,systemd 执行最终的系统清理: 文件系统操作: 调用 sync() 同步所有已挂载文件系统的数据到磁盘 按照逆向挂载顺序卸载所有挂载点 卸载外接硬盘分区等外部存储设备 进程管理: 向所有剩余进程发送 SIGTERM,给它们最后清理机会 等待超时后,对顽固进程发送 SIGKILL 强制终止 清理僵尸进程和孤儿进程 Watchdog 监控: systemd 的看门狗机制监控服务关闭进度 如果服务停止超过 TimeoutStopSec,强制终止服务 防止系统在关机过程中无限挂起 资源清理: GPU 驱动重置显卡状态,释放 VRAM 网络设备完全断电 音频设备硬件重置 1.5 硬件关机 当所有用户空间和内核资源处理完毕后,系统进入硬件关机: ACPI 操作: systemd 通过 ACPI 向固件发出关机指令 进入 ACPI S5 状态,告诉固件关闭电源 固件接管: BIOS/UEFI 接管系统控制权 执行电源关断,所有设备(CPU、内存、GPU、外部设备)断电 固件执行最后的清理工作 强制关机保护: 如果系统未能正常关机,硬件看门狗可能强制切断电源 用户长按电源键也会触发强制关机 此时机器完全断电,关机过程结束。下次开机将重新开始完整的启动周期。 1.6 关机故障排查 常见关机问题与优化: 服务停止超时: bash # 查看超时服务 journalctl -b -1 | grep -i "timeout" # 检查特定服务配置 systemctl cat <service> | grep Timeout 服务停止超时优化: TimeoutStopSec 参数控制服务停止的最大等待时间,默认值为 90 秒。systemd 在停止服务时会等待服务自行退出,超时后强制终止。对于快速停止的服务,可以设置较短的超时时间(如 10-30 秒), 配置示例:TimeoutStopSec=30s 设置 30 秒超时。 服务停止优化包括:服务应该正确处理 SIGTERM 信号,完成必要的清理工作;避免在停止过程中进行耗时的操作;确保及时释放文件句柄、网络连接等资源。 文件系统卸载失败: bash # 查找占用文件系统的进程 lsof | grep <mountpoint> # 检查文件系统状态 fsck -n /dev/<device> 文件系统卸载优化: 进程占用检查使用 lsof 命令查找仍在使用文件系统的进程。常见原因是应用程序未正确关闭文件句柄,或进程仍在运行。解决方案是强制终止占用进程,或等待进程自然结束。 文件系统状态检查包括:使用 fsck -n 进行只读检查,不修复文件系统;检查文件系统是否正确挂载,是否有错误标记;定期进行文件系统检查,及时发现和修复问题。 设备繁忙: bash # 检查设备占用 lsof | grep /dev/<device> # 查看块设备状态 lsblk -f 设备占用优化: 设备占用分析检查哪些进程仍在使用设备文件。常见设备包括 USB 设备、外部存储、网络设备等。解决方案是确保应用程序正确关闭设备,或强制卸载设备。 块设备状态检查包括:使用 lsblk 查看设备挂载状态和文件系统类型;检查设备是否处于忙碌状态; 在关机前确保所有外部设备已安全移除。 强制关机处理与优化: 当正常关机失败时,可以使用以下方法: bash # 安全强制关机 systemctl poweroff -f # 紧急关机(立即执行) systemctl poweroff -ff # 内核强制重启 echo b > /proc/sysrq-trigger # 内核强制关机 echo o > /proc/sysrq-trigger 强制关机方法: systemctl poweroff -f 强制关机,跳过某些检查和服务停止。强制终止所有进程,直接进入关机流程,可能导致数据丢失,应谨慎使用,适用于系统响应缓慢但仍有基本功能时。 systemctl poweroff -ff 紧急关机,立即执行,不等待任何操作完成。立即终止所有进程,强制关机,高数据丢失风险,仅在紧急情况下使用,适用于系统完全无响应,需要立即关机。 echo b > /proc/sysrq-trigger 内核级别的强制重启。直接调用内核重启功能,绕过用户空间,即使系统完全无响应也能执行,适用于系统完全卡死,无法响应用户命令。 echo o > /proc/sysrq-trigger 内核级别的强制关机。直接调用内核关机功能,立即断电,最高数据丢失风险,适用于极端紧急情况,需要立即断电。 关机优化最佳实践: 预防措施:定期检查服务配置,确保服务能正常停止;监控文件系统状态,及时处理问题;避免在关机前进行大量 I/O 操作。 优雅关机:优先使用正常的关机命令;给系统足够时间完成清理工作;避免频繁使用强制关机。 故障预防:定期更新系统和驱动;监控系统资源使用情况;及时处理系统警告和错误。 系统休眠与挂起 除了关机,Linux 还提供了两种重要的电源管理功能:休眠(Hibernate)和挂起 (Suspend)。这两种功能可以让系统快速进入低功耗状态,同时保持工作状态,是日常使用中非常实用的功能。 3.1 休眠(Hibernate)功能 休眠是将系统内存中的所有数据保存到磁盘(通常是交换分区或交换文件),然后完全关闭电源。当系统从休眠中恢复时,会从磁盘读取保存的数据,恢复到休眠前的状态。 休眠的工作原理: 内存数据保存:将 RAM 中的所有数据写入到交换分区或专门的休眠文件 系统状态保存:保存 CPU 状态、设备状态、网络连接等 完全断电:系统完全关闭,所有硬件断电 快速恢复:开机时直接从磁盘恢复内存状态,跳过正常启动过程 休眠配置: bash # 检查当前休眠配置 cat /sys/power/state cat /sys/power/disk # 检查交换分区大小(需要足够容纳内存数据) swapon --show free -h # 检查休眠文件(如果使用文件而非交换分区) ls -lh /swapfile 启用休眠功能: bash # 方法一:使用交换分区 # 1. 确保有足够大的交换分区(建议为内存大小的 1.5-2 倍) sudo swapon --show # 2. 获取交换分区的 UUID sudo blkid | grep swap # 3. 更新 GRUB 配置 sudo nano /etc/default/grub # 添加:GRUB_CMDLINE_LINUX_DEFAULT="resume=UUID=your-swap-uuid" # 4. 更新 GRUB 配置 sudo update-grub # 5. 重新生成 initramfs sudo update-initramfs -u # 方法二:使用交换文件 # 1. 创建交换文件(大小建议为内存的 1.5-2 倍) sudo fallocate -l 8G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile # 2. 永久挂载交换文件 echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # 3. 配置休眠到交换文件 echo 'RESUME=UUID=$(findmnt -no UUID -T /swapfile)' | sudo tee /etc/initramfs-tools/conf.d/resume sudo update-initramfs -u 休眠故障排查: bash # 检查休眠支持 cat /sys/power/state | grep disk # 检查休眠目标 cat /sys/power/disk # 测试休眠功能 sudo systemctl hibernate # 查看休眠日志 journalctl -b | grep -i hibernate dmesg | grep -i hibernate # 检查交换空间使用情况 swapon --show free -h 常见休眠问题: 交换空间不足: 问题:交换分区或文件太小,无法容纳内存数据 解决:增加交换空间大小,建议为内存的 1.5-2 倍 休眠文件损坏: 问题:休眠文件损坏导致恢复失败 解决:删除损坏的休眠文件,重新创建 硬件不支持: 问题:某些硬件不支持休眠功能 解决:检查 BIOS/UEFI 设置,更新固件 3.2 挂起(Suspend)功能 挂起是将系统进入低功耗状态,保持内存供电,CPU 和大部分硬件断电。系统可以快速恢复到挂起前的状态,但需要持续供电。 挂起的工作原理: 内存保持供电:RAM 继续供电,保持数据不丢失 CPU 进入睡眠状态:CPU 进入深度睡眠,功耗极低 外设断电:硬盘、USB 设备、网络设备等断电 快速唤醒:通过键盘、鼠标、网络唤醒等快速恢复 挂起类型: S1(Power On Suspend):CPU 停止执行,但保持供电 S2(CPU Off):CPU 断电,但保持缓存 S3(Suspend to RAM):CPU 和缓存断电,仅内存供电 S4(Suspend to Disk):等同于休眠 挂起配置: bash # 检查支持的挂起状态 cat /sys/power/state # 检查当前挂起模式 cat /sys/power/mem_sleep # 设置挂起模式(deep 为 S3,s2idle 为 S2) echo deep | sudo tee /sys/power/mem_sleep # 永久设置挂起模式 echo 'mem_sleep_default=deep' | sudo tee -a /etc/default/grub sudo update-grub 挂起故障排查: bash # 测试挂起功能 sudo systemctl suspend # 查看挂起日志 journalctl -b | grep -i suspend dmesg | grep -i suspend # 检查挂起相关服务 systemctl status systemd-suspend systemctl status systemd-hibernate # 检查挂起钩子脚本 ls -la /usr/lib/systemd/system-sleep/ 常见挂起问题: 挂起后无法唤醒: 问题:系统挂起后无法通过键盘、鼠标唤醒 解决:检查 BIOS 设置,启用 USB 唤醒功能 挂起后系统重启: 问题:挂起后系统自动重启而不是恢复 解决:检查硬件兼容性,更新驱动 挂起功耗过高: 问题:挂起状态下功耗仍然很高 解决:检查外设电源管理,禁用不必要的设备 3.3 电源管理模式对比 模式 功耗 恢复时间 数据保持 适用场景 关机 0W 30-60秒 不保持 长时间不使用 休眠 0W 10-30秒 完全保持 长时间不使用,需要快速恢复 挂起 1-5W 1-3秒 完全保持 短时间不使用,需要快速恢复 选择建议: 短时间离开(几分钟到几小时):使用挂起 长时间离开(几小时到几天):使用休眠 长期不使用(几天以上):使用关机 混合使用策略: bash # 设置自动挂起(当系统空闲时) sudo systemctl enable systemd-suspend.timer # 设置定时休眠(夜间自动休眠) sudo systemctl edit systemd-hibernate.timer # 添加: [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true 在实际使用中,大多数用户通过桌面环境的设置界面来配置电源管理功能。GNOME、KDE Plasma、XFCE 等桌面环境都提供了图形化的电源管理设置,可以方便地配置自动挂起和休眠时间。 对于使用 Wayland 合成器(如 Sway、Hyprland)的用户,通常使用专门的 idle 守护进程来管理电源状态。swayidle、hypridle 等工具可以配置系统在空闲时自动锁屏、关闭显示器或进入挂起状态。 电源管理优化: bash # 检查电源管理配置 cat /sys/power/pm_async cat /sys/power/pm_freeze_timeout # 优化挂起延迟 echo 5000 | sudo tee /sys/power/pm_freeze_timeout # 检查设备电源管理 ls /sys/bus/usb/devices/*/power/ cat /sys/bus/usb/devices/*/power/control 通过合理配置和使用休眠、挂起功能,可以显著提高 Linux 桌面系统的使用体验,既节省电力又保持工作状态的连续性。 实战案例:综合故障排查 在实际使用 Linux 桌面系统时,往往会遇到多层次、多组件交织的故障。通过系统化的排查方法,可以快速定位问题并制定解决方案。本章通过几个典型案例,讲解如何综合使用日志、调试工具和系统命令进行故障排查。 2.1 案例一:桌面环境无法启动 现象:用户登录后,屏幕闪烁后回到登录界面,桌面无法显示。 排查步骤: 检查显示管理器状态: bash systemctl status display-manager journalctl -u display-manager -b 确认用户会话: bash loginctl list-sessions loginctl show-session <session_id> 检查合成器日志(Wayland 示例): bash journalctl --user -u sway -f export WAYLAND_DEBUG=1 检查 GPU 驱动状态: bash lspci -k | grep -A 3 -i vga dmesg | grep -i drm 常见原因: 驱动不匹配或未加载 合成器启动失败 用户环境变量设置错误 解决方法: 更新或切换 GPU 驱动 使用默认配置启动合成器 检查 $XDG_RUNTIME_DIR 和 $WAYLAND_DISPLAY 是否正确 2.2 案例二:应用程序崩溃或无响应 现象:某些应用程序启动后立即崩溃,或运行中无响应。 排查步骤: 查看用户服务日志: bash journalctl --user -b -u <application>.service 启用应用调试信息: bash export GDK_DEBUG=all # GTK 应用 export QT_LOGGING_RULES="qt.qpa.*=true" # Qt 应用 export WAYLAND_DEBUG=1 分析核心转储: bash coredumpctl list coredumpctl info <pid> coredumpctl debug <pid> 检查依赖库版本: bash ldd $(which <application>) 常见原因: 缺少或版本不匹配的库 Wayland/Xwayland 支持不完整 GPU 驱动异常 解决方法: 安装或升级依赖库 强制应用使用 X11 或 Wayland 后端 检查驱动更新或使用回滚版本 2.3 案例三:系统关机或重启异常 现象:系统关机卡住,服务停止超时,最终需要强制关机。 排查步骤: 查看关机日志: bash journalctl -b -1 -e systemd-analyze blame shutdown.target 检查服务停止状态: bash systemctl list-units --state=deactivating journalctl -b -1 | grep -E "(Stopping|Stopped)" 文件系统状态: bash mount | grep -v "on / type" lsof | grep <mountpoint> 硬件设备状态: bash lsblk -f dmesg | grep -i "error\|fail\|timeout" 常见原因: 某些服务或进程未能及时停止 文件系统被占用或损坏 设备驱动异常导致无法卸载 解决方法: 强制停止顽固服务: bash systemctl stop <service> -i 检查并修复文件系统: bash fsck -n /dev/<device> 临时使用强制关机: bash systemctl poweroff -ff 2.4 案例四:网络异常导致应用无法访问 现象:应用启动正常,但无法连接网络资源。 排查步骤: 检查网络接口和状态: bash ip addr ip route nmcli device status 测试 DNS 和连通性: bash ping 8.8.8.8 dig www.example.com 查看网络服务日志: bash journalctl -u NetworkManager -b 检查防火墙和权限: bash sudo iptables -L -v -n sudo nft list ruleset 常见原因: DHCP 或静态 IP 配置错误 DNS 配置异常 防火墙阻塞访问 解决方法: 修复网络配置 检查防火墙规则 重启网络服务 2.5 综合排查方法 面对复杂问题,单靠经验可能难以定位故障,推荐遵循以下方法: 日志为先:系统日志、用户服务日志、应用日志是最直接的线索 逐层排查:从硬件 → 驱动 → 系统服务 → 用户会话 → 应用 最小复现:关闭非必要服务和应用,简化环境重现问题 工具辅助:journalctl、strace、coredumpctl、lsof、perf 等 文档与社区:查阅官方文档和社区经验,快速定位常见故障 通过上述方法,可以系统化地分析并解决大多数 Linux 桌面问题,提高系统稳定性和用户体验。 总结 至此,我们已经完成了《Linux 桌面系统故障排查指南》系列的全部六篇文章。通过这个系列,我们全面了解了 Linux 桌面系统的各个组件,从启动安全到网络配置,从多媒体输入到会话管理,从系统服务到电源管理。 Linux 桌面系统虽然有时候会出各种奇怪的问题,但理解其工作原理后,大部分问题都能找到解决思路。关键是要有耐心,多实践,多总结。特别是在电源管理方面,合理使用关机、休眠和挂起功能,可以显著提高系统的使用体验和电力效率。 这个系列到这里就结束了,希望这些内容能帮助你在 Linux 桌面的道路上走得更顺畅一些。 🔗 相关资源 技术文档:systemd 官方文档、Wayland 协议规范 社区资源:Arch Wiki、Gentoo Wiki

2025/10/19
articleCard.readMore

Linux 桌面系统故障排查指南(五) - 网络

AI 创作声明:本系列文章由笔者借助 ChatGPT, Kimi K2, 豆包和 Cursor 等 AI 工具创作,有很大篇幅的内容完全由 AI 在我的指导下生成。如有错误,还请指正。 前言 网络连接是现代桌面的基础功能,涉及硬件驱动、固件加载、网络管理和 DNS 解析等多个环节。 本文将从网卡驱动开始,经过内核网络栈,到达应用层,了解 Linux 网络系统的完整架构,包括如何配置网络连接,如何设置防火墙规则,以及如何诊断各种网络问题。 网络连接与管理 网络连接是现代桌面的基础功能,涉及硬件驱动、固件加载、网络管理和 DNS 解析等多个环节。网络故障是最常见的桌面问题之一,理解其工作原理有助于快速定位和解决连接问题。 1.1 网络架构概览 现代 Linux 桌面大多使用 systemd-networkd 配合 iwd 进行网络管理,形成完整的网络解决方案。 虽然目前仍有部分系统默认使用 NetworkManager 管理网络,用 wpa_supplicant 管理 WiFi, 但这已经不够「现代」了(逃 网络协议栈: 硬件层:网卡驱动和固件 链路层:MAC 地址管理和链路检测 网络层:IP 地址配置和路由管理 传输层:TCP / UDP 连接管理 应用层:DNS 解析和服务发现 主要组件: systemd-networkd:网络接口管理,处理 DHCP 和静态配置 iwd:无线网络管理,支持 WPA2 / WPA3 systemd-resolved:DNS 解析和缓存 1.2 网络连接流程 有线网络: 内核加载网卡驱动 检测链路状态(网线连接) systemd-networkd 通过 DHCP 获取 IP 配置 配置路由和 DNS 无线网络: 加载无线网卡驱动和固件 iwd 扫描可用网络 选择网络并进行认证(WPA2 / WPA3) 建立连接后通过 DHCP 获取 IP 网络管理命令: bash # 查看接口状态 ip link show ip addr show # 无线网络管理(iwd) iwctl station wlan0 scan iwctl station wlan0 connect "SSID" # 网络服务状态 systemctl status systemd-networkd iwd # DNS 解析测试 resolvectl query example.com resolvectl status 1.3 IPv4 / IPv6 双栈配置 现代网络正在往 IPv6 迁移的过程中,目前仍有许多站点都只支持 IPv6,因此 IPv4+IPv6 双栈成为一个过渡方案,systemd-networkd 提供完整的双栈支持。 双栈特点: IPv4:通过 DHCP 获取配置,32 位地址 IPv6:通过 Router Advertisement 获取,128 位地址 并行工作:两个协议栈同时运行 IPv6 优先:通常有 IPv6 的会优先走 IPv6 网络,没有才走 IPv4. Linux 中通过 glibc 的 getaddrinfo() 来实现该逻辑,可通过 /etc/gai.conf 调整该函数的地址排序算法。因为 APP 通常直接使用第一条记录发起连接,所以 /etc/gai.conf 通常能直接决定系统中是 IPv6 优先还是 IPv4 优先。 双栈验证: bash # 查看 IPv4 配置 ip -4 addr show ip -4 route # 查看 IPv6 配置 ip -6 addr show ping -6 2001:4860:4860::8888 # DNS 双栈测试 nslookup -type=A google.com nslookup -type=AAAA google.com 1.4 网络故障排查 连接问题诊断流程: 硬件层面: bash # 检查接口存在 ip link show # 查看驱动加载 dmesg | grep -i firmware lspci | grep -i network 链路层面: bash # 有线:检查链路状态 ethtool eth0 # 无线:扫描网络 iw dev wlan0 scan | grep SSID 网络配置: bash # DHCP 状态 journalctl -u systemd-networkd # IP 配置检查 ip addr show dev eth0 # 路由表 ip route DNS 解析: bash # DNS 配置 resolvectl status cat /etc/resolv.conf # 解析测试 dig @8.8.8.8 example.com nslookup example.com 常见问题与解决: 无法获取 IP:检查 DHCP 服务、网线连接、无线密码 DNS 解析失败:验证 DNS 服务器配置、检查 systemd-resolved 状态 IPv6 无连接:确认路由器支持 IPv6、检查 IPv6AcceptRA 配置 连接不稳定:查看信号强度、检查驱动兼容性 防火墙与网络安全 2.1 nftables 防火墙配置 nftables 是现代 Linux 的防火墙解决方案,它提供比 iptables 更简洁的语法和更好的性能。 基本概念: 表(Table):包含链和规则的容器 链(Chain):规则的有序列表 规则(Rule):匹配条件和动作 集合(Set):用于批量匹配的地址或端口列表 nftables 的四表五链、规则等概念跟 iptables 是完全一致的,这一部分可以参考我之前的文章iptables 及 docker 容器网络分析, 这里不再赘述。 NixOS 配置示例: nix # configuration.nix networking.nftables = { enable = true; ruleset = '' # 定义表 table inet filter { # 定义链 chain input { type filter hook input priority 0; policy drop; # 允许回环接口 if lo accept # 允许已建立的连接 ct state established,related accept # 允许 SSH tcp dport 22 accept # 允许 HTTP/HTTPS tcp dport {80, 443} accept # 允许 DNS udp dport 53 accept tcp dport 53 accept # 允许 DHCP udp dport 67 accept udp dport 68 accept # 允许 ICMP icmp type {echo-request, echo-reply, destination-unreachable} accept ip6 nexthdr icmpv6 icmpv6 type {echo-request, echo-reply, destination-unreachable} accept } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } } ''; }; 常用 nftables 命令: bash # 查看当前规则 nft list ruleset # 查看特定表 nft list table inet filter # 临时添加规则 nft add rule inet filter input tcp dport 8080 accept # 删除规则 nft delete rule inet filter input handle <handle> # 清空表 nft flush table inet filter 2.2 网络地址转换(NAT) 端口转发配置: nix networking.nftables.ruleset = '' table inet nat { chain prerouting { type nat hook prerouting priority 0; # 端口转发:将外部 8080 端口转发到内网 192.168.1.100:80 tcp dport 8080 dnat to 192.168.1.100:80 } chain postrouting { type nat hook postrouting priority 100; # 源地址转换(SNAT) oifname "eth0" masquerade } } ''; 虚拟网络技术 3.1 VPN 连接管理 WireGuard 配置: nix # configuration.nix networking.wireguard.interfaces = { wg0 = { ips = [ "10.0.0.2/24" ]; privateKeyFile = "/etc/wireguard/private.key"; peers = [ { publicKey = "peer-public-key"; allowedIPs = [ "0.0.0.0/0" ]; endpoint = "vpn.example.com:51820"; persistentKeepalive = 25; } ]; }; }; 3.2 虚拟网络接口 TUN/TAP 接口: bash # 创建 TUN 接口 ip tuntap add dev tun0 mode tun ip addr add 10.0.0.1/24 dev tun0 ip link set tun0 up # 创建 TAP 接口 ip tuntap add dev tap0 mode tap ip addr add 192.168.100.1/24 dev tap0 ip link set tap0 up 桥接网络: bash # 创建网桥 ip link add name br0 type bridge ip link set dev br0 up # 添加接口到网桥 ip link set dev eth1 master br0 ip link set dev tap0 master br0 # 配置网桥 IP ip addr add 192.168.1.1/24 dev br0 3.3 容器网络 Docker 网络管理: bash # 查看网络 docker network ls # 创建自定义网络 docker network create --driver bridge --subnet=172.20.0.0/16 mynetwork # 连接容器到网络 docker network connect mynetwork container_name # 查看网络详情 docker network inspect mynetwork Podman 网络配置: bash # 创建网络 podman network create mynet # 运行容器 podman run --network mynet -d nginx # 查看网络 podman network ls 网络性能优化 4.1 网络参数调优 内核网络参数: nix # configuration.nix boot.kernel.sysctl = { # TCP 缓冲区大小 "net.core.rmem_max" = 134217728; "net.core.wmem_max" = 134217728; "net.ipv4.tcp_rmem" = "4096 87380 134217728"; "net.ipv4.tcp_wmem" = "4096 65536 134217728"; # TCP 拥塞控制 "net.ipv4.tcp_congestion_control" = "bbr"; # 连接跟踪 "net.netfilter.nf_conntrack_max" = 1048576; "net.netfilter.nf_conntrack_tcp_timeout_established" = 3600; # 网络队列 "net.core.netdev_max_backlog" = 5000; "net.core.netdev_budget" = 600; }; 网络参数调优: TCP 缓冲区优化: net.core.rmem_max = 134217728 设置 TCP 接收缓冲区的最大值为 128MB。更大的接收缓冲区可以处理突发的高流量,减少丢包,提高网络吞吐量,特别适合高带宽网络环境,适用于高带宽、高延迟网络,如光纤网络、VPN 连接。 net.core.wmem_max = 134217728 设置 TCP 发送缓冲区的最大值为 128MB。更大的发送缓冲区可以缓存更多待发送数据,提高发送效率,减少发送阻塞,提高网络传输效率,适用于大文件传输、流媒体上传、高并发网络应用。 net.ipv4.tcp_rmem = "4096 87380 134217728" 设置 TCP 接收缓冲区的初始值、默认值和最大值。参数说明:初始值 4KB,默认值 87KB,最大值 128MB。动态调整接收缓冲区大小,根据网络条件自动优化,在低延迟和高吞吐量之间自动平衡。 net.ipv4.tcp_wmem = "4096 65536 134217728" 设置 TCP 发送缓冲区的初始值、默认值和最大值。参数说明:初始值 4KB,默认值 64KB,最大值 128MB。动态调整发送缓冲区大小,适应不同的网络负载,在内存使用和网络性能之间找到最佳平衡点。 TCP 拥塞控制优化: net.ipv4.tcp_congestion_control = "bbr" 使用 BBR(Bottleneck Bandwidth and RTT)拥塞控制算法。BBR 是 Google 开发的现代拥塞控制算法,基于带宽和延迟测量,在高带宽、高延迟网络环境下性能更好,减少延迟和丢包,适用于现代网络环境,特别是高带宽网络和长距离连接。 连接跟踪优化: net.netfilter.nf_conntrack_max = 1048576 增加连接跟踪表大小到 100 万条记录。支持更多并发网络连接,避免连接跟踪表溢出,支持高并发网络应用,如 P2P 下载、多用户服务,适用于服务器环境、高并发网络应用。 net.netfilter.nf_conntrack_tcp_timeout_established = 3600 设置已建立连接的超时时间为 1 小时。延长连接跟踪时间,减少连接重建的频率,减少连接重建开销,提高长连接应用的性能,适用于长连接应用,如数据库连接、WebSocket 连接。 网络队列优化: net.core.netdev_max_backlog = 5000 增加网络设备接收队列大小到 5000 个数据包。更大的接收队列可以处理突发流量,减少丢包,提高网络处理能力,减少因队列满而导致的丢包,适用于高流量网络环境,如服务器、网络设备。 net.core.netdev_budget = 600 增加每次网络处理的数据包数量到 600 个。提高网络处理效率,减少处理开销,提高网络吞吐量,减少 CPU 使用率,适用于高负载网络环境,需要优化网络处理性能。 优化效果评估:通过缓冲区优化,网络吞吐量可提升 20-50%;BBR 拥塞控制算法可显著减少网络延迟;连接跟踪优化支持更多并发连接;队列优化减少丢包,提高网络稳定性。 4.2 网络监控与分析 网络流量监控: bash # 实时流量监控 iftop -i eth0 # 网络连接监控 netstat -tuln ss -tuln # 网络统计 cat /proc/net/dev cat /proc/net/snmp # 带宽测试 iperf3 -s # 服务器端 iperf3 -c server_ip # 客户端 网络延迟分析: bash # ping 测试 ping -c 10 8.8.8.8 # 路由跟踪 traceroute 8.8.8.8 mtr 8.8.8.8 # 网络质量测试 qperf server_ip tcp_bw tcp_lat 4.3 网络故障诊断 连接问题排查: bash # 检查网络接口状态 ip link show ip addr show # 检查路由表 ip route show ip route get 8.8.8.8 # 检查 ARP 表 ip neigh show # 检查网络统计 cat /proc/net/dev cat /proc/net/snmp DNS 问题排查: bash # 测试 DNS 解析 dig @8.8.8.8 example.com nslookup example.com # 检查 DNS 配置 resolvectl status cat /etc/resolv.conf # 测试 DNS 性能 dig @8.8.8.8 example.com +stats 防火墙问题排查: bash # 检查防火墙规则 nft list ruleset iptables -L -v -n # 测试端口连通性 telnet server_ip port nc -zv server_ip port # 检查连接跟踪 cat /proc/net/nf_conntrack 高级网络配置 5.1 多网卡绑定 网卡绑定配置: nix # configuration.nix networking.bonds = { bond0 = { interfaces = [ "eth0" "eth1" ]; driverOptions = { mode = "802.3ad"; lacp_rate = "fast"; xmit_hash_policy = "layer3+4"; }; }; }; networking.interfaces.bond0.ipv4.addresses = [{ address = "192.168.1.100"; prefixLength = 24; }]; 5.2 VLAN 配置 VLAN 网络配置: nix # configuration.nix networking.vlans = { vlan100 = { id = 100; interface = "eth0"; }; vlan200 = { id = 200; interface = "eth0"; }; }; networking.interfaces.vlan100.ipv4.addresses = [{ address = "192.168.100.1"; prefixLength = 24; }]; networking.interfaces.vlan200.ipv4.addresses = [{ address = "192.168.200.1"; prefixLength = 24; }]; 5.3 网络命名空间 创建网络命名空间: bash # 创建命名空间 ip netns add ns1 ip netns add ns2 # 创建 veth 对 ip link add veth1 type veth peer name veth2 # 将接口移到命名空间 ip link set veth1 netns ns1 ip link set veth2 netns ns2 # 配置命名空间内的网络 ip netns exec ns1 ip addr add 10.0.1.1/24 dev veth1 ip netns exec ns1 ip link set veth1 up ip netns exec ns2 ip addr add 10.0.1.2/24 dev veth2 ip netns exec ns2 ip link set veth2 up # 测试连通性 ip netns exec ns1 ping 10.0.1.2 总结 网络是计算机科学中最复杂的技术之一,数据在互联网中的流动造就了现代信息社会,现代 AI 的发展也与现代网络中产生的超大规模数据密不可分。 本文只是对 Linux 网络的一个简单介绍,下一篇文章我们会聊聊系统关机和故障排查,看看系统是如何优雅地关机的,以及遇到问题时该如何处理。 快速参考 常用网络管理命令 bash # 网络接口管理 ip link show # 查看网络接口 ip addr show # 查看 IP 地址 ip route show # 查看路由表 ip neigh show # 查看 ARP 表 # 网络连接管理 ss -tuln # 查看网络连接 netstat -tuln # 传统网络连接查看 lsof -i # 查看端口占用 # 网络测试 ping -c 4 8.8.8.8 # ping 测试 traceroute 8.8.8.8 # 路由跟踪 mtr 8.8.8.8 # 网络质量测试 常用防火墙命令 bash # nftables 管理 nft list ruleset # 查看所有规则 nft list table inet filter # 查看特定表 nft add rule inet filter input tcp dport 8080 accept # 添加规则 nft delete rule inet filter input handle <handle> # 删除规则 # iptables 管理(传统) iptables -L -v -n # 查看规则 iptables -A INPUT -p tcp --dport 22 -j ACCEPT # 添加规则 iptables -D INPUT -p tcp --dport 22 -j ACCEPT # 删除规则 常用网络诊断命令 bash # DNS 解析测试 dig @8.8.8.8 example.com # DNS 查询 nslookup example.com # 传统 DNS 查询 resolvectl query example.com # systemd-resolved 查询 # 网络监控 iftop -i eth0 # 实时流量监控 tcpdump -i eth0 # 网络包捕获 wireshark # 图形化网络分析 # 带宽测试 iperf3 -s # 启动 iperf3 服务器 iperf3 -c server_ip # 客户端测试 重要配置文件位置 bash # 网络配置 /etc/systemd/network/ # systemd-networkd 配置 /etc/nftables.conf # nftables 配置 /etc/resolv.conf # DNS 配置 # 网络服务 /etc/systemd/system/ # systemd 服务配置 /etc/wireguard/ # WireGuard 配置 /etc/openvpn/ # OpenVPN 配置 # 网络状态 /proc/net/dev # 网络接口统计 /proc/net/snmp # 网络协议统计 /proc/net/nf_conntrack # 连接跟踪表

2025/10/19
articleCard.readMore

Linux 桌面系统故障排查指南(四) - 多媒体处理与中文支持

AI 创作声明:本系列文章由笔者借助 ChatGPT, Kimi K2, 豆包和 Cursor 等 AI 工具创作,有很大篇幅的内容完全由 AI 在我的指导下生成。如有错误,还请指正。 前言 Linux 桌面系统的多媒体处理和中文支持涉及多个子系统。音频延迟、字体渲染质量、输入法响应速度等问题看似简单,背后却涉及 PipeWire、fontconfig、fcitx5 等多个组件的协同工作。 本文将深入探讨 Linux 桌面系统的多媒体处理能力,了解 PipeWire 如何统一管理音频和视频,fontconfig 如何优化字体显示,以及 fcitx5 如何提供流畅的中文输入体验。 多媒体处理 现代 Linux 桌面(Wayland) PipeWire 统一处理音频和视频,取代了传统的 PulseAudio 和 JACK。PipeWire 提供了更低的延迟、更好的硬件兼容性,以及统一的媒体处理框架。 1.1 PipeWire 架构概览 https://docs.pipewire.org/page_overview.html PipeWire 作为媒体服务器的核心,连接应用程序和硬件设备,提供音频混合、视频处理和路由功能。它从一开始就定位为"通用多媒体处理框架",而非仅局限于音频,这种设计源于现代多媒体场景(如视频会议、屏幕共享、直播、跨应用媒体协作等)对"音频+视频"统一处理的强需求。Pipewire 支持所有接入 PulseAudio,JACK,ALSA 和 GStreamer 的程序。 核心组件: pipewire:核心守护进程,管理媒体流图 wireplumber:会话管理器,处理设备连接和路由策略 pipewire-pulse:PulseAudio 兼容层 pipewire-jack:JACK 专业音频兼容层 pipewire-alsa:ALSA 兼容层 技术特点: 统一架构:同时处理音频、视频、MIDI 低延迟:相比 PulseAudio 显著降低音频延迟 硬件兼容:支持专业音频设备和消费级硬件 安全隔离:通过权限控制保护媒体数据 NixOS 配置: nix services.pipewire = { enable = true; alsa.enable = true; # ALSA 兼容 pulse.enable = true; # PulseAudio 兼容 jack.enable = true; # JACK 兼容 }; services.pipewire.wireplumber.enable = true; # 禁用 PulseAudio 避免冲突 hardware.pulseaudio.enable = false; 配置文件路径: /etc/pipewire/pipewire.conf:主配置文件 /etc/pipewire/pipewire-pulse.conf:PulseAudio 兼容配置 /etc/wireplumber/:WirePlumber 会话管理器配置 1.2 音频处理流程 应用播放音频的典型流程: API 连接:应用通过 ALSA / PulseAudio / JACK API 连接到 PipeWire 流创建:在 PipeWire 图中创建音频流节点 路由决策:WirePlumber 根据策略路由到输出设备 音频处理:混合多个应用流,执行格式转换、音量调节、调整音频效果 硬件输出:通过 ALSA 驱动将 PCM 数据发送给声卡 DAC,最终输出模拟音频输出 音频节点管理: bash # 查看音频设备 pw-cli list-objects | grep -E "(Audio|Sink|Source)" # 实时监控音频流 pw-top # 图形界面管理 pavucontrol # 查看 ALSA 设备 aplay -l arecord -l 音频路由控制: bash # 设置默认输出设备 pactl set-default-sink alsa_output.pci-0000_00_1f.3.analog-stereo # 应用音量控制 pactl list sink-inputs pactl set-sink-input-volume 123 50% # 创建自定义连接 pw-cli create-link <source-node> <sink-node> 1.3 视频处理与屏幕共享 1.3.1 为什么 PipeWire 要支持视频流处理? 传统 Linux 系统中,音频和视频处理长期处于"各自为战"的状态: 音频:由 PulseAudio(桌面)、JACK(专业)等系统负责 视频:依赖 V4L2(摄像头捕获)、X11/Wayland(屏幕截图)、GStreamer(流处理)、FFmpeg(编解码)等分散组件 这种碎片化导致了诸多问题: 跨应用同步困难:直播时麦克风声音与摄像头画面延迟不一致 权限管理混乱:沙盒应用如 Flatpak 访问摄像头/屏幕需单独适配 现代场景支持不足:Wayland 下的屏幕共享、HDR 视频渲染缺乏统一支持 硬件加速复杂:GPU 编解码需各组件单独对接,兼容性差 PipeWire 的设计初衷就是打破这种割裂:通过一套统一的框架同时管理音频和视频流,让"音频+ 视频"的协作(如会议软件同时捕获麦克风和摄像头、直播工具混合游戏画面与解说声音)变得简单高效。因此,视频处理是其"统一多媒体管道"目标的自然延伸。 1.3.2 PipeWire 视频处理的核心优势 PipeWire 作为现代 Linux 桌面系统的多媒体框架,相比传统方案具有以下核心优势: 统一的"管道"模型: 音频流和视频流都被抽象为"节点" 通过统一的"节点-端口-连接"模型实现跨应用的音视频混合 框架内置时间戳同步机制,确保音视频流始终保持时序一致(延迟误差可控制在毫秒级) 原生适配现代桌面协议: 作为 Wayland 官方推荐的多媒体中间层 通过与 xdg-desktop-portal 深度集成,实现"授权式"屏幕共享 支持 HDR 视频和高分辨率流传输,性能损耗远低于传统 X11 截图 简化沙盒应用权限: 通过 Polkit 权限系统集中管理设备访问 沙盒应用无需直接操作硬件设备,只需通过 PipeWire API 请求流数据 支持动态权限调整 高效硬件加速整合: 内置统一的硬件加速抽象层 通过 GStreamer 或 FFmpeg 后端自动适配底层硬件加速接口 支持"零拷贝"传输,CPU 占用率可降低 50% 以上 灵活的动态路由: 允许实时调整视频流路径 用户可通过图形工具拖拽节点实现流的动态切换 支持自动故障恢复和流的动态转换 1.3.3 Wayland 屏幕共享协议 在 Wayland 环境中,屏幕共享功能是通过 PipeWire 的 screen-capture 协议实现的。这与 X11 有很大的不同,后者是通过其自身的扩展(如 X11R6 的 XFIXES 扩展)实现的。 协议优势: 安全性:需要用户明确授权才能进行屏幕共享 性能:直接访问合成器缓冲区,避免额外的内存拷贝 兼容性:支持多显示器、不同分辨率和刷新率 隐私保护:可以只共享特定窗口而非整个屏幕 主流应用支持:目前主流的 OBS、Discord、Zoom、Teams、Chrome/Chromium 等应用都已经支持了 Wayland 下的 screen-capture 协议。 1.3.4 视频设备管理 摄像头设备管理: bash # 查看 PipeWire 视频设备 pw-cli list-objects | grep -i video # 查看 V4L2 设备 v4l2-ctl --list-devices # 摄像头格式查询 v4l2-ctl --device=/dev/video0 --list-formats # 摄像头权限检查 ls -l /dev/video* groups $USER # 确认在 video 组 # 测试摄像头 ffplay /dev/video0 屏幕共享环境配置: bash # Wayland 环境检查 echo $WAYLAND_DISPLAY echo $XDG_SESSION_TYPE # 设置桌面环境标识(重要!) export XDG_CURRENT_DESKTOP=sway # 或 gnome, kde, xfce 等 # 检查 PipeWire 服务状态 systemctl --user status pipewire-session-manager systemctl --user status pipewire # 检查桌面门户服务 systemctl --user status xdg-desktop-portal systemctl --user status xdg-desktop-portal-wlr # Sway/Hyprland # 或 systemctl --user status xdg-desktop-portal-gnome # GNOME 1.3.5 视频流处理配置 PipeWire 视频配置: NixOS 中可通过 services.pipewire.extraConfig.pipewire."10-video"."context.properties" 来声明这部分配置。 bash # 编辑 PipeWire 主配置 vim ~/.config/pipewire/pipewire.conf # 视频相关配置示例 context.properties = { # 视频缓冲区配置 default.video.rate = 30 default.video.size = "1920x1080" # 硬件加速配置 gstreamer.plugins = [ "vaapi" # Intel/AMD GPU 硬件加速 "nvenc" # NVIDIA GPU 硬件加速 ] } 1.3.6 视频处理性能优化 硬件加速配置: bash # 检查硬件加速支持 vainfo # VA-API 支持检查 nvidia-smi # NVIDIA GPU 状态 # 环境变量设置 export LIBVA_DRIVER_NAME=i965 # Intel GPU export LIBVA_DRIVER_NAME=radeonsi # AMD GPU export LIBVA_DRIVER_NAME=nvidia # NVIDIA GPU # GStreamer 硬件加速测试 gst-launch-1.0 videotestsrc ! vaapih264enc ! mp4mux ! filesink location=test.mp4 视频编码优化: bash # FFmpeg 硬件加速编码 ffmpeg -f v4l2 -i /dev/video0 -c:v h264_vaapi -b:v 2M output.mp4 # OBS 硬件编码配置 # 设置 -> 输出 -> 编码器选择 "FFmpeg VAAPI" 或 "NVENC" 内存和 CPU 优化: bash # 调整视频缓冲区大小 vim ~/.config/pipewire/pipewire.conf context.properties = { # 减少视频缓冲区延迟 default.video.quantum = 1/30 # 30fps default.video.min-quantum = 1/30 default.video.max-quantum = 1/15 # 最大 15fps 延迟 } 1.4 故障排查 屏幕共享问题: Wayland 协议支持:确认合成器支持 screen-capture 协议 环境变量设置:正确设置 XDG_CURRENT_DESKTOP 权限配置:检查摄像头和屏幕录制权限 应用兼容性:部分应用需要特定版本的 PipeWire 音频设备识别问题: 检查设备存在: bash aplay -l arecord -l 验证 PipeWire 运行: bash systemctl --user status pipewire wireplumber journalctl --user -u pipewire -f 权限检查: bash ls -l /dev/snd/ groups $USER # 确认在 audio 组 音频延迟优化: bash # 编辑用户配置 vim ~/.config/pipewire/pipewire.conf # 低延迟配置示例 context.properties = { default.clock.rate = 48000 default.clock.quantum = 32 default.clock.min-quantum = 32 default.clock.max-quantum = 32 } PipeWire 低延迟配置: default.clock.rate = 48000 设置音频采样率为 48kHz,平衡音质和性能。48kHz 是专业音频的标准采样率,提供良好的音质同时保持合理的计算开销。相比 44.1kHz 提供更好的音质,相比 96kHz 减少 CPU 和内存使用,适用于大多数音频应用,特别是需要低延迟的实时音频处理。 default.clock.quantum = 32 设置音频缓冲区大小为 32 个样本,约 0.67ms 延迟。较小的缓冲区减少音频延迟,但需要更频繁的音频处理。计算方式:32 样本 ÷ 48000Hz = 0.67ms 延迟,适用于实时音频应用,如音乐制作、游戏、视频会议。 default.clock.min-quantum = 32 设置最小缓冲区大小,防止系统动态调整到更小的值。固定最小缓冲区大小,避免系统在低负载时过度优化导致的不稳定,确保延迟的一致性,避免音频处理的不稳定。 default.clock.max-quantum = 32 设置最大缓冲区大小,防止系统动态调整到更大的值。固定最大缓冲区大小,避免系统在高负载时增加延迟,确保延迟的上限,保持低延迟特性。 延迟优化效果:约 0.67ms 的音频延迟,适合实时应用;适度的 CPU 使用增加,但通常可接受;固定缓冲区大小提供更稳定的音频处理;特别适合音乐制作、游戏、实时通信等对延迟敏感的应用。 注意事项:过小的缓冲区可能导致音频断断续续或 CPU 使用率过高;需要根据具体硬件和应用需求调整参数;某些音频设备可能不支持极小的缓冲区大小。 中文支持 中文支持是中文用户桌面体验的核心组成部分,包括字体渲染配置和中文输入法设置。本章节将详细介绍如何在 Linux 桌面环境中正确配置中文字体和输入法,解决常见的显示和输入问题。 2.1 字体渲染 字体渲染是桌面应用显示质量的关键因素,特别是对于中文用户,CJK(中日韩)字体的正确配置直接影响阅读体验。Linux 桌面通过 fontconfig 系统统一管理字体配置,解决字体匹配、渲染和显示问题。 2.1.1 fontconfig 架构概览 fontconfig 是 Linux 桌面系统的字体配置框架,负责: 字体发现:扫描系统字体目录,建立字体索引 字体匹配:根据应用请求的字体特征(族名、样式、语言等)选择最合适的字体 字体渲染:配置字体渲染参数(抗锯齿、子像素渲染、提示等) 字体替换:当请求的字体不存在时,提供合适的替代字体 核心组件: fc-cache:字体缓存生成工具 fc-list:字体列表查询工具 fc-match:字体匹配测试工具 配置文件:XML 格式的字体配置规则 配置文件层次: bash # 系统级配置(优先级从高到低) /etc/fonts/fonts.conf # 主配置文件 /etc/fonts/conf.d/ # 配置片段目录 # 用户级配置 ~/.config/fontconfig/fonts.conf # 用户主配置 ~/.config/fontconfig/conf.d/ # 用户配置片段 2.1.2 CJK 字体配置基础 常见 CJK 字体族: 字体族 特点 适用场景 Source Han Sans Adobe 开源,专业设计 现代应用,网页显示 Source Han Serif Adobe 开源,衬线字体 设计软件,印刷 Source Han Mono 思源等宽字体 编程,代码显示 Noto Sans CJK Google 开源,与 Source Han 为同一字体 系统界面,兼容性 WenQuanYi 文泉驿,轻量级 系统界面,终端 说明:Source Han 系列和 Noto CJK 系列实际上是同一套字体,只是分别由 Adobe 和 Google 以自己的品牌名发布。 以及一些新兴的开源字体: 字体族 特点 适用场景 LXGW WenKai Screen 霞鹜文楷屏幕版 屏幕阅读,文档 Maple Mono NF CN 中英文等宽字体 编程,终端 NixOS 字体配置示例: nix # configuration.nix fonts = { # 禁用默认字体包,使用自定义配置 enableDefaultPackages = false; fontDir.enable = true; # 安装常用 CJK 字体和图标字体 packages = with pkgs; [ # 图标字体 material-design-icons font-awesome nerd-fonts.symbols-only nerd-fonts.jetbrains-mono # Noto 是 Google 开发的开源字体家族 # 名字的含义是「没有豆腐」(no tofu),因为缺字时显示的方框或者方框被叫作 tofu # # Noto 系列字族只支持西文,命名规则是 Noto + Sans 或 Serif + 文字名称。 noto-fonts # 大部分文字的常见样式,不包含汉字 noto-fonts-color-emoji # 彩色的表情符号字体 # Noto CJK 为「思源」系列汉字字体,由 Adobe + Google 共同开发 # Google 以 Noto Sans/Serif CJK SC/TC/HK/JP/KR 的名称发布该系列字体。 # 这俩跟 noto-fonts-cjk-sans/serif 实际为同一字体,只是分别由 Adobe/Google 以自己的品牌名发布 # noto-fonts-cjk-sans # 思源黑体 # noto-fonts-cjk-serif # 思源宋体 # Adobe 以 Source Han Sans/Serif 的名称发布此系列字体 source-sans # 无衬线字体,不含汉字。字族名叫 Source Sans 3,以及带字重的变体(VF) source-serif # 衬线字体,不含汉字。字族名叫 Source Serif 4,以及带字重的变体 # Source Hans 系列汉字字体由 Adobe + Google 共同开发 source-han-sans # 思源黑体 source-han-serif # 思源宋体 source-han-mono # 思源等宽 ]; # 字体渲染配置 fontconfig = { enable = true; antialias = true; # 启用抗锯齿 hinting.enable = false; # 高分辨率下禁用字体微调 subpixel.rgba = "rgb"; # IPS 屏幕使用 RGB 子像素排列 # 默认字体族配置 defaultFonts = { serif = [ "Source Serif 4" # 西文衬线字体 "Source Han Serif SC" # 中文宋体 "Source Han Serif TC" # 繁体宋体 ]; sansSerif = [ "Source Sans 3" # 西文无衬线字体 "Source Han Sans SC" # 中文黑体 "Source Han Sans TC" # 繁体黑体 ]; monospace = [ "Maple Mono NF CN" # 中英文等宽字体 "Source Han Mono SC" # 中文等宽 "JetBrainsMono Nerd Font" # 西文等宽 ]; emoji = [ "Noto Color Emoji" ]; }; }; }; 字体渲染配置参数: antialias = true 启用字体抗锯齿,让字体边缘更平滑,提升显示质量。通过灰度插值技术平滑字体边缘,减少锯齿效果,显著提升文字显示质量,特别是在高分辨率屏幕上,适用于所有现代显示设备,特别是高分辨率屏幕。 hinting.enable = false 在高分辨率屏幕(如 4K)上禁用字体微调,避免过度渲染。字体微调 (hinting)是为低分辨率屏幕设计的优化技术,在高分辨率下可能造成过度渲染,在高分辨率屏幕上提供更自然的字体显示效果,适用于高分辨率屏幕(通常 200+ DPI),如 4K 显示器、高分辨率笔记本屏幕。 subpixel.rgba = "rgb" 针对 IPS 屏幕的 RGB 子像素排列优化,提升字体清晰度。利用 LCD 屏幕的 RGB 子像素结构,通过子像素渲染技术提升字体清晰度,在 LCD 屏幕上显著提升字体清晰度,减少模糊感,适用于 IPS、TN、VA 等 LCD 屏幕,不适用于 OLED 屏幕。 字体渲染优化效果:抗锯齿和子像素渲染显著提升文字显示质量;在高分辨率屏幕上禁用微调提供更自然的显示效果;合理的字体回退机制确保各种文字的正确显示;优化的渲染配置在提升质量的同时保持良好性能。 重要说明:Source Han 系列(Adobe 发布)和 Noto CJK 系列(Google 发布)实际上是同一套字体,只是分别由 Adobe 和 Google 以自己的品牌名发布。在 NixOS 中,source-han-sans 和noto-fonts-cjk-sans 指向的是同一套字体文件。 2.1.3 常见 CJK 字体问题与解决方法 问题 1:中文字符显示为方块或问号 原因:系统缺少中文字体或字体匹配规则不正确 排查步骤: bash # 1. 检查已安装的 CJK 字体 fc-list :lang=zh-cn # 2. 测试字体匹配 fc-match "sans-serif:lang=zh-cn" fc-match "serif:lang=zh-cn" # 3. 查看字体详细信息 fc-list | grep -i "noto\|source\|wqy" 使用上面提供的示例配置通常可解决问题。 问题 2:中文字体中夹杂日语字体 原因:CJK 字体通常包含中文、日文、韩文字符,当系统缺少专门的中文字体时,会使用包含日文字符的 CJK 字体,导致中文字符显示为日语字形。 排查步骤: bash # 检查当前使用的字体 fc-match "sans-serif:lang=zh-cn" fc-match "serif:lang=zh-cn" # 查看字体包含的语言支持 fc-list :lang=zh-cn fc-list :lang=ja 解决方法: nix # configuration.nix fonts.fontconfig = { enable = true; defaultFonts = { sansSerif = [ "Source Han Sans SC" # 简体中文优先 "Source Han Sans TC" # 繁体中文备选 "Source Sans 3" # 西文备选 ]; serif = [ "Source Han Serif SC" # 简体中文优先 "Source Han Serif TC" # 繁体中文备选 "Source Serif 4" # 西文备选 ]; }; }; 2.1.4 字体调试与优化工具 字体信息查询: bash # 列出所有字体 fc-list # 按语言过滤字体 fc-list :lang=zh-cn fc-list :lang=en # 查看字体详细信息 fc-list -v "Source Han Sans SC" fc-list -v "LXGW WenKai Screen" # 测试字体匹配 fc-match -v "sans-serif:lang=zh-cn" fc-match -v "serif:lang=zh-cn" fc-match -v "monospace:lang=zh-cn" 字体渲染测试: bash # 临时安装字体测试工具 nix shell nixpkgs#pango # 创建测试文本文件 echo "中文测试 Chinese Test 123" > test.txt # 使用不同字体渲染测试 pango-view --font="Source Han Sans SC 12" test.txt pango-view --font="LXGW WenKai Screen 12" test.txt pango-view --font="Maple Mono NF CN 12" test.txt 2.2 中文输入法 现代 Linux 桌面主要使用 fcitx5 作为中文输入解决方案,它通过插件系统支持多种输入引擎,并与图形环境深度集成。 2.2.1 输入法框架架构 核心组件: fcitx5-daemon:主守护进程,管理输入法状态 输入引擎:拼音、五笔、仓颉等具体输入法实现 图形前端:负责候选词界面显示 配置工具:fcitx5-configtool 提供图形化配置 配置文件路径: ~/.config/fcitx5/config:主配置文件 ~/.config/fcitx5/profile:输入法引擎配置 ~/.config/fcitx5/conf/:各输入法引擎的详细配置 2.2.2 Wayland 原生输入法流程 Wayland text-input 协议流程: 按键捕获:键盘事件首先到达 Wayland 合成器 协议通信:合成器通过 text-input 协议与客户端应用通信 输入法服务:fcitx5 作为 Wayland 输入法服务接收事件 候选生成:fcitx5 处理按键并生成候选词 候选显示:通过 Wayland 协议在光标位置显示候选窗口 文本提交:用户选择后通过 text-input 协议提交最终文本 text-input 协议有 v1 跟 v3 两个版本,目前(2025-09)Electron/Chrome 以及其他大部分程序框架都已经支持了 text-input-v3. 桌面环境方面所有主流 Compositor 也都支持 text-input-v3. 所以目前 wayland 下输入法的可用性已经很高了。 2.2.3 X11 / XWayland 输入法流程 XWayland 使用场景: 尚未支持 Wayland 的旧版应用 需要特定 X11 功能的专业软件 通过应用启动脚本单独设置环境变量 XWayland 应用输入流程: 按键捕获:键盘事件首先进入 Wayland 合成器(Hyprland、KWin 等)。 事件转发给 XWayland(例如xwayland-satellite) 如果目标是 X11 应用窗口,合成器会将事件交给 XWayland。 XWayland 将 Wayland 输入事件转换为 X11 协议事件(如 KeyPress/KeyRelease),并交付给目标应用。 应用侧的输入法模块拦截事件 X11 应用(GTK/Qt 程序)内部加载了 fcitx5-gtk / fcitx5-qt 插件(通常根据环境变量加载,后面会介绍这些环境变量)。 这些插件拦截来自 XWayland 的键盘事件,并通过 D-Bus 将事件上报给 fcitx5。 此时应用相当于是「把键盘输入交给 fcitx5 代管」。 fcitx5 处理输入逻辑 fcitx5 收到键盘序列后,进入输入法逻辑:拼音解析、候选词生成。 fcitx5 控制候选窗口的显示位置(通常跟随输入光标),候选窗口本身可能是 X11 窗口(由 fcitx5 自己创建,并通过 XWayland 显示)。 输入结果返回应用 当用户选定候选词后,fcitx5 通过 D-Bus 调用 IM 插件接口直接把确认后的字符串传给应用。 应用的 IM 插件收到字符串后,调用应用内的「输入上下文 API」插入文本。 在应用看来,它就像直接得到了「输入了一串中文」的事件。 XWayland 环境变量设置: bash # GTK 应用使用 fcitx(通过 GTK IM 模块) export GTK_IM_MODULE=fcitx # Qt 应用使用 fcitx(通过 Qt IM 模块) export QT_IM_MODULE=fcitx # X11 应用使用 fcitx(通过 XIM 协议) export XMODIFIERS=@im=fcitx 输入法机制说明: GTK IM 模块、Qt IM 模块以及 XIM 协议,都是 X11 下的东西,在 wayland 下只需要 text-input 协议即可,不需要这些幺蛾子。 2.2.4 混合环境管理策略 推荐配置策略: 默认 Wayland 优先: 让现代应用使用原生 Wayland text-input 协议 按需 XWayland: 使用 GDK_BACKEND=x11 强制特定应用使用 XWayland 为特定应用创建启动脚本设置 IM_MODULE 相关环境变量 应用启动脚本示例: bash #!/bin/bash # 强制特定应用使用 XWayland export GTK_IM_MODULE=fcitx # 使用 GTK IM 模块 export QT_IM_MODULE=fcitx # 使用 Qt IM 模块 export GDK_BACKEND=x11 # 强制使用 X11 后端 your-application 2.2.5 故障排查与优化 输入法无响应问题: 进程状态检查: bash ps aux | grep fcitx5 systemctl --user status fcitx5 环境变量验证(仅 xwayland 场景): bash echo $GTK_IM_MODULE $QT_IM_MODULE $XMODIFIERS echo $XDG_RUNTIME_DIR $DBUS_SESSION_BUS_ADDRESS D-Bus 通信检查: bash busctl --user tree org.fcitx.Fcitx5 dbus-monitor --session "interface='org.fcitx.Fcitx5'" 诊断工具使用: bash fcitx5-diagnose fcitx5-configtool 候选框显示问题: Wayland 原生应用排查: bash # 检查 Wayland 环境 echo $WAYLAND_DISPLAY $XDG_RUNTIME_DIR # 检查 text-input 协议支持 wayland-info | grep text-input # 查看合成器日志中 text-input 相关错误 journalctl --user -u fcitx5 XWayland 应用排查: bash # 检查 XWayland 环境变量 echo $GTK_IM_MODULE $QT_IM_MODULE $XMODIFIERS # 检查 XWayland 连接 echo $DISPLAY # 验证 XIM 连接 xdpyinfo | grep -i input 权限和会话检查: bash # 确认 fcitx5 在正确的用户会话中运行 loginctl show-session $(loginctl | grep $USER | awk '{print $1}') # 检查 D-Bus 会话 echo $DBUS_SESSION_BUS_ADDRESS 应用兼容性: Wayland 应用:部分应用需要重新启动才能识别输入法 XWayland 应用:需要正确设置 XMODIFIERS 环境变量 混合环境:某些应用可能在不同环境下表现不同 性能优化: bash # 调整 fcitx5 配置 vim ~/.config/fcitx5/profile # 禁用不需要的输入引擎 # 减少候选词数量提高响应速度 # 云拼音配置 vim ~/.config/fcitx5/conf/cloudpinyin.conf 特殊场景处理: 多显示器环境: Wayland:候选框通常能正确跟随光标位置 XWayland:候选框可能在错误屏幕显示,需要调整 X11 配置 高分屏适配: Wayland:自动适配系统缩放比例 XWayland:可能需要手动设置 GDK_SCALE 或 QT_SCALE_FACTOR 游戏和全屏应用: Wayland:部分游戏可能需要 gamescope 等工具 XWayland:传统全屏游戏通常工作正常 终端应用: Wayland 终端:需要终端模拟器支持 text-input 协议 XWayland 终端:使用 X11 的 XIM 协议或 GTK/Qt IM 模块 总结 本文详细介绍了 Linux 桌面系统的多媒体处理能力,重点阐述了 PipeWire 如何统一管理音频和视频,以及 fontconfig 和 fcitx5 如何提供完善的中文支持。 PipeWire 的革命性意义 PipeWire 支持视频流处理,本质是为了解决 Linux 多媒体生态中长期存在的"音频-视频割裂"“传统协议适配困难"“沙盒权限复杂"等问题。相比传统方法,它通过统一管道模型、原生适配现代桌面、简化权限管理、整合硬件加速、动态路由等特性,让视频流的捕获、传输、处理和协作变得更高效、更安全、更易用。 如今,PipeWire 已成为 Linux 桌面视频处理的事实标准(如 GNOME 45+、KDE Plasma 6 均默认依赖),未来还将进一步整合 AI 处理(如实时美颜、降噪)等新功能,成为连接硬件、应用与用户的"多媒体中枢”。 中文支持的重要性 中文支持方面,虽然配置稍微复杂一些,但一旦搞定就基本不用再操心了。fontconfig 的字体匹配机制和 fcitx5 的输入法框架为中文用户提供了完整的桌面体验。 下一篇文章我们会聊聊网络架构,看看系统是如何处理网络连接和管理的。

2025/10/19
articleCard.readMore

Linux 桌面系统故障排查指南(三) - 桌面会话与图形渲染

AI 创作声明:本系列文章由笔者借助 ChatGPT, Kimi K2, 豆包和 Cursor 等 AI 工具创作,有很大篇幅的内容完全由 AI 在我的指导下生成。如有错误,还请指正。 前言 Systemd 及各项系统服务启动后会进入登录页面,从这一捕开始的 Linux 桌面使用过程涉及会话管理、窗口合成、图形渲染和输入处理等多个组件。 本文将探讨 Linux 桌面系统的图形架构,从用户登录到应用渲染的完整流程,包括 Wayland 和 X11 的区别,图形驱动的工作原理,以及如何诊断和解决各种图形问题。 用户会话:从登录到桌面 用户从登录到进入桌面环境的过程涉及多个组件的协调:display manager 负责认证,systemd-logind 管理会话,window compositor 提供图形环境。这个阶段的故障往往表现为登录失败、权限错误或图形界面异常。 1.1 登录流程 典型的图形登录流程: 显示管理器启动:greetd / GDM 等显示管理器显示登录界面 用户认证:通过 PAM 验证用户名 / 密码 会话创建:Display Manager 请求 logind 创建 session 用户服务启动:systemd 用户实例启动,运行用户配置的服务 合成器启动:获得环境变量和设备访问权限 关键观察点: bash # 查看显示管理器日志 journalctl -u greetd journalctl -b _COMM=greetd # 检查会话状态 loginctl list-sessions loginctl show-session <id> --property=Name,UID,State # 查看用户服务日志 journalctl --user -b 故障排查示例:用户登录后合成器未启动 检查用户服务日志:journalctl --user -u hyprland.service 验证会话状态:loginctl show-session <id> -p Active -p State 查看 PAM 认证日志:journalctl -t login 1.2 会话管理 systemd-logind 是连接登录、会话、设备权限和电源管理的核心服务。它通过 D-Bus 暴露 API,管理用户会话并分配设备 ACL。 核心职责: 会话管理:创建和维护用户会话,映射 session -> UID -> TTY / seat 设备访问:基于 udev 标签分配设备 ACL 给当前会话 电源管理:处理电源键事件,根据策略触发 suspend / shutdown 多座席支持:支持 seat 概念,管理多用户场景 seat(座席)概念 https://www.freedesktop.org/wiki/Software/systemd/multiseat/ seat(座席)是 systemd/logind 引入的术语,用来表示“一组物理设备的集合”(例如一个显示器 + 一套键盘和鼠标 + 音频设备),以及与之关联的会话(sessions)。 所有设备默认都会被分配给 seat0, 想再搞一个 seat1 实现多人图形化登录,必须通过 udev 规则完成如下操作: 必须拥有第二张显卡,这是硬性的前提!为了让 seat1 实际可用,还必须拥有第二套键鼠与声卡: 给第二块显卡写 udev 规则,打上 TAG+="master-of-seat" 并设置ENV{ID_SEAT}="seat1"; 把第二套键盘、鼠标、声卡等设备也写规则改成 ENV{ID_SEAT}="seat1"; 重启系统 logind 会把 VT/图形会话绑定到具体 seat,从而按 seat 粒度做电源管理、设备访问控制、空闲检测等策略。 远程 SSH 登录不生成也不归属任何 seat;logind 仅为其建立会话对象,seat 字段留空。因此 seat 概念对 SSH 完全透明。 注意:虽然 SSH 会话不归属任何 seat,但这不影响大多数设备的访问。设备权限管理有两套并行的机制:传统的 Unix 权限模型(基于用户组,如 video、audio、input 等)和现代的 systemd-logind ACL 机制(基于 seat 和会话)。SSH 会话主要依赖前者,因此只要用户具有相应的设备权限,仍可正常访问 GPU、声卡、存储设备等硬件资源。seat 机制主要影响的是需要图形界面交互的设备(如显示器、键盘鼠标)的访问控制。 现代 Linux 桌面系统基本都是单用户使用,因此后续讨论默认聚焦单 seat 场景。 常用命令 bash # 会话管理 loginctl list-sessions # 列出所有会话 loginctl show-session <id> -p Name -p UID -p Seat # 会话详情 loginctl terminate-session <id> # 终止会话 # seat 管理 loginctl seat-status # 查看 seat 状态 loginctl seat-status seat0 # 特定 seat 详情 # D-Bus 接口调试 busctl --system call org.freedesktop.login1 \ /org/freedesktop/login1 org.freedesktop.login1.Manager \ ListSessions 设备权限问题排查 Wayland compositor 启动但无法打开 /dev/dri/card0(GPU 权限问题) 排查: 确认 ls -l /dev/dri/card0 的 owner/group。通常应为 root:video,并且当前会话应被授予设备 ACL。 loginctl seat-status seat0 查看是否列出 /dev/dri/card0 并显示 ACL 给当前 session。 若无,通过 udevadm info /dev/dri/card0 检查 udev 是否为 GPU 设备打上了TAG+="uaccess" 或 TAG+="seat"。 查看 journalctl -u systemd-logind,看是否在用户登录时有关于设备分配的错误。 若服务是以 system user 的方式启动,确保 compositor 的进程是在用户 session 下,而不是 systemd 服务或 root 启动的进程(起进程身份不同会导致权限问题)。 意外挂起/关机(电源键/睡眠按钮不按用户设置工作) 检查 logind.conf(NixOS 对应位置请用 NixOS config 来覆写)中 HandlePowerKey,HandleLidSwitch 的配置。 journalctl -u systemd-logind 查看触发事件时间点;通常按键会以 D-Bus 事件或 ACPI 事件入日志。 若某桌面环境或应用拦截了按键,会阻止 logind 行为。可以通过 busctl monitor 监听org.freedesktop.login1 的消息,看是否收到请求。 若需要监控 logind 在登录/登出时做了什么,可以用busctl monitor --system org.freedesktop.login1 或: bash sudo dbus-monitor --system "interface='org.freedesktop.login1.Manager'" 这能观察到 session 创建、移除、seat 分配、锁屏请求等信号。 Wayland 图形架构 Wayland 是现代 Linux 桌面系统的图形协议,采用客户端-服务器模型,合成器同时扮演显示服务器和窗口管理器的角色,直接与内核的 DRM/KMS 和输入设备交互。 2.1 架构对比:X11 vs Wayland X11(传统):在 X11 架构中,X Server(例如 Xorg)是显示服务器,直接与显卡驱动和输入设备交互; 窗口管理器 / 桌面环境(例如 i3、GNOME)则作为 X client 连接到 X Server,负责窗口摆放、装饰以及用户界面。使用 startx(实际上调用 xinit)启动图形会话时,本质流程是:先启动 X Server,再在其中运行窗口管理器或桌面环境(如 exec i3)。Display Manager(如 GDM、SDDM)在图形登录时会自动启动 X Server,并完成用户认证、设置DISPLAY 等环境变量,然后再运行会话。 Wayland(现代): Wayland 合成器本身既是显示服务器,又是窗口管理器。它直接通过内核的 DRM/KMS 控制显示模式,通过 evdev/libinput 采集并分发输入事件。Wayland 客户端应用通过 Wayland socket(通常位于 $XDG_RUNTIME_DIR/wayland-0,但具体名字可变)与合成器通信。因为合成器本身直接控制显示和输入设备,所以它可以直接从一个已登录的 TTY 启动,作为该 TTY 的图形会话的 “display server”,无需先用 startx 启动一个独立的 X Server。如果使用 Display Manager 登录 Wayland 会话,则由 DM 在合适的 TTY 启动合成器并准备会话环境。 架构差异带来的实际影响 安全与权限:Wayland 把合成器放在更核心的位置(它有直接设备访问),因此确保合成器运行在正确会话(由 logind 管理)下至关重要。错误地以 root 或 system service 启动合成器会导致权限/ACL 不一致(compositor 无法访问设备或安全级别问题)。 简化流程:Wayland 把多个角色合并到合成器进程,消除了 X11 时代客户端/窗口管理器与服务器的分离复杂度,令直接从 tty 启动合成器成为可行且常见的做法。 兼容性:Xwayland 提供对 legacy X11 应用的兼容,合成器负责在启动时/按需启动 Xwayland 以支持老应用。 2.2 Wayland 协议与通信 客户端-服务器架构: 客户端-服务器模型:应用作为客户端,合成器作为服务器 Unix 域套接字:通过 $XDG_RUNTIME_DIR/wayland-0 进行通信 协议扩展:支持 xdg-shell、text-input 等扩展协议 安全隔离:应用只能访问自己的窗口和输入事件 核心协议: wayland-core:基础协议,定义 surface、buffer 等核心对象 xdg-shell:窗口管理协议,定义窗口、对话框等 wl_seat:输入设备协议,处理键盘、鼠标、触摸板 wl_output:显示输出协议,管理显示器配置 调试 Wayland 通信: bash # 查看 Wayland 环境 echo $WAYLAND_DISPLAY $XDG_RUNTIME_DIR # Wayland 调试输出 export WAYLAND_DEBUG=1 # 检查协议支持 wayland-info | grep text-input # 跟踪系统调用 strace -f -e trace=network,ipc <application> 2.3 合成器架构 输入处理组件: libinput:从 /dev/input/* 读取事件并做预处理(手势识别、触摸板边缘、键盘元键处理等) 合成器使用 libinput 的 API 进行设备枚举与事件回调 可用 libinput list-devices 查看被 libinput 管理的设备(需 root 或在 session 中运行) 设备访问: 合成器通过 /dev/dri/card0 与内核 DRM 交互 通过 /dev/input/event* 访问输入设备 通过 PipeWire 处理音频、视频和屏幕共享(详见后续多媒体章节) 常用调试命令: bash # 列出 libinput 管理设备(需要 root) $ sudo libinput list-devices # 检查输入设备权限 ls -la /dev/input/event* 图形驱动与渲染 现代 Linux 桌面系统的图形渲染涉及多个层次的组件,从底层的硬件驱动到高层的图形 API,各层协同工作实现高效的图形渲染。 3.1 图形栈架构 架构层次: 硬件层:GPU 和显示设备 驱动层:Mesa 图形驱动和内核 DRM 系统层:Wayland 协议和合成器 工具包层:GTK、Qt 等图形界面库 应用层:具体的桌面应用程序 核心组件: Mesa:提供 OpenGL/Vulkan 的开源实现 EGL:Khronos 组织定义的接口,将 OpenGL/Vulkan 与窗口系统连接 GBM(Generic Buffer Manager):Mesa 的缓冲管理接口,用于分配图形缓冲区给 GPU DRM:内核中的 Direct Rendering Manager,控制显示模式设置(KMS)和页面翻转 3.2 渲染管线 完整渲染流程: 应用创建渲染上下文: 应用调用 OpenGL/Vulkan API 创建渲染上下文 EGL 负责将图形 API 与窗口系统连接 Mesa 驱动加载并初始化 GPU 上下文 GPU 渲染执行: 应用调用 OpenGL/Vulkan API 绘制界面内容 Mesa 将 API 调用转换为 GPU 指令 在 GPU 上执行渲染,生成帧缓冲数据 缓冲区管理: GBM 负责分配和管理图形缓冲区 应用将渲染完成的缓冲区提交给合成器 合成器接收缓冲区后进行最终合成和显示 合成与展示: 合成器将多个应用的缓冲区组合成最终帧 通过 DRM/KMS 将最终帧提交到显示设备 3.3 驱动信息查询 驱动信息查询: bash # GPU device $ ls /dev/dri # 查看 Mesa/OpenGL renderer $ glxinfo | grep "OpenGL renderer" # OpenGL 信息 glxinfo | grep "OpenGL renderer" # Vulkan 信息 vulkaninfo | grep "GPU id" # DRM 设备 ls -la /dev/dri/ # 内核驱动 lspci -k | grep -A 3 -i vga 3.4 渲染器选择与优化 GTK 应用渲染器选择: bash # GTK 应用渲染器选择 export GSK_RENDERER=vulkan # 使用 Vulkan 渲染 export GSK_RENDERER=opengl # 使用 OpenGL 渲染 export GSK_RENDERER=cairo # 使用软件渲染 GSK_RENDERER=vulkan:使用现代低级别图形 API Vulkan,提供更好的多线程支持和更低的 CPU 开销。性能最佳,支持现代 GPU 特性,多线程渲染效率高,适用于现代 GPU 和需要最佳性能的应用,但需要支持 Vulkan 的 GPU 驱动。 GSK_RENDERER=opengl:使用传统硬件加速渲染 OpenGL,兼容性好,性能稳定。兼容性最佳,支持广泛的硬件和驱动,适用于大多数现代 GPU 和需要稳定兼容性的应用,特点是单线程渲染,CPU 开销相对较高。 GSK_RENDERER=cairo:使用 CPU 软件渲染,不依赖 GPU 硬件加速。兼容性最好,不依赖 GPU 驱动,适用于 GPU 驱动问题时的备选方案,或对性能要求不高的应用,缺点是性能最低,CPU 占用高。 Qt 应用渲染器选择: bash # Qt 应用渲染器选择 export QT_OPENGL=desktop # 使用桌面 OpenGL export QT_OPENGL=software # 使用软件渲染 export QT_OPENGL=angle # 使用 ANGLE(Windows 兼容层) QT_OPENGL=desktop:使用桌面版 OpenGL,支持完整的 OpenGL 功能集。功能完整,性能良好,适用于大多数桌面应用,需要完整 OpenGL 支持。 QT_OPENGL=software:使用 CPU 软件渲染,完全绕过 GPU。兼容性最好,不依赖 GPU,适用于 GPU 驱动问题,或需要确保兼容性的场景。 QT_OPENGL=angle:使用 ANGLE 将 OpenGL ES 转换为 DirectX,主要用于 Windows 兼容性。在某些 Windows 兼容层环境下性能更好,适用于 Wine 等 Windows 兼容层环境。 Mesa 驱动优化参数: bash # Mesa 驱动选择 export MESA_GL_VERSION_OVERRIDE=4.5 export MESA_GLSL_VERSION_OVERRIDE=450 # 调试信息 export MESA_DEBUG=1 # 启用 Mesa 调试信息 export LIBGL_DEBUG=verbose # 启用 OpenGL 调试信息 MESA_GL_VERSION_OVERRIDE=4.5:强制使用指定版本的 OpenGL,解决某些应用的兼容性问题。覆盖应用请求的 OpenGL 版本,强制使用指定版本,适用于应用要求过高 OpenGL 版本导致无法启动时。 MESA_GLSL_VERSION_OVERRIDE=450:强制使用指定版本的 GLSL 着色器语言,确保着色器兼容性。覆盖着色器编译器版本,避免版本不匹配问题,适用于着色器编译错误或版本不匹配时。 MESA_DEBUG=1:启用详细的 Mesa 调试信息,帮助诊断图形问题。显示详细的 OpenGL 调用信息和错误,用于开发调试和图形问题排查。 LIBGL_DEBUG=verbose:启用 OpenGL 库的详细调试输出。显示 OpenGL 函数调用和参数,用于深入分析 OpenGL 调用问题。 常见问题与解决方法: 应用闪退:尝试 GSK_RENDERER=cairo 或 QT_OPENGL=software 渲染异常:检查 GPU 驱动,尝试不同的 GSK_RENDERER 值 性能问题:优先使用 vulkan 或 opengl 硬件加速 兼容性问题:某些老旧应用可能需要软件渲染模式 应用程序与工具包 GUI 应用程序是用户与 Linux 桌面交互的主要方式。在 Wayland 环境下,应用通过标准化的协议与合成器通信,实现窗口管理、输入处理和图形渲染。 4.1 应用启动流程 标准启动过程: 环境准备: 设置 WAYLAND_DISPLAY 和 XDG_RUNTIME_DIR 加载图形工具包库(GTK/Qt) 初始化 Wayland 连接 窗口创建: 创建 Wayland 表面(surface) 设置窗口属性和装饰 注册事件监听器 渲染初始化: 创建 EGL 上下文 加载 Mesa 驱动 配置图形缓冲区 内容绘制: 应用调用 OpenGL/Vulkan API 绘制界面内容 Mesa 将 API 调用转换为 GPU 指令 在 GPU 上执行渲染,生成帧缓冲数据 应用将渲染完成的缓冲区提交给合成器 合成与展示: 合成器接收缓冲区后进行最终合成和显示 合成器将多个应用的缓冲区组合成最终帧 通过 DRM/KMS 将最终帧提交到显示设备 调试启动问题: bash # 查看 Wayland 环境 echo $WAYLAND_DISPLAY $XDG_RUNTIME_DIR # 检查应用日志 journalctl --user -u <application>.service # Wayland 调试变量 export WAYLAND_DEBUG=1 export MESA_DEBUG=1 # 跟踪系统调用 strace -f -e trace=network,ipc <application> 4.2 工具包支持 GTK 应用: GTK3/4 原生支持 Wayland 自动检测运行环境 可通过 GDK_BACKEND 强制指定后端 bash # 强制使用 Wayland GDK_BACKEND=wayland gtk-application # 强制使用 X11(通过 Xwayland) GDK_BACKEND=x11 gtk-application Qt 应用: Qt5/6 支持 Wayland 需要安装 Wayland 平台插件 自动选择最佳后端 bash # 查看 Qt 平台插件(NixOS) ls /run/current-system/sw/lib/qt*/plugins/platforms/ # 传统发行版 ls /usr/lib/qt*/plugins/platforms/ # Qt 调试信息 export QT_LOGGING_RULES="qt.qpa.*=true" SDL 应用: SDL2 内置 Wayland 支持 主要用于游戏和多媒体应用 自动适配运行环境 故障排查 5.1 会话管理问题 登录失败排查: bash # 检查显示管理器状态 systemctl status display-manager journalctl -u display-manager -b # 查看用户会话 loginctl list-sessions loginctl show-session <session_id> # 检查 PAM 认证 journalctl -t login -f 权限问题排查: bash # 检查设备权限 loginctl seat-status seat0 ls -la /dev/dri/card0 # 查看 ACL 分配 getfacl /dev/dri/card0 5.2 图形渲染问题 应用崩溃诊断: 核心转储分析: bash # 查看核心转储 coredumpctl list coredumpctl info <pid> # 调试核心文件 coredumpctl debug <pid> GPU 问题诊断: bash # 检查 GPU 重置 dmesg | grep -i "gpu hang\|reset" # Mesa 调试信息 export MESA_DEBUG=1 export LIBGL_DEBUG=verbose Wayland 协议错误: bash # Wayland 调试输出 export WAYLAND_DEBUG=1 # 合成器日志 journalctl --user -u <compositor> -f 性能问题分析: bash # GPU 使用率 nvidia-smi # NVIDIA radeontop # AMD # CPU 使用率分析 perf top -p <pid> # 内存使用 smem -p | grep <application> # 帧率监控 export __GL_SHOW_GRAPHICS_OSD=1 # NVIDIA 兼容性问题: Xwayland 问题:部分 X11 应用在 Xwayland 下运行异常 Wayland 协议缺失:某些功能需要特定的 Wayland 扩展 驱动兼容性:GPU 驱动可能不完全支持某些 Wayland 特性 解决方法: 更新 Mesa 和 GPU 驱动 检查合成器对必要 Wayland 扩展的支持 对于顽固问题,可临时使用 X11 会话 总结 从用户登录到画面显示,这一整套流程确实挺复杂的,展开说那可能得好几本大部头了。 Wayland 虽然还在发展中,但确实比 X11 要现代化很多,性能和安全性的提升是实实在在的,而且在 2025 年的今天 Wayland 生态的可用性已经很不错了。 下一篇文章我们会聊聊多媒体和中文支持,看看系统是如何处理音频视频和中文显示的。 快速参考 常用会话管理命令 bash # 会话管理 loginctl list-sessions # 列出所有会话 loginctl show-session <id> -p Name -p UID -p Seat # 会话详情 loginctl terminate-session <id> # 终止会话 # seat 管理 loginctl seat-status # 查看 seat 状态 loginctl seat-status seat0 # 特定 seat 详情 # 设备权限检查 ls -la /dev/dri/card0 # GPU 设备权限 ls -la /dev/input/event* # 输入设备权限 常用图形调试命令 bash # 图形驱动信息 glxinfo | grep "OpenGL renderer" # OpenGL 信息 vulkaninfo | grep "GPU id" # Vulkan 信息 lspci -k | grep -A 3 -i vga # 显卡驱动 ls -la /dev/dri/ # DRM 设备 # Wayland 环境 echo $WAYLAND_DISPLAY $XDG_RUNTIME_DIR # 环境变量 wayland-info | grep text-input # 协议支持 # 调试变量 export WAYLAND_DEBUG=1 # Wayland 调试 export MESA_DEBUG=1 # Mesa 调试 export GSK_RENDERER=vulkan # GTK 渲染器 export QT_OPENGL=desktop # Qt 渲染器 重要配置文件位置 bash # 会话相关 /etc/systemd/logind.conf # logind 配置 ~/.config/systemd/user/ # 用户服务配置 # 图形相关 ~/.config/wayland/ # Wayland 配置 ~/.config/gtk-3.0/ # GTK 配置 ~/.config/qt5ct/ # Qt 配置 ~/.config/mesa/ # Mesa 配置 # 设备权限 /etc/udev/rules.d/ # udev 规则 /dev/dri/ # GPU 设备 /dev/input/ # 输入设备 # 显示管理器 /etc/gdm/ # GDM 配置 /etc/lightdm/ # LightDM 配置 /etc/sddm.conf # SDDM 配置

2025/10/19
articleCard.readMore

Linux 桌面系统故障排查指南(二) - systemd 全家桶与服务管理

AI 创作声明:本系列文章由笔者借助 ChatGPT, Kimi K2, 豆包和 Cursor 等 AI 工具创作,有很大篇幅的内容完全由 AI 在我的指导下生成。如有错误,还请指正。 概述 本文是《Linux 桌面系统故障排查指南》系列的第二篇,专注于 systemd 生态系统与服务管理。在上一篇中,我们了解了系统启动与安全框架,现在让我们深入探讨 systemd 核心功能以及 systemd 生态系统中的各个专门化组件。 ⚙️ 本文主要介绍如下内容: systemd 核心功能:服务管理、依赖关系、并行启动、单元类型配置 systemd 生态系统服务:systemd-journald、systemd-oomd、systemd-resolved、systemd-timesyncd、systemd-udevd 等 设备管理:udev 规则和设备权限分配、故障排查 D-Bus 系统总线:进程间通信机制、权限管控、调试方法 1. systemd 核心功能 systemd 作为 PID 1,是现代 Linux 系统的初始化系统和服务管理器。它负责并行启动服务、维护依赖关系、管理 cgroups,并提供统一的系统管理接口。 1.1 systemd 概览与基本操作 systemd 作为现代 Linux 系统的初始化系统和服务管理器,主要专注于服务管理和系统控制。 核心功能: 服务管理:并行启动 units,维护依赖关系 资源控制:通过 cgroups 实现进程隔离和资源限制 系统状态管理:通过 target 管理不同的系统运行状态 单元生命周期管理:管理各种类型单元(service、mount、timer 等)的启动、停止和重启 常用命令: bash # 系统状态查看 systemctl get-default # 默认 target systemctl list-units --type=service # 列出服务 systemctl status sshd.service # 服务状态 # 性能分析 systemd-analyze blame # 启动耗时分析 systemd-analyze critical-chain # 关键路径分析 # 服务管理 systemctl start/stop/restart service # 服务控制 systemctl enable/disable service # 开机自启控制 systemctl reload service # 重载配置 NixOS 特殊说明:在 NixOS 中,/etc/systemd/system 下的配置文件都是通过声明式参数生成的软链接,指向 /nix/store。修改配置应通过 NixOS 配置系统,而非直接编辑这些文件。NixOS 没有传统的 /usr 和 /lib 等 FHS 目录,所有软件包都存储在 /nix/store 中,通过/run/current-system/sw/ 等符号链接提供访问。 配置文件路径: /etc/systemd/system/:系统级服务配置 /run/current-system/sw/lib/systemd/system/(NixOS)或 /usr/lib/systemd/system/(传统发行版):软件包提供的默认配置 /etc/systemd/user/:用户级服务配置 1.2 服务单元类型与配置 systemd 支持多种单元类型,每种类型都有其特定的用途和配置方式。 主要单元类型: service:服务单元,管理后台进程 target:目标单元,用于系统状态管理 mount:挂载单元,管理文件系统挂载 timer:定时器单元,替代 cron 任务 socket:套接字单元,按需启动服务 服务单元配置示例: ini [Unit] Description=My Custom Service After=network.target Wants=network.target [Service] Type=simple ExecStart=/usr/bin/my-service Restart=always User=myuser Group=mygroup [Install] WantedBy=multi-user.target 1.3 systemd 依赖关系与启动顺序 systemd 通过依赖关系管理服务的启动顺序,确保服务按正确的顺序启动。 依赖关系类型: Requires:强依赖,被依赖服务失败时,依赖服务也会失败 Wants:弱依赖,被依赖服务失败时,依赖服务仍可启动 After:启动顺序依赖,确保在指定服务之后启动 Before:启动顺序依赖,确保在指定服务之前启动 示例配置: ini [Unit] Description=Web Server After=network.target Wants=network.target Requires=nginx.service [Service] Type=forking ExecStart=/usr/sbin/nginx Restart=always [Install] WantedBy=multi-user.target 2. systemd 生态系统服务 除了基本的服务管理外,systemd 还提供了多个专门化的系统服务来支持现代 Linux 桌面的核心功能,包括日志管理、内存管理、DNS 解析和时间同步等。 本节内容仅介绍最核心的几个 systemd 服务。 systemd 全家桶,你值得拥有( 2.1 日志系统:systemd-journald systemd-journald 是 systemd 内置的日志收集守护进程,统一处理内核、系统服务及应用的日志,是现代 Linux 系统日志管理的核心组件。 2.1.1 核心特性 特性 说明 统一收集 内核日志、systemd 单元(stdout/stderr)、普通进程、容器、第三方 syslog 均汇总到同一日志流。 二进制索引 以 B+树(有序索引)+偏移量建立字段索引,支持精确查询与时间/优先级范围查询,速度远超文本 grep。 字段化存储 自动生成 _PID、_UID、_SYSTEMD_UNIT 等可信字段(不可伪造);支持自定义 FOO=bar 字段。 自动轮转与压缩 按“大小、时间、文件数”回收日志;轮转后默认用 LZ4 压缩,节省 60% 以上空间。 速率限制 可通过 RateLimitIntervalSec=/RateLimitBurst= 调整。 日志防篡改 配置 Seal=yes 后,用 journalctl --setup-keys 生成密钥,之后可用该密钥验证日志完整性。 2.1.2 日志的4个收集入口 journald 仅通过标准化入口收集日志,确保来源可追溯: 内核日志:内核 printk() 输出 → /dev/kmsg → journald(会自动添加 _PID/_COMM 等字段); systemd 单元 stdout/stderr:单元进程输出自动捕获,会附加_SYSTEMD_UNIT=xxx.service 等 systemd 相关字段; 本地 Socket:/run/systemd/journal/socket 等,接收 logger/systemd-cat 及旧 syslog 应用日志; 显式 API:sd_journal_send(),仅需自定义复杂结构化日志时使用(譬如 Docker daemon), 一般直接 print 即可。 2.1.3 日志优先级与核心配置 1. 日志优先级简述 日志按严重程度分 8 级(数字越小,级别越高),常用级别: err:错误(部分功能异常),级别 3 warning:警告(潜在风险),级别 4 info:信息(常规运行日志),级别 6 debug:调试(开发细节),级别 7 可用于筛选关键日志。 2. journald 配置 主配置文件:/etc/systemd/journald.conf,支持通过 /etc/systemd/journald.conf.d/*.conf 覆盖配置,核心配置项如下: 配置项 说明 示例 Storage= 存储策略 persistent(存 /var/log/journal,推荐)/volatile(存内存) SystemMaxUse= 持久存储最大占用 1G MaxRetentionSec= 日志最大保留时间 1month ForwardToSyslog= 是否转发到旧日志系统 yes(兼容传统文本日志) Seal= 是否启用日志防篡改 yes 生产配置示例: ini # /etc/systemd/journald.conf.d/00-production.conf [Journal] Storage=persistent SystemMaxUse=2G MaxRetentionSec=3month ForwardToSyslog=yes Seal=yes 配置生效需重启服务:sudo systemctl restart systemd-journald 2.1.4 实验:用 logger 验证日志收集 下面演示如何使用 logger 将结构化日志直接写进 journal,并立即用 journalctl 检索。 首先写入日志: bash logger --journald <<EOF SYSLOG_IDENTIFIER=myapp PRIORITY=3 MESSAGE=用户登录失败 USER_ID=alice LOGIN_RESULT=fail EOF 其中的 SYSLOG_IDENTIFIER, PRIORITY, MESSAGE 在 journald 中都有属性对应,而后两个USER_ID 与 LOGIN_RESULT 则属于自定义的日志标签。 然后查询日志: bash # 2. 按标识符过滤 journalctl -t myapp # 等价于 journalctl SYSLOG_IDENTIFIER=myapp # 3. 按优先级+自定义字段精确定位 journalctl -p err LOGIN_RESULT=fail 2.1.5 旧日志系统与 /var/log/ 解析 旧日志系统:基于 syslog 的文本管理 在 systemd 普及前,Linux 依赖 syslog 协议+文本文件 管理日志,核心组件是rsyslog(syslog 主流实现,功能强于早期 syslogd)。 旧系统工作流:应用通过 syslog(3) 接口输出日志 → rsyslog 接收 → 按“设施+优先级”写入/var/log/ 文本文件; 现代系统中的角色:rsyslog 不再是核心收集器,而是作为“兼容层”——接收 journald 转发的日志,生成传统文本文件(如 /var/log/auth.log),或转发到远程日志服务器(支持 TCP/TLS 加密)。 /var/log/ 常见文件及功能 现代系统中,这些文件由 rsyslog 生成(兼容旧习惯),不同发行版名称略有差异,但都为纯文本格式: 文件(或目录) 主要发行版差异 功能说明 /var/log/messages RHEL/CentOS/SUSE 系统通用日志:服务启停、内核提示、非专项应用消息。 /var/log/syslog Ubuntu/Debian 等价于 RHEL 的 messages,存储内核及一般系统日志。 /var/log/auth.log(Ubuntu) / /var/log/secure(RHEL) 名称不同 认证与授权事件:SSH 登录、su/sudo、用户添加/删除、PAM 告警。安全审计必看。 /var/log/kern.log 通用 仅内核环控输出:硬件故障、驱动加载、OOM、segfault。 /var/log/cron 通用 crond 执行记录:任务启动/结束、错误输出、邮件发送结果。 /var/log/btmp 通用 二进制文件,记录失败登录(lastb 读取);大小随暴力破解增长。 /var/log/wtmp 通用 二进制文件,记录成功登录/注销/重启(last、who 读取)。 /var/log/lastlog 通用 二进制文件,记录每个用户最近一次登录时间(lastlog 读取)。 /var/log/journal/ 启用 systemd-journald 后可见 目录;若 Storage=persistent,则二进制 journal 文件存于此。 2.1.6 日志写入最佳实践 场景 推荐做法 Shell脚本(独立运行) logger -t 脚本名 -p daemon.err "错误:$msg"(如 logger -t backup -p err "备份失败") 应用程序 优先考虑使用 systemd service, 少数场景可考虑直接调用 sd_journal_send() API 容器 Docker/Podman 加 --log-driver=journald(容器内正常输出即可) 高频日志 设 RateLimitIntervalSec=0 关闭限制(需评估风险),或批量写入 敏感信息 脱敏处理(如 PASSWORD=***),避免明文存储 2.1.7 运维命令速查 bash # 一、日志查询(含优先级过滤) # 实时跟踪服务日志(仅看 err 及以上级别) journalctl -f -p err -u sshd.service # 等价于 journalctl -f -p err _SYSTEMD_UNIT=sshd.service # 按时间+优先级过滤(过去1小时 warning 及以上) journalctl --since "1h ago" -p warning # -p 的参数既可使用名称,也可使用对应的数字,warning 对应 4 journalctl --since "1h ago" -p 4 # 内核日志(本次启动的 err 日志) journalctl -k -p err -b # 按自定义字段过滤(USER_ID=1001 + 优先级 err) journalctl USER_ID=1001 -p err # 通过 Perl 格式的正则表达式搜索日志 journalctl --grep "Auth" # 二、日志管理 # 查看 journal 占用空间 sudo journalctl --disk-usage # 清理日志(保留最近2周/500M) sudo journalctl --vacuum-time=2weeks sudo journalctl --vacuum-size=500M # 手动轮转日志 sudo journalctl --rotate # 三、旧日志文件操作 # 实时查看认证日志(Ubuntu) tail -f /var/log/auth.log # 实时查看认证日志(CentOS) tail -f /var/log/secure # 四、日志防篡改验证 sudo journalctl --setup-keys > /etc/journal-seal-key sudo chmod 600 /etc/journal-seal-key sudo journalctl --verify --verify-key=$(cat /etc/journal-seal-key) 2.2 内存管理:systemd-oomd systemd-oomd 是 systemd 提供的内存不足(OOM)守护进程,用于在系统内存紧张时主动终止进程, 防止系统完全卡死。听起来有点"残忍",不过总比系统彻底死机要好。 工作原理: 内存监控:实时监控系统内存使用情况和内存压力 智能选择:基于 cgroup 层次结构和内存使用量选择要终止的进程 用户空间保护:优先终止用户空间进程,保护系统关键服务 渐进式处理:逐步释放内存,避免过度 kill 进程 配置示例: nix # NixOS 配置 systemd.oomd.enable = true; systemd.oomd.extraConfig = '' [OOM] DefaultMemoryPressureLimitSec=20s DefaultMemoryPressureLimit=60% ''; 配置文件路径:/etc/systemd/oomd.conf 监控与调试: bash # 查看 oomd 状态 systemctl status systemd-oomd # 内存压力信息 cat /proc/pressure/memory # 查看 oomd 日志 journalctl -u systemd-oomd -f # 内存使用统计 systemctl status user@$(id -u).service 2.3 DNS 解析:systemd-resolved systemd-resolved 提供统一的 DNS 解析服务,支持 DNSSEC 验证、DNS over TLS 等现代 DNS 特性。名字是长了点,不过功能倒是挺全面的,基本上把 DNS 解析这件事包圆了。 主要功能: 统一接口:为系统提供单一的 DNS 解析入口 本地缓存:缓存 DNS 查询结果,提高解析速度 DNSSEC 支持:验证 DNS 响应的真实性 隐私保护:支持 DNS over TLS(DoT), 但截止目前(2025 年)尚未支持 DNS over HTTPS(DoH). 配置方法: nix # 启用 systemd-resolved services.resolved.enable = true; # 配置 DNS 服务器 networking.nameservers = [ "8.8.8.8" "1.1.1.1" # IPv4 "2001:4860:4860::8888" "2606:4700:4700::1111" # IPv6 ]; # 高级配置 services.resolved.extraConfig = '' [Resolve] DNSSEC=yes DNSOverTLS=yes Cache=yes ''; 配置文件路径:/etc/systemd/resolved.conf 使用命令: bash # DNS 状态查看 resolvectl status # DNS 查询测试 resolvectl query example.com resolvectl query -t AAAA ipv6.google.com # 缓存管理 resolvectl flush-caches resolvectl statistics # DNS 服务器状态 resolvectl dns 2.4 时间同步:systemd-timesyncd systemd-timesyncd 是轻量级 NTP 客户端,负责保持系统时间与网络时间服务器同步。功能简单直接,就是确保你的系统时间不会跑偏,避免出现"时间穿越"的尴尬情况。 功能特点: 轻量级设计:相比完整 NTP 服务占用更少资源 自动同步:定期与时间服务器同步 SNTP 协议:使用简单网络时间协议 systemd 集成:与 systemd 服务管理深度集成 NixOS 配置: nix # 启用时间同步 services.timesyncd.enable = true; # 配置 NTP 服务器 services.timesyncd.servers = [ "pool.ntp.org" "time.google.com" "ntp.aliyun.com" ]; 配置文件路径:/etc/systemd/timesyncd.conf 时间同步管理: bash # 时间状态查看 timedatectl status timedatectl timesync-status # 手动控制 timedatectl set-ntp true # 启用 NTP timedatectl set-timezone Asia/Shanghai # 查看同步日志 journalctl -u systemd-timesyncd -f # 时间精度检查 chronyc tracking # 如果安装了 chrony 3. 设备管理:udev 与 systemd-udevd udev 是 Linux 用户空间的设备管理员,负责处理内核的设备事件,创建节点并设置权限。在现代 systemd 系统中,udev 功能由 systemd-udevd 守护进程实现,它是 systemd 生态系统的重要组成部分。 3.1 udev 与 systemd-udevd 3.1.1 udev 设备管理框架 udev 是 Linux 内核的用户空间设备管理框架,负责处理内核的设备事件并管理 /dev 目录下的设备节点。 udev 的核心功能: 动态设备管理:当硬件设备插入或移除时,自动创建设备节点 设备命名:提供一致的设备命名规则,如 /dev/disk/by-uuid/、/dev/input/by-id/ 权限控制:根据设备类型和用户需求设置适当的设备权限 规则系统:通过规则文件实现复杂的设备处理逻辑 udev 的工作原理: 内核检测到硬件变化,通过 netlink socket 发送 uevent 到用户空间 udev 守护进程接收 uevent,解析设备属性 根据规则文件匹配设备,执行相应的动作(创建设备节点、设置权限等) 3.1.2 systemd-udevd 实现 在现代 systemd 系统中,udev 用户空间的功能由 systemd-udevd 守护进程实现,它是 systemd 生态系统的重要组成部分。 systemd-udevd 的优势: systemd 集成:作为 systemd 服务运行,享受 systemd 的服务管理、日志记录、依赖管理等功能 性能优化:相比传统的 udevd,systemd-udevd 在启动速度和资源使用上有所优化 统一管理:与 systemd 的其他组件(如 systemd-logind)深度集成,提供统一的设备权限管理 systemd-udevd 服务管理: bash # 查看服务状态 systemctl status systemd-udevd # 重启服务 sudo systemctl restart systemd-udevd # 查看服务日志 journalctl -u systemd-udevd -f 3.1.3 工作流程 完整的设备管理流程如下: 硬件检测:内核检测到硬件变化(插入、移除、状态改变) 事件发送:内核通过 netlink socket 发送 uevent 到用户空间 事件接收:systemd-udevd 接收 uevent,解析设备属性 规则匹配:根据规则文件(/run/current-system/sw/lib/udev/rules.d/(NixOS)或/usr/lib/udev/rules.d/(传统发行版)、/etc/udev/rules.d/)匹配设备 动作执行:执行匹配规则中定义的动作(RUN 脚本、设置 OWNER/GROUP/MODE、创建 symlink、设置权限) systemd 集成:通知 systemd,可能触发 device units 3.1.4 配置示例 基本规则示例: ini # /etc/udev/rules.d/90-mydevice.rules SUBSYSTEM=="input", ATTRS{idVendor}=="abcd", ATTRS{idProduct}=="1234", MODE="660", GROUP="input", TAG+="uaccess" 规则说明: SUBSYSTEM=="input":匹配输入设备子系统 ATTRS{idVendor}=="abcd":匹配厂商 ID ATTRS{idProduct}=="1234":匹配产品 ID MODE="660":设置设备权限为 660 GROUP="input":设置设备组为 input TAG+="uaccess":添加 uaccess 标签,让 systemd-logind 接管设备权限 高级规则示例: ini # /etc/udev/rules.d/99-custom-storage.rules # 为特定 USB 存储设备创建符号链接 SUBSYSTEM=="block", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", SYMLINK+="myusb" # 为特定网卡设置持久化名称 SUBSYSTEM=="net", ATTRS{address}=="aa:bb:cc:dd:ee:ff", NAME="eth0" # 为特定设备运行自定义脚本 SUBSYSTEM=="usb", ATTRS{idVendor}=="abcd", RUN+="/usr/local/bin/my-device-handler.sh" TAG+="uaccess" 是现代桌面用来让 systemd-logind 接管设备权限与 session ACL(由 logind 配置),确保只有当前活动会话能访问输入、音频、GPU 等设备。 3.2 设备权限与 ACL 现代 systemd + logind 使用 udev tag uaccess 或 seat 标签来由 logind 把设备 ACL 授予当前的登录 session。具体流程: systemd-udevd 创建 /dev/input/eventX 并打上 TAG+="uaccess". systemd-logind 对应的 PAM/session 系统会把该设备的 ACL 授予当前会话的用户,这样运行在会话内的 Wayland compositor 与其子进程可以访问设备。 检查设备权限分配: bash # 查看某设备的 udev 属性 $ udevadm info -a -n /dev/input/event5 # 实时监控 udev 事件 $ sudo udevadm monitor --udev --property # 查看 seat 状态与 ACL $ loginctl seat-status seat0 # 或 $ loginctl show-session <id> -p Remote -p Display -p Name 3.3 故障排查 场景:插入外接键盘后,Wayland 会话收不到键盘事件(键盘无效) 排查步骤: 检查 systemd-udevd 服务状态: bash systemctl status systemd-udevd 在主机上用 udevadm monitor 插入键盘,观察是否有 udev 事件被触发: bash sudo udevadm monitor --udev 检查 /dev/input/ 是否生成新节点:ls -l /dev/input/by-id。 用 udevadm info -a -n /dev/input/eventX 查看该设备的属性,确认 TAG 是否包含uaccess 或 seat. 使用 loginctl seat-status seat0 看设备是否分配给当前会话。若没有,可能是 PAM/session 未正确建立或 udev 规则没有打上 tag。 检查 systemd-udevd 的日志: bash journalctl -b -u systemd-udevd journalctl -k | grep -i udev 临时解决:用 chmod/chown 修改设备权限验证是否恢复(不建议长期采用): bash sudo chown root:input /dev/input/eventX sudo chmod 660 /dev/input/eventX 永久修复:在 /etc/udev/rules.d/ 中添加规则确保 TAG+="uaccess" 或正确的OWNER/GROUP。然后 udevadm control --reload-rules && sudo udevadm trigger。 注意:NixOS 下直接编辑 /etc/udev/rules.d 可能是临时的(Nix 管理的文件会被系统重建覆盖),正确做法是在 configuration.nix 中配置 services.udev.extraRules 或把规则放在environment.etc 并由 Nix 管理。 配置文件路径: /etc/udev/rules.d/:系统管理员自定义规则(优先级最高) /run/current-system/sw/lib/udev/rules.d/(NixOS)或 /usr/lib/udev/rules.d/(传统发行版):软件包提供的默认规则 4. D-Bus 系统总线 - 应用间通信的主要通道 D-Bus 是 Linux 系统中主流的进程间通信(IPC)机制,旨在解决不同进程(尤其是桌面应用、系统服务)间的高效、安全通信问题,广泛用于 GNOME、KDE 等桌面环境及系统服务管理(如 systemd)。它本质是 “消息总线”,通过中心化的 “总线守护进程” 实现多进程间的消息路由。名字虽然有点奇怪, 功能倒是挺实在的。 4.1 D-Bus 项目背景 D-Bus 并非 systemd 社区的项目,而是 freedesktop.org 的独立项目。D-Bus 在 systemd 出现之前就已经存在,是 Linux 桌面环境标准化进程间通信的重要基础设施。 D-Bus 与 systemd 的关系: 独立项目:D-Bus 由 freedesktop.org 维护,有自己的发布周期和开发团队 深度集成:systemd 将 D-Bus 作为核心依赖,深度集成到其架构中 服务管理:systemd 负责启动和管理 D-Bus 守护进程(dbus-daemon) 统一接口:systemd 通过 D-Bus 提供统一的服务管理接口 systemd 本身就是一个 D-Bus 服务,我们在使用 systemctl 命令与 systemd 交互时,实际上就是 D-Bus 与 org.freedesktop.systemd1 通信。 4.2 关键概念 D-Bus 通过 “对象 - 接口” 模型封装功能,以下结合 systemd1 与 logind1 的真实定义,对应核心概念: 概念 定义与作用 示例(systemd1/logind1) 总线(Bus) 消息传输的 “高速公路”,分系统 / 会话两类 系统总线 /var/run/dbus/system_bus_socket(systemd1/logind1 唯一使用的总线) 服务名(Name) 服务端在总线上的 “身份证”,唯一可请求 org.freedesktop.systemd1(systemd 服务名)、org.freedesktop.login1(logind 服务名) 对象(Object) 服务端功能的 “实例载体”,有唯一路径 /org/freedesktop/systemd1(systemd1 根对象)、/org/freedesktop/login1(logind1 根对象) 接口(Interface) 定义对象的 “功能契约”(方法、信号、属性) org.freedesktop.systemd1.Manager(systemd1 核心接口)、org.freedesktop.login1.Manager(logind1 核心接口) 方法(Method) 客户端可主动调用的 “同步功能”(有请求有返回) systemd1 的 StartUnit(启动系统单元,如 nginx.service)、logind1 的 ListSessions(查询所有活跃用户会话) 信号(Signal) 服务端主动发送的 “异步通知”(无返回) systemd1 的 UnitActiveChanged(单元状态变化,如 nginx 从 inactive 变为 active)、logind1 的 SessionNew(新用户登录创建会话) 属性(Property) 对象的 “状态数据”,支持读取 / 写入 systemd1 的 ActiveUnits(所有活跃系统单元列表)、logind1 的 CanPowerOff(当前系统是否允许关机,布尔值) 可使用 busctl list 查看系统中的所有 D-Bus 对象: bash # 所有 system bus 对象 › busctl --system list --no-pager | grep org. org.blueman.Mechanism - - - (activatable) - - - org.bluez 1421 bluetoothd root :1.6 bluetooth.service - - org.bluez.mesh - - - (activatable) - - - org.freedesktop.Avahi 1420 avahi-daemon avahi :1.7 avahi-daemon.service - - org.freedesktop.DBus 1 systemd root - init.scope - - org.freedesktop.Flatpak.SystemHelper - - - (activatable) - - - org.freedesktop.GeoClue2 - - - (activatable) - - - org.freedesktop.PolicyKit1 2216 polkitd polkituser :1.22 polkit.service - - org.freedesktop.RealtimeKit1 2539 rtkit-daemon root :1.41 rtkit-daemon.service - - org.freedesktop.UDisks2 2492 udisksd root :1.31 udisks2.service - - org.freedesktop.home1 - - - (activatable) - - - org.freedesktop.hostname1 - - - (activatable) - - - org.freedesktop.import1 - - - (activatable) - - - org.freedesktop.locale1 - - - (activatable) - - - org.freedesktop.login1 1504 systemd-logind root :1.8 systemd-logind.service - - org.freedesktop.machine1 - - - (activatable) - - - org.freedesktop.network1 1292 systemd-network systemd-network :1.3 systemd-networkd.service - - org.freedesktop.oom1 934 systemd-oomd systemd-oom :1.1 systemd-oomd.service - - org.freedesktop.portable1 - - - (activatable) - - - org.freedesktop.resolve1 1293 systemd-resolve systemd-resolve :1.0 systemd-resolved.service - - org.freedesktop.systemd1 1 systemd root :1.4 init.scope - - org.freedesktop.sysupdate1 - - - (activatable) - - - org.freedesktop.timedate1 - - - (activatable) - - - org.freedesktop.timesync1 1148 systemd-timesyn systemd-timesync :1.2 systemd-timesyncd.service - - org.opensuse.CupsPkHelper.Mechanism - - - (activatable) - - - # 所有 session bus 对象 › busctl --user list --no-pager | grep org. ... org.fcitx.Fcitx-0 76699 fcitx5 ryan :1.284 user@1000.service - - org.fcitx.Fcitx5 76699 fcitx5 ryan :1.282 user@1000.service - - org.freedesktop.DBus 2127 systemd ryan - user@1000.service - - org.freedesktop.FileManager1 - - - (activatable) - - - org.freedesktop.Notifications 3539 .mako-wrapped ryan :1.81 user@1000.service - - org.freedesktop.ReserveDevice1.Audio0 2542 wireplumber ryan :1.50 user@1000.service - - org.freedesktop.ReserveDevice1.Audio1 2542 wireplumber ryan :1.50 user@1000.service - - org.freedesktop.ScreenSaver 2192 niri ryan :1.9 user@1000.service - - org.freedesktop.a11y.Manager 2192 niri ryan :1.13 user@1000.service - - org.freedesktop.impl.portal.PermissionStore 2410 .xdg-permission ryan :1.28 user@1000.service - - org.freedesktop.impl.portal.Secret - - - (activatable) - - - org.freedesktop.impl.portal.desktop.gnome - - - (activatable) - - - org.freedesktop.impl.portal.desktop.gtk 2475 .xdg-desktop-po ryan :1.33 user@1000.service - - org.freedesktop.portal.Desktop 2350 .xdg-desktop-po ryan :1.26 user@1000.service - - org.freedesktop.portal.Documents 2428 .xdg-document-p ryan :1.30 user@1000.service - - org.freedesktop.portal.Fcitx 76699 fcitx5 ryan :1.283 user@1000.service - - org.freedesktop.portal.Flatpak - - - (activatable) - - - org.freedesktop.portal.IBus 76699 fcitx5 ryan :1.285 user@1000.service - - org.freedesktop.secrets 2161 .gnome-keyring- ryan :1.55 session-1.scope 1 - org.freedesktop.systemd1 2127 systemd ryan :1.1 user@1000.service - - ... 4.3 系统总线与会话总线 总线类型 作用场景 典型用途 运行用户 系统总线(System Bus) 系统级服务通信 systemd1 单元管理(启动 / 停止服务)、logind1 用户会话 / 电源控制(关机 / 重启) root(特权) 会话总线(Session Bus) 单个用户会话内的应用通信 桌面应用交互(如窗口切换、通知) 当前登录用户 4.4 D-Bus 的三类角色 总线守护进程(dbus-daemon) 架构的 “中枢”,每个总线对应一个守护进程,核心职责: 管理进程的连接(如验证 普通用户 是否有权调用 logind1 的 PowerOff 方法); 路由消息(将客户端请求的 “启动 nginx 服务” 转发给 systemd1); 维护服务注册表(记录 org.freedesktop.login1 与 logind 进程的映射关系)。 服务端(Service) 提供功能的进程(如 systemd 进程、logind 进程),核心操作: 向总线注册 “服务名”(systemd1 注册 org.freedesktop.systemd1,logind1 注册org.freedesktop.login1,均为唯一标识); 暴露 “对象” 和 “接口”(如 systemd1 暴露 /org/freedesktop/systemd1 对象与org.freedesktop.systemd1.Manager 接口),供客户端调用。 客户端(Client) 调用服务的进程(如 systemctl 命令、桌面电源菜单),核心操作: 连接系统总线后,通过服务名(如 org.freedesktop.login1)找到 logind 服务; 调用服务端暴露的方法(如通过 logind1 的 ListSessions 查询当前用户会话),或订阅信号(如监听 systemd1 的 UnitActiveChanged 单元状态变化)。 4.5 常见操作示例 下面我们通过一些命令来演示 D-Bus 总线的用途: bash # 模拟 `systemctl status dbus` 的功能 busctl --system --json=pretty call \ org.freedesktop.systemd1 \ /org/freedesktop/systemd1/unit/dbus_2eservice \ org.freedesktop.DBus.Properties GetAll s org.freedesktop.systemd1.Unit # 模拟 `systemctl stop sshd` sudo gdbus call --system \ --dest org.freedesktop.systemd1 \ --object-path /org/freedesktop/systemd1 \ --method org.freedesktop.systemd1.Manager.StopUnit \ "sshd.service" "replace" # 模拟 `systemctl start sshd` sudo gdbus call --system \ --dest org.freedesktop.systemd1 \ --object-path /org/freedesktop/systemd1 \ --method org.freedesktop.systemd1.Manager.StartUnit \ "sshd.service" "replace" # 模拟 `notify-send "The Summary" "Here’s the body of the notification"` nix shell nixpkgs#glib gdbus call --session \ --dest org.freedesktop.Notifications \ --object-path /org/freedesktop/Notifications \ --method org.freedesktop.Notifications.Notify \ my_app_name \ 42 \ gtk-dialog-info \ "The Summary" \ "Here’s the body of the notification" \ [] \ {} \ 5000 # 获取当前时区 busctl get-property org.freedesktop.timedate1 /org/freedesktop/timedate1 \ org.freedesktop.timedate1 Timezone # 查询主机名 busctl get-property org.freedesktop.hostname1 /org/freedesktop/hostname1 \ org.freedesktop.hostname1 Hostname 4.6 调试与监控命令 bash # 看 systemctl 与 systemd 的完整交互(method-call + signal) sudo busctl monitor --system | grep 'org.freedesktop.systemd1' # 或者使用 --match 过滤,但这需要提前知道 interface 的全名 sudo busctl monitor --match='interface=org.freedesktop.systemd1.Manager' # 跟 busctl monitor 功能几乎完全一致,也可通过 match rule 过滤 sudo dbus-monitor --system "interface='org.freedesktop.systemd1.Manager'" # gdbus 只监听 signals,只能用来调试「服务有没有正确发出 signal」 sudo gdbus monitor --system -d org.freedesktop.systemd1.Manager 4.7 D-Bus 的权限管控 4.7.1 D-Bus 的原生权限管控机制 D-Bus 本身具备多层权限管控能力,从总线接入、消息路由到敏感操作授权,形成了系统级的基础安全保障,核心机制包括: 总线配置文件(静态规则管控) 通过 XML 配置文件定义细粒度访问规则,实现对 “谁能访问哪些服务 / 方法” 的静态限制。例如: 系统总线的服务级规则(如 /etc/dbus-1/system.d/org.freedesktop.login1.conf)可限制普通用户调用 org.freedesktop.login1.Manager.PowerOff(关机方法); 全局规则(如 /etc/dbus-1/system.conf)可限定仅 root 或 dbus 组用户访问org.freedesktop.systemd1(systemd 服务)的核心接口。 规则遵循 “deny 优先级高于 allow、服务级规则高于全局规则” 的逻辑,从总线层面直接拦截未授权请求。 PolicyKit(动态授权管控) 针对静态规则无法覆盖的动态场景(如普通用户临时需要执行敏感操作),D-Bus 集成 PolicyKit(现称 polkit)实现动态授权。系统服务(如 logind1、systemd1)会在/run/current-system/sw/share/polkit-1/actions/(NixOS 中)或/run/current-system/sw/share/polkit-1/actions/(NixOS)或/usr/share/polkit-1/actions/(传统发行版中)定义 “可授权动作”,例如org.freedesktop.login1.power-off(对应 logind1 的关机方法): 普通用户调用时,会触发认证流程(如输入管理员密码),认证通过后临时获得授权; 活跃控制台用户可直接授权,无需额外验证,兼顾安全性与易用性。 连接层基础隔离 D-Bus 总线套接字(如系统总线 /var/run/dbus/system_bus_socket)默认仅开放 root 和dbus 组用户的读写权限,普通进程需通过 dbus-daemon 认证后才能建立连接。同时,每个连接会被分配唯一 ID(如 :1.42),并与进程的 PID/UID/GID 绑定,防止身份伪造与未授权接入。 4.7.2 Flatpak 对 D-Bus 权限的细粒度管控 在现代 Linux 桌面中,若需将商业软件等非信任应用运行在沙箱中,同时保障 “必要 D-Bus 交互不中断、越权访问被阻断”,Flatpak 采用 “底层沙箱隔离 + 上层代理过滤” 的双层方案 —— 其中bubblewrap 是 Flatpak 依赖的底层沙箱工具,负责环境隔离;xdg-dbus-proxy 是上层过滤组件,负责 D-Bus 细粒度管控,两者协同实现完整安全隔离: 4.7.2.1 底层基础隔离:bubblewrap 的 “socket 隐藏与代理挂载” Flatpak 以 bubblewrap(简称 bwrap)为底层沙箱基础,利用其 bind mount 和user namespace 能力完成环境初始化,核心目标是切断沙箱应用与宿主 D-Bus 总线的直接联系: 隐藏宿主 socket:bubblewrap 会屏蔽宿主的 D-Bus 总线套接字(如不将/var/run/dbus/system_bus_socket 挂载进沙箱),避免应用绕过管控直接访问宿主总线; 挂载代理 socket:同时,bubblewrap 会将 xdg-dbus-proxy 在宿主侧预先创建的 私有代理 socket,通过 bind mount 挂载到沙箱内的默认 D-Bus socket 路径(如沙箱内的/var/run/dbus/system_bus_socket)。 此时沙箱应用感知到的 “D-Bus 总线”,实际是 xdg-dbus-proxy 提供的代理接口,无法直接接触宿主真实总线。 4.7.2.2 上层规则过滤:xdg-dbus-proxy 的 “白名单校验” xdg-dbus-proxy 作为 Flatpak 内置的 D-Bus 代理组件,会随沙箱应用启动,加载 Flatpak 根据应用权限声明自动生成的过滤规则(粒度远细于 D-Bus 原生静态配置),例如: bash --talk=org.freedesktop.portal.FileChooser # 允许调用文件选择门户服务 --talk=org.freedesktop.Notifications # 允许发送桌面通知 --deny=org.freedesktop.systemd1 # 拒绝访问 systemd 服务 --deny=org.freedesktop.login1.Manager.PowerOff # 拒绝调用关机方法 这些规则可精确到 “服务名 + 接口 + 方法 + 对象路径”,弥补 D-Bus 原生配置在沙箱场景下 “动态性不足、粒度较粗” 的局限。 4.7.2.3 消息流转:代理的 “校验 - 转发” 逻辑 沙箱应用无需修改代码,会默认连接沙箱内的 “代理 socket”,所有 D-Bus 消息(方法调用、信号订阅)均需经过 xdg-dbus-proxy 的校验: 若目标服务 / 方法在白名单内(如 org.freedesktop.portal.FileChooser.OpenFile),代理会将消息转发至宿主 D-Bus 总线,并把返回结果回传应用; 若目标不在白名单内(如 org.freedesktop.login1.Manager.PowerOff),代理直接返回AccessDenied 错误,不向宿主总线转发任何消息,彻底阻断越权访问。 总结 本文深入探讨了 systemd 核心功能及其生态系统,从服务管理到各个专门化组件: systemd 核心功能:作为 PID 1 的服务管理器,专注于服务管理、依赖关系管理、资源控制和系统状态管理 systemd 生态系统服务:包括日志管理(journald)、内存管理(oomd)、DNS 解析 (resolved)、时间同步(timesyncd)、设备管理(udevd)等 设备管理:udev 规则和设备权限分配,通过 systemd-udevd 确保硬件设备正确识别和访问 D-Bus 系统总线:进程间通信机制,支持系统服务和桌面应用的交互 虽然 systemd 的争议一直存在,但不可否认的是,它确实让系统管理变得更加统一和高效。掌握了这些组件的使用方法,你在面对各种系统问题时就不会那么手足无措了。 下一篇文章我们会聊聊桌面会话和图形渲染,看看用户登录后系统是如何把漂亮的桌面呈现给你的。

2025/10/19
articleCard.readMore

Linux 桌面系统故障排查指南(一) - 系统启动与安全框架

AI 创作声明:本系列文章由笔者借助 ChatGPT, Kimi K2, 豆包和 Cursor 等 AI 工具创作,有很大篇幅的内容完全由 AI 在我的指导下生成。如有错误,还请指正。 前言 本文将简要介绍 Linux 桌面系统的启动机制,从 UEFI 引导到内核加载,从 initramfs 到 systemd 服务启动,再到桌面环境加载。同时还会探讨系统的安全框架,了解 PAM、PolicyKit 等组件如何保护系统安全。 1. 系统启动流程 启动的四个关键阶段 Linux 桌面系统的启动过程可以分为以下几个主要阶段: 固件阶段:UEFI 固件初始化硬件 引导加载器阶段:加载内核和 initramfs 内核阶段:硬件探测和驱动加载 initramfs 阶段:准备根文件系统 用户空间阶段:systemd 接管系统管理 UEFI:系统启动的起点 现代系统普遍使用 UEFI 固件 代替 BIOS。UEFI 初始化硬件后,从 EFI System Partition (ESP) 中加载启动管理器。 NixOS 默认使用 grub,启用 Secure Boot(lanzaboote) 时需改用systemd-boot. systemd-boot 的全局配置是 /boot/loader/loader.conf,具体的启动项配置需要分类讨论: Type 1:手动配置 (Boot Loader Specification Type #1) 配置方式:/loader/entries/*.conf,位于 EFI 系统分区(ESP)或 Extended Boot Loader Partition(XBOOTLDR)下 特点: 可自定义启动项名称、内核参数、initrd 等 描述 Linux 内核及其 initrd,也可以描述任意 EFI 可执行文件 包括 fallback / rescue 内核 示例: ini title NixOS Linux linux /vmlinuz-linux initrd /initrd-linux.img options root=UUID=xxxx rw Type 2:统一内核镜像 (Boot Loader Specification Type #2) 配置方式:将 EFI 格式的 UKI 镜像放在 ESP 分区的 /EFI/Linux/ 下即可 工作原理: systemd-boot 在启动时扫描 ESP 的 /EFI/Linux/ 目录 systemd-boot 会自动将扫描到的内核镜像添加到启动菜单,无需单独的 .conf 文件 特点: 免配置,自动出现在启动菜单中 vmlinuz-linux, initrd 跟 cmdline 等信息被统一打包成一个 EFI 镜像,一个镜像就包含了系统启动需要的所有数据,更方面简洁。 其他自动识别的启动项 Microsoft Windows EFI boot manager(如果已安装) Apple macOS boot manager(如果已安装) EFI Shell 可执行文件(如果已安装) 「Reboot Into Firmware Interface」选项(如果 UEFI 固件支持) Secure Boot 变量注册(如果固件处于 setup 模式,且 ESP 提供了相关文件) 常用命令: efibootmgr -v:查看 / 修改固件启动顺序 bootctl status:检查 systemd-boot 安装与 ESP 状态 bootctl list:列出启动条目 ukify inspect /boot/EFI/Linux/nixos-xxx.efi: 查看 efi 镜像中包含的信息 示例: bash # 查看固件启动顺序 $ nix run nixpkgs#efibootmgr -v BootCurrent: 0000 Timeout: 0 seconds BootOrder: 0000,0004 Boot0000* NixOS HD(1,GPT,34286f3b-d4df-456d-bf7a-eb67f2bf1a72,0x1000,0x12b000)/EFI\BOOT\BOOTX64.EFI ... Boot0004* Windows Boot Manager HD(1,GPT,34286f3b-d4df-456d-bf7a-eb67f2bf1a72,0x1000,0x12b000)/\EFI\Microsoft\Boot\bootmgfw.efi0000424f # 检查 systemd-boot 安装与 ESP 状态 $ bootctl status System: Firmware: UEFI 2.80 (American Megatrends 5.27) Firmware Arch: x64 Secure Boot: enabled (user) TPM2 Support: yes Measured UKI: yes Boot into FW: supported Current Boot Loader: Product: systemd-boot 257.7 Features: ✓ Boot counting ✓ Menu timeout control ✓ One-shot menu timeout control ✓ Default entry control ✓ One-shot entry control ✓ Support for XBOOTLDR partition ✓ Support for passing random seed to OS ✓ Load drop-in drivers ✓ Support Type #1 sort-key field ✓ Support @saved pseudo-entry ✓ Support Type #1 devicetree field ✓ Enroll SecureBoot keys ✓ Retain SHIM protocols ✓ Menu can be disabled ✓ Multi-Profile UKIs are supported ✓ Boot loader set partition information Partition: /dev/disk/by-partuuid/34286f3b-d4df-456d-bf7a-eb67f2bf1a72 Loader: └─EFI/BOOT/BOOTX64.EFI Current Entry: nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi ... Available Boot Loaders on ESP: ESP: /boot (/dev/disk/by-partuuid/34286f3b-d4df-456d-bf7a-eb67f2bf1a72) File: ├─/EFI/systemd/systemd-bootx64.efi (systemd-boot 257.7) └─/EFI/BOOT/BOOTX64.EFI (systemd-boot 257.7) ... Default Boot Loader Entry: type: Boot Loader Specification Type #2 (.efi) title: NixOS Xantusia 25.11.20250830.d7600c7 (Linux 6.16.4) (Generation 848, 2025-09-01) id: nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi source: /boot//EFI/Linux/nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi (on the EFI System Partition) sort-key: lanza version: Generation 848, 2025-09-01 linux: /boot//EFI/Linux/nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi options: init=/nix/store/gaj3sp3hrzjhp59bvyxhc8flg5s6iimg-nixos-system-ai-25.11.20250830.d7600c7/init nvidia-drm.fbdev=1 root=fstab loglevel=4 lsm=landlock,yama,bpf nvidia-drm.modeset=1 nvidia-drm.fbdev=1 nvidia.NVreg_PreserveVideoMemoryAllocations=1 nvidia.NVreg_OpenRmEnableUnsupportedGpus=1 # 查看上述启动项中 uki efi 文件的内容 $ nix shell nixpkgs#systemdUkify $ ukify inspect /boot/EFI/Linux/nixos-generation-848-jattq2uvv2snrigcxtdcxelgaawdb3s6lar3ualze77id46h5adq.efi .osrel: size: 141 bytes sha256: e486dea4910eb9262efc47464f533f96093293d37c3d25feb954c098865a4be6 text: ID=lanza PRETTY_NAME=NixOS Xantusia 25.11.20250830.d7600c7 (Linux 6.16.4) (Generation 848, 2025-09-01) VERSION_ID=Generation 848, 2025-09-01 # 启动内核时使用的内核命令行参数 .cmdline: size: 284 bytes sha256: 7f94ffed08359eb1d2749176eba57e085113f46208702a8c0251376d734f19ce text: init=/nix/store/gaj3sp3hrzjhp59bvyxhc8flg5s6iimg-nixos-system-ai-25.11.20250830.d7600c7/init nvidia-drm.fbdev=1 root=fstab loglevel=4 lsm=landlock,yama,bpf nvidia-drm.modeset=1 nvidia-drm.fbdev=1 nvidia.NVreg_PreserveVideoMemoryAllocations=1 nvidia.NVreg_OpenRmEnableUnsupportedGpus=1 # initramfs 内容的引用,实际镜像位于 ESP 的 /EFI/nixos/initrd-*.efi .initrd: size: 81 bytes sha256: 26d9b1f52806c48c6287272cb26b8a640b62d55f09149abf3415c76c38e0b56e # 内核映像(vmlinuz)的引用,实际镜像位于 ESP 的 /EFI/nixos/kernel-*.efi .linux: size: 81 bytes sha256: 41ff83e4cae160fb9ce55392943e6d06dbf9f37b710bf719f7fe2c28ec312be5 内核启动后,会探测 CPU、内存、PCI、USB、ACPI 等硬件,加载关键驱动,然后挂载 initramfs 并执行 option 中指定的 init 程序。 观察方法: bash # 查看内核早期日志 sudo dmesg --level=err,warn,info | less # 查看本次启动的完整日志 journalctl -b initramfs 阶段 initramfs(Initial RAM File System)是一个临时的根文件系统,在真正的根文件系统挂载之前提供必要的功能。它在启动阶段被加载到 RAM 中并被挂载为根目录。 initramfs 阶段的主要职责: 硬件检测与驱动加载: 检测存储设备(SATA、NVMe、USB 等) 加载必要的存储驱动模块 识别网络设备(如果需要网络启动) 存储设备准备: 解密 LUKS 加密分区 激活 LVM 逻辑卷 处理 RAID 阵列 挂载临时文件系统 根文件系统挂载: 根据内核参数 root= 找到根分区 挂载根文件系统到 /new_root 执行 switch_root 切换到真正的根文件系统 启动用户空间: 执行 /sbin/init(通常是 systemd) 在 NixOS 中,init 程序是 /nix/store 中的一个 Shell 脚本,它首先完成一些必要的初始化工作,之后才启动 systemd. 常见故障与排查: 找不到根分区:检查 cat /proc/cmdline 的 root= 参数与 blkid 输出是否一致 缺少驱动模块:确保 NixOS 配置包含所需模块:boot.initrd.kernelModules = [ "nvme" "dm_mod" ]; LUKS 解密失败:检查密码输入或密钥文件配置 LVM 激活失败:确认 LVM 配置和卷组状态 排查步骤: 编辑内核 cmdline,添加 init=/bin/sh 或 break=mount 进入 initramfs shell 运行 lsblk、blkid 确认设备 查看 dmesg 中的磁盘或 LVM 错误 检查 /proc/cmdline 中的启动参数 2. 启动故障排查 启动故障排查流程 flowchart LR A[系统无法启动] --> B{能否进入 UEFI/BIOS?} B -->|否| C[硬件问题 检查电源、内存、CPU] B -->|是| D{能否看到启动菜单?} D -->|否| E[引导加载器问题 检查 ESP 分区和启动项] D -->|是| F{能否选择启动项?} F -->|否| G[启动项配置错误 检查 bootctl 配置] F -->|是| H{内核能否加载?} H -->|否| I[内核或 initramfs 问题 检查内核参数和驱动] H -->|是| J{能否进入 initramfs?} J -->|否| K[initramfs 问题 检查根分区和文件系统] J -->|是| L{能否挂载根分区?} L -->|否| M[文件系统或加密问题 检查 LUKS 和 LVM] L -->|是| N{systemd 能否启动?} N -->|否| O[用户空间问题 检查 systemd 服务] N -->|是| P[启动成功] 常见启动问题:症状与解决方案 在系统启动过程中,可能会遇到各种问题。以下是按启动阶段分类的常见问题及排查方法: 2.2.1 固件和引导加载器问题 问题症状: 系统无法启动,停留在固件界面 显示 “No bootable device” 错误 启动菜单不显示或显示异常 排查步骤: 使用 USB 启动盘进入 LiveOS, 进行如下检查: bash # 检查 UEFI 设置 efibootmgr -v # 检查 ESP 分区状态 bootctl status # 验证启动项配置 bootctl list 2.2.2 内核和 initramfs 问题 问题症状: 内核 panic 或无法加载 initramfs 阶段卡住 找不到根分区 排查步骤: bash # 进入 initramfs shell 进行调试 # 在内核参数中添加:init=/bin/sh 或 break=mount # 检查设备识别 lsblk blkid # 查看内核日志 dmesg | grep -i error # 检查文件系统完整性 fsck /dev/sdX 启动性能优化 2.3.1 启动时间分析 bash # 使用 systemd-analyze 分析启动时间 systemd-analyze systemd-analyze blame systemd-analyze critical-chain # 生成启动时间报告 systemd-analyze plot > boot-time.svg 这些工具可以帮助你分析系统启动性能: systemd-analyze 显示总启动时间,包括内核和用户空间的启动耗时 systemd-analyze blame 按耗时排序显示各服务启动时间,找出最耗时的服务 systemd-analyze critical-chain 显示关键路径分析,找出阻塞启动的服务链 systemd-analyze plot 生成启动时间图表,可视化各服务的启动顺序和耗时 识别到启动阶段的性能瓶颈后,就能据此优化服务依赖关系,加快启动速度。 2.3.2 启动优化策略 优化启动速度可以从多个层面入手: 硬件层面 使用 SSD 存储是最直接有效的优化方法。固态硬盘的随机读写性能远超机械硬盘,能显著减少文件系统访问延迟。启动时间通常可减少 50-80%,特别是对于大量小文件读取的场景。适用于所有系统,特别是启动时间较长的系统。 内核层面 启用内核并行初始化可以提升启动速度。现代内核支持并行初始化硬件设备,减少串行等待时间。通过内核参数如 initcall_debug 和 acpi=noirq 等可以优化启动流程,减少硬件初始化时间。 服务层面 优化 systemd 服务依赖关系可以减少启动延迟。减少不必要的服务依赖,避免串行启动造成的延迟。使用 systemctl list-dependencies 分析依赖关系,移除不必要的依赖,减少服务启动等待时间, 提升并行启动效率。 启动流程 使用 UKI(统一内核镜像)可以减少启动步骤。将内核、initramfs、cmdline 打包成单个 EFI 文件, 减少启动步骤和文件系统访问。减少文件系统挂载次数,简化启动流程。在 NixOS 中通过boot.loader.systemd-boot.enable 和 boot.loader.efi.canTouchEfiVariables 启用。 3. 系统安全框架:认证、授权与密钥管理 现代 Linux 桌面系统的安全架构由多个相互协作的组件构成,包括 PAM(认证)、PolicyKit(授权)、以及桌面环境提供的密钥管理服务。这些组件共同构建了一个多层次的安全防护体系,既保证了系统的安全性,又提供了良好的用户体验。 NOTE: 注意 PAM 与 PolicyKit 的设计目的都是为普通用户提供权限提升手段。对 root 用户而言,这些框架的限制很少或几乎不存在。如果你希望限制整个系统全局的权限(包括 root 用户), 应该考虑 SELinux/AppArmor 等强制访问控制框架。 3.1 PAM - 可插拔认证模块 PAM(Pluggable Authentication Modules)是 Linux 系统的认证框架,为应用程序提供统一的认证接口。它允许系统管理员灵活配置认证策略,支持多种认证方式(密码、指纹、智能卡等),是现代 Linux 安全体系的基础组件。 3.1.1 PAM 工作机制与配置对应关系 PAM 采用模块化设计,将认证过程分解为四个独立的阶段: 认证(Authentication):验证用户身份(用户名/密码、生物识别等) 授权(Authorization):检查用户是否有权限访问特定资源 账户管理(Account Management):检查账户状态(是否过期、是否被锁定等) 会话管理(Session Management):管理用户会话的建立和销毁 程序与 PAM 配置的对应关系: 程序与 PAM 配置的对应关系是通过**服务名(Service Name)**建立的。当程序调用 PAM 时,它需要指定一个服务名,这个服务名决定了使用哪个 PAM 配置文件。 c // 程序调用 pam_start 时指定服务名 pam_start("login", username, &conv, &pamh); // 使用 /etc/pam.d/login pam_start("sudo", username, &conv, &pamh); // 使用 /etc/pam.d/sudo pam_start("sshd", username, &conv, &pamh); // 使用 /etc/pam.d/sshd 实际对应关系表: 程序 服务名 配置文件 说明 login "login" /etc/pam.d/login 控制台登录程序 gdm "gdm" /etc/pam.d/gdm GNOME 显示管理器 greetd "greetd" /etc/pam.d/greetd greetd 显示管理器 sudo "sudo" /etc/pam.d/sudo sudo 命令 su "su" /etc/pam.d/su su 命令 sshd "sshd" /etc/pam.d/sshd SSH 守护进程 passwd "passwd" /etc/pam.d/passwd 密码修改程序 PAM 调用流程示例: 如下是一个用户登录流程的 PAM 调用示例: c #include <stdio.h> #include <stdlib.h> #include <security/pam_appl.h> #include <security/pam_misc.h> static void log_result(pam_handle_t *pamh, int ret, const char *step) { if (ret == PAM_SUCCESS) { printf("[✓] %s 成功\n", step); } else { fprintf(stderr, "[✗] %s 失败: %s(返回码 %d)\n", step, pam_strerror(pamh, ret), ret); } } int main(int argc, char *argv[]) { pam_handle_t *pamh = NULL; struct pam_conv conv = { misc_conv, NULL }; const char *user; int ret; if (argc != 2) { fprintf(stderr, "用法: %s 用户名\n", argv[0]); return 1; } user = argv[1]; /* 1. 初始化 */ ret = pam_start("login", user, &conv, &pamh); if (ret != PAM_SUCCESS) { log_result(pamh, ret, "pam_start"); return 1; } /* 2. 认证 */ ret = pam_authenticate(pamh, 0); log_result(pamh, ret, "pam_authenticate"); if (ret != PAM_SUCCESS) { pam_end(pamh, ret); return 1; } /* 3. 帐户检查 */ ret = pam_acct_mgmt(pamh, 0); log_result(pamh, ret, "pam_acct_mgmt"); if (ret != PAM_SUCCESS) { pam_end(pamh, ret); return 1; } /* 4. 打开会话 */ ret = pam_open_session(pamh, 0); log_result(pamh, ret, "pam_open_session"); if (ret != PAM_SUCCESS) { /* 常见原因提示 */ fprintf(stderr, "\n提示:\n" " 1. 若您以普通用户运行,失败通常是权限不足(写 /var/run/utmp 等)。\n" " 2. 以 root 再次运行即可验证会话模块能否通过:sudo %s %s\n", argv[0], user); pam_end(pamh, ret); return 1; } printf("\n全部 PAM 阶段通过!\n"); /* 5. 关闭会话并清理 */ pam_close_session(pamh, 0); pam_end(pamh, PAM_SUCCESS); return 0; } 将上述配置保存为 pam_test.c, 再创建一个 shell.nix 内容如下: nix { pkgs ? import <nixpkgs> {} }: pkgs.mkShell { buildInputs = with pkgs; [ pam gcc ]; } 最后编译运行: bash # 进入引入了 pam 链接库的环境 nix-shell # 编译 gcc pam_test.c -o pam_test -lpam -lpam_misc # 测试 ./pam_test ryan 3.1.2 PAM 配置语法与模块 配置语法: text <type> <control> <module> [arguments] 类型(type): auth:认证模块 account:账户管理模块 password:密码管理模块 session:会话管理模块 控制标志(control): required:必须成功,失败后继续执行其他模块但最终失败 requisite:必须成功,失败后立即返回失败 sufficient:成功即可通过,失败不影响最终结果 optional:可选模块,不影响认证结果 NixOS 实际配置示例: bash # /etc/pam.d/login 实际配置(NixOS 生成) # Account management. account required /nix/store/xxx-linux-pam-1.7.1/lib/security/pam_unix.so # Authentication management. auth optional /nix/store/xxx-linux-pam-1.7.1/lib/security/pam_unix.so likeauth nullok auth optional /nix/store/xxx-gnome-keyring-48.0/lib/security/pam_gnome_keyring.so auth sufficient /nix/store/xxx-linux-pam-1.7.1/lib/security/pam_unix.so likeauth nullok try_first_pass auth required /nix/store/xxx-linux-pam-1.7.1/lib/security/pam_deny.so # Password management. password sufficient /nix/store/xxx-linux-pam-1.7.1/lib/security/pam_unix.so nullok yescrypt password optional /nix/store/xxx-gnome-keyring-48.0/lib/security/pam_gnome_keyring.so use_authtok # Session management. session required /nix/store/xxx-linux-pam-1.7.1/lib/security/pam_env.so conffile=/etc/pam/environment readenv=0 session required /nix/store/xxx-linux-pam-1.7.1/lib/security/pam_unix.so session required /nix/store/xxx-linux-pam-1.7.1/lib/security/pam_loginuid.so session optional /nix/store/xxx-systemd-257.8/lib/security/pam_systemd.so session required /nix/store/xxx-linux-pam-1.7.1/lib/security/pam_limits.so conf=/nix/store/xxx-limits.conf session optional /nix/store/xxx-gnome-keyring-48.0/lib/security/pam_gnome_keyring.so auto_start 常用 PAM 模块: 模块名 功能 用途 pam_unix.so Unix 标准认证 基于 /etc/passwd 和 /etc/shadow 的认证 pam_deny.so 拒绝访问 默认拒绝所有认证请求 pam_env.so 环境变量管理 设置用户会话环境变量 pam_loginuid.so 登录 UID 管理 记录用户登录的 UID pam_systemd.so systemd 集成 与 systemd 用户会话集成 pam_limits.so 资源限制 设置用户资源使用限制 pam_gnome_keyring.so GNOME Keyring 集成 自动解锁用户密钥环 pam_ldap.so LDAP 认证 企业环境中的集中认证 pam_fprintd.so 指纹认证 生物识别认证 pam_google_authenticator.so 双因子认证 TOTP 时间令牌认证 3.1.3 PAM 调试与故障排查 PAM 调试主要涉及配置验证、模块检查和日志分析。常用的调试方法包括: 配置验证:使用 pamtester 工具测试特定服务的认证流程 模块检查:验证 PAM 模块的依赖关系和加载状态 日志分析:通过 journalctl 查看认证相关的系统日志 程序跟踪:使用 strace 验证程序与 PAM 配置的对应关系 具体的调试命令请参考 3.5.3 故障排查 章节。 NixOS PAM 配置特点: 声明式配置:PAM 配置通过 NixOS 配置系统生成,不直接编辑 /etc/pam.d/ 文件 模块路径:所有 PAM 模块都使用完整的 /nix/store 路径,确保版本一致性 自动集成:GNOME Keyring 等组件会自动集成到 PAM 配置中 可重现性:配置变更通过 nixos-rebuild 应用,确保系统状态可重现 常见问题: 认证失败:检查 /etc/passwd 和 /etc/shadow 文件权限 模块加载失败:确认 PAM 模块文件存在且可执行 配置语法错误:使用 pamtester 验证配置 服务名不匹配:如果程序指定的服务名与配置文件不匹配,会使用 other 配置 3.2 PolicyKit - 细粒度的系统权限管理 PolicyKit(现称 polkit)是一个用于控制系统级权限的框架,它提供了一种比传统 Unix 权限更细粒度的授权机制。在现代 Linux 桌面系统中,PolicyKit 允许非特权用户执行某些需要特权的系统操作 (如关机、重启、挂载设备、修改系统时间等),而无需获取完整的 root 权限。 3.2.1 PolicyKit 的核心概念 配置文件路径: /etc/polkit-1/:NixOS 声明式配置中定义的自定义规则(优先级最高) /run/current-system/sw/share/polkit-1/(NixOS)或 /usr/share/polkit-1/(传统发行版):软件包提供的默认规则 上述文件夹中又包含两类配置: 动作(Actions) 定义在配置文件夹的 actions 目录中的 XML 文件(如 /etc/polkit-1/actions/),描述可授权的操作。每个动作都有唯一的标识符,如 org.freedesktop.login1.power-off 表示关机操作。 规则(Rules) JavaScript 文件,定义授权决策逻辑,位于上述配置文件夹的 rules.d/ 目录中(如/etc/polkit-1/rules.d/)。规则决定了在特定条件下是否授权某个操作。在 NixOS 中,推荐使用声明式配置而非直接修改 /etc 目录。 身份认证代理(Authentication Agents):桌面环境提供的图形界面组件,用于在用户需要身份验证时弹出认证对话框。例如,当普通用户尝试关机时,认证代理会提示输入管理员密码。 举例来说,我使用的是 Niri 窗口管理器,它的 Nix Flake 启用了 pokit-kde-agent-1 作为其 Authentication Agent, 配置参见sodiboo/niri-flake. 3.2.2 PolicyKit 的工作原理 当应用程序请求执行需要特权的操作时,系统服务会询问 PolicyKit 是否授权。PolicyKit 的评估过程如下: 身份识别:确定请求者的身份(用户、组、会话等) 规则匹配:检查是否有适用的规则文件 权限评估:根据规则返回以下结果之一: yes:直接允许,无需认证 no:直接拒绝 auth_self:需要用户自己认证(输入当前用户密码) auth_admin:需要管理员认证(输入 root 密码) auth_self_keep/auth_admin_keep:认证后在一段时间内保持授权 3.2.3 PolicyKit 的配置示例 在传统的 Linux 发行版中,管理员可以通过创建自定义规则来修改默认行为。例如,允许 wheel 组的用户无需密码即可关机: javascript // /etc/polkit-1/rules.d/10-shutdown.rules polkit.addRule(function (action, subject) { if (action.id == "org.freedesktop.login1.power-off" && subject.isInGroup("wheel")) { return polkit.Result.YES } }) NixOS 中的配置方法:在 NixOS 中,推荐使用声明式配置而非直接修改 /etc 目录。可以通过security.polkit 配置项来管理 PolicyKit 规则: nix # configuration.nix { security.polkit.enable = true; # 添加自定义规则 security.polkit.extraConfig = '' polkit.addRule(function(action, subject) { if (action.id == "org.freedesktop.login1.power-off" && subject.isInGroup("wheel")) { return polkit.Result.YES; } }); ''; } 3.2.4 PolicyKit 与 D-Bus 的集成 PolicyKit 与 D-Bus 深度集成,为 D-Bus 服务提供动态授权机制。许多系统服务(如 systemd、NetworkManager、udisks 等)都使用 PolicyKit 来控制对其 D-Bus 接口的访问。当客户端通过 D-Bus 调用需要特权的方法时,服务会调用 PolicyKit 进行授权检查。 PolicyKit 调试主要涉及服务状态检查、权限测试和规则验证。常用的调试方法包括: 服务状态检查:验证 PolicyKit 守护进程的运行状态 权限测试:使用 pkcheck 工具测试特定操作的授权情况 日志分析:查看 PolicyKit 的授权决策日志 规则验证:检查当前生效的 PolicyKit 规则配置 具体的调试命令请参考 3.5.3 故障排查 章节。 3.3 桌面密钥管理 现代 Linux 桌面环境提供了统一的密钥管理服务,用于安全存储用户的密码、证书、密钥等敏感信息。 GNOME Keyring 和 KDE Wallet 分别是 GNOME 和 KDE 桌面环境的密钥管理解决方案,它们通过加密存储和自动解锁机制,为用户提供了便捷而安全的密码管理体验。 GNOME Keyring 和 KDE Wallet 都实现了标准的Secrets API, 可以根据需要任选一个使用。不过据我观察大部分窗口管理器的用户都是用的 GNOME Keyring. 3.3.1 密钥管理系统架构 GNOME Keyring 架构: 密钥环(Keyring):加密的存储容器,每个密钥环有独立的密码 密钥环守护进程(gnome-keyring-daemon):管理密钥环的生命周期和访问控制 API:Gnome 原生支持 org.freedesktop.secrets DBus API, 目前流行的 secrets 客户端库 libsecret 也是 gnome 开发的。 PAM 集成:通过 pam_gnome_keyring.so 实现登录时自动解锁 KDE Wallet 架构: KWalletManager:图形界面管理工具 kwalletd:钱包守护进程 API:KDE Wallet 从 5.97.0 (2022 年 8 月)开始支持org.freedesktop.secrets DBus API, 因此可以直接通过 libsecret 往 KDE Wallet 中存取 passwords 等 secret. PAM 集成:通过 pam_kwallet.so 实现自动解锁 核心组件路径: bash # GNOME Keyring 组件(NixOS 中位于 nix store) /run/current-system/sw/bin/gnome-keyring-daemon /run/current-system/sw/lib/libsecret-1.so /run/current-system/sw/lib/security/pam_gnome_keyring.so # KDE Wallet 组件(NixOS 中位于 nix store) /run/current-system/sw/bin/kwalletd5 /run/current-system/sw/bin/kwalletmanager5 /run/current-system/sw/lib/security/pam_kwallet.so # 配置文件位置 ~/.local/share/keyrings/ # GNOME 密钥环存储目录 ~/.local/share/kwalletd/ # KDE 钱包文件存储目录 ~/.config/kwalletrc # KDE 钱包配置文件 3.3.2 密钥环类型与用途 密钥环类型 用途 解锁时机 login 登录密钥环,存储用户密码 用户登录时自动解锁 default 默认密钥环,存储应用密码 首次访问时解锁 session 会话密钥环,临时存储 会话开始时创建 crypto 加密密钥环,存储证书和私钥 按需解锁 3.3.3 钱包创建与管理 图形界面管理: bash # GNOME 密钥环管理器 seahorse # KDE 钱包管理器 kwalletmanager5 通过图形界面可以: 创建新的密钥环/钱包 设置密码和加密算法 管理存储的密码和证书 配置自动解锁策略 备份和恢复密钥环 基本命令行操作: bash # 使用 secret-tool 管理 GNOME Keyring secret-tool store --label="My Password" application myapp secret-tool lookup application myapp # 使用 kwallet-query 管理 KDE Wallet kwallet-query --write password "MyApp" "username" "password" kwallet-query --read password "MyApp" "username" 3.3.4 应用程序集成 常见应用程序集成: VSCode: 自动集成系统密钥管理服务 存储 Git 凭据、扩展设置等敏感信息 通过 git credential.helper 配置自动使用 GitHub CLI: bash # 配置 GitHub CLI 使用系统密钥管理 gh auth login --web # 凭据会自动存储到系统密钥环中 浏览器集成: Firefox、Chrome 等现代浏览器支持系统密钥管理 网站密码自动保存到密钥环/钱包中 跨设备同步(如果启用) API 集成示例: libsecret API 3.3.5 配置与优化 NixOS 配置示例: nix # configuration.nix # 启用 GNOME Keyring services.gnome.gnome-keyring.enable = true; # GNOME Keyring GUI 客户端 programs.seahorse.enable = true; # 启用 PAM 集成 security.pam.services.login.enableGnomeKeyring = true; 3.4 安全故障排查 3.4.1 认证问题排查 常见认证失败场景: 用户无法登录 检查 PAM 配置是否正确 查看认证日志中的错误信息 验证用户账户状态和密码 sudo 权限问题 确认用户在正确的用户组中 检查 sudoers 配置 验证 PAM 认证流程 SSH 登录失败 检查 SSH 服务状态 查看 SSH 认证日志 验证网络连接和防火墙设置 3.4.2 权限管理问题排查 PolicyKit 权限问题: 无法关机/重启:检查 PolicyKit 规则配置和用户组权限 无法挂载设备:检查 udisks2 服务和 PolicyKit 集成 无法修改系统时间:检查时间同步服务权限和用户组设置 3.4.3 密钥管理问题排查 GNOME Keyring 问题: 检查密钥环守护进程是否正常运行 验证 PAM 集成是否正确配置 查看密钥环状态和自动解锁设置 KDE Wallet 问题: 检查钱包守护进程状态 验证钱包配置和访问权限 测试钱包的读写功能 具体的调试命令和排查步骤请参考 3.5.3 故障排查 章节。 3.5 安全组件集成与最佳实践 3.5.1 组件协作流程 现代 Linux 桌面的安全组件协作流程: 用户登录:PAM 验证用户身份 密钥环解锁:PAM 模块自动解锁用户密钥环/钱包 应用启动:应用程序通过 libsecret/KWallet API 访问存储的密码 特权操作:PolicyKit 控制需要特权的系统操作 会话结束:密钥环/钱包自动锁定 3.5.2 安全最佳实践 密钥管理: 使用强密码保护密钥环/钱包 定期备份密钥环文件 避免在脚本中硬编码密码 使用应用程序专用的密钥环 认证配置: bash # 启用双因子认证 auth required pam_google_authenticator.so auth required pam_unix.so # 配置密码策略 password required pam_cracklib.so retry=3 minlen=8 difok=3 password required pam_unix.so use_authtok 权限管理: javascript // PolicyKit 规则示例:限制特定操作 polkit.addRule(function (action, subject) { if (action.id == "org.freedesktop.login1.power-off" && subject.user == "guest") { return polkit.Result.NO } }) 3.5.3 故障排查 PAM 认证调试: bash # 安装 PAM 测试工具 nix shell nixpkgs#pamtester # 测试 PAM 配置 pamtester login $USER authenticate pamtester sudo $USER authenticate # 查看 PAM 配置 cat /etc/pam.d/login cat /etc/pam.d/greetd cat /etc/pam.d/sudo # 检查 PAM 模块 ldd /run/current-system/sw/lib/security/pam_unix.so ldd /run/current-system/sw/lib/security/pam_gnome_keyring.so # 查看认证日志 journalctl -t login -f journalctl -t greetd -f journalctl -t sshd -f journalctl -t sudo # 验证程序与配置的对应关系 strace -e trace=pam_start login 2>&1 | grep pam_start strace -e trace=openat login 2>&1 | grep pam.d PolicyKit 权限调试: bash # 检查 PolicyKit 服务状态 systemctl status polkit # 测试特定权限 pkcheck --action-id org.freedesktop.login1.power-off --process $$ --allow-user-interaction # 查看 PolicyKit 日志 journalctl -u polkit -f # 查看 PolicyKit 动作定义 ls -la /run/current-system/sw/share/polkit-1/actions/ # 查看当前生效的 PolicyKit 规则 ls -la /etc/polkit-1/rules.d/ 密钥管理调试: bash # GNOME Keyring 检查 ps aux | grep gnome-keyring seahorse # GNOME Keyring GUI # KDE Wallet 检查 ps aux | grep kwalletd kwalletmanager5 # KDE Wallet GUI kwallet-query kdewallet --list-entries # 系统日志检查 sudo journalctl -u systemd-logind 调试技巧: 使用 strace 跟踪应用程序的密钥访问 通过 journalctl 查看认证和授权日志 使用 pamtester 测试 PAM 配置 通过 pkcheck 测试 PolicyKit 权限 通过理解这些安全组件的协作机制,用户可以更好地配置和管理 Linux 桌面的安全策略,在保证安全性的同时提供良好的用户体验。 总结 从 UEFI 到 systemd,从 PAM 到 PolicyKit,本文详细介绍了 Linux 桌面系统启动与安全框架的核心组件。 下一篇文章将深入探讨 systemd 全家桶与服务管理,包括 D-Bus 系统总线、日志系统和设备管理等核心功能,这些组件为桌面环境提供了强大的基础设施支持。 快速参考 常用启动排查命令 bash # 启动时间分析 systemd-analyze systemd-analyze blame systemd-analyze critical-chain # 引导加载器检查 bootctl status bootctl list efibootmgr -v # 内核和硬件信息 dmesg | grep -i error lspci -k lsusb lsblk # 进入救援模式 # 在内核参数中添加:init=/bin/sh 或 break=mount 常用安全排查命令 安全相关的调试命令请参考 3.5.3 故障排查 章节,该章节提供了完整的 PAM、PolicyKit 和密钥管理调试命令。 重要配置文件位置 bash # 启动相关 /boot/loader/loader.conf # systemd-boot 全局配置 /boot/EFI/Linux/ # UKI 镜像位置 /etc/pam.d/ # PAM 配置文件 /etc/polkit-1/ # PolicyKit 配置 # 密钥管理 ~/.local/share/keyrings/ # GNOME Keyring 存储 ~/.local/share/kwalletd/ # KDE Wallet 存储 ~/.config/kwalletrc # KDE Wallet 配置

2025/10/19
articleCard.readMore

Linux 桌面系统故障排查指南(零) - 组件概览

AI 创作声明:本系列文章由笔者借助 ChatGPT, Kimi K2, 豆包和 Cursor 等 AI 工具创作,有很大篇幅的内容完全由 AI 在我的指导下生成。如有错误,还请指正。 定位与目标 Linux 桌面包含了相当多的系统组件,这些组件组合形成了一个精密的系统,它们共同管理着从硬件设备到用户会话的方方面面。 即使我已经有七八年的 Linux 使用经验,在遇到系统的各种大小毛病时,还是常常觉得问题的定位跟解决很是艰难。倘若我们能像庖丁那样"目无全牛",对整个系统的架构了如指掌,在定位问题时顺着骨节筋脉下刀,那解决起问题来自然也将游刃有余。 而这就是这个系列文章的目的——搭建起一幅 Linux 桌面系统的完整「解牛图」。 本系列面向已经有一定 Linux 桌面使用经验的读者。我们用一条从「开机」到「APP 运行」再到「关机/ 断电」的完整时间线为轴,深入讲解每一步发生了什么、哪里能看到证据(日志 / 设备节点 / D‑Bus 信号)、可通过哪些命令排查验证,以及常见问题的修复思路。 本文作为系列概览,主要起导航和架构梳理的作用,帮助读者建立整体认知框架。 文章系列导航 📋 Linux 桌面系统故障排查指南(一) - 系统启动与安全框架 涵盖内容: 系统启动流程:从 UEFI 固件到 systemd 用户空间的完整启动过程,包括 systemd-boot 配置、UKI 统一内核镜像、initramfs 阶段详解 安全框架深度解析:PAM 认证机制、PolicyKit 权限管理、GNOME Keyring 密钥管理,以及各组件间的协作关系 启动故障排查:系统化的问题诊断流程,从固件到用户空间的逐层排查方法 启动性能优化:硬件、内核、服务层面的优化策略,包括 UKI 使用和启动时间分析 ⚙️ Linux 桌面系统故障排查指南(二) - systemd 全家桶与服务管理 涵盖内容: systemd 核心功能:服务管理、依赖关系、并行启动、单元类型配置和生命周期管理 systemd 生态系统服务:着重介绍 systemd-journald 日志系统、systemd-oomd 内存管理、systemd-resolved DNS 解析、systemd-timesyncd 时间同步 设备管理:udev 规则系统、systemd-udevd 用户空间实现、设备权限分配和故障排查 D-Bus 系统总线:进程间通信机制、权限管控、Flatpak 沙盒环境下的 D-Bus 代理过滤 服务管理最佳实践:服务配置优化、依赖关系管理、性能调优 🖥️ Linux 桌面系统故障排查指南(三) - 桌面会话与图形渲染 涵盖内容: 用户会话管理:登录流程详解、systemd-logind 会话控制、seat 概念和多用户场景 Wayland 图形架构:与 X11 的深度对比、客户端-服务器模型、协议扩展和安全性优势 图形渲染栈:DRM/KMS 显示管理、Mesa 驱动、EGL/GBM 接口、OpenGL/Vulkan 渲染管线 输入处理系统:libinput 事件处理、evdev 内核接口、手势识别、多点触控支持 设备访问控制:ACL 权限分配、GPU 设备管理、输入设备权限、systemd-logind 集成 应用程序架构:启动流程、图形驱动选择、工具包支持(GTK/Qt/SDL)、渲染器优化 应用兼容性:Wayland/XWayland 兼容性、沙盒化应用(Flatpak)、性能调优 会话故障排查:登录失败、权限错误、图形界面异常、渲染问题的系统化诊断 🎵 Linux 桌面系统故障排查指南(四) - 多媒体处理与中文支持 涵盖内容: PipeWire 统一多媒体架构:音频视频处理、屏幕共享、兼容层(PulseAudio/JACK/ALSA) 视频处理与屏幕共享:Wayland screen-capture 协议、硬件加速、DMA-BUF 传递、权限管理 音频处理流程:低延迟配置、音频路由控制、设备管理和性能优化 字体渲染系统:fontconfig 配置、CJK 字体管理、渲染参数优化、字体匹配规则 中文输入法:fcitx5 框架、Wayland text-input 协议、XWayland 兼容性、混合环境管理 故障排查与优化:音频延迟优化、输入法无响应、字体显示问题、屏幕共享故障 🌐 Linux 桌面系统故障排查指南(五) - 网络 涵盖内容: 网络连接与管理:从硬件驱动到应用层的完整协议栈,systemd-networkd 和 iwd 的现代网络管理 IPv4/IPv6 双栈技术:地址分配机制、路由表管理、协议优先级配置、双栈验证方法 防火墙与网络安全:nftables 现代防火墙、NAT 配置、端口转发、流量控制规则 虚拟网络技术:TUN/TAP 接口、VPN 连接管理(WireGuard)、桥接网络、容器网络 网络性能优化:内核参数调优、TCP 拥塞控制(BBR)、连接跟踪优化、网络监控分析 高级网络配置:多网卡绑定、VLAN 配置、网络命名空间、网络故障诊断 🔄 Linux 桌面系统故障排查指南(六) - 系统关机与电源管理 涵盖内容: 系统关机流程详解:介绍完整关机过程,从用户会话清理到硬件关机的每个步骤 电源管理功能:休眠(Hibernate)和挂起(Suspend)的配置、工作原理和故障排查 关机故障排查:服务停止超时、文件系统卸载失败、设备繁忙等问题的诊断和解决 实战故障案例:桌面环境启动失败、应用崩溃、网络异常、系统关机卡住等综合问题 系统化排查方法:日志分析、逐层排查、工具使用技巧、最佳实践总结 电源管理优化:自动挂起配置、定时休眠设置、功耗优化、硬件兼容性处理 技术栈说明 本系列文章基于以下现代 Linux 桌面技术栈: 引导系统:UEFI + systemd-boot 初始化系统:systemd 显示协议:Wayland 音频系统:PipeWire 网络管理:systemd-networkd + iwd 防火墙:nftables 输入法:fcitx5 字体系统:fontconfig 发行版:主要基于 NixOS,同时补充说明与传统发行版的差异 技术选择说明: systemd-boot:相比 GRUB 更简洁,支持 UKI 和 Secure Boot,启动速度更快 Wayland:相比 X11 更安全、性能更好 PipeWire:统一的多媒体处理框架,相比 PulseAudio 延迟更低,支持统一处理音频跟视频 systemd-networkd + iwd:相比 NetworkManager + wpa_supplicant 更现代、更轻量 nftables:相比 iptables 语法更简洁,性能更好 fcitx5:相比 ibus 对 Wayland 支持更好,配置更灵活 虽然以 NixOS 为例,但涉及的核心概念、配置方法和故障排查技巧同样适用于其他现代 Linux 发行版 (如 Arch Linux、Fedora、Ubuntu 等)。 总结 Linux 桌面系统虽复杂,但每个组件都有明确作用和逻辑关系。 希望这份完整的"解牛图"能成为你探索 Linux 桌面世界的有力工具,让你的 Linux 之旅更加顺畅与愉快。 参考 Understanding Linux Desktop Components: Display Servers, Compositors, Window Managers, and Desktop Environments

2025/9/9
articleCard.readMore

KubeCon China 2025 见闻

前言 今年 1 月底辞职后,在家过了个年,接着在上海、张家界、重庆、苏州、南京玩了一圈,4 月中旬才回深圳开始找工作。本来看到 6 月就是 KubeCon China 2025,还不太确定自己到时候会不会有时间去。不过很幸运,最后确定 offer 的公司非常重视技术,leader 在面试的时候就说看到我博客里写了 KubeCon 的经历,公司非常鼓励参加这种技术交流活动,去报个 Talk 也完全可以,公司报销所有费用。 于是我在入职还没满一个月的时候,就直接公费出差去香港 KubeCon China 2025 玩了一圈( 也问过同事们是否有想法,但种种原因最后还是只有我一个人参加了(悲 TL;DR 简单的说,今年的 KubeCon China 几乎全都是聊 AI on Kubernetes 的,感觉都可以改名叫 CloudNative AI Con 了。 今年 KubeCon China 只有两天,Talks 明显比去年少了很多,几乎只有去年的一半,所以我也在线上看了许多 KubeCon Europe 2025 的 Talks 作为补充。 总的来说我今年的感觉是: Kubernetes 已经成为一个相当成熟的基座,任何可以在 K8s 上跑的东西最终都会被搬到 K8s 上跑 ( AI 让 CloudNative 社区焕发了新生,围绕 AI 在过去两年间涌现了许多新的 CloudNative 项目。AI 话题已经成为了 KubeCon 绝对的主旋律。 AI 部署部分主要在讨论 AI 推理,关键技术点:分布式推理、扩缩容与 LLM-Aware 的负载均衡以及 AI 模型分发 AIOps 也有好几个讨论,简单的用法就是 ChatBot,复杂点的会尝试使用 Multi-Agent 完成更复杂的任务(比如云成本分析优化)。 快手尝试在超大规模集群中利用 Logs/Metrcis 为每个服务训练一个模型用于动态调整 HPA,实现 SLA 与成本的平衡(如果我记错了概不负责 hhh)。 OpenTelemetry 日渐成熟,已经很接近它统一 Logs/Traces/Metrics 三大 Signals 的目标了。 目前已经出现了 Uptrace 之类的大一统观测平台,充分利用了 OTel 的标签来关联 Logs/Traces. 当前的最佳实践是,在 Infra 层面仍然使用传统方式采集 Logs 与 Metrics,而在 APP 层面则改由 OTel 统一采集所有 Logs, Traces 与 Metrics,OTel 会通过 Span ID 把这些数据关联起来, 而且标签语义完全一致。 WASM 仍在探寻自己的应用场景,今年介绍的场景主要是在边缘侧跑小模型。 KubeCon China 2025 与 KubeCon Europe 2025 的视频列表如下: KubeCon + CloudNativeCon China 2025 (Hong Kong) - Youtube KubeCon + CloudNativeCon Europe 2025(London) - Youtube 视频相关的 PPT 可以在这里下载(NOTE: 不是所有 Talks 都会上传 PDF): KubeCon + CloudNativeCon China 2025 - Schedule KubeCon + CloudNativeCon Europe 2025 - Schedule 接下来我会把我听过的一些比较有意思的内容分 Topic 大概介绍下,也会附上对应的视频跟可能的 PPT 链接。 Talks 大一统的 LLM 推理解决方案 Introducing AIBrix: Cost-Effective and Scalable Kubernetes Control Plane for VLLM - Jiaxin Shan & Liguang Xie, ByteDance AIBrix 是一整套在 K8s 上跑 LLM 分布式推理的解决方案,它包含了: 分布式推理的部署 LLM 扩缩容 LLM 请求路由(负载均衡) 分布式 KV 缓存 主要是中心化存储这些数据,减少对 HBM 显存的使用,降低显存需求。 LoRa 的动态加载 … 代码: https://github.com/vllm-project/aibrix AIBrix 目前放在了 vllm-project 项目下,stars 也不少,感觉项目还是挺健康的,值得关注。 分布式 LLM 推理的部署 More Than Model Sharding: LWS & Distributed Inference - Peter Pan & Nicole Li, DaoCloud & Shane Wang, Intel 全场最有意思的 Talks 之一,大概介绍了分布式推理的架构、优化点,以及 LWS 的优点与用法。 代码: https://github.com/kubernetes-sigs/lws 简单的说 LWS 是一个专门为 LLM 分布式推理的部署而设计的 CRD, 主要是支持了 LLM 任务的分组调度。 NOTE: 看 issue AIBrix 还有跟 LWS 结合使用的可能性(甚至可能被官方支持):https://github.com/vllm-project/aibrix/issues/843#issuecomment-2728305020 LLM 扩缩容与负载均衡 KubeCon EU 2025 - Optimizing Metrics Collection & Serving When Autoscaling LLM Workloads 讲得挺风趣,不过可能我对这块比较熟悉,基本能猜到就是自定义业务 metrics + 用 KEDA 做 custom metrics based scaling,所以就只是简单看了看。 KubeCon EU 2025 - Keynote: LLM-Aware Load Balancing in Kubernetes: A New Era of Efficiency - Clayton Coleman, Distinguished Engineer, Google & Jiaxin Shan, Software Engineer, Bytedance 很有意思,LLM 的请求跟传统的 API 请求区别非常大,主要点在于: input 长度区别就非常大,有的请求 input 很简单,相对就很轻量,而有的可能直接丢一份 PDF 或者别的超长文本输入。输出也同样如此,如果用户明确要求深度推理,可能会导致大量性能消耗。 不同机器可能会使用不同的 GPU 类型,而这些 GPU 的性能各异。 在一个支持多模型的平台上,不同模型的高低峰期也存在比较明显的区别。 上面这些特征导致传统的负载均衡策略完全失效。 解决方案: https://github.com/kubernetes-sigs/gateway-api-inference-extension AI 模型分发 AI Model Distribution Challenges and Best Practices 几位开发者聊怎么在集群里分发数百 GB 大小的 LLM 模型,业界目前的手段: dragonfly juicefs oci model spec + oci volume (k8s 1.33+) 可观测性 Antipatterns in Observability: Lessons Learned and How OpenTelemetry Solves Them - Steve Flanders, Splunk 这位也讲得挺有意思,而且有干货。他列举的可观测性方面的 Antipatterns 有 Telemetry Data IncompleteInstrumentation - 需要引入zero-code 的 otel sdk 实现自动数据采集 metrcis/logs/metrics 三类 signals 不一定都默认启用,具体得看对应的 agent 实现情况 在 k8s 中建议同时禁用将日志输出到 stdout 的功能以及传统的给 prometheus pull 的 /metrics 端点,由 otel agent 全权负责 App-level 三大信号的处理。daemonset 模式的 otel (或者 vector/fluentbit)则主要用于采集 sidecar/k8s 等 Infra-level 的日志。 Over-Instrumentation - 需要在 otel-collector 层过滤精简指标,再发送到对应的后端存储。 Inconsistent Naming Conventions - 全盘替换为 OpenTelemetry 方案,即可享受统一的命名。 Observability Platform Vendor Lock-in - 只选用支持 OTel 标准的平台并使用 Otel 命名规范。 Tool Sprawl - 使用大一统的观测平台,如 Uptrace, 支持自动关联 Logs 与 Traces. Underestimating Scalability Requirements - 使用 OTel 采集信号,并选用可拓展性好的后端存储,如 VictoriaMetrics. Company Culture Silos and Lack of Collaboration Lack of Ownership & Accountability KubeCon EU 2025 - From Logs To Insights: Real-time Conversational Troubleshooting for Kubernetes With GenAI - Tiago Reichert & Lucas Duarte, AWS 开场的 OnCall 小品就很真实… 不过 pod pending 1 分钟就电话告警有点夸张了… 演完小品才开始讲正式内容,大体上就是把日志用 embed 模型编码后存在 OpenSearch 里做 RAG,还给了 ChatBot k8s readonly 的权限(ban 掉了 secrets access),然后通过 Deepseek/Claude 问答来解决问题。 代码: https://github.com/aws-samples/sample-eks-troubleshooting-rag-chatbot Portrait Service: AI-Driven PB-Scale Data Mining for Cost Optimization and Stability Enhancement - Yuji Liu & Zhiheng Sun, Kuaishou 讲快手怎么在 20 万台机器的超大规模集群上做稳定性管理与性能优化。 介绍得比较浅,大概就是会收集集群中非常多的信息,用一套大数据系统持续处理,再丢给后面训练专用模型,每个服务都可能有一个专门的资源优化模型,用它来做最终的资源优化。 这一套可能太重了,可以借鉴,但是在我目前的工作场景中不太有用(规模太小)。 Service Mesh Revolutionizing Sidecarless Service Mesh With eBPF - Zhonghu Xu & Muyang Tian, Huawei 主要就讲 Huawei 自己搞的 Kmesh,有比较详细的讲底层的实现架构(其实跟去年 KubeCon 听过的内容几乎一样)。 简单讲就是 Ambient Mode 通过 istio-cni(底层是 iptables)将流量拦截到用户态的 ztunnel 进行 L4 流量处理,而 Kmesh 使用 eBPF 在内核层实现了这些 L4 的功能。另外还简单介绍了 Cilium Service Mesh,是一个 Per-Node 的 Proxy,主要缺点是必须用 Cilium 网络插件,以及它的 CRD 过于原始,使用复杂。 Kmesh 也尝试用 eBPF 实现了 HTTP 协议的解析,但是这需要对内核打补丁,代价比较高。 KubeCon EU 2025 - Choosing a Service Mesh - Alex McMenemy & Dimple Thoomkuzhy, Compare the Market 虽然我接触过的基本都用的 Istio,不过看看别人怎么做选择总没坏处 KubeCon EU 2025 - Navigating the Maze of Multi-Cluster Istio: Lessons Learned at Scale - Pamela Hernandez, BlackRock Istio 多集群在挺多大公司有应用,之前面试就被问到过,可以玩玩看。 KubeCon EU 2025 - A Service Mesh Benchmark You Can Trust - Denis Jannot, solo.io 做一个好的 Benchmark 对比还挺费时间费精力的,还是直接看人家给的结果最方便( Ingress-Nginx The Next Steps for Ingress-NGINX and the Ingate Project - Jintao Zhang, Kong Inc. Ingress-NGINX 终于要寿终正寝了,它的继任者叫 InGate,不过 InGate 目前还几乎是个空壳( 代码 https://github.com/kubernetes-sigs/ingate 安全性 Keynote: Who Owns Your Pod? Observing and Blocking Unwanted Behavior at eBay With eBPF 主要就介绍 cilium 家的 tetragon, 一个基于 eBPF 的 K8S 安全工具,跟 apparmor 感觉会有点类似,但是能做到更精细的权限管理。 朋友跟我 Argue 这种工具不是很有必要,应该用 GitOps 流程,然后将安全检查前置在 CICD 流水线中。 云成本分析与优化 KubeCon EU 2025 - Autonomous Al Agents for Cloud Cost Analysis - Ilya Lyamkin, Spotify 实现一个会自动做 Plan,编写 SQL 与 Python 进行云成本分析的 Multi-Agent 系统,很有参考价值。 WASM 相关 Keynote: An Optimized Linux Stack for GenAI Workloads - Michael Yuan, WasmEdge 讲怎么用 WasmEdge + LlamaEdge 在边缘设备上跑 LLM 小模型,还是挺有意思的。 如何搭建一个 AI 工作流 KubeCon EU 2025 - Tutorial: Build, Operate, and Use a Multi-Tenant AI Cluster Based Entirely on Open Source 长度超过一个小时的教程,IBM 出品。装了一堆东西,包括 Kueue, Kubeflow, PyTorch, Ray, vLLM, and Autopilot Non-Tech 参加 KubeCon 其实不仅仅是听一听过去一年技术方面的变化与进展,还有个很重要的目的是跟各个方向的开发者们 Social, 也可以说是某种大型网友见面会( 今年拉到了 @scruelt, @ox-warrior 等几位朋友一起去 KubeCon 玩,然后在会场又陆续跟@cookie, @rizumu, @ayakaneko 以及 @dotnetfx35 见面闲聊瞎扯了一波,收获了 @rizumu 跟 @ayakaneko 用 3D 打印机打印的 Kubernetes 跟 Go 小饼干各一枚,顺便传教了 NixOS( 面基成功!顺便传教 NixOS 拿到的 K8s/Go 小饼干以及 Istio 冰箱贴 Day 2 上午发现没啥想听的,发现有个 Peer Group Meeting 参加,不过需要先 sign up. 跟@scruelt 一起去报了名,本来还担心只提前 20 分钟 sign up 会不会没机会了,结果到会议室发现只有 3 个 mentors 在场,于是就我们俩跟他们随便闲聊 emmm 三位 mentors 分别是 Nate Waddington (Head of Mentorship & Documentatio, Canada),Kohei Ota(CNCF Ambassador, Japan)以及 Amit DSouza(co-founder of Odyssey Cloud, Australia),另外聊到半途一位 Cisico 的老哥也加入了进来。 基本就是闲聊,@scruelt 口语比我好,而且刚辞职也有许多问题想问,绝大部分话题都是他提出来的。我因为最近诸事皆顺,反而没啥想问的。 进了 Peer Group Meeting 发现只有 Mentors hhh 最后就放些图吧。 欢迎光临 KubeCon China 2025 先领个 T 恤嘿嘿 茶歇时间 SUSE 的毛绒玩具好想要! 大 SUSE 上一只小 SUSE 用 tetragon 限制文件访问 LWS 的 Talk,在讲 PD 分离 Switch 店在宣传 Miku Boxing 累计有三个朋友 KubeCon 期间在这里买了 Switch 2,它这波血赚 我的所有'战利品' hhh 登机了,再见深圳 这是我第几次坐飞机来着? 总之玩得很开心,明年再见!

2025/6/15
articleCard.readMore

我的 2024 - 稳中求进、热爱生活

前言 相比跌宕起伏的 2023 年, 2024 年我少了一些焦虑与内耗, 花在技术上的时间也少了不少. 我将大量的精力转移到了徒步旅行上, 享受了诸多旅行的快乐. 可能因为 23 年写的太多,24 年少了些创作的热情,也因此这份年终一直拖着。本来想效仿去年的风格过一遍一整年中比较有意义的事情,但是不太能下手。 不过,总得写点什么给这一年画上一个句号,今天总算交差了. 2024 年 Highlights 1. 旅行与徒步 2024 年跟 2023 年最大的变化, 是我 3 月份抽时间办了港澳通行证跟护照, 在香港跟国内完成了多次徒步旅行, 今年的最后一天也是在香港维多利亚港的烟花中度过的. 这篇文章的封面图就是香港维多利亚港的跨年烟花(因为自己拍摄的角度不太好, 所以网上找了这张图). 我在 2024 年的徒步旅行与 City Walk 记录如下: 3/30 - SRE 小组第一次以户外运动的形式进行团建,一起爬了凤凰山(鲲鹏径) 4/4 - 跟我妹一起逛了仙湖植物园,很多奇花异草,另外回程意外爬上了梧桐山,给我俩都累坏了, 当然也很开心 4/14 - 第一次去香港玩,从维多利亚港沿着海岸线一路徒步到坚尼地城,然后坐地铁回家,海岸线很美,香港也有独特的风土人情在 解锁成就 - 第一次出境中国大陆 5/2 ~ 5/3 - 单人刷了一遍香港麦里浩径二段,从北潭凹管理站下车开始徒步,沿着麦理浩径二段又走回到北潭凹站,算是环线,大概 20 到 30 公里的样子,中间在西湾村租帐篷露营了一晚上 解锁成就 - 第一次露营、第一次在山林里孤身赶夜路 不知道是谁,在牛粪上插鲜花 emmm 这一段风景绝赞,全程最佳! 5/18 - 5/19 - 单人背着 17 公斤的背包重装徒步麦理浩径三段,中间还解锁了一些支线,全程走了 14 公里,走走停停近 8 个小时(体力不够所以走得很慢),夜间在水浪窝营地露营了一晚上 解锁成就 - 第一次重装徒步 山顶继续前行,远方城市灯火通明 清晨 7 点多,解决卫生问题,顺便随处走走,发现营地标牌 5/25 - 继续单人徒步麦径四段,坐九巴 299X 路到大浪窝站下车开始徒步,从四段起点出发的时间为 13:20,到达大老山隧道站时已经是 22:20, 全程差不多 9 个小时,超过 21 公里,背的还是 16kg 的重装背包 解锁成就 - 第一次重装徒步超过 20 公里,到目前为止的人生巅峰了 这应该是四段风景最好的一段, 可惜雾太大 6/22 - 6/23 - 徒步至铅矿坳营地露营 解锁成就 - 这次带了卡式炉气罐跟炊具,第一次户外做饭,很香 6/29 - 与同事四人组团麦理浩径一二段徒步 从北潭凹反穿,一路走到万宜水库东坝,因为计划单日徒步,这次只背了 30 升小包,运动量相比之前几次并不大 解锁成就 - 第一次与同事组团长距离徒步、第一次大雨中徒步(有风险,不建议冒雨上山) 08/03 - 跟老妹一起在香港维多利亚港沿海漫步,人比之前五一假期少了太多,体验非常好!可以悠闲地慢慢走,拍照,聊天 8/21 - 8/23 - 在香港参加为期三天的 KubeCon China 2024, 顺便跟着朋友逛香港 主会场过厅,海景不错的 冰镇饮料也可以随便喝,好哇 好多的 CNCF 贴纸,可以随便拿,我给同事也带了一些 香港夜景,相当繁华哪 Linus 咱的合影 10/18 - 10/19 - 公司团建,在惠州东江玩皮划艇,18 公里,挺愉快 跟同事划皮划艇 江边放点烟花,不得不说公司是会玩的 10/26 - 10/27 - 跟同事武功山徒步,10/25 提前下班坐高铁到长沙休息,10/26 早上坐高铁到萍乡再叫车送到武功山下开始徒步。我们是反穿,第一天徒步到云中峰客栈住宿,第二天上午继续徒步到武功山大门口,中间乘了两段下山索道。两天武功山都起大雾,没看到日出,视野也差了许多,但云海也还算不错,在山脊线上走,两边都是悬崖,而且还好大的风,还是有点刺激的 10/27 凌晨, 喝着热水欣赏早晨四五点的山景 10/27 快清晨六点了,对面山上的早餐叫卖声隔这么远都听得到 10/27 从云中峰客栈再次出发 11/23 - SRE 小组深圳梅林登山徒步, 路程大概 14 公里, 早上 9 点 30 从梅林水库大坝出发,下午 14 点 50 到终点, 全程 5 个多小时 11/24 - 到香港招商局码头看海南舰(075 两栖攻击舰), 不过没拿到门票上船参观 11/30 - 陪朋友香港办银行卡, 顺便在皇后大道跟维多利亚港一直 City Walk 到晚上九点 沿着皇后大道走到了一条市集小街,节日氛围浓厚 K11 商场海边的圣诞树布景,好多人拍照 维港渡轮上回头,能看到标志性的摩天轮 12/31 - 2024 年最后一天, 在香港 City Walk, 晚上到维多利亚港看跨年烟花,人山人海,很有氛围 我拍的烟花,位置不好效果差挺多 我拍的烟花 - 2 我拍的烟花 - 3 关于香港徒步旅行的细节, 我之前专门写过篇文章, 有兴趣的可以看看: 2024 年香港徒步旅行记录(一) - This Cute World 总的来说, 我 2024 年的运动量远超 2023 年, 这是一个很好的开始. 2. 业余技术 今年业余技术上的进展比较符合去年底的期望. 首先是在我 Homelab 上更深入地使用了 NixOS 系统, 其次也发表了一些不错的 NixOS 文章, 还给 Nixpkgs 提了一些 PR, 另外去年做的几个 Nix 相关开源项目的 stars 也持续增长. 其次是在 Linux 系统编程跟 Rust 语言方面取得了不错的进展, 学习这些技术的过程中, 对过去遇到的许多 Linux 系统故障也有了更深的理解. 算是年底两个月最有价值的技术突破. 2024 年我写过的一些技术文章: 个人数据安全不完全指南 这是我 2023 年 5 月开始的一个长期计划,到 2024 年中时这份计划基本落地,写了这篇博客总结我当前的方案。 Kubernetes 集群伸缩组件 - Karpenter 这篇文章来自我过去几年工作中对 Karpenter 的研究与改造经验. KubeCon China 2024 之旅 参加 KubeCon China 2024 的一些感受, 技术跟旅行结合的一篇文章. OS as Code - 我的 NixOS 使用体会 在知乎上回答了一个关于 NixOS 的问题写的文章, 英文版还得到了 NixOS 官方的推特转发. NixOS 在 Lichee Pi 4A 上是如何启动的 这篇实际是 23 年的存货. 24 年我写的文章相较 23 年少了不少, 不过整体质量是有所提高的. 考虑到 24 年我在旅行徒步以及关心家人上花了许多时间, 这个成绩也可以接受. 最后再对比下从 2024 年 12 月 31 日到现在,我的 GitHub Metrics 统计数据: 2023/12/31 GitHub 统计数据 2025/01/01 GitHub 统计数据 2024 年我没有开什么新的项目, 上述成绩基本都是 2023 年的旧项目 Stars 稳步增长带来的. 3. 工作 工作上, 2024 也仍然是按部就班的一年, 我有做一些新技术的尝试, 但总体来说变化不多. 与往年不同的是, 今年在工作上遇到的更多是技术之外的问题. 一些团队协作、沟通、管理等问题, 让我认识到了公司与各个团队的另一面, 以及人的复杂性. 单纯从工作内容的角度看, 工作越来越得心应手, 相对的也就越来越难以激发我的兴趣与动力, 对 ADHDer 而言要按部就班地把这类工作做好, 挑战很大. 总之多方因素影响下, 我在 24 年底不想干了, 遂向 leader 提出了辞职, 目前已经确定 last day 是 2025 年 1 月底, 正在交接工作中. 我 2021 年入职这家公司, 到离开大概是 3 年零 10 个月, 一段说长不长说短不短的时光. 这是我从业生涯的第二份工作, 回过头看, 21 年刚入职时我还是萌新一个, 做事情都很小心翼翼, 当时我对公司的评价是 梦幻般的待遇,不限量的三餐供应,窗明几净的落地窗工位,这一切都像是在做梦 还有 22 年初发过的推文也是相当正面的: 新办公区真好呐~ 值此良辰美景,好想整个榻榻米坐垫,坐在角落的落地窗边工作🤣 pic.twitter.com/FASffzw8N3 — ryan4yin | 於清樂 (@ryan4yin) January 17, 2022 从入职一直到 24 年上半年, 我在这里的工作体验都是很不错的. 只能说很感慨吧, 三年多的时间, 我在这里学到了很多. 我很感谢我的两任 leader, 他们都给我了很多机会, 让我能够在工作中不断成长. 也很感谢 SRE 的其他同事, 在我遇到困难时给予了我很多帮助. 后会有期! 4. 阅读 2024 年我在阅读方面, 最大的亮点应该是终于读完了《Linux/Unix 系统编程手册(上册)》, 并且使用 Rust 做了不少习题. 2024 年完整的已读书目: 《户外旅行终极指南:基础装备、露营技能、交通方式、饮食、环境和急救》:内容很多,但都比较入门级,好处是图很多,读着很轻松,几个小时就能走马观花全过一遍。 Programming Kubernetes - Developing Cloud Native Applications: 2022 年开始读的书,但当时没啥兴趣。最近在照猫画虎写 karpenter provider,有了些编程经验后又对它产生了兴趣。书不厚,花了三个小时走马观花全读了一遍,代码内容大都跳过了(不少也过时了,譬如还在介绍 dep),做了些笔记。挺有帮助,帮我系统地梳理了最近折腾 karpenter 学到的 operator 编程相关知识。 走出荒野 2021 年 2 月读此书的评价:「没读书的内容前,我完全没预料到作者的人生曾如此不堪。 最近刚离职,毕业后的第一份工作就这样结束了。心里好多想法,也好想多看看这个世界。 嗯,有点想来上一次徒步旅行了哈哈。」 2024 年 7 月重读评价:「今年我爱上了徒步,重读此书,又有新知。我想徒步也类似跑步,也存在村上春树所言的跑者蓝调。今年已经在香港麦理浩径上完成了 5 次徒步,越发上瘾。我想我也该带老妹体验下,见山见水见自我。」 Linux/Unix 系统编程手册(上册) 未读完书目: 《Educated - A Memoir(中文名:你当像鸟飞往你的山)》 Linux/Unix 系统编程手册(下册) 《这才是心理学 - 看穿伪科学的批判性思维 第 11 版》 年初定的目标是每月一本书, 但实际上只读完了 4 本, 25 年再接再厉吧! 2025 年展望 我在去年年终总结的文末写了, 我对自己 2024 年的期许是「工作与业余爱好上稳中求进,生活上锻炼好身体、多关心家人」,感觉确实应验了。由衷地喜欢与感谢这一年来乐观、开朗、积极的自己, 也感谢身边的亲人、朋友、同事. 人的一生, 尤其是 ADHDer 的一生要怎么过才能拥有鲜活、快乐且充实的人生? 我们天生只有在自己喜欢的事情上才能摆脱拖延症并获得足够的专注力, 这就注定了我们无法适应「稳定、枯燥」的工作与生活. 2025 年, 我不急着找下一份工作, 计划先 gap 几个月, 调整下自己的心态, 重新审视自己的职业生涯, 以及未来的规划. 世界那么大, 我想去看看, 也许在旅行中能找到一些答案. 因此, 我给自己定的 2025 年目标是: 深入浅出 Linux, 徒步中国、徒步世界 作为一名从未出过国的 IT 农民工, 我对世界上其他国家的认识仅仅停留在书本与各种网络资料上. 为了能够亲眼见识下中国以外的世界, 我计划在 2025 年开始走出国门, 亲身体验不同国家的文化与风景. 我已经办好了日本签证, 正在办韩国签证, 打算先去这两个国家徒步旅行. 如果签证顺利的话, 我在日韩之后还想去尼泊尔、马来西亚、澳洲跟欧洲旅行. 但这个并没有那么急, 如果 2025 年 gap 的这几个月不够用的话, 未来还有很多机会. 除了去国外旅行满足我对「外国」的好奇心, 我也很想在 2025 年去亲眼见证 960 万平方公里的中国大地, 亲眼看看这片土地上的鬼斧神工. 不过暂时还没有很明确的计划, 中国的风景太多太美, 也许我会先去青海, 又或者是广西? 路还很长, 2025 年, 让我用双脚去丈量这个世界~ Carpe Diem. Seize The Day, Boys. Make Your Lives Extraordinary. – 《死亡诗社》

2025/1/7
articleCard.readMore

KubeCon China 2024 之旅

前言 很早就有了解到今年的 KubeCon China 会在香港举办,虽然有些兴趣,但我最初是有被 KubeCon 高昂的门票价格劝退了的。 有时候不得不相信运气的魔力,机缘巧合之下,我从朋友 @Kev 处得知了 KubeCon 的「最终用户门票计划」并借此 0 元购了门票,又邀上了 0xFFFF 社区 的@Chever-John @0xdeadbeef @茗洛 三位朋友一起参加,在香港租了个 airbnb 住宿,期间也逛了香港城市中的不少地方,收获颇丰。 其实本来也尝试过邀请其他认识的朋友同事,但都因为种种原因无法参加,略感遗憾。 TL;DR 本文多图,也挺多技术无关的内容,为了方便想了解技术的朋友,我先做个大致的总结。 从 KubeCon 回来后我又听了些 CNCF 其他的会议视频,其中比较有印象的是这几个: Keynote: Cloud Native in its Next Decade - KubeCon Europe 2024: 聊了 CloudNative 的未来,结论跟这次在 KubeCon China 现场听到的内容类似。 Another Choice for Istio Multi-Cluster & Multi-Network Deployment Model - KubeCon Europe 2024: 提到了 Istio 多集群方案的痛点,介绍了中国移动的解决方案。我一直有想尝试多集群方案,但一直担心 hold 不住而不敢下手,这个视频给了我一些启发。 DRA in KubeVirt: Overcoming Challenges & Implementing Changes - KubeCon Europe 2024: DRA 也是 K8s 中的新 API,这里介绍如何在 kubevirt 中使用 DRA 解决一些问题。从这里能感觉到 K8s 这几年还是弄了不少新东西的。 结合 KubeCon China 三天的经历,以及上面这些视频的内容,我大概的感觉是: (几乎)所有聊网络的人都在聊 eBPF, Envoy, Gateway API. Istio 的 Ambient Mode 吸引了很多曾经因为 sidecar 性能问题而放弃使用服务网格的公司。 Karmada 多集群管理方案在许多公司得到了实际应用,挺多讲这个的。 AI 与 WASM 方面的演讲也有不少,但感觉有些无趣,可能是我对这方面不太感兴趣。 蔚来汽车、中国移动等公司正在尝试将 K8s 应用在边缘计算场景(智能汽车、通信基站),但这些离普通互联网公司有点远。 云原生的未来十年会变成什么样? Kubernetes, Service Mesh 等过去十年的新兴技术,现在已经成为了「Boring but useful infrastructrue」,它们将是其他云原生技术潮流的基石,被广泛应用,但自身不会再有太多的变化。 AI, eBPF, WASM, Rust 等技术也将在未来十年走向成熟,取代 Kubernetes 当前的地位。 KubeCon China 2024 的会议视频将会陆续被添加到如下这个 Youtube Playlist 中,有兴趣的朋友可以一观: KubeCon + CloudNativeCon + Open Source Summit + AI_dev China 2024 - Youtube 视频相关的 PPT 可以在这里下载: KubeCon China 2024 - Schedule 技术 这次我主要关注的是 Istio、Gateway API 相关的议题,最近在研究 Istio 的 Ambient Mode,因此希望能够从会议中了解到更多的实现细节与其中的权衡。 三天下来听到的内容也很好的满足了我的期待,Istio / Envoy Gateway / Ingress Controller 的几位核心贡献者分享了很多这些项目的最新进展,实现细节,以及未来的发展方向。 Ambient Mode 在最近 beta 了,是我关注的重点,总结下目前了解到的几个关键点: istio/ztunnel: 一个 userspace 的 l4 proxy,仅支持处理 L4 流量。 ztunnel 会分别与上游和下游建立连接,导致 A <=> B 之间的一个连接会变成 A <=> ztunnel <=> ztunnel <=> B 三个连接,这也会带来性能开销。 因为所有流量都经由 ztunnel 转发,更新 ztunnel 会导致短暂的流量中断。感觉比较好的解决方案是采用 recreate 的更新策略 + 滚动更新节点组下的所有节点来更新 ztunnel. ztunnel 使用的 HBONE 协议强制启用 mTLS,无法关闭,对于不要求安全性的场景会带来额外的性能开销。 istio/proxy: 基于 envoy 的 l7 proxy,在 ambient mode 下它被单独部署为一个 waypoint, 用于处理 L7 流量。 waypoint 架构下 proxy 与上下游 Pod 很可能在不同的节点上,这会导致比 sidecar 模式多一次网络跳转,可能带来性能损耗,以及跨 Zone 流量上涨。 waypoint 与 sidecar 都是 envoy,它是通过减少 envoy 容器的数量来达到减少资源消耗的目的。 以及一些其他方案: kmesh: 架构类似 Ambient Mode,特点是完全使用 eBPF 来实现 L4 proxy,好处是 eBPF 直接在内核空间修改网络包,不需要与上下游分别建立连接。因此性能更好,而且 eBPF 程序更新不会中断流量。 cilium service mesh: 特点是 per-node proxy,l7 的 envoy proxy 运行在每个节点上,而不是像 waypoint 一样单独通过 deployment 部署。但也存在一些问题: per-node proxy 无法灵活地调整资源占用,可能会导致资源浪费。 同一节点上的所有流量都由同一 envoy proxy 处理,无法实现 waypoint 那样的 namespace 级别的流量隔离。 与 cilium cni 强绑定,必须使用 cilium cni 才能使用 cilium service mesh. 据说使用起来较为复杂? 总体而言,KubeCon 是一次了解行业前沿技术动态、跟项目开发者及其他行业内的技术人面对面认识交流的好机会,可以帮助自己提升技术视野,维持技术热情与动力,不至于局限在公司业务中闭门造车。 行程 住宿 因为要在香港呆三天,衣食住行是必须要考虑的事情。这方面我拉上的几位朋友都比较有旅行住宿的经验,我们最后在香港找了个离会场不远的 airbnb 住宿,最终的体验也是相当不错。房间干净整洁有格调,虽然我觉得稍微有点小了,但朋友说这个空间在香港都是一家三四口住的标准,已经吊打同价位的酒店了。 Day 1 虽然提早定了住宿,做了点功课,但第一天就出了问题——深圳这边一直下雨导致 @Chever-John 的飞机直接被取消,改订了另一趟航班也晚点。虽然正点到达了会场,但他一晚上就睡了俩小时,在深圳定的前一晚的酒店也没住成,第一天看他整个人都听得迷迷糊糊的。不过没事,至少我听了个爽 说回正题,到了会场领完胸牌,我们就开始了为期三天的 KubeCon China 之旅。 具体的技术内容已经在前文总结过了,这里主要就贴些照片吧。 主会场过厅,海景不错的 去各会议室的过道,酒店的服务很到位 午休茶歇,吃饱喝足 冰镇饮料也可以随便喝,好哇 几位大佬聊 Istio 与 Gateway API 的未来 然后晚上@茗洛带着我们逛了香港的诚品书店,书店好几层,但感兴趣的书不多。后面又逛了好多电子商城、二次元周边店,我算是开了眼界。 《我推的孩子》 另一本 好多二次元钢琴谱,有《四月是我的谎言》 不知道逛到了哪,到处都是二次元周边 轻小说书店 1 轻小说书店 2 轻小说书店 3 第一天就差不多是这样,听了点技术,晚上逛了逛香港,回去休息。 Day 2 好多的 CNCF 贴纸,可以随便拿,我给同事也带了一些 我收获的 CNCF 贴纸 首先是听了华为的演讲,介绍了 Kmesh 的方案创新,技术细节讲得很赞。想看 PPT 与视频请移步用内核原生无边车架构彻底改变服务网格 - Xin Liu, Huawei Technologies Co., Ltd. 华为介绍 Kmesh 介绍 Kmesh 如何借助 eBPF 实现热更新不中断连接 还听了晋涛老师的十年云原生之旅:容器技术和Kubernetes生态系统的演变 - Jintao Zhang, Kong Inc. 晋涛不愧是行业老将,这么早就开始玩 Docker 了 然后晚上我们随便走了走逛了逛,看了看香港海边夜景。 香港夜景,相当繁华哪 灯红酒绿,游人如织 路上碰到了《商务印书馆》 Day 3 第三天早上是我这次最期待的,Linus 的访谈,见到了本人,这次行程也圆满了。 Linus 第三天没什么我特别感兴趣的话题,听完 Linus 的访谈后随便逛了逛,跟几位 朋友合了个影,就搭地铁回家了。 咱的合影 咱的 PC 与鲨鲨合影 另外朋友听了个 TiDB 的演讲,看 PPT 是有点意思的哈哈。 TiDB 以及在项目展厅三天逛下来,我帆布袋领了四个,T恤领了三件,还有别的小礼品一堆,吃的喝的都不用说了,管够。另外看网上不少信息说香港的服务业态度很差,但这家酒店可能星级比较高,体验还是相当到位的。 总之,体验相当不错的,有机会的话明年还来!Love you, KubeCon China & Hong Kong!

2024/8/27
articleCard.readMore

Kubernetes 集群伸缩组件 - Karpenter

前言 Kubernetes 具有非常丰富的动态伸缩能力,这体现在多个层面: Workloads 的伸缩:通过 Horizontal Pod Autoscaler(HPA)和 Vertical Pod Autoscaler(VPA)等资源,可以根据资源使用情况自动调整 Pod 的数量和资源配置。 相关项目: metrics-server: 采集指标数据供 HPA 使用 KEDA: 用于支持更多的指标数据源与触发方式 kubernetes/autoscaler: 提供 VPA 功能 Nodes 的伸缩:根据集群的负载情况,可以自动增加或减少 Nodes 的数量,以适应负载的变化。 相关项目: kubernetes/autoscaler: 目前最流行的 Node 伸缩方案,支持绝大多数云厂商。 karpenter: AWS 捐给 CNCF 的一个新兴 Node 伸缩方案,目前仅支持 AWS/Azure,但基于其核心库可以很容易地扩展支持其他云厂商。 本文主要介绍新兴 Node 伸缩与管理方案 Karpenter 的优势、应用场景及使用方法。 Karpenter 简介 Karpenter 项目由 AWS 于 2020 年创建,其目标是解决 AWS 用户在 EKS 上使用 Cluster Autoscaler 做集群伸缩时遇到的一些问题。在经历了几年发展后,Karpenter 于 2023 年底被捐献给 CNCF(kubernetes/org#4258),成为目前 (2024/07/10)唯二的官方 Node 伸缩方案之一。 我于 2022 年 4 月在做 Spark 离线计算平台改造的时候尝试了 Karpenter v0.8.2,发现它的确比 Cluster Autoscaler 更好用,并在随后的两年中逐渐将它推广到了更多的项目中。目前我司在 AWS 云平台上所有的离线计算任务与大部分在线服务都是使用 Karpenter 进行的集群伸缩。另外我还为 karpenter 适配了 K3s 与 DigitalOcean 云平台用于一些特殊业务,体验良好。 Karpenter 官方目前只有 AWS 与 Azure 两个云平台的实现,也就是说只有在这两个平台上 karpenter 才能开箱即用。但考虑到它在易用性与成本方面的优势以及在可拓展性、标准化方面的努力,我对它的未来发展持乐观态度。 Karpenter 与 Cluster Autoscaler 的对比 Cluster Autoscaler 是目前社区最流行的 Node 伸缩方案,基本所有云厂商的 Kubernetes 服务默认都会集成它。 Karpenter 与 Cluster Autoscaler 的设计理念与实现方式有很大的不同。 Cluster Autoscaler 是 Kubernetes 平台上早期的集群伸缩方案,也是目前最流行的方案。但它做的事情比较有限,最大的问题是它本身并不直接管理集群的节点,而是借助云厂商的伸缩组 (AutoScaling Group)或节点池(Node Pool)来间接地控制节点(云服务器)的数量。这样的设计导致了一些问题: 部署与维护比较繁琐:需要先在云厂商的控制台上创建好伸缩组或节点池,然后再在 Kubernetes 集群上部署 Cluster Autoscaler,并将伸缩组或节点池的名称等信息填写到 Cluster Autoscaler 的配置文件中。增删节点池时也需要走一遍这个流程。 能力受限于云厂商的伸缩组或节点池服务:如果云厂商的伸缩组或节点池服务不支持某些功能,那么 Cluster Autoscaler 也无法使用这些功能。 举例来说,AWS EKS 的 Node Group 功能非常难用,毛病一大堆。但如果要用 Cluster Autoscaler,你就没得选,Node Group 再难用也只能忍着。 而 Karpenter 则完全从零开始实现了一套节点管理系统,它直接管理所有节点(云服务器,如 AWS EC2),负责节点的创建、删除、修改等操作。 相较 Cluster Autoscaler, Karpenter 的优势主要体现在以下几个方面: 声明式地定义节点池: Karpenter 提供了一套 CRD 来定义节点池,用户只需要编写好 Yaml 配置部署到集群中,Karpenter 就会根据配置自动申请与管理节点。这比 Cluster Autoscaler 的配置要方便得多。 以 AWS 为例,你简单地改几行 Yaml 配置,就可以修改掉节点池的实例类型、AMI 镜像、数量上下限、磁盘大小、节点 Labels 跟 Taints、EC2 Tags 等信息。 借助 Flux 或 ArgoCD 等 GitOps 工具,你还可以实现自动化的节点池管理以及配置的版本控制。 成本感知的节点管理:Karpenter 不仅负责节点数量的伸缩,它还能根据节点的规格、负载情况、成本等因素来选择最优的节点类型,以达成成本、性能、稳定性之间的平衡。 - 具体而言,Karpenter 在成本优化方面具有这些 Cluster Autoscaler 不具备的功能: Spot/On-Demand 实例调整: 在 AWS 上,Karpenter 可以设置为优先使用 Spot 实例,并在申请不到 Spot 实例时自动切换到 On-Demand 实例,从而大大降低成本。 多节点类型支持: Karpenter 支持在同一个集群中使用多种不同规格的节点,并且支持控制不同实例类型的优先级、数量或占比,以满足不同的业务需求。 节点替换策略:Karpenter 支持灵活的节点替换策略,可以通过 Yaml 控制每个节点池的节点替换条件、频率、比例等参数,以避免因节点替换导致的服务不可用。 节点的生命周期管理:Karpenter 支持定义节点的生命周期策略,可以根据节点的年龄、负载、成本等因素来决定节点的续租、下线、销毁等操作。而 Cluster Autoscaler 只能控制节点的数量,它不直接管理节点,也就做不到此类节点的精细管理。 主动优化:Karpenter 支持主动根据负载情况使用不同实例类型的节点替换高风险节点,或合并低负载节点,以节省成本。 Pod 精细化调度:Karpenter 本身也是一个调度器,它能根据 Pod 的资源需求、优先级、Node Affinity、Topology Spread Constraints 等因素来申请节点并主动将 Pod 调度到该节点上。而 Cluster Autoscaler 只能控制节点的数量,并无调度能力。 快速、高效:因为 Karpenter 直接创建、删除节点,并且主动调度 Pod,所以它的伸缩速度与效率要比 Cluster Autoscaler 高很多。这是因为 Karpenter 能快速获知节点创建、删除、加入集群是否成功,而 Cluster Autoscaler 只能被动地等待云厂商的伸缩组或节点池服务完成这些操作,它无法主动感知节点的状态。 总之,个人的使用体验上,Karpenter 吊打了 Cluster Autoscaler. Karpenter 的使用 这部分建议直接阅读官方文档Karpenter - Just-in-time Nodes for Any Kubernetes Cluster. 适配其他 Kubernetes 发行版与云服务商 如果你使用的是 Proxmox VE, Aliyun 等其他云平台,或者使用的是 K3s, Kubeadm 等非托管 Kubernetes 发行版,那么你就需要自己适配 Karpenter 了。 Karpenter 官方目前并未提供详细的适配文档,社区建议以用于测试的Kwok Provider 为参考,自行实现。Kwok 是一个极简的 Karpenter Provider 实现,更复杂的功能也可以参考 AWS 与 Azure 的实现。 国内云服务方面, 目前已经有人做了 Aliyun 的适配,项目地址如下: karpenter-provider-aliyun 对于个人 Homelab 玩家来说,使用 Proxmox VE + K3s 这个组合的用户应该会比较多。我个人目前正在尝试为这个组合适配 Karpenter,希望能够在未来的文章中分享一些经验。项目地址如下: ryan4yin/karpenter-provider-proxmox: 因为我已经换了 KubeVirt, 这个项目缺乏开发动力,暂时搁置,并改为私有仓库了… Karpenter 与 Cluster API Cluster API (CAPI) 是 Kubernetes 社区提供的一个用于管理多集群的项目,从介绍上看,它跟 Karpenter 好像没啥交集。但如果你有真正了解使用过 CAPI 的话,你会发现 Karpenter 与 CAPI 有一些功能上的重叠: CAPI 的 Infrastructure Provider 专门负责处理云厂商相关逻辑的组件。Karpenter 的标准实现内也包含了 cloud provider 相关代码,还提供了 NodeClass 这个 CRD 用于设定云服务器相关的参数。 Cluster API Bootstrap Provider (CABP) 负责将云服务器初始化为 Kubernetes Node,实际上就是生成对应的 cloud-init user data. Karpenter 的 NodeClass 实现中同样也包含了 user data 的生成逻辑。 Cluster API 的目标是多集群管理,并且它的设计上将 Bootstrap, ControlPlane 跟 Infrastructure 三个部分分离出来了,好处是方便各云厂商、各 Kubernetes 发行版的接入,但也导致了它的架构比较复杂、出问题排查起来会比较麻烦。 历史案例:Istio 曾经就采用了微服务架构,结果因为性能差、维护难度高被不少人喷,后来才改成了单体结构。 而 Karpenter 则是一个单体应用,它的核心功能被以 Go Library 的形式发布,用户需要基于这个库来实现自己的云平台适配。这样的设计使得 Karpenter 的架构简单、易于维护。但这也意味着 Karpenter 的可扩展性、通用性不如 Cluster API. 从结果来看,现在 Cluster API 的生态相当丰富,从Provider Implementations - Cluster API Docs 能看到已经有了很多云厂商、发行版的适配. 而 Karpenter 2023 年底才捐给 CNCF,目前只有 AWS 与 Azure 的实现,未来发展还有待观察。 那么有没有可能结合两者的优势呢?Kubernetes 社区其实就有类似的尝试: Cluster API Karpenter Feature Group Notes Karpenter Provider Cluster API Open Questions elmiko/karpenter-provider-cluster-api 上面这个实验性质的项目尝试使用 Karpenter 作为 Cluster API 的 Node Autoscaler,取代掉现在的 Cluster Autoscaler. 我目前对 Cluster API 有些兴趣,但感觉它还是复杂了点。我更想试试在 Karpenter 的实现中复用 Cluster API 各个 Provider 的代码,快速适配其他云厂商与 Kubernetes 发行版。 参考资料 Cluster Autoscaling - Kubernetes Official Docs

2024/7/10
articleCard.readMore

2024 年香港徒步旅行记录(一)

缘起 在 2023 年年度总结中,我给 2024 年定下的目标是「工作与业余爱好上稳中求进,生活上锻炼好身体、多关心家人」,为此今年我做了许多旅行的准备。 在深圳呆了快五年,挺长时间里一直将自己局限在技术与工作中,少有时间去关注周围的世界——生存已属不易,没有多余的精力去关心其他事情。2023 年是一个分界点,工作上越来越得心应手,手头也不再紧张,个人精力跟业余时间得以解放,我自然开始追求更多的生活体验。 说是要锻炼身体,多旅游,但也没啥明确的计划,只是想着该出国走一走开开眼界,于是 3 月份到出入境管理局搞定了护照跟港澳通行证。 香港离深圳近得很,自然就想着先去香港多走走,这是缘起。 而这件事情后来的发展,现在回看起来,跟我当初折腾 NixOS 或电子电路的故事如出一辙,大概性格使然吧哈哈。 简单总结下呢,就是 4 月份去香港海边徒步了一天,从维多利亚港沿着海岸线一路走到坚尼地城,然后就爱上了这种在海边徒步的感觉,回来后也一直念念不忘,查了许多香港徒步路线的资料。 五一假期的时候跟着小红书和 Bilibili 上的攻略,徒步了香港麦理浩径第一段的部分以及第二段全程。因为准备不足,举着手机走了一个小时夜路,并且在西湾村营地租帐篷露营了一晚上。这次体验又让我迷上了夜间徒步跟露营。一回家就查各种徒步露营装备,疯狂下单,收了一大堆快递,花掉了一万多 RMB… 接着就是 5 月 18 日,背上我 65L 的重装背包,总重量大概有 17 kg,从深圳坐地铁 + 大巴直达北潭凹,爬了一遍麦理浩径三段,夜间在水浪窝营地露营一晚,第二天一早直接打道回府。本来计划是周末两天刷完三四段,甚至有余力再走完第五段的。但是我显然高估了自己的体力,而且背着 17 kg 的重装背包,这也不是一件轻松的事情。 休息了一周后,在 5 月 25 日,我又完成了麦理浩径第四段的徒步。这次出发前根据上次的经验精简了装备,背包重量应该轻了大概 1kg,但仍旧有 16kg。四段是麦径难度最高的一段,而且终点基维尔营地不接受个人预约,又导航徒步到大老山隧道站乘九巴 74X 路至广福邨下车,再步行到地铁站乘车到罗湖口岸回家,到达车站时已经是 22:20。全程差不多 9 个小时,步行超过 21 公里,是我目前的人生巅峰。 以上就是我到目前为止的香港徒步经历,目前对重装徒步兴趣浓厚,计划今年先把麦理浩径全程走完, 积累经验,后续再看看香港跟国内外的其他徒步路线。 一、从维多利亚港到坚尼地城 时间:2024-04-14 这是我第一次出境游,整个行程都是临时起意,没有做任何准备,即使是过了罗湖口岸,站在东铁线地铁上的时候,我对整个行程也并无太多期待,心里想着无非是不同的人与物,无甚特殊的。但总得出去走走,看看大陆之外究竟是个什么样子,所以就这么随意地来到了香港。 具体有多随意呢?就这么说,过了口岸没多久,手机就没信号了,我这才意识到香港终归不是大陆,慌得我又出站再往回坐,在罗湖口岸连上网络开了个漫游跟流量包,这才算是正式开始了我的香港之旅。 最初期待甚少,但现实常常超出我的想象。在香港徒步的一日,我切实感受到了更宽广更多元化的世界,这是在大陆境内无法体会到的。 刚上东铁线甚至刚出地铁站的时候,我对自己已经出境这一点尚无实感,因为香港的地铁跟深圳实在太像了,不论是地铁车厢的设计还是车站的建筑风格、干净整洁的程度,都跟深圳非常相似。 后面跟朋友聊起来,才知道国内深圳地铁跟香港地铁是有合作关系的,很多技术最初都来自香港,算是一脉相承了。 在去往维多利亚港的路上,看到香港多的双层巴士、随处的繁体字跟英文标识,才逐渐感觉到这里确实是个不同的地方。 出了会展地铁站,往维多利亚港去的路上 路上遇到的敞篷观光大巴 接着就到了维港的海边步道,海景很美,两岸都是高楼大厦,海上有很多游船,还有很多钓鱼的人。 海边 两岸都挺繁华 大海与高耸的大楼 钓鱼佬 从另一个角度看钓鱼佬 接着我意识到,从我住处可全程地铁直达维港,坐个地铁再浅浅走几步路就能看到这么漂亮的海景,这不比去深圳大鹏半岛方便多了!我为此兴奋起来。这风景吊打深圳湾,但深圳湾步道上的人甚至比维港还多,只能说深圳人真的太多了… 补充:听说深圳湾以前真的是海边,但填海造路使它成了现在的烂泥滩 沿着海岸线继续走,渐渐发现人群里外国游客相当的多,粗略估算超过一半是外国人,这让我感受到了香港的国际化氛围。 裹头巾的女游客在自拍,有点异域风情 外国游客们音箱歌一放,就地跳起了舞来 海边还有很多细节值得一看。 海岸上的微观建筑,挺有意思 天星小轮的港口,休息、舞蹈的人挺多 天星小轮 摩天轮旁的小摊贩,这小摩托车比较有年代感了... 维多利亚港的免费 WiFi 在维港逗留玩耍了一个小时,买了点面包当午餐吃了,然后就沿着海边步道继续往前走,也没啥明确的目的地,反正累了就回家。维港的人很多,但出了维港后步道上人就少了很多,走在这样的路上看着风景,相当闲适。 海与城市 沿着海岸线继续走,阳光正好 波光潋滟 充满涂鸦的转角,有人在拍照 似是废弃的小码头 被改造成游玩地点的码头 海边步道上的小店,小女孩在选冰棒 香港的叮叮车 悠闲、轻快地随走随停,大概一个半小时后,到达了步道的终点——坚尼地城,我还有点失望,这步道居然就这么没了。只好掉转方向往城市内走,市内倒也别有一番风景。 天快黑了,沿街商店亮起了灯牌 这家店门口人挺多,或许味道不错? 在市内逛了逛,确认了香港各种店铺的特点是小而美,各种店铺都非常小,但弄得比较精致,能看得出花了心思。相对而言深圳的商场店铺就大气很多了,但也缺了香港这些店铺的种种细节。 玩得尽兴,又找了家店吃饱喝足,打道回府。 坚尼地城地铁站,回家啦 这一趟下来,我的感觉是,香港确实是国际化大都市,跟深圳的气息很不一样,海边随处可见的酒吧, 比例非常高的外国游客跟在香港工作的外国人,让我感觉到了一种国际化的氛围。至于城市的繁华程度,跟深圳各有千秋。 二、麦理浩径第一二段 我的路线(抄了近路) - Google Earth Map 食髓知味,在香港海边徒步一日后,我对香港徒步来了兴趣,一直瞅着机会再来一次。 到了五一假期,我终于有了机会,随便在小红书上查了些资料,发现麦理浩径一二段挺有名,还能露营,于是就决定是它了。 看天气预报最近几天都有雨,五一那天下得比较大没出门,顺便就买了迪卡侬冲锋衣、冲锋裤、速干 T 恤、大号水杯,想着明天就算有雨也要去。 幸而天公作美,第二天天气转阴,上午东西到货,又做了大半天准备才出门。因为计划是连续步行两天,所以跟着小红书上的香港旅游攻略,吃的穿的用的都带了挺多。 下午出境,首先买了 OPPO 9.9 元的香港流量包(五一特惠),然后坐地铁到深水埗(bù)地铁站整了张八达通,200 港币,其中 50 块押金。另外考虑到以后会常去香港,又多充了 100 块。 香港地铁 深水埗(同埠)办八达通实体卡,因为计划常来香港玩 接着就是各种地图导航,首先坐小巴到西贡码头,然后查半天导航,坐九巴 94 路到麦理浩径起点。 小巴要叫停车才停,真的 I 人(内向)天敌,决定以后尽量选九巴,下一站前按个 Stop (香港人称掀钟)这种才适合 I 人。 乘小巴到达西贡 乘上九巴 94 路去往麦理浩径起点 路边海湾 导航本来是说坐到麦理浩夫人度假村站,但我一下没注意就坐过了,然后下车时第一次「掀钟」不熟悉,又过了一站才按到 Stop,结果就是在「北潭凹管理站」才下车(16:34),然后往回走了半个小时才到「麦理浩径起点」(17:09)。 坐过了站,在北潭凹管理处下车 路上几乎没人,风景很好,很安静 麦理浩夫人渡假村 多走了半个小时公路才到达麦径起点,路上体验很好,主要是远离城市,很安静 从「麦理浩径起点」往万宜水库的路上(大网仔路)看到很多出租车来来往往,联想到之前查的攻略, 就知道这些出租车是为了接送徒步的人。我是单人徒步,为了省 150 的打车费,就选择了坐九巴到起点然后步行。 路上标牌 路上很多烧烤点,貌似是政府修建的,可以免费用,但没遇到过人在用这些烧烤点,可能季节或天气原因? 到达万宜水库岔路口时已经是 17:26 了,走第一段晚上肯定到不了我的目标地址「咸田湾」,而且我了解到的是二段风景最好。一番心理斗争后,我直接走了左边的路线,绕过麦径一段,直接走到西湾亭走二段的路线。 到达万宜水库岔路口,往右是正经麦径,不过比较晚了,我抄近路走了左边。 万宜水库 一路上依旧遇不到人,远离城市跟人群的感觉很奇妙 风景很不错 瞅一眼路边地图看看我在哪 抄近路到达西湾亭,从麦径起点算用了一个半小时 沿着大路走了整整一个小时才到西湾亭,为了省个出租车费我也是拼了。不过倒也不算亏,这两个小时的路上风景相当不错,而且没遇到几个人,体验非常棒!有种我很喜欢的孤独感。 这时已经是 18:34,天色已经快黑了,我有点慌,刚好有辆出租送人到这,我向司机问路,他好心地帮我指了路。从这里开始才是山路,前面两个小时一直走在大马路上,而且我的脚在走了两个小时后已经很痛了。 接着就手机打着灯,一直走到 19:10 才到西湾村。 走小路下山到西湾村 走到 M30 标距柱(每 500 米一个)时天已经黑了 快到西湾村了,路边开始出现路灯 看到第一个租帐篷的店,立马就整了套帐篷。押金 300 租一晚 200,跟国内比可能很贵,但跟香港劳动节期间 1300+ 的旅馆比已经性价比爆棚了! 我之前的目标是到咸水湾租帐露营篷,但到西湾村的时候天已经完全黑了,而且一路心惊胆战地连续赶了三个小时路后我也累得不行了,在往西湾村的路上我就一直憧憬着西湾村总该能找到地方住,万幸它没让我失望。 终于到了,租了个帐篷露营,200 港币一晚,因为港币不够付了人民币 我的帐篷 本来想吃口热乎的,但看到店里 55 港币起步的面食,我还是决定吃点自己的能量棒当晚餐。 洗澡收费 30,但人太多了,我就没洗。晚上海浪声、旁边帐篷里人的说话声等,环境变化太大,睡不着,小说看到凌晨三点,然后跑去海滩上听了听海浪声,回来的路上一路被村民家的狗狂吠,惊出我一声冷汗,万幸我还懂点这种场面的处理方式,盯着狗看,慢慢后退。即使走得远了,我还是一步三回头,置到回到营地把门关上才松了一口气。并且明白一个道理 —— 晚上还是不要这么跳脱的好… 西湾村手机信号挺好,但用的人太多带宽不够,很多站点怎么刷都刷不出来… 想起 21 年看过的《走出荒野》,讲的是通过徒步自我救赎的故事,打算翻出来读一读但网络问题根本刷不出来。 然后迷糊睡到了早上 7 点,出帐篷发现我这个营地人都走差不多了。想洗漱但意识到我带了牙刷却没带牙膏…因为最初是打算住旅馆的,谁 TM 知道居然没找到个便宜旅馆,香港的物价太 TM 离谱了, 逼得我连夜赶路到这里来住帐篷。 厕所也没上,早餐看太贵了也没吃,买了瓶 0.6L 的水(20 港币),07:24 直接出发了,早餐仍然是能量棒配水。 第二天一早从西湾村出发。 西湾村出发点 - M31 标距柱 没走几步就看到了西湾营地,原来西湾村过来没几步就是西湾营地,垃圾好多… 西湾营地 西湾营地这有黄色标牌,写着野猪出没。朋友也提醒过我,说它们不咬人,但如果你包里有吃的,它们会弄坏你的包。 过了西湾营地又开始爬山。 过了西湾营地开始上山,这张图漂亮 回望西湾营地 山上海景独好 07:58 到达我印象中全程风景最赞的地方 —— 接近咸田湾沙滩的一段步道。 这一段风景绝赞,全程最佳! 这段山路也很有味道 08:15 到达咸田湾沙滩,这里人很多,帐篷也很多。从这里开始接着是往赤径去,钻的就是山路了,这一段不是海景,但也别有韵味。 看到了咸田湾 咸田湾的独木桥 10:00 到达赤径,这一处水湾风景也很美!在这里玩了很久。 在赤径休息 10:30 到达赤径公厕解决了下个人卫生问题。意外发现赤径公厕个别涂鸦很可爱!随手一拍。 赤径公厕的简笔画 接着是从赤径往北潭凹的路,同样是起起伏伏。 继续往北潭凹走,左侧指路牌 路上遇到一泡插着鲜花的牛粪,很有意思,前面的女生蹲下拍照,搞得我差点以为花是她插的… 一路上再怎么陡峭的步道上都能看得到牛粪,挺原生态的哈哈。甚至回去路上在西贡码头汽车站都看到了两头牛在绿化带上吃草。 不知道是谁,在牛粪上插鲜花 emmm 接着一路上攀,脚踝已经酸痛得不行,非常痛苦。有一段累到直接坐在地上看了 20 分钟《走出荒野》。 到达 M044 柱子上的简笔画 11:57 到达麦径二段的终点,走不动了。 到达终点的公厕 终点的指示牌 又坐九巴 94 路回西贡市区,来去都是这趟车,从「北潭凹管理站」下车又从「北潭凹」上车,几乎是围着这一片走了刚好一圈。 又坐九巴 94 路回西贡市区 12:30 到西贡市区,饿得不行,找一圈吃饭的地儿发现,麦当劳居然是最实惠的——因为它价格跟国内差不多。于是搞了一顿麦当劳,发现公司有点事,因为今天我 Oncall,顺便拿出 MacBook 处理了下公司的事。是的没错,我背包里还有一台 1.4kg 的 MacBook Pro,真是要了老命。 到达市区,饿得不行到处找吃的 发现麦当劳是最实惠的,跟深圳差不多价。顺便处理个 Oncall... 累得不行,昨晚又没洗澡,一路上疯狂出汗,浑身气味比较感人。吃完饭随便逛了逛,就打道回府了。从西贡坐九巴 299x 路到沙田站,然后转乘东铁线到罗湖口岸,再坐深圳地铁回家。 这家店装修有点意思 理发店,香港物价... 这几家店面比较有年代感 西贡码头 总的来说,香港西贡这块开发得更好,步道很多,原生态的同时路线也足够成熟,而且过来比深圳大鹏半岛更方便,期待下次再来。 查攻略最有用的几个 APP: Bilibili -香港麦理浩径 一二段最全攻略。支线破边洲,蚺蛇尖一并献上 - 发现的最详细的攻略,相当用心 小红书 - 许多有用的信息 三、麦理浩径第三段 上次徒步走完麦理浩径二段后就有点上头了,刚回到家累得不行,脚都要废了,但隔天还想找个步道走走。另外就是这两次徒步都太随意,缺乏登山杖、登山鞋、背包等专业装备,而且露营还是租的人家帐篷,接着就是看各种徒步教程攻略,疯狂买买买。 买的一堆装备陆续到货,在家试用了好几天,比如说穿登山鞋上班、空调开 16 度在室内搭帐篷露营、床上铺蛋巢垫盖睡袋、晚餐吃自热米饭,等等不一而足 emmm 试用露营锅具、炉子以及食物 试用了好几天帐篷(室内露营 emmm) 目前的所有装备,算上没拍进来的衣物,总重量大概 14kg 接着 5 月 18 跟 19 两日又是个空闲的周末,这次做足了准备,计划两天走完麦理浩径三四段。 整理背包时,意识到气罐很难处理,带着上地铁、过海关,感觉都不太行,在香港我也不知道是否好买,所以把锅具跟炉子都踢出了背包,只带了两盒海底捞自热米饭跟一些能量胶、压缩饼干以及零食。另外水带得相当充足,3L 水袋 + 600ml 小水杯,光水就有 3.6kg. 大约 9 点多出发,首先是乘东铁线到沙田站,转程九巴 299x 路到麦边站,再转乘 94 路到麦径三段的起点。 麦边站等九巴 94 路 在二段终点解决完个人卫生问题,热了个身,顺便帮一批反穿麦径二段的大陆人合了个影,接着就开始上山。 三段一开始就是急攀,路面很陡,而且透露着一股年久失修的味道,路况比一二段差远了。 麦径三段-开头的急攀路段 三段的人流量相比二段那是断崖式下降,几乎依山遇不到人,有一种远离人世的孤独感。有的人可能会喜欢热闹,但我恰恰相反,相当享受这种孤独感。 上升太快,我又负重 17 公斤,没爬几步就累到要休息,甚至有点怀疑今天能不能走完三段。但辛苦带来的收获也挺大,越往上爬,风景越美,山景与海景交相辉映,让人心旷神怡。 风景很不错 山路 山路上美美自拍 继续急攀 在山顶还解锁了一些隐藏支线,因为走的人少,灌木丛茂盛,登山杖几乎没法用,这时候就很庆幸我穿了长袖紧身运动打底衣裤,不然走这种路小腿难免挂彩。 接着就是下降。 开始下降 山水共长天一色 Me - 发现眼镜确实变色了欸 下降路段走完,到达嶂上营地跟士多店。 这里再往前就是障上士多店 麦径路标 - M61 又开始上升,不过跟三段起始的那段比起来,这段路还算平缓。接着我高兴没多久,就到了一段相当陡峭,几乎没有台阶的路段。 上升,没有台阶的陡坡 一小段平路 坐下休息一会儿,顺便自拍一个 又二十多分钟后,累得不行,寻个地坐下,顺便回头看看。 回望 继续走,没多久就到了 17 点,天开始黑了,我也提前翻出了头灯准备着随时打开。 继续走 晚上 7 点,天开始黑了 到达山顶,已经打开了头灯 山顶继续前行,远方城市灯火通明 遇到的轻装夜爬团,应该是香港学生,而重装徒步的我已经精疲力竭,一阶阶楼梯地蹒跚下降,被很快超越 到达 M68,离三段终点近了,全身无一处不疼,真真是行百里者半九十,最后这一公里路相当折磨人 到达三段终点,接水、自热米饭当晚餐,休息了一个多小时。 很想直接回家,但导航发现水浪窝营地只差 500 米了,内心天人交战后继续往营地进发 继续夜行 路标 500 米走了我 25 分钟,中间还有一段陡坡 到达营地开始搭帐篷,营地很空旷,只有我跟另一伙人露营。 18 号这一天下来一共走了 33582 步,而且背着十七八公斤爬上爬下,最后两三公里完全是咬着牙拼命爬的。 慢吞吞搭帐篷,花了半小时才搞定 第二日早上 6 点醒来 7 点多,在流动厕所解决完卫生问题,在营地逛了一圈没找到水源,只好拿折叠水桶在流动厕所的洗手池接了点水洗脸顺便擦身体。 随处走走,发现营地标牌 早餐随便吃了点东西,接着就收拾帐篷打算回家,发现收拾起来还挺费劲,慢慢吞吞弄了也大概 40 分钟,而且发现蛋巢垫下面有跟毛毛虫,帐篷内还有好几只蚂蚁,还发现一只跳蚤,另外内帐外面也爬了根看着就很毒的毛毛虫… 蛋巢垫下的毛毛虫、帐篷内的跳蚤大概是昨晚搭帐篷时,把东西放在一个石墩上,从石墩上爬上来的, 蚂蚁可能是从内帐的孔洞爬进来的。总之它们吓得我收拾东西的时候检查了一遍又一遍,生怕碰到虫子或者把虫子收拾进了行李中。 回家路上,取水点 在车站等车 总的来说,计划两日麦理浩径徒步,实际只 18 号走完麦径三段就精疲力尽了,第二日早上直接打道回府。从二段起点走到终点,用时 12:45 - 20:30,在交界处吃晚饭、休息了 1 个小时。之后从 21:50 - 22:15 走到水浪窝营地,到 22:50 才搭好帐篷。这是我第一次重装徒步,积累了宝贵的经验,也发现了许多问题,下次再徒步麦理浩径肯定能更得心应手了~ 只爬完第三段就精疲力尽,我分析了主要有这几个原因: 体力不够,还需要多加锻炼 第一次重装徒步,装备不够精炼,一共得有十七八公斤,其中有许多东西完全没用上。 带了过多食物,可以更精简。 四、麦理浩径第四段 徒步完第三段后,休息没一周,又是周末,周五下班后赶紧跑去续签了香港签证,精简了一番装备,周六上午就再次出发徒步第四段了。 这次出发前根据上次的经验精简了装备,总总量应该轻了大概 1kg,但仍旧有 16kg。主要变化: 去掉了折叠桌跟折叠凳:上次带了没用上,吃饭都是在营地或者烧烤点,路上补充能量都是直接拿出零食或能量胶随便啃两口,都用不上它们。 自热米饭也从两盒改成了一盒:因为计划就徒步一天然后露营,第二天回家。只晚上休息的时候来顿热乎的就 OK 了。 四段是麦径难度最高的一段,而且终点基维尔营地不接受个人预约,只接受团体预约。但我还是抱着侥幸心理背着重装背包去了,想着路上总不会只有这一个营地吧(后面的经历证明我有点鲁莽了)。 早饭吃得饱饱的,又洗了澡、休息了会儿消消食,然后 10 点左右就从家里出发了。 深圳地铁一号线上,我滴背包 12:13,香港沙田汽车站,排队上九巴 299X 路 到达大浪窝站,风景很好 去往四段起点路上,美美自拍 烧烤点与海湾,以及远方的城市 走了约十分钟到达三段终点,在这里用直饮水机将 3L 水袋灌满,然后取出登山杖就出发了。 首先是到达上次露营过的水浪窝营地,然后沿着大路继续上山,路上没人。 开始登山不久,发现起雾了 沿大路到达山顶后,是沿着黄泥小路下山。这两天下雨,路面泥泞湿滑,我又背着个 16kg 的重装背包,走起来有点难度。还好有登山杖,倒不用怕滑倒。 黄泥路,这两天下雨,路面湿滑,还好有登山杖 细竹林 岔路口 走黄泥路下完山,接着又是沿着石阶路开始登山,石阶也有点湿滑。 石阶,路面湿润 登山没多久,就遇到了雾气,接着雾就越来越浓。 登山路上也开始有雾了 跟山路上的雾来个合照 雾气加重 因为雾气跟汗水,头发已经湿了 下完一座小山又开始登另一座,路边好多棕叶 又到达一座山顶,在这里休息了一阵子,吃了点东西,接着突然想到我出发时没拉伸,想着补救一下, 就在这里做了个拉伸。 中间还遇到位外国女士背着个很小的跑步背包爬上来,也休息了一会儿喝了口水,往另一边去了,很快消失在了雾中。 到达山顶,雾相当的重 在山顶休息了一会儿,吃了点东西 好重的雾啊,啥都看不清 休息好了开始下山,没多久就到了四段风景最好的一段,因为浓雾没远景看没,如果没雾这里视野会是很开阔的。 这应该是四段风景最好的一段 跟浓雾来个合照 继续前行,到达昂平营地,这里是一块山顶平原草地,浓雾下也有点意境。走着走着旁边雾中出来一只狗跟一个人,说起来这路线上挺多人遛狗的,今天遇到两三波了。 昂平,山顶平原草地 M88 标距柱 因为阴天,下细雨,又这么大雾,天黑得很快,17 点后天就有点看不清路面了,开始需要灯光。头灯在包里懒得拿出来,就把背包背带上的手电夹到腰间别着的便携坐垫上照亮路面,还别说,效果不错。 17:12 了,走树林路已经需要借助灯光 17:40,天黑差不多了,先休息一会儿 可能因为下雨,路上挺多癞蛤蟆 到 20:10 左右,终于到达基维尔营地,听到了有人声,也看到了灯光。一番调查确认了跟之前了解到的一样,这里不接受个人露营。地图上往前看也没露营点,我有点慌了,但总之先到四段终点瞧瞧吧。走到终点发现就是基维尔营地下山的大水泥路面,既然有大路,那就能沿着它回到城市,这样想着终于有了一点安全感。 20:27,沿着大马路下山 下了个山坡后实在累得不行,把便携坐垫一铺就坐下休息了,尝试用高德地图导航,但信号有点差,一直转不出来。 然后一辆车从山下开上来,看到我瘫坐在路边,停下来问我要去哪,了解清楚情况后又给我指路,还说这里徒步下去要一个多小时,路上没路灯,问我行不行,要不要他送我去车站。 手机一直没信号,我一开始是有点心动的,但不想麻烦别人,刚好手机终于加载出了导航,我对照了下跟他指的路是同一个方向,就婉拒了他,并给他展示我的手电筒表示我不担心走夜路。 高德地图给的教我先徒步到大老山隧道站乘九巴 74X 路至广福邨下车,再步行到地铁站乘车到罗湖口岸。 但它给的徒步路线有坑,我跟着导航越走就越荒凉,公路路面开叉,长满荒草,接着就直接没公路了。我慌了,仔细确认才意识到它教我往草丛里钻,仔细看草丛里还真有条路… 但这条小路显然已经半荒废,草木林立,不仔细看几乎分辨不出路线,让人忍不住怀疑这条路真的能走吗?不会走到一半成断头路吧。 还好这条灌木丛近期有人走过,沿途草木有明显被人趟过,沿途灌木上偶尔还扎了很干净显眼的飘带, 明显才扎上去没多久,这给我增加了一点对它的信心。 钻灌木丛,中间还过了条溪流,接着又是上山,好走的山路没走多远,接着又是在山上钻更深的灌木丛,我越来越慌——这真的是下山的路吗?同时我也有点担心被灌木遮挡的路面会有蛇,但现在已经很晚了,下山心切的我没时间顾虑这些,一路急行。 灌木钻了没多久就开始下山路,而且能看到山下明亮的高速公路跟城市夜景了,这让我放心了一点——至少确实是在下山朝城市里去。但这下山路可不好走,几乎是钻着灌木丛林走直线下降,而且是原生地形,非常的陡,即使有登山杖的辅助,也摔了好多跤,还好草木灌木比较密,有效减缓了摔倒的冲击, 也避免我滚下山。 下山路没走多久,我突然发现腰包里的水杯跟夹着的折叠坐垫都不见了,显然是在前面几次摔跤的时候掉了,不过也就三四十块钱,不管了,继续向下。 下山路仿佛没有尽头,万幸途中发现底下的山坳里有好几片亮光,一开始怀疑是山里废弃的房子,前面趟这条路的旅游队在房子里露营,这也给了我希望——或许能有人给点帮助跟一口吃的,一起露营也不错。 到九点半左右终于下到山脚的时候,发现是个电站之类的建筑,周围还有监控警示,挺失望的。不过到这里又是大路了,也能很明显听到不远处车辆来往的声音,悬着的心总算放了下来。 考虑到手表快没电了,先把登山记录停了,显示今天徒步了接近 21 公里,真是累到够呛。 OPPO 健康 - 日行四万步 登山 21 公里 在路边找了个地坐着休息,想到因为钻灌木林、摔跤、一路剧烈运动疯狂出汗,加之今天又下小雨,身上都是各种小叶子、木棍、汗水、沾了叶子上的雨水,这个样子可不好上车见人。见周围也没人,我直接把衣服都脱掉,换了身干净的。 登山鞋里也湿透了,刚刚钻林子导致石子叶子小木棍雨水也进去了不少,也换了备用的沙滩鞋。换鞋时不知道哪跑出来只蚂蟥在我脚面上爬,赶紧给拔掉丢了(也很庆幸我一直穿运动紧身内衣裤,不然这一趟灌木丛徒步下来,小腿刮伤不说,还可能被蚂蟥等各种虫子叮咬)。 衣服鞋子换好后又休息了挺久,然后沿着大路走了可能十多二十分钟,才终于到了山脚,远远看到不远处就有个公交站,再次导航一下,确认它就是大老山隧道站。 到达车站已经是 22:20 了,乘九巴 74X 路到广福邨下车,再步行到地铁站乘车到罗湖口岸、过关、再乘一号线时已经 23:40,这个点居然还有一趟末班车。最后到家已经过了 0 点,饿得不行搞完夜宵、休息、再洗澡,搞到两三点才睡觉。 22:37,在乘九巴 74X 路往广福邨的路上,香港城市夜景 下车,步行前往地铁站 22:52,到达大埔墟地铁站 22:57,等地铁中 总的来说这次徒步距离真的是到目前为止的人生巅峰了,另外这次下山路线也是我走过最险的一次,有点刺激跟后怕,高德地图坑我啊。 装备总结 不应该带的装备: 折叠桌跟折叠凳: 沐浴露:许多营地没洗澡的地方,擦身体也用不上沐浴露,带这个不如带点湿巾。 洗洁精:同理,就几天徒步,用纸巾擦一擦就行,回家后再用洗洁精清理一遍不迟。 太阳能电板:三天以内,一个 30000 mah 的电源完全够用。如果 3-6 天,还不如带两个移动电源。目前根本没有徒步超过一周的计划,所以这个也没必要带。 保温杯、餐具、炉子:现在是夏季,香港挺温暖,我全程靠能量胶跟压缩饼干维持体力,晚饭吃自热米饭,用不上这些装备。其实主要是气罐不好带,担心被海关查扣。 起了重大作用的装备: 曦途第三代 7075 登山杖:铝合金本身够轻,将部分腿部负重转移到上半身,减轻了腿部的负担, 在重装徒步时能起到保护膝盖、提升稳定性、延长行走时间的作用。 目前发现的缺点: 使用时登山杖中部会共振,声音稍微有点吵,不过问题不大。 一次下雨徒步后放了一天多,就发现杖尖周围生锈了,看来是用的材料不够好(毕竟是不到 60 块钱一根,还要啥自行车)。 挪客云径 65L 徒步背包:够大,带背负系统,非常好用。 目前发现的缺点: 腰封的带子会滑动,行走久了就会松掉,需要重新手动调整。这个很烦,走着走着肩部受力就越来越大,而且行走的时候调整带子松紧也很不方便。 挪客 3L 水袋、600ml 水杯:喝水,必备 迪卡侬 MH100 登山鞋:好的硬底登山鞋,防滑、防水、防磨损,能保护脚部,提供足够的支撑, 减少脚部疲劳。 挪客夏季信封睡袋:即使是现在夏季,山上的营地(水浪窝)晚上还是有点冷的,盖一个夏季睡袋刚刚好。 能量胶、盐丸、压缩饼干、零食:帮助保持体力。能量胶恢复体力但饿得快,压缩饼干能维持饱腹感,零食能提供口感。 能量胶跟盐丸非常有用! 折叠水桶、速干毛巾:打水洗头洗澡洗衣必备!(营地没洗澡的地方,用湿毛巾擦擦身体,也OK, 擦完舒服太多了) 运动速干套装(紧身速干打底衣裤长袖 + 短袖短裤 + 薄外套):防止蚊虫叮咬、速干、防晒、防风,钻个灌木丛也不惧,我愿称之为徒步必备! 速干汗带:吸汗,防止汗水流入眼睛,而且速干款比棉制品强多了,蒸发很快。 变色防风眼镜:防风防晒、防雨、防风沙蚊虫。不管是白天也是夜间徒步,都很实用! 保鲜袋:一时半会儿没吃完的食物,用这个装非常方便。 一次性内裤、袜子:用完即抛,不用洗,方便得很。 神火 A20 手电筒:一开始觉得好像没啥用,毕竟已经有头灯了,结果发现晚上搭帐篷、在营地逛逛,这个很好用也很有必要。 30000 mah 移动电源(111Wh):显然这个起了大作用,用完一天到回家的时候,还有 70% 电量, 它大概能顶三天,650g 的重量物有所值。 大概两年前买的,最近查了下,有个别能量密度更高的电源,但市面上大部分电源的能量密度都跟这差不多,没有太大的提升。 曦途折叠坐垫:行走路上累了,随时坐下休息,这个很有用。 本来是打算用折叠凳的,但折叠凳收纳还是麻烦许多,而且它建议承重 75Kg,我背着个接近 20 公斤的包,很怕一屁股给坐坏了。 不是很满意的装备 Warsun W81s 头灯:用了两个半小时就没电了,而且还不能换电池!后面去露营地点的路上只能手举手电筒同时还要用登山杖,很不方便。必须得换个电池可更换的头灯,最好是能用 18650 电池! 下单了耐朗 B71 转角手电筒、不知名的万向手电筒 U 型夹(当手电腰夹用)、倍量的最高密度 18650 电池跟 21700 电池。这样转角手电 + 直角手电 + 手电腰夹 + 备用电池组合,能应对任何多日长线徒步的夜行情况了 防晒袖套脖套:穿了长袖紧身衣的情况下袖套感觉就没啥必要了,脖套可以保留一个。 迪卡侬 MH500 冲锋衣 + 普通雨裤:占地方,而且也挺重,这次没派上用场。但考虑到万一下大雨刮大风,这俩还是得带上。大风天气下雨披用处不大,只能靠冲锋衣。 急救包:有 400g,感觉太重了,下次可以精简一下。 牧高笛雨披天幕:至少在香港用这东西,有点太热了,而且我买的这个没有腰带,背部也没有加长,现在看应该买三峰出的那个会好很多。 这次没带,但发觉应该补充的装备 能放水杯、证件、手机的腰包:背包腰封会挡住裤子口袋,腰封的口袋也没法放手机,这个时候腰包就非常有用了。 驱蚊水:驱蚊驱虫,一是防止叮咬,二是防止蚊虫进入帐篷,我还挺怕小虫子的。 湿巾:一些营地淡水不太好获得(比如水浪窝营地,我这次是用了水袋里的饮水擦身体…),这时候湿巾就非常有用了,可以用来简单清理下身体。 短款雪套:防止各种碎石、树枝、草叶进入鞋子,保护脚部。用短款是因为紧身运动裤已经提供了保护腿部的功能,长款雪套就显得多余了(下雨另当别论)。 TODOs: 需要练习下雨披 + 背包的使用方式,这次路上下雨点,手忙脚乱地穿上,搞半天都盖不住背包。 学习下雨天如何快速收拾帐篷,这次收拾都花了半个多小时,要是下大雨大概帐篷跟东西得全搞湿了。 如何选择扎营地点?这次选在了树下的草地,结果恰好是少有人扎营的一块,虫子特别多(毛毛虫、跳蚤、蚂蚁、蜘蛛,等等…)。 登山杖的手柄系带如何拆卸清洗,以及如何调整长短。走完一趟系带都被汗水浸湿了,而且有点长了。 再复习一遍登山杖使用方法,这次发现登山杖起了大用,而且经过一天的高强度使用,经验也丰富了,现在再回头补充下理论知识,应该会有更深的体会。 学习伞绳、急救包的使用方法,经常徒步的话,出了意外不会用就尴尬了。 现在已经爱上了徒步这种运动方式,花钱折磨自己毫不手软( 参考资料 如下是我近期发现的一些高质量徒步相关资料,对我有挺大帮助: 比装备推荐更重要的户外知识:重装徒步入坑指南,学到挺多新 户外装备超级指南 - 知乎专栏: 一位户外高玩的专栏,深入浅出地讲解了许多重装徒步装备(包括用药)的设计原理、使用方法、选购策略等等,非常有用! 户外这 7 种常见的地形,登山杖的使用方法也各不相同 - Bilibili: 这位 UP 主出了许多简短的视频讲解各种户外技巧,非常实用,尤其是这个讲解登山杖用法的视频。 35位医生投票:家中常备什么药?竟然有一个德不配位!- 哔哩哔哩: 常用药指南,结合户外背包打包与装备检查清单 - 1.22 医疗救援与防护用品 的内容选择药品,非常实用。 一個人的麥理浩徑: 包含详细的麦理浩徑全程路线、政府营地、水源、交通等信息,非常实用。 露營地點 - 香港渔农自然护理署: 包含香港全部 41 个政府营地的介绍。

2024/5/21
articleCard.readMore

OS as Code - 我的 NixOS 使用体会

本文最初发表于 如何评价NixOS? - 知乎,觉得比较有价值所以再搬运到我的博客。 我 23 年 4 月开始用 NixOS 之前看过(如何评价NixOS? - 知乎) 这个问答,几个高赞回答都从不同方面给出了很有意义的评价,也是吸引我入坑的原因之一。 现在是 2024 年 2 月,距离我入坑 NixOS 刚好 10 个月,我当初写的新手笔记已经获得了大量好评与不少的赞助,并成为了整个社区最受欢迎的入门教程之一。自 2023 年 6 月我为它专门创建一个 GitHub 仓库与单独的文档站点以来,它已经获得了 1189 个 stars,除我之外还有 37 位读者给它提了 PR: NixOS 与 Flakes - 一份非官方的新手指南 NixOS & Flakes Book 那么作为一名已经深度使用 NixOS 作为主力桌面系统接近 10 个月的熟手,我在这里也从另一个角度来分享下我的入坑体会。 注意,这篇文章不是 NixOS 入门教程,想看教程请移步上面给的链接。 Nixpkgs 中的包太少? 先澄清下一点,NixOS 的包非常多,Repository statistics 的包仓库统计数据如下: 上面这个 Nixpkgs 的包数量确实有挺多水分——Nixpkgs 还打包了许多编程语言的 Libraries(貌似挺多 Haskell 人用 nix 当语言包管理器用),比如Haskell Packages(18000+),R Packages(27000+),Emacs Packages(6000+) ,但即使把它们去掉后 Nixpkgs 的包数量也有大约 40000+,虽然逊色于 AUR,但这个数量再怎么算也跟「包太少」这个描述扯不上关系。 包仓库这里也是 NixOS 跟 Arch 不太同的地方,Arch 的官方包仓库收录很严格,相对的 AUR 生态相当繁荣。但任何人都能往 AUR 上传内容,虽然有一个投票机制起到一定审核作用,但个人感觉这个限制太松散了。 而 NixOS 就很不一样了,它的官方包仓库 Nixpkgs 很乐于接受新包,想为 Nixpkgs 提个 PR 加包或功能相对其他发行版而言要简单许多,这是 Nixpkgs 的包数量这么多的重要原因(GitHub 显示 Nixpkgs 有 5000+ 历史贡献者,这很夸张了)。 Nixpkgs 仓库的更新流程相对 AUR 也严格许多,PR 通常都需要通过一系列的 GitHub Actions 测试 + Maintainer Review + Ofborg 检查与自动构建测试后才能被合并,Nixpkgs 也鼓励维护者为自己的包添加测试(包的 doCheck 默认为 true),这些举措都提升了 Nixpkgs 的包质量。 NixOS 其实也有个与 AUR(Arch User Repository) 类似的 NUR(Nix User Repository),但因为 Nixpkgs 的宽松,NUR 反而没啥内容。 举例来说,QQ 能直接从 Nixpkgs 官方包仓库下载使用,而在 Arch 上你得用 AUR 或者 archlinux-cn. 这算是各有优势吧。NixOS 被人喷包少,主要是因为它不遵循 FHS 标准,导致大部分网上下载的 Linux 程序都不能直接在 NixOS 上运行。这当然有解决方案,我建议是首先看看 Nixpkgs 中是否已经有这个包了,有的话直接用就行。如果没有,再尝试一些社区的解决方案,或者自己给打个包。 用 NixOS 的话自己打包程序是不可避免的,因为即使 Nixpkgs 中已经有了这么多包,但它仍然不可能永远 100% 匹配你的需求,总有你想用但 Nixpkgs 跟 NUR 里边都没有的包,在 NixOS 上你常常必须要给你的包写个打包脚本,才能使它在 NixOS 上正常运行。 另外即使有些程序本身确实能在 NixOS 上无痛运行,但为了做到可复现,NixOS 用户通常也会选择自己手动给它打个包。 OK,闲话说完,下面进入正题。 首先,NixOS 比传统发行版复杂很多,也存在非常多的历史遗留问题。 举例来说,它的官方文档烂到逼得我一个刚学 NixOS 的新手自己边学边写入门文档。在我用自己的渣渣英语把笔记翻译了一遍发到 reddit (NixOS & Nix Flakes - A Guide for Beginners) 后,居然还获得了许多老外的大量好评(经过这么长时间的持续迭代,现在甚至已经变成了社区最受欢迎的新手教程之一),这侧面也说明官方文档到底有多烂。 NixOS 值不值得学? NixOS 值不值得学或者说投入产出比是否够高?在我看来,这归根结底是个规模问题。 这里的规模,一是指你对 Linux 系统所做的自定义内容的规模,二是指你系统更新的频繁程度,三是指你 Linux 机器的数量。 下面我从个人经历的角度来讲下我以前用 Arch Linux、Ubuntu 等传统发行版的体验,以及我为什么选择了 NixOS,NixOS 又为我带来了什么样的改变。 举个例子,以前我用 Deepin Ubuntu 时我基本没对系统做过什么深入定制,一是担心把系统弄出问题修复起来头疼,二是如果不额外写一份文档或脚本记录下步骤的话,我做的所有定制都是黑盒且不可迁移的,一个月后我就全忘了,只能战战兢兢地持续维护这个随着我的使用而越来越黑盒、状态越来越混沌的系统。 如果用的是 Arch 这种滚动发行版还好,系统一点点增量更新,遇到的一般都是小问题。而对 Ubuntu Deepin 这种,原地升级只出小问题是很少见的,这基本就意味着我必须在某个时间点,在新版本的 Ubuntu 上把我以前做过的定制再全部重做一遍,更关键的是,我非常有可能已经忘了我以前做了什么,这就意味着我得花更多的时间去研究我的系统环境里到底都有些啥东西,是怎么安装配置的,这种重复劳动非常痛苦。 总之很显然的一点是,我对系统做的定制越多越复杂,迁移到新版本的难度就越大。 我想也正是因为这一点,Arch、Gentoo、Fedora 这种滚动发行版才在 Linux 爱好者圈子中如此受欢迎,喜欢定制自己系统的 Linux 用户也大都使用这类滚动发行版。 那么 Arch、Fedora 就能彻底解决问题了么?显然并不是。首先它们的更新频率比较高,这代表着你会更容易把你的系统搞出点毛病来。当然这其实是个小问题,现在 Linux 社区谁还没整上个 btrfs / zfs 文件系统快照啊,出问题回滚快照就行。它们最根本的问题是: 你的 Arch 系统环境、文件系统快照、或者虚拟机快照,它们仍然是个黑盒,仍然会随着你的持续使用而越来越混沌,也并不包含如何从零构建这个环境的「知识」,是不可解释的。 我在工作中就见到过一些「祖传虚拟机快照」或「祖传云服务器快照」,没人知道这个环境是怎么搭建的,每一任接手的人都只能继续往上叠 Buff,然后再把这个定时炸弹传给下一任。这就像那个轮流往一个水杯里加水的游戏,最后在谁加水的时候溢出来了,那就算他倒霉。 Arch 实质要求你持续跟着它的更新走,这意味着你必须要持续更新维护它。 如果你把机器放了一年半载跑得很稳定,然后你想要更新一下,那出问题的风险会相当高。如果你因此而决定弄台最新版本的 Arch 机器再把旧环境还原出来,那就又回到了之前的问题——你得想办法从旧环境中还原出你的定制流程,这也不是个好差事。 快照与当前硬件环境强相关,直接在不同硬件的机器上使用很容易遇到各种奇怪的问题,也就是说这东西不可迁移。 快照是一堆庞大的二进制文件,它的体积非常大,这使得备份与分享它的成本高昂。 Docker 能解决上述问题中的一部分。首先它的容器镜像可由 Dockerfile 完全描述,也就是说它是可解释的,此外容器镜像能在不同环境中复现出完全一致的环境,这表明它是可迁移的。对于服务器环境,将应用程序全都跑在容器中,宿主机只负责跑容器,这种架构使得你只需要维护最基础的系统环境,以及一些 Dockerfile 跟 yaml 文件,这极大地降低了系统的维护成本,从而成为了 DevOps 的首选。 但 Docker 容器技术是专为应用程序提供一致的运行环境而设计的,在虚拟机、桌面环境等场景下它并不适用(当然你非要这么弄也不是不行,很麻烦就是了)。此外 Dockerfile 仍旧依赖你所编写的各种脚本、命令来构建镜像,这些脚本、命令都需要你自己维护,其运行结果的可复现能力也完全看你自己的水平。 如果你因为这些维护难题而选择极简策略——尽可能少地定制任何桌面系统与虚拟机环境,能用默认的就用默认——这就是换到 NixOS 之前的我。为了降低系统维护难度,我以前使用 Deepin Manjaro EndeavourOS 的过程中,基本没对系统配置做任何大变动。作为一名 SRE/DevOps,我在工作中就已经踩了够多的环境问题的坑,写腻写烦各种安装脚本、Ansible 配置了,业余完全不想搞这些幺蛾子。 但如果你是个喜欢定制与深入研究系统细节的极客,随着你对系统所做的定制越来越多,越来越复杂, 或者你 Homelab 与云上的 Linux 机器越来越多,你一定会在某个时间点开始编写各种部署流程的文档、部署脚本或使用一些自动化工具帮自己完成一些繁琐的工作。 文档就不用说了,这个显然很容易过时,没啥大用。如果你选择自己写自动化脚本或选用自动化工具, 它的配置会越来越复杂,而且系统更新经常会破坏掉其中一些功能,需要你手动修复。此外它还高度依赖你当前的系统环境,当你某天装了台新机器然后信心满满地用它部署环境时,大概率会遇到各种环境不一致导致的错误需要手动解决。还有一点是,你写的脚本大概率并没有仔细考量抽象、模块化、错误处理等内容,这也会导致随着规模的扩大,维护它变得越来越痛苦。 然后你发现了 NixOS,它有什么声明式的配置,你仔细看了下它的实现,哦这声明式的配置,不就是把一堆 bash 脚本封装了下,对用户只提供了一套简洁干净的 api 么,它实际干的活不跟我自己这几年写的一堆脚本一模一样?好像没啥新鲜的。 嗯接着你试用了一下,发现 NixOS 的这套系统定制脚本都存在一个叫 Nixpkgs 的仓库中,有数千人在持续维护它,几十年积累下来已经拥有了一套非常丰富、也比较稳定的声明式抽象、模块系统、类型系统、专为这套超大型的软件包仓库与 NixOS 系统配置而开发的大规模 CI 系统 Hydra、以及逐渐形成的能满足数千人协作更新这套复杂配置的社区运营模式。 你立马学习 nix 语言,然后动手把这套维护了 N 年的脚本改写成 NixOS 配置。 越写就对它越满意,改造后的配置缩水了相当多,维护难度直线下降。 很大部分以前自己用各种脚本跟工具实现的功能,都被 Nixpkgs 封装好了,只需要 enable 一下再传几个关键参数,就能无痛运行。nixpkgs 中的脚本都有专门的 maintainer 维护更新,任何发现了问题的用户也可以提个 PR 修下问题,在没经过 CI 与 staging unstable 等好几个阶段的广泛验证前,更新也不会进入 stable. 上面所说的你,嗯就是我自己。 现在回想下我当初就为了用 systemd 跑个简单的小工具而跟 systemd 疯狂搏斗的场景,泪目… 要是我当初就懂 NixOS… NixOS 的声明式配置 - OS as Code 有过一定编程经验的人都应该知道抽象与模块化的重要性,复杂程度越高的场景,抽象与模块化带来的收益就越高。Terraform、Kubernetes 甚至 Spring Boot 的流行都体现了这一点。NixOS 的声明式配置也是如此,它将底层的实现细节都封装起来了,并且这些底层封装大都有社区负责更新维护,还有 PR Review、CI 与多阶段的测试验证确保其可靠性,这极大地降低了我的心智负担,从而解放了我的生产力。它的可复现能力则免除了我的后顾之忧,让我不再担心搞坏系统。 NixOS 构建在 Nix 函数式包管理器这上,它的设计理念来自 Eelco Dolstra 的论文 The Purely Functional Software Deployment Model(纯函数式软件部署模型),纯函数式是指它没有副作用, 就类似数学函数 $y = f(x)$,同样的 NixOS 配置文件(即输入参数 $x$ )总是能得到同样的 NixOS 系统环境(即输出 $y$)。 这也就是说 NixOS 的配置声明了整个系统完整的状态,OS as Code! 只要你 NixOS 系统的这份源代码没丢,对它进行修改、审查,将源代码分享给别人,或者从别人的源代码中借鉴一些自己想要的功能,都是非常容易的。你简单的抄点其他 NixOS 用户的系统配置就能很确定自己将得到同样的环境。相比之下,你抄其他 Arch/Ubuntu 等传统发行版用户的配置就要麻烦的多,要考虑各种版本区别、环境区别,不确定性很高。 NixOS 的学习成本 NixOS 的入门门槛相对较高,也不适合从来没接触过 Linux 与编程的小白,这是因为它的设计理念与传统 Linux 发行版有很大不同。但这也是它的优势所在,跨过那道门槛,你会发现一片新天地。 举例来说,NixOS 用户翻 Nixpkgs 中的实现源码实际是每个用户的基本技能,给 Nixpkgs 提 PR 加功能、加包或者修 Bug 的 NixOS 用户也相当常见。 这既是使新用户望而却步的拦路之虎,同时也是给选择了 NixOS 的 Linux 用户提供的进阶之梯。 想象下大部分 Arch 用户(比如以前的我)可能用了好几年 Arch,但根本不了解 Arch 底层的实现细节,没打过自己的包。而 NixOS 能让翻源码成为常态,实际也说明理解它的实现细节并不难。我从两个方面来说明这一点。 第一,Nix 是一门相当简单的语言,语法规则相当少,比 Java Python 这种通用语言简单了太多。因此有一定编程经验的工程师能花两三个小时就完整过一遍它的语法。再多花一点时间,读些常见 Nix 代码就没啥难度了。 第二,NixOS 良好的声明式抽象与模块化系统,将 OS 分成了许多层来实现,使用户在使用过程中, 既可以只关注当前这一层抽象接口,也可以选择再深入到下一层抽象来更自由地实现自己想要的功能(这种选择的权利,实际也给了用户机会去渐进式地理解 NixOS 本身)。举例来说,新手用户只要懂最上层的抽象就正常使用 NixOS。当你有了一点使用经验,想实现些自定义需求,挖下深挖一层抽象(比如说直接通过 systemd 的声明式参数自定义一些操作)通常就足够了。如果你已经是个 NixOS 熟手,想更极客一点,就可以再继续往下挖。 总之因为上面这两点,理解 Nixpkgs 中的源码或者使用 Nix 语言自己打几个包并不难,可以说每个有一定经验的 NixOS 用户同时也会是 NixOS 打包人。 NixOS 的卖点? 我们看了许多人提到 NixOS 的优点,上面我也提到了不少。Nix 的圈外人听得比较多的可能主要是它解决了依赖冲突问题,能随时回滚,强大的可复现能力。如果你有实际使用过 NixOS,那你也应该知道 NixOS 的这些优势: NixOS 的 Flakes 特性使你能将系统锁定在一个特定的状态,你可以在任何想更新的时候才更新它,即使有个一年半载不更新也完全没毛病。NixOS 不会强迫你频繁更新系统,你可以选择是否这么做。因为系统的状态可以完全从你的 NixOS 配置中推断出来,所以从旧版本升级到最新版本也容易很多。 有的选总是好的,我不喜欢被强迫频繁更新(即使我实际更新还挺频繁的),公司里的系统管理员或者 DevOps 就更是如此了。 系统更新具有类似数据库事务的原子化特性,这意味着你的系统更新要么成功要么失败,(一般) 不会出现中间状态。 NixOS 的声明式配置实际实现了 OS as Code,这使得这些配置非常便于分享。直接在 GitHub 上从其他 NixOS 用户那里 Copy 需要的代码到你的系统配置中,你就能得到一个一模一样的功能。新手用户也能很容易地从别人的配置中学到很多东西。 这也是近几年 GitHub 与 reddit r/unixporn 上使用 NixOS 做桌面 ricing 的用户越来越多的原因。 声明式配置为用户提供了高度便捷的系统自定义能力,通过改几行配置,就可以快速更换系统的各种组件。 等等 这些都是 NixOS 的卖点,其中一些特性现在在传统发行版上也能实现,Fedora Silverblue 等新兴的不可变发行版也在这些方面有些不错的创新。但能解决所有这些问题的系统,目前只有 NixOS(以及更小众的 Guix. 据Guix 的 README 所言,它同样基于 Nix 包管理器)。 NixOS 的缺点与历史债务 自 NixOS 项目创建至今二十多年来,Nix 包管理器与 NixOS 操作系统一直是非常小众的技术,尤其是在国内,知道它们存在的人都是少数 Linux 极客,更别说使用它们了。 NixOS 很特殊,很强大,但另一方面它也有着相当多的历史债务,比如说: 文档混乱不说人话 Flakes 特性使 NixOS 真正满足了它一直宣称的可复现能力,但从 2021 年正式发布到现在 2024 年,它仍旧处在实验状态。 Nix 的 CLI 处在换代期,新版本的 CLI 优雅很多,但其实现目前与 Flakes 特性强绑定,导致两项功能都难以 stable,甚至还阻碍了许多其他特性的开发工作。 模块系统的缺陷与 Nix 错误处理方面的不足,导致长期以来它的报错信息相当隐晦,令人抓狂 Nix 语言太过简单导致 Nixpkgs 中大量使用 Bash 脚本,以及 Nix 语言的大多数特性都完全是使用 C++ 实现的,从 Nix 语言的角度看这很黑盒。 NixOS 的大量实现细节隐藏在 Nixpkgs 源码中,比如说软件包的分类、derivation 有哪些属性可被 override。 Nixpkgs 长期一直使用文件夹来对软件包进行分类,没有任何查看源码之外的手段来分类查询其中的软件包。 Nixpkgs 中的所有 derivation 相关信息,目前也只能通过查看源码来了解。 https://nixos.wiki 站点维护者跑路,官方又长期未提供替代品,导致 NixOS 的文档在本来就很烂的基础上又雪上加霜。 Nix/NixOS 近来快速增长的用户群体,使得它的社区运营模式也面临着挑战 … 这一堆历史债是 NixOS 一直没能得到更广泛使用的主要原因。但这些问题也是 NixOS 未来的机会,社区目前正在积极解决这些问题,我很期待看到这些问题被解决后, NixOS 将会有怎样的发展。 NixOS 的未来 谁也不会对一项没前途的技术感兴趣,那么 NixOS 的未来如何呢?我是否看好它?这里我尝试使用一些数据来说明我对 NixOS 的未来的看法。 首先看 Nixpkgs 项目,它存储了 NixOS 所有的软件包及 NixOS 自身的实现代码: 上图能看到从 2021 年开始 Nixpkgs 项目的活跃度开始持续上升,Top 6 贡献者中有 3 位都是 2021 年之后开始大量提交代码,你点进 GitHub 看,能看到 Top 10 贡献者中有 5 位都是 2021 年之后加入社区的(新增的 @NickCao 与 @figsoda 都是 NixOS 中文社区资深用户)。 再看看 Nix 包管理器的提交记录,它是 NixOS 的底层技术: 上图显示 Nix 项目的活跃度在 2020 年明显上升,Top 6 贡献者中有 5 位都是在 2020 年之后才开始大量贡献代码的。 再看看 Google Trends 中 NixOS 这个关键词的搜索热度: 这个图显示 NixOS 的搜索热度有几个明显的上升时间点: 2021 年 12 年 这大概率是因为在 2021 年 11 月Nix 2.4 发布了,它带来了实验性的 Flakes 特性与新版 CLI,Flakes 使得 NixOS 的可复现能力得到了极大的提升,新 CLI 也更符合用户直觉。 2023 年 6 月 最重要的原因应该是,Youtube 上 Linux 相关的热门频道在这个时间点推出了好几个关于 NixOS 的视频,截至 2024-02-23,Youtube 上播放量最高的三个 NixOS 相关视频都是在 2023-06 ~ 2023-07 这个时间段推出的,它们的播放量之和超过了 130 万。 China 的兴趣指数在近期最高,这可能是因为国内的用户群一直很少,然后我在 6 月份发布了NixOS 与 Flakes - 一份非官方的新手指南, 并且在 科技爱好者周刊 等渠道做了些推广,导致 NixOS 的相对指数出现明显上升。 2024 年 1 月 这个我目前不太确定原因。 再看看 Nix/NixOS 社区从 2022 年启用的年度用户调查。 2022 Nix Survey Results, 根据其中数据计算可得出: 74.5% 的用户是在三年内开始使用 Nix/NixOS 的。 关于如何拓展 Nixpkgs 的调查中,36.7% 的用户使用 Flakes 特性拓展 Nixpkgs,仅次于传统的 overlays. Nix Community Survey 2023 Results, 简单计算可得出, 54.1% 的用户是在三年内开始使用 Nix/NixOS 的。 关于如何拓展 Nixpkgs 的调查中,使用 Flakes 特性的用户占比为 49.2%,超过了传统的 Overlays. 关于实验特性的调查中,使用 Flakes 特性的用户占比已经达到了 59.1%. 2024-04-12 更新:NixCon 2024 也有一个演讲提供了 Nix 社区的各种历史数据:Nix, State of the Union - NixCon 2024 另外 GitHub 的Octoverse 2023 也难得地提了一嘴 Nixpkgs: Developers see benefits to combining packages and containerization. As we noted earlier, 4.3 million repositories used Docker in 2023. > On the other side of the coin, Linux distribution NixOS/nixpkgs has been on the top list of open source projects by contributor for the last two years. 这些数据与我们前面提到的 Nixpkgs 与 Nix 项目的活跃度相符,都显示 Nix/NixOS 社区在 2021 年之后开始迅速增长壮大。 结合上面这些数据看,我对 NixOS 的未来持很乐观的态度。 总结 从决定入坑 NixOS 到现在,短短 10 个月,我在 Linux 上取得的收获远超过去三年。我已经在 PC 上尝试了非常多的新技术新工具,我的 Homelab 内容也丰富了非常多(我目前已经有了十多台 NixOS 主机),我对 Linux 系统结构的了解也越来越深刻。 光是这几点收获,就完全值回票价了,欢迎入坑 NixOS~

2024/2/21
articleCard.readMore

个人数据安全不完全指南

零、前言 在接触电脑以来很长的一段时间里,我都没怎么在意自己的数据安全。比如说: 长期使用一个没有 passphrase 保护的 SSH 密钥(RSA 2048 位),为了方便我还把它存到了 onedrive 里,而且在各种需要访问 GitHub/Gitee 或 SSH 权限的虚拟机跟 PC 上传来传去。 Homelab 跟桌面 PC 都从来没开过全盘加密。 在 2022 年我的 Homelab 坏掉了两块国产固态硬盘(阿斯加特跟光威弈 Pro 各一根),都是系统一启动就挂,没法手动磁盘格式化,走售后直接被京东换货了。因为我的数据是明文存储的,这很可能导致我的个人数据泄露… 几个密码在各种站点上重复使用,其中重要账号的随机密码还是我在十多年前用 lastpass 生成的,到处用了这么多年,很难说这些密码有没有泄露(lastpass 近几年爆出的泄漏事故就不少…) GitHub, Google, Jetbrains 等账号的 Backup Code 被我明文存储到了百度云盘,中间发现百度云盘安全性太差又转存到了 OneDrive,但一直是明文存储,从来没加过密。 一些银行账号之类的随机密码,因为担心遗忘,长期被我保存在一份印象笔记的笔记里,也是明文存储,仅做了些简单的内容替换,要猜出真正的密码感觉并不是很难。 以前也有过因为对 Git 操作不熟悉或者粗心大意,在公开仓库中提交了一些包含敏感信息的 commit,比如说 SSH 密钥、密码等等,有的甚至很长时间都没发现。 现在在 IT 行业工作了几年,从我当下的经验来看,企业后台的管理员如果真有兴趣,查看用户的数据真的是很简单的一件事,至少国内大部分公司的用户数据,都不会做非常严格的数据加密与权限管控。就算真有加密,那也很少是用户级别的,对运维人员或开发人员而言这些数据仍旧与未加密无异。对系统做比较大的迭代时,把小部分用户数据导入到测试环境进行测试也是挺常见的做法… 总之对我而言,这些安全隐患在过去并不算大问题,毕竟我 GitHub, Google 等账号里也没啥重要数据,银行卡里也没几分钱。 但随着我个人数据的积累与在 GitHub, Google 上的活动越来越多、银行卡里 Money 的增加(狗头),这些数据的价值也越来越大。比如说如果我的 GitHub 私钥泄漏,仓库被篡改甚至删除,以前我 GitHub 上没啥数据也没啥 stars 当然无所谓,但现在我已经无法忍受丢失 GitHub 两千多个 stars 的风险了。 在 2022 年的时候我因为对区块链的兴趣顺便学习了一点应用密码学,了解了一些密码学的基础知识, 然后年底又经历了几次可能的数据泄漏,这使我意识到我的个人数据安全已经是一个不可忽视的问题。因此,为了避免 GitHub 私钥泄漏、区块链钱包助记词泄漏、个人隐私泄漏等可能,我在 2023 年 5 月做了全面强化个人数据安全的决定,并在 0XFFFF 社区发了篇帖子征求意见——学习并强化个人的数据安全性(持续更新)。 现在大半年过去,我已经在个人数据安全上做了许多工作,目前算是达到了一个比较不错的状态。 我的个人数据安全方案,有两个核心的指导思想: 零信任:不信任任何云服务提供商、本地硬盘、网络等的可靠性与安全性,因此任何数据的落盘、网络传输都应该加密,任何数据都应该有多个副本(本地与云端)。 基于这一点,应该尽可能使用经过广泛验证的开源工具,因为开源工具的安全性更容易被验证, 也避免被供应商绑架。 Serverless: 尽可能利用已有的各种云服务或 Git 之类的分布式存储工具来存储数据、管理数据版本。 实际上我个人最近三四年都没维护过任何个人的公网服务器,这个博客以及去年搭建的 NixOS 文档站全都是用的 Vercel 免费静态站点服务,各种数据也全都优先选用 Git 做存储与版本管理。 我 Homelab 算力不错,但每次往其中添加一个服务前,我都会考虑下这是否有必要,是否能使用已有的工具完成这些工作。毕竟跑的服务越多,维护成本越高,安全隐患也越多。 这篇文章记录下我所做的相关调研工作、我在这大半年的实践过程中逐渐摸索出的个人数据安全方案以及未来可能的改进方向。 注意这里介绍的并不是什么能一蹴而就获得超高安全性的傻瓜式方案,它需要你需要你有一定的技术背景跟时间投入,是一个长期的学习、实践与方案迭代的过程。另外如果你错误地使用了本文中介绍的工具或方案,可能反而会降低你的数据安全性,由此产生的任何损失与风险皆由你自己承担。 一、个人数据安全包含哪些部分? 数据安全大概有这些方面: 保障数据不会泄漏——也就是加密 保障数据不会丢失——也就是备份 就我个人而言,我的数据安全主要考虑以下几个部分: SSH 密钥管理 各种网站、APP 的账号密码管理 灾难恢复相关的数据存储与管理 比如说 GitHub, Twitter, Google 等重要账号的二次认证恢复代码、账号数据备份等,日常都不需要用到,但非常重要,建议离线加密存储 需要在多端访问的重要个人数据 比如说个人笔记、图片、视频等数据,这些数据具有私密性,但又需要在多端访问。可借助支持将数据加密存储到云端的工具来实现 个人电脑與 Homelab 的数据安全与灾难恢复 我主要使用 macOS 与 NixOS,因此主要考虑的是这两个系统的数据安全与灾难恢复 下面就分别就这几个部分展开讨论。 二、是否需要使用 YubiKey 等硬件密钥? 硬件密钥的好处是可以防止密钥泄漏,但 YubiKey 在国内无官方购买渠道,而且价格不菲,只买一个 YubiKey 的话还存在丢失的风险。 另一方面其实基于现代密码学算法的软件密钥安全性对我而言是足够的,而且软件密钥的使用更加方便。或许在未来,我会考虑使用canokey-core、OpenSK、solokey 等开源方案 DIY 几个硬件密钥,但目前我并不觉得有这必要。 三、SSH 密钥管理 2.1 SSH 密钥的生成 我们一般都是直接使用 ssh-keygen 命令生成 SSH 密钥对,OpenSSH 目前主要支持两种密钥算法: RSA: 目前你在网上看到的大部分教程都是使用的 RSA 2048 位密钥,但其破解风险在不断提升,目前仅推荐使用 3072 位及以上的 RSA 密钥。 ED25519: 这是密码学家 Dan Bernstein 设计的一种新的签名算法,其安全性与 RSA 3072 位密钥相当,但其签名速度更快,且密钥更短,因此目前推荐使用 ED25519 密钥。 2.2 SSH 密钥的安全性 RSA 跟 ED25519 都是被广泛使用的密码学算法,其安全性都是经过严格验证的,因此我们可以放心使用。但为了在密钥泄漏的情况下,能够尽可能减少损失,强烈建议给个人使用的密钥添加 passphrase 保护。 那这个 passphrase 保护到底有多安全呢? 有一些密码学知识的人应该知道,passphrase 保护的实现原理通常是:通过 KDF 算法(或者叫慢哈希算法、密码哈希算法)将用户输入的 passphrase 字符串转换成一个二进制对称密钥,然后再用这个密钥加解密具体的数据。 因此,使用 passphrase 加密保护的 SSH Key 的安全性,取决于: passphrase 的复杂度,这对应其长度、字符集、是否包含特殊字符等。这由我们自己控制。 所使用的 KDF 算法的安全性。这由 OpenSSH 的实现决定。 那么,OpenSSH 的 passphrase 是如何实现的?是否足够安全? 我首先 Google 了下,找到一些相关的文章(注意如下文章内容与其时间点相关,OpenSSH 的新版本会有些变化): (2018)The default OpenSSH key encryption is worse than plaintext: OpenSSH 默认的 SSH RSA 密钥格式直接使用 MD5 来派生出用于 AES 加密的对称密钥,再用这个密钥加密你的 RSA 私钥,这意味着它的破解速度将会相当的快。 (2021)Password security of encrypted SSH private key: How to read round number or costfactor of bcrypt: 这里有个老哥在回答中简单推算了下,以说明他认为 OpenSSH 默认的 passphrase 加密相当安全。 在 OpenSSH release notes 中搜索 passphrase 跟 kdf 两个关键字,找到些关键信息如下: text OpenSSH 9.4/9.4p1 (2023-08-10) * ssh-keygen(1): increase the default work factor (rounds) for the bcrypt KDF used to derive symmetric encryption keys for passphrase protected key files by 50%. ---------------------------------- OpenSSH 6.5/6.5p1 (2014-01-30) * Add a new private key format that uses a bcrypt KDF to better protect keys at rest. This format is used unconditionally for Ed25519 keys, but may be requested when generating or saving existing keys of other types via the -o ssh-keygen(1) option. We intend to make the new format the default in the near future. Details of the new format are in the PROTOCOL.key file. 时间阶段 (OpenSSH 版本) ssh-keygen 默认密钥类型 ssh-keygen 默认密钥长度 私钥 KDF 算法 (带 passphrase 时) 默认/主要 KEX 算法 默认/主要对称加密算法 OpenSSH 4.x (约2005 - 2008) RSA 2048 位 (RSA, 自 4.0 起) 基于 MD5 (OpenSSL PEM 格式) diffie-hellman-group1-sha1, diffie-hellman-group-exchange-sha1 AES-CBC (更普遍), 3DES-CBC; HMAC-SHA1 OpenSSH 5.x (约2009 - 2013) RSA 2048 位 (RSA) 基于 MD5 (OpenSSL PEM 格式) ecdh-sha2-nistp256 等 ECDH 系列引入 (自 5.7), DH 仍常见 AES-CTR (自 5.2 起优先于 CBC), AES-CBC; HMAC-SHA1 OpenSSH 6.x (约2014 - 2015) RSA (Ed25519 于 6.5 引入) 2048 位 (RSA) bcrypt_pbkdf (新格式, 自 6.5; Ed25519 默认, RSA 需 -o) curve25519-sha256 (自 6.5 起引入并优先), ECDH 系列 chacha20-poly1305@openssh.com (自 6.5), AES-GCM (自 ~6.2); CBC 模式于 6.7 默认禁用 OpenSSH 7.x (约2015 - 2018) RSA 2048 位 (RSA) bcrypt_pbkdf (自 7.8 起所有新密钥默认) curve25519-sha256, ECDH 系列; diffie-hellman-group1-sha1 于 7.0 禁用; rsa-sha2-256/512 签名 (自 7.2) chacha20-poly1305, AES-GCM (AEAD 优先); 3DES-CBC 从客户端默认移除 (7.4) OpenSSH 8.x (约2019 - 2021) RSA (Ed25519 逐渐流行) 3072 位 (RSA, 自 8.0 起) bcrypt_pbkdf curve25519-sha256, ECDH 系列; ssh-rsa (SHA1 签名) 于 8.8 禁用主机认证; Ed25519 签名优先 (自 8.5) chacha20-poly1305, AES-GCM OpenSSH 9.x (约2022 - 至今) Ed25519 (自 9.5 起) 256 位 (Ed25519); 3072 位 (若选 RSA) bcrypt_pbkdf PQC 混合 KEX: sntrup761x25519-sha512@openssh.com (自 9.0 默认); mlkem768x25519-sha256 (自 9.9 默认提供); Terrapin 缓解 (9.6) chacha20-poly1305, AES-GCM OpenSSH 10.0 (预计 2025年4月) Ed25519 256 位 (Ed25519); 3072 位 (若选 RSA) bcrypt_pbkdf mlkem768x25519-sha256 (PQC KEX 默认); 服务器端默认禁用有限域 DH (modp); DSA 完全移除 chacha20-poly1305 (最优先), AES-GCM (优先于 AES-CTR) 所以从 2014 年 1 月发布的 OpenSSH 6.5 开始,才可使用 ed25519 密钥,它的 passphrase 默认使用 bcrypt_pbkdf 生成的。而对于 RSA 类型的密钥,一直到 2018-08-24 发布的 OpenSSH 7.8 才从 MD5 改到 bctypt_pbkdf. 即使 2023-08-10 发布的 9.4 版本增加了默认的 bcrypt KDF rounds 次数,它的安全性仍然很值得怀疑。bcrypt 本身的安全性就越来越差,现代化的加密工具基本都已经升级到了 scrypt 甚至 argon2. 因此要想提升安全性,最好是能更换更现代的 KDF 算法,或者至少增加 bcrypt_pbkdf 的 rounds 数量。 我进一步看了 man ssh-keygen 的文档,没找到任何修改 KDF 算法的参数,不过能通过 -a 参数来修改 KDF 的 rounds 数量,OpeSSh 9.4 的 man 信息中写了默认使用 16 rounds. 我们再了解下 ssh-keygen 默认参数,在 release note 中我进一步找到这个: text OpenSSH 9.5/9.5p1 (2023-10-04) Potentially incompatible changes -------------------------------- * ssh-keygen(1): generate Ed25519 keys by default. Ed25519 public keys are very convenient due to their small size. Ed25519 keys are specified in RFC 8709 and OpenSSH has supported them since version 6.5 (January 2014). 也就是说从 2023-10-04 发布的 9.5 开始,OpenSSH 才默认使用 ED25519。 再看下各主流操作系统与 OpenSSH 的对应关系: OS Distro Version Year 大致的 OpenSSH 版本 Ubuntu (LTS) Ubuntu 18.04 LTS 2018 OpenSSH 7.6p1 Ubuntu 20.04 LTS 2020 OpenSSH 8.2p1 Ubuntu 22.04 LTS 2022 OpenSSH 8.9p1 Ubuntu 24.04 LTS 2024 OpenSSH 9.6p1 Debian (稳定版) Debian 9 (Stretch) 2017 OpenSSH 7.4p1 Debian 10 (Buster) 2019 OpenSSH 7.9p1 Debian 11 (Bullseye) 2021 OpenSSH 8.4p1 Debian 12 (Bookworm) 2023 OpenSSH 9.2p1 macOS (主要版本) macOS 10.14-10.15 2018 OpenSSH 7.9p1, LibreSSL 2.7.3 macOS 11 (Big Sure) 2020 OpenSSH 8.1p1+ macOS 12 (Monterey) 2021 OpenSSH 8.6p1+ macOS 13 (Ventura) 2022 OpenSSH 9.0p1+ macOS 14 (Sonoma) 2023 OpenSSH 9.3p1+ macOS 15 (Sequoia) 2024 OpenSSH 9.6p1+ 考虑到大部分 Linux 用户或 SysAdm 都没有密码学基础,大概率不知道 KDF、Rounds 是什么意思,有理由怀疑很多人会使用默认参数生成密钥,由此可以推断出: 2023 年及之前发布的 Linux/macOS 使用的 OpenSSH 版本都低于 9.5 结论:绝大部分用户都仍然在使用 RSA 密钥 2020 年各主流 OS 才陆续升级到 OpenSSH 8.x 结论:在这之前,绝大部分用户都仍然在使用 RSA 2048 位密钥 2019 年之前各主流 OS 的 OpenSSH 发行版大都低于 7.8 结论:在这些老系统上生成的密钥,几乎全部都仍然在使用 PEM 格式的 RSA 密钥,这种密钥使用 MD5 派生密钥,加了密码也几乎等于裸奔。 如果你使用的也这种比较老的密钥类型,那千万别觉得自己加了 passphrase 保护就很安全,这完全是错觉( 即使是使用最新的 ssh-keygen 生成的 ED25519 密钥,其默认也是用的 bcrypt 16 rounds 生成加密密钥,其安全性在我看来也是不够的。 总结下,在不考虑其他硬件密钥/SSH CA 的情况下,最佳的 SSH Key 生成方式应该是: bash ssh-keygen -t ed25519 -a 256 -C "xxx@xxx" rounds 的值根据你本地的 CPU 性能来定,我在 Macbook Pro M2 上测了下,64 rounds 大概是 0.5s,128 rounds 大概需要 1s,256 rounds 大概 2s,用时与 rounds 值是线性关系。 考虑到我的个人电脑性能都还挺不错,而且只需要在每次重启电脑后通过 ssh-add ~/.ssh/xxx 解锁一次,后续就一直使用内存中的密钥了,一两秒的时间还是可以接受的,因此我将当前使用的所有 SSH Key 都使用上述参数重新生成了一遍。 2.3 SSH 密钥的分类管理 在所有机器上使用同一个 SSH 密钥,这是我过去的做法,但这样做有几个问题: 一旦某台机器的密钥泄漏,那么就需要重新生成并替换所有机器上的密钥,这很麻烦。 密钥需要通过各种方式传输到各个机器上,这也存在泄漏的风险。 因此,我现在的做法是: 对所有桌面电脑跟笔记本,都在其本地生成一个专用的 SSH 密钥配置到 GitHub 跟常用的服务器上。这个 SSH 私钥永远不会离开这台机器。 对于一些相对不重要的 Homelab 服务器,额外生成一个专用的 SSH 密钥,配置到这些服务器上。在一些跳板机跟测试机上会配置这个密钥方便测试与登录到其他机器。 上述所有 SSH 密钥都添加了 passphrase 保护,且使用了 bcrypt 256 rounds 生成加密密钥。 我通过这种方式缩小了风险范围,即使某台机器的密钥泄漏,也只需要重新生成并替换这台机器上的密钥即可。 最后再说明一点:OpenSSH 密钥并不是生成一次然后就可以高枕无忧了,为了确保足够安全性,也必须隔几年更换一次新密钥。 2.4 SSH CA - 更安全合理的 SSH 密钥管理方案? 搜到些资料: SSH 证书登录教程 TODO 待研究。 四、个人的账号密码管理 我曾经大量使用了 Chrome/Firefox 自带的密码存储功能,但用到现在其实也发现了它们的许多弊端。有同事推崇 1Password 的使用体验,它的自动填充跟同站点的多密码管理确实做得非常优秀,但一是要收费,二是它是商业的在线方案,基于零信任原则,我不太想使用这种方案。 作为开源爱好者,我最近找到了一个非常适合我自己的方案:password-store. 这套方案使用 gpg 加密账号密码,每个文件就是一个账号密码,通过文件树来组织与匹配账号密码与 APP/站点的对应关系,并且生态完善,对 firefox/chrome/android/ios 的支持都挺好。 缺点是用 GPG 加密,上手有点难度,但对咱来说完全可以接受。 我在最近使用 pass-import 从 firefox/chrome 中导入了我当前所有的账号密码,并对所有的重要账号密码进行了一次全面的更新,一共改了二三十个账号,全部采用了随机密码。 当前的存储同步与多端使用方式: pass 的加密数据使用 GitHub 私有仓库存储,pass 原生支持基于 Git 的存储方案。 因为数据全都是使用 ECC Curve 25519 GPG 加密的,即使仓库内容泄漏,数据的安全性仍然有保障。 在浏览器与移动端,则分别使用这些客户端来读写 pass 中的密码: Android: https://github.com/android-password-store/Android-Password-Store IOS: https://github.com/mssun/passforios Brosers(Chrome/Firefox): https://github.com/browserpass/browserpass-extension 基於雞蛋不放在同一個籃子裏的原則,otp/mfa 的動態密碼則使用 google authenticator 保存與多端同步,並留有一份離線備份用於災難恢復。登錄 Google 賬號目前需要我 Android 手機或短信驗證,因此安全性符合我的需求。 我的详细 pass 配置见ryan4yin/nix-config/password-store. 其他相关资料: awesome-password-store https://github.com/gopasspw/gopass: reimplement in go, with more features. 遇到过的一些问题与解法: passforios - Merge conflicts 3.1 pass 使用的 GPG 够安全么? GnuPG 是一个很有历史,而且使用广泛的加密工具,但它的安全性如何呢? 我找到些相关文档: 2021年,用更现代的方法使用PGP(上) Predictable, Passphrase-Derived PGP Keys OpenPGP - The almost perfect key pair 简单总结下,GnuPG 的每个 secret key 都是随机生成的,互相之间没有关联(即不像区块链钱包那样具有确定性)。生成出的 key 被使用 passphrase 加密保存,每次使用时都需要输入 passphrase 解密。 那么还是之前在调研 OpenSSH 时我们提到的问题:它使用的 KDF 算法与参数是否足够安全? OpenPGP 标准定义了String-to-Key (S2K) 算法用于从 passphrase 生成对称加密密钥,GnuPG 遵循该规范,并且提供了相关的参数配置选项,相关参数的文档OpenPGP protocol specific options 内容如下: text --s2k-cipher-algo name Use name as the cipher algorithm for symmetric encryption with a passphrase if --personal-cipher-preferences and --cipher-algo are not given. The default is AES-128. --s2k-digest-algo name Use name as the digest algorithm used to mangle the passphrases for symmetric encryption. The default is SHA-1. --s2k-mode n Selects how passphrases for symmetric encryption are mangled. If n is 0 a plain passphrase (which is in general not recommended) will be used, a 1 adds a salt (which should not be used) to the passphrase and a 3 (the default) iterates the whole process a number of times (see --s2k-count). --s2k-count n Specify how many times the passphrases mangling for symmetric encryption is repeated. This value may range between 1024 and 65011712 inclusive. The default is inquired from gpg-agent. Note that not all values in the 1024-65011712 range are legal and if an illegal value is selected, GnuPG will round up to the nearest legal value. This option is only meaningful if --s2k-mode is set to the default of 3. 默认仍旧使用 AES-128 做 passphrase 场景下的对称加密,数据签名还是用的 SHA-1,这俩都已经不太安全了,尤其是 SHA-1,已经被证明存在安全问题。因此,使用默认参数生成的 GPG 密钥,其安全性是不够的。 为了获得最佳安全性,我们需要: 使用如下参数生成 GPG 密钥: text gpg --s2k-mode 3 --s2k-count 65011712 --s2k-digest-algo SHA512 --s2k-cipher-algo AES256 ... 加密、签名、认证都使用不同的密钥,每个密钥只用于特定的场景,这样即使某个密钥泄漏,也不会影响其他场景的安全性。 为了在全局使用这些参数,可以将它们添加到你的 ~/.gnupg/gpg.conf 配置文件中。 详见我的 gpg 配置ryan4yin/nix-config/gpg 五、跨平台的加密备份同步工具的选择 我日常同时在使用 macOS 与 NixOS,因此不论是需要离线存储的灾难恢复数据,还是需要在多端访问的个人数据,都需要一个跨平台的加密备份与同步工具。 前面提到的 pass 使用 GnuPG 进行文件级别的加密,但在很多场景下这不太适用,而且 GPG 本身也太重了,还一堆历史遗留问题,我不太喜欢。 为了其他数据备份与同步的需要,我需要一个跨平台的加密工具,目前调研到有如下这些: 文件级别的加密 这个有很多现成的现代加密工具,比如 age/sops, 都挺不错,但是针对大量文件的情况下使用比较繁琐。 全盘加密,或者支持通过 FUSE 模拟文件系统 首先 LUKS 就不用考虑了,它基本只在 Linux 上能用。 跨平台且比较活跃的项目中,我找到了 rclone 与 restic 这两个项目,都支持云同步,各有优缺点。 restic 相对 rclone 的优势,主要是天然支持增量 snapshots 的功能,可以保存备份的历史快照,并设置灵活的历史快照保存策略。这对可能有回滚需求的数据而言是很重要的。比如说 PVE 虚拟机快照的备份,有了 restic 我们就不再需要依赖 PVE 自身孱弱的快照保留功能,全交给 restic 实现就行。 多端加密同步 上面提到的 rclone 与 restic 都支持各种云存储,因此都是不错的多端加密同步工具。 最流行的开源数据同步工具貌似是 synthing,但它对加密的支持还不够完善,暂不考虑。 进一步调研后,我选择了 age, rclone 与 restic 作为我的跨平台加密备份与同步工具。这三个工具都比较活跃,stars 很高,使用的也都是比较现代的密码学算法: age: 对于对称加密的场景,使用 ChaCha20-Poly1305 AEAD 加密方案,对称加密密钥使用 scrypt KDF 算法生成。 rclone: 使用基于 XSalsa20-Poly1305 的 AEAD 加密方案,key 通过 scrypt KDF 算法生成,并且默认会加盐。 restic: 使用 AES-256-CTR 加密,使用 Poly1305-AES 认证数据,key 通过 scrypt KDF 算法生成。 对于 Nix 相关的 secrets 配置,我使用了 age 的一个适配库 agenix 完成其自动加解密配置,并将相关的加密数据保存在我的 GitHub 私有仓库中。详见 ryan4yin/nix-config/secrets. 关于这个仓库的详细加解密方法,在后面第八节「桌面电脑的数据安全」中会介绍。 六、灾难恢复相关的数据存储与管理 相关数据包括:GitHub, Twitter, Google 等重要账号的二次认证恢复代码、账号数据备份、PGP 主密钥与吊销证书等等。 这些数据日常都不需要用到,但在账号或两步验证设备丢失时就非要使用到其中的数据才能找回账号或吊销某个证书,是非常重要的数据。 我目前的策略是:使用 rclone + 1024bits 随机密码加密存储到两个 U 盘中(双副本),放在不同的地方,并且每隔半年到一年检查一遍数据。 对应的 rclone 解密配置本身也设置了比较强的 passphrase 保护,并通过我的 secrets 私有 Git 仓库多端加密同步。 七、需要在多端访问的重要个人数据 相关数据包括:个人笔记、重要的照片、录音、视频、等等。 因为日常就需要在多端访问,因此显然不能离线存储。 1. 个人笔记 不包含个人隐私的笔记,我直接用公开 GitHub 仓库 [ryan4yin/knowledge] (https://github.com/ryan4yin/knowledge/) 存储了,不需要加密。 对于不便公开的个人笔记,有这些考虑: 我的个人笔记目前主要是在移动端编辑,因此支持 Android/iOS 的客户端是必须的。 要能支持 Markdown/Orgmode 等通用的纯文本格式,纯文本格式更容易编写与分析,而通用格式则可以避免被平台绑定。 因为主要是移动端编辑,其实不需要多复杂的功能。 以后可能会希望在桌面端做富文本编辑,但目前还没这种私人笔记的需求。 希望具有类似 Git 的分布式存储与同步、笔记版本管理功能,如果能直接使用 Git 那肯定是最好的。 端到端的加密存储与同步 如果有类似 Git 的 Diff 功能就更好了。 我一开始考虑直接使用基于 Git 仓库的方案,能获得 Git 的所有功能,同时还避免额外自建一个笔记服务。找到个 GitJournal ,数据存在 GitHub 私有仓库用了一个月,功能不太多但够用。但发现它项目不咋活跃,基于 SSH 协议的 Git 同步在大仓库上也有些毛病,而且数据明文存在 Git 仓库里,安全性相对差一些。 另外找到个 git-crypt 能在 Git 上做一层透明加密,但没找到支持它的移动端 APP,而且项目也不咋活跃。 在 https://github.com/topics/note-taking 下看了些流行项目,主要有这些: Joplin 支持 S3/WebDAV 等多种协议同步数据,支持端到端加密 Outline 等 Wiki 系统 它直接就是个 Web 服务,主要面向公开的 Wiki,不适合私人笔记 Logseq/Obsidian 等双链笔记软件(其中 Obsidian 是闭源软件) 都是基于本地文件的笔记系统,也没加密工具,需要借助其他工具实现数据加密与同步 其中 Logseq 是大纲流,一切皆列表。而 Obsidian 是文档流,比较贴近传统的文档编辑体验。 Obsidian 跟 Logseq 的 Sync 功能都是按月收费,相当的贵。社区有通过 Git 同步的方案,但都很 trickk,也不稳定。 AppFlowy/Affine/apitable 等 Notion 替代品 都是富文本编辑,不适合移动端设备 在移动端使用 Synthing 或 Git 等第三方工具同步笔记数据,都很麻烦,而且安全性也不够。因此目前看在移动端也能用得舒服的话,最稳妥的选择是第一类笔记 APP,简单试用后我选择了最流行的 Joplin. 2. 照片、视频等其他个人数据 Homelab 中的 Windows-NAS-Server,两个 4TB 的硬盘,通过 SMB 局域网共享,公网所有客户端 (包括移动端)都能通过 tailscale + rclone 流畅访问。 部分重要的数据再通过 rclone 加密备份一份到云端,可选项有: 青云对象存储 与七牛云对象存储 Kodo,它们都有每月 10GB 的免费存储空间,以及 1GB-10GB 的免费外网流量。 阿里云 OSS 也能免费存 5GB 数据以及每月 5GB 的外网流量,可以考虑使用。 八、桌面电脑與 Homelab 的数据安全 我的桌面电脑都是 macOS 与 NixOS,Homlab 虚拟机也已经 all in NixOS,另外我目前没有任何云上服务器。 另外虽然也有两台 Windows 虚拟机,但极少对它们做啥改动,只要做好虚拟机快照的备份就 OK 了。 对于 NixOS 桌面系统与 Homelab 虚拟机,我当前的方案如下: 桌面主机 启用 LUKS2 全盘加密 + Secure Boot,在系统启动阶段需要输入 passphrase 解密 NixOS 系统盘才能正常进入系统。 LUKS2 的 passphrase 为一个比较长的密码学随机字符串。 LUKS2 的所有安全设置全拉到能接受的最高(比较重要的是 --iter-time,计算出 unlock key 的用时,默认 2s,安全起见咱设置成了 5s) text cryptsetup --type luks2 --cipher aes-xts-plain64 --hash sha512 --iter-time 5000 --key-size 256 --pbkdf argon2id --use-urandom --verify-passphrase luksFormat device LUKS2 使用的 argon2id 是比 scrypt 更强的 KDF 算法,其安全性是足够的。 桌面主機使用 tmpfs 作为根目录,所有未明确声明持久化的数据,都会在每次重启后被清空,这强制我去了解自己装的每个软件都存了哪些数据,是否需要持久化,使整个系统更白盒,提升了整个系统的环境可信度。 Homelab Proxmox VE 物理机全部重装为 NixOS,启用 LUKS 全盘加密与 btrfs + zstd 压缩,买几个便宜的 U 盘用于自动解密(注意解密密钥的离线加密备份)。使用 K3s + KubeVirt 管理 QEMU/KVM 虚拟机。 Secrets 說明 重要的通用 secrets,都加密保存在我的 secrets 私有仓库中,在部署我的 nix-config 时使用主机本地的 SSH 系统私钥自动解密。 也就是说要在一台新电脑(不論是桌面主機還是 NixOS 虛擬機)上成功部署我的 nix-config 配置,需要的准备流程: 本地生成一个新的 ssh key,将公钥配置到 GitHub,并 ssh-add 这个新的私钥,使其能够访问到我的私有 secrets 仓库。 将新主机的系统公钥 /etc/ssh/ssh_host_ed25519_key.pub 发送到一台旧的可解密 secrets 仓库数据的主机上。如果该文件不存在则先用 sudo ssh-keygen -A 生成。 在旧主机上,将收到的新主机公钥添加到 secrets 仓库的 secrets.nix 配置文件中,并使用 agenix 命令 rekey 所有 secrets 数据,然后 commit & push。 现在新主机就能够通过 nixos-rebuild switch 或 darwin-rebuild switch 成功部署我的 nix-config 了,agenix 会自动使用新主机的系统私钥/etc/ssh/ssh_host_ed25519_key 解密 secrets 仓库中的数据并完成部署工作。 这份 secrets 配置在 macOS 跟 NixOS 上通用,也与 CPU 架构无关,agenix 在这两个系统上都能正常工作。 基于安全性考虑,对 secrets 进行分类管理与加密: 桌面电脑能解密所有的 secrets Homelab 中的跳板机只能解密 Homelab 相关的所有 secrets 其他所有的 NixOS 虚拟机只能解密同类别的 secrets,比如一台监控机只能解密监控相关的 secrets. 对于 macOS,它本身的磁盘安全我感觉就已经做得很 OK 了,而且它能改的东西也比较有限。我的安全设置如下: 启用 macOS 的全盘加密功能 常用的 secrets 的部署与使用方式,与前面 NixOS 的描述完全一致 macOS/NixOS 数据的灾难恢复? 在使用 nix-darwin 跟 NixOS 的情况下,整个 macOS/NixOS 的系统环境都是通过我的ryan4yin/nix-config 声明式配置的,因此桌面电脑的灾难恢复根本不是一个问题。 只需要简单的几行命令就能在一个全新的系统上恢复出我的 macOS / NixOS 桌面环境,所有密钥也会由 agenix 自动解密并放置到正确的位置。 要说有恢复难题的,也就是一些个人数据了,这部分已经在前面第七小节介绍过了,用 rclone/restic 就行。 九、总结下我的数据存在了哪些地方 secrets 私有仓库: 它会被我的 nix-config 自动拉取并部署到所有主力电脑上,包含了 homelab ssh key, GPG subkey, 以及其他一些重要的 secrets. 它通过我所有桌面电脑的 /etc/ssh/ssh_host_ed25519_key.pub 公钥加密,在部署时自动使用对应的私钥解密。 此外该仓库还添加了一个灾难恢复用的公钥,确保在我所有桌面电脑都丢失的极端情况下,仍可通过对应的灾难恢复私钥解密此仓库的数据。该私钥在使用 age 加密后(注:未使用 rclone 加密)与我其他的灾难恢复数据保存在一起。 password-store: 我的私人账号密码存储库,通过 pass 命令行工具管理,使用 GPG 加密,GPG 密钥备份被通过 age/agenix 加密保存在上述 secrets 仓库中。 由于 GnuPG 自身导出的密钥备份数据安全性欠佳,因此我使用了 age + passphrase 对其进行了二次对称加密,然后再通过 agenix 加密(第三次加密,使用非对称加密算法)保存在 secrets 仓库中。这保障了即使我的 GPG 密钥在我所有的桌面电脑上都存在,但安全性仍旧很够。 rclone 加密的备份 U 盘(双副本):离线保存一些重要的数据。其配置文件被加密保存在 secrets 仓库中,其配置文件的解密密码被加密保存在 password-store 仓库中。 这套方案的大部分部署工作都是由我的 Nix 配置自动完成的,整个流程的自动化程度很高,所以这套方案带给我的额外负担并不大。 secrets 这个私有仓库是整个方案的核心,它包含了所有重要数据(password-store/rclone/…)的解密密钥。如果它丢失了,那么所有的数据都无法解密。 但好在 Git 仓库本身是分布式的,我所有的桌面电脑上都有对应的完整备份,我的灾难恢复存储中也会定期备份一份 secrets/password-store 两个仓库的数据过去以避免丢失。 另外需要注意的是,为了避免循环依赖,secrets 与 password-store 这两个仓库的备份不应该使用 rclone 再次加密,而是直接使用 age 对称加密。这样只要我还记得 age 的解密密码、gpg 密钥的 passphrase 等少数几个密码,就能顺着整条链路解密出所有的数据。 十、这套方案下需要记忆几个密码?这些密码该如何设计? 绝大部分密码都建议设置为包含大小写跟部分特殊字符的密码学随机字符串,通过 pass 加密保存与多端同步与自动填充,不需要额外记忆。考虑到我们基本不会需要手动输入这些密码,因此它们的长度可以设置得比较长,比如 16-24 位(不使用更长密码的原因是,许多站点或 APP 都限制了密码长度,这种长度下使用 passphrase 单词组的安全性相对会差一点,因此也不推荐)。 再通过一些合理的密码复用手段,可以将需要记忆的密码数量降到 3 - 5 个,并且确保日常都会输入,避免遗忘。 不过这里需要注意一点,就是 SSH 密钥、GPG 密钥、系统登录密码这三个密码最好不要设成一样。前面我们已经做了分析,这三个 passphrase 的加密强度区别很大,设成一样的话,使用 bcrypt 的 SSH 密钥将会成为整个方案的短板。 而关于密码内容的设计,这个几核心 passphrase 的长度都是不受限的,有两个思路(注意不要在密码中包含任何个人信息): 使用由一个个单词组成的较长的 passphrase,比如don't-do-evil_I-promise-this-would-become-not-a-dark-corner 这样的。 使用字母大小写加数字、特殊字符组成的密码学随机字符串,比如 fsD!.*v_F*sdn-zFkJM)nQ 这样的。 第一种方式的优点是,这些单词都是常用单词,记忆起来会比较容易,而且也不容易输错。 第二种方式的优点是,密码学随机字符串可以以更短的长度达到与第一种方式相当的安全性。但它的缺点也比较明显——容易输错,而且记忆起来也不容易。 两种方式是都可以,如果你选择第二种方式,可以专门编些小故事来通过联想记忆它们,hint 中也能加上故事中的一些与密码内容无直接关联的关键字帮助回忆。毕竟人类擅长记忆故事,但不擅长记忆随机字符。举个例子,上面的密码 fsD!.*v_F*sdn-zFkJM)nQ,可以找出这么些联想: fs: 「佛说」这首歌里面的歌词 D!: 头文字D! .*: 地面上的光斑(.),天上的星光(*) v_: 嘴巴张开(v)睡得很香的样子,口水都流到地上了(_) F*sdn: F*ck 软件定义网络(sdn) zFkJM: 在政府(zf)大门口(k),看(k) 见了 Jack Ma (JM) 在跳脱yi舞… )nQ: 宁静的夏夜,凉风习习,天上一轮弯月,你(n)问(Q)我,当下这一刻是否足够 把上面这些联想串起来,就是一个怀旧、雷人、结尾又有点温馨的无厘头小故事了,肯定能令你自己印象深刻。故事写得够离谱的话,你可能想忘都忘不掉了。 总之就是用这种方式,然后把密码中的每个字符都与故事中的某个关键字联系起来,这样就能很容易地记住这个密码了。如果你对深入学习如何记忆这类复杂的东西感兴趣,可以看看这本我最想要的记忆魔法书. 最后一点,就是定期更新一遍这些密码、SSH 密钥、GPG 密钥。所有数据的加密安全性都是随着时间推移而降低的,曾经安全的密码学算法在未来也可能会变得不再安全(这方面 MD5, SHA-1 都是很好的例子),因此定期更新这些密码跟密钥是很有必要的。 几个核心密码更新起来会简单些,可以考虑每年更新一遍,而密钥可以考虑每两三年更新一遍(时间凭感觉说的哈,没有做论证)。其他密码密钥则可以根据数据的重要性来决定更新频率。 十一、为了落地这套方案,我做了哪些工作? 前面已经基本都提到了,这里再总结下: 重新生成了所有的 SSH Key,增强了 passphrase 强度,bcrypt rounds 增加到 256,通过ssh-add 使用,只需要在系统启动后输入一次密码即可,也不麻烦。 重新生成了所有的 PGP Key,主密钥离线加密存储,本地只保留了加密、签名、认证三个 PGP 子密钥。 重新生成了所有重要账号的密码,全部使用随机密码,一共改了二三十个账号。考虑到旧的 backup code 可能已经泄漏,我也重新生成了所有重要账号的 backup code. 重装 NixOS,使用 LUKS2 做全盘加密,启用 Secure Boot. 同时使用 tmpfs 作为根目录,所有未明确声明持久化的数据,都会在每次重启后被清空。 使用 nix-darwin 与 home-manager 重新声明式地配置了我的两台 MacBook Pro(Intel 跟 Apple Silicon 各一台),与我的 NixOS 共用了许多配置,最大程度上保持了所有桌面电脑的开发环境一致性,也确保了我始终能快速地在一台新电脑上部署我的整个开发环境。 注销印象笔记账号,使用 evernote-backup 跟 evernote2md 两个工具将个人的私密笔记遷移到了 Joplin + OneDrive 上,Homelab 中設了通過 restic 定期自動加密備份 OneDrive 中的 Joplin 數據。 比较有价值的 GitHub 仓库,都设置了禁止 force push 主分支,并且添加了 GitHub action 自动同步到国内 Gitee. All in NixOS,将 Homelab 中的 PVE 全部使用 NixOS + K3s + KubeVirt 替换。从偏黑盒且可复现性差的 Ubuntu、Debian, Proxmox VE, OpenWRT 等 VM 全面替换成更白盒且可复现性强的 NixOS、KubeVirt,提升我对内网环境的掌控度,进而提升内网安全性。 十二、灾难恢复预案 这里考虑我的 GPG 子密钥泄漏了、pass 密码仓库泄漏了等各种情况下的灾难恢复流程。 TODO 后续再慢慢补充。 十三、未来可能的改进方向 目前我的主要个人数据基本都已经通过上述方案进行了安全管理。但还有这些方面可以进一步改进: 针对 Homelab 的虚拟机快照备份,从我旧的基于 rclone + crontab 的明文备份方案,切换到了基于 restic 的加密备份方案。 手机端的照片视频虽然已经在上面设计好了备份同步方案,但仍未实施。考虑使用 roundsync 加密备份到云端,实现多端访问。 进一步学习下 appamor, bubblewrap 等 Linux 下的安全限制方案,尝试应用在我的 NixOS PC 上。 当前成果nix-config/hardening Git 提交是否可以使用 GnuPG 签名,目前没这么做主要是觉得 PGP 这个东西太重了,目前我也只在 pass 上用了它,而且还在研究用 age 取代它。 尝试通过 hashcat,wifi-cracking 等手段破解自己的重要密码、SSH 密钥、GPG 密钥等数据,评估其安全性。 使用一些流行的渗透测试工具测试我的 Homelab 与内网环境,评估其安全性。 安全总是相对的,而且其中涉及的知识点不少,我 2022 年学了密码学算是为此打下了个不错的基础, 但目前看前头还有挺多知识点在等待着我。我目前仍然打算以比较 casual 的心态去持续推进这件事情,什么时候兴趣来了就推进一点点。 这套方案也可能存在一些问题,欢迎大家审阅指正。

2024/1/30
articleCard.readMore

NixOS 在 Lichee Pi 4A 上是如何启动的

文章是 2023-08-07 写的,后面就完全忘掉这回事了,今天偶然翻到它才想起要整理发布下…所以注意文章中的时间线是 2023 年 8 月。 零、前言 我从今年 5 月份初收到了内测板的 Lichee Pi 4A,这是当下性能最高的 RISC-V 开发板之一,不过当时没怎么折腾。 6 月初的时候我开始尝试在 Orange Pi 5 上运行 NixOS,在NixOS on ARM 的 Matrix 群组 中得到了俄罗斯老哥 @K900 的帮助,没费多大劲就成功了,一共就折腾了三天。 于是我接着尝试在 Lichee Pi 4A 上运行 NixOS,因为已经拥有了 Orange Pi 5 上的折腾经验,我以为这次会很顺利。但是实际难度远远超出了我的预期,我从 6 月 13 号开始断断续续折腾到 7 月 3 号,接触了大量的新东西,包括 U-Boot、OpenSBI、SPL Flash、RISCV Boot Flows 等等,还参考了 @chainsx 的 Fedora for Lichee Pi 4A 方案,请教了 @NickCao 许多 NixOS 相关的问题,@revy 帮我修了好几个 revyos/thead-kernel 在标准工具链上编译的 bug,期间也请教过 @HougeLangley 他折腾 Lichee Pi 4A 的经验。我在付出了这么多的努力后,才最终成功编译出了 NixOS 的系统镜像(包含 boot 跟 rootfs 两个分区)。 但是!现在要说「但是」了。 镜像是有了,系统却无法启动…找了各种资料也没解决,也没好意思麻烦各位大佬,搞得有点心灰意冷,就先把这部分工作放下了。 接着就隔了一个多月没碰 Lichee Pi 4A,直到 8 月 5 号,外国友人 @JayDeLux 在Mainline Linux for RISC-V TG 群组中询问我 NixOS 移植工作的进展如何(之前有在群里提过我在尝试移植),我才决定再次尝试一下。 在之前工作的基础上一番骚操作后,我在 8 月 6 号晚上终于成功启动了 NixOS,这次意外的顺利,后续也成功通过一份 Nix Flake 配置编译出了可用的 NixOS 镜像。 最终成果:https://github.com/ryan4yin/nixos-licheepi4a 整个折腾过程相当曲折,虽然最终达成了目标,但是期间遭受的折磨也真的不少。总的来说仍然是一次很有趣的经历,既学到了许多新技术知识、认识了些有趣的外国友人(@JayDeLux 甚至还给我打了 $50 美刀表示感谢),也跟 @HougeLangley 、@chainsx 、@Rabenda(revy) 等各位大佬混了个脸熟。 这篇文章就是记录下我在这个折腾过程中学到的所有知识,以飨读者,同时也梳理一下自己的收获。 本文的写作思路是自顶向下的,先从 NixOS 镜像的 boot 分区配置、启动脚本开始分析,过渡到实际的启动日志,再接续分析下后续的启动流程。NixOS 分析完了后,再看看与 RISC-V 相关的硬件固件与 bootloader 部分要如何与 NixOS 协同工作,使得 NixOS 能够在 Lichee Pi 4A 上正常启动。 一、基础知识介绍 1. Lichee Pi 4A 介绍 LicheePi 4A 是当前市面上性能最高的 RISC-V Linux 开发板之一,它以 TH1520 为主控核心 (4xC910@1.85G, RV64GCV,4TOPS@int8 NPU, 50GFLOP GPU),板载最大 16GB 64bit LPDDR4X,128GB eMMC,支持 HDMI+MIPI 双4K 显示输出,支持 4K 摄像头接入,双千兆网口(其中一个支持POE供电)和 4 个 USB3.0 接口,多种音频输入输出(由专用 C906 核心处理)。 以上来自 Lichee Pi 4A 官方文档Lichee Pi 4A - Sipeed Wiki. 总之它是我手上性能最高的 RISC-V 开发板。 LicheePi 4A 官方主要支持 RevyOS—— 一款针对 T-Head 芯片生态的 Debian 优化定制发行版。根据猴哥(@HougeLangley)文章介绍,它也是目前唯一且确实能够启用 Lichee Pi 4A 板载 GPU 的发行版, 2. NixOS 介绍 这个感觉就不用多说了,我在这几个月已经给 NixOS 写了非常多的文字了,感兴趣请直接移步ryan4yin/nixos-and-flakes-book. 在 4 月份接触了 NixOS 后,我成了 NixOS 铁粉。作为一名铁粉,我当然想把我手上的所有性能好点的板子都装上 NixOS,Lichee Pi 4A 自然也不例外。 我目前主要完成了两块板子的 NixOS 移植工作,一块是 Orange Pi 5,另一块就是 Lichee Pi 4A。 Orange Pi 5 是 ARM64 架构的,刚好也遇到了拥有该板子的 NixOS 用户 @K900,在他的帮助下我很顺利地就完成了移植工作。 而 Lichee Pi 4A 就比较曲折,也比较有话题性。所以才有了这篇文章。 二、移植思路 一个完整的嵌入式 Linux 系统,通常包含了 U-Boot、kernel、设备树以及根文件系统(rootfs)四个部分。 其中 U-Boot,kernel 跟设备树,都是与硬件相关的,需要针对不同的硬件进行定制。而 rootfs 的大部分内容(比如说 NixOS 系统的 rootfs 本身),都是与硬件无关的,可以通用。 我的移植思路是,从 LicheePi4A 官方使用的 RevyOS 中拿出跟硬件相关的部分(也就是 U-Boot, kernel 跟设备树这三个),再结合上跟硬件无关的 NixOS rootfs,组合成一个完整的、可在 LicheePi4A 上正常启动运行的 NixOS 系统。 RevyOS 针对 LicheePi4A 定制的几个项目源码如下: https://github.com/revyos/thead-kernel.git https://github.com/revyos/thead-u-boot.git https://github.com/revyos/thead-opensbi.git 思路很清晰,但因为 NixOS 本身的特殊性,实际操作起来,现有的 Gentoo, Arch Linux, Fedora 的移植仓库代码全都无法直接使用,需要做的工作还是不少的。 三、NixOS 启动流程分析 要做移植,首先就要了解 NixOS 系统本身的文件树结构以及系统启动流程,搞明白它跟 Arch Linux, Fedora 等其他发行版的区别,这样才好参考其他发行版的移植工作,搞明白该如何入手。 1. Bootloader 配置与系统文件树分析 这里方便起见,我直接使用我自己为 LicheePi4A 构建好的 NixOS 镜像进行分析。首先参照ryan4yin/nixos-licheepi4a 的 README 下载解压镜像,再使用 losetup 跟 mount 直接挂载镜像中的各分区进行初步分析: bash # 解压镜像 › mv nixos-licheepi4a-sd-image-*-riscv64-linux.img.zst nixos-lp4a.img.zst › zstd -d nixos-lp4a.img.zst # 将 img 文件作为虚拟 loop 设备连接到系统中 › sudo losetup --find --partscan nixos-lp4a.img # 查看挂载的 loop 设备 › lsblk | grep loop loop0 7:0 0 1.9G 0 loop ├─loop0p1 259:8 0 200M 0 part └─loop0p2 259:9 0 1.7G 0 part # 分别挂载镜像中的 boot 跟 rootfs 分区 › mkdir boot root › sudo mount /dev/loop0p1 boot › sudo mount /dev/loop0p2 root # 查看 boot 分区内容 › ls boot/ ╭───┬───────────────────────────┬──────┬─────────┬──────────────╮ │ # │ name │ type │ size │ modified │ ├───┼───────────────────────────┼──────┼─────────┼──────────────┤ │ 0 │ boot/extlinux │ dir │ 4.1 KB │ 44 years ago │ │ 1 │ boot/fw_dynamic.bin │ file │ 85.9 KB │ 24 years ago │ │ 2 │ boot/light_aon_fpga.bin │ file │ 50.3 KB │ 24 years ago │ │ 3 │ boot/light_c906_audio.bin │ file │ 16.4 KB │ 24 years ago │ │ 4 │ boot/nixos │ dir │ 4.1 KB │ 44 years ago │ ╰───┴───────────────────────────┴──────┴─────────┴──────────────╯ # 查看 root 分区内容 › ls root/ ╭───┬────────────────────────────┬──────┬──────────┬──────────────╮ │ # │ name │ type │ size │ modified │ ├───┼────────────────────────────┼──────┼──────────┼──────────────┤ │ 0 │ root/boot │ dir │ 4.1 KB │ 54 years ago │ │ 1 │ root/lost+found │ dir │ 16.4 KB │ 54 years ago │ │ 2 │ root/nix │ dir │ 4.1 KB │ 54 years ago │ │ 3 │ root/nix-path-registration │ file │ 242.0 KB │ 54 years ago │ ╰───┴────────────────────────────┴──────┴──────────┴──────────────╯ 可以看到 NixOS 整个根目录(/root)下一共就四个文件夹,其中真正保存有系统数据的文件夹只有/boot 跟 /nix 这两个,这与传统的 Linux 发行版大相径庭。有一点 Linux 使用经验的朋友都应该清楚,传统的 Linux 发行版遵循 UNIX 系统的FHS 标准,根目录下会有很多文件夹,比如/bin、/etc、/home、/lib、/opt、/root、/sbin、/srv、/tmp、/usr、/var 等等。 那 NixOS 它这么玩,真的能正常启动么?这就是我在构建出镜像后却发现无法在 LicheePi 4A 上启动时,最先产生的疑问。在询问 @chainsx 跟 @revy 系统无法启动的解决思路的时候,他们也一脸懵逼,觉得这个文件树有点奇葩,很怀疑是我构建流程有问题导致文件树不完整。 但实际上 NixOS 就是这么玩的,它 rootfs 中所有的数据全都存放在 /nix/store 这个目录下并且被挂载为只读,其他的文件夹以及其中的文件都是在运行时动态创建的。这是它实现声明式系统配置、可回滚更新、可并行安装多个版本的软件包等等特性的基础。 下面继续分析,先仔细看下 /boot 的内容: bash › tree boot boot ├── extlinux │   └── extlinux.conf ├── fw_dynamic.bin ├── light_aon_fpga.bin ├── light_c906_audio.bin └── nixos ├── 2n6fjh4lhzaswbyacaf72zmz6mdsmm8l-initrd-k-riscv64-unknown-linux-gnu-initrd ├── l18cz7jd37n35dwyf8wc8divm46k7sdf-k-riscv64-unknown-linux-gnu-dtbs │   ├── sifive │   │   └── hifive-unleashed-a00.dtb │   └── thead │   ├── fire-emu-crash.dtb │   ├── fire-emu.dtb │   ├── ...... (省略) │   ├── light-fm-emu-audio.dtb │   ├── light-fm-emu-dsi0-hdmi.dtb │   ├── light-fm-emu-dsp.dtb │   ├── light-fm-emu-gpu.dtb │   ├── light-fm-emu-hdmi.dtb │   ├── light-lpi4a-ddr2G.dtb │   └── light_mpw.dtb └── l18cz7jd37n35dwyf8wc8divm46k7sdf-k-riscv64-unknown-linux-gnu-Image 6 directories, 64 files 可以看到: 它使用 /boot/extlinux/extlinux.conf 作为 U-Boot 的启动项配置,据U-Boot 官方的 Distro 文档 所言,这是 U-Boot 的标准配置文件。 另外还有一些 xxx.bin 文件,这些是一些硬件固件,其中的 light_c906_audio.bin 显然是玄铁 906 这个 IP 核的音频固件,其他的后面再研究。 NixOS 的 initrd, dtbs 以及 Image 文件都是在 /boot/nixos 下,这三个文件也都是跟 Linux 的启动相关的,现在不用管它们,下一步会分析。 再看下 /boot/extlinux/extlinux.conf 的内容: bash › cat boot/extlinux/extlinux.conf # Generated file, all changes will be lost on nixos-rebuild! # Change this to e.g. nixos-42 to temporarily boot to an older configuration. DEFAULT nixos-default MENU TITLE ------------------------------------------------------------ TIMEOUT 50 LABEL nixos-default MENU LABEL NixOS - Default LINUX ../nixos/l18cz7jd37n35dwyf8wc8divm46k7sdf-k-riscv64-unknown-linux-gnu-Image INITRD ../nixos/2n6fjh4lhzaswbyacaf72zmz6mdsmm8l-initrd-k-riscv64-unknown-linux-gnu-initrd APPEND init=/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b/init console=ttyS0,115200 root=UUID=14e19a7b-0ae0-484d-9d54-43bd6fdc20c7 rootfstype=ext4 rootwait rw earlycon clk_ignore_unused eth=$ethaddr rootrwoptions=rw,noatime rootrwreset=yes loglevel=4 FDT ../nixos/l18cz7jd37n35dwyf8wc8divm46k7sdf-k-riscv64-unknown-linux-gnu-dtbs/thead/light-lpi4a.dtb 从上述配置中能获得这些信息: 它创建了一个名为 nixos-default 的启动项并将它设为了默认启动项,extlinux 在启动阶段会根据该配置启动 NixOS 系统 启动项中的 LINUX INITRD FDT 三个参数分别指定了 kernel(Image 文件)、initrd 以及设备树(dtb)的位置,这三个文件我们在前面已经看到了,都在 /boot/nixos 下。 根据 Linux 官方文档Using the initial RAM disk (initrd) 所言,在使用了 initrd 这个内存盘的情况下,Linux 的启动流程如下: bootloader(这里是 u-boot) 根据配置加载 kernel 文件(Image)、dtb 设备树文件以及initrd 文件系统,然后以设备树跟 initrd 的地址为参数启动 Kernel. Kernel 将传入的 initrd 转换成一个内存盘并挂载为根文件系统,然后释放 initrd 的内存。 Kernel 接着运行 init 参数指定的可执行程序,这里是/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b/init, 这个 init 程序会挂载真正的根文件系统,并在其上执行后续的启动流程。 initrd 文件系统被移除,系统启动完毕。 initrd 这样一个临时的内存盘,通常用于在系统启动阶段加载一些内核中未内置但启动却必需的驱动或数据文件供 init 程序使用,以便后续能够挂载真正的根文件系统。 比如说挂载一个 LUKS 加密的根文件系统,这通常会涉及到提示用户输入 passphrase、从某个地方读取解密用的 keyfile 或者与插入的 USB 硬件密钥交互,这会需要读取内核之外的 keyfile 文件、用到内核之外的加密模块、USB 驱动、HID 用户输入输出模块或者其他因为许可协议、模块大小等问题无法被静态链接到内核中的各种内核模块或程序。initrd 就是用来解决这些问题的。 APPEND 参数包含有许多关键信息: 系统的 init 程序,也就是传说中的 1 号进程(PID 1),被设置为/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b/init, 这实际是一个 shell 脚本,我们下一步会重点分析它。 在传统的 Linux 发行版中,init 通常使用默认值 /sbin/init,它会被链接到/lib/systemd/systemd,也就是直接使用 systemd 作为 1 号进程。你可以在 Fedora/Ubuntu 等传统发行版中运行 ls -al /sbin/init 确认这一点,以及检查它们的/boot/grub/grub.cfs 启动项配置,看看它们有无自定义内核的 init 参数。 系统的 rootfs 分区为 /dev/disk/by-uuid/14e19a7b-0ae0-484d-9d54-43bd6fdc20c7,使用的文件系统为 ext4. earlycon(early console) 表示在系统启动早期就启用控制台输出,这样可以在系统启动阶段通过 UAER/HDMI 等接口看到相关的启动日志,方便调试。 其他参数先不管。 这样一分析就能得出结论:在执行 init 程序之前的启动流程都未涉及到真正的根文件系统,NixOS 与其他发行版在该流程中并无明显差异。 2. 实际启动日志分析 为了方便后续内容的理解,先看下 NixOS 系统在 LicheePi 4A 上的实际启动日志是个很不错的选择。 按我项目中的 README 正常烧录好系统后,使用 USB 转串口工具连接到 LicheePi 4A 的 UART0 串口,然后启动系统,就能看到 NixOS 的启动日志。 接线示例: LicheePi4A UART 调试接线 - 正面 LicheePi4A UART 调试接线 - 反面 接好线后使用 minicom 查看日志: bash › ls /dev/ttyUSB0 ╭───┬──────────────┬─────────────┬──────┬───────────────╮ │ # │ name │ type │ size │ modified │ ├───┼──────────────┼─────────────┼──────┼───────────────┤ │ 0 │ /dev/ttyUSB0 │ char device │ 0 B │ 6 minutes ago │ ╰───┴──────────────┴─────────────┴──────┴───────────────╯ › minicom -d /dev/ttyusb0 -b 115200 一个正常的启动日志示例如下: text Welcome to minicom 2.8 brom_ver 8 [APP][E] protocol_connect failed, exit. OpenSBI v0.9 ____ _____ ____ _____ / __ \ / ____| _ \_ _| | | | |_ __ ___ _ __ | (___ | |_) || | | | | | '_ \ / _ \ '_ \ \___ \| _ < | | | |__| | |_) | __/ | | |____) | |_) || |_ \____/| .__/ \___|_| |_|_____/|____/_____| | | |_| Platform Name : T-HEAD Light Lichee Pi 4A configuration for 8GB DDR board Platform Features : mfdeleg Platform HART Count : 4 Platform IPI Device : clint Platform Timer Device : clint Platform Console Device : uart8250 Platform HSM Device : --- Platform SysReset Device : thead_reset Firmware Base : 0x0 Firmware Size : 132 KB Runtime SBI Version : 0.3 Domain0 Name : root Domain0 Boot HART : 0 Domain0 HARTs : 0*,1*,2*,3* Domain0 Region00 : 0x000000ffdc000000-0x000000ffdc00ffff (I) Domain0 Region01 : 0x0000000000000000-0x000000000003ffff () Domain0 Region02 : 0x0000000000000000-0xffffffffffffffff (R,W,X) Domain0 Next Address : 0x0000000000200000 Domain0 Next Arg1 : 0x0000000001f00000 Domain0 Next Mode : S-mode Domain0 SysReset : yes Boot HART ID : 0 Boot HART Domain : root Boot HART ISA : rv64imafdcvsux Boot HART Features : scounteren,mcounteren,time Boot HART PMP Count : 0 Boot HART PMP Granularity : 0 Boot HART PMP Address Bits: 0 Boot HART MHPM Count : 16 Boot HART MHPM Count : 16 Boot HART MIDELEG : 0x0000000000000222 Boot HART MEDELEG : 0x000000000000b109 [ 0.000000] Linux version 5.10.113 (nixbld@localhost) (riscv64-unknown-linux-gnu-gcc (GCC) 13.1.0, GN0 [ 0.000000] OF: fdt: Ignoring memory range 0x0 - 0x200000 [ 0.000000] earlycon: uart0 at MMIO32 0x000000ffe7014000 (options '115200n8') [ 0.000000] printk: bootconsole [uart0] enabled [ 2.292495] (NULL device *): failed to find vdmabuf_reserved_memory node [ 2.453953] spi-nor spi0.0: unrecognized JEDEC id bytes: ff ff ff ff ff ff [ 2.460971] dw_spi_mmio ffe700c000.spi: cs1 >= max 1 [ 2.466001] spi_master spi0: spi_device register error /soc/spi@ffe700c000/spidev@1 [ 2.497453] sdhci-dwcmshc ffe70a0000.sd: can't request region for resource [mem 0xffef014064-0xffef01] [ 2.509014] misc vhost-vdmabuf: failed to find vdmabuf_reserved_memory node [ 3.386036] debugfs: File 'SDOUT' in directory 'dapm' already present! [ 3.392692] debugfs: File 'Playback' in directory 'dapm' already present! [ 3.399524] debugfs: File 'Capture' in directory 'dapm' already present! [ 3.406262] debugfs: File 'Playback' in directory 'dapm' already present! [ 3.413067] debugfs: File 'Capture' in directory 'dapm' already present! [ 3.425466] aw87519_pa 5-0058: aw87519_parse_dt: no reset gpio provided failed [ 3.432752] aw87519_pa 5-0058: aw87519_i2c_probe: failed to parse device tree node <<< NixOS Stage 1 >>> running udev... Starting systemd-udevd version 253.6 kbd_mode: KDSKBMODE: Inappropriate ioctl for device Gstarting device mapper and LVM... checking /dev/disk/by-label/NIXOS_SD... fsck (busybox 1.36.1) [fsck.ext4 (1) -- /mnt-root/] fsck.ext4 -a /dev/disk/by-label/NIXOS_SD NIXOS_SD: recovering journal NIXOS_SD: clean, 148061/248000 files, 818082/984159 blocks mounting /dev/disk/by-label/NIXOS_SD on /... <<< NixOS Stage 2 >>> running activation script... setting up /etc... ++ /nix/store/2w8nachmhqvbjswrrsdia5cx1afxxx60-util-linux-riscv64-unknown-linux-gnu-2.38.1-bin/bin/findm/ + rootPart=/dev/disk/by-label/NIXOS_SD ++ lsblk -npo PKNAME /dev/disk/by-label/NIXOS_SD + bootDevice=/dev/mmcblk1 ++ lsblk -npo MAJ:MIN /dev/disk/by-label/NIXOS_SD ++ /nix/store/zag1z2yvsh2ccpsbgsda7xhv4sfha7mj-gawk-riscv64-unknown-linux-gnu-5.2.1/bin/awk -F: '{print ' + partNum='26 ' + echo ,+, + sfdisk -N26 --no-reread /dev/mmcblk1 GPT PMBR size mismatch (8332023 != 122894335) will be corrected by write. The backup GPT table is not on the end of the device. This problem will be corrected by write. warning: /dev/mmcblk1: partition 26 is not defined yet Disk /dev/mmcblk1: 58.6 GiB, 62921900032 bytes, 122894336 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 Disklabel type: gpt Disk identifier: 58B10C85-BB4D-F94A-9194-82020FC9DC23 Old situation: Device Start End Sectors Size Type /dev/mmcblk1p1 16384 425983 409600 200M Microsoft basic data /dev/mmcblk1p2 425984 8331990 7906007 3.8G Linux filesystem /dev/mmcblk1p26: Created a new partition 3 of type 'Linux filesystem' and of size 54.6 GiB. New situation: Disklabel type: gpt Disk identifier: 58B10C85-BB4D-F94A-9194-82020FC9DC23 Device Start End Sectors Size Type /dev/mmcblk1p1 16384 425983 409600 200M Microsoft basic data /dev/mmcblk1p2 425984 8331990 7906007 3.8G Linux filesystem /dev/mmcblk1p3 8333312 122892287 114558976 54.6G Linux filesystem The partition table has been altered. Calling ioctl() to re-read partition table. Re-reading the partition table failed.: Device or resource busy The kernel still uses the old table. The new table will be used at the next reboot or after you run part. Syncing disks. + /nix/store/wm9ynqbkqi7gagggb4y6f4l454kkga32-parted-riscv64-unknown-linux-gnu-3.6/bin/partprobe + /nix/store/yln7ma9dldr3f2dva4l0iq275s4brxml-e2fsprogs-riscv64-unknown-linux-gnu-1.46.6-bin/bin/resize2D resize2fs 1.46.6 (1-Feb-2023) Filesystem at /dev/disk/by-label/NIXOS_SD is mounted on /; on-line resizing required old_desc_blocks = 1, new_desc_blocks = 1 The filesystem on /dev/disk/by-label/NIXOS_SD is now 988250 (4k) blocks long. + /nix/store/f5w7dd1f195bxkashhr5x0a788nxrxvc-nix-riscv64-unknown-linux-gnu-2.13.3/bin/nix-store --load-b + touch /etc/NIXOS + /nix/store/f5w7dd1f195bxkashhr5x0a788nxrxvc-nix-riscv64-unknown-linux-gnu-2.13.3/bin/nix-env -p /nix/vm + rm -f /nix-path-registration starting systemd... Welcome to NixOS 23.05 (Stoat)! [ OK ] Created slice Slice /system/getty. [ OK ] Created slice Slice /system/modprobe. [ OK ] Created slice Slice /system/serial-getty....... ...... 简单总结下日志中的信息: 整个启动流程被分成了三个阶段,分别是: OpenSBI: 这个阶段貌似进行了一些硬件相关的初始化,比如说串口、SPI、SD 卡等,貌似还有些报错,先不管。 NixOS Stage 1: 这应该就是 initrd 阶段干的活,内核加载了 systemd udev 内核模块,然后使用 busybox 的 fsck 检查了根文件系统,接着挂载了根文件系统。 NixOS Stage 2: 运行了一个什么activation script,它首先设置好了 /etc 文件夹,然后检查了根分区文件系统的情况,并自动执行了分区与文件系统的扩容操作。 接着通过 nix-env -p /nix/vm... 大概是切换了个运行环境。 最后启动了 systemd,这之后的流程就跟其他发行版没啥区别了(都是 systemd)。 3. init 程序分析 有了上面这些信息,我们就可以比较容易地理解 init 这个程序了,它主要对应前面日志中的 NixOS Stage 2,即在真正挂载根文件系统之后,执行的第一个用户态程序。 在 NixOS 中这个 init 程序实际上是一个 shell 脚本,可以直接通过 cat 或者 vim 来查看它的内容: bash › cat /nix/store/a5gnycsy3cq4ix2k8624649zj8xqzkxc-nixos-system-nixos-23.05.20230624.3ef8b37/init #! /nix/store/91hllz70n1b0qkb0r9iw1bg9xzx66a3b-bash-5.2-p15-riscv64-unknown-linux-gnu/bin/bash systemConfig=/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b export HOME=/root PATH="/nix/store/fifbf1h3i83jvan2vkk7xm4fraq7drm7-coreutils-riscv64-unknown-linux-gnu-9.1/bin:/nix/store/2w8nachmhqvbjswrrsdia5cx1afxxx60-util-linux-riscv64-unknown-linux-gnu-2.38.1-bin/bin" if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then # Process the kernel command line. for o in $(</proc/cmdline); do case $o in boot.debugtrace) # Show each command. set -x ;; esac done # Print a greeting. echo echo -e "\e[1;32m<<< NixOS Stage 2 >>>\e[0m" echo # Normally, stage 1 mounts the root filesystem read/writable. # However, in some environments, stage 2 is executed directly, and the # root is read-only. So make it writable here. if [ -z "$container" ]; then mount -n -o remount,rw none / fi fi # Likewise, stage 1 mounts /proc, /dev and /sys, so if we don't have a # stage 1, we need to do that here. if [ ! -e /proc/1 ]; then specialMount() { local device="$1" local mountPoint="$2" local options="$3" local fsType="$4" # We must not overwrite this mount because it's bind-mounted # from stage 1's /run if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" = true ] && [ "${mountPoint}" = /run ]; then return fi install -m 0755 -d "$mountPoint" mount -n -t "$fsType" -o "$options" "$device" "$mountPoint" } source /nix/store/vn0sga6rn69vkdbs0d2njh0aig7zmzi6-mounts.sh fi if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" = true ]; then echo "booting system configuration ${systemConfig}" else echo "booting system configuration $systemConfig" > /dev/kmsg fi # Make /nix/store a read-only bind mount to enforce immutability of # the Nix store. Note that we can't use "chown root:nixbld" here # because users/groups might not exist yet. # Silence chown/chmod to fail gracefully on a readonly filesystem # like squashfs. chown -f 0:30000 /nix/store chmod -f 1775 /nix/store if [ -n "1" ]; then if ! [[ "$(findmnt --noheadings --output OPTIONS /nix/store)" =~ ro(,|$) ]]; then if [ -z "$container" ]; then mount --bind /nix/store /nix/store else mount --rbind /nix/store /nix/store fi mount -o remount,ro,bind /nix/store fi fi if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then # Use /etc/resolv.conf supplied by systemd-nspawn, if applicable. if [ -n "" ] && [ -e /etc/resolv.conf ]; then resolvconf -m 1000 -a host </etc/resolv.conf fi # Log the script output to /dev/kmsg or /run/log/stage-2-init.log. # Only at this point are all the necessary prerequisites ready for these commands. exec {logOutFd}>&1 {logErrFd}>&2 if test -w /dev/kmsg; then exec > >(tee -i /proc/self/fd/"$logOutFd" | while read -r line; do if test -n "$line"; then echo "<7>stage-2-init: $line" > /dev/kmsg fi done) 2>&1 else mkdir -p /run/log exec > >(tee -i /run/log/stage-2-init.log) 2>&1 fi fi # Required by the activation script install -m 0755 -d /etc /etc/nixos install -m 01777 -d /tmp # Run the script that performs all configuration activation that does # not have to be done at boot time. echo "running activation script..." $systemConfig/activate # Record the boot configuration. ln -sfn "$systemConfig" /run/booted-system # Run any user-specified commands. /nix/store/91hllz70n1b0qkb0r9iw1bg9xzx66a3b-bash-5.2-p15-riscv64-unknown-linux-gnu/bin/bash /nix/store/cmvnjz39iq4bx4cq3lvri2jj0sjq5h24-local-cmds # Ensure systemd doesn't try to populate /etc, by forcing its first-boot # heuristic off. It doesn't matter what's in /etc/machine-id for this purpose, # and systemd will immediately fill in the file when it starts, so just # creating it is enough. This `: >>` pattern avoids forking and avoids changing # the mtime if the file already exists. : >> /etc/machine-id # No need to restore the stdout/stderr streams we never redirected and # especially no need to start systemd if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then # Reset the logging file descriptors. exec 1>&$logOutFd 2>&$logErrFd exec {logOutFd}>&- {logErrFd}>&- # Start systemd in a clean environment. echo "starting systemd..." exec /run/current-system/systemd/lib/systemd/systemd "$@" fi 简单总结下这个脚本的功能: 通过 mount -o remount,ro,bind /nix/store 将 /nix/store 目录重新挂载为只读,确保 Nix Store 的不可变性,从而使系统状态可复现。 直接开始执行 $systemConfig/activate 这个程序。 activate 完毕后,启动真正的 1 号进程 systemd,进入后续启动流程。 4. activate 程序分析 前面的 init 程序其实没干啥,根据我们看过的启动日志,大部分的功能应该都是在$systemConfig/activate 这个程序中完成的。 再看看其中的 $systemConfig/activate 的内容,它同样是一个 shell 脚本,直接 cat/vim 查看下: bash › cat root/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b/activate #!/nix/store/91hllz70n1b0qkb0r9iw1bg9xzx66a3b-bash-5.2-p15-riscv64-unknown-linux-gnu/bin/bash systemConfig='/nix/store/71wh9lvf94i1jcd6qpqw228fy5s8fv24-nixos-system-lp4a-23.05.20230806.240472b' export PATH=/empty for i in /nix/store/fifbf1h3i83jvan2vkk7xm4fraq7drm7-coreutils-riscv64-unknown-linux-gnu-9.1 /nix/store/x3hfwbwcqgl9zpqrk8kvm3p2kjns9asm-gnugrep-riscv64-unknown-linux-gnu-3.7 /nix/store/qn0yhj5d7r432rdh1885cn40gz184ww9-findutils-riscv64-unknown-linux-gnu-4.9.0 /nix/store/slwk77dzar2l1c4h9fikdw93ig4wdfy1-getent-glibc-riscv64-unknown-linux-gnu-2.37-8 /nix/store/yrf57f5h1rwmf3q70msx35a2p9f0rsjr-glibc-riscv64-unknown-linux-gnu-2.37-8-bin /nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13 /nix/store/2imxx6v9xhy8mbbx9q1r2d991m81inar-net-tools-riscv64-unknown-linux-gnu-2.10 /nix/store/2w8nachmhqvbjswrrsdia5cx1afxxx60-util-linux-riscv64-unknown-linux-gnu-2.38.1-bin; do PATH=$PATH:$i/bin:$i/sbin done _status=0 trap "_status=1 _localstatus=\$?" ERR # Ensure a consistent umask. umask 0022 #### Activation script snippet specialfs: _localstatus=0 specialMount() { local device="$1" local mountPoint="$2" local options="$3" local fsType="$4" if mountpoint -q "$mountPoint"; then local options="remount,$options" else mkdir -m 0755 -p "$mountPoint" fi mount -t "$fsType" -o "$options" "$device" "$mountPoint" } source /nix/store/vn0sga6rn69vkdbs0d2njh0aig7zmzi6-mounts.sh if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "specialfs" "$_localstatus" fi #### Activation script snippet binfmt: _localstatus=0 mkdir -p -m 0755 /run/binfmt if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "binfmt" "$_localstatus" fi #### Activation script snippet stdio: _localstatus=0 if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "stdio" "$_localstatus" fi #### Activation script snippet binsh: _localstatus=0 # Create the required /bin/sh symlink; otherwise lots of things # (notably the system() function) won't work. mkdir -m 0755 -p /bin ln -sfn "/nix/store/4y83vxk3mfk216d1jjfjgckkxwrbassi-bash-interactive-5.2-p15-riscv64-unknown-linux-gnu/bin/sh" /bin/.sh.tmp mv /bin/.sh.tmp /bin/sh # atomically replace /bin/sh if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "binsh" "$_localstatus" fi #### Activation script snippet check-manual-docbook: _localstatus=0 if [[ $(cat /nix/store/xzgmgymf510dicgppghq27lrh9fjpxfi-options-used-docbook) = 1 ]]; then echo -e "\e[31;1mwarning\e[0m: This configuration contains option documentation in docbook." \ "Support for docbook is deprecated and will be removed after NixOS 23.05." \ "See nix-store --read-log /nix/store/n232fhpqqqnlfjl0rj59xxms419glja2-options.json.drv" fi if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "check-manual-docbook" "$_localstatus" fi #### Activation script snippet domain: _localstatus=0 if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "domain" "$_localstatus" fi #### Activation script snippet users: _localstatus=0 install -m 0700 -d /root install -m 0755 -d /home /nix/store/6fap9xv6snx5fr2m7m804v4gc23pb1jh-perl-riscv64-unknown-linux-gnu-5.36.0-env/bin/perl \ -w /nix/store/gx91fdp4a099jpfwdkbdw2imvl3lalsk-update-users-groups.pl /nix/store/1zj6fk93qkqd3z8n34s4r40xnby2ci21-users-groups.json if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "users" "$_localstatus" fi #### Activation script snippet groups: _localstatus=0 if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "groups" "$_localstatus" fi #### Activation script snippet etc: _localstatus=0 # Set up the statically computed bits of /etc. echo "setting up /etc..." /nix/store/habrmd12my31s9r9fdby78l2dg5p7qyx-perl-riscv64-unknown-linux-gnu-5.36.0-env/bin/perl /nix/store/rg5rf512szdxmnj9qal3wfdnpfsx38qi-setup-etc.pl /nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "etc" "$_localstatus" fi #### Activation script snippet hashes: _localstatus=0 users=() while IFS=: read -r user hash tail; do if [[ "$hash" = "$"* && ! "$hash" =~ ^\$(y|gy|7|2b|2y|2a|6)\$ ]]; then users+=("$user") fi done </etc/shadow if (( "${#users[@]}" )); then echo " WARNING: The following user accounts rely on password hashing algorithms that have been removed. They need to be renewed as soon as possible, as they do prevent their users from logging in." printf ' - %s\n' "${users[@]}" fi if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "hashes" "$_localstatus" fi #### Activation script snippet hostname: _localstatus=0 hostname "lp4a" if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "hostname" "$_localstatus" fi #### Activation script snippet modprobe: _localstatus=0 # Allow the kernel to find our wrapped modprobe (which searches # in the right location in the Nix store for kernel modules). # We need this when the kernel (or some module) auto-loads a # module. echo /nix/store/wv00igsmj6mkk1ybssdch52hx0hx0x67-kmod-riscv64-unknown-linux-gnu-30/bin/modprobe > /proc/sys/kernel/modprobe if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "modprobe" "$_localstatus" fi #### Activation script snippet nix: _localstatus=0 install -m 0755 -d /nix/var/nix/{gcroots,profiles}/per-user if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "nix" "$_localstatus" fi #### Activation script snippet nix-channel: _localstatus=0 # Subscribe the root user to the NixOS channel by default. if [ ! -e "/root/.nix-channels" ]; then echo "https://nixos.org/channels/nixos-23.05 nixos" > "/root/.nix-channels" fi if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "nix-channel" "$_localstatus" fi #### Activation script snippet systemd-timesyncd-init-clock: _localstatus=0 if ! [ -f /var/lib/systemd/timesync/clock ]; then test -d /var/lib/systemd/timesync || mkdir -p /var/lib/systemd/timesync touch /var/lib/systemd/timesync/clock fi if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "systemd-timesyncd-init-clock" "$_localstatus" fi #### Activation script snippet udevd: _localstatus=0 # The deprecated hotplug uevent helper is not used anymore if [ -e /proc/sys/kernel/hotplug ]; then echo "" > /proc/sys/kernel/hotplug fi # Allow the kernel to find our firmware. if [ -e /sys/module/firmware_class/parameters/path ]; then echo -n "/nix/store/ann0ayjx9qf296pssrk2b26fry235idz-firmware/lib/firmware" > /sys/module/firmware_class/parameters/path fi if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "udevd" "$_localstatus" fi #### Activation script snippet usrbinenv: _localstatus=0 mkdir -m 0755 -p /usr/bin ln -sfn /nix/store/fifbf1h3i83jvan2vkk7xm4fraq7drm7-coreutils-riscv64-unknown-linux-gnu-9.1/bin/env /usr/bin/.env.tmp mv /usr/bin/.env.tmp /usr/bin/env # atomically replace /usr/bin/env if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "usrbinenv" "$_localstatus" fi #### Activation script snippet var: _localstatus=0 # Various log/runtime directories. mkdir -m 1777 -p /var/tmp # Empty, immutable home directory of many system accounts. mkdir -p /var/empty # Make sure it's really empty /nix/store/yln7ma9dldr3f2dva4l0iq275s4brxml-e2fsprogs-riscv64-unknown-linux-gnu-1.46.6-bin/bin/chattr -f -i /var/empty || true find /var/empty -mindepth 1 -delete chmod 0555 /var/empty chown root:root /var/empty /nix/store/yln7ma9dldr3f2dva4l0iq275s4brxml-e2fsprogs-riscv64-unknown-linux-gnu-1.46.6-bin/bin/chattr -f +i /var/empty || true if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "var" "$_localstatus" fi #### Activation script snippet wrappers: _localstatus=0 chmod 755 "/run/wrappers" # We want to place the tmpdirs for the wrappers to the parent dir. wrapperDir=$(mktemp --directory --tmpdir="/run/wrappers" wrappers.XXXXXXXXXX) chmod a+rx "$wrapperDir" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/chsh" echo -n "/nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13/bin/chsh" > "$wrapperDir/chsh.real" # Prevent races chmod 0000 "$wrapperDir/chsh" chown root:root "$wrapperDir/chsh" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/chsh" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/dbus-daemon-launch-helper" echo -n "/nix/store/jqk530wxiq3832zyiqn8qi6i2pr3snnl-dbus-riscv64-unknown-linux-gnu-1.14.8/libexec/dbus-daemon-launch-helper" > "$wrapperDir/dbus-daemon-launch-helper.real" # Prevent races chmod 0000 "$wrapperDir/dbus-daemon-launch-helper" chown root:messagebus "$wrapperDir/dbus-daemon-launch-helper" chmod "u+s,g-s,u+rx,g+rx,o-rx" "$wrapperDir/dbus-daemon-launch-helper" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/fusermount" echo -n "/nix/store/2d68cpnlqls47ijrwss83swjk2q1v953-fuse-riscv64-unknown-linux-gnu-2.9.9/bin/fusermount" > "$wrapperDir/fusermount.real" # Prevent races chmod 0000 "$wrapperDir/fusermount" chown root:root "$wrapperDir/fusermount" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/fusermount" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/fusermount3" echo -n "/nix/store/06w8lm5k9i2n1xhkszsf4pa9hw9l0r5s-fuse-riscv64-unknown-linux-gnu-3.11.0/bin/fusermount3" > "$wrapperDir/fusermount3.real" # Prevent races chmod 0000 "$wrapperDir/fusermount3" chown root:root "$wrapperDir/fusermount3" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/fusermount3" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/mount" echo -n "/nix/store/2w8nachmhqvbjswrrsdia5cx1afxxx60-util-linux-riscv64-unknown-linux-gnu-2.38.1-bin/bin/mount" > "$wrapperDir/mount.real" # Prevent races chmod 0000 "$wrapperDir/mount" chown root:root "$wrapperDir/mount" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/mount" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/newgidmap" echo -n "/nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13/bin/newgidmap" > "$wrapperDir/newgidmap.real" # Prevent races chmod 0000 "$wrapperDir/newgidmap" chown root:root "$wrapperDir/newgidmap" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/newgidmap" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/newgrp" echo -n "/nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13/bin/newgrp" > "$wrapperDir/newgrp.real" # Prevent races chmod 0000 "$wrapperDir/newgrp" chown root:root "$wrapperDir/newgrp" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/newgrp" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/newuidmap" echo -n "/nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13/bin/newuidmap" > "$wrapperDir/newuidmap.real" # Prevent races chmod 0000 "$wrapperDir/newuidmap" chown root:root "$wrapperDir/newuidmap" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/newuidmap" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/passwd" echo -n "/nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13/bin/passwd" > "$wrapperDir/passwd.real" # Prevent races chmod 0000 "$wrapperDir/passwd" chown root:root "$wrapperDir/passwd" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/passwd" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/ping" echo -n "/nix/store/mckzq3q58m31d8ax04gnjqx43niamis0-iputils-riscv64-unknown-linux-gnu-20221126/bin/ping" > "$wrapperDir/ping.real" # Prevent races chmod 0000 "$wrapperDir/ping" chown root:root "$wrapperDir/ping" # Set desired capabilities on the file plus cap_setpcap so # the wrapper program can elevate the capabilities set on # its file into the Ambient set. /nix/store/z2gpziznsj8rnv55vyq5n287g5cvx7lg-libcap-riscv64-unknown-linux-gnu-2.68/bin/setcap "cap_setpcap,cap_net_raw+p" "$wrapperDir/ping" # Set the executable bit chmod u+rx,g+x,o+x "$wrapperDir/ping" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/sg" echo -n "/nix/store/9al8xczxbm72i5q63n91fli5rynrfprl-shadow-riscv64-unknown-linux-gnu-4.13/bin/sg" > "$wrapperDir/sg.real" # Prevent races chmod 0000 "$wrapperDir/sg" chown root:root "$wrapperDir/sg" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/sg" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/su" echo -n "/nix/store/gbp100zp8a8gja22dyjz4nwv0qsxb7qy-shadow-riscv64-unknown-linux-gnu-4.13-su/bin/su" > "$wrapperDir/su.real" # Prevent races chmod 0000 "$wrapperDir/su" chown root:root "$wrapperDir/su" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/su" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/sudo" echo -n "/nix/store/scywdc7rd6cjfvji166a6d0bsjj90vys-sudo-riscv64-unknown-linux-gnu-1.9.13p3/bin/sudo" > "$wrapperDir/sudo.real" # Prevent races chmod 0000 "$wrapperDir/sudo" chown root:root "$wrapperDir/sudo" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/sudo" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/sudoedit" echo -n "/nix/store/scywdc7rd6cjfvji166a6d0bsjj90vys-sudo-riscv64-unknown-linux-gnu-1.9.13p3/bin/sudoedit" > "$wrapperDir/sudoedit.real" # Prevent races chmod 0000 "$wrapperDir/sudoedit" chown root:root "$wrapperDir/sudoedit" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/sudoedit" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/umount" echo -n "/nix/store/2w8nachmhqvbjswrrsdia5cx1afxxx60-util-linux-riscv64-unknown-linux-gnu-2.38.1-bin/bin/umount" > "$wrapperDir/umount.real" # Prevent races chmod 0000 "$wrapperDir/umount" chown root:root "$wrapperDir/umount" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/umount" cp /nix/store/wl1c1dgxb1zklpy5inpk7p798pm4zcca-security-wrapper-riscv64-unknown-linux-gnu/bin/security-wrapper "$wrapperDir/unix_chkpwd" echo -n "/nix/store/cn72qv0n576vg61mgaran7g2vj6gdjwn-linux-pam-riscv64-unknown-linux-gnu-1.5.2/bin/unix_chkpwd" > "$wrapperDir/unix_chkpwd.real" # Prevent races chmod 0000 "$wrapperDir/unix_chkpwd" chown root:root "$wrapperDir/unix_chkpwd" chmod "u+s,g-s,u+rx,g+x,o+x" "$wrapperDir/unix_chkpwd" if [ -L /run/wrappers/bin ]; then # Atomically replace the symlink # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/ old=$(readlink -f /run/wrappers/bin) if [ -e "/run/wrappers/bin-tmp" ]; then rm --force --recursive "/run/wrappers/bin-tmp" fi ln --symbolic --force --no-dereference "$wrapperDir" "/run/wrappers/bin-tmp" mv --no-target-directory "/run/wrappers/bin-tmp" "/run/wrappers/bin" rm --force --recursive "$old" else # For initial setup ln --symbolic "$wrapperDir" "/run/wrappers/bin" fi if (( _localstatus > 0 )); then printf "Activation script snippet '%s' failed (%s)\n" "wrappers" "$_localstatus" fi # Make this configuration the current configuration. # The readlink is there to ensure that when $systemConfig = /system # (which is a symlink to the store), /run/current-system is still # used as a garbage collection root. ln -sfn "$(readlink -f "$systemConfig")" /run/current-system # Prevent the current configuration from being garbage-collected. mkdir -p /nix/var/nix/gcroots ln -sfn /run/current-system /nix/var/nix/gcroots/current-system exit $_status 这个脚本有点长,简单总结下它干了啥: 通过 source /nix/store/vn0sga6rn69vkdbs0d2njh0aig7zmzi6-mounts.sh 挂载一些目录,看下这个文件内容就知道,挂的是 /proc /sys /dev /rum 等几个临时目录。 通过 mkdir/install 等指令自动创建 /home /root /bin /usr /usr/bin 等目录 通过perl /nix/store/rg5rf512szdxmnj9qal3wfdnpfsx38qi-setup-etc.pl /nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc 配置生成 /etc 目录中的各种文件。 通过 ln 命令添加其他各种软链接,以及一些别的设置。 其中第三步 etc 目录的设置,实际数据基本都来自该脚本的第二个参数: bash › ls root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc ╭────┬──────────────────────────────────────────────────────────────────────────┬─────────┬────────┬──────────────╮ │ # │ name │ type │ size │ modified │ ├────┼──────────────────────────────────────────────────────────────────────────┼─────────┼────────┼──────────────┤ │ 0 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/bashrc │ symlink │ 54 B │ 54 years ago │ │ 1 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/binfmt.d │ dir │ 4.1 KB │ 54 years ago │ │ 2 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/dbus-1 │ symlink │ 50 B │ 54 years ago │ │ 3 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/default │ dir │ 4.1 KB │ 54 years ago │ │ 4 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/dhcpcd.exit-hook │ symlink │ 60 B │ 54 years ago │ │ 5 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/fonts │ symlink │ 69 B │ 54 years ago │ │ 6 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/fstab │ symlink │ 53 B │ 54 years ago │ │ 7 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/fuse.conf │ symlink │ 57 B │ 54 years ago │ │ 8 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/host.conf │ symlink │ 57 B │ 54 years ago │ │ 9 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/hostname │ symlink │ 56 B │ 54 years ago │ │ 10 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/hosts │ symlink │ 49 B │ 54 years ago │ │ 11 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/inputrc │ symlink │ 51 B │ 54 years ago │ │ 12 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/issue │ symlink │ 49 B │ 54 years ago │ │ 13 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/kbd │ symlink │ 61 B │ 54 years ago │ │ 14 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/locale.conf │ symlink │ 55 B │ 54 years ago │ │ 15 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/login.defs │ symlink │ 54 B │ 54 years ago │ │ 16 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/lsb-release │ symlink │ 59 B │ 54 years ago │ │ 17 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/lvm │ dir │ 4.1 KB │ 54 years ago │ │ 18 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/man_db.conf │ symlink │ 59 B │ 54 years ago │ │ 19 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/modprobe.d │ dir │ 4.1 KB │ 54 years ago │ │ 20 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/modules-load.d │ dir │ 4.1 KB │ 54 years ago │ │ 21 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/nanorc │ symlink │ 54 B │ 54 years ago │ │ 22 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/netgroup │ symlink │ 56 B │ 54 years ago │ │ 23 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/nix │ dir │ 4.1 KB │ 54 years ago │ │ 24 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/nscd.conf │ symlink │ 57 B │ 54 years ago │ │ 25 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/nsswitch.conf │ symlink │ 61 B │ 54 years ago │ │ 26 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/os-release │ symlink │ 58 B │ 54 years ago │ │ 27 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/pam │ dir │ 4.1 KB │ 54 years ago │ │ 28 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/pam.d │ dir │ 4.1 KB │ 54 years ago │ │ 29 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/pki │ dir │ 4.1 KB │ 54 years ago │ │ 30 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/profile │ symlink │ 55 B │ 54 years ago │ │ 31 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/protocols │ symlink │ 75 B │ 54 years ago │ │ 32 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/pulse │ dir │ 4.1 KB │ 54 years ago │ │ 33 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/resolvconf.conf │ symlink │ 63 B │ 54 years ago │ │ 34 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/rpc │ symlink │ 90 B │ 54 years ago │ │ 35 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/samba │ dir │ 4.1 KB │ 54 years ago │ │ 36 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/services │ symlink │ 74 B │ 54 years ago │ │ 37 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/set-environment │ symlink │ 59 B │ 54 years ago │ │ 38 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/shells │ symlink │ 54 B │ 54 years ago │ │ 39 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/ssh │ dir │ 4.1 KB │ 54 years ago │ │ 40 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/ssl │ dir │ 4.1 KB │ 54 years ago │ │ 41 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/sudoers │ symlink │ 51 B │ 54 years ago │ │ 42 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/sudoers.gid │ file │ 3 B │ 54 years ago │ │ 43 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/sudoers.mode │ file │ 5 B │ 54 years ago │ │ 44 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/sudoers.uid │ file │ 3 B │ 54 years ago │ │ 45 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/sysctl.d │ dir │ 4.1 KB │ 54 years ago │ │ 46 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/systemd │ dir │ 4.1 KB │ 54 years ago │ │ 47 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/terminfo │ symlink │ 70 B │ 54 years ago │ │ 48 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/tmpfiles.d │ dir │ 4.1 KB │ 54 years ago │ │ 49 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/udev │ dir │ 4.1 KB │ 54 years ago │ │ 50 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/vconsole.conf │ symlink │ 57 B │ 54 years ago │ │ 51 │ root/nix/store/qsbx6lnsbs54yszy7d1ni7xgz6h6ayjd-etc/etc/zoneinfo │ symlink │ 97 B │ 54 years ago │ ├────┼──────────────────────────────────────────────────────────────────────────┼─────────┼────────┼──────────────┤ │ # │ name │ type │ size │ modified │ ╰────┴──────────────────────────────────────────────────────────────────────────┴─────────┴────────┴──────────────╯ 这个 perl 脚本基本就是根据这个 nix store 中的 etc 文件夹,生成 /etc 目录中的各种文件或软链接。 四、硬件驱动部分 NixOS 要能在 LicheePi 4A 上正常启动,还需要有硬件固件的支持,因此光了解 NixOS 的启动流程还不够,还需要了解硬件固件的启动流程。这里简要介绍下 Linux 在 RISC-V 上的启动流程。 1. u-boot,u-boot-spl,u-boot-tpl 的关系 U-Boot 是嵌入式领域最常用的 bootloader, 对于一般嵌入式系统而言只需要一个 u-boot 作为 bootloader 即可,但入今的嵌入式 IC 已经转向 SOC 片上系统,其内部不仅仅是一颗 CPU 核,还可能包含各种各样的其他 IP,因而相关的上层软件也需要针对性的划分不同的功能域,操作域,安全域等上层应用。为了支持这些复杂而碎片化的应用需求,又或者因为 SRAM 太小以致无法放下整个 bootloader,SOC 的 Boot 阶段衍生出了多级 BootLoader,u-boot 为此定义了二三级加载器: spl:Secondary Program Loader,二级加载器 tpl:Tertiary Program Loader,三级加载器 spl 和 tpl 走 u-boot 完全相同的 boot 流程,不过在 spl 和 tpl 中大多数驱动和功能被去除了, 根据需要只保留一部分 spl 和 tpl 需要的功能,通过 CONFIG_SPL_BUILD 和 CONFIG_TPL_BUILD 控制;一般只用 spl 就足够了,spl 完成 ddr 初始化,并完成一些外设驱动初始化,比如 usb,emmc, 以此从其他外围设备加载 u-boot,但是如果对于小系统 spl 还是太大了,则可以继续加入 tpl,tpl 只做 ddr 等的特定初始化保证代码体积极小,以此再次从指定位置加载 spl,spl 再去加载 u-boot。 LicheePi4A 就使用了二级加载器,它甚至写死了 eMMC 的分区表,要求我们使用 fastboot 往对应的分区写入 u-boot-spl.bin,官方给出的命令如下: bash # flash u-boot into spl partition sudo fastboot flash ram u-boot-with-spl.bin sudo fastboot reboot # flash uboot partition sudo fastboot flash uboot u-boot-with-spl.bin 2. RISC-V 的启动流程 网上找到的一个图,涉及到一些 RISC-V 指令集相关的知识点: RISCV 开发版当前的引导流程 根据我们前面的 NixOS 启动日志,跟这个图还是比较匹配的,但我们没观察到任何 U-Boot 日志,有可能是因为 U-Boot 没开日志,暂时不打算细究。 3. OpenSBI 前面的 NixOS 启动日志跟启动流程图中都出现了 OpenSBI,那么 OpenSBI 是什么呢?为什么 ARM 开发版的启动流程中没有这么个玩意儿? 查了下资料,大概是说因为 RISC-V 是一个开放指令集,任何人都可以基于 RISC-V 开发自己的定制指令集,或者定制 IC 布局。这显然存在很明显的碎片化问题。OpenSBI 就是为了避免此问题而设计的, 它提供了一个标准的接口,即 Supervisor Binary Interface, SBI. 上层系统只需要适配 SBI 就可以了,不需要关心底层硬件的细节。IC 开发商也只需要实现 SBI 的接口,就可以让任何适配了 SBI 的上层系统能在其硬件平台上正常运行。 而 OpenSBI 则是 SBI 标准的一个开源实现,IC 开发商只需要将 OpenSBI 移植到自己的硬件平台上即可支持 SBI 标准。 而 ARM 跟 X86 等指令集则是封闭的,不允许其他公司修改与拓展其指令集,因此不存在碎片化的问题,也就不需要 OpenSBI 这样的东西。 4. fw_dynamic.bin 跟 u-boot-spl.bin 两个文件 fw_dynamic.bin: 我们 NixOS 镜像的 /boot 中就有这个固件,它是 OpenSBI 的编译产物。 RevyOS 的定制 OpenSBI 构建方法:https://github.com/revyos/thead-opensbi/blob/lpi4a/.github/workflows/build.yml u-boot-spl.bin: 这个文件是 u-boot 的编译产物,它是二级加载器。 RevyOS 的定制 u-boot 构建方法:https://github.com/revyos/thead-u-boot/blob/lpi4a/.github/workflows/build.yml 5. T-Head 官方的编译工具链 因为历史原因,TH1520 设计时貌似 RVV 还没出正式的规范,因此它使用了一些非标准的指令集,GCC 官方貌似宣称了永远不会支持这些指令集…(个人理解,可能有误哈) 因此为了获得最佳性能,LicheePi4A 官方文档建议使用 T-Head 提供的工具链编译整个系统。 但我在研究了 NixOS 的工具链实现,以及咨询了 @NickCao 后,确认了在 NixOS 上这几乎是不可行的。NixOS 因为不遵循 FHS 标准,它对 GCC 等工具链做了非常多的魔改,要在 NixOS 上使用 T-Head 的工具链,就要使这一堆魔改的东西在 T-Head 的工具链上也能 Work,这个工作量很大,也很有技术难度。 所以最终选择了用 NixOS 的标准工具链编译系统,@revy 老师也为此帮我做了些适配工作,解决了一些标准工具链上的编译问题。 Issue 区也有人提到了这个问题,Revy 老师也帮助补充了些相关信息:https://github.com/ryan4yin/nixos-licheepi4a/issues/14 五、我是如何构建出一个可以在 LicheePi 4A 上运行的 NixOS 镜像的 到这里,NixOS 在 LicheePI4A 上启动的整个流程就基本讲清楚了, NixOS 跟其他传统发行版在启动流程中最大的区别是它自定义了一个 init 脚本,在启动 systemd 之前,它会先执行这个脚本进行文件系统的初始化操作,准备好最基础的 FHS 目录结构,使得后续的 systemd 以及其他服务能正常启动。正是因为这个 init 脚本,NixOS 才能在仅有 /boot 与 /nix 这两个目录的情况下正常启动整个系统。 NixOS 数据的集中化只读存储使更多的骚操作成为可能,比如直接使用 tmpfs 作为根文件系统,将需要持久化的目录挂载到外部存储设备上,这样每次重启系统时,所有预期之外的临时数据都会被清空,进一步保证了系统的可复现性与安全性。如果你有系统洁癖,而且有兴趣折腾,那就快来看看 @LanTian 写的NixOS 系列(四):「无状态」操作系统 吧~ 最终在 LicheePi4A 成功启动后的登录的截图: NixOS 成功启动 那么基于我们到目前为止学到的知识,要如何构建出一个可以在 LicheePi 4A 上运行的 NixOS 镜像呢? 这个讲起来就很费时间了,涉及到了 NixOS 的交叉编译系统,内核 override,flakes,镜像构建等等,要展开讲的话也是下一篇文章了,有兴趣的可以直接看我的 NixOS on LicheePi4A 仓库:https://github.com/ryan4yin/nixos-licheepi4a. 简单的说,NixOS 跟传统 Linux 发行版的系统镜像构建思路是一致的,但因为其声明式与可复现性的特点,实际实现时出现了非常大的区别。以我的项目仓库为例,整个项目完全使用 Nix 语言声明式编写(内嵌了部分 Shell 脚本…),而且这份配置也可用于系统后续的持续声明式更新部署(我还给出了一个 demo)。 最后,再推荐一波我的 NixOS 入门指南:ryan4yin/nixos-and-flakes-book, 对 NixOS 感兴趣的读者们,快进我碗里来( 参考 Systemd Stage 1 - NixCon NA 2024 - California LicheePi 4A —— 这个小板有点意思(第一部分) - HougeLangley Analyzing the Linux boot process - By Alison Chaiken OpenSBI Platform Firmwares An Introduction to RISC-V Boot flow: Overview, Blob vs Blobfree standards 基于 qemu-riscv 从 0 开始构建嵌入式 linux 系统 ch5-1. 什么是多级 BootLoader 与 opensbi(上)¶ Using the initial RAM disk (initrd) - kernel.org Differences Between vmlinux, vmlinuz, vmlinux.bin, zimage, and bzimage U-Boot 官方的 Distro 文档

2024/1/29
articleCard.readMore

我的 2023 - 认识更多有趣的人,见识更宽广的世界

闲言碎语 啊呀,又到了一年一度的传统节目——年终总结时间。 2023 年流水账 还是跟去年一样,先简单过一下我 2023 年的流水账(基本都摘抄自我的 /history,类似日记?): 1 月 再一次完成了公司 K8s 集群一年一度的升级,虽然仍然有比较大的压力,但这次的过程相当顺利。 然后就是朋友约饭,玩耍,回家过春节。 2 月 延续去年底开始对嵌入式硬件的兴趣,继续折腾 stm32 / orange pi 5 / esp32 等嵌入式硬件。 用 STM32 点亮了 TFT 液晶屏,以及搞定了使用 printf 打印日志到串口 -ryan4yin/learn-stm32f103c8t6 研究在 orangepi5(rk3558s) 上用 npu 跑 AI 任务,写了点笔记demos_rk3588 折腾 Proxmox VE 集群 主力节点 UM560 固态翻车了,是才用了三个月的 Asgard 512G SSD,颗粒是长江存储的。走京东售后了。(上次翻车是 2022-11-02 炸了根光威弈 Pro 1T,这也没隔多久啊…) 2022-11-02 翻车记录 - 系统无法启动 2023-02-03 翻车记录 - 系统能启动但是文件损坏 研究 Homelab 备份与数据同步方案,写了点笔记数据备份与同步策略 发布文章EE 入门(二) - 使用 ESP32 + SPI 显示屏绘图、显示图片、跑贪吃蛇 简单玩了玩 Stable-Diffusion-WebUI 跟 sd-webui-controlnet,抄了些网上的提示词,效果确实很惊艳。不过玩了一波就没啥兴趣了,不太想花很多精力去折腾它。 3 月 生活上 读完了「The Moon and Sixpence」 跟朋友到游泳馆游泳,算是开年以来最剧烈的一次运动… 跟同事们约着一起穿越了东西冲海岸线,这是我第三次走这条线,风景仍旧很美,当然也走得挺累… 买了块冲浪训练平衡板,练习了一段时间,挺有意思。 业余技术上 仍旧是折腾各种嵌入式设备,新入手了 Sipeed MAIX-III AXera-Pi AX620A(爱芯派)+ Maix M0s MCU, 野火鲁班猫 0,荔枝糖 Nano 9K、M1s Dock、Longan Nano 等一堆大小开发板,折腾各种 Linux 编译、嵌入式开发的内容。 被 Copilot X 小小震撼了下,花了 100 美刀买了个 1 年的订阅,价格不贵,希望能切实帮我提升写代码的效率。 发了篇新博客:Linux 上的 WireGuard 网络分析(一) 读了一点Linux Device Drivers Development 2nd Edition 4 月 在业余爱好上投入的精力越来越多,工作上相对的越来越懈怠,感觉碰到了瓶颈,但搞不明白该怎么解决。 听了 wd 佬的建议整了个达尔优 A87Pro 天空轴 v3,一番体验这个天空轴 v3 手感确实贼爽、声音也小,感觉可能有点类似静电容了(虽然我没用过静电容 emmm)。 我毕业以来就 19 年跟 20 年分别买过两把 IKBC 的茶轴跟红轴,茶轴放家里了,红轴一直放在公司用。当时国产轴感觉还不太出名,但是现在我聊键盘的朋友都看不上 cherry 轴了,网上搜了下 cherry 轴也有各种品控下降、轴心不稳、杂音大的诟病。 结合朋友推荐,另外看到 v2ex 上聊键盘的朋友也有说天空轴 v3 好用的,还在知乎上也看到有人说这个轴不错,于是就按捺不住心思下单了。到手确实很惊艳,甚至让我再一次喜欢上了打字的感觉!打了几篇小鹤练习群的赛文享受这种飘逸的 feel~ 搞了个 chatglm-6b int4 量化版,本地用我的拯救者笔记本(16G RAM + RTX3070 8G)玩了下, 响应速度感觉可以,确实有一定的上下文联想能力,简单的问题也能解答,但是有点不聪明的样子,内容投毒比较严重。 玩 AI 联想到淘垃圾显卡,看嗨了就直接整了套新主机新显示器(我的第一台 PC 主机,以前只买过笔记本电脑),玻璃侧透机箱,RTX 4090,双水冷,32 寸 4K 显示器。组装了大半夜,后面又折腾了好几天,机箱直接当手办展示柜了,效果相当惊艳!缺点一是套下来貌似花了两万多, 罪魁祸首是 RTX4090… 主机配置 机箱展示 机箱展示 去听了个 Live House,乐队叫迎春归,青岛的乐队,不过前面许多歌我都觉得一般般,主唱唱功也差了点,全靠架子鼓贝斯烘托。不过末尾几首歌还挺好听的。 天依手办到货,很飒~ 洛天依 秘境花庭 常服手办 新主机装了个 Endeavour OS 遇到些奇怪的问题,一怒之下决定换 OS,刚好朋友提到了 NixOS, 听说过这玩意儿能做到「可复现」,直接就在 Homelab 里开了个 NixOS 虚拟机开始折腾,由此开始了我的 NixOS 之旅。 用新主机试玩了米忽悠的新游戏「星穹铁道」,还是熟悉的画风跟 UI,制作质量也很高,回合式对战的玩法我本以为会枯燥,不过也还 OK。最重要是 4090 画质够高,很多可爱的角色,游戏的动画跟剧情也都很在线,总体很 Nice! 用新主机连 Quest 2 打 VR 游戏,发现做过参数优化后,RTX4090 跑 beta saber,Quest 2 的画质参数全调到最高, 5K 120 帧无压力,相当流畅。 用 RTX4090 玩 Cyperpunk 2077,顶配光追画质(叫啥 onedrive)贼棒,真的非常还原真实环境,在 GeForce Experience 上调了个啥优化参数后,4K 差不多能稳定在 100 帧,看半天风景。 5 月 月初,在虚拟机里折腾了大半个月 NixOS 后,成功地用几条简单的命令,在我的新主机上复现了整个 NixOS 环境,那一刻真的超级开心,半个月的努力终于得到了回报! 在新主机上成功复现出我的 NixOS 环境后,紧接着发布了我的系统配置ryan4yin/nix-config/v0.0.2) 以及这大半个月的学习笔记NixOS 与 Nix Flakes 新手入门, 然后事情就变得越来越有趣起来了!随着读者的反馈以及我对它的不断迭代,这份学习笔记逐渐膨胀成一篇一万多字的博文,并且有了中英双语,然后又转变成一本开源书藉nixos-and-flakes-book,在 NixOS 国际社区获得了大量好评!它给我带来了巨大的成就感以及社区参与感。 NixOS & Flakes Book 的部分评论 - Reddit NixOS & Flakes Book 的部分评论 - Discourse 与 GitHub NixOS & Flakes Book 的部分评论 - Discord 在 NixOS 上尝试了 i3 与 Hyprland 两个窗口管理器,并且使用 agenix 管理了系统中的敏感信息,比如密码、私钥、wireguard 配置等。 agenix 确实 OK,但它纯 bash 脚本实现的核心功能,体验太差了,错误信息一团糟,解决错误全靠自己摸索。 6 月 立了个 flag - 把 NixOS 移稙到我手上的两块开发版上跑起来,一块 ARM64 架构的 Orange Pi 5,以及另一块 RISC-V 架构的 LicheePi 4A. 花了好几天时间研究,在俄罗斯网友的耐心帮助下,终于在 6/4 晚上在 Orange Pi 5 上把 NixOS 跑起来了,还挺有成就感的(虽然现在也不知道拿这板子用来干啥…) 之后断断续续折腾了一个月的 NixOS on LicheePi 4A,试了很多方案,还请教了HougeLangley、@nickcao、@chainsx 等大佬,学会了很多 Linux 相关的东西,费尽千辛万苦终于成功把 rootfs 编译出来了,但死活引导不成功。感觉是 uboot-spl 跟 boot 分区这两个地方的内容有问题,但不知道怎么解决,累觉不爱。 收到一封来自 2018 年的我在 futureme.org 发送的邮件,回想起来,当时我是真迷茫哪。 2018 年写给 5 年后的我的邮件 受读者评论启发,将之前的 NixOS 笔记做成了一个单独的文档站点 + GitHub 仓库,nixos-and-flakes-book,也对其内容做了大量更新,用 ChatGPT 3.5 全面优化了英文内容,阅读体验大大提升(英文苦手默默路过…) NixOS & Flakes Book 7 月 NixOS 系统配置 ryan4yin/nix-config 迭代: 把办公电脑 Macbook Pro 2020 重裝了一遍系統,新系统環境完全通過 nix-darwin 安裝管理, 就連大部分的 macOS 系統配置也完全声明式管理了。至此,我的常用电脑环境(NixOS+macOS) 全部都使用同一份 nix 配置管理起来了,感覺非常香! Linux 与 macOS 都使用了同一份小鹤音形的 rime 配置,现在输入法的跨平台体验也完全一致了,非常棒! nixpkgs 对 macOS 的支持有限,因此常用的 GUI 程序都通过 nix-darwin 调用 homebrew 进行安装管理。 所有命令行工具的主题,全部统一为了catppuccin-mocha. 壁纸文件太大了,将它们拆分到单独的仓库中,方便管理。同时还添加了随机切换壁纸的功能。 添加了三台在 Proxmox VE 上运行的 NixOS 虚拟机,并且尝试用它们组建 NixOS 的分布式构建集群,挺有意思。 发现之前用的 alacritty 功能有限,于是将主力终端换成了 kitty,wezterm 作为备用选择, 而 alacritty 就基本不使用了。 主力编辑器从 VSCode 换成了 AstroNvim, 一个 Neovim 发行版,使用非常顺手,启动速度以及使用流畅度都比 VSCode 快很多,缺点就是花了挺长的时间完善我的 Neovim 配置(时间销金窟哪)。 AstroNvim(Neovim) 基于在 macOS 上折腾 nix-darwin 的经验,制作了一个ryan4yin/nix-darwin-kickstarter 模板仓库,并且在 Twitter 等社区分享了一波,获得一波好评。 从 4 月份折腾 NixOS 到现在,GitHub 上开了五六个 Nix 项目,获得了接近 400 stars,也认识了许多朋友、收到了许多好评,在这个圈子里是有点混开了的 feel. 甚至发现有俄罗斯的老铁在将我的 NixOS 小书翻译成俄语!fl42v/nixos-and-flakes-book-ru, 成就感又涨了一点。 8 月 时隔一个多月,在 LicheePi 的 Telegram 群组被老外 ping NixOS 移植进展。又来了点动力再次接续之前 6 月份的移植工作,一番尝试后成功在 Lichee Pi 4A 上把 NixOS 跑起来了!离开始移植已经过去了两个月,迟来的成功,泪目!ping 我的老外也在第二天用我提供的镜像成功把 NixOS 跑起来了!他甚至给我打了 $50 美元以表感谢,原因是「这太有意思了!」 ryan4yin/nixos-licheepi4a NixOS on LicheePi4A 排查问题的方法:首先刷好一个可在 LicheePi 4A 上正常启动的 Fedora 系统,然后用我编译出的 NixOS 的 rootfs 与 initrd 等文件,替换掉 Fedora 的 rootfs 以及 boot 分区中对应的文件,结果发现就能正常启动了!进一步排查确认到,我 6 月份生成的 NixOS rootfs 无法启动的原因是: 我使用了 opensbi 的主线代码编译出的 opensbi,而 LicheePi 4A 的 TH1520 核心需要使用它 fork 的分支 此外我生成的 img 镜像,分区也存在问题,root 分区的大小不对劲。 有读者在 NixOS Discourse 上询问我是否会考虑在 Patreon 上创建一个赞助页面,再加上之前已经有老外赞助了我 $50 刀,我于是在 GitHub 个人页面以及项目中都新增了 Patreon、buymeacoffee、爱发电与 Ethereum Address 等赞助链接。 截止 2023 年底,Patreon 共收到赞助 10 刀,buymeacoffee 收到赞助 70 刀,爱发电收到赞助 ¥25 元,以及加密货币收到赞助 50 刀。 Patreon Messages 写下新文章人生已过四分之一, 回顾我到目前为止的人生,以及对未来的展望,挺多感想。 在 @Manjusaka_Lee 的熏陶下,我也整了一个新的邮箱地址 ryan4yin@linux.com。先给 Linux Foundation 捐 99 刀,然后再付 150 刀就能得到这个终身邮箱地址。 一是用了这么久 Linux 也该捐点钱,二是感觉这个邮箱很酷! 因为一些心态上的变化,开始参加一些公益活动,想在工作与业余爱好之外,再找到一些自我价值感,比如说加入了「恒晖公益月捐」活动,每月捐 300 块。 在 @chainsx 的帮助下,成功在 rock 5a 跟 orange pi 5 plus 两块板子上把 NixOS 跑了起来。 9 月 临时起意看了个午夜场的《奥本海默》,IMAX 巨幕。给个 4 星没问题吧,演挺好的,原来美共曾经有这么多美国高级知识分子,这是我以前不了解的。 前几天跟老妹聊时,她引用了我看的小说里的一句话,然后我看@仿生狮子的荐书时发现,这一句就是《山月记》的摘抄,药哥说他也看过这书,挺好。当时就下单了,今天书到了,决定读一读。 读了第一个短篇,最知名的《山月记》,更类似一个寓言故事,最有韵味的就是那一句「深怕自己并非明珠而不敢刻苦琢磨,又自信有几分才华,不甘与瓦砾为伍。日渐避世离俗,心中自卑怯懦之自尊终在愤懑与羞怒中愈发张狂。世人皆为驯兽师,猛兽即个人性情。」 MIDI 键盘、山月记、以及凌乱的桌台... 跟朋友聊到陈行甲老师,他给我分享了深慈联《2023 年第二期慈善大讲堂》(报道见“坚守初心,笃行致远”,深慈联举办2023年第二期慈善大讲堂) 的视频,看上去分享者与与会者年龄段主要在 40+ 到 50+,他们看待问题的角度跟我们年轻人完全不同,陈行甲老师以及肖兴萍老师的演讲都干货满满,很有收获。 请半天假去看了中国国际光博会,看到了挺多新鲜的东西,像啥气动开关啊、光波导眼镜啊、高能量密度的锌空电池充电包啊、中科院光电所的两台小光刻机啊、长春光机所的空间站小机械臂啊、以及压电陶瓷驱动技术的各种应用,长了见识。 看了些 NixCon 2023 的视频,挺有意思。而且发现所有视频都有一只招财猫在讲台上哈哈。 最感兴趣的内容是这个 -NixCon2023 Bootstrapping Nix and Linux from TinyCC 看了记录片史蒂夫·乔布斯 Steve Jobs,Jobs 简直是最差的丈夫、父亲跟同事,但他确实是最牛逼的设计天才(或许这两句都应该再加一个「之一」)。 听了天依的新曲《歌行四方 - 天依游学记》,曲跟词都非常棒!完美融合了二次元跟三次元的各种传统音乐,天依的人物建模、服装设计跟渲染质量也提升了一个档次。 很多年前第一次听天依,感觉声音怪怪的,后来出了全息现场会,效果其实也挺差的,然后一步步地建模质量越来越好,人物越来越生动活泼,声音越来越自然,现在又走进了三次元,与传统音乐大师一起合奏。听下来真的很感动,有一种老父亲甚感欣慰的 feel. 了解了下世界法治指数 与中国各省份司法文明指数 湖南省貌似一直在倒数前二徘徊…很尴尬。 今天看到推上有 MtF 说自己双相情感障碍(躁郁症),突然就想百度一下,进一步找到了注意力缺失障碍(ADHD)这个病,联想到我自己好像有这个问题。 大学时曾经怀疑自己有这个注意力缺失症,还买了本《分心不是我的错》,但书买了一直没看 (我整个大学期间都不太看得下书),也没去医院看过。 中秋国庆连休 看完了《被讨厌的勇气》,觉得它虽然缺乏科学依据,但这套理论得确实很有价值,给我很大启发。 看了一点《这才是心理学》 带我妹使用 ESP32 练习 C 语言,兴趣导向。玩了用 5x5 的 WS2812 彩灯模块写跑马灯、红绿灯之类的小程序,她还非要用 SSD1306 小屏幕显示个「原神启动」,搞得挺开心 emmm 突然对 FPGA 燃起了兴趣,在 HDLBits 上刷了许多 Verilog 入门题。 10 月 读完了《叫魂:1768 年中国妖术大恐慌》 看了记录片《溥仪:末代皇帝》跟电影《末代皇帝》,两个片子的内容有些出入,不过这边的史料显然可信度更高,都好评。 我的开源小书 NixOS & Flakes Book 上了 Hacker News 热门,很开心:NixOS and Flakes Book: An unofficial book for beginners (free) 之前跟朋友聊过我可能有注意力缺失障碍(ADHD),朋友提到可以去看看医生。国庆后经@咩咩 再次提醒,约了深圳康宁医院(深圳市精神卫生中心)的特需门诊,然后确诊,开始服药治疗… 跟 0xffff 群友辩论 ADHD 病症相关问题,讨论的内容逐渐发散 -从 ADHD 注意力缺失症聊开去 遵医嘱,搜了些正念冥想的资料,看了点正念减压疗法创始人-乔.卡巴金 教正念冥想大师课(中英字幕), 尝试了下还有点意思。 跟我妹沟通后感觉她也比较有可能有 ADHD,提前安排她来深圳看心理医生。我妹确诊了抑郁症 + ADHD,医生给开了安非他酮,先吃半个月看看效果再复诊,同时也建议多带我妹出去运动散心。 我妹确诊抑郁症这一点真的让我很意外,让我意识到我一直有些忽视她的心理健康问题。 我确诊 ADHD 并开始用药之后心态有很大的变化,我妹确诊抑郁症又给了我很大的触动。我完全放下了所有技术上的业余爱好,工作上专心工作,业余时间更多地花在了关心家人、运动、学习心理学等事情上。 11 月 我的 NixOS & Flakes Book 被加进了 NixOS 官方文档的推荐阅读,挺开心的:nix.dev - PR 781 持续服药治疗以及复诊,确诊后这段时间是我印象中工作效率最高的一段时间,治疗挺有成效。 给我妹买往返火车票、学校请假、预约医生、带她在深圳到处玩。 有挺长一阵没怎么碰电脑了,最近发现我的 NixOS 总是启动没多久网络就会卡死,为了解决问题,我重装了 NixOS 系统,顺便给系统添加了 LUKS 全盘加密 + Secure Boot + impermanence, 根卷换成了 tmpfs,因此任何未显式声明持久化的数据,每次重启系统都会被清空。优雅,太优雅了! 重装好系统后网络问题就消失了,猜测是 home 目录有什么 X Server 相关的配置文件有毛病导致的。 12 月 沉寂一个多月的对业余项目的兴趣回来了一点,对 NixOS 相关的几个项目做了一番更新。 工作电脑用满三年换新了,新电脑是 Macbook Pro M2,终于用上了 M 系列的 CPU,体验显然比之前的 Intel 版本好很多,不发热了风扇也不响了,续航知道很牛但没啥机会测试。 淘汰下的旧工作电脑给装了个 NixOS,体验还不错,有些小问题但勉强能忍受。 主要问题:Touchbar 跑着跑着会失灵,Touch ID 无法使用,盖上盖子会直接关机,触摸板比较容易误触。其他的体验都挺不错的,第一次在笔记本上用 NixOS,还挺新奇的。 折腾 Guix 系统、Scheme 语言、Emacs 编辑器以及 nushell. 打算后面多写写 Scheme 跟 nushell,Python 脚本有点写腻歪了,而且 Python 在 NixOS 上有点水土不服。 将 Zellij 设为了默认的终端环境,体验非常丝滑,而且新手引导做得简直绝了,极大地降低了上手难度。 带我妹逛了深圳 AD01 动漫展。第一次逛漫展,体验挺不错的,玩得挺开心。 我妹对看到的各路原神、星穹铁道、魔道祖师、天依、芙莉莲等角色如数家珍,我变成了单纯的陪玩哈哈,已经变成跟不上时代的老东西了(不是 原神角色是最多的,如果论单个角色的话 Miku 应该是 Top1,另外因为芙莉莲正在热播,所以她的角色也很多。 感觉漫展真的相当年轻化,大都是 00 后,超有活力,是平常我接触不到的人群,混迹其中有找回一点我逝去的青春 emmmm 明年再参加的话,或许该考虑下 Cos 个啥角色,更能融入这个圈子,玩得更开心。 漫展刚入场 古风 coser - 魔道祖师? 芙莉莲 Miku 卡通背包 - 我妹同款 emmm 狐妖小红娘布景 卖海报的小店 跟我妹一起下馆子 - 老北京铜锅涮 网易云音乐年度歌手 - 今年是初音 2023 年 Highlights 1. 业余技术 技术方面我今年的进展还是挺大的,可跟去年写的展望几乎没啥关联,人生总是充满了意外哈哈… 今年的主要技术成就基本完全集中在 NixOS 这一块,新建的几个项目都收到了挺多 stars 跟好评。 截止 2023/12/31,我 stars 比较高的几个项目如下: ryan4yin/nixos-and-flakes-book: 这本开源小书的仓库于 2023/6/23 创建,目前获得了 15 个 issues,24 位贡献者,43 个 forks,923 个 stars,以及 4 位国外读者的共计 $80 零花钱赞助。 是我目前 stars 数最高的项目 它的文档站目前稳定在每天 150 UV ryan4yin/knowledge: 这份个人笔记我从 2019 年工作开始写,目前有了 38 个 forks,363 个 stars. ryan4yin/nix-config: 这份 NixOS 系统配置仓库于 2023/4/23 创建,目前获得了 6 位贡献者,23 个 forks,以及 297 个 stars. ryan4yin/nix-darwin-kickstarter: 我于 2023/7/19 创建的一个 Nix-Darwin 模板仓库,目前 133 stars. ryan4yin/nixos-rk3588: 这是我在 2023/6/4 创建的一个 NixOS 移植项目,目前支持了三块 RK3588 开发板,获得了 2 位贡献者,9 个 forks,11 个 issues,以及 49 stars. ryan4yin/nixos-licheepi4a: 同样是一个 NixOS 移植项目,但目标是基于 RISC-V 指令集的 LicheePi 4A 开发板。目前获得了 3 位贡献者与 23 stars,其中一位贡献者还赞助了 $50 给我。 这个项目断断续续花了两个月才搞定,用时远超预料…不过成功后获得的成就感也是巨大的! 对比下从 2023 年 1 月 1 日到现在,我的 GitHub Metrics 统计数据: 2023/01/01 GitHub 统计数据 2023/12/31 GitHub 统计数据 几个关键指标的变化: Stars: 312 => 2072, 涨幅 564%. Followers: 152 => 468, 涨幅 208%. Forkers: 97 => 203, 涨幅 109%. Watchers: 39 => 102, 涨幅 161%. 在折腾 NixOS 的过程中我写的入门指南 (nixos-and-flakes-book)获得了国内外社区的大量好评,其他项目也各有斩获;另外认识了好几位国内外的 NixOS 资深用户、开源项目作者以及嵌入式开发者,还收到了一些外国读者的打赏。 这些成绩给我带来了巨大的成就感以及社区参与感,也完全契合了我年初给自己的期许——「认识更多有趣的人,见识下更宽广的世界」。 今年不仅给 AstroNvim, ESP-IDF 等知名项目贡献了少许代码,甚至还创造了这么多个受欢迎的新项目、收到了几十个 PR。之前定的给一些开源项目贡献代码的目标,完全是超额完成了。 在折腾上 NixOS 后,年中的时候又顺带将 VSCode 彻底换成了 Zellij + Neovim,甚至年底又开始折腾 Guix 系统、Emacs 编辑器,以及 nushell 等新鲜玩意儿。 总的来说,业余技术今年搞到这个程度,相比去年,能称得上是「优秀」、「超出预期」。 要说我在搞技术这方面有啥诀窍可分享的话,那应该就是 Learning by Teaching,我 GitHub 上目前 stars 最高的两个项目(NixOS & Flakes Book 以及 knowledge)以及这个博客站点就是我在践行它最好的证明,而它们获得的评论、 stars、以及零花钱赞助,则证明了它的价值。 2. 工作 工作上只能说马马虎虎,上半年业余在 NixOS 上做的成果得到了非常多的认可,相当有成就感,花了大量的精力在 NixOS 上,也创建了许多相关项目。但另一方面,精力就这么多,我也一直做不到平衡好工作与生活/业余爱好,其结果就是那段时间没啥心思在工作上,把工作搞得一团糟。当时觉得自己进入了一个瓶颈期,在工作上找不到什么成就感,业余爱好虽然做出了不错的成绩,但又不能靠这个吃饭。 在折腾业余爱好期间,一种找不到方向的焦虑感一直萦绕着我,有跟一些朋友、同事沟通过这个问题, 但大道理谁都懂,真要做起来又是另一回事了。 因为业余搞了些嵌入式硬件感觉有意思,也有隐约考虑过转行搞硬件,但只是些粗浅的想法。到 8 月份的时候,做的几个 NixOS 项目收到些赞助,让我可能有点异想天开?了解了些「如何通过开源项目养活自己」类似的信息,8 月中下旬的时候在苏洋的折腾群里提到这个想法,被洋哥泼了冷水 emmm 冷静下来后回想,洋哥说的挺在理的,靠开源用爱发电真能养活自己的凤毛麟角,如果专门往商业项目的方向做,又没了那份折腾的快乐了。 8 月底的时候,苏洋的折腾群里发起一场自我介绍活动,读了许多群友的自我介绍,很有感触,于是基于我自己在群里发的自我介绍调整扩写,成果就是这篇人生已过四分之一。 当时文章完成后发出来,真觉得自己想明白不少,也跟领导同事做了些沟通,工作上状态有所好转。但还是很难集中注意力,分心的情况仍然相当严重。也尝试了通过番茄钟之类的方式来提高工作效率,但效果不佳。当时有点认命了,想着人生可能就是这样永远在这样未知的道路上的挣扎着前进,也有痛苦,也有快乐。 转折点是国庆前跟朋友提了嘴感觉自己有 ADHD,国庆后就被@咩咩催促去看心理医生,之后就确诊了 ADHD 并开始服药。确诊让我的心态出现巨大的改变,业余爱好因此放下了一个多月。而服药则帮助我扭转了工作状态,我的专注能力有了质的提升,这也是我今年最大的收获之一。 总体上,我今年的工作做得比去年差,尤其是上半年,总体上只能算「及格」吧。 3. 生活 3.1. 确诊 ADHD 以及治疗 前文提了,我今年确诊了 ADHD,这是我今年最大的收获之一。它让我搞明白了,原来我一直存在的注意力问题,并不是什么人格缺陷或者意志力不够,而是一种有挺多人都存在的功能失调(disorder), 可以通过药物等方式治疗缓解。 考虑到 ADHD 的遗传特性,跟妹妹、父母一番沟通后,带我妹来深圳看医生,确诊了 ADHD 以及抑郁症。说真的,我一直知道我妹妹情绪比较敏感,但从没想过是因为抑郁症。 这之后,除了工作外,我大部分的精力都花在了关心家人、运动、学习心理学等事情上。这是一个非常明显的转变,我跟我妹的关系更好了,另外持续好几个月的治疗跟每个月带她在深圳散散心,她的情绪也有了很大的改善。 3.2. 参与公益活动 因缘际会跟 @Manjusaka_Lee 以及另一位朋友聊到了公益, 当时在工作上缺乏动力,想在其他的事情上找找感觉,公益本身也是一件很有意义的事情,那段时间学习了解了许多公益资料,参加了一些公益会展与捐款活动,还一度考虑做志愿者。 今年算是迈出了参与公益活动的第一步,这拓展了我的视野,让我更加了解了社会。在学习了解公益期间也结识了一些志同道合的朋友,这也挺符合我去年给自己的期许——「认识更多有趣的人,见识下更宽广的世界」。 做事情,最难的就是从 0 到 1,因此今年的这一进展就显得尤为可贵,称得上是「优秀」。 3.3. 阅读与写作 首先是我 2023 年的读书记录: 2023-03-09 - The Moon and Sixpence 你是要地上的六便士,还是要天上的月亮? 2023-08-31 -How to Do Great Work - Paul Graham 黑客与画家一篇两万多字的长文,也算是一本小书了吧,干货满满。 2023-09-29 - 《被讨厌的勇气》 一本通过对话的形式讲述阿勒德心理学的书,这门心理学与现代科学心理学不同,它更偏向哲学。 2023-10-08 - 《叫魂:1768 年中国妖术大恐慌》 从 1768 年的叫魂案出发,分析了乾隆时期的中国社会的许多方面。比如因各种原因导致人口过度增长、人均资源减少、社会矛盾激化导致的普遍恐慌,以及官僚君主制的运作机制、存在的问题。此书以史为鉴,能帮助我们理解现代中国政治与中国社会的一些基本问题。 2023-10-17 - 《分心不是我的错》 介绍 ADHD 的一本书,列举了很多 ADHD 案例,也给出了诊疗建议,对我相当有用! 以及一些未读完的书: 《这才是心理学 - 看穿伪科学的批判性思维 第 11 版》 Psychology and Life, 20th edition, by Richard J. Gerrig The Great Gatsby - 10/41 《复杂 - 梅拉尼 米歇尔》 Linux Device Driver Development - Second Edition: Linux 驱动编程入门,2022 年出的新书,基于 Linux 5.10,amazon 上评价不错,目前只有英文版,写的很好,对新手很友好。 2023 年我读的书籍数量不多,没达成每月读一本书的目标。而写作方面,算上这篇总结,今年我一共写了 9 篇博文,也没达成每月写一篇的目标。 但鉴于我今年写了一本挺受欢迎的小书NixOS & Flakes Book,它得到了 NixOS 社区诸多好评,还在 10 月份上了 Hacker News 热榜,甚至被官方文档nix.dev 列入推荐阅读,我姑且将今年「阅读与写作」这一项的评分定为「超出期待」! 3.4. 其他 运动方面乏善可陈,穿越了一次东西冲海岸线,游了几次泳,12 月初晨跑了一周但因为是空腹跑, 胃炎给跑犯了,就没再跑了。体重全年都在 60kg 波动,没啥变化。 给我老爸全款买了我们全家的第一辆小轿车(自己没买,一是天天坐地铁用不到,二是我对车也缺乏兴趣)。 计划开始给父母约年度体检,待实施。 3.5. 总结 生活上今年达成了这么多有意义的成就(确诊 ADHD、将更多精力花在关心家人上、参与公益活动、给家里买车等等),我给自己的评价当然是「优秀」。 4. 各种站点的统计数据 首先是我的博客站点 https://thiscute.world/ 的统计数据: thiscute.world - 2023 年 Google Analytics 访问统计 thiscute.world - 2023 年 Google Analytics 访问统计 - 按国家分类 thiscute.world - 2023 年 Google Search 统计数据 另外是我今年 6 月份新建的 NixOS 笔记站点 https://nixos-and-flakes.thiscute.world 的统计数据(国外读者相当多,这跟 stars 以及收到的赞助情况也比较匹配): NixOS & Flakes Book - 2023 年 Google Analytics 访问统计 NixOS & Flakes Book - 2023 年 Google Analytics 访问统计 - 按国家分类 NixOS & Flakes Book - 2023 年 Google Search 访问统计 以及两个站点全年在 Vercel 上的流量统计(感谢 Vercel 每个月的 100G 免费流量,目前看白嫖阶段流量还有挺大增长空间哈哈): Vercel - 2023 年流量统计 还有文章阅读排行统计: 2023 年文章阅读排行 此外,我今年在 Twitter(X) 上比较活跃,也新增了不少粉丝: 2023 年 Twitter 统计数据 最后,是一些赞助平台上收到的零花钱统计: 2023 年 buymeacoffee 收入统计 2023 年 patreon 收入统计 2023 年爱发电收入统计 另有加密货币 $50 没有好的统计页面,就不放截图了。以及部分国外读者希望使用我未使用的支付方式赞助,我比较懒没折腾了… 2024 年展望 我当下工作维持着不错的状态,业余兴趣不减,生活上也挺满意。所以其实对明年反而没啥特别的期望,保持现在的状态就挺好的(这大概就是「现充」吧 emmm) 但总不能因为这个就摆烂,还是要给自己定一些目标,能否达成就看缘分了。 1. 业余技术 遵循兴趣优先的原则,简单列一下: 2023 年 AIGC 飞速发展,预期 2024 年很多新兴的 AIGC 技术将会落地到各种产品中,我也希望能玩一玩。不过貌似工作上就能玩得上,所以就不列在这里了 emmmm. 2023 年既然意外地点亮了 NixOS 跟 Neovim 这两项技能点,希望 2024 年能接着深入学习使用 NixOS 以及参与社区贡献。 操作系统(ARM64 + RISCV64) 2022 年看了一半《Linux/Unix 系统编程手册 - 上册》,2023 全年没动这本书,2024 年总该看完了吧… MIT6.S081 Operating System Engineering (Fall 2020) 之前定了个目标学一遍但完全没开始,2024 年继续… (低优先级)2024 年有机会的话用 Rust 语言整点活 (低优先级)2024 年在 EE 与嵌入式方面,也希望能更深入些,设计一点自己的板子玩玩,做点有意思的东西,比如无人机啊、智能机械臂啊啥的。 总结下主要就是三个学习目标::搞搞 AIGC、学学操作系统、尝试更深入地使用 NixOS 以及参与社区贡献. 2. 业余生活 生活上,2024 年希望能: 首先仍然是每年固定的目标:每月读一本书、写一篇博客。 新增的旅游目标:带家人至少出省旅游三次。 运动:2023 年意识到了身体健康的重要性,在 2024 年想多多运动,晨跑就是个不错的选择(但可别空腹晨跑,胃炎犯了很难受)。 学学心理学:要往「久病成医」的方向发展了 emmmm. 2024 年想入门个心理学,帮我更好地照顾自己、关心家人、处理人际关系。 音乐:今年买了个 Quest 3 VR 头显,它有个 AR 弹琴的游戏挺不错,希望 2023 年能借此学会些简单的钢琴曲。 3. 工作 这部分于 2024-01-13 新增 其实全文写下来,基本完全没提到我 2023 年的主要工作。主要还是因为前面提到的一些心态问题,以及工作部分相对而言没那么亮眼,所以没提。 但最近年终嘛,回顾了下我 2023 年下半年的工作,主要是在 infra 层面支撑 AIGC 团队,期间开开心心地折腾了挺多 Nvidia、PyTorch 之类的新鲜东西,也拿到了不错的成果。另一方面又了解到工作上 2024 年我会与 AIGC 团队更深入地合作,更深入地折腾 AIGC、Nvidia 生态、Pytorch 相关的东西。目前感觉还是挺有意思的,所以我工作上姑且也给自己定个目标吧:既学学新鲜热门的 AIGC 涨涨见识,又借此更好地支撑 AIGC 团队,Win Win! 结语 2022 年的年终总结文末,我给自己 2023 年的期许是「认识更多有趣的人,见识下更宽广的世界」,感觉确实应验了。由衷感谢在 2023 年给我帮助与支持的亲人、朋友、同事,以及努力探索未知的我自己! 那么在 2024 年,我希望自己能够「工作与业余爱好上稳中求进,生活上锻炼好身体、多关心家人」~ Carpe Diem. Seize The Day, Boys. Make Your Lives Extraordinary. – 《死亡诗社》 文末推荐一个年终回顾与展望的帮助手册,感觉设计得很好:https://yearcompass.com/cn/

2023/12/31
articleCard.readMore

两岸猿声啼不住,轻舟已过万重山——我的四分之一人生

本文稍有点长,推荐配合歌曲《夜空中最亮的星——逃跑计划》食用。我曾在无数个白天夜晚,听着这首歌,想着自己的人生,书写本文时也不例外。 2023 年,按我能长命百岁来计算,我已经走过了四分之一的人生路。 如果要我用一句话总结我过去这四分之一的人生,我想用这句诗再合适不过了: 两岸猿声啼不住,轻舟已过万重山。 我想大部分人前四分之一的人生,主旋律都是求学,我也不例外。 我的求学之路并不顺利,小学初中时我不知道自己想要什么,高中时压力太大几乎退学,大学时我又因为自己的问题无法毕业。但是在工作后我反而逐渐建立起了自信心,就像是突然进入了康庄大道。 最近我又经历了许多,受到了一些启发,觉得到了一个合适的时机,因此写下这篇文章,既是记录我的过去,也同时思考下未来的路该怎么走。 这篇文章主要是写给我自己看的,但如果也能带给你一些启发,那就再好不过了。 我的高中 2015 年,我高三,那时候我是一个科幻迷,并且刚刚经历过一次退学风波。为什么会闹出这样的事情?简单的说就是因为我抗压能力差,高三巨大的压力压得我喘不过气来,我想要逃避。 当时班主任找我谈话,怎么劝都劝不动我,劝不动他就开始骂,把我骂个狗血淋头,这一骂把我给骂醒了,我因此得以坚持到高考结束。至今仍然相当感谢我的班主任王老师,他骂醒了我,让我不至于走上另一条更艰难的路。 但是即使如此,我还是没有任何动力去坚持备考。当时整个班级的学习强度都越来越高,只有我仿佛活在另一个世界。整个高三下学期,只有各种考试跟测验我会认真完成,其他时间我都在看各种与学习无关的书。 我读了很多的书。 我读刘慈欣的《三体》、山田正纪的《艾达》、保罗·巴奇加卢皮的《发条女孩》跟《拆船工》、以及《科幻世界》,我读川端康成的《雪国》跟《千只鹤》、村上春树的《当我谈跑步时我谈些什么》,读卡勒德·胡赛尼的《追风筝的人》、马尔克斯的《百年孤独》,读江南的《龙族》、余秋雨的《中国戏剧史》。因为压力太大,我甚至开始对哲学、禅宗感兴趣,我读了妙莉叶·芭贝里的《刺猬的优雅》、乔斯坦·贾德的《苏菲的世界》,我甚至开始读《心之道》、《学禅方便谭》、《新世界 : 灵性的觉醒》。 读了村上春树的《当我谈跑步时我谈些什么》后,我开始把跑步当成一种宣泄压力的方式,我经常晚自习时偷偷溜去操场跑步。跑了五圈、十圈、二十圈,跑到感觉脚下力重千钧,呼吸火辣辣地痛,这些痛苦与不适,让我忘记了高考的压力。 我还喜欢上了看星星,经常在晚上熄灯后,偷偷爬上宿舍天台,用手机的《星图》APP,寻找各种星宿、寻找牛郎织女、北斗七星、大角星、看可见卫星过境。 我考的是理科,高考结束后,我拿网上的答案随便估了个分,尽量往低了估,算出二本都不够的。但这也没啥,我自认只能做到这个程度,当时的想法就是尽人事,听天命。 然后是浑浑噩噩地等待高考成绩,中间也胡思乱想过一些要不要考虑复读的问题,但最终还是决定不复读了,因为我觉得我不可能再坚持一年了。 或许是因为我心态很平稳(心如死灰),考试时发挥得很好,成绩出来居然超过了一本线十多分。 之后就是填报志愿了,我不知道自己想学什么,可能是电子信息?毕竟我小时候挺喜欢捣鼓各种电子设备。 偶然想起在学校阅览室读杂志时,曾被科幻世界 2013 年 12 期里沖氏武彦的《在回声中重历》给打动——用耳朵“看见”世界实在是太奇妙了,我当时痴痴地幻想了好几天。 这样我开始考虑选择声学。 我从高中同桌推荐的《刀剑神域》开始接触日本的 ACG 文化,后来接触到初音未来和洛天依,就对歌声合成(singing synthesis)产生了很大的兴趣,仔细一想发现这也应该是声学的范畴,这使我坚定了我选择声学的想法。 家里父母都没学问,他们的意见就是听我的,于是我的第一志愿就填了安徽建筑大学的声学专业。 声学是一个非常冷门的专业,我很顺利地被录取了。 就这样,我开始了我错位的大学生活,并为我自己草率的选择付出了巨大的代价。 我的大学 我凭着自己粗浅的想象与一时热血,填报了声学专业,但现实不同于想象,我发现我并不喜欢声学专业。 一开始,到新的学校,我接触到的一切都让我觉得自己就像是刘姥姥进大观园,处处都是新鲜事物——山里的孩子出来,第一次发现大家都只讲普通话,第一次见识到平原的广阔,第一次见识到城市的繁华, 第一次见识到大学的自由。 但渐渐的问题就变得明显了,我发现学校教授的声学课程与我想象的完全不同,教的东西跟声学成像、声音合成几乎毫无关联,而且大学也并没有我想象的那般神圣无垢,它照样存在着各种各样的问题(比如一些形式主义、一些水课)。另一方面我刚进大学时,为了弥补自己高三的遗憾,如同苦行僧一般的学习,而这种状态完全无法持续。 我很快就出了问题,这从我以前写过的文章中可见一斑: 2017-03-07 - 我患上了阅读焦虑症 2017-02-06 - 忽而假末 另外大学期间我迷上了编程,开始自学各种编程技术,最开始是 C,后来有一次学校校友、在美国常春滕读天体物理学博士的季索清学长(现在已经是 Professor of Astrophysics 了)回校演讲,谈到了他们实验室现在都用 Python 了,Matlab 都没人用了,于是我开始学习 Python. 后来又跟着知乎上的推荐学习路线学习过 SICP、Linux C 编程等各种内容。 大学四年,学校对我帮助最大的,一是让我接触到了更大的世界,二是图书馆藏书丰富,尤其是计算机类书籍。四年时间我读了很多很多的书,学校的图书馆成了我最喜欢的地方。 但我始终无法平衡学业与编程,我开始彻底忽略学业。 问题愈演愈烈,大三时我尝试过申请休学,但是又被劝住了。到大四临近毕业时,我心理问题已经相当严重。尤其因为我性格还比较轴,觉得这个世界不应该是这样的,不想学的课程我一个字都不想看,相关的考试我要么缺考要么交白卷。 结果就是我完全无法毕业,觉得前途一片灰暗,见不到任何光亮,甚至感觉自己要得抑郁症,那是我这四分之一人生中最黑暗的时期。 某天跟在深圳工作的初中同学聊天,他说到深圳这方面的公司挺多的,建议我考虑下。 于是我买了张火车票直接就奔去了深圳,还写下了逃离我的大学,现在回想起来,当时的想法大概是,「做什么都好,总之再也不想继续待在这个对我而言暗无天日的地方了。」 在学校的经历让我对学校产生了极大的反感,我拉黑了学校的所有联系方式。这完全是我自己的问题, 我的导员对我很好,我在学校也有过许多美好的回忆,但我就是心理上无法接受再跟这个地方有任何联系。 工作后也有过很多朋友觉得我应该回去把学位拿了,每次我都会回答,如果能做到我也想,但我真的做不到,再在学校呆下去我真的要疯掉了。至今我仍然觉得这是我当初做的最正确的决定。 导员联系不上我,就让同学给我传话,又为我着想给我办理了延期,但后面的一年我也一门网课都没听过,又一年后,我的学历状态就变成了结业。 我的打工人生活 我初中同学是读书不好的那种,高中就直接读了武术学校,在深圳教跆拳道。我跟他一张床挤着睡了三天(很感谢他愿意为我做到这个程度),期间投了几十份简历。 我大学时自学涉猎过 Python Java Linux,因此后端、运维都有投,但只收到一家 Python 自动化运维的面试邀请,面试对我而言难度不高,很轻易就通过了,这家小创业公司也不看学历。不知该说是运气好还是不好,我之后再也没收到过任何面试邀请。 就这样,我开始了我的打工人生活。 我在上大学时虽然自学了许多计算机与编程知识,但是那个时候全是我自己单干,现实中几乎没接触过其他做 IT 的人。另一方面大学读得一团糟,因此刚工作时我相当不自信甚至可以说是自卑。在工作后,我发现周围都是同样做技术的人,这种一起解决技术问题、完成技术目标的感觉是我从未体验过的,另一方面我的工作成果也获得了大家的认可,这让我在工作期间一直非常开心(虽然工资真的相当低,而且加班很严重。毕竟我没学历,当时能找到个工作都谢天谢地了)。 在这第一份正式工作期间,我学会了 Kubernetes、Istio、Docker、阿里云、Terraform/Pulumi、Argo Workflows 等云原生技术,Jenkins 等自动化运维技术,写了很多 Python 代码锻炼了自己的代码能力,还折腾了洋垃圾服务器、组装了公司办公电脑,工作能力也得到了领导同事的认可。 我很感谢这家公司,它是我 IT 生涯的起点,在这里我学到了令我足以在 IT 行业立足的技术,也建立起了技术自信。但它给的工资实在太低了,还加班严重、画饼充饥,很多东西都不规范,处处透露着小作坊的气息,因此在工作了一年零八个月后,我决定离职。 离职后我休息了一个多月,给自己放了一个长假,期间也写了我在创业公司做技术一年多的一点体会 跟 脚踏实地,仰望星空。现在看来,当时这些文章也是写得纯真又幼稚。或许再过几年回头看,我会觉得现在这篇文章也纯真又幼稚?那就再好不过了——我对这个世界的认知又更正确了一点,我得以再次优化指导我行动的「人生模型」,避免在未来因此造成更大的问题(笑 之后我开始找工作。我当时很自信地(其实也有点忐忑)在简历上写下了「本人因学分不足,未能取得学位」,实话说,这句话帮我省去了不少无意义的沟通,大概 50% 的公司在确认了情况后会直接忽略我。即使如此,我仍然拿到好几份 offer,也得以进入现在这家公司,职位是 SRE。 在新公司这几年的经历相当丰富,我简单总结如下: 2021 年: 年初的期许:拆破玉笼飞彩凤,顿开金锁走蛟龙。 工作上我的工资相比之前翻了数倍,工作环境也好了太多。同事中不乏 985 211、海归甚至清北的大佬(同事的 title 可能代表不了什么,但这确实让我很有成就感),跟他们学到了许多东西, 熟悉了全新的工作文化(OKR 等),接触到了拥有数千万日活、十多万 QPS 的云上系统架构,并且完成了其中核心组件 K8s 集群的运维升级工作,获得了许多牛逼同事与领导对我专业能力的认可。 业余生活上被同事带着第一次海边冲浪、烧烤,又开始玩轮滑,还学上了泡茶。 2021 年年终总结 2022 年: 年初的期许:更上一层楼 工作上一是通过了职级晋升,不再是 SRE 萌新了。二是在流量链路上做了很多工作,帮公司省了很多钱,还因此拿了一个 S(公司最高绩效),年终奖也很丰厚。 业余生活上做出了更多的探索,学了很多新技术,认识了很多有趣的人(0xffff 社区),还坚持学了好几个月的英语。 2022 年年终总结 2023 年(虽然还没过完): 年初的期许:认识更多有趣的人,见识下更宽广的世界 今年在工作上没有做出很好的成绩,马马虎虎。我更多的把时间投入到了业余爱好上。 业余生活上,我又折腾了许多新技术(MCU 开发、各种 ARM/RISCV 开发板、Homelab、NixOS), 并且因此认识了许多嵌入式领域的大佬。在折腾 NixOS 的过程中我做的开源项目、入门指南更是获得了国内外社区的大量好评,认识了好几位外国朋友,还收到了一些外国读者的打赏。这完全契合了我年初给自己的期许。 我今年写 NixOS 入门指南的经历 两份工作,四年多的时间,我的经历很难通过上面这只言片语就完全概括,中间当然也有过许多挣扎、迷茫、许多心酸苦辣。但就得到的结果来说,在第一家公司我学到了很多很多,接着从进入新公司开始到现在,我每一年年初给自己的期许,也都能如约兑现。 现在,我相信在深圳这四年只是我上升期的第一步,这一步我完成了自信心的构建、眼界的开拓、基础技术能力的积累,也攒下了能让我衣食无忧好几年的少许财富(就在今天,还给我爸全款买了家里的第一辆小轿车,一家子都很开心)。下一个四年或者五年,我一定能收获更多,就像几年前我第一份工作刚结束,好朋友 @Likenttt 送我的诗一样: 拆破玉笼飞彩凤,顿开金锁走蛟龙。 人生还很长,我想一个阶段的失败,可能只是在提前优化我对世界的认知,帮我提前发现并解决我「人生模型」中隐藏的问题,为下一个阶段的成功做铺垫。 我的未来 我过去的这四分之一人生,很难复刻,其中有太多的莽撞、理想主义让我饱尝苦果,其中的转折点也有许多运气与机遇的成分。但我的未来正是构建在此之上。 有的人喜欢稳定,但当今大世,AI 飞速发展、中美摩擦不断、欧洲也各种难民、党争问题,全世界都在变化,真的有什么绝对稳定的东西可以依靠么?世事无常,纷繁复杂,我能做的,是在接受这份无常的同时,仍然能维持自驱力,在这个世界中探索出自己的一片天地。 最近在苏洋的折腾群,从大家的自我介绍里看到了形形色色的人生,年龄从 20+ 到 50+,学历从高中专科到博士,职业从软硬件到电工、灯光师、全职公益人、见证纸媒体消亡的电脑报前编辑,生活地点也遍布全球。我甚至还发现最近跟我深入交流过 NixOS 相关技术问题的群友,在十多年前就做过我当时使用或者接触过的产品,而那个时候我还在上初中甚至小学,这让我感到十分震撼。其中大部群友的年龄都比我大许多,他们的经历给了我很大的启发,让我意识到我往后 3/4 的人生还有很多的可能。 另外随着我近两年逐渐在自己的业余爱好上有所建树,我也越来越觉得工作作为养家糊口的手段,确实很重要,但它只是生活的一部分,在工作之外我还有许多可以做的事。 我一直在践行「兴趣是最好的老师」,虽然因为太过兴趣驱动以及一些其他原因导致我大学读得比较糟糕,但是能让我达成现在的成就的同样是兴趣,让我最近几个月接触 NixOS 并且获得了众多好评与感谢的同样是兴趣。最近有推友分享了一篇很实用的长文How to Do Great Work - Paul Graham(中译【实用指南】Paul Graham 两万字新文:如何取得杰出成就 ), 我读了个开头,还没看完,但是发觉它很契合我,它与我的经历能相互印证,也对我未来的行动很有指导意义。其中我目前读到印象最深刻的一句话就是: The three most powerful motives are curiosity, delight, and the desire to do something impressive. Sometimes they converge, and that combination is the most powerful of all. 三个最强大的内在动机是好奇心、快乐和做出令人印象深刻的事情的欲望,当它们汇聚在一起时,会成为最强大的组合。 写这篇文章花了我一整天时间,第二天我又做了不少修修补补的工作。写作时我回想了很多的东西,也翻阅了我自己过往的各种日记、随笔,往事历历在目。 我甚至有一点使命感,能感觉到这是一件相当有意义的事情。 文章写完后,我又反复读了好多遍,越读我越喜欢它,觉得它会成为我的一个人生里程碑。这个里程碑不只有纪念意义,它更是对我未来方向的指引。迈过这个里程碑,我对仍旧未知的未来,有了更多的期待。 后记 因着今天发现我认识的网友中就有人在深圳做了多年全职公益人,我想起了去年 8 月份看过的《在峡江的转弯处——陈行甲人生笔记》,作者现在也在深圳做公益。我又把书翻出来略读了一遍,很有些感触。 偶尔回忆起自己当初的自卑、迷茫、挣扎,我会意识到现在的我虽然不再自卑,但仍然会迷茫、挣扎, 怀疑自己的想法是否正确。但我不觉得这是坏事,这正说明我走在了正确的路上。经常会有人说要「走出舒适区」,有这种迷茫、挣扎的感觉,说明我正在这么做。 正因为曾经经历过人生的灰暗时刻,所以我更希望自己能记住,这是一个可爱的世界,这正是我博客域名 thiscute.world 的由来,今后我也会牢记这一点。 其实这段人生是最美好的,以后可能没有这么好的日子了。 —— v2ex 网友的评论,留做警示。最近几年过得一帆风顺,我确实是有点飘了,应该「居安思危」。 文章的最后,我想我应该再次感谢,感谢这一路走来,帮助过我的老师、同学、朋友,认可我工作的同事跟领导,鼓励过我的家人、朋友、网友,感谢你们!没有你们,我可能早就迷失了方向,更不会有现在的成绩了。 四年多前,我从学校不辞而别,我欠我的导员圆圆姐一个道歉,一份感谢,一个交代。这次,我也会一并补上已经补上了: 人是社会性动物,我们互相成就。我今后也会争取交到更多有趣的朋友,认识更多有趣的人,见识这个宽广、可爱的世界。 评论区 我也在其他平台分享了这篇文章,其中评论有些不礼貌的 judge(直接无视掉就好,有的人这辈子也就剩这点东西了),但也不乏好的内容。其中许多的留言相当治愈,让人心里暖暖的,有些留言让我会心一笑,这些内容都让我觉得,能够把文章分享到这些平台,让大家看到,真的是太好了!在跟评论区一些朋友交流时,也碰撞出了许多思想的火花,这也让我相当开心。 为着让大家都能看到其中好的内容,我把它们都列在这里。 0xffff 社区: 0xffff 评论区真的有很多真知灼见,强烈推荐一读! v2ex: 有些很治愈的评论,也有个别不好的,我觉得这些评论都挺有意思。 博客园 关于这些不好的评论(譬如 v2ex 上有人评论我是在「无病呻吟」,还挺多人点赞。另外 Twitter 上有人发推喷我「谁 TM 在意你的人生怎么样」,我觉得都挺好笑的),我想在这里为一些因此不快意的读者解释下。我其实感觉到,在我自己的体系能够自洽后,看待这类评论时我更像是一个旁观者。我甚至完全不觉得这些评论冒犯了我(笑)。这样的深入剖析自我的文章,肯定会刺痛到一些被生活磨去了棱角,迷失了自己的人。这种人别说跟我共情了,他们甚至下意识就要攻击我、反驳我。 以前看过朋友推荐的一本小说,里面有一句话我印象很深刻:「正如纯氧对生物有害,毫无保留的真相,只会把人的精神击溃。一比五(四)的氧与氮,才是可供呼吸的空气。同样,呼吸着以戏言稀释的少量真实,人才能维持健全的心。」 对这种被世界的真相击溃的人,我没啥好说的。以铜为镜,可以正衣冠;以人为镜,可以明得失。这些评论在警示我,不要成为这样的人, 看清世界的真相后,仍要热爱生活。 如果说体系自洽有点不好懂,那我可以用个简单的类比来说明这一点:面对这种品头论足,我觉得我简直是在对牛弹琴。牛的哞哞叫会让我感到不开心么?我有必要跟牛解释我弹的曲子么?它听不懂关我何事?

2023/8/19
articleCard.readMore

为什么我折腾这些小众技术?

我折腾过许多的小众技术,而今年新折腾的主要有 NixOS、窗口管理器 i3 / hyprland、以及 Neovim,其中 NixOS 我甚至折腾到了一个新境界——出了一本帮助新手入门的中英双语开源书籍nixos-and-flakes-book,还搞了好几个 NixOS 相关的开源项目(比如nix-darwin-kickstarter 跟ryan4yin/nix-config),都收到了许多好评。 结合我自己折腾这些小众技术的经历,以及我经常被问到的问题(为什么你选择用NixOS / Neovim /小鹤音形中文输入法?它有什么好处?它真的能提升效率吗?等等),我想在这里简单谈谈我对它们的看法。 什么是小众技术? 小众,是相对于大众而言的。小众技术,指在该领域中用户占比较相对较小的技术。 基于这样的定义,我可以列举出我接触过的不同领域的一些小众技术: 领域 小众技术 大众技术 编辑器 Neovim、Emacs VSCode、PyCharm、IDEA 中文输入方案 双拼、小鹤音形、五笔、二笔、郑码、灵形速影 智能拼音 Linux 操作系统 NixOS、Gentoo、Arch Linux Ubuntu、Fedora 窗口管理器 i3、hyprland KDE、GNOME 大多数人在使用这些领域的技术时,都会选择大众技术,因为它们的入门门槛低,使用起来也比较方便。我曾经也是这大多数人之一,但是我渐渐发现,这些小众技术也有它们的优势,所以我开始尝试使用它们,并逐渐过渡到了它们。 这些小众技术有什么特点? 小众技术显然得拥有一些优势,才能吸引到一部分用户,让这些用户选择它们而不是大众技术。 从我个人的使用经验来看,我用过的这些小众技术,具有一些比较明显的共同特征。 首先是它们共同的劣势:入门门槛更高,入门阶段需要花费更多的时间去学习、熟悉。 这就过滤掉了大部分用户,只有那些喜欢折腾、喜欢挑战的人才会去尝试这些小众技术。 比如说五笔输入法,它们的入门门槛很高,需要花费大量的时间去记忆它的键位编排、去练习,前期的输入体验会跌到谷底。要想达到你曾经智能拼音的输入速度,感觉至少得每天练习 1 个小时,持续一个月(这很可能还不够)。 其他形码输入法也是一样,我用的小鹤音形算是一个折衷的选择,它的入门门槛比五笔低一些,学会后也能获得类似五笔的输入体验。 再说说它们共同的优势: 定制程度高:用户可以根据自己的需求,自由地定制各种功能。 强烈的掌控感、绝佳的使用体验:高度的自定义,让用户感觉到自己在使用这些技术的过程中,能够完全掌控一切,从而带来绝佳的使用体验。 用户黏性高、社区活跃:用户在使用这些技术的过程中,会不断地去探索、去学习、去定制, 这会让用户对它们产生强烈的归属感。 也因为上面这些原因,用户一旦成功入门某项小众技术(比如说形码输入法、Neovim/Emacs 编辑器),就很难再退回到曾经的大众方案——他们会发现曾经的大众方案用起来,各种不顺手、不爽快。 我为什么折腾这些小众技术? 我折腾过许多小众技术,而原因中最大的一部分,应该是好奇心。但好奇心只能让我去尝试,让我留下来的,是它们优秀的使用体验。 比如说最近折腾的 Neovim 编辑器、Hyprland 窗口管理器,让我留下来继续使用它们的原因,一是 Neovim 跟 Hyprland 配置好了之后,真的很漂亮!而且 Neovim 速度真的超快、太快了!一些从没深度体验过 Neovim 的 VSCode / IDEA 用户可能会觉得这种快不过如此,但是一旦你真的体验过,就会发现这种快真的很爽,就像流浪地球 2 中图恒宇的感叹一样(550W 太快了!这速度太快了!) 二是实际入门后,发现它们用起来很爽快,基于键盘的交互,能带给我形码输入法的那种掌控感、流畅感(优雅,太优雅了 hhh)。 我的 NixOS + Hyprland 桌面 我的 Neovim 编辑器 而我折腾并且爱上 NixOS,也是基于类似的原因。拥有声明式、可复现(一致的运行环境)、OS as Code 等这些特点的 NixOS,对于本运维狗而言,真就是理想中的样子,这让我迫不及待地想要使用它,即使发现了问题也希望能尽快完善它,使它能够适用于更多的场景。 前两天在 4chan 上看到某外国网友的这么一段评论(虽然言词有点偏激,但我还真有点认同…): Completely and utterly unacceptable. Imagine having a tool that can’t even properly undo an operation and then relying on it to manage an operating system.apt, pip, pm, rpm, pacman, whatever are all a mad fucking joke. 小众工具或技术能提升效率吗? 有许多人说,Neovim 编辑器、i3 窗口管理器、形码输入法等这些小众工具或技术,能提升效率,我觉得这是一个误区。相反,其中许多工具或技术,实际上是一个时间销金窟,你会被自己的兴趣驱使着去不断探索它们的边界、调整它的配置使其更契合自己的需求。这导致至少前面较长一段上升期,这些投入的时间会比你效率提升所省下的时间多得多。 所以说到底,想用这些技术来提升效率啥的还是不用想了。它能提升你的效率,但是比较有限,除非你写代码/文档的效率是受限于你的手速 emmm 当然也有些特殊场景,比如说有的人需要经常输入些生僻字,这时候智能拼音就比较鸡肋了,五笔等形码输入法就确实能大大提升输入效率。 或者有人会说,完全熟悉后,vim/emacs 能使你更容易进入心流状态?这个也很难说吧。 那折腾这些东西,到底有什么好处? 如果从很功利的角度看的话,确实就没啥好处,就跟打游戏一样,单纯在消遣时光而已。 要说跟做些无聊的事消遣时光有啥区别的话,大概就是还确实能获得点有用的东西。比如我,遇到 AstroNvim 的 bug ,会提 PR 给上游仓库。发现 NixOS 的文档很糟糕,我直接自己写文档并分享出来。发现 NixOS 缺少对我手头某块开发板的支持,我会自己尝试移植。啥时候发现某工具缺少自己想要的功能,我也可能直接自己写一个。 这些折腾过程中获得的经验、创建的开源项目、在上游仓库中留下的 PR 、在社区中收获的感谢,感觉都是有价值的。它不一定有啥业务价值,但是它好玩啊,还能交到朋友,帮到别人,在开源社区留下自己的痕迹,这不是很有意思么? Linus 最开始写 Linux, 也只是为了好玩(Just For Fun). 结语 你展望人生的时候,不可能把这些点连起来;只有当你回顾人生的时候,才能发现它们之间的联系。所以你必须有信心,相信这些点总会以某种方式,对你的未来产生影响。你必须相信一些事情——你的勇气、命运、人生、缘分等等。这样做从未令我失望,反而决定了我人生中所有与众不同之处。 Stay Hungry. Stay Foolish. ——You’ve got to find what you love, by Steve Jobs, CEO of Apple Computer 评论区 文末附上来自其他论坛的评论,其中不乏一些有趣的观点: 0xffff.one: https://0xffff.one/d/1595-wei-shen-me-wo-zhe-teng-zhei-xie v2ex: https://www.v2ex.com/t/961562

2023/8/1
articleCard.readMore

MacOS 窗口管理器 yabai 玩耍笔记

2024-08-30 更新:我已经用 aerospace 替代了 yabai + shkd,体验好了太多,而且也不用关掉 SIP 啥的,强烈推荐一用。 2024-01-28 更新:换了新 Macbook Pro 之后,我又重新把 yabai 装上了,目前体验还不错,比 23 年好了不少。另外我新的配置完全基于 nix-darwin 部署,内容也有些改动,有兴趣的可以看看:ryan4yin/nix-config/darwin/wm 附上 nix-darwin 新手起步模板ryan4yin/nix-darwin-kickstarter 在 Linux 上用了一段时间 i3wm 后,我就有点忍受不了工作电脑的桌面环境了,公司给配的是 Macbook Pro 2020,一番查找发现 yabai 比较符合我的需求,于是开始了折腾之旅。 使用体验总结 我的电脑配置为 Macbook Pro 2020,i5 + 16G RAM + 512G Disk,性能尚可。 一句话总结:体验还不错,但是还不太成熟,Bug 比较多,而且有点吃性能,安装 yabai 后偶尔就会卡顿一下。 自动分屏 + 快捷键自动调整窗口的体验还是很舒服的,劝退我的主要是如下这些问题: 对有些软件,比如企业微信、微信、QQ,自动分屏功能不太行,会出现窗口错位。 如下两个问题逼着我一会儿进入全屏模式,一会儿又要退出全屏,简直离谱。 全屏下 Chrome 搜索框下方的提示栏被会 Chrome 本身遮挡,必须退出全屏功能才能看到。 非全屏下,Chrome 页面中的输入框「自动填充」功能会被 Chrome 遮挡,必须进入全屏模式才能看到… 在右键修改 Firefox Bookmark 中标签时,弹出的修改菜单会被 Bookmark 收藏夹本身的弹窗遮挡,导致有些选项无法点击到。 开始使用 yabai 后,系统经常性地卡顿,或者风扇狂转,说明这玩意儿有点吃性能。 安装流程 首先参考这篇官方 WikiDisabling System Integrity Protection 关闭 SIP,然后参照如下流程安装 yabai 与 skhd。 shell # 安装yabai brew install koekeishiya/formulae/yabai sudo yabai --install-sa # 启动yabai 这时候需要授权辅助功能 brew services start yabai # 安装skhd brew install koekeishiya/formulae/skhd # 启动skhd 这时候需要授权辅助功能 brew services start skhd ########### 为 yabai 添加 sudo 权限 ########### sudo yabai --load-sa sudo visudo -f /private/etc/sudoers.d/yabai # 然后输入以下内容 其中 <user> 修改为当前 mac 的用户名 # input the line below into the file you are editing. # replace <yabai> with the path to the yabai binary (output of: which yabai). # replace <user> with your username (output of: whoami). # replace <hash> with the sha256 hash of the yabai binary (output of: shasum -a 256 $(which yabai)). # this hash must be updated manually after running brew upgrade. <user> ALL=(root) NOPASSWD: sha256:<hash> <yabai> --load-sa 上面就完成了安装流程,但是到这里还不能使用,还需要为 skhd 与 yabai 添加配置文件,并添加自定义配置。 shell # 创建yabai配置文件 touch ~/.yabairc chmod +x ~/.yabairc # 创建skhd配置文件 touch ~/.skhdrc chmod +x ~/.skhdrc # 之后在 ~/.yabairc 中添加以下命令 cat <<EOF > ~/.yabairc #!/usr/bin/env sh # wiki 要求在配置最前面加这个,看起来是跟 sudo 权限相关的东西 sudo yabai --load-sa yabai -m signal --add event=dock_did_restart action="sudo yabai --load-sa" EOF 自定义 skhd 与 yabai 配置 这里配置的目标是,尽量与 i3wm 的默认快捷键保持一致,因为我在家用的是 Linux,只有办公电脑是 Mac. 我目前的 ~/.yabairc,它用于配置 yabai 的各种行为: shell #!/usr/bin/env sh # wiki 要求在配置最前面加这个,看起来是跟 sudo 权限相关的东西 sudo yabai --load-sa yabai -m signal --add event=dock_did_restart action="sudo yabai --load-sa" ## 输出 debug 日志,出问题时方便排查 yabai -m config debug_output on # 窗口平铺 yabai -m space --layout bsp # 默认拆分规则 first_child second_child yabai -m config window_placement second_child # 窗口间距设置 yabai -m config top_padding 10 yabai -m config bottom_padding 10 yabai -m config left_padding 10 yabai -m config right_padding 10 yabai -m config window_gap 10 # 自动平衡所有窗口始终占据相同的空间 yabai -m config auto_balance off # 如果禁用自动平衡,此项属性定义的是新窗口占用的空间量。0.5意为旧窗口占用50% yabai -m config split_ratio 0.50 # 鼠标修饰键 意思就是按着这个键就可以使用鼠标单独修改窗口大小了 yabai -m config mouse_modifier ctrl # ctrl + 鼠标左键 移动窗口 yabai -m config mouse_action1 move # ctrl + 鼠标右键 调整窗口大小 yabai -m config mouse_action2 resize # 焦点跟随鼠标 默认off: 关闭 autoraise:自动提升 autofocus: 自动对焦 yabai -m config focus_follows_mouse autofocus # 设置鼠标是否跟随当前活动窗口 默认 off: 关闭 on: 开启 yabai -m config mouse_follows_focus on # 浮动窗口问题在顶部 yabai -m config window_topmost on # 修改窗口阴影 on: 打开 off: 关闭 float: 只显示浮动窗口的阴影 yabai -m config window_shadow float # 窗口透明度设置 yabai -m config window_opacity on # 配置活动窗口不透明度 yabai -m config active_window_opacity 0.98 yabai -m config normal_window_opacity 0.9 yabai -m config window_opacity_duration 0.0 # 在所有显示器上的每个空间顶部添加 0 填充 底部添加 0 填充 yabai -m config external_bar all:0:0 # ================================ 规则 ================================ # 打开系统偏好设置,不使用平铺模式 yabai -m rule --add app="^系统偏好设置$" manage=off yabai -m rule --add app="^提醒事项$" manage=off yabai -m rule --add app="^关于本机$" manage=off echo "yabai configuration loaded.." 再就是 ~/.skhdrc,它负责配置各种快捷键,如下是我的配置: shell # 配置语法 : <modifier> - <key> : <command> # modifier 可以是单个键比如 cmd, alt, ctrl, 也可以是组合键比如 ctrl + shift, ctrl + alt # ================================ 打开终端 ================================ # 启动终端 cmd - return : open -a iTerm # 关闭当前窗口,这个不需要加,macOS 默认是 cmd + q,我 Linux 也这么设置的 # ================================ 窗口设置 ================================ # =============== 为了避免快捷键冲突改用了 ctrl 作为 modifier ================= # ctrl + e 切换为平铺模式 ctrl - e : yabai -m space --layout bsp # ctrl + s 切换为堆叠模式 ctrl - s : yabai -m space --layout stack # 浮动/不浮动窗口 float ctrl - f : yabai -m window --toggle float # ================================ 多桌面配置 ================================ # 创建一个新桌面,并把当前活动的窗口发送到新桌面,并且自动跳转到新桌面. 需要 jq 支持 brew install jq shift + cmd - n : yabai -m space --create && index="$(yabai -m query --spaces --display | jq '.| length')" && yabai -m window --space "${index}" && yabai -m space --focus "${index}" && yabai -m space --layout bsp # 在 stack 模式下通过方向键切换窗口 ctrl - down : yabai -m window --focus stack.next || yabai -m window --focus south ctrl - up : yabai -m window --focus stack.prev || yabai -m window --focus north # 在 bsp 模式下通过方向键切换窗口 cmd - left : yabai -m window --focus west cmd - right : yabai -m window --focus east # 在 9 个桌面之间切换 ctrl - 1 : yabai -m space --focus 1 ctrl - 2 : yabai -m space --focus 2 ctrl - 3 : yabai -m space --focus 3 ctrl - 4 : yabai -m space --focus 4 ctrl - 5 : yabai -m space --focus 5 ctrl - 6 : yabai -m space --focus 6 ctrl - 7 : yabai -m space --focus 7 ctrl - 8 : yabai -m space --focus 8 ctrl - 9 : yabai -m space --focus 9 # 将窗口发送到某个其他桌面 ctrl + shift - 1 : yabai -m window --space 1 ctrl + shift - 2 : yabai -m window --space 2 ctrl + shift - 3 : yabai -m window --space 3 ctrl + shift - 4 : yabai -m window --space 4 ctrl + shift - 5 : yabai -m window --space 5 ctrl + shift - 6 : yabai -m window --space 6 ctrl + shift - 7 : yabai -m window --space 7 ctrl + shift - 8 : yabai -m window --space 8 ctrl + shift - 9 : yabai -m window --space 9 配置加好后重启 yabai 与 skhd: shell brew services restart yabai brew services restart skhd 现在就可以随便打开几个程序试试,正常情况下 yabai 会自动帮你分屏。再尝试下添加好的这些快捷键,看看是否生效。 问题排查 1. yabai 如果 yabai 配置没有生效,有可能是权限问题,可以试下这个命令重启 yabai: shell sudo yabai --uninstall-sa; sudo yabai --load-sa; brew services restart yabai 其他问题可查看 yabai 的日志解决: 错误日志路径: /usr/local/var/log/yabai/yabai.err.log 普通日志路径: /usr/local/var/log/yabai/yabai.out.log 2. skhd 如果 skhd 配置没有生效,首先可以查看 skhd 的日志: shell cat /usr/local/var/log/skhd/*.log 如果日志文件不存在,可以停止 skhd 服务并手动启动它,看看是否有输出报错: shell brew services stop skhd skhd -c ~/.skhdrc 比如我之前改错了配置,执行上述命令就会报错: text #27:7 expected modifier 提示我配置的第 27 行配置有问题,我就去看了下,发现是我把 cmd - return 写成了cmd + return,改正后再 brew services start skhd 重启 skhd 就好了。 堆叠模式下的可视化 yabai 在堆叠模式下的可视化效果不是很好,可以使用stackline 来改善一下。 shell # stackline 依赖 hammerspoon,这是一个 macOS 桌面自动化工具 brew install hammerspoon --cask # 现在将 stackline 安装到 hammerspoon 的配置目录中 git clone https://github.com/AdamWagner/stackline.git ~/.hammerspoon/stackline # Make stackline run when hammerspoon launches cd ~/.hammerspoon echo 'stackline = require "stackline"' >> init.lua echo 'stackline:init()' >> init.lua 现在还需要安装下 hammerspoon 的命令行工具 hs,它用于在脚本中执行 stackline 操作,安装方法如下: 首先搜索打开 Hamerspoon 程序,或者使用命令 open -a "Hammerspoon" 这里启动时会申请权限,需要手动打开下 同时注意勾选登录时自动启动 在下方的命令输出栏中键入 hs.ipc.cliInstall() 再回车,即可完成安装 现在确认下 hs 命令已经可用: shell which hs 使用时的常见问题与解决方法 Chrome/WeChat 等程序的弹窗无法显示: 尝试下进入全屏或者退出全屏,总有一种场景下可以显示弹窗… … 参考 mac 下的平铺桌面 yabai 使用 - 月青悠 Yabai setup for i3wm users - Krever

2023/5/22
articleCard.readMore

NixOS 与 Nix Flakes 新手入门

随着文章的更新,文章内容逐渐增多,为了方便阅读,文章内容已经迁移到单独的站点: 文档站: https://nixos-and-flakes.thiscute.world/zh/ GitHub: https://github.com/ryan4yin/nixos-and-flakes-book 非常感谢Reddit、文章评论区、V2EX 以及0xffff.one 等平台上各位朋友的反馈、批评与建议 ❤️

2023/5/4
articleCard.readMore

Linux 上的 WireGuard 网络分析(一)

阅读此文章需要前置知识:Linux 网络基础知识、iptables、conntrack 本文内容部分采用了 Copilot 提示内容,也有部分内容用了 ChatGPT 免费版进行分析,确实都比较有帮助。 最近因为工作需要研究了一波 WireGuard 协议,在这篇文章中简单记录下心得。 WireGuard 是什么 WireGuard 是极简主义思想下的 VPN 实现,解决了很多现存 VPN 协议存在的问题。它于 2015 年由 Jason A. Donenfeld 设计实现,因其代码实现简洁易懂、配置简单、性能高、安全强度高而受到广泛关注。 WireGuard 在 2020 年初进入 Linux 主线分支,随后成为 Linux 5.6 的一个内核模块,这之后很快就涌现出许多基于 WireGuard 的开源项目与相关企业,各大老牌 VPN 服务商也逐渐开始支持 WireGuard 协议,很多企业也使用它来组建企业 VPN 网络。 基于 WireGuard 的明星开源项目举例: tailscale: 一套简单易用的 WireGuard VPN 私有网络解决方案,强烈推荐! headscale: tailscale 控制服务器的开源实现,使你可以自建 tailscale 服务。 kilo: 基于 WireGuard 的 Kubernetes 多云网络解决方案。 … 除了上面这些,还有很多其他 WireGuard 项目,有兴趣可以去awesome-wireguard 仓库看看。 WireGuard 本身只是一个点对点隧道协议,只提供点对点通信的能力(这也是其极简主义思想的体现)。而其他网络路由、NAT 穿越、DNS 解析、防火墙策略等功能都是基于 Linux 系统的现有工具来实现的。 在这篇文章里,我将搭建一个简单的单服务器 + 单客户端 WireGuard 网络,然后分析它如何使用 Linux 系统现有的工具,在 WireGuard 隧道上搭建出一个安全可靠的虚拟网络。 文章测试用到的服务器与客户端均为虚拟机,使用 Ubuntu 20.04 系统,内核版本为 5.15,也就是说都包含了 wireguard 内核模块。 WireGuard 服务端网络分析 简单起见,这里使用 docker-compose 启动一个 WireGuard 服务端,使用的镜像是linuxserver/docker-wireguard。 配置文件如下,内容完全参考自此镜像的官方 README: yaml --- version: "2.1" services: wireguard: image: lscr.io/linuxserver/wireguard:latest container_name: wireguard cap_add: - NET_ADMIN - SYS_MODULE environment: - PUID=1000 - PGID=1000 - TZ=Etc/UTC - SERVERURL=auto # 自动确定服务器的外部 IP 地址,在生成客户端配置时会用到 - SERVERPORT=51820 # 服务端监听的端口号 - PEERS=1 # 自动生成 1 个客户端配置 - PEERDNS=auto # 自动确定客户端的 DNS 服务器地址,同样是在生成客户端配置时会用到 - INTERNAL_SUBNET=10.13.13.0 # WireGuard 虚拟网络的网段 - ALLOWEDIPS=0.0.0.0/0 # 这条规则表示允许虚拟网络内的所有客户端将流量发送到此节点 # 众所周知,NAT 网络需要定期发送心跳包来保持 NAT 表内容不过期,俗称连接保活。 # 这里设置为 all 表示所有客户端都开启连接保活。 - PERSISTENTKEEPALIVE_PEERS=all - LOG_CONFS=true # 开启日志 volumes: - ./config:/config - /lib/modules:/lib/modules # 将宿主机的内核模块挂载到容器内,用于加载 WireGuard 内核模块 ports: - 51820:51820/udp sysctls: - net.ipv4.conf.all.src_valid_mark=1 restart: unless-stopped 将上面的配置文件保存为 docker-compose.yml,然后通过如下命令后台启动 WireGuard 服务端: shell docker-compose up -d WireGuard 服务端启动好了,现在查看下服务端容器的日志(我加了详细注释说明): shell $ docker logs wireguard # ...省略若干内容 .:53 # 这几行日志是启动 CoreDNS,为虚拟网络提供默认的 DNS 服务 CoreDNS-1.10.1 # 实际上 CoreDNS 不是必须的,客户端可以改用其他 DNS 服务器 linux/amd64, go1.20, 055b2c3 [#] ip link add wg0 type wireguard # 创建一个 wireguard 设备 [#] wg setconf wg0 /dev/fd/63 # 设置 wireguard 设备的配置 [#] ip -4 address add 10.13.13.1 dev wg0 # 为 wireguard 设备添加一个 ip 地址 [#] ip link set mtu 1420 up dev wg0 # 设置 wireguard 设备的 mtu [#] ip -4 route add 10.13.13.2/32 dev wg0 # 为 wireguard peer1 添加路由,其地址来自 wireguard 配置的 `allowedIPs` 参数 # 下面这几条 iptables 命令为 wireguard 设备添加 NAT 规则,使其成为 WireGuard 虚拟网络的默认网关 # 并使虚拟网络内的其他 peers 能通过此默认网关访问外部网络。 [#] iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE [ls.io-init] done. 通过日志能看到,程序首先创建了 WireGuard 设备 wg0 并绑定了地址 10.13.13.1。作为 WireGuard 网络中的服务端,它所创建的这个 wg0 的任务是成为整个 WireGuard 虚拟网络的默认网关,处理来自虚拟网络内的其他 peers 的流量,构成一个星型网络。 然后服务端为它所生成的 peer1 添加了一个路由,使得 peer1 的流量能够被正确路由到 wg0 设备上。 最后为了让 WireGuard 虚拟网络内的其他 peers 的流量能够通过 wg0 设备访问外部网络或者互相访问,服务端为 wg0 设备添加了如下的 iptables 规则: iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT;:允许进出 wg0 设备的数据包通过 netfilter 的 FORWARD 链(默认规则是 DROP,即默认是不允许通过的) iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE:在 eth+ 网卡上添加 MASQUERADE 规则,即将数据包的源地址伪装成 eth+ 网卡的地址,目的是为了允许 wireguard 的数据包通过 NAT 访问外部网络。 而回来的流量会被 NAT 的 conntrack 链接追踪规则自动允许通过,不过 conntrack 表有自动清理机制,长时间没流量的话会被从 conntrack 表中移除。这就是前面 docker-compose.yml 中的 PERSISTENTKEEPALIVE_PEERS=all 参数解决的问题通过定期发送心跳包来保持 conntrack 表中的连接信息。 这里还涉及到了 NAT 穿越相关内容,就不多展开了,感兴趣的可以自行了解。 WireGuard 的实现中还有一个比较重要的概念叫做 AllowedIPs,它是一个 IP 地址列表,表示允许哪些 IP 地址的流量通过 WireGuard 虚拟网络。为了详细说明这一点,我们先看下服务端配置文件夹中 wg0 的配置: shell $ cat wg0.conf [Interface] Address = 10.13.13.1 ListenPort = 51820 PrivateKey = kGZzt/CU2MVgq19ffXB2YMDSr6WIhlkdlL1MOeGH700= # wg0 隧道启动后添加 iptables 规则 PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE # wg0 隧道停止后删除前面添加的 iptables 规则 PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth+ -j MASQUERADE [Peer] # peer1 PublicKey = HR8Kp3xWIt2rNdS3aaCk+Ss7yQqC9cn6h3WS6UK3WE0= PresharedKey = 7mCNCZdMKeRz1Zrpl9bFS08jJAdv6/USazRVq7tjznY= # AllowedIPs 设置为 peer1 的虚拟 IP 地址,表示允许 peer1 的流量通过 WireGuard 虚拟网络 AllowedIPs = 10.13.13.2/32 AllowedIPs 实际就是每个 peer 在服务端路由表中的 ip 地址,它既可以是 ip 也可以是网段,而且能设置多个,这使所有 peer 都可以负责一个甚至多个 ip 段的转发,也就是充当局域网的路由器——VPN 子路由。 WireGuard 本身只是一个点对点隧道协议,它非常通用。通过 AllowedIPs 参数,我们就能在每个 peer 上添加各 peers 的配置与不同的路由规则,构建出各种复杂的网络拓扑,比如星型、环型、树型等等。 WireGuard 客户端网络分析 现在换台虚拟机跑 WireGuard 客户端,首先需要安装 wireguard 命令行工具: shell sudo apt install wireguard resolvconf 第二步是从服务端的配置文件夹中找到 peer1/peer1.conf,它是服务端容器根据参数 PEERS=1 自动生成的客户端配置文件,先确认下它的内容: shell $ cd ./config/peer1 $ cat peer1.conf [Interface] Address = 10.13.13.2 PrivateKey = +GLDb5QQOHQ2QKWvuFS/4FiWpnivaxzwlm0QmFJIHV8= ListenPort = 51820 DNS = 10.13.13.1 [Peer] PublicKey = t95vF4b11RLCId3ArVVIJoC5Ih9CNbI0VTNuDuEzZyw= PresharedKey = 7mCNCZdMKeRz1Zrpl9bFS08jJAdv6/USazRVq7tjznY= # 需要注意的是这个 Peer Endpoint 的 IP 是否正确 Endpoint = 192.168.5.198:51820 AllowedIPs = 0.0.0.0/0 插入下,这个 Endpoint 的地址也很值得一说,能看到服务端 wg0.conf 的配置中,peer1 并未被设置任何 Endpoint,这实质是表示这个 peer1 的 Endpoint 是动态的,也就是说每次 peer1 发送数据到服务端 wg0 时,服务端通过认证加密技术认证了数据后,就会以数据包的来源 IP 地址作为 peer1 的 Endpoint,这样 peer1 就可以随意更换自己的 IP 地址(Roaming),而 WireGuard 隧道仍然能正常工作(IP 频繁更换的一个典型场景就是手机的网络漫游与 WiFi 切换)。这使 WireGuard 具备了比较明显的无连接特性,也就是说 WireGuard 隧道不需要保持一个什么连接,切换网络也不需要重连,只要数据包能够到达服务端,就能够正常工作。 因为我这里是内网环境测试,配置文件中的 Peer - Endpoint 的 IP 地址直接用服务端的内网 IP 地址就行,也就是 192.168.5.198。 如果你的服务端有公网 IP 地址(比如是云服务器,或者通过端口映射用家庭宽带的动态公网 IP),这个 Endpoint 地址也可以使用该公网 IP 地址,效果是一样的。 配置文件确认无误后,将该配置文件保存到客户端的 /etc/wireguard/peer1.conf 这个路径下,然后使用如下命令启动 WireGuard 客户端: shell sudo wg-quick up peer1 上述命令会自动在 /etc/wireguard/ 目录下找到名为 peer1.conf 的配置文件,然后根据其内容启动一个名为 peer1 的 WireGuard 设备并完成对应配置。 我启动时的日志如下,wg-quick 打印出了它执行的所有网络相关指令(我添加了详细的注释): shell $ sudo wg-quick up peer1 [#] ip link add peer1 type wireguard # 创建一个名为 peer1 的 WireGuard 设备 [#] wg setconf peer1 /dev/fd/63 # 设置 peer1 设备的配置 [#] ip -4 address add 10.13.13.2 dev peer1 # 设置 peer1 设备的 IP 地址 [#] ip link set mtu 1420 up dev peer1 # 设置 peer1 设备的 MTU [#] resolvconf -a tun.peer1 -m 0 -x # 设置 peer1 设备的 DNS,确保 DNS 能够正常工作 [#] wg set peer1 fwmark 51820 # 将 peer1 设备的防火墙标记设为 51820,用于标记 WireGuard 出站流量 # 在后面的路由策略中会使用该标记使 WireGuard 出站流量走默认路由表 [#] ip -4 route add 0.0.0.0/0 dev peer1 table 51820 # 创建单独的路由表 51820,默认将所有流量转发到 peer1 接口 [#] ip -4 rule add not fwmark 51820 table 51820 # 所有不带 51820 标记的流量(普通流量),都转发到前面新建的路由表 51820 # 也就是所有普通流量都转发到 peer1 接口 [#] ip -4 rule add table main suppress_prefixlength 0 # 流量全都走 main 路由表(即默认路由表),但是排除掉前缀长度(掩码) <= 0 的流量 # 掩码 <= 0 的只有 0.0.0.0/0,即默认路由。所以意思是所有非默认路由策略的流量都走 main 路由表 [#] sysctl -q net.ipv4.conf.all.src_valid_mark=1 # 启用源地址有效性检查,用于防止伪造源地址 [#] nft -f /dev/fd/63 # 配置 nftables 规则,用于确保 WireGuard 流量能正确路由,并防止恶意数据包进入网络 跑完后我们现在确认下状态,应该是能正常走 WireGuard 访问相关网络了,可以 WireShark 抓个包确认下。 如果网络不通,那肯定是中间哪一步配置有问题,可以根据上面的日志一步步排查网络接口、路由表、路由策略、iptables/nftables 的配置,必要时可以通过 WireShark 抓包定位。 现在再检查下系统的网络状态,首先检查下路由表,会发现路由表没任何变化: shell $ ip route ls default via 192.168.5.201 dev eth0 proto static 192.168.5.0/24 dev eth0 proto kernel scope link src 192.168.5.197 但是我们的 WireGuard 隧道已经生效了,这就说明现在我们的流量已经不是直接走上面这个默认路由表了,还有其他配置在起作用。往回看看前面的客户端启动日志,其中显示 wg-quick 创建了一个名为 51820 的路由表,我们来检查下这个表: shell ryan@ubuntu-2004-builder:~$ ip route ls table 51820 default dev peer1 scope link 能看到这个表确实是将所有流量都转发到了 WireGuard 的 peer1 接口,基本能确认现在流量都走了这个路由表。那么问题来了,系统的流量是如何被转发到这个路由表的呢?为什么默认的路由表现在不生效了? 要理清这个问题,需要补充点知识——Linux 从 2.2 开始支持了多路由表,并通过路由策略数据库来为每个数据包选择正确的路由表,这个路由策略数据库可以通过 ip rule 命令来查看、修改。 前置知识补充完毕,现在来看下系统当前的路由策略,同样我已经补充好了注释: shell $ ip rule show 0: from all lookup local # 0 是最高优先级,`all` 表示所有流量,`lookup local` 表示查找 local 路由表。 # local 是一个特殊路由表,包含对本地和广播地址的优先级控制路由。 32764: from all lookup main suppress_prefixlength 0 # 32764 目前是第二优先级,将所有流量路由到 main 路由表,但是排除掉默认路由(前缀/掩码 <= 0) # 功能是让所有非默认路由的流量都走 main 路由表 # 这条规则前面实际解释过了,它是 wg-quick 在启动隧道时添加的规则。 32765: not from all fwmark 0xca6c lookup 51820 # 所有不带 0xca6c 标记(51820 的 16 进制格式)的流量(普通流量),都走 51820 路由表 # 也就是都转发到 WireGuard peer1 接口。 # 这条规则是前面的 `ip -4 rule add not fwmark 51820 table 51820` 命令添加的。 # 而它所匹配的防火墙标记则是由前面的 `wg set peer1 fwmark 51820` 命令设置的。 32766: from all lookup main # 所有流量都走 main 路由表,当前是不生效状态,因为前面的规则优先级更高。 # main 是系统的默认路由表,通常我们使用 ip route 命令都是在这个表上操作。 32767: from all lookup default # 所有流量都走 default 路由表,当前同样是不生效状态。 # default 是一个系统生成的兜底路由表,默认不包含任何路由规则,可用于自定义路由策略,也可删除。 结合注释看完上面的路由策略,现在你应该理清楚 WireGuard 的路由规则了,它加了条比默认路由策略 32766 优先级更高的路由策略 32765,将所有普通流量都通过它的自定义路由表路由到 peer1 接口。另一方面 peer1 接口在前面已经被打了 fwmark 标记 51820 也就是 16 进制的 0xca6c,所以 peer1 出站到服务端的流量不会被 32765 匹配到,所以会走优先级更低的 32766 策略,也就是走了 main 路由表。 另外 32764 这条路由策略有点特殊,这里也简单解释下,此策略在前面注释中已经做了解释——是让所有非默认路由的流量都走 main 路由表,而 main 路由表中的非默认路由一般都是其他程序自动管理添加的,或者是我们手动添加的,所以这条规则其实就是确保这些路由策略仍然有效,避免 WireGuard 策略把它们覆盖掉而导致问题。 前面都分析完了,现在还剩下 wg-quick 日志的最后一行 nft -f /dev/fd/63,它到底做了什么呢? nft 是 nftables 的命令行工具名称,所以它实际是设置了一些 nftables 规则,我们查看下它的规则内容: 注意:nftables 的这些 chain 名称是完全自定义的,没啥特殊意义 shell $ sudo nft list ruleset table ip wg-quick-peer1 { chain preraw { type filter hook prerouting priority raw; policy accept; iifname != "peer1" ip daddr 10.13.13.2 fib saddr type != local drop } chain premangle { type filter hook prerouting priority mangle; policy accept; meta l4proto udp meta mark set ct mark } chain postmangle { type filter hook postrouting priority mangle; policy accept; meta l4proto udp meta mark 0x0000ca6c ct mark set meta mark } } 可以看到这里是创建了一个 wg-quick-peer1 表,通过该表在 netfilter 上设置了如下规则: preraw 链:此链用于防止恶意数据包进入网络。 type 开头的一行是规则的类型,这里是 filter,仅匹配了 raw 链的 prerouting 表。 它丢弃掉所有来源接口不是 peer1、目的地址是 10.13.13.2、且源地址不是本地地址的数据包。 总结下就是只允许本地地址或者 peer1 直接访问 10.13.13.2 这个地址。 premangle 链:此链用于确保所有 UDP 数据包都能被正确从 WireGuard 接口入站。 它将所有 UDP 数据包的标记设置为连接跟踪标记(没搞懂这个标记是如何生效的….)。 postmangle 链:此链用于确保所有 UDP 数据包都能被正确从 WireGuard 接口出站。 它将所有 UDP 数据包的标记设置为 0xca6c(51820 的 16 进制格式)(同样没理解这个标记是如何生效的…)。 最后看下 WireGuard 的状态,它是前面 wg setconf peer1 /dev/fd/63 设置的: shell ryan@ubuntu-2004-builder:~$ sudo wg show interface: peer1 public key: HR8Kp3xWIt2rNdS3aaCk+Ss7yQqC9cn6h3WS6UK3WE0= private key: (hidden) listening port: 51820 fwmark: 0xca6c peer: t95vF4b11RLCId3ArVVIJoC5Ih9CNbI0VTNuDuEzZyw= preshared key: (hidden) endpoint: 192.168.5.198:51820 allowed ips: 0.0.0.0/0 latest handshake: 18 minutes, 59 seconds ago transfer: 124 B received, 324 B sent 分析完毕,现在关闭掉 WireGuard 客户端,将客户端主机的网络恢复到正常状态。 shell $ sudo wg-quick down peer1 [#] ip -4 rule delete table 51820 [#] ip -4 rule delete table main suppress_prefixlength 0 [#] ip link delete dev peer1 [#] resolvconf -d tun.peer1 -f [#] nft -f /dev/fd/63 结语 一通分析,你是否感觉到了 wg-quick 的实现十分巧妙,通过简单几行 iptables/nftables 与 iproute2 命令就在 WireGuard 隧道上实现了一个 VPN 网络,更妙的是只要把新增的这些 iptables/nftables 与 iproute2 规则删除,就能恢复到 WireGuard 未启动的状态,相当于整个工作是完全可逆的(显然前面的 sudo wg-quick down peer1 就是这么干的)。 总之这篇文章简单分析了 wireguard 虚拟网络在 Linux 上的实现,希望对你有所帮助。 下一篇文章(如果有的话…),我会带来更多的 WireGuard 实现细节,敬请期待。 参考 wireguard protocol: 官方文档还有官方的白皮书,都写得很清晰易懂。 WireGuard到底好在哪?: 比较深入浅出的随想,值得一读。 Understanding modern Linux routing (and wg-quick): 对 WireGuard 客户端用到的多路由表与路由策略技术做了详细的介绍。 它的中文翻译:WireGuard 基础教程:wg-quick 路由策略解读 - 米开朗基扬

2023/3/28
articleCard.readMore

EE 入门(二) - 使用 ESP32 与 SPI 显示屏绘图、显示图片、跑贪吃蛇

零、硬件准备与依赖库调研 之前淘货买了挺多显示屏的,本文使用的是这一块: 3.5 寸电阻触摸屏,480 * 320,SPI 协议,显示屏驱动 IC 为 ILI9488 开发板是 ESP-WROOM-32 模组开发板。其他需要的东西:杜邦线、面包板、四个 10 K$\Omega$ 电阻、四个按键。 至于需要的依赖库,我找到如下几个 stars 数较高的支持 ILI9488 + ESP32 的显示屏驱动库: Bodmer/TFT_eSPI: 一个基于 Arudino 框架的 tft 显示屏驱动,支持 STM32/ESP32 等多种芯片。 lv_port_esp32: lvgl 官方提供的 esp32 port,但是几百年不更新了,目前仅支持到 esp-idf v4,试用了一波被坑了,不建议使用。 esp-idf/peripherals/lcd: ESP 官方的 lcd 示例,不过仅支持部分常见显示屏驱动,比如我这里用的 ili9488 官方就没有。 总之强烈推荐 TFT_eSPI 这个库,很好用,而且驱动支持很齐全。 一、开发环境搭建、电路搭建与测试 1. 创建项目并配置好环境 ESP32 开发有好几种方式: vscode 的 esp-idf 插件 + 官方的 esp-idf 工具 vscode 的 platformio 插件 + arudino 框架 Bodmer/TFT_eSPI 这个依赖库两种方式都支持,不过看了下官方文档,仓库作者表示 ESP-IDF 的支持是其他人提供的,他不保证能用,所以稳妥起见我选择了 PlatformIO + Arduino 框架作为开发环境。 首先当然是创建一个空项目,点击 VSCode 侧栏的 PlatformIO 图标,再点击列表中的PlatformIO Core CLI 选项进入 shell 执行如下命令: shell pio project init --ide=vscode -d tft_esp32_arduino 这条命令会创建一个空项目,并配置好 vscode 插件相关配置,这样就算完成了一个空的项目框架。 1. 显示屏接线与项目参数配置 网上简单搜了下 ESP32 pinout,找到这张图,引脚定义与我的 ESP32 开发板完全一致,用做接线参考: 可以看到这块 ESP32 开发板有两个 SPI 端口:HSPI 跟 VSPI,这里我们使用 HSPI,那么 MOSI/MISO/SCK 三个引脚的接线必须与上图的定义完全一致。而其他引脚随便找个普通 GPIO 口接上就行。 此外背光灯的线我试了下接 GPIO 口不好使,建议直接接在 3V3 引脚上(缺点就是没法通过程序关闭背光,问题不大)。 我的接线如下: 使用 wokwi.com 制作的示意图 接线实操 线接好后需要更新下 PlatformIO 项目根目录 platformio.ini 的配置,使其显示屏引脚相关的参数与我们的接线完全对应起来,这样才能正常驱动这个显示屏。 这里我以驱动库官方提供的模板Bodmer/TFT_eSPI/docs/PlatformIO 为基础,更新了其构建参数对应的引脚,加了点注释,得到的内容如下(如果你的接线与我一致,直接抄就行): ini [env:esp32dev] platform = espressif32 board = esp32dev framework = arduino lib_deps = bodmer/TFT_eSPI@^2.5.0 Bodmer/TFT_eWidget@^0.0.5 monitor_speed = 115200 build_flags = -Os -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG -DUSER_SETUP_LOADED=1 ; Define the TFT driver, pins etc here: ; 显示屏驱动要对得上 -DILI9488_DRIVER=1 # 宽度与高度 -DTFT_WIDTH=480 -DTFT_HEIGHT=320 # SPI 引脚的接线方式, -DTFT_MISO=12 -DTFT_MOSI=13 # SCLK 在显示屏上对应的引脚可能叫 SCK,是同一个东西 -DTFT_SCLK=14 -DTFT_CS=15 # DC 在显示屏上对应的引脚可能叫 RS 或者 DC/RS,是同一个东西 -DTFT_DC=4 -DTFT_RST=2 # 背光暂时直接接在 3V3 上 ; -DTFT_BL=27 # 触摸,暂时不用 ;-DTOUCH_CS=22 -DLOAD_GLCD=1 # 其他配置,保持默认即可 -DLOAD_FONT2=1 -DLOAD_FONT4=1 -DLOAD_FONT6=1 -DLOAD_FONT7=1 -DLOAD_FONT8=1 -DLOAD_GFXFF=1 -DSMOOTH_FONT=1 -DSPI_FREQUENCY=27000000 修好后保存修改,platformio 将会自动检测到配置文件变更,并根据配置文件下载 Arduino/ESP32 工具链,更新构建配置、拉取依赖库(建议开个全局代理,不然下载会贼慢)。 3. 测试验证 现在找几个 demo 跑跑看,新建文件 src/main.ino,从如下文件夹中随便找个 demo copy 进去然后编译上传,看看效果: Bodmer/TFT_eSPI - examples/480x320 可以直接从 libdeps 中 copy examples 代码过来测试:cp .pio/libdeps/esp32dev/TFT_eSPI/examples/480\ x\ 320/TFT_Meters/TFT_Meters.ino src/main.ino 我跑出来的效果: 二、显示图片、文字 这需要首先将图片/文字转换成 bitmap 格式的 C 代码,可使用在线工具javl/image2cpp 进行转换,简单演示下: 注意高度与宽度调整为与屏幕大小一致,设置放缩模式,然后色彩改为 RGB565,最后上传图片、生成代码。 将生成好的代码贴到 src/test_img.h 中: c // We need this header file to use FLASH as storage with PROGMEM directive: // Icon width and height const uint16_t imgWidth = 480; const uint16_t imgHeight = 320; // 'evt_source', 480x320px const uint16_t epd_bitmap_evt_source [] PROGMEM = { // 这里省略掉图片内容...... } 然后写个主程序 src/main.ino 显示图像: c #include <TFT_eSPI.h> // Hardware-specific library TFT_eSPI tft = TFT_eSPI(); // Invoke custom library // Include the header files that contain the icons #include "test_img.h" void setup() { Serial.begin(115200); tft.begin(); tft.setRotation(1); // landscape tft.fillScreen(TFT_BLACK); // Swap the colour byte order when rendering tft.setSwapBytes(true); // 显示图片 tft.pushImage(0, 0, imgWidth, imgHeight, epd_bitmap_evt_source); delay(2000); } void loop() {} 编译上传,效果如下: 三、写个极简贪吃蛇游戏 N 年前我写的第一篇博客文章,是用 C 语言写一个贪吃蛇,这里把它移植过来玩玩看~ 我的旧文章地址为:贪吃蛇—C—基于easyx图形库(下):从画图程序到贪吃蛇【自带穿墙术】 , 里面详细介绍了程序的思路。 那么现在开始代码移植,TFT 屏幕前面已经接好了不需要动,要改的只有软件部分,还有就是添加上下左右四个按键的电路。 首先清空 src 文件夹,新建文件 src/main.ino,内容如下,其中主要逻辑均移植自我前面贴的文章: c #include <math.h> #include <stdio.h> #include <TFT_eSPI.h> // Hardware-specific library #define WIDTH 480 #define HEIGHT 320 // 四个方向键对应的 GPIO 引脚 #define BUTTON_UP_PIN 5 #define BUTTON_LEFT_PIN 18 #define BUTTON_DOWN_PIN 19 #define BUTTON_RIGHT_PIN 21 TFT_eSPI tft = TFT_eSPI(); // Invoke custom library typedef struct Position // 坐标结构 { int x; int y; } Pos; Pos SNAKE[3000] = {0}; Pos DIRECTION; Pos EGG; long SNAKE_LEN; void setup() { Serial.begin(115200); tft.begin(); tft.setRotation(1); // landscape tft.fillScreen(TFT_BLACK); // Swap the colour byte order when rendering tft.setSwapBytes(true); // initialize the pushbutton pin as an input: the default state is LOW pinMode(BUTTON_UP_PIN, INPUT); pinMode(BUTTON_LEFT_PIN, INPUT); pinMode(BUTTON_DOWN_PIN, INPUT); pinMode(BUTTON_RIGHT_PIN, INPUT); init_game(); } void loop() { command(); // 获取按键消息 move(); // 修改头节点坐标-蛇的移动 eat_egg(); draw(); // 作图 eat_self(); delay(100); } void init_game() { // 初始化小蛇 SNAKE_LEN = 1; SNAKE[0].x = random(50, WIDTH - 50); // 头节点位置随机化 SNAKE[0].y = random(50, HEIGHT - 50); DIRECTION.x = pow(-1, random()); // 初始化方向向量 DIRECTION.y = 0; creat_egg(); Serial.println("GAM STARTED, Having Fun~"); } void creat_egg() { while (true) { int ok = 0; EGG.x = random(50, WIDTH - 50); // 头节点位置随机化 EGG.y = random(50, HEIGHT - 50); for (int i = 0; i < SNAKE_LEN; i++) { if (SNAKE[i].x == 0 && SNAKE[i].y == 0) continue; if (fabs(SNAKE[i].x - EGG.x) <= 10 && fabs(SNAKE[i].y - EGG.y) <= 10) ok = -1; break; } if (ok == 0) return; } } void command() // 获取按键命令命令 { if (digitalRead(BUTTON_LEFT_PIN) == HIGH) { if (DIRECTION.x != 1 || DIRECTION.y != 0) { // 如果不是反方向,按键才有效 Serial.println("Turn Left!"); DIRECTION.x = -1; DIRECTION.y = 0; } } else if (digitalRead(BUTTON_RIGHT_PIN) == HIGH) { if (DIRECTION.x != -1 || DIRECTION.y != 0) { Serial.println("Turn Right!"); DIRECTION.x = 1; DIRECTION.y = 0; } } else if (digitalRead(BUTTON_UP_PIN) == HIGH) { if (DIRECTION.x != 0 || DIRECTION.y != 1) { // 注意 Y 轴,向上是负轴,因为屏幕左上角是原点 (0,0) Serial.println("Turn Up!"); DIRECTION.x = 0; DIRECTION.y = -1; } } else if (digitalRead(BUTTON_DOWN_PIN) == HIGH) { if (DIRECTION.x != 0 || DIRECTION.y != -1) { Serial.println("Turn Down!"); DIRECTION.x = 0; DIRECTION.y = 1; } } } void move() // 修改各节点坐标以达到移动的目的 { // 覆盖尾部走过的痕迹 tft.drawRect(SNAKE[SNAKE_LEN - 1].x - 5, SNAKE[SNAKE_LEN - 1].y - 5, 10, 10, TFT_BLACK); for (int i = SNAKE_LEN - 1; i > 0; i--) { SNAKE[i].x = SNAKE[i - 1].x; SNAKE[i].y = SNAKE[i - 1].y; } SNAKE[0].x += DIRECTION.x * 10; // 每次移动10pix SNAKE[0].y += DIRECTION.y * 10; if (SNAKE[0].x >= WIDTH) // 如果越界,从另一边出来 SNAKE[0].x = 0; else if (SNAKE[0].x <= 0) SNAKE[0].x = WIDTH; else if (SNAKE[0].y >= HEIGHT) SNAKE[0].y = 0; else if (SNAKE[0].y <= 0) SNAKE[0].y = HEIGHT; } void eat_egg() { if (fabs(SNAKE[0].x - EGG.x) <= 5 && fabs(SNAKE[0].y - EGG.y) <= 5) { // shade old egg tft.drawCircle(EGG.x, EGG.y, 5, TFT_BLACK); creat_egg(); // add snake node SNAKE_LEN += 1; for (int i = SNAKE_LEN - 1; i > 0; i--) { SNAKE[i].x = SNAKE[i - 1].x; SNAKE[i].y = SNAKE[i - 1].y; } SNAKE[0].x += DIRECTION.x * 10; // 每次移动10pix SNAKE[0].y += DIRECTION.y * 10; } } void draw() // 画出蛇和食物 { for (int i = 0; i < SNAKE_LEN; i++) { tft.drawRect(SNAKE[i].x - 5, SNAKE[i].y - 5, 10, 10, TFT_BLUE); } tft.drawCircle(EGG.x, EGG.y, 5, TFT_RED); } void eat_self() { if (SNAKE_LEN == 1) return; for (int i = 1; i < SNAKE_LEN; i++) if (fabs(SNAKE[i].x - SNAKE[0].x) <= 5 && fabs(SNAKE[i].y - SNAKE[0].y) <= 5) { delay(1000); tft.setTextColor(TFT_RED, TFT_BLACK); tft.drawString("GAME OVER!", 200, 150, 4); delay(3000); setup(); break; } } 代码就这么点,没几行,接下来我们来接一下按键电路,这部分是参考了 arduino 的官方文档How to Wire and Program a Button 接线方式如下,主要原理就是通过 GND 接线,使四个方向键对应的 GPIO 口默认值为低电平。当按键按下时,GPIO 口会被拉升成高电平,从而使程序识别到该按键被按下。 接线示意图如下(简单起见,省略了前面的显示屏接线部分): 使用 wokwi.com 制作的示意图 现在运行程序,效果如下(手上只有两个按键,所以是双键模式请见谅…):

2023/3/5
articleCard.readMore

EE 入门(一) - 电子电路基础知识

前言 我是从去年 12 月开始玩电子的,起因是 11 月搞了个 Homelab,然后就一路玩到 ESPHome,买了一堆传感器。玩了一阵后 ESPHome 这种小白式玩法就满足不了我了,于是开始学习电路知识,用树莓派跟其他单片机开始折腾遥控小车、简易机械臂、跑马灯等等,可以说是玩得很尽兴。 今年我也打算继续玩一玩这一块,尤其想玩一玩用 ESP32/STM32 自制无人机,如果能搞搞无人机编队飞行就更好了~ 言归正传,这篇文章是我入门电子电路的第一篇笔记,涵盖最基础的电路理论与一些焊接知识,末尾还包含了后续的学习规划。 笔记内容参考了许多网上的资料,主要有如下几个: 纵横向导 - 电路入门 采用类比的方法来讲解电路基础,很适合业余玩家零基础快速入门。 Electrical Engineering Essentials - sparkfun 同样是零基础入门,尤其是还介绍了电烙铁等玩电路板的实用知识。 The Amazing World of Electronics - Only the Cool Stuff :-) 这个是一篇篇零散的文章,每篇文章一个知识点,但是讲得比较深入透彻。 我看完上面的文章后,随着玩得越来越深入,又陆续了解了这些内容: 电路基础 什么是面包板、面包线、杜邦线 如何使用万用表测电压、电流、电阻、电容,判断二极管、三极管引脚。 (N 年前学这玩意儿时用的是最简单易懂的物理指针表,但是实际显然是电子的用着更方便) 回忆下用法:首先调到合适档位,然后测电流要串联到电路中、测电压要与被测器件并联、测电阻直接接在被测器件两端即可。 对于手动量程万用表(如 DT-9205A),它的显示单位为量程名字末尾非数字部分。 比如电阻 200 量程的显示单位就为 Ω;2K/20K/200K 这三个量程的显示单位都为 KΩ;2M/20M/200M 的显示单位都为 MΩ 对于电流/电压/电容也是一样。 什么元件需要防静电,以及有线防静电手环/台垫 如何读色环电阻的阻值?(不会读不如直接万用表走起…) 如何选购、使用电烙铁/吸锡器等焊接工具 … 单片机(MCU)与单板计算机(SBC) 什么是开发版 什么是 TTL 串口、串口驱动、波特率 什么是 SPI/UART/I$^{2}$C 数据传输协议 什么是 GPIO 引脚,以及开发版的引脚各有什么功能 如何使用 USB 转 TTL 串口板给 ESP32/ESP8266/51/STM32 等单片机刷固件 ST-Link/J-Link/DAPLink 调试编程器(仿真器)与 TTL 串口有何区别,JTAG 和 SWD 接口又是个啥?该用哪个? 如何使用 C 语言为单片机编写程序?如何上传编译好的固件?如何调试? … 总之兴趣驱动,不会的东西就 Google 一下或者问问 ChatGPT,玩起来~ 一、常见基础公式 欧姆定律: $U = IR$ 电压 U 单位伏特 Volt,符号 $V$ 电流 I 单位安培 Ampere,符号 $A$ 电阻 R 单位欧姆 Ohm,符号 $\Omega$ 电功率公式: $p= UI$ 功率 p 单位瓦特 Watt,符号 $W$,等同于 $V \cdot A$ 的缩写 电能公式: $w = pT$ 其中 p 为电功率,单位前面说了就是 Watt T 为时间,单位秒 Second $w$ 电能的单位为焦耳 joule,等同于 $V \cdot A \cdot s$ 常见的电池通常会使用 $mA \cdot h$ 或者 $w \cdot h$ 来标记其电能容量。 $mA \cdot h$ 乘上电压再转换下电流跟时间的单位为 A 跟 s,就得到焦耳数 $w \cdot h$ 直接乘 3600(1 小时的秒数)就得到焦耳数 电容量公式: $C = Q/U$ 电容量 C 单位为法拉 Farad,符号为 $F$ 带电量 Q 的单位为库仑 Coulomb,符号为 $C$ 库仑的定义: $1C = 1A \cdot s$ 1 库仑即 $6.24146 \times 10^{18}$ 个电子所带的电荷量 电感 TODO(貌似用得比较少…待补充) 二、常用电子元件介绍 常见电子元器件: 电阻 二极管 Diode 发光二极管 整流二极管 稳压二极管 三极管 MOSFET 场效应管 电压转换器(power converter):整流器(rectifier)、逆变器(inverter)、斩波器 (chopper)及变频驱动器(VFD) 电容 电解电容 瓷片电容 独石电容 晶振 1. 二极管 Diode https://learn.sparkfun.com/tutorials/diodes 二极管是一种只允许电流由单一方向流过,具有两个电极的元件,是现代电子产业的基石。可类比水流中的单向阀门,水只能从一端流向另一端,而不能逆流。 最初的二极管是真空电子二极管,很大、需要预热、功耗大,易破碎。后来美国人使用半导体材料发明了晶体二极管(或者叫半导体二极管)。目前常用的二极管都是晶体二极管,主要使用硅或者锗这类半导体材料。 晶体二极管的核心是 PN 结(p–n junction),要了解 PN 结,需要先介绍半导体的几个概念: 空穴:又称 Electron hole,物理学中指原子的共价键上流失一个电子,最后在共价键上留下的空位。 载流子:半导体中有两种载流子,即价带中带正电的空穴,和导带中带负电的电子。 P 型半导体:P 表示 Positive,指以带正电的空穴导电为主的半导体,也叫空穴半导体。 在纯净的硅晶体中掺入三价元素(如硼),使之取代晶格中硅原子的位置,就形成P型半导体。 N 型半导体:N 表示 Negative,指自由电子浓度远大于空穴浓度的杂质半导体。 例如,含有适量五价元素砷、磷、锑等的锗或硅等半导体。 懂了上面这些后,让我们考虑在一个 N 型半导体跟 P 型半导体形成的 PN 结中,电子显然只能从 N 极流向 P 极,因为只有 N 极才有足够的电子。相反电流只能从 P 级流向 N 极,因为只有 P 极才有足够的空穴。 如果电流要反向流动,那 PN 结的 P 极的电子会更多,而 N 级的空穴也会更多,电势差会更大,显然就会非常费劲。 二极管在导通状态下二示意图如下,其中也展示了二极管对应的符号与真实二极管的结构(带环的一侧为其 N 极): 电阻拥有线性的伏安特性曲线,遵从欧姆定律。而二极管则完全不同,它伏安特性曲线 (Current-Voltage Graph)如下: 几个主要特征与相关名词介绍: 更详细的文章:PN Junction Diode 正向压降 Forward Voltage: 指使电流能够导通的最小电压 $V_F$ 「正向压降」被用于克服二极管的内部电场了,所以在电流通过二极管后,电压需要减去这个电压,这也是中文名「正向压降」的由来。 硅二极管的正向压降通常为 0.6v - 1v,锗二极管的正向压降通常为 0.3v 根据伏安特性曲线,实际上随着电流的变化,「正向压降」也是有小幅波动的,不过计算时一般都认为它是固定值。 击穿电压 Breakdown Voltage: 指使电流能否反向导通的最小电压,从图中标识看 $V_{BR}$ 为 -50v,显然它远大于不到 1v 的「正向压降」。 当电流能经过二极管反向导通时,我们称二极管被击穿(Breakdown) 二极管依据其设计目标,分类了许多不同类别: 普通二极管 整流器(rectifier) / 功率二极管(power diode) 依靠二极管只能单向导通的原理,可以使用它将交流电变成直流电。 能承受较大的正向电流和较高的反向电压 发光二极管(Light-Emitting Diodes, LEDs) LED 的正向压降取决于它的颜色,而且比较固定,通常红色约为 1.6v,绿色有 2v 和 3v 两种, 黄色和橙色约为 2.2v,蓝色约为 3.2v 稳压二极管 利用二极管在反向击穿状态,其电流可在很大范围内变化而电压基本不变的现象,制成的起稳压作用的二极管。 开关二极管 能够快速由导通变为截止或由截止变为导通的一种二极管。 检波二极管 TODO 阻尼二极管 具有较低有电压降和较高的工作频率,且能承受较高的反向击穿电压和较大的峰值电流。 还有二极管堆组: 整流桥堆(半桥、全桥) 菱形联接 等等… 2. 三极管 triode / bipolar transistor 三极管即双极型晶体管,缩写 BJT,前面介绍了二极管结构为单个 PN 结,而三极管的结构则为 PNP 或者 NPN 结构,具有电流放大作用,是电子电路的核心元件之一。 它的工作方式就像是一个一个液压阀门,通过小电流来顶开中间的通路,使大电流得以通过,一个 NPN 型放大器电路的示意图如下: b 与 e 之间的电压形成一个小电流,这个小电流越大,c 与 e 之间的电阻就越小。 就像是如下液压阀门,b 处的水压越大,液压阀门被推得越开,c 与 e 之间的水流就越大: 三极管不是凭空把电放大了,而是说: 小的电信号(小水流)把另一个通路的大电流的阀门打开了, 后面的器件能够感受到这个大电流, 所以是放大了。对电来说 实际有两个电源供电的 一个是小电源 (小信号、信号源) 一个是大电源。 咱们的收音机,实际就是天线,接收到空气中的小电流,你可以理解为毛毛雨。 这个毛毛雨到了三极管的一个脚上打开阀门, 电池供电通过另外两个脚流动,再打开一个后面的三极管, 一级级的这样不断打开,一般收音机最早的时候是三管收音机、六管收音机,就是这么个意思一直到这个水流大到能够推动喇叭就发声了。 一个极简三级放大收音机电路: 两种三极管的符号与识别: 三个电极介绍: C: 即 Collector 集电极 B: 即 Base 基极 E: 即 Emitter 发射极 可以看到 NPN 跟 PNP 三极管最大的区别,是在于电流流向: NPN 的 Base 基极是 P 对应正极,电流从 B 与 C 极 流向 E 极 PNP 的 Base 基极是 N 对应负极,电流从 E 极流向 B 与 C 两个电极 根据 B 极电流 $I_B$ 的变化,$V_{CE}$ 的变化曲线如图: 可以看到在 $I_{B}$ 一定的情况下,不论 $V_{CE}$ 在 2v 以上如何变化,$I_{C}$ 的电流都几乎是恒定的。换个角度看电压在 2v - 12v 之间时,$I_{B}$ 与 $I_{C}$ 几乎是完全的线性关系,不受电压波动的影响。 注意 12v 以上只是没有画出来,假使这个三极管最多只能承受 12v 电压,那更高的电压会击穿它, 你就能看到三极管冒火花了… $\frac{I_{C}}{I_{B}}$ 之间的比率(常数)被称做三极管的电流增益(Current Gain),一般使用 $\beta$ 表示。 因为实际场景中 $I_{B}$ 不太好判断,通常都是直接调整 $V_{BE}$,因此我们再换个角度,对比下 $I_{C}$ 与 $V_{BE}$: 通过上图可以发现三极管的另外两个特征: $V_{BE}$ 需要一个启动电压,大约在 0.7v 左右,低于 0.7v 时$I_C$ 的电流一直非常小。 在 $V_{BE}$ 超过 0.7v 后,任何此电压的小变化,都会导致 $I_{C}$ 的剧烈变化。 一个常见的单状态 NPN 放大器电路如下: 可以注意到,输入 $V_{in}$是一个很小的交流信号,过来之前加了一个电容隔绝掉其中参杂的直流信号。 其次因为 $V_{BE}$ 需要一个启动电压才能进入电流放大的工作区间,这里通过 $R1$ 与 $R2$ 为 $V_{BE}$ 提供了一个启动电压 DC Biasing Point. 3. MOSFET 金属氧化物场效应晶体管 https://elamazing.com/2021/03/31/mosfet/ MOSFET 与三极管的区别与选用:https://www.eet-china.com/mp/a17394.html CMOS 集成电路工艺 - 百科: https://www.zgbk.com/ecph/words?SiteID=1&ID=124559&Type=bkzyb MOSFET 晶体管一般简称 MOS 管,是电压控制元件(通过栅极电压控制源漏间导通电阻),而双极型晶体管(三极管)是电流控制元件(通过基极较小的电流控制较大的集电极电流)。 MOS 管在导通压降下,导通电阻小,栅极驱动不需要电流,损耗小,驱动电路简单,自带保护二极管, 热阻特性好,适合大功率并联,缺点开关速度不高,比较昂贵。 而功能与 MOS 管类似的三极管,特点是开关速度高,大型三极管的 IC 可以做的很大,缺点损耗大, 基极驱动电流大,驱动复杂。 一般来说低成本场合,普通应用优先考虑用三极管,不行的话才考虑 MOS 管。 场效应管能在很小电流和很低电压的条件下工作,功耗低,而且可以很方便地把很多场效应管集成在一块硅片上,因此场效应管在大规模集成电路中得到了广泛的应用。目前主流的数字集成电路,包括 CPU/GPU/RAM,基本都是通过光刻制造的 CMOS 集成电路(Complementary Metal-Oxide-Semiconductor Integrated Circuit),CMOS 就是基于 MOSFET 技术实现的。 MOSFET 管的结构、极性,用法等内容,待补充… TODO 4. 电容 Capacitor 电容是电能的容器,里面存储的是电荷,电容在电路中是储能、缓冲、减压、过滤器件。。 水要通过池塘、湖泊,首先需要灌满它才能过得去。所以这部分水(电能)可以被这些容器保存下来, 这是电容的储能作用,另外很明显,无论前面的水流多么湍急,到了湖泊就要先灌满它,湖泊开口再向下游流水,自然流水就缓慢一些,所以它也有缓冲的作用。大波浪到了湖泊变平稳,实际变成了小波浪,波的形状都变了,这就是过滤的作用,只允许特定的波通过。 再回顾下电容相关的公式: 电容量公式: $C = Q/U$ 电容量 C 单位为法拉 Farad,符号为 $F$ 带电量 Q 的单位为库仑 Coulomb,符号为 $C$ 库仑的定义: $1C = 1A \cdot s$ 1 库仑即 $6.24146 \times 10^{18}$ 个电子所带的电荷量 电容的类型: 瓷片电容 用陶瓷材料作介质,在陶瓷表面涂覆一层金属(银)薄膜,再经高温烧结后作为电极而成。 用途:通常用于高稳定振荡回路中,作为回路、旁路电容器及垫整电容器。但仅限于在工作频率较低的回路中作旁路或隔直流用,或对稳定性和损耗要求不高的场合〈包括高频在内〉。瓷片电容不宜使用在脉冲电路中,因为它们易于被脉冲电压击穿。 铝电解电容(有极性) 有极性铝电解电容器是将附有氧化膜的铝箔(正极)和浸有电解液的衬垫纸,与阴极(负极)箔叠片一起卷绕而成。 优点: 容量范围大,一般为110 000 μF,额定工作电压范围为6.3 V450 V。 缺点: 介质损耗、容量误差大(最大允许偏差+100%、–20%)耐高温性较差,存放时间长容易失效。 用途: 通常在直流电源电路或中、低频电路中起滤波、退耦、信号耦合及时间常数设定、隔直流等作用。 注意:因其具有极性,不能用于交流电路。 独石电容 独石电容是用钛酸钡为主的陶瓷材料烧结制成的多层叠片状超小型电容器。 优点: 性能可靠、耐高温、耐潮湿、容量大(容量范围1 pF ~ 1 μF)、漏电流小等 缺点: 工作电压低(耐压低于100 V) 用途: 广泛应用于谐振、旁路、耦合、滤波等。 常用的有CT4 (低频) 、CT42(低频);CC4(高频)、CC42(高频)等系列。 钽电解电容 有两种制作工艺: 箔式钽电解电容器:内部采用卷绕芯子,负极为液体电解质,介质为氧化钽 粉烧结式: 阳极(正极)用颗粒很细的钽粉压块后烧结而成 优点: 介质损耗小、频率特性好、耐高温、漏电流小。 缺点: 生产成本高、耐压低 用途: 广泛应用于通信、航天、军工及家用电器上各种中 、低频电路和时间常数设置电路中。 等等 5. 电感 Inductance 「电磁感应(Electromagnetic induction)」我们都学过,它是指放在变化磁通量中的导体,会产生电动势。 此电动势称为感应电动势或感生电动势,若将此导体闭合成一回路,则该电动势会驱使电子流动,形成感应电流(感生电流)。 简单的说就是磁场变化能产生电能,电流变化也会形成磁场。 电磁感应最为人所知的应用应该就是「发电机」、「电动马达」跟「变压器」了。「发电机」通过电磁感应将机械能转换为电能,而「电动马达」刚好相反,它通过电磁感应将电能转换为机械能。这个转换实际上都是依靠磁场与「电磁感应」实现的。 而我们这里提的电感这种元器件,其核心原理是楞次定律(Lenz’s law): 由于磁通量的改变而产生的感应电流,此电流的流向为抗拒磁通量改变的方向。 将楞次定律应用在闭合回路的自感效应中,得到的结论是: 电路上所诱导出的电动势的方向,总是使得它所驱动的电流,会阻碍原先产生它(即电动势)的磁通量之变化。 具体而言,对于「电感」,当电流增加时它会将能量以磁场的形式暂时存储起来,等到电流减小时它又会将磁场的能量释放出来,这会产生抵抗电流变化的效果。 电感并不损耗能量,它只是临时将电流存储起来,待会儿再释放出来而已(这叫什么?削峰填谷,平滑算法)。 电感的结构通常是漆包铜线缠绕在一个永磁体上,因为需要有电流的变化才能工作,通常仅应用在交流电领域。 6. 电阻 足够深入的分析:电阻的定义到底是什么? 我们对电阻最直观的理解,是中学时学过的: $$R = \frac{V}{I}$$ 但是在简单的含有电阻 R + 一个电感或电容的直流电路中,电流是随时间变化的,并在最终达到一个稳态。 这时根据上面的公式计算,因为电压是固定的,我们发现电路中电阻 R 的阻值实际是随时间变化的。 这个问题在直流电路中并不明显,因为电路最终仍然会达到稳态,这时电阻就跟它的标称电阻差距不大了。 但是在交流电路中,因为电流始终是在震荡的,这个问题就会变得相当明显,以至于无法简单地使用「电阻」来表达一个电阻器的特性,为此引入了一个新概念叫「阻抗」。 在具有电阻、电感和电容的电路里,对电路中的电流所起的阻碍作用叫做阻抗。阻抗常用Z表示,是一个复数,实部称为电阻,虚部称为电抗,其中电容在电路中对交流电所起的阻碍作用称为容抗 ,电感在电路中对交流电所起的阻碍作用称为感抗,电容和电感在电路中对交流电引起的阻碍作用总称为阻抗。 阻抗的单位是欧姆。阻抗的概念不仅存在于电路中,在力学的振动系统中也有涉及。 如果仔细看看你买过的耳机的相关参数,会发现它就包含一个「阻抗」参数,知乎上就有相关讨论耳机是不是阻抗越高越好?. 对电阻更精确的理解是:电阻是电压对电流的变化率,它不一定是一个静态值(也就是说可能是非线性的,比如二极管的伏安特性曲线就不是直线)。 单片机的下拉电阻与上拉电阻 用单片机设计电路时,一个重要的点就是下拉电阻与上拉电阻。 不太好直接解释,直接看视频吧,下面这两个视频解释得很清晰: 上拉电阻的通俗解释 下拉电阻的通俗解释 再补充一个博友的文章单片机的GPIO配置,详细解释了 GPIO 相关的配置原理。 7. 晶振 (Xtal) 与振荡电路 秒懂单片机晶振电路原理 石英晶体或晶振,是利用石英晶体(又称水晶)的压电效应,用来产生高精度振荡频率的一种电子器件,属于被动器件(无源源件)。 晶体是指其中的原子、分子、或离子以规则、重复的模式朝各方向延伸的一种固体。晶体与几乎所有的弹性物质都具有自然共振频率,透过适当的传感器可加以利用。 石英晶体的优点是在温度变化时,影响震荡频率的弹性系数与尺寸变化轻微,因而在频率特性上表现稳定。 石英晶体谐振器的原理: 石英晶体上的电极对一颗被适当切割并安置的石英晶体施以电场时,晶体会产生形变。这与晶体受压力产生电势的现象刚好相反,因此被称做逆压电效应。 当外加电场移除时,石英晶体又会恢复原状并发出电场,因而在电极上产生电压,这是我们熟知的压电效应。 逆压电效应 + 压电效应 这两个特性造成石英晶体在电路中的行为,类似于某种电感器、电容器、与电阻器所组合成的 RLC 电路。组合中的电感电容谐振频率则反映了石英晶体的实体共振频率。 当外加交变电压的频率与晶片的固有频率(决定于晶片的尺寸与切割方法)相等时,机械振动的幅度将急剧增加,这种现象称为压电谐振。 可能有些初学者会对晶振的频率感到奇怪,12M、24M 之类的晶振较好理解,选用如 11.0592MHZ 的晶振给人一种奇怪的感觉,这个问题解释起来比较麻烦,如果初学者在练习串口编程的时候就会对此有所理解,这种晶振主要是可以方便和精确的设计串口或其它异步通讯时的波特率。 8. 地 电路中每个器件上有电能量集聚,形成电势差,就相当于物体的高度差。假设没有一个参考基准点,就没法测量这个电势差了,因此规定电路的某个点就是作为基准面,也就是地(GND/Ground)了。 地/GND 并不需要是真正的地面,对于我主要关注的弱电电路板而言,电路的负极就是地。 同理可推出,如果需要将同一个电路板同时接入多个源电路,则必须将这多个电路板的负极连接在一起,这样它们的「GND」参考基准点才是一致的! 静电破坏与防静电 https://zhuanlan.zhihu.com/p/570713171 弱电领域另外一个常见的接地应该就是静电接地了,这是为了确保人体/工作台与地面的电势差为零, 避免工作时静电放电导致元器件损坏。 人体感应的静电电压一般在 2KV-4KV 左右,通常是人体轻微运动或与绝缘摩擦引起的。这么高的电压,足够击穿很多电子元件了,所以电子厂都会强制员工穿戴防静电装置(有线防静电手环)。 静电接地通常要求接真正的地面,比如与建筑物接触紧密的金属门窗、水龙头等都算是「地」。个人用的话,据朋友介绍效果最好的方法是:穿拖鞋,并且一只脚踩地上哈哈~ 三、常见电路计算方式 1. 如何选用正确的电阻? 这需要使用到我们中学学过的物理学欧姆定律公式: $$V = I \cdot R$$ 首先针对电子电路领域的 hello world,即发光二极管 + 电阻: 我们可以根据 LED 灯的最大电流来估算电阻值,根据欧姆定律有 $$R = \frac{V}{I}$$ 普通发光二极管的正常工作电流通常为 $2 \text{mA}$ ~ $20 \text{mA}$,电流越大它就越亮,正向压降有好几种,假设我们的为 $3.3v$。 因此电路允许的最大电流为 $0.02 \text{A}$,如果电源电压为 3.7v,那电阻得到的电压大概为 $0.4 \text{V}$,这样可计算得到 $R$ 为 $20 \Omega$. 发光二极管在正常工作状态几乎没有电阻,因此可以直接将上面计算出的结果当作串联电阻的阻值。 因此为了使发光二极管正常工作,串联电阻应该略大于 $25 \Omega$. 2. 电路分析中的两个重要定律 KAL 基尔霍夫电流定律:所有进入某节点的电流的总和等于所有离开这节点的电流的总和 KVL 基尔霍夫电压定律:沿着闭合回路所有元件两端的电势差(电压)的代数和等于零 这两个定律感觉通过「能量守恒」去理解,会显得很直观,不论是电流还是电压,都不会无中生有,在整个电路上它始终是守恒的。 KVL + 节点电压法是分析电路的一种有效手段。 3. 隔直通交与隔交通直 上面这个是常见的简单特性描述,但是不够准确,准确的说: 电容是隔断不变的电信号,通过变化的电信号。 电感是阻碍变化的电信号,通过不变的电信号。 显然直流电的电流也是可以变化的,比如刚过了整流桥的直流电就是一个脉动信号。 4. 交直流叠加信号 交流信号就很好,很真实,为什么还要有交流直流叠加信号,到最后还要把直流信号去掉,只保留交流信号,多麻烦。这是因为,任何器件如果想打开或者处于一定状态,多少都需要一定的能量驱动的,如果这个能量不足,让器件处于不稳定的状态,我们还原不了真实的信号,所以三极管放大加上静态偏置,实际上就是为了让他先工作在临近放大区,再来交流信号才能正确还原。 所谓的静态偏置,实际上就是挂上个电阻先给这个三极管的某个引脚加上直流电。再来的交流信号与直流叠加变成交直流混叠信号,来驱动三极管的b极。 犹如大坝的开口在5米处,但是交流信号(变化的信号)只有1米的波动,所以先把水位抬高到5米,这个波动才能送过去。 现在信号放大电路大部分被运算放大器替代,两个运放之间有一个隔直电容,这是因为运放不需要这种类似三极管的偏置,它不需要抬高水位,本身它建立的条件就是你来波动我就能正常反馈到后级,你这个时候如果叠加了直流信号,反而出问题了,因为你把水位抬高了,比较低的信号不能正常反馈到后级被这个直流信号掩盖了。 四、电子电路工具套装介绍 玩硬件的话,工具套装是必不可少的,最先遇到的场景就是——很多的传感器都需要自己焊接排针。 常用工具的主要有这些: 万用电表(Multimeter) 面包板(Breadboard) 电烙铁(Soldering Iron) 玻纤洞洞板(Stripboard / Perfboard) 其他进阶玩耍时可能会用到的工具: 示波器(Oscilloscope) 可调直流稳压电源(Adjustable DC Power Supply) 频谱分析仪 电子元器件又主要分类两类: 插式元器件 传统电子元器件,都带有较长引脚,PCB 版需要为引脚预留通孔。 相关技术:through-hole technology 这种元件比较大个,都很适合手工焊接,焊接完成后还需要剪掉多余引脚。 片式元器件 SMD (surface-mount device) 一种新型元器件,也叫贴片元件。 比插式元器件要小很多,而且 PCB 板不需要预留插孔,更节省材料跟空间,广泛应用在各种小型化电子设备中。 相关技术:(SMT) Surface-mount technology 相关设备:激光打印钢网、贴片机(巨贵) 贴片元件手工焊接时不适合用电烙铁焊,因为它太小了,这样焊接会很费劲。 贴片元件最简单的手工焊接方法是使用针筒焊锡膏在 PCB 触点上涂好锡膏,可用牙签去掉多余锡膏,然后用镊子将贴片元件放上去,最后使用恒温焊台加热完成焊接。元件放歪点没关系,加热时它会因为液态锡的表面张力自动正位。 1. 电烙铁篇 电烙铁主要考虑的是升温速度跟温度保持能力,便宜的电烙铁基本都有升温慢、焊接中途易失温等毛病。目前总结的电烙铁信息如下: 便携电烙铁:入门级别推荐 优缺点: 便携、价格低。但是升温相对焊台要慢一些,温控相对不够精确,而且无自动休眠, 空烧烙铁头容易氧化,再有就是它没有接地不防经典,焊接精密元件比较危险。 貌似主要推荐广东黄花 907 电烙铁,淘宝官方店买个刀头的 54 大洋 热风枪:主要用来拆焊,以及焊接贴片元件 + 芯片。 我买了一把德力西 2000W 的数显热风枪,不过貌似更多人推荐那种二合一焊台,直接控制热风枪跟电烙铁两个玩意儿。 焊台:进阶推荐,也可考虑一步到位… 优缺点: 发热很快、热容相对较大,自动休眠不会空烧、还有过流保护、单片机稳定温控。缺点是要贵一些,另外相对没那么便携。 相关流行产品 白菜白光 T12 恒温焊台,最早是网友基于日本白光公司 T12 烙铁头(日本工厂到期强制报废的洋垃圾)配上自制恒温控制电路完成的 DIY 焊台,因为相对高端焊台相当便宜所以冠以「白菜」之名。 淘宝上有一些卖这个的,质量见仁见智吧,我没买过。 日本白光 HAKKO 焊台:这个很多人推荐,说是质量好。不过贵,新手用可能有点奢侈了。 二合一焊台:焊台自带热风枪 + 高频电烙铁两件套,高手必备(一般拆机才会用到热风枪) 高频电烙铁使用的是跟电磁炉一样的高频涡流发热原理,电烙铁头自身发热,不需要任何发热芯, 发热很快、热容大、烙铁头更换便宜。高端烙铁头都是高频的。 反正就很高级也很贵啦。我现阶段买了它也是浪费钱,所以没了解具体型号啥的了 恒温加热台:功能跟热风枪差不多,但是体积大很多,而且更贵,新手不推荐买。 关于电烙铁头,貌似刀头是最推荐的,因为它用途最广泛,热容大,基本适用所有场景。 电烙铁,我最后买的第一把电烙铁是网友 DIY 的「L245 焊笔 玫瑰金」,铝合金 CNC 切割工艺,Type-C 供电,最高支持到 PD 120W,颜值很高,口碑也很好,价格是 148 大洋。使用起来还是比较 OK 的,热得很快,热容也 OK。不过毕竟是 DIY 的便宜焊笔,质量不稳,我有遇到过多次误休眠、未识别到焊芯、芯片系统崩溃等问题,都是靠断电重启解决的。 电烙铁的使用与保养 参考:http://www.cxg.cn/newshow1346.html 前面讲了,我毕竟买的是 150 一把的焊笔,C245 这个烙铁头也不便宜,直接当耗材随便折腾就太浪费了。有必要搞清楚怎么使用与保养电烙铁: 焊接作业前,先为高温海绵加水湿润,再挤掉部分水分。 如果使用非湿润的清洁海绵,会使烙铁头受损氧化,导致不沾锡。 另外我发现「镀铜钢丝球」确实比高温海绵好用多了,墙裂推荐!笔不干净了往钢丝球里插一插,立即光亮如新。 焊接作业中,每次都先在高温海绵上擦干净烙铁头上的旧锡,再进行焊接。 如果是用「镀铜钢丝球」,那就直接在钢丝球里插一插,立即光洁如新。 中途不使用时,如果无自动休眠功能,可以手动将温度调低至 200 度以下,避免空烧。空烧会降低烙铁头寿命。 焊接完毕后,将温度调至约 250 摄氏度,使用湿润的高温海绵清洁烙铁头,最后将烙铁头加上一层新锡作保护,这样可以保护烙铁头和空气隔离,烙铁头不会氧化变黑。 烙铁头已经氧化、不沾锡时应如何处理 先把温度调到 300 摄氏度,用清洁海绵清理烙铁头,并检查烙铁头状况。 如果烙铁头的镀锡层部分含有黑色氧化物时,可镀上新锡层,再用清洁海绵抹净烙铁头。如此重复清理,直到彻底去除氧化物,然后在镀上新锡层。 将温度调至 200 摄氏度左右貌似比较容易上锡,不易聚成球。 实测上锡再用海绵抹除,每次都能摸走一些黑色氧化物,非常有效。不过要清理干净还是需要一些耐心。 如果烙铁头变形或穿孔,必须替换新咀。 其他注意事项 勿大力焊接:只要让烙铁头充分接触焊点,热量就可传递,无需大力焊接。 尽量低温焊接:高温焊接会加速烙铁头氧化,降低烙铁头使用寿命。如烙铁头温度超过470℃,它的氧化速度是380℃的两倍。 经常保持烙铁头上锡:这可以减低烙铁头的氧化机会,使烙铁头更耐用。 保持烙铁头清洁与及时清理氧化物 小心放入烙铁架:如果烙铁头接触到烙铁架无法自动休眠,长时间空烧将会毁掉烙铁头。 选用活性低的助焊剂:最便宜的就是松香,更好一点的是无铅无酸无卤素助焊剂。 焊锡丝在焊接过程中为什么会爆锡? https://zhuanlan.zhihu.com/p/584316437 建议直接看上面的文章,可能的原因大概是: 受潮 焊锡丝混有杂质,或者助焊剂含量过高 焊接操作时手上有汗或是洗过手后手没有完全干就开始焊接 烙铁温度过高 我最近买的两卷焊锡丝就有爆锡的问题,烙铁温度是设的很常规的 320 度甚至更低的 290 度,现在怀疑是不是这个无铅焊锡丝有问题。罪魁祸首找到了,是因为我的锡线架,它下方就是湿润的高温海绵…这显然很容易受潮… 如何使用电烙铁进行拆焊 当你焊错焊反了元件,或者你需要修修改改电路板时,就需要先进行拆焊。 用电烙铁进行拆焊需要注意这些事项: 温度必须要高,起码 350 以上 烙铁尖必须留点锡在上面。如果烙铁尖不挂锡,焊接的时候会发现即使温度高,电路板的焊锡也很难融化 另一个方式是先加点有铅锡丝降低焊锡熔点,然后再用吸锡器或吸锡带来吸 如果用吸锡器,发现不撤烙铁头直接把吸锡器怼上去,效果是最好的 如果使用的是吸锡带,温度就必须更高,估计至少得 380 甚至更高 因为吸锡带一般是纯铜,导热性能很好。一般 320 度就很容易熔的锡,上了吸锡带后热量全被吸锡带传导走了,温度不高根本融化不了 或者直接上热风枪+镊子也行(我还没试过…)。 2. 其他工具与材料 焊材(建议日常用贵一点的无铅锡丝,虽然熔点高些,但对身体好): 焊锡丝:最常见的焊材,不过稍微要求一点焊接技术,可能需要大约半个小时熟悉下 常用 0.8mm 跟 1.0mm 的锡丝 个人玩建议买无铅的,虽然贵点熔点高一点,但更环保,对身体也好。 锡膏:新型焊接材料,由焊锡粉、助焊剂以及其它的表面活性剂等混合成的膏状物。 对于常用焊接场景,可以直接抹上锡膏,然后用热风枪一吹,或者用烙铁刀头拖焊,或者直接上发热板 / 恒温加热台,据说非常简单好用。 最常用的场景是复杂 PCB 板子,直接用定制的钢丝网覆盖 PCB 板子刷上锡膏、直接就把触点都刷上了,然后再用镊子手工贴上贴片元器件。不过这个有难度…已经是高手玩法了。最省心是花钱直接找 PCB 厂子给打印 + 焊接(钞能力)。 对于焊点不多的贴片,可以直接使用针筒式的锡膏挤上去,然后再用牙签或镊子去掉多余的锡膏, 用镊子把贴片元件放上去(有点歪没事,加热时焊锡的张力会使它自动回正),最后直接上热风枪或加热台就能焊接 ok 了。 同样建议买无铅的,虽然贵点熔点高一点,但更环保,对身体也好。 高温海绵:可以说是焊接必备了,一定要加水湿润后再使用。可以多备几片,脏了洗洗,洗不干净就换。 镀铜钢丝球:同样是用于清洁烙铁头的,前面讲焊接技术时已经说过了,这个确实比高温海绵好用很多。 助焊剂 Flux: 在焊接工艺中能帮助和促进焊接过程,同时具有保护作用、阻止氧化反应的化学物质。 高纯度松香:便宜常用,一般焊个传感器跟普通 PCB 板子完全够用。 如果板子要长期使用,那焊完需要用酒精浸泡清洗,避免助焊剂碳化导致绝缘性能下降。(如果只是练手的板子,那就无所谓了) 无铅无卤无酸助焊剂:高端助焊剂,免洗 无铅主要是为了环保,对身体好。 因为卤素离子很难清洗干净,助焊剂残留将导致绝缘性能下降,因此免洗助焊剂必须得无卤素。 无酸是为了避免助焊剂腐蚀电路板跟、引脚、烙铁头。 以及其他焊接相关工具: 吸锡器:主要用于电器拆焊 场景:一是焊错了或者锡多了,拆焊后重新焊接。二是拆焊其他电路 这玩意儿一个便宜的才十多块,入门阶段买一个也行。不过也有说拿电烙铁热一下然后一磕,焊锡就自己掉下去了,自己玩不一定需要这玩意儿。 吸锡带:拿来清理表贴焊盘上的残锡。就是一卷细铜丝编制的带子,融化的锡容易被它吸走 比吸锡器更便宜万用,缺点是需要更高的温度,可以考虑买一卷。 热风枪:貌似主要是拆焊用的,当然用来吹热缩管也很好用。 焊台夹具:焊线焊板子都挺实用,相当于长出来四只手。而且相比放桌面,它的散热速度低很多,更难失温。 尖嘴钳:焊接完一些非贴片元件,必须要把多余的引脚剪掉,尖嘴钳感觉挺需要的。 维修工作台(耐高温硅胶垫):淘宝上一二十块钱一块,可以保护桌子、方便放一些小元器件。 还有就是跟焊接没啥关系,但是 DIY 常用的工具: 切割垫:如果需要做一些切割,这个应该也很有用,看许多网友都有,不过我暂时没搞清楚自己是否需要。 螺丝磁性收纳垫:其实跟焊接关系不大了,不过也列一下 螺丝刀 + 万能扳手 + 水口钳:这个好像跟焊接没啥关系,不过也可以列一下 尤其是电动螺丝刀,刀头一定要买好一点的,并且最好是标准有替代品的。我以前用电动螺丝刀就遇到过刀头硬度不行被十字螺丝刀头磨平了的情况… 水口钳推荐德力西 螺丝 + 螺母:螺丝刀跟扳手都有了,螺丝螺母不得买几套? 其中有些特别的是自锁螺母,这种螺母自带尼龙自锁圈,即使没拧到位也能自锁。不过需要用比较大的力气才能拧进去,这是正常现象。 螺母防松的六种基本方法,你知道几个?(动图) 螺丝的型号,DIY 中常用的,M3即螺絲外徑為 3mm, M4 即螺絲外徑為 4mm,同理 M5 即 5mm 有時會註明螺絲牙距,如 M3x0.5,M4x0.70,M5x0.8,M6x1,但因為这是標準規範,通常不提 对结构强度要求不高的场景,也可以自己用 3D 打印机打印螺丝螺母。 螺丝更详细的中英术语对照:螺絲規格與定義 - 緯丞螺絲 游标卡尺 + 卷尺:最简单的是买数字的,不需要费心思读数…也推荐德力西的 3D 打印机、激光切割机等等其他 DIY 工具 五、后续学习路线 有了电路基础后,首先可以买一些入门的焊接套件练练焊接技术,并搞明白它的原理。我在淘宝「电子爱好者之家」上买了几个焊接套件,如指尖陀螺、5v 升 12v 升压板、LED 摇摇棒、十二个实验洞洞板套件、高压发生器等。 边玩边学习相关知识是最有意思的,玩到一定阶段后,可以再考虑补一补基础知识。基础理论方面我查到这几本(为了我的英语能力,选择读英文的): Practical Electronics for Inventors, Fourth Edition: 中文版名为《实用电子元器件与电路基础》,是评论区 @辛未羊 推荐的,感觉确实很适合我这种业余新手入门~ Foundations of Analog and Digital Electronic Circuits: 这本书比较受推荐,中文版《模拟和数字电子电路基础》,豆瓣评分 9.3,不过我看了下比上面那本要难,感觉适合后面进阶看。 学习基础的电路理论时可以仿真软件同步学习,如: Multisim(元器件仿真)、Proteus(单片机仿真) 这两个软件都非常流行,不过基本都仅支持 Windows 系统,我选择放弃。 EDA(Electronic Design Automation) 电路板原理图、PCB(Printed Circuit Board) 设计工具 立创 EDA: 国产 EDA,全平台支持,也提供 Web 版 KiCAD: 开源电路板设计工具,功能强大,支持插件,社区资源多。 1. 单片机 有一定电路基础后,就可以开始玩单片机了。 介绍:单片机的英文名叫 Microcontroller Unit,缩写为 MCU. 它是把 CPU、RAM、定时/计数器(timer/counter)、I/O 接口等都集成在一块集成电路芯片上的微型计算机。 应用:主要用于前端的无操作系统、以实时控制为主的环境,如电子钟表、电机控制等。在硬件爱好者手中可用于机器人前端控制,四轴飞行器前端控制,3D打印机前端控制等。 典型产品: Arduino: AVR 单片机为核心控制器的单片机应用开发板,是开源硬件,新手友好 STM32: 貌似是单片机从业人员的入行首选,使用 ARM Cortex-M 系列核心。 补充说明: 单片机非常简单,因为很接近底层,而且硬件配置极差,干不了太多的事。主要的优势就是稳定、开发也简单。 单片机跟硬件的绑定很严重,经常出现一套代码换一个单片机平台,就得完全重写。 单片机最简单的玩法当属 esphome,只需要会 yaml 配置语言就能开始用 ESP32/ESP8266/ESP32-C3 等 MCU 玩智能家居,不需要写任何代码,生态非常丰富,作为入门路径感觉很合适(文章开头就说了,我就是从这玩意儿入坑的硬件…) 但是 ESPHome 毕竟太简单,用的都是别人写好的现成模块,想实现点更自定义的功能就得自己学习单片机编程了。 我的单片机编程学习路径大概是: 8051: 最简单最经典的单片机 我的 8051 汇编学习笔记与代码:ryan4yin/learn-8051-asm STM32: 工业届应用最广泛的单片机,网上资料众多。 开发工具链很成熟完善,不过有点偏底层,适合用于学习底层知识。 我的 STM32 学习笔记与代码(持续更新中,使用 C 语言,后续打算试下 Rust):ryan4yin/learn-stm32f103c8t6 ESP32: 包含 wifi 蓝牙功能的 IoT 单片机,在物联网领域应用非常广泛,硬件发烧友的最爱。 乐鑫官方的 ESP-IDF 完全开源,功能比较完善,封装层次比 STM32 HAL 更高,而且迭代很快,用起来更简单(不过相对地就对底层更缺乏掌控)。 我的 ESP32 学习笔记与代码(同样持续更新中,也是用的 C,后面也打算用 Rust 搞搞):electrical-engineering/esp32 其他 买了矽速科技新出的 Maix Zero M0S/M1S,使用 RISC-V 架构的 MCU,貌似目前必须用芯片官方 (博流智能)的 SDK 写代码。点了个灯就一直吃灰了( 单片机领域目前仍然是 ARM32 架构的天下,不过开源免费的 RISC-V 架构发展迅猛,有望与 ARM32 分庭抗礼。目前乐鑫基于 RISC-V 的 ESP32C3 就挺受欢迎的,还出了书,另外后续版本 ESP32C5 也已经被 ESP-IDF 支持了,发展很快。 2. 嵌入式 Linux(Linux on Embedded System) 嵌入式系统(Embedded System),是指嵌入机械或电气系统内部、具有专一功能和实时计算性能的计算机系统。 单片机玩够了后,就可以开始玩嵌入式 Linux了。 介绍:嵌入式 Linux,即运行 Linux 操作系统的、性能比 MCU 更高的微型计算机,行业上最常用 ARM Cortex-A5X 系列芯片与 Linux 开发一些嵌入式设备。 应用:路由器、电视盒子、智能家居等。在硬件爱好者手里可以用来做计算机视觉控制小车、WiFi、蓝牙控制中枢等等。 典型产品 Raspberry Pi: 使用 ARM Cortex-A 系列 CPU 的微型计算机,社区庞大,生态丰富。 其他各种国产派,如基于瑞芯微 RK35XX 系列 SoC 的 OrangePi、RockPi、野火鲁班猫等,它们都比现在的树莓派便宜很多(2023 年的 4B 2G 全新要 1000+ 太恐怖了),性能也更高,生态差一些不过瑕不掩瑜。 STM32/IMX6ULL 也有相关产品 补充说明 嵌入式 Linux 代码的可移植性相对要好很多,因为硬件相关的逻辑都封装在驱动层了。 我目前的学习顺序与进度: 瑞芯微 RK3588s 系列国产派: 性能贼强,还自带 NPU(2TOPS * 3) 玩耍的笔记代码放在了这里(Python 与 C 语言)electrical-engineering/rk3588 也用它玩上了 NixOS: ryan4yin/nixos-rk3588 树莓派 4B: 玩耍笔记与代码:electrical-engineering/raspberrypi 其他 除了前面俩,还兜兜转转玩了很多新产品,笔记都写在这里面了:electrical-engineering MAIX-III AXera-Pi AX620A(爱芯派),1.8TOPS 算力(标称 3.6TOPS 的一半不能用于 AI) 这块板子的 NPU 感觉性能还可以,但是 CPU 跟 IO 都有点拉,跑个 pip3 list 都要卡老半天。毕竟 A7 内核,估计性能也就这样了,全靠交叉编译续命。 没啥开源资料,只适合用来玩玩 AI,Linux 系统没啥可玩性。 鲁班猫 0 无线版(LubanCat Zero W) 基于 RK3566,开放的资料非常全,包含 SoC 原厂的各种文档、SDK 驱动开发包、核心板封装库,还提供许多免费的在线文档,内容包含 Linux 内核编译部署、Linux 驱动开发、嵌入式 QT 开发等等 因为资料很全,用来学 Linux 内核驱动开发感觉是比较合适的。 矽速科技的 LicheePi 4A,国产高性能 RISC-V 开发版。 已经用它玩上了 NixOS:ryan4yin/nixos-licheepi4a 群星闪耀家的 Milk-V Mars,同样是国产高性能 RISC-V 开发版,不过比 LicheePi 4A 弱一些 (价格低好多哪)。 用的是赛昉家的 JH7100 芯片,用 Nick_Cao 老师的代码跑了 NixOS 玩。 吐槽:它家这名字是会取的,不论是「群星闪耀」还是「Milk-V」都很有意思 其他我感兴趣的资料(资料内容有一定的重叠): 《Linux/Unix 系统编程手册》:讲解 Linux 的主要系统 API 野火嵌入式 Linux 系列教程: 基础使用 + 内核编程: 感觉跟《Linux/Unix 系统编程手册》内容是重复的,可以简单过一过 Linux 镜像构建与部署: 跟随此文档自己构建一个 Linux 镜像部署到板卡上,这样可以更好的理解 Linux 的启动过程 Linux 驱动开发入门 - 基于鲁班猫 RK356X 系列板卡: Linux 驱动开发入门教程。 Linux Device Driver Development - Second Edition: Linux 驱动编程入门,2022 年出的新书,基于 Linux 5.10,amazon 上评价不错,目前只有英文版,写的很好,对新手很友好。内容跟野火的教程差不多,可以对照学习。 另外还有本 2018 年出的Linux Driver Development for Embedded Processors 2nd Edition 可当作参考书看,写得没上面那本好、内容也没那么新,但是看评价也不错,特点是有许多的 Lab 可做。 Linux Kernel Programming: A comprehensive guide to kernel internals: Linux 内核编程领域的新书,适合入门 Linux 内核,amazon 上评价挺好,先收藏一个 南京大学 计算机科学与技术系 计算机系统基础 课程实验 (PA) Understanding the Linux Kernel, 3rd Edition:Linux 内核技术进阶。 嵌入式 Linux 领域目前也仍然是 ARM 架构的天下,但是开源免费的 RISC-V 架构发展也很快,性能越来越强,生态越来越好,很值得期待。 其他 最近也整了点 FPGA 玩,学了点 Verilog 语言,浅尝辄止,做了点笔记: ee/fpga 社区公众号收藏 单纯一个人埋头自学未免太过枯燥,效率也不一定高,偶尔也可以逛逛各种社区、看看相关的技术博客、文章,是一个更丰富的信息源。 我收集的一些相关论坛、公众号、交流群总结在了这里,可供参考: 可以给我推荐几个相关的论坛或者微信公众号吗? - 知乎 最后简单总结下 上面这些都学了一遍的话,业余玩玩硬件应该就很够用了,期待我完成这个学习路线的那一天…

2023/1/31
articleCard.readMore

2022 年年终总结

闲言碎语 是的又过去一年,又到了一年一度的传统节目——年终总结时间。 2022 年流水账 先简单过一下我 2022 年的流水账(有记录一个 /history,回顾起来就是方便): 1 月 购入 Synthesizer V + 青溯 AI 声库,简单调了几首歌试试,效果非常棒。然后就一直放了一年没碰它…还试用了免费的 ACE 虚拟歌姬,合成效果确实很强,跟收费的 Synthesizer V 有的一拼。 在家过春节,给家里二楼装了空调、加湿器跟地垫。但是没买地暖垫,导致开了空调后地上的垫子冰凉。后面补买了地暖垫但是已经要上班了没体验上。 2 月跟 3 月 想学下区块链技术,结果发现课程一开始就讲加密哈希函数的基本性质,就决定先搞一波密码学, 结果就是输出了一个《写给开发人员的实用密码学》系列文章, 内容大部分是翻译的,少部分是我自己补充。 主要工作:跟推荐系统大佬一起将服务从 HTTP 切换到 gRPC,效果立竿见影,服务流量下降 50% ~ 60%,延迟下降 30% ~ 50%。 4 月份 读完了 Mastering Ethereum,对以太坊有了基本的了解。 读了《Go 程序设计语言(英文版)》 Go 程序设计语言(英文版) 2022-08-19 补图 很高兴通过了职级晋升,不再是 SRE 萌新了。 主要工作:使用 aws/karpenter 实现离线计算集群的弹性扩缩容,省了一波成本。 5 月份 主要是学完了《深入浅出 Kubernetes》这个极客时间专栏 通过《分布式协议与算法实战》等相关资料简单了解了下分布式共识算法的原理,记录了些笔记,8 月份的时候把笔记整理输出为了一篇博客分布式系统的一致性问题与共识算法 还读了许多社区的区块链相关资料,包括但不限于Web 3.0:穿越十年的讨论 - 知乎、《Web3 DApp 最佳编程实践指南》、dcbuild3r/blockchain-development-guide 因为 AI 发展迅猛,来了三分钟兴趣学了一点动手学深度学习 - Pytorch 版,但是进度条走了不到 15% 就不了了之了。 主要工作:研究跨云应用部署方案与跨云 kubernetes 网络方案,如 karmada/kubevela/istio, 以及 L4/L7 层的开源/商业网关方案 6 月份 读完了《在生命的尽头拥抱你-临终关怀医生手记》 读了一点买的新书:《语言学的邀请》跟《Intimate Relationship》 7 月份 主要工作:确定并实施网关架构优化的初步方案,使用 Go 语言写了一个 Nginx Gateway 控制器,迁移流量到新容器化网关省了一波成本。 8 月 读完了《在峡江的转弯处 - 陈行甲人生笔记》 陈行甲人生笔记 延续上个月对 Linux 系统的兴趣,快速过了一遍 The ANSI C Programming Language 以熟悉 C 的语法,之后开始阅读 Linux/Unix 系统编程手册(上册) 写了一个小项目 video2ascii-c 练手 C 语言。 The ANSI C Programming Language 因为今年搞网关 APISIX/Nginx 接触比较多,看了一点极客时间《OpenResty 从入门到实战》但是因为兴趣并不强烈,又不了了之了。 主要工作: 搞网关优化省了一波成本,但是期间也搞出一个严重故障… 承接了一个数据上报网关的需求,需要在网关层支持一些稍微复杂点的功能确保升级流程的稳定性。跟 APISIX 官方沟通后得到了比较好的解决方案custom plugin - set an upstream as a http fallback server 9 月 偶然发现手机桌面上有一个安装了好久但是一直没用过的 APP 英语流利说,顺手用它测了下自己的英文水平。然后就对英语感兴趣了,制定了英语学习计划并发布对应的博文Learn English Again,然后就开始坚持学英语,感觉整个过程都很顺利。 主要工作: 仍然是搞网关优化省成本,因为各种原因,再次输出一篇 Post Mortem 搞数据上报网关的需求 10 月 找了很多英语学习资料,通过每日的坚持学习,渐渐找到了自己的英语学习节奏,完善了学习规划。 《Linux/Unix 系统编程手册(上册)》阅读进度过半,但是业余时间就这么点,同时用来学习 Linux 跟英语实在有点吃力,这本书的阅读就慢慢放下了。 Linux/Unix 系统编程手册(上册) 通过友链漫游,发现了 0xFFFF 社区,内容质量很高,也在社区的 QQ 群里跟群友们聊了些有意思有价值的内容。 打游戏学英语超飒的重剑女仆 Noelle DEEMO 2 中丰富的对话内容 因为许多原因,中概股大跌,公司架构大调整,走了很多大佬,包括去年带我冲浪的算法部门前辈。 主要工作 搞数据上报网关的需求,一路踩坑,总算把数万 QPS 的流量全部迁移到新网关上了。 11 月 重新对搞 Homelab 产生了兴趣,买了三台 MINI 主机组了一个 Homelab,时隔一年多又开始折腾 Proxmox VE,做各种规划。 迭代了很多次后的个人 Homelab 文档:ryan4yin/knowledge/homelab 我的 Homelab 导航页 2022-11-12 因为业余时间沉迷搞 Homelab,英语打卡就变得断断续续了…但是词汇量测试的效果出乎意料, 进步速度喜人,阅读能力也能感觉到有明显提升。 月底搬家换了个新租房,床是挂天花板上的,房间就宽敞了很多,而且拉了独立的电信宽带,网速杠杠的。 11/25 去东莞松山湖跟高中同学聚会,然后跟几位同学打麻将打到半夜三点多… 还远远眺望了眼同学读博的地方——「中国散裂中子源」,感觉很高大上 主要工作:继续推进线上网关优化项目,以及调研 K8s / Istio 的新版本变化,为集群升级做预备工作。 12 月 从 Homelab 折腾到 HomeAssistant/ESPHome,然后就折腾 ESP32/ESP8266,结果很意外地就买了一堆硬件,入手了电烙铁热风枪万用表等各种仪器,ESP/51/STM32 都玩了个遍… 输出内容有两个代码仓库:learn-8051-asm 与 learn-stm32f103c8t6,以及一份 EE 笔记:Electrical Engineering 我的电子电路初体验 8051 汇编 - 数码管显示 2023 ChatGPT 横空出世,引发全网热潮。有技术大佬感慨,这个时刻竟然来临得如此之快,惊喜之余也有点猝不及防。我也把玩了一波,也用它帮助我学了许多硬件相关的东西,很有帮助。 个人猜测未来 ChatGPT 成熟后大概率能极大提升技术人员的工作效率,很可能间接影响到许多人的工作。 年底还入手了一台 3D 打印机 ELEGOO Neptune 3 Pro… 全国逐渐放开疫情管控,我得了新冠,然后康复… 这个月折腾硬件,英语漏打卡更严重了,但是词汇量仍然在稳步增长,阅读起来也是越来越顺畅。 主要工作: 线上网关优化项目基本落地,取得了预期收益,但是没达到之前设的激进目标。(旧网关仍留存极少部分流量,还需要时间去统一网关架构) 做 K8s 集群升级准备,然后月底公司大面积新冠,拖慢了这项工作的进度,即使后调了升级时间,仍然感觉有点虚… 最后是连续三年蝉联我年度歌手的天依同学,截图放这里纪念一下: 我的网易云年度歌手 2022 年 Highlights 1. 英语 英语也是我今年比较惊喜的一个部分,很长一段时间内,我都觉得英语的优先级并不高,一直没有把它的学习排上日程,水平也一直没啥显著提升。 但是从今年 9 月份开始到现在这四个月的英语学习中,我的进步相当明显,从去年大概 4700 词,到现在测试结果为 6583 词,涨了近 2000 词,月均接近 500 词(按这个速度,2023 年 10000 词的目标好像没啥难度了)。 词汇量测试结果按时间排序如下,使用的测试工具是Test Your Vocabulary : 2023-01-02 词汇量测试结果:6583 词 2022-12-19 词汇量测试结果:6300 词 2022-11-17 词汇量测试结果:5600 词 2022-10-18 词汇量测试结果:5100 词 另外因为主要是靠读书来学英语,今年的英文阅读能力也有明显提升,跟 9 月份刚开始读的时候比, 阅读体验要流畅多了。一些英文原版书阅读成就: 在薄荷阅读上读完的第一本英语原版书 而口语、写作这些今年基本没练习,原地踏步。 2. 业余技术 今年业余搞的技术,感觉这些都是我比较满意的: Web3: 今年上半年花了不少时间去了解 Web3,但是仍然没敢说自己已经懂了它。水比较深,浅尝辄止。 电子电路(硬件):点亮这个技能完全是个意外…但也挺惊喜的,毕竟我大学学的建筑声学,以前都没接触过硬件。 Go 语言:去年底定的目标是将 Go 语言应用在至少两个项目上,实际上只用在了一个项目上,完成度 50% 吧。 Linux: Linux 今年主要是复习了一遍 C 语言,然后看了半本《Linux/Unix 系统编程手册(上册)》,之后因为学英语就给放下了。 毕竟英语的成果很不错,这个结果我觉得也是预期内的。 博客:今年博客经营得尚可,数了下有 18 篇技术干货,四篇非技术文章。最主要是三月份翻译密码学的文章冲了一波内容量。虽然 12 月份又鸽掉了…总体还是满意的。 3. 工作 SRE 组 2022 年工作的主旋律其实就是省钱,我 2022 年的工作上有更多的挑战,不过因为得心应手很多,反倒没什么想特别着墨描述的了。 我上半年工作成果比较突出,下半年虽然工作成果差一些,但是业余的学习成果相当突出,总体很满意自己今年的成绩。 单纯从工作方面讲,我给自己的评价仍然是「良好」。 4. 阅读 2022 年一共读完了这些书: 《人间失格》 《月宫》 《Practical Cryptography for Developers》 《Mastering Ethereum》 《Go 程序设计语言(英文版)》 《深入浅出 Kubernetes - 张磊》 《在生命的尽头拥抱你-临终关怀医生手记》 《在峡江的转弯处 - 陈行甲人生笔记》 《The ANSI C Programming Language》 The Time Machine Learn Robotics With Raspberry Pi 学习使用树莓派控制智能小车,结合本书与网上资料,我制作了一台使用 Xbox One 手柄遥控的四驱小车,相当有意思~ Learn Robotics Programming, 2nd Edition 跟前面那本一样是讲树莓派小车的,不过这本书更深入,代码含量高很多。 快速翻了一遍,跳过了其中大部分代码,因为书中的小车不太符合我的需求。 The Unlikely Pilgrimage of Harold Fry 51 单片机自学笔记 看起来,去年定的一个月至少读一本书的目标,还是达成了滴~ 2023 年的展望 技术侧 2022 年的结果跟年初的展望区别仍然是挺大的,但是我个人挺满意。 这里再记一下 2023 年技术上的展望,看看今年能实现多少,又会点出多少意料之外的技能吧哈哈: 云原生 去年定的阅读 k8s 及相关生态的源码没任何进度,2023 年继续… 2019 年到现在,我的工作时长已经有三年半了,希望更多的东西能通过学习底层知识去知其所以然,而不是全靠网上找资料,人云亦云一知半解地去解决问题。 Linux 与网络 2023 年把《Linux/Unix 系统编程手册》这套书看完,并且过完0xFFFF - MIT6.S081 Operating System Engineering (Fall 2020) 这个课,对 Linux 内核与操作系统形成较深入的理解。 借着对硬件的兴趣学一学 Linux 驱动开发。 学习学习时下超流行的 eBPF 技术 3D 打印 2022 年底买了台打印机,那不必须得打印点自己设计的东西? FreeCAD 学!Blender 可能跟 3D 打印没啥关系但是也要学! 编程语言 今年 Go/C 两个语言的技能点感觉是点出来了,2023 年需要巩固下,用它们完成些更复杂的任务。 另外借着搞硬件的兴趣,把 Rust/C++ 两门语言也玩一玩 C++ 主要是用来玩 ESP32/ESP8266,rust 那可是时下最潮的系统级语言,2022 年虽然用 rust 写了点 demo 但离熟练还差很远。 其他 2022 年我给开源社区提交的代码贡献几乎没有,希望 2023 年能至少给三个开源项目提交一些代码贡献,这也是检验自己的代码水平。 制作一台自己的无人机或者穿越机(虽然还不太懂什么是穿越机…),并借此练习自己学习的软硬件知识。 更多地在公司内部、博客等地方分享自己所学的知识,提升所学知识的可复用性,同时也碰撞出更多的灵光,更深入地理解它们。 生活侧 2022 年初我写的生活上的展望,貌似只有「阅读」这一项达标了… 不过今年也仍旧记录下 2023 年的展望: 2022 年因为疫情以及自己懒,参与的户外活动相当少,2023 年希望能更多的做些户外运动,身体还是很重要的啊。 把轮滑水平练上去一点,轮滑鞋在 2022 年吃灰了几乎一整年… 音乐上,口琴、竹笛、midi 键盘、Synthesizer V / ACE Studio / Reaper,总要把其中一个练一练吧…(什么?学吉他??不敢开新坑了,旧坑都还没填完呢…) 阅读:仍然跟去年保持一样的节奏就好,目标是一个月至少阅读一本书。 英语:英语的规划在Learn English Again 中已经做得比较详尽了,这里仅摘抄下目标。 2023 年达到 CEFR 的 C1 等级,报考并取得 BEC 高级证书 2023 年底词汇量超过 10000 结语 2021 年的年终总结文末,我给自己 2022 年的期许是「更上一层楼」,感觉确实应验了。 那么 2023 年,我希望自己能够「认识更多有趣的人,见识下更宽广的世界」~ 更多有趣的的 2022 年度总结:https://github.com/saveweb/review-2022

2023/1/2
articleCard.readMore

Proxmox Virtual Environment 使用指南

本文介绍我使用 PVE 的一些心得(不保证正确 emmmm),可能需要一定使用经验才能顺畅阅读。 前言 我在去年的文章 「QEMU/KVM 虚拟化环境的搭建与使用」 中介绍了如何使用 QEMU/KVM 作为桌面虚拟化软件,其功能对标开源免费的Oracle VM VirtualBox 以及收费但是用户众多的VMware Workstation Pro. 虽然我们也可以远程使用 QEMU/KVM,但是使用门槛比较高。而且如果要管理多台服务器,各种命令也比较繁琐。我们显然需要更易用的软件来管理服务器场景下的虚拟化。 而这篇文章介绍的 Proxmox Virtual Environment(后续简称 PVE),就是一个基于 QEMU/KVM 的虚拟机集群管理平台。 PVE 以 Debian + QEMU/KVM + LXC 为基础进行了深度定制,提供了一套比较完善的 Web UI,基本上 95% 的操作都可以直接通过它的 Web UI 完成,但是仍然有些功能只需要使用它的 CLI 完成,或者说需要手动修改一些配置文件。 PVE 完全基于 Linux 世界的各种开源技术,存储技术使用了 LVM(也支持 Ceph/iSCSI/NFS),也支持通过 cloudinit 预配置网络、磁盘扩容、设置 hostname(这其实是 libvirtd 的功能)。它的文档也比较齐全,而且写得清晰易懂,还包含许多它底层的 QEMU/KVM/CEPH/Cloudinit 等开源技术的内容, 对学习 Linux 虚拟化技术也有些帮助。(这里必须喷下 VMware 的文档,真的是写得烂得一批,不知所云) 总的来说,PVE 没有 vSphere Hypervisor 跟 Windows Hyper-V 那么成熟、完善、稳定,但是基于 QEMU/KVM 且能够免费使用,很适合 Linux/开源/虚拟化 爱好者折腾。 你可能还听说过 OpenStack,不过这个玩意儿我没接触过,所以这里略过了它。 因为这些原因,我选择了 PVE 作为我的 Homelab 系统。 先贴一张我当前 Homelab 的 PVE 控制台截图,然后就进入正文。 我的 PVE 集群 如果你想了解我的 PVE 集群都跑了些啥,可以瞅一瞅homelab - ryan4yin/knowledge. 一、安装 PVE 系统 建议直接使用 ventoy 制作一个 U 盘启动盘,把官网下载的 PVE ISO 镜像拷贝进去,即可使用它进行系统安装。安装过程中需要注意的点有: 如果你有多台机器,每台机器需要使用不同的主机名称(hostname),否则后面组建 PVE 集群时会有麻烦。 建议使用机器型号 + 数字编号作为机器的 hostname 为每台 PVE 节点配置静态 IP,避免 IP 变更。 系统安装好后即可按照提示直接访问其 Web UI,会提示 HTTPS 证书无效,忽略即可。另外还会有一个烦人的 PVE 订阅提示,也可直接忽略(7.2 及以上版本,暂时没找到怎么禁用掉这个提示)。 此外对于国内环境,建议使用如下命令配置国内镜像源(提升软件安装速度): shell # 设置 debian 的阿里镜像源 cp /etc/apt/sources.list /etc/apt/sources.list.bak sed -i "s@\(deb\|security\).debian.org@mirrors.aliyun.com@g" /etc/apt/sources.list # 设置 pve 国内镜像源 # https://mirrors.bfsu.edu.cn/help/proxmox/ echo 'deb https://mirrors.bfsu.edu.cn/proxmox/debian buster pve-no-subscription' > /etc/apt/sources.list.d/pve-no-subscription.list 组建 PVE 集群 如果你仅使用单机 PVE,可忽略这一节内容。 将多台 PVE 节点组成一个集群,可以获得很多新玩法,比如虚拟机在多节点间的热迁移。 注意 CPU 架构差别较大很可能会导致无法热迁移,建议使用同品牌、同代的 CPU,最好是 CPU 型号完全一致。比如都是 Intel 的 12 代 CPU,或者都是 AMD 的 5 代 CPU。 这个也还挺简单的,首先随便登入一台机器的 Web Console,点击「Datacenter」=>「Cluster」=>「Create Cluster」即可创建一个 PVE 集群。 接着复制「Join Information」中的内容,在其他每台 PVE 节点的 Web Console 页面中,点击「Datacenter」=>「Cluster」=>「Join Cluster」,然后粘贴前面复制的「Join Information」,再输入前面节点的密码,等待大约一分钟,然后刷新页面,PVE 集群即组建完成。 PVE 集群配置 PVE 集群的所有节点是完全平等的,集群组建完成后,登录其中任意一个节点的 Web Console 都可以管理集群中所有节点的资源。 二、PVE 控制台的使用 PVE 控制台的使用还挺简单的,多试试基本就会用了。这里不做详细介绍,主要说明下创建虚拟机时一些重要的参数: CPU 将 CPU 类型设置为 host 可以提高性能,适合比较吃性能或者对实时性要求高的虚拟机如 windows/openwrt 对于虚拟机核数,建议将 sockets 设为 1(即 CPU 插槽数,一般物理服务器才会有 2 及以上的 CPU 插槽),cores 设为你想分配给该虚拟机的 CPU 核数 仅针对多物理 CPU 场景(多 sockets)才需要启用 NUMA(个人猜测,可能有错) 磁盘、网卡 磁盘驱动建议用 virtio SCSI、网卡驱动建议用 VirtIO(paravirtualized),它的性能更高。 Linux 虚拟机原生支持 virtio 半虚拟化,而 windows 想要完全开启半虚拟化,需要手动安装驱动,详见Windows_VirtIO_Drivers - Proxmox WIKI, 简单的说就是要下个 iso 挂载到 windows 主机中,并安装其中的驱动。 如果硬盘是 SSD,虚拟机磁盘可以启用 SSD Emulation,对于 IO 性能要求高的场景还可以为磁盘勾选 IO Thread 功能 显示器 默认使用 std 类型,兼容性最好,但是是纯 CPU 模拟的,比较耗 CPU。 如果你有需要显卡加速的桌面虚拟机,但是又不想搞复杂的显卡直通,可以选择VirGL GPU(virtio-gl) 类型(注意不是 VirtIO-GPU(virtio),这个驱动没有显卡加速能力),它能以较小的性能损耗将虚拟机中的 3D/2D 运算 offload 到 host GPU,而且避免复杂的驱动配置,只需要在 PVE 中执行。但是目前它仅支持 Linux 4.4+ 的 Guest 主机,并且要求 mesa (>=11.2) compiled with the option gallium-drivers=virgl(我感觉这功能目前还有点鸡肋)。 要使用 VirGL GPU(virtio-gl),还需要在 PVE 主机上安装额外的依赖:apt install libgl1 libegl1,安装好后即可使用。 详见QEMU Graphic card - Proxmox VE 其他选项 调整启动项顺序,对于 cloud image 建议只启用 scsi0 这个选项 虚拟机模板(Template)与克隆(Clone) 建议首先使用 ubuntu/opensuse cloud image 配置好基础环境(比如安装好 vim/curl/qemu-guest-agent),然后转换为 template,其他所有 Linux 虚拟机都可以直接 clone 一个,改个新名字,再改改 cloudinit 配置跟磁盘大小,就能直接启动使用了。相当方便。 仅 Full Clone 的虚拟机才可以在 PVE 集群节点间随意迁移,因此如果你需要虚拟机迁移功能, 请不要使用 Link Clone. BIOS 通常都建议使用默认的 SeaBIOS,仅 Windows 等场景才建议换成 OMVF(UEFI) OMVF 的分辨率、Secure Boot 等参数,都可以在启动时按 ESC 进入 UEFI 配置界面来调整。 上面这些内容,官方有详细文档,能读英文的话可以直接看Qemu/KVM Virtual Machines - Proxmox WIKI. 1. 使用 cloudinit 自动配置网卡、SSH密钥、存储空间 完全参照官方文档Cloud-Init_Support - PVE Docs 注意:下面的几种镜像都分别有自己的坑点,仅 Ubuntu/OpenSUSE 测试通过,其他发行版的 Cloud 镜像都有各种毛病… 一般配 Linux 虚拟机,我们当然希望能在虚拟机启动时,就自动配置好 IP 地址、SSH 密钥、文件系统自动扩容,这样能免去很多手工操作。cloudinit 就是一个能帮你自动完成这些功能的工具,AWS、阿里云等各大云服务厂商都支持这种配置方式,好消息是 PVE 也支持。 下面简单介绍下如何使用 cloudinit 来自动化配置 Linux 虚拟机。 首先 cloudinit 必须使用特殊的系统镜像,下面是几个知名发行版的 Cloud 系统镜像: Ubuntu Cloud Images (RELEASED): 提供 img 格式的裸镜像(PVE 也支持此格式) 请下载带有 .img 结尾的镜像,其中以 kvm.img 结尾的镜像会更精简一点,而名称中不包含 kvm 的镜像会稍微大一点,但是带了所有常用的内核模块。(如果你不理解前者精简了啥,请选择后者——也就是稍大的这个镜像文件。) OpenSUSE Cloud Images 请下载带有 NoCloud 或者 OpenStack 字样的镜像。 对于其他镜像,可以考虑手动通过 iso 来制作一个 cloudinit 镜像,参考openstack - create ubuntu cloud images from iso 注:Debian Cloud Images 的镜像无法使用,其他 ubuntu/opensuse 的 cloud 镜像也各有问题…在后面的常见问题中有简单描述这些问题。 这里评论区有些新内容,指出 cloud image 的各种毛病可能的解决方案,想深入了解请移步评论区。 上述镜像和我们普通虚拟机使用的 ISO 镜像的区别,一是镜像格式不同,二是都自带了cloud-init/cloud-utils-growpart 等用于自动化配置虚拟机的相关工具。 其名字中的 NoCloud 表示支持 cloudinit NoCloud 数据源——即使用 seed.iso 提供 user-data/meta-data/network-config 配置,PVE 就是使用的这种模式。而 Openstack 镜像通常也都支持 NoCloud 模式,所以一般也是可以使用的。 以 ubuntu 的 cloudimg 镜像为例,下载好镜像后,首先创建虚拟机,并以导入的磁盘为该虚拟机的硬盘,命令如下: 如下操作也可在 Web UI 上操作,这里仅以命令行为例。 shell # 创建新虚拟机 qm create 9000 --name ubuntu-bionic-template --memory 2048 --net0 virtio,bridge=vmbr0 # 将下载好的 img/qcow2 镜像导入为新虚拟机的硬盘 qm importdisk 9000 ubuntu-20.10-server-cloudimg-amd64.img local-lvm # 通过 scsi 方式,将导入的硬盘挂载到虚拟机上 qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9000-disk-0 # qcow2 镜像默认仅 2G 大小,需要手动扩容到 32G,否则虚拟机启动会报错 qm resize 9000 scsi0 32G 然后创建挂载 cloud-init 的 seed.iso,修改启动项以及其他: shell # 创建一个 cloud-init 需要使用的 CDROM 盘(sr0) qm set 9000 --ide2 local-lvm:cloudinit # 设置系统引导盘 qm set 9000 --boot c --bootdisk scsi0 # 设置 serial0 为显示终端,很多云镜像都需要这个。 qm set 9000 --serial0 socket --vga serial0 上面的工作都完成后,还需要做一些后续配置 手动设置 cloud-init 参数,重新生成 cloudinit image,启动虚拟机,并通过 ssh 登入远程终端 cloud image 基本都没有默认密码,并且禁用了 SSH 密码登录。必须通过 cloud-init 参数添加私钥、设置账号、密码、私钥。 检查 qemu-guest-agent,如果未自带,一定要手动安装它! ubuntu 需要通过 sudo apt install qemu-guest-agent 手动安装它 安装所需的基础环境,如 docker/docker-compose/vim/git/python3 关闭虚拟机,然后将虚拟机设为模板 接下来就可以从这个模板虚拟机,克隆各类新虚拟机了~ 保险起见,改完配置后记得点下 Regenerate Image 其他 cloudinit 相关文档: 配置 Cloud-Init 工具 - 华为云 canonical/cloud-init - github Run Amazon Linux 2 as a virtual machine on premises 2. 虚拟机硬盘扩容 CentOS/Ubuntu/Debian 提供的 Cloud 镜像,都自带了 cloud-utils-growpart 这个组件,可以实现在扩容物理硬盘时,自动调整 Linux 的分区大小。 因此需要扩容虚拟机时,直接通过 UI 面板/命令行扩容虚拟机的硬盘,然后重启虚拟机即可,Linux的分区会在系统启动阶段被 cloud-utils-growpart 自动扩容。 PVE 可通过如下命令进行磁盘扩容: shell # 将 id 为 9000 的虚拟机的 scsi0 磁盘,扩容到 32G # 请自行修改虚拟机 ID 与磁盘大小,注意仅支持扩容!不能缩容。 qm resize 9000 scsi0 32G 而其他非 Cloud 镜像,则需要在扩容磁盘后再进入虚拟机手动扩容分区跟磁盘,具体命令就不介绍了,请自行查阅相关文档吧。 因为这个方便的特性,也为了减少虚拟化的开销,Cloud 镜像默认是不使用 LVM 逻辑分区的。LVM逻辑分区虽然方便,但是它对物理机的作用更大些。虚拟机因为本身就能动态扩容“物理硬盘”的大小, 基本不用不到 LVM。 还有一点,就是虚拟机通常只需要一个根分区就行,尤其是归 openstack/kubernetes 管的虚拟机。只有在使用分布式存储之类的场景下,数据需要独立存储,才需要用到额外的分区(/data 之类的)。一般只有物理机,才需要像网上很多文章提的那样,为 /boot / /home 去单独分区。而且现在大家都用 SSD 了,物理机这样做分区的都少了,比如我个人电脑,就是一个 / 分区打天下。。。 三、常见问题 1. 导入已有的 qcow2 镜像 这一步必须要命令行操作,WebUI 界面不支持。 首先在页面上新建一台新虚拟机,记录下虚拟机 ID。 假设你创建的虚拟机 id 为 201,现在通过 scp/rsync 等手段将 qcow2 传输到 PVE 节点上,然后命令行使用如下命令导入 qcow2/img 镜像: shell # 命令格式 qm importdisk <vmid> <source> <storage> # 示例 qm importdisk 201 vm-201-disk-1.qcow2 local-lvm 导入完成后,在虚拟机的 WebUI 界面中,会看到底下多了一个「未使用的磁盘 0」。 接着删除掉默认的磁盘(分离+删除,要两步),再挂载这个「未使用的磁盘 0」。 挂载完成后直接启动是不行的,还需要在设置中将新磁盘添加到启动项中,这样就能正常启动了。 2. 点击 shutdown 后 PVE 系统卡住 PVE 的 shutdown 功能依赖 qemu-guest-agent,对于还没有安装 qemu-guest-agent 的任何主机,或者已经卡死无响应的虚拟机,千万不要点 shutdown 按钮,因为一定会卡住很久,最后失败! shutdown 卡住的解决办法:手动在下方的「Tasks」面板中双击卡住的「Shutdown」操作,然后点击「stop」停止该操作。 该如何关闭这类没有 qemu-guest-agent 或者已经卡死无响应的主机?答案是使用 stop! 3. can’t lock file ‘/var/lock/qemu-server/lock-xxx.conf’ – got timeout PVE 虚拟机卡在 BIOS 系统引导这一步,无法启动,也无法 stop! 解决方法:手动删除掉 lockfile: /var/lock/qemu-server/lock-xxx.conf 因为虚拟机还卡在 BIOS 引导这一步,删除掉 lockfile 再关闭虚拟机并不会导致数据丢失。 4. PVE 集群有一个节点宕机,如何解除关联? 将多个节点组成一个 PVE Cluster 是很自然的一个选择,它能提供虚拟机热迁移、统一管理面板等非常方便的功能。但是这会带来集群级别的高可用问题。 根据官方文档 Cluster_Manager - Proxmox,如果你需要容忍一定数量的节点宕机,PVE Cluster 至少需要三台主机(这跟 Etcd 一样,大概是 Raft 共识算法的要求),并且所有节点的 PVE 版本要完全一致。 那么如果个别节点出了问题,无法修复,该如何将它踢出集群呢? 如果在线节点占比超过 50%,节点删除的流程如下: 首先通过访问节点的 shell 界面,通过命令 pvecm nodes 确认集群的所有节点 将需要移除的节点彻底关机,并且确保它不会以当前配置再次启动(也就是说关机后需要清空硬盘, 避免数据混乱) 如果被删除节点已宕机,则可跳过 关机 步骤 通过命令 pvecm delnode xxx 将问题节点移除集群 重置旧节点硬盘,并重新装机,用做其他用途。 如果你的集群只有 2 个节点,或者有超过 3 个节点但是宕机节点数不低于 50%,那出于数据一致性要求 Raft 算法会禁止更新集群数据,上面的流程就走不通了。如果你直接走上面的流程,它会报错cluster not ready - no quorum? 这时需要首先修改配置,使剩下的节点重新达成一致。其实就是修改选主节点时的投票数。 对于 2 个节点但挂掉 1 个的情况,首先执行如下指令允许当前节点自己选主: shell # 设置只需要 1 票就能当前主节点 # 潜在问题是可能有些集群元数据只在损坏节点上有,这么改会导致这些数据丢失,从而造成一些问题。 # 安全起见,建议在修复集群后,再重启一遍节点... pvecm expected 1 现在 quorum 就已经恢复了,可以走前面给出的节点移除流程。 如果节点已经删除,但是 Web GUI 上仍然显示有被删除的节点,可以在集群的所有剩余节点上,手动删除掉 /etc/pve/nodes/node-name/ 文件夹,即可从集群中彻底删除该节点的数据,注意千万别删错了,不然就尴尬了… 如果 corosync 完全无法启动,上面给出的命令也会修改选主投票参数也会失败,这时可以直接手动修改 /etc/corosync/corosync.conf 删除掉有问题的节点对应的配置,调低 expected 投票数,使 corosync 能正常启动,再执行前述操作。 5. cloud image 的坑 ubuntu cloud image 的坑 ubuntu 启动时会报错 no such device: root,但是过一会就会正常启动。 这是 ubuntu cloud image 的 bug: https://bugs.launchpad.net/cloud-images/+bug/1726476 ubuntu 启动后很快就会进入登录界面,但是 root 密码可能还没改好,登录会报密码错误,等待一会再尝试登录就 OK 了 ubuntu 的默认网卡名称是 ens3,不是 eth0,注意修改 network_config 的网卡名称,否则网络配置不会生效 以 kvm 结尾的 Ubuntu Cloud Image 无法识别到 USB 设备,将 USB 端口映射到该虚拟机中没有任何作用。 kvm 使用了精简版的 linux 内核,去掉了 USB 等在云上用不到的驱动,建议改用无 kvm 结尾的镜像。 「Ubuntu Cloud Image 无法识别到 USB 设备」的排查记录 现象: 在尝试使用 PVE 将 USB 接口直通到 Ubuntu Cloud Image 启动的虚拟机作为 NAS 系统时,发现lsblk 根本无法找到我的 USB 硬盘 换成我笔记本接硬盘盒,能够正常识别并挂载硬盘 使用 lsusb 不会报错,但是也看不到任何内容 使用 lspci 能找到 USB 对应的 PCI 设备 进一步使用 cat /proc/modules | grep usb 与 lsmod | grep usb 均查不到任何 usb 相关的内核模块 而在我笔记本上 lsmod | grep usb 能够输出 usb_storage usb_core 等多项内核模块。 再用 modprobe usb 会提示modprobe: FATAL: Module usb not found in directory /lib/modules/5.15.0-1021-kvm 问题原因很明显了,Ubuntu 根本没有为 cloud image 预置 usb 内核模块,所以才有这个问题… 进一步搜索发现这个帖子:What’s the difference between ubuntu’s amd64-disk-kvm.img and the regular amd64.img cloud images?, 解答了我的疑惑。 原因是,我使用了 ubuntu 为 cloud 环境做了精简的 kvm 内核,非常轻量,但是缺少 usb 等常用内核模块。 对于 NAS 外接存储这个场景,我应该使用不以 kvm 结尾的 ubuntu cloud image,换了个基础镜像后问题就解决了~ opensuse cloud image 的坑 opensuse leap 15 只支持 network_config v1,对 v2 的支持有 bug,gateway4 不会生效 debian cloud image 的坑 debian 的 cloud 镜像根本没法用,建议避免使用它。 debian 启动时会彻底卡住,或者直接报错 kernel panic 原因是添加了 spice 图形卡,换成 vnc 就正常了 Debian Cloud Images 中的 nocloud 镜像不会在启动时运行 cloudinit,cloudinit 完全不生效 不知道是啥坑,没解决 6. 克隆创建的虚拟机,卡在 Booting from Hard Disk... 状态 被用做模板的虚拟机可以正常启动,但是克隆的虚拟机就卡住了。 可能的原因: 磁盘有问题,出这个问题的 cloud image 是 ubuntu-20.10-server-cloudimg-amd64.img,我更换成 ubuntu-20.10-server-cloudimg-amd64-disk-kvm.img 就没问题了。 磁盘镜像均下载自 https://cloud-images.ubuntu.com/releases/groovy/release-20201210/ BIOS 不匹配:将 BIOS 从 SeaBIOS 切换到 OVMF(UEFI) 如果仍然无法启动,请进入 OVMF 的 BIOS 界面关闭「Secure Boot」后再重启看看 7. 虚拟机启动时 cloudinit 报错 failed to start OpenBSD Secure Shell server 有如下几种可能: 可能性一:虚拟机名称包含非法字符 pve 的 cloudinit 配置会在启动时尝试将虚拟机 hostname 修改为与虚拟机一致,但是又没有对虚拟机名称做合法性校验… 当你使用的虚拟机名称包含了非法字符时就会出这个问题,比如ubuntu-22.10-cloudimage-template,其中的 . 就是非法的, . 在 DNS 中用于划分不同的域! 解决方法:克隆个新虚拟机并改用合法名称,再删除旧虚拟机,问题就解决了。 可能性二:磁盘空间不足 qcow 镜像转换成的虚拟机磁盘很小,只有 2G,如果不扩容,启动时就会出各种奇怪的问题。 解决方法:通过 Web UI 扩容磁盘大小,建议至少给 32G。 8. 修改 Linux 虚拟机的 Hostname 如前所述,pve 的 cloudinit 配置会在启动时尝试将虚拟机 hostname 修改为与虚拟机一致,这导致手动修改无法生效无效。 解决方法:从旧的虚拟机克隆一个新虚拟机,将新虚拟机名称设为你期望的 hostname,然后删除旧虚拟机,启动新克隆的虚拟机,即完成了 hostname 重命名。 9. 虚拟机迁移时报错 Host key verification failed 社区相关帖子:https://forum.proxmox.com/threads/host-key-verification-failed-when-migrate.41666/ 这通常是因为节点增删,或者不小心动到了 ~/.ssh/known_hosts 文件,导致的问题。 可以通过手动在每台节点上执行如下命令解决: shell ssh -o 'HostKeyAlias=<Target node Name>' root@<Target node IP> 注意将上述命令中的 Target node Name> 改为节点名称,将 <Target node IP> 改为节点 IP 地址。 10. PVE 的 vm 不支持 vmx/svm 虚拟化指令集 在 Linux 虚拟机中运行如下命令: shell egrep '(vmx|svm)' --color=always /proc/cpuinfo 有输出则说明此虚拟机本身也支持 vmx/svm 虚拟化指令集(vmx 是 intel 指令集,svm 是 amd 的指令集)。 如果没有任何输出,说明此虚拟机不支持嵌套虚拟机,无法在其内部运行 Hyper-V 或者 kvm 虚拟化程序。 一般来说 PVE 宿主机默认就会启用嵌套虚拟化功能,可通过如下指令验证: shell # intel 用这个命令,输出 Y 则表示启用了嵌套虚拟化 cat /sys/module/kvm_intel/parameters/nested # amd 用如下指令,输出 1 则表示启用了嵌套虚拟化 cat /sys/module/kvm_amd/parameters/nested 如果输出不是 Y/1,则需要手动启用嵌套虚拟化功能。 如果是 intel cpu,需要使用如下命令启用嵌套虚拟化功能: shell ## 1. 关闭所有虚拟机,并卸载 kvm_intel 内核模块 sudo modprobe -r kvm_intel ## 2. 启用嵌套虚拟化功能 sudo modprobe kvm_intel nested=1 ## 3. 保存配置,使嵌套虚拟化功能在重启后自动启用 cat <<EOF | sudo tee /etc/modprobe.d/kvm.conf options kvm_intel nested=1 EOF 如果是 amd cpu,则应使用如下命令启用嵌套虚拟化功能: shell ## 1. 关闭所有虚拟机,并卸载 kvm_intel 内核模块 sudo modprobe -r kvm_amd ## 2. 启用嵌套虚拟化功能 sudo modprobe kvm_amd nested=1 ## 3. 保存配置,使嵌套虚拟化功能在重启后自动启用 cat <<EOF | sudo tee /etc/modprobe.d/kvm.conf options kvm_amd nested=1 EOF 上面这么一堆操作后,宿主机就已经启用了嵌套虚拟化,但是虚拟机内部却仍然不一定能有虚拟化指令集。 根本原因是 PVE 默认使用 kvm64 这种虚拟化的 CPU 类型,它不支持 vmx/svm 指令集!将虚拟机的 CPU 类型改为 host,然后重启虚拟机,问题就解决了。 11. 如何在多台主机间同步 iso 镜像、backup 文件 PVE 自动创建的备份,默认都只会保存到本机的 local 分区中,那万一机器出了问题,很可能备份就一起丢了。为了确保数据安全,就需要做多机备份,或者将数据统一备份到另一个 NAS 系统。 我考虑了如下几个备份方案: proxmox-backup-server:proxmox 官方推出的一个备份工具,使用 rust 编写。 它的主要好处在于,支持直接在 proxmox-ve 中将其添加为 cluster 级别的 storage,然后就可以通过 PVE 的定时备份任务,直接将数据备份到 proxmox-backup-server 中。但是我遇到这么几个问题,导致我放弃了它: 一是我想直接把数据通过 SMB 协议备份到 Windows Server 远程存储中,但是将 SMB 挂载磁盘用做 proxmox-backup-server 的 Datastore 会出问题,备份时 pbs 会创建一些特殊的临时文件,可能要用到 SMB 挂载插件不支持的特性,导致操作会失败。 二是我的 proxmox-backup-server 跟 Windows Server 都跑在 proxmox 虚拟机里面,那它就不能备份它自己,一备份就会卡住。 cronab + rclone/rsync: 极简方案,使用 crontab 跑定时脚本,用 rclone/rsync 同步数据。流程大致如下: 首先在 PVE DataCenter => Backup 中创建一个定期备份任务,将所有 vm 都备份到 local 存储中,它实际就存储位置为宿主机的 /var/lib/vz/dump。 通过 crontab 定时任务跑脚本,使用 rclone 将每个节点的 /var/lib/vz/ 中的文件全部通过 SMB 协议同步到 HDD 中。crontab 的运行时间设置在 PVE 完成后为最佳。并且将同步指标上传到 victoria-metrics 监控系统,如果备份功能失效,监控系统将通过短信或邮件告警。 /var/lib/vz/ 中除了备份文件外还保存了 iso 镜像等文件,这里也一起备份了。 restic: 一个更专业的远程增量备份工具,通过 rclone 支持几乎所有常见协议的远程存储(s3/ssh/smb 等),支持多种备份策略、版本策略、保留策略, 支持加密备份。 restic 看着确实挺棒,但是感觉有点复杂了,很多功能我都不需要。PVE 自带的备份功能已经提供了备份的「保留策略」,我这里实际只需要一个数据同步工具。因此没选择它。 如上文所述,一番研究后我抛弃了 proxmox-backup-server 与 restic,最终选择了最简单的 cronab + rclone 方案,简单实用又符合我自己的需求(仅我个人的选择,建议结合需求自行抉择)。 同步脚本也很简单,首先通过 rclone config 手动将所有 PVE 节点加入为 rclone 的 remote,再将我的 smb 远程存储加进来(也可以手动改 ~/.config/rclone/rclone.conf)。 这个方案最大的缺点是,所有备份都需要保存在每台节点的 local 卷中,所以有必要给 local 分配较大的磁盘空间,不然机器多的话很快就满了… rclone 配置好后,我写了个几行的 shell 脚本做备份同步: shell # 我的三台 pve 节点,对应的 rclone remote 名称 declare -a pve_nodes=( "pve-um560" "pve-gtr5" "pve-s500plus" ) # crontab 执行任务,需要指定下配置文件的绝对路径 for node in "${pve_nodes[@]}"; do rclone sync --progress \ --config=/home/ryan/.config/rclone/rclone.conf \ ${node}:/var/lib/vz \ smb-downloads:/Downloads/proxmox-backup/${node}/ done # TODO 上传监控指标到监控系统,用于监控任务是否成功。 然后手动执行 /bin/bash /home/ryan/rclone-sync-to-nas.sh > /home/ryan/rclone-sync.log 看看是否运行正常。 运行没问题后,再添加这么一个每天晚上 5 点(UTC 21 点)多执行的定时任务进行同步,就完成了: shell # 为了均衡负载,建议分钟值随便填个奇数。 17 21 * * * /bin/bash /home/ryan/rclone-sync-to-nas.sh > /home/ryan/rclone-sync.log 可以把运行时间调整到 1 分钟后确认下效果,如果要看实时日志可以用tail -f /home/ryan/rclone-sync.log 查看。 如果任务未执行,可以通过 sudo systmctl status cron 查看任务执行日志,排查问题。 12. 使用 cloud image 创建的虚拟机扩容磁盘并重启后,文件系统未自动扩容 这个我遇到过几次,都是因为磁盘容量用尽,导致 cloudinit 扩容脚本运行失败,只要手动回收些空间,再重启系统,就能自动扩容。 我试了只要能确保系统还剩余 100M 左右的存储空间就能正常扩容了,更低的还没试过。 如果数据实在不能清,也可以考虑手动扩容,有两种方法: 直接使用 growpart /dev/vda 1 进行扩容。第一个参数是磁盘路径,第二个参数是分区号,这里是 1,表示扩容第一个分区。此命令会同时扩容分区和文件系统。 用 fdisk 先删除分区,再重新创建分区,实现修改分区表扩容。然后还需要用 resize2fs 扩容文件系统。细节请自行网上搜索文档。 四、PVE 网络配置 1. 桥接多张物理网卡 示例如下,主要就是在 vmbr0 网桥的 Bridge Ports 里面: 桥接多张物理网卡 2. 手动添加 USB 物理网卡 参考官方文档:SysAdmin - Network Configuration 我遇到这个问题的场景是:我的 mini 主机(GTR5)只有两个 2.5G 网卡,不太够用。而家里的路由器剩下的都是千兆网口,路由器也难以拓展网卡。网上搜了下 2.5G 交换机又发现价格 429 起步,所以决定买两张 USB 2.5GbE 网卡插在这台小主机上作为便宜的网口拓展方案。 现在网卡有了,有两种方式可以让 PVE 识别到这张网卡: 好像 PVE 偶尔也能自动识别到网卡,就是比较慢… 方法一:直接重启机器,然后就能在 Web UI 的 Network 配置中见到这张 USB 网卡了。之后直接把该网卡加入到 vmbr 网桥的 Bridge Ports 中并应用配置,就大功告成了。 方法二:不重启机器实现添加 USB 网卡。如果机器不能重启,就可以走这个流程: 首先,使用 ip link 命令打印出当前的所有网络接口 将 2.5GbE 网卡插到 USB3.0 端口上,Linux 将自动识别到它 现在再使用 ip link 命令查看所有网络接口,找到新增的接口名称(通常在输出内容最末尾)。 在我的环境中新的 USB 网卡名称为 enx00e04c680178 在配置文件 /etc/network/interfaces 的末尾新增一行:iface enx00e04c680178 inet manual(注意替换网卡名称) 现在直接刷新 Web UI 页面, USB 网卡就会出现了。之后直接把该网卡加入到 vmbr 网桥的Bridge Ports 中并应用配置,就大功告成了。 3. 配置 WiFi 网卡 如果主机自带了 WiFi 网卡,启动后 Proxmox VE 能识别到该网卡,但是无法通过 Web UI 修改它的任何配置。 那么本着物尽其用的精神,该如何利用上这张 WiFi 网卡呢? 根据 PVE 官方文档 WLAN - Proxmox VE Docs,并不建议在 PVE 上使用 WLAN,它存在如下问题: WiFi 自身必须是一个 Linux Bridge 设备,无法被桥接到 vmbr0 等网桥上。因为大多数 Access Point 都会直接拒绝掉未授权的源地址发过来的数据包… 与有线连接相比,WiFi 的延迟要高得多,而且延迟波动较大。 因此仅建议在不得已的情况下,才使用 WiFi 网卡. 如果要配置 WLAN 网卡的话,官方建议直接参考 Debian 的官方文档进行配置:How to use a WiFi interface - Debian,不过这里也找到一篇中文博客: proxmox中使用ax210连接无线网络 - 佛西博客 五、提升 PVE 的安全性 1. 配置 ACME 证书并使其自动更新 对于个人使用而言,不配置证书好像也 ok,虽然访问 Web UI 时浏览器会提示不安全,但也不影响使用。 如果你拥有自己的域名,同时也期望更高的安全性,根据Certificate Management - 官方文档,pve 可借助 acme.sh 进行证书的申请与自动更新。 TODO 2. SSH 禁用密码登录 pve 的 ssh 默认是启用了密码登录的,为了安全性,建议上传 ssh 密钥改用密钥登录,并禁用密码登录功能。 详见Linux 主机安全设置.md - ryan4yin 3. 用户管理 PVE 支持对接多种授权协议,对于个人使用而言,直接使用 Linux PAM 是最简单的。 即使是在内网,为了安全性,也建议设置复杂密码,同时所有虚拟机也建议仅启用密钥登录,所有 Web 页面都建议设置复杂密码。(特别是家里没有访客网络的时候…) 六、PCI(e) 直通(显卡、硬盘、USB 设备等) QEMU/KVM 的 PCI(e) 直通功能可以让虚拟机独占指定的 PCI(e) 设备,越过宿主机控制器直接与该 PCI(e) 设备通信。 相比使用 QEMU/KVM 提供的 virtio 半虚拟化硬件,PCI(e) 直通有如下优势: 大大提升虚拟机与 PCI(e) 设备的 IO 性能(更低的延迟,更高的速度,更低的资源占用)。 可以利用上 QEMU/KVM 本身不支持的硬件特性,比如 PCI 直通最常见的使用场景——显卡直通。 那么最常见的 PCI(e) 直通需求有: 显卡直通,实现在内部 windows 主机中用宿主机显卡看影视、玩游戏、剪视频 硬盘或 USB 直通,以提升硬盘或 USB 的 IO 性能。 首先列举下相关的文档: PCI(e) Passthrough - Proxmox WIKI. GPU OVMF PCI Passthrough (recommended) - Proxmox WIKI QEMU/Guest graphics acceleration - Arch WIKI TODO 实操内容待补充… 拓展 - cloudinit 高级配置 PVE 使用 CDROM 只读盘(/dev/sr0)来进行 cloud-init 的配置。在虚拟机启动后,/dev/sr0 将被卸载。 可挂载上该只读盘,查看其中的初始化配置内容: shell $ mkdir cloud-config $ mount /dev/sr0 cloud-config mount: /dev/sr0 is write-protected, mounting read-only $ ls cloud-config meta-data network-config user-data 查看其中内容,会发现 user-data 有很多参数都被硬编码了,没有通过 PVE Web Console 暴露出来,导致我们无法自定义这些配置。 比如它硬编码了 manage_etc_hosts: true,强制每次都使用虚拟机的名称作为 hostname. 如果确认有修改这些配置的需求,完全可以修改掉 PVE 代码里的硬编码参数。通过全文搜索即可找到硬编码参数的位置,以 manage_etc_hosts 为例: shell # 在 /usr/share 中全文搜索 manage_etc_hosts 这个关键字 grep -r manage_etc_hosts /usr/share 直接就搜索到了硬编码位置是 /usr/share/perl5/PVE/QemuServer/Cloudinit.pm,修改对应的 cloudinit 配置模板,然后重启节点(重启才能重新加载对应的 ruby 程序),即可实现对该硬编码参数的修改。 拓展 - 自动化配置与监控告警 自动化配置相关工具: Telmate/terraform-provider-proxmox: 用户最多,但是只支持管理虚拟机资源 danitso/terraform-provider-proxmox: stars 少,但是可以管理 PVE 的大部分资源,包括节点、用户、资源池、TLS证书等等 代码更顺眼,但是作者忙,没时间合并 pr,导致 Bug 更多一些,而且很久没更新了… ryan4yin/pulumi-proxmox: 我维护的一个 proxmox 自动配置工具(很久没更新了…) Python SDK 监控告警: prometheus pve expoter: 通过 prometheus+grafana 监控 PVE 集群 拓展 - PVE 运行在 ARM 开发版上 PVE 官方目前还未推出 ARM 支持,但是社区已有方案: pimox7 安装Arm版本的Proxmox VE - 佛西博客 Proxmox-Arm64 proxmox 社区比较活跃,建议多在社区内看看相关进展。 拓展 - 其他 QEMU/KVM 相关的虚拟化平台 PVE 毕竟是一个商业系统,虽然目前可以免费用,但是以后就不一定了。 如果你担心 PVE 以后会不提供免费使用的功能,或者单纯想折腾折腾的技术,还可以试试下面这些虚拟化平台: webvirtcloud: 其前身是 webvirtmgr,一个完全开源的 QEMU/KVM Web UI,额外提供了用户管理功能。 kubevirt: 基于 Kubernetes 进行虚拟化管理 rancher/harvester: Rancher 开源的基于 Kubernetes 的超融合平台(HCI) 其底层使用 kubevirt 提供虚拟化能力,通过 longhorn 提供分布式存储能力。 HCI 超融合 = 计算虚拟化 + 网络虚拟化 + 分布式存储,它和传统的虚拟化软件最大的不同是: 分布式存储。 企业级场景下一般至少得 10GbE 网络 + SSD 才能 hold 住 HCI 超融合架构。 超融合对存储的一些要求: 软件定义 – 解除硬件绑定,可通过升级拓展更丰富的功能,自动化能力高 全分布式架构 - 扩展性好,消除单点故障风险 高可靠性 - 智能的故障恢复功能,丰富的数据保护手段 高性能 – 支持多种存储介质,充分挖掘和利用新式硬件的性能 高度融合 – 架构简单并易于管理 超融合架构可以降低私有云的构建与维护难度,让私有云的使用维护和公有云一样简单。 超融合架构下,虚拟机的计算和存储是完全高可用的:计算资源能智能动态更换,存储也是分布式存储,底层计算和存储也可以很简单的扩缩容。 我打算有时间在 PVE 集群里跑个 rancher/harvester 玩玩 emmmm 参考 KVM 虚拟化环境搭建 - ProxmoxVE KVM 虚拟化环境搭建 - WebVirtMgr Proxmox Virtual Environment - Proxmox WIKI QEMU - Arch Linux WIKI 佛西博客 - PVE 相关: 这位博主写了很多 pve 相关的内容,而且比较有深度

2022/11/27
articleCard.readMore

刻意练习

我最近理解到一个事实——许多知识或者技能,都是可以通过正确的学习方法,加上短时间大量的练习,就能达到 60 分及格标准的。而这个及格水平,相对于完全没有进行过这样训练的其他人而言, 可能就已经很惊艳了。 如果你总是半途而废,可能只是你受了快餐式短期快乐的诱惑而放弃,或者你潜意识觉得它并不重要从而无法坚持。 佐证之一 - 英语口语 受限于国内英语教育的方法,很多的同学朋友口语发音都比较糟糕。但我实践发现,纠正自己的口语发音,达到到 60 分水平,并不是一件很难的事。真正的问题在于,绝大多数人从没有去查过相关资料、并进行大量的练习。 我大三之前发音也惨不忍睹,然后因为个人兴趣与需求想把英语学好,就查了很多资料,跟着恶魔奶爸给出的英语口语学习方法,花了大概一个月的时间专门练了一波,效果立竿见影。 佐证之二 - 练字 我堂弟之前分享过他练字的经历给我。 他大学之前的字跟我一样丑不拉几。大学期间因为无聊,就专门查了很多资料,定下了练字计划。统共就练了两三个月,现在他写的字的美观程度,已经吊打我这个菜鸡。 佐证之三 - 减肥 我有一个同村的朋友,也是我小学同学。他从初中开始就胖得不行,一直到结婚工作,体重都没减下来。 但是在去年因为婚姻变故以及一些其他原因,比较失意,突然就打算减肥,就开始坚持跑步。 跑多远忘了,可能慢慢加量到每天十公里吧。他晚饭不吃,饿了就疯狂喝水。一开始因为体重太高,一趟下来膝关节直接跑到浮肿。 就这样坚持了大概三个月,直接瘦了 50 斤,整个人清爽帅气太多了。 结语 经常听人说要「踏出舒适区」、「延迟满足」,其实是一个道理。 即使掌握了正确的练习方法,如果因为坚持了三天发现没效果,就坚持不下去了,那无论如何都不会有好的效果。可如果把这个时间放大十倍——三十天,视目标的难度,就会开始出现比较明显的效果了。 如果你对自己比较狠(高强度练习),再把坚持的时间再放大到三个月,你的技能水平就能获得相当显著的提升!就像我那位减肥的朋友、我练字的堂弟一样! 三个月的时间,对于学习一项技能而言,真的是很短的一段时间,但是却有能力使你入门一项终生受益的技能。对未来的自己好一点,多投资投资自己,诸君共勉。 参考 Deliberate Practice: Achieve Mastery in Anything - Youtube

2022/10/5
articleCard.readMore

Learn English Again

一、缘起与学习目标 工作了三年多了,我的英语阅读水平大致一直处在「能较流畅地阅读各类技术文档,但是阅读与理解速度不够快」的程度。工作以来没有专门去学过英语,不过会有意识地尽量去阅读英文技术文档、英文技术书籍,或者在 Youtube 上找一些国际技术会议视频学习(如 KubeCon、IstioCon),所以这三年多我的英语水平提升应该是有一个缓慢的提升。 这周五的时候(是的就是 2022-09-02),偶然发现自己手机里还装了个英语流利说 APP,安装了两年多但一直没碰过它 emmmm 突然来了兴趣就打算试用一波,由此开始了我的重学英语之旅… 首先肯定是要测试一下自己的英文水平,确定英语学习的起点。流利说 APP 把英语分成 7 个 Level, 它评价我属于 Lv.5,大致对应 CEFR 评级 B2。五个评级维度中我的发音是最好的,口语是最差的。而词汇量大概在 5000 这个档位(我觉得如果算上我懂的计算机名词可能会更高些 emmmm)。 测出的结果跟我的自我感觉基本吻合。之前有跟 AWS 工程师做过几次英语沟通,发现我的口语勉强可以支撑日常技术沟通,但是感觉很费劲,原因显然是口语基本没怎么练过。而我的发音之所以还不错, 主要是大三的时候专门跟着奶爸推荐的《赖世雄美语音标》、《American Spoken English》等资料练习过,我当时还写过篇文章讲这个——学英语啊学英语。 先不提细节,总之我就是这么着,突然对学英语又来了兴趣~ 当天就深入探索了下英语流利说这个 APP,又花 49 元买了一个月的「懂你英语 A+ 个性化学习计划」,这个计划还给分配微信学习社群跟班主任,每天坚持 30 到 60 分钟。第一天体验这个学习计划,感觉确实跟我比较契合,而且也不枯燥,打算坚持一个月试试。 最后,为了更有目的性地提升自己的英语,我总结了下我此次「重学英语」的目的,以及当前的水平 (按优先级排序): 能流畅地用英语交流:流利说测试显示我这项技能很差,亟待提升。 流畅阅读各类英文资料:我目前可以无障碍阅读大多数编程相关的英文博客跟文档,但是阅读速度不够快,有些长句经常要读好几次才能理解大意。另外词汇量偏低,非技术类的资料我读起来非常吃力。 无障碍看美剧:我的听力水平跟词汇水平大概差不多,看美剧还是得有个英文字幕,还需要提升。 写英文博客:高中毕业后就没学过语法,也没怎么练过写作,词汇量又低,我的写作能力显然还有很大的提升空间。 二、英语自学路线完全指南 因为最近对英语来了电,就打算深入了解下市面上常见的英语学习路径,争取找到最佳学习路线。 1. 不花钱的自学路线 首先是不花钱的自学路线,这个我在大三时有过实践,当时打算考研,专门练过一波英语,大致路线是这样的: 恶魔奶爸总结的 练习英语口语的办法,本人实测效果显著 其中介绍的 ESLPod、EnglishPod 等材料,不仅可以提升语感,也能帮助提升听力。 如果是进阶的话,可以去 Youtube 上看一些流行的 Talk Show,这类视频很有意思、语速快信息量又大,是很合适的进阶学习材料 以看视频的方式学英语,一个要点就是「重复」或者说「精听」。 「一点英语」就是在这上面下了不少功夫,要选那种第一遍听有难度的视频,最好内容还比较有趣 (比如 Talk Show 就比较有意思)。 可行的学习方法:第一遍带字幕看视频,但是尽量别看字幕;第二遍带字幕反复精看听不懂的部分,第三遍再完全关掉字幕验证学习效果。 还有就是很多人推荐的阅读英文原版书 一开始可能会读得很慢,这个时候最重要的就是坚持!阅读量上来了,速度自然就提升了。 让自己更容易坚持的阅读方法: 要找一些你感兴趣的、难度适中的原版书(比如感兴趣的小说、专业相关的技术书等)。因为阅读的首要点是要能感受到乐趣,否则会很枯燥,很难坚持。 然后要做好规划并坚持执行,比如每天读 10 页。 当然你也可以走传统的背单词、词组、例句这样的路线,这条路线的好处是能快速提升自己的英语能力,但是相对的也很难坚持。 常见的有新东方的词汇录播课(网上能找到免费的资料分享),以前听过新东方朱伟老师的《恋练有词》,感觉挺不错的。 不背单词、百词斩这类 APP 也可以试试,不过我个人体验是基本坚持不了多久… 每日英语听力 跟欧路词典是同一家公司做的,很良心。 不过因为我已经买了其他课程了,这个 APP 目前用得比较少。 每日英语听写 Daily English Dictation 1-400 这是 Youtube 上相当火的一个英语听写系列视频,做得非常棒,坚持听完 400 期,听力就神功大成了! 个人感觉起码得有个 CET4 水平,才能比较舒服地跟这个课程。 一些对话、角色设定、世界观设定较多的游戏也是学英语的好材料,比如「Genshin Impact」、「DEEMO 2」,或者一些 Galgame 都可。娱乐跟英语两不误 emmm Genshin Impact 里超飒的重剑女仆 Noelle DEEMO 2 中丰富的对话内容 DEEMO 2 的一些设定 上面主要是针对 CET4 四级以上水平的英语学习者,如果基础比较差,可以考虑按照恶魔奶爸推荐的零基础英语学习路线进行自学: 英语:壹章 - 恶魔奶爸:针对零基础/基础超级不好的同学 英语:贰章 - 恶魔奶爸:针对处于英语初级阶段的同学 英语:叁章 - 恶魔奶爸:针对大学四六级水平的同学 (就跟我上面列的差不多了) 2. 花钱抄捷径 而对于我这种上班族或者其他有闲钱的学生,而且不求快速提升英语能力的人而言,如果愿意花点小钱,也是有捷径可以抄的。 首先就是线下课程跟线上视频课程(比如新东方、山姆英语),这个没时间参加,时间安排也不够灵活,而且可能会比较枯燥,直接 Pass. 其次就是英语学习类 APP 了,这个市面上还有挺多的,我试用了一波后感觉很适合我,并且筛选出了下面几个比较中意的 APP:欧路词典、知米阅读、开言英语以及一点英语。 并且使用上述几款软件提供的英语能力测试,完整评估了下我的英语水平。下图从左至右依次是一点英语、开言英语,以及英语流利说的整体英语水平跟口语水平两项测试结果(结果偏高,仅供参考): 各 APP 的英语水平测评结果(一点英语的结果明显偏高) 流利说英语评级对照表 下面详细介绍下我比较推荐的英语学习类 APP 跟课程: 欧路词典 最牛逼的词典软件,非常适合用于日常查词。 最强的一点在于它能导入各种离线词典,详见恶魔奶爸有分享的词典文章, 我用到的有如下几本: 中英双解词典 柯林斯双解 Collins Cobuild Advanced Learner’s English Dictionary:包含中文翻译、英英释义跟句子示例,还提供 1-5 的单词使用频率标识 Microsoft Bing Dictionary:是网友抓取制作的必应大词典,跟 Collins Cobuild 类似,包含级别(CET4/CET6/GRE)、中文翻译、英英释义跟句子翻译 英英词典 LDOCE朗文当代5 英英版: 光体积就 1G,包含了所有单词例句的录音,是非常详尽的一本英英学习词典。收词量相当大,短语、用法完备 词组搭配、同义词词典 牛津搭配词典第二版 Collins Thesaurus Collins COBUILD English Usage WordNet 3.0 USE THE RIGHT WORD: 同义近义词用法解释,很详细 Merriam-Webster’s Vocabulary Builder: 通过词根词缀来关联起各种相关单词 英语常用词疑难用法手册 薄荷阅读:199 元 / 100 天 阅读原版书学习英语 用它辅助阅读原版书的好处有: 会提前帮你选出生词进行预习 提供配套的有声书,可以听书也可以看书 随时双击查词,查词非常方便 学习完成后还有提供配套的讲义,对各项单词、词组、疑难句进行详细解释,这个感觉会非常有用! 缺点 每逢购物活动就开始打广告,让人有点烦。 相比后面的竞品,价格有点太高了… 知米阅读:199 元,365 天畅读所有书单 阅读原版书学习英语 跟薄荷阅读几乎一样的模式,功能大同小异。 但是它的年费会员仅为薄荷阅读 VIP 的 1/5,而且还能畅读所有书单! 个人感觉它的小程序设计感比薄荷阅读更好 一点英语: 折后 1998 元 270 天 如果坚持打卡 270 天,期末还返 1000 现金 + 500 续课券(实测大半年下来意外太多,基本不可能拿到、但是万一就成功了呢…) 通过「演讲/动漫/喜剧/音乐现场」等各种视频内容学习英语单词,同时练习听力跟口语 相比其他 APP,它的课程会有趣很多,不那么枯燥。我觉得用它练习听力非常合适。 缺陷: 它的视频语料有部分跟单词有点对不上,有时候会遇上,不过无伤大雅。 它背单词的功能跟「不背单词」或者「欧路词典」差距较大。其中英解释比较简陋、记忆算法强度没不背单词高,也没有词根词缀功能,此外例句偶尔会有些错误。 开言英语 通过外教录制的视频 + 程序控制实现了一定的可交互性,感觉课程比英语流利说做得要有趣些。 开言英语的 WeMeet 英语角体验真的超棒,而且还免费! 第一次参加时,一起聊天的两位朋友,一位在万豪酒店工作了差不多 10 年,另一位是产品经理,在腾讯、搜狐、美团等公司工作了差不多 15 年,聊得非常开心,很有收获! 价格 外教系统课是 2598 一年,提供海量外教视频互动课跟音频课 还有个外教视频课,互动性会更高,价格是 1598 一年 《潘吉 Jenny 告诉你》这个口碑在,我觉得开言英语的外教课程是可以考虑的 新东方在线 在线录播课、直播课,从以前学过的考研英语看,还是很专业、很成体系的。 对 BEC、雅思、托福都有对应的课程可选,价格对工作党而言问题不大。 VoiceTube:价格还不太清楚 应该是最早做「看视频学英语」功能的 APP 之一,它最初发布于 2013/09,而前面介绍的「一点英语」最初发布于 2018 年(而且 UI 布局也跟 VoiceTube 很类似)。 获得了 Facebook 2016 年年度最佳 APP 奖项 但是(因为版权原因)它基本都是直接内嵌的 Youtube 视频,它需要同时安装 Youtube(同时还需要一些使你能访问 Youtube 的特殊手段)。 另外一些我用过但是不推荐的 APP 也简单说下问题所在: 英语流利说 懂你英语 A+ 课程质量确实不错,但是全都是 AI 语音,虽然很标准,但是太机械了,不带啥感情,我学了才 6 天就开始厌倦了。个人不推荐选它 百词斩 更适合用于趣味性背单词 对很多学生党(比如大学期间的我)而言这些 APP 一年下来好几千的费用确实是难以承担,但是对我这样的工作党而言,不失为一个很好的学习手段。 上述这些软件最大的优势是「互动」,这会使它比单纯自学有趣很多,更容易坚持。 除薄荷英语外,其他三个 APP 都有提供长短不一的会员体验时间,有兴趣的话可以自己试用下~ 总结下,目前已买课程: 「知米阅读」199 元,365 天畅读所有书单。看的第一本书是「Time Machine」。阅读体验很不错,而且比薄荷阅读便宜太多了。 「一点英语」看视频/听歌学英语真的非常棒!已经花 1998 元买了 270 天的会员 争取每天打卡,赚到 1000 现金返还目前(2022-11-07)进度是 62 天,已经漏打卡 5 天… 部分原因是漏打卡第一天后,争取每天打卡的执念就没那么强烈了,偶尔哪一天太累了或者别的事没搞完,就直接不打卡了… 「薄荷英语」花了 199 元买了《一个人的朝圣》这个原版书,书已经看完了。内容不错,就是跟知米阅读比太贵了。 「英语流利说」花了 49 元,买了个 30 天的懂你英语 A+ 体验课,但是因为前述原因(AI 语音太死板,听多了烦)就用了一周就没继续用了。 三、如何练习英语口语? 首先,上面已经提过了,「开言英语」的线上英语角 WeMeet 非常的棒。这个功能是免费的,但是需要交 5 块钱押金,我觉得这 5 块钱的押金大大提升了这个英语角的队友质量,能确保大家都是有强烈意愿去用英语聊天的。实际的体验也非常棒,不仅能练习口语,还能交到各行各业喜欢英语的朋友。 其次,如果你的上网工具能使你用上 Discord,也有一些英语学习的 Discord Server 非常值得推荐, 你能在上面跟各种母语、各个国家、各种口音的朋友一起用英语聊天,体验还不错,还能交到有意思的国外朋友。 英语学习 Discord Server 地址: https://discord.gg/english https://discord.gg/c-e 加入 Server 后要往下翻很多页才能到 English Practice 部分,目前分别有 8 个「Beginner English」聊天室跟 8 个「Intermeidate/Advanced English」聊天室,赶快加入开聊吧~ 此外,如果你想走高效的氪金路线,也有很多线上真人上外教平台供你选择,主要有如下几个: Lingoda: 被认为是最佳成人英语学习平台之一,它有部分课还提供了学费返还机制,可以了解下。 Cambly: twitter 上推友推荐的,最流行的英语学习平台之一,真人外教一对一教学 据说打折时很便宜,优惠价只要人民币 30 元 30 分钟 Preply: 我用的词汇测试站点,现在就属于这家公司的产品。据说比较专业,提供 4-5 人小组教学跟一对一教学两种学习模式 貌似比较贵:$15-20 per hour 你还可以在这个站点上面应聘汉语教学(有 20%-30% 的抽成,150 美刀才能提现) Verbling: 一样也是一个老牌语言学习站点 据说价格比较优惠,还支持免费试课。 跟 Preply 一样你也可以在这个站点上面应聘汉语教学作为兼职。 ITalk: 国内创办的比较大的语言交换平台,据说是最 casual 的学习平台,所以不太适合自我驱动力不强的朋友。 四、总而言之 嗯,毕竟是花了钱的,这次的大目标就是用近一年的时间,全面提升英语水平。 听说考 BEC 高级对英语水平提升帮助很大,为了提供一点压力与动力,就定一个硬性目标: 2023 年达到 CEFR 的 C1 等级,报考并取得 BEC 高级证书 2023 年底词汇量超过 10000 其他资料 文末再分享几篇我收藏的英语学习相关的资料,个人感觉读了有些帮助: 编程的技术资料好多都是全英文的该怎么学习? 关键点总结:你要能接受自己不能 100% 看懂资料 如何做到流畅阅读英文资料和听懂国外公开课? 提升阅读速度过程中一个最基本的因素就是阅读量,跳出舒适区,刻意练习。 如何有效提高英语写作? 关键点总结:提高英文写作水平的第一步是「大量阅读」 YouTube 上有哪些质量不错的英文教学视频和英文视频? 这些视频很多都能在 Bilibili 上搜到 通过「听写」学习英语,效果怎么样? 英语学习过程中如何有效地提升词汇功底? 关键点总结:学习一门语言文字的最短最佳途径,是掌握它的词根(root),也就是那些其他单词借以形成的原生词 知乎「英语学习」话题下的精华回答 学习成果记录 - 持续更新 先简单记录下学习时间线: 2022/9 - 2022/12 平均每天大概花 30 分钟学习英语(偶尔兴之所至可能会花 1 小时甚至更多,也偶尔会断几天)。 主要学习方法就是通过「薄荷阅读」「知米阅读」读英文原版书,以及通过「一点英语」看视频练听力、记单词。 2023/1: 基本算是休息了一个月,没怎么学。 未完待续 1. 词汇量 词汇量测试结果按时间排序如下,使用的测试工具是Test Your Vocabulary : 2022 年 9 月初,测试结果:4700 词(无截图) 2022-10-18 词汇量测试结果:5100 词 2022-10-18 词汇量测试结果:5100 词 2022-11-17 词汇量测试结果:5600 词 2022-12-19 词汇量测试结果: 6300 词 2023-01-02 词汇量测试结果:6583 词 2023 年 1 月份暂停了学习,词汇量基本没涨,算是休息了一个月。 2. 口语交流能力 2022 年只是参与了两次「开言英语」的线上英语角 WeMeet,另外探索了下学习方法,但是时间原因未持续推进。 计划在 2023 年进行专门练习,未完待续… 3. 其他进展 写作能力 计划在 2023 年进行专门练习,未完待续… 阅读能力 2022 年的 4 个月里,阅读是我提升词汇量的主要手段之一,看统计读了有 15 万词,词汇量跟阅读速度都有明显提升。 在薄荷阅读上读完的第一本英语原版书

2022/9/4
articleCard.readMore

分布式数据库的一致性问题与共识算法

个人笔记,不保证正确! 谈到分布式数据库,不论是 Etcd/Zookeeper 这样的中心化分布式数据库,还是 Ethereum 区块链这样的去中心化数据库,都避免不了两个关键词:「一致性」跟「共识」。 本文是笔者学习「一致性」和「共识」以及相关的理论知识时记录的笔记,这些知识能帮助我们了解 Etcd/Zookeeper/Consul/MySQL/PostgreSQL/DynamoDB/Cassandra/MongoDB/CockroachDB/TiDB 等一众数据库的区别,理解各数据库的优势与局限性,搞懂数据库隔离级别的含义以及应该如何设置, 并使我们能在各种应用场景中选择出适用的数据库。 如果你对区块链感兴趣,那这篇文章也能帮助你了解区块链这样的去中心化数据库,跟业界流行的分布式数据库在技术上有何区别,又有哪些共同点,具体是如何实现。 一、一致性 - Consistency 「一致性」本身是一个比较模糊的定义,视使用场景的不同,存在许多不同的含义。由于数据库仍然是一个新兴领域,目前存在许多不同的一致性模型,其中的一些术语描述的一致性之间可能还有重叠关系,这些关系甚至会困扰专业的数据库开发人员。 但是究其根本,实际上在谈论一致性时,我们是在谈论事务一致性跟数据一致性,下面我们分别介绍下这两个一致性。 1. 事务一致性 - Transactions Consistency 「事务一致性」指的是数据库中事务的一致性,它是 ACID 理论中最不起眼的特性,也并不是本文的重点。但是这里就写这么一句话也说不过去,所以下面就仔细介绍下事务与 ACID 理论。 事务与 ACID 理论 事务是一种「要么全部完成,要么完全不做(All or Nothing)」的指令运行机制。 ACID 理论定义,拥有如下四个特性的「数据库指令序列」,就被称为事务: 原子性 Atomicity:事务是一个不可分割的工作单元,事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在某个中间状态。 比如 A 转账 100 元给 B,要么转账失败,要么转账成功,不可能卡在 A 被扣除了 100 元,而 B 还没收到 100 元的中间状态。 原子性在单机数据库上已得到妥善解决,但是在分布式数据库上它成为一项新的挑战。要在分布式架构下支持原子性并不容易,所以不少 NoSQL 产品都选择绕过这个问题,聚焦到那些对原子性不敏感的细分场景。 一致性 Consistency:也叫数据的「正确性 Correctness」或者完整性,指事务对数据库状态的变更必须满足所有预定义的规则,包括「约束 constraints」、「级联 cascades」、「触发器 triggers」以及这些规则的任何组合。 比如如果用户为某个字段设置了约束条件 unique,那么事务对该表的所有修改都必须保证此约束成立,否则它将会失败。 是存在感最低的特性 隔离性 Isolation:并发执行的多个事务之间是完全隔离的,它们的执行效果跟按事务的开始顺序串行执行完全一致。 事务中最复杂的特性 持久性 Durability:事务执行完毕后,结果就保存不变了。这个最好理解。 ACID 是传统的单机数据库的核心特性,比如 MySQL/PostgreSQL. ACID 中最复杂的特性 - 隔离性 完全地实现 ACID 得到的数据库,性能是非常差的。因此在关系数据库中,设计者通常会选择牺牲相对不重要的「隔离性」来获取更好的性能。 而一旦隔离不够彻底,就可能会遇到一些事务之间互相影响的异常情况,这些异常被分为如下几种: 脏写 Dirty writes:即事务 T1 跟事务 T2 同时在原数据的基础上更新同一个数据,导致结果不符合预期。 案例:两个事务同时尝试从账户中扣款 1000 元,但是它们读到的初始状态都是 5000 元,于是都尝试将账户修改为 4000 元,结果就是少扣了 1000 元。 最简单的解决方法:针对 UPDATE table SET field = field - 1000 WHERE id = 1 这类数据增删改的逻辑,需要对被更新的行加一把「行写锁」,使其他需要写此数据的事务等待。 脏读 Dirty reads:事务 T1 读取了事务 T2 未提交的数据。这个数据不一定准确,被称为脏数据,因为假如事务 T2 回滚了,T1 拿到的就是一个错误的数据 案例:假设小明小红在一个银行账户存了 5000 元,小明小红在用这同一个账户消费 1000 元,这中间小明付款的事务读取到账户已经被小红的事务修改为了 4000 元,于是它把余额修改为 3000 元,然后付款成功。但是在小明的付款事务成功后,小红的付款失败回滚了,余额又从 3000 被修改回 5000 元。小明就完成了 0 元购的壮举。 最简单的解决方法:事务 T2 写数据时对被修改的行加「行写锁」,T2 结束后再释放锁,这样事务 T1 的读取就会被阻塞,直到锁释放。 不可重复读 Non-repeatable reads:事务 T1 读取数据后,紧接着事务 T2 就更新了数据并提交,事务 T1 再次读取的时候发现数据不一致了 案例: 小明在京东上抢购商品,抢购事务启动时事务读到还剩 36 件商品,于是继续执行抢购逻辑,之后事务因为某种原因需要再读一次商品数量,结果发现商品数量已经变成 0 了,抢购失败。 更麻烦的是,不可重复读导致 SELECT 跟 UPDATE 之间也可能出现数据变更,如果你在事务中先通过 SELECT field INTO myvar FROM mytable WHERE uid = 1 读到余额,再在此基础上通过UPDATE 去更新余额,很可能导致数据变得一团糟! 正确的做法是使用 UPDATE mytable SET field = field - 1000 WHERE id = 1,因为每一条 SQL 命令本身都是原子的,这个 SQL 不会有问题。 最简单的解决办法:事务 T1 读数据时,也加一把「行」锁,直到不再需要读该数据了,再释放锁。 幻读 Phantom reads:事务 T1 在多次批量读数据时,事务 T2 往其中执行了插入/删除操作, 导致 T1 读到的是旧数据的一个残影,而非当前真实的数据状态。 最简单的解决办法:事务 T1 在批量读数据时,先加一把范围锁,在事务 T1 结束读取之后,再释放这把锁。这能同时解决「幻读」跟「不可重复读」的问题。 根据隔离程度,ANSI SQL-92 标准中将「隔离性」细分为四个等级(避免「脏写」是数据库的必备要求,因此未记录在下面的四个等级中): 串行化 Serializable:也就是完全的隔离,只要事务之间存在互相影响的可能,就(通过锁机制)强制它们串行执行。 可重复读 Repeatable read:可避免脏读、不可重复读的发生,但是解决不了幻读的问题。 读已提交 Read committed:只能避免脏读 读未提交 Read uncommitted:最低级别,完全放弃隔离性 MySQL 默认的隔离级别为「可重复读 Repeatable Read」,PostgreSQL 和 Oracle 默认隔离级别为「读已提交 Read committed」。 为什么 MySQL/PostgreSQL/Oracle 的默认隔离级别是这样设置的呢?该如何选择正确的隔离级别呢? 我们针对普通的高并发业务场景做个简单分析: 首先,「脏读」是必须避免的,它会使事务读到错误的数据!最低的「读未提交」级别直接排除 「串行化」的性能太差,也直接排除 只要 SQL 用得对,「不可重复读」问题对业务逻辑的正确性通常并无影响,所以是可以容忍的。 因此一般「读已提交」是最佳的隔离级别,这也是 PostgreSQL/Oracle 将其设为默认隔离级别的原因。 那么为什么 MySQL 这么特立独行,将默认隔离级别提高到了「可重复读」呢?为啥阿里这种大的互联网公司又会把 MySQL 默认的隔离级别改成「读已提交」? 根据网上查到的资料,这是 MySQL 的历史问题导致的。MySQL 5.0 之前只支持 statement 这种 binlog 格式,此格式在「读已提交」的隔离级别下会出现诸多问题,最明显的就是可能会导致主从数据库的数据不一致。 除了设置默认的隔离级别外,MySQL 还禁止在使用 statement 格式的 binlog 时,使用 READ COMMITTED 作为事务隔离级别,尝试修改隔离级别会报错Transaction level 'READ-COMMITTED' in InnoDB is not safe for binlog mode 'STATEMENT' 而互联网公司将隔离级别改为「读已提交」的原因也很好理解,正如前文所述「读已提交」是最佳的隔离级别,这样修改能够提升数据库的性能。 「隔离性」的本质其实就是事务的并发控制,不同的隔离级别代表了对并发事务的隔离程度,主要的实现手段是「多版本并发控制 MVCC」与「锁」。锁机制前面已经简单介绍过了,而 MVCC 其实就是为每个事务创建一个特定隔离级别的快照,这样读写不会互相阻塞,性能就提升了。(MVCC 暂时也是超纲知识,后面再研究吧 emmmm) ANSI SQL-92 对异常现象的分析仍然太过简单了,1995 年新发布的论文A Critique of ANSI SQL Isolation Levels 丰富和细化了 SQL-92 的内容,定义了六种隔离级别和八种异常现象(有大佬强烈建议通读此论文,重点是文中的快照隔离(Snapshot Isolation, SI)级别)。 2. 数据一致性 Data Consistency 「数据一致性」是指对数据库的每一次读操作都应该读到最新写入的数据,或者直接报错。 对单机数据库而言「数据一致性」往往不是问题,因为它通常只有一份保存在磁盘或内存中的数据。但是在分布式系统中,为了数据安全性或者为了性能,往往每一份数据都在多个节点上存有其副本,这就引出了数据副本们的一致性问题。因此,我们通常谈论的「数据一致性」就是指分布式系统的「数据一致性」。 CAP 原则是分布式系统领域一个著名的理论,它告诉我们在分布式系统中如下三种属性不可能全部达成,因此也被称作「CAP 不可能三角」: 数据一致性 Data Consistency:客户端的每次读操作,不管访问系统的哪个节点,要么读到的都是同一份最新写入的数据,要么读取失败 强调数据完全正确 可用性 Availability:任何来自客户端的请求,不管访问哪个非故障节点,都能得到响应数据,但不保证是同一份最新数据 强调的是服务可用,但不保证数据正确 分区容错性 Partition Tolerance:即使节点之间出现了任意数量的消息丢失或者高延迟,系统仍能正常运行 就是说网络丢包或延迟会导致系统被分成多个 Partition,系统能够容忍这种情况 为了保证分区容错性 P,考虑当分布式系统因为网络问题被割裂成多个分区时,每个分区只有如下两种选择,A 跟 C 必须牺牲掉其中之一: 取消操作并拒绝提供服务,这降低了可用性,但是能确保数据一致性 继续处理请求,这确保了可用性,但是数据一致性就无法保证了 如果系统的多个分区都在同时提供服务,导致数据不一致并且存在冲突无法合并,这就被称为分布式系统的「脑裂」,显然任何分布式系统都不会希望发生「脑裂」。 因为分布式系统与单机系统不同,它涉及到多节点间的网络通讯和交互,但是只要有网络交互就一定会有延迟和数据丢失,节点间的分区故障是很有可能发生的。因此为了正常运行,P 是分布式系统必须保证的特性,在出现分区故障时,为了 P 只能牺牲掉 A 或者 C。 工程上是要 AP 还是 CP,得视情况而定: Etcd/Zookeeper/Consul: 它们通常被用于存储系统运行的关键元信息,每次读,都要能读取到最新数据。因此它们实现了 CP,牺牲了 A DynamoDB/Cassandra/MongoDB:不要求数据一致性,一段时间内用旧的缓存问题也不大,但是要求可用性,因此应该实现 AP,牺牲掉 C 数据一致性模型 分布式系统中,多副本数据上的一组读写策略,被称为「(数据)一致性模型 Consistency Model」。一致性模型数量很多,让人难以分辨。为了便于理解,我们先从状态视角出发区分一下强一致与弱一致的概念,在这个的基础上再从操作视角去理解这众多的一致性模型。 1. 状态视角 - 强一致与弱一致 我们首先把整个分布式系统看作一个白盒,从状态视角看,任何变更操作后,分布式系统的多个数据副本只有如下三种状态: 在某些条件下,各副本状态不一致的现象只是暂时的,后续还会转换到一致的状态,这被称为「弱一致」; 这通常是使用异步复制来同步各副本的状态。 相对的说,如果系统各副本不存在「不一致」这种状态,只要变更操作成功数据就一定完全一致,那它就被称为「强一致」。 这要求所有副本之间的数据更新必须完全同步,就必须使用全同步复制。 永远不会一致:这在分布式系统中就是 bug 了,也被称为「脑裂」。 上面描述的是整个系统的客观、实际状态,但对于绝大部分用户而言分布式系统更多的是一个黑盒,因此更流行的是基于「黑盒」的分类方式,它根据系统的对外状态将系统分成两种类型: 强一致:指对系统的任何节点/进程,写操作完成后,任何用户对任何节点的后续访问都能读到新的值。就好像系统只存在一个副本一样。 最常用算法是 Raft/Paxos,它们的写操作只要求超过半数节点写入成功,因此写入完成时,内部状态实际是不一致的,但是对它进行读写,效果跟「全同步复制」没有区别。 弱一致:指对系统的任何节点/进程,写操作完成后,后续的任何访问可能会拿到的值是不确定的,但经过一段时间后,后续的任何访问都能读到新的值。 弱一致是非常模糊的定义。如果我们把最终所有用户都能访问到新的值被称为「系统收敛」, 系统收敛的用时可以有明确边界,也可以没有。系统收敛前的访问行为可以有明确规范,也可以不存在规范。一切都看具体系统的实现。 如果系统能够在有限时间内收敛,那它就是「最终一致」,否则可以认为它是「不一致」。 为了实际需要,数据库专家对系统收敛之前的读写效果进行各种限制,对系统的收敛时间进行各种限制,得到了许多一致性模型。 2. 操作视角 - 多种一致性模型 从每个客户端的操作角度看,有四种一致性模型: 写后读一致性 Read after Write Consistency:也被称作「读自己所写一致性」,即自己写完数据版本 N 后,后续读到的版本一定不小于版本 N。 它解决的问题:A 发了个抖音视频,刷新页面后却莫名其妙消失了(旧版本),几分钟后才重新刷出来。 实现方式之一:为写入者单独添加一个读取规则,他的读都由已更新其写入数据的副本来处理。 单调读一致性 Monotonic Read Consistency:保证多个读操作的顺序,即客户端一旦读到某个数据版本 N,后续不会读到比 N 更低的版本。 它解决的问题是:A 删除了一个抖音视频,可多次刷新,偶尔刷不到视频,偶尔又能刷到被删除视频(旧版本),几分钟后才彻底被删除。 实现方式之一:为每个用户的读都创建一个副本映射,后续的读都由一个固定的副本处理,避免随机切换副本而读到更老的值。 单调写一致性 Monotonic Write Consistency:保证多个写操作的顺序,即客户端对同一数据的两次写入操作,一定按其被提交的顺序被执行。 读后写一致性 Write after Read Consistency:读后写一致性,保证一个客户端读到数据版本 N 后(可能是其他客户端写入的),随后对同一数据的写操作必须要在版本号大于等于 N 的副本上执行。 上述四个一致性模型都只从每个客户端自身的角度定义规则,比较片面,因此它们都是「弱一致模型」。 而不考虑客户端,直接从所有数据库用户的操作视角看,有如下几种一致性模型: 线性一致性 Linearizability:线性一致性利用了事件的提交顺序,它保证任何读操作得到的数据,其顺序跟读/写事件的提交顺序一致。 简单的说它要求整个系统表现得像只存在一个副本,所有操作的执行结果就跟这些事件按提交顺序完全串行执行一样。这实际也是在说所有并发事件都是原子的,一旦互相之间存在冲突,就一定得按顺序执行,因此也有人称它为「原子一致性」。 线性一致性,完全等价于系统对外状态的「强一致性」 线性一致性的系统是完全确定性的 实现方式:需要一个所有节点都一致的「全局时钟」,这样才可以对所有事件进行全局排序。 大多数分布式数据库如 TiDB/Etcd 都是通过 NTP 等协议进行单点授时与同步实现的全局时钟。 有全球化部署需要的 Google Spanner 是使用 GPS + 原子钟实现的全局时钟 TrueTime,全局误差可以控制在 7ms 以内。 局限性:根据爱因斯坦相对论,「时间是相对的」,实际上并不存在绝对的时间,因此线性一致性只在经典物理学范围内适用。 顺序一致性 Sequentially Consistent: 顺序一致性最早是 Leslie Lamport 用来描述多核 CPU 的行为的,在分布式系统领域用得较少。 顺序一致性的要求有两点: 从单个进程(副本)的角度看,所有指令的执行顺序跟代码逻辑的顺序完全一致。 从所有的处理器(整个分布式系统)角度看,写操作不必立即对所有用户可见,但是所有副本必须以相同的顺序接收这些写操作。 顺序一致性和线性一致性都是要找到一个满足「写后读」的一组操作历史,差异在于线性一致性要求严格的时间序,而顺序一致性只要求满足代码的逻辑顺序,而其他代码逻辑未定义的事件顺序(比如多副本上各事件之间的顺序),具体是什么样的顺序无所谓,只要所有副本看到的事件顺序都相同就行。 顺序一致性并不能提供「确定性」,相同的两次操作仍然可能得到不同的事件顺序。 实现方式:因为不要求严格的全局时间序,它就不需要一个全局时钟了,但实际上为了满足全局的确定性,仍然需要一些复杂的操作。 因果一致性 Causal Consistency:线性一致性的全局时钟有其局限性,而因果一致性基于写事件的「偏序关系」提出了「逻辑时钟」的概念,并保证读顺序与逻辑时钟上的写事件顺序一致。 写事件的「偏序关系」关系是指,至少部分事件(比如一个节点内部的事件)是可以使用本地时钟直接排序的,而节点之间发生通讯时,接收方的事件一定晚于调用方的事件。基于这一点可以实现一个「逻辑时钟」,但逻辑时钟的缺点在于,如果某两个事件不存在相关性,那逻辑时钟给出的顺序就没有任何意义。 多数观点认为,因果一致性弱于线性一致性,但在并发性能上具有优势,也足以处理多数的异常现象,所以因果一致性也在工业界得到了应用。 CockroachDB 和 YugabyteDB 都在设计中采用了逻辑混合时钟(Hybrid Logical Clocks),这个方案源自 Lamport 的逻辑时钟,也取得了不错的效果 前缀一致性 Consistent Prefix:副本之间的同步过程中,会存在一些副本接收数据的顺序并不一致。「前缀一致性」是说所有用户读到的数据顺序的前缀永远是一致的。 「前缀」是指程序在执行写操作时,需要显式声明其「前缀」事件,这样每个事件就都存在一个由其他写事件排列而成的前缀。比如当前有写事件排列「A B C D」,那所有用户读到的数据都拥有同样的写事件前缀,比如「A」、「A B」、「A B C」、「A B C D」,但不可能出现「A C」或者「C A」等结果。 它解决的是分片分布式数据库的一致性问题:A B C 因为地域区别读写的是不同的副本,B 在抖音评论区问了个问题,然后 A 作出了回答。但是问题跟回答两个数据如果处于不同的分片,副本同步时这两个数据的顺序是无法保证的,C 可能会先读到回答信息,之后才刷新出 B 的提问, 历史事件的顺序就乱了。 实现方式:需要程序主动为消息之间添加显式的依赖关系,再据此控制其读取顺序,实现比较复杂。 存在的问题:只有被显式定义了因果关系的事件,它们之间的顺序才能被保证。 其中线性一致性就是强一致性,其他所有的模型都是弱一致性模型或者说最终一致性模型。所有这些模型按强度降序排列如下: 线性一致性/强一致性:系统对外表现得好像整个系统完全一致,不存在不一致的情况。 顺序一致性:只保证每个节点上的事件顺序一致,对节点之间的事件顺序只有非常宽松的要求。 因果一致性:同样只保证每个节点上的事件顺序一致,但是对节点之间的事件顺序的要求比顺序一致性更宽松。 有界旧一致性(Bounded Staleness):保证读到的数据与最新版本的差距不超过 K 个版本 会话一致性(Session Consistency):在一个会话内保证单调读,单调写,和读自己所写,会话之间不保证 前缀一致性:在每个会话内保证了单调读,会话之间不保证 客户端角度的四个一致性模型:写后读、单调读、单调写、读后写。这四个模型的视角都非常片面, 通常被包含在前述的一致性模型中。 更完整的关系树状图:Consistency Models 二、分布式系统的 BASE 与最终一致性 BASE 理论: 基本可用 Basically Available:当分布式系统在出现不可预知的故障时,允许损失部分功能的可用性,保障核心功能的可用性 四种实现基本可用的手段:流量削峰、延迟响应、体验降级、过载保护 软状态 Soft state:在柔性事务中,允许系统存在中间状态,且这个中间状态不会影响系统整体可用性。比如,数据库读写分离,写库同步到读库(主库同步到从库)会有一个延时,其实就是一种柔性状态。 最终一致性 Eventually consistent:前面已经说得很详细了,它指对系统的任何节点/进程, 写操作完成后,后续的任何访问可能会拿到的值是不确定的,但经过有限的一段时间后,后续的任何访问都能读到新的值。 ACID 与 BASE 实质上是分布式系统实现中的的两个极端: ACID 理论就如它的含义「酸」一样,是 CAP 原则中一致性的边界——最强的一致性,是牺牲掉 A 后达到 CP 的极致。 BASE 翻译过来就是「碱」,它是 CAP 原则中可用性的边界——最高的可用性,最弱的一致性,通过牺牲掉 C 来达到 AP 的极致。 根据 CAP 理论,如果在分布式系统中实现了一致性,可用性必然受到影响。比如,如果出现一个节点故障,则整个分布式事务的执行都是失败的。实际上,绝大部分场景对一致性要求没那么高,短暂的不一致是能接受的,另外,也基于可用性和并发性能的考虑,建议在开发实现分布式系统时,如果不是必须,尽量不要实现事务,可以考虑采用最终一致性。 最终一致性的实现手段: 读时修复:在读取数据时,检测数据的不一致,进行修复 写时修复:在写入数据时,检测数据的不一致,进行修复 异步修复:这个是最常用的方式,通过定时对账,检测副本数据的一致性并修复 在实现最终一致性的时候,还推荐同时实现自定义写一致性级别(比如 All、Quorum、One、Any),许多分布式数据库的最终一致性级别都是可调的。 但是随着 TiDB 等分布式关系数据库的兴起,分布式领域的 BASE 理论实际上正在被 ACID 赶超,ACID 焕发又一春了。 三、共识算法 共识算法,也被称为一致性协议,是指在分布式系统中多个节点之间对某个提案 Proposal(例如多个事务请求,先执行谁?)达成一致看法的一套流程。 提案的含义在分布式系统中十分宽泛,如多个事件发生的顺序、某个键对应的值、谁是主节点……等等。可以认为任何可以达成一致的信息都是一个提案。 对于分布式系统来讲,各个节点通常都是相同的确定性状态机模型(又称为状态机复制问题,State-Machine Replication),从相同初始状态开始接收相同顺序的指令,则可以保证相同的结果状态。因此,系统中多个节点最关键的是对多个事件的顺序进行共识,即排序。 共识算法是达成数据一致性的一种手段,而且是数据强一致性的必要非充分条件。比如直接使用 Raft 算法,但是允许读取集群的任何节点,只能得到数据的最终一致性,还需要其他手段才能确保强一致性。 拜占庭将军问题与拜占庭容错 拜占庭错误是 1982 年兰伯特在《拜占庭将军问题》中提出的一个错误模型,描述了在少数节点不仅存在故障,还存在恶意行为的场景下,能否达成共识这样一个问题,论文描述如下: 9 位拜占庭将军分别率领一支军队要共同围困一座城市,因为这座城市很强大,如果不协调统一将军们的行动策略,部分军队进攻、部分军队撤退会造成围困失败,因此各位将军必须通过投票来达成一致策略,要么一起进攻,要么一起撤退。 因为各位将军分别占据城市的一角,他们只能通过信使互相联系。在协调过程中每位将军都将自己投票“进攻”还是“撤退”的消息通过信使分别通知其他所有将军,这样一来每位将军根据自己的投票和其他将军送过来的投票,就可以知道投票结果,从而决定是进攻还是撤退。 而问题的复杂性就在于:将军中可能出现叛徒,他们不仅可以投票给错误的决策,还可能会选择性地发送投票。假设 9 位将军中有 1 名叛徒,8 位忠诚的将军中出现了 4 人投“进攻”,4 人投“撤退 ”,这时候叛徒可能故意给 4 名投“进攻”的将军投“进攻”,而给另外 4 名投“撤退”的将军投“撤退 ”。这样在 4 名投“进攻”的将军看来,投票是 5 人投“进攻”,从而发动进攻;而另外 4 名将军看来是 5 人投“撤退”,从而撤退。这样,一致性就遭到了破坏。 还有一种情况,因为将军之间需要通过信使交流,即便所有的将军都是忠诚的,派出去的信使也可能被敌军截杀,甚至被间谍替换,也就是说将军之间进行交流的信息通道是不能保证可靠性的。所以在没有收到对应将军消息的时候,将军们会默认投一个票,例如“进攻”。 更一般地,在已知有 N 个将军谋反的情况下,其余 M 个忠诚的将军在不受叛徒的影响下能否达成共识?有什么样的前提条件,该如何达成共识?这就是拜占庭将军问题。 如果一个共识算法在一定条件下能够解决拜占庭将军问题,那我们就称这个算法是「拜占庭容错 Byzantine Fault Tolerance(BFT)」算法。反之如果一个共识算法无法接受任何一个节点作恶,那它就被称为「非拜占庭容错 Crash Fault Tolerance (CFT)」算法。 可以通过简单穷举发现,二忠一叛是无法达成共识的,这个结论结合反证法可证明,拜占庭容错算法要求叛徒的比例必须低于 1/3。 常用共识算法 对于「非拜占庭容错 Crash Fault Tolerance (CFT)」的情况,已经存在不少经典的算法,包括 Paxos(1990 年)、Raft(2014 年)及其变种等。这类容错算法往往性能比较好,处理较快,容忍不超过一半的故障节点。 对于「拜占庭容错 Byzantine Fault Tolerance(BFT)」的情况,目前有 PBFT(Practical Byzantine Fault Tolerance,1999 年)为代表的确定性系列算法、PoW(1999 年)为代表的概率算法等算法可选。确定性算法一旦达成共识就不可逆转,即共识是最终结果;而概率类算法的共识结果则是临时的,随着时间推移或某种强化,共识结果被推翻的概率越来越小,最终成为事实上结果。拜占庭类容错算法往往性能较差,容忍不超过 1/3 的故障节点。 此外,XFT(Cross Fault Tolerance,2015 年)等最近提出的改进算法可以提供类似 CFT 的处理响应速度,并能在大多数节点正常工作时提供 BFT 保障。Algorand 算法(2017 年)基于 PBFT 进行改进,通过引入可验证随机函数解决了提案选择的问题,理论上可以在容忍拜占庭错误的前提下实现更好的性能(1000+ TPS)。 注:实践中,对客户端来说要拿到共识结果需要自行验证,典型地,可访问足够多个服务节点来比对结果,确保获取结果的准确性。 常见共识算法列举如下: 拜占庭容错 一致性 性能 可用性(能容忍多大比例的节点出现故障) 两阶段提交 2PC 否 强一致性 低 低 TCC(try-confirm-cancel) 否 最终一致性 低 低 Paxos 否 强一致性 中 中 ZAB 否 最终一致性 中 中 Raft 否 强一致性 中 中 Gossip 否 最终一致性 高 高 Quorum NWR 否 强一致性 中 中 PBFT 是 N/A 低 中 PoW 是 N/A 低 中 PoS 是 N/A 低 中 PoH 是 N/A 中 中 注:这里虽然列出了 PoW/PoS/PoH 等应用在区块链中的一致性算法,但是它们跟 PBFT 等其他拜占庭容错算法存在很大的区别,后面会给出介绍。 不同共识算法的应用场景 在不可信环境中,因为可能存在恶意行为,就需要使用支持拜占庭容错的共识算法如 PoW/PoS,使系统在存在部分节点作恶的情况下仍然能达成共识。这就是区块链使用 PoW/PoS 算法而不是 Paxos/Raft 算法的原因。 而在企业内网等场景下,可以认为是可信环境,基本不会出现恶意节点或者可以通过 mTLS 等手段进行节点身份认证,这种场景下系统具有故障容错能力就够了,就没必要做到拜占庭容错,因此常用 Raft/Paxos 等算法。 非拜占庭错误共识算法 Paxos 与 Raft 受限于篇幅与笔者精力,这部分暂时跳过…后面可能会写篇新的文章专门介绍 Paxos/Raft 算法。 能容忍拜占庭错误的 PoW 概率共识算法 PoW 即 Proof of Work 工作量证明,指系统为达到某一目标而设置的度量方法。简单理解就是一份证明,用来确认你做过一定量的工作。监测工作的整个过程通常是极为低效的,而通过对工作的结果进行认证来证明完成了相应的工作量,这个认证流程是非常简单高效的,这就是 PoW 的优势所在。 在 1993 年,Cynthia Dwork 和 Moni Naor 设计了一个系统用于反垃圾邮件、避免资源被滥用, 这是 PoW 算法的雏形。其核心思想如下: The main idea is to require a user to compute a moderately hard but not intractable function in order to gain access to the resource, thus preventing frivolous use. 翻译成中文: 其主要思想是要求用户计算一个中等难度但不难处理的函数,以获得对资源的访问,从而防止(系统资源被)滥用。 在 1999 年,Markus Jakobsson 与 Ari Juels 第一次从各种协议中提炼出 Proofs of Work 这个概念。 POW 系统中一定有两个角色,工作者和验证者,他们需要具有以下特点: 工作者需要完成的工作必须有一定的量,这个量由工作验证者给出。 验证者可以迅速的检验工作量是否达标。 工作者无法自己"创造工作",必须由验证者发布工作。 工作者无法找到很快完成工作的办法。 说到这里,我们对 PoW 应该有足够的理解了,它就是让工作者消耗一定的资源作为使用系统的成本。对于正常的用户而言这部分被消耗的资源是完全可以接受的,但是对于恶意攻击者而言,它如果想滥用系统的资源或者发送海量的垃圾邮件,就需要消耗海量的计算资源作为成本,这样就极大地提升了攻击成本。 再总结下,PoW 算法的核心是它为信息发送加入了成本,降低了信息传递的速率。 把比特币区块链转换成拜占庭将军问题来看,它的思路是这样的: 限制一段时间内提案的个数,只有拥有对应权限的节点(将军)可以发起提案。 这是通过 PoW 工作量证明实现的,比特币区块链要求节点进行海量的哈希计算作为获得提案权限的代价,算法难度每隔两周调整一次以保证整个系统找到正确 Hash 值的平均用时大约为 10 分钟。 由强一致性放宽至最终一致性。 对应一次提案的结果不需要全部的节点马上跟进,只需要在节点能搜寻到的全网络中的所有链条中,选取最长的链条进行后续拓展就可以。 使用非对称加密算法对节点间的消息传递提供签名技术支持,每个节点(将军)都有属于自己的秘钥 (公钥私钥),唯一标识节点身份。 使用非对称加密算法传递消息,能够保证消息传递的私密性。而且消息签名不可篡改,这避免了消息被恶意节点伪造。 我们前面有给出一个结论:拜占庭容错算法要求叛徒的比例必须低于 1/3。 但是区块链与拜占庭将军问题的区别很大,举例如下: 区块链允许任何节点随时加入或离开区块链,而拜占庭将军问题是预设了节点数,而且不考虑节点的添加或删除。 比特币区块链的 PoW 算法只能保证整个系统找到正确 Hash 值的平均用时大约为 10 分钟,那肯定就存在性能更好的节点用时更短,性能更差的节点用时更长,甚至某些节点运气好几秒钟就算出了结果,这都是完全可能的。而越早算出这个 Hash 值的节点,它的提案(区块)成为最长链条的概率就越大。 PoW 由强一致性放宽至最终一致性,系统总会选取最长的链进行后续拓展,那如果某个链条一开始不长,但是它的拓展速度足够快,它就能成为最长的链条。而拜占庭将军问题不允许任何分支,只存在一个结果! 只是受限于算力,随着时间的推移,短的链条追上最长链条的概率会越来越小。 总之因为区块链这样的特点,它会产生一些跟拜占庭容错算法不同的结果: 攻击者拥有的节点数量占比是毫无意义的,核心是算力,也就对应着区块链中的提案权。 即使攻击者拥有了 99% 的节点,但是它的总体算力很弱的话,它的提案(区块)成为最长链条的概率也会很低。 区块链的 51% 攻击:因为「系统总是选取最长链条进行后续拓展」这个原则,只有某个攻击者拥有了超过 50% 算力的情况下,它才拥有绝对性的优势,使它的区块在一定时间后一定能成为最长的链条,并且始终维持这样一个优势,从而达成攻击目的。 至于 PoW 算法的具体实现,以及它的替代算法 PoS/PoH 等新兴算法的原理与实现,将在后续的区块链系列文章中详细介绍,尽请期待… 参考 《Designing Data-Intensive Applications - The Big Ideas Behind Reliable, Scalable, and Maintainable Systems (Martin Kleppmann)》 极客时间《分布式数据库 30 讲》 极客时间《分布式协议与算法实战》 分布式存储系统的一致性是什么?- OceanBase 线性一致性和 Raft - PingCAP 一致性模型笔记 条分缕析分布式:浅析强弱一致性 - 张铁蕾 Why you should pick strong consistency, whenever possible - Google Spanner 拜占庭将军问题与区块链 区块链协议安全系列— —当拜占庭将军犯错时,区块链共识还安全么?(上集) Eventually Consistent - Revisited

2022/8/7
articleCard.readMore

Kubernetes 中的证书管理工具 - cert-manager

我在之前的文章写给开发人员的实用密码学(八)—— 数字证书与 TLS 协议 中,介绍了如何使用 openssl 生成与管理各种用途的数字证书,也简单介绍了如何通过 certbot 等工具与 ACME 证书申请与管理协议,进行数字证书的申请与自动更新(autorenew)。 这篇文章要介绍的 cert-manager,跟 certbot 这类工具有点类似,区别在于它是工作在 Kubernetes 中的。 cert-manager 是一个证书的自动化管理工具,用于在 Kubernetes 集群中自动化地颁发与管理各种来源、各种用途的数字证书。它将确保证书有效,并在合适的时间自动更新证书。 多的就不说了,证书相关的内容请参见我的写给开发人员的实用密码学(八)—— 数字证书与 TLS 协议 或者其他资料,现在直接进入正题。 注:cert-manager 的管理对象是「证书」,如果你仅需要使用非对称加密的公私钥对进行 JWT 签名、数据加解密,可以考虑直接使用secrets 管理工具 Vault. 一、部署 https://cert-manager.io/docs/installation/helm/ 官方提供了多种部署方式,使用 helm3 安装的方法如下: shell # 添加 cert-manager 的 helm 仓库 helm repo add jetstack https://charts.jetstack.io helm repo update # 查看版本号 helm search repo jetstack/cert-manager -l | head # 下载并解压 chart,目的是方便 gitops 版本管理 helm pull jetstack/cert-manager --untar --version 1.8.2 helm install \ cert-manager ./cert-manager \ --namespace cert-manager \ --create-namespace \ # 下面这个参数会导致使用 helm 卸载的时候,会删除所有 CRDs,可能导致所有 CRDs 资源全部丢失!要格外注意 --set installCRDs=true 二、创建 Issuer cert-manager 支持多种 issuer,你甚至可以通过它的标准 API 创建自己的 Issuer。 但是总的来说不外乎三种: 由权威 CA 机构签名的「公网受信任证书」: 这类证书会被浏览器、小程序等第三方应用/服务商信任 本地签名证书: 即由本地 CA 证书签名的数字证书 自签名证书: 即使用证书的私钥为证书自己签名 下面介绍下如何申请公网证书以及本地签名证书。 1. 通过权威机构创建公网受信证书 通过权威机构创建的公网受信证书,可以直接应用在边界网关上,用于给公网用户提供 TLS 加密访问服务,比如各种 HTTPS 站点、API。这是需求最广的一类数字证书服务。 cert-manager 支持两种申请公网受信证书的方式: ACME(Automated Certificate Management Environment (ACME) Certificate Authority server)证书自动化申请与管理协议。 venafi-as-a-service: venafi 是一个证书的集中化管理平台,它也提供了 cert-manager 插件,可用于自动化申请 DigiCert/Entrust/GlobalSign/Let’s Encrypt 四种类型的公网受信证书。 这里主要介绍使用 ACMEv2 协议申请公网证书,支持使用此开放协议申请证书的权威机构有: 免费服务 Let’s Encrypt: 众所周知,它提供三个月有效期的免费证书。 ZeroSSL: 貌似也是一个比较有名的 SSL 证书服务 通过 ACME 协议支持不限数量的 90 天证书,也支持多域名证书与泛域名证书。 它提供了一个额外的 Dashboard 查看与管理所有申请的证书,这是比较方便的地方。 付费服务 DigiCert: 这个非常有名(但也是相当贵),官方文档Digicert - Third-party ACME client automation Google Public Authority(Google Trust Services): Google 推出的公网证书服务,也是三个月有效期,其根证书交叉验证了 GlobalSign,OCSP 服务器在国内速度也很快。 详见acme.sh/wiki/Google-Public-CA 此功能目前(2022-08-10)仍处于 beta 状态,需要提表单申请才能获得使用 官方地址:https://pki.goog/ Entrust: 官方文档Entrust’s ACME implementation GlobalSign: 官方文档GlobalSign ACME Service 这里也顺便介绍下收费证书服务对证书的分级,以及该如何选用: Domain Validated(DV)证书 仅验证域名所有权,验证步骤最少,价格最低,仅需要数分钟即可签发。 优点就是易于签发,很适合做自动化。 各云厂商(AWS/GCP/Cloudflare,以及 Vercel/Github 的站点服务)给自家服务提供的免费证书都是 DV 证书,Let’s Encrypt 的证书也是这个类型。 很明显这些证书的签发都非常方便,而且仅验证域名所有权。 但是 AWS/GCP/Cloudflare/Vercel/Github 提供的 DV 证书都仅能在它们的云服务上使用,不提供私钥导出功能! Organization Validated (OV) 证书 是企业 SSL 证书的首选,通过企业认证确保企业 SSL 证书的真实性。 除域名所有权外,CA 机构还会审核组织及企业的真实性,包括注册状况、联系方式、恶意软件等内容。 如果要做合规化,可能至少也得用 OV 这个级别的证书。 Extended Validation(EV)证书 最严格的认证方式,CA 机构会深度审核组织及企业各方面的信息。 被认为适合用于大型企业、金融机构等组织或企业。 而且仅支持签发单域名、多域名证书,不支持签发泛域名证书,安全性杠杠的。 ACME 支持 HTTP01 跟 DNS01 两种域名验证方式,其中 DNS01 是最简便的方法。 下面分别演示如何使用 AWS Route53 跟 AliDNS,通过 DNS 验证方式申请一个 Let’s Encrypt 证书。 (其他 DNS 提供商的配置方式请直接看官方文档) 1.1 使用 AWS Route53 创建一个证书签发者「Certificate Issuer」 非 AWS Route53 用户可忽略这一节 https://cert-manager.io/docs/configuration/acme/dns01/route53/ 1.1.1 通过 IAM 授权 cert-manager 调用 AWS Route53 API 这里介绍一种不需要创建 ACCESS_KEY_ID/ACCESS_SECRET,直接使用 AWS EKS 官方的免密认证的方法。会更复杂一点,但是更安全可维护。 首先需要为 EKS 集群创建 OIDC provider,参见aws-iam-and-kubernetes, 这里不再赘述。 cert-manager 需要查询与更新 Route53 记录的权限,因此需要使用如下配置创建一个 IAM Policy, 可以命名为 <ClusterName>CertManagerRoute53Access(注意替换掉 <ClusterName>): json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "route53:GetChange", "Resource": "arn:aws:route53:::change/*" }, { "Effect": "Allow", "Action": ["route53:ChangeResourceRecordSets", "route53:ListResourceRecordSets"], "Resource": "arn:aws:route53:::hostedzone/*" }, { "Effect": "Allow", "Action": "route53:ListHostedZonesByName", "Resource": "*" } ] } 比如使用 awscli 创建此 policy: shell aws iam create-policy \ --policy-name XxxCertManagerRoute53Access \ --policy-document file://cert-manager-route53-access.json 然后通过上述配置创建一个 IAM Role 并自动给 cert-manager 所在的 EKS 集群添加信任关系: shell export CLUSTER_NAME="xxx" export AWS_ACCOUNT_ID="112233445566" # 使用 eksctl 自动创建对应的 role 并添加信任关系 # 需要先安装好 eksctl eksctl create iamserviceaccount \ --cluster "${CLUSTER_NAME}" --name cert-manager --namespace cert-manager \ --role-name "${CLUSTER_NAME}-cert-manager-route53-role" \ --attach-policy-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:policy/<ClusterName>CertManagerRoute53Access" \ --role-only \ --approve 之后需要为 cert-manager 的 ServiceAccount 添加注解来绑定上面刚创建好的 IAM Role,首先创建如下 helm values 文件 cert-manager-values.yaml: yaml # 如果把这个改成 false,也会导致 cert-manager 的所有 CRDs 及相关资源被删除! installCRDs: true serviceAccount: annotations: # 注意修改这里的 ${AWS_ACCOUNT_ID} 以及 ${CLUSTER_NAME} eks.amazonaws.com/role-arn: >- arn:aws:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-cert-manager-route53-role securityContext: enabled: true # 根据官方文档,还得修改下这个,允许 cert-manager 读取 ServiceAccount Token,从而获得授权 fsGroup: 1001 然后重新部署 cert-manager: shell helm upgrade -i cert-manager ./cert-manager -n cert-manager -f cert-manager-values.yaml 这样就完成了授权。 1.1.2 创建一个使用 AWS Route53 进行验证的 ACME Issuer 在 xxx 名字空间创建一个 Iusser: yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: letsencrypt-prod-aws namespace: xxx spec: acme: # 用于接受域名过期提醒的邮件地址 email: user@example.com # ACME 服务器,比如 let's encrypt、Digicert 等 # let's encrypt 的测试 URL,可用于测试配置正确性 # server: https://acme-staging-v02.api.letsencrypt.org/directory # let's encrypt 的正式 URL,有速率限制 server: https://acme-v02.api.letsencrypt.org/directory # 用于存放 ACME 账号私钥的 Secret 名称,Issuer 创建时会自动生成此 secret privateKeySecretRef: name: letsencrypt-prod-aws # DNS 验证设置 solvers: - selector: # 在有多个 solvers 的情况下,会根据每个 solvers 的 selector 来确定优先级,选择其中合适的 solver 来处理证书申请事件 # 以 dnsZones 为例,越长的 Zone 优先级就越高 # 比如在为 www.sys.example.com 申请证书时,sys.example.com 的优先级就比 example.com 更高 dnsZones: - "example.com" dns01: # 使用 route53 进行验证 route53: region: us-east-1 # cert-manager 已经通过 ServiceAccount 绑定了 IAM Role # 这里不需要补充额外的 IAM 授权相关信息! 1.2 使用 AliDNS 创建一个证书签发者「Certificate Issuer」 https://cert-manager.io/docs/configuration/acme/dns01/#webhook cert-manager 官方并未提供 alidns 相关的支持,而是提供了一种基于 WebHook 的拓展机制。社区有第三方创建了对 alidns 的支持插件: cert-manager-alidns-webhook 下面我们使用此插件演示下如何创建一个证书签发者。 1.1.1 通过 IAM 授权 cert-manager 调用 AWS Route53 API 首先需要在阿里云上创建一个子账号,名字可以使用 alidns-acme,给它授权 DNS 修改权限,然后为该账号生成 ACCESS_KEY_ID/ACCESS_SECRET。 完成后,使用如下命令将 key/secret 内容创建为 secret 供后续步骤使用: shell # 注意替换如下命令中的 <xxx> 为你的 key/secret kubectl -n cert-manager create secret generic alidns-secrets \ --from-literal="access-token=<your-access-key-id>" \ --from-literal="secret-key=<your-access-secret-key>" 接下来需要部署cert-manager-alidns-webhook 这个 cert-manager 插件: shell # 添加 helm 仓库 helm repo add cert-manager-alidns-webhook https://devmachine-fr.github.io/cert-manager-alidns-webhook helm repo update # 安装插件 ## 其中的 groupName 是一个全局唯一的标识符,用于标识创建此 webhook 的组织,建议使用公司域名 ## groupName 必须与后面创建的 Issuer 中的 groupName 一致,否则证书将无法通过验证! helm -n cert-manager install alidns-webhook \ cert-manager-alidns-webhook/alidns-webhook \ --set groupName=example.com 1.1.2 创建一个使用 AliDNS 进行验证的 ACME Issuer 在 xxx 名字空间创建一个 Iusser: yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: letsencrypt-prod-alidns namespace: xxx spec: acme: # 用于接受域名过期提醒的邮件地址 email: user@example.com # ACME 服务器,比如 let's encrypt、Digicert 等 # let's encrypt 的测试 URL,可用于测试配置正确性 # server: https://acme-staging-v02.api.letsencrypt.org/directory # let's encrypt 的正式 URL,有速率限制 server: https://acme-v02.api.letsencrypt.org/directory # 用于存放 ACME 账号私钥的 Secret 名称,Issuer 创建时会自动生成此 secret privateKeySecretRef: name: letsencrypt-prod-alidns # DNS 验证设置 solvers: - selector: # 在有多个 solvers 的情况下,会根据每个 solvers 的 selector 来确定优先级,选择其中合适的 solver 来处理证书申请事件 # 以 dnsZones 为例,越长的 Zone 优先级就越高 # 比如在为 www.sys.example.com 申请证书时,sys.example.com 的优先级就比 example.com 更高 # 适用场景:如果你拥有多个域名,使用了多个域名提供商,就可能需要用到它 dnsZones: - "example.com" dns01: webhook: config: accessTokenSecretRef: key: access-token name: alidns-secrets regionId: cn-beijing secretKeySecretRef: key: secret-key name: alidns-secrets # 这个 groupName 必须与之前部署插件时设置的一致! groupName: example.com solverName: alidns-solver 1.3 通过 ACME 创建证书 https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources 在创建证书前,先简单过一下证书的申请流程,示意图如下(出问题时需要靠这个来排查): text ( +---------+ ) ( | Ingress | ) Optional ACME Only! ( +---------+ ) | | | +-------------+ +--------------------+ | +-------+ +-----------+ |-> | Certificate |----> | CertificateRequest | ----> | | Order | ----> | Challenge | +-------------+ +--------------------+ | +-------+ +-----------+ | 使用如下配置创建证书,并将证书保存到指定的 Secret 中: yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: example-com namespace: xxx spec: # Secret names are always required. # Istio Gateway/Ingress/Gateway API 都可以通过直接引用这个 secret 来添加 TLS 加密。 secretName: tls-example.com # secretTemplate is optional. If set, these annotations and labels will be # copied to the Secret named tls-example.com. These labels and annotations will # be re-reconciled if the Certificate's secretTemplate changes. secretTemplate # is also enforced, so relevant label and annotation changes on the Secret by a # third party will be overwritten by cert-manager to match the secretTemplate. secretTemplate: annotations: my-secret-annotation-1: "foo" my-secret-annotation-2: "bar" labels: my-secret-label: foo duration: 2160h # 90d renewBefore: 360h # 15d # https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificatePrivateKey privateKey: algorithm: ECDSA # RSA/ECDSA/Ed25519,其中 RSA 应用最广泛,Ed25519 被认为最安全 encoding: PKCS1 # 对于 TLS 加密,通常都用 PKCS1 格式 size: 256 # RSA 默认为 2048,ECDSA 默认为 256,而 Ed25519 不使用此属性! rotationPolicy: Always # renew 时总是重新创建新的私钥 # The use of the common name field has been deprecated since 2000 and is # discouraged from being used. commonName: example.com # At least one of a DNS Name, URI, or IP address is required. dnsNames: - example.com - "*.example.com" isCA: false usages: - server auth - client auth # uris: # 如果想在证书的 subjectAltNames 中添加 URI,就补充在这里 # - spiffe://cluster.local/ns/sandbox/sa/example # ipAddresses: # 如果想在证书的 subjectAltNames 添加 ip 地址,就补充在这里 # - 192.168.0.5 subject: # 证书的补充信息 # 字段索引:https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.X509Subject organizations: - xxx # Issuer references are always required. issuerRef: name: letsencrypt-prod-aws # name: letsencrypt-prod-alidns # 如果你前面创建的是 alidns 那就用这个 kind: Issuer # 如果你创建的是 ClusterIssuer 就需要改下这个值 group: cert-manager.io 部署好 Certificate 后,describe 它就能看到当前的进度: text Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Issuing 117s cert-manager-certificates-trigger Issuing certificate as Secret does not exist Normal Generated 116s cert-manager-certificates-key-manager Stored new private key in temporary Secret resource "example.com-f044j" Normal Requested 116s cert-manager-certificates-request-manager Created new CertificateRequest resource "example.com-unv3d" Normal Issuing 20s cert-manager-certificates-issuing The certificate has been successfully issued 如果发现证书长时间未 Ready,可以参照官方文档 - Troubleshooting Issuing ACME Certificates, 按证书申请流程进行逐层排查: 首先 cert-manager 发现 Certificate 描述的 Secret 不存在,于是启动证书申请流程 首先生成私钥,存放在一个临时 Secret 中 然后通过私钥以及 Certificate 资源中的其他信息,生成 CSR 证书申请请求文件 这也是一个 CRD 资源,可以通过 kubectl get csr -n xxx 查看 接着将 CSR 文件提交给 ACME 服务器,申请权威机构签发证书 这对应 CRD 资源 kubectl get order 对于上述 ACME 证书申请流程,Order 实际上会生成一个 DNS1 Challenge 资源 可以通过 kubectl get challenge 检查此资源 challenge 验证通过后会逐层往回走,前面的 Order CSR 状态都会立即变成 valid 最终证书签发成功,Certificate 状态变成 Ready,所有 Order CSR challenge 资源都被自动清理掉。 1.4 通过 csi-driver 创建证书 https://cert-manager.io/docs/projects/csi-driver/ 直接使用 Certificate 资源创建的证书,会被存放在 Kubernetes Secrets 中,被认为并非足够安全。而 cert-manager csi-driver 则避免了这个缺陷,具体而言,它提升安全性的做法有: 确保私钥仅保存在对应的节点上,并挂载到对应的 Pod,完全避免私钥被通过网络传输。 应用的每个副本都使用自己生成的私钥,并且能确保在 Pod 的生命周期中证书跟私钥始终存在。 自动 renew 证书 副本被删除时,证书就会被销毁 总的说 csi-driver 主要是用来提升安全性的,有需要可以自己看文档,就不多介绍了。 2. 通过私有 CA 颁发证书 Private CA 是一种企业自己生成的 CA 证书,通常企业用它来构建自己的 PKI 基础设施。 在 TLS 协议这个应用场景下,Private CA 颁发的证书仅适合在企业内部使用,必须在客户端安装上这个 CA 证书,才能正常访问由它签名的数字证书加密的 Web API 或者站点。Private CA 签名的数字证书在公网上是不被信任的! cert-manager 提供的 Private CA 服务有: Vault: 鼎鼎大名了,Vault 是一个密码即服务工具,可以部署在 K8s 集群中,提供许多密码、证书相关的功能。 开源免费 AWS Certificate Manager Private CA: 跟 Vault 的 CA 功能是一致的,区别是它是托管的,由 AWS 负责维护。 每个 Private CA 证书:$400/month 每个签发的证书(仅读取了私钥及证书内容后才会收费):按梯度一次性收费,0-1000 个以内是 $0.75 每个 其他的自己看文档… 这个因为暂时用不上,所以还没研究,之后有研究再给补上。 TO BE DONE. 三、cert-manager 与 istio/ingress 等网关集成 cert-manager 提供的 Certificate 资源,会将生成好的公私钥存放在 Secret 中,而 Istio/Ingress 都支持这种格式的 Secret,所以使用还是挺简单的。 以 Istio Gateway 为例,直接在 Gateway 资源上指定 Secret 名称即可: yaml apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: example-gateway spec: selector: istio: ingressgateway servers: - port: number: 8080 name: http protocol: HTTP hosts: - product.example.com tls: httpsRedirect: true # sends 301 redirect for http requests - port: number: 8443 name: https protocol: HTTPS tls: mode: SIMPLE # enables HTTPS on this port credentialName: tls-example.com # This should match the Certificate secretName hosts: - product.example.com # This should match a DNS name in the Certificate --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: product spec: hosts: - product.example.com gateways: - example-gateway http: - route: - destination: host: product port: number: 8080 --- apiVersion: v1 kind: Service metadata: labels: app: product name: product namespace: prod spec: ports: - name: grpc port: 9090 protocol: TCP targetPort: 9090 - name: http port: 8080 protocol: TCP targetPort: 8080 selector: app: product sessionAffinity: None type: ClusterIP --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: product spec: host: product # 定义了两个 subset subsets: - labels: version: v1 name: v1 - labels: version: v2 name: v2 --- # 其他 deployment 等配置 之后再配合 VirtualService 等资源,就可以将 Istio 跟 cert-manager 结合起来啦。 四、将 cert-manager 证书挂载到自定义网关中 注意,千万别使用 subPath 挂载,根据官方文档, 这种方式挂载的 Secret 文件不会自动更新! 既然证书被存放在 Secret 中,自然可以直接当成数据卷挂载到 Pods 中,示例如下: yaml apiVersion: v1 kind: Pod metadata: name: nginx spec: containers: - name: nginx image: nginx:latest volumeMounts: - name: tls-example.com mountPath: "/certs/example.com" readOnly: true volumes: - name: tls-example.com secret: secretName: tls-example.com optional: false # default setting; "mysecret" must exist 对于 nginx 而言,可以简单地搞个 sidecar 监控下,有配置变更就 reload 下 nginx,实现证书自动更新。 或者可以考虑直接写个 k8s informer 监控 secret 的变更,有变更就直接 reload 所有 nginx 实例,总之实现的方式有很多种。 五、监控告警 证书的过期时间是一个很重要的指标,证书过期了,网站就无法正常访问了。虽然正常情况下 cert-manager 应该能够自动更新证书,但是万一出现了问题,又没有及时发现,那就麻烦了。 因此,建议对证书的过期时间进行监控,当证书的过期时间小于一定阈值时,及时发出告警。 cert-manager 提供了 Prometheus 监控指标,可以直接使用 Prometheus 等工具进行监控告警。 官方文档是这个:https://cert-manager.io/docs/usage/prometheus-metrics/#scraping-metrics 文档中没详细列出所有的指标,可以直接接入到 Prometheus 中,然后通过 Grafana 查看。 比如要设置证书过期时间的告警,可以使用如下 PromQL: promql (certmanager_certificate_expiration_timestamp_seconds - time())/3600/24 < 20 上面这个 PromQL 表示,如果证书的过期时间小于 20 天,就会触发告警。 六、注意事项 服务端 TLS 协议的配置有许多的优化点,有些配置对性能的提升是很明显的,建议自行网上搜索相关资料,这里仅列出部分相关信息。 OCSP 证书验证协议会大幅拖慢 HTTPS 协议的响应速度 https://www.ssl.com/blogs/how-do-browsers-handle-revoked-ssl-tls-certificates/ https://imququ.com/post/why-can-not-turn-on-ocsp-stapling.html https://www.digicert.com/help/ 前面提到除了数字证书自带的有效期外,为了在私钥泄漏的情况下,能够吊销对应的证书,PKI 公钥基础设施还提供了 OCSP(Online Certificate Status Protocol)证书状态查询协议。 这导致了一些问题: Chrome/Firefox 等浏览器都会定期通过 OCSP 协议去请求 CA 机构的 OCSP 服务器验证证书状态, 这可能会拖慢 HTTPS 协议的响应速度。 所谓的定期是指超过上一个 OCSP 响应的 nextUpdate 时间(一般为 7 天),或者如果该值为空的话,Firefox 默认 24h 后会重新查询 OCSP 状态。 因为客户端直接去请求 CA 机构的 OCSP 地址获取证书状态,这就导致 CA 机构可以获取到一些对应站点的用户信息(IP 地址、网络状态等)。 为了解决这两个问题,rfc6066 定义了 OCSP stapling 功能,它使服务器可以提前访问 OCSP 获取证书状态信息并缓存到本地,基本 Nginx/Caddy 等各大 Web 服务器或网关,都支持 OCSP stapling 协议。 在客户端使用 TLS 协议访问 HTTPS 服务时,服务端会直接在握手阶段将缓存的 OCSP 信息发送给客户端。因为 OCSP 信息会带有 CA 证书的签名及有效期,客户端可以直接通过签名验证 OCSP 信息的真实性与有效性,这样就避免了客户端访问 OCSP 服务器带来的开销。 而另一个方法,就是选用 ocsp 服务器在目标用户区域速度快的 CA 机构签发证书。 可以使用如下命令测试,确认站点是否启用了 ocsp stapling: conf $ openssl s_client -connect www.digicert.com:443 -servername www.digicert.com -status -tlsextdebug < /dev/null 2>&1 | grep -i "OCSP response" 如果输出包含 OCSP Response Status: successful 就说明站点支持 ocsp stapling,如果输出内容为 OCSP response: no response sent 则说明站点不支持ocsp stapling。 实际上 Google/AWS 等大多数站点都不会启用也不需要启用 ocsp stapling,一是因为它们自己就是证书颁发机构,OCSP 服务器也归它们自己管,不存在隐私的问题。二是它们的 OCSP 服务器遍布全球,也不存在性能问题。这种情况下开个 OCSP Stapling 反而是浪费流量,因为每次 TLS 握手都得发送一个 OCSP 状态信息。 我测试发现只有 www.digicert.com/www.douban.com 等少数站点启用了 ocsp stapling,www.baidu.com/www.google.com/www.zhihu.com 都未启用 ocsp stapling.

2022/7/31
articleCard.readMore

Death Is But a Dream

我并不知道何时才是死期, 却日日问自己, 会不会有幽灵来牵我的手, 引我一路向西? 可他们看到的光明又在哪里? 会不会也为我亮起? 这一切快来吧,我已等不及! ——《红色地带的沉思》 by 临终患者 Patricia 我最近在看一本书,《在生命的尽头拥抱你-临终关怀医生手记》,它的英文原名即本文的标题。李白在《春夜宴从弟桃花园序》中写「而浮生若梦,为欢几何?」,仿照下文法,本文的标题「Death Is But a Dream」大概可以直译成「临终若梦」,跟此书的中文版名称有些不同的韵味在。 这书讲的是死亡的过程——临终梦境。随着年龄渐长,老一辈们慢慢老去,我们都不可避免地会越来越多地接触到死亡。这世间轮替更迭,爱恨情仇是不变的主题,但是死亡始终是配角,实在是因为每人一生都只有唯一一次机会去真正体味死亡,难有实感。而且死亡往往代表着终结,我们做为生者,当然更向往书写生者的世界。 救人一命胜造七级浮屠,珍爱生命是我们从小到大被教育的思想。但是这样的思想却也造成了许多悲剧,全世界有许多的患者痛苦不堪地活在世上,求死不能,最终在病魔的折磨下凄惨离世,如果我们能谨慎承认「死亡」的价值,从这个角度看也是拯救了许多的患者与家庭。 而关于死亡,在我亲身经历的几次长辈葬礼中,我发现父辈们对死亡大都看得很开,他们说「人总有一死,老人家过世了我们当子女的肯定要送最后一程,但是不需要想太多,魂归天地罢了。」,我佩服这种豁达。 但是说到临终梦境,我是真的没什么了解。我从来没跟长辈交流过生命末期的梦境,脑海中也挖掘不出相关的记忆。送走我爷爷的时候,看着爷爷因为呼吸困难而大口喘气,堂哥跪在我前面,双眼泛红隐含泪光,但我完全没有实感——一切都显得那么不真实。听着爷爷被痰堵塞气管、艰难的呼吸声,我甚至感到害怕,想要逃离。 爷爷过世后,奶奶就是一个人生活了,一个人起居、一个人给菜苗松土,然后在菜地里不小心跌了一跤,就随着爷爷去了。 2023/1/7 补充:跟堂弟印照记忆后发现,这里我的记忆或许已经不对了… 我后来在爷爷奶奶房里一个壁橱上,找到三四枚铜钱,还沾着泥土,有些锈蚀痕迹。我把其中一枚通宝红线串好,贴身带了好几年,心情不好的时候就凝视着这枚铜钱黯然神伤,心情好的时候也要捂着它入睡。 我还喜欢上了戏曲,缠着同学读了她的《中国戏剧史》。大学的时候又喜欢上越剧,吴侬软语。又因为初中时学过点竹笛,喜欢上了传统乐曲,我对一些经典老歌也情有独钟。在很多同学跟同事的眼中,我的音乐品味是很「独特」的,这或许都源自爷爷奶奶的熏陶。实际上我小的时候并不喜欢戏剧,我跟爷爷奶奶去看庙戏的目的,通常都只是为了吃一碗凉粉,或者为了去玩耍、看个热闹。偶尔去爷爷奶奶家玩,也只是觉得他们太孤单了,跟他们随便聊聊天,实际上这么多年,跟爷爷奶奶看过的戏曲,我就没听懂过几句台词。 那是多少年前了呢?只知道是很多年前了,不仔细回忆回忆、掐指算算,都搞不清具体过了多少年月。这么多个日日夜夜里,我幼稚过、热血过,也迷茫过、颓废过,倒也不算庸庸碌碌,我还是知足的,这种心态貌似是被称作现充 emmm 高三时曾经看过一本超级喜欢的励志书,后来我高考完于徽州求学、深圳打工,这么多年一直将它带在身边,书名叫《这一生再也不会有的奇遇》。书的扉页只有一句话:「当明天再也不是无限,你还会像今天一样度过你的人生吗?」 《这一生再也不会有的奇遇》已陪我度过了七八个春夏秋冬 当明天再也不是无限 写到这儿,我又想起我高中时还看过一本书《刺猬的优雅》,它同样陪伴我度过了四年大学岁月,后来又辗转到了深圳,但现在倒是不在身边。书中有几句话我印象深刻,放在这篇文章里也挺应景的: 话又说回来,不能因为有想死的心,往后就要像烂菜帮一样的过日子,甚至应该完全相反。 重要的不是死亡,而是在死亡的那一刻我们在做什么。 随意写下这些文字,脑子里各种想法恣意流淌,我打算提前写写我的临终遗言,把一切都准备好。 但在死亡到来之前,我仍要精彩的活! 参考 《在生命的尽头拥抱你-临终关怀医生手记》 Death Is But A Dream (2021) Official trailer I See Dead People: Dreams and Visions of the Dying | Dr. Christopher Kerr | TEDxBuffalo End-of-Life Experiences - Dr. Christopher Kerr 有谁看过《刺猬的优雅》这本书吗?如何? - 於清樂 有什么人生中值得一读的小说或名著?(最好现代作家的书)? - 於清樂

2022/5/24
articleCard.readMore