我开发了一个基于 Beancount 的账本托管服务 HostedBeans,欢迎大家来了解纯文本复式记账并试用我的服务。

为什么在 Apple Silicon 上装 Docker 这么难

最近公司的很多同事都换上了搭载 M1 Pro 或 M1 Max 的新款 MacBook Pro,虽然日常使用的软件如 Chrome、Visual Studio Code 和 Slack 都已经适配得很好了,但面对 Docker 却犯了难。

众所周知,Docker 用到了 Linux 的两项特性:namespaces 和 cgroups 来提供隔离与资源限制,因此无论如何在 macOS 上我们都必须通过一个虚拟机来使用 Docker。

在 2021 年 4 月时,Docker for Mac(Docker Desktop)发布了 对 Apple Silicon 的实验性支持,它会使用 QEMU 运行一个 ARM 架构的 Linux 虚拟机,默认运行 ARM 架构的镜像,但也支持运行 x86 的镜像。

QEMU 是一个开源的虚拟机(Virtualizer)和仿真器(Emulator),所谓仿真器是说 QEMU 可以在没有来自硬件或操作系统的虚拟化支持的情况下,去模拟运行一台计算机,包括模拟与宿主机不同的 CPU 架构,例如在 Apple Silicon 上模拟 x86 架构的计算机。而在有硬件虚拟化支持的情况下,QEMU 也可以使用宿主机的 CPU 来直接运行,减少模拟运行的性能开销,例如使用 macOS 提供的 Hypervisor.Framework

Docker for Mac 其实就是分别用到了 QEMU 的这两种能力来在 ARM 虚拟机上运行 x86 镜像,和在 Mac 上运行 ARM 虚拟机。

Docker for Mac 确实很好,除了解决新架构带来的问题之外它还对文件系统和网络进行了映射,容器可以像运行在本机上一样访问文件系统或暴露网络端口到本机,几乎感觉不到虚拟机的存在。但 LeanCloud 加入 TapTap 之后已经不是小公司了,按照 Docker Desktop 在 2021 年 8 月推出的 新版价格方案,我们每个人需要支付至少 $5 每月的订阅费用。倒不是我们不愿意付这个钱,只是我想要找一找开源的方案。

之前在 Intel Mac 上,我们会用 Vagrant 或 minikube 来创建虚拟机,它们底层会使用 VirtualBox 或 HyperKit 来完成实际的虚拟化。但 VirtualBox 和 HyperKit 都没有支持 Apple Silicon 的计划。实际上目前开源的虚拟化方案中只有 QEMU 对 Apple Silicon 有比较好的支持,QEMU 本身只提供命令行的接口,例如 Docker for Mac 调用 QEMU 时的命令行参数是这样:

/Applications/Docker.app/Contents/MacOS/qemu-system-aarch64 -accel hvf \
-cpu host -machine virt,highmem=off -m 2048 -smp 5 \
-kernel /Applications/Docker.app/Contents/Resources/linuxkit/kernel \
-append linuxkit.unified_cgroup_hierarchy=1 page_poison=1 vsyscall=emulate \
panic=1 nospec_store_bypass_disable noibrs noibpb no_stf_barrier mitigations=off \
vpnkit.connect=tcp+bootstrap+client://192.168.65.2:61473/f1c4db329a4a520d73a79eaa1360de7be7d09948a1ac348b04c8e01f6f6eb2c9 \
console=ttyAMA0 -initrd /Applications/Docker.app/Contents/Resources/linuxkit/initrd.img \
-serial pipe:/var/folders/12/_bbrd4692hv8r9bx_ggw5kp80000gn/T/qemu-console1367481183/fifo \
-drive if=none,file=/Users/ziting/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw,format=raw,id=hd0 \
-device virtio-blk-pci,drive=hd0,serial=dummyserial -netdev socket,id=net1,fd=3 -device virtio-net-device,netdev=net1,mac=02:50:00:00:00:01 \
-vga none -nographic -monitor none

为了实际使用 QEMU 进行开发,我们需要一个使用上更友好的封装,能够自动配置好 Docker 和 Kubernetes(或者至少方便编写像 Vagrantfile 一样的脚本),提供类似 Docker for Mac 的网络映射和文件映射,于是我找到了 Lima。

