MoreRSS

site iconzhonger 

前端开发者,喜爱运维管理
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

zhonger 的 RSS 预览

Kubernetes 不完全入门

2024-10-08 15:50:00

前言

  Web 应用的生产环境部署随着技术的发展不断地发生改变,如下图所示,从最早期的单机环境到多机环境,再发展到复杂环境:

  • 单机环境指的是代码、运行环境、文件存储、数据库服务都在同一台服务器上的应用部署方式。通常来说,个人应用或者早期 Demo 应用大多采用这类方式。单机环境的优点在于不需要太多服务器资源,缺点在于过分依赖本地资源而没有高可用性、高可扩展性以及数据的安全保障。如果是云服务器作为单机环境,可以通过升级配置的方式来提高 CPU、内存和存储资源。至于数据则可以通过异机备份或本地备份的方式来保障数据的可靠性。
  • 多机环境指的是同一应用所需的运行环境、数据库服务、文件存储服务分布在不同的节点或集群中的应用部署方式。这类方式的优点自然是具备高可用性、高可扩展性以及完备的数据安全保障,缺点则是需要大量的服务器资源。因此,多机环境通常是对外大量用户提供服务时常用的方式。
  • 复杂环境打破了原有的瓶瓶罐罐,是多机环境的一种的超级形态。在复杂环境中,不再拘泥于服务器节点本身,而是利用 Web 应用将已有的云资源联系在一起。说得更直白一点,就是应用开发者不再需要关心应用运行环境、数据库服务、文件存储等基础环境的配置和管理,唯一需要关心的只有应用代码本身。这也是现在大部分 Web 应用的真实部署方式。

  自从代码版本跟踪软件 Git 横空出世以来,逐步形成了以 Git 为中心的持续开发、持续集成和持续部署的现代应用开发方式。这与复杂环境的部署方式完全契合,由云服务提供商来提供和维护各类运行环境、数据库服务和文件存储,开发团队只需要专注于对代码存储库的管理。

  举个例子,当某个开发成员完成了某个模块的开发并推送到某个分支,该分支创建后会自动触发持续集成进行自动 Review。自动 Review 通过后,开发团队负责人可以对该分支的代码更改进行审核,通过后允许将该分支与其他某个指定分支进行合并(合并操作也是通过持续集成自动进行)。当所有代码开发完毕后,由总负责人审核汇入最终部署的分支。审核通过后持续集成会自动合并代码并通过持续部署将完整的代码部署到真实的运行环境中。如今的 GitHub、GitLab 均能完成持续开发、持续集成和持续部署的全过程。当然,也有一些软件(比如 Jenkins 等)可以完成持续集成和持续部署两步,而持续开发则可以依托任意的 Git 托管服务。

友情提醒

  以上叙述非专业解释,仅为个人看法,不喜勿喷。Kubernetes 官方将部署方式分为传统部署虚拟化部署容器部署三类。

(a) 单机环境和多机环境。(b) 复杂环境。(c)以 Git 为中心的持续开发、持续集成、持续部署的现代应用开发方式。

实例解析

  假设一个 Web 应用同时需要使用:

  • 运行环境:PHP 运行环境、Python 运行环境、NodeJS 运行环境。
  • 数据库服务:关系型数据库(比如 MySQL)、非关系型数据库(比如 MongoDB)、缓存数据库(比如 Redis)。
  • 文件存储:用于存储用户头像、上传文件的对象存储(S3)、用于存储运行代码的文件存储(比如 NFS)。
  • 高可用应用入口:比如 Nginx、HaProxy 等。

  在单机环境中,按照以前我们碰见这种要求可能就要头大了,毕竟同时配置这么多环境难免会有不可预知的问题。不过现在,容器化技术(比如 Docker)可以帮助我们将所有的需求都拆分成独立的 container 实例。不但可以让它们之间在内部网络中互通,还可以对外只暴露必要的应用入口所需的 80443 端口。这样一来,既将各项服务进行了合理拆分,又能保证应用服务的安全性。即使是需要对某个运行环境或者数据库服务进行版本升级,也可以很容易做到。

  在多机环境中,我们如果还想用容器化技术,那就必须用容器化集群。很久以前,Docker 官方就提供了一种 Swarm 模式来组成容器化集群。这种方式的好处是非常简单配置、轻量易用,对于熟悉使用 Docker 的开发者来说只需要花很少的时间就能搞明白。缺点也很明显,Docker Swarm 依赖于 Docker API。也就是说,Docker 本身不支持的东西还是不支持,比如更加高效安全的网络、花式多样的存储等。为了能够更好跨主机集群地自动部署、扩展以及运行应用程序容器,我们选择使用 Kubernetes(缩写为 K8S)。

小知识

  在 2014 年 Google 开源了 Kubenetes 项目,后来又贡献给了云原生计算基金会 CNCF。很多公司以 Kubernetes 为基础开发了自家的容器化集群平台,比如 RedHat 的 OpenShift,AWS 的 Elastic Kubenetes Services, EKS,Azure 的 Azure Kubernetes Services, AKS,阿里云的 Aliyun Container Service for Kubernetes, ACK,腾讯云的 Tencent Kubernetes Engine, TKE 等。

Kubernetes 架构

  虽然 Kubernetes 官方文档已经将架构图以及相关概念介绍得非常清楚,但还是想说说自己的理解。类似于一般集群平台,K8S 也需要有至少一个控制节点(官方称之为“控制平面”)和一个工作节点。默认来说,K8S 不推荐控制节点同时作为工作节点,因为这会影响集群调度的可靠性和可用性。从下面的重绘架构图可以看出,K8S 集群会对外提供 API 以供用户从集群外进行调度。在集群内部,工作节点通过 kubelet 服务与控制节点直接连接,控制节点也通过 kubelet 服务向工作节点下达调度指令来管理工作节点上的 pod。

Pod 的概念

  Pod 可以理解为 K8S 中应用的最小单位,一个 Pod 中可能会包括一个或多个 container (容器),这些容器间可以互通,但对外只有 Pod 有资格拥有 IP。这有点类似于进程与线程之间的关系,进程是拥有资源的最小单位,线程依赖于进程而存在,同一进程间的线程间可以无障碍通信,而不同进程间的通信则需要通过端口或 socket 来进行。

  同一个工作节点上的 Pod 的 IP 属于同一个子网,不同工作节点的子网又属于同一个大子网 (podSubnet,一般需要在初始化集群时定义)。这样的设计在很大程度上减少了 IP 管理的难度,并且能够最大程度上减少容器暴露的风险。

重绘 Kubernetes 官方文档架构图

CRI 的概念

  CRI,全名为 Container Runtime Interface(容器运行时接口),是 K8S 架构中 kubelet和容器运行时通信的主要协议。我们所熟知的 Docker 就是一种容器运行时,但是自从 K8S 1.20 版本弃用 Docker 自带的容器运行时接口 Dockershim 以来,我们只能使用额外的 CRI – cri-dockerd 来调用 Docker。因此推荐使用包含 CRI 的容器运行时 containerd 或者 cri-o 来替代 Docker。

组成部分

  由于 K8S 是一款平台无关的容器集群方案,所以官方提供的方案只是一个整体,我们需要自行选择以下各项组件:

  • 容器运行时 (CRI):如上所述,推荐使用 containerd 或者 cri-o 来替代 docker。下面实践部分将以 containerd 为例。
  • 网络组件 (CNI, Container Network Interface):K8S 中网络有三种 Node、Pod、Service,其中 Node IP 是节点的 IP,用于连通或者暴露端口。Pod IP 是 Pod 的独立内网 IP,只能在 K8S 集群间访问。Service IP 是多个 Pod 共同组成 Service 后需要互通时使用的,一般仅在 Service 内部可访问,不能被用户访问。K8S 官方文档中 联网和网络策略 列举了很多可用的 CNI,这里我们选用 Calico 来进行实践。
  • 服务发现 (DNS):默认为 CoreDNS,在配置完网络组件后自动创建,为 Pod 提供 DNS 解析服务,包括公网域名解析和 Service 别名解析。
  • 容器存储接口 (CSI, Container Storage Interface):目前 K8S 基本上移除了大部分的第三方软件相关存储插件,转而通过第三方自行维护的 CSI 来扩充存储类的支持。可以通过查看 K8S 官网文档的 存储制备器 和 kubernetes-csi 的 Drivier 来了解更多。这里我们选用 NFS 的 CSI Driver 作为例子进行实践。

搭建 K8S 集群

  在学习环境中,我们可以使用 kindminikube 或者 kubeadm 在本地快速部署 K8S 集群;在生产环境中,我们可以使用 kopsKubespray 或者 kubeadm 在多节点上快速部署 K8S 集群。所以这里我们采用了通用的 kubeadm 来搭建 K8S 集群。

  在正式部署之前需要规划实际架构、做一些基本准确以及安装必要的软件和工具 – kubeletkubectlkubeadm。下图为本实践规划的 NodeSubset、PodSubset 和 ServiceSubset。

K8S 集群实际架构和网络规划

基本准备

