0%

一直希望在公司或者其他地方能随时控制自己家里的 Mac,无奈家里没有公网 IP。囿于这个问题,我甚至准备开发一个基于 TCP 的 Server,让家里的电脑通过跟远端服务器建立持久连接并接收和执行远端命令。在动手之前,还好我搜索了一下,终于找到了直接使用 ssh 的方式来控制局域网内的电脑,让我不用去做无用功。

SSH 端口转发

在 ssh.com 上有这样一段话[1]

在 OpenSSH 中,可以指定 -R 选项来进行端口转发。

1
ssh -R 8080:localhost:80 public.example.com

这个命令可以让任何人通过远端服务器连上远端服务器的 8080 端口。该连接会被转发给客户端,客户端会向 80 端口发起 TCP 请求。
localhost:80 可以被任何域名或者 IP 代替。
这个示例对我们从外网连接内部服务器非常有用。

由此我们知道,我们可以通过 ssh -R 选项,将内网的端口映射到一台该主机可以连接的任何一台其他主机。

在 local_server 上执行

1
2
3
# N 选项为不执行命令,仅转发
# f 选项为后台运行
ssh -NfR 2222:localhost:22 root@remote_server

这样我们便将内网的 local_server 的 22 端口映射到了 remote_server 下的 2222 端口。

配置网关

通过上一步的命令,我们在 remote_server 上执行

1
ssh root@localhost -p 2222

便可以连接上 local_server 的主机。

然而,当我们从其他主机执行

1
ssh root@remote_server -p 2222

时,却会总是返回 “Connection refused”。

这是因为默认的 ssh 代理只允许本机访问该端口,如果需要让其他机器可以访问,则需要修改 /etc/sshd_config。

1
2
# no 修改为 yes
GatewayPorts yes

这样,我们便可以通过 ssh 代理的方式在任何地方访问内网的主机了。

总结

如果你想随时随地访问你内网的主机,你需要

  1. 一台有公网 ip 的其他主机;
  2. 通过 ssh -R 命令将内网主机代理到公网主机上;
  3. 通过修改 GatewayPorts 让任何能访问到你公网主机的设备都可以访问你的内网主机。

参考资料 & 说明

[1] 本段原文地址在 SSH Port Forwarding Example,原文为英文,本文对该段进行引用时翻译为了中文。

什么是分析型数据库?

数据处理可以分为 2 种,OLTP (Online Transaction Proccessing,联机事务处理) 和 OLAP(Online Analytical Proccessing,联机分析处理)。

OLTP 一般聚焦于日常的业务,例如支付系统里的一笔交易、图书馆里的借阅记录、博客系统里边的一篇日志。

OLAP 则着重强调数据分析,例如 9 月份的业务和 8 月份业务的对比、用户近半年的留存、用户的兴趣的分布,一般的数据仓库都是 OLAP 的存储形式。

OLTP 需要即时、快速、无差错的保证每一个业务的完成,因此要尽量简单。OLTP 的数据变化非常频繁,而且用户会经常基于行进行查询,因此数据要做好热备和缓存,在用户更新和获取时尽快返回,因此数据存在内存中最好不过。

OLAP 则一般用于决策,每次查询用的数据量都是百万以上的级别,这时候存储在内存显然不合适。OLAP 的数据一般不会经常变化,插入的需求远远多于更新。OLAP 对于查询的需求不要求像 OLTP 那样 ms 级别的返回,一般秒级别甚至分钟级别都可以接受。

适用于 OLAP 的数据库就是分析型数据库了,他们天生就是为了分析而生。

为什么使用了阿里云分析型数据库?

我们最终选择了阿里云分析型数据库 Postgres 版作为我们 OLAP 的数据存储方式,主要原因有以下几点:

  1. 支出列存储。在用户更新行为远少于插入行为时,列存储更快且支持较高的压缩率。
  2. 支持压缩。当数据量较大时,压缩可以减小存储体积,节省存储成本。
  3. 支持 JSON 操作。因为业务上的原因,有些数据比较复杂,难以直接作为列进行存储,这时候使用 JSON 可以大幅简化查询行为。

分析型数据库的具体使用

创建数据表

1
2
3
4
5
6
7
8
9
10
11
12
13
create table app_log (
log_id varchar(64),
session_id varchar(64),
event varchar(64),
event_time timestamp
)
WITH (appendonly = true, orientation = column, compresstype = zlib, compresslevel = 5) distributed by (log_id)
partition by range (event_time)
(
START (date '2020-2-4') INCLUSIVE
END (date '2024-2-4') EXCLUSIVE
EVERY (INTERVAL '1 month')
);

执行该语句,这样我们就成功地创建了一张表。

导入数据

开始时我们是直接向数据表中使用 insert 语句插入数据的,随着 QPS 的增加,insert 语句占用了数据库的大部分 CPU,导致我们平常进行查询时非常缓慢,因此我们改为了每日凌晨导入前一天数据的方式。这样我们既能在第二日正常地对既有数据进行分析,又不会使 CPU 一直占用很高。

我们采用了从 OSS 外表导入的方式来完成该功能。

  1. 写入数据到 CSV
  2. 将 CSV 数据上传到 OSS
  3. 从 OSS 上的 CSV 文件创建外部表
  4. 将外部表的数据导入到数据表

写入数据到 CSV

这一步的功能应该由业务实现,非常简单,我们来看下我们的数据文件。

1
2
ba386925fc2b266ab9ee859322a120d7,5086c4eb5d584621abe23492dead64cd,use_time,2020-01-06T11:47:16.380+08
378c880b114002f729a330e3c61bcc4b,5086c4eb5d584621abe23492dead64cd,use_time,2020-01-06T11:47:16.378+08

将 CSV 上传到 OSS

这一步的功能也非常简单,直接使用阿里云的 OSS 命令行工具或者 SDK 将 CSV 文件上传到 OSS 即可。不过要注意的是,当文件较大时(阿里云 SDK 提示的是 5G,实际中大概是 20G),会返回文件过大的错误。这时候你可以选择分割文件或者压缩文件。我采用的是压缩文件。

1
gzip 2020-01-06.csv

执行该命令即可得到名字为 2020-01-06.csv.gzip 的压缩文件。

从 OSS 创建只读外表

1
2
3
4
5
6
7
8
9
10
11
12
create readable external table external_log_20200106 (
log_id varchar(64),
session_id varchar(64),
event varchar(64),
event_time timestamp
)
location('oss://$oss
filepath=$filepath id=$oss_access_key_id
key=$oss_access_key_secret bucket=$bucket compressiontype=gzip compressionlevel=6')
FORMAT 'csv'
ENCODING 'utf8'
LOG ERRORS INTO my_error_rows SEGMENT REJECT LIMIT 1000;

这样便创建了名字为 external_log_20200106 的外部表。

需要注意的是,外部表的列名顺序和数据表要保持完全一致。

使用 Gzip 的情况下,压缩等级也要保持一致。

从外部表导入数据

1
insert into app_log select * from external_log_20200106;

执行该命令即可从外部表导入数据到数据表中,这样便完成了数据的写入。

查询数据

阿里云分析型数据库支持 BitMap,Hash,Btree 等类型的索引,在查询之前一定要根据自己的需求加好索引方便快速查询,这个由于大家的业务都不一样我就不举例了。

入门

HTTP/2 旨在解决许多 HTTP/1.x 的缺点。现在的网页使用很多 HTML、CSS、JavaScript、图片等资源,在 HTTP/1.x 中,每个资源必须被显式请求。这会是一个漫长的过程。浏览器先获取 HTML,然后一边解析页面,一边获取更多资源。服务器必须等着浏览器发起每次请求,这就导致网络会经常空闲、未充分利用起来。

为了改善延迟,HTTP/2 引入了 Server PushServer Push 允许资源在明确被请求之前,由服务器推送这些资源给浏览器。服务器通常知道需要加载哪些额外资源,因此可以在响应初始请求时一起推送这些资源。这让服务器能充分利用剩余空闲带宽来改善页面加载时间。

serverpush.svg

在协议层,HTTP/2 Server PushPUSH_PROMISE 帧发起。 一个 PUSH_PROMISE 表明了服务器预测浏览器将会发起的请求。浏览器一接收了该 PUSH_PROMISE,就会知道服务器将要分发资源。若浏览器之后发现需要这个资源,它就会等着该 Push 完成,而不是发送一个新请求。这就减少了浏览器在网络上花的时间。

net/http 包中的 Server Push

Go 1.8 从 http.Server 中引入了对 Server Push 响应的支持。如果运行的是 HTTP/2 的服务器,并且进来的连接使用了 HTTP/2 ,这个特性就能用了。在每个 HTTP 的处理程序中,你都可以通过检查 http.ResponseWriter 是否实现了 http.Pusher接口,来判断服务器是否支持 Server Push

例如,如果服务器知道渲染页面时需要 app.js,处理程序可以在 http.Pusher 可用时这样初始化一个 Push:

1
2
3
4
5
6
7
8
9
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if pusher, ok := w.(http.Pusher); ok {
// Push is supported.
if err := pusher.Push("/app.js", nil); err != nil {
log.Printf("Failed to push: %v", err)
}
}
// ...
})

Push 会为 app.js 创建了一个合成请求,把 app.js 合并到 Push_Promise 中,然后将合成请求转发给服务器处理程序,服务器处理程序会生成响应。Push 的第二个参数指定了包含在 Push_PROMISE中的额外 header。 例如,如果 app.js 的响应有 Accept-Encoding 上的不同,那 PUSH_PROMISE 应该包括 Access-Encoding 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if pusher, ok := w.(http.Pusher); ok {
// Push is supported.
options := &http.PushOptions{
Header: http.Header{
"Accept-Encoding": r.Header["Accept-Encoding"],
},
}
if err := pusher.Push("/app.js", options); err != nil {
log.Printf("Failed to push: %v", err)
}
}
// ...
})

完整示例见:

1
$ go get golang.org/x/blog/content/h2push/server

运行服务器,加载 https://localhost:8080,就能在浏览器开发者工具上看到服务器 推送了 app.jsstyle.css

networktimeline.png

在响应之前 Push

在发送响应之前调用 Push 方法是个好主意,否则可能会意外产生重复响应。例如,假设写入了部分响应:

1
2
3
<html>
<head>
<link rel="stylesheet" href="a.css">...

然后调用 Push("a.css", nil),浏览器可能会在接收到 PUSH_PROMISE 之前开始解析 HTML。这会导致除了接收到 PUSH_PROMISE ,还会再发起一个 a.css 请求,服务器会生成 2 次 a.css。 在写入响应之前调用 Push 完全避免掉了这种可能。

什么时候使用 Server Push

考虑在网络空闲的情况下使用 Server Push。刚给你的 web 应用发送完 HTML?别等了,开始使用 Server Push 推送客户端需要的资源吧。还在使用内联资源(inline-style-sheet)来减小延迟吗?别用了,换成 Server Push吧。重定向是另一个使用 Server Push 的好时机,因为客户端这时候正在往返请求上浪费时间。还有很多可以使用 Server Push 的情形,我们仅仅是做个入门。

要是一下几点不提一下的话是我们的失职。

  1. 只能推送你服务器有权推送的资源,第三方服务器或者 CDN 的资源无法推送。

  2. 除非确信客户端需要这些资源,否则别推送,不然的话会浪费带宽。要是浏览器已经缓存了这些资源的话,必须要避免重复推送。

  3. 天真地推送所有资源给页面会让性能更糟,在不能确定的情况下,要慎重。

以下资源做了不错的额外补充:

尾声

在 Go 1.8 中,标准库为 HTTP/2 Server Push 提供了一个开箱即用的支持,为优化你的网页应用提供了更多灵活性。

打开 HTTP/2 Server Push demo 看一下 Server Push 的实际效果。

image.png

参考资料 & 说明

[ 1 ] 本文翻译自 Go Blog 的 HTTP/2 Server Push

