Skip to content

Commit 7e9e8e1

Browse files
committed
Linux consumption cold start improvements. Part - 2
Part 1 - https://github.com/Azure/azure-functions-host/pull/6408/files This change addresses the following improvments based on profiling 1. Metrics publisher invokes expensive Process apis to collect memory metrics during cold start. Deferring this by 2 seconds moves it out of critical path. 2. Some of the operations in specialization code path were not being jit-ed since the warmup specialization call doesn't invoke all the operations. This change decrypts the host assignment context, validates and downloads a fake Website-Run-From-Pkg url to jit HttpDownload calls. The fake url points to a local endpoint and the downloaded contents are ignored. Fixes #6407
1 parent 107d72f commit 7e9e8e1

File tree

14 files changed

+401
-129
lines changed

14 files changed

+401
-129
lines changed

src/WebJobs.Script.WebHost/ContainerManagement/LinuxContainerInitializationHostService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ private async Task ApplyStartContextIfPresent()
5353
var encryptedAssignmentContext = JsonConvert.DeserializeObject<EncryptedHostAssignmentContext>(startContext);
5454
var assignmentContext = _startupContextProvider.SetContext(encryptedAssignmentContext);
5555

56-
bool success = _instanceManager.StartAssignment(assignmentContext, false);
56+
bool success = _instanceManager.StartAssignment(assignmentContext);
5757
_logger.LogInformation($"StartAssignment invoked (Success={success})");
5858
}
5959
else

