node.js异步数据库连接,事务查询思考

首先思考一下场景

1、同时有50个并发请求,
2、每个请求将连接PgSQL并执行一个事务: 调用2个不同的Model Bill与Profile,执行 Bill.insert 与 Profile.update
3、PgSQL最大连接数为20

因为使用到事务,所以同一个数据库连接不可能同用于两个不同的http请求

如果在PHP中,那么开20个不同的php-cgi,并对每个php-cgi进行操作,其余30个在请求nginx队列
1、nginx接受50个http请求
2、php-cgi接受20个请求,并建立20个全局持久化连接 db
3、Model Bill 与 Profile 通过全局变量 db 进行操作
4、cgi结束进行各种资源,连接回收
5、对队列中下一个请求进行操作

在node中行为将变成
1、接受50个http请求
2、从最大连接数为20的连接池中取得一个连接局域变量db,其余30个连接请求加入队列
3、Model Bill 与 Profile 通过某种途径访问局域变量进行操作
4、单个请求完毕,对资源连接回收
5、对队列中下一个请求进行操作

由此,对比PHP与node,本质上的不同仅仅是 db 变量的全局与局域的区别,

一、解决方法

以下测试均在CPU Atom D510 1.66G 单核下测试。

1. 通过vm

通过vm的runInNewContext可以把局域变量变成全局变量,但性能上大打折扣, 对于空脚本的
script.runInNewContext() 为 1386 tps,
script.runInThisContext() 为 416667 tps
eval 为 13966 tps,
空循环 为 Infinity tps

由上述可列公式,
在runInNewContext中的最终性能为 1386x / (1386 + x)
在runInThisContext中的最终性能为 416667x / (416667 + x)

比如,原程序通过使用node达到了1000 rps,也是node使用http.createServer跑hello world的性能
重新设计并使用scriptInNewContext后,那么性能最佳也只能有 1386k / 2386 = 580 rps,
而在实际使用中,包含一次redis查询的完整应用,包含render,cache等部份可以达到 550 rps,那么runInNewContext性能降至 316 rps,
而相对应的php,包含一次redis查询的更复杂的完整应用,也可以保持在240 rps,对比,使用了scriptInNewContext后,node不再具有性能优势。

如果使用scriptInThisContext,性能只下降到 416667k / 417667 = 997 rps,几乎没有影响。但在这里没啥用。scriptInThisContext用于模版rendering是非常之不错。

2. 通过建立model后赋于变量

每个Model实例添加额外的变量,用以保存当前请求将要用到的连接。
那么 model 则由new Model(arg1, arg2, …) 变成 req.create(Model, arg1, arg2, …),似乎能很好的解决创建问题。
但若要执行静态函数,Model.find_or_create 等, 变成 req.exec(Model, Model.find_or_create, arg1, arg2, ….),感觉上就不漂亮了。

3. Model模版

使用闭包生成一个新的Model类,是个好方法,在Model的使用上也无区别。对性能的影响为 714k tps, 如果每次必须重新生成10个Model, 则为71k,对性能的影响也是微乎其微。
ModelTemplate模型 ToffeeScript 代码

ModelTemplate = (req) ->
  {db} = req
  class Model
    @create: ->
      console.info "create #{db}"

st = Date.now()
for i in [1..1000]
  Model = ModelTemplate({db: 'hello'})
  
console.info Date.now() - st

如果有多个Model
比如 class Bill, class Profile,改进成

ModelTemplate = (req) ->
  {db} = req
  class Bill
    @create: ->
      console.info "create bill #{db}"

  class Profile
    @create: ->
      console.info "create profile #{db}"

  {Bill, Profile}


st = Date.now()
for i in [1..1e4]
  models = ModelTemplate({db: :hello})
  {Bill} = models

  
console.info 1e4 * 1e3 / (Date.now() - st)

但仍然有个缺点,各个Model揉在一起,再改进成


ModelTemplateBill = (req) ->
  {db} = req
  class Bill
    @create: ->
      console.info "create bill #{db}"

ModelTemplateProfile = (req) ->
  class Profile
    @create: ->
      console.info "create profile #{db}"

ModelTemplate = (req) ->
  {db} = req

  {Bill}    = ModelTemplateBill(req)
  {Profile} = ModelTemplateProfile(req)

  {Bill, Profile}


st = Date.now()
for i in [1..1e4]
  models = ModelTemplate({db: :hello})
  {Bill} = models

  
console.info 1e4 * 1e3 / (Date.now() - st)

若再借用exports思想,那么可以变成

# models/bill.toffee
ModelTemplateBill = (req) ->
  {db} = req
  class Bill
    @create: ->
      console.info "create bill #{db}"

module.exports = ModelTemplateBill

#----
ModelTemplate = (req) ->
  @BillTemplate    ?= require('./models/bill')
  @ProfileTemplate ?= require('./models/profile')

  Bill    = @BillTemplate(req)
  Profile = @ProfileTemplate(req)

  {Bill, Profile}

st = Date.now()
for i in [1..1e4]
  models = ModelTemplate({db: :hello})
  {Bill} = models

console.info 1e4 * 1e3 / (Date.now() - st)

4. 资源池

由于Model的引入的最主要目的是对连接的控制,达到类似连接池的效果,因此由此脱离req,建立持久化连接池才是根本目的
代码则变成

class ResourcePool
  # 代码略

st = Date.now()
res_pool = ResourcePool.create({db: :hello})

for i in [1..1e4]
  res = res_pool.create()
  {Bill, db} = res
  res.close()

console.info 1e4 * 1e3 / (Date.now() - st)

而resurce中的各个连接池独立,且redis,pgsql最大连接数量可以不同,只要在设计成在执行查询时进行连接即可,只要保证在resource中的db等连接在一次生命周期永远是同一指向即可。

二、小结

资源池应该是最佳方案,性能,易用性兼得。

发表评论

电子邮件地址不会被公开。 必填项已用*标注