[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