亚马逊AWS官方博客

基于Windows SaaS应用的现代化改造实践

对于SaaS软件开发团队,将基于Windows .Net的传统应用通过lift-shift方式迁移上云往往只是云端之旅的开始。长期以来,windows应用的运行成本高居不下、单体应用难以灵活扩展、为高峰负载预置云上资源等等,都成为困扰这些开发者的因素。对SaaS应用,在多租户高并发场景下这些矛盾则更为凸显。

本文结合SaaS应用系统的改造实践,介绍了Windows工作负载的现代化改造路径,包括单体应用程序向微服务的分解思路,Windows应用容器化实践,以及使用亚马逊云科技的ECS服务实现容器化改造过程中解决的实际问题。希望通过实践分享,助力您更快实现传统应用向现代化架构的转变。

面临挑战与破局路径

基于windows .Net的SaaS应用,现代化改造路径可参考下图所示:

  • 将应用程序从.Net Frame work移植到.Net core,实现应用向开源平台的迁移
  • 将数据库从SQL Server向PostgreSQL迁移,解除数据库的创新束缚
  • 使用容器和无服务器技术实施DevOps,加速应用的开发迭代
  • 将单体应用向微服务分解以支持应用的复杂性、可用性和便携性,并通过托管服务实现开发和运维效率的提升。

虽然应用程序架构的现代化有诸多好处,但实际改造过程中无论从业务层面还是技术层面都往往困难重重。比如SaaS系统,终端用户通常已经基于SaaS应用提供的SDK和.Net Framework做了二次开发,技术栈的变革会引起现有客户的后相兼容问题;再比如,遗留系统在历史演进过程中可能依赖了大量第三方组件,技术栈更迭将会引入巨大的改造工作和系统的不稳定因素。这些实际问题都阻碍了开发者的前进脚步。

为解决这一困局,在生产实践中我们采用了亚马逊的双向门破局思路。通过分析现有系统,选择相对独立的典型功能模块,将其先行剥离出来,通过微服务的方案落地实现。一方面MVP能够小范围快速试错,减小开发难度和改造范围;另一方面也能在保证主体应用稳定的前提,最大程度控制系统改造对终端用户的影响。

场景选择与改造方案

本次改造我们面向的是ERP SaaS系统,剥离模块选择了ERP系统中的库龄分析模块。如下图对应用服务器监控状态所示,该模块具有如下特点:

  • 运行频率低,但资源占用大。 库龄分析模块每周固定时间运行,运行时间长达20分钟左右;运算逻辑复杂,应用主机资源消耗大,单租户运行将耗费20+GB内存,多租户并发提交请求时资源抢占则更为明显,甚至会影响正常的业务运行。
  • 后台模块,独立运行 行业特征明显,按需计算且计算逻辑独立。与外围系统交互接口很少

改造方案上,我们选择亚马逊云科技的Serverless服务组件承载剥离的负载:

处理流程如下:

windows应用的容器化

以上处理流程中涉及到的关键技术就是对原有windows系统的功能模块做容器化改造

下面将对这部分做详细介绍。

Windows .Net的容器化改造

  • ECS optimizer的AMI 介绍

通过对库龄分析模块的调研,我们发现现有系统依赖了较多第三方组件难以在短时间内寻找.Net core的替代方案,且迁移到.Net core还将涉及到C#语法的变化,改造量较大,很难短时间内完成Serverless的快速验证。因此我们调整策略,选择保留原有.Net的技术框架,直接向windows容器化迁移。本次验证,我们选用亚马逊自研的容器托管平台ECS,其特点是简单高效,易于上手。

AWS提供了针对ECS optimizer的AMI,其上已经安装docker和ECS agent,可以在ECS集群中直接部署。推荐使用经Amazon ECS 优化的 Windows Server 2019 Full AMI和Windows Server 2019 Core AMI两个版本。Full 和core在技术和限制上并无明显区别,主要在于Full是完全安装,具有图形界面,适用于测试时使用;core为最小功能安装,无图形界面,能提供更高性能和资源使用率。

ECS optimizer的AMI 上预装了如下docker image:

amazon/amazon-ecs-pause                                   0.1.0      1b7d575058e5   13 days ago    257MB 
mcr.microsoft.com/windows/servercore                      ltsc2019   4abfec8815bc   2 weeks ago    5.7GB 
mcr.microsoft.com/windows/nanoserver                     1809       1dba00abbe16   2 weeks ago    257MB

Servercore 和nanoserver是基于windows的几个基础镜像,在 Windows Server 2019 上运行的任何 Docker 容器都将基于 Windows Server Core Nano Server。

  • 创建基于windows .Net的ECS task的步骤
    1. 选择.net framework的docker image .net Framework的镜像可参考https://hub.docker.com/_/microsoft-dotnet-framework-runtime/,目前提供4.8和3.5两个版本。基于实际需求,我们使用3.5版本镜像。
    2. 创建应用image
      FROM mcr.microsoft.com/dotnet/framework/runtime:3.5
      ADD / /     ##将当前所在目录下的所有文件拷到docker根目录
      ENTRYPOINT ERPApp.K3.SCM.AWS.Serverless.exe guid1234 invage ## 执行应用
      docker build -t ERPApp.K3.scm.aws.serverless:v2 .
      docker run -it  ERPApp.K3.scm.aws.serverless:v2

      docker images可以看到已经创建好的image,push到ECR

      aws ecr get-login-password --region cn-north-1 | docker login --username AWS --password-stdin AccountID.dkr.ecr.cn-north-1.amazonaws.com.cn/galaxy
      docker tag mypython:v1 AccountID.dkr.ecr.cn-north-1.amazonaws.com.cn/galaxy:v2
      docker push AccountID.dkr.ecr.cn-north-1.amazonaws.com.cn/galaxy:latest
    3. 创建ECS task definition
    • 创建windows的task definition注意container中的的port不能使用80端口 因为凭证代理占用了容器实例上的端口 80,因此,如果您使用 IAM 角色处理任务,则端口 80 不可用于任务

    1. 创建基于winows的ECS cluster

    ECS提供了专用于windows的ECS cluster,选择上面提到的ecs optimized ami构建ECS cluster。

ECS Cluster启动后,将自动生成autoscaling group,实现基于负载的自动扩展。其中autoscaling group的min,desired,max number都为创建cluster时指定的值。可以通过修改这个值和scale policy进行扩展。

这里需要注意,ECS cluster对应的安全组需要打开8080端口,否则提交task时会一直处于pending状态,原因是ECS 的master无法于worker node进行通讯。

  1. 运行任务

将task 提交到创建的cluster中

ECS可以支持基于Windows的Fargate功能,能够更好地体现无服务的按需启动,自动扩展能力。但由于当前尚未在中国区提供服务,本次改造仍使用基于EC2的 ECS cluster。

使用ECS实现容器托管的实际问题解决

实践中我们解决了两个性能问题:

  1. 基于Windows的容器在ECS运行较直接在EC2上运行存在性能问题
  2. 由于Windows的容器镜像size过大,镜像拉取和启动过程都耗时过长

下面逐一对问题的解决进行阐述

基于Windows的容器在ECS运行时的性能提升

POC测试中我们对应用逻辑做了简化。测试发现,同样的docker镜像在windows EC2上直接运行只需要17s,但运行在ECS上竟然需要7分钟。经过深入分析,我们发现这是Linux与windows 对容器的CPU使用限额有不同的处理机制导致的。

如下图,为ECS对容器的CPU使用资源限定。对Linux容器,该设定值为最小资源保证,即实际CPU资源使用会按照配置中设定的额度做对应的比例扩展,以与实例上的其他容器共享未分配的 CPU 单元。例如,如果在单核实例类型上运行一个单容器任务,同时为该容器指定 512 个 CPU 单元,而且这是在容器实例上运行的唯一任务,则该容器可在任何给定间使用完整的 1024 个 CPU 单元份额。但是,如果该容器实例上还启动了同一任务的另一个副本,则保证为每个任务提供最少 512 个 CPU 单元(如果需要),而且每个容器的 CPU 使用量会上浮到更高的值(如果另一容器未在使用它),但如果两个任务都始终处于完全活动状态,则它们将限于 512 个 CPU 单元。

但对windows容器,该设定值为将作为绝对限制或配额强制实施,Windows 容器只能访问任务定义中指定的 CPU 量。Null 或零 CPU 值将作为0传递给 Docker,Windows 将其解释为一个 CPU 的 1%。

