深入理解Kubernetes探针和.NET服务健康检查机制

前言

随着越来越多的软件采用云原生和微服务架构,我们面临着更多的技术挑战,比如:

  • Kubernetes如何在容器服务异常终止、死锁等情况下,发现并自动重启服务;
  • 当服务依赖的关键服务(例如数据库,Redis)无法正常连接时,如何及时发出告警;
  • 在需要同时部署有依赖关系的服务时,如何确保它们可以按正确的顺序进行初始化;
  • ......

我将介绍如何利用.NET框架的健康检查机制以及Kubernetes的探针来确保微服务集群的稳定性和可靠性。

本文主要分为Kubernetes探针介绍,.NET健康检测机制介绍,最佳实践原则,以及一个简单的Demo。你可以在我的Azure仓库找到完整的项目文件,并在本地部署和测试:

k8s-helm-micro-services - Repos (azure.com)icon-default.png?t=N7T8https://dev.azure.com/1903268310/k8s-helm-tpl/_git/k8s-helm-micro-services

Kubernetes Probe

什么是探针

Kubernetes使用三种容器探针,分别是Liveness、Readiness和Startup,来检查容器的运行状态。

简而言之,探针检测需要在服务中创建健康检查接口,该接口根据语义正确返回服务当前的健康状态。然后,Kubernetes根据部署时的探针配置,自动调用服务的健康检查接口,并根据返回的状态码判断当前服务是否健康。

Kubernetes支持HTTP、TCP和Grpc三种类型的探针接口,本文将重点介绍HTTP类型的探针接口,其中大于等于200并且小于400的状态码表示服务健康,其他状态码表示服务不健康。

Liveness Probe

Liveness Probe用于表示容器服务是否活跃。Kubernetes会在容器生命周期中不断检测Liveness Probe状态,当Liveness Probe返回失败并且次数超过配置阈值时,Kubernetes将认为容器服务已经终止或者无法继续处理请求,进而杀死容器所在的Pod并重新创建。

Readiness Probe

Readiness Probe用于表示服务当前是否可以正常处理请求,例如当服务依赖的某个重要组件状态异常时,我们可以让Readiness接口返回Unhealthy,当Kubernetes发现Readiness探针失败时,它会停止向Pod发送请求,但不会重启Pod,直到服务变为可用为止。

Startup Probe

与前两者不同,在Startup探针探测成功之前,Kubernetes不会向Pod发送流量,也不会启动Liveness和Readiness探测。

例如,如果我们在部署服务时使用了错误的数据库连接字符串,在服务接入集群之后将会导致大量的数据库连接错误。如果我们在Startup阶段进行了数据库连接检查,那么k8s启动探针检查失败之后将认为该容器启动失败,因此可以帮助我们尽早发现问题。

另外,Startup探针还可以用来保护慢启动容器,避免故障误判,控制多服务启动顺序,避免过早初始化等。

在Kubernetes为init container提供了更好的支持之后,越来越多的服务开始将Startup检查放在独立的init container来进行,这样的好处是容器职责更加清晰,并且init container作为服务的前置容器,可以使用独立的技术栈,拥有独立的生命周期,且易于批量定制化和替换。

.NET健康检查

接下来,让我们结合.NET项目,在实践中了解Kubernetes探针检测和.NET服务健康检查机制。

服务状态

ASP.NET Core框架将服务定义为三种健康状态:Healthy(状态码:200)、Degraded(状态码:200)、Unhealthy(状态码:503)。

  • Healthy:服务健康,可以正常处理外部请求。
  • Degraded:应用程序处于降级状态,例如依赖的某些非关键服务失败,导致某些功能或性能受影响。Degraded可能是一个暂时的状态,服务可能会在一段时间内恢复到正常状态。
  • Unhealthy:服务处于不健康状态,无法处理外部请求。

IHealthCheck

