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>