@ -0,0 +1,27 @@ | |||
# If you prefer the allow list template instead of the deny list, see community template: | |||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore | |||
# | |||
# Binaries for programs and plugins | |||
*.exe | |||
*.exe~ | |||
*.dll | |||
*.so | |||
*.dylib | |||
# Test binary, built with `go test1 -c` | |||
*.test1 | |||
# Output of the go coverage tool, specifically when used with LiteIDE | |||
*.out | |||
# Dependency directories (remove the comment below to include it) | |||
# vendor/ | |||
# Go workspace file | |||
go.work | |||
#idea files | |||
.idea | |||
.idea/* | |||
raftnode |
@ -0,0 +1,22 @@ | |||
# go-raft-kv | |||
--- | |||
基于go语言实现分布式kv数据库 | |||
# 环境与运行 | |||
使用环境是wsl+ubuntu | |||
go mod download安装依赖 | |||
./scripts/build.sh 会在根目录下编译出raftnode | |||
./scripts/run.sh 运行三个节点,目前能在终端进行读入,leader(n1)节点输出send log,其余节点输出receive log。终端输入后如果超时就退出(脚本运行时间可以在其中调整)。 | |||
# 注意 | |||
脚本第一次运行需要权限获取 chmod +x <脚本> | |||
如果出现tcp listen error可能是因为之前的进程没用正常退出,占用了端口 | |||
lsof -i :9091查看pid | |||
kill -9 <pid>杀死进程 | |||
# todo list | |||
消息通讯异常的处理 | |||
kv本地持久化 | |||
崩溃与恢复(以及对应的测试) |
@ -0,0 +1,40 @@ | |||
package main | |||
import ( | |||
"flag" | |||
"simple-kv-store/internal/logprovider" | |||
"simple-kv-store/internal/nodes" | |||
"go.uber.org/zap" | |||
"strconv" | |||
"strings" | |||
) | |||
var log, _ = logprovider.CreateDefaultZapLogger(zap.InfoLevel) | |||
func main() { | |||
defer func() { | |||
if err := recover(); err != nil { | |||
log.Info("i get a panic", zap.Any("panic error", err)) | |||
} | |||
}() | |||
port := flag.String("port", ":9091", "rpc listen port") | |||
cluster := flag.String("cluster", "127.0.0.1:9092,127.0.0.1:9093", "comma sep") | |||
id := flag.Int("id", 1, "node ID") | |||
pipe := flag.String("pipe", "", "input from scripts") | |||
isLeader := flag.Bool("isleader", false, "init node state") | |||
// 参数解析 | |||
flag.Parse() | |||
clusters := strings.Split(*cluster, ",") | |||
node := nodes.Init(*id, clusters, *pipe) | |||
log.Info("id: " + strconv.Itoa(*id) + "节点开始监听: " + *port + "端口") | |||
// 监听rpc | |||
node.Rpc(*port) | |||
// 开启 raft | |||
nodes.Start(node, *isLeader) | |||
select {} | |||
} |
@ -0,0 +1,10 @@ | |||
module simple-kv-store | |||
go 1.20 | |||
require go.uber.org/zap v1.24.0 | |||
require ( | |||
go.uber.org/atomic v1.11.0 // indirect | |||
go.uber.org/multierr v1.11.0 // indirect | |||
) |
@ -0,0 +1,13 @@ | |||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= | |||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | |||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= | |||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | |||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= | |||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= | |||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= | |||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= | |||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= | |||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= | |||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= | |||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= | |||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
@ -0,0 +1,24 @@ | |||
package logprovider | |||
import "fmt" | |||
const ( | |||
JsonLogFormat = "json" | |||
ConsoleLogFormat = "console" | |||
) | |||
var DefaultLogFormat = JsonLogFormat | |||
// ConvertToZapFormat converts and validated logprovider format string. | |||
func ConvertToZapFormat(format string) (string, error) { | |||
switch format { | |||
case ConsoleLogFormat: | |||
return ConsoleLogFormat, nil | |||
case JsonLogFormat: | |||
return JsonLogFormat, nil | |||
case "": | |||
return DefaultLogFormat, nil | |||
default: | |||
return "", fmt.Errorf("unknown logprovider format: %s, supported values json, console", format) | |||
} | |||
} |
@ -0,0 +1,14 @@ | |||
package logprovider | |||
import "go.uber.org/zap/zapcore" | |||
var DefaultLogLevel = "info" | |||
// ConvertToZapLevel converts logprovider level string to zapcore.Level. | |||
func ConvertToZapLevel(lvl string) zapcore.Level { | |||
var level zapcore.Level | |||
if err := level.Set(lvl); err != nil { | |||
panic(err) | |||
} | |||
return level | |||
} |
@ -0,0 +1,55 @@ | |||
package logprovider | |||
import ( | |||
"go.uber.org/zap" | |||
"go.uber.org/zap/zapcore" | |||
"time" | |||
) | |||
// CreateDefaultZapLogger creates a logger with default zap configuration | |||
func CreateDefaultZapLogger(level zapcore.Level) (*zap.Logger, error) { | |||
logCfg := DefaultZapLoggerConfig | |||
logCfg.Level = zap.NewAtomicLevelAt(level) | |||
c, err := logCfg.Build() | |||
if err != nil { | |||
return nil, err | |||
} | |||
return c, nil | |||
} | |||
// DefaultZapLoggerConfig defines default zap logger configuration. | |||
var DefaultZapLoggerConfig = zap.Config{ | |||
Level: zap.NewAtomicLevelAt(ConvertToZapLevel(DefaultLogLevel)), | |||
Development: false, | |||
Sampling: &zap.SamplingConfig{ | |||
Initial: 100, | |||
Thereafter: 100, | |||
}, | |||
Encoding: DefaultLogFormat, | |||
// copied from "zap.NewProductionEncoderConfig" with some updates | |||
EncoderConfig: zapcore.EncoderConfig{ | |||
TimeKey: "ts", | |||
LevelKey: "level", | |||
NameKey: "logger", | |||
CallerKey: "caller", | |||
MessageKey: "msg", | |||
StacktraceKey: "stacktrace", | |||
LineEnding: zapcore.DefaultLineEnding, | |||
EncodeLevel: zapcore.LowercaseLevelEncoder, | |||
// Custom EncodeTime function to ensure we match format and precision of historic capnslog timestamps | |||
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { | |||
enc.AppendString(t.Format("2006-01-02T15:04:05.999999Z0700")) | |||
}, | |||
EncodeDuration: zapcore.StringDurationEncoder, | |||
EncodeCaller: zapcore.ShortCallerEncoder, | |||
}, | |||
// Use "/dev/null" to discard all | |||
OutputPaths: []string{"stderr"}, | |||
ErrorOutputPaths: []string{"stderr"}, | |||
} |
@ -0,0 +1,105 @@ | |||
package nodes | |||
import ( | |||
"io" | |||
"net" | |||
"net/http" | |||
"net/rpc" | |||
"os" | |||
"simple-kv-store/internal/logprovider" | |||
"time" | |||
"go.uber.org/zap" | |||
) | |||
var log, _ = logprovider.CreateDefaultZapLogger(zap.InfoLevel) | |||
func newNode(address string) *Public_node_info { | |||
return &Public_node_info{ | |||
connect: false, | |||
address: address, | |||
} | |||
} | |||
func Init(id int, nodeAddr []string, pipe string) *Node { | |||
ns := make(map[int]*Public_node_info) | |||
for k, v := range nodeAddr { | |||
ns[k] = newNode(v) | |||
} | |||
// 创建节点 | |||
return &Node{ | |||
self: id, | |||
nodes: ns, | |||
pipeAddr: pipe, | |||
} | |||
} | |||
func Start(node *Node, isLeader bool) { | |||
if isLeader { | |||
node.state = Candidate // 需要身份转变 | |||
} else { | |||
node.state = Follower | |||
} | |||
go func() { | |||
for { | |||
switch node.state { | |||
case Follower: | |||
case Candidate: | |||
// candidate发布一个监听输入线程后,变成leader | |||
node.state = Leader | |||
go func() { | |||
if node.pipeAddr == "" { | |||
log.Error("暂不支持非管道读入") | |||
} | |||
pipe, err := os.Open(node.pipeAddr) | |||
if err != nil { | |||
log.Error("Failed to open pipe") | |||
} | |||
defer pipe.Close() | |||
// 不断读取管道中的输入 | |||
buffer := make([]byte, 256) | |||
for { | |||
n, err := pipe.Read(buffer) | |||
if err != nil && err != io.EOF { | |||
log.Error("Error reading from pipe") | |||
} | |||
if n > 0 { | |||
input := string(buffer[:n]) | |||
log.Info("send : " + input) | |||
// 将用户输入封装成一个 LogEntry | |||
kv := LogEntry{input, ""} | |||
node.log = append(node.log, kv) | |||
// 广播给其它节点 | |||
node.BroadCastKV(kv) | |||
} | |||
} | |||
}() | |||
case Leader: | |||
time.Sleep(50 * time.Millisecond) | |||
} | |||
} | |||
}() | |||
} | |||
func (node *Node) Rpc(port string) { | |||
err := rpc.Register(node) | |||
if err != nil { | |||
log.Fatal("rpc register failed", zap.Error(err)) | |||
} | |||
rpc.HandleHTTP() | |||
l, e := net.Listen("tcp", port) | |||
if e != nil { | |||
log.Fatal("listen error:", zap.Error(err)) | |||
} | |||
go func() { | |||
err := http.Serve(l, nil) | |||
if err != nil { | |||
log.Fatal("http server error:", zap.Error(err)) | |||
} | |||
}() | |||
} |
@ -0,0 +1,10 @@ | |||
package nodes | |||
type LogEntry struct { | |||
Key string | |||
Value string | |||
} | |||
type KVReply struct { | |||
Reply bool | |||
} |
@ -0,0 +1,107 @@ | |||
package nodes | |||
import ( | |||
"net/rpc" | |||
"go.uber.org/zap" | |||
) | |||
type State = uint8 | |||
const ( | |||
Follower State = iota + 1 | |||
Candidate | |||
Leader | |||
) | |||
type Public_node_info struct { | |||
connect bool | |||
address string | |||
} | |||
type Node struct { | |||
// 当前节点id | |||
self int | |||
// 除当前节点外其他节点信息 | |||
nodes map[int]*Public_node_info | |||
//管道名 | |||
pipeAddr string | |||
// 当前节点状态 | |||
state State | |||
// 简单的kv存储 | |||
log []LogEntry | |||
} | |||
func (node *Node) BroadCastKV(kv LogEntry) { | |||
// 遍历所有节点 | |||
for i := range node.nodes { | |||
go func (index int, kv LogEntry) { | |||
var reply KVReply | |||
node.sendKV(index, kv, &reply) | |||
} (i, kv) | |||
} | |||
} | |||
func (node *Node) sendKV(index int, kv LogEntry, reply *KVReply) { | |||
client, err := rpc.DialHTTP("tcp", node.nodes[index].address) | |||
if err != nil { | |||
log.Error("dialing: ", zap.Error(err)) | |||
return | |||
} | |||
defer func(client *rpc.Client) { | |||
err := client.Close() | |||
if err != nil { | |||
log.Error("client close err: ", zap.Error(err)) | |||
} | |||
}(client) | |||
callErr := client.Call("Node.ReceiveKV", kv, reply) // RPC | |||
if callErr != nil { | |||
log.Error("dialing: ", zap.Error(callErr)) | |||
} | |||
if reply.Reply { // 发送成功 | |||
} else { // 失败 | |||
} | |||
} | |||
// RPC call | |||
func (node *Node) ReceiveKV(kv LogEntry, reply *KVReply) error { | |||
log.Info("receive: " + kv.Key) | |||
reply.Reply = true; | |||
return nil | |||
} | |||
// func (node *Node) broadcastHeartbeat() { | |||
// // 遍历所有节点 | |||
// for i := range raft.nodes { | |||
// // request 参数 | |||
// hb := Heartbeat{ | |||
// Term: raft.currTerm, | |||
// LeaderId: raft.self, | |||
// CommitIndex: raft.commitIndex, | |||
// } | |||
// prevLogIndex := raft.nextIndex[i] - 1 | |||
// // 如果有日志未同步则发送 | |||
// if raft.getLastIndex() > prevLogIndex { | |||
// hb.PrevLogIndex = prevLogIndex | |||
// hb.PrevLogTerm = raft.log[prevLogIndex].CurrTerm | |||
// hb.Entries = raft.log[prevLogIndex:] | |||
// // log.Info("will send log entries", zap.Any("logEntries", hb.Entries)) | |||
// } | |||
// go func(index int, hb Heartbeat) { | |||
// var reply HeartbeatReply | |||
// // 向某一个节点发送 heartbeat | |||
// raft.sendHeartbeat(index, hb, &reply) | |||
// }(i, hb) | |||
// } | |||
// } | |||
@ -0,0 +1 @@ | |||
go build -o raftnode ./cmd/main.go |
@ -0,0 +1,61 @@ | |||
#!/bin/bash | |||
# 设置运行时间限制:s | |||
RUN_TIME=10 | |||
# 需要传递数据的管道 | |||
PIPE_NAME="/tmp/raft_input_pipe" | |||
# 启动节点1 | |||
echo "Starting Node 1..." | |||
timeout $RUN_TIME ./raftnode -id 1 -port ":9091" -cluster "127.0.0.1:9092,127.0.0.1:9093" -pipe "$PIPE_NAME" -isleader=true & | |||
# 启动节点2 | |||
echo "Starting Node 2..." | |||
timeout $RUN_TIME ./raftnode -id 2 -port ":9092" -cluster "127.0.0.1:9091,127.0.0.1:9093" -pipe "$PIPE_NAME" & | |||
# 启动节点3 | |||
echo "Starting Node 3..." | |||
timeout $RUN_TIME ./raftnode -id 3 -port ":9093" -cluster "127.0.0.1:9091,127.0.0.1:9092" -pipe "$PIPE_NAME"& | |||
echo "All nodes started successfully!" | |||
# 创建一个管道用于进程间通信 | |||
if [[ ! -p "$PIPE_NAME" ]]; then | |||
mkfifo "$PIPE_NAME" | |||
fi | |||
# 捕获终端输入并通过管道传递给三个节点 | |||
echo "Enter input to send to nodes:" | |||
start_time=$(date +%s) | |||
while true; do | |||
# 从终端读取用户输入 | |||
read -r user_input | |||
current_time=$(date +%s) | |||
elapsed_time=$((current_time - start_time)) | |||
# 如果运行时间大于限制时间,就退出 | |||
if [ $elapsed_time -ge $RUN_TIME ]; then | |||
echo 'Timeout reached, normal exit now' | |||
break | |||
fi | |||
# 如果输入为空,跳过 | |||
if [[ -z "$user_input" ]]; then | |||
continue | |||
fi | |||
# 将用户输入发送到管道 | |||
echo "$user_input" > "$PIPE_NAME" | |||
# 如果输入 "exit",结束脚本 | |||
if [[ "$user_input" == "exit" ]]; then | |||
break | |||
fi | |||
done | |||
# 删除管道 | |||
rm "$PIPE_NAME" | |||
# 等待所有节点完成启动 | |||
wait |