美文网首页
同步计数器的并发性能

同步计数器的并发性能

作者: fineen | 来源:发表于2020-07-16 23:15 被阅读0次

一个常见的需求是统计网页的浏览量,我们将使用不同的技术方案来比较对并发性能的影响,常用的技术方案包括使用内存,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依旧是很好的选择。

相关文章

网友评论

      本文标题:同步计数器的并发性能

      本文链接:https://www.haomeiwen.com/subject/qozmhktx.html