Microsoft Azure 托管服务实践 (CalculateCloud)
Azure是微软推出的云服务品牌之一,主要提供IAAS和PAAS服务。在Azure平台建立初期,微软认为PAAS既拥有IAAS的计算功能,同时还比IAAS更容易管理和规模扩展。在这个理念的推动下,微软在Azure平台只推出了一个PAAS层的计算产品:托管服务(Hosted Service)。
托管服务将业务系统中的模块抽象成“角色(Role)”逻辑单元,开发者只需编写每个“角色”的代码,并声明每个角色的运行环境(网络端口,存储)即可。Azure托管服务控制器(FC,或Fabric Controller)会负责分配虚拟机,配置环境和执行角色代码。
逻辑组件和托管服务组件的对应关系
相比于传统系统,托管服务的部署极为简练,并且,服务器的维护交给FC来自动完成。唯一不足之处是:托管服务的开发模型不同于传统Windows系统开发,传统已有程序不能直接运行在托管服务上,而用户要经过一定的学习才能掌握托管服务开发。
传统系统 |
托管服务 |
传统开发模型 需要购买/租借服务器 手动配置服务器环境 手动部署程序到服务器 定期维护服务器,打补丁 服务器故障时,手动做故障迁移 |
新开发模型,需要额外学习 通过Azure管理界面随时申请服务器资源。 用多少,付多少。 用户一键部署。 服务器维护,硬件故障迁移都有FC自动完成。 |
这里通过简单的项目示例,操练一下如何使用Azure托管服务。演示项目的源代码请在GitHub下载
https://github.com/mogliang/CalculateCloud
功能需求:
简单的乘法运算处理。用户通过网页递交和查看运算任务。
架构规划:
搭建一个两层结构的系统。前端Web层接受用户的计算请求,做初步处理,然后递交给后端业务逻辑层受理。后端业务逻辑层受理完毕后,由前端层返回结果给用户。
前端层由WebRole实现,后端业务层由WorkerRole担任。WebRole使用Azure消息队列(Queue)向WorkerRole传递任务。WorkerRole将运算结果保存在Azure表(Table)中,由WebRole读取并显示给客户。
托管服务的结构图
这里我们没有使用角色间TCP通信(或HTTP),是因为每个角色都部署在多台虚拟机上,若某台虚拟机故障或拓扑变化,将造成部分通信失败。当然,通过重新连接可以解决问题,只不过队列编程更加简便。
开发准备:
- Visual Studio 2012/2013
- 安装 Microsoft Azure Tool for Visual Studio
https://azure.microsoft.com/en-us/downloads/
实现步骤:
启动Visual Studio,创建一个托管服务。
按照之前规划,我们添加两个Role,一个Asp.net WebRole,取名Calculate.Web,另一个WorkerRole,取名Calculate.Worker。
首先编辑WebRole,在Cloud项目中双击Calculate.Web,打开Role属性面板。点击“Settings”页,添加一个连接字符串。Name为DataConnection,Value为“UseDevelopmentStorage=true”,表示使用云存储模拟器。
这里使用的Setting叫做“托管服务设置字段“,其功能类似于.net 配置文件中的<AppSettings>,这些设置字段可以在服务运行过程中动态更改。
接下来编辑Calculate.Web项目。WebRole项目和普通的ASP.net项目几乎完全相同,只不过WebRole运行在一组虚拟机上,由负载均衡器来把用户请求均分给每台虚拟机。因此,在编程过程中要考虑到分布式环境限制,比如Session存储。WebRole中可以通过RoleEnvironment类来访问托管服务、角色环境等信息。
创建用户提交计算任务的页面。打开Default.aspx,按如下格式添加两个TextBox和一个Button。
在Code-behind文件中引用如下名空间:
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Queue;
然后添加Button Click事件处理函数,把用户输入的公式包裹在消息里,放入Azure Queue:
// Submit calculation job.
protected void Button1_Click(object sender, EventArgs e)
{
// read Azure storage connection string from Cloud settings
var connstr = RoleEnvironment.GetConfigurationSettingValue("DataConnection");
// create Azure Queue Client
var storageAccount = CloudStorageAccount.Parse(connstr);
var queueClient = storageAccount.CreateCloudQueueClient();
// get queue named "caljobqueue", create if it doesn't exist
var queue = queueClient.GetQueueReference("caljobqueue");
queue.CreateIfNotExists();
// warp user's input into queue message, add to queue.
string msgstr = string.Format("{0},{1}",
TextBox1.Text,
TextBox2.Text);
queue.AddMessage(
new CloudQueueMessage(msgstr));
// all done. Write application log
System.Diagnostics.Trace.TraceInformation("Message added. " + msgstr);
}
接下来,编辑Calculate.Worker。同样先为Calculate.Worker添加Azure Storage连接字符串
接着编辑Calculate.Worker项目代码,让Worker接收Azure Queue消息并处理。
WorkerRole的入口类为WorkerRole,继承自RoleEntryPoint,他有三个重要函数
- OnStart() 角色实例初始化时被调用,用于执行开发人员添加到一些初始化任务,调用期间,角色实例在云端的状态报告为忙碌(busy)。
- Run() 主要的业务逻辑,开发人员在此处定义应用程序将要完成的任务,所有的代码由一个无限循环控制,正常情况下不会退出该方法。
- OnStop() 角色实例退出时执行该方法,开发者可以在此处添加代码执行后期数据处理和实例监控等。
我们不需要做初始化和析构,因此只需要重写Run()函数:
public class WorkerRole : RoleEntryPoint
{
public override void Run()
{
// read Azure storage connection string from Cloud settings
var connstr = RoleEnvironment.GetConfigurationSettingValue("DataConnection");
// create Azure Queue Client
var storageAccount = CloudStorageAccount.Parse(connstr);
var queueClient = storageAccount.CreateCloudQueueClient();
// get queue named "caljobqueue", create if it doesn't exist
var queue = queueClient.GetQueueReference("caljobqueue");
queue.CreateIfNotExists();
while (true)
{
// get message from queue, if queue is not empty,
// it return one message, otherwise, return null
var msg = queue.GetMessage();
if (msg != null)
{
// handle job here.
var nums = msg.AsString.Split(',');
double answer = double.Parse(nums[0]) * double.Parse(nums[1]);
string result = string.Format("Job handled. {0}*{1}={2}", nums[0], nums[1], answer);
queue.DeleteMessage(msg);
// add applciation log
Trace.TraceInformation(result);
}
Thread.Sleep(10000);
}
}
}
到此为止,项目实现了用户输入,和后端处理的逻辑。接下来编写结果反馈的代码。
在WorkerRole项目添加一个实体类,继承自Microsoft.WindowsAzure.Storage.Table.TableEntity
using Microsoft.WindowsAzure.Storage.Table;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Calculate.Worker
{
public class CalResultEntry:TableEntity
{
public string Result { set; get; }
}
}
然后在WorkerRole.cs中添加插入Table的函数
// initialize Table on Role start
CloudTable _resultTable = null;
public override bool OnStart()
{
// read Azure storage connection string from Cloud settings
var connstr = RoleEnvironment.GetConfigurationSettingValue("DataConnection");
// create Azure Table Client
var storageAccount = CloudStorageAccount.Parse(connstr);
var tableClient = storageAccount.CreateCloudTableClient();
// get table named "calresulttable", create if it doesn't exist
_resultTable = tableClient.GetTableReference("calresulttable");
_resultTable.CreateIfNotExists();
return base.OnStart();
}
void AddResultEntry(string result)
{
// PartitionKey+RowKey is table's primary index, must have
var newentry = new CalResultEntry
{
PartitionKey = DateTime.UtcNow.ToString("yyyyMMdd"),
RowKey = DateTime.UtcNow.Ticks.ToString(),
Result = result
};
var addOp = TableOperation.Insert(newentry);
_resultTable.Execute(addOp);
}
然后修改Run(),让计算结果输出到Table上
var nums = msg.AsString.Split(',');
double answer = double.Parse(nums[0]) * double.Parse(nums[1]);
string result = string.Format("Job handled. {0}*{1}={2}", nums[0], nums[1], answer);
AddResultEntry(result);
......
最后,为WebRole添加读取Table的代码。
在WebRole项目,添加一个已有文件,引用WorkerRole的“CalResultEntry.cs”
打开Default.aspx,添加一个Label用来显示结果,再添加一个按钮。
添加按钮点击的事件处理函数,读取当日的计算结果,显示出来
protected void Button2_Click(object sender, EventArgs e)
{
var connstr = RoleEnvironment.GetConfigurationSettingValue("DataConnection");
var storageAccount = CloudStorageAccount.Parse(connstr);
var tableClient = storageAccount.CreateCloudTableClient();
var table = tableClient.GetTableReference("calresulttable");
table.CreateIfNotExists();
// get today's result form table
var results = from en in table.CreateQuery<Calculate.Worker.CalResultEntry>()
where en.PartitionKey == DateTime.UtcNow.ToString("yyyyMMdd")
select en;
// display result on page
string resStr = "";
foreach (var en in results)
{
resStr += en.Result + "<br>";
}
Label1.Text = resStr;
}
在此博客最后可以下载完整的项目代码。
测试结果:
Azure Tool提供了托管服务模拟器,开发者可以在本机上运行和调试托管服务。
按F5启动调试,点击桌面右下角模拟器图标,显示模拟器UI,每个控制台表示一个正在运行的虚拟机,绿灯表示程序在虚拟机上正常运行。
在弹出的浏览器窗口中,测试一些基本乘法,工作正常。
大家应该发现,当输入非数字时,WorkerRole程序就会Crash,这是因为缺少输入数据格式检查。为了保持实例代码的简洁,项目缺少很多异常处理逻辑,读者需要注意并自行处理。
部署到云端
在真正部署到云端前,开发者需要拥有一个Azure订阅。有国外信用卡和手机号码的朋友可以在https://azure.microsoft.com/en-us/ 申请试用。国内的朋友能够从21世纪互联的https://www.windowsazure.cn/ 网站申请试用。
部署的方法有很多,我这里演示从Azure管理网站部署。最近,我和同事出版了一本Azure相关书籍《Microsoft Azure开发与应用》,其中对部署有更多的讨论,感兴趣的朋友可以买来看看。
首先注意,云端不能访问Azure Storage模拟器,因此必须修改WebRole和WorkerRole的连接字符串“DataConnection”,使用一个真实的Azure Storage。
没有Azure Storage的话,通过Azure管理网站创建一个。
打包托管服务项目。右键点击云项目,选择“Package”即可。
打包后生成两个文件,cspkg包含了项目编译后的文件和托管服务的环境定义(ServiceDefintion.csdef)。Cscfg为托管服务的配置。配置文件中的内容可以在托管服务运行时动态调整。
下一步,登录Azure 管理网站。点击“+NEW”-->”CLOUD SERVICE”-->”QUICK CREATE”,给个名字,选择“East Asia”数据中心(香港)。
创建成功后,点击进入托管服务项的详细页,再点击下方“UPLOAD”按钮上传托管服务包,勾选“Deploy even if one or more roles contains a single instance”。
部署进度可以通过下方状态栏观察,部署成功后,服务仍然需要时间来创建和初始化虚拟机,最终当虚拟机进入”Ready“阶段时,服务正式运行。
访问一下看看吧。服务的默认域名为<servicename>.cloudapp.net。
本地项目源文件下载