KOA-Vcenter开发手册 1

先介绍一下Vcenter,Vcenter是我欲开发的一个完全的后台项目,由KOA驱动,届时会将我手头所有的网站资源都转移到Vcenter之中,将其作为一个完全的后台处理中心。

有句话说得好,过早的优化是万恶之源

框架开发了一个起点,我就迫不及待开始对它进行优化

优化起点1 SESSION持久化

一个优秀的框架肯定要支持 SESSION ,当然,也可以适度支持一下 SESSION 持久化,在Vcenter中,对于SESSION的需求主要体现在网站方面,但是仅仅对网站提供 SESSION 肯定不够完美,最好对微信公众号后台也支持 SESSION ,但是由于服务器仅对微信消息暴露一个接收消息的接口,无法从HTTP协议上区分,因此必须设计一个兼容的 SESSION 存储方案。

npm上面那个koa-session肯定是不太符合的,那个必须通过cookies来标记,于是只能自己手动开发。

session插件开发的几个问题都比较好解决,唯一的难点是session持久化。
在持久化方案上,我选择了Mysql,直接存储SESSION对象的JSON字符串的方式。

再根据db的要求,稍稍优化一下代码,最后每个连接仅访问两次数据库,一次空连接的TTFB从10ms被压缩到6ms。

优化起点2 DB单例

Vcenter作为一个中心服务器的话,高并发支持是肯定要有的,所以我在导入DB之后就开始了DB的并发测试。
工具是Autocannon,测试结果非常糟糕。

在一开始的编码设计中,我为每一个ctx上下文创建了一个 数据库连接,这样当并发数较高的时候,就直接报出

too many connections

这样肯定是不行的,数据库又撑不住,因此必须对数据库的连接进行优化。
在DB框架上,我选择了一个Medoo,在PHP上,Medoo表现尚可,因此在nodejs上,虽然这个开发者的上次维护已经在两年前,但是我还是选择了继续使用这个框架,无他,唯手熟尔。

在ctx上下文中,我将db的导入做成了一个函数,使其状态单例,这样,整个应用程序并发的会话都将使用同一个数据库连接。
这也将空接口的TTFB时间从6ms缩短到2.5ms。

优化起点3 DB预编译语句

数据库状态单例以后,整个处理速度提升了一大截,由于SESSION依赖数据库,因此优化数据库的好处是很多的。

再用一次Autoconnon,这一次调高并发数和测试时间。

不出意外,数据库又报错了,这一次报的错误是

 Can't create more than max_prepared_stmt_count statements

赶紧Google之,发现是预编译语句的问题,调用了太多的预编译语句而没有释放,导致了这个问题。
要解决很简单,调高Mysql的预编译语句上限即可,但是问题不在此,我才写了几句sql,当然不可能是Mysql的问题

Medoo本身使用的是node-mysql2,因此先去node-mysql2的issue查找,很快找到了类似的问题,但是该问题早在6年前就已被修复。
细查mysql2的文档库,其清楚的表明了,使用execute()函数可以防注入,原理是将其预编译,在预编译以后,字符串部分会使用一个简单的缓存存储下来,以便下次直接使用。

看到这里都没啥问题,没办法了,log调试,好在medoo本身支持debug模式,开了以后,会在execute之前将sql语句打印出来。

仔细观察了一下,终于找到了原因。
Medoo本身对语句进行了防注入处理,但是编译出来的sql表达式却并非是正常的预编译语句

Medoo :
execute(SELECT `value` FROM `session` WHERE `NSESSIONID` = '97507215cd067b1e4bcc9b6b420a491be13cef8e' AND `expires` > 1539583338076 LIMIT 1)

Mysql2 :
execute('SELECT * FROM `table` WHERE `name` = ? AND `age` > ?',['Rick C-137', 53])

在Medoo提交execute的时候,查询条件也作为了预编译的一部分,由于每次请求都会产生不同的语句,导致缓存策略失效,每次产生不同的预编译语句使Mysql的预编译语句达到上限,由此产生了大量的报错。

