1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-24 04:22:22 +00:00
This commit is contained in:
Xargin 2018-12-21 15:24:49 +08:00
parent 2a57def713
commit c718dec526

View File

@ -1,12 +1,12 @@
# 5.6 Ratelimit 服务流量限制 # 5.6 Ratelimit 服务流量限制
计算机程序可依据其瓶颈分为 Disk IO-boundCPU-boundNetwork-bound,分布式场景下有时候也会外部系统而导致自身瓶颈。 计算机程序可依据其瓶颈分为磁盘IO瓶颈型CPU计算瓶颈型网络带宽瓶颈型,分布式场景下有时候也会外部系统而导致自身瓶颈。
Web 系统打交道最多的是网络,无论是接收,解析用户请求,访问存储,还是把响应数据返回给用户,都是要走网络的。在没有 epoll/kqueue 之类的系统提供的 IO 多路复用接口之前,多个核心的现代计算机最头痛的是 C10k 问题C10k 问题会导致计算机没有办法充分利用 CPU 来处理更多的用户连接,进而没有办法通过优化程序提升 CPU 利用率来处理更多的请求。 Web系统打交道最多的是网络无论是接收解析用户请求访问存储还是把响应数据返回给用户都是要走网络的。在没有`epoll/kqueue`之类的系统提供的IO多路复用接口之前多个核心的现代计算机最头痛的是C10k问题C10k问题会导致计算机没有办法充分利用CPU来处理更多的用户连接进而没有办法通过优化程序提升CPU利用率来处理更多的请求。
自从 Linux 实现了 epollFreeBSD 实现了 kqueue,这个问题基本解决了,我们可以借助内核提供的 API 轻松解决当年的 C10k 问题,也就是说如今如果你的程序主要是和网络打交道,那么瓶颈一定在用户程序而不在操作系统内核。 自从Linux实现了`epoll`FreeBSD实现了`kqueue`这个问题基本解决了我们可以借助内核提供的API轻松解决当年的C10k问题也就是说如今如果你的程序主要是和网络打交道那么瓶颈一定在用户程序而不在操作系统内核。
随着时代的发展,编程语言对这些系统调用又进一步进行了封装,如今做应用层开发,几乎不会在程序中看到 epoll 之类的字眼大多数时候我们就只要聚焦在业务逻辑上就好。Go 的 net 库针对不同平台封装了不同的 syscall APIhttp 库又是构建在 net 库之上,所以在 Go 我们可以借助标准库,很轻松地写出高性能的 http 服务,下面是一个简单的 `hello world` 服务的代码: 随着时代的发展,编程语言对这些系统调用又进一步进行了封装,如今做应用层开发,几乎不会在程序中看到`epoll`之类的字眼大多数时候我们就只要聚焦在业务逻辑上就好。Go 的 net 库针对不同平台封装了不同的syscall API`http`库又是构建在`net`库之上所以在Go语言中我们可以借助标准库很轻松地写出高性能的`http`服务,下面是一个简单的`hello world`服务的代码:
```go ```go
package main package main
@ -87,15 +87,15 @@ Requests/sec: 45118.57
Transfer/sec: 5.51MB Transfer/sec: 5.51MB
``` ```
多次测试的结果在 4w 左右的 QPS 浮动,响应时间最多也就是 40ms 左右对于一个Web程序来说这已经是很不错的成绩了我们只是照抄了别人的示例代码就完成了一个高性能的 `hello world` 服务器,是不是很有成就感? 多次测试的结果在4万左右的QPS浮动响应时间最多也就是40ms左右对于一个Web程序来说这已经是很不错的成绩了我们只是照抄了别人的示例代码就完成了一个高性能的`hello world`服务器,是不是很有成就感?
这还只是家用 PC线上服务器大多都是 24 核心起32G 内存+CPU 基本都是 Intel i7。所以同样的程序在服务器上运行会得到更好的结果。 这还只是家用PC线上服务器大多都是24核心起32G内存+CPU基本都是Intel i7。所以同样的程序在服务器上运行会得到更好的结果。
这里的 `hello world` 服务没有任何业务逻辑。真实环境的程序要复杂得多,有些程序偏 Network-bound例如一些 cdn 服务、proxy 服务;有些程序偏 CPU/GPU bound例如登陆校验服务、图像处理服务有些程序偏 Disk IO-bound,例如专门的存储系统,数据库。不同的程序瓶颈会体现在不同的地方,这里提到的这些功能单一的服务相对来说还算容易分析。如果碰到业务逻辑复杂代码量巨大的模块,其瓶颈并不是三下五除二可以推测出来的,还是需要从压力测试中得到更为精确的结论。 这里的`hello world`服务没有任何业务逻辑。真实环境的程序要复杂得多有些程序偏Network-bound例如一些cdn服务、proxy服务有些程序偏CPU/GPU bound例如登陆校验服务、图像处理服务有些程序瓶颈偏磁盘,例如专门的存储系统,数据库。不同的程序瓶颈会体现在不同的地方,这里提到的这些功能单一的服务相对来说还算容易分析。如果碰到业务逻辑复杂代码量巨大的模块,其瓶颈并不是三下五除二可以推测出来的,还是需要从压力测试中得到更为精确的结论。
对于 IO/Network bound 类的程序,其表现是网卡/磁盘 IO 会先于 CPU 打满,这种情况即使优化 CPU 的使用也不能提高整个系统的吞吐量,只能提高磁盘的读写速度,增加内存大小,提升网卡的带宽来提升整体性能。而 CPU bound 类的程序,则是在存储和网卡未打满之前 CPU 占用率提前到达 100%CPU 忙于各种计算任务IO 设备相对则较闲。 对于IO/Network瓶颈类的程序其表现是网卡/磁盘IO会先于CPU打满这种情况即使优化CPU的使用也不能提高整个系统的吞吐量只能提高磁盘的读写速度增加内存大小提升网卡的带宽来提升整体性能。而CPU bound类的程序则是在存储和网卡未打满之前CPU占用率提前到达100%CPU忙于各种计算任务IO设备相对则较闲。
无论哪种类型的服务,在资源使用到极限的时候都会导致请求堆积,超时,系统 hang 死,最终伤害到终端用户。对于分布式的 Web 服务来说,瓶颈还不一定总在系统内部,也有可能在外部。非计算密集型的系统往往会在关系型数据库环节失守,而这时候 Web 模块本身还远远未达到瓶颈。 无论哪种类型的服务在资源使用到极限的时候都会导致请求堆积超时系统hang死最终伤害到终端用户。对于分布式的Web服务来说瓶颈还不一定总在系统内部也有可能在外部。非计算密集型的系统往往会在关系型数据库环节失守而这时候Web模块本身还远远未达到瓶颈。
不管我们的服务瓶颈在哪里,最终要做的事情都是一样的,那就是流量限制。 不管我们的服务瓶颈在哪里,最终要做的事情都是一样的,那就是流量限制。
@ -106,31 +106,33 @@ Transfer/sec: 5.51MB
1. 漏桶是指我们有一个一直装满了水的桶,每过固定的一段时间即向外漏一滴水。如果你接到了这滴水,那么你就可以继续服务请求,如果没有接到,那么就需要等待下一滴水。 1. 漏桶是指我们有一个一直装满了水的桶,每过固定的一段时间即向外漏一滴水。如果你接到了这滴水,那么你就可以继续服务请求,如果没有接到,那么就需要等待下一滴水。
2. 令牌桶则是指匀速向桶中添加令牌,服务请求时需要从桶中获取令牌,令牌的数目可以按照需要消耗的资源进行相应的调整。如果没有令牌,可以选择等待,或者放弃。 2. 令牌桶则是指匀速向桶中添加令牌,服务请求时需要从桶中获取令牌,令牌的数目可以按照需要消耗的资源进行相应的调整。如果没有令牌,可以选择等待,或者放弃。
这两种方法看起来很像,不过还是有区别的。漏桶流出的速率固定,而令牌桶只要在桶中有令牌,那就可以拿。也就是说令牌桶是允许一定程度的并发的,比如同一个时刻,有 100 个用户请求,只要令牌桶中有 100 个令牌,那么这 100 个请求全都会放过去。令牌桶在桶中没有令牌的情况下也会退化为漏桶模型。 这两种方法看起来很像不过还是有区别的。漏桶流出的速率固定而令牌桶只要在桶中有令牌那就可以拿。也就是说令牌桶是允许一定程度的并发的比如同一个时刻有100个用户请求只要令牌桶中有100个令牌那么这100个请求全都会放过去。令牌桶在桶中没有令牌的情况下也会退化为漏桶模型。
![token bucket](../images/ch5-token-bucket.png) ![token bucket](../images/ch5-token-bucket.png)
实际应用中令牌桶应用较为广泛,开源界流行的限流器大多数都是基于令牌桶思想的。并且在此基础上进行了一定程度的扩充,比如 `github.com/juju/ratelimit` 提供了几种不同特色的令牌桶填充方式: *图 5-12 令牌桶*
实际应用中令牌桶应用较为广泛,开源界流行的限流器大多数都是基于令牌桶思想的。并且在此基础上进行了一定程度的扩充,比如`github.com/juju/ratelimit`提供了几种不同特色的令牌桶填充方式:
```go ```go
func NewBucket(fillInterval time.Duration, capacity int64) *Bucket func NewBucket(fillInterval time.Duration, capacity int64) *Bucket
``` ```
默认的令牌桶fillInterval 指每过多长时间向桶里放一个令牌capacity 是桶的容量,超过桶容量的部分会被直接丢弃。桶初始是满的。 默认的令牌桶fillInterval指每过多长时间向桶里放一个令牌capacity是桶的容量超过桶容量的部分会被直接丢弃。桶初始是满的。
```go ```go
func NewBucketWithQuantum(fillInterval time.Duration, capacity, quantum int64) *Bucket func NewBucketWithQuantum(fillInterval time.Duration, capacity, quantum int64) *Bucket
``` ```
和普通的 NewBucket 的区别是,每次向桶中放令牌时,是放 quantum 个令牌,而不是一个令牌。 和普通的`NewBucket()`的区别是,每次向桶中放令牌时,是放`quantum`个令牌,而不是一个令牌。
```go ```go
func NewBucketWithRate(rate float64, capacity int64) *Bucket func NewBucketWithRate(rate float64, capacity int64) *Bucket
``` ```
这个就有点特殊了,会按照提供的比例,每秒钟填充令牌数。例如 capacity 是 100而 rate 是 0.1,那么每秒会填充 10 个令牌。 这个就有点特殊了,会按照提供的比例,每秒钟填充令牌数。例如`capacity`是100`rate`是0.1那么每秒会填充10个令牌。
从桶中获取令牌也提供了几个 API 从桶中获取令牌也提供了几个API
```go ```go
func (tb *Bucket) Take(count int64) time.Duration {} func (tb *Bucket) Take(count int64) time.Duration {}
@ -142,11 +144,11 @@ func (tb *Bucket) Wait(count int64) {}
func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool {} func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool {}
``` ```
名称和功能都比较直观,这里就不再赘述了。相比于开源界更为有名的 google Java 工具库 Guava 中提供的 ratelimiter这个库不支持令牌桶预热且无法修改初始的令牌容量所以可能个别极端情况下的需求无法满足。但在明白令牌桶的基本原理之后如果没办法满足需求相信你也可以很快对其进行修改并支持自己的业务场景。 名称和功能都比较直观这里就不再赘述了。相比于开源界更为有名的google的Java工具库Guava中提供的ratelimiter这个库不支持令牌桶预热且无法修改初始的令牌容量所以可能个别极端情况下的需求无法满足。但在明白令牌桶的基本原理之后如果没办法满足需求相信你也可以很快对其进行修改并支持自己的业务场景。
## 5.6.2 原理 ## 5.6.2 原理
从功能上来看,令牌桶模型实际上就是对全局计数的加减法操作过程,但使用计数需要我们自己加读写锁,有小小的思想负担。如果我们对 Go 语言已经比较熟悉的话,很容易想到可以用 buffered channel 来完成简单的加令牌取令牌操作: 从功能上来看令牌桶模型实际上就是对全局计数的加减法操作过程但使用计数需要我们自己加读写锁有小小的思想负担。如果我们对Go语言已经比较熟悉的话很容易想到可以用buffered channel来完成简单的加令牌取令牌操作
```go ```go
var tokenBucket = make(chan struct{}, capacity) var tokenBucket = make(chan struct{}, capacity)
@ -219,7 +221,7 @@ current token cnt: 100 2018-06-16 18:17:50.304550145 +0800 CST m=+1.051517182
current token cnt: 100 2018-06-16 18:17:50.313970334 +0800 CST m=+1.060937371 current token cnt: 100 2018-06-16 18:17:50.313970334 +0800 CST m=+1.060937371
``` ```
1s 钟的时候刚好填满 100 没有太大的偏差。不过这里可以看到Go 的定时器存在大约 0.001s 的误差,所以如果令牌桶大小在 1000 以上的填充可能会有一定的误差。对于一般的服务来说,这一点误差无关紧要。 在1s钟的时候刚好填满100个没有太大的偏差。不过这里可以看到Go的定时器存在大约0.001s的误差所以如果令牌桶大小在1000以上的填充可能会有一定的误差。对于一般的服务来说这一点误差无关紧要。
上面的令牌桶的取令牌操作实现起来也比较简单,简化问题,我们这里只取一个令牌: 上面的令牌桶的取令牌操作实现起来也比较简单,简化问题,我们这里只取一个令牌:
@ -253,14 +255,14 @@ cur = k1 + ((t2 - t1)/ti) * x
cur = cur > cap ? cap : cur cur = cur > cap ? cap : cur
``` ```
我们用两个时间点的时间差,再结合其它的参数,理论上在取令牌之前就完全可以知道桶里有多少令牌了。那劳心费力地像本小节前面向 channel 里填充 token 的操作,理论上是没有必要的。只要在每次 `Take` 的时候,再对令牌桶中的 token 数进行简单计算,就可以得到正确的令牌数。是不是很像 `惰性求值` 的感觉? 我们用两个时间点的时间差再结合其它的参数理论上在取令牌之前就完全可以知道桶里有多少令牌了。那劳心费力地像本小节前面向channel里填充token的操作理论上是没有必要的。只要在每次`Take`的时候再对令牌桶中的token数进行简单计算就可以得到正确的令牌数。是不是很像`惰性求值`的感觉?
在得到正确的令牌数之后,再进行实际的 Take 操作就好,这个 Take 操作只需要对令牌数进行简单的减法即可,记得加锁以保证并发安全。`github.com/juju/ratelimit` 这个库就是这样做的。 在得到正确的令牌数之后再进行实际的Take操作就好这个Take操作只需要对令牌数进行简单的减法即可记得加锁以保证并发安全。`github.com/juju/ratelimit`这个库就是这样做的。
## 5.6.3 服务瓶颈和 QoS ## 5.6.3 服务瓶颈和 QoS
前面我们说了很多 CPU-bound、IO-bound 之类的概念,这种性能瓶颈从大多数公司都有的监控系统中可以比较快速地定位出来,如果一个系统遇到了性能问题,那监控图的反应一般都是最快的。 前面我们说了很多CPU瓶颈、IO瓶颈之类的概念,这种性能瓶颈从大多数公司都有的监控系统中可以比较快速地定位出来,如果一个系统遇到了性能问题,那监控图的反应一般都是最快的。
虽然性能指标很重要,但对用户提供服务时还应考虑服务整体的 QoS。QoS 全称是 Quality of Service顾名思义是服务质量。QoS 包含有可用性、吞吐量、时延、时延变化和丢失等指标。一般来讲我们可以通过优化系统,来提高 Web 服务的 CPU 利用率,从而提高整个系统的吞吐量。但吞吐量提高的同时,用户体验是有可能变差的。用户角度比较敏感的除了可用性之外,还有时延。虽然你的系统吞吐量高,但半天刷不开页面,想必会造成大量的用户流失。所以在大公司的 Web 服务性能指标中,除了平均响应时延之外,还会把响应时间的 95 分位99 分位也拿出来作为性能标准。平均响应在提高 CPU 利用率没受到太大影响时,可能 95 分位、 99 分位的响应时间大幅度攀升了,那么这时候就要考虑提高这些 CPU 利用率所付出的代价是否值得了。 虽然性能指标很重要但对用户提供服务时还应考虑服务整体的QoS。QoS全称是Quality of Service顾名思义是服务质量。QoS包含有可用性、吞吐量、时延、时延变化和丢失等指标。一般来讲我们可以通过优化系统来提高Web服务的CPU利用率从而提高整个系统的吞吐量。但吞吐量提高的同时用户体验是有可能变差的。用户角度比较敏感的除了可用性之外还有时延。虽然你的系统吞吐量高但半天刷不开页面想必会造成大量的用户流失。所以在大公司的Web服务性能指标中除了平均响应时延之外还会把响应时间的95分位99分位也拿出来作为性能标准。平均响应在提高CPU利用率没受到太大影响时可能95分位、99分位的响应时间大幅度攀升了那么这时候就要考虑提高这些CPU利用率所付出的代价是否值得了。
在线系统的机器一般都会保持 CPU 有一定的余裕。 在线系统的机器一般都会保持CPU有一定的余裕。