cubefs/client/fuse.go
Sheng Yong 447e213949 fuse: Wait ProfPort ready for use
This patch adds `waitListenAndServe' to make sure ProfPort is not
in use before timeout (1 min by default). If something goes wrong,
an error is returned to the main goroutine through a channel.

However, if the specific pprof port is in use after timeout, try
choosing a port automatically by system. And warning messages are
printed to show the new port.

Signed-off-by: Sheng Yong <shengyong2021@gmail.com>
2022-01-14 12:51:50 +08:00

500 lines
14 KiB
Go

// Copyright 2018 The Chubao 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 main
//
// Usage: ./client -c fuse.json &
//
// Default mountpoint is specified in fuse.json, which is "/mnt".
//
import (
"flag"
"fmt"
syslog "log"
"net"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"path"
"path/filepath"
"runtime"
"runtime/debug"
"strings"
"syscall"
"time"
"github.com/chubaofs/chubaofs/sdk/master"
sysutil "github.com/chubaofs/chubaofs/util/sys"
"bazil.org/fuse"
"bazil.org/fuse/fs"
cfs "github.com/chubaofs/chubaofs/client/fs"
"github.com/chubaofs/chubaofs/proto"
"github.com/chubaofs/chubaofs/util/config"
"github.com/chubaofs/chubaofs/util/errors"
"github.com/chubaofs/chubaofs/util/exporter"
"github.com/chubaofs/chubaofs/util/log"
"github.com/chubaofs/chubaofs/util/ump"
"github.com/jacobsa/daemonize"
)
const (
MaxReadAhead = 512 * 1024
defaultRlimit uint64 = 1024000
)
const (
LoggerDir = "client"
LoggerPrefix = "client"
LoggerOutput = "output.log"
ModuleName = "fuseclient"
ConfigKeyExporterPort = "exporterKey"
ControlCommandSetRate = "/rate/set"
ControlCommandGetRate = "/rate/get"
ControlCommandFreeOSMemory = "/debug/freeosmemory"
Role = "Client"
)
var (
configFile = flag.String("c", "", "FUSE client config file")
configVersion = flag.Bool("v", false, "show version")
configForeground = flag.Bool("f", false, "run foreground")
)
var GlobalMountOptions []proto.MountOption
func init() {
GlobalMountOptions = proto.NewMountOptions()
proto.InitMountOptions(GlobalMountOptions)
}
func main() {
flag.Parse()
if *configVersion {
fmt.Print(proto.DumpVersion(Role))
os.Exit(0)
}
if !*configForeground {
if err := startDaemon(); err != nil {
fmt.Printf("Mount failed: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
/*
* We are in daemon from here.
* Must notify the parent process through SignalOutcome anyway.
*/
cfg, _ := config.LoadConfigFile(*configFile)
opt, err := parseMountOption(cfg)
if err != nil {
err = errors.NewErrorf("parse mount opt failed: %v\n", err)
fmt.Println(err)
daemonize.SignalOutcome(err)
os.Exit(1)
}
if opt.MaxCPUs > 0 {
runtime.GOMAXPROCS(int(opt.MaxCPUs))
} else {
runtime.GOMAXPROCS(runtime.NumCPU())
}
level := parseLogLevel(opt.Loglvl)
_, err = log.InitLog(opt.Logpath, LoggerPrefix, level, nil)
if err != nil {
err = errors.NewErrorf("Init log dir fail: %v\n", err)
fmt.Println(err)
daemonize.SignalOutcome(err)
os.Exit(1)
}
defer log.LogFlush()
outputFilePath := path.Join(opt.Logpath, LoggerPrefix, LoggerOutput)
outputFile, err := os.OpenFile(outputFilePath, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
if err != nil {
err = errors.NewErrorf("Open output file failed: %v\n", err)
fmt.Println(err)
daemonize.SignalOutcome(err)
os.Exit(1)
}
defer func() {
outputFile.Sync()
outputFile.Close()
}()
syslog.SetOutput(outputFile)
syslog.Println(proto.DumpVersion(Role))
syslog.Println("*** Final Mount Options ***")
for _, o := range GlobalMountOptions {
syslog.Println(o)
}
syslog.Println("*** End ***")
changeRlimit(defaultRlimit)
if err = sysutil.RedirectFD(int(outputFile.Fd()), int(os.Stderr.Fd())); err != nil {
err = errors.NewErrorf("Redirect fd failed: %v\n", err)
syslog.Println(err)
daemonize.SignalOutcome(err)
os.Exit(1)
}
registerInterceptedSignal(opt.MountPoint)
if err = checkPermission(opt); err != nil {
err = errors.NewErrorf("check permission failed: %v", err)
syslog.Println(err)
log.LogFlush()
_ = daemonize.SignalOutcome(err)
os.Exit(1)
}
fsConn, super, err := mount(opt)
if err != nil {
err = errors.NewErrorf("mount failed: %v", err)
syslog.Println(err)
log.LogFlush()
_ = daemonize.SignalOutcome(err)
os.Exit(1)
} else {
_ = daemonize.SignalOutcome(nil)
}
defer fsConn.Close()
exporter.Init(ModuleName, cfg)
exporter.RegistConsul(super.ClusterName(), ModuleName, cfg)
err = log.OutputPid(opt.Logpath, ModuleName)
if err != nil {
log.LogFlush()
syslog.Printf("output pid err(%v)", err)
os.Exit(1)
}
if err = fs.Serve(fsConn, super); err != nil {
log.LogFlush()
syslog.Printf("fs Serve returns err(%v)", err)
os.Exit(1)
}
<-fsConn.Ready
if fsConn.MountError != nil {
log.LogFlush()
syslog.Printf("fs Serve returns err(%v)\n", err)
os.Exit(1)
}
}
func startDaemon() error {
cmdPath, err := os.Executable()
if err != nil {
return fmt.Errorf("startDaemon failed: cannot get absolute command path, err(%v)", err)
}
if len(os.Args) <= 1 {
return fmt.Errorf("startDaemon failed: cannot use null arguments")
}
args := []string{"-f"}
args = append(args, os.Args[1:]...)
if *configFile != "" {
configPath, err := filepath.Abs(*configFile)
if err != nil {
return fmt.Errorf("startDaemon failed: cannot get absolute command path of config file(%v) , err(%v)", *configFile, err)
}
for i := 0; i < len(args); i++ {
if args[i] == "-c" {
// Since *configFile is not "", the (i+1)th argument must be the config file path
args[i+1] = configPath
break
}
}
}
env := os.Environ()
// add GODEBUG=madvdontneed=1 environ, to make sysUnused uses madvise(MADV_DONTNEED) to signal the kernel that a
// range of allocated memory contains unneeded data.
env = append(env, "GODEBUG=madvdontneed=1")
err = daemonize.Run(cmdPath, args, env, os.Stdout)
if err != nil {
return fmt.Errorf("startDaemon failed: daemon start failed, cmd(%v) args(%v) env(%v) err(%v)\n", cmdPath, args, env, err)
}
return nil
}
func waitListenAndServe(statusCh chan error, addr string, handler http.Handler) {
var err error
var loop int = 0
var interval int = (1 << 17) - 1
var listener net.Listener
var dynamicPort bool
if addr == ":" {
addr = ":0"
}
// FIXME: 1 min timeout?
timeout := time.Now().Add(time.Minute)
for {
if listener, err = net.Listen("tcp", addr); err == nil {
break
}
// addr is not released for use
if strings.Contains(err.Error(), "bind: address already in use") {
if loop&interval == 0 {
syslog.Printf("address %v is still in use\n", addr)
}
runtime.Gosched()
} else {
break
}
if time.Now().After(timeout) {
msg := fmt.Sprintf("address %v is still in use after "+
"timeout, choose port automatically\n", addr)
syslog.Print(msg)
msg = "Warning: " + msg
daemonize.StatusWriter.Write([]byte(msg))
dynamicPort = true
break
}
loop++
}
syslog.Printf("address %v wait loop %v\n", addr, loop)
if dynamicPort {
ipport := strings.Split(addr, ":")
addr = ipport[0] + ":0"
listener, err = net.Listen("tcp", addr)
}
if err != nil {
statusCh <- err
return
}
statusCh <- nil
msg := fmt.Sprintf("Start pprof with port: %v\n",
listener.Addr().(*net.TCPAddr).Port)
syslog.Print(msg)
if dynamicPort {
msg = "Warning: " + msg
daemonize.StatusWriter.Write([]byte(msg))
}
http.Serve(listener, handler)
// unreachable
}
func mount(opt *proto.MountOptions) (fsConn *fuse.Conn, super *cfs.Super, err error) {
super, err = cfs.NewSuper(opt)
if err != nil {
log.LogError(errors.Stack(err))
return
}
http.HandleFunc(ControlCommandSetRate, super.SetRate)
http.HandleFunc(ControlCommandGetRate, super.GetRate)
http.HandleFunc(log.SetLogLevelPath, log.SetLogLevel)
http.HandleFunc(ControlCommandFreeOSMemory, freeOSMemory)
http.HandleFunc(log.GetLogPath, log.GetLog)
statusCh := make(chan error)
go waitListenAndServe(statusCh, ":"+opt.Profport, nil)
if err = <-statusCh; err != nil {
daemonize.SignalOutcome(err)
return
}
if err = ump.InitUmp(fmt.Sprintf("%v_%v", super.ClusterName(), ModuleName), opt.UmpDatadir); err != nil {
return
}
options := []fuse.MountOption{
fuse.AllowOther(),
fuse.MaxReadahead(MaxReadAhead),
fuse.AsyncRead(),
fuse.AutoInvalData(opt.AutoInvalData),
fuse.FSName("chubaofs-" + opt.Volname),
fuse.Subtype("chubaofs"),
fuse.LocalVolume(),
fuse.VolumeName("chubaofs-" + opt.Volname)}
if opt.Rdonly {
options = append(options, fuse.ReadOnly())
}
if opt.WriteCache {
options = append(options, fuse.WritebackCache())
}
if opt.EnablePosixACL {
options = append(options, fuse.PosixACL())
}
if opt.EnableUnixPermission {
options = append(options, fuse.DefaultPermissions())
}
fsConn, err = fuse.Mount(opt.MountPoint, options...)
return
}
func registerInterceptedSignal(mnt string) {
sigC := make(chan os.Signal, 1)
signal.Notify(sigC, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigC
syslog.Printf("Killed due to a received signal (%v)\n", sig)
os.Exit(1)
}()
}
func parseMountOption(cfg *config.Config) (*proto.MountOptions, error) {
var err error
opt := new(proto.MountOptions)
proto.ParseMountOptions(GlobalMountOptions, cfg)
rawmnt := GlobalMountOptions[proto.MountPoint].GetString()
opt.MountPoint, err = filepath.Abs(rawmnt)
if err != nil {
return nil, errors.Trace(err, "invalide mount point (%v) ", rawmnt)
}
opt.Volname = GlobalMountOptions[proto.VolName].GetString()
opt.Owner = GlobalMountOptions[proto.Owner].GetString()
opt.Master = GlobalMountOptions[proto.Master].GetString()
opt.Logpath = GlobalMountOptions[proto.LogDir].GetString()
opt.Loglvl = GlobalMountOptions[proto.LogLevel].GetString()
opt.Profport = GlobalMountOptions[proto.ProfPort].GetString()
opt.IcacheTimeout = GlobalMountOptions[proto.IcacheTimeout].GetInt64()
opt.LookupValid = GlobalMountOptions[proto.LookupValid].GetInt64()
opt.AttrValid = GlobalMountOptions[proto.AttrValid].GetInt64()
opt.ReadRate = GlobalMountOptions[proto.ReadRate].GetInt64()
opt.WriteRate = GlobalMountOptions[proto.WriteRate].GetInt64()
opt.EnSyncWrite = GlobalMountOptions[proto.EnSyncWrite].GetInt64()
opt.AutoInvalData = GlobalMountOptions[proto.AutoInvalData].GetInt64()
opt.UmpDatadir = GlobalMountOptions[proto.WarnLogDir].GetString()
opt.Rdonly = GlobalMountOptions[proto.Rdonly].GetBool()
opt.WriteCache = GlobalMountOptions[proto.WriteCache].GetBool()
opt.KeepCache = GlobalMountOptions[proto.KeepCache].GetBool()
opt.FollowerRead = GlobalMountOptions[proto.FollowerRead].GetBool()
opt.Authenticate = GlobalMountOptions[proto.Authenticate].GetBool()
if opt.Authenticate {
opt.TicketMess.ClientKey = GlobalMountOptions[proto.ClientKey].GetString()
ticketHostConfig := GlobalMountOptions[proto.TicketHost].GetString()
ticketHosts := strings.Split(ticketHostConfig, ",")
opt.TicketMess.TicketHosts = ticketHosts
opt.TicketMess.EnableHTTPS = GlobalMountOptions[proto.EnableHTTPS].GetBool()
if opt.TicketMess.EnableHTTPS {
opt.TicketMess.CertFile = GlobalMountOptions[proto.CertFile].GetString()
}
}
opt.AccessKey = GlobalMountOptions[proto.AccessKey].GetString()
opt.SecretKey = GlobalMountOptions[proto.SecretKey].GetString()
opt.DisableDcache = GlobalMountOptions[proto.DisableDcache].GetBool()
opt.SubDir = GlobalMountOptions[proto.SubDir].GetString()
opt.FsyncOnClose = GlobalMountOptions[proto.FsyncOnClose].GetBool()
opt.MaxCPUs = GlobalMountOptions[proto.MaxCPUs].GetInt64()
opt.EnableXattr = GlobalMountOptions[proto.EnableXattr].GetBool()
opt.NearRead = GlobalMountOptions[proto.NearRead].GetBool()
opt.EnablePosixACL = GlobalMountOptions[proto.EnablePosixACL].GetBool()
opt.EnableSummary = GlobalMountOptions[proto.EnableSummary].GetBool()
opt.EnableUnixPermission = GlobalMountOptions[proto.EnableUnixPermission].GetBool()
if opt.MountPoint == "" || opt.Volname == "" || opt.Owner == "" || opt.Master == "" {
return nil, errors.New(fmt.Sprintf("invalid config file: lack of mandatory fields, mountPoint(%v), volName(%v), owner(%v), masterAddr(%v)", opt.MountPoint, opt.Volname, opt.Owner, opt.Master))
}
return opt, nil
}
func checkPermission(opt *proto.MountOptions) (err error) {
var mc = master.NewMasterClientFromString(opt.Master, false)
// Check user access policy is enabled
if opt.AccessKey != "" {
var userInfo *proto.UserInfo
if userInfo, err = mc.UserAPI().GetAKInfo(opt.AccessKey); err != nil {
return
}
if userInfo.SecretKey != opt.SecretKey {
err = proto.ErrNoPermission
return
}
var policy = userInfo.Policy
if policy.IsOwn(opt.Volname) {
return
}
if policy.IsAuthorized(opt.Volname, opt.SubDir, proto.POSIXWriteAction) &&
policy.IsAuthorized(opt.Volname, opt.SubDir, proto.POSIXReadAction) {
return
}
if policy.IsAuthorized(opt.Volname, opt.SubDir, proto.POSIXReadAction) &&
!policy.IsAuthorized(opt.Volname, opt.SubDir, proto.POSIXWriteAction) {
opt.Rdonly = true
return
}
err = proto.ErrNoPermission
return
}
return
}
func parseLogLevel(loglvl string) log.Level {
var level log.Level
switch strings.ToLower(loglvl) {
case "debug":
level = log.DebugLevel
case "info":
level = log.InfoLevel
case "warn":
level = log.WarnLevel
case "error":
level = log.ErrorLevel
default:
level = log.ErrorLevel
}
return level
}
func changeRlimit(val uint64) {
rlimit := &syscall.Rlimit{Max: val, Cur: val}
err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, rlimit)
if err != nil {
syslog.Printf("Failed to set rlimit to %v \n", val)
} else {
syslog.Printf("Successfully set rlimit to %v \n", val)
}
}
func freeOSMemory(w http.ResponseWriter, r *http.Request) {
debug.FreeOSMemory()
}