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,创建一个托管服务。

按照之前规划,我们添加两个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。

本地项目源文件下载