使用原生的 Windows Docker 容器

一谈到 Docker 容器,按照以往的惯性思维,那就是 Linux 容器(LXC),和 Windows 没多大关系,顶多也就是在 Windows 的 Linux 虚拟机中跑 Docker 容器。

不过自从 Windows Server 2016 开始,出现了 Windows 原生的 Docker 容器,它再也不只是 Linux 下的专利了。Docker 容器中可以运行 Windows 系统了, 每个 Windows 容器共享宿主机的 Windows 内核(--isolation=process,),或使用一个高度优化虚拟机中的 Windows 内核(--isolation=hyperv)。

我们说自 Windows Server 2016 开始,包括现在的 Windows Server 2019, Windows Server 2022, 还有桌面系统的 Windows 10 和  11 上 借助于 Docker Desktop 也能跑 Windows 容器。

原本在 Windows 桌面版上安装 Docker Desktop 就能用来运行 Linux 容器,由此可知在 Windows 桌面版上(如 Windows 7, 10, 11) 可运行两种类型的容器

  1. Linux 容器: 每个容器运行的是 Linux 实例,用 cgcroups 命名空间隔离资源。默认的,使用 Docker Desktop 的 LinuxEngine
  2. Windows 容器:容器中运行的是 Windows 实例,进程隔离模式是容器共享主 机的 Windows 内核,Hyper-V 隔离模式是容器使用高度优化虚拟机的内核。需启用 Windows 的 Hyper-V 特性,并切换 Docker Desktop 使用 WindowsEngine

由于在传统的 LXC 的概念上加入了 Windows 容器,所以 Docker 的架构变成下面这样了

关于 Windows Docker 容器的知识可参考微软的官方文档 Containers on Windows Documentation

Windows 的最基础的镜像有以下四种,按重量级由重到轻排列:

  1. Windows: 包含全套 Windows API 和系统服务(但不含 Server 相关的),比如 Windows 10 镜像 20H2。
  2. Windows Server: 包含全套 Windows API 和系统服务,允许使用多数服务特性,需要 GPU 加速就用它。
  3. Windows Server Core: 只包括主要用以支持 .NET 框架的 Windows Server API 子集。也包含多数服务(如 Fax 服务就没有)。
  4. Nano Server: 最轻量级 Windows Server 镜像,仅包含支持 .NET Core API 一些服务。

我们构建自己的镜像可选择以上基础镜像,更快捷的方式是选择别人已添加有我们需要的软件包的镜像。比如要用 Windows 下运行 Python 可选择 3.10.2-windowsservercore-ltsc2022; .net sdk4.8 的 https://mcr.microsoft.com/dotnet/framework/sdk:4.8。

粗略对比一下各版本镜像文件大小(因版本而有很大的差异),以下是用 docker images 列出的镜像大小

  1. mcr.microsoft.com/windows:20H2 大小 16.2G
  2. mcr.microsoft.com/windows/server:ltsc2022 大小 11.4 G
  3. mcr.microsoft.com/windows/servercore:ltsc2022 大小 4.96 G, mcr.microsoft.com/windows/servercore:ltsc2016 却有 12G
  4. mcr.microsoft.com/windows/nanoserver:ltsc2022 大小 295 M,像是一个嵌入式系统

只能在 Windows 平台下 pull/run/build Windows 镜像,并且要求当前 Windows 平台与镜像的版本要兼容,不像 Linux 容器对当前平台没有任何要求。我们会在后面详细了解到。

Windows Server(包括目前的 Windows 2016, 2019, 2022) 安装 Docker,在 PowerShell 中执行

Install-Module -Name DockerMsftProvider -Repository PSGallery -Force

如提示要安装 NuGet 的话,选择 Y。如果上面命令要求 TLS 的话,请先执行

