背景

公司所有微服务都是使用同一个监控elasticsearch集群,这个集群存放应用监控metrics,还有一些错误log日志包堆栈信息。公司以nodejs为主要开发语言,使用measured-core这个库。库里面提供常用统计指标histogram, timer, meter, gauge 等指标。应用监控上传间隔1分钟,5分钟。

集群配置

日志集群由5台集群组成,其中3台角色同时为data-node, master, 5台机器配置如下:

cpu intel 8 core
内存 64G
数据盘 2T * 2 SSD,两个盘各自独立数据盘
elasticsearch, logstash 6.2.4

整个集群有10 SSD 盘作为数据盘。由于日志、监控数据不是重要数据,备份数目为1。

指标日志收集

上传指标量通过logstash进行收集的,然后按应用名称和环境提交es制定index上,收集配置如下:

  1. input {
  2. http {
  3. host => "0.0.0.0"
  4. port => "{{ metrics_port }}" # 端口logstash接口metircs端口
  5. tags => ["metrics-reporter"]
  6. }
  7. }
  8. filter {
  9. if "metrics-reporter" in [tags] and [metrics] and [defaultLabels][app] {
  10. split {
  11. field => "[metrics]"
  12. }
  13. mutate {
  14. remove_field => [ "headers" ]
  15. }
  16. }
  17. }
  18. output {
  19. if "metrics-reporter" in [tags] and [metrics] and [defaultLabels][app] and [defaultLabels][nodeEnv] {
  20. # stdout { codec => json_lines }
  21. # stdout { codec => rubydebug }
  22. elasticsearch {
  23. # hosts => {{ es_discovery_hosts | to_json }}
  24. hosts => [
  25. {% for host in es_discovery_hosts %}
  26. "{{ host }}:{{ es_http_port }}"{% if not loop.last %},{% endif %}
  27. {% endfor %}
  28. ]
  29. index => "metrics-%{[defaultLabels][nodeEnv]}-%{[defaultLabels][app]}-%{+YYYY.MM.dd}"
  30. }
  31. }
  32. }

按照这个配置,APP每个环境每天都会生成一个索引。日志格式:metrics-环境名称-应用名称-YYYY.MM.DD

索引规模

每个应用不同环境,每天都会产生一个索引,格式大概这样metrics-环境名称-应用名称-YYYY.MM.DD,可以通过下面脚本查看当前索引数目,例如当天2021.8.20, 命令如下:

  1. $ curl -XGET http://<es-ip>:9200/_cat/indices/metrics-*-2021.08.20
  2. # 统计数目
  3. $ curl -XGET http://<es-ip>:9200/_cat/indices/*-2021.08.20 | wc -l

通过命令查询当天日志索引有221个。就是说,当日期切换时候,有211个新建索引初始化,这个数目不少,当时压力肯定比较大。创建空索引没有问题,但是logstash根据指标量变化增加新的mapping字段

监控图表情况

grafana界面上也是每天8点钟,大概有10分钟日志是段开的,图表如下:
企业微信截图_16294500084564.png
在APP错误日志里里面:

  • 上报logstash10秒超时
  • 代码收到nginx/logstash http 420并发过高错误码。

告警系统

  • 由于应用监控数据上报不成功,导致触发许多错误告警

elasticsearch日志里面报很多下面错误:

  1. [2021-08-16T08:01:49,253][DEBUG][o.e.a.a.i.m.p.TransportPutMappingAction] [es-log-node-224] failed to put mappings on indices [[[metrics-xxxxxxx-2021.08.16/6xJPWn1nRb-QsBfZrsQKZA]]], type [doc]
  2. org.elasticsearch.cluster.metadata.ProcessClusterEventTimeoutException: failed to process cluster event (put-mapping) within 30s
  3. at org.elasticsearch.cluster.service.MasterService$Batcher.lambda$onTimeout$0(MasterService.java:125) ~[elasticsearch-6.2.4.jar:6.2.4]
  4. at java.util.ArrayList.forEach(ArrayList.java:1257) ~[?:1.8.0_181]
  5. at org.elasticsearch.cluster.service.MasterService$Batcher.lambda$onTimeout$1(MasterService.java:124) ~[elasticsearch-6.2.4.jar:6.2.4]
  6. at org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingRunnable.run(ThreadContext.java:573) [elasticsearch-6.2.4.jar:6.2.4]
  7. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_181]
  8. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_181]
  9. at java.lang.Thread.run(Thread.java:748) [?:1.8.0_181]

太多的更新mapping任务。

问题分析

为什么在8点

由于elasticsearchlogstash统一使用UTC时间,北京时间是: UTC+8, 所以在8点时候,logstash接收到数据以后,切换新index去提交数据。

同一时刻创建大量索引

logstash在日期切换以后,接收每个app指标量以后,使用对应app新索引存放数据。这个索引是在接收数据,发现不存在,才新创建的。这个时刻大量索引去新建,elasticsearch每创建一个索引耗时比较大的,应用提交监控数据到logstash响应也变慢。同时应用那边上传失败也会不断重试,所以出现http 420并发过高错误码。

创建索引耗时

获取其中一个es索引属性如下,

  1. $ curl -XGET http://<es-ip>:<es-port>/<metrics-index-name>

截取里面mapping配置mapping.json, 如下:

  1. {
  2. "mappings": {
  3. "doc": {
  4. "dynamic_templates": [{
  5. "floats": {
  6. "match_mapping_type": "long",
  7. "mapping": {
  8. "type": "float"
  9. }
  10. }
  11. }],
  12. "properties": {
  13. "@timestamp": {
  14. "type": "date"
  15. },
  16. "@version": {
  17. "type": "text",
  18. "fields": {
  19. "keyword": {
  20. "type": "keyword",
  21. "ignore_above": 256
  22. }
  23. }
  24. },
  25. "defaultLabels": {
  26. "properties": {
  27. "app": {
  28. "type": "text",
  29. "fields": {
  30. "keyword": {
  31. "type": "keyword",
  32. "ignore_above": 256
  33. }
  34. }
  35. },
  36. "hostname": {
  37. "type": "text",
  38. "fields": {
  39. "keyword": {
  40. "type": "keyword",
  41. "ignore_above": 256
  42. }
  43. }
  44. },
  45. "nodeEnv": {
  46. "type": "text",
  47. "fields": {
  48. "keyword": {
  49. "type": "keyword",
  50. "ignore_above": 256
  51. }
  52. }
  53. },
  54. "pid": {
  55. "type": "float"
  56. }
  57. }
  58. },
  59. "host": {
  60. "type": "text",
  61. "fields": {
  62. "keyword": {
  63. "type": "keyword",
  64. "ignore_above": 256
  65. }
  66. }
  67. },
  68. "metrics": {
  69. "properties": {
  70. "labels": {
  71. "properties": {
  72. "file": {
  73. "type": "text",
  74. "fields": {
  75. "keyword": {
  76. "type": "keyword",
  77. "ignore_above": 256
  78. }
  79. }
  80. },
  81. "reportUrl": {
  82. "type": "text",
  83. "fields": {
  84. "keyword": {
  85. "type": "keyword",
  86. "ignore_above": 256
  87. }
  88. }
  89. }
  90. }
  91. },
  92. "name": {
  93. "type": "text",
  94. "fields": {
  95. "keyword": {
  96. "type": "keyword",
  97. "ignore_above": 256
  98. }
  99. }
  100. },
  101. "value": {
  102. "properties": {
  103. "gauge": {
  104. "type": "float"
  105. },
  106. "meter": {
  107. "properties": {
  108. "15MinuteRate": {
  109. "type": "float"
  110. },
  111. "1MinuteRate": {
  112. "type": "float"
  113. },
  114. "5MinuteRate": {
  115. "type": "float"
  116. },
  117. "count": {
  118. "type": "float"
  119. },
  120. "currentRate": {
  121. "type": "float"
  122. },
  123. "mean": {
  124. "type": "float"
  125. }
  126. }
  127. },
  128. "timer": {
  129. "properties": {
  130. "histogram": {
  131. "properties": {
  132. "count": {
  133. "type": "float"
  134. },
  135. "max": {
  136. "type": "float"
  137. },
  138. "mean": {
  139. "type": "float"
  140. },
  141. "median": {
  142. "type": "float"
  143. },
  144. "min": {
  145. "type": "float"
  146. },
  147. "p75": {
  148. "type": "float"
  149. },
  150. "p95": {
  151. "type": "float"
  152. },
  153. "p99": {
  154. "type": "float"
  155. },
  156. "p999": {
  157. "type": "float"
  158. },
  159. "stddev": {
  160. "type": "float"
  161. },
  162. "sum": {
  163. "type": "float"
  164. },
  165. "variance": {
  166. "type": "float"
  167. }
  168. }
  169. },
  170. "meter": {
  171. "properties": {
  172. "15MinuteRate": {
  173. "type": "float"
  174. },
  175. "1MinuteRate": {
  176. "type": "float"
  177. },
  178. "5MinuteRate": {
  179. "type": "float"
  180. },
  181. "count": {
  182. "type": "float"
  183. },
  184. "currentRate": {
  185. "type": "float"
  186. },
  187. "mean": {
  188. "type": "float"
  189. }
  190. }
  191. }
  192. }
  193. }
  194. }
  195. }
  196. }
  197. },
  198. "tags": {
  199. "type": "text",
  200. "fields": {
  201. "keyword": {
  202. "type": "keyword",
  203. "ignore_above": 256
  204. }
  205. }
  206. },
  207. "timestamp": {
  208. "type": "date"
  209. }
  210. }
  211. }
  212. }
  213. }

通过下面命令创建索引,把mapping应用进去:

$ curl -XPUT http://<es-ip>:<es-port>/metrics-环境名称-应用名称-2021.08.21 -H 'Content-Type: application/json' -d @"mapping.json"

