kun432's blog

Alexaなどスマートスピーカーの話題中心に、Voiceflowの日本語情報を発信してます。たまにAWSやkubernetesなど。

〜スマートスピーカーやVoiceflowの記事は右メニューのカテゴリからどうぞ。〜

MacにRedisをインストールしてexpress-sessionから使ってみる。

f:id:kun432:20220212163518p:plain

本来やりたいことにすぐに進めずに、express周りを色々調べてます。

以前ちょっとやったexpress-sessionのセッションストレージとしてRedisを使うやつ、前回はHerokuだったのだけど、今回はローカルのMacにインストールしてみる。

目次

Redisのインストール

MacならHomebrewでかんたん。

$ brew install redis

これでサーバもクライアントも一緒に入る。

Homebrewでのサービスの制御&Redisサーバの起動

Homebrewでインストールしたサービスの起動はbrew servicesで操作します。

$ brew services --help
Usage: brew services [subcommand]

Manage background services with macOS' launchctl(1) daemon manager.

If sudo is passed, operate on /Library/LaunchDaemons (started at boot).
Otherwise, operate on ~/Library/LaunchAgents (started at login).

[sudo] brew services [list]:
    List all managed services for the current user (or root).

[sudo] brew services info (formula|--all):
    List all managed services for the current user (or root).

[sudo] brew services run (formula|--all):
    Run the service formula without registering to launch at login (or boot).

[sudo] brew services start (formula|--all):
    Start the service formula immediately and register it to launch at login
(or boot).

[sudo] brew services stop (formula|--all):
    Stop the service formula immediately and unregister it from launching at
login (or boot).

[sudo] brew services restart (formula|--all):
    Stop (if necessary) and start the service formula immediately and register
it to launch at login (or boot).

[sudo] brew services cleanup:
    Remove all unused services.

      --file                       Use the plist file from this location to
                                   start or run the service.
      --all                        Run subcommand on all services.
      --json                       Output as JSON.
  -d, --debug                      Display any debugging information.
  -q, --quiet                      Make some output more quiet.
  -v, --verbose                    Make some output more verbose.
  -h, --help                       Show this message.

何もつけずに実行すると、現在のサービスが表示されます。Redisがサービスとして登録されているのがわかる。ちなみに、うちの場合だとすでにPostgreSQLとunboundがもとから入っていて、PostgreSQLがサービスとして起動している。

$ brew services
Name       Status  User   File
postgresql started kun432 ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist
redis      none
unbound    none

サーバの起動・停止はbrew services start/stopで行う。systemctlでいうところのenable/disableも兼ねているみたい。

$ brew services start redis
==> Successfully started `redis` (label: homebrew.mxcl.redis)

プロセスが上がっている。

$ ps auxw | grep redis
kun432           61300   0.0  0.0 34158584   3668   ??  S     5:00PM   0:00.03 /usr/local/opt/redis/bin/redis-server 127.0.0.1:6379

redis-cliでつないで見る。当然ながらまだ何もkeyはない。

$ redis-cli
127.0.0.1:6379> keys *
(empty array)

express-sessionから使ってみる

ではexpress-sessionから使ってみる。以下で使用したコードをほぼそのまま使う。

npmパッケージの準備。node-redisは古いみたいなので、ioredisにしました。

$ npm init
$ npm install --save express express-session ioredis connect-redis

コードはこんな感じ。

const express = require('express');

const session = require("express-session");
const redis = require("ioredis");
const RedisStore = require("connect-redis")(session);

const app = express();

app.set('port', (process.env.PORT || 3000));

app.use(
    session({
        secret: 'secret',
        cookie: { maxAge: 3600 * 1000 },
        resave: false,
        saveUninitialized: false,
        store: new RedisStore({
            url: process.env.REDIS_URL,
            client: redis.createClient({
                url: process.env.REDIS_URL
            })
        })
    }),
);

app.get('/', function(request, response) {
  let session = request.session;
  console.log(JSON.stringify(session));
  if (!!session.count) {
    session.count += 1;
  } else {
    session.count = 1;
  }

  response.send(`Hello World! count: ${session.count}\n`);
});

app.listen(app.get('port'), function() {
  console.log("Node app is running at localhost:" + app.get('port'));
});

RedisのURLは環境変数で指定するようにしてるので以下。

$ export REDIS_URL="redis://localhost:6379"

では起動。

$ node app.js

ブラウザからhttp://localhost:3000にアクセスしてカウントアップされればOK。

redis-cliでもセッション情報が保持されているのがわかりますね。

$ redis-cli
127.0.0.1:6379> keys *
1) "sess:EZxHrgAzRqDBYVm-BWVozBomRoryhyzY"
127.0.0.1:6379> type sess:EZxHrgAzRqDBYVm-BWVozBomRoryhyzY
string
127.0.0.1:6379> get sess:EZxHrgAzRqDBYVm-BWVozBomRoryhyzY
"{\"cookie\":{\"originalMaxAge\":3600000,\"expires\":\"2022-02-12T09:59:33.666Z\",\"httpOnly\":true,\"path\":\"/\"},\"count\":19}"

おまけ

これ。

この記事から数年経ってて現在のHerokuではちゃんと設定されている。

timeout

The timeout setting sets the number of seconds Redis waits before killing idle connections. A value of zero means that connections are not closed. The default value is 300 seconds (5 minutes). You can change this value using the CLI:

maxmemory-policy

The maxmemory-policy setting sets the key eviction policy used when an instance reaches its storage limit. Available policies for key eviction include:

  • noeviction returns errors when the memory limit is reached.
  • allkeys-lru removes less recently used keys first.
  • volatile-lru removes less recently used keys first that have an expiry set.
  • allkeys-random evicts random keys.
  • volatile-random evicts random keys that have an expiry set.
  • volatile-ttl evicts keys with an expiry set and a short TTL.
  • volatile-lfu evicts using approximated LFU among the keys with an expire set.
  • allkeys-lfu evicts any key using approximated LFU.
  • Heroku Redis doesn’t support tuning lfu-log-factor or lfu-decay-time

By default, this setting is set to noeviction. You can change this value using the CLI:

Homebrewでローカルに入れた場合、もちろんこのあたりの設定は行われていません。

$ cat /usr/local/etc/redis.conf
(...snip...)
# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0
(...snip...)
# The default is:
#
# maxmemory-policy noeviction
(...snip...)

Hobby DevなRedisインスタンスはこんな感じのスペックっぽい。

  • メモリ 25MB
  • 接続数 20

Herokuにあわせて、かつ、タイムアウトなど設定をいじるならこんな感じにしておけば良さそう。

timeout 60
maxclients 20
maxmemory 26214400
maxmemory-policy allkeys-lru

restartして反映しておく。

$ brew services restart redis

あと、Redisのキーは、express-sessionでcookie.maxAgeを設定しておけば、勝手に消える。

(...snip...)
app.use(
    session({
        secret: 'secret',
        cookie: { maxAge: 3600 * 1000 },
(...snip...)

cookie.maxAgeが着れるまでにアクセスするとTTLが更新される。以下のように、cookie.maxAgeを60秒で設定して、60秒立つ前にアクセスすると、更新されているのがわかる。

127.0.0.1:6379> keys *
1) "sess:hANI7uJn5gR0C8wgdA6b_ClUnQtuYyHT"
127.0.0.1:6379> ttl sess:hANI7uJn5gR0C8wgdA6b_ClUnQtuYyHT
(integer) 45
127.0.0.1:6379> ttl sess:hANI7uJn5gR0C8wgdA6b_ClUnQtuYyHT
(integer) 44
127.0.0.1:6379> ttl sess:hANI7uJn5gR0C8wgdA6b_ClUnQtuYyHT
(integer) 44
127.0.0.1:6379> ttl sess:hANI7uJn5gR0C8wgdA6b_ClUnQtuYyHT
(integer) 43
〜ここで一度アクセスする〜
127.0.0.1:6379> ttl sess:hANI7uJn5gR0C8wgdA6b_ClUnQtuYyHT
(integer) 58

この挙動を無効にするにはdisableTouch: trueにすれば良いらしいけど、セッション継続してるのに強制的に消す必要は一般的にはそれほどない気がする。あと、あくまでもこれはセッションの内容が「更新」される場合のみで、参照するだけでは更新されないみたい。

ttl

If the session cookie has a expires date, connect-redis will use it as the TTL.

Otherwise, it will expire the session using the ttl option (default: 86400 seconds or one day).

Note: The TTL is reset every time a user interacts with the server. You can disable this behavior in some instances by using disableTouch.

Note: express-session does not update expires until the end of the request life cycle. Calling session.save() manually beforehand will have the previous value.

その場合、以下にある通り、touchメソッドを使って意図的に更新するか、

disableTouch

Disables re-saving and resetting the TTL when using touch (default: false)

The express-session package uses touch to signal to the store that the user has interacted with the session but hasn't changed anything in its data. Typically, this helps keep the users session alive if session changes are infrequent but you may want to disable it to cut down the extra calls or to prevent users from keeping sessions open too long. Also consider enabling if you store a lot of data on the session.

もしくは、express-sessionのオプションでresave:trueにする必要があるみたい。

(...snip...)
app.use(
    session({
        secret: 'secret',
        cookie: { maxAge: 3600 * 1000 },
        resave: true,
        saveUninitialized: false,
(...snip...)

github.com