一般对于提供出来的接口,虽然知道在哪些业务场景下才会被调用,但是不知道什么时候被调用、调用的频率、接口性能,当出现问题的时候也不容易重现请求;为了追踪这些内容就需要把每次接口的调用信息给完整的记录下来,也就是记录日志。日志中可以把调用方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 Cache unCache

    CPU uncache 写就可以了 CPU chace这种flag的话 还要flush过去 不然gpu那边拿到的这块buffer里面没有内容 都是空

  2. spring boot 缺点优点?

    作者:八面山人链接:https://www.zhihu.com/question/39483566/answer/246333825来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请 ...

  3. 我的MAC可能在设置环境变量的时候设置错了,现在整个MAC的vi,ls等命令都执行不了了。

    1,在命令行中输入export PATH=/usr/bin:/usr/sbin:/bin:/sbin:/usr/X11R6/bin这样可以保证命令行命令暂时可以使用.命令执行完之后先不要关闭终端或者c ...

  4. JAVA Eclipse中如何简易的实现消息机制

    大部分情况下,我们需要实现的消息机制无非是某个类得到了数据,需要传递到某个主界面上去显示,可以把这个消息在类中做成全局变量,主界面的类用一个线程定时扫描,如果这个数据不是空,则说明被类刷新了,那么更新 ...

  5. Linux组件封装(二)中条件变量Condition的封装

    条件变量主要用于实现线程之间的协作关系. pthread_cond_t常用的操作有: int pthread_cond_init(pthread_cond_t *cond, pthread_conda ...

  6. VB断点调试

    最近都在敲机房收费系统,这个系统是我们第一次自己在没有源代码的情况下进行的系统. 写程序的时候逻辑非常重要,可是我们还要清楚非常多时候你以为的并非你以为的! 就像在敲机房的时候,我们明明理清了逻辑.并 ...

  7. SoC嵌入式软件架构设计之三:代码分块(Bank)设计原则

    上一节讲述了在没有MMU的CPU(如80251.MIPS M控制器系列.ARM cortex m系列)上实现虚拟内存管理的集成硬件设计方法.新设计的内存管理管理单元要实现虚拟内存管理还须要操作系统.代 ...

  8. Linux——使用国内镜像通过pip安装python的一些包

    学习flask,安装virtualenv环境,这些带都ok,但是一安装包总是出错无法安装, http://e.pypi.python.org/这个就是官网了,清华大学提供的 建议非清华大学校内的使用这 ...

  9. (十)Thymeleaf用法——Themeleaf内联

    5. 内联 [[...]]是内联文本的表示格式,但需要使用th:inline属性(分为text,javascript,none)激活. 5.1 文本内联    <p th:inline=&quo ...

  10. Ubuntu16.04 打开txt文件乱码

    最近遇到个小问题:Ubuntu16.04下打开txt出现乱码,倒腾下解决了这个问题,记录下来. Ubuntu16.04 默认已经安装gedit.直接双击被打开的文件默认用gedit打开,显然这种方式行 ...