Nginx 日志分析之 Loki

211次阅读
没有评论

共计 19253 个字符,预计需要花费 49 分钟才能阅读完成。

背景

ELK 是业内流行的日志组件,公司内也一直在使用。

近日来我的站点访问量慢慢增长,于是萌生了分析日志的念头。

由于个人环境日志量小且硬件资源有限,在一番比较后选择了更为轻量的 Grafana Loki​ 。

为什么选择 Loki

优点:

  1. 轻量级: Loki 专注于提供一种更轻量级的日志聚合解决方案,它的存储需求通常比 ELK 栈要少。
  2. 成本效益: 由于其存储方式,Loki 可能在存储成本上更有优势,特别是在大规模部署时。
  3. 与 Grafana 集成: Loki 是为了与 Grafana 紧密集成而设计的,这意味着如果你已经在使用 Grafana,那么添加 Loki 作为日志管理工具会非常方便。
  4. 简化的数据模型: Loki 使用一个简单的数据模型,它索引日志流的元数据而不是内容,这使得其性能更高,操作也更简单。

缺点:

  1. 搜索能力: 由于 Loki 不索引日志内容,其搜索能力不如 ELK 强大,对于复杂的文本搜索和分析可能不够用。
  2. 成熟度: Loki 相对于 ELK 来说是较新的项目,可能在特性支持和社区成熟度方面不如 ELK。
  3. 数据聚合: Loki 在数据转换和聚合方面的功能比 ELK 有限,可能需要额外的工具或服务来补充。

对比ELK,在个人环境中 Loki 的 轻量特性 优势巨大,而且研究 Loki 这种前沿竞品,对个人技能提升而言提升更明显。

前置准备

要分析 nginx 请求,首先需要按期望的格式保存 nginx 日志。除了常规字段外,我还期望对用户的地理位置进行分析。

Nginx GeoIP2

源码编译 Nginx

nginx 可以引用 GeoIP 或 GeoIP2 ,根据请求中 remote_addr​ 或 http_x_forwarded_for​ 等值为 IP 的变量查询数据库。

GeoIP 使用 .dat​ 格式的数据库,而 GeoIP2 使用 .mmdb​ 数据库。

两者我都实践了下,最终选择了 GeoIP2。

原因是 GeoIP2 方式提供的数据库更 新,IP 解析的位置更为准确。而 GeoIP 只能在互联网上获取一些老旧的数据库,位置解析不够准确。

操作系统:Ubuntu20.04

需要先源码编译,安装对应的模块。

对于 GeoIP ,需要额外添加编译参数 --with-http_geoip_module​。

# 下载源码包
mkdir /usr/local/nginx && cd /usr/local/nginx
wget https://nginx.org/download/nginx-1.24.0.tar.gz
tar xf nginx-1.24.0.tar.gz
cd nginx-1.24.0

# 查看 旧nginx编译参数,增加 --with-http_geoip_module
nginx -V

# 安装编译环境所需的依赖
apt install openssl libssl-dev libpcre3 libpcre3-dev zlib1g-dev make libgeoip-dev

# 编译安装
./configure --prefix=/usr/local/nginx --with-pcre-jit --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module --with-http_v2_module --without-mail_pop3_module --without-mail_imap_module --without-mail_smtp_module --with-http_stub_status_module --with-http_realip_module --with-http_addition_module --with-http_auth_request_module --with-http_secure_link_module --with-http_random_index_module --with-http_gzip_static_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-threads --with-stream --with-http_ssl_module --with-http_geoip_module
make -j 2
make install

# 确认重新编译后的参数
/usr/local/sbin/nginx -V

# 重载 systemd
systemctl daemon-reload
systemctl restart nginx

对于 GeoIP2,需要额外引入 --add-module=./ngx_http_geoip2_module-3.4​,这个模块并不在源码包中,需要单独下载。

参考链接:CSDN Nginx GeoIP2 配置

