Skip to content

Files

Latest commit

678890d · Jan 8, 2022

History

History
1212 lines (907 loc) · 55.8 KB

File metadata and controls

1212 lines (907 loc) · 55.8 KB

九、应用服务

简介

任何做一些至少稍微复杂的事情的应用程序都依赖于一些服务。称它们为共同关心的问题、应用服务、中间件或其他任何东西。这里的挑战是,我们不能只使用这些服务,而不为它们提供一些上下文,即当前的租户。例如,考虑缓存:您可能不希望为特定租户缓存的内容被其他租户访问。在这里,我们将看到一些通过利用 ASP.NET 和。NET 框架已经提供。

控制反转

在整个代码中,我一直使用公共服务定位器来检索服务,而不管用于实际注册它们的控制反转容器是什么,但最终,我们使用的是 Unity。当我们真正注册它们时,我们有几个选择:

  • 注册静态实例
  • 注册类实现
  • 注册注射工厂

对于那些需要上下文的服务(知道当前的租户),注入工厂是我们的朋友,因为这些信息只有在服务被实际请求时才能获得。使用 Unity,我们使用如下代码注册它们:

代码示例 89

          var container = UnityConfig.GetConfiguredContainer();
         container.RegisterType<IContextfulService>(new PerRequestLifetimeManager(), 
               new InjectionFactory(x =>
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     return new ContextfulServiceImpl(tenant.Name);
               }));

值得注意的是:

  • perrequestlifetime manager是一个 Unity 生存期管理器实现,它为注册的组件提供了每个请求的生存期,这意味着组件只有在当前 HTTP 请求的范围内创建,如果它们还没有创建的话。否则,将始终返回同一个实例。一次性组件在请求结束时得到适当处理。
  • 注入工厂接受一个委托,该委托只返回一个预先创建的实例。在这个例子中,我们正在构建一个伪造的服务实现, ContextfulServiceImpl ,它接受一个构造函数参数,即当前租户的名称。

| | 提示:将注入工厂与 PerRequestLifetimeManager 以外的生存期管理器一起使用没有多大意义。 |

| | 注意:正如我之前所说的,您并不局限于 Unity——您可以使用任何您喜欢的 IoC 容器,前提是(为了本书示例的目的)它为公共服务定位器提供了一个适配器。 |

还记得代表租户配置的界面 ITenantConfiguration 吗?如果你这样做了,你就知道它有一个通用的索引集合, Properties 。我们可以实现一个自定义的生存期管理器,以透明的方式将项目存储在当前租户的 Properties 集合中:

代码示例 90

          public sealed class PerTenantLifetimeManager : LifetimeManager
         {
               private readonly Guid id = Guid.NewGuid();
               private static readonly ConcurrentDictionary<String, 
                     ConcurrentDictionary<Guid, Object>> items = 
                     new ConcurrentDictionary<String, 
                           ConcurrentDictionary<Guid, Object>>();

               public override Object GetValue()
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     ConcurrentDictionary<Guid, Object> registrations = null;
                     Object value = null;

                     if (items.TryGetValue(tenant.Name, out registrations))
                     {
                           registrations.TryGetValue(this.id, out value);
                     }

                     return value;
               }

               public override void RemoveValue()
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     ConcurrentDictionary<Guid, Object> registrations = null;

                     if (items.TryGetValue(tenant.Name, out registrations))
                     {
                           Object value;
                           registrations.TryRemove(this.id, out value);
                     }
               }

               public override void SetValue(Object newValue)
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     var registrations = items.GetOrAdd(tenant.Name, 
                           new ConcurrentDictionary<Guid, Object>());
                     registrations[this.id] = newValue;
               }
         }

缓存

缓存是可以显著提高应用程序性能的技术之一,因为它将获取成本可能很高的数据保存在内存中。我们需要能够根据一个密钥存储数据,其中几个租户可以使用同一个密钥。这里的技巧是,在幕后,将提供的密钥与特定于租户的标识符连接起来。例如,典型的缓存服务接口可能是:

代码示例 91

          public interface ICache
         {
               void Remove(String key, String regionName = null);
               Object Get(String key, String regionName = null);
               void Add(String key, Object value, DateTime absoluteExpiration, 
                     String regionName = null);
               void Add(String key, Object value, TimeSpan slidingExpiration, 
                     String regionName = null);
         }

以及使用 ASP.NET 内置缓存的实现:

代码示例 92

          public sealed class AspNetMultitenantCache : ICache, ITenantAwareService
         {
               public static readonly ICache Instance = new AspNetMultitenantCache();

               private AspNetMultitenantCache()
               {
                     //private constructor since this is meant to be used as a singleton
               }

               private String GetTenantKey(String key, String regionName)
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant().Name;
                     key = String.Concat(tenant, ":", regionName, ":", key);
                     return key;
               }

               public void Remove(String key, String regionName = null)
               {
                     HttpContext.Current.Cache.Remove(this.GetTenantKey(key, 
                     regionName));
               }

               public Object Get(String key, String regionName = null)
               {
                     return HttpContext.Current.Cache.Get(this.GetTenantKey(key, 
                     regionName));
               }
                     public void Add(String key, Object value, DateTime absoluteExpiration, 
                     String regionName = null)
               {
                     HttpContext.Current.Cache.Add(this.GetTenantKey(key, regionName),
                     value, null, absoluteExpiration, Cache.NoSlidingExpiration, 
                     CacheItemPriority.Default, null);
               }
                     public void Add(String key, Object value, TimeSpan slidingExpiration, 
                     String regionName = null)
               {
                     HttpContext.Current.Cache.Add(this.GetTenantKey(key, regionName),
                     value, null, Cache.NoAbsoluteExpiration, slidingExpiration, 
                     CacheItemPriority.Default, null);
               }
         }

| | 注意:不要担心它。它只是我用于那些了解当前租户的服务的标记接口。 |

这里没有什么特别的——我们只是用当前租户的 ID 和地区名包装用户提供的密钥。这是作为单例实现的,因为只有一个 ASP.NET 缓存,没有任何状态并且都指向同一个缓存的 AspNetMultitenantCache 的多个实例是没有意义的。这就是 ITenantAwareService 接口所说的:它知道我们在寻址谁,所以每个租户不需要有不同的实例。

| | 注意:注意类名上的> AspNet 前缀。这表明这个班需要 ASP.NET 去工作。 |

注册缓存服务很容易,我们不需要使用注入工厂,因为我们将注册一个单独的:

代码示例 93

          container.RegisterInstance<ICache>(AspNetMultitenantCache.Instance);

配置

我不知道有哪个中型应用程序不需要任何配置。这里的问题是一样的:两个租户可能共享相同的配置密钥,同时他们期望不同的值。让我们为基本配置特性定义一个公共接口:

代码示例 94

          public interface IConfiguration
         {
               Object GetValue(String key);
               void SetValue(String key, Object value);
               Object RemoveValue(String key);
         }

让我们也定义一个多租户实现,它使用每个租户配置文件中的应用程序设置作为后备存储:

代码示例 95

          public class AppSettingsConfiguration : IConfiguration, ITenantAwareService
         {
               public static readonly IConfiguration Instance = 
                     new AppSettingsConfiguration();

               private AppSettingsConfiguration()
               {
               }

               private void Persist(Configuration configuration)
               {
                     configuration.Save();
               }

               private Configuration Configuration
               {
                     get
                     {
                           var tenant =  TenantsConfiguration.GetCurrentTenant();
                           var configMap = new ExeConfigurationFileMap();
                           configMap.ExeConfigFilename =  String.Format(                    AppDomain.CurrentDomain.BaseDirectory, 
                                 tenant.Name, ".config");

                           var configuration = ConfigurationManager
                                 .OpenMappedExeConfiguration(configMap, 
                                      ConfigurationUserLevel.None);

                           return configuration;
                     }
               }

               public Object GetValue(String key)
               {
                     var entry = this.Configuration.AppSettings.Settings[key];
                     return (entry != null) ? entry.Value : null;
               }

               public void SetValue(String key, Object value)
               {
                     if (value == null)
                     {
                           this.RemoveValue(key);
                     }
                     else
                     {
                           var configuration = this.Configuration;
                           configuration.AppSettings.Settings
                                 .Add(key, value.ToString());
                           this.Persist(configuration);
                     }
               }

               public Object RemoveValue(String key)
               {
                     var configuration = this.Configuration;
                     var entry = configuration.AppSettings.Settings[key];

                     if (entry != null)
                     {
                           configuration.AppSettings.Settings.Remove(key);
                           this.Persist(configuration);
                           return entry.Value;
                     }

                     return null;
               }
         }

| | 注意:这个类对于网络和非网络是完全不可知的;它可以用于任何类型的项目。它还实现了 ITenantAwareService,原因和之前一样。 |

有了这个实现,我们每个租户可以有一个文件,比如说 abc.com.config ,或者 xyz.net.config ,语法将与普通的一样。NET 配置文件:

代码示例 96

          <configuration>
                <appSettings>
                      <add key="Key" value="Value"/>
                </appSettings>
          <configuration>

这种方法很好,因为我们甚至可以在运行时更改文件,并且不会导致 ASP.NET 应用程序重新启动,如果我们更改 Web.config 文件,就会发生这种情况。

我们将使用相同的模式进行注册:

代码示例 97

          container.RegisterInstance<IConfiguration>(AppSettingsConfiguration.Instance);

测井

当有这么多好的日志框架可供选择时,实现另一个日志框架会很麻烦,也很愚蠢。对于这本书,我选择了企业库日志应用块 (ELLAB)。您可能希望通过 NuGet 来增加对它的支持:

图 18:安装企业程序库日志应用程序块

ELLAB 提供了一个 API,您可以使用它将日志发送到多个来源,如下图所示:

Follow link to expand image

图 19:企业库日志应用程序块架构

这些来源包括:

现在,我们的要求是将每个租户的输出发送到自己的文件中。例如,租户“**abc.com**”的输出将转到“**abc.com.log**”,等等。但首先,这是我们的基本日志合同:

代码示例 98

          public interface ILog
          {
                      void Write(Object message, Int32 priority, TraceEventType severity, 
                                  Int32 eventId = 0);
          }

我保持简单,但是,无论如何,添加任何你认为合适的辅助方法。

ELLAB 可以记录:

  • 任意的信息
  • 一个字符串类别:我们将它用于租户名称,所以我们不会在 API 中公开它
  • 整数优先级
  • 事件严重性
  • 事件标识符,可选( eventId )

在《电子学习法》的基础上实施该法可以是:

代码示例 99

          public sealed class EntLibLog : ILog, ITenantAwareService
          {
                public static readonly ILog Instance = new EntLibLog();

                private EntLibLog()
                {
                      //private constructor since this is meant to be used as a singleton
                }

               public void Write(Object message, Int32 priority, TraceEventType severity,            Int32 eventId = 0)
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     Logger.Write(message, tenant.Name, priority, eventId, severity);
               }
         }

注册遵循与之前相同的模式:

代码示例 100

          container.RegisterInstance<ILog>(EntLibLog.Instance);

这一次,我们还需要考虑 ELLAB 配置。因为我们需要单独设置每个租户,所以我们必须在启动时运行以下代码:

代码示例 101

          private void CreateLogFactories(IEnumerable<ITenantConfiguration> tenants)
         {
               foreach (var tenant in tenants)
               {
                     try 
                     {
                           var configurationSource = 
                                 new FileConfigurationSource(tenant.Name + 
                                 ".config");
                           var logWriterFactory = new LogWriterFactory(
                                 configurationSource);
                           Logger.SetLogWriter(logWriterFactory.Create());
                     }
                     catch {}
               }
         }

          var tenants = TenantsConfiguration.GetTenants();
         CreateLogFactories(tenants);

| | 提示:围绕初始化代码的 try…catch 块就在那里,这样如果我们不想要特定租户的配置文件,它就不会抛出。 |

TenantsConfiguration.GetTenants方法在“寻找租户”一章中介绍。该代码,特别是FileConfigurationSource类,要求所有租户都将其配置保存在一个单独的文件中,以租户命名( <tenant> .config)。接下来是一个日志配置示例,它使用每周循环的滚动平面文件:

代码示例 102

          <configuration>
                <configSections>
                      <section name="loggingConfiguration" 
                            type="Microsoft.Practices.EnterpriseLibrary.Logging.
          Configuration.LoggingSettings, Microsoft.Practices.EnterpriseLibrary.Logging" />
                </configSections>
                <loggingConfiguration name="loggingConfiguration" tracingEnabled="true"
                      defaultCategory="" logWarningsWhenNoCategoriesMatch="true">
                      <listeners>
                            <add name="Rolling Flat File Trace Listener"
                                  type="Microsoft.Practices.EnterpriseLibrary
          .Logging.TraceListeners.RollingFlatFileTraceListener,
                            Microsoft.Practices.EnterpriseLibrary.Logging"                   listenerDataType="Microsoft.Practices.EnterpriseLibrary
          .Logging.Configuration.RollingFlatFileTraceListenerData,
                            Microsoft.Practices.EnterpriseLibrary.Logging"
                            fileName="abc.com.log"
                            footer="---------------------------"
                            formatter="Text Formatter"
                            header="---------------------------"
                            rollFileExistsBehavior="Increment"
                            rollInterval="Week"
                            timeStampPattern="yyyy-MM-dd hh:mm:ss"
                            traceOutputOptions="LogicalOperationStack, DateTime,
                            Timestamp, ProcessId, ThreadId, Callstack"
                            filter="All" />
                      </listeners>
                      <formatters>
                            <add type="Microsoft.Practices.EnterpriseLibrary.Logging.
          Formatters.TextFormatter,
                            Microsoft.Practices.EnterpriseLibrary.Logging"
                            template="Timestamp: {timestamp}&#xA;
                            Message: {message}&#xA;Category: {category}&#xA;
                            Priority: {priority}&#xA;EventId: {eventid}&#xA;
                            Severity: {severity}&#xA;Title:{title}&#xA;
                            Machine: {machine}&#xA;Process Id: {processId}&#xA;
                            Process Name: {processName}&#xA;"
                            name="Text Formatter" />
                      </formatters>
                      <categorySources>
                            <add switchValue="All" name="General">
                                  <listeners>
                                        <add name=
                                       "Rolling Flat File Trace Listener" />
                                  </listeners>
                            </add>
                      </categorySources>
                      <specialSources>
                            <allEvents switchValue="All" name="All Events">
                                  <listeners>
                                       <add name=
                                       "Rolling Flat File Trace Listener" />
                                  </listeners>
                            </allEvents>
                            <notProcessed switchValue="All"
                                  name="Unprocessed Category">
                                  <listeners>
                                       <add name=
                                       "Rolling Flat File Trace Listener" />
                                  </listeners>
                            </notProcessed>
                            <errors switchValue="All" 
                                  name="Logging Errors &amp; Warnings">
                                  <listeners>
                                       <add name=
                                       "Rolling Flat File Trace Listener" />
                                  </listeners>
                            </errors>
                      </specialSources>
                </loggingConfiguration>
          </configuration>

| | 注:有关企业库日志记录应用程序块的更多信息,请访问企业库网站。要登录数据库,需要安装日志记录应用程序块数据库提供程序 NuGet 软件包。 |

监测

ASP.NET 和提供的经典宣传短片。用于诊断和监控的. NET 应用程序不能很好地处理多租户,即:

在这一章中,我们将研究一些使这些 API 多租户的技术。在此之前,让我们为所有这些不同的 API 定义一个统一的契约:

代码示例 103

          public interface IDiagnostics
         {           Int64 IncrementCounterInstance(String instanceName, Int32 value = 1);
               Guid RaiseWebEvent(Int32 eventCode, String message, Object data, 
                     Int32 eventDetailCode = 0);
               void Trace(Object value, String category);
         }

需要对每种方法进行解释:

  • Trace :写入注册的跟踪侦听器
  • IncrementCounterInstance :增加一个针对当前租户的性能计数器
  • RaiseWebEvent :在 ASP.NET 健康监测基础设施中引发网络事件

在接下来的部分中,我们将更详细地了解每一部分。

追踪

ASP.NET 追踪

ASP.NET 追踪服务对于分析你的 ASP.NET Web Forms 非常有用,它甚至可以帮助你解决一些问题。

在使用跟踪之前,需要通过一个跟踪元素在 Web.config 文件中进行全局启用:

代码示例 104

          <system.web>
                                  <trace enabled="true" localOnly="true" writeToDiagnosticsTrace="true" 
                                  pageOutput="true" traceMode="SortByTime" requestLimit="20"/>
          </system.web>

这个宣言说的是:

  • 描摹的是 enabled
  • 跟踪输出仅呈现给本地用户( localOnly )。
  • 通知标准诊断跟踪侦听器( writeToDiagnosticsTrace )。
  • 输出被发送到每个页面的底部,而不是显示在跟踪处理程序 URL ( pageOutput )上。
  • 跟踪事件按时间戳排序( traceMode )。
  • 最多存储 20 个请求( requestLimit )。

您还需要逐页启用它(默认为禁用):

代码示例 105

          <%@ Page Language="C#" CodeBehind="Default.aspx.cs" Inherits="WebForms.Default" 
          Trace="true" %>

我们不会详细讨论 ASP.NET 追踪。相反,让我们看一个 ASP.NET 网络表单的示例跟踪:

图 20:网页表单的页面跟踪

在 MVC 中,唯一的区别就是控制树表是空的,这是显而易见的,一旦想起来。

该轨迹显示在页面本身上,如pageOutput所示;另一个选项是让跟踪只显示在跟踪处理程序页面Trace.axd上,而不显示该页面。无论哪种方式,输出都是一样的。

跟踪条目对应于服务器处理的请求。我们可以通过调用 TraceContext 类中的一个跟踪方法,将我们自己的跟踪消息添加到条目中,方便地作为页面使用。在网页表单中追踪:

代码示例 106

          protected override void OnLoad(EventArgs e)
         {
               this.Trace.Write("On Page.Load");
               base.OnLoad(e);
         }

或者作为 Trace 类的静态方法(对于 MVC 或者一般来说),它甚至有一个用于传递条件的重载:

代码示例 107

          public ActionResult Index()
         {
               Trace.WriteLine("Before presenting a view");
               var tenant = TenantsConfiguration.GetCurrentTenant();
               Trace.WriteLineIf(tenant.Name != "abc.com", "Not abc.com");
               return this.View();
         }

现在,跟踪提供程序保留了许多跟踪,最大值由requestLimit指定—默认值为 10,最大值为 10,000。这意味着对我们所有租户的请求将以相同的方式处理,因此如果我们转到Trace.axd网址,我们无法知道该请求是针对哪个租户的。但是如果您仔细查看图 20 中跟踪信息表的消息列,您会注意到一个前缀,该前缀对应于发出请求的租户。为此,我们需要在 Web.config 文件的 system.diagnostics 部分注册一个自定义诊断侦听器:

代码示例 108

          <system.diagnostics>
                <trace autoflush="true">
                      <listeners>
                            <add name="MultitenantTrace" 
                                  type="WebForms.MultitenantTraceListener, 
          WebForms" />
                      </listeners>
                </trace>
          </system.diagnostics>

MultitenantTraceListener 的代码如下:

代码示例 109

          public sealed class MultitenantTraceListener : WebPageTraceListener
         {
               private static readonly MethodInfo GetDataMethod = typeof(TraceContext)
               .GetMethod("GetData", BindingFlags.NonPublic | BindingFlags.Instance);

               public override void WriteLine(String message, String category)
               {
                     var ds = GetDataMethod.Invoke(HttpContext.Current.Trace, null)
                            as DataSet;
                     var dt = ds.Tables["Trace_Trace_Information"];
                     var dr = dt.Rows[dt.Rows.Count - 1];
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     dr["Trace_Message"] = String.Concat(tenant.Name, ": ", 
                           dr["Trace_Message"]);

                     base.WriteLine(message, category);
               }
         }

这是通过一点反射魔法,获取对包含最后一个跟踪的当前数据集的引用,并在最后一个跟踪(当前请求的跟踪)上添加一个前缀,即当前租户的名称(例如,abc.com)。

Web.config 不是唯一可以注册诊断侦听器的方式;还有代码: Trace。听众。使用这种机制,您可以添加自定义侦听器,当发出跟踪调用时,这些侦听器将执行各种操作:

代码示例 110

          protected void Application_Start()
         {
               //unconditionally adding a listener
               Trace.Listeners.Add(new CustomTraceListener());
         }

          protected void Application_BeginRequest()
         {
               //conditionally adding a listener
               var tenant = TenantsConfiguration.GetCurrentTenant();
               if (tenant.Name == "abc.com")
               {    
                     Trace.Listeners.Add(new AbcComTraceListener());
               }
         }

跟踪提供程序

中存在其他跟踪提供程序。NET 基类库:

所有这些都可以在系统诊断追踪中注册。听众。我们将有一个用于 Trace 静态方法的包装类,其中我们实现了IDiagnostics接口的Trace方法:

代码示例 111

          public sealed class MultitenantDiagnostics : IDiagnostics, ITenantAwareService
         {
               public void Trace(Object value, String category)
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     System.Diagnostics.Trace.AutoFlush = true;
                     System.Diagnostics.Trace.WriteLine(String.Concat(tenant.Name,
                           ": ", value), category);
               }
         }

性能计数器

性能计数器是一项 Windows 功能,可用于提供对正在运行的应用程序和服务的洞察。甚至有可能让 Windows 自动对性能计数器的值超过给定阈值的情况做出反应。如果我们愿意,我们可以使用性能计数器向相关方传达应用程序状态的各个方面,其中该状态由整数值组成。

性能计数器以下列方式组织:

  • 类别:名称
  • 计数器:一个名称和一个类型(让我们暂时忘记类型)
  • 实例:给定类型的名称和值(我们假设是一个长整数)

图 21:性能计数器的基本概念

ITenantConfiguration界面中,我们添加了一个Counters属性,实现后用于自动创建性能计数器实例。我们将遵循这种方法:

表 6:映射多租户的性能计数器概念

| 概念 | 内容 | | 种类 | “房客” | | 计数器 | 租户名称(如abc.comxyz.net | | 情况 | 一个租户特有的名字,来自 ITenantConfiguration.Counters |

TenantsConfiguration类中自动创建每个性能计数器和实例的代码如下:

代码示例 112

          public sealed class TenantsConfiguration : IDiagnostics, ITenantAwareService
         {
               private static void CreatePerformanceCounters(
                     IEnumerable<ITenantConfiguration> tenants)
               {
                     if (PerformanceCounterCategory.Exists("Tenants"))
                     {
                           PerformanceCounterCategory.Delete("Tenants");
                     }

                     var counterCreationDataCollection = 
                           new CounterCreationDataCollection(
                                 tenants.Select(tenant => 
                                      new CounterCreationData(
                                            tenant.Name,
                                            String.Empty,
                                      PerformanceCounterType.NumberOfItems32))
                                 .ToArray());

                     var category = PerformanceCounterCategory.Create("Tenants",
                           "Tenants performance counters",
                           PerformanceCounterCategoryType.MultiInstance,
                           counterCreationDataCollection);

                     foreach (var tenant in tenants)
                     {
                           foreach (var instance in tenant.Counters)
                           {
                                 var counter = new PerformanceCounter(
                                       category.CategoryName,
                                      tenant.Name,
                                      String.Concat(tenant.Name,
                                            ": ",
                                            instance.InstanceName), false);
                           }
                     }
               }
         }

另一方面,递增性能计数器实例的代码是在IDiagnostics接口(IncrementCounterInstance)中定义的,我们可以将其实现为:

代码示例 113

          public sealed class MultitenantDiagnostics : IDiagnostics, ITenantAwareService
         {
               public Int64 IncrementCounterInstance(String instanceName,
                     Int32 value = 1)
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     using (var pc = new PerformanceCounter("Tenants", tenant.Name,
                           String.Concat(tenant.Name, ":", instanceName), false))
                     {
                           pc.RawValue += value;
                           return pc.RawValue;
                     }
               }
         }

当应用运行时,我们可以通过性能监视器应用实时观察计数器实例的值:

图 22:显示性能计数器的性能监视器应用程序

我们只需要在显示器上增加一些计数器;我们的将在租户 - <租户名称> - <实例名称> 下提供:

图 23:添加性能计数器

在本例中,可以感觉到两个租户,即abc.comxyz.net,具有相同名称的计数器实例,但情况并非如此。