[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;

最后安装  Docker

Install-Package -Name docker -ProviderName DockerMsftProvider

Windows 10 或 11 下要安装 Docker Desktop

接下来以 Windows 10(同样适用于 Windows 11) 为例, 看两种容器类型(Linux/Windows)的不同,为此还专门安装了一个干净的系统, 以下是测试机器的软硬件环境

  1. CPU: Intel i7-7700 @3.60GHz
  2. 内存:48 GB
  3. Windows 10 Pro, 21H1, OS Version: 10.0.19043
  4. Docker Desktop 4.5.1 (74721), 并其要求的 WSL 2
  5. 还未开启额外的 Windows 特性,如 Containers, Hyper-V, Windows Hypervisor Platform, 但发现默认开启了 Virtual Machine Platform

Windows 下的 Linux 容器

在刚安装完 Windows 10 Pro + Docker Desktop, 默认情况下,是用的 LinuxEngine,所以只能支持 Linux 容器,运行 docker version 查看 server/client 的版本

我们看到上面的  Server Engine OS/Arch: linux/amd64。 Server 的 linux/amd64 表示 Docker 只能运行 Linux 容器。

这时候我们可以运行

docker pull busybox
docker run busybox echo hello world!

也能构建 Linux 镜像,如 Dockerfile 内容

然后

docker build -t test .
docker run test

如果此时试图去 pull 或 run 一个 Windows 容器是非法的

C:\Users\yanbin> docker pull mcr.microsoft.com/windows/nanoserver:20H2
20H2: Pulling from windows/nanoserver
no matching manifest for linux/amd64 in the manifest list entries

当然 run/build Windows 镜像也不行,因为在 run/build 之前都要 pull Windows 镜像。原因为与当前的 linux/amd64 不匹配。

探究 Windows 下的 Docker Desktop LinuxEngine

还是用 理解 Docker Client/Server 架构, 找寻 Docker Desktop 替代品 中的老办法,揭一揭 Docker Desktop 的小底

进到 Docker 的宿主机

物理内存为 48G, Docker 宿主机可使用最大 39G 的内存,CPU 也可用所有的 8 个内核。这与 Mac OS X 下 Docker Desktop 默认分配置 2G 内存,CPU 总内核数一半数量是不同的。

注意,到目前为止 Windows 10 的 Hyper-V 是未启用的,用命令查看

切换到使用 Windows 容器

如果要在 Windows 10 中使用 Windows 容器,先对 Docker Desktop 进行切换到  Windows containers,点开系统栏上的 Docker Desktop, 进入它的上下文菜单

或者用 DockerCli 命令来切换

"c:\Program Files\Docker\Docker\DockerCli.exe" -SwitchWindowsEngine

如果要从 WindowsEngine 切换回到 LinuxEngine 的话,参数就是 -SwitchLinuxEngine

或者用 -SwitchDaemon 参数在 LinuxEngine 与 WindowsEngine 之间来回切换

"c:\Program Files\Docker\Docker\DockerCli.exe" -SwitchDaemon

不过,现在无论是通过 UI 还是命令试图切换到 WindowsEngine 都会弹出一个错误容器

原因就是 Hyper-V 没开启,可用 Turn Windows features on or off 界面中操作

或用前面提示的 PowerShell 指令

Enable-WindowsOptionalFeature -Online -FeatureName $("Microsoft-Hyper-V", "Containers") -All

开启 Hyper-V 后 Windows 会自动重启。完后再执行上一步操作就能成功切换 Docker Desktop 到 Window containers 模式,此时再查看 docker version

版本信息显示的更简单些,我们看到 Server / OS/Arch 变成了 windows/amd64。

如果此时试图运行 Linux 容器也是不行的

C:\Users\yanbin>docker run busybox
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
docker: no matching manifest for windows/amd64 10.0.19043 in the manifest list entries.
See 'docker run --help'.

构建运行 Windows 镜像/容器

C:\Users\yanbin>docker run mcr.microsoft.com/windows:20H2 cmd /c "echo hello world!"
hello world!

构建 Windows 镜像,Dockerfile 内容为

C:\Users\yanbin>docker build -t test .
C:\Users\yanbin>docker run test
hello world!

WindowsEngine 下的 Docker Desktop

切换到 WindowsEngine 后看下 docker context

出现了一个 desktop-windows,而不是先前的 desktop-linux

至于 Windows 容器的宿主机就和隔离模式有关了,有两种,分别是进程隔离和 Hyper-V 隔离

进程隔离

它与 Linux 容器的 cgroups 命名隔离类似的,所有容器共享当前系统内核,容器其实就是一个当前系统下的进程。当前系统为 Windows 容器的宿主机,这时候要求所运行的容器与当前操作系统一致的版本,否则无法共享内核,也就会出现下面的错误

C:\Users\yanbin>docker run -it --isolation=process mcr.microsoft.com/windows/servercore:ltsc2016 ping localhost -t
docker: Error response from daemon: hcsshim::CreateComputeSystem 2cc0e4e8ce8c0f7e8c82bef76df4a9dbf0672d6ffe29776814891ce27c6bb3fe: The container operating system does not match the host operating system.

和 Linux 的容器类似,如果用 --isolation=process 启动的进程可用 Get-Process 命令列出来

Hyper-V 隔离

docker 运行时的 --isolation 的默认值为 hyperv, 此时容器会运行在一个高度优化的虚拟机当中,容器进程也不会出现在当前系统中,而是被包裹在一个个的 vmwp 虚拟机进程当中。也就是说那个虚拟机才是 Windows 容器的宿主机。但哪里能查看到那个所谓高度优化的虚拟机呢?在 Hyper-V Manager 中没有,用 Get-VM 命令看列不出来,优化的太有高度了。

学习到 Windows 容器的两种隔离模式,这或许可用来解释为什么 docker run -p 80:8080 ... 映射端口时不通的原因,默认 hyperv 隔离时 80 开到了那个高度优化的虚拟机中了,而不是当前操作系统,应该用 --isolation=process 尝试下,只是必须保证当前系统版本与容器中系统版本要高度一致。----- 经验证,即使用 --isolation=process -p 80:8080, 端口还是无法映射出来。

还有启动容器的速度也许与选择的隔离模式有关,再高度优化的虚拟机也应该比进程隔离方式启动容器要慢,因为进程隔离本质上就是本地的一个进程。----- 实际测试好像差不了多少,反正都比 Linux 容器慢多了,至少一个数量级的差异。

在某一个特定的 Windows 操作系统上,并不是所有 Windows 镜像都支持进程隔离,Hyper-V 隔离都是支持的,参考 Windows 容器版本兼容性列表 Windows container version compatibility

Windows 系统与容器版本兼容性

了解这方面的内容可指导我们选择什么版本的 Windows 来构建镜像,在什么版本的 Windows 系统上运行容器。这对 Linux 容器根本不是问题,因为基本上只要是一个能运行 docker 命令的 Linux 系统就能自由的构建/运行任何 Linux 发行版的镜像。而首次接触到 Windows 容器时,选择一个 Windows 机器,并且能以 WindowsEngine 方式运行 docker 命令, 想要 pull, run 或 build 一个镜像时,头脑中仍然保有 Linux 容器的思维定式,很容易遭受挫折。

比如在 Windows 2016 上 pull mcr.microsoft.com/windows/servercore:ltsc2022 的镜像

PS C:\docker> docker pull mcr.microsoft.com/windows/servercore:ltsc2022
ltsc2022: Pulling from windows/servercore
8f616e6e9eec: Extracting [==================================================>] 1.252 GB/1.252 GB
898469748ff6: Download complete
failed to register layer: re-exec error: exit status 1: output: ProcessUtilityVMImage C:\ProgramData\docker\windowsfilter\ce66a282a87a379aca443a594025eee342160907305a3f4323f2baaa38d89937\Util
ityVM: The system cannot find the path specified.

但 pull mcr.microsoft.com/windows/servercore:ltsc2016 是没问题的。docker run/build 基于不兼容的镜像版本也是一样的问题,因为 rub/build 之前需要先 pull。

PS C:\> docker run mcr.microsoft.com/windows/servercore:ltsc2016 cmd /c "echo hello"
docker: Error response from daemon: hcsshim::CreateComputeSystem a9c5b39283e525ca8cbf688f95d7aaabfdd1eb7dd21914d9e587144a455125ca: The container operating system does not match the host operating system.

因此,清楚的了解 Windows 系统与容器版本兼容性也不至于一接触 Windows 容器就可能令我们垂头丧气,甚至有些抓狂。

Windows 系统与 Windows 镜像版本的对应,以及是否支持 Hyper-V 或进程隔离请参考这个列表 Windows container version compatibility。基本上是新版本兼容旧版本,如 Windows Server 2019 下可运行 Windows Server 2019 和 Windows Server 2016, Windows Server 2022 就能运行从 Windows 2016 到 Windows 2022 之间所有的版本。但支持进程隔离的话,必须是平台与容器的版本一致,如 Windows Server 2019 以进程隔离方式运行 Windows 容器就只能支持 Windows Server 2019。

同样的,在用 docker pull 或 build 时也要遵行前面这个兼容性表格。构建时使用高版本操作系统平台更具广泛的兼容性,但要用到 --isolation=process 的好处运行容器时就要完全一致的版本。

这种 Windows 镜像版本与 Windows 宿主机的严格的匹配并系破坏了运行 Linux 容器的初衷,我们在一个 Linux 宿主机中运行其他的 Linux 容器根本都不用在乎是什么内核版本或发行版本。这有点像当初 Windows 对 Java 掺一手搞出个 Visual J++ 直接破坏了 Java 所号称的一次编写到处运行的口号。

AWS 对 Windows 容器的支持

之前一直在 ECS 中使用 Linux 容器服务,因为定义 Task 时必须指定镜像,而那时对 Windows 容器不是一片黑,一直觉得 ECS 根本就不支持 Windows 容器。最近才注意到 AWS 早已提供了很多的优化了的 Windows Server ECS AMI 用来跑 docker Windows 容器, 如

Windows_Server-2016-English-Core-Containers-*
Windows_Server-2016-English-Full-ECS_Optimized-*
Windows_Server-2019-English-Core-Containers
Windows_Server-2019-English-Full-ECS_Optimized-*
Windows_Server-2022-English-Core-Containers-*
Windows_Server-2022-English-Full-ECS_Optimized-*
Windows_Server-20H2-English-Core-Containers
Windows_Server-20H2-Core-ECS_Optimized-*

等等

Windows 的版本可以通过 ver, systeminfo 命令获得

在选择的 Windows_Server-2016-English-Core-Containers-2016  上试图用 docker run --isolation=hyperv 时却提示没有 hypervisor

PS C:\docker> docker run --isolation=hyperv mcr.microsoft.com/windows/servercore:ltsc2016 cmd /c "echo hello"
C:\Program Files\Docker\docker.exe: Error response from daemon: container fba5ff713c0e9a4fc439747c6792251855c06dfcb1649c3e58d72280b9d62a23 encountered an error during CreateContainer: failure
in a Windows system call: No hypervisor is present on this system. (0xc0351000) extra info.......................

虽然 ECS 支持 Windows 容器,但 Windows 镜像身躯十分巨大,动不动 10G 起步,相比 Linux 的 100-200M 左右的镜像,可称是个巨兽,这会严重影响构建,推送,拉取镜像的速度。且镜像下载后启动一个 Windows 容器也比较慢。所以对于 Windows 应用程序或许要跑过 ECS, 而让  ELB 直接连向 EC2 Target Group。

启动速度测试

对于同一个镜像还无法进行不同隔离模式下启动速度的测试,因为在 Windows Server 上不能用 --isolation=hyperv 隔离级别。

Windows 容器不同隔离模式

在 Windows Server 2016 上分别以进程隔离和 Hyper-V 隔离测试启动 Windows 容器的速度

平均耗时 6.15 秒。

这与在 Linux 下启动一个 Linux 容器是无法比拟的

在 Windows 10 下启动一个 ubuntu:20.04 的时间为

总结

刚开始用到 Windows 容器时头脑中总是延续着 Linux 容器的思维,哪知其实几乎是进入了另外一个世界,本来一个十分趁手的 Docker 容器在 Windows 那边却被压缩的会四处碰壁。Windows 容器不光是个头大,而且 Windows 容器启动明显示的缓慢。 

至今仍有一个问题有待解决,就是用 -p 端口映射时无法在执行 docker 命令的机器上启动相应的端口。

所以还是能用 Linux 的地方坚决拥护 Linux,像 C# 的代码如果能用 .NET Core 解决的话一定是件幸事。


端口映射的问题(已解决)[2022-03-02]

运行 Windows  Server Docker container

docker run --name aspnet_sample --rm -it -p 8080:80 mcr.microsoft.com/dotnet/framework/samples:aspnetapp

之前依旧的按照 Linux 容器的惯性,先用 docker ps 验证端口映射是否已设置(下面只显示 Name 和 Ports)

PS C:\Windows\system32> docker ps --format "{{.Names}}: {{.Ports}}"
aspnet_sample: 0.0.0.0:8080->80/tcp

端口映射没问题,从宿主机的 8080 到容器的  80

先用类似于 Linux 的 netstat -na|grep 8080 检查一下在宿主机上是否开启了 8080 端口

PS C:\Windows\system32> netstat -na|findstr 8080
PS C:\Windows\system32>

发现什么都没有,再加上 telnet 双重验证

PS C:\Windows\system32> telnet localhost 8080
Connecting To localhost...Could not open connection to the host, on port 8080: Connect failed

这时候查看容器的 IP 和端口号

 

容器内启动的 80 端口是没问题的, 所以通过容器 IP 访问 http://172.25.8.94:80 是没问题的,有些地方却介绍访问 http://172.25.8.94:8080 来访问(此路不通)。而且一个理由是在更早的版本要用容器 IP,哪个更早又没说清楚。

After the application starts, navigate to http://localhost:8080 in your web browser. You need to navigate to the application via IP address instead of localhost for earlier Windows versions
https://hub.docker.com/_/microsoft-dotnet-framework-samples/?tab=description

看到上面的景象后,基本就气馁了 -- 1) localhost 的 8080 没有打开, 2) 容器的 80 端口确实是打开了,但外部不知道容器的 IP 一般也无法直接访问容器,即使能直接访问容器(设置不同的网络类型),那所谓的 -p 8080:80 端口映射根本就没必要的,因为不管有无 -p 参数,容器内的 80 端口都会开启。