[ 2 ] 本文在翻译过程中参考了 在 Go 中使用 HTTP/2 Server Push。该文章源地址在 在 Go 中使用 HTTP/2 Server Push

[ 3 ] 本文在翻译过程中进行了小部分的演绎,请知悉。由于本人水平有限,如有错讹,可以在留言区指出,不胜感激。

OpenCV 是一个跨平台的计算机视觉库,提供了很多图像处理能力。

FFmpeg 则是一个强大的视频处理工具,很多视频软件的基础功能都依赖于他。

我们可以用 OpenCV 和 FFmpeg 组合起来,来去除视频中的水印。

我们旨在解决不同视频中水印大小和位置会变化,但是水印样式一致的视频。另外,单个视频中的水印大小和位置会变化的视频不适合本方案。

从视频中截取首帧截图

1
ffmpeg -i example.mp4  -y -f image2  -ss 0 -vframes frame1.jpg

处理首帧截图拿到水印

这一步需要让你们的 UI 使用 PhotoShop 操作,获取水印并将水印外的部分中透明处理。水印图片应该为 PNG 格式。

image.png
水印

匹配水印

匹配水印需要使用 OpenCV 库,本来我是准备用 Golang 的OpenCV 库 gocv.io/x/gocv 来完成操作,但是由于 CGo 模块的报错超出我的能力范围无法解决,所以只能用 python 来完成这部分工作。

image.png
golang 下使用 opencv 的报错

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
32
33
34
35
36
37
def has_watermark(img_name, watermark_name):
# 获取原始图片的灰化图
img_rgb = cv2.imread(img_name)
img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)

img_w, img_h = img_gray.shape[::-1]

# 获取水印的灰化图
img_template = cv2.imread(watermark_name, 0)

# 根据水印所在图片的大小,调整当前水印的大小,比如说我的水印是从宽度 540 的图片上处理出来的,如果当前图片的宽度是 480,就要进行缩放,比例为 480/540=0.889
w, h = img_template.shape[::-1]
w, h = int(w * img_w / 540), int(h * img_w / 540) # 540 换成你的原始水印所在图片的宽度
pic = cv2.resize(img_template, (w, h))

# 进行水印匹配,使用了 TM_CCOEFF_NORMED 算法
res = cv2.matchTemplate(img_gray, pic, cv2.TM_CCOEFF_NORMED)
# 阈值,根据匹配的情况进行调整,如果匹配不到调小,如果匹配的过多了调大,阈值范围 (-1, 1)
threshold = 0.4

loc = np.where(res >= threshold)
x = loc[0]
y = loc[1]

if len(x) and len(y):
for pt in zip(*loc[::-1]):
# 一张图片可能会匹配到多个水印,我们只取第一个,为了避免覆盖不到,对水印的宽高稍微变大
print("x=" + str(pt[0]) + ":y=" + str(pt[1]) + ":w=" + str(w + 2) + ":h=" + str(h + 2))
cv2.rectangle(img_rgb, pt, (pt[0] + w + 2, pt[1] + h + 2), (0, 255, 255), 2, 4)
break

# 如果匹配到把标记了水印的图片写入到新文件
new_name = os.path.dirname(os.path.abspath(img_name)) + "/" + "new" + os.path.basename(img_name)
cv2.imwrite(new_name, img_rgb)
return True
else:
return False

执行 python watermark.py 1.jpg,输出

1
2
3
$ python3.7 watermark.py 1.jpg
x=375:y=900:w=149:h=37
Found

image.png
1.jpg
image.png
new_1.jpg

去除水印

使用 ffmpeg 来去除水印,Mac 上你可以执行 brew install ffmpeg 来安装。

执行

1
ffmpeg -y -i 1.mp4 -vf "delogo=x=375:y=900:w=149:h=37"  new_1.mp4

image.png
去水印后效果

这样水印就被去除了。

⚠️警告

本文仅为技术尝试,切勿用于非法用途,谨记谨记。

书籍推荐

  1. 2019 读过的好书推荐

Linux

  1. Linux 内核的一些知识点(中)

  2. 学习 Linux 命令,看这篇 2w 多字的命令详解就够了

