Робимо простий і надійний микросервис розсилки пушей на компонентах AWS
Всім привіт! Я — Андрій Товстоног, DevOps Engineer в компанії Genesis. У статті поділюся досвідом побудови маленького микросервиса з використанням бессерверной архітектури AWS. Також розповім, як працюють push-повідомлення і з якими проблемами ми зіткнулися при реалізації цього рішення.
Зараз саме час сказати: «Друже, та ти чого, навіщо розбиратися з цією лямбдой і правами доступу для неї! Адже простіше підняти інстанси і кроном смикати скрипт ну або закинути все в docker, щоб піднімався, відпрацьовував і схлопывался».
Не можу не погодитися: таке рішення має право на життя. Але це додає роботи. Піднятий інстанси потрібно моніторити і в разі чого «гасити пожежі». А, як відомо, інженерів завжди напружує, коли щось йде не так. Та й просто хотілося «помацати» такі сервіси, як Lambda і DynamoDB, нехай навіть такий невеликий, але дуже цікавої задачі. Поїхали.
Постановка задачі
Ми працюємо в сфері медіа, і нам важливо здійснювати розсилку повідомлень, щоб тримати зацікавлену аудиторію в курсі подій. До цього завдання у нас були відправки тільки ручних push'їй, тобто редактори завжди самі здійснювали розсилку якийсь новини. Трохи поміркувавши, ми прикинули, що було б дуже непогано зробити автоматичну відправку повідомлень, грунтуючись на топі читаних статей. Та ні, ручні push'і ніхто не скасовував, вони як і раніше залишаються :)
Окей, ідея є — тепер сформулюємо задачу.
Необхідно за розкладом виконувати розсилку push-повідомлень користувачам. Для цього будемо використовувати сервіс аналітики (Analytics Service), який може віддавати по API топ-статті за кількістю переглядів за певний період. На підставі отриманої інформації формувати тіло push'а й відправляти все це добро на API сервісу відправки push'їй (Push Service).
Як працюють push-повідомлення
Перед тим як приступити до виконання завдання, пропоную розглянути в загальних рисах, як працюють push-повідомлення. Почнемо з невеликої схеми.
Рис. 1. Приклад роботи push-нотифікації (на схемі є Apple Push Notification service, але все опис далі буде стосуватися Firebase Cloud Messaging — у Apple принцип роботи схожий)
Отже, що ми маємо:
- Push Service— сервіс, який відповідає за відправку push-повідомлень. Це красива, функціональна обгортка (стандартизований API) над FCM (Firebase Cloud Messaging)/APNs (Apple Push Notification service), яка надає сервіс угруповання користувачів (наприклад, ми можемо відправити push навіть одній конкретній людині, провести A/B-тестування, переглянути інформативні звіти і статистику). Цей сервіс дозволяє також не морочитися з форматами push-повідомлень, так як він сам цим займається, а з нашої сторони потрібно всього лише дати йому дані (payload).
- FCM— це Firebase Cloud Messaging (далі — FCM), сервіс, який, власне, і здійснює безпосередню відсилання push-повідомлень користувачам, використовуючи унікальні ідентифікатори. Тут може виникнути законне питання: а яким чином FCM знає, як доставити push-повідомлення? Вся справа в тому, що браузер (User Agent) тримає перманентне TCP-з'єднання (одне для всього User Agent) з серверами Google, які є частиною FCM. Це і пояснює, яким чином push доставляється практично моментально після відправки. Взагалі, концептуально FCM складається з двох частин: FCM backend і application server.
- FCM backend— це балансировщики навантаження, а також сервери, які виконують доставку повідомлень клієнту. А ось і відповідь на те, чому вони виконують і роль балансування навантаження. Вони посилають клієнту спеціальні службові повідомлення про те, що необхідно змінити connection-сервер, а це означає, що поточне з'єднання розривається і встановлюється нове; так і проводиться балансування.
- Application server— це і є Push Service, тобто частина, що відповідає за передачу повідомлення нашого додатком через FCM backend.
- User Agent— це наш браузер.
- Service Worker(див. рис. 2) — це JavaScript-файл, який браузер запускає в бекграунді, окремо від веб-сторінки, відкриваючи доступ до фічами, які не вимагають запущеної сторінки сайту або будь-яких дій з боку користувача, а також відповідає за взаємодію c Browser APIs, такі як Push API і Notification API. Ця штука є певним проксі, перебуваючи між веб-сайтом, мережею і кешем. Так-так, саме він керує тим, сходити нам у кеш або зробити запит до сервера. Він і дозволяє перехоплювати події і виконувати якісь дії на них. Можна ще відмітити, що він працює не постійно, тобто засинає, коли не використовується, і відновлює свою роботу, коли відбувається якась подія, на яку він підписаний, в нашому випадку це push event.
- Push API— це web API для керування підпискою на push-повідомлення від Push Service, а також для обробки push-повідомлень. Завдяки Push API Service Worker отримує можливість обробляти подія onpush.
- Notification API— відповідає за показ повідомлення користувачу.
Ще кілька слів про API.
Рис. 2. Так працює Service Worker
API в контексті нашої теми діляться на два виду:
- API браузера;
- сторонні API.
Також вони мають одну загальну назву — це Web API, які покликані полегшити життя програмістам.
Так от, Push API і Notification API відносяться до API браузера і являють собою конструкції (набір об'єктів, функцій, властивостей та констант), побудовані на основі мови JavaScript.
Тепер, коли ми розібралися з яких елементів складається процес відсилання push-повідомлення, можемо описати весь робочий процес (workflow).
Звідки ж береться Service Worker
Перш ніж Service Worker зможе приступити до виконання своєї роботи, він повинен пройти певний життєвий цикл, який складається з 4 кроків.
Рис. 3. Життєвий цикл Service Worker
- Реєстрація. Відбувається один раз при першому зверненні до сайту.
- Завантаження. Виконується при першому зверненні до сайту і повторно через певні проміжки часу для того, щоб запобігти використання старої версії.
- Установка. У разі першого завантаження буде проведена установка, а в разі повторного завантаження виконується операція побайтового порівняння і, якщо є відмінності, проводиться його оновлення.
- Активація.
Service Worker готовий до роботи — поїхали далі!
Підписуємося на push-повідомлення
Тут можна відзначити два кроки:
- Запит дозволу на відображення push-повідомлень. Це саме те настирливе вікно, яке спливає зліва вгорі з двома кнопками — Block і Allow, коли ми заходимо на сайт в перший раз і на сайті підключені push-повідомлення.
- Передплата на push-повідомлення (Push Subscription).
Підписка містить всю інформацію, яка необхідна для відправки повідомлення користувачу. З боку Push Service це виглядає як унікальний ідентифікатор пристрою — ID. Далі це все добро (Push Subscription) відправляється на наш Push Service, де зберігається в базі для подальшої відправки push-повідомлень зареєстрованому користувачеві.
За ці всі процедури відповідає той самий згаданий вище Push API.
Відправка push'а
Відправка push-повідомлення полягає в тригері API нашого Push Service. Цей виклик повинен містити інформацію, яку ми повинні показати користувачеві (payload), і групу користувачів, яким цей push буде відправлений. Після того як ми зробили API-виклик, Push Service сформує правильний формат для браузера і віддасть його в FCM, який поставить повідомлення в чергу і буде чекати, коли User Agent з'явиться в мережі.
Але push-повідомлення не може жити в черзі вічно, тому у Push Service є опція TTL (Time to Live), або час життя попередження, після якого повідомлення буде видалено :)
Отримання push-повідомлення користувачем
Після того як Push Service відправив push-повідомлення, FCM доставить (ну або не доставить, якщо щось пішло не так) його в браузер, останній створить таку штуку, як Push Event, на який відреагує Service Worker і запустить обробку push-повідомлення.
Ось ми і дізналися в загальних рисах, як працюють push'і, так що тепер можемо приступити до виконання завдання.
Реалізація
Як завжди, у правильних інженерів всі веселощі починається з планування — ми нічим не гірші :) Тому:
Рис. 4. Схема микросервиса
Склад нашого стека:
- AWS Lambda Function — власне центр нашого микросервиса, виконує написаний в нашому випадку на Python код. Ця штука сама виділяє ресурси, які необхідні для обчислення коду, і сплачується лише за фактичний час роботи та за споживану пам'ять. Якщо вірити опису документації AWS, то хмара навіть гарантує високу доступність сервісу, що, звичайно ж, не може не радувати.
- AWS DynamoDB — це база даних пар «ключ — значення» та документів, що забезпечує затримку менше 10 мс при роботі в будь-якому масштабі. Це повністю керована база даних, яка працює в декількох регіонах з кількома провідними серверами і володіє вбудованими засобами безпеки, резервного копіювання і відновлення, а також кешування пам'яті для інтернет-додатків. DynamoDB може обробляти більше 10 трлн запитів в день з можливістю обробки пікових навантажень — понад 20 млн запитів в секунду. У процесі формування і відправки push-повідомлення на Push Service виконується перевірка, чи push-повідомлення з цим ID вже відправлено чи ні, а ці дані дістаються з DynamoDB і пишуться в ній.
- AWS CloudWatch — в нашому випадку «двоголовий змій», який виконує роль лог-сервісу, записуючого лог-виконання Lambda і Event rule, який триггерит функцію за вказаною часу, фактично виконуючи роль планувальника (cron).
- Analytics service — сервіс з API, в якому «відбувається магія», і в результаті ми можемо діставати топ-статті, наприклад за кількістю переглядів.
- Push Service — сервіс, що дозволяє відправляти наші push'в.
- Slack — старий добрий «слак», куди ж без нього, який робимо нотифікацію по відправці.
Отже, починаємо з того, що пишемо код для Lambda-функції. Особливість його в тому, що необхідно визначити вхідну точку, так як Lambda-функція викликає функцію (2) всередині Lambda-функції, яка оголошується як Handler (1) :)
1) Handler — це і є вхідна точка, яку і викликає Lambda. Назва Handler повинно збігатися з ім'ям функції в коді; 2) ім'я функції, яка буде вхідною точкою в Lambda; 3) Event та Context — вбудовані параметри, які передаються викликається функції
Тут виникає законне питання: а що ж таке ці змінні, які ми передаємо функції?
Документація говорить вичерпно:
Event — цей параметр використовується для передачі даних про події обробникові. У нашому випадку ми дістаємо з поля event тіло відповіді від API Analytics Service. Прилітає воно у форматі json.
Context — передає контекст об'єкта обробникові. Цей об'єкт надає методи і властивості, які надають інформацію про виклик, функції та середовищі виконання. Перейшовши по посиланню , можна побачити методи і властивості, а також тут доступний приклад логування інформації контексту. З контексту ми беремо request_id, який використовується для того, щоб Lambda не запускалася кілька разів.
А ось як це все працює під капотом. CloudWatch event rule за розкладом триггерит Lambda-функцію, вона в свою чергу виконує код, виконує запит/оновлення даних в DynamoDB, записує логи через CloudWatch і робить повідомлення у Slack.
Нижче представлена блок-схема, яка описує алгоритм роботи сервісу.
Рис. 5. Алгоритм роботи микросервиса
Робота скрипта починається з того, що він робить запит в сервіс аналітики (Analytics Service) і дістає 100 топ-статей. Чому саме 100? Це пов'язано з поддоменами, так як на кожен піддомен повинна відправлятися стаття, опублікована саме на ньому, наприклад example.com і subdomain.example.com. На example.com повинна піти стаття, опублікована на example.com, а на subdomain.example.com — опублікована на піддомені subdomain. Так як піддомен subdomain є більш специфічним або конкретизованим, то статті на ньому з'являються рідше. А якщо зовсім просто, то Analytics Service API не вміє повертати список статей для конкретного поддоменного імені, а тільки для домену цілком. От якось так :)
Далі виконуємо перевірку, був push з таким article_id відправлений чи ні. Для цього ми викликаємо функцію, яка дістає записи з DynamoDB, порівнює і оновлює запису в DynamoDB і в підсумку повертає нам значення змінної uniq.
Після цього виконується відправка push-повідомлення користувачам з унікальною статтею.
Проблеми, з якими зіткнулися
А тепер про проблеми, з якими ми зіткнулися під час реалізації цього сервісу.
Спочатку скрипт представляв собою, умовно кажучи, 5 рядків:
- брали одну топ-статтю з сервісу аналітики;
- формували json;
- відправляли на Push Service.
Перша проблема полягала в повторної відправки push-повідомлення користувачам, так як стаття могла триматися в топі цілий день. Але цікаво в цій ситуації було те, що на повторний push реагувало більше користувачів :)
Ми вирішили цю проблему, підключивши в нашу логіку DynamoDB для запису відправленого article_id. Після маленьких правок ми стали діставати з сервісу аналітики по 5 топ-статей і порівнювали article_id c записом article_id з бази; якщо повторювався, брали наступну статтю з топ-вибірки, оновлювали запис в базі і відправляли push.
Перевіряємо, політ нормальний, але недовгий :)
Наздогнала нас проблема, що push'і почали повторюватися через 1-2 відправлення (ну воно й логічно :)), так як у нас в базі лежить тільки один ID статті і він перезаписывался — невеликий прорахунок в архітектурі сервісу.
Тому наступним кроком стало те, що ми почали створювати масив елементів, що складається з article_id, і записувати його в базу DynamoDB. Довжину масиву вирішили визначити рівну 5 — цього більш ніж достатньо, але в разі потреби завжди можна збільшити. Перевіряємо, політ нормальний, але недовгий, хоча довше, ніж у попередньому випадку.
Наступна проблема — це перезапуск Lambda-функції, що тягло за собою відправку 3 push-повідомлень з інтервалом в 1 хв. Це відбувалося, коли сервіс відправки push'їй відвалювався по тайм-ауту і Lambda-функція вважала, що вона завалилася, і запускалася повторно. Це, як виявилося, поведінка для Lambda-функції, яка працює в асинхронному режимі, і, якщо при виконанні функції вона поверне помилку, Lambda спробує виконати її ще раз. За замовчуванням Lambda буде пробувати додатково 2 рази з інтервалом в 1 хв.
Вирішили цю проблему додаванням в DynamoDB такого поля, як request_id. При кожному новому запуску Lambda генерує унікальний request_id (він не змінюється при перезапуску функції), який ми і витягаємо з context. Перевірку унікальності request_id виконуємо перед перевіркою article_id, а оновлюємо його кожен раз, коли article_id унікальний. У підсумку ми обриваємо повторне виконання функції, якщо такі спроби з'являються.
Наступне питання може стати таким: «Так якщо Push Service відвалюється по тайм-ауту, то як ви розумієте, пішов push чи ні?». І це теж дуже правильне запитання. Насправді протягом усього польоту «не було жодного розриву» :) тобто push відправлявся завжди, навіть якщо ми не отримували жодної відповіді від API Push Service. Про це говорить статистика відправки в адміністративній панелі Push Service, а також повідомлення Slack.
Підводячи підсумки
Використовуючи частини вельми цікавого стека бессерверной архітектури AWS, вийшло зробити вельми придатний микросервис, що працює так само надійно, як пружина від дивана. Це рішення виконує корисну функцію, яка сприяє розвитку бізнесу, і позбавляє нас від деяких невеликих проблем, пов'язаних з підтримкою такого ж рішення в себе. І найцікавіше те, що плата за все це становить менше 1 $ на місяць.
Опубліковано: 03/05/19 @ 07:00
Розділ seo Сервіси
Рекомендуємо:
DOU Books: 5 корисних книг, які ви, швидше за все, не читали, від Олексія Орапа, CEO YouScan
Що змінилося в Google в березні 2019?
Виклики лідера на шляху до команди мрії
Досвід роботи з контент-біржею WorkHard
Python дайджест #20: Iodide - науковий Python-стек в браузері