关于日志:十行代码让日志存储降低80

前言履约治理是一个面向物流商家的OMS工作台,自从初代目把架子搭起来之后,就没有持续投入了,起初始终是合作伙伴同学在负责日常保护和需要撑持。通过几年的横蛮成长,零碎曾经杂草丛生,乱象百出。再起初,甚至一度成为一块无主之地,走行业共建的形式来反对。对于一个不反对行业隔离的零碎,行业共建象征这个零碎将疾速腐化。两年前我开始接管履约治理,来到这片广大的蛮荒之地,正如所有那些渴望造物乐趣并且手里刚好有锤子镰刀的人,我就像一匹脱缰的野马,脑子里常常会产生很多大胆且离奇的想法,心愿借此把履约治理打造成一个完满的零碎。只惋惜真正可能付诸实践的少之又少,本篇就是为数不多得以落地,并且有相当实用价值idea中的一个,整理出来分享给有须要的同学做参考。 日志乱象日志是日常开发中最有可能被忽视,最容易被滥用的一个模块。被忽视是因为打日志切实是一个再简略不过的事,前人设计好了一个logback.xml,前面只须要如法炮制定义一个logger,顺手一个info调用就搞定,他甚至不确定这条日志能不能打进去,也不晓得会打在哪个文件,反正先跑一次试试,不行就换error。被滥用是因为不同场景日志的格局内容千差万别,或者说日志打法太灵便,太随便了,格调太多样化了,以至于简直每个人一言不合就要本人写一个LogUtil,我见过最夸大的,一个零碎中用于打日志的工具类,有二三十个之多,前人纠结该用哪个工具可能就要做半个小时的思想斗争,完满诠释了什么叫破窗效应。最好的学习形式就是通过反面教材吸取教训,上面咱们列举一些最常见的日志设计开发过程中的问题。 分类之乱一般来说,一个零碎必然须要设计多个日志文件以辨别不同业务或场景,不可能所有的日志都打到一个文件里。然而怎么进行分类,没人通知咱们,于是就有了各种各样的分类。按零碎模块分。这种分类应该是最根底的一种分类,也是最有层次感的分类。比方履约服务中枢的零碎分层。基本上每一层对应一个日志文件。 按租户身份分。个别中台零碎都会反对多个租户(行业),每一个租户独自对应一个日志文件。这种分类个别不会独自应用,除非你要做齐全意义上的租户隔离。意识流分类法。不合乎MECE法令,没有清晰对立的分类逻辑,按业务分,按零碎模块分,按接口能力分,按新老链路分,各种分法的影子都能看到,后果就是分进去几十个文件,打日志的人基本就不晓得这一行的日志会打进哪个文件。以上说的各种分类形式,都不是相对纯正的,因为无论哪一种,无论一开始设计的如许边界清晰,随着工夫的推动,最初都会演变为一个大杂烩。 某人心愿独自监控某个类产生的日志,新增日志文件;新增了一个业务,比方一盘货,想独自监控,新增日志文件;发动了一场服务化战斗,针对服务化链路独自监控,新增日志文件;某个业务想采集用户行为,又不想全接日志音讯,新增日志文件;资损敞口的场景,须要特地关注,新增日志文件;非凡期间内产生的日志,比方大促,新增日志文件;凡此种种,不一而足。发现没有,总有那么一瞬间能让人产生新增日志文件的神经激动,他们的诉求和场景也不堪称不合理,只管这些日志的维度齐全不相干,然而没有什么能阻止这种激动。最开始的那一套日志设计,就像一个濒临死亡的大象,一直地被不同的利益方从身上扯下一块分去。 格局之乱对于日志须要有肯定的格局这点置信没有人会有异议,格局的乱象次要体现在两个方面,一个是格局的设计上,有些零碎设计了非常复杂的格局,用多种分隔符组合,反对日志内容的分组,用关键词定位的形式代替固定地位的格局,同时反对格局扩大,这对人脑和计算机去解析都是一种累赘。第二个是同一个日志文件,还能呈现不同格局的内容,堆栈和失常业务日志混淆。来看一个例子,我不给任何提醒,你能在大脑里很快剖析出这个日志的构造吗? requestParam$&trace@2150435916867358634668899ebccf&scene@test&logTime@2023-06-14 17:44:23&+skuPromiseInfo$&itemId@1234567:1&skuId@8888:1&buyerId@777:1&itemTags@,123:1,2049:1,249:1,&sellerId@6294:1&toCode@371621:1&toTownCode@371621003:1&skuBizCode@TMALL_TAOBAO:1&skuSubBizCode@TMALL_DEFAULT:1&fromCode@DZ_001:1+orderCommonInfo$&orderId@4a04c79734652f6bd7a8876379399777&orderBizCode@TMALL_TAOBAO&orderSubBizCode@TMALL_DEFAULT&toCode@371621&toTownCode@371621003&+工具之乱有时候甚至会呈现,同一个类,同一个办法中,两行不同的日志埋点,打进去的日志格局不一样,落的日志文件也不一样。为什么会呈现这种状况?就是因为用了不同的日志工具。要究其根源,咱们须要剖析一下不同的工具到底是在做什么。能够发现,很多工具之间的差异就是反对的参数类型不一样,有些是打印订单对象的,有些是打印消息的,有些是打印调度日志的。还有一些差异是面向不同业务场景的,比方一盘货专用工具,负卖专用工具。还有一些差别是面向不同的异样封装的,有些是打印ExceptionA,有些是打印ExceptionB的。世间离奇事,莫过于此,或者只能用存在即正当去解释了。 日志分层我始终崇奉极简的设计准则,简略意味着颠扑不破。下面提到,一套日志零碎最终的终局肯定是走向凌乱,既然这种趋势无奈防止,那么咱们在最后设计的时候就只能确保一件事,保障原始的分类尽量简略,且不重叠。其实通用的分类形式无非就两种,一种按职能程度拆分,一种按业务垂直拆分。一般来说,一级分类,应该采纳程度拆分。因为业务的边界个别是很难划清的,边界绝对含糊,职能的边界就绝对清晰稳固很多,职能其实反映的是工作流,工作流一经造成,根本不会产生太大的结构性变动。基于这种思路,我设计了如下的日志分层。 从档次上来看,其实只有三层,入口,内核,进口。入口日志只负责打印流量入口的出入参,比方HSF,controller。进口日志负责打印所有第三方服务调用的出入参。内核日志,负责打印所有两头执行过程中的业务日志。就三层足矣,足够简略,不重不漏。另外把堆栈日志独自拎进去,堆栈相比业务日志有很大的特殊性,本文题目所指出的日志存储升高优化,也只是针对堆栈日志做的优化,这个前面再讲。 格局设计日志的格局设计也有一些考究。首先日志的设计是面向人可读的,这个无需多言。另外也十分重要的一个点,要面向可监控的设计,这是容易被很多人漠视的一个点。基于这两个准则,说一下我在格局设计上的一些思路。首先要做维度形象。既然是面向监控,监控个别须要反对多个保护,比方行业维度,服务维度,商家维度等等,那么咱们就须要把所有的维度因子抽出来。那么这些维度理论打印的时候怎么传给logger呢?倡议是把他们存到ThreadLocal中,打的时候从上下文中取。这样做还有一个益处是,日志打印工具设计的时候就会很优雅,只须要传很少的参数。格局尽量简略,采纳约定大于配置的准则,每一个维度占据一个固定的地位,用逗号宰割。切忌设计一个大而全的模型,而后间接整个的序列化为一个JSON字符串。也不要被所谓的扩展性给引诱,给应用方轻易开出一个可能自定义格局的口子,即使你能轻而易举的提供这种能力。依据我的教训,这种扩展性肯定会被滥用,到最初连设计者也不晓得理论的格局到底是怎么的。当然这个须要设计者有较高的视线和远见,不过这不是难点,难的还是克服本人炫技的欲望。在内容上,尽量打印能够自解释的文本,做到见名知义。举个例子,咱们要打印退款标,退款标本来是用1, 2, 4, 8这种二进制位存储的,打印的时候不要间接打印存储值,翻译成一个能形容它含意的英文code。格局示例 timeStamp|threadName logLevel loggerName|sourceAppName,flowId,traceId,sceneCode,identityCode,loginUserId,scpCode,rpcId,isYace,ip||businessCode,isSuccess||parameters||returnResult||内容示例2023-08-14 14:37:12.919|http-nio-7001-exec-10 INFO c.a.u.m.s.a.LogAspect|default,c04e4b7ccc2a421995308b3b33503dda,0bb6d59616183822328322237e84cc,queryOrderStatus,XIAODIAN,5000000000014,123456,0.1.1.8,null,255.255.255.255||queryOrderStatus,success||{"@type":"com.alibaba.common.model.queryorder.req.QueryOrderListReq","currentUserDTO":{"bizGroup":888,"shopIdList":[123456],"supplierIdList":[1234,100000000001,100000000002,100000000004]},"extendFields":{"@type":"java.util.HashMap"},"invokeInfoDTO":{"appName":"uop-portal","operatorId":"1110","operatorName":"account_ANXRKY8NfqFjXvQ"},"orderQueryDTO":{"extendFields":{"@type":"java.util.HashMap"},"logisTypeList":[0,1],"pageSize":20,"pageStart":1},"routeRuleParam":{"@type":"java.util.HashMap","bizGroup":199000},"rule":{"$ref":"$.routeRuleParam"}}||{"@type":"com.alibaba.common.model.ResultDTO","idempotent":false,"needRetry":false,"result":{"@type":"com.alibaba.common.model.queryorderstatus.QueryOrderStatusResp","extendFields":{"@type":"java.util.HashMap"}},"success":true}||堆栈倒打本文的重点来啦,这个设计就是结尾提到的奇思妙想。堆栈倒打源于我在排查另一个零碎问题过程中感触到的几个痛点,首先来看一个堆栈示例。 这么长的堆栈,这稀稀拉拉的字母,即便是天天跟它打交道的开发,置信第一眼看上去也会头皮发麻。回忆一下咱们看堆栈,真正想得到的是什么信息。所以我感触到的痛点外围有两个。第一个是,SLS(阿里云日志产品零碎)上搜进去的日志,默认是折叠的。对于堆栈,咱们应该都晓得,传统异样堆栈的特色是,最顶层的异样,是最靠近流量入口的异样,这种异样咱们个别状况下不太关怀。最底层的异样,才是引起系列谬误的源头,咱们日常排查问题的时候,往往最关怀的是谬误源头。所以对于堆栈日志,咱们无奈通过摘要一眼看出问题出在哪行代码,必须点开,拉到最上面,看最初一个堆栈能力确定源头。我写了一个谬误示例来阐明这个问题。惯例的堆栈构造其实分两局部,我称之为,异样起因栈,和谬误堆栈。 如上,一个堆栈蕴含有三组异样,每一个RuntimeException是一个异样,这三个异样连起来,咱们称为一个异样起因栈。每一个RuntimeException外部的堆栈,咱们称为谬误堆栈。阐明一下,这两个名词是我杜撰的,没有看到有人对二者做辨别,咱们个别都统称为堆栈。读者能了解我想表白的就行,不必太纠结名词。第二个痛点是,这种堆栈存储老本太高,无效信息承载率很低。诚实说这一点可能大多数一线开发并没有太强烈的体感,但在这个降本增效的大环境下,咱们每个人应该把这点作为本人的OKR去践行,变被动为被动,否则在机器老本和人力老本之间,公司只好做选择题了。当初指标很明确了,那咱们就开始隔靴搔痒。外围思路有两个。针对堆栈折叠的问题,采纳堆栈倒打。倒打之后,最底层的异样放在了最下面,甚至不必点开,瞟一眼就能晓得起因。 同时咱们也反对异样起因栈层数配置化,以及谬误堆栈的层数配置化。解这个问题,实质上就是这样一个简略的算法题:倒序打印堆栈的最初N个元素。外围代码如下。 /** * 递归逆向打印堆栈及cause(即从最底层的异样开始往上打) * @param t 原始异样 * @param causeDepth 须要递归打印的cause的最大深度 * @param counter 以后打印的cause的深度计数器(这里必须用援用类型,如果用根本数据类型,你对计数器的批改只能对以后栈帧可见,然而这个计数器,又必须在所有栈帧中可见,所以只能用援用类型) * @param stackDepth 每一个异样栈的打印深度 * @param sb 字符串结构器 */public static void recursiveReversePrintStackCause(Throwable t, int causeDepth, ForwardCounter counter, int stackDepth, StringBuilder sb){ if(t == null){ return; } if (t.getCause() != null){ recursiveReversePrintStackCause(t.getCause(), causeDepth, counter, stackDepth, sb); } if(counter.i++ < causeDepth){ doPrintStack(t, stackDepth, sb); }}要升高存储老本,同时也要确保信息不失真,咱们思考对堆栈行下手,把全限定类名简化为类名全打,包门路只打第一个字母,行号保留。如:c.a.u.m.s.LogAspect#log:88。外围代码如下。 ...

September 21, 2023 · 1 min · jiezi

关于日志:日志开源组件六Adaptive-Sampling-自适应采样

业务背景有时候日志的信息比拟多,怎么样才能够让零碎做到自适应采样呢? 拓展浏览日志开源组件(一)java 注解联合 spring aop 实现主动输入日志 日志开源组件(二)java 注解联合 spring aop 实现日志traceId惟一标识 日志开源组件(三)java 注解联合 spring aop 主动输入日志新增拦截器与过滤器 日志开源组件(四)如何动静批改 spring aop 切面信息?让主动日志输入框架更好用 日志开源组件(五)如何将 dubbo filter 拦截器原理使用到日志拦截器中? 自适应采样是什么?系统生成的日志能够蕴含大量信息,包含谬误、正告、性能指标等,但在理论利用中,解决和剖析所有的日志数据可能会对系统性能和资源产生累赘。 自适应采样在这种状况下发挥作用,它可能依据以后零碎状态和日志信息的重要性,智能地决定哪些日志须要被采样记录,从而无效地治理和剖析日志数据。 采样的必要性日志采样系统会给业务零碎额定减少耗费,很多零碎在接入的时候会比拟排挤。 给他们一个百分比的抉择,或者是一个不错的开始,而后依据理论须要抉择适合的比例。 自适应采样是一个对用户通明,同时又十分优雅的计划。 如何通过 java 实现自适应采样?接口定义首先咱们定义一个接口,返回 boolean。 依据是否为 true 来决定是否输入日志。 /** * 采样条件 * @author binbin.hou * @since 0.5.0 */public interface IAutoLogSampleCondition { /** * 条件 * * @param context 上下文 * @return 后果 * @since 0.5.0 */ boolean sampleCondition(IAutoLogContext context);}百分比概率采样咱们先实现一个简略的概率采样。 0-100 的值,让用户指定,依照百分比决定是否采样。 ...

August 28, 2023 · 4 min · jiezi

关于日志:基于-log4j2-插件实现统一日志脱敏性能远超正则替换

前言金融用户敏感数据如何优雅地实现脱敏? 日志脱敏之后,无奈依据信息疾速定位怎么办? 通过了这两篇文章之后,咱们对日志脱敏应该有了肯定的了解。 然而理论我的项目中,咱们遇到的状况往往更加简单: 1)我的项目的 java bean 定义不标准,大量接口应用 map。 2)历史我的项目泛滥,革新老本微小。 种种原因,导致应用注解的形式消耗大量的工夫。然而个别给咱们革新的工夫是无限的。 那么,有没有一种办法能够对立对敏感信息进行脱敏解决呢? 答案是有的,咱们能够基于 log4j2 实现本人的脱敏策略,对立实现日志的脱敏。 log4j2 Rewrite咱们能够基于 log4j2 RewritePolicy 对立应用脱敏策略。 本我的项目自 V1.2.0 增加对应反对,后续将晋升对应的可拓展性。 阐明:如果应用 slf4j 接口,实现为 log4j2 时也是反对的。 应用入门maven 引入引入外围脱敏包。 <dependency> <groupId>com.github.houbb</groupId> <artifactId>sensitive-log4j2</artifactId> <version>1.2.1</version></dependency>其余的个别我的项目中也有,如 log4j2 包: <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>${log4j2.version}</version></dependency><dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>${log4j2.version}</version></dependency>log4j2.xml 配置例子如下: <?xml version="1.0" encoding="UTF-8"?><Configuration status="WARN" packages = "com.github.houbb.sensitive.log4j2.rewrite"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> </Console> <Rewrite name="rewrite"> <AppenderRef ref="Console"/> <SensitiveRewritePolicy/> </Rewrite> </Appenders> <Loggers> <Root level="DEBUG"> <AppenderRef ref="rewrite" /> </Root> </Loggers></Configuration>几个步骤: 指定 package 为 packages = "com.github.houbb.sensitive.log4j2.rewrite"依照 log4j2 Rewrite 标准,指定重写策略为 SensitiveRewritePolicy输入时,间接指定为对应的重写之后的后果 <AppenderRef ref="rewrite" />测试失常的日志打印: ...

June 5, 2023 · 1 min · jiezi

关于日志:日志脱敏之后无法根据信息快速定位怎么办

日志脱敏之殇小明同学在一家金融公司下班,为了满足安全监管要求,最近天天忙着做日志脱敏。 无意间看到了一篇文章金融用户敏感数据如何优雅地实现脱敏? 感觉写的不错,用起来也很不便。 不过日志脱敏之后,新的问题就诞生了:日志脱敏之后,很多问题无奈定位。 比方身份证号日志中看到的是 3****************8,业务方给一个身份证号也没法查日志。这可怎么办? 平安与数据唯一性相似于数据库中敏感信息的存储,个别都会有一个哈希值,用来定位数据信息,同时保障平安。 那么日志中是否也能够应用相似的形式呢? 说干就干,小明在开源我的项目 sensitive 根底上,增加了对应的哈希实现。 应用入门开源地址https://github.com/houbb/sensitive应用形式1)maven 引入 <dependency> <groupId>com.github.houbb</groupId> <artifactId>sensitive-core</artifactId> <version>1.1.0</version></dependency>2)疏导类指定 SensitiveBs.newInstance().hash(Hashes.md5())将哈希策略指定为 md5 3)功能测试 final SensitiveBs sensitiveBs = SensitiveBs.newInstance() .hash(Hashes.md5());User sensitiveUser = sensitiveBs.desCopy(user);String sensitiveJson = sensitiveBs.desJson(user);Assert.assertEquals(sensitiveStr, sensitiveUser.toString());Assert.assertEquals(originalStr, user.toString());Assert.assertEquals(expectJson, sensitiveJson);能够把如下的对象 User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}间接脱敏为: User{username='脱**|00871641C1724BB717DD01E7E5F7D98A', idCard='123456**********34|1421E4C0F5BF57D3CC557CFC3D667C4E', password='null', email='12******.com|6EAA6A25C8D832B63429C1BEF149109C', phone='1888****888|5425DE6EC14A0722EC09A6C2E72AAE18'}这样就能够通过明文,获取对应的哈希值,而后搜寻日志了。 新的问题不过小明还是感觉不是很称心,因为有很多零碎是曾经存在的。 如果全副用注解的形式实现,就会很麻烦,也很难推动。 应该怎么实现呢? 小伙伴们有什么好的思路?欢送评论区留言

June 1, 2023 · 1 min · jiezi

关于日志:日志服务SLS助力Hago积极开辟社交泛娱乐出海征程

2022年人均挪动设施应用时长近5小时,其中社交类利用占据了约70%。随着市场需求与用户规模的持续增长,社交泛娱乐产业成为新蓝海,越来越多的翻新场景利用应运而生:社交+直播,社交+Avatar,社交+游戏...通过模式丰盛的社交类利用,人们所取得的不再仅限于娱乐消遣,还有逾越时空的连贯、归属感与满足感。 随着国内市场的日趋成熟与饱和,为了能取得更大的市场,局部娱乐行业公司抉择了出海这条路,其业务也由区域化逐步向全球化转变。 因而,本来以“跟着产品走”的方针,须要转变为“就地取材”的策略。针对不同地区的市场,须要鉴参考当地用户特点,在内容和推广素材上做出差异化。 残缺内容请点击下方链接查看: https://developer.aliyun.com/article/1178520?utm_content=g_10... 版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。

May 15, 2023 · 1 min · jiezi

关于日志:iLogtail-开源之路

2022年6月底,阿里云iLogtail代码残缺开源,正式公布了残缺性能的iLogtail社区版。iLogtail作为阿里云SLS官网标配的采集器,多年以来始终稳固服务阿里团体、蚂蚁团体以及泛滥私有云上的企业客户,目前曾经有千万级的装置量,每天采集数十PB的可观测数据,广泛应用于线上监控、问题剖析/定位、经营剖析、平安剖析等多种场景。此次残缺开源,iLogtail社区版首次在内核能力上与企业版齐全对齐,开发者能够构建出与企业版性能相当的iLogtail云原生可观测性数据采集器。 iLogtail的外围定位是可观测数据的采集器,帮忙开发者构建对立的数据采集层,助力可观测平台打造各种下层的利用场景。iLogtail一贯秉承凋谢共建的准则,欢送任何模式的社区探讨交换及公建。 可观测性探讨生存中的可观测 可观测性指的是从零碎的内部输入推断及掂量零碎外部状态。在咱们生存当中也会遇到很多可观测的例子。汽车仪表盘就是一个很典型的可观测例子,在驾驶汽车过程中,特地须要高度重视就是行驶平安问题。而汽车仪表盘升高了辨认汽车外部状态的门槛,即便非汽车工程业余人员也能通过仪表盘疾速辨认汽车的外部状态。 另外,咱们平时的看病能够认为是人体可观测的例子。在现代,医疗程度比较落后,整体来说人体是一个黑盒,只能通过外表的望闻问切来诊断病因,然而这种形式适度的依赖医生的教训、不足无力的数据撑持。而到了近代,随着心电图、X光等医疗设施的倒退,人体的外部机制变得越来越通明,大幅晋升了医疗程度,给人们的身体健康带来了福音。通过上述的例子咱们能够看到,可观测性不仅要能定性地反馈系统外部状态,最重要的是要定量的论证零碎外部状态,须要有足够的数据根据,也就是咱们提到的可观测数据的品质和准确性。 时机与挑战 回到咱们软件行业,通过几十年的飞速发展,整个开发模式、零碎架构、部署模式、基础设施等也都通过了几次颠覆性的改革,这些改革带来了更快的开发和部署效率,但随之而来整个的零碎也更加的简单、开发所依赖人和部门也更多、部署模式和运行环境也更加动静和不确定,这也对可观测数据采集提出了更高的要求。首先须要适应开发模式疾速迭代的需要,须要可能与DevOps流程等进行高度的集成,通过轻量级、自动化集成的形式实现开发、测试、运维的一体化;也须要适应部署架构分布式、容器化的需要,晋升业务服务动静、及时、精确发现的能力;最初,云原生的倒退也带来了更多的上下游依赖,因而也须要适应数据起源、数据类型越来越多的需要。 可观测性的数据根底 Logs、Traces、Metrics作为可观测性数据的三大支柱,根本能够满足各类监控、告警、剖析、问题排查等需要。这里大抵剖析下这三类数据的特点、转化形式以及实用场景: Logs:作为软件运行状态的载体,通过日志能够具体解释零碎运行状态及还原业务解决的过程。常见日志类型包含运行日志、拜访日志、交易日志、内核日志、满日志、谬误日志等。Metrics:是指对系统中某一类信息的统计聚合,绝对比拟离散。个别有name、labels、time、values组成,Metrics数据量个别很小,绝对老本更低,查问的速度比拟快。Traces:是最规范的调用日志,除了定义了调用的父子关系外(个别通过TraceID、SpanID、ParentSpanID),个别还会定义操作的服务、办法、属性、状态、耗时等详细信息。三者间的转换关系:Logs在调用链场景结构化后其实能够转变为Trace,在进行聚合、降采样操作后会变成Metrics。 开源计划探讨 目前行业上支流的可观测开源计划,大略能够分为5个局部。 采集端:承载可观测数据采集及一部分前置的数据处理性能。随着云原生的倒退,采集端也须要适应时代潮流,提供对K8s采集的敌对反对。常见的采集端有Filebeat、FluentD/Fluent-bIt,以及咱们开源的iLogtail。音讯队列:采集Agent往往不会间接将采集到的数据发送到存储系统,而是写入音讯队列,起到削峰填谷的作用,防止流量洪峰导致存储系统宕机。常见音讯队列为Kafka、RabbitMQ等。计算:用于生产音讯队列中的数据,通过解决、聚合后输入到存储系统。比拟常见的为Flink、Logstash等。存储剖析引擎:提供采集数据长久化存储能力,并提供查问剖析能力。常见的存储剖析引擎为Elasticsearch、ClickHouse及Loki。可视化:借助Kibana和Grafana提供采集数据的可视化能力。另外,日志服务SLS作为一款云原生观测与剖析平台,为Log、Metric、Trace等数据提供大规模、低成本、实时的平台化服务。SLS一站式提供数据采集、加工、查问与剖析、可视化、告警、生产与投递等性能,用户能够基于SLS疾速构建一套残缺的可观测平台。iLogtail企业版作为SLS官网标配的采集器,承载了业务数据采集的职责,而iLogtail社区版正是从企业版倒退而来的,性能及性能天然也继承了企业版的绝大部分能力。 iLogtail倒退历程 iLogtail的前身源自阿里云的神农我的项目,自从2013年正式孵化以来,iLogtail始终在一直演进。 诞生初期,面对阿里云本身和晚期客户运维和可观测性需求,iLogtail次要解决的是从单机、小规模集群到大规模的运维监控挑战,此时的iLogtail曾经具备了根本的文件发现和轮转解决能力,能够实现日志、监控实时采集,抓取毫秒级提早,单核解决能力约为10M/s。通过Web前端可反对中心化配置文件主动下发,反对3W+部署规模,上千采集配置项,实现日10TB数据的高效采集。 2015年,阿里巴巴开始推动团体和蚂蚁金服业务上云,面对近千个团队、数百万终端、以及双11、双12等超大流量数据采集的挑战,iLogtail在性能、性能、稳定性和多租户反对方面都须要进行微小的改良。至2017年前后,iLogtail曾经具备了正则、分隔符、JSON等多个格局日志的解析能力,反对多种日志编码方式,反对数据过滤、脱敏等高级解决能力,单核解决能力极简模式下晋升到100M/s,正则、分隔符、JSON等形式20M/s+。采集可靠性方面,减少文件发现Polling形式兜底、轮转队列程序保障、日志清理失落爱护、CheckPoint加强;过程可靠性方面,减少异样主动复原、Crash主动上报、守护过程等。通过全流程多租户隔离、多级高下水位队列、配置级/过程级流量管制、长期降级等机制,反对百万+部署规模,千级别租户,10万+采集配置项,实现日PB级数据的稳固采集。 随着阿里推动外围业务全面上云,以及iLogtail所属日志服务(SLS)正式在阿里云上商业化,iLogtail开始全面拥抱云原生。面对多元的云上环境、迅速倒退的开源生态和大量涌入的行业客户需要,iLogtail的倒退的重心转移到解决如何适应云原生、如何兼容开源协定和如何去解决碎片化需要等问题上。2018年iLogtail正式反对docker容器采集,2019年反对containerd容器采集,2020年全面降级Metric采集, 2021年减少Trace反对。通过全面反对容器化、K8S Operator管控和可扩大插件零碎,iLogtail反对千万部署规模,数万内外部客户,百万+采集配置项,实现日数十PB数据的稳固采集。2021年11月iLogtail迈出了开源的第一步,将Golang插件代码开源。自开源以来,吸引了数百名开发者的关注,并且也有不少开发者奉献了processor跟flusher插件。2022年6月C++外围代码也正式开源了,自此开发者能够基于该版本构建残缺的云原生可观测数据采集计划。 iLogtail外围劣势 外围劣势 -- 轻量、高效、稳固、牢靠轻量可观测数据采集Agent作为一个基础设施,往往须要每台机器都有部署,比方目前阿里外部有数百万的装机量,每天会产生几十PB的可观测数据。因而不论是内存还是CPU的一点点节俭,都能带来比拟大的老本收益。特地是,K8s Sidecar模式对于内存的要求更加刻薄,因为iLogtail与业务容器独特部署,iLogtail部署量会随业务规模扩充而增长。 从设计初期,咱们就比拟器重iLogtail的资源占用问题,抉择了主体局部C++、插件局部Golang实现,并且通过一些技术细节(详见下文)的优化,使得内存还是CPU绝对于同类Agent都有较大的劣势。 高效采集对于日志采集,比拟常见的伎俩是轮询机制,这是一种被动探测的收集形式,通过定期检测日志文件有无更新来触发日志采集;相应的也存在一种被动监听的事件模式,依赖于操作系统的事件告诉(对操作系统有肯定的要求),常见的事件告诉机制是Linux 2.6.13内核版本引入的inotify机制。轮询绝对事件告诉的实现复杂度要低很多、人造反对跨平台而且对于零碎限制性不高;但轮询的采集提早以及资源耗费较高,而且在文件规模较大时轮询一次的工夫较长,比拟容易产生采集提早。 为了同时兼顾采集效率以及跨平台的反对,iLogtail采纳了轮询(polling)与事件(inotify)并存的模式进行日志采集,既借助了inotify的低提早与低性能耗费的特点,也通过轮询的形式兼顾了运行环境的全面性。 iLogtail外部以事件的形式触发日志读取行为。其中,polling和inotify作为两个独立模块,别离将各自产生的Create/Modify/Delete事件,存入Polling Event Queue和Inotify Event Queue中。轮询模块由DirFilePolling和ModifyPolling两个线程组成,DirFilePolling负责依据用户配置定期遍历文件夹,将合乎日志采集配置的文件退出到modify cache中;ModifyPolling负责定期扫描modify cache中文件状态,比照上一次状态(Dev、Inode、Modify Time、Size),若发现更新则生成modify event。inotify属于事件监听形式,依据用户配置监听对应的目录以及子目录,当监听目录存在变动,内核会产生相应的告诉事件。由Event Handler线程负责将两个事件队列合并到外部的Event Queue中,并解决相应的Create/Modify/Delete事件,进而进行理论的日志采集。此外,咱们也通过一些技术手段,保障了polling、inotify两种机制的高效配合,整体近一步晋升了iLogtail运行效率。 事件合并:为防止轮询事件和inotify事件屡次触发事件处理行为,iLogtail在事件处理之前将反复的轮询/inotify事件进行合并,缩小有效的事件处理行为;轮询主动降级:如果在零碎反对且资源足够的场景下,inotify无论从提早和性能耗费都要优于轮询,因而当某个目录inotify能够失常工作时,则该目录的轮询进行主动降级,轮询距离大幅升高到对CPU根本无影响的水平。日志程序采集日志程序性采集是日志采集须要提供的基本功能,也是一个采集的难点,须要思考如下场景: 适应不同的日志轮转(rotate)机制:日志轮转是指当日志满足肯定条件(工夫或文件大小)时,须要进行重命名、压缩等操作,之后创立新的日志文件持续写入。另外,不同应用日志库轮转文件的格局不尽相同,有的工夫结尾,有的数字结尾等。适应不同的采集门路配置形式:优良的日志采集agent并不应该强制限度用户的配置形式,尤其在指定日志采集文件名时,须要适应不同用户的配置习惯。不论是精准门路匹配,还是含糊匹配,例如.log或.log*,都不能呈现日志轮转时多收集或者少收集的状况。为了实现日志文件的程序采集,首先须要定义好文件的惟一标识。咱们晓得在文件系统中,能够通过dev+inode的组合惟一标识一个文件。文件的move操作尽管能够扭转文件名,但并不波及文件的删除创立,dev+inode并不会变动,因而通过dev+inode能够十分不便的判断一个文件是否产生了轮转。然而dev+inode只能保障同一时刻文件的唯一性,当波及文件疾速删除创立的时候,前后两个文件的dev+inode很可能是雷同的。因而纯正通过dev+inode判断轮转并不可行,iLogtail引入了文件签名(signature)的概念,应用日志文件的前1024字节的hash作为文件的signature,只有当dev+inode+signature统一的状况下才会认为该文件是轮转的文件。此外,iLogtail外部也引入了文件轮转队列,保障了文件的程序采集。 采集可靠性iLogtail作为一个可观测数据根底采集组件,除了资源、性能外,可靠性也是一项要害指标。对于一些异样场景,iLogtail也有充沛的设计思考,保障了在网络异样、流量突增、过程重启等场景下仍然可能实现失常的采集工作。 日志解决阻塞问题形容:iLogtail须要大量部署在不同的业务机器上,运行环境是复杂多变的,利用日志burst写入、网络暂时性阻塞、服务端Quota有余、CPU/磁盘负载较低等状况在劫难逃,当这些状况产生时,很容易造成日志采集进度落后于日志产生进度,此时,iLogtail须要在正当的资源限度下尽可能保留住这些日志,期待网络复原或零碎负载下降时将这些日志采集到服务器,并且保障日志采集程序不会因为采集阻塞而凌乱。 解决思路: iLogtail外部通过放弃轮转日志file descriptor的关上状态来避免日志采集阻塞时未采集实现的日志文件被file system回收(在文件轮转队列中的file descriptor始终放弃关上状态,保障文件援用计数至多为1)。同时,通过文件轮转队列的程序读取保障日志采集程序与日志产生程序统一。若日志采集进度长时间继续落后于日志产生进度,齐全的不回收机制,则很有可能呈现文件轮转队列会有限增长的状况,进而导致磁盘被写爆,因而iLogtail外部对于文件轮转队列设置了下限,当size超过下限时禁止后续Reader的创立,只有这种继续的极其状况呈现时,才会呈现丢日志采集的状况。当然,在问题被放大之前,iLogtail也会通过报警的形式,告诉用户及时染指修复问题。 采集配置更新/过程降级问题形容:配置更新或进行降级时须要中断采集并从新初始化采集上下文,iLogtail须要保障在配置更新/过程降级时,即便日志产生轮转也不会失落日志。 解决思路: 为保障配置更新/降级过程中日志数据不失落,在iLogtail在配置从新加载前或过程被动退出前,会将以后所有采集的状态保留到本地的checkpoint文件中;当新配置利用/过程启动后,会加载上一次保留的checkpoint,并通过checkpoint复原之前的采集状态。然而在老版本checkpoint保留结束到新版本采集Reader创立实现的时间段内,很有可能呈现日志轮转的状况,因而新版本在加载checkpoint时,会查看对应checkpoint的文件名、dev+inode有无变动。若文件名与dev+inode未变且signature未变,则间接依据该checkpoint创立Reader即可。若文件名与dev+inode变动则从当前目录查找对应的dev+inode,若查找到则比照signature是否变动。若signature未变则认为是文件轮转,依据新文件名创立Reader;若signature变动则认为是该文件被删除后从新创立,疏忽该checkpoint。过程crash、宕机等异常情况问题形容:在过程crash或宕机时,iLogtail须要提供容错机制,不丢数据,尽可能的少反复采集。解决思路:过程crash或宕机没有退出前记录checkpoint的机会,因而iLogtail还会定期将采集进度dump到本地:除了恢复正常日志文件状态外,还会查找轮转后的日志,尽可能升高日志失落危险。外围劣势 -- 性能及隔离性 无锁化设计及工夫片调度业界支流的Agent对于每个配置会调配独立的线程/go runtime来进行数据读取,而iLogtail数据的读取只配置了一个线程,次要起因是: 数据读取的瓶颈并不在于计算而是磁盘,单线程足以实现所有配置的事件处理以及数据读取。单线程的另一个劣势是能够使事件处理和数据读取在无锁环境下运行,绝对多线程解决性价比较高。iLogtail数据读取线程可实现每秒200MB以上的数据读取(SSD速率能够更高)。但单线程的状况下会存在多个配置间资源分配不均的问题,如果应用简略的FCFS( First Come First Serve)形式,一旦一个写入速度极高的文件占据了处理单元,它就始终运行上来,直到该文件被解决实现并被动开释资源,此形式很有可能造成其余采集的文件被饿死。因而咱们采纳了基于工夫片的采集调度计划,使各个配置间尽可能偏心的调度,避免采集文件饿死的景象产生。iLogtail将Polling以及Inotify事件合并到无锁的事件队列中,每个文件的批改事件会触发日志读取。每次读取从上次记录的偏移地位开始,并尝试在固定的工夫片内将文读取到EOF处。如果工夫片内读取结束,则示意事件处理实现,删除事件;否则,将事件从新Push到队列尾部,期待下一次调用。 通过以上设计,保障了iLogtail能够高性能的进行数据采集。比照数据能够详见:https://developer.aliyun.com/article/850614 多租户隔离基于工夫片的采集调度保障了各个配置的日志在数据读取时失去偏心的调度,满足了多租户隔离中根本的公平性,但对于隔离性并未起到帮忙作用。例如当局部采集配置因解决简单或网络异样等起因阻塞时,阻塞配置仍然会进行解决,最终会导致队列达到下限而阻塞数据读取线程,影响其余失常配置。为此咱们设计了一套多级高下水位反馈队列用以在不同采集配置间实现读取、解析、发送各个步骤的无效的协调,为了更好的实现不同配置间隔离性,所以iLogtail会为每个配置创立一组队列。 多级:iLogtail从采集到输入总体要通过读取->解析->发送三个步骤,iLogtail在相邻步骤间别离设置一个队列。高下水位: 每个队列设置高下两个水位,高水位时进行非紧急数据写入,只有复原到低水位时才容许再次写入。反馈: 在筹备读取以后队列数据时会同步查看下一级队列状态,下一级队列高水位则跳过读取;以后队列从高水位生产到低水位时,异步告诉关联的前一级队列。极其场景解决对于一些阻塞场景的可靠性也须要思考,次要包含事件处理、数据读取逻辑以及数据发送管制三个方面: 事件处理与数据读取无关,即便读取关联的队列满也照常解决,这里的解决次要是更新文件meta、将轮转文件放入轮转队列,可保障在配置阻塞的状况下,即便文件轮转也不会失落数据。当配置关联的解析队列满时,如果将事件从新放回队列尾,则会造成较多的有效调度,使CPU空转。因而iLogtail在遇到解析队列满时,将该事件放到一个专门的blocked队列中,当解析队列异步反馈时从新将blocked队列中的数据放回事件队列。Sender中每个配置的队列关联一个SenderInfo,SenderInfo中记录该配置以后网络是否失常、Quota是否失常以及最大容许的发送速率。每次Sender会依据SenderInfo中的状从队列中取数据,这里包含:网络失败重试、Quota超限重试、状态更新、流控等逻辑。外围劣势 -- 插件化扩大能力 ...

January 10, 2023 · 1 min · jiezi

关于日志:图解-Fluent-Bit-内部设计

图解 Fluent Bit 外部设计本文摘录自我的开源书:《Mark’s DevOps 雜碎》中的 图解 Fluent Bit 外部设计。如果你看到图片不清晰,请转回原文。前言最近,因为工作须要,用老程序员在 羊之前最快的速度,通过文档、源码,学习了 Fluent Bit 的一些外围实现设计。正所谓,学得快,忘得更快。固作本笔记,以备未来学不动。 本文指标本文并<mark>不是要介绍 Fluent Bit</mark>。也<mark>不是学习如何应用它</mark>(当然,学习实现自身就是为更好地应用它的性能,同时更好地躲避它的问题。即:<mark>让 3PP的应用预期可控</mark>)。本文次要是初步简略钻研 Fluent Bit 外部实现后的总结与笔记。次要是从繁冗的文档与代码中,找到一点全局观、根底概念,以让学习的人不至于迷路其中细节。 本文是包含学习到的局部设计。 Fluent Bit 虽自称 lightweight,其实实现并不简略。 因为学习工夫无限,应用教训也不多,所以内容不免有错。应用请审慎! 对读者的假如假如读者曾经理解过 Fluent Bit。想看看其实现机理,以提前发现可能潜在的坑。 互动图片 本文的失常关上办法是,点击 “用 Draw.io 关上” 后,进入互动图片状态。图中很多元素提供链接到相干源码或文档。能够做穿插参考,是进一步深刻的入口,也是图可信性取证。本文的大部分内容是放在图中了。看图比文字更重要。什么是Fluent BitFluent Bit 官网 这样介绍本人: Fluent Bit 是一种超疾速、轻量级且高度可扩大的日志记录和指标处理器和转发器。它是云和容器化环境的首选。而在开源网站 Fluent Bit 是实用于 Linux、Windows、嵌入式 Linux、MacOS 和 BSD 系列操作系统的疾速日志处理器和转发器。它是 Graduated Fluentd 生态系统和 CNCF 子项目的一部分。 Fluent Bit 容许从不同起源收集日志事件或指标,对其进行解决并将它们传送到不同的后端,例如 Fluentd、Elasticsearch、Splunk、DataDog、Kafka、New Relic、Azure 服务、AWS 服务、Google 服务、NATS、InfluxDB 或任何自定义 HTTP 端点。概念上面只介绍本文用到的局部概念。 Record 概念能够简化认为,日志文件中的一行,就是一个 Record。外部以 json 树模式来记录一个 Record。 ...

December 17, 2022 · 5 min · jiezi

关于日志:美团高性能终端实时日志系统建设实践

你是否常常遇到线上须要日志排查问题但迟迟分割不上用户上报日志的状况?或者是否常常陷入因为存储空间有余而导致日志写不进去的囧境?本文介绍了美团是如何从0到1搭建高性能终端实时日志零碎,从此彻底解决日志失落和写满问题的。心愿能为大家带来一些帮忙和启发。1 背景1.1 Logan 简介Logan 是美团面向终端的对立日志服务,已反对挪动端App、Web、小程序、IoT等多端环境,具备日志采集、存储、上传、查问与剖析等能力,帮忙用户定位研发问题,晋升故障排查效率。同时,Logan 也是业内开源较早的大前端日志零碎,具备写入性能高、安全性高、日志防失落等长处。 1.2 Logan 工作流程为了不便读者更好地了解 Logan 零碎是如何工作的,下图是简化后的 Logan 零碎工作流程图。次要分为以下几个局部: 被动上报日志:终端设备在须要上报日志时,能够通过 HTTPS 接口被动上传日志到 Logan 接管服务,接管服务再把原始日志文件转存到对象存储平台。日志解密与解析:当研发人员想要查看被动上报的日志时会触发日志下载与解析流程,原始加密日志从对象存储平台下载胜利后进行解密、解析等操作,而后再投递到日志存储系统。日志查问与检索:日志平台反对对单设施所有日志进行日志类型、标签、过程、关键字、工夫等维度的筛选,同时也反对对一些特定类型的日志进行可视化展现。 1.3 为什么须要实时日志?如前文所述,这套“本地存储+被动上报”的模式尽管解决了大前端场景下根底的日志应用需要,然而随着业务复杂度的一直减少,用户对日志的要求也越来越高,以后 Logan 架构存在的问题也变得越来越突出,次要体现在以下几个方面: 局部场景上报日志受限:因为在 Web 与小程序上用户个别的应用场景是用完即走,当线上呈现问题时再分割用户被动上报日志,整个解决周期较长,有可能会错过最佳排查工夫。短少实时剖析和告警能力:以后短少实时剖析和告警的能力,用户曾多次提到过想要对线上异样日志进行监控,当有合乎规定的异样日志呈现时能收到告警信息。短少全链路追踪能力:以后多端的日志散落在各个系统中,研发人员在定位问题时须要手动去关联日志,操作起来很不不便,美团外部不足一个通用的全链路追踪计划。针对以上痛点问题,咱们提出了建设 Logan 实时日志,旨在提供对立的、高性能的实时日志服务,以解决美团现阶段不同业务零碎想要日志上云的需要。 1.4 Logan 实时日志是什么?Logan 实时日志是服务于挪动端 App、Web、小程序、IoT 等终端场景下的实时日志解决方案,旨在提供高扩展性、高性能、高可靠性的实时日志服务,包含日志采集、上传、加工、生产、投递、查问与剖析等能力。 2 设计实现2.1 整体架构 如上图所示,整体架构次要分为五个局部,它们别离是: 采集端:负责端上日志数据的采集、加密、压缩、聚合和上报等。接入层:负责提供日志上报接口,接管日志上报数据,并将数据转发到数据处理层。数据处理层:负责日志数据的解密、拆分、加工和荡涤等。数据消费层:负责日志数据的过滤、格式化、投递等。日志平台:负责日志数据查问与剖析、业务零碎接入配置、统计与告警等。2.2 采集端通用采集端架构,解决跨平台复用 采集端SDK用于端侧日志收集,须要在多种终端环境落地,然而因为端和平台较多、技术栈和运行环境也不统一,多端开发和保护老本会比拟高。因而,咱们设计了一套外围逻辑复用的通用采集端架构,具体的平台依赖代码则独自进行适配。咱们目前上线了微信、MMP、Web、MRN 端侧,逻辑层代码做到了齐全复用。采集端架构设计图如下: 重点模块介绍: 配置管理:采集端初始化实现后,首先启动配置管理模块,拉取和刷新配置信息,包含上报限流配置、指标采样率、性能开关等,反对对要害配置进行灰度公布。加密:所有记录的日志都应用 ECDH + AES 计划加密,确保日志信息不透露。Web 版加密模块应用浏览器原生加密 API 进行适配,可实现高性能异步加密,其余平台则应用纯 JS 实现。存储管理:线上数据表明,因为页面敞开导致的日志失落占比高达 1%,因而咱们设计了日志落盘性能,当日志上传失败后会先缓存在本地磁盘,等到页面下一次启动时再从新复原上传。队列治理:须要发送的日志首先进行分组聚合,如果期待分组过多则须要抛弃一部分申请,避免在弱网环境或者日志沉积太多时造成内存继续上涨而影响用户。落盘缓存 + 上报复原,避免日志失落 为了不便读者更好地了解端上日志采集过程,上面将具体介绍下采集端流程设计。当采集端初始化 API 开始调用时,先创立 Logger、Encryptor、Storage 等实例对象,并异步拉取环境配置文件。初始化实现之后,先查看是否有胜利落盘,然而上报失败的日志,如果有的话立刻开始复原上传流程。当失常调用写日志 API 时,原始日志被加密后退出以后上报组,等到有上报事件(工夫、条数、导航等)触发时,以后上报组内的所有日志被退出上报队列并开始上传。采集端具体流程图如下: 2.3 数据接入层对于实时日志零碎来讲,接入层须要满足以下几点要求:(1)反对公网上报域名;(2)反对高并发解决;(3)具备高实时性,提早是分钟级;(4)反对投递数据到 Kafka 音讯队列。 ...

November 7, 2022 · 1 min · jiezi

关于日志:动态调整日志级别思路实现

引言上篇文章 性能调优——小小的 log 大大的坑 已将具体的介绍了高并发下,不正确的应用日志姿态,可能会导致服务性能急剧下降问题。文末也给各位留下了解决方案——日志级别动静调整。本文将具体介绍“动静日志”的实现原理及源码,心愿各位能在今后的生产环境中应答日志问题能“得心应手”! 背景日志的重要性显而易见,是咱们排查问题,解决 BUG 的重要伎俩之一,然而在高并发环境下,又会存在悖论:大量打印日志,耗费 I/O,导致 CPU 占用率高;缩小日志,性能是下来了,然而排查问题的链路断掉了。 痛点:一方面须要借助日志可疾速排查问题,另一方面要兼顾性能,二者是否得兼?那么本文的动静日志调整实现就是为了能解决这个痛点所构思开发的。 性能个性低侵入,疾速接入:以二方包(jar)的模式染指,只须要配置启用,对业务无感及时响应,随调随改:应答研发不小心在大流量入口链路打印了大量 INFO 日志,能及时调整日志级别阶梯配置反对:默认全局设置兜底,又能够反对部分 Logger 放/限流人性化操作:与操作界面,不便批改<!-- more --> 技术实现如下,我将以 log4j2 为实例作解说,其它日志实现大同小异,参照实现即可。如下是 log 染指的配置文件示例: <?xml version="1.0" encoding="UTF-8"?><configuration status="info"><Properties> // 全局参数信息</Properties><appenders> // appender 具体配置</appenders><loggers> // 配置 appender 指向</loggers></configuration>以往咱们调整我的项目的日志时,要么是删除代码中的废日志,要么是批改下面的 xml 配置,针对某个包下或者类作日志级别限度,再从新打包部署失效。此时的效率是非常低的,不符个咱们的诉求。那么如何实现动静调整呢,首先想到的是 xml 调整日志级别后是如何失效的?xml 自身就是一些配置信息, log 的实现类读取 xml 信息动静批改日志级别,有没有可能咱们在程序中间接去调用 log4j 外部的封装办法,绕过 xml 不就好了? 动静调整日志级别源码查看:具体源码我已放在 github dynamic-logger-util,可自行查看。顺着思路,查看 log4j 源码后,发现的确可行,如下即是调整日志办法的实现代码: // 获取日志上下文LoggerContext logContext = LoggerContext.getContext(false);Configuration configuration = logContext.getConfiguration();LoggerConfig loggerConfig = configuration.getRootLogger();loggerConfig.setLevel(level);// 失效logContext.updateLoggers();获取以后的 LoggerContext 后,再获取 configuration,以后的配置即是 xml 内的配置转换过去的,再获取 root logger, 即对应 xml 中的配置如下: ...

September 8, 2022 · 2 min · jiezi

关于日志:Java-日志框架slf4j

理解日志框架Java日志框架:slf4j作用及其实现原理Java日志零碎历史从入门到解体 常见的日志产品SLF4j框架通过扫描classpath上面的org/slf4j/impl/StaticLoggerBinder.class类来发现日志产品jar,个别用到的日志jar 有logback、log4j2。如下是是SLF4j框架对接各个日志产品的依赖。 slf4j-api 是SLF4j的框架APIlogback和slf4j-simple都是间接实现SLF4j框架的接口log4j 不是间接实现SLF4j接口,因而须要一个桥接jar,即slf4j-log4j12log4j2 和logj相似,也须要一个桥接的jar,即log4j-slf4j-impl,留神这个jar和log4j不一样。此外log4j2自身由两个jar组成,即log4j-api和log4j-core <dependencies> <!-- slf4j api--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <!-- slf4j logback implement--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <!-- slf4j simple implement--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.25</version> </dependency> <!-- slf4j adapter for log4j start--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- slf4j adapter for log4j end--> <!-- slf4j adapter for log4j2 start--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.11.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.12.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.12.1</version> </dependency> <!-- slf4j adapter for log4j2 end--> </dependencies>SLF4J如何进行SPISPI的全称是Service Provider Interface,是Java提供的可用于第三方实现和扩大的机制,通过该机制,咱们能够实现解耦,SPI接口方负责定义和提供默认实现,SPI调用方能够按需扩大。 ...

May 13, 2022 · 1 min · jiezi

关于日志:日志收集

一、日志简介1.1 什么是日志?日志也叫 Log。用来记录零碎在运行过程中,产生的信息(变量值、参数、响应后果、谬误、异样)。保留到 xxx.log 文件中1.2 日志的作用剖析代码,定位bug调试代码理解零碎运行的状态剖析用户行为,做数据统计1.3 日志的级别logging.DEBUG:调试级别【高】logging.INFO:信息级别【次高】logging.WARNING:正告级别【中】logging.ERROR:谬误级别【低】logging.CRITICAL:严重错误级别【极低】 个性:日志级别设定后,只有比该级别 低 的日志会打印如:设定日志级别为 info。 debug 级别的日志信息,不会打印。 warning、error、critical 会打印1.4 日志的应用步骤将 日志源码文件(logging_use.py),存入 common/ 目录在 我的项目 目录下创立 log目录, 用来存储日志文件。初始化 日志配置 文件名,日志文件 切分的工夫距离单位,单位个数,保留的日志文件数打印日志(logging.级别("输入的信息"))1.5 日志的应用# 1.将 logging_use.py 文件部署到我的项目中。 个别:拷贝至 common/ 下。# 2.在 我的项目下 创立 log/ 用来寄存生成的 xxx.log 日志文件#3.在 `__init__.py`文件内,初始化日志信息,指定 日志文件名【必填】、单位、单位个数、保留文件个数。#4.在 原须要print打印输出的地位,替换应用 logging.级别(“想要输入的信息”)

May 6, 2022 · 1 min · jiezi

关于日志:应云而生一文看懂端到端的可观测体系构建

2021年初,可观测性的概念在国内市场还鲜少有人提到,但到了2021年下半年,无关可观测性的研究和实际却开始如雨后春笋般层出不穷,出名公司 Grafana 甚至间接将原来的监控工具改成了可观测性技术栈并推了一系列服务。可观测性真的可能解决传统监控体系面临的诸多问题吗?又该如何构建可观测体系?本期,亚马逊云科技 Tech Talk 特地邀请到观测云 CEO 蒋烁淼带来分享《构建端到端的可观测体系最佳实际》。 可观测性为何忽然“火出圈”可观测性看似是个陈腐词,但其实它的起源远比咱们的认知要早得多。可观测性最早是匈牙利裔工程师鲁道夫·卡尔曼针对线性动静零碎提出的概念。若以信号流图来看,若所有的外部状态都能够输入到输入信号,此零碎即有可观测性。1948年伯特·维纳发表的著述《控制论-对于动物和机器中管制和通信的迷信》同样提到了可观测性。管制实践中的可观测性是指零碎能够由其内部输入推断其外部状态的水平。 随着云计算的倒退,可观测性的概念逐步走入计算机软件畛域。为什么近期可观测性的热度显著晋升了呢? 蒋烁淼认为,这很大水平是因为零碎复杂性的加强。IT 零碎的实质是一个数字化的零碎,过来,零碎自身构造简略,多为单体式架构,且基础设施绝对固定,能够通过监控去查看零碎。但随着云原生时代的到了,治理对象从繁多主机逐步变成云,起初又变成云原生的分布式简单零碎,传统的面向基础设施的监控、简略的日志和简略的 APM 没有方法解决问题,因而,须要构建零碎残缺的可观测性。 可观测性中应用的次要数据类是指标、日志、链路。它们通常被称为“可观测性的三大支柱”。 指标(Metric):指标是间断工夫下的零碎的值的记录,根底指标通常用于形容两种数据类型,一种是计数(Count),一种是计量(Gauge)。日志(Log):零碎/利用输入的工夫相干的记录,通常由零碎/软件开发人员输入,不便定位系统的谬误和状态。链路(Tracing):基于有向无环图构建的软件各个模块间接地调用关系。三大支柱至关重要,开发者正是通过这三个维度的数据来断定利用零碎的情况。和传统监控相比,可观测体系领有诸多劣势。 传统监控面向已知的问题,只能去发现和告诉那些已知可能会产生的故障,如:CPU>90%。次要监控对象是 IT 对象,仅面向服务端的组件,解决根底的运维问题。 而可观测性则可能帮助发现并定位未知的问题。其外围是一直收集零碎产生的各种外围指标与数据,通过数据分析的形式来保障和优化业务,如:发现小程序客户端在某个城市的领取失败率十分高,从而判断是否是代码层面上导致这样一个异样。可观测性次要监测的对象不仅仅是IT对象,还有利用和业务,面向云、分布式系统、APP/小程序。 在分享中蒋烁淼谈到,随着基础设施的倒退,传统监控将逐渐被可观测性所取代。 他将构建可观测性的价值总结为以下五点: 让 SLO 可视化,清晰的指标和现状发现与定位未知问题缩小团队间的廓清老本升高业务异样造成的无奈预知的经济损失晋升最终用户体验和满意度开源 or SaaS,可观测性构建正确的打开方式是?相比于传统监控体系,构建可观测性既然有诸多劣势和价值。那么该如何构建可观测性呢? 首先,须要尽可能地收集所有组件零碎的所有相干⾯的根底数据,包含云、主机、容器、Kubernetes 集群,应⽤和各种终端。实时收集这些数据的老本并不⾼,但如果没有收集,⼀旦系统故障须要排查剖析的时候,就⽆法无效评估过后的状态。 其次,要明确零碎可观测性构建的责任。谁是这个组件的构建者,谁负责定义这个组件的 SLI,谁负责收集所有的相干根底数据并构建相应的仪表盘以及谁为相干的组件的 SLO 负责,须要责任到人。 第三,开发者须要为可观测性负责。开发者要将⾃⼰开发零碎的可观测性数据裸露作为软件品质⼯程的⼀局部,如果说单元测试是为了保障最⼩单元代码的可⽤性,那么开发者标准化裸露可观测性根底数据也将作为⽣产系统可靠性的必要条件。 第四,须要建⽴统⼀的指标、⽇志、链路标准,统⼀团队的⼯具链。即采取雷同的指标命名标准,雷同的⽇志格局,雷同的链路零碎。如果在遵循 OpenTelemetry 规范后,仍有不同,则可定义串联整个零碎的统⼀TAG 标准,如:所有谬误都是 state:error。 第五,要继续优化改良整体可观测性。针对整个零碎的可观测,包含数据收集,视图构建,TAG 体系建⽴,这些步骤均须要工夫,不能因为覆盖度或者构建的仪表盘未能在某次事变中施展作⽤而持续⽤过来的⽅式解决问题。每次未被观测的故障都是进⼀步晋升可观测范畴的绝佳机会。 从可观测性构建的门路不难看出,其过程是非常复杂的。那么,支流的构建形式有哪些?蒋烁淼介绍了两种最为常见的可观测性构建形式,别离是通过开源的形式构建和采纳 SaaS 产品进行构建。 得益于开源生态的蓬勃发展,为可观测性的构建提供了诸多抉择。采纳开源的形式构建,须要构建者从前端的数据抓取到后端的数据处理,包含数据展现、告警等周边性能的相干常识有十分详尽的理解把握。因而,这种形式适宜于那些有足够实力或者学习老本及工夫老本绝对短缺的团队。 相比于开源的形式,采纳成熟的 SaaS 产品构建可观测性是一种更加高效的形式。蒋烁淼以观测云的产品为例,介绍了这种形式的四点劣势。 不做缝合怪:在服务器内仅装置一个 agent 就能够收集这台主机所有相干的零碎数据,防止成堆的 agent 和配置项。不做小白鼠:能提供端到端的残缺笼罩,并能做到开箱即用,防止参差不齐的集成,如:观测云就可能反对超过200种技术栈,实现端到端的笼罩。不关闭、高度可编程:可实现轻松构建任意的可观测场景,甚至将业务数据参数引入到整体的观测中,灵活性强。此外,还可能防止死板的集成,领有弱小的二次开发能力。不留隐患:察看云对用户侧代码永恒开源,单向通信,不会也不能向客户环境下发指令。所有的数据收集默认脱敏且用户可对整个过程进行管制。 后面提到,可观测性的构建是应“云”而生的,不仅如此,观测云自身也是完完全全的云原生产品。观测云中整套产品包含数据平台,都是部署在亚马逊云科技的 EKS 之上的,并基于容器进行编排。观测云的整体架构非常简单,即通过一个 agent 将海量数据进行对立,进入数据平台,而后通过平台的能力提供残缺的可观测性。整个零碎分为外围平台层、Web 层和数据接入层,外围平台层是齐全由观测云进行自研的,没有进行开源。下层的 Web 层,在外围数据处理平台上有一套与平台对接的 API。蒋烁淼说:“对于客户来说,更举荐间接抉择观测云的 SaaS 产品,如果违心,客户也能够在亚马逊上齐全孤立地进行部署,也是十分不便的,只不过整体费用要比间接采纳 SaaS 产品高得多。 为什么会抉择亚马逊云科技?次要是基于以下考量: 观测零碎自身要有高一个数量级的可靠性和更高的 SLA:观测云是帮忙客户构建可观测性零碎的平台,因而须要本身领有很高的可靠性,如果不能提供足够高的可靠性,一旦观测零碎呈现故障,便无奈及时揭示客户,提供具体的剖析更无从谈起。此外,抉择云服务自身也可能让一部分观测云平台的 SLA 由亚马逊来提供。更成熟的 Marketplace:用户可通过中国的团队间接在亚马逊上进行产品购买,亚马逊云科技会把产品生产间接在 Marketplace 上记账。须要阐明的是,观测云的产品是依据数据规模来付费的,当用户没有数据量的时候简直是收费的。全球性:亚马逊云科技可能提供比海内产品更好的兼容性,尤其对于中国的技术栈整体老本更低。蒋烁淼在分享中走漏:“在春节过后,观测云将会在海内亚马逊云科技节点部署咱们的观测平台。观测云心愿用中国力量为中国的出海客户提供比海内产品更好的、老本更低的抉择。”借力 APN 融入亚马逊云科技寰球网络:观测云心愿借助亚马逊云科技弱小的生态,将可观测性作为最终对客户提供服务的伎俩,并心愿可能借力APN,帮忙更多用户理解可观测性的成果,这个也是观测云抉择亚马逊科技十分重要的起因之一。除了是完完整整的云原生产品,在观测云的零碎中,还蕴含几个十分乏味的设计。首先,在采集侧: ...

January 24, 2022 · 1 min · jiezi

关于日志:三款日志管理工具横向对比Splunk-vs-Sumo-Logic-vs-Logstash

在生产环境记录利用的运行日志曾经成为常规,但日志须要通过解决和剖析才有意义,第三方日志管理工具的呈现正旨在解决这个问题。 软件剖析公司 Takipi 负责产品市场的 Josh Dreyfuss 今日撰文,比拟了三个有代表性的日志管理工具: Splunk 、 Sumo Logic 和 Logstash , 从性能、易用性、资源占用等方面剖析了它们的优缺点。 概述这篇文章别离从 on-premises、SaaS,以及开源三种模式选取一个有代表性的日志管理工具进行剖析, Splunk、Sumo Logic 和 Logstash 别离对应这三种不同的模式。 Splunk是日志管理工具行业一个比拟大的玩家,它专攻企业市场,并以 on-premises 模式运作。Splunk 领有最多功能和弱小的整合能力,但价格也最高。为了投合目前 ITOA 畛域的发展趋势,Splunk 也提供了 SaaS 版本,并且为 SMB 筹备的更轻量级的版本。不过本文将只剖析 Splunk 的 on-premises 业务所提供的服务。Sumo Logic最后试图成为 Splunk 的 SaaS 版本代替,不过他们走出了本人独有的倒退路线。到当初,它们曾经是市场上性能最丰盛的 SaaS 版日志管理工具之一,并且它们也专攻企业市场。Logstash是一款开源的工具,常常作为 ELK 技术栈的其中之一应用,另外是 ElasticSearch 和 Kibana。在 ELK 技术栈里,Logstash 承当的是日志解决,它创立一个集中化的管道来贮存、搜寻和剖析日志文件。它应用内建的过滤器和输入输出,以及一些插件来给日志治理提供弱小的性能。比拟装置:因为 on-premises、SaaS、开源模式的不同,三者的装置也各不相同,应用 Splunk 和 Logstash 你须要提供硬件和网络等基础设施,另外应用 Logstash 你通常还须要装置 ELK 外面的另外两个。性能:Splunk 的性能可能是市面上最丰盛的,你简直能够从它的 UI 或 API 调出你所须要的任何数据——前提是你的日志蕴含这些数据。并且它领有高可用性、可扩展性,以及很器重安全性,另外它还能解决很多类型的机器数据。Sumo Logic 则是性能最丰盛的日志治理 SaaS 软件之一,它领有很多和 Splunk 相似的性能,另外还有一项个性是设置工夫的触发揭示。Logstash 则因为其开源个性,领有最强的自定义能力,你能应用自定义的日志格局或者定制自定义插件。应用界面:Logstash 并不蕴含 UI,其前端局部由 ELK 中的 Kibana 实现,你也能够应用 Graphite、Librato 和 DataDog 来代替 Kibana。Splunk 提供一个内容丰盛且灵便的界面,可通过 XML 或拖拽来定制。Sumo Logic 的界面则专一于显示实时数据。集成与插件:抉择工具时一个重要的点是看它是否与你现有的工作流集成良好,对于日志管理工具的度量指标,则须要思考它是否能解决你已有的日志以及是否与你的环境同步。Splunk 有超过 600 个可用的插件,为它解决日志提供了强力的撑持。Sumo Logic 是为特定的大型工具设计,包含开发自动化工具、云平台、零碎平台,以及平安工具。Sumo Logic 笼罩了支流的工具,但对于较小的或比拟少见的工具则无能为力。Logstash 作为开源工具,它的插件始终在增长,目前它已有超过 160 个插件,大部分来自社区。价格:Splunk 的价格为每年 1800 到 60000 美元,Sumo Logic 提供一个精简的免费版,以及 60 美元 /G 的免费版本。Logstash 则为开源收费,不过你依然须要本人承当服务器和带宽费用。文档和社区:对于工具的抉择,文档的丰盛水平以及社区的沉闷水平也是重要的考量指标。Splunk 领有十分丰盛的文档,以及以在线论坛模式组织的社区。Logstash 的文档也不错,不过有些中央存在前后不统一。Sumo Logic 的文档则不够丰盛,社区也比拟令人困惑。总结 ...

December 8, 2021 · 1 min · jiezi

关于日志:如何将-winston-log-库记录的日志写入-mongo-DB-数据库

官网 Winston 非常适合配置不同的日志目的地。 在咱们的小应用程序中,让咱们创立另一个传输。 这次我想把日志保留到一个数据库中,MongoDB 简洁一些。 在 logger.js 文件上,复制以下代码块。 确保装置 Winston MongoDB,即 npm install winston-mongodb。 How to use MongoDB下载并装置 MongoDB 社区服务器。 导航到您的环境变量(对于 Windows 用户),在用户变量下,抉择门路 → 编辑 → 新建,增加 C:\Program Files\MongoDB\Server\4.4\bin(4.4 可能因您计算机上安装的 MongoDB 版本而异). 关上命令提示符并键入 mongo。 这将查看您是否已胜利装置 MongoDB。 MongoDB shell 版本将打印在您的终端上,这意味着您的装置胜利。 输出 use logs 创立数据库日志。 输出 db.createCollection("server_logs") 以创立 collection. 在 logger.js 里插入下列代码: const { createLogger, format, transports } = require('winston');// Import mongodbrequire('winston-mongodb');module.exports = createLogger({transports:[// File transport new transports.File({ filename: 'logs/server.log', format:format.combine( format.timestamp({format: 'MMM-DD-YYYY HH:mm:ss'}), format.align(), format.printf(info => `${info.level}: ${[info.timestamp]}: ${info.message}`),)}),// MongoDB transport new transports.MongoDB({ level: 'error', //mongo database connection link db : 'mongodb://localhost:27017/logs', options: { useUnifiedTopology: true }, // A collection to save json formatted logs collection: 'server_logs', format: format.combine( format.timestamp(), // Convert logs to a json format format.json()) })]});运行 node app.js 以启动服务器并拜访以下 URL 以触发服务器响应和申请。 ...

October 27, 2021 · 1 min · jiezi

关于日志:日志库-winston-的学习笔记-loggerinfo-打印到控制台上的实现原理

if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.combine( winston.format.colorize({ all: true }), winston.format.simple() )}));}上述代码的含意是,如果以后 Node.js 执行环境不是生产环境,则将 winston 的输入,打印到管制台上。 _stream_readable.js 抛出 data 事件: 读取事件处理函数: console 对应的 event handler: transformed: console 有专门对应的 transport 实现文件,位于 console.js 内: 从 data 字符串能看出在 console 打印黑白字符串的实现形式: 并没有像我设想的那样,执行到 82 行的 console.log console 对象的 _stdout 属性,在 internal 这个 constructor.js 里注入: 这个 writeUtf8String 函数,是原生 native API: 位于 Node.js 源代码 的 internal/stream_base_common.js 外部: ...

October 27, 2021 · 1 min · jiezi

关于日志:日志库-winston-的学习笔记-loggerinfo-的实现原理单步调试

依照这篇文章日志库 winston 的学习笔记 - 创立一个应用 winston 的 Node.js 利用里的代码,对下列办法进行单步调试: 因为咱们调用的是 info 办法,所以生成的日志,level 为 info: 第一个参数为 message,前面的都是 meta 信息:在 info 的实现代码里,首先判断传入 log 办法的参数个数: 如果参数个数为 0 或者 1,有专门的实现。否则,进入 self.log: 结构 info 对象: 其中 msg 变量存储的是用户调用 info 办法传入的第一个参数,meta 是传递的第二个参数。 最初调用外部的 write 办法,传入的 message,是两个参数的连贯。 encoding 是 utf8 chunk: write 外面先 read,而后再 _transform: format 咱们抉择的是 json format: json.js 负责把 info 对象序列化成 json 字符串: 后果: addChunk: emit: 三个 listeners: data listener: ...

October 27, 2021 · 1 min · jiezi

关于日志:日志库-winston-的学习笔记-创建一个使用-winston-的-Nodejs-应用

winston 被设计为一个简略且通用的日志库,反对多种传输。 传输实质上是日志的存储设备。 每个 winston 记录器都能够在不同级别配置多个存储渠道。例如,人们可能心愿将谬误日志存储在长久的近程地位(如数据库),但所有调试日志都输入到控制台或本地文件。 应用 winston 的举荐办法是创立您本人的记录器。 最简略的办法是应用 winston.createLogger: const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.json(), defaultMeta: { service: 'user-service' }, transports: [ // // - Write all logs with level `error` and below to `error.log` // - Write all logs with level `info` and below to `combined.log` // new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), ],}); //// If we're not in production then log to the `console` with the format:// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `//if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.simple(), }));}winston 的日志等级const levels = { error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6};如何创立 loggerconst logger = winston.createLogger({ transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'combined.log' }) ]});即便 logger 实例创立之后,也能容易地删除或者削减新的 transport: ...

October 27, 2021 · 2 min · jiezi

关于日志:日志日志链路追踪系统调研

须要思考的问题1.log要具备显示调用方文件名和行号的能力,要不然你连谁打的这个log都不晓得2.log要具备按申请聚合的能力,不然上下文全是乱的,没法看。光给你一行报错log你能剖析为啥出错?必须是这个申请的残缺log才有价值。3.在2的根底上要有按用户聚合的能力,不便查流水4.在3的根底上要有染色能力,指定用户能log全开,实时定位问题5.log能还原成fiddler抓包,重现现场,对于概率性问题保留现场再重要不过了。6.log要具备单机调试性能,能够不停机编写条件开启指定log,忽视日志级别。 分布式跟踪零碎-产品比照https://github.com/1046102779/opentracing/blob/master/%E5%88%86%E5%B8%83%E5%BC%8F%E8%B7%9F%E8%B8%AA%E7%B3%BB%E7%BB%9F%E2%80%94%E2%80%94%E4%BA%A7%E5%93%81%E5%AF%B9%E6%AF%94.md Jaegerhttps://pjw.io/articles/2018/...https://www.alibabacloud.com/... How to usehttps://github.com/jaegertrac... Documenthttps://www.jaegertracing.io/...阿里云jaeger:基于 Jeager 开发的分布式追踪零碎,反对将采集到的追踪数据长久化到日志服务中,并通过 Jaeger 的原生接口进行查问和展现 https://github.com/aliyun/ali...基于opentracing + jaeger 实现全链路追踪 https://www.jianshu.com/p/fbe...opentracing + jaeger node实际: [https://www.bookstack.cn/read...] PandoraWhy 1.x.xhttps://github.com/midwayjs/p...因为原定的配套sandbox在社区部署十分艰难,导致pandora这个货色无奈疾速部署,展示报表等,后续应该也很难在社区应用,倡议应用其余同类产品代替(pm2 等),咱们也会在其余中央标注。如上所说,当初 2.0 版本次要在团体内应用。当初文档是针对 Pandora.js 1.0 的,能够尝试装置 pandora@1.x.x;How to usenpm install pandora@1.x.x -gDocumentshttps://midwayjs.org/pandora/zh-cn/guide/introduce.html#%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99 TipsMidway npm源应用社区源midway启动自定义:https://midwayjs.org/midway/guide.html#%E6%A1%86%E6%9E%B6%E6%89%A9%E5%B1%95pandora启动配置:https://midwayjs.org/midway/guide.html#%E9%80%9A%E8%BF%87%E5%86%85%E7%BD%AE%E7%9A%84%E5%90%AF%E5%8A%A8%E6%96%87%E4%BB%B6启动参数配置:https://midwayjs.org/midway/guide.html#%E5%90%AF%E5%8A%A8%E5%8F%82%E6%95%B0%E4%BC%A0%E9%80%92npm run build ; pandora startTSWhttps://github.com/Tencent/TSW

October 11, 2021 · 1 min · jiezi

关于日志:日志分析常规操作

前言日志是开发者用来分析程序和排查问题的重要工具。随着零碎架构从晚期的单体利用,演变到现在的微服务架构,日志的重要性也逐渐晋升。除了用日志辅助问题排查,还能够通过日志对微服务申请的全链路进行性能剖析,甚至能够它用来解决分布式系统中的一致性问题。与此同时,零碎产生的日志量和日志治理难度也显著减少。于是,日志管理工具随之诞生并迭代降级。从最开始登录到跳板机上查看日志,到自建分布式日志核心来对立治理日志流,到云平台厂商提供专门的日志治理服务。开发者只须要在利用中接入SDK将日志回流到日志平台,就能够应用日志平台提供智能检索、数据分析以及链路剖析等能力,平台中易用的图形化界面和成熟的数据管理能力极大的晋升了开发效率。 然而,日志治理平台并不是万能的,总有一些场景它会缺席(如本地调试产生的日志并不会回流到日志平台,不反对简单的数据分析,当然还有最常见也是最令人解体的,数据失落了。。。),而咱们不得不和一大堆原始日志文件面面相觑。这时咱们就不得不从工具包中掏出原始的武器-linux指令,开始一顿操作猛如虎。 本文将联合本人在日常开发过程中遇到的场景给出对应的日志检索语句,也欢送大家将它珍藏到本人的工具包中,或是在下方留言本人遇到的日志剖析难题,博主会将其欠缺到文章中。 日志构造在理解到日志剖析语句之前,先简略介绍一下日志的类型和构造,后序将以这一节介绍的内容作为背景提供具体的日志剖析语句。 日志类型次要有两种,一种是业务日志,即实现业务性能的过程中产生的日志,通常是开发者在程序中被动埋点触发的。还有一种是系统日志,这一类日志范畴更大,底下还能够持续细分,如零碎所在的宿主机各项指标的快照,或者是依赖的中间件外部打印的日志等。 业务日志和系统日志通常在不同的目录下,事实上,不同类型的系统日志个别也会用独立的目录进行隔离。以一个接入了Mysql,RocketMq的零碎为例,它的日志目录构造可能如下所示: /log /mysql /rocketmq /app /app_name1 /app_name2可见,app下不同的业务零碎之间也会进行日志隔离,不便检索和查看。 接着看一下每个目录下日志文件的构造,这个往往没有相对的规范,开发者通常依照零碎的须要设计日志文件构造,甚至产生指定用处的日志(如mysql的bin log和relay log)。这里简略介绍一下我进行零碎开发时习惯的日志结构设计。 通常我至多会辨别出三个日志文件: application.log, application-error.log和application-warn.log。正如文件的名称所示,它们别离对应不同级别的日志,application.log中会蕴含利用生命周期中的全副日志,而application-error.log和application-warn.log日志则别离只记录error级别日志和warn级别日志,从而不便疾速定位系统异样。然而,如果你间接查看目录下的所有日志文件,会发现它不止有这三个文件. 这是因为零碎运行过程中会产生大量的日志,如果只用一个文件进行日志的存储,会导致文件变得极为宏大并重大耗费磁盘空间。因而,操作系统或是日志工具在通过配置后会执行日志截断,压缩和备份等操作,缩小日志对整个宿主机稳定性的影响。被截断后的日志会依据配置在日志名加上后缀并保留,通常是加上工夫戳。 除了上文所示的依据日志级别来划分多个日志文件,还能够从别的维度设计日志文件,比方将零碎流量的入口和进口别离打印日志。流量的入口能够了解为RPC接口Server端,HTTP服务Server端,MQ接管消息日志等,与之绝对的流量的进口是指RPC接口Client端,调用上游HTTP服务等。因而整个日志目录将会蕴含以下几个文件 application.logappilcation-error.logapplication-warn.logrpc-client.logrpc-server.logmq.log具体的日志配置不在本文的范畴内,大家能够自行浏览logback等日志框架的文档。 日志剖析小操作接下来将会列出在日常开发过程中常见的日志查问和剖析场景,并给出对应的指令。 查看日志查看单个日志文件cat是咱们最罕用的浏览文件的指令,通过cat ${filename}即可展现文件的内容。以application.log为例 cat application.log这个指令实用于查看所有可读文件。 查看多个日志文件上文提到,因为Logrotate机制的存在,日志文件往往会被截断成多个带有不同工夫戳后缀的文件,而咱们又不确定想要查问的日志具体在哪个文件中,这时候能够将多个文件都传给cat指令,cat ${filename} ${filename2} ${filename...},cat会一一读取文件并展现。然而如果文件数量十分大呢?幸好cat指令反对相似正则的匹配,*关键字容许咱们匹配任意多个字段。 cat application1.log application2.log application3.logcat application.log*当然,在文件数量很多的时候用cat指令查看全量日志曾经不是很好的抉择了,下文将会给出其它日志查询方法。 查看最初几行日志cat指令会将整个日志文件从头到尾读取并展现在控制台,然而有时咱们往往只须要看最近一段时间的日志即可。而且在日志文件特地大的时候,用cat指令不仅比较慢,而且会导致大量无关的日志充斥屏幕影响浏览。这时用tail指令就能够很好的解决这个问题。tail指令能够只读取日志最初几行内容并展现在屏幕上。 tail application.log # 读取application.log文件最初一部分日志tail指令同样反对传入多个文件,它会依照程序别离读取几个文件的最初一部分内容并打印到控制台 tail application1.log application2.log application3.log如果想要指定展现最初100行的日志,则能够应用tail -n来配合查问: tail -n 100 application.log查看增量日志有时,咱们心愿实时查看日志文件的内容,从而更疾速的捕捉到零碎的行为,tail -f指令则反对动静的展现文件新增的内容。如果想要退出主动刷新,能够通过ctrl+c指令来实现: tail -f application.log分页查看日志有时,因为日志内容切实太多,导致控制台疯狂输入,间接吞没了要害信息。因而,须要一个指令可能分页查看日志内容,升高控制台刷新的频率。more指令为这个场景提供了十分好的反对。 more application.log执行了more指令后,控制台将会逐屏展现文件内容,能够应用空格(space键)来展现下一屏的内容,回车(Enter键)展现下一行的内容,Q键退出more指令 至此,文件的全文查问和局部查问的次要指令曾经给出,上面给出另一种类型查问,依据关键字查问,的相干指令 关键字检索依据关键字检索日志在分布式系统中,往往会有数十甚至数百个零碎参加到流程中,这时流量的入口会生成一个惟一的logId用来串联和标记全链路申请。当咱们须要上下游排查问题时,往往会将logId提供给对方来排查。同样,当咱们拿到logId时也须要从日志中跟该logId有关联的日志内容查问进去。这就是一个典型的依据关键字检索日志的场景。grep指令很好的解决了这个问题,它可能将日志中和关键字匹配的行打印进去。 grep "logId" application.log下面的指令会将application.log文件中所有蕴含logId的行打印进去。grep指令同样反对多文件查问 grep "logId" application1.log application2.log application3.loggrep "logId" application*.log还有正则表达式的匹配或者是大小写不敏感的匹配 grep "logId" application.log -i # 大小写不敏感grep -E "[\w\d]*" application.log # 正则表达式这里顺便揭示一个零碎设计的留神点,在分布式系统中logId是通过写入以后线程上下文中实现传递,因而如果在以后线程中提交了一部分工作给异步线程执行,同时有心愿可能用以后线程来跟踪,则务必记得将logId传递到异步线程的上下文中。 ...

October 2, 2021 · 2 min · jiezi

关于日志:日志服务数据导入

体验简介本场景将提供一台配置了CentOS 7.7版本的ECS实例(云服务器)。通过本教程的操作,您能够基于已有环境疾速采集Nginx日志。 体验此场景后,能够把握的常识有: 日志服务数据导入基本操作。 背景常识本场景次要波及以下云产品和服务: 日志服务:日志服务SLS是云原生观测与剖析平台,为Log、Metric、Trace等数据提供大规模、低成本、实时的平台化服务。日志服务一站式提供数据采集、加工、查问与剖析、可视化、告警、生产与投递等性能,全面晋升您在研发、运维、经营、平安等场景的数字化能力。 云服务器ECS:云服务器(Elastic Compute Service,简称ECS)是阿里云提供的性能卓越、稳固牢靠、弹性扩大的IaaS(Infrastructure as a Service)级别云计算服务。云服务器ECS免去了您洽购IT硬件的后期筹备,让您像应用水、电、天然气等公共资源一样便捷、高效地应用服务器,实现计算资源的即开即用和弹性伸缩。阿里云ECS继续提供创新型服务器,解决多种业务需要,助力您的业务倒退。 上传日志文件1、关上虚构桌面的FireFox ESR浏览器,在RAM用户登录页面,输出云产品资源列表中的子用户名称,而后单击下一步。2、在用户明码页面,输出云产品资源列表中的子用户明码,而后单击登录。 3、在浏览器中关上新页签,拜访如下地址,下载测试日志文件。 https://sls-shiyanshi.oss-cn-hangzhou.aliyuncs.com/sls/testlog.csv4、在浏览器中关上新页签,复制如下对象存储OSS控制台地址,粘贴到地址栏中并拜访。 https://oss.console.aliyun.com/5、在对象存储控制台左侧导航栏中,单击Bucket列表。6、在Bucket列表页面,单击Bucket名称。7、在文件治理页面,单击新建目录。8、在新建目录对话框中,输出目录名sls,单击确定。9、在文件治理页面,单击文件名sls。10、在sls目录中,单击上传文件。11、在上传文件页面,文件ACL抉择公共读,单击扫描文件,找到目录/home/adc/下载,抉择下载好的测试日志test.log,而后单击上传文件。 在工作列表页面,期待状态变为上传胜利,示意测试日志文件曾经上传胜利到OSS。 创立Logstore1、在浏览器中关上新页签,复制如下日志服务控制台地址,粘贴到地址栏中并拜访。 https://sls.console.aliyun.com/2、在日志服务控制台页面下方的Project列表区域,抉择云产品资源列表中Project name,而后单击Project名称。3、在左侧日志库性能栏中,单击 4、在创立Logstore对话框中,输出Logstore名称,关上WebTracking开关,而后单击确定。参数阐明: Logstore名称:自定义Logstore名称,在其所属Project内必须惟一。创立Logstore胜利后,无奈更改其名称。 WebTracking:关上WebTracking开关,您能够通过WebTracking从HTML、H5、iOS或Android上采集数据到日志服务。5、在创立胜利对话框中,单击勾销。 导入OSS数据1、在日志服务控制台的接入数据区域,单击OSS-对象存储。2、在抉择日志空间页面,在我的项目Project中抉择云产品资源列表中的Project name,在日志库Logstore中抉择步骤三中创立的Logstore,而后单击下一步。3、在数据源设置页面,输出配置名称,抉择OSS Region、Bucket,输出文件夹前缀,抉择数据格式,而后单击预览。参数阐明: 配置名称:自定义配置名称。 OSS Region:待导入的OSS文件所在Bucket的地区,可在云产品资源列表中查看Bucket的地区。 Bucket:待导入的OSS文件所在的Bucket,可在云产品资源列表中查看Bucket Name。 文件夹前缀:待导入的OSS文件所在文件夹的前缀,输出sls/。 数据格式:文件的解析格局,抉择单行文本日志。 4、在文件预览后果框中,预览数据,后果无误后,单击下个配置。 5、在数据格式配置页面,单击测试,测试后果无误后,单击下个配置。 6、在调度距离页面,关上立刻执行,单击下一步。 7、在查问剖析配置页面,单击下一步。8、在完结页面,单击查问日志。9、在查问剖析对话框中,单击确定。10、在查问剖析页面,稍等几分钟后,单击查问/剖析,即可查问到从OSS导入的日志数据。

September 28, 2021 · 1 min · jiezi

关于日志:你的日志打印对了么

一、前言日志不仅记录了程序的执行过程,同时也是剖析问题的一种重要伎俩。 对于神策剖析 iOS SDK 而言,通过日志零碎岂但能够理解到 SDK 的行为,而且便于咱们排查问题。因而,日志零碎是 SDK 中必不可少的一项性能。 上面针对神策剖析 iOS SDK 日志零碎进行解析,心愿可能给大家提供一些参考。 二、日志打印形式对于 iOS 开发而言,在控制台打印日志的罕用形式有 NSLog 和 printf,咱们先来看一下两者的区别。 2.1NSLogNSLog 是 Foundation 框架提供的日志输入函数,能够在控制台进行格式化输入。 日志的内容会主动蕴含一些零碎信息,例如:项目名称、工夫等。另外,NSLog 还能够打印 OC 中的对象,并且输入的内容会主动换行。示例代码如下: //代码 NSString * a = @"Hello World!"; NSLog(@"这里是第一个 %@", a); NSLog(@"这里是第二个 %@", a); /**控制台日志: 2021-05-17 14:45:01.550403+0800 use-sdk[4608:164047] 这里是第一个 Hello World! 2021-05-17 14:45:01.550498+0800 use-sdk[4608:164047] 这里是第二个 Hello World! */ 2.2printfprintf 只能打印 char 类型,并且内容不会主动换行,须要咱们手动操作[1]。示例代码如下: //代码 printf("Hello World!!!\nHello World!!!\nHello World!!!"); /** Hello World!!! Hello World!!! Hello World!!! ...

August 16, 2021 · 3 min · jiezi

关于日志:Logs-日志功能-彩色命令行工具

先记录后优化应用命令行查看日志的时候,如果想要带有黑白的话,简略脚本如下。 colorize.sh #!/bin/bash# 上面的 ERROR 等是匹配到的,辨别大小写,这里的例子是 springboot 的日志# Example:# 2021-07-14 14:34:19.222 DEBUG 5960 --- [http-nio-8089-exec-3] c.y.c.b.m.P.selectList : <== Total: 8awk 'function color(c,s) { printf("\033[%dm%s\033[0m\n",30+c,s)}/ERROR/ {color(1,$0);next}/SUCCESS/ {color(2,$0);next}/WARNING/ {color(3,$0);next}/INFO/ {color(7,$0);next}/DEBUG/ {color(6,$0);next}{print}' $1# 应用例子:$ sed -n '/2021-07-14 14/,$p' ./logs/spring.log | colorize如果想要更多的色调,上面的文章有将到,前面有空再写 256 color 的脚本。 Refs: Shell - Customize the color of each line of a log file based on a patternTerminal Control Sequences 终端管制转义序列Enable 256 Color Terminal in UbuntuFeatures/256 Color Terminals在 NodeJS 终端输入有简略款式的文本内容

July 14, 2021 · 1 min · jiezi

关于日志:云图说|不要小看不起眼的日志小日志大作用

摘要:云日志服务(Log Tank Service,简称 LTS)能够提供日志收集、剖析、存储等服务。用户能够通过云日志服务疾速高效地进行设施运维治理、用户业务趋势剖析、安全监控审计等操作。本文分享自华为云社区《【云图说】第32期 窥探日志的机密》,原文作者:阅识风波 。 网络设备、操作系统及服务程序等软硬件在运行时都会产生 log,记录着该事件产生的工夫、使用者、具体操作等相干形容。然而,因为日志通常不属于零碎的外围性能,所以经常不被器重。直到须要决策分析和问题定位时,这时日志的重要性才会凸显进去。 点击关注,第一工夫理解华为云陈腐技术~

May 12, 2021 · 1 min · jiezi

关于日志:zap源码阅读1

zap是 Uber 开发的一个高性能、强类型、分 level 的 go 语言日志库1.对象创立1.1 Logger 构造体type Logger struct { core zapcore.Core // 外围接口 development bool addCaller bool onFatal zapcore.CheckWriteAction // default is WriteThenFatal name string errorOutput zapcore.WriteSyncer addStack zapcore.LevelEnabler callerSkip int clock Clock}Logger对象的创立有两种形式: 通过New 间接创立func New(core zapcore.Core, options ...Option) *Logger { if core == nil { return NewNop() } log := &Logger{ core: core, errorOutput: zapcore.Lock(os.Stderr), addStack: zapcore.FatalLevel + 1, clock: _systemClock, } return log.WithOptions(options...)}函数WithOptions应用的是函数式抉择模式, 和ants源码浏览(https://segmentfault.com/a/11... 中应用的办法统一。 ...

April 26, 2021 · 7 min · jiezi

关于日志:轻量级的日志系统-Loki-体验

以前有关注到 Loki 这个开源我的项目,还是 1.x 的,借鉴了prometheus 的 chunks 存储,labels, promql 查问等个性,做了一套日志零碎。 和 docker / k8s 集成比拟好,最近看到版本到 2.2,感觉能够小试一下 整体架构 下面的架构图,形容了 Loki 的几个根底组件 promtail: 日志采集组件,通过配置scrape_configs(嗯,看着和prometheus的采集配置如同啊),采集对应的本地文件,journal日志,应用positions(一个yaml配置文件),治理采集偏移量等,在通过http的形式,推送到远方的存储中loki: 外围日志存储,查问组件,能够拆分为 querier,ingester, distributor 等多个组件,也能够部署为一个过程,数据存储能够落地为S3的文件中(大杀器啊,对象存储切实是太好用了)grafana: 数据查问模块,能够应用LogQL(一种相似promql的语法)查问当然还有一些其余组件,如loki-canary(日志链路可用性监控), fluentd(和fluentd对接的)等 梳理下需要本人始终有多个 VPS,先看下本人有哪些节点 3台VPS:广州,香港,美国各1, 配置1外围/2G ~ 2外围4G,都有公网IP1台软路由:没有公网IP,可上网1台NAS:11T硬盘,16G内存,家里,可上网3 个都是 linux 零碎,而且下面的组件都容器化的,应用 docker-compose 治理,以前应用过 ELK,对于 VPS 来讲,太重了,始终找不到一个适合的日志零碎,当初能够在 loki 上小试牛刀 因为都曾经容器化,所以 docker 的规范输入是须要收集的,采集的需要总结如下 docker 规范输入本地的日志文件systemd 日志文件对应不通过的地区,还须要买通相干的网络环境,可能对立收集到内网的 NAS 中 如何收集 docker 规范输入日志对于 docker 标出输入,默认实现上,是会落到本地文档的,能够通过docker inspect id, 返回的LogPath 看到具体文件门路地址 个别的采集计划,就是主动获取 LogPath,而后采集对应的文件即可 Loki 采纳的是实现了一个规范的 Docker Log Driver, 装置好插件,就能够间接推送了,应用流程如下 先部署 loki-docker-driver ...

April 1, 2021 · 2 min · jiezi

关于运维:filebeatkafkagraylogesmongodb可视化日志详解

graylog 是一个开源的业余的日志聚合、剖析、审计、展现、预警的工具,跟 ELK 很类似,然而更简略,上面说一说 graylog 如何部署,应用,以及对 graylog 的工作流程做一个简略的梳理本文篇幅比拟长,一共应用了三台机器,这三台机器上部署了 kafka 集群(2.3),es 集群(7.11.2),MongoDB 正本集(4.2),还有 graylog 集群(4.0.2),收集的日志是 k8s 的日志,应用 DaemonSet 的形式通过 filebeat(7.11.2)将日志收集到 kafka 中。本文将从部署开始,一步一步的理解 graylog 是怎么部署,以及简略的应用。 graylog 介绍 组件介绍从架构图中能够看出,graylog 是由三局部组成: mongodb 寄存 gralog 管制台上的配置信息,以及 graylog 集群状态信息,还有一些元信息es 寄存日志数据,以及检索数据等graylog 相当于一个直达的角色mongodb 和 es 没什么好说的,作用都比拟清晰,重点说一下 graylog 的一些组件,及其作用。 Inputs 日志数据起源,能够通过 graylog 的 Sidecar 来被动抓取,也能够通过其余 beats,syslog 等被动推送Extractors 日志数据格式转换,次要用于 json 解析、kv 解析、工夫戳解析、正则解析Streams 日志信息分类,通过设置一些规定来将日志发送到指定的索引中Indices 长久化数据存储,设置索引名及索引的过期策略、分片数、正本数、flush 工夫距离等Outputs 日志数据的转发,将解析的 Stream 发送到其余的 graylog 集群Pipelines 日志数据的过滤,建设数据荡涤的过滤规定、字段增加或删除、条件过滤、自定义函数Sidecar 轻量级的日志采集器LookupTables 服务解析,基于 IP 的 Whois 查问和基于源 IP 的情报监控Geolocation 可视化地理位置,基于起源 IP 的监控流程介绍 ...

March 18, 2021 · 7 min · jiezi

关于amazon-web-services:AWS流量日志的实时分析之Suricata

背景介绍因为VPC的Flow log不是实时的, VPC flow log在聚合工夫距离内捕捉数据后,须要额定的工夫来解决数据并将其公布到 CloudWatch Logs 或 Amazon S3。此额定工夫,对于公布到 CloudWatch Logs 约为 5 分钟,对于公布到 Amazon S3 约为 10 分钟。流日志服务在此额定的工夫内以最大限度提供。在某些状况下,您的日志可能会提早超过后面提到的 5 到 10 分钟的额定工夫,无奈满足一些客户对于流量剖析的实时性要求,因而客户会询问咱们是否反对一些第三方的流量剖析零碎来对流量进行实时的监控与剖析。 在2021年的2月23日,亚马逊云中国区发表了VPC Traffic Mirroring的性能落地[1],应用此性能,能够联合一些第三方的流量剖析零碎,解决客户对于流量剖析的实时性需要。在文档[2]中,列出了联合应用Suricata进行流量剖析的实例,在此篇文档中进行测试与扩大。 依据文档[3]如下测试步骤,只能在log中看到Vxlan的镜像报文,无奈对报文内内容(例如HTTP申请)进行记录与剖析:Step 1: Install the Suricata software on the EC2 instance targetStep 2: Create a traffic mirror targetStep 3: Create a traffic mirror filterStep 4: Create a traffic mirror session 依据下列配置与测试,可能胜利记录客户的HTTP申请。应用下列命令更新Suricata Rule sudo suricata-update通过查看更新后的rule,能够看到http相干的rule检测前会先应用“http $HOME_NET any -> $EXTERNAL_NET any”匹配数据报文的源目IP,$HOME_NET和$EXTERNAL_NET是在Suricata的配置文件/etc/suricata/suricata.yaml中做的定义,因而更改其配置文件,设置$HOME_NET和$EXTERNAL_NET均为any,配置后如下: HOME_NET: "any" EXTERNAL_NET: "any"因为通过Traffic Mirror镜像过去的报文是具备Vxlan封装的,因而须要Suricata对报文进行Vxlan的解封装,更改配置文件/etc/suricata/suricata.yaml使其对报文进行Vxlan的解封装,配置后如下: decoder: vxlan: enabled: true ports: $VXLAN_PORTS # syntax: '8472, 4789'更改配置文件/etc/suricata/suricata.yaml开启HTTP Log,配置后如下: - http-log: enabled: yes filename: http.log append: yes重启Suricata过程使更改后的配置失效 (须要先kill掉以后suricata过程,删除rm -rf /var/run/suricata.pid文件,而后再次运行命令“suricata -c /etc/suricata/suricata.yaml -k none -i eth0 -D”启动过程),重启过程不会删除之前曾经生成的日志.进行测试,在镜像流量的源设施上拜访www.baidu.com curl www.baidu.com 在镜像流量的指标设施上(装置了Suricata的设施)查看/var/log/suricata/http.log,能够看到申请被胜利记录: 03/15/2021-07:04:00.347144 www.baidu.com[]/[]curl/7.61.1**GET[]HTTP/1.1[]200[]2381 bytes[]172.31.27.94:39626 -> 220.181.38.15补充阐明:1.Suricata报文解决流程图 ...

March 16, 2021 · 1 min · jiezi

关于日志:EMAS远程日志-移动端问题排查利器

简介: 近程日志是什么?具体做了哪些事件?外部是怎么实现的?本文将从 性能、架构、体验优化三个方面来介绍一下近程日志倒退过程及瞻望。阿里云 云原生利用研发平台EMAS 张月(此间) 前言当 App 公布到用户手里之后,开发者对 App 运行状态的感知就只能通过各类业务和稳定性监控了。这些监控平台会把线上问题(如解体堆栈、异样网络申请等)及业务数据采集到服务端,而后给用户提供聚合 Metrics 等 BI 数据。但这个过程很容易失落细节,不能直观反映问题产生起因,导致线上问题很难排查剖析。 可能聪慧的你会想,我把所有的日志都上报到后端不就行了?然而这样会给 App 带来很多无意义的网络耗费,也会造成比拟大的网络和存储的压力。阿里云近程日志( https://www.aliyun.com/product/emascrash/tlog )在这个场景下应运而生,通过将日志寄存 App 本地,须要时拉取的形式,在就义了一点实时性的状况下,解决了上报日志费流量存储,不上报日志没方法查问题的窘境。 近程日志具体在外面做了哪些事件?外部又是怎么实现的?接下来我就会从 性能、架构、体验优化 三个方面来介绍一下近程日志倒退过程,最初再聊聊咱们的瞻望。 性能如前言介绍的一样,咱们会把日志先存在 App 本地,当须要的时候再拉取上来做剖析查看。整个过程能够简略拆解成如下几步: 挪动端个性C层面实现:性能晋升,一份代码多端反对加密存储:应用非对称加密形式,日志存储上报更平安日志轮转:最长反对7天日志存储,每天最大存储 10MMMAP机制:防止缓存日志失落,晋升性能 (注:MMAP 机制是将文件间接映射成内存的操作,防止页缓存到文件的拷贝,详情可移步:https://www.cnblogs.com/huxiao-tee/p/4660352.html )后端个性近程日志的定位是异步拉取,把问题日志从挪动设施端拉回来剖析。联合不同应用场景,用户对设施精准度、拉取及时性、拉取成功率等不同偏重的特点,咱们在拉取模式和产品联动上做了不同的实际。 拉取模式精准拉取:指定设施列表举个例子,一个风和日丽的上午,你刚到公司,发现老板满脸不爽的通知你昨天晚上他 App 解体了。那摆在眼前就是两条路,要么搞定这个问题,要么“提桶跑路”。这个时候近程日志出场了,你能够纯熟的输出老板的设施Id,做一次日志拉取,查查老板 App 的日志定位起因,解决问题。 这个模式下,用户应用面临了两个体验问题: 拉取速度慢。下发拉取工作后,须要终端用户从新关上App能力实现日志上传,但这个工夫很不可控。拉取成功率低。下发拉取工作会有局部设施inactive,导致没方法承受拉取指令,导致日志无奈上传。针对这个问题,咱们在拉取上做了相干的优化,实现了智能筛选的性能。 智能筛选:指定筛选条件用户不须要指定指标设施列表,而是关怀某个或几个维度下的设施具体日志。那用户能够设定拉取的设施组合条件,零碎主动帮用户选取设施。 为了放慢设施拉取速度以及拉取成功率,近程日志在选取设施减少了如下策略: 主动筛选最近启动的设施拉取。主动调整筛选出的设施池,扩充拉取规模,最多扩充到原始规模的8倍等。联动解体数据在端上问题产生的场景中,大多数是因为解体或者异样行为。为了给与对这种场景撑持,咱们提供了解体剖析数据联动的反对,突破了「解体剖析」 和「近程日志」两个产品之间的数据孤岛,提供了问题排查的更多的可能性。 数据联动:解体设施列表通过解体剖析提供解体设施列表,能够帮近程日志间接划定待拉取设施范畴,用户更加省力,通过 EMAS 「解体剖析」中的列表页一键跳转拉取。 这个形式极大简化了用户在排查解体相干问题时选定设施列表的工作,但对于每个解体问题还是须要创立拉取日志工作。这个拉取过程还是存在一个工夫上的通畅感,这不仅会打断工程师的排查思路,也会消磨排查问题的积极性。咱们是否罢黜拉取动作,间接把解体问题对应的设施日志筹备好呢?「智能拉取」就是为了解决这个场景的问题。 智能拉取:提前拉取咱们加深了后面的数据联动,对于首现和 Top 解体问题,每天7点定时创立工作,开发同学下班的时候基本上都曾经拉取胜利了,极大的晋升了开发同学的问题排查效率。 架构 体验优化除了在内功上打磨,产品的应用体验上咱们也做了相当多的优化,也和大家分享一下。 工作音讯告诉,让你可能第一工夫感知日志上报整合工作详情与日志详情页面,查看工作日志更不便欠缺工作、设施、日志查问筛选工作工夫线透出,感知工作生命周期顶部菜单栏变侧边栏,控制台与EMAS格调对立拉取工作长久化,容错性更好。 到此,近程日志的现有的性能、架构和体验介绍就此结束了,接下来说说咱们对将来的布局。 瞻望减少上报模式目前都是通过服务端下发工作,端上接受任务上报这种「被动上报」的模式,有肯定的局限性,咱们接下来心愿对客户端在某些状况下「被动上报」日志,比方间断Crash,或者用户在App内反馈问题时上报做相应的反对。 丰盛采集数据当初咱们的日志打印仅限于用户日志,还须要反对更多的无痕埋点,记录用户操作门路和网络IO等操作,让日志数据更丰盛,可能通过日志复现用户操作流水,机器状态的变动。 更多产品联动「解体剖析」是咱们产品联动的第一站,除了解体和异样,对品质有谋求的App开发者也越发器重性能问题,毕竟「性能决定当初,性能决定将来」,后续咱们也会思考和如何将「性能剖析」产品和近程日志买通,更好的服务咱们的用户。 挪动研发平台 EMAS阿里巴巴利用研发平台 EMAS 是国内当先的云原生利用研发平台(挪动App、H5利用、小程序、Web利用等),基于宽泛的云原生技术(Backend as a Service、Serverless、DevOps、低代码等),致力于为企业、开发者提供一站式的利用研发治理服务,涵盖开发、测试、运维、经营等利用全生命周期。欢送大家移步应用:https://cn.aliyun.com/product/emas

December 16, 2020 · 1 min · jiezi

slf4j-不同业务日志写到不同的文件中

在resources目录下增加logback.xml文件 <?xml version="1.0" encoding="UTF-8"?><configuration> <springProperty name="LOG_PATH" source="logging.path" defaultValue="/home/admin/xxx/logs" /> <!--自定义控制台日志格局--> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder charset="UTF-8"> <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %level [%thread] %file:%line - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <!--零碎INFO级别日志-滚动记录日志--> <appender name="SYS_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--被写入的文件名,能够是绝对目录,也能够是相对目录,如果下级目录不存在会主动创立,没有默认值--> <File>${LOG_PATH}/sys_info.log</File> <!--如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true。--> <append>true</append> <!--级别过滤器(LevelFilter),此处只打INFO级别的日志--> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>INFO</level> <!--上面2个属性示意匹配规定level的承受打印,不匹配的(即非INFO)回绝打印--> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <!-- 最罕用的滚动策略,它依据工夫来制订滚动策略,既负责滚动也负责登程滚动--> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--设置滚动文件规定,如果间接应用 %d,默认格局是 yyyy-MM-dd--> <fileNamePattern>${LOG_PATH}/sys_info.log.%d</fileNamePattern> <!--保留30天的日志--> <maxHistory>30</maxHistory> </rollingPolicy> <encoder charset="UTF-8"> <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %level [%thread] %file:%line - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <!--零碎ERROR级别日志-滚动记录日志--> <appender name="SYS_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <File>${LOG_PATH}/sys_error.log</File> <append>true</append> <!--此处只打ERROR级别的日志--> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/sys_error.log.%d</fileNamePattern> <maxHistory>12</maxHistory> </rollingPolicy> <encoder charset="UTF-8"> <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %level [%thread] %file:%line - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <!--业务business-1日志--> <appender name="bs1_Appender" class="ch.qos.logback.core.rolling.RollingFileAppender"> <File>${LOG_PATH}/api.log</File> <append>true</append> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>INFO</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/api.log.%d</fileNamePattern> <maxHistory>12</maxHistory> </rollingPolicy> <encoder charset="UTF-8"> <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %level [%thread] %file:%line - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- additivity属性为false,示意此logger的打印信息不再向下级传递(注:该值默认为true,logger的日志信息会顺次向下级传递,最高级logger为root,如果不加则至多打印2次,自身一次,root一次)--> <logger name="bs1" additivity="false" level="INFO"> <appender-ref ref="bs1_Appender"/> </logger> <!--info和error离开打印,注:ERROR > WARN > INFO > DEBUG > TRACE--> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="SYS_INFO"/> <appender-ref ref="SYS_ERROR"/> </root></configuration>在application.yml文件中增加日志目录 ...

July 9, 2020 · 1 min · jiezi

日志系统新贵Loki确实比笨重的ELK轻

作者: inkt1234 来源:https://blog.csdn.net/Linktha... 最近,在对公司容器云的日志方案进行设计的时候,发现主流的ELK或者EFK比较重,再加上现阶段对于ES复杂的搜索功能很多都用不上最终选择了Grafana开源的Loki日志系统,下面介绍下Loki的背景。 背景和动机当我们的容器云运行的应用或者某个节点出现问题了,解决思路应该如下: 我们的监控使用的是基于prometheus体系进行改造的,prometheus中比较重要的是metric和alert,metric是来说明当前或者历史达到了某个值,alert设置metric达到某个特定的基数触发了告警,但是这些信息明显是不够的。我们都知道,k8s的基本单位是pod,pod把日志输出到stdout和stderr,平时有什么问题我们通常在界面或者通过命令查看相关的日志,举个例子:当我们的某个pod的内存变得很大,触发了我们的alert,这个时候管理员,去页面查询确认是哪个pod有问题,然后要确认pod内存变大的原因,我们还需要去查询pod的日志,如果没有日志系统,那么我们就需要到页面或者使用命令进行查询了: 如果,这个时候应用突然挂了,这个时候我们就无法查到相关的日志了,所以需要引入日志系统,统一收集日志,而使用ELK的话,就需要在Kibana和Grafana之间切换,影响用户体验。所以 ,loki的第一目的就是最小化度量和日志的切换成本,有助于减少异常事件的响应时间和提高用户的体验 ELK存在的问题现有的很多日志采集的方案都是采用全文检索对日志进行索引(如ELK方案),优点是功能丰富,允许复杂的操作。但是,这些方案往往规模复杂,资源占用高,操作苦难。很多功能往往用不上,大多数查询只关注一定时间范围和一些简单的参数(如host、service等),使用这些解决方案就有点杀鸡用牛刀的感觉了。因此,Loki的第二个目的是,在查询语言的易操作性和复杂性之间可以达到一个权衡。 因此,Loki的第二个目的是,在查询语言的易操作性和复杂性之间可以达到一个权衡。 成本全文检索的方案也带来成本问题,简单的说就是全文搜索(如ES)的倒排索引的切分和共享的成本较高。后来出现了其他不同的设计方案如:OKlog(https://github.com/oklog/oklo...、基于网格的分布策略。这两个设计决策提供了大量的成本降低和非常简单的操作,但是查询不够方便。因此,Loki的第三个目的是,提高一个更具成本效益的解决方案。 整体架构Loki的架构如下: 不难看出,Loki的架构非常简单,使用了和prometheus一样的标签来作为索引,也就是说,你通过这些标签既可以查询日志的内容也可以查询到监控的数据,不但减少了两种查询之间的切换成本,也极大地降低了日志索引的存储。Loki将使用与prometheus相同的服务发现和标签重新标记库,编写了pormtail, 在k8s中promtail以daemonset方式运行在每个节点中,通过kubernetes api等到日志的正确元数据,并将它们发送到Loki。下面是日志的存储架构: 读写日志数据的写主要依托的是Distributor和Ingester两个组件,整体的流程如下: Distributor一旦promtail收集日志并将其发送给loki,Distributor就是第一个接收日志的组件。由于日志的写入量可能很大,所以不能在它们传入时将它们写入数据库。这会毁掉数据库。我们需要批处理和压缩数据。Loki通过构建压缩数据块来实现这一点,方法是在日志进入时对其进行gzip操作,组件ingester是一个有状态的组件,负责构建和刷新chunck,当chunk达到一定的数量或者时间后,刷新到存储中去。每个流的日志对应一个ingester,当日志到达Distributor后,根据元数据和hash算法计算出应该到哪个ingester上面。 此外,为了冗余和弹性,我们将其复制n(默认情况下为3)次。 Ingesteringester接收到日志并开始构建chunk: 基本上就是将日志进行压缩并附加到chunk上面。一旦chunk“填满”(数据达到一定数量或者过了一定期限),ingester将其刷新到数据库。我们对块和索引使用单独的数据库,因为它们存储的数据类型不同。 刷新一个chunk之后,ingester然后创建一个新的空chunk并将新条目添加到该chunk中。 Querier读取就非常简单了,由Querier负责给定一个时间范围和标签选择器,Querier查看索引以确定哪些块匹配,并通过greps将结果显示出来。它还从Ingester获取尚未刷新的最新数据。对于每个查询,一个查询器将为您显示所有相关日志。实现了查询并行化,提供分布式grep,使即使是大型查询也是足够的。 可扩展性Loki的索引存储可以是cassandra/bigtable/dynamodb,而chuncks可以是各种对象存储,Querier和Distributor都是无状态的组件。对于ingester他虽然是有状态的但是,当新的节点加入或者减少,整节点间的chunk会重新分配,已适应新的散列环。而Loki底层存储的实现Cortex已经 在实际的生产中投入使用多年了。有了这句话,我可以放心的在环境中实验一把了。 微信搜索:【Java小咖秀】更多精彩等着您~回复手册获取博主15万字Java面试通关手册&1.4万字Linux命令实战书册~

June 30, 2020 · 1 min · jiezi

ELK日志系统架构解析笔记

一、ELK服务架构解析 (1)收集记录日志流程思考//备注:思考如果我们自己搭建一个日志收集平台应该如何操作和搭建?具体会有哪些动作?(1)目的:将线上的服务器日志不断的收集到一个统一的平台,并且提供一个可视化的界面能对所有的日志进行索引查询(2)架构拆解之需要哪些步骤: 线上服务器日志收集工具----> 统一的日志存储平台----> 日志分析工具 ----> 可视化界面(3)以上步骤,对应ELK就是: 「L代表」logstash[或者filebeat]:日志收集工具,部署在线上服务器中,对相关的yml配置文件进行配置可以对日志进行收集并传输到统一的日志存储平台,其中filebeat更轻量,一般使用filebeat 「E代表」Elasticsearch:日志分析工具,用来对所有的日志创建各种索引进行搜索等,也是我们常说的搜索引擎 「K代表」Kibana:可视化界面,用来对日志进行可视化展示和搜索,相当于一个管理界面 (2)ELK架构需要安装的服务工具 工具名称作用部署位置配置文件备注filebeat收集日志产生日志的线上服务器,比如线上有两台web服务器,那么需要在两台web服务器都部署filebeatfilebeat.yml轻量,推荐Logstash收集日志产生日志的服务器上pipelines.ymllogstash.yml和filebeat比较更重,线上服务器本身运行web服务,部署logstash更耗费线上资源,不推荐elasticsearch日志分析工具,搜索引擎filebeat或者logstash的输出服务器elasticsearch.yml能对大容量的数据进行接近实时的存储、搜索和分析操作。通常被用作某些应用的基础搜索引擎,使其具有复杂的搜索功能Kibana日志可视化平台配合elasticsearch使用kibana.yml通常与 Elasticsearch 配合使用,对其中数据进行搜索、分析和以统计图表的方式展示各配置文件解析(1)Filebeat配置文件主要配置解析 //filebeat配置文件filebeat.yml解析 filebeat.inputs: - type: log # Change to true to enable this input configuration. enabled: true # Paths that should be crawled and fetched. Glob based paths. paths: //这一项配置,代表要收集日志的目录 - /opt/nginx-1.14.0/logs/stars/star.access.log output.logstash: # The Logstash hosts hosts: ["10.3.0.115:5401"] //该项是日志收集以后到一个统一平台,作为输出的IP地址 # Configure processors to enhance or manipulate events generated by the beat. processors: - add_host_metadata: ~ - add_cloud_metadata: ~ - add_docker_metadata: ~ - add_kubernetes_metadata: ~(2)logstash配置文件主要配置解析【主要四种插件,inputs,codecs,filters,outputs。】 ...

June 10, 2020 · 1 min · jiezi

日志格式

<property name="PATTERN">%d{yyyy-MM-dd HH:mm:ss} [%p] [%-40.40c{2.}] [%X{ip}] [%X{id}] %msg%n</property>模式转换字符:下表说明了以上模式使用的字符和所有其他字符,可以在自定义模式中使用: 转换字符表示的意思c用于输出的记录事件的类别。例如,对于类别名称”a.b.c” 模式 %c{2} 会输出 “b.c”C用于输出呼叫者发出日志请求的完全限定类名。例如,对于类名 “org.apache.xyz.SomeClass”, 模式 %C{1} 会输出 “SomeClass”.d用于输出的记录事件的日期。例如, %d{HH:mm:ss,SSS} 或 %d{dd MMM yyyy HH:mm:ss,SSS}.F用于输出被发出日志记录请求,其中的文件名l用于将产生的日志事件调用者输出位置信息L用于输出从被发出日志记录请求的行号m用于输出使用日志事件相关联的应用程序提供的消息M用于输出发出日志请求所在的方法名称n输出平台相关的行分隔符或文字p用于输出的记录事件的优先级r用于输出毫秒从布局的结构经过直到创建日志记录事件的数目t用于输出生成的日志记录事件的线程的名称x用于与产生该日志事件的线程相关联输出的NDC(嵌套诊断上下文)X在X转换字符后面是键为的MDC。例如 X{clientIP} 将打印存储在MDC对键clientIP的信息%文字百分号 %%将打印%标志格式修饰符:默认情况下,相关资料原样输出。然而,随着格式修饰符的帮助下,可以改变最小字段宽度,最大字段宽度和对齐。 下表涵盖了各种各样的修饰符的情况: Format modifierleft justifyminimum widthmaximum width注释%20cfalse20none用空格左垫,如果类别名称少于20个字符长%-20ctrue20none用空格右垫,如果类别名称少于20个字符长%.30cNANONE30从开始截断,如果类别名称超过30个字符长%20.30cfalse2030用空格左侧垫,如果类别名称短于20个字符。但是,如果类别名称长度超过30个字符,那么从开始截断。%-20.30ctrue2030用空格右侧垫,如果类别名称短于20个字符。但是,如果类别名称长度超过30个字符,那么从开始截断。

June 7, 2020 · 1 min · jiezi

Choerodon如何进行日志收集与告警

应用程序日志是由软件应用程序记录的事件文件, 它一般包含错误,信息事件和警告。一个良好的日志系统有助于快速发现问题,定位问题,同时也为业务分析起到一定的作用。 传统ELK系统ELK系统是目前比较流行的日志解决方案,由Elasticsearch、Logstash、Kibana组成,目前三个组件都归属于Elastic。 Elasticsearch是一个基于Lucene库的搜索引擎。它提供了一个分布式、支持多租户的全文搜索引擎,具有HTTP Web接口和无模式JSON文档。Elasticsearch是用Java开发的,并在Apache许可证下作为开源软件发布。 自2010年发布以来,Elasticsearch已迅速成为最受欢迎的搜索引擎,常用于日志分析,全文搜索和业务分析等业务场景。 Logstash 将日志收集后发送到Elasticsearch中进行存储,用户访问Kibana提供的UI界面查询数据。 Choerodon中的日志系统总览和ELK类似,Choerodon选用了Elasticsearch存储日志数据,并由Kibana展示数据。Choerodon平台运行在Kubernetes平台之上,同时也管理多个Kubernetes集群,为了让日志系统尽可能的不影响业务系统,Choerodon使用了比Logstash更轻量的由C语言编写的fluent bit替代采集端工具Logstash。fluent bit通过Deamonset的方式运行在Kubernetes集群中的每一个可调度的节点上,实时采集日志,发送到Elasticsearch中,一般情况下,从日志产生到Kibana中可以查看到的延迟不超过1秒钟。精简结构图如下: 先看一下查看界面: 通过搜索关键字error查询含有该关键字的日志,界面显示最近15分钟gateway-helper服务出现了三次error的日志信息,列表中为该日志的缩略信息,可以点击日志前面的小箭头展开查看完整的信息。 展开之后就可以看到更加详细的信息了。 PS:多行展示官方的fluent bit截止目前暂未良好的支持docker中的json-file日志,建议使用Choerodon定制fluent bit。 Fluent bit vs FluentdFluentd和Fluent Bit项目均由Treasure Data创建和赞助,旨在解决日志的收集,处理和交付问题。 两个项目都有很多相似之处,Fluent Bit完全基于Fluentd架构和一般设计的设计和经验。选择使用哪一个取决于最终需求,从架构角度可以考虑: Fluentd是日志收集器,处理器和聚合器,使用Ruby和C构建。Fluent Bit是一个日志收集器和处理器,它没有像Fluentd一样强大的聚合功能。在Choerodon日志方案聚合功能由Elasticsearch提供。Fluent bit一般情况下占用内存要仅为fluentd十分之一以下。类似于Fluent bit的组件还有很多如Filebeat等,Choerodon也在关注各主流组件的更新,选择最合适的日志采集端工具。 如何自动收集日志一般在采集日志的时候,为了更容易分析日志,需要将日志进行解析。下面的这个图中将Java应用的一条日志解析为level,class,processid和msg四个部分: 解析日志需要指定解析规则,Choerodon部署界面可以为应用配置解析规则,当配置了解析规则后即表示该应用的日志需要按照配置的规则收集,部署界面如下图所示: 通过mysql这个解析规则解析该应用的日志,目前Choerodon日志解决方案中默认提供了docker、mysql、tomcat、springboot和nginx的日志解析规则,如果你认为需要添加其他通用的日志解析规则欢迎到Choerodon社区中建议。 在Fluent-bit中可以配置通配符"*”来收集匹配规则的日志,但是很多时候开发者希望在部署应用时指定是否收集日志。在Choerodon平台中,应用是运行在Kubernetes平台之上的,所以开发者可以通过给应用的部署集添加标签来表示需不要收集日志,再通过一个程序去读取标签的内容,自动修改Fluent-bit的配置就可以随心的控制是否需要收集日志了。如果需要默认收集所有应用的日志,排除部分日志可以使用Fluent-bit提供的 fluentbit.io/exclude注解。 在日志中添加集群相关的信息Choerodon的服务运行在Kubernetes集群中,如果能够在查看日志的时候也能看到日志来自哪个服务器,属于哪个Pod就能够更快的定位和查找问题。 Fluent bit提供了Kubernetes的filter,通过赋予Fluent bit查询权限,它就能够自动的为每条日志附加集群的相关信息。 如何告警收集日志之后开发者需要对某个关键字出现的次数进行告警,如Exception这个关键字在某服务中一分钟出现了5次以上,需要将这个消息通知给特定的人员。 在这之前大家先来了解一下Choerodon中的监控方案: 应用监控数据经Prometheus采集处理之后展示在Grafana中,告警信息通过Alertmanager发送给用户。因为在监控方案中已经有可用的告警机制,开发者只需要将日志系统中的内容转换为Prometheus可以采集的指标数据即可使用监控方案中的告警机制。 Elastalert是用Python编写的Elasticsearch告警工具,通过配置一定时间间隔查询elasticsearch数据库,对比预设规则达到告警的目的,Choerodon可以通过简单的改造elastalert实现将elastalert查询的结果转换为Prometheus的数据格式供Prometheus拉取。改造步骤分为以下几个部分: 引入prometheusSDK:prometheus提供了Python的SDK,简单的引入之后应用就具有了可以被监控的特性,可以选择监听指定端口已提供监控数据。埋点:将更新监控数据的操作置于elastalert每次执行查询完成后已更新监控数据即可。目前Choerodon正在用Golang开发新的日志监控工具,得益于Golang的特性,新的日志监控工具将以更低的内存消耗,更低的cpu占用和更稳定的运行状态为日志监控提供支持。 使用Choerodon认证登录Kibana如上所示,Kibana作为日志查看界面,如果使用社区版Kibana是没有权限校验的,会存在一定风险,希望授权用户才能访问日志查询界面。为此Choerodon设计开发了一个认证代理服务,将无权限控制的Kibana放置于认证代理的后端,只有通过了认证,才能访问到Kibana的界面。如下图所示: 效果图: 现在,你已经了解Choerodon的日志方案,接下来就可以跟随着Choerodon官网部署尝试一下吧。 参考文献: https://zh.wikipedia.org/wiki/Elasticsearchhttps://docs.fluentbit.io/manual/about/fluentd_and_fluentbit本篇文章出自Choerodon猪齿鱼社区董文启。

June 3, 2020 · 1 min · jiezi

初探Logback学会看懂Logback配置文件

前言在现如今的应用中,日志已经成为了一个非常重要的工具。通过系统打印的日志,可以监测系统的运行情况,排查系统错误的原因。日志从最早期的System.out.print到如今各种成熟的框架,使得日志打印更加规范化和清晰化。尤其是SLF4J的出现,为日志框架定义了通用的FACADE接口和能力。只需要在应用中引入SLF4J包和具体实现该FACADE的日志包,上层应用就可以只需要面向SLF4J接口编程,而无需关心具体的底层的日志框架,实现了上层应用和底层日志框架的解耦。Logback作为一个支持SLF4J通用能力的框架,成为了炙手可热的日志框架之一。今天就来稍微了解一下Logback日志的一些基础能力以及配置文件。 快速上手Logback引入MAVEN依赖logback主要由三个模块组成,分别是logback-core,logback-classic和logback-access。其中logback-core是整个Logback的核型模块,logback-classic支持了SLF4J FACADE,而logback-access则集成了Servlet容齐来提供HTTP日志功能,适用于web应用。下面主要是基于logback-classic来进行介绍。 引入logback-classic的包如下: <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.3.0-alpha5</version></dependency>上面拉取的Maven包基于传递性远离,会自动拉取logback-classic,logback-core和slf4j-api.jar,因此无需在项目中再额外声明SLF4J和logback-core的依赖。 使用Logback因为logback-classic实现了SLF4J FACADE,所以上层应用只需要面向SLF4J的调用语法即可。下面代码展示了如何获取到Logger对象用来打印日志。 import org.slf4j.Logger;import org.slf4j.LoggerFactory;import ch.qos.logback.classic.LoggerContext;import ch.qos.logback.core.util.StatusPrinter;public class HelloWorld2 { public static void main(String[] args) { //这里的Logger和LoggerFactory均为SLF4J的类,真正调用时会使用Logback的日志能力 //getLogger方法中传入的是Logger的名称,这个名称在后面讲解配置文件中的<logger>时会继续提到 Logger logger = LoggerFactory.getLogger("chapters.introduction.HelloWorld2"); //打印一条Debug级别的日志 logger.debug("Hello world."); //获取根Logger,使用场景比较少 Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); }}日志级别Logback中每一个Logger都有对应的日志级别,该日志级别可以是Logger自己定义的,也可以是从父Logger上继承下来的。Logback一共支持5个日志级别,从高到低分别是ERROR,WARN,INFO,DEBUG,TRACE。Logger的日志级别决定了哪些级别的日志可以被输出。只有大于等于该Logger级别的日志才会被打印出来。比如假设上文中获取的名为"chapters.introduction.HelloWorld2"的Logger日志级别为INFO,则调用logger.debug("xxx")不会输出日志内容,因为DEBUG日志级别低于INFO日志级别。 日志级别可以帮助我们控制日志打印的粒度,比如在开发环境可以将日志级别设置到DEBUG帮助排查问题,而在生产环境则可以将日志级别设置到INFO,从而减少不必要的打印日志带来的性能影响。 参数化输出有时候我们往往并不只是打印出一条完整的日志,而是希望在日志中附带一些运行中参数,如下: Logger logger = LoggerFactory.getLogger("chapters.introduction.HelloWorld2");logger.debug("Hello World To " + username);上文的日志中除了打印了一些结构化的语句,还拼接了运行时执行这段逻辑的用户的名称。这里就会带来一个问题,即字符串拼接的问题。虽然JVM对String字符串的拼接已经进行了优化,但是假如当前的日志级别为INFO,那么这段代码所执行字符串拼接操作就是完全不必要的。因此,建议在代码加上一行日志级别的判断进行优化,如下: //非debug级别不会执行字符串拼接操作,但是debug级别会执行两次isDebugEnabled操作,性能影响不大if(logger.isDebugEnabled()) { logger.debug("Hello World To " + username);}但是,logback并不推荐在系统中使用字符串拼接的方式来输出日志,而是提倡使用参数传递的方式,由logback自己来执行日志的序列化。如下: //logger方法会判断是否为debug级别,再决定将entry序列化拼接如字符串logger.debug("The entry is {}.", entry);这种日志输出方式就无需额外包一层日志级别的判断,因为logger.debug方法内部自己会判断一次日志级别,再去执行日志内容转码的操作。注意,传入的参数必须实现了toString方法,不然日志在对对象进行转码时,只会打印出对象的内存地址,而不是对象中的具体内容 整体架构前文已经简单介绍了logback包含的三个主要模块,以及如何在代码中基于SLF4J FACADE自由的使用日志框架。下面开始从配置文件的角度来了解如何配置Logback。Logback主要支持XML和groovy结构的配置文件,下文中将以XML结构为基础进行介绍。 上图为官网中对Logback配置文件整体结构的描述。配置文件以<configuration>作为根元素,其下包含1个<root>元素用于定义根日志的配置信息,还有0到多个<logger>元素以及0到多个<appender>元素。其中<logger>元素对应了应用中通过LoggerFactory.getLogger()获取到的日志工具,<appender>元素定义了日志的输出目的地,一个<logger>可以关联多个<appender>,即允许将同样的一行日志输出到多个目的地。 ...

November 3, 2019 · 1 min · jiezi

log4g站在巨人的头上实现一个可配置的Go日志库

更多精彩博文,欢迎访问我的个人博客 前言本人Java程序员一枚,眼看着这几年Go的势头不错,本着技多不压身的原则,也随大流慢慢学习。不得不说Go其实跟Java差别还是挺大的,毕竟习惯了面向对象的思想,一时间也有点接受不过来。俗话说实践才能出真知,本想着拿刚学的点皮毛练练手,结果就遇到了一个问题:日志。 Go语言不像Java中有诸如Log4j的大哥存在,其自带的log库其实功能有限。虽然催生出了诸如logrus、zap等一系列优秀的三方日志库,但在github上找了半天始终没有找到一款符合自己需求的。 我需求的日志库功能本来是想要一个支持日志分割,并且支持通过配置将日志分级别输出到不同目录的功能(好吧我就是觉得log4j真不错),但很多三方库都不支持这个功能。诚然,将日志写入logstash或者数据库等已经越来越成为主流,但不能分割日志难免有些遗憾。 于是想着去网上copy一下,应该有现成的,但是找了一圈要么就是复制粘贴的,要么就跟我想要的效果不一样,于是便想着自己动手实现一个。logrus不是支持hook么,那还不好说么(大概吧)? 自己实现一个日志库想象中的实现应该是这个样子: 代码太麻烦就不粘贴了,成品已经放在github:https://github.com/jptangchin... 直接就可以使用: package mainimport log "github.com/jptangchina/log4g"func main() { log.Info("Test info output")}主要实现了如下功能: 配置文件配置输出行为,包括文件大小分割,时间分割,日志等级等当输出到文件时屏蔽logrus控制台输出不同级别的日志可以输出到不同文件不同级别日志可以输出到相同文件哎呀,说不清楚,可以自行体会总结总的来讲,要真完全自己写,还是挺难写的,好在前人做足了功夫。不过自己也才刚开始学习Go,肯定还写得不太好,发出来主要是大家交流交流。如果有同实现的更好的工具这里也求个推荐,我实在是找不到了。另外如果写得有不对的地方,可以指正,但是请不要杠,我也刚学,不要杠!杠精请走开! 更多精彩博文,欢迎访问我的个人博客

October 17, 2019 · 1 min · jiezi

基于bash-shell脚本制定灵活的trimlog日志清理策略

前言透过Zabbix文件系统监控告警发现日志未定时清理,进一步排查后确认前同事写的trimlog脚本在某些服务器上执行定时任务时出现了异常,review后发现代码逻辑本身没有问题,主要由于cron job在执行date参数时没有使用转义字符导致,不过trimlog.sh这个日志清理脚本代码结构清晰易读,做下共享方便大家参考和二次修改。 基于bash shell脚本制定灵活的trimlog日志清理策略更新历史2019年10月08日 - 初稿 阅读原文 - https://wsgzao.github.io/post... 扩展阅读 基于bash find命令执行log日志备份和清理的一次简单实践 问题描述# 检查crontabcat /etc/crontab#Ansible: gdp_trim_log0 1 * * * root /data/release/trimlog.sh '/data/release/log/gop_gdp' 30 1 '*' '%Y%m%d' >> /data/release/trimlog.txt0 1 * * * root /data/release/trimlog.sh '/data/release/log/cacheupdate' 30 1 '*' '%Y%m%d' >> /data/release/trimlog.txt#Ansible: gdp_trim_backup0 1 * * * root /data/release/trimbackup.sh '/data/release/backup/gop_gdp' 30 >> /data/release/trimbackup.txt# 检查crontab日志less /var/log/cronOct 8 01:00:01 sg-gop-10-71-12-146 CROND[183782]: (root) CMD (/data/release/trimlog.sh '/data/release/log/gop_gdp' 30 1 '*' ')Oct 8 01:00:01 sg-gop-10-71-12-146 CROND[183783]: (root) CMD (/data/release/trimbackup.sh '/data/release/backup/gop_gdp' 30 >> /data/release/trimbackup.txt)Oct 8 01:00:01 sg-gop-10-71-12-146 CROND[183784]: (root) CMD (/data/release/trimlog.sh '/data/release/log/cacheupdate' 30 1 '*' ')Oct 8 01:00:01 sg-gop-10-71-12-146 CROND[183786]: (root) CMD (/usr/lib64/sa/sa1 1 1)Oct 8 01:00:01 sg-gop-10-71-12-146 CROND[183788]: (root) CMD (cd /tmp && ss -tna | awk '{print $1}' | sort | uniq -c > snmpss.tmp && chcon system_u:object_r:snmpd_var_run_t:s0 snmpss.tmp && mv -f snmpss.tmp snmpss.cache)Oct 8 01:00:01 sg-gop-10-71-12-146 CROND[183779]: (root) CMDOUT (/bin/bash: -c: line 0: unexpected EOF while looking for matching `'')Oct 8 01:00:01 sg-gop-10-71-12-146 CROND[183779]: (root) CMDOUT (/bin/bash: -c: line 1: syntax error: unexpected end of file)Oct 8 01:00:01 sg-gop-10-71-12-146 CROND[183778]: (root) CMDOUT (/bin/bash: -c: line 0: unexpected EOF while looking for matching `'')Oct 8 01:00:01 sg-gop-10-71-12-146 CROND[183778]: (root) CMDOUT (/bin/bash: -c: line 1: syntax error: unexpected end of file)Oct 8 01:00:01 sg-gop-10-71-12-146 CROND[183777]: (root) CMDOUT (find: ‘/data/release/backup/gop_gdp’: No such file or directory)# 在crontab的date中加入转移字符即可0 1 * * * root /data/release/trimlog.sh '/data/release/log/gop_gdp' 30 1 '*' '\%Y\%m\%d' >> /data/release/trimlog.txt0 1 * * * root /data/release/trimlog.sh '/data/release/log/cacheupdate' 30 1 '*' '\%Y\%m\%d' >> /data/release/trimlog.txtbash shell代码vim /data/release/trimlog.sh#!/bin/bashdir=`dirname $0`if [ $# -lt 1 ]then echo "usage: $0 DIRECTORY [DELETE_DAYS] [COMPRESS_DAYS] [COMPRESS_FILE_PATTERN] [DATE_PATTERN]" echo "multiple matches example for COMPRESS_FILE_PATTERN: data|error" exitficurrent_date=$(date +"%Y-%m-%d %T")echo "Start trim log at $current_date "path=$1if [ $# -gt 1 ]then log_delete_days=$2else log_delete_days=7fiif [ $# -gt 2 ]then log_compress_days=$3else log_compress_days=0fiif [ $# -gt 3 ]then log_compress_match=$4else log_compress_match='*'fiif [ $# -gt 4 ]then date_pattern=$5else date_pattern="%Y-%m-%d"fiif [ $log_delete_days -gt 0 ]then files=`ls $path/*.log.*` i=0 while [ $i -le $log_delete_days ] do d=`date "+$date_pattern" --date "$i day ago"` files=`echo "$files"|grep -v $d` i=`expr $i + 1` done echo "$files"|xargs rm -ffiif [ $log_compress_days -gt 0 ] && [ -n "$log_compress_match" ]then files=`ls $path/*.log.*|grep -v "\.gz$"|grep -E "$log_compress_match"` i=0 while [ $i -lt $log_compress_days ] do d=`date "+$date_pattern" --date "$i day ago"` files=`echo "$files"|grep -v $d` i=`expr $i + 1` done echo "$files"|xargs gzipficurrent_date=$(date +"%Y-%m-%d %T")echo "Finish trim log at $current_date "vim /data/release/trimbackup.sh#!/bin/bashdir=`dirname $0`if [ $# -lt 1 ]then echo "usage: $0 DIRECTORY [KEEP_BACKUP_NUM]" exitficurrent_date=$(date +"%Y-%m-%d %T")echo "Start trim backup at $current_date "path=$1if [ $# -gt 1 ]then keep_backup_num=$2else keep_backup_num=10fii=0find $path -maxdepth 1 ! -path $path -type d -printf '%p\n' | sort -r -n | while read dir; do i=`expr $i + 1` if [ $i -gt $keep_backup_num ] then echo "del $dir" rm -rf $dir else echo "keep $dir" fidonecurrent_date=$(date +"%Y-%m-%d %T")echo "Finish trim backup at $current_date "

October 8, 2019 · 3 min · jiezi

阿里云InfluxDB®-Raft-HybridStorage实现方案

背景阿里云InfluxDB®是阿里云基于开源版InfluxDB打造的一款时序数据库产品,提供更稳定的持续运行状态、更丰富强大的时序数据计算能力。在现有的单节点版本之外,阿里云InfluxDB®团队还将推出多节点的高可用版本。 我们知道现有的开源版InfluxDB只提供单节点的能力,早期开源的集群版本功能不完善、且社区不再提供更新与支持。经过对官网商业版InfluxDB现有文档的研究,我们猜测在商业版InfluxDB集群方案中,meta信息集群是基于一致性协议Raft做同步的,而数据是异步复制的。这种分离的方式虽然有优点,但也引起了一系列的一致性问题,在一些公开的文档中,官方也承认这种数据复制方案并不令人满意。 因此,团队在参考多项技术选型后,决定采用最为广泛使用并有较长历史积累的ETCD/Raft作为核心组件实现阿里云InfluxDB®的Raft内核,对用户所有的写入或一致性读请求直接进行Raft同步(不做meta信息同步与数据写入在一致性过程中的拆分),保证多节点高可用版本拥有满足强一致性要求的能力。 有幸笔者参与到多节点的高可用版本的开发中,期间遇到非常多的挑战与困难。其中一项挑战是ETCD的Raft框架移植过程中,在移除了ETCD自身较为复杂、对时序数据库没有太多作用的Raft日志模块后,所带来的一系列问题。本文就业界Raft日志的几种不同实现方案做讨论,并提出一种自研的Raft HybridStorage方案。 业内方案ETCD由于我们采用了ETCD/Raft的方案,绕不开讨论一下ETCD本家的Raft日志实现方式。 官网对Raft的基本处理流程总结参考下图所示,协议细节本文不做扩展: 对于ETCD的Raft日志,主要包含两个主要部分:文件部分(WAL)、内存存储部分(MemoryStorage)。 文件部分(WAL),是ETCD Raft过程所用的日志文件。Raft过程中收到的日志条目,都会记录在WAL日志文件中。该文件只会追加,不会重写和覆盖。 内存存储部分(MemoryStorage),主要用于存储Raft过程用到的日志条目一段较新的日志,可能包含一部分已共识的日志和一些尚未共识的日志条目。由于是内存维护,可以灵活的重写替换。MemoryStorage有两种方式清理释放内存:第一种是compact操作,对appliedId之前的日志进行清理,释放内存;第二种是周期snapshot操作,该操作会创建snapshot那一时刻的ETCD全局数据状态并持久化,同时清理内存中的日志。 在最新的ETCD 3.3代码仓库中,ETCD已经将Raft日志文件部分(WAL)和Raft日志内存存储部分(MemoryStorage)都抽象提升到了与Raft节点(Node)、Raft节点id以及Raft集群其他节点信息(*membership.RaftCluster)平级的Server层级,这与老版本的ETCD代码架构有较大区别,在老版本中Raft WAL与MemoryStorage都仅仅只是Raft节点(Node)的成员变量。 一般情况下,一条Raft日志的文件部分与内存存储部分配合产生作用,写入时先写进WAL,保证持久化;随之马上追加到MemoryStorage中,保证热数据的高效读取。 无论是文件部分还是内存存储部分,其存储的主要数据结构一致,都是raftpb.Entry。一条log Entry主要包含以下几个信息: 参数描述Termleader的任期号Index当前日志索引Type日志类型Data日志内容此外,ETCD Raft日志的文件部分(WAL)还会存储针对ETCD设计的一些额外信息,比如日志类型、checksum等等。 CockroachDBCockroachDB是一款开源的分布式数据库,具有NoSQL对海量数据的存储管理能力,又保持了传统数据库支持的ACID和SQL等,还支持跨地域、去中 心、高并发、多副本强一致和高可用等特性。 CockroachDB的一致性机制也是基于Raft协议:单个Range的多个副本通过Raft协议进行数据同步。Raft协议将所有的请求以Raft Log的形式串行化并由Leader同步给Follower,当绝大多数副本写Raft Log成功后,该Raft Log会标记为Committed状态,并Apply到状态机。 我们来分析一下CockroachDB Raft机制的关键代码,可以很明显的观察到也是从鼻祖ETCD的Raft框架移植而来。但是CockroachDB删除了ETCD Raft日志的文件存储部分,将Raft日志全部写入RocksDB,同时自研一套热数据缓存(raftentry.Cache),利用raftentry.Cache与RocksDB自身的读写能力(包括RocksDB的读缓存)来保证对日志的读写性能。 此外,Raft流程中的创建snapshot操作也是直接保存到RocksDB。这样实现的原因,个人推测是可能由于CockroachDB底层数据存储使用的就是RocksDB,直接使用RocksDB的能力读写WAL或者存取snapshot相对简单,不需要再额外开发适用于CockroachDB特性的Raft日志模块了。 自研HybridStorage移除snapshot在阿里云InfluxDB多节点高可用方案实现过程中,我们采用了ETCD/Raft作为核心组件,根据移植过程中的探索与InfluxDB实际需要,移除了原生的snapshot过程。同时放弃原生的日志文件部分WAL,而改用自研方案。 为什么移除snapshot呢?原来在Raft的流程中,为了防止Raft日志的无限增加,会每隔一段时间做snapshot,早于snapshot index的Raft日志请求,将直接用snapshot回应。然而我们的单Raft环架构如果要做snapshot,就是对整个InfluxDB做,将非常消耗资源和影响性能,而且过程中要锁死整个InfluxDB,这都是不能让人接受的。所以我们暂时不启用snapshot功能,而是存储固定数量的、较多的Raft日志文件备用。 自研的Raft日志文件模块会周期清理最早的日志防止磁盘开销过大,当某个节点下线的时间并不过长时,其他正常节点上存储的日志文件如果充足,则足够满足它追取落后的数据。但如果真的发生单节点宕机太长,正常节点的日志文件已出现被清理而不足故障节点追取数据时,我们将利用InfluxDB的backup和restore工具,将落后节点还原至被Raft日志涵盖的较新的状态,然后再做追取。 在我们的场景下,ETCD自身的WAL模块并不适用于InfluxDB。ETCD的WAL是纯追加模式的,当故障恢复时,正常节点要相应落后节点的日志请求时,就有必要分析并提取出相同index且不同term中那条最新的日志,同时InfluxDB的一条entry可能包含超过20M的时序数据,这对于非kv模式的时序数据库而言是非常大的磁盘开销。 HybridStorage设计我们自研的Raft日志模块命名为HybridStorage,即意为内存与文件混合存取,内存保留最新热数据,文件保证全部日志落盘,内存、文件追加操作高度一致。 HybridStorage的设计思路是这样的: (1)保留MemoryStorage:为了保持热数据的读取效率,内存中的MemoryStorage会保留作为热数据cache提升性能,但是周期清理其中最早的数据,防止内存消耗过大。 (2)重新设计WAL:WAL不再是像ETCD那样的纯追加模式、也不需要引入类似RocksDB这样重的读写引擎。新增的日志在MemoryStorage与WAL都会保存,WAL文件中最新内容始终与MemoryStorage保持完全一致。 一般情况下,HybridStorage新增不同index的日志条目时,需要在写内存日志时同时操作文件执行类似的增减。正常写入流程如下图所示: 当出现了同index不同term的日志条目的情况,此时执行truncate操作,截断对应文件位置之后一直到文件尾部的全部日志,然后重新用append方式写入最新term编号的日志,操作逻辑上十分清晰,不存在Update文件中间的某个位置的操作。 例如在一组Raft日志执行append操作时,出现了如下图所示的同index(37、38、39)不同term的日志条目的情况。在MemoryStorage的处理方式是:找到对应index位置的内存位置(内存位置37),并抛弃从位置A以后的全部旧日志占用的内存数据(因为在Raft机制中,这种情况下内存位置37以后的那些旧日志都是无效的,无需保留),然后拼接上本次append操作的全部新日志。在自研WAL也需要执行类似的操作,找到WAL文件中对应index的位置(文件位置37),删除从文件位置37之后的所有文件内容,并写入最新的日志。如下图分析: 方案对比ETCD的方案,Raft日志有2个部分,文件与内存,文件部分因为只有追加模式,因此并不是每一条日志都是有效的,当出现同index不同term的日志条目时,只有最新的term之后的日志是生效的。配合snapshot机制,非常适合ETCD这样的kv存储系统。但对于InfluxDB高可用版本而言,snapshot将非常消耗资源和影响性能,而且过程中要锁死整个InfluxDB。同时,一次Raft流程的一条entry可能包含超过20M的时序数据。所以这种方案不适合。 CockroachDB的方案,看似偷懒使用了RocksDB的能力,但因其底层存储引擎也是RocksDB,所以无何厚非。但对于我们这样需要Raft一致性协议的时序数据库而言,引入RocksDB未免过重了。 自研的Raft HybridStorage是比较符合阿里云InfluxDB®的场景的,本身模块设计轻便简介,内存保留了热数据缓存,文件使用接近ETCD append only的方式,遇到同index不同term的日志条目时执行truncate操作,删除冗余与无效数据,降低了磁盘压力。 总结本文对比了业内常见的两种Raft日志的实现方案,也展示了阿里云InfluxDB®团队自研的HybridStorage方案。在后续开发过程中,团队内还会对自研Raft HybridStorage进行多项优化,例如异步写、日志文件索引、读取逻辑优化等等。也欢迎读者提出自己的解决方案。相信阿里云InfluxDB®团队在技术积累与沉淀方面会越做越好,成为时序数据库技术领导者。 本文作者:德施阅读原文 本文为云栖社区原创内容,未经允许不得转载。

July 11, 2019 · 1 min · jiezi

容器日志采集利器filebeat深度剖析与实践

在云原生时代和容器化浪潮中,容器的日志采集是一个看起来不起眼却又无法忽视的重要议题。对于容器日志采集我们常用的工具有filebeat和fluentd,两者对比各有优劣,相比基于ruby的fluentd,考虑到可定制性,我们一般默认选择golang技术栈的filbeat作为主力的日志采集agent。 相比较传统的日志采集方式,容器化下单节点会运行更多的服务,负载也会有更短的生命周期,而这些更容易对日志采集agent造成压力,虽然filebeat足够轻量级和高性能,但如果不了解filebeat的机制,不合理的配置filebeat,实际的生产环境使用中可能也会给我们带来意想不到的麻烦和难题。 整体架构日志采集的功能看起来不复杂,主要功能无非就是找到配置的日志文件,然后读取并处理,发送至相应的后端如elasticsearch,kafka等。 filebeat官网有张示意图,如下所示:针对每个日志文件,filebeat都会启动一个harvester协程,即一个goroutine,在该goroutine中不停的读取日志文件,直到文件的EOF末尾。一个最简单的表示采集目录的input配置大概如下所示: filebeat.inputs:- type: log # Paths that should be crawled and fetched. Glob based paths. paths: - /var/log/*.log不同的harvester goroutine采集到的日志数据都会发送至一个全局的队列queue中,queue的实现有两种:基于内存和基于磁盘的队列,目前基于磁盘的队列还是处于alpha阶段,filebeat默认启用的是基于内存的缓存队列。 每当队列中的数据缓存到一定的大小或者超过了定时的时间(默认1s),会被注册的client从队列中消费,发送至配置的后端。目前可以设置的client有kafka、elasticsearch、redis等。 虽然这一切看着挺简单,但在实际使用中,我们还是需要考虑更多的问题,例如: 日志文件是如何被filbebeat发现又是如何被采集的?filebeat是如何确保日志采集发送到远程的存储中,不丢失一条数据的?如果filebeat挂掉,下次采集如何确保从上次的状态开始而不会重新采集所有日志?filebeat的内存或者cpu占用过多,该如何分析解决?filebeat如何支持docker和kubernetes,如何配置容器化下的日志采集?想让filebeat采集的日志发送至的后端存储,如果原生不支持,怎样定制化开发?这些均需要对filebeat有更深入的理解,下面让我们跟随filebeat的源码一起探究其中的实现机制。 一条日志是如何被采集的filebeat源码归属于beats项目,而beats项目的设计初衷是为了采集各类的数据,所以beats抽象出了一个libbeat库,基于libbeat我们可以快速的开发实现一个采集的工具,除了filebeat,还有像metricbeat、packetbeat等官方的项目也是在beats工程中。 如果我们大致看一下代码就会发现,libbeat已经实现了内存缓存队列memqueue、几种output日志发送客户端,数据的过滤处理processor等通用功能,而filebeat只需要实现日志文件的读取等和日志相关的逻辑即可。 从代码的实现角度来看,filebeat大概可以分以下几个模块: input: 找到配置的日志文件,启动harvesterharvester: 读取文件,发送至spoolerspooler: 缓存日志数据,直到可以发送至publisherpublisher: 发送日志至后端,同时通知registrarregistrar: 记录日志文件被采集的状态1. 找到日志文件对于日志文件的采集和生命周期管理,filebeat抽象出一个Crawler的结构体,在filebeat启动后,crawler会根据配置创建,然后遍历并运行每个input: for _, inputConfig := range c.inputConfigs { err := c.startInput(pipeline, inputConfig, r.GetStates()) }在每个input运行的逻辑里,首先会根据配置获取匹配的日志文件,需要注意的是,这里的匹配方式并非正则,而是采用linux glob的规则,和正则还是有一些区别。 matches, err := filepath.Glob(path)获取到了所有匹配的日志文件之后,会经过一些复杂的过滤,例如如果配置了exclude_files则会忽略这类文件,同时还会查询文件的状态,如果文件的最近一次修改时间大于ignore_older的配置,也会不去采集该文件。 2. 读取日志文件匹配到最终需要采集的日志文件之后,filebeat会对每个文件启动harvester goroutine,在该goroutine中不停的读取日志,并发送给内存缓存队列memqueue。 在(h *Harvester) Run()方法中,我们可以看到这么一个无限循环,省略了一些逻辑的代码如下所示: for { message, err := h.reader.Next() if err != nil { switch err { case ErrFileTruncate: logp.Info("File was truncated. Begin reading file from offset 0: %s", h.state.Source) h.state.Offset = 0 filesTruncated.Add(1) case ErrRemoved: logp.Info("File was removed: %s. Closing because close_removed is enabled.", h.state.Source) case ErrRenamed: logp.Info("File was renamed: %s. Closing because close_renamed is enabled.", h.state.Source) case ErrClosed: logp.Info("Reader was closed: %s. Closing.", h.state.Source) case io.EOF: logp.Info("End of file reached: %s. Closing because close_eof is enabled.", h.state.Source) case ErrInactive: logp.Info("File is inactive: %s. Closing because close_inactive of %v reached.", h.state.Source, h.config.CloseInactive) default: logp.Err("Read line error: %v; File: %v", err, h.state.Source) } return nil } ... if !h.sendEvent(data, forwarder) { return nil }}可以看到,reader.Next()方法会不停的读取日志,如果没有返回异常,则发送日志数据到缓存队列中。 返回的异常有几种类型,除了读取到EOF外,还会有例如文件一段时间不活跃等情况发生会使harvester goroutine退出,不再采集该文件,并关闭文件句柄。 filebeat为了防止占据过多的采集日志文件的文件句柄,默认的close_inactive参数为5min,如果日志文件5min内没有被修改,上面代码会进入ErrInactive的case,之后该harvester goroutine会被关闭。 这种场景下还需要注意的是,如果某个文件日志采集中被移除了,但是由于此时被filebeat保持着文件句柄,文件占据的磁盘空间会被保留直到harvester goroutine结束。 ...

July 10, 2019 · 3 min · jiezi

使用Spark-Streaming-SQL基于时间窗口进行数据统计

1.背景介绍流式计算一个很常见的场景是基于事件时间进行处理,常用于检测、监控、根据时间进行统计等系统中。比如埋点日志中每条日志记录了埋点处操作的时间,或者业务系统中记录了用户操作时间,用于统计各种操作处理的频率等,或者根据规则匹配,进行异常行为检测或监控系统告警。这样的时间数据都会包含在事件数据中,需要提取时间字段并根据一定的时间范围进行统计或者规则匹配等。使用Spark Streaming SQL可以很方便的对事件数据中的时间字段进行处理,同时Spark Streaming SQL提供的时间窗口函数可以将事件时间按照一定的时间区间对数据进行统计操作。本文通过讲解一个统计用户在过去5秒钟内点击网页次数的案例,介绍如何使用Spark Streaming SQL对事件时间进行操作。 2.时间窗语法说明Spark Streaming SQL支持两类窗口操作:滚动窗口(TUMBLING)和滑动窗口(HOPPING)。 2.1滚动窗口 滚动窗口(TUMBLING)根据每条数据的时间字段将数据分配到一个指定大小的窗口中进行操作,窗口以窗口大小为步长进行滑动,窗口之间不会出现重叠。例如:如果指定了一个5分钟大小的滚动窗口,数据会根据时间划分到 [0:00 - 0:05)、 [0:05, 0:10)、[0:10, 0:15)等窗口。 语法GROUP BY TUMBLING ( colName, windowDuration ) 示例对inventory表的inv_data_time时间列进行窗口操作,统计inv_quantity_on_hand的均值;窗口大小为1分钟。 SELECT avg(inv_quantity_on_hand) qohFROM inventoryGROUP BY TUMBLING (inv_data_time, interval 1 minute)2.2滑动窗口 滑动窗口(HOPPING),也被称作Sliding Window。不同于滚动窗口,滑动窗口可以设置窗口滑动的步长,所以窗口可以重叠。滑动窗口有两个参数:windowDuration和slideDuration。slideDuration为每次滑动的步长,windowDuration为窗口的大小。当slideDuration < windowDuration时窗口会重叠,每个元素会被分配到多个窗口中。所以,滚动窗口其实是滑动窗口的一种特殊情况,即slideDuration = windowDuration则等同于滚动窗口。 语法GROUP BY HOPPING ( colName, windowDuration, slideDuration ) 示例对inventory表的inv_data_time时间列进行窗口操作,统计inv_quantity_on_hand的均值;窗口为1分钟,滑动步长为30秒。 SELECT avg(inv_quantity_on_hand) qohFROM inventoryGROUP BY HOPPING (inv_data_time, interval 1 minute, interval 30 second)3.系统架构 业务日志收集到Aliyun SLS后,Spark对接SLS,通过Streaming SQL对数据进行处理并将统计后的结果写入HDFS中。后续的操作流程主要集中在Spark Streaming SQL接收SLS数据并写入HDFS的部分,有关日志的采集请参考日志服务。 4.操作流程4.1环境准备 ...

July 8, 2019 · 1 min · jiezi

使用-requestId-标记全链路日志

标记全链路日志有助于更好的解决 bug 和分析接口性能,本篇文章使用 node 来作为示例 代码示例本文地址github当一个请求到来时,会产生哪些日志本次请求报文本次请求涉及到的数据库操作本次请求涉及到的缓存操作本次请求涉及到的服务请求本次请求所遭遇的异常本次请求执行的关键函数本次请求所对应的响应体如何查询本次从请求到响应全链路的所有日志使用 requestId 唯一标识每个请求,有时它又被称为 sessionId 或者 transactionId。 使用 requestId 标记每次请求全链路日志,所要标记的日志种类如上所示通过把 X-Request-Id (X-Session-Id) 标记在请求头中,在整个链路进行传递async function context (ctx: KoaContext, next: any) { const requestId = ctx.header['x-request-id'] || uuid() ctx.res.setHeader('requestId', requestId) ctx.requestId = requestId await next()}app.use('/todos/:id', (ctx) => { User.findByPk(ctx.body.id, { logging () { // log ctx.requestId } })})如何以侵入性更小的方式来标记每次请求如上,在每次数据库查询时手动对 requestId 进行标记过于繁琐。可以统一设计 logger 函数进行标记 具体代码可见我一个脚手架中的 logger.ts 这里使用了流行的日志库 winston (13582 Star) import winston, { format } from 'winston'const requestId = format((info) => { info.requestId = session.get('requestId') return info})const logger = winston.createLogger({ format: format.combine( format.timestamp(), requestId(), format.json() )})如何在 logger.ts 中绑定 requestId或者说如何在 logger.ts 如何获得整个请求响应生命周期中的 requestId ...

July 6, 2019 · 1 min · jiezi

云原生应用-Kubernetes-监控与弹性实践

前言   云原生应用的设计理念已经被越来越多的开发者接受与认可,而Kubernetes做为云原生的标准接口实现,已经成为了整个stack的中心,云服务的能力可以通过Cloud Provider、CRD Controller、Operator等等的方式从Kubernetes的标准接口向业务层透出。开发者可以基于Kubernetes来构建自己的云原生应用与平台,Kubernetes成为了构建平台的平台。今天我们会向大家介绍一个云原生应用该如何在Kubernetes中无缝集成监控和弹性能力。 本文整理自由阿里云容器平台技术专家 刘中巍(莫源)在 KubeCon 分享的《Cloud Native Application monitoring and autoscaling in kubernetes》演讲。获取 KubeCon 全部阿里演讲PPT,关注阿里巴巴云原生公众号,微信菜单栏点击 PPT下的“获取PPT” 阿里云容器服务Kubernetes的监控总览 云服务集成  阿里云容器服务Kubernetes目前已经和四款监控云服务进行了打通,分别是SLS(日志服务)、ARMS(应用性能监控)、AHAS(架构感知监控服务)、Cloud Monitor(云监控)。 SLS主要负责日志的采集、分析。在阿里云容器服务Kubernetes中,SLS可以采集三种不同类型的日志 APIServer等核心组件的日志Service Mesh/Ingress等接入层的日志应用的标准日志  除了采集日志的标准链路外,SLS还提供了上层的日志分析能力,默认提供了基于APIServer的审计分析能力、接入层的可观测性展现、应用层的日志分析。在阿里云容器服务Kubernetes中,日志组件已经默认安装,开发者只需要通过在集群创建时勾选即可。 ARMS主要负责采集、分析、展现应用的性能指标。目前主要支持Java与PHP两种语言的集成,可以采集虚拟机(JVM)层的指标,例如GC的次数、应用的慢SQL、调用栈等等。对于后期性能调优可以起到非常重要的作用。 AHAS是架构感知监控,通常在Kubernetes集群中负载的类型大部分为微服务,微服务的调用拓扑也会比较复杂,因此当集群的网络链路出现问题时,如何快速定位问题、发现问题、诊断问题则成为了最大的难题。AHAS通过网络的流量和走向,将集群的拓扑进行展现,提供更高层次的问题诊断方式。 开源方案集成开源方案的兼容和集成也是阿里云容器服务Kubernetes监控能力的一部分。主要包含如下两个部分: Kubernetes内置监控组件的增强与集成  在kubernetes社区中,heapster/metrics-server是内置的监控方案,而且例如Dashboard、HPA等核心组件会依赖于这些内置监控能力提供的metrics。由于Kubernetes生态中组件的发布周期和Kubernetes的release不一定保证完整的同步,这就造成了部分监控能力的消费者在Kubernetes中存在监控问题。因此阿里云就这个问题做了metrics-server的增强,实现版本的兼容。此外针对节点的诊断能力,阿里云容器服务增强了NPD的覆盖场景,支持了FD文件句柄的监测、NTP时间同步的校验、出入网能力的校验等等,并开源了eventer,支持离线Kubernetes的事件数据到SLS、kafka以及钉钉,实现ChatOps。 Prometheus生态的增强与集成   Promethes作为Kubernetes生态中三方监控的标准,阿里云容器服务也提供了集成的Chart供开发者一键集成。此外,我们还在如下三个层次作了增强: 存储、性能增强:支持了产品级的存储能力支持(TSDB、InfluxDB),提供更持久、更高效的监控存储与查询。采集指标的增强:修复了部分由于Prometheus自身设计缺欠造成的监控不准的问题,提供了GPU单卡、多卡、共享分片的exporter。提供上层可观测性的增强:支持场景化的CRD监控指标集成,例如argo、spark、tensorflow等云原生的监控能力,支持多租可观测性。阿里云容器服务Kubernetes的弹性总览   阿里云容器服务Kubernetes主要包含如下两大类弹性组件:调度层弹性组件与资源层弹性组件。 调度层弹性组件  调度层弹性组件是指所有的弹性动作都是和Pod相关的,并不关心具体的资源情况。 HPAHPA是Pod水平伸缩的组件,除了社区支持的Resource Metrics和Custom Metrics,阿里云容器服务Kubernetes还提供了external-metrics-adapter,支持云服务的指标作为弹性伸缩的判断条件。目前已经支持例如:Ingress的QPS、RT,ARMS中应用的GC次数、慢SQL次数等等多个产品不同维度的监控指标。 VPA VPA是Pod的纵向伸缩的组件,主要面向有状态服务的扩容和升级场景。    cronHPA cronHPA是定时伸缩组件,主要面向的是周期性负载,通过资源画像可以预测有规律的负载周期,并通过周期性伸缩,实现资源成本的节约。 ResizerResizer是集群核心组件的伸缩控制器,可以根据集群的CPU核数、节点的个数,实现线性和梯度两种不同的伸缩,目前主要面对的场景是核心组件的伸缩,例如:CoreDNS。 资源层弹性组件资源层弹性组件是指弹性的操作都是针对于Pod和具体资源关系的。 Cluster-Autoscaler Cluster-Autoscaler是目前比较成熟的节点伸缩组件,主要面向的场景是当Pod资源不足时,进行节点的伸缩,并将无法调度的Pod调度到新弹出的节点上。 virtual-kubelet-autoscaler virtual-kubelet-autoscaler是阿里云容器服务Kubernetes开源的组件,和Cluster-Autoscaler的原理类似,当Pod由于资源问题无法调度时,此时弹出的不是节点,而是将Pod绑定到虚拟节点上,并通过ECI的方式将Pod进行启动。 Demo Show Case  最后给大家进行一个简单的Demo演示:应用主体是apiservice,apiservice会通sub-apiservice调用database,接入层通过ingress进行管理。我们通过PTS模拟上层产生的流量,并通过SLS采集接入层的日志,ARMS采集应用的性能指标,并通过alibaba-cloud-metrics-adapster暴露external metrics触发HPA重新计算工作负载的副本,当伸缩的Pod占满集群资源时,触发virtual-kubelet-autoscaler生成ECI承载超过集群容量规划的负载。 总结  在阿里云容器服务Kubernetes上使用监控和弹性的能力是非常简单的,开发者只需一键安装相应的组件Chart即可完成接入,通过多维度的监控、弹性能力,可以让云原生应用在最低的成本下获得更高的稳定性和鲁棒性。 本文作者:jessie筱姜阅读原文 本文为云栖社区原创内容,未经允许不得转载。

July 4, 2019 · 1 min · jiezi

SLS机器学习最佳实战批量时序异常检测

1. 高频检测场景1.1 场景一集群中有N台机器,每台机器中有M个时序指标(CPU、内存、IO、流量等),若单独的针对每条时序曲线做建模,要手写太多重复的SQL,且对平台的计算消耗特别大。该如何更好的应用SQL实现上述的场景需求? 1.2 场景二针对系统中的N条时序曲线进行异常检测后,有要如何快速知道:这其中有哪些时序曲线是有异常的呢? 2. 平台实验2.1 解决一针对场景一中描述的问题,我们给出如下的数据约束。其中数据在日志服务的LogStore中按照如下结构存储: timestamp : unix_time_stampmachine: name1metricName: cpu0metricValue: 50---timestamp : unix_time_stampmachine: name1metricName: cpu1metricValue: 50---timestamp : unix_time_stampmachine: name1metricName: memmetricValue: 50---timestamp : unix_time_stampmachine: name2metricName: memmetricValue: 60在上述的LogStore中我们先获取N个指标的时序信息: * | select timestamp - timestamp % 60 as time, machine, metricName, avg(metricValue) from log group by time, machine, metricName现在我们针对上述结果做批量的时序异常检测算法,并得到N个指标的检测结果: * | select machine, metricName, ts_predicate_aram(time, value, 5, 1, 1) as res from ( select timestamp - timestamp % 60 as time, machine, metricName, avg(metricValue) as value from log group by time, machine, metricName )group by machine, metricName通过上述SQL,我们得到的结果的结构如下 ...

July 1, 2019 · 2 min · jiezi

云上的Growth-hacking之路打造产品的增长引擎

增长关乎产品的存亡增长!增长!增长!业务增长是每一个创业者每天面临的最大问题。无论你的产品是APP,还是web,或者是小程序,只能不断的维持用户的增长,才能向资本市场讲出一个好故事,融资活下去。活到最后的产品,才有机会盈利。 为了获取用户的增长,可以投放广告,也可以内容营销、社交传播、销售地推,或者持续的专注于产品优化。无论哪一种方式,我们都面临这几个问题: 运营活动,覆盖了多少用户?多少用户,开始使用产品?多少用户付费?多少用户持续的活跃?下一步,我们应该把精力放在哪些方面?是持续运营?还是开发新功能? 如果不能回答这些问题,无疑我们的运营活动或者开发就是盲人摸象,完全靠运气。为了解答这些问题,我们不妨关注一下growth hacking这种数据驱动的手段。 Growth Hacker的核心思想传统的市场营销策略,例如投放电视广告,覆盖了多少人,有多少人看过广告后进行了购买,多少人进行了复购,没有准确的数据进行衡量,只能依赖于资深专家根据经验判断。在互联网行业,每一个产品都是新的,前所未有的。每一个产品能不能存活,每一次运营的效果如何,没有多少经验可供借鉴,结果是不确定的。 GrowthHacking是兴起于硅谷的创业公司的marketing手段,旨在使用少量预算获得巨量增长。由于其极高的性价比和有效性,非常适合于创业公司,因而得到了广泛传播。 Growth Hacker的核心思想是通过数据指标,驱动运营决策,以及优化产品。Growthacker通过关注用户获取、用户转化、用户留存、用户推荐、盈利等核心的一系列指标,以及通过各种维度拆解,分析出下一步的增长决策。通过Growth Hacking,打造一个产品增长策略的闭环。 那么我们如何才能搭建出GrowthHacking架构,为自己的产品赋能呢? GrowthHacking之架构Growth Hacking 包含了数据的采集、存储、分析、报表、A/B test等系统,首先我们来看,传统的解决方案,搭建出GrowthHacking有哪些痛点: 搭建运营体系的痛点搭建运营体系的过程中,常常面临以下问题: 缺少数据,数据散落在各个地方,有的是app数据,有的是web数据,有的是小程序数据,没有一个统一的架构来把数据采集到一个地方。缺少一个分析平台。传统的策略,需要运维团队帮助搭建hadoop集群,需要专门团队持久运维。离线跑报表,一晚上才能拿到一次结果,周期太长。手工跑一次,几个小时过去了,有什么新的想法,不能及时验证。严重影响运营效率。借助云服务搭建的GrowthHacking技术架构为了解决以上问题, 日志服务提供了日志采集、存储、交互分析、可视化的一整套基础设施,可以帮助用户快速搭建出来灵活易用的Growthing Hacking的技术架构,每天的工作只需要专注于运营分析即可。 Growth Hacking首先从数据采集开始,定义清楚要采集的日志内容、格式。把各个终端、服务器的日志集中采集到云端的日志服务。后续通过日志服务提供的SQL实时分析功能,交互式的分析。定义一些常规报表,每日打开报表自动计算最新结果,也可以定义报告,自动发送最新报表。全部功能参考用户手册 此外,除了日志数据的分析,还可以为用户定义一些标签,存储在rds中,通过rds和日志的联合分析,挖掘不同标签对应的指标。 日志服务有如下特点: 免运维:一次完成数据的埋点、数据接入,之后只需专注于运营分析即可,无需专门的运维团队。实时性:用SQL实时计算,秒级响应。快人一步得到分析结果。灵活性:任意调整SQL,实时获取结果,非常适合交互式分析。弹性:遇到运营活动,流量突然暴涨,动动手指快速扩容。性价比:市场上常见的分析类产品,多采用打包价格,限制使用量。日志服务按量付费,价格更低,功能更强大。借助于日志服务提供的这套数据采集、存储、分析的基础设施。运营者可以从繁重的数据准备工作重解脱出来,专注于使用SQL去分析数据,配置报表,验证运营想法。 开始搭建GrowthHacking系统具体而言,Growth Hacking的架构可以拆分如下: 数据收集 定义埋点的规范,定义要采集的事件内容、字段、格式。通过Android SDK,iOS SDK, Web tracking等手段在客户端埋点。存储 选择日志服务的region。定义每一种日志存储的Project & LogStore。分析 开启分析之路,定义常规报表,或者交互式分析。通过分析结果,调整运营策略,有针对性的优化产品。基于日志服务,可以完成Growth Hacking的分析策略: 定义北极星指标。拉新分析。留存分析。事件分析。漏斗分析。用户分群。A/B test。在日志服务中,可以通过定义一系列仪表盘,来沉淀数据分析的结果。接下来的几篇文章中,将依次介绍如何在日志服务实现上述几种策略。 总结本文主要介绍Growth Hacking的整体架构,之后将用一系列文章介绍step by step如何介入数据,如何分析数据。 本文作者:云雷 阅读原文 本文为云栖社区原创内容,未经允许不得转载。

June 25, 2019 · 1 min · jiezi

揭秘每秒千万级的实时数据处理是怎么实现的

1、设计背景闲鱼目前实际生产部署环境越来越复杂,横向依赖各种服务盘宗错节,纵向依赖的运行环境也越来越复杂。当服务出现问题的时候,能否及时在海量的数据中定位到问题根因,成为考验闲鱼服务能力的一个严峻挑战。 线上出现问题时常常需要十多分钟,甚至更长时间才能找到问题原因,因此一个能够快速进行自动诊断的系统需求就应用而生,而快速诊断的基础是一个高性能的实时数据处理系统。 这个实时数据处理系统需要具备如下的能力: 1、数据实时采集、实时分析、复杂计算、分析结果持久化。2、可以处理多种多样的数据。包含应用日志、主机性能监控指标、调用链路图。3、高可靠性。系统不出问题且数据不能丢。4、高性能,底延时。数据处理的延时不超过3秒,支持每秒千万级的数据处理。 本文不涉及问题自动诊断的具体分析模型,只讨论整体实时数据处理链路的设计。 2、输入输出定义为了便于理解系统的运转,我们定义该系统整体输入和输出如下:  输入: 服务请求日志(包含traceid、时间戳、客户端ip、服务端ip、耗时、返回码、服务名、方法名)        环境监控数据(指标名称、ip、时间戳、指标值)。比如cpu、 jvm gc次数、jvm gc耗时、数据库指标。 输出: 一段时间内的某个服务出现错误的根因,每个服务的错误分析结果用一张有向无环图表达。(根节点即是被分析的错误节点,叶子节点即是错误根因节点。叶子节点可能是一个外部依赖的服务错误也可能是jvm异常等等)。 3、架构设计 在实际的系统运行过程中,随着时间的推移,日志数据以及监控数据是源源不断的在产生的。每条产生的数据都有一个自己的时间戳。而实时传输这些带有时间戳的数据就像水在不同的管道中流动一样。 如果把源源不断的实时数据比作流水,那数据处理过程和自来水生产的过程也是类似的: 自然地,我们也将实时数据的处理过程分解成采集、传输、预处理、计算、存储几个阶段。 整体的系统架构设计如下: 采集采用阿里自研的sls日志服务产品(包含logtail+loghub组件),logtail是采集客户端,之所以选择logtail是因为其优秀的性能、高可靠性以及其灵活插件扩展机制,闲鱼可以定制自己的采集插件实现各种各样数据的实时采集。 传输loghub可以理解为一个数据发布订阅组件,和kafka的功能类似,作为一个数据传输通道其更稳定、更安全,详细对比文章参考:https://yq.aliyun.com/articles/35979?spm=5176.10695662.1996646101.searchclickresult.6f2c7fbe6g3xgP 预处理实时数据预处理部分采用blink流计算处理组件(开源版本叫做flink,blink是阿里在flink基础上的内部增强版本)。目前常用的实时流计算开源产品有Jstorm、SparkStream、Flink。Jstorm由于没有中间计算状态的,其计算过程中需要的中间结果必然依赖于外部存储,这样会导致频繁的io影响其性能;SparkStream本质上是用微小的批处理来模拟实时计算,实际上还是有一定延时;Flink由于其出色的状态管理机制保证其计算的性能以及实时性,同时提供了完备SQL表达,使得流计算更容易。  计算与持久化数据经过预处理后最终生成调用链路聚合日志和主机监控数据,其中主机监控数据会独立存储在tsdb时序数据库中,供后续统计分析。tsdb由于其针对时间指标数据的特别存储结构设计,非常适合做时序数据的存储与查询。调用链路日志聚合数据,提供给cep/graph service做诊断模型分析。cep/graph service是闲鱼自研的一个应用,实现模型分析、复杂的数据处理以及外部服务进行交互,同时借助rdb实现图数据的实时聚合。 最后cep/graph service分析的结果作为一个图数据,实时转储在lindorm中提供在线查询。lindorm可以看作是增强版的hbase,在系统中充当持久化存储的角色。 4、设计细节与性能优化采集日志和指标数据采集使用logtail,整个数据采集过程如图: 其提供了非常灵活的插件机制,共有四种类型的插件: inputs: 输入插件,获取数据。processors: 处理插件,对得到的数据进行处理。aggregators: 聚合插件,对数据进行聚合。flushers: 输出插件,将数据输出到指定 sink。由于指标数据(比如cpu、内存、jvm指标)的获取需要调用本地机器上的服务接口获取,因此应尽量减少请求次数,在logtail中,一个input占用一个goroutine。闲鱼通过定制input插件和processors插件,将多个指标数据(比如cpu、内存、jvm指标)在一个input插件中通过一次服务请求获取(指标获取接口由基础监控团队提供),并将其格式化成一个json数组对象,在processors插件中再拆分成多条数据,以减少系统的io次数同时提升性能。 传输数据传输使用LogHub,logtail写入数据后直接由blink消费其中的数据,只需设置合理的分区数量即可。分区数要大于等于bink读取任务的并发数,避免blink中的任务空转。 预处理预处理主要采用bink实现,主要的设计和优化点: 1:编写高效的计算流程blink是一个有状态的流计算框架,非常适合做实时聚合、join等操作。在我们的应用中只需要关注出现错误的的请求上相关服务链路的调用情况,因此整个日志处理流分成两个流:a、服务的请求入口日志作为一个单独的流来处理,筛选出请求出错的数据。b、其他中间链路的调用日志作为另一个独立的流来处理,通过和上面的流join on traceid实现出错服务依赖的请求数据塞选。  如上图所示通过双流join后,输出的就是所有发生请求错误相关链路的完整数据。 2:设置合理的state生存周期blink在做join的时候本质上是通过state缓存中间数据状态,然后做数据的匹配。而如果state的生命周期太长会导致数据膨胀影响性能,如果state的生命周期太短就会无法正常关联出部分延迟到来的数据,所以需要合理的配置state生存周期,对于该应用允许最大数据延迟为1分钟。 使用niagara作为statebackend,以及设定state数据生命周期,单位毫秒state.backend.type=niagarastate.backend.niagara.ttl.ms=600003:开启 MicroBatch/MiniBatchMicroBatch 和 MiniBatch 都是微批处理,只是微批的触发机制上略有不同。原理上都是缓存一定的数据后再触发处理,以减少对 state 的访问从而显著提升吞吐,以及减少输出数据量。 开启joinblink.miniBatch.join.enabled=true使用 microbatch 时需要保留以下两个 minibatch 配置blink.miniBatch.allowLatencyMs=5000防止OOM,每个批次最多缓存多少条数据blink.miniBatch.size=200004:动态负载使用 Dynamic-Rebalance 替代 Rebalanceblink任务在运行是最忌讳的就是存在计算热点,为保证数据均匀使用Dynamic Rebalance,它可以根据当前各subpartition中堆积的buffer的数量,选择负载较轻的subpartition进行写入,从而实现动态的负载均衡。相比于静态的rebalance策略,在下游各任务计算能力不均衡时,可以使各任务相对负载更加均衡,从而提高整个作业的性能。 开启动态负载task.dynamic.rebalance.enabled=true5:自定义输出插件数据关联后需要将统一请求链路上的数据作为一个数据包通知下游图分析节点,传统的方式的是通过消息服务来投递数据。但是通过消息服务有两个缺点:1、其吞吐量和rdb这种内存数据库相比比还是较大差距(大概差一个数量级)。2、在接受端还需要根据traceid做数据关联。 ...

June 21, 2019 · 1 min · jiezi

容器服务Windows-Kubernetes使用阿里云日志服务来收集容器日志

目前,容器服务Windows Kubernetes支持将业务容器产生的stdout输出、日志文件同步到阿里云日志服务(SLS)进行统一管理。 支撑组件安装在Windows Kubernetes集群安装界面勾选使用日志服务,集群会安装支持日志收集的必要组件logtail。 集群安装完毕后,可以在日志服务控制台 查看到按k8s-sls-{Kubernetes 集群 ID}形式命名的工程。收集到的业务容器日志都会放在该工程下。 使用YAML模版部署业务容器YAML 模板的语法同 Kubernetes 语法,但是为了给容器指定采集配置,需要使用 env 来为 container 增加采集配置和自定义 Tag,并根据采集配置,创建对应的 volumeMounts 和 volumns。以下是一个简单的 Deployment 示例: apiVersion: extensions/v1beta1kind: Deploymentmetadata: labels: app: logtail-test name: logtail-testspec: replicas: 1 template: metadata: labels: app: logtail-test name: logtail-test spec: containers: - name: logtail image: registry-vpc.cn-hangzhou.aliyuncs.com/acs/windows-logtail:1809-1.0.0.4 command: ["powershell.exe"] args: [cmd /k "ping -t 127.0.0.1 -w 10000 > C:\log\data.log"] env: ######### 配置 环境变量 ########### - name: aliyun_logs_log-stdout value: stdout - name: aliyun_logs_log-varlog value: C:\log\*.log - name: aliyun_logs_log_tags value: tag1=v1 ################################# ######### 配置vulume mount ####### volumeMounts: - name: volumn-sls-win mountPath: c:\log volumes: - name: volumn-sls-win emptyDir: {} ############################### nodeSelector: beta.kubernetes.io/os: windows其中有三部分需要根据您的需求进行配置,一般按照顺序进行配置。 ...

June 20, 2019 · 1 min · jiezi

十分钟上线-函数计算构建支付宝小程序的后端

阿里云函数计算服务(FunctionCompute,FC)是一个事件驱动的全托管计算服务。通过函数计算与云端各个服务的广泛集成,开发者只需要编写函数代码,就能够快速地开发出弹性高可用的后端系统。接下来我们使用FC,来快速实现一个图片转换服务, 并把这个图片转换服务作为支付宝小程序的后端。 支付宝小程序demo前端效果图: 资源下载及准备工作示例代码附件 【必须】 支付宝小程序开发工具下载 【非必须】 函数计算FC 快捷入口对象存储OSS 快捷入口日志服务Log Service 快捷入口 简明架构图 函数入口普通函数入口 def my_handler(event, context): return 'hello world'函数名my_handler需要与创建函数时的"Handler"字段相对应:例如创建函数时指定的 Handler 为main.my_handler,那么函数计算会去加载main.py中定义的my_handler函数 event 参数event 参数是用户调用函数时传入的数据,其类型是str context 参数context 参数中包含一些函数的运行时信息(例如 request id/临时 AK 等)。其类型是FCContext,具体结构和使用在下面的使用 context介绍 返回值函数的返回值会作为调用函数的结果返回给用户,它可以是任意类型:对于简单类型会函数计算会把它转换成 str 返回,对于复杂类型会把它转换成 JSON 字符串返回 HTTP 触发器的函数入口 HELLO_WORLD = b"Hello world!\n"def handler(environ, start_response): context = environ['fc.context'] status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return [HELLO_WORLD]environ : environ 参数是一个 python 字典,里面存放了所有和客户端相关的信息,具体详情参考 environ 参数,函数计算增加了两个自定义的 key,分别是 fc.context 和 fc.request_uri ...

June 19, 2019 · 2 min · jiezi

基于Knative开发应用

目录安装 Istio安装 Knative玩转 helloworld-goWordPress 实战创建 Kubernetes 集群确保 Kubernetes 集群创建的时候已经选择了启用日志服务确保 Kubernetes 集群和 OSS 在一个 regionKubernetes 集群创建的时候需要开启 kube-apiserver 公网访问提前帮用户配置好 kubeconfig 命令行安装 Istio安装 Istio 时注意以下几点: 默认要安装 gateway日志服务和 Xtrace 要提前开通,Istio 需要使用 ZipKin v1 向 Xtrace 汇报监控数据在容器服务集群管理页面可以直接在目标集群上部署 Istio 安装 Knative选择好目标集群使用一键部署功能直接安装即可, 安装文档 玩转 helloworld-go配置日志采集策略部署 Helloworld监控告警调用链压测数据展示日志管理日志服务控制台: https://sls.console.aliyun.com本示例以容器标准输出采集为例进行展示,详细设置步骤可以参考日志服务文档根据 Kubernetes 集群 ID 找到对应的日志服务 Project创建一个新的 Logstore设置数据导入方式 选择 Docker标准输出 配置容器标准输出日志采集策略{ "inputs": [ { "detail": { "IncludeEnv": { "K_SERVICE": "helloworld-go" }, "IncludeLabel": {}, "ExcludeLabel": {} }, "type": "service_docker_stdout" } ], "processors": [ { "detail": { "KeepSource": false, "NoMatchError": true, "Keys": [ "time", "level", "msg" ], "NoKeyError": true, "Regex": "(\\d+-\\d+-\\d+ \\d+:\\d+:\\d+)\\s+(\\w+)\\s+(*)", "SourceKey": "content" }, "type": "processor_regex" } ]}分别为相应的键值 time、level 和 msg 设置数据类型 ...

June 17, 2019 · 2 min · jiezi

Logtail提升采集性能

默认性能限制为防止滥用消耗过多机器资源,我们对默认安装的Logtail进行了一系列的资源限制。默认安装的Logtail最多日志采集速度为20M/s,20个并发发送。 其他资源限制请参考:启动参数 https://help.aliyun.com/docum... 中的默认配置。 采集能力单核能力 如果放开发送流控,Logtail默认单核的能力大致如下(具体根据不同正则、日志类型、采集提取的key数量、机器配置等会有一定浮动): 备注:测试环境 CPU :Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz MEM : 64GB OS : Linux version 2.6.32-220.23.2.ali1113.el5.x86_64 多核能力 Logtail默认只开一个线程处理数据,如果开启多核,性能会有提升,但并不是线性关系,实测最多开到8个线程后,性能几乎没有上涨。 极简模式最高性能可达:440MB/s正则最高性能可达:70MB/s分隔符最高性可达:75MB/sJSON最高性能可达:75MB/s日志格式建议根据您的使用目的,合理选择对应的日志格式 搬数据:使用极简模式,性能最高数据分析:多字符分隔符>单字符分隔符>JSON模式>正则模式Java堆栈类型数据:正则模式注意:正则模式采集性能和正则优化有非常大关系。如何放开资源限制可通过调整Logtail的启动参数来放开默认的资源限制,下面我们推荐2种配置方式: 注意:Logtail使用短连接发送数据,如果发送并发过高,建议调整服务器的tcp参数,防止过多time_wait调整方式:sudo sysctl -w net.ipv4.tcp_tw_timeout=5单核小资模式 在配置文件末尾追加以下两个参数,注意JSON需合法。 多核极致模式 在配置文件末尾追加以下几个参数,需保证,注意JSON需合法。 注意:需保证 cpu_usage_limit > process_thread_count 本文作者:元乙 原文链接 本文为云栖社区原创内容,未经允许不得转载。

June 6, 2019 · 1 min · jiezi

达摩院首席数据库科学家李飞飞云原生新战场我们如何把握先机

阿里妹导读:云计算大潮来袭,传统数据库市场正面临重新洗牌的情境,包括云数据库在内的一批新生力量崛起,动摇了传统数据库的垄断地位,而由云厂商主导的云原生数据库则将这种“改变”推向了高潮。云时代的数据库将面临怎样的变革?云原生数据库有哪些独特优势?在 DTCC 2019大会上,阿里巴巴副总裁 李飞飞博士就《下一代云原生数据库技术与趋势》进行了精彩分享。 李飞飞(花名:飞刀),阿里巴巴集团副总裁,高级研究员,达摩院首席数据库科学家,阿里云智能事业群数据库产品事业部负责人,ACM 杰出科学家。 大势所趋:云数据库市场份额增速迅猛如下图所示的是 Gartner 关于全球数据库市场份额的报告,该报告指出目前全球数据库市场份额大约为400亿美金,其中,中国数据库市场份额占比为3.7%,大约为14亿美金。 具体到数据库市场分布,传统五大数据库厂商 Oracle、Microsoft、IBM、SAP、Teradata 占比达到了80%,云数据库的份额占比接近10%,并且云数据库市场份额占比每年也在快速增长,因此, Oracle、MongoDB 等也在大力布局其在云数据库市场的竞争态势。 根据 DB-Engines 数据库市场分析显示,数据库系统正朝着多样化、多元化的方向发展,从传统的 TP 关系型数据库发展到今天的多源异构的数据库形态。目前,处于主流位置的还是大家耳熟能详的数据库系统,比如商业数据库 Oracle、SQL Server以及开源的 MySQL、PostgreSQL 等。而一些比较新的数据库系统,比如MongoDB、Redis 则开辟了一个新的赛道。数据库 License 的传统销售方式在逐渐走下坡路,而开源以及云上数据库 License 的流行程度却在不断提升。 数据库:云上应用关键的一环正如 AWS 创始人 Jeff Bezos 所说:“The real battle will be in databases”。因为云最早是从 IaaS 做起来的,从虚拟机、存储、网络,到现在如火如荼的语音识别、计算机视觉以及机器人等智能化应用,都是基于 IaaS 的,而数据库就是连接 IaaS 与智能化应用 SaaS 最为关键的一环。从数据产生、存储到消费的各个环节,数据库都至关重要。 数据库主要包括四大板块,即 OLTP、OLAP、NoSQL 以及数据库服务和管理类工具,也是云数据库厂商发力的四个方向。对于 OLTP 而言,技术发展已经历经了40年,而如今大家还在做的一件事情就是“加10元和减10元”,也就是所谓的事务处理。当数据量变得越来越大和读写冲突的原因,对数据进行在线实时分析的需求衍生出了 OLAP。由于需要 Scale out,而数据强一致性不能够得到保证,就有了NoSQL 。而最近又出现了一个新名词—— NewSQL,这是因为 NoSQL 也有所不足,故将传统 OLTP 的 ACID 保证与 NoSQL 的 Scale out 能力进行了整合,变成了NewSQL。 ...

June 6, 2019 · 2 min · jiezi

基于Egg框架的日志链路追踪实践分享

快速导航[Logger-Custom] 需求背景[Logger-Custom] 自定义日志插件开发[Logger-Custom] 项目扩展[Logger-Custom] 项目应用[ContextFormatter] contextFormatter自定义日志格式[Logrotator] 日志切割需求背景实现全链路日志追踪,便于日志监控、问题排查、接口响应耗时数据统计等,首先 API 接口服务接收到调用方请求,根据调用方传的 traceId,在该次调用链中处理业务时,如需打印日志的,日志信息按照约定的规范进行打印,并记录 traceId,实现日志链路追踪。 日志路径约定/var/logs/${projectName}/bizLog/${projectName}-yyyyMMdd.log日志格式约定日志时间[]traceId[]服务端IP[]客户端IP[]日志级别[]日志内容采用 Egg.js 框架 egg-logger 中间件,在实现过程中发现对于按照以上日志格式打印是无法满足需求的(至少目前我还没找到可实现方式),如果要自己实现,可能要自己造轮子了,好在官方的 egg-logger 中间件提供了自定义日志扩展功能,参考 高级自定义日志,本身也提供了日志分割、多进程日志处理等功能。 egg-logger 提供了多种传输通道,我们的需求主要是对请求的业务日志自定义格式存储,主要用到 fileTransport 和 consoleTransport 两个通道,分别打印日志到文件和终端。 自定义日志插件开发基于 egg-logger 定制开发一个插件项目,参考 插件开发,以下以 egg-logger-custom 为项目,展示核心代码编写 编写logger.jsegg-logger-custom/lib/logger.jsconst moment = require('moment');const FileTransport = require('egg-logger').FileTransport;const utils = require('./utils');const util = require('util');/** * 继承 FileTransport */class AppTransport extends FileTransport { constructor(options, ctx) { super(options); this.ctx = ctx; // 得到每次请求的上下文 } log(level, args, meta) { // 获取自定义格式消息 const customMsg = this.messageFormat({ level, }); // 针对 Error 消息打印出错误的堆栈 if (args[0] instanceof Error) { const err = args[0] || {}; args[0] = util.format('%s: %s\n%s\npid: %s\n', err.name, err.message, err.stack, process.pid); } else { args[0] = util.format(customMsg, args[0]); } // 这个是必须的,否则日志文件不会写入 super.log(level, args, meta); } /** * 自定义消息格式 * 可以根据自己的业务需求自行定义 * @param { String } level */ messageFormat({ level }) { const { ctx } = this; const params = JSON.stringify(Object.assign({}, ctx.request.query, ctx.body)); return [ moment().format('YYYY/MM/DD HH:mm:ss'), ctx.request.get('traceId'), utils.serviceIPAddress, utils.clientIPAddress(ctx.req), level, ].join(utils.loggerDelimiter) + utils.loggerDelimiter; }}module.exports = AppTransport;工具egg-logger-custom/lib/utils.jsconst interfaces = require('os').networkInterfaces();module.exports = { /** * 日志分隔符 */ loggerDelimiter: '[]', /** * 获取当前服务器IP */ serviceIPAddress: (() => { for (const devName in interfaces) { const iface = interfaces[devName]; for (let i = 0; i < iface.length; i++) { const alias = iface[i]; if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) { return alias.address; } } } })(), /** * 获取当前请求客户端IP * 不安全的写法 */ clientIPAddress: req => { const address = req.headers['x-forwarded-for'] || // 判断是否有反向代理 IP req.connection.remoteAddress || // 判断 connection 的远程 IP req.socket.remoteAddress || // 判断后端的 socket 的 IP req.connection.socket.remoteAddress; return address.replace(/::ffff:/ig, ''); }, clientIPAddress: ctx => { return ctx.ip; },}注意:以上获取当前请求客户端IP的方式,如果你需要对用户的 IP 做限流、防刷限制,请不要使用如上方式,参见 科普文:如何伪造和获取用户真实 IP ?,在 Egg.js 里你也可以通过 ctx.ip 来获取,参考 前置代理模式。 ...

June 5, 2019 · 3 min · jiezi

MySQL80-新特性-说说InnoDB-Log-System的隐藏参数

InnoDB在设计lock-free的log system时,除了已有的参数外,还通过宏控制隐藏了一些参数,如果你使用源码编译时,打开cmake选项-DENABLE_EXPERIMENT_SYSVARS=1, 就可以看到这些参数了。本文主要简单的过一下这些隐藏的参数所代表的含义 A.innodb_log_write_eventsinnodb_log_flush_events两者的含义类似,表示用来唤醒等待log write/flush的event的个数,默认值都是2048比如你要等待的位置在lsnA,那么计算的slot为:slot = (lsnA - 1) /OS_FILE_LOG_BLOCK_SIZE & (innodb_log_write/flush_events - 1)这意味着:如果事务的commit log的end lsn落在相同block里,他们可能产生event的竞争当然如果不在同一个block的时候,如果调大参数,就可以减少竞争,但也会有无效的唤醒唤醒操作通常由后台线程log_write_notifier 或者log_flush_notifier异步来做,但如果推进的log write/flush还不足一个block的话,那就log_writter/flusher自己去唤醒了。 B.innodb_log_recent_written_size, 默认1MB表示recent_written这个link_buf的大小,其实控制了并发往log buffer中同时拷贝的事务日志量,向前由新的日志加入,后面由log writer通过写日志向前推进,如果写的慢的话,那这个link_buf很可能用满,用户线程就得spin等待。再慢io的系统上,我们可以稍微调大这个参数 innodb_Log_recent_closed_size, 默认2MB表示recent closed这个link_buf的大小,也是维护可以并发往flush list上插入脏页的并罚度,如果插入脏页速度慢,或者lin_buf没有及时合并推进,就会spin wait 简单说下link_buf, 这本质上是一个数组,但使用无锁的使用方式来维护lsn的推进,比如获得一个lsn开始和结束,那就通过设置buf[start_lsn] = end_lsn的类似方式来维护lsn链,基于lsn是连续值的事实,最终必然不会出现空洞,所以在演化的过程中,可以从尾部推进连续的lsn,头部插入新的值.如果新插入的值超过了尾部,表示buf满了,就需要spin wait了C.innodb_log_wait_for_write_spin_delay, innodb_log_wait_for_write_timeout 从8.0版本开始用户线程不再自己去写redo,而是等待后台线程去写,这两个变量控制了spin以及condition wait的timeout时间,当spin一段时间还没推进到某个想要的lsn点时,就会进入condition wait 另外两个变量innodb_log_wait_for_flush_spin_delayinnodb_log_wait_for_flush_timeout含义类似,但是是等待log flush到某个指定lsn 注意在实际计算过程中,最大spin次数,会考虑到cpu利用率,以及另外两个参数:innodb_log_spin_cpu_abs_lwminnodb_log_spin_cpu_pct_hwm 如果是等待flush操作的话,还收到参数innodb_log_wait_for_flush_spin_hwm限制,该参数控制了等待flush的时间上限,如果平均等待flush的时间超过了这个上限的话, 就没必要去spin,而是直接进入condition wait 关于spin次数的计算方式在函数log_max_spins_when_waiting_in_user_thread中": 函数的参数即为配置项innodb_log_wait_for_write_spin_delay或innodb_log_wait_for_flush_spin_delay值 static inline uint64_t log_max_spins_when_waiting_in_user_thread( uint64_t min_non_zero_value) { uint64_t max_spins; /* Get current cpu usage. */ const double cpu = srv_cpu_usage.utime_pct; /* Get high-watermark - when cpu usage is higher, don't spin! */ const uint32_t hwm = srv_log_spin_cpu_pct_hwm; if (srv_cpu_usage.utime_abs < srv_log_spin_cpu_abs_lwm || cpu >= hwm) { /* Don't spin because either cpu usage is too high or it's almost idle so no reason to bother. */ max_spins = 0; } else if (cpu >= hwm / 2) { /* When cpu usage is more than 50% of the hwm, use the minimum allowed number of spin rounds, not to increase cpu usage too much (risky). */ max_spins = min_non_zero_value; } else { /* When cpu usage is less than 50% of the hwm, choose maximum spin rounds in range [minimum, 10*minimum]. Smaller usage of cpu is, more spin rounds might be used. */ const double r = 1.0 * (hwm / 2 - cpu) / (hwm / 2); max_spins = static_cast<uint64_t>(min_non_zero_value + r * min_non_zero_value * 9); } return (max_spins);}D.以下几个参数是后台线程等待任务时spin及condition wait timeout的值log_writer线程:innodb_log_writer_spin_delay,innodb_log_writer_timeout ...

June 4, 2019 · 2 min · jiezi

Log-In-Action

Log In Action0. 生产环境要关闭debug日志,严禁在生产环境打debug级别日志1. trace/debug/info 日志输出采用占位符的方式,禁止使用字符串拼接的方式说明:logger.debug("=====" + b),如果当前的日志级别是warn,上述日志不会打印,但会执行字符串拼接操作,如果b是对象,会调用b的toString()方法,这样会非常浪费系统资源,特别是当b是个大对象的时候。 bad case: log.info("finish import seller " + sellerId)# 有现成的占位符的方式,不要使用String.format(),代码复杂log.info(String.format("group_id=%s",groupId)) good case: logger.info("modify audit status, noteId: [{}], creativityId: [{}]", noteId, creativity.getId());个人也不推荐条件输出形式,用这种if的方式判断,显然不够优雅 if (logger.isDebugEnabled()) { logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);}2. 使用[]进行参数变量隔离这样的格式写法,可读性更好,对于排查问题又帮助。 good case: logger.info("modify audit status, noteId: [{}], creativityId: [{}]", noteId, creativity.getId())3. 使用warn级别日志记录用户输入参数错误的情况,不要使用error级别日志记录此类错误,避免频繁报警bad case: log.error("Porch: 创建账号失败:传入参数有误:email={}, name={}", email, name)good case: logger.warn("创建单元名称重复,unit_name={}", req.getUnitName())4. 异常信息应该包含两类信息:案发现场和异常堆栈信息bad case: ...

May 30, 2019 · 1 min · jiezi

自动化日志收集及分析在支付宝-App-内的演进

背景结合《蚂蚁金服面对亿级并发场景的组件体系设计》,我们能够通盘了解支付宝移动端基础组件体系的构建之路和背后的思考,本文基于服务端组建体系的大背景下,着重探讨“自动化日志手机与分析”在支付宝 App 内的演进之路。 支付宝移动端技术服务框架 这是整个支付宝移动端无线基础团队的技术架构图,同时蚂蚁金服体系内的其他业务 口碑、网商银行、香港支付宝、天弘基金等) 通过移动开发平台 mPaaS进行代码开发、打包、灰度发布、上线、问题修复、运营、分析。因此,mPaaS 源自于支付宝移动端的核心技术架构,并在 App 全生命周期的每个环节提供特定的能力支持。接下来,我们将着重分享“日志诊断”和“移动分析”两个能力背后的架构演进和选型思考。 支付宝移动端技术服务框架:数据分析框架 如图所示,即数据分析能力的技术架构图,其中“数据同步”、“埋点SDK”、“日志网关”是移动端专属的能力,其余部分是所有的数据分析平台都必须具备的数据结构。 1. 日志采集 接下来我们重点分析支付宝移动端的日志采集框架,首先第一部分是“日志 SDK”,日志 SDK 会向业务层提供一个埋点接口,使用起来就和 Java 里面的 logger.info 的感觉一样:业务层只需要把想记录的信息传递给日志 SDK。日志 SDK 会在拿到业务日志后,去系统内部获取相关的系统级别的上下文信息,比如机型、操作系统版本、App 版本、手机分辨率、用户ID (如果用户登录了)、设备ID、上一个页面、当前页面等,接着把这些信息与业务日志内容整合成一个埋点,然后记录到设备的硬盘上。对,只是记录到硬盘上,并没有上报到服务端。 日志 SDK 会在合适的时机,去和日志网关交互,判断获取什么样的日志、什么级别的日志可以上报。如果可以上报,是以什么频率上报、在什么网络条件下上报。因此通过日志 SDK 与日志网关的交付,我们可以来实现日志的策略式降级。日志的策略式降级这点对于支付宝特别重要,因为目前支付宝的体量,日常的日志上报量为约 30W 条日志/s;而在大促的时候,日志量会是日常的几十倍! 所以,如果大促期间不采用任何的日志降级策略的话,我们的带宽会被日志打包,支付宝的正常业务功能就不可用了。 由此,我们可以总结,在大促高并发场景下,我们需要只保留关键日志的上传,其余日志统统降级。即使是日常业务处理,我们也只允许日志在 WIFI 条件下上报,防止发生流量相关的投诉。 埋点网关除了提供日志上报的策略开关能力之外,就是接收客户端的日志。它在接受到客户端日志之后,会对日志内容进行校验,发现无效的日志会丢弃掉。而对有效合法的埋点,会根据客户端上报的公网 IP 地址,反解出城市级别的地址位置信息并添加到埋点中,然后将埋点存放在它自己的硬盘上。 2. 埋点分类 经过多年的实践,支付宝把日志埋点分为了四类。 (1)行为埋点:用于监控业务行为,即业务层传递的日志埋点属于行为埋点,行为埋点属于“手动埋点”,需要业务层开发同学自己开发。不过也不是全部的业务行为都需要业务方手动开发,有一些具备非常通用性的业务事件,支付宝把它们的埋点记录放到了框架层,如报活事件、登录事件。因此,行为埋点也可以叫做 "半自动埋点"。 (2)自动化埋点:属于“全自动化埋点”,用于记录一些通用的页面级别、组件级别的行为,比如页面打开、页面关闭、页面耗时、组件点击等。 (3)性能埋点:属于“全自动化埋点”,用于记录 App 的电量使用情况、流量使用、内存使用、启动速度等。 (4)异常埋点:属于“全自动化埋点”,严格上讲,也算是性能埋点的一种。但是它记录的是最关键的最直接影响用户的性能指标,比如 App 的闪退情况、卡死、卡顿情况等。这类埋点,就属于即时大促期间也不能降级的埋点! 图中的代码示例即为一条行为埋点样例,大家可以看到,埋点实际上就是一条 CSV 文本。我们可以看到里面有日志网关添加进行的地址位置信息内容,也有日志 SDK 给添加的客户端设备信息。 3. 日志处理模型 下面我们来整体了解支付宝内部日志处理的通用流程: (1)日志切分 我们已经了解到,埋点实际上即为一段 CSV 文本。而所谓日志切分呢,即将 CSV 格式的文本转成 KV,通俗点说就是内存里面的 HASHMAP。这个切分的过程,可以直接根据逗号进行切换,当然也还有很多其他的办法。 ...

May 30, 2019 · 2 min · jiezi

阿里PB级Kubernetes日志平台建设实践

摘要: 将在QCon上分享的《阿里PB级Kubernetes日志平台建设实践》整理出来,分享给大家。阿里PB级Kubernetes日志平台建设实践QCon是由InfoQ主办的综合性技术盛会,每年在伦敦、北京、纽约、圣保罗、上海、旧金山召开。有幸参加这次QCon10周年大会,作为分享嘉宾在刘宇老师的运维专场发表了《阿里PB级Kubernetes日志平台建设实践》,现将PPT和文字稿整理下来,希望和更多的爱好者分享。 计算形态的发展与日志系统的演进 在阿里的十多年中,日志系统伴随着计算形态的发展在不断演进,大致分为3个主要阶段: 在单机时代,几乎所有的应用都是单机部署,当服务压力增大时,只能切换更高规格的IBM小型机。日志作为应用系统的一部分,主要用作程序Debug,通常结合grep等Linux常见的文本命令进行分析。随着单机系统成为制约阿里业务发展的瓶颈,为了真正的Scale out,飞天项目启动:2009年开始了飞天的第一行代码,2013年飞天5K项目正式上线。在这个阶段各个业务开始了分布式改造,服务之间的调用也从本地变为分布式,为了更好的管理、调试、分析分布式应用,我们开发了Trace(分布式链路追踪)系统、各式各样的监控系统,这些系统的统一特点是将所有的日志(包括Metric等)进行集中化的存储。为了支持更快的开发、迭代效率,近年来我们开始了容器化改造,并开始了拥抱Kubernetes生态、业务全量上云、Serverless等工作。要实现这些改造,一个非常重要的部分是可观察性的工作,而日志是作为分析系统运行过程的最佳方式。在这阶段,日志无论从规模、种类都呈现爆炸式的增长,对日志进行数字化、智能化分析的需求也越来越高,因此统一的日志平台应运而生。日志平台的重要性与建设目标 日志不仅仅是服务器、容器、应用的Debug日志,也包括各类访问日志、中间件日志、用户点击、IoT/移动端日志、数据库Binlog等等。这些日志随着时效性的不同而应用在不同的场景: 准实时级别:这类日志主要用于准实时(秒级延迟)的线上监控、日志查看、运维数据支撑、问题诊断等场景,最近两年也出现了准实时的业务洞察,也是基于这类准实时的日志实现。小时/天级别:当数据积累到小时/天级别的时候,这时一些T+1的分析工作就可以开始了,例如用户留存分析、广告投放效果分析、反欺诈、运营监测、用户行为分析等。季度/年级别:在阿里,数据是我们最重要的资产,因此非常多的日志都是保存一年以上或永久保存,这类日志主要用于归档、审计、攻击溯源、业务走势分析、数据挖掘等。在阿里,几乎所有的业务角色都会涉及到各式各样的日志数据,为了支撑各类应用场景,我们开发了非常多的工具和功能:日志实时分析、链路追踪、监控、数据清洗、流计算、离线计算、BI系统、审计系统等等。其中很多系统都非常成熟,日志平台主要专注于智能分析、监控等实时的场景,其他功能通常打通的形式支持。 阿里日志平台现状 目前阿里的日志平台覆盖几乎所有的产品线和产品,同时我们的产品也在云上对外提供服务,已经服务了上万家的企业。每天写入流量16PB以上,对应日志行数40万亿+条,采集客户端200万,服务数千Kubernetes集群,是国内最大的日志平台之一。    为何选择自建                       日志系统存在了十多年,目前也有非常多的开源的方案,例如最典型的ELK(Elastic Search、Logstash、Kibana),通常一个日志系统具备以下功能:日志收集/解析、查询与检索、日志分析、可视化/告警等,这些功能通过开源软件的组合都可以实现,但最终我们选择自建,主要有几下几点考虑: 数据规模:这些开源日志系统可以很好的支持小规模的场景,但很难支持阿里这种超大规模(PB级)的场景。资源消耗:我们拥有百万规模的服务器/容器,同时日志平台的集群规模也很大,我们需要减少对于采集以及平台自身的资源消耗。多租户隔离:开源软件搭建的系统大部分都不是为了多租户而设计的,当非常多的业务 / 系统使用日志平台时,很容易因为部分用户的大流量 / 不恰当使用而导致打爆整个集群。运维复杂度:在阿里内部有一套非常完整的服务部署和管理系统,基于内部组件实现会具备非常好的运维复杂度。高级分析需求:日志系统的功能几乎全部来源与对应的场景需求,有很多特殊场景的高级分析需求开源软件没办法很好的支持,例如:上下文、智能分析、日志类特殊分析函数等等。 Kubernetes日志平台建设难点围绕着Kubernetes场景的需求,日志平台建设的难点主要有以下几点: 日志采集:采集在Kubernetes中极其关键和复杂,主要因为Kubernetes是一个高度复杂的场景,K8s中有各式各样的子系统,上层业务支持各种语言和框架,同时日志采集需要尽可能的和Kubernetes系统打通,用K8的形式来完成数据采集。资源消耗:在K8s中,服务通常都会拆的很小,因此数据采集对于服务自身的资源消耗要尽可能的少。这里我们简单的做一个计算,假设有100W个服务实例,没个采集Agent减少1M的内存、1%的CPU开销,那整体会减少1TB的内存和10000个CPU核心。运维代价:运维一套日志平台的代价相当之大,因此我们不希望每个用户搭建一个Kubernetes集群时还需再运维一个独立的日志平台系统。因此日志平台一定是要SaaS化的,应用方/用户只需要简单的操作Web页面就能完成数据采集、分析的一整套流程。便捷使用:日志系统最核心的功能是问题排查,问题排查的速度直接决定了工作效率、损失大小,在K8s场景中,更需要一套高性能、智能分析的功能来帮助用户快速定位问题,同时提供一系列简单有效的可视化手段进行辅助。阿里PB级Kubernetes日志平台建设实践Kubernetes日志数据采集 无论是在ITOM还是在未来的AIOps场景中,日志获取都是其中必不可少的一个部分,数据源直接决定了后续应用的形态和功能。在十多年中,我们积累了一套物理机、虚拟机的日志采集经验,但在Kubernetes中不能完全适用,这里我们以问题的形式展开: 问题1:DaemonSet or Sidecar 日志最主要的采集工具是Agent,在Kubernetes场景下,通常会分为两种采集方式: DaemonSet方式:在K8S的每个node上部署日志agent,由agent采集所有容器的日志到服务端。Sidecar方式:一个POD中运行一个sidecar的日志agent容器,用于采集该POD主容器产生的日志。每种采集方式都有其对应的优缺点,这里简单总结如下: DaemonSet方式Sidecar方式采集日志类型标准输出+部分文件文件部署运维一般,需维护DaemonSet较高,每个需要采集日志的POD都需要部署sidecar容器日志分类存储一般,可通过容器/路径等映射每个POD可单独配置,灵活性高多租户隔离一般,只能通过配置间隔离强,通过容器进行隔离,可单独分配资源支持集群规模中小型规模,业务数最多支持百级别无限制资源占用较低,每个节点运行一个容器较高,每个POD运行一个容器查询便捷性较高,可进行自定义的查询、统计高,可根据业务特点进行定制可定制性低高,每个POD单独配置适用场景功能单一型的集群大型、混合型、PAAS型集群在阿里内部,对于大型的PAAS集群,主要使用Sidecar方式采集数据,相对隔离性、灵活性最好;而对与功能比较单一(部门内部/产品自建)的集群,基本都采用DaemonSet的方式,资源占用最低。 问题2:如何降低资源消耗 我们数据采集Agent使用的是自研的Logtail,Logtail用C++/Go编写,相对开源Agent在资源消耗上具有非常大的优势,但我们还一直在压榨数据采集的资源消耗,尤其在容器场景。通常,为了提高打日志和采集的性能,我们都使用本地SSD盘作为日志盘。这里我们可以做个简答的计算:假设每个容器挂载1GB的SSD盘,1个物理机运行40个容器,那每台物理机需要40GB的SSD作为日志存储,那5W物理机则会占用2PB的SSD盘。为了降低这部分资源消耗,我们和蚂蚁金服团队的同学们一起开发了FUSE的日志采集方式,使用FUSE(Filesystem in Userspace,用户态文件系统)虚拟化出日志盘,应用直接将日志写入到虚拟的日志盘中,最终数据将直接从内存中被Logtail采集到服务端。这种采集的好处有: 物理机无需为容器提供日志盘,真正实现日志无盘化。应用程序视角看到的还是普通的文件系统,无需做任何额外改造。数据采集绕过磁盘,直接从内存中将数据采集到服务端。所有的数据都存在服务端,服务端支持横向扩展,对于应用来说他们看到的日志盘具有无线存储空间。问题3:如何与Kubernetes无缝集成 Kubernetes一个非常大的突破是使用声明式的API来完成服务部署、集群管理等工作。但在K8s集群环境下,业务应用/服务/组件的持续集成和自动发布已经成为常态,使用控制台或SDK操作采集配置的方式很难与各类CI、编排框架集成,导致业务应用发布后用户只能通过控制台手动配置的方式部署与之对应的日志采集配置。因此我们基于Kubernetes的CRD(CustomResourceDefinition)扩展实现了采集配置的Operator,用户可以直接使用K8s API、Yaml、kubectl、Helm等方式直接配置采集方式,真正把日志采集融入到Kubernetes系统中,实现无缝集成。 问题4:如何管理百万级Logtail 对于人才管理有个经典的原则:10个人要用心良苦,100个人要杀伐果断,1000个人要甩手掌柜。而同样对于Logtail这款日志采集Agent的管理也是如此,这里我们分为3个主要过程: 百规模:在好几年前,Logtail刚开始部署时,也就在几百台物理机上运行,这个时期的Logtail和其他主流的Agent一样,主要完成数据采集的功能,主要流程为数据输入、处理、聚合、发送,这个时期的管理基本靠手,采集出现问题的时候人工登录机器去看问题。万规模:当越来越多的应用方接入,每台机器上可能会有多个应用方采集不同类型的数据,手动配置的接入过程也越来越难以维护。因此我们重点在多租户隔离以及中心化的配置管理,同时增加了很多控制相关的手段,比如限流、降级等。百万规模:当部署量打到百万级别的时候,异常发生已经成为常态,我们更需要的是靠一系列的监控、可靠性保证机制、自动化的运维管理工具,让这些机制、工具来自动完成Agent安装、监控、自恢复等一系列工作,真正做到甩手掌柜。Kubernetes日志平台架构 上图是阿里Kubernetes日志平台的整体架构,从底到上分为日志接入层、平台核心层以及方案整合层: 平台提供了非常多的手段用来接入各种类型的日志数据。不仅仅只有Kubernetes中的日志,同时还包括和Kubernetes业务相关的所有日志,例如移动端日志、Web端应用点击日志、IoT日志等等。所有数据支持主动Push、被动Agent采集,Agent不仅支持我们自研的Logtail,也支持使用开源Agent(Logstash、Fluentd、Filebeats等)。日志首先会到达平台提供的实时队列中,类似于Kafka的consumer group,我们提供实时数据订阅的功能,用户可以基于该功能实现ETL的相关需求。平台最核心的功能包括: 实时搜索:类似于搜索引擎的方式,支持从所有日志中根据关键词查找,支持超大规模(PB级)。实时分析:基于SQL92语法提供交互式的日志分析方法。机器学习:提供时序预测、时序聚类、根因分析、日志聚合等智能分析方法。流计算:对接各类流计算引擎,例如:Flink、Spark Stream、Storm等。离线分析:对接离线分析引擎,例如Hadoop、Max Compute等。基于全方位的数据源以及平台提供的核心功能,并结合Kubernetes日志特点以及应用场景,向上构建Kubernetes日志的通用解决方案,例如:审计日志、Ingress日志分析、ServiceMesh日志等等。同时对于有特定需求的应用方/用户,可直接基于平台提供的OpenAPI构建上层方案,例如Trace系统、性能分析系统等。下面我们从问题排查的角度来具体展开平台提供的核心功能。 PB级日志查询 排查问题的最佳手段是查日志,大部分人脑海中最先想到的是用 grep 命令查找日志中的一些关键错误信息, grep 是Linux程序员最受欢迎的命令之一,对于简单的问题排查场景也非常实用。如果应用部署在多台机器,那还会配合使用pgm、pssh等命令。然而这些命令对于Kubernetes这种动态、大规模的场景并不适用,主要问题有: 查询不够灵活,grep命令很难实现各种逻辑条件的组合。grep是针对纯文本的分析手段,很难将日志格式化成对应的类型,例如Long、Double甚至JSON类型。grep命令的前提条件是日志存储在磁盘上。而在Kubernetes中,应用的本地日志空间都很小,并且服务也会动态的迁移、伸缩,本地的数据源很可能会不存在。grep是典型的全量扫描方式,如果数据量在1GB以内,查询时间还可以接受,但当数据量上升到TB甚至PB时,必须依赖搜索引擎的技术才能工作。我们在2009年开始在飞天平台研发过程中,为够解决大规模(例如5000台)下的研发效率、问题诊断等问题,开始研支持超大规模的日志查询平台,其中最主要的目标是“快”,对于几十亿的数据也能够轻松在秒级完成。 日志上下文 当我们通过查询的方式定位到关键的日志后,需要分析当时系统的行为,并还原出当时的现场情况。而现场其实就是当时的日志上下文,例如: 一个错误,同一个日志文件中的前后数据一行LogAppender中输出,同一个进程顺序输出到日志模块前后顺序一次请求,同一个Session组合一次跨服务请求,同一个TraceId组合在Kubernetes的场景中,每个容器的标准输出(stdout)、文件都有对应的组合方式构成一个上下文分区,例如Namesapce+Pod+ContainerID+FileName/Stdout。为支持上下文,我们在采集协议中对每个最小区分单元会带上一个全局唯一并且单调递增的游标,这个游标对单机日志、Docker、K8S以及移动端SDK、Log4J/LogBack等输出中有不一样的形式。 为日志而生的分析引擎 ...

May 30, 2019 · 1 min · jiezi

TalkingData的Spark-On-Kubernetes实践

摘要: 本文整理自talkingdata云架构师徐蓓的分享,介绍了Spark On Kubernetes在TalkingData的实践。众所周知,Spark是一个快速、通用的大规模数据处理平台,和Hadoop的MapReduce计算框架类似。但是相对于MapReduce,Spark凭借其可伸缩、基于内存计算等特点,以及可以直接读写Hadoop上任何格式数据的优势,使批处理更加高效,并有更低的延迟。实际上,Spark已经成为轻量级大数据快速处理的统一平台。Spark作为一个数据计算平台和框架,更多的是关注Spark Application的管理,而底层实际的资源调度和管理更多的是依靠外部平台的支持: Spark官方支持四种Cluster Manager:Spark standalone cluster manager、Mesos、YARN和Kubernetes。由于我们TalkingData是使用Kubernetes作为资源的调度和管理平台,所以Spark On Kubernetes对于我们是最好的解决方案。 如何搭建生产可用的Kubernetes集群部署 目前市面上有很多搭建Kubernetes的方法,比如Scratch、Kubeadm、Minikube或者各种托管方案。因为我们需要简单快速地搭建功能验证集群,所以选择了Kubeadm作为集群的部署工具。部署步骤很简单,在master上执行: kubeadm init在node上执行: kubeadm join --token : --discovery-token-ca-cert-hash sha256:具体配置可见官方文档:https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/。需要注意的是由于国内网络限制,很多镜像无法从k8s.gcr.io获取,我们需要将之替换为第三方提供的镜像,比如:https://hub.docker.com/u/mirrorgooglecontainers/。 网络 Kubernetes网络默认是通过CNI实现,主流的CNI plugin有:Linux Bridge、MACVLAN、Flannel、Calico、Kube-router、Weave Net等。Flannel主要是使用VXLAN tunnel来解决pod间的网络通信,Calico和Kube-router则是使用BGP。由于软VXLAN对宿主机的性能和网络有不小的损耗,BGP则对硬件交换机有一定的要求,且我们的基础网络是VXLAN实现的大二层,所以我们最终选择了MACVLAN。CNI MACVLAN的配置示例如下: { "name": "mynet", "type": "macvlan", "master": "eth0", "ipam": { "type": "host-local", "subnet": "10.0.0.0/17", "rangeStart": "10.0.64.1", "rangeEnd": "10.0.64.126", "gateway": "10.0.127.254", "routes": [ { "dst": "0.0.0.0/0" }, { "dst": "10.0.80.0/24", "gw": "10.0.0.61" } ] }}Pod subnet是10.0.0.0/17,实际pod ip pool是10.0.64.0/20。cluster cidr是10.0.80.0/24。我们使用的IPAM是host-local,规则是在每个Kubernetes node上建立/25的子网,可以提供126个IP。我们还配置了一条到cluster cidr的静态路由10.0.80.0/24,网关是宿主机。这是因为容器在macvlan配置下egress并不会通过宿主机的iptables,这点和Linux Bridge有较大区别。在Linux Bridge模式下,只要指定内核参数net.bridge.bridge-nf-call-iptables = 1,所有进入bridge的流量都会通过宿主机的iptables。经过分析kube-proxy,我们发现可以使用KUBE-FORWARD这个chain来进行pod到service的网络转发: ...

May 23, 2019 · 2 min · jiezi

蚂蚁金服面对亿级并发场景的组件体系设计

作者:吕丹(凝睇),2011 年加入支付宝,先后负责了支付宝 Wap、alipass 卡券、SYNC 数据同步等项目,并参与了多次双十一、双十二、春节红包大促活动,在客户端基础服务方面有一定的项目实践经验与积累。目前负责蚂蚁金服移动开发平台 mPaaS 服务端组件体系优化与架构设计。5 月 6 日,InfoQ 主办的 QCon 2019 全球软件开发大会在北京举行。蚂蚁金服技术专家吕丹(凝睇)在大会上做了《蚂蚁金服面对亿级并发场景的组件体系设计》的分享,我们根据演讲整理如下: 今天,我主要想和大家分享一下移动领域基础组件体系,内容大致可以分为四大块,第一块是标准移动研发所需的基础服务体系,第二块是支撑亿级并发的核心组件“移动接入”的架构演进过程,第三块是双十一、双十二、新春红包这种大促活动的的应付方法,最后一块是目前已经对外输出的基础服务产品。 0. 移动研发基础服务体系 首先介绍一下支付宝客户端的演进过程。之前,支付宝客户端的主要功能是转账、订单支付、交易查询等等,更像是一个工具类的 APP,在需要付钱的时候才会掏出来,用完了就放回去了。2013 年,蚂蚁金服 all in 无线之后,加入了很多服务,例如余额宝、卡券、探索发现等,基本是把支付宝网站上的功能都尽量迁移到客户端,支付宝也逐渐演化成一个平台级别的客户端。之后,随着移动互联网的快速发展,公司内部孵化出了更多的 APP,其他行业也在移动互联网圈内铺开了大量的业务,为了提升用户量、用户粘性,APP 之间也开始进行了大量的业务融合,超级 APP 也因此而诞生,APP 开始朝着生态化的模式发展。 截止到目前为止,支付宝客户端的年活跃用户数超过 8 亿,在大促场景下,同时在线量超过 3 亿,并发请求超过 1 亿,同时上线的用户数超过百万每秒。 而在这些数据的背后一定需要一套庞大、复杂、完整的支撑体系来支持支付宝的运作,移动研发基础服务体系就是其中的重要组成部分。 按照研发过程,我们把移动研发基础服务体系分成四大块:APP 研发阶段,主要包括 App 框架、基础组件、云端服务和研发工具;App 测试阶段 ,主要包括研发协作平台和真机测试平台,其中研发协作平台包含版本管理、迭代管理、安装包编译、构建和打包的能力,而真机测试主要是代替人工服务,减少人工消耗,提升测试效率; App 运维阶段 ,主要包括智能发布、日志回溯、应急管理和动态配置;App 运营阶段,主要包括舆情反馈、实时分析、离线计算和智能营销。 1. 蚂蚁移动接入架构演进 今天的主题为支撑亿级并发下的基础服务,而在亿级并发下移动接入又是最核心、最重要的一个环节。移动接入并不是单个系统,而是一整套组件的总称,包括:Spanner+ 连接管理、API 网关、PUSH 通知和 SYNC 数据同步,它是所有移动业务的流量入口,需要维持客户端的状态,支持进行统一的管控,同时还需要进行部分的业务数据处理。 其实,一开始并没有移动接入这个说法,与支付宝客户端的演进过程类似,后端移动接入也是逐步迭代演进的。最开始,各个业务服务都是自己提供 API 或者直接暴露能力给客户端,没有统一的架构,没有统一的模型,也没有统一的管控。 为了解决这个问题,在 all in 阶段我们引申出了一个 API 网关,由它来做集中式管理,同时添加了 PUSH 推送的能力。因为公司内部有很多 APP,我们希望这些能力能够复用,所以在架构上,我们支持多 APP 同构,客户端会提供多个 SDK,可以随时进行集成。 ...

May 21, 2019 · 2 min · jiezi

MariaDB-开启慢查询-Log找出到底慢在哪个-Query-语句上-Slow-query-logHigh-CPU

本教学使用环境介绍伺服器端:Ubuntu 18.04 LTS资料库:Mariadb 10.1.34(Mysql)语言版本:php 7.3本机端:MacOS High Sierra 网路上写的设定档是 /etc/mysql/my.cnf但我实际去找是在 mariadb.conf.d 底下的 50-server.cnf 才是(可能是 Mysql 跟 MariaDB 的差别?) $ cd /etc/mysql/mariadb.conf.d使用 nano 开启 $ nano 50-server.cnf进入后会看到 [mysqld] ,直接在下面加上 slow_query 变成: [mysqld]# 开启慢日志功能slow_query_log = 1# 查询时间超过 2 秒则定义为慢查询(可自行改秒数)long_query_time = 2# 将产生的 slow query log 放到你指定的地方slow_query_log_file = /var/www/slow_query.log保存后别忘了重启资料库 systemctl restart mariadb.service验证是否成功开启先透过 CLI 进入 Mysql (Mariadb) $ mysql -u root -p进入后输入指令以下 MariaDB [(none)]> 简称为 > > show variables like '%quer%';会出现以下表格 ...

May 16, 2019 · 1 min · jiezi

借助混沌工程工具-ChaosBlade-构建高可用的分布式系统

在分布式架构环境下,服务间的依赖日益复杂,可能没有人能说清单个故障对整个系统的影响,构建一个高可用的分布式系统面临着很大挑战。在可控范围或环境下,使用 ChaosBlade 工具,对系统注入各种故障,持续提升分布式系统的容错和弹性能力,以构建高可用的分布式系统。 ChaosBlade 是什么?ChaosBlade 是一款遵循混沌工程实验原理,建立在阿里巴巴近十年故障测试和演练实践基础上,并结合了集团各业务的最佳创意和实践,提供丰富故障场景实现,帮助分布式系统提升容错性和可恢复性的混沌工程工具。点击这里,了解详情。 ChaosBlade 无需编译,下载解压即可使用,支持基础资源、Java 应用、容器服务类的混沌实验,特点是操作简洁、无侵入、扩展性强。 ChaosBlade @GitHub,点击进入 下面我们以微服务分布式系统举例,一步一步构建高可用的分布式系统。 构建高可用的分布式系统ChaosBlade 的使用方式 ChaoBlade 通过 CLI 方式调用,比如我们模拟 A 服务调用 B 提供的 com.alibaba.demo.HelloService 服务下的 hello 服务延迟 3 秒,我们可以在 B 应用上注入延迟故障,仅需两步操作:第一步:准备阶段。由于 Java 应用的故障注入是通过 Java Agent 机制实现,所以首先要先挂载 agent,执行的命令是 blade prepare jvm --process <PROCESS NAME OF B APPLICATION>第二步:执行阶段,注入故障。执行命令是 blade create dubbo delay --time 3000 --service com.alibaba.demo.HelloService --methodname hello --provider,即对 B 服务提供方提供的 com.alibaba.demo.HelloService#hello 服务注入 3 秒延迟。 ChaosBlade 使用简洁,如果想了解命令的如何使用,可在命令后面添加 -h 参数,比如 blade create dubbo delay -h。更详细的 chaosblade 操作,可详见新手指南 ...

May 14, 2019 · 2 min · jiezi

处理网络超时问题的最佳实践

对于云上的用户来说,业务日志里面报超时问题处理起来往往比价棘手,因为1) 问题点可能在云基础设施层,也有可能在业务软件层,需要排查的范围非常广;2) 这类问题往往是不可复现问题,抓到现场比较难。在本文里就分析下如何来分辨和排查这类问题的根本原因。 业务超时 != 网络丢包由于业务的形态不同,软件实现语言和框架的不同,业务日志中打印出的信息可能是各不相同,比如如下关键字: "SocketTimeOut", "Read timed out", "Request timeout" 等从形式看都属于网络超时这一类,但是需要明确一个概念:这类问题是发生的原因是请求超过了设定的timeout时间,这个设置有可能来自客户端,服务器端或者网络中间节点,这是直接原因。网络丢包可能会导致超时,但是并不是充分条件。总结业务超时和网络丢包的关系如下: 网络丢包可能造成业务超时,但是业务超时的原因不一定是丢包。明确了这个因果关系后,我们再来看怎么分析业务超时。如果武断地将业务超时等同于网络抖动丢包,那这个排查分析过程就完全错过了业务软件层本身的原因,很容易进入困局。 本文会从云基础设施层和业务软件层对业务超时做分析,总体来讲基础设置层面的丢包原因相对容易排查,阿里云有完善的底层监控,根据业务日志报错的对应时间段,从监控数据中可以确定是否有基础设施网络问题。而业务层的超时通常是软件层面的设置,和软件实现及业务形态都有关系,这种往往是更加难以排查的。 网络丢包为什么导致业务超时网络抖动可能造成业务超时,其主要原因是网络抖动会带来不同程度的延迟。本文以互联网大部分应用以来的TCP为对象来介绍,一个丢包对数据传输的完整性其实是没有影响的,因为TCP协议本身已经有精密的设计来处理丢包,乱序等异常情况。并且所有重传的处理都在内核TCP协议栈中完成,操作系统用户空间的进程对这个处理实际上是不感知的。丢包唯一的副作用的就是会增加延迟,如果这段延迟的时间足够长,达到了应用进程设置的某个Timeout时间,那么在业务应用侧表现出来的就是业务超时。 丢包出现时会不会发生超时,取决于应用进程的Timeout设置。比如数据传输中的只丢了一个TCP数据包,引发200 ms后的超时重传: 如果应用设置的Timeout为100 ms,TCP协议栈没有机会重传,应用就认为超时并关闭连接;如果应用设置的Timeout为500 ms,则TCP协议栈会完成重传,这个处理过程对应用进程透明。应用唯一的感知就是处理这次报文交互比基线处理时长多了200 ms,对于时间敏感度不是非常高的应用来说这个影响非常小。延迟到底有多大?在设置应用进程Timeout时间时有没有可以参考的定量值呢?虽然TCP中的RTT/RTO都是动态变化的,但TCP丢包的产生的影响可以做一定的定量总结。 对丢包产生的延迟主要有如下两类: TCP建连超时。如果网络抖动不幸丢掉了TCP的第一个建连SYN报文,对与不太老的内核版本来说,客户端会在1秒(Draft RFC 2988bis-02中定义)后重传SYN报文再次发起建连。1秒对于内网环境来说非常大,对于阿里云一个区域的机房来说,正常的RTT都是小个位数毫秒级别,1秒内如果没有丢包足够完成百个数据报的交互。TCP中间数据包丢包。TCP协议处理中间的数据丢包有快速重传和超时重传两种机制。快速重传通常比较快,和RTT相关,没有定量的值。超时重传 (RTO, Retrasmission Timeout) 也和RTT相关,但是Linux中定义的RTO的最小值为,TCP_RTO_MIN = 200ms。所以在RTT比较小的网络环境下,即使RTT小于1ms,TCP超时重传的RTO最小值也只能是200ms。这类丢包带来的延迟相对小。除了丢包,另外一类比较常见的延迟是TCP Delayed ACK带来的延迟。这个和协议设计相关,和丢包其实没有关系,在这里一并总结如延迟定量部分。在交互式数据流+Nagle算法的场景下比较容易触发。Linux中定义的Delayed ACK的最小值TCP_DELACK_MIN是40 ms。 所以总结下来有如下几个定量时间可以供参考: 40 ms, 在交互数据流中TCP Delayed ACK + Nagle算法开启的场景,最小delay值。200 ms,在RTT比较小的正常网络环境中,TCP数据包丢包,超时重传的最小值。1 s,较新内核版本TCP建立的第一个SYN包丢包时的重传时间,RFC2988bis中定义的initial RTO value TCP_TIMEOUT_INIT。3 s, 较老内核版本TCP建立的第一个SYN包丢包时的重传时间,RFC 1122中定义的initial RTO value TCP_TIMEOUT_INIT。云基础设施网络丢包基础设施网络丢包的原因可能有多种,主要总结如下3类: 云基础设施网络抖动 网络链路,物理网络设备,ECS/RDS等所在的宿主机虚拟化网络都有可能出现软硬件问题。云基础设施已经做了完备的冗余,来保证出现问题时能快速隔离,切换和恢复。 现象: 因为有网络冗余设备并可以快速恢复,这类问题通常表现为某单一时间点网络抖动,通常为秒级。抖动的具体现象是在那个时段新建连接失败,已建立的连接中断,在业务上可能表现为超时。影响面: 网络设备下通常挂很多主机,通常影响面比较大,比如同时影响多个ECS到RDS的连接。 云产品的限速丢包 很多网络云产品在售卖的时候有规格和带宽选项,比如ECS, SLB, NAT网关等。当云产品的流量或者连接数超过规格或者带宽限制时,也会出现丢包。这种丢包并非云厂商的故障,而是实际业务流量规模和选择云产品规格时的偏差所带来。这种问题通常从云产品提供的监控中就能分辨出来。 现象: 当流量或者连接数超过规格时,出现流量或者连接丢弃。问题可能间断并持续地出现,网络流量高峰期出现的概率更大。影响面: 通常只影响单一实例。但对于NAT网关做SNAT的场景,可能影响SNAT后的多个实例。 运营商网络问题在走公网的场景中,客户端和服务器之间的报文交互往往要经过多个AS (autonomous system)。若运营商中间链路出现问题往往会导致端到端丢包。 ...

May 14, 2019 · 1 min · jiezi

如何把创建ECS(CreateInstance)作为触发器来触发函数计算

摘要: 函数计算是阿里云上类似lamda的服务。 本文介绍了如何通过日志服务投递ECS创建等行为的日志,从而触发函数计算服务。问题描述函数计算虽然不支持直接集成到ECS的管控事件上,但是函数计算本身是支持日志服务作为触发器的。即可以配置日志服务中logstore里的增强日志作为触发器来触发函数计算服务中的函数,同时可以传递project 和 logstore的name以及beginCursor/endCursor 等相关日志信息作为event到函数计算服务,供其做二次处理和加工。这样相当于提供了一个思路,即我们可以把创建ECS或者其他相关的操作想办法作为日志投递到日志服务中,这样就可以触发相关的函数计算服务了。那么这种方法是什么呢?一种可行的方式是操作审计服务。操作审计可以记录所有API级别的用户记录,当然也包括CreateInstance这类操作。所以整个流程就变成了:开通操作审计服务->配置操作审计跟踪,将event投递到日志服务中->配置日志服务作为函数计算触发器并传递日志->触发函数举个栗子开通操作审计服务后,创建一个日志跟踪然后创建一个实例,可以看到操作审计记录了这个行为同时日志服务里也找到了这个行为记录接下来我们可以配置一个函数计算服务,具体的过程可以参考文中最后的文档,这里强调下配置触发器的配置,这里要注意的是图中有关logstore的配置,上面的是触发日志的logstore,下面的是写日志的lostore,不能搞混。然后复制进去一段代码,这段代码的核心是拿到触发event的具体日志信息,然后写到函数计算本地的日志库里。# -- coding: utf-8 --import loggingimport jsonfrom aliyun.log import LogClientfrom time import timedef logClient(endpoint, creds): logger = logging.getLogger() logger.info(‘creds info’) logger.info(creds.access_key_id) logger.info(creds.access_key_secret) logger.info(creds.security_token) accessKeyId = ‘XXX’ accessKey = ‘XXX’ client = LogClient(endpoint, accessKeyId, accessKey) return clientdef handler(event, context): logger = logging.getLogger() logger.info(‘start deal SLS data’) logger.info(event.decode().encode()) info_arr = json.loads(event.decode()) fetchdata(info_arr[‘source’],context) return ‘hello world’def fetchdata(event,context): logger = logging.getLogger() endpoint = event[’endpoint’] creds = context.credentials client = logClient(endpoint, creds) if client == None : logger.info(“client creat failed”) return False project = event[‘projectName’] logstore = event[’logstoreName’] start_cursor = event[‘beginCursor’] end_cursor = event[’endCursor’] loggroup_count = 10 shard_id = event[‘shardId’] while True: res = client.pull_logs(project, logstore, shard_id, start_cursor, loggroup_count, end_cursor) res.log_print() next_cursor = res.get_next_cursor() if next_cursor == start_cursor : break start_cursor = next_cursor #log_data = res.get_loggroup_json_list() return True以上配置完成后,一个控制台创建ECS(当然也包括其他可以被审计的行为)的行为就可以用来触发函数计算的函数了。结果我们把刚才创建的实例再释放掉,看到操作审计的日志然后我们在函数计算的日志库里也看到了对应的日志,这个日志是刚才操作审计记录的日志传递给函数计算并记录的。在真正的应用场景下,客户可以拿到这个日志中的相关信息做更多操作。更多sample可以参考:https://github.com/aliyun/aliyun-log-fc-functions总结产品侧无法直接支持的功能,可以看下是否有workaround很多阿里云产品之间的集成,都可以看下是否可以通过日志服务来做。参考资料https://help.aliyun.com/document_detail/84090.html?spm=a2c4g.11174283.6.619.5a6552120001Hlhttps://help.aliyun.com/document_detail/60781.html?spm=a2c4g.11186623.2.12.62727c9fIeDQwY本文作者:拱卒阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 15, 2019 · 1 min · jiezi

恭喜 Fluentd 从 CNCF 毕业

今年新闻不断,多数早期进入 CNCF 的项目都相继宣布毕业。CNCF(云原生计算基金会)在美国时间 2019 年 4 月 11 日宣布 fluentd 今天正式毕业了。这是 CNCF 中毕业的第 6 个项目,之前已经毕业的项目为 Kubernetes、Prometheus、Envoy 、CoreDNS 和 containerd 。fluentd 自 2011 年由 Treasure Data 公司的联合创始人 Sadayuki “Sada” Furuhashi 创建,作为构建统一记录层的开源数据收集器,统一记录层统一收集采集和消费,以便更好的使用和理解数据。在 2016 年 11 月,fluentd 也是第 6 个成为 CNCF 托管项目的。fluentd 可以从多种数据源采集事件,并将它写入文件, RDBMS, NoSQL, IaaS, SaaS, Hadoop等等各类的目标地址。截至目前,fluentd 在 GitHub 上有 7629 个 star ,895 个 fork,以及 166 位贡献者,超过 4k+ commit 。做日志相关的小伙伴基本都玩过 ELK ,我们都知道在大规模使用 Logstash 时的痛苦(还记得被 Logstash 配置文件支配的恐惧吗? 2333) 而 fluentd 的事件路由是通过 tag 来做,相比 Logstash 使用管道将所有数据路由到单个流里再通过配置将它发送到对应的目标而言这将大大简化配置的复杂度。(是的,这里是吐槽)再一个,便是需要考虑部署和插件生态,首先来说部署:fluentd 使用 C + Ruby 编写(Ruby 写起来蛮舒服的,早先写过一段时间),只要有 Ruby 的环境,可以很方便的进行部署。而大多数的 Linux 发行版是默认带着 Ruby 环境的,这也非常方便。Logstash 使用 JRuby 编写(JRuby 就是使用 Java 实现的 Ruby 解释器),部署时需要有 JDK 和 JRuby 的环境。这里只做陈述,不再展开。回到插件生态上:两者都有丰富的插件,并且编写插件也很简单。不过插件这种东西,按需使用,日常需要的基本都能找的到。唯一需要注意的就是选择插件时,需要仔细甄别。“Fluentd has earned its place as the industry standard for log collection and shipping, and I am excited to see it as a graduated CNCF project,” said Gabe Monroy, Lead Program Manager for Containers, Microsoft Azure. “At Microsoft, we are proud to use Fluentd to power our cloud native logging subsystems and we look forward to working with the growing the open source community around Fluentd.”引用一段话,fluentd 是否成为整个日志收集的行业标准,这个我不确定, 但在它托管至 CNCF 后,在云原生领域它确实发展迅速,多数公司都会采用 EFK 的方式进行云原生时代下的日志方案。附一张 fluentd 的图,有空会写下 fluentd 的使用姿势 (flag++)再次恭喜 fluentd 毕业。可以通过下面二维码订阅我的文章公众号【MoeLove】 ...

April 12, 2019 · 1 min · jiezi

Node.js 应用故障排查手册 —— 大纲与常规问题指标简介

楔子你是否想要尝试进行 Node.js 应用开发但是又总听人说它不安全、稳定性差,想在公司推广扩张大前端的能力范畴和影响又说服不了技术领导。JavaScript 发展到今天,早已脱离原本浏览器的战场,借助于 Node.js 的诞生将其触角伸到了服务端、PC 跨平台客户端方案等各个领域,但是与此同时,JS Runtime 对于绝大部分的开发者来说又一如既往的处于黑盒状态——开发者无法感知其运行状态,出现一些性能、内存问题时也没有很好的工具链进行更深入的支持。本书将在基于 Node.js 性能平台 的基础上,从多个大家开发上线过程中可能遇到的疑难杂症的视角,观察如何去发现、定位和解决这些问题,帮助读者构建对 Node.js 这门语言的更多信心。因为本书将属于 Node.js 开发进阶的内容,因此我们希望本书的读者具备以下的基本技能:常规的 Node.js 应用开发的能力常规的服务器性能指标参数的理解,比如 CPU、Memory、Load、文件打开数等常见的数据库、缓存等操作负载均衡、多进程模型如果使用容器,容器的基本知识,资源管理等本书首发在 Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区会同步更新。常规排查的指标当我们第一次遇到线上异常时,很多人会感觉无从下手。本节作为预备篇,将从服务器异常时常见的排查指标开始,帮助大家建立一个更加直观的问题处理体系。毕竟如果我们面对线上异常时,如果连系统哪里有问题都不知道,那么后续的借助 Node.js 性能平台 更深入定位问题代码就更加无从谈起了。错误日志当我们的应用出现问题时,首先需要去查看我们应用的错误日志,观察在这段时间内是不是有错误在一直抛出,导致了我们的服务不稳定。这一块的信息显然是因各个应用而异的,当我们的项目比较大(Ecs/Docker 节点比较多)的时候,就需要对错误日志的进行统一的采集收集来保证出问题时的快速定位。一个比较简单的统一日志平台可以设计如下:其中的采集服务器和 Agent 上报之间一般会采用消息队列(Kafka)来作为缓冲区减轻双方的负载,ELK 就是一个比较成熟的日志服务。有了统一的日志平台后,当我们的应用出现问题时,首先应该去日志平台上查看当前的错误日志信息,特别是对于那些在 频繁出现 的错误日志应当引起警惕,需要去仔细地结合产生错误的代码段进行回溯确认是否是造成当前服务不稳定的元凶,Node.js 性能平台 也实现了一个简单的错误日志回溯 + 告警的系统,本书第二部分会更详细说明。系统指标如果在上述的错误日中没有看到可疑的信息(实际上错误日志以及本节的系统指标排查先后顺序并无固定,大家可以视自己的需求进行),那么接下来我们就应该关注下问题是不是因为服务器或者 Node.js 应用本身的负载到了极限导致的问题。一些比较常见的大家需要关注的系统指标如下所示:CPU & MemoryDisk 磁盘占用率I/O 负载TCP 连接状态下面逐一讲解这些可能存在问题的系统指标。I. CPU & Memory使用 top 命令来观察和 Node.js 应用进程的 CPU 和 Memory 负载情况。一般来说,对于 CPU 很高 Node.js 进程,我们可以使用 Node.js 性能平台 提供的 CPU Profiling 工具来在线 Dump 出当前的 Javascript 运行情况,进而找到热点代码进行优化,具体在本书第二部分会有更详细地说明。那么对于 Memory 负载很高的情况,正常来说就是发生了内存泄漏(或者有预期之外的内存分配导致溢出),那么同样的我们可以用性能平台提供的工具来在线 Dump 出当前的 Javascript 堆内存和服务化的分析来结合你的业务代码找到产生泄漏的逻辑。这里需要注意的是,目前性能平台能够进行详尽分析的地方集中在你的 JS 代码上,对于完全是 C++ 扩展执行的或者完全的 V8/Libuv 底层执行(这部分功能后面会补上)的逻辑,以及不分配在 V8 Heap 上的内存,性能平台目前没有更好的办法来进行分析处理。而实际上在我们遇到的案例中,大家编写的 JS 代码出问题占了绝大部分,也就是性能平台目前针对 JS 部分比较完善的在线 Dump + 服务化分析基本上能够解决开发者 95% 甚至以上的问题了。II. Disk 磁盘占用率使用 df 命令可以观察当前的磁盘占用情况,这个也是非常常见的问题,很多开发者会忽略对服务器磁盘的监控告警,当我们的日志/核心转储等大文件逐渐将磁盘打满到 100% 的时候,Node.js 应用很可能会无法正常运行,Node.js 性能平台 目前也提供了对磁盘的监控,在本书第二部分同样会有更详细地说明。III. I/O 负载使用 top/iostat 和 cat /proc/${pid}/io 来查看当前的 I/O 负载,这一项的负载很高的话,也会使得 Node.js 应用出现卡死等情况。IV. TCP 连接状态绝大部分的 Node.js 应用实际上是 Web 应用,每个用户的连接都会创建一个 Socket 连接,在一些异常情况下(比如遭受半连接攻击或者内核参数设置不合理),服务器上会有大量的 TIME_WAIT 状态的连接,而大量的 TIME_WAIT 积压会导致 Node.js 应用的卡死(内核无法为新的请求分配创建新的 TCP 连接),我们可以使用 netstat -ant|awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print (a,S[a])}’ 命令来确认这个问题。核心转储(Core dump)线上 Node.js 应用故障往往也伴随着进程的 Crash,借助于一些守护进程的自检重启拉起,我们的服务依旧在运行,但是我们不应该去忽略这些意外的 Crash —— 当流量增大或者造成服务器的问题用户访问被别有用心之人抓住时,我们集群就变得岌岌可危了。绝大部分情况下,会造成 Node.js 应用 Crash 掉的错误日志往往并不会记录到我们的错误日志文件中,幸运的是,服务器内核提供了一项机制帮助我们在应用 Crash 时自动地生成核心转储(Core dump)文件,让开发者可以在事后进行分析还原案发现场。核心转储核心转储(Core dump)实际上是我们的应用意外崩溃终止时,计算机自动记录下进程 Crash 掉那一刻的内存分配信息、Program counter 以及堆栈指针等关键信息来生成核心转储文件,因此获取到核心转储文件后,我们可以通过 MDB、GDB、LLDB 等工具即可实现解析诊断实际进程的 Crash 原因。生成文件触发核心转储生成转储文件目前主要有两种方式:I. 设置内核参数使用 ulimit -c unlimited 打开内核限制,并且考虑到默认运行模式下,Node.js 对 JS 造成的 Crash 是不会触发核心转储动作的,因此我们可以在 Node 应用启动时加上参数 –abort-on-uncaught-exception 来对出现未捕获的异常时也能让内核触发自动的核心转储动作。II. 手动调用手动调用 gcore <pid> (可能需要 sudo 权限)的方式来手动生成,因为此时 Node.js 应用依旧在运行中,所以实际上这种方式一般用于 「活体检验」,用于 Node.js 进程假死状态 下的问题定位。这里需要注意的是,以上的生成核心转储的操作都 并没有那么安全务必记得对服务器磁盘进行监控和告警**。获取到 Node.js 应用生成的核心转储文件后,我们可以借助于 Node.js 性能平台 提供的在线 Core dump 文件分析功能进行分析定位进程 Crash 的原因了,具体用法会在本书第二部分进行说明。小结本节从常见的几个服务器问题点,给大家对线上 Node.js 应用出现故障时如何去排查定位有了一些大概的印象,本章也是后续内容的一个预备知识,了解了这部分内容,才能在后面的一些实战案例中明白为何我们忽略了其它而选择详尽地服务化分析其中的一些要点。而核心转储的深入分析则能够帮助我们解决 Node.js 应用的绝大部分底层故障,因为其可以还原出问题 JavaScript 代码和引发问题的参数,功能非常地强大。本文作者:奕钧阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 11, 2019 · 1 min · jiezi

在线生成txt图案或者logo

有些同学喜欢在项目启动日志时加上logo之类的,特别java启动的项目,如Jhispter。这里介绍一个小工具,可以生成图案的txt文本。如下面的图案_/ | | | _ _ | | | | | | |/ | | | |/ | |__| | || | () | || | /|,|/ , || / | |_/ 工具是在线生成的,地址:http://patorjk.com/software/t…在文本框中输入你想要的文字,选择自己喜欢的字体即可在线预览。使用的话像文字那样直接拷贝下来粘帖就可以。

April 11, 2019 · 1 min · jiezi

探索Java日志的奥秘:底层日志系统-log4j2

前言log4j2是apache在log4j的基础上,参考logback架构实现的一套新的日志系统(我感觉是apache害怕logback了)。log4j2的官方文档上写着一些它的优点:在拥有全部logback特性的情况下,还修复了一些隐藏问题API 分离:现在log4j2也是门面模式使用日志,默认的日志实现是log4j2,当然你也可以用logback(应该没有人会这么做)性能提升:log4j2包含下一代基于LMAX Disruptor library的异步logger,在多线程场景下,拥有18倍于log4j和logback的性能多API支持:log4j2提供Log4j 1.2, SLF4J, Commons Logging and java.util.logging (JUL) 的API支持避免锁定:使用Log4j2 API的应用程序始终可以选择使用任何符合SLF4J的库作为log4j-to-slf4j适配器的记录器实现自动重新加载配置:与Logback一样,Log4j 2可以在修改时自动重新加载其配置。与Logback不同,它会在重新配置发生时不会丢失日志事件。高级过滤: 与Logback一样,Log4j 2支持基于Log事件中的上下文数据,标记,正则表达式和其他组件进行过滤。插件架构: Log4j使用插件模式配置组件。因此,您无需编写代码来创建和配置Appender,Layout,Pattern Converter等。Log4j自动识别插件并在配置引用它们时使用它们。属性支持:您可以在配置中引用属性,Log4j将直接替换它们,或者Log4j将它们传递给将动态解析它们的底层组件。Java 8 Lambda支持自定义日志级别产生垃圾少:在稳态日志记录期间,Log4j 2 在独立应用程序中是无垃圾的,在Web应用程序中是低垃圾。这减少了垃圾收集器的压力,并且可以提供更好的响应时间性能。和应用server集成:版本2.10.0引入了一个模块log4j-appserver,以改进与Apache Tomcat和Eclipse Jetty的集成。Log4j2类图:这次从四个地方去探索源码:启动,配置,异步,插件化源码探索启动log4j2的关键组件LogManager根据配置指定LogContexFactory,初始化对应的LoggerContextLoggerContext1、解析配置文件,解析为对应的java对象。2、通过LoggerRegisty缓存Logger配置3、Configuration配置信息4、start方法解析配置文件,转化为对应的java对象5、通过getLogger获取logger对象LoggerLogManaer该组件是Log4J启动的入口,后续的LoggerContext以及Logger都是通过调用LogManager的静态方法获得。我们可以使用下面的代码获取LoggerLogger logger = LogManager.getLogger();可以看出LogManager是十分关键的组件,因此在这个小节中我们详细分析LogManager的启动流程。LogManager启动的入口是下面的static代码块:/** * Scans the classpath to find all logging implementation. Currently, only one will be used but this could be * extended to allow multiple implementations to be used. / static { // Shortcut binding to force a specific logging implementation. final PropertiesUtil managerProps = PropertiesUtil.getProperties(); final String factoryClassName = managerProps.getStringProperty(FACTORY_PROPERTY_NAME); if (factoryClassName != null) { try { factory = LoaderUtil.newCheckedInstanceOf(factoryClassName, LoggerContextFactory.class); } catch (final ClassNotFoundException cnfe) { LOGGER.error(“Unable to locate configured LoggerContextFactory {}”, factoryClassName); } catch (final Exception ex) { LOGGER.error(“Unable to create configured LoggerContextFactory {}”, factoryClassName, ex); } } if (factory == null) { final SortedMap<Integer, LoggerContextFactory> factories = new TreeMap<>(); // note that the following initial call to ProviderUtil may block until a Provider has been installed when // running in an OSGi environment if (ProviderUtil.hasProviders()) { for (final Provider provider : ProviderUtil.getProviders()) { final Class<? extends LoggerContextFactory> factoryClass = provider.loadLoggerContextFactory(); if (factoryClass != null) { try { factories.put(provider.getPriority(), factoryClass.newInstance()); } catch (final Exception e) { LOGGER.error(“Unable to create class {} specified in provider URL {}”, factoryClass.getName(), provider .getUrl(), e); } } } if (factories.isEmpty()) { LOGGER.error(“Log4j2 could not find a logging implementation. " + “Please add log4j-core to the classpath. Using SimpleLogger to log to the console…”); factory = new SimpleLoggerContextFactory(); } else if (factories.size() == 1) { factory = factories.get(factories.lastKey()); } else { final StringBuilder sb = new StringBuilder(“Multiple logging implementations found: \n”); for (final Map.Entry<Integer, LoggerContextFactory> entry : factories.entrySet()) { sb.append(“Factory: “).append(entry.getValue().getClass().getName()); sb.append(”, Weighting: “).append(entry.getKey()).append(’\n’); } factory = factories.get(factories.lastKey()); sb.append(“Using factory: “).append(factory.getClass().getName()); LOGGER.warn(sb.toString()); } } else { LOGGER.error(“Log4j2 could not find a logging implementation. " + “Please add log4j-core to the classpath. Using SimpleLogger to log to the console…”); factory = new SimpleLoggerContextFactory(); } } }这段静态代码段主要分为下面的几个步骤:首先根据特定配置文件的配置信息获取loggerContextFactory如果没有找到对应的Factory的实现类则通过ProviderUtil中的getProviders()方法载入providers,随后通过provider的loadLoggerContextFactory方法载入LoggerContextFactory的实现类如果provider中没有获取到LoggerContextFactory的实现类或provider为空,则使用SimpleLoggerContextFactory作为LoggerContextFactory。根据配置文件载入LoggerContextFactory// Shortcut binding to force a specific logging implementation. final PropertiesUtil managerProps = PropertiesUtil.getProperties(); final String factoryClassName = managerProps.getStringProperty(FACTORY_PROPERTY_NAME); if (factoryClassName != null) { try { factory = LoaderUtil.newCheckedInstanceOf(factoryClassName, LoggerContextFactory.class); } catch (final ClassNotFoundException cnfe) { LOGGER.error(“Unable to locate configured LoggerContextFactory {}”, factoryClassName); } catch (final Exception ex) { LOGGER.error(“Unable to create configured LoggerContextFactory {}”, factoryClassName, ex); } }在这段逻辑中,LogManager优先通过配置文件”log4j2.component.properties”通过配置项”log4j2.loggerContextFactory”来获取LoggerContextFactory,如果用户做了对应的配置,通过newCheckedInstanceOf方法实例化LoggerContextFactory的对象,最终的实现方式为:public static <T> T newInstanceOf(final Class<T> clazz) throws InstantiationException, IllegalAccessException, InvocationTargetException { try { return clazz.getConstructor().newInstance(); } catch (final NoSuchMethodException ignored) { // FIXME: looking at the code for Class.newInstance(), this seems to do the same thing as above return clazz.newInstance(); } }在默认情况下,不存在初始的默认配置文件log4j2.component.properties,因此需要从其他途径获取LoggerContextFactory。通过Provider实例化LoggerContextFactory对象代码:if (factory == null) { final SortedMap<Integer, LoggerContextFactory> factories = new TreeMap<>(); // note that the following initial call to ProviderUtil may block until a Provider has been installed when // running in an OSGi environment if (ProviderUtil.hasProviders()) { for (final Provider provider : ProviderUtil.getProviders()) { final Class<? extends LoggerContextFactory> factoryClass = provider.loadLoggerContextFactory(); if (factoryClass != null) { try { factories.put(provider.getPriority(), factoryClass.newInstance()); } catch (final Exception e) { LOGGER.error(“Unable to create class {} specified in provider URL {}”, factoryClass.getName(), provider .getUrl(), e); } } } if (factories.isEmpty()) { LOGGER.error(“Log4j2 could not find a logging implementation. " + “Please add log4j-core to the classpath. Using SimpleLogger to log to the console…”); factory = new SimpleLoggerContextFactory(); } else if (factories.size() == 1) { factory = factories.get(factories.lastKey()); } else { final StringBuilder sb = new StringBuilder(“Multiple logging implementations found: \n”); for (final Map.Entry<Integer, LoggerContextFactory> entry : factories.entrySet()) { sb.append(“Factory: “).append(entry.getValue().getClass().getName()); sb.append(”, Weighting: “).append(entry.getKey()).append(’\n’); } factory = factories.get(factories.lastKey()); sb.append(“Using factory: “).append(factory.getClass().getName()); LOGGER.warn(sb.toString()); } } else { LOGGER.error(“Log4j2 could not find a logging implementation. " + “Please add log4j-core to the classpath. Using SimpleLogger to log to the console…”); factory = new SimpleLoggerContextFactory(); } }这里比较有意思的是hasProviders和getProviders都会通过线程安全的方式去懒加载ProviderUtil这个对象。跟进lazyInit方法:protected static void lazyInit() { //noinspection DoubleCheckedLocking if (INSTANCE == null) { try { STARTUP_LOCK.lockInterruptibly(); if (INSTANCE == null) { INSTANCE = new ProviderUtil(); } } catch (final InterruptedException e) { LOGGER.fatal(“Interrupted before Log4j Providers could be loaded.”, e); Thread.currentThread().interrupt(); } finally { STARTUP_LOCK.unlock(); } } }再看构造方法:private ProviderUtil() { for (final LoaderUtil.UrlResource resource : LoaderUtil.findUrlResources(PROVIDER_RESOURCE)) { loadProvider(resource.getUrl(), resource.getClassLoader()); } }这里的懒加载其实就是懒加载Provider对象。在创建新的providerUtil实例的过程中就会直接实例化provider对象,其过程是先通过getClassLoaders方法获取provider的类加载器,然后通过loadProviders(classLoader);加载类。在providerUtil实例化的最后,会统一查找”META-INF/log4j-provider.properties”文件中对应的provider的url,会考虑从远程加载provider。而loadProviders方法就是在ProviderUtil的PROVIDERS列表中添加对一个的provider。可以看到默认的provider是org.apache.logging.log4j.core.impl.Log4jContextFactoryLoggerContextFactory = org.apache.logging.log4j.core.impl.Log4jContextFactoryLog4jAPIVersion = 2.1.0FactoryPriority= 10很有意思的是这里懒加载加上了锁,而且使用的是lockInterruptibly这个方法。lockInterruptibly和lock的区别如下:lock 与 lockInterruptibly比较区别在于:lock 优先考虑获取锁,待获取锁成功后,才响应中断。lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。ReentrantLock.lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt 方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。 ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。上面有一句注释值得注意:/* * Guards the ProviderUtil singleton instance from lazy initialization. This is primarily used for OSGi support. * * @since 2.1 / protected static final Lock STARTUP_LOCK = new ReentrantLock(); // STARTUP_LOCK guards INSTANCE for lazy initialization; this allows the OSGi Activator to pause the startup and // wait for a Provider to be installed. See LOG4J2-373 private static volatile ProviderUtil INSTANCE;原来这里是为了让osgi可以阻止启动。再回到logManager:可以看到在加载完Provider之后,会做factory的绑定:if (factories.isEmpty()) { LOGGER.error(“Log4j2 could not find a logging implementation. " + “Please add log4j-core to the classpath. Using SimpleLogger to log to the console…”); factory = new SimpleLoggerContextFactory(); } else if (factories.size() == 1) { factory = factories.get(factories.lastKey()); } else { final StringBuilder sb = new StringBuilder(“Multiple logging implementations found: \n”); for (final Map.Entry<Integer, LoggerContextFactory> entry : factories.entrySet()) { sb.append(“Factory: “).append(entry.getValue().getClass().getName()); sb.append(”, Weighting: “).append(entry.getKey()).append(’\n’); } factory = factories.get(factories.lastKey()); sb.append(“Using factory: “).append(factory.getClass().getName()); LOGGER.warn(sb.toString()); }到这里,logmanager的启动流程就结束了。配置在不使用slf4j的情况下,我们获取logger的方式是这样的:Logger logger = logManager.getLogger(xx.class)跟进getLogger方法: public static Logger getLogger(final Class<?> clazz) { final Class<?> cls = callerClass(clazz); return getContext(cls.getClassLoader(), false).getLogger(toLoggerName(cls)); }这里有一个getContext方法,跟进,public static LoggerContext getContext(final ClassLoader loader, final boolean currentContext) { try { return factory.getContext(FQCN, loader, null, currentContext); } catch (final IllegalStateException ex) { LOGGER.warn(ex.getMessage() + " Using SimpleLogger”); return new SimpleLoggerContextFactory().getContext(FQCN, loader, null, currentContext); } }上文提到factory的具体实现是Log4jContextFactory,跟进getContext方法:public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext, final boolean currentContext) { final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext); if (externalContext != null && ctx.getExternalContext() == null) { ctx.setExternalContext(externalContext); } if (ctx.getState() == LifeCycle.State.INITIALIZED) { ctx.start(); } return ctx; }直接看start:public void start() { LOGGER.debug(“Starting LoggerContext[name={}, {}]…”, getName(), this); if (PropertiesUtil.getProperties().getBooleanProperty(“log4j.LoggerContext.stacktrace.on.start”, false)) { LOGGER.debug(“Stack trace to locate invoker”, new Exception(“Not a real error, showing stack trace to locate invoker”)); } if (configLock.tryLock()) { try { if (this.isInitialized() || this.isStopped()) { this.setStarting(); reconfigure(); if (this.configuration.isShutdownHookEnabled()) { setUpShutdownHook(); } this.setStarted(); } } finally { configLock.unlock(); } } LOGGER.debug(“LoggerContext[name={}, {}] started OK.”, getName(), this); }发现其中的核心方法是reconfigure方法,继续跟进:private void reconfigure(final URI configURI) { final ClassLoader cl = ClassLoader.class.isInstance(externalContext) ? (ClassLoader) externalContext : null; LOGGER.debug(“Reconfiguration started for context[name={}] at URI {} ({}) with optional ClassLoader: {}”, contextName, configURI, this, cl); final Configuration instance = ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl); if (instance == null) { LOGGER.error(“Reconfiguration failed: No configuration found for ‘{}’ at ‘{}’ in ‘{}’”, contextName, configURI, cl); } else { setConfiguration(instance); / * instance.start(); Configuration old = setConfiguration(instance); updateLoggers(); if (old != null) { * old.stop(); } / final String location = configuration == null ? “?” : String.valueOf(configuration.getConfigurationSource()); LOGGER.debug(“Reconfiguration complete for context[name={}] at URI {} ({}) with optional ClassLoader: {}”, contextName, location, this, cl); } }可以看到每一个configuration都是从ConfigurationFactory拿出来的,我们先看看这个类的getInstance看看:public static ConfigurationFactory getInstance() { // volatile works in Java 1.6+, so double-checked locking also works properly //noinspection DoubleCheckedLocking if (factories == null) { LOCK.lock(); try { if (factories == null) { final List<ConfigurationFactory> list = new ArrayList<ConfigurationFactory>(); final String factoryClass = PropertiesUtil.getProperties().getStringProperty(CONFIGURATION_FACTORY_PROPERTY); if (factoryClass != null) { addFactory(list, factoryClass); } final PluginManager manager = new PluginManager(CATEGORY); manager.collectPlugins(); final Map<String, PluginType<?>> plugins = manager.getPlugins(); final List<Class<? extends ConfigurationFactory>> ordered = new ArrayList<Class<? extends ConfigurationFactory>>(plugins.size()); for (final PluginType<?> type : plugins.values()) { try { ordered.add(type.getPluginClass().asSubclass(ConfigurationFactory.class)); } catch (final Exception ex) { LOGGER.warn(“Unable to add class {}”, type.getPluginClass(), ex); } } Collections.sort(ordered, OrderComparator.getInstance()); for (final Class<? extends ConfigurationFactory> clazz : ordered) { addFactory(list, clazz); } // see above comments about double-checked locking //noinspection NonThreadSafeLazyInitialization factories = Collections.unmodifiableList(list); } } finally { LOCK.unlock(); } } LOGGER.debug(“Using configurationFactory {}”, configFactory); return configFactory; }这里可以看到ConfigurationFactory中利用了PluginManager来进行初始化,PluginManager会将ConfigurationFactory的子类加载进来,默认使用的XmlConfigurationFactory,JsonConfigurationFactory,YamlConfigurationFactory这三个子类,这里插件化加载暂时按下不表。回到reconfigure这个方法,我们看到获取ConfigurationFactory实例之后会去调用getConfiguration方法:public Configuration getConfiguration(final String name, final URI configLocation, final ClassLoader loader) { if (!isActive()) { return null; } if (loader == null) { return getConfiguration(name, configLocation); } if (isClassLoaderUri(configLocation)) { final String path = extractClassLoaderUriPath(configLocation); final ConfigurationSource source = getInputFromResource(path, loader); if (source != null) { final Configuration configuration = getConfiguration(source); if (configuration != null) { return configuration; } } } return getConfiguration(name, configLocation); }跟进getConfiguration,这里值得注意的是有很多个getConfiguration,注意甄别,如果不确定的话可以通过debug的方式来确定。public Configuration getConfiguration(final String name, final URI configLocation) { if (configLocation == null) { final String config = this.substitutor.replace( PropertiesUtil.getProperties().getStringProperty(CONFIGURATION_FILE_PROPERTY)); if (config != null) { ConfigurationSource source = null; try { source = getInputFromUri(FileUtils.getCorrectedFilePathUri(config)); } catch (final Exception ex) { // Ignore the error and try as a String. LOGGER.catching(Level.DEBUG, ex); } if (source == null) { final ClassLoader loader = LoaderUtil.getThreadContextClassLoader(); source = getInputFromString(config, loader); } if (source != null) { for (final ConfigurationFactory factory : factories) { final String[] types = factory.getSupportedTypes(); if (types != null) { for (final String type : types) { if (type.equals(””) || config.endsWith(type)) { final Configuration c = factory.getConfiguration(source); if (c != null) { return c; } } } } } } } } else { for (final ConfigurationFactory factory : factories) { final String[] types = factory.getSupportedTypes(); if (types != null) { for (final String type : types) { if (type.equals(”*”) || configLocation.toString().endsWith(type)) { final Configuration config = factory.getConfiguration(name, configLocation); if (config != null) { return config; } } } } } } Configuration config = getConfiguration(true, name); if (config == null) { config = getConfiguration(true, null); if (config == null) { config = getConfiguration(false, name); if (config == null) { config = getConfiguration(false, null); } } } if (config != null) { return config; } LOGGER.error(“No log4j2 configuration file found. Using default configuration: logging only errors to the console.”); return new DefaultConfiguration(); }这里就会根据之前加载进来的factory进行配置的获取,具体的不再解析。回到reconfigure,之后的步骤就是setConfiguration,入参就是刚才获取的configprivate synchronized Configuration setConfiguration(final Configuration config) { Assert.requireNonNull(config, “No Configuration was provided”); final Configuration prev = this.config; config.addListener(this); final ConcurrentMap<String, String> map = config.getComponent(Configuration.CONTEXT_PROPERTIES); try { // LOG4J2-719 network access may throw android.os.NetworkOnMainThreadException map.putIfAbsent(“hostName”, NetUtils.getLocalHostname()); } catch (final Exception ex) { LOGGER.debug(“Ignoring {}, setting hostName to ‘unknown’”, ex.toString()); map.putIfAbsent(“hostName”, “unknown”); } map.putIfAbsent(“contextName”, name); config.start(); this.config = config; updateLoggers(); if (prev != null) { prev.removeListener(this); prev.stop(); } firePropertyChangeEvent(new PropertyChangeEvent(this, PROPERTY_CONFIG, prev, config)); try { Server.reregisterMBeansAfterReconfigure(); } catch (final Throwable t) { // LOG4J2-716: Android has no java.lang.management LOGGER.error(“Could not reconfigure JMX”, t); } return prev; }这个方法最重要的步骤就是config.start,这才是真正做配置解析的public void start() { LOGGER.debug(“Starting configuration {}”, this); this.setStarting(); pluginManager.collectPlugins(pluginPackages); final PluginManager levelPlugins = new PluginManager(Level.CATEGORY); levelPlugins.collectPlugins(pluginPackages); final Map<String, PluginType<?>> plugins = levelPlugins.getPlugins(); if (plugins != null) { for (final PluginType<?> type : plugins.values()) { try { // Cause the class to be initialized if it isn’t already. Loader.initializeClass(type.getPluginClass().getName(), type.getPluginClass().getClassLoader()); } catch (final Exception e) { LOGGER.error(“Unable to initialize {} due to {}”, type.getPluginClass().getName(), e.getClass() .getSimpleName(), e); } } } setup(); setupAdvertisement(); doConfigure(); final Set<LoggerConfig> alreadyStarted = new HashSet<LoggerConfig>(); for (final LoggerConfig logger : loggers.values()) { logger.start(); alreadyStarted.add(logger); } for (final Appender appender : appenders.values()) { appender.start(); } if (!alreadyStarted.contains(root)) { // LOG4J2-392 root.start(); // LOG4J2-336 } super.start(); LOGGER.debug(“Started configuration {} OK.”, this); }这里面有如下步骤:获取日志等级的插件初始化初始化Advertiser配置先看一下初始化,也就是setup这个方法,setup是一个需要被复写的方法,我们以XMLConfiguration作为例子,@Override public void setup() { if (rootElement == null) { LOGGER.error(“No logging configuration”); return; } constructHierarchy(rootNode, rootElement); if (status.size() > 0) { for (final Status s : status) { LOGGER.error(“Error processing element {}: {}”, s.name, s.errorType); } return; } rootElement = null; }发现这里面有一个比较重要的方法constructHierarchy,跟进:private void constructHierarchy(final Node node, final Element element) { processAttributes(node, element); final StringBuilder buffer = new StringBuilder(); final NodeList list = element.getChildNodes(); final List<Node> children = node.getChildren(); for (int i = 0; i < list.getLength(); i++) { final org.w3c.dom.Node w3cNode = list.item(i); if (w3cNode instanceof Element) { final Element child = (Element) w3cNode; final String name = getType(child); final PluginType<?> type = pluginManager.getPluginType(name); final Node childNode = new Node(node, name, type); constructHierarchy(childNode, child); if (type == null) { final String value = childNode.getValue(); if (!childNode.hasChildren() && value != null) { node.getAttributes().put(name, value); } else { status.add(new Status(name, element, ErrorType.CLASS_NOT_FOUND)); } } else { children.add(childNode); } } else if (w3cNode instanceof Text) { final Text data = (Text) w3cNode; buffer.append(data.getData()); } } final String text = buffer.toString().trim(); if (text.length() > 0 || (!node.hasChildren() && !node.isRoot())) { node.setValue(text); } }发现这个就是一个树遍历的过程。诚然,配置文件是以xml的形式给出的,xml的结构就是一个树形结构。回到start方法,跟进doConfiguration:protected void doConfigure() { if (rootNode.hasChildren() && rootNode.getChildren().get(0).getName().equalsIgnoreCase(“Properties”)) { final Node first = rootNode.getChildren().get(0); createConfiguration(first, null); if (first.getObject() != null) { subst.setVariableResolver((StrLookup) first.getObject()); } } else { final Map<String, String> map = this.getComponent(CONTEXT_PROPERTIES); final StrLookup lookup = map == null ? null : new MapLookup(map); subst.setVariableResolver(new Interpolator(lookup, pluginPackages)); } boolean setLoggers = false; boolean setRoot = false; for (final Node child : rootNode.getChildren()) { if (child.getName().equalsIgnoreCase(“Properties”)) { if (tempLookup == subst.getVariableResolver()) { LOGGER.error(“Properties declaration must be the first element in the configuration”); } continue; } createConfiguration(child, null); if (child.getObject() == null) { continue; } if (child.getName().equalsIgnoreCase(“Appenders”)) { appenders = child.getObject(); } else if (child.isInstanceOf(Filter.class)) { addFilter(child.getObject(Filter.class)); } else if (child.getName().equalsIgnoreCase(“Loggers”)) { final Loggers l = child.getObject(); loggers = l.getMap(); setLoggers = true; if (l.getRoot() != null) { root = l.getRoot(); setRoot = true; } } else if (child.getName().equalsIgnoreCase(“CustomLevels”)) { customLevels = child.getObject(CustomLevels.class).getCustomLevels(); } else if (child.isInstanceOf(CustomLevelConfig.class)) { final List<CustomLevelConfig> copy = new ArrayList<CustomLevelConfig>(customLevels); copy.add(child.getObject(CustomLevelConfig.class)); customLevels = copy; } else { LOGGER.error(“Unknown object "{}" of type {} is ignored.”, child.getName(), child.getObject().getClass().getName()); } } if (!setLoggers) { LOGGER.warn(“No Loggers were configured, using default. Is the Loggers element missing?”); setToDefault(); return; } else if (!setRoot) { LOGGER.warn(“No Root logger was configured, creating default ERROR-level Root logger with Console appender”); setToDefault(); // return; // LOG4J2-219: creating default root=ok, but don’t exclude configured Loggers } for (final Map.Entry<String, LoggerConfig> entry : loggers.entrySet()) { final LoggerConfig l = entry.getValue(); for (final AppenderRef ref : l.getAppenderRefs()) { final Appender app = appenders.get(ref.getRef()); if (app != null) { l.addAppender(app, ref.getLevel(), ref.getFilter()); } else { LOGGER.error(“Unable to locate appender {} for logger {}”, ref.getRef(), l.getName()); } } } setParents(); }发现就是对刚刚获取的configuration进行解析,然后塞进正确的地方。回到start方法,可以看到昨晚配置之后就是开启logger和appender了。异步AsyncAppenderlog4j2突出于其他日志的优势,异步日志实现。我们先从日志打印看进去。找到Logger,随便找一个log日志的方法。public void debug(final Marker marker, final Message msg) { logIfEnabled(FQCN, Level.DEBUG, marker, msg, msg != null ? msg.getThrowable() : null); }一路跟进@PerformanceSensitive // NOTE: This is a hot method. Current implementation compiles to 29 bytes of byte code. // This is within the 35 byte MaxInlineSize threshold. Modify with care! private void logMessageTrackRecursion(final String fqcn, final Level level, final Marker marker, final Message msg, final Throwable throwable) { try { incrementRecursionDepth(); // LOG4J2-1518, LOG4J2-2031 tryLogMessage(fqcn, level, marker, msg, throwable); } finally { decrementRecursionDepth(); } }可以看出这个在打日志之前做了调用次数的记录。跟进tryLogMessage,@PerformanceSensitive // NOTE: This is a hot method. Current implementation compiles to 26 bytes of byte code. // This is within the 35 byte MaxInlineSize threshold. Modify with care! private void tryLogMessage(final String fqcn, final Level level, final Marker marker, final Message msg, final Throwable throwable) { try { logMessage(fqcn, level, marker, msg, throwable); } catch (final Exception e) { // LOG4J2-1990 Log4j2 suppresses all exceptions that occur once application called the logger handleLogMessageException(e, fqcn, msg); } }继续跟进:@Override public void logMessage(final String fqcn, final Level level, final Marker marker, final Message message, final Throwable t) { final Message msg = message == null ? new SimpleMessage(Strings.EMPTY) : message; final ReliabilityStrategy strategy = privateConfig.loggerConfig.getReliabilityStrategy(); strategy.log(this, getName(), fqcn, marker, level, msg, t); }这里可以看到在实际打日志的时候,会从config中获取打日志的策略,跟踪ReliabilityStrategy的创建,发现默认的实现类为DefaultReliabilityStrategy,跟进看实际打日志的方法@Override public void log(final Supplier<LoggerConfig> reconfigured, final String loggerName, final String fqcn, final Marker marker, final Level level, final Message data, final Throwable t) { loggerConfig.log(loggerName, fqcn, marker, level, data, t); }这里实际打日志的方法居然是交给一个config去实现的。。。感觉有点奇怪。。跟进看看@PerformanceSensitive(“allocation”) public void log(final String loggerName, final String fqcn, final Marker marker, final Level level, final Message data, final Throwable t) { List<Property> props = null; if (!propertiesRequireLookup) { props = properties; } else { if (properties != null) { props = new ArrayList<>(properties.size()); final LogEvent event = Log4jLogEvent.newBuilder() .setMessage(data) .setMarker(marker) .setLevel(level) .setLoggerName(loggerName) .setLoggerFqcn(fqcn) .setThrown(t) .build(); for (int i = 0; i < properties.size(); i++) { final Property prop = properties.get(i); final String value = prop.isValueNeedsLookup() // since LOG4J2-1575 ? config.getStrSubstitutor().replace(event, prop.getValue()) // : prop.getValue(); props.add(Property.createProperty(prop.getName(), value)); } } } final LogEvent logEvent = logEventFactory.createEvent(loggerName, marker, fqcn, level, data, props, t); try { log(logEvent, LoggerConfigPredicate.ALL); } finally { // LOG4J2-1583 prevent scrambled logs when logging calls are nested (logging in toString()) ReusableLogEventFactory.release(logEvent); } }可以清楚的看到try之前是在创建LogEvent,try里面做的才是真正的log(好tm累),一路跟进。private void processLogEvent(final LogEvent event, LoggerConfigPredicate predicate) { event.setIncludeLocation(isIncludeLocation()); if (predicate.allow(this)) { callAppenders(event); } logParent(event, predicate); }接下来就是callAppender了,我们直接开始看AsyncAppender的append方法:/** * Actual writing occurs here. * * @param logEvent The LogEvent. / @Override public void append(final LogEvent logEvent) { if (!isStarted()) { throw new IllegalStateException(“AsyncAppender " + getName() + " is not active”); } final Log4jLogEvent memento = Log4jLogEvent.createMemento(logEvent, includeLocation); InternalAsyncUtil.makeMessageImmutable(logEvent.getMessage()); if (!transfer(memento)) { if (blocking) { if (AbstractLogger.getRecursionDepth() > 1) { // LOG4J2-1518, LOG4J2-2031 // If queue is full AND we are in a recursive call, call appender directly to prevent deadlock AsyncQueueFullMessageUtil.logWarningToStatusLogger(); logMessageInCurrentThread(logEvent); } else { // delegate to the event router (which may discard, enqueue and block, or log in current thread) final EventRoute route = asyncQueueFullPolicy.getRoute(thread.getId(), memento.getLevel()); route.logMessage(this, memento); } } else { error(“Appender " + getName() + " is unable to write primary appenders. queue is full”); logToErrorAppenderIfNecessary(false, memento); } } }这里主要的步骤就是:生成logEvent将logEvent放入BlockingQueue,就是transfer方法如果BlockingQueue满了则启用相应的策略同样的,这里也有一个线程用来做异步消费的事情private class AsyncThread extends Log4jThread { private volatile boolean shutdown = false; private final List<AppenderControl> appenders; private final BlockingQueue<LogEvent> queue; public AsyncThread(final List<AppenderControl> appenders, final BlockingQueue<LogEvent> queue) { super(“AsyncAppender-” + THREAD_SEQUENCE.getAndIncrement()); this.appenders = appenders; this.queue = queue; setDaemon(true); } @Override public void run() { while (!shutdown) { LogEvent event; try { event = queue.take(); if (event == SHUTDOWN_LOG_EVENT) { shutdown = true; continue; } } catch (final InterruptedException ex) { break; // LOG4J2-830 } event.setEndOfBatch(queue.isEmpty()); final boolean success = callAppenders(event); if (!success && errorAppender != null) { try { errorAppender.callAppender(event); } catch (final Exception ex) { // Silently accept the error. } } } // Process any remaining items in the queue. LOGGER.trace(“AsyncAppender.AsyncThread shutting down. Processing remaining {} queue events.”, queue.size()); int count = 0; int ignored = 0; while (!queue.isEmpty()) { try { final LogEvent event = queue.take(); if (event instanceof Log4jLogEvent) { final Log4jLogEvent logEvent = (Log4jLogEvent) event; logEvent.setEndOfBatch(queue.isEmpty()); callAppenders(logEvent); count++; } else { ignored++; LOGGER.trace(“Ignoring event of class {}”, event.getClass().getName()); } } catch (final InterruptedException ex) { // May have been interrupted to shut down. // Here we ignore interrupts and try to process all remaining events. } } LOGGER.trace(“AsyncAppender.AsyncThread stopped. Queue has {} events remaining. " + “Processed {} and ignored {} events since shutdown started.”, queue.size(), count, ignored); } /* * Calls {@link AppenderControl#callAppender(LogEvent) callAppender} on all registered {@code AppenderControl} * objects, and returns {@code true} if at least one appender call was successful, {@code false} otherwise. Any * exceptions are silently ignored. * * @param event the event to forward to the registered appenders * @return {@code true} if at least one appender call succeeded, {@code false} otherwise / boolean callAppenders(final LogEvent event) { boolean success = false; for (final AppenderControl control : appenders) { try { control.callAppender(event); success = true; } catch (final Exception ex) { // If no appender is successful the error appender will get it. } } return success; } public void shutdown() { shutdown = true; if (queue.isEmpty()) { queue.offer(SHUTDOWN_LOG_EVENT); } if (getState() == State.TIMED_WAITING || getState() == State.WAITING) { this.interrupt(); // LOG4J2-1422: if underlying appender is stuck in wait/sleep/join/park call } } }直接看run方法:阻塞获取logEvent将logEvent分发出去如果线程要退出了,将blockingQueue里面的event消费完在退出。AsyncLogger直接从AsyncLogger的logMessage看进去:public void logMessage(final String fqcn, final Level level, final Marker marker, final Message message, final Throwable thrown) { if (loggerDisruptor.isUseThreadLocals()) { logWithThreadLocalTranslator(fqcn, level, marker, message, thrown); } else { // LOG4J2-1172: avoid storing non-JDK classes in ThreadLocals to avoid memory leaks in web apps logWithVarargTranslator(fqcn, level, marker, message, thrown); } }跟进logWithThreadLocalTranslator,private void logWithThreadLocalTranslator(final String fqcn, final Level level, final Marker marker, final Message message, final Throwable thrown) { // Implementation note: this method is tuned for performance. MODIFY WITH CARE! final RingBufferLogEventTranslator translator = getCachedTranslator(); initTranslator(translator, fqcn, level, marker, message, thrown); initTranslatorThreadValues(translator); publish(translator); }这里的逻辑很简单,就是将日志相关的信息转换成RingBufferLogEvent(RingBuffer是Disruptor的无所队列),然后将其发布到RingBuffer中。发布到RingBuffer中,那肯定也有消费逻辑。这时候有两种方式可以找到这个消费的逻辑。找disruptor被使用的地方,然后查看,但是这样做会很容易迷惑按照Log4j2的尿性,这种Logger都有对应的start方法,我们可以从start方法入手寻找在start方法中,我们找到了一段代码:final RingBufferLogEventHandler[] handlers = {new RingBufferLogEventHandler()}; disruptor.handleEventsWith(handlers);直接看看这个RingBufferLogEventHandler的实现:public class RingBufferLogEventHandler implements SequenceReportingEventHandler<RingBufferLogEvent>, LifecycleAware { private static final int NOTIFY_PROGRESS_THRESHOLD = 50; private Sequence sequenceCallback; private int counter; private long threadId = -1; @Override public void setSequenceCallback(final Sequence sequenceCallback) { this.sequenceCallback = sequenceCallback; } @Override public void onEvent(final RingBufferLogEvent event, final long sequence, final boolean endOfBatch) throws Exception { event.execute(endOfBatch); event.clear(); // notify the BatchEventProcessor that the sequence has progressed. // Without this callback the sequence would not be progressed // until the batch has completely finished. if (++counter > NOTIFY_PROGRESS_THRESHOLD) { sequenceCallback.set(sequence); counter = 0; } } /* * Returns the thread ID of the background consumer thread, or {@code -1} if the background thread has not started * yet. * @return the thread ID of the background consumer thread, or {@code -1} / public long getThreadId() { return threadId; } @Override public void onStart() { threadId = Thread.currentThread().getId(); } @Override public void onShutdown() { }}顺着接口找上去,发现一个接口:/* * Callback interface to be implemented for processing events as they become available in the {@link RingBuffer} * * @param <T> event implementation storing the data for sharing during exchange or parallel coordination of an event. * @see BatchEventProcessor#setExceptionHandler(ExceptionHandler) if you want to handle exceptions propagated out of the handler. /public interface EventHandler<T>{ /* * Called when a publisher has published an event to the {@link RingBuffer} * * @param event published to the {@link RingBuffer} * @param sequence of the event being processed * @param endOfBatch flag to indicate if this is the last event in a batch from the {@link RingBuffer} * @throws Exception if the EventHandler would like the exception handled further up the chain. / void onEvent(T event, long sequence, boolean endOfBatch) throws Exception;}通过注释可以发现,这个onEvent就是处理逻辑,回到RingBufferLogEventHandler的onEvent方法,发现里面有一个execute方法,跟进:public void execute(final boolean endOfBatch) { this.endOfBatch = endOfBatch; asyncLogger.actualAsyncLog(this); }这个方法就是实际打日志了,AsyncLogger看起来还是比较简单的,只是使用了一个Disruptor。插件化之前在很多代码里面都可以看到final PluginManager manager = new PluginManager(CATEGORY);manager.collectPlugins(pluginPackages);其实整个log4j2为了获得更好的扩展性,将自己的很多组件都做成了插件,然后在配置的时候去加载plugin。跟进collectPlugins。 public void collectPlugins(final List<String> packages) { final String categoryLowerCase = category.toLowerCase(); final Map<String, PluginType<?>> newPlugins = new LinkedHashMap<>(); // First, iterate the Log4j2Plugin.dat files found in the main CLASSPATH Map<String, List<PluginType<?>>> builtInPlugins = PluginRegistry.getInstance().loadFromMainClassLoader(); if (builtInPlugins.isEmpty()) { // If we didn’t find any plugins above, someone must have messed with the log4j-core.jar. // Search the standard package in the hopes we can find our core plugins. builtInPlugins = PluginRegistry.getInstance().loadFromPackage(LOG4J_PACKAGES); } mergeByName(newPlugins, builtInPlugins.get(categoryLowerCase)); // Next, iterate any Log4j2Plugin.dat files from OSGi Bundles for (final Map<String, List<PluginType<?>>> pluginsByCategory : PluginRegistry.getInstance().getPluginsByCategoryByBundleId().values()) { mergeByName(newPlugins, pluginsByCategory.get(categoryLowerCase)); } // Next iterate any packages passed to the static addPackage method. for (final String pkg : PACKAGES) { mergeByName(newPlugins, PluginRegistry.getInstance().loadFromPackage(pkg).get(categoryLowerCase)); } // Finally iterate any packages provided in the configuration (note these can be changed at runtime). if (packages != null) { for (final String pkg : packages) { mergeByName(newPlugins, PluginRegistry.getInstance().loadFromPackage(pkg).get(categoryLowerCase)); } } LOGGER.debug(“PluginManager ‘{}’ found {} plugins”, category, newPlugins.size()); plugins = newPlugins; }处理逻辑如下:从Log4j2Plugin.dat中加载所有的内置的plugin然后将OSGi Bundles中的Log4j2Plugin.dat中的plugin加载进来再加载传入的package路径中的plugin最后加载配置中的plugin逻辑还是比较简单的,但是我在看源码的时候发现了一个很有意思的东西,就是在加载log4j2 core插件的时候,也就是PluginRegistry.getInstance().loadFromMainClassLoader()这个方法,跟进到decodeCacheFiles:private Map<String, List<PluginType<?>>> decodeCacheFiles(final ClassLoader loader) { final long startTime = System.nanoTime(); final PluginCache cache = new PluginCache(); try { final Enumeration<URL> resources = loader.getResources(PluginProcessor.PLUGIN_CACHE_FILE); if (resources == null) { LOGGER.info(“Plugin preloads not available from class loader {}”, loader); } else { cache.loadCacheFiles(resources); } } catch (final IOException ioe) { LOGGER.warn(“Unable to preload plugins”, ioe); } final Map<String, List<PluginType<?>>> newPluginsByCategory = new HashMap<>(); int pluginCount = 0; for (final Map.Entry<String, Map<String, PluginEntry>> outer : cache.getAllCategories().entrySet()) { final String categoryLowerCase = outer.getKey(); final List<PluginType<?>> types = new ArrayList<>(outer.getValue().size()); newPluginsByCategory.put(categoryLowerCase, types); for (final Map.Entry<String, PluginEntry> inner : outer.getValue().entrySet()) { final PluginEntry entry = inner.getValue(); final String className = entry.getClassName(); try { final Class<?> clazz = loader.loadClass(className); final PluginType<?> type = new PluginType<>(entry, clazz, entry.getName()); types.add(type); ++pluginCount; } catch (final ClassNotFoundException e) { LOGGER.info(“Plugin [{}] could not be loaded due to missing classes.”, className, e); } catch (final LinkageError e) { LOGGER.info(“Plugin [{}] could not be loaded due to linkage error.”, className, e); } } } final long endTime = System.nanoTime(); final DecimalFormat numFormat = new DecimalFormat("#0.000000”); final double seconds = (endTime - startTime) * 1e-9; LOGGER.debug(“Took {} seconds to load {} plugins from {}”, numFormat.format(seconds), pluginCount, loader); return newPluginsByCategory; }可以发现加载时候是从一个文件(PLUGIN_CACHE_FILE)获取所有要获取的plugin。看到这里的时候我有一个疑惑就是,为什么不用反射的方式直接去扫描,而是要从文件中加载进来,而且文件是写死的,很不容易扩展啊。然后我找了一下PLUGIN_CACHE_FILE这个静态变量的用处,发现了PluginProcessor这个类,这里用到了注解处理器。/* * Annotation processor for pre-scanning Log4j 2 plugins. /@SupportedAnnotationTypes(“org.apache.logging.log4j.core.config.plugins.")public class PluginProcessor extends AbstractProcessor { // TODO: this could be made more abstract to allow for compile-time and run-time plugin processing /** * The location of the plugin cache data file. This file is written to by this processor, and read from by * {@link org.apache.logging.log4j.core.config.plugins.util.PluginManager}. */ public static final String PLUGIN_CACHE_FILE = “META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat”; private final PluginCache pluginCache = new PluginCache(); @Override public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) { System.out.println(“Processing annotations”); try { final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Plugin.class); if (elements.isEmpty()) { System.out.println(“No elements to process”); return false; } collectPlugins(elements); writeCacheFile(elements.toArray(new Element[elements.size()])); System.out.println(“Annotations processed”); return true; } catch (final IOException e) { e.printStackTrace(); error(e.getMessage()); return false; } catch (final Exception ex) { ex.printStackTrace(); error(ex.getMessage()); return false; } }}(不太重要的方法省略)我们可以看到在process方法中,PluginProcessor会先收集所有的Plugin,然后在写入文件。这样做的好处就是可以省去反射时候的开销。然后我又看了一下Plugin这个注解,发现它的RetentionPolicy是RUNTIME,一般来说PluginProcessor是搭配RetentionPolicy.SOURCE,CLASS使用的,而且既然你把自己的Plugin扫描之后写在文件中了,RetentionPolicy就没有必要是RUNTIME了吧,这个是一个很奇怪的地方。小结总算是把Log4j2的代码看完了,发现它的设计理念很值得借鉴,为了灵活性,所有的东西都设计成插件式。互联网技术日益发展,各种中间件层出不穷,而作为工程师的我们更需要做的是去思考代码与代码之间的关系,毫无疑问的是,解耦是最具有美感的关系。本文作者:netflix阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 10, 2019 · 19 min · jiezi

Node.js 应用故障排查手册 —— 综合性 GC 问题和优化

楔子本章前面两节生产案例分别侧重于单一的 CPU 高和单一的内存问题,我们也给大家详细展示了问题的定位排查过程,那么实际上还有一类相对更复杂的场景——它本质上是 V8 引擎的 GC 引发的问题。简单的给大家介绍下什么是 GC,GC 实际上是语言引擎实现的一种自动垃圾回收机制,它会在设定的条件触发时(比如堆内存达到一定值)时查看当前堆上哪些对象已经不再使用,并且将这些没有再使用到的对象所占据的空间释放出来。许多的现代高级语言都实现了这一机制,来减轻程序员的心智负担。本书首发在 Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区会同步更新。GC 带来的问题虽然上面介绍中现代语言的 GC 机制解放了程序员间接提升了开发效率,但是万事万物都存在利弊,底层的引擎引入 GC 后程序员无需再关注对象何时释放的问题,那么相对来说程序员也就没办法实现对自己编写的程序的精准控制,它带来两大问题:代码编写问题引发的内存泄漏程序执行的性能降低内存泄漏问题我们已经在上一节的生产案例中体验了一下,那么后者是怎么回事呢?其实理解起来也很简单:原本一个程序全部的工作都是执行业务逻辑,但是存在了 GC 机制后,程序总会在一定的条件下耗费时间在扫描堆空间找出不再使用的对象上,这样就变相降低了程序执行业务逻辑的时间,从而造成了性能的下降,而且降低的性能和耗费在 GC 上的时间,换言之即 GC 的次数 * 每次 GC 耗费的时间成正比。问题现象与原始分析现在大家应该对 GC 有了一个比较整体的了解,这里我们可以看下 GC 引发的问题在生产中的表现是什么样的。在这个案例中,表象首先是 Node.js 性能平台 上监控到进程的 CPU 达到 100%,但是此时服务器负载其实并不大,QPS 只有 100 上下,我们按照前面提到的处理 CPU 问题的步骤抓取 CPU Profile 进行分析可以看到:这次的问题显然是 Garbage Collector 耗费的 CPU 太多了,也就是 GC 的问题。实际上绝大部分的 GC 机制引发的问题往往表象都是反映在 Node.js 进程的 CPU 上,而本质上这类问题可以认为是引擎的 GC 引起的问题,也可以理解为内存问题,我们看下这类问题的产生流程:堆内存不断达到触发 GC 动作的预设条件进程不断触发 GC 操作进程 CPU 飙高而且 GC 问题不像之前的 ejs 模板渲染引发的问题,就算我们在 CPU Profile 中可以看到这部分的耗费,但是想要优化解决这个问题基本是无从下手的,幸运的是 Node.js 提供了(其实是 V8 引擎提供的)一系列的启动 Flag 能够输出进程触发 GC 动作时的相关日志以供开发者进行分析:–trace_gc:一行日志简要描述每次 GC 时的时间、类型、堆大小变化和产生原因–trace_gc_verbose: 结合 –trace_gc 一起开启的话会展示每次 GC 后每个 V8 堆空间的详细状况–trace_gc_nvp: 每一次 GC 的一些详细键值对信息,包含 GC 类型,暂停时间,内存变化等信息加粗的 Flag 意味着我们需要在启动应用前加上才能在运行时生效,这部分的日志实际上是一个文本格式,可惜的是 Chrome devtools 原生并不支持 GC 日志的解析和结果展示,因此需要大家获取到以后进行对应的按行解析处理,当然我们也可以使用社区提供 v8-gc-log-parser 这个模块直接进行解析处理,对这一块有兴趣的同学可以看 @joyeeCheung 在 JS Interactive 的分享: Are Your V8 Garbage Collection Logs Speaking To You?,这里不详细展开。更好的 GC 日志展示虽然 Chrome devtools 并不能直接帮助我们解析展示 GC 日志的结果,但是 Node.js 性能平台 其实给大家提供了更方便的动态获取线上运行进程的 GC 状态信息以及对应的结果展示,换言之,开发者无需在运行你的 Node.js 应用前开启上面提到的那些 Flag 而仍然可以在想要获取到 GC 信息时通过控制台拿到 3 分钟内的 GC 数据。对应在这个案例中,我们可以进入平台的应用实例详情页面,找到 GC 耗费特别大的进程,然后点击 GC Trace 抓取 GC 数据:这里默认会抓取 3 分钟的对应进程的 GC 日志信息,等到结束后生成的文件会显示在 文件 页面:此时点击 转储 即可上传到云端以供在线分析展示了,如下图所示:最后点击这里的 分析 按钮,即可看到 AliNode 定制后的 GC 信息分析结果的展现:结果展示中,可以比较方便的看到问题进程的 GC 具体次数,GC 类型以及每次 GC 的耗费时间等信息,方便我们进一步的分析定位。比如这次问题的 GC Trace 结果分析图中,我们可以看到红圈起来的几个重要信息:GC 总暂停时间高达 47.8s,大头是 Scavenge3min 的 GC 追踪日志里面,总共进行了 988 次的 Scavenge 回收每次 Scavenge 耗时均值在 50 ~ 60ms 之间从这些解困中我们可以看到此次 GC 案例的问题点集中在 Scavenge 回收阶段,即新生代的内存回收。那么通过翻阅 V8 的 Scavenge 回收逻辑可以知道,这个阶段触发回收的条件是:Semi space allocation failed。这样就可以推测,我们的应用在压测期间应该是在新生代频繁生成了大量的小对象,导致默认的 Semi space 总是处于很快被填满从而触发 Flip 的状态,这才会出现在 GC 追踪期间这么多的 Scavenge 回收和对应的 CPU 耗费,这样这个问题就变为如何去优化新生代的 GC 来提升应用性能。优化新生代 GC通过平台提供的 GC 数据抓取和结果分析,我们知道可以去尝试优化新生代的 GC 来达到提升应用性能的目的,而新生代的空间触发 GC 的条件又是其空间被占满,那么新生代的空间大小由 Flag –max-semi-space-size 控制,默认为 16MB,因此我们自然可以想到要可以通过调整默认的 Semi space 的值来进行优化。这里需要注意的是,控制新生代空间的 Flag 在不同的 Node.js 版本下并不是一样的,大家具体可以查看当前的版本来进行确认使用。在这个案例中,显然是默认的 16M 相对当前的应用来说显得比较小,导致 Scavenge 过于频繁,我们首先尝试通过启动时增加 –max-semi-space-size=64 这个 Flag 来将默认的新生代使用到的空间大小从 16M 的值增大为 64M,并且在流量比较大而且进程 CPU 很高时抓取 CPU Profile 观察效果:调整后可以看到 Garbage collector 阶段 CPU 耗费占比下降到 7% 左右,再抓取 GC Trace 并观察其展示结果确认是不是 Scavenge 阶段的耗费下降了:显然,Semi space 调大为 64M 后,Scavenge 次数从近 1000 次降低到 294 次,但是这种状况下每次的 Scavenge 回收耗时没有明显增加,还是在 50 ~ 60ms 之间波动,因此 3 分钟的 GC 追踪总的停顿时间从 48s 下降到 12s,相对应的,业务的 QPS 提升了约 10% 左右。那么如果我们通过 –max-semi-space-size 这个 Flag 来继续调大新生代使用到的空间,是不是可以进一步优化这个应用的性能呢?此时尝试 –max-semi-space-size=128 来从 64M 调大到 128M,在进程 CPU 很高时继续抓取 CPU Profile 来查看效果:此时 Garbage collector 耗费下降相比上面的设置为 64M 并不是很明显,GC 耗费下降占比不到 1%,同样我们来抓取并观察下 GC Trace 的结果来查看具体的原因:很明显,造成相比设置为 64M 时 GC 占比提升不大的原因是:虽然此时进一步调大了 Semi space 至 128M,并且 Scavenge 回收的次数确实从 294 次下降到 145 次,但是每次算法回收耗时近乎翻倍了,因此总收益并不明显。按照这个思路,我们再使用 –max-semi-space-size=256 来将新生代使用的空间进一步增大到 256M 再来进行最后一次的观察:这里和调整为 128M 时是类似的情况: 3 分钟内 Scavenge 次数从 294 次下降到 72 次,但是相对的每次算法回收耗时波动到了 150ms 左右,因此整体性能并没有显著提升。借助于性能平台的 GC 数据抓取和结果展示,通过以上的几次尝试改进 Semi space 的值后,我们可以看到从默认的 16M 设置到 64M 时,Node 应用的整体 GC 性能是有显著提升的,并且反映到压测 QPS 上大约提升了 10%;但是进一步将 Semi space 增大到 128M 和 256M 时,收益确并不明显,而且 Semi space 本身也是作用于新生代对象快速内存分配,本身不宜设置的过大,因此这次优化最终选取对此项目 最优的运行时 Semi space 的值为 64M。结尾在本生产案例中,我们首先可以看到,项目使用的三方库其实也并不总是在所有场景下都不会有 Bug 的(实际上这是不可能的),因此在遇到三方库的问题时我们要敢于去从源码的层面来对问题进行深入的分析。最后实际上在生产环境下通过 GC 方面的运行时调优来提升我们的项目性能是一种大家不那么常用的方式,这也有很大一部分原因是应用运行时 GC 状态本身不直接暴露给开发者。通过上面这个客户案例,我们可以看到借助于 Node.js 性能平台,实时感知 Node 应用 GC 状态以及进行对应的优化,使得不改一行代码提升项目性能变成了一件非常容易的事情。本文作者:奕钧阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 9, 2019 · 2 min · jiezi

设置日志超过30天自动清除

一、查找删除在项目目录下创建shell文件# touch clear-log.sh // 创建clear-log.sh文件# chmod +x clear-log.sh //给clear-log文件加可执行权限在新创建的文件中加入脚本#!/bin/shfind /Users/lvmoumou/marry/log-storage/logs_store/ -mtime +30 -name “*.log” -exec rm -rf {} ;其中+30代表查询三十天前的文件,-exec rm -rf {} ;是固定写法,表示强制删除包括目录。下一步只需要在当前目录下执行./clear-log.sh即可实现删除30天前的日志啦。二、设置脚本自动执行有了第一步只完成了我们想要的一半内容,删除可以了,如何设置自动删除呢。在终端输入#crontab -e编辑自动执行任务。执行后输入i,编辑任务命令,命令如下50 10 * * * /Users/lvmoumou/marry/log-storage/clear-log.sh >/dev/null 2>&1 50,10代表在10点50分时执行/Users/lvmoumou/marry/log-storage/clear-log.sh这个文件,后面的>/dev/null 2>&1的前半部分>/dev/null:表示标准输出重定向到空设备文件,也就是不输出任何信息到终端,不显示任何信息。后半部分2>&1:接着,标准错误输出重定向到标准输出,因为之前标准输出已经重定向到了空设备文件,所以标准错误输出也重定向到空设备文件。这条命令的意思就是在后台执行这个程序,并将错误输出2重定向到标准输出1,然后将标准输出1全部放到/dev/null文件,也就是清空.所以可以看出" >/dev/null 2>&1 “常用来避免shell命令或者程序等运行中有内容输出。

April 7, 2019 · 1 min · jiezi

通过DataWorks数据集成归档日志服务数据至MaxCompute进行离线分析

通过DataWorks归档日志服务数据至MaxCompute官方指导文档:https://help.aliyun.com/document_detail/68322.html但是会遇到大家在分区上或者DataWorks调度参数配置问题,具体拿到真实的case模拟如下:创建数据源:步骤1、进入数据集成,点击作业数据源,进入Tab页面。步骤2、 点击右上角新增数据源,选择消息队列 loghub。步骤3、编辑LogHub数据源中的必填项,包括数据源名称、LogHubEndpoint、Project、AK信息等,并点击 测试连通性。创建目标表:步骤1、在左侧tab也中找到临时查询,并右键>新建ODPS SQL节点。步骤2、编写建表DDL。步骤3、点击执行 按钮进行创建目标表,分别为ods_client_operation_log、ods_vedio_server_log、ods_web_tracking_log。步骤4、直到日志打印成本,表示三条DDL语句执行完毕。步骤5、可以通过desc 查看创建的表。其他两张表也可以通过desc 进行查询。确认数据表的存在情况。创建数据同步任务数据源端以及在DataWorks中的数据源连通性都已经配置好,接下来就可以通过数据同步任务进行采集数据到MaxCompute上。操作步骤步骤1、点击新建业务流程 并 确认提交,名称为 直播日志采集。步骤2、在业务流程开发面板中依次创建如下依赖并命名。依次配置数据同步任务节点配置:web_tracking_log_syn、client_operation_log_syn、vedio_server_log_syn。步骤3、双击web_tracking_log_syn 进入节点配置,配置项包括数据源(数据来源和数据去向)、字段映射(源头表和目标表)、通道控制。根据采集的时间窗口自定义参数为:步骤4、可以点击高级运行进行测试。可以分别手工收入自定义参数值进行测试。步骤5、使用SQL脚本确认是否数据已经写进来。如下图所示:日志服务的日志正式的被采集入库,接下来就可以进行数据加工。比如可以通过上述来统计热门房间、地域分布和卡顿率,如下所示:具体SQL逻辑不在这里展开,可以根据具体业务需求来统计分析。依赖关系配置如上图所示。本文作者:祎休阅读原文本文为云栖社区原创内容,未经允许不得转载。

April 2, 2019 · 1 min · jiezi

信用算力基于 RocketMQ 实现金融级数据服务的实践

导读:微服务架构已成为了互联网的热门话题之一,而这也是互联网技术发展的必然阶段。然而,微服务概念的提出者 Martin Fowler 却强调:分布式调用的第一原则就是不要分布式。纵观微服务实施过程中的弊端,可以推断出作者的意图,就是希望系统架构者能够谨慎地对待分布式调用,这是分布式系统自身存在的缺陷所致。但无论是 RPC 框架,还是 REST 框架,都因为驻留在不同进程空间的分布式组件,而引入了额外的复杂度。因而可能对系统的效率、可靠性、可预测性等诸多方面带来负面影响。信用算力自2016年开始实施微服务改造,通过消息队列(Message Queue),后文简称MQ,来规避微服务存在的缺陷,实现金融级数据服务。以下是一些使用场景和心得。为什么需要 MQ一、案例介绍先来看一个当前的真实业务场景。对于通过信息流获客的企业而言,当用户注册时,因业务需求会调用用户服务,然后执行一系列操作,注册 -> 初始化账户信息 -> 邀友奖励发放 -> 发放优惠券 -> … -> 信息流数据上报。用户服务的开发人员压力非常大,因为需要调用非常多的服务,业务耦合严重。如果当时账户服务正在执行发版操作,那么初始化账户动作会失败。然而平台经过不断的迭代更新,后续又新增了一个签到业务,新注册用户默认签到一次。这就需要修改用户服务,增加调用签到服务的接口。每当遇到此种情况,开发用户服务的同学就非常不爽了,为什么总是我?新增签到业务和用户服务又有什么关系?为解决此类重度依赖的问题,我们在架构层面引入了 MQ,用来规避微服务之间重度耦合调用的弊端。新架构如下图:用户完成注册动作后,只需要往 MQ 发送一个用户注册的通知消息,下游业务如需要依赖注册相关的数据,订阅注册消息的 topic 即可,从而实现了业务的解耦。看完上述真实的案例后,大家可能产生疑惑,到底什么是 MQ,使用 MQ 又有什么好处?适合使用 MQ 的场景和不适合使用 MQ 的场景有哪些不同?二、什么是 MQ?简单来说,MQ(MessageQueue)是一种跨进程的通信机制,用于上下游传递消息。适合使用 MQ 的场景有:1、上游不关心下游执行结果,例如上述案例中用户注册后,我们并不关心账户是否初始化,是否上报了信息流等;2、异步返回执行时间长:例如上述案例中,当邀友奖励发放,需要经历很多风控规则,执行时间比较长,但是用户并不关注奖励何时发放。不适合使用MQ场景调用方实时关注执行结果,例如用户发起注册动作后,需要立刻知道,注册结果是成功还是失败,这种需要实时知道最终执行结果的场景,就不适合使用MQ。三、使用MQ的好处:1、解耦2、可靠投递3、广播4、最终一致性5、流量削峰6、消息投递保证7、异步通信(支持同步)8、提高系统吞吐、健壮性MQ 的技术选型目前业内比较主流的 MQ 包括 RocketMQ、ActiveMQ、RabbitMQ、Kafka等,关于性能、存储、社区活跃度等各方面的技术对比已经很多,本文不再重复。但我们发现通过简单的选型对比,很难抉择到底选择哪款MQ产品。因为金融行业对于数据一致性以及服务可用性的要求非常高,所以任何关于技术的选项都显得尤为重要。经调研,如微众银行、民生银行、平安银行等国内知名的互联网银行和直销银行代表,都在使用 RocketMQ,且 RocketMQ 出生在阿里系,经受过各种生产压力的考验,非常稳定。并且,目前此项技术已经捐增给 Apache 社区,社区活跃度非常高。另外 RocketMQ 开发语言是Java,开发同学遇到解决不了的问题点,或者不清楚的概念,可以直接 Debug 源码。经过多方面的比较,我们选择 RocketMQ 作为规避微服务弊端的利器。MQ 在微服务下的使用场景MQ 是一种跨进程的通信机制,用于上下游传递消息,目前信用算力将 RocketMQ 应用于解耦、流量削峰、分布式事务的处理等几个场景。一、解耦通常解耦的做法是生产者发送消息到 MQ,下游订阅 MQ 的特定 topic,当下游接收到消息后开始处理业务逻辑。那么,消息发送方到底应该是由谁来承担?是服务提供者在处理完RPC请求后,根据业务需求开始发送消息吗?但此刻开发人员就会抱怨为什么总是我?为什么处理完业务后需要发送 MQ?为此,在解耦的过程中通过订阅数据库的 BinLog 日志,开发了一套 BinLog 日志解析模块,专门解析日志,然后生成 JSON 字符串后发送消息到 MQ,下游订阅 MQ 即可。流程如下:目前所有需要依赖下游服务的业务线,其数据变动都采用此方案。方案优缺点:优点:1、服务之间依赖完全解耦,任何基于注册行为的业务变更,都无需依赖上游,只需订阅MQ即可;2、系统的稳定性和吞吐量增加了,用户注册的响应时间缩短了;缺点:1、引入MQ后系统复杂性增加,维护成本增加;2、从注册开始到全部数据初始化结束的整体时间增加了;二、流量削峰每逢遇到会员日的时候,平台会发送大量的会员福利活动通知,以短信、站内信、PUSH 消息的方式通知注册用户。所有的消息会在很短的时间全部推送到消息中心,同时正常的业务通知任然有大量业务消息推送到消息中心。为保障平台的稳定性和可靠性,在消息中心前置了多种 topic,如短信、推送、站内提醒。消息中心接收到消息后会全部写入不同 topic 的 MQ,多个消费者来消费并把信息推送给终端用户。三、分布式事务用户在平台上支付他订购某种业务的时候,需要涉及到支付服务、账户服务、优惠券服务、积分服务,在单体模式下这种业务非常容易实现,通过事务即可完成,伪代码如下:然而,在微服务的情况下,原本通过简单事务处理的却变得非常复杂,若引入两阶段提交(2PC)或者补偿事务(TCC)方案,则系统的复杂程度会增加。信用算力的做法是通过本地事务 + MQ 消息的方式来解决, 虽然 RocketMQ 也支持事务消息,但是其他主流 MQ 并没有此项功能,所以综合考虑采用如下方案:消息上游:需要额外建一个tc_message表,并记录消息发送状态。消息表和业务数据在同一个数据库里面,而且要在一个事务里提交。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送;消息上游:开启定时任务扫描tc_message表,如果超过设置的时间内状态没有变更,会再次发送消息到MQ,如重试次数达到上限则发起告警操作;消息下游:需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理完成了,需要发起业务回调通知业务方;方案优缺点:优点:1、用最小的代价实现分布式事物,以达到数据最终一致性;2、方案非常灵活,任何环节都可以人为控制;缺点:1、复杂性增加了,业务操作的时候需要写入 tc_message 表以及发送 MQ,同时还需要考虑状态超时未变更的补发机制以及告警处理机制;2、用户看到的数据,存在有短暂不一致的情况;心得体会使用 RocketMQ 3年多了,总体来说运行的非常稳定,基本上没有发生过生产事故,下面说说这几年使用下来的心得体会:1、一个应用尽可能用一个 Topic,消息子类型用 tags 来标识。Topic 名称和 Tags 名称可以自行设置。Producer,Consumer都需要规范,要做到见名知意。发送消息时候必须携带 Tags,消费方在订阅消息时,才可以利用 Tags 在 Broker 做消息过滤。2、每条消息在业务层面有唯一标识码,方便在系统出现异常的情况,可以通过业务维度查询。举个栗子,当用户在平台注册成功后,会以 Topic 和 UserID 作为唯一标识码(topic_user_10011),服务器会为每个消息创建索引,该消息会持久化入库,以防将来定位消息丢失等问题。下游收到消息后会以 Topic+Key 方式来记录消费行为,包括消息日期、当前机器IP地址、处理结果等;也可以通过 Topic+Key 的方式来查询这条消息内容,包括消息被谁消费,以及这条 MQ 在每个环节的处理状态。3、消息发送成功或者失败,都需要记录 log 日志,且必须打印 sendresult、MsgID、唯一标识码。4、由于上游会做消息重试机制,所以下游消息必须要做幂等处理。5、需要封装 MQ 的 API 在封装后,API 需屏蔽底层 MQ 的特性,开发人员无需关注到底是用的哪个 MQ 来支持本地分布式事物、MQ 消息自动入库、自动打印日志,减少开发人员操作成本。总的来说,MQ 是一个互联网架构中常见的解耦利器,在这3年中,信用算力在微服务中一直使用 MQ 来为金融客户提供高质量的数据服务。虽然 MQ 不是唯一方案,但是从目前阶段来看,的确是一种非常不错的解决方案。本文作者:潘志伟(信用算力技术总监,QCon 演讲嘉宾,十多年 Java 从业经验,精通微服务架构,精通大数据。拥有亿级用户平台架构经验,万级并发的API网关经验。)本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 29, 2019 · 1 min · jiezi

Kubernetes Ingress 日志分析与监控的最佳实践

Ingress 主要提供 HTTP 层(7 层)路由功能,是目前 K8s 中 HTTP/HTTPS 服务的主流暴露方式。为简化广大用户对于 Ingress 日志分析与监控的门槛,阿里云容器服务和日志服务将 Ingress 日志打通,只需要应用一个 yaml 资源即可完成日志采集、分析、可视化等一整套 Ingress 日志方案的部署。前言目前 Kubernetes(K8s)已经真正地占领了容器编排市场,是默认的云无关计算抽象,越来越多的企业开始将服务构建在K8s集群上。在 K8s 中,组件通过 Service 对外暴露服务,常见的包括 NodePort、LoadBalancer、Ingress 等。其中 Ingress 主要提供 HTTP 层(7 层)路由功能,相比 TCP(4 层)的负载均衡具备非常多的优势(路由规则更加灵活、支持金丝雀、蓝绿、A/B Test 发布模式、SSL 支持、日志、监控、支持自定义扩展等),是目前 K8s 中 HTTP/HTTPS 服务的主流暴露方式。Ingress 简介K8s 中 Ingress 只是一种 API 资源的声明,具体的实现需要安装对应的 Ingress Controller,由 Ingress Controller 接管 Ingress 定义,将流量转发到对应的 Service。目前 Ingress Controller 的实现有非常多种(具体可以参考 Ingress Controller官方文档),比较流行的有 Nginx、Traefik、Istio、Kong 等,在国内接受度最高的是 Nginx Ingress Controller。日志与监控日志和监控是所有 Ingress Controller 都会提供的基础功能,日志一般包括访问日志(Access Log)、控制日志(Controller Log)和错误日志(Error Log),监控主要从日志以及 Controller 中提取部分 Metric 信息。这些数据中访问日志的量级最大、信息最多、价值也最高,一般7层的访问日志包括:URL、源 IP、UserAgent、状态码、入流量、出流量、响应时间等,对于 Ingress Controller 这种转发型的日志,还包括转发的 Service 名、Service 响应时间等额外信息。从这些信息中,我们能够分析出非常多的信息,例如:网站访问的 PV、UV;访问的地域分布、设备端分布;网站访问的错误比例;后端服务的响应延迟;不同 URL 访问分布。我们的开发、运维、运营、安全等人员可以基于这些信息完成各自的需求,例如:新老版本发布前后的数据指标对比;网站质量监控、集群状态监控;恶意攻击检测、反作弊;网站访问量统计、广告转化率统计。然而手动搭建、运维一整套的 Ingress 日志分析与监控系统非常复杂,系统所需要的模块有:部署日志采集 Agent 并配置采集、解析规则;由于 K8s 集群中,访问量相对较大,因此需要搭建一个缓冲队列,例如 Redis、Kafka 等;部署实时数据分析引擎,例如 Elastic Search、clickhouse 等;部署可视化组件并搭建报表,例如 grafana、kibana 等;部署告警模块并配置告警规则,例如 ElastAlert、alertmanager 等。阿里云日志服务Ingress解决方案为简化广大用户对于 Ingress 日志分析与监控的门槛,阿里云容器服务和日志服务将 Ingress 日志打通(官方文档https://help.aliyun.com/document_detail/86532.html)),只需要应用一个 yaml 资源即可完成日志采集、分析、可视化等一整套 Ingress 日志方案的部署。Ingress 可视化分析日志服务默认为 Ingress 创建 5 个报表,分别是:Ingress 概览、Ingress 访问中心、Ingress 监控中心、Ingress 蓝绿发布监控中心、Ingress 异常检测中心。不同角色的人员可根据需求使用不同的报表,同时每个报表均提供筛选框用于筛选特定的 Service、URL、状态码等。所有的报表均基于日志服务提供的基础可视化组件实现,可根据公司实际场景进行定制化调整。Ingress 概览Ingress 概览报表主要展示当前 Ingress 的整体状态,主要包括以下几类信息:整体架构状态(1 天),包括:PV、UV、流量、响应延迟、移动端占比、错误比例等;网站实时状态(1 分钟),包括:PV、UV、成功率、5XX 比例、平均延迟、P95/P99 延迟等;用户请求类信息(1 天),包括:1天/7天访问PV对比、访问地域分布、TOP访问省份/城市、移动端占比、Android/IOS 占比等;TOPURL 统计(1 小时),包括:访问 TOP10、延迟 TOP10、5XX 错误 TOP10、404 错误 TOP10。Ingress 访问中心Ingress 访问中心主要侧重于用于访问请求相关的统计信息,一般用于运营分析,包括:当日 UV/PV、UV/PV 分布、UV/PV 趋势、TOP 访问省份/城市、TOP 访问浏览器、TOP 访问IP、移动端占比、Android/IOS 占比等。Ingress 监控中心Ingress 监控中心主要侧重于网站实时监控数据,一般用于实时监控与告警,包括:请求成功率、错误比例、5XX 比例、请求未转发比例、平均延迟、P95/P99/P9999 延迟、状态码分布、Ingress 压力分布、Service 访问 TOP10、Service 错误 TOP10、Service 延迟 TOP10、Service 流量 TOP10 等。Ingress 蓝绿发布监控中心Ingress 蓝绿发布监控中心主要用于版本发布时的实时监控与对比(版本前后对比以及蓝绿版本当前对比),以便在服务发布时快速检测异常并进行回滚。在该报表中需要选择进行对比的蓝绿版本(ServiceA 和 ServiceB),报表将根据选择动态显示蓝绿版本相关指标,包括:PV、5XX 比例、成功率、平均延迟、P95/P99/P9999 延迟、流量等。Ingress 异常检测中心Ingress 异常检测中心基于日志服务提供的机器学习算法,通过多种时序分析算法从 Ingress 的指标中自动检测异常点,提高问题发现的效率。实时监控与告警Ingress 作为 K8s 网站请求的主要入口,实时监控与告警是必不可少的 Ops 手段之一。在日志服务上,基于上述的报表,只需 3 个简单的步骤即可完成告警的创建。下述示例为 Ingress 配置 5XX 比例的告警,告警每 5 分钟执行一次,当 5XX 比例超过 1% 时触发。除了通用的告警功能外,日志服务还额外支持:多维度数据关联,即通过多组 SQL 结果交叉判断进行告警,增加告警准确度;除支持短信、语音、通知中心、email 外,还支持钉钉机器人通知、自定义 WebHook 扩展;告警的记录也以日志的形式记录,可以实现对告警失败进行告警的双保险。订阅报告日志服务除支持通过告警方式通知外,还支持报表订阅功能,可使用该功能将报表定期渲染成图片并通过邮件、钉钉群等方式发送。例如每天早上 10 点向运营群中发送昨日网站访问情况、每周发送报告到邮件组中存档、新版本发布时每 5 分钟发送一次监控报表…自定义分析如果容器服务 Kubernetes 版提供的默认报表无法满足你的分析需求,可以直接使用日志服务 SQL、仪表盘等功能进行自定义的分析和可视化。尝鲜为了让大家可以体验 Kubernetes 审计日志功能,我们特别开通了体验中心,大家可以通过 https://promotion.aliyun.com/ntms/act/logdoclist.html 进入,该页面提供了非常多和 Kubernetes相关的报表。参考文档[1]https://www.aliyun.com/product/sls[2]https://www.aliyun.com/product/kubernetes[3]https://help.aliyun.com/document_detail/86532.html[4]https://help.aliyun.com/document_detail/48162.html[5]https://help.aliyun.com/document_detail/107758.html[6]https://kubernetes.io/docs/concepts/services-networking/ingress/[7]https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/本文作者:jessie筱姜阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 27, 2019 · 1 min · jiezi

What’s New in TiDB 3.0.0 Beta.1

作者:申砾今年 1 月份,我们发布了 TiDB 3.0.0 Beta 版本,DevCon 上也对这个版本做了介绍,经过两个月的努力,今天推出了下一个 Beta 版本 3.0.0 Beta.1。让我们看一下这个版本相比于之前有什么改进。新增特性解读Skyline Pruning查询计划正确性和稳定性对于关系型数据库来说至关重要,3.0.0 Beta.1 对这部分进行了优化,引入一个叫 Skyline Pruning 的框架,通过一些启发式规则来更快更准确地找到最好的查询计划。详细信息可以参考 这篇设计文档。日志格式统一日志是排查程序问题的重要工具,统一且结构化的日志格式不但有利于用户理解日志内容,也有助于通过工具对日志进行定量分析。3.0.0 Beta.1 版本中对 tidb/pd/tikv 这三个组件的日志格式进行了统一,详细格式参见 这篇文档。慢查询相关改进慢查询日志是常用于排查性能问题, 在 3.0.0 Beta.1 之前慢查询日志跟其他日志混合存储在同个日志文件,并且格式为自定义的格式,不支持使用 SQL 语句或工具对其进行分析,严重影响排查问题的效率。从3.0.0 Beta.1 版本开始 TiDB 将查询日志文件输出到单独的日志文件中(默认日志文件名为 tidb-slow.log),用户可以系统变量或配置文件进行修改,同时兼容 MySQL 慢查询日志格式,支持使用 MySQL 生态分析工具(如 pt-query-digest)对慢查询日志进行分析。除了慢查询日志之外,还增加一个虚拟表 INFORMATION_SCHEMA.SLOW_QUERY,可以对慢查询日志进行展示和过滤。关于如何处理慢查询,我们后续还会专门写一篇文档进行介绍。如果你有一些好用的慢查询处理工具,也欢迎和我们进行交流。Window FunctionMySQL 所支持的 Window Function TiDB 3.0.0 Beta.1 版本已经全都支持,这为 TiDB 向 MySQL 8 兼容迈出了一大步。想体验功能的可以下载版本尝鲜,但是不建议在生产中使用,这项功能还需要大量的测试,欢迎大家测试并反馈问题。热点调度策略可配置化热点调度是保持集群负载均衡的重要手段,但是一些场景下默认的热点调度显得不那么智能,甚至会对集群负载造成影响,所以 3.0.0 Beta.1 中增加了对负载均衡策略的人工干预方法,可以临时调整调度策略。优化 Coprocessor 计算执行框架目前已经完成 TableScan 算子,单 TableScan 即扫表性能提升 5% ~ 30%,接下来会对 IndexScan、Filter、Aggregation 等算子以及表达式计算框架进行优化。TiDB Lightning 性能优化Lightning 是将大量数据导入 TiDB 的最佳方式,在特定表结构,单表数量,集群已有数量等条件下 1TB 数据导入性能提升 1 倍,时间从 6 小时降低到 3 小时以内,性能优化的脚步不会停,我们期望进一步提升性能,降低时间,期望能优化到 2 小时以内。易用性相关的特性使用 /debug/zip HTTP 接口, 可以方便地一键获取当前 TiDB 实例的信息,便于诊断问题。新增通过 SQL 语句方式管理 pump/drainer 状态,简化 pump/drainer 状态管理,当前仅支持查看状态。支持通过配置文件管理发送 binlog 策略, 丰富 binlog 管理方式。更多的改进可以参见 Release Notes,除了这些已经完成的特性之外,还有一些正在做的事情,比如 RBAC、Plan Management 都在密集开发中,希望在下一个 Beta 版本或者 RC 版本中能与大家见面。开源社区在这个版本的开发过程中,社区依然给我们很有力的支持,比如潘迪同学一直在负责 View 的完善和测试,美团的同学在推进 Plan Management,一些社区同学参与了 TiDB 性能改进 活动。在这里对各位贡献者表示由衷的感谢。接下来我们会开展更多的专项开发活动以及一系列面向社区的培训课程,希望能对大家了解如何做分布式数据库有帮助。One More ThingTiDB DevCon 2019 上对外展示的全新分析类产品 TiFlash 已经完成 Alpha 版本的开发,目前已经在进行内部测试,昨天试用了一下之后,我想说“真香”。 ...

March 27, 2019 · 1 min · jiezi

关于Paxos 幽灵复现问题的看法

由于郁白之前写的关于Multi-Paxos 的文章流传非常广, 具体地址: http://oceanbase.org.cn/?p=111 原文提出了一个叫"幽灵复现" 的问题, 认为这个是一个很诡异的问题, 后续和很多人交流关于一致性协议的时候, 也经常会提起这个问题, 但是其实这个问题我认为就是常见的"第三态"问题加了一层包装而已.幽灵复现问题来自郁白的博客:使用Paxos协议处理日志的备份与恢复,可以保证确认形成多数派的日志不丢失,但是无法避免一种被称为“幽灵复现”的现象,如下图所示: LeaderABC第一轮A1-101-51-5第二轮B宕机1-6,201-6,20第三轮A1-201-201-20第一轮中A被选为Leader,写下了1-10号日志,其中1-5号日志形成了多数派,并且已给客户端应答,而对于6-10号日志,客户端超时未能得到应答。第二轮,A宕机,B被选为Leader,由于B和C的最大的logID都是5,因此B不会去重确认6-10号日志,而是从6开始写新的日志,此时如果客户端来查询的话,是查询不到6-10号日志内容的,此后第二轮又写入了6-20号日志,但是只有6号和20号日志在多数派上持久化成功。第三轮,A又被选为Leader,从多数派中可以得到最大logID为20,因此要将7-20号日志执行重确认,其中就包括了A上的7-10号日志,之后客户端再来查询的话,会发现上次查询不到的7-10号日志又像幽灵一样重新出现了。对于将Paxos协议应用在数据库日志同步场景的情况,幽灵复现问题是不可接受,一个简单的例子就是转账场景,用户转账时如果返回结果超时,那么往往会查询一下转账是否成功,来决定是否重试一下。如果第一次查询转账结果时,发现未生效而重试,而转账事务日志作为幽灵复现日志重新出现的话,就造成了用户重复转账。为了处理“幽灵复现”问题,我们在每条日志的内容中保存一个generateID,leader在生成这条日志时以当前的leader ProposalID作为generateID。按logID顺序回放日志时,因为leader在开始服务之前一定会写一条StartWorking日志,所以如果出现generateID相对前一条日志变小的情况,说明这是一条“幽灵复现”日志(它的generateID会小于StartWorking日志),要忽略掉这条日志。第三态问题第三态问题也是我们之前经常讲的问题, 其实在网络系统里面, 对于一个请求都有三种返回结果成功失败超时未知前面两种状态由于服务端都有明确的返回结果, 所以非常好处理, 但是如果是第三种状态的返回, 由于是超时状态, 所以服务端可能对于这个命令是请求是执行成功, 也有可能是执行失败的, 所以如果这个请求是一个写入操作, 那么下一次的读取请求可能读到这个结果, 也可能读到的结果是空的就像在 raft phd 那个论文里面说的, 这个问题其实是和 raft/multi-paxos 协议无关的内容, 只要在分布式系统里面都会存在这个问题, 所以大部分的解决方法是两个对于每一个请求都加上一个唯一的序列号的标识, 然后server的状态机会记录之前已经执行过序列号. 当一个请求超时的时候, 默认的client 的逻辑会重试这个逻辑, 在收到重试的逻辑以后, 由于server 的状态机记录了之前已经执行过的序列号信息, 因此不会再次执行这条指令, 而是直接返回给客户端由于上述方法需要在server 端维护序列号的信息, 这个序列号是随着请求的多少递增的, 大小可想而知(当然也可以做一些只维护最近的多少条序列号个数的优化). 常见的工程实现是让client 的操作是幂等的, 直接重试即可, 比如floyd 里面的具体实现那么对应于raft 中的第三态问题是, 当最后log Index 为4 的请求超时的时候, 状态机中出现的两种场景都是可能的所以下一次读取的时候有可能读到log Index 4 的内容, 也有可能读不到, 所以如果在发生了超时请求以后, 默认client 需要进行重试直到这个操作成功以后, 接下来才可以保证读到的写入结果. 这也是工程实现里面常见的做法对应于幽灵问题, 其实是由于6-10 的操作产生了超时操作, 由于产生了超时操作以后, client 并没有对这些操作进行确认, 而是接下来去读取这个结果, 那么读取不到这个里面的内容, 由于后续的写入和切主操作有重新能够读取到这个6-10 的内容了, 造成了幽灵复现, 导致这个问题的原因还是因为没有进行对超时操作的重确认.回到幽灵复现问题那么Raft 有没有可能出现这个幽灵复现问题呢?其实在早期Raft 没有引入新的Leader 需要写入一个包含自己的空的Entry 的时候也一样会出现这个问题Log Index 4,5 客户端超时未给用户返回, 存在以下日志场景然后 (a) 节点宕机, 这个时候client 是查询不到 Log entry 4, 5 里面的内容在(b)或(c) 成为Leader 期间, 没有写入任何内容, 然后(a) 又恢复, 并且又重新选主, 那么就存在一下日志, 这个时候client 再查询就查询到Log entry 4,5 里面的内容了那么Raft 里面加入了新Leader 必须写入一条当前Term 的Log Entry 就可以解决这个问题, 其实和之前郁白提到的写入一个StartWorking 日志是一样的做法, 由于(b), (c) 有一个Term 3的日志, 就算(a) 节点恢复过来, 也无法成了Leader, 那么后续的读也就不会读到Log Entry 4, 5 里面的内容那么这个问题的本质是什么呢?其实这个问题的本质是对于一致性协议在recovery 的不同做法产生的. 关于一致性协议在不同阶段的做法可以看这个文章 http://baotiao.github.io/2018/01/02/consensus-recovery/也就是说对于一个在多副本里面未达成一致的Log entry, 在Recovery 需要如何处理这一部分未达成一致的log entry.对于这一部分log entry 其实可以是提交, 也可以是不提交, 因为会产生这样的log entry, 一定是之前对于这个client 的请求超时返回了.常见的Multi-Paxos 在对这一部分日志进行重确认的时候, 默认是将这部分的内容提交的, 也就是通过重确认的过程默认去提交这些内容而Raft 的实现是默认对这部分的内容是不提交的, 也就是增加了一个当前Term 的空的Entry, 来把之前leader 多余的log 默认不提交了, 幽灵复现里面其实也是通过增加一个空的当前Leader 的Proposal ID 来把之前的Log Entry 默认不提交所以这个问题只是对于返回超时, 未达成一致的Log entry 的不同的处理方法造成的.在默认去提交这些日志的场景, 在写入超时以后读取不到内容, 但是通过recovery 以后又能够读取到这个内容, 就产生了幽灵复现的问题但是其实之所以会出现幽灵复现的问题是因为在有了一个超时的第三态的请求以后, 在没有处理好这个第三态请求之前, 出现成功和失败都是有可能的.所以本质是在Multi-Paxos 实现中, 在recovery 阶段, 将未达成一致的Log entry 提交造成的幽灵复现的问题, 本质是没有处理好这个第三态的请求。一站式开发者服务,海量学习资源0元起!阿里热门开源项目、机器学习干货、开发者课程/工具、小微项目、移动研发等海量资源;更有开发者福利Kindle、技术图书幸运抽奖,100%中–》https://www.aliyun.com/acts/product-section-2019/developer?utm_content=g_1000047140本文作者:陈宗志阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 19, 2019 · 1 min · jiezi

蚂蚁金服开源 SOFAJRaft:生产级 Java Raft 算法库

什么是 SOFAJRaft?SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。 使用 SOFAJRaft 你可以专注于自己的业务领域,由 SOFAJRaft 负责处理所有与 Raft 相关的技术难题,并且 SOFAJRaft 非常易于使用,你可以通过几个示例在很短的时间内掌握它。SOFAJRaft 是从百度的 braft 移植而来,做了一些优化和改进,感谢百度 braft 团队开源了如此优秀的 C++ Raft 实现。基础知识:分布式共识算法 (Consensus Algorithm)如何理解分布式共识?多个参与者某一件事一致 :一件事,一个结论已达成一致的结论,不可推翻有哪些分布式共识算法?Paxos:被认为是分布式共识算法的根本,其他都是其变种,但是 Paxos 论文中只给出了单个提案的过程,并没有给出复制状态机中需要的 multi-paxos 的相关细节的描述,实现 Paxos 具有很高的工程复杂度(如多点可写,允许日志空洞等)。Zab:被应用在 Zookeeper 中,业界使用广泛,但没有抽象成通用的 library。Raft:以容易理解著称,业界也涌现出很多 Raft 实现,比如大名鼎鼎的 etcd, braft, tikv 等。什么是 Raft?Raft 是一种更易于理解的分布式共识算法,核心协议本质上还是师承 Paxos 的精髓,不同的是依靠 Raft 模块化的拆分以及更加简化的设计,Raft 协议相对更容易实现。模块化的拆分主要体现在:Raft 把一致性协议划分为 Leader 选举、MemberShip 变更、日志复制、Snapshot 等几个几乎完全解耦的模块。更加简化的设计则体现在:Raft 不允许类似 Paxos 中的乱序提交、简化系统中的角色状态(只有 Leader、Follower、Candidate 三种角色)、限制仅 Leader 可写入、使用随机化的超时时间来设计 Leader Election 等等。特点:Strong Leader系统中必须存在且同一时刻只能有一个 Leader,只有 Leader 可以接受 Clients 发过来的请求;Leader 负责主动与所有 Followers 通信,负责将“提案”发送给所有 Followers,同时收集多数派的 Followers 应答;Leader 还需向所有 Followers 主动发送心跳维持领导地位(保持存在感)。一句话总结 Strong Leader: “你们不要 BB! 按我说的做,做完了向我汇报!"。另外,身为 Leader 必须保持一直 BB(heartbeat) 的状态,否则就会有别人跳出来想要 BB 。Raft 中的基本概念篇幅有限,这里只对 Raft 中的几个概念做一个简单介绍,详细请参考 Raft paper。Raft-node 的 3 种角色/状态Follower:完全被动,不能发送任何请求,只接受并响应来自 Leader 和 Candidate 的 Message,每个节点启动后的初始状态一定是 Follower;Leader:处理所有来自客户端的请求,以及复制 Log 到所有 Followers;Candidate:用来竞选一个新 Leader (Candidate 由 Follower 触发超时而来)。Message 的 3 种类型RequestVote RPC:由 Candidate 发出,用于发送投票请求;AppendEntries (Heartbeat) RPC:由 Leader 发出,用于 Leader 向 Followers 复制日志条目,也会用作 Heartbeat (日志条目为空即为 Heartbeat);InstallSnapshot RPC:由 Leader 发出,用于快照传输,虽然多数情况都是每个服务器独立创建快照,但是Leader 有时候必须发送快照给一些落后太多的 Follower,这通常发生在 Leader 已经丢弃了下一条要发给该Follower 的日志条目(Log Compaction 时清除掉了) 的情况下。任期逻辑时钟时间被划分为一个个任期 (term),term id 按时间轴单调递增;每一个任期的开始都是 Leader 选举,选举成功之后,Leader 在任期内管理整个集群,也就是 “选举 + 常规操作”;每个任期最多一个 Leader,可能没有 Leader (spilt-vote 导致)。本图出自《Raft: A Consensus Algorithm for Replicated Logs》什么是 SOFAJRaft?SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。 使用 SOFAJRaft 你可以专注于自己的业务领域,由 SOFAJRaft 负责处理所有与 Raft 相关的技术难题,并且 SOFAJRaft 非常易于使用,你可以通过几个示例在很短的时间内掌握它。SOFAJRaft 是从百度的 braft 移植而来,做了一些优化和改进,感谢百度 braft 团队开源了如此优秀的 C++ Raft 实现。SOFAJRaft 整体功能&性能优化功能支持1.Leader election:Leader 选举,这个不多说,上面已介绍过 Raft 中的 Leader 机制。2.Log replication and recovery:日志复制和日志恢复。Log replication 就是要保证已经被 commit 的数据一定不会丢失,即一定要成功复制到多数派。Log recovery 包含两个方面:Current term 日志恢复:主要针对一些 Follower 节点重启加入集群或者是新增 Follower 节点后如何追日志;Prev term 日志恢复:主要针对 Leader 切换前后的日志一致性。3.Snapshot and log compaction:定时生成 snapshot,实现 log compaction 加速启动和恢复,以及 InstallSnapshot 给 Followers 拷贝数据,如下图:本图出自《In Search of an Understandable Consensus Algorithm》4.Membership change:用于集群线上配置变更,比如增加节点、删除节点、替换节点等。5.Transfer leader:主动变更 leader,用于重启维护,leader 负载平衡等。6.Symmetric network partition tolerance:对称网络分区容忍性。如上图 S1 为当前 leader,网络分区造成 S2 不断增加本地 term,为了避免网络恢复后 S2 发起选举导致正在良心 工作的 leader step-down,从而导致整个集群重新发起选举,SOFAJRaft 中增加了 pre-vote 来避免这个问题的发生。SOFAJRaft 中在 request-vote 之前会先进行 pre-vote(currentTerm + 1, lastLogIndex, lastLogTerm),多数派成功后才会转换状态为 candidate 发起真正的 request-vote,所以分区后的节点,pre-vote 不会成功,也就不会导致集群一段时间内无法正常提供服务。7.Asymmetric network partition tolerance:非对称网络分区容忍性。如上图 S1 为当前 leader,S2 不断超时触发选主,S3 提升 term 打断当前 lease,从而拒绝 leader 的更新。在 SOFAJRaft 中增加了一个 tick 的检查,每个 follower 维护一个时间戳记录下收到 leader 上数据更新的时间(也包括心跳),只有超过 election timeout 之后才允许接受 request-vote 请求。8.Fault tolerance:容错性,少数派故障不影响系统整体可用性,包括但不限于:机器掉电强杀应用慢节点(GC, OOM 等)网络故障其他各种奇葩原因导致 raft 节点无法正常工作9.Workaround when quorate peers are dead:多数派故障时,整个 grop 已不具备可用性,安全的做法是等待多数节点恢复,只有这样才能保证数据安全;但是如果业务更加追求系统可用性,可以放弃数据一致性的话,SOFAJRaft 提供了手动触发 reset_peers 的指令以迅速重建整个集群,恢复集群可用。10.Metrics:SOFAJRaft 内置了基于 Metrics 类库的性能指标统计,具有丰富的性能统计指标,利用这些指标数据可以帮助用户更容易找出系统性能瓶颈。11.Jepsen:除了几百个单元测试以及部分 chaos 测试之外, SOFAJRaft 还使用 jepsen 这个分布式验证和故障注入测试框架模拟了很多种情况,都已验证通过:随机分区,一大一小两个网络分区随机增加和移除节点随机停止和启动节点随机 kill -9 和启动节点随机划分为两组,互通一个中间节点,模拟分区情况随机划分为不同的 majority 分组性能优化除了功能上的完整性,SOFAJRaft 还做了很多性能方面的优化,这里有一份 KV 场景(get/put)的 Benchmark 数据, 在小数据包,读写比例为 9:1,保证线性一致读的场景下,三副本最高可以达到 40w+ 的 ops。这里挑重点介绍几个优化点:Batch: 我们知道互联网两大优化法宝便是 Cache 和 Batch,SOFAJRaft 在 Batch 上花了较大心思,整个链路几乎都是 Batch 的,依靠 disruptor 的 MPSC 模型批量消费,对整体性能有着极大的提升,包括但不限于:批量提交 task批量网络发送本地 IO batch 写入要保证日志不丢,一般每条 log entry 都要进行 fsync 同步刷盘,比较耗时,SOFAJRaft 中做了合并写入的优化。批量应用到状态机需要说明的是,虽然 SOFAJRaft 中大量使用了 Batch 技巧,但对单个请求的延时并无任何影响,SOFAJRaft 中不会对请求做延时的攒批处理。Replication pipeline:流水线复制,通常 Leader 跟 Followers 节点的 Log 同步是串行 Batch 的方式,每个 Batch 发送之后需要等待 Batch 同步完成之后才能继续发送下一批(ping-pong),这样会导致较长的延迟。SOFAJRaft 中通过 Leader 跟 Followers 节点之间的 pipeline 复制来改进,非常有效降低了数据同步的延迟, 提高吞吐。经我们测试,开启 pipeline 可以将吞吐提升 30% 以上,详细数据请参照 Benchmark。Append log in parallel:在 SOFAJRaft 中 Leader 持久化 log entries 和向 Followers 发送 log entries 是并行的。Fully concurrent replication:Leader 向所有 Follwers 发送 Log 也是完全相互独立和并发的。Asynchronous:SOFAJRaft 中整个链路几乎没有任何阻塞,完全异步的,是一个完全的 callback 编程模型。ReadIndex:优化 Raft read 走 Raft log 的性能问题,每次 read,仅记录 commitIndex,然后发送所有 peers heartbeat 来确认 Leader 身份,如果 Leader 身份确认成功,等到 appliedIndex >= commitIndex,就可以返回 Client read 了,基于 ReadIndex Follower 也可以很方便的提供线性一致读,不过 commitIndex 是需要从 Leader 那里获取,多了一轮 RPC;关于线性一致读文章后面会详细分析。Lease Read:SOFAJRaft 还支持通过租约 (lease) 保证 Leader 的身份,从而省去了 ReadIndex 每次 heartbeat 确认 Leader 身份,性能更好,但是通过时钟维护 lease 本身并不是绝对的安全(时钟漂移问题,所以 SOFAJRaft 中默认配置是 ReadIndex,因为通常情况下 ReadIndex 性能已足够好。SOFAJRaft 设计Node:Raft 分组中的一个节点,连接封装底层的所有服务,用户看到的主要服务接口,特别是 apply(task)用于向 raft group 组成的复制状态机集群提交新任务应用到业务状态机。存储:上图靠下的部分均为存储相关。Log 存储,记录 Raft 用户提交任务的日志,将日志从 Leader 复制到其他节点上。LogStorage 是存储实现,默认实现基于 RocksDB 存储,你也可以很容易扩展自己的日志存储实现;LogManager 负责对底层存储的调用,对调用做缓存、批量提交、必要的检查和优化。Metadata 存储,元信息存储,记录 Raft 实现的内部状态,比如当前 term、投票给哪个节点等信息。Snapshot 存储,用于存放用户的状态机 snapshot 及元信息,可选:SnapshotStorage 用于 snapshot 存储实现;SnapshotExecutor 用于 snapshot 实际存储、远程安装、复制的管理。状态机StateMachine:用户核心逻辑的实现,核心是 onApply(Iterator) 方法, 应用通过 Node#apply(task) 提交的日志到业务状态机;FSMCaller:封装对业务 StateMachine 的状态转换的调用以及日志的写入等,一个有限状态机的实现,做必要的检查、请求合并提交和并发处理等。复制Replicator:用于 Leader 向 Followers 复制日志,也就是 Raft 中的 AppendEntries 调用,包括心跳存活检查等;ReplicatorGroup:用于单个 Raft group 管理所有的 replicator,必要的权限检查和派发。RPC:RPC 模块用于节点之间的网络通讯RPC Server:内置于 Node 内的 RPC 服务器,接收其他节点或者客户端发过来的请求,转交给对应服务处理;RPC Client:用于向其他节点发起请求,例如投票、复制日志、心跳等。KV Store:KV Store 是各种 Raft 实现的一个典型应用场景,SOFAJRaft 中包含了一个嵌入式的分布式 KV 存储实现(SOFAJRaft-RheaKV)。SOFAJRaft Group单个节点的 SOFAJRaft-node 是没什么实际意义的,下面是三副本的 SOFAJRaft 架构图:SOFAJRaft Multi Group单个 Raft group 是无法解决大流量的读写瓶颈的,SOFAJRaft 自然也要支持 multi-raft-group。SOFAJRaft 实现细节解析之高效的线性一致读什么是线性一致读? 所谓线性一致读,一个简单的例子就是在 t1 的时刻我们写入了一个值,那么在 t1 之后,我们一定能读到这个值,不可能读到 t1 之前的旧值 (想想 Java 中的 volatile 关键字,说白了线性一致读就是在分布式系统中实现 Java volatile 语义)。如上图 Client A、B、C、D 均符合线性一致读,其中 D 看起来是 stale read,其实并不是,D 请求横跨了 3 个阶段,而读可能发生在任意时刻,所以读到 1 或 2 都行。重要:接下来的讨论均基于一个大前提,就是业务状态机的实现必须是满足线性一致性的,简单说就是也要具有 Java volatile 的语义。要实现线性一致读,首先我们简单直接一些,是否可以直接从当前 Leader 节点读?仔细一想,这显然行不通,因为你无法确定这一刻当前的 “Leader” 真的是 Leader,比如在网络分区的情况下,它可能已经被推翻王朝却不自知。最简单易懂的实现方式:同 “写” 请求一样,“读” 请求也走一遍 Raft 协议 (Raft Log)。本图出自《Raft: A Consensus Algorithm for Replicated Logs》这一定是可以的,但性能上显然不会太出色,走 Raft Log 不仅仅有日志落盘的开销,还有日志复制的网络开销,另外还有一堆的 Raft “读日志” 造成的磁盘占用开销,这在读比重很大的系统中通常是无法被接受的。ReadIndex Read这是 Raft 论文中提到的一种优化方案,具体来说:Leader 将自己当前 Log 的 commitIndex 记录到一个 Local 变量 ReadIndex 里面;接着向 Followers 发起一轮 heartbeat,如果半数以上节点返回了对应的 heartbeat response,那么 Leader 就能够确定现在自己仍然是 Leader (证明了自己是自己);Leader 等待自己的状态机执行,直到 applyIndex 超过了 ReadIndex,这样就能够安全的提供 Linearizable Read 了,也不必管读的时刻是否 Leader 已飘走 (思考:为什么等到 applyIndex 超过了 ReadIndex 就可以执行读请求?);Leader 执行 read 请求,将结果返回给 Client。通过ReadIndex,也可以很容易在 Followers 节点上提供线性一致读:Follower 节点向 Leader 请求最新的 ReadIndex;Leader 执行上面前 3 步的过程(确定自己真的是 Leader),并返回 ReadIndex 给 Follower;Follower 等待自己的 applyIndex 超过了 ReadIndex;Follower 执行 read 请求,将结果返回给 Client。(SOFAJRaft 中可配置是否从 Follower 读取,默认不打开)ReadIndex小结:相比较于走 Raft Log 的方式,ReadIndex 省去了磁盘的开销,能大幅度提升吞吐,结合 SOFAJRaft 的 batch + pipeline ack + 全异步机制,三副本的情况下 Leader 读的吞吐可以接近于 RPC 的吞吐上限;延迟取决于多数派中最慢的一个 heartbeat response,理论上对于降低延时的效果不会非常显著。Lease ReadLease Read 与 ReadIndex 类似,但更进一步,不仅省去了 Log,还省去了网络交互。它可以大幅提升读的吞吐也能显著降低延时。基本的思路是 Leader 取一个比 election timeout 小的租期(最好小一个数量级),在租约期内不会发生选举,这就确保了 Leader 不会变,所以可以跳过 ReadIndex 的第二步,也就降低了延时。可以看到 Lease Read 的正确性和时间是挂钩的,因此时间的实现至关重要,如果时钟漂移严重,这套机制就会有问题。实现方式:定时 heartbeat 获得多数派响应,确认 Leader 的有效性 (在 SOFAJRaft 中默认的 heartbeat 间隔是 election timeout 的十分之一);在租约有效时间内,可以认为当前 Leader 是 Raft Group 内的唯一有效 Leader,可忽略 ReadIndex 中的 heartbeat 确认步骤(2);Leader 等待自己的状态机执行,直到 applyIndex 超过了 ReadIndex,这样就能够安全的提供 Linearizable Read 了 。在 SOFAJRaft 中发起一次线性一致读请求的代码展示:// KV 存储实现线性一致读public void readFromQuorum(String key, AsyncContext asyncContext) { // 请求 ID 作为请求上下文传入 byte[] reqContext = new byte[4]; Bits.putInt(reqContext, 0, requestId.incrementAndGet()); // 调用 readIndex 方法, 等待回调执行 this.node.readIndex(reqContext, new ReadIndexClosure() { @Override public void run(Status status, long index, byte[] reqCtx) { if (status.isOk()) { try { // ReadIndexClosure 回调成功,可以从状态机读取最新数据返回 // 如果你的状态实现有版本概念,可以根据传入的日志 index 编号做读取 asyncContext.sendResponse(new ValueCommand(fsm.getValue(key))); } catch (KeyNotFoundException e) { asyncContext.sendResponse(GetCommandProcessor.createKeyNotFoundResponse()); } } else { // 特定情况下,比如发生选举,该读请求将失败 asyncContext.sendResponse(new BooleanCommand(false, status.getErrorMsg())); } } });}应用场景Leader 选举;分布式锁服务,比如 Zookeeper,在 SOFAJRaft 中的 RheaKV 模块提供了完整的分布式锁实现;高可靠的元信息管理,可直接基于 SOFAJRaft-RheaKV 存储;分布式存储系统,如分布式消息队列、分布式文件系统、分布式块系统等等。使用案例RheaKV:基于 SOFAJRaft 实现的嵌入式、分布式、高可用、强一致的 KV 存储类库。AntQ Streams QCoordinator:使用 SOFAJRaft 在 Coordinator 集群内做选举、使用 SOFAJRaft-RheaKV 做元信息存储等功能。Schema Registry:高可靠 schema 管理服务,类似 kafka schema registry,存储部分基于 SOFAJRaft-RheaKV。SOFA 服务注册中心元信息管理模块:IP 数据信息注册,要求写数据达到各个节点一致,并且在少数派节点挂掉时保证不影响数据正常存储。实践一、基于 SOFAJRaft 设计一个简单的 KV Store二、基于 SOFAJRaft 的 RheaKV 的设计功能名词PD全局的中心总控节点,负责整个集群的调度,不需要自管理的集群可不启用 PD (一个 PD 可管理多个集群,基于 clusterId 隔离)。Store集群中的一个物理存储节点,一个 Store 包含一个或多个 Region。Region最小的 KV 数据单元,每个 Region 都有一个左闭右开的区间 [startKey, endKey), 可根据请求流量/负载/数据量大小等指标自动分裂以及自动副本搬迁。特点嵌入式强一致性自驱动自诊断, 自优化, 自决策以上几点(尤其2、3) 基本都是依托于 SOFAJRaft 自身的功能来实现,详细介绍请参考 SOFAJRaft 文档 。致谢感谢 braft、etcd、tikv 贡献了优秀的 Raft 实现,SOFAJRaft 受益良多。招聘蚂蚁金服中间件团队持续在寻找对于基础中间件(如消息、数据中间件以及分布式计算等)以及下一代高性能面向实时分析的时序数据库等方向充满热情的小伙伴加入,有意者请联系 boyan@antfin.com。参考资料SOFAJRaft 源码SOFAJRaft 详细文档RaftRaft paperRaft: A Consensus Algorithm for Replicated LogsPaxos/Raft:分布式一致性算法原理剖析及其在实战中的应用braft 文档线性一致性和 RaftStrong consistency modelsetcd raft 设计与实现《一》MetricsjepsenBenchmark本文作者:s潘潘阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 15, 2019 · 5 min · jiezi

LINE案例研究:使用Fluentd从批处理到流日志处理

用几句话:公司:LINE公司地点:日本东京用例概述:Terabyte级:Fluentd用作主要数据流处理器,每天处理数TB的数据。灵活的数据处理:开发了许多自定义插件。Fluentd充满活力和开放的社区是其在LINE上取得成功的关键。将Fluentd的可扩展性提升到一个新的水平:他们的一位工程师在Fluentd之上构建了一个无模式SQL流处理引擎。目标:近实时地汇总和处理日志数据LINE公司以其同名的应用程序及其平台上的各种服务而闻名,面临着每天收集、存储和分析海量日志数据的挑战。当Satoshi Tagomori,他们的数据工程团队成员四年前加入公司时,他们有一个经典的Hadoop设置:他们使用Scribe日志收集器将所有东西收集到Hadoop中,并在Hadoop上运行批处理作业来处理日志。这个设置工作得很好,但Tagomori先生认为有一些改进的地方。Hadoop旨在以可扩展的方式运行批处理作业,但不太适合实时的流内处理:Tagomori先生想要将一些日志处理(特别是针对固定时间窗口的处理)放入到数据收集器中,以便在日志进入时进行分析。该系统可以近乎实时地为内部利益相关者提供有关日志数据的统计数据,从而使整个公司能够更快地做出决策。通过将日志处理卸载到数据收集器,可以更好地利用Hadoop集群的资源:通过将Hadoop上的日志数据的一些批处理转换为数据收集器中的流内处理,他们将能够释放Hadoop的资源并使用它用于其他批处理作业。Tagomori先生探索了各种选择,包括自己建造原型。最终,他找到了Fluentd并开始与社区合作。与社区共同发展当Fluentd于2011年10月首次推出时,它已经拥有了许多当前的主要优势:可插拔架构使其易于定制和扩展其行为、内存占用空间小、可靠的缓冲和负载平衡机制。然而,有一件事它没有,那就是性能。“Fluentd…不是很快。”Tagomori先生回忆道,他现在是Fluentd的核心维护者。“但由于它是一个开源项目,我开始与核心开发人员合作,帮助他们对Fluentd的性能进行基准测试。在接下来的一年左右,Fluentd的性能提高了10x-15x。”今天,Fluentd对于LINE来说非常快:他们每天使用Fluentd处理1.5TB(56亿条记录)的日志数据,在高峰时段每秒数据超过120,000条记录。此外,作为Fluentd的活跃用户,Tagomori先生已经贡献了34个插件,从HDFS输出插件到各种过滤器插件,现在在许多其他组织中使用。“社区是我最喜欢Fluentd的地方之一。”Tagomori先生说。“我确实考虑过其他开源数据采集器,但Fluentd拥有一个充满活力的社区,可以通过坚定的承诺保持Fluentd的简单、强大和快速。此外,它的插件系统经过精心设计,使贡献变得非常容易。”现在:在Fluentd上使用SQL进行无模式流处理在开发了数十个插件之后,Tagomori先生开始认为他可以将Fluentd演变成一个轻量级数据流处理器。2014年5月,他推出了无模式流处理引擎Norikra,允许用户针对数据流编写SQL查询。“Norikra可以在Fluentd上对数据流进行采样、过滤、聚合和连接。它的查询语言是SQL,所以没有特殊的DSL需要学习,你可以随时添加和删除SQL查询。Norikra已经替LINE解决了一些数据相关的挑战,允许我们直接查询数据流。”KubeCon + CloudNativeCon和Open Source Summit大会日期:会议日程通告日期:2019 年 4 月 10 日会议活动举办日期:2019 年 6 月 24 至 26 日KubeCon + CloudNativeCon和Open Source Summit赞助方案KubeCon + CloudNativeCon和Open Source Summit多元化奖学金现正接受申请KubeCon + CloudNativeCon和Open Source Summit即将首次合体落地中国KubeCon + CloudNativeCon和Open Source Summit购票窗口,立即购票!CNCF邀请你加入最终用户社区

March 12, 2019 · 1 min · jiezi

nginx日志自动每日切割脚本

1、脚本内容#!/bin/bash#日志文件存放目录logs_path="/var/log/nginx/e/"# pid文件pid_path="/run/nginx.pid"#重命名日志文件mv ${logs_path}access.log ${logs_path}access_$(date -d “yesterday” +"%Y%m%d").log#向nginx主进程发送信号以重新打开日志kill -USR1 cat ${pid_path}2、Crontab任务配置0 0 * sh /home/ubuntu/www/Pikachu/shell/cutAccessLogs.sh

March 7, 2019 · 1 min · jiezi

Yii2 设置 分类型分日期保存日志

直接上代码:’log’ => [ ’traceLevel’ => YII_DEBUG ? 3 : 0, ’targets’ => [ [ ‘class’ => ‘yii\log\FileTarget’, ’levels’ => [’error’], ’logFile’ => ‘@runtime/logs/’.date(‘Ym’).’/app.error.log’, ], [ ‘class’ => ‘yii\log\FileTarget’, ’levels’ => [‘warning’], ’logFile’ => ‘@runtime/logs/’.date(‘Ym’).’/app.warning.log’, ], ],],

March 4, 2019 · 1 min · jiezi

如何评估深度学习模型效果?阿里工程师这么做

小叽导读:复杂的深度模型中,如果效果不好,是因为网络设计的欠缺?还是数据天然缺陷?是训练代码的bug?还是Tensorflow自身的问题?基于此,阿里工程师推出了DeepInsight深度学习质量平台,致力于解决当前模型调试和问题定位等一系列问题。接下来,阿里巴巴高级技术专家、DeepInsight深度学习质量平台技术负责人:孙凯(花名:路宸),带我们一起探索。1. 背景机器学习训练过程的调试、可视化以及训练效果的评估一直是业界难题。在数据较少,模型较简单,如LR、GBDT、SVM,超参不多的情况下,模型的可调性和可解释性都有一定保障,那么我们用简单的训练,再观察召回/精度/AUC等指标就可以应对。而深度学习时代,模型的复杂性远远超乎想象,层层嵌套的网络结构,优化器和大量超参的选择,特征的连续化,一起构建了复杂的深度模型。如果效果不好,其原因是多样的,为了定位和解决这些问题,算法研发同学需要花费大量精力反复尝试,而且很可能得不到准确的答案。简单来说,网络模型近似于黑盒。2. DeepInsight通过研究,我们发现训练和评估过程中大量中间指标与模型效果能产生关系,通过系统的分析建模张量、梯度、权重和更新量,能够对算法调优、问题定位起到辅助决策作用。而且,通过改进AUC算法,分析ROC、PR、预估分布等更多评估指标,能够更全面地评估模型效果。通过2个多月的努力,我们推出了DeepInsight平台,致力于解决当前模型调试和问题定位等一系列问题。提交模型开始训练之后,用户可以通过DeepInsight平台,能一站式查看并分析训练过程,从训练中间指标到预测指标,再到性能数据,一应俱全。对于训练中明显的问题,平台也会高亮给予提示。未来,我们希望平台能更好地帮助用户发现和定位训练中的问题,并能给予适当提示(如更改某些子网络的最优化算法、更改学习率动量等),就如同GDB之于C++一样。2.1 目标沉淀并持久化训练数据。深度学习的数据非常宝贵,每次训练的网络拓扑、参数、训练中间过程、模型评估指标都会持久存储,方便后续人工分析和二次建模;沉淀对模型训练的认识,提供分析调优手段,辅助决策,同时规避各类已知问题;利用大数据分析建模,寻找中间过程指标的关系,更好地辅助决策,我们称这个目标为Model on Model,即利用新的模型来分析评估深度模型;在大数据分析建模的基础上,尝试对已有模型进行深度强化学习(DRL),提高深度学习调试效率。2.2 架构系统主要分为四层:输入层、解析层、评估层、输出层;同时包括五大组件:Tensorboard+可视化分析;TensorViewer日志展示对比;TensorDealer集成配置;TensorTracer数据透出;TensorDissection分析调优。2.3 进展2.3.1 高性能可视化组件TensorBoard+Google的TensorBoard(简称为TB)是TensorFlow(简称为TF)的可视化组件,可以查看深度学习的网络结构、中间指标等。原生的TB是单机版命令行方式运行,无法多用户使用;易用性差,每次切换日志路径都需要kill掉当前进程;同时性能也很差,加载工业模型数据立即卡死;指标分层混乱,几千个指标全都罗列,无法查看;用法复杂功能较弱,不支持已展示图形的二次数据对比,不支持X轴浮点数据展示等。因此,我们重构了TB的核心代码,支持GB级日志加载和数据分层,将整个服务改造成多用户版本,利用Docker灵活管理资源并自动回收。UI上支持了高亮自定义指标、分层展示、数据对比、日志上传等,具体如下:支持在线更改TF日志路径:支持图形数据在线聚合对比:支持X轴浮点数值类型展示:支持图形数据Hightlight分维度显示:支持手动调整前端定时刷新时间,实时展示数据:2.3.2 集成配置日志管理系统TensorViewerTF的任务缺乏有效管理,用户无法按需查看和分析数据,更无法回顾历史数据。我们打通了TF与DeepInsight的通路,收集了所有任务的信息,用户可以查看每次训练的实时数据和所有历史数据,支持多任务对比分析;同时支持一键跳转到Tensorboard+,直接对当前日志数据进行可视化展示。2.3.3 改进TensorFlow的可视化数据透出我们定义了一套数据透出方式,可以把所有内部数据透出成统一的Summary格式,并被Tensorboard+处理。由于PS架构没有Master集中处理中间数据,再加上张量、梯度等指标的透出是极为消耗资源的,所以,如何透出数据是值得深入研究的。当前我们在Worker0上透出数据,能满足一般模型训练的要求,未来,会研究Snapshot数据透出方案,在大规模网络下也能取得较好效果。当前,我们已经初步解析了Tensorflow透出的过程指标,正在这些海量指标上进行有监督和无监督的建模探索。2.3.4 改进模型评估指标Tensorflow自带的AUC计算方式分桶较少,计算精度有bug,在处理大量数据时性能不够,而且,仅仅能计算AUC,无法绘制ROC、PR等曲线。我们改进了计算方式,引入更多桶,并提升计算效率,同时,绘制了更多新的指标。当前绘制的指标包括AUC、ROC、PR、波动率、正负样本分桶分布。通过观察正负样本的分布,我们发现Tensorflow异步计算的缺陷,导致某些桶的样本数量有误差,会带来AUC上极小波动,这个bug目前尚未解决。所有的预估指标都无缝接入DeepInsight平台。2.3.5 研究模型训练中间指标通过深入观察和建模大规模Embedding子网络的训练指标,我们发现权重(偏置)值的变化可以反应出相关网络结构是否被有效训练。权重(偏置)值变化微弱的区域即为训练的“盲区”—该部分网络没有被训练起来。通过观察权重(偏置)的梯度,可以帮助我们诊断梯度弥散或梯度爆炸等问题,分析了解训练该部分网络的难易程度,有针对性地调整优化器以及学习率等设置。通过全面考察整个网络各部分的激活以及梯度,可以帮助我们深入了解整个网络前后向多路信息相互耦合、协同传导的复杂机制,从而更有效地进行模型结构的设计调优。对中间指标的研究会沉淀回流到DeepInsight,在训练指标产出后,对用户给予提示,做到辅助决策的作用。本文作者:孙凯阅读原文本文来自云栖社区合作伙伴“ 阿里技术”,如需转载请联系原作者。

February 22, 2019 · 1 min · jiezi

Java日志组件间关系

一、 总览本文章不对日志组件进行优劣评价,只是对关系进行对比。在日志中组件中存在这样的几种关系, 这几种关系理解清楚, 有助于我们对日志的引入和使用。二、 日志门面日志门面就是指直接引入我们程序中进行记录日志的日志组件,作为日志门面的这些组件会在程序中直接依赖, 上图中就列举的几种常见的日志门面的组件。像一些软件直接回默认使用一些组件, 比如Spring使用的就是commons-logging, activiti使用的日志门面就是slf4j, 其他的软件也都会选用自己认为好用的日志门面。三、 日志实现除了log4j既是门面又是实现之外, commons-logging和slf4j 都是能直接打印日志的, 都需要依赖一个日志实现来打印日志,上图中也举了几个日志实现。四、桥接方式slf4j默认和logback做了一些桥接的处理,那么桥接的作用是什么呢, 假如我想使用slf4j做为实现的门面,然而同时我想使用log4j作为真正的日志实现,这个时候就需要slf4j-logrj12 jar包, 现在应该可以理解桥接方式了。五、 改变依赖这里的作用是为了避免一些冲突, 例如在这样的场景下,我们使用spring做为开发,而我们开发的软件要使用的是slf4j作为日志门面,这个时候因为Spirng默认使用的是commons-logging作为日志门面,这个时候就会发生一些冲突, 所以我们可以引入jcl-over-slf4j, 通过这个组件把commons-logging覆盖掉, 为了把历史软件内部的依赖覆盖掉, 就可通过这个方式改变依赖。

February 19, 2019 · 1 min · jiezi

如何合理的规划jvm性能调优

JVM性能调优涉及到方方面面的取舍,往往是牵一发而动全身,需要全盘考虑各方面的影响。但也有一些基础的理论和原则,理解这些理论并遵循这些原则会让你的性能调优任务将会更加轻松。为了更好的理解本篇所介绍的内容。你需要已经了解和遵循以下内容:1、已了解jvm 垃圾收集器2、已了解jvm 性能监控常用工具3、能够读懂gc日志4、确信不为了调优而调优,jvm调优不能解决一切性能问题如果对这些不了解不建议读本篇文章。本篇文章基于jvm性能调优,结合jvm的各项参数对应用程序调优,主要内容有以下几个方面:1、jvm调优的一般流程2、jvm调优所要关注的几个性能指标3、jvm调优需要掌握的一些原则4、调优策略&示例一、性能调优的层次为了提升系统性能,我们需要对系统的各个角度和层次来进行优化,以下是需要优化的几个层次。从上面我们可以看到,除了jvm调优以外,还有其他几个层面需要来处理,所以针对系统的调优不是只有jvm调优一项,而是需要针对系统来整体调优,才能提升系统的性能。本篇只针对jvm调优来讲解,其他几个方面,后续再介绍。在进行jvm调优之前,我们假设项目的架构调优和代码调优已经进行过或者是针对当前项目是最优的。这两个是jvm调优的基础,并且架构调优是对系统影响最大的 ,我们不能指望一个系统架构有缺陷或者代码层次优化没有穷尽的应用,通过jvm调优令其达到一个质的飞跃,这是不可能的。另外,在调优之前,必须得有明确的性能优化目标, 然后找到其性能瓶颈。之后针对瓶颈的优化,还需要对应用进行压力和基准测试,通过各种监控和统计工具,确认调优后的应用是否已经达到相关目标。二、jvm调优流程调优的最终目的都是为了令应用程序使用最小的硬件消耗来承载更大的吞吐。jvm的调优也不例外,jvm调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量。当然这里的最少是最优的选择,而不是越少越好。1、性能定义要查找和评估器性能瓶颈,首先要知道性能定义,对于jvm调优来说,我们需要知道以下三个定义属性,依作为评估基础:吞吐量:重要指标之一,是指不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标。延迟:其度量标准是缩短由于垃圾啊收集引起的停顿时间或者完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。内存占用:垃圾收集器流畅运行所需要 的内存数量。这三个属性中,其中一个任何一个属性性能的提高,几乎都是以另外一个或者两个属性性能的损失作代价,不可兼得,具体某一个属性或者两个属性的性能对应用来说比较重要,要基于应用的业务需求来确定。2、性能调优原则在调优过程中,我们应该谨记以下3个原则,以便帮助我们更轻松的完成垃圾收集的调优,从而达到应用程序的性能要求。1. MinorGC回收原则: 每次minor GC 都要尽可能多的收集垃圾对象。以减少应用程序发生Full GC的频率。2. GC内存最大化原则:处理吞吐量和延迟问题时候,垃圾处理器能使用的内存越大,垃圾收集的效果越好,应用程序也会越来越流畅。3. GC调优3选2原则: 在性能属性里面,吞吐量、延迟、内存占用,我们只能选择其中两个进行调优,不可三者兼得。3、性能调优流程以上就是对应用程序进行jvm调优的基本流程,我们可以看到,jvm调优是根据性能测试结果不断优化配置而多次迭代的过程。在达到每一个系统需求指标之前,之前的每个步骤都有可能经历多次迭代。有时候为了达到某一方面的指标,有可能需要对之前的参数进行多次调整,进而需要把之前的所有步骤重新测试一遍。另外调优一般是从满足程序的内存使用需求开始的,之后是时间延迟的要求,最后才是吞吐量的要求,要基于这个步骤来不断优化,每一个步骤都是进行下一步的基础,不可逆行之。以下我们针对每个步骤进行详细的示例讲解。在JVM的运行模式方面,我们直接选择server模式,这也是jdk1.6以后官方推荐的模式。在垃圾收集器方面,我们直接采用了jdk1.6-1.8 中默认的parallel收集器(新生代采用parallelGC,老生代采用parallelOldGC)。三、确定内存占用在确定内存占用之前,我们需要知道两个知识点:应用程序的运行阶段jvm内存分配1、运行阶段应用程序的运行阶段,我可以划分为以下三个阶段:1、初始化阶段 : jvm加载应用程序,初始化应用程序的主要模块和数据。2、稳定阶段:应用在此时运行了大多数时间,经历过压力测试的之后,各项性能参数呈稳定状态。核心函数被执行,已经被jit编译预热过。3、总结阶段:最后的总结阶段,进行一些基准测试,生成响应的策报告。这个阶段我们可以不关注。确定内存占用以及活跃数据的大小,我们应该是在程序的稳定阶段来进行确定,而不是在项目起初阶段来进行确定,如何确定,我们先看以下jvm的内存分配。2、jvm内存分配&参数jvm堆中主要的空间,就是以上新生代、老生代、永久代组成,整个堆大小=新生代大小 + 老生代大小 + 永久代大小。 具体的对象提升方式,这里不再过多介绍了,我们看下一些jvm命令参数,对堆大小的指定。如果不采用以下参数进行指定的话,虚拟机会自动选择合适的值,同时也会基于系统的开销自动调整。在设置的时候,如果关注性能开销的话,应尽量把永久代的初始值与最大值设置为同一值,因为永久代的大小调整需要进行FullGC 才能实现。3、计算活跃数据大小计算活跃数据大小应该遵循以下流程:如前所述,活跃数据应该是基于应用程序稳定阶段时,观察长期存活与对象在java堆中占用的空间大小。计算活跃数据时应该确保以下条件发生:1.测试时,启动参数采用jvm默认参数,不人为设置。2.确保Full GC 发生时,应用程序正处于稳定阶段。采用jvm默认参数启动,是为了观察应用程序在稳定阶段的所需要的内存使用。如何才算稳定阶段?一定得需要产生足够的压力,找到应用程序和生产环境高峰符合状态类似的负荷,在此之后达到峰值之后,保持一个稳定的状态,才算是一个稳定阶段。所以要达到稳定阶段,压力测试是必不可少的,具体如何如何对应用压力测试,本篇不过多说明,后期会有专门介绍的篇幅。在确定了应用出于稳定阶段的时候,要注意观察应用的GC日志,特别是Full GC 日志。GC日志指令: -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:<filename>GC日志是收集调优所需信息的最好途径,即便是在生产环境,也可以开启GC日志来定位问题,开启GC日志对性能的影响极小,却可以提供丰富数据。必须得有FullGC 日志,如果没有的话,可以采用监控工具强制调用一次,或者采用以下命令,亦可以触发jmap -histo:live pid在稳定阶段触发了FullGC我们一般会拿到如下信息:从以上gc日志中,我们大概可以分析到,在发生fullGC之时,整个应用的堆占用以及GC时间,当然了,为了更加精确,应该多收集几次,获取一个平均值。或者是采用耗时最长的一次FullGC来进行估算。在上图中,fullGC之后,老年代空间占用在93168kb(约93MB),我们以此定为老年代空间的活跃数据。其他堆空间的分配,基于以下规则来进行。基于以上规则和上图中的FullGC信息,我们现在可以规划的该应用堆空间为:java 堆空间: 373Mb (=老年代空间93168kb4)新生代空间:140Mb(=老年代空间93168kb1.5)永久代空间:5Mb(=永久代空间3135kb1.5)老年代空间: 233Mb=堆空间-新生代看空间=373Mb-140Mb对应的应用启动参数应该为:java -Xms373m -Xmx373m -Xmn140m -XX:PermSize=5m -XX:MaxPermSize=5m四、延迟调优在确定了应用程序的活跃数据大小之后,我们需要再进行延迟性调优,因为对于此时堆内存大小,延迟性需求无法达到应用的需要,需要基于应用的情况来进行调试。在这一步进行期间,我们可能会再次优化堆大小的配置,评估GC的持续时间和频率、以及是否需要切换到不同的垃圾收集器上。1、系统延迟需求在调优之前,我们需要知道系统的延迟需求是那些,以及对应的延迟可调优指标是那些。应用程序可接受的平均停滞时间: 此时间与测量的Minor GC持续时间进行比较。可接受的Minor GC频率:Minor GC的频率与可容忍的值进行比较。可接受的最大停顿时间: 最大停顿时间与最差情况下FullGC的持续时间进行比较。可接受的最大停顿发生的频率:基本就是FullGC的频率。以上中,平均停滞时间和最大停顿时间,对用户体验最为重要,可以多关注。基于以上的要求,我们需要统计以下数据:MinorGC的持续时间;统计MinorGC的次数;FullGC的最差持续时间;最差情况下,FullGC的频率;2、优化新生代的大小比如如上的gc日志中,我们可以看到Minor GC的平均持续时间=0.069秒,MinorGC 的频率为0.389秒一次。如果,我们系统的设置的平均停滞时间为50ms,当前的69ms明显是太长了,就需要调整。我们知道新生代空间越大,Minor GC的GC时间越长,频率越低。如果想减少其持续时长,就需要减少其空间大小。如果想减小其频率,就需要加大其空间大小。为了降低改变新生代的大小对其他区域的最小影响。在改变新生代空间大小的时候,尽量保持老年代空间的大小。比如此次减少了新生代空间10%的大小,应该保持老年代和持代的大小不变化,第一步调优后的参数如下变化:java -Xms359m -Xmx359m -Xmn126m -XX:PermSize=5m -XX:MaxPermSize=5m新生代的大小有140m变为126,堆大小顺应变化,此时老年代是没有变化的。3、优化老年代的大小同上一步一样,在优化之前,也需要采集gc日志的数据。此次我们关注的是FullGC的持续时间和频率。上图中,我们可以看到FullGC 平均频率 =5.8sFullGC 平均持续时间=0.14s(以上为了测试,真实项目的fullGC 没有这么快)如果没有FullGC的日志,有办法可以评估么?我们可以通过对象提升率进行计算。对象提升率比如上述中启动参数中,我们的老年代大小=233Mb。那么需要多久才能填满老年代中这233Mb的空闲空间取决于新生代到老年代的提升率。每次提升老年代占用量=每次MinorGC 之后 java堆占用情况 减去 MinorGC后新生代的空间占用对象提升率=平均值(每次提升老年代占用量) 除以 老年代空间有了对象提升率,我们就可以算出填充满老年代空间需要多少次minorGC,大概一次fullGC的时间就可以计算出来了。比如:上图中:第一次minor GC 之后,老年代空间:13740kb - 13732kb =8kb第二次minor GC 之后,老年代空间:22394kb - 17905kb =4489kb第三次minor GC 之后,老年代空间:34739kb - 17917kb =16822kb第四次minor GC 之后,老年代空间:48143kb - 17913kb =30230kb第五次minor GC 之后,老年代空间:62112kb - 17917kb =44195kb老年代每次minorGC提升率4481kb 第二次和第一次minorGC之间12333kb 第3次和第2次minorGC之间13408kb 第4次和第3次minorGC之间13965kb 第5次和第4次minorGC之间我们可以测算出:每次minorGC 的平均提升为12211kb,约为12Mb上图中,平均minorGC的频率为 213ms/次提升率=12211kb/213ms=57kb/ms老年代空间233Mb ,占满大概需要2331024/57=4185ms 约为4.185s。FullGC的预期最差频率时长可以通过以上两种方式估算出来,可以调整老年代的大小来调整FullGC的频率,当然了,如果FullGC持续时间过长,无法达到应用程序的最差延迟要求,就需要切换垃圾处理器了。具体如何切换,下篇再讲,比如切换为CMS,针对CMS的调优方式又有会细微的差别。五、吞吐量调优经过上述漫长 调优过程,最终来到了调优的最后一步,这一步对上述的结果进行吞吐量测试,并进行微调。吞吐量调优主要是基于应用程序的吞吐量要求而来的,应用程序应该有一个综合的吞吐指标,这个指标基于真个应用的需求和测试而衍生出来的。当有应用程序的吞吐量达到或者超过预期的吞吐目标,整个调优过程就可以圆满结束了。如果出现调优后依然无法达到应用程序的吞吐目标,需要重新回顾吞吐要求,评估当前吞吐量和目标差距是否巨大,如果在20%左右,可以修改参数,加大内存,再次从头调试,如果巨大就需要从整个应用层面来考虑,设计以及目标是否一致了,重新评估吞吐目标。对于垃圾收集器来说,提升吞吐量的性能调优的目标就是就是尽可能避免或者很少发生FullGC 或者Stop-The-World压缩式垃圾收集(CMS),因为这两种方式都会造成应用程序吞吐降低。尽量在MinorGC 阶段回收更多的对象,避免对象提升过快到老年代。六、最后据Plumbr公司对特定垃圾收集器使用情况进行了一次调查研究,研究数据使用了84936个案例。在明确指定垃圾收集器的13%的案例中,并发收集器(CMS)使用次数最多;但大多数案例没有选择最佳垃圾收集器。这个比例占用在87%左右。JVM调优是一个系统而又复杂的工作,目前jvm下的自动调整已经做的比较优秀,基本的一些初始参数都可以保证一般的应用跑的比较稳定了,对部分团队来说,程序性能可能优先级不高,默认垃圾收集器已经够用了。调优要基于自己的情况而来。本文作者:wier_ali阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 18, 2019 · 1 min · jiezi

日志服务与SIEM(如Splunk)集成方案实战

背景信息目标本文主要介绍如何让阿里云日志服务与您的SIEM方案(如Splunk)对接, 以便确保阿里云上的所有法规、审计、与其他相关日志能够导入到您的安全运维中心(SOC)中。名词解释LOG(SLS) - 阿里云日志服务,简写SLS表示(Simple Log Service)。SIEM - 安全信息与事件管理系统(Security Information and Event Management),如Splunk, QRadar等。Splunk HEC - Splunk的Http事件接收器(Splunk Http Event Collector), 一个 HTTP(s)接口,用于接收日志。审计相关日志安全运维团队一般对阿里云相关的审计日志感兴趣,如下列出所有存在于所有目前在日志服务中可用的相关日志(但不限于):Regions化 - 时刻更新,请以最新的产品文档为准。阿里云日志服务阿里云的日志服务(log service)是针对日志类数据的一站式服务,无需开发就能快捷完成海量日志数据的采集、消费、投递以及查询分析等功能,提升运维、运营效率。日志服务主要包括 实时采集与消费、数据投递、查询与实时分析 等功能,适用于从实时监控到数据仓库的各种开发、运维、运营与安全场景:目前,以上各个阿里云产品已经与日志服务打通,提供近实时的日志自动采集存储、并提供基于日志服务的查询分析、报表报警、下游计算对接与投递的能力。集成方案建议概念项目(Project)项目(Project)是日志服务中的资源管理单元,用于资源隔离和控制。您可以通过项目来管理某一个应用的所有日志及相关的日志源。它管理着用户的所有日志库(Logstore),采集日志的机器配置等信息,同时它也是用户访问日志服务资源的入口。日志库(Logstore)日志库(Logstore)是日志服务中日志数据的收集、存储和查询单元。每个日志库隶属于一个项目,且每个项目可以创建多个日志库。分区(Shard)每个日志库分若干个分区(Shard),每个分区由MD5左闭右开区间组成,每个区间范围不会相互覆盖,并且所有的区间的范围是MD5整个取值范围。服务入口(Endpoint)日志服务入口是访问一个项目(Project)及其内部日志数据的 URL。它和 Project 所在的阿里云区域(Region)及 Project 名称相关。https://help.aliyun.com/document_detail/29008.html访问秘钥(AccessKey)阿里云访问秘钥是阿里云为用户使用 API(非控制台)来访问其云资源设计的“安全口令”。您可以用它来签名 API 请求内容以通过服务端的安全验证。https://help.aliyun.com/document_detail/29009.html假设这里假设您的SIEM(如Splunk)位于组织内部环境(on-premise)中,而不是云端。为了安全考虑,没有任何端口开放让外界环境来访问此SIEM。概览推荐使用SLS消费组构建程序来从SLS进行实时消费,然后通过Splunk API(HEC)来发送日志给Splunk。使用消费组编程协同消费库(Consumer Library)是对日志服务中日志进行消费的高级模式,提供了消费组(ConsumerGroup)的概念对消费端进行抽象和管理,和直接使用SDK进行数据读取的区别在于,用户无需关心日志服务的实现细节,只需要专注于业务逻辑,另外,消费者之间的负载均衡、failover等用户也都无需关心。Spark Streaming、Storm 以及Flink Connector都以Consumer Library作为基础实现。基本概念消费组(Consumer Group) - 一个消费组由多个消费者构成,同一个消费组下面的消费者共同消费一个logstore中的数据,消费者之间不会重复消费数据。消费者(Consumer) - 消费组的构成单元,实际承担消费任务,同一个消费组下面的消费者名称必须不同。在日志服务中,一个logstore下面会有多个shard,协同消费库的功能就是将shard分配给一个消费组下面的消费者,分配方式遵循以下原则:每个shard只会分配到一个消费者。一个消费者可以同时拥有多个shard。新的消费者加入一个消费组,这个消费组下面的shard从属关系会调整,以达到消费负载均衡的目的,但是上面的分配原则不会变,分配过程对用户透明。协同消费库的另一个功能是保存checkpoint,方便程序故障恢复时能接着从断点继续消费,从而保证数据不会被重复消费。部署建议硬件建议硬件参数:需要一台机器运行程序,安装一个Linux(如Ubuntu x64),推荐硬件参数如下:2.0+ GHZ X 8核16GB 内存,推荐32GB1 Gbps网卡至少2GB可用磁盘空间,建议10GB以上网络参数:从组织内的环境到阿里云的带宽应该大于数据在阿里云端产生的速度,否则日志无法实时消费。假设数据产生一般速度均匀,峰值在2倍左右,每天100TB原始日志。5倍压缩的场景下,推荐带宽应该在4MB/s(32Mbps)左右。使用(Python)这里我们描述用Python使用消费组进行编程。对于Java语言用法,可以参考这篇文章.注意:本篇文章的代码可能会更新,最新版本在这里可以找到:Github样例.安装环境强烈推荐PyPy3来运行本程序,而不是使用标准CPython解释器。日志服务的Python SDK可以如下安装:pypy3 -m pip install aliyun-log-python-sdk -U更多SLS Python SDK的使用手册,可以参考这里程序配置如下展示如何配置程序:配置程序日志文件,以便后续测试或者诊断可能的问题。基本的日志服务连接与消费组的配置选项。消费组的一些高级选项(性能调参,不推荐修改)。SIEM(Splunk)的相关参数与选项。请仔细阅读代码中相关注释并根据需要调整选项:#encoding: utf8import osimport loggingfrom logging.handlers import RotatingFileHandlerroot = logging.getLogger()handler = RotatingFileHandler("{0}_{1}.log".format(os.path.basename(file), current_process().pid), maxBytes=10010241024, backupCount=5)handler.setFormatter(logging.Formatter(fmt=’[%(asctime)s] - [%(threadName)s] - {%(module)s:%(funcName)s:%(lineno)d} %(levelname)s - %(message)s’, datefmt=’%Y-%m-%d %H:%M:%S’))root.setLevel(logging.INFO)root.addHandler(handler)root.addHandler(logging.StreamHandler())logger = logging.getLogger(name)def get_option(): ########################## # 基本选项 ########################## # 从环境变量中加载SLS参数与选项 endpoint = os.environ.get(‘SLS_ENDPOINT’, ‘’) accessKeyId = os.environ.get(‘SLS_AK_ID’, ‘’) accessKey = os.environ.get(‘SLS_AK_KEY’, ‘’) project = os.environ.get(‘SLS_PROJECT’, ‘’) logstore = os.environ.get(‘SLS_LOGSTORE’, ‘’) consumer_group = os.environ.get(‘SLS_CG’, ‘’) # 消费的起点。这个参数在第一次跑程序的时候有效,后续再次运行将从上一次消费的保存点继续。 # 可以使”begin“,”end“,或者特定的ISO时间格式。 cursor_start_time = “2018-12-26 0:0:0” ########################## # 一些高级选项 ########################## # 一般不要修改消费者名,尤其是需要并发跑时 consumer_name = “{0}-{1}".format(consumer_group, current_process().pid) # 心跳时长,当服务器在2倍时间内没有收到特定Shard的心跳报告时,服务器会认为对应消费者离线并重新调配任务。 # 所以当网络不是特别好的时候,不要调整的特别小。 heartbeat_interval = 20 # 消费数据的最大间隔,如果数据生成的速度很快,并不需要调整这个参数。 data_fetch_interval = 1 # 构建一个消费组和消费者 option = LogHubConfig(endpoint, accessKeyId, accessKey, project, logstore, consumer_group, consumer_name, cursor_position=CursorPosition.SPECIAL_TIMER_CURSOR, cursor_start_time=cursor_start_time, heartbeat_interval=heartbeat_interval, data_fetch_interval=data_fetch_interval) # Splunk选项 settings = { “host”: “10.1.2.3”, “port”: 80, “token”: “a023nsdu123123123”, ‘https’: False, # 可选, bool ’timeout’: 120, # 可选, int ‘ssl_verify’: True, # 可选, bool “sourcetype”: “”, # 可选, sourcetype “index”: “”, # 可选, index “source”: “”, # 可选, source } return option, settings数据消费与转发如下代码展示如何从SLS拿到数据后转发给Splunk。from aliyun.log.consumer import from aliyun.log.pulllog_response import PullLogResponsefrom multiprocessing import current_processimport timeimport jsonimport socketimport requestsclass SyncData(ConsumerProcessorBase): "”" 这个消费者从SLS消费数据并发送给Splunk """ def init(self, splunk_setting): “““初始化并验证Splunk连通性””” super(SyncData, self).init() assert splunk_setting, ValueError(“You need to configure settings of remote target”) assert isinstance(splunk_setting, dict), ValueError(“The settings should be dict to include necessary address and confidentials.”) self.option = splunk_setting self.timeout = self.option.get(“timeout”, 120) # 测试Splunk连通性 s = socket.socket() s.settimeout(self.timeout) s.connect((self.option[“host”], self.option[‘port’])) self.r = requests.session() self.r.max_redirects = 1 self.r.verify = self.option.get(“ssl_verify”, True) self.r.headers[‘Authorization’] = “Splunk {}".format(self.option[’token’]) self.url = “{0}://{1}:{2}/services/collector/event”.format(“http” if not self.option.get(‘https’) else “https”, self.option[‘host’], self.option[‘port’]) self.default_fields = {} if self.option.get(“sourcetype”): self.default_fields[‘sourcetype’] = self.option.get(“sourcetype”) if self.option.get(“source”): self.default_fields[‘source’] = self.option.get(“source”) if self.option.get(“index”): self.default_fields[‘index’] = self.option.get(“index”) def process(self, log_groups, check_point_tracker): logs = PullLogResponse.loggroups_to_flattern_list(log_groups, time_as_str=True, decode_bytes=True) logger.info(“Get data from shard {0}, log count: {1}".format(self.shard_id, len(logs))) for log in logs: # 发送数据到Splunk # 如下代码只是一个样例(注意:所有字符串都是unicode) # Python2: {u”time”: u"12312312", u"topic": u"topic", u"field1": u"value1", u"field2": u"value2"} # Python3: {"time": “12312312”, “topic”: “topic”, “field1”: “value1”, “field2”: “value2”} event = {} event.update(self.default_fields) if log.get(u"topic") == ‘audit_log’: # suppose we only care about audit log event[’time’] = log[u’time’] event[‘fields’] = {} del log[’time’] event[‘fields’].update(log) data = json.dumps(event, sort_keys=True) try: req = self.r.post(self.url, data=data, timeout=self.timeout) req.raise_for_status() except Exception as err: logger.debug(“Failed to connect to remote Splunk server ({0}). Exception: {1}”, self.url, err) # TODO: 根据需要,添加一些重试或者报告的逻辑 logger.info(“Complete send data to remote”) self.save_checkpoint(check_point_tracker)主逻辑如下代码展示主程序控制逻辑:def main(): option, settings = get_monitor_option() logger.info("** start to consume data…") worker = ConsumerWorker(SyncData, option, args=(settings,) ) worker.start(join=True)if name == ‘main’: main()启动假设程序命名为"sync_data.py",可以如下启动:export SLS_ENDPOINT=<Endpoint of your region>export SLS_AK_ID=<YOUR AK ID>export SLS_AK_KEY=<YOUR AK KEY>export SLS_PROJECT=<SLS Project Name>export SLS_LOGSTORE=<SLS Logstore Name>export SLS_CG=<消费组名,可以简单命名为"syc_data">pypy3 sync_data.py限制与约束每一个日志库(logstore)最多可以配置10个消费组,如果遇到错误ConsumerGroupQuotaExceed则表示遇到限制,建议在控制台端删除一些不用的消费组。监测在控制台查看消费组状态通过云监控查看消费组延迟,并配置报警性能考虑启动多个消费者基于消费组的程序可以直接启动多次以便达到并发作用:nohup pypy3 sync_data.py &nohup pypy3 sync_data.py &nohup pypy3 sync_data.py &…注意: 所有消费者使用了同一个消费组的名字和不同的消费者名字(因为消费者名以进程ID为后缀)。因为一个分区(Shard)只能被一个消费者消费,假设一个日志库有10个分区,那么最多有10个消费者同时消费。Https如果服务入口(endpoint)配置为https://前缀,如https://cn-beijing.log.aliyuncs.com,程序与SLS的连接将自动使用HTTPS加密。服务器证书*.aliyuncs.com是GlobalSign签发,默认大多数Linux/Windows的机器会自动信任此证书。如果某些特殊情况,机器不信任此证书,可以参考这里下载并安装此证书。性能吞吐基于测试,在没有带宽限制、接收端速率限制(如Splunk端)的情况下,以推进硬件用pypy3运行上述样例,单个消费者占用大约10%的单核CPU下可以消费达到5 MB/s原始日志的速率。因此,理论上可以达到50 MB/s原始日志每个CPU核,也就是每个CPU核每天可以消费4TB原始日志。注意: 这个数据依赖带宽、硬件参数和SIEM接收端(如Splunk)是否能够较快接收数据。高可用性消费组会将检测点(check-point)保存在服务器端,当一个消费者停止,另外一个消费者将自动接管并从断点继续消费。可以在不同机器上启动消费者,这样当一台机器停止或者损坏的清下,其他机器上的消费者可以自动接管并从断点进行消费。理论上,为了备用,也可以启动大于shard数量的消费者。更多参考日志服务Python消费组实战(一):日志服务与SIEM(如Splunk)集成实战日志服务Python消费组实战(二):实时日志分发日志服务Python消费组实战(三):实时跨域监测多日志库数据本文Github样例本文作者:成喆阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 22, 2019 · 3 min · jiezi

解读:spring-boot logging。记一次Logback在spring-boot中的使用方法

有个任务停留在任务列表中很久了:使用Appenders 完成 loger4j 的日志推送,始终没有成功实现。追其原因,仍然是官方的文档没有认真看。在spring-boot的项目中看到log4j,就想当然的认为Spring-boot使用的是log4j,然后不假思索的去google。最终导致的就是:功能没有实现,而且还浪费了很多不必要的时间,最后:还是老老实实的回来阅读spring-boot的官方文档。本文主要对官方文档Logging部分进行解读。原文地址:https://docs.spring.io/spring-boot/docs/current/reference/html/howto-logging.html.如果你使用的是不是最新版本,那么应该使用https://docs.spring.io/spring-boot/docs/版本号/reference/htmlsingle/#boot-features-logging 如:https://docs.spring.io/spring-boot/docs/1.5.3.RELEASE/reference/htmlsingle/#boot-features-logging76 日志在web开中,我们仅需要依赖于spring-boot-starter-web便自动启用了日志系统Logback。如果仅仅是想改变日志的等级,则可以直接使用logging.level前缀在application.properties中进行设置,比如:logging.level.org.springframework.web=DEBUGlogging.level.org.hibernate=ERROR除了控制日志的等级外,还可以使用logging.file来定义日志输入到的文件位置。如果我们还想配置更多选项,则可以在classpath(resourse)中定义logback.xml或logback-spring.xml。76.1 配置Logback找到logback.xml或logback-spring.xml,复制以下基本内容:<?xml version=“1.0” encoding=“UTF-8”?><configuration> <include resource=“org/springframework/boot/logging/logback/base.xml”/> <logger name=“org.springframework.web” level=“DEBUG”/></configuration>使用idea的ctrl+o来打开spring-boot jar中的base.xml,我们会看到配置信息包含一些特殊的字符,解读如下:${PID}当前的进程ID${LOG_FILE} 如果设置了logging.file,则使用logging.file做为日志输入文件。${LOG_PATH} 同上.指定日志输出路径。${LOG_EXCEPTION_CONVERSION_WORD} ..我们自己定义日志输入的方式和字符串时,当然也可以使用它们了。76.1.1 配置:将日志仅写入文件如果我们想禁用控制台的日志输出(生产环境中,我们的确是要这么做的),然后把日志写入某个日志文件的话。那么需要新建logback-spring.xml,并引入file-appender.xml,比如:<?xml version=“1.0” encoding=“UTF-8”?><configuration> <include resource=“org/springframework/boot/logging/logback/defaults.xml” /> <property name=“LOG_FILE” value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}spring.log}"/> <include resource=“org/springframework/boot/logging/logback/file-appender.xml” /> <root level=“INFO”> <appender-ref ref=“FILE” /> </root></configuration>然后:在application.properties定义logging.file来指定日志文件位置.例:logging.file=myapplication.log再看看上面是怎么回事:打开org/springframework/boot/logging/logback/file-appender.xml内容如下:<?xml version=“1.0” encoding=“UTF-8”?><!–File appender logback configuration provided for import, equivalent to the programmaticinitialization performed by Boot–><included> <appender name=“FILE” class=“ch.qos.logback.core.rolling.RollingFileAppender”> <encoder> <pattern>${FILE_LOG_PATTERN}</pattern> </encoder> <file>${LOG_FILE}</file> <rollingPolicy class=“ch.qos.logback.core.rolling.FixedWindowRollingPolicy”> <fileNamePattern>${LOG_FILE}.%i</fileNamePattern> </rollingPolicy> <triggeringPolicy class=“ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy”> <MaxFileSize>10MB</MaxFileSize> </triggeringPolicy> </appender></included>注意:这里面有个<appender name=“FILE”,指定了appender名称为FILE,对应logback-spring.xml的以下语句: <root level=“INFO”> <!–日志等级–> <appender-ref ref=“FILE” /> <!–指定appender为FILE,则前面我们刚刚找到的name值–> </root>总结有了以上内容,我们知道了如下知识点:spring-boot默认使用的是Logback而非log4j。我们可以单独建立logback-spring.xml来细化Logback的配置。在Logback中,是可以指定使用不同的appender来定义日志的输出的。是否可以自定义appender来达到将日志输出到我们的日志服务器,从而达到系统监控的目的呢? ...

January 15, 2019 · 1 min · jiezi

k8s与log--利用lua为fluent bit添加一个filter

前言之前我们介绍过fluent bit这个日志收集神器。最近我们遇到奇葩的需求,不得不利用lua编写fluent bit的filter,来满足需求。首先介绍一下需求:非容器的日志团队使用filebeat, 其配置文件部分如下:processors:- dissect: tokenizer: “/data/logs/%{appname}/%{filename}.log” field: “source” target_prefix: ““即需要从日志record的source filed 提取appname和filename两个filed。fluent bit 并没有如此的插件,所以不得不自己实现。实现lua编写filter规范官方给出的示例如下:function cb_print(tag, timestamp, record) return code, timestamp, recordendFunction 输入参数Function ArgumentsnamedescriptiontagName of the tag associated with the incoming record.timestampUnix timestamp with nanoseconds associated with the incoming record. The original format is a double (seconds.nanoseconds)recordLua table with the record contentReturn ValuesEach callback must return three values:namedata typedescriptioncodeintegerThe code return value represents the result and further action that may follows. If code equals -1, means that filter_lua must drop the record. If code equals 0 the record will not be modified, otherwise if code equals 1, means the original timestamp or record have been modified so it must be replaced by the returned values from timestamp (second return value) and record (third return value).timestampdoubleIf code equals 1, the original record timestamp will be replaced with this new value.recordtableif code equals 1, the original record information will be replaced with this new value. Note that the format of this value must be a valid Lua table.理解上面的规范可以结合下面的写法。注意返回值的code。代码实现编写实现类似功能的lua文件,如下:function dissect(tag, timestamp, record) source = record[“source”] if (source == nil) then return 0, 0, 0 else new_record = record local result = { } local from = 1 local delim_from, delim_to = string.find( source, “/”, from ) while delim_from do table.insert( result, string.sub( source, from , delim_from-1 ) ) from = delim_to + 1 delim_from, delim_to = string.find( source, “/”, from ) end table.insert( result, string.sub( source, from ) ) new_record[“appname”] = result[7] new_record[“filename”] = string.sub( result[8], 1, -5 ) return 1, timestamp, new_record end end备注:在我们k8s环境下,业务日志挂盘路径类似于下面的格式:source = /data/logs/default/tomcat/742473c7-17dc-11e9-afc5-0a07a5c4fbe2/appname/filename.logresult[7]之所以出现这种及其容易出现bug的写法,一是由于我们这边有严格的日志规范,另外,只是给大家提供一种lua写filter的思路。制作镜像我们是基于fluent bit 1.0.2 。所以找到官方的代码仓库,git clone 下来,稍作更改。新的dockerfile如下:FROM debian:stretch as builder# Fluent Bit versionENV FLB_MAJOR 1ENV FLB_MINOR 0ENV FLB_PATCH 2ENV FLB_VERSION 1.0.2ENV DEBIAN_FRONTEND noninteractiveENV FLB_TARBALL http://github.com/fluent/fluent-bit/archive/v$FLB_VERSION.zipRUN mkdir -p /fluent-bit/bin /fluent-bit/etc /fluent-bit/log /tmp/fluent-bit-master/RUN apt-get update && \ apt-get install -y –no-install-recommends \ build-essential \ cmake \ make \ wget \ unzip \ libssl1.0-dev \ libasl-dev \ libsasl2-dev \ pkg-config \ libsystemd-dev \ zlib1g-dev \ ca-certificates \ && wget -O “/tmp/fluent-bit-${FLB_VERSION}.zip” ${FLB_TARBALL} \ && cd /tmp && unzip “fluent-bit-$FLB_VERSION.zip” \ && cd “fluent-bit-$FLB_VERSION”/build/ \ && rm -rf /tmp/fluent-bit-$FLB_VERSION/build/WORKDIR /tmp/fluent-bit-$FLB_VERSION/build/RUN cmake -DFLB_DEBUG=On \ -DFLB_TRACE=Off \ -DFLB_JEMALLOC=On \ -DFLB_TLS=On \ -DFLB_SHARED_LIB=Off \ -DFLB_EXAMPLES=Off \ -DFLB_HTTP_SERVER=On \ -DFLB_IN_SYSTEMD=On \ -DFLB_OUT_KAFKA=On ..RUN make -j $(getconf _NPROCESSORS_ONLN)RUN install bin/fluent-bit /fluent-bit/bin/# Configuration filesCOPY fluent-bit.conf \ parsers.conf \ parsers_java.conf \ parsers_extra.conf \ parsers_openstack.conf \ parsers_cinder.conf \ plugins.conf \ /fluent-bit/etc/COPY dissect.lua /fluent-bit/bin/FROM gcr.io/distroless/ccMAINTAINER Eduardo Silva <eduardo@treasure-data.com>LABEL Description=“Fluent Bit docker image” Vendor=“Fluent Organization” Version=“1.1"COPY –from=builder /usr/lib/x86_64-linux-gnu/sasl /usr/lib/x86_64-linux-gnu/COPY –from=builder /usr/lib/x86_64-linux-gnu/libz /usr/lib/x86_64-linux-gnu/COPY –from=builder /lib/x86_64-linux-gnu/libz* /lib/x86_64-linux-gnu/COPY –from=builder /usr/lib/x86_64-linux-gnu/libssl.so* /usr/lib/x86_64-linux-gnu/COPY –from=builder /usr/lib/x86_64-linux-gnu/libcrypto.so* /usr/lib/x86_64-linux-gnu/# These below are all needed for systemdCOPY –from=builder /lib/x86_64-linux-gnu/libsystemd* /lib/x86_64-linux-gnu/COPY –from=builder /lib/x86_64-linux-gnu/libselinux.so* /lib/x86_64-linux-gnu/COPY –from=builder /lib/x86_64-linux-gnu/liblzma.so* /lib/x86_64-linux-gnu/COPY –from=builder /usr/lib/x86_64-linux-gnu/liblz4.so* /usr/lib/x86_64-linux-gnu/COPY –from=builder /lib/x86_64-linux-gnu/libgcrypt.so* /lib/x86_64-linux-gnu/COPY –from=builder /lib/x86_64-linux-gnu/libpcre.so* /lib/x86_64-linux-gnu/COPY –from=builder /lib/x86_64-linux-gnu/libgpg-error.so* /lib/x86_64-linux-gnu/COPY –from=builder /fluent-bit /fluent-bit#EXPOSE 2020# Entry pointCMD ["/fluent-bit/bin/fluent-bit”, “-c”, “/fluent-bit/etc/fluent-bit.conf”]注意增加了 COPY dissect.lua /fluent-bit/bin/ 。然后就build镜像即可。使用姿势使用比较简单的。demo如下: [FILTER] Name lua Match app.* script /fluent-bit/bin/dissect.lua call dissectscript lua脚本的存放路径。call 即为lua函数名。总结通过写这个filter,有一下几个感悟吧。官方的镜像基于谷歌的distroless镜像,没有shell,没有包管理,调试起来很费力。平时的业务容器化场景中,明显的不合适,与阿里的富容器思维南辕北辙。当然除非你公司的业务开发能力足够强。fluent bit 相关的资料还是有点少。遇到问题和使用一些不明白的地方,解决起来费力。除非你是c专家。官方文档写的也不够详细,只是描述了个大概。 ...

January 14, 2019 · 3 min · jiezi

SpringBoot 日志框架

日志框架日志框架就是为了更好的记录日志使用的,记录日志是为了我们在工作中可以更好的查找相对应的问题,也算是以中留痕操作!以前我们刚开始学习的时候都是用System.out.println()去在控制台记录,怎么说呢?这种方式伴随着我们很长时间,之后我们就遇到了断点调试的方式,逐渐不在使用System.out.println()进行调试,但是你别忘记,那是一种记录不管是否有用,你都应该去记录!市面上的日志框架;JUL、JCL、Jboss-logging、logback、log4j、log4j2、slf4j….日志门面 (日志的抽象层)日志实现JCL(Jakarta Commons Logging) SLF4j(Simple Logging Facade for Java) jboss-loggingLog4j JUL(java.util.logging) Log4j2 LogbackSpringBoot:底层是Spring框架,Spring框架默认是用JCL;‘ <==SpringBoot选用 SLF4j和logback;==>1.SLF4j使用1.1 如何在系统中使用SLF4jSLF4J的官方网站手册以后开发的时候,日志记录方法的调用,不应该来直接调用日志的实现类,而是调用日志抽象层里面的方法;给系统里面导入slf4j的jar和 logback的实现jar使用方式如下:import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class HelloWorld { public static void main(String[] args) { Logger logger = LoggerFactory.getLogger(HelloWorld.class); logger.info(“Hello World”); }}每一个日志的实现框架都有自己的配置文件。使用slf4j以后,配置文件还是做成日志实现框架自己本身的配置文件;1.2 历史遗留问题我们接触过的框架使用的日志框架都有所不同,因此,统一日志记录,即使是别的框架和我一起统一使用slf4j进行输出?1.3 slf4j统一“天下”将系统中其他日志框架先排除出去;用中间包来替换原有的日志框架;我们导入slf4j其他的实现;2. Spring Boot 日志配置SpringBoot使用它来做日志功能:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId></dependency>SpringBoot底层也是使用slf4j+logback的方式进行日志记录SpringBoot也把其他的日志都替换成了slf4j中间替换包?@SuppressWarnings(“rawtypes”)public abstract class LogFactory { static String UNSUPPORTED_OPERATION_IN_JCL_OVER_SLF4J = “http://www.slf4j.org/codes.html#unsupported_operation_in_jcl_over_slf4j"; static LogFactory logFactory = new SLF4JLogFactory();如果我们要引入其他框架?一定要把这个框架的默认日志依赖移除掉示例:Spring框架用的是commons-logging;<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions></dependency>pringBoot能自动适配所有的日志,而且底层使用slf4j+logback的方式记录日志,引入其他框架的时候,只需要把这个框架依赖的日志框架排除掉即可;3. 日志的使用Spring Boot 默认给我们已经配置好了日志,测试如下://记录器Logger logger = LoggerFactory.getLogger(getClass());@Testpublic void contextLoads() { //System.out.println(); //日志的级别; //由低到高 trace<debug<info<warn<error //可以调整输出的日志级别;日志就只会在这个级别以以后的高级别生效 logger.trace(“这是trace日志…”); logger.debug(“这是debug日志…”); //SpringBoot默认给我们使用的是info级别的,没有指定级别的就用SpringBoot默认规定的级别;root级别 logger.info(“这是info日志…”); logger.warn(“这是warn日志…”); logger.error(“这是error日志…”);}3.1 日志格式说明 日志输出格式: %d表示日期时间, %thread表示线程名, %-5level:级别从左显示5个字符宽度 %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息, %n是换行符 –> %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%nSpringBoot修改日志的默认配置logging.level.com.hanpang=trace# com.hanpang是说明的包名#logging.path=# 不指定路径在当前项目下生成springboot.log日志# 可以指定完整的路径;#logging.file=G:/springboot.log# 在当前磁盘的根路径下创建spring文件夹和里面的log文件夹;使用 spring.log 作为默认文件logging.path=/spring/log# 在控制台输出的日志的格式logging.pattern.console=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n# 指定文件中日志输出的格式logging.pattern.file=%d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} ==== %msg%nlogging.filelogging.pathExampleDescription(none)(none) 只在控制台输出指定文件名(none)my.log输出日志到my.log文件(none)指定目录/var/log输出到指定目录的 spring.log 文件中3.2 指定配置给类路径下放上每个日志框架自己的配置文件即可,SpringBoot就不使用他默认配置的了Logging SystemCustomizationLogbacklogback-spring.xml, logback-spring.groovy, logback.xml or logback.groovyLog4j2log4j2-spring.xml or log4j2.xmlJDK (Java Util Logging)logging.propertieslogback.xml:直接就被日志框架识别了;一般情况不推荐(推荐)logback-spring.xml:日志框架就不直接加载日志的配置项,由SpringBoot解析日志配置,可以使用SpringBoot的高级Profile功能<springProfile name=“staging”> <!– configuration to be enabled when the “staging” profile is active –> 可以指定某段配置只在某个环境下生效</springProfile>示例代码<appender name=“stdout” class=“ch.qos.logback.core.ConsoleAppender”> <!– 日志输出格式: %d表示日期时间, %thread表示线程名, %-5level:级别从左显示5个字符宽度 %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息, %n是换行符 –> <layout class=“ch.qos.logback.classic.PatternLayout”> <springProfile name=“dev”> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} —-> [%thread] —> %-5level %logger{50} - %msg%n</pattern> </springProfile> <springProfile name="!dev”> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern> </springProfile> </layout> </appender>如果使用logback.xml作为日志配置文件,还要使用profile功能,会有以下错误:no applicable action for [springProfile]4. 切换日志框架可以按照slf4j的日志适配图,进行相关的切换;slf4j+log4j的方式,pom.xml配置如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>logback-classic</artifactId> <groupId>ch.qos.logback</groupId> </exclusion> <exclusion> <artifactId>log4j-over-slf4j</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions></dependency><dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId></dependency>如果切换为log4j2,pom.xml配置如下:log4j2配置的参考文章log4j2的配置说明<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-logging</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId></dependency>5.“坑”logging.path和logging.file不可以同时配置,同时配置也只有logging.file起效配置logging.path将会在指定文件夹下面生成spring.log文件,文件名字无法控制配置logging.file,如果只是文件名如:demo.log只会在项目的根目录下生成指定文件名的日志文件,,如果想控制日志路径,可以选择完整路径,如:E:\demo\demo.log接下来看看自定义配置文件,这个就要方便很多了,还是喜欢自定义配置文件的方式在src/main/resources下面新建文件logback.xml这个也是spring boot默认的配置文件名,如果需要自定义文件名,如:logback-test.xml需要在application.properties添加配置================但是,我们习惯使用logback-spring.xml==========================logging.config=classpath:logback-test.xmlspring boot默认载入的相关配置文件,详见jar包;spring-boot-1...RELEASE.jar下面org/springframework/boot/logging/logback/详细文件:base.xml //基础包,引用了下面所有的配置文件console-appender.xml //控制台输出配置defaults.xml //默认的日志文件配置file-appender.xml //文件输出配置附录logback-spring.xml 配置说明<?xml version=“1.0” encoding=“UTF-8”?><configuration> <!–定义日志文件的存储地址 勿在 LogBack的配置中使用相对路径 –> <property name=“LOG_HOME” value="/tmp/log" /> <!– 控制台输出 –> <appender name=“STDOUT” class=“ch.qos.logback.core.ConsoleAppender”> <encoder class=“ch.qos.logback.classic.encoder.PatternLayoutEncoder”> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30} - %msg%n</pattern> </encoder> </appender> <!– 按照每天生成日志文件 –> <appender name=“FILE” class=“ch.qos.logback.core.rolling.RollingFileAppender”> <rollingPolicy class=“ch.qos.logback.core.rolling.TimeBasedRollingPolicy”> <FileNamePattern>${LOG_HOME}/logs/smsismp.log.%d{yyyy-MM-dd}.log</FileNamePattern> <!–日志文件保留天数 –> <MaxHistory>30</MaxHistory> </rollingPolicy> <encoder class=“ch.qos.logback.classic.encoder.PatternLayoutEncoder”> <!–格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 –> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30} - %msg%n</pattern> </encoder> <!–日志文件最大的大小 –> <triggeringPolicy class=“ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy”> <MaxFileSize>10MB</MaxFileSize> </triggeringPolicy> </appender> <!– 日志输出级别 –> <root level=“INFO”> <appender-ref ref=“STDOUT” /> <appender-ref ref=“FILE” /> </root> <!– 定义各个包的详细路径,继承root宝的值 –> <logger name=“com.hry.spring.log” level=“INFO” /> <logger name=“com.hry.spring” level=“TRACE” /> <!– 此值由 application.properties的spring.profiles.active=dev指定–> <springProfile name=“dev”> <!–定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径 –> <property name=“LOG_HOME” value="/tmp/log" /> <logger name=“org.springboot.sample” level=“DEBUG” /> </springProfile> <springProfile name=“pro”> <!–定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径 –> <property name=“LOG_HOME” value="/home" /> <logger name=“org.springboot.sample2” level=“INFO” /> </springProfile> </configuration>部分说明appender name=“STDOUT”: 日志打印到控制台appender name=“FILE”: 日志按日打印到文件中,最多保留MaxHistory天,每个文件大水为MaxFileSizeencoder:定义输出格式<root level=“INFO”>: 定义根logger,通过appender-ref指定前方定义的appender<logger name=“com.hry.spring.log” level=“INFO” />:在继承root的logger上对com.hry.spring.log包日志作特殊处理<springProfile name=“dev”>: 定义profile的值,只有特定profile的情况下,此间定义的内容才启作用application.properties 启动dev配置信息 server.port=8080 spring.profiles.active=devspring.profiles.active指定本次启动的active的值是什么。本次是dev,则logback-spring.xml里<springProfile name=“dev”>的内容启作用import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplicationpublic class LogApplication { private static final Logger log = LoggerFactory.getLogger(LogApplication.class); public static void main(String[] args) { String str1 = “string1”; String str2 = “string2”; log.info(“Begin Start {}…{}”, str1, str2); SpringApplication.run(LogApplication.class, args); log.info(“Stop …”); }}logback-spring.xml 其他的写法<?xml version=“1.0” encoding=“UTF-8”?><configuration> <include resource=“org/springframework/boot/logging/logback/base.xml” /> <logger name=“org.springframework.web” level=“INFO”/> <logger name=“org.springboot.sample” level=“TRACE” /> <!– 测试环境+开发环境. 多个使用逗号隔开. –> <springProfile name=“test,dev”> <logger name=“org.springframework.web” level=“DEBUG”/> <logger name=“org.springboot.sample” level=“DEBUG” /> <logger name=“com.example” level=“DEBUG” /> </springProfile> <!– 生产环境. –> <springProfile name=“prod”> <logger name=“org.springframework.web” level=“ERROR”/> <logger name=“org.springboot.sample” level=“ERROR” /> <logger name=“com.example” level=“ERROR” /> </springProfile></configuration>这里说明一下:1) 引入的base.xml是Spring Boot的日志系统预先定义了一些系统变量的基础配置文件2) 在application.properties中设置环境为prod,则只会打印error级别日志3) 如果在application.properties中定义了相同的配置,则application.properties的日志优先级更高可以在内部进行引用<?xml version=“1.0” encoding=“utf-8”?><configuration scan=“true” scanPeriod=“10 seconds”> <!– 文件输出格式 –> <property name=“pattern” value="%d{yyyy-MM-dd HH:mm:ss.SSS} -%5p ${PID:-} [%15.15t] %-40.40logger{39} : %m%n" /> <property name=“charsetEncoding” value=“UTF-8” /> <!–<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>–> <!–控制台日志–> <appender name=“console” class=“ch.qos.logback.core.ConsoleAppender”> <encoder> <pattern>${pattern}</pattern> <charset>UTF-8</charset> </encoder> </appender> <appender name=“file” class=“ch.qos.logback.core.FileAppender”> <file>./logback/logfile.log</file> <append>true</append> <encoder> <pattern>${pattern}</pattern> <charset>${charsetEncoding}</charset> </encoder> </appender> <appender name=“dailyRollingFileAppender” class=“ch.qos.logback.core.rolling.RollingFileAppender”> <File>./logback/log.log</File> <rollingPolicy class=“ch.qos.logback.core.rolling.TimeBasedRollingPolicy”> <!– daily rollover –> <FileNamePattern>logback.%d{yyyy-MM-dd_HH}.log</FileNamePattern> <!– keep 30 days’ worth of history –> <maxHistory>7</maxHistory> </rollingPolicy> <encoder> <Pattern>${pattern}</Pattern> </encoder> </appender> <logger name=“org.springframework.web” level=“debug”/> <!– show parameters for hibernate sql 专为 Hibernate 定制 –> <logger name=“org.hibernate.type.descriptor.sql.BasicBinder” level=“TRACE” /> <logger name=“org.hibernate.type.descriptor.sql.BasicExtractor” level=“DEBUG” /> <logger name=“org.hibernate.SQL” level=“DEBUG” /> <logger name=“org.hibernate.engine.QueryParameters” level=“DEBUG” /> <logger name=“org.hibernate.engine.query.HQLQueryPlan” level=“DEBUG” /> <!–myibatis log configure–> <logger name=“com.apache.ibatis” level=“TRACE”/> <logger name=“java.sql.Connection” level=“DEBUG”/> <logger name=“java.sql.Statement” level=“DEBUG”/> <logger name=“java.sql.PreparedStatement” level=“DEBUG”/> <root level=“debug”> <appender-ref ref=“console”/> <appender-ref ref=“dailyRollingFileAppender”/> <appender-ref ref=“file”/> </root></configuration> ...

December 27, 2018 · 3 min · jiezi

k8s与log--利用fluent bit收集k8s日志

前言收集日志的组件多不胜数,有ELK久负盛名组合中的logstash, 也有EFK组合中的filebeat,更有cncf新贵fluentd,另外还有大数据领域使用比较多的flume。本次主要说另外一种,和fluentd一脉相承的fluent bit。Fluent Bit是一个开源和多平台的Log Processor and Forwarder,它允许您从不同的来源收集数据/日志,统一并将它们发送到多个目的地。它与Docker和Kubernetes环境完全兼容。Fluent Bit用C语言编写,具有可插拔的架构,支持大约30个扩展。它快速轻便,通过TLS为网络运营提供所需的安全性。之所以选择fluent bit,看重了它的高性能。下面是官方贴出的一张与fluentd对比图: FluentdFluent BitScopeContainers / ServersContainers / ServersLanguageC & RubyCMemory40MB450KBPerformanceHigh PerformanceHigh PerformanceDependenciesBuilt as a Ruby Gem, it requires a certain number of gems.Zero dependencies, unless some special plugin requires them.PluginsMore than 650 plugins availableAround 35 plugins availableLicenseApache License v2.0Apache License v2.0在已经拥有的插件满足需求和场景的前提下,fluent bit无疑是一个很好的选择。fluent bit 简介在使用的这段时间之后,总结以下几点优点:支持routing,适合多output的场景。比如有些业务日志,或写入到es中,供查询。或写入到hdfs中,供大数据进行分析。fliter支持lua。对于那些对c语言hold不住的团队,可以用lua写自己的filter。output 除了官方已经支持的十几种,还支持用golang写output。例如:fluent-bit-kafka-output-plugink8s日志收集k8s日志分析主要讲kubeadm部署的k8s集群。日志主要有:kubelet和etcd的日志,一般采用systemd部署,自然而然就是要支持systemd格式日志的采集。filebeat并不支持该类型。kube-apiserver等组件stderr和stdout日志,这个一般输出的格式取决于docker的日志驱动,一般为json-file。业务落盘的日志。支持tail文件的采集组件都满足。这点不在今天的讨论范围之内。部署方案fluent bit 采取DaemonSet部署。 如下图:部署yaml—apiVersion: v1kind: Servicemetadata: name: elasticsearch-logging namespace: kube-system labels: k8s-app: elasticsearch-logging kubernetes.io/cluster-service: “true” addonmanager.kubernetes.io/mode: Reconcile kubernetes.io/name: “Elasticsearch"spec: ports: - port: 9200 protocol: TCP targetPort: db selector: k8s-app: elasticsearch-logging—# RBAC authn and authzapiVersion: v1kind: ServiceAccountmetadata: name: elasticsearch-logging namespace: kube-system labels: k8s-app: elasticsearch-logging kubernetes.io/cluster-service: “true” addonmanager.kubernetes.io/mode: Reconcile—kind: ClusterRoleapiVersion: rbac.authorization.k8s.io/v1metadata: name: elasticsearch-logging labels: k8s-app: elasticsearch-logging kubernetes.io/cluster-service: “true” addonmanager.kubernetes.io/mode: Reconcilerules:- apiGroups: - "” resources: - “services” - “namespaces” - “endpoints” verbs: - “get”—kind: ClusterRoleBindingapiVersion: rbac.authorization.k8s.io/v1metadata: namespace: kube-system name: elasticsearch-logging labels: k8s-app: elasticsearch-logging kubernetes.io/cluster-service: “true” addonmanager.kubernetes.io/mode: Reconcilesubjects:- kind: ServiceAccount name: elasticsearch-logging namespace: kube-system apiGroup: ““roleRef: kind: ClusterRole name: elasticsearch-logging apiGroup: “”—# Elasticsearch deployment itselfapiVersion: apps/v1kind: StatefulSetmetadata: name: elasticsearch-logging namespace: kube-system labels: k8s-app: elasticsearch-logging version: v6.3.0 kubernetes.io/cluster-service: “true” addonmanager.kubernetes.io/mode: Reconcilespec: serviceName: elasticsearch-logging replicas: 2 selector: matchLabels: k8s-app: elasticsearch-logging version: v6.3.0 template: metadata: labels: k8s-app: elasticsearch-logging version: v6.3.0 kubernetes.io/cluster-service: “true” spec: serviceAccountName: elasticsearch-logging containers: - image: k8s.gcr.io/elasticsearch:v6.3.0 name: elasticsearch-logging resources: # need more cpu upon initialization, therefore burstable class limits: cpu: 1000m requests: cpu: 100m ports: - containerPort: 9200 name: db protocol: TCP - containerPort: 9300 name: transport protocol: TCP volumeMounts: - name: elasticsearch-logging mountPath: /data env: - name: “NAMESPACE” valueFrom: fieldRef: fieldPath: metadata.namespace # Elasticsearch requires vm.max_map_count to be at least 262144. # If your OS already sets up this number to a higher value, feel free # to remove this init container. initContainers: - image: alpine:3.6 command: ["/sbin/sysctl”, “-w”, “vm.max_map_count=262144”] name: elasticsearch-logging-init securityContext: privileged: true volumeClaimTemplates: - metadata: name: elasticsearch-logging annotations: volume.beta.kubernetes.io/storage-class: gp2 spec: accessModes: - “ReadWriteOnce” resources: requests: storage: 10Gi—apiVersion: v1kind: ConfigMapmetadata: name: fluent-bit-config namespace: kube-system labels: k8s-app: fluent-bitdata: # Configuration files: server, input, filters and output # ====================================================== fluent-bit.conf: | [SERVICE] Flush 1 Log_Level info Daemon off Parsers_File parsers.conf HTTP_Server On HTTP_Listen 0.0.0.0 HTTP_Port 2020 @INCLUDE input-kubernetes.conf @INCLUDE filter-kubernetes.conf @INCLUDE output-elasticsearch.conf input-kubernetes.conf: | [INPUT] Name tail Tag kube.* Path /var/log/containers/.log Parser docker DB /var/log/flb_kube.db Mem_Buf_Limit 5MB Skip_Long_Lines On Refresh_Interval 10 [INPUT] Name systemd Tag host. Systemd_Filter SYSTEMD_UNIT=kubelet.service Path /var/log/journal DB /var/log/flb_host.db filter-kubernetes.conf: | [FILTER] Name kubernetes Match kube.* Kube_URL https://kubernetes.default.svc.cluster.local:443 Merge_Log On K8S-Logging.Parser On K8S-Logging.Exclude On [FILTER] Name kubernetes Match host.* Kube_URL https://kubernetes.default.svc.cluster.local:443 Merge_Log On Use_Journal On output-elasticsearch.conf: | [OUTPUT] Name es Match * Host ${FLUENT_ELASTICSEARCH_HOST} Port ${FLUENT_ELASTICSEARCH_PORT} Logstash_Format On Retry_Limit False parsers.conf: | [PARSER] Name apache Format regex Regex ^(?<host>[^ ]) [^ ] (?<user>[^ ]) [(?<time>[^]])] “(?<method>\S+)(?: +(?<path>[^"]?)(?: +\S)?)?” (?<code>[^ ]) (?<size>[^ ])(?: “(?<referer>[^"])” “(?<agent>[^"])”)?$ Time_Key time Time_Format %d/%b/%Y:%H:%M:%S %z [PARSER] Name apache2 Format regex Regex ^(?<host>[^ ]) [^ ] (?<user>[^ ]) [(?<time>[^]])] “(?<method>\S+)(?: +(?<path>[^ ]) +\S)?” (?<code>[^ ]) (?<size>[^ ])(?: “(?<referer>[^"])” “(?<agent>[^"])”)?$ Time_Key time Time_Format %d/%b/%Y:%H:%M:%S %z [PARSER] Name apache_error Format regex Regex ^[[^ ]* (?<time>[^]])] [(?<level>[^]])](?: [pid (?<pid>[^]])])?( [client (?<client>[^]])])? (?<message>.)$ [PARSER] Name nginx Format regex Regex ^(?<remote>[^ ]) (?<host>[^ ]) (?<user>[^ ]) [(?<time>[^]])] “(?<method>\S+)(?: +(?<path>[^"]?)(?: +\S*)?)?” (?<code>[^ ]) (?<size>[^ ])(?: “(?<referer>[^"])” “(?<agent>[^"])”)?$ Time_Key time Time_Format %d/%b/%Y:%H:%M:%S %z [PARSER] Name json Format json Time_Key time Time_Format %d/%b/%Y:%H:%M:%S %z [PARSER] Name docker Format json Time_Key time Time_Format %Y-%m-%dT%H:%M:%S.%L Time_Keep On # Command | Decoder | Field | Optional Action # =============|==================|================= Decode_Field_As escaped log [PARSER] Name syslog Format regex Regex ^&lt;(?<pri>[0-9]+)&gt;(?<time>[^ ]* {1,2}[^ ]* [^ ]) (?<host>[^ ]) (?<ident>[a-zA-Z0-9/.-])(?:[(?<pid>[0-9]+)])?(?:[^:]:)? (?<message>.)$ Time_Key time Time_Format %b %d %H:%M:%S—apiVersion: extensions/v1beta1kind: DaemonSetmetadata: name: fluent-bit namespace: kube-system labels: k8s-app: fluent-bit-logging version: v1 kubernetes.io/cluster-service: “true"spec: template: metadata: labels: k8s-app: fluent-bit-logging version: v1 kubernetes.io/cluster-service: “true” annotations: prometheus.io/scrape: “true” prometheus.io/port: “2020” prometheus.io/path: /api/v1/metrics/prometheus spec: containers: - name: fluent-bit image: fluent/fluent-bit:1.0.0 imagePullPolicy: Always ports: - containerPort: 2020 env: - name: FLUENT_ELASTICSEARCH_HOST value: “elasticsearch-logging” - name: FLUENT_ELASTICSEARCH_PORT value: “9200” volumeMounts: - name: varlog mountPath: /var/log - name: varlibdockercontainers mountPath: /data/docker/containers readOnly: true - name: fluent-bit-config mountPath: /fluent-bit/etc/ terminationGracePeriodSeconds: 10 volumes: - name: varlog hostPath: path: /var/log - name: varlibdockercontainers hostPath: path: /data/docker/containers - name: fluent-bit-config configMap: name: fluent-bit-config serviceAccountName: fluent-bit tolerations: - key: node-role.kubernetes.io/master operator: Exists effect: NoSchedule—apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: fluent-bit-readroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: fluent-bit-readsubjects:- kind: ServiceAccount name: fluent-bit namespace: kube-system—apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: fluent-bit-readrules:- apiGroups: [””] resources: - namespaces - pods verbs: [“get”, “list”, “watch”]—apiVersion: v1kind: ServiceAccountmetadata: name: fluent-bit namespace: kube-system—apiVersion: apps/v1kind: Deploymentmetadata: name: kibana-logging namespace: kube-system labels: k8s-app: kibana-logging kubernetes.io/cluster-service: “true” addonmanager.kubernetes.io/mode: Reconcilespec: replicas: 1 selector: matchLabels: k8s-app: kibana-logging template: metadata: labels: k8s-app: kibana-logging annotations: seccomp.security.alpha.kubernetes.io/pod: ‘docker/default’ spec: containers: - name: kibana-logging image: docker.elastic.co/kibana/kibana-oss:6.3.2 resources: # need more cpu upon initialization, therefore burstable class limits: cpu: 1000m requests: cpu: 100m env: - name: ELASTICSEARCH_URL value: http://elasticsearch-logging:9200 ports: - containerPort: 5601 name: ui protocol: TCP—apiVersion: v1kind: Servicemetadata: name: kibana-logging namespace: kube-system labels: k8s-app: kibana-logging kubernetes.io/cluster-service: “true” addonmanager.kubernetes.io/mode: Reconcile kubernetes.io/name: “Kibana” annotations: service.beta.kubernetes.io/aws-load-balancer-type: nlbspec: ports: - port: 5601 protocol: TCP targetPort: ui selector: k8s-app: kibana-logging type: LoadBalancer—总结真实场景的日志收集比较复杂,在日志量大的情况下,一般要引入kafka。此外关于注意日志的lograte。一般来说,docker是支持该功能的。可以通过下面的配置解决:cat > /etc/docker/daemon.json <<EOF{ “log-opts”: { “max-size”: “100m”, “max-file”: “3” }}EOF在k8s中运行的业务日志,不仅要考虑清除过时的日志,还要考虑新增pod的日志的收集。这个时候,往往需要在fluent bit上面再包一层逻辑,获取需要收集的日志路径。比如log-pilot。 ...

December 26, 2018 · 4 min · jiezi

KubeCon 2018 参会记录 —— FluentBit Deep Dive

在最近的上海和北美KubeCon大会上,来自于Treasure Data的Eduardo Silva(Fluentd Maintainer)带来了最期待的关于容器日志采集工具FluentBit的最新进展以及深入解析的分享;我们知道Fluentd是在2016年底正式加入CNCF,成为CNCF项目家族的一员,其被广泛用于容器集群中进行应用日志的采集、处理和聚合,但今天主要是跟大家分享一下同样来自于Treasure Data新开源的日志采集工具——FluentBit。FluentBit vs Fluentd既然已经有了Fluentd,那么为什么还要开发一个FluentBit呢?我们知道,Fluentd是基于Ruby语言的,在一些应用日志量较大或者单节点日志量较大的场景下,通过Fluentd采集日志的速率会远落后于应用日志的产生速率,进而导致日志采集的延迟时间较大,这对于一些实时性要求较高的业务系统或者监控系统来说是不可接受的;另外一方面,也是由于Fluentd自身的日志处理逻辑越来越复杂,全部放置在一个组件里来完成会导致越来越臃肿,因此Treasure Data在基于Fluentd优秀的架构和设计理念上重新开发了一个更加轻量级、更加高性能的日志采集工具——FluentBit,其主要采用C语言进行开发。从上面我们可以清晰地看到FluentBit本身占用的内存资源会比Fluentd少很多,且基本没有其他额外的环境依赖,但是支持的插件数相较于Fluentd会少很多,需要时间来慢慢丰富。FluentBit WorkflowFluentBit 内置了一个Service Engine,其每采集到一条日志时都会执行从Input到Output的整个Action Chain:- Input日志数据入口,FluentBit支持多种不同数据来源类型的Input Plugin,不仅能采集容器日志、内核日志、syslog、systemd日志,还支持通过TCP监听接收远程客户端的日志,同时还能够采集系统的CPU、内存和DISK的使用率情况以及本机Network流量日志。- Parser通过情况下我们的应用日志都是非结构化的,那么Parser主要是负责将采集到的非结构化日志解析成结构化的日志数据,一般为JSON格式;FluentBit 默认已经预置了下面几种Parser:JSON:按照JSON格式来进行日志数据解析;Regex:依据配置的正则表达式来进行日志数据解析;Apache:遵循Apache日志格式来进行解析;Nginx:遵循Nginx日志格式来进行解析;Docker:遵循Docker标准输出日志格式进行解析;Syslog rfc5424:按照syslog rfc5424规范格式进行日志解析;Syslog rfc3164:按照syslog rfc3164规范格式进行日志解析;- Filter在实际的生产应用中,我们通常需要对采集到的应用日志记录进行修改或者添加一些关键信息,这都可以Filter Plugin来完成;目前FluentBit也已预置了多种Filter插件:Grep:允许匹配或者过滤掉符合特定正则表达式的日志记录;Record Modifier:允许对日志数据进行修改或者添加新的KV数据,通过此可以方便我们对日志数据进行打标;Throttle:支持采用漏桶和滑动窗口算法进行日志采集速率控制;Kubernetes:自动提取容器或者POD相关信息并添加到日志数据中;Modify:基于设置的规则来对日志数据进行修改;Standard Output:允许将日志数据直接打印到标准输出;Lua:支持通过嵌入Lua Script来修改添加日志数据;- BufferFluentBit 内部本身提供了Buffer机制,会将采集到的日志数据暂存在Memory中直到该日志数据被成功路由转发到指定的目标存储后端。- Routing路由是FluentBit的一个核心功能,它允许我们配置不同的路由规则来将同一条日志数据记录转发到一个或多个不同的接收后端,其内部主要是基于每条日志数据的Tag来进行路由转发,同时支持正则匹配方式;如下面配置则表示希望将Tag满足正则表达式my_的日志直接打印到标准输出中:[INPUT] Name cpu Tag my_cpu[INPUT] Name mem Tag my_mem[OUTPUT] Name stdout Match my_- OutputOutput 主要是用来配置采集到的日志数据将要被转发到哪些日志存储服务中,目前已支持多种主流的存储服务,如ElasticSearch、NATS、InfluxDB、Kafka、Splunk、File、Console等,同样也支持将日志数据继续通过HTTP(S)协议将其传输到其他服务接口中;另外这里有一个比较特殊的Output就是Fluentd,可能大家会比较奇怪,其实在未来的日志架构模型中,FluentBit主要是在采集端专职负责日志的高性能采集,然后可以将采集到的日志在Fluentd中进行较复杂的聚合处理(同Filebeat和Logstash):Other FeaturesEvent Driven内置的Service Engine采用完全异步的事件驱动模型来进行日志的采集和分发。Configuration简单灵活的、高可读性的配置方式,FluentBit的Workflow模型可完全通过配置文件的方式清晰制定。Upstream Manager采用统一的日志上游服务的网络连接管理,包括Keepalive和IO Error处理。TLSv1.2 / Security对于安全敏感的日志数据,支持通过TLS加密通道进行日志传输。Upcoming FeaturesFilesystem buffering mode当前FluentBit只支持Memory的buffer方式,但考虑到内存的易失性,未来也将会支持基于Filesystem的buffer机制。Optional plugins as shared libraries未来会将一些已内置的但又不是必需的插件以共享链接库的方式来进行动态加载。Kubernetes Filter improvements未来会继续深度整合Kubernetes,通过API获取更多POD关键信息并自动添加到日志数据记录中。Summary这两次的KubeCon大会上Eduardo Silva对日志采集工具FluentBit都进行了深度的解析分享,不仅介绍了FluentBit的整个架构模型,而且还分享了未来的发展方向,从整个分享来看FluentBit会侧重在日志的高性能采集方面;而阿里云容器服务在2017年初开源的Log-Pilot:https://github.com/AliyunContainerService/log-pilot ,其不仅能够采集容器的标准输出日志,而且还能动态地发现采集容器内文件日志,同时支持简单高效的日志声明式配置、支持日志路由、日志数据打标以及多种日志采集插件,未来我们将进一步与社区紧密结合,整合FluentBit的高性能采集特性以及Log-Pilot的动态发现和声明式配置优势来进一步增强容器化应用日志的配置采集效率。本文作者:chenqz阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 24, 2018 · 1 min · jiezi

日志分析必备指令集【来自一段线上日志的查看的经历】

线上日志查看基础查看线上机器的一些信息和基础命令:du、df查看大小相关cat、zcat、less、tail、head查看文件内容grep、awk处理文件内容sort、uniq、wc统计scp文件传输du、df查看大小相关df 查看系统挂载磁盘大小df [选项]… [FILE]…文件-a, –all 包含所有的具有 0 Blocks 的文件系统文件–block-size={SIZE} 使用 {SIZE} 大小的 Blocks文件-h, –human-readable 使用人类可读的格式(预设值是不加这个选项的…)文件-H, –si 很像 -h, 但是用 1000 为单位而不是用 1024文件-i, –inodes 列出 inode 资讯,不列出已使用 block文件-k, –kilobytes 就像是 –block-size=1024文件-l, –local 限制列出的文件结构文件-m, –megabytes 就像 –block-size=1048576文件–no-sync 取得资讯前不 sync (预设值)文件-P, –portability 使用 POSIX 输出格式文件–sync 在取得资讯前 sync文件-t, –type=TYPE 限制列出文件系统的 TYPE文件-T, –print-type 显示文件系统的形式文件-x, –exclude-type=TYPE 限制列出文件系统不要显示 TYPE文件-v (忽略)常见使用实例:df -hdu会显示指定的目录或文件所占用的磁盘空间du [-abcDhHklmsSx][-L <符号连接>][-X <文件>][–block-size][–exclude=<目录或文件>][–max-depth=<目录层数>][–help][–version][目录或文件]参数说明:-a或-all 显示目录中个别文件的大小。-b或-bytes 显示目录或文件大小时,以byte为单位。-c或–total 除了显示个别目录或文件的大小外,同时也显示所有目录或文件的总和。-D或–dereference-args 显示指定符号连接的源文件大小。-h或–human-readable 以K,M,G为单位,提高信息的可读性。-H或–si 与-h参数相同,但是K,M,G是以1000为换算单位。-k或–kilobytes 以1024 bytes为单位。-l或–count-links 重复计算硬件连接的文件。-L<符号连接>或–dereference<符号连接> 显示选项中所指定符号连接的源文件大小。-m或–megabytes 以1MB为单位。-s或–summarize 仅显示总计。-S或–separate-dirs 显示个别目录的大小时,并不含其子目录的大小。-x或–one-file-xystem 以一开始处理时的文件系统为准,若遇上其它不同的文件系统目录则略过。-X<文件>或–exclude-from=<文件> 在<文件>指定目录或文件。–exclude=<目录或文件> 略过指定的目录或文件。–max-depth=<目录层数> 超过指定层数的目录后,予以忽略。使用实例:查看当前文件夹的一级内容大小du -h –max-depth=1cat、zcat、less、tail、head查看文件内容 cat 命令用于连接文件并打印到标准输出设备上cat [-AbeEnstTuv] [–help] [–version] fileName-n 或 --number:由 1 开始对所有输出的行数编号。-b 或 --number-nonblank:和 -n 相似,只不过对于空白行不编号。-s 或 --squeeze-blank:当遇到有连续两行以上的空白行,就代换为一行的空白行。-v 或 --show-nonprinting:使用 ^ 和 M- 符号,除了 LFD 和 TAB 之外。-E 或 --show-ends : 在每行结束处显示 $。-T 或 --show-tabs: 将 TAB 字符显示为 ^I。-A, --show-all:等价于 -vET。-e:等价于"-vE"选项;-t:等价于"-vT"选项;zcatt命令用于不真正解压缩文件,就能显示压缩包中文件的内容的场合。head显示档案的开头至标准输出中,默认head命令打印其相应文件的开头10行head [参数]… [文件]… -q 隐藏文件名-v 显示文件名-c&lt;字节&gt; 显示字节数-n&lt;行数&gt; 显示的行数tail用于查看文件的内容,有一个常用的参数 -f 常用于查阅正在改变的日志文件tail[必要参数][选择参数][文件] -f 循环读取-q 不显示处理信息-v 显示详细的处理信息-c&lt;数目&gt; 显示的字节数-n&lt;行数&gt; 显示行数--pid=PID 与-f合用,表示在进程ID,PID死掉之后结束. -q, --quiet, --silent 从不输出给出文件名的首部 -s, --sleep-interval=S 与-f合用,表示在每次反复的间隔休眠S秒 less可以随意浏览文件less [参数] 文件-b &lt;缓冲区大小&gt; 设置缓冲区的大小-e 当文件显示结束后,自动离开-f 强迫打开特殊文件,例如外围设备代号、目录和二进制文件-g 只标志最后搜索的关键词-i 忽略搜索时的大小写-m 显示类似more命令的百分比-N 显示每行的行号-o &lt;文件名&gt; 将less 输出的内容在指定文件中保存起来-Q 不使用警告音-s 显示连续空行为一行-S 行过长时间将超出部分舍弃-x &lt;数字&gt; 将“tab”键显示为规定的数字空格/字符串:向下搜索“字符串”的功能?字符串:向上搜索“字符串”的功能n:重复前一个搜索(与 / 或 ? 有关)N:反向重复前一个搜索(与 / 或 ? 有关)b 向后翻一页d 向后翻半页h 显示帮助界面Q 退出less 命令u 向前滚动半页y 向前滚动一行空格键 滚动一行回车键 滚动一页[pagedown]: 向下翻动一页[pageup]: 向上翻动一页grep、awk处理文件内容grep用于查找文件里符合条件的字符串。grep [-abcEFGhHilLnqrsvVwxy][-A<显示列数>][-B<显示列数>][-C<显示列数>][-d<进行动作>][-e<范本样式>][-f<范本文件>][–help][范本样式][文件或目录…]-a 或 –text : 不要忽略二进制的数据。-A<显示行数> 或 –after-context=<显示行数> : 除了显示符合范本样式的那一列之外,并显示该行之后的内容。-b 或 –byte-offset : 在显示符合样式的那一行之前,标示出该行第一个字符的编号。-B<显示行数> 或 –before-context=<显示行数> : 除了显示符合样式的那一行之外,并显示该行之前的内容。-c 或 –count : 计算符合样式的列数。-C<显示行数> 或 –context=<显示行数>或-<显示行数> : 除了显示符合样式的那一行之外,并显示该行之前后的内容。-d <动作> 或 –directories=<动作> : 当指定要查找的是目录而非文件时,必须使用这项参数,否则grep指令将回报信息并停止动作。-e<范本样式> 或 –regexp=<范本样式> : 指定字符串做为查找文件内容的样式。-E 或 –extended-regexp : 将样式为延伸的普通表示法来使用。-f<规则文件> 或 –file=<规则文件> : 指定规则文件,其内容含有一个或多个规则样式,让grep查找符合规则条件的文件内容,格式为每行一个规则样式。-F 或 –fixed-regexp : 将样式视为固定字符串的列表。-G 或 –basic-regexp : 将样式视为普通的表示法来使用。-h 或 –no-filename : 在显示符合样式的那一行之前,不标示该行所属的文件名称。-H 或 –with-filename : 在显示符合样式的那一行之前,表示该行所属的文件名称。-i 或 –ignore-case : 忽略字符大小写的差别。-l 或 –file-with-matches : 列出文件内容符合指定的样式的文件名称。-L 或 –files-without-match : 列出文件内容不符合指定的样式的文件名称。-n 或 –line-number : 在显示符合样式的那一行之前,标示出该行的列数编号。-q 或 –quiet或–silent : 不显示任何信息。-r 或 –recursive : 此参数的效果和指定"-d recurse"参数相同。-s 或 –no-messages : 不显示错误信息。-v 或 –revert-match : 显示不包含匹配文本的所有行。-V 或 –version : 显示版本信息。-w 或 –word-regexp : 只显示全字符合的列。-x –line-regexp : 只显示全列符合的列。-y : 此参数的效果和指定"-i"参数相同。awk处理文本文件的语言,是一个强大的文本分析工具。awk ‘{pattern + action}’ {filenames}或者awk [-F field-separator] ‘commands’ input-file(s)awk -F, ‘$2 ~ /test/ {print $2"\t"$4}’ log.txt使用-F,的作用是每行按照,分割,$1-$2-$n就是分割的结果的对应顺序的值$2 ~ /th/就是需要分割的第二个数据需要和test匹配的上{print $2$4}输出分割结果的第二个和第四个处理的信息是log.txtawk -F ‘[:=]’ ‘{match($5,/.uc_name=(.)&extend=test./,a); print a[1]}’ log.txt-F ‘[:=]‘使用多个分隔符,先使用:分割,然后在对分割结果使用=二次分割。’{match($5,/.uc_name=(.)&extend=test./,a); print a[1]}‘对分割后的第五个结果处理,需要能够匹配上其中的正则,并把匹配结果放到数组a中,其中a数组的结果a[0]是全匹配的结果,a[1]是正则匹配的子表达式结果,并输出子表达式。sort、uniq、wc统计sort用于将文本文件内容加以排序。sort [-bcdfimMnr][-o<输出文件>][-t<分隔字符>][+<起始栏位>-<结束栏位>][–help][–verison][文件]-b 忽略每行前面开始出的空格字符。-c 检查文件是否已经按照顺序排序。-d 排序时,处理英文字母、数字及空格字符外,忽略其他的字符。-f 排序时,将小写字母视为大写字母。-i 排序时,除了040至176之间的ASCII字符外,忽略其他的字符。-m 将几个排序好的文件进行合并。-M 将前面3个字母依照月份的缩写进行排序。-n 依照数值的大小排序。-o<输出文件> 将排序后的结果存入指定的文件。-r 以相反的顺序来排序。-t<分隔字符> 指定排序时所用的栏位分隔字符。+<起始栏位>-<结束栏位> 以指定的栏位来排序,范围由起始栏位到结束栏位的前一栏位。uniq用于检查及删除文本文件中重复出现的行列,需要与 sort 命令结合使用[一定要排序,不然去重无效,只会去除相邻的重复项]uniq [-cdu][-f<栏位>][-s<字符位置>][-w<字符位置>][–help][–version][输入文件][输出文件]-c或–count 在每列旁边显示该行重复出现的次数。-d或–repeated 仅显示重复出现的行列。-f<栏位>或–skip-fields=<栏位> 忽略比较指定的栏位。-s<字符位置>或–skip-chars=<字符位置> 忽略比较指定的字符。-u或–unique 仅显示出一次的行列。-w<字符位置>或–check-chars=<字符位置> 指定要比较的字符。wc计算文件的Byte数、字数、或是列数,若不指定文件名称、或是所给予的文件名为"-",则wc指令会从标准输入设备读取数据。uniq [-cdu][-f<栏位>][-s<字符位置>][-w<字符位置>][–help][–version][输入文件][输出文件]-c或–bytes或–chars 只显示Bytes数。-l或–lines 只显示行数。-w或–words 只显示字数。scp文件传输scp是linux系统下基于ssh登陆进行安全的远程文件拷贝命令[需要配置ssh登入,密码或者公钥免密]scp [-1246BCpqrv] [-c cipher] [-F ssh_config] [-i identity_file] [-l limit] [-o ssh_option] [-P port] [-S program] [[user@]host1:]file1 […] [[user@]host2:]file2-1: 强制scp命令使用协议ssh1-2: 强制scp命令使用协议ssh2-4: 强制scp命令只使用IPv4寻址-6: 强制scp命令只使用IPv6寻址-B: 使用批处理模式(传输过程中不询问传输口令或短语)-C: 允许压缩。(将-C标志传递给ssh,从而打开压缩功能)-p:保留原文件的修改时间,访问时间和访问权限。-q: 不显示传输进度条。-r: 递归复制整个目录。-v:详细方式显示输出。scp和ssh(1)会显示出整个过程的调试信息。这些信息用于调试连接,验证和配置问题。-c cipher: 以cipher将数据传输进行加密,这个选项将直接传递给ssh。-F ssh_config: 指定一个替代的ssh配置文件,此参数直接传递给ssh。-i identity_file: 从指定文件中读取传输时使用的密钥文件,此参数直接传递给ssh。-l limit: 限定用户所能使用的带宽,以Kbit/s为单位。-o ssh_option: 如果习惯于使用ssh_config(5)中的参数传递方式,-P port:注意是大写的P, port是指定数据传输用到的端口号-S program: 指定加密传输时所使用的程序。此程序必须能够理解ssh(1)的选项。实践线上日志结构:183.250.223.158 [16/Dec/2018:23:57:15 +0800] “GET /log?skdata=sdadadad111%22username%33%3ftestets%22fsfdsfssadadasd%34fdsfs%34 HTTP/1.0” 200 2 “-” “testiPhone/d64556” “120.188.90.136” “jsdgajdsad” “dasdadd” “test” “ceshi"分析数据命令:cat /home/logs/2018/12/access.2018-12-16.log | grep -E ‘skdata.*prodetail.*etype%2522%253A0.*eid.theme_prodetail.vs_theme’ | awk ‘{match($5,/.uc_name%2522%253A%2522(.)%2522%252C%2522etype./,a); print a[1]}’ | sort | uniq -c | wc -lcat查看某一天的日志文件,访问日志一般文件一天记录一份grep查看符合需求的访问日志,本实例使用正则匹配-E来筛选出符合要求的详情页访问日志,输出awk分析符合要求的详情页访问日志,把每行详情页访问日志中$5【空格分割的第五个字符串,本实例就是以/log?开头的字符串】去匹配一个正则.uc_name%2522%253A%2522(.)%2522%252C%2522etype.,并把结果放到变量a中【a[0]是全匹配,a[1]是第一个字表达式】,然后把匹配成功符合结果的a[1]输出sort将上述结果排序 [一定要排序,不然去重无效,只会去除相邻的重复项]uniq -c将上述结果去重wc -l查看总共结果数量 ...

December 18, 2018 · 2 min · jiezi

被忽略的console.log

除了console.log之外,还有更多方式调试JavaScript来输出值。 看起来很明显我们没有。人们告诉我,做JavaScript应该使用浏览器的调试器,但这肯定是要看运行环境的。 但是很多时候你只想知道代码的某一部分是执行还是变量是什么,而不会看着断点消失庞大的代码类库中。尽管如此,虽然我们使用console.log,但是很多人都没有意识到控制台本身除了基本日志之外还有很多其他选项。 适当使用这些功能可以使调试更容易,更快速,更直观。console.log()在旧的console.log中有超出人期望令人惊讶的功能。 虽然大多数人将它用作console.log(obj),但您也可以执行console.log(object,otherObject,string),它会将它们全部记录下来。 有时候方便。除此之外,还有另一种格式:console.log(msg,values)。 这很像像C或PHP中的sprintf。console.log(‘I like %s but I do not like %s.’, ‘Skittles’, ‘pus’);将完全按照您的预期输出。> I like Skittles but I do not like pus.常见的占位符是%o(这是一个字母o,而不是零),它接受一个对象,%s接受一个字符串,%d是一个十进制或整数。另一个有趣的是%c, 它实际上是CSS值的占位符。console.log(‘I am a %cbutton’, ‘color: white; background-color: orange; padding: 2px 5px; border-radius: 2px’);这些值会运行到后面的任何内容上,没有“结束标记”,这有点奇怪。 但你可以将它变得像这样。它不优雅,也不是特别有用。 当然,这不是一个真正的按钮。它有用吗?Ehhhhh。console.dir()在大多数情况下,console.dir()函数非常类似于log(),尽管它看起来有点不同。向下的小箭头将显示与上面相同的确切对象详细信息,这也可以从console.log版本中看到。 当你看到元素时,事物的分歧更加剧烈,更有趣。let element = document.getElementById(‘2x-container’);这是记录输入的输出:我打开了一些元素。 这清楚地显示了DOM,我们可以浏览它。 但是console.dir(element)为我们提供了惊人的不同输出。这是一种更加客观的方式来查看元素。 有时候这就是你真正想要的东西,更像是检查元素。console.warn()可能是最明显的直接替换log(),你可以用完全相同的方式使用console.warn()。 唯一真正的区别是输出有点黄。 具体来说,输出处于警告级别而不是信息级别,因此浏览器将稍微区别对待它。 这具有使其在杂乱输出中更明显的效果。但是,有一个更大的优势。 因为输出是警告而不是信息,所以您可以过滤掉所有console.log并仅保留console.warn。 这对于偶尔会在浏览器中输出大量无用废话的偶尔繁琐的应用程序尤其有用。 清除噪音可以让您更轻松地看到输出。console.table()令人惊讶的是,这并不是更为人所知,但是console.table()函数旨在以比抛出原始对象数组更简洁的方式显示表格数据。例如,这是一个数据列表。const transactions = [{ id: “7cb1-e041b126-f3b8”, seller: “WAL0412”, buyer: “WAL3023”, price: 203450, time: 1539688433},{ id: “1d4c-31f8f14b-1571”, seller: “WAL0452”, buyer: “WAL3023”, price: 348299, time: 1539688433},{ id: “b12c-b3adf58f-809f”, seller: “WAL0012”, buyer: “WAL2025”, price: 59240, time: 1539688433}];如果我们使用console.log来转储上面的内容,我们会得到一些非常无用的输出:▶ (3) [{…}, {…}, {…}]小箭头让你点击并打开阵列,当然,但这并不是我们想要的“一目了然”。虽然console.tabl(data)的输出更有帮助。可选的第二个参数是您想要的列的列表。 显然默认为所有列,但我们也可以这样做。> console.table(data, [“id”, “price”]);我们得到这个输出,只显示id和价格。 适用于过大的物体,细节基本无关。 索引列是自动创建的,并且据我所知不可以去掉。这里要注意的是这是乱序的 - 最右边的列标题上的箭头显示了原因。 我点击该列进行排序。 找到列的最大或最小,或者只是对数据进行不同的查看非常方便。 顺便说一句,该功能与显示部分列无关。 它始终可用。console.table()只能处理最多1000行,因此可能不适合所有数据集。console.assert()断言有用的函数assert() 与log() 相同,但仅在第一个参数为false的情况下。 如果第一个参数为真,它什么都不做。这对于有循环(或几个不同的函数调用)并且只有一个显示特定行为的情况特别有用。 基本上它和这样做是一样的。if (object.whatever === ‘value’) { console.log(object);}澄清的是,当我说“相同”时,做起来却是相反的。 所以你需要反转条件。因此,让我们假设上面的一个值是在时间戳中使用null或0,这会搞砸我们的代码格式化日期。console.assert(tx.timestamp, tx);当与任何有效的事务对象一起使用时,它只是跳过去。 但是false会触发我们的日志记录,因为时间戳是0或null。有时我们想要更复杂的条件。 例如,我们已经看到用户WAL0412的数据存在问题,并且只想显示来自它们的事务。 这是直观的解决方案。console.assert(tx.buyer === ‘WAL0412’, tx);这看起来正确,但不起作用。 请记住,条件必须是false…我们要断言,而不是过滤。console.assert(tx.buyer !== ‘WAL0412’, tx);这将做我们想要的。 买方不是WAL0412的任何交易在该条件下都是正确的,只留下那些。 或者……不是。像其中的一些,console.assert()并不总是特别有用。 但在特定情况下它可以是一个优雅的解决方案。console.count()另一个使用有用的功能,count只是作为一个计数器,可选择作为一个命名计数器。for(let i = 0; i < 10000; i++) { if(i % 2) { console.count(‘odds’); } if(!(i % 5)) { console.count(‘multiplesOfFive’); } if(isPrime(i)) { console.count(‘prime’); }}这不是有用的代码,有点抽象。 此外,我不打算演示isPrime函数,这是个伪代码。我们得到的应该基本上是一个列表odds: 1odds: 2prime: 1odds: 3multiplesOfFive: 1prime: 2odds: 4prime: 3odds: 5multiplesOfFive: 2…等等。 这对于您可能只是转储索引的情况很有用,或者您希望保留一个(或多个)运行计数。您也可以像这样使用console.count(),不带参数。 这样做会将其称为默认值。如果您愿意,还可以使用相关的console.countReset()来重置计数器。console.trace()这很难用简单的数据进行演示。 它擅长的地方在于你试图弄清楚实际调用者导致问题的类或库。例如,可能有12个不同的组件调用服务,但其中一个组件没有正确设置依赖关系。export default class CupcakeService { constructor(dataLib) { this.dataLib = dataLib; if(typeof dataLib !== ‘object’) { console.log(dataLib); console.trace(); } } …}在这里单独使用console.log()会告诉我们传入的dataLib是什么,而不是在哪里。 但是,堆栈跟踪将非常清楚地告诉我们问题是Dashboard.js,我们可以看到它是新的CupcakeService(false)并导致错误。console.time()用于跟踪操作所用时间的专用函数console.time()是跟踪JavaScript执行所用微时间的更好方法。function slowFunction(number) { var functionTimerStart = new Date().getTime(); // something slow or complex with the numbers. // Factorials, or whatever. var functionTime = new Date().getTime() - functionTimerStart; console.log(Function time: ${ functionTime });}var start = new Date().getTime();for (i = 0; i < 100000; ++i) { slowFunction(i);}var time = new Date().getTime() - start;console.log(Execution time: ${ time });这是一种老式的方法。 我还应该指向上面的console.log。 很多人都没有意识到你可以在那里使用模板字符串和插值,但你可以。 很有帮助。所以让我们使用新方法试试。const slowFunction = number => { console.time(‘slowFunction’); // something slow or complex with the numbers. // Factorials, or whatever. console.timeEnd(‘slowFunction’);}console.time();for (i = 0; i < 100000; ++i) { slowFunction(i);}console.timeEnd();我们现在不再需要做任何数学运算或设置临时变量。console.group()现在可能是控制台输出中最复杂和最先进的区域。 group让你……好吧,分组。 特别是它可以让你嵌套东西。 它擅长的地方在于显示代码中存在的结构。// this is the global scopelet number = 1;console.group(‘OutsideLoop’);console.log(number);console.group(‘Loop’);for (let i = 0; i < 5; i++) { number = i + number; console.log(number);}console.groupEnd();console.log(number);console.groupEnd();console.log(‘All done now’);这又是一种循环。 你可以在这里看到输出。虽然不是很有用,但你可能会看到其中一些是如何组合在一起的。class MyClass { constructor(dataAccess) { console.group(‘Constructor’); console.log(‘Constructor executed’); console.assert(typeof dataAccess === ‘object’, ‘Potentially incorrect dataAccess object’); this.initializeEvents(); console.groupEnd(); } initializeEvents() { console.group(’events’); console.log(‘Initialising events’); console.groupEnd(); }}let myClass = new MyClass(false);这是很多工作和很多用于调试信息的代码,可能不是那么有用。 但它仍然是一个有趣的想法,您可以看到它可以使您的日志记录更加清晰。最后要指出的是console.groupCollapsed。 在功能上,这与console.group相同,但块开始关闭。 它没有得到很好的支持,但如果你有一大堆废话,你可能想要默认隐藏它是一个选项。结论这里没有太多结论。 所有这些工具都可能有用,如果你可能只需要一点点console.log(pet),但实际上并不需要调试器。可能最有用的是console.table,但所有其他api也都有自己的作用。 我是console.assert的粉丝,因为我们想调试一些东西,但只能在特定情况下调试。 ...

December 11, 2018 · 2 min · jiezi

面向容器日志的技术实践

摘要: 本文以 Docker 为例,依托阿里云日志服务团队在日志领域深耕多年积累下的丰富经验,介绍容器日志处理的一般方法和最佳实践。背景自 2013 年 dotCloud 公司开源 Docker 以来,以 Docker 为代表的容器产品凭借着隔离性好、可移植性高、资源占用少、启动迅速等特性迅速风靡世界。下图展示了 2013 年以来 Docker 和 OpenStack 的搜索趋势。容器技术在部署、交付等环节给人们带来了很多便捷,但在日志处理领域却带来了许多新的挑战,包括:如果把日志保存在容器内部,它会随着容器的销毁而被删除。由于容器的生命周期相对虚拟机大大缩短,创建销毁属于常态,因此需要一种方式持久化的保存日志;进入容器时代后,需要管理的目标对象远多于虚拟机或物理机,登录到目标容器排查问题会变得更加复杂且不经济;容器的出现让微服务更容易落地,它在给我们的系统带来松耦合的同时引入了更多的组件。因此我们需要一种技术,它既能帮助我们全局性的了解系统运行情况,又能迅速定位问题现场、还原上下文。日志处理流程本文以 Docker 为例,依托阿里云日志服务团队在日志领域深耕多年积累下的丰富经验,介绍容器日志处理的一般方法和最佳实践,包括:容器日志实时采集;查询分析和可视化;日志上下文分析;LiveTail - 云上 tail -f。容器日志实时采集容器日志分类采集日志首先要弄清日志存在的位置,这里以 Nginx、Tomcat 这两个常用容器为例进行分析。Nginx 产生的日志包括 access.log 和 error.log,根据 nginx Dockerfile 可知 access.log 和 error.log 被分别重定向到了 STDOUT 和 STDERR 上。Tomcat 产生的日志比较多,包括 catalina.log、access.log、manager.log、host-manager.log 等,tomcat Dockerfile 并没有将这些日志重定向到标准输出,它们存在于容器内部。容器产生的日志大部分都可以归结于上述情形。这里,我们不妨将容器日志分成以下两类。容器日志分类定义标准输出通过 STDOUT、STDERR 输出的信息,包括被重定向到标准输出的文本文件。文本日志存在于容器内部并且没有被重定向到标准输出的日志。标准输出使用 logging driver容器的标准输出会由 logging driver 统一处理。如下图所示,不同的 logging driver 会将标准输出写往不同的目的地。通过 logging driver 采集容器标准输出的优势在于使用简单,例如:# 该命令表示在 docker daemon 级别为所有容器配置 syslog 日志驱动dockerd -–log-driver syslog –-log-opt syslog-address=udp://1.2.3.4:1111# 该命令表示为当前容器配置 syslog 日志驱动docker run -–log-driver syslog –-log-opt syslog-address=udp://1.2.3.4:1111 alpine echo hello world缺点除了 json-file 和 journald,使用其他 logging driver 将使 docker logs API 不可用。例如,当您使用 portainer 管理宿主机上的容器,并且使用了上述两者之外的 logging driver,您会发现无法通过 UI 界面观察到容器的标准输出。使用 docker logs API对于那些使用默认 logging driver 的容器,我们可以通过向 docker daemon 发送 docker logs 命令来获取容器的标准输出。使用此方式采集日志的工具包括 logspout、sematext-agent-docker 等。下列样例中的命令表示获取容器自2018-01-01T15:00:00以来最新的5条日志。docker logs –since “2018-01-01T15:00:00” –tail 5 <container-id>缺点当日志量较大时,这种方式会对 docker daemon 造成较大压力,导致 docker daemon 无法及时响应创建容器、销毁容器等命令。采集 json-file 文件默认 logging driver 会将日志以 json 的格式写入宿主机文件里,文件路径为/var/lib/docker/containers/<container-id>/<container-id>-json.log。这样可以通过直接采集宿主机文件来达到采集容器标准输出的目的。该方案较为推荐,因为它既不会使 docker logs API 变得不可用,又不会影响 docker daemon,并且现在许多工具原生支持采集宿主机文件,如 filebeat、logtail 等。文本日志挂载宿主机目录采集容器内文本日志最简单的方法是在启动容器时通过 bind mounts 或 volumes 方式将宿主机目录挂载到容器日志所在目录上,如下图所示。针对 tomcat 容器的 access log,使用命令docker run -it -v /tmp/app/vol1:/usr/local/tomcat/logs tomcat将宿主机目录/tmp/app/vol1挂载到 access log 在容器中的目录/usr/local/tomcat/logs上,通过采集宿主机目录/tmp/app/vol1下日志达到采集 tomcat access log 的目的。计算容器 rootfs 挂载点使用挂载宿主机目录的方式采集日志对应用会有一定的侵入性,因为它要求容器启动的时候包含挂载命令。如果采集过程能对用户透明那就太棒了。事实上,可以通过计算容器 rootfs 挂载点来达到这种目的。和容器 rootfs 挂载点密不可分的一个概念是 storage driver。实际使用过程中,用户往往会根据 linux 版本、文件系统类型、容器读写情况等因素选择合适的 storage driver。不同 storage driver 下,容器的 rootfs 挂载点遵循一定规律,因此我们可以根据 storage driver 的类型推断出容器的 rootfs 挂载点,进而采集容器内部日志。下表展示了部分 storage dirver 的 rootfs 挂载点及其计算方法。Logtail 方案在充分比较了容器日志的各种采集方法,综合整理了广大用户的反馈与诉求后,日志服务团队推出了容器日志一站式解决方案。功能特点logtail 方案包含如下功能:支持采集宿主机文件以及宿主机上容器的日志(包括标准输出和日志文件);支持容器自动发现,即当您配置了采集目标后,每当有符合条件的容器被创建时,该容器上的目标日志将被自动采集;支持通过 docker label 以及环境变量过滤指定容器,支持白名单、黑名单机制;采集数据自动打标,即对收集上来的日志自动加上 container name、container IP、文件路径等用于标识数据源的信息;支持采集 K8s 容器日志。核心优势通过 checkpoint 机制以及部署额外的监控进程保证 at-least-once 语义;历经多次双十一、双十二的考验以及阿里集团内部百万级别的部署规模,稳定和性能方面非常有保障。K8s 容器日志采集和 K8s 生态深度集成,能非常方便地采集 K8s 容器日志是日志服务 logtail 方案的又一大特色。采集配置管理:支持通过 WEB 控制台进行采集配置管理;支持通过 CRD(CustomResourceDefinition)方式进行采集配置管理(该方式更容易与 K8s 的部署、发布流程进行集成)。采集模式:支持通过 DaemonSet 模式采集 K8s 容器日志,即每个节点上运行一个采集客户端 logtail,适用于功能单一型的集群;支持通过 Sidecar 模式采集 K8s 容器日志,即每个 Pod 里以容器的形式运行一个采集客户端 logtail,适用于大型、混合型、PAAS 型集群。关于 Logtail 方案的详细说明可参考文章全面提升,阿里云Docker/Kubernetes(K8S) 日志解决方案与选型对比。查询分析和可视化完成日志采集工作后,下一步需要对这些日志进行查询分析和可视化。这里以 Tomcat 访问日志为例,介绍日志服务提供的强大的查询、分析、可视化功能。快速查询容器日志被采集时会带上 container name、container IP、目标文件路径等信息,因此在查询的时候可以通过这些信息快速定位目标容器和文件。查询功能的详细介绍可参考文档查询语法。实时分析日志服务实时分析功能兼容 SQL 语法且提供了 200 多种聚合函数。如果您有使用 SQL 的经验,能够很容易写出满足业务需求的分析语句。例如:统计访问次数排名前 10 的 uri。* | SELECT request_uri, COUNT() as c GROUP by request_uri ORDER by c DESC LIMIT 10统计当前15分钟的网络流量相对于前一个小时的变化情况。 | SELECT diff[1] AS c1, diff[2] AS c2, round(diff[1] * 100.0 / diff[2] - 100.0, 2) AS c3 FROM (select compare( flow, 3600) AS diff from (select sum(body_bytes_sent) as flow from log))该语句使用同比环比函数计算不同时间段的网络流量。可视化为了让数据更加生动,您可以使用日志服务内置的多种图表对 SQL 计算结果进行可视化展示,并将图表组合成一个仪表盘。下图展示了基于 Tomcat 访问日志的仪表盘,它展示了错误请求率、网络流量、状态码随时间的变化趋势等信息。该仪表盘展现的是多个 Tomcat 容器数据聚合后的结果,您可以使用仪表盘过滤器功能,通过指定容器名查看单个容器的数据。日志上下文分析查询分析、仪表盘等功能能帮助我们把握全局信息、了解系统整体运行情况,但定位具体问题往往需要上下文信息的帮助。上下文定义上下文指的是围绕某个问题展开的线索,如日志中某个错误的前后信息。上下文包含两个要素:最小区分粒度:区分上下文的最小空间划分,例如同一个线程、同一个文件等。这一点在定位问题阶段非常关键,因为它能够使得我们在调查过程中避免很多干扰。保序:在最小区分粒度的前提下,信息的呈现必须是严格有序的,即使一秒内有几万次操作。下表展示了不同数据源的最小区分粒度。分类最小区分粒度单机文件IP + 文件Docker 标准输出Container + STDOUT/STDERRDocker 文件Container + 文件K8s 容器标准输出Namespace + Pod + Container + STDOUT/STDERRK8s 容器文件Namespace + Pod + Container + 文件SDK线程Log Appender线程上下文查询面临的挑战在日志集中式存储的背景下,采集端和服务端都很难保证日志原始的顺序:在客户端层面,一台宿主机上运行着多个容器,每个容器会有多个目标文件需要采集。日志采集软件需要利用机器的多个 cpu 核心解析、预处理日志,并通过多线程并发或者单线程异步回调的方式处理网络发送的慢 IO 问题。这使得日志数据不能按照机器上的事件产生顺序依次到达服务端。在服务端层面,由于水平扩展的多机负载均衡架构,使得同一客户端机器的日志会分散在多台存储节点上。在分散存储的日志基础上再恢复最初的顺序是困难的。原理日志服务通过给每条日志附加一些额外的信息以及服务端的关键词查询能力巧妙地解决了上述难题。原理如下图所示。日志被采集时会自动加入用于标识日志来源的信息(即上文提到的最小区分粒度)作为 source_id。针对容器场景,这些信息包括容器名、文件路径等;日志服务的各种采集客户端一般会选择批量上传日志,若干条日志组成一个数据包。客户端会向这些数据包里写入一个单调递增的 package_id,并且包内每条日志都拥有包内位移 offset;服务端会将 source_id、package_id、offset 组合起来作为一个字段并为其建立索引。这样,即使各种日志在服务端是混合存储的状态,我们也可以根据 source_id、package_id、offset 精确定位某条日志。想了解更多有关上下文分析的功能可参考文章上下文查询、分布式系统日志上下文查询功能。LiveTail - 云上 tail -f除了查看日志的上下文信息,有时我们也希望能够持续观察容器的输出。传统方式下表展示了传统模式下实时监控容器日志的方法。类别步骤标准输出1. 定位容器,获取容器 id;2. 使用命令docker logs –f <container id>或kubectl logs –f <pod name>在终端上观察输出;3. 使用grep或grep –v过滤关键信息。 || 文本日志 | 1. 定位容器,获取容器 id;2. 使用命令docker exec或kubectl exec进入容器;3. 找到目标文件,使用命令tail –f观察输出;4. 使用grep或grep –v过滤关键信息。 |痛点通过传统方法监控容器日志存在以下痛点:容器很多时,定位目标容器耗时耗力;不同类型的容器日志需要使用不同的观察方法,增加使用成本;关键信息查询展示不够简单直观。功能和原理针对这些问题,日志服务推出了 LiveTail 功能。相比传统模式,它有如下优点:可以根据单条日志或日志服务的查询分析功能快速定位目标容器;使用统一的方式观察不同类型的容器日志,无需进入目标容器;支持通过关键词进行过滤;支持设置关键列。在实现上,LiveTail 主要用到了上一章中提到的上下文查询原理快速定位目标容器和目标文件。然后,客户端定期向服务端发送请求,拉取最新数据。视频样例您还可以通过观看视频,进一步理解容器日志的采集、查询、分析和可视化等功能。参考资料LC3视角:Kubernetes下日志采集、存储与处理技术实践 - https://yq.aliyun.com/articles/606248全面提升,阿里云Docker/Kubernetes(K8S) 日志解决方案与选型对比 - https://yq.aliyun.com/articles/448676本文作者:吴波bruce_wu阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 21, 2018 · 2 min · jiezi