几年前,当你有一个单体应用程序时,调试和诊断相当容易,因为可能只有一个服务有几个用户。如今,系统被分解为更小的微服务,这些微服务部署在 Kubernetes 之上的容器中,分布在不同云环境的多个集群中。在这些类型的分布式环境中,需要观察所有情况,包括整体情况,如果需要,还需要在更细粒度的级别上进行观察。
可观察性大致可以分为三个子类别:日志记录、度量和跟踪。在这篇博文中,我们将向您展示在新的或现有的 MinIO 应用程序中设置跟踪是多么简单。我们将构建一个小型 MinIO 应用程序来执行一些基本请求;这将是我们的基础应用程序,我们将添加跟踪以更好地了解系统组件和功能如何交互。
跟踪是用于描述记录和观察应用程序发出的请求的活动,以及这些请求如何通过系统传播的术语。当系统以分布式方式设置时,我们称之为分布式跟踪,因为它涉及观察应用程序及其通过多个系统的交互。例如,作为开发人员,您的代码可能包含多个函数,但您更感兴趣的是 MinIO 函数需要多长时间执行以及这些函数在应用程序中使用时的相互依赖性。跟踪将通过以下方式提供必要的见解:
识别性能和延迟瓶颈
重大事件发生后寻找根本原因分析
确定多服务架构中的依赖关系
监控应用程序中的事务
我们将从一个小型的 MinIO Python 应用程序开始,它将展示几个简单的操作。稍后我们将添加用于跟踪的代码,以测量代码执行所需的时间。
有几种方法可以在各种环境中安装 MinIO 。在这篇博文中,我们将使用 Docker 启动 MinIO,但在生产环境中请确保安装在分布式设置中。
在您的本地机器上创建一个目录,MinIO 将在其中保存数据
$ mkdir -p /minio/数据
使用 Docker 启动 MinIO 容器
$ docker run -d \ -p 9000:9000 \ -p 9001:9001 \ --name minio \ -v /minio/data:/data \ -e "MINIO_ROOT_USER=minio" \ -e "MINIO_ROOT_PASSWORD=minioadmin" \ quay .io/minio/minio 服务器 /data --console-address ":9001"
注意:保留上面使用的凭据的副本,稍后您将需要它们来访问 MinIO。
验证您是否可以通过使用浏览器通过http://localhost:9001/使用用于启动上述容器的凭据登录来访问 MinIO 。
有几个SDK支持您将您的应用程序与 MinIO API 集成。在本例中,我们将使用Python SDK。
安装 MinIO Python SDK
$ pip 安装 minio
将包含 MinIO 函数的整个 Python 脚本复制并粘贴到本地文本编辑器中,并将其另存为minio_app.py. 您可以参考它,因为我们将在下面描述它的作用。
from minio import Minio # 方便的基本配置config = { "dest_bucket" : "processed" , # 这将自动创建"minio_endpoint" : "localhost:9000" , "minio_username" : "minio" , "minio_password" : " minioadmin" , } # 初始化 MinIO 客户端minio_client = Minio(config[ "minio_endpoint" ], secure= False , access_key=config[ "minio_username"], secret_key=配置[ "minio_password" ] ) #如果不存在,则创建目标桶minio_client.bucket_exists (config[ "dest_bucket" ]): minio_client.make_bucket(config[ "dest_bucket" ]) print( "Destination Bucket '%s' has been created" % (config[ "dest_bucket" ])) # 创建一个测试对象file_path = "test_object.txt" f = open(file_path, "w" ) f.write( "created test object" ) f.close() #将一个对象放入存储桶minio_client 中。fput_object(配置[ “dest_bucket” ], file_path, file_path) # 从桶中获取对象minio_client.fget_object(config[ "dest_bucket" ], file_path, config[ "dest_bucket" ] + "/" + file_path) # 获取minio_client.list_objects中obj的对象列表(config[ "dest_bucket" ]): print(obj) print( "Some objects here" )
让我们来看看上面的脚本。我们正在使用我们在上一步中启动的 MinIO 容器调用一些基本操作。
在最顶部,我们正在导入我们之前安装的 MinIO Python SDK 并使用默认值初始化它
MinIO 端点
MinIO 用户名
MinIO 密码
MinIO 中的目标存储桶名称
from minio import Minio # 方便的基本配置config = { "dest_bucket" : "processed" , # 这将自动创建"minio_endpoint" : "localhost:9000" , "minio_username" : "minio" , "minio_password" : " minioadmin" , } # 初始化 MinIO 客户端minio_client = Minio(config[ "minio_endpoint" ], secure= False , access_key=config[ "minio_username"], secret_key=配置[ “minio_password” ] )
检查特定的目标桶是否存在;如果不创建它。
#如果不存在则创建目标桶minio_client.bucket_exists (config[ "dest_bucket" ]): minio_client.make_bucket(config[ "dest_bucket" ]) print( "Destination Bucket '%s' has been created" % (config [ “dest_bucket” ]))
创建一个测试对象来执行基本操作。在这里,我们正在创建一个包含一些文本的文件
# 创建一个测试对象file_path = "test_object.txt" f = open(file_path, "w" ) f.write( "created test object" ) f.close()
将测试对象放入我们之前创建的桶中
minio_client.fput_object(config[ "dest_bucket" ], file_path, file_path)
获取我们在上一步中添加的测试对象。在您运行脚本的机器上,我们将文件放置在<bucket_name>/<file_path>它不会与原始文件冲突的位置
minio_client.fget_object(config[ "dest_bucket" ], file_path, config[ "dest_bucket" ] + "/" + file_path)
获取存储桶中的对象列表以确认我们的文件在那里
对于minio_client.list_objects中的obj (config[ "dest_bucket" ]): print(obj) print( "Some objects here" )
输出将如下所示。我们添加的文件显示为 Python 对象。现在我们知道物体在那里。
<minio.datatypes.Object object at 0x109b1d3d0 >这里有一些对象
使用以下命令运行脚本。您应该会在存储桶中看到新对象。
$ python minio_app.py
这可以通过浏览器上的控制台 UI 进行验证http://localhost:9001。
现在我们在您运行脚本的机器上拥有了对象,让我们从 MinIO 存储桶中删除它并验证那里没有其他对象。
minio_client.remove_object(config[ "dest_bucket" ], file_path) for obj in minio_client.list_objects(config[ "dest_bucket" ]): print(obj) print( "No objects here" )
由于这是唯一的对象,现在我们可以删除我们之前创建的存储桶
if minio_client.bucket_exists(config[ "dest_bucket" ]): minio_client.remove_bucket(config[ "dest_bucket" ]) print( "目标桶 '%s' 已被删除" % (config[ "dest_bucket" ]))
请记住,我们在本教程中使用了一个使用 MinIO SDK 的非常简单的应用程序。由此,您可以轻松了解如何在您自己的应用程序中包含跟踪。虽然在构建应用程序时添加跟踪是理想的,但添加它并利用它提供的洞察力永远不会太晚。
OpenTelemetry 是一个框架,它允许您从应用程序中获取跟踪、指标和日志,并将它们标准化,以便它们可以被 Jaeger 等许多导出器使用。
与 MinIO 一样,OpenTelemetry 支持许多SDK。有些功能比其他功能更丰富,但已构建具有所有功能的 SDK 之一是Python SDK。我们需要安装两个 Python 包
$ pip install opentelemetry-api $ pip install opentelemetry-sdk
安装所需的包后,将它们导入到minio_app.py我们在上一节中启动的脚本中。
导入以下包
从opentelemetry导入跟踪从opentelemetry.sdk.trace导入TracerProvider从opentelemetry.sdk.trace.export导入ConsoleSpanExporter从opentelemetry.sdk.trace.export导入BatchSpanProcessor从opentelemetry.sdk.resources导入SERVICE_NAME,资源
在资源属性中设置服务名称,以便在搜索时轻松找到跟踪。我们为服务命名,my-minio但您可以随意命名。此代码完成并初始化跟踪
资源 = 资源(属性 = { SERVICE_NAME:“my-minio” })提供者 = TracerProvider(资源 = 资源)处理器 = BatchSpanProcessor(ConsoleSpanExporter())提供者.add_span_processor(处理器)trace.set_tracer_provider(提供者)跟踪器 = trace.get_tracer( 姓名)
我们已经创建了所有的构建块;现在让我们创建一个可以观察的跨度。但什么是跨度?简单来说,跨度只不过是函数发出的单个请求的开始和结束。可能有父跨度和子跨度;这些一起形成一个痕迹。
我们提出的第一个请求是检查存储桶是否存在。让我们为它创建一个跨度
使用tracer.start_as_current_span( "检查存储桶是否存在" ):
此时脚本应该看起来像这样
from minio import Minio from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.resources import SERVICE_NAME, Resource # 方便的字典basic config config = { "dest_bucket" : "processed" , # 这将是自动创建的"minio_endpoint" : "localhost:9000" , "minio_username" : "minio" , "minio_password" : "minioadmin" , } # 初始化 MinIO 客户端minio_client = Minio(config[ "minio_endpoint" ], secure= False , access_key=config[ "minio_username" ] , secret_key=config[ "minio_password" ] ) # 初始化 OpenTelemetry 提供程序resource = Resource(attributes={ SERVICE_NAME: "my-minio" }) provider = TracerProvider(resource=resource) processor = BatchSpanProcessor(ConsoleSpanExporter()) provider.add_span_processor(processor) trace.set_tracer_provider(provider) tracer = trace.get_tracer(name) with tracer.start_as_current_span( "check if bucket exists" ): #如果目标 bucket 不存在,则创建它minio_client.bucket_exists (config[ "dest_bucket" ]): minio_client.make_bucket(config[ "dest_bucket " ]) 打印( "已创建目标存储桶 '%s'" % (config[ "dest_bucket" ])) ...截断...
跨度可以发送到多个导出器,但首先我们只需将跟踪发送到 CLI。如果您运行上面的脚本,您应该会在最后看到 JSON 输出,如下所示。这是你的踪迹。
$ python3 minio_app.py Destination Bucket 'processed'已创建<minio.datatypes.Object object at 0x103f36eb0 > Some objects here Destination Bucket 'processed'已被删除{ "name" : "check if bucket exists" , "context" : { “trace_id” :“0xef41e07cf082045a2fc4eea70fd1a6de” ,“span_id” :“0x867c14fe1fd97590” ,“trace_state” :“[]” },“kind” :“SpanKind。内部” ,“parent_id” :空, “start_time” :“2022-09-14T20:49:15.569511Z” ,“end_time” :“2022-09-14T20:49:15.599886Z” ,“status” :{ “status_code” :“UNSET” },“属性" : {}, "events" : [], "links" : [], "resource" : { "attributes" : { "service.name" : "my-minio" }, "schema_url" : "" } }
通过添加其他属性,可以通过多种方式自定义每个跨度。让我们添加一个function.name带值的属性CHECK_BUCKET
使用tracer.start_as_current_span( "检查存储桶是否存在" ): current_span = trace.get_current_span() current_span.set_attribute( "function.name" , "CHECK_BUCKET" )
如果您再次重新运行脚本,您会注意到在输出中会有一个新属性
...截断... },“属性” :{ “function.name” :“CHECK_BUCKET” },“事件” :[], ...截断...
您还可以添加事件以进一步丰富跟踪
current_span.add_event( "检查桶是否存在。" ) if not minio_client.bucket_exists(config[ "dest_bucket" ]): current_span.add_event( "桶不存在,要创建它。" ) minio_client.make_bucket(config[ " dest_bucket" ])
再次运行脚本,您会注意到 JSON 输出中有两个新事件
...截断... "events" : [ { "name" : "检查存储桶是否存在。" , "timestamp" : "2022-09-14T21:09:48.505709Z" , "attributes" : {} }, { "name" : "bucket 不存在,要创建它。" , "时间戳" : "2022-09-14T21:09:48.514541Z" , "属性" : {} } ], ...截断...
到目前为止,我们只添加了一个跨度。为了使它更有用,让我们添加更多的跨度以及其他属性和事件。通常添加更多跨度不会降低应用程序的性能,因为这些是异步请求。
考虑到这一点,这里的脚本更新了一些额外的跨度
from minio import Minio from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.resources import SERVICE_NAME, Resource # 方便的字典basic config config = { "dest_bucket" : "processed" , # 这将是自动创建的"minio_endpoint" : "localhost:9000" , "minio_username" : "minio" , "minio_password" : "minioadmin" , } # 初始化 MinIO 客户端minio_client = Minio(config[ "minio_endpoint" ], secure= False , access_key=config[ "minio_username" ] , secret_key=config[ "minio_password" ] )资源 = Resource(attributes={ SERVICE_NAME: "my-minio"})提供者 = TracerProvider(resource=resource)处理器 = BatchSpanProcessor(ConsoleSpanExporter()) provider.add_span_processor(processor) trace.set_tracer_provider(provider) tracer = trace.get_tracer(name) # 如果目标bucket不存在就创建目标bucket with tracer.start_as_current_span( "check if bucket exists" ): current_span = trace.get_current_span() current_span .set_attribute( "function.name" , "CHECK_BUCKET" ) current_span.add_event( "检查桶是否存在。" ) if not minio_client.bucket_exists(config[ "dest_bucket" ]): current_span.add_event( "桶不存在,继续去创造它。”)与 tracer.start_as_current_span( "create bucket" ): minio_client.make_bucket(config[ "dest_bucket" ]) current_span.add_event( "bucket has been created." ) print( "Destination Bucket '%s' has been created" % (config[ "dest_bucket" ])) with tracer.start_as_current_span( "create object to add" ): current_span = trace.get_current_span() current_span.set_attribute( "function.name" , "CREATE_OBJECT" ) # 创建一个测试对象 file_path = "test_object.文本” f = 打开(文件路径,“w” ) f.write( "created test object" ) f.close() current_span.add_event( "Test object has been created." ) #用tracer.start_as_current_span( "add created object to bucket" ) 将对象放入桶中: current_span = trace.get_current_span() current_span.set_attribute( "function.name" , "CREATE_OBJECT" ) minio_client.fput_object(config[ "dest_bucket" ], file_path, file_path) current_span.add_event( "测试对象已放入存储桶中。" ) # 从桶中获取对象 tracer.start_as_current_span( "从桶中获取对象" ): current_span = trace.get_current_span() current_span.set_attribute( "function.name" , "FETCH_OBJECT" ) minio_client.fget_object(config[ "dest_bucket" ], file_path, config[ "dest_bucket " ] + "/" + file_path) current_span.add_event( "Test object has been fetched from bucket." ) #在minio_client.list_objects(config[ "dest_bucket" ]) 中获取 obj 的对象列表:print ( obj) print( "这里有一些对象” ) #使用tracer.start_as_current_span( "remove object from bucket" ) 从存储桶中删除对象: current_span = trace.get_current_span() current_span.set_attribute( "function.name" , "REMOVE_OBJECT" ) minio_client.remove_object(config[ "dest_bucket" ] , file_path) current_span.add_event( "Test object has been removed from bucket." ) #在minio_client.list_objects(config[ "dest_bucket" ]) 中获取 obj 的对象列表:print ( obj) print( "No objects here" ) # 如果目标存储桶确实存在,则删除它tracer.start_as_current_span( "检查存储桶是否存在" ): current_span = trace.get_current_span() current_span.set_attribute( "function.name" , "REMOVE_BUCKET" ) current_span.add_event( "检查存储桶是否存在存在。" ) if minio_client.bucket_exists(config[ "dest_bucket" ]): current_span.add_event( "bucket 存在,将删除它。" ) with tracer.start_as_current_span( "delete bucket" ): minio_client.remove_bucket(config[ "dest_bucket" ]) current_span.add_event( "桶已被删除。" ) print( "目标桶'%s'已被删除" % (config[ "dest_bucket" ]))
这些只是几个例子;您可以尽可能详细地使您的跟踪对您的团队有所帮助。你可以测量
数据库调用的性能
AI/ML 作业的处理时间和性能
连接到外部服务时的延迟
但是有一个问题:如果你现在尝试运行脚本,它会运行,但是你会遇到一堵可能比只有一个单跨。为了理解这些痕迹,我们需要一个工具来收集和处理它们。
您可以使用许多工具,但 Jaeger 是最受欢迎的工具之一。它像 MinIO 一样很容易启动和运行,并且像 MinIO 一样,功能非常丰富,可以帮助您进行根本原因分析和服务依赖性分析等工作。
我们将 Jaeger 部署为 Docker 容器并公开必要的端口
安装 Jaeger 容器
$ docker run -d --name jaeger -p 16686 : 16686 -p 6831 : 6831 /udp jaegertracing/all - in - one
以上将暴露两个端口localhost
6831: thrift 服务器端口,这是接受跟踪的入站端口
16686: Jaeger UI 让您可视化跟踪
访问http://localhost:16686/访问 Jaeger UI
目前,我们的跟踪正在发送到 CLI。现在我们将稍微修改 Python 脚本以将它们发送到我们刚刚创建的 Jaeger 容器。
安装 OpenTelemetry 的 Jaeger 导出器
$ pip install opentelemetry-exporter-jaeger
通过替换以下行在 Python 中导入包
从opentelemetry.sdk.trace.export导入ConsoleSpanExporter
和
从opentelemetry.exporter.jaeger.thrift导入JaegerExporter
添加 Jaeger 导出器主机信息
jaeger_exporter = JaegerExporter( agent_host_name= "localhost" , agent_port= 6831 , )
替换以下行
处理器 = BatchSpanProcessor(ConsoleSpanExporter())
和
处理器 = BatchSpanProcessor(jaeger_exporter)
最终结果看起来像这样
...截断... 从opentelemetry.exporter.jaeger.thrift导入JaegerExporter ...截断... jaeger_exporter = JaegerExporter( agent_host_name= "localhost" , agent_port= 6831 , ) provider = TracerProvider(resource=resource) processor = BatchSpanProcessor(jaeger_exporter) provider.add_span_processor(processor) trace.set_tracer_provider(provider) tracer = trace.get_tracer(name) ...截断...
重新运行脚本。不要看到发出的 JSON blob(记住我们的文本墙),而是转到 Jaeger UI,在左侧您将看到该my-minio服务。选择它,然后单击Find Traces。
到目前为止,我们只提出了一个请求,并且应该从我们创建的几个跨度中得到一些痕迹。
单击显示的痕迹之一2 Spans;让我们选择一个说“检查桶是否存在”的。您可以以一种比 JSON Blob 更实用的方式查看所有细节,从而永远消除文本墙并重新提高效率。
运行脚本五到六次后,我们可以开始看到模式出现。您在上面看到的时间是执行不同跨度所花费的时间。我们在这里看到两个跨度,因为如果您还记得我们添加了两个父跨度和子跨度。这不仅在视觉上比怪物 JSON blob 更具吸引力,而且也更有帮助。
然后,您可以将此数据从 Jaeger 发送到 Grafana,以便获取历史图表,甚至可以根据某些阈值设置警报。例如,如果 AI/ML 作业执行其功能的时间超过 10 毫秒,则根据设置的阈值发出警报。您的应用程序在哪个环境中运行并不重要。你可以确保你可以用一块玻璃来保存手表。
一旦我们有了跟踪,您就想建立一个历史数据库,您可以回顾它以查看趋势和相关性。这就是指标派上用场的地方。OpenTelemetry 支持完整的指标缓存和值得一试的日志框架。
跟踪只是通往可观察性道路上的一小部分。通常,当事件发生时,我们用来确定和解决问题的不仅仅是一个跟踪、一个日志或一个指标。通常,要了解根本原因所需的这些事物的组合。
可观察性为自动化打开了大门。我们是自动化的忠实拥护者,但为了在云规模上高效运行,您需要对日志和应用程序性能有一个坚实的基础和可见性。
如果您需要有关 MinIO 的 Python SDK 或如何在您的 MinIO 应用程序中实现跟踪的任何帮助,请随时通过Slack与我们联系。