0%

原因

在 15.9 版本中,gitlab ci 中的 job 无法在失败之后进行重试,表现为失败之后进入 pending 状态,一直持续。从而导致了在有时候 job 偶尔的失败需要手动去重试,非常不方便。有时候还会因为来不及手动重试会直接影响线上服务。

gitlab 15.9 使用的 docker 配置

gitlab 使用的是 docker 部署,docker-compose 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
gitlab:
container_name: gitlab
image: 'gitlab/gitlab-ce:15.9.3-ce.0'
restart: always
hostname: '192.168.2.168'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://192.168.2.168:8200'
pages_external_url 'http://192.168.2.168:8300'
gitlab_rails['gitlab_shell_ssh_port'] = 2222
ports:
- '8200:8200'
- '8300:8300'
- '2222:22'
volumes:
- '/usr/lnmp/gitlab/config:/etc/gitlab'
- '/usr/lnmp/gitlab/logs:/var/log/gitlab'
- '/usr/lnmp/gitlab/data:/var/opt/gitlab'
shm_size: '256m'

要解决的几个关键问题

因为是 docker 配置,所以本来打算直接用旧的文件夹来启动一个新版本的容器,不过在启动新版本的容器的时候起不来。 因为版本跨度过大,可能中间太多不兼容,最后决定起一个新的容器,然后把旧的数据手动迁移过去。

在本次迁移中,为了保证旧的用户密码在迁移后依然可用,直接使用了旧的配置文件来启动新的 gitlab 容器,也就是上面 docker-compose 配置中的 /user/lnmp/gitlab/config 这个文件夹。

gitlab 没有什么工具可以用来迁移数据的,所以比较麻烦。

在迁移过程中,有几个很关键的问题需要解决:

  1. 用户和组迁移
  2. 用户的 ssh key 迁移
  3. 项目迁移
  4. 用户权限迁移

用户权限这个没有什么好的办法,只能手动去设置。如果用户太多的话,可以看看 gitlab 有没有提供 API,使用它的 API 可能会方便一些。

其他的几个问题可以稍微简单一点,本文会详细介绍。

gitlab 获取 token 以及调用 API 的方法

我们需要使用管理员账号来创建 token,其他账号是没有权限的。

gitlab 提供了很多 REST API,可以通过这些 API 来获取到 gitlab 的数据,以及对 gitlab 进行操作。 我们的一些迁移操作就调用了它的 API 来简化操作,同时也可以避免人为操作导致的一些错误。

但是调用这些 API 之前,我们需要获取到一个 token,从 gitlab 个人设置中获取到 token,然后使用这个 token 来调用 API(Preferences -> Access Tokens,添加 token 的时候把所有权限勾上就好)。

通过 REST API 导出分组的示例:

1
curl --request POST --header "PRIVATE-TOKEN: glpat-L1efQKvKeWu" "http://192.168.2.168:8200/api/v4/groups/1/export"

说明:

  • PRIVATE-TOKEN header:就是我们在 gitlab 个人设置中获取到的 token。
  • /api/v4/groups/1/export:这个是 gitlab 的 API,可以通过这个 API 导出分组,这里的 1 是分组的 ID。

我们可以将这个 curl 命令通过使用自己熟悉的编程语言来调用,这样可以很方便地对获取到的数据进行后续操作。比如我在这个过程中就是使用 python 来调用 gitlab 的 API。

启动一个新的 gitlab 16.10 版本的容器

在开始迁移之前,需要先启动一个新的 gitlab 16.10 版本的容器,这个容器是全新的,没有任何数据。但配置文件是复制了一份旧的配置文件。新的 docker-compose.yml 配置文件如下:

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
38
39
40
41
42
43
gitlab:
container_name: gitlab
image: 'gitlab/gitlab-ce:15.9.3-ce.0'
restart: always
hostname: '192.168.2.168'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://192.168.2.168:8200'
pages_external_url 'http://192.168.2.168:8300'
gitlab_rails['gitlab_shell_ssh_port'] = 2222
ports:
- '8200:8200'
- '8300:8300'
- '2222:22'
volumes:
- '/usr/lnmp/gitlab/config:/etc/gitlab'
- '/usr/lnmp/gitlab/logs:/var/log/gitlab'
- '/usr/lnmp/gitlab/data:/var/opt/gitlab'
shm_size: '256m'
networks:
- elasticsearch

gitlab-new:
container_name: "gitlab-new"
image: 'gitlab/gitlab-ce:16.10.0-ce.0'
restart: always
hostname: '192.168.2.168'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://192.168.2.168:8211'
pages_external_url 'http://192.168.2.168:8311'
gitlab_rails['gitlab_shell_ssh_port'] = 2233
ports:
- '8211:8211'
- '8311:8311'
- '2233:22'
volumes:
- '/usr/lnmp/gitlab-new/config:/etc/gitlab'
- '/usr/lnmp/gitlab-new/logs:/var/log/gitlab'
- '/usr/lnmp/gitlab-new/data:/var/opt/gitlab'
shm_size: '256m'
networks:
- elasticsearch

在个配置文件中:

  1. 旧的配置保持不变
  2. 旧的配置目录复制了一份 /usr/lnmp/gitlab/config => /usr/lnmp/gitlab-new/config
  3. /usr/lnmp/gitlab-new 目录下只有 config 文件夹,没有 logsdata 文件夹,这两个文件夹会在容器启动过程中生成。

这样我们就可以在旧的 gitlab 运行过程中,先进行用户、组等数据的迁移。

用户迁移

Gitlab 提供了获取用户信息的 API,可以通过这个 API 获取到用户的信息(从旧的 gitlab),然后再通过 API 创建用户(在新的 gitlab)。

我们可以在 https://docs.gitlab.com/16.10/ee/api/users.html 查看 gitlab 的用户 API 详细文档。

我们会用到它的几个 API:

  1. 获取用户信息:GET /users:通过这个 API 我们可以获取到所有用户的信息(从旧的 gitlab)。
  2. 获取用户的 ssh key:GET /users/:id/keys:通过这个 API 我们可以获取到用户的 ssh key(从旧的 gitlab)。
  3. 创建用户:POST /users:通过这个 API 我们可以创建用户(到新的 gitlab)。
  4. 创建用户的 ssh key:POST /users/:id/keys:通过这个 API 我们可以创建用户的 ssh key(到新的 gitlab)。

从旧的 gitlab 获取用户信息

完整代码太长了,这里只放出关键代码:

1
2
3
# http://192.168.2.168:8200 是旧的 gitlab 地址
response = requests.get("http://192.168.2.168:8200/api/v4/users?per_page=100", headers={'Private-Token': tk})
users = response.json()

这里的 tk 是我们在 gitlab 个人设置中获取到的 token。

它返回的数据格式如下(users):

1
2
3
4
5
6
7
8
9
10
11
[
{
"id": 33,
"username": "x",
"name": "x",
"state": "deactivated",
"avatar_url": null,
"web_url": "http://192.168.2.168:8200/x"
// 其他字段....
}
]

从旧的 gitlab 获取用户的 ssh key

在上一步获取到所有的用户信息之后,我们可以通过用户的 ID 来获取用户的 ssh key。

1
2
3
4
# http://192.168.2.168:8200 是旧的 gitlab 地址
for user in users:
ssh_keys_response = requests.get(f"http://192.168.2.168:8200/api/v4/users/{user['id']}/keys", headers={'Private-Token': tk})
user['keys'] = ssh_keys_response.json()

这里的 user['keys'] 就是用户的 ssh key 信息,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
{
"user_id": 6,
"username": "ming",
"name": "ming",
"keys": [
{
"id": 24,
"title": "win7",
"created_at": "2023-04-10T03:15:54.428Z",
"expires_at": null,
"key": "ssh-rsa AAAAB3Nza...",
"usage_type": "auth_and_signing"
}
]
}
]

这一步,我们获取了用户的 ssh key 并关联到了 user 中了。

在新的 gitlab 上创建用户

在获取到了用户信息之后,我们就可以在新的 gitlab 上创建用户了。

1
2
3
4
5
6
7
8
9
10
11
12
# http://192.168.2.168:8211 是新的 gitlab 的地址
create_user_response = requests.post("http://192.168.2.168:8211/api/v4/users", headers={'Private-Token': tk_new}, json={
'username': user['username'],
"email": user['email'],
"name": user['name'],
"password": '12345678Git*',

'note': user['note'],
'bio': user['bio'],
'commit_email': user['commit_email'],
})
user['new_user'] = create_user_response.json()

这个接口会返回新创建的用户信息,格式如下,我们可以通过这个信息来获取到新创建的用户的 ID。

因为新旧 gitlab 的用户 ID 会不一样,所以这里需要获取新的 ID 来创建 ssh key。

1
2
3
4
5
6
7
8
9
10
{
"id": 2,
"username": "xx",
"name": "xx",
"state": "active",
"locked": false,
"avatar_url": "",
"web_url": "http://192.168.2.168:8211/",
// 其他字段
}

在新的 gitlab 上创建用户的 ssh key

创建了用户之后,我们就可以创建用户的 ssh key 了。

1
2
3
4
5
6
7
8
# http://192.168.2.168:8211 是新的 gitlab 的地址
for ssh_key in user['keys']:
create_keys_response = requests.post(
f"http://192.168.2.168:8211/api/v4/users/{user['new_user']['id']}/keys",
headers={'Private-Token': tk_new},
json={"title": ssh_key['title'],"key": ssh_key['key']}
)
print(create_keys_response.json())

