代码改变世界

《WF编程》系列之44 - 承载工作流:跟踪服务 Tracking Service

2008-09-10 09:07  Windie Chai  阅读(4370)  评论(16编辑  收藏  举报
Windows Workflow 提供了可扩展可收缩的跟踪功能来捕捉和记录工作流的执行信息。对于接收到的工作流信息,跟踪服务会使用跟踪配置(tracking profile)来进行筛选。WF Runtime可以发送许多信息,包括工作流事件、活动状态更改、规则判断和我们的自定义检测数据(custom instrumentation data)。跟踪服务有能力决定如何处理接收到的数据,可以把它们记录到日志文件里,也可以保存到数据库中。跟踪服务会参与到工作流Runtime中来,以确保它记录的信息是一致并且持久的。
晕了吗?在Windows Workflow中有两种跟踪:Trace和Tracking。虽然这两个它们都可以用来曝露工作流Runtime内部重要事件的细节,但Tracking信息是通过序列化API曝露给Windows Workflow的。在本节,我们将会看到Tracking信息是如何通过WF提供的API保存在数据库里的。
跟踪信息对系统管理员来说像是一个不错的功能,除了可以用来分析资源使用率之外,还可以才想到跟踪信息在大的业务系统中的作用。比如,公司可以用记录的跟踪信息来统计发票数量、或者计算结束交易所需的平均时间。而我们就可以利用跟踪信息来发现业务进程的不足并加以改善。

6.4.1 跟踪类

所有的跟踪服务都继承自TrackingService基类。这个类定义了运行跟踪配置和跟踪信道的API。上面曾提到,跟踪配置定义并过滤了我们需要从Runtime接收的信息的类型。而跟踪信道则是则是连接工作流Runtime和跟踪服务的通信管道。Runtime会向跟踪服务索取一个基于跟踪配置的跟踪信道。一旦Runtime获得了开放的信道,它就会通过这个信道给跟踪服务发送信息。我们在编写自定义跟踪服务的同时也需要提供相应的跟踪信道。
Windows Workflow提供了一个现成的跟踪服务的实现:SqlTrackingService类。SqlTrackingService将接收到的跟踪信息和跟踪配置储存在SQL Server数据库中。
 
和SQL持久化服务一样,使用SQL跟踪服务也需要执行架构脚本来创建必须的数据库结构。我们也可以在SQL持久化数据库中执行SQL跟踪服务的架构脚本。创建跟踪架构的脚本位于和持久化脚本相同的位置(经典的C:\WINDOWS\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL)。脚本的文件名是Tracking_Schema.sql和Tracking_Logic.sql。我们必须先执行架构脚本,然后再执行逻辑脚本。下图演示了如何使用命令行工具来执行脚本:
 

6.4.2 跟踪配置

我们可以通过编程或者应用程序配置文件来配置跟踪服务。下面的配置文件将会按照默认的参数加载SQL跟踪服务: 
<?xml version="1.0" encoding="utf-8" ?>
 
<configuration>
   
<configSections>
   
<section name="WorkflowWithTracking"
   type
="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
   
</configSections>
   
<WorkflowWithTracking>
   
<CommonParameters>
   
<add name="ConnectionString"
   value
="Data Source=.;Initial Catalog=c6ps;Integrated Security=True"/>
   
</CommonParameters>
   
<Services>
   
<add type="System.Workflow.Runtime.Tracking.SqlTrackingService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35"/>
   
</Services>
   
</WorkflowWithTracking>
 
</configuration>

下表列出了可以传递给服务的参数: 
 参数名称  描述
ConnectionString 跟踪数据库的连接字符串。
 EnableRetries 当值为true时,如果连接跟踪数据库失败,服务就会尝试重新连接(最多尝试20次),默认值是false。
IsTransactional 当值为true时,服务会像持久化服务那样可以参与到事务中,默认值是true。
PartitionOnCompletion 为了更长的运行时间和更高的可伸缩性,分区功能(partitioning feature)会定期创建一系列新的数据表来存储完成的工作流实例。默认值是false。
ProfileChangeCheckInterval 服务会缓存跟踪配置,但也会定期检查跟踪配置是否发生变化。默认的检查时间间隔是1分钟。
UseDefaultProfile 当值为true时,会在没有定义任何跟踪配置的情况下返回默认跟踪配置,默认值是true。

6.4.3 运行跟踪服务

在配置文件中配置了跟踪服务之后,我们不需要对Runtime做什么特别的改动,下面是创建Runtime并运行工作流的代码:
static void Main(string[] args)
 {
   
using (WorkflowRuntime runtime = new WorkflowRuntime("WorkflowWithTracking"))
   
using (AutoResetEvent reset = new AutoResetEvent(false))
   {
   runtime.WorkflowCompleted 
+= delegate { reset.Set(); };
   runtime.WorkflowTerminated 
+= delegate { reset.Set(); };
   WorkflowInstance instance;
   instance 
= runtime.CreateWorkflow(typeof(WorkflowWithTrackingService));
   instance.Start();
   reset.WaitOne();
   DumpTrackingEvents(instance.InstanceId);
   }
 }
当程序运行结束后,就会有大量的信息被记录在跟踪数据库中的多个表中。我们可以使用一些查询语句去获取跟踪数据,也可以查看随跟踪架构一起安装到数据库中的许多视图。当然,我们也可以以编程的方式通过对象模型来查询跟踪信息。下图演示了我们可以使用的类: 
 
SqlTrackingWorkflowInstance类是我们访问指定工作流实例的跟踪信息的入口。它的ActivityEvents属性会返回一个ActivityTrackingRecord对象集合。同样的,WorkflowEvents属性会返回WorkflowTrackingRecord对象集合,UserEvents属性会返回UserTrackingRecord对象集合(UserEvents就是我们可以自定义的事件)。而且需要注意的是这些信息中还包含了时间戳、参数和状态代码。SqlTrackingWorkflowInstance类还包含了一个WorkflowDefinition属性,可以返回一个工作流的XAML定义,这个功能在审核使用了动态更新或者为每个客户端进行了定制的工作流时非常有用。
下面的代码使用了这些类来获取一些跟踪信息。通过传递连接字符串和工作流实例ID,SqlTrackingQuery类可以返回一个SqlTrackingWorkflowInstance对象,我们可以通过这个对象来访问该工作流实例的所有记录。连接字符串可以从应用程序配置文件中获取,而实例ID则作为参数来指定。
public static void DumpTrackingEvents(Guid instanceID)
 {
   WorkflowRuntimeSection config;
   config 
= ConfigurationManager.GetSection("WorkflowWithTracking"as WorkflowRuntimeSection;
   SqlTrackingQuery query 
= new SqlTrackingQuery();
   query.ConnectionString 
= config.CommonParameters["ConnectionString"].Value;
   SqlTrackingWorkflowInstance trackingInstace;
   query.TryGetWorkflow(instanceID, 
out trackingInstace);
   
if (trackingInstace != null)
   {
   Console.WriteLine(
"Tracking Information for {0}", instanceID);
   Console.WriteLine(
" Workflow Events");
   
foreach (WorkflowTrackingRecord r in trackingInstace.WorkflowEvents)
   {
   Console.WriteLine(
" Date: {0}, Status: {1}",
   r.EventDateTime, r.TrackingWorkflowEvent);
   }
   Console.WriteLine(
" Activity Events");
   
foreach (ActivityTrackingRecord r in trackingInstace.ActivityEvents)
   {
   Console.WriteLine(
" Activity: {0} Date: {1} Status: {2}",
   r.QualifiedName, r.EventDateTime, r.ExecutionStatus);
   }
   }
 }
下图演示了这段代码的输出结果:
 
运行简单的工作流都会生成大量的跟踪信息(上个示例中我们看到的只是其中一小部分而已)。SQL跟踪服务提供了一个默认的跟踪配置来处理Runtime生成的所有信息。如果我们只想跟踪特定的信息,就需要自定义一个跟踪配置。

6.4.3.1 跟踪配置

当工作流Runtime创建了新的工作流实例后,它会调用每个跟踪服务的TryGetProfile方法,并将工作流实例作为参数传递给这个方法。如果有针对工作流类型配置过的跟踪配置,TryGetProfile方法就会通过输出参数返回TrackingProfile类型的跟踪配置。Runtime过滤了跟踪信息,然后使用跟踪配置中定义的跟踪点来把它们发送到服务。下图演示了创建跟踪配置的相关类: 
 
如果我们不想跟踪工作流内部的活动,而只是想跟踪工作流自身的信息,那么就需要定义和创建一个新的TrackingProfile对象并为它的WorkflowTrackPoints添加数据。而ActivityTrackPoints和UserTrackPoints属性将会被留空。 
TrackingProfile profile = new TrackingProfile();
 profile.Version 
= new Version("1.0.0");
 WorkflowTrackPoint workflowTrackPoint 
= new WorkflowTrackPoint();
 Array statuses 
= Enum.GetValues(typeof(TrackingWorkflowEvent));
 
foreach (TrackingWorkflowEvent status in statuses)
 {
   workflowTrackPoint.MatchingLocation.Events.Add(status);
 }
 profile.WorkflowTrackPoints.Add(workflowTrackPoint);
 
string profileAsXml = SerializeProfile(profile);
 UpdateTrackingProfile(profileAsXml);
跟踪配置还需要一个版本号。跟踪服务会将跟踪配置缓存起来以避免Runtime每次索取跟踪配置时都要重新获取。如果我们更新了配置,我们就需要改变它的版本号,以便让跟踪服务知道它更新了。
上面的代码在WorkflowTrackPoints集合中添加了我们想要跟踪的工作流事件。通过使用对TrackingWorkflowEvent使用Enum.GetValues,我们可以获取所有可能发生的事件,包括Started、Comleted、Persisted和Terminated等等。

创建了TrackingProfile对象之后,还需要在跟踪服务里更新跟踪配置。更新过程的第一步是将配置对象序列化为XML。TrackingProfileSerializer将会为我们执行序列化动作。

private static string SerializeProfile(TrackingProfile profile)
  {
    TrackingProfileSerializer serializer;
    serializer 
= new TrackingProfileSerializer();
    StringWriter writer 
= new StringWriter(new StringBuilder());
    serializer.Serialize(writer, profile);
    
return writer.ToString();
  }
SQL跟踪服务会在TrackingProfile表中以XML的形式存储跟踪配置(默认跟踪配置保存在DefaultTrackingProfile表中)。在这张表中更新或插入数据的最好方式是使用UpdateTrackingProfile存储过程。当我们添加了新的跟踪配置,我们必须将这个配置和一个工作流类型做关联。下面的代码演示了如何将跟踪配置关联到SimpleWorkflow工作流: 

 

private static void UpdateTrackingProfile(string profileAsXml)
 {
   WorkflowRuntimeSection config;
   config 
= ConfigurationManager.GetSection("WorkflowWithTracking")
   
as WorkflowRuntimeSection;
   
using (SqlConnection connection = new SqlConnection())
   {
   connection.ConnectionString 
= config.CommonParameters["ConnectionString"].Value;
   SqlCommand command 
= new SqlCommand();
   command.Connection 
= connection;
   command.CommandType 
= CommandType.StoredProcedure;
   command.CommandText 
= "dbo.UpdateTrackingProfile";
   command.Parameters.Add(
new SqlParameter("@TypeFullName",
   
typeof(WorkflowWithTrackingService).ToString()));
   command.Parameters.Add(
new SqlParameter("@AssemblyFullName",
   
typeof(WorkflowWithTrackingService).Assembly.FullName));
   command.Parameters.Add(
new SqlParameter("@Version",
   
"1.0.1"));
   command.Parameters.Add(
new SqlParameter("@TrackingProfileXml"
   profileAsXml));
   connection.Open();
   command.ExecuteNonQuery();
   }
 }
UpdateTrackingProfile存储过程包含四个参数。@TypeFullName参数需要和跟踪配置相关联的工作流类型的完整名称(包含命名空间),同样的,@AssemblyFullName参数也需要包含该工作流定义的程序集的完整名称。@Version参数应该指定为跟踪配置的版本号,@TrackingProfileXml则包含用于表示TrackingProfile对象的XML。
在运行程序后,我们可以通过数据库中的跟踪配置来记录不同的信息。其它工作流则仍然使用默认跟踪配置并记录所有事件。下图演示了示例运行的输出结果,可以看到跟踪服务记录了工作流事件,但却没有记录任何活动事件。
 

6.4.4 数据维护

SQL跟踪服务提供了一个分区功能来将跟踪信息从跟踪数据表的主数据集(primary data set)移动到分区数据集(partitioned data set)中。这个功能帮助我们管理这些不断增长的跟踪数据。管理员可以移动并归档旧的跟踪信息,并且可以确保当前正在使用的数据分区表永远不会变得过分庞大。
当分区执行时,会为每一段已经过去了的分区时间间隔创建新数据集的数据表。SetPartionInterval存储过程可以配置分区时间间隔。默认的时间间隔是一个月,其它可选的值包括一天、一周和一年。分区功能创建的数据表的名称里会包含分区时间。执行分区动作有两种方式:自动分区和手工分区。自动分区可以通过将SQL跟踪服务的PartitionOnCompletion参数配置为true来启用。自动分区会在工作流执行完之后马上将跟踪信息移动到新的分区。自动分区适用于那些从不休止的应用程序,但因为每次当工作流完成时都需要记录跟踪信息到分区数据集中,所以会增加一些开销。
我们还可以通过运行PartitionCompletedWorkflowInstances存储过程来使用手工分区。这个存储过程会将已经完成的工作流的跟踪记录移动到分区表中。手工分区适用于那些总会有一段停机时间的应用程序,这样我们就可以选择在非繁忙时间来执行手工分区。