Expert JS React Developers for TUI wanted. Join Ciklum and get a $4000 sign-on bonus!
×Закрыть

Наша serverless story. Як ми створили generic-рішення завдяки сервісам Azure

Мене звати Ілля Чуйков, я Cloud Dev/DevOps Engineer у VISEO. Наша компанія працює за аутстафінговою моделлю, надає послуги своїх IT-спеціалістів різним організаціям.

У статті я розповім, як ми збудували рішення із serverless-архітектурою, яке завдяки сервісам Azure зекономило ресурси на його розробку та підтримку. Воно орієнтоване на збереження єдиної бази коду та управління модифікаціями проєкту лише через конфігураційні змінні. Я сподіваюсь, що матеріал буде корисний усім, хто розпочинає працювати або вже давно працює з Azure Functions та іншими ресурсами Azure. Також буду дуже радий почути ваші ідеї та коментарі щодо можливих удосконалень у нашому рішенні.

Проєкт і команда

Спершу розповім трохи про проєкт. Наш клієнт — одна з найбільших транспортних компаній Франції, що займається різними видами пасажирських перевезень. Компанія має матричну структуру, тобто в неї входять досить незалежні у своїх діях філіали, а головний офіс надає їм усе необхідне, зокрема програмне забезпечення.

Сам проєкт розпочався майже три роки тому, я ж долучився до нього понад рік тому як бекенд-розробник. Тоді якраз закінчилися підготовчі моменти та розпочалася активна трансформація рішення, були зроблені перші кроки для міграції в cloud, сформована нова бекенд-команда.

Наша команда складається з трьох бекенд-розробників та одного архітектора, ми співпрацюємо з багатьма іншими командами, що пишуть мобільні та вебзастосунки для кінцевих користувачів, водіїв, аналітиків, маркетологів, працівників підтримки. Але усі ці програми використовують наш back-end. Крім того, ми самостійно займаємося усіма активностями, пов’язаними з деплойментом рішень, тому кожен учасник розуміється не лише в розробці, а й у процесах, пов’язаних з девопсом.

Проблеми

Аналізуючи ситуацію на проєкті, ми виділили суттєві проблеми як з технічного погляду, так і з погляду бізнесу. Клієнт обмежений у фінансових ресурсах, особливо останнім часом через зрозумілі всім причини. Висока конкуренція на ринку зумовлює потребу у швидких та частих релізах, постійні зміни у вподобаннях користувачів змушують бізнес все частіше запускати нові продукти та оновлювати старі.

Кожен з філіалів компанії може самостійно обирати партнерів, тому використовує у роботі різноманітні сервіси оплати, побудови оптимальних маршрутів, надсилання повідомлень, геолокації тощо. З усіма ними ми зобов’язані інтегруватися, підтримуючи різні схеми взаємодії.

Серед проблем, які належать до розряду технічних, особливо виділялися три «кити», на яких була збудована стара система:

  • величезний шматок legacy-коду з монолітною архітектурою, який здебільшого написаний близько 15 років тому;
  • велика кількість on-premises ресурсів, що коштували великих грошей для бізнесу та мали безліч проблем у підтриманні;
  • найрізноманітніші джерела даних (навіть файли Excel), що зберігали несистематизовану розрізнену інформацію про користувачів та робили всі зусилля команди маркетингу марними.

З-поміж інших проблем: відсутність чіткої та налагодженої системи взаємодії між командами, майже повна відсутність автоматизації процесів підтримки якості та DevOps, низький рівень безпеки рішень.

Отже, перед нами постало велике завдання. З одного боку, необхідно було мігрувати старий проєкт у хмару, модернізувати його код і побудувати чіткі процеси взаємодії з іншими командами. З іншого — постійно підтримувати амбіційні плани щодо релізу нових рішень для чималої кількості філіалів, кожен з яких має свої потреби. І водночас дотримуватися закладеного бюджету.

Архітектура та інструменти

Щоб модифікувати старе рішення, не відкладаючи надовго реліз нових проєктів, ми використали Strangler application pattern. Суть патерну полягає в тому, що наявне монолітне рішення має бути оточене новими API, що дасть змогу клієнтським застосункам використовувати функціонал моноліту вже не прямо, а за допомогою цих API.

