最近常收到SOD框架的朋友报告的SOD的SQL日志功能报错:文件句柄丢失。经过分析得知,这些朋友使用SOD框架开发了访问量比较大的系统,由于忘记关闭SQL日志功能所以出现了很高频率的日志写入操作,从而偶然引起错误。后来我建议只记录出错的或者执行时间较长的SQL信息,暂时解决了此问题。但是作为一个热心造轮子的人,一定要看看能不能造一个更好的轮子出来。

前面说的错误原因已经很直白了,就是频繁的日志写入导致的,那么解决方案就是将多次写入操作合并成一次写入操作,并且采用异步写入方式。要保存多次操作的内容就要有一个类似“队列”的东西来保存,而一般的线程安全的队列,都是“有锁队列”,在性能要求很高的系统中,不希望在日志记录这个地方耗费多一点计算资源,所以最好有一个“无锁队列”,因此最佳方案就是Ring Buffer(环形缓冲区)了。

什么是Ring Buffer?顾名思义,就是一个内存环,每一次读写操作都循环利用这个内存环,从而避免频繁分配和回收内存,减轻GC压力,同时由于Ring Buffer可以实现为无锁的队列,从而整体上大幅提高系统性能。Ring Buffer的示意图如下,有关具体原理,请参考此文《Ring Buffer 有什么特别?》。

上文并没有详细说明如何具体读写Ring Buffer,但是原理介绍已经足够我们怎么写一个Ring Buffer程序了,接下来看看我在 .NET上的实现。

首先,定一个存放数据的数组,记住一定要用数组,它是实现Ring Buffer的关键并且CPU友好。

  1. const int C_BUFFER_SIZE = ;//写入次数缓冲区大小,每次的实际内容大小不固定
  2. string[] RingBuffer = new string[C_BUFFER_SIZE];
    int writedTimes = 0;

变量writedTimes 记录写入次数,它会一直递增,不过为了线程安全的递增且不使用托管锁,需要使用原子锁Interlocked。之后,根据每次 writedTimes 跟环形缓冲区的大小求余数,得到当前要写入的数组位置:

  1. void SaveFile(string fileName, string text)
  2. {
  3. int currP= Interlocked.Increment(ref writedTimes);
  4. int writeP= currP % C_BUFFER_SIZE ;
  5. int index = writeP == ? C_BUFFER_SIZE - : writeP - ;
  6. RingBuffer[index] = " Arr[" + index + "]:" + text;
  7. }

Ring Buffer的核心代码就这么点,调用此方法,会一直往缓冲区写入数据而不会“溢出”,所以写入Ring Buffer效率很高。

一个队列如果只生产不消费肯定不行的,那么如何及时消费Ring Buffer的数据呢?简单的方案就是当Ring Buffer“写满”的时候一次性将数据“消费”掉。注意这里的“写满”仅仅是指写入位置 index达到了数组最大索引位置,而“消费”也不同于常见的堆栈,队列等数据结构,只是读取缓冲区的数据而不会移除它。

所以前面的代码只需要稍加改造:

  1. void SaveFile(string fileName, string text)
  2. {
  3. int currP= Interlocked.Increment(ref writedTimes);
  4. int writeP= currP % C_BUFFER_SIZE ;
  5. int index = writeP == ? C_BUFFER_SIZE - : writeP - ;
  6. RingBuffer[index] = " Arr[" + index + "]:" + text;
  7. if (writeP == )
  8. {
  9. string result = string.Concat( RingBuffer);
  10. FlushFile(fileName, result);
  11. }
  12. }

writeP == 0 表示当前一轮的缓冲区已经写满,然后调用函数 FlushFile 将Ring Buffer的数据连接起来,整体写入文件。

  1. void FlushFile(string fileName, string text)
  2. {
  3. using (FileStream fs = new FileStream(fileName, FileMode.Append, FileAccess.Write, FileShare.Write, , FileOptions.Asynchronous))
  4. {
  5. byte[] buffer = System.Text.Encoding.UTF8.GetBytes(text);
  6. IAsyncResult writeResult = fs.BeginWrite(buffer, , buffer.Length,
  7. (asyncResult) =>
  8. {
  9. fs.EndWrite(asyncResult);
  10.  
  11. },
  12. fs);
  13. //fs.EndWrite(writeResult);//这种方法异步起不到效果
  14. fs.Flush();
  15.  
  16. }
  17. }

