一個(gè)常見的需求是統(tǒng)計(jì)網(wǎng)頁(yè)的瀏覽量,我們將使用不同的技術(shù)方案來(lái)比較對(duì)并發(fā)性能的影響,常用的技術(shù)方案包括使用內(nèi)存,redis,mysql等保存計(jì)數(shù),這里為了突出個(gè)方案對(duì)并發(fā)性能造成的影響,我們將使用同步計(jì)數(shù)器,即計(jì)數(shù)完成之后才返回頁(yè)面,我們將使用express.js框架來(lái)提供接口并使用autocannon工具進(jìn)行壓測(cè)。
首先我們看下一個(gè)簡(jiǎn)單的helloworld頁(yè)面:
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}`))
看一下這個(gè)頁(yè)面的qps,平均2萬(wàn)1左右,我們將使用這個(gè)作為基準(zhǔn)看各個(gè)方案對(duì)并發(fā)性能造成的影響:
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)首先,我們考慮使用內(nèi)存計(jì)數(shù)器:
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}`))
看一下并發(fā)性能,qps依舊保持在2萬(wàn)多,對(duì)性能幾乎沒(méi)有什么影響,但內(nèi)存計(jì)數(shù)器的劣勢(shì)也是很明顯的,不支持集群,不能做持久化,node進(jìn)程結(jié)束內(nèi)存中的數(shù)據(jù)就丟了。
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命令實(shí)現(xiàn)自增,很適合用作計(jì)數(shù)器:
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}`))
看一下并發(fā)性能,qps保持在1萬(wàn)9左右,比內(nèi)存計(jì)數(shù)器要稍微少一點(diǎn),但并沒(méi)有損失多少,是一個(gè)很理想的替代方案。
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)我們?cè)趤?lái)看使用mysql的話,性能會(huì)影響多少,首先我們來(lái)設(shè)計(jì)一張表來(lái)保存計(jì)數(shù):
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}`))
我們看下并發(fā)性能,發(fā)現(xiàn)qps降到了2千多,差不多只有原接口的1/10,響應(yīng)時(shí)間也由原來(lái)的0.02ms增加到4.14ms,對(duì)性能的影響還是很大的。
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
總結(jié)
在上面的例子中,我們使用了同步計(jì)數(shù)器來(lái)比較內(nèi)存、redis和mysql對(duì)并發(fā)性能的影響,內(nèi)存和redis對(duì)性能的影響較小,mysql影響較大,而且redis還可以使用在集群中并且支持持久化,因此是最佳選擇。在真實(shí)的使用場(chǎng)景中,對(duì)于計(jì)數(shù)器,我們一般使用異步的方式,因此對(duì)性能影響不會(huì)有較大差異,但使用redis依舊是很好的選擇。