在.NET项目中,我们需要多种IHealthCheck接口的实现,以检查依赖的服务是否可用。一个广泛应用的健康检查开源库:Github: AspNetCore.Diagnostics.HealthChecks,它提供了许多服务和中间件的健康检查方案。

以针对PostgreSQL数据库的检查为例,该检查从服务配置中读取PostgreSQL相关配置,在CheckHealthAsync方法中尝试打开数据库连接并执行一条测试语句。成功执行则认为PostgreSQL服务是健康的;否则,认为数据库服务不健康,并返回相应的健康状态。

public class NpgSqlHealthCheck : IHealthCheck
{
    private readonly NpgSqlHealthCheckOptions _options;

    public NpgSqlHealthCheck(NpgSqlHealthCheckOptions options)
    {
        Debug.Assert(options.ConnectionString is not null || options.DataSource is not null);
        Guard.ThrowIfNull(options.CommandText, true);
        _options = options;
    }

    /// <inheritdoc />
    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            await using var connection = _options.DataSource is not null
                ? _options.DataSource.CreateConnection()
                : new NpgsqlConnection(_options.ConnectionString);

            _options.Configure?.Invoke(connection);
            await connection.OpenAsync(cancellationToken).ConfigureAwait(false);

            using var command = connection.CreateCommand();
            command.CommandText = _options.CommandText;
            var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);

            return _options.HealthCheckResultBuilder == null
                ? HealthCheckResult.Healthy()
                : _options.HealthCheckResultBuilder(result);
        }
        catch (Exception ex)
        {
            return new HealthCheckResult(context.Registration.FailureStatus, description: ex.Message, exception: ex);
        }
    }
}

注册健康检测

在.NET项目中,我们可以通过注册实现了IHealthCheck接口的服务来进行健康检测。为了后续创建相应的探针端点,我们可以为每个HealthCheck指定name或者tag。

public const string StartupCheck = "/health/startup";
public const string ReadinessCheck = "/health/ready";
public const string LivenessCheck = "/health/live";

public static IHealthChecksBuilder AddAppsHealthChecks(this IHealthChecksBuilder healthChecksBuilder) =>
    healthChecksBuilder
        .AddCheck<App3StartupCheck>(StartupCheck)
        .AddCheck<App2ReadinessCheck>(ReadinessCheck)
        .AddCheck(LivenessCheck, () => HealthCheckResult.Healthy());

创建探针接口

.NET提供了两种方式来创建健康检测接口:UseHealthChecksMapHealthChecks。Kubernetes将利用这些接口来确定容器当前状态,它们的最大区别在于中间件管道中的顺序不同。

以下是一个示例,我们将创建一个请求路径为"/health/startup"的健康检测端点,该端点将根据HealthCheck的注册名字来筛选Startup Probe所需检查的服务。

        public static IApplicationBuilder UseAppsHealthChecks(this IApplicationBuilder app) =>
            app.AddLivenessProbe().AddReadinessProbe().AddLivenessProbe();

        public static IApplicationBuilder AddStartupProbe(this IApplicationBuilder app) =>
            app.UseHealthChecks(new PathString(StartupCheck), new HealthCheckOptions
            {
                Predicate = check => check.Name == StartupCheck,
            });

实践原则

服务依赖

在微服务架构中,服务之间的依赖关系会非常复杂。因此,在创建探针接口时,我们要充分考虑Kubernetes三种探针的运行机制以及服务之间的依赖关系,避免形成死锁,例如A依赖B可用,B又依赖C可用,而C又依赖A可用等。

探针配置

在Kubernetes中,配置容器的探针检测周期与超时时间时,需要综合考虑服务的负载和问题发现的时效性。通过结合日志和监控,可以调整服务探针机制到最佳状态。

探针实现

通常来说,Startup探针的目的是检测容器工作时机是否合适。因为其成功后通常不再运行,适合用于充分的状态检查。

对于Liveness探针,通常越简单越好。我们可以使用下面的代码添加一个基本的Liveness健康检查,即服务能够接受请求并返回,即认为其存活。

