套接字句柄泄漏的原因

huangapple go评论58阅读模式
英文:

Why socket handles are leaked

问题

以下是翻译好的部分:

我有一个部署到Azure App Service的.NET Core Web应用程序。API只有一个端点,它的功能是调用另一个API(在MyCLass内部)并返回响应。

我在我的Startup中有以下代码:

services.AddScoped<IOAuthService, OAuthService>();
services.AddHttpClient<IMyClass, MyClass>(client =>
{
    var authConfig = config.Get<OAuthConfig>();
    var oAuthService = services.BuildServiceProvider().GetRequiredService<IOAuthService>();

    var token = oAuthService.GetTokenAsync(authConfig).GetAwaiter().GetResult();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
});

以下是OAuthService.GetTokenAsync方法:

private IConfidentialClientApplication app;
public async Task<string> GetTokenAsync(OAuthConfig config)
{
    app ??= ConfidentialClientApplicationBuilder.Create(config.ClientId)
        .WithTenantId(config.TenantId)
        .WithClientSecret(config.ClientSecret)
        .WithLegacyCacheCompatibility(false) // 不需要与ADAL.NET共享; 增加性能
        .Build();

    var scopes = new string[] { $"{config.Resource}/.default" };
    var authResult = await app.AcquireTokenForClient(scopes)
        .ExecuteAsync()
        .ConfigureAwait(false);

    return authResult.AccessToken;
}

一个HttpClient被注入到MyClass中,在那里它调用另一个API并返回响应。上面的代码导致了套接字句柄泄漏和SNAT端口耗尽的问题。

然而,以下更改将解决这个问题:1. 不要在DI中获取令牌,而是在每次发出请求时在MyCLass中动态获取令牌(不需要对OAuthService进行任何更改)。 2. 将上述DI代码更改为以下内容:

services.AddSingleton<IOAuthService, OAuthService>();
services.AddHttpClient<IHdcApiDataConnector, HdcApiDataConnector>();

我的问题是,为什么第二段代码可以解决套接字句柄泄漏的问题?

英文:

I have a .Net Core web app deployed to Azure App Service. The API has only one endpoint, and all it does is call another API (inside MyCLass) and return the response.

I have the following in my Startup:

services.AddScoped&lt;IOAuthService, OAuthService&gt;();
services.AddHttpClient&lt;IMyClass, MyClass&gt;(client =&gt;
{
    var authConfig = config.Get&lt;OAuthConfig&gt;();
    var oAuthService = services.BuildServiceProvider().GetRequiredService&lt;IOAuthService&gt;();

    var token = oAuthService.GetTokenAsync(authConfig).GetAwaiter().GetResult();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(&quot;Bearer&quot;, token);
});

And here is the OAuthService.GetTokenAsync method:

private IConfidentialClientApplication app;
public async Task&lt;string&gt; GetTokenAsync(OAuthConfig config)
{
    app ??= ConfidentialClientApplicationBuilder.Create(config.ClientId)
        .WithTenantId(config.TenantId)
        .WithClientSecret(config.ClientSecret)
        .WithLegacyCacheCompatibility(false) // No need to share with ADAL.NET; increases performance
        .Build();

    var scopes = new string[] { $&quot;{config.Resource}/.default&quot; };
    var authResult = await app.AcquireTokenForClient(scopes)
        .ExecuteAsync()
        .ConfigureAwait(false);

    return authResult.AccessToken;
}

A HttpClient is injected into MyClass where it calls another API and returns the response. The above code results in socket handle leaks and SNAT port exhaustion.

However, the following changes would solve the problem: 1. Instead of getting the token once in DI, get the token in MyCLass on the fly every time a request is made (without any changes to OAuthService). 2. Changing the above DI code to the following:

services.AddSingleton&lt;IOAuthService, OAuthService&gt;();
services.AddHttpClient&lt;IHdcApiDataConnector, HdcApiDataConnector&gt;();

My question is why exactly the second code solves the socket handle leak problem?

答案1

得分: 1

答案我的问题。

经过一些调查,我找出了发生了什么:

  1. 使用 ??== 是为了确保只创建一次 ConfidentialClientApplicationBuilder。然而,将 IOAuthService 注册为作用域意味着每个传入的请求都会创建一个新实例,因此在每个实例中创建一个 ConfidentialClientApplicationBuilder。在第二种方法中将其设置为单例将解决此问题。
  2. services.AddHttpClient&lt;IMyClass, MyClass&gt;IMyClass 注册为瞬态。这意味着每个传入请求都会创建一个新的 MyClass 实例。此外,对于每个这样的实例,代码 services.BuildServiceProvider().GetRequiredService&lt;IOAuthService&gt;() 创建另一个 IOAuthService 实例,从而导致上面提到的问题。

除了 DI 问题,我的代码还存在一些其他问题:

  1. 正如 @JoelCoehoorn 在评论中提到的,第一种方法中还有一个严重的缺陷:令牌会过期,需要刷新。
  2. app ??= ConfidentialClientApplicationBuilder.Create 可能应该移到构造函数或甚至放在 DI 中,将这种逻辑(确保 ConfidentialClientApplicationBuilder 是单例的)放在类方法内部非常令人困惑且容易出错。

此外,请注意,如果 ConfidentialClientApplicationBuilder 是单例的,那么令牌将在内部缓存,您不必自己实现令牌的 刷新(参见此链接)。

英文:

Answering my question.

Ok after some investigation, I found out what was happening:

  1. Using ??== was an attempt to make sure ConfidentialClientApplicationBuilder is created only once. However, registering IOAuthService as scoped means a new instance is created for every incoming request and hence one ConfidentialClientApplicationBuilder is created in each instance. Making it singleton in the second approach would solve this issue.
  2. services.AddHttpClient&lt;IMyClass, MyClass&gt; registers IMyClass as transient. This means a new instance of MyClass is created for each incoming request. Further, for every such instance, the code services.BuildServiceProvider().GetRequiredService&lt;IOAuthService&gt;() creates another instance of IOAuthService causing the problem mentioned above.

Aside from the DI issue, there are some other issues with my code above:

  1. As @JoelCoehoorn mentioned in the comments, there is another big flaw in the first approach: tokens expire and should be refreshed.
  2. app ??= ConfidentialClientApplicationBuilder.Create should probably be moved to either the constructor or even in DI, it's very confusing and error-prone to have that kind of logic (making sure the ConfidentialClientApplicationBuilder is singleton) inside the class method.

Further, note that if the ConfidentialClientApplicationBuilder is singleton, then the tokens are cached under the hood and you don't have to implement refreshing tokens yourself (see this).

huangapple
  • 本文由 发表于 2023年2月18日 09:47:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/75490678.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定