0x01 前言

elastic stack在4月11日发布了7.0正式版,而我也在第一时间完成家里测试环境的升级工作。因为我是从6.7.0升级到7.0的,同时经过x-pack升级助手的检查,升级前后均没问题。

但在升级filebeat之后却出现大量错误日志,而在此之前,我QQ群内一位朋友也发现类似的问题,而他找出了报错的原因。

0x02 BUG

首先是日志,为了方便查看,我对日志格式做了些调整:

[2019-04-14T23:17:43,051][WARN ][logstash.outputs.elasticsearch] 

Could not index event to Elasticsearch. {:status=>400, :action=>["index", 

{:_id=>nil, :_index=>"public-nginx-alias", :_type=>"_doc", :routing=>nil}, 

#<LogStash::Event:0x2efc3092>], 

:response=>{
  "index"=>{
    "_index"=>"public-nginx-index-v1", 
      "_type"=>"_doc", 
      "_id"=>"7NZsHGoB4Cf78Hum8oUF", 
      "status"=>400, 
      "error"=>{
        "type"=>"mapper_parsing_exception", 
          "reason"=>"failed to parse field [agent] of type [text] in document with id '7NZsHGoB4Cf78Hum8oUF'", 
          "caused_by"=>{
            "type"=>"illegal_state_exception", 
            "reason"=>"Can't get text on a START_OBJECT at 1:206"}
        }
    }
}

}

以上是logstash7.0的日志,而且这些日志只出现在使用7.0版本的filebeat服务器中。由以上日志信息可以发现处理名为agent的字段失败,而出现illegal_state_exception的原因一般是字段异常。

经过群友的耐心debug发现这个问题是因为我以前文章中logstash过滤器的grok规则导致的。

如果大家有关注我的博客,可以在以下文章中找到相关的nginx日志的grok规则:

以上文章中均包含以下字段:

# 分词规则
%{QS:agent}

# useragent分析规则
useragent {
  source => "agent"
  target => "ua"
}

它匹配的是日志中的useragent,如下所示:

"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36"

上述的分词规则会匹配被双引号包裹的相关字符,并赋值给名为agent的字段,最终导入到elasticsearch,结果如下:

而useragent分析规则则是从agent字段中取值并调用useragent 插件处理,处理完的结果赋值给名为ua的字段,结果如下:

    "ua": {
      "os_name": "Windows",
      "minor": "0",
      "build": "",
      "name": "Chrome",
      "os": "Windows",
      "device": "Other",
      "patch": "3683",
      "major": "73"
    },

这个日志分词和useragent分析功能在7.0之前是正常的;但从7.0开始,agent字段属于内置的保留字段,主要记录着该beat所在的主机的一些信息:

    "agent": {
      "id": "899ef414-7b63-4d75-b9dd-28e750504c44",
      "type": "filebeat",
      "hostname": "pub-ngx",
      "version": "7.0.0",
      "ephemeral_id": "9bcaed33-660f-45e0-9062-27aa860eb1bb"
    },

所以问题很简单,就是新旧版本的字段冲突了。

0x03 DEBUG

既然字段名冲突,那只需要修改字段名即可,但实际上没那么简单。

因为数据是不断传入logstash进行处理的,所以logstash和elasticsearch的服务不能长时间中断;其次是历史数据不能丢失。

因为我只有一个logstash节点,所以修改过滤器规则后重启logstash会导致一小段时间的服务中断,而且服务中断期间的日志会丢失。但对于我的环境来说,这一部分日志的丢失是可以接受的;如果需要高可用,则建议部署多个logstash节点后再进行调整。

首先修改过滤器规则:

[root@logstash6-node1 ~]# cat /etc/logstash/conf.d/10-nginx-fliter.conf 
filter {
  if "server_ngx_access_log" in [tags] {
    grok {
      match => { "message" => "%{IPORHOST:client_ip} - (%{GREEDYDATA:auth}|-) \[%{HTTPDATE:timestamp}\] \"(%{WORD:verb} %{GREEDYDATA:request} HTTP/%{NUMBER:http_version}|%{GREEDYDATA:request})\" (%{IPORHOST:domain}|%{URIHOST:domain}|-) %{NUMBER:response} %{NUMBER:bytes} %{QS:referrer} %{QS:ngx_ua} \"(%{IPORHOST:x_forword_ip}, .*|%{IPORHOST:x_forword_ip}|unknown|-)\" (%{IPORHOST:upstream_host}|-)(\:%{NUMBER:upstream_port}|) (%{NUMBER:upstream_response}|-) (%{WORD:upstream_cache_status}|-) \"%{NOTSPACE:upstream_content_type}(; charset\=%{NOTSPACE:upstream_content_charset}|)\" (%{NUMBER:upstream_response_time}|-) > %{NUMBER:request_time}" }
    }
 
    geoip {
      source => "client_ip"
    }

    geoip {
      source => "x_forword_ip"
      target => "x_forword_geo"
    }
 
    date {
      match => [ "timestamp" , "dd/MMM/YYYY:HH:mm:ss Z" ]
    }

    useragent {
      source => "ngx_ua"
      target => "ua"
    }

    mutate {
      split  => { "x_forword" => ", " }
    }

  }
}

我修改了grok规则中与useragent插件中的字段名称为ngx_ua,修改完成后即可重启logstash。

然后需要使用kibana中的Dev Tools调用elasticsearch API。首先建立一个新的索引:

PUT public-nginx-index-v3

然后修改别名的指向,在此之前先获取目前的别名指向情况:

GET _cat/aliases

接下来即可进行修改:

POST /_aliases
{
  "actions": [
    {
      "remove": {
        "index": "public-nginx-index-v2",
        "alias": "public-nginx-alias"
      }
    },
    {
      "add": {
        "index": "public-nginx-index-v3",
        "alias": "public-nginx-alias"
      }
    }
  ]
}

成功执行该API后,所有数据都会写入到public-nginx-index-v3索引中。

完成上述步骤后才可以进行历史数据字段名称的修改,要不然有可能会使历史数据受损或遭到污染。

对于历史数据,我才用较为简单的手段处理:将历史数据中的agent字段的数据传递给新字段ngx_ua,而后删除旧字段,最终将数据写入新索引。

这一系列步骤我通过reindex API完成:

POST _reindex
{
  "source": {
    "index": "public-nginx-index-v2",
  },
  "dest": {
    "index": "public-nginx-index-v3"
  },
  "script": {
    "inline": "ctx._source.ngx_ua = ctx._source.remove(\"agent\"); "
  }
}

上面命令的意思是将v2索引中的数据迁移至v3中,在这过程中将旧索引的agent字段赋值给新索引的ngx_ua字段后删除自身。

根据数据量的不同,reindex所需要的时间也各有长短,完成后即可删除该索引以减少空间消耗:

DELETE public-nginx-index-v2

0x04 结语

为了减少不必要的操作,确保数据安全与洁净,建议对历史数据操作前先通过快照API对数据做快照;另外,在进行实操前需进行全盘规划。

整个过程会对IO造成极大压力,请适当调整elasticsearch节点数以均衡负载,减少对线上业务的影响。