healthChecksBuilder.AddCheck(LivenessCheck, () => HealthCheckResult.Healthy());

Readiness探针的设计需要根据服务的具体情况而定,必须弄清楚在当前微服务架构中,哪些服务对于当前服务是必须的,或者在什么状态下需要将当前服务标记为Unhealthy,即无法正常处理请求。

实践教程

在示例代码k8s-helm-micro-services - Repos (azure.com)中,我创建了三个服务并使用helm template创建了helm chart,其中,app1依赖app2,app2又依赖app3。

探针配置

因为我使用helm template创建deployment等文件,因此我在values.yaml文件中配置了探针相关参数,并在deployment.yaml中引用,以app1为例:

values.yaml

安装微服务

我们先使用minikube和helm命令,部署app2和app3到cluster:

docker build -t app3:v1 .
minikube image load app3:v1
helm upgrade -i app3 .\app3\ -n test

docker build -t app2:v1 .
minikube image load app2:v1
helm upgrade -i app2 .\app2\ -n test

测试Readiness探针

为了方便测试,我在app3中添加了一个REST接口用来修改服务的HealthStatus,并且作为Readiness检查的结果:

        public static HealthStatus HealthStatus;

        [HttpGet("set")]
        public HealthStatus Set(HealthStatus healthStatus)
        {
            HealthStatus = healthStatus;
            return HealthStatus;
        }

    public class ReadinessCheck : IHealthCheck
    {
        public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) 
            => Task.FromResult(new HealthCheckResult(App3Controller.HealthStatus));
    }

由于HealthStatus默认值是Unhealthy,因此在部署之后我们可以看到Pod的状态是:Readiness probe failed: HTTP probe failed with statuscode: 503

此时,如果我们从app2中请求app3的接口,将会得到一个Connection refused错误,这是因为Readiness probe failed时,集群将停止向该Pod发送任何请求。

但此时app3容器是依然存活的,且其中的.NET服务依然在运行,因此我们可以通过kubectl port-forward等手段,访问到它的接口。 我们通过调用set接口,将服务健康状态设置为Healthy。

之后,app3服务探针检查恢复正常,app2可以成功的从app3中查询数据了

测试Startup和Liveness探针

我在app1当中添加了Startup探针,检测的内容是app3是否存活,因此在部署app1到cluster之前,我们先删除app3,以此来测试Startup探针的工作情况。

public class App3StartupCheck : IHealthCheck
    {
        private readonly ILogger<App3StartupCheck> _logger;
        public App3StartupCheck(ILogger<App3StartupCheck> logger)
        {
            _logger = logger;
        }

        public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
        {
            try
            {
                using var client = new HttpClient { BaseAddress = new Uri("http://app3.test") };

                var result = await client.GetStringAsync("health/live");
                return new HealthCheckResult(Enum.Parse<HealthStatus>(result));
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "App3 is not ready");
                return HealthCheckResult.Unhealthy();
            }
        }
    }

可以看到,在部署app1之后,因为app3服务已经被我们删除掉了,所以会出现:Startup probe failed: HTTP probe failed with statuscode: 503Startup探针检查失败导致该Pod一直被k8s重启。

当我们将app3重新部署回来之后,因为app3的Readiness默认健康状态是Unhealthy,所以尽管它处于存活状态,但此时仍无法处理任何请求。app1仍然无法通过接口获取到app3的Liveness状态,所以app1依然处于Startup probe failed。

接下来,我们通过上面提到的port-forward的方式,直接访问app3的容器接口,将HealthStatus的值修改为Healthy:

我们可以看到app3的探针状态已经恢复正常,而app1也会在下一次Pod重建时成功通过Startup探针检查。

 总结

本文以非常简单的示例,为大家介绍了如何将.NET服务的健康检查机制与Kubernetes探针结合使用,希望能够对大家有所启发。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/349428.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Git笔记:常用使用Git命令+使用Git关联本地仓库和远程仓库操作步骤