在函数 FlushFile 中我们使用了异步写入文件的技术,注意 FileOptions.Asynchronous ,使用它才可以真正利用Windows的完成端口IOCP,将文件异步写入。

当然这段代码也可以使用.NET最新版本支持的 async/await ,不过我要让SOD框架继续支持.NET 2.0,所以只好这样写了。

现在,我们可以开多线程来测试这个循环队列效果怎么样:

  1. Task[] arrTask = new Task[];
  2. for (int i = ; i < arrTask.Length; i++)
  3. {
  4. arrTask[i] = new Task(obj => SaveFile( (int)obj) ,i);
  5. }
  6. for (int i = ; i < arrTask.Length; i++)
  7. {
  8. arrTask[i].Start();
  9. }
  10.  
  11. Task.WaitAll(arrTask);
  12. MessageBox.Show(arrTask.Length +" Task All OK.");

这里开启20个Task任务线程来写入文件,运行此程序,发现20个线程才写入了10条数据,分析很久才发现,文件异步IO太快的话,会有缓冲区丢失,第一次写入的10条数据无法写入文件,多运行几次就没有问题了。所以还是得想法解决此问题。

通常情况下我们都是使用托管锁来解决这种并发问题,但本文的目的就是要实现一个“无锁环形缓冲区”,不能在此“功亏一篑”,所以此时“信号量”上场了。

同步可以分为锁定和信号同步,信号同步机制中涉及的类型都继承自抽象类WaitHandle,这些类型有EventWaitHandle(类型化为AutoResetEvent、ManualResetEvent)、Semaphore以及Mutex。见下图:

首先声明一个 ManualResetEvent对象:

  1. ManualResetEvent ChangeEvent = new ManualResetEvent(true);

这里我们将 ManualResetEvent 对象设置成 “终止状态”,意味着程序一开始是允许所有线程不等待的,当我们需要消费Ring Buffer的时候再将  ManualResetEvent 设置成“非终止状态”,阻塞其它线程。简单说就是当要写文件的时候将环形缓冲区阻塞,直到文件写完才允许继续写入环形缓冲区。

对应的新的代码调整如下:

  1. void SaveFile(string fileName, string text)
  2. {
  3. ChangeEvent.WaitOne();
  4. int currP= Interlocked.Increment(ref writedTimes);
  5. int writeP= currP % C_BUFFER_SIZE ;
  6. int index = writeP == ? C_BUFFER_SIZE - : writeP - ;
  7. RingBuffer[index] = " Arr[" + index + "]:" + text;
  8. if (writeP == )
  9. {
  10. ChangeEvent.Reset();
  11. string result = string.Concat( RingBuffer);
  12. FlushFile(fileName, result);
  13. }
  14. }

然后,再FlushFile 方法的 回掉方法中,加入设置终止状态的代码,部分代码如下:

  1. (asyncResult) =>
  2. {
  3. fs.EndWrite(asyncResult);
  4. ChangeEvent.Set();
  5. }

OK,现在我们的程序具备高性能的安全的写入日志文件的功能了,我们来看看演示程序测试的日志结果实例:

  1. Arr[0]:Thread index:0--FFFFFFF
  2. Arr[1]:Thread index:1--FFFFFFF
  3. Arr[2]:Thread index:8--FFFFFFF
  4. Arr[3]:Thread index:9--FFFFFFF
  5. Arr[4]:Thread index:3--FFFFFFF
  6. Arr[5]:Thread index:2--FFFFFFF
  7. Arr[6]:Thread index:4--FFFFFFF
  8. Arr[7]:Thread index:10--FFFFFFF
  9. Arr[8]:Thread index:5--FFFFFFF
  10. Arr[9]:Thread index:6--FFFFFFF
  11. Arr[0]:Thread index:7--FFFFFFF
  12. Arr[1]:Thread index:11--FFFFFFF
  13. Arr[2]:Thread index:12--FFFFFFF
  14. Arr[3]:Thread index:13--FFFFFFF
  15. Arr[4]:Thread index:14--FFFFFFF
  16. Arr[5]:Thread index:15--FFFFFFF
  17. Arr[6]:Thread index:16--FFFFFFF
  18. Arr[7]:Thread index:17--FFFFFFF
  19. Arr[8]:Thread index:18--FFFFFFF
  20. Arr[9]:Thread index:19--FFFFFFF

