Observability:从零基础到能够完成微服务可观测性的专家 - Service Map 实践

本文详细介绍了如何使用ElasticStack,特别是APM,对包含1000+微服务的大型IT系统进行性能监控和故障排查,通过实例演示了从零开始实现微服务可观测性,包括ServiceMap、交易跟踪和日志整合。

现在的 IT 系统越来越复杂,而微服务也被广泛使用于越来越多的大型 IT 系统中。 微服务是一种软件开发技术- 面向服务的体系结构(SOA)架构样式的一种变体,将应用程序构造为一组松散耦合的服务。在微服务体系结构中,服务是细粒度的,协议是轻量级的。

对于一些大型的 IT 系统来说,微服务的个数可能达到 1000 多个或者更多。如果我们的系统变得很慢,我们想查出是哪个环节出了问题。如果没有一个很好的可观测性的工具。我们有时是一头的雾水。很幸运的是 Elastic Stack 提供了一套完整的 APM (应用性能监控)可观测性软件栈,为我们对微服务的调试提供了完美的解决方案。

在今天的文章中,我们将使用一个简单的例子来展示如何从0基础到一个掌控微服务可观察性的专家。你不需要具有先前的很多知识。对于 Elastic APM 不是很熟的开发者来说,你可以阅读我之前的文章  “Elastic:应用程序性能监控/管理(APM)实践”。

在今天的实践中,我将使用如下的代码来进行展示:

git clone https://github.com/liu-xiao-guo/from-zero-to-hero-with-observability

在做实验之前,请使用上面的命令下载代码。

Service Map 是应用程序体系结构中已检测服务的实时可视表示。 它显示了这些服务的连接方式,以及诸如平均交易持续时间,每分钟请求数和每分钟错误数之类的高级指标。 如果启用,服务图还将与机器学习集成-基于异常检测分数的实时健康指标。 所有这些功能都可以帮助你快速直观地评估服务的状态和运行状况。上面的例子的微服务服务图如下:

整个软件有如下的几个部分组成:

  • h2:是一个本地数据库
  • backend-java :是一个 Spring 的网路服务器。它接受来自 fronend-react 的数据请求
  • localhost:3000: 是一个服务器,它用作数据展示
  • backend-golang:它是一个由 Golang 写的服务,可以访问 redis 数据库

在下面,我们一步一步地来展示如何从 0 开始启动微服务的可观测性。我将以 7.10 版本为例来进行展示。

安装

Elasticsearch 及 Kibana

我们可以按照我们的文章 “Elastic:开发者上手指南” 来安装及运行我们的 Elasticsearch 及 Kibana。安装完后,并安装相应的指令分别进行运行。

APM server 

我们接下来安装 APM 服务器。打开 Kibana:

我们可以根据自己的操作系统来分别进行安装。在我的实验中,我将以 macOS 为例来进行展示。通过这种安装的好处是它永远可以匹配你当前运行的 Elasticsearch 及 Kibana 的版本,同时你也可以找到适合自己 OS 的 APM Server 的安装方法。

在我们启动 APM 服务器之前,我们必须修改 APM server 安装根目录下的配置文件 apm-server.yml。我们必须在这个文件的最后部分添加如下的一句话:

apm-server.rum.enabled: true

这个原因是因为在我们的实验中有 frontend-react 这个服务。我们通过打开 RUM (Real User Monitoring) 可以监视从网页发出的请求。

我们可以通过如下的方法来进行运行 APM server:

如果一切正常,我们可以看到如上所示的信息。它表明我们的 APM  server 已经成功地被安装好了。

Redis 

在我们的实践中,我们也使用 redis 存储。如果大家还没安装好自己的 redis 的话,我们可以参考我之前的文章 “使用 Elastic Stack 对 Redis 监控” 来对 Redis 进行安装。

你可以查看一下你下载的项目 GitHub - liu-xiao-guo/from-zero-to-hero-with-observability: From Zero to Hero with Observability。里面有一个叫做 dump.rdb 的文件:

$ pwd
/Users/liuxg/demos/from-zero-to-hero-with-observability
liuxg:from-zero-to-hero-with-observability liuxg$ ls
LICENSE            backend-golang     docker-compose.yml images
README.md          backend-java       frontend-react     redis-data
liuxg:from-zero-to-hero-with-observability liuxg$ ls redis-data/
dump.rdb

这个是 redis 的数据文件。我们可以直接把这个文件拷贝到 macOS 的如下目录:

$ pwd
/usr/local/var/db/redis
liuxg:redis liuxg$ ls
dump.rdb         redis-server.log redis.log

这样当我们启动 redis 的时候,我们可以看到预先配置好的数据。我们通过如下的方法来运行 redis:

sudo redis-server /usr/local/etc/redis.conf

一旦 redis 运行成功后,我们可以使用如下的命令来进行检查:

$ redis-cli 
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> keys *
 1) "ferrari"
 2) "toyota"
 3) "koenigsegg"
 4) "tesla"
 5) "bugatti"
 6) "mclaren"
 7) "exotic-cars"
 8) "nissan"
 9) "mercedes"
10) "lamborghini"
11) "base-price-default"
12) "lexus"
13) "ford"
127.0.0.1:6379> 

我们可以看到 redis 运行于默认的端口 6379 上。如果你能看到上面的输出,则表明你的配置是成功的。

至此,我们的安装以及全部完成。接下来我们需要来完成各个服务的启动。

启动服务

在这个章节里,我将来启动各个服务。

backend-golang

这个是一个 Golang 的服务。在这个项目中有一个叫做 run-locally.sh 的脚本文件。我们打开这个文件,并做如下的配置:

#!/bin/bash
# set -x

export ELASTIC_APM_SERVER_URL=http://localhost:8200
export ELASTIC_APM_SECRET_TOKEN=
export REDIS_URL=127.0.0.1:6379

go build -o backend-golang
./backend-golang >> backend-golang.json

在上面,我们配置了 APM  Server 的地址。由于它可以访问 redis,所以我也配置 redis 的访客地址及端口。

这样我们的配置就基本完成了。当我们编译并运行时可能会出现不能访问 github 的一些库的情况。我们可以在 terminal 中先执行如下的命令,然后再执行 run-locally.sh:

export GO111MODULE=on
export GOPROXY=https://goproxy.io

然后再执行:

./run-locally.sh 

这样我们就完成了 frontend-react 的启动工作了。

backend-java

首先,我们打开地址:Maven Central Repository Search,并找到最新的 elastic-apm-agent 的版本号码:

在上面显示有一个叫做 1.19.0 的发布版。我们可以点击右边的下载按钮进行直接下载,并拷贝到 backend-java 的根目录下。或者,我们直接有如下的 run-locally.sh 来帮我们进行下载。

我们接下来配置 backend-java。打开这个项目的根目录,我们找 run-locally.sh 这个脚本文件:

在上面我们必须修改 AGENT_VERSION 这个变量的值。如果我们没有下载 elastic-apm-agent 的话,在下来的 curl 指令会帮我们下载。这个依赖于你的下载速度。

我们做如下的配置:

export ELASTIC_APM_SERVER_URL=http://localhost:8200
export ELASTIC_APM_SECRET_TOKEN=
export ESTIMATOR_URL=http://localhost:8888

我们通过如下的命令来运行这个服务:

/run-locally.sh 

当我们成功运行时,我们可以看到:

这是一个 Spring 的 Web 服务。

frontend-react

这个是我们的前端。我们打开这个项目,并找到 run-locally.sh 脚本文件。

我们对它作如下的配置:

export ELASTIC_APM_SERVER_URL=http://localhost:8200
export BACKEND_URL=http://localhost:8080

我们在运行 run-locally.sh 之前,需要使用使用如下的命令来安装 env-cmd:

npm install env-cmd

然后,我们使用如下的命令来启动:

./run-locally.sh

这样我们的 frontend-react 启动起来了。我们可以在浏览器中访问 http:.//localhost:3000:

从上面,我们可以看出来这是一个显示汽车信息及价格的一个列表。我们可以直接在网页上点击每个项进行修改,删除或创建一个新的汽车。

通过 APM 来展示微服务的可观察性

展示 Service Map

我们直接进入 Obverability overview 页面:

从上面的界面显示,我们可以看出来有3个 Services。我们点击 View in app:

从上面我们可以看出来有三个服务:backend-java, frontend-react 以及 backend-golang。我们点击 Service Map:

我们可以点击每个节点,并查看详细信息:

从上面的图,我们可以看出来 frontend-react 调用 backend-java,而 backend-java 调用 h2 数据库。到目前为止 backend-goland 是单独的一个服务。它和其它的服务没有任何的联系。我们接下来在 localhost:3000 来创建一个新的汽车:

点击上面的 Save 按钮:

我们可以看到新添加的叫做 Hyundai 的汽车。这个时候,我们重新刷新我们之前的 Service Map 界面:

这个时候,我们会发现 Service Map 有了新的变化。 backend-java 这个时候调用 backend-golang 服务了。

我们接下来查看一个典型的 transaction:

从上面我们可以看出从界面点击 New Car 所创建的一个 transaction 经历的所有 span。每个 span 都有相应的执行时间。我们很清楚整个调用的时间是花在哪里。如果我们的应用出现性能问题,我们很容从上面的图中看出来。上面的每个不同的颜色代表不同的微服务或数据库访问。我们可以点进每个 span 去查看具体的执行。比如点击上面的 INSERT INTO car:

这个就是 APM 最好的地方。它很清楚地展示了我们的代码的执行情况。

调试应用

我们接下来使用 UI 来创建一个新的汽车:

我们按照如上所示的数据来添加一个叫做 Ferrari (法拉利)的汽车。点击 Save 按钮:

我可以看到一个新增加的一个 Ferrari 汽车,但是我们会发现这次的操作和之前添加 Hyundai 所需要的时间要长很多。它需要花去5秒钟的时间。这到底是为什么呢?我们必须找出问题所在的原因。

我们还是回到之前 Add car 的那个 transaction:

我们选择执行时间较长的那个 transaction:

我们很快地发现在 calculateEstimate 的 span 里,它几乎占据了整个的执行时间。将近5秒的时间。我们直接点击上面的链接:

首先我们不用想很多,它清楚地指出了在 backend-goland 服务中的 main.go 109 行代码有问题。点击 Metadata:

它显示 brand 是 Ferrari,model 是 2020年,生产日期是 2020 年。

我们直接打开 main.go 文件:

在上面的代码中,我们定义了一个叫做 calculateEstimate 的 span。在这个代码中,我们定义了 brand, model 以及 year。这些对应于我们上面显示的 metadata。

我们向下滚动追查 calculateEstimate 函数:

func calculateEstimate(ctx context.Context, brand string, model string, year int) Estimate {

	logger.Info("Value estimation for brand: "+brand,
		zap.String("event.dataset", eventDataset))

	estimate := Estimate{
		Brand: brand,
		Model: model,
		Year:  year,
	}

	brand = strings.ToLower(brand)

	// Retrieve the base price for the car
	redisConn := apmredigo.Wrap(redisPool.Get()).WithContext(ctx)
	defer redisConn.Close()
	basePrice, err := redis.Int(redisConn.Do("GET", brand))
	if err != nil {
		logger.Error(fmt.Sprintf("Error getting base price for '%s'", brand),
			zap.Error(err), zap.String("event.dataset", eventDataset))
	}
	if basePrice == 0 {
		basePrice, err = redis.Int(redisConn.Do("GET", basePriceDefault))
		if err != nil {
			logger.Error("Error getting base price default", zap.Error(err),
				zap.String("event.dataset", eventDataset))
		}
	}

	// Calculate mark up of 5% on top of the base price
	markUp := int(((float64(5) * float64(basePrice)) / float64(100)))

	// Exotic cars have an additional markup
	isExotic, err := redis.Bool(redisConn.Do("SISMEMBER", exoticCars, brand))
	if err != nil {
		logger.Error(fmt.Sprintf("Error checking if '%s' is exotic", brand),
			zap.Error(err), zap.String("event.dataset", eventDataset))
	}
	if isExotic {
		markUp += additionalMarkUp()
	}

	estimate.Estimate = basePrice + markUp
	return estimate

}

