回顾上文

  作为单体程序,依赖的第三方服务虽不多,但是2C的程序还是有不少内容可讲; 作为一个常规互联网系统,无外乎就是接受请求、处理请求,输出响应。

由于业务渐渐增长,单机多核的共享内存模式带来的问题很多,编程也困难,随着多核时代和分布式系统的到来,共享模型已经不太适合并发编程,因此Actor模型又重新受到了人们的重视。

-----调试多线程都懂------

* 传统的编程模型通常使用回调和同步对象(如锁)来协调任务和访问共享数据,  从宏观看传统模型: 任务是一步步紧接着完成的,资源是需要抢占的。

* Actor模式是一种并发模型,与另一种模型共享内存完全相反,Actor模型share nothing。所有的线程(或进程)通过消息传递的方式进行合作,这些线程(或进程)称为Actor, 预先定义了任务的流水线后,不关注数据什么时候流到这个任务 ,专注完成工序任务。

https://www.cnblogs.com/csguo/p/7521322.html

https://www.cnblogs.com/youxin/p/3589881.html

.Net TPL  Dataflow组件帮助我们快速实现Actor模型。

TPL Dataflow是微软前几年给出的数据处理库, 内置常见的处理块,可将这些块组装成一个处理管道,"块"对应处理管道中的"阶段", 可类比AspNetCore 中Middleware 和pipeline.。

  • TPL Dataflow库为消息传递和并行化CPU密集型和I / O密集型应用程序提供了编程基础,这些应用程序具有高吞吐量和低延迟。它还可以让您明确控制数据的缓冲方式并在系统中移动。

  • 为了更好地理解数据流编程模型,请考虑从磁盘异步加载图像并创建这些图像的应用程序。
    • 传统的编程模型通常使用回调和同步对象(如锁)来协调任务和访问共享数据,

    • 通过使用数据流编程模型,您可以创建在从磁盘读取图像时处理图像的数据流对象。在数据流模型下,您可以声明数据在可用时的处理方式以及数据之间的依赖关系。 由于运行时管理数据之间的依赖关系,因此通常可以避免同步访问共享数据的要求。此外,由于运行时调度基于数据的异步到达而工作,因此数据流可以通过有效地管理底层线程来提高响应性和吞吐量。  

  • 需要注意的是:TPL Dataflow 非分布式数据流,消息在进程内传递,   使用nuget引用 System.Threading.Tasks.Dataflow 包。

TPL Dataflow 核心概念

Buffer & Block

TPL Dataflow 内置的Block覆盖了常见的应用场景,当然如果内置块不能满足你的要求,你也可以自定“块”。

Block可以划分为下面3类:

  • Buffering Only    【Buffer不是缓存Cache的概念, 而是一个缓冲区的概念】
  • Execution

  • Grouping

使用以上块混搭处理管道, 大多数的块都会执行一个操作,有些时候需要将消息分发到不同Block,这时可使用特殊类型的缓冲块给管道“”分叉”。

Execution Block

  可执行的块有两个核心组件:
  • 输入、输出消息的缓冲区(一般称为Input,Output队列)

  • 在消息上执行动作的委托

  消息在输入和输出时能够被缓冲:当Func委托的运行速度比输入的消息速度慢时,后续消息将在到达时进行缓冲;当下一个块的输入缓冲区中没有容量时,将在输出时缓冲。

每个块我们可以配置:

  • 缓冲区的总容量, 默认无上限

  • 执行操作委托的并发度, 默认情况下块按照顺序处理消息,一次一个。

我们将块链接在一起形成一个处理管道,生产者将消息推向管道。

TPL Dataflow有一个基于pull的机制(使用Receive和TryReceive方法),但我们将在管道中使用块连接和推送机制。

  • TransformBlock(Execution category)-- 由输入输出缓冲区和一个Func<TInput, TOutput>委托组成,消费的每个消息,都会输出另外一个,你可以使用这个Block去执行输入消息的转换,或者转发输出的消息到另外一个Block。

  • TransformManyBlock (Execution category) -- 由输入输出缓冲区和一个Func<TInput, IEnumerable<TOutput>>委托组成, 它为输入的每个消息输出一个 IEnumerable<TOutput>

  • BroadcastBlock (Buffering category)-- 由只容纳1个消息的缓冲区和Func<T, T>委托组成。缓冲区被每个新传入的消息所覆盖,委托仅仅为了让你控制怎样克隆这个消息,不做消息转换。

            该块可以链接到多个块(管道的分叉),虽然它一次只缓冲一条消息,但它一定会在该消息被覆盖之前将该消息转发到链接块(链接块还有缓冲区)。

  • ActionBlock (Execution category)-- 由缓冲区和Action<T>委托组成,他们一般是管道的结尾,他们不再给其他块转发消息,他们只会处理输入的消息。

  • BatchBlock (Grouping category)-- 告诉它你想要的每个批处理的大小,它将累积消息,直到它达到那个大小,然后将它作为一组消息转发到下一个块。

  还有一下其他的Block类型:BufferBlock、WriteOnceBlock、JoinBlock、BatchedJoinBlock,我们暂时不会深入。

Pipeline Chain React

  当输入缓冲区达到上限容量,为其供货的上游块的输出缓冲区将开始填充,当输出缓冲区已满时,该块必须暂停处理,直到缓冲区有空间,这意味着一个Block的处理瓶颈可能导致所有前面的块的缓冲区被填满。

  但是不是所有的块变满时,都会暂停,BroadcastBlock 有允许1个消息的缓冲区,每个消息都会被覆盖, 因此如果这个广播块不能将消息转发到下游,则在下个消息到达的时候消息将丢失,这在某种意义上是一种限流(比较生硬).

编程实践

生产者投递消息

可使用Post或者SendAsync 方法向首块投递消息

  • Post方法即时返回true/false, True意味着消息被block接收(缓冲区有空余), false意味着拒绝了消息(缓冲区已满或者Block已经出错了)。

  • SendAsync方法返回一个Task<bool>, 将会以异步的方式阻塞直到块接收、拒绝、块出错。

Post、SendAsync的不同点在于SendAsync可以延迟投递(下一管道的输入buffer不空,可稍后投递消息)。

定义流水线

按照上图工作流定义 流水线

  1. public EqidPairHandler(IHttpClientFactory httpClientFactory, RedisDatabase redisCache, IConfiguration con, LogConfig logConfig, ILoggerFactory loggerFactory)
  2. {
  3. _httpClient = httpClientFactory.CreateClient("bce-request");
  4. _redisDB0 = redisCache[];
  5. _redisDB = redisCache;
  6. _logger = loggerFactory.CreateLogger(nameof(EqidPairHandler));
  7. var option = new DataflowLinkOptions { PropagateCompletion = true };
  8.  
  9. publisher = _redisDB.RedisConnection.GetSubscriber();
  10. _eqid2ModelTransformBlock = new TransformBlock<EqidPair, EqidModel>
  11. (
  12. // redis piublih 没有做在TransformBlock fun里面, 因为publih失败可能影响后续的block传递
  13. eqidPair => EqidResolverAsync(eqidPair),
  14. new ExecutionDataflowBlockOptions
  15. {
  16. MaxDegreeOfParallelism = con.GetValue<int>("MaxDegreeOfParallelism")
  17. }
  18. );
  19. // https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/walkthrough-creating-a-dataflow-pipeline
  20. _logBatchBlock = new LogBatchBlock<EqidModel>(logConfig, loggerFactory);
  21. _logPublishBlock = new ActionBlock<EqidModel>(x => PublishAsync(x) );
  22. _broadcastBlock = new BroadcastBlock<EqidModel>(x => x); // 由只容纳一个消息的缓存区和拷贝函数组成

      _broadcastBlock.LinkTo(_logBatchBlock.InputBlock, option);
      _broadcastBlock.LinkTo(_logPublishBlock, option);
      _eqid2ModelTransformBlock.LinkTo(_broadcastBlock, option);

  1. }
  1. public class LogBatchBlock<T> : ILogDestination<T> where T : IModelBase
  2. {
  3. private readonly string _dirPath;
  4. private readonly Timer _triggerBatchTimer;
  5. private readonly Timer _openFileTimer;
  6. private DateTime? _nextCheckpoint;
  7. private TextWriter _currentWriter;
  8. private readonly LogHead _logHead;
  9. private readonly object _syncRoot = new object();
  10. private readonly ILogger _logger;
  11. private readonly BatchBlock<T> _packer;
  12. private readonly ActionBlock<T[]> batchWriterBlock;
  13. private readonly TimeSpan _logFileIntervalTimeSpan;
  14.  
  15. /// <summary>
  16. /// Generate request log file.
  17. /// </summary>
  18. public LogBatchBlock(LogConfig logConfig, ILoggerFactory loggerFactory)
  19. {
  20. _logger = loggerFactory.CreateLogger<LogBatchBlock<T>>();
  21.  
  22. _dirPath = logConfig.DirPath;
  23. if (!Directory.Exists(_dirPath))
  24. {
  25. Directory.CreateDirectory(_dirPath);
  26. }
  27. _logHead = logConfig.LogHead;
  28.  
  29. _packer = new BatchBlock<T>(logConfig.BatchSize);
  30. batchWriterBlock = new ActionBlock<T[]>(models => WriteToFile(models)); // 形成pipeline必须放在LinkTo前面
  31. _packer.LinkTo(batchWriterBlock, new DataflowLinkOptions { PropagateCompletion = true });
  32.  
  33. // 防止BatchPacker一直不满足10条数据,无法打包,故设定间隔15s强制写入
  34. _triggerBatchTimer = new Timer(state =>
  35. {
  36. _packer.TriggerBatch();
  37. }, null, TimeSpan.Zero, TimeSpan.FromSeconds(logConfig.Period));
  38.  
  39. // 实时写文件流能确保随时生成文件,但存在极端情况:某小时没有需要写入的数据,导致该小时不会创建文件,以下定时任务确保创建文件
  40. _logFileIntervalTimeSpan = TimeSpan.Parse(logConfig.LogFileInterval);
  41. _openFileTimer = new Timer(state =>
  42. {
  43. AlignCurrentFileTo(DateTime.Now);
  44. }, null, TimeSpan.Zero, _logFileIntervalTimeSpan);
  45. }
  46.  
  47. public ITargetBlock<T> InputBlock => _packer;
  48.  
  49. private void AlignCurrentFileTo(DateTime dt)
  50. {
  51. if (!_nextCheckpoint.HasValue)
  52. {
  53. OpenFile(dt);
  54. }
  55. if (dt >= _nextCheckpoint.Value)
  56. {
  57. CloseFile();
  58. OpenFile(dt);
  59. }
  60. }
  61.  
  62. private void OpenFile(DateTime now, string fileSuffix = null)
  63. {
  64. string filePath = null;
  65. try
  66. {
  67. var currentHour = now.Date.AddHours(now.Hour);
  68. _nextCheckpoint = currentHour.Add(_logFileIntervalTimeSpan);
  69. int hourConfiguration = _logFileIntervalTimeSpan.Hours;
  70. int minuteConfiguration = _logFileIntervalTimeSpan.Minutes;
  71. filePath = $"{_dirPath}/u_ex{now.ToString("yyMMddHH")}{fileSuffix}.log";
  72.  
  73. var appendHead = !File.Exists(filePath);
  74. if (filePath != null)
  75. {
  76. var stream = new FileStream(filePath, FileMode.Append, FileAccess.Write);
  77. var sw = new StreamWriter(stream, Encoding.Default);
  78. if (appendHead)
  79. {
  80. sw.Write(GenerateHead());
  81. }
  82. _currentWriter = sw;
  83. _logger.LogDebug($"{DateTime.Now} TextWriter has been created.");
  84. }
  85. }
  86. catch (Exception e)
  87. {
  88. if (fileSuffix == null)
  89. {
  90. _logger.LogWarning($"OpenFile failed:{e.StackTrace.ToString()}:{e.Message}." );
  91. OpenFile(now, $"-{Guid.NewGuid()}");
  92. }
  93. else
  94. {
  95. _logger.LogError($"OpenFile failed after retry: {filePath}", e);
  96. }
  97. }
  98. }
  99.  
  100. private void CloseFile()
  101. {
  102. if (_currentWriter != null)
  103. {
  104. _currentWriter.Flush();
  105. _currentWriter.Dispose();
  106. _currentWriter = null;
  107. _logger.LogDebug($"{DateTime.Now} TextWriter has been disposed.");
  108. }
  109. _nextCheckpoint = null;
  110. }
  111.  
  112. private string GenerateHead()
  113. {
  114. StringBuilder head = new StringBuilder();
  115. head.AppendLine("#Software: " + _logHead.Software)
  116. .AppendLine("#Version: " + _logHead.Version)
  117. .AppendLine($"#Date: {DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")}")
  118. .AppendLine("#Fields: " + _logHead.Fields);
  119. return head.ToString();
  120. }
  121.  
  122. private void WriteToFile(T[] models)
  123. {
  124. try
  125. {
  126. lock (_syncRoot)
  127. {
  128. var flag = false;
  129. foreach (var model in models)
  130. {
  131. if (model == null)
  132. continue;
  133. flag = true;
  134. AlignCurrentFileTo(model.ServerLocalTime);
  135. _currentWriter.WriteLine(model.ToString());
  136. }
  137. if (flag)
  138. _currentWriter.Flush();
  139. }
  140. }
  141. catch (Exception ex)
  142. {
  143. _logger.LogError("WriteToFile Error : {0}", ex.Message);
  144. }
  145. }
  146.  
  147. public bool AcceptLogModel(T model)
  148. {
  149. return _packer.Post(model);
  150. }
  151.  
  152. public string GetDirPath()
  153. {
  154. return _dirPath;
  155. }
  156.  
  157. public async Task CompleteAsync()
  158. {
  159. _triggerBatchTimer.Dispose();
  160. _openFileTimer.Dispose();
  161. _packer.TriggerBatch();
  162. _packer.Complete();
  163. await InputBlock.Completion;
  164. lock (_syncRoot)
  165. {
  166. CloseFile();
  167. }
  168. }
  169. }

仿IIS日志写入组件

注意事项 :异常处理

  上述程序在部署时就遇到相关的坑位,在测试环境_eqid2ModelTransformBlock 内Func委托稳定执行,程序并未出现异样;

  部署到生产之后, 该Pipeline运行一段时间就停止工作,一直很困惑, 后来通过监测_eqid2ModelTransformBlock.Completion 属性,发现该块在执行某次Func委托时报错,提前进入完成态

官方资料表明: 某块进入Fault、Cancel状态,都会导致该块提前进入“完成态”,但因Fault、Cancle进入的“完成态”会导致 输入buffer和输出buffer 被清空。

  After Fault has been called on a dataflow block, that block will complete, and its Completion task will enter a final state. Faulting a block, as with canceling a block, causes buffered messages (unprocessed input messages as well as unoffered output messages) to be lost.

当TPL Dataflow不再处理消息并且能保证不再处理消息的时候,就被定义为 "完成态", IDataflow.Completion属性(Task对象)标记了该状态,Task对象的TaskStatus枚举值描述了此Block进入完成态的真实原因

- TaskStatus.RanToCompletion      "成功完成" 在Block中定义的任务

- TaskStatus.Fault                        因未处理的异常  导致"过早的完成"

- TaskStatus.Cancled                   因取消操作  导致 "过早的完成"

  故需要小心处理异常, 一般情况下我们使用try、catch包含所有的执行代码以确保所有的异常都被处理。

  本文作为TPL Dataflow的入门指南,微软技术栈的同事可持续关注这个基于Actor模型的流水线处理组件,处理单体程序中高并发,低延迟场景相当巴适。

作者:JulianHuang

码甲拙见,如有问题请下方留言大胆斧正;码字+Visio制图,均为原创,看官请不吝好评+关注,  ~。。~

本文欢迎转载,请转载页面明显位置注明原作者及原文链接

 

TPL DataFlow .Net 数据流组件,了解一下的更多相关文章

  1. .NET并发编程-TPL Dataflow并行工作流

    本系列学习在.NET中的并发并行编程模式,实战技巧 本小节了解TPL Dataflow并行工作流,在工作中如何利用现成的类库处理数据.旨在通过TDF实现数据流的并行处理. TDF Block 数据流由 ...

  2. SSIS自定义数据流组件开发(血路)

    由于特殊的原因(怎么特殊不解释),需要开发自定义数据流组件处理. 查了很多资料,用了不同的版本,发现各种各样的问题没有找到最终的解决方案. 遇到的问题如下: 用VS2015编译出来的插件,在SSDTB ...

  3. TPL DataFlow初探(一)

    属性TPL Dataflow是微软面向高并发应用而推出的一个类库.借助于异步消息传递与管道,它可以提供比线程池更好的控制,也比手工线程方式具备更好的性能.我们常常可以消息传递,生产-消费模式或Acto ...

  4. 一个使用C#的TPL Dataflow Library的例子:分析文本文件中词频

    博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:一个使用C#的TPL Dataflow Library的例子:分析文本文件中词频.

  5. FluentDataflow - Fluent Style TPL Dataflow

    我的新英文博客文章: FluentDataflow - Fluent Style TPL Dataflow 介绍了本人最新发布的一个开源类库:FluentDataflow--Fluent风格的TPL ...

  6. 微软面向高并发应用而推出的新程序库——TPL Dataflow

    TPL Dataflow库的几个扩展函数 TPL Dataflow是微软面向高并发应用而推出的新程序库.借助于异步消息传递与管道,它可以提供比线程池更好的控制.本身TPL库在DataflowBlock ...

  7. Vue 中数据流组件

    好久不见呀,这两年写了很多很多东西,也学到很多很多东西,没有时常分享是因为大多都是我独自思考.明年我想出去与更多的大神交流,再修筑自己构建的内容. 有时候我会想:我们遇到的问题,碰到的界限,是别人给的 ...

  8. TPL DataFlow初探(二)

    上一篇简单的介绍了TDF提供的一些Block,通过对这些Block配置和组合,可以满足很多的数据处理的场景.这一篇将继续介绍与这些Block配置的相关类,和挖掘一些高级功能. 在一些Block的构造函 ...

  9. .Net Core中利用TPL(任务并行库)构建Pipeline处理Dataflow

    在学习的过程中,看一些一线的技术文档很吃力,而且考虑到国内那些技术牛人英语都不差的,要向他们看齐,所以每天下班都在疯狂地背单词,博客有些日子没有更新了,见谅见谅 什么是TPL? Task Parall ...

随机推荐

  1. Linux技术学习路线图

  2. 一个字体,大小,颜色可定义的自绘静态框控件-XColorStatic 类(比较好看,一共19篇自绘文章)

    翻译来源:https://www.codeproject.com/Articles/5242/XColorStatic-a-colorizing-static-control XColor Stati ...

  3. c#利用IronPython调用python的过程种种问题

    c#利用IronPython调用python的过程种种问题 小菜鸟一枚,最新学习了Python,感觉语言各种简短,各种第三方类库爽歪歪,毕竟之前是从c#转来的,看到Python的request类各种爽 ...

  4. Android零基础入门第88节:Fragment显示和隐藏、绑定和解绑

    在上一期我们学习了FragmentManager和FragmentTransaction的作用,并用案例学习了Fragment的添加.移除和替换,本期一起来学习Fragment显示和隐藏.绑定和解绑. ...

  5. 数据库连接池之_c3p0

    C3p0 1,手动设置参数 @Test public void demo1(){ Connection connection =null; PreparedStatement preparedStat ...

  6. MacOS X编译OpenSceneGraph

    本文主要记录在MacOS X上编译OpenSceneGraph,方便日后查阅.所使用的环境如下: MacOS X 10.10 Yosemite XCode 6.3.2 CMake 3.3.0 Open ...

  7. 深入理解 Win32 PE 文件格式 Matt Pietrek(慢慢体会)

    这篇文章假定你熟悉C++和Win32. 概述 理解可移植可执行文件格式(PE)可以更好地了解操作系统.如果你知道DLL和EXE中都有些什么东西,那么你就是一个知识渊博的程序员.这一系列文章的第一部分, ...

  8. mstsc也要使用/admin参数

    mstsc.exe /admin http://stackoverflow.com/questions/6757232/service-not-responding-error-1053

  9. PHP开发框架 Laravel

    Laravel 是一套简洁.优雅的PHP Web开发框架(PHP Web Framework).它可以让你从面条一样杂乱的代码中解脱出来:它可以帮你构建一个完美的网络APP,而且每行代码都可以简洁.富 ...

  10. kube框架结构-一个小型响应式CSS框架

    当你开始初建一个新的项目时,你可能需要一个不太复杂的基础框架,Kube框架应该是你最好的选择.一个独立的CSS文件,帮助你更简单的创建响应式的的布局设计. Kube Framework包括网格.按钮. ...