前几天我就一直被这种假象所困扰,由 netstat -na 看到不主机的 8080 端口,且 telnet localhost 8080 也不通,本能的认为 Windows Server 容器的端口映射没戏,进一步用它来做 ECS 的端口映射也势将无法成功。其实这只是一个显示 Bug, 见 Open issue: [Windows] Port binding is not visible with 'netstat' but works correctly. #30300

这种情况下,端口映射其实是成功的,只是不能用 localhost:8080 来访问,用 ipconfig 找到主机 IP, 然后访问 主机 IP:8080 是能通的

然后 

telnet 10.255.60.241 8080

也没问题的,也就是从远程访问 http://10.255.60.241:8080 是通的,自然作为 ECS 的端口映射也不是个问题

netstat -na 是个假象,localhost:8080 确实也不通,127.0.0.1:8080 也不行,也就是说启动 Windows 容器时的 docker -p 8080:80 只会在网卡的 8080 端口上监听,只对 netstat -na, 甚至是 PowerShell 命令 Get-NetTCPConnection 都不可见。不过再来一个 -p 8080:80 就能暴露问题了

PS C:\Windows\system32> docker run -p 8080:80 mcr.microsoft.com/dotnet/framework/samples:aspnetapp
C:\Program Files\Docker\docker.exe: Error response from daemon: failed to create endpoint dazzling_clarke on network nat: HNS failed with error : The object already exists.

端口 8080 已被占用,证明 8080 已在某个我们所看不到的地方已绑定了。

自此,在 ECS 中使用 Windows 容器值得进一步去实践,下面的任务大约就是如何控制 Windows Docker 镜像的大小,放到网速快的 Docker Registry 中。

链接:

  1. Docker and Microsoft: Integrating Docker with Windows Server and Microsoft Azure
  2. Docker的Windows容器初体验
  3. Run Docker Containers on Windows Server 2019

本文链接 https://yanbin.blog/windows-native-docker-container/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments