devops| 日志服務(wù)實(shí)踐

date: 2018-4-4 18:28:08
title: devops| 日志服務(wù)實(shí)踐
description: 阿里云日志服務(wù)實(shí)踐: nginx access log; yii 框架接入阿里云日志服務(wù)

devops| 日志服務(wù)實(shí)踐
技術(shù)分享 - devops| 日志服務(wù)實(shí)踐

提綱:

  1. 日志服務(wù)功能點(diǎn)一覽
  2. 阿里云日志服務(wù)實(shí)踐
  3. 示例一: nginx access log
  4. 示例二: yii 框架接入阿里云日志服務(wù)
  5. 再探 protobuf

日志服務(wù)可以說的上是構(gòu)建軟件項(xiàng)目的基石之一, 系統(tǒng)持續(xù)穩(wěn)定運(yùn)行必不可少的一部分. 這里從阿里云日志服務(wù)入手, 借助云平臺(tái)帶來的技術(shù)更新迭代, 聊一聊日志服務(wù)實(shí)踐.

以前日志服務(wù)的打開方式

日志最常見的方式是寫入到文件中. 「小作坊」的情況下, 把服務(wù)器的權(quán)限給開發(fā), 開發(fā)自己 ssh 到服務(wù)器上面用 grep 查日志. 是的, 我就是這樣過來的, 所以常用的幾個(gè) grep 命令, 甚至一些稍微高級(jí)的命令, 還能默寫出來:

grep xxx xxFile # 正則匹配查詢字符串
grep 'xxx xxx' # 查詢包含特殊字符, 比如空格的字符串
grep -i xxx # 忽略大小寫
grep -n xx # 顯示行號(hào)
grep -v xxx # 查詢不包含字符串的行
grep -r xxx xxDir # 在文件夾中遞歸查詢
ps aux | grep xxx | grep -v 'grep' # -v 常用的一種方式

# 2個(gè)復(fù)雜些的例子
# 獲取訪問 ip 統(tǒng)計(jì)
cat /var/log/nginx/access.log|awk '{print $1}'|sort|uniq -c|sort -nr|more
# 獲取 http 狀態(tài)碼
cat /var/log/nginx/access.log|grep -ioE 'HTTP/1.[0|1]" [0-9]{3}'|awk '{print $2}'

grep 查詢可以使用多種 正則 方式: 基礎(chǔ), 擴(kuò)展, perl. 支持的正則功能一次增多, 部分細(xì)節(jié)有些許差異.

-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

一句話概括這種方式: 簡單直接. 當(dāng)然有時(shí)直接 vim 打開, 然后再查看的. 不過數(shù)據(jù)量一大, vim 的速度就不樂觀了. 所以通常會(huì)對(duì)日志文件進(jìn)行 切分, 這樣也便于以后 歸檔:

  • 按照業(yè)務(wù)切分: 服務(wù)器各項(xiàng)日志, 不同業(yè)務(wù)模塊的日志, 第三方接口的日志
  • 按照時(shí)間維度: 日切 xxx_20180421.log; 月切 xxx_201804.log
  • 按照文件大小: 比如到 1G 了, 從 xxx.log.1xxx.log.2, 一次遞增
  • 多種方式組合使用

文件一多, 查詢就變得困難起來了.

數(shù)據(jù)量大, 還要考慮日志的 寫入性能, 通常的做法是 加緩存: 這里稱之為 刷新(flash):

  • 一定時(shí)間間隔寫入一次
  • 日志達(dá)到多少條寫入一次
  • 日志超過多大寫入一次

開放服務(wù)器 ssh 權(quán)限出來, 會(huì)帶來 安全隱患, 有開發(fā)上去誤操作就不好了. 所以有了新的替代方案:

  • 運(yùn)維開日志的 ftp, 需要看日志自己去下載
  • 存儲(chǔ)到數(shù)據(jù)庫中, 比如 MongoDB, 走數(shù)據(jù)庫查詢
  • 自建日志中心

當(dāng)然 自建日志中心 是最高級(jí)的玩法, 之前鵝廠的分享提到過, 會(huì)走 UDP 進(jìn)行日志的上傳與統(tǒng)一分析.

關(guān)于日志的其他細(xì)節(jié):

  • 全鏈路跟蹤: 去年很火的方案, 請(qǐng)求進(jìn)來時(shí)生成一個(gè) trace_id, 之后的所有調(diào)用都會(huì)帶上這個(gè) trace_id, 這樣就可以在日志中通過 trace_id 查詢到整個(gè)調(diào)用鏈路
  • 安全問題: 日志中可能有用戶未經(jīng)處理的敏感信息, 比如手機(jī)號(hào), 甚至沒有經(jīng)過處理的密碼
  • 日志歸檔的問題: 打包歸檔歷史數(shù)據(jù)來降低日志存儲(chǔ)的成本

最后, 對(duì)大部分使用日志的人(通常是開發(fā), 定位 犯罪現(xiàn)場)而言, 好查 好用 才是重中之重, 日志的存儲(chǔ)/歸檔都不用自己操心, 由日志系統(tǒng)來解決.

上手阿里云日志服務(wù)

阿里云的日志服務(wù)上手比較容易, 在控制臺(tái)點(diǎn)點(diǎn)點(diǎn)即可, 大致的分層設(shè)計(jì)如下:

  • 開通日志服務(wù): 總的入口
  • project: 項(xiàng)目, 第一級(jí)分層, project + region 構(gòu)成 api 的訪問地址
  • logStore: 日志存儲(chǔ), 每個(gè) project 下可以建立多個(gè) logstore, logStore 可以配置多個(gè) shared過期時(shí)間
  • log data source: 需要為每個(gè) logStore 配置數(shù)據(jù)源
  • 日志投遞: 日志數(shù)據(jù)除了供日志服務(wù)消費(fèi)外, 還可以投遞給其他云產(chǎn)品, 比如 OSS 進(jìn)行歸檔處理
  • 日志查詢: 重點(diǎn)功能, 包含 search analysis chart 3個(gè)主要部分

配置數(shù)據(jù)源常用的方式:

  • nginx access log: 下面還會(huì)詳細(xì)提到
  • 文本 + logtail 工具收集 + 自定義日志分割
  • sdk 接入

關(guān)于 logtail: 阿里云提供的日志收集工具, 安裝到 ecs 上就可以按照 logStore 配置的日志路徑進(jìn)行搜集

PS: 如果 ecs 和 日志服務(wù)是不同的賬號(hào)下的, 需要配置授權(quán)

日志查詢快捷指南:

  • search: 支持部分正則的查詢語法; 直接點(diǎn)擊日志即可查詢
  • analysis: 使用 | 管道對(duì)查詢結(jié)果進(jìn)行分析; 類 sql 的語法
  • chart: 將 analysis 得到的結(jié)果轉(zhuǎn)化為圖表, 更直觀
  • 其他小技巧: 保存常用查詢
日志數(shù)據(jù)源配置

實(shí)踐一: nginx access log

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

  • 配置好 logtail 收集 access log
  • logStore 中配置 log_format 就可以自動(dòng)分割日志建立索引
  • 配置常用 查詢/分析/圖表

推薦下面的 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: 細(xì)節(jié)出魔鬼, 之前沒有采用 || 的方式, 導(dǎo)致部分日志解析出現(xiàn)問題, 字段沒有對(duì)上

日志記錄:

nginx-access-log 示例

配置好圖表:

image

關(guān)于 request_body:

  • 推薦加到 log_format
  • 當(dāng)前 nginx 版本(我的是 1.13) 直接配置上就可以收集 form-data x-www-form-urlencoded application/json 等格式的 post 數(shù)據(jù)

如何解析 form-data 格式的數(shù)據(jù):

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 無法記錄, 網(wǎng)上提供了 2 種方案(當(dāng)前版本并不需要):

  • 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';

實(shí)踐二: 接入日志服務(wù)sdk(以 yii 框架為例)

