cubefs/blockcache/bcache/bcache_store_service.go
leonrayang 9a30b9f884 update: update to CubeFS
Signed-off-by: leonrayang <chl696@sina.com>
2022-08-22 18:16:09 +08:00

408 lines
11 KiB
Go

// Copyright 2022 The CubeFS Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
package bcache
import (
"crypto/md5"
"encoding/hex"
"encoding/xml"
"fmt"
"github.com/cubefs/cubefs/util/stat"
"io"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"sync"
"time"
"github.com/cubefs/cubefs/cmd/common"
"github.com/cubefs/cubefs/util/config"
"github.com/cubefs/cubefs/util/errors"
"github.com/cubefs/cubefs/util/log"
"github.com/gorilla/mux"
_ "net/http/pprof"
)
const (
UnixSocketPath = "/var/lib/kubelet/device-plugins/bcache.socket"
CacheKey = "cachekey"
CheckSum = "md5sum"
OffSet = "offset"
Len = "len"
Volume = "volume"
ContextKeyStatusCode = "status_code"
ContextKeyErrorMessage = "error_message"
ContextKeyRequestID = "ctx_request_id"
//config
CacheDir = "cacheDir"
CacheSize = "cacheSize"
CacheLimit = "cacheLimit"
CacheFree = "cacheFree"
BlockSize = "blockSize"
MaxBlockSize = 8 << 20
)
var (
keyRegexp = regexp.MustCompile("^.+?_(\\d)+_[0-9a-zA-Z]+?")
)
var (
BadDigest = &ErrorCode{ErrorCode: "BadDigest", ErrorMessage: "The Content-MD5 you specified did not match what we received.", StatusCode: http.StatusBadRequest}
KeyTooLongError = &ErrorCode{ErrorCode: "KeyTooLongError", ErrorMessage: "", StatusCode: http.StatusBadRequest}
InvalidKey = &ErrorCode{ErrorCode: "InvalidKey", ErrorMessage: "Cache key is Illegal", StatusCode: http.StatusBadRequest}
EntityTooLarge = &ErrorCode{ErrorCode: "EntityTooLarge", ErrorMessage: "Your proposed upload exceeds the maximum allowed object size.", StatusCode: http.StatusBadRequest}
InvalidArgument = &ErrorCode{ErrorCode: "InvalidArgument", ErrorMessage: "Invalid Argument,cachekey|md5|offsetlen is required.", StatusCode: http.StatusBadRequest}
InvalidNumber = &ErrorCode{ErrorCode: "InvalidNumber", ErrorMessage: "Invalid Argument type,need number .", StatusCode: http.StatusBadRequest}
NoSuchCacheKey = &ErrorCode{ErrorCode: "NoSuchCacheKey", ErrorMessage: "The specified cache key does not exist.", StatusCode: http.StatusNotFound}
ReadStreamError = &ErrorCode{ErrorCode: "ReadStreamError", ErrorMessage: "Read body stream unknown exception.", StatusCode: http.StatusBadRequest}
InternalError = &ErrorCode{ErrorCode: "InternalError", ErrorMessage: "Server interval error.", StatusCode: http.StatusInternalServerError}
)
type ErrorCode struct {
ErrorCode string
ErrorMessage string
StatusCode int
}
type bcacheConfig struct {
CacheDir string
BlockSize uint32
Mode uint32
CacheSize int64
FreeRatio float32
Limit int
}
type bcacheStore struct {
bcache BcacheManager
conf *bcacheConfig
unixListener net.Listener
control common.Control
bufPool sync.Pool
}
func NewServer() *bcacheStore {
return &bcacheStore{}
}
func (s *bcacheStore) Start(cfg *config.Config) (err error) {
runtime.GOMAXPROCS(runtime.NumCPU())
return s.control.Start(s, cfg, doStart)
}
func (s *bcacheStore) Shutdown() {
s.control.Shutdown(s, doShutdown)
}
func (s *bcacheStore) Sync() {
s.control.Sync()
}
func doStart(server common.Server, cfg *config.Config) (err error) {
s, ok := server.(*bcacheStore)
if !ok {
return errors.New("Invalid node Type!")
}
// parse the config file
var bconf *bcacheConfig
bconf, err = s.parserConf(cfg)
if err != nil {
err = errors.NewErrorf("block config parser error.")
panic(err)
}
// start bcache manage
bm := newBcacheManager(bconf)
if bm == nil {
err = errors.NewErrorf("block cache manager init fail.")
panic(err)
}
s.bcache = bm
s.conf = bconf
s.bufPool = sync.Pool{New: func() interface{} {
buf := make([]byte, MaxBlockSize)
return &buf
}}
// start unix domain socket
s.startUnixHttpServer()
return
}
func doShutdown(server common.Server) {
s, ok := server.(*bcacheStore)
if !ok {
return
}
//stop unix domain socket
if s.unixListener != nil {
s.unixListener.Close()
}
}
func (s *bcacheStore) registerHandler(router *mux.Router) {
router.NewRoute().Methods(http.MethodPut).Path("/cache").HandlerFunc(s.cacheBlock)
router.NewRoute().Methods(http.MethodGet).Path("/load").HandlerFunc(s.loadBlock)
router.NewRoute().Methods(http.MethodDelete).Path("/evict").HandlerFunc(s.evictBlock)
router.NewRoute().Methods(http.MethodHead).Path("/stop").HandlerFunc(s.stopService)
}
func (s *bcacheStore) startUnixHttpServer() {
os.MkdirAll(filepath.Dir(UnixSocketPath), FilePerm)
if _, err := os.Stat(UnixSocketPath); err == nil {
existErr := fmt.Sprintf("Another process is running or %s already exist.", UnixSocketPath)
log.LogErrorf(existErr)
os.Remove(UnixSocketPath)
}
router := mux.NewRouter().SkipClean(true)
s.registerHandler(router)
go func() {
lis, err := net.Listen("unix", UnixSocketPath)
if err != nil {
panic(err)
}
s.unixListener = lis
http.Serve(lis, router)
}()
go func() {
for {
time.Sleep(time.Minute * 2)
runtime.GC()
}
}()
}
func (s *bcacheStore) cacheBlock(w http.ResponseWriter, r *http.Request) {
var err error
var errorCode *ErrorCode
bgTime := stat.BeginStat()
defer func() {
if errorCode != nil {
errorCode.ServeResponse(w, r)
}
if r != nil && r.Body != nil {
r.Body.Close()
}
stat.EndStat("Cache", err, bgTime, 1)
}()
key := r.URL.Query().Get(CacheKey)
md5Sum := r.URL.Query().Get(CheckSum)
if key == "" || md5Sum == "" {
log.LogErrorf("InvalidArgument,cacheKey or md5 is nil.")
errorCode = InvalidArgument
return
}
if !keyRegexp.MatchString(key) {
log.LogErrorf("InvalidKey,cacheKey is invalid. key(%v)", key)
errorCode = InvalidKey
return
}
var (
readN int
hash = md5.New()
)
buf := s.bufPool.Get().(*[]byte)
defer func() {
s.bufPool.Put(buf)
}()
defer r.Body.Close()
if r.ContentLength > MaxBlockSize {
log.LogWarnf("EntityTooLarge. key(%v) readN(%v) blockSize(%v)", key, readN, s.conf.BlockSize)
errorCode = EntityTooLarge
return
}
content := make([]byte, r.ContentLength)
n, err := io.ReadFull(r.Body, content)
if err != nil && err != io.EOF && int64(n) != r.ContentLength {
log.LogErrorf("ReadStreamError. key(%v) err(%v) ContentLength(%v) readN(%v)", key, err, r.ContentLength, n)
errorCode = ReadStreamError
return
}
readN = copy(*buf, content)
if uint32(readN) > MaxBlockSize {
log.LogErrorf("EntityTooLarge. key(%v) readN(%v) blockSize(%v)", key, readN, s.conf.BlockSize)
errorCode = EntityTooLarge
return
}
if readN > 0 {
hash.Write((*buf)[:readN])
md5 := hex.EncodeToString(hash.Sum(nil))
if md5 == md5Sum {
s.bcache.cache(key, content, false)
} else {
errorCode = BadDigest
log.LogErrorf("BadDigest. key(%v) request_md5(%v) md5(%v)", key, md5Sum, md5)
return
}
}
return
}
func (s *bcacheStore) loadBlock(w http.ResponseWriter, r *http.Request) {
var errorCode *ErrorCode
var err error
var length uint64
bgTime := stat.BeginStat()
defer func() {
if errorCode != nil {
errorCode.ServeResponse(w, r)
}
stat.EndStat("GetCache", err, bgTime, 1)
}()
parameter := r.URL.Query()
key := parameter.Get(CacheKey)
offsetStr := parameter.Get(OffSet)
lenStr := parameter.Get(Len)
if !keyRegexp.MatchString(key) {
errorCode = InvalidKey
err = errors.New(InvalidKey.ErrorCode)
return
}
if key == "" || offsetStr == "" || lenStr == "" {
errorCode = InvalidArgument
err = errors.New(InvalidArgument.ErrorCode)
return
}
offset, err := strconv.ParseUint(offsetStr, 10, 64)
if err != nil {
errorCode = InvalidNumber
err = errors.New(InvalidNumber.ErrorCode)
return
}
length, err = strconv.ParseUint(lenStr, 10, 64)
if err != nil {
errorCode = InvalidNumber
err = errors.New(InvalidNumber.ErrorCode)
return
}
in, err := s.bcache.read(key, offset, uint32(length))
if err != nil {
if os.IsNotExist(err) {
errorCode = NoSuchCacheKey
err = errors.New(NoSuchCacheKey.ErrorCode)
} else {
errorCode = InternalError
err = errors.New(InternalError.ErrorCode)
}
return
}
w.Header()["Content-Type"] = []string{"application/octet-stream"}
defer in.Close()
io.Copy(w, in)
return
}
func (s *bcacheStore) stopService(w http.ResponseWriter, r *http.Request) {
s.Shutdown()
}
func (s *bcacheStore) parserConf(cfg *config.Config) (*bcacheConfig, error) {
bconf := &bcacheConfig{}
cacheDir := cfg.GetString(CacheDir)
cacheLimit := cfg.GetString(CacheLimit)
cacheFree := cfg.GetString(CacheFree)
blockSize := cfg.GetString(BlockSize)
bconf.CacheDir = cacheDir
if cacheDir == "" {
return nil, errors.NewErrorf("cacheDir is required.")
}
if v, err := strconv.Atoi(blockSize); err == nil {
bconf.BlockSize = uint32(v)
}
if v, err := strconv.Atoi(cacheLimit); err == nil {
bconf.Limit = v
}
if v, err := strconv.ParseFloat(cacheFree, 32); err == nil {
bconf.FreeRatio = float32(v)
}
return bconf, nil
}
func (s *bcacheStore) evictBlock(w http.ResponseWriter, r *http.Request) {
var errorCode *ErrorCode
var err error
bgTime := stat.BeginStat()
defer func() {
if errorCode != nil {
errorCode.ServeResponse(w, r)
}
stat.EndStat("EvictCache", err, bgTime, 1)
}()
parameter := r.URL.Query()
key := parameter.Get(CacheKey)
if !keyRegexp.MatchString(key) {
errorCode = InvalidKey
err = errors.New(InvalidKey.ErrorCode)
return
}
if key == "" {
errorCode = InvalidArgument
err = errors.New(InvalidArgument.ErrorCode)
return
}
s.bcache.erase(key)
}
func (code ErrorCode) ServeResponse(w http.ResponseWriter, r *http.Request) error {
var err error
var marshaled []byte
mux.Vars(r)[ContextKeyStatusCode] = strconv.Itoa(code.StatusCode)
mux.Vars(r)[ContextKeyErrorMessage] = code.ErrorMessage
requestId := mux.Vars(r)[ContextKeyRequestID]
var xmlError = struct {
XMLName xml.Name `xml:"Error"`
Code string `xml:"Code"`
Message string `xml:"Message"`
Resource string `xml:"Resource"`
RequestId string `xml:"RequestId"`
}{
Code: code.ErrorCode,
Message: code.ErrorMessage,
Resource: r.URL.String(),
RequestId: requestId,
}
if marshaled, err = xml.Marshal(&xmlError); err != nil {
return err
}
w.Header()["Content-Type"] = []string{"application/xml"}
w.WriteHeader(code.StatusCode)
if _, err = w.Write(marshaled); err != nil {
return err
}
return nil
}