devops| 日志服务实践

本文涉及的产品
对象存储 OSS,20GB 3个月
对象存储 OSS,恶意文件检测 1000次 1年
对象存储 OSS,内容安全 1000次 1年
简介: 阿里云日志服务实践: nginx access log; yii 框架接入阿里云日志服务
devops| 日志服务实践
技术分享 - devops| 日志服务实践

提纲:

  1. 日志服务功能点一览
  2. 阿里云日志服务实践
  3. 示例一: nginx access log
  4. 示例二: yii 框架接入阿里云日志服务
  5. 再探 protobuf

日志服务可以说的上是构建软件项目的基石之一, 系统持续稳定运行必不可少的一部分. 这里从阿里云日志服务入手, 借助云平台带来的技术更新迭代, 聊一聊日志服务实践.

以前日志服务的打开方式

日志最常见的方式是写入到文件中. 「小作坊」的情况下, 把服务器的权限给开发, 开发自己 ssh 到服务器上面用 grep 查日志. 是的, 我就是这样过来的, 所以常用的几个 grep 命令, 甚至一些稍微高级的命令, 还能默写出来:

grep xxx xxFile # 正则匹配查询字符串
grep 'xxx xxx' # 查询包含特殊字符, 比如空格的字符串
grep -i xxx # 忽略大小写
grep -n xx # 显示行号
grep -v xxx # 查询不包含字符串的行
grep -r xxx xxDir # 在文件夹中递归查询
ps aux | grep xxx | grep -v 'grep' # -v 常用的一种方式

# 2个复杂些的例子
# 获取访问 ip 统计
cat /var/log/nginx/access.log|awk '{print $1}'|sort|uniq -c|sort -nr|more
# 获取 http 状态码
cat /var/log/nginx/access.log|grep -ioE 'HTTP/1.[0|1]" [0-9]{3}'|awk '{print $2}'

grep 查询可以使用多种 正则 方式: 基础, 扩展, perl. 支持的正则功能一次增多, 部分细节有些许差异.

-E, --extended-regexp     PATTERN is an extended regular expression (ERE)
-G, --basic-regexp        PATTERN is a basic regular expression (BRE)
-P, --perl-regexp         PATTERN is a Perl regular expression

一句话概括这种方式: 简单直接. 当然有时直接 vim 打开, 然后再查看的. 不过数据量一大, vim 的速度就不乐观了. 所以通常会对日志文件进行 切分, 这样也便于以后 归档:

  • 按照业务切分: 服务器各项日志, 不同业务模块的日志, 第三方接口的日志
  • 按照时间维度: 日切 xxx_20180421.log; 月切 xxx_201804.log
  • 按照文件大小: 比如到 1G 了, 从 xxx.log.1xxx.log.2, 一次递增
  • 多种方式组合使用

文件一多, 查询就变得困难起来了.

数据量大, 还要考虑日志的 写入性能, 通常的做法是 加缓存: 这里称之为 刷新(flash):

  • 一定时间间隔写入一次
  • 日志达到多少条写入一次
  • 日志超过多大写入一次

开放服务器 ssh 权限出来, 会带来 安全隐患, 有开发上去误操作就不好了. 所以有了新的替代方案:

  • 运维开日志的 ftp, 需要看日志自己去下载
  • 存储到数据库中, 比如 MongoDB, 走数据库查询
  • 自建日志中心

当然 自建日志中心 是最高级的玩法, 之前鹅厂的分享提到过, 会走 UDP 进行日志的上传与统一分析.

关于日志的其他细节:

  • 全链路跟踪: 去年很火的方案, 请求进来时生成一个 trace_id, 之后的所有调用都会带上这个 trace_id, 这样就可以在日志中通过 trace_id 查询到整个调用链路
  • 安全问题: 日志中可能有用户未经处理的敏感信息, 比如手机号, 甚至没有经过处理的密码
  • 日志归档的问题: 打包归档历史数据来降低日志存储的成本

最后, 对大部分使用日志的人(通常是开发, 定位 犯罪现场)而言, 好查 好用 才是重中之重, 日志的存储/归档都不用自己操心, 由日志系统来解决.

上手阿里云日志服务

阿里云的日志服务上手比较容易, 在控制台点点点即可, 大致的分层设计如下:

  • 开通日志服务: 总的入口
  • project: 项目, 第一级分层, project + region 构成 api 的访问地址
  • logStore: 日志存储, 每个 project 下可以建立多个 logstore, logStore 可以配置多个 shared过期时间
  • log data source: 需要为每个 logStore 配置数据源
  • 日志投递: 日志数据除了供日志服务消费外, 还可以投递给其他云产品, 比如 OSS 进行归档处理
  • 日志查询: 重点功能, 包含 search analysis chart 3个主要部分

配置数据源常用的方式:

  • nginx access log: 下面还会详细提到
  • 文本 + logtail 工具收集 + 自定义日志分割
  • sdk 接入

关于 logtail: 阿里云提供的日志收集工具, 安装到 ecs 上就可以按照 logStore 配置的日志路径进行搜集

PS: 如果 ecs 和 日志服务是不同的账号下的, 需要配置授权

日志查询快捷指南:

  • search: 支持部分正则的查询语法; 直接点击日志即可查询
  • analysis: 使用 | 管道对查询结果进行分析; 类 sql 的语法
  • chart: 将 analysis 得到的结果转化为图表, 更直观
  • 其他小技巧: 保存常用查询

日志数据源配置

实践一: nginx access log

nginx access log 的接入提供了很好的支持:

  • 配置好 logtail 收集 access log
  • logStore 中配置 log_format 就可以自动分割日志建立索引
  • 配置常用 查询/分析/图表

推荐下面的 log_format:

log_format main '$remote_addr||$remote_user||$time_local||$request||$http_host||$status||$request_length||$body_bytes_sent||$http_referer||$http_user_agent||$request_time||$upstream_response_time||$request_body';

PS: 细节出魔鬼, 之前没有采用 || 的方式, 导致部分日志解析出现问题, 字段没有对上

日志记录:

nginx-access-log 示例

配置好图表:

image

关于 request_body:

  • 推荐加到 log_format
  • 当前 nginx 版本(我的是 1.13) 直接配置上就可以收集 form-data x-www-form-urlencoded application/json 等格式的 post 数据

如何解析 form-data 格式的数据:

function hextostr($hex) {
    return preg_replace_callback('/\\\x([0-9a-fA-F]{2})/', function($matches) {
        return chr(hexdec($matches[1]));
    }, $hex);
}
echo hextostr('----------------------------400719531552868304622917\x0D\x0AContent-Disposition:');

如果 request_body 无法记录, 网上提供了 2 种方案(当前版本并不需要):

  • access log 记录到 fastcgi_pass 配置处

    location ~ .php$ {

    fastcgi_pass fpm:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    access_log /var/log/nginx/yii_access.log main;
    include fastcgi_params;

    }

  • 使用 nginx_lua 模块
lua_need_request_body on;
content_by_lua 'local s = ngx.var.request_body';

实践二: 接入日志服务sdk(以 yii 框架为例)

使用 logtail 来作为数据源实在是 简单, 搜集的数据通过 分隔符 或者 正则 进行分割, 有时候会很麻烦. 写过 正则 的人都知道, 正则这东西并不难, 它就是 , 稍微有一点点变动, 正则可能就需要调整了. 而且用 logtail 来收集日志, 看似和业务 比较隔离, 实际感觉确实 偏离业务 更多一点. 接入 日志服务sdk 会是一个不错的选择.

在之前 yii| 最佳实践之黑箱思维 提到过 yii 的日志服务, 这里再简要复述一下:

  • 分层设计: logger - dispatch - target 3层, logger 专注于日志功能, dispatch 来调度, target 来适配不同日志存储
  • 日志标记: 包括 level / category / tag / perfix 等多种日志标记方式, 方便对日志更细粒度的控制
  • flush, 刷新, 比如 1000 条后再输出(落地). 缓冲(buffer) 的思想可以说在系统设计中比比皆是.
  • 切片, 比如说日志按照时间日切, 或者按照大小 10m 一切
