.NET LINQ分析AWS ELB日志


前言

小明是個單純的.NET開發,一天大哥叫住他,安排了一項任務:

“小明,分析一下我們超牛逼網站上個月的所有AWS ELB流量日志,這些日志保存在AWS S3上,你分析下,看哪個API的響應時間中位數最長。”

“對了,別用Excel,哥給你寫好了一段Python腳本,可以自動解析統計一個AWS ELB文件的日志,你可以利用一下。”

“好的✌,大哥真厲害!”。

小明看了一下,然后傻眼了,在管理控制台中,九月份AWS ELB日志文件翻了好幾頁都沒翻完,大概算算,大概有1000個文件不止。想想自己又不懂Python,又不是搞數據分析專業出身的,這個“看似簡單”的工作完不成,這周怕是陪不了女朋友,搞不好還要996.ICU,小明幾乎要流下了沒有技術的淚水……

不怕!會.NET就行!

要完成這項工作,光老老實實將文件從管理控制台下載到本地,估計都夠喝一壺。若小明稍機靈點,他可能會找到AWS S3的文件管理器,然后……發現只有付費版才有批量下載功能。

其實要完成這項工作,只需做好兩項基本任務即可:

  • AWS S3下載9月份的所有ELB日志
  • 聚合並分析這1000多個日志文件,然后按響應時間中位數倒排序

AWS資源

能在管理控制台上看到的AWS資源,AWS都提供了各語言的SDK可供操作(可在SDK上操作的東西,如批量下載,反倒不一定能在界面上看到)。SDK支持多種語言,其中(顯然)也包括.NET

對於AWS S3的訪問,Amazon提供的NuGet包叫:AWSSDK.S3,在Visual Studio中下載並安裝,即可運行本文的示例。

要使用AWSSDK.S3,首先需要實例化一個AmazonS3Client,並傳入aws access keyaws secret keyAWS區域等參數:

var credentials = new BasicAWSCredentials(
    Util.GetPassword("aws_live_access_key"), 
    Util.GetPassword("aws_live_secret_key"));
var s3 = new AmazonS3Client(credentials, RegionEndpoint.USEast1);

注意:本文的所有代碼全部共享這一個s3的實例。因為根據文檔,AmazonS3Client實例是設計為線程安全的。

在下載AWS S3的文件(對象)之前,首先需要知道有哪些對象可供下載,可通過ListObjectsV2Async方法列出某個bucket的文件列表。注意該方法是分頁的,經我的測試,無論MaxKeys參數設置多大,該接口最多一次性返回1000條數據,但這顯然不夠,因此需要循環分頁去拿。

分頁時該響應對象中包含了NextContinuationTokenIsTruncated屬性,如果IsTruncated=true,則NextContinuationToken必定有值,此時下次調用ListObjectsV2Async時的請求參數傳入NextContinuationToken即可實現分頁獲取S3文件列表的功能。

這個過程說起來有點繞,但感謝C#提供了yield關鍵字來實現協程-coroutine,代碼寫起來非常簡單:

IEnumerable<List<S3Object>> Load201909SuperCoolData(AmazonS3Client s3)
{
    ListObjectsV2Response response = null;
    do
    {
        response = s3.ListObjectsV2Async(new ListObjectsV2Request
        {
            BucketName = "supercool-website",
            Prefix = "AWSLogs/1383838438/elasticloadbalancing/us-east-1/2019/09",
            ContinuationToken = response?.NextContinuationToken, 
            MaxKeys = 100, 
        }).Result;
        yield return response.S3Objects;
    } while (response.IsTruncated);
}

注意:Prefix為前綴,AWS ELB日志都會按時間會有一個前綴模式,從文件列表中找到這一模式后填入該參數。

接下來就簡單了,通過GetObjectAsync方法即可下載某個對象,要直接分析,最好先轉換為字符串,拿到文件流stream后,最簡單的方式是使用StreamReader將其轉換為字符串:

IEnumerable<string> ReadS3Object(AmazonS3Client s3, S3Object x)
{
    using GetObjectResponse obj = s3.GetObjectAsync(x.BucketName, x.Key).Result;
    using var reader = new StreamReader(obj.ResponseStream);
    while (!reader.EndOfStream)
    {
        yield return reader.ReadLine();
    }
}

注意:

  1. GetObjectAsync方法返回的GetObjectResponse類實現了IDisposable接口,因為它的ResponseStream實際上是非托管資源,需要單獨釋放。因此需要使用using關鍵字來實現資源的正確釋放。
  2. 可以直接調用StreamReader.ReadToEnd()方法直接獲取全部字符串,然后再通過Split將字符串按行分隔,但這樣會浪費大量內存,影響性能。

這時一般會將這個stream緩存到本地磁盤以供慢慢分析,但也可以一鼓作氣直接將該stream轉換為字符串直接分析。本文將采取后者做法。

分析1000多個文件

每個ELB日志文件的格式如下:

2019-08-31T23:08:36.637570Z SUPER-COOLELB 10.0.2.127:59737 10.0.3.142:86 0.000038 0.621249 0.000041 200 200 6359 291 "POST http://super-coolelb-10086.us-east-1.elb.amazonaws.com:80/api/Super/Cool HTTP/1.1" "-" - -
2019-08-31T23:28:36.264848Z SUPER-COOLELB 10.0.3.236:54141 10.0.3.249:86 0.00004 0.622208 0.000045 200 200 6359 291 "POST http://super-coolelb-10086.us-east-1.elb.amazonaws.com:80/api/Super/Cool HTTP/1.1" "-" - -

可見該日志有一定格式,Amazon提供了該日志的詳細文檔中文說明:https://docs.aws.amazon.com/zh_cn/elasticloadbalancing/latest/application/load-balancer-access-logs.html#access-log-entry-format

根據文檔,這種日志可以通過按簡單的空格分隔來解析,但后面的RequestInfoUserAgent字段稍微麻煩點,這種可以使用正則表達式來實現比較精致的效果:

public static LogEntry Parse(string line)
{
    MatchCollection s = Regex.Matches(line, @"[\""].+?[\""]|[^ ]+");
    string[] requestInfo = s[11].Value.Replace("\"", "").Split(' ');
    return new
    {
        Timestamp = DateTime.Parse(s[0].Value),
        ElbName = s[1].Value,
        ClientEndpoint = s[2].Value,
        BackendEndpoint = s[3].Value,
        RequestTime = decimal.Parse(s[4].Value),
        BackendTime = decimal.Parse(s[5].Value),
        ResponseTime = decimal.Parse(s[6].Value),
        ElbStatusCode = int.Parse(s[7].Value),
        BackendStatusCode = int.Parse(s[8].Value),
        ReceivedBytes = long.Parse(s[9].Value),
        SentBytes = long.Parse(s[10].Value),
        Method = requestInfo[0],
        Url = requestInfo[1],
        Protocol = requestInfo[2],
        UserAgent = s[12].Value.Replace("\"", ""),
        SslCypher = s[13].Value,
        SslProtocol = s[14].Value,
    };
}

LINQ

數據下載好了,解析也成功了,這時即可通過強大的LINQ來進行分析。這里將用到以下的操作符:

  • SelectMany 數據“打平”(和js數組的.flatMap方法類似)
  • Select 數據轉換(和js數組的.map方法類似)
  • GroupBy 數據分組

首先,通過AWSSDKListObjectsV2Async方法,獲取的是文件列表,可以通過.SelectMany方法將多個下載批次“打平”:

Load201909SuperCoolData(s3)
    .SelectMany(x => x)

然后通過Select,將單個文件Key下載並讀為字符串:

Load201909SuperCoolData(s3)
	.SelectMany(x => x)
	.SelectMany(x => ReadS3Object(s3, x))

然后再通過Select,將文件每一行日志轉換為一條.NET對象:

Load201909SuperCoolData(s3)
	.SelectMany(x => x)
	.SelectMany(x => ReadS3Object(s3, x))
    .Select(LogEntry.Parse)

有了.NET對象,即可利用LINQ進行愉快地分析了,如小明需要求,只需加一個GroupBySelect,即可求得根據Url分組的響應時間中位數,然后再通過OrderByDescending即按該數字排序,最后通過.Dump顯示出來:

Load201909SuperCoolData(s3)
	.SelectMany(x => x)
	.SelectMany(x => ReadS3Object(s3, x))
    .Select(LogEntry.Parse)
    .GroupBy(x => x.Url)
    .Select(x => new
    {
        Url = x.Key, 
        Median = x.OrderBy(x => x.BackendTime).ElementAt(x.Count() / 2)
    })
    .OrderByDescending(x => x.Median)
    .Dump();

運行效果如下:

多線程下載

解析和分析都在內存中進行,因此本代碼的瓶頸在於下載速度。

上文中的代碼是串行、單線程下載,帶寬利用率低,下載速度慢。可以改成並行、多線程下載,以提高帶寬利用率。

傳統的多線程需要非常大的功力,需要很好的技巧才能完成。但.NET 4.0發布了Parallel LINQ,只需極少的代碼改動,即可享受到多線程的便利。在這里,只需將在第二個SelectMany后加上一個AsParallel(),即可瞬間獲取多線程下載優勢:

Load201909SuperCoolData(s3)
	.SelectMany(x => x)
    .AsParallel() // 重點
	.SelectMany(x => ReadS3Object(s3, x))
    .Select(LogEntry.Parse)
    .GroupBy(x => x.Url)
    .Select(x => new
    {
        Url = x.Key, 
        Median = x.OrderBy(x => x.BackendTime).ElementAt(x.Count() / 2)
    })
    .OrderByDescending(x => x.Median).Take(15)
    .Dump();

注意:寫AsParallel()的位置有講究,這取決於你對性能瓶頸的把控。總的來說:

  • 太靠后了不行,因為AsParallel之前的語句都是串行的;
  • 靠前了也不行,因為靠前的代碼往往數據量還沒擴大,並行沒意義;

擴展

到了這一步,如果小明足夠機靈,其實還能再擴展擴展,將平均值,總響應時間一並求出來,改動代碼也不大,只需將下方那個Select改成如下即可:

    .Select(x => new
    {
        Url = x.Key, 
        Median = x.OrderBy(x => x.BackendTime).ElementAt(x.Count() / 2), 
        Avg = x.Average(x => x.BackendTime), 
        Sum  = x.Sum(x => x.BackendTime), 
    })

運行效果如下:

總結

看來並不需要python,有了.NETLINQ兩大法寶,看來小明周末又可以陪女朋友了😎

喜歡的請關注我的微信公眾號:【DotNet騷操作】

DotNet騷操作


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM