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

Пример приложения

Давайте рассмотрим упрощенный пример приложения Spring Boot, сгенерированного с помощью Spring Initializr. Приложение будет предоставлять только API-интерфейс привода, который по умолчанию определяет конечную точку проверки работоспособности. В нашем примере также будет настроен почтовый модуль.

Раздел зависимостей build.gradle:

dependencies { 
compile('org.springframework.boot:spring-boot-starter-actuator') 
compile('org.springframework.boot:spring-boot-starter-mail') compile('org.springframework.boot:spring-boot-starter-web') compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}") compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") 
}

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

Проверки работоспособности автоматически обнаруживаются и включаются при включении Spring Boot Actuator. По умолчанию для конечной точки используется путь /health. Очевидно, необходимо настроить хост SMTP-сервера, порт и учетные данные. Требуется как минимум запись хоста, например. spring.mail.host=localhost. В целях отладки можно отключить защиту привода с помощью management.security.enabled=false, чтобы проверить, какие проверки работоспособности выполняются приводом.

Последняя часть нашего примера — тривиальный код приложения:

@SpringBootApplication 
class Application { 
    companion object { 
       @JvmStatic 
       fun main(args: Array<String>) { SpringApplication.run(Application::class.java, *args) 
      } 
   } 
}

При запросе /health API вернет ответ, подобный следующему:

HTTP/1.1 200
Content-Type: application/vnd.spring-boot.actuator.v1+json;charset=UTF-8 
Date: Mon, 23 Oct 2017 08:08:32 GMT 
Transfer-Encoding: chunked 
X-Application-Context: application 
{ 
  "diskSpace": { 
  "free": 105755779072,
  "status": "UP",
  "threshold": 10485760,
  "total": 499046809600 
  }, 
 "mail": { 
    "location": "localhost:-1",
    "status": "UP" 
  },
  "status": "UP"
}

API работоспособности приложения, как в нашем примере, часто подключается к внешнему программному обеспечению для мониторинга. Монитор запрашивает целевое приложение о его работоспособности через регулярные промежутки времени, например. каждые 5 секунд.

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

Представьте, что конечная точка работоспособности проверяется каждые 5 секунд и что с сервером SMPT возникают периодические проблемы. Конечная точка работоспособности будет правомерно пытаться подключиться к SMTP-серверу и время от времени будет возвращать сообщение об ошибке. По моему опыту, когда вводится проверка работоспособности, обычно требуется некоторое время, чтобы настроить пороговые значения монитора, чтобы избавиться от ложных срабатываний. Таким образом, очень легко игнорировать периодически возникающие ошибки, когда мы думаем, что они вызваны слишком чувствительными порогами. Однако проигнорированные ошибки могут через некоторое время привести к тому, что наш сервер перестанет отвечать на любой запрос.

Почему это может произойти, спросите вы, и я отвечу. Тайм-аут не настроен!

Проверка работоспособности почтового сервера использует javax.mail.Service.connect под капотом. По ряду причин попытка установить TCP-соединение может занять произвольно больше времени, чем обычно. К сожалению, таймауты по умолчанию, используемые javax.mail.*, являются бесконечными. Поток, ожидающий установления соединения, не может обслуживать другие запросы, даже если он почти не использует ЦП. Максимальный размер пула потоков по умолчанию, используемый встроенным Tomcat в приложении Spring Boot, составляет 200. Если предположить, что заблокированная попытка подключения происходит дважды в час, наше приложение перестанет работать через 4 дня.

Никогда не используйте бесконечные тайм-ауты

Как видите, очень легко пропустить необходимость настройки тайм-аута. Справедливости ради, в документации Spring Boot четко сказано:

В частности, некоторые значения времени ожидания по умолчанию являются бесконечными, и вы можете изменить это, чтобы избежать блокировки потока из-за не отвечающего почтового сервера:

spring.mail.properties.mail.smtp.connectiontimeout=5000 spring.mail.properties.mail.smtp.timeout=3000 spring.mail.properties.mail.smtp.writetimeout=5000

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

Тайм-ауты нужны везде

Представьте себе метод действия контроллера, который вставляет одну строку в базу данных. Далее предположим, что конечная точка вызывается 50 раз в секунду, и обычно это занимает 100 мс. Все работает хорошо, пока мы не сталкиваемся с периодической неряшливостью базы данных, и теперь вставка занимает 2 секунды. Клиенты, вызывающие API, не тормозят. Дополнительные потоки запросов блокируются, а больше соединений с базой данных удаляются из пула. Как только все соединения с базой данных используются, а все другие конечные точки API начинают давать сбой. Это пример каскадного сбоя, т. е. проблемы в одном компоненте, распространяющейся на другие. Таких проблем легче избежать, когда настроен тайм-аут на действие контроллера и взаимодействие с базой данных.

Для каждой конечной точки API должен быть настроен тайм-аут, если мы хотим, чтобы наши службы были устойчивыми. К сожалению, этим правилом легко пренебречь. Более того, некоторые фреймворки даже не предоставляют такой возможности. Даже в чрезвычайно популярном Spring Mvc я не смог найти способ установить такой тайм-аут, кроме как с помощью асинхронного метода. К счастью, есть библиотеки, например. Hystrix, которые решают эту проблему и легко интегрируются.

Напомним, вот краткий и неполный список случаев, когда необходимо настроить тайм-аут:

  • действие контроллера
  • запрос к базе данных, заявление
  • взаимодействие с пулом базы данных
  • взаимодействие с пулом потоков
  • клиент API, например. HTTP, SOAP, SMTP

В следующих постах я опишу, как бороться с кейсами.

Петр Мионсковски

Поклонник TDD, стремящийся узнать что-то новое

Личный блог Электронная почта Twitter Github Stackoverflow

Важность тайм-аутов была опубликована 23 октября 2017 г.

Эта статья кросспостирована с личным блогом автора.

Первоначально опубликовано на сайте brightinventions.pl 23 октября 2017 г.