[{"content":"2026年了，MoE大模型微调竟然还没有彻底极其方便的通解。这合理吗？\n为什么MoE大模型多卡分布式LoRA微调是困难的？ 📌 重要 一句话总结\nMoE模型在LoRA微调中产生巨量低秩矩阵，叠加低效的MoE实现，导致多卡梯度计算困难，最终容易卡死。\n一开始我也很惊奇，都2026年了，Qwen3模型问世已半年有余，**难道业界就没有MoE做LoRA微调的需求么？**怎么会一直存在这个问题呢？\n后来多方查阅资料，这个问题还真没那么好解决：\nMoE架构的模型中存在数量极其庞大的小专家FFN，每个专家（Gated MLP）内有三个Linear模块； 在LoRA微调时，LoRA会为每个Linear模块添加两个低秩矩阵(A/B)； 这导致模型中参与计算的模块数量极其庞大，CUDA计算图相较Dense模型极其复杂； 在 HF transformers 4.* 版本中，MoE的专家路由与FFN选择甚至使用Python for循环实现； 低效的专家选择和计算加剧了多卡分布式计算的困难，也容易导致多卡不同rank数据不对齐，reduce失败； 最终导致训练完全卡死，表现为显卡占用率100%却没有计算量，NCCL通信超时； 所有基于transformers库搭建的框架（LLaMA-Factory等）都面临相同问题。 包括我在内，很多人惯性认为，现在的计算能力足以满足更大规模的模型训练，小小MoE引入的计算开销应当不足为惧才对？\n让我们看看例子，以Qwen3-30B-A3B为例：\n模型中每一层有128个专家 模型总共48层，因此整个模型就有6000多个专家FFN模块 而每个FFN模块都是一个Gated MLP模块，内含3个Linear矩阵 此时，单论专家部分，模型中就总共有18000+ Linear矩阵了，还没考虑Attention等部件 使用LoRA微调时，需要对每个Linear增加两个矩阵(A/B)，再次翻倍变成36000+矩阵参与反向传播 作为对比，Qwen3-32B共64层，每层包括Attention和Norm在内也不过11个Linear矩阵，整个模型应用LoRA后的可更新Linear数控制在2000以内，还不到MoE模型的零头。\n解决方案 分析了问题原因，解决方案呼之欲出，无非两种：\nSensei，我不做LoRA微调啦！泷泽LoRA哒！ 你去把 Huggingface 师徒干掉。我？ 第一种方案是研究论文的主要手段，研究一些小模型尚且可以接受。\n但眼下是大模型时代！Qwen3发布的MoE最小也有30B参数量，全参数微调需要的VRAM奔着360G以上去了，只有财大气粗的团队才能轻松实现吧。\n第二种方案说得好，既然transformers库的MoE实现有问题，那我们不用不就行了？\n很好，现在请开始手搓一个Fused MoE Kernel吧。这就是传说中的从入门到入土吗？\n等一下，为什么要自己手搓呢？还好，有人帮我们实现过了！\nMegatron + Swift 现在，有请NVIDIA开发的Megatron库，以及Modelscope开发的ms-swift库。\n上面两个库是什么，还请自行搜索。总而言之，借助这两个库，我们可以在transformers v5真正解决MoE计算效率问题前，高效地训练MoE模型LoRA。\n现在让我们直接开始准备吧！\n环境配置 这一套方案需要使用Python 3.11，原始配环境教程在这里。\n相较于原始教程，我做了一点点修改，总结了一些踩坑的改进，可以和原文档一起阅读。\n假设此时系统已配置好CUDA Toolkit，也就是nvcc那一套东西。\n另外，我现在更偏好使用uv作为pip的替代，使用pip时请自行调整。\n基础环境 准备以下requirements.txt：\n1 2 3 4 5 6 7 ms-swift==3.12 torch==2.8.0 transformers\u0026lt;4.58 trl\u0026lt;0.25 peft\u0026lt;0.19 deepspeed\u0026lt;0.18 pybind11 需要注意，这一套环境中，PyTorch 2.9.0或更高版本似乎存在一些问题，所以建议使用2.8.0。\n接下来，根据机器上NVIDIA驱动的CUDA版本，选择好希望使用的CUDA版本号，然后开装！这里我们使用CUDA 12.6版本的PyTorch。\n1 2 uv venv -p 3.11 uv pip install -r requirements.txt --torch-backend cu126 外部依赖项 接下来，我们需要获取一些外部依赖项，这些依赖需要手动从Github上拉取后本地编译安装。\n1 2 3 4 5 6 7 8 # Create a folder, or NOT as you wish. mkdir deps # NVIDIA APEX git clone https://github.com/NVIDIA/apex deps/apex # NVIDIA Megatron git clone --branch core_r0.15.0 https://github.com/NVIDIA/Megatron-LM.git deps/Megatron-LM 本地编译依赖 现在，我们需要开始一项项编译依赖。首先是transformer_engine：\n1 2 3 source .venv/bin/activate SITE_PACKAGES=$(uv run python -c \u0026#34;import site; print(site.getsitepackages()[0])\u0026#34;) \u0026amp;\u0026amp; echo $SITE_PACKAGES \u0026amp;\u0026amp; CUDNN_PATH=$SITE_PACKAGES/nvidia/cudnn CPLUS_INCLUDE_PATH=\u0026#34;$SITE_PACKAGES/nvidia/cudnn/include:$SITE_PACKAGES/nvidia/nccl/include\u0026#34; uv pip install --no-build-isolation transformer_engine[pytorch] 上面这一串看起来怪吓人的，其实就是把Python虚拟环境中的include文件夹暴露给编译器，避免重复下载CUDNN或者NCCL等库的麻烦。参考这个Issue。\n接下来是APEX：\n1 uv pip install -v --disable-pip-version-check --no-cache-dir --no-build-isolation --config-settings=\u0026#34;--build-option=--cpp_ext\u0026#34; --config-settings=\u0026#34;--build-option=--cuda_ext\u0026#34; ./deps/apex/ MS-Swift教程中的命令不完全兼容uv指令，需要像上面的例子一样，--config-settings后用等于号=连接参数字符串。\n然后是Megatron：\n1 uv pip install ./deps/Megatron-LM/ 最后是Flash Attention：\n1 uv pip install \u0026#34;flash-attn==2.8.3\u0026#34; --no-build-isolation 如果没有出错，那么环境就配好啦。\n参数调优 请参考Megatron Core MoE\n坑 数据加载有问题 上述依赖会在环境中使用datasets==3.6.0，如果你生成的数据集文件使用了不同版本的datasets库，会容易出错。\n解决方法很简单：在生成数据集文件时，就使用datasets==3.6.0。\n","date":"2026-01-21T11:00:00+08:00","permalink":"/2026/01/21/lora-fine-tune-on-qwen3-moe/","title":"MoE模型的高效微调"},{"content":" 从Incus出发 在过去的一些文章里，我曾经着迷于Incus作为容器实现。\n在我看来，LXC/Incus那一套共享内核同时分隔文件系统与命名空间的法则很有魅力，用来创建低开销的类虚拟机实例再适合不过了。\n不过，Incus的局限也有不少，或许配置麻烦就是其中之一，需要用户学习一套全新的配置模式。\n另一方面，尽管Incus已经实现OCI镜像支持，但它不直接兼容当下流行的Dockerfile + Docker Compose模式。\n理论上，这两者可被Canonical生态的cloud-init脚本替代，Incus对此有非常好的支持，但改写这些脚本总归是一个调试繁琐、易出Bug的工作。\n说到底，Incus更偏向于系统级容器的理念，本身就和Docker/Podman的应用级容器定位有很大差异。\n如果仍然迷恋Docker风格的应用级容器，或许可以试试Podman，其绝大部分操作API或指令均和Docker相似，并默认运行在用户态，不依赖root权限级别的daemon。\n安装 Podman的安装非常简单，在Debian下只需要：\n1 sudo apt install podman 在Windows下安装也非常简单，可参考Podman for Windows文档。基本步骤是：\n前往Github Release，下载最新版podman-installer-windows-amd64.exe文件（MSI也行；别被附件里几个不同名字的.exe骗了，它们的SHA256可能是相同的）； 开始安装，安装时可选HyperV或WSL后端，推荐WSL； 安装程序结束后，打开命令提示符，执行podman machine init，自动下载宿主虚拟机/WSL镜像并导入 若是好奇podman machine init命令干了什么，可以翻阅上面的文档，内有非常详细的介绍。\n常见问题 找不到podman.sock /run/user/\u0026lt;uid\u0026gt;/podman/podman.sock No such file or directory\n请尝试：\n1 systemctl --user enable --now podman.socket 容器莫名其妙自动结束 如果是以下情况：用户在保持有登录会话（如SSH）时，容器从不自动结束。而在用户关闭所有会话后一小段时间内，容器就自动退出了。\n那么请尝试：\n1 sudo loginctl enable-linger \u0026lt;username\u0026gt; Windows WSL Podman设置代理 若要让基于Windows WSL的Podman使用宿主机网络代理，通常要先确定宿主机的IP，再设置环境变量。\n太麻烦了，让我们用一个脚本解决这个问题吧。\n使用方法：\n复制以下脚本 根据自己的网络配置修改USER CONFIGURATION配置项 打开Podman WSL Machine，在WSL的/etc/profile.d/文件夹下新建个配置文件，贴进去 📝 WSL代理自动配置脚本 /etc/profile.d/wsl-proxy.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 #!/bin/bash # WSL Proxy Environment Setup Script # Place this script in /etc/profile.d/ to auto-load proxy settings for all users # Or source it manually # ============================================================================= # USER CONFIGURATION SECTION # Modify these variables according to your proxy server settings # ============================================================================= # Proxy protocol schema (http, https, socks4, socks5) # Default: http PROXY_SCHEMA=\u0026#34;${PROXY_SCHEMA:-http}\u0026#34; # Proxy server hostname or IP address # Use \u0026#34;{GATEWAY}\u0026#34; to automatically detect Windows host IP via WSL gateway # PROXY_HOST=\u0026#34;${PROXY_HOST:-localhost}\u0026#34; PROXY_HOST=\u0026#34;{GATEWAY}\u0026#34; # Proxy server port number # Default: 8080 PROXY_PORT=\u0026#34;${PROXY_PORT:-8080}\u0026#34; # Proxy authentication username (leave empty if not required) # Default: empty PROXY_USER=\u0026#34;${PROXY_USER:-}\u0026#34; # Proxy authentication password (leave empty if not required) # Default: empty PROXY_PASSWORD=\u0026#34;${PROXY_PASSWORD:-}\u0026#34; # Comma-separated list of addresses to bypass proxy # These domains/IPs will not use the proxy connection # Default: local addresses (localhost, 127.0.0.1, etc.) PROXY_BYPASS=\u0026#34;${PROXY_BYPASS:-localhost,127.0.0.1,::1,169.254.169.254}\u0026#34; # ============================================================================= # INTERNAL FUNCTIONS # ============================================================================= # Detect WSL gateway IP address (Windows host IP) # This is useful when proxy server runs on Windows host _get_wsl_gateway() { local gateway_ip # Method 1: Use ip route to find default gateway gateway_ip=$(ip route show default 2\u0026gt;/dev/null | awk \u0026#39;/default/ {print $3}\u0026#39; | head -n1) # Method 2: Fallback to /etc/resolv.conf nameserver (WSL2) if [[ -z \u0026#34;$gateway_ip\u0026#34; ]] \u0026amp;\u0026amp; [[ -r /etc/resolv.conf ]]; then gateway_ip=$(awk \u0026#39;/^nameserver/ {print $2; exit}\u0026#39; /etc/resolv.conf) fi # Method 3: Fallback to Windows host IP pattern (WSL1) if [[ -z \u0026#34;$gateway_ip\u0026#34; ]]; then gateway_ip=$(ip route show | grep -oP \u0026#39;via \\K[0-9.]+\u0026#39; | head -n1) fi echo \u0026#34;$gateway_ip\u0026#34; } # Build proxy URL with optional authentication _build_proxy_url() { local schema=\u0026#34;$1\u0026#34; local host=\u0026#34;$2\u0026#34; local port=\u0026#34;$3\u0026#34; local user=\u0026#34;$4\u0026#34; local password=\u0026#34;$5\u0026#34; local url=\u0026#34;${schema}://\u0026#34; # Add authentication if credentials provided if [[ -n \u0026#34;$user\u0026#34; ]]; then url+=\u0026#34;${user}\u0026#34; if [[ -n \u0026#34;$password\u0026#34; ]]; then url+=\u0026#34;:${password}\u0026#34; fi url+=\u0026#34;@\u0026#34; fi # Add host and port url+=\u0026#34;${host}:${port}\u0026#34; echo \u0026#34;$url\u0026#34; } # ============================================================================= # MAIN EXECUTION # ============================================================================= # Resolve special {GATEWAY} keyword to actual WSL gateway IP if [[ \u0026#34;$PROXY_HOST\u0026#34; == \u0026#34;{GATEWAY}\u0026#34; ]]; then PROXY_HOST=$(_get_wsl_gateway) # Validate gateway detection if [[ -z \u0026#34;$PROXY_HOST\u0026#34; ]]; then echo \u0026#34;Warning: Failed to detect WSL gateway IP. Using \u0026#39;localhost\u0026#39; as fallback.\u0026#34; \u0026gt;\u0026amp;2 PROXY_HOST=\u0026#34;localhost\u0026#34; fi fi # Construct full proxy URL PROXY_URL=$(_build_proxy_url \u0026#34;$PROXY_SCHEMA\u0026#34; \u0026#34;$PROXY_HOST\u0026#34; \u0026#34;$PROXY_PORT\u0026#34; \u0026#34;$PROXY_USER\u0026#34; \u0026#34;$PROXY_PASSWORD\u0026#34;) # Export standard proxy environment variables export HTTP_PROXY=\u0026#34;$PROXY_URL\u0026#34; export http_proxy=\u0026#34;$PROXY_URL\u0026#34; export HTTPS_PROXY=\u0026#34;$PROXY_URL\u0026#34; export https_proxy=\u0026#34;$PROXY_URL\u0026#34; # Export no-proxy settings export NO_PROXY=\u0026#34;$PROXY_BYPASS\u0026#34; export no_proxy=\u0026#34;$PROXY_BYPASS\u0026#34; # ============================================================================= # VERBOSE OUTPUT (optional) # Uncomment the following lines to enable debug output # ============================================================================= # echo \u0026#34;WSL Proxy Configuration Loaded:\u0026#34; # echo \u0026#34; HTTP_PROXY: $HTTP_PROXY\u0026#34; # echo \u0026#34; HTTPS_PROXY: $HTTPS_PROXY\u0026#34; # echo \u0026#34; NO_PROXY: $NO_PROXY\u0026#34; 为容器启用N卡 通常来说，只需要首先安装NVIDIA Container Toolkit，然后使用以下命令启动容器：\n1 podman run --rm --gpus all docker.io/debian:trixie-slim nvidia-smi 找不到GPU Podman主要通过Container Device Interface (CDI)寻找调用系统中的显卡。\n在安装NVIDIA Container Toolkit时，会附带一个Systemd服务nvidia-cdi-refresh.service，该服务理论上会在后台运行，自动查找系统中的N卡并刷新CDI信息。\n可以通过以下命令查看N卡是否成功注册到CDI中，正常应该是这样的：\n1 2 3 4 5 $ nvidia-ctk cdi list INFO[0000] Found 3 CDI devices nvidia.com/gpu=0 nvidia.com/gpu=GPU-12345678-1234-5678-90ab-1234567890ab nvidia.com/gpu=all 然而，有时这一服务无法正常运行（可通过sudo systemctl status nvidia-cdi-refresh.service查看），上述服务不能正确更新CDI中的N卡注册信息，最终会导致Podman无法找到我们想要启用的显卡资源。\n解决方法是，可以手动运行CDI注册命令：\n1 sudo nvidia-ctk cdi generate --output=/var/run/cdi/nvidia.yaml Podman Compose设置GPU 在docker-compose.yml文件中，可以这样定义使用GPU:\n1 2 3 4 5 6 services: llama-server: image: nvidia/cuda:12.2.0-runtime-ubuntu22.04 command: [\u0026#34;nvidia-smi\u0026#34;] devices: - nvidia.com/gpu=all 如果使用Docker Compose运行，可能会遇到错误：Error response from daemon: container create: stat nvidia.com/gpu=all: no such file or directory\n解决方法非常简单：请使用Podman-Compose而不是Docker-Compose:\n1 2 3 sudo apt install podman-compose podman-compose -f docker-compose.yml up ","date":"2025-12-03T16:00:00+08:00","permalink":"/2025/12/03/podman-migrating/","title":"Podman迁移计划"},{"content":" 背景知识 Stable Diffusion WebUI (以下简称sd-webui) 和 ComfyUI 都是调用模型进行AI图片视频生成的前端，并不直接提供具体模型，原则上这两个只需其一即可完成创作。\n至于优劣，原始A1111版sd-webui需要插件才能使用诸如LyCORIS之类更多的模型，在视频生产等工作上也没那么方便。ComfyUI 工作流上手则需要更多一点基础知识(我觉得其实还好)。\nStable Diffusion WebUI 系统依赖 作为一个AI任务，基础的CUDA组件自然是必不可少的，不再赘述。\n在容器Debian中，可能还需要额外安装一些组件：\n1 sudo apt install bc google-perftools libgl1 libglib2.0-0 --no-install-recommends 若是还有缺失的组件，莫慌，可以去https://packages.debian.org，在下面的Search the contents of packages中直接搜索缺失的库文件，安装对应的包即可，基本上不需要手动编译额外库文件。\n设置网络 在安装过程中，需要加载一些网络资源，需要首先设置网络访问。\n最简单的方法是设置一些PROXY，不过需要注意，要将localhost排除在外，否则webui.sh执行中会报错。\n1 2 3 export NO_PROXY=\u0026#34;localhost, 127.0.0.1, ::1\u0026#34; export HTTP_PROXY=http://127.0.0.1:8888 export HTTPS_PROXY=http://127.0.0.1:8888 Python环境准备 如果没有额外需求，将一切包袱丢给webui.sh，任其自动创建所需的虚拟环境，倒也是一种懒人方法。\n不过，如果期望自定义配置，那额外折腾的部分也不见得麻烦。\n默认情况下，sd-webui期望的Python版本是3.10，对于比较新的系统，请考虑使用Conda等方法创建虚拟环境，并指定Python版本号。\n虚拟环境激活后，sd-webui就会使用检测到的Python虚拟环境。\n📝 备注 使用uv管理环境 更激进一点，如果使用uv作为pip的替代品，情况又略有不同，因为sd-webui直接通过调用pip模块安装所需组件，但uv创建的虚拟环境并没有提供pip模块。\n幸好，安装过程也不麻烦，只需要不断重复运行webui.sh，把报错的pip install命令复制出来，直接改成uv pip install并执行即可。\n需要注意，uv pip install没有--prefer-binary，似乎直接省略也无伤大雅。\n克隆环境 1 git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui xformers 如果使用的是uv，那其实很好办，uv能自动处理xformers和torch的兼容关系，只需要在安装torch的时候一并安装即可。\n1 uv pip install \u0026#34;numpy\u0026lt;2\u0026#34; torch==2.1.2 torchvision==0.16.2 xformers --extra-index-url https://download.pytorch.org/whl/cu121 如果使用的是pip，在无法自动匹配正确版本，或是pip动了卸载torch的心时，则需要亲自去Releases · facebookresearch/xformers · GitHub翻一下时间线，寻找和环境中torch版本匹配的小版本，然后指定版本号手动安装。\n模型文件 把模型文件放到对应的models/Stable-diffusion/, models/VAE/, 还有models/Lora/文件夹下，就可以了。\n一些插件 1 2 3 4 5 6 7 8 9 10 11 12 13 git clone \u0026#34;https://github.com/Physton/sd-webui-prompt-all-in-one.git\u0026#34; extensions/sd-webui-prompt-all-in-one vim extensions/sd-webui-prompt-all-in-one/install.py vim extensions/sd-webui-prompt-all-in-one/scripts/physton_prompt/packages.py uv pip install chardet PyExecJS lxml pathos cryptography git clone \u0026#34;https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git\u0026#34; extensions/a1111-sd-webui-tagcomplete git clone https://github.com/CompVis/taming-transformers.git repositories/taming-transformers git clone https://github.com/hako-mikan/sd-webui-regional-prompter.git extensions/sd-webui-regional-prompter git clone https://github.com/pkuliyi2015/multidiffusion-upscaler-for-automatic1111.git extensions/multidiffusion-upscaler-for-automatic1111 git clone https://github.com/Mikubill/sd-webui-controlnet.git extensions/sd-webui-controlnet git clone https://github.com/dtlnor/stable-diffusion-webui-localization-zh_CN.git extensions/stable-diffusion-webui-localization-zh_CN git clone https://github.com/AlUlkesh/stable-diffusion-webui-images-browser.git extensions/stable-diffusion-webui-images-browser git clone https://github.com/arenasys/stable-diffusion-webui-model-toolkit.git extensions/stable-diffusion-webui-model-toolkit 启动！ 1 ./webui.sh ComfyUI 克隆环境 1 git clone https://github.com/comfyanonymous/ComfyUI.git 安装虚拟环境 如果使用的是uv，那其实很好办，uv能自动处理xformers和torch的兼容关系，只需要在安装torch的时候一并安装即可。\n和 A1111 的 sd-webui 不同，ComfyUI所需的Python依赖项均已提供在requirements.txt中。\n不过，默认的requirements.txt没有定义torch的版本，也没有定义xformers依赖项（xformers能大幅减少显存消耗）。所以，我们可以稍微修改一下。\nComfyUI对任意新版torch的兼容性都比较好，所以我们可以去pytorch官网，找到自己适合的torch版本，将版本信息写到requirements.txt中。同时，加上xformers依赖项。\n1 2 3 4 5 6 7 8 9 10 -torch +torch==2.5.1 -torchvision +torchvision==0.20.1 -torchaudio +torchaudio==2.5.1 +xformers 其他就都不用管了。直接创建一个虚拟环境，然后开装！\n1 2 3 4 uv venv venv -p 3.12 source venv/bin/activate uv pip install -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu121 模型文件复用 装完环境后，如果原本安装有sd-webui，还有一大堆模型文件，不想将模型文件再复制一份？可以直接将ComfyUI目录下的extra_model_paths.yaml.example复制一份，命名为extra_model_paths.yaml，然后按照里面的提示进行编辑，就可以指示ComfyUI前往sd-webui的目录搜索加载Stable Diffusion模型检查点了。\nNginx反向代理 如果在使用Nginx反向代理ComfyUI，并在保存工作流时遇到报错: Error storing user data file 'workflows/xxx': 405，请检查Nginx配置文件，看proxy_pass末尾是否存在斜杠。\n如果存在斜杠，请将其删除。\n1 2 3 4 5 6 7 8 location / { # 这是不OK的！ - proxy_pass http://127.0.0.1:8188/; # 这是OK的！ + proxy_pass http://127.0.0.1:8188; } 参考: [Bug]: Save workflow 405(Method Not Allowed) When using Nginx reverse\n提示词插件 手写提示词太痛苦，尤其是习惯了sd-webui的sd-webui-prompt-all-in-one插件后。\n幸好，ComfyUI也有类似的插件，叫WeiLin-Comfyui-Tools。只需要git clone进ComfyUI/custom_nodes，然后根据该插件的requirements.txt补齐Python依赖，在ComfyUI中就可以使用类似的Prompt编辑器了。\n当然，要是不喜欢，也可以用原来all-in-one插件的独立版本sd-webui-prompt-all-in-one-app，可以作为一个独立的Web应用运行。\n","date":"2025-08-09T11:00:00+08:00","permalink":"/2025/08/09/stable-diffusion-webui-comfyui-install/","title":"Stable Diffusion WebUI 与 ComfyUI 安装"},{"content":"省流: 直接在git lfs pull中使用--include/-I和--exclude/-X选择或者排除需要拉取LFS文件。\n快速下载 在Huggingface等平台上，各种模型数据库文件都会被存储为Git LFS对象。\n如果不想使用Huggingface等平台提供的专用工具，如何方便地下载呢？\n最简单的，无脑的方法，只需要遵照Huggingface模型页面提供的代码，克隆仓库即可\n1 git clone https://huggingface.co/my/model 但是！这种方法会一次性拉取并检出所有文件，包括所有LFS文件对象。\n有时，我们并不需要仓库中的所有LFS文件。比方说，一部分仓库同时提供.pth和.safetensors格式的模型权重，但我们只需要其中一种就够了。\n那么，有没有更自定义一点的法子？\n我们可以在克隆时忽略克隆完整的LFS对象，只保留LFS对象的指针。与此同时，正常拉取仓库中其他普通文件。\n1 GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/my/model 这样一来，仓库中所有非LFS对象就获取完毕了，接下来只需要按需获取LFS文件。怎么做呢？非常简单，git lfs pull提供了一个--include/-I参数：\n1 git lfs pull --include \u0026#34;*.safetensors\u0026#34; 与之相对应地，还有一个--exclude/-X参数，可以指定排除的文件列表。\n这样，就可以非常简单地选择性拉取一部分LFS文件，也不需要将命令拆分为多条命令，或是配置git仓库之类的。\n","date":"2025-08-05T11:00:00+08:00","permalink":"/2025/08/05/git-lfs-quick-pull/","title":"Git LFS 选择性拉取文件"},{"content":" uv是什么？ 请看: https://docs.astral.sh/uv/\n简单来说，uv是一个Rust写的Python环境与包管理器，旨在取代pip。\n基本的uv安装配置 No! 我们的宝贵时间，不应该浪费在这种重复工作上！请直接参考官网教程。\n对喜欢自己动手，不喜欢官方安装脚本的人来说，安装也很简单：直接去Github Release下载发行包，把uv和uvx丢进~/.local/bin，确保这个目录在PATH里，然后往~/.bashrc加两句：\n1 2 eval \u0026#34;$(uv generate-shell-completion bash)\u0026#34; eval \u0026#34;$(uvx --generate-shell-completion bash)\u0026#34; 开始使用 定义项目依赖 uv当然可以像pip那样使用，例如uv pip install numpy。但这似乎不是最优雅的方法。\n对于uv来说，每个文件夹就可以被认为是一个Project。因此，推荐的做法是，在pyproject.toml定义项目依赖的pip包，以及版本号要求等。例如:\n1 2 3 4 5 6 7 8 [project] name = \u0026#34;my-project\u0026#34; version = \u0026#34;0.0.1\u0026#34; dependencies = [ \u0026#34;numpy\u0026#34;, \u0026#34;torch==2.5.1\u0026#34;, \u0026#34;torchvision==0.20.1\u0026#34;, ] 这之后，只需要执行一句:\n1 uv sync uv就会在当前目录.venv下创建一个虚拟环境，并安装pyproject.toml中定义的依赖项。\n只需要等待处理完毕，然后source .venv/bin/activate，即可激活使用环境。\n配置pypi软件源 众所周知，配置软件仓库镜像站已成为配环境的必备之路。\n配环境笑传之Configure仓必。\nuv默认不读取pip.conf中的配置项，因此先前针对pip配置的软件源不会影响uv的设置。\n如果希望镜像配置仅对当前项目生效，可以直接编辑pyproject.toml，填入以下内容:\n1 2 3 [[tool.uv.index]] url = \u0026#34;https://mirrors.sustech.edu.cn/pypi/web/simple\u0026#34; default = true 📌 重要 关于Pytorch\n鉴于眼下PyTorch包没有更新频繁、好用高速的镜像站，基本还是走的官方下载渠道。\n因此，如果只是简单需要在环境中安装指定CUDA/ROCM版本的PyTorch，不必像我下面的示例一样配置。\n只需要简单地在uv pip install命令中指定--torch-backend参数即可。具体可参考文档。\n例如：uv pip install \u0026quot;torch==2.8.0\u0026quot; --torch-backend=cu126，即可安装CUDA 12.6版本的Torch 2.8啦。\n而对于一些使用独立软件源的包（如PyTorch），则需要定义额外的index项目，并在sources中显式要求这些包使用这些软件源:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [tool.uv.sources] torch = [ { index = \u0026#34;pytorch-cu121\u0026#34; }, ] torchvision = [ { index = \u0026#34;pytorch-cu121\u0026#34; }, ] torchaudio = [ { index = \u0026#34;pytorch-cu121\u0026#34; }, ] [[tool.uv.index]] name = \u0026#34;pytorch-cu121\u0026#34; url = \u0026#34;https://download.pytorch.org/whl/cu121\u0026#34; explicit = true 上述index定义适用于提供index-url，也就是符合PEP 503规范的软件源。\n而对于find-links的软件源，只需要在定义index时加一句format = \u0026quot;flat\u0026quot;即可。例如:\n1 2 3 4 5 6 7 8 9 10 11 12 pyg_lib = [ { index = \u0026#34;pyg-torch251-cu121\u0026#34; }, ] torch_scatter = [ { index = \u0026#34;pyg-torch251-cu121\u0026#34; }, ] [[tool.uv.index]] name = \u0026#34;pyg-torch251-cu121\u0026#34; url = \u0026#34;https://data.pyg.org/whl/torch-2.5.1+cu121.html\u0026#34; format = \u0026#34;flat\u0026#34; explicit = true 怎么判断需要用的软件源要不要加format = \u0026quot;flat\u0026quot;一句呢？\n很简单，看这个软件源提供的pip安装示例代码。如果其通过-i/--index-url \u0026lt;url\u0026gt;指定软件源，那就不需要加。反之，如果是通过-f/--find-links \u0026lt;url\u0026gt;指定的，那就需要。\n📝 备注 TOML语法 包括我在内，一定有人好奇，为啥上面的配置文件中，为啥[[tool.uv.index]]中需要两对方括号[[]]？\n这是TOML中的Array of Tables语法，Table实际上就是字典。\n具体到uv配置文件，可以认为uv的配置项目中存在一个index数组，该数组包含了多条index信息字典。\n例如：\n1 2 3 4 5 6 7 8 [[tool.uv.index]] url = \u0026#34;https://mirrors.sustech.edu.cn/pypi/web/simple\u0026#34; default = true [[tool.uv.index]] name = \u0026#34;pytorch-cu121\u0026#34; url = \u0026#34;https://download.pytorch.org/whl/cu121\u0026#34; explicit = true 等价到JSON中就是:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { \u0026#34;tool\u0026#34;: { \u0026#34;uv\u0026#34;: { \u0026#34;index\u0026#34;: [ { \u0026#34;url\u0026#34;: \u0026#34;https://mirrors.sustech.edu.cn/pypi/web/simple\u0026#34;, \u0026#34;default\u0026#34;: true, }, { \u0026#34;name\u0026#34;: \u0026#34;pytorch-cu121\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://download.pytorch.org/whl/cu121\u0026#34;, \u0026#34;explicit\u0026#34;: true, }, ], }, }, } 配置文件细节 uv会从多个位置探测并加载可能的配置文件，按照大致的覆盖顺序为（后面的会覆盖先前的）：\nSystem-Level Config Unix: /etc/uv/uv.toml和$XDG_CONFIG_DIRS/uv/uv.toml Windows: %SYSTEMDRIVE%\\ProgramData\\uv\\uv.toml Use-Level Config Unix: ~/.config/uv/uv.toml和$XDG_CONFIG_HOME/uv/uv.toml Windows: %APPDATA%\\uv\\uv.toml Project-Level Config ./pyproject.toml ./uv.toml 和pyproject.toml不同，uv.toml中的配置项不需要包含tool.uv前缀。\n例如，为了配置uv使用的pypi镜像站，在pyproject.toml中可以指定:\n1 2 3 [[tool.uv.index]] url = \u0026#34;https://mirrors.sustech.edu.cn/pypi/web/simple\u0026#34; default = true 而在uv.toml中，则需要写成:\n1 2 3 [[index]] url = \u0026#34;https://mirrors.sustech.edu.cn/pypi/web/simple\u0026#34; default = true 管理Python版本 uv还提供了管理Python版本的功能，可以像conda那样，安装指定版本的Python解释器。\n例如，如果需要安装3.14版本的Python:\n1 uv python install 3.14 同时，如果希望为每个项目指定版本号，也可以在pyproject.toml中指定requires-python。例如：\n1 2 [project] requires-python = \u0026#34;\u0026gt;=3.12,\u0026lt;3.13\u0026#34; 然而，uv使用的Python二进制包来自astral-sh/python-build-standalone项目，该项目是托管在Github上的，下载有时候并不顺畅。\n一种解决思路是，在uv配置中指定python-install-mirror，指向你能访问到的最快的Github。例如，在pyproject.toml中:\n1 2 [tool.uv] python-install-mirror = \u0026#34;https://github.com/astral-sh/python-build-standalone/releases/download\u0026#34; 手动创建虚拟环境 是的，总有人喜欢掌控一切的感觉。\n例如，如果就是不喜欢uv默认在每个项目下创建.venv的行为，就是喜欢创建一些集中式的虚拟环境，然后在其他项目中激活使用，那该怎么办呢？\n很简单:\n1 uv venv /path/to/venv 甚至还能指定一下Python版本号:\n1 uv venv ./venv -p 3.14 不过，在可能的时候，uv会使用文件链接创建环境，多个环境内的相同依赖项会被复用。所以其实不太需要担心创建环境带来的磁盘重复开销问题。\n但是，如果需要使用uv sync安装软件包，uv sync默认使用的环境是当前目录下的.venv。如何让uv sync使用任意虚拟环境呢？\n也很简单，只需要先激活对应的环境，然后在uv sync加上一个参数--active即可，这个参数会命令uv使用和修改当前已经激活好的环境。\n1 2 source /path/to/venv/bin/activate uv sync --active 和Conda/Mamba一起使用 到目前为止，既然uv已经能实现管理Python版本的功能，实话说已经想不太出Conda/Mamba的用武之地。\n如果希望在Conda中创建环境，然后使用uv管理软件包，那其实整体上和上面的手动创建虚拟环境是类似的。\n不过，uv sync可能会遇到问题，无法识别Conda创建的环境。这时候就需要使用uv pip install安装包。\n1 2 mamba activate -n myvenv uv pip install -r pyproject.toml uv pip install是最通用的包安装方法，-r pyproject.toml表示从pyproject.toml中读取依赖项并安装，该参数也可以是requirements.txt。\n链接模式 在安装软件包时，如果目标虚拟环境和uv的缓存不在一个文件系统下，可能会弹出以下警告:\n1 2 3 warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance. If the cache and target directories are on different filesystems, hardlinking may not be supported. If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning. 在Windows和Linux下，uv默认通过创建缓存到site-packages的hardlink以安装软件包。(MacOS下是创建的CoW克隆，不过我没有Mac，不太清楚实现细节)\n然而，众所周知，Hardlink不能跨文件系统创建，那难道就要像上面的说明一样，给环境文件都做个全量复制？\n好奇宝宝们，就不好奇一下，有没有比较折中的方案——SymbolLink吗？\n有的，兄弟，有的。不过要翻一翻手册才能明确。只需要在命令行中指定--link-mode symlink:\n1 uv sync --link-mode symlink 🚨 注意 Symlink模式与缓存兼容性 好奇宝宝们，有没有想过，在文件系统中，Symlink无法追踪一个源文件正在被多少个链接追踪。\n也就是说，对于缓存系统内的某一个包，uv无法追踪缓存中的包被链接到了多少个虚拟环境中，这些虚拟环境是否还存在。\n要命的是，uv cache clean的机制似乎是简单地删除整个缓存目录，删起来就不知道天地为何物了。\n对Hardlink来说，这不会造成什么影响，因为文件系统会自动维护每个文件inode的指针数量。\n但对Symlink来说，删除缓存后所有虚拟环境中的Symlink都会变成无根之树，全都指向不存在的路径。截至2025年7月，这一问题仍然存在。\n考虑到这一问题主要出在无法创建硬链接的跨文件系统场景下，一种折中的方法是：在与uv缓存相同的文件系统上创建虚拟环境，然后将整个虚拟环境文件夹软链接到希望使用的地方，如项目路径下等。\n1 2 3 ls /local/disk/.cache/uv/ uv venv /local/disk/envs/myenv ln -s /local/disk/envs/myenv /another/fs/myproject/.venv ","date":"2025-07-28T11:00:00+08:00","permalink":"/2025/07/28/manage-python-venv-with-uv/","title":"使用uv管理Python环境"},{"content":" 1 2 3 4 5 6 7 8 9 10 11 12 13 root@enabling-clam:~/testdir# echo \u0026#34;foolish rm\u0026#34; \u0026gt; \u0026#34;-rf\u0026#34; \u0026amp;\u0026amp; ls -la total 8 -rw-r--r-- 1 root root 11 Jul 8 06:37 -rf drwxr-xr-x 1 root root 32 Jul 8 06:37 . drwx------ 1 root root 52 Jul 8 06:35 .. drwxr-xr-x 1 root root 14 Jul 8 06:36 mydir1 -rw-r--r-- 1 root root 6 Jul 8 06:36 myfile1 root@enabling-clam:~/testdir# rm * \u0026amp;\u0026amp; ls -la total 4 -rw-r--r-- 1 root root 11 Jul 8 06:37 -rf drwxr-xr-x 1 root root 6 Jul 8 06:37 . drwx------ 1 root root 52 Jul 8 06:35 .. 一段有趣的Bash命令记录 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 root@enabling-clam:~# mkdir testdir \u0026amp;\u0026amp; cd testdir \u0026amp;\u0026amp; ls -la total 0 drwxr-xr-x 1 root root 0 Jul 8 06:35 . drwx------ 1 root root 52 Jul 8 06:35 .. root@enabling-clam:~/testdir# mkdir mydir1 \u0026amp;\u0026amp; echo \u0026#34;Test1\u0026#34; \u0026gt; myfile1 \u0026amp;\u0026amp; echo \u0026#34;Test2\u0026#34; \u0026gt; mydir1/myfile2 \u0026amp;\u0026amp; ls -la total 4 drwxr-xr-x 1 root root 26 Jul 8 06:36 . drwx------ 1 root root 52 Jul 8 06:35 .. drwxr-xr-x 1 root root 14 Jul 8 06:36 mydir1 -rw-r--r-- 1 root root 6 Jul 8 06:36 myfile1 root@enabling-clam:~/testdir# echo \u0026#34;foolish rm\u0026#34; \u0026gt; \u0026#34;-rf\u0026#34; \u0026amp;\u0026amp; ls -la total 8 -rw-r--r-- 1 root root 11 Jul 8 06:37 -rf drwxr-xr-x 1 root root 32 Jul 8 06:37 . drwx------ 1 root root 52 Jul 8 06:35 .. drwxr-xr-x 1 root root 14 Jul 8 06:36 mydir1 -rw-r--r-- 1 root root 6 Jul 8 06:36 myfile1 root@enabling-clam:~/testdir# rm * \u0026amp;\u0026amp; ls -la total 4 -rw-r--r-- 1 root root 11 Jul 8 06:37 -rf drwxr-xr-x 1 root root 6 Jul 8 06:37 . drwx------ 1 root root 52 Jul 8 06:35 .. root@enabling-clam:~/testdir# rm -rf \u0026amp;\u0026amp; ls -la total 4 -rw-r--r-- 1 root root 11 Jul 8 06:37 -rf drwxr-xr-x 1 root root 6 Jul 8 06:37 . drwx------ 1 root root 52 Jul 8 06:35 .. root@enabling-clam:~/testdir# rm \u0026#34;-rf\u0026#34; \u0026amp;\u0026amp; ls -la total 4 -rw-r--r-- 1 root root 11 Jul 8 06:37 -rf drwxr-xr-x 1 root root 6 Jul 8 06:37 . drwx------ 1 root root 52 Jul 8 06:35 .. root@enabling-clam:~/testdir# rm \u0026#34;./-rf\u0026#34; \u0026amp;\u0026amp; ls -la total 0 drwxr-xr-x 1 root root 0 Jul 8 06:38 . drwx------ 1 root root 52 Jul 8 06:35 .. root@enabling-clam:~/testdir# 为什么？ 这两个问题本质上都与 Bash 的 Shell展开 有关。\n文件名展开 先看 rm * 一句命令，为什么这一行命令看起来像被解析成了 rm -rf mydir1 myfile1？\n原因很简单：文件名通配符 * 遵照 Bash 的文件名展开机制，在真正执行 rm 命令前就被处理完毕。\nAfter word splitting, \u0026hellip; Bash scans each word for the characters *, ?, and [ \u0026hellip; then the word is regarded as a pattern, and replaced with a sorted list of filenames matching the pattern\n也就是说，rm 命令接收到的，是经过 Bash 展开后的文件名列表，在上面的例子中就等价于：\n1 rm -rf mydir1 myfile1 双引号处理 我们还注意到，rm \u0026quot;-rf\u0026quot; 一句命令并没能成功移除 -rf 文件。明明我们已经试着用引号告诉 rm 这是一个整体，为什么还会失败呢？\n在 Bash 中，经过参数展开、文件名展开等处理的命令在传递给程序前，还会最终经过 引号移除。\n引号移除发生在所有展开操作的最后，所有未经引号或转移保护的斜杠、单双引号都会被移除。\n这也就是说，对于上面的例子，双引号对 -rf 的保护仍然局限在 Shell 处理阶段，保护其中字符的字面值，阻止空格分词。\n然而，稍后 rm 命令收到的仍然是 -rf 字符串，并在稍后将其解析为 -r 和 -f 两个 option，最后还是没能将其作为要删除的文件名称进行处理。\n解决方案与启示 想要正确删除 -rf 文件，有以下两种方法：\n1 2 3 4 5 # Same as Example rm ./-rf # Another Example rm -- -rf 上面两个例子都不需要给 -rf 加引号。\n对于第一种写法，其提供给 rm 的参数首字符为点号 . ，以避免被 rm 当作短横线参数 - 处理。 第二种写法中，-- 通常约定表示“选项到此结束”，位于 -- 后面的参数，即使以 - 开头，也应该被视为操作数而非选项。 Shell 层安全 != 命令语义安全\n","date":"2025-07-08T14:00:00+08:00","permalink":"/2025/07/08/funny-rm-rf/","title":"有趣的rm -rf"},{"content":"对于按段落组织的文档来说，Markdown可以说是最易于排版和使用的格式了，同时兼具机器可读性与人类可读性。\n那么，在一些情况下，我们可能会希望将Markdown转换成PDF文档，方便打印也好方便传输也好，这个需求或许并不少见。\n这篇文章将介绍如何使用工具Pandoc，借助LaTeX将Markdown转换为PDF文档。\n📝 备注 值得一看的新项目: Typst\nTypst大体上基于Markdown语法撰写文档，同时提供了许多额外命令调整格式等，且程序性能远远快于LaTeX。就私人文档与只提交PDF的简单排版场景来说，Typst无疑是LaTeX之外的一个不错选择。\nTypst似乎目前的最大局限性在于，许多出版商要求最终提交Word或LaTeX工程文件。截止2025年2月，Typst暂时没有被主流出版商接受，也似乎暂时没有成熟的转换为LaTeX的方案。\n工具选择 虽然我们介绍的方法是基于pandoc和LaTeX的，但其实条条大路通罗马，想要实现Markdown至PDF的转换并非只有这一条路。只要思想不滑坡，办法总比困难多。\n本文介绍的方法，使用pandoc与LaTeX进行转换。可能效果是最好的，但也是最费事的。 由于Markdown到HTML的渲染工具极其丰富且高效，因此可以直接找个在线转换工具，将Markdown转成HTML，再用浏览器打开HTML，打印网页为PDF。 把Markdown导入到WPS云文档、飞书、Google Docs等任意在线文档中，再导出成PDF（没想到吧！）。 如果没有特殊文本格式要求，直接把Markdown全文复制粘贴到Word中，导出（最鸡肋的一条）。 Why Pandoc? 为啥我们在通往罗马的条条大路里，选了最费事的一条呢？因为：\n这条路径涉及到的工具完全开源，完全可控地运行在本地。 提供了非常多的可配置参数，（理论上）可以任意配置导出PDF的格式，字体、边距啥的都不在话下。 吃饱了闲的。 但甜蜜常常是痛苦的来源，这一套方案麻烦就麻烦在，需要考虑的事情比较多，比较麻烦，不是开箱即用的方案。\n如果你还有兴致继续研究，让我们往下看吧。\n环境配置 环境配置之前需要先稍稍解释一下Pandoc+LaTeX方案的原理。\nPandoc是如何将Markdown转换成PDF的呢？实际上可以理解成两个步骤：\nPandoc将Markdown转换成TeX文件。 Pandoc再调用LaTeX组件根据第一步生成的TeX文件进一步生成PDF文件。 那么，很显然，我们需要安装的软件就是：\nPandoc LaTeX发行版(TeXLive，或者MiKTex，或者其他啥都好) 如果有将 含有中文字符（中日韩语言字符） 的文档转换成PDF的需要，那么在安装LaTeX时，请记得安装xelatex组件（除非你打算折腾ctex）。\n准备Markdown 接下来，需要先准备好要转换的Markdown文档。文档的主要内容可以都保持不变，只需要向Markdown文件开头添加一个YAML Metadata块，填入文章相关的一些信息如主标题、作者、日期等，这样生成的文档中就会像学术论文一样包含这些信息。\n举个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 --- title: 文章的主标题 author: 作者名称 date: March 01, 2025 --- # 第一章 这是第一章的内容。 ## 第一小节 这是第一小节的内容。 上面的例子中，两行---围住的，就是这个Markdown文件的Metadata块，这些信息将被传递给Pandoc的LaTeX模板，以生成LaTeX文件。\n有哪些信息可以通过Metadata块进行传递呢？这取决于使用的模板文件。例如，转换成LaTeX的默认模板可见default.latex，有需要的可以进一步研究。\n小试牛刀 好了，软件都装好了，Markdown文件也准备好了，那应该就可以开心地转换了吧？\npandoc命令并不复杂，如果输入的Markdown文件是input.md，期望输出的文件名是output.pdf，那么命令就是：\n1 pandoc input.md -o output.pdf 执行命令，轰，报错了，怎么回事呢？\n排错与常见问题 LaTeX Error: Unicode character 熟悉LaTeX的小伙伴都知道，常见的pdfLaTeX是不支持中文字符的。对中文字符（或者说中日韩文字）支持最好的，应当是XeLaTeX，这一引擎可以直接使用系统中安装好的字体生成PDF。\n所以，我们要告诉Pandoc，使用xelatex引擎为我们生成PDF文件。\n1 pandoc input.md -o output.pdf --pdf-engine xelatex 但这还不够，Pandoc还是不太清楚怎么寻找中文字体，因此我们还要告诉Pandoc需要使用的中日韩文字字体。以思源宋体(Source Han Serif SC)为例：\n1 pandoc input.md -o output.pdf --pdf-engine xelatex -V CJKmainfont=\u0026#34;Source Han Serif SC\u0026#34; 再次编译，生成的PDF文件就应该正确包含中文字符啦。\n除了在Pandoc命令中指定使用的字体，还可以通过Markdown文件中的YAML Metadata块进行指定。以上命令等价于修改Markdown文档如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 --- title: 文章的主标题 author: 作者名称 date: March 01, 2025 CJKmainfont: \u0026#34;Source Han Serif SC\u0026#34; --- # 第一章 这是第一章的内容。 ## 第一小节 这是第一小节的内容。 事实上，通过YAML Metadata块传递信息，与通过pandoc命令的-V参数传递信息，都是一样的，都是指定template variables的一部分。LateX字体设置的默认模板可以见font-settings.latex\nPDF文件中文段落不换行 如果按照前面的步骤配置XeLaTeX和CJK字体，那么应该不会发生这一问题才是。\n但如果你在看到这篇文章前已经搜索了很长时间，并且你在其他人那得到的命令是通过mainfont参数指定字体，那或许这就是原因。\n请你将pandoc命令中的-V mainfont替换成上一个小节中给出的-V CJKmainfont，再次尝试。\n这似乎是最简洁的方案，不需要像很多博客那样下载额外的模板文件。\nPDF文件的外边距太宽 默认LaTeX模板的外边距非常宽，一页PDF下来没有多少空间留给内容。想要修改PDF文件的边距，同样可以通过传递参数实现。\n1 2 3 4 5 6 --- # 其他template variables geometry: margin=2cm --- 正文内容。 ","date":"2025-03-05T12:00:00+08:00","permalink":"/2025/03/05/markdown-to-pdf-with-pandoc/","title":"使用Pandoc将Markdown转换成PDF文档"},{"content":"对于喜欢折腾的人来说，经常会遇到环境玩崩的情况，在Linux下更是一件常事。而如果每次都要重装系统，其实又非常不方便，也没必要。\n在BTRFS文件系统逐步稳定并可用的今天，如果我们将根文件系统迁移到了BTRFS，或许，我们可以借助BTRFS快照功能，实现Linux环境的无痛切换与还原？\n思路 为了方便，我们假设一个非常无聊的例子：\n希望在一个分区内，安装一个Debian系统 在Debian系统上，分别安装配置GNOME和KDE两个桌面环境 实现两个桌面环境的根文件系统互不干扰，可按需分别启动 需要说明的是，这套配置里，我们假设包括/usr, /etc等路径均没有分配独立的挂载点，否则那就太麻烦啦！/home是否独立挂载取决于环境隔离的程度。\n仔细一想，想要实现上述功能的思路是非常简单的：\n在BTRFS文件系统上安装一个基础操作系统 对基础操作系统进行快照 根据基础快照，创建两组新的读写快照（子卷），在两个子卷内分别安装GNOME和KDE环境，并按需切换 其实，以Debian为例，如果在安装系统时指定了根文件系统为BTRFS文件系统，Debian安装程序会贴心地为我们创建一个@rootfs子卷，而后每次启动电脑时，实际上都是将@rootfs子卷挂载成/节点。因此，以上步骤的前两步都已被自动完成。\n似乎，我们只要解决第三步就行？但麻烦的也在这第三步。\nGRUB的麻烦 回忆一下，Linux是如何确定自动挂载文件系统配置的？第一个想到的一定是/etc/fstab文件。\n还是以Debian的为例，默认情况下，如果设置根文件系统为BTRFS，在/etc/fstab文件里面，我们可以找到类似于以下定义：\n1 2 # / was on /dev/sda1 during installation UUID=12345678-1234-1234-5678-123456789012 / btrfs defaults,subvol=@rootfs 0 0 显然，这一句的意思是，将硬盘上的@rootfs子卷，挂载到/路径上。\n那么接下来，我们似乎只要创建一个@rootfs的快照子卷，然后修改上面的fstab文件，把subvol=后面替换成新子卷名称，就可以实现快速切换了？\n一开始我也是这么认为的，直到我安装完KDE桌面重启后，对着仍然黑麻麻的屏幕两眼发黑，我才意识到不对劲。\n让我们瞄一眼/boot/grub/grub.cfg文件：\n1 2 3 4 5 6 7 8 9 menuentry \u0026#39;Debian GNU/Linux\u0026#39; --class debian --class gnu-linux --class gnu --class os $menuentry_id_option \u0026#39;gnulinux-simple-12345678-1234-1234-5678-123456789012\u0026#39; { # ...... insmod btrfs # ...... echo \u0026#39;Loading Linux 6.1.0-30-amd64 ...\u0026#39; linux /@rootfs/boot/vmlinuz-6.1.0-30-amd64 root=UUID=12345678-1234-1234-5678-123456789012 ro rootflags=subvol=@rootfs quiet echo \u0026#39;Loading initial ramdisk ...\u0026#39; initrd /@rootfs/boot/initrd.img-6.1.0-30-amd64 } 好吧……事实上根文件系统的子卷名称，是写入到了GRUB配置文件里的。在GRUB引导时，会按照/boot/grub/grub.cfg的配置查找子卷，然后加载该子卷中的系统。\n而要修改GRUB配置，似乎就有点麻烦了。如果手动修改配置，需要在每次安装完驱动或更新内核后再手动调整一遍配置，极其麻烦且容易疏漏。而如果希望自动修改，又暂时没有好主意。\n重命名子卷 修改GRUB配置的路子走不通，系统默认引导@rootfs子卷的行为有点难修改。既然如此，为什么我们不通过替换@rootfs子卷的方式，狸猫换太子呢！\n安装基本系统 首先正常安装Debian基本系统，在配置分区的时候，为/挂载点分配一个BTRFS文件系统。\n进行基础配置 安装完系统后，重启进入基本系统，进行必要的配置，安装一些在不同环境下都用得上的包。也就是准备一个基本系统的过程啦！\n拍摄基础快照 重启进LiveCD，挂载BTRFS分区整个卷：\n1 2 mount /dev/sda1 /mnt cd /mnt 我们给系统子卷@rootfs创建一个快照@sysbase（名字可以随意起），用于记录此刻基本系统的状态：\n1 btrfs subvolume snapshot @rootfs @sysbase 如果希望基本系统内容不变，可以增加一个选项-r，以创建一个只读快照。\n配置环境1 接着，重启电脑，按需要配置第一个环境，例如安装GNOME桌面等。\n配置环境2 之后，如果我们想要从基本系统开始重新配置一个新环境，我们需要再次进入LiveCD，挂载BTRFS分区完整卷到/mnt下。\n这回，我们需要把环境1，也就是现在的@rootfs先移到一边，也就是重命名一下。\n1 mv @rootfs @gnome-env 现在，我们的硬盘上似乎只有两个子卷了：\n1 2 3 /mnt/ ├── @gnome-env └── @sysbase 我们现在想让系统回到基本系统，也就是回到@sysbase的状态。怎么做呢？我们可以，给@sysbase再创建一个快照，让这个快照成为新的@rootfs：\n1 btrfs subvolume snapshot @sysbase @rootfs 接着重启电脑，我们就回到了基本系统状态，此时我们对系统的修改，就变成了我们的环境2。\n还要更多环境！ 如果还需要更多环境，只需要像配置环境2时一样，将当前@rootfs重命名一下，再从基本系统，或者任何一个其他快照，创建一个新的@rootfs快照，重启电脑，就可以创建新环境了。\n而如果我们希望在环境之间切换，也非常简单，同样地，将当前@rootfs重命名为其他名字，再将想要切换的快照重命名成@rootfs，或者创建一个新@rootfs快照都行。\n如果我们玩坏了环境，也不用完全重装系统，只需要将不想要的环境子卷删除，btrfs subvolume delete @broken-env，再选择一个好的环境为@rootfs即可。非常灵活！\n","date":"2025-01-26T16:00:00+08:00","permalink":"/2025/01/26/btrfs-linux-quick-snapshot/","title":"利用BTRFS快照实现快速Linux环境切换"},{"content":" 📝 喜报 这张P104显卡，在搬运过程中由于震动，金手指不幸接触不良，享练习时长一年半。\n目前，该显卡的任务已由另一张锻炼过的P102-100所替代。\n在内存全面涨价，V100价格先落后涨的2026年，P104价格已经落到80出头，而P102也只需要180元。\n很难说这些电子垃圾还有没有必要购买，但下一代A100等产品的大船恐怕没那么容易靠岸。\n从某海鲜市场捡回一个P104显卡。\n是的，2025年了，还在捡垃圾卡，毕竟在当前的大模型浪潮下，连退役多年的麦克斯韦架构Tesla M40 24G二手推理卡，都能从2年多前500上下的价格炒到现在的1200以上。就这，还不是每个商家都有货呢。\n与此同时，曾经的垃圾佬挚爱P106-100，价格也来到了130以上的区间。有没有搞错，一两年前这玩意只需要大约一半的价钱。而P102-100价格已经来到了280附近，好吧，1080Ti的核心屏蔽一下，12G显存还是有一定可玩性的。\n相较之下，捡来的这张P104-100，8G显存比P106的6G显然更有可玩性。而大约是1070水平的核心显然不能和P102比，但在我下单时只需要100出头的价格，不仅比P102便宜了一半有余，甚至还没有溢价后的P106昂贵。反正买回来是插在Linux主机上，不指望打游戏，P104似乎是省钱和好玩的平衡点。\n显卡上机，暂时没有爆炸，这里记录一下如何把显卡映射到Incus容器环境里，让容器能利用显卡的方法。\n不得不说，有时候捡垃圾确实能学到一些东西。或者说，捡垃圾的优点就在于，它能教会大家解决买全新产品不会遇到的问题的能力。\n宿主机配置 NVIDIA驱动 显然，开源的驱动nouveau是不能用的。\n常见的建议是安装英伟达官方打的.run驱动包，这当然可以，但我个人不是很喜欢这种方法。\n事实上，只需要装上系统apt源里的英伟达闭源驱动，如Debian的nvidia-driver包，或者是Ubuntu的nvidia-headless-550之类，就是OK的了。\n除了驱动，还要在主机上安装一个NVIDIA Container Toolkit，这个工具能自动将主机上的NVIDIA运行时及工具等挂载到容器中。\n一定会有人问：要不要在主机上装CUDA Toolkit？\n在Ubuntu官方提供的教程中，的确提到需要在宿主机上安装CUDA Toolkit。但根据我的实验，如果在宿主机上没有使用CUDA工具如nvcc等的需要，其实完全没必要在宿主机上装CUDA Toolkit。\n实际上，根据网上的说法，CUDA Toolkit主要包含三部分内容：\nNVIDIA驱动程序（跟我们用apt装的，.run文件装的没啥区别） CUDA SDK（NVCC，编译器，库文件，示例程序等） 其他工具（Debugger等） 显然，后两个部分实际上，要么是静态文件，要么是运行在用户态的程序，这些程序显然遵循“在哪里使用，就在哪里安装”的方法比较好。\n因此，关于CUDA的结论是：\n如果完全不涉及到C++ CUDA工具的编译等任务，可以完全不安装CUDA Toolkit； 如果只是想在容器中使用nvidia-smi等工具，也不需要安装CUDA Toolkit，系统会自动将宿主机的nvidia-smi映射到容器中； 如果需要在容器里使用NVCC等SDK工具，则需要在容器中安装CUDA Toolkit； 📝 备注 P104等矿卡与驱动问题\n显然，P104这样的矿卡流入用户市场，并不是皮衣男想看到的。\n一般来说在Windows上使用矿卡，都建议使用民间的魔改驱动。但在Linux下就比较微妙了。\n官方驱动是否会限制显卡频率？\n有坊间传闻说，自从535版本开始，N卡就在驱动程序层面限制了矿卡的功耗。\n但就我测试的情况来看，使用Debian软件源中提供的535.216版本N卡驱动，P104在nvidia-smi工具中仍然能跑到180W的功率。\n因此暂时无法确认该说法是否正确，以及该说法的适用范围。\n矿卡能否使用NVENC视频解码器\n另一个坊间传闻是，皮衣男还限制了矿卡上的NVENC视频编码器，要使用民间魔改驱动才能解锁NVENC。\n我测试的结果是，使用ffmpeg命令无法显示出当前系统支持NVENC编码器。而在jellyfin-ffmpeg中设置使用NVENC硬件加速会报错。\n因此暂时没有找到使用矿卡NVENC编码器的有效方法。\n容器配置 时至今日，虚拟化GPU并将物理GPU与虚拟机共享，已经不是一件新鲜事了，从WSL到Docker，都有非常成熟且广泛的应用。\n在Incus/LXD容器框架下，这一功能同样也是被支持的。然而，Incus的文档实在是语焉不详，缺少明确可复用的示例。说人话就是，官方文档看了一头雾水，没东西可以抄，最后绝知此事还是得躬行。\n首先初始化一个容器：\n1 incus create images:debian/12 gpu-test 指定使用显卡：\n1 2 incus config set gpu-test nvidia.runtime=true nvidia.driver.capabilities=all incus config device add gpu-test nvidia-0 gpu 到现在，启动容器，在/dev/下就可以找到N卡的设备文件了，同时也可以使用nvidia-smi等工具查看显卡信息。\n值得一提的是，上面指定的NVIDIA Driver Capability，可选的值及意义如下表：\nDriver Capability Description compute required for CUDA and OpenCL applications. compat32 required for running 32-bit applications. graphics required for running OpenGL and Vulkan applications. utility required for using nvidia-smi and NVML. video required for using the Video Codec SDK. display required for leveraging X11 display. 一般来说，如果只需要运行CUDA计算等，指定为utility,compute就足够了。\n附赠：Micromamba配置教程 🚨 注意 悲报\n就在这篇笔记写成之后没几天，PyTorch宣布自v2.6开始不再发布官方Conda包。如有需要，请直接安装 pip 包或者安装 conda-forge 渠道包。\n好吧，除非有同时使用多个版本Python的需求，不然conda/mamba在Python生态中的应用场景就比较狭窄了。\nmamba is a CLI tool to manage conda s environments.\nIf you already know conda, great, you already know mamba!\nMamba是一个C++编写的Conda实现，而Micromamba是Mamba的精简版，不附带base环境，只需要一个独立的可执行文件就能承担起以往Conda的职能，而且速度更快。\n安装很简单，只需要前往Micromamba的Github Release下载最新的包，解压后提取其中的bin/micromamba可执行文件，放到自己的可执行文件目录下，例如~/.local/bin，然后确保将这一目录加入到PATH环境变量中，就算安装好了一半。\n接着执行：\n1 micromamba shell init -s bash -r ~/micromamba 这会修改~/.bashrc，配置与Micromamba相关的环境变量。如果使用的是zsh，则需要将上面的shell部分改成-s zsh。\n接下来我们还要配置使用的软件源。事实上，Micromamba与以往的Conda的配置文件.condarc完全兼容，如果曾经安装过Conda，其实不需要重复配置。\n如果是全新安装，则可以编辑~/.mambarc，填写以下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 channels: - defaults show_channel_urls: true default_channels: - https://mirrors.sustech.edu.cn/anaconda/pkgs/main - https://mirrors.sustech.edu.cn/anaconda/pkgs/free - https://mirrors.sustech.edu.cn/anaconda/pkgs/r - https://mirrors.sustech.edu.cn/anaconda/pkgs/pro - https://mirrors.sustech.edu.cn/anaconda/pkgs/msys2 custom_channels: conda-forge: https://mirrors.sustech.edu.cn/anaconda/cloud msys2: https://mirrors.sustech.edu.cn/anaconda/cloud bioconda: https://mirrors.sustech.edu.cn/anaconda/cloud menpo: https://mirrors.sustech.edu.cn/anaconda/cloud pytorch: https://mirrors.sustech.edu.cn/anaconda/cloud simpleitk: https://mirrors.sustech.edu.cn/anaconda/cloud 现在似乎就可以用Micromamba创建环境并安装包了。\n报错 报错1: Critical libmamba: No CA certificates found on system, aborting\n1 sudo apt install ca-certificates 报错2: Error libmamba: ZSTD decompression error: Unknown frame descriptor\n尝试换一个镜像源。\n","date":"2025-01-15T22:00:00+08:00","permalink":"/2025/01/15/incus-nvidia-gpu/","title":"在Incus容器中使用NVIDIA显卡"},{"content":"曾经用过一段时间的Zerotier作为异地组网/内网穿透的工具，用了一段时间感觉还行。\n后来听说了Tailscale这个工具，再后来了解到了还有Headscale这样的开源实现，觉得这玩意似乎更加优雅，于是，折腾了半个晚上后总算把网络组起来了。在这里记录一下详细的过程。\n术语 这类网络工具总是存在非常多的专有名词，所以得稍微先介绍一下：\nTailscale Tailscale是一个基于WireGuard协议的组网工具。这一协议的好处是，在建立设备间通信时会尝试打洞，从而实现设备之间的P2P点对点通信，即便这些设备位于一些NAT网关后（不过根据NAT类型不同，打洞成功的可能性也不同，可参见：How NAT traversal works）。\nTailscale的工作流程不准确概括如下，如果希望更深入了解，可以阅读Tailscale: How it works。\nTailscale官方搭设了一系列中央服务器，为用户提供注册、通信建立与数据中继等功能。 用户在需要组网的设备上安装Tailscale客户端，登录账号并加入虚拟子网。 虚拟子网建立后，所有子网内设备在通信时，将先与Tailscale中央服务器进行通信，判断两个设备间能否直连（打洞）。 如果能打洞，直连(DIRECT)；如果不能打洞，则两个设备间的通信将需要中继服务器转发流量，即中继连接(RELAY)。 Tailscale服务器 值得注意的是，在Tailscale中，中央服务器和中继服务器可以是不同的服务器。\n中央服务器, Control Server: 控制用户认证、存储组网配置等信息。 中继服务器, DERP Server: 专用于在设备间无法建立P2P直接通信时，转发设备间流量实现互相访问。 值得注意的是，所有设备间流量在网络上传输时都是加密的，且私钥都在设备本地，因此通常认为中继服务器是无法解密传输流量的。\nHeadscale Tailscale官方尽管允许自行部署DERP服务器，但Tailscale中央服务器程序是闭源的，官方也不允许我们部署。而人们又常常因为一些原因，想要或者需要搭建自己控制的中央服务器，真正地将所有信息都掌控在可控范围内。\n因此，Headscale，也就是今天我们要介绍的主角出现了。Headscale是Tailscale中央服务器程序的开源实现，并采用了BSD协议发行，我们可以任意部署到我们的服务器上。\n有意思的是，Headscale的一位主要开发者实际上是Tailscale公司的雇员，Headscale的开发也受到了Tailscale公司的支持，参见: Making heads or tails of open source。\n除了基本的Headscale程序，我们今天还会稍稍提一嘴Headscale-ui，这是一个管理Headscale服务器的Web UI界面，可以免去敲命令管理Headscale组网的烦恼。一般来说需要为Headscale配置API Key，这点我们后面说。\n技术细节 这篇文章采用的小技术细节：\nHeadscale运行在Docker上，方便管理； Nginx实现反向代理； 提供Web UI管理Headscale服务器； 使用一个子域名如myvlan.example.com作为访问入口； 在已托管有好几个网站的服务器上搭设，因此如TLS证书、Nginx配置等都与现有其他网站兼容。 配置Headscale服务器 准备工作 显然，在开始配置自己的Headscale中央服务器之前，我们需要：\n一台具备公网IP的服务器（至少对你来说，这个IP地址应当确保在你的应用场景下，总是可达的） 一个可用的域名（没有也能组网，但域名能省去非常多麻烦。同时，也假设已经为该域名配置好了HTTPS与证书） Docker（直接在本机系统上设置也行，具体可以参考安装文档） Nginx（如果你的服务器同时还运行着其他网站，需要充当反向代理的角色） 接下来让我们开始吧！\n准备配置文件 首先我们需要前往Headscale的Github仓库，复制一份配置文件样板config-example.yaml。\n按需要修改配置文件，可以参考下面我的示例，一些无关紧要的项目注释已被删除，有需要可以参考上述完整的样板文件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 --- # Headscale服务器暴露在公网上的地址 # 该地址可被用于客户端与服务器的通信 server_url: https://myvlan.example.com # Headscale服务器监听的端口 # 使用Docker可以直接写0.0.0.0:8080 # 若直接在本机系统上运行且8080端口被占用，可调整为其他空闲端口 listen_addr: 0.0.0.0:8080 # Headscale /metrics Web API # 可用于获取Headscale运行状态 # 根据自身需求选择是否暴露该API metrics_listen_addr: 127.0.0.1:9090 # Headscale gRPC 端口，用于CLI工具远程控制服务器 # 该功能需要配合证书使用 # 根据自身需求选择是否暴露该端口 grpc_listen_addr: 127.0.0.1:50443 grpc_allow_insecure: false noise: private_key_path: /var/lib/headscale/noise_private.key # 设置Headscale分配的虚拟内网IP地址段 # 根据设计，Headscale要求内网IP段 **必须** 在以下子网范围内： # - IPv4: 100.64.0.0/10 # - IPv6: fd7a:115c:a1e0::/48 # 请注意，这一功能与CGNAT冲突，见：https://tailscale.com/kb/1015/100.x-addresses # 因此不能在如阿里云等启用了CGNAT的云服务器上安装 **Headscale客户端** # 也就是不能将云服务器作为一台客户机加入VLAN。 prefixes: v4: 100.98.76.0/24 v6: fd7a:115c:a1e0::/48 # IP分配策略，有`sequential`和`random`两种可选 allocation: sequential ## DERP配置 # # 配置Headscale向客户机所分发的DERP服务器列表 # 这些DERP服务器可由3种不同方式定义配置 # 三种方法建议启用至少一种，以实现Relay模式下的访问 derp: # 方法1：Headscale内置DERP服务器 server: # 主开关，指定是否启用Headscale服务器内置的DERP服务器 # 启用后，可以将这台服务器主机作为转发中继流量的节点之一 # 由于DERP服务器强制要求TLS，因此如果启用这一选项，`server_url`必须以`https`开头 enabled: true # 内置DERP服务器的Region ID # 可以随便写，但要注意检查与其他DERP的Region ID冲突 # 如果冲突，内置DERP的Region ID会覆盖外部导入DERP region_id: 999 # 随便写 region_code: \u0026#34;myoffice\u0026#34; region_name: \u0026#34;DERP in My Office\u0026#34; # 用于DERP服务器STUN连接的端口，可以自定义 # 需要前往云服务器防火墙设置中放行对应端口的UDP连接 # 注意，这一端口不需要Nginx反代 stun_listen_addr: \u0026#34;0.0.0.0:10086\u0026#34; private_key_path: /var/lib/headscale/derp_server_private.key automatically_add_embedded_derp_region: true # 填写云服务器的IP地址 ipv4: 1.2.3.4 ipv6: 2001:db8::1 # 方法2：可订阅的JSON格式响应API，获取一系列DERP服务器列表 # 在示例配置文件中，默认包含了一条从Tailscale服务器上拉取DERPMAP的URL # 如果想完全禁用Tailscale提供的服务器，请直接将这一段改成：`urls: []` urls: - https://controlplane.tailscale.com/derpmap/default # 方法3：本地DERP YAML文件定义 # 指定所使用的DERP定义YAML文件绝对路径 # 示例： # paths: # - /etc/headscale/myderp.yaml paths: [] auto_update_enabled: true update_frequency: 24h disable_check_updates: true ephemeral_node_inactivity_timeout: 30m database: type: sqlite debug: false gorm: prepare_stmt: true parameterized_queries: true skip_err_record_not_found: true slow_threshold: 1000 sqlite: path: /var/lib/headscale/db.sqlite write_ahead_log: true wal_autocheckpoint: 1000 ## TLS配置 # # 因为我的服务器上已配置好HTTPS，且我希望由Nginx反向代理管理HTTPS连接 # 因此本配置文件将这部分内容置空，令Headscale服务器跳过自动配置TLS # 如果有需要，请参考官方提供的完整示例文件 acme_url: https://acme-v02.api.letsencrypt.org/directory acme_email: \u0026#34;\u0026#34; tls_letsencrypt_hostname: \u0026#34;\u0026#34; tls_letsencrypt_cache_dir: /var/lib/headscale/cache tls_letsencrypt_challenge_type: HTTP-01 tls_letsencrypt_listen: \u0026#34;:http\u0026#34; tls_cert_path: \u0026#34;\u0026#34; tls_key_path: \u0026#34;\u0026#34; log: format: text level: info policy: mode: file path: \u0026#34;\u0026#34; ## DNS配置 # # Headscale可支持Talescale的DNS配置以及MagicDNS，具体可参考文档： # - https://tailscale.com/kb/1054/dns/ # - https://tailscale.com/kb/1081/magicdns/ # - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ # - https://tailscale.com/kb/1235/resolv-conf # 如果不希望Headscale/Tailscale管理DNS，请将以下所有子项目值置空 dns: # MagicDNS主开关 # 我个人挺喜欢的一项功能，可以自动为每台加入虚拟局域网的主机分配一个DNS记录 # 如果不需要，或在复杂网络配置下担心扰乱现有网络配置，请关闭这一选项 magic_dns: true # MagicDNS基础域名（二级虚拟域名），附加在主机名后作为DNS记录 # 这一名称不可以和`server_url`使用的域名相同 # 建议可以使用如`.lan`等目前IANA没有分配的顶级域名，不干扰其他正常域名解析 # # 例如，若`base_domain`取值为`my.lan`， # 则可以用`myroom.my.lan`指代并访问网络中名为`myroom`的主机 base_domain: my.lan # 向客户端暴露的DNS列表，要求客户端使用这些DNS进行解析 # 这一选项在公司等网络中非常有用 nameservers: global: - 114.114.114.114 - 223.5.5.5 - 8.8.8.8 - 2400:3200::1 - 2001:4860:4860::8888 # Split DNS (参见：https://tailscale.com/kb/1054/dns/), split: {} # foo.bar.com: # - 1.1.1.1 # darp.headscale.net: # - 1.1.1.1 # - 8.8.8.8 search_domains: [] # 设置Headscale服务器上额外的DNS记录 # 目前只支持A和AAAA记录 # 参见：docs/ref/dns.md extra_records: [] # - name: \u0026#34;git.myroom.my.lan\u0026#34; # type: \u0026#34;A\u0026#34; # value: \u0026#34;100.98.76.2\u0026#34; # # # 也可以写在一行里 # - { name: \u0026#34;git.myroom.my.lan\u0026#34;, type: \u0026#34;A\u0026#34;, value: \u0026#34;100.98.76.2\u0026#34; } # # 也可以从一个外部JSON文件导入，该文件每次被更改时会被自动解析加载 # extra_records_path: /var/lib/headscale/extra-records.json # Unix socket可以让CLI不需要认证就能连接到服务器控制程序，直接抄就行 unix_socket: /var/run/headscale/headscale.sock unix_socket_permission: \u0026#34;0770\u0026#34; logtail: enabled: false randomize_client_port: false 写好以后，保存，文件名为config.yaml。\n准备Docker 在上一节中准备好的config.yaml文件，我们将其放入一个文件夹，假设叫做hs-etc。\n现在我们需要准备Docker需要的docker-compose.yml文件，可以将其与hs-etc文件夹放到一块，例如以下文件结构：\n1 2 3 4 5 6 hs-server/ ├── docker-compose.yml └── hs-etc └── config.yaml 2 directories, 2 files 编辑docker-compose.yml文件如下。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 services: headscale: image: headscale/headscale restart: unless-stopped container_name: headscale ports: # Headscale服务器主端口，如果不打算使用反向代理可以去掉前面的`127.0.0.1` - \u0026#34;127.0.0.1:10080:8080\u0026#34; # /metrics API端口，根据需要开放（在前面Headscale配置里需要开放外部监听） # - \u0026#34;127.0.0.1:10089:9090\u0026#34; # DERP STUN，如果启用了内置DERP服务器，请在这里映射对应端口 # 再次提醒，DERP STUN端口不需要经过Nginx反代 # - 10086:10086 # - 10086:10086/udp volumes: # 配置文件文件夹 - ./hs-etc/:/etc/headscale # 用一个卷volume存放Headscale的数据库等 - hs-data:/var/lib/headscale # 将本机系统时间与时区等映射到容器内 - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro command: serve volumes: hs-data: 完成后，保存。\n启动容器 Docker，启动！\n1 docker compose up -d 可以curl访问一下刚刚映射的主端口，检查Headscale是否正常启动。Headscale对于根节点/请求将返回404，所以记得用-D -输出响应头检查确认。\n1 2 3 4 user@MyServer:~$ curl -D - http://127.0.0.1:10080 HTTP/1.1 404 Not Found Date: Sun, 11 Jan 2025 12:24:22 GMT Content-Length: 0 开放防火墙（可选） 如果配置了DERP服务器，请务必记得在系统防火墙与云服务商控制面板中放行对应端口，例如上面示例配置文件中的10086端口的UDP通信。\n配置Nginx反向代理 不过，到目前为止，我们搭建的Headscale中央服务器还没有映射到我们的子域名，如myvlan.example.com上。\n而且，还记得我们前面提到，最好为服务器启用TLS加密吗？在这一套实现中，这相关的工作也要交给Nginx完成。\n这里放上Nginx的示例配置文件，按需修改其中使用的子域名、SSL证书路径、反向代理Headscale服务器地址等参数后，就可以和其他Nginx配置一起使用了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 map $http_upgrade $connection_upgrade { default upgrade; \u0026#39;\u0026#39; close; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name myvlan.example.com; index index.html index.htm; ssl_certificate /path/to/your/cert/fullchain.pem; ssl_certificate_key /path/to/your/cert/privkey.pem; ssl_dhparam /path/to/your/dhparam.pem; ssl_prefer_server_ciphers on; ssl_session_timeout 10m; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection \u0026#34;1; mode=block\u0026#34;; location / { # 这里填写Headscale中央服务器的本地监听端口 proxy_pass http://127.0.0.1:10080; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $server_name; proxy_redirect http:// https://; proxy_buffering off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; add_header Strict-Transport-Security \u0026#34;max-age=15552000; includeSubDomains\u0026#34; always; } } server { listen 80; listen [::]:80; server_name myvlan.example.com; if ($host = myvlan.example.com) { return 301 https://$host$request_uri; } return 404; } 重启Nginx，访问子域名，看是否能响应HTTP请求。\n开始组网 添加用户 在Tailscale中，每个组成的虚拟子网，都被称为一个Tailnet。一个Tailnet可以加入多台设备，且在一个Tailnet内，所有设备彼此可见，通过一个局域网IP进行互相访问。而即便是在同一个中央服务器上，隶属于不同Tailnet的设备，彼此之间是不可见的。\n而在Headscale中，与 Tailnet 相对应的概念是 User。将不同的设备加入到一个User下，就意味着这些设备隶属于同一个用户，该用户下的所有设备共享一个虚拟子网。\n现在，让我们来创建一个用户（子网）吧！在中央服务器上执行命令：\n1 docker exec -it headscale headscale users create \u0026lt;username\u0026gt; 这里请把\u0026lt;username\u0026gt;替换成任意喜欢的名称，执行后即可成功创建。\n📝 备注 Users还是Namespaces? 许多关于Headscale的文档还使用headscale namespaces命令管理子网，似乎它的功能和users差不多？\n其实，在2023年1月（似乎是v0.19.0）之后的版本中，Headscale将所有namespaces相关的命令和概念都换成了users。\n所以，如果使用的是新版的Headscale，请按照users进行使用。\n参见以下Issues\nRename Namespace to User #329 Rename [Nn]amespace -\u0026gt; [Uu]ser #1144。 注册设备 常规方法 现在，我们需要将我们想要彼此连接的设备添加到刚刚创建的用户（子网）中。\n在设备上安装好Tailscale客户端后，打开终端或命令提示符，执行以下命令。\n1 tailscale login --login-server https://myvlan.example.com 命令行会返回类似如下的信息：\n1 2 3 To authenticate, visit: https://myvlan.example.com/register/ab-CDE_fghijkl0123456789 可以继续访问上面的链接，网页会告诉我们，需要前往Headscale中央服务器执行一串命令。实际上，这行命令的作用就是让Headscale中央服务器接受我们在客户端上的入网请求。\n我们也可以注意到，在刚刚给出的链接中最后一部分，例如上面链接的ab-CDE_fghijkl0123456789，其实就是认证用的密钥。\n让我们回到中央服务器，执行命令：\n1 docker exec -it headscale headscale nodes register -u \u0026lt;username\u0026gt; -k mkey:\u0026lt;yourkey\u0026gt; 请将\u0026lt;username\u0026gt;替换成用户名（子网名），\u0026lt;yourkey\u0026gt;替换成在上面链接中获得的密钥，例如ab-CDE_fghijkl0123456789。\n预认证密钥（另一种方法） 当然，还有另一种入网方法。\n我们可以先在服务端上为设备预先创建好一个密钥，然后让客户端设备拿着密钥进行认证，这样就不需要从客户端上想办法获得确认口令，再去服务器上批准入网了。\n在中央服务器上执行：\n1 docker exec headscale headscale preauthkeys create -u \u0026lt;username\u0026gt; -e 2h 这里，-u \u0026lt;username\u0026gt;仍然是用户名，而-e 2h表示这条预认证密钥将在2小时后失效。因此在创建这条密钥之后，需要在2小时内到客户端上使用并完成认证。\n没有意外的话，服务器会返回类似以下内容：\n1 2 2025-01-11T12:00:00+08:00 TRC expiration has been set expiration=7200000 abcdefghijklmnopqrstuvwxyz0123456789012345678901 第二行就是生成的预认证密钥啦！让我们来到客户端，修改一下刚刚的入网命令：\n1 tailscale login --login-server https://myvlan.example.com --auth-key \u0026lt;yourkey\u0026gt; 把\u0026lt;yourkey\u0026gt;替换成上面的预认证密钥，执行命令，就可以成功入网，不需要再次回到中央服务器批准请求啦。\n开心使用 组网之后，最基本的使用当然是利用分配的虚拟子网IP互相访问。不过如果仅限于此，似乎又差点意思。\nMagicDNS 事实上这是我最喜欢的Tailscale/Headscale功能之一。\n在使用Zerotier时，在设备间使用域名进行访问相当困难，似乎唯一可行的方法是在子网内搭建私有DNS服务器，手动维护DNS记录。而mDNS这类基于广播的方案在Zerotier上支持也并不好。\n但是，在Headscale上，一切就变得简单起来了！\n回到先前的Headscale配置文件，如果我们定义了类似于以下内容：\n1 2 3 dns: magic_dns: true base_domain: my.lan 假设在我们的房间里有一台设备的Hostname为myroom的主机，那么我们就可以用myroom.my.lan这个域名访问这台主机。在这个过程中，我们甚至不太需要和设备的虚拟子网IP地址打交道。\nMagicDNS与多级域名 人总是贪心的，当MagicDNS提供了一个域名指向一台机器，就一定会有人希望能设置多个DNS记录指向同一台机器，从而根据不同域名分别访问不同的Web服务，类似于公网上的subdomain.example.com。\n设想一下，假设myroom主机上同时运行了一个Gitlab实例和一个PhotoPrism实例。我们很自然地希望，在myroom主机的80或443端口上设置一个Nginx服务器，分别监听前往git.myroom.my.lan和pic.myroom.my.lan域名的请求，再分别反向代理到对应服务端口上。这样可以免去记忆服务端口号的烦恼。\n感谢MagicDNS，和在公网上类似，我们可以在Headscale内网中设置多条A或AAAA记录，指向不同的IP。\n我们找到Headscale服务器配置中以下部分（在本文前面的配置文件示例中，注释已经写得很详细了）：\n1 2 dns: extra_records: [] 根据目标地址的域名、IP分别修改一下：\n1 2 3 4 5 dns: extra_records: - name: \u0026#34;git.myroom.my.lan\u0026#34; type: \u0026#34;A\u0026#34; value: \u0026#34;100.98.76.2\u0026#34; 保存配置文件，重启Docker，现在就可以在客户端上通过git.myroom.my.lan域名访问地址为100.98.76.2的主机了。\n不过这种方案并不优雅，每次增删改DNS记录后都要重启Headscale实例。还好，我们可以进一步引入一个外置的DNS记录配置JSON文件避免这种尴尬。\n创建一个dns-records.json文件，用于和config.yaml一同挂载到Docker容器内。\n1 2 3 4 5 6 7 hs-server/ ├── docker-compose.yml └── hs-etc ├── config.yaml └── dns-records.json 2 directories, 3 files 在dns-records.json中填入类似以下内容：\n1 2 3 4 5 6 7 [ { \u0026#34;name\u0026#34;: \u0026#34;git.myroom.my.lan\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;A\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;100.98.76.2\u0026#34; } ] 再修改一下config.yaml：\n1 2 dns: extra_records_path: /etc/headscale/dns-records.json 重启Headscale。在此之后，Headscale会在每次检测到dns-records.json发生改动时，自动读取该文件并更新DNS记录。\n⚠️ 警告 DNS记录JSON文件格式问题 在headscale/docs/ref/dns.md中有这样一段话：\nBe sure to \u0026ldquo;sort keys\u0026rdquo; and produce a stable output in case you generate the JSON file with a script. Headscale uses a checksum to detect changes to the file and a stable output avoids unnecessary processing.\n似乎是说，在写入文件时，需要尽可能保持输出格式的稳定，也就是保证一个字典内包括\u0026quot;name\u0026quot;, \u0026ldquo;type\u0026rdquo;, \u0026ldquo;value\u0026quot;在内的keys顺序保持一致稳定。\n子网路由 (Subnet Router) 考虑以下场景，假设我们有3台电脑：\nMyLaptop (IP: 公共网络): 随我们走天下的笔记本电脑，安装了Tailscale客户端 MyServer (IP: 192.168.100.101): 放在房间里的常开服务器主机，也安装了Tailscale客户端 MyDev (IP: 192.168.100.102): 放在家里的奇怪开发板，和MyServer在同一个网络，但无法安装Tailscale 现在，假设我们出门在外，需要通过网络访问MyDev设备的资源。我们应该怎么做呢？\n如果不想深入配置，那么最简单的办法自然是，首先通过Tailscale内网用SSH访问MyServer，再操作MyServer在物理内网中访问MyDev。\n然而，这种方法自然是不够优雅的。\n让我们看看Tailscale的解决方案：子网路由 Subnet Router\n在Headscale文档中，对应Routes这一章节。\n原理 在上述案例中，倘若我们出门在外，连接的公共网络自然无法访问MyServer和MyDev所在的内网192.168.100.0/24网段。\n但既然我们可以通过Tailscale内网访问到MyServer，为什么不能让MyServer充当192.168.100.0/24的转发器，让MyServer转发访问该网段的请求呢？\n因此，子网路由正是针对上述场景中，我们需要访问的目标机器因种种限制无法安装Tailscale，但Tailscale内网中又存在另一台机器可访问到目标机器的情况。\n转发客户端设置 还是用上面的例子，现在我们需要将MyServer设置为转发器。\n那么首先，如果MyServer是Linux系统，我们需要启用操作系统的转发功能：\n1 2 echo \u0026#39;net.ipv4.ip_forward = 1\u0026#39; | sudo tee -a /etc/sysctl.d/99-tailscale.conf echo \u0026#39;net.ipv6.conf.all.forwarding = 1\u0026#39; | sudo tee -a /etc/sysctl.d/99-tailscale.conf 还是在MyServer上，需要告诉Tailscale客户端，向网络广播前往192.168.100.0/24网段的路由：\n1 sudo tailscale set --advertise-routes=192.168.100.0/24 到此为止，客户端的设置基本上就完成了。\nHeadscale服务器授权 出于安全考虑，显然不能允许任意一台客户端都随意声称自己拥有某某网段，然后控制该网段的路由权。\n因此，在客户端中设置广播路由后，我们还需要登录到Headscale服务器，首先运行以下命令：\n1 docker exec headscale headscale nodes list-routes 会获得类似以下输出：\n1 2 ID | Hostname | Approved | Available | Serving (Primary) 6 | MyServer | | 192.168.100.0/24 | 可以看到，192.168.100.0/24网段已经出现在Available列中，但还没有获得批准。\n现在，我们需要批准MyServer转发192.168.100.0/24网段：\n1 docker exec headscale headscale nodes approve-routes --identifier 6 --routes 192.168.100.0/24 其中，--identifier就是前面的ID。\n命令完成后，再次执行headscale nodes list-routes，应该输出如下：\n1 2 ID | Hostname | Approved | Available | Serving (Primary) 6 | MyServer | 192.168.100.0/24 | 192.168.100.0/24 | 192.168.100.0/24 Linux客户端设置 到此为止，如果MyLaptop是Windows系统，那么已经可以试着直接访问192.168.100.0/24网段下的MyDev设备啦。\n但如果MyLaptop是Linux系统，还需要执行最后一步操作：\n1 sudo tailscale set --accept-routes 一切应该都OK啦！\n（附赠）自签名HTTPS证书 搭建起Headscale后，一种常见用途就是访问内网其他设备的Web服务。在Tailscale/Headscale内网中，所有数据包都是加密后发送的，理论上讲其实并不会因为使用HTTP直接访问造成安全问题。\n但是，一部分Web应用要求必须使用安全连接，因此通常又有使用HTTPS的需求。\n然而，显然，没有任何第三方CA会给我们的内网域名颁发TLS证书。考虑到内网中的设备其实都是我们自己使用的，此时，自签名证书就成了最方便灵活的选择。\n一般的自签名证书只对应于一组域名，在有多个设备多个域名的场景下，相比起重复颁发并在客户端上信任多个证书，或是顶着浏览器的不安全警告继续访问，或许我们其实可以采用类似于第三方CA的做法。\n具体来说，我们的思路是：\n先签发一个根证书(Root CA Certificate)，此时我们就扮演了Root CA的角色。 使用根证书，为需要信任的域名（比方说myroom.my.lan）颁发一个新的证书（本文称为子证书）。 子证书安装到提供该Web服务的服务器上，并配置使用如Nginx等提供HTTPS服务。 在客户端（也就是访问Web服务的终端）上安装根证书，此时客户端将信任所有基于该根证书签发的子证书。 由此，可以在客户端上愉快地内网HTTPS服务。 在未来需要为新的域名颁发证书（例如another.my.lan）时，只需要执行2~3步骤，不再需要在客户端上安装新证书，因为根证书已经被信任了。 接下来是具体步骤，这些步骤需要使用OpenSSL执行。除特别说明外，这些证书与密钥的存储位置与文件名都是比较随意的，只需要考虑合适的文件权限与维护的便利即可。\n签发Root CA根证书 我们首先需要做好扮演Root CA的任务，有了根证书以后才能基于该证书签发新的证书。\n第一步，生成Root CA的私钥rootca.key。该文件需要妥善保管，不能外泄。（其实泄了也没啥，反正是跑在虚拟内网上的。）\n下面命令中末尾的4096是RSA密钥长度。都2025年了，别再用2048位的RSA了。\n1 openssl genrsa -out rootca.key 4096 📝 备注 证书加密签名算法的问题 一定有小伙伴想，RSA都是多久以前的加密算法了，要不我们换一点新玩意，比方说ECDSA，EdDSA之类的（其实这些算法地位上似乎并不能并列）。\n事实上，ECDSA已经开始用于一些网站中。但是EdDSA几乎没有得到支持。而且，EdDSA的证书在Windows 10中无法被正确识别。\n考虑到ECDSA是NSA弄出来的玩意，要不，我们还是用更长的RSA吧。\n第二步，准备一个Root CA证书生成配置文件rootca.conf。该文件主要是提供证书中包含的信息，如别名等。\n在这个配置文件中，[req_distinguished_name]段的内容可以随意更改。自签名证书嘛，随便填，好玩就行。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [req] distinguished_name = req_distinguished_name x509_extensions = v3_ca prompt = no [req_distinguished_name] # Country/Region C = CN # State/Province ST = Somewhere # Location/City L = OnTheCloud # Organization O = Togawa Group # Common Name CN = Togawa Group ROOT CA v1 [v3_ca] basicConstraints = critical,CA:TRUE subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer:always keyUsage = critical,digitalSignature,keyCertSign,cRLSign 第三步，使用上面的私钥和配置文件，接下来我们就可以生成Root CA的根证书文件rootca.crt了。\n这个根证书文件rootca.crt，就是接下来可安装到各客户端上的证书。\n1 openssl req -x509 -new -key rootca.key -days 3650 -sha512 -config rootca.conf -out rootca.crt 顺带一提，如果不希望使用配置文件rootca.conf控制证书信息，也可以去掉-config rootca.conf这一参数。OpenSSL会启动一个问答，根据提示填写即可。\n签发子证书 假设我们希望给myroom.my.lan这台服务器签发一个通配符证书。该证书既信任myroom.my.lan域名，也同时信任*.myroom.my.lan这一泛域名，日后我们使用pic.myroom.my.lan这样的域名访问该服务器时，就不再需要签发新证书了。\n首先，我们需要为新的子证书生成一个私钥myroom.key。原则上讲，该私钥不需要透露给Root CA。不过现在只有我们一个人，这些都无所谓啦。\n1 openssl genrsa -out myroom.key 4096 第二步，编写子证书的证书请求配置文件myroom.conf。和前面一样，[req_distinguished_name]块的内容可以随意配置，但CN也就是Common Name部分需要填写为证书对应的域名。\n同时，如果希望该证书同时信任其他域名，只需要在[alt_names]块部分按格式继续添加即可。注意，myserver.lan和*.myserver.lan是不同的域名，需要分别列出！\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [req] distinguished_name = req_distinguished_name prompt = no [req_distinguished_name] C = CN ST = Tokyo L = Tokyo O = Anon Tokyo CN = myroom.my.lan [v3_req] basicConstraints = CA:FALSE keyUsage = digitalSignature, keyEncipherment authorityKeyIdentifier = keyid:always,issuer:always extendedKeyUsage = serverAuth subjectAltName = @alt_names [alt_names] DNS.1 = myroom.my.lan DNS.2 = *.myroom.my.lan 第三步，接下来需要生成子证书的证书请求(Certificate Request)文件myroom.csr。在Root CA和子证书请求分离的场景下，证书请求是可以发送给Root CA以申请证书的。\n1 openssl req -new -key myroom.key -config myroom.conf -out myroom.csr 第四步，Root CA收到了证书请求myroom.csr后，利用Root CA自己的私钥，以及根证书和配置文件，最终生成子证书myroom.crt。\n1 2 openssl x509 -req -in myroom.csr -CA rootca.crt -CAkey rootca.key -CAcreateserial -days 730 -sha512 -extfile myroom.conf -extensions v3_req -out myroom.crt 到此为止，所有需要的证书、密钥文件都已经生成。只需要按需部署即可。\nNginx绑定证书 使用Nginx提供HTTPS时，需要使用上面子证书签发中获得的myroom.crt证书文件与myroom.key密钥文件。Nginx配置的重点在于以下两句，后缀名是不重要的。\n1 2 ssl_certificate /path/to/self-signed/myroom.crt; ssl_certificate_key /path/to/self-signed/myroom.key; 将根证书安装到Windows 找到刚生成的Root CA根证书rootca.crt 双击打开后点击安装证书（在资源管理器右键“安装证书”也行） 存储位置选择当前用户 接下来选将所有的证书都放入下列存储，点击右侧浏览，选择受信任的根证书颁发机构 之后一路确定即可。 将根证书安装到Linux 找到刚生成的Root CA根证书rootca.crt 将其复制到/usr/local/share/ca-certificates/目录下。需要注意，文件名随意但后缀名必须为.crt。以上命令生成的.crt证书格式就是可以被正常识读的，不需要再做转换。 执行sudo update-ca-certificates更新系统证书缓存。如果报错发现刚刚添加的证书被跳过了，可以试着加--fresh参数执行。 顺带一提，Linux的Firefox浏览器不依赖系统的证书库，而是使用自建的证书库。因此在Linux上使用Firefox时，需要手动前往设置，在隐私与安全部分，将根证书导入到Authorities列表中。\n","date":"2025-01-11T20:00:00+08:00","permalink":"/2025/01/11/headscale-self-hosted-quick-start/","title":"Tailscale/Headscale自建异地组网"},{"content":"对于经常需要使用Python进行数据分析与处理的人来说，Jupyter Notebook是一个非常不错的选择。但Notebook文件在版本管理中，常常会造成麻烦。\n这是因为，Notebook的.ipynb文件格式上实质是一个Json文件，包含了相当多的辅助信息，记录了诸如单元格类型、执行次数、单元格输出等信息，且源代码实质上已经被转义成Json兼容的字符串数组。\n这一特性导致，在使用Git之类版本管理工具管理Notebook源代码时，其实并不方便。举例来说，即便是切换了Notebook运行的Kernel，也会修改Notebook文件，在版本管理工具中就是一次文件改动。\n显然，大多数情况下，我们希望追踪的是Notebook中的源代码，单元格输出等信息并不是使用Git等工具管理代码仓库的目标。\n因此，我们是否能找到一种方法缓解上述问题呢？\n用Python文件存储不就好了？ 是的！非常正常的想法，既然在Notebook中我们编写的都是.py代码，那么为什么我们不能将Notebook直接存储为.py文件呢？\n事实上，无论是Jupyter的浏览器客户端，还是nbconvert模块，抑或是VSCode之类的编辑器，都提供了将Notebook转换为Python源代码文件的功能。\n并且，只需要辅助以正确的助记符，包括Markdown单元格等信息，都可以被正常存储。\n怎么转换回Jupyter Notebook？ 存储的问题解决了，但怎么从.py文件转回.ipynb文件呢？\n这时候就需要一个名为jupytext的模块了。详情请看：https://jupytext.readthedocs.io/en/latest/。\n手动转换好麻烦 不过，设想一下，如果用手动的方法，我们每次编辑一个文件将包含以下几个流程：\n将.py转换成.ipynb 在.ipynb文件上编程和调试 一切OK后，将.ipynb转换为.py 在Git中提交更改 很麻烦对吧？有没有能帮我们自动做这些的程序呢？\n当然有！jupytext本身就提供了将.ipynb与.py配对的能力。\n具体来说，只要将一个.ipynb文件与指定的格式进行配对，配对完成后，当修改两个文件的其中一个，同步一下，另一个文件也将自动反映出最新更改。\n这样一来，只需要在Git仓库里追踪.py文件，然后尽情修改.ipynb，所有更改都会自动同步。我们就实现了，在.ipynb文件中调试，用.py文件存储的目标。\n而所谓的配对，其实也没什么特别的，无非就是在配对文件.ipynb的metadata里，写入配对的格式信息，例如一个最简单的HelloWorld Notebook在配对后，大概是这样的：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 { \u0026#34;cells\u0026#34;: [ { \u0026#34;cell_type\u0026#34;: \u0026#34;code\u0026#34;, \u0026#34;execution_count\u0026#34;: null, \u0026#34;metadata\u0026#34;: {}, \u0026#34;outputs\u0026#34;: [], \u0026#34;source\u0026#34;: [ \u0026#34;print(\\\u0026#34;Hello World!\\\u0026#34;)\u0026#34; ] } ], \u0026#34;metadata\u0026#34;: { \u0026#34;jupytext\u0026#34;: { \u0026#34;formats\u0026#34;: \u0026#34;ipynb,py:percent\u0026#34; }, \u0026#34;kernelspec\u0026#34;: { \u0026#34;display_name\u0026#34;: \u0026#34;myvenv\u0026#34;, \u0026#34;language\u0026#34;: \u0026#34;python\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;python3\u0026#34; }, \u0026#34;language_info\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;python\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;3.12.7\u0026#34; } }, \u0026#34;nbformat\u0026#34;: 4, \u0026#34;nbformat_minor\u0026#34;: 2 } 详情参见上述jupytext文档中的示例。\nVSCode里怎么办呢 然而，jupytext的配对功能仅限于网页客户端的原生Jupyter，VSCode似乎并没有对这一功能的支持，因此似乎只能每次手动敲命令进行转换。\n等一等，敲命令？那能不能让VSCode自动为我们执行转换的命令呢？别忘了VSCode的任务系统！\n具体来说，我们需要做以下工作：\n使用命令实现配对 由于配对是需要显式执行的，因此我们首先用命令实现这一点：\n1 2 3 4 jupytext --set-formats ipynb,py:percent test-hello.ipynb # Wildcard is OK for multiple files jupytext --set-formats ipynb,py:percent *.ipynb 注意到上述命令中的ipynb,py:percent了吗？它的意思是将一个.ipynb文件与 percent 格式的.py文件配对起来。\n什么是 percent 格式呢？实际上就是通过# %%等助记符作为Notebook单元格的划分，方便阅读也方便将.py转换回.ipynb文件。这一格式的介绍也请阅读jupytext的文档啦。\n利用VSCode Tasks实现一键同步 现在，我们可以试着借助VSCode Tasks，实现.py与.ipynb文件内容更改的一键同步。\n编辑工作路径.vscode/tasks.json文件（没有就新建一个），编辑内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format \u0026#34;version\u0026#34;: \u0026#34;2.0.0\u0026#34;, \u0026#34;tasks\u0026#34;: [ { \u0026#34;label\u0026#34;: \u0026#34;JupyText Sync\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;shell\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;${command:python.interpreterPath}\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;-m\u0026#34;, \u0026#34;jupytext\u0026#34;, \u0026#34;--sync\u0026#34;, \u0026#34;${file}\u0026#34; ], \u0026#34;group\u0026#34;: { \u0026#34;kind\u0026#34;: \u0026#34;build\u0026#34;, \u0026#34;isDefault\u0026#34;: true } } ] } 保存文件。\n现在，每当我们在编辑.ipynb或.py文件的其中一个并保存后，只要我们按下Ctrl + Shift + B键，运行VSCode的构建任务，我们配对的同名文件就会自动同步更改。\n","date":"2025-01-08T16:00:00+08:00","permalink":"/2025/01/08/jupytext-py-ipynb-convert-in-vscode/","title":"在VSCode中使用Jupytext实现ipynb与py文件转换"},{"content":"网络配置大概是容器配置中最无聊的部分了。\nIncus与Macvtap 什么是Macvtap？太简单了请自行百度。总之就是快速将虚拟机/容器作为一个独立机器，暴露给主机所在的网络的功能。\n需要注意，Macvtap能实现与局域网内其他主机的通信，但唯独不能实现与主机的通信！\n如果有和主机通信的需要，建议为虚拟机配置一块额外的网卡，或使用传统的Bridge网络。\n1 2 3 4 5 6 7 8 9 10 11 # 创建一个配置文件 # macvlan-enp4s0是配置文件名称 # eth0是虚拟机中这个macvtap网卡的名称 # enp4s0是想要连接到的主机网卡名称 incus profile device add macvlan-enp4s0 eth0 nic nictype=macvlan parent=enp4s0 # 以该配置创建一个新容器 incus init images:debian/12 my-debian --profile default --profile macvlan-enp4s0 # 或是为虚拟机设置配置文件 incus profile add my-debian macvtap-enp4s0 Incus端口发布 在Docker中，我们可能已经习惯了采用-p 10086:80这样的命令，将容器的一个端口映射到主机上另一个端口。\n这一目标在Incus中，需要借助Proxy这一设备类型实现，不过会稍显复杂。\n📝 备注 Network Forward转发与Proxy有什么区别？ 参考：Difference between network forward and proxy device\nThe proxy device is instance specific and can forward connections in either direction, between protocols, and when operating in non Nat mode doesn’t require a network connection between host and instance.\nA forward on the other hand is more like a proxy device operating in Nat mode. But its defined at the network level rather than instance level and allows sharing an ip between multiple instances because it can forward different ports to different ips.\n首先，如果是绑定到普通用户默认Project的容器实例，我们要先解除该Project禁止设置Proxy设备的限制。\n1 sudo incus project edit user-1000 是的，sudo，除非账户在incus-admin用户组里，否则请用sudo。\n在config下添加一行：\n1 2 3 config: # ...... restricted.devices.proxy: allow 保存。现在我们就可以向容器示例添加proxy设备了。\n假设在容器my-http里，有一个Nginx正在监听80端口，我们现在希望将容器的80端口映射到主机的10086端口，以实现访问主机的10086端口，就能访问到容器的80端口。那么，命令如下：\n1 incus config device add my-http port10086to80 proxy connect=\u0026#34;tcp:127.0.0.1:80\u0026#34; listen=\u0026#34;tcp:0.0.0.0:10086\u0026#34; 完成以后，可以在主机运行curl http://127.0.0.1:10086命令，查看绑定是否成功。\n如果计划使用反向代理将外部的请求转发到容器，同时禁止外部网络直接通过访问端口号访问到容器内服务，那么可以将最后的listen字段中IP，改成127.0.0.1。\n","date":"2024-12-04T22:00:00+08:00","permalink":"/2024/12/04/incus-networking-guide/","title":"Incus网络配置指北"},{"content":" 安装相关组件 1 sudo apt install btrfs-progs 创建普通文件系统 1 sudo mkfs.btrfs -L MyDisk /dev/sdxy 创建RAID文件系统 1 sudo mkfs.btrfs -f -d raid1 -m raid1 /dev/sdc1 /dev/sdd2 -f: 强制执行，即无视指定分区上现存的文件系统。 -d raid1: 指定数据块存储模式。具体请参考官方手册RAID功能介绍。 -m raid1: 指定Metadata块存储模式。具体请参考官方手册RAID功能介绍。 在后面指定跨盘卷使用到的块设备即可。\n设置卷名 1 sudo btrfs filesystem label /dev/sdc1 MyData 子卷(Subvolume) 创建子卷：\n1 2 # 假设主卷已经挂载到/mnt/mydata sudo btrfs subvolume create /mnt/mydata/@mysubvol 命令行挂载子卷：\n1 sudo mount -o subvol=@mysubvol UUID=abcd1234-5678-90ab-cdef-1234567890ab /some/place/ 访问子卷内容：\n1 2 3 4 5 6 # 可以挂载后访问： sudo mount -o subvol=... UUID=... /some/place/ cd /some/place/ # 当然还可以直接从卷根挂载点访问 cd /mnt/mydata/@mysubvol 快速问答 Q: 子卷是啥？\nA: 就理解成BTRFS文件系统下，一个可以独立挂载，独立快照，独立操作的子文件夹吧。（尽管内部实现显然不是这样）\nQ: 为啥子卷名称前面有个@符号？\nA: 似乎是约定俗成，换成其他符号也行，不用也行。参阅Re: btrfs subvolume naming scheme.\nQ: fstab中如何使用子卷？\nA: 看下文。\n挂载(fstab) BTRFS文件系统的挂载和其他文件系统别无二致。\n在使用块设备挂载RAID卷时，只需要指定其中一个块设备，就可以完成挂载。\n不过，在/etc/fstab中设置挂载点，尤其是跨盘卷时，建议使用UUID进行挂载。\n查看UUID:\n1 2 3 4 5 6 7 8 # Recommended sudo btrfs filesystem show # OR sudo blkid # OR ls -lha /dev/disk/by-uuid/ 注意我们需要的是整个BTRFS卷的UUID，在RAID中不是某个单一块设备的PARTUUID。\n接着编辑/etc/fstab:\n最简单的：\n1 UUID=abcd1234-5678-90ab-cdef-1234567890ab /mnt/mydata btrfs defaults 0 0 RAID也是一样：\n1 UUID=abcd1234-5678-90ab-cdef-1234567890ab /mnt/mydata btrfs defaults 0 0 挂载子卷（注意这里使用的UUID仍然是主卷的UUID，不要与PARTUUID混淆）：\n1 UUID=abcd1234-5678-90ab-cdef-1234567890ab /media/mysubvol/ btrfs defaults,subvol=@mysubvol 0 0 我喜欢的外挂硬盘挂载选项，可以在硬盘缺失时自动跳过检查：\n1 UUID=abcd1234-5678-90ab-cdef-1234567890ab /media/mysubvol/ btrfs defaults,subvol=@mysubvol,nofail,x-systemd.automount,x-systemd.device-timeout=3 0 0 查看磁盘空间占用 需要注意，系统的du命令输出中，BTRFS的空间统计信息并不总是准确的。\n1 sudo btrfs filesystem df /mnt/mydata 全盘检错 BTRFS的一大特色就是自动化的错误检测。\n在BTRFS的Checksum Tree里，保存着整个文件系统中包括Metadata块与数据块的Checksum，而在每次读取这些数据时，BTRFS会自动进行校验，进而发现硬盘数据存在的错误。\n不过，有时我们还希望主动执行检查，校验硬盘上的数据是否发生了损坏。那么我们可以这么做：\n1 sudo btrfs scrub start /mnt/mydata 上面一条命令将会启动一个后台检查任务，检查整个文件系统存在的错误。对于RAID系统，检查会在多块磁盘上并行进行。\n如果发现了数据错误且配置了RAID等数据冗余，BTRFS将会自动尝试从好数据中恢复损坏的数据。如果只是想查错，不希望BTRFS自动纠错，可以用-r参数指定只读模式运行。\n开始检查后，可以用以下命令读取检查进度与报告：\n1 sudo btrfs scrub status /mnt/mydata 查询读写错误发生次数 1 sudo btrfs device stats /mnt/mydata ","date":"2024-11-28T16:00:00+08:00","permalink":"/2024/11/28/btrfs-quick-start/","title":"💾几分钟能了解BTRFS吗？💾"},{"content":"习惯了在公网服务器上用多个子域名访问不同应用，在局域网环境中我们也常常希望这么做。\n对于自带DNS服务器并维护了.lan域名的路由器来说，一切非常简单，不需要额外配置就能直接使用主机名.lan解析其他主机地址。当然，也仅限于此，增加子域名解析或泛域名解析还需要额外配置。\n在局域网里增设一个DNS服务器，直接解析内网主机的地址，或许这是最为强大的方法，但也或许是最麻烦的方法。\nWindows的NetBIOS，以及Linux的Avahi，是操作系统附带的主机地址广播方案，在我看来似乎是更加灵活的选择。\n且Windows 10自较为近期的版本开始，就自带解析 .local mDNS的组件，Windows和Linux之间的地址广播方案实现了互通。\n接下来就以在Linux服务器上配置.local域名作为例子。\n基本组件 在Debian系统上直接安装：\n1 sudo apt install avahi-daemon avahi-utils 现在起，\u0026lt;主机名\u0026gt;.local将指向这台主机。\n添加额外域名 在一些服务，尤其是HTTP服务中，通过不同域名指向不同服务是非常便利的方法，如blog.\u0026lt;主机名\u0026gt;.local指向博客，nextcloud.\u0026lt;主机名\u0026gt;.local指向Nextcloud实例等等。\n甚至，有些HTTP应用必须运行在/下，无法通过绑定子目录方式访问，此时必须为其分配独立的域名/子域名。\nAvahi的确可以添加泛域名或子域名记录，但大部分操作系统并不能正确解析mDNS的泛域名或子域名记录，或是依赖客户端系统的复杂配置，并不简单易行。\n但是，在局域网环境中，域名空间常常充足，退而求其次，我们完全可以为同一台主机分配多个.local记录，只要不与网内其他主机冲突即可。\navahi-utils软件包提供了avahi-publish工具，该工具可以广播用户指定的mDNS记录到内网。例如：\n1 avahi-publish -a -R another.local 192.168.1.123 这可以将another.local域名指向IP地址192.168.1.123。-R选项代表不要广播反向（IP到主机名）解析记录，由于IP地址到主机名的唯一性，这一选项应当被保留。\n也可以将其编写成一个Systemd服务单元，例如，可以将以下内容填入/etc/systemd/system/avahi-alias@.service：\n1 2 3 4 5 6 7 8 9 10 11 12 13 [Unit] Description=Publish %I as alias for %H.local via mdns Requires=avahi-daemon.service After=avahi-daemon.service [Service] Type=simple ExecStart=/bin/bash -c \u0026#34;/usr/bin/avahi-publish -a -R %I $(avahi-resolve -4 -n %H.local \u0026gt; /dev/null \u0026amp;\u0026amp; ip route get 1 | awk \u0026#39;{print $7;exit}\u0026#39;)\u0026#34; Restart=always RestartSec=3 [Install] WantedBy=multi-user.target 之后执行：\n1 2 sudo systemctl daemon-reload sudo systemctl enable --now avahi-alias@another.local 即可对外发布mDNS记录。\n","date":"2024-08-14T12:00:00+08:00","permalink":"/2024/08/14/play-with-local-domain/","title":"简易使用局域网Local域名"},{"content":"在拥有了自己的Distrobuilder后（还没有？请参见：这篇博客），我们来一起构建用于Incus的容器镜像，实现容器镜像自由吧！\n一定会有人疑惑：既然已经有了LXC镜像站，为什么还需要自己构建镜像呢？\n这是因为，本文希望描述一个全程可控的，避免过度抽象细节的，创建Incus容器镜像的方法。\n预先准备 需要在系统上安装distrobuilder，可以使用snap下载，也可以自己构建。\n案例1: Alpine Linux镜像 准备根文件系统(rootfs) 既然我们在讨论可控的构建过程，那么这一定包括一个可控的根文件系统(Root FileSystem, rootfs)。\n让我们打开downloads | Alpine Linux，下拉找到MINI ROOT FILESYSTEM项目，下载对应架构的根文件系统。\n例如，对于64位系统来说，我们将需要下载alpine-minirootfs-3.20.2-x86_64.tar.gz文件。中间的版本号请不要介意。\n能不能用镜像站完成这一下载呢？又或者能不能用自己打包的Alpine Linux根文件系统呢？当然可以！这正是可控过程的魅力所在。\n当然，distrobuilder实际上也已将下载rootfs的过程封装在程序中。就Alpine Linux来说，distrobuilder下载的rootfs和我们现在手动下载的是同一个文件。\n如果更希望让distrobuilder代劳，请跳过当前这一步骤，后面会再展开介绍。\n准备构建配置模板 构建镜像时需要的许多信息都由模板文件提供，如镜像的名称、类型、发行版，还有需要安装的软件包、执行的动作等等。\n在distrobuilder中，镜像的模板文件是一个YAML文件，标准格式在官方仓库doc/examples/scheme.yaml中给出，文末也附有该文件复制过来的内容：\n只不过，格式文件里，有非常多对键值。看起来一头雾水对吧？没关系，加法做不通我们可以做减法。\n转到lxc/lxc-ci/images，这一目录下的镜像配置文件，就是Incus官方镜像服务器上，镜像构建所采用的配置文件。我们目前正在构建Alpine Linux，所以我们直接复制alpine.yaml文件中的内容到本地。由于内容太长，同样地，原始内容放在文末。\n这文件也太长了吧？而且仔细一看，还有很多似乎我们不太用得上的配置项，例如适用于虚拟机镜像的命令，在容器中是用不到的。因此，让我们来进一步修改这些配置。\n修改构建配置文件 Image: 容器镜像配置 首先，文件的第一节image部分，主要定义了镜像的名称和版本号等信息。\n为了避免与官方构建的Alpine Linux镜像名称冲突，我们可以稍微修改一下这部分内容：\n1 2 3 4 image: distribution: \u0026#34;Custom Alpinelinux\u0026#34; name: custom-alpine release: 3.20 Source: 镜像rootfs来源配置 配置文件第二节，source部分，主要定义了用于构建容器镜像的rootfs如何获得。由于各Linux发行版的rootfs定义与下载地址都有差异，因此distrobuilder预定义了许多Source，用于解析各种操作系统的rootfs下载规则。\n对于Alpine Linux，distrobuilder已经定义好了alpinelinux-http这一Source，因此如果希望distrobuilder帮我们自动下载rootfs，此处可定义如下：\n1 2 3 4 source: downloader: alpinelinux-http same_as: 3.12 url: https://mirrors.hust.edu.cn/alpine/ 其中，URL可以是任意Alpine Linux镜像站，以上例子使用了华中科大镜像站。不过，URL也可以file://开头，指向本地已经下载好的rootfs文件包。\n如你所见，示例配置文件包含了一大段GPG公钥，用于验证下载的rootfs。如果信任下载源与下载文件完整性，并希望删掉keys部分以缩简配置文件，请在source节内加上skip_verification: true选项。\n不过，既然我们正在尽可能追求可控，我们一定会想：\nalpinelinux-http源在下载rootfs文件之外，是否还有额外的操作/Tricks？ 不考虑后文安装软件包等定制操作，在当前这一步是否有通用于所有Linux发行版rootfs的方法？ 是否可以利用已预先准备好的rootfs？ 尽管根据sources/alpine-http.go源代码，就目前来说，并没有什么下载之外的额外小Tricks。但到目前为止，distrobuilder最新版3.0并没有直接提供使用现有rootfs目录直接构建容器系统的选项。如果直接在配置文件中删除source一节，进行构建时也会报错。\n但是，distrobuilder的确提供了一种使用任意rootfs文件包的Source，即rootfs-http，相关源代码可见sources/rootfs-http.go。简单来说只需要提供一个指向rootfs包的URL，本地网络皆可。\n因此，上述source节也可以改为（使用file://指向本地文件时需要使用绝对路径）：\n1 2 3 source: downloader: rootfs-http url: file:///absolute/path/to/alpine-minirootfs-3.20.2-x86_64.tar.gz Target: 目标特定配置 如果仅希望构建Incus容器镜像，不需要LXC镜像和虚拟机镜像，可以直接把这一节大幅精简：\n1 2 targets: incus: 如有需要，请直接参阅Targets - distrobuilder documentation官方文档。\nFiles: 文件生成规则配置 和上面类似地，如果不需要Incus虚拟机镜像，仅仅需要保留用于Incus容器的部分，可以直接删除掉所有包含以下标记的虚拟机专用文件生成规则。\n1 2 types: - vm Packages: 软件包安装预定义配置 这部分将定义Incus在构建镜像时，需要使用何种包管理器，安装哪些软件包，大部分内容可以直接照抄示例。同上，可以删掉所有虚拟机限定的配置。\n重点讨论一下软件源相关配置。官方手册中，与定制软件源相关的选项位于packages节下，可以通过添加repositories节设置额外的软件源。例如：\n1 2 3 4 5 6 7 8 packages: # Other parameters # ... repositories: - name: hust url: |- https://mirrors.hust.edu.cn/alpine/v3.20/main https://mirrors.hust.edu.cn/alpine/v3.20/community 但是，这种方法会将新软件源附加到原本的软件源后，并不会替换掉速度可能非常缓慢的官方软件源。\n如果希望替换掉rootfs中的官方软件源，我们可以不采用这种方案，请往下看。\nActions: 自定义操作 在这一部分，我们可以定义在容器构建的不同的时机要执行的操作(Action)。\n目前，distrobuilder提供的执行时机（触发器/Trigger）包括：\npost-unpack: 在rootfs被解压释放后执行。 post-update: 如果packages.update为true，在包管理器完成更新后执行。 post-packages: 在包管理器安装完所有指定的包后执行。 post-files: 在files配置块执行完成后执行。这一部分默认仅在build-lxc, build-incus, pack-lxc和pack-incus操作中执行。如果希望在build-dir操作中也执行这一块，需要指定--with-post-files选项。 例如，如果我们希望把rootfs中官方的软件源配置为镜像站，则可以添加一个post-unpack操作，内容如下：\n1 2 3 4 5 6 actions: - trigger: post-unpack action: |- #!/bin/sh set -eux sed -i \u0026#39;s/dl-cdn.alpinelinux.org/mirrors.hust.edu.cn/g\u0026#39; /etc/apk/repositories 该命令会在rootfs解压挂载后，容器执行apk update更新软件源之前执行。\nMappings: 架构名称映射配置 这部分主要是告诉Distrobuilder如何处理不同架构名称之间的映射关系，如amd64与x86_64，aarch64与arm64等。\n这部分按照模板直接保留就行，详情见Mappings - distrobuilder documentation官方文档。\n不得不感慨一下，Linux由于各种历史包袱和不一致的规范，有时会略显混乱。\n最终配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 image: name: custom-alpine distribution: \u0026#34;Custom Alpinelinux\u0026#34; release: 3.20 source: downloader: rootfs-http url: file:///absolute/path/to/alpine-minirootfs-3.20.2-x86_64.tar.gz targets: incus: files: - path: /etc/hostname generator: hostname - path: /etc/hosts generator: hosts - path: /etc/network/interfaces generator: dump content: |- auto eth0 iface eth0 inet dhcp hostname $(hostname) - path: /etc/inittab generator: dump content: |- # /etc/inittab ::sysinit:/sbin/openrc sysinit ::sysinit:/sbin/openrc boot ::wait:/sbin/openrc default # Set up a couple of getty\u0026#39;s ::respawn:/sbin/getty 38400 console tty1::respawn:/sbin/getty 38400 tty1 tty2::respawn:/sbin/getty 38400 tty2 tty3::respawn:/sbin/getty 38400 tty3 tty4::respawn:/sbin/getty 38400 tty4 # Stuff to do for the 3-finger salute ::ctrlaltdel:/sbin/reboot # Stuff to do before rebooting ::shutdown:/sbin/openrc shutdown - path: /etc/inittab generator: template name: inittab content: |- # /etc/inittab ::sysinit:/sbin/openrc sysinit ::sysinit:/sbin/openrc boot ::wait:/sbin/openrc default # Set up a couple of getty\u0026#39;s ::respawn:/sbin/getty 38400 console # Stuff to do for the 3-finger salute ::ctrlaltdel:/sbin/reboot # Stuff to do before rebooting ::shutdown:/sbin/openrc shutdown - name: meta-data generator: cloud-init variants: - cloud - name: network-config generator: cloud-init content: |- version: 1 config: - type: physical name: eth0 subnets: - type: dhcp control: auto variants: - cloud - name: user-data generator: cloud-init variants: - cloud - name: vendor-data generator: cloud-init variants: - cloud packages: manager: apk update: true cleanup: true sets: - packages: - alpine-base - logrotate - doas action: install - packages: - cloud-init - openssh - e2fsprogs-extra action: install variants: - cloud - packages: - py3-pyserial - py3-netifaces action: install variants: - cloud releases: - 3.17 - 3.18 - 3.19 - 3.20 - edge actions: - trigger: post-unpack action: |- #!/bin/sh set -eux sed -i \u0026#39;s/dl-cdn.alpinelinux.org/mirrors.hust.edu.cn/g\u0026#39; /etc/apk/repositories - trigger: post-packages action: |- #!/bin/sh set -eux rm -f /var/cache/apk/* - trigger: post-packages action: |- #!/bin/sh set -eux # Rewrite configuration for LXC sed -i \u0026#39;s/#rc_sys=\u0026#34;\u0026#34;/rc_sys=\u0026#34;lxc\u0026#34;/\u0026#39; /etc/rc.conf # Honor fstab by not making the localmount script a noop sed -i \u0026#39;s/-lxc//\u0026#39; /etc/init.d/localmount # Enable services for svc_name in bootmisc syslog devfs; do ln -s /etc/init.d/${svc_name} /etc/runlevels/boot/${svc_name} done for svc_name in networking crond; do ln -s /etc/init.d/${svc_name} /etc/runlevels/default/${svc_name} done types: - container - trigger: post-files action: |- #!/bin/sh set -eux setup-cloud-init variants: - cloud mappings: architecture_map: alpinelinux 我们将最终的配置文件保存，随便起个名字，比方说alpine.yaml即可。\n开始构建 新建rootfs和images两个文件夹，分别用于暂存解压的rootfs，以及构建生成的镜像文件。\n1 2 mkdir rootfs mkdir images 首先需要执行的是build-dir指令。\n1 sudo distrobuilder build-dir alpine.yaml rootfs 再接着执行pack-incus指令。\n1 sudo distrobuilder pack-incus alpine.yaml rootfs images 等待一切完成，images文件夹下就存放着构建好的容器镜像了。\n此时一定有人会问：为什么要分两步？为什么不直接执行build-incus指令？\n答案当然是可以的。\n1 sudo distrobuilder build-incus alpine.yaml images 有什么区别呢？我们来看看Distrobuilder开发者在论坛中的讨论：\nOops, I said build instead of pack here as that’s likely what you actually want to use.build-incus is equivalent of build-dir + pack-incus.\nThe reason why we do it in two steps in our CI is because we produce both LXC and Incus images so that lets us use the same rootfs for both. But if all you care about is the LXD/Incus image, just use build-incus directly.\n导入Incus并开始使用 拥有了镜像，只需要导入Incus就可以开始使用了。\n1 incus image import images/incus.tar.xz images/rootfs.squashfs --alias custom-alpine/3.20 命令末尾的--alias选项用于给镜像定义一个别名。如果没有别名，我们就只能用镜像的哈希指代镜像了。\n接着创建实例，启动镜像。\n1 2 incus launch local:custom-alpine/3.20 test-alp incus exec test-alp -- /bin/ash 附录 配置文件 doc/examples/scheme.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 # This example contains every possible key image: description: |- here goes the image description distribution: distro release: release architecture: x86_64 expiry: 30d variant: default name: distro-release-x86_64 serial: some-random-string source: downloader: ubuntu-http url: http://archive.ubuntu.com keys: - 0xdeadbeaf keyserver: http://keyserver.ubuntu.com variant: default suite: suite same_as: bionic skip_verification: false components: - main targets: lxc: create_message: |- You just created an {{ image.description }} container. To enable SSH, run: apt install openssh-server No default root or user password are set by LXC. config: - type: all before: 5 content: |- lxc.include = LXC_TEMPLATE_CONFIG/ubuntu.common.conf - type: user before: 5 content: |- lxc.include = LXC_TEMPLATE_CONFIG/ubuntu.userns.conf - type: all after: 4 content: |- lxc.include = LXC_TEMPLATE_CONFIG/common.conf - type: user after: 4 content: |- lxc.include = LXC_TEMPLATE_CONFIG/userns.conf - type: all content: |- lxc.arch = {{ image.architecture_personality }} incus: vm: size: 2147483648 filesystem: ext4 files: - generator: dump path: /some/path content: |- here goes the content name: name mode: 0644 gid: 1000 uid: 1000 template: properties: key: value when: - always templated: true releases: - a - b architectures: - x86_64 variants: - default - generator: hostname path: /etc/hostname - generator: hosts path: /etc/hosts - generator: remove path: /root/file - generator: template name: foo content: |- Here goes the content template: properties: key: value when: - create - copy packages: manager: apt custom_manager: clean: cmd: mgr flags: - clean install: cmd: mgr flags: - install remove: cmd: mgr flags: - remove refresh: cmd: mgr flags: - refresh update: cmd: mgr flags: - update flags: - --yes update: true cleanup: false sets: - packages: - gnupg action: install early: true - packages: - vim action: install releases: - a - b architectures: - x86_64 variants: - default - packages: - lightdm action: install flags: - --no-install-recommends - packages: - grub action: remove repositories: - name: reponame url: |- deb http://archive.ubuntu.com/ubuntu {{ image.release }}-updates main restricted universe multiverse type: type key: 0xdeadbeaf releases: - a - b architectures: - x86_64 variants: - default actions: - trigger: post-unpack action: |- #!/bin/sh do something after the rootfs has been unpacked - trigger: post-files action: |- #!/bin/sh do something after the files section has been processed - trigger: post-update action: |- #!/bin/sh do something after packages have been processed - trigger: post-packages action: |- #!/bin/sh do something after the packages section has been processed releases: - a - b architectures: - x86_64 variants: - default mappings: architectures: a: b c: d architecture_map: debian environment: clear_defaults: true variables: - key: FOO value: bar lxc/lxc-ci/images/alpine.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 image: distribution: \u0026#34;alpinelinux\u0026#34; source: downloader: alpinelinux-http same_as: 3.12 url: https://mirror.csclub.uwaterloo.ca/alpine/ keys: # 0482D84022F52DF1C4E7CD43293ACD0907D9495A - |- -----BEGIN PGP PUBLIC KEY BLOCK----- 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 bcgRRcklNr8mTzJH =ckew -----END PGP PUBLIC KEY BLOCK----- targets: lxc: create_message: | You just created an {{ image.description }} container. config: - type: all before: 5 content: |- lxc.include = LXC_TEMPLATE_CONFIG/alpine.common.conf - type: user before: 5 content: |- lxc.include = LXC_TEMPLATE_CONFIG/alpine.userns.conf - type: all after: 4 content: |- lxc.include = LXC_TEMPLATE_CONFIG/common.conf - type: user after: 4 content: |- lxc.include = LXC_TEMPLATE_CONFIG/userns.conf - type: all content: |- lxc.arch = {{ image.architecture_personality }} incus: vm: filesystem: ext4 files: - path: /etc/hostname generator: hostname - path: /etc/hosts generator: hosts - generator: fstab types: - vm - generator: incus-agent types: - vm - path: /etc/default/grub generator: dump pongo: true content: |- # Set the recordfail timeout GRUB_RECORDFAIL_TIMEOUT=0 # Do not wait on grub prompt GRUB_TIMEOUT=0 # Set the default commandline GRUB_CMDLINE_LINUX_DEFAULT=\u0026#34;console=tty1 console=ttyS0 modules=sd-mod,usb-storage,{{ targets.incus.vm.filesystem }} rootfstype={{ targets.incus.vm.filesystem }}\u0026#34; # Set the grub console type GRUB_TERMINAL=console # Disable os-prober GRUB_DISABLE_OS_PROBER=true types: - vm - path: /etc/network/interfaces generator: dump content: |- auto eth0 iface eth0 inet dhcp hostname $(hostname) - path: /etc/inittab generator: dump content: |- # /etc/inittab ::sysinit:/sbin/openrc sysinit ::sysinit:/sbin/openrc boot ::wait:/sbin/openrc default # Set up a couple of getty\u0026#39;s ::respawn:/sbin/getty 38400 console tty1::respawn:/sbin/getty 38400 tty1 tty2::respawn:/sbin/getty 38400 tty2 tty3::respawn:/sbin/getty 38400 tty3 tty4::respawn:/sbin/getty 38400 tty4 # Stuff to do for the 3-finger salute ::ctrlaltdel:/sbin/reboot # Stuff to do before rebooting ::shutdown:/sbin/openrc shutdown - path: /etc/inittab generator: template name: inittab content: |- # /etc/inittab ::sysinit:/sbin/openrc sysinit ::sysinit:/sbin/openrc boot ::wait:/sbin/openrc default # Set up a couple of getty\u0026#39;s ::respawn:/sbin/getty 38400 console # Stuff to do for the 3-finger salute ::ctrlaltdel:/sbin/reboot # Stuff to do before rebooting ::shutdown:/sbin/openrc shutdown - name: meta-data generator: cloud-init variants: - cloud - name: network-config generator: cloud-init content: |- version: 1 config: - type: physical name: eth0 subnets: - type: dhcp control: auto variants: - cloud - name: user-data generator: cloud-init variants: - cloud - name: vendor-data generator: cloud-init variants: - cloud packages: manager: apk update: true cleanup: true sets: - packages: - alpine-base - logrotate - doas action: install - packages: - cloud-init - openssh - e2fsprogs-extra action: install variants: - cloud - packages: - py3-pyserial - py3-netifaces action: install variants: - cloud releases: - 3.17 - 3.18 - 3.19 - 3.20 - edge - packages: - acpi - gcc - grub-efi - linux-virt - udev action: install types: - vm actions: - trigger: post-packages action: |- #!/bin/sh set -eux rm -f /var/cache/apk/* - trigger: post-packages action: |- #!/bin/sh set -eux # Rewrite configuration for LXC sed -i \u0026#39;s/#rc_sys=\u0026#34;\u0026#34;/rc_sys=\u0026#34;lxc\u0026#34;/\u0026#39; /etc/rc.conf # Honor fstab by not making the localmount script a noop sed -i \u0026#39;s/-lxc//\u0026#39; /etc/init.d/localmount # Enable services for svc_name in bootmisc syslog devfs; do ln -s /etc/init.d/${svc_name} /etc/runlevels/boot/${svc_name} done for svc_name in networking crond; do ln -s /etc/init.d/${svc_name} /etc/runlevels/default/${svc_name} done types: - container - trigger: post-files action: |- #!/bin/sh set -eux for svc_name in acpid bootmisc hostname hwclock loadkmap modules networking swap sysctl syslog urandom; do ln -fs /etc/init.d/${svc_name} /etc/runlevels/boot/${svc_name} done for svc_name in devfs dmesg hwdrivers mdev udev udev-settle udev-trigger; do ln -fs /etc/init.d/${svc_name} /etc/runlevels/sysinit/${svc_name} done target=/boot/grub/grub.cfg grub-mkconfig -o \u0026#34;${target}\u0026#34; sed -i \u0026#34;s#root=[^ ]*#root=${DISTROBUILDER_ROOT_UUID}#g\u0026#34; \u0026#34;${target}\u0026#34; TARGET=\u0026#34;x86_64\u0026#34; [ \u0026#34;$(uname -m)\u0026#34; = \u0026#34;aarch64\u0026#34; ] \u0026amp;\u0026amp; TARGET=\u0026#34;arm64\u0026#34; grub-install --target=${TARGET}-efi --no-nvram --removable grub-install --target=${TARGET}-efi --no-nvram # Attempt to correct errors in the installation of all packages. apk fix types: - vm - trigger: post-files action: |- #!/bin/sh set -eux for svc_name in sshd; do ln -fs /etc/init.d/${svc_name} /etc/runlevels/default/${svc_name} done # Tweak to prevent cloud-init from getting stuck. echo \u0026#34;datasource_list: [\u0026#39;NoCloud\u0026#39;, \u0026#39;ConfigDrive\u0026#39;, \u0026#39;LXD\u0026#39;, \u0026#39;None\u0026#39;]\u0026#34; \u0026gt; /etc/cloud/cloud.cfg.d/99_lxc.cfg types: - vm variants: - cloud - trigger: post-files action: |- #!/bin/sh set -eux setup-cloud-init variants: - cloud mappings: architecture_map: alpinelinux ","date":"2024-08-07T21:00:00+08:00","permalink":"/2024/08/07/incus-distrobuilder-build-container-os-image/","title":"使用Distrobuilder构建Incus容器镜像"},{"content":"Distrobuilder作为构建Incus/LXC容器镜像的主要工具，竟然只通过Snap商店提供预编译的二进制包。\n岂有此理，让我们设置一个Distrobuilder构建环境吧！一切其实很简单！\n启动基础容器 当然，构建一系列动作都可以在本机上直接进行。如果决意如此，请自动忽略本文中容器相关的操作。\n不过，Distrobuilder是用Golang编写的程序，因此我们完全可以在容器中构建完成后，移动到主机系统中运行。\n既然，构建生成的可执行文件要在主机系统中运行，那当然更推荐设置一个和主机系统相符或接近的容器环境。\n例如，我的主机系统是Debian，那么现在一个Debian容器构建环境将具备更好的兼容性。\n1 incus launch images:debian/12 go-dev 安装容器基础软件 由于Debian Bookworm中的golang-go软件包版本号过低，不满足distrobuilder的最低要求版本1.21，因此需要先安装好系统基础组件，稍后自行下载golang编译器。\n1 2 3 4 5 cat \u0026lt;\u0026lt;EOF | incus exec go-dev /bin/bash sed -i \u0026#39;s/deb.debian.org/mirrors.bfsu.edu.cn/g\u0026#39; /etc/apt/sources.list apt-get update apt-get -y install debootstrap rsync gpg squashfs-tools git make build-essential EOF 🚨 注意 不要遗漏构建基础套件 对于手动安装golang编译器的用户，请务必记得安装build-essential或其他构建基础套件！\n安装Golang套件 1 2 3 4 cat \u0026lt;\u0026lt;EOF | incus exec go-dev -- /bin/bash rm -rf /usr/local/go \u0026amp;\u0026amp; wget -qO- https://go.dev/dl/go1.22.5.linux-amd64.tar.gz | tar xz -C /usr/local echo \u0026#39;export PATH=\\$PATH:/usr/local/go/bin\u0026#39; \u0026gt;\u0026gt; /root/.bashrc EOF 如有需要，可以将上述下载链接更换为镜像站链接。\n获取distrobuilder源代码 直接前往Releases - lxc/distrobuilder下载最新的源代码包。\n如果在本机上下载完成了，可以推送到容器中：\n1 incus file push distrobuilder-distrobuilder-3.0.tar.gz go-dev/root/ 执行incus exec go-dev -- /bin/bash进入容器，接着执行：\n1 2 3 4 mkdir -p $HOME/go/src/github.com/lxc/ cd $HOME/go/src/github.com/lxc/ tar xzf /root/distrobuilder-distrobuilder-3.0.tar.gz mv distrobuilder-distrobuilder-3.0 distrobuilder 配置go语言资源代理（可选） 以南京大学镜像站为例：\n1 2 go env -w GO111MODULE=on go env -w GOPROXY=\u0026#34;https://repo.nju.edu.cn/go/,direct\u0026#34; 开始编译！ 1 2 cd distrobuilder/ make 试运行 1 2 cd ~/go/bin/ ./distrobuilder --help 若能成功运行，就可以拉回本机使用了。需要回到主机运行：\n1 incus file pull go-dev/root/go/bin/distrobuilder . 容器也可以随之摧毁：\n1 incus delete go-dev -f ","date":"2024-08-07T16:00:00+08:00","permalink":"/2024/08/07/incus-build-distrobuilder/","title":"为Incus构建Distrobuilder"},{"content":"当我们使用容器时，时常希望容器能挂载访问本地文件系统。例如，Docker可以将本地路径挂载为一个Volume访问。\nIncus/LXD当然也提供了这一功能，但配置起来却有时略显复杂。\n只读挂载 非常简单：\n1 incus config device add \u0026lt;container-name\u0026gt; \u0026lt;device-name\u0026gt; disk source=/path/on/host path=/path/in/container 读写权限 然而，以上命令挂载的目录，在容器中常常只有只读权限。\n这是因为，Incus非特权容器会默认将容器进程映射到不同的非特权UID/GID运行。\n为了让容器内进程也能读写目录，常常可以采用下列方法。\n方法1：shift选项 在添加挂载配置时，使用shift=true选项挂载，即可将容器中UID/GID映射到主机文件系统，命令如下：\n1 incus config device add \u0026lt;container-name\u0026gt; \u0026lt;device-name\u0026gt; disk source=/path/on/host path=/path/in/container shift=true 这种方法看上去是最为便捷的，简洁，不需要额外配置。然而，这种方法的局限性与麻烦隐藏在背后。\nRestricted path 在Debian/Ubuntu上，当属于incus组且不属于incus-admin组的用户访问Incus时，Incus会为用户创建一个独立的Incus项目(Project)，并限定用户的所有访问资源都位于这一项目下。\n显然，这种设计增强了系统安全性，然而，需要注意到，这个项目是资源受限的。很不幸地，挂载主机文件系统同样在限制范围内。\n此时，如果用户执行上述含有shift=true选项的挂载命令，会出现以下报错：\n1 Error: Failed to start device \u0026#34;my-vol\u0026#34;: The \u0026#34;shift\u0026#34; property cannot be used with a restricted source path 具体来说，在普通用户Incus项目中，Restricted选项会限制挂载的源路径。可以参考Project配置文档进一步了解。\n那么，如何在坚持使用shift=true选项的前提下挂载目录呢？\n最简单的方法之一是，使用root身份修改用户项目配置：\n1 sudo incus project edit user-1000` 找到restricted.devices.disk.paths一行，将后面的路径删除，留下一对空白的双引号即可。\n1 2 - restricted.devices.disk.paths: /home/xxx + restricted.devices.disk.paths: \u0026#34;\u0026#34; 安全隐忧 如果上述问题只是多了一个额外的步骤，修改完项目配置就一劳永逸了。那么接下来的问题恐怕能让安全人员睡不着觉。\n当shift=true时，容器向挂载目录写入的文件，其用户组信息都将与容器中用户组保持一致！！！\n例如，假设在容器的挂载路径中有一test.txt文件，如果其拥有者是root:root（当然也可以在容器中以root身份设置所有权），此时回到主机文件系统，test.txt文件的拥有者将会是主机上的root:root！\n在这种情况下，任何一个拥有了不受挂载目录限制的，具备incus组身份的用户，都等价于拥有了主机上的root权限。\n又或者，拥有容器root权限的，且能以主机上任意一个用户身份执行特定可知路径可执行文件的用户，都等价于拥有了主机上的root权限。\n很可怕吗？是的很可怕。\n⚠️ 警告 威胁的来源？ 大体来说，任何具备了容器内root权限的人，都可以向挂载路径中以root身份写入可执行文件，并设置setgid属性与777权限。\n此时只需要再在主机上获得任一用户shell，执行挂载路径中的可执行文件并setgid系统调用，即可获得root权限。\n相比之下，这一问题就显得更加棘手了。\n目前比较适合的解决方案是，在容器中创建非特权用户以执行代码，像保护主机root用户一样保护容器root用户。\n但，恐怕大家都会同意的是，到这一步为止，一切都显得有些复杂了。\n方法2：UID与GID映射 另一种方法显得较为传统，当然也是在网上最容易找到的方案，也就是将容器中的进程UID与GID，映射为主机上某一用户的UID与GID，从而赋予容器进程“扮演”该用户访问文件系统，从而达到读写主机文件的目的。\n这一方法的好处是，可以非常方便地保证，容器进程对挂载文件目录的权限，与主机系统上特定角色的权限保持一致。常见的场景是：\n如果希望容器访问我们的~家目录，只需要赋予容器和我们账户一样的UID与GID，再将我们的~挂载到容器中，剩下的事情就是一切如常了。 如果希望容器按照我们想要的权限，例如只读地访问一些文件，我们可以将容器里的用户组映射到本机某一用户组上，容器中的进程也将获得被映射到的组的文件系统权限，例如www-data等。 让我们直接参考Incus/LXD主要开发者的博客吧。\n配置subuid与subgid 这里的第一步是，需要把希望容器映射到的用户ID和组ID，设置为允许Incus主进程，也就是root用户使用，从而让Incus能“扮演”我们用户账户。\n如果只是想让Incus扮演我们当前使用的账户，访问我们当前账户能访问的资源，那么只需要执行以下命令：\n1 2 printf \u0026#34;root:$(id -u):1\\n\u0026#34; | sudo tee -a /etc/subuid printf \u0026#34;root:$(id -g):1\\n\u0026#34; | sudo tee -a /etc/subgid 现在，让我们重启Incus，让配置生效：\n1 sudo systemctl restart incus 接下来是具体说明，不想看的可以略过。\n实际上，这一步配置主要是向/etc/subuid和/etc/subgid文件添加类似下方的一行。\n1 root:1000:1 这一句配置的意思是，允许root用户/组映射UID与GID，而映射的目标UID/GID范围从1000开始，root可以使用接下来的1个UID/GID，也就是1000这一个UID/GID。具体可以按需调整。\n配置Incus容器 还差最后一步！我们现在需要让容器使用刚刚指定的UID与GID，真正开始扮演指定的用户或组。\n依然是省流，如果只是希望test容器中的root用户(UID/GID均为0)扮演我们当前使用的账户，请直接执行：\n1 printf \u0026#34;uid $(id -u) 0\\ngid $(id -g) 0\u0026#34; | incus config set test raw.idmap - 接下来是详细解释。以上命令会在容器实例的配置文件中出现类似于下面的一段：\n1 2 3 4 config: raw.idmap: |- uid 1000 0 gid 1000 0 这段配置的意思非常直观：将本机上的UID1000，映射成容器中的UID0。同理对GID也进行这一映射。\n这一段配置使得容器中的root用户，实际上具备了主机中UID为1000用户的权限，因此，容器中以root身份执行的程序，实际上是以本机UID为1000的用户身份执行的。\n📝 备注 能否在容器中使用其他用户身份？ 当然可以！在上面的配置中，每一行末尾的0即表示容器中root用户的UID与GID。\n如果希望映射成容器中的其他用户，假设容器内用户的UID和GID是10086，那么只需要将刚刚的命令修改成：\n1 printf \u0026#34;uid $(id -u) 10086\\ngid $(id -g) 10086\u0026#34; | incus config set test raw.idmap - 就可以将容器内的10086用户映射成主机上的当前用户了。\n现在，我们就可以像设置只读挂载一样，将本地路径挂载到容器中，不再需要shift=true选项。\n1 incus config device add \u0026lt;container-name\u0026gt; \u0026lt;device-name\u0026gt; disk source=/path/on/host path=/path/in/container 至此，我们终于能让Incus容器，在保持正确权限的同时，读写主机上挂载的文件目录。\n","date":"2024-08-07T12:00:00+08:00","permalink":"/2024/08/07/incus-filesystem-mount-access/","title":"Incus/LXD文件系统挂载与访问"},{"content":"由于网络连通性原因，DockerHub的访问变得困难。然而在服务器上部署服务时，又经常需要使用Docker。\n为了避免在服务器上进行复杂的网络设置，一种相对折中的方法是：在一台网络流畅（下面称为本机）的机器上获取Docker镜像，再将镜像复制到服务器上使用。\n工具 - Skopeo 一种传统的方法是，在本机上也安装一个Docker Engine，使用docker pull命令拉取镜像，再使用docker save导出镜像。\n这种方法的缺点在于，我们在本机上或许并不需要/不想要/难以安装Docker引擎。\n而维新派指出，在Docker引擎官方代码仓库moby/moby/contrib下，有一个download-frozen-image-v2.sh脚本，该脚本提供了对Docker镜像的下载功能。听起来不错？\n而当然，也有更便捷的工具：containers/skopeo。这也是本文介绍的主角。\n安装Skopeo 安装skopeo相当简单，例如在Debian 11+/Ubuntu 20.10+内，可以直接在apt源中下载安装。Alpine Linux同样可以使用apk直接添加。\n尽管skopeo不支持Windows版本，但完全可以创建一个简易的Alpine Linux WSL环境完成以上任务。\n使用Skopeo获取镜像 主要使用的是skopeo copy命令，文档参见skopeo/docs/skopeo-copy.1.md。\n具体来说，在拷贝时，我们常常希望把镜像打成一个tar包，可以方便地在网络上传输。skopeo可以直接导出tar文件，具体如下：\n1 skopeo copy docker://\u0026lt;image\u0026gt;:\u0026lt;tag\u0026gt; docker-archive:/path/to/archive.tar:\u0026lt;image\u0026gt;:\u0026lt;tag\u0026gt; 例如，如果想要拉取busybox的最新版本镜像busybox:latest，可以执行：\n1 skopeo copy docker://busybox:latest docker-archive:myarchive.tar:busybox:latest 如果镜像是由非Docker官方发布的，镜像名称中会含有斜杠，请放心，此时镜像名称\u0026lt;image\u0026gt;就是包含斜杠的完整字符串。例如，以下命令当然是没问题的。\n1 skopeo copy docker://bitnami/postgresql:14 docker-archive:/path/to/archive.tar:bitnami/postgresql:14 值得注意的是，在上述命令里，附加在打包文件名后的\u0026lt;image\u0026gt;:\u0026lt;tag\u0026gt;，即为之后导入docker时，该镜像所关联到的名称。\n例如，如果执行：\n1 skopeo copy docker://busybox:latest docker-archive:myarchive.tar:busybox:10086 导入到docker中的镜像名称也会被显示为busybox:10086：\n1 2 3 user@server:~$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE busybox 10086 1234567890ab 1 months ago 1.23MB 打包后，为了节约传输带宽和时间，可以对打包好的tar文件压缩一下。可以使用zstd（gz等当然也行），大约能缩小2/3的体积。\n1 zstd -9 myarchive.tar 在服务器上导入镜像 使用喜欢的方法将打包文件传输到服务器后，就可以导入Docker引擎：\n1 docker load \u0026lt; myarchive.tar.zstd 这里使用的是重定向输入文件流，当然也可以使用-i参数引导读取指定路径的文件。同时，docker load也可以加载包括gz, xz, bzip2, zstd在内的压缩文件。\n与同名同tag现有镜像的冲突 导入时会覆盖掉同名镜像，即原有镜像的名称会被置空，变成孤立的镜像。运行docker images prune可以删除这些孤立镜像。\n","date":"2024-06-29T12:00:00+08:00","permalink":"/2024/06/29/docker-images-pulling-transfering/","title":"Docker镜像本地拉取与迁移"},{"content":"前言 实际上这又是一篇类似于 这篇博客 的文章，但内容主要是使用和测试树莓派时踩过的坑与解决方法。\n文章的内容从树莓派系统的安装配置，到程序的编程开发都有涉及。但都不是什么保姆式的指引，毕竟树莓派的资料众多，可以在网上轻松搜索到。\n背景 前段时间突发奇想，想要在房间里搭建一些物联网模块，自己动手实现一个包括温湿度监测、异常状态检测、稀奇古怪的音乐播放等功能。\n购入一块树莓派Zero 2后，又发现了还有包括STM32/ESP32等等架构可以试玩，还有像CH32这种国产化的同型号替代可以测试，正是一入硬件深似海啊。\n至于树莓派，主要是作为原型测试使用，毕竟相较于硬件性价比更高的国产开发板，树莓派的活跃社区和丰富文档可能是仅有的优势之一了。之后只要有条件，肯定会测试采用香橙派、幸狐这种国产板子的方案的。届时再在本站点上进行更新。\n不过，也别太认真，这些方案更多是测试和整活为主，事实上要实现的内容已经有相当成熟的产品可以直接购买。就请把这一系列当作是一个大学生消遣娱乐所玩弄的东西好了。\n树莓派Raspberry Pi OS防止Wifi自动休眠 问题 由于是嵌入式场景，给树莓派安装的是官方的Raspberry Pi OS Lite（也就是以前的Raspbian，无GUI版），并使用SSH通过局域网访问树莓派。\n但在使用的头几天里，总是遇到一个问题：静置一段时间后，树莓派就从网络上消失了。路由器里也找不到树莓派的连接。\n一开始以为，是系统存在自动休眠机制，但按网上的说法，无GUI版RPiOS Lite不存在X Window，按理说不会自动休眠。同时查看dmesg，也不存在休眠的记录。\n后来查阅资料才知道，树莓派官方Raspberry Pi OS默认启用Wifi网卡自动休眠，以节约能耗。\n但对于嵌入式与服务器这种，需要长期开机联网的场景来说，这简直是灾难性的。\n解决方法 解决方法当然是关闭Wifi网卡的自动休眠。只需要一句命令：\n1 2 3 4 iw dev wlan0 set power_save off # OR use nmcli nmcli connection modify \u0026lt;CONNECTION\u0026gt; wifi.powersave 2 首先创建一个systemd服务：\n1 2 3 4 5 6 sudo systemctl --full --force edit wifi-powersave-off.service # OR edit service file manually sudo vim /etc/systemd/system/wifi-powersave-off.service # Reloading daemon is needed after editing manually sudo systemctl daemon-reload 填入以下内容：\n1 2 3 4 5 6 7 8 9 10 11 [Unit] Description=Turn off WiFi power save After=sys-subsystem-net-devices-wlan0.device [Service] Type=oneshot RemainAfterExit=yes ExecStart=/sbin/iw dev wlan0 set power_save off [Install] WantedBy=multi-user.target 保存上述文件，启用该服务，并重启系统，网络就不会随意中断了。\n1 2 sudo systemctl enable wifi-powersave-off sudo reboot 设置chroot交叉编译环境 问题 不同的体系结构 树莓派Zero 2采用的是ARM64(aarch64, ARMv8)处理器架构，因此不能简单使用x86平台工具链为树莓派开发应用。\n想要针对树莓派开发应用，一种最简单能想到的方法是，直接在树莓派上安装和设置编译工具链，在树莓派上进行编译和生成应用程序。\n这种方法最为直接且麻烦最少，但最大的瓶颈在于树莓派极其有限的硬件资源。对于一个四核A53 SoC来说，编译大型应用有点强人所难了。\n因此，通常还希望寻找在普通x86电脑上编译ARM应用的方法，以充分利用不同平台上的硬件优势。\n为了在x86 PC上编译ARM应用，一种方法是直接在x86系统中引入ARM的编译工具链，就可以生成对应的可执行文件。针对ARM的GNU工具链可以在ARM官网Home / Downloads / Arm GNU Toolchain Downloads找到。\n使用上述工具，编写如Hello World等程序就是没有问题的了。这种方法同样具有简单易行的优点。\n不同的依赖库体系结构 然而，事实上，我们编写的程序常常还需要链接到各种第三方库，如ncurses, openmp等等。而在我们x86主机上，这些库文件通常是x86版本的，不能被正常链接到ARM的应用。\n既然如此，那是否可以将ARM的库文件放到x86系统里供编译器查找链接呢？答案当然是可以的。\n例如在Debian/Ubuntu系统中，可以使用多架构功能，为系统添加不同的体系结构，之后就可以直接使用apt安装指定的库文件。如：\n1 2 3 sudo dpkg --add-architecture arm64 sudo apt update sudo apt install libc6:arm64 不同的库环境 然而，上述方法存在一些问题。首先，向系统中添加多体系结构软件包容易造成混乱，毕竟并非人人都是Debian系统运维专家。\n更重要的是，通过上述方法获得的ARM库文件，其版本号直接由x86主机系统决定，这可能与树莓派Raspberry Pi OS的版本号不同，从而造成一些奇怪的问题。\n这并非是什么稀奇事，因为Linux系统的诸多发行版，库文件之间常常存在兼容性差异。\n就我个人经历而言，也遭遇了在x86 Ubuntu系统上编译好的程序，在Raspberry Pi OS Bookworm中，由于Debian Bookworm和Ubuntu的GLIBCXX库版本号有3个小版本的差距，从而导致找不到库文件，程序无法正确运行的情况。\n虚拟化的选择 到了这里，鉴于直接使用x86主机系统进行编译时存在的诸多限制和挑战，一个答案呼之欲出：虚拟化。我们希望在虚拟化的环境中完成对树莓派应用的编译。\n目前为止，能在Linux上虚拟出ARM系统的，似乎只有QEMU。但QEMU的完全虚拟化ARM虚拟机性能极其感人，不信可以自己试试看。\n且慢，我们只是需要一套虚拟的环境，并不需要一个完全隔离的系统。因此，我们可以使用轻量级的chroot设置一个隔离的系统目录环境。\n到此为止，我们就确定了接下来的目标：在x86系统中设置针对树莓派开发的ARM体系结构chroot环境。\n本文还使用了在Debian系统下的chroot管理小工具schroot。要是不喜欢这玩意，也没关系，它最大的作用就是简化chroot之前的挂载步骤和设置资源隔离（但实际上在我们当前场景中安全并不是很重要）。只需要自行在chroot前设置好/dev, /sys, /proc等挂载点，两者效果完全相同。\n设置x86主机系统 在开始前首先需要设置一下x86主机上的主系统。\n笔者使用的x86主机系统是Debian 12，其他发行版可能需要按需修改一下。经测试，加入一些简单的额外步骤后也可以用于WSL内的Debian/Ubuntu系统。\n安装schroot 首先需要安装schroot（会一同安装build-essential）:\n1 sudo apt install sbuild 安装debootstrap 如果希望创建一个Debian的chroot环境，那么肯定需要用到debootstrap。\n1 sudo apt install debootstrap 如果需要使用基于Ubuntu的chroot环境，可以使用Ubuntu Base。其他发行版需要自行寻找方法。\n(仅WSL)开启Systemd 接下来使用的binfmt_misc模块需要systemd的支持，因此需要提前为WSL子系统开启systemd。\n首先编辑WSL子系统内的/etc/wsl.conf，填入以下内容：\n1 2 [boot] systemd=true 然后在子系统内安装systemd组件：\n1 sudo apt install systemd systemd-sysv 完成后退出WSL，在Windows命令行中执行wsl --shutdown以彻底关闭WSL系统，再重新打开WSL即可。\n配置ARM二进制文件转译支持 首先安装：\n1 sudo apt install qemu-user-static binfmt-support 两个软件包中，qemu-user-static可以在允许用户直接执行另一个体系结构的可执行文件。\n而binfmt-support会为系统添加binfmt_misc模块，从而在直接调用ARM体系结构ELF二进制文件时，自动判断所属体系结构，并自动调用qemu-user-static翻译器。查看该模块的运行状态与已注册的文件类型可以前往/proc/sys/fs/binfmt_misc/查看。\n完成安装后重启系统或WSL子系统。到此为止，x86系统就可以（高性能损失地）翻译执行ARM应用了。以Debian软件源中的hello示例软件包为例：\n1 2 3 4 5 6 7 8 # Check package version and filename if failed or 404 curl -sSL http://deb.debian.org/debian/pool/main/h/hello/hello_2.10-3_arm64.deb | dpkg -x - hello # Run hello world from ARM64 architecture. ./hello/usr/bin/hello # (Optional) Equals to above line. qemu-aarch64-static ./hello/usr/bin/hello 安装libc以支持动态链接 为了支持ARM虚拟环境中的动态链接，需要在主机上安装ARM体系结构的libc库。\n1 2 3 sudo dpkg --add-architecture arm64 sudo apt update sudo apt install libc6:arm64 至此，主系统上的前期配置就已经全部完成。下面将开始设置chroot环境。\n创建ARM架构chroot环境 在接下来的文段中，会把chroot出来的虚拟化ARM环境，称为虚拟环境。\n初始化虚拟环境文件目录(rootfs) 以创建Debian Bookworm ARM64的虚拟环境为例，需要运行以下命令：\n1 sudo debootstrap --arch=arm64 --foreign bookworm /srv/chroot/bookworm-arm64-build https://deb.debian.org/debian/ 着重解释：\n--foreign参数会跳过debootstrap的第二阶段，即apt执行软件包配置代码的阶段。由于体系结构的不同，在当前阶段直接执行配置代码会出错，因此需要推迟到chroot进入系统之后执行（后文介绍步骤）。本参数不能省略！ /srv/chroot/bookworm-arm64-build是自行指定的chroot环境存放目录，爱放哪放哪。不过/srv/chroot是schroot推荐的目录。 https://deb.debian.org/debian/是指定的软件源，国内可以使用如http://mirrors.163.com/debian/等镜像站替代。 执行上述命令后，/srv/chroot/bookworm-arm64-build就是新创建的ARM架构系统根目录(rootfs, /)。\n接着还需要复制QEMU翻译器到chroot目录下：\n1 2 3 4 sudo cp /usr/bin/qemu-aarch64-static /srv/chroot/bookworm-arm64-build/usr/bin # (Alternative) This should work too. sudo cp \u0026#34;$(which qemu-aarch64-static)\u0026#34; /srv/chroot/bookworm-arm64-build/usr/bin 挂载文件系统 进入虚拟环境前还需要给它挂载一些有用的虚拟系统。以下命令根据chroot - Debian Wiki编写：\n1 for f in dev dev/pts sys proc run ; do sudo mount --bind /$f /srv/chroot/bookworm-arm64-build/$f ; done （另一种方案）不过在chroot - ArchWiki上，介绍的挂载点略有差异，但似乎都能运行，暂时没遇到问题：\n1 2 3 4 cd /srv/chroot/bookworm-arm64-build/ sudo mount -t proc /proc proc/ sudo mount --rbind /sys sys/ sudo mount --rbind /dev dev/ 完成Debootstrap第二阶段 如果使用上上节中的debootstrap命令创建Debian系统，那么现在还需要执行debootstrap的第二阶段，以完成虚拟环境的配置。\n1 sudo chroot /srv/chroot/bookworm-arm64-build /debootstrap/debootstrap --second-stage 经过漫长的等待后，配置过程总算结束了。\n使用schroot管理chroot环境 schroot配置chroot环境 实际上，sbuild等工具可以帮助我们自动创建虚拟环境并自动配置。但是！相较于完全透明的完整工具，我还是更享受将这些过程掌握在手里的感觉。\nschroot可以帮忙管理chroot环境，简化激活chroot环境所需要的命令。\n如果希望使用schroot而非chroot，只需要编写配置文件即可。\n在/etc/schroot/chroot.d/下创建配置文件，如/etc/schroot/chroot.d/bookworm-arm64-build.conf：\n1 2 3 4 5 [bookworm-arm64-build] type=directory description=Debian Bookworm arm64 build directory=/srv/chroot/bookworm-arm64-build root-users=root 这样就足以直接开始使用：\n1 2 3 4 5 # List all chroot(s) schroot -l # chroot to virtual environment. schroot -c bookworm-arm64-build -d / 上述配置是最简单的配置，可以附加其他很多选项，参见：schroot(1)和schroot.conf(5)。\n例如，希望使用一个更短的别名：\n1 2 3 4 5 6 [bookworm-arm64-build] type=directory description=Debian Bookworm arm64 build directory=/srv/chroot/bookworm-arm64-build +aliases=d12arm64 root-users=root 之后就可以直接使用以下命令访问容器：\n1 schroot -c d12arm64 -d / schroot配置文件目录挂载 有时我们希望允许虚拟环境访问指定的某个目录，但在chroot环境下，软连接是无法访问的（超过了访问路径范围），而硬链接又只能链接单个文件。\n因此，通常的做法是把主机上希望允许访问的目录mount --bind到虚拟环境中。例如：\n1 sudo mount --bind /host/example /srv/chroot/bookworm-arm64-build/chroot/some/example 这种方法，在使用原始的chroot，或是当上一节schroot配置文件第一行指定环境类型为type=plain时是可行的。\n需要首先介绍一下schroot的大致工作机制。schroot为了允许指定的非root用户访问虚拟环境，在用户要求访问虚拟环境时，会先将虚拟环境的rootfs，也就是/srv/chroot/bookworm-arm64-build/，使用mount挂载到/run/schroot/mount/下，再chroot到这一映射后的目录下，允许用户访问。\n当配置文件type=plain时，schroot在mountrootfs时使用的参数是--rbind，此时原先挂载到rootfs下的所有挂载点都能被正常跟随处理。\n但在type=directory时，由于schroot改而使用--bind参数（有什么差别？参见mount(8) - Linux manual page）挂载rootfs，此时原先挂载到虚拟环境中的挂载点将不会被正常跟随，在打开的虚拟环境中，所见目标目录为空。\n解决这一问题的办法一是，直接将配置文件中的type=directory改回type=plain：\n1 2 3 4 5 6 7 8 ```diff [bookworm-arm64-build] -type=directory +type=plain description=Debian Bookworm arm64 build directory=/srv/chroot/bookworm-arm64-build aliases=d12arm64 root-users=root 然而plain类型的schroot环境需要手动挂载dev/sys/proc等路径，和chroot没有大差异。在不需要考虑安全等因素的场景中，选择使用schroot正是为了免去这些麻烦而准备的，现在直接改回去大有一夜回到解放前的感觉。\n因此尝试以下第二种方法，也就是schroot.conf(5)所提到的，通过指定挂载虚拟环境时的fstab文件配置挂载点。该文件格式与/etc/fstab格式相同。\n下面以挂载/host/example到虚拟环境中的/chroot/example为例。注意这里的/chroot/example是虚拟环境中的路径，不是相对主机系统而言的。\n首先在/etc/schroot/下创建一个fstab.d文件夹，再创建一个独立的fstab文件（实际上后缀名不作强制要求）。\n1 2 sudo mkdir -p /etc/schroot/fstab.d sudo vim /etc/schroot/fstab.d/bookworm-arm64-build.fstab 填入以下内容，其中有一部分内容可以直接照搬/etc/schroot/default/fstab：\n1 2 3 4 5 6 7 8 9 # Note that the mount point will be prefixed by the chroot path # (CHROOT_PATH) # # \u0026lt;file system\u0026gt; \u0026lt;mount point\u0026gt; \u0026lt;type\u0026gt; \u0026lt;options\u0026gt; \u0026lt;dump\u0026gt; \u0026lt;pass\u0026gt; /proc /proc none rw,bind 0 0 /sys /sys none rw,bind 0 0 /dev /dev none rw,bind 0 0 /dev/pts /dev/pts none rw,bind 0 0 /host/example /chroot/example none rw,bind 0 0 再修改/etc/schroot/chroot.d/bookworm-arm64-build.conf中的内容，添加一行setup.fstab，指向刚刚创建的fstab文件，该路径是相对于/etc/schroot/而言的。\n1 2 3 4 5 6 7 [bookworm-arm64-build] type=directory description=Debian Bookworm arm64 build directory=/srv/chroot/bookworm-arm64-build aliases=d12arm64 root-users=root +setup.fstab=fstab.d/bookworm-arm64-build.fstab 保存后，还需要在虚拟环境文件系统中创建挂载点：\n1 sudo mkdir -p /srv/chroot/bookworm-arm64-build/chroot/example 重新进入schroot，就可以在/chroot/example下见到主机/host/example中的文件了。\n","date":"2024-06-26T11:00:00+08:00","permalink":"/2024/06/26/raspberry-pi-quick-start/","title":"树莓派快速入门"},{"content":"想在本地玩大模型但服务器光有CPU没有高端显卡？\nNo No No，为什么不试试最新最潮（也不算）的LLaMA.cpp，只要有一定内存，和支持AVX512指令集的英特尔服务器CPU，尽管推理速度慢了一些，但只需要64G以上普通内存，依然可以试玩大模型。\n打开教程一看，又得自己编译，又要配环境，还要量化模型……\n觉得复杂？一切没有想象中困难！\n(2026年更新) Vulkan支持 也不知道什么时候起，llama.cpp的Github页面就提供了支持Vulkan的预编译包。\n所以理论上，只要系统安装了支持Vulkan的显卡驱动，配置好Vulkan环境，不管是哪家的显卡，都可以用同一个二进制文件，利用GPU进行LLM推理。\n对于一些容器镜像，系统中的Vulkan包可能不齐全。对于Ubuntu，可以使用以下命令补全系统依赖：\n1 apt-get update \u0026amp;\u0026amp; apt-get install -y --no-install-recommends libglvnd0 libgl1 libglx0 libegl1 libgles2 \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* 安装Intel oneAPI编译工具 首先需要前往英特尔官网Download the Intel® oneAPI Base Toolkit下载支持AVX512指令集的编译器套件。\n下载完成后安装，安装命令是：\n1 sh ./l_BaseKit_p_2024.1.0.596_offline.sh -a --silent --cli --eula accept 官网示例命令中使用了sudo，但实际上也支持用户模式安装。-a选项有点莫名其妙，实际作用是把后面的参数传递给安装器。如果希望交互式安装，则需要去掉--silent选项。\n引入cmake和oneAPI套件（可选） 如果没有root权限，cmake和oneAPI都只能安装在用户目录下，则需要先引入上述环境。\n1 2 source /path/to/intel/oneapi/setvars.sh source /path/to/cmake/activate_cmake.sh 这里的activate_cmake.sh文件是自己创建的，只是简单地在PATH中添加了cmake的bin文件路径，方便后续使用而已，内容如下：\n1 2 #!/bin/bash export PATH=\u0026#34;$PATH:/path/to/cmake/bin\u0026#34; 编译安装LLaMA.cpp 克隆LLaMA.cpp仓库（不要直接下载Release中的打包源代码，编译时会检测git仓库是否存在，请直接克隆）\n1 2 git clone https://github.com/ggerganov/llama.cpp cd llama.cpp 按照仓库里的指引，找到Intel oneMKL部分的编译指令，直接执行：\n1 2 cmake -B build -DLLAMA_BLAS=ON -DLLAMA_BLAS_VENDOR=Intel10_64lp -DCMAKE_C_COMPILER=icx -DCMAKE_CXX_COMPILER=icpx -DLLAMA_NATIVE=ON cmake --build build --config Release 等待编译完成，可执行文件就位于build/bin下面。如果希望将这些文件安装到特定文件夹，可以继续执行：\n1 cmake --install build/ --config Release --prefix /path/you/like/ 量化和加载模型文件 得到可用的LLaMA.cpp后，就可以对训练好的模型量化然后使用了。当然，如果不想自己炼丹/量化，也可以下载已经量化好的模型。\n以千问模型为例，参考llama.cpp - Qwen的指引。\n首先可以前往Huggingface下载模型的GGUF文件，例如Qwen/Qwen2-7B-Instruct-GGUF。\n和HF上其他格式的检查点不同，这里只需要下载一个合适的GGUF文件就行。\n下载完成后加载模型文件和初始提示词：\n1 ./main -m ../models/qwen2-7b-instruct-q5_k_m.gguf -n 512 --color -i -cml -f ../prompts/chat-with-qwen.txt 接着就可以对话了：\n1 2 3 4 5 \u0026gt; Hello, tell me more about yourself. I am an artificial intelligence designed to assist with a wide range of tasks, including answering questions, providing information, and performing various tasks. I am constantly learning and improving, and I am constantly updated with the latest technology and information. I am designed to assist with a wide range of tasks, and I am always ready to help you with whatever you need. \u0026gt; What is 1+2? 1 + 2 equals 3. 踩坑 量化模型有很多，选哪个？ 请参见Difference in different quantization methods.\nquantize --help outputs a helpful table:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Allowed quantization types: 2 or Q4_0 : 3.50G, +0.2499 ppl @ 7B - small, very high quality loss - legacy, prefer using Q3_K_M 3 or Q4_1 : 3.90G, +0.1846 ppl @ 7B - small, substantial quality loss - legacy, prefer using Q3_K_L 8 or Q5_0 : 4.30G, +0.0796 ppl @ 7B - medium, balanced quality - legacy, prefer using Q4_K_M 9 or Q5_1 : 4.70G, +0.0415 ppl @ 7B - medium, low quality loss - legacy, prefer using Q5_K_M 10 or Q2_K : 2.67G, +0.8698 ppl @ 7B - smallest, extreme quality loss - not recommended 12 or Q3_K : alias for Q3_K_M 11 or Q3_K_S : 2.75G, +0.5505 ppl @ 7B - very small, very high quality loss 12 or Q3_K_M : 3.06G, +0.2437 ppl @ 7B - very small, very high quality loss 13 or Q3_K_L : 3.35G, +0.1803 ppl @ 7B - small, substantial quality loss 15 or Q4_K : alias for Q4_K_M 14 or Q4_K_S : 3.56G, +0.1149 ppl @ 7B - small, significant quality loss 15 or Q4_K_M : 3.80G, +0.0535 ppl @ 7B - medium, balanced quality - *recommended* 17 or Q5_K : alias for Q5_K_M 16 or Q5_K_S : 4.33G, +0.0353 ppl @ 7B - large, low quality loss - *recommended* 17 or Q5_K_M : 4.45G, +0.0142 ppl @ 7B - large, very low quality loss - *recommended* 18 or Q6_K : 5.15G, +0.0044 ppl @ 7B - very large, extremely low quality loss 7 or Q8_0 : 6.70G, +0.0004 ppl @ 7B - very large, extremely low quality loss - not recommended 1 or F16 : 13.00G @ 7B - extremely large, virtually no quality loss - not recommended 0 or F32 : 26.00G @ 7B - absolutely huge, lossless - not recommended The ppl column is perplexity increase relative to unquantized. Q4_K_M, Q5_K_S and Q5_K_M are considered \u0026ldquo;recommended\u0026rdquo;.\n出现unknown argument: -cml 自从Releases/b3087开始，LLaMA.cpp对main程序进行了重构，删去了-cml参数。但千问文档中需要使用这一参数。\n直接解决这一问题的方法比较麻烦，当然也有不需要动脑的方法：回退到之前的版本Releases/b3086。\n在LLaMA.cpp主目录执行以下命令：\n1 git revert --no-commit b3086..HEAD 重新编译一次即可。\n更多请见Discussions/7837。\nlibmkl_intel_ilp64.so.2: No such file or directory 如果出现以下报错：\n1 ./main: error while loading shared libraries: libmkl_intel_ilp64.so.2: cannot open shared object file: No such file or directory 这是因为英特尔的MKL库文件libmkl_intel_ilp64.so.2没有在LD_LIBRARY_PATH中可以被搜索到，该文件实际位于/path/to/intel/oneapi/mkl/\u0026lt;year\u0026gt;.\u0026lt;ver\u0026gt;/lib里。\n解决方法也相当简单，直接再次source /path/to/intel/oneapi/setvars.sh，脚本会自动帮我们把库文件添加到环境变量中。\noneAPI Compiler与GCC的性能 然而，实际测试中，较新版本的GCC也能利用AVX512指令集为程序加速。于是恭喜你和我一样，踩坑了！\n","date":"2024-06-10T11:00:00+08:00","permalink":"/2024/06/10/llama-cpp-avx512-setup/","title":"在电脑上使用AVX512加速LLaMA.cpp本地推理"},{"content":"还在为帮导师纯体力打工而烦恼吗？还在为统计论文分区而浪费时间吗？面对数以千计的论文记录和期刊列表，还在寻找浏览器扩展一条条搜索分区信息吗？\n计算机的学生要有计算机学生解决问题的方式。说得好，我们要来点替代体力劳动的东西。\n期刊分区数据库 想要自动化处理和分类期刊，一个期刊的分区数据库是不可缺少的。\n本想研究下一些所谓的学术用浏览器扩展，找到可以查询期刊分区的API接口，后来想想不如找一找是否有现成的数据库。\n这不，可以直接找到：hitfyd/ShowJCR。打开项目目录中的中科院分区表及JCR原始数据文件文件夹，找到jcr.db文件，这就是我们需要的数据库了。\n具体数据库内容可参见仓库说明，值得注意的是，JCR/SCI分区信息更新于每年6月，而中科院分区信息更新于每年年底，CCF……不太清楚，反正就那回事。\n数据库中有以下表（截至2024年5月）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 jcr.db - JCR2022 # JCR分区2022年版 - JCR2021 # JCR分区2021年版 - JCR2020 # JCR分区2020年版 - GJQKYJMD2024 # 国际期刊预警名单2024年版 - GJQKYJMD2023 # 国际期刊预警名单2023年版 - GJQKYJMD2021 # 国际期刊预警名单2022年版 - GJQKYJMD2020 # 国际期刊预警名单2021年版 - CCF2022 # CCF推荐期刊列表2022年版 - CCFT2022 # CCF推荐国内期刊（？）列表2022年版 - FQBJCR2023 # 中科院分区2023年版 - FQBJCR2022 # 中科院分区2022年版 - FQBJCR2021 # 中科院分区2021年版 或许有用的SQL示例 运行以下SQL代码：\n1 2 3 SELECT jcr.Journal, jcr.ISSN, jcr.\u0026#34;IF(2022)\u0026#34; AS IFactor, jcr.IFQuartile FROM JCR2022 jcr WHERE jcr.Journal = \u0026#39;nature\u0026#39; COLLATE NOCASE 结果：\n1 NATURE|0028-0836|64.8|Q1 或许有用的Python脚本示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import sqlite3 JCR_DB = \u0026#34;jcr.db\u0026#34; class JCRQuerier(): def __init__(self, jcr_db: str) -\u0026gt; None: self.con = sqlite3.connect(jcr_db) self.con.execute(\u0026#39;PRAGMA foreign_keys = ON\u0026#39;) def get_journal_jcr_rank(self, journal_name: str): cursor = self.con.cursor() query = \u0026#34;\u0026#34;\u0026#34; SELECT jcr.Journal, jcr.ISSN, jcr.\u0026#34;IF(2022)\u0026#34; AS Ifactor, jcr.IFQuartile FROM JCR2022 jcr WHERE jcr.Journal = ? COLLATE NOCASE \u0026#34;\u0026#34;\u0026#34; cursor.execute(query, (journal_name,)) res = cursor.fetchall() cursor.close() return res def get_journal_cas_rank(self, journal_name: str): cursor = self.con.cursor() query = \u0026#34;\u0026#34;\u0026#34; SELECT cas.Journal, cas.大类分区 AS CASBig, cas.小类1分区 CASSmall1 FROM FQBJCR2023 cas WHERE cas.Journal = ? COLLATE NOCASE \u0026#34;\u0026#34;\u0026#34; cursor.execute(query, (journal_name,)) res = cursor.fetchall() cursor.close() return res def get_journal_rank_union(self, journal_name: str): cursor = self.con.cursor() query = \u0026#34;\u0026#34;\u0026#34; SELECT jcr.Journal, jcr.ISSN, jcr.\u0026#34;IF(2022)\u0026#34; AS Ifactor, jcr.IFQuartile, cas.大类分区 AS CASBig, cas.小类1分区 CASSmall1 FROM JCR2022 jcr, FQBJCR2023 cas WHERE jcr.Journal = cas.Journal AND jcr.Journal = ? COLLATE NOCASE \u0026#34;\u0026#34;\u0026#34; cursor.execute(query, (journal_name,)) res = cursor.fetchall() cursor.close() return res querier = JCRQuerier(JCR_DB) journal_name = \u0026#34;nature\u0026#34; rank = querier.get_journal_jcr_rank(journal_name) print(rank) ","date":"2024-05-10T15:00:00+08:00","permalink":"/2024/05/10/jcr-cas-ranking-database/","title":"论文分区数据库与应用"},{"content":"或许家家都有本难念的经，而在我的电脑上常常有些奇怪的问题或需求。为了这样的内容单独写一篇文章真的很逊，但如果不找个地方记下来又怕哪日再寻找时完全忘记了。\n不如在这里将这些奇怪的内容整理在一个页面中，方便快速搜索查找。\n应用程序篇 Firefox强制启用阅读视图/打印简化版本 打印Firefox浏览器渲染的网页时，有时可以看到“格式”下存在一个 “精简” 选项，这个选项通常可以把网页的主要内容提炼出来，以紧凑的布局打印，效果还是很不错的。\n然而，在有些网站上打开打印对话框时，并不能看到 “精简” 格式选项，这是因为，该选项实际上是先将网页渲染为Firefox的阅读视图再进行打印，而一部分网页并不能被Firefox自动识别并排版为阅读视图，自然无法打印。\n那，我们能否强制Firefox不管结果，直接渲染网页为阅读试图呢？答案是肯定的。下面提供两种方法。不过，丑话说前头，这些方法**都无法保证网页渲染结果正常，**因此渲染成功后还需要自行检查格式和内容。\n不修改设置不安装插件 在地址栏中，直接在整个URL最前面添加前缀：about:reader?url=。例如，如果当前正在访问http://www.example.com/abc，那么应当修改地址栏中的URL为：\n1 about:reader?url=http://www.example.com/abc 此时，就可以强制将URL后的网页渲染成阅读视图。\n修改Firefox设置 在Firefox中访问about:config，搜索设置项reader.parse-on-load.force-enabled，设置成true(boolean)，就可以让进入阅读模式的按钮一直出现在地址栏中。\n打印 开心地按下Ctrl + P键打印吧。\n虚拟化篇 QEMU/KVM便捷地创建桥接网络 有时候，我们希望把虚拟机暴露到局域网LAN中，方便局域网内其他机器访问。然而，在Debian系统上使用QEMU/KVM虚拟化技术，配合libvirt，其默认创建的default网络是NAT模式的，不能满足以上要求。\n目前网络上关于创建QEMU桥接网络的方法有很多，但通常很复杂，而且常常要求机器上有两个网卡。\n问题在于，普通家用电脑上，通常很难找到两个同时连接到网络的网卡。\n因此，这里推荐一种相对方便的，使用macvtap的桥接网络创建方法。\n宿主机创建macvtap网络接口 在宿主机上使用以下命令创建一个macvtap网络接口：\n1 nmcli connection add type macvlan dev eth0 mode bridge tap yes ifname macvtap0 其中，eth0是连接到网络，也就是虚拟机希望桥接到的网络的物理网卡接口。ifname后的macvtap0是可以任意指定的接口名称。\n再执行以下命令：\n1 2 nmcli connection modify macvlan-macvtap0 ipv6.method disabled nmcli connection modify macvlan-macvtap0 ipv4.method disabled 根据前面接口名称的不同，modify后面的连接名称可能也有差异。\n重新启动以上网络连接。\n1 2 nmcli connection down macvlan-macvtap0 nmcli connection up macvlan-macvtap0 虚拟机连接到macvtap网络接口 打开virt-manager界面，修改虚拟机网卡NIC设置，网络类型选择Macvtap device。网络设备名称填写上面创建的接口名，例如macvtap0。\n保存后启动虚拟机，此时虚拟机就桥接连接到网卡所在网络中了。\n宿主机和虚拟机的访问 macvtap的一大局限是，默认配置下，宿主机不能和虚拟机通过此网卡通信。即便分配到了局域网地址，宿主机访问虚拟机的局域网地址也是不通的。\n为了允许宿主机也可以和虚拟机通信，一种解决方法是，额外在虚拟机上创建一个NIC，并连接到默认的NAT连接上。\nLinux运维优化篇 Gnome应用列表按字母表排序 至今，我依然没有弄明白，GNOME的应用列表里，图标究竟遵循了怎样的排序规律。\n或许翻阅源代码可以弄明白，但或许也不需要了，因为我更习惯按应用名称字母表排序。想要设置按字母表排序，只需要在命令行中执行以下命令即可：\n1 gsettings set org.gnome.shell app-picker-layout \u0026#34;[]\u0026#34; Debian系统列出可升级软件包所含Bug 前不久，Debian Testing更新内核版本号至6.10，导致默认软件源中的NVIDIA驱动535版本DKMS模块无法正常编译，我意识到：原来Debian Testing作为桌面系统也会产生严重Bug啊！\n大家都知道，Debian软件包上发现的Bug，都会在Debian bug tracking system里被跟踪，但在近乎滚动更新的Debian Testing上，一次更新可能超过200个软件包，不可能一个个手动分开查询。\n我们可以使用apt-listbugs工具完成这一操作。如果系统上没有，使用APT即可快速安装。\n接着只需要执行以下命令，待更新的软件包中有哪些严重Bug就都知道了。\n1 apt-listbugs -s critical,grave,serious list $(apt list --upgradable 2\u0026gt;/dev/null | awk -F/ \u0026#39;{print $1}\u0026#39; | grep -v \u0026#34;Listing\u0026#34; | tr \u0026#39;\\n\u0026#39; \u0026#39; \u0026#39;) 其中，-s参数指定了查询的Bug严重程度，默认就是上例中的critical,grave,serious，如果希望无论轻重列出所有Bug，只需要把这一串参数改成all即可。$()中的一大串是，将APT命令列出的可升级软件包列表格式化成apt-listbugs可接受的空格列表。\nSSH Host Key遗忘 我，毋畏遗忘\n虽然SSH Host Key在不可信网络上确实是验证SSH连接可信性的重要手段，但是，在会频繁刷新Host Key的如容器、虚拟机场景下，又显得不太方便。\n然而，SSH并没有直接提供诸如“不记录本机Host Key”等功能，有一个选项StrictHostKeyChecking no，但其仍然会在Host Key不匹配时弹出大大的警告窗口。\n这样的目标可以简单变通实现，只需要在 SSH Config 配置中设置 UserKnownHostsFile，将其指向黑洞即可。\n例如：\n1 2 3 4 5 6 7 8 9 10 11 12 Host myserver Hostname 192.168.1.2 Port 22 User root IdentityFile ~/.ssh/id_rsa StrictHostKeyChecking no # Linux # UserKnownHostsFile /dev/null # Windows UserKnownHostsFile \\\\.\\NUL Systemd限制用户计算资源 在多人使用的Linux服务器上，会遇到这样的情况：某些淘气的小伙伴把服务器资源都占满了，其他人完全无法使用，能不能做点限制呢？\n限制用户CPU使用 假设服务器上一共有32个逻辑CPU（线程），如果有一个用户运行的程序将32个逻辑核全部吃满，会导致其他所有用户程序，即使是一些简单的小工作，都很难正常运行。\n在这种时候，若希望保障其他用户进行小工作处理的能力，可以用Systemd对占满大量资源的用户进行限制。\n在Systemd中，用户进程和服务都会受到user-\u0026lt;UID\u0026gt;.slice配置文件的管理。其中，就提供了计算资源限制相关的属性。\n首先来看最简单的限制方法：限制用户使用的最大CPU时间。假设服务器上有32个逻辑核，可以只允许某一用户最多使用等价24个核的CPU计算时间。这样一来，CPU就有余力处理其他用户的请求。\n1 2 3 USER_UID=$(id -u \u0026lt;限制目标用户名\u0026gt;) sudo systemctl set-property \u0026#34;user-${USER_UID}.slice\u0026#34; CPUQuota=2400% 如果有需要，甚至可以把该用户的进程全部赶到前24个逻辑核上，这样其他用户就稳定地还有8个核可用，非常划得来。\n1 sudo systemctl set-property \u0026#34;user-${USER_UID}.slice\u0026#34; CPUQuota=2400% AllowedCPUs=0-23 但这种方法会导致，即使没有其他用户使用服务器，被限制的用户也最多只能使用2400%的CPU时间，没法充分利用CPU算力资源。\n而理想状态下，我们其实只是希望，要长时间高负载占用CPU的用户，能在其他用户使用时，稍微让出一条路，让其他用户的小工作能正常进行。\n所以有第二种方法：限制用户的进程优先级。\n通过设置进程调度，可以将某个用户的进程优先级拉低，这样，其他用户进程会被分配更多时间片以保证交互流畅，而长时任务在没其他人时，依然可以满负载运行。\n1 sudo systemctl set-property \u0026#34;user-${USER_UID}.slice\u0026#34; CPUQuota= CPUWeight=25 IOWeight=25 Linux默认的CPUWeight是100，权重越大，得到的CPU时间就越多。在这里，IOWeight也是同理，且IO优先级对一些程序的交互体验影响更加明显。\n限制内存使用 假设系统总内存128G，若希望限制所有用户内存总使用量在限定范围内，杜绝OOM，可以试着这样操作：\n1 sudo systemctl set-property user.slice MemoryHigh=96G MemoryMax=112G 实战中只拿来阻止过某个程序的 OOM Bug.\n限制用户GPU访问 有时，资源管理也颇为无奈，曾遇到一个情况，已经脱离团队的人，却仍然在使用组内的GPU。\n这时，其他成员急着用卡，碍于身份难以开口让其停止使用。而且，更不可能直接强制删除用户数据，再怎么说都要给人家下载数据的时间，怎么办呢？\n能不能悄悄地把这名用户的GPU访问权限下掉，但同时保留正常的用户登录、文件访问权限呢？\n这也可以通过Systemd设备管理实现。对于需要限制的用户，创建一个文件夹/etc/systemd/system/user-\u0026lt;UID\u0026gt;.slice.d/：\n1 mkdir /etc/systemd/system/user-${USER_UID}.slice.d 然后创建一个文件override.conf，填入以下内容：\n1 2 3 4 5 6 7 8 # /etc/systemd/system/user-1001.slice.d/override.conf [Slice] DevicePolicy=closed # Interactive terminals DeviceAllow=/dev/tty rw DeviceAllow=char-pts rw 实际上，这种限制等级比较高，对于该用户来说，不仅GPU无法使用，系统上位于/dev下的设备，包括未挂载的硬盘、USB等都无法访问。当然，在科学计算服务器上，普通用户往往也无需访问这些设备。\n施加这一设置后，对被限制的用户来说，其运行nvidia-smi只会提示找不到GPU设备。同时，还不影响其访问/home路径下的数据，可以让其自行将数据导出带走。\n密码强度设置 在Debian/Ubuntu下，怎么设置系统用户密码的最低强度标准？\n首先需要安装libpam-pwquality包：\n1 sudo apt install libpam-pwquality 然后编辑/etc/security/pwquality.conf文件，可参考文件注释进行调整：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # Number of characters in the new password that must not be present in the # old password. difok = 3 # Minimum acceptable size for the new password (plus one if # credits are not disabled which is the default). (See pam_cracklib manual.) # Cannot be set to lower value than 6. minlen = 12 # The maximum credit for having digits in the new password. If less than 0 # it is the minimum number of digits in the new password. dcredit = -1 # The maximum credit for having uppercase characters in the new password. # If less than 0 it is the minimum number of uppercase characters in the new # password. ucredit = -1 # The maximum credit for having lowercase characters in the new password. # If less than 0 it is the minimum number of lowercase characters in the new # password. lcredit = -1 # The maximum credit for having other characters in the new password. # If less than 0 it is the minimum number of other characters in the new # password. ocredit = -1 上面的配置对应于以下密码强度约束：\n修改密码与原密码必须有3个字符以上不同 密码最小长度为12字符 密码必须包含大小写字母、数字、其他字符（标点） 快速设置命令特定的环境变量组 众所周知，在Linux中给普通应用设置环境变量，一般只需要在~/.bashrc等Shell配置文件中直接写export语句即可。\n不过，有时候存在下面需求：\n希望给不同的命令指定同名环境变量的不同值，例如MYENV=abc cmd1和MYENV=def cmd2 不希望环境变量对所有命令全局生效，只对特定命令生效 在.bashrc中用更加简洁的语法管理环境变量 那么，相较于原始的大量独立export语句写法，可以试试下面的写法。\n假设原先的.bashrc如下：\n1 2 3 export MYAPP_ENV_1=abcd export MYAPP_ENV_2=efg export MYAPP_ENV_3=hijk 再假设需要设置上述环境变量的命令为myapp，那么可以将上面的export语句换成下面这种写法：\n1 2 3 4 5 6 7 8 9 MYAPP_ENVS=( MYAPP_ENV_1=abcd MYAPP_ENV_2=efg MYAPP_ENV_3=hijk ) myapp() { env \u0026#34;${MYAPP_ENVS[@]}\u0026#34; myapp \u0026#34;$@\u0026#34; } 这样设置的好处包括：\n在Shell中调用myapp命令与常规调用等价，不影响参数传递，也不影响argv等参数内容 MYAPP_ENVS中定义的变量只影响myapp一个命令，对其他命令全无影响 增删环境变量只需直接按行修改，可按行注释掉不想要的环境变量，维护方便 若定义了多个环境变量数组，只需要在env命令后，程序命令之前添加即可：\n1 2 3 4 5 6 7 8 ANOTHER_MYAPP_ENVS=( MYAPP_ENV_4=hijk MYAPP_ENV_5=xyz ) myapp() { env \u0026#34;${MYAPP_ENVS[@]}\u0026#34; \u0026#34;${ANOTHER_MYAPP_ENVS[@]}\u0026#34; myapp \u0026#34;$@\u0026#34; } Claude Code 快速安装使用 注意：npm安装方式未来可能有变动，请先行查阅官方文档\n首先执行 npm install -g @anthropic-ai/claude-code。\n安装完成就要开始配置。如果使用A社官方模型API，记得设置好网络。\n当然，我用的是Deepseek，其他第三方模型也可以参考下面的配置方法。\n新建一个~/.claude.json文件，按需填写：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { \u0026#34;env\u0026#34;: { \u0026#34;ANTHROPIC_BASE_URL\u0026#34;: \u0026#34;https://api.deepseek.com/anthropic\u0026#34;, \u0026#34;ANTHROPIC_AUTH_TOKEN\u0026#34;: \u0026#34;sk-12345678\u0026#34;, \u0026#34;ANTHROPIC_MODEL\u0026#34;: \u0026#34;deepseek-v4-pro[1m]\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_OPUS_MODEL\u0026#34;: \u0026#34;deepseek-v4-pro[1m]\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_SONNET_MODEL\u0026#34;: \u0026#34;deepseek-v4-pro[1m]\u0026#34;, \u0026#34;ANTHROPIC_DEFAULT_HAIKU_MODEL\u0026#34;: \u0026#34;deepseek-v4-flash\u0026#34;, \u0026#34;CLAUDE_CODE_SUBAGENT_MODEL\u0026#34;: \u0026#34;deepseek-v4-pro[1m]\u0026#34;, \u0026#34;CLAUDE_CODE_EFFORT_LEVEL\u0026#34;: \u0026#34;max\u0026#34;, \u0026#34;DISABLE_AUTOUPDATER\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;ENABLE_TOOL_SEARCH\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;IS_SANDBOX\u0026#34;: \u0026#34;1\u0026#34; }, // 这一行很重要，否则会在启动时连接A社API并提示地区不支持 \u0026#34;hasCompletedOnboarding\u0026#34;: true } ","date":"2024-01-29T11:00:00+08:00","permalink":"/2024/01/29/diabolic-tricks-wicked-craft/","title":"持续更新中的稀奇古怪技巧"},{"content":"基于方面的情感分析(Aspect Based Sentiment Analysis, ABSA)是一种细粒度的情感分析任务，即，对于给定的一段文本，识别出该文本针对文中指定的某一方面的情感极性。\n📝 备注 2025年11月：更新了基于CausalLM的方法\n本文将介绍如何在本机上部署并使用预训练的DeBERTa V3模型对文本中的某一特定方面进行情感分析，从而实现观点挖掘相关的工作。当然，由于本人没有涉足过NLP的具体研究，可能很多表述并不严谨，敬请批评指正。\n前言 这两天接触了一下情感分析相关的工作，大致是需要确定一些文本对某些个体的情感倾向。经过搜索，这属于情感分析(Sentiment Analysis)的任务范畴，而更具体地，实际上是基于方面的情感分析(Aspect Based Sentiment Analysis, ABSA)。\n所谓基于方面的情感分析，我们用一个例子来说明就可以。\nThe food here is delicious, but the serivce is terrible.\n如果上面这句话是某个顾客对某家餐馆的评价，那么我们应该如何精确地描述这名顾客对餐馆的观点呢？\n一个很显然的结论是，恐怕我们不能简单地将 delicious 或 terrible 作为顾客的态度代表，也不能简单地因为文本中出现了一个积极的词语和一个消极的词语，分别赋分1和-1，然后加起来得分为0，结论为情感中性。\n但如果现在有一位饥肠辘辘的游人来到了这座城市，他更在乎食物的口味，但却对餐馆的服务态度毫不在意。现在，他正在手机上搜寻附近口味出众的餐馆，根据上述顾客的评论，我们能否将这家餐馆推荐给他呢？\n通过上述这个案例，我们会发现，在现实中，我们面对的大量文本可能会同时描述多个实体(Entity)，例如上面出现在同一句话中的 food 和 serivce ，并表示出出截然相反的态度。然而，传统的方法无法准确地判别出隐藏在文本中的上述情感差异。\n而通过对文本中不同的实体进行情感分析，就有可能发现人们在观点/意见(Opinion)上的差异。在上述例子中，顾客的评价是对餐馆不同方面/实体的观点存在差异，现实中包括新闻、媒体、文章等，可能都会对不同的事物存在不同的态度。通过发掘，可能发现上述差异，从而辅助决策。\n(2025年11月更新) 基于CausalLM的实现方法 在CausalLM时代 两年前写下这篇文章时，GPT还是个新鲜事物，宛若天顶星科技一般，或许有人想过用GPT的API完成本文介绍的情感分析任务，但看在API价格的份上也还得慎重考虑。\n两年后的今天，各种Decoder-Only架构的CausalLM获得了极其亮眼的进步，大模型也不再是少数公司的专利，开放权重的模型可以轻松获取，而LLM API的价格已经足够便宜。\n在各种模型一路高歌猛进，卷着Agentic能力、Coding能力、Novelty的路上，本文所介绍的ABSA任务，被 “轻松地” 解决了，以更便宜更好的效果，像马儿扬鞭飞驰而去，徒留下一阵尘土。\n回头来看，只是不由得感慨，这样的进步不可谓不日新月异哇。\n实现案例 时至今日，这样的任务简单到只要是一个训练正常的CausalLM，哪怕参数量只有0.5B都能轻松解决，不仅分类效果好，支持多语言，对输出分类也可以极其轻松地自定义。\n写了一个使用Qwen2.5-0.5B的示例，直接上代码和提示词：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 # %% from transformers import AutoTokenizer, AutoModelForCausalLM # %% model_id = \u0026#34;./Qwen2.5-0.5B-Instruct\u0026#34; tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_pretrained(model_id, dtype=\u0026#34;auto\u0026#34;, device_map=\u0026#34;auto\u0026#34;) # %% sentence = \u0026#34;The food here is delicious but the service is awful.\u0026#34; entity = \u0026#34;service\u0026#34; prompt = f\u0026#34;\u0026#34;\u0026#34; Analyze the sentiment expressed toward the specified entity in the sentence. Classify it as exactly ONE of these categories: Happy, Sad, Angry, Surprise. Output only the category name. Sentence: {sentence} Entity: {entity} \u0026#34;\u0026#34;\u0026#34; messages = [ { \u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;You are Qwen, created by Alibaba Cloud. You are a helpful assistant.\u0026#34;, }, { \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: prompt }, ] text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) model_inputs = tokenizer([text], return_tensors=\u0026#34;pt\u0026#34;).to(model.device) generated_ids = model.generate(**model_inputs, max_new_tokens=512) generated_ids = [ output_ids[len(input_ids) :] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) ] response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] print(response) 事已至此，微调什么的都不需要了（笑）。\n实现方法 搜索ABSA相关的应用方法，首先发现的是Azure提供的Sentiment analysis and opinion mining服务，刚好免费申请的Azure for Students权益包里似乎也能用这个API，只要把需要分析的文本上传，就可以得到分析的结果。\n但思来想去，觉得光是调用API就没意思了。\n继续搜索，发现HuggingFace上有很多训练好的模型，仔细一看，推理使用到的计算硬件资源也不大，在可以接受的范围内，于是决定试试在自己电脑上部署模型进行推理。\n只是奈何自己也不会什么模型，最后还是得用其他人做好的模型来完成这项工作。不过倒也可以之后再改进吧。\n这里我们使用的是DeBERTa V3模型，HuggingFace上已经有人使用ABSADatasets训练好了可用于ABSA任务的模型，请见yangheng/deberta-v3-large-absa-v1.1.\n可以参考的其他模型 在情感分析方面，当然也有一些其他预训练好的模型可以使用，例如：\nbhadresh-savani/distilbert-base-uncased-emotion: 尽管这并不是一个ABSA任务的模型，但其可以输出6种类别的情感。 部署细节 下面大致介绍一下本地部署模型的相关步骤和可能踩坑的地方。\n获取模型 确保Git已经启用LFS扩展，然后执行如下命令：\n1 2 3 GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/yangheng/deberta-v3-large-absa-v1.1 git lfs pull -I \u0026#34;model.safetensors\u0026#34; git lfs pull -I \u0026#34;spm.model\u0026#34; 在我们使用的这一模型仓库里，我们需要拉取的是model.safetensors和spm.model两个文件，其中model.safetensors包含了模型主要的权重，因此不需要重复下载pytorch_model.bin权重文件。\n本机环境 为了在本机上进行推理，需要创建一个Python环境，依赖如下：\ntransformers numpy 这两个就是最简单的，使用CPU进行推理的配置了。稍后我们介绍不同模式时会再介绍其他模式下依赖的Python包。\n运行推理 回到上面克隆的模型仓库目录的同一级目录下，创建一个.py文件（或者.ipynb，如果你喜欢），这里取名run.py。大致文件结构如下：\n1 2 3 - MyABSA - deberta-v3-large-absa-v1.1/ - run.py 编辑run.py文件，填入以下代码：\n1 2 3 4 5 6 7 8 9 10 from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline model_path = \u0026#34;deberta-v3-large-absa-v1.1\u0026#34; model = AutoModelForSequenceClassification.from_pretrained(model_path) tokenizer = AutoTokenizer.from_pretrained(model_path) recognizer = pipeline(\u0026#34;text-classification\u0026#34;, tokenizer=tokenizer, model=model) sentence = \u0026#34;[CLS] The food here is delicious but the service is awful. [SEP] service [SEP]\u0026#34; recognizer(sentence) 这里我们传入模型的输入分为两个部分，前半段即为本文开头的餐馆评价示例，而后面，用[SEP]标志和主要文段内容分割开的，就是我们希望探知的情感的实体。\n在上面的输入中，我们希望模型给出，前面语句对于service这个实体的态度。\n运行上述代码，或许你会得到类似这样的输出：\n1 2 3 [[{\u0026#39;label\u0026#39;: \u0026#39;Negative\u0026#39;, \u0026#39;score\u0026#39;: 0.9997419714927673}, {\u0026#39;label\u0026#39;: \u0026#39;Neutral\u0026#39;, \u0026#39;score\u0026#39;: 0.00018644658848643303}, {\u0026#39;label\u0026#39;: \u0026#39;Positive\u0026#39;, \u0026#39;score\u0026#39;: 7.166137220337987e-05}]] 结果不难解读，模型给出的推理认为，前面的文本对service一词表现出的消极态度占据最主导的地位。\nWebAPI化 为了便于对外设计接口，还可以将推理任务设计成WebAPI，对外暴露服务。\n测试了一下使用ONNX Runtime进行的部署，甚至还测试了使用DirectML加速的ONNX Runtime，但似乎没啥用，测试下来尽管GPU占用是有了，但速度甚至比用numpy在CPU上的推理还要慢了。很大可能是我代码写的不好，数据在CPU和GPU之间反复倒腾了，导致速度上不去。\n总之这一部分的代码，首先需要导出模型为ONNX格式，然后再放到这里进行加载。其实这部分内容主要是我想亲自动手看看，在pipeline中间到底发生了什么。实际使用其实并不用这样做的。\n使用CPU进行推理的具体代码看下面吧，依赖项在引入部分已经写清楚了：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 from fastapi import FastAPI, Depends, Request from pydantic import BaseModel from transformers import AutoTokenizer from scipy.special import softmax import json import numpy as np import onnxruntime MODEL_DIR = \u0026#34;deberta-v3-large-absa-v1.1_onnx\u0026#34; app = FastAPI() def create_onnxruntime_infersess(model_file: str): print(f\u0026#34;Infering using {onnxruntime.get_available_providers()}\u0026#34;) return onnxruntime.InferenceSession(model_file) def create_tokenizer(model_dir: str): return AutoTokenizer.from_pretrained(model_dir) def create_id2label_dict(model_config: str): with open(model_config, \u0026#34;r\u0026#34;) as f: return {int(k): v for k, v in json.load(f)[\u0026#34;id2label\u0026#34;].items()} ort_infersess = create_onnxruntime_infersess(f\u0026#34;{MODEL_DIR}/model.onnx\u0026#34;) bert_tokenizer = create_tokenizer(MODEL_DIR) id2label_dict = create_id2label_dict(f\u0026#34;{MODEL_DIR}/config.json\u0026#34;) def get_inference_session(): return ort_infersess def get_tokenizer(): return bert_tokenizer def get_id2label_dict(): return id2label_dict def predict(sentence: str, aspect: str): infsess = get_inference_session() tokenizer = get_tokenizer() id2label = get_id2label_dict() inputs = tokenizer(sentence, aspect, return_tensors=\u0026#34;np\u0026#34;) outputs = infsess.run( None, { \u0026#34;input_ids\u0026#34;: inputs[\u0026#34;input_ids\u0026#34;].astype(dtype=np.int64), \u0026#34;attention_mask\u0026#34;: inputs[\u0026#34;attention_mask\u0026#34;].astype(dtype=np.int64), }, ) pred_probs = softmax(outputs).flatten() return [(id2label[arg], pred_probs[arg]) for arg in np.flip(pred_probs.argsort())] def get_predict_handler(request: Request): return predict class SentimentAnalysisRequest(BaseModel): sentence: str aspect: str @app.post(\u0026#34;/analysis\u0026#34;) async def post_analysis( request: SentimentAnalysisRequest, predictor=Depends(get_predict_handler) ): sentence = request.sentence aspect = request.aspect result_list = predictor(sentence, aspect) return { \u0026#34;aspect\u0026#34;: aspect, \u0026#34;mostlike_label\u0026#34;: result_list[0][0], \u0026#34;mostlike_probability\u0026#34;: f\u0026#34;{result_list[0][1]:.6f}\u0026#34;, \u0026#34;full_result\u0026#34;: [{\u0026#34;label\u0026#34;: label, \u0026#34;prob\u0026#34;: f\u0026#34;{prob:.6f}\u0026#34;} for label, prob in result_list], } 这将创建一个WebAPI，监听POST请求，请求主体是一个JSON格式的输入，包含sentence和aspect两个字段，前者是文本内容，后者就是希望获取态度的Aspect/Entity。\nTODO []试着在预训练模型上进行fine-tune. []试着将推理过程运行在GPU上进行加速 ","date":"2023-12-10T15:00:00+08:00","permalink":"/2023/12/10/deberta-v3-aspect-based-sentiment-analysis/","title":"使用DeBERTa V3进行基于方面的情感分析(ABSA)"},{"content":"前言 因为一些缘故，接触了一下对空间地理数据的分析工作。忙完以后，想了想决定写下这篇文章，方便日后如有需要，可以快速查阅，也供后人参考。\n背景条件 我们主要是针对给定的空间地理数据（如GeoJson，Shapefile）进行一些数据提取和分析，获得我们想要的分析结果。本文不讨论这些数据如何收集，抑或是如何编制，我们只讨论分析方面的内容，并且也不会涉及非常深，因为本文重点更在于，快速介绍对地理数据进行分析的大致方法，而具体的分析思路、分析流程，是各位灵感发挥的重要地方。\n本文会涉及到对PostgreSQL数据库的使用，建议未接触过PostgreSQL的读者自行查阅相关的基础资料。不过，假如对基本的SQL，特别是MySQL/MariaDB有基本的了解，甚至是一定的使用经验，可以参考本文的前序文章在MySQL基础上速通PostgreSQL\n因为我本身不是学地理信息专业的，因此以上知识与理解是临时抱佛脚速成学得，如果本文有什么错误，还请批评指正（请发邮件给我）！多谢各位包涵。\n目标 我们的分析工作环境可能需要我们做到以下几点。\n建立一个PostgreSQL数据库（下面简称psql），并启用PostGIS 将收集到的数据放到psql中 使用Python对这些数据进行进一步的分析计算 数据库为什么选择PostgreSQL呢？可能是由于PostgreSQL中的GIS插件PostGIS比较优秀，支持比较好。\n而选用Python进行数据分析，是因为其提供了Numpy/Pandas/Geopandas/Scipy等十分通用的工具。如果你喜欢其他工具体系，完全可以替换，只要可以连接读取psql中的地理数据即可。这里我们以Geopandas作为主要工具。\n环境配置 我觉得配环境向来是浪费口舌的，但又是最重要的一环，没有配好的环境，我们什么都做不了。\n我们需要安装的软件有:\nPostgreSQL + PostGIS插件 Python + Pandas/Numpy/Scipy/SQLAlchemy/Matplotlib模块 Geopandas/Geoalchemy 一般来说，最上面的两项是比较容易配置的，安装就好了，有非常多的方法，PostgreSQL在Windows上有安装包，在Linux上有软件包管理器。而Python部分无论是使用Conda还是使用Pip，都非常方便。\n但有个例外，就是Geopandas/Geoalchemy两项。不知道是怎么回事，Conda源中Geopandas版本号比较古老，而使用conda-forge渠道会把安装卡在环境依赖关系检测部分。我当时是尝试了几次都没弄出理想的效果。\n如果你也遇到了这样的问题，或许可以试试下面的方法：容器/Windows Subsystem of Linux\n独立的环境 实际上这个方法比较诡异，但至少是能非常顺利地使用的。\n我当时是无意中，看到Ubuntu的APT源中，存在python3-geopandas和python3-geoalchemy2两个软件包，版本号也不过时(Debian源中也有，但Debian Bullseye使用的版本号比Ubuntu 22.04的老一些)，遂直接放弃使用Conda配环境的念头，直接安装这两个软件包，运行一切正常。\n当然，这样的方法能不出乱子，也得益于这些软件包没有依赖特定的硬件配置或者组件版本号。如果是需要依赖某个特定版本CUDA Toolkit的软件包，或许这样会出现更大的问题。\n不过，这样一来，环境隔离就变成了一个需要考虑的问题。我们希望将这些环境隔离起来，万一日后发生错误，可以整个删除，而不对骨干系统造成影响。而APT安装的软件包，是全局性的。\n因此，我们可以通过将这部分环境，整个作为一个容器进行隔离。\n如果你主力操作系统使用的是Windows，那么可以尝试在WSL中安装Ubuntu（目前WSL可以自己手动import发行版，只要空间足够，可以添加非常多个实例，作为各自隔离的子系统，即便它们是同一个发行版，这和应用商店安装的有所不同）。 如果你主力操作系统使用的是Linux，那么可以试试LXC/LXD解决方案，直接运行一个Ubuntu容器。Docker应该也是不错的选择。 如果你主力操作系统使用的是Mac OS X，那………我也不清楚有什么好的解决方案，毕竟我不是苹果用户，反正思路和上面的是一样的。实在不行还有虚拟机对吧。 创建容器后，就可以直接通过APT安装python3-geopandas和python3-geoalchemy2，以及其他需要的Python模块。\n如果你喜欢使用Visual Studio Code 配合 Jupyter Notebook，那么只需要安装python3-ipykernel软件包，并将Visual Studio Code连接到你的WSL或者容器中，打开工作文件夹，创建一个.ipynb文件，选择内核为系统目录下的python，你喜欢的工作流程就呈现在你的面前了。\nPostGIS速通 接下来，我们试着介绍以下，如何使用PostGIS对地理数据进行查询。假设我们需要在名为gis_test1的数据库上存储空间数据。\n顺带一提，我好像并没有接触3D形状的查询分析，我接触的都是在平面上的地理数据分析。不过，方法几乎是相同的。\n启用PostGIS 若想要在一个数据库(Database)中存储地理空间数据，则必须启用PostGIS扩展。\n⚠️ 警告 这里的数据库(Database)概念，指的是PostgreSQL服务器下,一个个相对独立的Database，并不是指整个PostgreSQL服务器实例。\n我们连接到目标数据库后，使用以下代码启用PostGIS。操作前请确认执行以下语句时，是否已经切换到对应的数据库。\n1 CREATE EXTENSION postgis GIS中的形状 小学数学课上学过的，点动成线，线动成面，面动成体，在我们三维空间中都遵循着这些道理。\n而如果我们将目光投向真实世界平面地图，我们会发现，地图上的许多事物，可以概括为以下几种类型:\n点(Point): 地图上的一个点，例如，家门口的小卖部，抑或是市内的一座医院，就是一个点 线(Linestring): 地图上的一条线，例如，一条没有分支的地铁线路，从头到尾就是一条线 多边形(Polygon): 地图上的一个多边形，例如，在中国，一个县级行政区划（市辖区、县级市、县、旗等）所管辖的区域，就是一个多边形 这几种类型，正是在GIS中，对二维地图上的空间事物进行描述的基本类型。参考9. Geometries — Introduction to PostGIS\n在此之上，如果我们将多个同类型的事物，放到一起，就会得到几个集合类型(Collection)，其中几个都是以 Multi 开头的\n点集(MultiPoint): 地图上的多个点，例如，市内同名连锁便利店的集合，就是一个点集 线集(MultiLineString): 地图上的多条线，例如，市内地铁网络的线路集合，就是一个线集 多边形集(MultiPolygon): 地图上的多个多边形，例如，在中国，一个地级市下属的所有县级行政区划（市辖区、县级市、县、旗等），就是多个多边形组成的集合 地理集合(GeometryCollection): 如你所猜想的，以上几种任意类型的集合，而且也可以包含集合类型 这时候一定会有人问，上文中为什么要将医院、便利店标记成点(Point)？许多地点都有一定的尺寸、占地面积，将其抽象成点是否合适\n参照上面参考页面:\nPoints are used to represent objects when the exact details, such as shape and size, are not important at the target scale. For example, cities on a map of the world can be described as points, while a map of a single state might represent cities as polygons.\nPolygons are used to represent objects whose size and shape are important. City limits, parks, building footprints or bodies of water are all commonly represented as polygons when the scale is sufficiently high to see their area. Roads and rivers can sometimes be represented as polygons.\n省流版本: 这取决于考察的区域范围与这些地点大小的相对关系，以及这些地点的尺寸是否会对分析结果产生影响。\n举个例子，我们如果想要在城市火车站一角，张贴一张地铁线网概览图，那么我们可以放心地将地铁线路抽象为一条条线，而且这些线在图中的粗细，除了影响游客的可读性外，在传达信息的准确性上，并不会带来什么负面影响。\n但如果，我们需要在地铁线路附近新建一栋大楼，需要向下挖掘地基，那么我们就不能用上面例子中的概览图来确定施工范围边界！毕竟一条线本身并没有粗细，在考察施工边界这样精度的问题时，这样的一条条线不能告诉我们地铁隧道具体位于何处，也不能告诉我们应该从何处向下挖掘地基。我们必须将地下的铁路隧道视为具有具体粗细的线，也就是一个多边形，才能在地图中准确表现出地铁隧道的位置，从而让我们准确避开地铁隧道进行施工。\nPostGIS的形状运算 我们定义出上述元素，自然是为了方便接下来的运算工作。\n通常，我们存储到数据库中的数据，常常是地图上事物的原始数据，如城市中公交车站的位置，各建筑物的具体地理边界等。然而，对于我们的分析需求来说，从数据库中读取这些原始数据往往是不够的，我们通常需要进行更复杂的运算，例如查询某个区域内公交车站的数量，或是某个区域内道路的总长度。\nPostGIS提供的函数可以帮助我们通过SQL语句，构造上述查询目标的SQL表达式，从而轻松从数据库中获得上述信息。\n形状的关系 在中小学时期，我们就学习过，两个图形是否相交，如何相交，会构成不同的模式。在PostGIS中，这一问题进一步和图形的内部、边界、外部结合起来，构成了GIS系统中的九维相交模型(9-Intersection Model)。\n具体的解释，可以参考https://postgis.net/docs/manual-3.2/using_postgis_query.html\n简单来说，我们常常涉及到的运算，其实主要以计算相交(Intersect)和包含(Contain)关系为主。而PostGIS的函数，通常以ST_开头。上述链接也提供了相关的示例，可以看看。\n以我编写的一个SQL查询为例。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 SELECT n.area_id, n.area_name, ca.crime_area, ST_Area(ssa2.geometry) AS nei_area FROM neighbourhoods n JOIN ( SELECT n.area_id, SUM(ST_Area(ST_Intersection(ssa.geometry, bae.geometry))) AS crime_area FROM neighbourhoods n JOIN sa2_statistical_areas ssa ON n.area_id = ssa.\u0026#34;SA2_MAIN16\u0026#34; JOIN break_and_enter bae ON ST_Intersects(ssa.geometry, bae.geometry) GROUP BY n.area_id ) ca ON ca.area_id = n.area_id JOIN sa2_statistical_areas ssa2 ON n.area_id = ssa2.\u0026#34;SA2_MAIN16\u0026#34; 在上述查询中，我们用ST_Intersects函数，计算ssa表中的geometry列，与bae表中的geometry列的交集，如果有两条记录产生了交集，则JOIN条件为真，联表查询生成的结果中会多出一行。\n","date":"2022-05-23T12:09:16+08:00","permalink":"/2022/05/23/quickrun-spatial-analysis-with-postgis-geopandas/","title":"速通PostGIS地理数据分析"},{"content":"前言 因为一些缘故，接触了一下对空间地理数据的分析工作。忙完以后，想了想决定写下这篇文章，方便日后如有需要，可以快速查阅，也供后人参考。\n背景条件 在我的分析过程中，需要使用PostgreSQL，但我之前用MySQL/MariaDB比较多，稍微花了一点点时间适应。怕下次忘了，干脆写出来。\n本文还假设你已经具备了最基本的SQL，尤其是MySQL知识。有了这些基础知识，我们的适应就会非常迅速。\nPostgreSQL速通 我们会着重介绍几个概念。\n数据库实例 - Cluster 和我们平时常说的计算集群(Computer Cluster)不同，也和SQL中的聚合索引(Clustered Index)不同，PostgreSQL中的Cluster，其实可以简单理解成一个数据库实例。\n具体来说，同一套PostgreSQL的二进制文件，可以运行多个实例(Cluster)，每个实例具有不同的数据目录，配置文件，监听不同的端口。\n而如果想要创建一个Cluster，使用initdb命令是最简单的方法。其手册可以参考https://www.postgresql.org/docs/current/app-initdb.html\n也可以参考这里https://www.postgresql.org/docs/current/creating-cluster.html\n数据库初始化 大部分情况下，包括Windows安装包安装，以及Debian/Ubuntu APT源中的安装包安装，都会自动为我们创建好一个可以直接使用的Cluster，我们一般不需要手动创建一个Cluster。\n不过，如果在Windows上使用Zip压缩包进行安装时，需要运行initdb.exe，指定一个数据目录，创建一个新的Cluster。毕竟没有程序自动地帮你做这件事。\n用户与权限 在较新版本(\u0026gt;=8.1)的PostgreSQL中，用户和组都统称为角色(ROLE)。在我看来，区别用户和组的最大的区别，或许在于，是否给一个角色赋予登录权限。\n在Debian/Ubuntu系统下，当从APT源安装PostgreSQL时，会在系统中自动创建一个postgres用户，这个用户可以通过Unix Socket访问到PostgreSQL默认Cluster中创建的同名超级用户。\n1 2 3 4 5 root@MyPC:~# sudo -u postgres -i psql psql (14.2 (Ubuntu 14.2-1ubuntu1)) Type \u0026#34;help\u0026#34; for help. postgres=# 如果你需要创建其他角色，则需要具有CREATEROLE权限。\n要创建一个可以登录的角色（下面我就简称用户啦），可以使用以下SQL语句:\n1 CREATE ROLE alice LOGIN PASSWORD \u0026#39;12345\u0026#39;; 这会创建一个名为alice的用户，赋予（通过网络）登录权限，密码为12345。我们可以运行psql命令登录到这一用户。\n1 2 3 4 5 6 7 8 9 root@MyPC:~# psql -U alice -W postgres -h localhost Password: psql (14.2 (Ubuntu 14.2-1ubuntu1)) SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) Type \u0026#34;help\u0026#34; for help. postgres=\u0026gt; \\conninfo You are connected to database \u0026#34;postgres\u0026#34; as user \u0026#34;alice\u0026#34; on host \u0026#34;localhost\u0026#34; (address \u0026#34;127.0.0.1\u0026#34;) at port \u0026#34;5432\u0026#34;. SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) 如果你希望赋予这个用户一些权限，例如CREATEDB, CREATEROLE, 甚至是SUPERUSER，都可以在上方指定。例如，我们可以在创建时指定Alice为Superuser\n1 CREATE ROLE alice SUPERUSER LOGIN PASSWORD \u0026#39;12345\u0026#39;; 查看\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 root@MyPC:~# psql -U alice -W postgres -h localhost Password: psql (14.2 (Ubuntu 14.2-1ubuntu1)) SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) Type \u0026#34;help\u0026#34; for help. postgres=# \\du+ List of roles Role name | Attributes | Member of | Description -----------+------------------------------------------------------------+-----------+------------- alice | Superuser | {} | postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {} | postgres=# 但使用SQL语句直接创建用户的记录，会将密码明文存储在数据库的历史查询中。我们觉得这样不好。\nPostgreSQL提供了许多命令行工具，即便不用显式登录到psql，也可以管理数据库。createuser便是其中用于创建角色的工具。其文档可以参考https://www.postgresql.org/docs/current/sql-createrole.html\n基于此，我们可以将上面的创建用户代码，替换成Shell指令: createuser -l -s -e -P alice\n1 2 3 4 5 postgres@MyPC:~$ createuser -l -s -e -P alice Enter password for new role: Enter it again: SELECT pg_catalog.set_config(\u0026#39;search_path\u0026#39;, \u0026#39;\u0026#39;, false); CREATE ROLE alice PASSWORD \u0026#39;SCRAM-SHA-256$4096:91DfHk6BEMX4Ay7S8TsfQw==$2tGBIh4PC/ce4ThLajfvm9ZwwVzidWCorlsHuHBKhX8=:0HZpvT5kReucYmGGwKR6I1OLsaZo3aOjcSMlBI7vrfQ=\u0026#39; SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN; 这一串命令中，\n-l代表赋予LOGIN登录权限 -s代表赋予SUPERUSER超级用户身份 -e指示该命令将最终发送给PostgreSQL服务器的SQL语句打印出来（最后2行） -P指示命令将提示输入该新用户的密码 可以看到，我们发送给数据库服务器的SQL命令，已经将密码哈希处理了。\n当然如果你觉得这一串参数记忆复杂，也可以直接使用--interactive选项，回答程序提出的一系列问题，即可创建用户。\n数据库 \u0026amp; 模式 \u0026amp; 数据表 这三个词分别对应着:\n数据库 - Database 模式 - Schema 数据表 - Table 数据表很好理解。但前两个常常带来争议。\nMySQL的Schema 在MySQL中，我们可以在MySQL :: MySQL 8.0 Reference Manual :: MySQL Glossary中看到这一段话:\nIn MySQL, physically, a schema is synonymous with a database. You can substitute the keyword SCHEMA instead of DATABASE in MySQL SQL syntax, for example using CREATE SCHEMA instead of CREATE DATABASE.\nSome other database products draw a distinction. For example, in the Oracle Database product, a schema represents only a part of a database: the tables and other objects owned by a single user.\n简单来说，在MySQL中，Database 等价于 Schema.\nPostgreSQL的Schema 但PostgreSQL不完全这么想。在PostgreSQL: Documentation: 14: 5.9. Schemas中，是这么说的:\nA PostgreSQL database cluster contains one or more named databases. Roles and a few other object types are shared across the entire cluster. A client connection to the server can only access data in a single database, the one specified in the connection request.\nA database contains one or more named schemas, which in turn contain tables. Schemas also contain other kinds of named objects, including data types, functions, and operators. The same object name can be used in different schemas without conflict; for example, both schema1 and myschema can contain tables named mytable. Unlike databases, schemas are not rigidly separated: a user can access objects in any of the schemas in the database they are connected to, if they have privileges to do so.\n简单来说，在PostgreSQL中，一个实例(Cluster)可以包含多个数据库(Database)，一个数据库(Database)可以包含多个模式(Schema)，一个模式(Schema)下面有很多表(Table)，数据就存储在表(Table)中。\nPostgreSQL中的默认Schema 在PostgreSQL中创建一个数据库时，该数据库会默认包含一个public模式。对在该数据库上执行的所有SQL语句来说，如果没有显式指定模式名称，则都会被认为是对public的操作。\n假设数据库中存在一个名为public的模式，其下有一个mytable表，则以下两个SQL语句是等效的。\n1 SELECT * FROM mytable; 或者\n1 SELECT * FROM public.mytable; 跨库查询 两个数据库在组织这些术语时的差异，就进一步导出了一个，非常明显，但仔细思考之下显得比较有道理的限制条件：MySQL允许跨数据库查询，而PostgreSQL不允许跨数据库查询。\n更进一步，PostgreSQL虽然不允许跨数据库(Database)查询，但允许跨模式(Schema)查询。\n或许是因为，PostgreSQL认为，不同数据库存储的数据，涉及到的业务内容相互间应该是没有紧密关联的（如果有紧密联系，就应该放在同一个数据库内！）\n因此，PostgreSQL选择，从逻辑上将数据库彼此隔离。\n或许，只要我们把跨数据库查询(Cross-Database Query)，改名为跨模式查询(Cross-Schema Query)，或许就变成这两个数据库都能支持的操作了呢？\n构建数据库的思考 紧接而来的问题是，在PostgreSQL中，我们应该如何选择数据库的构建形式？\n这样的问题在应用程序不涉及跨Schema查询时会更令人举棋不定，而跨Schema查询在构建微服务时从来都不是一个建议的选择。\n现在考虑这两种构建形式:\n第一种，大致如下图所示。每个数据库中只有一个Schema，即public。使用多个数据库存储不同业务模块数据，每个数据库彼此分离。\n1 2 3 4 5 6 7 8 9 10 11 12 - PostgreSQL |---- mydatabase1 | |---- public | |---- mytable1 | |---- mytable1 | |---- mytable3 | |---- mydatabase2 | |---- public | |---- mytable4 | |---- mytable5 | |---- mytable6 第二种，大致如下图所示。每个数据库中使用多个Schema，尽可能减少PostgreSQL服务器下，Database的数量。\n1 2 3 4 5 6 7 8 9 10 11 - PostgreSQL |---- mydatabase1 |---- myschema1 | |---- mytable1 | |---- mytable1 | |---- mytable3 | |---- myschema2 |---- mytable4 |---- mytable5 |---- mytable6 这两种方案从实现角度都是可行，现成的。第一种方案相较于第二种方案，失去了跨库查询的能力，但提高了隔离性。\n这时候一定有人问：性能如何？\n目前似乎在网上暂时搜不到这两种方案性能差异的对比。StackOverflow上2009年的帖子里，有人曾经引用Heroku PostgreSQL上的文档，认为 一个数据库内多个Schema(One Database - Many Schema) 的方案会严重拖累服务器性能，但似乎目前在该文档中已经找不到这一段文字，且实际上也没有给出两个方案具体的性能差异。\n因此我更倾向于认为，两者在性能上不会有天差地别的影响。\n📝 备注 当数据越来越多时候，数据库的效率会自然下降。或许考虑其他方法，如分表，索引等手段加快性能，收益会更为明显。\n至于选择，我个人认为，第一种方案，即 One Database - One Schema 会更方便我们管理数据库的权限，我更倾向采用这种方案。\n命名规则 虽然PostgreSQL的命名规则并不离经叛道，但我个人并不建议在数据库名、模式名、表名、字段名中使用大写字母！\n因为，在PostgreSQL中，如果上述名字含有大写字母，需要将这一部分用双引号包起来！\n📌 重要 逃课方法: 使用GUI数据库管理工具，如DBeaver、Beekeeper Studio等。 可以直接无视此部分问题。生成SQL语句复制粘贴即可。\n例如，如果我们创建一个数据表如下所示。注意到Name字段包含了大写字母。\n1 2 3 4 5 CREATE TABLE public.mytb1 ( id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY, \u0026#34;Name\u0026#34; varchar NULL, CONSTRAINT mytb1_pk PRIMARY KEY (id) ); 插入一些数据\n1 2 INSERT INTO public.mytb1 (\u0026#34;Name\u0026#34;) VALUES(\u0026#39;Bob\u0026#39;); INSERT INTO public.mytb1 (\u0026#34;Name\u0026#34;) VALUES(\u0026#39;Cindy\u0026#39;); 执行简单的查询\n1 2 3 4 5 6 mytest1=# SELECT * FROM mytb1; id | Name ----+------- 1 | Bob 2 | Cindy (2 rows) 如果我们在语句中，包含对Name的操作但没有将其用双引号包围，会出现错误\n1 2 3 4 5 mytest1=# SELECT * FROM mytb1 WHERE Name = \u0026#39;Cindy\u0026#39;; ERROR: column \u0026#34;name\u0026#34; does not exist LINE 1: SELECT * FROM mytb1 WHERE Name = \u0026#39;Cindy\u0026#39;; ^ HINT: Perhaps you meant to reference the column \u0026#34;mytb1.Name\u0026#34;. 稍作修改，给Name加上双引号，就可以正常工作\n1 2 3 4 5 mytest1=# SELECT * FROM mytb1 WHERE \u0026#34;Name\u0026#34; = \u0026#39;Cindy\u0026#39;; id | Name ----+------- 2 | Cindy (1 row) 如果你在查询时，总是找不到对应的关系，不妨检查一下有没有遗漏双引号。\n数据类型 PostgreSQL的数据类型也没啥特别的。不过，根据SQL规范，整型的名称默认应该是integer，因此从MySQL迁移过来时需要稍微注意。\n另外值得一提的还有自动增长类型的功能。在PostgreSQL中，自动增长常用Identity指代。注意，另有一种实现方法是通过serial实现，但这似乎是旧的方法，推荐采用Identity的方法。\nIdentity 有两种模式，即BY DEFAULT和ALWAYS。它们的区别在PostgreSQL: Documentation: 14: CREATE TABLE中有说明:\nThe clauses ALWAYS and BY DEFAULT determine how explicitly user-specified values are handled in INSERT and UPDATE commands.\nIn an INSERT command, if ALWAYS is selected, a user-specified value is only accepted if the INSERT statement specifies OVERRIDING SYSTEM VALUE. If BY DEFAULT is selected, then the user-specified value takes precedence. See INSERT for details. (In the COPY command, user-specified values are always used regardless of this setting.)\nIn an UPDATE command, if ALWAYS is selected, any update of the column to any value other than DEFAULT will be rejected. If BY DEFAULT is selected, the column can be updated normally. (There is no OVERRIDING clause for the UPDATE command.)\n省流: ALWAYS在接受用户对数据表中，该自增序列作插入或修改时，条件比BY DEFAULT更严苛。\n⚠️ 警告 BY DEFAULT比较听用户的话，ALWAYS有自己的执着。\n如果你之前有使用其他SQL的经验，上面的内容足够让你开心地在PostgreSQL上执行一系列查询了。\n","date":"2022-05-23T12:09:16+08:00","permalink":"/2022/05/23/quickrun-postgresql-from-mysql/","title":"在MySQL基础上速通PostgreSQL"},{"content":"前言 想必各位对ASP.NET Core容器化部署，特别是微软非常喜欢强调的 微服务(Micro-Service) 概念有一定的兴趣。\n这一过程需要使用到ASP.NET Core, Docker, Nginx三个组件，要让它们相互良好地配合起来，还是需要精心编写一下配置文件的。\n然而，当真的开始着手实操的时候，假如阅读微软文档给出的一些文章或者链接，例如:\nHosting ASP.NET Core image in container using docker compose with HTTPS | Microsoft Docs Docker images for ASP.NET Core | Microsoft Docs Configure ASP.NET Core to work with proxy servers and load balancers | Microsoft Docs Host ASP.NET Core on Linux with Nginx | Microsoft Docs ASP.NET Core Runtime by Microsoft | Docker Hub 或许对于幼儿园的小朋友来说，这些文章可能显得比较幼稚。但对我这种饭来张口衣来伸手的大学生来说，这些文章只能说刚刚好。把文章拼起来就是需要的答案，可惜在尝试的过程中会显得有些不适。所以不妨总结一下，如何将一个ASP.NET Core Web应用部署到Docker中，然后用Nginx做反向代理进行访问。\n准备 已经开发好的ASP.NET Core Web应用程序(Razor Pages, Blazor, MVC, Web API啥的，都可以) Docker Nginx 这里，我们会使用最新的.NET 6.0进行部署。\nASP.NET Core代码的适配 在本教程中，我们由于需要使用Nginx配置反向代理，这需要我们对ASP.NET Core应用进行响应的修改。\n关闭HTTPS重定向 ASP.NET Core项目默认会重定向所有HTTP请求到HTTPS，但在反向代理的场景下，HTTPS不是容器中的ASP.NET Core程序该操心的事情。我们应该在Nginx这样的Web服务器部分完成HTTPS相关的配置，包括HTTP重定向HTTPS。\n打开Program.cs，找到如下代码:\n1 app.UseHttpsRedirection(); 将app.UseHttpsRedirection()一句删除或注释掉，关闭HTTPS重定向。\n设置反向代理使用的响应标头 为了能正确跟踪请求的来源，处理重定向、认证等关键的事件，我们还需要配置转发响应标头。\n同样是在Program.cs中，我们在其他中间件之前，加入UseForwardedHeaders代码段:\n1 2 3 4 5 6 7 8 9 10 ...... var app = builder.Build(); + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto + }); if (app.Environment.IsDevelopment()) ...... 关于中间件先后顺序的讨论，可以参考ASP.NET Core Middleware | Microsoft Docs.\n配置步骤 第1步-创建文件夹 创建一个新文件夹，用来存放构建镜像所需的文件，包括Dockerfile等。我们假设这个新文件夹名为myapp。\n第2步-发布ASP.NET Core应用 这一步我们要将编写的ASP.NET Core应用发布，生成生产环境下的二进制代码。\n打开终端，切换到项目根目录，运行:\n1 dotnet publish -c Release -o publish --no-restore 其中-o publish的意思是将发布的二进制代码放入名为publish的文件夹中。发布完成后，进入该文件夹，可以找到与当前项目同名的一个.dll文件，这就是你的项目编译生成的二进制代码。我们这里假设该文件名为MyWebApp.dll。\n发布完成后，将publish文件夹复制到myapp文件夹中。\n第3步-编写Dockerfile 在myapp文件夹中，新建一个Dockerfile文件，编写内容:\n1 2 3 4 FROM mcr.microsoft.com/dotnet/aspnet:6.0 WORKDIR /app COPY publish statics ./ ENTRYPOINT [\u0026#34;dotnet\u0026#34;, \u0026#34;MyWebApp.dll\u0026#34;] 依次解释上述语句含义:\nFROM mcr.microsoft.com/dotnet/aspnet:6.0指定了基础的镜像文件，我们选用的是微软发布的最新ASP.NET Core Runtime镜像，不包含SDK。 WORKDIR /app指定接下来容器中的操作，基础目录是/app。 COPY publish statics ./会将 本机publish文件夹下的所有文件 复制到 容器当前目录下。 ENTRYPOINT [\u0026quot;dotnet\u0026quot;, \u0026quot;MyWebApp.dll\u0026quot;]放在最后，容器将执行这一命令，即通过dotnet命令运行上一行语句中复制到/app目录下的MyWebApp.dll文件。这将会保持该Web程序一直运行，直到被要求退出。 第4步-构建并运行Docker镜像 在myapp文件夹中，运行命令:\n1 docker build -t mywebapp . -t mywebapp指定了生成镜像的标签(tag)为mywebapp，当然你也可以写成mywebapp:0.1.2这样的形式来指定一个版本号。\n运行命令，Docker会拉取最新的ASP.NET Core Runtime镜像，然后执行我们在Dockerfile中指定的操作，构建成一个新的镜像，放在系统中。\n接下来我们就可以启动我们的应用了。执行命令:\n1 docker run -d -p 12345:80 --name mywebapp-123 mywebapp 参数的解释:\n-d表示容器在后台运行(daemon) -p 12345:80表示将主机的12345端口映射到容器的80端口上，即可以通过主机的12345端口直接访问容器内ASP.NET Core应用。由于我们需要设置反向代理，出于安全考虑，我们可以设置只映射来自本机的访问，即写成-p 127.0.0.1:12345:80这样的形式，此时即便没有防火墙，外界直接访问12345端口也无法访问到我们的ASP.NET Core应用程序。 --name mywebapp-123，给你的容器起一个名字。 mywebapp，选择运行容器使用的镜像Tag。（和上面容器名字可以相同，也可以不同，但这个名字来源于上面构建Docker镜像中指定的Tag，如果设置了版本号，还应当指明版本号。） 如果没有意外情况，执行上述命令后，我们的应用就运行在机器上了。可以通过docker ps查看正在运行的容器，看我们刚刚建立的容器是否正常运行。\n第5步-设置反向代理 在现代的网站中，我们不是很喜欢通过端口号访问网站，更多情况下，我们需要借助如Nginx等Web服务器的反向代理等功能，完成更复杂的工作。因此，我们现在看看怎么使用Nginx为我们的应用设置反向代理。\n⚠️ 警告 再次确认\n执行本步骤前，请确保容器中运行的程序已经按照 ASP.NET Core代码的适配 部分修改并生成。\n这里，我们使用Debian/Ubuntu系Linux发行版的配置文件目录习惯，如果使用的是Fedora/RedHat系发行版，需要自行确定下Nginx配置文件的目录，并创建对应的文件。\n我们在/etc/nginx/sites-available/下创建一个新的站点配置文件，可以起名为myapp.conf。编写如下内容:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 server { listen 80; listen [::]:80; server_name myapp.example.com; location / { proxy_pass http://127.0.0.1:12345; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } 其他部分大同小异，我们主要需要注意的是以下几点。\n如果你的服务器上还运行了其他域名的应用，请确认server_name后正确指定了需要转发给ASP.NET Core应用程序的域名，本例中为myapp.example.com proxy_pass后需要正确指定 第4步-构建并运行docker镜像 中映射到容器80端口的本机端口号。 (可选)第6步-设置HTTPS重定向 现在绝大部分网站都已经使用了HTTPS，我们为了安全理应同样使用HTTPS。当然，这里我们不讨论如何获得HTTPS证书，我们稍微提一提怎么做重定向。\n将第五步中的Nginx配置文件修改为:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name myapp.example.com; ssl_certificate /etc/ssl/certs/myapp.crt; ssl_certificate_key /etc/ssl/private/myapp.key; ssl_dhparam /etc/ssl/certs/myapp.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers \u0026#34;EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4\u0026#34;; ssl_ecdh_curve secp384r1; ssl_prefer_server_ciphers on; ssl_session_timeout 10m; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection \u0026#34;1; mode=block\u0026#34;; location / { proxy_pass http://127.0.0.1:12345; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } server { listen 80; listen [::]:80; server_name myapp.example.com; root /var/www/html; if ($host = myapp.example.com) { return 301 https://$host$request_uri; } return 404; } 上述Nginx的TLS配置仅供参考。我们选取了一个相对较为安全的配置。\n第7步-启动Nginx并测试访问 执行命令:\n1 nginx -s reload Nginx将会重新读取配置文件，设置好DNS解析（如果使用了域名的话），就可以通过绑定的域名访问到容器中的ASP.NET Core Web应用了。\n","date":"2022-05-06T15:21:23+08:00","permalink":"/2022/05/06/deploy-asp-net-core-application-to-docker-container-with-dockerfile/","title":"利用Dockerfile将ASP.NET Core Web应用部署到Docker容器"},{"content":"前言 叔叔我啊，最讨厌不能赚钱的东西了。\n今年不知道哪个版本号更新后，Windows应用商店里的B站客户端下载的视频即便设置导出为MP4文件，也无法直接用通用的视频播放器播放，这实在是有失体统。偌大一个公司开发的软件，怎么能“写错”这么基本的功能呢？\n问题分析 MP4文件头为三个00字节，这是公开的标准。我们将从B站上随意下载的一个视频放到十六进制编辑器中，可以看到\n1 2 3 4 5 6 7 Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00000000 FF FF FF 00 00 00 20 66 74 79 70 69 73 6F 6D 00 ...... ftypisom. 00000010 00 02 00 69 73 6F 6D 69 73 6F 32 61 76 63 31 6D ...isomiso2avc1m 00000020 70 34 31 00 00 00 08 66 72 65 65 04 2B B2 37 6D p41....free.+.7m 00000030 64 61 74 00 00 00 5C 06 05 58 B3 E1 63 30 8C 3C dat...\\..X..c0.\u0026lt; 00000040 9E 4F C2 39 81 09 7E AA A5 2E 20 42 49 4C 49 41 .O.9..~... BILIA 后面都是很正常的MP4文件内容，唯独在文件前端，多出了三个FF字节，这就是导致下载的视频文件无法被通用视频播放器播放的原因。我想肯定是程序员不小心写出来的bug啦，才不是叔叔想要赚钱呢！\n解决方法 那么接下来，我们就帮粗心的程序员修复一下下载的文件，让它们能被正常播放吧！\n如果电脑上有Linux环境(WSL也可以)，那就是再方便不过的事情啦。只需要执行如下命令:\n1 tail -c +4 YourLoveYourLife.mp4 \u0026gt; LemonRipe.mp4 其中，tail读取文件内容并输出到标准输出中。-c +NUM参数，代表从NUM位字节开始读取文件内容，如上面的-c +4表示从第4个字节开始读取文件(跳过前3个FF字节)，YourLoveYourLife.mp4是你使用Windows哔哩哔哩客户端下载的视频文件路径，LemonRipe.mp4是最终你生成的可以用第三方播放器播放的视频文件路径。使用\u0026gt;进行输出重定向。\n","date":"2022-05-05T20:02:32+08:00","permalink":"/2022/05/05/fix-windows-store-bilibili-mp4-files/","title":"修复Windows10应用商店内哔哩哔哩应用下载MP4文件无法播放问题"},{"content":"前言 前几天在咸鱼上买到了一块龙芯的电脑主板，CPU型号是龙芯(Loongson) 3A3000，芯片组似乎是780E。回来装系统的时候遇到了不少麻烦，这里记录一下，如果能帮到后来者，那自然是最好不过。\n系统选择 实际上，我仍然推荐有条件的用户，使用龙芯官方适配的操作系统Loongnix，因为这是龙芯优化过的系统。\n只不过，因为mips64el架构上的Loongnix是基于Fedora发行版的，且版本古老。我个人习惯的环境是Debian，只有LoongArch上的Loongnix才是基于Debian制作的。况且，Loongnix默认带一个GUI，连服务器版都带，我不是很喜欢先装后卸，因此我还是希望安装一个相对干净的Debian系统上去。\n龙芯3A3000基于mips架构，具体到Debian上，属于mips64el架构（MIPS 64-bit little-endian mode, MIPS 64位 小端序），请记住这个代号，因为我们后面会反复用到。\n关于Debian的系统代号选择，在本文编写时，最新的Debain系统是Debian 11 Bullseye，使用的系统内核是Linux 5.10. 但我们目前会使用上一个版本，也就是Debain 10 Buster，具体原因下面会说明。\n下面我们讨论的方法，基于狗剩百科:Debian 系发行版安装指南，并做一些自己经验的额外补充。\n条件限制和实现方式 安装方式的选择 在平时x86计算机上，安装Debian的方法，首选当然是直接从Debian CD源中下载安装镜像，将镜像刻录到U盘上，引导镜像中的安装程序完成安装。这样当然是最简单的。\n然而，根据我的简单测试，似乎Debian CD源中的ISO镜像，并不能顺利地在我手头上这块主板中引导。\n📝 失败的Debian安装程序引导测试 这一段是我尝试在龙芯上引导Debian安装镜像时的失败操作，或许是该镜像不具备在龙芯电脑上引导的能力，也有可能是我本身操作方法不正确。因此我将这一过程记录在此，如有谬误，还请批评指正。如果您希望参考本文进行Debian系统的安装，请略过这一段。\n下载Debian CD中的安装镜像debian-11.2.0-mips64el-netinst.iso 使用dd命令，将其刻录到U盘上: dd if=debian-11.2.0-mips64el-netinst.iso of=/dev/sdb bs=8M，（其中/dev/sdb是我的U盘设备） 将U盘插入龙芯电脑，启动，提示找不到LiveCD中的一个文件。 根据上述表现，个人猜测电脑已经成功读取并加载了U盘上的引导程序，但不知为何无法加载U盘上的后续文件，无法进入安装程序。\n此外，目前我个人还暂时无法确定这块龙芯主板究竟是BIOS/CSM引导模式还是EFI引导模式。BIOS设定中无对应选项可调整。\n在龙芯提供的Loongnix镜像中，存在一个EFI文件夹，且引导该Loongnix镜像时，在GRUB Shell中执行echo $grub_platform，回显结果为efi。且在Loongnix LiveCD系统中，grub-install命令提示安装的平台为MIPS下的EFI平台。\n但进入该LiveCD系统后，使用Loongnix的安装程序尝试安装系统时，如果磁盘为GPT分区表，会提示当前计算机运行在BIOS/CSM模式下，需要创建额外分区才能继续安装。采用MS-DOS分区表则无此问题，安装时不需要创建ESP分区也可完成安装并正常引导本地磁盘系统。这也是我电脑目前的状态。\n因此，我已经无法区分这台龙芯电脑运行在何种模式下，需要进一步考察。如果您有什么看法或观点，欢迎通过邮件同我交流，我的电子邮件地址在站内可找到。\n有些难办的是，mips64el的Debian源，没有GRUB2软件包提供！ 这就意味着，我们需要借助其他方法，安装GRUB引导器 —— 如果没有GRUB引导器，我们是无法引导我们的系统的！\n因此，我们需要另辟蹊径，分两步安装Debian系统：\n安装龙芯的Loongnix系统到本地硬盘上。我选用的镜像是loongnix-20180930.iso。这一步骤主要目的是借助官方的安装程序，完成分区并将GRUB与Linux内核安装到本地硬盘。 重新进入LiveCD系统，删除本地硬盘中原有的根目录结构 （但需要保留/boot目录下的所有内容！因此建议在 第一步安装Loongnix系统时，将/boot作为一个独立的分区，空间大约1GB） 使用debootstrap工具，创建Debian根文件系统，并在此基础上，进行修改。更多相关的信息可以阅读通过 Unix/Linux 系统来安装 Debian GNU/Linux。 总体而言，我们相当于借用了Loongnix系统提供的内核引导Debian系统。大部分情况下，这种方法没有问题，但也有一些情况下，系统中的部分程序要求Linux内核版本号高于一个指定版本。此时需要自行斟酌是否升级，升级到何版本。本文后面会讨论这一问题。\nDebian大版本号的选择 不同Debian大版本号下，对应的Linux内核版本号也不同。原则上讲，我们使用的内核版本号应该接近Debian官方指定的内核版本号。在Debian软件源中，龙芯3代CPU使用的内核包名为linux-image-loongson-3。\nDebian版本号 Debian代号 软件源中使用的内核版本号 9 stretch 4.9 10 buster 4.19 11 bullseye 5.10 此外，北京龙芯也有提供一些预编译的Debian格式内核包：https://mirrors.tuna.tsinghua.edu.cn/bjlx/pool/main/l/，我目前使用的是其提供的linux-4.19.0-loongson-3下的包。暂时不清楚北京龙芯提供的内核包相对于Debian提供的包是否有特别的优化，或其他区别。\n选用Linux 4.19内核的前提下，Debian 10就成为了适配的最佳选择。\n过段时间我会尝试下是否能直接用Debian 11 + Linux 5.10，届时会更新后续。\n📝 备注 后续1: 2022年4月11日下午\n根据我的简单测试，Debian维护者打包的Linux 5.10内核，虽然可以正常工作，但似乎和我手上这块780E主板有点不相兼容。\n具体表现为，开机后风扇非常嘈杂，怀疑是有一部分功能没能成功移交给操作系统。过段时间再试试看吧。\n安装流程 准备 🚨 注意 备份文件警告！\n请务必在执行后续操作前，备份目标计算机上的所有重要文件！本人不会对您任何操作/误操作造成的损失负责！\n一个空白U盘，用于刻录LiveCD镜像 Loongnix LiveCD镜像，我选用的版本为loongnix-20180930.iso（实际上这个版本有些不好用，因为其基于Fedora 21构建，软件包版本非常老，如果有新版本的Loongnix LiveCD在你的电脑上可用，完全可以替换）。 有效的网络连接（这要求第二步中的镜像可以正常驱动网卡） 同时，下面的操作几乎都是在root权限中完成的。\n刻录LiveCD镜像并引导 我想这一步会是大伙比较熟悉的了。\n1 dd if=loongnix-20180930.iso of=/dev/sdX bs=8M of=/dev/sdX部分，请替换成你的U盘实际设备块，如/dev/sdb。不清楚的可以使用fdisk -l查看。\n写入完成后，将U盘插到龙芯电脑上，开机并从U盘引导。\n安装debootstrap 进入LiveCD环境后，首先配置好网络。接着安装debootstrap。可以使用命令dnf install debootstrap从Fedora源中下载安装。如果源中的工具太老不方便，也可以直接从Debian软件源中下载debootstrap的.deb包，使用tar与ar将其解压即可使用该程序（在Loongnix/Fedora上也可以！）\n挂载目标分区 接下来，随便在根目录下创建一个新路径，例如/deb，用于挂载我们的本地磁盘分区。\n接着，将本地磁盘上，已经安装有Loongnix的根目录分区，挂载到挂载点上。但此时先不需要挂载/boot分区。如果真的需要挂载boot，请在删除时选择忽略/boot下的所有文件！\n挂载之后，删除该分区下的所有文件\n下面的例子，我们假设本地磁盘上，/dev/sda1是/boot分区，/dev/sda2是/分区，/dev/sda3是swap分区。\n1 2 3 mkdir /deb mount /dev/sda2 /deb rm -rf /deb/* 执行安装程序 接下来，我们要用debootstrap创建一个Debian根文件系统。\n1 2 debootstrap --arch mips64el buster \\ /deb http://ftp.cn.debian.org/debian 最后的地址可以替换成你所在位置速度最快的Debian镜像地址。\n如果debootstrap提示找不到一个名称中包含buster的配置文件，可能是当前使用的debootstrap工具版本太老（常见于使用Fedora源安装）。一种解决方法是下载新版的debootstrap工具，另一种是直接使用Debian的相对代号，如在本文编写时，Debian 10 Buster是上一个稳定版，则可以使用oldstable替代buster。同理，如果使用旧版的debootstrap安装Debian 11 Bullseye报错，也可以使用stable替代bullseye。\n坐下来，等一等，喝杯咖啡，根文件系统就创建好了。\n基础系统配置 为了让系统能跑起来，单有个根文件系统是完全不够的。\n复制内核模块 虽然这么做不知道是否合适，可以考虑试试。\n将Loongnix的内核模块，复制到目标Debian系统中。\n1 cp -r /lib/modules /deb/lib/ fstab文件 /etc/fstab文件指定了系统启动时挂载文件目录的规则，需要仔细配置。\n编写该文件，可以自己亲历亲为，仔细编写即可。但懒人有懒人的方法，不喜欢自己写这种配置文件，怕出错的话，也完全可以用ArchLinux团队提供的genfstab脚本，自动生成配置文件。方法也非常简单，可以去人家Github主页GitHub - archlinux/arch-install-scripts: Useful scripts for installing Arch Linux 直接下载源码包，make一下就好。或者，现在包括Debian在内的发行版仓库也包含了这个脚本的可执行文件，下载解压运行一气呵成。\n使用genfstab，需要在LiveCD操作系统中运行，且需要在运行前将/boot分区挂载到Debian系统的/boot上，物归原主。如果有swap分区的话，也需要确认已经启用。\n1 mount /dev/sda1 /deb/boot 确认无误后，就可以使用下面的命令生成fstab文件了。\n1 genfstab -U /deb \u0026gt; /deb/etc/fstab -U参数是指定使用UUID作为分区识别的标准。/deb是Debian根文件系统的挂载点，最后的重定向是将输出写入到目标系统的/etc/fstab中。\n建议生成后自行检查一下文件是否符合预期。\nchroot环境 还需要chroot进入Debian系统，对其进行一些配置。\n1 LANG=C.UTF-8 chroot /deb /bin/bash 设置root口令 1 passwd root 安装常用软件包 或许下列软件包会提供一些提示，具体是否需要可以看您的需求。\nlocales: 使用英语以外的语言必须的软件包 vim/emacs/nano: 文本编辑器 openssh-server: SSH服务器，使用SSH登录系统的必须 build-essential: Debian下用于构建软件包的套件，包含了gcc/g++/make等工具 network-manager: Network Manager工具，可以自动配置网络 安装这些软件包只要用apt工具即可。如：\n1 apt install locales vim build-essential 网络配置 网络配置是一件烦人的事情，人生或许应该用在更有意义的事情上。因此我个人喜欢用Netowork Manager自动接管网络配置。\n1 apt install network-manager 还有什么比开机就能看到有线网络已连接，无线网络只需要一行命令就可连接，更令人开心的事情吗？\n当然，如果你喜欢其他方法，如直接编辑/etc/network/interfaces，那也可以参考对应的教程。\n此外，我们还需要配置hostname，也就是本机的主机名。\n1 echo myloong \u0026gt; /etc/hostname 完成 我想上述配置已经可以让Debain系统跑起来了。退出chroot环境，重启电脑，记得拔掉U盘，没有意外的话，就可以进入Debian系统了。\n如果电脑引导卡在Loongnix字样的引导动画，不妨尝试按下 Ctrl + Alt + F2 （F2~F8都可以试试），看能否见到熟悉的登录界面。\n更换内核 如果你阅读到这里，看起来你已经进入了Debian系统，那么先恭喜你，你的Debian系统已经可以使用了！\n但问题随之而来，如果你使用的Loongnix镜像和我相同，那你一定会发现，Loongnix使用的内核版本较老，可能是3.10附近的版本。而即便使用Fedora 28，其内核版本号也约为4.4~4.9左右的版本，和Debian官方的内核版本号是不匹配的。对于强迫症来说，这样的情况不太爽， 我们得想想办法替换内核！\n备份 然而，这样的操作注定是危险的，经常失败的，我想在进行下述操作前，需要先将系统备份一次。毕竟从debootstrap安装系统的流程终究繁琐，值得创建一个小小的备份帮助我们快速恢复。\n具体备份的方法就不展开了。数据备份的责任在各位操作者本身！\n安装新内核 安装新版本内核的方法有许多，Debian官方软件源中的会是一个选择。不过既然我没用过，那我也不具体展开介绍了。反正安装方法极为简单，apt install linux-image-loongson-3 linux-headers-loongson-3 linux-libc-dev就可以了。\n我们接下来介绍怎么安装北京龙芯提供的4.19.0内核包。\n下载地址位于https://mirrors.tuna.tsinghua.edu.cn/bjlx/pool/main/l/linux-4.19.0-loongson-3/，需要下载该地址下的三个文件：\nlinux-headers-4.19.0-loongson-3_4.19.124-lemote-20200525_mips64el.deb linux-image-4.19.0-loongson-3_4.19.124-lemote-20200525_mips64el.deb linux-libc-dev_4.19.124-lemote-20200525_mips64el.deb 可以考虑将三个文件下载到同一个文件夹中。随后进入该文件夹，执行\n1 dpkg -i *.deb 即可安装三个软件包。\n更新initramfs 首先安装initramfs-tools软件包\n1 apt install initramfs-tools 执行下述命令，更新initramfs\n1 update-initramfs -k 4.19.0-loongson-3 -u -v 这会在/boot下生成一个initrd.img-4.19.0-loongson-3文件。我们接下来配置GRUB引导项会使用到这一文件。\n修改GRUB配置文件 最后一步，修改GRUB配置文件。\n📝 失败的GRUB配置生成 这一部分我也不确定最佳做法是什么，因此还请熟悉GRUB的同学批评指正！\n众所周知，在x86平台下，每次更新内核后，只需要使用grub-mkconfig命令，即可自动生成正确的GRUB配置文件。\n尽管Debian MIPS64EL软件源不提供GRUB2软件包，但提供了grub-common软件包，其包含了grub-mkconfig命令。因此我尝试使用该程序为新内核创建引导菜单。就生成的结果看，其生成确实正常。\n但在重启引导时，GRUB提示insmod命令无法找到。\n这是很奇怪的事情，因为无论是原Loongnix生成的grub.cfg还是Debian生成的grub.cfg，都使用了insmod命令。\n目前我尚未弄清楚造成该问题的原因，正在比较两配置文件的差异，还请大家批评指正。\n这里，我们使用的方法是，直接修改原有的/boot/grub.cfg文件，将原先引导的3.x内核文件，改为新安装的4.19内核文件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 menuentry \u0026#39;Loongnix GNU/Linux\u0026#39; --class loongnix --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option \u0026#39;gnulinu$ set root=\u0026#39;hd0,msdos1\u0026#39; if [ x$feature_platform_search_hint = xy ]; then search --no-floppy --fs-uuid --set=root --hint-ieee1275=\u0026#39;ieee1275//disk@0,msdos1\u0026#39; --hint-bios=hd0,msdos1 --hint-efi=hd0,msd$ else search --no-floppy --fs-uuid --set=root b6a3a91c-82b0-4ac6-ba62-19a9d4cf63c7 fi - echo \u0026#39;Loading Linux 3.10.84-20.fc21.loongson.3.mips64el ...\u0026#39; - linux /vmlinuz-3.10.84-20.fc21.loongson.3.mips64el root=/dev/sda2 ro rhgb quiet loglevel=0 LANG=zh_CN.UTF-8 - initrd /initramfs-3.10.84-20.fc21.loongson.3.mips64el.img + echo \u0026#39;Loading Linux 4.19.0-loongson-3 ...\u0026#39; + linux /vmlinuz-4.19.0-loongson-3 root=/dev/sda2 ro rhgb quiet loglevel=0 LANG=en_US.UTF-8 + initrd /initrd.img-4.19.0-loongson-3 boot } 编辑后，保存该文件，重启，至少在我的电脑上，这种方法成功替换了系统内核。\n目前我还没探索到更优雅地生成GRUB配置文件的方法，如果您有好的想法或见解，欢迎给我发邮件，敬请批评指正。\n可能的改进方法 这部分主要是我暂时的改进想法，如果您是参考本文安装Debian系统，或许可以跳过这一段落了。\n很明显，两种GRUB配置文件中，有部分insmod命令没有正常执行。\n一种解决方法是，每次安装内核，都通过自己手动修改/boot/grub.cfg文件，达到引导新内核的目的，缺点是不优雅，容易出错。\n另一种解决方法是，我们都知道，GRUB配置文件的生成是基于/etc/grub.d/下的模板文件的，那么可以考虑将Loongnix系统中的模板文件复制到Debian中，即可生成格式接近的配置文件，或许也可以解决问题。\n但目前头疼的问题还有，Debian不提供MIPS64EL架构的GRUB2包，导致必须依赖Loongnix/Fedora安装GRUB主引导记录。我在尝试自行编译GRUB，具体测试情况可能得等下一篇博文更新了。\n尾声 目前经过几天的使用， 龙芯3A3000整体还是超过了我的预期的。基本上，3A3000接近于英特尔J1900水准。据说使用LoongArch架构的3A5000性能上了几个台阶，或许今后有机会可以试试看。\n我会更期待，我们有更多的实干家出现。\n","date":"2022-03-19T10:53:24+08:00","permalink":"/2022/03/19/loongson-first-run-install-debian/","title":"龙芯小试 —— 龙芯3A3000主机简单配置与Debian系统安装"},{"content":"这篇文章将介绍一种方法，使用Windows Subsystem for Linux (WSL), Alpine Linux发行版, Git等工具，配置一个方便易用的的Hexo博客编写系统。\n如果结合已有的网站服务器，还可以使用Git快速进行站点部署与更新。\n前言 如您所见，这个网站的渲染生成器是Hexo。众所周知，Hexo是跑在Node.js上的玩意，但npm环境配置是出了名的复杂，甚至在我看来还不是那么优雅，略显混乱，在Windows上更是如此。\n过去在Node.js上开发项目，总是避免不了和系统里的npm斗智斗勇，几番折腾，最后竟然无法正常地生成Hexo博客。实际上，也并不是没有考虑过切换博客生成后端，但迁移的成本同样存在，而且选择也较为有限。\n不可否认，目前有许多优秀的Node.js管理系统，例如volta之类，但这些工具在Windows下支持也不能算好。而采用Linux作为主桌面？在目前还不能算特别方便。\n所以，是否有一种方法，既能有效地管理npm，从而为Hexo创建一个隔离的环境，又能保证整体性能不差，还与Windows轻松兼容呢？\n笔者认为，在2021年年底的当下，微软提供的Linux子系统，也就是WSL，是解决这一问题的比较优雅方法。它能满足上面的所有设计要求。以下是具体方法。\n预备条件 启用了 Windows Subsystem for Linux 功能的 Windows 10+ 操作系统 Alpine Linux的MINI ROOT FILESYSTEM(最小根文件系统)压缩包，可以在downloads | Alpine Linux找到 其他必须工具，如git等 📝 备注 关于Alpine Linux Alpine Linux是一个非常精简的Linux发行版，本文编写时，其最新版minirootfs的tar.gz包，仅2.6MB大小，可以在为我们提供尽可能的环境隔离同时，大幅度节省隔离环境所需的额外空间开销。\n📝 备注 MINI ROOT FILESYSTEM的作用是? 用于我们手动导入Alpine Linux到WSL中，作为我们的Hexo环境。不过，目前微软商店里已经上架了Alpine WSL应用程序包，和手动导入效果类似，可供选择。如果选择从应用商店安装，请跳过下面导入Alpine Linux一节。\n📝 备注 为什么使用Alpine Linux发行版? Alpine Linux发行版体积比目前绝大多数其他通用的发行版（无论是Debian系还是CentOS系）都要小得多。先前，Alpine由于musl的原因，兼容性不如其他glibc发行版。但后来docker普及之后，许多软件都提供了运行于Alpine Linux的二进制文件。包括Visual Studio Code远程插件，也提供了Alpine Linux的支持。因此，就本文描述的任务而言，选用Alpine Linux是完全没有问题的。\n当然，如果更喜欢使用其他发行版，也是完全没有问题的，操作流程和下文整体上还是相同的。注意一下部分命令的替换就好。\n导入Alpine Linux 解开GZip包 这一步，我们要导入Alpine Linux系统作为一个WSL的发行版(Distro)。\n首先下载Alpine Linux的.tar.gz包，然后用解压工具，解压出.tar包。记住/复制这个.tar包的路径。例如，D:\\alpine-minirootfs-3.15.0-x86_64.tar。\n⚠️ 警告 注意事项 只需要将压缩包的第一层.gz部分解开，得到里面的.tar包即可。不需要解开.tar包里面的文件系统。\n建立目录并导入发行版 在你希望放置这个发行版相关系统文件的地方，创建一个文件夹，这里为了举例子，我们取D:\\AlpineHexoBlog。这个路径不一定需要在C盘，可以在你想的合理的位置。名字也是随意的。\n接着，我们输入下述命令。\n1 wsl --import AlpineHexo D:\\AlpineHexoBlog D:\\alpine-minirootfs-3.15.0-x86_64.tar --version 2 这个命令的意思是，将我们解压得到的.tar包，作为一个Linux发行版，导入一个新的发行版，并将相关文件放置在D:\\AlpineHexoBlog目录下，这个发行版的名称叫AlpineHexo(这个名字可以随意)，并指定使用WSL版本2（关于WSL1和2的差别，请自行参阅微软官方文档）。\n这样，一个自定义的发行版就导入完成了。可以在普通命令行/Powershell里面输入wsl -d AlpineHexo(注意名称替换)进入该发行版的终端。如果电脑装有Windows Terminal，也可以在标签页启动列表中直接选择该发行版。\n安装Hexo相关环境 切换到Alpine Linux终端，执行命令。\n1 apk add nodejs npm git 等待安装完成。\n安装Hexo 这部分的步骤和Hexo官方文档部分相同，因此直接放出命令如下：\n1 npm install -g hexo-cli 创建博客资源主目录 参考Setup | Hexo。\n1 hexo init MyBlog 上面命令中，MyBlog是可以自定义的文件夹名称，这个文件夹用于存储博客网站的源文件，如站点设置，博客文章原始文件等。在生成渲染网站时，hexo程序会根据这一目录里的配置生成静态网站，我们的大部分操作也都将在这个文件夹里完成。本文剩余部分中，这个目录将称为Hexo主文件夹。\n如果输出如下所示，说明创建成功了。\n1 2 3 INFO Cloning hexo-starter https://github.com/hexojs/hexo-starter.git INFO Install dependencies INFO Start blogging with Hexo! 接着进入这个新文件夹，执行npm install。\n1 2 cd MyBlog npm install 这下，我们的站点就初始化完成了。我们可以任意对这个站点进行自定义，如主题自定义，撰写文章等。\n(可选)使用Git管理站点内容历史记录 这就完了？不打算让博客内容管理更加自动化一些吗？比方说版本控制，自动部署等等。我们接下来就来介绍！\n我们首先从使用Git管理站点内容历史记录开始说起。网站内容日新月异，因此有需要将我们的博客站点原始内容，如博客文章的原始Markdown文件，站点的配置文件，Node.js包依赖等，做一些历史备份。\n还可以注意到，Hexo的站点文件，包括配置、文章等，实际上都可以被视为文本文件，因此Git就是负责管理版本历史的极佳选择。\n以下方法不依赖于具体的Git云托管平台，因此不必纠结Github还是Gitee这种问题，原理都是一样的。\n初始化Git仓库 进入Hexo主文件夹，并执行git init，这将在本地端创建一个初始Git仓库。\n1 2 cd MyBlog git init 接着，我们让git跟踪Hexo主文件夹内所有有用的文件，并进行第一次提交(commit)。其中-m参数后的是提交信息，内容任你填写。\n1 2 git add . git commit -m \u0026#34;Init commit.\u0026#34; 推送到在线Git云托管平台 如果希望同步到在线的Git云托管平台，需要先自行在网页中创建一个空仓库，之后推送到该远程仓库中。这里以内网的一台Git服务器为例，创建的仓库地址为http://git.mysrv.in/dhao2001/MyBlog.git\n1 2 git remote add origin http://git.mysrv.in/dhao2001/MyBlog.git git push -u origin main 在另一个地方继续创作 下次，当重装了操作系统，或是更换了电脑，希望在另一个地方恢复相同的博客环境以及源文件，只需要这么做:\n首先还是要搭好最基本的环境，也就是git, nodejs, npm。安装完成后克隆仓库到当前系统。\n1 git clone http://git.mysrv.in/dhao2001/MyBlog.git 进入该文件夹，执行npm install，这将根据Hexo主文件夹里的package.json文件，安装所需的node.js包。\n1 2 cd MyBlog npm install 至此，在新环境中的博客编写环境已经准备就绪，就可以像在老地方一样创作和发布博客了。\n(可选)使用Git进行页面部署 咦，怎么又是Git？上一节不是刚刚才讲过吗？\n刚刚上一节的内容，主要是使用Git对博客的原始配置文件、原始代码、原始Markdown源文件进行管理。\n而本节的内容是，使用Git工具，将本机上通过Hexo命令渲染生成的静态Html等站点资源文件，推送到远程服务器的指定Web目录内，供其他人通过Web服务器进行访问。\n设置远程服务器 第一步，首先需要登录到博客网站主机，也就是部署在公网的Web服务器上。\n接着，在服务器上安装git软件包。例如，如果使用的是Debian/Ubuntu系统，可以运行:\n1 sudo apt install git 如果使用的是其他发行版，请参阅对应发行版的软件包安装手册。\n设置Web服务器 📝 备注 关于Web服务器的选择\n本教程以nginx作为Web服务器，若希望使用其他Web服务器如Apache，请自行调整以下步骤中Web服务器配置文件内容。\n在/var/www下新建一个文件夹，用于存放博客网站的静态Html文件，这里以blog作为文件夹名称示例。\n1 mkdir -p /var/www/blog 接下来修改nginx的站点配置文件(Debian/Ubuntu系统位于/etc/nginx/sites-enabled/下)，修改配置文件中的server块(当然，更建议新建一个server块，更方便区分和管理，详见其他教程)。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 server { listen 80 default_server; listen [::]:80 default_server; root /var/www/blog; index index.html; server_name _; location / { try_files $uri $uri/ =404; } } 修改完配置文件后，重启nginx:\n1 2 3 4 service nginx restart # 使用系统服务管理重启nginx # 如果上面的命令无效 nginx -s reload # 使用nginx命令重新读取配置文件 以上配置足以满足Hexo这样的静态网站部署要求。此时，可以考虑在/var/www/blog/下新建一个测试用Html文件，测试能否正常访问网站。\n设置git仓库用于传输博客网站资源 设置好Web服务器后，如何使用git，把本机生成好的Hexo站点静态文件，推送到远程Web服务器上的网站目录，也就是本例中的/var/www/blog去呢？\n实际上，在服务器和本地机器之间传输文件有很多方法。为了方便，考虑Hexo提供的部署工具集合，我们可以继续使用Git工具完成这项任务。\n使用Git传输网站资源主要分成两个过程：\n第一步，先将本机生成的静态文件，通过Git传输到Web服务器上。 第二步，使用Git钩子，将传输到Web服务器上的网站资源，复制到/var/www/blog，供Web用户访问。 为了达到这一目标，需要按照以下方法，在本机和远程Web服务器上分别调整配置。\n远程Web服务器的配置 首先，在远程Web服务器上，创建一个用户，用户名可以自拟。当然，考虑使用Git的习惯，甚至可以给这个用户起名为git，如本例接下来所示。\n1 adduser -s /bin/git-shell git ⚠️ 警告 关于Git Shell\n出于安全考虑，可以将git用户的默认shell指定为只有简单功能的git-shell，执行命令结束后就会退出。\n注意，/bin/git-shell是Debian/Ubuntu系发行版的默认路径，新版本或其他发行版请务必确认git-shell的具体位置，并将/bin/git-shell替换成真实路径。\n为了配置自动部署，建议使用密钥对进行认证。SSH密钥对可以在本机上使用ssh-keygen或putty等工具生成。\n1 2 3 4 5 6 sudo -u git /bin/bash -i # 以git用户身份进行下列操作 cd ~ mkdir .ssh cd .ssh vim authorized_keys # 将公钥添加到authorized_keys中 chmod 600 authorized_keys 完成用户创建后，选择任意一处地方，创建一个git裸仓库，该仓库就是用于传输网站资源的git仓库。如果没有其他要求的话，git用户的家目录/home/git似乎就是一个不错的选择。\n1 2 cd ~ git init -b main --bare myblog.git 为了在上传网站资源文件后，git可以自动帮我们将更新的资源文件，拷贝到/var/www/blog文件夹，我们需要在git仓库中创建一个post-receive钩子。\n1 vim myblog.git/hooks/post-receive 添加如下内容：\n1 2 #!/bin/sh git --work-tree=/var/www/blog --git-dir=/home/git/myblog.git checkout -f 保存该文件，回到终端，赋予该文件可执行权限。\n1 chmod 744 myblog.git/hooks/post-receive 也不要忘记将nginx网站根目录/var/www/blog所有权转移给用户git，否则将无法进行写入和更新操作！\n1 chown -R git:git /var/www/blog 这样，远程Web服务器端的配置就完成了。\n本机端的操作 这部分的配置也可以参考官方文档One-Command Deployment | Hexo。\n回到本机上的WSL Hexo环境。首先切到Hexo主文件夹。\n1 cd MyBlog 为了使用Git进行一键部署，我们需要安装Hexo的Git一键部署插件。\n1 npm install hexo-deployer-git --save 在_config.yml最后deploy下添加一节。\n1 2 3 4 5 deploy: - type: git repo: git@mysrv.in:myblog.git branch: main message: 记得把mysrv.in替换成你的远程Web服务器地址。如果有其他需求，如指定认证使用的私钥位置、端口等，可以参考我先前写的文章SSH配置主机别名及指定认证私钥路径，修改ssh_config文件实现。\n完成以后，试试运行命令:\n1 hexo g -d 然后再访问一下博客站点，是不是已经成功将Hexo内容部署上去了呢？\n常见问题的解决 本机部署命令执行成功，但远程服务器上访问到的资源未更新 在调试网站并刷新时，建议使用Ctrl+F5快捷键强制刷新，这将强制浏览器重新请求网站文件，从而可以避免缓存对调试结果造成影响。\n其次，检查网站目录/var/www/blog的内容是否已经更新，以及nginx配置文件正确性。\n如果明明git推送成功，但却没更新到网站目录中，需要检查post-receive钩子内容是否正确，路径是否正确，权限是否正确。\n在这里，post-receive钩子文件对以上用户（本例中为git）必须具备可执行权限。同时，nginx的站点文件夹（本例中为/var/www/blog）必须对git用户开放写入权限，对nginx用户www-data开放读取权限。\n正确的权限是，权限8进制代码为744或755，所有者为git:git。可以通过下列命令指定。\n如果不确定，可以以git用户身份尝试以下命令：\n1 2 3 cd ~ chown git:git myblog.git/hooks/post-receive chmod 744 myblog.git/hooks/post-receive 再以root身份执行以下命令：\n1 2 sudo chown -R git:git /var/www/blog sudo chmod -R 744 /var/www/blog 修改博客后更新资源时，发现生成的页面字母大小写异常 这是由于默认的Hexo Git插件对大小写不敏感导致的。需要修改\u0026lt;Hexo主目录\u0026gt;/.deploy_git/.git/下面的config文件，在[core]一节中添加一行ignorecase = false:\n1 2 3 4 5 6 7 8 9 [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true + ignorecase = false [branch \u0026#34;master\u0026#34;] remote = git@mysrv.in:myblog.git merge = refs/heads/master ","date":"2021-12-29T16:44:23+08:00","permalink":"/2021/12/29/wsl-create-mini-hexo-blog-environment/","title":"使用WSL创建精简的Hexo写作环境"},{"content":"自从Eclipse基金会将Java EE转到Jakarta EE后，一些软件包的名称就发生了变化。而最新版的Tomcat 10，也将jakarta作为默认的命名空间。因此，从未来的角度考虑，我们新建Java Web项目的时候，就应该使用jakarta而不是javax了。\n在IDEA里边，选用哪个命名空间，有一个选项可以直接切换。而如果使用的是开源的IDE Eclipse JEE版本，情况可能会让新手摸不着头脑。为了避免日后再次踩坑，这里记录一下，如何使用Eclipse jee新建一个web项目，并使用jakarta命名空间以兼容Tomcat 10或以上版本。\n开发工具平台 Eclipse IDE for Enterprise Java and Web Developers Tomcat支持Jakarta EE以上的版本（10+），可以前往Tomcat 10下载页面 上述两个工具下载好后解压即可使用。\n开始我们的第一个项目 我们首先创建一个Eclipse的工作区。不用担心，工作区就是个文件夹，用来存储你的项目。找个你觉得合适的地方，新建个文件夹就可以了。这里，我们的工作区名为JakartaProjects_2021。\n接着，我们点击左上角的File-\u0026gt;New-\u0026gt;Dynamic Web Project，创建一个Java Web动态网站项目。\n📌 重要 小插曲: 添加Tomcat服务器\n在弹出来的新建项目对话框中，如果你的Target runtime一项为\u0026lt;None\u0026gt;，需要先点击旁边的New Runtime，以添加我们的Tomcat。\n展开Apache一项，选择和你下载Tomcat版本号一致的运行时，这里我们选的是Apache Tomcat v10.0。\n点击下一步，第一行名称我们可以不管，重点是第二行，我们需要在指定我们先前下载的Tomcat解压后的目录。\n点击Finish，Tomcat就添加好了。\n接下来，填写项目名称，项目路径，Tomcat运行时等。注意，为了使用Jakarta规范，Dynamic web module version一项应该大于等于5.0.\n⚠️ 警告 如果在后面的流程中，新建的文件莫名其妙地只有javax而没有jakarta，可以再次确认是否在本步骤中误选Dynamic web module version一项为4.0或者更低版本号。\n点击确定，让Eclipse为我们创建好项目。\n这样一来，初始项目就创建好了。\n编写第一个Servlet 从简单入手，我们从编写第一个Servlet开始。首先，我们在src/main/java上，右键单击，新建一个包，名字可以由你自己决定。这里我就选择了com.test.servlet作为包名。\n接着，在新建的包上，右键单击，新建一个Servlet。\n弹出来的小窗口，我们指定新Servlet的类名，这里我写的是MyFirstServlet。同时请务必注意，在下面的祖先类一栏，我们应该选择/填写jakarta.servlet.http.HttpServlet。\n点击确定，就会为我们创建出正确的Servlet类了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.test.servlet; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; /** * Servlet implementation class MyFirstServlet */ public class MyFirstServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public MyFirstServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub response.getWriter().append(\u0026#34;Served at: \u0026#34;).append(request.getContextPath()); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } } 可以展开观察一下顶部的import语句，确保使用了jakarta命名空间而非javax命名空间。我曾经有同学在初学Java Web时，因为这个原因，调试了一个多小时没找到原因。最后发来源代码一看，正是在Tomcat 10上，运行着javax.*的代码，IDE也没有成功给出错误提示。\n同时，Eclipse还会为我们添加Servlet到web.xml里。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;web-app xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns=\u0026#34;https://jakarta.ee/xml/ns/jakartaee\u0026#34; xmlns:web=\u0026#34;http://xmlns.jcp.org/xml/ns/javaee\u0026#34; xsi:schemaLocation=\u0026#34;https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd http://xmlns.jcp.org/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd\u0026#34; id=\u0026#34;WebApp_ID\u0026#34; version=\u0026#34;5.0\u0026#34;\u0026gt; \u0026lt;display-name\u0026gt;MyJakartaWeb1\u0026lt;/display-name\u0026gt; \u0026lt;welcome-file-list\u0026gt; \u0026lt;welcome-file\u0026gt;index.html\u0026lt;/welcome-file\u0026gt; \u0026lt;welcome-file\u0026gt;index.jsp\u0026lt;/welcome-file\u0026gt; \u0026lt;welcome-file\u0026gt;index.htm\u0026lt;/welcome-file\u0026gt; \u0026lt;welcome-file\u0026gt;default.html\u0026lt;/welcome-file\u0026gt; \u0026lt;welcome-file\u0026gt;default.jsp\u0026lt;/welcome-file\u0026gt; \u0026lt;welcome-file\u0026gt;default.htm\u0026lt;/welcome-file\u0026gt; \u0026lt;/welcome-file-list\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;description\u0026gt;\u0026lt;/description\u0026gt; \u0026lt;display-name\u0026gt;MyFirstServlet\u0026lt;/display-name\u0026gt; \u0026lt;servlet-name\u0026gt;MyFirstServlet\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;com.test.servlet.MyFirstServlet\u0026lt;/servlet-class\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;MyFirstServlet\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/MyFirst\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; \u0026lt;/web-app\u0026gt; 也可以在MyFirstServlet类开头，使用注解的方式添加虚拟路径。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.test.servlet; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import jakarta.servlet.annotation.WebServlet; /** * Servlet implementation class MyFirstServlet */ @WebServlet(name = \u0026#34;MyFirstServlet\u0026#34;, value = \u0026#34;First\u0026#34;) public class MyFirstServlet extends HttpServlet { /** Other Codes */ } 测试运行 点击运行，首次运行会提示需要配置Tomcat服务器，只需要展开Apache项目，选择先前添加的对应Tomcat，其他问题Eclipse会帮我们解决。\n在浏览器中访问地址。注意路径要和web.xml或者注解中匹配噢。\n剩下的具体开发问题，不在本文讨论范围，我们下次再讨论。\n","date":"2021-12-14T22:47:55+08:00","permalink":"/2021/12/14/create-jakarta-ee-web-application-with-eclipse/","title":"使用eclipse创建Jakarta EE Web项目"},{"content":"为什么要写这个呢？ 主要吧，现在在Windows上，不管有什么多种多样的shell移植如Msys，Cygwin之类，用得最多最方便的，特别是和我目前主要使用的两个IDE/编辑器: Visual Studio与Visual Studio Code最相合的，肯定是Powershell。\n但是，默认的Powershell自动补全非常难用，可以自动补全的参数就寥寥几个。我觉得还是可以稍微改进一下。\n方法 首先需要安装好PSReadLine模块。\n安装好后，使用你最喜欢的编辑器，编辑$PROFILE这个变量对应的文件。这里我们以使用Windows自带的记事本notepad为例，在Powershell中输入:\n1 notepad $PROFILE 添加如下片段:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Import-Module PSReadLine ## Shows navigable menu of all options when hitting Tab Set-PSReadLineKeyHandler -Key Tab -Function MenuComplete ## Autocompleteion for Arrow keys Set-PSReadLineOption -HistorySearchCursorMovesToEnd Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward Set-PSReadLineOption -ShowToolTips Set-PSReadLineOption -PredictionSource History #Set the color for Prediction (auto-suggestion) Set-PSReadLineOption -Colors @{ Command = \u0026#39;Magenta\u0026#39; Number = \u0026#39;DarkBlue\u0026#39; Member = \u0026#39;DarkBlue\u0026#39; Operator = \u0026#39;DarkBlue\u0026#39; Type = \u0026#39;DarkBlue\u0026#39; Variable = \u0026#39;DarkGreen\u0026#39; Parameter = \u0026#39;DarkGreen\u0026#39; ContinuationPrompt = \u0026#39;DarkBlue\u0026#39; Default = \u0026#39;DarkBlue\u0026#39; InlinePrediction = \u0026#39;DarkGray\u0026#39; } oh-my-posh --init --shell pwsh --config ~/jandedobbeleer.omp.json | Invoke-Expression 每句配置的含义都非常容易懂。颜色部分是我自己定义的，都可以自由改动。此外我还使用了oh-my-posh定义shell的样式。改动结束后，保存文件，重新打开Powershell即可应用配置。或者使用. $PROFILE以在当前终端读取配置文件。\n","date":"2021-12-06T01:30:00+08:00","permalink":"/2021/12/06/enable-more-comfortable-auto-completion-for-powershell/","title":"为Powershell启用更舒服的自动补全"},{"content":"因此从易用性角度还是选用了VSCode，一开始以为配置VSCode使用LaTeX会非常麻烦，但实际上比想象中简单很多。\n预备条件 VSCode VSCode插件LaTeX Workshop Tex环境套件，更推荐TeXLive TeXLive套件可以从TUG网站上下载安装，如果用的是Linux也可以直接使用系统软件源中提供的打包，功能上都是等价的。\n如果文章是中文的，需要在定制功能的时候选择中文的支持和字体，并最好安装XeTeX组件，该组件支持Unicode文件编码，对中文的处理更好。\n📝 备注 latexindent报错问题\nlatexindent是格式化.tex文件的常见选择。然而如果在Debian/Ubuntu上运行该程序时出现了类似以下报错：Can't locate Config/YAML.pm in @INC或是Can't locate File/HomeDir.pm in @INC，这表明当前系统中的perl没有安装相应的模块。\n在Debian/Ubuntu下，与其费劲从CPAN安装，不如直接apt install libyaml-tiny-perl libfile-homedir-perl，几秒钟搞定。\n配置工作区 配置文件其实是参考的https://zhuanlan.zhihu.com/p/38178015\n不过，我更喜欢将设置放到工作区里面，这样就更方便一些。\n丢上我的配置文件.vscode/settings.json\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 { \u0026#34;latex-workshop.latex.tools\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;xelatex\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;xelatex\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;-synctex=1\u0026#34;, \u0026#34;-interaction=nonstopmode\u0026#34;, \u0026#34;-file-line-error\u0026#34;, \u0026#34;-pdf\u0026#34;, \u0026#34;%DOCFILE%\u0026#34; ] }, { \u0026#34;name\u0026#34;: \u0026#34;pdflatex\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;pdflatex\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;-synctex=1\u0026#34;, \u0026#34;-interaction=nonstopmode\u0026#34;, \u0026#34;-file-line-error\u0026#34;, \u0026#34;%DOCFILE%\u0026#34; ] }, { \u0026#34;name\u0026#34;: \u0026#34;bibtex\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;bibtex\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;%DOCFILE%\u0026#34; ] } ], \u0026#34;latex-workshop.latex.recipes\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;xelatex\u0026#34;, \u0026#34;tools\u0026#34;: [ \u0026#34;xelatex\u0026#34; ], }, { \u0026#34;name\u0026#34;: \u0026#34;pdflatex\u0026#34;, \u0026#34;tools\u0026#34;: [ \u0026#34;pdflatex\u0026#34; ] }, { \u0026#34;name\u0026#34;: \u0026#34;xe-\u0026gt;bib-\u0026gt;xe-\u0026gt;xe\u0026#34;, \u0026#34;tools\u0026#34;: [ \u0026#34;xelatex\u0026#34;, \u0026#34;bibtex\u0026#34;, \u0026#34;xelatex\u0026#34;, \u0026#34;xelatex\u0026#34; ] }, { \u0026#34;name\u0026#34;: \u0026#34;pdf-\u0026gt;bib-\u0026gt;pdf-\u0026gt;pdf\u0026#34;, \u0026#34;tools\u0026#34;: [ \u0026#34;pdflatex\u0026#34;, \u0026#34;bibtex\u0026#34;, \u0026#34;pdflatex\u0026#34;, \u0026#34;pdflatex\u0026#34; ] } ], \u0026#34;latex-workshop.view.pdf.viewer\u0026#34;: \u0026#34;browser\u0026#34; } 注意，我这里设置了在默认浏览器中预览生成的PDF，这是因为我有两个屏幕，一个编写一个预览，如果你觉得集成在一个VSCode窗口里更好，请把browser换成tab。\n编译的时候，只需要在VSCode左侧，TeX，点xetex旁边的播放按钮，就会启动编译流程。\n","date":"2021-12-06T01:27:24+08:00","permalink":"/2021/12/06/write-latex-with-visual-studio-code-editor/","title":"使用VSCode编写Latex文章"},{"content":"但有一个问题，有时候你可能拥有几个不同的SSH私钥（虽然没什么使用多个密钥对的必要性），或者私钥文件文件名不是默认的~/.ssh/id_rsa，应该怎么指定需要使用的私钥文件路径呢？\n几种常见的情景如下所示，更多详细内容，和标准的定义，请参考ssh_config手册，或在终端中键入man ssh_config查看。\n使用命令行登录SSH 第一种，就是我们最常见的登录SSH服务器的方式。当你在终端中输入如下命令。\n1 ssh myname@my.host.addr SSH客户端会连接到该服务器，并尝试使用默认路径（见上述手册，IdentityFile部分）下的私钥，进行认证。如果私钥不在默认路径下，也未显式指定私钥文件，而服务器要求必须使用私钥认证，则认证失败。\n如果想显式指定私钥，可以使用-i参数，指定私钥文件。例如。\n1 ssh myname@myhost -i /path/to/mykey 如果是连接SSH服务器啥的，这就够用了。但缺点很明显，每次登录，都必须显式指定私钥文件。而如果是git命令，则会因为没有-i参数支持，命令执行失败。怎么改进呢？我们看下面。\n使用ssh_conifg文件 配置文件 实际上，关于一个SSH连接，一些行为是可以在配置文件中定义的。你可以针对不同的目标服务器，配置不同的连接行为。\n对于Linux中的一个用户而言，SSH配置文件位于~/.ssh/config。对于整个系统而言，SSH配置文件位于/etc/ssh/ssh_config（注意区别/etc/ssh/sshd_config，sshd_config是SSH服务器进程的配置文件，不用于客户端。）\n我们用一个~/.ssh/config的片段来说明。\n1 2 3 4 Host myserver Hostname 192.168.1.123 User root IdentityFile ~/mykey 上面这个配置文件非常好读懂。\n最开始的Host，代表的是，你在ssh命令中，给定的host参数的值，常常就是你输入username@host中@后面那部分。如果匹配，则应用下面的规则。对于该Host的定义，在config文件中持续到下一个Host或者Match。\n接下来是Hostname指定了你服务器的真实地址，可以是ip，当然也可以是域名。\nUser指定了登录的用户名。\nIdentityFile指定了登录该Host使用的认证私钥文件路径。\n使用 编写了配置文件，就可以使用了。现在，登录到你的服务器更加简单，只需要在终端中输入。\n1 ssh myserver 就可以应用Host myserver块下的配置，自动使用指定的私钥文件进行认证。\nGit 📝 备注 关于Git使用场景的提醒 虽然大抵是简单的触类旁通：上面用于Git的案例，不仅适用于Github，也适用于其他Git平台，甚至包括自己搭建的Git服务器，像本网站的生成部署脚本，就利用了这个方法达到一键部署的效果。\n我们可以用上述方法，解决git命令没有-i参数的不便之处。以Github为例，我们只需要向~/.ssh/config添加如下一个段落。\n1 2 3 Host github.com HostName github.com IdentityFile ~/mykey 这样，下次对远程仓库代码进行更改时候，就会使用你指定的SSH私钥了。\n","date":"2021-12-04T21:06:43+08:00","permalink":"/2021/12/04/ssh-config-specify-private-key-hostname/","title":"SSH配置主机别名及指定认证私钥路径"},{"content":"使用注册表，可以轻松向系统中添加该双拼方案。但之前在网上搜索时候，有些方法不知道为什么会失败。这里贴一个我用起来没问题的。\n使用方法：\n新建一个文本文件 将下列文本复制粘贴到该文本文件中 修改文件后缀名为.reg，双击合并该注册表 1 2 3 4 5 6 7 Windows Registry Editor Version 5.00 [HKEY_CURRENT_USER\\Software\\Microsoft\\InputMethod\\Settings\\CHS] \u0026#34;Enable Double Pinyin\u0026#34;=dword:00000001 \u0026#34;DoublePinyinScheme\u0026#34;=dword:0000000a \u0026#34;Expand Double Pinyin\u0026#34;=dword:00000000 \u0026#34;UserDefinedDoublePinyinScheme0\u0026#34;=\u0026#34;XiaoHe*2*^*iuvdjhcwfg xmlnpbksqszxkrltvyovt\u0026#34; ","date":"2021-12-04T19:06:18+08:00","permalink":"/2021/12/04/windows10-enable-xiaohe-double-pinyin-with-registry/","title":"注册表开启Windows 10微软拼音输入法小鹤双拼"},{"content":" 💡 提示 事到如今，投入 Hugo 后端怀抱什么的已经说不出口了。\n📝 备注 为了响应业内同仁们逐步AI化的浪潮，如你所见，新站点头像、favicon都是AI生成的了。\n","date":"2021-12-04T19:00:00+08:00","permalink":"/2021/12/04/first-post/","title":"第N+1篇 First Post"}]