本篇是整个系列的最后一篇了,本来打算在系列的最后一两篇写一下关于k8s部署相关的内容,在构思的过程中觉得自己对k8s知识的掌握还很不足,在自己没有理解掌握的前提下我觉得也很难写出自己满意的文章,大家看了可能也会觉得内容没有干货。我最近也在学习k8s的一些最佳实践以及阅读k8s的源码,等待时机成熟的时候可能会考虑单独写一个k8s实战系列文章。
下面列出了整个系列的每篇文章,这个系列文章的主要特点是贴近真实的开发场景,并针对高并发请求以及常见问题进行优化,文章的内容也是循序渐进的,先是介绍了项目的背景,接着进行服务的拆分,拆分完服务进行API的定义和表结构的设计,这和我们实际在公司中的开发流程是类似的,紧接着就是做一些数据库的CRUD基本操作,后面用三篇文章来讲解了缓存,因为缓存是高并发的基础,没有缓存高并发系统就无从谈起,缓存主要是应对高并发的读,接下来又用两篇文章来对高并发的写进行优化,最后通过分布式事务保证为服务间的数据一致性。如果大家能够对每一篇文章都能理解透彻,我觉得对于工作中的绝大多数场景都能轻松应对。
对于文章配套的示例代码并没有写的很完善,有几点原因,一是商城的功能点非常多,很难把所有的逻辑都覆盖到;二是多数都是重复的业务逻辑,只要大家掌握了核心的示例代码,其他的业务逻辑可以自己把代码down下来进行补充完善,这样我觉得才会进步。如果有不理解的地方大家可以在社区群中问我,每个社区群都可以找到我。
go-zero微服务实战系列(八、如何处理每秒上万次的下单请求)
软件测试由单元测试开始(unit test)。更复杂的测试都是在单元测试之上进行的。如下所示测试的层级模型:
单元测试(unit test)是最小、最简单的软件测试形式、这些测试用来评估某一个独立的软件单元,比如一个类,或者一个函数的正确性。这些测试不考虑包含该软件单元的整体系统的正确定。单元测试同时也是一种规范,用来保证某个函数或者模块完全符合系统对其的行为要求。单元测试经常被用来引入测试驱动开发的概念。
go语言的测试依赖go test
工具,它是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go
为后缀的源代码文件都是 go test
测试的一部分,不会被go build
编译到最终的可执行文件。
在*_test.go
文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数:
类型 | 格式 | 作用 |
---|---|---|
测试单数 | 函数名前缀为Test | 测试程序的一些逻辑行为是否正确 |
基准函数 | 函数名前缀为Benchmark | 测试函数的性能 |
示例函数 | 函数名前缀为Example | 提供示例 |
go test
会遍历所有*_test.go
文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数。
每个测试函数必须导入testing
包,测试函数的基本格式如下:
func TestName(t *testing.T) {
// ......
}
测试函数的名字必须以Test
开头,可选的后缀名必须以大写字母开头,示例如下:
func TestDo(t *testing.T) { //...... }
func TestWrite(t *testing.T) { // ...... }
testing.T
用于报告测试失败和附加的日志信息,拥有的主要方法如下:
Name() string
Fail()
Failed() bool
FailNow()
logDepth(s string, depth int)
Log(args ...any)
Logf(format string, args ...any)
Error(args ...any)
Errorf(format string, args ...any)
Fatal(args ...any)
Fatalf(format string, args ...any)
Skip(args ...any)
Skipf(format string, args ...any)
SkipNow()
Skipped() bool
Helper()
Cleanup(f func())
Setenv(key string, value string)
在这个路径下lebron/apps/order/rpc/internal/logic/createorderlogic.go:44
有一个生成订单id的函数,函数如下:
func genOrderID(t time.Time) string {
s := t.Format("20060102150405")
m := t.UnixNano()/1e6 - t.UnixNano()/1e9*1e3
ms := sup(m, 3)
p := os.Getpid() % 1000
ps := sup(int64(p), 3)
i := atomic.AddInt64(&num, 1)
r := i % 10000
rs := sup(r, 4)
n := fmt.Sprintf("%s%s%s%s", s, ms, ps, rs)
return n
}
我们创建createorderlogic_test.go
文件并为该方法编写对应的单元测试函数,生成的订单id长度为24,单元测试函数如下:
func TestGenOrderID(t *testing.T) {
oid := genOrderID(time.Now())
if len(oid) != 24 {
t.Errorf("oid len expected 24, got: %d", len(oid))
}
}
在当前路径下执行 go test
命令,可以看到输出结果如下:
PASS
ok github.com/zhoushuguang/lebron/apps/order/rpc/internal/logic 1.395s
还可以加上 -v
输出更完整的结果,go test -v
输出结果如下:
=== RUN TestGenOrderID
--- PASS: TestGenOrderID (0.00s)
PASS
ok github.com/zhoushuguang/lebron/apps/order/rpc/internal/logic 1.305s
在执行 go test
命令的时候可以添加 -run
参数,它对应一个正则表达式,又有函数名匹配上的测试函数才会被 go test
命令执行,例如我们可以使用 go test -run=TestGenOrderID
来值运行 TestGenOrderID
这个单测。
表格驱动测试不是工具,它只是编写更清晰测试的一种方式和视角。编写好的测试并不是一件容易的事情,但在很多情况下,表格驱动测试可以涵盖很多方面,表格里的每一个条目都是一个完整的测试用例,它包含输入和预期的结果,有时还包含测试名称等附加信息,以使测试输出易于阅读。使用表格测试能够很方便的维护多个测试用例,避免在编写单元测试时频繁的复制粘贴。
在 lebron/apps/product/rpc/internal/logic/checkandupdatestocklogic.go:53
我们可以编写如下表格驱动测试:
func TestStockKey(t *testing.T) {
tests := []struct {
name string
input int64
output string
}{
{"test one", 1, "stock:1"},
{"test two", 2, "stock:2"},
{"test three", 3, "stock:3"},
} for _, ts := range tests {
t.Run(ts.name, func(t *testing.T) {
ret := stockKey(ts.input)
if ret != ts.output {
t.Errorf("input: %d expectd: %s got: %s", ts.input, ts.output, ret)
}
})
}
}
执行命令 go test -run=TestStockKey -v
输出如下:
=== RUN TestStockKey
=== RUN TestStockKey/test_one
=== RUN TestStockKey/test_two
=== RUN TestStockKey/test_three
--- PASS: TestStockKey (0.00s)
--- PASS: TestStockKey/test_one (0.00s)
--- PASS: TestStockKey/test_two (0.00s)
--- PASS: TestStockKey/test_three (0.00s)
PASS
ok github.com/zhoushuguang/lebron/apps/product/rpc/internal/logic 1.353s
表格驱动测试中通常会定义比较多的测试用例,而go语言又天生支持并发,所以很容易发挥自身优势将表格驱动测试并行化,可以通过t.Parallel()
来实现:
func TestStockKeyParallel(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input int64
output string
}{
{"test one", 1, "stock:1"},
{"test two", 2, "stock:2"},
{"test three", 3, "stock:3"},
} for _, ts := range tests {
ts := ts
t.Run(ts.name, func(t *testing.T) {
t.Parallel()
ret := stockKey(ts.input)
if ret != ts.output {
t.Errorf("input: %d expectd: %s got: %s", ts.input, ts.output, ret)
}
})
}
}
测试覆盖率是指代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总的代码的比例。go提供内置的功能来检查代码覆盖率,即使用 go test -cover
来查看测试覆盖率:
PASS
coverage: 0.6% of statements
ok github.com/zhoushuguang/lebron/apps/product/rpc/internal/logic 1.381s
可以看到我们的覆盖率只有 0.6% ,哈哈,这是非常不合格滴,大大的不合格。go还提供了一个 -coverprofile
参数,用来将覆盖率相关的记录输出到文件 go test -cover -coverprofile=cover.out
:
PASS
coverage: 0.6% of statements
ok github.com/zhoushuguang/lebron/apps/product/rpc/internal/logic 1.459s
然后执行 go tool cover -html=cover.out
,使用cover
工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成测试报告
对于单测中的依赖,我们一般采用mock的方式进行处理,gomock是Go官方提供的测试框架,它在内置的testing包或其他环境中都能够很方便的使用。我们使用它对代码中的那些接口类型进行mock,方便编写单元测试。对于gomock的使用请参考gomock文档
mock依赖interface,对于非interface场景下的依赖我们可以采用打桩的方式进行mock数据,monkey是一个Go单元测试中十分常用的打桩工具,它在运行时通过汇编语言重写可执行文件,将目标函数或方法的实现跳转到桩实现,其原理类似于热补丁。monkey库很强大,但是使用时需注意以下事项:
-gcflags=-l
关闭Go语言的内联优化。社区中经常有人问画图用的是什么工具,本系列文章中的插图工具主要是如下两个
https://www.onemodel.app/
https://whimsical.com/
代码不光是要实现功能,很重要的一点是代码是写给别人看的,所以我们对代码的质量要有一定的要求,要遵循规范,可以参考go官方的代码review建议
https://github.com/golang/go/wiki/CodeReviewComments
时间过得贼快,不知不觉间这个系列已经写到十一篇了。按照每周更新两篇的速度也写了一个多月了。写文章是个体力活且非常的耗时,又生怕有写的不对的地方,对大家产生误导,所以还需要反复的检查和查阅相关资料。平均一篇文章要写一天左右,平时工作日比较忙,基本都是周六日来写,因此最近一个月周六日基本没有休息过。
但我觉得收获也非常大,在写文章的过程中,对于自己掌握的知识点,是一个复习的过程,可以让自己加深对知识点的理解,对于自己没有掌握的知识点就又是一个学习新知识的过程,让自己掌握了新的知识,所以我和读者也是一起在学习进步呢。大家都知道,对于自己理解的知识,想要说出来或者写出来让别人也理解也是不容易的,因此写文章对自己的软实力也是有很大的提升。
所以,我还是会继续坚持写文章,坚持输出,和大家一起学习成长。同时,我也欢迎大家来 "微服务实践" 公众号来投稿。可能有些人觉得自己的水平不行,担心写的内容不高端,没有逼格,我觉得大可不必,只要能把知识点讲明白就非常棒了,可以是基础知识,也可以是最佳实践等等。kevin会对投稿的每一篇文章都认真审核,写的不对的地方他都会指出来,所有还有和kevin一对一交流学习的机会,小伙伴们抓紧行动起来呀。
非常感谢大家这一个多月以来的支持。看到每篇文章有那么多的点赞,我十分的开心,也更加的有动力,所以,也在计划写下个系列的文章,目前有两个待选的主题,分别是《go-zero源码系列》和《gRPC实战源码系列》,欢迎小伙伴们在评论区留下你的评论,说出你更期待哪个系列,如果本篇文章点赞数超过66的话,咱就继续开整。
代码仓库: https://github.com/zhoushuguang/lebron
推荐阅读