# 下载源码包
mkdir /usr/local/nginx && cd /usr/local/nginx
wget https://nginx.org/download/nginx-1.24.0.tar.gz
tar xf nginx-1.24.0.tar.gz
cd nginx-1.24.0

# 查看并记录旧nginx编译参数
nginx -V

# Ubuntu 20.04 安装编译环境所需的依赖
apt install openssl libssl-dev libpcre3 libpcre3-dev zlib1g-dev make libgeoip-dev

# CentOS 7 安装编译环境所需的依赖
# yum -y install zlib zlib-devel openssl openssl-devel pcre pcre-devel gcc gcc-c++ autoconf automake make psmisc net-tools lsof vim geoip geoip-devel libxml2-devel libxslt-devel gd-devel perl-ExtUtils-Embed gperftools-devel

# 编译安装 libmaxminddb 依赖
wget -c https://github.com/maxmind/libmaxminddb/releases/download/1.6.0/libmaxminddb-1.6.0.tar.gz
tar -zxvf libmaxminddb-1.6.0.tar.gz 
cd libmaxminddb-1.6.0
./configure
make && make install

# 下载 ngx_http_geoip2_module-3.4
wget http://nginx.org/download/nginx-1.22.0.tar.gz
tar xf nginx-1.22.0.tar.gz

# 编译安装nginx,在旧的基础上增加 --add-module=./ngx_http_geoip2_module-3.4
./configure --prefix=/usr/local/nginx --with-pcre-jit --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module --with-http_v2_module --without-mail_pop3_module --without-mail_imap_module --without-mail_smtp_module --with-http_stub_status_module --with-http_realip_module --with-http_addition_module --with-http_auth_request_module --with-http_secure_link_module --with-http_random_index_module --with-http_gzip_static_module --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-threads --with-stream --with-http_ssl_module --add-module=./ngx_http_geoip2_module-3.4
make -j 2
make install

# 确认重新编译后的参数
/usr/local/sbin/nginx -V

# 重载 systemd
systemctl daemon-reload
systemctl restart nginx

下载 GeoIP2 数据库

参考链接:CSDN GeoIP2 数据库选择

实践时我尝试了三个不同的数据库:

站点主要用户群在国内,我期望精度可以精确到城市,测试下来对于中国城市,dbip 的数据最精准。

lite版本的数据库是免费的:dbip Geoip2 mmdb 下载地址

Nginx 日志分析之 Loki

# 创建数据库存储目录
mkdir /usr/share/GeoIP/ && cd /usr/share/GeoIP/

# 下载并解压
wget https://download.db-ip.com/free/dbip-city-lite-2023-12.mmdb.gz
gzip -dc dbip-city-lite-2023-12.mmdb.gz > dbip-city-lite-2023-12.mmdb

# 测试 编译安装的 libmaxminddb 和数据库准确性
# mmdblookup  --file /usr/share/GeoIP/dbip-city-lite-2023-12.mmdb --ip 111.183.64.239

  {
    "city": 
      {
        "names": 
          {
            "en": 
              "Wulipu" <utf8_string>
          }
      }
    "continent": 
      {
        "code": 
          "AS" <utf8_string>
        "geoname_id": 
          6255147 <uint32>
        "names": 
          {
            "de": 
              "Asien" <utf8_string>
            "en": 
              "Asia" <utf8_string>
            "es": 
              "Asia" <utf8_string>
            "fa": 
              " آسیا" <utf8_string>
            "fr": 
              "Asie" <utf8_string>
            "ja": 
              "アジア大陸" <utf8_string>
            "ko": 
              "아시아" <utf8_string>
            "pt-BR": 
              "Ásia" <utf8_string>
            "ru": 
              "Азия" <utf8_string>
            "zh-CN": 
              "亚洲" <utf8_string>
          }
      }
    "country": 
      {
        "geoname_id": 
          1814991 <uint32>
        "is_in_european_union": 
          false <boolean>
        "iso_code": 
          "CN" <utf8_string>
        "names": 
          {
            "de": 
              "China, Volksrepublik" <utf8_string>
            "en": 
              "China" <utf8_string>
            "es": 
              "China" <utf8_string>
            "fa": 
              "چین" <utf8_string>
            "fr": 
              "Chine" <utf8_string>
            "ja": 
              "中国" <utf8_string>
            "ko": 
              "중국" <utf8_string>
            "pt-BR": 
              "China" <utf8_string>
            "ru": 
              "Китай" <utf8_string>
            "zh-CN": 
              "中国" <utf8_string>
          }
      }
    "location": 
      {
        "latitude": 
          30.737800 <double>
        "longitude": 
          112.238000 <double>
      }
    "subdivisions": 
      [
        {
          "names": 
            {
              "en": 
                "Hubei" <utf8_string>
            }
        }
      ]
  }