从上面的代码中,我们可以看出来有两个 Redis 操作:

  • GET
  • SISMEMBER

他们分别对应于我们之前显示的图:

那么我们的时间到底是花在哪里呢?我们先来查看如下的一个调用:

	// Exotic cars have an additional markup
	isExotic, err := redis.Bool(redisConn.Do("SISMEMBER", exoticCars, brand))
	if err != nil {
		logger.Error(fmt.Sprintf("Error checking if '%s' is exotic", brand),
			zap.Error(err), zap.String("event.dataset", eventDataset))
	}
	if isExotic {
		markUp += additionalMarkUp()
	}

在上面的 SISMEMBER 调用中它检查输入的汽车是否为 exotic (外来的)汽车。如果是需要调用  additionalMarkup()。这是一个模拟的针对外来汽车需要额外执行的函数。

我们打开 redis 进行检查:

$ redis-cli 
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> keys *
 1) "ferrari"
 2) "toyota"
 3) "koenigsegg"
 4) "tesla"
 5) "bugatti"
 6) "mclaren"
 7) "exotic-cars"
 8) "nissan"
 9) "mercedes"
10) "lamborghini"
11) "base-price-default"
12) "lexus"
13) "ford"
127.0.0.1:6379> SMEMBERS exotic-cars
1) "ferrari"
2) "mercedes"
3) "lamborghini"
4) "koenigsegg"
5) "bugatti"
6) "mclaren"
127.0.0.1:6379> 

从上面的图中,我们可以看出来 ferrari 确实是一个 exotic 的车,那么它需要执行如下的函数:

func additionalMarkUp() int {
	logger.Debug("Waiting for the market data...",
		zap.String("event.dataset", eventDataset))
	time.Sleep(5 * time.Second)
	return rand.Intn(3) * 10000
}

在上面的函数中,我们使用了一个 Sleep 5秒的办法把当前的线程停止5秒。这也就是为什么我可以看到整个 calculateEstimate 需要大约5秒的时间来完成的原因。

假如我们相对某段代码增加新的监视,我们可以仿照如下的办法来进行。我们重新编写 calculateEstimate()

func calculateEstimate(ctx context.Context, brand string, model string, year int) Estimate {

	logger.Info("Value estimation for brand: "+brand,
		zap.String("event.dataset", eventDataset))

	estimate := Estimate{
		Brand: brand,
		Model: model,
		Year:  year,
	}

	brand = strings.ToLower(brand)

	// Retrieve the base price for the car
	redisConn := apmredigo.Wrap(redisPool.Get()).WithContext(ctx)
	defer redisConn.Close()
	basePrice, err := redis.Int(redisConn.Do("GET", brand))
	if err != nil {
		logger.Error(fmt.Sprintf("Error getting base price for '%s'", brand),
			zap.Error(err), zap.String("event.dataset", eventDataset))
	}
	if basePrice == 0 {
		basePrice, err = redis.Int(redisConn.Do("GET", basePriceDefault))
		if err != nil {
			logger.Error("Error getting base price default", zap.Error(err),
				zap.String("event.dataset", eventDataset))
		}
	}

	// Calculate mark up of 5% on top of the base price
	markUp := int(((float64(5) * float64(basePrice)) / float64(100)))

	// Exotic cars have an additional markup
	isExotic, err := redis.Bool(redisConn.Do("SISMEMBER", exoticCars, brand))
	if err != nil {
		logger.Error(fmt.Sprintf("Error checking if '%s' is exotic", brand),
			zap.Error(err), zap.String("event.dataset", eventDataset))
	}
	if isExotic {
		myspan, ctx := opentracing.StartSpanFromContext(request.Context(), "additionalMarkUp")
		markUp += additionalMarkUp()
		myspan.Finish()
	}

	estimate.Estimate = basePrice + markUp
	return estimate
}