关闭 SWAP 交换分区

  K8S 为了性能考虑默认必须关闭 SWAP 交换分区,而通常实体服务器安装后会有 SWAP 交换分区,云服务器或 VPS 没有。通过 sudo swapoff -a 命令可以临时关闭 SWAP 分区,或者通过注释 /etc/fstab 文件中的 swap.img 这一行并重启服务器永久关闭 SWAP 交换分区。

开启 IPv4 转发

  由于 K8S 集群中同一个 Service 的 Pod 可能被分配到不同节点,那么不同节点间的 Pod 通信是非常必要的,即不同 PodSubnet 之间的通信需要通过 Node IP 来进行 IPv4 转发。执行以下命令添加允许 IPv4 转发到 /etc/sysctl.d/k8s.conf 文件里,并且立即生效:

# 添加配置
sudo tee -a /etc/sysctl.d/k8s.conf << EOF
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF

# 立即生效
sudo sysctl --system

配置主机名对应

  K8S 集群初始化时会自动搜索主机名的 DNS 解析,目前测试的主机名没有完整的 FQDN 或 PTR 解析,因此有必要设置好主机名和 IP 对应信息到本地静态 DNS 解析文件 /etc/hosts 中。

# 分别在不同节点根据规划设置好主机名
sudo hostnamectl set-hostname vm01
sudo hostnamectl set-hostname vm02
sudo hostnamectl set-hostname vm03

# 修改所有节点的 /etc/hosts
sudo tee -a /etc/hosts << EOF
192.168.120.1  vm01
192.168.120.2  vm02
192.168.120.3  vm03
EOF

同步时间

  K8S 集群的运行必须保证节点的时间是完全同步的,否则容易造成某些未知的 Bug。比如证书的过期时间将会被某些时间不同步的节点错误判断。推荐使用同一时区和同一 NTP 服务器,如下即可完成设置。

# 设置相同时区并查看
sudo timedatectl set-timezone Asia/Shanghai
timedatectl
timedatectl status

# 修改 NTP 服务器
sudo timedatectl set-ntp false
sudo sed -i 's/#NTP=/NTP=ntp.lisz.top/' /etc/systemd/timesyncd.conf
sudo sed -i 's/#FallbackNTP=ntp.ubuntu.com/FallbackNTP=ntp.aliyun.com/' /etc/systemd/timesyncd.conf
sudo timedatectl set-ntp true

# 重启服务使配置生效、同步时间并查看信息
sudo systemctl restart systemd-timesyncd
timedatectl show-timesync --all
date

安装必要软件和工具

安装 containerd

  containerd 虽然是由 containerd 开发团队负责发布,但是 APT 或 YUM 镜像源仍然是由 Docker 官方负责,所以当我们添加 docker-ce 的镜像源后可以直接下载 containerd.io 来安装 containerd。当然, 我们可以从 containerd/containerd 直接下载二进制可执行文件。

# 添加镜像源
curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/docker.gpg
sudo tee -a /etc/apt/sources.list.d/docker.list << EOF
    deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/docker.gpg] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu/ $(lsb_release -c --short) stable
EOF

# 更新软件列表缓存并安装 containerd
sudo apt update && sudo apt upgrade -y && sudo apt install -y containerd.io
小建议

  个人推荐使用 APT 或 YUM 方式安装 containerd。原因有二:一是国内有 docker-ce 镜像安装比较快,二是想要更新时非常容易。

配置 containerd

  containerd 安装后默认没有配置文件也不会自动启动后台程序,所以需要准备配置文件并复制到 /etc/containerd/config.toml 再启动。由于 K8S 集群默认使用 registry.k8s.ioregistry-1.docker.io 源下载容器镜像,为了提升速度建议切换到阿里云和 DaoCloud 的加速器。

containerd config default > containerd_config.toml
sed -i "s#registry.k8s.io#registry.cn-hangzhou.aliyuncs.com/google_containers#g" containerd_config.toml
sed -i "/containerd.runtimes.runc.options/a\ \ \ \ \ \ \ \ \ \ \ \ SystemdCgroup = true" containerd_config.toml
sed -i "s#https://registry-1.docker.io#https://docker.m.daocloud.io#g" containerd_config.toml

sudo mkdir -p /etc/containerd
cp containerd_config.toml /etc/containerd/config.toml

sudo systemctl daemon-reload
sudo systemctl enable containerd
sudo systemctl restart containerd

sudo systemctl status containerd.service && sudo ctr --version
小提示

  由于 ctr 命令连接的 containerd 的 socket 文件只有 root 用户组有权限访问,所以目前只能使用 sudo ctr。如果想要直接使用 ctr 命令,可以使用 sudo usermod -aG root ubuntu 来将当前用户添加到 root 用户组。赋权之后需退出登录后再次登录才能生效。

安装 kubeadm 等

  国内清华大学 TUNA 镜像源、阿里云镜像源等都提供了 kubeadm 等三件套工具的 APT 或 YUM 源,通过以下命令可以很容易完全安装。

友情提醒

  由于 kubernetes 不同版本可能会存在较大差异,并且为了避免节点 kubernetes 版本在不自觉的时候升级造成兼容性问题,这里推荐固定 kubeadm 等三件套版本号,即不启用 apt upgrade 自动升级。管理员关闭 K8S 集群手动升级版本时不受影响。

# 添加镜像源
curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/kubernetes/core:/stable:/v1.30/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg 
sudo tee -a /etc/apt/sources.list.d/kubernetes.list << EOF
deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.tuna.tsinghua.edu.cn/kubernetes/core:/stable:/v1.30/deb/ / 
# deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.tuna.tsinghua.edu.cn/kubernetes/addons:/cri-o:/stable:/v1.30/deb/ / 
EOF

# 更新软件列表缓存并安装 kubeadm 三件套
sudo apt update && sudo apt install -y kubeadm kubelet kubectl 

# 固定 kubeadm 等三件套版本
sudo apt-mark hold kubeadm 
sudo apt-mark hold kubelet 
sudo apt-mark hold kubectl

初始化集群

预下载镜像

  在所有节点上使用以下命令提前下载好 K8S 集群所需的基本镜像,避免初始化时一直在等待各个节点下载镜像。

kubeadm config images list | sed -e 's/^/sudo ctr image pull /g' -e 's#registry.k8s.io#registry.cn-hangzhou.aliyuncs.com/google_containers#g' | sh -x

初始化控制节点

  下载完所需的容器镜像后,在控制节点上使用 sudo kubeadm init --config=kubeadm_config.yaml 命令初始化控制节点。kubeadm_config.yaml 的内容如下所示:(建议将配置文件放置在 ~/k8s 目录下,cd ~/k8s 目录后执行初始化命令。)

apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 192.168.120.1
  bindPort: 6443
nodeRegistration:
  criSocket: "unix:///run/containerd/containerd.sock"
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: stable
imageRepository: "registry.cn-hangzhou.aliyuncs.com/google_containers"
networking:
  podSubnet: 192.168.144.0/20
  serviceSubnet: 192.168.244.0/24
友情提醒

  如果没有配置主机名对应的话,这里初始化会一直卡在 API Health 检测的步骤,实际上是因为没有主机名和 IP 对应而无法启动 API Server。

  kubeadm 初始化成功后需要复制验证文件才能在控制节点管理 K8S 集群,如下所示:

# 复制验证文件
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

# 查询节点状态
kubectl get nodes -o wide

工作节点加入集群

  刚才初始化集群后会出现工作节点加入集群的命令,形如:

kubeadm join 192.168.120.1:6443 --token z9sdsdi.tdeu74psxqi8rhdt \
    --discovery-token-ca-cert-hash sha256:87c4f8dd9dabaf2e5e793c0404c74dd8f9f56153000dad3c1a3238a3e8b0beff

  注意这里需要加上 sudo 之后再在工作节点上执行加入集群操作。加入完成后可以在控制节点上使用 kubectl get nodes -o wide 查看是否有了刚加入的工作节点的信息。由于目前还没有配置网络组件,除了控制节点外,其他工作节点应该均为 NotReady 状态。如果使用 kubectl get pods --all-namespaces 命令查看所有启动的 Pod,应该看到除两个 CordDNS 的 Pod (比如 0/1) 以外的所有 Pod 的状态都是完成启动 (比如 1/1)。

  工作节点加入后可以配置不同的标签,比如如下是配置为工作节点和添加 gputype 字段标签:

kubectl label node vm02 node-role.kubernetes.io/worker=
kubectl label node vm03 node-role.kubernetes.io/worker=
kubectl label node vm02 gputype=P100
kubectl label node vm03 gputype=A100

添加组件

配置 Calico 网络

  Calico 支持一套灵活的网络选项,可以根据情况选择最有效的选项,包括非覆盖和覆盖网络,带或不带 BGP。Calico 使用相同的引擎为主机、Pod 和应用程序在服务网格层执行网络策略。如下所示可以很简单地为 K8S 集群启用 Calico 网络:

  1. 创建 Calico 所需的 operator(需要镜像 quay.io/tigera/operator:v1.34.5,可提前下载),仅在控制节点创建 Pod;
  2. 创建和初始化 K8S 集群时对应的网络规划,主要是 PodSubset 配置,如下面修改过的 yaml 配置文件。这里为了加速创建过程,还添加了 registry 字段来使用 DaoCloud 加速器。