Поступово функціонал моноліту має бути замінений новим, проте для клієнтських програм це не матиме жодних наслідків. Створивши нові API, ми значно полегшили працю фронтенд-команд, оскільки вони змогли використовувати нові RESTful API замість SOAP. Великий плюс для нас — можливість відкласти переписування старого коду і зосередити всю увагу на новому функціоналі та архітектурі generic-рішення.

Також вирішили максимально використовувати нативні хмарні сервіси. Побудували generic-рішення, в основі якого Azure Functions. Azure Functions — це хмарний обчислювальний сервіс, що дозволяє виконувати код, ініційований подіями, без попереднього налаштування середовища. Щоб знайти більш докладну інформацію, раджу користуватися офіційною документацією від Microsoft.

Нижче наведено приклад архітектури одного з наших солюшенів, що є досить типовим:

Переваги рішення

Низька вартість. Насамперед нас цікавило фінансове питання. Використовуючи Azure Functions, ми платимо лише за час їх виконання, а Azure надає на кожен місяць 2500 хвилин безкоштовно. Цього часу цілком достатньо для більшої частини сервісів невеликих філіалів клієнта.

Scale та monitoring. Крім того, навантаження на наші сервіси нерівномірне, а завдяки serverless-рішенню ми переносимо всі зусилля щодо scale та monitoring на хмарний провайдер і не переплачуємо за ресурси, які нам не потрібні.

Bindings та triggers. Функції мають вже вбудовані зв’язки практично з усіма сервісами Azure (Service Bus, Notification Hub, Cosmos DB тощо), які можуть слугувати як вхідними, так і вихідними даними. Функція має різноманітні тригери — елементи, що викликають її старт, найчастіше вони мають інформацію, що використовується під час її роботи. Такими тригерами можуть бути http-виклики, нові повідомлення в черзі, таймер тощо.

Докладніше про види тригерів і зв’язок ви можете дізнатися тут. Усе це допомагає побудувати гнучке рішення, при цьому зекономити зусилля для написання коду з нуля.

Недоліки рішення

Єдиною вагомою проблемою нашого рішення, яку ми виділили для себе, є значна залежність від конкретного клауд провайдеру. Наше рішення буде дуже важко мігрувати, якщо виникне така необхідність. Проте ми вирішили погодитися на цей ризик, натомість отримавши наведені вище переваги.

Також досить загальною проблемою при використанні Azure-функцій є проблема, що притаманна усім serverless-рішенням — холодний старт. Це час, необхідний провайдеру для налаштування середовища для виконання функції під час першого звернення. У нашому випадку цей час не є чимось критичним. За потреби, ми зможемо використати більш дорогий сервіс-план, який значно знизить цей час.

Dependency injection в Azure Functions

Значним поштовхом для використання Azure Functions у великих проєктах стало впровадження механізму Dependency injection (DI), починаючи з версії 2.0. Dependency injection дає змогу створювати залежні об’єкти поза класом і надає їх класу різними способами. До переваг використання DI належать можливості повторного використання коду, краща читабельність і тестованість, а також зменшення кількості залежностей.

Я не можу навести приклади коду з реального проєкту, тому створив невеликий демопроєкт, де містяться основні ідеї. Усі подальші приклади я буду брати з нього.

Використання DI у функціях майже ідентичне до використання у стандартних .NET Core проєктах, необхідно додати у проєкт пакунки Microsoft.Azure.Functions.Extensions та Microsoft.NET.Sdk.Functions від 1.0.28 версії. Тепер, наслідуючи FunctionStartup, можемо перевизначити метод Configure:

    /// <summary>
    /// Contains configuration method to register services.
    /// </summary>
    internal class Startup : FunctionsStartup
    {
        /// <inheritdoc/>
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddLogging();
 
            builder.Services.Configure<ServiceBusOptions>(opts =>
            {
                opts.ConnectionString = Environment.GetEnvironmentVariable("ServiceBusConnection");
                opts.ListenConnectionString = Environment.GetEnvironmentVariable("ServiceBusConnection");
                opts.TimeToLive = Environment.GetEnvironmentVariable("TimeToLive");
            });
 
            builder.Services.AddServiceBusHelper();
 
            builder.Services.AddTransient<ISenderService<SmsDto>, SmsSenderService>();
            builder.Services.AddTransient<ISenderService<SendGridMessage>, EmailSenderService>();
 
            builder.Services.AddTransient<IConfigurationHelper, ConfigurationHelper>();
 
            builder.Services.AddTransient<INotificator, SmsNotificator>();
            builder.Services.AddTransient<INotificator, EmailNotificator>();
            builder.Services.AddTransient<INotificationRequestParser, NotificationRequestParser>();
            builder.Services.AddTransient<INotificationTypeFactory, NotificationTypeFactory>();
        }
    }

Саме використання впровадження залежностей дозволило нам створити generic-рішення. На мою думку, ми розв’язали проблему простим та елегантним способом за допомогою фабрики.

Пропоную розглянути проєкт, що займається надсиланням різних електронних листів. Оскільки маємо багато клієнтських застосунків, складно чітко визначити кількість типів повідомлень. Тому вирішили створити єдине API, що має отримувати на вхід різні типи повідомлень і надсилати їх. При цьому легко додавати нові види листів.

Для цього створили фабрику, яка реєструє всі наявні сервіси, що перелічені в EmailTypeEnum:

 
    /// <summary>
    /// A factory to create the concrete email.
    /// </summary>
    public class EmailBuilderFactory : IEmailBuilderFactory
    {
        private readonly Dictionary<EmailTypeEnum, EmailAbstract> _factories;
 
        /// <summary>
        /// Initializes a new instance of the <see cref="EmailBuilderFactory"/> class.
        /// </summary>
        /// <param name="serviceProvider">The service provider to use.</param>
        public EmailBuilderFactory(IServiceProvider serviceProvider)
        {
            var emailTypes = (EmailAbstract[])serviceProvider.GetService(typeof(IEnumerable<EmailAbstract>));
            _factories = new Dictionary<EmailTypeEnum, EmailAbstract>();
            foreach (EmailTypeEnum emailType in Enum.GetValues(typeof(EmailTypeEnum)))
            {
                var type = Type.GetType($"Demo.AzureFunctions.Builder.{emailType}Email");
                var instance = emailTypes.FirstOrDefault(x => x.GetType() == type);
                if (instance == null)
                {
                    throw new ArgumentNullException($"The instance with {type} can not be null.");
                }
 
                _factories.Add(emailType, instance);
            }
        }
 
        /// <summary>
        /// Create an instance of IEmail.
        /// </summary>
        /// <param name="emailType">Email type.</param>
        /// <returns>The implementation of IEmail.</returns>
        public EmailAbstract Create(EmailTypeEnum emailType) => _factories[emailType];
    }

Тепер зареєструємо необхідні сервіси у Startup:

            builder.Services.AddTransient<IEmailBuilderFactory, EmailBuilderFactory>();
            builder.Services.AddTransient<EmailAbstract, AccountConfirmationEmail>();
            builder.Services.AddTransient<EmailAbstract, PersonalInfoModificationEmail>();
            builder.Services.AddTransient<EmailAbstract, AccountActivationEmail>();
            builder.Services.AddTransient<EmailAbstract, PasswordChangedEmail>();

Усі ці сервіси є досить типовими, ось приклад одного з них:

   /// <summary>
    /// Creates an account activation email from generic email data.
    /// </summary>
    public class AccountActivationEmail : EmailAbstract
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="AccountActivationEmail"/> class.
        /// </summary>
        /// <param name="configurationHelper">The configuration helper to work with config values.</param>
        public AccountActivationEmail(IConfigurationHelper configurationHelper)
            : base(configurationHelper)
        {
        }
 
        /// <inheritdoc/>
        public override SendGridMessage GetMessage(EmailDto emailDto)
        {
            var accountActivationEmail = emailDto.EmailData.ToObject<AccountActivationEmailModel>();
            var fromEmail = string.IsNullOrEmpty(emailDto.FromEmail) ? FromEmail : new EmailAddress(emailDto.FromEmail);
            var toEmail = string.IsNullOrEmpty(emailDto.ToEmail) ? ToEmail : new EmailAddress(emailDto.ToEmail);
 
            var data = new Dictionary<string, string>
            {
                { SendGridConstants.ActivationLinkSendGridProperty, accountActivationEmail.ActivationLink }
            };
 
            var message = MailHelper.CreateSingleTemplateEmail(
                fromEmail,
                toEmail,
                _configurationHelper.SendGridAccountActivationTemplateId(),
                data);
 
            return message;
        }
    }

Далі наведено приклад функції, що безпосередньо використовує наші сервіси:

    /// <summary>
    /// Contains function that sends the email to the user.
    /// </summary>
    public class HandleEmails
    {
        private readonly ISenderService<SendGridMessage> _emailSenderService;
        private readonly IEmailBuilderFactory _emailBuilderFactory;
 
        /// <summary>
        /// Initializes a new instance of the <see cref="HandleEmails"/> class.
        /// </summary>
        /// <param name="emailSenderService">The service to send emails.</param>
        /// <param name="emailBuilderFactory">The service to create the emails.</param>
        public HandleEmails(ISenderService<SendGridMessage> emailSenderService, IEmailBuilderFactory emailBuilderFactory)
        {
            _emailSenderService = emailSenderService;
            _emailBuilderFactory = emailBuilderFactory;
        }
 
        /// <summary>
        /// Sends the email from the Service Bus queue.
        /// </summary>
        /// <param name="queueMessage">The email from the queque.</param>
        /// <param name="logger">The logger to log info messages.</param>
        /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
        [FunctionName("HandleEmails")]
        public async Task RunAsync(
            [ServiceBusTrigger("%emailQueueName%", Connection = "ServiceBusConnection")]string queueMessage,
            ILogger logger)
        {
            logger.LogInformation($"{nameof(HandleEmails)} function starts.");
            logger.LogInformation($"Email to send as JSON: {queueMessage}.");
 
            var emailDto = JsonConvert.DeserializeObject<EmailDto>(queueMessage);
 
            var email = _emailBuilderFactory
                .Create(emailDto.Type)
                .GetMessage(emailDto);
 
            await _emailSenderService.SendAsync(email);
 
            logger.LogInformation($"{nameof(HandleEmails)} function ends.");
        }
    }

Тригером, що запускає функцію, є нове повідомлення у черзі. Визначивши його тип, фабрика створює необхідний інстанс, що відповідає конкретному типу листа. Ми отримуємо його контент, який далі надсилає сервіс, що використовує Sendgrid для роботи з email-листуванням. Схожим чином побудовано усі наші рішення. Керуючи лише значеннями, доступними в EmailTypeEnum, ми можемо визначати, які з листів доступні конкретному філіалу, при цьому абсолютно не змінюючи код.

Деплоймент

Щоб автоматизувати процеси деплою наших рішень, зекономити час і зменшити ймовірність помилок, ми використали підхід «інфраструктура як код» за допомогою Azure Resource Manager (ARM) темплейтів. У цих темплейтах у форматі JSON описуються необхідна інфраструктура та конфігурація, що має бути задеплоєна. Для докладнішого ознайомлення скористайтесь посиланням.

Для того, щоб забезпечити незалежний деплоймент кожного проєкту окремо, залежно від потреб певного філіалу, ARM-темплейт для кожного рішення містить інструкції для деплойменту всіх необхідних ресурсів.

Щоб гарантувати високий рівень безпеки та значно спростити взаємне використання конфігураційних даних різними проєктами, ми обрали Azure Key Vault. Це хмарне рішення, що дає змогу надійно зберігати різноманітні секрети (строки підключення, ключі API тощо).

На цьому малюнку схематично представлено ідею, яка була покладена в основу Azure Key Vault. Таким чином навіть розробники не мають змоги отримати реальні значення, сховані в секретах сховища.

Проте ми мали вирішити проблему, пов’язану з деплойментом Azure Key Vault. Оскільки сховище містить дані, які є дуже цінними, і є, ймовірно, їх єдиним джерелом, операція його видалення реалізована як soft delete. Так є можливість ще протягом 90 днів відновити видалене сховище. Тому в темлейті є дві опції: create та recover. Але ми деплоїмо Key Vault з кожним проєктом, тому нам необхідно розуміти, сховище розміщується вперше чи воно вже було задеплоїне раніше. Інакше всі правила доступу, які було додано для інших функцій, будуть заміщуватися останньою функцію, для якої було виконано деплоймент.

Ми розв’язали цю проблему: додали наступну умову безпосередньо у темплейт і скрипт у релізі, що буде визначати значення цієї змінної. Це найкраще, що ми змогли реалізувати для повної автоматизації деплою.

  {
            "type": "Microsoft.KeyVault/vaults",
            "apiVersion": "[variables('keyVaultApiVersion')]",
            "name": "[variables('keyVaultName')]",
            "location": "[parameters('location')]",
            "tags":"[variables('resourceTags')]",
            "dependsOn": [
                "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]"
            ],
            "properties": {
                "enabledForDeployment": "true",
                "enabledForDiskEncryption": "false",
                "enabledForTemplateDeployment": "false",
                "createMode": "[if(parameters('firstDeploy'), 'create', 'recover')]",
                "tenantId": "[subscription().tenantId]",
                "sku": {
                   "name": "Standard",
                   "family": "A"
                },
                "accessPolicies": "[if(parameters('firstDeploy'), json('[]'), json('null'))]"
            },

Тестування

Щоб забезпечити якість розроблених рішень, ми активно працюємо над підтриманням рівня покриття коду юніт-тестами. Ми відстежуємо цей показник безпосередньо під час CI-процесу. Для написання тестів використовуємо загальновживані фреймворки — xUnit та Moq. Для того, щоб мати досить зручний звіт щодо покриття коду тестами, ми використовуємо cobertura.

Аби перевірити інші показники, такі як час відповіді функції, правильність даних у відповідях, використовуємо API тести, які автоматизували за допомогою newman, що запускає колекцію Postman-тестів у кінці релізу кваліфікаційних енвайронментів.

Нещодавно ми почали працювати над реалізацією безпекових тестів. Тести ж презентаційного рівня виконуються іншими командами.

Висновок

Ми змогли досягти нашої мети: реалізували нове архітектурне рішення та успішно вивели в продакшн понад 10 філіалів клієнта, зекономивши час і ресурси на розробку. Крім того, підвищили рівень безпеки проєкту, змогли модернізувати або цілком замінити значну частину старого рішення і побудували чіткі процеси взаємодії. Коли одна команда повністю відповідає за створене рішення, починаючи від розробки та закінчуючи моніторингом вже розміщеного проєкту у продуктовому середовищі й активно взаємодіє з іншими командами.

Перед нами стоїть ще багато завдань, пов’язаних із поліпшенням та автоматизацією тестування, втіленням тестів безпеки та покращенням процесів міграції для SQL баз даних, що працюють з Azure Functions. Також хотілося б поекспериментувати з можливими рішеннями для найбільш використовуваного функціонала, щоб якимось чином зменшити проблему холодного старту або взагалі розв’язати її.

Сподіваюся, що зміг розкрити ідею, що була покладена в основу нашої архітектури, буду радий відповісти на ваші запитання та отримати оцінку від спільноти розробників.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

👍НравитсяПонравилось5
В избранноеВ избранном7
Подписаться на тему «Azure»
LinkedIn

Похожие статьи




Підписуйтесь: Soundcloud | Google Podcast | YouTube


29 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Так а чем, собственно, обоснован выбор такой сомнительной технологии как лямбды? Из всего что вы описали видна только возможность «сегодня» сэкономить на дэвопсе. Ну и желание инженеров потрогать новую интересную штуку. Какие у вас были специфические проблемы, которые решаются хорошо лямбдами, но плохо классическим вэб-сервером/микросервисами?

Деплоить что функции что app-сервисы одинаково по сложности/атоматизации, т.к. что тут на дэвопсах не экономят. У функций есть свои плюсы(или минусы): они сильно привязаны к azure, имеют уже встроенный, настроенный логгер ai. В azure можно посмотреть статистику какие функции запускаются чаще всего, как долго выполняются и т.д.. девопсы могу конфигурить каждую функцию по отдельности просто через интерфейс azure. Цена считается от протраченного CPU, т.к. что можно видеть какие фукнции используются чаще и оптимизировать их. Функции — это рекомендуемый способ ms повесить триггер на какое-то событие просто используя azure sdk, напр. на получение событий из queue. Т.к что если такой функционал нужен — функции норм. вариант.

Для того, чтобы я мог наиболее полным образом ответить на Ваш вопрос, Вы могли бы объяснить в чем состоит сомнительность технологии?
Главной причиной выбора стала экономия на плате за ресурсы проекта, а не экономия на процессах дэвопса. У функций такой же процесс деплоймента, как и у других апп сервисов. Функции в сравнении с обычными апп сервисами значительно дешевле.
Еще среди плюсов, в сравнении могу выделить легкость в работе с keyvault, уменьшение количества кода за счет использования готовых bindings и outputs. Но в принципе все они описаны в статье. Еще одна причина, это наша общая командная вера, что за serverless будущее :)
В то же время, не могу выделить каких-то особых преимуществ «классических веб сервисов», да ASP.NET Core предоставляет middleware, но у нас не было какой-то исключительной потребности в этом.

P.S. Желание инженеров попробовать какие-то новые вещи, по-моему, всегда очень хорошая мотивация для использования технологии, при прочих равных :)

Если честно, то впечатление сомнительности технологии у меня сложилось в первую очередь на основании отзывов о лямбдах на Амазоне и Гугле. И в первую очередь это касалось цены, которая для перенагруженного приложения превращалась в x10 от цены условных EC2 на тех-же облаках. Года полтора тут на DOU была статья где автор делился опытом когда они в виде эксперимента переписали часть своего функционала на serverless и оказалось что платить за запросы это очень дорого. Я на тот момент прикидывал сколько бы это стоило для проекта на котором на тот момент работал и очень быстро исчисления перешли на тысячи долларов в месяц. Знакомый на проекте тоже использовал AWS Labmda и делился опытом — причина использования была совсем не в деньгах. И для себя я решил что serverless это либо когда сервис используется очень мало, например для какого-то датчика IoT, либо когда нужно обрабатывать запредельные пиковые нагрузки, при этом не имея проблемы с задержкой начала их обработки, например когда раз в месяц приходит петабайт-другой данных и их нужно обработать. Может за полтора года что-то поменялось, может именно у Microsoft они другие... ну а вторая причина это то что столько молодая технология по определению уступает богатству инструментария разрабатываемому для серверов уже не одно десятилетие. Например, у Google функций очень красивый логгер который работает вообще без настройки со стороны разработчиков. Очевидноя экономия на DevOps. Но это не Kebana — если функций будет по объему как в хорошем нескольколетнем приложении я очень не уверен что он будет справляться

Каждой технологии свой собсственный кейс использования, сложно сказать насколько та или иная технология сомнтиельная не примерив ее на конкретный кейс использования.
Касательно стоимости по своему опыту могу сказать что, чаще всего Run проекта считается не верно, просто так сравнивать в лоб EC2 и Lambda абсолютно не верно, т.к. в стоимости функции заложена часть NoOps составляющей. Для правильного расчета TCO нужно добавить все те составляющие которые есть в функциях и которых нет из коробки в EC2.
Также отмечу что нужно понимать что стоит запускать в функциях, т.к. стоимость все таки расчитывается из 2х компонентов (касается и AWS и Azure), цена за запрос и цена за трафик.
Касательно интрументария: сравнивать инструментарий managed service и просто кастомного кода сложно, использование первого вынуждает адаптироваться к ограничениям платформы, второй же ж дает максимум свободы и максимум ответственности за запуск и run
Касательно сравнивания функций и обыкновенных приложений, по опыту могу сказать, на самом деле не важно какое количество функций в проекте, важно понимание что каждая функция делает и важно знание интрументария нативного мониторинга.

ужно понимать что стоит запускать в функциях

Вот об этом я и говорю: мне кажется что лямбды это специализированный инструмент которых позволяет решить ранее нерешаемую задачу неограниченной масштабируемости, правда с задержкой. Можно им неплохо решить задачи когда нагрузки очень мало — признаком такой задачи является то что она укладывается полностью или почти полностью в бесплатный план. Но ожидаемо что класть в функции все должно оказаться невыгодно. И чем больше сервис — тем невыгоднее

Не могу согласится с зависимостью размера проекта от выгодности использования функций.
Выгодность использования решается непосредственно самой задачей и архитектурным выбором а не размером проекта.
Функции по сути это одна из вариаций имплементации логики сервисной/микросервисной архитектуры, эффективно используя этот инструмент можно построить супер выгодное решение со всех сторон. Даже при выходе из бесплатного плана функции могут оставаться супер выгодным решением в разрезе их дальнейшей эксплуатации, разработки.

Т.е. у Вас все-все workload’ы переедут на функции? Не было каких-то ограничений в процессе анализа требований?

Да, на данном этапе все работает через функции. По поводу ограничений, из уже реализованного функционала не могу вспомнить каких-то проблем. Но сейчас начали работу над функционалом, в котором ряд операций будет занимать значительное время. А в http функциях, есть ограничение на время ответа. Сейчас думаем вместе с бизнесом, как можно изменить флоу или будем выбирать другие варианты и придется отказаться от функций для этой задачи.

У функций есть ограничение по выполнению по времени (что у google, что у azure), думаю, потому что облачным провайдерам не выгодно, что бы оперативка была занята, а пользователь за нее почти не платил(ведь CPU практически не тратится, а плата едет за CPU). Для долго выполнимых задач есть вебджобы, апп-сервисы.

Есть еще вариант использования durable функций для задач, которые требуют большого времени выполнения или зависят от результатов выполнения других операций. Но да, они значительно дороже, потому что подразумевают существование функции-оркестратора, которая работает все время.

В примерах и демо-приложении везде используется:

builder.Services.AddTransient

Замечу, что для серверных приложений лучше сразу стараться писать код так что бы сервисы были Singlton-ами(имутабельными объектами), с целью загрузить весь код в память 1 раз, что бы реже и меньше работал GC.

Спасибо за Ваше замечание, Вы затронули очень важную тему в целом, и одну из критических тем для функций, поскольку в Azure function есть ограничение на количество активных исходящих подключений равное 600. Поэтому действительно необходимо использовать Singleton для разнообразных клиентов (http, cosmos и т. д.) иначе будут происходить весьма неприятные вещи, которые на данный момент плохо мониторятся самим Azure.
Но вряд ли можно однозначно сказать, что лучше всегда использовать тот или иной тип. При выборе времени жизни сервиса необходимо смотреть на очень многие факторы. Например, используя Singleton мы забиваем память, которая не будет высвобождена в течении всей работы. Кроме того, любые другие сервисы, которые будут внедрены в Singleton будут сами превращены по сути в синглтоны, что не всегда является очевидным.
Что касается моего демо-проекта, я выбрал Transient как самый легковесный вариант, возможно, стоит заменить его на Scoped. Немного не понял Ваше замечание, на счет одного раза, Singleton ведь будет создан только во время первого обращения к нему.

Красота!

Спасибо, надеюсь, это не сарказм :)

Vendor-lock

Так, написав це серед недоліків.

Вендор-лок — это чаще всего пугалка. Для большинства бизнесов вкладываться в cloud-agnostic сетап выйдет сильно дороже, и поэтому бессмысленно.

Якби проводили бінго по інфраструктурним методичкам Microsoft, ваш проект його б виграв:)
У вас кожна Azure function відповідає за окремий endpoint? Не виникало хаосу через занадто велику кількість фунцій?
Ще питання по девопсу — ви кожного разу видаляєте існуючі ресурси і потім створюєте нові через ARM шаблони чи якось оновлюєте існуючі?

Так, у нас гарні шанси виграти :)
Під Azure function Ви розумієте окрему функцію чи апп сервіс? Для кожного ендпоінту ми створюємо окрему функцію. Функції об’єднані в єдиний апп сервіс за принципом єдності домену. Є невелика проблема з використанням APIMу, оскільки на данному етапі експорт функцій як API у нас не автоматизовано, а створених API вже багато і для кожного з філіалів необхідно додавати їх вручну.
Щодо питання про девопс, звісно ні, ми оновлюємо ресурси за рахунок використання інкрементал деплойменту.

окрему функцію чи апп сервіс?

Окрему функцію, дякую за відповідь.

Вижу вы используете cosmosdb
У вас там sql или mongodb?
Сервис достаточно дорогой, с непонятным бекапом и рестором даннных. Почему выбрали его? Надо было менеджед решение?

Да, sql. По поводу бэкапа мы используем дефолтную конфигурацию, фул бэкап каждые 4 часа, здесь, как мне кажется, хорошо описано в официальной документации. Восстановление работает через сапорт, но не думаю, что это большая проблема.
По поводу стоимости, согласен, сервис довольно-таки дорогой. Но если сравнивать cosmos с sql, то, чтобы добиться такого же перформанса нужно использовать сервис план, который будет значительно дороже, чем космос. Так как наше решение построено на функциях, время выполнения для нас критически важно, поэтому нам нужна высокая производительность. Кроме того, проанализировав использование космоса, мы смогли провести ряд оптимизаций, что значительно сократило стоимость.
Мы выбрали космос по ряду причин:
1. У нас очень часто изменяются требования, что ведет за собой изменение моделей, мы не хотели уделять очень много времени миграциям, джоинам, нормализации и т.д.
2. Были требования от бизнеса, которые идеально закрываются за счет использования Time to live у документов.
3. Сейчас мы начали активную работу над внедрением change feed, что должно значительно облегчить и вероятно удешевить часть функционала.
4. Ну и в качестве клауд провайдэра мы выбрали Azure:)

Спасибо, cosmosdb действительно заставляет оптимизировать запросы.
Иначе очень дорого или вообще не реально сделать выборку.

Мы юзаем mongodb api, из проблем:
1. Лимит количества индексов в документе.
2. Unique index — только на пустой коллекции
3. Длина запроса. Проблема даже не в самом запросе, а в использовании полей из вложенных документов.
4. Проблемы с развертыванием из терраформа — не все фичи сапортятся

Я лично не использовал mongodb API в серьезных проектах. Слышал о подобных проблемах от коллег, а чем был обусловлен выбор именно mongodb? Вы мигрировали в Cosmos уже существующее решение?
Вы используете различных cloud провайдеров в Вашем решении? Просто терраформ очень долго догоняет все изменения, также замечал, что он крайне нестабильно работает на других проектах. Нам в этом плане проще, мы используем только Azure, поэтому нам полностью подходит ARM, в нем доступна вся конфигурация космоса.

По поводу стоимости, согласен, сервис довольно-таки дорогой. Но если сравнивать cosmos с sql, то, чтобы добиться такого же перформанса нужно использовать сервис план, который будет значительно дороже, чем космос.

Если бы sql был дороже чем cosmos, тогда cosmos бы автоматом попадал в категорию дешевых технологий хранений на равне storage account опций. Можете аргументы какие-либо привести?

Да, возможно, я не совсем понятно выразился в своем ответе. Я не говорил, что Cosmos дешевый. Моя мысль состоит в том, что соотношение производительность/цена у Cosmos будет лучше, чем у SQL.

Можете описать в цифрах будет понятней — количестве коллекций, размер хранилища, размере документа усреднённо в кб, сколько rps на crud операции и сколько RU у вас.

К сожалению, не могу поделиться этой информацией, не получил разрешение от клиента. Постараюсь сделать тестовое хранилище и провести сравнение, чтобы не быть голословным.

Подписаться на комментарии