优秀的编程知识分享平台

网站首页 > 技术文章 正文

.NET LINQ分析AWS ELB日志避免996

nanyue 2024-08-09 07:08:47 技术文章 8 ℃

前言

小明是个单纯的 .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,在VisualStudio下载并安装,即可运行本文的示例。

要使用 AWSSDK.S3,首先需要实例化一个AmazonS3Client,并传入aws access keyaws secret keyAWS区域等参数:

  1. varcredentials =newBasicAWSCredentials(

  2. Util.GetPassword("aws_live_access_key"),

  3. Util.GetPassword("aws_live_secret_key"));

  4. vars3 =newAmazonS3Client(credentials, RegionEndpoint.USEast1);

注意:本文的所有代码全部共享这一个 s3的实例。因为根据文档,AmazonS3Client实例是设计为线程安全的。

下载 AWS S3的文件(对象)之前,首先需要知道有哪些对象可供下载,可通过ListObjectsV2Async方法列出某个bucket的文件列表。注意该方法是分页的,经我的测试,无论MaxKeys参数设置多大,该接口最多一次性返回1000条数据,但这显然不够,因此需要循环分页去拿。

分页时该响应对象中包含了 NextContinuationTokenIsTruncated属性,如果IsTruncated=true,则NextContinuationToken必定有值,此时下次调用ListObjectsV2Async时的请求参数传入NextContinuationToken即可实现分页获取S3文件列表的功能。

这个过程说起来有点绕,但感谢 C#提供了yield关键字来实现协程-coroutine,代码写起来非常简单:

  1. IEnumerable<List<S3Object>> Load201909SuperCoolData(AmazonS3Client s3)

  2. {

  3. ListObjectsV2Response response = ;

  4. do

  5. {

  6. response = s3.ListObjectsV2Async(newListObjectsV2Request

  7. {

  8. BucketName = "supercool-website",

  9. Prefix = "AWSLogs/1383838438/elasticloadbalancing/us-east-1/2019/09",

  10. ContinuationToken = response?.NextContinuationToken,

  11. MaxKeys = 100,

  12. }).Result;

  13. yieldreturnresponse.S3Objects;

  14. } while(response.IsTruncated);

  15. }

注意:Prefix为前缀,AWS ELB日志都会按时间会有一个前缀模式,从文件列表中找到这一模式后填入该参数。

接下来就简单了,通过 GetObjectAsync方法即可下载某个对象,要直接分析,最好先转换为字符串,拿到文件流stream后,最简单的方式是使用StreamReader将其转换为字符串:

  1. IEnumerable<string> ReadS3Object(AmazonS3Client s3, S3Object x)

  2. {

  3. usingGetObjectResponse obj = s3.GetObjectAsync(x.BucketName, x.Key).Result;

  4. usingvarreader =newStreamReader(obj.ResponseStream);

  5. while(!reader.EndOfStream)

  6. {

  7. yieldreturnreader.ReadLine;

  8. }

  9. }

注意:

  1. GetObjectAsync方法返回的GetObjectResponse类实现了IDisposable接口,因为它的ResponseStream实际上是非托管资源,需要单独释放。因此需要使用using关键字来实现资源的正确释放。

  2. 可以直接调用 StreamReader.ReadToEnd方法直接获取全部字符串,然后再通过Split将字符串按行分隔,但这样会浪费大量内存,影响性能。

这时一般会将这个 stream缓存到本地磁盘以供慢慢分析,但也可以一鼓作气直接将该stream转换为字符串直接分析。本文将采取后者做法。

分析1000多个文件

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

  1. 2019-08-31T23:08:36.637570ZSUPER-COOLELB10.0.2.127:5973710.0.3.142:860.0000380.6212490.0000412002006359291"POST http://super-coolelb-10086.us-east-1.elb.amazonaws.com:80/api/Super/Cool HTTP/1.1" "-" - -

  2. 2019-08-31T23:28:36.264848ZSUPER-COOLELB10.0.3.236:5414110.0.3.249:860.000040.6222080.0000452002006359291"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字段稍微麻烦点,这种可以使用正则表达式来实现比较精致的效果:

  1. publicstaticLogEntry Parse(stringline)

  2. {

  3. MatchCollection s = Regex.Matches(line, @"[\""].+?[\""]|[^ ]+");

  4. string requestInfo = s[11].Value.Replace("\"", "").Split(' ');

  5. returnnew

  6. {

  7. Timestamp = DateTime.Parse(s[0].Value),

  8. ElbName = s[1].Value,

  9. ClientEndpoint = s[2].Value,

  10. BackendEndpoint = s[3].Value,

  11. RequestTime = decimal.Parse(s[4].Value),

  12. BackendTime = decimal.Parse(s[5].Value),

  13. ResponseTime = decimal.Parse(s[6].Value),

  14. ElbStatusCode = int.Parse(s[7].Value),

  15. BackendStatusCode = int.Parse(s[8].Value),

  16. ReceivedBytes = long.Parse(s[9].Value),

  17. SentBytes = long.Parse(s[10].Value),

  18. Method = requestInfo[0],

  19. Url = requestInfo[1],

  20. Protocol = requestInfo[2],

  21. UserAgent = s[12].Value.Replace("\"", ""),

  22. SslCypher = s[13].Value,

  23. SslProtocol = s[14].Value,

  24. };

  25. }

LINQ

数据下载好了,解析也成功了,这时即可通过强大的 LINQ来进行分析。这里将用到以下的操作符:

  • SelectMany数据“打平”(和js数组的.flatMap方法类似)

  • Select数据转换(和js数组的.map方法类似)

  • GroupBy数据分组

首先,通过 AWSSDKListObjectsV2Async方法,获取的是文件列表,可以通过.SelectMany方法将多个下载批次“打平”:

  1. Load201909SuperCoolData(s3)

  2. .SelectMany(x => x)

然后通过 Select,将单个文件Key下载并读为字符串:

  1. Load201909SuperCoolData(s3)

  2. .SelectMany(x => x)

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

然后再通过 Select,将文件每一行日志转换为一条.NET对象:

  1. Load201909SuperCoolData(s3)

  2. .SelectMany(x => x)

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

  4. .Select(LogEntry.Parse)

有了 .NET对象,即可利用LINQ进行愉快地分析了,如小明需要求,只需加一个GroupBySelect,即可求得根据Url分组的响应时间中位数,然后再通过OrderByDescending即按该数字排序,最后通过.Dump显示出来:

  1. Load201909SuperCoolData(s3)

  2. .SelectMany(x => x)

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

  4. .Select(LogEntry.Parse)

  5. .GroupBy(x => x.Url)

  6. .Select(x => new

  7. {

  8. Url = x.Key,

  9. Median = x.OrderBy(x => x.BackendTime).ElementAt(x.Count / 2)

  10. })

  11. .OrderByDescending(x => x.Median)

  12. .Dump;

运行效果如下:

多线程下载

解析和分析都在内存中进行,因此本代码的瓶颈在于下载速度。

上文中的代码是串行、单线下载,带宽利用率低,下载速度慢。可以改成并行、多线程下载,以提高带宽利用率。

传统的多线程需要非常大的功力,需要很好的技巧才能完成。但 .NET4.0发布了ParallelLINQ,只需极少的代码改动,即可享受到多线程的便利。在这里,只需将在第二个SelectMany加上一个AsParallel,即可瞬间获取多线程下载优势:

  1. Load201909SuperCoolData(s3)

  2. .SelectMany(x => x)

  3. .AsParallel // 重点

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

  5. .Select(LogEntry.Parse)

  6. .GroupBy(x => x.Url)

  7. .Select(x => new

  8. {

  9. Url = x.Key,

  10. Median = x.OrderBy(x => x.BackendTime).ElementAt(x.Count / 2)

  11. })

  12. .OrderByDescending(x => x.Median)

  13. .Dump;

注意:写 AsParallel的位置有讲究,这取决于你对性能瓶颈的把控。总的来说:

  • 太靠后了不行,因为 AsParallel之前的语句都是串行的;

  • 靠前了也不行,因为靠前的代码往往数据量还没扩大,并行没意义;

扩展

到了这一步,如果小明足够机灵,其实还能再扩展扩展,将平均值,总响应时间一并求出来,改动代码也不大,只需将下方那个 Select改成如下即可:

  1. .Select(x => new

  2. {

  3. Url = x.Key,

  4. Median = x.OrderBy(x => x.BackendTime).ElementAt(x.Count / 2),

  5. Avg = x.Average(x => x.BackendTime),

  6. Sum = x.Sum(x => x.BackendTime),

  7. })

运行效果如下:

总结

看来并不需要 python,有了.NETLINQ两大法宝,看来小明周末又可以陪女朋友了

Tags:

最近发表
标签列表