解决方案,首先Fork了该Medoo库,由于原作者已经两年没有维护了,我也不指望能够通过Issue继续沟通,Fork之后直接将execute改成query,反正都已经生成完整的sql语句了,直接通过query执行查询即可。

但是与此同时,也表明着这可能会带来一定的注入风险,于是拿出TodoList,记下一笔

完善修复数据库框架Medoo ,修复其预编译功能

结语

过早的优化带来的将会是无尽的坑。
但是这次优化也给我提了个醒,npm并非是一块金山,其上的package大多良莠不齐。
在引入一些包和框架的同时,勤于测试也是保证代码质量的重要方法之一,否则一股脑上线了以后,这边报一个错,那边并发出问题,任谁也受不了。

以上就是Vcenter三个提前优化的全部心得,附一个Autocannon的测试数据。
server端:单进程
测试方案:100并发 * 10秒 * 2 pipe

snipaste_20181015_141050.png

在低并发数的情况下,响应时间要更为优异一点

Electron的一个增量升级方案

Electron的一个增量升级方案

本文主要是讲述一个托管于Github的Electron项目,如何实现使用小文件的增量升级

背景

我有一个托管在github的开源项目,使用的技术框架是 Electron+Vue,electron可以说是一个有一定名气的框架了,但是在处理一个增量更新的需求的时候就遇到了一些问题,特将我的处理方法分享一下。

已知 Electron其实是有很多自动升级的方案的,但是增量升级的非常少,所谓的自动升级方案大多是利用模块中的打包工具的预留接口或者在代码中直接下载整个安装数据包,如果是公司或者大型的项目,当然无可厚非,但是对于一个小型开源项目来说,这种做法十分的不经济。

首先一方面,我所使用的Electron-builder,每次打包生成的安装文件有40M之大,这是Electron 2.0的,前期项目还使用了Electron 1.8,安装文件也有36M。第二,这个项目比较小,直接托管在github上,使用github的release进行分发,如果要做自动更新的话,势必要自己部署更新和分发服务器(因为github的release基于aws,在国内被墙)。

想到之前看到的一篇文章,利用他们团队每次只更新Renderer.js的特性,做了个小型分发服务器,于是我开始寻找是否有直接分发升级代码的可能。

问题定位

我的项目是由 Electron-builder 进行打包的,其在打包过程中使用 nsis来创建windows环境下的安装包。于是先去找到安装后释放的文件。

在对应的安装目录下,会释放一个resource文件夹,其下有一个比较大的app.asar,根据程序 log,程序的主要代码文件就都储存在这里。

然后我们将 Electron-builder的配置文件进行修改,使其不打包为asar。

由于我使用的是Electron-Vue基于Vue-cli的自动构建,因此配置文件就是package.json

"build":{
   "asar": false, 
}

在build配置项下,将asar,设置为false,重新打包。这次直接使用dir模式打包来查看他的目录结构。

代码提示了一个不安全的warning,

asar using is disabled — it is strongly not recommended solution=enable asar and use asarUnpack to unpack files that must be externally available

不用管,打包完成后,在原app.asar的位置,变成了app文件夹,打开文件夹,是node_modules、package.json和一个dist文件夹,dist文件夹里是electron四件套

├─dist
│  └─electron
│      │  index.html
│      │  main.js
│      │  renderer.js
│      │  styles.css
│      │
│      └─fonts
│              element-icons--fonts.ttf

好了,这里就是重点了,这里的index.html、main.js、rederer.js和styles.css就是每次编译生成的文件,因此,如果要进行增量更新,这四个文件是必须更新的。

那么首先,我们要去除掉对这几个文件的打包,由于每次版本更新时,package.json的内容也会改变,所以package.json也必须排除。再次修改 Electron-builder的配置:

"build":{
   "asar": true,
    "asarUnpack":[
      "./dist/electron",
      "./package.json"
    ],
}

为什么一定要保留asar包呢?因为asar包有助于加快安装文件的安装速度,如果保留所有文件夹的话,在释放文件的时候磁盘压力会比较大,因此,使用asarUnpack属性,将不需要打进asar包里的文件路径指定。

