Azure PaaS -- 多服务并行托管及REST部署
在Microsoft Azure中部署多个网站项目,可以考虑在单个Web Role中加载多个网站项目,使得在减少Web Role角色数量的情况下降低Microsoft Azure的使用成本。同时,单个Web Role的多个实例仍然可以保证每个网站项目的SLA,以及进一步添加Auto Scaling功能可以保障整个托管服务Role的处理能力。因此,在同一个Web Role角色中加载多个网站项目在一些场景下是不错的选择。
1. 多服务并行托管
在同一个Web Role角色中加载多个网站项目主要有两种形式。
- „ 虚拟目录:主网站项目将部署在实例虚拟机的IIS中,对外服务使用指定端口(如80)。IIS在该主网站中创建虚拟目录,然后将额外的网站分别部署在这些虚拟目录中。部署完毕之后,主网站的对外服务网址是https://**.cloudapp.net,其他网站的对外服务地址是https://**.cloudapp.net/VirtualDirectoryName/。
- „ 多端口服务:主网站和额外的网站项目都部署在实例虚拟机的IIS中,对外服务使用不同的指定端口(如80和8080)。主网站的对外服务网址是https://**.cloudapp.net,其他网站的对外服务地址是https://**.cloudapp.net:8080。
采用虚拟目录方式部署使得所有的网站项目都在同一个服务进程中(W3WP),相互之间会有干扰,如果其中一个网站发生异常导致该进程退出后,该虚拟机上所有的其他网站都不再工作,但采用此方式的一个好处是所有的网站项目可以共享Session等状态信息。
如下图所示的实例项目,Web Role角色使用的网站项目是WebRole1,但整个解决方案中还包含两个额外的网站项目:SubWebApplication1和WebApplication2,它们都是单独的网站项目。部署到云端以后,主网站项目WebRole1对外服务的地址将是https://***.cloudapp.net。
若在该Web Role中以虚拟目录的形式加载网站项目SubWebApplication1,则需要对Web Role所在项目中的ServiceDefinition.csdef文件做以下配置(添加了VirtualApplication对应的配置部分)。
<Site name="Web">
<VirtualApplication name="TestVWeb" physicalDirectory="../SubWebApplication1"/>
<Bindings>
<Binding name="Endpoint1" endpointName="Endpoint1" />
</Bindings>
</Site>
此配置的作用是在WebRole1网站项目中创建一个虚拟目录TestVWeb,用以加载网站项目SubWebApplication1。部署到云端以后,SubWebApplication1网站对外服务的地址将是https://***.cloudapp.net/TestVWeb。
采用多端口服务方式部署将所有的网站运行在不同的服务进程(W3WP)中,相互之间独立运行,互不干扰。在上面的实例项目中,主网站WebRole1和以虚拟路径配置的网站SubWebApplication1均使用80端口对外服务,另外一个网站项目WebApplication2可以使用另外一个对外服务端口8080,同样对Web Role所在项目中的ServiceDefinition.csdef文件添加以下配置。
<Site name="WebSite2" physicalDirectory="../WebApplication2/">
<Bindings>
<Binding name="Endpoint2" endpointName="Endpoint2" />
</Bindings>
</Site>
该配置的作用是指定网站项目WebApplication2使用第二个服务端口Endpoint2(即8080),部署到云端以后,WebApplication2网站对外服务的地址将是https://***.cloudapp.net:8080。
若同时在Web Role项目中加载额外的网站项目SubWebApplication1和WebApplication2,则完整的ServiceDefinition.csdef配置如下:
<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="webrole_config_rewriting" xmlns="https://schemas. microsoft.com/ServiceHosting/2008/10/ServiceDefinition" schemaVersion="2012-10.1.8">
<WebRole name="WebRole1" vmsize="Small">
<Runtime executionContext="elevated" />
<Sites>
<Site name="Web">
<VirtualApplication name="TestVWeb" physicalDirectory="../ SubWebApplication1" />
<Bindings>
<Binding name="Endpoint1" endpointName="Endpoint1" />
</Bindings>
</Site>
<Site name="WebSite2" physicalDirectory="../WebApplication2/">
<Bindings>
<Binding name="Endpoint2" endpointName="Endpoint2" />
</Bindings>
</Site>
</Sites>
<Endpoints>
<InputEndpoint name="Endpoint1" protocol="http" port="80" />
<InputEndpoint name="Endpoint2" protocol="http" port="8080" />
</Endpoints>
<Imports>
<Import moduleName="Diagnostics" />
</Imports>
</WebRole>
</ServiceDefinition>
部署到云端的托管服务以后,登录到Web Role的虚拟机实例,在IIS管理界面中,可以看到上述三个网站的实际部署形式,如图所示。
如前面所述,三个网站对外的服务地址分别是:
https://***.cloudapp.net/TestVWeb
2. REST部署云服务
对于运维工程师或者非.NET开发者而言,如果只是使用管理门户或者Visual Studio来逐一管理多版本的托管服务或者频繁地进行上线升级操作,那么工作效率会比较低下。针对这种情况,开发者(运维工程师)可以充分考虑使用Microsoft Azure提供的基于REST的管理API来创建、部署和升级服务。
发布托管服务的REST API定义如下表所示,定义指定了发布包的Package文件需要预先放在WAS文件存储中,供该REST请求读取。
方 法 |
请求地址 |
HTTP版本 |
请求头部 |
请求内容 |
POST |
https://management.core. windows.net/<subscription-id>/services/hostedservices/ <cloudservice-name> /deploymentslots/ <deployment-slot> 其中<subscription-id>替换为用户的订阅ID <cloudservice-name>为目标 托管服务的名称 <deployment-slot>为production或者staging |
HTTP/1.1 |
Content-Type: application/xml. x-ms-version:2009-10-01 or later |
<?xml version="1.0" encoding="utf-8"?> <CreateDeployment xmlns="https://schemas.microsoft. com/windowsazure"> <Name>deployment-name</Name> <PackageUrl>package-url-in-blob-storage </PackageUrl> <Label>base64-encoded-deployment-label</Label> <Configuration>base64-encoded-configuration- file</Configuration> <StartDeployment>true</StartDeployment> </CreateDeployment> |
下面使用C# 实现REST请求,根据REST API的定义构造Web请求,然后发送到目标请求地址。(不熟悉.Net的开发人员可以使用其他的技术来构造同样的Web请求,实现同样的REST部署操作)
public static string CreateDeployment()
{
string requestUrl = "https://management.core.windows.net/<subscriptionid>/services/hostedservices/test1205/deploymentslots/staging";
string requestBody = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<CreateDeployment xmlns=\"https://schemas.microsoft.com/windowsazure\">" +
"<Name>mydeployment</Name>" +
"<PackageUrl>https://zdatatest.blob.core.windows.net/blobcontainer/TempWebRole.cspkg</PackageUrl>" +
"<Label>" + Convert.ToBase64String(Encoding.UTF8.GetBytes("mydeployment")) + "</Label>" +
"<Configuration>" + Convert.ToBase64String(Encoding.UTF8.GetBytes(File.ReadAllText(@"E:\ServiceConfiguration.Cloud.cscfg"))) + "</Configuration>" +
"<StartDeployment>true</StartDeployment>" +
"</CreateDeployment>";
string thumbprint = "2C501CB7434273E29E0A866919994B36686437DF"; //got from Azure portal | Management Certificates
//get the certificate and add it to the request
var certByteData = File.ReadAllBytes(@"E:\PublicKey ManageAzure.pfx");
X509Certificate2 cert = new X509Certificate2(certByteData, "1");
HttpWebRequest httpWebRequest = (HttpWebRequest)HttpWebRequest.Create(new Uri(requestUrl));
httpWebRequest.ClientCertificates.Add(cert);
httpWebRequest.Headers.Add("x-ms-version", "2010-10-28");
httpWebRequest.ContentType = "application/xml";
if (!string.IsNullOrEmpty(requestBody))
{
httpWebRequest.Method = "POST";
byte[] requestBytes = Encoding.UTF8.GetBytes(requestBody);
httpWebRequest.ContentLength = requestBytes.Length;
using (Stream stream = httpWebRequest.GetRequestStream())
{
stream.Write(requestBytes, 0, requestBytes.Length);
}
}
try
{
using (HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse())
{
Console.WriteLine("Response status code: " + httpWebResponse.StatusCode);
WriteRespone(httpWebResponse);
string requestID = httpWebResponse.Headers["x-ms-request-id"];
for (int i = 0; i < 100; i++)
{
GetOperStatus(requestID);
System.Threading.Thread.Sleep(10000);
}
}
}
catch (WebException ex)
{
Console.WriteLine("Response status code: " + ex.Status);
WriteRespone(ex.Response);
throw ex;
}
return "Exit from the function CreateHostedService.";
}
static void WriteRespone(WebResponse webResponse)
{
using (Stream responseStream = webResponse.GetResponseStream())
{
if (responseStream == null) return;
using (StreamReader reader = new StreamReader(responseStream))
{
Console.WriteLine("Response output:");
Console.WriteLine(reader.ReadToEnd());
}
}
}
调用CreateDeployment后,可以观察到该部署包迅速被部署到Azure云服务中。如图:
小结:
对于多个网站服务,开发者可以考虑使用虚拟目录或者多端口服务的方式来并行托管在同一个云服务的同一个Role中。
对于云服务的部署和其他操作,开发者可以给予云服务的REST API开发灵活的工作,供自动化运维管理。
PS:基于REST API的操作中,有一种操作在解决平台问题的时候非常有效,其是:删除指定的角色实例虚机。如下删除指定云服务test0613中部署包426b150adcb04c0896f3bfa91df3ab26下的虚机WebRole1_IN_0。
public static string requestID = "request failed.";
public static string DeleteRoleInstance()
{
//subscription ID, cloud service name and deployment name.
string requestUrl = "https://management.core.windows.net/<subscriptionid>/services/hostedservices/test0613/deployments/426b150adcb04c0896f3bfa91df3ab26/roleinstances/?comp=delete";
string requestBody = "<RoleInstances xmlns=\"https://schemas.microsoft.com/windowsazure\" xmlns:i=\"https://www.w3.org/2001/XMLSchema-instance\">" +
"<Name>WebRole1_IN_0</Name>" +
"</RoleInstances>";
string thumbprint = "2C501CB7434273E29E0A866919994B36686437DF"; //got from Azure portal | Management Certificates
//get the certificate and add it to the request
//var certByteData = File.ReadAllBytes(@"E:\Azure Micro\FDocs_Certs\cert files\Pragraming\pfxREST-111.pfx");
//X509Certificate2 cert = new X509Certificate2(certByteData, "111");
HttpWebRequest httpWebRequest = (HttpWebRequest)HttpWebRequest.Create(new Uri(requestUrl));
httpWebRequest.ClientCertificates.Add(cert);
httpWebRequest.Headers.Add("x-ms-version", "2013-08-01");
httpWebRequest.ContentType = "application/xml";
if (!string.IsNullOrEmpty(requestBody))
{
httpWebRequest.Method = "POST";
byte[] requestBytes = Encoding.UTF8.GetBytes(requestBody);
httpWebRequest.ContentLength = requestBytes.Length;
using (Stream stream = httpWebRequest.GetRequestStream())
{
stream.Write(requestBytes, 0, requestBytes.Length);
}
}
try
{
using (HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse())
{
Console.WriteLine("Response status code: " + httpWebResponse.StatusCode);
WriteRespone(httpWebResponse);
requestID = httpWebResponse.Headers["x-ms-request-id"];
}
}
catch (WebException ex)
{
Console.WriteLine("Response status code: " + ex.Status);
WriteRespone(ex.Response);
throw ex;
}
catch (System.Net.HttpListenerException eex)
{
Console.WriteLine(eex.Message);
}
return "Exit from the function DeleteRoleInstance.";
}
static void WriteRespone(WebResponse webResponse)
{
using (Stream responseStream = webResponse.GetResponseStream())
{
if (responseStream == null) return;
using (StreamReader reader = new StreamReader(responseStream))
{
Console.WriteLine("Response output:");
Console.WriteLine(reader.ReadToEnd());
}
}
}