Go标准库http与fasthttp服务端性能对比场景分析

目录
  • 1. 背景
  • 2. 性能测试
  • 3. 对结果的简要分析
  • 4. 优化途径

1. 背景

Go初学者学习Go时,在编写了经典的“hello, world”程序之后,可能会迫不及待的体验一下Go强大的标准库,比如:用几行代码写一个像下面示例这样拥有完整功能的web server:

// 来自https://tip.golang.org/pkg/net/http/#example_ListenAndServe
package main
import (
    "io"
    "log"
    "net/http"
)
func main() {
    helloHandler := func(w http.ResponseWriter, req *http.Request) {
        io.WriteString(w, "Hello, world!\n")
    }
    http.HandleFunc("/hello", helloHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

go net/http包是一个比较均衡的通用实现,能满足大多数gopher 90%以上场景的需要,并且具有如下优点:

  • 标准库包,无需引入任何第三方依赖;
  • 对http规范的满足度较好;
  • 无需做任何优化,即可获得相对较高的性能;
  • 支持HTTP代理;
  • 支持HTTPS;
  • 无缝支持HTTP/2。

不过也正是因为http包的“均衡”通用实现,在一些对性能要求严格的领域,net/http的性能可能无法胜任,也没有太多的调优空间。这时我们会将眼光转移到其他第三方的http服务端框架实现上。

而在第三方http服务端框架中,一个“行如其名”的框架fasthttp被提及和采纳的较多,fasthttp官网宣称其性能是net/http的十倍(基于go test benchmark的测试结果)。

fasthttp采用了许多性能优化上的最佳实践,尤其是在内存对象的重用上,大量使用sync.Pool以降低对Go GC的压力。

那么在真实环境中,到底fasthttp能比net/http快多少呢?恰好手里有两台性能还不错的服务器可用,在本文中我们就在这个真实环境下看看他们的实际性能。

2. 性能测试

我们分别用net/http和fasthttp实现两个几乎“零业务”的被测程序:

  • nethttp:
// github.com/bigwhite/experiments/blob/master/http-benchmark/nethttp/main.go
package main
import (
    _ "expvar"
    "log"
    "net/http"
    _ "net/http/pprof"
    "runtime"
    "time"
)
func main() {
    go func() {
        for {
            log.Println("当前routine数量:", runtime.NumGoroutine())
            time.Sleep(time.Second)
        }
    }()

    http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, Go!"))
    }))

    log.Fatal(http.ListenAndServe(":8080", nil))
}
  • fasthttp:
// github.com/bigwhite/experiments/blob/master/http-benchmark/fasthttp/main.go
package main
import (
    "fmt"
    "log"
    "net/http"
    "runtime"
    "time"
    _ "expvar"
    _ "net/http/pprof"
    "github.com/valyala/fasthttp"
)
type HelloGoHandler struct {
}
func fastHTTPHandler(ctx *fasthttp.RequestCtx) {
    fmt.Fprintln(ctx, "Hello, Go!")
}
func main() {
    go func() {
        http.ListenAndServe(":6060", nil)
    }()
    go func() {
        for {
            log.Println("当前routine数量:", runtime.NumGoroutine())
            time.Sleep(time.Second)
        }
    }()
    s := &fasthttp.Server{
        Handler: fastHTTPHandler,
    }
    s.ListenAndServe(":8081")
}

对被测目标实施压力测试的客户端,我们基于hey这个http压测工具进行,为了方便调整压力水平,我们将hey“包裹”在下面这个shell脚本中(仅适于在linux上运行):

// github.com/bigwhite/experiments/blob/master/http-benchmark/client/http_client_load.sh
# ./http_client_load.sh 3 10000 10 GET http://10.10.195.181:8080
echo "$0 task_num count_per_hey conn_per_hey method url"
task_num=$1
count_per_hey=$2
conn_per_hey=$3
method=$4
url=$5
start=$(date +%s%N)
for((i=1; i<=$task_num; i++)); do {
    tm=$(date +%T.%N)
        echo "$tm: task $i start"
    hey -n $count_per_hey -c $conn_per_hey -m $method $url > hey_$i.log
    tm=$(date +%T.%N)
        echo "$tm: task $i done"
} & done
wait
end=$(date +%s%N)
count=$(( $task_num * $count_per_hey ))
runtime_ns=$(( $end - $start ))
runtime=`echo "scale=2; $runtime_ns / 1000000000" | bc`
echo "runtime: "$runtime
speed=`echo "scale=2; $count / $runtime" | bc`
echo "speed: "$speed

该脚本的执行示例如下:

bash http_client_load.sh 8 1000000 200 GET http://10.10.195.134:8080
http_client_load.sh task_num count_per_hey conn_per_hey method url
16:58:09.146948690: task 1 start
16:58:09.147235080: task 2 start
16:58:09.147290430: task 3 start
16:58:09.147740230: task 4 start
16:58:09.147896010: task 5 start
16:58:09.148314900: task 6 start
16:58:09.148446030: task 7 start
16:58:09.148930840: task 8 start
16:58:45.001080740: task 3 done
16:58:45.241903500: task 8 done
16:58:45.261501940: task 1 done
16:58:50.032383770: task 4 done
16:58:50.985076450: task 7 done
16:58:51.269099430: task 5 done
16:58:52.008164010: task 6 done
16:58:52.166402430: task 2 done
runtime: 43.02
speed: 185960.01

从传入的参数来看,该脚本并行启动了8个task(一个task启动一个hey),每个task向http://10.10.195.134:8080建立200个并发连接,并发送100w http GET请求。

我们使用两台服务器分别放置被测目标程序和压力工具脚本:

  • 目标程序所在服务器:10.10.195.181(物理机,Intel x86-64 CPU,40核,128G内存, CentOs 7.6)
$ cat /etc/redhat-release
CentOS Linux release 7.6.1810 (Core) 

$ lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                40
On-line CPU(s) list:   0-39
Thread(s) per core:    2
Core(s) per socket:    10
座:                 2
NUMA 节点:         2
厂商 ID:           GenuineIntel
CPU 系列:          6
型号:              85
型号名称:        Intel(R) Xeon(R) Silver 4114 CPU @ 2.20GHz
步进:              4
CPU MHz:             800.000
CPU max MHz:           2201.0000
CPU min MHz:           800.0000
BogoMIPS:            4400.00
虚拟化:           VT-x
L1d 缓存:          32K
L1i 缓存:          32K
L2 缓存:           1024K
L3 缓存:           14080K
NUMA 节点0 CPU:    0-9,20-29
NUMA 节点1 CPU:    10-19,30-39
Flags:                 fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch epb cat_l3 cdp_l3 intel_pt ssbd mba ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts pku ospke spec_ctrl intel_stibp flush_l1d
  • 压力工具所在服务器:10.10.195.133(物理机,鲲鹏arm64 cpu,96核,80G内存, CentOs 7.9)
# cat /etc/redhat-release
CentOS Linux release 7.9.2009 (AltArch)

# lscpu
Architecture:          aarch64
Byte Order:            Little Endian
CPU(s):                96
On-line CPU(s) list:   0-95
Thread(s) per core:    1
Core(s) per socket:    48
座:                 2
NUMA 节点:         4
型号:              0
CPU max MHz:           2600.0000
CPU min MHz:           200.0000
BogoMIPS:            200.00
L1d 缓存:          64K
L1i 缓存:          64K
L2 缓存:           512K
L3 缓存:           49152K
NUMA 节点0 CPU:    0-23
NUMA 节点1 CPU:    24-47
NUMA 节点2 CPU:    48-71
NUMA 节点3 CPU:    72-95
Flags:                 fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma dcpop asimddp asimdfhm

我用dstat监控被测目标所在主机资源占用情况(dstat -tcdngym),尤其是cpu负荷;通过[expvarmon监控memstats],由于没有业务,内存占用很少;通过go tool pprof查看目标程序中对各类资源消耗情况的排名。

下面是多次测试后制作的一个数据表格:

图:测试数据

3. 对结果的简要分析

受特定场景、测试工具及脚本精确性以及压力测试环境的影响,上面的测试结果有一定局限,但却真实反映了被测目标的性能趋势。我们看到在给予同样压力的情况下,fasthttp并没有10倍于net http的性能,甚至在这样一个特定的场景下,两倍于net/http的性能都没有达到:我们看到在目标主机cpu资源消耗接近70%的几个用例中,fasthttp的性能仅比net/http高出30%~70%左右。

那么为什么fasthttp的性能未及预期呢?要回答这个问题,那就要看看net/http和fasthttp各自的实现原理了!我们先来看看net/http的工作原理示意图:

图:nethttp工作原理示意图

http包作为server端的原理很简单,那就是accept到一个连接(conn)之后,将这个conn甩给一个worker goroutine去处理,后者一直存在,直到该conn的生命周期结束:即连接关闭。

下面是fasthttp的工作原理示意图:

图:fasthttp工作原理示意图

而fasthttp设计了一套机制,目的是尽量复用goroutine,而不是每次都创建新的goroutine。fasthttp的Server accept一个conn之后,会尝试从workerpool中的ready切片中取出一个channel,该channel与某个worker goroutine一一对应。一旦取出channel,就会将accept到的conn写到该channel里,而channel另一端的worker goroutine就会处理该conn上的数据读写。当处理完该conn后,该worker goroutine不会退出,而是会将自己对应的那个channel重新放回workerpool中的ready切片中,等待这下一次被取出

fasthttp的goroutine复用策略初衷很好,但在这里的测试场景下效果不明显,从测试结果便可看得出来,在相同的客户端并发和压力下,net/http使用的goroutine数量与fasthttp相差无几。这是由测试模型导致的:在我们这个测试中,每个task中的hey都会向被测目标发起固定数量的[长连接(keep-alive)],然后在每条连接上发起“饱和”请求。这样fasthttp workerpool中的goroutine一旦接收到某个conn就只能在该conn上的通讯结束后才能重新放回,而该conn直到测试结束才会close,因此这样的场景相当于让fasthttp“退化”成了net/http的模型,也染上了net/http的“缺陷”:goroutine的数量一旦多起来,go runtime自身调度所带来的消耗便不可忽视甚至超过了业务处理所消耗的资源占比。下面分别是fasthttp在200长连接、8000长连接以及16000长连接下的cpu profile的结果:

200长连接:

(pprof) top -cum
Showing nodes accounting for 88.17s, 55.35% of 159.30s total
Dropped 150 nodes (cum <= 0.80s)
Showing top 10 nodes out of 60
      flat  flat%   sum%        cum   cum%
     0.46s  0.29%  0.29%    101.46s 63.69%  github.com/valyala/fasthttp.(*Server).serveConn
         0     0%  0.29%    101.46s 63.69%  github.com/valyala/fasthttp.(*workerPool).getCh.func1
         0     0%  0.29%    101.46s 63.69%  github.com/valyala/fasthttp.(*workerPool).workerFunc
     0.04s 0.025%  0.31%     89.46s 56.16%  internal/poll.ignoringEINTRIO (inline)
    87.38s 54.85% 55.17%     89.27s 56.04%  syscall.Syscall
     0.12s 0.075% 55.24%     60.39s 37.91%  bufio.(*Writer).Flush
         0     0% 55.24%     60.22s 37.80%  net.(*conn).Write
     0.08s  0.05% 55.29%     60.21s 37.80%  net.(*netFD).Write
     0.09s 0.056% 55.35%     60.12s 37.74%  internal/poll.(*FD).Write
         0     0% 55.35%     59.86s 37.58%  syscall.Write (inline)
(pprof) 

8000长连接:

(pprof) top -cum
Showing nodes accounting for 108.51s, 54.46% of 199.23s total
Dropped 204 nodes (cum <= 1s)
Showing top 10 nodes out of 66
      flat  flat%   sum%        cum   cum%
         0     0%     0%    119.11s 59.79%  github.com/valyala/fasthttp.(*workerPool).getCh.func1
         0     0%     0%    119.11s 59.79%  github.com/valyala/fasthttp.(*workerPool).workerFunc
     0.69s  0.35%  0.35%    119.05s 59.76%  github.com/valyala/fasthttp.(*Server).serveConn
     0.04s  0.02%  0.37%    104.22s 52.31%  internal/poll.ignoringEINTRIO (inline)
   101.58s 50.99% 51.35%    103.95s 52.18%  syscall.Syscall
     0.10s  0.05% 51.40%     79.95s 40.13%  runtime.mcall
     0.06s  0.03% 51.43%     79.85s 40.08%  runtime.park_m
     0.23s  0.12% 51.55%     79.30s 39.80%  runtime.schedule
     5.67s  2.85% 54.39%     77.47s 38.88%  runtime.findrunnable
     0.14s  0.07% 54.46%     68.96s 34.61%  bufio.(*Writer).Flush

16000长连接:

(pprof) top -cum
Showing nodes accounting for 239.60s, 87.07% of 275.17s total
Dropped 190 nodes (cum <= 1.38s)
Showing top 10 nodes out of 46
      flat  flat%   sum%        cum   cum%
     0.04s 0.015% 0.015%    153.38s 55.74%  runtime.mcall
     0.01s 0.0036% 0.018%    153.34s 55.73%  runtime.park_m
     0.12s 0.044% 0.062%       153s 55.60%  runtime.schedule
     0.66s  0.24%   0.3%    152.66s 55.48%  runtime.findrunnable
     0.15s 0.055%  0.36%    127.53s 46.35%  runtime.netpoll
   127.04s 46.17% 46.52%    127.04s 46.17%  runtime.epollwait
         0     0% 46.52%       121s 43.97%  github.com/valyala/fasthttp.(*workerPool).getCh.func1
         0     0% 46.52%       121s 43.97%  github.com/valyala/fasthttp.(*workerPool).workerFunc
     0.41s  0.15% 46.67%    120.18s 43.67%  github.com/valyala/fasthttp.(*Server).serveConn
   111.17s 40.40% 87.07%    111.99s 40.70%  syscall.Syscall
(pprof)

通过上述profile的比对,我们发现当长连接数量增多时(即workerpool中goroutine数量增多时),go runtime调度的占比会逐渐提升,在16000连接时,runtime调度的各个函数已经排名前4了。

4. 优化途径

从上面的测试结果,我们看到fasthttp的模型不太适合这种连接连上后进行持续“饱和”请求的场景,更适合短连接或长连接但没有持续饱和请求,在后面这样的场景下,它的goroutine复用模型才能更好的得以发挥。

但即便“退化”为了net/http模型,fasthttp的性能依然要比net/http略好,这是为什么呢?这些性能提升主要是fasthttp在内存分配层面的优化trick的结果,比如大量使用sync.Pool,比如避免在[]byte和string互转等。

那么,在持续“饱和”请求的场景下,如何让fasthttp workerpool中goroutine的数量不会因conn的增多而线性增长呢?fasthttp官方没有给出答案,但一条可以考虑的路径是使用os的多路复用(linux上的实现为epoll),即go runtime netpoll使用的那套机制。在多路复用的机制下,这样可以让每个workerpool中的goroutine处理同时处理多个连接,这样我们可以根据业务规模选择workerpool池的大小,而不是像目前这样几乎是任意增长goroutine的数量。当然,在用户层面引入epoll也可能会带来系统调用占比的增多以及响应延迟增大等问题。至于该路径是否可行,还是要看具体实现和测试结果。

注:fasthttp.Server中的Concurrency可以用来限制workerpool中并发处理的goroutine的个数,但由于每个goroutine只处理一个连接,当Concurrency设置过小时,后续的连接可能就会被fasthttp拒绝服务。因此fasthttp的默认Concurrency为:

const DefaultConcurrency = 256 * 1024

到此这篇关于Go标准库http与fasthttp服务端性能比较的文章就介绍到这了,更多相关go http与fasthttp服务端性能内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • golang高性能的http请求 fasthttp详解

    fasthttp是golang下的一个http框架,顾名思义,与原生的http实现相比,它的特点在于快,按照官网的说法,它的客户端和服务端性能比原生有了十倍的提升. 它的高性能主要源自于"复用",通过服务协程和内存变量的复用,节省了大量资源分配的成本. fasthttp 据说是目前golang性能最好的http库,相对于自带的net/http,性能说是有10倍的提升,具体介绍可以看看官方介绍:valyala/fasthttp 1,首先安装fasthttp go get -u githu

  • 浅谈golang fasthttp踩坑经验

    一个简单的系统,结构如下: 我们的服务A接受外部的http请求,然后通过golang的fasthttp将请求转发给服务B,流程非常简单.线上运行一段时间之后,发现服务B完全不再接收任何请求,查看服务A的日志,发现大量的如下错误 从错误原因看是因为连接被占满导致的.进入服务A的容器中(服务A和服务B都是通过docker启动的),通过netstat -anlp查看,发现有大量的tpc连接,处于ESTABLISH.我们采用的是长连接的方式,此时心里非常疑惑:1. fasthttp是能够复用连接的,为什

  • Golang请求fasthttp实践

    目录 基础API演示 高性能API演示 测试服务 Golang单元测试 测试报告 原计划学完Golang语言HTTP客户端实践之后,就可以继续了,没想到才疏学浅,在搜资料的时候发现除了Golang SDK自带的net/http,还有一个更牛的HttpClient实现github.com/valyala/fasthttp,据说性能是net/http的10倍,我想可能是有点夸张了,后期我会进行测试,以正视听. 在github.com/valyala/fasthttp用到了对象池,为了在高性能测试中减

  • Go标准库http与fasthttp服务端性能对比场景分析

    目录 1. 背景 2. 性能测试 3. 对结果的简要分析 4. 优化途径 1. 背景 Go初学者学习Go时,在编写了经典的“hello, world”程序之后,可能会迫不及待的体验一下Go强大的标准库,比如:用几行代码写一个像下面示例这样拥有完整功能的web server: // 来自https://tip.golang.org/pkg/net/http/#example_ListenAndServe package main import ( "io" "log"

  • 基于Nuxt.js项目的服务端性能优化与错误检测(容错处理)

    nuxt.js 是一个基于 Vue.js 的服务端渲染应用框架,使用nuxt.js在做同构项目开发时,需要考虑的一些点总结如下: 一.node服务端性能优化(提高node应用程序处理高流量的能力) 基于nuxt.js的服务端渲染项目我们能做的服务端性能优化有以下几点(需要注意的是持久化缓存不应该在本地开发环境去做,这样在缓存期间不会暴露本地开发中代码的问题) 优化点 参考文档及思路 优化场景/条件 特别说明 检测方法 1. 页面缓存 vue官方文档 页面内容不是用户特定(即对于相同的 URL,总

  • Netty分布式Server启动流程服务端初始化源码分析

    目录 第一节:服务端初始化 group方法 初始化成员变量 初始化客户端Handler 第一节:服务端初始化 首先看下在我们用户代码中netty的使用最简单的一个demo: //创建boss和worker线程(1) EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); //创建ServerBootstrap(2) ServerBootst

  • React服务端渲染(总结)

    一.前言 为什么需要服务端渲染?什么情况下进行服务端渲染?笔者认为,当我们要求渲染时间尽量快.页面响应速度快时(优点),才会采用服务器渲染,并且应该"按需"对页面进行渲染 --"首次加载/首屏".即服务端渲染的优势在于:由中间层( node端 )为客户端请求初始数据.并由node渲染页面.那客户端渲染和服务端渲染有什么差别?服务端渲染究竟快在哪里呢? 二.原因与思路 客户端渲染路线:1. 请求一个html -> 2. 服务端返回一个html -> 3.

  • 详解React+Koa实现服务端渲染(SSR)

    React是目前前端社区最流行的UI库之一,它的基于组件化的开发方式极大地提升了前端开发体验,React通过拆分一个大的应用至一个个小的组件,来使得我们的代码更加的可被重用,以及获得更好的可维护性,等等还有其他很多的优点... 通过React, 我们通常会开发一个单页应用(SPA),单页应用在浏览器端会比传统的网页有更好的用户体验,浏览器一般会拿到一个body为空的html,然后加载script指定的js, 当所有js加载完毕后,开始执行js, 最后再渲染到dom中, 在这个过程中,一般用户只能

  • 使用pthread库实现openssl多线程ssl服务端和客户端

    服务端代码如下: 复制代码 代码如下: #include <stdio.h>#include <stdlib.h>#include <memory.h>#include <errno.h>#ifndef    _WIN32#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#inclu

  • 解析Go 标准库 http.FileServer 实现静态文件服务

    http.FileServer 方法属于标准库 net/http,返回一个使用 FileSystem 接口 root 提供文件访问服务的 HTTP 处理器.可以方便的实现静态文件服务器. http.ListenAndServe(":8080", http.FileServer(http.Dir("/files/path"))) 访问 http://127.0.0.1:8080,即可看到类似 Nginx 中 autoindex 目录浏览功能. 源码解析 我们现在开始将

  • 使用gd库实现php服务端图片裁剪和生成缩略图功能分享

    裁剪示例: 最终裁剪成的图片: 其中虚线框内就是要裁剪出来的图片,最终保存成100宽的图片.代码如下: 复制代码 代码如下: $src_path = '1.jpg';//创建源图的实例$src = imagecreatefromstring(file_get_contents($src_path)); //裁剪开区域左上角的点的坐标$x = 100;$y = 12;//裁剪区域的宽和高$width = 200;$height = 200;//最终保存成图片的宽和高,和源要等比例,否则会变形$fi

  • 详解使用Next.js构建服务端渲染应用

    next.js简介 最近在学React.js,React官方推荐使用next.js框架作为构建服务端渲染的网站,所以今天来研究一下next.js的使用. next.js作为一款轻量级的应用框架,主要用于构建静态网站和后端渲染网站. 框架特点 使用后端渲染 自动进行代码分割(code splitting),以获得更快的网页加载速度 简洁的前端路由实现 使用webpack进行构建,支持模块热更新(Hot Module Replacement) 可与主流Node服务器进行对接(如express) 可自

  • Openssl实现双向认证教程(附服务端客户端代码)

    一.背景说明 1.1 面临问题 最近一份产品检测报告建议使用基于pki的认证方式,由于产品已实现https,商量之下认为其意思是使用双向认证以处理中间人形式攻击. <信息安全工程>中接触过双向认证,但有两个问题. 第一个是当时最终的课程设计客户端是浏览器,服务端是tomcat双向认证只需要对两者进行配置并不需要自己真的实现代码. 第二个是虽然课程也有接近双向认证的实现代码,但当时是Java+JCE环境现在要用C+++OpenSSL环境,总体意思确实还是差不多但具体函数和参数差别还是不少. 所以

随机推荐