2017 年 12 月

第 32 卷,第 12 期

容器 - 通过 Docker 和 Windows Server 容器使 .NET 应用现代化

作者:Sean Iannuzzi

到目前为止,你很可能听说过 Docker、Docker 容器以及随着 Windows Server 2016 的推出而出现的集成 Windows Server 容器。我真的相信,在几年之内,Docker 容器将成为 Web 站点、应用程序和其他系统运行方式的标准,而不是依靠运行虚拟机 (VM) 来支持应用程序。使用 Docker 可以实现可伸缩性、隔离性和安全性,同时还可以确保利用极少的支持就可正确配置应用程序和系统(从部署的角度来看)。与设置 VM 和配置所有必需功能的复杂性相比,Docker 安装的简单性非常有优势。就像物理计算机的请求逐渐被淘汰而转向 VM 一样,最迟在未来的几年里,Docker 将很可能开始取代 VM 的需求。

在本文中,我将重点介绍如何利用容器方法,通过使用 Windows Server 2016、文件共享以及与 Windows Server 容器的套接字通信来实现几个 .NET 应用程序的现代化。我将详细介绍如何使用 Windows PowerShell 创建 Docker 映像以及在主机 Windows Server 2016 系统和 Windows Server 容器之间共享文件和套接字。你的许多应用程序可能都有这样的通用功能,例如,需要启用以确保 .NET 应用程序可以移植到 Docker 容器的功能。我查看的许多功能不是 Windows Server 容器特有的,可用于任何具有类似功能的应用程序。  

业务挑战:CPU、内存和磁盘密集型 .NET 应用程序

我面临的业务挑战是实现几个现有 .NET 和 C++ 控制台应用程序的现代化,这几个应用程序负责处理涉及 CPU、内存和磁盘密集型处理的大量数据。我需要将这些控制台应用程序公开在更传统的 Web 模型中,系统将在这里从单用户系统迁移到多用户支持的设置。考虑到如何设置应用程序以及正在处理的数据量,我不想在 VM 上管理数据或可执行文件的多个副本。

作为挑战的一部分,我需要确定如何以最佳方式横向扩展这些应用程序,并尽可能减少整个网络中的网络延迟和文件管理。应用程序的性能至关重要,使用网络共享、文件共享或其他分布式处理都会显著影响其性能。因此,为了成功应对这一挑战,我需要提供一个可扩展的模型,在不需要维护多个数据副本的情况下也能够获得高水平的性能(相对于 CPU、内存和磁盘 IO)。与大多数项目一样,为这些应用程序提供全新的现代化和可扩展版本的时间非常有限,没有可能完全重新设计。

重要功能:Docker 中的文件共享、套接字连接和 .NET

对于我的一些特定应用程序,我考虑在使用 Docker 登录之前使用几个选项,更具体地说,是在 Windows Server 容器上。作为我评估的一部分,为了将应用程序成功迁移到 Docker,我遇到了三个非常具体的技术挑战: 

  • 在 Docker 中运行传统的 .NET 应用程序。
  • 利用主机系统和我的 Docker 容器之间的文件共享。
  • 在主机和 Docker 容器之间启用套接字通信。

我将详细介绍如何克服这些技术挑战,以及如何实现在 Windows Server 2016 上运行 Docker 和 Windows Server 容器的概念。在考虑可能有多少个 .NET 应用程序迁移到 Docker 或 Windows Server 容器时,这些概念本身只是一个开始。我要回顾的例子可以得到更广泛地应用或扩展,以解决各种应用程序功能,从而可以为您的应用程序提供更现代化的部署。

应用程序性能:8GB RAM,10 TB 文件处理

在深入探讨我所考虑的选项和概念之前,我想详细介绍一下我要移至 Docker 容器的应用程序和系统。首先,这些应用程序所执行的工作类型是相当独特的,而且它们都使用非常密集的进程、内存和磁盘资源。而且,应用程序的执行速度对系统的成功至关重要。

我的应用程序主要为单个用户而设计,结合使用 C++ 和 .NET Framework 而构建,它们可执行非常复杂的计算处理数据文件。为了让你了解我的系统所面临的性能挑战,可以想象每个用户需要大约 8GB 的 RAM 才能对 10TB 以上的数据文件进行计算,并且需要预先分配的内存和极快的磁盘速度以在短时间内处理大量数据。系统还使用套接字连接进行请求者的调用和通知。随着应用程序和系统的发展,我发现我需要一种快速的方法来扩展系统并支持多用户处理。我想很多人能够想到类似的应用程序,它们都可以通过移至容器而受益。

解决方案选项:重策划、自动缩放、Docker

我面临的技术挑战包括评估我可能实现目标的不同方式。我考虑了三个选项。

  1. 重策划:一个选项是重策划整个应用程序套件。这肯定管用,但考虑到我的系统的规模和复杂性,我需要一个能降低风险而且不用花太长时间就能完成的解决方案。等待几个月甚至一年来重新设计系统是不可接受的。但是,如果这个选项是一个合理的解决方案,那么评估这个选项仍然很重要。 
  2. 自动缩放:另一个选项是评估如何利用 VM 和自动缩放。这肯定会比重写应用程序更快,并能降低整体风险。但是,由于分配 VM(尤其是具有 10 TB 存储容量的 VM)所花费的时间,这会增加很多开销。尽管我可以为此找到解决方案,例如使用备用实例,然后通过附加层或应用程序来处理服务器的设置和取消设置,但似乎仍然不是最好的方法。但是,该选项确实使我朝着正确的方向发展,因为它不涉及重策划整个应用程序,并且可以为每个 VM 部署多个可执行文件并自动扩展 VM。我决定使用更新式的技术方法继续搜索更简单的实现模型。
  3. Docker 容器:我考虑的最后一个选项是使用 Docker,主机系统和 Docker 容器之间具有互操作性。使用 Docker 容器使我可以按需扩展系统,而无需重策划整个系统。这种方法可以降低重策划应用程序所带来的风险,为安全目的提供一定程度的隔离,并使我可以快速实施这些更新,同时仍提供所需的规模级别。

使用 Docker 部署 .NET 应用程序

我使用 Docker 选项遇到的主要问题是,应用程序是用 .NET 和 C++ 编写的,我担心它无法直接在 Docker 中运行。我开始研究如何将 .NET/C++ 应用程序迁移到 Docker 后,我就发现它需要升级或重新设计。请记住,我的方法必须很快,因此我开始深入了解 Windows Server 2016 和完全集成的 Windows Server 容器。通过利用 Windows Server 容器,我希望能够将应用程序保持原样,并将所有依赖项与其他必需的设置一起部署在我的容器中。我遇到的第一个技术挑战是:面向 .NET 应用程序的传统 Docker 容器需要 .NET Core,而我的应用程序是使用 .NET 和 C++ 编写的。当然,我可以将应用程序升级到 .NET Core,但是这将涉及大量工作,我正在尝试部署一个速度尽可能快、风险尽可能低的解决方案。我也尝试确保将扩展能力以及应用程序的隔离和安全级别包含在内。

 虽然使用 Windows Server 容器开始看起来非常有前景,但我仍然需要测试许多不同的概念,例如文件共享和套接字连接,你可能也会发现这些概念非常有用。虽然我介绍的许多内容对于特定的设置都是独一无二的,但选项和概念不是,它们可用于需要此类迁移或扩展的其他系统,无需重新设计或重写应用程序。当然,这种方法并不能取代应用程序的重新设计,但如果这是一个理想的方向,确实给团队提供了重新设计应用程序的时间。作为重新设计的一部分,团队可以重新设计兼容或启用 Docker 的应用程序。

在接下来的几个部分中,我将介绍:

  1. 如何设置 Windows Server 2016 VM 来支持 Windows Server 容器。
  2. 如何使用 PowerShell 创建 Docker 映像。
  3. 基于 Windows Server Core 的 Docker 文件。
  4. 如何启用主机和容器之间的高级文件共享。
  5. 如何从主机和容器启用套接字侦听器。

Windows Server 2016 和容器

首先,我部署了一个 Windows Server 2016 VM 并启用了相应的功能,例如 .NET Framework、IIS 和容器,如图 1 所示。

启用 .NET Framework 和容器服务
****图 1 启用 .NET Framework 和容器服务

请注意,要生成这种类型的解决方案,必须安装 .NET Framework。

安装所有必需的功能后,我相应地验证了其中每个功能。为了确保 Docker 正常运行,我运行了 PowerShell 命令 docker –version。然后,我在 PowerShell 中输入 “(get-service “Docker”).Status” 验证了 Windows Docker 引擎服务也在运行。作为最后一步,我从 dockr.ly/2i7pDSn 对 Windows Server Core Docker 映像执行了 docker 拉取请求。拉取请求完成后,我通过运行命令 docker 映像验证 Docker 映像是否成功创建。

安装了 Windows 容器服务并使用基础 Docker 映像设置环境后,我就可以开始使用 .NET 控制台应用程序了。

.NET 应用程序安装

