Redis 字典与渐进式 rehash:为什么“慢慢搬”是合理的选择

如果你用过 Redis,大概率知道它“快”。但“快”从来不是偶然的。Redis 字典的渐进式 rehash,就是一个典型的例子:它为了不阻塞,主动选择了一种“看起来更慢”的搬迁方式。

这篇文章不贴源码,而是回答几个具体问题:一次性 rehash 到底会阻塞多久?迁移过程中增删改查怎么处理?为什么是“每次搬一点”,而不是“分批定时搬”?以及——这个设计牺牲了什么。


一、字典的基本结构:两个哈希表

Redis 的字典 dict 内部有两个哈希表:ht[0] 和 ht[1]。

正常情况下,数据都在 ht[0] 里,ht[1] 是空的。只有当需要扩容或缩容时,ht[1] 才会被分配空间,然后数据从 ht[0] 逐渐迁移到 ht[1]。

为什么要两个表?

因为 Redis 是单线程的。如果只用一张表,扩容时你得原地把所有元素重新计算位置——这在单线程下意味着什么都干不了。两张表 + 一个 rehashidx 索引,让迁移可以“边干活边搬”。

每个哈希表底层是数组 + 链表,用链地址法解决冲突。dictEntry 就是链表节点,同一个桶里的元素用 next 指针串起来。


二、一次性 rehash 到底会阻塞多久?

这是核心问题。要回答它,先看数据量。

假设:一个字典有 1 亿个键值对。

关键数字:Redis 迁移一个键值对的典型耗时,保守估计在 100 纳秒左右。

那么:

1
1 亿 × 100 ns = 10,000,000,000 ns = 10 秒

10 秒。 在这 10 秒里,Redis 的主线程完全被占住。所有客户端请求全部排队等待。 对于一个标榜 P99 延迟在毫秒级的服务来说,这是灾难性的。

更极端的场景:如果 value 是大 key(比如一个几百 MB 的 List),单次迁移的耗时可能以秒为单位。有测试数据显示,迁移一个 600MB 的 list 类型 key 需要 6.7 秒。这还只是一个 key。

所以答案很明确:一次性 rehash 的阻塞时间与数据量成正比,在百万级以上的字典中,阻塞时间会达到秒级甚至更长。 对 Redis 来说,这等于服务中断。


三、渐进式 rehash 到底“渐进”在哪里?

Redis 的解法是:不一次性搬完,而是把搬迁工作分摊到每一次操作里。

具体机制:

  1. 扩容时,为 ht[1] 分配新空间(通常是 ht[0].used * 2 向上取最近的 2 的幂);
  2. 把 rehashidx 设为 0,表示 rehash 正式开始;
  3. 每次对字典执行增删改查时,顺带把 ht[0] 中 rehashidx 位置上的整个桶(链表上所有节点)迁移到 ht[1];
  4. 迁移完成后,rehashidx 加一,指向下一个桶;
  5. 当所有桶都迁移完毕,把 ht[1] 设为新的 ht[0],清空旧表,rehashidx 重置为 -1。

关键细节:一次迁移的是一个“桶”,不是“一个键值对”。因为一个桶可能是一个链表,上面挂着多个节点。但从实际数据看,由于负载因子控制在合理范围,平均每个桶通常只有 1 个节点,最多一般不超过 5 个。所以单次迁移的开销很小。


四、迁移过程中,增删改查怎么处理?

这是设计中最精妙的部分。两个表同时存在,但操作规则不同。

操作 规则
查找 先在 ht[0] 找,找不到再去 ht[1] 找
删除 两个表都要操作(因为 key 可能在任意一个表里)
更新 两个表都要操作(先找到,再更新)
新增 只往 ht[1] 里加,绝不往 ht[0] 加

新增只写 ht[1] 的原因很直接:保证 ht[0] 的节点数量只减不增。 如果新增还往旧表里写,那 ht[0] 永远清不空,rehash 永远结束不了。

这个规则带来的一个副作用是:迁移期间的查找可能比平时稍慢,因为最坏情况下要找两张表。但这是可接受的代价。


五、为什么是“每次操作顺带搬”,而不是“分批定时搬”?

Redis 其实两种都有。

  • 被动搬迁:每次增删改查时顺带迁移一个桶(_dictRehashStep);
  • 主动搬迁:服务器定时任务(databasesCron)中调用 dictRehashMilliseconds,在1 毫秒内尽可能多地迁移。

为什么以被动为主、主动为辅?