Lima 自称是 macOS 上的 Linux 子系统(macOS subsystem for Linux),它使用 QEMU 运行了一个 Linux 虚拟机,其中安装有 rootless 模式的 containerd,还通过 SSH 提供了文件映射和自动的端口转发。

但为什么是 containerd 而不是 Docker 呢?随着容器编排平台 Kubernetes 如日中天,社区希望将运行容器这个关键环节进行标准化,让引入 Docker 之外的其他容器运行时更加容易,于是 推出了 Container Runtime Interface (CRI)。containerd 就是从 Docker 中拆分出的一个 CRI 的实现,相比于 Docker 本体更加精简,现在也交由社区维护。

因此如 Lima 这样新的的开源软件会更偏好选择 containerd 来运行容器,因为组件更加精简会有更好的性能,也不容易受到 Docker 产品层面变化的影响。nerdctl 是与 containerd 配套的命令行客户端(nerdcontainerd 的末尾 4 个字母),用法与 docker 或 docker-compose 相似(但并不完全兼容)。

所谓 rootless 则是指通过替换一些组件,让容器运行时(containerd)和容器都运行在非 root 用户下,每个用户都有自己的 containerd,这样绝大部分操作都不需要切换到 root 来进行,也可以减少安全漏洞的攻击面。

但我们希望能在本地运行完整的 rootful 模式的 dockerd 和 Kubernetes 来尽可能地模拟真实的线上环境,好在 Lima 提供了丰富的 自定义能力,我基于社区中的一些脚本(docker.yamlminikube.yaml)实现了我们的需求,而且这些自定义的逻辑都被以脚本的形式写到了 yaml 描述文件中,只需一条命令就可以创建出相同的虚拟机。

~ ❯ limactl start docker.yaml
? Creating an instance "docker" Proceed with the default configuration
INFO[0005] Attempting to download the image from "https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-arm64.img"
INFO[0005] Using cache "/Users/ziting/Library/Caches/lima/download/by-url-sha256/ae20df823d41d1dd300f8866889804ab25fb8689c1a68da6b13dd60a8c5c9e35/data"
INFO[0006] [hostagent] Starting QEMU (hint: to watch the boot progress, see "/Users/ziting/.lima/docker/serial.log")
INFO[0006] SSH Local Port: 55942
INFO[0006] [hostagent] Waiting for the essential requirement 1 of 5: "ssh"
INFO[0039] [hostagent] Waiting for the essential requirement 2 of 5: "user session is ready for ssh"
INFO[0039] [hostagent] Waiting for the essential requirement 3 of 5: "sshfs binary to be installed"
INFO[0048] [hostagent] Waiting for the essential requirement 4 of 5: "/etc/fuse.conf to contain \"user_allow_other\""
INFO[0051] [hostagent] Waiting for the essential requirement 5 of 5: "the guest agent to be running"
INFO[0051] [hostagent] Mounting "/Users/ziting"
INFO[0051] [hostagent] Mounting "/tmp/lima"
INFO[0052] [hostagent] Forwarding "/run/lima-guestagent.sock" (guest) to "/Users/ziting/.lima/docker/ga.sock" (host)
INFO[0092] [hostagent] Waiting for the optional requirement 1 of 1: "user probe 1/1"
INFO[0154] [hostagent] Forwarding TCP from [::]:2376 to 127.0.0.1:2376
INFO[0304] [hostagent] Forwarding TCP from [::]:8443 to 127.0.0.1:8443
INFO[0332] [hostagent] Waiting for the final requirement 1 of 1: "boot scripts must have finished"
INFO[0351] READY. Run `limactl shell docker` to open the shell.
INFO[0351] To run `docker` on the host (assumes docker-cli is installed):
INFO[0351] $ export DOCKER_HOST=tcp://127.0.0.1:2376
INFO[0351] To run `kubectl` on the host (assumes kubernetes-cli is installed):
INFO[0351] $ mkdir -p .kube && limactl cp minikube:.kube/config .kube/config

我还发现了另外一个基于 Lima 的封装 —— Colima,默认提供 rootful 的 dockerd 和 Kubernetes,但 Colima 并没有对外暴露 Lima 强大的自定义能力,因此我们没有使用,但对于没那么多要求的开发者来说,也是一个更易用的选择。

