Привет! Меня зовут Павел Голов, я инженер в юните Communications. Наш юнит развивает функционал взаимодействия пользователей на Авито. В феврале 2022 года произошло большое событие для нашей команды — мы закрыли все объявления частных пользователей защитными номерами. О том, какой путь мы прошли, я хотел бы рассказать в этой статье. Сначала быстро разберёмся, что же такое защита номера, как она работает и зачем нужна пользователям. Защита номера — бесплатная услуга для частных пользователей. Если вы продаёте на Авито, то при создании или редактировании объявления могли наблюдать такое предупреждение:  Когда продавец размещает объявление, мы прикрепляем к нему дополнительный виртуальный номер, который и показываем покупателям, решившим связаться с продавцом. Когда покупатель звонит на этот виртуальный номер, мы переадресуем звонки на реальный номер продавца. В чём цели защиты номера и зачем она продавцам? Настоящий номер продавца не попадает в базы злоумышленников. Продавать на площадке становится безопаснее, и можно не беспокоиться, что после закрытия объявления будут названивать спамеры. Пользователю нельзя написать в сторонний мессенджер или отправить СМС с поддельными ссылками, что делает жизнь злоумышленников труднее. Сообщения на Авито при этом приходят как и раньше. Мы фильтруем звонки на защитные номера от спама. Система блокирует значительное количество нецелевых вызовов, и продавцы на них не отвлекаются. В 2018 году на Авито появился первый прототип защиты номера для частных пользователей. С точки зрения user-story, защита номера представляла из себя два сценария:  Показ номера при просмотре объявления. Звонки на защитные номера.  Рассмотрим, как эти сценарии были реализованы изначально. Показ номера при просмотре объявления. При создании объявления сервис защиты номера асинхронно получал событие из Databus — нашего брокера сообщений, основанного на Kafka. Если продавец включал защиту номера телефона, мы выбирали один из свободных и доступных нам виртуальных номеров и привязывали его к созданному объявлению. Виртуальный номер был привязан к объявлению на протяжении всей его жизни, мы отвязывали виртуальный номер после закрытия или удаления объявления. Когда покупатель заходил на страницу объявления, он видел кнопку «Показать телефон». При нажатии на эту кнопку отправлялся запрос на бэкенд к сервису защиты номеров. Далее мы проверяли, привязан ли к этому объявлению виртуальный номер: если да — показывали покупателю защитный номер, если нет — показывали настоящий номер продавца. Звонки на защитные номера. Покупатель видел защитный номер на странице объявления, набирал его на телефоне и звонил. В первую очередь звонок доходил до оператора сотовой связи, владеющего виртуальным номером. Оператор понимал, что этот номер закреплен за Авито и отправлял запрос в наш API.  Запрос приходил в микросервис обработки звонков. На этом этапе мы, используя алгоритмы машинного обучения, определяли, является ли звонок спамом. В случае, если звонок определялся как спам, оператору отправлялась команда сбросить звонок. Если звонок — не спам, мы узнавали, привязан ли виртуальный номер, на который пытаются позвонить, к реальному номеру продавца. Если активная связка была, мы отвечали на запрос оператору номером телефона, на который нужно переадресовать текущий вызов, и звонок доходил до продавца. На практике схема звонков выглядела немного сложнее, но я оставил самую важную часть, чтобы не перегружать вас лишней информацией. Первый прототип был запущен для частных пользователей категорий «Недвижимость» и «Авто», что покрывало примерно 20% частных пользователей Авито. Прототип хорошо себя зарекомендовал, и мы приняли решение о раскатке защиты номера на всех частных пользователей. Прототип рабочий, казалось бы, просто масштабируем его на все категории Авито, и дело в шляпе! Но всё немного сложнее: как я описал выше, мы прикрепляли виртуальный номер к каждому объявлению пользователей для сохранения атрибуции вызовов. И эта связка была активна до тех пор, пока было активно само объявление.  Сейчас на Авито больше 90 миллионов активных объявлений, и эта цифра постоянно растёт. Часть объявлений принадлежит профессиональным пользователям, и тем не менее для закрытия всех объявлений частников нам потребовались бы десятки, а в перспективе и сотни миллионов виртуальных номеров. Это выливалось бы в огромные затраты, чего мы хотели избежать. Так как виртуальных номеров у нас в десятки раз меньше, чем активных объявлений, пришлось подумать, как закрыть все объявления имеющимся количеством номеров. Мы провели ряд аналитических исследований, которые показали, что покупатели совершают ~98,2% первичных звонков в течение первых двух часов с момента получения номера при просмотре объявления.  Что касается звонков, когда покупатель перезванивает продавцу по тому же номеру, то ~99,7% звонков совершается в первые 14 дней после первичного звонка. Опираясь на эти данные, мы построили алгоритм динамической защиты номеров, который стал улучшением прототипа, созданного ранее. Алгоритм основан на следующих тезисах: Виртуальные номера закрепляются за объявлениями не на весь жизненный цикл объявления, а лишь на определённое время. В каждом регионе РФ мы выделяем ограниченный пул виртуальных номеров. Виртуальные номера постоянно ротируются между объявлениями. User-story в новом алгоритме остаются теми же, что и в прототипе, но реализация отличается. Рассмотрим каждую историю подробнее. Теперь виртуальные номера закрепляются за объявлениями лишь на определённый срок и постоянно ротируются между ними. Как это работает и как мы определяем, какой виртуальный номер нужно показать при запросе покупателя? Когда покупатель нажимает кнопку «Показать телефон» на странице объявления, запрос по-прежнему идёт в микросервис защиты номеров. Если к объявлению уже привязан виртуальный номер, то мы просто его и показываем. Если же у объявления нет активной привязки, то мы: смотрим на пул виртуальных номеров конкретного региона РФ, где зарегистрирован реальный номер продавца;  ищем в пуле номер, который не показывался на сайте дольше всех и при этом показывался более двух часов назад.  После того, как такой виртуальный номер найден, мы отвязываем его от предыдущего объявления, привязываем к текущему и показываем покупателю. Всё происходит за десятки миллисекунд. Рассмотрим на простых примерах, как работает ротация номеров. Пусть у нас есть пул номеров, в котором всего 1 виртуальный номер. Как я говорил, виртуальные номера сгруппированы по регионам РФ. Прямоугольниками на примерах отмечены временные отрезки, в течение которых покупатели могут дозвониться по полученным номерам телефонов: В первом примере у нас всего 1 продавец, у которого 1 объявление. В момент t1 покупатель получает номер телефона на карточке объявления. С этого момента к реальному номеру продавца прикрепляется виртуальный номер из пула и начинается отсчёт времени, в течение которого покупатель может дозвониться продавцу по полученному виртуальному номеру. Сейчас это 2 часа.  В момент t2, при условии, что никакой другой покупатель не запрашивал номер телефона с этого объявления, виртуальный номер перестаёт переадресовывать звонок на номер продавца. Если же до момента t3 этот виртуальный номер ещё не перешёл никакому другому продавцу, а какой-либо покупатель снова запросил номер телефона на объявлении в момент t3, тот же виртуальный номер снова становится «активным» и переадресует вызовы на реальный номер нашего первого продавца.  Отмечу, что каждый просмотр телефона на объявлении продлевает время, в течение которого мы переадресуем звонки с текущего виртуального номера на реальный номер продавца. Второй пример сложнее: теперь у нас есть 2 продавца, каждый со своим объявлением. В момент t1 покупатель запрашивает номер телефона на объявлении 1, в результате чего к реальному номеру продавца 1 прикрепляется виртуальный номер из пула. В момент t2 время переадресации заканчивается, и закреплённый номер становится неактивным.  Пусть в момент t3 покупатель запрашивает номер телефона на объявлении 2, которое принадлежит другому продавцу из того же региона РФ, что и у продавца с объявлением 1. Так как регионы совпадают, виртуальные номера для них берутся из одного пула. Единственный виртуальный номер из текущего пула в этот момент закреплен за объявлением 1, но, так как время переадресации для этого номера уже истекло, он переходит на объявление 2. С этого момента виртуальный номер переадресует вызовы на реальный номер продавца 2. В момент t4 время переадресации снова заканчивается, номер становится неактивным и остается закреплённым за вторым продавцом.  Стоит отметить, что если бы во втором примере время переадресации виртуального номера на объявление 1 ещё не истекло, когда произошёл запрос номера на объявлении 2, случилась бы ошибка отсутствия свободных номеров в пуле. Мы постоянно следим за тем, чтобы в пулах всегда было достаточное количество виртуальных номеров для обеспечения работы алгоритма. Для этого в моменты перехода номера от одного продавца к другому мы пишем временную метрику, которая вычисляется как: Здесь: Ti — вычисляемая метрика для i-ого региона (период обращения номеров). now() — время в момент перехода номера от одного продавца к другому. lastShowTime — время последнего показа на сайте виртуального номера, который переходит от одного продавца к другому. В качестве примера привожу график, построенный на основе этой метрики за несколько дней в регионе «Москва и Московская область»: Из графика можно сделать вывод о том, что, чем больше трафика (днём), тем меньше рассчитываемое время. Если временная метрика доходит до порогового значения в 2 часа, это признак того, что в пуле не хватает виртуальных номеров, и нужно их добавить. При переходе номера от одного продавца к другому мы ищем в пуле виртуальный номер, который дольше всех не показывался на сайте. Расскажу, как мы реализовали это технически. Когда алгоритм динамической защиты номера находился на стадии AB-теста, мы не хотели тратить много сил на разработку сложного решения и организовали своего рода очередь с приоритетом на базе PostgreSQL. У нас была таблица с основной информацией о привязке виртуальных номеров к объявлениям и колонка last_show_time, в которой хранилось время последнего показа виртуального номера на Авито.  В момент, когда нам требовалось перевести номер от одного продавца к другому, мы открывали транзакцию, в которой выполняли два запроса. Первый запрос искал в таблице виртуальный номер, который не показывался дольше всех на сайте: Второй запрос обновлял информацию о продавце, которому принадлежит полученный виртуальный номер телефона. Очевидно, что у этого подхода есть свои минусы. Во-первых, чем больше становилась таблица dynamic_protections, тем дольше выполнялся запрос из-за необходимости сортировки. Во-вторых, с небольшой вероятностью запрос возвращал виртуальный номер, который показывался на сайте совсем недавно, что было некорректно. Это было связано с особенностью работы блокировки строк и одновременной сортировки данных в запросе. Подробнее можно почитать в документации PostgreSQL в разделе “The Locking Clause”. Позже, когда алгоритм в ходе AB-теста доказал свою пригодность, мы переделали схему получения виртуального номера, который не показывался на сайте дольше всех. За основу взяли Redis и его структуру данных Sorted Set, которая, по сути, является очередью с приоритетом. Для каждого пула виртуальных номеров мы заводим собственную очередь с приоритетом. Наполнением очередей занимается отдельный воркер. Он запускается раз в несколько секунд и делает запрос в PostgreSQL с целью получить виртуальные номера, которые стали доступны для перехода другому пользователю с момента последнего запроса. После получения номеров мы кладём их в очереди в Redis, причём в качестве score — параметра, по которому происходит сортировка в очереди — используется поле last_show_time, приведённое в формат unix-time.  В момент, когда приходит запрос на получение номера от пользователя и возникает необходимость перехода номера от одного продавца к другому, мы идём в Redis в очередь нужного пула. Из очереди получаем номер, который дольше всех не показывался на сайте и обновляем по нему данные в PostgreSQL. В случае, если виртуальный номер с какого-то объявления уже попал в очередь и при этом какой-то покупатель решил посмотреть его до того, как номер ушёл другому пользователю, мы удаляем этот номер из очереди. Сценарий звонков на динамические защитные номера не сильно отличается от сценария из прототипа, но есть некоторые особенности.  Когда покупатель получает динамический защитный номер на карточке объявления, у него есть 2 часа с момента последнего показа номера на сайте, чтобы совершить первичный звонок продавцу. После того, как покупатель совершил первичный звонок по определённому номеру телефона и дозвонился до продавца, у него есть 14 дней с момента последнего звонка, чтобы по тому же виртуальному номеру дозвониться до того же продавца. Мы провели AB-тест алгоритма динамической защиты номера, который показал, что алгоритм уменьшает метрику подтверждённого телефонного мошенничества на 52%. Это отличный результат, в связи с чем было принято решение масштабировать алгоритм на всех пользователей Авито. Масштабирование происходило поэтапно: каждые несколько дней мы включали алгоритм в нескольких новых регионах РФ и наблюдали за основными техническими и продуктовыми метриками. Параллельно проводили нагрузочные тестирования с предполагаемой нагрузкой, чтобы доказать, что наши сервисы её выдержат. В результате в начале февраля 2022 года алгоритм был раскатан на всех частных пользователей Авито! Мы гордимся нашим решением, так как мы первые в России, кому удалось закрыть защитными номерами такое большое количество объявлений и пользователей. Теперь мы замыкаем коммуникации между покупателями и частными продавцами внутри Авито и делаем его ещё безопаснее.  Software Engineer