mkdir -p ~/k8s/calico & cd ~/k8s/calico
wget -c https://raw.githubusercontent.com/projectcalico/calico/v3.28.1/manifests/tigera-operator.yaml
wget -c https://raw.githubusercontent.com/projectcalico/calico/v3.28.1/manifests/custom-resources.yaml 
kubectl create -f tigera-operator.yaml 
kubectl create -f custom-resources.yaml
# This section includes base Calico installation configuration.
# For more information, see: https://docs.tigera.io/calico/latest/reference/installation/api#operator.tigera.io/v1.Installation
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
  name: default
spec:
  # Configures Calico networking.
  calicoNetwork:
    ipPools:
    - name: default-ipv4-ippool
      blockSize: 26
      cidr: 192.168.144.0/20
      encapsulation: VXLANCrossSubnet
      natOutgoing: Enabled
      nodeSelector: all()
  registry: m.daocloud.io
---

# This section configures the Calico API server.
# For more information, see: https://docs.tigera.io/calico/latest/reference/installation/api#operator.tigera.io/v1.APIServer
apiVersion: operator.tigera.io/v1
kind: APIServer
metadata:
  name: default
spec: {}

  如果通过 kubectl get pod --all-namespaces 发现哪个相关 Pod 卡在了拉取镜像的步骤,可以手动镜像。一般来说,使用修改的 DaoCloud 加速器下载应该没什么太大问题。创建 Calico 网络完成后会多出来 3 个命名空间: tigera-operatorcalico-systemcalico-apiserver。新增的 Pod 应该如下所示:

节点主机名 新增 Pod 备注
vm01 tigera-operator Calico 网络所需的描述子
vm01 calico-kube-controller Calico 网络控制器
vm01 calico-apiserver Calico 网络 API,一般有两个 Pod
vm01 calico-typha 优化和减少 Calico 对 K8S API 服务器的负载
vm01 calico-node Calico 网络节点客户端
vm01 csi-node-driver CSI 驱动
vm02 calico-typha 优化和减少 Calico 对 K8S API 服务器的负载
vm02 calico-node Calico 网络节点客户端
vm02 csi-node-driver CSI 驱动
vm03 calico-node Calico 网络节点客户端
vm03 csi-node-driver CSI 驱动

配置 NFS CSI 驱动和存储类

  NFS CSI 驱动由 kubernetes-csi/csi-driver-nfs 项目提供支持。不过在正式安装驱动之前需要先安装 NFS 客户端,否则 NFS CSI 驱动也无法正常启用。为了加速下载容器镜像,这里推荐将配置中的 registry.k8s.io 源切换到 k8s.m.daocloud.io 加速器。

# 在所有节点安装 NFS 客户端支持
sudo apt install -y nfs-common

# 下载 NFS CSI Driver 配置文件
mkdir -p ~/k8s/csi && cd ~/k8s/csi
git clone https://github.com/kubernetes-csi/csi-driver-nfs.git 