在默认的情况下,Lima 中的 Docker 在 Apple Silicon 上只能运行 ARM 架构的镜像,但就像前面提到的那样,我们可以使用 QEMU 的模拟运行的能力来运行其他架构(如 x86)的容器。qemu-user-static 是一个进程级别的模拟器,可以像一个解释器一样运行其他架构的可执行文件,我们可以利用 Linux 的一项 Binfmt_misc中文版)的特性让 Linux 遇到特定架构的可执行文件时自动调用 qemu-user-static,这种能力同样适用于容器中的可执行文件。

社区中也有 qus 这样的项目,对这些能力进行了封装,只需执行一行 docker run --rm --privileged aptman/qus -s -- -p x86_64 就可以让你的 ARM 虚拟机魔法般地支持运行 x86 的镜像。

/usr/bin/containerd-shim-runc-v2
\_ /qus/bin/qemu-x86_64-static /usr/sbin/nginx -g daemon off;
    \_ /qus/bin/qemu-x86_64-static /usr/sbin/nginx -g daemon off;
    \_ /qus/bin/qemu-x86_64-static /usr/sbin/nginx -g daemon off;
    \_ /qus/bin/qemu-x86_64-static /usr/sbin/nginx -g daemon off;
    \_ /qus/bin/qemu-x86_64-static /usr/sbin/nginx -g daemon off;

使用 qus 运行 x86 镜像的进程树如上,所有进程(包括创建出的子进程)都自动通过 QEMU 模拟运行。

回到题目中的问题,因为 Docker 依赖于 Linux 内核的特性,所以在 Mac 上必须通过虚拟机来运行;Apple Silicon 作为新的架构,虚拟机的选择比较受限,因为有些镜像并不提供 ARM 架构的镜像,所以有时还有模拟运行 x86 镜像的需求;Docker Desktop 作为商业产品,有足够的精力来去解决这些「脏活累活」,但它在这个时间点选择不再允许所有人免费使用;开源社区中新的项目都希望去 Docker 化,用 containerd 取代 dockerd,但这又带来了使用习惯的变化并且可能与线上环境不一致。因为这些原因,目前在 Apple Silicon 上安装 Docker 还是需要花一些时间去了解背景知识的,但好在依然有这些优秀的开源项目可供选择。

虽然 云引擎 也是基于 Docker 等容器技术构建的,但云引擎力图为用户提供开箱即用的使用体验而不必自己配置容器环境、编写构建脚本、收集日志和统计数据。如果想得到容器化带来的平滑部署、快速回滚、自动扩容等好处但又不想花时间配置,不如来试试云引擎。

其他参考资料:

2021 年度小结

2021 年新冠疫情仍然没有结束,我们甚至已经习惯了它的存在。

和蛋黄在昆山住了三年之后,今年最大的变化是我们搬到了上海。相比于之前在北京的一年,因为收入更高了也有条件在上海租更好的房子,离公司和地铁站都非常近,面积也并 不比昆山的房子小太多

因为在走之前我有些舍不得我投入了这么多精力改造的家,直到搬走前三天我们才开始高强度地打包收拾。这次我们的物品达到了 惊人的 1000 千克、7.9 立方米,到了上海后又断断续续花了一个月收拾,买了沙发、桌子、电视、洗衣机、新的净水器,再次自己安装了窗帘杆,为了将洗碗机和洗衣机放到理想的位置做了不少改造,对于选择哪个房间做卧室(同时也决定了我们的桌子如何摆放)也纠结了很久。在又投入了这么多精力之后,我发现我也不会去想到昆山了,毕竟当下才是自己最理想的家。

上海的确有更多的地方可以逛、可以玩,但因为我周末总是下午才起床,所以我们不经常去比较远的地方。而是就像 2020 年一样,在夜里骑着电动车说走就走,走遍了家附近方圆七、八公里的区域。

新的环境也确实对我的情绪有一些改善。想到和蛋黄刚在一起的时候就说「重要的是我们在一起并不需要做出什么改变就可以很开心,而不一定要按照社会的期待去改变自己」,我得承认在一些事情上我没有做到,有时候会因为觉得大家都可以这样做而去要求蛋黄。但她却一直是这样做的,会包容我所有和其他人不一样的地方,从未要求我改变什么。

