全干工程师的基本素养

Posted by Leo Eatle on 2019-08-30

这篇文章里我会记录一些平时做node后台的一些小点,非常零碎,不适合完整阅读。

1. 日志定位之awk的妙用

定位日志是web后台开发的日常,由于我这个部门基础设施比较原始,并没有诸如ELK的强大日志系统。一切都靠grepgrepgrep

但是,除了grep还有另一个强大的过滤日志工具,那就是awk

只要日志符合规范,就能够用awk按照分隔符来轻松取出对应字段并加上逻辑进行处理。比如我们的日志就是按照空格去分隔的,假设时间的是第4个,IP的是第8个。

zcat ~/log/error/xxx.log.gz | grep 'xxx' | awk '{if(\$4<\"20:26:00\" && \$4>\"17:45:00\"){print \$8}}' | sort | uniq -c | wc -l

上面这个命令,就可以通过去重IP,找到这段时间内调用这条cgi的用户量。

sort是排序

uniq是去重 -c参数是统计出每个IP重复的数量

wc是用来统计行数,有多少个IP就多少个用户

2. ssh的加解密算法配置

其实一般来说ssh配置已经没有什么坑了,一般的机器可以如下配置:

Host NewDevelop
User xxx
HostName 8.8.8.8
Port 3000
ServerAliveInterval 120

这样,直接通过ssh NewDevelop就能连接到对应机器。

但是万万没想到,在这个部门遇到个老古董机器,普通的ssh机器是无法连接的。

这台机器是用了特殊的加解密算法,经过一段时间的google和摸索,终于摸索出了一套算法配置:

Host NewDevelop
User xxx
HostName 8.8.8.8
Port 3000
ServerAliveInterval 120
Ciphers aes128-cbc,aes128-ctr
HostKeyAlgorithms ssh-dss
KexAlgorithms diffie-hellman-group1-sha1

3. gzip加密过的html解码

有个需求是需要在node端解析用户设置的url,然后从中提取出图文。没错就是模仿微信的图文消息那种感觉。

(问题是,人家微信是提供了js api,然后用户直接在html里调用api来设置头图的,我们敢吗,敢也没人用==)

总之用了一个xss库,本来是用来过滤html中的xss的,结果被我用来解析出里面的各种标签和图片。策略是解析出<title>和第一个<img>标签中的图。

然后遇到一个投诉,说解析出来是乱码。一开始怀疑是编码问题,但是发现那个返回的html已经设置了utf-8的,就算是gb2312,我node后台也有做识别和转码。这个url用浏览器访问没有任何问题,虽然是用asp的,但是返回的html还是比较符合规范的。

所以问题出在哪呢?试了半天发现,原来这个网址用的是gzip压缩,我并没有做解压,浏览器则都是自动识别+解压的。

其实一般的场景下,用request这个自带的模块传对应参数,就能自动做到解压gzip。但我们场景特殊,对于外网请求都要用一个C++插件去proxy转发,那个插件可没帮我们做gzip解压。

还好node有自带的解压库zlib,这个库就可以解压gzip压缩过的文档流。

大致判断是这样的(不要吐槽var,我们还在用node 0.12)

if (data.head.indexOf('Content-Encoding: gzip') > -1) {
// 对于通过gzip压缩返回的,需要解压
var gunzipPromise = new Promise(function(resolve, reject) {
self.ungzip(data.body, function(unzipData) {
resolve(unzipData);
});
});
return gunzipPromise;
}

但是在写这个ungzip方法的时候发现,如果直接用自带的zlib.gunzip去解压整个html,会报错Error: unexpected end of file

经过一番摸索,我发现还有一种基于流的解压方式:

/**
* 对于gzip过的html文档流进行解压
* 很奇怪的一点是如果用zlib.gunzip这种回调方式,会报错Error: unexpected end of file
* 所以采用了流的方式,把除了结尾部分的二进制解压后输出成string返回
* 即使error也无视啦
* @param {*} buffer gzip过的二进制数据
* @param {*} callback 流的回调,传入的data是string
*/
ungzip: function(buffer, callback) {
var Readable = require('stream').Readable;
var stream = new Readable();
stream.push(buffer);
stream.push(null);
var gunzip = zlib.createGunzip();
var body = '';
var callbackWrapper = function(e) {
callback(body);
};
stream.pipe(gunzip).on('data', function(chunk) {
body = body + chunk.toString();
}).on('close', callbackWrapper).on('error', callbackWrapper);
},

虽然对于这个用户的案例来说,每次解压到最后一定会报错,但是没关系,数据已经拿到了,直接回调就是了。

4. rsync的优化