src/WebJobs.Script.WebHost/Controllers/InstanceController.cs

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -39,34 +39,26 @@ public async Task<IActionResult> Assign([FromBody] EncryptedHostAssignmentContex
3939
{
4040
_logger.LogDebug($"Starting container assignment for host : {Request?.Host}. ContextLength is: {encryptedAssignmentContext.EncryptedContext?.Length}");
4141

42-
bool succeeded = false;
43-
if (!encryptedAssignmentContext.IsWarmup)
44-
{
45-
var assignmentContext = _startupContextProvider.SetContext(encryptedAssignmentContext);
46-
47-
// before starting the assignment we want to perform as much
48-
// up front validation on the context as possible
49-
string error = await _instanceManager.ValidateContext(assignmentContext, encryptedAssignmentContext.IsWarmup);
50-
if (error != null)
51-
{
52-
return StatusCode(StatusCodes.Status400BadRequest, error);
53-
}
54-
55-
// Wait for Sidecar specialization to complete before returning ok.
56-
// This shouldn't take too long so ok to do this sequentially.
57-
error = await _instanceManager.SpecializeMSISidecar(assignmentContext, encryptedAssignmentContext.IsWarmup);
58-
if (error != null)
59-
{
60-
return StatusCode(StatusCodes.Status500InternalServerError, error);
61-
}
42+
var assignmentContext = _startupContextProvider.SetContext(encryptedAssignmentContext);
6243

63-
succeeded = _instanceManager.StartAssignment(assignmentContext, encryptedAssignmentContext.IsWarmup);
44+
// before starting the assignment we want to perform as much
45+
// up front validation on the context as possible
46+
string error = await _instanceManager.ValidateContext(assignmentContext);
47+
if (error != null)
48+
{
49+
return StatusCode(StatusCodes.Status400BadRequest, error);
6450
}
65-
else
51+
52+
// Wait for Sidecar specialization to complete before returning ok.
53+
// This shouldn't take too long so ok to do this sequentially.
54+
error = await _instanceManager.SpecializeMSISidecar(assignmentContext);
55+
if (error != null)
6656
{
67-
succeeded = true;
57+
return StatusCode(StatusCodes.Status500InternalServerError, error);
6858
}
6959

60+
var succeeded = _instanceManager.StartAssignment(assignmentContext);
61+
7062
return succeeded
7163
? Accepted()
7264
: StatusCode(StatusCodes.Status409Conflict, "Instance already assigned");

src/WebJobs.Script.WebHost/Management/IInstanceManager.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ public interface IInstanceManager
1111
{
1212
IDictionary<string, string> GetInstanceInfo();
1313

14-
Task<string> ValidateContext(HostAssignmentContext assignmentContext, bool isWarmup);
14+
Task<string> ValidateContext(HostAssignmentContext assignmentContext);
1515

16-
bool StartAssignment(HostAssignmentContext assignmentContext, bool isWarmup);
16+
bool StartAssignment(HostAssignmentContext assignmentContext);
1717

18-
Task<string> SpecializeMSISidecar(HostAssignmentContext assignmentContext, bool isWarmup);
18+
Task<string> SpecializeMSISidecar(HostAssignmentContext assignmentContext);
1919
}
2020
}

src/WebJobs.Script.WebHost/Management/InstanceManager.cs

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ public InstanceManager(IOptionsFactory<ScriptApplicationHostOptions> optionsFact
4646
_optionsFactory = optionsFactory ?? throw new ArgumentNullException(nameof(optionsFactory));
4747
}
4848

49-
public async Task<string> SpecializeMSISidecar(HostAssignmentContext context, bool isWarmup)
49+
public async Task<string> SpecializeMSISidecar(HostAssignmentContext context)
5050
{
51-
if (isWarmup)
51+
// No cold start optimization needed for side car scenarios
52+
if (context.IsWarmupRequest)
5253
{
5354
return null;
5455
}
@@ -89,16 +90,19 @@ public async Task<string> SpecializeMSISidecar(HostAssignmentContext context, bo
8990
return null;
9091
}
9192

92-
public bool StartAssignment(HostAssignmentContext context, bool isWarmup)
93+
public bool StartAssignment(HostAssignmentContext context)
9394
{
9495
if (!_webHostEnvironment.InStandbyMode)
9596
{
9697
_logger.LogError("Assign called while host is not in placeholder mode");
9798
return false;
9899
}
99100

100-
if (isWarmup)
101+
if (context.IsWarmupRequest)
101102
{
103+
// Based on profiling download code jit-ing holds up cold start.
104+
// Pre-jit to avoid paying the cost later.
105+
Task.Run(async () => await Download(context.GetRunFromPkgContext()));
102106
return true;
103107
}
104108
else if (_assignmentContext == null)
@@ -134,16 +138,11 @@ public bool StartAssignment(HostAssignmentContext context, bool isWarmup)
134138
}
135139
}
136140

137-
public async Task<string> ValidateContext(HostAssignmentContext assignmentContext, bool isWarmup)
141+
public async Task<string> ValidateContext(HostAssignmentContext assignmentContext)
138142
{
139-
if (isWarmup)
140-
{
141-
return null;
142-
}
143-
144-
_logger.LogInformation($"Validating host assignment context (SiteId: {assignmentContext.SiteId}, SiteName: '{assignmentContext.SiteName}')");
143+
_logger.LogInformation($"Validating host assignment context (SiteId: {assignmentContext.SiteId}, SiteName: '{assignmentContext.SiteName}'. IsWarmup: '{assignmentContext.IsWarmupRequest}')");
145144
RunFromPackageContext pkgContext = assignmentContext.GetRunFromPkgContext();
146-
_logger.LogInformation($"Will be using {pkgContext.EnvironmentVariableName} app setting as zip url");
145+
_logger.LogInformation($"Will be using {pkgContext.EnvironmentVariableName} app setting as zip url. IsWarmup: '{assignmentContext.IsWarmupRequest}')");
147146

148147
if (pkgContext.IsScmRunFromPackage())
149148
{
@@ -154,7 +153,7 @@ public async Task<string> ValidateContext(HostAssignmentContext assignmentContex
154153
{
155154
// In AppService, ZipUrl == 1 means the package is hosted in azure files.
156155
// Otherwise we expect zipUrl to be a blobUri to a zip or a squashfs image
157-
(var error, var contentLength) = await ValidateBlobPackageContext(pkgContext.Url);
156+
(var error, var contentLength) = await ValidateBlobPackageContext(pkgContext);
158157
if (string.IsNullOrEmpty(error))
159158
{
160159
assignmentContext.PackageContentLength = contentLength;
@@ -172,8 +171,12 @@ public async Task<string> ValidateContext(HostAssignmentContext assignmentContex
172171
}
173172
}
174173

175-
private async Task<(string, long?)> ValidateBlobPackageContext(string blobUri)
174+
private async Task<(string, long?)> ValidateBlobPackageContext(RunFromPackageContext context)
176175
{
176+
string blobUri = context.Url;
177+
string eventName = context.IsWarmUpRequest
178+
? MetricEventNames.LinuxContainerSpecializationZipHeadWarmup
179+
: MetricEventNames.LinuxContainerSpecializationZipHead;
177180
string error = null;
178181
HttpResponseMessage response = null;
179182
long? contentLength = null;
@@ -186,7 +189,7 @@ await Utility.InvokeWithRetriesAsync(async () =>
186189
{
187190
try
188191
{
189-
using (_metricsLogger.LatencyEvent(MetricEventNames.LinuxContainerSpecializationZipHead))
192+
using (_metricsLogger.LatencyEvent(eventName))
190193
{
191194
var request = new HttpRequestMessage(HttpMethod.Head, blobUri);
192195
response = await _client.SendAsync(request);
@@ -199,7 +202,7 @@ await Utility.InvokeWithRetriesAsync(async () =>
199202
}
200203
catch (Exception e)
201204
{
202-
_logger.LogError(e, $"{MetricEventNames.LinuxContainerSpecializationZipHead} failed");
205+
_logger.LogError(e, $"{eventName} failed");
203206
throw;
204207
}
205208
}, maxRetries: 2, retryInterval: TimeSpan.FromSeconds(0.3)); // Keep this less than ~1s total
@@ -208,7 +211,7 @@ await Utility.InvokeWithRetriesAsync(async () =>
208211
catch (Exception e)
209212
{
210213
error = $"Invalid zip url specified (StatusCode: {response?.StatusCode})";
211-
_logger.LogError(e, "ValidateContext failed");
214+
_logger.LogError(e, $"ValidateContext failed. IsWarmupRequest = {context.IsWarmUpRequest}");
212215
}
213216

214217
return (error, contentLength);
@@ -372,25 +375,28 @@ private async Task<string> Download(RunFromPackageContext pkgContext)
372375
if (pkgContext.PackageContentLength != null && pkgContext.PackageContentLength > 100 * 1024 * 1024)
373376
{
374377
_logger.LogInformation($"Downloading zip contents from '{cleanedUrl}' using aria2c'");
375-
AriaDownload(tmpPath, fileName, zipUri);
378+
AriaDownload(tmpPath, fileName, zipUri, pkgContext.IsWarmUpRequest);
376379
}
377380
else
378381
{
379382
_logger.LogInformation($"Downloading zip contents from '{cleanedUrl}' using httpclient'");
380-
await HttpClientDownload(filePath, zipUri);
383+
await HttpClientDownload(filePath, zipUri, pkgContext.IsWarmUpRequest);
381384
}
382385

383386
return filePath;
384387
}
385388

386-
private async Task HttpClientDownload(string filePath, Uri zipUri)
389+
private async Task HttpClientDownload(string filePath, Uri zipUri, bool isWarmupRequest)
387390
{
388391
HttpResponseMessage response = null;
389392
await Utility.InvokeWithRetriesAsync(async () =>
390393
{
391394
try
392395
{
393-
using (_metricsLogger.LatencyEvent(MetricEventNames.LinuxContainerSpecializationZipDownload))
396+
var downloadMetricName = isWarmupRequest
397+
? MetricEventNames.LinuxContainerSpecializationZipDownloadWarmup
398+
: MetricEventNames.LinuxContainerSpecializationZipDownload;
399+
using (_metricsLogger.LatencyEvent(downloadMetricName))
394400
{
395401
var request = new HttpRequestMessage(HttpMethod.Get, zipUri);
396402
response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
@@ -403,31 +409,36 @@ await Utility.InvokeWithRetriesAsync(async () =>
403409
_logger.LogError(e, error);
404410
throw;
405411
}
406-
_logger.LogInformation($"{response.Content.Headers.ContentLength} bytes downloaded");
412+
_logger.LogInformation($"{response.Content.Headers.ContentLength} bytes downloaded. IsWarmupRequest = {isWarmupRequest}");
407413
}, 2, TimeSpan.FromSeconds(0.5));
408414

409-
using (_metricsLogger.LatencyEvent(MetricEventNames.LinuxContainerSpecializationZipWrite))
415+
using (_metricsLogger.LatencyEvent(isWarmupRequest ? MetricEventNames.LinuxContainerSpecializationZipWriteWarmup : MetricEventNames.LinuxContainerSpecializationZipWrite))
410416
{
411417
using (var content = await response.Content.ReadAsStreamAsync())
412418
using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
413419
{
414420
await content.CopyToAsync(stream);
415421
}
416-
_logger.LogInformation($"{response.Content.Headers.ContentLength} bytes written");
422+
_logger.LogInformation($"{response.Content.Headers.ContentLength} bytes written. IsWarmupRequest = {isWarmupRequest}");
417423
}
418424
}
419425

420-
private void AriaDownload(string directory, string fileName, Uri zipUri)
426+
private void AriaDownload(string directory, string fileName, Uri zipUri, bool isWarmupRequest)
421427
{
422-
(string stdout, string stderr, int exitCode) = RunBashCommand($"aria2c --allow-overwrite -x12 -d {directory} -o {fileName} '{zipUri}'", MetricEventNames.LinuxContainerSpecializationZipDownload);
428+
var metricName = isWarmupRequest
429+
? MetricEventNames.LinuxContainerSpecializationZipDownloadWarmup
430+
: MetricEventNames.LinuxContainerSpecializationZipDownload;
431+
(string stdout, string stderr, int exitCode) = RunBashCommand(
432+
$"aria2c --allow-overwrite -x12 -d {directory} -o {fileName} '{zipUri}'",
433+
metricName);
423434
if (exitCode != 0)
424435
{
425436
var msg = $"Error downloading package. stdout: {stdout}, stderr: {stderr}, exitCode: {exitCode}";
426437
_logger.LogError(msg);
427438
throw new InvalidOperationException(msg);
428439
}
429440
var fileInfo = FileUtility.FileInfoFromFileName(Path.Combine(directory, fileName));
430-
_logger.LogInformation($"{fileInfo.Length} bytes downloaded");
441+
_logger.LogInformation($"{fileInfo.Length} bytes downloaded. IsWarmupRequest = {isWarmupRequest}");
431442
}
432443

433444
private async Task UnpackPackage(string filePath, string scriptPath, RunFromPackageContext pkgContext)

src/WebJobs.Script.WebHost/Metrics/LinuxContainerMetricsPublisher.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class LinuxContainerMetricsPublisher : IMetricsPublisher
3333

3434
private readonly TimeSpan _memorySnapshotInterval = TimeSpan.FromMilliseconds(1000);
3535
private readonly TimeSpan _metricPublishInterval = TimeSpan.FromMilliseconds(30 * 1000);
36+
private readonly TimeSpan _timerStartDelay = TimeSpan.FromSeconds(2);
3637
private readonly IOptionsMonitor<StandbyOptions> _standbyOptions;
3738
private readonly IDisposable _standbyOptionsOnChangeSubscription;
3839
private readonly string _requestUri;
@@ -251,8 +252,8 @@ public void Initialize()
251252
public void Start()
252253
{
253254
Initialize();
254-
_processMonitorTimer = new Timer(OnProcessMonitorTimer, null, TimeSpan.Zero, _memorySnapshotInterval);
255-
_metricsPublisherTimer = new Timer(OnFunctionMetricsPublishTimer, null, TimeSpan.Zero, _metricPublishInterval);
255+
_processMonitorTimer = new Timer(OnProcessMonitorTimer, null, _timerStartDelay, _memorySnapshotInterval);
256+
_metricsPublisherTimer = new Timer(OnFunctionMetricsPublishTimer, null, _timerStartDelay, _metricPublishInterval);
256257

257258
_logger.LogInformation(string.Format("Starting metrics publisher for container : {0}. Publishing endpoint is {1}", _containerName, _requestUri));
258259
}
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
using System;
54
using Newtonsoft.Json;
65

76
namespace Microsoft.Azure.WebJobs.Script.WebHost.Models
@@ -10,8 +9,5 @@ public class EncryptedHostAssignmentContext
109
{
1110
[JsonProperty("encryptedContext")]
1211
public string EncryptedContext { get; set; }
13-
14-
[JsonProperty("isWarmup")]
15-
public bool IsWarmup { get; set; }
1612
}
1713
}

src/WebJobs.Script.WebHost/Models/HostAssignmentContext.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Linq;
77
using Microsoft.Extensions.Logging;
88
using Newtonsoft.Json;
9-
using Newtonsoft.Json.Linq;
109

1110
namespace Microsoft.Azure.WebJobs.Script.WebHost.Models
1211
{
@@ -36,6 +35,13 @@ public class HostAssignmentContext
3635
[JsonProperty("Secrets")]
3736
public FunctionAppSecrets Secrets { get; set; }
3837

38+
// This will be true for dummy specialization calls to pre-jit specialization code.
39+
// For warmup requests the Run-From-Pkg appsetting will point to a local endpoint that
40+
// returns 200 to ensure downloading app contents succeeds.
41+
// All the other fields will be empty.
42+
[JsonProperty("isWarmupRequest")]
43+
public bool IsWarmupRequest { get; set; }
44+
3945
public long? PackageContentLength { get; set; }
4046

4147
public string AzureFilesConnectionString
@@ -55,29 +61,29 @@ public RunFromPackageContext GetRunFromPkgContext()
5561
{
5662
return new RunFromPackageContext(EnvironmentSettingNames.AzureWebsiteRunFromPackage,
5763
Environment[EnvironmentSettingNames.AzureWebsiteRunFromPackage],
58-
PackageContentLength);
64+
PackageContentLength, IsWarmupRequest);
5965
}
6066
else if (Environment.ContainsKey(EnvironmentSettingNames.AzureWebsiteAltZipDeployment))
6167
{
6268
return new RunFromPackageContext(EnvironmentSettingNames.AzureWebsiteAltZipDeployment,
6369
Environment[EnvironmentSettingNames.AzureWebsiteAltZipDeployment],
64-
PackageContentLength);
70+
PackageContentLength, IsWarmupRequest);
6571
}
6672
else if (Environment.ContainsKey(EnvironmentSettingNames.AzureWebsiteZipDeployment))
6773
{
6874
return new RunFromPackageContext(EnvironmentSettingNames.AzureWebsiteZipDeployment,
6975
Environment[EnvironmentSettingNames.AzureWebsiteZipDeployment],
70-
PackageContentLength);
76+
PackageContentLength, IsWarmupRequest);
7177
}
7278
else if (Environment.ContainsKey(EnvironmentSettingNames.ScmRunFromPackage))
7379
{
7480
return new RunFromPackageContext(EnvironmentSettingNames.ScmRunFromPackage,
7581
Environment[EnvironmentSettingNames.ScmRunFromPackage],
76-
PackageContentLength);
82+
PackageContentLength, IsWarmupRequest);
7783
}
7884
else
7985
{
80-
return new RunFromPackageContext(string.Empty, string.Empty, PackageContentLength);
86+
return new RunFromPackageContext(string.Empty, string.Empty, PackageContentLength, IsWarmupRequest);
8187
}
8288
}
8389

src/WebJobs.Script.WebHost/Models/RunFromPackageContext.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ namespace Microsoft.Azure.WebJobs.Script.WebHost.Models
1010
{
1111
public class RunFromPackageContext
1212
{
13-
public RunFromPackageContext(string envVarName, string url, long? packageContentLength)
13+
public RunFromPackageContext(string envVarName, string url, long? packageContentLength, bool isWarmupRequest)
1414
{
1515
EnvironmentVariableName = envVarName;
1616
Url = url;
1717
PackageContentLength = packageContentLength;
18+
IsWarmUpRequest = isWarmupRequest;
1819
}
1920

2021
public string EnvironmentVariableName { get; set; }
@@ -23,6 +24,8 @@ public RunFromPackageContext(string envVarName, string url, long? packageContent
2324

2425
public long? PackageContentLength { get; set; }
2526

27+
public bool IsWarmUpRequest { get; }
28+
2629
public bool IsScmRunFromPackage()
2730
{
2831
return string.Equals(EnvironmentVariableName, EnvironmentSettingNames.ScmRunFromPackage,

0 commit comments

Comments
 (0)