kakts-log

programming について調べたことを整理していきます

redis 4系から追加されたUNLINKについて

はじめに

2017年7月にリリースされたredis4.0から、特定のキーの値を非同期的に削除するUNLINKコマンドがでました。 UNLINKコマンドの既存のDELコマンドとの違いついてまとめます。

redis4.0系リリース

2017年7月にredis4.0系がリリースされました。 主な機能に関しては、下記のqiitaの記事にまとめられています。 qiita.com

redis4.0.0のリリースノートは下記のリンクで見ることができます。 https://raw.githubusercontent.com/antirez/redis/4.0/00-RELEASENOTES

若干読みづらいですが、 UNLINKに関する記述を抜粋します。

Lazy freeing of keys. Redis is now able to delete keys in the background in a different thread without blocking the server. The new UNLINK command is the same as DEL but working in a non blocking way.

UNLINKコマンドが生まれた経緯 シングルスレッドモデルにおける DELコマンドの問題点

redisはシングルスレッドモデルのため、既存のDELコマンドの場合だと 複数のクライアントからクエリを実行された時、1つのキューに並んで順番に処理されます。
そのため、データサイズの大きいデータセットに対してDELを実行すると、計算量が大きいため、後続の処理が遅れて大きなパフォーマンス低下の要因となってしまいます。

今回4.0系で登場したUNLINKは、ある値を削除する際にこの問題を解決するコマンドです。
UNLINKを使ったクエリが実行された際、文字通り、指定されたキーの値を即座に削除せず、非同期的にデータセット内のキーを格納したキースペースからUNLINKされ、実際のデータは後に削除されます。 これにより、ノンブロッキングな処理が可能となります。

クエリの実行的には即座に完了し、データセットのサイズに関係なく定数時間 O(1)で処理が完了します。

同時に複数のキーを指定した場合は キーの数N 文の時間の計算量O(N)となります。

UNLINKの内部処理を見る

UNLINKコマンドの内部処理が気になったので、githubに上がっているredisのソースを読んでみました。 関連のソースは、 src/lazyfree.cあたりにまとまっています。

UNLINKが実行されたときに、dbAsyncDeleteと呼ばれる関数が呼ばれます。 https://github.com/antirez/redis/blob/unstable/src/lazyfree.c#L49-L85 文字通り、データを非同期的に削除する処理を行っています。

/* Delete a key, value, and associated expiration entry if any, from the DB.
 * If there are enough allocations to free the value object may be put into
 * a lazy free list instead of being freed synchronously. The lazy free list
 * will be reclaimed in a different bio.c thread. */
#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
    /* Deleting an entry from the expires dict will not free the sds of
     * the key, because it is shared with the main dictionary. */
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

    /* If the value is composed of a few allocations, to free in a lazy way
     * is actually just slower... So under a certain limit we just free
     * the object synchronously. */
    dictEntry *de = dictUnlink(db->dict,key->ptr);
    if (de) {
        robj *val = dictGetVal(de);
        size_t free_effort = lazyfreeGetFreeEffort(val);

        /* If releasing the object is too much work, let's put it into the
         * lazy free list. */
        if (free_effort > LAZYFREE_THRESHOLD) {
            atomicIncr(lazyfree_objects,1);
            bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
            dictSetVal(db->dict,de,NULL);
        }
    }

    /* Release the key-val pair, or just the key if we set the val
     * field to NULL in order to lazy free it later. */
    if (de) {
        dictFreeUnlinkedEntry(db->dict,de);
        if (server.cluster_enabled) slotToKeyDel(key);
        return 1;
    } else {
        return 0;
    }
}

さらに内部を辿っていくと、 src/dict.cの方で dictGenericDeleteと呼ばれるstatic関数を呼び出しています。
これは、データセット内の特定のキーへのリンクを解除しています。
https://github.com/antirez/redis/blob/06263485d46696ba76a653d2b594f3493103c001/src/dict.c#L361-L399

/* Search and remove an element. This is an helper function for
 * dictDelete() and dictUnlink(), please check the top comment
 * of those functions. */
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
    unsigned int h, idx;
    dictEntry *he, *prevHe;
    int table;

    if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;

    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);

    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        prevHe = NULL;
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                /* Unlink the element from the list */
                if (prevHe)
                    prevHe->next = he->next;
                else
                    d->ht[table].table[idx] = he->next;
                if (!nofree) {
                    dictFreeKey(d, he);
                    dictFreeVal(d, he);
                    zfree(he);
                }
                d->ht[table].used--;
                return he;
            }
            prevHe = he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return NULL; /* not found */
}

更に細かくは見れていないですが、 データセットは内部的にlinked listのようなデータ構造でリストを保持していて、 dictGenericDelete内では、対象のキーへの参照を外すことで、UNLINKを行っているようです。
上記のコードをのwhileループ内の処理を抜粋してみると、おぼろげながらも理解ができるかと思います。

        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                /* Unlink the element from the list */
                if (prevHe)
                    prevHe->next = he->next;
                else
                    d->ht[table].table[idx] = he->next;
                if (!nofree) {
                    dictFreeKey(d, he);
                    dictFreeVal(d, he);
                    zfree(he);
                }
                d->ht[table].used--;
                return he;
            }
            prevHe = he;
            he = he->next;
        }

dictGenerictDeleteの第3引数nofree は2値の値で、1の場合は、直接データを消さずに参照だけ外し、 0の場合は直接データを消す、つまりメモリをfreeしているようです。

static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {

実際には下記の用な感じで呼び出されています。

nofree == 0 つまりメモリをfreeさせる場合
/* Remove an element, returning DICT_OK on success or DICT_ERR if the
 * element was not found. */
int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}
nofree == 1 つまり 今回紹介したUNLINKのように参照を外す場合
/* Remove an element from the table, but without actually releasing
 * the key, value and dictionary entry. The dictionary entry is returned
 * if the element was found (and unlinked from the table), and the user
 * should later call `dictFreeUnlinkedEntry()` with it in order to release it.
 * Otherwise if the key is not found, NULL is returned.
 *
 * This function is useful when we want to remove something from the hash
 * table but want to use its value before actually deleting the entry.
 * Without this function the pattern would require two lookups:
 *
 *  entry = dictFind(...);
 *  // Do something with entry
 *  dictDelete(dictionary,entry);
 *
 * Thanks to this function it is possible to avoid this, and use
 * instead:
 *
 * entry = dictUnlink(dictionary,entry);
 * // Do something with entry
 * dictFreeUnlinkedEntry(entry); // <- This does not need to lookup again.
 */
dictEntry *dictUnlink(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,1);
}

という感じになります。 このあたりの実装は読んでいて非常に勉強になるので、redisを使っている方は読んでみることをおすすめします。

まとめ

redis4.0系から、データセット内の特定のキーを非同期で削除できるUNLINKコマンドが追加されました。 UNLINKは、DELとは異なり、実際にクエリが実行されたときにデータを削除せず、あくまでデータセット内からキーの参照を非同期で UNLINKしています。
これにより、シングルスレッドモデルを採用しているredisで、データ容量が大きい場合にDELによって実行速度が遅くなってしまう問題を解消できます。

実際にパフォーマンスについては今回検証できていないですが、redis4.0系以上を使う場合は UNLINKを使うことによって大きくパフォーマンス改善が図れるかと思います。

Golang listパッケージを使ったdoubly linked listの操作

Golang listという標準パッケージを使ってdoubly linked listの操作を簡単に行えるので、まとめます。

list - The Go Programming Language

使い方

listパッケージを使うためには、公式ドキュメントにある通り、"container/list"をインポートします。

list.New()で新たなlistインスタンスを生成する事ができます。

http://golang-jp.org/pkg/container/list/#New

func New() *List

List構造体とListがもつ要素を表すElement構造体は、以下のようなデータ構造となります。

// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List struct {
    root Element // sentinel list element, only &root, root.prev, and root.next are used
    len  int     // current list length excluding (this) sentinel element
}

// Element is an element of a linked list.
type Element struct {
    // Next and previous pointers in the doubly-linked list of elements.
    // To simplify the implementation, internally a list l is implemented
    // as a ring, such that &l.root is both the next element of the last
    // list element (l.Back()) and the previous element of the first list
    // element (l.Front()).
    next, prev *Element

    // The list to which this element belongs.
    list *List

    // The value stored with this element.
    Value interface{}
}

List構造体は、Element型のroot要素を一つもつのと、List自体の長さを表すlenを持ちます。
そしてElement構造体は、要素自身がもつ値Valueと、自分が属するListのポインタ Listと、さらには、自分の要素の前・後に位置する要素のポインタ next, prevを持ちます。

List Element構造体を使って、他の言語でもよく見られる一般的なdoubly linked listの構造をあらわしています。

package main

import (
    "container/list"
    "fmt"
)

func main() {
    // create a new list
    l := list.New()
    l.PushBack(1) // l {1}

    // list mに要素を2つ追加
    m := list.New()
    m.PushBack(2) // m {2}
    m.PushBack(3) // m {2, 3}
    fmt.Printf("Length of m is %d\n", m.Len())

    // lの後ろにmをつなげる
    l.PushBackList(m)

    // Iterate through list and print its contents.
    // 1 2 3と表示される
    for e := l.Front(); e != nil; e = e.Next() {
        fmt.Println(e.Value)
    }

    // l の先頭に要素追加
    l.PushFront(5) // l {5, 1, 2, 3}

    // l の先頭にmの要素をつなげる
    l.PushFrontList(m) // l {2, 3, 5, 1 , 2, 3}

    // Iterate through list and print its contents.
    // 2 3 5 1 2 3 と表示される
    for e := l.Front(); e != nil; e = e.Next() {
        fmt.Println(e.Value)
    }

    // 最後の要素を削除する
    l.Remove(l.Back())
    // Iterate through list and print its contents.
    // 2 3 5 1 2 と表示される
    for e := l.Front(); e != nil; e = e.Next() {
        fmt.Println(e.Value)
    }

}

非常に簡単に使えます。
Goに関しては勉強中で、サードパーティのコレクションライブラリなどもあるかどうかは調べれてないですが、
listパッケージを使って問題なくdoubly linked listを利用できます。

node.js nightlyバージョンで使えるようになった http2モジュールについて

概要

先日 node.jsにおいて http/2の実装がマージされました。
experimentalな機能で、現在だとnode nightlyバージョン(9.x系)で実行時にオプションを付けることで http2 コアモジュールが利用可能です。
近々node.js 8.xに取り込まれるようで年内にはちゃんと使えるようです。
今回は、このhttp2コアモジュールについて簡単な使い方と内部実装について調べたことをまとめます。

ドキュメントはnodeのmasterブランチにマージされており、見ることができます。
- github.com

http2コアモジュールの実装について

http2の実装はnghttp2というCライブラリが採用されております。 このライブラリはhttp/2の実装のなかで人気であり、 RFC7540 HTTP/2 とRFC7541の仕様に基づいて実装されています。
- github.com

http2 コアモジュールのJS apiは下記で定義されています。
- node/http2.js at master · nodejs/node · GitHub

http/2のJS apiの各実装は lib/internal/http2配下のファイルにあります。内部の実装を見たい人はここで見ることができます。
- node/lib/internal/http2 at master · nodejs/node · GitHub

nghttp2ライブラリへのインターフェースはsrc/node_http2.ccと src/node_http2.hで定義されています
nghttp2とコアモジュールのやり取りはこのあたりを見ればよいかと思います。
- node/node_http2_core.cc at master · nodejs/node · GitHub
- node/node_http2.h at master · nodejs/node · GitHub

上記のhttp2コアモジュールを使うにあたり、nodeの実行時に –expose-http2 フラグを追加することで、 require(‘http2’) が使えるようになります。
これは、すでに http2というnpmモジュールが存在しているため、 –expose-http2フラグを付けないと下記のnpmモジュールをrequireするようになり、既存のnpm http2モジュールを使っているプロジェクトは注意が必要です。
- www.npmjs.com

node.js nightly版のインストール

http2のコアモジュールを使うために、node.jsの最も最新のnightly版(現時点で9.x系)を使えるようにします。
mac OSの環境が前提ですが、 node.js nightly版を簡単にインストールできる node-nightlyというnpmモジュールを使います。

node-nightlyモジュールインストール
$npm install --global node-nightly

nightly最新版のnodeインストール
$ node-nightly 
Downloading the nightly version, hang on...
node-nightly is available on your CLI!

nightlyバージョンの確認
$ node-nightly -v
 New nightly available. To upgrade: `node-nightly --upgrade` 
v9.0.0-nightly20170806e96ca62480

これでnode.js nightly版を簡単にインストールすることができました。

http2.createSecureServerを使ってhttp2サーバを作る

ここでさっそくhttp2のTLS/SSLを使ったセキュアなサーバを作ってみます。
http2.createSecureServer()というメソッドで、サーバのインスタンスを作成することができます。

http2.createSecureServer()について

http2.createSecureServer()の内部の実装は、主要なところだけ抜粋すると以下のようになっています。 - https://github.com/nodejs/node/blob/master/lib/internal/http2/core.js#L2427-L2434

function createSecureServer(options, handler) {
  if (typeof options === 'function') {
    handler = options;
    options = Object.create(null);
  }
  debug('creating http2secureserver');
  // Http2SecureServerのインスタンスを返す
  return new Http2SecureServer(options, handler);
}

・・・

// Http2SecureServerクラス
class Http2SecureServer extends TLSServer {
  // コンストラクタ
  constructor(options, requestListener) {
    options = initializeTLSOptions(options);
    super(options, connectionListener);
    this[kOptions] = options;
    this.timeout = kDefaultSocketTimeout;
    this.on('newListener', setupCompat);
    if (typeof requestListener === 'function')
      this.on('request', requestListener);
    this.on('tlsClientError', onErrorSecureServerSession);
    debug('http2secureserver created');
  }

  // setTImeoutメソッド
  setTimeout(msecs, callback) {
    this.timeout = msecs;
    if (callback !== undefined) {
      if (typeof callback !== 'function')
        throw new errors.TypeError('ERR_INVALID_CALLBACK');
      this.on('timeout', callback);
    }
    return this;
  }
}

このメソッドの戻り値として、Http2SecureServerクラスのインスタンスを返します。
Http2SecureServerクラスはTLSServerクラスを継承しており、接続においてSSL/TLSを使う事となります。

TLSServerクラスはtlsというコアモジュールで定義され、実装は下記になります。

tls_wrapでTLSServerクラスの実装が見れます。
[https://github.com/nodejs/node/blob/master/lib/
tls_wrap.js#L784-L893]

かなり階層が深くなりますが、TLS/SSLを使ったセキュアなサーバを作る場合、http2.createSecureServerの引数にオプションを渡すことができ、 接続において使うTLS/SSLの証明書や秘密鍵を設定することができます。 optionとして、オブジェクトのkey, certにそれぞれ指定してあげます。
node/_tls_wrap.js at master · nodejs/node · GitHub

TLS/SSL証明書 秘密鍵の作成

今回はここで証明書と秘密鍵を指定するために、ローカルで自前で準備していきます。
ここではあくまでローカル環境でのhttp2サーバを作ることが目的のため、自前でローカルpcでopensslコマンドを使って作成します。
- stackoverflow.com

mac OSでopensslを使って以下のコマンドを実行することでカレントディレクトリに下記2つのファイルが生成されるので、 サーバインスタンス生成時にこれを指定します。
- server.crt: SSLサーバ証明書 - server.key: SSL公開鍵暗号用の秘密鍵

サーバを立ててみる

さっそく上記の説明を踏まえてhttp2サーバを立ててみます。
ここではあくまでhttp2コアモジュールのサンプルコードをそのまま使用します。

http2サーバインスタンス生成時に 証明書・秘密鍵を指定し、 3000番ポートで待ち受けます。
https://localhost:3000をブラウザでアクセスしてhello worldを表示させる簡単なものを作ります。

サーバを立てた後、アクセスがくると サーバインスタンスに対して"stream"イベントが発火されるので、 ハンドラ関数を設定します。
ここではresponse statusと hello worldを記述した簡単なhtmlをstreamで返します。

// http2コアモジュールをrequire
const http2 = require('http2');

// createSecureServerでのオプションを指定する
const options = {
  key: fs.readFileSync('server.crt'), // 公開鍵暗号の秘密鍵
  cert: fs.readFileSync('server.key') // TLS/SSL サーバ証明書
};

// Create a plain-text HTTP/2 server
const server = http2.createSecureServer(options);

// アクセスが来た場合、streamイベントが発火されう
server.on('stream', (stream, headers) => {
  stream.respond({
    'content-type': 'text/html',
    ':status': 200
  });
  stream.end('<h1>Hello World</h1>');
});

server.listen(3000);

chromeでアクセスする

ここで、サーバを立ち上げる事ができたので、chromeブラウザで https://localhost:3000へアクセスしてみます。
f:id:kakts:20170809005348p:plain
上述したように、今回はTLS/SSL証明書を自前で用意したため、chromeでwarning画面がでますが、無視して、「詳細設定」→「localhost にアクセスする(安全ではありません)」をクリックすることでアクセスが可能です。

f:id:kakts:20170809005555p:plain

まとめ

これでhttp/2のセキュアなサーバにアクセスできました。 今回は簡単な開設のため、http/2固有の機能を使ったサーバの作り方は解説せず、ここで終わります。
http/2固有の機能を使うことでよりパフォーマンスが向上するため、時間があるときに、http/2RFCをちゃんと読んで仕様を把握していきたいと思います。

参考

qiita.com

d.hatena.ne.jp

babel ES6からES5へのトランスパイル

概要

babel.jsをつかって ES6のシンタックスで書かれたjavascriptファイルをES5のシンタックスにトランスパイルする方法をまとめました。
実務で使ったことなかったので勉強がてらさらっとまとめます。

babel-cliのインストー

babelをコマンドラインで利用するに当たって、babelのコマンドラインクライアントである、npm モジュールのbabel-cliをインストールする必要があります。

www.npmjs.com

開発環境のみで使うので –save-devでpackage.jsonに対してdevDependenciesにモジュールの情報を追記させます。

$ npm install --save-dev babel-cli

今回はサンプルとして今回 hapiというhttpサーバモジュールを使うのでこちらもインストールしておきます

$ npm install --save hapi

.babelrcにプラグイン設定記述

babelでトランスパイルする際に、利用するプラグインを指定する必要があります。
今回はES6(ES2015とも言う)からES5へトランスパイルするため、ES2015 presetを指定します。
babeljs.io

babelコマンドの –presetsオプションでも指定できますが、毎回打つのは面倒なので、 babelの設定を記述するファイルである .babelrcを作成し、json形式で記述します。

.babelrc

{
  "presets": ["es2015"]
}

presets項目に使用するプラグイン名を指定します。

トランスパイル元のjsファイル作成

ここで、トランスパイル元のjsファイルである src/index.jsを作成します。
hapiモジュールを使って簡単なhttpサーバを作ります。
全てES6(ES2015)のシンタックスで記述します。

import Hapi from 'hapi';

const server = new Hapi.Server();
server.connection({
  host: 'localhost',
  port: 8000
});

// Add routing
server.route({
  method: 'GET',
  path: '/hello',
  handler: function(request, reply) {
    console.log('[/hello] requested');
    reply('hello world');
  }
});

server.start();

これを importや constを使わない、ES5形式にトランスパイルさせます。

babelコマンドをつかったトランスパイル

トランスパイルした後のファイルは dist/index.jsに作成させるとします。

さっそくコマンドラインでbabelコマンドを使ってトランスパイルさせます。
babelコマンドの –out-fileオプションをつかって、トランスパイル後のファイルの出力先を指定します。

$ babel src/index.js --out-file dist/index.js

これによって、 dist/index.jsにES5形式にトランスパイルされたjsファイルができあがります。

'use strict';

var _hapi = require('hapi');

var _hapi2 = _interopRequireDefault(_hapi);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var server = new _hapi2.default.Server();
server.connection({
  host: 'localhost',
  port: 8000
});

// Add routing
server.route({
  method: 'GET',
  path: '/hello',
  handler: function handler(request, reply) {
    console.log('[/hello] requested');
    reply('hello world');
  }
});

server.start();

元のjsファイルで使っていた import, const が使われずにそれぞれ require varを使うようにトランスパイルされているのが確認できます。

babelをつかったトランスパイルの手順は以上となります。 まだ試していませんが、最新のES2017形式のファイルをES5にトランスパイルする場合は ES2017 presetを使えばできます。
babeljs.io

node.js stringからbufferへの変換

node.jsにおいて、文字列をbufferへ変換させたいときの方法をまとめます

環境
node.js 8.1.3

Bufferインスタンスをどう生成するか

node.js 8.x系におけるBufferインスタンスの作成において、 new Bufferは既にdepricatedになっている*1ため、ここではbufferインスタンスの作成はBuffer.allocを利用します。

Buffer | Node.js v8.1.3 Documentation

Buffer.alloc と Buffer.allocUnsafe

Buffer.alloc(size)の他に、インスタンス生成時にデータを初期化させないBuffer.allocUnsafe(size)があります。
Buffer.allocは、インスタンス生成時にデータをzero-filledする初期化コストがかかるため、Buffer.allocUnsafeよりパフォーマンスは劣りますが、sensitiveなデータを含まないことが保証されるので、速度を最優先させる場面以外において、基本的にBuffer.allocを使う方が良いかと思います。
https://nodejs.org/dist/latest-v8.x/docs/api/buffer.html#buffer_class_method_buffer_allocunsafe_size

文字列をbufferに変換する場合において、Buffer.allocを利用時にBufferサイズ指定する必要があります。 下記に文字列をBufferに変換するスクリプトにあるように、ここで毎回文字列のサイズを渡してBufferインスタンスを生成します。

function bufferFromString(str) {
  // Allocates a new Buffer of size bytes.
  // If fill is undefined, the Buffer will be zero filled;
  // set size of string length
  var buffer = Buffer.alloc(str.length)
  buffer.write(str)
  return buffer
}

const text1 = bufferFromString('hello');
const text2 = bufferFromString('wor');

console.log('----text1', text1);
console.log('----text2', text2);

// text1のbufferにworを書き込む
text1.write('wor');

// 先頭の3文字分 のみ更新させるため、4,5文字目は変わらない
// hello → worlo
console.log(text1.toString('utf8'));

ただ、欠点なのは毎回Buffer.allock(size)でインスタンスを生成した後、Buffer.write()でデータを書き込む必要があるため、若干冗長です。

Buffer.from

Buffer.fromの引数に文字列を渡すと、一発で文字列をBufferに変換できます。

https://nodejs.org/dist/latest-v8.x/docs/api/buffer.html#buffer_class_method_buffer_from_string_encoding

> Buffer.from("hello world")
<Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>

まとめ

文字列からBufferへの変換方法をざっとまとめました。 Buffer.alloc Buffer.allocUnsafe Buffer.fromはそれぞれ挙動に違いがあるので、公式ドキュメントをちゃんと読んで、状況に応じて使い分けるとよいかと思います。

参考

Uncaught TypeError: Cannot read property 'split' of undefined · Issue #185 · Keyang/node-csvtojson · GitHub