用容器开发

用容器开发

2022/6/186 min
张筱

张筱

AI工程师

tl;dr: 我的开发环境全在这个docker 镜像

背景

作为一个全职开发者,我经常需要在办公室和家里交替办公。由此导致了一个问题:如何同步家里和办公室里的开发环境?

另一个每天都会遇到的问题是计算资源的紧缺。我的电脑经常在跑一两个重进程时 OOM。

最佳的效果是只需要配置一次开发环境,而且部署起来轻量简单,在遇到问题时可以很容易地用重置来解决。

思考及设计

最老土的办法是用脚本自动化所有人工配置的步骤。但是在不同平台上的脚本很难统一维护。

这就是容器有用的地方了,因工作需要,我对容器有一定的了解。容器的以下特性有助于解决此问题:

  1. 只需要考虑 1 个平台。
  2. 在所有主流操作系统上都能运行
  3. 重启很简单

IDE

第一个难点是 IDE。vscode 是桌面应用,而容器对 GUI 的支持很弱,因此我选择 Neovim。在 Linux 平台上我最熟悉的编辑器就是 vim 系列,选择 Neovim 而非 Vim 8 的理由是它由社区维护。

Neovim 现在提供内置的 LSP 支持,只需要从网上搜寻一些配置即可。我的配置在这里

部署

更重要的问题是如何使用准备好的容器镜像。不能简单地用docker run --rm -it onichandame/dev bash启动一个容器然后就直接开始开发,因为容器的数据没有持久化,重启就丢了。最简单粗暴的方案就是挂载宿主机的路径,但是我必须考虑在不同地点做开发,办公室的宿主机只能在办公室范围内提供服务。

答案是 Kubernetes(k8s)。一旦决定采用 k8s 问题就变得简单了,因此我不会提供所有细节。简单来说,我将容器部署在公网上的集群,因此在任何地点只要本地有 kubectl 都能直接使用。小提示:考虑 statefulset + ingress。

本地配置

在完成上面所有工作后,我仍然需要对本地机器做一些配置以使用远端的环境。这里只记录 Windows 系统的配置:

  1. Nerd Font
  2. Git
  3. NeoVim
  4. kubectl
    1. 集群的 kubeconfig
  5. Windows Terminal

其它尝试

在确定最终方案之前,我尝试过很多其它路子,也学到了很多教训。

本地容器

在发现 k8s 之前,我试过在不同平台本地跑容器。

Podman

Podman 是一个无 daemon 的容器运行环境。为绕过 SELinux 对硬盘读写的限制,需要使用--security-opt label=disable选项。

Podman 和 Docker 对容器有不同的定义。Podman 不会为每个容器分配一个新 IP,因为分配新 IP 需要 root 权限。为联通两个非 root 容器,需要使用一个 pod 将两个容器包裹起来。官方文档提供创造容器和 pod 的具体方法。官方文档提供同一 pod 内不同容器的通信方法。但是,新建 pod 需要从 k8s.gcr.io 拉取镜像,在国内由于墙这不可能。因此必须用一个国内镜像。

man container-registries.conf告诉我镜像的配置在/etc/containers/registries.conf文件中。这个路径在不同的发行版上可能不一样。在 CentOS 8 提供的默认配置中使用的是 v1 版本。但是镜像配置只在 v2 版本中可用,因此必须将配置转换为 v2 版本:

unqualified-search-registries = ['docker.io', 'registry.access.redhat.com', 'registry.fedoraproject.org', 'registry.centos.org']

[[registry]]
prefix="k8s.gcr.io"
location="k8s.gcr.io"

[[registry.mirror]]
location="registry.cn-hangzhou.aliyuncs.com/google_containers"

感谢此问题

在删除旧配置并添加新配置后,可以用阿里云镜像创造 pod。

在 pod 内的容器无法直接与 host 通过 port mapping 通信,因此必须在创建 pod 时就将 port mapping 设置好。

podman pod create -p 3000:3000

本地 Kubernetes

在拥有公网集群之前,我只能先用本地 k8s 集群。

Minikube

官方推荐使用minikube配置开发环境。但它严重依赖虚拟机技术,因此相对于其它选择,minikube 需要消耗更多资源。

Kind

kind在本地容器中启动一个 k8s 集群。它提供一个可执行程序用以管理集群,可以根据官方文档的指示进行安装。

下一步是安装 ingress 控制器。基于我的经验,将服务与端口一一对应非常不便于管理。而将不同服务由同一个端口的不同域名对应则更方便。

基于官方文档,有 3 种控制器可选:Ambassador, Contour 和 Nginx。基于我的实验结果,仅 ambassador 在中国可用。要安装 ambassador 控制器,执行以下命令。

kubectl apply -f https://github.com/datawire/ambassador-operator/releases/latest/download/ambassador-operator-crds.yaml
kubectl apply -n ambassador -f https://github.com/datawire/ambassador-operator/releases/latest/download/ambassador-operator-kind.yaml

在命名空间ambassador中的所有 pod 都上线后,控制器即为可用状态。

一个需要注意的点是,所有需要使用 ambassador 的 ingress 都需要标注为kubernetes.io/ingress.class: ambassador

要安装其它 ingress controller,管理员需要确保 gateway 安装在 extra port mapping 规则所在的 node 上。

因为 Istio 提供许多有关监控的功能,我选择使用 Istio。

  1. 运行curl -L https://istio.io/downloadIstio | sh -下载 Istio 的 CLI 客户端
  2. 运行istioctl install -f istio.yamlistio.yaml从此下载

现在 Istio default 已经安装完成。

最后一个手动步骤是将 namespace 标记上istio-injection=enabled,用以通知 Istio 向该 namespace 的每一个 pod 注入 Sidecar。这个步骤是大部分 Istio 核心功能的前提。

一个尚未完全解决的问题是当宿主机重启后,coredns 服务可能停止解析域名。一个临时的解决办法是在宿主机重启后手动重启 coredns 服务。

kubectl rollout restart deploy/coredns -n kube-system

K3D

K3dRancher的一个产品,它基于一个超轻量的 kubernetes 发布版:k3s。因此其适合在低资源消耗的场景中应用,如 IoT 和本地测试环境等。就像Kind一样,K3d利用容器技术模拟多 node 集群,但消耗更少的资源。

K3d依靠命令行参数实现个性化配置。我使用如下的命令:

k3d cluster create --no-lb --k3s-server-arg='--disable=traefik' -p 80:30001@agent[0] -p 443:30002@agent[0] -a 3 -v <path to workspace on host>:/git -v <path to registries configuration file>:/etc/rancher/k3s/registries.yaml dev

外部负载均衡和 traefik 都被禁用,因为我希望使用 istio 来代替。端口映射的参数也是为 istio 安装做准备。参数-a 3指定需要 3 个 worker node。volume mount 参数将宿主机的工作区加载到所有 node 上,以提供稳定的数据存储。加载到/etc/rancher/k3s/registries.yaml的配置文件将官方源如 docker.io 和 gcr.io 替换为镜像源以提升拉取镜像的速度。

istio 的安装方法如上所示。

Docker Desktop

Docker 最初是为 Linux 设计的,因此在 Windows 上运行 Docker 总会出问题。本节我记录了所有在 Windows 10 上遇到的问题及其解决方案。

Docker Engine

Docker Desktop 可以基于 2 个引擎:HyperV(旧引擎)和 WSL 2。

HyperV 就是一个传统的 VM hypervisor,Docker 用它建立一个跑 docker 后台的虚拟机。

而 WSL 2 是一个深度集成的 Linux 内核,它比其它方案更快。因此我选择使用它。

第一步是安装 WSL 2,点击此处获取官方教程。

安装完毕 WSL 2 后,勾选 Docker Desktop 的 General settings 中的Use the WSL 2 based engine选项,然后重启 docker 后台让更改生效。注意:重启后台后旧后台上的所有镜像都会删除!

当前版本的 WSL 2 有一个恶性 bug。如果你启动一个基于 WSL 2 的 Linux 发行版时,显示错误信息the attempted operation is not supported for the type of object referenced.,这意味着 WinSock 导致 WSL 2 崩溃了。基于此问题,以管理员模式运行netsh winsock reset可以永久解决这个问题。

HyperV

无论选择使用 HyperV 还是 WSL 2,HyperV 服务都必须被启用。但是微软做了个非常蠢的设定,在 HyperV 被启用后,BIOS 里的虚拟化会被关掉。因此在每次启用 HyperV 后,都必须手动进入 BIOS 启用虚拟化。

感谢此问题

其它

防止容器关闭

容器都会在 PID 1 的进程退出时自动关闭。对使用docker run -it --rm的无状态容器来说这不是问题。但在 kubernetes 中,所有容器都需要在后台运行并用kubectl exec -it进入容器进行交互。

为永久运行一个容器,有两种方法:

  1. tail -f /dev/null
  2. bash

第一个选择的问题在于它无法对 SIGKILL/SIGINT 正常响应,因此 pod 将在Terminating状态下卡死。

第二个选择的问题在于它需要一个 pseudo-tty 以防止进程关闭。

综合考虑,tail方法的问题无法找到一个简单的解决方法。但bash方法的问题可以用提供 pseudo-tty 解决。

缩减镜像大小

最终镜像的大小主要取决于 Linux distro 的选择。传统的 distro 如 CentOS 没有针对容器化做优化,因此基于它的镜像的大小都很容易失控。我选择 Alpine 3。基于 Alpine 3 的镜像大概在 1.8 GB 左右,但基于 CentOS 的版本超过了 3 GB。

NOFILE 限制

在开发前端时,开发服务器需要监控许多源文件的变动,在源文件很多时,经常会超出默认的打开文件数量限制。

文件数量限制由一个硬限制和一个软限制组成。当超过软限制时,用户会收到警告;当硬限制被突破时,系统会报错。因此两种限制都需要提升。

我的解决方案是提升宿主机的 NOFILE 限制,容器会自动继承宿主机的配置。详细步骤在<a href={'./ulimit'}>此文中。

UTF-8 编码

为提升 performance,大多数基础镜像都只支持 ASCII 以尽量缩减大小。这在宿主机支持 UTF-8 且容器直接将字符输出至屏幕时没有问题。但当容器内使用 tmux 时,tmux 先将字符解码,再将解码失败的乱码发送至屏幕。

解决的第一步是在容器中添加特殊字符支持。我需要添加简中,因此我安装了glibc-langpack-zh。运行locale -a以检查是否安装成功。

最后一步是选择默认编码。我在 bashrc 中添加了export LANG="zh_CN.UTF-8"export LC_ALL="zh_CN.UTF-8"

端口映射

为从宿主机访问容器中的服务器,最好将容器的端口映射到宿主机上。所需的命令仅仅是在创建容器时加入-p <host port>:<container port>。但是,端口映射的配置在容器创建后不能修改,因此容器中的服务器必须侦听同一个端口。但不同的项目有不同的配置,不能强行让项目的配置配合本地的容器配置。

一个解决方案是将导向映射的端口的请求导入服务器侦听的端口。socat 可以实现这个需求。

# redirect requests for the mapped port to the server port
socat TCP4-LISTEN:<mapped port>,fork,reuseaddr TCP4:localhost:<server port>