还有其他内置的 ASP。NET 相关的性能计数器,可用于监视应用程序。例如,在性能监视器中,添加一个新的计数器并选择ASP.NET 应用程序:

图 24:ASP.NET 应用程序性能计数器

您会注意到您有几个实例,每个运行的站点一个。这些实例会自动命名,但每个数字都可以追溯到一个应用程序。例如,如果您正在使用 IIS Express,请打开 ApplicationHost.config 文件(C:\ Users **\ Documents \ IIS Express \ Config)并转到站点部分,您会发现类似这样的内容(当然,在这种情况下,您正在为租户服务abc.comxyz.net):

代码示例 114

          <sites>
                 <site name="abc.com" id="1">
                       <application path="/" applicationPool="Clr4IntegratedAppPool">
                              <virtualDirectory path="/" 
                                     physicalPath="C:\InetPub\Multitenant" />
                       </application>
                        <bindings>
                              <binding protocol="http" bindingInformation="*:80:abc.com" />
                       </bindings>
                 </site>
                 <site name="xyz.net" id="2">
                       <application path="/" applicationPool="Clr4IntegratedAppPool">
                              <virtualDirectory path="/" 
                                     physicalPath="C:\InetPub\Multitenant" />
                       </application>
                       <bindings>
                              <binding protocol="http" bindingInformation="*:80:xyz.net" />
                       </bindings>
                 </site>
          </sites>

或者使用 IIS:

图 25:为 abc.com 创建一个单独的站点

图 26:为 xyz.net 创建一个单独的站点

为了有更多的控制权,我们把两个站点分开了;这是为了更精确地控制每一个。不过,代码库保持不变。

在每个实例的名称(**_LM_W3SVC_<*n>*_ROOT**)中,*<n>*将匹配这些数字中的一个。

另一方面,如果您想使用完整的 IIS,则 appcmd 命令会给您以下信息:

代码示例 115

          C:\Windows\System32\inetsrv>appcmd list site
          SITE "abc.com" (id:1,bindings:http/abc.com:80:,state:Started)
          SITE "xyz.net" (id:2,bindings:http/xyz.net:80:,state:Started)

          C:\Windows\System32\inetsrv>appcmd list apppool
          APPPOOL "DefaultAppPool" (MgdVersion:v4.0,MgdMode:Integrated,state:Started)
          APPPOOL "Classic .NET AppPool" (MgdVersion:v2.0,MgdMode:Classic,state:Started)
          APPPOOL ".NET v2.0 Classic" (MgdVersion:v2.0,MgdMode:Classic,state:Started)
          APPPOOL ".NET v2.0" (MgdVersion:v2.0,MgdMode:Integrated,state:Started)
          APPPOOL ".NET v4.5 Classic" (MgdVersion:v4.0,MgdMode:Classic,state:Started)
          APPPOOL ".NET v4.5" (MgdVersion:v4.0,MgdMode:Integrated,state:Started)
          APPPOOL "abc.com" (MgdVersion:v4.0,MgdMode:Integrated,state:Started)
          APPPOOL "xyz.net" (MgdVersion:v4.0,MgdMode:Integrated,state:Started)

我列出了所有网站(第一个列表),然后是所有应用程序池。

健康监测

ASP.NET 的健康监控功能允许配置规则来响应在 ASP.NET 应用程序中发生的某些事件。它使用提供的模型来决定当满足规则条件时该做什么。例如,当一分钟内登录尝试失败的次数达到三次时,发送通知邮件。因此,非常简单,健康监测功能允许我们:

  • 注册一些将执行操作的提供程序
  • 添加绑定到特定提供程序的命名规则
  • 将事件代码映射到创建的规则

ASP.NET API 在内部引发了许多开箱即用的事件,但我们也可以定义自己的事件:

ASP.NET Health Monitoring

图 27:包含的健康监控事件

事件分为类和子类。处理身份验证事件有特定的类( WebAuditEventWebFailureAuditEventWebSuccessAuditEvent )、请求( WebRequestEventWebRequestErrorEvent )和应用程序生存期事件(webapplicationlifetime event)、查看状态失败事件(WebViewStateFailureEvent)等。这些事件(和类)中的每一个都被分配一个唯一的数字标识符,该标识符列在网络事件代码字段中。自定义事件应以网络事件代码中的值开始。webextendbackbase+1。

当然,还包括许多提供者,用于在满足规则时执行操作。我们也可以实现自己的,通过继承 WebEventProvider 或者它的一个子类:

ASP .NET Health Monitoring Provider Class Diagram

图 28:包含的健康监控提供商

包括的提供商涵盖了许多典型场景:

在我们开始编写一些规则之前,我们先来看看我们的自定义提供程序, MultitenantEventProvider ,及其相关类 MultitenantWebEventMultitenantEventArgs :