作为全干工程师,devops当然也是要自己动手的,那么涉及到devops就难免涉及到持续集成,涉及到持续集成就难免涉及到rsync这个好用的命令。就这么一个命令,支撑起了那么多重要代码的传输也是神奇。

但是rsync有个问题在于,如果你的目录是动态生成的(比如根据分支名),那么极有可能你的目标机器上没有这样一个目录。这个时候rsync命令就会报错。

其实rsync是有参数来解决这个问题的,但是比较难用,那就是--include-from
这个东西一般是和--exclude-from一起用的,所以甚至需要专门读取一个配置文件,+代表include,-代表exclude。

在搜索了一大堆令人一头雾水的stackoverflow问题后,我决定还是放弃这个想法。

现在说下背景,之所以会有动态rsync路径的问题,是因为自己希望能够根据git push每次产生的对比文件列表来增量地rsync构建后的文件,这样就能大大降低CI的时间。(因为历史原因,我们的项目非常、非常庞大,如果全量rsync构建后的文件,时间可以长达1000s)

而构建后的路径,是按照git分支名区分的,目标机器(测试机)不一定有对应的目录,比如你开了一个新分支这种情况,目标机器没有对应目录,导致增量rsync单独文件的时候报错。

为了解决这个问题我甚至想过在测试机维护一个node服务,通过监听git的webhook来动态创建目录,但是在研究了那台机器的linux环境我果断选择了放弃,这台机器不仅外网不通,安装全靠手动上传,连vim、ssh的配置都被魔改过,我实在不想碰这个机器。

结果一位老程序员对我的提醒如醍醐灌顶:

“你直接创建一个同样目录结构的上级目录,只复制需要增量rsync的文件,然后直接rsync这个目录就行了”

这简直有种从更高维看事情的感觉,虽然很粗鲁,但是确实很香。

5. child_process的使用

前端出身的全干工程师,可能经常会写一些nodejs的脚本来代替shell脚本来做一些devops的事情,(事实上shell脚本确实不是很好用)。

但是毕竟nodejs也是基于调用系统底层接口来达到操作系统的目的,不可避免的经常需要用child_process来跑一些shell脚本,比如上面提到的rsync
当然,你可以通过像shelljs这种库来调用,但是…我这个项目吧,历史原因,历史原因。

举个例子,下面这个rsync的调用:

const child = require('child_process')
child.execSync(`rsync -avr ${src} root@8.8.8.8::${dist}`)

如果像这样调用的话,你会发现rsync的输出并没有到console中,这是因为如果不指定输出的参数,child_process不会把结果输出到console中。

可以这样指定输出参数:

child.execSync(`rsync -acvzK --exclude=".svn" ./server/proto/  root@8.8.8.8::/${BRANCH_NAME}/server/proto/`, {
stdio: [null, process.stdout, process.stderr]
})

这样的话,输出就会到console中。

6. docker 的多分支测试环境调试(这里可能之后单独分一篇文章)

经过半年的努力,终于把项目迁移到git + docker的分支部署测试环境模式,不管怎么说,对我来说,比原来svn的开发模式舒服多了。

但是目前分支实在太多(50+),每个微服务也都是共用这个母机的,所以做了多个母机来承载不同分支的测试用docker.

进入docker的命令是

docker exec -it yourdocker su - root

由于某些项目采用了docker-compose,当你重启mac的时候发现mac开机特别慢(不要问我为什么会重启,你敢相信互联网公司会不时断电),很有可能是因为自动启动了docker以及里面所有的容器。重启docker倒是没什么关系啦,但是所有容器一起启动就有点…你可以通过docker ps来查看当前正在运行的容器

# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1f3b771d1b6d mongo "docker-entrypoint.s…" 7 weeks ago Up 2 days 0.0.0.0:27017->27017/tcp mongo_1
fe890bc157de node-base "/bin/proxy/star…" 2 months ago Up 2 days 80/tcp, 0.0.0.0:8080->8080/tcp master_1
1ca45aba6b61 mysql:5.6 "docker-entrypoint.s…" 2 months ago Up 2 days 0.0.0.0:3306->3306/tcp mysql_1
af06d04efce6 memcached "docker-entrypoint.s…" 2 months ago Up 2 days 0.0.0.0:11211->11211/tcp memcached_1
f23c21999d4d runner "sh ./start" 2 months ago Up 2 days 0.0.0.0:8087-8088->8087-8088/tcp runner_1

你瞧,一重启mac,这些mongodb、mysql、memcached全都重启,那可真是要命。

所以可以通过docker update -restart=no CONTAINER-ID来把这些容器的自动重启关掉。

参考这个链接:https://stackoverflow.com/questions/37599128/docker-how-do-you-disable-auto-restart-on-a-container