.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 key
、aws secret key
、AWS
區域等引數:
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
條資料,但這顯然不夠,因此需要迴圈分頁去拿。
分頁時該響應物件中包含了NextContinuationToken
和IsTruncated
屬性,如果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();
}
}
注意:
GetObjectAsync
方法返回的GetObjectResponse
類實現了IDisposable
介面,因為它的ResponseStream
實際上是非託管資源,需要單獨釋放。因此需要使用using
關鍵字來實現資源的正確釋放。- 可以直接呼叫
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
根據文件,這種日誌可以通過按簡單的空格分隔來解析,但後面的RequestInfo
和UserAgent
欄位稍微麻煩點,這種可以使用正則表示式
來實現比較精緻的效果:
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
資料分組
首先,通過AWSSDK
的ListObjectsV2Async
方法,獲取的是檔案列表,可以通過.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
進行愉快地分析了,如小明需要求,只需加一個GroupBy
和Select
,即可求得根據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
,有了.NET
和LINQ
兩大法寶,看來小明週末又可以陪女朋友了