Nginx 保留客户端真实IP

# cat /etc/nginx.conf
...
    proxy_set_header    Host  $host;
    proxy_set_header    X-Real-IP  $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

...

# 重载nginx
nginx -t
nginx -s reload

Nginx 引用 mmdb

首先需要安装 geoip 相关的依赖:

# CentOS 7
yum install geoip-devel

# Ubuntu 20.04
apt install libgeoip-dev

这里遇到一个问题。

在站点未使用 cdn 时,客户端 IP 会存储在 nginx 请求的 $remote_addr​ 中。

在站点使用了 cdn 时,客户端 IP 会存储在 nginx 请求的 $http_x_forwarded_for​ 中。

绝大部分站点没有使用cdn,少量站点使用了cdn。对于这两种情况,需要分别进行处理。

将默认配置设置为从 $remote_addr​ 中分析 IP 位置,对于使用了 cdn 的域名,单独在子配置文件中的 server 块改写 geoip2 变量。

示例如下:

nginx 主配置文件:/etc/nginx/conf/nginx.conf

同时添加多个变量配置,分别从 $remote_addr​ 和 $http_x_forwarded_for​获取 IP :

# 以下配置在 http 块中
# cat /etc/nginx/conf/nginx.conf 
...
    # geoip2多个变量需要增加size
    variables_hash_max_size 2048;
    variables_hash_bucket_size 128;

    # 加载geoip2 mmdb数据库
    geoip2 /usr/share/GeoIP/dbip-city-lite-2023-12.mmdb {
        auto_reload 5m;
        # 默认配置
        $geoip2_data_country_code source=$remote_addr country iso_code;
        $geoip2_data_country_name source=$remote_addr country names en;

        $geoip2_data_city_name source=$remote_addr city names en;
        $geoip2_metadata_db_subdivisions_0_name  source=$remote_addr subdivisions 0 names en;

        # cdn 配置
        $geoip2_data_country_code_forwarded source=$http_x_forwarded_for country iso_code;
        $geoip2_data_country_name_forwarded source=$http_x_forwarded_for country names en;

        $geoip2_data_city_name_forwarded source=$http_x_forwarded_for city names en;
        $geoip2_metadata_db_subdivisions_0_name_forwarded source=$http_x_forwarded_for subdivisions 0 names en;
    }

...

nginx 子配置文件:/etc/nginx/conf/conf.d/cdn-opshub.cn.conf

在指定的 server 中,配置将 $geoip2_data<span style="font-weight: bold;" data-type="strong">*​ 改写为 $geoip2_data</span>*_forwarded​。

# 以下配置在 server 块中,将
# cat /etc/nginx/conf/conf.d/cdn-opshub.cn.conf
...
  set $geoip2_data_country_code $geoip2_data_country_code_forwarded;
  set $geoip2_data_country_name $geoip2_data_country_name_forwarded;
  set $geoip2_data_city_name $geoip2_data_city_name_forwarded;
  set $geoip2_metadata_db_subdivisions_0_name $geoip2_metadata_db_subdivisions_0_name_forwarded;
...

Nginx 日志格式

参考链接:grafana.com/grafana/dash…

按照 grafana dashborad loki 的推荐配置做了更改,精简了部分字段。