在工作方面最大的变化则是 LeanCloud 被心动收购,团队整体加入了 TapTap,办公地点也搬到了上海。公司被收购是一种非常独特的体验,整个过程充满了不确定性,自己和同事们也都在考虑各自后续的变化 —— 其实说起来那两个月都没什么心思工作。最后尘埃落定,加入一个新的公司时,你既是老员工,也是新员工,也有了更多的时间去审视自己与公司的关系。

我一直对于大公司非常抵触,我也绝对不会加入像阿里巴巴、拼多多、华为这样的公司。当公司规模大到老板无法知道每一个人在做什么的时候,就会开始引入组织架构。这种变化一方面带来了效率的降低 —— 部门间的利益和公司的利益不一定是一致的,需要大量的管理来「对齐」;另一方面也带来了一种系统性的压迫 —— 公司与员工的力量是如此地悬殊,制定和执行规则的人也与员工离得更远,他们会充分利用合同和协议将风险全部转移到员工身上,在这里要感谢我之前的两家公司从未让我有过这样的感觉。

心动的 CEO 黄一孟 在社交媒体上很活跃,也曾分享过心动使用 Slack 和 Confluence 实现内部透明,TapTap 的「离职致意金」和无限假期等政策,尤其后两者以我的理解是在倒逼中层管理人员来提高管理水平,及时辞退不合适的员工。且不论结果如何,对于这种解决「大公司问题」的尝试我是认可的,也是心动不同于同等规模公司的地方。

今年下半年受邀参与了几个智能合约项目的代码审计,虽然实际完成的工作很少,但也算是补习了近几年智能合约和 DeFi 的发展。可以看到现在已经很少有人再去质疑 Bitcoin 或者 Ethereum 的意义了,而是将焦点放在了 DeFi 和 NFT 上,这说明了整个密码货币产业还在不断在向前发展,我几年前曾写过 一篇文章 说比特币是一场实验,那么我觉得在今天这个时间点可以说这场实验已经成功了。

图为我持有的所有密码货币在 2021 年的波动

继软路由和 NAS 之后,今年投入了一些时间搭建 基于 Home Assistant 的智能家居,花的精力不算太多,主要是将部分受支持的米家设备接入了操作体验更好的 HomeKit。

今年 和蛋黄一起玩了双人成行、底特律:成为人类 和 DYSMANTLE,不同于之前引导蛋黄玩游戏的尝试,这三部作品都是素质过硬且适合双人游玩的游戏,我们也都通关了这三部作品。

我们并不需要 Deno

Deno 一出生便带着光环 —— 它发布于 Node.js 创始人 Ryan Dahl 的演讲「Design Mistakes in Node幻灯片)」,当时有些人说 Node.js 要凉了,但我不这么认为。

原生 TypeScript

其实目前我们在引擎的「用户态」去使用 TypeScript 并没有引入任何问题,而且给用户带来了很大的灵活性。考虑到 TypeScript 不可能离开 JavaScript 的生态 —— 毕竟引擎总是要支持 JavaScript 的;再加上 TypeScript 有不同的版本、不同的编译开关,在用户态使用 TypeScript 可以说是最好的方案了。TypeScirpt 迟早会成为 Deno 的历史包袱。

从性能的角度,在 TypeScript 没出现之前,V8 已经在 JavaScript 上进行大量 魔法优化 了,可以说 JIT 出来的代码并不比其他静态类型的语言差太多,是没法简单地通过 TypeScript 来提升性能的。再加上前面说了引擎总还是要支持 JavaScript、TypeScript 的运行时语义依然是 JavaScript(TypeScript 并不能保证对象的实际类型在运行时不被修改),所以引擎也不可能从对 JavaScript 的魔法优化切换到基于 TypeScript 的类型来做优化。

包管理器

我一直认为 NPM 是最好用的包管理器之一,这包括将依赖保存在项目目录中 —— 在调整一个项目的依赖时不必担心对其他项目产生影响;每个包都可以指定自己的依赖版本,允许多版本并存 —— 在升级一个包的依赖时不会影响到其他包,每个包都可以使用新的版本或继续使用旧的版本;NPM 负责查找和安装包,而 Node.js 则用相对简单的协议去使用这些包,它们可以彼此独立地升级演进。

