0%

问题

原始需求是通过 nginx 在请求链接中增加一个固定参数 custom_param,方法很简单,在 location 中重设 $args

1
2
3
# in location xxx
set $args $args&custom_param=test;
proxy_pass http://remote_host;

顺便说一下,即使链接中没有参数也不影响一般服务端解析,符号?会自动加上,如上例子 proxy_pass 后会路径将变成 http://remote_host/xxx?&custom_param=test。

但问题是在服务器如此配置 nginx 后,custom_param 参数并没有如约发给 remote_host……

Read more »

有关被 nginx 官方自我批判的 if 语句,《If Is Evil》—— 着实应成为每位开发者初次使用 nginx if 之前必读的文章。

即使笔者一年前就开始接触使用 nginx 和 if 的组合做 url 跳转,但涉及的功能太简单,一直没能见识到它的真面目。直到近期在多个 if 内处理 proxy_pass 时,噩梦果真就降临了……希望大家仔细阅读上面的文章,引以为戒。

如果想快速知道为什么 “if is evil”,以及如何避免被坑,可以向下阅读一探究竟。

Read more »

本篇不是讲如何处理代码内的定时任务,而是聊聊怎么借助 crontab 等工具,定时操作 pm2 (指令)。例如,定时重启 pm2 中的进程、定时执行 pm2 save 保存运行状态,等等。

Read more »

log4js 和 cluster

写策略

node cluster 多个进程同时写一个文件是不安全的,通常会只选择一个 master 进程负责写入,其他 worker 进程则将数据传输到 master。

log4js 的写策略正是如此,但默认只适用于 node 原生的 cluster 模式,然而通过 pm2 启动的进程都是 worker。

官方提供的方案是安装 pm2-intercom,并在代码配置 log4js 时打开 pm2: true 选项,其原理也是选出一个负责写文件的主进程。

1
pm2 install pm2-intercom

选举 master

log4js 选择主进程的策略

1
2
const isPM2Master = () => pm2 && process.env[pm2InstanceVar] === '0';
const isMaster = () => disabled || cluster.isMaster || isPM2Master();

其中 disabled 是 log4js 的 disableClustering 选项,设置为 true 后,所有进程都将作为 master 进而拥有写文件的权限,这并没有解决安全问题。它存在的价值后面再说。

每个 pm2 启动的进程都有唯一的 process.env.NODE_APP_INSTANCE 标识,process.env.NODE_APP_INSTANCE === '0' 是常见的选择主从方式。多进程同时记录日志时,也可以用此方式指定唯一的进程负责写文件,避免同时写文件造成的冲突。此外 pm2 支持通过重命名 instance_var 来改变 process.env 的标记名,目的是解决和 node-config 包共用导致的异常。

关于 NODE_APP_INSTANCE 的 pm2 官方说明

问题多多的 pm2-intercom 方案

当下 pm2 的版本是 3.2.x,而 pm2-intercom 在 pm2 2.x 版本就已经存在异常了,重复日志甚至丢失日志,或在开发环境运行正常,到了线上莫名失败。更严重的是,当一个 pm2 box 内运行多个 cluster 模式启动的应用时,日志记录会变得混乱,各应用的日志都乱入了最早启动的应用的日志文件中。

log4js 的维护者 nomiddlename 也在 issue 中表示 pm2-intercom 存在着古怪的问题。

log4js doesn’t work with PM2 cluster mode #265

pm2-intercom has always seemed a bit dodgy - for some people it never works at all anyway. It didn’t need to use git clone when I installed it. Best plan might be to use the disableClustering option in your log4js config, log to stdout and let pm2 handle the files as it normally would.

option.disableClustering 不是银弹

上面再次提到 disableClustering 选项,不错,pm2-intercom 异常的场景可以拿它救场,但要注意它本身不适用于直接写文件,每个进程都被赋予了 master 权限,会再次引发开篇的冲突问题。官方文档也明确警示:Be careful if you’re logging to files

pm2-intercom 粗解

这个模块是简易的 IPC,借助 process.send 方法,将多个进程的数据包统一发送至 pm2 中编号为 0 的 pm2-intercom 进程,此进程再将收到的消息推送至项目进程中的一个。

在log4js的使用场景,表现为各自进程的日志首先发送到 pm2-intercom,由 pm2-intercom 分发到全部进程,但只有 log4js isMaster 才会写文件。

分发代码如下

1
2
3
4
5
6
7
function(packet) {
async.forEachLimit(process_list, 3, function(proc, next) {
sendDataToProcessId(proc.pm_id, packet);
}, function(err) {
if (err) console.error(err);
});
}

小结

如果想在多进程模式下记录日志到同一个文件,log4js + PM2 显然不是完美的组合。winston 也有人反馈丢日志的问题,但没有得到官方回复前,仍需要验证。

如果想安全记录日志,还是得分多个文件,或脱离 pm2,像 egg 一样在框架层面自行 cluster。

Reference

What?

源于一次mongodb超时问题查证。具体表现是服务响应异常缓慢,mongodb查询甚至报出cursor id not found

在排除网络连通性和主机自身因素后,继续回归mongo连接异常的点上,直到发现mongo集群的iptables没有开放mongo端口,开墙后一切恢复正常。

Why?

据查证,防火墙关闭mongo端口已经有一段时间了,为什么很久之后才出现连接不上的问题?

因为iptables不会主动断开已经建立的连接,这不是packet filter的职责所在。

但它依然有办法阻止现有连接的数据包(iptables的过滤基于数据包而非连接)。只不过,通常我们配置防火墙时都会加上一句:

1
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

这就导致了已建立的连接可以无障碍地继续传输。

正因如此,mongodb的连接早已建立,被iptables置于ESTABLISHED中,所以不会受到仅ip规则更新的影响。当业务进程重启、网络波动等情况导致旧的连接断开时,将无法重新连接。

Reference

https://linux.die.net/man/8/iptables

https://serverfault.com/questions/785691/how-does-one-close-all-existing-tcp-connections-on-some-ports-using-iptables

概述

Node内部不支持直接操作GBK字符串,而实际也并不需要如此。

总的原则是,gbk的逻辑仅保留在输入和输出,内部处理一律使用utf8。编码转换主要基于iconv-lite库。

总结已经写在了前头,下面再列举几种http服务中常见的处理场景。

常见场景

请求返回值

最常用且容易处理,通常我们使用request发起http请求,options中设置encoding: null,这样返回的res.body为buffer,再对buffer进行解码iconv.decode(res.body, encoding)

引用:request返回值中文乱码问题

请求参数

这里直接用iconv-lite处理略显复杂,建议上urlencode

post请求时stringify整个body对象,用options.form提交。

1
urlencode.stringify(body, {charset: 'gbk'});

querystring则stringify后再拼到url中。

1
urlencode.stringify(qs, {charset: 'gbk'});

接口返回值

以koa举例,返回值先使用iconv-lite转为gbk Buffer,随后设置响应头的content-type。

1
2
ctx.body = iconv.encode('你好', 'gbk');
ctx.type = 'text/plain; charset=gbk';

接口参数

同样以koa举例,结合koa-bodyparser,一般http method的原始参数分布在ctx.request.rawBody和ctx.request.querystring中,使用urlencode.parse解析。

1
2
urlencode.parse(ctx.request.rawBody, {charset: 'gbk'});
urlencode.parse(ctx.request.querystring, {charset: 'gbk'});

特别地,当请求格式为multipart或json时需要结合具体情况具体分析。

例如,使用busboy等multipart解析库会将请求body挂在ctx.request.body上,规范的请求方式是会对字符进行url encode的,这时可以按gbk编码对字段decode(由于不能直接url decode,实际处理方法为转hex后再经buffer解码)。

如果请求参数是经过binary处理的,则binary decode。

综上,处理姿势大致如下。

1
2
3
4
5
6
7
8
9
lodash.mapValues(ctx.request.body, value => {
if (!value) return value;

const buff = /^(%\w{2})+$/.test(value)
? Buffer.from(value.replace(/%/g, ''), 'hex')
: Buffer.from(value, 'binary');

return iconv.decode(buff, 'gbk');
});

想兼容更多情况是比较复杂的,即使做基础服务也不必包容所有不规范的传值,大可以拒绝解析,因此按需调整即可。

如果能约定使用十六进制传参更好,处理hex就不需要在参数获取上额外操作了。可惜一般用到gbk的场景都是难以变更的、需要兼容的,否则肯定是让调用方改传utf8,皆大欢喜。

读写文件

默认方式(encoding: null)就是操作buffer,iconv转换无压力。

读:

1
2
const buff = fs.readFileSync('test.txt');
console.log(iconv.decode(buff, 'gbk'));

写:

1
2
const buff = iconv.encode('你好', 'gbk');
fs.writeFileSync('test.txt', buff);

近期Evernote因为局域网问题不能使用了,作为重要工具不能离手,于是借助ss代理方式应急一下。ubuntu18没有特别好用的ss GUI,故选择了命令行工具ss-local。部署没难度,操作流程是翻文档加自己探索,个人认为比网上其他攻略简单,分享出来希望有助于大家解决网络疑难杂症。同时声明,本文只涉及客户端部署,evernote.com截止文章发布时间并没有被墙,请使用国内云服务器代理合规站点。
Read more »

本文首先简单介绍Jest关于启动的配置项,阐述Jest测试生命周期,之后粗略总结不同情况的server应该何时启动,最终使用略粗鄙的办法解决本次问题。
Read more »

4.0正式版已经出了3个多月,相比测试阶段网上有价值的资料日渐丰富,版本升级以及使用事务需要了解的知识都可以在官网找到。在这里记录一下升级本地开发环境的过程,生产环境应当用数据备份再恢复的方案。

总文档:Release Notes for MongoDB 4.0

版本升级

单机开发环境,参考standalone升级文档

以下升级流程节选自上述文档

确保本地是3.6版本才能继续进行,以及兼容版本featureCompatibilityVersion为3.6。在mongo shell中可以执行检查和设置。

1
2
3
db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )

db.adminCommand( { setFeatureCompatibilityVersion: "3.6" } )

升级前应关闭mongod服务和备份数据,之后按照官网所给出对应系统的安装方法执行安装。

例如Ubuntu18,可以按下面依次执行

1
2
3
4
5
6
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4

echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.0.list

sudo apt-get update
sudo apt-get install -y mongodb-org

升级完成后在mongo shell中重新设置兼容性

1
db.adminCommand( { setFeatureCompatibilityVersion: "4.0" } )

事务使用

replSet

目前必须在replSet中使用,简单的配置方法是/etc/mongod.conf添加设置

1
2
replication:
replSetName: rs0

然后重启mongod service mongod restart,在mongo shell中执行初始化并查看结果

1
2
rs.initiate()
rs.conf()

API

官方使用教程,内含demo:
https://docs.mongodb.com/manual/core/transactions/

Nodejs相关文档(npm包需更新):

手中项目使用事务的场景不多,暂时没有遇到坑,之后遇到再补充。