测试结果符合预期!
到此,我们今天的主题就全部介绍完成了,不过要让本文的代码能够符合实际的运行,还要解决每次只写入少量数据并且将它定期写入日志文件的问题,这里贴出真正的局部代码:

PS:有朋友说采用信号量并不能完全保证程序安全,查阅了MSDN也说如果信号量状态改变还没有来得及应用,那么是起不到作用的,所以还需要检查业务状态标记,也就是在设置非终止状态后,马上设置一个操作标记,在其它线程中,需要检查此标记,以避免“漏网之鱼”引起不期望的结果。

再具体实现上,我们可以实现一个“自旋锁”,循环检查此状态标记,为了防止发生死锁,还需要有锁超时机制,代码如下:

  1. void SaveFile(string fileName, string text)
  2. {
  3. ChangeEvent.WaitOne();
  4. int currP= Interlocked.Increment(ref WritedTimes);
  5. int writeP= currP % C_BUFFER_SIZE ;
  6. int index = writeP == ? C_BUFFER_SIZE - : writeP - ;
  7.  
  8. if (writeP == )
  9. {
  10. ChangeEvent.Reset();
  11. IsReading = true;
  12. RingBuffer[index] = " Arr[" + index + "]:" + text;
  13.  
  14. LastWriteTime = DateTime.Now;
  15. WritingIndex = ;
  16. SaveFile(fileName,RingBuffer);
  17. }
  18. else if (DateTime.Now.Subtract(LastWriteTime).TotalSeconds > C_WRITE_TIMESPAN)
  19. {
  20. ChangeEvent.Reset();
  21. IsReading = true;
  22. RingBuffer[index] = " Arr[" + index + "]:" + text;
  23.  
  24. int length = index - WritingIndex + ;
  25. if (length <= )
  26. length = ;
  27. string[] newArr = new string[length];
  28. Array.Copy(RingBuffer, WritingIndex, newArr, , length);
  29.  
  30. LastWriteTime = DateTime.Now;
  31. WritingIndex = index + ;
  32. SaveFile(fileName, newArr);
  33. }
  34. else
  35. {
  36. //防止漏网之鱼的线程在信号量产生作用之前修改数据
  37. //采用“自旋锁”等待
  38. int count = ;
  39. while (IsReading)
  40. {
  41. if (count++ > )
  42. {
  43. Thread.Sleep();
  44. break;
  45. }
  46. }
  47. RingBuffer[index] = " Arr[" + index + "]:" + text;
  48. }
  49. }

完整的Ring Buffer代码会在最新版本的SOD框架源码中,有关本篇文章测试程序的完整源码,请加QQ群讨论获取,

群号码:SOD框架高级群 18215717 ,加群请注明 PDF.NET技术交流 ,否则可能被拒绝。

