mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-24 04:22:22 +00:00
fix
This commit is contained in:
parent
42f4b525fe
commit
2bc48ddcfc
@ -52,7 +52,7 @@ func main() {
|
|||||||
|
|
||||||
如果你过了30s还没有完成这个程序,请检查一下你自己的打字速度是不是慢了(开个玩笑 :D)。这个例子是为了说明在Go中写一个HTTP协议的小程序有多么简单。如果你面临的情况比较复杂,例如几十个接口的企业级应用,直接用`net/http`库就显得不太合适了。
|
如果你过了30s还没有完成这个程序,请检查一下你自己的打字速度是不是慢了(开个玩笑 :D)。这个例子是为了说明在Go中写一个HTTP协议的小程序有多么简单。如果你面临的情况比较复杂,例如几十个接口的企业级应用,直接用`net/http`库就显得不太合适了。
|
||||||
|
|
||||||
我们来看看开源社区中一个 kafka 监控项目中的做法:
|
我们来看看开源社区中一个Kafka监控项目中的做法:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
//Burrow: http_server.go
|
//Burrow: http_server.go
|
||||||
@ -69,7 +69,7 @@ func NewHttpServer(app *ApplicationContext) (*HttpServer, error) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面这段代码来自大名鼎鼎的linkedin公司的kafka监控项目 Burrow,没有使用任何router框架,只使用了`net/http`。只看上面这段代码似乎非常优雅,我们的项目里大概只有这五个简单的 URI,所以我们提供的服务就是下面这个样子:
|
上面这段代码来自大名鼎鼎的linkedin公司的Kafka监控项目 Burrow,没有使用任何router框架,只使用了`net/http`。只看上面这段代码似乎非常优雅,我们的项目里大概只有这五个简单的 URI,所以我们提供的服务就是下面这个样子:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
/
|
/
|
||||||
@ -150,7 +150,7 @@ func handleKafka(app *ApplicationContext, w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
根据我们的经验,简单地来说,只要你的路由带有参数,并且这个项目的API数目超过了10,就尽量不要使用`net/http`中默认的路由。在Go开源界应用最广泛的router是httpRouter,很多开源的router框架都是基于httpRouter进行一定程度的改造的成果。关于httpRouter路由的原理,会在本章节的router一节中进行详细的阐释。
|
根据我们的经验,简单地来说,只要你的路由带有参数,并且这个项目的API数目超过了10,就尽量不要使用`net/http`中默认的路由。在Go开源界应用最广泛的router是httpRouter,很多开源的router框架都是基于httpRouter进行一定程度的改造的成果。关于httpRouter路由的原理,会在本章节的router一节中进行详细的阐释。
|
||||||
|
|
||||||
再来回顾一下文章开头说的,开源界有这么几种框架,第一种是对httpRouter进行简单的封装,然后提供定制的middleware和一些简单的小工具集成比如gin,主打轻量,易学,高性能。第二种是借鉴其它语言的编程风格的一些MVC类框架,例如beego,方便从其它语言迁移过来的程序员快速上手,快速开发。还有一些框架功能更为强大,除了数据库schema设计,大部分代码直接生成,例如goa。不管哪种框架,适合开发者背景的就是最好的。
|
再来回顾一下文章开头说的,开源界有这么几种框架,第一种是对httpRouter进行简单的封装,然后提供定制的中间件和一些简单的小工具集成比如gin,主打轻量,易学,高性能。第二种是借鉴其它语言的编程风格的一些MVC类框架,例如beego,方便从其它语言迁移过来的程序员快速上手,快速开发。还有一些框架功能更为强大,除了数据库schema设计,大部分代码直接生成,例如goa。不管哪种框架,适合开发者背景的就是最好的。
|
||||||
|
|
||||||
本章的内容除了会展开讲解router和middleware的原理外,还会以现在工程界面临的问题结合Go来进行一些实践性的说明。希望能够对没有接触过相关内容的读者有所帮助。
|
本章的内容除了会展开讲解router和中间件的原理外,还会以现在工程界面临的问题结合Go来进行一些实践性的说明。希望能够对没有接触过相关内容的读者有所帮助。
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# 5.3 middleware 中间件
|
# 5.3 中间件
|
||||||
|
|
||||||
本章将对现在流行的Web框架中的中间件技术原理进行分析,并介绍如何使用中间件技术将业务和非业务代码功能进行解耦。
|
本章将对现在流行的Web框架中的中间件(middleware)技术原理进行分析,并介绍如何使用中间件技术将业务和非业务代码功能进行解耦。
|
||||||
|
|
||||||
## 5.3.1 代码泥潭
|
## 5.3.1 代码泥潭
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ func helloHandler(wr http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
修改到这里,本能地发现我们的开发工作开始陷入了泥潭。无论未来对我们的这个 Web 系统有任何其它的非功能或统计需求,我们的修改必然牵一发而动全身。只要增加一个非常简单的非业务统计,我们就需要去几十个handler里增加这些业务无关的代码。虽然一开始我们似乎并没有做错,但是显然随着业务的发展,我们的行事方式让我们陷入了代码的泥潭。
|
修改到这里,本能地发现我们的开发工作开始陷入了泥潭。无论未来对我们的这个 Web 系统有任何其它的非功能或统计需求,我们的修改必然牵一发而动全身。只要增加一个非常简单的非业务统计,我们就需要去几十个handler里增加这些业务无关的代码。虽然一开始我们似乎并没有做错,但是显然随着业务的发展,我们的行事方式让我们陷入了代码的泥潭。
|
||||||
|
|
||||||
## 5.3.2 使用 middleware 剥离非业务逻辑
|
## 5.3.2 使用中间件剥离非业务逻辑
|
||||||
|
|
||||||
我们来分析一下,一开始在哪里做错了呢?我们只是一步一步地满足需求,把我们需要的逻辑按照流程写下去呀?
|
我们来分析一下,一开始在哪里做错了呢?我们只是一步一步地满足需求,把我们需要的逻辑按照流程写下去呀?
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
这样就非常轻松地实现了业务与非业务之间的剥离,魔法就在于这个timeMiddleware。可以从代码中看到,我们的`timeMiddleware()`也是一个函数,其参数为`http.Handler`,`http.Handler`的定义在`net/http`包中:
|
这样就非常轻松地实现了业务与非业务之间的剥离,魔法就在于这个`timeMiddleware`。可以从代码中看到,我们的`timeMiddleware()`也是一个函数,其参数为`http.Handler`,`http.Handler`的定义在`net/http`包中:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
@ -207,7 +207,7 @@ customizedHandler = logger(timeout(ratelimit(helloHandler)))
|
|||||||
|
|
||||||
功能实现了,但在上面的使用过程中我们也看到了,这种函数套函数的用法不是很美观,同时也不具备什么可读性。
|
功能实现了,但在上面的使用过程中我们也看到了,这种函数套函数的用法不是很美观,同时也不具备什么可读性。
|
||||||
|
|
||||||
## 5.3.3 更优雅的 middleware 写法
|
## 5.3.3 更优雅的中间件写法
|
||||||
|
|
||||||
上一节中解决了业务功能代码和非业务功能代码的解耦,但也提到了,看起来并不美观,如果需要修改这些函数的顺序,或者增删中间件还是有点费劲,本节我们来进行一些“写法”上的优化。
|
上一节中解决了业务功能代码和非业务功能代码的解耦,但也提到了,看起来并不美观,如果需要修改这些函数的顺序,或者增删中间件还是有点费劲,本节我们来进行一些“写法”上的优化。
|
||||||
|
|
||||||
@ -221,7 +221,7 @@ r.Use(ratelimit)
|
|||||||
r.Add("/", helloHandler)
|
r.Add("/", helloHandler)
|
||||||
```
|
```
|
||||||
|
|
||||||
通过多步设置,我们拥有了和上一节差不多的执行函数链。胜在直观易懂,如果我们要增加或者删除middleware,只要简单地增加删除对应的Use调用就可以了。非常方便。
|
通过多步设置,我们拥有了和上一节差不多的执行函数链。胜在直观易懂,如果我们要增加或者删除中间件,只要简单地增加删除对应的`Use()`调用就可以了。非常方便。
|
||||||
|
|
||||||
从框架的角度来讲,怎么实现这样的功能呢?也不复杂:
|
从框架的角度来讲,怎么实现这样的功能呢?也不复杂:
|
||||||
|
|
||||||
@ -252,22 +252,22 @@ func (r *Router) Add(route string, h http.Handler) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
注意代码中的middleware数组遍历顺序,和用户希望的调用顺序应该是"相反"的。应该不难理解。
|
注意代码中的`middleware`数组遍历顺序,和用户希望的调用顺序应该是"相反"的。应该不难理解。
|
||||||
|
|
||||||
|
|
||||||
## 5.3.4 哪些事情适合在 middleware 中做
|
## 5.3.4 哪些事情适合在中间件中做
|
||||||
|
|
||||||
以较流行的开源Go语言框架chi为例:
|
以较流行的开源Go语言框架chi为例:
|
||||||
|
|
||||||
```
|
```
|
||||||
compress.go
|
compress.go
|
||||||
=> 对 http 的 response body 进行压缩处理
|
=> 对http的响应体进行压缩处理
|
||||||
heartbeat.go
|
heartbeat.go
|
||||||
=> 设置一个特殊的路由,例如 /ping,/healthcheck,用来给 load balancer 一类的前置服务进行探活
|
=> 设置一个特殊的路由,例如/ping,/healthcheck,用来给负载均衡一类的前置服务进行探活
|
||||||
logger.go
|
logger.go
|
||||||
=> 打印 request 处理日志,例如请求处理时间,请求路由
|
=> 打印请求处理处理日志,例如请求处理时间,请求路由
|
||||||
profiler.go
|
profiler.go
|
||||||
=> 挂载 pprof 需要的路由,如 /pprof、/pprof/trace 到系统中
|
=> 挂载pprof需要的路由,如`/pprof`、`/pprof/trace`到系统中
|
||||||
realip.go
|
realip.go
|
||||||
=> 从请求头中读取X-Forwarded-For和X-Real-IP,将http.Request中的RemoteAddr修改为得到的RealIP
|
=> 从请求头中读取X-Forwarded-For和X-Real-IP,将http.Request中的RemoteAddr修改为得到的RealIP
|
||||||
requestid.go
|
requestid.go
|
||||||
@ -278,13 +278,13 @@ throttler.go
|
|||||||
=> 通过定长大小的channel存储token,并通过这些token对接口进行限流
|
=> 通过定长大小的channel存储token,并通过这些token对接口进行限流
|
||||||
```
|
```
|
||||||
|
|
||||||
每一个Web框架都会有对应的middleware组件,如果你有兴趣,也可以向这些项目贡献有用的middleware,只要合理一般项目的维护人也愿意合并你的Pull Request。
|
每一个Web框架都会有对应的中间件组件,如果你有兴趣,也可以向这些项目贡献有用的中间件,只要合理一般项目的维护人也愿意合并你的Pull Request。
|
||||||
|
|
||||||
比如开源界很火的gin这个框架,就专门为用户贡献的middleware开了一个仓库,见*图 5-9*:
|
比如开源界很火的gin这个框架,就专门为用户贡献的中间件开了一个仓库,见*图 5-9*:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
*图 5-9 *
|
*图 5-9 *
|
||||||
|
|
||||||
如果读者去阅读gin的源码的话,可能会发现gin的middleware中处理的并不是`http.Handler`,而是一个叫`gin.HandlerFunc`的函数类型,和本节中讲解的`http.Handler`签名并不一样。不过实际上gin的handler也只是针对其框架的一种封装,middleware的原理与本节中的说明是一致的。
|
如果读者去阅读gin的源码的话,可能会发现gin的中间件中处理的并不是`http.Handler`,而是一个叫`gin.HandlerFunc`的函数类型,和本节中讲解的`http.Handler`签名并不一样。不过实际上gin的`handler`也只是针对其框架的一种封装,中间件的原理与本节中的说明是一致的。
|
||||||
|
|
||||||
|
@ -138,12 +138,12 @@ type FeatureSetParams struct {
|
|||||||
|
|
||||||
比如在前面的生成环境引入Web页面,只要让用户点点鼠标就能生成SDK,这些就靠读者自己去探索了。
|
比如在前面的生成环境引入Web页面,只要让用户点点鼠标就能生成SDK,这些就靠读者自己去探索了。
|
||||||
|
|
||||||
虽然我们成功地使自己的项目在入口支持了多种交互协议,但是还有一些问题没有解决。本节中所叙述的分层没有将middleware作为项目的分层考虑进去。如果我们考虑中间件的话,请求的流程是什么样的?见*图 5-18*所示。
|
虽然我们成功地使自己的项目在入口支持了多种交互协议,但是还有一些问题没有解决。本节中所叙述的分层没有将中间件作为项目的分层考虑进去。如果我们考虑中间件的话,请求的流程是什么样的?见*图 5-18*所示。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
*图 5-18 加入中间件后的控制流*
|
*图 5-18 加入中间件后的控制流*
|
||||||
|
|
||||||
之前我们学习的中间件是和http协议强相关的,遗憾的是在thrift中看起来没有和http中对等的解决这些非功能性逻辑代码重复问题的middleware。所以我们在图上写`thrift stuff`。这些`stuff`可能需要你手写去实现,然后每次增加一个新的thrift接口,就需要去写一遍这些非功能性代码。
|
之前我们学习的中间件是和http协议强相关的,遗憾的是在thrift中看起来没有和http中对等的解决这些非功能性逻辑代码重复问题的中间件。所以我们在图上写`thrift stuff`。这些`stuff`可能需要你手写去实现,然后每次增加一个新的thrift接口,就需要去写一遍这些非功能性代码。
|
||||||
|
|
||||||
这也是很多企业项目所面临的真实问题,遗憾的是开源界并没有这样方便的多协议中间件解决方案。当然了,前面我们也说过,很多时候我们给自己保留的http接口只是用来做debug,并不会暴露给外人用。这种情况下,这些非功能性的代码只要在thrift的代码中完成即可。
|
这也是很多企业项目所面临的真实问题,遗憾的是开源界并没有这样方便的多协议中间件解决方案。当然了,前面我们也说过,很多时候我们给自己保留的http接口只是用来做debug,并不会暴露给外人用。这种情况下,这些非功能性的代码只要在thrift的代码中完成即可。
|
||||||
|
@ -129,11 +129,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
因为我们的逻辑限定每个 goroutine 只有成功执行了 Lock 才会继续执行后续逻辑,因此在 Unlock 时可以保证 Lock struct 中的 channel 一定是空,从而不会阻塞,也不会失败。
|
因为我们的逻辑限定每个goroutine只有成功执行了`Lock`才会继续执行后续逻辑,因此在`Unlock`时可以保证Lock结构体中的channel一定是空,从而不会阻塞,也不会失败。
|
||||||
|
|
||||||
在单机系统中,trylock 并不是一个好选择。因为大量的 goroutine 抢锁可能会导致 cpu 无意义的资源浪费。有一个专有名词用来描述这种抢锁的场景:活锁。
|
在单机系统中,trylock并不是一个好选择。因为大量的goroutine抢锁可能会导致CPU无意义的资源浪费。有一个专有名词用来描述这种抢锁的场景:活锁。
|
||||||
|
|
||||||
活锁指的是程序看起来在正常执行,但实际上 cpu 周期被浪费在抢锁,而非执行任务上,从而程序整体的执行效率低下。活锁的问题定位起来要麻烦很多。所以在单机场景下,不建议使用这种锁。
|
活锁指的是程序看起来在正常执行,但实际上CPU周期被浪费在抢锁,而非执行任务上,从而程序整体的执行效率低下。活锁的问题定位起来要麻烦很多。所以在单机场景下,不建议使用这种锁。
|
||||||
|
|
||||||
## 6.2.3 基于 Redis 的 setnx
|
## 6.2.3 基于 Redis 的 setnx
|
||||||
|
|
||||||
@ -373,7 +373,7 @@ Redlock也是一种阻塞锁,单个节点操作对应的是 `set nx px` 命令
|
|||||||
|
|
||||||
业务还在单机就可以搞定的量级时,那么按照需求使用任意的单机锁方案就可以。
|
业务还在单机就可以搞定的量级时,那么按照需求使用任意的单机锁方案就可以。
|
||||||
|
|
||||||
如果发展到了分布式服务阶段,但业务规模不大,比如 qps < 1000,使用哪种锁方案都差不多。如果公司内已有可以使用的 ZooKeeper、etcd或者Redis集群,那么就尽量在不引入新的技术栈的情况下满足业务需求。
|
如果发展到了分布式服务阶段,但业务规模不大,qps很小的情况下,使用哪种锁方案都差不多。如果公司内已有可以使用的ZooKeeper、etcd或者Redis集群,那么就尽量在不引入新的技术栈的情况下满足业务需求。
|
||||||
|
|
||||||
业务发展到一定量级的话,就需要从多方面来考虑了。首先是你的锁是否在任何恶劣的条件下都不允许数据丢失,如果不允许,那么就不要使用Redis的setnx的简单锁。
|
业务发展到一定量级的话,就需要从多方面来考虑了。首先是你的锁是否在任何恶劣的条件下都不允许数据丢失,如果不允许,那么就不要使用Redis的setnx的简单锁。
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
我们在做系统时,很多时候是处理实时的任务,请求来了马上就处理,然后立刻给用户以反馈。但有时也会遇到非实时的任务,比如确定的时间点发布重要公告。或者需要在用户做了一件事情的X分钟/Y小时后,对其特定动作,比如通知、发券等等。
|
我们在做系统时,很多时候是处理实时的任务,请求来了马上就处理,然后立刻给用户以反馈。但有时也会遇到非实时的任务,比如确定的时间点发布重要公告。或者需要在用户做了一件事情的X分钟/Y小时后,对其特定动作,比如通知、发券等等。
|
||||||
|
|
||||||
如果业务规模比较小,有时我们也可以通过 db + 轮询来对这种任务进行简单处理,但上了规模的公司,自然会寻找更为普适的解决方案来解决这一类问题。
|
如果业务规模比较小,有时我们也可以通过数据库配合轮询来对这种任务进行简单处理,但上了规模的公司,自然会寻找更为普适的解决方案来解决这一类问题。
|
||||||
|
|
||||||
一般有两种思路来解决这个问题:
|
一般有两种思路来解决这个问题:
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ timer 的实现在工业界已经是有解的问题了。常见的就是时间
|
|||||||
|
|
||||||
小顶堆的好处是什么呢?实际上对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。所以对于定时check来说,时间复杂度是O(1)的。
|
小顶堆的好处是什么呢?实际上对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。所以对于定时check来说,时间复杂度是O(1)的。
|
||||||
|
|
||||||
当我们发现堆顶的元素 < 当前时间时,那么说明可能已经有一批事件已经开始过期了,这时进行正常的弹出和堆调整操作就好。每一次堆调整的时间复杂度都是 O(LgN)。
|
当我们发现堆顶的元素小于当前时间时,那么说明可能已经有一批事件已经开始过期了,这时进行正常的弹出和堆调整操作就好。每一次堆调整的时间复杂度都是O(LgN)。
|
||||||
|
|
||||||
Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆,而是使用了扁平一些的四叉堆。在最近的版本中,还加了一些优化,我们先不说优化,先来看看四叉的小顶堆长什么样:
|
Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆,而是使用了扁平一些的四叉堆。在最近的版本中,还加了一些优化,我们先不说优化,先来看看四叉的小顶堆长什么样:
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ Go 自身的 timer 就是用时间堆来实现的,不过并没有使用二叉
|
|||||||
|
|
||||||
下面给出一种思路:
|
下面给出一种思路:
|
||||||
|
|
||||||
我们可以参考 elasticsearch 的设计,每份任务数据都有多个副本,这里假设两副本:
|
我们可以参考Elasticsearch的设计,每份任务数据都有多个副本,这里假设两副本:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -74,12 +74,12 @@ Go 自身的 timer 就是用时间堆来实现的,不过并没有使用二叉
|
|||||||
|
|
||||||
一个任务只会在持有主副本的节点上被执行。
|
一个任务只会在持有主副本的节点上被执行。
|
||||||
|
|
||||||
当有机器故障时,任务数据需要进行 rebalance 工作,比如 node 1 挂了:
|
当有机器故障时,任务数据需要进行数据再平衡的工作,比如节点1挂了:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
node 1 的数据会被迁移到 node 2 和 node 3 上。
|
节点1的数据会被迁移到节点2和节点3上。
|
||||||
|
|
||||||
当然,也可以用稍微复杂一些的思路,比如对集群中的节点进行角色划分,由协调节点来做这种故障时的任务重新分配工作,考虑到高可用,协调节点可能也需要有1 ~ 2个备用节点以防不测。
|
当然,也可以用稍微复杂一些的思路,比如对集群中的节点进行角色划分,由协调节点来做这种故障时的任务重新分配工作,考虑到高可用,协调节点可能也需要有1 ~ 2个备用节点以防不测。
|
||||||
|
|
||||||
之前提到我们会用 MQ 触发对用户的通知,在使用 MQ 时,很多 MQ 是不支持 exactly once 的语义的,这种情况下我们需要让用户自己来负责消息的去重或者消费的幂等处理。
|
之前提到我们会用消息队列触发对用户的通知,在使用消息队列时,很多队列是不支持`exactly once`的语义的,这种情况下我们需要让用户自己来负责消息的去重或者消费的幂等处理。
|
||||||
|
@ -169,7 +169,7 @@ if field_1 == 1 || field_2 == 2 {
|
|||||||
|
|
||||||
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 做开发
|
### 基于client SDK做开发
|
||||||
|
|
||||||
初始化:
|
初始化:
|
||||||
|
|
||||||
@ -383,6 +383,6 @@ select * from wms_orders where update_time >= date_sub(
|
|||||||
|
|
||||||
业界使用较多的是阿里开源的Canal,来进行binlog解析与同步。canal会伪装成MySQL的从库,然后解析好行格式的binlog,再以更容易解析的格式(例如json)发送到消息队列。
|
业界使用较多的是阿里开源的Canal,来进行binlog解析与同步。canal会伪装成MySQL的从库,然后解析好行格式的binlog,再以更容易解析的格式(例如json)发送到消息队列。
|
||||||
|
|
||||||
由下游的kafka消费者负责把上游数据表的自增主键作为es的document的id进行写入,这样可以保证每次接收到binlog时,对应id的数据都被覆盖更新为最新。MySQL的row格式的binlog会将每条记录的所有字段都提供给下游,所以实际上在向异构数据目标同步数据时,不需要考虑数据是插入还是更新,只要一律按id进行覆盖即可。
|
由下游的Kafka消费者负责把上游数据表的自增主键作为es的document的id进行写入,这样可以保证每次接收到binlog时,对应id的数据都被覆盖更新为最新。MySQL的row格式的binlog会将每条记录的所有字段都提供给下游,所以实际上在向异构数据目标同步数据时,不需要考虑数据是插入还是更新,只要一律按id进行覆盖即可。
|
||||||
|
|
||||||
这种模式同样需要业务遵守一条数据表规范,即表中必须有唯一主键id来保证我们进入es的数据不会发生重复。一旦不遵守该规范,那么就会在同步时导致数据重复。当然,你也可以为每一张需要的表去定制消费者的逻辑,这就不是通用系统讨论的范畴了。
|
这种模式同样需要业务遵守一条数据表规范,即表中必须有唯一主键id来保证我们进入es的数据不会发生重复。一旦不遵守该规范,那么就会在同步时导致数据重复。当然,你也可以为每一张需要的表去定制消费者的逻辑,这就不是通用系统讨论的范畴了。
|
||||||
|
@ -75,7 +75,7 @@ func request(params map[string]interface{}) error {
|
|||||||
|
|
||||||
2. 洗牌不均匀,会导致整个数组第一个节点有大概率被选中,并且多个节点的负载分布不均衡。
|
2. 洗牌不均匀,会导致整个数组第一个节点有大概率被选中,并且多个节点的负载分布不均衡。
|
||||||
|
|
||||||
第一点比较简单,应该不用在这里给出证明了。关于第二点,我们可以用概率知识来简单证明一下。假设每次挑选都是真随机,我们假设第一个位置的 endpoint 在 len(slice) 次交换中都不被选中的概率是 ((6/7)*(6/7))^7 ≈ 0.34。而分布均匀的情况下,我们肯定希望被第一个元素在任意位置上分布的概率均等,所以其被随机选到的概率应该 ≈ 1/7 ≈ 0.14。
|
第一点比较简单,应该不用在这里给出证明了。关于第二点,我们可以用概率知识来简单证明一下。假设每次挑选都是真随机,我们假设第一个位置的endpoint在len(slice)次交换中都不被选中的概率是((6/7)*(6/7))^7 ≈ 0.34。而分布均匀的情况下,我们肯定希望被第一个元素在任意位置上分布的概率均等,所以其被随机选到的概率应该约等于1/7≈0.14。
|
||||||
|
|
||||||
显然,这里给出的洗牌算法对于任意位置的元素来说,有30%的概率不对其进行交换操作。所以所有元素都倾向于留在原来的位置。因为我们每次对shuffle数组输入的都是同一个序列,所以第一个元素有更大的概率会被选中。在负载均衡的场景下,也就意味着endpoints数组中的第一台机器负载会比其它机器高不少(这里至少是3倍以上)。
|
显然,这里给出的洗牌算法对于任意位置的元素来说,有30%的概率不对其进行交换操作。所以所有元素都倾向于留在原来的位置。因为我们每次对shuffle数组输入的都是同一个序列,所以第一个元素有更大的概率会被选中。在负载均衡的场景下,也就意味着endpoints数组中的第一台机器负载会比其它机器高不少(这里至少是3倍以上)。
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ func shuffle(n int) []int {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
在当前的场景下,我们只要用 rand.Perm 就可以得到我们想要的索引数组了。
|
在当前的场景下,我们只要用`rand.Perm`就可以得到我们想要的索引数组了。
|
||||||
|
|
||||||
## 6.5.3 ZooKeeper 集群的随机节点挑选问题
|
## 6.5.3 ZooKeeper 集群的随机节点挑选问题
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
除业务线管理之外,很多互联网公司会按照城市来铺展自己的业务。在某个城市未开城之前,理论上所有模块都应该认为带有该城市id的数据是脏数据并自动过滤掉。而如果业务开城,在系统中就应该自己把这个新的城市id自动加入到白名单中。这样业务流程便可以自动运转。
|
除业务线管理之外,很多互联网公司会按照城市来铺展自己的业务。在某个城市未开城之前,理论上所有模块都应该认为带有该城市id的数据是脏数据并自动过滤掉。而如果业务开城,在系统中就应该自己把这个新的城市id自动加入到白名单中。这样业务流程便可以自动运转。
|
||||||
|
|
||||||
再举个例子,互联网公司的运营系统中会有各种类型的运营活动,有些运营活动推出后可能出现了超出预期的事件(比如公关危机),需要紧急将系统下线。这时候会用到一些开关来快速关闭相应的功能。或者快速将想要剔除的活动 id 从白名单中剔除。在 Web 章节中的 ab test 一节中,我们也提到,有时需要有这样的系统来告诉我们当前需要放多少流量到相应的功能代码上。我们可以像那一节中,使用远程 rpc 来获知这些信息,但同时,也可以结合分布式配置系统,主动地拉取到这些信息。
|
再举个例子,互联网公司的运营系统中会有各种类型的运营活动,有些运营活动推出后可能出现了超出预期的事件(比如公关危机),需要紧急将系统下线。这时候会用到一些开关来快速关闭相应的功能。或者快速将想要剔除的活动id从白名单中剔除。在Web章节中的AB测试一节中,我们也提到,有时需要有这样的系统来告诉我们当前需要放多少流量到相应的功能代码上。我们可以像那一节中,使用远程RPC来获知这些信息,但同时,也可以结合分布式配置系统,主动地拉取到这些信息。
|
||||||
|
|
||||||
## 6.6.2 使用 etcd 实现配置更新
|
## 6.6.2 使用 etcd 实现配置更新
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ if err != nil {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
获取配置使用 etcd KeysAPI 的 Get 方法,比较简单。
|
获取配置使用etcd KeysAPI的`Get()`方法,比较简单。
|
||||||
|
|
||||||
### 6.6.2.4 配置更新订阅
|
### 6.6.2.4 配置更新订阅
|
||||||
|
|
||||||
@ -188,7 +188,7 @@ func main() {
|
|||||||
|
|
||||||
在业务系统的配置被剥离到配置中心之后,并不意味着我们的系统可以高枕无忧了。当配置中心本身宕机时,我们也需要一定的容错能力,至少保证在其宕机期间,业务依然可以运转。这要求我们的系统能够在配置中心宕机时,也能拿到需要的配置信息。哪怕这些信息不够新。
|
在业务系统的配置被剥离到配置中心之后,并不意味着我们的系统可以高枕无忧了。当配置中心本身宕机时,我们也需要一定的容错能力,至少保证在其宕机期间,业务依然可以运转。这要求我们的系统能够在配置中心宕机时,也能拿到需要的配置信息。哪怕这些信息不够新。
|
||||||
|
|
||||||
具体来讲,在给业务提供配置读取的 sdk 时,最好能够将拿到的配置在业务机器的磁盘上也缓存一份。这样远程配置中心不可用时,可以直接用硬盘上的内容来做兜底。当重新连接上配置中心时,再把相应的内容进行更新。
|
具体来讲,在给业务提供配置读取的SDK时,最好能够将拿到的配置在业务机器的磁盘上也缓存一份。这样远程配置中心不可用时,可以直接用硬盘上的内容来做兜底。当重新连接上配置中心时,再把相应的内容进行更新。
|
||||||
|
|
||||||
加入缓存之后务必需要考虑的是数据一致性问题,当个别业务机器因为网络错误而与其它机器配置不一致时,我们也应该能够从监控系统中知晓。
|
加入缓存之后务必需要考虑的是数据一致性问题,当个别业务机器因为网络错误而与其它机器配置不一致时,我们也应该能够从监控系统中知晓。
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ func main() {
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
上游的主要工作是根据预先配置好的起点来爬取所有的目标“列表页”,列表页的 html 内容中会包含有所有详情页的链接。详情页的数量一般是列表页的 10~100 倍,所以我们将这些详情页链接作为“任务”内容,通过 mq 分发出去。
|
上游的主要工作是根据预先配置好的起点来爬取所有的目标“列表页”,列表页的html内容中会包含有所有详情页的链接。详情页的数量一般是列表页的10~100倍,所以我们将这些详情页链接作为“任务”内容,通过消息队列分发出去。
|
||||||
|
|
||||||
针对页面爬取来说,在执行时是否偶尔会有重复其实不太重要,因为任务结果是幂等的(这里我们只爬页面内容,不考虑评论部分)。
|
针对页面爬取来说,在执行时是否偶尔会有重复其实不太重要,因为任务结果是幂等的(这里我们只爬页面内容,不考虑评论部分)。
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ nc.Flush()
|
|||||||
|
|
||||||
直接使用nats的subscribe API并不能达到任务分发的目的,因为pub sub本身是广播性质的。所有消费者都会收到完全一样的所有消息。
|
直接使用nats的subscribe API并不能达到任务分发的目的,因为pub sub本身是广播性质的。所有消费者都会收到完全一样的所有消息。
|
||||||
|
|
||||||
除了普通的subscribe之外,nats还提供了queue subscribe的功能。只要提供一个queue group名字(类似kafka中的 consumer group),即可均衡地将任务分发给消费者。
|
除了普通的subscribe之外,nats还提供了queue subscribe的功能。只要提供一个queue group名字(类似Kafka中的consumer group),即可均衡地将任务分发给消费者。
|
||||||
|
|
||||||
```go
|
```go
|
||||||
nc, err := nats.Connect(nats.DefaultURL)
|
nc, err := nats.Connect(nats.DefaultURL)
|
||||||
@ -132,7 +132,7 @@ if err != nil {
|
|||||||
|
|
||||||
// queue subscribe 相当于在消费者之间进行任务分发的分支均衡
|
// queue subscribe 相当于在消费者之间进行任务分发的分支均衡
|
||||||
// 前提是所有消费者都使用 workers 这个 queue
|
// 前提是所有消费者都使用 workers 这个 queue
|
||||||
// nats 中的 queue 概念上类似于 kafka 中的 consumer group
|
// nats 中的 queue 概念上类似于 Kafka 中的 consumer group
|
||||||
sub, err := nc.QueueSubscribeSync("tasks", "workers")
|
sub, err := nc.QueueSubscribeSync("tasks", "workers")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// log error
|
// log error
|
||||||
|
Loading…
x
Reference in New Issue
Block a user