Beats:将 Unix 域套接字中的数据索引到 Elastic Stack

156 阅读9分钟

这篇博文将解释什么是 UNIX 域套接字,以及如何将发送到 UNIX 域套接字的索引编入 Elastic Stack - 以及为此存在哪些不同的用例。

Unix Domain Socket with Filebeat and Elasticsearch

UNIX 域套接字 - 简短的历史

如果你想让进程相互通信,你有几种可能

  • 通过文件,一个组件写入,完成后另一个读取。需要同步(即文件锁定)
  • 信号(kill -HUP),不提供有效载荷,只是一个触发器
  • 共享内存、内存映射文件
  • 网络套接字,需要联网
  • Unix 域套接字,类似于套接字,但无法从外部访问

从实现的角度来看,UNIX 域套接字是一个套接字(socket),可以将其视为文件。这使得消费者和生产者很容易实现,因为你只需要 open() 它并向其写入数据。写入数据的有趣部分是,你可以像使用 recvmsg() 和 sendmsg() 系统调用写入网络套接字一样。

由于没有像 mkfifo 这样的工具来创建 UNIX 域套接字,你可以使用类似 socat 的东西来创建它,我们将在稍后完成。

在文件系统中创建 UNIX 域套接字的一个有趣方面是权限。为了让其他进程将数据发送到其接收器,你需要确保可以写入文件 - 这很容易检查和满足。这可以很容易地使用组权限建模。

数据的顺序是有保证的,你可以在沙盒进程中访问 UNIX 域套接字,这使它成为病毒扫描程序等工具的一个很好的用例 - 因为你还可以将文件描述符传递到套接字。

我不使用域套接字 - 怎么了?

让我向你保证,绝对没有😀

如果你不需要它,一切都很好。然而,可能有几个用例,我们将阐明。如果你有短暂的进程,这可能是一个有趣的日志记录选项。如果你使用文件,则需要与其他进程同步以确保写入有序,或者为每个短期进程创建一个新文件。想象一下在其请求/响应周期中记录某些内容的短时间运行的 PHP 调用。

这也可能是在将系统上运行的多个组件的本地日志记录集中到本地之前的一种解决方案,因为现在只需配置一个组件。然而,现在这种情况不太常见,因为你通常使用一个每个容器一个进程的方法,就像在 Docker 中一样,你的日志记录也只是转到 stdout 并由 docker 日志记录提供程序收集。

一些服务在不需要网络连接时使用 UNIX 域服务,例如 MySQL,或者当进程需要与 Web 服务器通信时已经提到,例如基于 PHP 或 uWSGI 的应用程序(在 Python 世界中很常见)。

今天我们将专注于日志记录用例。

一个简单的例子
所以,让我们开始并运行一个日志用例。

首先,让我们使用 socat 侦听 UNIX 域套接字。

创建 Unix 域套接字

我们可以有一下的两种方法来生成 Unix 域套接字:

1)使用 UC 命令

你需要安装 netcat 应用,并使用如下的命令:

nc -lkU aSocket.sock

我们使用 CTRL + C 来终止上面的命令的执行。在该目录下,我们可以发现一个新生成的 aSocket.sock 文件:

$ pwd
/Users/liuxg/tmp/socket
$ ls -al
total 0
drwxr-xr-x   3 liuxg  staff    96 Sep 22 12:19 .
drwxr-xr-x  72 liuxg  staff  2304 Sep 22 10:39 ..
srwxr-xr-x   1 liuxg  staff     0 Sep 22 12:19 aSocket.sock
$ file aSocket.sock 
aSocket.sock: socket

 2)使用 socat 命令

我们可以使用如下的命令:

socat UNIX-LISTEN:./my-socket.sock,fork -

在上面的命执行时,我们在另外一个 terminal 中打入如下的命令:

$ pwd
/Users/liuxg/tmp/socket
$ ls -al
total 0
drwxr-xr-x   3 liuxg  staff    96 Sep 22 12:23 .
drwxr-xr-x  72 liuxg  staff  2304 Sep 22 10:39 ..
srwxr-xr-x   1 liuxg  staff     0 Sep 22 12:23 my-socket.sock
$ file my-socket.sock 
my-socket.sock: socket

我们可以看到一个被生成的 my-socket.sock 文件。当我们使用 CTRL + C 命令终止命令的执行后,my-socket.sock 文件消失。

一个简单的例子

让我们开始并运行一个日志用例。

