现在的 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
我们在运行 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 的全方位客观则行。对于开发者来说,我们可以利用这个来对我们的系统进行监视。
转载:https://blog.csdn.net/UbuntuTouch/article/details/110850836