小言_互联网的博客

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

256人阅读  评论(0)

现在的 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 的文件:


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

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


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

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

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

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


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

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

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

 

启动服务

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

backend-golang

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


  
  1. #!/bin/bash
  2. # set -x
  3. export ELASTIC_APM_SERVER_URL=http://localhost:8200
  4. export ELASTIC_APM_SECRET_TOKEN=
  5. export REDIS_URL=127.0.0.1:6379
  6. go build -o backend-golang
  7. ./backend-golang >> backend-golang.json

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

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


  
  1. export GO111MODULE= on
  2. 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 指令会帮我们下载。这个依赖于你的下载速度。

我们做如下的配置:


  
  1. export ELASTIC_APM_SERVER_URL=http: //localhost:8200
  2. export ELASTIC_APM_SECRET_TOKEN=
  3. export ESTIMATOR_URL=http: //localhost:8888

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

/run-locally.sh 

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

这是一个 Spring 的 Web 服务。

 

frontend-react

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

我们对它作如下的配置:


  
  1. export ELASTIC_APM_SERVER_URL=http: //localhost:8200
  2. 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 函数:


  
  1. func calculateEstimate(ctx context.Context, brand string, model string, year int) Estimate {
  2. logger.Info( "Value estimation for brand: "+brand,
  3. zap.String( "event.dataset", eventDataset))
  4. estimate := Estimate{
  5. Brand: brand,
  6. Model: model,
  7. Year: year,
  8. }
  9. brand = strings.ToLower(brand)
  10. // Retrieve the base price for the car
  11. redisConn := apmredigo.Wrap(redisPool.Get()).WithContext(ctx)
  12. defer redisConn.Close()
  13. basePrice, err := redis.Int(redisConn.Do( "GET", brand))
  14. if err != nil {
  15. logger.Error(fmt.Sprintf( "Error getting base price for '%s'", brand),
  16. zap.Error(err), zap.String( "event.dataset", eventDataset))
  17. }
  18. if basePrice == 0 {
  19. basePrice, err = redis.Int(redisConn.Do( "GET", basePriceDefault))
  20. if err != nil {
  21. logger.Error( "Error getting base price default", zap.Error(err),
  22. zap.String( "event.dataset", eventDataset))
  23. }
  24. }
  25. // Calculate mark up of 5% on top of the base price
  26. markUp := int((( float64( 5) * float64(basePrice)) / float64( 100)))
  27. // Exotic cars have an additional markup
  28. isExotic, err := redis.Bool(redisConn.Do( "SISMEMBER", exoticCars, brand))
  29. if err != nil {
  30. logger.Error(fmt.Sprintf( "Error checking if '%s' is exotic", brand),
  31. zap.Error(err), zap.String( "event.dataset", eventDataset))
  32. }
  33. if isExotic {
  34. markUp += additionalMarkUp()
  35. }
  36. estimate.Estimate = basePrice + markUp
  37. return estimate
  38. }

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

  • GET
  • SISMEMBER

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

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


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

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

我们打开 redis 进行检查:


  
  1. $ redis-cli
  2. 127.0 .0 .1 :6379> ping
  3. PONG
  4. 127.0 .0 .1 :6379> keys *
  5. 1 ) "ferrari"
  6. 2 ) "toyota"
  7. 3 ) "koenigsegg"
  8. 4 ) "tesla"
  9. 5 ) "bugatti"
  10. 6 ) "mclaren"
  11. 7 ) "exotic-cars"
  12. 8 ) "nissan"
  13. 9 ) "mercedes"
  14. 10 ) "lamborghini"
  15. 11 ) "base-price-default"
  16. 12 ) "lexus"
  17. 13 ) "ford"
  18. 127.0 .0 .1 :6379> SMEMBERS exotic-cars
  19. 1 ) "ferrari"
  20. 2 ) "mercedes"
  21. 3 ) "lamborghini"
  22. 4 ) "koenigsegg"
  23. 5 ) "bugatti"
  24. 6 ) "mclaren"
  25. 127.0 .0 .1 :6379>

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


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

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

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


  
  1. func calculateEstimate(ctx context.Context, brand string, model string, year int) Estimate {
  2. logger.Info( "Value estimation for brand: "+brand,
  3. zap.String( "event.dataset", eventDataset))
  4. estimate := Estimate{
  5. Brand: brand,
  6. Model: model,
  7. Year: year,
  8. }
  9. brand = strings.ToLower(brand)
  10. // Retrieve the base price for the car
  11. redisConn := apmredigo.Wrap(redisPool.Get()).WithContext(ctx)
  12. defer redisConn.Close()
  13. basePrice, err := redis.Int(redisConn.Do( "GET", brand))
  14. if err != nil {
  15. logger.Error(fmt.Sprintf( "Error getting base price for '%s'", brand),
  16. zap.Error(err), zap.String( "event.dataset", eventDataset))
  17. }
  18. if basePrice == 0 {
  19. basePrice, err = redis.Int(redisConn.Do( "GET", basePriceDefault))
  20. if err != nil {
  21. logger.Error( "Error getting base price default", zap.Error(err),
  22. zap.String( "event.dataset", eventDataset))
  23. }
  24. }
  25. // Calculate mark up of 5% on top of the base price
  26. markUp := int((( float64( 5) * float64(basePrice)) / float64( 100)))
  27. // Exotic cars have an additional markup
  28. isExotic, err := redis.Bool(redisConn.Do( "SISMEMBER", exoticCars, brand))
  29. if err != nil {
  30. logger.Error(fmt.Sprintf( "Error checking if '%s' is exotic", brand),
  31. zap.Error(err), zap.String( "event.dataset", eventDataset))
  32. }
  33. if isExotic {
  34. myspan, ctx := opentracing.StartSpanFromContext(request.Context(), "additionalMarkUp")
  35. markUp += additionalMarkUp()
  36. myspan.Finish()
  37. }
  38. estimate.Estimate = basePrice + markUp
  39. return estimate
  40. }

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


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

我们相对 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


  
  1. filebeat.inputs:
  2. # Each - is an input. Most options can be set at the input level, so
  3. # you can use different inputs for various configurations.
  4. # Below are the input specific configurations.
  5. - type: log
  6. # Change to true to enable this input configuration.
  7. enabled: true
  8. # Paths that should be crawled and fetched. Glob based paths.
  9. paths:
  10. - /var/log/*.log
  11. - /Users/liuxg/demos/from-zero-to-hero-with-observability/backend-golang/*.json
  12. - /Users/liuxg/demos/from-zero-to-hero-with-observability/backend-java/*.json
  13. json.keys_under_root: true
  14. json.overwrite_keys: true

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

我们接下来运行 filebeat:


  
  1. ./filebeat setup
  2. ./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
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场