他のサーバーにアクセスを転送
二台構成にして、initializeリクエストを別のサーバーに送りたい時
func initialize(c echo.Context) error {
target, _ := url.Parse("http://192.168.11.2:8080") // 転送先のアプリのURL
proxy := httputil.NewSingleHostReverseProxy(target)
req := c.Request().Clone(c.Request().Context())
req.URL.Path = "/api/initialize2" // 転送先のendpoint
proxy.ServeHTTP(c.Response(), req)
Memcache
関数と変数定義
var mc *memcache.Client
func getMemcacheKey(keyType string, id interface{}) string {
return fmt.Sprintf("%s.%v", keyType, id)
}
func fetchIfMemcacheHit[T any](
cacheKey string,
onCacheNotHit func() (*T, error),
) (*T, error) {
item, err := mc.Get(cacheKey)
if errors.Is(err, memcache.ErrCacheMiss) {
// cache not hit
result, err := onCacheNotHit()
if err != nil {
return nil, err
}
val, err := msgpack.Marshal(result)
if err != nil {
return nil, err
}
if err := mc.Set(&memcache.Item{Value: val, Key: cacheKey}); err != nil {
return nil, err
}
return result, nil
} else if err != nil {
return nil, err
} else {
// cache hit
var result T
if err := msgpack.Unmarshal(item.Value, &result); err != nil {
return nil, err
}
return &result, nil
}
}
func fetchIfMemcacheHitBulk[T any](
cacheKeys []string,
onCacheNotHit func(idx int) (*T, error),
) ([]T, error) {
items, err := mc.GetMulti(cacheKeys)
if err != nil {
return nil, err
}
res := make([]T, len(cacheKeys))
for i, key := range cacheKeys {
item := items[key]
if item == nil {
// cache not hit
item, err := onCacheNotHit(i)
if err != nil {
return nil, err
}
val, err := msgpack.Marshal(item)
if err != nil {
return nil, err
}
if err := mc.Set(&memcache.Item{Value: val, Key: key}); err != nil {
return nil, err
}
res[i] = *item
continue
}
err := msgpack.Unmarshal(item.Value, &res[i])
if err != nil {
return nil, err
}
}
return res, nil
}
初期化
main関数に以下を追記する
memdAddr := os.Getenv("MEMCACHED_ADDRESS")
if memdAddr == "" {
memdAddr = "localhost:11211"
}
mc = memcache.New(memdAddr)
initialize関数に以下を追加する
mc.DeleteAll()
使用例
(はじめに) アップデートのときにキャッシュを消すのを忘れずに
cacheKey := getMemcacheKey("reaction.count", userID)
mc.Delete(cacheKey)
sliceの場合
cacheKey := getMemcacheKey("livestream_tags", livestreamModel.ID)
res, err := fetchIfMemcacheHit(cacheKey, func() (*[]LivestreamTagModel, error) {
var res []LivestreamTagModel
if err := tx.SelectContext(ctx, &res, "SELECT * FROM livestream_tags WHERE livestream_id = ?", livestreamModel.ID); err != nil {
return nil, err
}
return &res, nil
})
if err != nil {
return nil, err
}
構造体の場合
cacheKey := getMemcacheKey("user", userId)
res, err := fetchIfMemcacheHit(cacheKey, func() (*UserModel, error) {
var res UserModel
if err := tx.GetContext(ctx, &res, "SELECT * FROM users WHERE id = ?", userId); err != nil {
return nil, err
}
return &res, nil
})
if err != nil {
return nil, err
}
primitive型の場合
cacheKey := getMemcacheKey("reaction.count", userID)
res, err := fetchIfMemcacheHit(cacheKey, func() (*int64, error) {
var res int64
if err := tx.GetContext(ctx, &res, "SELECT COUNT(*) FROM reactions where user_id = ?", userID); err != nil {
return nil, err
}
return &res, nil
})
if err != nil {
return nil, err
}
排他制御
mutex.go
などのファイル名で保存する
package main
import "sync"
type KeyedMutex struct {
mu sync.Mutex
locks map[string]*sync.RWMutex
}
var keyedMutex *KeyedMutex = NewKeyedMutex()
func NewKeyedMutex() *KeyedMutex {
return &KeyedMutex{
locks: make(map[string]*sync.RWMutex),
}
}
func (km *KeyedMutex) getLock(key string) *sync.RWMutex {
km.mu.Lock()
defer km.mu.Unlock()
if lock, exists := km.locks[key]; exists {
return lock
}
km.locks[key] = &sync.RWMutex{}
return km.locks[key]
}
func (km *KeyedMutex) RLock(key string) {
lock := km.getLock(key)
lock.RLock()
}
func (km *KeyedMutex) RUnlock(key string) {
lock := km.getLock(key)
lock.RUnlock()
}
func (km *KeyedMutex) RWLock(key string) {
lock := km.getLock(key)
lock.Lock()
}
func (km *KeyedMutex) RWUnlock(key string) {
lock := km.getLock(key)
lock.Unlock()
}
使い方
以下のようにロックができる
keyedMutex.RLock("sample key")
defer keyedMutex.RUnlock("sample key")
ファイルの保存
先に"排他制御"の準備を終わらせる。 file.goなどの名前で保存する。
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
)
var UPLOADED_FILE_DIR = "/home/isucon/uploaded-files"
func saveFile(content []byte, fileName string) error {
filePath := filepath.Join(UPLOADED_FILE_DIR, fileName)
keyedMutex.RWLock(filePath)
defer keyedMutex.RWUnlock(filePath)
// remove the old file if it exists
if err := os.Remove(filePath); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to delete the old file: %w", err)
}
}
if err := os.WriteFile(filePath, content, os.ModePerm); err != nil {
return fmt.Errorf("failed to save the file: %w", err)
}
return nil
}
// ファイルが存在しない場合は `if errors.Is(err, os.ErrNotExist) {` で判定する
func readFile(fileName string) ([]byte, error) {
filePath := filepath.Join(UPLOADED_FILE_DIR, fileName)
keyedMutex.RLock(filePath)
defer keyedMutex.RUnlock(filePath)
return os.ReadFile(filePath)
}
initializeで削除するのを忘れずに
if err := os.RemoveAll(UPLOADED_FILE_DIR); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to remove the directory: "+err.Error())
}
if err := os.Mkdir(UPLOADED_FILE_DIR, os.ModeDir|os.ModePerm); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to create the directory: "+err.Error())
}
idが10000を超過したののみ消したい場合は
func removeExceptInitialImage() error {
files, err := os.ReadDir(UPLOADED_FILE_DIR)
if err != nil {
return err
}
for _, file := range files {
if file.IsDir() {
continue
}
filename := file.Name()
userId, err := strconv.Atoi(strings.Split(filename, ".")[0])
if err != nil {
return err
}
if userId > 10000 {
err := os.Remove(filepath.Join(UPLOADED_FILE_DIR, filename))
if err != nil {
return err
}
}
}
return nil
}
DBの画像データをストレージに保存する例
main関数の一番下でextractImgFromDB()
を呼び出す
func mimeToExtension(mime string) string {
if mime == "image/jpeg" {
return ".jpg"
}
if mime == "image/png" {
return ".png"
}
if mime == "image/gif" {
return ".gif"
}
return ".unknown"
}
func extractImgFromDB() {
time.Sleep(10 * time.Second)
exec.Command("mkdir", "-p", UPLOADED_FILE_DIR).Run()
rows, err := db.Query("SELECT id, imgdata FROM posts")
if err != nil {
log.Fatal(err)
return
}
defer rows.Close()
for rows.Next() {
var id int
var img []byte
err := rows.Scan(&id, &img)
if err != nil {
log.Fatal(err)
return
}
ext := mimeToExtension(http.DetectContentType(img))
filename := fmt.Sprintf("%d%s", id, ext)
err = saveFile(img, filename)
if err != nil {
log.Fatal(err)
return
}
fmt.Println(filename, "saved")
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
}
mysqlの権限設定
外部のネットワークからアクセスできるユーザーを新規作成する
DROP USER IF EXISTS `isucon`@`%`;
CREATE USER isucon IDENTIFIED BY 'isucon';
GRANT ALL PRIVILEGES ON isupipe.* TO 'isucon'@'%';