同步计数器的并发性能

一个常见的需求是统计网页的浏览量,我们将使用不同的技术方案来比较对并发性能的影响,常用的技术方案包括使用内存,redis,mysql等保存计数,这里为了突出个方案对并发性能造成的影响,我们将使用同步计数器,即计数完成之后才返回页面,我们将使用express.js框架来提供接口并使用autocannon工具进行压测。

首先我们看下一个简单的helloworld页面:

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

看一下这个页面的qps,平均2万1左右,我们将使用这个作为基准看各个方案对并发性能造成的影响:

Running 10s test @ http://localhost:3000
10 connections

┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬─────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max     │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼─────────┤
│ Latency │ 0 ms │ 0 ms │ 0 ms  │ 1 ms │ 0.02 ms │ 0.14 ms │ 8.21 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴─────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬──────────┬────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg      │ Stdev  │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼────────┼─────────┤
│ Req/Sec   │ 20543   │ 20543   │ 22159   │ 22287   │ 21785.46 │ 625.12 │ 20533   │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼────────┼─────────┤
│ Bytes/Sec │ 4.44 MB │ 4.44 MB │ 4.79 MB │ 4.81 MB │ 4.71 MB  │ 135 kB │ 4.44 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴──────────┴────────┴─────────┘

Req/Bytes counts sampled once per second.

240k requests in 11.05s, 51.8 MB read

(1)首先,我们考虑使用内存计数器:

const express = require('express')
const app = express()
const port = 3000
let counter = 0;

app.get('/', (req, res) => {
  counter++;
  res.send('Hello World!')
})

app.get('/counter', (req, res) => {
  res.json({ counter });
})

app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

看一下并发性能,qps依旧保持在2万多,对性能几乎没有什么影响,但内存计数器的劣势也是很明显的,不支持集群,不能做持久化,node进程结束内存中的数据就丢了。

Running 10s test @ http://localhost:3000
10 connections

┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 0 ms │ 0 ms │ 1 ms  │ 1 ms │ 0.04 ms │ 0.21 ms │ 16.01 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev   │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec   │ 13263   │ 13263   │ 21023   │ 21871   │ 20395.6 │ 2456.94 │ 13259   │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 2.87 MB │ 2.87 MB │ 4.54 MB │ 4.72 MB │ 4.41 MB │ 531 kB  │ 2.86 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘

Req/Bytes counts sampled once per second.

204k requests in 10.04s, 44.1 MB read

(2)要支持集群和持久化的话,我们首先想到的是使用redis,redis提供了INCR命令实现自增,很适合用作计数器:

const express = require('express')
const app = express()
const port = 3000
const redis = require('redis');
const client = redis.createClient();

app.get('/', (req, res) => {
  client.incr('counter', function(err, reply) {
    res.send('Hello World!')
  });
})

app.get('/counter', (req, res) => {
  client.get('counter', (err, counter) => {
    res.json({ counter });
  })
})

app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

看一下并发性能,qps保持在1万9左右,比内存计数器要稍微少一点,但并没有损失多少,是一个很理想的替代方案。

Running 10s test @ http://localhost:3000
10 connections

┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 0 ms │ 0 ms │ 0 ms  │ 1 ms │ 0.03 ms │ 0.22 ms │ 17.19 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬──────────┬─────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg      │ Stdev   │ Min     │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┼─────────┤
│ Req/Sec   │ 11895   │ 11895   │ 20431   │ 20767   │ 19630.91 │ 2464.33 │ 11889   │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┼─────────┤
│ Bytes/Sec │ 2.57 MB │ 2.57 MB │ 4.41 MB │ 4.49 MB │ 4.24 MB  │ 533 kB  │ 2.57 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴──────────┴─────────┴─────────┘

Req/Bytes counts sampled once per second.

216k requests in 11.05s, 46.6 MB read

(3)我们在来看使用mysql的话,性能会影响多少,首先我们来设计一张表来保存计数:

CREATE TABLE `hit_counter` (
  `id` int NOT NULL,
  `cnt` int unsigned NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `hit_counter`(`id`, `cnt`) VALUES (1, 0);

代码如下:

const express = require('express')
const app = express()
const port = 3000
const mysql = require('mysql');
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: '******',
  database: 'webapp'
});
connection.connect();

app.get('/', (req, res) => {
  connection.query('update hit_counter set cnt = cnt + 1 where id = 1', function(err, results) {
    res.send('Hello World!')
  });
})

app.get('/counter', (req, res, next) => {
  connection.query('select * from hit_counter where id = 1', function(err, results) {
    if (err) next(err);
    const counter = results[0].cnt;
    res.json({ counter });
  })
})

app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

我们看下并发性能,发现qps降到了2千多,差不多只有原接口的1/10,响应时间也由原来的0.02ms增加到4.14ms,对性能的影响还是很大的。

Running 10s test @ http://localhost:3000
10 connections

┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max      │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 4 ms │ 4 ms │ 5 ms  │ 7 ms │ 4.14 ms │ 0.59 ms │ 14.35 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬────────┬────────┬────────┬────────┬─────────┬─────────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%    │ 97.5%  │ Avg     │ Stdev   │ Min    │
├───────────┼────────┼────────┼────────┼────────┼─────────┼─────────┼────────┤
│ Req/Sec   │ 1925   │ 1925   │ 2157   │ 2193   │ 2135.73 │ 71.85   │ 1925   │
├───────────┼────────┼────────┼────────┼────────┼─────────┼─────────┼────────┤
│ Bytes/Sec │ 416 kB │ 416 kB │ 466 kB │ 474 kB │ 461 kB  │ 15.5 kB │ 416 kB │
└───────────┴────────┴────────┴────────┴────────┴─────────┴─────────┴────────┘

Req/Bytes counts sampled once per second.

23k requests in 11.03s, 5.07 MB read

总结
在上面的例子中,我们使用了同步计数器来比较内存、redis和mysql对并发性能的影响,内存和redis对性能的影响较小,mysql影响较大,而且redis还可以使用在集群中并且支持持久化,因此是最佳选择。在真实的使用场景中,对于计数器,我们一般使用异步的方式,因此对性能影响不会有较大差异,但使用redis依旧是很好的选择。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343