Solutions:如何使用Elastic APM来监控多语言微服务应用程序

微服务架构引入的挑战之一是了解应用程序的性能以及花费时间最多的能力。 Elastic Stack和Elastic APM可以为基于微服务的现代解决方案以及整体应用程序提供可观测性

应用程序性能监视(APM)结合了不同的技术,以提供相关的每个服务组件正在做什么,何时何地,何时以及持续多长时间的深入,透明和整体的视图。 APM展示了服务如何交互,在整个系统中进行transaction跟踪,并让您看到了瓶颈。 这些丰富的分析使我们能够更快地发现,调试和修复问题,从而改善了用户体验和业务效率。

另外,存储在Elasticsearch中的APM数据只是另一个索引-应用程序指标与其他运营数据的组合和关联变得易于使用。

可观察性和APM工具可以成为不同规模大小组织的DevOps培养催化剂。 他们将开发,运营,安全性,产品和其他团队聚集在一起,试图从各自的角度理解和改进其应用程序。

在此博客文章中,我们使用一个示例应用程序来演示如何使用Elastic APM来检测微服务应用程序,重点是分布式跟踪

这是应用程序架构的高级视图:

尽管是人为设计的最基本示例应用程序,但它超出了为“评估各种工具(监视,跟踪等)...”而构建的“ hello world”那样的最基本示例。原始项目是由其它地方建立分支而来,并且进行了一些修改和更新。 然后使用弹性APM对其进行检测。

显然,有多种方法可以配置代理并使用Elastic APM检测应用程序。 下面介绍的是众多选择中的一种-它可能并不是最好的设置或独特的设置方式,而是证明了一种良好的可观察性解决方案所提供的灵活性。

总览

该应用程序由五项服务组成:

  • Frontend是一个VueJS应用程序,它提供UI。
  • Auth API用Go编写,并提供授权功能。
  • TODOs API使用ExpressJS与NodeJS一起编写。 它为待办事项记录提供CRUD功能。 另外,它将“创建”和“删除”操作记录到Redis队列中,以便稍后可以由日志消息处理器处理。
  • User API是用Java编写的Spring Boot项目,并提供用户个人资料。
  • 日志消息处理器是用Python编写的队列处理器。 它从Redis队列中读取消息并将其打印到stdout。

该应用程序总共包括六个docker容器:

docker ps
CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                    NAMES
6696f65b1759        frontend                "docker-entrypoint.s..."   6 hours ago         Up 58 seconds       0.0.0.0:8080->8080/tcp   microservice-app-example_frontend_1
6cc8a27f10f5        todos-api               "docker-entrypoint.s..."   6 hours ago         Up 58 seconds       8082/tcp                 microservice-app-example_todos-api_1
3e471f2163ca        log-message-processor   "python3 -u main.py"       6 hours ago         Up 58 seconds                                microservice-app-example_log-message-processor_1
bc1f09716288        auth-api                "/bin/sh -c /go/src/..."   6 hours ago         Up 58 seconds       8081/tcp                 microservice-app-example_auth-api_1
7144abac0d7c        users-api               "java -jar ./target/..."   6 hours ago         Up 59 seconds       8083/tcp                 microservice-app-example_users-api_1
84416563c37f        redis                   "docker-entrypoint.s..."   6 hours ago         Up 59 seconds       6379/tcp                 microservice-app-example_redis-queue_1

如何设置和运行实验


先决条件

该演示在ESS上使用带有APM的Elastic Stack。 如果您没有Elasticsearch Service帐户,则可以设置免费试用版。 它默认附带一个APM服务器。如果您还不知道如何创建一个ESS的实例,请参阅我之前的文章“Elastic:在Elastic云上3分钟部署Elastic集群”。

该演示程序已在7.5.0和7.6.1上运行和测试。 这是所使用的简单测试集群实例的视图。

在构建时将安装最新版本的APM代理

该项目是使用docker-compose构建和运行的,因此您需要将其安装在本地计算机上。

配置

为了使示例应用程序在本地计算机上运行并配置为将APM数据发送到Elasticsearch Service集群,我们需要:

下载项目

git clone https://github.com/nephel/microservice-app-example.git

在项目的根目录下创建一个名为.env的文件,其内容如下:

cd microservice-app-example
vi .env

添加与我们的环境相对应的APM服务器的token和服务器URL值:

TOKEN=XXXX
SERVER=XXXX

可以从管理控制台上的以下页面获取值:

这些值作为环境变量从docker-compose.yaml文件传递到每个docker容器,例如:

ELASTIC_APM_SECRET_TOKEN: "${TOKEN}"
ELASTIC_APM_SERVER_URL: "${SERVER}"

运行项目

要启动上面的项目,请在项目的根目录中运行:

docker-compose up --build

 这将创建所有容器及其依赖项,并开始将应用程序跟踪数据发送到APM服务器。

首次使用docker-compose构建具有微服务及其依赖关系的应用程序时,将需要一些时间。

APM和微服务的配置处于详细调试模式。 这是有意的,因为它在设置和测试时很有用。 如果发现它太冗长,可以在docker-compose.yaml文件中进行更改。

在APM服务器配置端,我们需要启用RUM

apm-server.rum.enabled: true

在用户设置中会覆盖:

为了生成一些APM数据,我们需要访问应用程序并生成一些流量和请求。 为此,浏览至http://127.0.0.1:8080登录,将一些项目添加到每个用户的“待办事项”列表中,然后删除一些项目,然后注销并以其他用户重新登录并重复。

请注意,用户名/密码为:

  • admin/admin
  • johnd/foo
  • janed/ddd

这是一个简短的视频,展示了启动应用程序后和Kibana APM UI如何进行进行交互的:

Elastic APM Demo

微服务及其检测

这是在APM UI中显示的五种服务的视图:

每个服务将显示在列表中,并提供高级transaction,环境和语言信息。 现在,让我们看一下如何对每个微服务进行检测,以及我们在Kibana APM UI上看到的内容。

Frontend Vue JS

前端使用的技术是https://vuejs.org/,Elastic Real User Monitoring(RUM)代理支持该技术。

@elastic/apm-rum-vue软件包是通过npm通过添加到frontend/Dockerfile中安装的:

COPY package.json ./
RUN npm install
RUN npm install --save @elastic/apm-rum-vue
COPY . .
CMD ["sh", "-c", "npm start" ]

该软件包是在frontend/src/router/index.js中导入和配置的,如下所示:

...
import { ApmVuePlugin } from '@elastic/apm-rum-vue'
Vue.use(Router)
const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/login',
      name: 'login',
      component: require('@/components/Login.vue')
    },
    {
      path: '/',
      alias: '/todos',
      name: 'todos',
      component: require('@/components/Todos.vue'),
      beforeEnter: requireLoggedIn
    }
  ]
})
const ELASTIC_APM_SERVER_URL = process.env.ELASTIC_APM_SERVER_URL
Vue.use(ApmVuePlugin, {
  router,
  config: {
    serviceName: 'frontend',
    serverUrl: ELASTIC_APM_SERVER_URL,
    serviceVersion: '',
    logLevel: 'debug'
  }
})
...
...
export default router

除了自动检测量程外,还为各种组件添加了其他特定于组件的自定义量程,例如 在frontend/src/components /Login.vue中,添加了以下内容:

...
      span: null
    }
  },
  created () {
    this.span = this.$apm.startSpan('component-login', 'custom')
  },
  mounted () {
    this.span && this.span.end()
  }
...

page load事件和route-change事件均被捕获为transaction,并以路由的路径为其名称。

这是我们在检查与APM相关的索引和文档,过滤page load transaction时看到的:

您可以随时通过重新加载没有缓存的页面来创建页面加载事件和transaction(例如,在Mac上为Command-Shift-R)。

这是我们在前端服务中看到的交易的视图—请注意下拉菜单,我们可以根据交易类型进行过滤:

Auth API Golang Echo

此微服务是使用Elastic APM Go代理支持echo framework版本3在Golang中实现的。

主要更改和添加在auth-api/main.go文件中完成,其中导入了Elastic APM代理以及apmecho和apmhttp两个模块:
 

...
        "go.elastic.co/apm"
        "go.elastic.co/apm/module/apmecho"
        "go.elastic.co/apm/module/apmhttp"
        "github.com/labstack/echo/middleware"
...

echo中间件的检测在下面的第48行中完成:

...
 28 func main() {
 29         hostport := ":" + os.Getenv("AUTH_API_PORT")
 30         userAPIAddress := os.Getenv("USERS_API_ADDRESS")
 31
 32         envJwtSecret := os.Getenv("JWT_SECRET")
 33         if len(envJwtSecret) != 0 {
 34                 jwtSecret = envJwtSecret
 35         }
 36
 37         userService := UserService{
 38                 Client:         apmhttp.WrapClient(http.DefaultClient),
 39                 UserAPIAddress: userAPIAddress,
 40                 AllowedUserHashes: map[string]interface{}{
 41                         "admin_admin": nil,
 42                         "johnd_foo":   nil,
 43                         "janed_ddd":   nil,
 44                 },
 45         }
 46
 47         e := echo.New()
 48         e.Use(apmecho.Middleware())
 49
 50         e.Use(middleware.Logger())
 51         e.Use(middleware.Recover())
 52         e.Use(middleware.CORS())
 53
 54         // Route => handler
 55         e.GET("/version", func(c echo.Context) error {
 56                 return c.String(http.StatusOK, "Auth API, written in Go\n")
 57         })
 58
 59         e.POST("/login", getLoginHandler(userService)
...

此微服务向auth-api/user.go文件中实现的users-api微服务发出HTTP请求。

HTTP客户端的包装在上面的第38行完成,当请求从一个微服务移动到另一个微服务时,它使用分布式跟踪来跟踪传出的请求。

可以在此博客文章和相关文档中找到更多信息:

跟踪传出的HTTP请求

要跟踪传出的HTTP请求,您必须检测HTTP客户端,并确保将封装请求上下文传播到传出的请求。 被检测的客户端将使用它来创建span,并将标头注入到传出的HTTP请求中。 让我们看看实际情况
...
如果此传出请求由另一个装有Elastic APM的应用程序处理,您将最终获得“分布式跟踪”,即跨服务的跟踪(相关transaction和span的集合)。 被检测的客户端将注入一个标头,该标头标识出传出HTTP请求的跨度,接收服务将提取该标头,并将其用于将客户端span与其所记录的transaction相关联。

此外,添加了echo事务的跨度,涵盖了登录过程:

...
func getLoginHandler(userService UserService) echo.HandlerFunc {
        f := func(c echo.Context) error {
                span, _ := apm.StartSpan(c.Request().Context(), "request-login", "app")
                requestData := LoginRequest{}
                decoder := json.NewDecoder(c.Request().Body)
                if err := decoder.Decode(&requestData); err != nil {
                        log.Printf("could not read credentials from POST body: %s", err.Error())
                        return ErrHttpGenericMessage
                }
                span.End()
                span, ctx := apm.StartSpan(c.Request().Context(), "login", "app")
                user, err := userService.Login(ctx, requestData.Username, requestData.Password)
                if err != nil {
                        if err != ErrWrongCredentials {
                                log.Printf("could not authorize user '%s': %s", requestData.Username, err.Error())
                                return ErrHttpGenericMessage
                        }
                        return ErrWrongCredentials
                }
                token := jwt.New(jwt.SigningMethodHS256)
                span.End()
                // Set claims
                span, _ = apm.StartSpan(c.Request().Context(), "generate-send-token", "custom")
                claims := token.Claims.(jwt.MapClaims)
                claims["username"] = user.Username
                claims["firstname"] = user.FirstName
                claims["lastname"] = user.LastName
                claims["role"] = user.Role
                claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
                // Generate encoded token and send it as response.
                t, err := token.SignedString([]byte(jwtSecret))
                if err != nil {
                        log.Printf("could not generate a JWT token: %s", err.Error())
                        return ErrHttpGenericMessage
                }
                span.End()

Users-api SpringBoot

该服务使用Java代理支持的Spring Boot。 Spring Boot开箱即用非常简单。

使用用于自连接的程序化API设置安装了代理,位于users-api/src/main/java/com/elgris/usersapi/UsersApiApplication.java中:

...
import co.elastic.apm.attach.ElasticApmAttacher;
@SpringBootApplication
public class UsersApiApplication {
        public static void main(String[] args) {
                ElasticApmAttacher.attach();
                SpringApplication.run(UsersApiApplication.class, args);
        }
}
...

在users-api pom.xml中,添加了以下内容:

                <dependency>
                        <groupId>co.elastic.apm</groupId>
                        <artifactId>apm-agent-attach</artifactId>
                        <version>[1.14.0,)</version>
               </dependency>

下面是一个示例,其中包含登录过程的transaction和span的分布式跟踪。 我们将构成transaction和span的跟踪视为通过微服务的流。 从前端表单到POST请求到auth-API进行身份验证,后者依次向用户API发出GET请求以进行用户配置文件检索:

Todos-api NodeJS Express

“todo”服务使用Express框架,该框架由Elastic APM Node.js代理支持。

通过在Dockerfile中添加此代理程序来安装代理程序:

RUN npm install elastic-apm-node --save

为了初始化代理,在todos-api/server.js中添加了以下内容:

...
const apm = require('elastic-apm-node').start({
  // Override service name from package.json
  // Allowed characters: a-z, A-Z, 0-9, -, _, and space
  serviceName: 'todos-api',
  // Use if APM Server requires a token
  secretToken: process.env.ELASTIC_APM_SECRET_TOKEN,
  // Set custom APM Server URL (default: http://localhost:8200)
  serverUrl: process.env.ELASTIC_APM_SERVER_URL,
})
....

自定义span已添加到todos-api/todoController.js中,用于诸如在todo应用中创建和删除任务的操作:

...
    create (req, res) {
        // TODO: must be transactional and protected for concurrent access, but
        // the purpose of the whole example app it's enough
        const span = apm.startSpan('creating-item')
        const data = this._getTodoData(req.user.username)
        const todo = {
            content: req.body.content,
            id: data.lastInsertedID
        }
        data.items[data.lastInsertedID] = todo
        data.lastInsertedID++
        this._setTodoData(req.user.username, data)
        if (span) span.end()
        this._logOperation(OPERATION_CREATE, req.user.username, todo.id)
        res.json(todo)
    }
    delete (req, res) {
        const data = this._getTodoData(req.user.username)
        const id = req.params.taskId
        const span = apm.startSpan('deleting-item')
        delete data.items[id]
        this._setTodoData(req.user.username, data)
        if (span) span.end()
        this._logOperation(OPERATION_DELETE, req.user.username, id)
        res.status(204)
        res.send()
    }
...

日志通过Redis队列发送到Python微服务:

...
_logOperation(opName, username, todoId) {
 var span = apm.startSpan('logging-operation')
 this._redisClient.publish(
   this._logChannel,
   JSON.stringify({
     opName,
     username,
     todoId,
     spanTransaction: span.transaction
   }),
   function(err) {
     if (span) span.end()
     if (err) {
       apm.captureError(err)
     }
   }
 )
}
...

从上面可以看出,发送到Redis的数据包括opName和用户名以及span.transaction信息,该信息指向父transaction对象。

这是详细日志输出中捕获的spanTransaction的示例:

'spanTransaction': 
{
   "id":"4eb8801911b87fba",
   "trace_id":"5d9c555f6ef61f9d379e4a67270d2eb1",
   "parent_id":"a9f3ee75554369ab",
   "name":"GET unknown route (unnamed)",
   "type":"request",
   "subtype":"None",
   "action":"None",
   "duration":"None",
   "timestamp":1581287191572013,
   "result":"success",
   "sampled":True,
   "context":{
      "user":{
         "username":"johnd"
      },
      "tags":{
      },
      "custom":{
      },
      "request":{
         "http_version":"1.1",
         "method":"GET",
         "url":{
            "raw":"/todos",
            "protocol":"http:",
            "hostname":"127.0.0.1",
            "port":"8080",
            "pathname":"/todos",
            "full":"http://127.0.0.1:8080/todos"
         },
         "socket":{
            "remote_address":"::ffff:172.20.0.7",
            "encrypted":False
         },
         "headers":{
            "accept-language":"en-GB,en-US;q=0.9,en;q=0.8",
            "accept-encoding":"gzip, deflate, br",
            "referer":"http://127.0.0.1:8080/",
            "sec-fetch-mode":"cors",
            "sec-fetch-site":"same-origin",
            "elastic-apm-traceparent":"00-5d9c555f6ef61f9d379e4a67270d2eb1-a9f3ee75554369ab-01",
            "user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
            "authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODE1NDYzOTEsImZpcnN0bmFtZSI6IkpvaG4iLCJsYXN0bmFtZSI6IkRvZSIsInJvbGUiOiJVU0VSIiwidXNlcm5hbWUiOiJqb2huZCJ9.Kv2e7E70ysbVvP-hKlG-RJyfKSibmiy8kCO-xqm3P6g",
            "x-requested-with":"XMLHttpRequest",
            "accept":"application/json, text/plain, */*",
            "connection":"close",
            "host":"127.0.0.1:8080"
         }
      },
      "response":{
         "status_code":200,
         "headers":{
         }
      }
   },
   "sync":False,
   "span_count":{
      "started":5
   }
}

除其他事项外,它包括“ elastic-apm-traceparent”:“ 00-5d9c555f6ef61f9d379e4a67270d2eb1-a9f3ee75554369ab-01”。

Python微服务将使用此信息将此事务链接到父跟踪。 这将确保我们在待办事项应用程序请求遍历前端todos-api时进行分布式跟踪,然后通过Python处理器对其进行记录。

Log-message-processor Python

日志消息处理器服务未使用任何框架,例如Djangoflask,而是用Python3编写的,作为一个简单的使用者,它在Redis队列中侦听新消息。

在log-message-processor/requirements.txt中添加以下行:

elastic-apm

在log-message-processor/main.py中

该代理与TraceParent一起从elasticapm.utils.disttracing导入

...
import elasticapm
from elasticapm.utils.disttracing import TraceParent
from elasticapm import Client
client = Client({'SERVICE_NAME': 'python'})
...

使用了decorator选项来捕获log_message函数的范围:

...
@elasticapm.capture_span()
def log_message(message):
    time_delay = random.randrange(0, 2000)
    time.sleep(time_delay / 1000)
    print('message received after waiting for {}ms: {}'.format(time_delay, message))
...

transaction围绕日志记录开始和结束。 使用traceParent API,由Elastic-apm-transparent头(通过Redis队列发送)读取跟踪的父ID:

...
if __name__ == '__main__':
    redis_host = os.environ['REDIS_HOST']
    redis_port = int(os.environ['REDIS_PORT'])
    redis_channel = os.environ['REDIS_CHANNEL']
    pubsub = redis.Redis(host=redis_host, port=redis_port, db=0).pubsub()
    pubsub.subscribe([redis_channel])
    for item in pubsub.listen():
        try:
            message = json.loads(str(item['data'].decode("utf-8")))
        except Exception as e:
            log_message(e)
            continue
        spanTransaction = message['spanTransaction']
        trace_parent1 = spanTransaction['context']['request']['headers']['elastic-apm-traceparent']
        print('trace_parent_log: {}'.format(trace_parent1))
        trace_parent = TraceParent.from_string(trace_parent1)
        client.begin_transaction("logger-transaction", trace_parent=trace_parent)
        log_message(message)
        client.end_transaction('logger-transaction')
...

请注意,虽然TraceParent功能即将正式被归档,并且在本讨论主题中已提及并在此处进行了测试。

下面的屏幕快照是分布式跟踪以及基础transaction和span的示例。 它们显示了待办事项在微服务中流动时的创建和删除过程。 从前端到POST请求,再到todos-api及其创建和记录的span,以及后续的日志消息处理器transaction和span。

结论和下一步

我们亲眼目睹了如何使用不同的语言和框架来检测微服务应用程序。

我们没有探索很多领域,例如,Kibana中的Elastic APM应用程序还提供了错误和指标信息。

此外,Elastic APM允许您与从应用程序收集的日志关联。 这意味着您可以轻松地将跟踪信息注入到日志中,从而允许您在log应用中浏览日志,然后直接跳转到相应的APM跟踪中-保留了跟踪上下文。

此外,SIEM应用程序中还提供了APM收集的数据:

另一个有趣的领域是将APM数据与Elastic Stack的机器学习功能结合使用,例如 计算交易响应时间的异常分数。

关于以上任何问题,请随时与Elastic联系或在Discuss论坛平台上提交。

参考

【1】https://www.elastic.co/blog/how-to-instrument-a-polyglot-microservices-application-with-elastic-apm

发布了528 篇原创文章 · 获赞 132 · 访问量 93万+

猜你喜欢

转载自blog.csdn.net/UbuntuTouch/article/details/105224401