测试创建一个index, curl需要5s才返回成功响应。如果logstash在同一时间点创建索引,阻塞时间肯定大于5s,导致提交logstash的请求延时变大,CPU消耗也变大。上面mapping是动态模板,logstash根据metrics字段生成的。

解决方法

面对集中式创建引起大量耗时,可以把集中式创建修改分时创建。

  • 可以通过列举当天索引,提取名称,把明天日期的标记字符加上去,逐个提交创建。
  • 索引mapping配置怎么办,通过从天对应日志index获取,配置明天索引上就可以。

由于都是动态字段,使用nodejs动态类型语言比较简单实现。下面截取nodejs代码。

列举上天所有索引

涉及到日志转字符串格式,使用moment库比较方便,和es通信使用elasticsearch库,es使用UTC时间,所以列举最好使用UTC时间,否则获取不到当天库就麻烦了。

// 环境对应 development, stage, production 环境
private async listMetricsIndexToday(env) {
    const utcDate = moment(Date.now())
      .utc()
      .format('YYYY.MM.DD')
      .toString()
    const index = `metrics-${env}-*-${utcDate}`
    const metrics = await this.client.cat.indices({ index, format: 'json' })
    // 加入活跃metrics判断和提取
    const activeMetrics = metrics.filter((metric) => {
      if (metric['docs.count'] > 0) {
        return true
      }
      return false
    })
    return activeMetrics
 }

上面代码相当下面http api操作:

export env=development
export d=2021.08.20

curl -XGET http://<es-ip>:<es-port>/_cat/indices/metrics-${env}-*-2021.08.20?format=json

读取索引mapping配置

列举索引后,可以通过索引数组获取每个索引名称,可以通名称获取索引配置信息。由logstash自动生成索引配置信息字段编排大概如下:

{
  // index name
  "metrics-xxxx-2021.08.20":{
    "mappings": {
      // mapping 配置信息, 创建的时候,mapping配置只要去
    }
  }
}

mapping需要通过索引名称字段上获取:

  private async getIndexMappings(index: string) {
    const resp = await this.client.indices.getMapping({ index })
    const mappings = resp[index]
    return mappings
  }

代码相当于下面 http api

curl -XGET http://<es-ip>:<es-port>/<index-name>/_mapping?pretty

预先创建明天索引

读取当前索引mapping,可以根据这个信息创建新索引。索引:metrics-环境名称-应用名称-YYYY.MM.DD,其实就是把最后-后面日期替换新日期。

获取当天日期字符传

由于es使用UTC时间的,所以每天北京时间8点以后,UTC日期才和北京时间日期是一致的。获取日期字符串时候做了一些处理; 8点前当天UTC日期比北京时间前一天

private getNextDateIndexString() {
    const now = moment(Date.now())
    // 8点以后UTC时间才进入今天
    if (now.hour() < 8) {
      const nextDayIndex = now.format('YYYY.MM.DD')
      return nextDayIndex
    } else {
      const nextDayIndex = now.add(1, 'days').format('YYYY.MM.DD')
      return nextDayIndex
    }
  }

遍历当天索引创建明天索引

创建明天索引,使用API比较简单:

private async createTomorrowMetricIndex(metricName: string, mappings) {
    const nextDayIndex = this.getNextDateIndexString()
    const indexName = `${metricName}-${nextDayIndex}`
    console.log('creating ', indexName, mappings)
    // 使用nodejs,无类型处理json比较简单直观
    await this.client.indices.create({
      index: indexName,
      timeout: '10s',
      body: {
        ...mappings
      }
    })
  }

下面遍历遍历当天index:

  private async createAllTomorrowMetricsIndex(env: string) {
    // 拉取当天index
    const metrics = await this.listMetricsIndexToday(env)
    for (const metric of metrics) {
      try {
        // 获取mapping信息
        const mappings = await this.getIndexMappings(metric.index)
        // 分离日期后index名称
        const metricName = this.splitIndexName(metric.index)
        console.log(metricName)
        // 创建这个metric明天index
        await this.createTomorrowMetricIndex(metricName, mappings)
        // 减少压力
        await P.delay(10 * 1000)
      } catch (err) {
        console.log(err)
      }
    }
  }

创建任务都单线程执行

整个程序单任务逐个执行,每个index创建以后都休眠10s,把创建压力平分平时时间

public async start() {
    await this.init()
    await this.createAllTomorrowMetricsIndex('production')
    await this.createAllTomorrowMetricsIndex('stage')
    await this.createAllTomorrowMetricsIndex('development')
 }

总结

当前elasticsearch集群硬件设施支持当前日志并发量能力还是充足的,只有由于创建压力集中在8点这个时间点导致数据丢失,可以通过脚本,把创建压力均衡分配平时时间。这样没有比较再增加硬件处理必要了。

修改以后:
企业微信截图_20210823162926.png

只有一点点锯齿,有可能切换index导致,最好一分钟数据提交时候切换明天index。这样已经解决索引应用metrics切换时候导致告警系统错误告警问题。