一般对于提供出来的接口,虽然知道在哪些业务场景下才会被调用,但是不知道什么时候被调用、调用的频率、接口性能,当出现问题的时候也不容易重现请求;为了追踪这些内容就需要把每次接口的调用信息给完整的记录下来,也就是记录日志。日志中可以把调用方ip、服务器ip、调用时间点、时长、输入输出都给完整的记录下来,有了这些数据,排查问题、重现异常、性能瓶颈都能准确的找到切入点。

这种功能,当然没人想要去在每个Operation里边插入一段代码,如果有类似AOP的玩意就再好不过了。

wcf中有IDispatchMessageInspector分发消息检查器这么个玩意,

 namespace System.ServiceModel.Dispatcher
{
using System;
using System.ServiceModel;
using System.ServiceModel.Channels; public interface IDispatchMessageInspector
{
      // 接收请求后触发, 此方法的返回值将作为BeforeSendReply的第二个参数correlcationState传入
object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext);
      // 在输出相应触发
void BeforeSendReply(ref Message reply, object correlationState);
}
}

下面是英文解释:

         

亦即允许我们对进出服务的消息进行检查和修改,这看起来有点像mvc的过滤器

执行过程:  AfterReceiveRequest  ->  wcf操作 ->  BeforeSendReply

切入点找到了,需要做的就是实现这个接口方法,记录日志,还可以计算一段时间内访问的次数并设置相应的阀值也就是可以实现并发限流,限流的目的主要是防止恶意调用维持己方服务器的稳定。

实现思路:

  public class ThrottleDispatchMessageInspector : IDispatchMessageInspector
{
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
       // [并发限流]
       以ContractName+OperationName 作为MemoryCache的键,值为调用次数; 设置绝对过期时间
       if(次数 > 设定的阀值)
          request.Close(); 直接关闭请求
        
       // [log] 记录请求输入消息、开始时间、服务器ip、客户端ip、 访问的wcf契约和方法...
       LogVO log
       // [log] 将日志实体作为返回值
       return log; }
   public void BeforeSendReply(ref Message reply, object correlationState) {
        //[log]补充完整log的属性: 请求结束时间, 调用时长... 然后将log丢入队列(不直接插数据库,防止日志记录影响接口性能) 慢慢落地rds中
        var log = correlationState as LogVO;
        log => Queue
     }    
  }

完整的代码如下:

 // 自定义分发消息检查器
public class ThrottleDispatchMessageInspector : IDispatchMessageInspector
{
//TODO 这两个参数根据系统的配置处理方式存储,作为示例就直接写了
public static int throttleNum = ; // 限流个数
public static int throttleUnit = ; // s CacheItemPolicy policy = new CacheItemPolicy(); //! 过期策略,保证第一个set和之后set的绝对过期时间保持一致 #region implement IDispatchMessageInspector // 此方法的返回值 将作为方法BeforeSendReply的第二个参数 object correlationState传入
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{ // 获取ContractName和OperationName 用来作为缓存键
var context = OperationContext.Current;
string contractName = context.EndpointDispatcher.ContractName;
string operationName = string.Empty;
if (context.IncomingMessageHeaders.Action == null)
{
operationName = request.Properties.Values.LastOrDefault().ToString();
}
else
{
if (context.IncomingMessageHeaders.Action.Contains("/"))
{
operationName = context.IncomingMessageHeaders.Action.Split('/').LastOrDefault();
}
}
string throttleCacheKey = contractName + "_" + operationName;
// 缓存当前请求频率, 以内存缓存System.Runtime.Caching.MemoryCache为例(.net4.0+)
ObjectCache cache = MemoryCache.Default;
var requestCount = cache.Get(throttleCacheKey);
int currRequestCount = ;
if (requestCount != null && int.TryParse(requestCount.ToString(), out currRequestCount))
{
// 访问次数+1
currRequestCount++;
cache.Set(throttleCacheKey, currRequestCount, policy); //必须保证过期策略和第一次set的时候一致,不然过期时间会有问题
}
else
{
policy.AbsoluteExpiration = DateTime.Now.AddSeconds(throttleUnit);
cache.Set(throttleCacheKey, currRequestCount, policy);
} // 如果当前请求数大于阀值,直接关闭
if (currRequestCount > throttleNum)
{
request.Close();
} //作为返回值 传给BeforeSendReply
LogVO log = new LogVO
{
BeginTime = DateTime.Now,
ContractName = contractName,
OperationName = operationName,
Request = this.MessageToString(ref request),
Response = string.Empty
};
return log;
} public void BeforeSendReply(ref Message reply, object correlationState)
{ // 补充AfterReceiveRequest 传递过来的日志实体的属性, 记录
LogVO log = correlationState as LogVO;
log.EndTime = DateTime.Now;
log.Response = this.MessageToString(ref reply);
log.Duration = (log.EndTime - log.BeginTime).TotalMilliseconds; //attention 为不影响接口性能,日志实体push进队列(redis .etc),然后慢慢落地
//TODO 这里直接写文本啦~
try
{
string logPath = "D:\\WcfLog.txt";
if (!File.Exists(logPath))
{
File.Create(logPath);
}
StreamWriter writer = new StreamWriter(logPath, true);
writer.Write(string.Format("at {0} , {1} is called , duration: {2} \r\n", log.BeginTime, log.ContractName + "." + log.OperationName, log.Duration));
writer.Close();
}
catch (Exception ex) { }
}
#endregion
}

这边需要注意 : 1. 类似于Web上的HttpContext.Current.Cache 这边使用的是相应的内存缓存 MemoryCache,在更新缓存值的时候 过期策略要保持和初次设置的时候一致,如果没传入则缓存将不过期;

         2. 并发限制的配置根据自身系统框架来设定,不要写死

         3. 日志记录不要直接落rds,不然并发高了rds的连接数很容易爆,而且影响api的处理速度(可以push to redis, job/service land data)

         4. 读取wcf的消息载体Message的数据后,要重新写入(这个方法是直接用老外的)

接着,只要自定义服务行为在ApplyDispatchBehavior方法里将自定义的分发消息检查器给注入到分发运行时就可以了,直接贴代码:

     // 应用自定义服务行为的2中方式:
// 1. 继承Attribute作为特性 服务上打上标示
// 2. 继承BehaviorExtensionElement, 然后修改配置文件
public class ThrottleServiceBehaviorAttribute : Attribute, IServiceBehavior
{
#region implement IServiceBehavior
public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{ } public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
{
foreach (ChannelDispatcher channelDispather in serviceHostBase.ChannelDispatchers)
{
foreach (var endpoint in channelDispather.Endpoints)
{
// holyshit DispatchRuntime
endpoint.DispatchRuntime.MessageInspectors.Add(new ThrottleDispatchMessageInspector());
}
}
} public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
{ }
#endregion #region override BehaviorExtensionElement
//public override Type BehaviorType
//{
// get { return typeof(ThrottleServiceBehavior); }
//} //protected override object CreateBehavior()
//{
// return new ThrottleServiceBehavior();
//}
#endregion
}

这边,由于本人比较懒,直接就继承Attribute后 将服务行为贴在服务上;更好的做法是 继承 BehaviorExtensionElement , 然后在配置文件里边注册自定义行为让所有的接口走自定义检查器的逻辑。

试验: 阀值 10次每4秒

随便弄个Service

  [ThrottleServiceBehavior]
public class Service1 : IService1
{
public string GetData()
{
object num = MemoryCache.Default.Get("IService1_GetData") ?? ""; return string.Format("already request {0} times, Throttle is : {1} per {2} seconds", num, WcfDispatchMessageInspector.ThrottleDispatchMessageInspector.throttleNum, WcfDispatchMessageInspector.ThrottleDispatchMessageInspector.throttleUnit);
}
}

1. 四秒内刷5次:  

2. 超过10次的时候: 

直接就断开了~

完整的代码

wcf利用IDispatchMessageInspector实现接口监控日志记录和并发限流的更多相关文章

  1. C# 面向切面编程--监控日志记录方案

    背景:现在公司整体在做监控平台,要求把各个部分的细节都记录下来,在前台页面上有所显示,所以现在需要做的就是一个监控日志的记录工作,今天讲的就是渲染监控日志的例子. 现状:当前的渲染程序没有为监控日志记 ...

  2. 循序渐进nginx(三):日志管理、http限流、https配置,http_rewrite模块,第三方模块安装,结语

    目录 日志管理 access_log error_log 日志文件切割 自定义错误页 http访问限流 限制请求数 语法 使用 限制连接数 语法 测试 补充: https配置 使用 生成证书 配置ng ...

  3. 利用AOP与ToStringBuilder简化日志记录

    刚学spring的时候书上就强调spring的核心就是ioc和aop blablabla...... IOC到处都能看到...AOP么刚开始接触的时候使用在声明式事务上面..当时书上还提到一个用到ao ...

  4. 如何利用redis来进行分布式集群系统的限流设计

    在很多高并发请求的情况下,我们经常需要对系统进行限流,而且需要对应用集群进行全局的限流,那么我们如何类实现呢. 我们可以利用redis的缓存来进行实现,并且结合mysql数据库一起,先来看一个流程图. ...

  5. 高并发之 API 接口,分布式,防刷限流,如何做?

    在开发分布式高并发系统时有三把利器用来保护系统:缓存.降级.限流 缓存 缓存的目的是提升系统访问速度和增大系统处理容量 降级 降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解 ...

  6. 实例:接口并发限流RateLimiter

    需求:接口每秒最多只能相应1个请求 1.创建 全局类对象 import com.google.common.util.concurrent.RateLimiter; import org.spring ...

  7. springcloud zuulfilter 实现get,post请求日志记录功能

    import com.alibaba.fastjson.JSONObject; import com.idoipo.infras.gateway.open.model.InvokeLogModel; ...

  8. 【高并发】亿级流量场景下如何为HTTP接口限流?看完我懂了!!

    写在前面 在互联网应用中,高并发系统会面临一个重大的挑战,那就是大量流高并发访问,比如:天猫的双十一.京东618.秒杀.抢购促销等,这些都是典型的大流量高并发场景.关于秒杀,小伙伴们可以参见我的另一篇 ...

  9. 利用 Nginx 实现限流

    在当下互联网高并发时代中,项目往往会遇到需要限制客户端连接的需求.我们熟知的 Nginx 就提供了有这样的功能,可以简单的实现对客户端请求频率,并发连接和传输速度的限制…. Nginx 限流 Ngin ...

随机推荐

  1. 新人补钙系列教程之:拒绝CPU高占用

    1.关于MovieClip和Sprite的鼠标事件,当不需要鼠标事件的时候将mouseEnabled和mouseChildren设为false. 不断的检测鼠标交互事件会消耗CPU,尤其是大量交互对象 ...

  2. oracle闪回flashback_transaction_query知识点

    查询更新记录: select t.start_timestamp, t.commit_timestamp, t.logon_user, t.operation, t.table_name, t.tab ...

  3. shell中set命令

    set命令作用主要是显示系统中已经存在的shell变量,以及设置shell变量的新变量值.set命令不能够定义新的shell变量.如果要定义新的变量,可以使用declare命令以变量名=值的格式进行定 ...

  4. Nginx user_agent、if指令及全局变量

    Nginx user_agent.if指令及全局变量 1.User_agent User Agent中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本.CP ...

  5. Hadoop之——分布式集群安装过程简化版

    转载请注明出处:http://blog.csdn.net/l1028386804/article/details/46352315 1.hadoop的分布式安装过程 1.1 分布结构 主节点(1个,是 ...

  6. OpenCV2学习笔记(十五):利用Cmake高速查找OpenCV函数源代码

    在使用OpenCV时,在对一个函数的调用不是非常了解的情况下,通常希望查到该函数的官方声明.而假设想进一步研究OpenCV的函数,则必须深入到源码. 在VS中我们能够选中想要查看的OpenCV函数,点 ...

  7. C3:建造者模式 Builder

    将一个复杂对象的创建与表示分离,使得同样的构建过程可以创建不同的表示. 应用场景: A.创建这个对象通常需要较多的参数,才能完整的表示该对象.B.类的各个组成部分的具体实现类或算法经常面临变化,但将他 ...

  8. 浏览器网页推断手机是否安装IOS/Androidclient程序

    IOS 原理例如以下: 为HTML页面中的超链接点击事件添加一个setTimeout方法. 假设在iPhone上面500ms内,本机有应用程序能解析这个协议并打开程序,则这个回调方法失效. 假设本机没 ...

  9. Linux 下Office 软件名称

    Linux 下Office 软件名称

  10. d3.js封装文本实现自动换行和旋转平移等功能

    我们下面话不多说,本文主要介绍的是利用D3.js封装文本实现自动换行功能的步骤,下面来一起看看吧. 一.引用 multext.js 文件 multext.js function appendMulti ...