Java Lambda で巨大 JSON を扱う

InputStream を Lambda の入力にする記事を書きました。

Java Lambda の入出力型を試してみた〜Stream 編〜
https://qiita.com/kazfuku/items/6f0f55ffa3a88d76cfaa

InputStream を使用した場合のメリットとして、巨大な JSON を扱えると書いてます。そこで、実際のところどうなのかを検証してみました。

## テストデータ
まず、巨大 JSON を用意しました。name と text を持った object が 6000 個含まれるデータで、Lambda の入力サイズ上限 6MB に近い、約 5.9MB です。

```Json
{
  "data":[
    {
      "name":"vrZPwIw3T7","text":"Ku7aQqW3WzUeiRdXnNB26iVElWdOUj8mQhvHksvN1sMmQ2fT3M8navvbTJuspda2q0bY3FWvsDoguE33tTNtoxuiHjdkUIHmylIezYGitmhJ2bbgcHhcHPzGr4eg3Ger9EijFU82Sq4WS9G5UVW62Cw1rDMNdIld2yxn1Zd3DXqE26iOf1IaBQTzEG7Pld03hkXIkAdTdeAjXlAJlGrwnQgjMh1FohW1bUAYeaLi52qLnbgQd7lZAJuOlitfGUyUbP0BjbsPflOLGQwInPjr2Mt3mG4HDokWj2JJgRkXkRYxq34AxQGNWXjlfWKViDxk2InIP6oMsir5YmTL1oO58dzmBGCoYV7e0PTGQHXJbgPJUFUoCmv3mATCEg1xhOa4IUcP7vC7dMvydS3Qt1QHteYajeCvXiW0HjuHkm2oJ61yEg6JocqLVMQ75RaU0Wjb2KvbAwQmggSel5E6mMl0BacZwBXw7OaYHkHO1p1hQup2hhNkaAkN7B8NS8QJ3oSRPQsM6QsETC3x1ErrN0jZZVqupjDvPEr9xj0fDOpqCo7XqTuSPbf3UhHQgjPyikbc2JaqeMdJf1R0RojlqWmf2STGH8HTuGJTQG3vEP04BkrNLaKNVoXE49tPyePO6EqRAKWNxVZoQmw34Xv6yGzMfOLPcSRhML0rYk1FEaBDmgGNpQIPdYjbT3MC08eEY9cHa813iWvm42XmG5LaiIt2z4IcGaWnLwCRytYJJsdqphSEhyvyOpIKM4i02t9rx3Pkt0704EhFo3SD8gVVIE2y1coFUJqy2GxVqptZrKpFv56c4SsWSPqdLqTH9Gh09Y5Cph6eOKg0JXvip1GoONZ80oBUeRudMvsl32m23fYZlG1dNFnGUZSkz2TiGP9baIfLyPWCcPZGEvVaP9FR64FW0yOLvpKyTNYXg21ZsgEkYo3tbcn3AS5R3Ai4eg5hYMaBoVAsMBK1BPZAncDoqOs97nLa2DZFyqgNSz8Asgmh"
    },
    {
      "name":"OXJIXTP9dz","text":"HkWP0PumYHQZxiGNhGWASXOPrygri7cKXs2hrWx0WaumM8OEVc9UKs2EIknzCsBmAMFRER5YNIUs5oz30LjDrjn1PsoKuh60KPOEaHnBNTt8PivYx0hIfmLoLk56ad6LSLNpUVMCP26WiPyont6OjfD1c4sxtKn3qlg6SaNSs2B1tGoReVb3pwOVvPH2BaULL5rzYyDFfAqFqo4D2UevrdoUOeXK3Ks3tav92wHnECM9pbXdsCbWyr1BNulJ5elcZm8HALPgBeX9dg0vaqpgITfz3klyYIWynzPOJC92t0vMao2tL7lr6uxuQvldWgdhlzGjYP123pdWb2h0zItg8NyyK58tCKy2t2YqtdP23fvmVpmOFygiM6kF9LvDRfnu3mz0X2SvcsQh8UqB84dHiOXwicmnI6DX47OuPXOUZc0wICql8zit6WvbEmDchKy9M74u9mPaiIxGXBy8FvLEptqqGytywwC3GGYXEpLYZlbxDycrSTtCq6PUuWoUbfsJmZT4iZSvM0aoyVKBE2l23oXhFZpM4fxyyziIVAHP9YsQbHQlvr8adtD3voumsGKcklt4mnNQclQdSLKPKSIGdUlkvhcCO4MZcEpKcmSrFU6naOYGL1geB1CuTYHYuw0x6tc7JudQAEB6IWE8xwTgPWQUM15xTsqsLrBIwZ70MGpCGW8JCw6sqJExsXi6wpJ1I3L43TUG4hJOnEPIHeXTco06zaDiSrqG3LsLuCiHIkqYui1N0fJBRJhVcn2X8dXMnQKxqhISGrnP7TeBBcAhI8qrmNK0k9EV6mECQtN2g8qaRYVqwOqC4kwzMpvPWkUnNQuUZbknLlWOKuVeh0mrjTzIxQkMShqhdt21o75h9rz0DPxvNHkS6jLw7TBprYieZwcO8iIRy1zYFedSXyVktczdEczIebkfDFmjGtDeZw5RuuFUYKDk4U3J5lpmfmf9K3G6LuPeV5soPxL54l8ZxlJGNpP1kZftZeadtms7"
    },
...
```

## Map 方式で試す
まずは、これを Map 方式の実装で、data 数をカウントします。

Map 方式って？ -> [Java Lambda の入出力型を試してみた〜Map 編〜](https://qiita.com/kazfuku/items/6b2e9131eb5e6e175922)

```Java
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Collections;
import java.util.List;
import java.util.Map;

public class MapHugeJsonFunction implements RequestHandler<Map<String, Object>, Map<String, Object>> {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    @Override
    public Map<String, Object> handleRequest(Map<String, Object> event, Context context) {
        List<Map<String, Object>> data = (List<Map<String, Object>>) event.get("data");
        int count = data.size();

        return Collections.singletonMap("count", count);
    }
}
```

実行結果 (≠ Cold Start)

```
REPORT RequestId: ac45a98c-be93-49b5-8813-c30ae7d731c9	Duration: 2030.51 ms	Billed Duration: 2100 ms	Memory Size: 128 MB	Max Memory Used: 118 MB
```

最大メモリに近い 118MB を使っています。これは handlerRequest に渡す Map を構築した時点で、JSON をすべてメモリに載せている状態です。
これにビジネスロジックを増やすと、128MB ではメモリが足りないでしょう。

## Stream 方式で試す
同じ処理を Stream 方式で試してみました。この実装だと、JSON をすべてメモリに載せることなく処理できます。

JSON の Parse には [Jackson Streaming API](https://github.com/FasterXML/jackson-databind#5-minute-tutorial-streaming-parser-generator) を使用しています。XML でいうと SAX Parser を使うのと同じ考え方ですね。

Stream 方式って？ -> [Java Lambda の入出力型を試してみた〜Stream 編〜](
https://qiita.com/kazfuku/items/6f0f55ffa3a88d76cfaa)

```Java
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Map;

public class StreamHugeJsonFunction implements RequestStreamHandler {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
        JsonParser parser = OBJECT_MAPPER.createParser(inputStream);

        int count = 0;
        boolean inData = false;
        boolean inDataArray = false;
        JsonToken token;
        while((token = parser.nextToken()) != null) {
            if ("FIELD_NAME".equals(token.name()) && "data".equals(parser.getCurrentName())) {
                inData = true;
            }
            if (inData && "START_ARRAY".equals(token.name())) {
                inDataArray = true;
            }
            if (inDataArray && "END_ARRAY".equals(token.name())) {
                inDataArray = false;
            }
            if (inData && "END_OBJECT".equals(token.name())) {
                inData = false;
            }
            if (inDataArray && "START_OBJECT".equals(token.name())) {
                count++;
            }
        }

        Map<String, Integer> response = Collections.singletonMap("count", count);
        OBJECT_MAPPER.writeValue(outputStream, response);
    }
}
```

実行結果 (≠ Cold Start)

```
REPORT RequestId: 41f93e2e-e1db-4775-b296-8b280d2696f9	Duration: 871.01 ms	Billed Duration: 900 ms	Memory Size: 128 MB	Max Memory Used: 80 MB
```

メモリは 118 MB -> 80 MB に改善しました。処理時間も 2030 ms -> 871 ms に改善してます。

# まとめ
JSON 解析処理がめんどいものの、JSON データのすべてを必要としない処理の場合は、Stream 方式を使えば、処理時間もメモリも節約できそうです。