因为纯定时搬迁有一个问题:如果客户端一直不发请求,定时任务也不一定能保证及时搬完。 而且定时任务的执行时机依赖于 Redis 的事件循环,在高负载时可能被推迟。

被动搬迁的好处是:搬迁的开销天然地跟随负载分布。 请求越多,搬迁越快;请求越少,系统本身也不忙,晚一点搬完也无所谓。这是一种自适应的节奏。

主动搬迁则作为兜底:当客户端闲下来了,没有后续指令来触发被动搬迁时,Redis 不会“置之不理”,而是通过定时任务主动推进。dictRehashMilliseconds 每次只花 1ms 的 CPU 时间做搬迁,然后让出控制权。

所以准确地说:不是“不做定时”,而是“被动为主、定时为辅”。 两者结合,既保证了有请求时搬迁不拖后腿,也保证了没请求时不会永远卡在半途。


六、这个设计牺牲了什么,换来了什么?

牺牲的:

  1. 单次操作略慢。 迁移期间,每次增删改查都可能附带一次搬迁,或者需要查两张表。但开销被控制在单个桶的粒度,通常只有 1~5 个节点,对响应时间的影响很小。
  2. 内存占用翻倍。 rehash 期间两个哈希表同时存在,内存使用量会上升。这是“空间换时间”的典型体现。
  3. 实现复杂度上升。 所有字典操作都必须考虑“是否处于 rehash 状态”,代码路径分叉,更容易出 bug。

换来的:

  1. 不阻塞。 这是最核心的收益。无论字典多大,Redis 主线程永远不会因为 rehash 而停下来。
  2. 平滑的延迟曲线。 一次性 rehash 会导致 P99.9 和 max 延迟飙升;渐进式 rehash 把开销摊薄,让延迟保持稳定。
  3. 自适应节奏。 搬迁进度跟随负载,忙时搬得快,闲时搬得慢,不浪费 CPU。

楔子

在早些年开发 Node.js 项目的过程中,我使用过其子进程(child_process)模块的部分功能,当时需要在 Node.js 的程序中使用 Linux 下的一个命令来处理一些数据。比如实现一个使用 ls 命令来查看当前目录结构的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 这是 Node.js 官方版本 12.x 文档下提供的一个实现例子 [Child Process | Node.js v12.13.0 Documentation](https://nodejs.org/dist/latest-v12.x/docs/api/child_process.html#child_process_child_process_spawn_command_args_options)
const { spawn } = require(‘child_process’);
const ls = spawn(‘ls’, [‘-lh’, ‘/Users’]);

// 控制台正常输出
ls.stdout.on(‘data’, (data) => {
console.log(`stdout: ${data}`);
});

// 控制台异常输出
ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});

// 关闭
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});

事实上,我们可以通过以上方式,来实现任何与本地 Linux 服务交互的工作。任何 Linux 服务器下的系统命令,或者安装的第三方软件,均可以通过 Node.js 来使用。

达摩克利斯之剑

当时只是觉得这个功能好玩、实用,但是没有进一步深入研究。直到 周下载量过 200万的 npm 包被注入恶意代码 事件的发生,让我意识到 Node.js 的子进程功能还是一把达摩克利斯之剑。在安全防范较低或者没有的情况下,极其容易被恶意利用,而且后果不堪设想。

当然,该事件当中还藏着另外一把达摩克利斯之剑:JavaScript 下的 eval 函数 。eval 函数中可以执行几乎所有的 JavaScript 代码,简直就是一个行走的编译器。在使用该函数时,同样需要做好相关的安全工作,比如限制未知用户往里面传入任何数据。

当然,事件热的时候,关注过,凉了之后也就不了了之。

又见,思考

直到最近一段时间,在做一个项目,需要对图像文件做一下处理。在 Linux/Mac 下有一个图形图像处理工具,叫 imagemagick ,里面有瑞士军刀般多的好用功能。下意识中,我就想到了使用 Node.js 的子进程功能来直接使用。但是我又觉得这么实现似乎过于简单,而且太依赖本地环境。

于是,我就去 Node.js 官网查找了一下,看有没有新增处理图形图像的新特性,很遗憾,并没有!但是我并没死心,又继续在 bing 中搜索了一下。这时我找到一片文章(nodejs图片处理工具gm用法),介绍了如何在 Node.js 环境下,使用 npm 包 gm 来处理图形图像文件。

我很高兴,并认为这是一个理想的方案,因为那篇文中关于 gm 的介绍十分清楚,并且使用起来也很简洁。在甚至连 gm 在 npm 官网上的使用手册都没有看的前提下,我就贸然使用了 gm 。