PS: 这就是成熟框架的威力, 常用功能近乎全面无死角的解决掉

具体 yii 中接入, 其实就是新增一个 target, 通过阿里云日志服务SDK写入日志:

<?php
Yii::setAlias('@common', dirname(__DIR__));
Yii::setAlias('@frontend', dirname(dirname(__DIR__)) . '/frontend');
Yii::setAlias('@backend', dirname(dirname(__DIR__)) . '/backend');
Yii::setAlias('@console', dirname(dirname(__DIR__)) . '/console');

require __DIR__ . '/../sdk/aliyun-log-php-sdk/Log_Autoload.php';
?php
namespace common\components;

use yii\base\Component;

/**
 *  https://github.com/aliyun/aliyun-log-php-sdk
 */
class AliyunLog extends Component
{
    /**
     * 服务入口: https://help.aliyun.com/document_detail/29008.html
     * @var string
     */
    public $endPoint = 'cn-shanghai-intranet.log.aliyuncs.com';
    public $ak;
    public $sk;
    public $token = '';
    public $project;
    public $logStore;
    public $topic = 'TestTopic';
    /** @var \Aliyun_Log_Client $client */
    public $client;

    public function init()
    {
        $this->client = new \Aliyun_Log_Client(
            $this->endPoint,
            $this->ak,
            $this->sk,
            $this->token
        );
    }

    public function putLogs(array $logs)
    {
        $logitems = [];
        foreach ($logs as $log) {
            $logItem = new \Aliyun_Log_Models_LogItem();
            $logItem->setTime(time());
            $logItem->setContents($log);
            $logitems[] = $logItem;
        }

        $request = new \Aliyun_Log_Models_PutLogsRequest(
            $this->project,
            $this->logStore,
            $this->topic,
            null,
            $logitems
        );

        $this->client->putLogs($request);
    }
}
<?php
namespace common\components;

use yii\di\Instance;
use yii\helpers\VarDumper;
use yii\log\Logger;
use yii\log\Target;

class AliyunLogTarget extends Target
{
    /** @var AliyunLog $log */
    public $log = 'aliyunLog';
    public $project;
    public $logStore;
    public $topic;

    public function init()
    {
        $this->log = Instance::ensure($this->log);
    }

    public function export()
    {
        $rows = [];
        foreach ($this->messages as $message) {
            list($text, $level, $category, $timestamp) = $message;
            $level = Logger::getLevelName($level);
            if (!is_string($text)) {
                // exceptions may not be serializable if in the call stack somewhere is a Closure
                if ($text instanceof \Throwable || $text instanceof \Exception) {
                    $text = (string) $text;
                } else {
                    $text = VarDumper::export($text);
                }
            }
            $rows[] = [
                'level' => $level,
                'category' => $category,
                'prefix' => $this->getMessagePrefix($message),
                'message' => $text,
            ];
        }

        if ($this->project) {
            $this->log->project = $this->project;
        }
        if ($this->logStore) {
            $this->log->logStore = $this->logStore;
        }
        if ($this->topic) {
            $this->log->topic = $this->topic;
        }
        $this->log->putLogs($rows);
    }
}
...
    'components' => [
        'log' => [
            'targets' => [
                [
                    'class' => 'yii\log\FileTarget',
                    'levels' => ['error', 'warning'],
                ],
                [
                    'class' => \common\components\AliyunLogTarget::class,
                    'levels' => ['info', 'warning', 'error'],
                    'except' => $_info_except,
                    'logVars' => [],
                    'exportInterval' => YII_ENV_PROD ? 1000 : 1,
                    'topic' => 'console',
                ],
            ],
        ],
    ],
...

来张效果图:

aliyun-log-yii

题外: 什么是好的SDK

yii| 最佳实践之黑箱思维 里我还提到如何判断 好的sdk:

好用的 SDK, 只用看一下 sample 或者 quick start 就能分辨出来.

不过这次实践下来, 我要收回这句话, 从 「进化论」 的角度来看才更趋于真理:

好用的 sdk, 应该是能跟上社区最佳标准与实践, 不断进化的.

说一下项目实践中遇到的问题: 同时使用 阿里云日志服务和OSS服务的sdk, 而 2 这的 sdk 中都定义了 RequestCore 来作为 http 请求基类, 导致类冲突

<?php
/**
 * Copyright (C) Alibaba Cloud Computing
 * All rights reserved
 */
$version = '0.6.0';
function Aliyun_Log_PHP_Client_Autoload($className) {
    $classPath = explode('_', $className);
    if ($classPath[0] == 'Aliyun') {
        if(count($classPath)>5)
            $classPath = array_slice($classPath, 0, 5);
        if(strpos($className, 'Request') !== false){
            $lastPath = end($classPath);
            array_pop($classPath);
            array_push($classPath,'Request');
            array_push($classPath, $lastPath);
        }
        if(strpos($className, 'Response') !== false){
            $lastPath = end($classPath);
            array_pop($classPath);
            array_push($classPath,'Response');
            array_push($classPath, $lastPath);
        }
        $filePath = dirname(__FILE__) . '/' . implode('/', $classPath) . '.php';
        if (file_exists($filePath))
            require_once($filePath);
    }
}
spl_autoload_register('Aliyun_Log_PHP_Client_Autoload');
require_once realpath(dirname(__FILE__) . '/../Log_Autoload.php');

本来只是想对现有日志功能进行改造, 要是导致原有的 OSS 功能不能用了, 那就不好了. 基于此, 就动了直接接入日志服务 api 的念头:

public function actionAliyunlog2()
{
    $ak = 'bq2sjzesjmo86kq35behupbq';
    $sk = '4fdO2fTDDnZPU/L7CHNdemB2Nsk=';

    // 服务入口: https://help.aliyun.com/document_detail/29008.html
    $project = 'test-project';
    $endpoint = 'cn-hangzhou-devcommon-intranet.sls.aliyuncs.com';

    // 请求签名: https://help.aliyun.com/document_detail/29012.html
    // get
    $httpMethod = 'GET';
    $contentMd5 = '';
    $contentType = '';
    $gmDate = 'Mon, 09 Nov 2015 06:11:16 GMT';
    $logHeaders = [
        'x-log-apiversion:0.6.0',
        'x-log-signaturemethod:hmac-sha1',
    ];
    $logHeadersStr = join("\n", $logHeaders);
    $logResource = '/logstores?' . http_build_query(['logstoreName' => '', 'offset' => 0, 'size' => 1000]);
    $signStr = $httpMethod . "\n" . $contentMd5 . "\n" . $contentType . "\n" . $gmDate . "\n" .
        $logHeadersStr . "\n" . $logResource;
    $sign = base64_encode(hash_hmac('sha1', $signStr, $sk, true));

    // 公共请求头: https://help.aliyun.com/document_detail/29010.html
    $headers = [
        "Date: $gmDate",
        "Host: {$project}.{$endpoint}",
        "Authorization:LOG {$ak}:{$sign}",
    ];
    $headers = array_merge($headers, $logHeaders);

    // post
    // 数据编码方式 - protobuf: https://help.aliyun.com/document_detail/29055.html
    $body = [
        'TestKey' => 'TestContent',
    ];
    $contents = [];
    foreach ($body as $k => $v) {
        $content = new \Protobuf\Aliyunlog\Log_Content();
        $content->setKey($k);
        $content->setValue($v);
        $contents[] = $content;
    }
    $log = new \Protobuf\Aliyunlog\Log();
    $log->setTime(1447048976);
    $log->setContents($contents);
    $logGroup = new \Protobuf\Aliyunlog\LogGroup();
    $logGroup->setLogs([$log]);
    $logGroup->setTopic('');
    $logGroup->setSource('10.230.201.117');
    $bodyProto = $logGroup->serializeToString();

    $httpMethod = 'POST';
    $contentMd5 = strtoupper(md5($bodyProto));
    $contentType = 'application/x-protobuf';
    $contentLen = strlen($bodyProto);
    $gmDate = 'Mon, 09 Nov 2015 06:11:16 GMT';
    $logHeaders = [
        'x-log-apiversion:0.6.0',
        'x-log-signaturemethod:hmac-sha1',
    ];
    $logHeadersStr = join("\n", $logHeaders);
    $logResource = '/logstores?' . http_build_query(['logstoreName' => '', 'offset' => 0, 'size' => 1000]);
    $signStr = $httpMethod . "\n" . $contentMd5 . "\n" . $contentType . "\n" . $gmDate . "\n" .
        $logHeadersStr . "\n" . $logResource;
    $sign = base64_encode(hash_hmac('sha1', $signStr, $sk, true));

    $headers = [
        "Date: $gmDate",
        "Host: {$project}.{$endpoint}",
        "Authorization:LOG {$ak}:{$sign}",
        "Content-MD5: $contentMd5",
        "Content-Length: $contentLen",
    ];
    $headers = array_merge($headers, $logHeaders);
}

