Поддержка в рабочем состоянии заданного набора контейнеров является одним из главных преимуществ использования Kubernetes.

В этой статье я хотел бы на практике разобраться в этом аспекте, а именно в том, как проверяются живы ли контейнеры и как они перезапускаются в случае необходимости, а также как нам, разработчикам приложений, помочь системе оркестрации в этом деле.

Чтобы начать экспериментировать, мне понадобится кластер Kubernetes. Я буду использовать тестовое окружение, предоставляемое Docker Desktop. Для этого всего лишь потребуется активировать Kubernetes в настройках.

Как установить Docker Desktop в Windows 10 можно узнать из первой части этой статьи.

Создание Web API

Для дальнейших экспериментов я создам простой веб-сервис на основе шаблона ASP.NET Core Web API.

Новый проект с названием WebApiLiveness
Новый проект с названием WebApiLiveness

Добавлю через Package Manager пакет для генерации случайного текста командой Install-Package Lorem.Universal.Net -Version 3.0.69

Изменю файл Program.cs

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using System;

namespace WebApiLiveness
{
    public class Program
    {
        private static int _port = 80;
        private static TimeSpan _kaTimeout = TimeSpan.FromSeconds(1);

        public static void Main(string[] args)
        {
            CreateAndRunHost(args);
        }

        public static void CreateAndRunHost(string[] args)
        {
            var host = Host
                .CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder
                        .UseKestrel(options => 
                        {
                            options.ListenAnyIP(_port);
                            options.Limits.KeepAliveTimeout = _kaTimeout;
                        })
                        .UseStartup<Startup>();
                })
                .Build();

            host.Run();
        }
    }
}

Добавлю в проект класс LoremService, который будет возвращать случайно сгенерированный текст

using LoremNET;

namespace WebApiLiveness.Services
{
    public class LoremService
    {
        private int _wordCountMin = 7;
        private int _wordCountMax = 12;

        public string GetSentence()
        {
            var sentence = Lorem.Sentence(_wordCountMin, _wordCountMax);
            return sentence;
        }
    }
}

В классе Startup зарегистрирую созданный сервис

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<LoremService>();
    services.AddControllers();
}

Заменю созданный автоматически контроллер на LoremController

using Microsoft.AspNetCore.Mvc;
using System;
using System.Net;
using WebApiLiveness.Services;
using Env = System.Environment;

namespace WebApiLiveness.Controllers
{
  [ApiController]
  [Route("api/[controller]")]
  public class LoremController : ControllerBase
  {
    private readonly LoremService _loremService;

    public LoremController(LoremService loremService)
    {
        _loremService = loremService;
    }

    //GET api/lorem
    [HttpGet]
    public ActionResult<string> Get()
    {
      try
      {
          var localIp = Request.HttpContext.Connection.LocalIpAddress;
          var loremText = _loremService.GetSentence();
          var result =
            $"{Env.MachineName} ({localIp}){Env.NewLine}{loremText}";
          return result;
      }
      catch (Exception)
      {
          return new StatusCodeResult(
            (int)HttpStatusCode.ServiceUnavailable);
      }
    }
  }
}

И наконец, добавлю файл Dockerfile с инструкциями для создания образа с приложением. За основу взят образ, оптимизированный для запуска приложений ASP.NET с версией .NET 5.

FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim
COPY bin/Release/net5.0/linux-x64/publish/ App/
WORKDIR /App
ENTRYPOINT ["dotnet", "WebApiLiveness.dll"]

В результате у меня получилась следующая структура

Структура проекта
Структура проекта

Создание и публикация образа

Подготовлю релиз приложения dotnet publish -c Release -r linux-x64

Создам образ, используя ранее подготовленный Dockerfile. Выполню команду из папки, где непосредственно находится этот файл docker build -t sasha654/webapiliveness .

И наконец, отправлю образ в Docker Hub docker push sasha654/webapiliveness

Я разместил образ в общедоступном репозитории и теперь его можно получить с любого устройства. Но если вы собираетесь проделать то же самое в своем репозитории Docker Hub, то очевидно, необходимо подставить вместо префикса sasha654 свой Docker ID, полученный при регистрации.

