DynamoDBから特定のテーブルの全項目データを取得する(TypeScript/Node.js)

AWS SDKのDynamoDB.DocumentClientscanメソッドを用いるのですが、そのままではテーブルのサイズが大きい場合に一部のデータしか取得できません。

バッチ処理などで、テーブル内の全項目を取得したいシーンもあると思います。(頻繁にそういった操作が必要になるのなら、アプリケーションの設計ミスのような気がしますが...)

そういった際はLastEvaluatedKeyを用いれば簡単に実現可能です。以下のコードはTypeScriptですが、JavaScriptの場合もほぼ同じです(型定義を消せばいいだけ)。

import awsSdk from 'aws-sdk';
import { DocumentClient } from 'aws-sdk/lib/dynamodb/document_client';

const documentClient = new awsSdk.DynamoDB.DocumentClient({
  region: 'ap-northeast-1',
});

const scanAll = async (params: DocumentClient.ScanInput) => {
  const items: DocumentClient.AttributeMap[] = [];

  let lastEvaluatedKey: undefined | DocumentClient.Key;

  while (true) {
    const res = await documentClient.scan({
      ...params,
      ExclusiveStartKey: lastEvaluatedKey,
    }).promise();

    if (res.Items) {
      res.Items.forEach(item => items.push(item));
    }

    if (!res.LastEvaluatedKey) {
      break;
    }

    lastEvaluatedKey = res.LastEvaluatedKey;
  }

  return items;
};

以下のように用いることができます。

scanAll({ TableName: 'テーブル名' })
  .then((items) => {
    // ...
  });

express.js + passport.jsで1分でbasic認証を実装する

f:id:k-kty:20190307181543p:plain

passport.jsを用いると、expressでbasic認証が簡単にできます。

準備

$ npm i express passport passport-http

コード

以下のファイルをindex.jsとして保存します。

const express = require('express');
const passport = require('passport');
const passportHttp = require('passport-http');

passport.use(new passportHttp.BasicStrategy(
  function(username, password, done) {
    if (username === 'correct-username' && password == 'correct-password') {
      return done(null, true);
    } else {
      return done(null, false);
    }
  }
));

const app = express();

app.get('/', passport.authenticate('basic', { session: false }), (req, res) => {
  res.sendStatus(200);
});

app.listen(3000);

確認

サーバーを立ち上げます。

$ node index.js

curlで動作を確かめてみます。

$ curl -u correct-username:correct-password http://localhost:3000
OK
$ curl http://localhost:3000
Unauthorized

正しく動いていることがわかります。

ウェブ開発でNode.jsを採用するメリット・デメリット

f:id:k-kty:20190111210413p:plain

ウェブ開発においてある程度の地位を築いた感のあるNode.jsですが、自分も2年ほど使っています。 この記事では、Node.jsの良い点・悪い点をまとめてみます。

メリット

フロントエンドと同じ言語(JavaScript)でバックエンドを書くことができる

これは素晴らしいことだと思います。「一人がフロントエンドとバックエンドの両方に関わる」ということが容易になり、バックエンド開発者とフロントエンド開発者の間で共有できる知見の幅も広がります。

プロトタイピングに向いている

JavaScriptがダイナミックな言語だということや、フレームワークが充実していることにより、高速なプロトタイプ開発が可能です。

AltJSを用いてJavaScriptに足りない点を補完できる

ある程度大規模なものを作るとなると、JavaScriptにはない機能(型など)が欲しくなってきます。 そういう場合にはAltJSを用いることで解決することができます。 特にTypeScriptは開発も進んでおり、エコシステムとしても成熟が進んでいるのでよく用いられています。

シンプルな設計思想

豊富なパッケージ

NPMにはかなり多くのパッケージが公開されています。ここをみると、他の言語・レジストリよりも多くのパッケージが公開されていることがわかります(量だけではなく質も大切ですがそれを定量化するのは難しいので...)。

効率的なIO処理

シングルスレッド上でのイベントループにより、少ないオーバーヘッドで効率的にIOを並列に処理できます。大量のスレッドでメモリが逼迫されるということもなく、よくスケールします。

デメリット

計算量の多いタスクに向いていない

計算に時間のかかるタスクを実行するとき、(workerを使って別スレッドで実行するなどしない限り)イベントループをブロックします。イベントループをブロックしている間は他の処理が基本的にストップしてしまいます。

イベンドリブンなコードには慣れを要する

Node.jsでは、コールバックを利用するなどしてイベントドリブンなコードを書くことになります。1スレッド/1リクエストのモデルではシンプルに書けることも、少し複雑になってしまいます。これには慣れを要します。

コールバック地獄

Node.jsのコードでは、複数のコールバックがネストされてコードが意味不明になってしまう「コールバック地獄」を生み出してしまいやすいです。現在はPromiseや async-awaitなどの普及によって改善されつつあります。

まとめ

パフォーマンスのボトルネックが、計算よりもIOになりそうな場合(ウェブアプリケーションの多くはそうだと思います)においてはNode.jsは採用する価値があると思います。実際、Paypal, Netflix, LinkedInなども利用しているようです。

Nvidia Dockerを用いてDockerコンテナからGPUを利用する

ディープラーニング系の処理でもDockerを使いたい!ということで、Nvidia Dockerを利用してDockerコンテナでGPUを使ってみました。 マシンはAWSのp2.xlargeインスタンス(NVIDIA K80を積んでいます)を、OSはUbuntu 18.04を利用しました。

Dockerのインストール・設定

まずはDockerをインストールします。

インストール

バージョンは1.12以降である必要があります。

Dockerの公式ドキュメント を参考にしています。

$ sudo apt-get update
$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io

確認

実際に入ったか確かめます。Hello from Docker! みたいなのが表示されれば成功です。

$ sudo docker run hello-world

権限周りの設定

rootでdockerコマンドを毎回実行するのもあれなので、設定します。 dockerグループに現在のユーザーを追加します。

$ sudo usermod -aG docker $USER

再ログインすると、sudoなしでdockerコマンドを実行できるようになります。

Nvidia CUDA Toolkitのインストール

Nvidia Dockerをインストールするために必要なCUDA Toolkitをインストールします。 Nvidiaの公式ドキュメントが参考になります。

準備

gccやmakeなどもろもろが必要になるので、インストールします。

$ sudo apt-get install build-essential

インストール

インストーラをダウンロードします。

$ wget https://developer.nvidia.com/compute/cuda/10.1/Prod/local_installers/cuda_10.1.105_418.39_linux.run

こちらを参考にハッシュ値を確認します。

$ md5sum cuda_10.1.105_418.39_linux.run

インストーラを実行します。

$ sudo sh cuda_10.1.105_418.39_linux.run

途中で、規約への同意が求められます。また、その後何をインストールしたいのか聞かれますが全部選択しても大丈夫だと思います。

確認

インストールが終了したら、以下のコマンドを実行してみます。現在のGPUの状態などが表示されたら成功です。

$ nvidia-smi

Nvidia Dockerのインストール

以下のようなコマンドでNvidia Dockerをインストールします。これで終わりです。

$ curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | \
  sudo apt-key add -
$ distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
$ curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \
  sudo tee /etc/apt/sources.list.d/nvidia-docker.list
$ sudo apt-get update
$ sudo apt-get install -y nvidia-docker2
$ sudo pkill -SIGHUP dockerd

実際に使ってみる

TensorFlowの公式ドキュメントにあった、GPUを用いた計算の例をを実行してみます。2次元のテンソルの要素の和を求めています。

$ docker run --runtime=nvidia -it --rm tensorflow/tensorflow:latest-gpu \
   python -c "import tensorflow as tf; tf.enable_eager_execution(); print(tf.reduce_sum(tf.random_normal([1000, 1000])))"
2019-02-28 00:15:38.920366: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
2019-02-28 00:15:41.032922: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:998] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2019-02-28 00:15:41.033785: I tensorflow/compiler/xla/service/service.cc:150] XLA service 0x3f815f0 executing computations on platform CUDA. Devices:
2019-02-28 00:15:41.033815: I tensorflow/compiler/xla/service/service.cc:158]   StreamExecutor device (0): Tesla K80, Compute Capability 3.7
2019-02-28 00:15:41.055212: I tensorflow/core/platform/profile_utils/cpu_utils.cc:94] CPU Frequency: 2300025000 Hz
2019-02-28 00:15:41.055484: I tensorflow/compiler/xla/service/service.cc:150] XLA service 0x3fe90b0 executing computations on platform Host. Devices:
2019-02-28 00:15:41.055521: I tensorflow/compiler/xla/service/service.cc:158]   StreamExecutor device (0): <undefined>, <undefined>
2019-02-28 00:15:41.056190: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1433] Found device 0 with properties:
name: Tesla K80 major: 3 minor: 7 memoryClockRate(GHz): 0.8235
pciBusID: 0000:00:1e.0
totalMemory: 11.17GiB freeMemory: 11.11GiB
2019-02-28 00:15:41.056223: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1512] Adding visible gpu devices: 0
2019-02-28 00:15:41.057198: I tensorflow/core/common_runtime/gpu/gpu_device.cc:984] Device interconnect StreamExecutor with strength 1 edge matrix:
2019-02-28 00:15:41.057230: I tensorflow/core/common_runtime/gpu/gpu_device.cc:990]      0
2019-02-28 00:15:41.057255: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1003] 0:   N
2019-02-28 00:15:41.057853: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1115] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 10805 MB memory) -> physical GPU (device: 0, name: Tesla K80, pci bus id: 0000:00:1e.0, compute capability: 3.7)
tf.Tensor(54.97377, shape=(), dtype=float32)

うまくいったみたいです。

Numbaを用いたPython/Numpyの高速化

f:id:k-kty:20190216135709p:plain

まえがき

機械学習関連のプロジェクトではPythonを使う人が多いと思うのですが、Pythonでの処理って遅いですよね。

Tensorflowでの学習などにおいてはPython側で計算をしているわけではないので大丈夫なのですが、前処理の際などにPythonの実行速度の遅さがとても気になります。コンパイラ言語とスクリプト言語を比べるのは酷ですが、「C++なら数秒で終わるのになあ」という処理も数分かかってしまったりします。

もちろんC++のプログラムに匹敵するほどの速度を出すのは無理ですが、できるだけ高速化したいですよね。 そのための方法はいろいろあるのですが、今回はNumbaというJITコンパイラを用いる方法を紹介したいと思います。

Numbaとは

公式サイトによると、

Numba is an open source JIT compiler that translates a subset of Python and NumPy code into fast machine code.

「Python/Numpyコード(の一部)を、高速な機械語に変換するためのオープンソースJITコンパイラ」ってことみたいですね。

もう少し細かい特徴としては以下のような感じです。

  • 関数にデコレータをつけるだけで動作する
  • Numpyの配列や関数を用いた処理に特化している
  • ランタイム時に、最適化された機械語を作成して実行してくれる
  • Numbaを利用するためにPythonのインタープリターを変えたり、コードの実行前にコンパイルするようにしたりする必要がない
  • CPU/GPUを用いた並列計算のための機能もある

実際に使ってみる

高速化前のコード

以下のような関数があったとします。 配列(arrとします)を受け取り、[log(arr[1]/arr[0]), log(arr[2]/arr[1])... ] のような(長さがarrより1短い)配列を返します。

def calculate_diffs(arr):
  ret = numpy.empty(arr.size - 1)
  for i in range(arr.size - 1):
    ret[i] = numpy.log(arr[i + 1] / arr[i])
  return ret

以下のコードを用いて測定をすると(かなり雑ですが、今回はだいたいのパフォーマンスの比較ということで...)、出力は 183.20000410079956 となりました。約3分です。 なお関数内のnumpy.empty(...)の部分は計算とは関係ありませんがe-5秒くらいのオーダーだったので無視してよさそうです。(ちなみにですがnumpy.random.rand(...)の部分は無視できないほど時間がかかります。)

import time
import numpy

# 以上の関数定義

if __name__ == '__main__':
  arr = numpy.random.rand(10 ** 8)

  start_t = time.time()
  calculate_diffs(arr)
  end_t = time.time()

  print(end_t - start_t)

JITを有効化してみる

以下のようにデコレータをつけて、JITを有効化してみます。

@numba.jit(nopython=True)
def calculate_diffs(arr):
  ret = numpy.empty(arr.size - 1)
  for i in range(arr.size - 1):
    ret[i] = numpy.log(arr[i + 1] / arr[i])
  return ret

測定の結果、出力は1.805896282196045となり、100倍ほどの高速化に成功しました。

JITを有効化/並列化してみる

以下のようにデコレータを変更し、range関数をnumba.prange関数に置き換え、並列化してみます。 ret = numpy.empty(arr.size - 1)の部分は、numbaが勝手にうまいこと(hoist)してくれて、期待通りの動作をします。

@numba.jit(nopython=True, parallel=True)
def calculate_diffs(arr):
  ret = numpy.empty(arr.size - 1)
  for i in numba.prange(arr.size - 1):
    ret[i] = numpy.log(arr[i + 1] / arr[i])
  return ret

測定の結果、出力は0.7335679531097412となり、200倍以上の高速化に成功したことになります。

まとめ

限られたケースにはなりますが、Numbaをうまく使えば非常に簡単に高速化ができることがわかりました。

Express/Node.jsでTypeScriptを使ってみる

f:id:k-kty:20190213160011p:plain

最近、バックエンド開発でNode.jsを用いています。 JavaScriptは仕様がどんどん良くなってきているし、コールバックを用いた非同期処理も個人的には好きです。

しかしコードベースが大きくなると「型があればなあ...」と思うこともしばしば。 そこでTypeScriptに足を突っ込んでみることにしました。

そのときの記録の一部として、TypeScript + Node.js/ExpressでWebバックエンドの開発を始める方法を紹介します。

TypeScriptの文法・言語の機能についてはあまり触れません。(それらについては以下が参考になります。)

準備

npmプロジェクトを作成する

npm init を用いるなどして、package.jsonを作成します。

TypeScriptをインストール

$ npm i typescript --save-dev

JavaScriptからTypeScriptへのコンパイルに必要です。

TypeScriptの設定ファイルを作成

以下のような内容をtsconfig.jsonという名前で保存します。

{
  "compilerOptions": {
    "target": "es5",
    "outDir": "dist",
    "strict": true
  },
  "include": [
    "src/**/*",
  ]
}

軽く説明すると、このようになります。

  • target で、「どの仕様に対応したJavaScriptのコードをコンパイルによって生成するか」を指定しています。ここではES5を指定しています。
  • outDir で、コンパイル後のコードを保存するディレクトリを指定します。
  • strict をtrueにすることで、いろいろなTypeScriptのチェック機構が有効になります。
  • include で、コンパイルの対象にしたいファイル群を指定します。

expressとその「型定義ファイル」をインストール

$ npm i express --save
$ npm i @types/express --save-dev

このようにパッケージと一緒に型定義ファイルもインストールすることで、そのパッケージの関数やオブジェクトを使うときにもTypeScriptの型チェックを利用できるようになります。

ts-nodeをインストール

$ npm i ts-node --save-dev

これにより、node コマンドでJavaScriptコードを実行できるように、ts-node コマンドでTypeScriptコードを実行できるようになります。 ts-nodeは、「tscを用いてTypeScriptのコードをJavaScriptへコンパイルし、それをnodeで実行する」ということをしてくれています。 開発中に使うと便利です。なお、本番環境での仕様は推奨されていません。

実行スクリプトを追加

package.jsonの scripts の中身を以下のように書き換えます。

{
  "build": "tsc",
  "start": "node dist/index.js",
  "dev": "ts-node src/index.ts"
}

npm run build でTypeScriptのコードをコンパイルし、npm run start でそのJavaScriptコードを実行できるようになります。 npm run dev はその2つを組み合わせたことをやってくれますが、開発中のみ利用します。

Hello, Worldしてみる

コード

src/index.ts

import app from './app';

app.listen(process.env.PORT || 3000);

src/app.ts

import express from 'express';
import routes from './routes';

const app = express();

app.use('/', routes);

export default app;

src/routes.ts

import express from 'express';

const router = express.Router();

router.get('/', (req, res, next) => {
  res.send('Hello, World');
});

export default router;

起動

以下のコマンドでサーバーが起動します。

$ npm run dev

以下のように、きちんと動いていることがわかります。

$ curl http://localhost:3000/
Hello, World

テストをする

supertest を用いて、エンドポイント単位のテストをしてみます。

準備

supertestのインストール

supertestは、Node.jsのHTTPサーバーをテストするためのライブラリです。 型定義ファイルも同時にインストールします。

$ npm i supertest @types/supertest --save-dev

参考: https://github.com/visionmedia/supertest

mochaのインストール

mochaは、テストのためのフレームワークです。 下のコードに出てくる describe とか it とかいう関数はmochaが提供しているものです。 やはり、型定義ファイルも同時にインストールします。

$ npm i mocha @types/mocha --save-dev

参考: https://mochajs.org/

実行スクリプトを追加

package.json内の scripts に以下を追加します。 これにより、 npm run test でテストを実行できるようになります。

"test": "mocha --require ts-node/register test/**/*.ts"

--require ts-node/register により、JavaScriptによってコンパイルすること無くテストを実行できます。

コード

test/routes.ts

とても簡単なものです。

import request from 'supertest';
import 'mocha';

import app from '../src/app';

describe('ルーティング', () => {
  describe('/', () => {
    it('ステータスコード200を返す', async () => {
      await request(app)
        .get('/')
        .expect(200);
    });
  });
});

import mochadescribeit を使うために必要です。

実行

$ npm run test
  ルーティング
    /
      ✓ ステータスコード200を返す
  1 passing (37ms)

テストが通りました。

ビルド・実行する

本番環境で実行する際には、JavaScriptにコンパイル(ビルド)して、その結果生成されたファイルをNodeで実行する必要があります。 このための準備はすでにできていて、以下のスクリプトを実行するだけで大丈夫です。

$ npm run build
$ npm start

AWS S3 の特徴まとめ

AWSを使っている人全員が触ったことがあるであろうS3。整理のためのメモ。

  • データを複数の場所に複製するという特徴から、データの更新・削除には結果整合性が採用されている
  • Getでファイルをダウンロードできる
  • Putでファイルをアップロード(新規/更新)できる(最大5GBまでだが、Multipart Uploadを利用すると5TBまで可能)
  • Listでオブジェクト一覧を取得できる(最大1000件まで)
  • Copyでオブジェクトを複製できる(最大5GBまでだが、Multipart Uploadを利用すると5TBまで可能)
  • Deleteでオブジェクトを削除できる(最大1000個)
  • Headでオブジェクトのメタデータを取得できる
  • RestoreでアーカイブされたオブジェクトをS3に取り出すことができる
  • ユーザーポリシー/バケットポリシー/ACLを用いたアクセス権の決定ができる
  • Get/Putについては、Pre-signed URLを利用することでセキュアに操作を行うことができる
  • 静的なwebサイトをホスティング可能
  • AJAXなどを利用して、異なるドメインからS3でホスティングしているファイルにアクセスしたい場合にはCORSの設定をする
  • ホスティングの際は、CloudFrontとともに使うことを推奨
  • VPC Endpointを利用すると、VPC内のPrivate SubnetからNAT GatewayやNATインスタンスを経由せずに直接S3とセキュアに通信ができる(同一リージョンの場合のみ)
  • クロスリージョンレプリケーションを用いると、異なるリージョン間でオブジェクトのレプリケーションが可能
  • バージョン管理が可能(バケットに対して設定する)
  • ライフサイクル管理を用いると、バケット全体/Prefixに対して、日単位でのストレージクラスの変更/削除処理が可能
  • オブジェクトをアーカイブすると、データはGlacierに移動する
  • S3インベントリを利用すると、オブジェクトのリストをcsvとして取得できる
  • バケットにイベントが発生した際に、SNS/SQS/Lambdaなどに通知(連携)することができる
  • LambdaでS3バケットのイベントを利用したい場合は、Lambdaが利用するIAM RoleにS3の権限を付与する必要がある
  • 大きなサイズのファイルをダウンロードする際にRange Getを利用することで、マルチスレッド環境で高速なダウンロードが可能(Multipart Upload時と同じチャンクサイズを利用)
  • Multipart Uploadはチャンクサイズと並列コネクション数のバランスが重要
  • ファイルが100MBを超える場合に、Multipart Uploadの利用が推奨される
  • Multipart Uploadでは各チャンクは5GB以下である必要がある
  • バケットへのリクエストが定常的に数百RPSを超える場合は、キー名の先頭部分の文字列をランダムにすると良い

参考になるサイト・スライド

aws.amazon.com

www.slideshare.net