Garnet

Microsoft研究開発の次世代キャッシュストア。.NET C#で構築され、Redis互換プロトコルRESPをサポート。極低レイテンシ(99.9%が300μs未満)を実現。

cache-serverMicrosoftRedis-compatible.NETTsavoriteRESPclustering

Garnet

GarnetはMicrosoft Researchが開発した高性能なRedis互換分散キーバリューストアです。Tsavoriteストレージエンジンをベースとし、Redis互換性を保ちながら優れたスループットと低レイテンシを実現する次世代キャッシュストアとして、2024年にオープンソースで公開されました。

概要

Garnetは2024年にMicrosoft Researchが発表した革新的なキャッシュサーバーで、既存のRedisクライアントとの完全な互換性を保ちながら、パフォーマンスとスケーラビリティにおいて大幅な改善を実現しています。最新の.NET技術をベースに構築されており、クロスプラットフォーム対応、拡張性、モダンなアーキテクチャを特徴としています。Azure Resource Manager等のMicrosoft内部サービスで既に本番運用されており、Redis、KeyDB、DragonflyといったRedis系キャッシュサーバーと比較して圧倒的な性能向上を示しています。

特徴

主要機能

  • Redis互換性: RESP(Redis Serialization Protocol)を採用し、既存のRedisクライアントとの完全互換性
  • 高性能: Redis比で大幅なスループット向上と99.9パーセンタイルで300マイクロ秒以下の低レイテンシ
  • Tsavoriteストレージエンジン: Microsoftが開発した高性能ストレージエンジンによる最適化
  • クラスタリング対応: シャーディング、レプリケーション、動的キーマイグレーション機能
  • .NET統合: 最新の.NET技術による効率的なメモリ管理とガベージコレクション最適化
  • 企業レベル機能: 永続化、リカバリ、自動フェイルオーバー、認証システム

アーキテクチャ特性

Garnetの技術的優位性:

  • 階層化アーキテクチャ: Server、Protocol、API、Storage、Clusterの5層構造による明確な責任分離
  • デュアルストレージシステム: Main Store(単純なKV操作)とObject Store(複雑なデータ構造)の最適化
  • AOF永続化: Append-Only Fileによる高速かつ信頼性の高いデータ永続化
  • 効率的クラスタリング: Redisハッシュスロットによるデータシャーディングと動的リバランシング

パフォーマンス特性

  • GETコマンドでDragonflyの10倍以上のスループット
  • Redis、KeyDBを大幅に上回る並行接続時のスケーラビリティ
  • 99.9パーセンタイルで300マイクロ秒以下の低レイテンシ
  • 小バッチサイズでの効率的な処理による大規模アプリケーションでのコスト削減

メリット・デメリット

メリット

  • 卓越したパフォーマンス: ベンチマークでRedis系サーバーを圧倒する性能指標
  • Redis互換性: 既存アプリケーションの移行が容易で学習コストが低い
  • モダンアーキテクチャ: 最新の.NET技術による効率的なリソース利用
  • エンタープライズ対応: クラスタリング、レプリケーション、フェイルオーバー機能
  • オープンソース: MITライセンスによる自由な利用と改良
  • Microsoft製: Azure等での実証済みの信頼性と継続的な開発サポート

デメリット

  • コマンド制限: Redisコマンドの約3分の1のみサポート(完全互換ではない)
  • Luaスクリプト非対応: 多くの.NETキャッシュ/セッション管理ライブラリで使用されるLuaが未サポート
  • 新しい技術: 2024年公開のため長期運用実績やエコシステムが限定的
  • .NET依存: .NET環境での動作に最適化されており、他の環境では恩恵が限定的
  • ドキュメント不足: 新しいプロジェクトのため情報やコミュニティサポートが限定的
  • 学習コストあり: Redisとの微細な差異を理解するための追加学習が必要

参考ページ

書き方の例

Garnetサーバーの起動

# Garnetバイナリのダウンロードと実行
# GitHubからreleasesをダウンロード
wget https://github.com/microsoft/garnet/releases/latest/download/garnet-linux-x64.zip
unzip garnet-linux-x64.zip

# 基本的な起動
./GarnetServer

# カスタム設定での起動
./GarnetServer --port 6379 --memory 4g --logdir ./logs --aof

# クラスタモードでの起動
./GarnetServer --cluster --port 7000 --memory 2g

# 設定ファイルを使用した起動
./GarnetServer --config-file garnet.conf

Dockerでの実行

# Dockerコンテナでの実行
docker run --rm -p 6379:6379 microsoft/garnet

# カスタム設定でのDocker実行
docker run --rm -p 6379:6379 \
  -v $(pwd)/data:/data \
  microsoft/garnet \
  --memory 2g --aof --logdir /data

# クラスタ構成でのDocker実行
docker run --rm -p 7000:7000 \
  --name garnet-node1 \
  microsoft/garnet \
  --cluster --port 7000 --announce-ip 127.0.0.1

docker run --rm -p 7001:7001 \
  --name garnet-node2 \
  microsoft/garnet \
  --cluster --port 7001 --announce-ip 127.0.0.1

# クラスタの結合
docker exec garnet-node2 redis-cli -p 7001 cluster meet 127.0.0.1 7000

C#/.NETクライアントでの基本操作

using StackExchange.Redis;
using System;
using System.Threading.Tasks;

public class GarnetClientExample
{
    private static IDatabase database;
    
    public static async Task Main(string[] args)
    {
        // Garnetサーバーへの接続(Redis互換)
        var redis = ConnectionMultiplexer.Connect("localhost:6379");
        database = redis.GetDatabase();
        
        Console.WriteLine("=== Garnet Basic Operations ===");
        
        // 文字列操作
        await database.StringSetAsync("user:1000:name", "John Doe");
        await database.StringSetAsync("user:1000:email", "[email protected]");
        await database.StringSetAsync("user:1000:age", "30");
        
        string name = await database.StringGetAsync("user:1000:name");
        string email = await database.StringGetAsync("user:1000:email");
        int age = await database.StringGetAsync("user:1000:age");
        
        Console.WriteLine($"User: {name}, Email: {email}, Age: {age}");
        
        // 有効期限付きキー
        await database.StringSetAsync("session:abc123", "active", TimeSpan.FromMinutes(30));
        var ttl = await database.KeyTimeToLiveAsync("session:abc123");
        Console.WriteLine($"Session TTL: {ttl?.TotalSeconds} seconds");
        
        // ハッシュ操作
        await database.HashSetAsync("product:2000", new HashEntry[]
        {
            new("name", "Gaming Laptop"),
            new("price", "1299.99"),
            new("category", "Electronics"),
            new("stock", "50")
        });
        
        var productName = await database.HashGetAsync("product:2000", "name");
        var productPrice = await database.HashGetAsync("product:2000", "price");
        Console.WriteLine($"Product: {productName}, Price: ${productPrice}");
        
        // リスト操作
        await database.ListLeftPushAsync("task_queue", "process_payment");
        await database.ListLeftPushAsync("task_queue", "send_email");
        await database.ListLeftPushAsync("task_queue", "update_inventory");
        
        var queueLength = await database.ListLengthAsync("task_queue");
        Console.WriteLine($"Queue length: {queueLength}");
        
        string task = await database.ListRightPopAsync("task_queue");
        Console.WriteLine($"Processing task: {task}");
        
        // セット操作
        await database.SetAddAsync("user:1000:interests", "technology");
        await database.SetAddAsync("user:1000:interests", "gaming");
        await database.SetAddAsync("user:1000:interests", "programming");
        
        var interests = await database.SetMembersAsync("user:1000:interests");
        Console.WriteLine($"User interests: {string.Join(", ", interests)}");
        
        // 並行アクセステスト
        await PerformanceBenchmark();
        
        redis.Close();
    }
    