首先,让我们使用 socat 侦听 UNIX 域套接字。

socat - UNIX-LISTEN:./my-socket.sock

我们可以在另外一个 terminal 中进行查看。我们可以看到一个新生成的 my-socket.sock 文件:

$ pwd
/Users/liuxg/tmp/socket
$ ls
my-socket.sock
$ file my-socket.sock 
my-socket.sock: socket

所以这是一个套接字,基于我的默认 umask,每个人都可以在该系统上读取该文件。 让我们写信给它:

echo "Hello Unix Domain Socket" | socat - UNIX-CONNECT:./my-socket.sock
$ pwd
/Users/liuxg/tmp/socket
$ ls
my-socket.sock
$ file my-socket.sock 
my-socket.sock: socket
$ echo "Hello Unix Domain Socket" | socat - UNIX-CONNECT:./my-socket.sock

那么在运行 socat - UNIX-LISTEN:./my-socket.sock 命令的 terminal 中,我们可以看到:

$ socat - UNIX-LISTEN:./my-socket.sock
Hello Unix Domain Socket

socat 指令退出,并打印出刚才发送的信息。

如果你想保留它,请使用 fork 选项启动 socat 侦听过程:

socat UNIX-LISTEN:./my-socket.sock,fork -

在许多编程语言中,连接到 UNIX 域套接字相当简单,以这个 Crystallang 示例为例:

require "socket"

sock = UNIXSocket.new(ARGV.shift)
sock.puts ARGV.join " "
sock.close

你可以通过 Crystal write-to-socket.cr ./my-socket.sock 运行它,这是一个测试并查看发送到套接字的数据。

所以,回到我们的日志问题,接下来让我们使用 Filebeat 从 UNIX 域套接字读取……

配置 Filebeat

让我们从一个简单地转储事件的配置开始,类似于 socat 所做的,通过创建 filebeat-unix-domain-socket.yml 配置文件:

filebeat-unix-domain-socket.yml

filebeat.inputs:
- type: unix
  path: "/tmp/socket.sock"

output.console:
  pretty: true

像这样启动 Filebeat:

./filebeat -c filebeat-unix-domain-socket.yml -e

当运行完上面的命令后,我们可以在 /tmp/ 目录中发现一个新生成的 socket.sock 文件:

$ pwd
/tmp
$ ls socket.sock 
socket.sock

再次使用 socat 连接到 UNIX 域套接字

echo "Hello Unix Domain Socket" | socat - UNIX-CONNECT:/tmp/socket.sock

这将在你启动 Filebeat 的终端中显示一个事件:

 Filebeat 中的 UNIX 输入有几个更有趣的配置选项

  • group 用于配置正确的套接字组以便其他应用程序更容易访问
  • 文件系统权限模式 mode(如果你使用上面的组,组必须可能是可写的)
  • socket_type,stream 和 datagram 之一
  • delimiter,如果你有带有分隔内容的自定义数据格式分隔符
  • framing 以支持固定宽度的框架来拆分传入事件而不是分隔符

最重要的是,如果需要,你甚至可以在 UNIX 域套接字上使用 SSL。

发送基于文本的数据的一个缺点是无法区分从不同应用程序索引的数据,除非你检查消息的格式。一种解决方法可能是在 Filebeat 中定义多个输入,每个输入都有自己的 UNIX 域套接字,因此你可以在输入中设置自定义标签以进行区分。

另一种选择是在你的应用程序中创建所需的元数据,甚至可以直接将其作为 JSON 发送。让我们使用将数据发送到 Elasticsearch 的最终配置来完成此操作。

filebeat.inputs:
  - type: unix
    path: "/tmp/socket.sock"

processors:
  - add_host_metadata: ~
  - add_cloud_metadata: ~
  - add_docker_metadata: ~
  - add_kubernetes_metadata: ~
  - decode_json_fields:
      fields: ["message"]
      target: ""
      expand_keys: true
      overwrite_keys: true

output.elasticsearch:
  hosts: ["http://localhost:9200"]

将文档索引到我们的索引中:

echo '{"message": "Hello", "first.second.third" : "somevalue"}' \ 
  | socat - UNIX-CONNECT:/tmp/socket.sock

生成的文档将如下所示:

{
  "_index" : "filebeat-7.14.0-2021.09.22-000001",
  "_type" : "_doc",
  "_id" : "aPDWC3wB-4qu1NFrUNP0",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "@timestamp" : "2021-09-22T04:48:55.366Z",
    "ecs" : {
      "version" : "1.10.0"
    },
    "host" : {
      "id" : "E51545F1-4BDC-5890-B194-83D23620325A",
      "ip" : [
        "fe80::aede:48ff:fe00:1122",
        "fe80::c69:c5e:d752:745",
        "192.168.0.3",
        "fe80::cce0:9ed:6c55:a8cd",
        "fe80::9138:27d3:6b45:c8ca"
      ],
      "mac" : [
        "ac:de:48:00:11:22",
        "a6:83:e7:69:f5:24",
        "a4:83:e7:69:f5:24",
        "32:88:ee:55:cc:8c",
        "32:88:ee:55:cc:8c",
        "82:51:61:05:3c:05",
        "82:51:61:05:3c:04",
        "82:51:61:05:3c:01",
        "82:51:61:05:3c:00",
        "98:fc:84:e4:2f:46",
        "82:51:61:05:3c:01"
      ],
      "hostname" : "liuxg",
      "name" : "liuxg",
      "architecture" : "x86_64",
      "os" : {
        "type" : "macos",
        "platform" : "darwin",
        "version" : "10.16",
        "family" : "darwin",
        "name" : "Mac OS X",
        "kernel" : "20.6.0",
        "build" : "20G95"
      }
    },
    "agent" : {
      "id" : "c9ee8b8f-fe17-4b83-a01c-bd9b162569b5",
      "name" : "liuxg",
      "type" : "filebeat",
      "version" : "7.14.0",
      "hostname" : "liuxg",
      "ephemeral_id" : "27551d16-130b-4aa4-a3ab-253cbb123131"
    },
    "first" : {
      "second" : {
        "third" : "somevalue"
      }
    },
    "message" : "Hello",
    "input" : {
      "type" : "unix"
    }
  }
}

查看覆盖的 message 字段,它只包含来自 JSON 文档的消息以及最后的第一个字段,现在具有正确的 JSON 结构。

通过成帧处理消息

如果你想一次发送多条消息,可以使用分帧。 它所需要的只是在发送消息时进行轻微的配置更改和小的修改。 所以这是 unix输入的配置更改:

filebeat.inputs:
  - type: unix
    path: "/tmp/socket.sock"
    framing: "rfc6587"

现在无论何时发送消息,都需要指定下一条消息的长度,后跟一个空格,然后是消息本身。 像这样:

echo -e "6 Hello18 Hello123" | socat - UNIX-CONNECT:/tmp/socket.sock

这表示第一条消息 Hello1 的长度为 6,第二条消息 Hello123 的长度为 8。 这就是所谓的八位字节计数帧。 上面提到的 RFC 中也定义了一个替代方案是基于分隔符的非透明框架,默认情况下它是一个换行符:

echo -e "Hello1\nHello2\n" | socat - UNIX-CONNECT:/tmp/socket.sock

你还可以指定一个特殊的分隔符,它允许发送甚至包含换行符的事件(想想多行异常)

filebeat.inputs:
  - type: unix
    path: "/tmp/socket.sock"
    framing: "rfc6587"
    line_delimiter: "\0"
echo -e '{"message":"value\\n\\n\\nvalue"}\0{"message":"value"}\0' \
  | socat - UNIX-CONNECT:/tmp/socket.sock

因此,也可以使用单个请求发送多个事件,并且可能会加快你的短暂进程。

总结

可能 99% 的人都不需要这个。但是:永远不要低估遗留软件的存在......

更重要的一点:并非每个日志库都正确支持通过 UNIX 域套接字进行日志记录。例如,log4j 似乎无法登录到 UNIX 域套接字。我找不到 log4j2 的任何明确信息。然而,大多数 Java 程序不被认为是短暂的,所以这可能不是问题。

Java 在 JEP 380 中获得了对 UNIX 域套接字的支持,它是在 Java 16 中添加的,所以是最近的。根据 reddit 的讨论,缺少一项重要功能:传递文件描述符的能力。然而,有支持它的 junixsocket

也就是说,如果您有可能登录到文件而不是套接字,您可能想要这样做。如果日志组件不可用,您可以更轻松地重播,因为 UNIX 域套接字不会消失。还要确保您测试了损坏的情况,其中要记录的套接字不存在或不可读,并查看您的应用程序中发生了什么。

译文:Indexing Data From Unix Domain Sockets Into The Elastic Stack