filer_sync.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. package command
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "github.com/chrislusf/seaweedfs/weed/glog"
  7. "github.com/chrislusf/seaweedfs/weed/pb"
  8. "github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
  9. "github.com/chrislusf/seaweedfs/weed/replication"
  10. "github.com/chrislusf/seaweedfs/weed/replication/sink"
  11. "github.com/chrislusf/seaweedfs/weed/replication/sink/filersink"
  12. "github.com/chrislusf/seaweedfs/weed/replication/source"
  13. "github.com/chrislusf/seaweedfs/weed/security"
  14. "github.com/chrislusf/seaweedfs/weed/util"
  15. "github.com/chrislusf/seaweedfs/weed/util/grace"
  16. "google.golang.org/grpc"
  17. "strings"
  18. "time"
  19. )
  20. type SyncOptions struct {
  21. isActivePassive *bool
  22. filerA *string
  23. filerB *string
  24. aPath *string
  25. bPath *string
  26. aReplication *string
  27. bReplication *string
  28. aCollection *string
  29. bCollection *string
  30. aTtlSec *int
  31. bTtlSec *int
  32. aDiskType *string
  33. bDiskType *string
  34. aDebug *bool
  35. bDebug *bool
  36. aProxyByFiler *bool
  37. bProxyByFiler *bool
  38. }
  39. var (
  40. syncOptions SyncOptions
  41. syncCpuProfile *string
  42. syncMemProfile *string
  43. )
  44. func init() {
  45. cmdFilerSynchronize.Run = runFilerSynchronize // break init cycle
  46. syncOptions.isActivePassive = cmdFilerSynchronize.Flag.Bool("isActivePassive", false, "one directional follow from A to B if true")
  47. syncOptions.filerA = cmdFilerSynchronize.Flag.String("a", "", "filer A in one SeaweedFS cluster")
  48. syncOptions.filerB = cmdFilerSynchronize.Flag.String("b", "", "filer B in the other SeaweedFS cluster")
  49. syncOptions.aPath = cmdFilerSynchronize.Flag.String("a.path", "/", "directory to sync on filer A")
  50. syncOptions.bPath = cmdFilerSynchronize.Flag.String("b.path", "/", "directory to sync on filer B")
  51. syncOptions.aReplication = cmdFilerSynchronize.Flag.String("a.replication", "", "replication on filer A")
  52. syncOptions.bReplication = cmdFilerSynchronize.Flag.String("b.replication", "", "replication on filer B")
  53. syncOptions.aCollection = cmdFilerSynchronize.Flag.String("a.collection", "", "collection on filer A")
  54. syncOptions.bCollection = cmdFilerSynchronize.Flag.String("b.collection", "", "collection on filer B")
  55. syncOptions.aTtlSec = cmdFilerSynchronize.Flag.Int("a.ttlSec", 0, "ttl in seconds on filer A")
  56. syncOptions.bTtlSec = cmdFilerSynchronize.Flag.Int("b.ttlSec", 0, "ttl in seconds on filer B")
  57. syncOptions.aDiskType = cmdFilerSynchronize.Flag.String("a.disk", "", "[hdd|ssd|<tag>] hard drive or solid state drive or any tag on filer A")
  58. syncOptions.bDiskType = cmdFilerSynchronize.Flag.String("b.disk", "", "[hdd|ssd|<tag>] hard drive or solid state drive or any tag on filer B")
  59. syncOptions.aProxyByFiler = cmdFilerSynchronize.Flag.Bool("a.filerProxy", false, "read and write file chunks by filer A instead of volume servers")
  60. syncOptions.bProxyByFiler = cmdFilerSynchronize.Flag.Bool("b.filerProxy", false, "read and write file chunks by filer B instead of volume servers")
  61. syncOptions.aDebug = cmdFilerSynchronize.Flag.Bool("a.debug", false, "debug mode to print out filer A received files")
  62. syncOptions.bDebug = cmdFilerSynchronize.Flag.Bool("b.debug", false, "debug mode to print out filer B received files")
  63. syncCpuProfile = cmdFilerSynchronize.Flag.String("cpuprofile", "", "cpu profile output file")
  64. syncMemProfile = cmdFilerSynchronize.Flag.String("memprofile", "", "memory profile output file")
  65. }
  66. var cmdFilerSynchronize = &Command{
  67. UsageLine: "filer.sync -a=<oneFilerHost>:<oneFilerPort> -b=<otherFilerHost>:<otherFilerPort>",
  68. Short: "resumeable continuous synchronization between two active-active or active-passive SeaweedFS clusters",
  69. Long: `resumeable continuous synchronization for file changes between two active-active or active-passive filers
  70. filer.sync listens on filer notifications. If any file is updated, it will fetch the updated content,
  71. and write to the other destination. Different from filer.replicate:
  72. * filer.sync only works between two filers.
  73. * filer.sync does not need any special message queue setup.
  74. * filer.sync supports both active-active and active-passive modes.
  75. If restarted, the synchronization will resume from the previous checkpoints, persisted every minute.
  76. A fresh sync will start from the earliest metadata logs.
  77. `,
  78. }
  79. func runFilerSynchronize(cmd *Command, args []string) bool {
  80. util.LoadConfiguration("security", false)
  81. grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.client")
  82. grace.SetupProfiling(*syncCpuProfile, *syncMemProfile)
  83. go func() {
  84. for {
  85. err := doSubscribeFilerMetaChanges(grpcDialOption, *syncOptions.filerA, *syncOptions.aPath, *syncOptions.aProxyByFiler, *syncOptions.filerB,
  86. *syncOptions.bPath, *syncOptions.bReplication, *syncOptions.bCollection, *syncOptions.bTtlSec, *syncOptions.bProxyByFiler, *syncOptions.bDiskType, *syncOptions.bDebug)
  87. if err != nil {
  88. glog.Errorf("sync from %s to %s: %v", *syncOptions.filerA, *syncOptions.filerB, err)
  89. time.Sleep(1747 * time.Millisecond)
  90. }
  91. }
  92. }()
  93. if !*syncOptions.isActivePassive {
  94. go func() {
  95. for {
  96. err := doSubscribeFilerMetaChanges(grpcDialOption, *syncOptions.filerB, *syncOptions.bPath, *syncOptions.bProxyByFiler, *syncOptions.filerA,
  97. *syncOptions.aPath, *syncOptions.aReplication, *syncOptions.aCollection, *syncOptions.aTtlSec, *syncOptions.aProxyByFiler, *syncOptions.aDiskType, *syncOptions.aDebug)
  98. if err != nil {
  99. glog.Errorf("sync from %s to %s: %v", *syncOptions.filerB, *syncOptions.filerA, err)
  100. time.Sleep(2147 * time.Millisecond)
  101. }
  102. }
  103. }()
  104. }
  105. select {}
  106. return true
  107. }
  108. func doSubscribeFilerMetaChanges(grpcDialOption grpc.DialOption, sourceFiler, sourcePath string, sourceReadChunkFromFiler bool, targetFiler, targetPath string,
  109. replicationStr, collection string, ttlSec int, sinkWriteChunkByFiler bool, diskType string, debug bool) error {
  110. // read source filer signature
  111. sourceFilerSignature, sourceErr := replication.ReadFilerSignature(grpcDialOption, sourceFiler)
  112. if sourceErr != nil {
  113. return sourceErr
  114. }
  115. // read target filer signature
  116. targetFilerSignature, targetErr := replication.ReadFilerSignature(grpcDialOption, targetFiler)
  117. if targetErr != nil {
  118. return targetErr
  119. }
  120. // if first time, start from now
  121. // if has previously synced, resume from that point of time
  122. sourceFilerOffsetTsNs, err := getOffset(grpcDialOption, targetFiler, SyncKeyPrefix, sourceFilerSignature)
  123. if err != nil {
  124. return err
  125. }
  126. glog.V(0).Infof("start sync %s(%d) => %s(%d) from %v(%d)", sourceFiler, sourceFilerSignature, targetFiler, targetFilerSignature, time.Unix(0, sourceFilerOffsetTsNs), sourceFilerOffsetTsNs)
  127. // create filer sink
  128. filerSource := &source.FilerSource{}
  129. filerSource.DoInitialize(sourceFiler, pb.ServerToGrpcAddress(sourceFiler), sourcePath, sourceReadChunkFromFiler)
  130. filerSink := &filersink.FilerSink{}
  131. filerSink.DoInitialize(targetFiler, pb.ServerToGrpcAddress(targetFiler), targetPath, replicationStr, collection, ttlSec, diskType, grpcDialOption, sinkWriteChunkByFiler)
  132. filerSink.SetSourceFiler(filerSource)
  133. persistEventFn := genProcessFunction(sourcePath, targetPath, filerSink, debug)
  134. processEventFn := func(resp *filer_pb.SubscribeMetadataResponse) error {
  135. message := resp.EventNotification
  136. for _, sig := range message.Signatures {
  137. if sig == targetFilerSignature && targetFilerSignature != 0 {
  138. fmt.Printf("%s skipping %s change to %v\n", targetFiler, sourceFiler, message)
  139. return nil
  140. }
  141. }
  142. return persistEventFn(resp)
  143. }
  144. processEventFnWithOffset := pb.AddOffsetFunc(processEventFn, 3 * time.Second, func(counter int64, lastTsNs int64) error {
  145. glog.V(0).Infof("sync %s to %s progressed to %v %0.2f/sec", sourceFiler, targetFiler, time.Unix(0, lastTsNs), float64(counter)/float64(3))
  146. return setOffset(grpcDialOption, targetFiler, SyncKeyPrefix, sourceFilerSignature, lastTsNs)
  147. })
  148. return pb.FollowMetadata(sourceFiler, grpcDialOption, "syncTo_" + targetFiler,
  149. sourcePath, sourceFilerOffsetTsNs, targetFilerSignature, processEventFnWithOffset, false)
  150. }
  151. const (
  152. SyncKeyPrefix = "sync."
  153. )
  154. func getOffset(grpcDialOption grpc.DialOption, filer string, signaturePrefix string, signature int32) (lastOffsetTsNs int64, readErr error) {
  155. readErr = pb.WithFilerClient(filer, grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
  156. syncKey := []byte(signaturePrefix + "____")
  157. util.Uint32toBytes(syncKey[len(signaturePrefix):len(signaturePrefix)+4], uint32(signature))
  158. resp, err := client.KvGet(context.Background(), &filer_pb.KvGetRequest{Key: syncKey})
  159. if err != nil {
  160. return err
  161. }
  162. if len(resp.Error) != 0 {
  163. return errors.New(resp.Error)
  164. }
  165. if len(resp.Value) < 8 {
  166. return nil
  167. }
  168. lastOffsetTsNs = int64(util.BytesToUint64(resp.Value))
  169. return nil
  170. })
  171. return
  172. }
  173. func setOffset(grpcDialOption grpc.DialOption, filer string, signaturePrefix string, signature int32, offsetTsNs int64) error {
  174. return pb.WithFilerClient(filer, grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
  175. syncKey := []byte(signaturePrefix + "____")
  176. util.Uint32toBytes(syncKey[len(signaturePrefix):len(signaturePrefix)+4], uint32(signature))
  177. valueBuf := make([]byte, 8)
  178. util.Uint64toBytes(valueBuf, uint64(offsetTsNs))
  179. resp, err := client.KvPut(context.Background(), &filer_pb.KvPutRequest{
  180. Key: syncKey,
  181. Value: valueBuf,
  182. })
  183. if err != nil {
  184. return err
  185. }
  186. if len(resp.Error) != 0 {
  187. return errors.New(resp.Error)
  188. }
  189. return nil
  190. })
  191. }
  192. func genProcessFunction(sourcePath string, targetPath string, dataSink sink.ReplicationSink, debug bool) func(resp *filer_pb.SubscribeMetadataResponse) error {
  193. // process function
  194. processEventFn := func(resp *filer_pb.SubscribeMetadataResponse) error {
  195. message := resp.EventNotification
  196. var sourceOldKey, sourceNewKey util.FullPath
  197. if message.OldEntry != nil {
  198. sourceOldKey = util.FullPath(resp.Directory).Child(message.OldEntry.Name)
  199. }
  200. if message.NewEntry != nil {
  201. sourceNewKey = util.FullPath(message.NewParentPath).Child(message.NewEntry.Name)
  202. }
  203. if debug {
  204. glog.V(0).Infof("received %v", resp)
  205. }
  206. if !strings.HasPrefix(resp.Directory, sourcePath) {
  207. return nil
  208. }
  209. // handle deletions
  210. if message.OldEntry != nil && message.NewEntry == nil {
  211. if !strings.HasPrefix(string(sourceOldKey), sourcePath) {
  212. return nil
  213. }
  214. key := buildKey(dataSink, message, targetPath, sourceOldKey, sourcePath)
  215. return dataSink.DeleteEntry(key, message.OldEntry.IsDirectory, message.DeleteChunks, message.Signatures)
  216. }
  217. // handle new entries
  218. if message.OldEntry == nil && message.NewEntry != nil {
  219. if !strings.HasPrefix(string(sourceNewKey), sourcePath) {
  220. return nil
  221. }
  222. key := buildKey(dataSink, message, targetPath, sourceNewKey, sourcePath)
  223. return dataSink.CreateEntry(key, message.NewEntry, message.Signatures)
  224. }
  225. // this is something special?
  226. if message.OldEntry == nil && message.NewEntry == nil {
  227. return nil
  228. }
  229. // handle updates
  230. if strings.HasPrefix(string(sourceOldKey), sourcePath) {
  231. // old key is in the watched directory
  232. if strings.HasPrefix(string(sourceNewKey), sourcePath) {
  233. // new key is also in the watched directory
  234. if !dataSink.IsIncremental() {
  235. oldKey := util.Join(targetPath, string(sourceOldKey)[len(sourcePath):])
  236. message.NewParentPath = util.Join(targetPath, message.NewParentPath[len(sourcePath):])
  237. foundExisting, err := dataSink.UpdateEntry(string(oldKey), message.OldEntry, message.NewParentPath, message.NewEntry, message.DeleteChunks, message.Signatures)
  238. if foundExisting {
  239. return err
  240. }
  241. // not able to find old entry
  242. if err = dataSink.DeleteEntry(string(oldKey), message.OldEntry.IsDirectory, false, message.Signatures); err != nil {
  243. return fmt.Errorf("delete old entry %v: %v", oldKey, err)
  244. }
  245. }
  246. // create the new entry
  247. newKey := buildKey(dataSink, message, targetPath, sourceNewKey, sourcePath)
  248. return dataSink.CreateEntry(newKey, message.NewEntry, message.Signatures)
  249. } else {
  250. // new key is outside of the watched directory
  251. if !dataSink.IsIncremental() {
  252. key := buildKey(dataSink, message, targetPath, sourceOldKey, sourcePath)
  253. return dataSink.DeleteEntry(key, message.OldEntry.IsDirectory, message.DeleteChunks, message.Signatures)
  254. }
  255. }
  256. } else {
  257. // old key is outside of the watched directory
  258. if strings.HasPrefix(string(sourceNewKey), sourcePath) {
  259. // new key is in the watched directory
  260. key := buildKey(dataSink, message, targetPath, sourceNewKey, sourcePath)
  261. return dataSink.CreateEntry(key, message.NewEntry, message.Signatures)
  262. } else {
  263. // new key is also outside of the watched directory
  264. // skip
  265. }
  266. }
  267. return nil
  268. }
  269. return processEventFn
  270. }
  271. func buildKey(dataSink sink.ReplicationSink, message *filer_pb.EventNotification, targetPath string, sourceKey util.FullPath, sourcePath string) (key string) {
  272. if !dataSink.IsIncremental() {
  273. key = util.Join(targetPath, string(sourceKey)[len(sourcePath):])
  274. } else {
  275. var mTime int64
  276. if message.NewEntry != nil {
  277. mTime = message.NewEntry.Attributes.Mtime
  278. } else if message.OldEntry != nil {
  279. mTime = message.OldEntry.Attributes.Mtime
  280. }
  281. dateKey := time.Unix(mTime, 0).Format("2006-01-02")
  282. key = util.Join(targetPath, dateKey, string(sourceKey)[len(sourcePath):])
  283. }
  284. return escapeKey(key)
  285. }