Мягкое завершение работы |

куча сусликов изящно закрывает/закрывает свои магазины

Оригинал:

Если вы работаете с готовой службой, очень важно правильно закрыть ее, не вызывая ошибок у клиентов и не теряя никакой информации при завершении работы. Без надлежащего корректного завершения работы клиенты будут получать ошибки всякий раз, когда контейнер завершает работу на стороне сервера. В худшем случае это также может привести к потере данных. Давайте лучше разберемся в приведенном ниже примере.

Простой HTTP-сервер

Например, представьте себе «простой http-сервис, который регистрирует некоторые полезные нагрузки в kafka, а некоторые сохраняются в mysql». Этот http-сервис, работающий как контейнер в prod, по сути, представляет собой систему с вводом в качестве входящих запросов и выводом в качестве ответа на входящие запросы, созданием сообщений в теме kafka, вставкой в ​​mysql с использованием соединения mysql.

// pseudo code
func (svc Service) HandleRequest(ctx context.Context, request http.Request) (*http.Response, error) {
  err := svc.mysqlClient.Query(
    "INSERT INTO request_audit_log(request_payload, received_timestamp) "
    +"VALUES (?, ?)",
    request.Payload,
    time.Now(),
  )
  if err != nil {
    return nil, err
  }
  err := svc.kafkaClient.Produce(encode(Message{
    Payload: request.Payload,
    ReceivedTimestamp: time.Now(),
  }))
  if err != nil {
    return nil, err
  }
  
  return &http.Response{
    Payload: "success",
  }, nil
}

Как только эта система получает запрос, она не может мгновенно произвести все выходные данные. Таким образом, существует задержка между получением входящего запроса и созданием всех необходимых выходных данных. Из приведенного выше запроса мы можем вывести порядок выходных данных, т.е. первый вывод — это MySQL, затем kafka и, наконец, ответ.

Таким образом, чтобы корректно закрыть приложение, мы могли бы просто остановить входящие запросы от системы, а затем просто дождаться вывода ответа для всех соответствующих входящих запросов. и это именно то, что http.Server делает, когда вы запускаете srv.Shutdown(ctx)

func (srv *Server) Shutdown(ctx context.Context) error

Shutdown изящно завершает работу сервера, не прерывая никаких активных подключений. Выключение работает, сначала закрывая все открытые прослушиватели, затем закрывая все бездействующие соединения, а затем ожидая неопределенное время, пока соединения не вернутся в состояние бездействия, а затем отключаются. Если срок действия предоставленного контекста истекает до завершения завершения работы, Shutdown возвращает ошибку контекста, в противном случае он возвращает любую ошибку, возвращенную при закрытии базового прослушивателя (прослушивателей) Сервера.

Выходы системы вне жизненного цикла запроса/ответа

Итак, все просто, нам нужно подключить Shutdown функция с SIGTERM, SIGINT сигналы, и мы готовы, верно? Ну нет. Давайте немного подправим указанный выше HTTP-сервер, изменив производителя kafka с синхронизировать к асинхронный производитель, здесь асинхронный производитель будет буферизовать созданные записи в памяти и сбрасывать их в kafka через равные промежутки времени, поэтому вывод kafka системы может поступать после вывода ответа системы. тот же принцип применяется к другим типам буферов в памяти, рабочих пулов, случайных подпрограмм, соединений MySQL, соединений Redis и т. д.

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

1d0e854a-9163-45d3-82d2-6b2917b93388_4111x2370.png

Итак, теперь корректное завершение работы необходимо применить и к производителю kafka, и к любому другому компоненту, который поддерживает с ним буферы или любые другие ресурсы (соединения, горутины и т. д.) даже после жизненного цикла запроса/ответа.

Порядок пакетов Мягкое завершение работы

из приведенного выше примера, если мы сначала изящно остановим производителя kafka, то до тех пор, пока HTTP-сервер не будет остановлен, запросы, которые записывают какие-либо данные в kafka, будут получать ошибки. И то же самое произойдет, если параллельно попытаться изящно остановить асинхронный производитель kafka и HTTP-сервер. поэтому порядок закрытия отдельных пакетов системы имеет важное значение.

Здесь порядок исходит из того факта, что производителя kafka необходимо передать обработчику HTTP-сервера, поэтому до тех пор, пока сервер не будет выключен, закрывать производителя kafka небезопасно, поскольку обработчик может использовать объект производителя kafka в любое время.

Для более сложных систем может быть несколько уровней буферов и текущих операций, как в приведенном выше примере HTTP-сервера. Поэтому мы должны строго следовать правильному порядку при закрытии пакетов.

Внедрение зависимостей FTW

После некоторого размышления несложно прийти к выводу, что нам нужно изящно закрыть все зависимости пакета перед закрытием самого пакета. Мы уже знаем зависимости во время запуска программы, так как мы бы вручную соединили их вместе, используя внедрение зависимостей. Порядок выключения должен реплицировать инъекцию зависимостей при запуске в обратном порядке; таким образом, всякий раз, когда мы пытаемся изящно закрыть компонент, мы можем быть уверены, что никто другой не владеет этим объектом компонента и все еще не использует его.

// pseudo code
func main() {
  mysqlClient := NewMySQLClient()
  kafkaProducerClient := NewKafkaProducerClient()
  svc := NewService(
    mysqlClient,
    kafkaProducerClient
  )
  httpServer := http.NewServer()
  httpServer.Register("/", svc.HandleRequest)
  httpServer.ListenAndServeNonBlocking()

  // unblock once there is a graceful shutdown signal
  <- signal.Notify(signal.SIGINT, signal.SIGTERM)

  // creation order is
  // 1. mysql client
  // 2. kafka producer client
  // 3. http request handler service
  // 4. http server

  // so now the shutdown order needs to be exactly reverse
  // 1. http server
  // 2. http request handler service
  // 3. kafka producer client
  // 4. mysql client

  // http.Server will make sure to stop accepting new requests
  httpServer.Close()  
  // in case the request handler maintains any buffers or state, this will trigger the necessary cleanup
  svc.Close()
  // kafka producer client will flush all the data it buffered to the kafka and close connections
  kafkaProducerClient.Close()
  // mysql client will gracefully close all the connections and makes sure that 
  mysqlClient.Close()
}

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

Заключение

Хотя корректное завершение работы широко задокументировано, также очень важно отметить порядок завершения работы компонента. Следование Dependency Injection косвенно даст способ выяснить, каков правильный порядок завершения работы компонента.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *