<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Nikolay Matrosov on Medium]]></title>
        <description><![CDATA[Stories by Nikolay Matrosov on Medium]]></description>
        <link>https://medium.com/@nikolaymatrosov?source=rss-9e9ece12f4d7------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/2*wA1z20JmyCfjdYj6fs7BfQ.jpeg</url>
            <title>Stories by Nikolay Matrosov on Medium</title>
            <link>https://medium.com/@nikolaymatrosov?source=rss-9e9ece12f4d7------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 23:10:14 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@nikolaymatrosov/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Переезд на standalone блог]]></title>
            <link>https://nikolaymatrosov.medium.com/%D0%BF%D0%B5%D1%80%D0%B5%D0%B5%D0%B7%D0%B4-%D0%BD%D0%B0-standalone-%D0%B1%D0%BB%D0%BE%D0%B3-fccb502f0ae6?source=rss-9e9ece12f4d7------2</link>
            <guid isPermaLink="false">https://medium.com/p/fccb502f0ae6</guid>
            <category><![CDATA[standalone]]></category>
            <category><![CDATA[docusaurus]]></category>
            <dc:creator><![CDATA[Nikolay Matrosov]]></dc:creator>
            <pubDate>Thu, 10 Aug 2023 09:54:29 GMT</pubDate>
            <atom:updated>2023-08-10T09:54:29.923Z</atom:updated>
            <content:encoded><![CDATA[<p>После блокировки Роскомнадзором платформы Medium я решил переехать на standalone блог.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ihB4GefNH1NGzW0ver_RYQ.png" /><figcaption>Просмотры бложика в апреле-мае до блокировки</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*j6XG3CYU8Pp14D3SXLJuIg.png" /><figcaption>И после. Свежие данные</figcaption></figure><p>И хоть графики нормализованы по максимальным значениям, но по абсолютным данным видно, что просмотры упали в 4 раза.</p><p>В итоге я смахнул пыль с домена <a href="http://nikolaymatrosov.ru/">http://nikolaymatrosov.ru</a> и развернул там копию этого блога. В качестве движка выбрал <a href="https://docusaurus.io/">https://docusaurus.io/</a> второй версии.</p><p>Standalone блог даст возможность пользоваться более выразительными средствами, чем Medium, если мне захочется вставить D3 графики или еще какого-то интерактива добавить.</p><p>Посещаемость кончено пока не дотягивает даже до текущих цифр тут. Но надеюсь со временем подтянется. Потому что, признаюсь, растущие циферки просмотров статей — и есть главная мотивация для написания новых.</p><p>Я пока еще не решил, стоит ли продолжать публиковать новые посты или полностью заморозить этот блог.</p><p>Но вот туда я уже выложил новый <a href="https://nikolaymatrosov.ru/2023-08-09-Why-you-dont-need-API-Gateway">пост с рассуждениями о том почему вам может быть не нужен API Gateway перед legacy-приложением развернутым на одной VM</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fccb502f0ae6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[WASM фильтры в FluentBit]]></title>
            <link>https://nikolaymatrosov.medium.com/wasm-%D1%84%D0%B8%D0%BB%D1%8C%D1%82%D1%80%D1%8B-%D0%B2-fluentbit-f444769aac27?source=rss-9e9ece12f4d7------2</link>
            <guid isPermaLink="false">https://medium.com/p/f444769aac27</guid>
            <category><![CDATA[fluentbit]]></category>
            <category><![CDATA[webassembly]]></category>
            <category><![CDATA[rust]]></category>
            <category><![CDATA[go]]></category>
            <dc:creator><![CDATA[Nikolay Matrosov]]></dc:creator>
            <pubDate>Wed, 17 May 2023 17:53:38 GMT</pubDate>
            <atom:updated>2023-05-17T17:53:38.994Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5VdWA0FxGdiIPeSoKn9wZQ.jpeg" /></figure><p>Начиная с версии 2.0 FluentBit поддерживает фильтры скомпилированные в WASM. При этом вы можете писать код на любом языке. У них в документации есть примеры на <a href="https://github.com/fluent/fluent-bit/tree/master/examples/filter_wasm_c">C</a>, <a href="https://github.com/fluent/fluent-bit/tree/master/examples/filter_wasm_go">TinyGo</a> и <a href="https://github.com/fluent/fluent-bit/tree/master/examples/filter_rust">Rust</a>.</p><h4>Зачем вам может понадобиться свой фильтр?</h4><p>Ну, например, вы можете захотеть описать маппинг полей. Да, вы можете описать его цепочкой фильтров modify, но это будет длинный и не самый читаемый конфиг.</p><p>Еще одна альтернатива — <a href="https://docs.fluentbit.io/manual/pipeline/filters/lua">Lua</a>. Но это не так прикольно. Если честно, я решил использовать, WASM именно потому, что мне давно хотелось его попробовать применить где-нибудь, а не потому что я провел глубокие сравнительные исследования WASM и Lua.</p><h4>Установка инструментов.</h4><p>Для начала нам понадобится поставить TinyGo. Он как зависимость хочет go. Ставить я буду через <a href="https://brew.sh/">brew</a>. Другие способы установки вы можете найти <a href="https://tinygo.org/getting-started/install/">здесь</a>.</p><pre>brew install tinygo</pre><p>У меня установилась версия tinygo version 0.27.0 darwin/amd64 (using go version go1.20.2 and LLVM version 15.0.0).</p><h4>Код фильтра</h4><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/76da74d1849fd547ee8c5aa5a570d20f/href">https://medium.com/media/76da74d1849fd547ee8c5aa5a570d20f/href</a></iframe><p>Фильтр получается максимально простым.</p><ol><li>Сначала мы вычитываем запись лога record. (L12–14)</li><li>Парсим полученную строку в JSON-объект. (L15–25)</li><li>Читаем значение нужного нам поля L27</li><li>Маппим его на новое значение.</li><li>И записываем обратно в объект L48–50</li></ol><p>На что нужно обратить внимание.</p><ol><li>Комментарий //export level_mapper на 10 строке важен. Без него компилятор не сохранит имя функции и мы не сможем вызвать ее из FluentBit.</li><li>На 52 строке мы дописываем в конец строки null byte. Он просигнализирует FluentBit об окончании возвращаемой из фильтра записи лога.</li></ol><p>Теперь мы можем скомпилировать наш фильтр.</p><pre>tinygo build --no-debug -scheduler=none -target=wasi -o filter.wasm ./filter.go</pre><p>В итоге получается файл filter.wasm размером 123Kb. Не сказать, что мало, но и не много по сравнению с обычными go бинарными файлами. При помощи wasm-opt можно оптимизировать его еще немного, в итоге получив 118Kb. Оптимизированная версия фильтра, написанная на Rust и скомпилированная под WASM весит 161Kb.</p><p>И проверить его работоспособность запустив FluentBit со следующим конфигом.</p><pre>[SERVICE]<br>    Flush        1<br>    Daemon       Off<br>    Log_Level    info<br><br><br>[INPUT]<br>    Name dummy<br>    Tag dummy.local<br>    Dummy {&quot;level&quot;: &quot;4&quot;, &quot;message&quot;: &quot;warn&quot;}<br><br>[FILTER]<br>    Name wasm<br>    Match dummy.*<br>    WASM_Path filter.wasm<br>    Function_Name level_mapper<br>    Accessible_Paths .<br><br>[OUTPUT]<br>    Name  stdout<br>    Match *</pre><p>Полный код <a href="https://github.com/nikolaymatrosov/fb-wasm-filter">https://github.com/nikolaymatrosov/fb-wasm-filter</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f444769aac27" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Cloud Logging: долгосрочное хранение и анализ]]></title>
            <link>https://nikolaymatrosov.medium.com/cloud-logging-%D0%B4%D0%BE%D0%BB%D0%B3%D0%BE%D1%81%D1%80%D0%BE%D1%87%D0%BD%D0%BE%D0%B5-%D1%85%D1%80%D0%B0%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B8-%D0%B0%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7-40afb258bd0d?source=rss-9e9ece12f4d7------2</link>
            <guid isPermaLink="false">https://medium.com/p/40afb258bd0d</guid>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[yandex-query]]></category>
            <category><![CDATA[yandex-cloud]]></category>
            <category><![CDATA[logging]]></category>
            <dc:creator><![CDATA[Nikolay Matrosov]]></dc:creator>
            <pubDate>Tue, 18 Apr 2023 15:19:56 GMT</pubDate>
            <atom:updated>2023-04-18T15:19:56.474Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RzrDyUqr4GFbAFqC4R-3DQ.jpeg" /></figure><p>На сегодняшний день Yandex Cloud Logging предоставляет возможность хранения логов в течение 3 дней. И даже если срок хранения расширят до месяца, это все равно не будет покрывать многие сценарии. Так как же организовать долгосрочное хранение логов? Для этого нам понадобится создать <a href="https://cloud.yandex.ru/docs/data-streams/">Data Stream</a>, лог группу, которая будет в него писать, а так же <a href="https://cloud.yandex.ru/docs/data-transfer/">Data Transfer</a>, которы сможет перекладывать данные в какое-то долгосрочное хранилище. В этом примере я буду использовать <a href="https://cloud.yandex.ru/docs/storage/">Yandex Cloud Object Storage</a> (аналог AWS S3). Это самый дешевый из возможных вариантов, при этом обеспечивающий не только хранение, но и с выходом <a href="https://cloud.yandex.ru/docs/query/">Yandex Query</a> и возможность анализа хранимых логов.</p><p>А теперь давайте по порядку.</p><h3>Создаем инфраструктуру</h3><p>Создадим serverless базу YDB</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a5PI8EaLulL-MEDuDyk0dg.png" /></figure><p>Она нам понадобится для того, чтобы в ней создать поток Data Stream.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*f1b-zxPoJP0SxNzvQbt51A.png" /></figure><p>В параметрах укажем только что созданную базу. Если же выбрать минимальную конфигурацию потока, то он может быть вообще бесплатным, но не более одного такого потока привязанного к платежному аккаунту, далее платно.</p><p>Теперь создадим бакет, куда мы будем писать логи.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4519FurI0s0Pm4fHWxxGpA.png" /></figure><p>Теперь можно создать лог-группу в сервисе Cloud Logging. Ее мы сможем указывать в функции, serverless контейнере, API Gateway или любом другом месте откуда мы собираемся писать логи.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*X-Ae7jNp3AnhJOMT5W9H4g.png" /></figure><p>Теперь нам нужно создать эндпоинты Data Transfer: источник данных из стрима logs и приемник данных — наш бакет sls-log-to-s3.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HUcKlWZJMCHK6cSccx5xJA.png" /><figcaption>Источник</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eJYYmZ-MqDAjwtJKG92_zA.png" /><figcaption>Приёмник</figcaption></figure><p>А теперь объединить их в трансфер.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qbKEmj_bOCRLfEPWUfQ3Eg.png" /><figcaption>Трансфер с только что созданными источником и приёмником.</figcaption></figure><p>Теперь трансфер нужно активировать.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*O_6HzA4CqkDeOk6uKWdrBg.png" /></figure><p>Вот в принципе и все. Теперь остается только создать функцию, передать в нее лог-группу и погенерировать логов, чтобы убедиться, что всё работает.</p><pre>import logging<br>import random<br>from pythonjsonlogger import jsonlogger<br><br><br>class YcLoggingFormatter(jsonlogger.JsonFormatter):<br>    def add_fields(self, log_record, record, message_dict):<br>        super(YcLoggingFormatter, self).add_fields(log_record, record, message_dict)<br>        log_record[&#39;logger&#39;] = record.name<br>        log_record[&#39;level&#39;] = str.replace(str.replace(record.levelname, &quot;WARNING&quot;, &quot;WARN&quot;), &quot;CRITICAL&quot;, &quot;FATAL&quot;)<br><br>logHandler = logging.StreamHandler()<br>logHandler.setFormatter(YcLoggingFormatter(&#39;%(message)s %(level)s %(logger)s&#39;))<br><br>logger = logging.getLogger(&#39;MyLogger&#39;)<br>logger.propagate = False<br>logger.addHandler(logHandler)<br>logger.setLevel(logging.DEBUG)<br><br>def handler(event, context):<br>    val = random.random()<br>    if val &gt; .8:<br>        logger.error(&quot;some error log&quot;, extra={&quot;val&quot;: val})<br>    elif val &gt; .5:<br>        logger.info(&quot;test logging&quot;, extra={&quot;val&quot;: val})<br>    else:<br>        logger.debug(&quot;everything is fine&quot;, extra={&quot;val&quot;: val})<br><br>    <br>    return {<br>        &#39;statusCode&#39;: 200,<br>        &#39;body&#39;: &#39;Hello World!&#39;,<br>    }</pre><p>И в requirements.txt добавить python-json-logger==2.0.4</p><h3>Анализ</h3><p>Теперь перейдя в бакет мы можем увидеть, что логи туда успешно реплицируются.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t0rIS_n0Li-PKJjmtuuYkQ.png" /></figure><p>Давайте теперь попробуем проанализировать их при помощи Yandex Query. Для этого создадим новое соединение, нажав на кнопку «Обработать в YQ».</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qYujs2EmZbzR1eoy6qYnHg.png" /></figure><p>Начать можно с простого запроса</p><pre>SELECT *<br>FROM `sls-log-to-s3-2023/04/18/`.`2023/04/18/`<br>WITH<br>(<br>    format=json_as_string, <br>    SCHEMA<br>        (<br>            Data Json<br>        )<br>)<br>LIMIT 100</pre><p>Тут мы указываем, что будем обращаться к созданному ранее источнику `sls-log-to-s3–2023/04/18/` по пути `2023/04/18/`. Таким образом YQ возьмет все файлы за день и будет использовать данные из них при выполнении запроса.</p><p>Далее мы указываем формат json_as_string. Подробнее про форматы можно прочитать в <a href="https://cloud.yandex.ru/docs/query/sources-and-sinks/formats">документации</a>.</p><p>Отлично, теперь мы видим, шейп данных.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9CIDWK3gUkvh06S0YNOGXg.png" /></figure><p>Для того чтобы обратиться к вложенным полям JSON-объекта мы будем использовать JSON_VALUE. К сожалению напрямую в GROUP BY его мы использовать не можем, но можем завернуть извлечение данных в подзапрос, а группировку вынести выше.</p><p>Вот пример чуть более сложного запроса с аггрегациями.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aWaWh8VIRM3IvKc5XrNuDw.png" /></figure><pre>SELECT message, COUNT(*) as message_count, AVG(val) as avg_val<br>from (<br>    SELECT<br>    JSON_VALUE(Data, &quot;$.resource.id&quot;) as resource_id, <br>    JSON_VALUE(Data, &quot;$.message&quot;) as message,<br>    Cast(JSON_VALUE(Data, &quot;$.jsonPayload.val&quot;) as Double) as val,<br>    FROM `sls-log-to-s3-2023/04/18/`.`2023/04/18/`<br>    WITH<br>    (<br>        format=json_as_string, <br>        SCHEMA<br>            (<br>                Data Json<br>            )<br>    )<br>)<br>GROUP BY message<br>HAVING COUNT(*) &gt; 1<br>ORDER by message_count DESC</pre><p>В завершении хочу добавить, что вы можете также настроить политики хранения данных в Object Storage, чтобы перекладывать старые логи по истечении какого-то времени в другие классы хранения или вообще удалять их.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=40afb258bd0d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Dotnet 6 Web API в Yandex Cloud Serverless Container]]></title>
            <link>https://nikolaymatrosov.medium.com/dotnet-6-web-api-%D0%B2-yandex-cloud-serverless-container-21be8824d080?source=rss-9e9ece12f4d7------2</link>
            <guid isPermaLink="false">https://medium.com/p/21be8824d080</guid>
            <category><![CDATA[webapi]]></category>
            <category><![CDATA[csharp]]></category>
            <category><![CDATA[dotnet6]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[yandex-cloud]]></category>
            <dc:creator><![CDATA[Nikolay Matrosov]]></dc:creator>
            <pubDate>Tue, 31 Jan 2023 14:19:36 GMT</pubDate>
            <atom:updated>2023-01-31T19:52:01.604Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*06KYyrOatsujnh5UmdUAgQ.jpeg" /></figure><p>Для начала создадим проект на основе примера Web API.</p><h3>Базовое приложение</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lwODXLvsjLsNTlvYSVIsVQ.png" /><figcaption>Используем один из шаблонов в Rider’е.</figcaption></figure><p>Отлично. Теперь пройдемся по изменениям, которые нужно внести в проект, чтобы он «взлетел» в Serverless Containers.</p><p>Как я писал в посте про <a href="https://nikolaymatrosov.medium.com/%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA-serverless-container-%D0%BF%D0%BE-%D1%82%D0%B0%D0%B9%D0%BC%D0%B5%D1%80%D1%83-3872bd402db0">запуск контейнера по триггеру</a> на необходимо реализовать интерфейс HTTP сервера. К счастью, dotnet core Web API именно его и реализует и нам остается только добавить несколько настроек, чтобы выполнить главное условие — запускать сервер на порту, переданном приложение через переменную окружения PORT. Для этого в Program.cs добавим следующее:</p><pre>var port = Environment.GetEnvironmentVariable(&quot;PORT&quot;) ?? &quot;8080&quot;;<br>builder.WebHost.UseKestrel(<br>    serverOptions =&gt;<br>    {<br>        serverOptions.ListenAnyIP(int.Parse(port));<br>    });</pre><p>Теперь, нам нужно удалить еще одну настройку:</p><pre>app.UseHttpsRedirection();</pre><p>Она нам не понадобится так как вызов контейнера идет по HTTP, а в логах мы будем видеть следующие предупреждения, ведь порт для HTTPS мы не задаем:</p><pre>warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]<br>      Failed to determine the https port for redirect.</pre><p>Еще я бы убрал из Dockerfile строки EXPOSE, чтобы не вводить в заблуждение, ведь наше приложение больше не использует эти порты.</p><pre>EXPOSE 80<br>EXPOSE 443</pre><p>Также можно почистить %project%/Properties/launchSettings.json от лишних параметров.</p><p>Теперь можно развернуть приложение и убедиться, что все работает. Я репозитории с примером буду делать это при помощи Terraform CDK. Работа с ним выходит за рамки этого туториала, но у меня есть отдельная <a href="https://medium.com/%40nikolaymatrosov/cloud-development-kit-%D0%B4%D0%BB%D1%8F-terraform-49d612b4272b">статья про TF CDK</a>. И так я перейду в папку cdk и выполню следующую команду.</p><pre>TF_VAR_folderId=b1g*** TF_VAR_token=`yc iam create-token` \<br>  cdktf deploy --output ../cdktf.out</pre><p>В результате будет собран docker image, запушен в облачный репозиторий и из него будет развернут serverless контейнер.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nn-ZdLcFCKReq--mq6qfnw.png" /><figcaption>Все работает</figcaption></figure><h3>Авторизация</h3><p>Если вы попробуете добавить авторизацию в ваше приложение развернутое в YC Serverless Containers вас ждет неприятный сюрприз: как сказано в <a href="https://cloud.yandex.ru/docs/serverless-containers/concepts/invoke#filter">документации</a> заголовки Authorization и Cookie из запроса вырезаются, как и некоторые другие.</p><p>Что же делать? Ставить контейнер за API Gateway. Кроме возможности использовать авторизацию нормально, он также, например, позволит использовать свой собственный домен.</p><h4>API Gateway</h4><p>Для того чтобы создать API GW, нам понадобится OpenAPI спецификация описывающая наше API. Её можно написать руками, а можно сгенерировать на основе кода. Тем более в базовом примере и так уже используется пакет Swashbuckle для генерации спеки для Swagger. Установим CLI которую, будем вызывать при билде.</p><pre>dotnet tool install SwashBuckle.AspNetCore.Cli --global</pre><p>Теперь мы можем добавить в csproj команду для вызова этой утилиты.</p><pre>&lt;Target Name=&quot;OpenAPI&quot; AfterTargets=&quot;Build&quot; Condition=&quot;&#39;$(Configuration)&#39; == &#39;Debug&#39;&quot;&gt;<br>    &lt;Exec Command=&quot;swagger tofile --output ./swagger.yaml --yaml $(OutputPath)$(AssemblyName).dll v1&quot; WorkingDirectory=&quot;$(ProjectDir)&quot; /&gt;<br>&lt;/Target&gt;</pre><p>В итоге мы получим swagger.yaml где будет OpenAPI 3.0 документ, описывающий наше API. Чтобы использовать его в API Gateway нам понадобится лишь одна доработка: нам нужно разметить объекты операций расширением спецификации x-yc-apigateway-integration, которе скажет гейтвею, что для этого запроса нужно вызвать контейнер.</p><p>Для этого нам понадобится написать класс имплементирующий интерфейс IOperationFilter. Его метод Apply будет вызываться для каждого объекта операций в OpenAPI спецификации и в нем мы сможем модифицировать операцию как захотим.</p><pre>using Microsoft.OpenApi.Any;<br>using Microsoft.OpenApi.Models;<br>using Swashbuckle.AspNetCore.SwaggerGen;<br><br>namespace ServerlessContainerDemo.OpenAPI.Filters;<br><br>public class YcExtensionsFilter : IOperationFilter<br>{<br>    public void Apply(OpenApiOperation operation, OperationFilterContext context)<br>    {<br>        operation.Extensions.Add(&quot;x-yc-apigateway-integration&quot;, new OpenApiObject()<br>        {<br>            [&quot;type&quot;] = new OpenApiString(&quot;serverless_containers&quot;),<br>            [&quot;container_id&quot;] = new OpenApiString(&quot;${container_id}&quot;),<br>            [&quot;service_account_id&quot;] = new OpenApiString(&quot;${container_service_account_id}&quot;),<br>        });<br>    }<br>}</pre><p>Значения ${container_id} и ${container_service_account_id} мы подставим в шаблонизаторе в Terraform.</p><p>Теперь зарегистрируем фильтр в Program.cs.</p><pre>builder.Services.AddSwaggerGen(c =&gt;<br>{<br>    c.EnableAnnotations();<br>    c.SwaggerDoc(&quot;v1&quot;, new OpenApiInfo() {Title = &quot;Serverless Container Demo&quot;, Version = &quot;v1&quot;});<br>    c.OperationFilter&lt;YcExtensionsFilter&gt;();<br>});</pre><h4>Добавляем авторизацию</h4><blockquote>Этот хэндлер приведен только для демонстрационных целей.</blockquote><pre>namespace ServerlessContainerDemo.Auth;<br><br>using System.Security.Claims;<br>using System.Text;<br>using System.Text.Encodings.Web;<br>using Microsoft.AspNetCore.Authentication;<br>using Microsoft.Extensions.Options;<br><br>public class BasicAuthenticationHandler : AuthenticationHandler&lt;AuthenticationSchemeOptions&gt;<br>{<br>    public BasicAuthenticationHandler(<br>        IOptionsMonitor&lt;AuthenticationSchemeOptions&gt; options,<br>        ILoggerFactory logger,<br>        UrlEncoder encoder,<br>        ISystemClock clock<br>    ) : base(options, logger, encoder, clock)<br>    {<br>    }<br><br>    protected override Task&lt;AuthenticateResult&gt; HandleAuthenticateAsync()<br>    {<br>        var authHeader = Request.Headers[&quot;Authorization&quot;].ToString();<br>        if (authHeader != null &amp;&amp; authHeader.StartsWith(&quot;basic&quot;, StringComparison.OrdinalIgnoreCase))<br>        {<br>            var token = authHeader.Substring(&quot;Basic &quot;.Length).Trim();<br>            System.Console.WriteLine(token);<br>            var credentialString = Encoding.UTF8.GetString(Convert.FromBase64String(token));<br>            var credentials = credentialString.Split(&#39;:&#39;);<br>            if (credentials[0] == &quot;admin&quot; &amp;&amp; credentials[1] == &quot;admin&quot;)<br>            {<br>                var claims = new[] {new Claim(&quot;name&quot;, credentials[0]), new Claim(ClaimTypes.Role, &quot;Admin&quot;)};<br>                var identity = new ClaimsIdentity(claims, &quot;Basic&quot;);<br>                var claimsPrincipal = new ClaimsPrincipal(identity);<br>                return Task.FromResult(<br>                    AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, Scheme.Name)));<br>            }<br><br>            Response.StatusCode = 401;<br>            Response.Headers.Add(&quot;WWW-Authenticate&quot;, &quot;Basic&quot;);<br>            return Task.FromResult(AuthenticateResult.Fail(&quot;Invalid Authorization Header&quot;));<br>        }<br>        else<br>        {<br>            Response.StatusCode = 401;<br>            Response.Headers.Add(&quot;WWW-Authenticate&quot;, &quot;Basic&quot;);<br>            return Task.FromResult(AuthenticateResult.Fail(&quot;Invalid Authorization Header&quot;));<br>        }<br>    }<br>}</pre><p>Теперь добавим опции в конфигурацию приложения.</p><pre>builder.Services.AddAuthentication()<br>    .AddScheme&lt;AuthenticationSchemeOptions, BasicAuthenticationHandler&gt;<br>        (&quot;BasicAuthentication&quot;, null);<br>builder.Services.AddAuthorization(options =&gt;<br>{<br>    var basicAuthenticationSchemePolicyBuilder = new AuthorizationPolicyBuilder(&quot;BasicAuthentication&quot;);<br>    options.AddPolicy(&quot;BasicAuthentication&quot;, basicAuthenticationSchemePolicyBuilder<br>        .RequireClaim(ClaimTypes.Role, &quot;Admin&quot;)<br>        .Build());<br>});</pre><p>И после этого мы можем использовать аннотацию Authorize на хэндлерах нашего контроллера:</p><pre>[Authorize(Policy = &quot;BasicAuthentication&quot;)]<br>[HttpGet(Name = &quot;GetWeatherForecast&quot;)]<br>public IEnumerable&lt;WeatherForecast&gt; Get()</pre><p>Теперь если мы вызовем эндпоинт с неправильной авторизацией то получим:</p><pre>HTTP/2 401<br>date: Mon, 30 Jan 2023 22:15:54 GMT<br>content-length: 0<br>access-control-allow-origin: *<br>x-content-type-options: nosniff<br>x-serverless-gateway-path: /WeatherForecast<br>x-yf-remapped-date: Mon, 30 Jan 2023 22:15:54 GMT<br>x-yf-remapped-server: Kestrel<br>x-yf-remapped-www-authenticate: Basic<br>server: Yandex-API-Gateway/1.0<br></pre><p>К сожалению вместо WWW-Authenticate в ответе API Gateway мы получим x-yf-remapped-www-authenticate. Это не позволит браузеру узнать, что ресурс требует Basic-авторизацию и отобразить окно ввода логина и пароля.</p><h3>Асинхронные задачи</h3><p>Если в двух словах, то вызов контейнера синхронен. То есть если вы ответите на запрос и поставите асинхронную задачу в надежде, что она выполнится позже, то разочарую вас этого не случится. Процессы которые оркестрируют работу контейнеров ничего не знают про асинхронные таски внутри них и к сожалению нет механизмов, чтобы они о них узнали. Так что вся критичная работа должна быть выполнена до ответа пользователю.</p><p>Есть возможность выполнения асинхронных задач через помещение их в очередь и разбор очереди при <a href="https://medium.com/%40nikolaymatrosov/%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA-serverless-container-%D0%BF%D0%BE-%D1%82%D0%B0%D0%B9%D0%BC%D0%B5%D1%80%D1%83-3872bd402db0">помощи триггера</a>.</p><p>Подробнее про асинхронность в серверлесс средах Облака можно прочитать в <a href="https://nikolaymatrosov.medium.com/%D0%BF%D1%80%D0%BE-%D0%B0%D1%81%D0%B8%D0%BD%D1%85%D1%80%D0%BE%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D1%8C-%D0%B2-cloud-functions-536cbcf8fe20">отдельном посте</a>.</p><h3>Логирование</h3><p>Пользовательские логи, которые пишет контейнер в стандартный поток вывода (stdout) и стандартный поток вывода ошибок (stderr) могут быть простыми и структурированными.</p><p>По умолчанию Web API приложение будет писать логи в стандартном формате, что будет восприниматься сервисом Cloud Logging как простые текстовые логи. В таком случае мы не имеем возможности фильтровать логи по уровню критичности, а также передавать дополнительные параметры. Чтобы это все заработало, нужно писать логи в JSON-формате. Тогда поля message или msg будут восприниматься, поле содержащее основной текст записи лога, поле level — как поле, указывающее уровень логирования.</p><p>Для того чтобы удобно это настроить мы можем воспользоваться библиотекой Serilog или Nlog. Я выбрал вторую.</p><p>И так для того, чтобы добавить библиотеку, нам нужно будет установить пакет NLog.Web.AspNetCore — он обеспечивает легкую интеграцию и конфигурирование логгера в Web API приложении.</p><pre>builder.Logging.ClearProviders();<br>builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Debug);<br>builder.Host.UseNLog();</pre><p>Теперь добавим в корень проекта файл конфигурации логгера nlog.config, именно по этому стандартному пути конфиг и будет найден при вызовеUseNLog().</p><pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;<br><br>&lt;nlog xmlns=&quot;http://www.nlog-project.org/schemas/NLog.xsd&quot;<br>      xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&gt;<br><br>    &lt;!-- куда писать --&gt;<br>    &lt;targets&gt;<br>        &lt;target xsi:type=&quot;Console&quot; name=&quot;ConsoleTarget&quot;&gt;<br>            &lt;!-- Важно добавить includeEventProperties=&quot;true&quot;.<br>              Так в логи попадут все дополнительные параметры, что мы передадим.<br>              Больше инфы тут: https://github.com/NLog/NLog/wiki/JsonLayout<br>             --&gt;<br>            &lt;layout xsi:type=&quot;JsonLayout&quot; includeEventProperties=&quot;true&quot; includeScopeProperties=&quot;true&quot;&gt;<br>                &lt;attribute name=&quot;time&quot; layout=&quot;${longdate}&quot; /&gt;<br>                &lt;!-- Эти два поля нужны, чтобы cloud logging подхватил наши<br>                структурированные логи --&gt;<br>                &lt;attribute name=&quot;level&quot; layout=&quot;${level:upperCase=true}&quot; /&gt;<br>                &lt;attribute name=&quot;message&quot; layout=&quot;${message}&quot; /&gt;<br>                &lt;!-- Если хотите, чтобы в логи попадали стектрейсы. --&gt;<br>                &lt;attribute name=&#39;exception&#39; layout=&#39;${exception}&#39; /&gt;<br>            &lt;/layout&gt;<br>        &lt;/target&gt;<br>    &lt;/targets&gt;<br><br>    &lt;!-- правила сопоставления логгера и таргета --&gt;<br>    &lt;rules&gt;<br>        &lt;!--Output hosting lifetime messages to console target for faster startup detection --&gt;<br>        &lt;logger name=&quot;Microsoft.Hosting.Lifetime&quot; minlevel=&quot;Info&quot; writeTo=&quot;ConsoleTarget&quot; final=&quot;true&quot; /&gt;<br><br>        &lt;!--Skip non-critical Microsoft logs and so log only own logs (BlackHole) --&gt;<br>        &lt;logger name=&quot;Microsoft.*&quot; maxlevel=&quot;Info&quot; final=&quot;true&quot; /&gt;<br>        &lt;logger name=&quot;System.Net.Http.*&quot; maxlevel=&quot;Info&quot; final=&quot;true&quot; /&gt;<br><br>        &lt;logger name=&quot;*&quot; minlevel=&quot;Trace&quot; writeTo=&quot;ConsoleTarget&quot; /&gt;<br>    &lt;/rules&gt;<br>&lt;/nlog&gt;</pre><p>Теперь о том как получить логгер. Есть два пути:</p><pre>Logger log = LogManager.GetCurrentClassLogger();</pre><p>Использовать NLog напрямую. Плюсы этого решения:</p><ul><li>Наилучшая производительность</li><li>Больше опций для взаимодействия с API логгера, например Logger.WithProperty(..)</li><li>Работает со всеми платформами</li><li>Не требует Dependency Injection</li></ul><p>Второй вариант — Microsoft.Extensions.Logging</p><pre>ILogger&lt;Т&gt; logger</pre><ul><li>Решение полностью интегрировано с ASP.NET Core, то есть логи которые пишутся фреймворком тоже будут обработаны и/или отфильтрованы NLog.</li><li>Не придется писать абстракции связанные с логированием</li><li>Отлично работает с .NET Core Dependency injection</li><li>Можно загрузить конфигурацию NLog из <a href="https://github.com/NLog/NLog.Extensions.Logging/wiki/NLog-configuration-with-appsettings.json">appsettings.json</a> вместо XML файла NLog.config</li></ul><h4>Использование</h4><pre>using (_logger.BeginScope(&quot;{reminder}&quot;, reminder))<br>{<br>    if (value % 10 == reminder)<br>    {<br>        throw new Exception(&quot;Random exception&quot;);<br>    }<br><br>    _logger.LogInformation(&quot;valid {value}&quot;, value);<br>}</pre><p>Хоть строка message в логе и выглядит как шаблон для подстановки значений, при логировании интерполяция строк не будет производиться. На самом деле значения из фигурных скобок будут использованы как ключи для аргументов переданных после message при записи в объект структурированного лога.</p><pre>{<br>  &quot;message&quot;: &quot;valid value&quot;<br>  &quot;value&quot;: 3666638874307776000,<br>  &quot;reminder&quot;: 7<br>}</pre><p>Как видно выше мы можем задавать именованные области через _logger.BeginScope, рамках которых определять дополнительные параметры для записей лога.</p><p>Репозиторий с примером. <a href="https://github.com/nikolaymatrosov/yc-sls-container-csharp">https://github.com/nikolaymatrosov/yc-sls-container-csharp</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=21be8824d080" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Serverless Full Text Search на Go]]></title>
            <link>https://nikolaymatrosov.medium.com/serverless-full-text-search-%D0%BD%D0%B0-go-ebc2c529cd56?source=rss-9e9ece12f4d7------2</link>
            <guid isPermaLink="false">https://medium.com/p/ebc2c529cd56</guid>
            <category><![CDATA[go]]></category>
            <category><![CDATA[yandex-cloud]]></category>
            <category><![CDATA[blevesearch]]></category>
            <category><![CDATA[severless]]></category>
            <dc:creator><![CDATA[Nikolay Matrosov]]></dc:creator>
            <pubDate>Sat, 07 Jan 2023 21:06:38 GMT</pubDate>
            <atom:updated>2023-01-28T12:37:58.351Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N20prPrsDM88rgyyPOB6cQ.jpeg" /></figure><p>В прошлом <a href="https://medium.com/@nikolaymatrosov/serverless-full-text-search-e10f9201e905">Serverless Full Text Search</a> я рассмотрел JavaScript библиотеку для in-memory полнотекстового поиска Lyra. Почему я выбрал именно ее? Во-первых, именно на доклад про неё я наткнулся на YouTube. Во-вторых, мне понравилась её универсальность и возможность притащить её в браузер.</p><p>Но при этом я могу выделить несколько минусов:</p><ol><li>In-memory. И хотя именно это и дает ей портабельность и скорость, но загрузку индекса в память на холодном старте уходит значительное время (~5сек) и с этим ничего не сделать.</li><li>К тому же индекс в памяти занимает значительное место, то есть нам придется выделять функции больше памяти, а значит и платить за эти ресурсы.</li><li>На текущий момент поддерживаются индексы только по строковым полям, другие типы, например, по числовым, по датам или GeoSpatial индексы не поддержаны.</li><li>Нет <a href="https://ru.wikipedia.org/wiki/%D0%A4%D0%B0%D1%81%D0%B5%D1%82%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA">фасетного поиска</a>. Что это такое? Это когда при поиске найденные документы классифицируются по заданным свойствам — фасетам. Эта информация возвращается вместе результатами поиска и у вас есть возможность на фронте отрисовать дополнительные фильтры, чтобы пользователь мог легко уточнить свой запрос. <em>(UPD 2023–01–27: В Lyra </em><a href="https://github.com/LyraSearch/lyra/pull/259"><em>добавили</em></a><em> фасеты.)</em></li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lOPBeHkYdFGx0LUEAPjIig.png" /><figcaption>Фильтры по фасетам на Маркете для уточнения запроса «Ноутбуки»</figcaption></figure><p>И поискав немного, я нашел новую библиотеку на этот раз на Go.</p><h3>Bleve</h3><p>Название не очень благозвучное, но оно происходит от акронима BLEVE — <em>Boiling liquid expanding vapor explosion</em>. Именно поэтому у библиотеки такое лого.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1EltZqAjHj16NKm6h1Kg6g.jpeg" /></figure><p>Она поддерживает</p><ul><li>Индексацию любых Go структур, включая JSON;</li><li>Гибкую настройку, при этом имея значения по умолчания, которые позволят вам свести конфигурирование к минимуму;</li><li>Индексы по: Text, Numeric, Datetime, Boolean;</li><li>Разные типы запросов: <br>— Термин или фраза с точным или частичным совпадением, Fuzzy-поиск (неточное совпадение по расстоянию Левенштейна); <br>— Конъюнкция(«И»), дизъюнкция(«ИЛИ»), Булевы модификаторы (must в поисковом запросе + перед термином, should — уровень по умолчанию и must_not — - перед термином);<br>— Term Range, Numeric Range, Date Range;<br>— <a href="https://github.com/blevesearch/bleve/blob/master/geo/README.md">Geo Spatial</a></li><li>Удобный для людей <a href="http://blevesearch.com/docs/Query-String-Query/">синтаксис запросов</a>;</li><li><a href="https://en.wikipedia.org/wiki/Tf-idf">tf-idf</a> скоринг;</li><li>Бустинг (Вы можете повлиять на относительную важность частей запроса, добавив к ним суффикс с помощью оператора ^, за которым следует число.);</li><li>Подсветку найденного в результатах;</li><li>Поддержка агрегации фасетов: по терминам, числовым или временным диапазонам.</li></ul><h3>Эксперименты</h3><p>Для того чтобы результаты можно было сравнивать с предыдущей статьёй я буду использовать тот же датасет анекдотов на русском языке на 124к документов.</p><h4>Построение индекса</h4><p>Строить индекс можно и программно, но кроме этого bleve предоставляет одноименную консольную утилиту, которая позволит вам работать с индексом.</p><p>Установка.</p><pre>go install github.com/blevesearch/bleve/v2/cmd/bleve@latest</pre><pre>$ bleve --help<br>Bleve is a command-line tool to interact with a bleve index.<br><br>Usage:<br>  bleve [command]<br><br>Available Commands:<br>  bulk        bulk loads from newline delimited JSON files<br>  check       checks the contents of the index<br>  count       counts the number documents in the index<br>  create      creates a new index<br>  dictionary  prints the term dictionary for the specified field in the index<br>  dump        dumps the contents of the index<br>  fields      lists the fields in this index<br>  help        Help about any command<br>  index       adds the files to the index<br>  mapping     prints the mapping used for this index<br>  query       queries the index<br>  registry    registry lists the bleve components compiled into this executable<br>  scorch      command-line tool to interact with a scorch index<br><br>Flags:<br>  -h, --help   help for bleve<br><br>Use &quot;bleve [command] --help&quot; for more information about a command.</pre><p>Именно при помощи нее я подготовил индекс, загрузив в него документы в формате JSON per line. Но для начала нам нужно будет создать файл с описанием структуры индексируемых документов.</p><pre>{<br>  &quot;types&quot;: {<br>    &quot;joke&quot;: {<br>      &quot;fields&quot;: [<br>        {<br>          &quot;name&quot;: &quot;joke&quot;,<br>          &quot;type&quot;: &quot;text&quot;,<br>          &quot;analyzer&quot;: &quot;ru&quot;,<br>          &quot;store&quot;: true,<br>          &quot;index&quot;: true<br>        }<br>      ]<br>    }<br>  },<br>  &quot;type_field&quot;: &quot;_type&quot;,<br>  &quot;default_type&quot;: &quot;film&quot;<br>}</pre><p>Далее на основе него создать индекс.</p><pre>bleve create -m data/mapping.json data/index</pre><p>Далее индексируем датасет.</p><pre>bleve bulk data/index data/dataset.jsonl</pre><p>В директории data/index появится файл с метаданными индекса и директория store со следующим содержимым:</p><pre>-rw-------  1 nikolay  nikolay    40M Jan  7 21:46 000000000044.zap<br>-rw-------  1 nikolay  nikolay   9.1M Jan  7 21:46 000000000050.zap<br>-rw-------  1 nikolay  nikolay   9.1M Jan  7 21:46 000000000059.zap<br>-rw-------  1 nikolay  nikolay   9.0M Jan  7 21:46 000000000064.zap<br>-rw-------  1 nikolay  nikolay   8.6M Jan  7 21:46 00000000006e.zap<br>-rw-------  1 nikolay  nikolay   8.8M Jan  7 21:46 000000000079.zap<br>-rw-------  1 nikolay  nikolay   9.3M Jan  7 21:46 000000000083.zap<br>-rw-------  1 nikolay  nikolay   1.2M Jan  7 21:46 000000000084.zap<br>-rw-------  1 nikolay  nikolay   1.2M Jan  7 21:46 000000000085.zap<br>-rw-------  1 nikolay  nikolay   1.3M Jan  7 21:46 000000000086.zap<br>-rw-------  1 nikolay  nikolay   1.2M Jan  7 21:46 000000000087.zap<br>-rw-------  1 nikolay  nikolay   1.2M Jan  7 21:46 000000000088.zap<br>-rw-------  1 nikolay  nikolay   1.3M Jan  7 21:46 000000000089.zap<br>-rw-------  1 nikolay  nikolay   1.3M Jan  7 21:46 00000000008a.zap<br>-rw-------  1 nikolay  nikolay   1.3M Jan  7 21:46 00000000008b.zap<br>-rw-------  1 nikolay  nikolay   1.3M Jan  7 21:46 00000000008c.zap<br>-rw-------  1 nikolay  nikolay   1.3M Jan  7 21:46 00000000008d.zap<br>-rw-------  1 nikolay  nikolay   1.2M Jan  7 21:46 00000000008f.zap<br>-rw-------  1 nikolay  nikolay   236K Jan  7 21:46 000000000090.zap<br>-rw-------  1 nikolay  nikolay    64K Jan  7 21:47 root.bolt</pre><p>Общий размер файлов 109Mb, что меньше сериализованного в JSON индекса Lyra(130Mb). К сожалению этот индекс гораздо хуже жмется zip’ом — 59Mb против 33. Зато его не нужно будет грузить в память.</p><blockquote>Изначально Bleve использовал индекс под названием «upsidedown», который в качестве хранилища использовал Key-Value хранилища BoltDB/RocksDB/LevelDB/Moss. Новый индекс «scorch» <a href="https://news.ycombinator.com/item?id=16089082">работает с диском</a> напрмую. При создании вы можете выбрать тип индекса.</blockquote><p>Тесты bleve индекса я проводил на функции со <strong>128Mb</strong>, в то время как для Lyra требовалось минимум <strong>1Gb</strong>. Вероятно, дело не только в размере памяти, но и в выделяемой доле CPU, которая пропорциональна памяти, потому что при 768Mb запрос к Lyra не укладывался в 15 секунд на «холодном» старте.</p><p>То есть bleve версия уже в <strong>8 раз дешевле. </strong>Давайте сравним тайминги «холодного» старта.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dkOiqaF2VbRQeKknxdr5Zg.png" /><figcaption>Стрельбы танком по bleve индексу.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VTHa5IY93_u087daNJOLGA.png" /><figcaption>Стрельбы танком по lyra индексу.</figcaption></figure><p>Что можно заметить? Во-первых, гораздо ниже время холодного старта ~2,5сек против ~7сек. Во-вторых, нет 429 кодов ошибок. Это тоже связано с меньшим временем старта: запросы не успевают переполнить <a href="https://cloud.yandex.ru/docs/functions/concepts/function#scaling">очередь</a> и превысить zone_requests_limit. В остальном значения на 90-персентиле сравнимы, а значит при кванте в 100мс вы будете оплачивать одинаковое время, с упомянутой выше поправкой на размер требуемой памяти.</p><p>В итоге экономия в зависимости от количества «холодных» стартов у вас будет <strong>от 8 до 23 раз</strong>.</p><p>Еще мне было интересно посмотреть куда в bleve уходит время.</p><pre>{<br>  ...  <br>  &quot;durations&quot;: {<br>    &quot;checkCache&quot;: 372,<br>    &quot;getObjectInfo&quot;: 35617,<br>    &quot;downloadAndUnzip&quot;: 2095123,<br>    &quot;openIndex&quot;: 18213,<br>    &quot;queryIndex&quot;: 2804,<br>    &quot;closeIndex&quot;: 238,<br>  }<br>}</pre><p>Значения в микросекундах (10⁻⁶). Проверка наличия кэша, получение данных про индекс(необходимо знать размер для распаковки zip) скачивание и распаковка, открытие индекса, запрос и закрытие индекса.</p><blockquote>Если индекс не закрыть, то на «горячем» старте функция повиснет в ожидании доступа к базе.</blockquote><h3>Фасеты и язык запросов</h3><h4>Подготовка данных и измерения</h4><p>Так как на анекдотах строить фасеты не на чем, для этого эксперимента я взял датасет с сайта <a href="https://opendata.mkrf.ru/opendata/7705851331-register_movies">открытых данных министерства культуры</a>. В нем 97k документов про выданные прокатные удостоверения.</p><p>Когда я пробовал построить индекс используя все поля у меня не хватало места в директории /tmp, хотя формально я не превышал указанные в документации 512mb. Но разбираться в чем там проблема мне не очень хотелось, поэтому я выкинул часть полей, получив индекс меньшего размера — 189Mb. Именно для него будут приведены следующие измерения.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zuBRbBp9SsgGGEKRPXusjA.png" /><figcaption>Размеры сжатого индекса (меньше лучше).</figcaption></figure><p>Отсюда видно, что 7z дает наилучшее сжатие в этом случае, но если посмотреть на время распаковки, то станет понятно, почему я бы не стал его рекомендовать для использования в serverless варианте.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-r30t1_Dq0KzDsAHCCzb7g.png" /><figcaption>Время распаковки в секундах (меньше лучше)</figcaption></figure><p>Как видно из графика 7z в разы медленнее остальных алгоритмов. Замеры времени распаковки проводились локально, в функции со 128Мб памяти время может быть больше.</p><p>Победитель тут Tar архив сжатый при помощи Zstd (степень сжатия 11). И если посмотреть на замеры скачивания архивов из S3 в функцию, то можем увидеть, что при потоковой распаковке этот вариант не должен вносить сильных дополнительных расходов.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QnfRGZlUOHpxIHrc_ZbGbg.png" /><figcaption>Время скачивания в миллисекундах (меньше лучше)</figcaption></figure><p>Из этого графика так же видно, что время скачивания архива для всех случаев сравнимо или даже меньше времени распаковки, так что даже сильный разброс значений не даст значимого влияния на общее время «холодного» старта.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5hR9Blhvcm9fVvP3Ghhy6g.png" /><figcaption>Время архивации в секундах (меньше лучше)</figcaption></figure><p>Стоит конечно упомянуть, что время создания .tar.zst архива на порядок больше чем .tar.gz. Но для статических индексов (количество поисков &gt;&gt;&gt; количества обновлений), я считаю, это все равно оптимальный вариант.</p><p>Увеличение ресурсов функции (128 → 1024Мб) не дало значительного снижения времени холодного старта. Разница составила не более 10%. Это меньшее влияние, чем выбор оптимального алгоритма сжатия.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NDEsFks40p6xmGc28X91AQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Qy3-vmvOq53thrTiTGcf_A.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jP72_mN1GiRGF9B6OmYopw.png" /><figcaption>Brotli, Zstd, Gzip</figcaption></figure><h4>Построение фасетов</h4><p>Для того чтобы запрос вернул фасеты, вам нужно описать их в поисковом запросе.</p><pre>searchRequest := bleve.NewSearchRequest(query)<br> <br>// для построения фасета по годам мы добаляем числовые дипазоны <br>yearFacet := &amp;bleve.FacetRequest{<br>  Size:  20,<br>  Field: &quot;crYearOfProduction&quot;,<br>}<br><br>for i := 2000; i &lt; 2020; i++ {<br>  minY := float64(i)<br>  maxY := float64(i + 1)<br>  yearFacet.AddNumericRange(strconv.Itoa(i), &amp;minY, &amp;maxY)<br>}<br><br>searchRequest.Facets = bleve.FacetsRequest{<br>  &quot;year&quot;: yearFacet,<br>// фасет по странам будет содержать не более 5 наиболее популярных вариантов<br>  &quot;country&quot;: &amp;bleve.FacetRequest{<br>    Size:  5,<br>    Field: &quot;countryOfProduction&quot;,<br>  },<br>}</pre><p>В ответе сервера мы получим следующее.</p><pre>{  <br>  ...<br>  &quot;total_hits&quot;: 23,<br>  &quot;took&quot;: 24200091, // 24ms<br>  &quot;facets&quot;: {<br>    &quot;country&quot;: {<br>      &quot;field&quot;: &quot;countryOfProduction&quot;,<br>      &quot;total&quot;: 25,<br>      &quot;missing&quot;: 0,<br>      &quot;other&quot;: 3,<br>      &quot;terms&quot;: [<br>        {<br>          &quot;term&quot;: &quot;россия&quot;,<br>          &quot;count&quot;: 11<br>        },<br>        {<br>          &quot;term&quot;: &quot;индия&quot;,<br>          &quot;count&quot;: 5<br>        },<br>        {<br>          &quot;term&quot;: &quot;канада&quot;,<br>          &quot;count&quot;: 3<br>        },<br>        {<br>          &quot;term&quot;: &quot;великобритания&quot;,<br>          &quot;count&quot;: 2<br>        },<br>        {<br>          &quot;term&quot;: &quot;австралия&quot;,<br>          &quot;count&quot;: 1<br>        }<br>      ]<br>    },<br>    &quot;year&quot;: {<br>      &quot;field&quot;: &quot;crYearOfProduction&quot;,<br>      &quot;total&quot;: 23,<br>      &quot;missing&quot;: 0,<br>      &quot;other&quot;: 0,<br>      &quot;numeric_ranges&quot;: [<br>        {<br>          &quot;name&quot;: &quot;2004&quot;,<br>          &quot;min&quot;: 2004,<br>          &quot;max&quot;: 2005,<br>          &quot;count&quot;: 23<br>        }<br>      ]<br>    }<br>  }<br>}</pre><p>Внимательный читатель обратит внимание, что total в фасетах различается. Всего поиск вернул 23 результата, а для стран количество 25. Это потому что у двух документов в этом поле указано 2 страны.</p><blockquote>Вообще хотел бы отметить, что хоть документация для этой библиотеки и существует, но вы довольно быстро перейдете к чтению исходных кодов. Если вас это не пугает, то bleve очень мощный и относительно простой инструмент.</blockquote><p>Как обычно ссылка не <a href="https://github.com/nikolaymatrosov/go-sls-search">репозиторий с кодом</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ebc2c529cd56" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Serverless Full Text Search]]></title>
            <link>https://nikolaymatrosov.medium.com/serverless-full-text-search-e10f9201e905?source=rss-9e9ece12f4d7------2</link>
            <guid isPermaLink="false">https://medium.com/p/e10f9201e905</guid>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[lambda-function]]></category>
            <category><![CDATA[full-text-search]]></category>
            <category><![CDATA[lyra]]></category>
            <category><![CDATA[yandex-cloud]]></category>
            <dc:creator><![CDATA[Nikolay Matrosov]]></dc:creator>
            <pubDate>Wed, 04 Jan 2023 14:16:47 GMT</pubDate>
            <atom:updated>2023-03-22T17:01:03.964Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fIW2viUzo_DfwkRmad7fFA.png" /></figure><p>Если вы для своего проекта выбрали serverless стек, то наверняка вы заметили, что в Яндекс Облаке для него нет решения обеспечивающего полнотекстовый поиск. Ну, то есть вы конечно можете поднять кластер ElasticSearch. Но при его минимальной стоимости и выделяемых ресурсах это наверняка будет стрельбой из пушки по воробьям.</p><p>Но что же делать, если хочется сохранить низкие расходы на инфраструктуру и получить быстрый полнотекстовый поиск?</p><p>Построить индекс и работать с ним из функций. Для этого мы возьмем js библиотеку <a href="https://docs.lyrasearch.io/">Lyra</a>. Это библиотека in-memory полнотекстового поиска, которая работает как на клиенте, так и на сервере.</p><blockquote>Upd: Авторы переименовали библиотеку в <a href="https://docs.oramasearch.com/">Orama</a></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sq8RB6ZgD9TVXrSTKyB1pw.png" /></figure><p>Благодаря использованию оптимизированного <a href="https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D1%84%D0%B8%D0%BA%D1%81%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE">префиксного дерева</a> и некоторым хакам Lyra может выполнять поиск по миллионам записей за миллисекунды.</p><h4>Использование</h4><p>Для начала вам нужно будет создать индекс. Для этого нужно описать схему документа. Поля, не описанные в схеме, не будут индексироваться, но будут доступны в результатах поиска.</p><pre>import { create, insert } from &#39;@lyrasearch/lyra&#39;; <br><br>const movieDB = create({<br>  schema: {<br>    title: &#39;string&#39;,<br>    director: &#39;string&#39;,<br>    plot: &#39;string&#39;,<br>    year: &#39;number&#39;,<br>    isFavorite: &#39;boolean&#39;<br>  }<br>});</pre><blockquote>На текущий момент Lyra версии <em>0.3</em> умеет искать только по строковым полям, так что остальные поля описывать в схеме бесполезно. Если верить официальной документции то такая возможность может появиться в будущем.</blockquote><p>То есть в описании схемы можно ограничиться только следующим.</p><pre>import { create, insert } from &#39;@lyrasearch/lyra&#39;; <br><br>const movieDB = create({<br>  schema: {<br>    title: &#39;string&#39;,<br>    director: &#39;string&#39;,<br>    plot: &#39;string&#39;,<br>  }<br>});</pre><p>Далее вставить в него документы.</p><pre>const { id: thePrestige } = insert(movieDB, {<br>  title: &#39;The prestige&#39;,<br>  director: &#39;Christopher Nolan&#39;,<br>  plot: &#39;Two friends and fellow magicians become bitter enemies after a sudden tragedy. As they devote themselves to this rivalry, they make sacrifices that bring them fame but with terrible consequences.&#39;,<br>  year: 2006,<br>  isFavorite: true<br>});<br><br>const { id: bigFish } = insert(movieDB, {<br>  title: &#39;Big Fish&#39;,<br>  director: &#39;Tim Burton&#39;,<br>  plot: &#39;Will Bloom returns home to care for his dying father, who had a penchant for telling unbelievable stories. After he passes away, Will tries to find out if his tales were really true.&#39;,<br>  year: 2004,<br>  isFavorite: true<br>});</pre><p>Также есть метод для пакетной вставки документов insertBatch.</p><p>Для поиска по подготовленному индексу можно воспользоваться методом search. Подробнее про параметры поиска можно прочитать в <a href="https://docs.lyrasearch.io/usage/search#filters">документации</a>.</p><pre>const searchResult = search(movieDB, {<br>  term: &quot;Harry&quot;,<br>  properties: &quot;*&quot;,<br>});</pre><h3>Эксперимент</h3><p>Для того чтобы потестировать эту библиотеку я нашел датасет русских анекдотов на 26Мб, 124155 документов. Мне показалось, что это неплохой размер данных для serverless поиска. Если у вас данных больше, то наверняка вам стоит задуматься об использовании нормального инструмента, а не мастерить велосипеды.</p><p>Итак, для начала я импортировал датасет в индекс Lyra и посмотрел, сколько он будет занимать в сериализованном виде.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oyJofEYuAijzYoUFLEGkeQ.png" /><figcaption>Сравнение размеров сериализованных индексов с размером исходных данных.</figcaption></figure><p>Как видно из графика индекс сериализованный в JSON занимает 130Mb. Lyra предоставляет плагин для сериализации. Кроме json там есть binary(206Mb) и dpack(123Mb) форматы. И вроде dpack чуть меньше, но очевидно, что можно получить результат лучше. Для этого я пожал JSON разными популярными алгоритмами сжатия.</p><p>Почему именно они? Потому что они есть в стандартной библиотеке Node.js zlib.</p><p>Почему я не стал рассматривать сжатие binary и dpack? Потому что кроме размера получаемого артефакта я также посмотрел на время его десереализации. Данные получены на моем ноутбуке, так что стоит смотреть скорее не на абсолюты, а на относительную разницу.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WsdbKvq6UjEsgVF28NS2Ug.png" /><figcaption>Сравнение времени десереализации индекса из разных форматов.</figcaption></figure><p>Как видно большую часть времени занимает именно построение объекта индекса, что даже добавление распаковки из архива не сильно сказывается на общем времени. При этом бинарные форматы проигрывают JSON. Он отлично оптимизирован. Именно поэтому я не стал экспериментировать с дополнительным их сжатием, так как это только увеличило бы их отставание от JSON.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pSYQYIP9CvKD4yXj-qhJoA.png" /><figcaption>Относительные значения</figcaption></figure><p>Теперь посмотрим на время сжатия. Эта метрика важна для нас если мы хотим иметь не статический индекс, а обновлять его периодически.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lYEfVWhO9rOnodW1fNvsGw.png" /></figure><p>Как видно brotli с минимальной степенью сжатия 0 дает тот же самый размер, что и gzip в режиме приоритета скорости, при этом всё равно выигрывая его в два с половиной раза по скорости. Более высокие степени сжатия хоть и дают меньший размер артефакта сильно проигрывают в скорости. Как вы могли заметить на график не попал brotli11 с максимальной степенью сжатия 11. Это потому что на текущем индексе сжатие занимало 4.5–5 минут, хотя и давало наименьший результат. Этот вариант я могу рекомендовать лишь для статических индексов, где нет необходимости обновления, а значит затрат на их обновление.</p><p>Для дальнейших исследований я выберу gzip (как вариант для обновляемых индексов) и brotli11 (вариант для статики), чтобы сравнить, как размер сериализованного индекса скажется на времени холодного старта функции.</p><p>Кроме представленных форматов я рассматривал еще и Protobuf. Но к сожалению он не поддерживает вложенные словари, а формат индекса их активно использует. В итоге у меня получилось написать сериализатор, но проигрывал в скорости JSON, требовал дополнительно описывать структуру индекса и в итоге не попал в сравнение.</p><h4>Функция</h4><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/07a6b2b3b8016570391f2c1cabafa0cd/href">https://medium.com/media/07a6b2b3b8016570391f2c1cabafa0cd/href</a></iframe><p>Кажется код достаточно понятен. На первом старте индекс будет подгружен из Object Storage и десериализован. Поэтому холодный старт будет, в нашем случае не очень маленького индекса, около 5 секунд.</p><h4>Подводные камни</h4><p>Так же я наступил в ошибку: загрузка приводит к ошибке аллокации памяти Reached heap limit Allocation failed — JavaScript heap out of memory. Несмотря на то, что при создании версии ее было выделено достаточно (2Гб), функция падает в районе потребления 256Мб.</p><pre>&lt;--- Last few GCs ---&gt;<br><br>[8:0x4cc6180]     3469 ms: Mark-sweep (reduce) 254.6 (256.6) -&gt; 254.3 (256.9) MB, 11.0 / 0.0 ms  (+ 4.0 ms in 228 steps since start of marking, biggest step 2.6 ms, walltime since start of marking 84 ms) (average mu = 0.950, current mu = 0.859) allocation<br>[8:0x4cc6180]     3486 ms: Mark-sweep (reduce) 255.3 (256.9) -&gt; 255.3 (257.9) MB, 12.3 / 0.0 ms  (average mu = 0.899, current mu = 0.283) allocation failure scavenge might not succeed<br><br><br>&lt;--- JS stacktrace ---&gt;<br><br>FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory</pre><p>В принципе, решение этой проблемы довольно легко <a href="https://felixgerschau.com/javascript-heap-out-of-memory-error/">гуглится</a>: нужно передать node.js флаг --max-old-space-size=2048, где число — количество мегабайт выделяемой памяти. Не самыми очевидными были следующие шаги — как передать его, чтобы он надежно подхватился рантаймом? Скажу честно, их мне подсказали в поддержке:</p><ol><li>Этот флаг можно передать через переменную окружения NODE_OPTIONS. Но это не решает проблему до конца, так как последняя версия Node-рантайма nodejs16 является предзагружаемой — это значит, что для нее применяются оптимизации, которые позволяют значительно уменьшить время холодного старта. К сожалению, они приводят к тому, что вы можете получить рантайм из пула предсозданных, а значит ключи из NODE_OPTIONS к нему не применятся.</li><li>Оптимизация холодного старта актуальна в случае обычных функций, у которых время исполнения измеряется десятками миллисекунд. Наша же функция на первом старте в любом случае грузит индекс, так что выигрышем в пары сотен миллисекунд на первом старте мы можем пожертвовать. Они не будут заметны на фоне остального времени старта в 5 секунд. Поэтому мы можем применить второй флаг, отключающий эту оптимизацию: YCF_NO_RUNTIME_POOL=1</li></ol><p>Давайте посмотрим на результаты стрельб Танком.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aMzz-vWwRsyLpOMiufgmDA.png" /><figcaption>До включения YCF_NO_RUNTIME_POOL=1</figcaption></figure><p>Как видно из результатов, часть рантаймов досталась нам из пула и это привело к 502 ошибкам. Ошибки 429 — это слишком активно отсылаемые Танком запросы в тот момент пока создаются инстансы рантаймов и не было достаточно ресурсов чтобы их обработать.</p><p>После отключения рантайм пула картина следующая.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RkDIDbU8bIZcdEpMO51pxA.png" /></figure><p>Совсем избавиться от 429 ошибок не получается, зато 502 ошибки ушли.</p><p>Теперь если посмотреть на результаты времени поиска в индексе, то типичными значениями будут 30–50мс, а общее время выполнения запроса 250–300мс. На мой взгляд очень неплохо.</p><h4>Обновление индекса</h4><p>Перестроение индекса можно организовать в функции вызываемой по cron-триггеру, она будет вычитывать обновления из БД и дописывать их и индекс, а затем заливать его в S3.</p><p>В случае же, если вам не нужно обновлять данные, то их можно попробовать положить рядом с кодом функции. Это позволит избавиться от лишних походов в S3, тем самым выиграть еще немного на холодном старте.</p><p>Ну и напоследок ссылка на <a href="https://docs.lyrajs.io/demo">live demo</a> от авторов и репозиторий с кодом <a href="https://github.com/nikolaymatrosov/sls-search">примера</a>.</p><p><strong>UPD</strong>: Если вам, как и мне, казалось, что время «холодного» старта можно улучшить, то про это я расскажу в <a href="https://medium.com/p/ebc2c529cd56">следующем посте</a>. Правда там будет уже не JavaScript, а Go.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e10f9201e905" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Cloud Development Kit для Terraform]]></title>
            <link>https://nikolaymatrosov.medium.com/cloud-development-kit-%D0%B4%D0%BB%D1%8F-terraform-49d612b4272b?source=rss-9e9ece12f4d7------2</link>
            <guid isPermaLink="false">https://medium.com/p/49d612b4272b</guid>
            <category><![CDATA[cdk]]></category>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[typescript]]></category>
            <dc:creator><![CDATA[Nikolay Matrosov]]></dc:creator>
            <pubDate>Sat, 03 Dec 2022 11:40:11 GMT</pubDate>
            <atom:updated>2022-12-03T11:40:11.493Z</atom:updated>
            <content:encoded><![CDATA[<p>Что это такое? Удобнее ли обычного Terraform? И можно ли им заменить Serverless framework?</p><h3>Что такое TF CDK?</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l6kk8f-setpNooW5_lyMzA.png" /></figure><p>Cloud Development Kit позволит вам описывать ресурсы Terraform на привычном вам языке программирования, используя все его нативные конструкции (циклы, условия и т.п.).</p><p>Первыми такую концепцию описания ресурсов представили AWS, выпустив в 2018 году первые беты своего CDK.</p><p>На самом деле, CDK для Terraform использует концепцию «стека приложения» из библиотеки AWS.</p><h4>Как же работает TF CDK?</h4><p>CDK, основываясь на описании инфраструктуры на одном из поддерживаемых языков (Go, TypeScript, Python, Java и C#), <a href="https://developer.hashicorp.com/terraform/cdktf/cli-reference/commands#synth">синтезирует</a> JSON документ описанного <a href="https://developer.hashicorp.com/terraform/language/syntax/json">формата</a>, который в свою очередь может быть использован Terraform’ом для развертывания инфраструктуры.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3G2Uxe7aSJOHGHObaov7-w.png" /></figure><p>Для того чтобы начать пользоваться CDK, вам нужно <a href="https://developer.hashicorp.com/terraform/cdktf/cli-reference/cli-configuration">установить</a> его при помощи npm.</p><pre>npm install -g cdktf-cli</pre><p>Теперь разберемся откуда же возьмутся библиотеки для языка программирования, если для терраформа описаны лишь провайдеры? Варианта два:</p><ol><li>Для самых популярных провайдеров вы можете установить предкомпилированные пакеты.</li><li>Во всех остальных случаях нужно лишь указать в cdktf.json список используемых провайдеров и выполнить командуcdktf get. Она скачает нужные провайдеры и сгененрирует на их основе все необходимое.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*se3mjbm35E2OWf8MYuGU-A.png" /><figcaption>Схема генерации библиотек для CDK</figcaption></figure><h4>Архитектура CDK приложения</h4><p>Приложения написанные с использованием CDKTF представляют собой дерево из <a href="https://github.com/aws/constructs">конструктов</a>, которые в документации AWS определены как «классы, описывающие “часть состояния системы”».</p><p>Тремя основными классами для создания приложения с CDKTF являются App, Stackи Resource.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zwgw1u9Cn-KDvmgEjJD1Gg.png" /></figure><p>Каждый проект построенный на CDKTF имеет один или насколько инстансов App— это контейнеры для конфигурации инфраструктуры, которую вы создаете и развертываете. App может содержать несколько <a href="https://developer.hashicorp.com/terraform/cdktf/concepts/stacks">Stack</a>, которые представляют наборы связанной инфраструктуры. Stack синтезирует отдельную конфигурацию терраформа, аналог <a href="https://developer.hashicorp.com/terraform/cli/init">рабочей директории</a>.</p><p>Стеки позволяют разделить управление состоянием внутри приложения. Например, вы можете захотеть развернуть отдельные ресурсы инфраструктуры для разработки и тестирования. Подробнее в <a href="https://developer.hashicorp.com/terraform/cdktf/concepts/stacks">документации</a>.</p><p><a href="https://developer.hashicorp.com/terraform/cdktf/concepts/resources">Resource</a> описывает отдельный объект инфраструктуры, точно так же как и в обычном терраформе.</p><h3>В чем же здесь удобство?</h3><p>Во-первых, вы пишете код не на json или hcl, а на вашем любимом языке программирования. У вас есть вся мощь IDE, которые умеют отлично навигироваться по коду и давать подсказки.</p><p>Во-вторых, вы можете удобно задать дефолтные значения и не писать их каждый раз заново.</p><p>В-третьих, вы можете, используя композицию, определять свои <a href="https://developer.hashicorp.com/terraform/cdktf/develop-custom-constructs/construct-design">собственные ресурсы</a>, тем самым повышая удобство и переиспользование кода. При этом, ресурсы описанные на одном языке программирования могут быть <a href="https://developer.hashicorp.com/terraform/cdktf/develop-custom-constructs/publishing-and-distribution">опубликованы</a> и стать доступными для использования на любом из поддерживаемых языков.</p><blockquote>Например, в вашей организации принято использовать бакеты S3 с включенным шифрованием и версионированием. Для того, чтобы сделать это в обычном терраформе вам нужно не забыть определить эти параметры каждый раз, когда вы описываете бакет. Используя CDK вы же можете описать свой ресурс, который будет основан на Bucket, назвать его EncryptedVersionedBucket, описать для него один раз все необходимые параметры, а затем поделиться им с коллегами.</blockquote><p>В-четвертых, у вас есть возможность <a href="https://developer.hashicorp.com/terraform/cdktf/test/unit-tests">писать тесты</a>, ведь код CDK приложения ничем не отличается от другого кода. В тестах вы можете проверять использование принятых в вашей компании правил создания инфраструктуры, начиная от именования ресурсов, заканчивая соблюдением заданных политик безопасности, а также подстраховаться от нежелательного изменения ресурсов.</p><h3>TF CDK vs Serverless Framework</h3><p>Если рассматривать Serverless Framework только как средство декларативного деплоя функций, то тут на мой взгляд CDK удобнее. Вам не нужно забираться как описать что-то в YAML.</p><p>Но Serverless имеет неплохой набор плагинов и пусть они не всегда хорошо совместимы друг с другом, ожидают разное формат конфигурационного файла, но они добавляют функциональность, которой вам может не хватать в CDK.</p><h3>Пример.</h3><p>Чтобы продемонстрировать TF CDK, я переписал деплой одного из моих старых примеров из поста про serverless snapshot’ы с использованием CDK.</p><p><a href="https://github.com/nikolaymatrosov/yc-serverless-snapshot/blob/b06b3e360f402af71833d54b63ab7f0930f1d36c/tfcdk/main.ts">yc-serverless-snapshot/main.ts at b06b3e360f402af71833d54b63ab7f0930f1d36c · nikolaymatrosov/yc-serverless-snapshot</a></p><p>Еще идеи для вдохновения можно поискать тут. Стоит отметить, что примеры отсюда используют AWS CDK, но так как TF CDK перенял от него все основные концепции, их можно переписать и для него.</p><p><a href="https://github.com/aws-samples/aws-cdk-examples">GitHub - aws-samples/aws-cdk-examples: Example projects using the AWS CDK</a></p><p>Еще один полезный ресурс по этой теме — <a href="https://cdk.dev/">https://cdk.dev/</a>. Там можно найти ссылки на статьи и примеры с использованием AWS CDK, Terraform CDK и K8s CDK.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=49d612b4272b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Генерация приватных ключей Ed25519 на Go]]></title>
            <link>https://nikolaymatrosov.medium.com/%D0%B3%D0%B5%D0%BD%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F-%D0%BF%D1%80%D0%B8%D0%B2%D0%B0%D1%82%D0%BD%D1%8B%D1%85-%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%B9-ed25519-%D0%BD%D0%B0-go-35ac80aa213a?source=rss-9e9ece12f4d7------2</link>
            <guid isPermaLink="false">https://medium.com/p/35ac80aa213a</guid>
            <category><![CDATA[go]]></category>
            <category><![CDATA[ssh]]></category>
            <category><![CDATA[packers]]></category>
            <category><![CDATA[ed25519]]></category>
            <dc:creator><![CDATA[Nikolay Matrosov]]></dc:creator>
            <pubDate>Sat, 06 Aug 2022 14:44:08 GMT</pubDate>
            <atom:updated>2022-09-14T16:12:46.642Z</atom:updated>
            <content:encoded><![CDATA[<p>Небольшая предыстория. Мне понадобилось собрать Ubuntu 22.04 Jammy Jellyfish при помощи Packer используя ssh provisioner. А для дебага этого процесса захотелось научиться сохранять создаваемые пакером ключи на диск, чтобы с их помощь можно было зайти на ВМ.</p><p>Начиная с версии 8.7 (а в jammy стоит 8.9) <a href="https://www.openssh.com/txt/release-8.7">OpenSSH</a> считает небезопасными ключи ssh-rsa и отключает их по умолчанию.</p><blockquote>В протоколе SSH схема подписи ssh-rsa использует SHA-1<br>алгоритм хэширования в сочетании с алгоритмом открытого ключа RSA. <a href="https://eprint.iacr.org/2020/014.pdf">Сейчас</a> можно выполнять атаки с выбранным префиксом против алгоритма SHA-1 менее чем за 50 тысяч долларов США.</blockquote><p>Стоит отметить, что деактивация подписей ssh-rsa необязательно<br>требует прекращения использования ключей RSA. В протоколе SSH ключи могут быть подписаны с использованием нескольких алгоритмов. В частности, ssh-rsaключи могут подписываться с использованием rsa-sha2–256 (RSA/SHA256), rsa-sha2–512(RSA/SHA512) и ssh-rsa (RSA/SHA1). Так вот по умолчанию отключен только последний из них.</p><p>Но так как Packer написан на Go, а в Go до сих пор <a href="https://github.com/golang/go/issues/49952">не поддержаны</a> нормально разрешенные алгоритмы подписи RSA ключей, мой выбор пал на формат ключей ssh-ed25519, так как он входит в список рекомендованных OpenSSH альтернатив и поддерживается в Go.</p><p>Одна загвоздка. Go умеет создавать такие ключи. Но совершенно не озаботился включить в стандартную библиотеку метод для сериализации ключа в формат, совместимый с OpenSSH.</p><p>Отправной точкой для меня стал <a href="https://github.com/hashicorp/packer-plugin-sdk/blob/main/communicator/sshkey/generate.go#L69">метод в SDK Packer’а</a>. К сожалению автор честно признается в комментарии к коду, что его ключи при использовании в OpenSSH выдают ошибку:</p><pre>PairFromED25519 marshalls a valid pair of openssh pem for ED25519 keypairs.<br>NewPair can handle ed25519 pairs but generates the wrong format apparently:<br>`<strong>Load key &quot;id_ed25519&quot;: invalid format`</strong> is the error that happens when I try to ssh with such a key.</pre><p>В этот момент я попытался найти документацию, как же должен выглядеть валидный формат.</p><p><a href="https://www.rfc-editor.org/rfc/rfc4253.html">RFC 4253, раздел 6.6</a> описывает формат открытых ключей OpenSSH, и, следуя этому RFC, довольно легко реализовать синтаксический анализатор и декодировать различные биты, составляющие открытый ключ OpenSSH.</p><p>Однако, в отличие от открытых ключей OpenSSH, не существует документа RFC, описывающего бинарный формат закрытых ключей, которые генерируются ssh-keygen.</p><p>Ответ, как это не удивительно, был найден в <a href="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key">репозитории OpenSSH</a>.</p><pre>1. Overall format</pre><pre>The key consists of a header, a list of public keys, and<br>an encrypted list of matching private keys.</pre><pre>#define AUTH_MAGIC      &quot;openssh-key-v1&quot;</pre><pre> byte[] AUTH_MAGIC<br> string ciphername<br> string kdfname<br> string kdfoptions<br> uint32 number of keys N<br> string publickey1<br> string publickey2<br> ...<br> string publickeyN<br> string encrypted, padded list of private keys</pre><p>Сначала дан общий обзор формата. Далее подробнее разбирается возможные значения для полей.</p><pre>2. KDF options for kdfname &quot;bcrypt&quot;</pre><pre>The options:</pre><pre> string salt<br> uint32 rounds</pre><pre>are concatenated and represented as a string.</pre><p>Кстати, кроме значения bcrypt можно также использовать none(об этом говорится в пункте 5). Что мы и будем делать, так как это проще, а у меня нет цели написать сериализатор, который будет поддерживать все возможные кейсы.</p><p>Теперь разберемся как должны быть указаны приватные ключи.</p><pre>3. Unencrypted list of N private keys</pre><pre>The list of privatekey/comment pairs is padded with the<br>bytes 1, 2, 3, ... until the total length is a multiple<br>of the cipher block size.</pre><pre> uint32 checkint<br> uint32 checkint<br> byte[] privatekey1<br> string comment1<br> byte[] privatekey2<br> string comment2<br> ...<br> string privatekeyN<br> string commentN<br> byte 1<br> byte 2<br> byte 3<br> ...<br> byte padlen % 255</pre><pre>where each private key is encoded using the same rules as used for<br>SSH agent.</pre><pre>Before the key is encrypted, a random integer is assigned<br>to both checkint fields so successful decryption can be<br>quickly checked by verifying that both checkint fields<br>hold the same value.</pre><p>Видно, что формат допускает наличие нескольких приватных ключей. Мы же будем использовать всего лишь один. Комменты тоже дописывать не будем.</p><p>Обратите внимание на паддинг. Мы к нему еще вернемся чуть позже.</p><p>Далее приведены опции для шифрованных и нешифрованных ключей. И нам интересна именно последняя.</p><pre>4. Encryption</pre><pre>The KDF is used to derive a key, IV (and other values required by<br>the cipher) from the passphrase. These values are then used to<br>encrypt the unencrypted list of private keys.</pre><pre>5. No encryption</pre><pre>For unencrypted keys the cipher &quot;none&quot; and the KDF &quot;none&quot;<br>are used with empty passphrases. The options if the KDF &quot;none&quot;<br>are the empty string.</pre><p>Как видно функция для сериализации ключа в многом повторяет описанный формат, но есть некоторые отличия.</p><p>Для того чтобы наглядно убедиться, что результат работы ssh-keygen и go совпадает, я поставил <a href="https://github.com/WerWolv/ImHex">https://github.com/WerWolv/ImHex</a>. (Про который услышал в <a href="https://radio-t.com/p/2022/07/30/podcast-817/">последнем выпуске Радио-Т</a>)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2Joh4ZZp9UX4irGuy6k7Gw.jpeg" /><figcaption>На скриншоте видно, что это уже моя шестая попытка генерации ключа.</figcaption></figure><p>Отличия состояли в том, что в файл не записывался публичный ключ отдельным полем, как это описано в п.1. А так же не хватало паддинга приватного ключа.</p><p>Оставалось только разобраться, какой же паддинг ожидается. В пункте 3 говорится, что нужно добиться кратности размеру блока шифрования. Но так как у нас нет шифрования, то непонятно какой размер ожидается. К счастью <a href="https://dnaeon.github.io/openssh-private-key-binary-format/">в статье</a> такого же исследователя приватных ed25519 ключей, как и я, я нашел подсказку. Допустимые значения шифров и размеры блоков описаны в <a href="https://github.com/openssh/openssh-portable/blob/eba523f0a130f1cce829e6aecdcefa841f526a1a/cipher.c#L86-L115">cipher.c</a>.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/189831141a8403f180ef95ec15d352ac/href">https://medium.com/media/189831141a8403f180ef95ec15d352ac/href</a></iframe><p>И как видно для нашего none размер блока должен быть 8.</p><p>С этими исправлениями у мен получилось собрать рабочее решение.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/df8620638b65ecdb6d75fa6340bc619e/href">https://medium.com/media/df8620638b65ecdb6d75fa6340bc619e/href</a></iframe><blockquote>Ну и напоследок интересный факт: номер в названии алгоритма обозначает кривую на которой он основан —<a href="https://en.wikipedia.org/wiki/Curve25519"> Curve25519</a>. А она в свою очередь так названа, потому что является <a href="https://en.wikipedia.org/wiki/Montgomery_curve">Кривой Монтгомери</a> над <a href="https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%BB%D0%B5_(%D0%B0%D0%BB%D0%B3%D0%B5%D0%B1%D1%80%D0%B0)#%D0%9F%D1%80%D0%BE%D1%81%D1%82%D0%BE%D0%B5_%D0%BF%D0%BE%D0%BB%D0%B5">простым полем</a>, определяемым простым числом 2²⁵⁵-19.</blockquote><p>Upd: pull request с этими изменениями приняли и он попал в <a href="https://github.com/hashicorp/packer-plugin-sdk/releases/tag/v0.3.2">релиз SDK 0.3.2</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=35ac80aa213a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Запуск Serverless Container по таймеру]]></title>
            <link>https://nikolaymatrosov.medium.com/%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA-serverless-container-%D0%BF%D0%BE-%D1%82%D0%B0%D0%B9%D0%BC%D0%B5%D1%80%D1%83-3872bd402db0?source=rss-9e9ece12f4d7------2</link>
            <guid isPermaLink="false">https://medium.com/p/3872bd402db0</guid>
            <category><![CDATA[yandex-cloud]]></category>
            <category><![CDATA[cron]]></category>
            <category><![CDATA[serverless-containers]]></category>
            <dc:creator><![CDATA[Nikolay Matrosov]]></dc:creator>
            <pubDate>Sun, 24 Jul 2022 13:15:29 GMT</pubDate>
            <atom:updated>2023-01-22T13:34:12.953Z</atom:updated>
            <content:encoded><![CDATA[<h3>Запуск Serverless Container по триггеру</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MpKmpBUm75S70NUJIaktdQ.png" /></figure><p>Итак, у вас есть цель выполнять какую-то задачу внутри serverless контейнера по расписанию. К сожалению, вы не сможете просто взять положить в контейнер ваш код и надеяться что все заработает просто так.</p><p>Тут нужно уточнить детали реализации взаимодействия serverless контейнера и cron-триггера, который будет его запускать. Триггер запускает контейнер просто отправляя ему http-запрос в корень (path /) и ожидая в конце выполнения ответа c HTTP кодом200.</p><p>Если же вы в контейнере не реализуете HTTP-сервер, то триггер не может узнать о том, корректно ли завершилось выполнение контейнера. Это будет воспринято как ошибка и триггер попробует запустить контейнер снова. В итоге вы получите много запусков вашего кода, которых вы не ожидали, и большой счёт за их выполнение.</p><p>Поэтому правильно будет добавить в контейнер простой HTTP-сервер, который будет слушать на порту, переданному в переменной окружения PORT, и по окончанию выполнения полезной работы сможет корректно ответить триггеру.</p><h4>Сервер на Go</h4><p>Давайте напишем простой сервер на go, и добавим его в наш контейнер.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d00843c08c2ff7fa2f91675faaee18a2/href">https://medium.com/media/d00843c08c2ff7fa2f91675faaee18a2/href</a></iframe><p>При обработке запроса от триггера мы просто запускаем скрипт command.sh в котором можно описать произвольные команды.</p><h4>Dockerfile</h4><p>Теперь надо упаковать это все в докер-контейнер.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/94a40ff030a77ad7930c49847b5e59fb/href">https://medium.com/media/94a40ff030a77ad7930c49847b5e59fb/href</a></iframe><p>Здесь мы используем подход многофазного построения контейнера. На первом шаге мы собираем наш сервер. Назовем этот контейнер build.</p><p>На втором шаге просто копируем сервер в чистый образ ubuntu:22.04. Туда же вы можете добавить все необходимое для запуска вашей задачи. А сам процесс запуска описать в command.sh.</p><p>Таким образом вы сможете легко корректно обрабатывать вызовы триггера и не получать в логах контейнера сообщений типа:</p><pre>{&quot;errorMessage&quot;:&quot;user container finished with error: exit status 1&quot;,&quot;errorType&quot;:&quot;UserCodeError&quot;}</pre><p>Аналогично можно написать сервер которы будет обрабатывать сообщения от других типов триггеров, с той лишь разницей, что, вероятно, придется добавить код разбора сообщения, полученного от триггера.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3872bd402db0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Про асинхронность в Cloud Functions]]></title>
            <link>https://nikolaymatrosov.medium.com/%D0%BF%D1%80%D0%BE-%D0%B0%D1%81%D0%B8%D0%BD%D1%85%D1%80%D0%BE%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D1%8C-%D0%B2-cloud-functions-536cbcf8fe20?source=rss-9e9ece12f4d7------2</link>
            <guid isPermaLink="false">https://medium.com/p/536cbcf8fe20</guid>
            <category><![CDATA[sqs]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[yandex-cloud]]></category>
            <dc:creator><![CDATA[Nikolay Matrosov]]></dc:creator>
            <pubDate>Sun, 26 Jun 2022 13:04:01 GMT</pubDate>
            <atom:updated>2022-07-30T13:01:55.779Z</atom:updated>
            <content:encoded><![CDATA[<p>Сегодня я хотел бы разобрать модель работы облачных функций в Яндекс Облаке.</p><p>Функции в облаке запускаются <strong>синхронно</strong> и в один момент времени обрабатывают <strong>ровно один запрос</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Y1L-Rm9vvdRWvrSpxhaE0Q.png" /></figure><p>Это значит, что вы можете быть уверены, что все переменные созданные внутри обработчика будут относиться именно к текущему контексту. Т.е. если вы в обработчике идете в базу, чтобы получить текущего пользователя, это будет именно тот пользователь, чей id пришел в запросе.</p><p>Однако, экземпляр рантайма может быть переиспользован для обработки разных запросов. То есть, когда закончится обработка первого запроса, в тот же рантайм может быть передан для обработки следующий запрос. Ему могут быть доступны переменные определенные вне функции-обработчика.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gm5aKuOZ3xN13T7nEesaCw.png" /></figure><p>Это одновременно позволяет нам кешировать между вызовами объекты, на чью инициализацию может потребоваться время (например, подключения к базе данных), но так же заставляет нас внимательнее следить, что мы кешируем. Например, не стоит в глобальные переменные помещать данные про пользователя, так как следующий запрос может относиться к другому пользователю.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dyNIL6RJ48oJhH90Le8RwQ.png" /></figure><p>Теперь подробнее разберем, что значит синхронность запуска функции.</p><p>Для этого возьмем простую функцию на Python, которая будет делать только одно: ждать 1 секунду и возвращать ответ Ok.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/67e328444385e6128fccf27f9be4b8ce/href">https://medium.com/media/67e328444385e6128fccf27f9be4b8ce/href</a></iframe><p>Теперь, если мы вызовем ее при помощи curl, то через 1 секунду получим ответ Ok.</p><pre>curl &#39;<a href="https://functions.yandexcloud.net/d4eic18eqks97gmifd3l&#39;">https://functions.yandexcloud.net/d4eic18eqks97gmifd3l&#39;</a></pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*57iyx5Gn9ttkYSBPLMMdSA.png" /></figure><p>Но, что будет, если мы будем ждать ответа не 1 секунду, 500мс?</p><pre>curl -m .5 &#39;<a href="https://functions.yandexcloud.net/d4eic18eqks97gmifd3l&#39;">https://functions.yandexcloud.net/d4eic18eqks97gmifd3l&#39;</a></pre><p>В ответ мы увидим вот что:</p><pre>curl: (28) Operation timed out after 504 milliseconds with 0 bytes received</pre><p>А в логах функции:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*saahVJguaWpkRP2Q22RTFg.png" /></figure><p>Что же произошло?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0LrNqt5Sn725V5CQ_1taqg.png" /></figure><p>Рантайм функций увидел, что клиент разорвал соединение и больше не ждет ответа, значит функция не сможет вернуть результат своего выполнения, вызвавшему её коду, а значит можно больше не тратить деньги клиента Облака (ведь облачные функции тарифицируются именно за время исполнения) и прервать функцию. В логах такое поведение можно найти по коду ответа 499. Этот нестандартный код ответа говорит, что клиент не дождался и отменил запрос.</p><blockquote>При этом эта ошибка в логах необязательно говорит о каких-то проблемах. Ведь логика клиентского приложения может быть построена так, что оно отменяет запросы в которых более не нуждается, чтобы тем самым снизить нагрузку и на сервер.</blockquote><blockquote>То есть, например, пользователь решил не ждать загрузки витрины. Отлично, можно отменить этот запрос, ведь нам не понадобится его результат для отрисовки страницы в браузере.</blockquote><p>Что же это значит для разработчика, который пишет приложение в serverless модели? Нужно постараться не использовать синхронные вызовы одной функции из другой.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Z11WmSAXRzdxb083S05k0g.png" /><figcaption>Каскадный таймаут</figcaption></figure><p>Потому что когда наступит таймаут у внешней функции, то и вызванная из неё функция завершится с ошибкой. И тут либо аккуратно следить за настройками функций, поддерживая таймауты у внешних чуть больше, чем у внутренних, либо использовать асинхронные подходы.</p><blockquote>К тому же, вызывая одну функцию из другой, вы заплатите вдвойне, т.к. будет работать два экземпляра рантаймов. Так же придется следить за квотами.</blockquote><p>В итоге если функции написаны на одном языке программирования, то проще вызвать код вложенной функции нативно: включив его в сборку внешней функции.</p><h4>Асинхронность в функциях.</h4><p>Первое, о чем вы могли подумать: «Точно просто вместо обычной функции-обработчика сделаем её асинхронной!»</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ff4f2a53d0a13ceb2741a58ca2bbbba3/href">https://medium.com/media/ff4f2a53d0a13ceb2741a58ca2bbbba3/href</a></iframe><p>Но это не поможет. Да обработчик стал асинхронным, теперь мы можем использовать в нём синтаксический сахар async/await, но вызов функции так и останется синхронным. То есть, если вы вызывая функцию по http, не дождетесь окончания ее исполнения, то она точно так же как и в случае с синхронным обработчиком вернёт 499 код ошибки.</p><p>Так что же делать, если вы хотите выполнить в облачной функции какую-то длительную работу и не хотите ждать ее завершения, и вас бы устроило, чтобы например результат был записан в базу данных?</p><p>Для этого можно использовать очереди сообщений, таким образом развязать постановку задачи и ее выполнение. А заодно и распараллелить выполнение задач.</p><h4>Как положить сообщение в очередь?</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8ypQKP_Q54zv2yHbWsu9aA.png" /></figure><p>У нас есть как минимум два похода:</p><ol><li>Мы можем напрямую взаимодействовать с очередью через AWS SDK (т.к. YMQ поддерживает протокол AWS SQS);</li></ol><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/7d1c64a464587c3aa985e634244cff6e/href">https://medium.com/media/7d1c64a464587c3aa985e634244cff6e/href</a></iframe><p>2. Воспользоваться <a href="https://cloud.yandex.ru/docs/api-gateway/concepts/extensions/ymq">интеграцией</a> API Gateway.</p><p>После этого нужно будет лишь <a href="https://cloud.yandex.ru/docs/functions/concepts/trigger/ymq-trigger">настроить триггер</a>, который будет обрабатывать сообщения из очереди и готово.</p><p>Таким образом, если у вас есть долгая операция и требование от внешней системы быстро отвечать на запросы, вы сможете выполнить оба условия. В первой функции быстро складывать данные в очередь задач и возможно проверять по ключу в базе готова ли задача, а другой отдельной асинхронной функции (кстати, это может быть даже и не функция, а виртуальная машина, в том случае, если вам не хватает ограничений serverless среды и нужны, например большие ресурсы, вычисления на GPU или доступ к сервисам из внутренней VPC) выполнять отложенные задачи.</p><h4>Ограничения YMQ.</h4><p>У вас может возникнуть желание применить описанный выше подход и для FIFO-очередей. К сожалению в текущей реализации триггеров вы не сможете создать триггер для FIFO-очереди YMQ. В таком случае у вас есть опции использовать для разбора очереди приложение развернутое на ВМ, в нём вы сможете читать любые YMQ очереди. Или же, если вы хотите оставаться serverless, вы можете использовать Yandex Data Streams.</p><p>Это сервис, который реализует API совместимое с AWS Kinesis. Вы точно так же, как и в случае с YMQ, сможете писать в него при помощи либо AWS SDK, либо интеграции с API Gateway. Yandex Data Streams предоставит вам гарантии порядка обработки сообщений записанных в стрим. Вам лишь нужно будет <a href="https://cloud.yandex.ru/docs/data-streams/concepts/partition-keys">определить ключ</a>, по которому будут шардироваться сообщения и помнить, что порядок гарантируется только в рамках сегмента, который задается ключом.</p><p>То есть, если вы хотите гарантировать, что сообщения относящиеся к одному пользователю обрабатывались в порядке записи, то вам стоит включить в ключ шардирования например идентификатор пользователя, чтобы убедиться, что они попадут в один сегмент.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=536cbcf8fe20" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>