Comments 24
Федя предупредил Васю, что приложение раскатывается в AppStore и GooglePlay постепенно: в отличие от обновлений бэкенда,
А оказалось, что бэкенд тоже раскатывается постепенно.
Выдергиваете из контекста.
в отличие от обновлений бэкенда, в этом случае откатить приложение до более низкой версии не получится
Бекенд можно оперативно закатить обратно, если что-то пошло не так. Но да, конечно, бекенд раскатывается не мгновенно (если у вас не один сервер), это нужно учитывать.
Что я выдёргиваю? Вторая часть как бы про откат, к ней претензий нет.
Первая же часть прямо утверждает, что бэкэнд раскатывается мгновенно.
Приложение = постепенно.
бэкенд + в отличие от = мгновенно.
Может у меня конечно с русским языком плохо, и значение "в отличие от" стало уже другим?
Я не особо филолог, но двоеточие - "более сильный" разделитель, чем запятая. "В отличие от" относится к откату, а не к "постепенно". Возможно, лучше было написать "постепенно и неотвратимо" - в отличие от бекенда, который можно откатить. Получается, что кусочек "в отличие от" вы выдергиваете из контекста отката.
Хорошо, я не буду утверждать как понял эту задачу тот-же Вася.
Но останусь при своём мнении, тут сделан акцент на развёртывании приложения, про откат вообще не думаешь на этапе обдумывания изменений.
Понятно что откат приложения болезненней.
Так что 1/2 вины на продакт-менеджере, сделал акцент на приложении, забыв про бэкенд.
GET /feedback-screen и POST /save-feedback
Это нормальная практика? Почему бы не GET /feedback и POST /feedback например? POST /save-feedback вообще как масло масленое
Спасибо за вопрос!
В статье я намеренно привожу такие именования - так проще воспринимается текст. Не нужно лишний раз думать во время чтения, идёт ли речь о сохранении или получении данных.
В целом API сильно упрощён, о чем я упомянул вначале. В Такси у нас в основном RESTfull API у всех микросервисов, но бывают и исключения - при разработке мы стараемся исходить из здравой логики.
Скорее всего в продакшене endpoint был бы таким, как вы предположили - POST /feedback
Если POST /save-feedback воспринимается проще, почему бы и нет? При POST /feedback на первый взгляд может показаться, что каждый раз создаём новый фидбек, хотя на деле фидбек у заказа может быть только один, и по существу это идемпотентный вызов.
HTTP-пуристы могли бы предложить что-то вроде PUT /feedback/<order_id> (при этом имеем ввиду, что нейминг с REST никак не связан).
А как ведётся статистическая обработка данных по работе сервиса? Есть отдельные микросервисы, которые выдают всевозможную статистическую информацию по тем или иным моментам по запросу?
Спасибо за вопрос!
На все сервисы у нас есть риалтайм мониторинги и графики с кодами ответов, потребляемыми ресурсами и другими метриками. Время реакции у них не больше 1 минуты - по ним мы оперативно можем замечать текущие проблемы.
Если нам хочется более подробной информации по работе сервиса - сервисы пишут логи, логи уезжают в Elasticsearch для оперативного поиска и хранятся там 2 дня. Для долгосрочного хранения и анализа больших объёмов они также архивируются на Mapreduce (в Яндексе есть свой, называется YT)
По эластику можно искать логи и что-то считать через UI - через Kibana.
Но для каких-то частотных сценариев у нас написан свой “микросервис логов” - он, например, умеет разархивировать логи с YT и заливать из обратно в эластик, позволяет быстро накликать фильтры (например логи по сервису, городу или заказу), склеивает и показывает логи в рамках одного запроса (простая реализация opentracing) и тд.
не понял насчет гибридного метода, передавать алгоритм не совсем секьюрно. может в текущей реализации для модалок и опросов сойдет, но в целом, ничто не мешает клиенту подменять алгоритм на выгодный для себя
ну то же касаемо и толстого клиента. некоторая бизнес-логика должна быть защищена и может быть только на бэкенде
Да, вы правы
Если сервис внутренний, то проблема безопасности может быть не так критична.
Если внешний, то нужно хорошо взвесить, какой профит вы получаете и какие меры безопасности надо предпринять. Сверять чексумму, что клиент и алгоритм не модифицировали, считать все важные данные в приложении, запретив их внешнее изменение на уровне ОС и так далее
На самом деле это очень редкий кейс, на мой взгляд - обычно в нём сразу понятно, что по-другому никак не сделать. В обычных задачах я бы рекомендовал делать более простые вещи
По толстому клиенту есть проблемы для небольших компаний) вам конечно безразлично.
Есть скажем ios-приложение, android с гугл-сервисами, android/huawei. не берем в расчет всякую экзотику аля windows phone. есть веб-приложение. возможно у кого то есть еще и десктопные.
И вот меняется логика выползания какой то модалки и .... это ж какие ресурсы надо иметь
чтоб везде править логику. тонкий клиент дешевле обойдется
Можно я немного позанудствую по поводу идемпотентности?
Я понимаю посыл, который в этой части вложен, понимаю, что там описана такая логика ретрая (ну может еще с отбоем по кол-ву ретраев):
`repeat until PostSaveFeedback(stars, reasons, better_quality_reasons, order_id)`
но представленное решение не до конца правильное. Истинной идемпотентности можно было добиться дополнительно изменив метод запроса /save-feedback с POST на PUT.
В таком случае не сломается логика, если по прошествии трех плохих ретраев обработчик вернет управление UI, а там пользователь сможет поменять ответ (ну кто его знает, бизнес-требования все время изменяются). В текущем решении мы рискуем получить разные данные для better_quality_reasons между вот этими вызовами:
send_to_support(better_quality_reasons, support_task_id);
write_better_quality_reasons_to_db(better_quality_reasons, order_id);
ведь send_to_support отвалится с DuplicateTask
Однако, такое решение опять сломает обратную совместимость, ведь часть серверов еще может не поддерживать PUT, как Вася уже начнет выкатывать все это на фронт.
Хороший пример, спасибо
Он конечно не совсем про идемпотентность - по ее определению подразумевается, что функция была вызвана с теми же самыми аргументами. Но все равно интересный кейс
Не очень понял, как конкретно смена http-метода помогает избежать этой ситуации.
Тут скорее нужен просто правильный код под капотом - например, безусловно перезаписывать старые данные, если новые отличаются.
Ну и конечно интересно сразу подумать про гонки - что в каком порядке вызывается, если параллельно пришло несколько разных параллельных запросов, и что делать с согласованностью данных.
Чтобы не думать об этом, параллельные запросы можно запрещать на уровне UI (блокирующий UI, запрет кросс-девайс запросов - проще, но не защищает от curl из консольки), либо более умно - через глобальный лок или через разбор случаев, атомарные операции над ресурсами (типично в продуктовой разработке - ресурс это БД) и проверки, что никакой другой запрос не успел параллельно поменять те же самые данные (тоже типично - на уровне SQL). Второй способ более сложен в разработке и понимании людьми вне контекста задачи, и часто бизнес-требования не такие строгие, допускается просто не переспрашивать фидбек, если не вышло единожды.
Ага, это я перепутал на какой стороне Вася написал код.
Сейчас попробую развернуть свою мысль про PUT. Если мы примем за факт то, что одна поездка может иметь только одну оценку, то ключом идемпотентности всего запроса можно считать order_id. Семантика метода PUT говорит нам о том, что ресурс должен быть изменен (или создан, если его еще не существует), что дает нам право рассчитывать на то, что повторный вызов этого метода не будет пытаться создавать новые ресурсы (задачи для саппорта). Согласно RFC7231 метод PUT является идемпотентным, а POST - нет.
Ок, я понимаю, что в Вашем примере Вася сделал идемпотентным вызов send_to_support, но по поводу остального мы никаких гарантий дать не можем. Т.е. я, например, не уверен, что вызванный дважды write_reasons_to_db не создаст две записи в БД.
Теперь представим, что мы изменили метод на PUT, и в соответствии с его семантикой давайте попробуем изменить код под капотом, для этого я предлагаю использовать в функциях работы с БД не INSERT, а UPSERT. Я уверен, что сейчас оно так и есть, но вот название функции add_rating наводит меня на мысль, что под капотом у нее что-то вроде INSERT. Если это не так - изменим ее название.
После того, как мы убедились, что все вставки в БД работают как UPSERT и при создании новой заявки в суппорт (что тоже лучше сделать как PUT) не создается дубликатов, мы можем считать, что код работает как обработчик PUT. Может быть в моем тексте много букв и кое-что уводит от того посыла, который я пытаюсь сюда вложить, поэтому я сейчас выражу его в пунктах:
давайте стараться не делать ретраи для POST, хотите ретраить - безопаснее это делать на бэкэнде (ну запуште сообщение в брокер и обрабатывайте, когда сможете, в Вашем примере пользователю все равно, когда это произойдет)
если мы сможем сделать идемпотентным все тело обработчика, то можно использовать метод, который в RFC7231 обозначен, как идемпотентный (тогда даже прокси сможет сделать вам ретрай)
если выполнение запроса отвалилось с ошибкой 5xx, то хорошо бы, чтобы мы не имели неконсистентные данные (хочется атомарности, откатите половину изменений, которые внесли)
Кроме того, если send_to_support поставить после вызова write_better_quality_reasons_to_db, то отказоустойчивость этого эндпоинта улучшится прямо на пустом месте за бесплатно.
Ну и все это пока не касается гонок, curl и прочих особенных случаев. Только безопасного повторного выполнения
Вася изменил тип переменной на
decimal64
— это позволяет передавать цену как строку в API, а в коде работать с ней как с числом с плавающей точкой без потери точности.
А потом вдруг выяснилось, что у клиента была списана сумма чаевых в 10 раз больше... В чём же дело? Оказывается, в локали приложения число 1.5 было сериализовано с запятой, а парсер бэкенда в локали en-US "проглотил" запятую - для него десятичный разделитель - это точка. Вот и списали 15 баксов...
А избежать этого можно было бы, либо передавая число число (json стандарт это позволяет), тут правда надо учесть отдельным полем валюту. Либо стандартизировать сериализацию.
спасибо, в целом валидно, что тут много тонкостей
парсер бэкенда в локали en-US "проглотил" запятую
обычно на такое юнит-тесты пишут, особенно если работа с деньгами
передавая число число (json стандарт это позволяет
с этим связаны свои риски, что дальше на таком double (вместо decimal) по ошибке пойдет арифметика и получим проблемы с точностью.
Либо стандартизировать сериализацию
мы в Яндекс Go передаем суммы как int в API - умножаем decimal на 10000 (знаем что больше 4-х знаков не поддерживаем) - там где нужна обработка значения. Там где не нужна, например, просто отрендерить на клиенте текст, там строкой.
Спасибо за коммент!
Вижу, что вам уже ответил Денис Исаев.
Я уточнил в своей статье, что decimal конечно нужен, только когда начинается арифметика, и мы можем потерять в точности чисел при операциях с ними.
Вы правы, в самом API можно передавать число как число. В таких случаях надо помнить, что в коде бэкенда не стоит делать промежуточную десериализацию этого числа в float из-за неточности представления, нужно создавать decimal сразу из полученной строки. Можно посмотреть, почему так, на примере метода from_float тут: https://docs.python.org/3/library/decimal.html
Сделайте возможность забанить водителя, не выставляя ему оценку. Иногда бывает, что водитель не сделал ничего объективно ужасного, но по каким-то субъективным причинам он мне не нравится. И мне неудобно портить ему рейтинг плохой оценкой, но и увидеть его во второй раз тоже не хочется.
>>> Запрос прилетал на машины бэкенда, куда ещё не успел раскатиться релиз.
а можно вопрос вот про эту часть задать? есть скажем 1_000_000 девайсов, 1_000 серверов. запрос с каждого девайса распределяется на какой то "случайным" образом выделенный сервер, как у вас это происходит? есть какая то точка входа скажем api.yandex.ru за ним лоад балансер который распределяет запрос на один из 1_000 серверов? или при начале работе выделяет за клиентом какой то сервер и он с ним общается? или каждый запрос каждый раз случайным образом попадает на разный сервер?
есть ли единая точка входа? что если она упадет? откуда приложение знает на какой сервер идти отправлять запрос?
Выделенного соединения между клиентами и сервером в обычных сценариях общения нет. Между ними обычно стоят лоад-балансеры (как L3, так и L4). Они балансируют запрос в определенный датацентр, затем как-то выбирают между всеми инстансами бэкенда (типично - по round robin). Для проверки доступности инстансов мы используем как пассивные, так и активные проверки.
При общении между микросервисами каждый из них тоже закрыт балансировщиком, и запросы идут через него, но тут есть нюансы. Иногда мы не хотим делать кросс-ДЦ запросы на этом этапе, а хотим ходить только внутри одного датацентра для уменьшения latency. А иногда вообще балансировщик выглядит лишним, если есть хороший service-discovery, то можно прикрутить клиентскую балансировку и ходить напрямую.
При этом для узкого класса задач у нас всетаки устанавливается прямое TCP-соединение между бэкендом и каким-то объектом. Например, в этот класс попадают все задачи по телеметрии самокатов, которые мы запустили этим летом.
В дополнение к своему комменту приведу ссылку на хорошую вводную статью про балансировку: https://habr.com/ru/company/mailru/blog/347026/
Стажёр Вася и его опыт разработки нового API