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
    http://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订阅。有国外信用卡和手机号码的朋友可以在http://azure.microsoft.com/en-us/ 申请试用。国内的朋友能够从21世纪互联的http://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。

本地项目源文件下载

 

Comments (0)