Skip to content

Commit

Permalink
update mark
Browse files Browse the repository at this point in the history
  • Loading branch information
cch123 committed Aug 21, 2018
1 parent 4d07d68 commit 7a02e92
Show file tree
Hide file tree
Showing 13 changed files with 77 additions and 68 deletions.
14 changes: 7 additions & 7 deletions ch5-web/ch5-02-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ DELETE /user/starred/:owner/:repo

如果我们的系统也想要这样的 URI 设计,使用标准库的 mux 显然就力不从心了。

## httprouter
## 5.2.1 httprouter

较流行的开源 golang web 框架大多使用 httprouter,或是基于 httprouter 的变种对路由进行支持。前面提到的 github 的参数式路由在 httprouter 中都是可以支持的。

Expand Down Expand Up @@ -103,7 +103,7 @@ r.PanicHandler = func(w http.ResponseWriter, r *http.Request, c interface{}) {

目前开源界最为流行(star 数最多)的 web 框架 [gin](https://github.com/gin-gonic/gin) 使用的就是 httprouter 的变种。

## 原理
## 5.2.2 原理

httprouter 和众多衍生 router 使用的数据结构被称为 radix tree,压缩字典树。读者可能没有接触过压缩字典树,但对字典树 trie tree 应该有所耳闻。下图是一个典型的字典树结构:

Expand All @@ -117,7 +117,7 @@ httprouter 和众多衍生 router 使用的数据结构被称为 radix tree,

每个节点上不只存储一个字母了,这也是压缩字典树中“压缩”的主要含义。使用压缩字典树可以减少树的层数,同时因为每个节点上数据存储也比通常的字典树要多,所以程序的局部性较好(一个节点的 path 加载到 cache 即可进行多个字符的对比),从而对 CPU 缓存友好。

## 压缩字典树创建过程
## 5.2.3 压缩字典树创建过程

我们来跟踪一下 httprouter 中,一个典型的压缩字典树的创建过程,路由设定如下:

Expand All @@ -136,7 +136,7 @@ GET /marketplace_listing/plans/ohyes

最后一条补充路由是我们臆想的,除此之外所有 API 路由均来自于 api.github.com。

### root 节点创建
### 5.2.3.1 root 节点创建

httprouter 的 Router struct 中存储压缩字典树使用的是下述数据结构:

Expand Down Expand Up @@ -192,7 +192,7 @@ indices: 子节点索引,当子节点为非参数类型,即本节点的 wild

当然,PUT 路由只有唯一的一条路径。接下来,我们以后续的多条 GET 路径为例,讲解子节点的插入过程。

### 子节点插入
### 5.2.3.2 子节点插入

当插入 `GET /marketplace_listing/plans` 时,类似前面 PUT 的过程,GET 树的结构如图所示:
![get radix step 1](../images/ch6-02-radix-get-1.png)
Expand All @@ -207,7 +207,7 @@ indices: 子节点索引,当子节点为非参数类型,即本节点的 wild

上面这种情况比较简单,新的路由可以直接作为原路由的子节点进行插入。实际情况不会这么美好。

### 边分裂
### 5.2.3.3 边分裂

接下来我们插入 `GET /search`,这时会导致树的边分裂。

Expand All @@ -219,7 +219,7 @@ indices: 子节点索引,当子节点为非参数类型,即本节点的 wild

![get radix step 4](../images/ch6-02-radix-get-4.png)

### 子节点冲突处理
### 5.2.3.4 子节点冲突处理

在路由本身只有字符串的情况下,不会发生任何冲突。只有当路由中含有 wildcard(类似 :id) 或者 catchAll 的情况下才可能冲突。这一点在前面已经提到了。

Expand Down
8 changes: 4 additions & 4 deletions ch5-web/ch5-03-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

本章将对现在流行的 web 框架中的中间件技术原理进行分析,并介绍如何使用中间件技术将业务和非业务代码功能进行解耦。

## 代码泥潭
## 5.3.1 代码泥潭

先来看一段代码:

Expand Down Expand Up @@ -94,7 +94,7 @@ func helloHandler(wr http.ResponseWriter, r *http.Request) {

修改到这里,本能地发现我们的开发工作开始陷入了泥潭。无论未来对我们的这个 web 系统有任何其它的非功能或统计需求,我们的修改必然牵一发而动全身。只要增加一个非常简单的非业务统计,我们就需要去几十个 handler 里增加这些业务无关的代码。虽然一开始我们似乎并没有做错,但是显然随着业务的发展,我们的行事方式让我们陷入了代码的泥潭。

## 使用 middleware 剥离非业务逻辑
## 5.3.2 使用 middleware 剥离非业务逻辑

我们来分析一下,一开始在哪里做错了呢?我们只是一步一步地满足需求,把我们需要的逻辑按照流程写下去呀?

Expand Down Expand Up @@ -205,7 +205,7 @@ customizedHandler = logger(timeout(ratelimit(helloHandler)))

功能实现了,但在上面的使用过程中我们也看到了,这种函数套函数的用法不是很美观,同时也不具备什么可读性。

## 更优雅的 middleware 写法
## 5.3.3 更优雅的 middleware 写法

上一节中解决了业务功能代码和非业务功能代码的解耦,但也提到了,看起来并不美观,如果需要修改这些函数的顺序,或者增删 middleware 还是有点费劲,本节我们来进行一些“写法”上的优化。

Expand Down Expand Up @@ -253,7 +253,7 @@ func (r *Router) Add(route string, h http.Handler) {
注意代码中的 middleware 数组遍历顺序,和用户希望的调用顺序应该是"相反"的。应该不难理解。


## 哪些事情适合在 middleware 中做
## 5.3.4 哪些事情适合在 middleware 中做

以较流行的开源 golang 框架 chi 为例:

Expand Down
6 changes: 3 additions & 3 deletions ch5-web/ch5-04-validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

实际上这是一个语言无关的场景,需要进行字段校验的情况有很多,web 系统的 Form/json 提交只是一个典型的例子。我们用 go 来写一个类似上图的校验 demo。然后研究怎么一步步对其进行改进。

## 重构请求校验函数
## 5.4.1 重构请求校验函数

假设我们的数据已经通过某个 binding 库绑定到了具体的 struct 上。

Expand Down Expand Up @@ -69,7 +69,7 @@ func register(req RegisterReq) error{

代码更清爽,看起来也不那么别扭了。这是比较通用的重构理念。虽然使用了重构方法使我们的 validate 过程看起来优雅了,但我们还是得为每一个 http 请求都去写这么一套差不多的 validate 函数,有没有更好的办法来帮助我们解除这项体力劳动?答案就是 validator。

## 用 validator 解放体力劳动
## 5.4.2 用 validator 解放体力劳动

从设计的角度讲,我们一定会为每个请求都声明一个 struct。前文中提到的校验场景我们都可以通过 validator 完成工作。还以前文中的 struct 为例。为了美观起见,我们先把 json tag 省略掉。

Expand Down Expand Up @@ -121,7 +121,7 @@ fmt.Println(err) // Key: 'RegisterReq.PasswordRepeat' Error:Field validation for

如果觉得这个 validator 提供的错误信息不够人性化,例如要把错误信息返回给用户,那就不应该直接显示英文了。可以针对每种 tag 进行错误信息定制,读者可以自行探索。

## 原理
## 5.4.3 原理

从结构上来看,每一个 struct 都可以看成是一棵树。假如我们有如下定义的 struct:

Expand Down
6 changes: 3 additions & 3 deletions ch5-web/ch5-05-database.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

本节将对 db/sql 官方标准库作一些简单分析,并介绍一些应用比较广泛的开源 ORM 和 sql builder。并从企业级应用开发和公司架构的角度来分析哪种技术栈对于现代的企业级应用更为合适。

## 从 database/sql 讲起
## 5.5.1 从 database/sql 讲起

Go官方提供了 `database/sql` 包来给用户进行和数据库打交道的工作,实际上 `database/sql` 库就只是提供了一套操作数据库的接口和规范,例如抽象好的 sql 预处理(prepare),连接池管理,数据绑定,事务,错误处理等等。官方并没有提供具体某种数据库实现的协议支持。

Expand Down Expand Up @@ -105,7 +105,7 @@ func main() {

是的,所以社区才会有各种各样的 sql builder 和 orm 百花齐放。

## 提高生产效率的 ORM 和 SQL Builder
## 5.5.2 提高生产效率的 ORM 和 SQL Builder

在 web 开发领域常常提到的 ORM 是什么?我们先看看万能的维基百科:

Expand Down Expand Up @@ -179,7 +179,7 @@ orders := orderModel.GetList(where, limit, orderBy)

一旦你做的是高并发的 OLTP 在线系统,且想在人员充足分工明确的前提下最大程度控制系统的风险,使用 sql builder 就不合适了。

## 脆弱的 db
## 5.5.3 脆弱的 db

无论是 ORM 还是 sql builder 都有一个致命的缺点,就是没有办法进行系统上线的事前 sql 审核。虽然很多 orm 和 sql builder 也提供了运行期打印 sql 的功能,但只在查询的时候才能进行输出。而 sql builder 和 ORM本身提供的功能太过灵活。使得你不可能通过测试枚举出所有可能在线上执行的 sql。例如你可能用 sql builder 写出下面这样的代码:

Expand Down
6 changes: 3 additions & 3 deletions ch5-web/ch5-06-ratelimit.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Transfer/sec: 5.51MB

不管我们的服务瓶颈在哪里,最终要做的事情都是一样的,那就是流量限制。

## 常见的流量限制手段
## 5.6.1 常见的流量限制手段

流量限制的手段有很多,最常见的:漏桶、令牌桶两种:

Expand Down Expand Up @@ -142,7 +142,7 @@ func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool {}

名称和功能都比较直观,这里就不再赘述了。相比于开源界更为有名的 google 的 Java 工具库 Guava 中提供的 ratelimiter,这个库不支持令牌桶预热,且无法修改初始的令牌容量,所以可能个别极端情况下的需求无法满足。但在明白令牌桶的基本原理之后,如果没办法满足需求,相信你也可以很快对其进行修改并支持自己的业务场景。

## 原理
## 5.6.2 原理

从功能上来看,令牌桶模型实际上就是对全局计数的加减法操作过程,但使用计数需要我们自己加读写锁,有小小的思想负担。如果我们对 Go 语言已经比较熟悉的话,很容易想到可以用 buffered channel 来完成简单的加令牌取令牌操作:

Expand Down Expand Up @@ -255,7 +255,7 @@ cur = cur > cap ? cap : cur

在得到正确的令牌数之后,再进行实际的 Take 操作就好,这个 Take 操作只需要对令牌数进行简单的减法即可,记得加锁以保证并发安全。`github.com/juju/ratelimit` 这个库就是这样做的。

## 服务瓶颈和 QoS
## 5.6.3 服务瓶颈和 QoS

前面我们说了很多 CPU-bound、IO-bound 之类的概念,这种性能瓶颈从大多数公司都有的监控系统中可以比较快速地定位出来,如果一个系统遇到了性能问题,那监控图的反应一般都是最快的。

Expand Down
10 changes: 5 additions & 5 deletions ch5-web/ch5-08-interface-and-web.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@

我们可以思考一下怎么缓解这个问题。

## 业务系统的发展过程
## 5.8.1 业务系统的发展过程

互联网公司只要可以活过三年,工程方面面临的首要问题就是代码膨胀。系统的代码膨胀之后,可以将系统中与业务本身流程无关的部分做拆解和异步化。什么算是业务无关呢,比如一些统计、反作弊、营销发券、价格计算、用户状态更新等等需求。这些需求往往依赖于主流程的数据,但又只是挂在主流程上的旁支,自成体系。

这时候我们就可以把这些旁支拆解出去,作为独立的系统来部署、开发以及维护。这些旁支流程的时延如若非常敏感,比如用户在界面上点了按钮,需要立刻返回(价格计算、支付),那么需要与主流程系统进行 RPC 通信,并且在通信失败时,要将结果直接返回给用户。如果时延不敏感,比如抽奖系统,结果稍后公布的这种,或者非实时的统计类系统,那么就没有必要在主流程里为每一套系统做一套 RPC 流程。我们只要将下游需要的数据打包成一条消息,传入消息队列,之后的事情与主流程一概无关(当然,与用户的后续交互流程还是要做的)。

通过拆解和异步化虽然解决了一部分问题,但并不能解决所有问题。随着业务发展,单一职责的模块也会变得越来越复杂,这是必然的趋势。一件事情本身变的复杂的话,这时候拆解和异步化就不灵了。我们还是要对事情本身进行一定程度的封装抽象。

## 使用函数封装业务流程
## 5.8.2 使用函数封装业务流程

最基本的封装过程,我们把相似的行为放在一起,然后打包成一个一个的函数,让自己杂乱无章的代码变成下面这个样子:

Expand Down Expand Up @@ -52,7 +52,7 @@ func CreateOrder() {

在阅读业务流程代码时,我们只要阅读其函数名就能知晓在该流程中完成了哪些操作,如果需要修改细节,那么就继续深入到每一个业务步骤去看具体的流程。写得稀烂的业务流程代码则会将所有过程都堆积在少数的几个函数中,从而导致几百甚至上千行的函数。这种意大利面条式的代码阅读和维护都会非常痛苦。在开发的过程中,一旦有条件应该立即进行类似上面这种方式的简单封装。

## 使用 interface 来做抽象
## 5.8.3 使用 interface 来做抽象

业务发展的早期,是不适宜引入 interface 的,很多时候业务流程变化很大,过早引入 interface 会使业务系统本身增加很多不必要的分层,从而导致每次修改几乎都要全盘否定之前的工作。

Expand Down Expand Up @@ -156,7 +156,7 @@ func BusinessProcess(bi BusinessInstance) {

直接面向 interface 编程,而不用关心具体的实现了。如果对应的业务在迭代中发生了修改,所有的逻辑对平台方来说也是完全透明的。

## interface 的优缺点
## 5.8.4 interface 的优缺点

Go 被人称道的最多的地方是其 interface 设计的正交性,模块之间不需要知晓相互的存在,A 模块定义 interface,B 模块实现这个 interface 就可以。如果 interface 中没有 A 模块中定义的数据类型,那 B 模块中甚至都不用 import A。比如标准库中的 `io.Writer`

Expand Down Expand Up @@ -236,7 +236,7 @@ func main() {

所以 interface 也可以认为是一种编译期进行检查的保证类型安全的手段。

## table-driven 开发
## 5.8.5 table-driven 开发

熟悉开源 lint 工具的同学应该见到过圈复杂度的说法,在函数中如果有 if 和 switch 的话,会使函数的圈复杂度上升,所以有强迫症的同学即使在入口一个函数中有 switch,还是想要干掉这个 switch,有没有什么办法呢?当然有,用表驱动的方式来存储我们需要实例:

Expand Down
14 changes: 7 additions & 7 deletions ch5-web/ch5-09-gated-launch.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

在对系统的旧功能进行升级迭代时,第一种方式用的比较多。新功能上线时,第二种方式用的比较多。当然,对比较重要的老功能进行较大幅度的修改时,一般也会选择按业务规则来进行发布,因为直接全量开放给所有用户风险实在太大。

## 通过分批次部署实现灰度发布
## 5.9.1 通过分批次部署实现灰度发布

假如服务部署在 15 个实例(可能是物理机,也可能是容器)上,我们把这 7 个实例分为三组,按照先后顺序,分别有 1-2-4-8 台机器,保证每次扩展时大概都是二倍的关系。

Expand Down Expand Up @@ -48,7 +48,7 @@

如果有异常情况,首先要做的自然就是回滚了。

## 通过业务规则进行灰度发布
## 5.9.2 通过业务规则进行灰度发布

常见的灰度策略有多种,较为简单的需求,例如我们的策略是要按照千分比来发布,那么我们可以用用户 id、手机号、用户设备信息,等等,来生成一个简单的哈希值,然后再求模,用伪代码表示一下:

Expand All @@ -64,7 +64,7 @@ func passed() bool {
}
```

### 可选规则
### 5.9.2.1 可选规则

常见的灰度发布系统会有下列规则提供选择:

Expand Down Expand Up @@ -134,11 +134,11 @@ func isTrue(phone string) bool {
+--------+
```

## 如何实现一套灰度发布系统
## 5.9.3 如何实现一套灰度发布系统

前面也提到了,提供给用户的接口大概可以分为和业务绑定的简单灰度判断逻辑。以及输入稍微复杂一些的哈希灰度。我们来分别看看怎么实现这样的灰度系统(函数)。

### 业务相关的简单灰度
### 5.9.3.1 业务相关的简单灰度

公司内一般都会有公共的城市名字和 id 的映射关系,如果业务只涉及中国国内,那么城市数量不会特别多,且 id 可能都在 10000 范围以内。那么我们只要开辟一个一万大小左右的 bool 数组,就可以满足需求了:

Expand Down Expand Up @@ -207,7 +207,7 @@ func isPassed(rate int) bool {
注意初始化种子。
### 哈希算法
### 5.9.3.2 哈希算法
求哈希可用的算法非常多,比如 md5,crc32,sha1 等等,但我们这里的目的只是为了给这些数据做个映射,并不想要因为计算哈希消耗过多的 cpu,所以现在业界使用较多的算法是 murmurhash,下面是我们对这些常见的 hash 算法的简单 benchmark:
Expand Down Expand Up @@ -287,7 +287,7 @@ ok _/Users/caochunhui/test/go/hash_bench 7.050s
可见 murmurhash 相比其它的算法有三倍以上的性能提升。
#### 分布是否均匀
### 5.9.3.3 分布是否均匀
对于哈希算法来说,性能是一方面的问题,另一方面还要考虑哈希后的值是否分布均匀。
Expand Down
10 changes: 5 additions & 5 deletions ch6-cloud/ch6-02-dist-id.md → ch6-cloud/ch6-01-dist-id.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 6.2 分布式 id 生成器
# 6.1 分布式 id 生成器

有时我们需要能够生成类似 MySQL 自增 ID 这样不断增大,同时又不会重复的 id。以支持业务中的高并发场景。比较典型的,电商促销时,短时间内会有大量的订单涌入到系统,比如每秒 10w+。明星出轨时,会有大量热情的粉丝发微博以表心意,同样会在短时间内产生大量的消息。

Expand Down Expand Up @@ -39,7 +39,7 @@ Twitter 的 snowflake 算法是这种场景下的一个典型解法。先来看

表示 timestamp 的 41 位,可以支持我们使用 69 年。当然,我们的时间毫秒计数不会真的从 1970 年开始记,那样我们的系统跑到 `2039/9/7 23:47:35` 就不能用了,所以这里的 timestamp 实际上只是相对于某个时间的增量,比如我们的系统上线是 2018-08-01,那么我们可以把这个 timestamp 当作是从 `2018-08-01 00:00:00.000` 的偏移量。

## worker id 分配
## 6.1.1 worker id 分配

timestamp,datacenter_id,worker_id 和 sequence_id 这四个字段中,timestamp 和 sequence_id 是由程序在运行期生成的。但 datacenter_id 和 worker_id 需要我们在部署阶段就能够获取得到,并且一旦程序启动之后,就是不可更改的了(想想,如果可以随意更改,可能被不慎修改,造成最终生成的 id 有冲突)。

Expand All @@ -64,9 +64,9 @@ mysql> select last_insert_id();
考虑到集群中即使有单个 id 生成服务的实例挂了,也就是损失一段时间的一部分 id,所以我们也可以更简单暴力一些,把 worker_id 直接写在 worker 的配置中,上线时,由部署脚本完成 worker_id 字段替换。
## 开源实例
## 6.1.2 开源实例
### 标准 snowflake 实现
### 6.1.2.1 标准 snowflake 实现
`github.com/bwmarrin/snowflake` 是一个相当轻量化的 snowflake 的 Go 实现。其文档指出:
Expand Down Expand Up @@ -122,7 +122,7 @@ func main() {
Epoch 就是本节开头讲的起始时间,NodeBits 指的是机器编号的位长,StepBits 指的是自增序列的位长。
### sonyflake
### 6.1.2.2 sonyflake
sonyflake 是 Sony 公司的一个开源项目,基本思路和 snowflake 差不多,不过位分配上稍有不同:
Expand Down
Loading

0 comments on commit 7a02e92

Please sign in to comment.