mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-23 20:02:22 +00:00
remove redundant words
This commit is contained in:
parent
8d23f5bbf5
commit
47c9d3459a
@ -146,7 +146,7 @@ func handleKafka(app *ApplicationContext, w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
```
|
||||
|
||||
因为默认的`net/http`包中的`mux`不支持带参数的路由,所以Burrow这个项目使用了非常蹩脚的字符串`Split`和乱七八糟的 `switch case`来达到自己的目的,但实际上却让本来应该很集中的路由管理逻辑变得复杂,散落在系统的各处,难以维护和管理。如果读者细心地看过这些代码之后,可能会发现其它的几个`handler`函数逻辑上较简单,最复杂的也就是这个`handleKafka()`。但实际上我们的系统总是从这样微不足道的混乱开始积少成多,最终变得难以收拾。
|
||||
因为默认的`net/http`包中的`mux`不支持带参数的路由,所以Burrow这个项目使用了非常蹩脚的字符串`Split`和乱七八糟的 `switch case`来达到自己的目的,但却让本来应该很集中的路由管理逻辑变得复杂,散落在系统的各处,难以维护和管理。如果读者细心地看过这些代码之后,可能会发现其它的几个`handler`函数逻辑上较简单,最复杂的也就是这个`handleKafka()`。而我们的系统总是从这样微不足道的混乱开始积少成多,最终变得难以收拾。
|
||||
|
||||
根据我们的经验,简单地来说,只要你的路由带有参数,并且这个项目的API数目超过了10,就尽量不要使用`net/http`中默认的路由。在Go开源界应用最广泛的router是httpRouter,很多开源的router框架都是基于httpRouter进行一定程度的改造的成果。关于httpRouter路由的原理,会在本章节的router一节中进行详细的阐释。
|
||||
|
||||
|
@ -98,7 +98,7 @@ func helloHandler(wr http.ResponseWriter, r *http.Request) {
|
||||
|
||||
我们来分析一下,一开始在哪里做错了呢?我们只是一步一步地满足需求,把我们需要的逻辑按照流程写下去呀?
|
||||
|
||||
实际上,我们犯的最大的错误是把业务代码和非业务代码揉在了一起。对于大多数的场景来讲,非业务的需求都是在http请求处理前做一些事情,并且在响应完成之后做一些事情。我们有没有办法使用一些重构思路把这些公共的非业务功能代码剥离出去呢?回到刚开头的例子,我们需要给我们的`helloHandler()`增加超时时间统计,我们可以使用一种叫`function adapter`的方法来对`helloHandler()`进行包装:
|
||||
我们犯的最大的错误,是把业务代码和非业务代码揉在了一起。对于大多数的场景来讲,非业务的需求都是在http请求处理前做一些事情,并且在响应完成之后做一些事情。我们有没有办法使用一些重构思路把这些公共的非业务功能代码剥离出去呢?回到刚开头的例子,我们需要给我们的`helloHandler()`增加超时时间统计,我们可以使用一种叫`function adapter`的方法来对`helloHandler()`进行包装:
|
||||
|
||||
```go
|
||||
|
||||
@ -147,7 +147,7 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
|
||||
}
|
||||
|
||||
```
|
||||
实际上只要你的handler函数签名是:
|
||||
只要你的handler函数签名是:
|
||||
|
||||
```go
|
||||
func (ResponseWriter, *Request)
|
||||
@ -187,7 +187,7 @@ customizedHandler = logger(timeout(ratelimit(helloHandler)))
|
||||
|
||||
*图 5-8 请求处理过程*
|
||||
|
||||
再直白一些,这个流程在进行请求处理的时候实际上就是不断地进行函数压栈再出栈,有一些类似于递归的执行流:
|
||||
再直白一些,这个流程在进行请求处理的时候就是不断地进行函数压栈再出栈,有一些类似于递归的执行流:
|
||||
|
||||
```
|
||||
[exec of logger logic] 函数栈: []
|
||||
@ -286,5 +286,5 @@ throttler.go
|
||||
|
||||
*图 5-9 gin的中间件仓库*
|
||||
|
||||
如果读者去阅读gin的源码的话,可能会发现gin的中间件中处理的并不是`http.Handler`,而是一个叫`gin.HandlerFunc`的函数类型,和本节中讲解的`http.Handler`签名并不一样。不过实际上gin的`handler`也只是针对其框架的一种封装,中间件的原理与本节中的说明是一致的。
|
||||
如果读者去阅读gin的源码的话,可能会发现gin的中间件中处理的并不是`http.Handler`,而是一个叫`gin.HandlerFunc`的函数类型,和本节中讲解的`http.Handler`签名并不一样。不过gin的`handler`也只是针对其框架的一种封装,中间件的原理与本节中的说明是一致的。
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
*图 5-10 validator流程*
|
||||
|
||||
实际上这是一个语言无关的场景,需要进行字段校验的情况有很多,Web系统的Form或JSON提交只是一个典型的例子。我们用Go来写一个类似上图的校验示例。然后研究怎么一步步对其进行改进。
|
||||
这其实是一个语言无关的场景,需要进行字段校验的情况有很多,Web系统的Form或JSON提交只是一个典型的例子。我们用Go来写一个类似上图的校验示例。然后研究怎么一步步对其进行改进。
|
||||
|
||||
## 5.4.1 重构请求校验函数
|
||||
|
||||
|
@ -31,7 +31,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
我们需要衡量一下这个Web服务的吞吐量,再具体一些,实际上就是接口的QPS。借助wrk,在家用电脑 Macbook Pro上对这个 `hello world` 服务进行基准测试,Mac的硬件情况如下:
|
||||
我们需要衡量一下这个Web服务的吞吐量,再具体一些,就是接口的QPS。借助wrk,在家用电脑 Macbook Pro上对这个 `hello world` 服务进行基准测试,Mac的硬件情况如下:
|
||||
|
||||
```shell
|
||||
CPU: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
|
||||
@ -148,7 +148,7 @@ func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool {}
|
||||
|
||||
## 5.6.2 原理
|
||||
|
||||
从功能上来看,令牌桶模型实际上就是对全局计数的加减法操作过程,但使用计数需要我们自己加读写锁,有小小的思想负担。如果我们对Go语言已经比较熟悉的话,很容易想到可以用buffered channel来完成简单的加令牌取令牌操作:
|
||||
从功能上来看,令牌桶模型就是对全局计数的加减法操作过程,但使用计数需要我们自己加读写锁,有小小的思想负担。如果我们对Go语言已经比较熟悉的话,很容易想到可以用buffered channel来完成简单的加令牌取令牌操作:
|
||||
|
||||
```go
|
||||
var tokenBucket = make(chan struct{}, capacity)
|
||||
|
@ -74,7 +74,7 @@ func HTTPCreateOrderHandler(wr http.ResponseWriter, r *http.Request) {
|
||||
|
||||
理论上我们可以用同一个请求结构体组合上不同的tag,来达到一个结构体来给不同的协议复用的目的。不过遗憾的是在thrift中,请求结构体也是通过IDL生成的,其内容在自动生成的ttypes.go文件中,我们还是需要在thrift的入口将这个自动生成的结构体映射到我们logic入口所需要的结构体上。gRPC也是类似。这部分代码还是需要的。
|
||||
|
||||
聪明的读者可能已经可以看出来了,协议细节处理这一层实际上有大量重复劳动,每一个接口在协议这一层的处理,无非是把数据从协议特定的结构体(例如`http.Request`,thrift的被包装过了) 读出来,再绑定到我们协议无关的结构体上,再把这个结构体映射到Controller入口的结构体上,这些代码实际上长得都差不多。差不多的代码都遵循着某种模式,那么我们可以对这些模式进行简单的抽象,用代码生成的方式,把繁复的协议处理代码从工作内容中抽离出去。
|
||||
聪明的读者可能已经可以看出来了,协议细节处理这一层有大量重复劳动,每一个接口在协议这一层的处理,无非是把数据从协议特定的结构体(例如`http.Request`,thrift的被包装过了) 读出来,再绑定到我们协议无关的结构体上,再把这个结构体映射到Controller入口的结构体上,这些代码长得都差不多。差不多的代码都遵循着某种模式,那么我们可以对这些模式进行简单的抽象,用代码生成的方式,把繁复的协议处理代码从工作内容中抽离出去。
|
||||
|
||||
先来看看HTTP对应的结构体、thrift对应的结构体和我们协议无关的结构体分别长什么样子:
|
||||
|
||||
@ -106,7 +106,7 @@ type CreateOrderParams struct {
|
||||
|
||||
```
|
||||
|
||||
我们需要通过一个源结构体来生成我们需要的HTTP和thrift入口代码。再观察一下上面定义的三种结构体,实际上我们只要能用一个结构体生成thrift的IDL,以及HTTP服务的“IDL(实际上就是带json或form相关tag的结构体定义)” 就可以了。这个初始的结构体我们可以把结构体上的HTTP的tag和thrift的tag揉在一起:
|
||||
我们需要通过一个源结构体来生成我们需要的HTTP和thrift入口代码。再观察一下上面定义的三种结构体,我们只要能用一个结构体生成thrift的IDL,以及HTTP服务的“IDL(只要能包含json或form相关tag的结构体定义信息)” 就可以了。这个初始的结构体我们可以把结构体上的HTTP的tag和thrift的tag揉在一起:
|
||||
|
||||
```go
|
||||
type FeatureSetParams struct {
|
||||
|
@ -74,7 +74,7 @@ type OrderCreator interface {
|
||||
|
||||
我们只要把之前写过的步骤函数签名都提到一个接口中,就可以完成抽象了。
|
||||
|
||||
在进行抽象之前,我们应该想明白的一点是,引入接口对我们的系统本身是否有意义,这是要按照场景去进行分析的。假如我们的系统只服务一条产品线,并且内部的代码只是针对很具体的场景进行定制化开发,那么实际上引入接口是不会带来任何收益的。至于说是否方便测试,这一点我们会在之后的章节来讲。
|
||||
在进行抽象之前,我们应该想明白的一点是,引入接口对我们的系统本身是否有意义,这是要按照场景去进行分析的。假如我们的系统只服务一条产品线,并且内部的代码只是针对很具体的场景进行定制化开发,那么引入接口是不会带来任何收益的。至于说是否方便测试,这一点我们会在之后的章节来讲。
|
||||
|
||||
如果我们正在做的是平台系统,需要由平台来定义统一的业务流程和业务规范,那么基于接口的抽象就是有意义的。举个例子:
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
*图 5-20 分组部署*
|
||||
|
||||
为什么要用2倍?这样能够保证我们不管有多少台机器,都不会把组划分得太多。例如1024台机器,实际上也就只需要1-2-4-8-16-32-64-128-256-512部署十次就可以全部部署完毕。
|
||||
为什么要用2倍?这样能够保证我们不管有多少台机器,都不会把组划分得太多。例如1024台机器,也就只需要1-2-4-8-16-32-64-128-256-512部署十次就可以全部部署完毕。
|
||||
|
||||
这样我们上线最开始影响到的用户在整体用户中占的比例也不大,比如1000台机器的服务,我们上线后如果出现问题,也只影响1/1000的用户。如果10组完全平均分,那一上线立刻就会影响1/10的用户,1/10的业务出问题,那可能对于公司来说就已经是一场不可挽回的事故了。
|
||||
|
||||
@ -249,7 +249,7 @@ PASS
|
||||
ok _/Users/caochunhui/test/go/hash_bench 7.050s
|
||||
```
|
||||
|
||||
可见murmurhash相比其它的算法有三倍以上的性能提升。显然做负载均衡的话,用murmurhash要比md5和sha1都要好,实际上这些年社区里还有另外一些更高效的哈希算法出现,感兴趣的读者可以自行调研。
|
||||
可见murmurhash相比其它的算法有三倍以上的性能提升。显然做负载均衡的话,用murmurhash要比md5和sha1都要好,这些年社区里还有另外一些更高效的哈希算法涌现,感兴趣的读者可以自行调研。
|
||||
|
||||
### 5.9.3.3 分布是否均匀
|
||||
|
||||
|
@ -16,7 +16,7 @@ Twitter的snowflake算法是这种场景下的一个典型解法。先来看看s
|
||||
|
||||
数据中心加上实例id共有10位,可以支持我们每数据中心部署32台机器,所有数据中心共1024台实例。
|
||||
|
||||
表示`timestamp`的41位,可以支持我们使用69年。当然,我们的时间毫秒计数不会真的从1970年开始记,那样我们的系统跑到`2039/9/7 23:47:35`就不能用了,所以这里的`timestamp`实际上只是相对于某个时间的增量,比如我们的系统上线是2018-08-01,那么我们可以把这个timestamp当作是从`2018-08-01 00:00:00.000`的偏移量。
|
||||
表示`timestamp`的41位,可以支持我们使用69年。当然,我们的时间毫秒计数不会真的从1970年开始记,那样我们的系统跑到`2039/9/7 23:47:35`就不能用了,所以这里的`timestamp`只是相对于某个时间的增量,比如我们的系统上线是2018-08-01,那么我们可以把这个timestamp当作是从`2018-08-01 00:00:00.000`的偏移量。
|
||||
|
||||
## 6.1.1 worker_id分配
|
||||
|
||||
|
@ -137,7 +137,7 @@ func main() {
|
||||
|
||||
在单机系统中,trylock并不是一个好选择。因为大量的goroutine抢锁可能会导致CPU无意义的资源浪费。有一个专有名词用来描述这种抢锁的场景:活锁。
|
||||
|
||||
活锁指的是程序看起来在正常执行,但实际上CPU周期被浪费在抢锁,而非执行任务上,从而程序整体的执行效率低下。活锁的问题定位起来要麻烦很多。所以在单机场景下,不建议使用这种锁。
|
||||
活锁指的是程序看起来在正常执行,但CPU周期被浪费在抢锁,而非执行任务上,从而程序整体的执行效率低下。活锁的问题定位起来要麻烦很多。所以在单机场景下,不建议使用这种锁。
|
||||
|
||||
## 6.2.3 基于Redis的setnx
|
||||
|
||||
@ -226,7 +226,7 @@ current counter is 2028
|
||||
unlock success!
|
||||
```
|
||||
|
||||
通过代码和执行结果可以看到,我们远程调用`setnx`实际上和单机的trylock非常相似,如果获取锁失败,那么相关的任务逻辑就不应该继续向前执行。
|
||||
通过代码和执行结果可以看到,我们远程调用`setnx`运行流程上和单机的trylock非常相似,如果获取锁失败,那么相关的任务逻辑就不应该继续向前执行。
|
||||
|
||||
`setnx`很适合在高并发场景下,用来争抢一些“唯一”的资源。比如交易撮合系统中卖家发起订单,而多个买家会对其进行并发争抢。这种场景我们没有办法依赖具体的时间来判断先后,因为不管是用户设备的时间,还是分布式场景下的各台机器的时间,都是没有办法在合并后保证正确的时序的。哪怕是我们同一个机房的集群,不同的机器的系统时间可能也会有细微的差别。
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
1. 实现一套类似crontab的分布式定时任务管理系统。
|
||||
2. 实现一个支持定时发送消息的消息队列。
|
||||
|
||||
两种思路进而衍生出了一些不同的系统,但其本质是差不多的。都是需要实现一个定时器(timer)。在单机的场景下定时器其实并不少见,例如我们在和网络库打交道的时候经常会调用`SetReadDeadline()`函数,这实际上就是在本地创建了一个定时器,在到达指定的时间后,我们会收到定时器的通知,告诉我们时间已到。这时候如果读取还没有完成的话,就可以认为发生了网络问题,从而中断读取。
|
||||
两种思路进而衍生出了一些不同的系统,但其本质是差不多的。都是需要实现一个定时器(timer)。在单机的场景下定时器其实并不少见,例如我们在和网络库打交道的时候经常会调用`SetReadDeadline()`函数,就是在本地创建了一个定时器,在到达指定的时间后,我们会收到定时器的通知,告诉我们时间已到。这时候如果读取还没有完成的话,就可以认为发生了网络问题,从而中断读取。
|
||||
|
||||
下面我们从定时器开始,探究延时任务系统的实现。
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
|
||||
*图 6-4 二叉堆结构*
|
||||
|
||||
小顶堆的好处是什么呢?实际上对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。定时检查的时间复杂度是`O(1)`。
|
||||
小顶堆的好处是什么呢?对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。定时检查的时间复杂度是`O(1)`。
|
||||
|
||||
当我们发现堆顶的元素小于当前时间时,那么说明可能已经有一批事件已经开始过期了,这时进行正常的弹出和堆调整操作就好。每一次堆调整的时间复杂度都是`O(LgN)`。
|
||||
|
||||
|
@ -171,7 +171,7 @@ if field_1 == 1 || field_2 == 2 {
|
||||
3 < i && x > 10
|
||||
```
|
||||
|
||||
es的`Bool Query`方案,实际上就是用json来表达了这种程序语言中的Boolean Expression,为什么可以这么做呢?因为json本身是可以表达树形结构的,我们的程序代码在被编译器parse之后,也会变成AST,而AST抽象语法树,顾名思义,就是树形结构。理论上json能够完备地表达一段程序代码被parse之后的结果。这里的Boolean Expression被编译器Parse之后也会生成差不多的树形结构,而且只是整个编译器实现的一个很小的子集。
|
||||
es的`Bool Query`方案,就是用json来表达了这种程序语言中的Boolean Expression,为什么可以这么做呢?因为json本身是可以表达树形结构的,我们的程序代码在被编译器parse之后,也会变成AST,而AST抽象语法树,顾名思义,就是树形结构。理论上json能够完备地表达一段程序代码被parse之后的结果。这里的Boolean Expression被编译器Parse之后也会生成差不多的树形结构,而且只是整个编译器实现的一个很小的子集。
|
||||
|
||||
### 基于client SDK做开发
|
||||
|
||||
@ -266,7 +266,7 @@ func deleteDocument(
|
||||
}
|
||||
```
|
||||
|
||||
因为Lucene的性质,本质上搜索引擎内的数据是不可变的,所以如果要对文档进行更新,实际上是按照id进行完全覆盖的操作,所以与插入的情况是一样的。
|
||||
因为Lucene的性质,本质上搜索引擎内的数据是不可变的,所以如果要对文档进行更新,Lucene内部是按照id进行完全覆盖(本质是取同一 id 最新的segment中的数据)的操作,所以与插入的情况是一样的。
|
||||
|
||||
使用es作为数据库使用时,需要注意,因为es有索引合并的操作,所以数据插入到es中到可以查询的到需要一段时间(由es的refresh_interval决定)。所以千万不要把es当成强一致的关系型数据库来使用。
|
||||
|
||||
@ -369,7 +369,7 @@ SQL的where部分就是boolean expression。我们之前提到过,这种bool
|
||||
|
||||
*图 6-13 基于时间戳的数据同步*
|
||||
|
||||
这种同步方式与业务强绑定,例如WMS系统中的出库单,我们并不需要非常实时,稍微有延迟也可以接受,那么我们可以每分钟从MySQL的出库单表中,把最近十分钟创建的所有出库单取出,批量存入es中,具体的逻辑实际上就是一条SQL:
|
||||
这种同步方式与业务强绑定,例如WMS系统中的出库单,我们并不需要非常实时,稍微有延迟也可以接受,那么我们可以每分钟从MySQL的出库单表中,把最近十分钟创建的所有出库单取出,批量存入es中,取数据的操作需要执行的逻辑可以表达为下面的SQL:
|
||||
|
||||
```sql
|
||||
select * from wms_orders where update_time >= date_sub(now(), interval 10 minute);
|
||||
@ -393,6 +393,6 @@ select * from wms_orders where update_time >= date_sub(
|
||||
|
||||
业界使用较多的是阿里开源的Canal,来进行binlog解析与同步。canal会伪装成MySQL的从库,然后解析好行格式的binlog,再以更容易解析的格式(例如json)发送到消息队列。
|
||||
|
||||
由下游的Kafka消费者负责把上游数据表的自增主键作为es的文档的id进行写入,这样可以保证每次接收到binlog时,对应id的数据都被覆盖更新为最新。MySQL的Row格式的binlog会将每条记录的所有字段都提供给下游,所以实际上在向异构数据目标同步数据时,不需要考虑数据是插入还是更新,只要一律按id进行覆盖即可。
|
||||
由下游的Kafka消费者负责把上游数据表的自增主键作为es的文档的id进行写入,这样可以保证每次接收到binlog时,对应id的数据都被覆盖更新为最新。MySQL的Row格式的binlog会将每条记录的所有字段都提供给下游,所以在向异构数据目标同步数据时,不需要考虑数据是插入还是更新,只要一律按id进行覆盖即可。
|
||||
|
||||
这种模式同样需要业务遵守一条数据表规范,即表中必须有唯一主键id来保证我们进入es的数据不会发生重复。一旦不遵守该规范,那么就会在同步时导致数据重复。当然,你也可以为每一张需要的表去定制消费者的逻辑,这就不是通用系统讨论的范畴了。
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
## 6.5.1 常见的负载均衡思路
|
||||
|
||||
如果我们不考虑均衡的话,现在有n个服务节点,我们完成业务流程实际上只需要从这n个中挑出其中的一个。有几种思路:
|
||||
如果我们不考虑均衡的话,现在有n个服务节点,我们完成业务流程只需要从这n个中挑出其中的一个。有几种思路:
|
||||
|
||||
1. 按顺序挑: 例如上次选了第一台,那么这次就选第二台,下次第三台,如果已经到了最后一台,那么下一次从第一台开始。这种情况下我们可以把服务节点信息都存储在数组中,每次请求完成下游之后,将一个索引后移即可。在移到尽头时再移回数组开头处。
|
||||
|
||||
@ -69,7 +69,7 @@ func request(params map[string]interface{}) error {
|
||||
|
||||
### 6.5.2.1 错误的洗牌导致的负载不均衡
|
||||
|
||||
真的没有问题么?实际上还是有问题的。这段简短的程序里有两个隐藏的隐患:
|
||||
真的没有问题么?还是有问题的。这段简短的程序里有两个隐藏的隐患:
|
||||
|
||||
1. 没有随机种子。在没有随机种子的情况下,`rand.Intn()`返回的伪随机数序列是固定的。
|
||||
|
||||
@ -93,7 +93,7 @@ func shuffle(indexes []int) {
|
||||
}
|
||||
```
|
||||
|
||||
在Go的标准库中实际上已经为我们内置了该算法:
|
||||
在Go的标准库中已经为我们内置了该算法:
|
||||
|
||||
```go
|
||||
func shuffle(n int) []int {
|
||||
|
Loading…
x
Reference in New Issue
Block a user