Web 请求序列可视化

分析系统

让我们构建一个我们想要分析其请求的系统。您可以从GitHub获取其完整代码。

我的系统将包含几个服务(Service1Service2Service3ExternalService):

这些ServiceN服务是我系统的组件。他们在我的控制之下。他们做一些工作,在日志中写一些东西,向其他服务发出请求。他们实际上在做什么对我们来说并不重要。以下是此类服务的典型示例:

C#

但是除了我的服务之外,还有外部服务。简而言之,这些服务不会向我们的日志系统写入任何内容。它可以是任何东西:邮件、数据库、授权、客户端 webhook... 这种类型的服务在这里用ExternalService.

现在我们的系统已经准备好了。让我们配置它。

系统配置

首先,我想在一个地方收集我的所有日​​志。我将使用Seq只是因为它很容易与 Docker 一起使用。这是相应的 Docker Compose 文件:

<span style="color:#000000"><span style="background-color:#fbedbb">version: "3"
services:
  seq:
    image: datalust/seq
    container_name: seq
    environment:
      - ACCEPT_EULA=Y
    ports:
      - "5341:5341"
      - "9090:80"</span></span>

现在在地址http://localhost:9090我可以访问 Seq UI。我可以使用Serilog在 Seq 中写入日志:

C#

但是我的日志系统还有一个要求。它必须让我通过一些 API 访问日志条目。对于 Seq,有一个 NuGet 包Seq.Api。这对我来说已经足够了。

现在我想在日志中添加一些信息,稍后我将使用这些信息来构建请求序列图。为此,我将创建 ASP.NET Core 中间件并将其添加到请求处理管道中。这是主要代码:

C#

我们在这里能看到什么?我们将以下信息添加到所有日志条目中:

  • 当前服务的名称。这里没有魔法。它可以只是程序集名称或您想要的任何名称。
  • 相关标识。我希望它不需要很长的介绍。它连接与单个外部请求关联的所有日志条目。
  • 向其发送外部请求的服务的名称。这是请求进入我们系统的地方。此信息仅为方便起见,不会在本文中使用。
  • 请求链中上一个服务的名称。知道请求来自哪里很有用。
  • 有些timestamp不依赖于不同服务的物理时钟。我们稍后会更详细地讨论这个问题。

在请求处理开始时,我们需要从请求对象中获取所有这些值。这就是所有这些GetNNN方法在Invoke. 我们来看看GetCorrelationId方法。其他方法大体相同。

C#

这些值的提供者通常也相同。它们在请求期间将值存储在 type 的字段中AsyncLocal<T>

C#
收缩▲   

但这种简单性有一个例外。我说的是单调的时钟。现在是讨论它的时候了。

单调的请求序列

从技术上讲,每个日志条目都有自己的时间戳。是什么阻止我按此时间戳排序并考虑这样的记录序列?有几个障碍。

首先,正如我已经说过的,不同服务的时钟可能不同步。即使是几十毫秒的微小变化也可能导致日志条目的洗牌。

但是即使时钟完全同步,这也不能完全解决我们的问题。想象一下,我的服务多次调用同一个端点,每次参数略有不同。我正在并行执行以提高性能。这些调用的日志条目将不可避免地被打乱。

我们对于它可以做些什么呢?我们需要某种对所有服务都相同的神奇单调时钟,或者单调递增的数字序列。我们可以如下使用它。当我的系统收到请求时,我将这个时钟设置为 0。当我需要调用另一个服务时,我将时钟值加一并将这个新值发送给另一个服务。它获取值并进行自己的调用,每次都会增加该值。最终,它会将最后一个值返回给我。有了这个值,我继续我的工作。

这种方法有许多缺点。首先,外部(不是我的)系统不会更新我的时钟值。但这不是那么重要。更糟糕的是我无法进行并行调用。我必须等待下一次通话结束才能获得更新的时钟值。

