.NET LINQ分析AWS ELB日志避免996
前言
小明是個(gè)單純的 .NET開發(fā),一天大哥叫住他,安排了一項(xiàng)任務(wù):
“小明,分析一下我們 超牛逼網(wǎng)站上個(gè)月的所有 AWS ELB流量日志,這些日志保存在 AWS S3上,你分析下,看哪個(gè) API的響應(yīng)時(shí)間中位數(shù)最長(zhǎng)。”
“對(duì)了,別用 Excel,哥給你寫好了一段 Python腳本,可以自動(dòng)解析統(tǒng)計(jì)一個(gè) AWS ELB文件的日志,你可以利用一下。”
“好的?,大哥真厲害!”。
小明看了一下,然后傻眼了,在管理控制臺(tái)中,九月份 AWS ELB日志文件翻了好幾頁都沒翻完,大概算算,大概有 1000個(gè)文件不止。想想自己又不懂 Python,又不是搞數(shù)據(jù)分析專業(yè)出身的,這個(gè)“看似簡(jiǎn)單”的工作完不成,這周怕是陪不了女朋友,搞不好還要 996.ICU,小明幾乎要流下了沒有技術(shù)的淚水……
不怕!會(huì).NET就行!
要完成這項(xiàng)工作,光老老實(shí)實(shí)將文件從管理控制臺(tái)下載到本地,估計(jì)都?jí)蚝纫粔亍H粜∶魃詸C(jī)靈點(diǎn),他可能會(huì)找到 AWS S3的文件管理器,然后……發(fā)現(xiàn)只有付費(fèi)版才有批量下載功能。
其實(shí)要完成這項(xiàng)工作,只需做好兩項(xiàng)基本任務(wù)即可:
從?AWS S3下載9月份的所有?ELB日志
聚合并分析這1000多個(gè)日志文件,然后按響應(yīng)時(shí)間中位數(shù)倒排序
AWS資源
能在管理控制臺(tái)上看到的 AWS資源, AWS都提供了各語言的 SDK可供操作(可在 SDK上操作的東西,如批量下載,反倒不一定能在界面上看到)。SDK支持多種語言,其中(顯然)也包括 .NET。
對(duì)于 AWS S3的訪問, Amazon提供的 NuGet包叫:AWSSDK.S3,在 VisualStudio中下載并安裝,即可運(yùn)行本文的示例。
要使用 AWSSDK.S3,首先需要實(shí)例化一個(gè) AmazonS3Client,并傳入 aws access key、 aws secret key、 AWS區(qū)域等參數(shù):
var credentials = new BasicAWSCredentials( Util.GetPassword("aws_live_access_key"), Util.GetPassword("aws_live_secret_key")); var s3 = new AmazonS3Client(credentials, RegionEndpoint.USEast1);注意:本文的所有代碼全部共享這一個(gè) s3的實(shí)例。因?yàn)楦鶕?jù)文檔, AmazonS3Client實(shí)例是設(shè)計(jì)為線程安全的。
在下載 AWS S3的文件(對(duì)象)之前,首先需要知道有哪些對(duì)象可供下載,可通過 ListObjectsV2Async方法列出某個(gè) bucket的文件列表。注意該方法是分頁的,經(jīng)我的測(cè)試,無論 MaxKeys參數(shù)設(shè)置多大,該接口最多一次性返回 1000條數(shù)據(jù),但這顯然不夠,因此需要循環(huán)分頁去拿。
分頁時(shí)該響應(yīng)對(duì)象中包含了 NextContinuationToken和 IsTruncated屬性,如果 IsTruncated=true,則 NextContinuationToken必定有值,此時(shí)下次調(diào)用 ListObjectsV2Async時(shí)的請(qǐng)求參數(shù)傳入 NextContinuationToken即可實(shí)現(xiàn)分頁獲取 S3文件列表的功能。
這個(gè)過程說起來有點(diǎn)繞,但感謝 C#提供了 yield關(guān)鍵字來實(shí)現(xiàn) 協(xié)程-coroutine,代碼寫起來非常簡(jiǎn)單:
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日志都會(huì)按時(shí)間會(huì)有一個(gè)前綴模式,從文件列表中找到這一模式后填入該參數(shù)。
接下來就簡(jiǎn)單了,通過 GetObjectAsync方法即可下載某個(gè)對(duì)象,要直接分析,最好先轉(zhuǎn)換為字符串,拿到文件流 stream后,最簡(jiǎn)單的方式是使用 StreamReader將其轉(zhuǎn)換為字符串:
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類實(shí)現(xiàn)了?IDisposable接口,因?yàn)樗?ResponseStream實(shí)際上是非托管資源,需要單獨(dú)釋放。因此需要使用?using關(guān)鍵字來實(shí)現(xiàn)資源的正確釋放。
可以直接調(diào)用?StreamReader.ReadToEnd()方法直接獲取全部字符串,然后再通過?Split將字符串按行分隔,但這樣會(huì)浪費(fèi)大量?jī)?nèi)存,影響性能。
這時(shí)一般會(huì)將這個(gè) stream緩存到本地磁盤以供慢慢分析,但也可以一鼓作氣直接將該 stream轉(zhuǎn)換為字符串直接分析。本文將采取后者做法。
分析1000多個(gè)文件
每個(gè) 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提供了該日志的詳細(xì)文檔中文說明:https://docs.aws.amazon.com/zh_cn/elasticloadbalancing/latest/application/load-balancer-access-logs.html#access-log-entry-format
根據(jù)文檔,這種日志可以通過按簡(jiǎn)單的空格分隔來解析,但后面的 RequestInfo和 UserAgent字段稍微麻煩點(diǎn),這種可以使用 正則表達(dá)式來實(shí)現(xiàn)比較精致的效果:
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
數(shù)據(jù)下載好了,解析也成功了,這時(shí)即可通過強(qiáng)大的 LINQ來進(jìn)行分析。這里將用到以下的操作符:
SelectMany?數(shù)據(jù)“打平”(和?js數(shù)組的?.flatMap方法類似)
Select?數(shù)據(jù)轉(zhuǎn)換(和?js數(shù)組的?.map方法類似)
GroupBy?數(shù)據(jù)分組
首先,通過 AWSSDK的 ListObjectsV2Async方法,獲取的是文件列表,可以通過 .SelectMany方法將多個(gè)下載批次“打平”:
Load201909SuperCoolData(s3) .SelectMany(x => x)然后通過 Select,將單個(gè)文件 Key下載并讀為字符串:
Load201909SuperCoolData(s3) .SelectMany(x => x) .SelectMany(x => ReadS3Object(s3, x))然后再通過 Select,將文件每一行日志轉(zhuǎn)換為一條 .NET對(duì)象:
Load201909SuperCoolData(s3) .SelectMany(x => x) .SelectMany(x => ReadS3Object(s3, x)) .Select(LogEntry.Parse)有了 .NET對(duì)象,即可利用 LINQ進(jìn)行愉快地分析了,如小明需要求,只需加一個(gè) GroupBy和 Select,即可求得根據(jù) Url分組的響應(yīng)時(shí)間中位數(shù),然后再通過 OrderByDescending即按該數(shù)字排序,最后通過 .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();運(yùn)行效果如下:
多線程下載
解析和分析都在內(nèi)存中進(jìn)行,因此本代碼的瓶頸在于下載速度。
上文中的代碼是串行、單線程下載,帶寬利用率低,下載速度慢。可以改成并行、多線程下載,以提高帶寬利用率。
傳統(tǒng)的多線程需要非常大的功力,需要很好的技巧才能完成。但 .NET4.0發(fā)布了 ParallelLINQ,只需極少的代碼改動(dòng),即可享受到多線程的便利。在這里,只需將在第二個(gè) SelectMany后加上一個(gè) AsParallel(),即可瞬間獲取多線程下載優(yōu)勢(shì):
Load201909SuperCoolData(s3) .SelectMany(x => x) .AsParallel() // 重點(diǎn) .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();注意:寫 AsParallel()的位置有講究,這取決于你對(duì)性能瓶頸的把控。總的來說:
太靠后了不行,因?yàn)?AsParallel之前的語句都是串行的;
靠前了也不行,因?yàn)榭壳暗拇a往往數(shù)據(jù)量還沒擴(kuò)大,并行沒意義;
擴(kuò)展
到了這一步,如果小明足夠機(jī)靈,其實(shí)還能再擴(kuò)展擴(kuò)展,將平均值,總響應(yīng)時(shí)間一并求出來,改動(dòng)代碼也不大,只需將下方那個(gè) 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), })運(yùn)行效果如下:
總結(jié)
看來并不需要 python,有了 .NET和 LINQ兩大法寶,看來小明周末又可以陪女朋友了?
總結(jié)
以上是生活随笔為你收集整理的.NET LINQ分析AWS ELB日志避免996的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core 微信公众号小程序6种
- 下一篇: [ASP.NET Core 3框架揭秘]