可以看到 NPM 最终极大地减轻了开发者的心智负担,只要你按照正确的方式去使用它,极少会遇到其他语言中有关依赖管理的问题。而 Deno 则反其道行之。虽然 Deno 也提供了一些相关的功能(deno cache),但你会发现 Deno 的本意仍然是不希望进行「依赖管理」。

在代码中包含 URL 是一个非常糟糕的做法(Golang 也是如此),Deno 称之为去中心化,但其实它只是重新将使用包的代码与包的来源耦合在了一起(现在 Deno 提供了一个 官方的代理,但这样和 NPM 的中心仓库又有什么区别呢)。缓存机制也带来了相当大的不确定性:package-lock.json 可以保证每次安装的依赖是完全一致的,而 Deno 的 lock.json 只能检查依赖是否有变化(如果有的话就拒绝运行)。这使得开发者很难控制依赖更新的时机,Deno 则建议将依赖缓存放入 Git

内建权限系统

一直以来通用编程语言都不曾在语言层面引入权限控制,但确实开源社区也曾报出过多次恶意代码的事件,但 Deno 的权限机制相当粗糙 —— 只能在进程级别进行权限控制,我可以大胆地预言,在几乎所有的场景里我们都需要 --allow-all,并不能对安全起到太多作用。

我们需要考虑 Deno 的用户到底是开发者还是使用者:对于 Deno 脚本的使用者来说关注的当然是进程级别的权限;而对于开发者我认为更关注的是第三方包的权限,权限系统应该以包为单位(然而 Deno 里并没有包的概念了),Node 里本来也有 vm 模块可以一定程度上实现沙盒(但确实非常难以控制)。

而且说起来我们现在已经有了 Docker(或者更广泛的容器的概念)这种彻底的隔离和权限控制机制,业界对编程语言引入一套权限控制已经没有太大的需求了。

孤立的生态

可以说 JavaScript 的生态来自于用户态类库的充分竞争,Deno 则在 Runtime API 之外提供了 Standard Library(类似 golang.org/x)、提供了全套的开发工具链(fmt、test、doc、lint、bundle),在试图提供开箱即用的使用体验的同时,也削弱了第三方生态。

在 Node.js 和 NPM 已然成为 JavaScript 事实标准的一部分的情况下,Deno 本来可以通过兼容 Node.js 或 NPM 有一个非常好的开场。但 Deno 却选择了和 Node.js 划清界限,而是兼容了一些浏览器环境的 API(如 prompt 或 onload)。

Deno 自己的说法是为了遵循已有的 Web 标准避免发明新东西,但实际上这些 Web 标准在设计时并未充分考虑浏览器之外的 Runtime,况且 Deno 其实也没能避免发明新东西(这些新东西被放在了 Deno 这个命名空间中)。

小结

Deno 就是这样一个有着非常鲜明个人偏好的 JavaScript Runtime,它试图去纠正 Node.js 的一些「设计失误」、希望给出一种「JavaScript 最佳实践」,希望提供高质量且开箱即用的标准库和工具链。这些偏好的选择总会有人喜欢或不喜欢,但除此之外 Deno 实在是缺少一个 killer feature(杀手级特性)让一个「理性」的 Node.js 开发者(如一个公司)切换到 Deno。

通过单一文件发行、进程级别的权限控制使 Deno 会更适合命令行工具的开发,但能否与已经广泛用于命令行工具的 Golang 竞争尚且存疑。

作为一个 Node.js 开发者,我并不觉得 Deno 可以在未来替代 Node 成为我的主力开发工具,Deno 更像是 Golang 的设计哲学对 JavaScript 的一次入侵。

精子生于 1995 年,英文 ID jysperm.

订阅推送

通过 Telegram Channel 订阅我的博客日志、产品和项目的动态:

王子亭的博客 @ Telegram


通过邮件订阅订阅我的博客日志、产品和项目的动态(历史邮件):

该博客使用基于  Hexo  的  simpleblock  主题。博客内容使用  CC BY-NC-ND  授权发布。最后生成于 2024-04-08.