代码示例 116

          public sealed class MultitenantEventProvider : WebEventProvider
         {
               private static readonly IDictionary<String, MultiTenantEventProvider> 
                     providers = 
                     new ConcurrentDictionary<String, MultiTenantEventProvider>();

               private const String TenantKey = "tenant";

               public String Tenant { get; private set; }

               public event EventHandler<MultiTenantEventArgs> Event;

               public static void RegisterEvent(String tenantId,
                     EventHandler<MultiTenantEventArgs> handler)
               {
                     var provider = FindProvider(tenantId);
                     if (provider != null)
                     {
                           provider.Event += handler;
                     }
               }

               public static MultiTenantEventProvider FindProvider(String tenantId)
               {
                     var provider = null as MultiTenantEventProvider;

                     providers.TryGetValue(tenantId, out provider);

                     return provider;
               }

               public override void Initialize(String name, NameValueCollection config)
               {
                     var tenant = config.Get(TenantKey);

                     if (String.IsNullOrWhiteSpace(tenant))
                     {
                           throw new InvalidOperationException(
                                 "Missing tenant name.");
                     }

                     config.Remove(TenantKey);

                     this.Tenant = tenant;
                     providers[tenant] = this;

                     base.Initialize(name, config);
               }

               public override void Flush()
               {
               }

               public override void ProcessEvent(WebBaseEvent raisedEvent)
               {
                     var evt = raisedEvent as MultitenantWebEvent;
                     if (evt != null)
                     {
                           var tenant = TenantsConfiguration.GetCurrentTenant();

                           if (tenant.Name == evt.Tenant)
                           {
                                 var handler = this.Event;
                                 if (handler != null)
                                 {                                                          handler(this, 
                                            new MultitenantEventArgs(
                                                  this, evt));
                                 }
                           }
                     }
               }

               public override void Shutdown()
               {
               }
         }

         [Serializable]
          public sealed class MultitenantEventArgs : EventArgs
         {
               public MultitenantEventArgs(MultitenantEventProvider provider,
                     MultitenantWebEvent evt)
               {
                     this.Provider = provider;
                     this.Event = evt;
               }

               public MultitenantEventProvider Provider { get; private set; }
               public MultitenantWebEvent Event { get; private set; }
         }

          public class MultitenantWebEvent : WebBaseEvent
         {
               public MultitenantWebEvent(String message, Object eventSource,               Int32 eventCode, Object data) : 
                           this(message, eventSource, eventCode, data, 0) {}

               public MultitenantWebEvent(String message, Object eventSource, 
                     Int32 eventCode, Object data, Int32 eventDetailCode) : 
                           base(message, eventSource, eventCode, eventDetailCode)
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     this.Tenant = tenant.Name;
                     this.Data = data;
               }

               public String Tenant { get; private set; }
               public Object Data { get; private set; }
         }

因此,我们有一个提供者类(MultitenantEventProvider)、一个提供者事件参数(MultitenantEventArgs)和一个提供者事件(MultitenantWebEvent)。我们总是可以通过调用静态方法FindProvider找到为当前租户注册的提供程序,并且从这个提供程序中,我们可以将事件处理程序注册到Event事件。稍后,我们将了解如何连接,但首先,这里有一个IDiagnostics接口RaiseWebEvent方法的可能实现:

代码示例 117

          public sealed class MultitenantDiagnostics : IDiagnostics, ITenantAwareService
         {
               public Guid RaiseWebEvent(Int32 eventCode, String message, Object data, 
                     Int32 eventDetailCode = 0)
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     var evt = new MultitenantWebEvent(message, tenant, eventCode,
                           data, eventDetailCode);
                     evt.Raise();
                     return evt.EventID;
               }
         }

现在,让我们添加一些规则,看看事情的发展。首先,我们需要向 Web.config 文件的健康监控部分添加一些元素:

代码示例 118

          <configuration>
               <system.web>
                     <healthMonitoring enabled="true" heartbeatInterval="0">
                           <providers>
                                 <add name="abc.com"                                                                type="MultitenantEventProvider, MyAssembly"
                                      tenant="abc.com" />
                                 <add name="xyz.net"
                                      type="MultiTenantEventProvider, MyAssembly"
                                      tenant="xyz.net" />
                           </providers>
                           <rules>
                                 <add name="abc.com Custom Event"
                                      eventName="abc.com Custom Event"
                                      provider="abc.com"
                                      minInterval="00:01:00"
                                      minInstances="1" maxLimit="1" />
                                 <add name="xyz.net Custom Event"
                                      eventName="xyz.net Custom Event"
                                      provider="xyz.net"
                                       minInterval="00:01:00"
                                      minInstances="2" maxLimit="2" />
                           </rules>
                           <eventMappings>
                                 <add name="abc.com Custom Event"
                                      startEventCode="100001"
                                      endEventCode="100001"
                                      type="MultiTenantWebEvent, MyAssembly" />
                                 <add name="xyz.net Custom Event"
                                      startEventCode="200001"
                                      endEventCode="200001"
                                      type="MultiTenantWebEvent, MyAssembly" />
                           </eventMappings>
                     </healthMonitoring>
               </system.web>
         </configuration>

