Обработка ошибок при вызове другой компоненты
https://t.me/faangmasterДопустим у вас есть две компоненты, и одна компонента вызывает другую. Например, у вас есть сервис Users Service (сервис пользователей) и он вызывает сервис Orders Service (сервис заказов) для получения списка заказов для конкретного пользователя. Или ваша компонента читает/пишет сообщения из очереди типа AWS SQS или Rabbit MQ, или вы читаете/пишите из стрима типа Kafka или AWS Kinesis. Если вторая компонента не отвечает или бросает ошибки, то нужно соответствующим образом реагировать на такие ситуации.
Какие могут возникнуть проблемы?
1) Не удается установить соединение. Проблемы сети или компонента лежит.
2) Соединение удалось установить, но вызываемая компонента не возвращает никакого результата длительное время.
3) Соединение удалось установить, но вызываемая компонента возвращает ошибку.
4) Соединение удалось установить, но в процессе выполнения операции соединение прервалось.
5) Установление соединения занимает очень длительный период времени, но не возвращается ошибка о том, что сервер не доступен, т.к. сервер постоянно занят.
6) Сервер доступен, но он бросает ошибку по типу Throttling Exception, если для компоненты настроен Throttling (например, максимальное число вызовов в единицу времени или максимальное число соединений и т.д.)
Как можно реагировать в подобных случаях для достижения low latency, fault tolerance, resilience и availability?
1) Используйте Timeouts. В случае, если соединение удалось установить, но никакого результата не возвращается длительное время или не удается установить соединения длительное время, но не возвращается ошибка, что сервер не доступен, то очень полезно использовать таймауты в настройках вызова. Т.е. после определенного времени прекратить вызов, вернуть и обработать ошибку. Если этого не делать, то ваш поток, который делает вызов, зависнет на неопределенное время. И неполадка в одной компоненте приведет к неполадкам во второй и так далее по цепочке. И все ваше приложение зависнет или ляжет.
2) Используйте Retry Pattern. Если при каждой ошибке, при вызове компоненты, вы будете возвращать ошибку вплоть до уровня клиента, то это сильно ухудшит user experience, fault tolerance, resilience и availability вашего приложения. Множество ошибок - временные (transient errors). Временная проблема с сетью или временная сильная загрузка компоненты, которую вы вызываете и т.д. Эта ошибка может исчезнуть через условные 50 миллисекунд. Поэтому если в результате вызова вы получили ошибку, которую можно отнести к временной - то можно сделать повторный вызов (retry). Но тут надо быть аккуратным. В некоторых случаях мы можем усугубить ситуацию. Например, если компонента, которую мы вызываем, испытывает сильную нагрузку, то ее вызов с большой вероятностью приведет к ошибке. А если мы будем постоянно делать retry, то это только усилит нагрузку на эту компоненту, или вообще может ее положить в итоге. Это может привести к Retry Storm. Поэтому используйте ограниченное число retry + после каждого последующего retry увеличивайте промежутки между попытками (exponential backoff). Пример в Spring: https://www.baeldung.com/spring-retry
3) Используйте Circuit Breaker. Он позволяет мониторить процент успешных вызовов к компоненте в каком-то промежутке времени. Если этот процент меньше некоторого значения (threshold), то Circuit Breaker переходит из состояния Closed в состояние Open на некоторый промежуток времени. В этом состоянии при вызове компоненты сразу вернется ошибка. Это даст время компоненте восстановится. Через некий промежуток времени Circuit Breaker перейдет в состояние Half-open. В этом состоянии часть вызовов будет выполняться к компоненте и мониторится их успешность. Если они успешны, то Circuit Breaker снова перейдет в состояние Closed и все вызовы будут идти к компоненте. Пример в Spring: https://www.baeldung.com/spring-cloud-circuit-breaker
4) Используйте Backup Strategy. Если вызов после всех retry завершился ошибкой или Circuit Breaker в Open или Half-open состоянии, то можно вернуть какое-то Backup значение, вместо того, чтобы возвращать ошибку и показывать ее пользователю. Это, например, может быть значение из кэша. Например, у вас приложение типа Aviasales. Вы делаете вызов внешней компоненты для проверки цены на билет. И допустим она не отвечает, то можно вернуть значение из кэша. Оно может быть не самым актуальным, но это лучше, чем показать ошибку при поиске.
5) Улучшите мониторинг приложения и настройте нотификации. Можно эмитить какие-то метрики и/или писать логи с ошибками. Логи ошибок можно писать в ElasticSearch и мониторить их в Kibana. Метрики можно отображать Grafana. Если вы используете AWS, то это может быть AWS CloudWatch. В случае, если число ошибок или их процент больше какого-то значения, то настройте алерты/нотификации или автоматическое создание тикета. Это позволит оперативно обнаружить проблему и вмешаться. В Amazon мы прямо в catch блоке для серьезных ошибок дергали API для создание таски/тикета.
6) В случае, если ошибка серьезная и никакие из подходов не помогли ее решить автоматически, то сообщите о технической проблеме в UI пользователю.
7) Убедитесь, что архитектура вашего приложения позволяет избежать неработоспособности всего приложения, при отказе только одной компоненты и ее отказ не приведет по цепочке к отказу всех остальных компонент. Например, у вас интернет магазин и проблемы в сервисе отзывов не повлияет на возможность поиска или осуществления заказа товара.
Все эти подходы широко используются во многих компаниях, особенно, в FAANG. Почти каждый вызов другой компоненты обернут во все эти подходы.