到这里,我们就把用户迁移过来了。

新旧 gitlab 用户密码不一样的问题

在现代的应用中,密码一般会使用诸如 APP_KEY 这种应用独立的 key 来做一些加密,如果是不同的 key,那么加密出来的密码就会不一样。 出于这个考虑,我们在迁移的过程中,直接保留了旧的配置,里面包含了应用的一些 key,这样我们就可以直接从旧系统的数据库中提取出加密后的密码来使用, 因为加密的时候使用的 key 是一样的,所以放到新的系统中是可以直接使用的。

从旧的 gitlab 数据库中获取用户密码

  1. 进入 gitlab 容器内部:docker exec -it gitlab bash
  2. 进入 gitlab 数据库:gitlab-rails dbconsole
  3. 获取用户密码:select username, encrypted_password from users;

这会输出所有用户的用户名和加密后的密码,我们可以把这个密码直接放到新的 gitlab 中:

1
2
3
4
  username  |                      encrypted_password
------------+--------------------------------------------------------------
a | $2a$10$k1/Cj2qUCyWgwalCmzTFo.iqYgnqIoFmQVuT2S6mBuQF0Nql0CRGm
b | $2a$10$iWWXEcPJpyZVxfmIU6nv6e46JRj2dYnwGP6GXclryEAeEXJvOZ5aC

因为 username 是唯一的,所以我们可以通过 username 来找到新的 gitlab 中对应的用户(肉眼查找法),最后更新这个用户的 encrypted_password 字段即可。

当然,也可以复制出来做一些简单的文本处理:

1
2
3
4
5
6
7
8
9
10
11
12
text = """
a | $2a$10$k1/Cj2qUCyWgwalCmzTFo.iqYgnqIoFmQVuT2S6mBuQF0Nql0CRGm
b | $2a$10$iWWXEcPJpyZVxfmIU6nv6e46JRj2dYnwGP6GXclryEAeEXJvOZ5aC
"""

lines = text.strip().split("\n")
result = []
for line in lines:
kvs = [v.strip() for v in line.strip().split('|')]
result.append({kvs[0]: kvs[1]})

print(result)

这样我们就可以得到一个字典列表:

1
2
[{'a': '$2a$10$k1/Cj2qUCyWgwalCmzTFo.iqYgnqIoFmQVuT2S6mBuQF0Nql0CRGm'},
{'b': '$2a$10$iWWXEcPJpyZVxfmIU6nv6e46JRj2dYnwGP6GXclryEAeEXJvOZ5aC'}]

在新的 gitlab 中更新用户密码

同样地,需要进入新的 gitlab 容器内部,然后进入数据库,然后更新用户密码:

  1. 进入 gitlab 容器内部:docker exec -it gitlab-new bash
  2. 进入 gitlab 数据库:gitlab-rails dbconsole
  3. 获取用户密码:select id, username from users;

拿到上一步的字典后,我们可以通过 username 找到新的 gitlab 中对应的用户 ID,然后更新密码:

  1. 通过新的 gitlab 的用户 API 获取用户列表
  2. 循环这个用户列表,如果 username 在上一步的字典中,那么就更新这个用户的密码
1
2
3
4
# http://192.168.2.168:821 是新的 gitlab 地址
# tk_new 是新的 gitlab 上的 token
response = requests.get("http://192.168.2.168:8211/api/v4/users?per_page=100", headers={'Private-Token': tk_new})
users = response.json()

因为连接到 gitlab 的数据库又比较麻烦,所以这里只是生成了的 update 语句,然后手动在新的 gitlab 数据库中执行:

1
2
3
4
5
6
7
8
9
# users 是新 gitlab 中的用户
for user in users:
if user['username'] in result:
# 通过 username 找到旧的密码
encrypted_password = result[user['username']]
# 生成 update 语句
# 这个 update 语句会将新系统中的用户密码更新为旧系统中的密码
# 因为是使用相同的 key 加密的,所以迁移后也依然可以使用旧的密码来登录
print(f"update users set encrypted_password='{encrypted_password}', reset_password_token=null, reset_password_sent_at = null,confirmation_token=null,confirmation_sent_at=null where id={user['id']};")

这会生成一些 update 语句,我们可以复制出来在新的 gitlab 中执行(也就是本小节的 gitlab-rails dbconsole)。 到这一步,用户的迁移就算完成了。

Group 的迁移

Group 的迁移和用户的迁移类似,只是 Group 的 API 不同,我们可以在 https://docs.gitlab.com/ee/api/group_import_export.html 查看 gitlab 的 Group API 文档。

注意:如果我们的 Group 中除了项目,没什么东西的话,直接自己手动在 gitlab 上创建 Group,然后把项目迁移过去就好了。 使用它的 API 是因为它可以同时迁移:milestone、label、wiki、子 Group 等信息。

  1. 我们首先需要知道在旧的 gitlab 中的 Group ID,然后通过 API 导出 Group(旧的 gitlab)。
  2. 调用了导出的 API 之后,需要等待系统导出完成,然后下载导出的文件(旧的 gitlab)。
  3. 调用下载导出文件的 API,获取到导出的分组(旧的 gitlab)。
  4. 最后,调用导入分组的 API,将分组导入到新的 gitlab 中(新的 gitlab)。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 从旧的 gitlab 导出 Group,1 是 Group ID
curl --request POST --header "PRIVATE-TOKEN: glpat-abc" "http://192.168.2.168:8200/api/v4/groups/1/export"

# 下载导出的分组
# --output 指定下载的文件名
# `api/v4/groups/1/export/download` 中的 1 是 Group ID
curl --request GET \
--header "PRIVATE-TOKEN: glpat-abc" \
--output download_group_42.tar.gz \
"http://192.168.2.168:8200/api/v4/groups/1/export/download"

# 导入分组(在新的 gitlab)
# --form 指定 form 表单参数
# `name` 是新的 Group 名称(给人看的名称)
# `path` 是新的 Group 路径(体现在 git 仓库的路径上)
# `file` 是下载的文件(上一步导出的 Group 文件)
curl --request POST --header "PRIVATE-TOKEN: glpat-def" \
--form "name=g1" --form "path=g2" \
--form "file=@/Users/ruby/Code/devops/download_group_42.tar.gz" "http://192.168.2.168:8211/api/v4/groups/import"

这样就可以把 Group 迁移过来了。

项目迁移

项目的迁移没有找到什么好的方法,只能手动迁移了。

gitlab 的项目因为有很多分支、release、issue,所以不能只是简单地把 git 仓库拷贝过去就好了,还需要把这些信息也迁移过去。 这就需要将项目导出,然后导入到新的 gitlab 中。

具体操作步骤如下:

  1. 在项目主页的 Settings -> General -> Advanced -> Export project 中导出项目
  2. 点击导出之后,需要等待导出完成,然后下载导出的文件,还是在 Settings -> General -> Advanced -> Export project 中,点击下载
  3. 在新的 gitlab 中,点击 New Project,然后选择 Import project,选择上一步下载的文件,导入项目(导入的类型选择 Gitlab export
  4. 填写项目名,命名空间选择跟旧的 gitlab 一样的 Group,选择上一步下载的文件,然后点击 Import project,等待导入完成即可

权限迁移

这个也是得手动设置,没有什么好的办法。 也许有 API,但是用户少的时候,还是手动设置更快。

gitlab runner 迁移

这个也是看着旧的 gitlab runner 配置,手动配置一下就完了,没几个。

需要注意的是,gitlab runner 配置的 docker 类型的 runner 的时候,需要加上 pull_policy = ["if-not-present"],这样会在执行 job 的时候快很多,不然每次都会去拉取镜像。

1
2
3
4
[[runners]]
name = "docker-runner"
[runners.docker]
pull_policy = ["if-not-present"]

总结

最后,再回顾一下迁移过程的一些关键操作:

  1. 用户迁移:通过 API 从旧的 gitlab 获取用户信息、ssh key,然后在新的 gitlab 中通过 API 创建用户、创建 ssh key。
  2. 用户密码可以进入容器中使用 gitlab-rails dbconsole 来获取用户密码,然后在新的 gitlab 中更新用户密码。
  3. Group 迁移:通过 API 导出 Group,然后下载导出的文件,最后导入到新的 gitlab 中。
  4. 项目迁移:通过 gitlab 的项目导出、导入功能来迁移项目。这种迁移方式会保留项目的 issues、分支 等信息。

Filebeat 是一款功能强大的日志传送器,旨在简化从不同来源收集、处理和转发日志到不同目的地的过程。Filebeat 在开发时充分考虑了效率,可确保日志管理无缝且可靠。它的轻量级特性和处理大量数据的能力使其成为开发人员和系统管理员的首选。

在本综合指南中,您将深入探索 Filebeat 的功能。从基础知识开始,您将设置 Filebeat 以从各种来源收集日志。然后,您将深入研究高效处理这些日志的复杂性,从 Docker 容器收集日志并将其转发到不同的目标进行分析和监视。

前言

在上一篇文章《Filebeat vs Logstash:日志采集工具对比》中,我们对比了 Filebeat 和 Logstash 的一些优缺点,下面是一份简介版的总结:

我们可以看到,我们选择 Filebeat 一方面是因为它占用资源少,另外一方面是我们不需要对日志做复杂的处理,同时也不需要将日志发送到多个目的地。

环境准备

创建用以测试的目录:

1
2
mkdir log-processing-stack
cd log-processing-stack

为演示应用程序创建一个子目录并移动到该目录中:

1
mkdir logify && cd logify

完成这些步骤后,您可以在下一节中创建演示日志记录应用程序。

开发演示日志记录应用程序

在本节中,你将使用 Bash 脚本语言构建一个基本的日志记录应用程序。应用程序将定期生成日志,模拟应用程序生成日志数据的真实场景。

logify 目录中,创建一个名为 logify.sh 的文件,并将以下内容添加到文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
filepath="/var/log/logify/app.log"

create_log_entry() {
local info_messages=("Connected to database" "Task completed successfully" "Operation finished" "Initialized application")
local random_message=${info_messages[$RANDOM % ${#info_messages[@]}]}
local http_status_code=200
local ip_address="127.0.0.1"
local emailAddress="user@mail.com"
local level=30
local pid=$$
local time=$(date +%s)
local log='{"status": '$http_status_code', "ip": "'$ip_address'", "level": '$level', "emailAddress": "'$emailAddress'", "msg": "'$random_message'", "pid": '$pid', "timestamp": '$time'}'
echo "$log"
}

while true; do
log_record=$(create_log_entry)
echo "${log_record}" >> "${filepath}"
sleep 3
done

create_log_entry() 函数以 JSON 格式生成日志记录,包含严重性级别、消息、HTTP 状态代码和其他关键字段等基本详细信息。此外,它还包括敏感字段,例如电子邮件地址、和 IP 地址,这些字段是特意包含的,以展示 Filebeat 屏蔽字段中敏感数据的能力。

接下来,程序进入无限循环,重复调用 create_log_entry() 函数并将日志写入 /var/log/logify 目录中的指定文件。

添加完代码后,保存更改并使脚本可执行:

1
chmod +x logify.sh

然后,创建 /var/log/logify 用于存储应用程序日志的目录:

1
sudo mkdir /var/log/logify

接下来,使用 $USER 环境变量将 /var/log/logify 目录的所有权分配给当前登录的用户:

1
sudo chown -R $USER:$USER /var/log/logify/

在后台运行 logify.sh 脚本:

1
./logify.sh &

命令末尾的 & 符号指示脚本在后台运行,允许您在日志记录应用程序独立运行时继续使用终端执行其他任务。

当程序启动时,它将显示如下所示的输出:

1
[1] 91773

此处表示 91773 进程 ID,如果需要,该 ID 可用于稍后终止脚本。

若要查看 app.log 文件的内容,可以使用以下 tail 命令:

1
tail -n 4 /var/log/logify/app.log

此命令以 JSON 格式显示 app.log 文件中的最后 4 个日志条目:

1
2
3
4
{"status": 200, "ip": "127.0.0.1", "level": 30, "emailAddress": "user@mail.com", "msg": "Connected to database", "pid": 6512, "timestamp": 1709286422}
{"status": 200, "ip": "127.0.0.1", "level": 30, "emailAddress": "user@mail.com", "msg": "Initialized application", "pid": 6512, "timestamp": 1709286425}
{"status": 200, "ip": "127.0.0.1", "level": 30, "emailAddress": "user@mail.com", "msg": "Initialized application", "pid": 6512, "timestamp": 1709286428}
{"status": 200, "ip": "127.0.0.1", "level": 30, "emailAddress": "user@mail.com", "msg": "Operation finished", "pid": 6512, "timestamp": 1709286431}

现在,您已成功创建用于生成示例日志条目的日志记录应用程序。

安装 Filebeat

我的系统是 MacOS,所以执行下面的命令即可:

1
2
curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-8.12.2-darwin-x86_64.tar.gz
tar xzvf filebeat-8.12.2-darwin-x86_64.tar.gz

最后,进入 filebeat 目录下去执行 filebeat 命令:

1
2
cd filebeat-8.12.2-darwin-x86_64
./filebeat version

输出:

1
filebeat version 8.12.2 (amd64), libbeat 8.12.2 [0b71acf2d6b4cb6617bff980ed6caf0477905efa built 2024-02-15 13:39:16 +0000 UTC]

Filebeat 的工作原理

在开始使用 Filebeat 之前,了解其工作原理至关重要。在本节中,我们将探讨其基本组件和流程,确保您在深入研究实际使用之前有一个坚实的基础:

要了解 Filebeat 的工作原理,主要需要熟悉以下组件:

  • 收割机(Harvesters),收割机负责逐行读取文件的内容。当 Filebeat 配置为监控特定日志文件时,会为每个文件启动一个收集器。这些收割机不仅可以读取日志数据,还可以管理打开和关闭文件。通过逐行增量读取文件,收集器可确保有效地收集新附加的日志数据并转发以进行处理。
  • 输入(Inputs):输入充当收割机和数据源之间的桥梁。他们负责管理收割机并找到 Filebeat 需要从中读取日志数据的所有来源。可以为各种源(例如日志文件、容器或系统日志)配置输入。用户可以通过定义输入来指定 Filebeat 应监控的文件或位置。

Filebeat 读取日志数据后,日志事件将进行转换或使用数据进行扩充。最后发送到指定的目的地。

可以在 filebeat.yml 配置文件中指定以下行为:

我们可以在下载的目录中看到有一个 filebeat.yml 的配置文件。

1
2
3
4
5
6
filebeat.inputs:
. . .
processors:
. . .
output.plugin_name:
. . .

现在让我们详细研究每个部分:

  • filebeat.inputs:Filebeat 实例应监控的输入源。
  • processors:在将数据发送到输出之前对其进行扩充、修改或筛选。
  • output.plugin_name:Filebeat 应转发日志数据的输出目标。

这些指令中的每一个都要求您指定一个执行其相应任务的插件。

现在,让我们来探讨一些可以与 Filebeat 一起使用的输入、处理器和输出。

Filebeat 输入插件

Filebeat 提供了一系列输入插件,每个插件都经过定制,用于从特定来源收集日志数据:

  • container:收集容器日志。
  • filestream:主动从日志文件中读取行。
  • syslog:从 Syslog 中获取日志条目。
  • httpjson:从 RESTful API 读取日志消息。

Filebeat 输出插件

Filebeat 提供了多种输出插件,使您能够将收集的日志数据发送到不同的目的地:

  • File:将日志事件写入文件。
  • Elasticsearch:使 Filebeat 能够使用其 HTTP API 将日志转发到 Elasticsearch。
  • Kafka:将日志记录下发给 Apache Kafka。
  • Logstash:直接向 Logstash 发送日志。

Filebeat 模块插件

Filebeat 通过其模块简化日志处理,提供专为特定日志格式设计的预配置设置。这些模块使您能够毫不费力地引入、解析和丰富日志数据,而无需进行大量手动配置。以下是一些可以显著简化日志处理工作流程的可用模块:

  • Logstash
  • AWS
  • PostgreSQL
  • Nginx
  • RabbitMQ
  • HAproxy

Filebeat 入门

现在您已经了解了 Filebeat 的工作原理,让我们将其配置为从文件中读取日志条目并将其显示在控制台上。

首先,打开位于解压目录的 Filebeat 配置文件 filebeat.yml

1
vim filebeat.yml

接下来,清除文件的现有内容,并将其替换为以下代码:

1
2
3
4
5
6
7
filebeat.inputs:
- type: log
paths:
- /var/log/logify/app.log

output.console:
pretty: true

在本节 filebeat.inputs 中,您指定 Filebeat 应使用 logs 插件从文件中读取日志。paths 参数指示 Filebeat 将监控的日志文件的路径,此处设置为 /var/log/logify/app.log

output.console 部分将收集到的日志数据发送到控制台。pretty: true 参数可确保日志条目在控制台上显示时以可读且结构良好的格式显示。

添加这些配置后,保存文件。

在执行 Filebeat 之前,必须验证配置文件语法以识别和纠正任何错误:

1
./filebeat -c ./filebeat.yml test config

如果配置文件正确,则应看到以下输出:

1
Config OK

现在,继续运行 Filebeat:

1
./filebeat -c ./filebeat.yml

当 Filebeat 开始运行时,它将显示类似于以下内容的日志条目:

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
{
"@timestamp": "2024-03-02T01:35:34.696Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "8.12.2"
},
"log": {
"offset": 2875279,
"file": {
"path": "/var/log/logify/app.log"
}
},
"message": "{\"status\": 200, \"ip\": \"127.0.0.1\", \"level\": 30, \"emailAddress\": \"user@mail.com\", \"msg\": \"Task completed successfully\", \"pid\": 6512, \"timestamp\": 1709343333}",
"input": {
"type": "log"
},
"ecs": {
"version": "8.0.0"
},
"host": {
"name": "rubys-iMac.local"
},
"agent": {
"version": "8.12.2",
"ephemeral_id": "310a7b92-f2fb-42ca-b3d8-e32e348c7a57",
"id": "f177dd40-1249-487c-b9da-aaab03cfd05c",
"name": "rubys-iMac.local",
"type": "filebeat"
}
}

Filebeat 现在在控制台中显示日志消息。Bash 脚本中的日志事件位于 message 字段下,Filebeat 添加了其他字段以提供上下文。您现在可以通过按 CTRL + C 停止 Filebeat。

成功配置 Filebeat 以读取日志并将其转发到控制台后,下一节将重点介绍数据转换。

使用 Filebeat 转换日志

当 Filebeat 收集数据时,您可以在将其发送到输出之前对其进行处理。您可以使用新字段来丰富它,解析数据,以及删除或编辑敏感字段以确保数据隐私。

在本部分中,你将通过以下方式转换日志:

  • 解析 JSON 日志。
  • 删除不需要的字段。
  • 添加新字段。
  • 屏蔽敏感数据。

使用 Filebeat 解析 JSON 日志

由于演示日志记录应用程序以 JSON 格式生成日志,因此必须正确解析它们以进行结构化分析。

让我们检查上一节中的示例日志事件:

1
2
3
4
5
6
...
"message": "{\"status\": 200, \"ip\": \"127.0.0.1\", \"level\": 30, \"emailAddress\": \"user@mail.com\", \"msg\": \"Task completed successfully\", \"pid\": 6512, \"timestamp\": 1709343333}",
"input": {
"type": "log"
},
...

要将日志事件解析为有效的 JSON,请打开 Filebeat 配置文件:

1
vim filebeat.yml

然后,使用以下代码行更新文件:

1
2
3
4
5
6
7
8
9
10
11
12
filebeat.inputs:
- type: log
paths:
- /var/log/logify/app.log

processors:
- decode_json_fields:
fields: ["message"]
target: ""

output.console:
pretty: true

在上面的代码片段中,您将处理器配置为 decode_json_fields 解码每个日志条目 message 字段中的 JSON 编码数据,并将其附加到日志事件。

保存并退出文件。使用以下命令重新运行 Filebeat:

1
./filebeat -c ./filebeat.yml

如果配置文件正确,则应看到以下输出:

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
38
{
"@timestamp": "2024-03-02T01:43:07.367Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "8.12.2"
},
"status": 200,
"ip": "127.0.0.1",
"level": 30,
"message": "{\"status\": 200, \"ip\": \"127.0.0.1\", \"level\": 30, \"emailAddress\": \"user@mail.com\", \"msg\": \"Initialized application\", \"pid\": 6512, \"timestamp\": 1709343785}",
"ecs": {
"version": "8.0.0"
},
"host": {
"name": "rubys-iMac.local"
},
"agent": {
"type": "filebeat",
"version": "8.12.2",
"ephemeral_id": "f33a1fc6-e8f1-4dde-8740-5f85a1e8bcfd",
"id": "f177dd40-1249-487c-b9da-aaab03cfd05c",
"name": "rubys-iMac.local"
},
"pid": 6512,
"timestamp": 1709343785,
"log": {
"offset": 2898143,
"file": {
"path": "/var/log/logify/app.log"
}
},
"input": {
"type": "log"
},
"emailAddress": "user@mail.com",
"msg": "Initialized application"
}

在输出中,您将看到 message 字段中的所有属性(如 msgip 等)都已添加到日志事件中。

现在,您可以解析 JSON 日志,您将修改日志事件的属性。

使用 Filebeat 添加和删除字段

日志事件包含需要保护的敏感 emailAddress 字段。在本部分中,你将删除该 emailAddress 字段,并向日志事件添加一个新字段,以提供更多上下文。

打开 Filebeat 配置文件:

1
vim filebeat.yml

添加以下行以修改日志事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
filebeat.inputs:
- type: log
paths:
- /var/log/logify/app.log

processors:
- decode_json_fields:
fields: ["message"]
target: ""
- drop_fields:
fields: ["emailAddress", "message"]

- add_fields:
fields:
env: "environment" # Add a new 'env' field set to "development"
output.console:
pretty: true

若要修改日志事件,请添加 drop_fields 处理器,该处理器具有一个 field 选项,用于获取要删除的字段列表,包括敏感 EmailAddress 字段和 message 字段。删除 message 字段是因为在分析数据后,message 字段的属性已合并到日志事件中,从而使原始 message 字段过时。

编写代码后,保存并退出文件。然后,重新启动 Filebeat:

1
./filebeat -c ./filebeat.yml

运行 Filebeat 时,您会注意到该 emailAddress 字段已被成功删除,并且已将一个新 env 字段添加到日志事件中:

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
38
39
{
"@timestamp": "2024-03-02T01:47:33.907Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "8.12.2"
},
"ip": "127.0.0.1",
"level": 30,
"log": {
"offset": 2911714,
"file": {
"path": "/var/log/logify/app.log"
}
},
"ecs": {
"version": "8.0.0"
},
"msg": "Operation finished",
"pid": 6512,
"timestamp": 1709344053,
"status": 200,
"fields": {
"env": "environment"
},
"input": {
"type": "log"
},
"host": {
"name": "rubys-iMac.local"
},
"agent": {
"version": "8.12.2",
"ephemeral_id": "607c49c8-9339-4851-b8e6-caab3bf6138b",
"id": "f177dd40-1249-487c-b9da-aaab03cfd05c",
"name": "rubys-iMac.local",
"type": "filebeat"
}
}

现在,您可以扩充和删除不需要的字段,接下来将编写条件语句。

在 Filebeat 中使用条件语句

Filebeat 允许您检查条件,并在条件计算结果为 true 时添加字段。在本节中,您将检查该 status 值是否等于 true ,如果满足条件,您将向日志事件添加一个 is_successful 字段。

为此,请打开配置文件:

1
vim filebeat.yml

之后,添加突出显示的行以根据指定条件添加 is_successful 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
processors:
- decode_json_fields:
fields: ["message"]
target: ""
- drop_fields:
fields: ["emailAddress"] # Remove the 'emailAddress' field

- add_fields:
fields:
env: "environment" # Add a new 'env' field set to "development"
# 如果 status 字段的值为 200,则添加 is_successful 字段
- add_fields:
when:
equals:
status: 200
target: ""
fields:
is_successful: true
...

when 选项检查 status 字段值是否等于 200。如果为 true,则将该 is_successful 字段添加到日志事件中。

保存新更改后,启动 Filebeat:

1
./filebeat -c ./filebeat.yml

Filebeat 将生成与此内容密切相关的输出:

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
38
39
40
{
"@timestamp": "2024-03-02T01:50:31.697Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "8.12.2"
},
"status": 200,
"ip": "127.0.0.1",
"level": 30,
"ecs": {
"version": "8.0.0"
},
"fields": {
"env": "environment"
},
"input": {
"type": "log"
},
"host": {
"name": "rubys-iMac.local"
},
"agent": {
"version": "8.12.2",
"ephemeral_id": "20cca3c1-6ba3-4a78-8e0e-cb07bdb885f1",
"id": "f177dd40-1249-487c-b9da-aaab03cfd05c",
"name": "rubys-iMac.local",
"type": "filebeat"
},
"is_successful": true,
"msg": "Task completed successfully",
"pid": 6512,
"log": {
"file": {
"path": "/var/log/logify/app.log"
},
"offset": 2920724
},
"timestamp": 1709344231
}

在输出中,该 is_successful 字段已添加到日志条目中,HTTP 状态代码为 200

这负责根据条件添加新字段。

使用 Filebeat 编辑敏感数据

在本文前面,您删除了 emailAddress 字段以确保数据隐私。但是,IP 地址敏感字段仍保留在日志事件中。此外,组织内的其他开发人员可能会无意中将敏感数据添加到日志事件中。通过编辑与特定模式匹配的数据,您可以屏蔽任何敏感信息,而无需删除整个字段,从而确保保留消息的重要性。

在文本编辑器中,打开 Filebeat 配置文件:

1
vim filebeat.yml

添加以下代码以编辑 IP 地址:

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
...
processors:
- script:
lang: javascript
id: redact-sensitive-info
source: |
function process(event) {
// Redact IP addresses (e.g., 192.168.1.1) from the "message" field
event.Put("message", event.Get("message").replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, "[REDACTED-IP]"));
}
- decode_json_fields:
fields: ["message"]
target: ""
- drop_fields:
fields: ["emailAddress"] # Remove the 'emailAddress' field

- add_fields:
fields:
env: "environment" # Add a new 'env' field set to "development"
- add_fields:
when:
equals:
status: 200
target: ""
fields:
is_successful
...

在添加的代码中,您将定义一个用 JavaScript 编写的脚本,用于编辑日志事件中的敏感信息。该脚本使用正则表达式来标识 IP 地址,并分别将它替换为 [REDACTED-IP]

添加代码后,运行 Filebeat:

1
./filebeat -c ./filebeat.yml

输出:

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
38
39
40
{
"@timestamp": "2024-03-02T01:53:50.792Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "8.12.2"
},
"log": {
"offset": 2930777,
"file": {
"path": "/var/log/logify/app.log"
}
},
"pid": 6512,
"status": 200,
"fields": {
"env": "environment"
},
"msg": "Initialized application",
"is_successful": true,
"level": 30,
"input": {
"type": "log"
},
"ecs": {
"version": "8.0.0"
},
"host": {
"name": "rubys-iMac.local"
},
"agent": {
"id": "f177dd40-1249-487c-b9da-aaab03cfd05c",
"name": "rubys-iMac.local",
"type": "filebeat",
"version": "8.12.2",
"ephemeral_id": "8bf595e9-0376-42ec-be51-42a104527449"
},
"timestamp": 1709344429,
"ip": "[REDACTED-IP]"
}

输出中的日志事件现在将 IP 地址替换为 [REDACTED-IP]

注意:上面的脚本会将 message 中的所有 IP 地址都替换。

您现在可以停止 Filebeat 和 logify.sh 程序。

若要停止 bash 脚本,请获取进程 ID:

1
jobs -l | grep "logify"

输出:

1
[1]  + 6512 running    ./logify.sh

替换 kill 命令中的进程 ID:

1
kill -9 <6512>

成功编辑敏感字段后,您现在可以使用 Filebeat 从 Docker 容器收集日志,并将它们集中起来以进行进一步的分析和监控。

总结

在本文中,我们介绍了 Filebeat 的基本概念和工作原理。我们还演示了如何配置 Filebeat 以收集日志,并使用处理器对日志事件进行转换。我们还讨论了 Filebeat 的输入、输出和模块插件,以及如何使用条件语句和脚本处理器来编辑日志事件。

Filebeat 和 Logstash 均由 Elastic 开发,是 Elastic Stack 不可或缺的组件,它们都充当日志收集器,具有不同的特性和功能。Logstash 是 ELK Stack(Elasticsearch、Logstash、Kibana)的原始组件,旨在高效地从多个来源收集大量日志并将其分发到不同的目的地。

虽然 Logstash 越来越受欢迎,但由于资源密集型操作,尤其是在资源有限的系统上,它面临着挑战。为了解决这个问题,Elastic 推出了 Filebeat 作为 Beats 系列的一部分,为 Logstash 提供了轻量级的替代品。这一新增导致 ELK Stack 更名为 Elastic Stack,以更好地涵盖包括 Beats 在内的不断增长的工具套件。Filebeat 旨在补充 Logstash,但随着时间的推移演变成一个独立的日志收集器。

因此,鉴于这两个日志收集器的独特优势和功能,在这两个日志收集器之间做出决定可能是一项艰巨的任务。本文将比较 Filebeat 和 Logstash,探讨它们的优缺点,并提供有关何时选择其中一种的见解。

什么是 Filebeat?

Filebeat 是由 Elastic 开发的免费开源日志收集器,是 Elastic Stack 中 beats 系列的一部分。这套工具对于收集和传送各种类型的数据(如日志、指标和网络信息)至关重要。Filebeat 最初主要设计用于 Logstash,但随着时间的推移,随着 Elastic 对其日志处理能力的不断更新,Filebeat 已经超越了这一点。

此外,Filebeat 具有各种内置输入和输出,可满足不同的来源和目标。如果这些内置选项不符合特定要求,它还允许创建自定义插件。Filebeat 还包括内部模块,用于从广泛使用的工具(如 NGINX、Apache、系统日志和 MySQL)收集和解析日志。

Filebeat 因其轻量级设计、可靠性和稳健性而受到重视。它支持加密数据传输,并使用背压敏感协议,这在处理大量数据时很有用。此功能使其能够调整其数据传输速率,防止目的地过载。

什么是 Logstash?

Logstash 是由 Elastic 创建的免费开源数据管道工具。它旨在有效地收集、处理日志并将其发送到各种目标。Logstash 以其灵活性而闻名,提供多个输入、过滤器和输出,使其能够适应不同的日志处理需求。它擅长过滤、解析和转换日志,提供高级日志处理能力。

作为 Elastic Stack 的关键组件,Logstash 可与 Beats、Elasticsearch 和 Kibana 等其他工具无缝协作。它从各种来源提取数据并将其推送到 Elasticsearch,然后转发到 Kibana 进行分析和可视化。

Logstash 拥有超过 200 个插件,并提供了一个 API,用于创建满足特定用户需求的自定义插件。Logstash 的一个显著特点是其可靠性,它由一个持久队列支撑,该队列在传输日志事件时保存日志事件。在事件发送失败的情况下,Logstash 可以将其重新路由到另一个队列进行进一步检查和重新处理。

现在我们已经对 Filebeat 和 Logstash 有了基本的了解,让我们来比较一下这两个工具。我们的比较将重点关注以下关键标准:

特征 Filebeat Logstash
支持的平台 跨平台 跨平台
内存使用率/性能 轻量级 利用大量内存
生态系统和插件 少于 60 个插件 超过 200 个插件
日志解析 具有内置解析器和模块 具有更强大的解析器
事件路由 不支持 使用条件语句路由日志
传输 缓冲日志事件并具有持久性队列 缓冲日志事件并具有持久性队列
UI & UX 没有 UI,但可以与 Kibana 集成 没有 UI,但可以与 Kibana 集成

1. 支持的平台

Filebeat 是使用 Go 开发的,Go 是一种以创建高性能网络和基础设施程序而闻名的现代语言。其设计允许 Filebeat 以较低的内存占用量收集、处理和转发日志。此外,它的兼容性跨越各种平台,包括 Linux、Windows、MacOS,甚至容器化环境。

另一方面,Logstash 是使用 JRuby 构建的,JRuby 是 Java 中 Ruby 编程语言的高性能实现。要运行 Logstash,需要 Java 虚拟机 (JVM)。JVM 的跨平台兼容性确保 Logstash 可以在各种系统上运行,包括 Linux、Windows 和 MacOS。这使得 Logstash 在平台支持方面同样通用。

考虑到支持的平台,Filebeat 和 Logstash 都表现出很强的适应性,使其成为跨平台支持的绝佳选择。

2. 内存使用率/性能:Filebeat 获胜

Filebeat 旨在专注于轻量级效率,使其能够处理大量数据,同时保持最小的内存消耗。Filebeat 的单个实例通常使用不到 2MB 的内存,并使用不到 30% 的 CPU。如前所述,这种非凡的效率主要归功于它在 Go 中的发展。

此外,Filebeat 还具有负载平衡和故障转移功能,这对于确保一致的日志数据检索和转发至关重要,尤其是在高流量场景中。

相比之下,Logstash 需要的内存占用要高得多。根据官方文档,为了有效运行,它需要一个至少具有 2GB 内存的主机,建议的内存分配约为 4GB。这种需求的增加源于 Logstash 对 Java 虚拟机 (JVM) 及其复杂的日志处理功能的依赖,这自然会消耗更多的资源。虽然 Logstash 确实包含负载平衡和故障转移功能以实现可靠的日志处理,但其较高的资源要求使其不太适合内存和 CPU 使用率是关键限制的环境。

总之,对于性能和可扩展性至关重要且内存使用率至关重要的环境,Filebeat 是更有利的选择,因为它具有更低的内存占用和高效的资源利用率。

3. 生态系统和插件:Logstash 获胜

Filebeat 专注于轻量级和高效,仍然提供包含 60 多个插件的大量库。这些插件涵盖了各种输入和输出,包括 AWS S3、Kafka、Redis 和 File。这些插件的全面详细信息可以在文档中的详细输入和输出页面上找到。对于那些熟练使用 Go 的人来说,Filebeat 还允许创建自定义插件,为特定用例提供灵活性。此外,Filebeat 的模块简化了从 MySQL 或 Nginx 等流行工具读取日志的过程,增强了其易用性。

另一方面,Logstash 拥有丰富的插件生态系统,数量超过 200 个,分为输入、输出、过滤器和编解码器。这些插件大多是内置的,构成了 Logstash 高级日志处理能力的基础。广泛使用的插件包括 Grok 过滤器插件,它使用正则表达式解析日志,以及 GeoIP 过滤器,它为 IP 地址生成地理信息。

为 Logstash 创建自定义插件非常简单,官方文档提供了全面的指导。

由于其广泛的插件生态系统,Logstash 在需要高级数据处理的场景中成为明显的赢家。

4. 日志解析:Logstash 获胜

在日志解析方面,Filebeat 和 Logstash 都展示了高级日志解析功能,集成了能够处理结构化和非结构化日志的内置插件。

Filebeat 通过使用可用的处理器增强了其解析功能,能够解析 JSON 和 CSV 等标准格式。此外,内部模块使 Filebeat 能够解析来自 Nginx、MySQL 或 Apache 等来源的流行格式。

同样,Logstash 提供了强大的解析器和内置功能。Logstash 的与众不同之处在于包含 Grok,它使用正则表达式将日志事件与特定模式进行匹配。Grok 具有 200 多种预定义模式,能够解析来自不同来源(如 MongoDB、Postgres 和 AWS)的结构化和非结构化日志,并灵活地定义用于解析自定义日志格式的模式。

在日志解析方面,Filebeat 适合处理标准日志格式,而 Logstash 则凭借强大的解析功能脱颖而出。

5. 事件路由:Logstash 获胜

事件路由是一个过程,其中日志事件根据条件或每个日志事件中的内容定向到特定目标。这意味着您可以设置规则,根据每个日志包含的数据确定应将每个日志发送到何处。例如,您可以将日志收集器配置为将 HTTP 状态代码为 200 的所有日志事件发送到远程位置,而将状态代码为 400 的日志事件写入特定文件。

Filebeat 虽然在日志收集方面很高效,但并非设计为事件路由。它通常会将所有收集的日志转发到单个端点(通常是 Logstash),这与其原始设计目的一致。因此,在需要将日志从多个来源分发到不同目标的场景中,Filebeat 的功能会受到限制。

另一方面,Logstash 在事件路由方面表现出卓越的熟练程度。它支持使用条件语句(如 if-then-else )将日志事件路由到多个目标。这些条件允许 Logstash 根据特定条件评估日志事件并相应地指导它们。

鉴于事件路由在根据特定需求定制日志分发方面的重要性,Logstash 在这方面的表现优于 Filebeat。

6. 传输

在传输数据方面,Filebeat 和 Logstash 不相上下,它们都提供有效的输出插件,用于从各种来源收集日志并将其传送到多个目的地,包括云存储、Kafka、AWS 和本地文件。

这两种工具的一个关键特性是它们使用内存中队列来缓冲日志事件。此功能对于管理日志数据中的峰值和临时存储日志以防止数据输出过载至关重要。内存中队列在重新发送可能由于输出目标问题而无法传输的日志事件方面也发挥着作用。

在意外中断或过早退出时,存在丢失存储在这些内存中队列中的日志事件的风险。但是,Filebeat 和 Logstash 通过提供为持久性数据存储配置磁盘队列的选项来降低这种风险。这种额外的弹性层确保了工具可以无缝地恢复其操作,在突然关闭时从中断的地方继续运行。

需要注意的是,尽管 Logstash 和 Filebeat 具有持久队列,但偶尔会发生故障,尤其是在处理大量数据时。为了缓解这种情况,建议使用专用工具(如 Kafka)从日志收集器中卸载负载。该工具充当临时数据持久层,为日志管理基础结构增加了额外的弹性和稳定性。

考虑到它们在数据传输方面的能力,Filebeat 和 Logstash 都比对方具有明显的优势。两者都可以可靠地传输和持久化数据。

什么时候应该使用 Filebeat 或 Logstash ?

在 Filebeat 和 Logstash 之间做出决定取决于您的特定日志管理需求。

当您的主要需求是从各种来源收集日志并将其定向到 Elasticsearch 或云存储等单一目标时,Filebeat 是一个理想的选择。其他理想的情况是,您的日志需要基本解析,例如处理 Nginx 日志或管理不需要复杂处理的 syslog 输入。

另一方面,如果您正在处理需要复杂操作、扩充或过滤的半结构化或非结构化日志,那么 Logstash 更合适。当您的目标是将日志事件路由到多个目标时,它也是首选工具。

更有效的策略是结合使用 Filebeat 和 Logstash,特别是用于管理来自不同来源的日志。通常,由于 Filebeat 的效率,您可以使用 Filebeat 来收集日志。然后,收集的日志将转发到 Logstash 进行更复杂的处理。Logstash 可以配置为将日志分发到不同的目的地,或者将它们转发到 Elasticsearch 进行索引,然后转发到 Kibana 进行可视化。这种组合方法有效地利用了 Filebeat 和 Logstash 的优势。

有关更具可伸缩性的设置,请参阅下图:

实例

让我们看一个使用 Elastic Stack 的实际示例,并考虑 Filebeat 和 Logstash 如何协同工作来管理和处理日志。

假设您有以下格式的 Nginx 日志:

1
203.0.113.1 - - [14/Jan/2022:08:30:45 +0000] "GET /example-page HTTP/1.1" 200 1024 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"

以下是使用 Filebeat 和 Logstash 处理这些日志的方法:

  1. 使用 Filebeat:首先,使用 Filebeat 从 Nginx 收集日志。Filebeat 可以配置 Nginx 模块,该模块旨在解析 Nginx 日志。
  2. 转发到 Logstash:收集和初始解析后,配置 Filebeat 将这些日志发送到 Logstash。
  3. 在 Logstash 中处理:在 Logstash 中,您可以对日志进行高级操作。这包括提取特定字段、设置日期格式、编辑 IP 地址、重命名字段和其他所需的转换。
  4. 发送到 Elasticsearch 和 Kibana:处理完成后,Logstash 会将日志转发到 Elasticsearch 进行索引。索引后,可以在 Kibana 中对数据进行可视化和分析。

此工作流程演示了 Filebeat 和 Logstash 如何无缝协作。此设置对于处理需要简单和复杂处理和可视化过程的日志非常方便。

结论

在本文中,我们比较了 Filebeat 和 Logstash,展示了每种工具如何满足不同的需求。

Filebeat 是一个轻量级选项,非常适合资源有限且需要基本日志解析的环境。相反,Logstash 是为需要高级日志处理的场景量身定制的。

同时使用这两种工具也可以具有战略意义,以获得两全其美的优势。使用 Filebeat 收集日志并将其转发到 Logstash 进行高级转换提供了一种平衡的方法。

什么是分布式锁?

分布式锁是一种在分布式系统中用于控制并发访问的机制。在分布式系统中,多个客户端可能会同时对同一个资源进行访问,这可能导致数据不一致的问题。分布式锁的作用是确保同一时刻只有一个客户端能够对某个资源进行访问,从而避免数据不一致的问题。

分布式锁的实现通常依赖于一些具有分布式特性的技术,如 ZooKeeperRedis、数据库等。这些技术提供了在分布式环境中实现互斥访问的机制,使得多个客户端在竞争同一个资源时能够有序地进行访问。

通过使用分布式锁,可以确保分布式系统中的数据一致性和并发访问的有序性,从而提高系统的可靠性和稳定性。

Zookeeper 与 Redis 的分布式锁对比

ZooKeeperRedis 都是常用的实现分布式锁的工具,但它们在实现方式、特性、适用场景等方面有一些区别。以下是 ZooKeeper 分布式锁与 Redis 分布式锁的比较:

实现方式

  • ZooKeeper 分布式锁主要依赖于其临时节点和顺序节点的特性。客户端在 ZooKeeper 中创建临时顺序节点,并通过监听机制来实现锁的获取和释放。
  • Redis 分布式锁通常使用 SETNX(set if not exists) 命令来尝试设置一个 key,如果设置成功则获取到锁。也可以通过设置过期时间和轮询机制来防止死锁和提高锁的可靠性。

特性

  • ZooKeeper 分布式锁具有严格的顺序性和公平性,保证了锁的获取顺序与请求顺序一致,避免了饥饿问题。
  • Redis 分布式锁的性能通常更高,因为它是一个内存数据库,读写速度非常快。然而,它可能存在不公平性和死锁的风险,需要额外的机制来避免这些问题。

适用场景

  • ZooKeeper 分布式锁适用于对顺序性和公平性要求较高的场景,如分布式调度系统、分布式事务等。
  • Redis 分布式锁适用于对性能要求较高的场景,如缓存系统、高并发访问的系统等。Redis 的高性能使得它在处理大量并发请求时具有优势。

可靠性

  • ZooKeeper 分布式锁具有较高的可靠性,因为它依赖于 ZooKeeper 的高可用性和强一致性保证。即使部分节点宕机,ZooKeeper 也能保证锁的正确性和一致性。
  • Redis 分布式锁的可靠性取决于其实现方式和配置。在某些情况下,如 Redis 节点宕机或网络故障,可能会导致锁失效或死锁。因此,需要合理配置 Redis 和采取额外的措施来提高锁的可靠性。

综上所述,ZooKeeper 分布式锁和 Redis 分布式锁各有优缺点,具体选择哪种方式取决于实际业务场景和需求。在需要保证顺序性和公平性的场景下,ZooKeeper 分布式锁可能更适合;而在需要高性能和快速响应的场景下,Redis 分布式锁可能更合适。

为什么 Zookeeper 可以实现分布式锁

ZooKeeper 可以实现分布式锁,主要得益于其以下几个特性:

  1. 临时节点:ZooKeeper 支持创建临时节点,这些节点在创建它们的客户端会话结束时会被自动删除。这种特性使得 ZooKeeper 的节点具有生命周期,可以随着客户端的存活而存在,客户端断开连接后自动消失,非常适合作为锁的标识。
  2. 顺序节点:ZooKeeper 的另一个重要特性是支持创建顺序节点。在创建节点时,ZooKeeper 会在节点名称后自动添加一个自增的数字,确保节点在 ZNode 中的顺序性。这个特性使得 ZooKeeper 可以实现分布式锁中的公平锁,按照请求的顺序分配锁。
  3. Watcher 机制:ZooKeeper 还提供了 Watcher 机制,允许客户端在指定的节点上注册监听事件。当这些事件触发时,ZooKeeper 服务端会将事件通知到感兴趣的客户端,从而允许客户端做出相应的措施。这种机制使得 ZooKeeper 的分布式锁可以实现阻塞锁,即当客户端尝试获取已经被其他客户端持有的锁时,它可以等待锁被释放。

基于以上特性,ZooKeeper 可以实现分布式锁。具体实现流程如下:

  1. 客户端需要获取锁时,在 ZooKeeper 中创建一个临时顺序节点作为锁标识。
  2. 客户端判断自己创建的节点是否是所有临时顺序节点中序号最小的。如果是,则客户端获得锁;如果不是,则客户端监听序号比它小的那个节点。
  3. 当被监听的节点被删除时(即持有锁的客户端释放锁),监听者会收到通知,然后重新判断自己是否获得锁。
  4. 当客户端释放锁时,只需要将会话关闭,临时节点就会被自动删除,从而释放了锁。

因此,ZooKeeper 通过其临时节点、顺序节点和 Watcher 机制等特性,实现了分布式锁的功能。

使用 Golang 实现 Zookeeper 分布式锁

下面我们通过一个简单的例子来演示如何使用 Golang 实现 ZooKeeper 分布式锁。

创建 zookeeper 客户端连接

1
2
3
4
5
6
7
8
9
10
import "github.com/go-zookeeper/zk"

func client() *zk.Conn {
// 默认端口 2181
c, _, err := zk.Connect([]string{"192.168.2.168"}, time.Second)
if err != nil {
panic(err)
}
return c
}

创建父节点 - /lock

我们可以在获取锁之前,先创建一个父节点,用于存放锁节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Lock struct {
c *zk.Conn
}

