Chris Lu
3 years ago
12 changed files with 381 additions and 30 deletions
-
4weed/Makefile
-
1weed/command/command.go
-
1weed/command/filer.go
-
219weed/command/filer_remote_sync.go
-
6weed/command/filer_replication.go
-
31weed/command/imports.go
-
1weed/filer/filer_remote_storage.go
-
32weed/filer/stream.go
-
6weed/remote_storage/remote_storage.go
-
86weed/remote_storage/s3/s3_storage_client.go
-
11weed/shell/command_remote_configure.go
-
5weed/shell/command_remote_mount.go
@ -0,0 +1,219 @@ |
|||||
|
package command |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"github.com/chrislusf/seaweedfs/weed/filer" |
||||
|
"github.com/chrislusf/seaweedfs/weed/glog" |
||||
|
"github.com/chrislusf/seaweedfs/weed/pb" |
||||
|
"github.com/chrislusf/seaweedfs/weed/pb/filer_pb" |
||||
|
"github.com/chrislusf/seaweedfs/weed/remote_storage" |
||||
|
"github.com/chrislusf/seaweedfs/weed/replication/source" |
||||
|
"github.com/chrislusf/seaweedfs/weed/security" |
||||
|
"github.com/chrislusf/seaweedfs/weed/util" |
||||
|
"github.com/golang/protobuf/proto" |
||||
|
"google.golang.org/grpc" |
||||
|
"strings" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
type RemoteSyncOptions struct { |
||||
|
filerAddress *string |
||||
|
grpcDialOption grpc.DialOption |
||||
|
readChunkFromFiler *bool |
||||
|
debug *bool |
||||
|
timeAgo *time.Duration |
||||
|
dir *string |
||||
|
} |
||||
|
|
||||
|
const ( |
||||
|
RemoteSyncKeyPrefix = "remote.sync." |
||||
|
) |
||||
|
|
||||
|
var _ = filer_pb.FilerClient(&RemoteSyncOptions{}) |
||||
|
|
||||
|
func (option *RemoteSyncOptions) WithFilerClient(fn func(filer_pb.SeaweedFilerClient) error) error { |
||||
|
return pb.WithFilerClient(*option.filerAddress, option.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error { |
||||
|
return fn(client) |
||||
|
}) |
||||
|
} |
||||
|
func (option *RemoteSyncOptions) AdjustedUrl(location *filer_pb.Location) string { |
||||
|
return location.Url |
||||
|
} |
||||
|
|
||||
|
var ( |
||||
|
remoteSyncOptions RemoteSyncOptions |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
cmdFilerRemoteSynchronize.Run = runFilerRemoteSynchronize // break init cycle
|
||||
|
remoteSyncOptions.filerAddress = cmdFilerRemoteSynchronize.Flag.String("filer", "localhost:8888", "filer of the SeaweedFS cluster") |
||||
|
remoteSyncOptions.dir = cmdFilerRemoteSynchronize.Flag.String("dir", "/", "a mounted directory on filer") |
||||
|
remoteSyncOptions.readChunkFromFiler = cmdFilerRemoteSynchronize.Flag.Bool("filerProxy", false, "read file chunks from filer instead of volume servers") |
||||
|
remoteSyncOptions.debug = cmdFilerRemoteSynchronize.Flag.Bool("debug", false, "debug mode to print out filer updated remote files") |
||||
|
remoteSyncOptions.timeAgo = cmdFilerRemoteSynchronize.Flag.Duration("timeAgo", 0, "start time before now. \"300ms\", \"1.5h\" or \"2h45m\". Valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\"") |
||||
|
} |
||||
|
|
||||
|
var cmdFilerRemoteSynchronize = &Command{ |
||||
|
UsageLine: "filer.remote.sync -filer=<filerHost>:<filerPort> -dir=/mount/s3_on_cloud", |
||||
|
Short: "resumeable continuously write back updates to remote storage if the directory is mounted to the remote storage", |
||||
|
Long: `resumeable continuously write back updates to remote storage if the directory is mounted to the remote storage |
||||
|
|
||||
|
filer.remote.sync listens on filer update events. |
||||
|
If any mounted remote file is updated, it will fetch the updated content, |
||||
|
and write to the remote storage. |
||||
|
`, |
||||
|
} |
||||
|
|
||||
|
func runFilerRemoteSynchronize(cmd *Command, args []string) bool { |
||||
|
|
||||
|
util.LoadConfiguration("security", false) |
||||
|
grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.client") |
||||
|
remoteSyncOptions.grpcDialOption = grpcDialOption |
||||
|
|
||||
|
// read filer remote storage mount mappings
|
||||
|
mappings, readErr := filer.ReadMountMappings(grpcDialOption, *remoteSyncOptions.filerAddress) |
||||
|
if readErr != nil { |
||||
|
fmt.Printf("read mount mapping: %v", readErr) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
filerSource := &source.FilerSource{} |
||||
|
filerSource.DoInitialize( |
||||
|
*remoteSyncOptions.filerAddress, |
||||
|
pb.ServerToGrpcAddress(*remoteSyncOptions.filerAddress), |
||||
|
"/", // does not matter
|
||||
|
*remoteSyncOptions.readChunkFromFiler, |
||||
|
) |
||||
|
|
||||
|
var found bool |
||||
|
for dir, remoteStorageMountLocation := range mappings.Mappings { |
||||
|
if *remoteSyncOptions.dir == dir { |
||||
|
found = true |
||||
|
storageConf, readErr := filer.ReadRemoteStorageConf(grpcDialOption, *remoteSyncOptions.filerAddress, remoteStorageMountLocation.Name) |
||||
|
if readErr != nil { |
||||
|
fmt.Printf("read remote storage configuration for %s: %v", dir, readErr) |
||||
|
continue |
||||
|
} |
||||
|
fmt.Printf("synchronize %s to remote storage...\n", *remoteSyncOptions.dir) |
||||
|
if err := util.Retry("filer.remote.sync "+dir, func() error { |
||||
|
return followUpdatesAndUploadToRemote(&remoteSyncOptions, filerSource, dir, storageConf, remoteStorageMountLocation) |
||||
|
}); err != nil { |
||||
|
fmt.Printf("synchronize %s: %v\n", *remoteSyncOptions.dir, err) |
||||
|
} |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if !found { |
||||
|
fmt.Printf("directory %s is not mounted to any remote storage\n", *remoteSyncOptions.dir) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
func followUpdatesAndUploadToRemote(option *RemoteSyncOptions, filerSource *source.FilerSource, mountedDir string, remoteStorage *filer_pb.RemoteConf, remoteStorageMountLocation *filer_pb.RemoteStorageLocation) error { |
||||
|
|
||||
|
dirHash := util.HashStringToLong(mountedDir) |
||||
|
|
||||
|
// 1. specified by timeAgo
|
||||
|
// 2. last offset timestamp for this directory
|
||||
|
// 3. directory creation time
|
||||
|
var lastOffsetTs time.Time |
||||
|
if *option.timeAgo == 0 { |
||||
|
mountedDirEntry, err := filer_pb.GetEntry(option, util.FullPath(mountedDir)) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("lookup %s: %v", mountedDir, err) |
||||
|
} |
||||
|
|
||||
|
lastOffsetTsNs, err := getOffset(option.grpcDialOption, *option.filerAddress, RemoteSyncKeyPrefix, int32(dirHash)) |
||||
|
if err == nil && mountedDirEntry.Attributes.Crtime < lastOffsetTsNs/1000000 { |
||||
|
lastOffsetTs = time.Unix(0, lastOffsetTsNs) |
||||
|
glog.V(0).Infof("resume from %v", lastOffsetTs) |
||||
|
} else { |
||||
|
lastOffsetTs = time.Unix(mountedDirEntry.Attributes.Crtime, 0) |
||||
|
} |
||||
|
} else { |
||||
|
lastOffsetTs = time.Now().Add(-*option.timeAgo) |
||||
|
} |
||||
|
|
||||
|
client, err := remote_storage.GetRemoteStorage(remoteStorage) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
eachEntryFunc := func(resp *filer_pb.SubscribeMetadataResponse) error { |
||||
|
message := resp.EventNotification |
||||
|
if message.OldEntry == nil && message.NewEntry == nil { |
||||
|
return nil |
||||
|
} |
||||
|
if message.OldEntry == nil && message.NewEntry != nil { |
||||
|
if len(message.NewEntry.Chunks) == 0 { |
||||
|
return nil |
||||
|
} |
||||
|
fmt.Printf("create: %+v\n", resp) |
||||
|
dest := toRemoteStorageLocation(util.FullPath(mountedDir), util.NewFullPath(message.NewParentPath, message.NewEntry.Name), remoteStorageMountLocation) |
||||
|
reader := filer.NewChunkStreamReader(filerSource, message.NewEntry.Chunks) |
||||
|
return client.WriteFile(dest, message.NewEntry, reader) |
||||
|
} |
||||
|
if message.OldEntry != nil && message.NewEntry == nil { |
||||
|
fmt.Printf("delete: %+v\n", resp) |
||||
|
dest := toRemoteStorageLocation(util.FullPath(mountedDir), util.NewFullPath(resp.Directory, message.OldEntry.Name), remoteStorageMountLocation) |
||||
|
return client.DeleteFile(dest) |
||||
|
} |
||||
|
if message.OldEntry != nil && message.NewEntry != nil { |
||||
|
oldDest := toRemoteStorageLocation(util.FullPath(mountedDir), util.NewFullPath(resp.Directory, message.OldEntry.Name), remoteStorageMountLocation) |
||||
|
dest := toRemoteStorageLocation(util.FullPath(mountedDir), util.NewFullPath(message.NewParentPath, message.NewEntry.Name), remoteStorageMountLocation) |
||||
|
if resp.Directory == message.NewParentPath && message.OldEntry.Name == message.NewEntry.Name { |
||||
|
if isSameChunks(message.OldEntry.Chunks, message.NewEntry.Chunks) { |
||||
|
fmt.Printf("update meta: %+v\n", resp) |
||||
|
return client.UpdateFileMetadata(dest, message.NewEntry) |
||||
|
} |
||||
|
} |
||||
|
fmt.Printf("update: %+v\n", resp) |
||||
|
if err := client.DeleteFile(oldDest); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
reader := filer.NewChunkStreamReader(filerSource, message.NewEntry.Chunks) |
||||
|
return client.WriteFile(dest, message.NewEntry, reader) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
processEventFnWithOffset := pb.AddOffsetFunc(eachEntryFunc, 3*time.Second, func(counter int64, lastTsNs int64) error { |
||||
|
lastTime := time.Unix(0, lastTsNs) |
||||
|
glog.V(0).Infof("remote sync %s progressed to %v %0.2f/sec", *option.filerAddress, lastTime, float64(counter)/float64(3)) |
||||
|
return setOffset(option.grpcDialOption, *option.filerAddress, RemoteSyncKeyPrefix, int32(dirHash), lastTsNs) |
||||
|
}) |
||||
|
|
||||
|
return pb.FollowMetadata(*option.filerAddress, option.grpcDialOption, |
||||
|
"filer.remote.sync", mountedDir, lastOffsetTs.UnixNano(), 0, processEventFnWithOffset, false) |
||||
|
} |
||||
|
|
||||
|
func toRemoteStorageLocation(mountDir, sourcePath util.FullPath, remoteMountLocation *filer_pb.RemoteStorageLocation) *filer_pb.RemoteStorageLocation { |
||||
|
var dest string |
||||
|
source := string(sourcePath[len(mountDir):]) |
||||
|
if strings.HasSuffix(remoteMountLocation.Path, "/") { |
||||
|
dest = remoteMountLocation.Path + source[1:] |
||||
|
} else { |
||||
|
dest = remoteMountLocation.Path + source |
||||
|
} |
||||
|
return &filer_pb.RemoteStorageLocation{ |
||||
|
Name: remoteMountLocation.Name, |
||||
|
Bucket: remoteMountLocation.Bucket, |
||||
|
Path: dest, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func isSameChunks(a, b []*filer_pb.FileChunk) bool { |
||||
|
if len(a) != len(b) { |
||||
|
return false |
||||
|
} |
||||
|
for i := 0; i < len(a); i++ { |
||||
|
x, y := a[i], b[i] |
||||
|
if !proto.Equal(x, y) { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
return true |
||||
|
} |
@ -0,0 +1,31 @@ |
|||||
|
package command |
||||
|
|
||||
|
import ( |
||||
|
_ "net/http/pprof" |
||||
|
|
||||
|
_ "github.com/chrislusf/seaweedfs/weed/remote_storage/s3" |
||||
|
|
||||
|
_ "github.com/chrislusf/seaweedfs/weed/replication/sink/azuresink" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/replication/sink/b2sink" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/replication/sink/filersink" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/replication/sink/gcssink" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/replication/sink/localsink" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/replication/sink/s3sink" |
||||
|
|
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/cassandra" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/elastic/v7" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/etcd" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/hbase" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/leveldb" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/leveldb2" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/leveldb3" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/mongodb" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/mysql" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/mysql2" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/postgres" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/postgres2" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/redis" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/redis2" |
||||
|
_ "github.com/chrislusf/seaweedfs/weed/filer/sqlite" |
||||
|
|
||||
|
) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue