From 8105ba5ebfa6e0dba3d936941a9b0f4e24c103ce Mon Sep 17 00:00:00 2001 From: GongHongjun Date: Tue, 19 Mar 2024 12:39:08 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BC=98=E5=8C=96MultiBulkReply.ToBytes?= =?UTF-8?q?=E4=B8=AD=E5=86=85=E5=AD=98=E5=88=86=E9=85=8D=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E5=BC=8F(=E6=8F=90=E5=89=8D=E5=88=86=E9=85=8D);=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E4=BD=BF=E7=94=A8concatstrings=E5=92=8Cslicebytetostr?= =?UTF-8?q?ing=E4=BB=A5=E6=8F=90=E9=AB=98=E6=80=A7=E8=83=BD;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- redis/protocol/reply.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/redis/protocol/reply.go b/redis/protocol/reply.go index 98cadb1a..10bca09d 100644 --- a/redis/protocol/reply.go +++ b/redis/protocol/reply.go @@ -50,14 +50,34 @@ func MakeMultiBulkReply(args [][]byte) *MultiBulkReply { // ToBytes marshal redis.Reply func (r *MultiBulkReply) ToBytes() []byte { - argLen := len(r.Args) var buf bytes.Buffer - buf.WriteString("*" + strconv.Itoa(argLen) + CRLF) + //Calculate the length of buffer + argLen := len(r.Args) + bufLen := 1 + len(strconv.Itoa(argLen)) + 2 + for _, arg := range r.Args { + if arg == nil { + bufLen += 3 + 2 + } else { + bufLen += 1 + len(strconv.Itoa(len(arg))) + 2 + len(arg) + 2 + } + } + //Allocate memory + buf.Grow(bufLen) + //Write string step by step,avoid concat strings + buf.WriteString("*") + buf.WriteString(strconv.Itoa(argLen)) + buf.WriteString(CRLF) for _, arg := range r.Args { if arg == nil { - buf.WriteString("$-1" + CRLF) + buf.WriteString("$-1") + buf.WriteString(CRLF) } else { - buf.WriteString("$" + strconv.Itoa(len(arg)) + CRLF + string(arg) + CRLF) + buf.WriteString("$") + buf.WriteString(strconv.Itoa(len(arg))) + buf.WriteString(CRLF) + //Write bytes,avoid slice of byte to string(slicebytetostring) + buf.Write(arg) + buf.WriteString(CRLF) } } return buf.Bytes() From 83d33037d9805d0508de27db5b7bed95dffc67f5 Mon Sep 17 00:00:00 2001 From: GongHongjun Date: Mon, 1 Apr 2024 15:06:58 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BC=98=E5=8C=96MultiBulkReply.ToBytes?= =?UTF-8?q?=E4=B8=AD=E8=AE=A1=E7=AE=97=E6=89=80=E9=9C=80=E5=86=85=E5=AD=98?= =?UTF-8?q?=E7=9A=84=E7=AE=97=E6=B3=95,=E6=8F=90=E9=AB=98=E6=80=A7?= =?UTF-8?q?=E8=83=BD;=E5=9C=A8utils=E4=B8=AD=E6=96=B0=E5=A2=9EGetItoaLen?= =?UTF-8?q?=E6=96=B9=E6=B3=95,=E4=BB=A5=E6=9B=BF=E4=BB=A3=E5=8E=9Flen(strc?= =?UTF-8?q?onv.Itoa(i=20int))=E8=AE=A1=E7=AE=97=E6=95=B0=E5=AD=97=E8=A2=AB?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E4=B8=BA=E5=AD=97=E7=AC=A6=E4=B8=B2=E4=BB=A5?= =?UTF-8?q?=E5=90=8E=E5=AD=97=E7=AC=A6=E4=B8=B2=E7=9A=84=E9=95=BF=E5=BA=A6?= =?UTF-8?q?;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/utils/utils.go | 16 +++++ lib/utils/utils_test.go | 134 ++++++++++++++++++++++++++++++++++++++++ redis/protocol/reply.go | 5 +- 3 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 lib/utils/utils_test.go diff --git a/lib/utils/utils.go b/lib/utils/utils.go index a912e3e6..731e62be 100644 --- a/lib/utils/utils.go +++ b/lib/utils/utils.go @@ -84,3 +84,19 @@ func ConvertRange(start int64, end int64, size int64) (int, int) { } return int(start), int(end) } + +// GetItoaLen Calculates the length of an integer after it is converted to a string +func GetItoaLen(i int) int { + strLen := 0 + if i < 0 { + strLen++ + } + for { + strLen++ + i = i / 10 + if i == 0 { + break + } + } + return strLen +} diff --git a/lib/utils/utils_test.go b/lib/utils/utils_test.go new file mode 100644 index 00000000..1ff221fe --- /dev/null +++ b/lib/utils/utils_test.go @@ -0,0 +1,134 @@ +package utils + +import ( + "math/rand" + "strconv" + "testing" + "time" +) + +// Function test of GetItoaLen + +func TestGetIntCnvStrLenWithRandom(t *testing.T) { + type args struct { + } + tests := []struct { + name string + args args + }{ + // TODO: Add test cases. + {"随机数测试", args{}}, + } + for _, tt := range tests { + r := rand.NewSource(time.Now().UnixNano()) + t.Run(tt.name, func(t *testing.T) { + for i := 0; i < 1000000; i++ { + num := r.Int63() + if got := GetItoaLen(int(num)); got != len(strconv.Itoa(int(num))) { + t.Errorf("FastItoa() = %v, want %v", got, len(strconv.Itoa(int(num)))) + } + } + }) + } +} + +func TestGetIntCnvStrLenWithRandomNegative(t *testing.T) { + type args struct { + } + tests := []struct { + name string + args args + }{ + // TODO: Add test cases. + {"随机负数测试", args{}}, + } + for _, tt := range tests { + r := rand.NewSource(time.Now().UnixNano()) + t.Run(tt.name, func(t *testing.T) { + for i := 0; i < 1000000; i++ { + num := r.Int63() + if got := GetItoaLen(int(0 - num)); got != len(strconv.Itoa(int(0-num))) { + t.Errorf("FastItoa() = %v, want %v", got, len(strconv.Itoa(int(num)))) + } + } + }) + } +} + +// Benchmark strconv.Itoa vs GetItoaLen + +func Benchmark_Itoa_20bit(b *testing.B) { + //strconv.Itoa(argLen) + type args struct { + i int + } + tests := []struct { + name string + args args + }{ + // TODO: Add test cases. + {"20 bit", args{-2381934732381934731}}, + } + for _, tt := range tests { + for i := 0; i < b.N; i++ { + _ = len(strconv.Itoa(tt.args.i)) + } + } +} + +func Benchmark_GetIotaLen_20bit(b *testing.B) { + //strconv.Itoa(argLen) + type args struct { + i int + } + tests := []struct { + name string + args args + }{ + // TODO: Add test cases. + {"20 bit", args{-2381934732381934731}}, + } + for _, tt := range tests { + for i := 0; i < b.N; i++ { + _ = GetItoaLen(tt.args.i) + } + } +} + +func Benchmark_Itoa_10bit(b *testing.B) { + //strconv.Itoa(argLen) + type args struct { + i int + } + tests := []struct { + name string + args args + }{ + // TODO: Add test cases. + {"10 bit", args{-238193473}}, + } + for _, tt := range tests { + for i := 0; i < b.N; i++ { + _ = len(strconv.Itoa(tt.args.i)) + } + } +} + +func Benchmark_GetIotaLen_10bit(b *testing.B) { + //strconv.Itoa(argLen) + type args struct { + i int + } + tests := []struct { + name string + args args + }{ + // TODO: Add test cases. + {"10 bit", args{-238193473}}, + } + for _, tt := range tests { + for i := 0; i < b.N; i++ { + _ = GetItoaLen(tt.args.i) + } + } +} diff --git a/redis/protocol/reply.go b/redis/protocol/reply.go index 10bca09d..917d4a1b 100644 --- a/redis/protocol/reply.go +++ b/redis/protocol/reply.go @@ -3,6 +3,7 @@ package protocol import ( "bytes" "github.com/hdt3213/godis/interface/redis" + "github.com/hdt3213/godis/lib/utils" "strconv" ) @@ -53,12 +54,12 @@ func (r *MultiBulkReply) ToBytes() []byte { var buf bytes.Buffer //Calculate the length of buffer argLen := len(r.Args) - bufLen := 1 + len(strconv.Itoa(argLen)) + 2 + bufLen := 1 + utils.GetItoaLen(argLen) + 2 for _, arg := range r.Args { if arg == nil { bufLen += 3 + 2 } else { - bufLen += 1 + len(strconv.Itoa(len(arg))) + 2 + len(arg) + 2 + bufLen += 1 + utils.GetItoaLen(len(arg)) + 2 + len(arg) + 2 } } //Allocate memory From afe2f219dd6c2da9cb16b7bb82f55e8d806ebcfe Mon Sep 17 00:00:00 2001 From: GongHongjun Date: Wed, 22 May 2024 09:58:49 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E5=92=8C=E6=89=93=E5=8C=85=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 26 ++++++++++++++++ Plan.MD | 78 ++++++++++++++++++++++++++++++++++++++++++++++ PullRequest.MD | 50 +++++++++++++++++++++++++++++ PullRequest0401.MD | 25 +++++++++++++++ build-linux.bat | 4 +++ 5 files changed, 183 insertions(+) create mode 100644 Dockerfile create mode 100644 Plan.MD create mode 100644 PullRequest.MD create mode 100644 PullRequest0401.MD create mode 100644 build-linux.bat diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..09bd67b1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM golang:1.17.10 as build + +# 容器环境变量添加 +ENV GO111MODULE=on +ENV GOPROXY=https://goproxy.cn,direct + +# 设置当前工作区 +WORKDIR /go/release + +# 把全部文件添加到/go/release目录 +ADD . . + +# 编译: 把main.go编译为可执行的二进制文件, 并命名为app +RUN GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -ldflags="-s -w" -installsuffix cgo -o godis . + +# 运行: 使用scratch作为基础镜像 +FROM golang:1.17.10 as prod + +# 在build阶段, 复制时区配置到镜像的/etc/localtime +COPY --from=build /usr/share/zoneinfo/Asia/Shanghai /etc/localtime + +# 在build阶段, 复制./app目录下的可执行二进制文件到当前目录 +COPY --from=build /go/release/godis / + +# 启动服务 +CMD ["/app"] diff --git a/Plan.MD b/Plan.MD new file mode 100644 index 00000000..11066791 --- /dev/null +++ b/Plan.MD @@ -0,0 +1,78 @@ +1,使用AF_PACKET替代net.con,提高吞吐率 /tcp/server.go 44行 +预计需要实现Listener接口 +```go + type Listener interface { + // Accept waits for and returns the next connection to the listener. + Accept() (Conn, error) + + // Close closes the listener. + // Any blocked Accept operations will be unblocked and return errors. + Close() error + + // Addr returns the listener's network address. + Addr() Addr + } +``` + +和Conn接口 +```go +type Conn interface { + // Read reads data from the connection. + // Read can be made to time out and return an error after a fixed + // time limit; see SetDeadline and SetReadDeadline. + Read(b []byte) (n int, err error) + + // Write writes data to the connection. + // Write can be made to time out and return an error after a fixed + // time limit; see SetDeadline and SetWriteDeadline. + Write(b []byte) (n int, err error) + + // Close closes the connection. + // Any blocked Read or Write operations will be unblocked and return errors. + Close() error + + // LocalAddr returns the local network address, if known. + LocalAddr() Addr + + // RemoteAddr returns the remote network address, if known. + RemoteAddr() Addr + + // SetDeadline sets the read and write deadlines associated + // with the connection. It is equivalent to calling both + // SetReadDeadline and SetWriteDeadline. + // + // A deadline is an absolute time after which I/O operations + // fail instead of blocking. The deadline applies to all future + // and pending I/O, not just the immediately following call to + // Read or Write. After a deadline has been exceeded, the + // connection can be refreshed by setting a deadline in the future. + // + // If the deadline is exceeded a call to Read or Write or to other + // I/O methods will return an error that wraps os.ErrDeadlineExceeded. + // This can be tested using errors.Is(err, os.ErrDeadlineExceeded). + // The error's Timeout method will return true, but note that there + // are other possible errors for which the Timeout method will + // return true even if the deadline has not been exceeded. + // + // An idle timeout can be implemented by repeatedly extending + // the deadline after successful Read or Write calls. + // + // A zero value for t means I/O operations will not time out. + SetDeadline(t time.Time) error + + // SetReadDeadline sets the deadline for future Read calls + // and any currently-blocked Read call. + // A zero value for t means Read will not time out. + SetReadDeadline(t time.Time) error + + // SetWriteDeadline sets the deadline for future Write calls + // and any currently-blocked Write call. + // Even if write times out, it may return n > 0, indicating that + // some of the data was successfully written. + // A zero value for t means Write will not time out. + SetWriteDeadline(t time.Time) error +} + +``` + +参考:https://mp.weixin.qq.com/s/6DRg_6P_Gz0ZWw_JPCKuLA diff --git a/PullRequest.MD b/PullRequest.MD new file mode 100644 index 00000000..c2c97279 --- /dev/null +++ b/PullRequest.MD @@ -0,0 +1,50 @@ + +### 提交说明 +优化MultiBulkReply.ToBytes中计算所需内存的算法,提高性能;在utils中新增GetItoaLen方法,以替代原len(strconv.Itoa(i int))计算数字被转换为字符串以后字符串的长度; + +### 基准测试 [utils_test.go](lib%2Futils%2Futils_test.go) +#### 测试 +OS:Centos 7 +VM:VM Workstation 17.5.0 +VM配置:2C4G + +#### 测试结果 +Now:当前版本 +New Version:本次提交的版本 + +* 非LRANGE指令 + 测试方法:运行以下指令3次,取平均值 +```text +redis-benchmark -q -d 4096 --csv -t SET,GET,INCR,LPUSH,RPUSH,LPOP,RPOP,SADD,HSET,SPOP +``` + +| Redis 6.2.6 | Request/S | Now | Request/S | New Version | Request/S | New Vs Now | New Vs Redis6 | +|-------------|-------------|-------|-------------|-------------|-------------|------------|---------------| +| SET | 160605.7133 | SET | 68052.81667 | SET | 66993.39333 | -2% | -58% | +| GET | 152806.5767 | GET | 62314.54667 | GET | 62106.69667 | 0% | -59% | +| INCR | 175523.5433 | INCR | 96774.33667 | INCR | 103187.75 | 7% | -41% | +| LPUSH | 125236.7433 | LPUSH | 70687.95333 | LPUSH | 71056.98 | 1% | -43% | +| RPUSH | 138791 | RPUSH | 68633.12667 | RPUSH | 67574.54 | -2% | -51% | +| LPOP | 153191.02 | LPOP | 60513.26333 | LPOP | 61848.86333 | 2% | -60% | +| RPOP | 134505.1 | RPOP | 68805.09333 | RPOP | 70723.33333 | 3% | -47% | +| SADD | 170620.0933 | SADD | 103671.8467 | SADD | 105098.9467 | 1% | -38% | +| HSET | 157717.3833 | HSET | 70449.39667 | HSET | 70000.03333 | -1% | -56% | +| SPOP | 167282.3467 | SPOP | 104680.9 | SPOP | 113612.5367 | 9% | -32% | + +总结:本次提交的性能优化,对非LRANGE的指令吞吐率有部分提升; + +* LRANGE指令 + 测试方法:运行以下指令3次,取平均值 +```text +redis-benchmark -q -d 4096 --csv -t lrange -n 10000 +``` + +| Redis 6.2.6 | Request/S | Now | Request/S | New Version | Request/S | New Vs Now | New Vs Redis6 | +|------------------------------------|-------------|------------------------------------|-------------|------------------------------------|-------------|------------|---------------| +| LPUSH (needed to benchmark LRANGE) | 138415.7267 | LPUSH (needed to benchmark LRANGE) | 31227.84667 | LPUSH (needed to benchmark LRANGE) | 66234.6 | 112% | -52% | +| LRANGE_100 (first 100 elements) | 2394.883333 | LRANGE_100 (first 100 elements) | 1392.1 | LRANGE_100 (first 100 elements) | 2100.576667 | 51% | -12% | +| LRANGE_300 (first 300 elements) | 737.4066667 | LRANGE_300 (first 300 elements) | 562.9166667 | LRANGE_300 (first 300 elements) | 762.1166667 | 35% | 3% | +| LRANGE_500 (first 450 elements) | 480.99 | LRANGE_500 (first 450 elements) | 349 | LRANGE_500 (first 450 elements) | 522.4533333 | 50% | 9% | +| LRANGE_600 (first 600 elements) | 348.7066667 | LRANGE_600 (first 600 elements) | 281.02 | LRANGE_600 (first 600 elements) | 389.0833333 | 38% | 12% | + +总结:本次提交的性能优化,对LRANGE的指令吞吐率提升明显,部分超过Redis6; \ No newline at end of file diff --git a/PullRequest0401.MD b/PullRequest0401.MD new file mode 100644 index 00000000..12ecd538 --- /dev/null +++ b/PullRequest0401.MD @@ -0,0 +1,25 @@ +### 提交说明 +优化MultiBulkReply.ToBytes中计算所需内存的算法,提高性能;在utils中新增GetItoaLen方法,以替代原len(strconv.Itoa(i int))计算数字被转换为字符串以后字符串的长度; + +### 功能和benchmark测试 [utils_test.go](lib%2Futils%2Futils_test.go) + +#### 测试结果 +```text +goos: windows +goarch: amd64 +pkg: github.com/hdt3213/godis/lib/utils +cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz +Benchmark_Itoa_20bit +Benchmark_Itoa_20bit-12 21663034 57.17 ns/op +Benchmark_GetIotaLen_20bit +Benchmark_GetIotaLen_20bit-12 39857972 28.99 ns/op +Benchmark_Itoa_10bit +Benchmark_Itoa_10bit-12 29314266 39.26 ns/op +Benchmark_GetIotaLen_10bit +Benchmark_GetIotaLen_10bit-12 130224921 9.493 ns/op +PASS + +Process finished with the exit code 0 +``` + +总结:采用新的方法可以一定程度上提升效率;但是在总体吞吐量测试上,表现不明显;经过分析,吞吐率受内存分配影响最大,这部分优化对总体影响较小. diff --git a/build-linux.bat b/build-linux.bat new file mode 100644 index 00000000..3d0afcf0 --- /dev/null +++ b/build-linux.bat @@ -0,0 +1,4 @@ +#正式版打包 +set GOOS=linux +set GOARCH=amd64 +go.exe build -ldflags "-s -w" -o target/godis-linux ./