Это история про то, что тестировать внутреннюю платформу для запуска и разработки приложений сложно, но если подойти к вопросу творчески, то можно и попробовать. В прошлой части мы уже подготовились к тестированию, теперь рассмотрим какими способами его осуществляли. Своим успешным опытом выполнения этой непростой задачи поделится QA-инженер Лариса Седнина.
PaaS (Platform as a Service) — внутренняя платформа для запуска и разработки приложений. Если коротко, то наш PaaS позволяет легко и, можно сказать, при нулевом знании внутренней кухни создать свой сервис и начать пилить продуктовые компоненты. Более длинное объяснение — в этом видео. Под катом небольшой рассказ о том, с какими проблемами пришлось столкнуться при первом приближении к тестированию продукта, как происходил сам процесс тестирования платформенных решений на примерах и какую пользу это принесло.
Меня зовут Лариса Седнина, я работаю QA-инженером в Авито в юните QA Center of Excellence. Наш юнит — это центр экспертизы по обеспечению качества, основная задача которого в распространении лучших практик тестирования, помощи в настройке процесса тестирования и разработке инструментов для тестирования.
Подготовка
Чтобы найти ошибку, надо думать как ошибка, быть как ошибка, быстрым и внезапным. Для этого я надела шапку разработчика, создала свой собственный сервис на Golang с помощью команды нашей утилиты Avito CLI (avito service create), которая под капотом делает всю рутинную работу за меня — создает репозиторий в git, проект в sentry, директорию на рабочей машине для локальной разработки, шаблон проекта и т.д. На выходе работы команды у нас получается настроенный пустой проект сервиса, садись да код пиши.
Попытка 0
Для начала, изучив инцидент по шагам, я решила использовать внутреннюю библиотеку api-composition, которая инкапсулирует в себе набор инструментов для работы с внешними потребителями. Выпор пал на него, так как в инциденте был замечен код, который использовал эту библиотеку в момент паники.
Задача стояла реализовать простой handler, за который можно было подёргать, чтобы принудительно вызвать панику в pod-e. Код приложения выглядел примерно так :
func main() {
_, err = os.Stat("/tmpfs/aragorn-fatality")
if os.IsNotExist(err) {
log.Info(ctx, "aragorn-fatality does not exists")
}
if err == nil {
os.Exit(1)
}
}
И handler-а:
func (h *Handler) Handle(ctx context.Context, request fatality.Request, response fatality.Response) error {
response.OK(&fatality.OKRespData{
Result: []components.ServiceResourceMetrics{},
})
if queryVar, err := request.Parameters.Query(); err == nil {
if queryVar.IsFatal {
return os.WriteFile("/tmpfs/aragorn-fatality", []byte(""), os.ModePerm)
}
}
return nil
}
Деплоим сервис, дожидаемся, когда пройдут readiness probe-ы, и дёргаем за ручку, чтобы вызвать нашу отложенную панику и падение pod-ов.
Получилось ли? Конечно же, нет, потому что панику успешно отлавливала платформа и аккуратно поднимала pod обратно.
Тогда попробуем другой вариант.
Попытка 1
Подготовка — использовать также api-composition, но сделать более топорный вызов паники из handler-а.
func (h *Handler) Handle(ctx context.Context, request fatality.Request, response fatality.Response) error {
response.OK(&fatality.OKRespData{
Result: []components.ServiceResourceMetrics{},
})
if queryVar, err := request.Parameters.Query(); err == nil {
if queryVar.IsFatal {
names := []string{
"lobster",
"sea urchin",
"sea cucumber",
}
fmt.Println(names[len(names)])
}
}
return nil
}
Что здесь происходит: если пришел определенный параметр из Query, то мы просто говорим, что хотим выбрать третий элемент из нашего массива. Но здесь нет третьего элемента, потому что, как и во многих языках программирования, нумерация массивов в Go начинается с нуля (0, 1, 2), что, соответственно, вызывает панику.
Сказано — сделано, задеплоили код, дёрнули ручку и… снова не получилось! Успеха не было, потому что какая-то платформенная библиотека эту панику по-прежнему корректно обрабатывала и наши pod-ы успешно перезапускались.
Попытка 2
Снова изучив логи инцидента, я обратила внимание, что там использовалась устаревшая библиотека app-boilerplate для запуска клиента приложения. Она всё ещё на поддержке, но не развивается, поэтому не стоит ее списывать со счета.
Приложение:
func main() {
app := appBoilerplate.New(appOpts...)
app.RegisterMiddleware(observability.ServerMW)
handler := aws.NewHandler()
app.RegisterHTTPHandler("/v1/fatality", handler.V1())
err = app.Run()
if err != nil {
app.Logger.Error("Error")
}
handler:
func (h *handler) V1() http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
h.handleFatality(resp, req)
})
}
func (h *handler) handleFatality(resp http.ResponseWriter, req *http.Request) {
isFatal := req.URL.Query().Get("isFatal")
if isFatal != "" {
names := []string{
"lobster",
"sea urchin",
"sea cucumber",
}
fmt.Println(names[len(names)])
}
}
Но вариант оказался снова не рабочим, значит, надо сделать перерыв на кофе и подумать ещё.
Попытка 3
Перечитав логи инцидента, я осознала, что не заметила важного нюанса — панику надо было создавать в горутине! А для этого просто внутри функции вызовем другую функцию, в которой и кинем панику.
В итоге код приложения менять не нужно, а код handler-а стал таким:
func (h *handler) handleFatality(resp http.ResponseWriter, req *http.Request) {
isFatal := req.URL.Query().Get("isFatal")
if isFatal != "" {
go func() {
err := os.WriteFile("/tmpfs/aragorn-fatality", []byte(""), os.ModePerm)
if err != nil {
fmt.Print("Error!")
}
names := []string{
"lobster",
"sea urchin",
"sea cucumber",
}
fmt.Println(names[len(names)])
}()
}
}
И тут получилось! Паника никак не обрабатывалась, pod-ы стали падать с ошибкой CrashLoopBackOff.
А что дальше?
Когда удалось получить стабильное воспроизведение проблемы, стало можно выявить корневые причины, которые приводили к инциденту и прийти с результатами к разработчикам.
По итогу работы у нас есть:
- Стабильное воспроизведение проблемы;
- Баг в CI с успешным деплоем канарейки с не прошедшими readiness probe-ами pod-ов;
- А также случайно замеченный баг, что если запустить следующий билд, пока еще идет предыдущий — он будет тоже успешен, хотя он ничего не сделает, а в логах билда завершится с ошибкой.
Взяв всё что у нас имеется, я пришла к разработчикам. Первое, что мне сказали: «Не надо допускать паник в горутинах». Но так как мир не идеален, то могут случаться ситуации, когда паника возникает в абсолютно неконтролируемых условиях. Например, у нас был кейс, который проявлялся только при высокой нагрузке и только если не отвечал какой-то зависимый сервис.
Один из вариантов решения данной проблемы — это навесить мониторинг на количество перезапусков pod-ов сервисов, выводить на графики сколько раз сервис рестартовал за последний час, какое количество подов сейчас живое, сколько они занимают ресурсов.
Также дополнительной мерой, по запросу от продуктовых команд, можно сделать алерты для критичных сервисов, что поды слишком часто рестартуют за определенный промежуток времени.
А баги в CI исправили в тот же день как мы их нашли.
Итог
Платформенные сервисы тестировать сложно, но можно. Также нужно быть готовым к тому, что иногда тестировать придется одни только граничные значения, так как успешный сценарий уже проверен при реализации задачи.
Задачи в платформенных сервисах безумно интересные, иногда это настоящий челлендж, чтобы посидеть и подумать как можно протестировать то или иное изменение, подумать над их реализацией и представить результаты разработчикам, чтобы они могли обработать полученный фидбэк.
И, даже несмотря на то, что задачи порой такие абстрактные, для них все равно нужен QA инженер.
27-28 июня в Москве впервые пройдет TestDriven Conf 2022 — профессиональная конференция для senior тестировщиков и QA-инженеров. Она будет посвящена всем вопросам автоматизации в тестировании и рядом. Расписание и тезисы докладов уже на сайте. И можно забронировать билеты по выгодной цене — чем ближе к конференции, тем будет дороже.