Ifood е бразилска компания за хранителни технологии, която доставя повече от 1 милион поръчки на ден и раства с около 110% всяка година. Като хранителен технолог, часовете за надникване в платформата са предимно около обяд и вечеря и стават още по-високи през уикендите.

В някои специални дни (напр.: поради маркетингови кампании) бием последния рекорд и виждаме, че платформата получава най-високото си представяне за всички времена. Миналият 12 юни беше този ден. Видяхме, че една микроуслуга достига 2 милиона заявки в минута.

Някаква предистория

Работя в екипа за акаунт и идентичност в областта на платформения инженеринг в Ifood от около година и половина. Това беше доста пътуване и от време на време се сблъскваме с много предизвикателства поради бързото разрастване на компанията. Когато проектираме нови решения, винаги трябва да имаме предвид идеята, че за няколко месеца използването на системата ще нарасне 2 или 3 пъти.

Историята, която ще разкажа днес, е пример за горния случай. Системата е разработена около 2018 г., когато компанията е доставяла 13 милиона поръчки на месец. В днешно време те са над 30 милиона. Това не винаги е вярно, но в този случай използването на системата е нараснало със същата част от растежа на компанията, а по-късно започна да расте още по-агресивно.

Вътрешно наричаме тази микроуслуга метаданни на акаунта. Въпреки че това е нещо като родово име, то също така обяснява какво прави услугата: тя се занимава с метаданните на акаунтите. Какво представляват метаданните на акаунта? Е, най-вече това, което не е основна/основна информация за клиента. Да ви дам няколко примера: ако потребителят предпочита да получава известия чрез SMS или имейл, любимите видове храни (като хамбургер, паста, японска храна и т.н.), някои флагове за функции, брой поръчки, направени за този потребител и т.н. Обичайно е събирането на данни от различни места и лесното обслужване на мобилното приложение, но също и на други микроуслуги, така че те просто трябва да извикат една микроуслуга вместо десет.

Още през 2018 г. метаданните на акаунта бяха създадени главно за поставяне на произволна (и не толкова използвана) информация, която, честно казано, нямаше друго място за поставяне. Имахме нужда от малко структура и мощност на заявките и трябваше да е лесно да го мащабираме, така че избрахме осигурения DynamoDB от AWS. Само за да стане ясно тук, ние сме наясно, че системата може да расте, освен това компанията вече беше доста голяма и средното натоварване беше предизвикателство. Въпреки това, нямаше начин да разберем, че ще преминем от 10 000 заявки в минута (rpm) на 200 000 и след това на 2 MM об/мин.

След първото издание на тази микроуслуга нямаше много използване (в сравнение с други микроуслуги в екипа за акаунт и самоличност). Въпреки това, няколко седмици по-късно и някои бизнес решения бяха взети, системата стана много важна и това щеше да бъде едно от първите обаждания, които мобилното приложение ще направи, за да получи цялата тази информация за клиента.

Няколко месеца след това решение други екипи започнаха да виждат метаданните на акаунта като хубаво място за поставяне на информация, която е разделена на няколко места и на която трудно може да се разчита. Освен това започнахме да създаваме повече агрегации, които биха направили живота на други микроуслуги наистина по-лесен, увеличавайки важността на системата и това просто разпространи нейната популярност и значение в други екипи. Сега метаданните на акаунта се извикват всеки път, когато потребител отвори приложението и от много екипи в много различни контексти.

И това е много, много много кратко резюме на случилото се и как системата стана толкова важна от 2018 г. досега. През този период екипът (аз плюс осем наистина брилянтни човека, с които имам голям късмет да работя) работи активно по него, но не само. Все още поправяме, разработваме и поддържаме другите десет микроуслуги, които притежаваме.

Направихме куп промени и да опишем всички сценарии, през които преминахме, ще отнеме твърде много време, така че пиша под текущата архитектура, която трябва да можемда доставяме здравословно 2 MM заявкив минута. И накрая, време е да се потопите в техническата част.

Гмуркане в техническата част

Както казах, тази микроуслуга съхранява метаданни за клиента. В базата данни ние разделяме тези метаданни на различни контексти (или както нарекохме в кода: именни пространства). Клиент (customer_id като разделителен ключ) може да има едно до N пространства от имена (като ключ за сортиране) и всяко от тях има фиксирана и твърда схема, която се дефинира и проверява (преди вмъкване) от jsonschema. С това можем да се уверим, че обаче вмъкването на данни в пространство от имена (повече подробности за това по-късно) ще спазва неговата схема и правилното му използване.

Използвахме този подход, защото четенето и записът в тази система се извършват от наистина различни области на компанията.

Вмъкването се извършва от екипа за наука за данни, тъй като те ежедневно експортират милиони записи от своите вътрешни инструменти към тази микроуслуга, като я извикват чрез API, разделяйки тези милиони записи на партиди от ~ 500 елемента. И така, в даден момент от деня тази микроуслуга получава милиони обаждания (в интервал от 10 до 20 минути), за да вмъкне данни в DynamoDB. Ако партидният API, който получава ~ 500 елемента, ги запише директно в базата данни, може да имаме някои проблеми с мащабирането на Dynamo, а също така може да е трудно да поддържаме бавно време за реакция. Начин за коригиране на това затруднение би било екипът за данни да записва своите данни директно в базата данни, но ние трябва да проверим дали елементите спазват jsonschema, дефинирана за пространството от имена, което ще се съхранява и това е отговорност на микроуслугата.

Така че решението беше, че този API ще получи партидата от елементи и ще ги публикува в SNS/SQS, които ще бъдат използвани от друга част на приложението, която след това ще потвърди елемента и ако е добре, ще го запише в Dynamo. С този подход крайната точка, която получава пакета от елементи, може да отговори много бързо и ние можем да направим записа, без да разчитаме на HTTP връзката (това е много важно, защото комуникацията с Dynamo може да се провали и опитайте отново може да намали времето за отговор на HTTP наистина бавно). Друго предимство е, че можем да контролираме колко бързо/бавно искаме да четем данните от SQS и да ги записваме на Dynamo, като контролираме потребителите.

Извън този работен процес метаданните на акаунта също се извикват от друга услуга, която го извиква всеки път, когато се получи поръчка от платформата, за да актуализира информация относно тази поръчка. Като се има предвид, че Ifood прави повече от 1 милион поръчки на ден, микроуслугите също получават това количество обаждания.

Въпреки че има много тежък процес на запис върху него, 95% от натоварването идва от извиквания на API за четене на данни. Както казах, записът и данните се извършват от много различни екипи, а разговорите за четене се извършват от много, много екипи и мобилни приложения. За наш късмет от тази микроуслуга се изисква много повече да чете данни, отколкото да ги записва, така че е малко по-лесно да я мащабирате. Тъй като всяка система, която чете много данни, се нуждае от кеш, това го прави и вместо да използва Redis или нещо подобно, AWS предоставя DAX като „донякъде вграден“ кеш за DynamoDB. За да го използвате, просто трябва да смените клиента и да разберете забавянето на репликацията, което може да се случи при различните операции на заявки.

При това количество обаждания е съвсем нормално да получаваме някаква нередност. В нашия случай започнахме да виждаме някои заявки в Dynamo, които отнемат повече от 2 или 3 секунди, когато 99,99% от обажданията бяха под 17 ms. Въпреки че са само няколко хиляди на ден, бихме искали да предоставим по-добро SLA за екипите. Така че решихме да направим повторен опит, ако получим таймаут от Dynamo. Също така говорихме с екипи, така че те да конфигурират нисък таймаут, когато извикват нашите API. По подразбиране за по-голямата част от техния HTTP клиент беше 2s, така че променихме на ~100ms. Ако получат изчакване (да кажем, че микроуслугата направи повторен опит за динамо, но отново не успя), те могат да опитат отново и много вероятно ще получат отговор.

За да го внедрим, ние използваме k8s (достигайки около 70 подове) и го мащабираме с нарастването на заявките в секунда. DynamoDB е зададен като осигурен вместо при поискване.

Важна стъпка, за да се уверим, че системата ще може да работи здравословно в случаи на наистина висока производителност, ние провеждаме тест за натоварване/стрес срещу нея всеки ден, за да сме сигурни, че внедряванията от деня по-горе не са влошили производителността и че нещата все още са наред и работят добре. С резултатите от този тест за натоварване бихме могли да проследим дали дадена крайна точка се подобрява или влошава с времето и нейното развитие.

С времето тази микроуслуга стана доста важна за някои екипи и това е проблем, ако по някаква причина системата падне. За да коригираме това, молим екипите да извикат микроуслугата през Kong (нашият API шлюз) и да конфигурират резервен вариант там. Така че, ако микроуслугата не работи или върне 500, Kong ще активира резервния вариант и клиентът ще получи отговор. Резервният вариант в този случай е S3 контейнер с копие на данните, които системата ще предостави. Може да е остаряло, но това е по-добре, отколкото изобщо да нямате отговор.

И това е, накратко, как работи. Има и някои други работни процеси в него, но нищо толкова уместно като тези, които казах.

Следващите стъпки все още не са много ясни за екипа. Използването на микроуслугата може да нарасне още повече и можем да достигнем точка, в която започва да става все по-трудно и по-трудно да я мащабираме. Алтернатива може да бъде да го разделите на различни микроуслуги (може би дори с различни бази данни) или да обедините повече данни, за да ги обслужвате по-добре. Във всеки случай ще продължим да правим тестове, да намираме тесните места и да се опитваме да ги коригираме.