🐃

Redisのクラスター構成で発生した「CROSSSLOT Keys in ...」エラーの解決(NestJS/Bull)

2024/05/07に公開

はじめに

今回はNestJSが公式で提供しているジョブキューライブラリ(Bull)のラッパーライブラリ(NestJS/Bull)を利用するにあたって、ローカルで動作検証中にCROSSSLOT Keys in request don't hash to the same slotというエラーに遭遇したのですが、Redis自体の知見が浅いことも相まって解決に苦戦したので、解決方法を残しておきます。

NestJS/Bullの基本的な使い方については公式ドキュメントに記載されています。

環境

エラーが発生したコード

今回のエラーが発生した実装を記載します。

queue.module.ts
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { QueueResolver } from './queue.resolver';
import { QueueProcessor } from './queue.processor';

@Module({
  imports: [
    BullModule.registerQueue({
      name: 'async_process',
    }),
  ],
  providers: [QueueResolver, QueueProcessor],
  exports: [QueueResolver],
})
export class QueueModule
queue.resolver.ts
import { InjectQueue } from '@nestjs/bull';
import { Resolver, Int, Mutation } from '@nestjs/graphql';
import { Queue } from 'bull';

@Resolver()
export class QueueResolver {
  constructor(@InjectQueue('async_process') private createFileJobQueue: Queue) {}

  @Mutation(() => Int, {
    nullable: true,
    description: 'JobQueueを登録する',
  })
  async addQueue(): Promise<number | null> {
    await this.createFileJobQueue.add('create_excel_file', {
      userId: 1
    });

    return null; // 適当にnullを返す
  }
}
queue.processor.ts
import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';

@Processor('async_process')
export class QueueProcessor {
  @Process('create_excel_file')
  async createExcelFile(job: Job<{ userId: number }>) {
    console.log('completed create_excel_file🎉', job.data);
  }
}

上記のコードは、Mutation.addQueue実行時にBullを介してジョブデータを追加する処理を行っており、もし、上記のジョブキューが正常に登録されると、QueueProcessor.createExcelFileがジョブデータを取り込み、ログが出力されるといった実装になっているのですが、今回はジョブキューの登録を行うthis.createFileJobQueue.addの部分でエラーになりました。

エラー内容
CROSSSLOT Keys in request don't hash to the same slot

原因

まず、このエラーについて簡単に調べたところ、Redisのクラスター構成において、複数キーの操作を複数スロットに跨って行うことによって発生することがわかりました。
参考:AWS ElastiCache で「CROSSSLOT Keys in request don't hash to the same slot」エラーが発生する

スロットという概念が最初分からず、実際にスロットがどのような役割を担っているかRedisCLIで確認してみました。

クラスター情報取得
redis-cli -p 7000 -c 
127.0.0.1:7000> CLUSTER NODES
36582a9e09aeebe201cc8e13f6fc9b45284caa4d 127.0.0.1:7000@17000 myself,master - 0 1714430287000 1 connected 0-5460
1516ec9e77eeb3d296a1e1eb2d40c65bfdf5cb6b 127.0.0.1:7001@17001 master - 0 1714430288000 2 connected 5461-10922
97f895ae12f3c06db02103ca63cafecb0f82624a 127.0.0.1:7002@17002 master - 0 1714430289018 3 connected 10923-16383
4c878290e2afa991695d87e061a0e3b0ea8c7b8f 127.0.0.1:7003@17003 slave 97f895ae12f3c06db02103ca63cafecb0f82624a 0 1714430288712 3 connected
6a385eca88c3cb338ab085bf12806d0eced3ca72 127.0.0.1:7004@17004 slave 36582a9e09aeebe201cc8e13f6fc9b45284caa4d 0 1714430287911 1 connected
c044310ae73687fb33235c95cb73392d946fd3ee 127.0.0.1:7005@17005 slave 1516ec9e77eeb3d296a1e1eb2d40c65bfdf5cb6b 0 1714430287000 2 connected

上記を見ると、3つのマスターノードがあり、各マスターノードに対して、0-54605461-1092210923-16383のスロットが割り当てられていることがわかります。(読み取り専用のスレイブノードにはスロットが割り当てられていないようです)

さらにここで適当にキーバリューをセットしてみますと、セットするたびに異なるスロット(ノード)に分散して保存されていることも確認できました。

スロット割り当て確認
SET mykey1 1
-> OK
GET mykey1
-> Redirected to slot [1860] located at 127.0.0.1:7000 # 1860スロットに保存されている
"1"

SET mykey2 2
-> OK
GET mykey2
-> Redirected to slot [14119] located at 127.0.0.1:7002 # 14119スロットに保存されている
"2"

要するにスロットという概念によって、どのノードにデータを格納するか決定づけ、Redisクラスター全体でデータを水平分割(シャーディング)する仕組みを実現しているようです。

なんとなくスロットが何をしているかがわかったので、改めて今回のエラーの詳細を見てみます。すると下記のようなRedisのコマンドが実行されていることがわかりました。

スタックトレース
EVALSHA 8f55ae4a3be429c6d38c5d5db3e80edf89197b64 6 "bull:main_service:wait" "bull:main_service:paused" "bull:main_service:meta-paused" "bull:main_service:id" "bull:main_service:delayed" "bull:main_service:priority" "bull:main_service:" "" "create_excel_file" "{\"userId\":1}" "{\"attempts\":25,\"timeout\":14400000,\"removeOnComplete\":true,\"removeOnFail\":true,\"backoff\":{\"type\":\"exponential\",\"delay\":5000},\"delay\":0,\"timestamp\":1714432557853}" "1714432557853" "0" "0" "0" LPUSH "8db063de-303f-4a99-ba46-833eebf02fe9"

コマンドの詳細は割愛しますが、EVALSHAコマンドで6つのキー(bull:main_service:waitbull:main_service:pausedbull:main_service:meta-pausedbull:main_service:idbull:main_service:delayedbull:main_service:priority)に対する操作を一括で行なおうとしているようです。

この複数キー × 複数スロットの操作がエラーの根本的な原因となります。

解決方法

先程のElasitCacheのナレッジセンターにも記載されているのですが、、'{}'で囲まれた文字列をキーの先頭に付与して、キーのハッシュ化を行う必要があります。

ハッシュ化されたキー
- bull:main_service:wait
+ {async_process}:bull:main_service:wait

上記のハッシュ化をNestJs/Bullで行う場合は、BullModule.registerQueueメソッドの引数でprefixを指定することで、キーのハッシュ化を行います。こちらも'{}'で囲ってください。

queue.module.ts
# ...省略
@Module({
  imports: [
    BullModule.registerQueue({
      name: 'async_process',
+     prefix: '{async_process}', // キーのハッシュ化
    }),
  ],
  providers: [QueueResolver, QueueProcessor],
  exports: [QueueResolver],
})
# ...省略

この状態で改めてMutationを実行すると、先程のエラーは解消され、キーバリューの格納からプロセッサーによるジョブの実行まで正常に動作することが確認できました!

動作確認
completed create_excel_file🎉 { userId: 1 }

まとめ

いかがでしたでしょうか?
今回はRedisのクラスター構成とNestJS/Bullを利用している際に遭遇したエラーを解決する方法を紹介しました。初歩的なレベルかもしれませんが、実際にスロットの割り当ての挙動などを確認するいい機会となりました。

最後に

弊社では、NestJSフレームワークを使った新規機能追加等にも取り組んでいます。
カジュアルな面談でも構いませんので、ご興味のある方のご応募をお待ちしております!
https://spacemarket.co.jp/recruit/engineer/

スペースマーケット Engineer Blog

Discussion