Как использовать карты источников в лямбда-функциях TypeScript (с контрольными показателями)

TypeScript — популярный язык для разработчиков всех видов, и он оставил свой след в бессерверном пространстве. Большинство крупных Lambda-фреймворков теперь имеют надежную поддержку TypeScript. Дни борьбы с конфигурациями webpack в основном позади.

Оглавление

  • Трассировка стека
  • Выдача карт исходных текстов
  • Поддержка карт исходных текстов
  • Пример CDK
    • Бессерверный стек
  • Пример SAM
    • Архитектор
  • Пример бессерверного фреймворка
  • Бенчмарки
    • Без минимизации без исходных карт
    • Минифицирован без карт исходных текстов
    • Минифицировано с картами исходных текстов
    • Ошибка минимизирована без карт исходных кодов
    • Ошибка минимизирована с помощью карт источников
  • Заключение

Трассировка стека

Если мы уже транспилируем наш код, чтобы преобразовать исходный текст TypeScript в JavaScript, дружественный к Lambda, мы также можем минифицировать и древовидно перетряхивать код. Маленькие пакеты могут ускорить развертывание и даже помочь с холодным стартом и временем выполнения. Однако это может затруднить отладку, когда мы начинаем видеть трассировку стека, которая выглядит следующим образом:

{
    "errorType": "SyntaxError",
    "errorMessage": "Unexpected end of JSON input",
    "stack": [
        "SyntaxError: Unexpected end of JSON input",
        "    at JSON.parse (<anonymous>)",
        "    at VA (/var/task/index.js:25:69708)",
        "    at Runtime.R8 [as handler] (/var/task/index.js:25:69808)",
        "    at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"
    ]
}
Вход в полноэкранный режим Выход из полноэкранного режима

Сообщение об ошибке здесь дает нам знать, что мы не можем разобрать строку JSON, но сама трассировка стека бесполезна для поиска строки, в которой произошел сбой кода. Нам ничего не остается, как просмотреть наш код и надеяться найти ошибку. Возможно, мы сможем выполнить текстовый поиск по JSON.parse, но если это происходит в одной из наших зависимостей, поиск не сработает. Что дальше? Добавлять в код кучу утверждений журнала? Нет! Если мы используем Source Maps, мы можем получить более полезные трассировки стека:

{
    "errorType": "SyntaxError",
    "errorMessage": "Unexpected end of JSON input",
    "stack": [
        "SyntaxError: Unexpected end of JSON input",
        "    at JSON.parse (<anonymous>)",
        "    at VA (/fns/db.ts:39:8)",
        "    at Runtime.R8 (/fns/list.ts:6:24)",
        "    at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"
    ]
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы видим, что стек включает вызов из строки 6 файла list.ts, который в итоге не работает на строке 39 файла db.ts.

Чтобы включить Source Maps в нашем приложении, нам нужно указать нашему инструменту сборки эмитировать Source Maps и включить поддержку Source Map в нашей среде выполнения.

Создание карт источников

Эмиссия карт исходников очень проста в esbuild. Мы просто устанавливаем свойство boolean в нашей конфигурации. Теперь, когда мы запустим сборку, мы получим файл index.js.map, а также наш index.js. Этот файл должен быть загружен в службу Lambda. Мы рассмотрим, как это сделать с помощью AWS CDK, AWS SAM и Serverless Framework немного позже в этой статье.

Поддержка исходных карт

Наличие файла index.js.map в нашей среде выполнения Lambda недостаточно для включения Source Maps. Мы также должны убедиться, что среда выполнения знает, как их использовать. К счастью, это очень просто, начиная с версии Node.js 12.12.0. Нам просто нужно установить опцию командной строки --enable-source-maps. Опции командной строки могут быть установлены в AWS Lambda путем установки переменной окружения NODE_OPTIONS. На момент написания этой статьи AWS Lambda поддерживает Node.js версий 12 и 14. AWS не публикует младшие версии, используемые в Lambda, но мы можем узнать это, указав process.version в функции. По состоянию на конец января 2022 года версии Node.js, используемые в Lambda в us-east-1, были v12.22.7 и v14.18.1, поэтому у нас не будет проблем с использованием Source Maps.

Если бы нам нужно было включить Source Maps в среде исполнения, которая не поддерживает родную версию, мы всегда могли бы использовать Source Map Support.

Пример CDK

Весь код примера доступен на GitHub.

AWS CDK — мой любимый инструмент для написания и развертывания бессерверных приложений, отчасти из-за конструкции aws-lambda-nodejs. Эта конструкция позволяет очень легко работать с TypeScript. Она оборачивает esbuild и раскрывает опции. Она также поддерживает установку переменных окружения.

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

const lambdaProps = {
  architecture: Architecture.ARM_64,
  bundling: { minify: true, sourceMap: true },
  environment: {
    NODE_OPTIONS: '--enable-source-maps',
  },
  logRetention: RetentionDays.ONE_DAY,
  runtime: Runtime.NODEJS_14_X,
  memorySize: 512,
  timeout: Duration.minutes(1),
};

new NodejsFunction(this, 'FuncOne', {
  ...lambdaProps,
  entry: `${__dirname}/../fns/one.ts`,
});

new NodejsFunction(this, 'FuncTwo', {
  ...lambdaProps,
  entry: `${__dirname}/../fns/two.ts`,
});
Вход в полноэкранный режим Выход из полноэкранного режима

Как мы видим, включить Source Maps очень просто, если уже использовать AWS CDK и NodejsFunction.

Бессерверный стек

Serverless Stack — это очень крутое дополнение, которое строится поверх AWS CDK, предоставляя потрясающий опыт разработчика и приборную панель. Собственная версия NodejsFunction от SST, Function автоматически создает карты исходников. Их использование можно включить, просто установив NODE_OPTIONS, как описано выше.

Пример SAM

Поддержка SAM для TypeScript некоторое время отставала, но только что был объединен запрос на исправление, который должен изменить ситуацию. Похоже, что мы сможем добавить ключ aws_sam в файл package.json, чтобы включить сборку на движке SAM как часть sam build. Хотя этот PR был объединен с aws-lambda-builders, движком, лежащим в основе sam build, его все еще необходимо добавить в aws-sam-cli и выпустить (как ожидается, с большим шумом), прежде чем его можно будет использовать в SAM.

Тем временем — или если мы рассматриваем другие варианты развертывания наших функций — мы можем добавить дополнительный шаг сборки. Мы создадим файл esbuild.ts, который транспонирует функции, а затем направим наш файл SAM template.yaml на выход этого шага.

import { build, BuildOptions } from 'esbuild';

const buildOptions: BuildOptions = {
  bundle: true,
  entryPoints: {
    ['create/index']: `${__dirname}/../fns/create.ts`,
    ['delete/index']: `${__dirname}/../fns/delete.ts`,
    ['list/index']: `${__dirname}/../fns/list.ts`,
  },
  minify: true,
  outbase: 'fns',
  outdir: 'sam/build',
  platform: 'node',
  sourcemap: true,
};

build(buildOptions);
Вход в полноэкранный режим Выход из полноэкранного режима

Шаблоны SAM будут принимать все в CodeUri, поэтому приведенный выше код транспонирует файл TypeScript по адресу fns/list.ts и выведет sam/build/list/index.js и sam/build/list/index.js.map. Установив CodeUri: sam/build/list, SAM упакует эти два файла и загрузит их в Lambda.

Теперь нам осталось установить переменную окружения. Это достаточно легко сделать в шаблоне SAM. Мы можем задать ее как глобальную, поэтому она должна быть в шаблоне только один раз.

Globals:
  Function:
    Environment:
      Variables:
        NODE_OPTIONS: '--enable-source-maps'
Вход в полноэкранный режим Выйти из полноэкранного режима

Для того чтобы убедиться, что мы всегда собираем перед развертыванием, мы можем добавить несколько npm-скриптов.

"scripts": {
  "build:lambda": "npm run clean && ts-node --files sam/esbuild.ts",
  "clean": "rimraf cdk.out sam/build",
  "deploy:sam": "npm run build:lambda && sam deploy --template template.yaml",
  "destroy:sam": "sam delete"
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Это работает достаточно хорошо, но требует некоторых дополнительных усилий. Пользователи SAM, несомненно, с нетерпением ждут улучшения поддержки TypeScript.

Architect

В качестве альтернативы можно использовать Architect. Architect — это сторонний инструмент для разработчиков, созданный на основе AWS SAM. Architect включает в себя плагин TypeScript.

Пример Serverless Framework

Serverless Framework известен своей системой плагинов. serverless-esbuild привносит комплектацию и все опции, необходимые для поддержки Source Maps в TypeScript, в команды sls package и sls deploy.

Плагин настраивается в нашем файле serverless.yml.

custom:
  esbuild:
    bundle: true
    minify: true
    sourcemap: true
Вход в полноэкранный режим Выход из полноэкранного режима

А затем мы просто направляем наши функции на наши обработчики TypeScript.

functions:
  create:
    handler: fns/create.handler
Вход в полноэкранный режим Выход из полноэкранного режима

Как и SAM, Serverless позволяет нам задавать глобальные переменные окружения для наших функций.

provider:
  name: aws
  lambdaHashingVersion: 20201221
  runtime: nodejs14.x
  environment:
    NODE_OPTIONS: '--enable-source-maps'
Войти в полноэкранный режим Выход из полноэкранного режима

Как и AWS CDK, это хороший опыт для разработчиков TypeScript. Тем, кто уже использует Serverless Framework, будет легко добавить Source Maps в свои приложения.

Бенчмарки

Существует много рекомендаций против использования Source Maps в производстве из-за предполагаемого негативного влияния на производительность. Давайте измерим, по общему признанию, простую функцию list и посмотрим, оказывает ли минификация или использование карт исходных кодов какое-либо заметное влияние.

Я использовал autocannon для тестирования функции при 100 одновременных выполнениях в течение 30 секунд. Я также использовал Lambda Power Tuning для поиска идеальной конфигурации памяти, которой оказалось 512 МБ. Все результаты доступны.

Неминифицированная функция без исходных карт

Размер неминифицированной функции составляет 1,2 МБ. Это в основном из @aws-sdk/client-dynamodb и @aws-sdk/lib-dynamodb. Может ли использование SDK v2 улучшить производительность — это хорошая тема для другого сообщения. Эта функция занимает более одного мегабайта, несмотря на небольшое количество пользовательского кода.

При выполнении теста средняя продолжительность выполнения функции составляет 46,99 мс, а максимальная — 914 мс. 99% моих запросов выполняются в течение 90 мс или менее.

Минификация без карт исходного кода

Минимизация функции уменьшает размер до 534,8 кб, что меньше половины размера. Мой тест показал среднее время отклика 47,83 мс, максимальное 1010 мс и 90 мс в 99-м процентиле. Это немного хуже, но не является статистически значимым. Я ожидаю, что если бы я проводил эти тесты снова и снова, то увидел бы, что минификация кода не оказывает реального влияния на производительность. Это не удивительно. 1,2 МБ — это все еще довольно мало, и я не ожидал увидеть много дополнительных задержек при таком размере.

Минификация с исходными картами

Конечно, размер минифицированной функции по-прежнему составляет 534,8 кб, но исходная карта занимает 1,5 МБ, так что это будет самая большая загрузка, не то чтобы ~2 МБ было много или значительно замедлит наши развертывания.

Среднее время отклика для этого теста составляет 46,52 мс, максимальное — 968 Мб, а 99-й процентиль — 82 мс. Это лучший результат на данный момент, но он не является статистически значимым. Я бы сказал, что это действительно трехсторонняя ничья, которая говорит нам, что добавление Source Maps к этой функции не увеличило задержку.

Это благодаря встроенной поддержке в Node.js! Поскольку стековые трассы не создавались, на Source Maps никогда не ссылались. Возможно, если бы нам пришлось полагаться на библиотеку для этой возможности, мы бы не получили такого результата.

Минификация ошибок без карт исходных текстов

Будет ли триггер условий ошибки иметь значение? Давайте посмотрим. Я провел тот же тест, но на этот раз в функции есть ошибка JSON.parse. Это происходит перед обращением к DynamoDB, поэтому можно ожидать, что она будет немного быстрее, чем успешная функция. Мы получаем 37,86 мс в среднем, 1004 мс в максимуме и 58 мс при 99%. Впечатляет, что обращение к DynamoDB добавляет только около 10 мс задержки!

Минимизация ошибок с помощью Source Maps

Включение Source Maps для ошибок действительно влияет на производительность. Наше среднее значение снизилось до 97,54 мс, максимальное — 1129 мс, а 99% — 243. Это значительное увеличение. Карты источников действительно влияют на задержку при возникновении ошибки. Это логично и подтверждает идею о том, что на карты источников ссылаются только при возникновении ошибки — но теперь карту источников нужно разобрать, а это занимает время.

Заключение

Используйте карты источников в производстве! На мой взгляд, если вы получаете так много ошибок, что падение производительности от Source Maps влияет на вашу итоговую прибыль, вам, вероятно, следует пойти и исправить эти ошибки. Очень легко реализовать Source Maps в различных популярных Lambda-фреймворках, и они не влияют на успешное выполнение. Когда функции все же терпят неудачу, полезная трассировка стека будет стоить дополнительных задержек. Часы разработчика всегда будут стоить дороже, чем несколько миллисекунд выполнения Lambda.

ОБЛОЖКА

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *