作者:Connor Brewster
译者:弯月
出品:CSDN
在讨论 Nix 的时候,大家总是喜欢拿它和 Docker 做比较。但我认为 Nix 和 Docker 是解决不同问题的工具,前者是构建和部署容器的工具包,而后者则是包和配置管理器。但是,这两个工具在功能上确实有一些重叠,都可用于创建可重现的环境。所谓“可重现的环境”指的是如果重复完全相同的步骤,就能建立完全相同的环境。也就是说,这些环境拥有相同的工具、版本和配置。
可重现的环境可以确保项目中的所有开发人员都拥有完全相同的工具集。此外,开发人员可以在与生产环境类似的环境中开发软件,因此可以避免部署时发生意外。
Nix 和 Docker 都可以解决一个由来已久的问题:“在我的机器上可以正常运行,不知道你这里怎么回事。”然而,二者采用的方法却不相同。
Docker 提供了创建容器镜像的工具。镜像存储了容器的内容和配置,通常包含文件系统、一些环境变量和命令。
有了镜像,你就可以在不同的机器上创建出功能完全相同的新容器。
你可以将镜像分发给其他开发人员,这样他们就可以构建出相同的环境了,而且你也可以通过镜像将服务部署到生产环境。
Docker 镜像可以由 Dockerfile 创建。该文件告诉 docker 运行构建镜像的命令,包括从宿主系统复制文件,或使用包管理器(如 apt 或 apk)安装软件包。
下面是一个 Dockerfile 的示例:
FROM node:12-alpine
RUN apk add --no-cache python g++ make
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
Docker 会创建一个新容器,并在其中构建镜像。在执行 Dockerfile 中的每条命令之前,Docker都会创建一个新的文件系统层。Docker 使用的是联合挂载(Union mount)文件系统,这种系统可以将多个文件系统或目录叠加起来,显示为一个文件系统。
由于每条命令都有自己的文件系统层,因此 Docker 可以检测哪些层仍然有效,而哪些层需要根据项目的变化重建。正确排序的命令可以提高 Docker 重建镜像的速度。
此外,每个文件系统层可以在多个镜像之间共享。当需要利用同一个基础 Docker 镜像多个新镜像时,这会非常有用。例如,上述 Dockerfile 的基础镜像为 node:12-alpine,只要将其保存在某个地方,其他 Docker 镜像就可以共享了。
构建好镜像后,还需要命名、打标签,然后上传到镜像存储库,然后就可以轻松与他人共享了。
虽然有了镜像就可以构建可重现的 Docker 容器,但 Docker 并不能保证创建的镜像是可重现的,也就是说即便使用同一个 Dockerfile,运行两次 docker build,也有可能得到两个行为不同的镜像。例如,第三方软件包可能在不知不觉间发布了更新,因此导致构建的镜像也发生变化。确保使用固定的某个版本可以从一定程度上缓解这个问题,但也不能完全杜绝。
另一个需要注意的问题是,在创建镜像时,Docker 只允许从单个文件系统层继承。你可以在 Dockerfile 中指定基础镜像,但不能将两个镜像合并起来。假设你想使用 node 和 rustc 构建容器,由于不能将 node 和 rustc 的镜像合并起来,所以只能先利用 node 镜像构建容器,然后再手动安装 rustc,或者反过来,先用 rustc 构建容器,然后再手动安装 node 。
为了构建可重现的环境,Nix 采用了最基本的方式。Nix 提供了一个完整的构建系统,可以单独构建各个软件包。
在构建软件包时,你需要执行如下步骤:
每个软件包的构建都可以重复这个过程,为了确保每个环境都是可重现的,Nix 做了大量努力。首先,Nix 会限制网络访问、文件系统访问,有时甚至还会在构建过程中在沙盒容器中运行,以防止构建过程中受到任何外部因素的影响。
由于软件包会依赖第三方库,形成巨大的依赖关系网,因此 Nix 会在构建过程中遍历所有依赖项。
而你可以使用 Nix 创建可重现的环境,只需在关系网中添加一个节点,然后让 Nix 完成第一步构建环境的工作即可。
在开发中,你可能不希望或不需要 Nix 执行第二步。相反,你可以将 Nix 放入bash shell,自行运行构建命令。
Nix 自带一个名叫 nix-shell 的工具,它可以为你完成这项工作。将 shell.nix 文件添加到项目的根目录,nix-shell 就可以根据所有指定的软件包构建一个新环境。
下面是一个 shell.nix 文件的示例:
{ pkgs ? import {} }:
pkgs.mkShell {
nativeBuildInputs = [ pkgs.rustc pkgs.cargo ];
}
Nix 并不会使用容器,它只会修改环境变量。 例如,将某个二进制包添加到环境变量 PATH 中。
由于 Nix 可以保证构建可重复的环境,因此,只需共享此 shell.nix 文件,即可为开发人员提供功能完全相同的开发环境。
与 Docker 不同,Nix 是一个成熟的包管理器,因此 Nix 可以自由组合环境。以上述 Docker 示例为例,如果我们想在同一个环境中同时部署 node 和 rust,则只需告诉 Nix 将二者添加到构建的输入中:
{ pkgs ? import {} }:
pkgs.mkShell {
nativeBuildInputs = [ pkgs.rustc pkgs.cargo pkgs.nodejs-16_x ];
}
在考虑这个问题之前,我们首先应该回顾一下:Docker 和 Nix 是完全不同的工具。
Docker 镜像只是 Docker 提供的一小部分,Docker 为整个容器生态系统提供了工具。
而 Nix 的设计旨在构建可重现的包和环境。
如果你的目的是构建可重现的开发环境,则选用 Nix 更合适。
如果你希望寻找一种方法来构建、打包和部署自己的服务,则 Docker 提供的更丰富的工具更适合你。毕竟,容器乃是如今部署 Web 服务的标准方式。
即便如此,Docker 镜像的构建仍有很多不足之处。不过,好在我还有一张王牌:利用 Nix 构建Docker 镜像
例如:
{ pkgs ? import { }
, pkgsLinux ? import { system = "x86_64-linux"; }
}:
pkgs.dockerTools.buildImage {
name = "cowsay-container";
config = {
Cmd = [ "${pkgsLinux.cowsay}/bin/cowsay" "I'm a container" ];
};
}
如此一来,我们就可以鱼与熊掌兼得:
目前,我们正在从通过巨大的 Docker 镜像 Polygott 提供开发工具朝着通过 Nix 提供软件包过渡。你可以看一看用户使用 Nix 构建的应用(https://replit.com/apps/nix)。
我们发现, Nix 解决了我们在使用 Docker 镜像时遇到的下列问题:
如今在使用了 Nix 后,我们可以让用户自由选择他们需要的工具。同时还解决了上述问题,减轻了我们内部的维护负担。双赢!
虽然我们使用了 Nix,但我们也不会因此而放弃 Docker 容器。我们仍然需要容器来为每个用户执行环境提供一个隔离的环境。
Nix 会超越 Docker 吗?不会,这两款工具实现了不同的目标,二者的结合可以提供完美的解决方案:可重现的环境与容器化的部署。
参考链接:
https://blog.replit.com/nix-vs-docker