在上面,我为如下的代码进行了修改:

	if isExotic {
		myspan, ctx := opentracing.StartSpanFromContext(request.Context(), "additionalMarkUp")
		markUp += additionalMarkUp()
		myspan.Finish()
	}

我们相对 addtionalMarkup 的调用进行监视。最终在我们的 Add car 中会有一个相应的 additionalMarkup span 出现。为了能够是这个代码起作用。我们重新启动各个服务。我们在 UI 添加一个新的汽车 lamborghini。这显然是一个 exotic 汽车:

同样地,我们可以看到新添加的汽车:

由于 lamborghini (兰博基尼) 是一个 exotic 的汽车。毫无例外地我们可以发现它需要5秒的时间才能在页面上进行显示。

我们重新来打开 Add car 这个 transaction。一定要选最新这个 transation:

如上图所示,我们可以看到一个叫做 addtionalMarkUp 的 span。

运用 Filebeat 来提高可观测性

Elastic Stack 最大的优点就是可以把指标,日志以及 APM 集成到一个环境中提供全面的可观测性。在这节中,我们来安装 filebeat 来提高整个微服务的可观测性。首先我们按照之前的文章 “Beats 入门教程 (二)” 来进行安装 Filebeat。

我们使用如下的命令来启动对 System 模块的监控:

./filebeat modules enable system

我们接着修改 filebeat.yml 的配值文件:

filebeat.yml

filebeat.inputs:

# Each - is an input. Most options can be set at the input level, so
# you can use different inputs for various configurations.
# Below are the input specific configurations.

- type: log

  # Change to true to enable this input configuration.
  enabled: true

  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    - /var/log/*.log
    - /Users/liuxg/demos/from-zero-to-hero-with-observability/backend-golang/*.json
    - /Users/liuxg/demos/from-zero-to-hero-with-observability/backend-java/*.json

  json.keys_under_root: true
  json.overwrite_keys: true

我们修改 filebeat 的前面部分为上面的内容。上面的路径依赖于你自己的日志位置需要进行相应的修改。

我们接下来运行 filebeat:

./filebeat setup
./filebeat -e

上面显示连接到 Elasticsearch 是成功的。

上面的 Logs 中可以看出来有两中 logs。点击 View in App:

在上面它显示了目前所有的 Log。我们回到前段的界面,重新输入一个新的汽车:

点击 SAVE 按钮。我们回到 Logs 应用中:

当我们搜索的时候,我们会发现一些关于这个输入相关的 log。如上所示,我们可以找到 Test 相关的日志。

我们现在重新回到 APM 应用的界面。我们找到 Add car 这个 transaction。我们确保点击最新的一个 transaction。

点击上面的 Trace logs:

我们可以查看到当前 transaction 的所有日志。准确地说我们可以把 APM 和日志绑定在一起。在查看 APM 的同时,我们也可以查看日志。

总结

在本文章中,我详述了如何使用 Elastic Stack 来对一个多微服务的 IT 系统进行性能监视,并提供良好的可观测性。Elastic Stack 在同一个软件栈中同时提供日志,指标以及 APM 的全方位客观则行。对于开发者来说,我们可以利用这个来对我们的系统进行监视。

评论 24
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值