所以,这个问题的答案似乎是“不,你不能真正知道为什么,你可以推断它确实。”
我的意思是,您可以添加一些 Application Insights 日志记录,例如
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));
}
您最终会得到"The application is shutting down because of 'ConfigurationChange'." 或"The application is shutting down because of 'HostingEnvironment'.",但它并不能真正告诉您主机级别发生了什么。
我需要接受的是,App Service 会不时重启,并问自己为什么在乎。 App Service 应该足够聪明,可以在向应用程序池发送请求之前等待应用程序池预热(如重叠回收)。然而,我的应用在回收后会在 CPU 上运行 1-2 分钟。
我花了一段时间才弄明白,但罪魁祸首是我所有的应用程序都有一个重写规则,可以从 HTTP 重定向到 HTTPS。这不适用于应用程序初始化模块:它向根发送一个请求,并且它从 URL Rewrite 模块获得了一个 301 重定向,并且 ASP.NET 管道根本没有受到影响,辛苦的工作没有t实际上完成了。 App Service/IIS 然后认为工作进程已准备好,然后向它发送流量。但是第一个“真正的”请求实际上遵循 301 重定向到 HTTPS URL,并且 bam!该用户遇到了冷启动的痛苦。
I added a rewrite rule described here 免除应用程序初始化模块需要 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 的日记中的众多条目之一 - 事实证明,当某些东西在很少重新启动的传统 VM 上运行时,您可以摆脱很多事情,但它需要一些 TLC 来解决在迁移到我们勇敢的云端新世界时解决问题....
--
2017 年 10 月 27 日更新: 自撰写本文以来,Azure 在“诊断和解决问题”下添加了一个新工具。点击“Web App Restarted”,它会告诉你原因,通常是因为存储延迟或基础设施升级。尽管如此,上述内容仍然有效,因为当迁移到 Azure 应用服务时,最好的前进方式是你真的只需要哄你的应用适应随机重启。
--
2018 年 2 月 11 日更新: 在将多个旧系统迁移到中型应用服务计划的单个实例(具有大量 CPU 和内存开销)之后,我遇到了一个令人烦恼的问题,我的暂存槽的部署可以无缝进行,但每当我因为 Azure 基础设施维护而被引导到新主机时,一切都会陷入混乱,停机时间为 2-3 分钟。我一直在努力弄清楚为什么会这样,因为应用服务应该等到它收到来自您的应用的成功响应后,才能将您引导到新主机。
对此我感到非常沮丧,以至于我准备将应用服务归类为企业垃圾并返回到 IaaS 虚拟机。
事实证明这是多个问题,我怀疑其他人在将他们自己的野兽般的遗留 ASP.NET 应用程序移植到应用服务时会遇到它们,所以我想我会在这里解决所有问题。
首先要检查的是您是否真的在您的Application_Start 中进行实际工作。例如,我正在使用 NHibernate,它虽然擅长很多事情,但在加载其配置方面却是一头猪,所以我确保在 Application_Start 期间实际创建 SessionFactory 以确保完成艰苦的工作。
如上所述,要检查的第二件事是您没有干扰应用服务预热检查的 SSL 重写规则。如上所述,您可以从重写规则中排除预热检查。或者,自从我最初编写该解决方法以来,应用服务添加了一个 HTTPS Only 标志,允许您在负载均衡器而不是在 web.config 文件中执行 HTTPS 重定向。由于它是在应用程序代码上方的间接层处理的,因此您不必考虑它,因此我建议将 HTTPS Only 标志作为要走的路。
要考虑的第三件事是您是否使用App Service Local Cache Option。简而言之,这是一个选项,应用服务会将应用程序的文件复制到其正在运行的实例的本地存储中,而不是从网络共享中复制,如果您的应用程序不关心它,这是一个很好的选择丢失写入本地文件系统的更改。它提高了 I/O 性能(这很重要,因为请记住,App Service runs on potatoes)并消除了由网络共享的任何维护引起的重新启动。但是,关于应用服务的基础架构升级有一个特定的细微之处,记录不充分,您需要注意。具体来说,Local Cache 选项在第一次请求后在单独的应用程序域中在后台启动,然后当本地缓存准备好时切换到应用程序域。这意味着应用服务将对您的站点发出预热请求,获得成功的响应,将流量指向该实例,但是(哎呀!)现在本地缓存正在后台研磨 I/O,如果您有很多站点在这种情况下,您已经停下来,因为应用服务 I/O 非常可怕。如果您不知道这种情况正在发生,它在日志中看起来很诡异,因为就好像您的应用程序在同一个实例上启动了两次(因为确实如此)。解决方案是遵循此Jet blog post 并创建一个应用程序初始化预热页面来监视环境变量,该环境变量会告诉您本地缓存何时准备就绪。这样,您可以强制应用服务延迟启动到新实例,直到本地缓存完全准备好。这是我用来确保我也可以与数据库通信的一个:
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>
要考虑的第四件事是,有时应用服务会因为看似垃圾的原因重新启动您的应用。似乎将fcnMode 属性设置为Disabled 会有所帮助;如果有人在服务器上使用配置文件或代码,它会阻止运行时重新启动您的应用程序。如果您正在使用暂存槽并以这种方式进行部署,那么这不应该打扰您。但是,如果您希望能够通过 FTP 输入文件并欺骗文件并看到该更改反映在生产中,那么请不要使用此选项:
<httpRuntime fcnMode="Disabled" targetFramework="4.5" />
要考虑的第五件事,这一直是我的主要问题,是您是否使用启用了AlwaysOn 选项的暂存槽。 AlwaysOn 选项的工作原理是每分钟左右对您的站点进行一次 ping 操作,以确保它是温暖的,这样 IIS 就不会停止它。莫名其妙地,this isn't a sticky setting,所以你可能在你的生产和暂存槽上都打开了AlwaysOn,这样你就不必每次都搞砸了。这会导致应用服务基础结构升级在将您引导到新主机时出现问题。情况如下:假设您在一个实例上托管了 7 个站点,每个站点都有自己的暂存槽,所有站点都启用了AlwaysOn。应用服务对您的 7 个生产槽进行预热和应用程序初始化,并尽职尽责地等待它们成功响应,然后再重定向流量。 但它不会对暂存槽执行此操作。因此它将流量引导到新实例,但随后 AlwaysOn 会在 1-2 分钟后在暂存槽上启动,所以现在你有了另有 7 个站点同时启动。请记住,App Service runs on potatoes,因此同时发生的所有这些额外 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”,因为它会与插槽交换,并且具有它在所有插槽上都会导致性能问题。在某些时候我不清楚,they seem to have fixed this so that "AlwaysOn" isn't swapped。我的脚本实际上仍然在使用 AlwaysOn,但实际上它现在最终变成了无操作。因此,为您的暂存槽关闭 AlwaysOn 的建议仍然有效,但您不应该再在脚本中做这个小杂耍了。