微服务架构引入的挑战之一是了解应用程序的性能以及花费时间最多的能力。 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

除了自动检测量程外,还为各种组件添加了其他特定于组件的自定义 span,例如 在 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-ap i微服务发出 HTTP 请求。

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

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

跟踪传出的 HTTP 请求

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

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

...
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

日志消息处理器服务未使用任何框架,例如 Django 或 flask,而是用 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

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