我们谈在性能调优时可能存在有很多不同的方面可以进行性能的优化,比如:良好的的编码习惯,最大限度的发掘服务器性能,减少下载流量等。但我们今天说的异步和分流是在一个更大粒度下进行性能优化,当然异步服务框架不仅仅是用来调优性能的、凡是需要异步、离线、延时操作、处理高成本操作的场景都可以考虑使用异步服务框架。
这个异步服务框架会用到一些核心的技术:WCF 、MSMQ、IOC等,简单介绍一下,WCF是用来在不同应用程序间进行通信,WCF是一个很好的SOA应用的技术;MSMQ是一个队列服务,它提供一套完整的异步离线解决方案,这里我们没有直接使用MSMQ,而是通过WCF的封装来应用,WCF的NetMsmqBinding很好的对MSMQ进行了包装应用。IOC容器,用来支持不同应用插件的开发和调用。
我们先来预览一下异步服务框架的几个特点,在后续的讲解中会有更加深刻的体会:
1、优化性能:分流高并发、高耗时、实时性不强的操作
2、易于扩展:通过部署更多的服务,以支持更多消息流
3、分组管理:合理分配消息流
4、插件模式:为不同的应用开发独立插件
一、接下来看一下基础系统框架图,框架中还会有很多不同的服务,但是为了为了表述更为清晰,这里就不一一列出。
#图中可以看到所用到的必要技术:WCF、MSMQ、IOC、分组管理
#简单的流程:客户端-->分组管理-->调用信息(选择节点)+内部标识参数—>WCF(MSMQ)-->接收服务-->选择插件应用-->执行业务逻辑
#分组管理:客户端—>SoureName-->分组管理-->分组下的服务节点列表-->选择对应的服务节点
我们说服务可部署成很多不同的服务节点,所以客户端在调用服务之前必须要知道自已应该调用的服务节点和通信协议,所以客户端会根据为自已的调用所分配置好的SourceName来从分组管理中获取到自已对应的服务组,并从服务组的节点列表中跟据优先级的不同选择性能负载较好的节点和协议以备调用。invoker_groups.xml
#MSMQ:客户端-->要处理的消息及调用标识-->客户端队列—>WCF(MSMQ)-->服务端队列-->处理……
客户端把消息插入客户端队列后,调用就完成了,这样的操作耗时非常短,剩下的其它操作就由WCF(MSMQ)和后端的服务来完成,所以响应时间非常之快,如果是分流一个耗时的高成本操作,通过这样的方式可以有效提高响应性能,避免线程阻塞。
如果由于网络故障不能与服务器连接,客户端队列也不会掉失,直到网络故障排除后可以自动恢复传输。
如果服务器端的服务升级停止了服务,服务端的队列也不会掉失,直到服务正常启动后自动恢复队列的处理。
服务端队列出队的操作在WCF的处理下可以控制并发数量,通过控制并发量可以有效控制单服务节点的外理能力。
#插件模式、动态调用:Source-->ServiceName,Version-->ICallService接口-->实际处理器-->处理……
Source对象中有两个重要的参数ServiceName和Version,用来决定调用哪个ICallService接口的实现
二、客户端用例子:通过简单的配置便可以调用异步服务框架中的服务。
1、引用组件:DMedia.Fetion.Framework.ServiceModel.dll, DMedia.Fetion.InvokeService.Contract.dll
2、添加配置:
<!-- localhost,online,functest –>
<add key="IServiceInvoker.RuntimeState" value="localhost" />
<add key="IServiceInvoker.ConfigurationFilePath" value="Configuration|*" />
其中:
RuntimeState 表示当前运行在哪种状态之下,localhost表示本机调用,online表示线上生产环境,functest 表示功能测试环境
ConfigurationFilePath 表示分组配置文件的存放路径,Configuration|* 表示优先选择应用程序根目录下的Configuration目录,如果目录下存在则就放在根目录下,如果想再自义存放的路径App_Data, 则可以写成App_Data|Configuration|*
3、调用例子:
#方法一: ServiceInvokerManager.Invoke(“sourceName”, “identity”);
#方法二:ServiceInvokerManager.CreateIServiceInvoker(“sourceName”).Invoke(
Source.FromConfiguration(“sourceName”).SetClientIP("127.0.0.1"),
“identity”);
#方法三:ServiceInvokerManager.CreateIServiceInvoker(“sourceName”)
.Invoke(() => Source.FromConfiguration(“sourceName”).SetClientIP("127.0.0.1"), “identity”);
#标准方法:
IServiceInvoker iServiceInvoker = ServiceInvokerManager.CreateIServiceInvoker(“sourceName”);
if (iServiceInvoker != null)
{
Source source = Source.FromConfiguration(“sourceName”).SetClientIP("127.0.0.1");
if (source != null)
{
iServiceInvoker.Invoke(source, "abc");
}
}
三、插件开发
1、实现ICallService接口
2、添加到容器ICallService.config
3、更新分组配置(invoker_groups.xml),给客户端调用分配服务结点
四、更多服务
1、ILogService内部日志
2、容错服务(重试队列和死信记录)
3、异步错误日志记录
4、异步性能计数
5、回调服务功能(外部接口封装)
6、……
首先,我们创建一个通用的bat让它来对某个文件进行获取、签出、复制、签出操作。
postbuild.bat
REM 在生成后事件中写D:\projects\_CommonLibrary\postbuild.bat $(TargetDir) D:\projects\_CommonLibrary\XXX $(TargetFileName)
if defined TFPATH goto runtf
if defined ProgramFiles set TFPATH=%ProgramFiles%\Microsoft Visual Studio 9.0\Common7\IDE
if defined ProgramFiles(x86) set TFPATH=%ProgramFiles(x86)%\Microsoft Visual Studio 9.0\Common7\IDE
:runtf
@echo '更新开始'
"%TFPATH%\TF.exe" get %2\%3
"%TFPATH%\TF.exe" checkout %2\%3
copy %1\%3 %2\%3
"%TFPATH%\TF.exe" checkin %2\%3 /override:reason.txt /noprompt
set TFPATH=
@echo '完成'
这个bat文件可以用在项目的生成后事件中,这样每次编译就可以自动把生成的dll签入到tfs中;同样也可以写一个外部的bat文件使用这样的功能。
例如还有一个copy.bat任务:
call D:\postbuild.bat D:\xxx\bin\Release D:\yyy\abc.dll
call D:\postbuild.bat D:\xxx\bin\Release D:\yyy\abc.pdb
CollabNet Subversion 1.56 现在集成了Apache2.2.8,安装比以前方便不少。
1、安装CollabNet Subversion
2、打开C:\Program Files\CollabNet Subversion Server\httpd\conf\httpd.conf 文件
#找到<Location /svn>节点,修改内容为:
<Location /svn>
DAV svn
SVNParentPath d:\svn_repository
AuthType Basic
AuthName "Subversion Repository"
AuthUserFile "d:\svn_repository\passwords.auth"
AuthzSVNAccessFile "d:\svn_repository\access.auth"
Require valid-user
</Location>
#在117行,添加这行内容:
LoadModule authz_svn_module modules/mod_authz_svn.so
3、在d:\svn_repository目录下创建三个文夹。
d:\svn_repository\svn1
d:\svn_repository\svn2
d:\svn_repository\svn3
#执行 svnadmin create d:\svn_repository\svn1
#执行 svnadmin create d:\svn_repository\svn2
#执行 svnadmin create d:\svn_repository\svn3
4、在d:\svn_repository目录下创建两个文件。
passwords.auth
access.auth
#passwords.auth使用如果命令添加加密的用户密码:
C:\Program Files\CollabNet Subversion Server\httpd\bin>htpasswd -c D:\svn_reposi
tory\passwords.auth user1
C:\Program Files\CollabNet Subversion Server\httpd\bin>htpasswd D:\svn_reposi
tory\passwords.auth user2
#access.auth,添加如下验证内容
[groups]
dev1 = user1,user2 #开发用户组1
[/]
@dev1 = rw #设定dev1组对根目录有读和写权限
[svn1:/]
@dev1 = rw #设定dev1组对根目录有读和写权限
[svn2:/]
@dev1 = rw #设定dev1组对根目录有读和写权限
[svn3:/]
@dev1 = rw #设定dev1组对根目录有读和写权限
5、客户端访问如下地址:
http://svn.xxx.com:8090/svn/svn1
http://svn.xxx.com:8090/svn/svn2
http://svn.xxx.com:8090/svn/svn3
svn://svn.xxx.com:3690/svn1
svn://svn.xxx.com:3690/svn2
svn://svn.xxx.com:3690/svn3
通过实现IFormatProvider, ICustomFormatter接口可以实现自定义的格式输现,这里有实现一个例子,以输出日期格式为例(显示今天、明天、后天和"x月x日"等)
1、假设我们有多种显示日期格式的需求,我们可以定义一个枚举如下:
{
WithDayName,//含"前天、昨天、今天、明天、后天的显示"
WithWeek//含星期几的显示
}
2、每种类型实现 IFormatProvider, ICustomFormatter接口。
{
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
{
return this;
}
else
{
return Thread.CurrentThread.CurrentCulture.GetFormat(formatType);
}
}
public string Format(string format, object arg, IFormatProvider formatProvider)
{
string str;
IFormattable formatable = arg as IFormattable;
if (formatable == null)
{ str = arg.ToString();}
else
{
str = formatable.ToString(format, formatProvider);
}
if (arg.GetType() == typeof(DateTime))
{
DateTime dt = (DateTime)arg;
int days = (DateTime.Now - dt).Days;
switch(days)
{
case -2:
str="前天";
break;
case -1:
str = "明天";
break;
case 0:
str = "今天";
break;
case 1:
str = "明天";
break;
case 2:
str = "后天";
break;
default:
str = string.Format("{0}月{1}日", dt.Month, dt.Day);
break;
}
}
return str;
}
}
{
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
{
return this;
}
else
{
return Thread.CurrentThread.CurrentCulture.GetFormat(formatType);
}
}
public string Format(string format, object arg, IFormatProvider formatProvider)
{
string str;
IFormattable formatable = arg as IFormattable;
if (formatable == null)
{ str = arg.ToString(); }
else
{
str = formatable.ToString(format, formatProvider);
}
if (arg.GetType() == typeof(DateTime))
{
DateTime dt = (DateTime)arg;
string dayOfWeef = dt.DayOfWeek.ToString().Trim();
switch (dayOfWeef)
{
case "Monday":
dayOfWeef = "一";
break;
case "Tuesday":
dayOfWeef = "二";
break;
case "Wednesday":
dayOfWeef = "三";
break;
case "Thursday":
dayOfWeef = "四";
break;
case "Friday":
dayOfWeef = "五";
break;
case "Saturday":
dayOfWeef = "六";
break;
case "Sunday":
dayOfWeef = "日";
break;
}
return string.Format("{0}年{1}月{2}日 星期{3}", dt.Year, dt.Month, dt.Day, dayOfWeef);
}
return str;
}
}
3、使用扩展方法对使用进行封装
{
public static string ToCustomFormat(this DateTime dt, DateTimeFormat format)
{
string str;
switch (format)
{
case DateTimeFormat.WithDayName:
str = string.Format(new WithDayNameFormat(), "{0}", dt);
break;
case DateTimeFormat.WithWeek:
str = string.Format(new WithWeekFormat(), "{0}", dt);
break;
default:
throw new NotSupportedException("不支持的日期格式类型DateTimeFormat");
break;
}
return str;
}
}
4、客户端使用:
{
Console.WriteLine(DateTime.Now.AddDays(-3).ToCustomFormat(DateTimeFormat.WithDayName));
Console.WriteLine(DateTime.Now.AddDays(-2).ToCustomFormat(DateTimeFormat.WithDayName));
Console.WriteLine(DateTime.Now.AddDays(-1).ToCustomFormat(DateTimeFormat.WithDayName));
Console.WriteLine(DateTime.Now.AddDays(0).ToCustomFormat(DateTimeFormat.WithDayName));
Console.WriteLine(DateTime.Now.AddDays(1).ToCustomFormat(DateTimeFormat.WithDayName));
Console.WriteLine(DateTime.Now.AddDays(2).ToCustomFormat(DateTimeFormat.WithDayName));
Console.WriteLine(DateTime.Now.AddDays(3).ToCustomFormat(DateTimeFormat.WithDayName));
Console.WriteLine(DateTime.Now.AddDays(-3).ToCustomFormat(DateTimeFormat.WithWeek));
Console.WriteLine(DateTime.Now.AddDays(-2).ToCustomFormat(DateTimeFormat.WithWeek));
Console.WriteLine(DateTime.Now.AddDays(-1).ToCustomFormat(DateTimeFormat.WithWeek));
Console.WriteLine(DateTime.Now.AddDays(0).ToCustomFormat(DateTimeFormat.WithWeek));
Console.WriteLine(DateTime.Now.AddDays(1).ToCustomFormat(DateTimeFormat.WithWeek));
Console.WriteLine(DateTime.Now.AddDays(2).ToCustomFormat(DateTimeFormat.WithWeek));
Console.WriteLine(DateTime.Now.AddDays(3).ToCustomFormat(DateTimeFormat.WithWeek));
Console.ReadKey();
}
通常情况下我们关闭一个WCF链接都是简单地写把ICommunicationObject.Close()方法,但是这个方法有个问题就是当调用发生异常时,Close()会发生次生的异常,导致链接不能正常关闭。如果当这种异常很多时,必然对系统的稳定性有很大的影响,所以我们必须要考虑异常发生后如何关闭链接的问题。
我们可以写一个扩展来专门关闭WCF链接,而不是使用原来的Close
{
if (myServiceClient.State != CommunicationState.Opened)
{
return;
}
try
{
myServiceClient.Close();
}
catch (CommunicationException ex)
{
Debug.Print(ex.ToString());
myServiceClient.Abort();
}
catch (TimeoutException ex)
{
Debug.Print(ex.ToString());
myServiceClient.Abort();
}
catch (Exception ex)
{
Debug.Print(ex.ToString());
myServiceClient.Abort();
throw;
}
}
然后可以使用这个扩展:
{
if (client != null)
{
IChannel iChannel = client as IChannel;
if (iChannel != null)
iChannel.CloseConnection();
else
{
IDisposable iDisposable = client as IDisposable;
if (iDisposable != null) iDisposable.Dispose();
}
}
}
通过往WCF消息头中添加自定义信息,可以用于各种用途,比如可以用于传递AuthKey来判断调用是否合法。
客户端:
{
MessageHeader<string> mh = new MessageHeader<string>("abcde");
MessageHeader header = mh.GetUntypedHeader("AuthKey", http://www.cjb.com/);
OperationContext.Current.OutgoingMessageHeaders.Add(header);
return func();
}
if (OperationContext.Current != null)
{
authKey = OperationContext.Current.IncomingMessageHeaders.GetHeader<string>("AuthKey", http://www.cjb.com);
}

