RedisのLuaスクリプティング機能について

the Lua interpreter built into Redis
the Lua interpreter built into Redis

僕のRedisについての知識はv2.2くらいで止まっていたので、それ以降のRedisに備わった機能を調べているんですけど、その中でもv2.6からサポートされたLuaスクリプト実行環境について今回は整理します。

技術Wikiの方にもRedisについてのメモを残しています。
* Redis – Tech Note

環境

CentOS 5.8 (x86_64)
Redis 2.6.10 (malloc=jemalloc-3.2.0 bits=64)
Pythonクライアント (redis-py 2.7.2)

※ Redis 2.6で利用できるLuaのバージョンは5.1です。

Lua言語について

Lua – Wikipediaによると、

Lua は、C言語のホストプログラムに組み込まれることを目的に設計されており、高速な動作と、高い移植性、組み込みの容易さが特徴である。いったんバイトコードにコンパイルされ、Lua VM で実行される。LuaJIT は The Computer Language Benchmarks Game によると、変数に型のないスクリプト言語では最速の言語・処理系である。

LuaJITの速さは有名ですね。ただ、Web開発の現場だとLuaを使う人は少ないかもしれません。ちなみにApache2.4からは mod_lua が使えますけど、これも遊びでならともかく実務で活用している人はそんなに多くはないと思います。僕の周りでは独自プロトコルを使っている社内システム用にWireshark(パケットキャプチャツール)のプラグインを書くときなどに使っている人はたまにいますが、Luaの主戦場はやはりゲーム業界のようです。あと、TAS(Tool Assisted Speedrun)制作をする人はLuaスクリプトが書けるといろいろ捗るらしいですね、ゲームは好きなのでちょっと興味あります。

Luaパッケージリポジトリ LuaRocks

LuaRocksはLuaモジュールパッケージのアーカイブ、CPANやRubyGemsみたいなものです。今回はRedisサーバ上でLuaスクリプトを実行するのですが、パッケージ管理ツールを含めたLua開発環境をローカルに整えておくと自作スクリプトの動作確認をする時などに便利なのでぜひ入れておきましょう。
[bash]
## モジュールのインストール
## –local オプションを付けるとユーザーディレクトリ($HOME/.luarocks 以下)にインストールされる
$ luarocks install –local redis-lua

## インストール済みモジュールの表示
$ luarocks list
Installed rocks:
—————-

lua-cjson
2.1.0-1 (installed) – /home/ryo/.luarocks/lib/luarocks/rocks

lua-cmsgpack
0.3-2 (installed) – /home/ryo/.luarocks/lib/luarocks/rocks

luasocket
2.0.2-5 (installed) – /home/ryo/.luarocks/lib/luarocks/rocks

redis-lua
2.0.4-1 (installed) – /home/ryo/.luarocks/lib/luarocks/rocks
[/bash]
CPANやRubyGemsほどではないですが、データベースドライバやHTTPクライアントなどいろんなモジュールが揃っているようなので、組み込み用途だけではなく簡単なバッチ処理などでもLuaは使えるのではないでしょうか。

では、Redis公式サイトのEVAL – Redisを読みながら整理を進めていきます。

Hello, World

EVAL script numkeys key [key ...] arg [arg ...]
summary: Execute a Lua script server side

EVALコマンドは引数に渡した文字列ををLuaスクリプトとして実行(評価)します。numkeys には操作するkeyの数を指定します。keyの操作が必要ないスクリプトの場合は0を指定すればOKです。
[bash]
> eval “return ‘hello, world'” 0
“hello, world”
[/bash]

スクリプトファイルを読み込んで実行

redis-cli コマンドの –eval オプションを使えば外部スクリプトファイルを読み込んで実行できます。
[javascript]
— hello.lua
local message = ‘hello, world’
return message
[/javascript]
[bash]
## keyの数は指定しなくてよい
$ redis-cli –eval hello.lua
“hello, world”
[/bash]
または、以下のようにシェルでファイルの中身を展開して渡す方法もあります。
[bash]
$ redis-cli eval “$(cat hello.lua)” 0
“hello, world”
[/bash]