Git 分布式版本控制系统 常用Git命令 创建一个管理代码的文件目录右键Git Bath打开完成配置信息&#xff0c;输入个人用户名、邮件信息 git config --global user.name 用户名 git config --global user.email 邮箱地址初始化仓库 git init: 初始化仓库 文件目录里会出现一个…

Linux(2)——Linux中的Vim编辑器:从入门到精通

Linux中的Vim编辑器&#xff1a;从入门到精通 插播&#xff01;插播&#xff01;插播&#xff01;亲爱的朋友们&#xff0c;我们的Cmake/Makefile/Shell这三个课程上线啦&#xff01;感兴趣的小伙伴可以去下面的链接学习哦~ 构建工具大师-CSDN程序员研修院 一、Vim的基本概念…

基于ssm+vue在线考试系统

摘要 在线考试系统是一种利用现代技术手段实现的教育评估工具&#xff0c;它为学生提供了更灵活、便捷的考试方式&#xff0c;同时为教育机构提供了高效管理和评估学生学业水平的手段。在这个背景下&#xff0c;基于SSM&#xff08;SpringSpringMVCMyBatis&#xff09;框架和Vu…

cocos creator 碰撞系统

设置碰撞组件 3种组件类型&#xff0c;矩形碰撞&#xff0c;圆形碰撞&#xff0c; 多边形碰撞 开启碰撞检测 start() {//开启碰撞管理器let cm cc.director.getCollisionManager()cm.enabled true//绘制碰撞检测边界线。用于调试cm.enabledDebugDraw true//绘制精灵的边界c…

5.【SpringBoot3】文件上传

1. 文件上传到本地 需求分析 在用户更换头像或发布文章时&#xff0c;需要携带一个图片的 url 地址&#xff0c;该 url 地址是当用户访问文件上传接口&#xff0c;将图片上传成功后&#xff0c;服务器返回的地址。所以&#xff0c;后台需要提供一个文件上传接口&#xff0c;用…

85 总结一下最近遇到的一些 jar发布 相关的知识

前言 呵呵 最近有一些构建服务, 发布服务的一些需求 我们这里的服务 一般来说是 java application, spring boot application 针对发布, 当然最好是 增量发布, 尽量的减少需要传递给 发布服务器 的资源的大小 比如 我的这个 java application, 可能会存在很多依赖, 常规…

C++实用教程(四):面向对象核心多态 笔记

推荐B站视频&#xff1a;C现代实用教程(四):面向对象核心多态_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV15v4y1M7fF/?spm_id_from333.999.0.0&vd_sourcea934d7fc6f47698a29dac90a922ba5a3 本项目通用的tasks.json文件和launch.json tasks.json {"versi…

【并发编程】 synchronized的普通方法,静态方法,锁对象,锁升级过程,可重入锁,非公平锁

目录 1.普通方法 2.静态方法 3.锁对象 4.锁升级过程 5.可重入的锁 6.不公平锁 非公平锁的 lock 方法&#xff1a; 1.普通方法 将synchronized修饰在普通同步方法&#xff0c;那么该锁的作用域是在当前实例对象范围内,也就是说对于 SyncDemosdnewSyncDemo();这一个实例对象…

基于Spring Boot的饮食分享平台设计与实现

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

springboot-mybatis项目

一、后端开发环境搭建 1、File->New->Projet 2选择 Spring Initializr &#xff0c;然后选择默认的 url 点击next 3勾选Spring Web、SQL模板&#xff0c;next 4点击finish&#xff0c;搭建完成 二 数据库 1 新建数据库 2 执行sql建表 SET NAMES utf8mb4; SET FOREIGN…

中移(苏州)软件技术有限公司面试问题与解答(4)—— virtio所创建的设备2

接前一篇文章&#xff1a;中移&#xff08;苏州&#xff09;软件技术有限公司面试问题与解答&#xff08;4&#xff09;—— virtio所创建的设备1 在上一篇文章中&#xff0c;对于面试所提出的问题“virtio会创建哪些设备&#xff1f;”&#xff0c;有了初步答案&#xff0c;即…

0125-1-vue3初体验

vue3尝鲜体验 初始化 安装vue/clinext&#xff1a; yarn global add vue/clinext # OR npm install -g vue/clinext然后在 Vue 项目运行&#xff1a; vue upgrade --next项目目录 vue3-template ├── index.html // html模板 ├── mock // mock数据 │ └── user.…

【前沿技术杂谈:NLP技术的发展与应用】探索自然语言处理的未来

【前沿技术杂谈&#xff1a;NLP技术的发展与应用】探索自然语言处理的未来 NLP技术的发展与应用&#xff1a;探索自然语言处理的未来方向一&#xff1a;技术进步词嵌入&#xff08;Word Embeddings&#xff09;Transformer架构自然语言推理 方向二&#xff1a;应用场景智能客服…

深入浅出 diffusion(2):pytorch 实现 diffusion 加噪过程

我在上篇博客深入浅出 diffusion&#xff08;1&#xff09;&#xff1a;白话 diffusion 原理&#xff08;无公式&#xff09;中介绍了 diffusion 的一些基本原理&#xff0c;其中谈到了 diffusion 的加噪过程&#xff0c;本文用pytorch 实现下到底是怎么加噪的。 import torch…

微软 Power Apps model drven app 模型驱动应用使用Plugin插件实现业务流程跳转阶段功能

微软 Power Apps model drven app 模型驱动应用使用Plugin插件实现业务流程跳转阶段功能 模型驱动应用使用插件实现跳转业务流程阶段跳转功能 在实际操作中总会遇到使用业务流程的需求&#xff0c;那么如何使用plugin实现跳转阶段的功能呢 需求背景是主表上有业务流程&#x…

解密Java并发中的秘密武器:LongAdder与Atomic类型

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 解密Java并发中的秘密武器&#xff1a;LongAdder与Atomic类型 前言引言&#xff1a;为何需要原子操作&#xff1f;挑战和问题&#xff1a;原子操作的概念和重要性&#xff1a; AtomicInteger和AtomicL…

leetcode hot100 组合总和Ⅲ

本题中&#xff0c;要求我们求在1-9范围内&#xff0c;满足k个数的和为n的组合&#xff08;组合是无序的&#xff0c;并且题目中要求不可以重复&#xff09;。 这种组合问题依旧需要用回溯算法来解决。因为我们没办法控制产生k层for循环。回溯算法的过程是构建树结构&#xff…

【并发编程】锁死的问题——如何解决?以及如何避免?

目录 1.如何解决 一、死锁的定义和原因 1.1 定义 1.2 原因 二、常见的死锁场景 2.1 线程间相互等待资源 2.2 嵌套锁的循环等待 2.3 对资源的有序请求 三、死锁排查的方法 3.1 使用jstack命令 3.2 使用jconsole 3.3 使用VisualVM 四、常见的解决方案 4.1 避免嵌套锁…

16、Kafka ------ SpringBoot 整合 Kafka (配置 Kafka 属性 及对应的 属性处理类 解析)

目录 配置 Kafka 及对应的 属性处理类配置KafkaKafka配置属性的约定代码演示生产者相关的配置消费者相关的配置 代码&#xff08;配置文件&#xff09;application.properties 配置 Kafka 及对应的 属性处理类 配置Kafka spring.kafka.* 开头的配置属性&#xff0c;这些属性将由…

【Vue2 + ElementUI】分页el-pagination 封装成公用组件

效果图 实现 &#xff08;1&#xff09;公共组件 <template><nav class"pagination-nav"><el-pagination class"page-area" size-change"handleSizeChange" current-change"handleCurrentChange":current-page"c…