Redis

  1. 头条高级面试题:请谈谈Redis 9种数据结构以及它们的内部编码实现

Kafka

  1. 真的,Kafka 入门一篇文章就够了

  2. Thorough Introduction to Apache Kafka™

  3. 如何快速全面掌握Kafka?5000字吐血整理

网络

协议

TCP

  1. TCP 三次握手背的滚瓜乱熟,那意外情况呢?丢包了呢?故意不回复 ACK 呢?

综合

  1. 程序员不得不了解的硬核知识大全

为什么要做代码检查?

在代码进行更改后,往往可能会造成预期之外的效果。比如说更改了某个函数的名字,但是进行函数调用的地方有遗漏;或者是修改了简单逻辑,但是未通过单元测试。如果我们在这时候提交了代码,往往会导致分支代码的不正确,甚至可能导致出现生产事故。

为了避免上述情况的发生,我希望每个团队的协作者在代码提交之前,能够检查自己的代码格式、运行单元测试来尽量避免错误的发生。通过「前置」而非「后置」的模式,防患于未然。

怎么做代码检查?

我们知道,代码检查通常有简单的格式检查和单元测试。如果一个程序无法通过格式检查、编译和单元测试,那么无疑是有问题的。

我们无法确保团队的每个成员在提交的时候让他们做手动检查,这样既容易遗忘又麻烦。

既然我们使用 Git,便可以使用 Git 的 Hooks 来完成这项工作。

Git 的 Hooks 可以让你在发布前后做一些工作,具体的原理参考 自定义 Git - Git 钩子

另外,你可以使用 Gerrit 等代码审查软件来完成这项工作。由于复杂性和易接受性方面的原因,我们没有采纳,这里也不进行过多介绍,如感兴趣可以参考 Gerrit 官网的介绍进行配置。

以下以 Git Hooks 方式实现了代码提交检查。

示例(以一个 Golang 项目为例)

1. 编辑 .git/hooks/pre-commit 文件

新建 pre-commit 文件

1
2
touch .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

编辑 pre-commit 的内容

1
2
3
4
5
6
7
8
9
10
11
work_dir=$(git rev-parse --show-toplevel)

if ! go vet $(go list $work_dir/...); then
echo "Go vet failed, please check your code."
exit 1
fi

if ! go test $(go list $work_dir/...); then
echo "Go test failed, please check your code."
exit 1
fi

2. 构造一个无法通过编译或者无法通过单元测试的场景,提交代码。

image.png
出错时会无法提交代码

3. 将 Hooks 加入版本控制系统

在此之后,我们会遇到一个问题,.git 目录本身作为 Git 的仓库目录是无法添加到版本控制系统中的。

从 Git 2.9 开始,Git 可以手动指定 Git 的 Hooks 目录。

1
2
3
mkdir .githooks
cp .git/hooks/pre-commit .githooks
git config --local core.hooksPath .githooks

这样便可以将 Git Hooks 进行共享。

我们在 Makefile 中加入一段话

1
2
3
.PHONY: init
init:
git config --local core.hooksPath .githooks

项目成员需要执行 make init 来初始化 Git hooks 目录。

最近做了几道很有意思的题,判断一个整数是不是 2、3、4 的幂。在求解的过程中引发了一些思考,记在这里。

判断 n 是否为 2 的幂

如果 n 是 2 的幂且 n 为整数,必然可表示为二进制的 0b1000.....0,我们可以发现 n -1 可表示为 0b11...1,二者进行与运算,结果必定为 0。其他任何数与 n 做与运算,结果必定大于0。

因此符合 n & (n-1) == 0 的数为 2 的整数次幂。

这里还有另外一种方法。

我们随便找到一个 2 的整数幂,例如 64,我们将 64 进行因数分解,64 = 2 * 2 * 2 * 2 * 2 * 2。64 的因数必定为 2 的整数幂。因此只要一个数字能被 64 整除,该数必定为 2 的幂。

在 int64 范围内的最大的 2 的幂是 1<<62,也就是 4611686018427387904。符合 4611686018427387904 % n == 0 的数为 2 的整数次幂。