    private static async Task PerformanceBenchmark()
    {
        Console.WriteLine("\n=== Performance Benchmark ===");
        
        var tasks = new List<Task>();
        int operations = 10000;
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        
        // 並行書き込みテスト
        for (int i = 0; i < operations; i++)
        {
            int taskId = i;
            tasks.Add(Task.Run(async () =>
            {
                await database.StringSetAsync($"benchmark:write:{taskId}", $"value_{taskId}");
            }));
        }
        
        await Task.WhenAll(tasks);
        stopwatch.Stop();
        
        Console.WriteLine($"Write operations: {operations}");
        Console.WriteLine($"Total time: {stopwatch.ElapsedMilliseconds} ms");
        Console.WriteLine($"Ops/sec: {operations * 1000.0 / stopwatch.ElapsedMilliseconds:F2}");
        
        // 並行読み取りテスト
        tasks.Clear();
        stopwatch.Restart();
        
        for (int i = 0; i < operations; i++)
        {
            int taskId = i;
            tasks.Add(Task.Run(async () =>
            {
                await database.StringGetAsync($"benchmark:write:{taskId}");
            }));
        }
        
        await Task.WhenAll(tasks);
        stopwatch.Stop();
        
        Console.WriteLine($"Read operations: {operations}");
        Console.WriteLine($"Total time: {stopwatch.ElapsedMilliseconds} ms");
        Console.WriteLine($"Ops/sec: {operations * 1000.0 / stopwatch.ElapsedMilliseconds:F2}");
    }
}

Python(redis-py)クライアントでの利用

import redis
import time
import threading
from concurrent.futures import ThreadPoolExecutor

def connect_to_garnet():
    """Garnetサーバーへの接続"""
    r = redis.Redis(host='localhost', port=6379, decode_responses=True)
    
    # 接続テスト
    try:
        r.ping()
        print("Connected to Garnet server successfully!")
        return r
    except redis.ConnectionError:
        print("Failed to connect to Garnet server")
        return None

def basic_operations(r):
    """基本的なRedis互換操作のデモ"""
    print("\n=== Basic Operations Demo ===")
    
    # 文字列操作
    r.set('greeting', 'Hello, Garnet!')
    print(f"Greeting: {r.get('greeting')}")
    
    # 数値操作
    r.set('counter', 100)
    r.incr('counter', 5)
    print(f"Counter: {r.get('counter')}")
    
    # リスト操作
    r.lpush('notifications', 'Welcome!', 'New message', 'System update')
    notifications = r.lrange('notifications', 0, -1)
    print(f"Notifications: {notifications}")
    
    # ハッシュ操作
    user_data = {
        'id': '12345',
        'name': 'Alice Johnson',
        'role': 'developer',
        'status': 'active'
    }
    r.hset('user:12345', mapping=user_data)
    
    user_name = r.hget('user:12345', 'name')
    user_role = r.hget('user:12345', 'role')
    print(f"User: {user_name} ({user_role})")
    
    # セット操作
    r.sadd('active_users', 'user1', 'user2', 'user3', 'user4')
    r.sadd('premium_users', 'user2', 'user4', 'user5')
    
    active_count = r.scard('active_users')
    premium_active = r.sinter('active_users', 'premium_users')
    print(f"Active users: {active_count}, Premium active: {list(premium_active)}")

def performance_test(r):
    """Garnetの性能テスト"""
    print("\n=== Performance Test ===")
    
    # 順次実行テスト
    operations = 10000
    start_time = time.time()
    
    for i in range(operations):
        r.set(f'perf:seq:{i}', f'value_{i}')
    
    sequential_time = time.time() - start_time
    print(f"Sequential SET operations: {operations}")
    print(f"Time: {sequential_time:.3f} seconds")
    print(f"Ops/sec: {operations / sequential_time:.2f}")
    
    # 並行実行テスト
    def concurrent_operations(thread_id, ops_per_thread):
        local_r = redis.Redis(host='localhost', port=6379, decode_responses=True)
        for i in range(ops_per_thread):
            local_r.set(f'perf:conc:{thread_id}:{i}', f'value_{thread_id}_{i}')
    
    threads = 10
    ops_per_thread = 1000
    
    start_time = time.time()
    with ThreadPoolExecutor(max_workers=threads) as executor:
        futures = [executor.submit(concurrent_operations, i, ops_per_thread) 
                  for i in range(threads)]
        for future in futures:
            future.result()
    
    concurrent_time = time.time() - start_time
    total_ops = threads * ops_per_thread
    
    print(f"Concurrent SET operations: {total_ops}")
    print(f"Threads: {threads}")
    print(f"Time: {concurrent_time:.3f} seconds")
    print(f"Ops/sec: {total_ops / concurrent_time:.2f}")

def pipeline_operations(r):
    """パイプライン操作による効率化"""
    print("\n=== Pipeline Operations ===")
    
    # 通常の操作
    start_time = time.time()
    for i in range(1000):
        r.set(f'normal:{i}', f'value_{i}')
    normal_time = time.time() - start_time
    
    # パイプライン操作
    start_time = time.time()
    pipeline = r.pipeline()
    for i in range(1000):
        pipeline.set(f'pipeline:{i}', f'value_{i}')
    pipeline.execute()
    pipeline_time = time.time() - start_time
    
    print(f"Normal operations time: {normal_time:.3f} seconds")
    print(f"Pipeline operations time: {pipeline_time:.3f} seconds")
    print(f"Pipeline speedup: {normal_time / pipeline_time:.2f}x")

def session_management_example(r):
    """セッション管理のサンプル"""
    print("\n=== Session Management Example ===")
    
    session_id = "sess_abc123"
    user_id = "user_789"
    
    # セッションデータの保存(30分の有効期限)
    session_data = {
        'user_id': user_id,
        'login_time': str(int(time.time())),
        'ip_address': '192.168.1.100',
        'user_agent': 'Mozilla/5.0 Browser'
    }
    
    pipeline = r.pipeline()
    pipeline.hset(f'session:{session_id}', mapping=session_data)
    pipeline.expire(f'session:{session_id}', 1800)  # 30分
    pipeline.execute()
    
    # アクティブセッションリストに追加
    r.sadd(f'user:{user_id}:sessions', session_id)
    
    # セッション検証
    if r.exists(f'session:{session_id}'):
        stored_user = r.hget(f'session:{session_id}', 'user_id')
        login_time = r.hget(f'session:{session_id}', 'login_time')
        ttl = r.ttl(f'session:{session_id}')
        
        print(f"Valid session for user {stored_user}")
        print(f"Login time: {login_time}")
        print(f"Expires in: {ttl} seconds")
    
    # セッションクリーンアップ
    r.delete(f'session:{session_id}')
    r.srem(f'user:{user_id}:sessions', session_id)

def main():
    # Garnetサーバーに接続
    r = connect_to_garnet()
    if not r:
        return
    
    try:
        # 各種操作のデモ
        basic_operations(r)
        performance_test(r)
        pipeline_operations(r)
        session_management_example(r)
        
    except Exception as e:
        print(f"Error: {e}")
    finally:
        r.close()

if __name__ == "__main__":
    main()

Node.js (ioredis) クライアントでの利用

const Redis = require('ioredis');

class GarnetClient {
    constructor() {
        this.redis = new Redis({
            host: 'localhost',
            port: 6379,
            retryDelayOnFailover: 100,
            enableReadyCheck: false,
            maxRetriesPerRequest: null,
        });
        
        this.redis.on('connect', () => {
            console.log('Connected to Garnet server');
        });
        
        this.redis.on('error', (err) => {
            console.error('Garnet connection error:', err);
        });
    }
    
    async basicOperations() {
        console.log('\n=== Basic Operations Demo ===');
        
        // 文字列操作
        await this.redis.set('app:version', '2.1.0');
        await this.redis.setex('temp:token', 300, 'abc123xyz'); // 5分の有効期限
        
        const version = await this.redis.get('app:version');
        const token = await this.redis.get('temp:token');
        const ttl = await this.redis.ttl('temp:token');
        
        console.log(`App version: ${version}`);
        console.log(`Temp token: ${token} (expires in ${ttl}s)`);
        
        // ハッシュ操作
        const orderData = {
            id: 'order_12345',
            customer: 'John Smith',
            amount: '299.99',
            status: 'processing',
            created: new Date().toISOString()
        };
        
        await this.redis.hmset('order:12345', orderData);
        const order = await this.redis.hgetall('order:12345');
        console.log('Order data:', order);
        
        // リスト操作 - タスクキュー
        await this.redis.lpush('task:email', 
            JSON.stringify({ type: 'welcome', userId: 'user_123' }),
            JSON.stringify({ type: 'order_confirmation', orderId: 'order_12345' }),
            JSON.stringify({ type: 'newsletter', segment: 'premium' })
        );
        
        const queueLength = await this.redis.llen('task:email');
        console.log(`Email queue length: ${queueLength}`);
        
        // タスク処理のシミュレーション
        const task = await this.redis.rpop('task:email');
        if (task) {
            const taskData = JSON.parse(task);
            console.log('Processing task:', taskData);
        }
        
        // セット操作 - タグ管理
        await this.redis.sadd('post:100:tags', 'javascript', 'redis', 'cache', 'performance');
        await this.redis.sadd('post:101:tags', 'python', 'redis', 'tutorial');
        
        const commonTags = await this.redis.sinter('post:100:tags', 'post:101:tags');
        console.log('Common tags:', commonTags);
    }
    
    async performanceBenchmark() {
        console.log('\n=== Performance Benchmark ===');
        
        const operations = 10000;
        
        // 単一クライアントでの性能テスト
        console.log('Single client benchmark...');
        const startTime = Date.now();
        
        const pipeline = this.redis.pipeline();
        for (let i = 0; i < operations; i++) {
            pipeline.set(`bench:single:${i}`, `value_${i}`);
        }
        await pipeline.exec();
        
        const singleClientTime = Date.now() - startTime;
        console.log(`Single client: ${operations} operations in ${singleClientTime}ms`);
        console.log(`Ops/sec: ${(operations * 1000 / singleClientTime).toFixed(2)}`);
        
        // 複数クライアントでの性能テスト
        console.log('Multi client benchmark...');
        const clients = 10;
        const opsPerClient = Math.floor(operations / clients);
        
        const multiStartTime = Date.now();
        const promises = [];
        
        for (let c = 0; c < clients; c++) {
            const client = new Redis({ host: 'localhost', port: 6379 });
            const promise = (async (clientId) => {
                const clientPipeline = client.pipeline();
                for (let i = 0; i < opsPerClient; i++) {
                    clientPipeline.set(`bench:multi:${clientId}:${i}`, `value_${clientId}_${i}`);
                }
                await clientPipeline.exec();
                client.disconnect();
            })(c);
            
            promises.push(promise);
        }
        
        await Promise.all(promises);
        const multiClientTime = Date.now() - multiStartTime;
        const totalOps = clients * opsPerClient;
        
        console.log(`Multi client: ${totalOps} operations in ${multiClientTime}ms`);
        console.log(`Ops/sec: ${(totalOps * 1000 / multiClientTime).toFixed(2)}`);
        console.log(`Speedup: ${(singleClientTime / multiClientTime).toFixed(2)}x`);
    }
    
    async cachePattern() {
        console.log('\n=== Cache Pattern Demo ===');
        
        // キャッシュアサイドパターン
        const cacheKey = 'user:profile:456';
        
        // 1. キャッシュから確認
        let userData = await this.redis.get(cacheKey);
        
        if (userData) {
            console.log('Cache hit:', JSON.parse(userData));
        } else {
            console.log('Cache miss, fetching from database...');
            
            // 2. データベースから取得(シミュレーション)
            const dbData = {
                id: 456,
                name: 'Sarah Connor',
                email: '[email protected]',
                preferences: {
                    theme: 'dark',
                    notifications: true
                },
                lastLogin: new Date().toISOString()
            };
            
            // 3. キャッシュに保存(1時間の有効期限)
            await this.redis.setex(cacheKey, 3600, JSON.stringify(dbData));
            console.log('Data cached:', dbData);
        }
        
        // Write-through パターン
        const updateData = {
            id: 456,
            name: 'Sarah Connor',
            email: '[email protected]',
            preferences: {
                theme: 'light',  // テーマ変更
                notifications: true
            },
            lastLogin: new Date().toISOString()
        };
        
        // データベース更新(シミュレーション)と同時にキャッシュ更新
        await this.redis.setex(cacheKey, 3600, JSON.stringify(updateData));
        console.log('Cache updated:', updateData);
    }
    
    async leaderboardExample() {
        console.log('\n=== Leaderboard Example ===');
        
        // ゲームスコアリーダーボード
        const scores = [
            ['player1', 1250],
            ['player2', 980],
            ['player3', 1100],
            ['player4', 1350],
            ['player5', 870],
        ];
        
        // スコア追加
        for (const [player, score] of scores) {
            await this.redis.zadd('leaderboard:game1', score, player);
        }
        
        // トップ3の取得
        const top3 = await this.redis.zrevrange('leaderboard:game1', 0, 2, 'WITHSCORES');
        console.log('Top 3 players:');
        for (let i = 0; i < top3.length; i += 2) {
            const rank = (i / 2) + 1;
            console.log(`${rank}. ${top3[i]} - ${top3[i + 1]} points`);
        }
        
        // 特定プレイヤーのランク
        const player1Rank = await this.redis.zrevrank('leaderboard:game1', 'player1');
        const player1Score = await this.redis.zscore('leaderboard:game1', 'player1');
        console.log(`player1 rank: ${player1Rank + 1}, score: ${player1Score}`);
    }
    
    async disconnect() {
        await this.redis.quit();
        console.log('Disconnected from Garnet server');
    }
}

async function main() {
    const client = new GarnetClient();
    
    try {
        await client.basicOperations();
        await client.performanceBenchmark();
        await client.cachePattern();
        await client.leaderboardExample();
    } catch (error) {
        console.error('Error:', error);
    } finally {
        await client.disconnect();
    }
}

// スクリプト実行時にmain関数を呼び出し
if (require.main === module) {
    main().catch(console.error);
}

module.exports = GarnetClient;

クラスター設定例

# garnet.conf - Garnetクラスター設定ファイル
# 基本設定
port 7000
bind 0.0.0.0

# メモリ設定
memory 2g
page-size 4KB
segment-size 1g

# クラスター設定
cluster yes
cluster-announce-ip 127.0.0.1
cluster-announce-port 7000

# 永続化設定
aof yes
log-dir ./logs

# 認証設定
auth-mode password
auth-password your-secure-password

# パフォーマンス調整
index-size 64m
object-store-memory 512m
# クラスター起動スクリプト
#!/bin/bash

# 3ノードクラスターの起動
./GarnetServer --config-file node1.conf --port 7000 --cluster-announce-port 7000 &
./GarnetServer --config-file node2.conf --port 7001 --cluster-announce-port 7001 &
./GarnetServer --config-file node3.conf --port 7002 --cluster-announce-port 7002 &

# ノード起動待機
sleep 5

# クラスターの構成
redis-cli -p 7000 cluster meet 127.0.0.1 7001
redis-cli -p 7000 cluster meet 127.0.0.1 7002

# ハッシュスロットの割り当て
redis-cli -p 7000 cluster addslots {0..5461}
redis-cli -p 7001 cluster addslots {5462..10922}
redis-cli -p 7002 cluster addslots {10923..16383}

echo "Garnet cluster started successfully!"
echo "Node 1: 127.0.0.1:7000"
echo "Node 2: 127.0.0.1:7001"
echo "Node 3: 127.0.0.1:7002"

Garnetは、Redis互換性を保ちながら圧倒的な性能改善を実現する革新的なキャッシュサーバーとして、特に.NET環境での高性能アプリケーション開発において大きな価値を提供します。