Azure App Service 重启的原因能否确定?

42

我有一堆网站在一个Azure App Service实例上运行,并且它们都设置为始终运行。它们突然同时重新启动,导致所有请求都变成了冷请求,使得一切都变得缓慢了几分钟。

如果服务将我移动到新主机上,我会理解这种情况,但事实并非如此 -- 我仍然使用相同的主机名。

重新启动时CPU和内存使用量均正常,并且我没有启动任何部署或类似的操作。我没有看到明显的重新启动原因。

是否有任何日志记录可以让我查看以弄清它们为什么重新启动?还是这只是App Service偶尔会发生的正常情况?


1
我可以从我的log4net跟踪中看到提示,这些提示转发给AI,表明这是一个“优雅”的重启,因为我在实例上托管了Azure Functions应用程序,并且它的CancellationToken被调用了。但是,没有任何迹象表明为什么它们都重新启动了。 - Nicholas Piasecki
我非常确定我无法访问App Service上的配置文件,因为这不是IaaS VM。 - Nicholas Piasecki
1
它们突然同时重新启动了。问题是偶尔出现还是经常出现?此外,如果可能的话,请尝试为您的应用程序扩展到其他实例,并检查是否有助于缓解问题。 - Fei Han
1
你是否在基本/标准/高级应用服务计划上运行你的应用程序?如果是,你可以使用“资源健康状况”来检查资源状态以及它是否按预期运行。这可能会给你更多的见解。 - Mihir
它偶尔会出现 - 实际上,这是我第一次看到它在没有主机名更改的情况下发生。这是一个标准计划,资源健康状况表明一切正常。RAM恒定在75%,CPU始终在30-40%之间,这是正常的。只是所有8个应用程序同时重新启动,而主机名没有更改,我可以通过每个应用程序在Kudu中的站点运行时间来确定。我可以接受它们在晚上重新启动,但是下午4:40 EDT突然回收所有东西的时间很奇怪。 - Nicholas Piasecki
显示剩余4条评论
2个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
75

所以,似乎答案是“不,你实际上无法知道为什么,你只能推断它确实发生了。”

我的意思是,你可以添加一些应用洞察日志记录,例如:

    private void Application_End()
    {
        log.Warn($"The application is shutting down because of '{HostingEnvironment.ShutdownReason}'.");

        TelemetryConfiguration.Active.TelemetryChannel.Flush();

        // Server Channel flush is async, wait a little while and hope for the best
        Thread.Sleep(TimeSpan.FromSeconds(2)); 
    }

你最终会得到"应用程序由于'ConfigurationChange'而关闭。""应用程序由于'HostingEnvironment'而关闭。",但它实际上并没有告诉你主机级别发生了什么。

我需要接受的是,App Service 会不时地重新启动,然后问自己为什么在意。App Service 应该足够聪明,在将请求发送到应用程序池之前等待其预热(如重叠回收)。然而,我的应用程序在重新启动后会在 CPU 上卡住 1-2 分钟。

我花了一段时间才弄清楚,但罪魁祸首是我所有应用程序都有一个重写规则,将 HTTP 重定向到 HTTPS。这与 Application Initialization 模块不兼容:它向根发送一个请求,但只会从 URL Rewrite 模块获得一个 301 重定向,并且 ASP.NET 管道根本没有被触发,真正的工作实际上并未完成。然后,App Service/IIS 认为工作进程已准备好,然后将流量发送到它。但第一个“真正”的请求实际上会遵循 301 重定向到 HTTPS URL,然后用户就会遇到冷启动的痛苦。

我添加了一个重写规则,如此描述,以免 Application Initialization 模块需要 HTTPS,因此当它访问站点的根时,它将实际触发页面加载和整个管道:

<rewrite>
  <rules>
    <clear />
    <rule name="Do not force HTTPS for application initialization" enabled="true" 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="Force HTTPS" enabled="true" stopProcessing="true">
      <match url="(.*)" ignoreCase="false" />
      <conditions>
        <add input="{HTTPS}" pattern="off" />
      </conditions>
      <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" appendQueryString="true" redirectType="Permanent" />
    </rule>
  </rules>