判断 n 是否为 3 的幂

我们将上边 2 的整数幂的第二个算法推广,便可以得出完全一样的判断是否为 3 的幂的算法。

27 = 3 * 3 * 3 * 1,因此 1,3,9,27 都是 3 的幂。

在 int64 范围内的最大的 3 的幂是 4052555153018976267,符合 4052555153018976267 % n == 0 的数为 3 的整数次幂。

判断 n 是否为某个素数的幂

将上边的算法进行再次推广,我们发现这个方法适合于任何的素数。

一个素数的幂必定所有的因数都是该素数的幂。

因此我们可以在整数范围内找到最大的该素数的幂,然后用这个数对 n 进行取余,结果为 0 则为该素数的幂。

判断 n 是否为 4 的幂

4 不是素数,所以上边的适用于素数的办法并不适合4。
例如 16 = 4 * 4,同时 16 = 8 * 2, 8 和 2 都不是 4 的幂。

对于 4 我们进行观察,首先发现 4 的幂必定为 2 的幂。

其次我们发现 2*x+1 位为 1,其他位为 0。

因此这个数与 0x[01]…[01] 进行与运算必定为它本身。不符合这样的数进行运算则为 0 。

我们在 int 范围内找到最大的数是 6148914691236517205 。所有符合 (6148914691236517205 & num == num) && (4611686018427387904 % num == 0) 的数为 4 的幂。

判断 n 是否为 8、16、32、64 … 的幂

对上边的结论再次推广,8 的幂的 3x+1 位必定为1,16 的幂的 4x+1 位必定为 1。

因此我们可以用类似 0b[001]…[001] 和 0b[0001]…[0001] 的方式来判断 8(23) 和 16(24) 的幂,也就是适用所有的 2n 的幂。

小结

我们在学习过程中总是会遇到各种各样的问题,在懂得相应的原理之后更应该多多思考,举一反三,很多时候会发现自己吃遇到的不仅仅是一个问题,而是一种广泛的思路。由 2、3 的思路我们可以推广到所有的素数,由 2、4 的思路我们可以推广到所有的 2 的幂,这便是我们日常中的开拓与巩固,在巩固过程中让自己真正理解了自己的思路。

公司刚刚开始创业,因为团队人数较少,没有运维来处理 CI/CD ,之前是我在 Makefile 中定义了发布命令。最近趁闲暇之际,花了点时间来搭建了使用 Gitlab 的 Pipeline 来进行 CI/CD。

Gitlab CI/CD 的几个概念

Runner

Runner 是定义在 .gitlab-ci.yml 中的脚本的运行者。

在运行 Job 或者 Pipeline 时,Gitlab 会根据 tag 找到对应的 runner 执行相应的命令。如果找不到对应的 runner,该 Job 就会进入阻塞状态。

Runner 需要在容器或者主机上安装 gitlab-runner,然后根据项目或者项目组的 token 进行注册。

Stages

Stages 定义了 Gitlab 执行的阶段。执行时 Gitlab 会依次执行 stages 下的各个 stage。

Enviorments

Enviorment 可以定义不同的环境,例如 debug, qa, product 等,在 Operations/Environments 可以快速根据不同的环境查看 CI/CD 记录。

Tags

Tags 定义了 gitlab-runner 的 Tag。默认情况下只有 Job 定义的 Tag 与 Runner 的 Tag 相匹配,该 Runner 才会执行。也就是说 Job 执行时会找到所有匹配该 Job 下 Tags 的 Runner 并执行。

Job

Job 定义了一个任务,当发生构建时,Job 找到匹配的 Runner,并执行 Job 下定义的 script。

Pipeline

Pipeline 定义了一组 Job,通过 stages 的各个 stage 先找到对应的 Job,再通过 Job 下的 tags 找到对应的 runner,然后交由 runner 执行 Job 下的 script,这样顺序执行下去,一个流水线作业便完成了。

Gitlab CI/CD 的过程

1. 注册Runner。

首先,安装 Runner。

在 linux 下执行