参考文档:https://docs.thinkwithwp.com/zh_cn/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_environment

Windows容器刚迁移到ECS时,性能之所以如此缓慢就是因为我们之前没有设定container的CPU使用额度,导致该容器只使用了一个CPU的1%额度。在如上图正确设置了CPU额度后,应用性能恢复正常,与EC2上直接运行表现一致。

Windows镜像拉取&启动过慢的问题解决

问题原因分析

基于Windows的容器镜像尺寸往往很大。如下截图可见,.Net应用的镜像大小为7.9GB,push到ECR后,大小有830.62MB。之间存在尺寸差异,是因为ECS optimizer的AMI中已经包含了servercore和nanoserver,在推送至ECR时会只推送包含应用部分的内容,这部分大小为830.62MB

如此的巨无霸镜像,首次拉取和启动时间通常需要10min之久。如下截图显示了整个启动过程的时间分布:01:34-01:36(2min)为镜像拉取时间,可以看到高网络流量;到01:45(近6min时间)为压缩镜像的解压时间,可以观察到消耗了高磁盘IO。

解决思路探究

镜像拉取和扩展操作并不是仅在容器所在虚拟机启动时执行一次。为防止容器实例存储被长期未使用的镜像占满,ECS 容器代理会根据配置参数定时自动清理已停止的任务和容器实例上的任何任务均未在使用的镜像。在Linux中可以通过/etc/ecs/ecs.config中对ECS_IMAGE_CLEANUP_INTERVAL等变量的修改进行调整,参阅https://docs.thinkwithwp.com/zh_cn/AmazonECS/latest/developerguide/automated_image_cleanup.html。但Windows中该类参数并不生效,而是定时会将一段时间未使用的镜像自动删除。导致当停滞几小时未对ECS windows镜像进行调用时再次调用,就会发生镜像被删除后的重新拉取和解压。

因此,对解决Windows容器的启动问题,如何避免镜像的拉取是需要解决的问题。

问题解决综述

使用亚马逊提供的image builder,可以将应用的container直接打包到ECS-optimized-windows server中形成客户化的AMI镜像,避免了对container的下载。当有新的应用变化时,可以通过Image builder提供的pipeline功能重新构建AMI镜像。

使用image builder创建AMI镜像

使用image builder创建客户化AMI镜像的过程如下:

1.创建recipe:

  • 选择基础镜像
  • 添加客户化组件(比如有需要预先拉取到AMI中的镜像,创建这个组件使用的脚本)
  • user Data (AMI实际启动时执行的脚本)
  1. 启动用于创建AMI的instance实例 创建infra configuration,SSM根据指定的instance configuration启动所需instance
  2. 创建image pipeline 指定创建AMI的schedule,创建客户化AMI镜像。

具体过程如下:

1.创建recipe:

  • 选择基础镜像。 这里选择windows server 2019 full ecs optimized x86。

  • UserData虽然base image已经预装了ecs agent,但需要使用Initialize-ECSAgent 将agent与ECS cluster关联,ECS agent才能正常启动。否则登录到启动的windows server,会发现ECS agent 服务没有在windows service中正常启动。
    <powershell>
    Import-Module ECSTools
    Initialize-ECSAgent -Cluster ECSClusterName -EnableTaskIAMRole 
    </powershell>
  • 添加客户化组件这里添加了客户化的Dockerpull组件,用于拉取客户化镜像。component的创建:Image builder -> Components
     

    name: DockerPull
    description: DockerImageCacheStrategy.
    schemaVersion: 1.0
    ​
    phases:
      - name: build
        steps:
          - name: Dockerpull
            action: ExecutePowerShell
            inputs:
              commands:  --登录到ECR,拉取ECR中的应用镜像到本机
                - (Get-ECRLoginCommand).Password | docker login --username AWS --password-stdin AccountID.dkr.ecr.cn-north-1.amazonaws.com.cn
                - docker pull AccountID.dkr.ecr.cn-north-1.amazonaws.com.cn/dotnet:vnoentry  -- ECR中的客户化镜像
  1. 创建infra configuration制作ami时SSM需要按照instance configuration实际启动instance。不设定,则会按照缺省方式启动实例,可能发生网络问题。应使用下面创建instance配置的章节在可达的subnet中配置,以便登录进去排错。生成配置的json文件,命名为profile001.json
{
    "name": "MyExampleInfrastructure",
    "description": "An example that will retain instances of failed builds",
    "instanceTypes": [
        "t3.large"   -- 启动的机型
    ],
    "instanceProfileName": "admin",  -- 机型使用的role的名称
    "securityGroupIds": [
        "sg-0641dd62"
    ],
    "subnetId": "subnet-9824dffc",
    "logging": {
        "s3Logs": {
            "s3BucketName": "kd199",  --日志数据的桶
            "s3KeyPrefix": "my-path"
        }
    },
    "keyPair": "MADtest",  
    "terminateInstanceOnFailure": false,
    "snsTopicArn": "arn:aws-cn:sns:cn-north-1:AccountID:amibuilder"
}