// 父节点 /lock 不存在的时候进行创建
func NewLock() *Lock {
c := client()
e, _, err := c.Exists("/lock")
if err != nil {
panic(err)
}
if !e {
_, err := c.Create("/lock", []byte(""), 0, zk.WorldACL(zk.PermAll))
if err != nil {
panic(err)
}
}

return &Lock{c: c}
}

获取锁

在 Zookeeper 分布式锁实现中,获取锁的过程实际上就是创建一个临时顺序节点,并判断自己是否是所有临时顺序节点中序号最小的。

获取锁的关键是:

  1. 创建的需要是临时节点
  2. 创建的需要是顺序节点

具体创建代码如下:

1
p, err := l.c.Create("/lock/lock", []byte(""), zk.FlagEphemeral|zk.FlagSequence, zk.WorldACL(zk.PermAll))

其中 zk.FlagEphemeral 表示创建的是临时节点,zk.FlagSequence 表示创建的是顺序节点。

判断当前创建的节点是否是最小节点

具体步骤如下:

  1. 通过 l.c.Children("/lock") 获取 /lock 下的所有子节点
  2. 对所有子节点进行排序
  3. 判断当前创建的节点是否是最小节点
  4. 如果是最小节点,则获取到锁,函数调用返回;如果不是,则监听前一个节点(这会导致函数调用阻塞)
1
2
3
4
5
6
7
8
9
10
11
12
13
childs, _, err := l.c.Children("/lock")
if err != nil {
return "", err
}

// childs 是无序的,所以需要排序,以便找到当前节点的前一个节点,然后监听前一个节点
sort.Strings(childs)

// 成功获取到锁
p1 := strings.Replace(p, "/lock/", "", 1)
if childs[0] == p1 {
return p, nil
}

不是最小节点,监听前一个节点

具体步骤如下:

  1. 通过 sort.SearchStrings 找到当前节点在所有子节点中的位置
  2. 调用 l.c.ExistsW 判断前一个节点是否依然存在(锁有可能在调用 ExistsW 之前已经被释放了),如果不存在则获取到锁
  3. 如果前一个节点依然存在,则阻塞等待前一个节点被删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 监听锁,等待锁释放
// 也就是说,如果当前节点不是最小的节点,那么就监听前一个节点
// 一旦前一个节点被删除,那么就可以获取到锁
index := sort.SearchStrings(childs, p1)
b, _, ev, err := l.c.ExistsW("/lock/" + childs[index-1])
if err != nil {
return "", err
}

// 在调用 ExistsW 之后,前一个节点已经被删除
if !b {
return p, nil
}

// 等待前一个节点被删除
<-ev

return p, nil

在调用 ExistsW 的时候,如果前一个节点已经被删除,那么 ExistsW 会立即返回 false,否则我们可以通过 ExistsW 返回的第三个参数 ev 来等待前一个节点被删除。

<-ev 处,我们通过 <-ev 来等待前一个节点被删除,一旦前一个节点被删除,ev 会收到一个事件,这个时候我们就可以获取到锁了。

释放锁

如果调用 Lock 可以成功获取到锁,我们会返回当前创建的节点的路径,我们可以通过这个路径来释放锁。

1
2
3
func (l *Lock) Unlock(p string) error {
return l.c.Delete(p, -1)
}

完整代码

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package main

import (
"github.com/go-zookeeper/zk"
"sort"
"strings"
"time"
)

func client() *zk.Conn {
c, _, err := zk.Connect([]string{"192.168.2.168"}, time.Second) //*10)
if err != nil {
panic(err)
}
return c
}

type Lock struct {
c *zk.Conn
}

func NewLock() *Lock {
c := client()
e, _, err := c.Exists("/lock")
if err != nil {
panic(err)
}
if !e {
_, err := c.Create("/lock", []byte(""), 0, zk.WorldACL(zk.PermAll))
if err != nil {
panic(err)
}
}

return &Lock{c: c}
}

func (l *Lock) Lock() (string, error) {
p, err := l.c.Create("/lock/lock", []byte(""), zk.FlagEphemeral|zk.FlagSequence, zk.WorldACL(zk.PermAll))
if err != nil {
return "", err
}
childs, _, err := l.c.Children("/lock")
if err != nil {
return "", err
}

// childs 是无序的,所以需要排序,以便找到当前节点的前一个节点,然后监听前一个节点
sort.Strings(childs)

// 成功获取到锁
p1 := strings.Replace(p, "/lock/", "", 1)
if childs[0] == p1 {
return p, nil
}

// 监听锁,等待锁释放
// 也就是说,如果当前节点不是最小的节点,那么就监听前一个节点
// 一旦前一个节点被删除,那么就可以获取到锁
index := sort.SearchStrings(childs, p1)
b, _, ev, err := l.c.ExistsW("/lock/" + childs[index-1])
if err != nil {
return "", err
}

// 在调用 ExistsW 之后,前一个节点已经被删除
if !b {
return p, nil
}

// 等待前一个节点被删除
<-ev

return p, nil
}

func (l *Lock) Unlock(p string) error {
return l.c.Delete(p, -1)
}

测试代码

下面这个例子模拟了分布式的 counter 操作,我们通过 ZooKeeper 分布式锁来保证 counter 的原子性。

当然这个例子只是为了说明 ZooKeeper 分布式锁的使用,实际上下面的功能通过 redis 自身提供的 incr 就可以实现,不需要这么复杂。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"sync"
)

func main() {
var count = 1000
var wg sync.WaitGroup
wg.Add(count)

l := NewLock()
// 创建 redis 客户端连接
redisClient = redis.NewClient(&redis.Options{
Addr: "192.168.2.168:6379",
Password: "", // no password set
DB: 0, // use default DB
})

for i := 0; i < count; i++ {
go func(i1 int) {
defer wg.Done()

// 获取 Zookeeper 分布式锁
p, err := l.Lock()
if err != nil {
return
}
// 成功获取到了分布式锁:
// 1. 从 redis 获取 zk_counter 的值
// 2. 然后对 zk_counter 进行 +1 操作
// 3. 最后将 zk_counter 的值写回 redis
cmd := redisClient.Get(context.Background(), "zk_counter")
i2, _ := cmd.Int()
i2++
redisClient.Set(context.Background(), "zk_counter", i2, 0)
// 释放分布式锁
err = l.Unlock(p)
if err != nil {
println(fmt.Errorf("unlock error: %v", err))
return
}
}(i)
}

wg.Wait()

l.c.Close()
}

我们需要将测试程序放到不同的机器上运行,这样才能模拟分布式环境。

总结

最后,再来回顾一下本文内容:

  1. sync.Mutex 这种锁只能保证单进程内的并发安全,无法保证分布式环境下的并发安全。
  2. 使用 ZookeeperRedis 都能实现分布式锁,但是 Zookeeper 可以保证顺序性和公平性,而 Redis 可以保证高性能。
  3. Zookeeper 通过其临时节点、顺序节点和 Watcher 机制等特性,实现了分布式锁的功能。

在实际的工作中,我们很多时候开发环境跟应用程序最终运行的环境是不同的操作系统,比如在 Windows 上进行开发,但是应用程序最终是要在 Linux 上运行的, 又或者是在 mac 下开发,在 Linux 下运行。这个时候我们就需要进行交叉编译,即在一个操作系统上编译出另一个操作系统的可执行文件。

使用 Go 的时候,我们可以很方便的进行交叉编译,只需要设置好环境变量或者设置构建标签即可,本文会通过一个简单的例子来演示如何进行交叉编译。

GOOS 和 GOARCH 环境变量所有可能的值

在 Go 语言中,我们可以通过设置环境变量 GOOSGOARCH 来指定目标操作系统和目标架构。 比如在我的系统上,查看 GOOSGOARCH 的值:

1
2
3
➜ go env GOOS GOARCH
darwin
amd64

在 Go 编译的时候,默认的 GOOSGOARCH 的值是当前系统的操作系统和架构,比如在我的系统上,GOOS 的值是 darwinGOARCH 的值是 amd64。 所以编译出来的就是当前系统可以执行的二进制文件,如果我们想要编译出其他系统的二进制文件,就需要设置 GOOSGOARCH 的值。

首先,我们需要了解这两个环境变量支持哪些值。下面是所有可能的值,我们可以通过 go tool dist list 列出来:

1
2
3
4
5
6
7
8
9
10
11
aix/ppc64        freebsd/amd64   linux/mipsle   openbsd/386
android/386 freebsd/arm linux/ppc64 openbsd/amd64
android/amd64 illumos/amd64 linux/ppc64le openbsd/arm
android/arm js/wasm linux/s390x openbsd/arm64
android/arm64 linux/386 nacl/386 plan9/386
darwin/386 linux/amd64 nacl/amd64p32 plan9/amd64
darwin/amd64 linux/arm nacl/arm plan9/arm
darwin/arm linux/arm64 netbsd/386 solaris/amd64
darwin/arm64 linux/mips netbsd/amd64 windows/386
dragonfly/amd64 linux/mips64 netbsd/arm windows/amd64
freebsd/386 linux/mips64le netbsd/arm64 windows/arm

在上面的输出中,/ 前面操作系统,/ 后面是架构。以 linux/386 为例,键值对以 GOOS 开始,在本例中将是 linux ,指的是 Linux 操作系统。这里的 GOARCH 将是 386 ,代表 Intel 80386 微处理器。

我们发现其实 Go 支持很多操作系统和架构,但是大多数情况下,你最终会使用 linuxwindowsdarwin 中的一个作为 GOOS 的值,这涵盖了三大操作系统平台:Linux、Windows 和 macOS。

使用文件名后缀实现交叉编译

使用场景:不同操作系统需要通过不同代码来实现。

Go 标准库中 path/filepath 包中的 Join 函数,在不同平台下会有不同的效果。该函数接受一些字符串,并返回一个使用正确文件路径分隔符连接在一起的字符串。

这是一个很好的示例程序,因为程序的操作取决于它运行的操作系统。在 Windows 上,路径分隔符是反斜杠 \,而 Unix 系统使用正斜杠 /

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
"path/filepath"
)

func main() {
s := filepath.Join("a", "b", "c")
fmt.Println(s)
}

这个程序在 Windows 上运行时,将输出 a\b\c,而在 Unix 系统上运行时,将输出 a/b/c

这是如何实现的呢?这就涉及到了 Go 中实现交叉编译的其中一种方式,就是指定文件名后缀, 我们看 Go 的源码或者一些开源项目的源码,就会发现有些文件的文件名带了操作系统的后缀,比如 file_windows.gofile_linux.gofile_darwin.go 等等。

同样的,path/filepath 包中的 Join 函数也是这样实现的,我们可以看到 path/filepath 包中有很多文件,比如 path_windows.gopath_unix.go 等等,其中:

  • path_windows.go 中实现了 Join 函数在 Windows 上的实现
  • path_unix.go 中实现了 Join 函数在 Unix 系统上的实现

我们点开 path_unix.go 文件,可以看到如下的代码:

1
2
3
4
5
// ...
const (
PathSeparator = '/' // OS-specific path separator
PathListSeparator = ':' // OS-specific path list separator
)

也就是说,Join 函数的路径分隔符是在这里通过 PathSeparator 定义成 / 的,而在 path_windows.go 文件中,PathSeparator 是定义成 \ 的。

1
2
3
4
5
6
// path_windows.go
// ...
const (
PathSeparator = '\\' // OS-specific path separator
PathListSeparator = ';' // OS-specific path list separator
)

有两个 \ 是因为需要转义。

同时在文件名中加上 GOARCH 后缀

在命名文件时,您可以按照顺序将 GOOSGOARCH 添加为文件名的后缀,用下划线(_)分隔这些值。如果您有一个名为 filename.go 的 Go 文件,您可以通过将文件名更改为 filename_GOOS_GOARCH.go 来指定操作系统和架构。例如,如果您希望将其编译为具有 64 位 ARM 架构的 Windows 文件,您将文件名更改为 filename_windows_arm64.go 。这种命名约定有助于保持代码整洁有序。

在我们编译的时候,如果我们当前的 GOOSGOARCH 跟文件名不匹配,则 Go 会忽略这个文件。

使用构建标签实现交叉编译

使用场景:不同操作系统需要使用不同代码。(跟上一个类似)

除了指定文件名后缀以外,我们还可以使用构建标签来实现交叉编译。具体来说,就是在文件的第一行添加 // +build 标签,比如:

1
2
3
4
5
// +build windows

package main

const PathSeparator = "\\"

这样的话,这个文件就只会在 Windows 上编译,而在其他系统上不会编译。

使用你本地 GOOS 和 GOARCH 的值进行交叉编译

使用场景:在本地开发环境编译出其他系统的可执行文件。

之前,您运行了 go env GOOS GOARCH 命令来查看您正在使用的操作系统和架构。当您运行 go env 命令时,它会查找两个环境变量 GOOSGOARCH;如果找到,它们的值将被使用,但如果未找到,则 Go 将使用当前平台的信息来设置它们。这意味着您可以更改 GOOSGOARCH,以便它们不会默认为您的本地操作系统和架构。这样就可以编译出其他平台的可执行文件。

go build 命令的行为方式类似于 go env 命令。您可以使用 go build 设置 GOOSGOARCH 环境变量以构建不同平台的应用程序。

如果您没有使用 Windows 系统,请在运行 go build 命令时将 GOOS 环境变量设置为 windows

1
GOOS=windows go build

你也可以同时设置 GOARCH 环境变量:

1
GOOS=linux GOARCH=amd64 go build

这将编译出一个 Linux 平台上的 64 位可执行文件,我们如果使用的是 macOS,我们可以通过 file 命令查看编译出来的文件的信息:

1
2
file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=xx, with debug_info, not stripped

我们会看到这是一个 64 位的 ELF 可执行文件,而 ELF 是 Linux 下的可执行文件格式。

更加现代化的交叉编译方式

我们前面讲了很多如何进行交叉编译,但是如果我们每次都需要针对不同平台来手动编译,未免过于麻烦,当然我们可以写一个脚本来自动化这个过程。

这一小节,我将介绍一个比较好用的交叉编译工具 goreleaser,我们只需要简单的配置一下,它就可以帮我们自动化交叉编译的过程。 比如 frp 这个开源项目就是使用 goreleaser 来进行发布新版本的。

下面是一个示例配置文件:

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
# .goreleaser.yml,放在项目根目录下
# 项目名称
project_name: goss
# 在执行前需要执行的命令
before:
hooks:
- go mod tidy
# 编译配置
builds:
- env:
# 可以指定环境变量
- CGO_ENABLED=0
goos: # 需要编译的操作系统
- linux
- windows
- darwin
archives:
- replacements: # 将 GOARCH 替换,因为用户更熟悉 x86_64
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

说明:

  • project_name 是项目的名称
  • before 是在执行前需要执行的命令
  • builds 是编译配置,env 是环境变量,goos 是需要编译的操作系统
  • archives 是归档配置,replacements 是将 GOARCH 替换。
  • checksum 是生成 checksum 的配置

接着我们只需要执行 goreleaser build 命令即可进行编译:

goreleaser 的安装方式可参考它的官网。

1
goreleaser build

输出:

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
• starting build...
• loading config file file=.goreleaser.yaml
• loading environment variables
• getting and validating git state
• building... commit=e913b9258e649f8f2784d9daaebbf3a4d7cf7a17 latest tag=v0.0.9
• parsing tag
• setting defaults
• running before hooks
• running hook=go mod tidy
• running hook=go generate ./...
• checking distribution directory
• loading go mod information
• build prerequisites
• writing effective config file
• writing config=dist/config.yaml
• generating changelog
• writing changelog=dist/CHANGELOG.md
• building binaries
• building binary=dist/goss_windows_amd64_v1/goss.exe
• building binary=dist/goss_darwin_arm64/goss
• building binary=dist/goss_windows_arm64/goss.exe
• building binary=dist/goss_darwin_amd64_v1/goss
• building binary=dist/goss_linux_amd64_v1/goss
• building binary=dist/goss_linux_arm64/goss
• building binary=dist/goss_windows_386/goss.exe
• building binary=dist/goss_linux_386/goss
• took: 39s
• storing release metadata
• writing file=dist/artifacts.json
• writing file=dist/metadata.json
• build succeeded after 39s

编译完成后,我们会在 dist 目录下看到编译好的文件:

1
ls -l dist/

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
total 32
-rw-r--r-- 1 ruby staff 36 Feb 3 11:12 CHANGELOG.md
-rw-r--r-- 1 ruby staff 1803 Feb 3 11:13 artifacts.json
-rw-r--r-- 1 ruby staff 3509 Feb 3 11:12 config.yaml
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_darwin_amd64_v1
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_darwin_arm64
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_linux_386
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_linux_amd64_v1
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_linux_arm64
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_windows_386
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_windows_amd64_v1
drwxr-xr-x 3 ruby staff 96 Feb 3 11:13 goss_windows_arm64
-rw-r--r-- 1 ruby staff 219 Feb 3 11:13 metadata.json

接着,我们就可以来发布这些二进制文件了。如果我们有其他个性化的需求,我们可以通过修改 .goreleaser.yml 文件来满足我们的需求。它还有很多配置可以自定义。 如果后续我们需要调整,只需要修改一下配置文件就行了,比如我们需要支持一个新的操作系统,只需要在 goos 下面增加一个新的操作系统即可。

使用 goreleaser 进行交叉编译的好处是,它会自动帮我们打包、生成 checksum、生成 changelog 等等,省去了很多手动操作。

另外,它还支持直接发布到 Github,使用 Github Actions 来自动化这个过程,这样我们只需要 push 代码,就可以自动进行编译、打包、发布。

下面是一个 github workflow 的示例配置文件:

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
38
39
40
name: goreleaser

on:
push:
# run only against tags
tags:
- '*'

permissions:
contents: write
# packages: write
# issues: write

jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
-
name: Fetch all tags
run: git fetch --force --tags
-
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
distribution: goreleaser
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

这个配置文件的意思是,当我们 push tag 的时候,就会触发这个 workflow,它会自动运行 goreleaser,然后进行编译、打包、发布。

总结

Go 支持我们很方便的进行交叉编译,只需要设置好环境变量或者设置构建标签即可:

  • 环境变量:GOOSGOARCH
  • 文件名后缀:filename_GOOS_GOARCH.go
  • 构建标签:// +build 标签

另外,我们还可以使用 goreleaser 这个工具来自动化交叉编译的过程,它还支持直接发布到 Github,使用 Github Actions 来自动化这个过程,这样我们只需要 push tag,就可以自动进行编译、打包、发布。