使用 IIS 托管 ASP.NET Core(.NET 6) 应用时的预热与空闲回收问题

Posted by

on

前言

我曾使用 ASP.NET Core 写过一个个人用简易微服务引擎(现名:iEdon Acorle),集成微服务网关用于管理分散的各接口。这个引擎最令我引以为傲的时候大概就是当年我使用它为校园小程序提供服务的那段时间了。当时最大的吞吐量(期末)我依稀记得有 40 req/s。这个数字显然非常的低,我也没有对这套系统做过任何压力测试。当前这个引擎集成了简化的配置中心 + 注册中心 + 网关,持久化采用了 MySQL。如果需要横向扩展,可以考虑 MySQL 的主从。但是对于个人使用而言,足够用了。也没必要上 Raft 等协议。

这个引擎在现在被我用于 个人 API(Demo: 纯真 IP 库)。博客主页的背景图片是 Bing 每日一图的跳转微服务。It just works.

这套系统部署在一台 Ubuntu Server 20.04 LTS,运行良好。后来,我想折腾一下。我手里有一台空闲的 Windows Server 物理机,想要简单设置一下基于 DNS 的负载均衡(效果即解析的 IP 在这两台系统之间浮动)。一切也毫无问题。直到我发现…有时候博客首页的背景图片无法正常加载,响应代码 404。奇怪的是刷新后,该问题在一段时间内再也无法复现。

问题

首先根据响应代码,我检查了 Acorle 的代码。

private IActionResult PlainError(ResponseCodeType responseCode)
{
    HttpContext.Response.Headers.Add("Cache-Control", "no-store,no-cache");
    HttpContext.Response.Headers.Add("Pragma", "no-cache");
    HttpContext.Response.StatusCode = responseCode switch
    {
        ResponseCodeType.RpcResponseError or ResponseCodeType.ServerException => StatusCodes.Status500InternalServerError,
        ResponseCodeType.InvalidBody or ResponseCodeType.BadRequest => StatusCodes.Status400BadRequest,
        ResponseCodeType.Forbidden => StatusCodes.Status403Forbidden,
        ResponseCodeType.SvcNotFoundOrUnavailable or ResponseCodeType.RpcInvalidZone or ResponseCodeType.SvcInvalidZone or ResponseCodeType.NotFound => StatusCodes.Status404NotFound,
        ResponseCodeType.MethodNotAllowed => StatusCodes.Status405MethodNotAllowed,
        ResponseCodeType.BadGateway => StatusCodes.Status502BadGateway,
        _ => StatusCodes.Status503ServiceUnavailable,
    };
    return new JsonResult(PacketHandler.MakePlainErrorObject(responseCode));
}

只有在业务状态为 SvcNotFoundOrUnavailable, RpcInvalidZone, SvcInvalidZone 或 NotFound 时,HTTP 的响应码才有可能被设置为 404。根据业务日志中并无相关日志的情况,我很快就锁定到状态 SvcInvalidZone。

if (!Registry.Zones.TryGetValue(requestZone, out Zone zone))
{
    response.Code = ResponseCodeType.SvcInvalidZone;
    return response;
}

当定位到这里的时候,问题就很明确了,即注册中心的数据库中并没有该微服务的注册记录。这显然有问题:程序启动时会立即从持久层数据库中恢复注册记录,微服务也会在注册期限内定期注册“续命”,以防止健康状态被判定为“不健康”。那么问题的最大可能结果就是:应用刚启动,并且没有从数据库中同步数据。但是服务器一直在线。

直到我观察到 IIS 工作进程的 CPU 时间只有 00:00:03,我顿悟:工作进程被回收了。新的请求到来,IIS 立即激活 WAS,加载托管代码。然而应用刚加载,还没有来得及从数据库中同步完注册信息时,线程控制权已经被交给 Controller 了。

这也解释了为什么相同的问题目前没有在 Linux 下遇到,因为在 Linux 下我使用 systemd 来托管 Acorle,并使用 nginx 反向代理 Acorle 监听的 Unix Socket。Acorle 在后台是常驻的。

解决记录

既然问题在于 IIS 的工作进程回收机制,那么我们只要关掉 IIS 的进程回收,让应用常驻即可。

  1. “IIS 管理器” -> “应用程序池” -> “你的应用” -> “高级设置” -> “回收” -> “固定时间间隔(分钟)”,修改为 “0”
  2. “IIS 管理器” -> “应用程序池” -> “你的应用” -> “高级设置” -> “(常规)” -> “启动模式”,修改为 “AlwaysRunning”
  3. “IIS 管理器” -> “网站” -> “你的网站” -> “高级设置” -> “预加载已启用”,修改为 “True”

按照这个思路,应当就能保证工作进程在启动时立即加载应用,并且不会在空闲时回收…才怪。

根据网上的参考方案,原来还需要在 “服务器管理器” 中为 IIS 添加 “应用程序初始化” 功能。才怪。安装完成后,问题并没有像网友们那样得到解决。直到我注意到这段话:

The applicationInitialization element tells IIS that it should issue a request to the application’s root Url (“/” in this example) in order to initialize the application.

这意味着,IIS 的应用程序初始化,其实是“很简陋”的实现。在工作进程初始化的时候,自己请求一下自己,以拉起应用程序处理器。那么一定是“自己访问自己”这块的逻辑出现了问题。终于我得知:应用程序初始化不适用于 HTTPS 站点。

解决思路很简单,同时为站点提供 HTTP 服务,将正常请求 301 至 HTTPS,将初始化请求正常处理。我们只需要利用 URL Rewrite 模块即可:

<rewrite>
    <rules>
        <rule name="No redirect on warmup request (request from localhost with warmup user agent)"
        stopProcessing="true">
            <match url=".*" />
            <conditions>
                <add input="{HTTP_HOST}" pattern="localhost" />
                <add input="{HTTP_USER_AGENT}" pattern="Initialization" />
            </conditions>
            <action type="Rewrite" url="{URL}" />
         </rule>
         <rule name="HTTP to HTTPS redirect for all requests" stopProcessing="true">
            <match url="(.*)" />
            <conditions>
                <add input="{HTTPS}" pattern="off" />
            </conditions>
            <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" />
        </rule>
    </rules>
</rewrite>

设定完成后,无论是手工回收工作进程,还是重启网站,都能看到工作进程的内存显著比之前增加(即应用程序在工作进程启动时被加载了)。

IIS 工作进程的初始化内存

一件落着。

参考

1. IIS Idle Timeout #12232, dotnet/AspNetCore.Docs, GitHub, https://github.com/dotnet/AspNetCore.Docs/issues/12232

2. Application Initialization module fails when web site requires SSL, Microsoft Docs, https://docs.microsoft.com/en-US/troubleshoot/aspnet/application-fail-ssl-web


13 responses to “使用 IIS 托管 ASP.NET Core(.NET 6) 应用时的预热与空闲回收问题”

  1. 是我的知识盲区,基本看不懂 :dinosaur-sweat:

    1. 有兴趣可以学一学,没有可以当故事看一看 😛

      1. 看不懂的故事 正常人谁能把这个当故事😂😂😂

  2. 叶开楗 Avatar
    叶开楗

    知道 asp.net 知道 IIS 就是不懂文章内容 哈哈。

  3. 时间停留在12/01

      1. 摸鱼划水 快乐 :dinosaur-shy:

  4. 我来踩空间

  5. 摸鱼也不来更新一下博客

    1. 今年浑身犯病,没几个月就要去一次医院…心情也比较低落,暂缓更新了

  6. linux环境也出现空闲后请求响应慢,请教下是什么原因可以怎么修改

  7. 博主,交换友情链接吗?

    1. 啊这?和百度交换吗 OvO

Leave a Reply

Your email address will not be published. Required fields are marked *