Прежде чем разворачивать приложение в кластере Kubernetes, я проверю, что на данном этапе все сделано правильно и запущу контейнер из созданного образа непосредственно через Docker docker run -p 8080:80 -d sasha654/webapiliveness

И отправлю запрос curl http://localhost:8080/api/lorem

Отлично! Теперь остановлю и удалю этот, уже ненужный, контейнер.

Запуск приложения в Kubernetes

Основной единицей развертывания в Kubernetes, является Pod – это группа, состоящая из одного (чаще) или нескольких (гораздо реже) контейнеров.

Поды почти никогда не создаются в кластере вручную. И в данном примере я создам другой ресурс Kubernetes – ReplicaSet, который в свою очередь будет создавать поды и следить за ними.

Манифест для ReplicaSet содержит инструкции для запуска 3 экземпляров подов на основе ранее опубликованного образа sasha654/webapiliveness. Созданным экземплярам будет присвоена метка api: loremapi.

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myrs
spec:
  replicas: 3
  selector:
    matchLabels:
      api: loremapi
  template:
    metadata:
      labels:
        api: loremapi
    spec:
      containers:
      - name: webapiliveness
        image: sasha654/webapiliveness

Применю команду kubectl create -f kuber-rs.yaml, а после выведу на экран информацию о созданных ресурсах командами kubectl get rs и kubectl get pods --show-labels

Чтобы получить доступ к работающим приложениям извне кластера, я создам ресурс типа Service LoadBalancer с помощью следующего манифеста. Здесь важно отметить, что служба будет предоставлять доступ к подам на основе селектора, указанного в разделе spec.

apiVersion: v1
kind: Service
metadata:
  name: mylb
spec:
  type: LoadBalancer
  selector:
    api: loremapi
  ports:
  - port: 8080
    targetPort: 80

Информация о созданном сервисе kubectl get svc

Теперь через браузер или Postman можно посылать запросы на адрес службы http://localhost:8080/api/lorem. А служба будет перенаправлять запрос к одному из относящихся к ней подов.

Здесь я бы хотел рассказать о сохранение сессий. При первом запросе к службе выбирается случайный под с которым устанавливается постоянное подключение. Соответственно все последующие запросы, принадлежащие этому подключению, будут отправляться в этот же под. И так до тех пор, пока подключение не будет закрыто. А для того чтобы убедиться, что каждый запрос все таки отправляется на случайно выбранный под, я установил в файле Program.cs значение для свойства KeepAliveTimeout равное 1 секунде, вместо 2 минут, принятых по умолчанию. В итоге, если отправлять запросы реже одной секунды, то каждый раз подключение будет принудительно закрываться, тем самым для обработки каждого нового запроса будет происходить новый выбор пода.

Теперь если произойдет сбой главного процесса в каком-либо из контейнеров, например, из-за ошибки в приложении, которая время от времени приводит к падению, то Kubernetes перезапустит его. Если Pod пропадет, например, из-за возникшей неисправности узла кластера, то Kubernetes создаст новый Pod.

Так, если я удалю вручную один из подов kubectl delete pod myrs-jqjsp, то почти сразу будет создан автоматически новый, тем самым будет восстановлен баланс между желаемым и фактическим количеством экземпляров.

Удалю все ресурсы из кластера kubectl delete all --all

Таким образом, даже не делая ничего в самом приложении, Kuberntes значительно повышает выживаемость.

Но бывают такие сценарии, когда приложение перестает корректно работать без падения процесса, а до тех пор пока процесс выполняется, Kubernetes будет считать, что все в порядке. И здесь я, как разработчик, должен позаботиться, чтобы известить Kubernetes, что приложение перестало работать должным образом и тем самым вынудить систему произвести перезапуск.

На сколько мне известно, есть 3 таких способа.

  • Проверка выполнения произвольной команды с помощью инструкции exec. Если команда по завершение возвращает 0, то все в порядке.

  • Проверка открытия TCP-порта. Если подключение установлено, то все в порядке.

  • Проверка отправкой GET-запроса по указанному маршруту. Если ответ получен и код ответа не указывает на ошибку, то, как вы уже догадались, все в порядке.