为了避免这个问题,我将使用不同的方法。我使用上一个服务中的时钟值作为我服务中时钟值的前缀。后缀将是一个数字,我会随着对另一个服务的每个请求单调增加。以下是它的实现方式:

C#
收缩▲   

我的中间件使用该SetPreviousServiceClock方法来初始化此请求的时钟。GetNextCurrentServiceClock每次我向另一个服务发送请求时都会使用该方法。

因此,如果我的服务收到时钟设置为2的请求,它将生成时钟值为2.02.12.2、...的其他服务的请求,如果服务收到时钟值为2.1的请求,它将生成值为2.1.0 , 2.1.1 , 2.1.2 , ...

如果我对每个日志条目都有这样的值,我可以轻松地对它们进行分组和排序。同一组中的条目可以按时间戳安全地排序,因为它们是在单个服务处理单个请求时创建的。这意味着它们的时间戳是由一个物理时钟创建的。

再说一句。可以说我在这里实现了Lamport 时间戳。可能是这样,但我不会冒险断言。我确信有更有效的算法可以解决这个问题。在实践中,您应该使用它们。但是在这里,我的实现就足够了。

请求发送

现在我们有了从请求到我们服务的信息。我们需要根据我们自己的每个请求进一步发送它。我们该怎么做?为简单起见,我将使用HttpClient. 这是我的其中一项服务的客户:

C#

我将它注册到依赖容器中,如下所示:

C#
<span style="color:#000000"><span style="background-color:#fbedbb">builder.Services.AddHttpClientWithHeaders<IService2Client, Service2Client>();</span></span>

这里的AddHttpClientWithHeaders方法如下所示:

C#

如您所见,我只是添加自己的请求处理程序。这是它的代码:

C#

首先,我在请求中添加了几个标头,您已经知道了它们的值。在这里,我将这些值传递给下一个服务。

然后我创建了一个带有两个特殊字段的附加日志条目。其中之一是请求的 URL。我保留它仅供参考。在序列图中,我将显示此地址。我将使用第二个字段 ( RequestBoundaryFor) 来了解我必须将来自目标服务的日志条目放在哪里。我们稍后会在讨论创建请求序列图时讨论这个主题。

系统启动

是时候提出一些要求了。首先,我将使用 Docker Compose 启动 Seq:

<span style="color:#000000"><span style="background-color:#fbedbb">> docker compose -f <span style="color:#800080">"</span><span style="color:#800080">docker-compose.yml"</span> up -d</span></span>

然后我将启动我的所有服务。这是我在 Visual Studio 中的启动配置:

现在我们可以向我们的一项服务发出请求(例如,向http://localhost:5222/weatherforecast)。

之后,我们将在 Seq 中有一些条目:

我只需要他们的相关ID。

让我们看看如何根据这些日志条目构建请求序列图。

请求序列图的构建

互联网上有免费的www.websequencediagrams.com服务。它有自己的语言来描述序列图。我们将使用这种语言来描述我们的请求。为此,我创建了EventsReader应用程序。

但首先,我们必须从 Seq 获取日志条目。我们将使用Seq.Api NuGet 包:

C#

ServicesRequestLogs课堂上,我们按照单调时钟的值对所有日志条目进行分组:

C#
收缩▲   

具有相同值的所有条目对应于一项服务对一个请求的处理。它们存储在一个简单的类中:

C#

现在我们将构建序列图的描述:

C#

PrintParticipants方法描述了通信中的所有参与者。服务的名称可能包含不可接受的字符websequencediagrams,因此我们使用别名:

C#

PrintServiceLogs方法在一个服务中打印请求处理的序列。该方法获取单调时钟的值作为参数:

C#
收缩▲   

在这里,我们获取此特定时钟值(logs变量)的所有日志条目。然后,在方法的开头和结尾,有一些代码应该让我们的图表更漂亮。这里没有什么重要的:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值