首先,我启动了使用 .NET Framework 4.6.1 的一个非常基本的控制台应用程序。该应用程序只是简单地获取参数和显示响应。在完全迁移到 Windows 容器之前,我想确保所需的功能按预期工作。但是,我需要先执行大量步骤,然后才可以在 Windows Server 2016 的容器中运行应用程序。

第一步是创建一个可重用的“生成”PowerShell 脚本,该脚本将生成应用程序并在 Windows Server 2016 上创建 Docker 映像。为了完成这个任务,我编写了两个函数,一个用来执行 msbuild,另一个用来创建实际的 Docker 映像,如图 2 所示。

图 2 用来生成应用程序和创建 Docker 映像的 PowerShell 函数

Set-StrictMode -Version Latest
$ErrorActionPreference="Stop"
$ProgressPreference="SilentlyContinue"
s
# Docker image name for the application
$ImageName="myconsoleapplication"
function Invoke-MSBuild ([string]$MSBuildPath, [string]$MSBuildParameters) {
  Invoke-Expression "$MSBuildPath $MSBuildParameters"
}
function Invoke-Docker-Build ([string]$ImageName, [string]$ImagePath,
  [string]$DockerBuildArgs = "") {
  echo "docker build -t $ImageName $ImagePath $DockerBuildArgs"
  Invoke-Expression "docker build -t $ImageName $ImagePath $DockerBuildArgs"
}

脚本的下一步是执行这两个函数,传入所有必需的参数:

Invoke-MSBuild -MSBuildPath "MSBuild.exe" -MSBuildParameters
  ".\myconsoleapplication.csproj /p:OutputPath=.\publish /p:Configuration=Release"
Invoke-Docker-Build -ImageName $ImageName -ImagePath "."

有了生成脚本,剩下的就是创建 Docker 文件,为在 Windows Server 2016 上运行的 Windows Server 容器启用控制台应用程序。请注意,从开发的角度来看,测试生成过程以使用 Visual Studio 命令提示符(包括路径中的 MSBuild)可能会有所帮助。作为初步设置的一部分,我安装了一个名为 Windows Server Core 的基础 Docker 映像,它具有运行应用程序所需的所有基本功能。创建 Docker 文件时,我告诉 Docker 使用这个映像,并以“myconsoleapplication.exe”作为入口点发布我的应用程序: 

FROM microsoft/windowsservercore
ADD publish/ /
ENTRYPOINT myconsoleapplication.exe

入口点将是控制台应用程序中的 Main 函数。

最终生成和部署到 Windows Server 2016

有了为 Windows Server 容器启用的完整 .NET 控制台应用程序后,我就能准备部署我的应用程序。我发现的可用于测试的一个简单方法是直接将应用程序文件夹复制到 VM。将应用程序复制到服务器后,执行 PowerShell 脚本来生成应用程序。导航到源目录,然后从 PowerShell 运行 ./build 命令。

生成脚本的输出应该类似于****图 3 中显示的结果。

图 3 生成脚本的输出结果

docker build -t myconsoleapplication .
Sending build context to Docker daemon  6.058MB
Step 1/3 : FROM microsoft/windowsservercore
 ---> 2cddde20d95d
Step 2/3 : ADD publish/ /
 ---> 452c4b42caa5
Removing intermediate container cafb387a3634
Step 3/3 : ENTRYPOINT myconsoleapplication.exe
 ---> Running in a128ff044ef3
 ---> 4c7dce888b36
Removing intermediate container a128ff044ef3
Successfully built 4c7dce888b36
Successfully tagged myconsoleapplication:latest

为了确认我的 Docker 映像已经成功创建,我再次运行了 docker 映像命令,我可以看到新的 Docker 映像,如图 4 所示。

作为 Docker 映像的控制台应用程序
图 4 作为 Docker 映像的控制台应用程序****

测试 Windows Server 容器控制台应用程序

在进入一些非常具体的功能之前,我执行的最后一步是测试我的应用程序,确保它实际上在 Windows Server 容器中运行。为此,我运行了以下命令:

docker run --rm -it myconsoleapplication
  ".NET Framework App Running in Windows Container"

正如所料,应用程序在控制台窗口中输出传递给它的参数。

这涉及有关部署、配置和设置可以在 Windows Server 容器中运行的 .NET 应用程序的基础知识。此时,你可能会想到许多现有的、有可能移到 Windows Server 容器的应用程序。但是,我发现还有一些关键功能非常有用,例如文件共享和套接字通信,你可能也会觉得有用。在下一部分中,我将深入探讨这些功能以及如何在自己的应用程序中使用它们。

Docker 容器:启用高级文件功能

与许多控制台应用程序一样,由于不同的原因,你的应用程序可能会使用相当多的文件;无论是用于日志记录、处理还是其他任务,文件的使用可能很密集。对于我的特定应用程序,我正在读取非常大的文件,并且不想将文件复制到每个容器。我也想尽我所能去优化磁盘 I/O,并且在尝试读取此类的大文件时,使用网络上的共享文件夹(一个文件服务器)引入了太多的延迟并影响了性能。此外,我不想创建具有各种配置、端口和目录的多个版本的应用程序,因为维修起来就像是场噩梦。因此,我开始评估如何在运行 Docker 服务的主机系统上共享我的文件,然后从我的容器中访问这些文件。我发现如果你在 Linux 中使用 Windows Server 容器或运行 Docker 容器,这非常简单且无关紧要。Docker 完全支持这种类型的功能。实际上,对我的设置最有利的是,只要我在容器中安装驱动器来关联内部目录,我甚至不需要修改我的应用程序。我做的唯一更改是,当我运行 Docker 容器而不是从配置文件中读取它时,用一个参数设置我的路径。

我能够保持所有的文件处理和路径不变,因为它们都是相对路径并在主目录中。这意味着我不必更改应用程序的核心逻辑,这反而会缓解我更改 .NET 控制台应用程序可能产生的大部分风险。

为了测试这个功能,我通过插入以下代码将一个基本的文件 IO 进程添加到我的控制台应用程序中:

using (StreamWriter sw = File.AppendText(@"C:\containertmp\testfile.txt"))
{
  sw.WriteLine(DateTime.Now.ToString() + " - " + args[0]);
}

然后,将我的解决方案重新部署到 Windows Server 2016 VM。这也需要通过运行 ./build PowerShell 脚本来重新生成映像。

启用这个功能的最后一步是在需要公开到 Docker 容器的主机上创建一个目录。在我的示例中,我刚刚创建了一个名为 hostcontainershare 的文件夹。这样做的关键是我如何将该文件夹从 Windows Server 主机系统装载到 Docker 容器。令人惊讶的是,通过将下面的参数传递给 docker 运行命令,这点非常容易实现:

-v [source directory or path]:[container directory or path]

此参数设置为接受源和目标。例如,我先传入本地 Windows Server 主机目录,然后将其装载到容器中。下面是整个 docker 运行命令:

docker run --rm -it -v c:\hostcontainershare:c:\containertmp myconsoleapplication
  ".NET Framework App Writing to Host Folder" 1

在 Windows Server 容器和 Docker 容器中有很多方法可以实现这个功能,但是对于我的 .NET 控制台应用程序,我发现这个方法非常简单而且易于实现。图 5 显示了如何设置的示例。

主机和 Windows Server 容器文件操作概述
****图 5 主机和 Windows Server 容器文件操作概述

Docker 运行命令的结果是将文件从我的 Docker 容器写入到主机目录,如图 6 所示。

从 Docker 容器到 Windows Server 2016 主机的写权限示例
****图 6 从 Docker 容器到 Windows Server 2016 主机的写权限

由于其对大型文件所执行的操作,启用此功能为我的应用程序提供了显著的优势。我发现我不需要在所有容器中复制我的文件。只要我的主机上有最佳或固态硬盘,文件处理比使用共享文件夹、网络驱动器或其他非本地站点要快得多。对传统控制台应用程序使用这种技术的好处是数不胜数的。

通过成功的文件共享,我拥有了最后一个需征服的功能 — 套接字连接,我将在下一部分中讨论这个功能。

Docker 容器:启用高级套接字功能

我需要证明的主要功能之一是,能够从主机套接字连接到内部容器套接字连接进行通信。同样,此功能的大部分内容都可以在 Windows Server 容器和 Docker 容器中使用,因为设置是通过命令行参数控制的,这些参数指定了 Docker 容器的运行方式以及正在公开的端口。

为了支持此功能,我创建了客户端和服务器套接字应用程序,用于建立从客户端应用程序(在 Windows Server 上运行)到服务器端应用程序侦听器(作为 Windows Server 容器运行)的连接。我还在我的应用程序中添加了侦听特定套接字然后在接收到数据和字节的控制台中响应所需的代码。

我利用了异步客户端套接字示例(在 bit.ly/2gDKYz2 上)和异步服务器套接字示例(在 bit.ly/2i8VUbK 上)中的 Microsoft 套接字示例,以便获得集成到应用程序中的基本代码段。

我对服务器端代码进行了一些更改,以帮助获取容器的 IP 地址,这样我在使用客户端套接字应用程序时将能够提供分配的 IP 地址。我能够通过运行以下命令获取了容器的 NAT 详细信息:

docker network inspect nat

我也运行了各种查找来检索容器的 IP 地址,但为了便于调试和排除故障,我在一个循环中添加了检索的所有 IP 地址,然后将它们写到控制台窗口中:

foreach (var info in ipHostInfo.AddressList)
{
  Console.WriteLine("\nIP: " + info);
}

我还将端口设置为套接字连接测试的特定端口。我再次将我的应用程序部署到 Windows Server 2016 VM,并将我的客户端应用程序复制到服务器以测试连接。默认情况下,没有自定义端口从容器中公开,该容器不允许进行 TCP 套接字连接。为了启用这个功能,我需要为 Docker 提供合适的运行参数,类似于共享文件夹所需的参数。

在我的示例中,我想将端口 50020 从运行客户端应用程序的主机连接到在 Windows Server 容器中运行的 .NET 控制台应用程序。 7 展示了如何设置应用程序。

客户端到 Windows Server 容器主机套接字的通信
图 7 客户端到 Windows Server 容器主机套接字的通信

一切设置和配置好后,我需要通知 Windows Server 容器和 Docker 容器,我想把容器中的某些端口公开给主机。为了实现这一点,我将以下参数指定给 Docker 运行命令:

-p [host port]:[container port]

可以通过为每个端口重复该参数来公开多个端口,例如 -p 50020:50020 –p 50019:50019 等。通过运行容器并公开端口,我已经准备好测试从 Windows Server 容器控制台应用程序到在 Windows Server 2016 VM 上运行的客户端的连接。

我用来运行 Windows Server 容器的完整命令是:

docker run --rm -it -p 50010:50010 -v c:\hostcontainershare:c:\containertmp myconsoleapplication
  ".NET Framework App Listening on Socket" 2

启动运行控制台应用程序的 Windows Server 容器后,我就准备启动客户端应用程序。容器控制台应用程序向我显示了容器的当前 IP 地址以及它正在监听我指定的套接字这一情况。下一步需要做的是启动客户端应用程序,传递我希望客户端应用程序连接到的容器的当前 IP 地址,之后我的测试即完成。如****图 8 所示,连接到容器控制台应用程序的 IP 地址的客户端应用程序显示在屏幕上,并通过套接字发送一小部分数据。成功!

客户端套接字应用程序将数据发送到容器控制台应用程序
图 8 客户端套接字应用程序将数据发送到容器控制台应用程序

总结

鉴于我正在运行的应用程序的性质,我需要 Docker 提供的几个特定功能。了解到 Windows Server 容器能让我运行 .NET 控制台应用程序时,我相当乐观地认为能够从主机访问文件并启用从主机系统到我的 Docker 容器的套接字通信。最令我印象深刻的是共享文件夹和文件的能力,同时还公开了特定于我的应用程序或任何其他应用程序的套接字和端口。有了 Windows Server 2016,Windows Server 容器的集成非常流畅,部署 Windows 容器所需的配置或编排很少。对于你计划迁移到 Docker 的 .NET 应用程序,我强烈建议使用 Windows Server 容器并根据需要公开 Docker 的功能,以确保你的应用程序能按预期运行。与大多数应用程序和资源共享一样,必须始终考虑和检查安全性。将数据或套接字从主机系统共享到容器时,仍然必须小心谨慎。启用此类功能时,你必须非常小心以免引入漏洞。另外,在主机系统和容器之间共享文件和打开端口必须小心处理以避免安全风险。我发现,通过我的应用程序,我能够提供高级别的可扩展性,同时也实现整个应用程序的某些组件的现代化。现在可以使用 Docker Swarm 或其他缩放模型将应用程序部署到更具可扩展性的设置,这些模型允许应用程序运行,仅受成本或硬件级别的限制。作为一大收获,这个解决方案为我提供了非常需要的时间来评估是否需要重新设计,或者这个解决方案是否可以作为永久的解决方案。通过本文中展示的许多功能,希望你可以开始自己的迁移和设计,实现 .NET 应用程序的现代化。


Sean Iannuzzi 在技术行业工作了 20 余年,在弥合当今大量的社交网络、大数据、数据库解决方案、云计算、电子商务和财务应用程序等技术和业务愿景之间的差距方面扮演着重要角色。Iannuzzi 在超过 50 个独特技术平台上都有相关经验,并获得了十多个技术奖项/认证,专注于推动技术进步和解决方案发展以帮助实现业务目标。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Jesse Squire


在 MSDN 杂志论坛讨论这篇文章