同时,因为上文所说的 cdn 站点,为了方便后续在 grafana 面板配置针对 loki 的查询语句,同时配置不同的日志记录格式。

将默认配置设置为从 $remote_addr​ 中保存用户 IP,对于使用了 cdn 的域名,单独在子配置文件中的 server 块改写 remote_addr​ 变量的值为 http_x_forwarded_for​。

# 以下配置在 http 块中
# cat /etc/nginx/nginx.conf
...
    # 默认日志格式配置
    log_format json_analytics_default escape=json '{'
                    '"remote_addr": "$remote_addr", ' # client IP
                    '"time_local": "$time_local", '
                    '"time_iso8601": "$time_iso8601", ' # local time in the ISO 8601 standard format
                    '"request": "$request", ' # full path no arguments if the request
                    '"request_uri": "$request_uri", ' # full path and arguments if the request
                    '"request_time": "$request_time", ' # request processing time in seconds with msec resolution
                    '"status": "$status", ' # response status code
                    '"http_referer": "$http_referer", ' # HTTP referer
                    '"http_user_agent": "$http_user_agent", ' # user agent
                    '"http_x_forwarded_for": "$http_x_forwarded_for", ' # http_x_forwarded_for
                    '"http_host": "$http_host", ' # the request Host: header
                    '"ssl_protocol": "$ssl_protocol", ' # TLS protocol
                    '"scheme": "$scheme", ' # http or https
                    '"request_method": "$request_method", ' # request method
                    '"server_protocol": "$server_protocol", ' # request protocol, like HTTP/1.1 or HTTP/2.0
                    '"geoip_country_code": "$geoip2_data_country_code",'
                    '"geoip_country_name": "$geoip2_data_country_name",'
                    '"geoip_city_name": "$geoip2_data_city_name",'
                    '"geoip_city_subdivisions_name": "$geoip2_metadata_db_subdivisions_0_name"'
                    '}';

    access_log /data/logs/nginx/json_access.log json_analytics_default;
...

# 以下配置在 http 块中
# cat /etc/nginx/conf/conf.d/cdn-opshub.cn.conf
...
log_format json_analytics_cdn escape=json '{'
                '"remote_addr": "$http_x_forwarded_for", ' # client IP
                '"time_local": "$time_local", '
                '"time_iso8601": "$time_iso8601", ' # local time in the ISO 8601 standard format
                '"request": "$request", ' # full path no arguments if the request
                '"request_uri": "$request_uri", ' # full path and arguments if the request
                '"request_time": "$request_time", ' # request processing time in seconds with msec resolution
                '"status": "$status", ' # response status code
                '"http_referer": "$http_referer", ' # HTTP referer
                '"http_user_agent": "$http_user_agent", ' # user agent
                '"http_x_forwarded_for": "$http_x_forwarded_for", ' # http_x_forwarded_for
                '"http_host": "$http_host", ' # the request Host: header
                '"ssl_protocol": "$ssl_protocol", ' # TLS protocol
                '"scheme": "$scheme", ' # http or https
                '"request_method": "$request_method", ' # request method
                '"server_protocol": "$server_protocol", ' # request protocol, like HTTP/1.1 or HTTP/2.0
                '"geoip_country_code": "$geoip2_data_country_code",'
                '"geoip_country_name": "$geoip2_data_country_name",'
                '"geoip_city_name": "$geoip2_data_city_name",'
                '"geoip_city_subdivisions_name": "$geoip2_metadata_db_subdivisions_0_name"'
                '}';
...

# 以下配置在 server 块中
# cat /etc/nginx/conf/conf.d/cdn-opshub.cn.conf
...
    access_log /data/logs/nginx/json_access.log json_analytics_cdn;
...


# 查看当前日志
# systemctl reload nginx
# cat /data/logs/nginx/json_access.log |grep "111.183.64.239" |tail -n 1
{"remote_addr": "111.183.64.239", "time_local": "14/Dec/2023:19:06:11 +0800", "time_iso8601": "2023-12-14T19:06:11+08:00", "request": "GET /video/:/transcode/universal/session/c6me3jdv2ise36v9q0ppfnvc/0/735.m4s HTTP/2.0", "request_uri": "/video/:/transcode/universal/session/c6me3jdv2ise36v9q0ppfnvc/0/735.m4s", "request_time": "6.794", "status": "200", "http_referer": "https://plex.opshub.cn/web/index.html", "http_user_agent": "Mozilla/5.0 (Linux; Android 11; SM-G9810 Build/RP1A.200720.012) AppleWebKit/537.36 (KHTML, like Gecko)  Chrome/99.0.4844.88 Mobile Safari/537.36", "http_x_forwarded_for": "", "http_host": "plex.opshub.cn", "ssl_protocol": "TLSv1.2", "scheme": "https", "request_method": "GET", "server_protocol": "HTTP/2.0", "geoip_country_code": "CN","geoip_country_name": "China","geoip_city_name": "Wulipu","geoip_city_subdivisions_name": "Hubei"}

日志为json格式,并且记录了配置好的字段。

Grafana Loki

参考链接:loki docker部署 官方文档

简单介绍下架构:

  • loki:主服务,负责存储日志和处理查询。

  • promtail:代理,负责收集日志并将其发送给 loki 。

  • Grafana:用于查询和绘制面板。

已经有现成的 Grafana,所以不再单独安装。

Loki

mkdir /usr/local/loki && cd /usr/local/loki

wget https://raw.githubusercontent.com/grafana/loki/v2.9.1/cmd/loki/loki-local-config.yaml -O loki-config.yaml

# 修改配置,用于解决 grafana 报错 `too many outstanding requests`
# vim loki-config.yaml
limits_config:
  split_queries_by_interval: 0

# 启动
docker run --name loki -d -v $(pwd):/mnt/config -p 3100:3100 grafana/loki:2.9.1 -config.file=/mnt/config/loki-config.yaml

# docker ps |grep loki
45fd786b258e   grafana/loki:2.9.1       "/usr/bin/loki -conf…"   About an hour ago   Up About an hour   0.0.0.0:3100->3100/tcp   loki

启动完毕后,访问端口为 3100 。

Promtail

此处按需修改配置文件和启动命令的日志路径,上文中配置的 nginx 日志路径是 /data/logs/nginx​。

wget https://raw.githubusercontent.com/grafana/loki/v2.9.1/clients/cmd/promtail/promtail-docker-config.yaml -O promtail-config.yaml