</rewrite>

这是移动旧应用程序到Azure日记中的许多条目之一--事实证明,当某些东西在传统虚拟机上运行且很少重启时,你可以逃脱很多事情,但是当迁移到我们勇敢的云新世界时,它需要一些关爱来解决问题....

--

更新于2017年10月27日: 自本文写作以来,Azure在“诊断和解决问题”下增加了一个新工具。单击“Web应用程序已重新启动”,它会告诉您原因,通常是由于存储延迟或基础架构升级。尽管如此,在迁移到Azure应用服务时,最好的方法仍然是让您的应用程序适应随机重启。

--

更新于2018年2月11日:将多个传统系统迁移到单个中型应用服务计划实例(有充足的CPU和内存空间),我遇到了一个令人烦恼的问题:从暂存插槽部署时一切顺利,但每当由于Azure基础设施维护而被引导到新主机时,一切都会出现2-3分钟的停机时间。我一直在努力找出为什么会发生这种情况,因为应该在App Service收到您的应用程序成功响应后再引导您到新主机。

我对此感到非常沮丧,准备将App Service归类为企业垃圾并返回到IaaS虚拟机。

事实证明,这是多个问题,并且我怀疑其他人在将自己庞大的传统ASP.NET应用程序移植到App Service时也会遇到它们,因此我想在这里逐一介绍它们。

第一件要检查的事情是确实在Application_Start中执行了真正的工作。例如,我正在使用NHibernate,虽然在许多方面表现良好,但在加载其配置方面相当耗费资源,因此我确保在Application_Start期间实际创建SessionFactory,以确保完成了繁重的工作。

如前所述,第二个需要检查的事情是,您是否有一个重写规则用于SSL,它会干扰App Service的预热检查。您可以像上面提到的那样将预热检查排除在重写规则之外。或者,自从我最初编写这个解决方法以来,App Service已经添加了一个HTTPS Only标志,允许您在负载均衡器中执行HTTPS重定向,而不是在web.config文件中执行。由于它在应用程序代码的上一层间接处理,因此您无需考虑它,因此我建议使用HTTPS Only标志。

第三个需要考虑的是您是否使用App Service本地缓存选项。简而言之,这是一个选项,其中App Service会将您的应用程序文件复制到其正在运行的实例的本地存储中,而不是从网络共享中获取,如果您的应用程序不关心对本地文件系统所做更改的丢失,则启用此选项是一个很好的选择。它可以加快I/O性能(这很重要,因为请记住,App Service在土豆上运行),并消除由于网络共享上的任何维护而导致的重新启动。但是,关于App Service的基础架构升级存在一些特定的微妙问题,文档记录不足,您需要知道。具体来说,本地缓存选项在第一个请求后在单独的应用程序域中后台启动,然后当本地缓存准备就绪时,您将被切换到应用程序域。这意味着App Service将针对您的站点进行预热请求,获得成功响应,将流量指向该实例,但是(哎呀!)现在本地缓存正在后台执行I/O,并且如果您在此实例上有很多站点,那么您已经停滞了,因为App Service I/O非常可怕。如果您不知道正在发生这种情况,则日志看起来很可怕,因为似乎您的应用程序在同一实例上启动了两次(因为确实是如此)。解决方案是遵循Jet博客文章并创建一个应用程序初始化预热页面,以监视告诉您何时准备好本地缓存的环境变量。这样,您可以强制App Service延迟将您引导到新实例,直到本地缓存完全准备就绪。这是我用来确保我也可以与数据库通信的方法:
public class WarmupHandler : IHttpHandler
{
    public bool IsReusable
    {
        get
        {
            return false;
        }
    }

    public ISession Session
    {
        get;
        set;
    }

    public void ProcessRequest(HttpContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }

        var request = context.Request;
        var response = context.Response;

        var localCacheVariable = Environment.GetEnvironmentVariable("WEBSITE_LOCAL_CACHE_OPTION");
        var localCacheReadyVariable = Environment.GetEnvironmentVariable("WEBSITE_LOCALCACHE_READY");
        var databaseReady = true;

        try
        {
            using (var transaction = this.Session.BeginTransaction())
            {
                var query = this.Session.QueryOver<User>()
                    .Take(1)
                    .SingleOrDefault<User>();
                transaction.Commit();
            }
        }
        catch
        {
            databaseReady = false;
        }

        var result = new
        {
            databaseReady,
            machineName = Environment.MachineName,
            localCacheEnabled = "Always".Equals(localCacheVariable, StringComparison.OrdinalIgnoreCase),
            localCacheReady = "True".Equals(localCacheReadyVariable, StringComparison.OrdinalIgnoreCase),
        };

        response.ContentType = "application/json";

        var warm = result.databaseReady && (!result.localCacheEnabled || result.localCacheReady);

        response.StatusCode = warm ? (int)HttpStatusCode.OK : (int)HttpStatusCode.ServiceUnavailable;

        var serializer = new JsonSerializer();
        serializer.Serialize(response.Output, result);
    }
}

同时记得映射路由并将应用程序初始化添加到你的web.config文件中:

<applicationInitialization doAppInitAfterRestart="true">
  <add initializationPage="/warmup" />
</applicationInitialization>
考虑的第四件事是,有时候App Service会因为看似无意义的原因重新启动你的应用程序。设置fcnMode属性为Disabled似乎可以帮助解决这个问题;它可以防止运行时在服务器上更改配置文件或代码时重新启动您的应用程序。如果您正在使用暂存槽并通过该方式进行部署,则不应受到此影响。但是,如果您希望能够通过FTP更改文件并在生产环境中看到更改反映出来,请勿使用此选项:
     <httpRuntime fcnMode="Disabled" targetFramework="4.5" />
第五个需要考虑的问题,而这主要是一直以来我的问题,就是你是否使用了启用了AlwaysOn选项的暂存槽。 AlwaysOn选项通过每分钟或更长时间的ping您的站点来工作,以确保它处于热状态,以便IIS不会将其关闭。莫名其妙的是,这不是一个固定设置,因此您可能已经在生产和暂存槽上都开启了AlwaysOn,以便每次都无需操作。这会在App Service基础结构升级时出现问题,当他们将您引导到新主机时。以下是发生的情况:假设您在一个实例上托管了7个站点,每个站点都有自己的暂存槽,所有内容都启用了AlwaysOn。 App Service对您的7个生产槽进行预热和应用程序初始化,并在成功响应后等待重定向流量。但是,它没有为暂存槽执行此操作。因此,它将流量重定向到新实例,但随后1-2分钟后,AlwaysOn在暂存槽上启动,因此现在您有7个站点同时启动。请记住,App Service运行在土豆上,因此所有这些额外的I / O同时发生将破坏生产槽的性能,并且会被认为是停机时间。

解决方案是在您的暂存槽上关闭AlwaysOn,这样在基础结构更新后就不会遭受这种同时进行的I/O狂潮的攻击。如果您正在使用通过PowerShell的交换脚本,保持在暂存中处于关闭状态,在生产中处于开启状态,这一点实际上需要非常冗长地执行:

Login-AzureRmAccount -SubscriptionId {{ YOUR_SUBSCRIPTION_ID }}

$resourceGroupName = "YOUR-RESOURCE-GROUP"
$appName = "YOUR-APP-NAME"
$slotName = "YOUR-SLOT-NAME-FOR-EXAMPLE-STAGING"

$props = @{ siteConfig = @{ alwaysOn = $true; } }

Set-AzureRmResource `
    -PropertyObject $props `
    -ResourceType "microsoft.web/sites/slots" `
    -ResourceGroupName $resourceGroupName `
    -ResourceName "$appName/$slotName" `
    -ApiVersion 2015-08-01 `
    -Force

