前几天用WCF做项目时发现了一个效率问题,由于系统对效率要求较高,困扰了很长时间终于将问题解决了,写下来为以后的兄弟们参考,第一次写博客有不准确的地方还望同行们多喷多指点,先行谢过啦...
问题场景是这样的,我上传很多数据到服务器端,测试用的是100万条,由于服务器端需要对数据筛选并过滤,那么就将数据驻留在内存中,在处理完以后才写入到文件,需要的时候再从文件中读取出来,当然向文件写入和读取都是单独的线程来并发执行的...就是这样看似谁都不影响谁的完美策略竟然导致后续Server端接收WCF请求的延迟,并不是服务方法的内部有多复杂,而是请求就不能立即进入响应方法中...困扰了三天之后也查阅了很多网上资料终于找到了原因,原因是我数据存到Server端以后因为某些需要又用了一个线程把数据挪到另外一个内存缓存中,按理说只是多占些内存而已不应该影响接收请求的速度啊,其实想来想去除了多占了很多内存以外还多开了好几个线程,而且伴有大量的内存到文件的IO写入写出,就这样造成了线程开启的延迟,其实WCF的一次请求也是开启一个线程响应的,这是我这次总结出来的,如果有不准确的地方希望兄弟们给指正,再次感谢
顺便总结一下WCF效率优化设置的常规思路:
1、针对ServerHost端能接收的最大请求数量限制
在服务端设置ServiceHost,如:
1 host.Description.Behaviors.Add(new ServiceThrottlingBehavior() 2 { 3 MaxConcurrentInstances = 2000, 4 MaxConcurrentCalls = 2000, 5 MaxConcurrentSessions = 2000 6 });
2、针对Server端服务对象的管理及并发方式
设置服务的InstanceContextMode 及ConcurrencyMode 特性,设置方法是在实现服务契约的类上面用如下特性:
1 [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, 2 ConcurrencyMode = ConcurrencyMode.Multiple, 3 UseSynchronizationContext=false)] 4 public class MyService : IInterface{...}
说明:
(1)InstanceContextMode.PerCall表示每接收一个新的请求WCF都会新建一个服务实例来响应,这样本质不同请求的响应就是并发执行的
(2)将ConcurrencyMode设置为ConcurrencyMode.Multiple也是为了保险起见,个人认为这个多线程访问只有在InstanceContextMode.Single的时候也就是服务端针对每个请求都是一个对象响应的时候才谈得上多线程并发访问,因为InstanceContextMode.PerCall每个请求都是不同实例响应的,人家本来就是并发的
(3)如果不需要WCF服务内部帮你协调同步,那么设置UseSynchronizationContext=false,以后有多线程共享数据自己拿Lock去控制,我就是这么干的,因为我Lock的东西跟WCF没啥关系
3、针对首次访问速度很慢,以后访问速度快的问题
如果是用的类似HttpBasicBinding,访问服务端时首次很慢,可以考虑将binding的useDefaultWebProxy设置为"false",如果是用的类似NetTcpBinding,是没有这个属性的,这个属性默认是true,意思是在wcf客户端向Server端请求的时候总是尝试先查找代理,找不到再去直接连接,如果您的服务端确定的话,干脆直接连接算了,不让客户端再去找代理,设置为false得了...
4、针对WCF串行处理造成的延迟
默认WCF的代理都是自动打开的,即使您将ConcurrencyMode设置为Multiple,WCF依旧会以串行的而不是并行的方式响应,因为选择自动打开代理时,多个请求同时来到服务端,服务端会对请求进行排序等待且执行反序列化等,等这些都执行完了才会并发执行,这就造成了有先有后执行,既然是这样我们就不要WCF自动打开代理,而是我们手动的显示打开,具体方法如:((ICommunicationObject)client).Open();
(大家可以看看这个博客,我是在这里学到的:http://www.cnblogs.com/tyb1222/archive/2012/05/23/2515535.html)
5、WCF接收请求本质上也是开启一个线程来响应client请求的,那么就涉及到windows线程池,一般线程池虽然可以通过ThreadPool.SetMinThreads方法设置最小的线程数量,但是系统内部其实对空闲线程过16s就清理一次的(即使你现在的活动线程没有达到最小的线程池数量),只留一个空闲线程接收请求,如果刚刚清理完线程池而且这个空闲线程又刚好正接受请求的时候又来了一个新的请求,这时候CLR就会重新new一个线程,这时就会消耗一些时间,而且消耗时间的长短是时随机的视系统情况而定(这个问题据说.net 4.0还有),为了防止这些情况发生就需要时刻提醒Windows多留出一个空闲的线程别被回收以备随时调用,我们首先想到的就是定时提醒,但是如果每提醒一下就新建一个线程那么以后线程会越来越多cpu就不用干别的了,在那里只切换线程就够它忙活一阵子了,好在系统有更好的机制叫完成端口(IOCP),这个东西大家可以去网上搜搜,说的都比我好多了,个人理解这种机制说白了就是有一个公共的消息队列和几个特定的线程,您cpu有几核就会有几个线程对应,它能保证空闲的cpu内核有空闲的线程,而且也不会没有数量限制的新建线程,说白了就是您往完成端口发东西就会激活一个线程...因此我们要做的是就是定时往完成端口队列中写点东西,让那个队列去通知完成端口开辟出线程来,而且线程的回调方法什么都不做,说白了就是为了打开一个空闲的线程留给新的请求调用,节约时间,具体的实现方法如下(注:第5条观点及代码引自http://blogs.msdn.com/b/wenlong/archive/2010/02/11/why-does-wcf-become-slow-after-being-idle-for-15-seconds.aspx,建议大家去看看,虽然是英文的,写的可比我好多了,哈):
1 static ManualResetEvent s_dummyEvent; 2 3 static RegisteredWaitHandle s_registeredWait; 4 5 /// <summary> 6 /// 函数功能:每隔0.5秒都向IO完成端口发送一个空的数据包,进而激活一个空闲的线程, 7 /// 保证时刻有一个空闲的线程来响应,这样wcf就避免请求因为新建线程延时了 8 /// 备 注:防止.net线程池每16秒回收只留一个线程的情况发生 9 /// </summary> 10 public static void DoWorkaround() 11 { 12 // Create an event that is never set 13 14 //创建一个永远都不启动的事件 15 s_dummyEvent = new ManualResetEvent(false); 16 17 // Register a wait for the event, with a periodic timeout. This causes callbacks 18 // to be queued to an IOCP thread, keeping it alive 19 20 //注册一个等待时间,每500ms注册一次,这样针对完成端口线程队列的回调会保证完成端口线程的活跃(英文不好,大家忍下吧) 21 s_registeredWait = ThreadPool.RegisterWaitForSingleObject( 22 s_dummyEvent, 23 (a, b) => 24 { 25 // Do nothing 26 }, 27 null, 28 500, 29 false); 30 }
写完上边的这个方法在代码中那里调用一下就行了...我修改了自己的程序结构以后那个请求延迟的问题没有了,思来想去我的问题应该是大量的IO线程导致的...
以上就是我总结出来的几点优化思路,希望能对其它兄弟有帮助,有不足的地方希望同行们多多指正