再次查看打包后的结果:

│  app.asar
│  electron.asar
│
└─app.asar.unpacked
    │  package.json
    │
    └─dist
        └─electron
            │  index.html
            │  main.js
            │  renderer.js
            │  styles.css
            │
            └─fonts
                    element-icons--fonts.ttf

我们可以看到,在原来resource文件下,多出了一个app.asar.unpack文件夹,里面有所有我们所需要的文件。

完整方案

之所以说是完整方案而不是完整代码,是因为其具备一定的特例性。

我们这次的重点,依然还是针对托管在github的项目的增量更新,因此,我们首先要保证的,就是增量更新还得在github上。

我们不可能在发布一个release安装包的同时还去发布一个增量更新文件来进行增量更新,这不好,也不自动化,根据上节,我们已知,我们需要更新的就是包括package.json在内的五个文件,因此,我们首先需要定位这五个文件的位置,我使用的方法是,基于__filename,在代码中,通过搜索app.asar来定位到resource文件夹,再通过相对的路径获取到对应的文件的目录。

为什么是执行的时候路径里会有app.asar而不是app.asar.unpack呢 ,我是这么理解的,unpack的作用就是使程序在每一次运行之前,将unpack的文件重新打进asar包,然后再继续运行,这样既不破坏asar包的路径关系,也不会影响文件的独立性。

然后,我们需要在git里解除对dist目录的ignore,这样,我们每一次提交都会留下一个可以使用的增量更新文件,在release以后也可以根据tag来找到对应的dist里的文件。

其他的就没什么好说的了,剩下的就是程序该干的事情了,使用程序检查更新,是使用github的

https://api.github.com/repos/{username}/{repo}/releases/latest

可以获取到最后一个release的tag和一些release的信息,我们主要使用的是tag_name,根据tag_name,我们可以在

https://raw.githubusercontent.com/{username}/{repo}/${tag}/{relative_route}

这个地址获取到项目对应文件的原始代码。

github的请求需要带上有效的UserAgent,否则会被认为是非法

请求到对应文件之后,根据前面获取的本地路径,调用fs模块将字符写进文件,然后重启程序,就完成了这一次的增量更新。

总的来说,这样的更新方式有优点有缺点,优点是:增量更新的文件小,更新速度快,没有分发的烦恼。缺点当然也有,缺点就是无法适用于依赖会改变的情况,如果程序的依赖发生更改,那么之前的app.asar的文件就不再和新文件匹配,需要重新安装新的安装包。

后记

从全局来看,这种增量更新的办法是非常好的,既解决了用户频繁更新需要下载大安装包的问题,又解决了开发者不便部署安装服务器的问题,整个操作的过程也并不十分复杂,只需要在取消git对dist的ignore,并且每次relase之前确保当前分支build了最新的dist文件即可,如果github的连通率太低,甚至还可以使用国内的gitee的raw文件,使用克隆git仓库,每次release之后到gitee手动点一下拉取最新更新即可,如果不嫌烦的话,还可以多部署几个其他的开源平台,运维成本基本等于0。


项目相关:BLS,一个B站直播批量挂机工具。

关于Node.js的依赖管理

关于Nodejs的依赖管理

npm和yarn之类的工具能够自动化引入和构建依赖,但是最近发现一个比较坑的地方。

在npm上发布的package一般来说会锁定其所依赖的包的版本

我在使用request-promise的时候,发现request-promise已经依赖了tough-cookie,于是在把几个代码文件迁移到另一个项目的时候没有重新添加tough-cookie的依赖,导致中间解析cookie时出现了很多莫名其妙的错误,遂重新添加依赖后才成功。

也就是说,当一个项目越做越大以后,里面的包的依赖关系将会变得更加复杂,一个包所依赖的其他包的可能包含了某个package的多个版本。而随着package的增多,这种臃肿的包将会越来越多。

不禁想反思一下前端这个步子是不是迈得太大了