Swap-AzureRmWebAppSlot `
    -SourceSlotName $slotName `
    -ResourceGroupName $resourceGroupName `
    -Name $appName

$props = @{ siteConfig = @{ alwaysOn = $false; } }

Set-AzureRmResource `
    -PropertyObject $props `
    -ResourceType "microsoft.web/sites/slots" `
    -ResourceGroupName $resourceGroupName `
    -ResourceName "$appName/$slotName" `
    -ApiVersion 2015-08-01 `
    -Force
此脚本将设置暂存插槽开启AlwaysOn,进行交换,使暂存成为生产环境,然后关闭暂存插槽的AlwaysOn,以避免基础设施升级后导致故障。一旦您成功运行此脚本,确实很好地实现了PaaS的安全更新和硬件故障处理功能。但实际上要达到这一点比营销材料所建议的要更加困难。希望这能对某些人有所帮助。

--

更新于2020年7月17日:在上面的简介中,我谈到如果您使用分阶段插槽,则需要调整“AlwaysOn”,因为它将与插槽交换,并且在所有插槽上都开启可能会导致性能问题。在某个我不清楚的时间点,他们似乎已经修复了这个问题,使得“AlwaysOn”不再被交换。我的脚本实际上仍然会对AlwaysOn进行调整,但实际上它最终变成了一个无操作。因此,保持AlwaysOn关闭以用于您的分阶段插槽的建议仍然有效,但您不再需要在脚本中执行这个小技巧。


我点赞了因为你的更新。之前不知道这个功能,现在很有帮助。谢谢! - Michael Adamission
@NeilThompson 感谢您记录您的发现所做出的努力。 这个问题在 Azure 中已经持续半年以上了,微软不知道有些人实际上在生产中使用 Azure 吗? - Simon
我不得不在“alwaysOn”周围使用引号才能使其工作。Set-AzureRmResource -PropertyObject @{ “alwaysOn” = $false } -ResourceType $env:resourceType -ResourceGroupName $env:resourceGroupName -ResourceName $env:webAppConfigResourceName -ApiVersion $APIVersion -Force - RClemens
@Nicholas Piasecki 这篇文章真的很有用,谢谢!在 WarmUpHandler 上,对于被抛弃的应用程序域进行任何预热是否有益处?如果第一个应用程序域从不服务请求(例如在此示例中),那么您最好根本不做任何工作吗?例如,您可以绕过数据库就绪检查,因为您知道如果应用程序域未在缓存上运行,则永远无法通过初始化? - Andrew Rimmer
@AndrewRimmer 如果你这样做了(比如检查环境并且如果本地缓存未启用则不返回成功),然后有人在之后禁用了本地缓存,那么你的应用程序将永远无法启动,这可能看起来非常神秘。否则我想那应该可以工作,但是很可能运行时无法成功,我认为在实例上直到第一个非LC请求成功之前,它才会首先启动Local Cache init。你可能需要进行一些实验。S计划在I/O方面仍然很糟糕,但P*v2计划更好。 - Nicholas Piasecki
显示剩余3条评论

2

如果由于OutOfMemoryExceptions而重新启动了您的服务,则应用程序可能会崩溃,导致Application_End无法运行。

我们将ASP.NET 4.8 MVC 5应用程序移至Azure App Services(使用Windows容器),并在上线后遇到了OOM。 应用程序崩溃非常严重,以至于Application_End事件无法记录任何消息。 我们确实在重启之前得到了间歇性的OOME,这些OOME可以通过AppInsights进行分发。

我们的工程师一直在寻找增加网站内存的方法(因为我们在以前的环境中确实使用了很多),但是找不到任何可用的参考资料。 最终,我们被Microsoft支持所拯救,他们建议使用此应用程序设置(添加到配置下)来增加内存:

WEBSITE_MEMORY_LIMIT_MB = 3072

他们将此参考添加到了Azure文档中: https://github.com/MicrosoftDocs/azure-docs/issues/13263#issuecomment-655051828

现在我们的应用程序在高峰时段运行得很顺利,最多会占用大约4200M的内存。我的服务计划有32G内存,有2个应用服务,总共有5个插槽,其中一个配置为使用5120M内存。还剩下大约40%的内存可以用来启动暂存插槽。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,