shell命令使用数组循环

shell 数组循环的用法,对于一些构建工具很有帮助

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 #!/bin/bash

#定义方法一 数组定义为空格分割
array=('a' 'b' 'c' 'd' 'e')

#定义方法二
arrayIndex[0]=1
arrayIndex[1]=2
arrayIndex[2]=3
arrayIndex[3]=4
arrayIndex[4]=5

#修改数组值
array[0]='f'
arrayIndex[1]=6

#打印数组长度
echo ${#arrayIndex[@]}

#for 遍历数组
for var in ${arrayIndex[@]}
do
echo $var
done

#while 遍历数组
i=0
while [[ i -lt ${#arrayIndex[@]} ]];do
echo ${arrayIndex[i]}
let i++
done

使用shell做服务器的健康检查脚本#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# health.sh

# 这个地方配置多个要进行检查的URL,一般的,这些URL都是来自于某个系统的
check_urls=(
'https://wiki.enbrands.com/display/IMX/hello'
'https://wiki.enbrands.com/display/IMX/hello'
)

for url in ${check_urls[@]}
do
curl url
# 进行请求,如果请求失败则直接将程序标记为失败
done

玩转钉钉机器人

写在前面#

高效的团队往往会充分利用不同的工具来提升自己的工作效率,譬如通过钉钉进行沟通协同,使用 Trello 进行任务管理,使用 GitLab 或者 GitHub 来进行代码管理,使用 JIRA 来进行项目与事务跟踪等等。不同的工具分工合作,把团队的事务数字化管理流转起来,并在一定程度上实现了流程的自动化,将大家从一些繁琐的事务中解放出来,有效地提升了大家的协同能力和工作质量。

本文来讨论一下利用 node 接入机器人来实现一些日常办公小功能的自动化。

什么是群机器人?群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。

首先来看一下案例:

博客机器人,cicd 触发时将新文章推送到群
博客机器人

bug 提醒,每天定时推送现有 bug 到群
bug机器人

如何接入自己的钉钉机器人?#

首先打开钉钉群右上角,点群设置。找到智能群助手,点击添加机器人。
添加机器人

这里要选择自定义机器人,安全策略要选择加签。
安全策略加签

这里我们拿到密钥,点击继续,拿到机器人的 webhook。
拿到webhook

关于签名的算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const crypto = require('crypto');

// webhook中access_token
const token = 'e5f004c907173f8188eca8696f6fa3d3a841e4174733bae334057e3e769e32ae';

// 时间戳
const timestamp = +new Date();

// 上一步拿到的密钥
const secret = 'SEC32f1a057885884cadf5eea01e713e54d4b97bccd85bcc86187ac735dba26b7c7';
const stringToSign = timestamp + '\n' + secret;

// 用 SHA256 算法,用密钥生成
const hmac = crypto.createHmac('sha256', secret);

// 用时间戳加密钥生成签名
hmac.update(stringToSign, 'utf-8');

// 用 base64 编码再 encoding
const sign = encodeURIComponent(hmac.digest('base64'));

// 将得到的签名,时间戳放进query部分
const url = `https://oapi.dingtalk.com/robot/send?access_token=${token}&timestamp=${timestamp}&sign=${sign}`;

本质上是我们向钉钉的 openAPI 发送一个 post 请求。只要我们拿到正确的 url 和请求格式,就可以向机器人发送消息。这样就完成了钉钉机器人的接入。
消息类型及数据格式

1
2
3
4
5
6
7
8
9
10
11
12
const axios = require('axios');

axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8';
try {
axios.post(url, msg).then(function (response) {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
});
} catch (error) {
console.error(error);
}

Example#

blog 机器人#

在 cicd 中配置在build的时候执行脚本,找出新增文章推送到群。
主要问题在于如何找出新的文章。
在构建时,会临时生成 db.json 文件,这个文件不会被push,部署时会被删除。我们可以每次构建时保存一份文章列表,一起推送到git上面去。这样我们每次构建完成后,部署之前,先读取旧的文章名单,然后再读取 db.json,对比找出新的文章。
每篇文章我们要生成摘要,方法是:找到第一个 markdown## 标记,将标题后面的文章的第一句话当作摘要。所以希望大家尽量要写 ## 标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 旧的文章list
const onlineList = JSON.parse(JSON.stringify(oldList)).map(value => value = value.title);

// 生成文章list
const list = [];
const reg = new RegExp(`((#)?(##)?(###)? ([\u4e00-\u9fa5A-Za-z0-9^%&',;=??$\/x22。,,.()()、]{0,})?\\n)(\\n)?([\u4e00-\u9fa5A-Za-z0-9^%&',;=?$\/x22,,.()()、 ]{0,})`);
fs.readFile('./db.json', (err, data) => {
if (err) throw err;
JSON.parse(data).models.Post.forEach(element => {
let abstract = reg.exec(element._content)[0];
abstract = abstract.split(`\n`);
abstract = abstract[abstract.length - 1] ? `${abstract.pop()}...` : '暂无摘要';
let url = element.source;
url = url.replace('_posts','')
list.push({
title: element.title,
abstract: abstract,
})
});

fs.writeFile('./article.json', JSON.stringify(list), () => { });

// 对比找出新增文章
let newArticle;
for (i of list) {
if (!onlineList.includes(i.title)) {
newArticle = i;
break;
}
}
}

禅道bug机器人#

目的是实现从禅道拉去 bug 清单,过滤出特定人员,发送到指定群。未来todo要实现能单独@机器人,他会把属于你的bug发给你。
难点在于如何从禅道拉取 bug 清单。禅道不是 Restful 的,他是用后端模板渲染的 html 返回的方式。要解决的问题是,模拟登录和拉取数据。
这里采用的方案是使用 puppeteer 包,作用是生成一个无头的浏览器,这样就可以操作DOM元素了。
puppeteer文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  // 模拟登录禅道
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://zentao.jifenn.com/pro/bug-browse-10--unclosed-0--130-500-1.html');

await new Promise(resolve => setTimeout(resolve, 800));

await page.waitForSelector('#account');
await page.waitForSelector('#submit');

await (await page.$('#account')).type(config.username);
await (await page.$("input[name='password']")).type(config.password);
await (await page.$('#submit')).click();

await browser.close();
```

需要注意的是,代码执行的时候,dom树可能还没渲染,所以要记得await某个元素。
编辑消息的时候,为了简化操作,可以利用 `ejs` 配置模板。有点像 `jsp` 的语法

```jsp
![screenshot](http://enbrands-2.oss-cn-shanghai.aliyuncs.com/user/lALPDefR0Z0O7TnM1c0CuQ_697_2134.png)

当前bug数量: **`<%= ourBugs.length %>`**

<% for(let i of ourBugs ){ %> 1. [[<%=i.id %>] <%= i.title %>](<%='https://zentao.jifenn.com'+i.url %>) - <%=i.assignedTo %>
<% } %>

最后#

希望这篇文章能给大家带来启发,不仅仅是机器人,大家都能搭建适合自己的自动化脚本。社会发展就是机器取代人,简单重复的工作被取代。而我们人最重要的一点,就是机器没有的思维。发动我们聪明绝顶的脑袋想一想,集思广益,助飞效能提升。

如何使用ngx

写在前面#

这篇文章写给一个姓港的同学,希望他能学会基本的ngx使用,然后再去实现自己的想法

准备容器#

启动 ngx 并关联 80 端口

1
docker run -d -p 80:80 --name nginx nginx:latest

进入容器,这个控制台留着刷新NGX配置使用

1
docker exec -it nginx bash

修改配置文件#

20210428132825

查询 docker ngx 的文档可知,配置文件在 /etc/nginx/conf.d/default.conf, 新增一个控制台,将docker里面的ngx配置文件拷贝出来进行调整

1
docker cp nginx:/etc/nginx/conf.d/default.conf ./default.conf

进行ngx的配置修改,然后将文件拷贝回去(下面这段配置是简单地一个代理)

1
2
3
4
5
6
7
8
9
server {
listen 80;
listen [::]:80;
server_name localhost;

location /{
proxy_pass http://enbrands-2.oss-cn-shanghai.aliyuncs.com/;
}
}

重新加载ngx并进行预览#

进行拷贝

1
docker cp  ./default.conf nginx:/etc/nginx/conf.d/default.conf

在容器里面执行命令

1
2
3
4
5
6

# 可以通过命令查看修改情况
more /etc/nginx/conf.d/default.conf

# 重启ngx
nginx -s reload

可以通过 http://localhost/user/d3a1a93e2d572937d7708b55d660bf46.png 查看效果

可以测试一下下列两个图片的展示情况

20210428134735

  • 如果要修改端口映射,启动阶段的时候 -p 8080:80, 则需要用 localhost:8080 进行访问

如何快速截图至md文件

描述#

在写md文件的时候,经常会用到图片素材,但是又需要自行上传生成链接等一系列繁琐操作,这里介绍一款vscode插件能够帮助我们一键实现截图到生生成链接的便捷操作,具体配置如下

效果展示#

配置#

1. 在插件应用商店搜索 aliyun-oss-paste-image ,点击 install 安装#

2. 插件相关使用方法及配置#

20210513140703
(备注:localTempPath这里需要指定一个已存在的文件路径,使用默认配置需要手动去c盘创建一个/tmp/.aliyun-oss-paste-image这样的文件夹路径)

3. 配置参考#

20210513140420

如何快速提审淘宝小程序

描述#

taobaodev 一款基于命令行的淘宝小程序研发工具。taobaodev提供了便捷的研发功能,使用自己喜欢的IDE + 浏览器就可以完成小程序的研发调试,预览,上传等操作。

配置#

1. npm install taobaodev -g#

2. taobaodev login#

  • 由于命令行借助浏览器的相关能力,所以需要本机安装chrome,如果chrome不在默认安装路径上,需要进行指定。

    20210524142656

1
2
3
4
5

// mac
taobaodev config set PUPPETEER_PATH /Applications/Google Chrome.app/Contents/MacOS/Google\ Chrome
// window 指定到chrome安装的路径
taobaodev config set PUPPETEER_PATH "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"

上传#

taobaodev upload –appId=3000000006501031

【深入探究Node】(3)“异步IO” 有九问

转载自 https://mp.weixin.qq.com/s/s5KrA8p-vgamKKmRjchgig

1.为什么要异步I/O?#

具体到实处,则可以从用户体验和资源分配这两个方面说起。

用户体验#

与前端JavaScript在单线程上执行,而且它还与UI渲染共用一个线程一样。JavaScript在执行的时候UI渲染和响应是处于停滞状态的。那么,在node中,假设此时不使用异步io,那么当一个io在执行的时候,另一个io的执行必须等待前一个io执行完毕才可以。那么速度就会慢很多,需要认识到只有后端能够快速响应资源,才能让前端的体验变好

资源分配#

我们首先需要知道计算机在发展过程中将组件进行了抽象,分为I/O设备和计算设备。

如果创建多线程的开销小于并行执行,那么多线程的方式是首选的。多线程的代价在于创建线程和执行期线程上下文切换的开销较大。另外,在复杂的业务中,多线程编程经常面临锁、状态同步等问题,这是多线程被诟病的主要原因。但是多线程在多核CPU上能够有效提升CPU的利用率,这个优势是毋庸置疑的。

单线程顺序执行任务的方式比较符合编程人员按顺序思考的思维方式。它依然是最主流的编程方式,因为它易于表达。但是串行执行的缺点在于性能,任意一个略慢的任务都会导致后续执行代码被阻塞。在计算机资源中,通常I/O与CPU计算之间是可以并行进行的。但是同步的编程模型导致的问题是,I/O的进行会让后续任务等待,这造成资源不能被更好地利用。

单线程同步编程模型会因阻塞I/O导致硬件资源得不到更优的使用。多线程编程模型也因为编程中的死锁、状态同步等问题让开发人员头疼。

Node在两者之间给出了它的方案:利用单线程,远离多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好地使用CPU。

异步I/O可以算作Node的特色,因为它是首个大规模将异步I/O应用在应用层上的平台,它力求在单线程上将资源分配得更高效。为了弥补单线程无法利用多核CPU的缺点,Node提供了类似前端浏览器中WebWorkers的子进程,该子进程可以通过工作进程高效地利用CPU和I/O。

异步I/O的提出是期望I/O的调用不再阻塞后续运算,将原有等待I/O完成的这段时间分配给其余需要的业务去执行。

下图为异步I/O的调用示意图。

20210621113030

2.说到异步IO,我也经常听到非阻塞IO,这两者是一个东西吗?#

异步与非阻塞听起来似乎是同一回事。从实际效果而言,异步和非阻塞都达到了我们并行I/O的目的。但是从计算机内核I/O而言,异步/同步和阻塞/非阻塞实际上是两回事。

操作系统内核对于I/O只有两种方式:阻塞与非阻塞。在调用阻塞I/O时,应用程序需要等待I/O完成才返回结果,如图所示。

20210621113059

阻塞I/O的一个特点是调用之后一定要等到系统内核层面完成所有操作后,调用才结束。以读取磁盘上的一段文件为例,系统内核在完成磁盘寻道、读取数据、复制数据到内存中之后,这个调用才结束。

阻塞I/O造成CPU等待I/O,浪费等待时间,CPU的处理能力不能得到充分利用。为了提高性能,内核提供了非阻塞I/O。非阻塞I/O跟阻塞I/O的差别为调用之后会立即返回,如图所示。

20210621113226

这个让我想起直接打印状态为pending的promise对象,也是可以打印出来的,这个就是异步吧,虽然状态还没变为resolved或者rejected,也一样返回了。

非阻塞I/O返回之后,CPU的时间片可以用来处理其他事务,此时的性能提升是明显的。

3.这样的话根本无法返回完整的数据,怎么办?#

层期望的数据,而仅仅是当前调用的状态。为了获取完整的数据,应用程序需要重复调用I/O操作来确认是否完成。这种重复调用判断操作是否完成的技术叫做轮询。

4.可以说下什么是轮询技术吗?#

任意技术都并非完美的。阻塞I/O造成CPU等待浪费,非阻塞带来的麻烦却是需要轮询去确认是否完全完成数据获取,它会让CPU处理状态判断,是对CPU资源的浪费。

轮询技术主要包括这几种:read、select、poll、epoll

read#

它是最原始、性能最低的一种,通过重复调用来检查I/O的状态来完成完整数据的读取。在得到最终数据前,CPU一直耗用在等待上。下图为通过read进行轮询的示意图。

20210621113408

select。
它是在read的基础上改进的一种方案,通过对文件描述符上的事件状态来进行判断。下图为通过select进行轮询的示意图。

20210621113419

select轮询具有一个较弱的限制,那就是由于它采用一个1024长度的数组来存储状态,所以它最多可以同时检查1024个文件描述符。

poll。
该方案较select有所改进,采用链表的方式避免数组长度的限制,其次它能避免不需要的检查。但是当文件描述符较多的时候,它的性能还是十分低下的。下图为通过poll实现轮询的示意图,它与select相似,但性能限制有所改善。

20210621113435

epoll。
该方案是Linux下效率最高的I/O事件通知机制,在进入轮询的时候如果没有检查到I/O事件,将会进行休眠,直到事件发生将它唤醒。它是真实利用了事件通知、执行回调的方式,而不是遍历查询,所以不会浪费CPU,执行效率较高。下图为通过epoll方式实现轮询的示意图。

20210621113449

轮询技术满足了非阻塞I/O确保获取完整数据的需求,但是对于应用程序而言,它仍然只能算是一种同步,因为应用程序仍然需要等待I/O完全返回,依旧花费了很多时间来等待。等待期间,CPU要么用于遍历文件描述符的状态,要么用于休眠等待事件发生。结论是它不够好

5.尽管epoll已经利用了事件来降低CPU的耗用,但是休眠期间CPU几乎是闲置的,对于当前线程而言利用率不够。那么,是否有一种理想的异步I/O呢?#

有啊。

我们期望的完美的异步I/O应该是应用程序发起非阻塞调用,无须通过遍历或者事件唤醒等方式轮询,可以直接处理下一个任务,只需在I/O完成后通过信号或回调将数据传递给应用程序即可

20210621113543

幸运的是,在Linux下存在这样一种方式,它原生提供的一种异步I/O方式(AIO)就是通过信号或回调来传递数据的。

但不幸的是,只有Linux下有,而且它还有缺陷——AIO仅支持内核I/O中的O_DIRECT方式读取,导致无法利用系统缓存。

6.现实的异步I/O是怎么实现的?#

现实比理想要骨感一些,但是要达成异步I/O的目标,并非难事。前面我们将场景限定在了单线程的状况下,多线程的方式会是另一番风景。通过让部分线程进行阻塞I/O或者非阻塞I/O加轮询技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信将I/O得到的数据进行传递,这就轻松实现了异步I/O(尽管它是模拟的),示意图如图。

20210621113558

另一个需要强调的地方在于我们时常提到Node是单线程的,这里的单线程仅仅只是JavaScript执行在单线程中罢了。在Node中,无论是*nix还是Windows平台,内部完成I/O任务的另有线程池。

7.以上是系统对异步IO的实现,那node中是怎么实现异步IO的#

完成整个异步I/O环节的有事件循环观察者线程池请求对象等。

事件循环#

首先,我们着重强调一下Node自身的执行模型——事件循环,正是它使得回调函数十分普遍。

在进程启动时,Node便会创建一个类似于while(true)的循环,每执行一次循环体的过程我们称为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行它们。然后进入下个循环,如果不再有事件处理,就退出进程。流程图如图

20210621113646

观察者#

在每个Tick的过程中,如何判断是否有事件需要处理呢?这里必须要引入的概念是观察者。每个事件循环中有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。

这个过程就如同饭馆的厨房,厨房一轮一轮地制作菜肴,但是要具体制作哪些菜肴取决于收银台收到的客人的下单。厨房每做完一轮菜肴,就去问收银台的小妹,接下来有没有要做的菜,如果没有的话,就下班打烊了。

在这个过程中,收银台的小妹就是观察者,她收到的客人点单就是关联的回调函数。当然,如果饭馆经营有方,它可能有多个收银员,就如同事件循环中有多个观察者一样。收到下单就是一个事件,一个观察者里可能有多个事件。

浏览器采用了类似的机制。事件可能来自用户的点击或者加载某些文件时产生,而这些产生的事件都有对应的观察者。在Node中,事件主要来源于网络请求、文件I/O等,这些事件对应的观察者有文件I/O观察者、网络I/O观察者等。观察者将事件进行了分类。

事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者,源源不断为Node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。

请求对象
我们可以先看这张图,大致了解一下node中异步io的实现,然后在看下面的分析。

20210621113719

由于下面的讲解中会引用到内部的一些方法,要记住这些方法是很困难的,所以我建议不必深究这些方法是怎么写的,只要能够弄清楚这张图的流程就好

我们将通过解释Windows下异步I/O(利用IOCP实现)的简单例子来探寻从JavaScript代码到系统内核之间都发生了什么。对于一般的(非异步)回调函数,函数由我们自行调用,如下所示:

1
2
3
4
5
var forEach = function (list, callback) {
for (var i = 0; i < list.length; i++) {
callback(list[i], i, list);
}
};

对于Node中的异步I/O调用而言,回调函数却不由开发者来调用。那么从我们发出调用后,到回调函数被执行,中间发生了什么呢?事实上,从JavaScript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,它叫做请求对象。

下面我们以最简单的fs.open()方法来作为例子,探索Node与底层之间是如何执行异步I/O调用以及回调函数究竟是如何被调用执行的:

1
2
3
4
5
6
7
fs.open = function (path, flags, mode, callback) {
// ...
binding.open(pathModule._makeLong(path),
stringToFlags(flags),
mode,
callback);
};

fs.open()的作用是根据指定路径和参数去打开一个文件,从而得到一个文件描述符,这是后续所有I/O操作的初始操作。从前面的代码中可以看到,JavaScript层面的代码通过调用C++核心模块进行下层的操作。

20210621113824

从JavaScript调用Node的核心模块,核心模块调用C++内建模块,内建模块通过libuv进行系统调用,这是Node里经典的调用方式。这里libuv作为封装层,有两个平台的实现,实质上是调用了uv_fs_open()方法。在uv_fs_open()的调用过程中,我们创建了一个FSReqWrap请求对象。从JavaScript层传入的参数和当前方法都被封装在这个请求对象中,其中我们最为关注的回调函数则被设置在这个对象的oncomplete_sym属性上:

1
req_wrap->object_->Set(oncomplete_sym, callback);

对象包装完毕后,在Windows下,则调用QueueUserWorkItem()方法将这个FSReqWrap对象推入线程池中等待执行,该方法的代码如下所示:

1
QueueUserWorkItem(& uv_fs_thread_proc, req, WT_EXECUTEDEFAULT)

QueueUserWorkItem()方法接受3个参数:第一个参数是将要执行的方法的引用,这里引用的是uv_fs_thread_proc,第二个参数是uv_fs_thread_proc方法运行时所需要的参数;第三个参数是执行的标志。当线程池中有可用线程时,我们会调用uv_fs_thread_proc()方法。

uv_fs_thread_proc()方法会根据传入参数的类型调用相应的底层函数。以uv_fs_open()为例,实际上调用fs__open()方法。

至此,JavaScript调用立即返回,由JavaScript层面发起的异步调用的第一阶段就此结束。JavaScript线程可以继续执行当前任务的后续操作。当前的I/O操作在线程池中等待执行,不管它是否阻塞I/O,都不会影响到JavaScript线程的后续执行,如此就达到了异步的目的。

请求对象是异步I/O过程中的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及I/O操作完毕后的回调处理。

8.So嘎,上面讲得是异步方法的调用,也就是fs.open这个方法的调用,那后面的io操作以及回调函数的执行呢?#

简单的回答就是:调用fs.open这个方法之后就会获得一个io读取操作,然后把这个操作放入到线程池,等待有空的线程来执行io的读取操作,然后得到结果,将数据传递给回调函数,再执行,再执行回调。

如下图所示。

20210621113910

下面是详细讲解:

组装好请求对象、送入I/O线程池等待执行,实际上完成了异步I/O的第一部分,回调通知是第二部分。

线程池中的I/O操作调用完毕之后,会将获取的结果储存在req->result属性上,然后调用PostQueuedCompletionStatus()通知IOCP,告知当前对象操作已经完成:

1
PostQueuedCompletionStatus((loop)->iocp, 0, 0, &((req)->overlapped))

PostQueuedCompletionStatus()方法的作用是向IOCP提交执行状态,并将线程归还线程池。通过PostQueuedCompletionStatus()方法提交的状态,可以通过GetQueuedCompletionStatus()提取。

在这个过程中,我们其实还动用了事件循环的I/O观察者。在每次Tick的执行中,它会调用IOCP相关的GetQueuedCompletionStatus()方法检查线程池中是否有执行完的请求,如果存在,会将请求对象加入到I/O观察者的队列中,然后将其当做事件处理。

I/O观察者回调函数的行为就是取出请求对象的result属性作为参数,取出oncomplete_sym属性作为方法,然后调用执行,以此达到调用JavaScript中传入的回调函数的目的。至此,整个异步I/O的流程完全结束。

事件循环、观察者、请求对象、I/O线程池这四者共同构成了Node异步I/O模型的基本要素。

9.setTimeout()、setInterval()、setImmediate()和process.nextTick()也是异步IO吗?#

并不是,这些是异步API。

这一部分也值得略微关注一下。

定时器#

setTimeout()和setInterval()与浏览器中的API是一致的,分别用于单次和多次定时执行任务。它们的实现原理与异步I/O比较类似,只是不需要I/O线程池的参与。调用setTimeout()或者setInterval()创建的定时器会被插入到定时器观察者内部的一个红黑树中。每次Tick执行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,如果超过,就形成一个事件,它的回调函数将立即执行。

20210621114005

定时器的问题在于,它并非精确的(在容忍范围内)。尽管事件循环十分快,但是如果某一次循环占用的时间较多,那么下次循环时,它也许已经超时很久了。譬如通过setTimeout()设定一个任务在10毫秒后执行,但是在9毫秒后,有一个任务占用了5毫秒的CPU时间片,再次轮到定时器执行时,时间就已经过期4毫秒。

process.nextTick()#

在未了解process.nextTick()之前,很多人也许为了立即异步执行一个任务,会这样调用setTimeout()来达到所需的效果:

1
2
3
setTimeout(function () {
// TODO
}, 0);

由于事件循环自身的特点,定时器的精确度不够。而事实上,采用定时器需要动用红黑树,创建定时器对象和迭代等操作,而setTimeout(fn, 0)的方式较为浪费性能。实际上,process.nextTick()方法的操作相对较为轻量,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
process.nextTick = function (callback) {
// on the way out, don't bother.
// it won't get fired anyway
if (process._exiting) return;

if (tickDepth >= process.maxTickDepth)
maxTickWarn();

var tock = { callback: callback };
if (process.domain) tock.domain = process.domain;
nextTickQueue.push(tock);
if (nextTickQueue.length) {
process._needTickCallback();
}
};

每次调用process.nextTick()方法,只会将回调函数放入队列中,在下一轮Tick时取出执行。定时器中采用红黑树的操作时间复杂度为O(lg(n)), nextTick()的时间复杂度为O(1)。相较之下,process.nextTick()更高效。

setImmediate()#

setImmediate()方法与process.nextTick()方法十分类似,都是将回调函数延迟执行。在Node v0.9.1之前,setImmediate()还没有实现,那时候实现类似的功能主要是通过process.nextTick()来完成,该方法的代码如下所示:

1
2
3
4
process.nextTick(function () {
console.log(’延迟执行’);
});
console.log(’正常执行’);

上述代码的输出结果如下:

20210621114145

而用setImmediate()实现时,相关代码如下:

1
2
3
4
setImmediate(function () {
console.log(’延迟执行’);
});
console.log(’正常执行’);

其结果完全一样:

20210621114212

但是两者之间其实是有细微差别的。将它们放在一起时,又会是怎样的优先级呢。示例代码如下:

1
2
3
4
5
6
7
process.nextTick(function () {
console.log('nextTick延迟执行’);
});
setImmediate(function () {
console.log('setImmediate延迟执行’);
});
console.log(’正常执行’);

其执行结果如下:

20210621114237

从结果里可以看到,process.nextTick()中的回调函数执行的优先级要高于setImmediate()。这里的原因在于事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者。在每一个轮循环检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者。

在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。在行为上,process.nextTick()在每轮循环中会将数组中的回调函数全部执行完,而setImmediate()在每轮循环中执行链表中的一个回调函数。如下的示例代码可以佐证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 加入两个nextTick()的回调函数
process.nextTick(function () {
console.log('nextTick延迟执行1');
});
process.nextTick(function () {
console.log('nextTick延迟执行2');
});
// 加入两个setImmediate()的回调函数
setImmediate(function () {
console.log('setImmediate延迟执行1');
// 进入下次循环
process.nextTick(function () {
console.log(’强势插入’);
});
});
setImmediate(function () {
console.log('setImmediate延迟执行2');
});
console.log(’正常执行’);

其执行结果如下:

20210621114322

从执行结果上可以看出,当第一个setImmediate()的回调函数执行后,并没有立即执行第二个,而是进入了下一轮循环,再次按process.nextTick()优先、setImmediate()次后的顺序执行。之所以这样设计,是为了保证每轮循环能够较快地执行结束,防止CPU占用过多而阻塞后续I/O调用的情况。

使用Puppeteer实现自动化

什么是 Puppeteer#

Puppeteer 是一个 Node 库,它提供了高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。

Puppeteer 翻译过来是傀儡师,操纵提线木偶的人。这里指你可以通过代码操作浏览器,就像用线操纵木偶一样。👉Puppeteer 中文文档

我们可以利用 Puppeteer 实现日常工作自动化,将一些机械性的浏览器操作交给 Puppeteer。

操作浏览器#

通过 puppeteer.launch 来创建一个浏览器实例。
browser.newPage 来创建一个新页面。
页面操作可以通过 page.goto 跳转到指定页面。
page.evaluate 在浏览器环境中执行js脚本。
browser.close 关闭浏览器。

1
2
3
4
5
6
7
8
9
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
const data = await page.evaluate(() => {
// todo
return ;
});
browser.close();

可能有用的小提示#

  • puppeteer 提供的 API 大多数都是 Promise ,执行的时候注意不要异步操作,同时避免 await 某个操作导致卡死。
  • page.waitFor 可以等待某个 dom元素/函数/请求 但是 await 可能很长导致超出30000ms,Puppeteer 抛出异常。
  • 可以用 page.setJavaScriptEnabled(false) 禁用加载js,以节省加载时间。适用于后台js很多的情况,比如线上 IDE 。但是禁用了以后 evaluate 只能用原生 JavaScript 了。
  • 可以用 puppeteer.devices 模拟手机访问。参数是数组,例如模拟 iPhone 6
    1
    const iPhone = puppeteer.devices['iPhone 6'];

在gitlab中玩转cicd

什么是CI/CD#

持续集成(CI)和持续部署(CD)是DevOps中重要的两个环节,传统的软件开发和交付方式在迅速变得过时。过去的敏捷时代里,大多数公司的软件发布周期是每月、每季度甚至每年,而在现在 DevOps 时代,每周、每天甚至每天多次都是常态。

开发团队通过软件交付流水线(Pipeline)实现自动化,以缩短交付周期,并通过自动化流程来检查代码并部署到新环境。以快速的进行敏捷迭代和开发。

在实际使用中,gitlab的CI/CD是通过开发者预先配置的一系列pipeline参数,通过精心配置的触发时机,在代码提交、合并、或者打tag时,触发自动构建流来完成构建到发布的动作。

任务目标#

20210330104247

我们的任务目标是搭建一个 gitlab + gitlab-runner 的CICD环境,在代码触发时,启动构建动作,构建完毕后将代码推送到应用服务器上进行部署

应用服务是是一个具备nginx的服务,他暴露了80端口允许你访问端口,应用我们选择前端的 hexo 博客系统,开箱即用

准备#

为了实现本文档的目标任务,需要做一下软件的前期准备

  • 安装docker
  • 了解docker常用操作
  • apt 软件安装操作

    相关材料#

  1. docker安装gitlab
  2. docker四种网络模式,容器localhost访问宿主机端口
  3. linux 安装 nodejs
  4. linux软连接
  5. 安装 gilab-runner
  6. Job artifacts
  7. Job artifacts

环境准备#

从安装一个 gitlab 开始#

首先我们在docker安装一个gitlab用来做本次实验

1
2
3
4
5
6
7
8
9
10
11
12
# 下载镜像
docker pull gitlab/gitlab-ee

# 启动
docker run -d \
--hostname localhost \
-p 80:80 \
--name gitlab \
gitlab/gitlab-ee:latest

# 查看日志
docker logs -f gitlab

启动阶段要做较多的初始化工作,需要耐心等待。完成后可以通过 8080 端口看到gilab。

20210330151758

安装并注册 git runner#

安装好 gitlab 后还要安装 gitlan-runner 并注册,gitlab-runner 主要用于 响应 gitlab CI/CD,CI/CD里面的script脚本将会被 gitlab-runner 所执行

1
2
3
4
5
6
7
8
docker pull gitlab/gitlab-runner
docker run -d \
--name gitlab-runner \
--network host \ # 共享主机网络保证gitrunner能够正确的访问gitlab
gitlab/gitlab-runner:latest

# 命令模式进入容器
docker exec -it gitlab-runner bash

进入容器控制台,输入如下命令进行注册

1
gitlab-ci-multi-runner register

注册时需要一个token参数,可以访问 http://localhost/admin/runners 这个页面去获取

20210328171907

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> gitlab-ci-multi-runner register

Runtime platform arch=amd64 os=linux pid=63 revision=943fc252 version=13.7.0
Running in system-mode.

Enter the GitLab instance URL (for example, https://gitlab.com/):
> http://localhost/

Enter the registration token:
> Q1L_vwDETgx8Fx-Yypfp

Enter a description for the runner:
[23229cabbaf2]: demo
Enter tags for the runner (comma-separated):

Registering runner... succeeded runner=Q1L_vwDE
Enter an executor: docker-ssh, virtualbox, kubernetes, docker-ssh+machine, custom, docker, parallels, shell, ssh, docker+machine:
> shell
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!

值得注意的是URL填写的时候,不能使用 https://localhost/ 请使用本机IP, 配置完后页面刷新后你会看到一个新注册的 runner

20210330163620

然后点击编辑,将 lock 那一项给点掉

20210330163658

最后返回列表你会看到

20210330161142

为了能运行nodejs项目,还需要继续安装 nodejs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

apt update

# 安装解压工具
apt install xz-utils bzip2 -y

# 下载
wget https://nodejs.org/dist/v10.9.0/node-v10.9.0-linux-x64.tar.xz

# 解压wget
tar -xf node-v10.9.0-linux-x64.tar.xz

# 建立软连接,让npm和node可以在全局调用
ln -b -s /node-v10.9.0-linux-x64/bin/npm /bin/npm
ln -b -s /node-v10.9.0-linux-x64/bin/node /bin/node

# 查看node版本
node -v
  • exector 表示执行器,表示执行脚本时,使用的命令行程序,配置docker比较复杂,此处直接使用shell执行器

安装一台用于部署的服务器#

至此,我们还需要一台用于部署应用的服务器,由于需要使用ssh进行连接(gitlab-runner使用该端口做远程部署),我们使用 nginx 镜像, 并在上面安装一个 openssh-server,最后打开ssh通道,并配置账号密码允许ssh访问

首先,先pull nginx 镜像, 然后启动nginx

1
2
3
4
docker pull nginx
docker run -d --name nginx \
-p 8080:80 \
nginx:latest

启动完毕后,8080端口就可以直接访问了 http://localhost:8080/

搭建代码仓库和cicd#

新建一个仓库#

至此,runner的执行环境基本做完了,接下来我们需要新建一个代码仓库,然后配置CI/CD的相关内容。

我们需要先创建一个仓库

20210330141010

找到

20210330141031

新建

20210330141102
20210330141103

测试CI/CD#

模板默认已经配好了CICD,可以直接运行

配置cicd#

仓库建完后,在仓库目录下面有一个 .gitlab-ci.yml 文件,点开编辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
image: node:10.15.3

cache:
paths:
- node_modules/

before_script:
- test -e package.json && npm install

pages:
script:
- ./node_modules/hexo/bin/hexo generate
# scp需要进行进行ssh相关的配置才可以使用,未完成
# - scp -r public root@localhost:/usr/share/nginx/html
artifacts:
paths:
- public
only:
- master

保存完毕后,CI/CD就开始执行

20210331134531

发布程序#

cicd执行完毕后,由于我们配置了 artifacts 参数,可以在ci/cd面板中下载构建产物
20210331172338

我们可以直接在应用服务器上面下载这个产物 细节看此链接

1
2
3
4
5
6
docker exec -it nginx bash
# 到ngx的目录
cd /usr/share/nginx/
# 下载最后一个在master上面构建的包
curl --output artifacts.zip --header "PRIVATE-TOKEN: s8Y9J1g5Azof89zfhEhN" "http://192.168.0.157/api/v4/projects/root%2Fblog/jobs/artifacts/master/download?job=pages"

命令中的 Ip 请修改成自己的IP,不要使用localhost, root%2Fblog 是项目路径 root/blog encode之后的,PRIVATE-TOKEN 需要在仓库的 Setting -> access token 获得
20210331174424

下载完毕后可以使用 ls 查看

1
2
root@6aca3f15f8d8:/usr/share/nginx# ls
50x.html artifacts.zip index.html

然后我们将其解压

1
2
3
4
5
6
7
8

apt update # 更新软件源
apt install unzip # 安装解压软件

unzip artifacts.zip

rm -rf html # 删除旧文件
mv public html # 将目录移到ngx配置目录

20210331180401

就可以看到结果了(样式问题是工程自己的问题)

直接在cicd中发布#

todo

1
2
3
4
# 安装openssh以及文本编辑器
apt install openssh-server vim -y

vim /etc/ssh/sshd_config

修改里面 port 以及 PermitRootLogin

20210330135033

修改完毕后,配置root密码

1
passwd root

重新启动sshd

1
/etc/init.d/ssh restart

完毕后就可以治用 xshel等软件进行连接
20210330135009

一些有趣的cicd实践#

  • 使用CICD变量(todo)
  • 构建产物(todo)
  • 跨仓库触发/远程触发(todo)
  • 为不同的分支指定不一样的构建动作(todo)