预写日志
意义
当存储核心接收到对数据记录的变更请求时不会立刻修改数据文件内容,而是将变更内容记录到预写日志中。
这是因为预写日志将存储核心对于数据文件的随机写转化为对日志文件顺序写,从而大幅提高写文件性能。
特性
- 支持原子写入、截断日志文件。承诺日志要 么完全写入文件,要么完全不写,不会出现写入文件一半数据的情况。
- 已经打开的日志实例会对日志目录加锁确保其他日志实例不会重入,避免预期外的错误发生。
- 提供灵活的配置能力,包括权限、缓存容量、数据文件大小、持久化级别。
概念
- Log:预写日志实体,由多个 Segment 组成。Log 将 Segment 维护在指定目录下。内部通过 LRU 缓存读命中的 Segment。
- Segment:日志数据文件,由零或多个 Block 组成。为了优化写时复制(Copy-On-Write)性能以及后续可能支持的并发写日志,一个 Log 中通常有多个 Segment。 Segment 会保证 Block 完整地存在同一个数据文件中,如果剩余容量不足以容纳下一个 Block会开启下一个 Segment。
- Block:日志记录逻辑实体,用于定位日志记录边界、记录完整性校验、限制 Log 下最大日志数量。
日志实例
NewLog
初始化日志实例
参数:
字段名 | 数据类型 | 描述 |
---|---|---|
dirPath | string | 存放日志数据文件的目录 |
opts | *Options | 配置选项,不传配置将设置成默认值 |
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
log | *Log | 日志实例 |
errs | *errs.KvErr | 可能出现的错误:
|
示例:
使用默认配置
package main
import "github.com/Trinoooo/eggie_kv/storage/core/ragdoll/wal"
func main() {
dirPath := "path/to/your/wal_dir"
log, err := wal.NewLog(dirPath, nil) // opt为空将使用默认配置
if err != nil {
panic(err)
}
}
使用自定义配置
package main
import (
"github.com/Trinoooo/eggie_kv/storage/core/ragdoll/wal"
"github.com/Trinoooo/eggie_kv/consts"
)
func main() {
dirPath := "path/to/your/wal_dir"
opts := wal.NewOptions().
SetLogDirPerm(0770).
SetDataFilePerm(0660).
SetDataFileCacheSize(5).
SetDataFileCapacity(int64(100 * consts.MB)).
SetSyncMode(FullManagedSync)
log, err := wal.NewLog(dirPath, opts)
if err != nil {
panic(err)
}
}
(*Log).Open
打开日志
为了避免内存泄漏,结束使用后需要显示调用 Close 关闭日志文件
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
errs | *errs.KvErr | 可能出现的错误:
|
示例:
package main
import (
"github.com/Trinoooo/eggie_kv/storage/core/ragdoll/wal"
"github.com/Trinoooo/eggie_kv/consts"
)
func main() {
dirPath := "path/to/your/wal_dir"
log, err := wal.NewLog(dirPath, nil)
if err != nil {
panic(err)
}
err = log.Open()
if err != nil {
panic(err)
}
}
(*Log).Close
关闭日志实例,释放资源
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
errs | *errs.KvErr | 可能出现的错误:
|
示例:
package main
import (
"github.com/Trinoooo/eggie_kv/storage/core/ragdoll/wal"
"github.com/Trinoooo/eggie_kv/consts"
)
func main() {
dirPath := "path/to/your/wal_dir"
log, err := wal.NewLog(dirPath, nil)
if err != nil {
panic(err)
}
err = log.Open()
if err != nil {
panic(err)
}
err = log.Close()
if err != nil {
panic(err)
}
}
(*Log).Write
写入日志数据
参数:
字段名 | 数据类型 | 描述 |
---|---|---|
data | []byte | 预期写入的日志数据 |
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
errs | *errs.KvErr | 可能出现的错误:
|
示例:
package main
import (
"fmt"
"github.com/Trinoooo/eggie_kv/storage/core/ragdoll/wal"
"github.com/Trinoooo/eggie_kv/consts"
)
func main() {
dirPath := "path/to/your/wal_dir"
log, err := wal.NewLog(dirPath, nil)
if err != nil {
panic(err)
}
err = log.Open()
if err != nil {
panic(err)
}
err := log.Write([]byte("what ever you want to record"))
if err != nil {
fmt.Println(err)
}
err = log.Close()
if err != nil {
fmt.Println(err)
}
}
(*Log).Read
读取从最早写入日志算起指定范围内的日志数据
参数:
字段名 | 数据类型 | 描述 |
---|---|---|
size | int64 | 读取从最早写入日志算起的 size 条日志 |
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
logsData | [][]byte | 查询范围内日志数据列表,按写入顺序有序 |
errs | *errs.KvErr | 可能出现的错误:
|
示例:
package main
import (
"fmt"
"github.com/Trinoooo/eggie_kv/storage/core/ragdoll/wal"
"github.com/Trinoooo/eggie_kv/consts"
)
func main() {
dirPath := "path/to/your/wal_dir"
log, err := wal.NewLog(dirPath, nil)
if err != nil {
panic(err)
}
err = log.Open()
if err != nil {
panic(err)
}
logsData, err := log.Read(100) // 读取从最早写入日志算起的100条日志
if err != nil {
fmt.Println(err)
}
err = log.Close()
if err != nil {
fmt.Println(err)
}
}
(*Log).Truncate
截断指定数量的日志
参数:
字段名 | 数据类型 | 描述 |
---|---|---|
size | int64 | 截断从最早写入日志算起的 size 条日志 |
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
errs | *errs.KvErr | 可能出现的错误:
|
示例:
package main
import (
"fmt"
"github.com/Trinoooo/eggie_kv/storage/core/ragdoll/wal"
"github.com/Trinoooo/eggie_kv/consts"
)
func main() {
dirPath := "path/to/your/wal_dir"
log, err := wal.NewLog(dirPath, nil)
if err != nil {
panic(err)
}
err = log.Open()
if err != nil {
panic(err)
}
// 循环执行完成后,日 志实例中写入100条日志数据
for i := 0; i < 100; i++ {
_, err := log.Write([]byte{1, 2, 3})
if err != nil {
fmt.Println(err)
}
}
err = log.Truncate(50) // 截断从最早写入日志算起的50条日志
if err != nil {
fmt.Println(err)
}
err = log.Close()
if err != nil {
fmt.Println(err)
}
}
(*Log).Sync
同步内存中的日志数据到磁盘中
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
errs | *errs.KvErr | 可能出现的错误:
|
示例:
package main
import (
"fmt"
"github.com/Trinoooo/eggie_kv/storage/core/ragdoll/wal"
"github.com/Trinoooo/eggie_kv/consts"
)
func main() {
dirPath := "path/to/your/wal_dir"
log, err := wal.NewLog(dirPath, nil)
if err != nil {
panic(err)
}
err = log.Open()
if err != nil {
panic(err)
}
// 循环执行完成后,日志实例中写入100条日志数据
for i := 0; i < 100; i++ {
_, err := log.Write([]byte{1, 2, 3})
if err != nil {
fmt.Println(err)
}
}
err = log.Sync()
if err != nil {
fmt.Println(err)
}
err = log.Close()
if err != nil {
fmt.Println(err)
}
}
(*Log).Len
获取日志实例中存储的日志数量
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
number | int64 | 日志实例中维护的日志数量 |
errs | *errs.KvErr | 可能出现的错误:
|
实例:
package main
import (
"fmt"
"github.com/Trinoooo/eggie_kv/storage/core/ragdoll/wal"
"github.com/Trinoooo/eggie_kv/consts"
)
func main() {
dirPath := "path/to/your/wal_dir"
log, err := wal.NewLog(dirPath, nil)
if err != nil {
panic(err)
}
err = log.Open()
if err != nil {
panic(err)
}
l, err := log.Len()
if err != nil {
fmt.Println(err)
}
err = log.Close()
if err != nil {
fmt.Println(err)
}
}
配置实例
NewOptions
初始化日志配置
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
opts | *Options | 日志配置实例 |
(*Options).SetLogDirPerm
设置存放日志数据文件的目录权限
参数:
字段名 | 数据类型 | 描述 |
---|---|---|
logDirPerm | os.FileMode | 存放日志数据文件的目录权限 |
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
opts | *Options | 日志配置实例 |
(*Options).SetDataFilePerm
设置日志数据文件权限
参数:
字段名 | 数据类型 | 描述 |
---|---|---|
dataFilePerm | os.FileMode | 日志数据文件权限 |
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
opts | *Options | 日志配置实例 |
(*Options).SetDataFileCapacity
设置日志数据文件最大容量
参数:
字段名 | 数据类型 | 描述 |
---|---|---|
dataFileCapacity | int64 | 日志数据文件最大容量 |
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
opts | *Options | 日志配置实例 |
(*Options).SetDataFileCacheSize
设置日志实例在内存中最大缓存的日志数据文件数量
参数:
字段名 | 数据类型 | 描述 |
---|---|---|
dataFileCacheSize | int | 日志实例在内存中最大缓存的日志数据文件数量 |
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
opts | *Options | 日志配置实例 |
(*Options).SetSyncMode
设置日志实例的持久化级别
持久化级别
日志提供三种数据持久化级别:
- 全托管 - 同步(FullManagedSync):每次写日志都会将数据同步到磁盘,一致性好,但性能差。
- 全托管 - 异步(FullManagedAsync):日志会先写入内存缓冲中,后台协程周期同步,这个时间默认是1s。
- 自托管(SelfManaged):除非日志实例中的数据文件写满会自动同步数据到磁盘,否则日志数据只会写入到内存,需要外部主动调用 Sync 保证日志数据持久化。
参数:
字段名 | 数据类型 | 描述 |
---|---|---|
syncMode | SyncMode | 日志实例的持久化级别,可选枚举如下:
|
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
opts | *Options | 日志配置实例 |
(*Options).SetSyncInterval
设置后台协程同步数据到磁盘的周期,只有设置持久化级别为 全托管 - 异步(FullManagedAsync) 时才生效
参数:
字段名 | 数据类型 | 描述 |
---|---|---|
syncInterval | time.Duration | 后台协程同步数据到磁盘的周期 |
返回值:
字段名 | 数据类型 | 描述 |
---|---|---|
opts | *Options | 日志配置实例 |