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