fasthttp:比net/http快十倍的Go框架(server 篇)
转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/574
我们在上一篇文章中讲解了 Go HTTP 标准库的实现原理,这一次我找到了一个号称比net/http快十倍的Go框架 fasthttp,这次我们再来看看它有哪些优秀的设计值得我们去挖掘。
一个典型的 HTTP 服务应该如图所示:
基于HTTP构建的服务标准模型包括两个端,客户端(Client
)和服务端(Server
)。HTTP 请求从客户端发出,服务端接受到请求后进行处理然后将响应返回给客户端。所以http服务器的工作就在于如何接受来自客户端的请求,并向客户端返回响应。
这篇我们来讲讲 Server 端的实现。
实现原理
net/http 与 fasthttp 实现对比
我们在讲 net/http
的时候讲过,它的处理流程大概是这样的:
- 注册处理器到一个 hash 表中,可以通过键值路由匹配;
- 注册完之后就是开启循环监听,每监听到一个连接就会创建一个 Goroutine;
- 在创建好的 Goroutine 里面会循环的等待接收请求数据,然后根据请求的地址去处理器路由表中匹配对应的处理器,然后将请求交给处理器处理;
这样做在连接数比较少的时候是没什么问题的,但是在连接数非常多的时候,每个连接都会创建一个 Goroutine 就会给系统带来一定的压力。这也就造成了 net/http
在处理高并发时的瓶颈。
下面我们再看看 fasthttp 是如何做的:
- 启动监听;
- 循环监听端口获取连接;
- 获取到连接之后首先会去 ready 队列里获取 workerChan,获取不到就会去对象池获取;
- 将监听的连接传入到 workerChan 的 channel 中;
- workerChan 有一个 Goroutine 一直循环获取 channel 中的数据,获取到之后就会对请求进行处理然后返回。
上面有提到 workerChan 其实就是一个连接处理对象,这个对象里面有一个 channel 用来传递连接;每个 workerChan 在后台都会有一个 Goroutine 循环获取 channel 中的连接,然后进行处理。如果没有设置最大同时连接处理数的话,默认是 256 * 1024
个。这样可以在并发很高的时候还可以同时保证对外提供服务。
除此之外,在实现上还通过 sync.Pool 来大量的复用对象,减少内存分配,如:
workerChanPool 、ctxPool 、readerPool、writerPool 等等多大30多个 sync.Pool 。
除了复用对象,fasthttp 还会切片,通过 s = s[:0]
和 s = append(s[:0], b…)
来减少切片的再次创建。
fasthttp 由于需要和 string 打交道的地方很多,所以还从很多地方尽量的避免[]byte
到string转换时带来的内存分配和拷贝带来的消耗 。
小结
综上我们大致介绍了一下 fasthttp 提升性能的点:
- 控制异步 Goroutine 的同时处理数量,最大默认是
256 * 1024
个; - 使用 sync.Pool 来大量的复用对象和切片,减少内存分配;
- 尽量的避免
[]byte
到string转换时带来的内存分配和拷贝带来的消耗 ;
源码解析
我们以一个简单的例子作为开始:
func main() {
if err := fasthttp.ListenAndServe(":8088", requestHandler); err != nil {
log.Fatalf("Error in ListenAndServe: %s", err)
}
}
func requestHandler(ctx *fasthttp.RequestCtx) {
fmt.Fprintf(ctx, "Hello, world!\n\n")
}
我们调用 ListenAndServe 函数会启动服务监听,等待任务进行处理。ListenAndServe 函数实际上会调用到 Server 的 ListenAndServe 方法,这里我们看一下 Server 结构体的字段:
上图简单的列举了一些 Server 结构体的常见字段,包括:请求处理器、服务名、请求读取超时时间、请求写入超时时间、每个连接最大请求数等。除此之外还有很多其他参数,可以在各个维度上控制服务端的一些参数。
Server 的 ListenAndServe 方法会获取 TCP 监听,然后调用 Serve 方法执行服务端的逻辑处理。
Server 方法主要做了以下几件事:
- 初始化并启动 worker Pool;
- 接收请求 Connection;
- 将 Connection 交给 worker Pool 处理;
func (s *Server) Serve(ln net.Listener) error {
...
s.mu.Unlock()
// 初始化 worker Pool
wp := &workerPool{
WorkerFunc: s.serveConn,
MaxWorkersCount: maxWorkersCount,
LogAllErrors: s.LogAllErrors,
Logger: s.logger(),
connState: s.setState,
}
// 启动 worker Pool
wp.Start()
// 循环处理 connection
for {
// 获取 connection
if c, err = acceptConn(s, ln, &lastPerIPErrorTime); err != nil {
wp.Stop()
if err == io.EOF {
return nil
}
return err
}
s.setState(c, StateNew)
atomic.AddInt32(&s.open, 1)
// 处理 connection
if !wp.Serve(c) {
// 进入if 说明已到并发极限
...
}
c = nil
}
}
worker Pool
worker Pool 是用来处理所有请求 Connection 的,这里稍微看看 workerPool 结构体的字段:
- WorkerFunc: 用来匹配请求对应的 handler 并执行;
- MaxWorkersCount:最大同时处理的请求数;
- ready:空闲的 workerChan;
- workerChanPool:workerChan 的对象池,是一个 sync.Pool 类型的;
- workersCount:目前正在处理的请求数;
下面我们看一下 workerPool 的 Start 方法:
func (wp *workerPool) Start() {
if wp.stopCh != nil {
panic("BUG: workerPool already started")
}
wp.stopCh = make(chan struct{})
stopCh := wp.stopCh
// 设置 worker Pool 的创建函数
wp.workerChanPool.New = func() interface{} {
return &workerChan{
ch: make(chan net.Conn, workerChanCap),
}
}
go func() {
var scratch []*workerChan
for {
// 没隔一段时间会清理空闲超时的 workerChan
wp.clean(&scratch)
select {
case <-stopCh:
return
default:
// 默认是 10 s
time.Sleep(wp.getMaxIdleWorkerDuration())
}
}
}()
}
Start 方法里面主要是:
- 设置 workerChanPool 的创建函数;
- 启动一个 Goroutine 定时清理 workerPool 中的 ready 中保存的空闲 workerChan,默认每 10s 启动一次。
获取连接
func acceptConn(s *Server, ln net.Listener, lastPerIPErrorTime *time.Time) (net.Conn, error) {
for {
c, err := ln.Accept()
if err != nil {
if c != nil {
panic("BUG: net.Listener returned non-nil conn and non-nil error")
}
...
return nil, io.EOF
}
if c == nil {
panic("BUG: net.Listener returned (nil, nil)")
}
// 校验每个ip对应的连接数
if s.MaxConnsPerIP > 0 {
pic := wrapPerIPConn(s, c)
if pic == nil {
if time.Since(*lastPerIPErrorTime) > time.Minute {
s.logger().Printf("The number of connections from %s exceeds MaxConnsPerIP=%d",
getConnIP4(c), s.MaxConnsPerIP)
*lastPerIPErrorTime = time.Now()
}
continue
}
c = pic
}
return c, nil
}
}
获取连接其实没什么好说的,和 net/http
库一样调用的 TCPListener 的 accept 方法获取 TCP Connection。
处理连接
处理连接部分首先会获取 workerChan ,workerChan 结构体里面包含了两个字段:lastUseTime、channel:
type workerChan struct {
lastUseTime time.Time
ch chan net.Conn
}
lastUseTime 标识最后一次被使用的时间;
ch 是用来传递 Connection 用的。
获取到 Connection 之后会传入到 workerChan 的 channel 中,每个对应的 workerChan 都有一个异步 Goroutine 在处理 channel 里面的 Connection。
获取 workerChan
func (wp *workerPool) Serve(c net.Conn) bool {
// 获取 workerChan
ch := wp.getCh()
if ch == nil {
return false
}
// 将 Connection 放入到 channel 中
ch.ch <- c
return true
}
Serve 方法主要是通过 getCh 方法获取 workerChan ,然后将当前的 Connection 传入到 workerChan 的 channel 中。
func (wp *workerPool) getCh() *workerChan {
var ch *workerChan
createWorker := false
wp.lock.Lock()
// 尝试从空闲队列里获取 workerChan
ready := wp.ready
n := len(ready) - 1
if n < 0 {
if wp.workersCount < wp.MaxWorkersCount {
createWorker = true
wp.workersCount++
}
} else {
ch = ready[n]
ready[n] = nil
wp.ready = ready[:n]
}
wp.lock.Unlock()
// 获取不到则从对象池中获取
if ch == nil {
if !createWorker {
return nil
}
vch := wp.workerChanPool.Get()
ch = vch.(*workerChan)
// 为新的 workerChan 开启 goroutine
go func() {
// 处理 channel 中的数据
wp.workerFunc(ch)
// 处理完之后重新放回到对象池中
wp.workerChanPool.Put(vch)
}()
}
return ch
}
getCh 方法首先会去 ready 空闲队列中获取 workerChan,如果获取不到则从对象池中获取,从对象池中获取的新的 workerChan 会启动 Goroutine 用来处理 channel 中的数据。
处理连接
func (wp *workerPool) workerFunc(ch *workerChan) {
var c net.Conn
var err error
// 消费 channel 中的数据
for c = range ch.ch {
if c == nil {
break
}
// 读取请求数据并响应返回
if err = wp.WorkerFunc(c); err != nil && err != errHijacked {
...
}
c = nil
// 将当前的 workerChan 放入的 ready 队列中
if !wp.release(ch) {
break
}
}
wp.lock.Lock()
wp.workersCount--
wp.lock.Unlock()
}
这里会遍历获取 workerChan 的 channel 中的 Connection 然后执行 WorkerFunc 函数处理请求,处理完毕之后会将当前的 workerChan 重新放入到 ready 队列中复用。
需要注意的是,这个循环会在获取 Connection 为 nil 的时候跳出循环,这个 nil 是 workerPool 在异步调用 clean 方法检查该 workerChan 空闲时间超长了就会往 channel 中传入一个 nil。
这里设置的 WorkerFunc 函数是 Server 的 serveConn 方法,里面会获取到请求的参数,然后根据请求调用到对应的 handler 进行请求处理,然后返回 response,由于 serveConn 方法比较长这里就不解析了,感兴趣的同学自己看看。
总结
我们这里分析了 fasthttp 的实现原理,通过原理我们可以知道 fasthttp 和 net/http
在实现上面有什么差异,从而大致得出 fasthttp 快的原因,然后再从它的实现细节知道它在实现上是如何做到减少内存分配从而提高性能的。
fasthttp:比net/http快十倍的Go框架(server 篇)的更多相关文章
- 一个比Spring Boot快44倍的Java框架!
最近栈长看到一个框架,官方号称可以比 Spring Boot 快 44 倍,居然这么牛逼,有这么神奇吗?今天带大家来认识一下. 这个框架名叫:light-4j. 官网简介:A fast, lightw ...
- TDengine能比Hadoop快10倍?
之前对国产的时序大数据存储引擎 TDengine 感兴趣,因为号称比Hadoop快十倍,一直很好奇怎么实现的,所以最近抽空看了下白皮书和设计文档. 如果用一句话总结,就是 TDengine 是为特定的 ...
- 十倍效能提升——Web 基础研发体系的建立
1 导读 web 基础研发体系指的是, web 研发中一线工程师所直接操作的技术.工具,以及所属组织架构的总和.在过去提升企业研发效能的讨论中,围绕的主题基本都是——”通过云计算.云存储等方式将底层核 ...
- [No0000D0] 让你效率“猛增十倍”,沉浸工作法到底是什么?
一位编剧在三天内完成两万字的剧本,而在此之前,他曾拖延了足足半年.一名大四学生用一天半写了8000多字,一鼓作气拿下毕业论文. 有人说:“用了这个方法,我的效率猛增十倍.只用短短两小时,就摧枯拉朽地完 ...
- 通过非聚集索引让select count(*) from 的查询速度提高几十倍、甚至千倍
通过非聚集索引,可以显著提升count(*)查询的性能. 有的人可能会说,这个count(*)能用上索引吗,这个count(*)应该是通过表扫描来一个一个的统计,索引有用吗? 不错,一般的查询,如果用 ...
- grep之字符串搜索算法Boyer-Moore由浅入深(比KMP快3-5倍)
这篇长文历时近两天终于完成了,前两天帮网站翻译一篇文章“为什么GNU grep如此之快?”,里面提及到grep速度快的一个重要原因是使用了Boyer-Moore算法作为字符串搜索算法,兴趣之下就想了解 ...
- Hadoop3.0新特性介绍,比Spark快10倍的Hadoop3.0新特性
Hadoop3.0新特性介绍,比Spark快10倍的Hadoop3.0新特性 Apache hadoop 项目组最新消息,hadoop3.x以后将会调整方案架构,将Mapreduce 基于内存+io+ ...
- delphi-json组件,速度非常快,要比superobject快好几倍
delphi-json组件,速度非常快,要比superobject快好几倍https://github.com/ahausladen/JsonDataObjectshttp://bbs.2ccc.co ...
- IPython,让Python显得友好十倍的外套——windows XP/Win7安装详解
前言 学习python,官方版本其实足够了.但是如果追求更好的开发体验,耐得住不厌其烦地折腾.那么我可以负责任的告诉你:IPython是我认为的唯一显著好于原版python的工具. 整理了 ...
随机推荐
- 某SPI设备驱动引起的开关机压力测试死机问题一例
环境 硬件平台:某ARM SoC 软件平台:Linux 问题现象:产品做开关机压力测试,发生死机. 分析 用crash工具解析两次死机dump信息,得到死机前的log如下.两次死机的backtrace ...
- 性能调优命令之jstack
jstack是java虚拟机自带的一种线程堆栈跟踪工具. /opt/java8/bin/jstack Usage: jstack [-l] <pid> (to connect to run ...
- pika详解(三)SelectConnection及其他Connection
pika详解(三)SelectConnection及其他Connection 本文链接:https://blog.csdn.net/comprel/article/details/94661147 ...
- 功率半导体碳化硅(SiC)技术
功率半导体碳化硅(SiC)技术 Silicon Carbide Adoption Enters Next Phase 碳化硅(SiC)技术的需求继续增长,这种技术可以最大限度地提高当今电力系统的效率, ...
- TensorRT 基于Yolov3的开发
TensorRT 基于Yolov3的开发 Models Desc tensorRT for Yolov3 https://github.com/lewes6369/TensorRT-Yolov3 Te ...
- PHPList 安装
邮件发送开源软件 PHPList 安装介绍 1:安装基本信息 官网地址 地址:https://www.phplist.com/ 其他:https://www.d-ip.jp/phplist/ 官网手册 ...
- NX二次开发-将3X3矩阵修正为正交且长度为单位长度的矩阵
函数:UF_MTX3_ortho_normalize() 函数说明:将矩阵修正为正交且xyz长度为单位长度的矩阵.下图中输入的矩阵为三条线段的端点,经过修正后,生成一个坐标系. 1 #include ...
- SpringCloud-OAuth2(二):实战篇
如果不了解Oauth2 是什么.工作流程的可以看我上一篇文章: SpringCloud-OAuth2(一):基础篇 这篇讲的内容是:Oauth2在SpringBoot/SpringCloud中的实战. ...
- 从 Nginx 优秀的核心架构设计,揭秘其为何能支持高并发?
目录: 1. Nginx的整体架构 2. Nginx的模块化设计 3. Nginx的请求方式处理 4. Nginx事件驱动模型 5. Nginx进程处理模型 写在前面 Nginx 是一个 免费的,开源 ...
- NOIP模拟测试10「大佬·辣鸡·模板」
大佬 显然假期望 我奇思妙想出了一个式子$f[i]=f[i-1]+\sum\limits_{j=1}^{j<=m} C_{k \times j}^{k}\times w[j]$ 然后一想不对得容 ...