使用 logtail 來作為數(shù)據(jù)源實(shí)在是 簡單, 搜集的數(shù)據(jù)通過 分隔符 或者 正則 進(jìn)行分割, 有時(shí)候會(huì)很麻煩. 寫過 正則 的人都知道, 正則這東西并不難, 它就是 , 稍微有一點(diǎn)點(diǎn)變動(dòng), 正則可能就需要調(diào)整了. 而且用 logtail 來收集日志, 看似和業(yè)務(wù) 比較隔離, 實(shí)際感覺確實(shí) 偏離業(yè)務(wù) 更多一點(diǎn). 接入 日志服務(wù)sdk 會(huì)是一個(gè)不錯(cuò)的選擇.

在之前 yii| 最佳實(shí)踐之黑箱思維 提到過 yii 的日志服務(wù), 這里再簡要復(fù)述一下:

  • 分層設(shè)計(jì): logger - dispatch - target 3層, logger 專注于日志功能, dispatch 來調(diào)度, target 來適配不同日志存儲(chǔ)
  • 日志標(biāo)記: 包括 level / category / tag / perfix 等多種日志標(biāo)記方式, 方便對(duì)日志更細(xì)粒度的控制
  • flush, 刷新, 比如 1000 條后再輸出(落地). 緩沖(buffer) 的思想可以說在系統(tǒng)設(shè)計(jì)中比比皆是.
  • 切片, 比如說日志按照時(shí)間日切, 或者按照大小 10m 一切

PS: 這就是成熟框架的威力, 常用功能近乎全面無死角的解決掉

具體 yii 中接入, 其實(shí)就是新增一個(gè) target, 通過阿里云日志服務(wù)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
{
    /**
     * 服務(wù)入口: 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| 最佳實(shí)踐之黑箱思維 里我還提到如何判斷 好的sdk:

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

不過這次實(shí)踐下來, 我要收回這句話, 從 「進(jìn)化論」 的角度來看才更趨于真理:

好用的 sdk, 應(yīng)該是能跟上社區(qū)最佳標(biāo)準(zhǔn)與實(shí)踐, 不斷進(jìn)化的.

說一下項(xiàng)目實(shí)踐中遇到的問題: 同時(shí)使用 阿里云日志服務(wù)和OSS服務(wù)的sdk, 而 2 這的 sdk 中都定義了 RequestCore 來作為 http 請(qǐng)求基類, 導(dǎo)致類沖突

<?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');

本來只是想對(duì)現(xiàn)有日志功能進(jìn)行改造, 要是導(dǎo)致原有的 OSS 功能不能用了, 那就不好了. 基于此, 就動(dòng)了直接接入日志服務(wù) api 的念頭:

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

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

    // 請(qǐng)求簽名: 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));

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

    // post
    // 數(shù)據(jù)編碼方式 - 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);
}

事實(shí)證明 我還是太年輕了, 日志傳輸用的 protobuf. 這東西說實(shí)話并不難, 之前的服務(wù)器系列有protobuf 的入門使用(blog - 服務(wù)器開發(fā)系列 1), 無非是安裝一個(gè) protobuf 的編譯器(protoc), 然后安裝一個(gè)protobuf的解析器(對(duì)應(yīng) php 中的 ext-protobuf 擴(kuò)展)

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; // 內(nèi)部字段,不需要填寫
    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; // 內(nèi)部字段,不需要填寫
    string topic = 3;
    string source = 4;
}
message LogGroupList
{
    repeated LogGroup logGroupList = 1;
}

導(dǎo)致的結(jié)果就是, protobuf序列化的數(shù)據(jù)大小, 和 demo 對(duì)上, api 自然就不通了. 而官方 SDK 中, 是用 pack() 自己一點(diǎn)點(diǎn)實(shí)現(xiàn)的. 這事我在剛接觸服務(wù)器開發(fā)的時(shí)候也干過...

不過好在, OSS的SDK按照 psr-4 標(biāo)準(zhǔn)進(jìn)行組織了, 引入命名空間后就不會(huì)有現(xiàn)在類沖突的尷尬了.

寫在最后

日志服務(wù)是一個(gè)深究起來還頗為復(fù)雜的話題, 重要實(shí)踐, 讓日志真正起到 系統(tǒng)保駕護(hù)航異常時(shí)還原犯罪現(xiàn)場的作用

推薦資源:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容