Azure云服务中的进程通信和数据预加载

https://blogs.msdn.com/b/jianwu/archive/2014/09/11/azure-paas.aspx(Azure PaaS - Cloud Service服务架构及快速调试)中,大家了解到了云服务(Cloud Service)在实际运行过程中的具体分布:多个进程依次被启动。其中,最为重要的是进程WaIISHost(WaWorkerHost)和w3wp进程,前者运行着WebRole.cs(WorkerRole.cs)中代码逻辑及维持着Microsoft.WindowsAzure.ServiceRuntime环境,后者运行着网站(服务)项目的业务逻辑。如图所示:

由于在开发时,WebRole.cs(WorkerRole.cs)和网站项目所属于同一个项目且定义在同一个命名空间(NameSpace)下面,在一些开发项目中,常常会遇到一个问题:

1. 如何让WebRole.cs(WorkerRole.cs)和网站项目快速的共享信息(即部署到云中后,WaIISHost(WaWorkerHost)和w3wp进程高效通信)

2. 如何使得网站项目w3wp能在刚启动时就先加载一些依赖数据(如供查询用的词典库)

本篇就这两个问题,结合手头上跟进的一两个项目,对常用的方法加以总结。主要覆盖以下几个方面:

1. 使用WCF服务来交换信息(进程通信)

2. 使用公有云缓存来共享信息(数据预加载)

3. 云中常用的数据交互方式

 

(一)使用WCF服务来交换信息

一个常见的场景是:WebRole.cs中的代码逻辑需要读取网站配置中的配置项(如变量字符串,数据库连接等),且网站配置中的内容有可能是来自于web.config,也有可能来自Web Config Transformation以后的结果。

关于Web Config Transformation,这里简单描述一下:

添加配置项到web.config时,既可以直接添加配置节 如<appSettings> <add key="setting1" value="hello, world."/> </appSettings>到web.config中,也可以通过项目属性中的setting来添加,如图:

添加完成以后,可以发现,两种不同方式添加进来的配置出现在不同的配置节里。<applicationSettings>中指明了配置变量的类型和使用空间范围,较<appSettings>更适用于较大的项目。

对于<appSettings>下的配置,可以采用System.Configuration.ConfigurationManager.AppSettings["setting1"].ToString();方法来读取。

对于<applicationSettings>下的配置,可以采用Properties.Settings.Default["Setting1"].ToString();方法来读取。

 

实际项目过程中,本地调试和线上运行时所依赖的配置项很可能不相同,仔细查看VS项目会发现,web.config下面有子文件,如web.debug.config,开发者可以在web.debug.config里面添加以下内容,其意义在于替换web.config中变量Setting1的值。

于是,在选择项目生成时,若使用web.debug.config,如下图,生成的web role部署包中的web.config既是web.debug.config覆盖原有web.config的结果。这既是web config transformation技术,在传统.Net网站开发中经常使用。

回到主题:如何使用WCF服务来使得webrole.cs和网站进程w3wp交换信息呢?如下实现方式:

1. 添加一个WCF服务到网站项目中

2. 在新建的服务中添加一个新的方法和具体实现

        public string GetWebConfigSetting(string key)
        {
            string retVal = null;
            try
            {
                retVal = Properties.Settings.Default[key].ToString();
            }
            catch (Exception ex)
            {
            }
            return retVal;
        }

3. 在WebRole.cs中添加以下代码来读取web.config中的内容:

        public override void Run()
        {
            System.Diagnostics.Trace.WriteLine("Run function is called");

 

            //get content by consuming the service in target web process, here it goes
            try
            {
                ChannelFactory<IService1> factory = new ChannelFactory<IService1>(
                    new BasicHttpBinding(),
                    new EndpointAddress(new Uri("https://test1205.cloudapp.net/Service1.svc"))); //make sure test1205.cloudapp.net should be replaced by your cloud service DNS in your solution.
                IService1 channel = factory.CreateChannel();

                for (int i = 0; i < 10; i++)
                {
                    str = channel.GetWebConfigSetting("Setting1");
                    System.IO.File.AppendAllText("log.txt", "\r\n" + DateTime.Now.ToString() + "   got value --> " + str);
                    System.Threading.Thread.Sleep(1000);
                }
            }
            catch (Exception ex)
            {
                System.IO.File.AppendAllText("error.txt", "\r\n" + DateTime.Now.ToString() + "   got error --> " + ex.Message);
            }
            //end of reading configuration

            base.Run();
        }

 如此,WebRole.cs即WaIISHost进程就可以随时获取web进程中的信息。运行结果如下:

注意:此处的test1205.cloudapp.net需要替换成云服务所在的DNS,或者虚机的内网IP。

 

 (二)使用公有云缓存来共享信息

 https://blogs.msdn.com/b/jianwu/archive/2014/08/26/windows-azure-caching.aspx中介绍了公有云中常用的缓存技术,其中,角色辅助模式的缓存技术和托管模式的缓存技术实施方法几乎一致,因此,开发者可以使用缓存技术来实现数据预加载。如:

1. WebRole启动过程中加载字典或关键词库。

2. 网站(服务)在运行是依赖已加载的数据来处理业务逻辑。

 其中,加载数据的工作既可以通过预启动任务来完成,也可以通过WebRole.cs中的代码完成,本篇以后者为例进行实践。

选中网站和WebRole.cs所在的项目,右键管理Nugget包,搜索并安装Windows Azure Cache包。

安装完后,参考https://blogs.msdn.com/b/jianwu/archive/2014/08/26/windows-azure-caching.aspx,修改web.config中的dataCacheClients配置,使用role based cache或者managed cache,下面是使用托管式缓存。

<dataCacheClients>
<dataCacheClient name="default">
<!--To use the in-role flavor of Windows Azure Cache, set identifier to be the cache cluster role name -->
<!--To use the Windows Azure Cache Service, set identifier to be the endpoint of the cache cluster -->
<autoDiscover isEnabled="true" identifier="myzigcache.cache.windows.net" />
<!--<localCache isEnabled="true" sync="TimeoutBased" objectCount="100000" ttlValue="300" />-->
<!--Use this section to specify security settings for connecting to your cache. This section is not required if your cache is hosted on a role that is a part of your cloud service. -->
<securityProperties mode="Message" sslEnabled="true">
<messageSecurity authorizationInfo="Ykeymh[key]HA6L[key]0Lw==" />
</securityProperties>
</dataCacheClient>
</dataCacheClients>

然后在网站项目中,添加一个和webrole同名的配置文件,如示例中的 WCFServiceWebRole1.dll.config。

【重要】:之前的云服务架构博文中介绍过,WebRole.cs就会运行在WaIISHost进程中,其加载入口为用户代码所形成的库文件如WebRole1.dll(WCFServiceWebRole1.dll),因此添加的配置文件WebRole1.dll.config或WCFServiceWebRole1.dll.config能被WaIISHost进程所读取。

 具体配置文件内容为:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="dataCacheClients" type="Microsoft.ApplicationServer.Caching.DataCacheClientsSection, Microsoft.ApplicationServer.Caching.Core" allowLocation="true" allowDefinition="Everywhere" />
    <section name="cacheDiagnostics" type="Microsoft.ApplicationServer.Caching.AzureCommon.DiagnosticsConfigurationSection, Microsoft.ApplicationServer.Caching.AzureCommon" allowLocation="true" allowDefinition="Everywhere" />
  </configSections>
  <dataCacheClients>
    <dataCacheClient name="default">
      <!--To use the in-role flavor of Windows Azure Cache, set identifier to be the cache cluster role name -->
      <!--To use the Windows Azure Cache Service, set identifier to be the endpoint of the cache cluster -->
      <autoDiscover isEnabled="true" identifier="myzigcache.cache.windows.net" />
      <!--<localCache isEnabled="true" sync="TimeoutBased" objectCount="100000" ttlValue="300" />-->
      <!--Use this section to specify security settings for connecting to your cache. This section is not required if your cache is hosted on a role that is a part of your cloud service. -->
      <securityProperties mode="Message" sslEnabled="true">
        <messageSecurity authorizationInfo="YWNz【key】Nh【key】Lw==" />
      </securityProperties>
    </dataCacheClient>
  </dataCacheClients>

</configuration>

 随后,即可在WebRole.cs中添加代码,开始预加载数据,如:

        public override bool OnStart()
        {

            //preload data for further use
            DataCache cache = new DataCache("default");
            try
            {
                cache.Add("mycache1", "Preloaded info from WebRole" + DateTime.Now.ToString());
                System.Threading.Thread.Sleep(2000);
                cache.Add("mycache2", "Preloaded info from WebRole" + DateTime.Now.ToString());
                System.Threading.Thread.Sleep(2000);
                cache.Add("mycache3", "Preloaded info from WebRole" + DateTime.Now.ToString());
                System.Diagnostics.Trace.WriteLine("Preloaded cache is done");
            }
            catch (Exception eex)
            {
                System.Diagnostics.Trace.WriteLine(eex.Message);
            }

            // For information on handling configuration changes
            // see the MSDN topic at https://go.microsoft.com/fwlink/?LinkId=166357.

            return base.OnStart();
        }

 这样以后,网站服务就可以使用这些预加载的数据了。如一个按钮的响应函数:

protected void Button3_Click(object sender, EventArgs e)
        {
            DataCache cache = new DataCache("default");
            object result = cache.Get("mycache1");
            TextBox2.Text += "\r\nRead content from Cache: [" + result.ToString() + "] at " + DateTime.Now.ToString();
        }

 运行结果如下:

(三)云中常用的数据交互方式

除了上述两种方式以外,还有一些间接但很常见的方法可以用于云服务中的数据交互。之前的PaaS实践的系列博文中就用到了这一点。大体分以下几种:

1. 使用同一个SQL Azure、Azure Storage等永久存储来连接不同的云服务进程(模块)。

2. 使用队列存储来交互信息(如storage queue, service bus queue)

3. 建立socket通信等传统的通信技术。以下是一个示例实现:(参考信息:https://msdn.microsoft.com/en-us/library/6y0e13d3(v=vs.110).aspx

服务端代码:

    class Program
    {
        static void Main(string[] args)
        {
            StartListening();
            Console.Read();
        }

        // Incoming data from the client.
        public static string data = null;
        public static void StartListening()
        {
            // Data buffer for incoming data.
            byte[] bytes = new Byte[1024];

            // Establish the local endpoint for the socket.
            // Dns.GetHostName returns the name of the
            // host running the application.
            IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName());
            IPAddress ipAddress = ipHostInfo.AddressList[0];
            IPEndPoint localEndPoint = new IPEndPoint(ipAddress, 1048);
 
            // Create a TCP/IP socket.
            Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // Bind the socket to the local endpoint and
            // listen for incoming connections.

            try
            {
                listener.Bind(localEndPoint);
                listener.Listen(10);

                // Start listening for connections.
                while (true)
                {
                    Console.WriteLine("Waiting for a connection... @" + localEndPoint.ToString());
                    // Program is suspended while waiting for an incoming connection.
                    Socket handler = listener.Accept();
                    data = null;
 
                    // An incoming connection needs to be processed.
                    for (int num = 0; num < 10000; num++)
                    {
                        while (true)
                        {
                            bytes = new byte[1024];
                            int bytesRec = handler.Receive(bytes);
                            data = Encoding.ASCII.GetString(bytes, 0, bytesRec);
                            if (data.IndexOf("<EOF>") > -1)
                            {
                                break;
                            }
                        }
 
                        // Show the data on the console.
                        //Console.WriteLine(num.ToString() + " Text received : {0}", data);
                        System.IO.File.AppendAllText("./socketserver.txt", "\r\n" + DateTime.Now.ToString() + " recvd --> " + data);
 
                        // Echo the data back to the client.
                        byte[] msg = Encoding.ASCII.GetBytes(data);
                        System.IO.File.AppendAllText("./socketserver.txt", "\r\n" + DateTime.Now.ToString() + " echo --> " + data);
                        handler.Send(msg);
                    }
                    handler.Shutdown(SocketShutdown.Both);
                    handler.Close();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
            Console.WriteLine("\nPress ENTER to continue...");
            Console.Read();
        }
    }

 

客户端代码:

    class Program
    {
        static void Main(string[] args)
        {
            StartClient();
            Console.Read();
        }

        public static void StartClient()
        {
            // Data buffer for incoming data.
            byte[] bytes = new byte[1024];

            // Connect to a remote device.
            try
            {
               // Establish the remote endpoint for the socket.
                // This example uses port 11000 on the local computer.
                IPHostEntry ipHostInfo = Dns.Resolve("<mycloudservice>.cloudapp.net");

                //ipHostInfo = Dns.Resolve(Dns.GetHostName());
                IPAddress ipAddress = ipHostInfo.AddressList[0];
                IPEndPoint remoteEP = new IPEndPoint(ipAddress, 1048);
 
                // Create a TCP/IP  socket.
                Socket sender = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                int interval = 0;

                // Connect the socket to the remote endpoint. Catch any errors.
                try
                {
                    sender.Connect(remoteEP);
                    Console.WriteLine("Socket connected to {0}", sender.RemoteEndPoint.ToString());
 
                    while (true)
                    {
                        // Encode the data string into a byte array.
                        byte[] msg = Encoding.ASCII.GetBytes(DateTime.Now.ToString() + " : This is a test <EOF>");
                        // Send the data through the socket.
                        int bytesSent = sender.Send(msg);
                        System.IO.File.AppendAllText("./socket.txt", "\r\n" + DateTime.Now.ToString() + " sent --> " + Encoding.ASCII.GetString(msg, 0, msg.Length));

                        // Receive the response from the remote device.
                        int bytesRec = sender.Receive(bytes);
                        //Console.WriteLine("Echoed test = {0}", Encoding.ASCII.GetString(bytes, 0, bytesRec));
                        System.IO.File.AppendAllText("./socket.txt", "\r\n" + DateTime.Now.ToString() + " recvd --> " + Encoding.ASCII.GetString(bytes, 0, bytesRec));

                        interval += 5 * 60 * 1000; //5 minutes
                        System.Threading.Thread.Sleep(interval);
                    }

                    // Release the socket.
                    sender.Shutdown(SocketShutdown.Both);
                    sender.Close();
                }
                catch (ArgumentNullException ane)
                {
                    Console.WriteLine(DateTime.Now.ToString() + "ArgumentNullException : {0}", ane.ToString());
                }
                catch (SocketException se)
                {
                    Console.WriteLine(DateTime.Now.ToString() + "SocketException : {0}", se.ToString());
                }
                catch (Exception e)
                {
                    Console.WriteLine(DateTime.Now.ToString() + "Unexpected exception : {0}", e.ToString());
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
    }

 运行日志:

Here is log from client app:

2014/10/10 11:23:15 sent --> 2014/10/10 11:23:15 : This is a test <EOF>

2014/10/10 11:23:15 recvd --> 2014/10/10 11:23:15 : This is a test <EOF>

2014/10/10 11:28:15 sent --> 2014/10/10 11:28:15 : This is a test <EOF>

2014/10/10 11:28:15 recvd --> 2014/10/10 11:28:15 : This is a test <EOF>

 

Here is the log from server app:

2014/10/10 11:23:15 recvd --> 2014/10/10 11:23:15 : This is a test <EOF>

2014/10/10 11:23:15 echo --> 2014/10/10 11:23:15 : This is a test <EOF>

2014/10/10 11:28:15 recvd --> 2014/10/10 11:28:15 : This is a test <EOF>

2014/10/10 11:28:15 echo --> 2014/10/10 11:28:15 : This is a test <EOF>