# 修改配置内容的job
# vim promtail-config.yaml
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
- job_name: nginx
  static_configs:
  - targets:
      - localhost
    labels:
      job: nginxlog
      host: centos-ops
      agent: promtail
      __path__: /data/logs/nginx/*log


# 修改启动命令日志路径
docker run --name promtail -d -v $(pwd):/mnt/config -v /data/logs/nginx:/data/logs/nginx --link loki grafana/promtail:2.9.1 -config.file=/mnt/config/promtail-config.yaml

Grafana 绘制 Nginx 请求面板

面板报错 too many outstanding requests​ 解决:community.grafana.com/t/…

添加 loki 数据源

Nginx 日志分析之 Loki

导入和绘制面板

参考链接:grafana.com/grafana/dash…

导入后,需要按进行如下修改:

  1. 修改 host 变量 的 query 日志路径

    Nginx 日志分析之 Loki

  2. 修改面板类型,由 Wordmap Panel​ 修改为 Geomap​,Wordmap Panel​已经停止维护并且后续版本不再支持。

    关于 Geomap​的具体配置,参考链接:grafana 世界地图 官方文档

    ​​Nginx 日志分析之 Loki

  3. 修改 promql 和排版,面板作者的 query 中添加了许多自定义的过滤项。按需修改。

最终效果如下:
​​Nginx 日志分析之 Loki​​​​​​

Nginx 日志分析之 Loki

Nginx 日志分析之 Loki

Grafana 查询 Loki 日志

参考链接:Grafana Loki 标签

上文中的 nginx 面板,仅用于作为 监控大盘​ 展示。事实上我们期望 loki 在 grafana 上能够像 kibana 一样,实现各种实时的自定义查询用于检索日志。

在此之前,需要先介绍 Loki 对于日志存储的一些底层逻辑,才会便于理解查询语句该怎么写。

关键字:流,块,标签,元数据,索引

如果对 prometheus 熟悉,应该很容易理解 metric(指标) 和 lable(标签)。

这是一行示例的 prometheus 监控指标:

# promql
node_cpu_seconds_total{host_ip="10.76.9.76"}

# 结果
{__name__="node_cpu_seconds_total", cpu="0", host_ip="10.76.9.76",  job="node_exporter", mode="system", public_cloud="aws"}

其中,metric​ 是 node_cpu_seconds_total,lable是 查询结果中的键值对。

对于 loki,grafana 复用了 prometheus 的数据结构,但又作出了一些小改动,取消了 metric,转而用流。

这是一行示例的 nginx 日志,日志路径位于 /data/logs/nginx/access.log:

11.11.11.11 - frank [25/Jan/2000:14:00:01 -0500] "GET /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
11.11.11.12 - frank [25/Jan/2000:14:00:02 -0500] "POST /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
11.11.11.13 - frank [25/Jan/2000:14:00:03 -0500] "GET /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
11.11.11.14 - frank [25/Jan/2000:14:00:04 -0500] "POST /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"

loki 可以收集日志并添加标签,然后将标签作为元数据,标签键值对相同的数据,会被视为同一条日志流。

即不会对日志内容进行索引,而只会对有关日志的元数据进行索引,作为每个日志流的一组标签。标签的键值对任意一个发生改变,都会创建新的日志流。

下面是 一个的 loki 配置文件示例,如何给日志增加静态标签:

scrape_configs:
 - job_name: system
   pipeline_stages:
   static_configs:
   - targets:
      - localhost
     labels:
      job: nginx-access-log
      __path__: /data/logs/nginx/access.log

添加标签后,loki 会将标签作为元数据进行索引,使用标签即可查询到符合条件的日志。

# 查询
{job="nginx-access-log"}

# 日志流
{job="nginx-access-log"} 11.11.11.11 - frank [25/Jan/2000:14:00:01 -0500] "GET /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
{job="nginx-access-log"} 11.11.11.12 - frank [25/Jan/2000:14:00:02 -0500] "POST /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
{job="nginx-access-log"} 11.11.11.13 - frank [25/Jan/2000:14:00:03 -0500] "GET /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
{job="nginx-access-log"} 11.11.11.14 - frank [25/Jan/2000:14:00:04 -0500] "POST /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"

事实上,也可以使用动态标签,这是一个配置文件示例:

- job_name: system
   pipeline_stages:
      - regex:
        expression: "^(?P<ip>\\S+) (?P<identd>\\S+) (?P<user>\\S+) \\[(?P<timestamp>[\\w:/]+\\s[+\\-]\\d{4})\\] \"(?P<action>\\S+)\\s?(?P<path>\\S+)?\\s?(?P<protocol>\\S+)?\" (?P<status_code>\\d{3}|-) (?P<size>\\d+|-)\\s?\"?(?P<referer>[^\"]*)\"?\\s?\"?(?P<useragent>[^\"]*)?\"?$"
    - labels:
        action:
        status_code:
   static_configs:
   - targets:
      - localhost
     labels:
      job: nginx-access-log
      env: dev
      __path__: /data/logs/nginx/access.log

在这个范例中,使用了正则表达式添加了 action​ 和 status_code​ 的动态标签,使用 job​,env​ 添加了静态标签。

现在可以这样查询:

# 查询
{action="GET" ,status_code="200" ,job="nginx-access-log"}

# 符合要求的日志流
{action="GET" ,status_code="200" ,job="nginx-access-log"} {job="nginx-access-log"} 11.11.11.11 - frank [25/Jan/2000:14:00:01 -0500] "GET /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"

# 所有日志流
{job="apache",env="dev",action="GET",status_code="200"} 11.11.11.11 - frank [25/Jan/2000:14:00:01 -0500] "GET /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
{job="apache",env="dev",action="POST",status_code="200"} 11.11.11.12 - frank [25/Jan/2000:14:00:02 -0500] "POST /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
{job="apache",env="dev",action="GET",status_code="400"} 11.11.11.13 - frank [25/Jan/2000:14:00:03 -0500] "GET /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
{job="apache",env="dev",action="POST",status_code="400"} 11.11.11.14 - frank [25/Jan/2000:14:00:04 -0500] "POST /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"

因为动态标签在这个例子中,生成了不同的键值对,所以这四个日志行将成为四个单独的流并开始填充四个单独的块。

快速计算一下,如果可能有四个常见操作(GET、PUT、POST、DELETE),并且可能有四个常见状态代码(可能超过四个!),则这将是 16 个流和 16 个流单独的块。现在,如果我们使用 ip​ 标签,则将其乘以每个用户。会快速创建数千或数万个流。

这是典型的高基数问题,会带来性能的快速下降。所以请避免滥用动态标签,需要谨慎考量动态标签可能存在的值的数量,不宜太多,否则会创建巨量的流。

标签的最佳实践

尽量使用静态标签

host​,application​,environment​是典型的良好标签,它们将针对给定的系统/应用程序进行修复并具有有限值。使用静态标签可以更轻松地从逻辑意义上查询日志(例如,显示给定应用程序和特定环境的所有日志,或显示特定主机上所有应用程序的所有日志)。

谨慎使用动态标签

太多的标签值组合会导致太多的流和块。Loki 对此的惩罚是存储中的大索引和小块,这实际上会显著降低性能。

从以往的日志系统的来看,大家已经习惯了对日志字段全部索引,但是在 Loki 的 实践中,并不需要这么做。只需要在必须时刻增加必要的动态标签。

例如以下两种查询方式,性能不会有显著差异:

# 使用索引
{app="loki",level="debug"}

# 使用过滤
{app="loki"} | level="debug"

过滤是对日志的全文模糊搜索,如果有需要频繁查询指定的字段,可以考虑在客户端产生日志时就进行结构化。例如上文中,我存储 nginx 日志时特意使用了 json 格式来进行存储。

但是这样又会给客户端本身带来额外的存储开销。当然,在使用了 Loki 或其他日志方案例如 elasticsearch 后,日志会被有序的存入到其他地方存储,客户端本身并不需要保留太长时间的日志。

在上文中,我们提到在非必要时不要添加动态标签,那么你什么时候需要动态标签?

稍后有一个关于 chunk_target_size​ 的部分。如果你将其设置为 1MB(这是合理的),系统会尝试在 1MB 的压缩大小处切割数据块,这大约相当于 5MB 左右的未压缩日志(根据压缩情况,可能高达 10MB)。如果日志有足够的增长速率,能在 max_chunk_age​ 时间内写入 5MB,或者在该时间范围内写入许多数据块,你可能需要考虑使用动态标签将其分割成单独的流。

标签值必须始终有界

如果动态设置标签,切勿使用可以具有无界或无限值的标签。这会导致创建巨量的流和块。

尝试将值限制在尽可能小的集合内。可以考虑动态标签的个位数或 10 个值。

这对于静态标签来说不太重要。例如,如果您的环境中有 1000 台主机,那么主机标签包含 1000 个值就可以了。

查询语法

参考链接:Grafana Loki 查询

上文介绍的都是基础知识,用于指导如何理解和配置标签。下面简单介绍 Loki 的查询语法 LogQL​。

LogQL 和 PromQL 很相似,查询有两种类型:

  • 日志查询:返回日志行的内容。
  • 指标查询:基于查询结果日志查询以计算值。

二元运算符和函数基本可以直接参考 PromQL,不过多赘述。

以现在的我收集的 nginx json 日志作为范例:

示例一:查询指定时间段内访问 www.opshub.cn​ 的日志

{filename="/data/logs/nginx/json_access.log", host="centos-ops"} |
  json |
  http_host="www.opshub.cn" 

Nginx 日志分析之 Loki

示例二:查询指定时间段内访问 www.opshub.cn​ 的日志,并进行格式化输出

{filename="/data/logs/nginx/json_access.log", host="centos-ops"} |
  json |
  http_host="www.opshub.cn" |
  line_format "located: {{.geoip_country_code}} code: {{.status}} IP: {{.remote_addr}} url: {{.scheme}}://{{.http_host}}{{.request_uri}}"

结果:

Nginx 日志分析之 Loki

上述两个示例中,如果点开具体的日志查看,可以发现每条日志都全字段索引了。这得益于在客户端(nginx) 存储日志时,已经使用 json 进行了结构化存储。

此时在 Loki 中并不会创建额外的流,因为索引仍是基于静态标签生成的。这种方式最利于以最低成本查询日志的指定字段。

Nginx 日志分析之 Loki

参考链接:Grafana Logcli

使用 logcli 对 job 进行 调试分析,看看当日志为 json 结构化存储时流和标签的情况:

# logcli series --analyze-labels '{job="nginxlog"}'
2023/12/18 10:04:53 http://localhost:3100/loki/api/v1/series?end=1702865093092568225&match=%7Bjob%3D%22nginxlog%22%7D&start=1702861493092568225
Total Streams:  1
Unique Labels:  4

Label Name  Unique Values  Found In Streams
job         1              1
host        1              1
filename    1              1
agent       1              1

事实上落地在 loki 存储内的索引,只有 4个 指定的静态标签,但是 grafana 执行查询时,可以全字段索引。

并且由于未使用动态标签,在 job:nginxlog 下 4 个静态标签都具有相同的键值对,只会创建一个流。

去掉 LogQL 的 json函数,再次查询,会发现索引只有配置文件中指定的标签:

# 查询语句
{filename="/data/logs/nginx/json_access.log", host="centos-ops"} |="www.opshub.cn"

Nginx 日志分析之 Loki

示例三:查询1小时内内访问 www.opshub.cn​ 的日志条数,这对应的是指标查询方式。上面两个示例是日志查询方式。

将面板 panel 由 Logs​ 改为其他类型。

# 查询语句
sum by(host) (count_over_time({filename="$filename", host="$host"}| json |http_host = "www.opshub.cn" |__error__="" [1h]))  

Nginx 日志分析之 Loki

需要说明,访问一个网站链接时,往往会产生不止一条日志,这是因为 Nginx 默认会记录每个请求的访问日志,包括请求的URL、客户端IP地址、响应状态码等信息。如果网页有大量的资源(例如图片、CSS、JavaScript等),每个资源请求都会生成一条访问日志,导致日志量增加。

示例四:查询1小时内内访问 www.opshub.cn​ 的日志占总日志的比例:

# 查询语句
sum by(host) (count_over_time({filename="$filename", host="$host"}| json |http_host = "www.opshub.cn" |__error__="" [1h]))  /
sum by(host) (count_over_time({filename="$filename", host="$host"}[1h]))

Nginx 日志分析之 Loki

总结

由于 Loki 不进行全文索引,在当前日志量只有几百MB的情况下,内存占用几乎可以忽略不计,Loki + Promtail 总共占用约100MB 内存。而 elk 单单部署了 elasticsearch,已经消耗了 8GB 内存。

对于目前分析个人站点日志数据,单点的Loki 已经足够使用。但是生产环境的最佳实践,例如 k8s 集群部署,多查询器并发查询,存储落地,标签优化,流和块的性能分析等,日后有机会再进一步探索。

引用链接

正文完
 
pengyinwei
版权声明:本站原创文章,由 pengyinwei 2023-12-15发表,共计19253字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处:https://www.opshub.cn
评论(没有评论)