# 修改容器镜像为 DaoCloud 加速器
cd csi-driver-nfs/deploy/v4.9.0
sed -i "s/registry.k8s.io/k8s.m.daocloud.io/" ./*

# 返回上上层目录,并安装 NFS CSI 驱动
cd ../../
./deploy/install-driver.sh v4.9.0 local 

  安装 NFS CSI 驱动后会在 kube-system 命名空间中多出四个 Pod,其中一个 Pod 为 csi-nfs-controller,其他每个节点一个 csi-nfs-node 的 Pod。然后需要使用 kubectl apply -f nfs.yaml 命令创建一个 NFS 的存储类用于提供给应用程序,配置文件 nfs.yaml 内容如下所示:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-csi
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: nfs.csi.k8s.io
parameters:
  server: nfs_server_ip
  share: /home/data
reclaimPolicy: Retain
volumeBindingMode: Immediate
allowVolumeExpansion: true
mountOptions:
  - async
  - rsize=32768
  - wsize=32768
  - nconnect=8
  - nfsvers=4.1
  - hard
# 查看新增的存储类
╰─$ kubectl get sc
NAME                PROVISIONER      RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
nfs-csi (default)   nfs.csi.k8s.io   Retain          Immediate           true                   4d1h

GPU 支持

  K8S 的 GPU 支持是由 NVIDIA 提供的,需要工作节点先安装 NVIDIA 驱动和容器驱动,再在控制节点上部署 nvidia-device 插件支持。

# 查看可安装 NVIDIA 驱动
╰─$ ubuntu-drivers devices
modalias : pci:v000010DEd000026BAsv000010DEsd00001957bc03sc02i00
vendor   : NVIDIA Corporation
driver   : nvidia-driver-550-open - distro non-free
driver   : nvidia-driver-550 - distro non-free recommended
driver   : nvidia-driver-535-server - distro non-free
driver   : nvidia-driver-535-server-open - distro non-free
driver   : nvidia-driver-535-open - distro non-free
driver   : nvidia-driver-535 - distro non-free
driver   : xserver-xorg-video-nouveau - distro free builtin

# 安装 NVIDIA 驱动,并重启生效
sudo apt install -y nvidia-driver-535
sudo apt-mark hold nvidia-driver-535

# 添加 NVIDIA Container Toolkit 源
curl -fsSL https://mirrors.ustc.edu.cn/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
&& curl -s -L https://mirrors.ustc.edu.cn/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
    sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
    sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

# 更新软件列表缓存,安装 nvidia-container-toolkit
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit

# 为 containerd 容器运行时增加 NVIDIA 选项
sudo nvidia-ctk runtime configure --runtime=containerd

# 修改 /etc/containerd/config.toml 配置文件中默认运行时为 NVIDIA
# 原来的默认运行时是 runc
[plugins."io.containerd.grpc.v1.cri".containerd]
      default_runtime_name = "nvidia"

# 重新加载 containerd 配置文件并重启服务生效
sudo systemctl daemon-reload
sudo systemctl restart containerd

# 在控制节点为 K8S 集群创建 NVIDIA device 插件支持
cd ~/k8s
wget -c https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.16.2/deployments/static/nvidia-device-plugin.yml

sed -i "s/nvcr.io/nvcr.m.daocloud.io/" nvidia-device-plugin.yml
kubectl create -f nvidia-device-plugin.yml

# 验证 GPU 是否被 K8S 识别
kubectl describe node vm02 | grep nvidia.com/gpu:
小提示

  NVIDIA GPU 驱动和容器驱动只需在有 NVIDIA GPU 的工作节点上配置,并且一定要修改默认运行时为 NVIDIA,否则无法被 K8S 识别。NVIDIA device 插件支持在控制节点上提交安装请求但会在每一个工作节点上安装,即使没有 NVIDIA GPU 存在。

可能遇到的问题和解答

如果各节点本身没有任何网络,需要使用内部 HTTP 代理上网怎么办?

  这种情况下需要为 kubelet 和 containerd 的 service 设置代理。kubelet 的配置文件为 /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf,containerd 的配置文件为 /usr/lib/systemd/system/containerd.service。配置内容如下所示。配置完后需要使用 sudo systemctl daemon-reload 来应用配置更改,并且使用 sudo systemctl restart kubeletsudo systemctl restart containerd 重启服务。

# sudo vim /usr/lib/systemd/system/containerd.service
# sudo vim /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf

[Service]
...
Environment="HTTP_PROXY=http://proxy.ip:3128"
Environment="HTTPS_PROXY=http://proxy.ip:3128"
Environment="NO_PROXY=localhost,127.0.0.1"

结语

  K8S 集群的搭建并非一件十分复杂的事情,比较复杂的是根据实际的需求和自己对于 K8S 的理解来搭建出更合适的 K8S 集群。虽然 K8S 集群已经逐步开始取代一般的 Docker 单机应用服务部署方案,但是就个人实际的应用规模或者应用本身而言,K8S 集群本身的维护和调整的代价要远高于 Docker 单机应用服务部署。如果是有高可用性、高可靠性等的需求,那么 K8S 可能是目前最好的需求。

  正如在前言中所述,有了 K8S 持续开发、持续集成和持续部署成为了现实,开发者可以把更多的注意力都放在应用代码开发。同时,类似于 JupyterHub 这类会有动态扩展和分配资源需求的应用,最佳的部署方式可能就是 K8S 了。当然,听说现在的大模型 ChatGPT 等也都是在 K8S 上训练出来的。

  K8S 的确是大有可为!

参考资料

  1. Continuous Development
  2. AWS – 什么是持续集成?
  3. IBM – What is continuous deployment?
  4. Docker Swarm vs Kubernetes: how to choose a container orchestration tool
  5. 维基百科 – Kubernetes
  6. IBM – 第5回 『Red Hat OpenShift と Kubernetes の違い』
  7. Kubernetes 文档 - 概念 - Kubernetes 架构
  8. 基于 Containerd 运行时搭建 Kubernetes 集群实践

生活中的小问题——公交计费问题

2024-07-02 14:15:00

前言

  谈到生活中经常坐的公交车,比较常见的算法问题可能是寻找“耗时最少公交路线”、“最少换乘公交路线”、“最便宜公交路线”、“综合最优公交路线”等。这些算法由于在地图软件中经常被使用,已经被大家研究得非常透彻,比如 Dijkstra 算法就可以用来计算“最短距离公交路线”。如想了解更多,可以阅读参考资料 2 给出的中文文献。

  相比这些常见的算法问题,不如让我们来一起看看不大被人提及的“公交计费问题”。笔者经常在下雨的时候乘坐公交车,每次上车前会先取一张票,然后下车前可以看屏幕显示来知道票价。于是笔者就有了一个小问题:票价是如何正确显示的,是否可以对其建模并写个小程序模拟一下。

问题描述

  如图 1 所示为公交计费问题描述。

  • (a) 公交路线图(图中为右循环路线,但对应的左循环路线也存在),0~20 为站点编号,且 1~3 与 20~18 分别对应相同。公交从站点 0 出发,按照箭头所指的方向依次行进,最终回到站点 0 并停止运行。
  • (b) 相邻站点间距离,以 km 为距离计算单位,给出的数值为公交在站点间实际行进距离。
  • (c) 公交票价计算方式,距离不足 2 km 为基本票价 190 日元,超过 2 km 的话每超过 1 km 加收 50 日元,如不足 1 km 按照 1 km 计算。注意,此处的距离为循环线路中上车站点与下车站点的有效距离,即站点间在循环线路上的最短距离而非实际运行距离。比如,即使从站点 0 上车,经过一圈循环后再从站点 0 下车,也只能收取基本票价 190 日元,因为有效距离为 0 km。(以上计算方式参考自资料 3)
  • (d) 公交最后返回起始站点时计费状态,灰底色为站点编号,白底色为票价。

图 1. 公交计费问题描述。 The description of bus ticket problem.

问题目标

  1. 打印欢迎消息,提示是否从站点 0 发车;
  2. 发车后,通过回车或其他操作在下一站点停车,打印当前票价状态,尚未抵达站点票价为空;
  3. 经过循环后回到站点 0,通过回车或其他操作停车,打印当前票价状态,如图 1(d) 所示。

解决方案

问题分析

  解决公交计费问题,首先要将图 1 中给出的信息进行集成,可得如下图 2。其中,站点间的橙色数字为相邻站点间距离,红色数字为几个关键(0~2 km,2~3 km 和 3~4 km 的阈值站点)站点与出发站点 0 之间的有效距离

图 2. 信息集成后的公交路线图。 The route including the distances and some importance valid distances.

  其实整个问题的核心就在于对有效距离的理解。从题干可知,有效距离并非是实际行进距离。这主要是因为给出的公交路线是环线而非直线,即出发站点与结束站点为同一站点。除此之外,刚开始的 1~3 与最后的 20~18 三个站点是重合的。根据给出的例子解释,我们可以将这里的“有效距离”粗略定义为“上车站点与下车站点在公交路线上正反距离的最小值”。我们不妨从以下示例中进一步加深对于“有效距离”的理解:(~ 表示“大约”)

  • 例 1:上车站点为站点 3,下车站点为站点 17。因为站点 3 与站点 18 重合,所以有效距离等同于站点 17 和 18 之间的距离 0.45 km。
  • 例 2:上车站点为站点 5,下车站点为站点 18。如例 1 方式计算可得有效距离为 0.85 km。
  • 例 3:上车站点为站点 4,下车站点为站点 17。虽然按照路线实际行进距离很远,但是实际两站之间路线上最短距离大约为 0.45 km,即有效距离为 ~0.45 km。
  • 例 4:上车站点为站点 5,下车站点为站点 15。如例 2 方式计算可得有效距离为 ~1.6 km。
  • 例 5:上车站点为站点 6,下车站点为站点 14。按照图中方向实际行进距离计算,可知正向距离为 2.75 km,按照路线上最短反向距离为 ~2.25 km,因此有效距离为 ~2.25 km。

得出总结:

  • 从例 1、2 可以看出,当上车站点和下车站点分别在重合直线和循环圈上时,位于重合直线上的站点需要注意切换到对应站点进行双重计算正反向距离,从而得到正确的有效距离。
  • 从例 3~5 可以看出,当上车站点和下车站点均在循环圈上时,计算反向距离不涉及直线站点(即跨过出发站点 0)。即使站点 17 到站点 4 的实际行进路线不存在,也需以站点 17 到站点 4 闭合的循环圈来进行计算反向距离。
为何不跨过出发站点 0 计算反向距离?

  题干中给出信息“公交从站点 0 出发最终回到站点 0 并停止运行”,鉴于任何跨过出发站点 0 计算的距离实际上只可能由两辆公交车完成,不可能出现在一辆公交的票价计算方式中,当只在循环圈上的站点上下车时应该不考虑直线上的站点(0~3、18~20)。

  说句题外话,如例 3~5 所示,可能直接走过去还更快更方便,而非坐这趟公交。

算法描述

变量声明

变量名 变量类型 描述
distances list 站点列表,[0, 0.2, 0.3, …]
currentStop int 当前站点编号,0
lineStops list 直线站点,[1, 2, 3, 18, 19, 20]
circleStops list 循环圈站点,[4, 5, …, 17]
ticketBase float 基础票价,190.00
ticketStep float 票价梯度,50.00
ticketUnit str 票价单位,JPY
baseDistance float 基础距离,2.00
stepDistance float 基础距离,1.00
distanceUnit str 距离单位,km
validDistances list 有效距离,长度为 21,默认值为 None
prices list 票价,长度为 21,默认值为 None
validStops list 有效站点,经过站点时将编号添加到列表里,默认为 [0]

步骤描述

  程序整体步骤:

  1. 初始化变量,当前站点编号为 0,询问是否启动;接受到启动指令(回车)后开始行进(提示)。
  2. 遇到停车指令(回车)后,切换站点编号为下一站点(+1),添加站点编号到 validStops。
  3. 循环计算 validStops 中各站点的有效距离 validDistances(具体见下)。
  4. 循环计算各站点的票价同时更新 prices。
  5. 打印计费矩阵。
  6. 接受到启动指令(回车)后继续行进(提示),重复 2~5 步骤直至重新回到站点 0。
  7. 打印到达终点站提示信息,结束程序。

  计算任意两个上下车站点间的有效距离的步骤:

  1. 已知上车站点 a 和下车站点 b,当两站点相同时有效距离为 0,如不同进入下一步骤。
  2. 利用 lineStops 和 circleStops 两个变量判断 a 和 b 位于直线部分或循环圈部分。
  3. 如果两站点都是直线部分,利用对称方式标准化为 1~3 的站点编号,直接计算之间距离为有效距离。
  4. 如果两站点都是循环圈部分,利用 circleStops 进行循环遍历叠加计算正反距离,取较小的值为有效距离。
  5. 如果一站点在直线部分、一站点在循环圈部分,对直线部分的站点(标准化后的 1~3 站点)计算正反距离,取较小的值为有效距离。
  6. 返回有效距离。

程序模拟

  根据以上思路,笔者采用 Python 实现了解决方案。源代码请见 Github Gist。以下为程序模拟运行效果:

结语

  虽然现有的公交线路大部分还是很规则的,不同时存在循环圈和直线的情况,计费也较为简单,但是思考特殊公交线路的计费方式也不失为一件有趣的事情。上面给出的分析和算法描述,也可以用其他编程语言实现,比如用前端编程语言就可以直接可视化整个公交计费过程。

参考资料

  1. 图文详解 Dijkstra 最短路径算法
  2. 周文峰等,《运筹与管理》,最优公交线路选择问题的数学模型及算法,2018
  3. 筑波大学循环线票价表

命令行工具开发指南——入门篇

2024-05-29 16:15:00

前言

  命令行工具(Command Line,Cli)作为我们日常开发常用的辅助性工具,几乎遍布于各种操作中。根据使用目的的不同大致可以分为以下几类:

  • 从模板中生成项目:比如使用 npm init 从空模版创建一个新的 NodeJS 项目,使用 composer create-project laravel/laravel example-app 创建一个全新的 Laravel 项目(PHP 项目)等。
  • 启用开发者服务模式:比如使用 python -m http.server 8000 在 8000 端口开启一个临时 HTTP 服务器,使用 bundle exec jekyll s 在 4000 端口开启一个临时 Jekyll 服务器等。
  • 特定功能交互:比如流行的 IP 信息查询工具 nali、磁盘空间利用率和空余空间查询工具 duf、快速磁盘使用分析工具 gdu 等。

  其实任何编程语言都可以用来开发命令行工具,无论是常见的 Golang、Python、NodeJS、PHP、Java,还是 Rust、Ruby、C++、C 或者是古老的 Fortran 等。只是取决于所要实现的功能和具体的使用场景,开发者会采用合适的编程语言开发命令行工具。比如说,Linux 系统中包含了大量的命令行工具,基本上都是用 C 语言编写的,主要是因为 C 语言在 Linux 系统中的执行效率相对更高。对于一般高级编程语言,自带的包管理工具也是由自身高级编程语言编写的命令行工具。类似 Rust、Fortran 等编译型语言则需要通过编译生成二进制可执行文件后才能执行相应的任务。

二进制可执行文件与源文件有何不同?

   二进制可执行文件是指源代码通过编译器编译成计算机可以直接识别的二进制码文件。二进制码文件是无法使用任何源码编辑器打开的,只能由操作系统调用执行或特别的二进制码查看器打开。一般来说,二进制可执行文件是很难跨越操作系统的,即针对不同的操作系统需要分别编译生成对应的二进制可执行文件。尤其是当有其他静态库或者动态链接库依赖时,二进制可执行文件甚至无法跨主机运行。 而源文件是可以在任何操作系统用源码编辑器打开的。大多时候商业公司为了保证源代码的商业版权,只会为用户提供应用的二进制可执行文件。(当然一般可能是包含图形用户界面的。)

为何命令行而非图形界面

  命令行可以说是操作系统应用和编程语言编写应用最基本的形式,图形(用户)界面(Graphic User Interface,GUI)则是在源代码的基础上提供可视化的交互方式、通过键鼠操作来降低用户使用的门槛。这也是为什么 Windows 操作系统比 Linux 操作系统更加流行的原因之一。但是有的时候,界面也有可能会成为用户学习和使用的累赘。

简单界面 vs 复杂界面

  就拿代码编辑器来说,我们所熟知的“宇宙第一编辑器” Visual Studio 几乎支持所有编程语言,尤其是对于构建 C# 项目来说可以半代码半可视化修改。尽管这在很大程度上降低了开发者使用成本,但是学习 Visual Studio 编辑器本身的成本却很高。(说句老实话,笔者从大学本科开始接触 Visual Studio 到现在都没怎么学会使用,😂只会最基本的功能而已。)而且,在普通笔记本电脑上使用 Visual Studio 编辑器运行大型项目时,CPU 和内存资源极大可能会被大量占用,打开一个浏览器页面可能也很艰难。

  相比而言,同样由微软推出的 Visual Studio Code 则是简单界面的优秀代表。化繁为简,Visual Studio Code 本身仅支持最简单的功能:文件目录区、编辑区、终端区三部分布局,基本的代码高亮功能,插件功能,主题功能等。无论是 Python 开发者,还是 Golang 开发者,都能一打开直接上手,只是需要根据编程语言不同安装一些插件来提升开发效率而已。在系统占用资源方面,Visual Studio Code 比 Visual Studio 显著降低,尽管可能也会受安装插件的少量影响。当然有得也有舍,Visual Studio Code 中支持更多文件定义配置或命令行配置,对于没有学过 Linux 的用户可能会有点学习难度。

Linux 哲学

  “一切皆文件”。任何系统、项目、工具都是由一系列的文件组成的,通过配置文件可以实现直接管理。

  虽然这是 Linux 系统设计的哲学思想,但其实是所有操作系统设计的哲学思想,只是顶层封装的程度有所不同。Windows 系统也是“一切皆文件”的,不然那些编辑器的配置都存在哪里了呢。相比 Linux 和 MacOS 系统而言,Windows 系统的顶层封装程度最高,用户对于底层文件的直接管理非常少,尤其是对系统级别的配置管理只能通过图形界面交互完成。MacOS 系统则是介于两者之间,顶层封装程度虽然高但也提供对大部分系统级别配置的直接管理,即可以通过修改文件来实现管理。尽管依旧存在有些系统级别配置难以直接修改,比如说操作系统启动项。

更简单的命令行

  界面在执行系列任务时一般需要多步操作,一顿点点点之后才能完成。当然如果图形界面和功能设计的比较合理的话,可能也只需要一步操作。当我们需要进行批量操作时,即使图形界面只需要一步操作,依旧需要一顿点点点。命令行则没有这种问题,只需要简单写个有循环的脚本即可循环调用命令行工具批量执行。

  另外,命令行工具仅在执行时会占用系统资源,一旦完成即可完全释放。图形界面应用一般需要常驻后台,虽然优化得好的时候所占用的系统资源也可忽略不计,但是还是会有后台进程的。

  尽管命令行工具极少会有显式的界面交互,但是也可以在终端提供非常丰富的命令行交互、功能解释、自动补全、自动建议等。用户使用起来一般没有太大问题,只需要调用子命令和参数即可实现操作。

命令行工具开发

设计标准和规范

  命令行工具开发通常依据两个标准和规范进行:POSIX (Protable Operating System Interface,可移植操作系统接口) 标准和 GNU (GNU’s Not Unix) 项目。POSIX 标准是 IEEE 为维护操作系统间适配性而制定的一系列标准,其中一个标准定义了命令行程序的语法和语义。GNU 旨在创建与 Unix 兼容的自由软件,其中一个子项目 GNU Coreutils 提供了很多常用的命令行程序,比如 lscpmv 等。据此为命令行程序建立了以下的设计标准和规范:

  • 单字母标志 (single-letter flag) 以一个短横线 - 开始,且可以合并使用:比如 -d (全称 --debug) 和 -v (全称 --version) 合并使用 -dv 来以调试模式输出命令行版本号。
  • 长标志 (long flag) 以两个短横线 -- 开始,但无法合并使用:比如 --debug--version 可以被命令行正常解析,但 --debugversion 无法被正常解析。
  • 选项 (Options) 跟在单字母标志后没有分隔符,但跟在长标志后使用等号 = 来分隔标志和选项值:比如 -n example--name=example 分别为单字母标志和长标志的选项用法,二者完全等价。
  • 参数 (Args) 跟在标志或选项之后时没有任何分隔符,仅有空格:比如 curl -o out.html https://www.google.com 中的 https://www.google.com 是参数,一般用法类似 curl [Options] <Arg>
  • 子命令 (Sub command) 与主命令之间没有分隔符,仅有空格:比如 git commitgit 是主命令、commit 是子命令。
  • 单独的两个短横线 --(后不接标志)表示标志或选项的结束和参数的开始:比如 rm -- -f 中的 -f 表示的是要删除的文件 -f,而不是强制删除文件的选项。
提示

  按照以上设计标准和规范开发的命令行工具使用体验会与常用的命令保持一致,对于用户来说非常容易上手,这也是制定设计标准和规范的主要原因。

功能设计

  核心功能设计主要是指命令行工具所支持的子命令、参数、选项设计。其中,子命令表示功能集合,参数表示输入输出变量,选项表示功能的微调。如下所示为笔者开发的命令行工具 pictl (基于 Python 语言开发)的帮助信息。目前提供四个子命令:config(配置基本信息),compress(压缩任意图片为 webp 格式),upload(上传图片)和 cup(压缩并上传图片)。全局只支持 -h--help)打印帮助信息和 -V--version)打印版本信息两个选项。

╰─$ pictl
Usage: pictl [OPTIONS] COMMAND [ARGS]...

  A command line tool for image processing and uploading (ex. S3-type).

  Now it supports:
    - transformation from other image types to `webp` image as well as
      image compression.
    - image file uploading to AWS S3 or Cloudflare R2.

Options:
  -V, --version  Show the pictl version.
  -h, --help     Show this message and exit.

Commands:
  compress  Compress any image into `webp` image.
  config    Operations for the config file `~/.pictlrc`.
  cup       Compress image and upload to remote storage (compress and...
  upload    Upload the file to remote storage.

子命令

  子命令是否越少越好或者越多越好?亦或是不多不少比较好?其实,根据实际功能需求的不同子命令的数量会有很大的差异。比如上面提到的 pictl 目前所支持的子命令只有 4 个,curl 不支持子命令但支持选项超过 20 个,git 支持的常用子命令多达 22 个(如下所示,实际子命令可能接近 100 个),docker 支持的子命令超过 30 个。子命令实际上是可以多层迭代调用的,即可以存在多层级。但是在功能设计时,一般将对同一对象操作的功能归类到同一子命令下面,形成多层级子命令。当然为了简化子命令的层级调用,最多的实践方式就是类似于 git 中的用选项来代替多层的子命令。比如 git branch 子命令是对分支(branch)的列举(git branch -l)、创建(git branch <name>)和删除(git branch -d <name>)的功能集合。

╰─$ git -h
usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           [--super-prefix=<path>] [--config-env=<name>=<envvar>]
           <command> [<args>]

These are common Git commands used in various situations:

start a working area (see also: git help tutorial)
   clone     Clone a repository into a new directory
   init      Create an empty Git repository or reinitialize an existing one

work on the current change (see also: git help everyday)
   add       Add file contents to the index
   mv        Move or rename a file, a directory, or a symlink
   restore   Restore working tree files
   rm        Remove files from the working tree and from the index

examine the history and state (see also: git help revisions)
   bisect    Use binary search to find the commit that introduced a bug
   diff      Show changes between commits, commit and working tree, etc
   grep      Print lines matching a pattern
   log       Show commit logs
   show      Show various types of objects
   status    Show the working tree status

grow, mark and tweak your common history
   branch    List, create, or delete branches
   commit    Record changes to the repository
   merge     Join two or more development histories together
   rebase    Reapply commits on top of another base tip
   reset     Reset current HEAD to the specified state
   switch    Switch branches
   tag       Create, list, delete or verify a tag object signed with GPG

collaborate (see also: git help workflows)
   fetch     Download objects and refs from another repository
   pull      Fetch from and integrate with another repository or a local branch
   push      Update remote refs along with associated objects

'git help -a' and 'git help -g' list available subcommands and some
concept guides. See 'git help <command>' or 'git help <concept>'
to read about a specific subcommand or concept.
See 'git help git' for an overview of the system.

  功能设计中对子命令的设计是由核心功能驱动的。在条件允许的情况下,尽可能压缩子命令列表是有利于用户上手使用的。为了命令行工具使用时命令不会过长,建议提供比较常用的默认选项值从而减少用户自定义的可能性。当然,对全部选项的单字母标志支持也是有效缩短命令长度的方法之一。除此之外,提供配置文件也是非常可取的方法。gitcurlwgetdocker 等都提供对应的配置文件 .gitconfig.curlrc.wgetrc.dockerrc 进行全局配置定义,当然 pictl 也提供 .pictlrc 配置文件。

代码架构

  尽管不同编程语言因为自身原因(编译型或解析型语言,面向对象或面向过程等),可能会有不同的代码架构偏好,我们可能依然可以采用一个宽泛且可行的通用代码架构思路——面向功能开发。这里的功能可以是一个操作或者一个对象,比如说压缩图片这样一个功能,主要是将输入图片转换成想要的图片格式输出。除此之外,可能还可以提供压缩质量控制、尺寸控制、自动重命名、自动加水印等微调功能。这些微调功能虽然实际上可以完全独立,但由于是压缩图片功能的附属功能,最好采用选项调用的方式来实现。代码架构上,子命令调用对应的函数会成为顶级函数。其他微调功能虽然是独立函数或对象,但仅在子命令函数中被调用。实际开发过程中,微调功能并非一开始就包括所有,大部分会作为一些特性逐步增加到主代码中。这意味着,对用户来说新增一个微调功能仅仅多了一个子命令下的选项支持,不需要重新学习和适应。

  如果压缩图片功能的基础(图片转换)需要自行编写代码,那么可能需要考虑到很多种图片格式的相互转换,这在具体代码实践中是非常麻烦的。比较可行的方法之一是,可以采用一种图片格式作为中间标准格式,每次新增一种图片格式的支持只需要增加与中间标准格式的相互转换即可。当然这里采用的中间标准格式可能是需要高保真的(或者高分辨率的),避免在用了中间标准格式转换之后图片质量自动下降。

错误处理和自动建议

  当我们打算开发一款命令行工具时,除了核心功能是必不可少的,错误处理和自动建议也是需要考虑在内的。了解这点最简单的办法就是从实例中学习。如下所示,是将 git 提交修改(commit)的命令 git commit 故意打成为 git commi 的输出结果。

╰─$ git commi
git: 'commi' is not a git command. See 'git --help'.

The most similar commands are
 commit
 column
 config

  当命令行工具接收到用户输入时,首先需要做的就是对输入的合法性进行验证:一方面,是否存在不可用的子命令或非法使用(比如错误迭代调用);另一方面,尝试解析参数并验证完整性。这两部分的验证会尽可能地将错误的原因和可能有用的建议提示给用户。类似上面,命令行工具会提示用户使用的子命令不存在,请使用 git --help 了解更多。另外,会将输入的子命令字符串与所有合法的子命令字符串进行对比,根据相似性大小排列向用户自动建议。至于对选项的解析相对来说可以比较宽容一点,即直接忽略不合法的选项声明、仅读取合法的选项声明,因此可以不返回相关错误提醒及帮助。

  当子命令、参数、选项均通过验证之后,命令行工具的功能代码执行时也会发生错误。如下所示,在一个非 git 项目文件夹内执行 git commit 时,命令行工具会将具体的错误直接提示:当前目录或任何父目录不是一个 git 项目,不存在 .git 文件夹。这里需要注意的是,通常我们可能对错误或异常的处理会直接使用编程语言本身提供的方式,比如 Python 语言中的 raise ValueError("'element' parameter is not defined.")。当然这种错误处理本身没有任何问题,只不过同时还会输出错误发生的代码位置等其他与用户使用无关的信息,尽管这种信息在开发过程中有利于开发者调试代码。出于为用户考虑,错误处理信息默认应该以简单可读的方式打印出来、且仅限于提示关键信息。如果用户或者开发者想要了解更多,可以通过 -v 或者 --verbose 选项来打印更多调试信息。

╰─$ git commit
fatal: not a git repository (or any of the parent directories): .git
-v 和 -V

  通常来说,-v--verbose)和-V--version)会被认为是不同的选项,分别用于开启调试模式和打印版本信息。当用户发现命令行工具使用出现不可预知的问题(错误提示与实际原因不符或其他不在开发者知晓范围内的问题)时,需要开启调试模式来排除本地环境的问题,同时也可能在向开发者反馈问题时提供足够的信息来帮助定位问题的原因。版本信息通常也是提交反馈问题时所需的必要信息。

技术选型

  技术选型指的是用哪种编程语言以及哪种框架来实现命令行工具第一种是从开发者自身熟悉和掌握的编程语言出发,尽可能降低编程语言上的学习成本。不过对于已经掌握一门或多门编程语言的开发者来说,学习新编程语言可能也不是件难事。第二种是从应用场景出发:如果是开发为机器学习应用的前置或后置步骤的数据准备、数据处理、可视化等功能的命令行工具,采用 Python 语言可能会更加方便有效;如果是开发包含与操作系统交互的网络分析、磁盘分析等功能的命令行工具,采用 Rust 或者 Golang 语言可能会比较得心应手;如果是开发与平台接口交互(如 Web API 调用)的命令行工具,采用 NodeJS 语言可能适配性更佳。详细请查看参考资料 [1-5]。

结语

  诚然,命令行工具并非是所有用户的喜爱,但的确是最小化操作步骤、提升效率的方式之一。以上所述的命令行工具开发指南入门篇大部分是在理论层面上的,至于在不同编程语言上的实践后续也计划展开:

  • 《命令行工具开发指南 —— Python 实践篇》
  • 《命令行工具开发指南 —— Rust 实践篇》
  • 《命令行工具开发指南 —— Golang 实践篇》
  • 《命令行工具开发指南 —— NodeJS 实践篇》

  有关于命令行工具开发进阶的依赖管理、编译构建、信号和日志处理、用户输入验证、自动补全(Auto Completion)、文档、测试和发布等也将在实践篇中分别具体阐述。

参考资料

  1. 快手数平前端团队 – 掌握 Node CLI 工具开发,为团队研发提效!
  2. 阮一峰的网络日志 – Node.js 命令行程序开发教程
  3. Rust 中的命令行应用
  4. 命令行应用 - Python 最佳实践指南
  5. Tony Bai – Go 开发命令行程序指南
  6. Wikipedia – POSIX

图片处理及上传命令行工具 —— PICTL

2023-06-13 12:41:01

前言

  自从博客从 WordPress 转到静态博客(先 Hexo 后 Jekyll)之后,文章的图片处理、图片上传就成了一个不大顺畅的事情。最先是使用了 vgy.me 提供的免费图床,支持直接从剪切板上传,操作上相对比较简单,也不需要任何本地存储。不过后来 vgy.me 进行了升级改版,原先的剪切板上传功能也不再支持了,偶尔还出现图片像素被降低、丢失的问题。同时,考虑到 WebP 格式可能会适合博客使用,而 vgy.me 还不支持该格式。于是开始切换到 “对象存储 + CDN” 的方案。本地准备好的 PNG 格式图片,先通过 cwebp 命令行转成 WebP 格式图片,再通过 uPic 工具修改文件名后上传到对象存储。由于此前采用的是腾讯云的 COS 对象存储和 CDN,经常面临着 SSL 证书更新等琐碎的事情。这样一来,整体的效率实际上并不高,只能说勉强接受。

现有工具及存储考察

  为此,也对其他工具和对象存储进行了考察。

  PicGo 是一款集客户端 UI 和命令行于一体的图片上传工具,虽然可以利用命令串联的方式简单将图片格式转换和上传两步变成一步,但还是有那么点不舒服的地方,比如 PicGo 不提供文件名修改(为固定长度随机字符串)的特性。而 uPic 本身关注于客户端桌面交互,不提供命令行接口。

  至于对象存储,国内各家云服务厂商提供的都需要自行手动更新 SSL 证书,且收费。尽管腾讯云 CDN 目前老用户可以每月领券免费使用,但一旦忘记就开始被收费了。实际上也有逐步转向收费的趋势。国外各家云服务厂商基本上都需要收费使用对象存储或者 CDN,大差不差。

  当然,网上一直有一种 “Backblaze B2 + Cloudflare” 的解决方案。由于 B2 本身存储和读写操作都有免费额度,流量需要收费,且无法自定义域名,而 B2 和 Cloudflare 同属于宽带联盟(内部流量免费),Cloudflare 还提供 URL 重写功能,这种解决方案一时成为了潮流。不过,B2 的访问链接是中间带着一串参数,重写之后仍然还是有部分参数,最终的 URL 并不是那么朴素。总的来说,还是有那么点别扭。

R2 的出现

  因此观望了许久,直到 Cloudflare 推出了 R2。R2 是一款对标 AWS S3 的产品,基于 Cloudflare 对宽带联盟的承诺而构建,为存储对象提供零成本出口,实际上就是免费 CDN。由于 Cloudflare 本身就是一家 CDN 服务商,自定义对象存储访问域名、自动生成部署 SSL 证书这些事情就变得轻而易举了。R2 提供 10GB 的免费存储,读操作每月免费 1000 万次,写、更新和删除操作每月免费 100 万次。这对于一般的静态博客来说,应该完全足够了,即使超出了免费额度,超出部分收费也相较其他云服务厂商便宜一些。

  对笔者而言,R2 产品将对象存储和 CDN 两款产品有机地结合起来,解决了静态博客图片对外访问前的“最后一公里”。于是想从 COS 迁移到 R2,无奈 uPic 这个时候卖了个“破绽”。uPic 似乎在开始转向商业收费,Github 上不再发布新版本,而仅在 Appstore 上继续更新对 R2 的支持。当然,Appstore 上的 uPic 是收费的(4.99 美元,其实也不贵)。

小提示

  R2 虽然说是对标 AWS S3,但是并没有完整实现所有 API 接口,所以目前 Github 上发布的 uPic 版本无法兼容。尽管可以自行通过修改 uPic 的开源代码来实现兼容,但毕竟修改别人的代码的成本还是有一点高的。

开个新项目

  在充分梳理个人使用需求之后,还是决定开个新项目——开发一款简单易用的命令行工具 PICTL(全称 Picture Control)。该工具须具备以下特性:

  • 考虑图片的存储空间大小和网页加载时长,所有图片应被压缩并转成 WebP 格式;
  • 所有图片的名称应被修改成一个固定长度随机字符串,并支持上传到类 S3 的对象存储;
  • 简单易用,命令行优先,无网页或桌面交互界面;
  • (可选)自动添加水印;
  • (可选)根据设定自动调整图片尺寸;
  • (可选)可以直接从剪切板读取图片。

架构设计

  根据所列出的特性,这款命令行工具主要的模块就是:图片处理模块上传模块,架构如下图所示。

PICTL 架构图 The architecture of PICTL

  图片处理模块包括图片压缩、图片格式转换、图片水印、尺寸调整等功能,可以进行无缝横向扩展。上传模块主要包括对于三种存储方式的支持:第一类是最为广泛的类 S3 对象存储,如 R2、AWS S3 及国内外云服务厂商各种对象存储等,第二类是目前仍然在博客中广泛流行的第三方图床,如 SM.MSvgy.mechevereto 类型图床等,第三类是自托管 Git 平台和 FTP 平台。

特此声明

  鉴于 Github、Gitlab、Gitee 等公共代码托管平台均禁止把 Git 仓库作为图床的做法,本工具仅支持自建 Git 平台,如自建 Gitlab 和 Gitlab Pages。如试图上传到公共代码托管平台,本工具会自行中断上传。

  本工具由于仅支持命令行,所以计划用 Python 和 Click 进行开发。虽然借助 Google Fire 也可以快速开发命令行工具,但其使用方式上与原生 *nix 的命令行工具有所不同。相比之下,用 Click 开发可能会麻烦一点,但能够开发出更类原生的 Python 命令行工具。当然,目前开发上还是比较喜欢使用 Rust 或者 Go 语言来开发高性能的命令行工具。不过,本工具只是做一些非常简单的工作,没有性能上的瓶颈,用 Python 开发也足够了。

开发计划

  请移步 Github Project 了解更多。

使用方法

  目前,PICTL 已经在 GithubPyPI 同步发布了 v0.1.0 版本。该版本实现了以上架构图中的基本功能,即特性中的前三点必要项。接下来介绍一下如何安装和使用本工具。

安装

  目前支持两种安装方式:源码安装PIP 安装,后续将增加对于 brew 的支持。

小提示

  安装前,请务必确保已满足 Python 版本高于 3.10 的条件。

源码安装

git clone https://github.com/zhonger/pictl
cd pictl
pip3 install .

PIP 安装

pip3 install pictl

使用

显示版本

╰─$ pictl -V
PICTL 0.1.0

列举所有支持命令

  目前 v0.1.0 版本包含四个子命令:config(配置管理),compress(图片处理),upload(上传文件)和 cup(一键式压缩、转换、上传)。

╰─$ pictl
Usage: pictl [OPTIONS] COMMAND [ARGS]...

  A command line tool for image processing and uploading (ex. S3-type).

  Now it supports:
    - transformation from other image types to `webp` image as well as
      image compression.
    - image file uploading to AWS S3 or Cloudflare R2.

Options:
  -V, --version  Show the pictl version.
  -h, --help     Show this message and exit.

Commands:
  compress  Compress any image into `webp` image.
  config    Operations for the config file `~/.pictlrc`.
  cup       Compress image and upload to remote storage (compress and...
  upload    Upload the file to remote storage

config 子命令

  config 子命令包含 add(添加配置)、delete(删除配置)、info(查看所有配置)和 init(初始化配置)四种操作。使用本工具需要先执行初始行配置操作,再执行其他配置操作。

╰─$ pictl config
Usage: pictl config [OPTIONS] COMMAND [ARGS]...

  Operations for the config file `~/.pictlrc`.

Options:
  -h, --help  Show this message and exit.

Commands:
  add     Add configs to the config file.
  delete  Delete config group from the config file.
  info    Check the configs.
  init    Initialize config file with default configs.

  如下所示,使用 pictl config init 命令初始化配置。第二次执行时,由于已存在配置文件,会提示已经初始化过了。

╰─$ pictl config init
The settings has been initilized in /home/ubuntu/.pictlrc.

╰─$ pictl config init
/home/ubuntu/.pictlrc already exists.
Please add settings or change it manaully.

  使用 pictl config info 以 JSON 格式打印所有配置信息。配置文件默认采用 TOML 格式。

╰─$ pictl config info
{
    'basic': {'length': 6, 'ntype': 'random', 'algorithm': 'sha1'},
}

╰─$ cat ~/.pictlrc
[basic]
length = 6
ntype = "random"
algorithm = "sha1"

  使用 pictl config add 以命令行交互方式添加配置(以下是 R2 的例子)。添加完成后可以再次查看新增后的所有配置。

╰─$ pictl config add
****** Please input these information ******
Group Name (default 'blog'): test
[?] Type: : R2
   S3
 > R2

Account ID: testid
Bucket Name: test
Key: testkey
Secret: testsecret
Prefix (Default is None):
Access Url (like `https://i.example.com`): https://i.example.com
New group 'test' has been saved in /Users/zhonger/.pictlrc.

╰─$ pictl config info
{
    'basic': {'length': 6, 'ntype': 'random', 'algorithm': 'sha1'},
    'test': {
        'endpoint': 'https://testid.r2.cloudflarestorage.com/test',
        'bucket': 'test',
        'prefix': '',
        'key': 'testkey',
        'secret': 'testsecret',
        'url': 'https://i.example.com',
        'type': 'R2'
    }
}

  使用 pictl config delete 命令可以选择删除远程配置组(此处为了展示,除 test 外还添加了其他配置组)。

╰─$ pictl config delete
[?] Please select one group: : test
   blog
   cover
 > test

test has been deleted.

compress 子命令

  本子命令输入参数为图片文件名,可以带路径。输出图片会被保存在命令执行位置,而非原图片所在目录。执行该操作后,图片还不会被上传,需要继续使用 upload 命令完成上传。

╰─$ pictl compress -h
Usage: pictl compress [OPTIONS] FILENAME

  Compress any image into `webp` image.

  FILENAME is the name of the file to compress.

Options:
  -h, --help  Show this message and exit.

╰─$ pictl compress ../../pictl/fig01.png
The output file is EVyP2J.webp

upload 子命令

  如下所示,使用 pictl upload 命令加上指定上传文件名和远程配置组即可完成上传。

╰─$ pictl upload -h
Usage: pictl upload [OPTIONS] FILENAME GROUP

  Upload the file to remote storage.

  FILENAME is the name of the file to upload.
  GROUP is the group in the config file you want to use.

Options:
  -h, --help  Show this message and exit.

╰─$ pictl upload EVyP2J.webp test
Direct URL: https://i.example.com/EVyP2J.webp
Markdown: ![EVyP2J.webp](https://i.example.com/EVyP2J.webp)
HTML Code: <img src="https://i.example.com/EVyP2J.webp" alt="EVyP2J.webp" />

cup 子命令

  cup 子命令 = compress 子命令 + upload 子命令,仅需要指定初始图片文件和远程配置组,即可一步完成图片压缩、格式转换、修改名称、上传。

╰─$ pictl cup -h
Usage: pictl cup [OPTIONS] FILENAME GROUP

  Compress image and upload to remote storage (compress and upload).

  FILENAME is the name of the file to compress.
  GROUP is the group in the config file you want to use.

Options:
  -h, --help  Show this message and exit.

╰─$ pictl cup ../../pictl/fig01.png test
The output file is Gl8qhI.webp
Direct URL: https://i.example.com/Gl8qhI.webp
Markdown: ![Gl8qhI.webp](https://i.example.com/Gl8qhI.webp)
HTML Code: <img src="https://i.example.com/Gl8qhI.webp" alt="Gl8qhI.webp" />

TODO

  目前的 v0.1.0 版本还比较简陋,不过已经能满足笔者的必要需求,有效提升效率。后续将继续完善、新增功能:

  • 自动补全(shell completion),增强命令行使用体验。
  • 增加对各云服务厂商提供对象存储的支持。
  • 增加对各流行图床的支持。
  • 增加对自托管 Git 的支持。
  • 增加对 FTP 的支持。
  • 增加包括水印、调整大小等在内的图片处理的支持。
  • 增加与 Unplash 等公共图库的连接,更加便于博客封面的操作。

参考资料

通用运行环境版本管理神器 ASDF

2023-03-20 14:50:00

前言

  之前介绍了高性能集群中常用的运行环境和软件版本管理工具 Modules,今天打算介绍一款适合个人或团队开发使用的通用运行环境版本管理神器 ASDF。与高性能计算任务不同,个人或团队开发项目一般来说都是使用独立的设备或环境,然后通过代码版本跟踪 git 等来进行异步协作。所以说,在每个人的单个或多个设备上都安装配置 Modules 显得有点不太现实和高效。但是项目开发所需的代码环境确实有的时候可能比较复杂,比如说同时需要 Ruby、NodeJS、Java、Python 四种环境,而且可能对于每种环境还有版本的限制。这样一来,光配置这一堆环境就要花上大半天时间了。

ASDF 简介

  ASDF 提供了全平台通用的环境配置方案,使用单个命令行工具和交互界面就可以管理超复杂的运行环境。以往针对不同运行环境,需要使用不同的配置文件来进行版本的声明。对于 ASDF,只需要一个可共享的 .tool-versions 配置文件即可。ASDF 涵盖了包括 Ruby、NodeJS、Java、PHP、.Net 在内的几百种运行环境,具体可以查看 ASDF 插件列表 了解更多。

  另外,ASDF 完全支持包括 Bash、Zsh、Fish 和 Elvish 在内的常用 shell 类型,并提供补全功能。在类似 Github Actions 等的 CI/CD 工作流中,也可以轻松使用 ASDF。值得一提的是,笔者翻译了 ASDF 文档的中文版本并被官方采纳,现在 ASDF 官网支持英语、巴西语和中文三种语言。

为何不选其他方案

Docker

  很显然,如果大家的设备上都有 Docker 环境且 CPU 架构相同的话,Docker 无疑是最省心的方案。Docker 镜像的确可以轻松涵盖所有开发环境和实际运行环境(Apache 等 HTTP 服务器、数据库以及其他)。相比笨重的 VMware 或 Virtualbox 虚拟机镜像而言,Docker 镜像也更加小巧、便捷。而且团队可以通过在内部搭建自己的 Docker 镜像仓库,来分享这些镜像给所有参与项目的开发者。唯一可能会有问题的是,设备 CPU 架构和操作系统的多样性可能会给实际操作带来了不小的问题。实际上可能会有 Windows 系统、Linux 系统、MacOS 系统以及 Intel 架构、AMD 架构、ARM 架构(如 M1、M2 等)。可行的解决方法是,尽可能地构建更多架构的镜像。

Anaconda

  Anaconda 现在可能已经完全超出了一个 Python 环境管理工具,有的时候也可以当成通用软件或环境管理器来用。但是毕竟还是以科学计算为主要目的,如果项目仅仅是 Python、R 语言可能还是比较合适的,对于实际编程所需的其他运行环境来说可能还是支持不够的。

小提示

  据笔者所知,在 conda-forge 频道里的确有 PHP 等编程语言的支持。除此之外,也有一些热心开发者在个人频道提供了 java-jdk、golang 等编程环境支持。

云开发

  云开发主要是指基于云基础设施的在线代码开发环境,主要的代表有:

  • Cloud9:笔者最早接触过的云开发,目前已被 AWS 收购,更名为 AWS Cloud9。
  • Codespaces:由 Azure 提供云服务、Github 负责运营的一站式云开发环境。
  • Gitpod:基于 VS Code 研发的优秀云开发环境,早期以“便捷、快速”著称,较先于 Codespaces 出现。
  • Cloud Studio:由腾讯云提供云服务、基于 VS Code 的国产云开发环境,能够很好地支持个人开发、招聘笔试、课堂教学、应用快捷部署等各种场景。
  • CodeArts IDE Online:由华为云提供云服务、基于 VS Code 的国产云开发环境。除了一般云开发所具备的特点,还支持华为鲲鹏原生环境,能够很好地满足跨架构应用开发的需求。
  • DevStudio:由阿里云提供云服务、基于 VS Code 的国产云开发环境。支持应用开发全流程管理,与阿里云各项基础服务紧密结合,适合大规模团队使用。

除了以上列举的云开发之外,也有一些比较传统的小型云开发实践,比如说知名的 JSFiddleCodePenReplit 等。虽然说这些云开发主要是适合较小代码库,但在实际学习过程中用处也是很大的。

  云开发不仅兼顾了传统开发过程中的协同与流程,又将资源与环境整合在云里面,自然而然是最好的解决方案。随着 VS Code 在开发者之间的流行和云服务提供商的努力,基于 VS Code 的云开发环境层出不穷。即使云开发环境本身免费,云也还是要按量按时计费的。对于还没有足够支持上云的团队或个人来说,ASDF 依然是个不错的选择。

实践

环境需求

  • Linux/Unix 环境(Linux、MacOS、Windows WSL)
  • git
  • bash 等(此处以 zsh 为例)

环境配置

# 下载源码到 ~/.asdf 目录
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.11.3

# 在 ~/.zshrc 文件中加入内容
tee -a ~/.zshrc << EOF
. "$HOME/.asdf/asdf.sh"
EOF

# 激活配置
source ~/.zshrc

# 验证
╰─$ asdf version
v0.11.3-0adc6c1

安装插件

  由于 ASDF 支持插件较多,这里以 Python 环境为例介绍 ASDF 安装插件。

# 添加插件
asdf plugin add python

# 查看已安装插件
╰─$ asdf plugin list
python

# 查看最新 Python 版本
╰─$ asdf latest python
3.11.2

# ASDF 安装 Python 3.11.2 (latest)
╰─$ asdf install python latest
python-build 3.11.2 /home/ubuntu/.asdf/installs/python/3.11.2
Downloading Python-3.11.2.tar.xz...
-> https://www.python.org/ftp/python/3.11.2/Python-3.11.2.tar.xz
Installing Python-3.11.2...
Installed Python-3.11.2 to /home/ubuntu/.asdf/installs/python/3.11.2

# 查看已安装 Python 版本列表
╰─$ asdf list
python
  3.11.2

使用

  ASDF 提供全局版本(Global)和本地版本(Local)两种方式定义运行环境版本。全局版本是系统级别的,类似于 PATH 变量中定义的;本地版本则是为了某个代码库或者部分代码准备的,通常在目录中的 .tool-versions 文件里定义。为了区分全局和本地的效果差别,这里再安装一个指定 Python 版本。

# 查询 Python 插件支持的所有版本
╰─$ asdf list all python
2.1.3
2.2.3
......
stackless-3.7.5

# 安装 Python 3.9.0
╰─$ asdf install python 3.9.0
python-build 3.9.0 /home/zhonger/.asdf/installs/python/3.9.0
Downloading Python-3.9.0.tar.xz...
-> https://www.python.org/ftp/python/3.9.0/Python-3.9.0.tar.xz
Installing Python-3.9.0...
patching file Misc/NEWS.d/next/Build/2021-10-11-16-27-38.bpo-45405.iSfdW5.rst
patching file configure
patching file configure.ac
Installed Python-3.9.0 to /home/zhonger/.asdf/installs/python/3.9.0

# 查看已安装 Python 版本列表
╰─$ asdf list
python
  3.11.2
  3.9.0

# 查看当前系统 Python 及 Python3 版本
╰─$ python -V
No python executable found for python system

╰─$ python3 -V
Python 3.10.6

# 更改全局版本为 3.11.2 并查看
╰─$ asdf list
python
 *3.11.2
  3.9.0

╰─$ python -V
Python 3.11.2

# 创建子目录指定本地版本并查看
mkdir py && cd py
asdf local python 3.9.0

╰─$ python -V
Python 3.9.0

# 返回父目录查看 Python 版本
╰─$ cd .. && python -V
Python 3.11.2

# 查看当前系统 Python3 版本
╰─$ python3 -V
Python 3.10.6
小提示

  这里有一点比较有趣的是:由于 ASDF 接管的 python 命令而非 python3 命令,所以 python3 命令输出的版本依然还是系统安装版本。

其他相关

  如果想要恢复到系统指定版本,可以很容易使用 asdf global python system 命令。当然,对于本地版本,可以使用 asdf local python system 来实现。除此之外,还有一些常规操作如下。

# 查看 Python 指定版本安装位置
╰─$ asdf where python 3.11.2
/home/zhonger/.asdf/installs/python/3.11.2

# 查看命令所在位置
╰─$ asdf which python
/home/zhonger/.asdf/installs/python/3.11.2/bin/python

# 查看当前 ASDF 管理的运行环境
╰─$ asdf current
python          3.11.2          /home/ubuntu/.tool-versions

# 查看全局和本地版本配置文件
╰─$ cat ~/.tool-versions
python 3.11.2

╰─$ cat ~/py/.tool-versions
python 3.9.0

参考资料