ASP.NET Core 10.0 中的健康检查
ASP.NET Core 提供健康检查中间件和库,用于报告应用基础结构组件的健康状态。
健康检查由应用程序作为 HTTP 端点公开。 可以为多种实时监控场景配置健康检查端点。
- 健康检查可以由容器编排器和负载均衡器用于检查应用程序状态。 例如,容器编排器可以通过停止滚动部署或重新启动容器来响应失败的健康检查。 负载均衡器可以通过将流量从失败的实例路由到正常实例,来应对不正常的应用。
- 可以监视内存、磁盘和其他物理服务器资源的使用情况来了解是否处于正常状态。
- 健康检查可以测试应用的依赖项(如数据库和外部服务端点)以确认是否可用和正常工作。
健康检查通常与外部监视服务或容器编排器一起用于检查应用的状态。 在向应用添加健康检查之前,需要确定要使用的监视系统。 监控系统指定要创建的健康检查类型以及如何配置其端点。
基本健康探测
对于许多应用,只需配置一个基本健康探测,报告应用是否能够处理请求(存活状态),就足以了解应用的状态。
基本配置会注册健康检查服务,并调用健康检查中间件,以便在某个 URL 端点返回健康检查响应。 默认情况下,不会注册任何特定健康检查来测试任何特定依赖项或子系统。 如果能够在健康状态端点 URL 处进行响应,则应用被视为正常。 默认响应编写器将 HealthStatus 作为纯文本响应写入客户端。 HealthStatus 为 HealthStatus.Healthy 、HealthStatus.Degraded 或 HealthStatus.Unhealthy 。
在 Program.cs 中使用 AddHealthChecks 注册健康检查服务。通过调用 MapHealthChecks 创建健康检查端点。
以下示例在 /healthz 中创建健康检查端点:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/healthz");
app.Run();
Docker HEALTHCHECK
Docker 包含一个内置 HEALTHCHECK 指令,可用于检查使用基本健康检查配置的应用的状态:
HEALTHCHECK CMD curl --fail http://localhost:5000/healthz || exit 1
前面的示例使用 curl 向 /healthz 的健康检查端点发出 HTTP 请求。 curl 未包含在 .NET Linux 容器映像中,但可以通过在 Dockerfile 中安装所需的包来添加它。 使用基于 Alpine Linux 的映像的容器可以使用包含的 wget 代替 curl。
创建健康检查
通过实现 IHealthCheck 接口来创建健康检查。 CheckHealthAsync 方法会返回 HealthCheckResult ,它以 Healthy、Degraded 或 Unhealthy 的形式指示健康状态。 结果被写成带有可配置状态代码的纯文本响应。 请参阅 「健康检查选项」 部分。 HealthCheckResult 还可以返回可选的键值对。
下面的示例显示健康检查的布局:
public class SampleHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
var isHealthy = true;
// ...
if (isHealthy)
{
return Task.FromResult(
HealthCheckResult.Healthy("A healthy result."));
}
return Task.FromResult(
new HealthCheckResult(
context.Registration.FailureStatus, "An unhealthy result."));
}
}
健康检查逻辑位于 CheckHealthAsync 方法中。 前面的示例将虚拟变量 isHealthy 设为 true。 如果 isHealthy 的值设为 false,则返回 HealthCheckRegistration.FailureStatus 状态。
如果 CheckHealthAsync 在检查过程中引发了异常,则会返回新的 HealthReportEntry ,并将其 HealthReportEntry.Status 设置为 FailureStatus 。 此状态由 AddCheck 定义(请参阅健康检查服务注册 部分),其中包含导致检查失败的内部异常 。 Description 设置为异常的消息。
注册健康检查服务
要注册健康检查服务,请在 Program.cs 中调用 AddCheck:
builder.Services.AddHealthChecks()
.AddCheck<SampleHealthCheck>("Sample");
以下示例中的 AddCheck 重载用于指定健康检查失败时报告的失败状态(HealthStatus)。如果失败状态为 null(默认值),则报告 HealthStatus.Unhealthy。这个重载适合库作者使用:当健康检查实现遵循该设置时,应用会在检查失败后采用库指定的失败状态。
标签可用于筛选健康检查。 筛选健康检查 部分介绍了标记。
builder.Services.AddHealthChecks()
.AddCheck<SampleHealthCheck>(
"Sample",
failureStatus: HealthStatus.Degraded,
tags: new[] { "sample" });
AddCheck 还可以执行 lambda 函数。在下面的示例中,健康检查始终返回健康结果:
builder.Services.AddHealthChecks()
.AddCheck("Sample", () => HealthCheckResult.Healthy("A healthy result."));
调用 AddTypeActivatedCheck 将参数传递到健康检查实现。 在下面的示例中,一个类型激活的健康检查在其构造函数中接受一个整数和一个字符串:
public class SampleHealthCheckWithArgs : IHealthCheck
{
private readonly int _arg1;
private readonly string _arg2;
public SampleHealthCheckWithArgs(int arg1, string arg2)
=> (_arg1, _arg2) = (arg1, arg2);
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
// ...
return Task.FromResult(HealthCheckResult.Healthy("A healthy result."));
}
}
若要注册上述健康检查,请调用 AddTypeActivatedCheck 并将该整数和字符串作为参数传递:
builder.Services.AddHealthChecks()
.AddTypeActivatedCheck<SampleHealthCheckWithArgs>(
"Sample",
failureStatus: HealthStatus.Degraded,
tags: new[] { "sample" },
args: new object[] { 1, "Arg" });
使用健康检查路由
在 Program.cs 中,在端点生成器上调用 MapHealthChecks,并传入端点 URL 或相对路径:
app.MapHealthChecks("/healthz");
需要主机
调用 RequireHost 来为健康检查端点指定一个或多个允许的主机。 对主机使用 Unicode 而不是 punycode,并且可以包含端口。 如果未提供集合,则接受任何主机:
app.MapHealthChecks("/healthz")
.RequireHost("www.contoso.com:5001");
若要将健康检查端点限制为仅响应特定端口,请在对 RequireHost 的调用中指定端口。 通常,在容器环境中使用此方法公开监视服务的端口:
app.MapHealthChecks("/healthz")
.RequireHost("*:5001");
警告
依赖于主机头 的 API(如 HttpRequest.Host 和 RequireHost )可能会受到客户端的欺骗。
若要防止主机和端口欺骗,请使用以下方法之一:
- 在检查端口的地方使用 HttpContext.Connection (ConnectionInfo.LocalPort )。
- 采用主机筛选 。
若要防止未经授权的客户端欺骗端口,请调用 RequireAuthorization :
app.MapHealthChecks("/healthz")
.RequireHost("*:5001")
.RequireAuthorization();
有关详细信息,请参阅路由中与 RequireHost 匹配的主机 。
需要授权
调用 RequireAuthorization,为健康检查请求端点运行授权中间件。RequireAuthorization 重载接受一个或多个授权策略。如果未提供策略,则使用默认授权策略:
app.MapHealthChecks("/healthz")
.RequireAuthorization();
启用跨域请求 (CORS)
尽管在浏览器中手动运行健康检查并不是常见场景,但可以通过在健康检查端点上调用 RequireCors 来启用 CORS 中间件。 RequireCors 重载接受 CORS 策略生成器委托 (CorsPolicyBuilder) 或策略名称。 有关详细信息,请参阅在 ASP.NET Core 中启用跨源请求 (CORS) 。
健康检查选项
HealthCheckOptions 提供自定义健康检查行为的机会:
筛选健康检查
默认情况下,健康检查中间件会运行所有已注册的健康检查。要运行其中的一个子集,请为 Predicate 选项提供一个返回布尔值的函数。
下面的示例筛选健康检查,以便仅运行带有 sample 标记的健康检查:
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
Predicate = healthCheck => healthCheck.Tags.Contains("sample")
});
自定义 HTTP 状态代码
使用 ResultStatusCodes 可自定义健康状态状态到 HTTP 状态代码的映射。 以下 StatusCodes 分配是中间件所使用的默认值。 更改状态代码值以满足要求:
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
ResultStatusCodes =
{
[HealthStatus.Healthy] = StatusCodes.Status200OK,
[HealthStatus.Degraded] = StatusCodes.Status200OK,
[HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
}
});
不设置防缓存响应头
AllowCachingResponses 控制健康检查中间件是否在探测响应中添加 HTTP 标头,以防止响应被缓存。 如果值为 false(默认值),则中间件会设置或替代 Cache-Control、Expires 和 Pragma 标头以防止响应缓存。 如果值为 true,则中间件不会修改响应的缓存标头:
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
AllowCachingResponses = true
});
自定义输出
若要自定义健康检查报告的输出,请将 HealthCheckOptions.ResponseWriter 属性设置为写入响应的委托:
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
ResponseWriter = WriteResponse
});
默认委托会使用 HealthReport.Status 字符串值编写最小的纯文本响应。 以下自定义委托使用 System.Text.Json 输出自定义 JSON 响应:
private static Task WriteResponse(HttpContext context, HealthReport healthReport)
{
context.Response.ContentType = "application/json; charset=utf-8";
var options = new JsonWriterOptions { Indented = true };
using var memoryStream = new MemoryStream();
using (var jsonWriter = new Utf8JsonWriter(memoryStream, options))
{
jsonWriter.WriteStartObject();
jsonWriter.WriteString("status", healthReport.Status.ToString());
jsonWriter.WriteStartObject("results");
foreach (var healthReportEntry in healthReport.Entries)
{
jsonWriter.WriteStartObject(healthReportEntry.Key);
jsonWriter.WriteString("status",
healthReportEntry.Value.Status.ToString());
jsonWriter.WriteString("description",
healthReportEntry.Value.Description);
jsonWriter.WriteStartObject("data");
foreach (var item in healthReportEntry.Value.Data)
{
jsonWriter.WritePropertyName(item.Key);
JsonSerializer.Serialize(jsonWriter, item.Value,
item.Value?.GetType() ?? typeof(object));
}
jsonWriter.WriteEndObject();
jsonWriter.WriteEndObject();
}
jsonWriter.WriteEndObject();
jsonWriter.WriteEndObject();
}
return context.Response.WriteAsync(
Encoding.UTF8.GetString(memoryStream.ToArray()));
}
健康检查 API 不内置支持复杂的 JSON 返回格式,因为具体格式取决于所选的监视系统。请根据需要自定义上例中的响应。关于使用 System.Text.Json 进行 JSON 序列化,请参阅如何在 .NET 中序列化和反序列化 JSON。
数据库检测
健康检查可以指定数据库查询作为布尔测试来运行,以指示数据库是否在正常响应。
AspNetCore.Diagnostics.HealthChecks 是一个适用于 ASP.NET Core 应用的健康检查库,包括针对 SQL Server 数据库运行的健康检查。 AspNetCore.Diagnostics.HealthChecks 对数据库执行 SELECT 1 查询以确认与数据库的连接是否正常。
警告
使用查询检查数据库连接时,请选择快速返回的查询。 查询方法会面临使数据库过载和降低其性能的风险。 在大多数情况下,无需运行测试查询。 只需建立成功的数据库连接便足矣。 如果发现需要运行查询,请选择简单的 SELECT 查询,如 SELECT 1。
若要使用此 SQL Server 健康检查,请包含对 AspNetCore.HealthChecks.SqlServer NuGet 包的包引用。 以下示例注册了 SQL Server 健康检查:
var conStr = builder.Configuration.GetConnectionString("DefaultConnection");
if (string.IsNullOrEmpty(conStr))
{
throw new InvalidOperationException(
"Could not find a connection string named 'DefaultConnection'.");
}
builder.Services.AddHealthChecks()
.AddSqlServer(conStr);
注意
Microsoft 不维护或支持 AspNetCore.Diagnostics.HealthChecks 。
Entity Framework Core DbContext 探针
DbContext 检查用于确认应用能否与 EF Core DbContext 所配置的数据库通信。支持 DbContext 检查的应用需要:
- 使用 Entity Framework (EF) Core 。
- 添加对
Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCoreNuGet 包的引用。
AddDbContextCheck 为 DbContext 注册健康检查。 DbContext 作为 TContext 提供给方法。 重载可用于配置失败状态、标记和自定义测试查询。
默认情况下:
DbContextHealthCheck调用 EF Core 的CanConnectAsync方法。 你可以自定义在使用AddDbContextCheck方法重载检查健康状态时运行的操作。- 健康检查的名称是
TContext类型名称。
以下示例注册了一个 DbContext 和一个关联的 DbContextHealthCheck:
builder.Services.AddDbContext<SampleDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddHealthChecks()
.AddDbContextCheck<SampleDbContext>();
单独的就绪状态和存活状态探测
在某些托管方案中,使用一对健康检查来区分两种应用状态:
- 就绪状态(readiness)用于判断应用是否正常运行、但尚未准备好接收请求。
- 存活状态(liveness)用于判断应用是否已经崩溃、因而必须重启。
例如,应用必须先下载一个大型配置文件,才能开始处理请求。如果第一次下载失败,你不希望重启应用,因为应用可以多次重试下载。此时用存活探测表示进程是否存活,不执行其他检查。同时,在配置文件下载成功之前,应阻止请求发送到应用。使用就绪探测持续报告“未就绪”,直到下载成功、应用准备好接收请求。
以下后台任务模拟了一个启动过程,大约需要 15 秒。 完成后,任务将 StartupHealthCheck.StartupCompleted 属性设置为 true:
public class StartupBackgroundService : BackgroundService
{
private readonly StartupHealthCheck _healthCheck;
public StartupBackgroundService(StartupHealthCheck healthCheck)
=> _healthCheck = healthCheck;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Simulate the effect of a long-running task.
await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken);
_healthCheck.StartupCompleted = true;
}
}
StartupHealthCheck 用于报告耗时启动任务是否已完成,并公开一个由后台服务设置的 StartupCompleted 属性:
public class StartupHealthCheck : IHealthCheck
{
private volatile bool _isReady;
public bool StartupCompleted
{
get => _isReady;
set => _isReady = value;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
if (StartupCompleted)
{
return Task.FromResult(HealthCheckResult.Healthy("The startup task has completed."));
}
return Task.FromResult(HealthCheckResult.Unhealthy("That startup task is still running."));
}
}
在 Program.cs 中,连同托管服务一起,使用 AddCheck 注册健康检查。 因为托管服务需要对健康检查设置该属性,所以健康检查也会在服务容器中注册为单一实例:
builder.Services.AddHostedService<StartupBackgroundService>();
builder.Services.AddSingleton<StartupHealthCheck>();
builder.Services.AddHealthChecks()
.AddCheck<StartupHealthCheck>(
"Startup",
tags: new[] { "ready" });
若要创建两个不同的健康检查端点,请调用 MapHealthChecks 两次:
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
Predicate = healthCheck => healthCheck.Tags.Contains("ready")
});
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
Predicate = _ => false
});
前面的示例创建了以下健康检查端点:
/healthz/ready(用于就绪状态检查)。 就绪检查将健康检查筛选为带有ready标记的健康检查。/healthz/live(用于存活状态检查)。 存活状态检查通过在 HealthCheckOptions.Predicate 委托中返回false来筛选出所有健康检查。 有关筛选健康检查的详细信息,请参阅本文中的筛选健康检查 。
启动任务完成之前,/healthz/ready 端点报告 Unhealthy;完成之后,报告 Healthy。/healthz/live 端点排除全部检查,对所有调用均报告 Healthy。
Kubernetes 示例
在诸如 Kubernetes 这类环境中,使用单独的就绪状态和存活状态检查会十分有用。 在 Kubernetes 中,应用可能需要运行耗时的启动工作,然后才能接受请求,例如对基础数据库可用性的测试。 通过使用单独的检查,编排器可以判断应用是否正常运行,但尚未准备就绪,或者应用是否无法启动。 有关 Kubernetes 中的就绪状态和存活状态探测的详细信息,请参阅 Kubernetes 文档中的配置存活状态和就绪状态探测 。
以下示例演示如何使用 Kubernetes 就绪状态探测配置:
spec:
template:
spec:
readinessProbe:
# an http probe
httpGet:
path: /healthz/ready
port: 80
# length of time to wait for a pod to initialize
# after pod startup, before applying health checking
initialDelaySeconds: 30
timeoutSeconds: 1
ports:
- containerPort: 80
分发健康检查库
将健康检查作为库进行分发:
- 编写一个独立的类,实现 IHealthCheck 接口。该类可以借助依赖注入(DI)、类型激活和命名选项访问配置数据。
- 编写一个带参数的扩展方法,供使用该库的应用在
Program.cs中调用。以下健康检查示例接受arg1和arg2作为构造函数参数:
public SampleHealthCheckWithArgs(int arg1, string arg2)
=> (_arg1, _arg2) = (arg1, arg2);
前面的签名指示健康检查需要自定义数据来处理健康检查探测逻辑。 当健康检查向扩展方法注册时,数据会提供给用于创建健康检查实例的委托。 在以下示例中,调用方会指定:
arg1:用于健康检查的整数数据点。arg2:健康检查的字符串参数。name:可选的健康检查名称。 如果为null,则使用默认值。failureStatus:一个可选的 HealthStatus ,用于报告失败状态。 如果为null,则使用 HealthStatus.Unhealthy 。tags:可选的IEnumerable<string>标记集合。
public static class SampleHealthCheckBuilderExtensions
{
private const string DefaultName = "Sample";
public static IHealthChecksBuilder AddSampleHealthCheck(
this IHealthChecksBuilder healthChecksBuilder,
int arg1,
string arg2,
string? name = null,
HealthStatus? failureStatus = null,
IEnumerable<string>? tags = default)
{
return healthChecksBuilder.Add(
new HealthCheckRegistration(
name ?? DefaultName,
_ => new SampleHealthCheckWithArgs(arg1, arg2),
failureStatus,
tags));
}
}
健康检查发布器
将 IHealthCheckPublisher 添加到服务容器后,健康检查系统会定期执行健康检查,并将结果传给 PublishAsync。这种方式适用于基于推送的健康监视系统:各个进程定期调用监视系统,报告健康状态。
HealthCheckPublisherOptions 允许设置:
- Delay :在执行 IHealthCheckPublisher 实例之前应用启动后的初始延迟。 延迟在启动时发生一次,不适用于以后的迭代。 默认值为 5 秒。
- Period:IHealthCheckPublisher 的执行周期,默认值为 30 秒。
- Predicate :如果 Predicate 为
null(默认值),则健康检查发布器服务运行所有已注册的健康检查。 若要健康检查的子集,请提供用于筛选检查项集合的函数。 每个时间段都会评估谓词。 - Timeout :执行所有 IHealthCheckPublisher 实例的健康检查的超时时间。 在使用InfiniteTimeSpan 时可以避免超时执行。 默认值为 30 秒。
以下示例展示健康检查发布器的基本结构:
public class SampleHealthCheckPublisher : IHealthCheckPublisher
{
public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
{
if (report.Status == HealthStatus.Healthy)
{
// ...
}
else
{
// ...
}
return Task.CompletedTask;
}
}
HealthCheckPublisherOptions 类提供了用于配置健康检查发布器的行为的属性。
以下示例将健康检查发布器注册为单一实例并配置 HealthCheckPublisherOptions :
builder.Services.Configure<HealthCheckPublisherOptions>(options =>
{
options.Delay = TimeSpan.FromSeconds(2);
options.Predicate = healthCheck => healthCheck.Tags.Contains("sample");
});
builder.Services.AddSingleton<IHealthCheckPublisher, SampleHealthCheckPublisher>();
AspNetCore.Diagnostics.HealthChecks :
- 包含多个系统的发布者(包括 Application Insights )。
- 不由 Microsoft 进行支持或维护。
单独的健康检查
可以为每一个 HealthCheckRegistration 单独设置 Delay 和 Period。如果希望某些检查采用不同于 HealthCheckPublisherOptions 所设置周期的执行频率,就设置这两个值。
以下代码为 SampleHealthCheck1 设置 Delay 和 Period:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks()
.Add(new HealthCheckRegistration(
name: "SampleHealthCheck1",
instance: new SampleHealthCheck(),
failureStatus: null,
tags: null,
timeout: default)
{
Delay = TimeSpan.FromSeconds(40),
Period = TimeSpan.FromSeconds(30)
});
var app = builder.Build();
app.MapHealthChecks("/healthz");
app.Run();
依赖注入和健康检查
可以使用依赖注入,在健康检查类内部使用特定 Type 的实例。依赖注入可以向健康检查提供选项或全局配置,但它并不是配置健康检查的常见方式。通常,每项健康检查都紧密对应一个具体测试,并通过 IHealthChecksBuilder 扩展方法配置。
以下示例演示了一个示例健康检查,用于通过依赖注入检索配置对象:
public class SampleHealthCheckWithDI : IHealthCheck
{
private readonly SampleHealthCheckWithDiConfig _config;
public SampleHealthCheckWithDI(SampleHealthCheckWithDiConfig config)
=> _config = config;
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
var isHealthy = true;
// use _config ...
if (isHealthy)
{
return Task.FromResult(
HealthCheckResult.Healthy("A healthy result."));
}
return Task.FromResult(
new HealthCheckResult(
context.Registration.FailureStatus, "An unhealthy result."));
}
}
SampleHealthCheckWithDiConfig 和健康检查需要添加到服务容器中:
builder.Services.AddSingleton<SampleHealthCheckWithDiConfig>(new SampleHealthCheckWithDiConfig
{
BaseUriToCheck = new Uri("https://sample.contoso.com/api/")
});
builder.Services.AddHealthChecks()
.AddCheck<SampleHealthCheckWithDI>(
"With Dependency Injection",
tags: new[] { "inject" });
UseHealthChecks 与 MapHealthChecks
你可以通过两种方式使健康检查对调用方可用:
- UseHealthChecks 在中间件管道中注册用于处理健康检查请求的中间件。
- MapHealthChecks 注册健康检查端点。 端点与应用中的其他端点一起匹配和执行。
通过使用 MapHealthChecks 而不是 UseHealthChecks,你可以使用端点感知中间件(例如授权),从而获得更细粒度的匹配策略控制。 通过使用 UseHealthChecks 而不是 MapHealthChecks,可以精确控制健康检查在中间件管道中的运行位置。
- 请求匹配健康检查端点时,终止管道。通常需要这种短路行为,以免执行不必要的工作,例如日志记录和其他中间件。
- 主要用于在管道中配置健康检查中间件。
- 可以将端口上的任何路径与
null或空PathString匹配。 允许对向指定端口发出的任何请求执行健康检查。 - 源代码
MapHealthChecks 允许:
- 当请求与健康检查端点匹配时,通过调用 ShortCircuit 来终止管道。 例如
app.MapHealthChecks("/healthz").ShortCircuit();。 有关详细信息,请参阅路由后短路中间件 。 - 映射特定路由或端点以进行健康检查。
- 可以自定义 URL 或路径,以便访问健康检查端点。
- 映射具有不同路由或配置的多个健康检查端点。多端点支持具有以下用途:
- 为不同类型的健康检查或组件提供独立端点。
- 区分应用健康状态的不同方面,或者将特定配置应用于一部分健康检查。
- 源代码
其他资源
注意
原文说明:本文的部分内容是在人工智能的帮助下创建的。发布前,原作者已审阅内容并根据需要作出修订。请参阅我们在 Microsoft Learn 上使用 AI 生成内容的原则。











暂无评论内容