创建instance configuration:

aws imagebuilder create-infrastructure-configuration --cli-input-json file://profile001.json  -- 使用上面创建的配置文件
  1. 创建image pipeline结合recipe、 infra configuration、创建AMI的schedule等信息创建image pipeline,实现AMI的创建指定schedule:

指定recipe

指定infra config

生成AMI的设置:

  1. 生成AMI执行image pipeline产生AMI,并监控AMI的生成过程

SSM manager中的automation显示了整个AMI的创建过程。这期间会看到EC2中启动了相应的instance。

  1. 创建auto scaling group使用上面生成的含有客户container image的 ECS 优化的AMI创建autoscaling group
  2. 将autoscaling group加入到cluster 的capacity provider

应用的启动

优化后,可以看到第一次启动时间从之前的10min已经缩短为2min,应用的实际运行时长为39s

改造效果分析

对库龄分析模块完成本次改造后,我们对如下三种运行方式做了配置和性能对比:

  • 应用在本地Windows主机运行
  • 以容器方式在EC2运行
  • 使用ECS平台运行

可以看到,使用容器在ECS平台运行剥离出来的库龄分析模块时,只需要配置0.25CPU/100M内存即可提供可接受的处理性能。在多租户提交并发请求时,ECS可基于CPU做横向扩展,满足多租户的并发需求。同时,通过尖峰负载的剥离,原有应用服务器的CPU可以长期稳定在20%,便于实现原有主机瘦身,甚至配置减半的目标,以实现成本优化。

在库龄分析模块改造完成后,我们继续对改造系统的其余模块所需工作量和可能性进行分析:

图中,黑色标注的模块部分,包括S3存储、Lambda函数、Step functions、结果的API展示都为公共模块,可以共用。对新剥离的模块,红色标注部分为需要进行修改的部分:

  • 数据准备模块不同模块需要单独处理,抽取计算过程中需要的原始数据
  • 业务处理 对模块所需依赖库和外部依赖的分析,对数据库从Sqlserver迁移至postgreSQL的分析。

通过对典型负载模块的迁移和后续改造的工作量分析,我们发现小步试错帮助我们对迁移过程、技术难点都有了较为清晰的了解,对继续推进后续的应用改造也做到了心中有数。

本篇作者

倪惠青

亚马逊云科技 解决方案架构师,负责基于AWS云计算方案架构的咨询和设计,在国内推广AWS云平台技术和各种解决方案。在加入AWS 之前曾在Oracle,Microsoft工作多年,负责企业公有云方案咨询和架构设计,在基础架构及大数据方面有丰富经验。

崔智刚

金蝶软件(中国)有限公司金蝶云·星空技术架构师,主要从事数据库和性能优化方面的工作。金蝶云·星空聚焦多组织、多利润中心的大中型企业,为数字经济时代的企业提供开放的ERP 云平台,帮助企业实现数字化营销新生态及管理重构。

孙华

亚马逊云科技 资深无服务器产品专家。 在过去的20多年一直从事软件架构、程序开发以及技术推广等领域的工作。他擅长Web领域应用、SaaS系统和云呼叫中心开发,也从事过多个大型软件项目的设计、开发与项目管理。目前他专注与云计算以及互联网等技术领域,致力于帮助中国的 开发者构建基于云计算的新一代的互联网应用。