1
2
3
4
5
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
sudo chmod +x /usr/local/bin/gitlab-runner
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash #具体的用户名可以根据自己需求修改
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

来安装 gitlab-runner(其他系统参考 Install GitLab Runner)。

然后执行注册。

在 settings/ci_cd 下的 Runner 中查看相关信息。

执行 sudo gitlab-runner register 之后填写相应的信息完成注册。

注册完成后可以在该页面看到绿色的 Runner正在运行中。

2. 编辑 gitlab-ci.yml

例如我的 golang 项目 qa 环境的自动发布流程。

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
before_script:
- export GOFLAGS=-mod=vendor
stages:
- test
- build
- deploy
format:
stage: test
script:
- go vet $(go list ./...)
tags:
- qa
compile:
stage: build
script:
- make build
tags:
- qa
deploy:
stage: deploy
script:
- make restart
tags:
- qa
when: always
environment:
name: qa
only:
- develop

3. 执行构建

将上边的文件提交到 gitlab 的 develop 分支,在默认的设置下会自动执行 format, compile, deploy 三个 Job。如此,一次构建便完成了。

如果需要关闭自动构建,可以把 when 下的 always 修改为 manual,这样就可以手动发布了。在 CI/CD / Pipelines 下点击 Run Pipeline 来手动执行构建。

结语

其实 Gitlab 的 CI/CD 还是比较容易理解的,我们更需要的是在使用 CI/CD 之前对 linux 比较熟稔、对自己项目的构建过程了然于心。如此,经过简单的学习便可以快速开始使用 Gitlab 的 CI/CD 进行构建,免去不规范的手动执行了。

在日常开发中,我们经常需要和 Json 打交道。大多数情况下接口返回我们的信息都是紧凑的 Json,例如

1
{"age":10,"name": "Bob"}

我们经常需要格式化成

1
2
3
4
{
"age": 10,
"name": "Bob"
}

来直观的获取结果。

之前我都是在 json.cn 上进行格式化。

image.png

但是这样非常麻烦,于是我开发了一个小工具,直接可以在命令行中格式化,并且会自动加上颜色。

image.png

我们平常经常会请求接口,可以这样

image.png
。通过这种方式再也不用打开浏览器格式化 Json 了。

项目地址:https://github.com/phpcyy/pson

如果你安装了 composer,可以直接通过

1
2
3
4
5
6
7
composer g require phpcyy/pson

# Format json from file.
~/.composer/vendor/bin/pson file.json

# Or from STDIN (standard input).
echo '{"message": "hello, 世界"}' | ~/.composer/vendor/bin/pson

把 pson 放入到环境变量中,就可以愉快地在直接使用 pson 命令在命令行中美化 Json 了。

对于默认的使用 http.ListenAndServe 的,直接使用

1
import _ "net/http/pprof"

因为 pprof 的 init 方法执行了如下操作

1
2
3
4
5
http.HandleFunc("/debug/pprof/", Index)
http.HandleFunc("/debug/pprof/cmdline", Cmdline)
http.HandleFunc("/debug/pprof/profile", Profile)
http.HandleFunc("/debug/pprof/symbol", Symbol)
http.HandleFunc("/debug/pprof/trace", Trace)

如果不是使用 DefaultServeMux(也就是说 http.ListenAndServe 函数的第二个参数不是 nil),可以在路由上使用下方的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
switch name {
case "cmdline":
pprof.Cmdline(ctx.Response, ctx.Request)

case "profile":
pprof.Profile(ctx.Response, ctx.Request)

case "symbol":
pprof.Symbol(ctx.Response, ctx.Request)

case "trace":
pprof.Trace(ctx.Response, ctx.Request)

default:
handler := pprof.Handler(name)
handler.ServeHTTP(ctx.Response, ctx.Request)
}

然后执行 go tool pprof -http=":8081" $host/debug/pprof/profile 即可。

执行该命令后会自动打开浏览器,通过切换 goroutines, heap, profile 可以分别查看 goroutines,堆栈信息和 cpu 占用情况。通过里边的各种图可以比较清晰地发现和排查性能问题。