以前简单测过 go 的性能, 高并发场景下确实比 node 会好一些, 一直想找个时间系统性地测一下, 手头正好有一台前段时间买的游戏主机, 装了 Ubuntu 就开测了
准备工作
测试机和试压机系统都是 Ubuntu 18.04.1
首先安装 node 和 go, 版本分别如下:
- node 10.13.0
- go 1.11
测试机和试压机修改 fd 的限制 ulimit -n 100000 , 否则 fd 很快就用完了.
如果是试压机是单机, 并且 QPS 非常高的时候, 也许你会经常见到试压机有 N 多的连接都是TIME_WAIT状态, 具体原因可以在网上搜一下, 执行以下命令即可:
- $ sysctl -w.NET.ipv4.tcp_timestamps=1
- $ sysctl -w.NET.ipv4.tcp_tw_reuse=1
- $ sysctl -w.NET.ipv4.tcp_tw_recycle=1
测试工具我用的是 siege, 版本是3.0.8.
测试的 JS 代码和 go 代码分别如下:
- Node(官网的 cluster 示例代码)
- const cluster = require('cluster');
- const http = require('http');
- const numCPUs = require('os').cpus().length;
- if (cluster.isMaster) {
- console.log(`Master ${process.pid} is running`);
- // Fork workers.
- for (let i = 0; i <numCPUs; i++) {
- cluster.fork();
- }
- cluster.on('exit', (worker, code, signal) => {
- console.log(`worker ${worker.process.pid} died`);
- });} else {
- // Workers can share any TCP connection
- // In this case it is an HTTP server
- http.createServer((req, res) => {
- res.end('hello world\n');
- }).listen(8000);
- console.log(`Worker ${process.pid} started`);}
- Go
- package main
- import(
- "net/http"
- "fmt"
- )
- func hello(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintf(w, "hello world")
- }
- func main() {
- http.HandleFunc("/", hello);
- err := http.ListenAndServe(":8000", nil);
- if err != nil {
- }
- }
开始测试
首先开始并发量的测试, 然而... 游戏主机的 CPU 是 i7-8700K, 性能太强, 以至于拿了两台 Mac 也没能压满...
四处寻找一番, 找到了好几年前花了千把块钱配的 nas, 上去一看是颗双核的 i3-4170, 妥了, 这下肯定没问题
正式开始
跳过了小插曲, 直接开测
I/O 密集型场景
Node - 多进程模型, 可以看到因为所有请求都由 master 进程转发, master 进程成为了瓶颈, 在 CPU 占用 100% 的情况下, worker 进程仅仅只占了 50%, 因此整体 CPU 利用率只有 70%.
qps: 6700
Go - 单进程多线程模型, 系统剩余 CPU 差不多也有 30%, 查了一下原因是因为网卡已经被打满了, 千兆网卡看了下已经扎扎实实被打满了.
qps: 37000
千兆网卡已经被打满了
在 helloworld 场景下, 如果我们有万兆网卡, 那么 go 的 qps 就是 50000,go 是 node 的 7 倍多, 乍一看这个结果就非常有意思了, 下面我们分析下原因:
首先 node 官方给的 cluster 的例子去跑压测是有问题的, CPU 物理核心双核, 超线程成 4 核, 姑且我们就认为有 4 核.
cluster 的方案, 先起主进程, 然后再起跟 CPU 数量一样的子进程, 所以现在总共 5 个进程, 4 个核心, 就会造成上下文频繁切换, QPS 异常低下
基于以上结论, 我减少了一个 worker 进程的数量
Node 多进程模型下, 1 个主进程, 3 个 worker 进程
qps: 15000 (直接翻倍)
以上的结果证明我们的想法是对的, 并且这个场景下 CPU 利用率几乎达到了 100%, 不是很稳定, 暂且我们可以认为压满了, 所以我们可以认为 1master4worker 场景下, 就是因为进程上下文频繁切换导致的 qps 低下
那么进一步想, 如果把进程和 CPU 绑定, 是不是可以进一步提高 qps?
Node 多进程模型, 并且用taskset命令把进程和 CPU 绑定了
qps: 17000 (比不绑定 CPU 性能提高了 10% 多)
结论 :node 在把 CPU 压满的情况下, 最高 qps 为: 17000, 而 go 在 CPU 利用率剩余 30% 的情况, qps 已经达到了 37000, 如果网卡允许, 那么 go 理论上可以达到 50000 左右的 qps, 是 node 的 2.9 倍左右.
CPU 密集场景
为了模拟 CPU 密集场景, 并保证两边场景一致, 我在 node 和 go 中, 分别添加了一段如下代码, 每次请求循环 100W 次, 然后相加:
- var b int
- for a := 0; a < 1000000; a ++ {
- b = b + a
- }
Node 多进程模型: 这里我的测试方式是开了 4 个 worker, 因为在 CPU 密集场景下, 瓶颈往往在 woker 进程, 并且将 4 个进程分别和 4 个核绑定, master 进程就让他随风飘摇吧
qps: 3000
go:go 不用做特殊处理, 不用感知进程数量, 不用绑定 CPU, 改了代码直接走起
qps: 6700, 依然是 node 的两倍多
结论
Node 因为用了 V8, 从而继承了单进程单线程的特性, 单进程单线程好处是逻辑简单, 什么锁, 什么信号量, 什么同步, 都是浮云, 老夫都是 await 一把梭.
而 V8 因为最初是使用在浏览器中, 各种设置放在 node 上看起来就不是非常合理, 比如最大使用内存在 64 位下才 1.4G, 虽然使用 buffer 可以避开这个问题, 但始终是有这种限制, 而且单线程的设计(磁盘 I/0 是多线程实现), 注定了在如今的多核场景下必定要开多个进程, 而多个进程又会带来进程间通信问题.
Go 不是很熟, 但是前段时间小玩过几次, 挺有意思的, 一直听闻 Go 性能好, 今天简单测了下果然不错, 但是包管理和错误处理方式实在是让我有点不爽.
总的来说在单机单应用的场景下, Go 的性能总体在 Node 两倍左右, 微服务场景下, Go 也能起多进程, 不过相比在机制上就没那么大优势了.
Node
优点
单进程单线程, 逻辑清晰
多进程间环境隔离, 单个进程 down 掉不会影响其他进程
开发语言受众广, 上手易
缺点
多进程模型下, 注定对于多核的利用会比较复杂, 需要针对不同场景 (CPU 密集或者 I/O 密集) 来设计程序
语言本身因为历史遗留问题, 存在较多的坑.
Go
优点
单进程多线程, 单个进程即可利用 N 核
语言较为成熟, 在语言层面就可以规避掉一些问题.
单从结果上来看, 性能确实比 node 好.
缺点
包管理方案并不成熟, 相对于 NPM 来说, 简直被按在地上摩擦
if err != nil ....
多线程语言无法避开同步, 锁, 信号量等概念, 如果对于锁等处理不当, 会使性能大大降低, 甚至死锁等.
本身就一个进程, 稍有不慎, 进程 down 了就玩完了.
来源: http://www.qdfuns.com/article/51116/f536e1eba46824cb6b87498758c43212.html