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

144 篇文章 29 订阅

现在的 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 的全方位客观则行。对于开发者来说,我们可以利用这个来对我们的系统进行监视。

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 24
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值