事实证明 我还是太年轻了, 日志传输用的 protobuf. 这东西说实话并不难, 之前的服务器系列有protobuf 的入门使用(blog - 服务器开发系列 1), 无非是安装一个 protobuf 的编译器(protoc), 然后安装一个protobuf的解析器(对应 php 中的 ext-protobuf 扩展)

message Log
{
    required uint32 time = 1; // UNIX Time Format
    message Content
    {
        required string key = 1;
        required string value = 2;
    }
    repeated Content contents= 2;
}
message LogGroup
{
    repeated Log logs= 1;
    optional string reserved =2; // 内部字段,不需要填写
    optional string topic = 3;
    optional string source = 4;
}
message LogGroupList
{
    repeated LogGroup logGroupList = 1;
}
syntax="proto3";
package Protobuf.Aliyunlog;

message Log
{
    uint32 time = 1; // UNIX Time Format
    message Content
    {
        string key = 1;
        string value = 2;
    }
    repeated Content contents= 2;
}
message LogGroup
{
    repeated Log logs= 1;
    string reserved =2; // 内部字段,不需要填写
    string topic = 3;
    string source = 4;
}
message LogGroupList
{
    repeated LogGroup logGroupList = 1;
}

导致的结果就是, protobuf序列化的数据大小, 和 demo 对上, api 自然就不通了. 而官方 SDK 中, 是用 pack() 自己一点点实现的. 这事我在刚接触服务器开发的时候也干过...

不过好在, OSS的SDK按照 psr-4 标准进行组织了, 引入命名空间后就不会有现在类冲突的尴尬了.

写在最后

日志服务是一个深究起来还颇为复杂的话题, 重要实践, 让日志真正起到 系统保驾护航异常时还原犯罪现场的作用

