You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Azure Function中Serverless SignalR调用InvokeAsync成功StartAsync后挂起问题

Azure Function中Serverless Hub客户端InvokeAsync挂起问题排查

问题背景

搭建Azure Function时配置了用于通知Web客户端的简易Serverless Hub,FunctionApp包含名为MyHub的Serverless Hub,同时内置一个客户端(最终将接收EventGrid事件并通过MyHub通知客户端)。客户端成功调用StartAsync后,调用InvokeAsync时出现挂起情况,需排查原因。

代码示例

using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.Functions.Worker.SignalRService;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Net;

new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services =>
    {
        // 不确定是否需要,看起来加不加都没区别
        //services.AddSignalR().AddAzureSignalR(); 

        services.AddServerlessHub<MyHub>();
        services.AddSingleton<Client>();
    })
    .Build()
    .Run();

public class Client
{
    private readonly HubConnection _connection;

    public Client()
    {
        string? hostname = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME");

        _connection = new HubConnectionBuilder()
            .WithUrl($"http://{hostname}/api")
            .Build();

        _connection.StartAsync().GetAwaiter().GetResult();
    }

    [Function("test")]
    public async Task Negotiate([HttpTrigger] HttpRequestData req)
    {
        await _connection.InvokeAsync("broadcast", req.ToString());
    }
}

[SignalRConnection("AzureSignalRConnectionString")]
public class MyHub : ServerlessHub
{
    private const string HubName = nameof(MyHub);

    public MyHub(IServiceProvider serviceProvider) : base(serviceProvider)
    {
    }

    [Function("negotiate")]
    public static HttpResponseData Negotiate([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequestData req,
        [SignalRConnectionInfoInput(HubName = HubName)] string connectionInfo)
    {
        HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
        response.Headers.Add("Content-Type", "application/json");
        response.WriteString(connectionInfo);
        return response;
    }

    [Function("broadcast")]
    public Task Broadcast(
        [SignalRTrigger(HubName, "messages", "broadcast", "message")] SignalRInvocationContext invocationContext,
        string message)
    {
        return Clients.All.SendAsync(message);
    }
}

相关配置(local.settings.json)

{
  "Host": {
    "CORS": "http://localhost:5002",
    "CORSCredentials": true
  },
  "IsEncrypted": false,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.AspNetCore.SignalR": "Trace"
    }
  },
  "Values": {
    "AzureSignalRConnectionString": "Endpoint=https://xxx.service.signalr.net;AccessKey=xxx;Version=1.0;",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
  }
}

可能的问题原因及解决方案

1. 同步阻塞初始化引发死锁

Client构造函数中用_connection.StartAsync().GetAwaiter().GetResult()同步等待异步操作完成,在.NET Isolated Worker环境中,这种同步阻塞会占用关键线程,导致后续InvokeAsync因无法获取执行线程而挂起。

解决方法:
改用IHostedService延迟初始化Hub连接,避免构造阶段同步阻塞:

public class Client : IHostedService
{
    private readonly HubConnection _connection;

    public Client()
    {
        string? hostname = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME") ?? "localhost:7071";
        _connection = new HubConnectionBuilder()
            .WithUrl($"http://{hostname}/api")
            .Build();
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await _connection.StartAsync(cancellationToken);
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        await _connection.StopAsync(cancellationToken);
    }

    [Function("test")]
    public async Task Negotiate([HttpTrigger] HttpRequestData req)
    {
        await _connection.InvokeAsync("broadcast", req.ToString());
    }
}

同时更新服务注册代码:

services.AddHostedService<Client>();

2. Hub连接URL缺失本地调试 fallback

本地调试时WEBSITE_HOSTNAME环境变量未设置,导致连接URL变为http://null/api,虽然StartAsync未报错,但实际连接未成功建立,后续InvokeAsync因无效连接挂起。

解决方法:
添加本地地址 fallback:

string? hostname = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME") ?? "localhost:7071";

3. Serverless Hub调用路径不匹配

直接使用/api作为连接URL无法正确路由到MyHub,客户端调用服务端方法需要基于协商接口返回的连接信息构建连接,而非硬编码URL。

解决方法:
通过协商接口获取合法连接信息后再初始化Hub连接:

public async Task StartAsync(CancellationToken cancellationToken)
{
    string? hostname = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME") ?? "localhost:7071";
    var httpClient = new HttpClient();
    var negotiateResp = await httpClient.GetStringAsync($"http://{hostname}/api/negotiate");
    var connectionInfo = JsonSerializer.Deserialize<ConnectionInfo>(negotiateResp);

    _connection = new HubConnectionBuilder()
        .WithUrl(connectionInfo.Url, opts =>
        {
            opts.AccessTokenProvider = () => Task.FromResult(connectionInfo.AccessToken);
        })
        .Build();

    await _connection.StartAsync(cancellationToken);
}

4. CORS或权限配置问题

本地调试时CORS范围未覆盖客户端地址,或客户端连接未携带合法令牌,导致InvokeAsync请求被阻塞,进而出现挂起。

解决方法:
调试阶段可临时将local.settings.json中的CORS设置为*(生产环境需指定具体域名),同时确保客户端连接时正确使用协商接口返回的令牌。

内容的提问来源于stack exchange,提问作者Alexandre Guimond

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 20:37:15