Поскольку в примере используется веб-приложение, я исследую проверку с помощью GET-запроса. Но перед тем как идти дальше я зафиксирую текущий образ на Docker Hub с тегом 1, т.к. совсем скоро я изменю веб-приложение и сделаю 2 версию образа.

Для следующей демонстрации я изменю код класса LoremService таким образом, чтобы после пяти запросов к API, приложение становилось неисправным, тем самым провоцируя Kubernetes перезапускать под с этим приложением.

using LoremNET;
using System;

namespace WebApiLiveness.Services
{
    public class LoremService
    {
        private int _wordCountMin = 7;
        private int _wordCountMax = 12;
        private int _numRequestBeforeError = 5;
        private int _requestCounter = 0;

        public LoremService()
        {
            IsOk = true;
        }

        public bool IsOk { get; private set; }

        public string GetSentence()
        {
            if (_requestCounter < _numRequestBeforeError)
            {
                _requestCounter++;
                var sentence = Lorem.Sentence(
                    _wordCountMin, _wordCountMax);
                return sentence;
            }
            else
            {
                IsOk = false;
                throw new InvalidOperationException(
                    $"{nameof(LoremService)} not available");
            }
        }
    }
}

Добавлю новый контроллер HealthController, который на GET-запрос будет возвращать состояние приложения.

using Microsoft.AspNetCore.Mvc;
using System.Net;
using WebApiLiveness.Services;

namespace WebApiLiveness.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class HealthController
    {
        private readonly LoremService _loremService;

        public HealthController(LoremService loremService)
        {
            _loremService = loremService;
        }

        //GET api/health
        [HttpGet]
        public StatusCodeResult Get()
        {
            if (_loremService.IsOk)
            {
                return new OkResult();
            }
            else
            {
                return new StatusCodeResult(
                    (int)HttpStatusCode.ServiceUnavailable);
            }
        }
    }
}

Как было описано ранее, опубликую новую версию и зафиксирую образ на Docker Hub, но уже с тегом 2.

Создам новый манифест ReplicaSet с проверкой состояния. В основном, этот манифест отличается от предыдущего тем, что будет создаваться всего 1 экземпляр пода, а также новым разделом livenessProbe, где указан путь по которому будет обращаться Kubernetes для проверки живости приложения.

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myrs
spec:
  replicas: 1
  selector:
    matchLabels:
      api: loremapi
  template:
    metadata:
      labels:
        api: loremapi
    spec:
      containers:
      - name: webapiliveness
        image: sasha654/webapiliveness:2
        livenessProbe:
          httpGet:
            path: /api/health
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 3

Манифест службы оставлю без изменений. Как и ранее, создам ресурсы ReplicaSet и Service.

После выполню более 5 запросов http://localhost:8080/api/lorem и тогда получу результат с ошибкой.

Через некоторое время единственный под будет перезапущен и снова будет готов обрабатывать запросы.

Взгляну подробнее на отчет, полученный командой kubectl describe pod myrs-787w2

Этот простой пример проверки жизни показал насколько это мощное и полезное средство для обеспечения восстановления и устойчивости приложений в целом.

Я не буду рассматривать подробно, но упомяну еще об одной важной проверке, которую может выполнять Kebernetes - проверка готовности (Readiness). Иногда я не хочу, чтобы только что запущенное приложение немедленно начинало принимать запросы, т. к. в некоторых сценариях для корректной работы необходимо выполнить загрузку больших данных из внешнего источника или выполнять какую-либо другую длительную операцию. На основе периодической проверки готовности определяется, должен ли конкретный под получать клиентские запросы на обработку или нет. Но в отличие от проверки на живость, если под не проходит проверку готовности, Kubernetes не убивает и не перезапускает его.

В завершении упомяну, что ASP.NET предоставляет ПО промежуточного слоя Microsoft.AspNetCore.Diagnostics.HealthChecks, упрощающее создание сценариев проверки. В частности имеются функции, которые позволяют проверить внешние ресурсы, например, СУБД SQL Server или удаленный API.

Здесь находится репозиторий с проектом.