推荐资源:

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
26天前
|
运维 安全 Devops
构建高效稳定的云基础设施:DevOps与容器化技术融合实践
在数字化转型的浪潮中,企业对于IT基础设施的要求越来越高,不仅需要快速响应市场变化,还要确保系统的稳定与安全。本文深入探讨了如何通过融合DevOps文化和容器化技术来构建一个高效、稳定且易于管理的云基础设施。通过实际案例分析,阐述了持续集成/持续部署(CI/CD)流程的优化、自动化测试、监控以及日志管理等关键环节的实施策略,旨在为运维专业人员提供一套切实可行的解决方案。
23 3
|
6月前
|
存储 弹性计算 监控
日志服务SLS实现云产品可观测实践
认证考试:日志服务SLS实现云产品可观测实践
|
29天前
|
运维 Kubernetes Devops
构建高效可靠的云基础设施:DevOps与容器化技术融合实践
【2月更文挑战第30天】 在当今快速迭代和竞争激烈的软件开发领域,传统的IT运维模式已难以满足业务发展的需要。本文将探讨如何通过整合DevOps文化和容器化技术,构建一个既高效又可靠的云基础设施。文章首先回顾了DevOps的核心理念及其对运维工作流的影响,接着深入讨论了容器化技术的优势和挑战,并提出了一套结合两者的实施方案。最后,通过案例分析展示了该方案在实际环境中的应用效果和潜在益处。
|
30天前
|
Prometheus 监控 Kubernetes
Kubernetes 集群监控与日志管理实践
【2月更文挑战第29天】 在微服务架构日益普及的当下,Kubernetes 已成为容器编排的事实标准。然而,随着集群规模的扩大和业务复杂度的提升,有效的监控和日志管理变得至关重要。本文将探讨构建高效 Kubernetes 集群监控系统的策略,以及实施日志聚合和分析的最佳实践。通过引入如 Prometheus 和 Fluentd 等开源工具,我们旨在为运维专家提供一套完整的解决方案,以保障系统的稳定性和可靠性。
|
9月前
|
消息中间件 弹性计算 数据可视化
SpringBoot 整合 Elastic Stack 最新版本(7.14.1)分布式日志解决方案,开源微服务全栈项目【有来商城】的日志落地实践
SpringBoot 整合 Elastic Stack 最新版本(7.14.1)分布式日志解决方案,开源微服务全栈项目【有来商城】的日志落地实践
|
4月前
|
存储 缓存 Java
浅析JAVA日志中的几则性能实践与原理解释
本篇文章通过几个技术点说明日志记录过程中的性能实践,计算机领域的性能往往都遵循着冰山法则,即你能看得见的、程序员能感知的只是其中的一小部分,还有大量的细节隐藏在冰山之下。
522 1
|
4天前
|
运维 Kubernetes Devops
构建高效自动化运维体系:DevOps与容器技术融合实践
【4月更文挑战第15天】 在当今快速发展的信息技术时代,传统的IT运维模式已难以满足业务敏捷性的需求。本文旨在探讨如何通过整合DevOps理念和容器技术来构建一个高效的自动化运维体系。文章将详细阐述DevOps的核心原则、容器技术的基础知识,以及两者结合的优势。此外,文中还将分享一系列实践经验,包括持续集成/持续部署(CI/CD)流程的搭建、微服务架构的应用,以及监控和日志管理策略的优化,以期帮助企业实现快速、可靠且安全的软件交付过程。
|
6天前
|
运维 Devops 持续交付
构建高效稳定的云基础设施:DevOps与容器化技术融合实践
【4月更文挑战第13天】 在当今快速迭代和持续部署的软件开发环境中,传统的IT运维模式已难以满足业务发展的需求。本文聚焦于如何通过融合DevOps理念与容器化技术,构建一个高效、稳定且易于管理的云基础设施。文章将探讨持续集成/持续交付(CI/CD)流程的优化、容器化技术的最佳实践、以及微服务架构下的应用管理,以期为企业提供一种改进运维效率、加速产品上市时间,同时保障系统稳定性的解决方案。
|
22天前
|
运维 Kubernetes Devops
构建高效稳定的云基础设施:DevOps与容器化技术融合实践
随着企业数字化转型的不断深入,传统的IT运维模式已经难以满足快速迭代和持续交付的需求。本文将探讨如何通过结合DevOps文化与容器化技术,构建一个既高效又稳定的云基础设施。文章首先概述了DevOps的核心理念及其在现代运维中的重要性,然后详细介绍了容器化技术,特别是Docker和Kubernetes在实现微服务架构中的应用。最后,文中通过案例分析展示了这一融合实践如何在真实环境中提升运维效率和系统稳定性。
20 7
|
24天前
|
运维 Kubernetes 监控
构建高效稳定的云基础设施:DevOps与容器化技术融合实践
在当今云计算时代,企业追求敏捷性、可扩展性以及成本效益的云基础设施。本文将探讨如何通过DevOps文化与容器化技术的融合,打造一个既高效又稳定的运维环境。文章不仅阐述了DevOps和容器化技术各自的优势,还提供了一个具体的实施案例,展示了这种结合如何优化资源利用、提高部署速度并降低运维复杂性。

热门文章

最新文章