Observability:从零基础到能够完成微服务可观测性的专家 – Service Map 实践
2021年1月9日 | by mebius
现在的 IT 系统越来越复杂,而微服务也被广泛使用于越来越多的大型 IT 系统中。 微服务是一种软件开发技术- 面向服务的体系结构(SOA)架构样式的一种变体,将应用程序构造为一组松散耦合的服务。在微服务体系结构中,服务是细粒度的,协议是轻量级的。
对于一些大型的 IT 系统来说,微服务的个数可能达到 1000 多个或者更多。如果我们的系统变得很慢,我们想查出是哪个环节出了问题。如果没有一个很好的可观测性的工具。我们有时是一头的雾水。很幸运的是 Elastic Stack 提供了一套完整的 APM (应用性能监控)可观测性软件栈,为我们对微服务的调试提供了完美的解决方案。
在今天的文章中,我们将使用一个简单的例子来展示如何从0基础到一个掌控微服务可观察性的专家。你不需要具有先前的很多知识。对于 Elastic APM 不是很熟的开发者来说,你可以阅读我之前的文章 “Solutions:应用程序性能监控/管理(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 进行安装。
你可以查看一下你下载的项目https://github.com/liu-xiao-guo/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
首先,我们打开地址:https://search.maven.org/search?q=a:elastic-apm-agent,并找到最新的 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
我们在运tgcode行 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 tgcode{
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 确实是一个 exotgcodetic 的车,那么它需要执行如下的函数:
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 的全方位客观则行。对于开发者来说,我们可以利用这个来对我们的系统进行监视。
文章来源于互联网:Observability:从零基础到能够完成微服务可观测性的专家 – Service Map 实践
相关推荐: Elasticsearch:Elasticsearch 开发入门 – Golang
在本文中,我将分享如何在 Golang 中如何使用Elasticsearch 来开发的经验。 顺便说一句,以防万一你从未听说过 Elasticsearch: Elasticsearch 是一个高度可扩展的开源全文本搜索和分析引擎。 它使你可以快速,近乎实时地存…