Serverless Framework: Otimizando Lambda "cold starts" com serverless-webpack
Tags: ptbr, aws, serverless, webpack
Published: 2020-04-13 03:31:59

[_Créditos da Imagem_](https://deanmraz.com/blog/serverless-nodejs-webpack)

Ultimamente, a AWS tem feito um excelente trabalho para reduzir os tempos de inicialização frio (_cold starts_) do Lambda em todos os _runtimes_ (ex: Node.js, Go, Python etc), como [melhorias na rede VPC](https://aws.amazon.com/blogs/compute/announcing-improved-vpc-networking-for-aws-lambda-functions/) e [adicionando suporte nativo para manter automaticamente seus _containers_ Lambda aquecidos](https://serverless.com/blog/aws-lambda-provisioned-concurrency/).

Essas mudanças reduziram o número de coisas que um desenvolvedor precisa fazer para melhorar o desempenho de inicialização do Lambda.

Mas ainda há uma variável sob o controle do desenvolvedor que afeta [significativamente a inicialização fria - o tamanho do pacote zippado](https://mikhail.io/serverless/coldstarts/aws/).

## Por que o tamanho do pacote ainda é importante

Apesar das melhorias que a AWS fez, seu Lambda ainda pode ser iniciado a frio. Esse tempo extra de inicialização afetará a capacidade de "resposta-percebida" das funções voltadas para seus usuários, como APIs ou aplicativos Slack.

Se você estiver usando [Serverless Framework](https://serverless.com/) com JavaScript, ele empacota seu código com todas as dependências npm por padrão, o que pode criar funções Lambda muito pesadas. Grande parte desse código geralmente não é usado, mas como está no pacote do aplicativo, ele ainda precisa ser copiado e extraído no sistema de arquivos toda vez que um novo container é provisionado. O resultado é um tempo de resposta mais lento para o seu cliente.

## Onde os desenvolvedores de back-end precisam aprender rapidamente

O tamanho do código compactado tem sido uma métrica de importância para os desenvolvedores da web. Muitas horas são gastas otimizando o JavaScript, imagens, CSS e HTML entregues a um aplicativo da Web - a quantidade de tempo necessária para o download afeta o tempo da primeira pintura e o tempo da primeira interação e pode ter impactos reais na retenção e conversão do cliente.

Os desenvolvedores de back-end conseguiram ignorar o tamanho do código por um longo tempo, devido à maneira como ele foi empacotado e executado. Aplicativos monolíticos geralmente são implantados em um servidor de longa execução, iniciados uma vez e deixados em execução por dias, portanto, os longos tempos de inicialização não eram realmente uma preocupação.

Normalmente, esses aplicativos são agrupados em cluster para dimensionar horizontalmente o desempenho e ajustar elasticamente a capacidade de alterar a carga do cliente. Como os tempos de inicialização já são ruins, os clusters normalmente são superprovisionados para compensar. Os desenvolvedores conseguiram esconder seus maus tempos de inicialização de aplicativos por trás disso.

Com Serverless em geral, as preocupações de arquitetura do front-end chegam ao back-end. Embora tenha liberado os desenvolvedores de pensar em várias preocupações operacionais, como fornecimento de infraestrutura e otimização de recursos, agora eles precisam considerar coisas como inicialização fria e gerenciamento de conexões de recursos que não eram necessárias no passado.

O tamanho do pacote afeta diretamente o tempo de inicialização de um Lambda, portanto, a otimização da quantidade de código implementada com um bundler ajudará a reduzir o tempo de inicialização fria.

Felizmente para desenvolvedores de JavaScript, as ferramentas usadas para agrupar o código são bastante maduras no front-end - [agora](https://parceljs.org/) [existem](https://webpack.js.org/) [muitos](http://browserify.org/) empacotadores para JavaScript e a maioria deles é adequada para empacotar código de back-end em execução no Node.js também!

## Como um empacotador ajuda nisso?

Os empacotadores trabalham examinando as importações e exportações em seu código e nos pacotes npm referenciados, criando uma árvore de dependência e gerando um único arquivo JavaScript que contém todo o código acumulado.

Isso traz vários benefícios:

- **‌Inclua apenas o código usado por sua função**

Você pode ter várias funções Lambda no mesmo projeto Serverless Framework que compartilham algum código, mas é improvável que todas as funções usem exatamente o mesmo conjunto de dependências compartilhadas. É possível otimizar o tamanho do seu Lambda incluindo apenas o código importado pela sua função e não por todas as suas funções.

- **Otimize suas dependências npm**

Você pode estar usando apenas algumas de suas dependências do npm, ou mesmo apenas parte desse código referenciado. Algumas de suas dependências do npm podem existir duas vezes porque são referenciadas por vários módulos.

Um empacotador pode seguir o gráfico de dependência em um módulo npm e pegar apenas os arquivos referenciados transitivamente pelo seu código, além de desduplicar (onde existem versões compatíveis).

- **‌Use um único arquivo para o seu código fonte**

Usar um único arquivo ao invés de vários arquivos pode reduzir a quantidade de acessos ao sistema de arquivos necessários ao Node.js. para carregar seu código. 

Esse aprimoramento geralmente é pequeno, mas é um efeito colateral do webpack, onde o empacotamento de um único arquivo para a web faz uma enorme diferença na rede.

O resultado é geralmente uma função Lambda muito menor do que se você tivesse zippado todo o seu código e feito o deploy você mesmo.

## Começando com serverless-webpack

O primeiro passo é instalar o plugin e registrá-lo no seu arquivo `serverless.yaml`.

Mude para o diretório do projeto e instale o módulo `serverless-webpack` junto com o webpack:

```bash
npm install --save-dev serverless-webpack@5.3.1 webpack@4.42.1
```

Adicione o plugin ao seu arquivo `serverless.yaml`:

```yaml
plugins:
  - serverless-webpack
  ...
  # se você estiver usando o `serverless-offline-plugin`
  # se certifique de colocá-lo APÓS o `serverless-webpack`
  - serverless-offline-plugin
```

Por fim, adicione um arquivo `webpack.config.js` na raiz do seu projeto:

```js
const slsw = require('serverless-webpack');

module.exports = {
  target: 'node',
  entry: slsw.lib.entries,
  mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
  node: false,
  optimization: {
    minimize: false,
  },
  devtool: 'inline-cheap-module-source-map',
};
```

Neste ponto, configuramos o plugin `serverless-webpack` e oferecemos uma configuração mínima do webpack.

Se você leu a documentação do `serverless-webpack`, deve ter notado que incluí algumas opções extras que não aparecem nos exemplos.

- `node: false` - é incomum, mas achei necessário para minhas configurações.

[Definir `node` como `target`](https://webpack.js.org/concepts/targets/) normalmente é suficiente para que o webpack seja compilado para o Node.js - ele usa o Common.js `require()` para dependências e garante que as dependências internas não sejam substituídas por stubs. No entanto, descobri que ele ainda interfere com o `process.env` global, necessário para usar variáveis ​​de ambiente no Lambda.

- `minimize: false` - [desativa](https://webpack.js.org/configuration/optimization/) a minimização de código (uglificação).

O ofuscamento torna mais difícil a leitura dos _stack traces_ do Lambda, ele reduz um pouco o tamanho do pacote, mas não o suficiente para justificar seu uso com projetos Serverless Framework) e não é realmente necessário onde o código não é distribuído diretamente aos usuários (como na Web).

Minha configuração desativa a minimização, porque reduz bastante a quantidade de CPU e memória necessária no tempo de compilação e ajuda a evitar erros _'out-of-memory'_.

## Crie um único pacote ou um pacote por Lambda

Nesse ponto, se você executar `serverless package`, seu terminal irá mostrar relatórios típicos do webpack, mostrando os tempos de compilação e os tamanhos dos pacotes.

![](https://www.gorillastack.com/static/be4f40944c1d422ad21a810c6a9225c0/f0628/webpack-output.png)

Por padrão, Serverless Framework gera um único pacote com todo o seu código e o implementa para todas as suas funções Lambda. O `serverless-webpack` também faz o mesmo, com uma diferença que ele cria um arquivo JavaScript por função Lambda, com todo o código necessário para cada Lambda, incluindo dependências npm, agrupadas e compactadas.

Você pode encontrar seu projeto empacotado em `<project>/.serverless`. Você descobrirá que essa ainda é uma enorme redução no tamanho em comparação com o padrão para aplicativos não triviais.

Se você deseja otimizar ainda mais e criar um pacote específico para cada função do Lambda, é possível dizer ao Serverless Framework para empacotar os Lambdas individualmente, configurando a seguinte opção em `serverless.yaml`:

```yaml
package:
  individually: true
```

O **serverless-webpack** também utiliza esse valor e criará um pacote separado para cada Lambda com o único pacote de webpack em cada um. Isso leva muito mais tempo, mas o resultado é uma função mais otimizada.

## Usando Babel

A documentação do plugin descreve como usar o babel na transpilação (mas incluí algumas das minhas próprias alterações para garantir que você aproveite ao máximo).

Primeiramente, vamos instalar as dependências do Babel:

```bash
npm install --save-dev @babel/core@7.9.0 @babel/preset-env@7.9.5 babel-loader@8.1.0 corejs@3.6.5
```

Inclua a seguinte entrada no seu `webpack.config.js` (certifique-se de definir a versão de execução do Node.js que você está usando no Lambda):

```js
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                [
                  '@babel/preset-env',
                  { targets: { node: '12' }, useBuiltIns: 'usage', corejs: 3 }
                ]
              ]
            }
          }
        ]
      }
    ]
  },
  ...
}
```

Comparadas à documentação do **serverless-webpack**, as principais diferenças com minha configuração são:

- adicionando a `@babel/preset-env` para garantir que não incluamos transformações ou polyfills para recursos que já estão disponíveis no Node.js.
- definido `useBuiltins: 'usage'` para que todos os polyfills usados ​​sejam incluídos caso a caso
- configurado `corejs: 3` para que o **core-js v2 antigo** e com bugs não seja usado para fornecer polyfills

## Copiando outros arquivos para o pacote

Como o Serverless Framework normalmente copia tudo para o pacote de deploy, você pode ficar surpreso quando seu código repentinamente deixa de funcionar por carregar arquivos arbitrários que não são parte do seu código e sim do sistema de arquivos.

Como mencionado anteriormente, o `serverless-webpack` inclui apenas o código-fonte, ignorando efetivamente o que está definido nas opções `include` e `exclude` na seção `package` do `serverless.yaml`. Você deve configurar o webpack para copiar os arquivos que não são de origem, porém necessários, usando o `copy-webpack-plugin`.

Vamos instalar com o npm:

```bash
npm install --save-dev copy-webpack-plugin
```

No seu `webpack.config.js` inclua e configure o plugin com os caminhos de arquivo e globs que você deseja copiar:

```js
const CopyPlugin = require('copy-webpack-plugin`);
...
module.exports = {
  ...
  plugins: [
    new CopyPlugin([
      'path/to/specific/file',
      'recursive/directory/**',
    ]),
  ],
  ...
};
```

## Problemas Comuns

### Sem Memória / Out of Memory

O uso do webpack para empacotar seu código usa muito mais CPU e memória do que o normal, e não é incomum o Node.js relatar um erro de falta de memória. O `serverless-webpack` executa uma instância do webpack por função e cada um deve combinar e minimizar sua saída:

```bash
Serverless: Bundling with Webpack...


<--- Last few GCs --->

[8233:0x393fa70]   207373 ms: Scavenge 1885.9 (2042.4) -> 1885.9 (2042.9) MB, 9.3 / 0.0 ms  (average mu = 0.262, current mu = 0.159) allocation failure
[8233:0x393fa70]   207386 ms: Scavenge 1886.4 (2042.9) -> 1886.4 (2043.6) MB, 11.4 / 0.0 ms  (average mu = 0.262, current mu = 0.159) allocation failure
[8233:0x393fa70]   207404 ms: Scavenge 1887.0 (2043.6) -> 1886.9 (2044.1) MB, 15.8 / 0.0 ms  (average mu = 0.262, current mu = 0.159) allocation failure


<--- JS stacktrace --->

==== JS stack trace =========================================

    0: ExitFrame [pc: 0x1374fd9]
    1: StubFrame [pc: 0x13afd14]
Security context: 0x39352fbc08a1 <JSObject>
    2: replace [0x39352fbccf51](this=0x2c60c4e7f591 <String[#27]\: \n//# sourceMappingURL=[url]>,0x16535c280e41 <JSRegExp <String[#7]: \[url\]>>,0x16535c280e79 <JSFunction (sfi = 0x35c9e77a48b9)>)
    3: /* anonymous */(aka /* anonymous */) [0x17c38da35a9] [/home/chris/dev/slack-app/bot/node_modules/webpack/lib/SourceMapDevToolPlu...

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
 1: 0x9da7c0 node::Abort() [node]
 2: 0x9db976 node::OnFatalError(char const*, char const*) [node]
 3: 0xb39f1e v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
 4: 0xb3a299 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
 5: 0xce5635  [node]
 6: 0xce5cc6 v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [node]
 7: 0xcf1b5a v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [node]
 8: 0xcf2a65 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
 9: 0xcf5478 v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [node]
10: 0xcc30c6 v8::internal::Factory::NewRawOneByteString(int, v8::internal::AllocationType) [node]
11: 0x103253a v8::internal::Runtime_StringBuilderConcat(int, unsigned long*, v8::internal::Isolate*) [node]
12: 0x1374fd9  [node]
```

A solução é aumentar a quantidade de espaço de heap disponível para o Node.js ao iniciar o comando de deploy do Serverless Framework (isso funciona apenas no Node.js v8 e posterior):

```bash
node --max-old-space-size=4096 \
	node_modules/.bin/serverless package --stage dev ...
```

Observe também que precisamos invocar o Serverless Framework pelo caminho completo na pasta `node_modules/bin`, porque estamos iniciando o Node.js. diretamente.

Se você não quiser chamar o Node.js diretamente, poderá definir as opções do Node.js usando uma variável de ambiente:

```bash
export NODE_OPTIONS=--max-old-space-size=4096
npx serverless package --stage dev ...
```

### Número de linha correto e nomes das funções nos stack traces

Como o webpack transforma seu código e o empacota no mesmo arquivo, os stack traces nos logs do CloudWatch refletirão o que é empacotado por padrão. Isso pode prejudicar sua capacidade de depurar seu código.

O webpack usa a opção `devtool` para controlar mapas de origem (_source maps_), que são comentários extras incluídos no código usado para ajudar depuradores e geradores de stack trace a converter essas referências ao código-fonte do pacote de volta ao código-fonte original ou transformado.

Em nosso exemplo `webpack.config.js`, configuramos a `devtool` como `'inline-cheap-module-source-map'`, que deve renderizar stack traces com referências de linha ao código-fonte original.

Essa opção pode afetar a velocidade de criação do webpack, portanto, se você quiser usar outra coisa, consulte a [documentação do webpack sobre devtool](https://webpack.js.org/configuration/devtool/).

# Créditos

- [Optimizing your Lambda cold starts with serverless-webpack](https://www.gorillastack.com/news/optimizing-your-lambda-cold-starts-with-serverless-webpack/), escrito originalmente por [Chris Armstrong](https://twitter.com/ckarmstrong).