当我激动地盯着屏幕,看着那段脚本启动,然后报异常后,我的心情先是失落,然后转为意外,最后,我静静地笑了。因为异常提示没有找到 convert 命令,而 convert 命令正是我上面提到 imagemagick 工具一下的一个命令。所以我推测这个 gm 包其实就是按照我最初的那种思路实现的。

为了一探究竟并验证我的想法。首先,我看了 gm 在 npm 上的官方使用手册。果然,文档的一开始就清楚的提示,使用该工具之前需要先安装 imagemagick 。那它到底是如何使用的 imagemagick 呢?让我们接着往下看。

我看了 gm 的源码。果然, gm 引用了一个叫 cross-spawn 的包,来执行 convert 命令,而 cross-spawn 的源码中,确实是使用 child_process 子进程来实现的。

到此为止,整个过程已经理顺。而我则陷入了沉思……

可想而知,利用这种模式,即 Node.js + child_process 的方式,可以实现很多基于操作系统命令行的功能。在没有遇到 gm 之前,我只是简单地这么认为。在遇到 gm 之后,我才发现,原来真的有工程师利用这种模式来输出被广泛( gm 在 npm 上的周下载量,截止我写这篇文章为止,已经超 10 万了)使用的工具。有时候,编程技术迭代很快,日新月异,每隔一段时间就会有一个新的很炫酷的技术冒出来,然后受到大家的追捧。而有时候,编程技术迭代又很慢,很多技术明明是新瓶装旧酒,却同样可以焕发出耀眼的光芒。

所以说,在计算机这一行,如果你真心的热爱她,就应该沉下心来,认真地去理解和感悟技术的本质,而不是去一个劲儿地追逐新时髦。唯有看透本质,才是获取真理钥匙的最佳途径。

导读

使用 npm 作为包管理工具已经很多年了,但是常用的其实就那么几个功能,一些基础的但很重要的概念和用法反而不太清楚。最近抽空整理并探索一下,重新学习 npm 。

今天先对 package.json 配置文件中的 dependencies 和 devDependencies 做一番研究。

dependencies

  1. 使用环境:生产环境下所依赖的 npm 包集合;
  2. 命令行安装单个包的方式:
1
2
3
4
# 在与 package.json 同级目录下执行
# —sava-prod 为默认安装模式
# 安装以后会自动在 package.json 的 dependencies 下添加 <package-name>
npm install <package-name> [—sava-prod]
  1. 命令行集成安装方式:
1
2
3
4
5
# 在与 package.json 同级目录下执行
# 安装 dependencies 和 devDependencies 中的所有 npm 包
npm install
# 仅安装 dependencies 下的所有 npm 包
npm install —production
  1. 被调用:如果将当前工程作为一个单独包,提供给其他人使用,在对方使用 npm install 命令安装时,会自动下载 dependencies 下的包集合。

devDependencies

  1. 使用环境:开发和测试环境下所依赖的 npm 包集合;
  2. 命令行安装单个包的方式:
1
2
3
# 在与 package.json 同级目录下执行
# 安装以后会自动在 package.json 的 devDependencies 下添加 <package-name>
npm install <package-name> —sava-dev
  1. 命令行集成安装方式:
1
2
3
# 在与 package.json 同级目录下执行
# 安装 dependencies 和 devDependencies 中的所有 npm 包
npm install
  1. 被调用:如果将当前工程作为一个单独包,提供给其他人使用,在对方使用 npm install 命令安装时,不会自动下载 devDependencies 下的包集合。

建议

部署

在我以往写代码的过程中,对于需要引用包的位置,并没有做任何区分,因为在使用 npm install 时会一起安装,所以没有太在意,直到使用 nest.js 时发现,默认安装的包居然有 200M 之多。

当我需要自动化部署它们到一台线上服务器时,会花费很多时间,但其实像 @nestjs/cli 、 @types/supertest 等开发和测试工具在线上根本是用不着的。

于是我对于要往线上环境上传的工程使用了 npm install —production 命令,这时文件大小缩小为 11M ,然后再压缩以后,最终仅仅剩下了 5M 。

发布

当整个工程开发完毕,作为一个独立包待发布时,同样需要注意!如果不对 dependencies 和 devDependencies 加以区分,将开发/测试依赖写在生产依赖下,会导致调用该 npm 包的用户下载很多与其无关的包,从而造成不必要的资源浪费。

参考来源

Specifying dependencies and devDependencies in a package.json file | npm Documentation
npm install 你很明白吗 - NeverYu - CSDN博客

起因

在一次使用 vs code 时,发现一个 bug :使用中文路径 split terminal (终端分屏)时会报异常并且失败,英文路径是ok的。

经过

确认 bug

然后我在不同场景下试图复现这个 bug :

  1. 使用 vs code 新建一个工程,然后新建中文目录, bug 会 100% 复现;
  2. 在 vs code 中选择不同 shell , bash 和 zsh , bug 仍旧 100% 复现;
  3. 因为vs code的终端是基于本地系统的,所以我在本地终端中,新建中文文件,然后基于该路径,使用终端软件自带分屏功能进行分屏,并没有出现 bug ,排除终端问题。

提交 bug

目前来看,这个应该是 vs code 的一个 bug,实锤了。因此我决定将这个 bug反馈给 vs code 的工作人员。

之前在使用 vs code 时,无意中发现右下角有个笑脸,是用来接收反馈信息的,于是乎我现在选择触发它。

触发以后,点击“提交 bug ”,会弹出一个界面,用于填写bug内容,这里需要使用英文编辑(如果是中文提问,工程师会告诉你他不会中文,并无情地关闭 issue )。然后点击“在 github 预览”会跳转到 github 下(需要登录),此时如果不需要编辑,直接 submit issue 就好。

来自工程师的响应

在提交完 issue 以后的第二天早上,我收到了来自 vs code 工作人员的回复,一位来自澳大利亚的软件工程师,他告诉我提交该类型的 issue ,还需要提供一下我的 vs code 版本信息,最好还有 bug 复现的视频、动图或者截屏。

得到对方的及时回复(毕竟有时差),我还是有点小激动的。然后按照对方提示,我认真的准备了 vs code 版本信息和 bug 复现动图,并再一次提交。

这一次,在当天晚上我就得到了回复,对方提供了解决方案,修改 setting 配置,在分屏时自动返回到 vs code 工程的父路径。这样配置以后,中文路径分屏确实不报异常了,但是却修改了我原本的使用习惯。于是乎,我把这个情况再一次反馈回去。

这位来自澳大利亚的帅气工程师,这次是这么回复的:你提的问题我们后续会修复,之前告诉你的也只是当前的解决方案。简洁并且直爽,人家之所以给你提之前的方案,并不是说 bug 不存在,只是另一个解决方式,bug我们还是会改,但是你得等待。

结果

到此为止,我认为我的目的达到了,于是乎就把这个issue关了。但是没过多久,这个工程却重新打开了 issue 。我没有再追问,我推测在 bug 修复之前, issue 应该是不会关闭,具体是不是这个澳洲小伙修复就不得而知了。

关于这次issue的整个过程,在 github 上完成过程,感兴趣的同学可以自行查阅。(terminal.integrataed.splitCwd inherited doesn’t work correctly for unicode characters · Issue #83496 · microsoft/vscode · GitHub)

总体思路

  1. 在linux服务器新建无登录账号(/sbin/nologin/)
  2. 构建自动化启动进程脚本
  3. 新建计划任务定时监控服务(crontab)

初衷

在服务器的管理中,我们需要启动一些服务,出于安全方面的考虑,我们不希望以root的方式启动它,而是为这些服务单独建立相应的账号,并且禁用它们的shell登录权限。

实现过程

场景说明

在linux服务器上实现对于nodejs进程进行守护的场景。

构建nodejs程序

  1. 我们新建一个nodejs程序server.js:
1
2
3
4
5
6
7
8
9
const http = require(‘http’)
const server = http.createServer(function (req, res) {
res.writeHead(200, {
‘Content-Type’: ‘application/json’
});
res.end(‘{“server”:”nodejs”}’);
});
server.listen(8080)
console.log(“port = 8080”)
  1. 常规启动:
1
node server.js

启动以后,server.js进程属于root用户,或者其他可以shell登录的用户:

3. 在浏览器敲入ip加端口即可查看运行结果:

新建无登录账号

  1. 新建账号:
1
useradd -M -s /sbin/nologin nodejsUser
  1. 在/etc/passwd下查看账号状态:

    nodejsUser是无法shell登录的,我们可以使用如下命令启动server.js:
1
2
3
4
# 赋予nodejsUser账号可访问server.js文件的权限
sudo chown nodejsUser /opt/nodejsUser/server.js
# 启动
sudo -u nodejsUser node /opt/nodejsUser/server.js
  1. 此时查看8080端口,可以发现,现在用户是nodejsUser了:

编写自动化启动脚本

  1. 此处我们需要借助tmux工具来实现实时监控nodejs进程的功能,下面是具体实现过程server.sh:
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
#!/bin/bash
# tumx启动脚本

# 兼容zsh
export DISABLE_AUTO_TITLE="true"
# 会话名称
session=“sever”
# 判断server.js进程是否存在
ps -ef|grep "node /opt/nodejsUser/server.js" |grep -v grep
if [ $? -ne 0 ]
then
echo "start process....."
# 判断tmux会话是否存在
tmux has-session -t $session
if [ $? = 0 ]
then
# 如果存在,则进入该会话
tmux attach-session -t $session
else
# 如果不存在,则新建会话
tmux new-session -d -s $session
fi
# 发送启动程序命令
tmux send-keys -t $session "sudo -u nodejsUser node /opt/nodejsUser/server.js" C-m
else
echo "runing....."
fi
exit
  1. 当tmux会话启动以后,可以随时查看server.js运行的情况:
1
2
# 	启动会话
tmux a -t server

自定义计划任务

  1. 新建计划任务:
1
crontab -e
  1. 任务内容:
1
*/1 * * * * /bin/bash /opt/nodejsUser/server.sh >> /opt/nodejsUser/run.log 2>&1

每分钟执行一次server.sh脚本,执行情况记录到日志文件run.log当中。crontab的定时机制可以自行查阅,比较好理解,这里不再赘述。

写在最后的话

到此为止,所有功能均已实现,我们的计划任务会定时对server.js进程扫描,当发现该进程crash时,会自动重启。当服务器重启时,计划任务也会超常运行。

其实在这之前,我在管理nodejs项目时,使用的是pm2监控工具,现成的免费工具,用起来确实也比较方便,但是在无登录账号下遇到无法启动pm2的问题,虽然花了一些时间攻克,但最终并没有解决,因此就有了本文中的解决方案,先用一段时间看看效果吧。

初闻

一次,一个同事在群里问,有谁知道这个函数该如何用ES6的方式简写:

1
2
3
4
5
function fn(n){
return function(m){
return m+n
}
}

我当时也没动脑子,直接按潜意识改了一下,发出去了:

1
2
3
4
5
fn=(n)=>{
return (m)=>{
return m+n
}
}

果然,发code的那家伙神秘一笑,说你再继续简化一下试试。我后背一凉,赶紧又看了看,改成这个样子又抛出去了:

1
2
3
fn=(n)=>(m)=>{
return m+n
}

我自认为这种总该差不多了吧,结果又被生生打脸。题主笑而不语,然后我恍然大悟,看来之前真是自以为会用,根本没有认真思考过,其实还可以进一步简化:

1
fn=(n)=>(m)=> m+n

最后题主说,把括弧也去了吧:

1
fn = n => m => m + n

看着这段code,我沉默了……原来code可以如此简洁、优美、易懂。

偶遇

上面的故事发生没多久以后,我在看一个帖子时,发现如下code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var arr = [
{ name:"小明", age:12 },
{ name:"小红", age:11 },
{ name:"小刚", age:15 },
{ name:"小华", age:13 }
];
function compare(p) { //这是比较函数
return function (m, n) {
var a = m[p];
var b = n[p];
return a - b; //升序
}
}
arr.sort(compares("age"));
console.log(arr);

然后我笑了,在评论区默默地留下了compares函数的简版:

1
compare = p => (m, n) => m[p] - n[p]

思考

Coding这么多年,其实大部分时间是比较麻木的,不走脑子的reading、copy以及coding,糊弄完了一个又一个项目。随着时光的飞逝,真正有意义的东西很少能留下来,所谓很少,其实还是有的,要不真该转行了,就比如这个ES6简写的过程,就是非常有趣并且优美的。但是这个过程很偶然,首先,你得初步了解过这个技术,然后,在一个被动的机会下,你静下心来,认真的体会技术带来的愉悦感,并真正的掌握这项技能。这似乎需要一个积累的过程,大量阅读以后再反复推敲,然后在这个过程中,自我成长,体会技术的真谛,并且使自己快乐!

概念

Tesseract是一个光学字符识别引擎,支持多种操作系统。Tesseract是基于Apache许可证的自由软件,自2006 年起由Google赞助开发。 2006年,Tesseract被认为是最精准的开源光学字符识别引擎之一。【维基百科】

声明

在使用tesseract-ocr的过程中,我遇到了一些问题,并找到了一种适合我的使用方式,在这里给大家分享一下。

困境

  1. 在Mac下使用Homebrew安装tesseract-ocr时,—with-training-tools参数取消,所以无法训练数据;如果使用github下载源码,然后编译安装的话,需要安装很多依赖,比较麻烦;
  2. 如果在ubuntu下使用apt-get安装,默认是携带各种训练工具的,但是我的ubuntu系统是server版,不支持桌面,所以在标注图片时很不方便。

我的解决方案

我的解决方案是:在MacOS下标注,然后把标注好的box文件送到Ubuntu Server下,打包训练语言模型。
注意:如果您满足我在上文中提到的困境,就可以继续往下读了。

其他方案

  1. 在Mac下编译tesseract,使用jTessBoxEditor标注+训练
  2. 在Linux桌面版安装tesseract,使用jTessBoxEditor标注+训练
  3. 在Windows安装tesseract,使用jTessBoxEditor标注+训练

安装

tesseract-ocr支持Linux、Windows、MacOS、Android、IOS等操作系统。由于本人使用的操作系统是MacOS和Linux ubuntu server,所以本文只介绍这两个环境的安装。

Mac

借助Homebrew,执行命令:

1
brew install tesseract-ocr

安装完以后,截至今天为止,默认是4.0.0版本,不支持—with-training-tools,所以也不支持训练功能。如果直接使用github下载源码编译安装,是可以支持的,但是我比较懒,在需要训练操作时,我就去Linux上搞了(Linux的4.0.0是携带辅助训练工具包的)。

Linux

借助apt-get,执行命令:

1
apt-get install tesseract-ocr

基本用法

  1. 设置识别语言,默认英文(eng),中文是chi_sim(需另行下载语言包),如果需要同时识别两种或者两种以上语言,比如同时识别中文和英文,格式为“-l chi_sim+eng”,执行以下命令,识别结果会以txt文本形式保存在目标路径下。
1
tesseract [待识别图片路径] [目标识别结果路径] -l eng
  1. 设置文字坐标输出,执行以下命令:
1
tesseract [待识别图片路径] [目标识别结果路径] -l test makebox

识别结果会以box后缀文件形式保存在目标路径下,坐标内容包含识别结果左上角的横纵坐标以及长宽值,比如识别如下字符:

其结果为(啧啧啧,通用中文库识别效果很差):

数据训练

借助第三方工具

jTessBoxEditor是一款辅助标记的工具,在本地安装完tesseract-ocr以后,可以在jTessBoxEditor中配置tesseract-ocr的启动路径,然后基于tesseract-ocr进行可视化标注。如下图所示:

环境配置

在jTessBoxEditor的Trainer标签下,主要功能说明:

  1. Tesseract Executables配置tesseract程序启动路径
  2. Training Data存放训练数据路径
  3. Language为自定义训练集名称
  4. Bootstrap Language是训练之前需要借助的语言模型名称
  5. 然后选择Make Box File Only
    注意:使用jTessBoxEditor需要配置Java环境变量。

初步识别

  1. 选择Make Box File Only
  2. 点击run按钮,就可以出来借助默认语言模型执行的识别结果。

开始标注

在获取到执行结果以后,切换到Box Editor标签,如下图所示:

标注过程:

  1. 首先点击“open”按钮打,打开之前识别结果box
  2. 在左侧列表中,选择一个字符,比如上图中手写版的“测”字,然后在右侧提示框中对其进行调整,直到红色框完美包含字符
  3. 然后依次调整其余所有字符
  4. 如果需要新增标注框,点击“insert”,或者需要删除,请点击“delete”
  5. 如何需要合并两个字符,点击“merge”,或者均分字符,请点击“split”
  6. 点击“save”保存box
  7. 返回上个标签Trainer,然后选择“Train with Existing Box”,点击“run”执行
  8. 这时会在训练路径下生存.traindate语言模型
  9. 最后,再换一张与训练数据类似的图片,点击Validate进行验证。
    注意:如果你没有在Mac上编译安装tesseract,只是通过brew安装了4.x.x版本,那么到第6步就可以停止了,因为该版本不携带训练工具,你可以拿着标注好的box文件,找一个linux机器手动生存语言模型包,过程参考上述“命令行打包语言模型”。

命令行打包语言模型

  1. 使用tesseract命令生成识别结果box文件
  2. 调整box(或者直接使用jTessBoxEditor标注结果)
  3. 依次执行以下命令:
1
2
3
4
5
6
7
8
9
# picture.jpeg是待识别图片
tesseract picture.jpeg picture nobatch box.train
unicharset_extractor picture.box
# 新建font_properties本文,内容为test 0 0 0 0 0
shapeclustering -F font_properties -U unicharset picture.tr
mftraining -F font_properties -U unicharset -O unicharset picture.tr
cntraining picture.tr
# 到此为止,会生成unicharset、inttemp、pffmtable、shapetable、normproto等五个文件,分别给他们加上test.前缀(test为自定义语言模型名称)
combine_tessdata test.
  1. 此时会在当前目录下生成test.traineddata文件,移动该文件到tesseract-ocr的tesedata目录下(我的路径是_usr_share_tesseract-ocr/4.00_tessdata/)
  2. 然后设置语言执行识别命令:
1
2
# test是刚才生成的语言模型名称
tesseract test1.jpeg test1 -l test
  1. 执行结果:(效果还是不错的)

总结

总体而言,虽然是桌面版标注+服务器端处理,但是使用起来还是可以接受的。如果将来有时间,可以研究一下在mac下之间编译tesseract源码

概念

PM2(process manage)是JavaScript运行时Node.js的进程管理器。

基本操作

安装

1
2
# 借助npm包管理器安装
npm install -g pm2

启动进程

命令行启动

  1. 默认启动
1
2
# 启动一个nodejs进程,进程名默认为nodejs文件名,默认为fork单进程模式
pm2 start app.js
  1. 自定义启动
1
2
# 启动并自定义进程名
pm2 start app.js --name processNames
  1. 负载均衡
1
2
# 负载均衡(服务器只是满足双核及以上),max为默认最大cpu数,可以自定义
pm2 start app.js -i max
  1. 目录监听
1
2
# 监听目录变化,默认不启动
pm2 start app.js --watch
  1. 携带参数启动
1
2
# 携带test和100启动进程
pm2 start app.js -- -test -100

配置启动脚本

  1. 首先,编辑启动脚本内容
1
2
3
4
5
6
7
8
9
10
11
{
"apps": [{
"name": "app1",
"script": "./app1.js",
"watch": true
}, {
"name": "app2",
"script": "./app1.js",
"watch": false
}]
}
  1. 然后执行启动命令
1
pm2 start app.json

重启进程

  1. 重启/更新单个进程
1
2
3
4
# 重启0进程
pm2 restart 0
# 更新0进程
pm2 reload 0

区别:重启的过程是先停止(stop)进程,然后再开启(start)进程,可能会造成已建立连接的实效;更新的过程是先开启(start)一个或者若干进程,同时停止旧的进程接收请求,然后旧的进程结束以后,再关闭旧的进程。
2. 重启/更新所有进程

1
2
3
4
# 重启所有进程
pm2 restart all
# 更新所欲进程
pm2 reload all
  1. 优雅的更新一个/所有进程
1
2
3
4
# 优雅的重启0进程
pm2 gracefulReload 0
# 优雅的重启所有进程
pm2 gracefulReload all

备注:gracefulReload区别于reload的地方在于,会发送一个shutdown的消息给旧的进程,这个过程可以在程序中监听到,然后根据自己的业务逻辑进程处理。

1
2
3
4
// 这里我们可以监听到‘shutdown’,然后在里面可以自定义一些跟自己项目相关的业务逻辑
process.on('shutdown', function () {
server.close();
});

进程日志

1
2
3
4
# 实时查阅进程0的日志
pm2 log 0
# 查阅所有进程的日志
pm2 logs

停止进程

1
2
3
4
# 停止0进程
pm2 stop 0
# 停止所有进程
pm2 stop all

删除进程

1
2
3
4
# 删除0进程
pm2 delete 0
# 删除所有进程
pm2 delete all

进程列表

1
2
# 查看进程状态
pm2 list

进程监控

1
2
# 监控所有进程的实施状态
pm2 monit

保存配置

1
2
# 保存当前所有进程任务
pm2 save

还原配置

1
2
# 还原最后一个保存的所有进程任务
pm2 resurrect

应用场景

守护进程

当进程出现崩溃、异常停止等现象时,pm2会帮你重启该进程。

进程监控

实时查看进程的cpu、内存占有情况以及运行状态等。

日志管理

所有pm2日志会默认保存在~/.pm2/logs目录下,以进程名命名生成相应的日志文件,可以借助pm2-logrotate模块实现日志的按时间或者大小切割,便于归档记录,排查故障。

概述

Tmux 是一个优秀的终端复用器类自由软件,但使用 BSD 许可发布。用户可以通过 tmux 在一个终端内管理多个分离的会话,窗口及面板,对于同时使用多个命令行,或多个任务时非常方便。

基本操作

会话

  1. 新建会话
1
2
3
4
# 新建会话,会话名默认生成
tmux
# 新建会话,并声明会话名
tmux new -s session_name
  1. 正在运行会话列表查阅
1
tmux ls
  1. 打开一个正在运行会话
1
tmux a -t session_name
  1. 退出当前会话
    快捷键:control+b,d
  2. 删除会话
1
2
3
4
# 删除一个正在运行的会话
tmux kill-session -t session_name
# 删除当前会话名以外的其余会话(不指定会话名,默认保留第一条会话)
tmux kill-session -a -t session_name

分屏

  1. 横向分屏
    快捷键:control+b,%
  2. 纵向分屏
    快捷键:control+b,”

杂项

  1. 移动光标
    快捷键:control+b,方向键
  2. 显示系统时间
    快捷键:control+b,t
  3. 关闭窗口
    在当前会话下,将光标移动到待关闭窗口,然后敲命令exit即可关闭窗口。

应用场景

代码调试

在Linux或者Mac终端水平或者垂直切割屏幕,将切割区域分为代码部分、编译部分、调试效果展示部分、日志记录部分等,当我们需要debug时:在代码部分敲入代码,然后在代码编译部分编译代码,最后在展示部分(可终端展示程序)查看效果,如果有问题呢,在日志部分也可以实时检查。可谓码农的编程利器啊!

系统监控

我们可以建立多个会话,分别监控我们需要关注的系统性能、报警等信息,比如cpu使用情况、网络使用情况、内存是否正常、磁盘是否够用等等,然后在每个会话里,我们还可以通过屏幕切割达到对于每项监控更加细致的分类,一目了然,掌控全局!

记忆标签

本地

很多使用Mac的同学都一个不成文的习惯,电脑长时间不关机,并且同时开启很多终端,原因有很多,比如担心重启电脑以后丢失之前进行到一半的任务,或则只是因为懒……当然,mac本身设计的很好,就算长期不重启也毫无压力。这个时候,其实我们需要一个记忆标签工具,来管理我们打开的数量庞大的终端们。不管开启多少个终端,只要我们简单的取个名字作为标记,下次再找的时候会快捷很多!

服务器

对于服务器来说,重启的机会更加微乎其微,这个时候,我们就更需要一个终端记忆标签工具来管理我们的远程会话了,当我们ssh连接服务器时,遇到突发事件,比如被叫去开会,或者下班了,没关系!在开完会或者第二天上班以后,我们还可以进入当时那个会话,继续之前没有完成的工作。

介绍

Secure Shell(安全外壳协议,简称SSH)是一种加密的网络传输协议,可在不安全的网络中为网络服务提供安全的传输环境。

连接方式

秘密验证

连接命令:

1
ssh username@ip

注释:其中username是服务器的用户名,ip是服务器的地址,然后输入服务器端该用户的登录密码即可完成连接验证。

密钥验证

  1. 生成密钥
    在客户端生成公钥和私钥,此处我们使用rsa作为加密方式(加密方式另外还有rsa1、ecdsa、ed25519、dsa等),使用以下命令生成密钥:
1
ssh-keygen -t rsa

并设置文件名和秘密(这里我们定义文件名为id_rsa),然后会在目录_.ssh_下生成私钥文件id_rsa和公钥文件id_rsa.pub。
2. 连接验证
生成密钥文件以后,将公钥放置在服务器端的目录
_.ssh_authorized_keys下,然后在客户端使用私钥进行免秘密验证,连接命令:

1
ssh -i ~/.ssh/id_rsa username@ip

快速连接配置

在文件目录~_.ssh_下有个config文件,我们可以将上文两种连接方式中写在里面,然后直接以ssh+服务器简称的方式进行快速连接。

  1. 秘密配置
1
2
3
4
Host    server01
HostName 192.168.1.10
Port 22
User username

注释:22是ssh的默认连接端口(可变更)。配置完成以后,可以通过以下命令连接,连接时需要再次输入密码。

1
ssh server01
  1. 密钥配置
1
2
3
4
5
Host    server02
HostName 192.168.1.11
Port 22
User username
IdentityFile ~/.ssh/id_rsa

注释:其中IdentityFile为私钥文件的路径。配置完成以后,可以通过以下命令连接,连接时无需再次输入密码。

1
ssh server02

搭建通道

最简单的文件复制通道,执行命令:

1
scp -i ~/.ssh/id_rsa '待传送文件' username@ip:'目标传送目录'

通过以上命令,可以建立客户机和服务器之间或者两台服务器之间,文件单向传送的通道。

安全性

SSH旨在保证非安全网络环境(例如互联网)中信息加密完整可靠。不过,SSH它也被指出有被嗅探甚至解密的漏洞。这里我们就不做具体介绍了。

0%