使用Ring Buffer构建高性能的文件写入程序的更多相关文章

  1. 利用memcached构建高性能的Web应用程序(转载)

    面临的问题 对于高并发高访问的Web应用程序来说,数据库存取瓶颈一直是个令人头疼的问题.特别当你的程序架构还是建立在单数据库模式,而一个数据池连接数峰 值已经达到500的时候,那你的程序运行离崩溃的边 ...

  2. NVIDIA DeepStream 5.0构建智能视频分析应用程序

    NVIDIA DeepStream 5.0构建智能视频分析应用程序 无论是要平衡产品分配和优化流量的仓库,工厂流水线检查还是医院管理,要确保员工和护理人员在照顾病人的同时使用个人保护设备(PPE),就 ...

  3. 构建高性能web站点--读书大纲

    用户输入你的站点网址,等了半天..还没打开,裤衩一下就给关了.好了,流失了一个用户.为什么会有这样的问题呢.怎么解决自己站点“慢”,体验差的问题呢. 在这段等待的时间里,到底发生了什么?事实上这并不简 ...

  4. 构建高性能服务(三)Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue--转载

    原文地址:http://maoyidao.iteye.com/blog/1663193 一个仅仅部署在4台服务器上的服务,每秒向Database写入数据超过100万行数据,每分钟产生超过1G的数据.而 ...

  5. hdfs文件写入kafka集群

    1. 场景描述 因新增Kafka集群,需要将hdfs文件写入到新增的Kafka集群中,后来发现文件不多,就直接下载文件到本地,通过Main函数写入了,假如需要部署到服务器上执行,需将文件读取这块稍做修 ...

  6. Java读文件写入kafka

    目录 Java读文件写入kafka 文件格式 pom依赖 java代码 Java读文件写入kafka 文件格式 840271 103208 0 0.0 insert 84e66588-8875-441 ...

  7. 一点公益商城开发系统模式Ring Buffer+

    一个队列如果只生产不消费肯定不行的,那么如何及时消费Ring Buffer的数据呢?简单的方案就是当Ring Buffer"写满"的时候一次性将数据"消费"掉. ...

  8. 【读书笔记】2016.12.10 《构建高性能Web站点》

    本文地址 分享提纲: 1. 概述 2. 知识点 3. 待整理点 4. 参考文档 1. 概述 1.1)[该书信息] <构建高性能Web站点>: -- 百度百科 -- 本书目录: 第1章 绪论 ...

  9. 构建高性能的ASP.NET应用程序

    看见大标题的时候,也许各位看官会自然而然的联想到如何在设计阶段考虑系统性能问题,如何编写高性能的程序代码.关于这一点,大家可以在MSDN和相关网站上找到非常多的介绍,不过大多是防患于未难,提供的是在设 ...

随机推荐

  1. 仿陌陌的ios客户端+服务端源码项目

    软件功能:模仿陌陌客户端,功能很相似,注册.登陆.上传照片.浏览照片.浏览查找附近会员.关注.取消关注.聊天.语音和文字聊天,还有拼车和搭车的功能,支持微博分享和查找好友. 后台是php+mysql, ...

  2. 给缺少Python项目实战经验的人

    我们在学习过程中最容易犯的一个错误就是:看的多动手的少,特别是对于一些项目的开发学习就更少了! 没有一个完整的项目开发过程,是不会对整个开发流程以及理论知识有牢固的认知的,对于怎样将所学的理论知识应用 ...

  3. 利用PowerShell复制SQLServer账户的所有权限

    问题 对于DBA或者其他运维人员来说授权一个账户的相同权限给另一个账户是一个很普通的任务.但是随着服务器.数据库.应用.使用人员地增加就变得很枯燥乏味又耗时费力的工作.那么有什么容易的办法来实现这个任 ...

  4. jQuery 的选择器常用的元素查找方法

    jQuery 的选择器常用的元素查找方法 基本选择器: $("#myELement")    选择id值等于myElement的元素,id值不能重复在文档中只能有一个id值是myE ...

  5. Android NDK debug 方法

    最近又频繁遇到 NDK 的错误,记录一下debug调试的一些经验,以备后续查看 一般来说,在Android Studio中的Monitor中将过滤器的 LOG TAG 设置为 "DEBUG& ...

  6. springMvc的日期转换之二

    方式一:使用@InitBinder注解实现日期转换 前台页面: 后台打印: 方式二:处理多种日期格式类型之间的转换 采用方式:由于binder.registerCustomEditor(Date.cl ...

  7. js中的null 和undefined

    参考链接:http://blog.csdn.net/qq_26676207/article/details/53100912 http://www.ruanyifeng.com/blog/2014/0 ...

  8. javascript中的变量作用域以及变量提升

    在javascript中, 理解变量的作用域以及变量提升是非常有必要的.这个看起来是否很简单,但其实并不是你想的那样,还要一些重要的细节你需要理解. 变量作用域 “一个变量的作用域表示这个变量存在的上 ...

  9. Linux.NET学习手记(7)

    前一篇中,我们简单的讲述了下如何在Linux.NET中部署第一个ASP.NET MVC 5.0的程序.而目前微软已经提出OWIN并致力于发展VNext,接下来系列中,我们将会向OWIN方向转战. 早在 ...

  10. 打造TypeScript的Visual Studio Code开发环境

    打造TypeScript的Visual Studio Code开发环境 本文转自:https://zhuanlan.zhihu.com/p/21611724 作者: 2gua TypeScript是由 ...