所以,我们这里有:

  • 注册在同一类别(MultitenantEventProvider)的两个提供商,每个租户一个,其属性tenant说明了这一点
  • 两个规则,对于给定的provider,当一个命名事件(eventName)在一定时间内(minInterval)被引发多次(minInstancesmaxLimit)时,每个规则都会引发一个自定义事件;
  • 两个事件映射(eventMappings),将事件 id 间隔(startEventCodeendEventCode)转换为某个事件类(type)。

我们还需要为正确租户的MultitenantEventProvider类添加一个事件处理程序,可能在应用程序 _ 开始中:

代码示例 119

          protected void Application_Start()
         {
               MultiTenantEventProvider.RegisterEvent("abc.com", (s, e) =>
               {
                     //do something when the event is raised
               });
         }

因此,最后,如果在任何规则中指定的时间段内引发了许多 web 事件,就会引发一个事件,希望会发生一些事情。

分析

不用说,谷歌分析事实上的标准,当分析你网站的流量时。据我所知,没有其他服务能提供同样多的信息和功能,所以使用它是有意义的,对于多租户网站也是如此。

如果你没有谷歌分析账户,那就去创建一个,这个过程非常简单(是的,你确实需要一个谷歌账户)。

有了之后,进入管理页面,创建尽可能多的房产(T2 房产下拉列表):

图 29:创建谷歌分析属性

每个租户将被分配一个唯一的密钥,形式为UA-<*nnnnnnnn*-*n>*,其中 n 是数字。如果您按照配置服务的前一个主题进行操作,您的网站的根目录下会有一个每个租户的配置文件( abc.com.config ,您可以在这里存储该密钥:

代码示例 120

          <configuration>
                <appSettings>
                      <add key="GoogleAnalyticsKey" value="UA-nnnnnnnn-n"/>
                </appSettings>
          </configuration>

当然,一定要用正确的键替换UA-<*nnnnnnnn*-*n>*

现在,我们需要在提到这个密钥和租户名称的页面上添加一些 JavaScript。这个脚本是实际调用谷歌分析应用编程接口的地方,它看起来像这样:

代码示例 121

          <script type="text/javascript">// <![CDATA[
               (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
                     (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
                     m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
                     })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
                     ga('create', '{0}', 'auto');
                     ga('send', 'pageview');
          // ]]></script>

如果你想知道这是从哪里来的,谷歌分析的管理页面已经准备好了这些信息供你选择——只需点击追踪信息找到你选择的房产(租户)。

您可能已经注意到了 {0} 令牌—这是一个标准。NET 字符串格式占位符。意思是需要用实际租户密钥(UA-*nnnnnnnn*-*n*)替换。每个租户都将该密钥存储在其配置中,例如,在密钥GoogleAnalyticsKey下。这样我们就可以通过刚才介绍的IConfiguration界面进行检索。

如果我们将使用 ASP.NET 网络表单,一个不错的包装可能是一个控件:

代码示例 122

          public sealed class GoogleAnalytics : Control
         {
               private const String Script = "<script type=\"text/javascript\">// <![CDATA[" +
               "(function(i,s,o,g,r,a,m){{i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){{" +
               "  (i[r].q=i[r].q||[]).push(arguments)}},i[r].l=1*new Date();a=s.createElement(o)," +
               "  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)" +
               "  }})(window,document,'script','//www.google-analytics.com/analytics.js','ga');" +
                     "  ga('create', '{0}', 'auto');" +
                     "  ga('send', 'pageview');" +
                     "// ]]></script>";

               protected override void Render(HtmlTextWriter writer)
               {
                     var config = ServiceLocator.Current
                           .GetInstance<IConfiguration>();
                     var key = config.GetValue("GoogleAnalyticsKey");

                     writer.Write(Script, key);
               }
         }

以下是页面或母版页上的示例声明:

代码示例 123

          <%@ Register Assembly="Multitenancy.WebForms" Namespace="Multitenancy.WebForms" 
          TagPrefix="mt" %>

          <mt:GoogleAnalytics runat="server" />

否则,对于 MVC 来说,正确的地方应该是对 HtmlHelper 的扩展方法:

代码示例 124

          public static class HtmlHelperExtensions
         {           private const String Script = "<script type=\"text/javascript\">// <![CDATA[" +            "(function(i,s,o,g,r,a,m){{i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){{" +
               "  (i[r].q=i[r].q||[]).push(arguments)}},i[r].l=1*new Date();a=s.createElement(o)," +
               "  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)" +
               "  }})(window,document,'script','//www.google-analytics.com/analytics.js','ga');" +
               "  ga('create', '{0}', 'auto');" +
              "  ga('send', 'pageview');" +
               "// ]]></script>";

               public static void GoogleAnalytics(this HtmlHelper html)
               {
                     var config = ServiceLocator.Current
                           .GetInstance<IConfiguration>();
                     var key = config.GetValue("GoogleAnalyticsKey");
                     html.Raw(String.Format(Script, key));
               }
         }

然后,在本地或共享视图中,它将被这样调用:

代码示例 125

          @Html.GoogleAnalytics()

就这样!每个租户将在谷歌分析上获得自己的物业页面,你可以开始监控他们。