在之前介绍了redisshake的源码 今天就来详细说一下redisshake的主要流程(main.go)
首先是源码
func main() {
if len(os.Args) < 2 || len(os.Args) > 3 {
fmt.Println("Usage: redis-shake <config file> <filter file>")
fmt.Println("Example: redis-shake config.toml filter.lua")
os.Exit(1)
}
//检查命令行参数 看合不合规范
// load filter file
if len(os.Args) == 3 {
luaFile := os.Args[2]
filter.LoadFromFile(luaFile)
}
//参数为3 加载过滤器文件
// load config
configFile := os.Args[1]
config.LoadFromFile(configFile)
//加载配置文件
log.Init()
log.Infof("GOOS: %s, GOARCH: %s", runtime.GOOS, runtime.GOARCH)
log.Infof("Ncpu: %d, GOMAXPROCS: %d", config.Config.Advanced.Ncpu, runtime.GOMAXPROCS(0))
log.Infof("pid: %d", os.Getpid())
log.Infof("pprof_port: %d", config.Config.Advanced.PprofPort)
//初始化日志模块
if len(os.Args) == 2 {
log.Infof("No lua file specified, will not filter any cmd.")
}
//如果命令行参数个数为2,则表示没有指定Lua脚本文件,代码会输出一条日志信息。
// start pprof
if config.Config.Advanced.PprofPort != 0 {
go func() {
err := http.ListenAndServe(fmt.Sprintf("localhost:%d", config.Config.Advanced.PprofPort), nil)
if err != nil {
log.PanicError(err)
}
}()
}
//启动pprof性能分析服务器。
// start statistics
if config.Config.Advanced.MetricsPort != 0 {
statistics.Metrics.Address = config.Config.Source.Address
go func() {
log.Infof("metrics url: http://localhost:%d", config.Config.Advanced.MetricsPort)
mux := http.NewServeMux()
mux.HandleFunc("/", statistics.Handler)
err := http.ListenAndServe(fmt.Sprintf("localhost:%d", config.Config.Advanced.MetricsPort), mux)
if err != nil {
log.PanicError(err)
}
}()
}
//启动metrics服务器,并提供收集Redis数据同步信息的接口。
// create writer
var theWriter writer.Writer
target := &config.Config.Target
switch config.Config.Target.Type {
case "standalone":
theWriter = writer.NewRedisWriter(target.Address, target.Username, target.Password, target.IsTLS)
case "cluster":
theWriter = writer.NewRedisClusterWriter(target.Address, target.Username, target.Password, target.IsTLS)
default:
log.Panicf("unknown target type: %s", target.Type)
}
//创建一个写入器,可以是独立模式的Redis或集群模式的Redis。
// create reader
source := &config.Config.Source
var theReader reader.Reader
if config.Config.Type == "sync" {
theReader = reader.NewPSyncReader(source.Address, source.Username, source.Password, source.IsTLS, source.ElastiCachePSync)
} else if config.Config.Type == "restore" {
theReader = reader.NewRDBReader(source.RDBFilePath)
} else if config.Config.Type == "scan" {
theReader = reader.NewScanReader(source.Address, source.Username, source.Password, source.IsTLS)
} else {
log.Panicf("unknown source type: %s", config.Config.Type)
}
ch := theReader.StartRead()
//创建一个读取器,可以是PSync、Restore或Scan
//创建一个通道(ch)用于接收读取到的数据
// start sync
statistics.Init()
//初始化统计信息
id := uint64(0)
//进入一个循环,不断从通道(ch)中接收读取到的数据
for e := range ch {
statistics.UpdateInQueueEntriesCount(uint64(len(ch)))
// calc arguments
e.Id = id
id++
e.CmdName, e.Group, e.Keys = commands.CalcKeys(e.Argv)
e.Slots = commands.CalcSlots(e.Keys)
// filter
code := filter.Filter(e)
statistics.UpdateEntryId(e.Id)
if code == filter.Allow {
theWriter.Write(e)
statistics.AddAllowEntriesCount()
} else if code == filter.Disallow {
// do something
statistics.AddDisallowEntriesCount()
} else {
log.Panicf("error when run lua filter. entry: %s", e.ToString())
}
}
theWriter.Close()
log.Infof("finished.")
}
整体步骤:
-
检查命令行参数,符不符合规范
-
加载过滤器文件
-
加载配置文件
-
初始化日志模块
-
如果命令行参数个数为2,则表示没有指定Lua脚本文件
-
如果有pprof端口,则启动pprof服务器
-
如果有metrics端口,则启动metrics服务器,并提供收集Redis数据同步信息的接口
-
创建一个写入器,可以是独立模式或集群模式
-
创建一个读取器,PSync、Restore或Scan
-
theReader的StartRead,返回一个通道(ch)用于接收读取到的数据
-
初始化统计信息(statistics)
-
进入一个循环,不断从通道(ch)中接收读取到的数据,并处理数据
目录
一.检查命令行参数
if len(os.Args) < 2 || len(os.Args) > 3 {
fmt.Println("Usage: redis-shake <config file> <filter file>")
fmt.Println("Example: redis-shake config.toml filter.lua")
os.Exit(1)
}
这里限定Args的个数为3
这里os.Args[1]为配置文件
os.Arg[2]为过滤文件
程序需要两个参数:一个配置文件和一个可选的过滤文件。如果参数数量不符合这个要求,程序将直接退出。
二.加载过滤器文件
if len(os.Args) == 3 {
luaFile := os.Args[2]
filter.LoadFromFile(luaFile)
}
func LoadFromFile(luaFile string) {
luaInstance = lua.NewState()
err := luaInstance.DoFile(luaFile)
if err != nil {
panic(err)
}
}
这里的加载文件没什么好说的 加载 Lua 代码,存到一个 Lua 实例
三.加载配置文件
configFile := os.Args[1]
config.LoadFromFile(configFile)
func LoadFromFile(filename string) {
buf, err := ioutil.ReadFile(filename)
if err != nil {
panic(err.Error())
}
decoder := toml.NewDecoder(bytes.NewReader(buf))
decoder.SetStrict(true)
err = decoder.Decode(&Config)
if err != nil {
missingError, ok := err.(*toml.StrictMissingError)
if ok {
panic(fmt.Sprintf("decode config error:\n%s", missingError.String()))
}
panic(err.Error())
}
// dir
err = os.MkdirAll(Config.Advanced.Dir, os.ModePerm)
if err != nil {
panic(err.Error())
}
err = os.Chdir(Config.Advanced.Dir)
if err != nil {
panic(err.Error())
}
//通过 os.MkdirAll() 进行目录的创建,以及通过 os.Chdir() 设置工作目录。
// cpu core
var ncpu int
if Config.Advanced.Ncpu == 0 {
ncpu = runtime.NumCPU()
} else {
ncpu = Config.Advanced.Ncpu
}
runtime.GOMAXPROCS(ncpu)
//通过 runtime.GOMAXPROCS() 函数设置并发执行环境中的 CPU 核心数量。
if Config.Source.Version < 2.8 {
panic("source redis version must be greater than 2.8")
}
if Config.Target.Version < 2.8 {
panic("target redis version must be greater than 2.8")
}
if Config.Type != "sync" && Config.Type != "restore" && Config.Type != "scan" {
panic("type must be sync/restore/scan")
}
}
1.解析TOML文件
decoder := toml.NewDecoder(bytes.NewReader(buf))
decoder.SetStrict(true)
err = decoder.Decode(&Config)
if err != nil {
missingError, ok := err.(*toml.StrictMissingError)
if ok {
panic(fmt.Sprintf("decode config error:\n%s", missingError.String()))
}
panic(err.Error())
}
读取buf后 将文件内容buf
解码到Config
结构体中
由于本人才疏学浅 没用过TOML文件 这里百度一下
“TOML,全称Tom’s Obvious, Minimal Language,是一个易于阅读和编写的最小化配置文件格式,由Tom Preston-Werner创建。它设计为清晰无歧义,并且映射到哈希表。这使得它对于配置文件、配置数据、交换数据等用途都非常有用。”
2.创建并使用目录
err = os.MkdirAll(Config.Advanced.Dir, os.ModePerm)
if err != nil {
panic(err.Error())
}
err = os.Chdir(Config.Advanced.Dir)
if err != nil {
panic(err.Error())
}
使用os.MkdirAll
创建目录,
然后使用os.Chdir
将程序的工作目录改为刚刚创建的目录。
3.设置CPU核心数量
// cpu core
var ncpu int
if Config.Advanced.Ncpu == 0 {
ncpu = runtime.NumCPU()
} else {
ncpu = Config.Advanced.Ncpu
}
runtime.GOMAXPROCS(ncpu)
设置Go运行环境可以使用的最大CPU核心数量。
既可以充分利用机器的CPU资源,也允许用户通过配置来限制CPU利用率,避免程序过度占用系统资源...
4.检查Redis版本
if Config.Source.Version < 2.8 {
panic(“source redis version must be greater than 2.8”)
}
if Config.Target.Version < 2.8 {
panic(“target redis version must be greater than 2.8”)
}
无论代码检查源Redis还是目标Redis的版本,不允许任一版本低于2.8
5.检查配置类型
if Config.Type != "sync" && Config.Type != "restore" && Config.Type != "scan" {
panic("type must be sync/restore/scan")
}
Config.Type
必须是"sync",“restore”,或"scan"中的一个 毕竟这是redisshake的特色之一
四.初始化日志模块
log.Init()
log.Infof("GOOS: %s, GOARCH: %s", runtime.GOOS, runtime.GOARCH)
log.Infof("Ncpu: %d, GOMAXPROCS: %d", config.Config.Advanced.Ncpu, runtime.GOMAXPROCS(0))
log.Infof("pid: %d", os.Getpid())
log.Infof("pprof_port: %d", config.Config.Advanced.PprofPort)
CPU 核心数 输出程序进程的 ID(PID) Pprof 的端口号...
五.如果有pprof端口,则启动pprof服务器
// start pprof
if config.Config.Advanced.PprofPort != 0 {
go func() {
err := http.ListenAndServe(fmt.Sprintf("localhost:%d", config.Config.Advanced.PprofPort), nil)
if err != nil {
log.PanicError(err)
}
}()
}
http.ListenAndServe
创建一个服务器,监听。函数的第一个参数是监听的地址
第二个参数为处理 HTTP 请求的处理器
但是本人也没用过pprof端口(可怜) 百度了一下
“启动服务器后,pprof 会开始收集并提供关于程序性能和调试的信息,例如堆栈跟踪、内存分配、goroutine 等。这样,开发人员可以通过访问指定的端口来调用和分析这些信息,从而进行性能分析和优化。”
六.如果有metrics端口,则启动metrics服务器
// start statistics
if config.Config.Advanced.MetricsPort != 0 {
statistics.Metrics.Address = config.Config.Source.Address
go func() {
log.Infof("metrics url: http://localhost:%d", config.Config.Advanced.MetricsPort)
mux := http.NewServeMux()
mux.HandleFunc("/", statistics.Handler)
err := http.ListenAndServe(fmt.Sprintf("localhost:%d", config.Config.Advanced.MetricsPort), mux)
if err != nil {
log.PanicError(err)
}
}()
}
首先如果 MetricsPort
不为零,表示需要启用该服务。
接下来将 Address
赋值给 statistics.Metrics.Address
。这个地址通常被用于获取指标数据,作为指标服务的目标地址。
然后是使用匿名函数和 goroutine(不会阻塞主程序的执行):
1.log.Infof
打印一条日志信息,表示指标服务的 URL。
2.创建一个新的路由器http.NewServeMux()
作为 HTTP 请求的处理器。
3.使用 mux.HandleFunc绑定路径
所有该路径的请求将由 statistics.Handler
函数处理。
4.http.ListenAndServe
函数来创建一个服务器,监听指定端口 第一个参数是监听的地址 第二个参数是之前创建的路由器
百度了一下metrics服务
“监听 HTTP 请求,通过访问相应的 URL,可以获取与程序性能相关的指标数据。”
然后这里还出现了statistics这个包 也是redisshake的.go文件
结合源码可知 该包其实就是统计Redis 同步服务的各种指标
func Handler(w http.ResponseWriter, _ *http.Request) {
w.Header().Add("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(Metrics)
if err != nil {
log.PanicError(err)
}
}
用于将 metrics 结构体以 JSON 格式输出到 HTTP 响应中。
七.创建写入器
// create writer
var theWriter writer.Writer
target := &config.Config.Target
switch config.Config.Target.Type {
case "standalone":
theWriter = writer.NewRedisWriter(target.Address, target.Username, target.Password, target.IsTLS)
case "cluster":
theWriter = writer.NewRedisClusterWriter(target.Address, target.Username, target.Password, target.IsTLS)
default:
log.Panicf("unknown target type: %s", target.Type)
}
分单机和集群模式 当然肯定要看它这么创建的 这里用NewRedisWriter举例
type redisWriter struct {
client *client.Redis
DbId int
cmdBuffer *bytes.Buffer
chWaitReply chan *entry.Entry
chWg sync.WaitGroup
UpdateUnansweredBytesCount uint64 // have sent in bytes
}
func NewRedisWriter(address string, username string, password string, isTls bool) Writer {
rw := new(redisWriter)
rw.client = client.NewRedisClient(address, username, password, isTls)
log.Infof("redisWriter connected to redis successful. address=[%s]", address)
rw.cmdBuffer = new(bytes.Buffer)
rw.chWaitReply = make(chan *entry.Entry, config.Config.Advanced.PipelineCountLimit)
rw.chWg.Add(1)
go rw.flushInterval()
return rw
}
我们看到这里redisWriter结构体里面有 client 以及下面函数都使用了client 所以先讲client
1.redisclient
我们要创建一个客户端 方便我们与redis服务器通信
type Redis struct {
reader *bufio.Reader
writer *bufio.Writer
protoReader *proto.Reader
protoWriter *proto.Writer
}
4个读写器 分别用于从 Redis 连接中读写数据 用于解析从 Redis 连接读写的数据,并进行协议处理。
func NewRedisClient(address string, username string, password string, isTls bool) *Redis { //创建新的Redis客户端
r := new(Redis)
//网络连接接口和网络拨号器,
var conn net.Conn
var dialer net.Dialer
var err error
//设置连接超时时间
dialer.Timeout = 3 * time.Second
if isTls {
//创建网络链接并判断是否使用TLS连接
conn, err = tls.DialWithDialer(&dialer, "tcp", address, &tls.Config{InsecureSkipVerify: true})
} else {
conn, err = dialer.Dial("tcp", address)
}
if err != nil {
log.PanicError(err)
}
//初始化Redis读写器(与conn绑定
r.reader = bufio.NewReader(conn)
r.writer = bufio.NewWriter(conn)
r.protoReader = proto.NewReader(r.reader)
r.protoWriter = proto.NewWriter(r.writer)
// auth
//进行Redis授权
if password != "" {
var reply string
if username != "" {
reply = r.DoWithStringReply("auth", username, password)
} else {
reply = r.DoWithStringReply("auth", password)
}
if reply != "OK" {
log.Panicf("auth failed with reply: %s", reply)
}
log.Infof("auth successful. address=[%s]", address)
} else {
log.Infof("no password. address=[%s]", address)
}
// ping to test connection
//测试连接
reply := r.DoWithStringReply("ping")
if reply != "PONG" {
panic("ping failed with reply: " + reply)
}
return r
}
new(Redis)
创建一个新的Redis实例
定义了conn
和dialer
,conn
是一个网络连接接口,dialer
是一个网络拨号器,用于建立网络连接。
设置连接超时时间 超时就停止尝试链接
判断是否使用TLS连接 绑定地址
使用bufio.NewReader
和bufio.NewWriter
创建了两个用于读写网络数据的缓冲区,并根据这个缓冲区创建协议读写器,用于读写Redis协议的数据 绑定conn
进行Redis授权
测试连接,返回结果
这段代码就是创建了一个绑定了conn和地址的redis客户端 而且初始化的读写器 那我们继续看看rediswritter
2.rediswriter
type redisWriter struct {
client *client.Redis
DbId int
cmdBuffer *bytes.Buffer
chWaitReply chan *entry.Entry
chWg sync.WaitGroup
UpdateUnansweredBytesCount uint64 // have sent in bytes
}
client 刚刚介绍了
DbId
一个整数字段,记录写入数据时使用的 Redis 数据库的 ID
cmdBuffer
用于缓冲待发送的 Redis 命令
chWaitReply
用于等待 Redis 命令的回复
chWg
用于等待 goroutine 完成特定任务的同步操作
UpdateUnansweredBytesCount
记录已发送但尚未得到回复的字节数
sync.WaitGroup
提供以下三个函数
Add(delta int)
:用于将计数器增加 计数,表示要等待的任务数量增加了。
Done()
:用于将计数器减少 1,表示一个任务已完成。
Wait()
:阻塞调用的goroutine,直到计数器减少为 0。这意味着所有的任务都已完成。
总的来说 这个结构体的类型配合使用,可以实现与 Redis 服务器进行写入操作并等待回复的功能。
func NewRedisWriter(address string, username string, password string, isTls bool) Writer {
rw := new(redisWriter)
rw.client = client.NewRedisClient(address, username, password, isTls)
log.Infof("redisWriter connected to redis successful. address=[%s]", address)
rw.cmdBuffer = new(bytes.Buffer)
rw.chWaitReply = make(chan *entry.Entry, config.Config.Advanced.PipelineCountLimit)
rw.chWg.Add(1)
go rw.flushInterval()
return rw
}
首先,创建了一个新的 redisWriter
结构体实例,并将其赋值给变量 rw
。
通过 传入地址、用户名、密码和 TLS 信息,创建一个 Redis 客户端实例。
使用 new
创建一个新的 bytes.Buffer
实例,并将其赋值给 rw.cmdBuffer
字段。用于缓冲待发送的 Redis 命令。
make
创建一个带有容量限制的通道 rw.chWaitReply
,该通道用于接收 Redis 命令的回复。
调用rw.chWg.Add(1)
将任务计数器增加 1,以表示有一个任务需要在后台 goroutine 中完成
go rw.flushInterval()
用于定期刷新数据到 Redis 服务器。
最后返回 rw,作为 Writer
接口
这就是创建写入器的流程
八.创建读取器
source := &config.Config.Source
var theReader reader.Reader
if config.Config.Type == "sync" {
theReader = reader.NewPSyncReader(source.Address, source.Username, source.Password, source.IsTLS, source.ElastiCachePSync)
} else if config.Config.Type == "restore" {
theReader = reader.NewRDBReader(source.RDBFilePath)
} else if config.Config.Type == "scan" {
theReader = reader.NewScanReader(source.Address, source.Username, source.Password, source.IsTLS)
} else {
log.Panicf("unknown source type: %s", config.Config.Type)
}
ch := theReader.StartRead()
根据配置文件中的类型选择相应的读取器,并使用读取器开始读取数据,并将读取的数据发送到通道 ch
(对应redisshake的三大类型)
以NewPSyncReader为例
sync
首先看代码
type psyncReader struct {
client *client.Redis
address string
ch chan *entry.Entry
DbId int
rd *bufio.Reader
receivedOffset int64
elastiCachePSync string
}
client 表示与 Redis 服务器进行通信的客户端 前面提到过
address
表示要连接的 Redis 服务器的地址
ch
用于接收读取到的 Redis 数据
DbId
读取数据时要使用的 Redis 数据库的 ID
rd
用于从 Redis 服务器进行数据读取。而且它提供了缓冲读取功能
receivedOffset
用于记录已接收的数据的偏移量。它表示从 Redis 服务器接收到的数据的位置
elastiCachePSync
雀氏不知道什么东西 查了一下 用于标识是否使用 Elasticache 的 PSYNC 协议?“elastiCache
是指亚马逊 AWS 云平台上提供的一项托管式 Redis 服务。”
总的来说 创建了一个具有与 Redis 服务器进行同步读取数据的能力的读取器。
func NewPSyncReader(address string, username string, password string, isTls bool, ElastiCachePSync string) Reader {
r := new(psyncReader)
r.address = address
r.elastiCachePSync = ElastiCachePSync
r.client = client.NewRedisClient(address, username, password, isTls)
r.rd = r.client.BufioReader()
log.Infof("psyncReader connected to redis successful. address=[%s]", address)
return r
}
绑定address ElastiCachePSync(上面提到过)
调用 BufioReader
,获取一个 bufio.Reader
对象 用来读取
总的来说 创建一个具有读取Redis服务器数据能力的psyncReader对象
然后就是启动 读取器
func (r *psyncReader) StartRead() chan *entry.Entry {
r.ch = make(chan *entry.Entry, 1024)
go func() {
r.clearDir()
go r.sendReplconfAck()
r.saveRDB()
startOffset := r.receivedOffset
go r.saveAOF(r.rd)
r.sendRDB()
time.Sleep(1 * time.Second) // wait for saveAOF create aof file
r.sendAOF(startOffset)
}()
return r.ch
}
创建一个容量为 1024r.ch
缓冲通道,用于接收读取到的 Redis 数据。
启动一个 goroutine
r.clearDir()
: 清理相关目录,准备接收数据。
sendReplconfAck():用于向主节点发送确认信息(ack)。
saveRDB():用于读取 Redis RDB 文件并发送给 r.ch 通道。
saveAOF:接收 Redis 服务器发送的 AOF数据,并将其保存到本地磁盘上。毕竟现版本并不支持AOF恢复,只是直接保存整个文件
sendRDB:用于发送 RDB 数据到从节点。
sendAOF:从Redis服务器读取数据 然后将这个对象送到通道 发送出去(当前版本不支持AOF读取,所以肯定不是redisshake解析的AOF)
这就是启动一个后台任务来读取 Redis 数据的过程
接下来是刚刚各个函数 不想看可以直接跳过
func (r *psyncReader) clearDir() {
files, err := ioutil.ReadDir("./")
//ioutil.ReadDir("./") 函数读取当前目录下的所有文件和子目录
if err != nil {
log.PanicError(err)
}
for _, f := range files {//确定当前文件是否以 “.rdb” 或 “.aof” 结尾
if strings.HasSuffix(f.Name(), ".rdb") || strings.HasSuffix(f.Name(), ".aof") {
err = os.Remove(f.Name())//如果是“.rdb” 或 “.aof” 就remove os.Remove(f.Name())
if err != nil {
log.PanicError(err)
}
log.Warnf("remove file. filename=[%s]", f.Name())
}
}
}
//清理旧数据,确保收到的数据是最新的
func (r *psyncReader) sendReplconfAck() {
for range time.Tick(time.Millisecond * 100) {
//每 100 毫秒执行一次下面的代码块
// send ack receivedOffset
r.client.Send("replconf", "ack", strconv.FormatInt(r.receivedOffset, 10))
//通过发送 ACK 并传递数据偏移量,表示成功接收并处理了到达的数据
}
}
func (r *psyncReader) saveRDB() {
log.Infof("start save RDB. address=[%s]", r.address)
//创建一个字符串,包含要传递给Redis客户端的参数,监听端口为10007
argv := []string{"replconf", "listening-port", "10007"} // 10007 is magic number
log.Infof("send %v", argv)
reply := r.client.DoWithStringReply(argv...)
//使用Redis客户端发送命令,并得到命令执行的回复结果。
if reply != "OK" {
log.Warnf("send replconf command to redis server failed. address=[%s], reply=[%s], error=[]", r.address, reply)
}
// send psync
argv = []string{"PSYNC", "?", "-1"}
//PSYNC命令和参数
if r.elastiCachePSync != "" {
argv = []string{r.elastiCachePSync, "?", "-1"}
}
r.client.Send(argv...)
//使用Redis客户端发送PSYNC命令
log.Infof("send %v", argv)
// format: \n\n\n$<reply>\r\n
//一个无限循环,用于接收PSYNC命令的回复
for true {
// \n\n\n$
b, err := r.rd.ReadByte()
if err != nil {
log.PanicError(err)
}
if b == '\n' {
continue
}
if b == '-' {//减号 失败
reply, err := r.rd.ReadString('\n')
if err != nil {
log.PanicError(err)
}
reply = strings.TrimSpace(reply)
log.Panicf("psync error. address=[%s], reply=[%s]", r.address, reply)
}
if b != '+' {//不是加号 回复无效
log.Panicf("invalid psync reply. address=[%s], b=[%s]", r.address, string(b))
}
break
}
reply, err := r.rd.ReadString('\n')
//从读取器 r.rd 中读取一行回复结果
if err != nil {
log.PanicError(err)
}
reply = strings.TrimSpace(reply)
//移除回复结果中的首尾空白字符
log.Infof("receive [%s]", reply)
masterOffset, err := strconv.Atoi(strings.Split(reply, " ")[2])
//从回复结果中解析出主服务器的偏移量
if err != nil {
log.PanicError(err)
}
r.receivedOffset = int64(masterOffset)
//设置偏移量
log.Infof("source db is doing bgsave. address=[%s]", r.address)
statistics.Metrics.IsDoingBgsave = true
timeStart := time.Now()
//记录开始执行后台保存操作的时间
// format: \n\n\n$<length>\r\n<rdb>
for true {
//用于读取RDB数据
// \n\n\n$
b, err := r.rd.ReadByte()
if err != nil {
log.PanicError(err)
}
if b == '\n' {
continue
}
if b != '$' {
//RDB数据格式无效
log.Panicf("invalid rdb format. address=[%s], b=[%s]", r.address, string(b))
}
break
}
statistics.Metrics.IsDoingBgsave = false //后台操作完成
log.Infof("source db bgsave finished. timeUsed=[%.2f]s, address=[%s]", time.Since(timeStart).Seconds(), r.address)
lengthStr, err := r.rd.ReadString('\n')
//从读取器 r.rd 中读取RDB数据的长度
if err != nil {
log.PanicError(err)
}
lengthStr = strings.TrimSpace(lengthStr)
length, err := strconv.ParseInt(lengthStr, 10, 64)
//将长度字符串解析为64位有符号整数
if err != nil {
log.PanicError(err)
}
log.Infof("received rdb length. length=[%d]", length)
statistics.SetRDBFileSize(uint64(length))
//印收到的RDB数据长度
// create rdb file
rdbFilePath := "dump.rdb"
//定义RDB文件的路径和名称为 dump.rdb
log.Infof("create dump.rdb file. filename_path=[%s]", rdbFilePath)
rdbFileHandle, err := os.OpenFile(rdbFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
log.PanicError(err)
}
// read rdb
remainder := length
const bufSize int64 = 32 * 1024 * 1024 // 32MB
buf := make([]byte, bufSize)
for remainder != 0 {
readOnce := bufSize
//确定每次读取的字节数
if remainder < readOnce {
readOnce = remainder
//如果剩余的数据长度小于每次读取的字节数,则修改 readOnce 为剩余的数据长度
}
n, err := r.rd.Read(buf[:readOnce])
//从读取器中读取 readOnce 字节的数据,并返回实际读取的字节数
if err != nil {
log.PanicError(err)
}
remainder -= int64(n)
//更新剩余数据的长度
statistics.UpdateRDBReceivedSize(uint64(length - remainder))
//更新收到的RDB数据的大小
_, err = rdbFileHandle.Write(buf[:n])
将读取的数据写入RDB文件
if err != nil {
log.PanicError(err)
}
}
err = rdbFileHandle.Close()
if err != nil {
log.PanicError(err)
}
log.Infof("save RDB finished. address=[%s], total_bytes=[%d]", r.address, length)
}
func (r *psyncReader) saveAOF(rd io.Reader) {
log.Infof("start save AOF. address=[%s]", r.address)
// create aof file
aofWriter := rotate.NewAOFWriter(r.receivedOffset)
//r.receivedOffset 作为起始偏移量
defer aofWriter.Close()
//defer确保会关闭
buf := make([]byte, 16*1024) // 16KB is enough for writing file
for {
//读取数据并将其写入AOF文件
n, err := rd.Read(buf)
// 读取数据,将读取的字节数存储在 n
if err != nil {
log.PanicError(err)
}
r.receivedOffset += int64(n)
//更新 r.receivedOffset,增加读取的字节数
statistics.UpdateAOFReceivedOffset(uint64(r.receivedOffset))
aofWriter.Write(buf[:n])
//将读取的数据写入AOF文件
}
}
单纯的保存文件
func (r *psyncReader) sendRDB() {
// start parse rdb
log.Infof("start send RDB. address=[%s]", r.address)
rdbLoader := rdb.NewLoader("dump.rdb", r.ch)
r.DbId = rdbLoader.ParseRDB()
log.Infof("send RDB finished. address=[%s], repl-stream-db=[%d]", r.address, r.DbId)
}
praseRDB是redisshake读取rdb的核心代码这里就不介绍了
func (r *psyncReader) sendAOF(offset int64) {
aofReader := rotate.NewAOFReader(offset)
defer aofReader.Close()
r.client.SetBufioReader(bufio.NewReader(aofReader))
for {
argv := client.A rrayString(r.client.Receive())
// select
if strings.EqualFold(argv[0], "select") {
DbId, err := strconv.Atoi(argv[1])
if err != nil {
log.PanicError(err)
}
r.DbId = DbId
continue
}
e := entry.NewEntry()
e.Argv = argv
e.DbId = r.DbId
e.Offset = aofReader.Offset()
r.ch <- e
}
}
很直观
九.初始化统计信息(statistics)
statistics.Init()
statistics 提供关于执行任务进度和状态的信息 redis源码也有类似的实现
unc Init() {
go func() {
seconds := config.Config.Advanced.LogInterval
//从配置文件中获取日志记录间隔时间
if seconds <= 0 {
//检查统计功能被禁用
log.Infof("statistics disabled. seconds=[%d]", seconds)
}
lastAllowEntriesCount := Metrics.AllowEntriesCount
lastDisallowEntriesCount := Metrics.DisallowEntriesCount
// 分别用于保存上一次允许操作和禁止操作计数的值
for range time.Tick(time.Duration(seconds) * time.Second) {
//无限循环,每隔一定时间触发一次
//根据当前类型执行
// scan
if config.Config.Type == "scan" {
Metrics.Msg = fmt.Sprintf("syncing. dbId=[%d], percent=[%.2f]%%, allowOps=[%.2f], disallowOps=[%.2f], entryId=[%d], InQueueEntriesCount=[%d], unansweredBytesCount=[%d]bytes",
Metrics.ScanDbId,
float64(bits.Reverse64(Metrics.ScanCursor))/float64(^uint(0))*100,
float32(Metrics.AllowEntriesCount-lastAllowEntriesCount)/float32(seconds),
float32(Metrics.DisallowEntriesCount-lastDisallowEntriesCount)/float32(seconds),
Metrics.EntryId,
Metrics.InQueueEntriesCount,
Metrics.UnansweredBytesCount)
log.Infof(strings.Replace(Metrics.Msg, "%", "%%", -1))
lastAllowEntriesCount = Metrics.AllowEntriesCount
lastDisallowEntriesCount = Metrics.DisallowEntriesCount
continue
}
// sync or restore
if Metrics.RdbFileSize == 0 {
Metrics.Msg = "source db is doing bgsave"
} else if Metrics.RdbSendSize > Metrics.RdbReceivedSize {
Metrics.Msg = fmt.Sprintf("receiving rdb. percent=[%.2f]%%, rdbFileSize=[%.3f]G, rdbReceivedSize=[%.3f]G",
float64(Metrics.RdbReceivedSize)/float64(Metrics.RdbFileSize)*100,
float64(Metrics.RdbFileSize)/1024/1024/1024,
float64(Metrics.RdbReceivedSize)/1024/1024/1024)
} else if Metrics.RdbFileSize > Metrics.RdbSendSize {
Metrics.Msg = fmt.Sprintf("syncing rdb. percent=[%.2f]%%, allowOps=[%.2f], disallowOps=[%.2f], entryId=[%d], InQueueEntriesCount=[%d], unansweredBytesCount=[%d]bytes, rdbFileSize=[%.3f]G, rdbSendSize=[%.3f]G",
float64(Metrics.RdbSendSize)*100/float64(Metrics.RdbFileSize),
float32(Metrics.AllowEntriesCount-lastAllowEntriesCount)/float32(seconds),
float32(Metrics.DisallowEntriesCount-lastDisallowEntriesCount)/float32(seconds),
Metrics.EntryId,
Metrics.InQueueEntriesCount,
Metrics.UnansweredBytesCount,
float64(Metrics.RdbFileSize)/1024/1024/1024,
float64(Metrics.RdbSendSize)/1024/1024/1024)
} else {
Metrics.Msg = fmt.Sprintf("syncing aof. allowOps=[%.2f], disallowOps=[%.2f], entryId=[%d], InQueueEntriesCount=[%d], unansweredBytesCount=[%d]bytes, diff=[%d], aofReceivedOffset=[%d], aofAppliedOffset=[%d]",
float32(Metrics.AllowEntriesCount-lastAllowEntriesCount)/float32(seconds),
float32(Metrics.DisallowEntriesCount-lastDisallowEntriesCount)/float32(seconds),
Metrics.EntryId,
Metrics.InQueueEntriesCount,
Metrics.UnansweredBytesCount,
Metrics.AofReceivedOffset-Metrics.AofAppliedOffset,
Metrics.AofReceivedOffset,
Metrics.AofAppliedOffset)
}
log.Infof(strings.Replace(Metrics.Msg, "%", "%%", -1))
// 打印统计信息的日志消息,其中将百分号 “%” 替换为 “%%” 以避免格式化参数的错误(这个应该都知道吧
lastAllowEntriesCount = Metrics.AllowEntriesCount
lastDisallowEntriesCount = Metrics.DisallowEntriesCount
}
}()
}
十.不断从ch中接收读取到的数据,并处理
id := uint64(0)
for e := range ch {
statistics.UpdateInQueueEntriesCount(uint64(len(ch)))
// calc arguments
e.Id = id
id++
e.CmdName, e.Group, e.Keys = commands.CalcKeys(e.Argv)
e.Slots = commands.CalcSlots(e.Keys)
// filter
code := filter.Filter(e)
statistics.UpdateEntryId(e.Id)
if code == filter.Allow {
theWriter.Write(e)
statistics.AddAllowEntriesCount()
} else if code == filter.Disallow {
// do something
statistics.AddDisallowEntriesCount()
} else {
log.Panicf("error when run lua filter. entry: %s", e.ToString())
}
}
theWriter.Close()
log.Infof("finished.")
}
首先一直从管道ch中读取事件e
statistics.UpdateInQueueEntriesCount(uint64(len(ch)))
:更新待处理的事件数统计量
e.Id = id
每一事件的标识为id
id++
计算e的Slots
code := filter.Filter(e)
进行过滤
如果事件被允许,把事件写入,统计
如果事件被不允许,那么不会写入,统计
那么将会报告一个错误,因为过滤结果无法识别
更新统计信息中记录的最新处理的事件id
由此可见 redisshake处理数据 主要靠从ch通道里读取
再来看看过滤是怎么做的
func Filter(e *entry.Entry) int {
if luaInstance == nil {
return Allow
}
keys := luaInstance.NewTable()
//将e.Keys和e.Slots中的每一个元素分别添加至keys和slots Lua表。
for _, key := range e.Keys {
keys.Append(lua.LString(key))
}
slots := luaInstance.NewTable()
for _, slot := range e.Slots {
slots.Append(lua.LNumber(slot))
}
f := luaInstance.GetGlobal("filter")
luaInstance.Push(f)
//获取全局函数filter 并压栈
luaInstance.Push(lua.LNumber(e.Id)) // id
luaInstance.Push(lua.LBool(e.IsBase)) // is_base
luaInstance.Push(lua.LString(e.Group)) // group
luaInstance.Push(lua.LString(e.CmdName)) // cmd name
luaInstance.Push(keys) // keys
luaInstance.Push(slots) // slots
luaInstance.Push(lua.LNumber(e.DbId)) // dbid
luaInstance.Push(lua.LNumber(e.TimestampMs)) // timestamp_ms
//将entry中的各个属性压入到Lua实例的栈中
luaInstance.Call(8, 2)
code := int(luaInstance.Get(1).(lua.LNumber))
e.DbId = int(luaInstance.Get(2).(lua.LNumber))
//这两行代码取出filter函数的返回值
luaInstance.Pop(2)
//清空Lua实例的栈。
return code
}
这个代码首先判断Lua实例是否存在 这在这篇文章目录二里面已经加载了
之所以要用栈 压入到Lua实例的栈中,处理结束之后,再从栈中取出改动后的数据,这样操作很简洁