Datadog 笔记

1. Datadog 笔记

1.1. Datadog 安装与卸载

1.1.1. 卸载

https://github.com/DataDog/documentation/blob/master/content/en/agent/faq/how-do-i-uninstall-the-agent.md

1.2. 编译 datadog-agent

参考自: https://github.com/DataDog/datadog-agent

1.2.1. 环境准备

1.2.1.1. /etc/profile.d/preload.sh
#!/bin/bash

vmhgfs-fuse .host:/ /mnt/hgfs -o allow_other -o uid=0 -o gid=0 -o umask=0022

export GOPATH=/mnt/hgfs/share/gopath
export GOBIN=/mnt/hgfs/share/gopath/bin

export PATH=$PATH:/mnt/hgfs/share/go/go/bin:$GOPATH:$GOBIN
1.2.1.2. 安装 pip
apt-get install python3-pip

# 或者

curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py   # 下载安装脚本
python3 get-pip.py

pip3 --version

1.2.2. 常见问题

1.2.2.1. go get 报错: dial tcp 142.251.43.17:443: i/o timeout

注: go 版本需要支持 mod

自动下载: go env -w GO111MODULE=on

设置环境为国内代理: go env -w GOPROXY=https://goproxy.cn,direct

1.3. Datadog 新特性

https://www.datadoghq.com/blog/dash-2021-new-feature-roundup/#infrastructure-and-network-monitoring

1.4. Datadog 源码

1.4.1. RtLoader

RtLoader is a C++ wrapper around the CPython API with a C89-compatible public API that can be used by foreing languages like Go. In order to provide support for multiple Python versions, RtLoader fully abstracts Python in order to decouple client applications and CPython. Which Python version to use can be decided at runtime, RtLoader will dlopen the proper backend libraries accordingly.

More: https://github.com/DataDog/datadog-agent/tree/main/rtloader

1.5. Datadog 的路径

  • 日志: /var/log/datadog
  • 配置文件: /etc/datadog-agent
  • 程序文件: /opt/datadog-agent

1.6. Datadog Troubleshooting

1.6.1. status

可以查看当前运行状态: datadog-agent status

1.6.2. check

可以检查指定配置状态。如: datadog-agent check python

1.6.3. Unable to load a check from instance of config ‘process_agent’: JMX Check Loader: check is not a jmx check, or unable to determine if it’s so; Python Check Loader: python is not initialized;

这是由于找不到 “.so” 文件造成的。

例如 “python is not initialized” 这个错误, 可以用 datadog-agent check python 命令知道是 libdatadog-agent-three.so 文件找不到(我编译的时候指定的是 Python3)。

1.7. Datadog Cgo and Python

如果你研究过 新近的 Datadog Agent , 你可能会注意到大部分代码库是用 Go 语言编写, 而我们用来收集指标的检查工具仍然是用的 Python 。这是有可能的, 因为 Datadog Agent(一个标准的 Go 二进制程序), 内嵌 了一个 CPython 解释器, 在需要运行 Python 代码的时候就会调用这个解释器。通过使用一个抽象层可以让整个过程是透明的, 由此, 你尽可以编写惯用的 Go 代码, 即使底层同时运行着 Python 代码。

想要在 Go 应用中嵌入 Python 的原因有很多种:

  • 对接口有益处; 逐步把现有 Python 项目的一部分迁移到新的语言, 在整个过程中不丢失功能。
  • 你可以重用现有的 Python 软件或库, 不必用新的语言重新实现它们。
  • 通过加载和运行标准的 Python 脚本你可以动态扩展你的软件, 即使是在运行时。

这个清单还可以继续下去, 但对 Datadog Agent 来说最后一点最为关键: 我们希望你能够执行自定义的检查和更改而不必去重新编译 Agent , 或者更广泛点儿, 编译任何一处。

内嵌的 CPython 非常简单而且文档也很完善。解释器本身使用 C 编写, 并且提供了一个 C API 使得可以用编程方式执行一些比较底层的操作, 例如创建对象, 导入模块, 以及调用函数。

在本文中我们会介绍一些简单的代码示例, 并且在与 Python 交互的时候, 我们会着力于保证 Go 代码仍符合语言习惯。但在开始之前我们需要先标记一处小障碍: 内嵌的 API 是 C 但我们的主应用却是 Go , 这又是怎么能完成的呢?

1.7.1. Cgo 简介 (Introducing cgo)

可能会有 很多原因 让你不希望引入 Cgo 到你的工作栈中, 但内嵌的 CPython 却是一个能让你同意的关键砝码。Cgo 既不是语言也不是编译器。它是 外部函数接口 (FFI), 一种能让我们在 Go 中调用其他语言编写的函数和服务, 尤其是 C 。

当我们提到 “Cgo” 时, 我们实际指的是底层 Go 工具链使用的一系列工具, 库, 函数, 以及类型, 所以我们依然可以用 go build 获取 Go 的二进制程序。一个使用 Cgo 的极简代码示例如下所示:

package main

// #include <float.h>
import "C"
import "fmt"

func main() {
    fmt.Println("Max float value of float is", C.FLT_MAX)
}

import "C" 上方的注释块可以预先调用, 并且能包含实际的 C 代码, 在这个例子里包含了一个头文件。一旦被导入, “C” 伪库就会让程序跳转到外部代码, 访问 FLT_MAX 宏。你可以通过调用 go build 来 build 该示例, 就跟普通的 Go 代码一样。

如果你想要了解一下底层 Cgo 所做的全部工作, 就运行 go build -x 。你将会看到 “Cgo” 工具被调用去生成一些 C 和 Go 的模块, 然后 C 和 Go 编译器会被调用以建立目标模块, 最终链接器会把一切都安排好。

你可以在 Go 博客 中阅读更多 Cgo 的内容。这篇文章包含更多示例, 以及一些深入细节的有用链接。

既然我们已经知道了 Cgo 可以帮助我们做哪些工作, 接下来就来看看我们如何借助该机制来运行 Python 代码。

1.7.2. 内嵌 CPython : 入门 (Embedding CPython: a primer)

一个 Go 程序, 严格来说, 内嵌 CPython 并不像你预计的那样复杂。事实上, 在最低限度上, 我们要做的所有工作就是在运行 Python 代码之前初始化解释器, 以及在运行结束后回收资源。请注意, 我们将会在所有的示例中都使用 Python 2.x , 但是这些都能够适用于 Python 3.x, 仅仅需要很少的修改。让我们先看一个例子:

package main

// #cgo pkg-config: python-2.7
// #include <Python.h>
import "C"
import "fmt"

func main() {
    C.Py_Initialize()
    fmt.Println(C.GoString(C.Py_GetVersion()))
    C.Py_Finalize()
}

上面的例子和下面的 Python 代码等价:

import sys
print(sys.version)

你可以看到我们放了一个 #cgo 在前面; 这些符号将会被传递给工具链, 从而改编 build 的工作流。在这个例子, 我们让 Cgo 去调用 “pkg-config” 来获取 build 需要的标志, 并且链接到一个叫 “python-2.7” 的库, 以及传递这些标志给 C 编译器。如果你的系统上安装了 CPython 开发库并用 pkg-config 连接, 这将使得你可以继续使用普通的 go build 编译上面的例子。

重新再看代码, 我们使用 Py_Initialize()Py_Finalize() 来开启和关闭解释器, 以及 C 函数 Py_GetVersion 来获取包含内嵌解释器版本信息的字符串。

如果你有疑问, 所有我们需要整合来调用 C Python API 的 Cgo 部分都是样板代码。这也是 Datadog Agent 依赖 go-python 来执行所有内嵌操作的原因所在; go-python 库提供了 Go 风格的对 C API 的简单封装, 并且隐藏了 Cgo 的细节。这是另一个简单的嵌入示例, 这次使用 go-python:

package main

import (
    python "github.com/sbinet/go-python"
)

func main() {
    python.Initialize()
    python.PyRun_SimpleString("print 'hello, world!'")
    python.Finalize()
}

这个例子更接近标准 Go 代码, 没有暴漏出更多的 Cgo , 而且我们可以在访问 Python API 的时候来回使用 Go 字符串。内嵌看起来功能强大并且对开发者友好。是时候好好使用解释器了: 让我们尝试加载一个磁盘上的 Python 模块。

我们没必要使用复杂的 Python 模块, 一个最简单的 “hello world” 就可以满足需求:

# foo.py
def hello():
    """
    Print hello world for fun and profit.
    """
    print "hello, world!"

Go 代码稍微复杂点, 但依然容易阅读:

// main.go
package main

import "github.com/sbinet/go-python"

func main() {
    python.Initialize()
    defer python.Finalize()

    fooModule := python.PyImport_ImportModule("foo")
    if fooModule == nil {
        panic("Error importing module")
    }

    helloFunc := fooModule.GetAttrString("hello")
    if helloFunc == nil {
        panic("Error importing function")
    }

    // The Python function takes no params but when using the C api
    // we're required to send (empty) *args and **kwargs anyways.
    helloFunc.Call(python.PyTuple_New(0), python.PyDict_New())
}

完成之后, 我们需要设置 PYTHONPATH 环境变量到当前工作目录, 由此 import 语句就能够找到 foo.py 模块。在 shell 中, 命令类似下面:

$ go build main.go && PYTHONPATH=. ./main
hello, world!

1.7.3. 糟糕的全局解释器锁 ( GIL ) (The dreadful Global Interpreter Lock)

为了内嵌 Python 引入 Cgo 是一个妥协: build 过程会变慢, 垃圾回收器不会帮我们管理外部系统使用的内存, 并且交叉编译也会有难度。是否为一个特定项目引入可能会引发争论, 但有一点我认为是无需讨论的: Go 并发模型。如果我们不能在一个 goroutine 里面运行 Python, 这一切就毫无意义。

在用 Python 和 cgo 实现并发之前 , 有一点我们需要了解: 就是全局解释器锁, 简称 GIL 。GIL 是一个被语言解释器 ( CPython 只是一种)广泛采用的机制, 目的是防止同一时刻有超过一个以上的线程运行。这意味着被 CPython 执行的 Python 程序不可能在同一个进程中并行。并发倒是仍然有可能, 锁是在速度, 安全性和实现难度上的一个较好权衡。那么它为什么会在内嵌的时候引出问题?

当一个标准的, 非内嵌 Python 程序启动时, 不会有 GIL 卷进来, 从而避免了锁操作带来的无用的间接损耗; GIL 第一次启动时一些 Python 代码会请求开启一个线程。对任何一个线程来说, 解释器创建一个数据结构来储存当前状态信息和锁住 GIL 。当线程结束后, 状态会被恢复而且 GIL 也会解锁, 从而可以被其它线程使用。

我们在 Go 程序中运行 Python 的时候, 以上这些都不会自动发生。没有 GIL, 我们的 Go 程序可能会创建多个 Python 线程。这将有可能引起竞争条件而导致致命的运行时错误, 而且一个模块的错误极有可能摧毁整个 Go 应用。

解决方案就是在任何时候运行 Go 中的多线程代码都要显式调用 GIL; 代码不会太复杂, 因为 C API 提供了我们需要的所有工具。为了更好的暴漏问题, 我们需要在 Python 中做一些 CPU 绑定的事情。让我们把这些函数添加到前面示例中的 foo.py 中:

# foo.py
import sys

def print_odds(limit=10):
    """
    Print odds numbers < limit
    """
    for i in range(limit):
        if i%2:
            sys.stderr.write("{}\n".format(i))

def print_even(limit=10):
    """
    Print even numbers < limit
    """
    for i in range(limit):
        if i%2 == 0:
            sys.stderr.write("{}\n".format(i))

我们会在 Go 中尝试并发的打印奇数和事件编号, 使用两个不同的 goroutine (由此引入线程):

package main

import (
    "sync"

    "github.com/sbinet/go-python"
)

func main() {
    // The following will also create the GIL explicitly
    // by calling PyEval_InitThreads(), without waiting
    // for the interpreter to do that
    python.Initialize()

    var wg sync.WaitGroup
    wg.Add(2)

    fooModule := python.PyImport_ImportModule("foo")
    odds := fooModule.GetAttrString("print_odds")
    even := fooModule.GetAttrString("print_even")

    // Initialize() has locked the the GIL but at this point we don't need it
    // anymore. We save the current state and release the lock
    // so that goroutines can acquire it
    state := python.PyEval_SaveThread()

    go func() {
        _gstate := python.PyGILState_Ensure()
        odds.Call(python.PyTuple_New(0), python.PyDict_New())
        python.PyGILState_Release(_gstate)

        wg.Done()
    }()

    go func() {
        _gstate := python.PyGILState_Ensure()
        even.Call(python.PyTuple_New(0), python.PyDict_New())
        python.PyGILState_Release(_gstate)

        wg.Done()
    }()

    wg.Wait()

    // At this point we know we won't need Python anymore in this
    // program, we can restore the state and lock the GIL to perform
    // the final operations before exiting.
    python.PyEval_RestoreThread(state)
    python.Finalize()
}

在阅读示例的时候你可能注意到了一个模式, 这个模式将会是我们运行内嵌 Python 的准则

  1. 保存状态并锁住 GIL。
  2. 执行 Python 。
  3. 恢复状态, 解锁 GIL。

代码可以说得上简洁明了, 但仍有一处细节需要指出: 注意, 即使是遵循 GIL 模式, 在一个例子里面我们运行 GIL 时是通过调用 PyEval_SaveThread()PyEval_RestoreThread() , 在另一个例子里(请看 goroutines 里面的代码)我们是通过调用 PyGILState_Ensure()PyGILState_Release()

我们说过, 当 Python 里面运行多线程时, 解释器会负责创建储存当前状态的数据结构, 但如果是在 C API 里面的话, 需要我们亲自动手实现。

当我们通过 go-python 初始化解释器的时候, 我们是运行在 Python 上下文环境。所以当调用 PyEval_InitThreads() 时解释器会初始化数据结构并锁住 GIL 。我们可以使用 PyEval_SaveThread()PyEval_RestoreThread() 对已经存在的状态进行操作。

在 goroutine 中, 我们则是在一个 Go 上下文环境中运行, 并且我们不需要显式的创建和删除状态, PyGILState_Ensure()PyGILState_Release() 负责完成这些工作。

1.7.4. 解放 Go 爱好者 (Unleash the Gopher)

现在我们已经知道怎么处理多线程 Go 代码在一个内嵌解释器中执行 Python 的过程了, 但是在 GIL 之后, 我们又面临着一个新的挑战: Go 调度器。

当一个 goroutine 启动时, 它会被调度运行在 GOMAXPROCS 个可用线程中的其中一个线程——点击这里 可以了解更多细节。当一个 goroutine 执行系统调用或者调用 C 代码时, 当前线程会把等待运行线程队列中的其它 goroutine 移交给另一个线程, 从而让这些 goroutine 有更多机会执行; 当前 goroutine 被挂起, 直到系统调用或是 C 函数返回。如果有返回发生, 线程就会试图唤醒被终止的 goroutine , 但如果没有返回的可能性, 那该线程就会请求 Go 运行时去查找另一个线程来完成该 goroutine , 并且进入睡眠状态。 goroutine 最终被调度给另一个线程, 然后结束。

考虑到这些, 让我们来看看当一个正在运行 Python 代码的 goroutine 被移动到一个新的线程时, goroutine 都会发生什么:

  1. 我们的 goroutine 启动后, 执行一个 C 函数调用, 然后挂起。GIL 被锁住。
  2. 当 C 函数调用返回, 当前线程试图唤醒该 goroutine, 但它失败了。
  3. 当前线程告诉 Go 运行时去查找另一个线程来唤醒我们的 goroutine。
  4. Go 调度器找到一个可用的线程, 并且 goroutine 也被唤醒。
  5. goroutine 基本完成, 并且尝试在返回前解锁 GIL。
  6. 当前状态存储的线程 ID 是初始线程的 ID, 和当前线程的 ID 不一致。
  7. Panic !

幸运的是, 我们可用强制要求 Go 运行时保证我们的 goroutine 一直运行在同一个线程上, 只要通过 goroutine 调用 runtime 包里的 LockOSThread 函数就行。

go func() {
    runtime.LockOSThread()

    _gstate := python.PyGILState_Ensure()
    odds.Call(python.PyTuple_New(0), python.PyDict_New())
    python.PyGILState_Release(_gstate)
    wg.Done()
}()

这将会干扰调度器并可能带来一些间接损耗, 但为了避免随机的 panic 我们愿意付出这种代价。

1.7.5. 结论 (Conclusions)

顾及到内嵌 Python , Datadog Agent 做出了以下取舍:

  • cgo 引入的间接损耗。
  • 手动操作 GIL。
  • 运行期间绑定 goroutine 到同一个线程的限制。

考虑到在 Go 中运行 Python 检查的便利, 我们很乐意接受这一切。但既然意识到了这些取舍, 我们就能够最小化它们带来的影响。对于为支持 Python 而带来的其它限制, 我们很难有对策处理可能的问题:

  • build 依然是自动化的并且可配置的, 因此开发者照样会类似于 go build
  • 一个轻量级版本的 agent 可以被创建, 而且剥离 Python 支持后也完全可以支持简单使用 Go build 标记。
  • 这样的版本只依赖于 agent 本身的硬编码核心检查(绝大部分是系统和网络检查), 但不受 cgo 限制而且可以被交叉编译。

我们会重新评估将来的选择, 并判断是否值得继续使用 cgo ; 我们甚至可能会考虑把 Python 作为一个整体是否值得, 等到 Go 插件包 足够成熟, 可以支持我们的用例。但至少现在内嵌 Python 工作的很好, 而且从旧 Agent 迁移到新的上面也很简单。

你是一个掌握并且喜欢混合不同语言编程的爱好者吗? 你热爱学习语言的内部工作机制来保证你的代码更加健壮吗? 请加入 Datadog !

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云满笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值