LuaからRedisのコマンドを呼ぶ

Luaスクリプト内でRedisの各コマンドを呼ぶことができます。Luaから使えるAPIは以下の2つ。

 * redis.call() (エラー発生時は処理を終了)
 * redis.pcall() (エラー発生時はエラーを捕捉して処理を継続)

この2つのAPIはエラー時の挙動が異なるという違いがあり、実務で使うには違いを正しく知っておかなければならないと思いますが、ここでは2つのAPIを使い分けることはせず redis.call() の方を使うことにします。
参考: Lua 5.1 リファレンスマニュアル 2.7 – エラー処理

まずは簡単な例から。
[bash]
> eval “return redis.call(‘INCR’, KEYS[1])” 1 foo
(integer) 1
[/bash]
ここで新しくKEYS配列(Luaでは配列もテーブルで表現されますが、ここでは便宜上”配列”と表記)が登場しました。これは名前の通り操作対象となるkeyが格納されている配列になります(Luaでは配列の添字は1から始まるので注意)。上記例では’foo’というkeyに対してINCRコマンドを呼んで値をインクリメントしています(存在しないkeyに対してINCRコマンドを実行するとRedisはintegerの1を返却する)。

また、更新する値を指定する場合はKEYSと併せてARGV配列も使います。このARGV配列には操作するkeyに対する値(value)が格納されます。
[bash]
> eval “return redis.call(‘SET’, KEYS[1], ARGV[1])” 1 foo 100
OK
> get foo
“100”

> eval “return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}” 2 key1 key2 first second
1) “key1”
2) “key2”
3) “first”
4) “second”
[/bash]
なお、redis-cli コマンドの –eval オプションは操作するkeyの数を指定できないためARGV配列は利用できません。

エラー処理

エラー処理用のヘルパー関数が提供されているので、これをLuaスクリプト内で利用することができます。

 * redis.error_reply(message)
 * {err=message} (errフィールドを持ったテーブルオブジェクト)

上のAPIは内部的にerrフィールドを持ったテーブルオブジェクトを生成して返すラッパー関数になっていますので、どちらを使っても挙動に違いはありません。
[bash]
> eval “return redis.error_reply(‘Error occurred’)” 0
(error) Error occurred
> eval “return {err=’Error occurred’}” 0
(error) Error occurred
[/bash]

ログ処理

ロギング用の関数も提供されているので、こちらもLuaスクリプト内で利用できます。

 * redis.log(loglevel, message)
  loglevel: LOG_DEBUG, LOG_VERBOSE, LOG_NOTICE, LOG_WARNING
[bash]
> eval “redis.log(redis.LOG_NOTICE, ‘log message here.’)” 0
(nil)
> eval “redis.log(redis.LOG_WARNING, ‘something is wrong with this script.’)” 0
(nil)
[/bash]
Redisのログファイルに出力されているか確認。

[30518] 11 Apr 16:36:31.597 * log message here.
[30518] 11 Apr 16:36:34.347 # something is wrong with this script.

Luaモジュールの利用

Redis v2.6.10に組み込まれているLua処理系では以下のモジュールが利用できます。

 * base
 * table
 * string
 * math
 * debug
 * cjson (3rd party)
 * cmsgpack (3rd party)

ここではいくつかのモジュールを紹介します。

table (テーブルモジュール)

tableモジュールはLuaの標準モジュールに含まれており、テーブル操作の為のユーティリティを提供しています。
[javascript]
local t = {3,5,1,4,2}
— テーブル内要素を降順ソート(in-place)
table.sort(t, function(a, b) return a > b end)
— テーブル内要素を ‘ > ‘ で繋げた文字列を返す
return table.concat(t, ‘ > ‘)
[/javascript]
返される文字列は “5 > 4 > 3 > 2 > 1” となります。

cjson (JSONモジュール)

cjsonモジュールを使えばJSONデータのエンコード/デコードが可能です。せっかくRedisを使っているのだからJSONのような構造化データは使わず、素直にRedisのListやHash型を活用した設計を心掛けた方が良いと思いますが、JSONデータをやりとりするWeb APIとの連携が必要な際などに有用なのではないでしょうか。
[javascript]
— cjsontest.lua
local key = ‘cjsontest’
if redis.call(‘EXISTS’, key) == 1 then
local encoded = redis.call(‘GET’, key)
local decoded = cjson.decode(encoded)
local str = ‘table: ‘
for k,v in pairs(decoded) do
str = str..k..’=’..v..’ ‘
end
return str
else
local data = {foo=1, bar=2, baz=3}
local encoded = cjson.encode(data)
redis.call(‘SET’, key, encoded)
return ‘set ok’
end
[/javascript]
[bash]
$ redis-cli –eval cjsontest.lua
“set ok”
$ redis-cli –eval cjsontest.lua
“table: foo=1 bar=2 baz=3 ”
[/bash]

cmsgpack (MessagePackモジュール)

cmsgpackモジュールを使えばMessagePack形式のデータを扱うことができます。
[javascript]
— cmsgpacktest.lua
local key = ‘cmsgpacktest’
if redis.call(‘EXISTS’, key) == 1 then
local packed = redis.call(‘GET’, key)
local unpacked = cmsgpack.unpack(packed)
local str = ‘table: ‘
for k,v in pairs(unpacked) do
str = str..k..’=’..v..’ ‘
end
return str
else
local data = {foo=1, bar=2, baz=3}
local packed = cmsgpack.pack(data)
redis.call(‘SET’, key, packed)
return ‘set ok’
end
[/javascript]
[bash]
$ redis-cli –eval cmsgpacktest.lua
“set ok”
$ redis-cli –eval cmsgpacktest.lua
“table: foo=1 bar=2 baz=3 ”
[/bash]
それはそうとMessagePackはIETF絡みに巻き込まれていろいろざわついているようですね。これからどうなるのか気になります。

簡単にですがいくつかのモジュールを紹介しました。今後はcjsonやcmsgpack以外のサードパーティーモジュールも組み込まれるのでしょうか。

RedisへのLuaスクリプトの登録

作成したLuaスクリプトをRedisサーバに登録する機能が提供されています。あらかじめサーバ側に登録しておけばスクリプトの転送コストを削減することができます。MySQLなどのRDBMSでいうところのStored Procedure/Functionのような機能ですね。

## Luaスクリプトのサーバへの登録、スクリプトのSHA1ハッシュダイジェストが返される
SCRIPT LOAD script
summary: Load the specified Lua script into the script cache.

## 登録されているLuaスクリプトを実行、第一引数にはスクリプト本体ではなくSHA1ハッシュ文字列を指定
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
summary: Execute a Lua script server side

[bash]
## SCRIPT LOADコマンドでLuaスクリプトの登録
> script load “return ARGV[1] + ARGV[2]”
“53c370cb7894e73f9aaf94627c0506c428b5cc97”
## EVALSHAコマンドで登録されているスクリプトを実行
> evalsha 53c370cb7894e73f9aaf94627c0506c428b5cc97 0 100 200
(integer) 300
## 未登録のSHA1ダイジェストを指定するとエラーを返す
> evalsha foobarbaz 0 100 200
(error) NOSCRIPT No matching script. Please use EVAL.
[/bash]
登録したLuaスクリプトの管理はどうするんだろうと悩んでしまいますが、MySQLでいうところの mysql.proc みたいなメタテーブルを別途用意して運用するんでしょうか。それはなんか事故りそう。。

SHA1ダイジェストを計算するLua API

公式ドキュメントには書かれていませんが、SHA1ダイジェストを計算するLua APIも公開されているので(実装はscripting.cの650行目あたり、fritzyさんのパッチ)、SHA1の計算をサーバ側に任せることができるので便利に利用できそうです。

* redis.sha1hex()
[bash]
> eval “return redis.sha1hex(‘calculate SHA1 hash’)” 0
“24291b3d56a34e934879e1864cf921665de7522b”
[/bash]

アプリケーションからの利用

実際にはコマンドラインからではなくプログラムから使うことが多いと思うので、ここではPythonクライアント(redis-py)からLuaスクリプトの実行を試してみます。
[python]
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import redis

try:
r = redis.Redis(host=’localhost’, port=6379)
## Luaスクリプト
script = ‘return redis.call(“INCR”, KEYS[1])’
## EVAL
ret = r.eval(script, 1, ‘foo’)
print ‘INCR’,ret
## SCRIPT LOAD
sha1 = r.script_load(script)
print ‘SHA1’,sha1
## EVALSHA
ret = r.evalsha(sha1, 1, ‘foo’)
print ‘INCR’,ret
except redis.RedisError as e:
print e
[/python]
* 実行結果

INCR 1
SHA1 f793247de6e1e3c553cd42d39c812df499e679e4
INCR 2

特に問題はなさそうです。他言語のクライアントライブラリもEVALコマンド対応済みのものが多いので確認してみてください。

注意点

RedisのLuaスクリプト実行環境を利用する上での注意点をいくつか挙げておきます。

グローバル変数は作れない

RedisのLuaインタプリタではグローバル変数の作成は許可されていません。
[bash]
> eval ‘g=1’ 0
(error) ERR Error running script (call to f_97a87ce4bb06826f606c5ff88d7d93417412e4d1): user_script:1: Script attempted to create global variable ‘g’
[/bash]

Redis-Lua間での型変換

RedisとLuaの間でデータをやりとりする際の型変換には注意する必要があります。特に数値の扱いは正しく理解しておかないと罠にハマってしまいます。
[bash]
## Luaの浮動小数点値はRedisでは整数として扱われる
> eval ‘return 3.14’ 0
(integer) 3
[/bash]
[javascript]
— numeric.lua
— Luaの数値(整数/浮動小数点数共に)をRedisに格納する際は文字列に変換される
local pi = 3.14
redis.call(‘SET’, ‘pi’, pi)
return redis.call(‘GET’, ‘pi’)
[/javascript]
[bash]
$ redis-cli –eval numeric.lua
“3.14”
[/bash]
ということで、Redis-Lua間で浮動小数点値をやりとりしたい時は文字列データとして運用しましょう。

テーブル内のnil

Luaのテーブルに nil が入ってしまわないように注意してください。テーブル {1, 2, nil, 4} はRedisでは {1, 2} と解釈されてしまいます。忘れがちなので注意。
[bash]
##
> eval “return {1,2,nil,4}” 0
1) (integer) 1
2) (integer) 2
[/bash]

スクリプト実行のアトミック性

Redis上で動作させるLuaスクリプトはアトミックに実行されるため、keyに対する操作を安全に行うことができます。ただし、アトミックということは1つのスクリプトの実行時間が長いと他のクライアントのスクリプト実行がブロックされてしまうので注意しておく必要があります。

Redis Luaスクリプトアーカイブ – EVALSHA

EVALSHA

In Redis, each Lua script is identified by taking the SHA1 of its body. This is a public place to discover, share and discuss scripts. Think of it like luarocks, cpan, rubygems, or npm but for Redis scripts.

Redis用Luaスクリプトのアーカイブサイトです。非公式で登録されているスクリプトの数もまだ少ないですが、誰でも簡単に自作Luaスクリプトを登録できます。僕もcjsonモジュールを使った簡単なスクリプトを登録してみました。
* JSON Decode – EVALSHA

感想とか

以上、RedisのLuaスクリプティング機能について簡単に整理しました。Lua言語自体の学習コストは低いですし、Redis-Lua間での型変換についてもルールを正しく知っていれば問題にはならないと思います。頭を悩ませそうなのは、やはりサーバ側に登録したLuaスクリプトの管理でしょうか。また、実務でRedisを活用しているところだと僕の周りではv2.2系のシェアが一番多いのですが、大規模運用している現場だとv2.6系への移行コストがどのくらいなのか気になります。

今回の検証でLua言語自体に強い興味を持ちましたので、C言語との連携等も含めいろいろ試していきたいと思います。

* 参考
EVAL – Redis
Lua: A Guide for Redis Users
Redis reliable queues with Lua scripting

あわせて読む:

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です