[Go] groupcache

๊ทธ๋ฃน์บ์‹œ๋Š” memcached ๊ฐ™์€ ์ธ๋ฉ”๋ชจ๋ฆฌ ๋Œ€์ฒด์šฉ์œผ๋กœ ๋งŒ๋“ค์–ด์ง„ ๋ถ„์‚ฐ ์บ์‹ฑ์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค. memcached ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ๋งŒ๋“ค์—ˆ๋‹ค.
ํ˜„์žฌ ๊ตฌ๊ธ€ ๋‚ด๋ถ€์—์„œ ๋‹ค์šด๋กœ๋“œ ์„œ๋ฒ„ ๋“ฑ์— ๋งŽ์ด ์‚ฌ์šฉ๋œ๋‹ค๊ณ  ํ•œ๋‹ค.

memcached ๊ฐ™์€ ์ธ๋ฉ”๋ชจ๋ฆฌ DB๋“ค๊ณผ์˜ ๊ฐ€์žฅ ํฐ ์ฐจ์ด์ ์€, DB๋ฅผ ๋ณ„๋„๋กœ ๋„์šฐ๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ in-program์œผ๋กœ Go ์ฝ”๋“œ ๋‚ด์—์„œ๋งŒ ๋™์ž‘ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

์‰ฝ๊ฒŒ ๋งํ•ด์„œ, ์ธ๋ฉ”๋ชจ๋ฆฌ DB๋ฅผ GO ํ”„๋กœ๊ทธ๋žจ ๋‚ด์—์„œ ์ง์ ‘ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด๋‹ค.
๋ง‰ ์›ํด๋ฆญ ํˆฌํด๋ฆญ์œผ๋กœ ์ƒ์„ฑํ•  ์ •๋„๋กœ ํŽธํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ ธ์žˆ๋Š”๊ฑด ์•„๋‹ˆ๊ณ , ์‚ฌ์šฉ์‚ฌ๋ก€์— ๋”ฐ๋ผ์„œ ์ง์ ‘ ๊ตฌํ˜„ํ•ด์„œ ํ•ธ๋“ค๋งํ•ด์•ผ ํ•˜๋Š” ๋ถ€๋ถ„์ด ๋งŽ๋‹ค. Memcache ๋Œ€์ฒด์šฉ์ด๋ผ๊ณ ๋Š” ํ•˜์ง€๋งŒ 1:1 ๋Œ€์‘์ด ๋˜๋Š” ๊ตฌ์กฐ๋„ ์•„๋‹ˆ๋‹ค. ๋งŽ์ด ๋‹ค๋ฅด๋‹ค.

๊ทธ๋ž˜์„œ ๋งŽ์ด ์•ˆ์“ฐ๋Š” ๊ฒƒ ๊ฐ™๋‹ค.




๊ฐ„๋‹จํ•œ ์˜ˆ์ œ

ํ•œ๋ฒˆ ์‹ฑ๊ธ€๋…ธ๋“œ์—์„œ ๊ทธ๋ฃน์บ์‹œ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•ด๋ณด๊ฒ ๋‹ค.
์ง„์งœ ๊ธฐ๋ณธ์ ์ธ ๋กœ์ง๋งŒ ์žˆ๋‹ค.

์ „์—ญ๋ณ€์ˆ˜๋กœ ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋‘๊ณ 

๊ทธ๋ฃน์„ ์ƒ์„ฑํ•จ๊ณผ ๋™์‹œ์—, Get์—์„œ ๋ฐฉ๊ธˆ ์ € ์ „์—ญ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋„๋ก ํ–ˆ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด Group ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค.

package main

import (
	"context"
	"errors"
	"log"

	"github.com/golang/groupcache"
)

var Store = map[string][]byte{
	"red":   []byte("#FF0000"),
	"green": []byte("#00FF00"),
	"blue":  []byte("#0000FF"),
}

var Group = groupcache.NewGroup("foobar", 64<<20, groupcache.GetterFunc(
	func(ctx context.Context, key string, dest groupcache.Sink) error {
		log.Println("looking up", key)
		v, ok := Store[key]
		if !ok {
			return errors.New("color not found")
		}
		dest.SetBytes(v)
		return nil
	},
))

func main() {
	var b []byte

	err := Group.Get(nil, "red", groupcache.AllocatingByteSliceSink(&b))
	if err != nil {
		log.Fatal(err)
		return
	}

	log.Println("red", string(b))
}

์ž˜ ๋™์ž‘์€ ํ•˜์ง€๋งŒ ์„œ๋ฒ„๋„ ์—†๊ณ , Set๋„ ์—†๊ณ , ๋ถ„์‚ฐ ๊ตฌ์„ฑ๋„ ์•ˆ๋˜์–ด์žˆ์–ด์„œ ๋ถˆ์™„์ „ํ•œ ๊ตฌ์„ฑ์ด๋‹ค.

์ด์ œ ํ™•์žฅํ•ด๋ณด์ž.




HTTP ์„œ๋ฒ„ ๊ตฌ์„ฑ (Read Only)

์ผ๋‹จ ๋ฐ์ดํ„ฐ ๋ช‡๊ฐœ๋งŒ ์žˆ๊ณ , ๋‹จ์ˆœ ์กฐํšŒ๋งŒ ๊ฐ€๋Šฅํ•œ ๋‹จ์ผ ์‹ฑ๊ธ€ ์บ์‹œ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด๋ณด๊ฒ ๋‹ค.
์‹ค์ œ๋กœ ์ด๋Ÿฐ ํ˜•ํƒœ๋กœ ์“ธ ์ผ์€ ์—†์„ ๊ฒƒ์ด๋‹ค.

์„œ๋ฒ„๋ฅผ ์ ์ ˆํžˆ ์ด๋Ÿฐ ํ˜•ํƒœ๋กœ ๋งŒ๋“ค์–ด์ค€๋‹ค.
GET์œผ๋กœ ๋“ค์–ด์˜ค๋ฉด ๊ทธ๋ฃน์บ์‹œ์—์„œ ์บ์‹œ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ„๋‹จํ•œ ๋กœ์ง๋งŒ์„ ํฌํ•จํ•œ๋‹ค.


๊ทธ๋ ‡๊ฒŒ ์‹คํ–‰ํ•˜๋ฉด


์ž˜ ๋™์ž‘ํ•  ๊ฒƒ์ด๋‹ค.

์•„๋ž˜๋Š” ์ „์ฒด ์ฝ”๋“œ๋‹ค.

package main

import (
	"errors"
	"fmt"
	"log"
	"net/http"

	"github.com/golang/groupcache"
)

var Store = map[string][]byte{
	"red":   []byte("#FF0000"),
	"green": []byte("#00FF00"),
	"blue":  []byte("#0000FF"),
}

var Group = groupcache.NewGroup("foobar", 64<<20, groupcache.GetterFunc(
	func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
		log.Println("looking up", key)
		v, ok := Store[key]
		if !ok {
			return errors.New("color not found")
		}
		dest.SetBytes(v)
		return nil
	},
))

func main() {
	myPort := ":8888"

	me := fmt.Sprintf("http://localhost%s", myPort)
	peers := []string{"http://localhost:8888"}

	http.HandleFunc("/color", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			color := r.URL.Query().Get("key")
			var b []byte
			err := Group.Get(nil, color, groupcache.AllocatingByteSliceSink(&b))
			if err != nil {
				http.Error(w, err.Error(), http.StatusNotFound)
				return
			}
			w.Write(b)
			w.Write([]byte{'\n'})
		case http.MethodPost:
		}
	})
	pool := groupcache.NewHTTPPool(me)
	pool.Set(peers...)

	fmt.Println("Groupcache server is running on", myPort)
	http.ListenAndServe(myPort, nil)
}

์—ฌ๊ธฐ์„œ๋Š” ๋ฉ”๋ชจ๋ฆฌ์— ๊ฐ’์„ ์ €์žฅํ•˜๊ณ  ๊ทธ๊ฑธ ์บ์‹œ๋˜๋„๋ก ํ–ˆ์ง€๋งŒ, File Read๋‚˜ Network I/O๋กœ ๊ฐ€์ ธ์˜ค๊ฒŒ ๊ตฌ์„ฑํ•ด๋„ GroupCache ์ž์ฒด์ ์œผ๋กœ ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ์— ๋„ฃ๋Š”๋‹ค.




HTTP ์„œ๋ฒ„ ๊ตฌ์„ฑ (์“ฐ๊ธฐ ๊ฐ€๋Šฅ)

์ด๋ฒˆ์—๋Š” ์“ฐ๊ธฐ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ๋„ ๋งŒ๋“ค์–ด๋ณด์ž.

๊ทผ๋ฐ ์—ฌ๊ธฐ์„œ๋Š” ๊ทธ๋ƒฅ ์“ฐ๊ธฐ ์ž‘์—…์„ ํ•  ์ˆ˜ ์—†๋Š” ๊ฒƒ์ด, ๋‹น์—ฐํžˆ http ์š”์ฒญ์— ์˜ํ•œ ํ”„๋กœ์„ธ์Šค๋Š” ๋ณ‘๋ ฌ์ ์œผ๋กœ ์‹คํ–‰๋  ์ˆ˜ ์žˆ๋‹ค.
๊ทธ๋ƒฅ ๋ฌด์ž‘์ • ์“ฐ๋ฉด ๋™์‹œ์„ฑ write๋กœ ํŒจ๋‹‰ ๋Œ€์ž”์น˜๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋‹ค.

์•ˆ์ „ํ•œ ์“ฐ๊ธฐ๋ฅผ ์œ„ํ•ด์„œ Lock์„ ํ•˜๋‚˜ ๋‘ฌ์•ผ ํ•œ๋‹ค.
์บ์‹œ ํŠน์„ฑ์ƒ ์“ฐ๊ธฐ๋ณด๋‹ค๋Š” ์ฝ๊ธฐ๊ฐ€ ๋งŽ๊ธฐ ๋•Œ๋ฌธ์— RWMutex๋กœ ๋’€๋‹ค.


์ฝ์„ ๋•Œ๋„ ๋ฝ์„ ๊ฑธ์–ด์ฃผ๊ณ 


์“ฐ๊ธฐ ๋™์ž‘์„ ๊ตฌํ˜„ํ•ด์„œ, ๊ฑฐ๊ธฐ์—๋„ ๋ฝ์„ ๊ฑธ์–ด์ค€๋‹ค.

์•„๋ž˜๋Š” ์ „์ฒด ์ฝ”๋“œ๋‹ค.

package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"sync"

	"github.com/golang/groupcache"
)

var Store = map[string][]byte{
	"red":   []byte("#FF0000"),
	"green": []byte("#00FF00"),
	"blue":  []byte("#0000FF"),
}

var StoreLock sync.RWMutex

var Group = groupcache.NewGroup("foobar", 64<<20, groupcache.GetterFunc(
	func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
		log.Println("looking up", key)
		StoreLock.RLock()
		v, ok := Store[key]
		StoreLock.RUnlock()
		if !ok {
			return errors.New("color not found")
		}
		dest.SetBytes(v)
		return nil
	},
))

func main() {
	myPort := ":8888"

	me := fmt.Sprintf("http://localhost%s", myPort)
	peers := []string{me}

	ctx := context.Background()

	http.HandleFunc("/color", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			color := r.URL.Query().Get("key")
			var b []byte

			err := Group.Get(ctx, color, groupcache.AllocatingByteSliceSink(&b))
			if err != nil {
				http.Error(w, err.Error(), http.StatusNotFound)
				return
			}
			w.Write(b)
			w.Write([]byte{'\n'})
		case http.MethodPost:
			bytedata, err := io.ReadAll(r.Body)

			if err != nil {
				w.Write([]byte("Invalid data"))
				return
			}

			type KV struct {
				Key   string `json:"key"`
				Value string `json:"value"`
			}

			var kv KV
			err = json.Unmarshal(bytedata, &kv)
			if err != nil {
				w.Write([]byte("Invalid JSON"))
				return
			}

			StoreLock.Lock()
			Store[kv.Key] = []byte(kv.Value)
			StoreLock.Unlock()

			w.Write([]byte("OK"))
		}
	})
	pool := groupcache.NewHTTPPool(me)
	pool.Set(peers...)

	fmt.Println("Groupcache server is running on", myPort)
	http.ListenAndServe(myPort, nil)
}

์ด๋Œ€๋กœ ์‹คํ–‰ํ•ด๋ณด๋ฉด

์ฒ˜์Œ์—๋Š” black์ด ์—†๋‹ค.


POST๋กœ ์ถ”๊ฐ€ํ•˜๋ฉด


์ด์ œ๋Š” ์กฐํšŒ๋œ๋‹ค.

์ด๊ฒŒ ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ถ„์‚ฐ ๋…ธ๋“œ์—์„œ๋„ ๋™์ž‘ํ•˜๋„๋ก ํ•˜๋ฉด ๋œ๋‹ค.




๋ถ„์‚ฐ ๋…ธ๋“œ ๊ตฌ์„ฑ

์‚ฌ์šฉ๋ฒ•์ด ์–ด๋ ต์ง€๋Š” ์•Š๋‹ค. ์ผ๋‹จ ๊ฐ๊ฐ์˜ ๊ณ ์œ ํ•œ IP๋ฅผ ๊ฐ€์ง„ ๋จธ์‹ ์ด 2๊ฐœ ์ด์ƒ ์žˆ์–ด์•ผ ํ•œ๋‹ค. ํฌํŠธ๋งŒ ๋‹ค๋ฅด๊ฒŒ ํ•œ๋‹ค๋˜๊ฐ€ ํ•˜๋Š” ํ˜•ํƒœ๋กœ๋Š” ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค.

0.8 IP๋ฅผ ๊ฐ€์ง„ A ์„œ๋ฒ„์—๋Š” ์ด๋ ‡๊ฒŒ

0.46 IP๋ฅผ ๊ฐ€์ง„ B ์„œ๋ฒ„์—๋Š” ์ด๋ ‡๊ฒŒ

์ „์ฒด ํ”ผ์–ด ๋ชฉ๋ก์„ ๋“ฑ๋กํ•ด์ค€๋‹ค.
์‹ค์‚ฌ์šฉ ํ™˜๊ฒฝ์—์„œ๋Š” ์ด๊ฑธ ์ด๋ ‡๊ฒŒ ์ฝ”๋“œ์— ๋‹ค ๋ฐ•๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ํ”ผ์–ด ๋“ฑ๋ก API๋ฅผ ๋งŒ๋“ค์–ด์„œ ์“ฐ๊ฒŒ ๋  ๊ฒƒ์ด๋‹ค.

์ด๊ฒƒ๋งŒ ํ•˜๋ฉด ๋œ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ๊ฐ๊ฐ์˜ ์„œ๋ฒ„๋ฅผ ๋„์šด๋‹ค.

์ด์ œ ํ•œ๋ฒˆ ๋Œ๋ ค๋ณด์ž.

์ฒ˜์Œ์—๋Š” black์ด ์—†๋‹ค.


B ์„œ๋ฒ„์— black์„ ์ง‘์–ด๋„ฃ์œผ๋ฉด


A ์„œ๋ฒ„์—์„œ๋„, B ์„œ๋ฒ„์—์„œ๋„ ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง„๋‹ค.
๋Œ€๋žต์ ์ธ ์‚ฌ์šฉ๋ฒ•์€ ๋Œ€์ถฉ ์ด์ •๋„๋‹ค.




์›๋ฆฌ

์‚ฌ์‹ค ์ด๊ฒŒ ๊ธฐ๋ณธ์ ์ธ ์›๋ฆฌ๋Š” ๋ณ„๊ฒŒ ์—†๋‹ค.

๊ทธ๋ƒฅ ์ž์‹ ์—๊ฒŒ ์—†๋Š” ๊ฐ’์ด ๋“ค์–ด์˜จ๋‹ค๋ฉด ๋‚ด๋ถ€์ ์œผ๋กœ ๋‹ค๋ฅธ ํ”ผ์–ด ์„œ๋ฒ„์— ์š”์ฒญํ•ด์„œ ๊ฐ’์„ ๊ฐ€์ ธ์˜จ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ๊ฐ’์„ ์ž์‹ ์˜ ์ธ๋ฉ”๋ชจ๋ฆฌ์—๋„ ์บ์‹ฑ์„ ํ•œ๋‹ค.

๊ต‰์žฅํžˆ ๋‹จ์ˆœํ•œ ๊ตฌ์กฐ๋‹ค.




๋‹จ์ 

๋‹จ์ˆœํ•˜๊ณ  ๋น ๋ฅด๋‚˜, ๋‹จ์ ๋„ ๋งŽ๋‹ค.

์ผ๋‹จ ์ € ์บ์‹œ ๋™์ž‘ ์ž์ฒด๋Š” "์ถ”๊ฐ€"๋งŒ ๊ฐ€๋Šฅํ•˜๋‹ค. ์ˆ˜์ •์ด๋‚˜ ์‚ญ์ œ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.
๋งŒ๋ฃŒ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋„ ์–ด๋ ต๋‹ค.

ํ”ผ์–ด, ํด๋Ÿฌ์Šคํ„ฐ ๊ด€๋ฆฌ๋ฅผ ์ง์ ‘ ๋‹ค ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

๊ฐ€์šฉ์„ฑ๋„ ๋†’๋‹ค๊ณ  ํ•˜๊ธฐ๋Š” ์–ด๋ ต๋‹ค.
์›์ฒœ ๋ฐ์ดํ„ฐ ์ž์ฒด๋Š” ๊ฒฐ๊ตญ ์›๋ณธ ์„œ๋ฒ„๋งŒ ๋“ค๊ณ  ์žˆ๊ณ , ๋‹ค๋ฅธ ์„œ๋ฒ„๋“ค์€ ๊ทธ ๋ฉ”๋ชจ๋ฆฌ ์นดํ”ผ๋งŒ์„ ๋“ค๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์›๋ณธ ์„œ๋ฒ„๊ฐ€ ์ฃฝ๊ณ  ๋‚œ ๋’ค์—๋Š” ๊ทธ๊ฑธ ๊ฐ€์ ธ์˜ฌ ์ˆ˜๊ฐ€ ์—†๋‹ค.

Memcache ๋Œ€์ฒด์šฉ์ด๋ผ๊ณ  ๊ฑฐ์ฐฝํ•˜๊ฒŒ ์†Œ๊ฐœ๋Š” ํ–ˆ์ง€๋งŒ ์‹ค์ œ๋กœ ๋Œ€์ฒด๋Š” ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.
๋‹จ์ˆœํžˆ ์บ์‹œ ์ถ”๊ฐ€๋งŒ ํ•ด๋„ ๋˜๋ฉฐ, ๊ทธ๊ฐœ ๋งค์šฐ ์˜ค๋žซ๋™์•ˆ ๋งŒ๋ฃŒ ์—†์ด ์กด์žฌํ•ด๋„ ๋˜๋Š” ํ•œ์ •๋œ ์˜์—ญ์—์„œ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.

๊ทผ๋ฐ ๋Œ€๋ถ€๋ถ„์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ๋Š” ์บ์‹œ๊ฐ€ ๋งŒ๋ฃŒ๋˜๊ฑฐ๋‚˜ ๊ฐฑ์‹ ๋˜๋Š” ๋™์ž‘์ด ํ•„์š”ํ•˜๋‹ค. ๋น›์ข‹์€ ๊ฐœ์‚ด๊ตฌ๋‹ค.



์ฐธ์กฐ
https://github.com/golang/groupcache
https://juejin.cn/post/6844903442855493646
https://sconedocs.github.io/groupcacheUseCase/
https://capotej.com/blog/2013/07/28/playing-with-groupcache/
https://www.mailgun.com/blog/it-and-engineering/golangs-superior-cache-solution-memcached-redis/
https://gist.github.com/fiorix/816117cfc7573319b72d
https://github.com/capotej/groupcache-db-experiment