能力说明:
了解变量作用域、Java类的结构,能够创建带main方法可执行的java应用,从命令行运行java程序;能够使用Java基本数据类型、运算符和控制结构、数组、循环结构书写和运行简单的Java程序。
暂时未有相关云产品技术能力~
@TOC一、API爆炸的时代随着最近行业的移动化、物联网化、数字化转型、微服务等多种概念的提出,对应的API数量已经呈现出爆炸式增长,由此带来的问题就是前后端的接口对接问题越来越来突出,我们能很难找到一个合适的技术工具提高我们的效率。由此带来的问题就是接口对接的繁琐,前端后端日常吵架。1.背景介绍现在我们其实有很多的API工具,在API文档设计有大名鼎鼎的Swagger,API开发调试我们有Postman、前端开发用的比较多的式Mock.js、自动化测试我们拥有JMeter,但是由于是多个软件,我们需要多次的输入相同的重叠数据到不同的系统才能实现我们需要的功能,而且在项目发生变更的时候我们就不得不进行多个地方的修改,一不留神忘记修改就是boom。2.问题引出所以为了应对上面的需求,我们需要的就是将这几个常用软件可以融合到一起,如果能够做到数据完全互通,当我们修改一个地方的时候所有地方都进行修改那就太完美了。3.解决方案今天在网上冲浪的时候发现了Apifox这款神器,官方宣传就是将多种行业的巨头软件进行了整合为一个统一的程序,通过一套系统、一份数据,解决多个系统之间的数据同步问题。只要定义好接口文档,接口调试、数据 Mock、接口测试就可以直接使用,无需再次定义;接口文档和接口开发调试使用同一个工具,接口调试完成后即可保证和接口文档定义完全一致。高效、及时、准确!官网地址软件现在已经支持web版了,所以整体的体验都是在web上进行的。二、核心功能1.API文档在API文档部分,不在是往日冷冰冰的文档,而是完全可视化、这无疑降低了我们的学习成本、并且文档是遵循 OpenAPI 规范的,也能提高我们文档的规范性。2.API调试在接口调试部分,我们一个接口可以创建多个用力并且自动跟随接口进行变更,并且Postman用的功能,Apifox都拥有,可以进行环境变量、全局变量、前后置脚本、全局共享等等功能,可谓是全面。并且支持运行任何语言代码:js、java、py、php等。3.Mock 数据Apifox完全支持 Mock.js 语法、并且扩展身份证、国内手机号等常用规则,可以根据接口定义里的数据结构、数据类型,自动生成 mock 规则。并且内置智能 mock 规则库,根据字段名、字段数据类型,智能优化自动生成的 mock 规则。可自动识别出图片、头像、用户名、手机号、网址、日期、时间、时间戳、邮箱、省份、城市、地址、IP 等字段,从而 Mock 出非常人性化的数据。支持自定义规则库,满足各种个性化需求。支持使用 正则表达式、通配符 来匹配字段名自定义 mock 规则。4.自动化测试支持对相关的测试用例步骤和对应的数据配置完成后进行自动化测试、我们可以很方便的对代码进行自动化测试。5. 在线调试这个文档是用 Apifox 做的,我之前有试用过这个工具,没想到最近又有这么多厉害的新功能出来了。点击文档右上角的运行按钮,就会出现“在线运行”的模块这个界面上就能直接调试接口了!直接 1. 填参数,2. 选环境,3. 点发送,接口请求就发出去了!下面就有返回结果!根本用不着 Postman!更不用把 API 照着抄一遍!我心想,如果当时上线之前,用的是 Apifox 的话,那简直是不会出现事故:参数不存在?我在线调试后获得数据了,通过比对我知道哪个参数不存在参数类型错误?同样的,在线调试之后,通过比对,我知道哪个参数的类型是错的接口不存在(是因为接口写错了)?调试的时候就报接口不存在了,第一时间找后端~三、其他功能1.代码生成这个就很离谱,可以直接生成对应的业务代码,解放双手从此成为ctrl + c ctrl + v程序员,可以根据接口/模型定义,自动生成各种语言/框架的业务、模型代码。并且支持 TypeScript、Java、Go、Swift、ObjectiveC、Kotlin、Dart、C++、C#、Rust 等 130 种语言及框架。有点科幻。2.数据导入/导出支持导出 OpenAPI (Swagger)、Markdown、Html 等数据格式。支持导入 OpenAPI (Swagger)、Postman、HAR、RAP2、JMeter、YApi、Eolinker、RAML、DOClever 、Apizza 、DOCWAY、ShowDoc、I/O Docs、WADL、Google Discovery 等数据格式。这样就可以方便我们进行数据的迁移。而且我们也可以即时备份存档,从此不为写文档而头秃。四、惊喜功能作为一个coder,最终的就是进行分享,Apifox官网的API Hub可以让我们方便的查看别人的项目进行学习,同时如果我们做了一份自认为完美的文档也可以进行分享,分享才能使我们更加的强大。可以增强我们的输出能力。五、总结整体体验下来,只能说Apifox想的非常全面,可以让我们从文档书写和接口对接工作中解脱出来,更加专注于代码的书写和业务逻辑的梳理,被接口对接烦透了的你不妨尝试一下,你会发现它像一个保姆一样为你做了所有该做的事情。下载体验一下吧:www.apifox.cn
前言 一、概述2022年5月25日,微博认证为“搜狐公司董事局主席兼CEO张朝阳”的“搜狐charles”用户发布信息称,因搜狐员工内部邮箱被盗,进而受到网络钓鱼攻击,并表示目前技术部门及时介入,损失较小。综合当前信息进行溯源追踪,经微步在线情报局确认,这是Ganb黑产组织(微步在线内部命名)发起的又一次“网络钓鱼”攻击。微步在线情报局早在去年就捕获并持续追踪一批灰黑产组织自2021年末至今以医疗保障金领取,公积金补贴等名义,通过大量群发钓鱼邮件和短信进行钓鱼诈骗。在2022年3月份左右,Ganb黑产组织攻击愈发猖獗,对金融行业展开大规模钓鱼攻击,微步情报局已及时对其活动进行通报。微步情报局对其具体分析如下:该黑产组织针对多个行业生成多种对应话术模板,通过邮件和短信大量群发进行广撒网钓鱼,使用的钓鱼话术以“医疗保障金领取”,”工资补贴”为主,其最终目的为盗取受害者的银行卡,手机号,银行卡密码,身份证号等信息并对其进行诈骗。关联发现,该黑产组织最早于2021年12月活跃至今,日渐猖獗。受害者涉及较多。该黑产组织关系模式属于“一人开发,分销多人”,即上游系统供应商负责开发出相应管理平台框架,开发完成后对其下游销售系统账号的使用权限,涉及多人。使用的资产具有较强的反侦察意识,相关域名、管理后台站点均隐藏信息,并使用全流量DNS解析服务进行分发流量,最终导向黑客组织拥有的香港亚马逊服务器该黑产组织使用DGA技术生成大量域名做跳板,保证其网站存活性同时具有迷惑作用,同时使用若干域名做调度,最终指向该组织的真实资产。该黑产组织使用的资产及跳板域名无明显特征,资产选用没有明显规律且资产变化部署极为迅速,便于在域名遭到封禁时快速转换资产绑定域名,保证其相关资产持续可访问。 二、事件分析微步情报局监测发现多起黑产组织针对国内手机用户的邮件和短信诈骗行为,其目的为收集受害者身份证、银行卡号、手机号等多种隐私信息并对其进行诈骗。其详细诈骗手法分析如下: 2.1 邮件投递首先该黑产组织群发邮件至受害者邮箱或群发短信至受害者手机(以邮件发送为主),邮件正文谎称”财务部发放工资补贴,扫码即可领取”,以及“领取医疗保险金”等来吸引受害者兴趣,该组织利用DGA域名生成技术,生成了大量用于做为跳板的DGA域名,将其制成二维码。受害者通过手机扫描二维码来解析到对应的钓鱼页面。 2.2 实施诈骗当用手机扫描二维码后,进入对应钓鱼页面,值得一提的是,在跳转的过程中,会通过获取请求流量中的特征(UserAgent字段和屏幕分辨率等信息)从而分辨受害者的手机系统类别(安卓,苹果)。检测到访问设备为电脑时会提示“请使用手机访问”,此页面主要作用为诱导受害者填写银行卡,姓名,手机号,身份证号等详细信息。当受害者如实填写信息提交后,该钓鱼页面会进行弹框提示,通过后台实时自定义的提示弹框提示对受害者进行下一步诈骗。如“CVV错误,请重新输入有效期和CVV”,“请输入网银密码”等,通过后台实时人工针对不同情况对受害者进行精准诈骗。 三、资产分析 3.1 资产特点1. 收集确认到大量的该黑产组织后相关资产及关联资产后,发现以下特征:钓鱼邮件中二维码扫描后解析出的域名一般为自动生成的无规律域名(俗称DGA域名),生成算法未知,但从注册域名长度一般为4-6位的随机数字或者字母组合,配合run、xyz、pro、nuo等免费顶级域名组合使用,如下图所示:2. 为管理生成的大量DGA域名并保证域名解析到指定的诈骗界面,该组织使用配置cname指向特定调度域名来对DGA域名进行分类调度。DGA域名被访问后,首先会解析到配置cname指向的某一site*.ganb.run域名,再由site*.ganb.run域名指向对应的解析ip,开始业务通信。原理如下图所示:3.根据其服务基本配置信息可以发现以下特征:a)用site*.ganb.run的公网域名作为CNAME调度域名,公网域名如下:i.site01.ganb.runii.site02.ganb.runiii.site03.ganb.runiv.site04.ganb.runv.site05.ganb.runvi.site06.ganb.runvii.site07.ganb.runb)实际的域名访问方式,DNS解析如下图所示,钓鱼域名cname到site.ganb.run,随后由site.ganb.run调度解析到实际解析ip。c)目前收集到*.ganb.run最终解析到的ip共有12个,包含国内外的各种云主机。地址如下:viii.103.123.161.205ix.167.172.61.83x.91.89.236.15xi.163.21.236.11xii.47.57.3.168xiii.45.129.11.106xiv.13.71.136.247xv.119.28.66.157xvi. 27.124.17.20xvii.103.158.190.187xviii. 47.242.105.202xix.103.118.40.161 3.2 溯源结果1.通过一定溯源分析手段,进入了该团伙后台总控地址,获取到网站详细信息。如下图所示:通过分析发现,该平台共有8个用户账号,对应使用不同的钓鱼手法及模板进行钓鱼。也因此推测该开发者该黑产组织关系模式属于“一人开发,分销多人”,即上游系统供应商负责开发出相应管理平台框架,开发完成后对其下游销售系统账号的使用权限。且根据账号密码特征分析,该系统开发者自身也使用了其中两个账号参与到了此次钓鱼攻击中。2.通过使用账号密码登陆对应后台,在后台界面发现大量用户信息,粗略统计,总体受害者规模已达数千人,且后台提交受害者数据仍在不断增加。在其后台设置发现,其后台系统设置中,内置了一些钓鱼页面字段填写开关,例如姓名开关,银行卡号开关等。旨在指定手机受害者相关信息。同时通过功能判断,其内置了共计8套钓鱼模板,且可自定义跳转的弹窗文字。具体模板如下:1.ETC模板(已有在野利用)2.新-ETC模板(已有在野利用)3.社保模板(已有在野利用)4.医保模板(已有在野利用)5.某团模板6.工商模板7.某政模板8.某东-某政模板9.某鱼模板(已有在野利用)部分钓鱼邮件话术模板如下:部分钓鱼网站模板如下:建议企业根据以上信息内部自查是否有收到涉及相关主题、正文的邮件及短信,并及时对员工通报预警,提醒员工不要相信此类话术及网站。3.通过其总控后台的域名管理发现,该黑产组织目前手中掌握有大量的DGA跳板域名,共计831个,用以快速变换域名绑定部署,对抗域名封禁。同时通过其域名添加时间记录发现,相关域名最早添加于2021年12月26日,证明该组织至少于2021年12月左右就已经开始登场活跃。4.通过以上多种数据维度分析,判断该组织为一个典型的黑产组织,以公积金,医疗保障金等生活相关的话术进行大批量撒网钓鱼,引导受害者进入其部署的诈骗网站。目前已有大量受害者。同时区别于以往黑产的自动化钓鱼方式,该组织使用了“人工值守”方式提高钓鱼成功率,即后台人员实时根据受害者填写提交的信息进行弹框提示,精准引导、诈骗受害者。网络钓鱼攻击通常都基于社会工程学,利用了员工的心理漏洞,绕过了企业的被动防御技术/措施,从而导致“中招”,这是网络钓鱼攻击屡屡得手最关键的因素之一,这就形成了目前的尴尬局面,防范网络钓鱼大多靠员工自觉,需要有“火眼金睛”去甄别。被动防御难以奏效,这就要求我么从一个新的角度,通过新的方法去主动防御。比如微步在线的OneDNS,通过DNS与威胁情报相结合,在点击链接或“扫码”跳转到“钓鱼网站”时,针对域名进行甄别,一旦发现是网络钓鱼等恶意域名时,就停止解析并返回拦截页面,提示访问有风险。图注:OneDNS拦截页面,OneDNS在域名解析时,会与威胁情报库比对,一旦发现是恶意域名,就会停止解析,并返回拦截页面如果您也担心或正在为网络/邮件钓鱼烦恼,那么不妨试试OneDNS。OneDNS申请试用地址:https://page.ma.scrmtech.com/landing-page/index?pf_uid=15831_1728&id=11278&channel=28881
现代Java应用架构越来越强调数据存储和处理分离,以获得更好的可维护性、可扩展性以及可移植性,比如火热的微服务就是一种典型。这种架构通常要求业务逻辑要在Java程序中实现,而不是像传统应用架构中放在数据库中。应用中的业务逻辑大都会涉及结构化数据处理。数据库(SQL)中对这类任务有较丰富的支持,可以相对简易地实现业务逻辑。但Java却一直缺乏这类基础支持,导致用Java实现业务逻辑非常繁琐低效。结果,虽然架构上有各种优势,但开发效率却反而大幅下降了。如果我们在Java中也提供有一套完整的结构化数据处理和计算类库,那这个问题就能得到解决:即享受到架构的优势,又不致于降低开发效率。需要什么样的能力?Java下理想的结构化数据处理类库应当具备哪些特征呢?我们可以从SQL来总结:1 集合运算能力结构化数据经常是批量(以集合形式)出现的,为了方便地计算这类数据,有必要提供足够的集合运算能力。如果没有集合运算类库,只有数组(相当于集合)这种基础数据类型,我们要对集合成员做个简单地求和也需要写四五行循环语句才能完成,过滤、分组聚合等运算则要写出数百行代码了。SQL提供有较丰富的集合运算,如 SUM/COUNT 等聚合运算,WHERE 用于过滤、GROUP 用于分组,也支持针对集合的交、并、差等基本运算。这样写出来的代码就会短小很多。2 Lambda语法有了集合运算能力是否就够了呢?假如我们为 Java 开发一批的集合运算类库,是否就可以达到 SQL 的效果呢?没有这么简单!以过滤运算为例。过滤通常需要一个条件,把满足条件的集合成员保留。在 SQL 中这个条件是以一个表达式形式出现的,比如写 WHERE x>0,就表示保留那些使得 x>0 计算结果为真的成员。这个表达式 x>0 并不是在执行这个语句之前先计算好的,而是在遍历时针对每个集合成员计算的。本质上,这个表达式本质上是一个函数,是一个以当前集合成员为参数的函数。对于 WHERE 运算而言,相当于把一个用表达式定义的函数用作了 WHERE 的参数。这种写法有一个术语叫做 Lambda 语法,或者叫函数式语言。如果没有 Lambda 语法,我们就要经常临时定义函数,代码会非常繁琐,还容易发生名字冲突。SQL中大量使用了 Lambda 语法,不在于必须过滤、分组运算中,在计算列等不必须的场景也可以使用,大大简化了代码。3 在 Lambda 语法中直接引用字段结构化数据并非简单的单值,而是带有字段的记录。我们发现,SQL 的表达式参数中引用记录字段时,大多数情况可以直接使用字段名称而不必指明字段所属的记录,只有在多个同名字段时才需要冠以表名(或别名)以区分。新版本的 Java 虽然也开始支持 Lambda 语法了,但只能把当前记录作为参数传入这个用 Lambda 语法定义的函数,然后再写计算式时就总要带上这个记录。比如用单价和数量计算金额时,如果用于表示当前成员的参数名为 x,则需要写成“x. 单价 x. 数量”这种啰嗦的形式。而在 SQL 中可以更为直观地写成 " 单价 数量”。4 动态数据结构SQL还能很好地支持动态数据结构。结构化数据计算中,返回值经常也是有结构的数据,而结果数据结构和运算相关,没办法在代码编写之前就先准备好。所以需要支持动态的数据结构能力。SQL中任何一个 SELECT 语句都会产生一个新的数据结构,在代码中可以随意添加删除字段,而不必事先定义结构(类)。Java 这类语言则不行,在代码编译阶段就要把用到的结构(类)都定义好,原则上不能在执行过程中动态产生新的结构。5 解释型语言从前面几条的分析,我们已经可以得到结论:Java 本身并不适合用作结构化数据处理的语言。它的 Lambda 机制不支持特征 3,而且作为编译型语言,也不能实现特征 4。其实,前面说到的 Lambda 语法也不太适合采用编译型语言来实现。编译器不能确定这个写到参数位置的表达式是应该当场计算出表达式的值再传递,还是把整个表达式编译成一个函数传递,需要再设计更多的语法符号加以区分。而解释型语言则没有这个问题,作为参数的表达式是先计算还是遍历集合成员时再计算,可以由函数本身来决定。SQL确实是解释型语言。引入 SPLStream是Java8以官方身份推出的结构化数据处理类库,但并不符合上述的要求。它没有专业的结构化数据类型,缺乏很多重要的结构化数据计算函数,不是解释型语言,不支持动态数据类型,Lambda语法的接口复杂。Kotlin属于Java生态系统的一部分,它在Stream的基础上进行了小幅改进,也提供了结构化数据计算类型,但因为结构化数据计算函数不足,不是解释型语言,不支持动态数据类型,Lambda语法的接口复杂,仍然不是理想的结构化数据计算类库。Scala提供了较丰富的结构化数据计算函数,但编译型语言的特点,也使它不能成为理想的结构化数据计算类库。那么,Java生态下还有什么可以用呢?集算器SPL。SPL是由Java解释执行的程序语言,具备丰富的结构化数据计算类库、简单的Lambda语法和方便易用的动态数据结构,是Java下理想的结构化处理类库。丰富的集合运算函数SPL提供了专业的结构化数据类型,即序表。和SQL的数据表一样,序表是批量记录组成的集合,具有结构化数据类型的一般功能,下面举例说明。解析源数据并生成序表:Orders=T("d:/Orders.csv")按列名从原序表生成新的序表:Orders.new(OrderID, Amount, OrderDate)计算列:Orders.new(OrderID, Amount, year(OrderDate))字段改名:Orders.new(OrderID:ID, SellerId, year(OrderDate):y)按序号使用字段:Orders.groups(year(_5),_2; sum(_4))序表改名(左关联)join@1(Orders:o,SellerId ; Employees:e,EId).groups(e.Dept; sum(o.Amount))序表支持所有的结构化计算函数,计算结果也同样是序表,而不是Map之类的数据类型。比如对分组汇总的结果,继续进行结构化数据处理:Orders.groups(year(OrderDate):y; sum(Amount):m).new(y:OrderYear, m*0.2:discount)在序表的基础上,SPL提供了丰富的结构化数据计算函数,比如过滤、排序、分组、去重、改名、计算列、关联、子查询、集合计算、有序计算等。这些函数具有强大的计算能力,无须硬编码辅助,就能独立完成计算:组合查询:Orders.select(Amount>1000 && Amount<=3000 && like(Client,"*bro*"))排序:Orders.sort(-Client,Amount)分组汇总:Orders.groups(year(OrderDate),Client; sum(Amount))内关联:join(Orders:o,SellerId ; Employees:e,EId).groups(e.Dept; sum(o.Amount))简洁的Lambda语法SPL支持简单的Lambda语法,无须定义函数名和函数体,可以直接用表达式当作函数的参数,比如过滤:Orders.select(Amount>1000)修改业务逻辑时,也不用重构函数,只须简单修改表达式:Orders.select(Amount>1000 && Amount<2000)SPL是解释型语言,使用参数表达式时不必明确定义参数类型,使Lambda接口更简单。比如计算平方和,想在sum的过程中算平方,可以直观写作:Orders.sum(Amount*Amount)和SQL类似,SPL语法也支持在单表计算时直接使用字段名:Orders.sort(-Client, Amount)动态数据结构SPL是解释型语言,天然支持动态数据结构,可以根据计算结果结构动态生成新序表。特别适合计算列、分组汇总、关联这类计算,比如直接对分组汇总的结果再计算:Orders.groups(Client;sum(Amount):amt).select(amt>1000 && like(Client,"*S*"))或直接对关联计算的结果再计算:join(Orders:o,SellerId ; Employees:e,Eid).groups(e.Dept; sum(o.Amount))较复杂的计算通常都要拆成多个步骤,每个中间结果的数据结构几乎都不同。SPL支持动态数据结构,不必先定义这些中间结果的结构。比如,根据某年的客户回款记录表,计算每个月的回款额都在前10名的客户:Sales2021.group(month(sellDate)).(~.groups(Client;sum(Amount):sumValue)).(~.sort(-sumValue)) .(~.select(#<=10)).(~.(Client)).isect()直接执行SQLSPL中还实现了SQL的解释器,可以直接执行SQL,从基本的WHERE、GROUP到JOIN、甚至WITH都能支持:$select * from d:/Orders.csv where (OrderDate<date('2020-01-01') and Amount<=100)or (OrderDate>=date('2020-12-31') and Amount>100)$select year(OrderDate),Client ,sum(Amount),count(1) from d:/Orders.csv group by year(OrderDate),Client having sum(Amount)<=100$select o.OrderId,o.Client,e.Name e.Dept from d:/Orders.csv o join d:/Employees.csv e on o.SellerId=e.Eid$with t as (select Client ,sum(amount) s from d:/Orders.csv group by Client) select t.Client, t.s, ct.Name, ct.address from t left join ClientTable ct on t.Client=ct.Client更多语言优势作为专业的结构化数据处理语言,SPL不仅覆盖了SQL的所有计算能力,在语言方面,还有更强大的优势:离散性及其支挂下的更彻底的集合化集合化是SQL的基本特性,即支持数据以集合的形式参与运算。但SQL的离散性很不好,所有集合成员必须作为一个整体参于运算,不能游离在集合之外。而Java等高级语言则支持很好的离散性,数组成员可以单独运算。但是,更彻底的集合化需要离散性来支持,集合成员可以游离在集合之外,并与其它数据随意构成新的集合参与运算 。SPL兼具了SQL的集合化和Java的离散性,从而可以实现更彻底的集合化。比如,SPL中很容易表达“集合的集合”,适合分组后计算。比如,找到各科成绩均在前10名的学生: A1=T("score.csv").group(subject)2=A2.(~.rank(score).pselect@a(~<=10))3=A1.(~(A3(#)).(name)).isect()SPL序表的字段可以存储记录或记录集合,这样可以用对象引用的方式,直观地表达关联关系,即使关系再多,也能直观地表达。比如,根据员工表找到女经理下属的男员工:Employees.select(性别:"男",部门.经理.性别:"女")有序计算是离散性和集合化的典型结合产物,成员的次序在集合中才有意义,这要求集合化,有序计算时又要将每个成员与相邻成员区分开,会强调离散性。SPL兼具集合化和离散性,天然支持有序计算。具体来说,SPL可以按绝对位置引用成员,比如,取第3条订单可以写成Orders(3),取第1、3、5条记录可以写成Orders([1,3,5])。SPL也可以按相对位置引用成员,比如,计算每条记录相对于上一条记录的金额增长率:Orders.derive(amount/amount[-1]-1)SPL还可以用#代表当前记录的序号,比如把员工按序号分成两组,奇数序号一组,偶数序号一组:Employees.group(#%2==1)更方便的函数语法大量功能强大的结构化数据计算函数,这本来是一件好事,但这会让相似功能的函数不容易区分。无形中提高了学习难度。SPL提供了特有的函数选项语法,功能相似的函数可以共用一个函数名,只用函数选项区分差别。比如select函数的基本功能是过滤,如果只过滤出符合条件的第1条记录,只须使用选项@1:Orders.select@1(Amount>1000)数据量较大时,用并行计算提高性能,只须改为选项@m:Orders.select@m(Amount>1000)对排序过的数据,用二分法进行快速过滤,可用@b:Orders.select@b(Amount>1000)函数选项还可以组合搭配,比如:Orders.select@1b(Amount>1000)结构化运算函数的参数常常很复杂,比如SQL就需要用各种关键字把一条语句的参数分隔成多个组,但这会动用很多关键字,也使语句结构不统一。SPL支持层次参数,通过分号、逗号、冒号自高而低将参数分为三层,用通用的方式简化复杂参数的表达:join(Orders:o,SellerId ; Employees:e,EId)扩展的Lambda语法普通的Lambda语法不仅要指明表达式(即函数形式的参数),还必须完整地定义表达式本身的参数,否则在数学形式上不够严密,这就让Lambda语法很繁琐。比如用循环函数select过滤集合A,只保留值为偶数的成员,一般形式是:A.select(f(x):{x%2==0} )这里的表达式是x%2==0,表达式的参数是f(x)里的x,x代表集合A里的成员,即循环变量。SPL用固定符号\~代表循环变量,当参数是循环变量时就无须再定义参数了。在SPL中,上面的Lambda语法可以简写作:A.select(~ %2==0)普通Lambda语法必须定义表达式用到的每一个参数,除了循环变量外,常用的参数还有循环计数,如果把循环计数也定义到Lambda中,代码就更繁琐了。SPL用固定符号#代表循环计数变量。比如,用函数select过滤集合A,只保留序号是偶数的成员,SPL可以写作:A.select(# %2==0)相对位置经常出现在难度较大的计算中,而且相对位置本身就很难计算,当要使用相对位置时,参数的写法将非常繁琐。SPL用固定形式[序号]代表相对位置: AB1=T("Orders.txt")/订单序表2=A1.groups(year(Date):y,month(Date):m; sum(Amount):amt)/按年月分组汇总3=A2.derive(amt/amt[-1]:lrr, amt[-1:1].avg():ma)/计算比上期和移动平均无缝集成、低耦合、热切换作为用Java解释的脚本语言,SPL提供了JDBC驱动,可以无缝集成进Java应用程中。简单语句可以像SQL一样直接执行:… Class.forName("com.esproc.jdbc.InternalDriver"); Connection conn =DriverManager.getConnection("jdbc:esproc:local://"); PrepareStatement st = conn.prepareStatement("=T(\"D:/Orders.txt\").select(Amount>1000 && Amount<=3000 && like(Client,\"*S*\"))"); ResultSet result=st.execute(); ...复杂计算可以存成脚本文件,以存储过程方式调用… Class.forName("com.esproc.jdbc.InternalDriver"); Connection conn =DriverManager.getConnection("jdbc:esproc:local://"); Statement st = connection.(); CallableStatement st = conn.prepareCall("{call splscript1(?, ?)}"); st.setObject(1, 3000); st.setObject(2, 5000); ResultSet result=st.execute(); ...将脚本外置于Java程序,一方面可以降低代码耦合性,另一方面利用解释执行的特点还可以支持热切换,业务逻辑变动时只要修改脚本即可立即生效,不像使用Java时常常要重启整个应用。这种机制特别适合编写微服务架构中的业务处理逻辑。SPL资料SPL官网SPL下载SPL源代码
《Kafka运维管控平台LogiKM》✏️更强大的管控能力✏️ 更高效的问题定位能力 更便捷的集群运维能力 更专业的资源治理 更友好的运维生态 地址:Kafka中生产消息时的三种分区分配策略KafkaProducer在发送消息的时候,需要指定发送到哪个分区, 那么这个分区策略都有哪些呢?我们今天来看一下使用分区策略的配置:属性描述默认值partitioner.class消息的分区分配策略org.apache.kafka.clients.producer.internals.DefaultPartitioner1. DefaultPartitioner 默认分区策略全路径类名:org.apache.kafka.clients.producer.internals.DefaultPartitioner如果消息中指定了分区,则使用它如果未指定分区但存在key,则根据序列化key使用murmur2哈希算法对分区数取模。如果不存在分区或key,则会使用粘性分区策略,关于粘性分区请参阅 KIP-480。粘性分区Sticky Partitioner为什么会有粘性分区的概念?首先,我们指定,Producer在发送消息的时候,会将消息放到一个ProducerBatch中, 这个Batch可能包含多条消息,然后再将Batch打包发送。关于这一块可以看看我之前的文章 图解Kafka Producer 消息缓存模型这样做的好处就是能够提高吞吐量,减少发起请求的次数。但是有一个问题就是, 因为消息的发送它必须要你的一个Batch满了或者linger.ms时间到了,才会发送。如果生产的消息比较少的话,迟迟难以让Batch塞满,那么就意味着更高的延迟。在之前的消息发送中,就将消息轮询到各个分区的, 本来消息就少,你还给所有分区遍历的分配,那么每个ProducerBatch都很难满足条件。那么假如我先让一个ProducerBatch塞满了之后,再给其他的分区分配是不是可以降低这个延迟呢?详细的可以看看下面这张图、这张图的前提是:Topic1 有3分区, 此时给Topic1 发9条无key的消息, 这9条消息加起来都不超过batch.size . 那么以前的分配方式和粘性分区的分配方式如下可以看到,使用粘性分区之后,至少是先把一个Batch填满了发送然后再去填充另一个Batch。不至于向之前那样,虽然平均分配了,但是导致一个Batch都没有放满,不能立即发送。这不就增大了延迟了吗(只能通过linger.ms时间到了才发送)划重点:当一个Batch发送之后,需要选择一个新的粘性分区的时候①. 可用分区<1 ;那么选择分区的逻辑是在所有分区中随机选择。②. 可用分区=1; 那么直接选择这个分区。③. 可用分区>1 ; 那么在所有可用分区中随机选择。当选择下一个粘性分区的时候,不是按照分区平均的原则来分配。而是随机原则(当然不能跟上一次的分区相同)例如刚刚发送到的Batch是 1号分区,等Batch满了,发送之后,新的消息可能会发到2或者3, 如果选择的是2,等2的Batch满了之后,下一次选择的Batch仍旧可能是1,而不是说为了平均,选择3分区。2.UniformStickyPartitioner 纯粹的粘性分区策略全路径类名:org.apache.kafka.clients.producer.internals.UniformStickyPartitioner他跟DefaultPartitioner 分区策略的唯一区别就是。DefaultPartitionerd 如果有key的话,那么它是按照key来决定分区的,这个时候并不会使用粘性分区UniformStickyPartitioner 是不管你有没有key, 统一都用粘性分区来分配。3. RoundRobinPartitioner 分区策略全路径类名:org.apache.kafka.clients.producer.internals.RoundRobinPartitioner如果消息中指定了分区,则使用它将消息平均的分配到每个分区中。与key无关 @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { List<PartitionInfo> partitions = cluster.partitionsForTopic(topic); int numPartitions = partitions.size(); int nextValue = nextValue(topic); List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic); if (!availablePartitions.isEmpty()) { int part = Utils.toPositive(nextValue) % availablePartitions.size(); return availablePartitions.get(part).partition(); } else { // no partitions are available, give a non-available partition return Utils.toPositive(nextValue) % numPartitions; } } 上面是具体代码。有个地方需要注意;当可用分区是0的话,那么就是遍历的是所有分区中的。当有可用分区的话,那么遍历的是所有可用分区的。
《Kafka运维管控平台LogiKM》✏️更强大的管控能力✏️ 更高效的问题定位能力 更便捷的集群运维能力 更专业的资源治理 更友好的运维生态 参数详解listeners侦听器列表,这里配置的监听器底层调用的是 ServerSocketAdaptor.bind(SocketAddress local) 那么这个说明什么意思呢?说明你配置的监听器将被用于监听网络请求。简单理解就是你建立监听一个通道,别人能够通过这个通道跟你沟通。所以我们需要设置 IP:Port. 更好的阅读体验Kafka如何进行内外网分流(超详细建议收藏)这个属性的格式为: listeners = listener_name://host_name:port,listener_name2://host_nam2e:port2可以同时配置多个, 并且用逗号隔开监听器的名称和端口必须是唯一的,端口相同,就冲突了host_name如果为空,例如(listeners = ://host_name:port),则会绑定到默认的接口(网卡),一般情况下是localhost,底层调用的是java.net.InetAddress.getCanonicalHostName()将host_name设置为0.0.0.0 则会绑定所有的网卡, 也就是说不管从哪个网卡进入的请求都会被接受处理。但是请注意,假如你设置的是0.0.0.0,那么advertised.listeners 必须要设置,因为advertised.listeners默认请看下使用的是listeners的配置发布到zk中,发布到zk中是给其他Brokers/Clients 来跟你通信的,你设置0.0.0.0,谁知道要请求哪个IP呢, 所以它必须要指定并明确 IP:PORT。具体详情请看下面 示例3listener_name 是监听名,唯一值, 他并不是安全协议(大部分人都会搞错),因为默认的4个安全协议已经做好了映射, 例如 :PLAINTEXT ==> PLAINTEXT . 所以你经常看到的配置## 这个PLAINTEXT是监听名称,刚好他对应的安全协议就是 PLAINTEXT ## 当然这个是可以自定义的, 详细情况 后面的配置listener.security.protocol.map listeners = PLAINTEXT://your.host.name:9092 可动态配置该属性advertised.listeners发布公开的监听器, 啥叫发布公开的监听器?就是,让Brokers和Clients们都能够知道的监听器,你想想看,listeners是Broker用来监听网络请求的那么,其他Broker或者客户端想要与它通信,则需要知道具体的IP:PORT吧?所以,为了让别人知道自己的监听器,那么就需要公开出去,当然这个公开的形式,是通过zk来共享数据。看看broker到zk节点/brokers/{brokerid}/ 下面的信息示例{ "features": {}, "listener_security_protocol_map": { "PLAINTEXT": "PLAINTEXT" }, "endpoints": ["PLAINTEXT://localhost:9092"], "jmx_port": -1, "port": 9092, "host": "localhost", "version": 5, "timestamp": "1647337490945" }其中endpoints就是我们发布出去的监听器。这个属性的格式为: advertised.listeners = listener_name://host_name:port,listener_name2://host_nam2e:port2 默认情况下,advertised.listeners不设置会自动使用listeners属性advertised.listeners不支持0.0.0.0这种形式, 所以如果listeners属性设置成0.0.0.0,则必须设置advertised.listeners属性。具体请看 示例3因为0.0.0.0是表示的是监听Broker上任意的网卡的, 你将这个发布出去,那么别的Broker和客户端怎么知道你具体的ip和端口呢?可以同时配置多个, 并且用逗号隔开可动态配置该属性listener.security.protocol.map监听器名称和安全协议之间的映射关系集合。listeners=PLAINTEXT://localhost:9092看看上面的配置, PLANINTEXT是监听器名称,那么它对应的安全协议是什么呢?它对应的安全协议是 PLANINTEXT, 为什么呢? 那是因为默认情况下,已经有了他们的映射关系。默认集合: PLAINTEXT:PLAINTEXT,SSL:SSL,SASL_PLAINTEXT:SASL_PLAINTEXT,SASL_SSL:SASL_SSL属性格式:监听名称1:安全协议1,监听名称2:安全协议2现有的安全协议,有4种, 分别如下, 下为默认监听器名称映射对应的安全协议情况。PLAINTEXT => PLAINTEXT 不需要授权,非加密通道SSL => SSL 使用SSL加密通道SASL_PLAINTEXT => SASL_PLAINTEXT 使用SASL认证非加密通道SASL_SSL => SASL_SSL 使用SASL认证并且SSL加密通道当然你也可以自己重新映射监听器名称和安全协议, 比如: 示例4inter.broker.listener.name用于Broker之间通信的listener的名称。如果未设置,则listener名称由 security.inter.broker.protocol 定义(security.inter.broker.protocol默认值是PLAINTEXT)。同时设置 这个和 security.inter.broker.protocol 属性是错误的。默认值:空不可动态配置。特别注意:这个属性表示是Broker之间的网络通信使用的监听器, 比如 Broker2Broker但是还有一种就是Controller2Broker、如果没有配置control.plane.listener.name ,那么走的也是inter.broker.listener.name 这个监听器。根据本地配置的监听器名称, 去查找其他Broker的监听器的EndPoint。所以一般所有Broker的监听器名称都必须一致,否则的话就找不到具体的EndPoint,无法正确的发起请求。security.inter.broker.protocol用于在代理之间进行通信的安全协议。 有效值为:PLAINTEXT、SSL、SASL_PLAINTEXT、SASL_SSL。同时设置 该属性和 inter.broker.listener.name 属性是错误的。默认值:PLAINTEXT (纯文本)注意这个跟inter.broker.listener.name是有区别的, 这个配置只有四个选项,是安全协议而inter.broker.listener.name是监听名称, 是需要通过这个监听名称去找到它映射的 安全协议 还有 IP:PORT如果inter.broker.listener.name没有配置,则默认使用 security.inter.broker.protocol 的配置. 对inter.broker.listener.name而言,最终还是要去找到对应的 IP:PORT。一般自定义了监听器名称, inter.broker.listener.name就是必须要设置的, 不能使用security.inter.broker.protocol 来代替。control.plane.listener.name用于Controller和Broker之间通信的监听器名称, Broker将会使用control.plane.listener.name 来定位监听器列表中的EndPoint如果未设置,则默认使用inter.broker.listener.name来通信,没有专门的链接。详情请看:Kafka的客户端NetworkClient如何发起的请求示例说明1 . 绑定一个IP, 客户端使用另外的IP访问让broker 监听localhost:9092. 然后客户端访问broker的具体IP.listeners=PLAINTEXT://localhost:9092启动之后查看一下监听情况Linux命令: netstat -anp |grep 9092 Mac环境命令:netstat -AaLlnW当然,如果你这台机器刚好还是Controller的话,除了了LISTEN, 还能看到ESTABLISHED状态的连接,因为Controller也会给这台Broker建立连接发起请求的,比如通知Broker更新元信息之类的。我们使用生产者客户端来生产几条消息sh bin/kafka-console-producer.sh --bootstrap-server 127.0.0.1:9092 --topic Topic4 ## 或者 sh bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic Topic4 可以发现正常发送消息。那么接下来,使用使用具体IP发起请求 sh bin/kafka-console-producer.sh --bootstrap-server 10.xxx.xx.128:9092 --topic Topic4 [2022-03-16 12:59:07,024] WARN [Controller id=1000, targetBrokerId=1000] Connection to node 1000 (/10.xxx.xxx.xx:9092) could not be established. Broker may not be available. (org.apache.kafka.clients.NetworkClient) 可以看到,客户端提示说不能跟这个ip:port建立连接。2. listeners 和 advertised.listeners 配置的IP不一样 listeners=PLAINTEXT://xx.xx.xxx.01:9092 advertised.listeners=PLAINTEXT:/xx.xx.xxx.02:9092 假设你本地监听和发布的监听不一样, 那么就会造成其他broker和客户端跟这台broker不能正确的建立链接。如果你这台Broker刚好还是Controller,那么他也会对自己建立连接, 都是根据advertised.listeners的配置来建立的,同样会失败。其他broker也一样。 [2022-03-16 12:59:07,024] WARN [Controller id=1000, targetBrokerId=1000] Connection to node 1000 (/10.xxx.xxx.xx:9092) could not be established. Broker may not be available. (org.apache.kafka.clients.NetworkClient) 3 . listeners监听任意可用IP, advertised.listeners发布指定IP在示例2中,我们指定 listeners 监听器和advertised.listeners发布的监听器不一致会导致异常。那么,我们只需要将监听器的和发布的监听器一致就行了当然,我们还可直接设置监听器监听任意可用IP(该Broker上的可用IP)listeners=PLAINTEXT://0.0.0.0:9092当然,如果只是将host设置为 0.0.0.0. 那么会报错java.lang.IllegalArgumentException: requirement failed: advertised.listeners cannot use the nonroutable meta-address 0.0.0.0. Use a routable IP address. at kafka.server.KafkaConfig.validateValues(KafkaConfig.scala:1789)因为默认情况下,advertised.listeners不设置的话,则默认使用listeners的属性,然而advertised.listeners是不支持0.0.0.0的,所以需要指定暴露的监听器,如下listeners=PLAINTEXT://0.0.0.0:9092 advertised.listeners=PLAINTEXT://xx.xx.xx.128:9092这样子配置就不会报错了,其他Broker和客户端会通过advertised.listeners发布的监听器来跟该Broker建立链接。注意: 这个时候你还可以在这台Broker的机器上使用 localhost 来进行访问。比如: sh bin/kafka-console-producer.sh --bootstrap-server 127.0.0.1:9092 --topic Topic4 可以看到也是可以正常发送消息的。4 . listeners配置多个监听器,内外网分流 listeners = INSIDE://内网IP:9091,OUTSIDE://外网IP:9092 #把OUTSIDE 的安全协议映射成PLAINTEXT INSIDE也映射成PLAINTEXT listener.security.protocol.map=INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT # Broker之间的连接用 INSIDE 监听器 inter.broker.listener.name=INSIDE 设置了2个监听器①. INSIDE 监听内网IP②. OUTSIDE 监听外网IP因为这个是我们自己定义的监听名称,listener.security.protocol.map默认映射中并没有对应的映射关系所以我们就需要主动设置这个映射关系 listener.security.protocol.map=INSIDE:PLAINTEXT,OUTSIDE:SSL不然会抛异常Caused by: java.lang.IllegalArgumentException: No security protocol defined for listener INSIDE at kafka.cluster.EndPoint$.$anonfun$createEndPoint$2(EndPoint.scala:48)注意:自定义了监听器,则必须要配置inter.broker.listener.name 确定好内部的broker之间通信的监听器. 并确保能够正常访问。这样Broker直接就会通过内网互相连接, 客户端除了可以通过内网连接(如果在内网环境的话),也可以通过外网连接。几种场景的配置方式1. 一台机器部署一套集群这种场景一般是自己开发测试的时候, 比如自己搭建一个集群,学习学习,但是又没有那么多机器,那么就可以在一台电脑上部署多个Broker。只配置listeners属性 listeners = 监听名称://your.host.name:port 关于监听名称,默认的映射关系有4种。PLAINTEXT => PLAINTEXT 不需要授权,非加密通道SSL => SSL 使用SSL加密通道SASL_PLAINTEXT => SASL_PLAINTEXT 使用SASL认证非加密通道SASL_SSL => SASL_SSL 使用SASL认证并且SSL加密通道简单一点,用PLAINTEXT就够了, 这里我们可以把host给去掉, 或者使用localhost listeners = PLAINTEXT://:port 或者 listeners = PLAINTEXT://localhost:port 如果没有配置host,会调用java.net.InetAddress.getCanonicalHostName()获取本机host. 默认情况下就是localhost.这里之所以建议你不填写具体的host,是因为一般自己搭建玩玩的时候可能网络IP会经常变动(例如家里的和公司), 如果绑定了具体的IP的话,每次重启都要更换配置就很麻烦。可以看看Broker启动后注册到zk中的配置如下{ "features": {}, "listener_security_protocol_map": { "PLAINTEXT": "PLAINTEXT" }, "endpoints": ["PLAINTEXT://localhost:9092"], "jmx_port": -1, "port": 9092, "host": "localhost", "version": 5, "timestamp": "1647337490945" }这个endpoints 就是broker注册到zk的访问地址, 如果其他Broker或者客户端要跟这台Broker发生网络请求话, 就是拿的这里面的值。所以,你想想看,如果是不同机器上,你配置的host是 localhost, 是不是就访问不了?当然,listeners属性的host,我们也可以自己去hosts文件里面配置别的域名。配置域名指向的具体IP, 这样的话那还能奏效。就是每个Broker和客户端都要配置host,这就比较麻烦,所以还不如直接配置IP呢。2. 内网环境多机器部署集群这种是绝大部分的场景, 一般公司部署集群都是在公司内网环境下, Broker之间和Broker与客户端之间都在同一个网络环境。并且安全协议都是直接PLAINTEXT(明文)或者其他 listeners=PLAINTEXT://ip:port 3. 内网和外网分流 listeners=INTERNAL://内网ip:port1,EXTERNAL://外网ip:port2 #把OUTSIDE 的安全协议映射成PLAINTEXT INSIDE也映射成PLAINTEXT listener.security.protocol.map=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT # Broker之间的连接用 INSIDE 监听器 inter.broker.listener.name=INTERNAL 配置了两个监听器,每个Brokerinter.broker.listener.name=INTERNAL 使用内网交流。其他的客户端例如Producer和Consumer 请求的时候直接访问外网IP.3. 内网和外网和Controller分流 listeners=INTERNAL://内网ip:port1,EXTERNAL://外网ip:port2,CONTROLLER://内网ip:port3, #把OUTSIDE 的安全协议映射成PLAINTEXT INSIDE也映射成PLAINTEXT listener.security.protocol.map=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT # Broker之间的连接用 INSIDE 监听器 inter.broker.listener.name=INTERNAL control.plane.listener.name=CONTROLLER 这样配置Controller2Broker或者Broker2ControllerBroker2BrokerClients2Broker他们都会会有独立的网络通信线程
《Kafka运维管控平台LogiKM》✏️更强大的管控能力✏️ 更高效的问题定位能力 更便捷的集群运维能力 更专业的资源治理 更友好的运维生态 大家好,我是彦祖呀在阅读本文之前, 希望你可以思考一下下面几个问题, 带着问题去阅读文章会获得更好的效果。发送消息的时候, 当Broker挂掉了,消息体还能写入到消息缓存中吗?当消息还存储在缓存中的时候, 假如Producer客户端挂掉了,消息是不是就丢失了?当最新的ProducerBatch还有空余的内存,但是接下来的一条消息很大,不足以加上上一个Batch中,会怎么办呢?那么创建ProducerBatch的时候,应该分配多少的内存呢?什么是消息累加器RecordAccumulatorkafka为了提高Producer客户端的发送吞吐量和提高性能,选择了将消息暂时缓存起来,等到满足一定的条件, 再进行批量发送, 这样可以减少网络请求,提高吞吐量。而缓存这个消息的就是RecordAccumulator类.上图就是整个消息存放的缓存模型,我们接下来一个个来讲解。消息缓存模型上图表示的就是 消息缓存的模型, 生产的消息就是暂时存放在这个里面。每条消息,我们按照TopicPartition维度,把他们放在不同的Deque<ProducerBatch> 队列里面。TopicPartition相同,会在相同Deque<ProducerBatch> 的里面。ProducerBatch : 表示同一个批次的消息, 消息真正发送到Broker端的时候都是按照批次来发送的,这个批次可能包含一条或者多条消息。如果没有找到消息对应的ProducerBatch队列, 则创建一个队列。找到ProducerBatch队列队尾的Batch,发现Batch还可以塞下这条消息,则将消息直接塞到这个Batch中找到ProducerBatch队列队尾的Batch,发现Batch中剩余内存,不够塞下这条消息,则会创建新的Batch当消息发送成功之后, Batch会被释放掉。ProducerBatch的内存大小那么创建ProducerBatch的时候,应该分配多少的内存呢?先说结论: 当消息预估内存大于batch.size的时候,则按照消息预估内存创建, 否则按照batch.size的大小创建(默认16k).我们来看一段代码,这段代码就是在创建ProducerBatch的时候预估内存的大小RecordAccumulator#append /** * 公众号: 石臻臻的杂货铺 * 微信:szzdzhp001 **/ // 找到 batch.size 和 这条消息在batch中的总内存大小的 最大值 int size = Math.max(this.batchSize, AbstractRecords.estimateSizeInBytesUpperBound(maxUsableMagic, compression, key, value, headers)); // 申请内存 buffer = free.allocate(size, maxTimeToBlock); 假设当前生产了一条消息为M, 刚好消息M找不到可以存放消息的ProducerBatch(不存在或者满了),那么这个时候就需要创建一个新的ProducerBatch了预估消息的大小 跟batch.size 默认大小16384(16kb). 对比,取最大值用于申请的内存大小的值。原文地址:图解Kafka Producer 消息缓存模型那么, 这个消息的预估是如何预估的?纯粹的是消息体的大小吗?DefaultRecordBatch#estimateBatchSizeUpperBound预估需要的Batch大小,是一个预估值,因为没有考虑压缩算法从额外开销 /** * 使用给定的键和值获取只有一条记录的批次大小的上限。 * 这只是一个估计,因为它没有考虑使用的压缩算法的额外开销。 **/ static int estimateBatchSizeUpperBound(ByteBuffer key, ByteBuffer value, Header[] headers) { return RECORD_BATCH_OVERHEAD + DefaultRecord.recordSizeUpperBound(key, value, headers); } 预估这个消息M的大小 + 一个RECORD_BATCH_OVERHEAD的大小RECORD_BATCH_OVERHEAD是一个Batch里面的一些基本元信息,总共占用了 61B消息M的大小也并不是单单的只有消息体的大小,总大小=(key,value,headers)的大小+MAX_RECORD_OVERHEADMAX_RECORD_OVERHEAD :一条消息头最大占用空间, 最大值为21B也就是说创建一个ProducerBatch,最少就要83B .比如我发送一条消息 " 1 " , 预估得到的大小是 86B, 跟batch.size(默认16384) 相比取最大值。 那么申请内存的时候取最大值 16384 。关于Batch的结构和消息的结构,我们回头单独用一篇文章来讲解。内存分配我们都知道RecordAccumulator里面的缓存大小是一开始定义好的, 由buffer.memory控制, 默认33554432 (32M)当生产的速度大于发送速度的时候,就可能出现Producer写入阻塞。而且频繁的创建和释放ProducerBatch,会导致频繁GC, 所有kafka中有个缓存池的概念,这个缓存池会被重复使用,但是只有固定( batch.size)的大小才能够使用缓存池。PS:以下16k指得是 batch.size的默认值.Batch的创建和释放1. 内存16K 缓存池中有可用内存①. 创建Batch的时候, 会去缓存池中,获取队首的一块内存ByteBuffer 使用。②. 消息发送完成,释放Batch, 则会把这个ByteBuffer,放到缓存池的队尾中,并且调用ByteBuffer.clear 清空数据。以便下次重复使用2. 内存16K 缓存池中无可用内存①. 创建Batch的时候, 去非缓存池中的内存获取一部分内存用于创建Batch. 注意:这里说的获取内存给Batch, 其实就是让 非缓存池nonPooledAvailableMemory 减少 16K 的内存, 然后Batch正常创建就行了, 不要误以为好像真的发生了内存的转移。②. 消息发送完成,释放Batch, 则会把这个ByteBuffer,放到缓存池的队尾中,并且调用ByteBuffer.clear 清空数据, 以便下次重复使用原文地址:图解Kafka Producer 消息缓存模型3. 内存非16K 非缓存池中内存够用①. 创建Batch的时候, 去非缓存池(nonPooledAvailableMemory)内存获取一部分内存用于创建Batch. 注意:这里说的获取内存给Batch, 其实就是让 非缓存池(nonPooledAvailableMemory) 减少对应的内存, 然后Batch正常创建就行了, 不要误以为好像真的发生了内存的转移。②. 消息发送完成,释放Batch, 纯粹的是在非缓存池(nonPooledAvailableMemory)中加上刚刚释放的Batch内存大小。 当然这个Batch会被GC掉4. 内存非16K 非缓存池内存不够用①. 先尝试将 缓存池中的内存一个一个释放到 非缓存池中, 直到非缓存池中的内存够用与创建Batch了②. 创建Batch的时候, 去非缓存池(nonPooledAvailableMemory)内存获取一部分内存用于创建Batch. 注意:这里说的获取内存给Batch, 其实就是让 非缓存池(nonPooledAvailableMemory) 减少对应的内存, 然后Batch正常创建就行了, 不要误以为好像真的发生了内存的转移。③. 消息发送完成,释放Batch, 纯粹的是在非缓存池(nonPooledAvailableMemory)中加上刚刚释放的Batch内存大小。 当然这个Batch会被GC掉例如: 下面我们需要创建 48k的batch, 因为超过了16k,所以需要在非缓存池中分配内存, 但是非缓存池中当前可用内存为0 , 分配不了, 这个时候就会尝试去 缓存池里面释放一部分内存到 非缓存池。释放第一个ByteBuffer(16k) 不够,则继续释放第二个,直到释放了3个之后总共48k,发现内存这时候够了, 再去创建Batch。注意:这里我们涉及到的 非缓存池中的内存分配, 仅仅指的的内存数字的增加和减少。问题和答案发送消息的时候, 当Broker挂掉了,消息体还能写入到消息缓存中吗?当Broker挂掉了,Producer会提示下面的警告⚠️, 但是发送消息过程中这个消息体还是可以写入到 消息缓存中的,也仅仅是写到到缓存中而已。 WARN [Producer clientId=console-producer] Connection to node 0 (/172.23.164.192:9090) could not be established. Broker may not be available 当最新的ProducerBatch还有空余的内存,但是接下来的一条消息很大,不足以加上上一个Batch中,会怎么办呢?那么会创建新的ProducerBatch。那么创建ProducerBatch的时候,应该分配多少的内存呢?触发创建ProducerBatch的那条消息预估大小大于batch.size ,则以预估内存创建。否则,以batch.size创建。还有一个问题供大家思考:当消息还存储在缓存中的时候, 假如Producer客户端挂掉了,消息是不是就丢失了?
哈喽~大家好啊,我是彦祖之前,我写过一篇文章叫做 Kafka如何修改分区Leader就是因为在我们实际的运维过程中,需要指定某个副本为ISR,但是呢 Kafka中的Leader选举策略并不支持这个功能,所以需要我们自己来实现它。 关于Leader选举策略,你可以看这篇文章 Leader选举流程和4种选举策略但是我们在之前的文章中,是留下了一个小尾巴-优化与改进。我们先简单的回顾一下之前的2种方案方案一: 分区副本重分配 (低成本方案)之前关于分区副本重分配 我已经写过很多文章了, 这里我就简单说一下;一般分区副本重分配主要有三个流程生成推荐的迁移Json文件执行迁移Json文件验证迁移流程是否完成这里我们主要看第2步骤, 来看看迁移文件一般是什么样子的{ "version": 1, "partitions": [{ "topic": "topic1", "partition": 0, "replicas": [0,1,2] }] }这个迁移Json意思是, 把topic1的「0」号分区的副本分配成[0,1,2] ,也就是说 topic1-0号分区最终有3个副本分别在 {brokerId-0,brokerId-1,brokerId-2} ; 又根据Leader的选举策略得知,不管是什么策略的选择,都是按照AR的顺序来选的修改AR顺序AR: 副本的分配顺序那么我们想要实现我们的需求是不是把这个Json文件 中的 "replicas": [0,1,2] 改一下就行了 比如改成 "replicas": [2,1,0] ,改完Json后执行,执行execute, 正式开始重分配流程! 迁移完成之后, 就会发现,Leader已经变成上面的第一个位置的副本「2」 了执行Leader选举修改完AR顺序就结束了吗?可以说是结束了,也可以说没有结束。上面只是修改了AR的顺序, 但是没有执行Leader选举呀,这个时候Leader还是原来的,所以我们需要主动触发一下Leader选举## 石臻臻的杂货铺 ## 微信: szzdzhp001 sh bin/kafka-leader-election.sh --bootstrap-server xxxx:9090 --topic Topic1 --election-type PREFERRED --partition 0 这样就会立马切换成我们想要的Leader了。也可以不主动触发,等Controller自动均衡。如果你觉得主动触发这个很麻烦,那么没有关系,那就不执行,如果你开启了自动均衡策略的话,默认是开启的。延伸: 自动均机制当一个broker停止或崩溃时,这个broker中所有分区的leader将转移给其他副本。这意味着在默认情况下,当这个broker重新启动之后,它的所有分区都将仅作为follower,不再用于客户端的读写操作。为了避免这种不平衡,Kafka有一个优先副本的概念。如果一个分区的副本列表是1,5,9,节点1将优先作为其他两个副本5和9的leader。Controller会有一个定时任务,定期执行优先副本选举,这样就不会导致负载不均衡和资源浪费,这就是leader的自动均衡机制属性释义默认auto.leader.rebalance.enable是否开启自动均衡trueleader.imbalance.check.interval.seconds自动均衡的周期时间,单位秒300leader.imbalance.per.broker.percentage标识每个 Broker 失去平衡的比率,如果超过改比率,则执行重新选举 Broker 的 leader;默认比例是10%;10优缺点优点: 实现了需求, 不需要改源码,也没有额外的开发工作。缺点: 操作比较复杂容易出错,需要先获取原先的分区分配数据,然后手动修改Json文件,这里比较容易出错,影响会比较大,当然这些都可以通过校验接口来做好限制, 最重要的一点是 副本重分配当前只能有一个任务 ! 假如你当前有一个「副本重分配」的任务在,那么这里就不能够执行了。方案二: 手动修改AR顺序(高成本方案)从zk中获取/brokers/topics/{topic名称}节点数据。手动调整一下里面的顺序将调整后的数据,重新覆盖掉之前的节点。删除zk中的/Controller节点,让它触发重新加载,并且同时触发Leader选举。例如:修改的时候请先用get获取数据,在那个基础上改,因为不同版本,里面的数据结构是不一样的,我们只需要改分区AR顺序就行了 "partitions":{"0":[0,1,2]} ## get zk 节点数据。 get /szz1/brokers/topics/Topic2 ## zk中的修改命令 set /szz1/brokers/topics/Topic2 {"version":2,"partitions":{"0":[0,1,2]},"adding_replicas":{},"removing_replicas":{}} 为什么要删除Controller的zk节点?之所以删除Controller节点,是因为我们手动修改了zk节点数据之后,因为没有副本的新增,是不会触发Controller去更新AR内存的,就算你主动触发Leader选举,AR还是以前的,并不会达到想要的效果。删除zk中的/Controller节点,会触发Controller重新选举,重新选举会重新加载所有元数据,所以我们刚刚加载的数据就会生效, 同时Controller重新加载也会触发Leader选举。简单代码当然上面功能,手动改起来麻烦,那么饿肯定是要集成到LogiKM 3.0中的咯;优缺点优点: 实现了目标需求, 简单, 操作方便缺点: 频繁的Controller重选举对生产环境来说会有一些影响;方案三:修改源码(高级方案推荐)我们方案二中的问题就是需要删除/Controller节点发送重新选举,我们能不能不重新选举Controller也能生效呢?如何让修改后的AR立即生效 ?Controller会监听每一个topic的节点/brokers/topics/{topic名称}KafkaController#processPartitionModifications /** * 石臻臻的杂货铺 * 微信:szzdzhp001 * 省略部分代码 **/ private def processPartitionModifications(topic: String): Unit = { def restorePartitionReplicaAssignment( topic: String, newPartitionReplicaAssignment: Map[TopicPartition, ReplicaAssignment] ): Unit = { val partitionReplicaAssignment = zkClient.getFullReplicaAssignmentForTopics(immutable.Set(topic)) val partitionsToBeAdded = partitionReplicaAssignment.filter { case (topicPartition, _) => controllerContext.partitionReplicaAssignment(topicPartition).isEmpty } if (topicDeletionManager.isTopicQueuedUpForDeletion(topic)) { } else if (partitionsToBeAdded.nonEmpty) { info(s"New partitions to be added $partitionsToBeAdded") partitionsToBeAdded.foreach { case (topicPartition, assignedReplicas) => controllerContext.updatePartitionFullReplicaAssignment(topicPartition, assignedReplicas) } onNewPartitionCreation(partitionsToBeAdded.keySet) } } } 这段代码省略了很多,我想让你看到的是只有新增了副本,才会执行更新Controller的内存操作。那么我们在这里面新增一段逻辑新增逻辑:如果只是变更了AR的顺序,那么我们也更新一下内存。来我们改一下源码 // 1. 找到 AR 顺序有变更的 所有TopicPartition val partitionsOrderChange = partitionReplicaAssignment.filter { case (topicPartition, _) => //这里自己写下过滤逻辑 把只是顺序变更的分区找出 true } if (topicDeletionManager.isTopicQueuedUpForDeletion(topic)) { if (partitionsToBeAdded.nonEmpty) { } else { } } else if (partitionsToBeAdded.nonEmpty) { info(s"New partitions to be added $partitionsToBeAdded") partitionsToBeAdded.foreach { case (topicPartition, assignedReplicas) => controllerContext.updatePartitionFullReplicaAssignment(topicPartition, assignedReplicas) } onNewPartitionCreation(partitionsToBeAdded.keySet) }else if (partitionsOrderChange.nonEmpty) { // ② .在这里加个逻辑 info(s"OrderChange partitions to be updatecache $partitionsToBeAdded") partitionsOrderChange.foreach { case (topicPartition, assignedReplicas) => controllerContext.updatePartitionFullReplicaAssignment(topicPartition, assignedReplicas) } } 改成这样之后,上面的流程就变成了从zk中获取/brokers/topics/{topic名称}节点数据。手动调整一下里面的顺序将调整后的数据,重新覆盖掉之前的节点。手动执行一次,优先副本选举。完美解决!思考方案三 改了之后会对其他的流程有影响吗?上面更改的方法,一般是在分区副本重分配或者新增分区的时候会触发。上面新增的逻辑并不会对现有流程有影响,因为假设都是上面的场景的情况下,他们都是会主动更新内存的。在我看来,这里的改动,完全可以向kafka社区提一个Pr. 来“修复”这个问题。因为提了这个PR,对我们有收益,没有额外的开销!欢迎留下你的看法,一起讨论!
有人报案最近技术群里面有几个同学碰到了 删除Topic的问题, 怎么样也删除不掉,然后我协助排查之后,就做个记录,写篇文章,大家在碰到这类型的问题的时候应该怎么去排查收集线索报not retrying deletion 异常版本:kafka_2.11-2.0.0删除前在执行重分配,但是失败了,强制停止数据迁移,手动删除了节点/admin/reassign_partitions再次重新删除提示异常Topic test is already marked for deletion所有Broker均在线delete.topic.enable=true检查了每个Broker都没有副本被删除,甚至也没有被标记为--delete调查线索从我们收集到的线索来看,有两个突破口not retrying deletionTopic test is already marked for deletion我们先看,第2个突破口,打开kafka_2.11-2.0.0源码,全局搜索关键字is already marked for deletion这个表示,你已经标记了这个topic删除了, 在zk上写入了节点/amin/delete_topics/{topicName}上面收集线索时候我们知道是它重新执行删除的时候抛出的异常,说明zk节点已经写入了,已经准备删除了;这里没有什么问题问题在于为什么没有执行删除呢?所以下一个突破口就在于Not retrying deletion of topic ....通过源码我们可以看到,出现了这个异常表示的是:当前这个topic不符合重试删除的条件怎么样才符合重试删除条件?在删除队列topicsToBeDeleted里面;这个队列是从zk节点/amin/delete_topics获取的数据当前还未开始对该Topic进题删除; 判定条件是没有副本处于开始删除的状态「ReplicaDeletionStarted」(当然如果delete.topic.enable=false这条肯定满足)主题没有被标记为不符合删除条件; 不符合删除条件的都保存在topicsIneligibleForDeletion抽丝剥茧,接近真相上面的3个条件,通过对方了解到/amin/delete_topics 节点下面有数据, 线索排除让对方查询了Deletion started for replicas这个日志,日志表示的是哪些副本状态变更成「开始删除」 ,日志有查询到如下然后让查询Dead Replicas (%s) found for topic %s (这个表示的是哪些副本离线了) 也查询到如下从日志,和源码我们可以得出,Not retrying deletion of topic 的原因是: 删除流程已经开始,但是存在离线的或不可用的副本 ,哪些副本异常,从上面的Dead Replicas (%s) found for topic %s 的日志可以得知, 既然知道了原因,那么解决方案:聚焦副本为何离线了,让副本恢复正常就行了 不过这里我们还有再重点说一下第3种情况前面2个说完了,接着说一下topicsIneligibleForDeletion到底是什么,什么情况下才会放到这里面来呢?不符合Topic删除的条件是什么?Controller初始化的时候判断条件kafka_2.11-2.0.0 没有这个步骤数据正在迁移中判断数据是否在迁移中是通过判断topic的是否存在要新增或者删除的副本, 查询/brokers/topics/{topicName}节点中有没有这两个属性值topic副本所在Broker有宕机导致的副本不在线副本所在的数据目录log.dirs存在脱机磁盘运行中判断条件发起的StopReplica 请求返回异常,加入不符合删除条件删除的过程中,发现该Topic 有副本重分配的操作 则加入不符合删除条件删除的过程,有副本下线了,则加入不符合删除条件开始执行副本重分配的操作, 则加入不符合删除条件结案经过深入源码排查走访,我们基本上确定了问题的根源副本离线,导致的删除流程不能完成; 通过查询日志,也锁定了那些个嫌疑犯,好家伙还是团伙作案最后的解决方案也很粗暴,找到副本不正常的那几台Broker, 重启 …之后副本疯狂同步(其他一些topic数据同步);最终topic正常删除了排查手册为了以后出现同样类似的问题,我总结了一下问题的排查手段,给大家指明一条思路; 快速破案确保 delete.topic.enable=true ;配置文件查询确保当前该topic没有进行 「副本重分配」 , 查询zk节点/admin/reassign_partitions的值是否有该topic、或者 节点/brokers/topics/{topicName}节点里面的属性adding_replicas、removing_replicas有没有值确保所有副本所属Broker均在线确保副本均在线, (Broker在线并且log.dirs没有脱机), 搜日志"Dead Replicas " 关键字查询到哪些副本异常解放方案根据上面的排查顺序,对应不同的解决方案;如果正在进行 「副本重分配」 那么等待分配完成就可以正常删除了如果是副本不在线,那么就去解决为啥不在线,该重启就重启幕后黑手这就完了吗?「log.dir为什么会脱机呢?」 「脱机跟数据迁移有关系吗?」
文章目录源码分析1. Broker启动加载动态配置1.1 启动加载动态配置总流程1. 2 加载Topic动态配置1.3 加载Broker动态配置2. 查询动态配置 流程 `--describe`3. 新增/修改/删除/动态配置 的流程Topic配置其他的类型都一样4. Broker监听/config/changes的变更源码总结Q&A如果我想在我的项目中获取kafka的所有配置该怎么办?是否可以直接在zk中写入动态配置?为什么不直接监听 `/config/`下面的配置?Hello~~ 大家好,我是石臻臻~~~~今天这篇文章,给大家分享一下最近看kafka中的动态配置,不需要重启Broker,即时生效的配置 欢迎留言一起探讨!kafka中的配置Broker静态配置 .properties文件ZK中的动态配置 全局 default配置ZK中动态配置 指定配置优先级从底到高不想看过程,可以直接看最后的源码总结部分源码分析1. Broker启动加载动态配置KafkaServer.startup1.1 启动加载动态配置总流程1. 动态配置初始化 config.dynamicConfig.initialize(zkClient)构造当前配置文件 currentConfig, 然后从zk中获取节点 /config/brokers/<default>信息,然后更新配置updateDefaultConfig; (动态默认配置覆盖静态配置)从节点/config/brokers/{当前BrokerId}获取配置, 如果配置中有ConfigType=PASSWORD的配置(例如ssl.keystore.password)存在,接着判断 是否存在password.encoder.old.secret 配置,(这个配置是用来加解密ConfigType=PASSWORD的旧的秘钥),尝试用旧秘钥解密秘钥; 然后将这些配置重新加密回写入/config/brokers/{当前BrokerId} ; 然后返回配置 (这里主要是动态配置里面有密码类型配置的时候需要做一次解密加密处理)将上面得到的配置(password类型修改之后) 更新内存总的配置;优先级 静态配置<动态默认配置<指定动态配置2. 注册可变更配置监听器如果有对应的配置变更了,那么相应的监听器就会收到通知去修改自己相应的配置; config.dynamicConfig.addReconfigurables(this) DynamicBrokerConfig.addReconfigurables// ......... def addReconfigurables(kafkaServer: KafkaServer): Unit = { kafkaServer.authorizer match { case Some(authz: Reconfigurable) => addReconfigurable(authz) case _ => } addReconfigurable(new DynamicMetricsReporters(kafkaConfig.brokerId, kafkaServer)) addReconfigurable(new DynamicClientQuotaCallback(kafkaConfig.brokerId, kafkaServer)) addBrokerReconfigurable(new DynamicThreadPool(kafkaServer)) if (kafkaServer.logManager.cleaner != null) addBrokerReconfigurable(kafkaServer.logManager.cleaner) addBrokerReconfigurable(new DynamicLogConfig(kafkaServer.logManager, kafkaServer)) addBrokerReconfigurable(new DynamicListenerConfig(kafkaServer)) addBrokerReconfigurable(kafkaServer.socketServer) }3. 动态配置启动监听 // Create the config manager. start listening to notifications dynamicConfigManager = new DynamicConfigManager(zkClient, dynamicConfigHandlers) dynamicConfigManager.startup()注册节点处理器change-notification-/config/changes = stateChangeHandler注册节点处理器/config/changes = zNodeChildChangeHandler获取/config/changes 所有子节点看看有哪些变更遍历所有节点并截取节点的编号, 判断一下是不是大于上一次执行过变更的节点ID lastExecutedChange(启动的时候是-1)上个条件满足的话,则执行通知操作;不同entity执行的操作不一样,具体请看下面每个类型更新lastExecutedChange清除过期的通知节点, 默认过期时间15 * 60 * 1000(15分钟) 就是删除/config/changes /下面的过期节点1. 2 加载Topic动态配置TopicConfigHandler.processConfigChanges获取节点的data数据, 如果获取到了则执行通知流程notificationHandler.processNotification(d),处理器是ConfigChangedNotificationHandler; 它先解析节点的json数据,根据版本信息不同调用不同的处理方法; 下面是version=2的处理方式;根据json数据可以得到 entityType 和entityName; 那么久可以去对应的zk数据里面getData获取数据; 并且将获取到的数据Decode成Properties对象entityConfig;将key为下图中的属性 隐藏掉; 替换成value: [hidden]调用EntityHandler; 这里是TopicConfigHandler.processConfigChanges来进行处理,方法里面再看看流程 ->从动态配置entityConfig里面获取message.format.version配置消息格式版本号; 如果当前Broker的版本inter.broker.protocol.version 小于message.format.version配置; 则将message.format.version配置 排除掉调用TopicConfigHandler.updateLogConfig 来更新指定Topic的所有TopicPartition的配置,其实是将TP正在加载或初始化的状态标记为没有完成初始化,这将会在后续过程中促成TP重新加载并初始化将动态配置和并覆盖Server的默认配置为新的 newConfig, 然后根据Topic获取对应的Logs对象; 遍历Logs去更新newConfig;并尝试执行 initializeLeaderEpochCache; (需要注意的是:这里的动态配置不是支持所有的配置参数,请看【kafka运维】Kafka全网最全最详细运维命令合集(精品强烈建议收藏!!!)的附件部分)当然特殊配置如leader.replication.throttled.replicas,follower.replication.throttled.replicas这两个限流相关;解析配置之后,然后通过quotaManager.markThrottled/quotaManager.removeThrottle更新/移除对应的限流分区集合如果动态配置了unclean.leader.election.enable=true(允许非同步副本选主 );那么就会执行TopicUncleanLeaderElectionEnable方法来让它改变选举策略(前提是当前Broker是Controller角色)1.3 加载Broker动态配置BrokerConfigHandler.processConfigChanges假设我们配置了默认配置; zk里面的节点是<default>sh bin/kafka-configs.sh --bootstrap-server xxxxx:9090 --alter --entity-type brokers --entity-default --add-config log.segment.bytes=88888888从zk节点/config/changes里面获取变更节点的json数据.然后去对应的 /config/{entityType}/{entituName}获取对应的数据如果是<default>节点,说明有配置动态默认配置; 则按照 静态配置<动态默认配置<动态指定配置 的顺序重新加载覆盖一下; 如果 新旧配置有变更(有可能执行了一次命令但是参数并没有变化的情况,修改了个寂寞)的情况下 才会做更新的; 并且 通知到所有的 BrokerReconfigurable; 这个就是上面启动时候 1.1 启动加载动态配置总流程的第2步骤 (注册可变更配置监听器) 注册的;如果是指定BrokerId, 则除了上面2重新加载覆盖之外, 相关限流 配置leader.replication.throttled.rate、follower.replication.throttled.rate、replica.alter.log.dirs.io.max.bytes.per.second 都会被更新一下quotaManagers.leader/leader/alterLogDirs.updateQuota ;如果这些配置没有配置的话,则用 Long.MaxValue(相当于是不限流)来更新2. 查询动态配置 流程 --describe简单检验根据类型查询entities ; type是topics就获取所有topic; type是broker|broker-loggers则查询所有Broker节点遍历entities获取配置 ;做些简单校验;然后想Broker发起describeConfigs请求; 节点策略是LeastLoadedNodeProvider节点调用方法 KafkaApis.handleDescribeConfigsRequest未经授权配置不查询经过授权的配置开始查询 ;当查询的是topics时, 去zk节点/confgi/类型/类型名 ,获取到动态配置数据之后, 然后将其覆盖本地跟Log相关的静态配置, 完事之后组装一下返回;(1.数据为空过滤2.敏感数据设置value=null; ConfigType=PASSWORD和不知道类型是啥的都是敏感数据 3. 组装所有的同义配置(静态默认配置、本地静态、默认动态配置、指定动态配置、等等多个配置))返回的数据类型如下:如果有broker|broker-loggers节点, 则在 获取到数据之后 然后指定nodeId节点发起 describeBrokerConfigs请求如果查询的是brokers如果查询的是 broker-loggers3. 新增/修改/删除/动态配置 的流程1. 发起请求查询当前的类型配置; 这里的查询 跟上面的--describe流程是一样的相关校验;如果有delete-config配置, 需要校验一下当前配置有没有;如果没有抛出异常;计算出需要变更的配置之后, 发起请求incrementalAlterConfigs;如果请求类型是 brokers/broker-loggers 则发起请求的接收方是 指定的Broker 节点; 否则就是LeastLoadedNodeProvider (当前负载最少的节点)2. incrementalAlterConfigs 增量修改配置KafkaApis.handleIncrementalAlterConfigsRequest通过请求参数解析 配置 configs过滤一下未授权的配置如果配置中有重复的项则抛出异常Topic配置获取节点 /config/topics/{topicName} 中的配置数据;然后根据请求参数的属性 ,组装好变更后的配置是什么样的 configs;简单校验一下, 并且支持自定义校验,如果有 alter.config.policy.class.name= 配置(默认null)的话,则会实例化指定的类(需要继承 AlterConfigPolicy类);并调用他的 validate方法来校验;调用写入zk配置的接口, 将动态配置重新写入(SetDataRequest)到接口 /config/topics/{topicName}中;创建并写入配置变更记录顺序节点 /config/changes/config_change_序列号 中; 这个节点主要是让Broker们来监听这个节点的来了解到哪个配置有变更的;其他的类型都一样省略4. Broker监听/config/changes的变更在 1. Broker启动加载动态配置 中我们了解到有对节点/config/change注册一个子节点变更的监听处理器那么对动态配置做出修改之后, 这个节点就会新增一条数据,那么所有的Broker都会收到这个通知;所以我们就要来看一看收到通知之后又做了哪些事情这个流程是又回到了上面的 1. 2 加载Topics/Brokers动态配置 的流程中了;源码总结原理部分讲解比较详细的可以看 : Kafka动态配置实现原理解析 - 李志涛 - 博客园Q&A如果我想在我的项目中获取kafka的所有配置该怎么办?启动的时候加载一次所有Broker的配置监听节点/config/change节点的变化是否可以直接在zk中写入动态配置?不可以,因为Broker是监听 /config/changes/里面的Broker节点,来实时得知有数据变更;为什么不直接监听 /config/下面的配置?没有必要,这样监听的数据数据太多了,而且 你不知道具体是改了哪个配置,所以每次都要全部更新一遍,无缘无故的加重负担了, 用/config/change 节点来得知哪个类型的数据变更, 只变更这个相关数据就可以了
1.查看日志文件 kafka-dump-log.sh参数 描述 例子--deep-iteration --files <String: file1, file2, ...> 必需; 读取的日志文件 –files 0000009000.log--key-decoder-class 如果设置,则用于反序列化键。这类应实现kafka.serializer。解码器特性。自定义jar应该是在kafka/libs目录中提供 --max-message-size 最大的数据量,默认:5242880 --offsets-decoder if set, log data will be parsed as offset data from the __consumer_offsets topic. --print-data-log 打印内容 --transaction-log-decoder if set, log data will be parsed as transaction metadata from the __transaction_state topic --value-decoder-class [String] if set, used to deserialize the messages. This class should implement kafka. serializer.Decoder trait. Custom jar should be available in kafka/libs directory. (default: kafka.serializer. StringDecoder) --verify-index-only if set, just verify the index log without printing its content. 查询Log文件sh bin/kafka-dump-log.sh --files kafka-logs-0/test2-0/00000000000000000300.log查询Log文件具体信息 --print-data-logsh bin/kafka-dump-log.sh --files kafka-logs-0/test2-0/00000000000000000300.log --print-data-log查询index文件具体信息sh bin/kafka-dump-log.sh --files kafka-logs-0/test2-0/00000000000000000300.index配置项为log.index.size.max.bytes; 来控制创建索引的大小;查询timeindex文件sh bin/kafka-dump-log.sh --files kafka-logs-0/test2-0/00000000000000000300.timeindex
文章目录消费者组管理 kafka-consumer-groups.sh1. 查看消费者列表`--list`2. 查看消费者组详情`--describe`3. 删除消费者组`--delete`4. 重置消费组的偏移量 `--reset-offsets`5. 删除偏移量`delete-offsets`More日常运维 、问题排查 怎么能够少了滴滴开源的滴滴开源LogiKM一站式Kafka监控与管控平台消费者组管理 kafka-consumer-groups.sh1. 查看消费者列表--listsh bin/kafka-consumer-groups.sh --bootstrap-server xxxx:9090 --list先调用MetadataRequest拿到所有在线Broker列表再给每个Broker发送ListGroupsRequest请求获取 消费者组数据2. 查看消费者组详情--describeDescribeGroupsRequest查看消费组详情--group 或 --all-groups查看指定消费组详情--groupsh bin/kafka-consumer-groups.sh --bootstrap-server xxxxx:9090 --describe --group test2_consumer_group查看所有消费组详情--all-groupssh bin/kafka-consumer-groups.sh --bootstrap-server xxxxx:9090 --describe --all-groups查看该消费组 消费的所有Topic、及所在分区、最新消费offset、Log最新数据offset、Lag还未消费数量、消费者ID等等信息查询消费者成员信息--members所有消费组成员信息sh bin/kafka-consumer-groups.sh --describe --all-groups --members --bootstrap-server xxx:9090指定消费组成员信息sh bin/kafka-consumer-groups.sh --describe --members --group test2_consumer_group --bootstrap-server xxxx:9090查询消费者状态信息--state所有消费组状态信息sh bin/kafka-consumer-groups.sh --describe --all-groups --state --bootstrap-server xxxx:9090指定消费组状态信息sh bin/kafka-consumer-groups.sh --describe --state --group test2_consumer_group --bootstrap-server xxxxx:90903. 删除消费者组--deleteDeleteGroupsRequest删除消费组–delete删除指定消费组--groupsh bin/kafka-consumer-groups.sh --delete --group test2_consumer_group --bootstrap-server xxxx:9090删除所有消费组--all-groupssh bin/kafka-consumer-groups.sh --delete --all-groups --bootstrap-server xxxx:9090PS: 想要删除消费组前提是这个消费组的所有客户端都停止消费/不在线才能够成功删除;否则会报下面异常Error: Deletion of some consumer groups failed: * Group 'test2_consumer_group' could not be deleted due to: java.util.concurrent.ExecutionException: org.apache.kafka.common.errors.GroupNotEmptyException: The group is not empty. 4. 重置消费组的偏移量 --reset-offsets能够执行成功的一个前提是 消费组这会是不可用状态;下面的示例使用的参数是: --dry-run ;这个参数表示预执行,会打印出来将要处理的结果;等你想真正执行的时候请换成参数--excute ;下面示例 重置模式都是 --to-earliest 重置到最早的;请根据需要参考下面 相关重置Offset的模式 换成其他模式;重置指定消费组的偏移量 --group重置指定消费组的所有Topic的偏移量--all-topicsh bin/kafka-consumer-groups.sh --reset-offsets --to-earliest --group test2_consumer_group --bootstrap-server xxxx:9090 --dry-run --all-topic重置指定消费组的指定Topic的偏移量--topicsh bin/kafka-consumer-groups.sh --reset-offsets --to-earliest --group test2_consumer_group --bootstrap-server xxxx:9090 --dry-run --topic test2重置所有消费组的偏移量 --all-group重置所有消费组的所有Topic的偏移量--all-topicsh bin/kafka-consumer-groups.sh --reset-offsets --to-earliest --all-group --bootstrap-server xxxx:9090 --dry-run --all-topic重置所有消费组中指定Topic的偏移量--topicsh bin/kafka-consumer-groups.sh --reset-offsets --to-earliest --all-group --bootstrap-server xxxx:9090 --dry-run --topic test2--reset-offsets 后面需要接重置的模式相关重置Offset的模式参数 描述 例子--to-earliest : 重置offset到最开始的那条offset(找到还未被删除最早的那个offset) --to-current: 直接重置offset到当前的offset,也就是LOE --to-latest: 重置到最后一个offset --to-datetime: 重置到指定时间的offset;格式为:YYYY-MM-DDTHH:mm:SS.sss; --to-datetime "2021-6-26T00:00:00.000"--to-offset 重置到指定的offset,但是通常情况下,匹配到多个分区,这里是将匹配到的所有分区都重置到这一个值; 如果 1.目标最大offset<--to-offset, 这个时候重置为目标最大offset;2.目标最小offset>--to-offset ,则重置为最小; 3.否则的话才会重置为--to-offset的目标值; 一般不用这个 --to-offset 3465--shift-by 按照偏移量增加或者减少多少个offset;正的为往前增加;负的往后退;当然这里也是匹配所有的; --shift-by 100 、--shift-by -100--from-file 根据CVS文档来重置; 这里下面单独讲解 --from-file着重讲解一下上面其他的一些模式重置的都是匹配到的所有分区; 不能够每个分区重置到不同的offset;不过**--from-file**可以让我们更灵活一点;先配置cvs文档格式为: Topic:分区号: 重置目标偏移量test2,0,100 test2,1,200 test2,2,300执行命令sh bin/kafka-consumer-groups.sh --reset-offsets --group test2_consumer_group --bootstrap-server xxxx:9090 --dry-run --from-file config/reset-offset.csv5. 删除偏移量delete-offsets能够执行成功的一个前提是 消费组这会是不可用状态;偏移量被删除了之后,Consumer Group下次启动的时候,会从头消费;sh bin/kafka-consumer-groups.sh --delete-offsets --group test2_consumer_group2 --bootstrap-server XXXX:9090 --topic test2相关可选参数参数 描述 例子--bootstrap-server 指定连接到的kafka服务; –bootstrap-server localhost:9092--list 列出所有消费组名称 --list--describe 查询消费者描述信息 --describe--group 指定消费组 --all-groups 指定所有消费组 --members 查询消费组的成员信息 --state 查询消费者的状态信息 --offsets 在查询消费组描述信息的时候,这个参数会列出消息的偏移量信息; 默认就会有这个参数的; dry-run 重置偏移量的时候,使用这个参数可以让你预先看到重置情况,这个时候还没有真正的执行,真正执行换成--excute;默认为dry-run --excute 真正的执行重置偏移量的操作; --to-earliest 将offset重置到最早 to-latest 将offset重置到最近 MoreKafka专栏持续更新中…(源码、原理、实战、运维、视频、面试视频)
文章目录1.删除指定分区的消息kafka-delete-records.sh2. 查看Broker磁盘信息kafka-log-dirs.shMore日常运维 、问题排查 怎么能够少了滴滴开源的滴滴开源LogiKM一站式Kafka监控与管控平台1.删除指定分区的消息kafka-delete-records.sh删除指定topic的某个分区的消息删除至offset为1024先配置json文件offset-json-file.json{"partitions": [{"topic": "test1", "partition": 0, "offset": 1024}], "version":1 }在执行命令sh bin/kafka-delete-records.sh --bootstrap-server 172.23.250.249:9090 --offset-json-file config/offset-json-file.json验证 通过 LogIKM 查看发送的消息从这里可以看出来,配置"offset": 1024 的意思是从最开始的地方删除消息到 1024的offset; 是从最前面开始删除的2. 查看Broker磁盘信息kafka-log-dirs.sh查询指定topic磁盘信息--topic-list topic1,topic2sh bin/kafka-log-dirs.sh --bootstrap-server xxxx:9090 --describe --topic-list test2查询指定Broker磁盘信息--broker-list 0 broker1,broker2sh bin/kafka-log-dirs.sh --bootstrap-server xxxxx:9090 --describe --topic-list test2 --broker-list 0例如我一个3分区3副本的Topic的查出来的信息logDir Broker中配置的log.dir{ "version": 1, "brokers": [{ "broker": 0, "logDirs": [{ "logDir": "/Users/xxxx/work/IdeaPj/ss/kafka/kafka-logs-0", "error": null, "partitions": [{ "partition": "test2-1", "size": 0, "offsetLag": 0, "isFuture": false }, { "partition": "test2-0", "size": 0, "offsetLag": 0, "isFuture": false }, { "partition": "test2-2", "size": 0, "offsetLag": 0, "isFuture": false }] }] }, { "broker": 1, "logDirs": [{ "logDir": "/Users/xxxx/work/IdeaPj/ss/kafka/kafka-logs-1", "error": null, "partitions": [{ "partition": "test2-1", "size": 0, "offsetLag": 0, "isFuture": false }, { "partition": "test2-0", "size": 0, "offsetLag": 0, "isFuture": false }, { "partition": "test2-2", "size": 0, "offsetLag": 0, "isFuture": false }] }] }, { "broker": 2, "logDirs": [{ "logDir": "/Users/xxxx/work/IdeaPj/ss/kafka/kafka-logs-2", "error": null, "partitions": [{ "partition": "test2-1", "size": 0, "offsetLag": 0, "isFuture": false }, { "partition": "test2-0", "size": 0, "offsetLag": 0, "isFuture": false }, { "partition": "test2-2", "size": 0, "offsetLag": 0, "isFuture": false }] }] }, { "broker": 3, "logDirs": [{ "logDir": "/Users/xxxx/work/IdeaPj/ss/kafka/kafka-logs-3", "error": null, "partitions": [] }] }] }如果你觉得通过命令查询磁盘信息比较麻烦,你也可以通过 LogIKM 查看MoreKafka专栏持续更新中…(源码、原理、实战、运维、视频、面试视频)
文章目录脚本参数1. 脚本的使用介绍1.1 生成推荐配置脚本1.2. 执行Json文件1.3. 验证2. 副本扩缩2.1 副本扩容2.1.1 计算副本分配方式2.1.2 执行--execute2.1.2 验证--verify2.2 副本缩容3. 分区扩容4. 分区迁移5. 副本跨路径迁移源码解析脚本参数参数 描述 例子--zookeeper 连接zk --zookeeper localhost:2181, localhost:2182--topics-to-move-json-file 指定json文件,文件内容为topic配置 --topics-to-move-json-file config/move-json-file.json Json文件格式如下:--generate 尝试给出副本重分配的策略,该命令并不实际执行 --broker-list 指定具体的BrokerList,用于尝试给出分配策略,与--generate搭配使用 --broker-list 0,1,2,3--reassignment-json-file 指定要重分配的json文件,与--execute搭配使用 json文件格式如下例如:--execute 开始执行重分配任务,与--reassignment-json-file搭配使用 --verify 验证任务是否执行成功,当有使用--throttle限流的话,该命令还会移除限流;该命令很重要,不移除限流对正常的副本之间同步会有影响 --throttle 迁移过程Broker之间现在流程传输的速率,单位 bytes/sec -- throttle 500000--replica-alter-log-dirs-throttle broker内部副本跨路径迁移数据流量限制功能,限制数据拷贝从一个目录到另外一个目录带宽上限 单位 bytes/sec --replica-alter-log-dirs-throttle 100000--disable-rack-aware 关闭机架感知能力,在分配的时候就不参考机架的信息 --bootstrap-server 如果是副本跨路径迁移必须有此参数 1. 脚本的使用介绍该脚本是kafka提供用来重新分配分区的脚本工具;1.1 生成推荐配置脚本关键参数--generate在进行分区副本重分配之前,最好是用下面方式获取一个合理的分配文件;编写move-json-file.json文件; 这个文件就是告知想对哪些Topic进行重新分配的计算{ "topics": [ {"topic": "test_create_topic1"} ], "version": 1 }然后执行下面的脚本,--broker-list "0,1,2,3" 这个参数是你想要分配的Brokers;sh bin/kafka-reassign-partitions.sh --zookeeper xxx:2181 --topics-to-move-json-file config/move-json-file.json --broker-list "0,1,2,3" --generate执行完毕之后会打印Current partition replica assignment//当前副本分配方式 {"version":1,"partitions":[{"topic":"test_create_topic1","partition":2,"replicas":[1],"log_dirs":["any"]},{"topic":"test_create_topic1","partition":1,"replicas":[3],"log_dirs":["any"]},{"topic":"test_create_topic1","partition":0,"replicas":[2],"log_dirs":["any"]}]} Proposed partition reassignment configuration//期望的重新分配方式 {"version":1,"partitions":[{"topic":"test_create_topic1","partition":2,"replicas":[2],"log_dirs":["any"]},{"topic":"test_create_topic1","partition":1,"replicas":[1],"log_dirs":["any"]},{"topic":"test_create_topic1","partition":0,"replicas":[0],"log_dirs":["any"]}]}需求注意的是,此时分区移动尚未开始,它只是告诉你当前的分配和建议。保存当前分配,以防你想要回滚它1.2. 执行Json文件关键参数--execute将上面得到期望的重新分配方式文件保存在一个json文件里面reassignment-json-file.json {"version":1,"partitions":[{"topic":"test_create_topic1","partition":2,"replicas":[2],"log_dirs":["any"]},{"topic":"test_create_topic1","partition":1,"replicas":[1],"log_dirs":["any"]},{"topic":"test_create_topic1","partition":0,"replicas":[0],"log_dirs":["any"]}]} 然后执行sh bin/kafka-reassign-partitions.sh --zookeeper xxxxx:2181 --reassignment-json-file config/reassignment-json-file.json --execute迁移过程注意流量陡增对集群的影响Kafka提供一个broker之间复制传输的流量限制,限制了副本从机器到另一台机器的带宽上限,当重新平衡集群,引导新broker,添加或移除broker时候,这是很有用的。因为它限制了这些密集型的数据操作从而保障了对用户的影响、例如我们上面的迁移操作加一个限流选项-- throttle 50000000> sh bin/kafka-reassign-partitions.sh --zookeeper xxxxx:2181 --reassignment-json-file config/reassignment-json-file.json --execute -- throttle 50000000在后面加上一个—throttle 50000000 参数, 那么执行移动分区的时候,会被限制流量在50000000 B/s加上参数后你可以看到The throttle limit was set to 50000000 B/s Successfully started reassignment of partitions.需要注意的是,如果你迁移的时候包含 副本跨路径迁移(同一个Broker多个路径)那么这个限流措施不会生效,你需要再加上|--replica-alter-log-dirs-throttle 这个限流参数,它限制的是同一个Broker不同路径直接迁移的限流;如果你想在重新平衡期间修改限制,增加吞吐量,以便完成的更快。你可以重新运行execute命令,用相同的reassignment-json-file1.3. 验证关键参数--verify该选项用于检查分区重新分配的状态,同时—throttle流量限制也会被移除掉; 否则可能会导致定期复制操作的流量也受到限制。sh bin/kafka-reassign-partitions.sh --zookeeper xxxx:2181 --reassignment-json-file config/reassignment-json-file.json --verify注意: 当你输入的BrokerId不存在时,该副本的操作会失败,但是不会影响其他的;例如2. 副本扩缩kafka并没有提供一个专门的脚本来支持副本的扩缩, 不像kafka-topic.sh脚本一样,是可以扩分区的; 想要对副本进行扩缩,只能是曲线救国了; 利用kafka-reassign-partitions.sh来重新分配副本2.1 副本扩容假设我们当前的情况是 3分区1副本,为了提供可用性,我想把副本数升到2;2.1.1 计算副本分配方式我们用步骤1.1的 --generate 获取一下当前的分配情况,得到如下json{ "version": 1, "partitions": [{ "topic": "test_create_topic1", "partition": 2, "replicas": [2], "log_dirs": ["any"] }, { "topic": "test_create_topic1", "partition": 1, "replicas": [1], "log_dirs": ["any"] }, { "topic": "test_create_topic1", "partition": 0, "replicas": [0], "log_dirs": ["any"] }] }我们想把所有分区的副本都变成2,那我们只需修改"replicas": []里面的值了,这里面是Broker列表,排在第一个的是Leader; 所以我们根据自己想要的分配规则修改一下json文件就变成如下{ "version": 1, "partitions": [{ "topic": "test_create_topic1", "partition": 2, "replicas": [2,0], "log_dirs": ["any","any"] }, { "topic": "test_create_topic1", "partition": 1, "replicas": [1,2], "log_dirs": ["any","any"] }, { "topic": "test_create_topic1", "partition": 0, "replicas": [0,1], "log_dirs": ["any","any"] }] }注意log_dirs里面的数量要和replicas数量匹配;或者直接把log_dirs选项删除掉; 这个log_dirs是副本跨路径迁移时候的绝对路径2.1.2 执行–execute如果你想在重新平衡期间修改限制,增加吞吐量,以便完成的更快。你可以重新运行execute命令,用相同的reassignment-json-file:2.1.2 验证–verify完事之后,副本数量就增加了;2.2 副本缩容副本缩容跟扩容是一个意思; 当副本分配少于之前的数量时候,多出来的副本会被删除;比如刚刚我新增了一个副本,想重新恢复到一个副本执行下面的json文件{ "version": 1, "partitions": [{ "topic": "test_create_topic1", "partition": 2, "replicas": [2], "log_dirs": ["any"] }, { "topic": "test_create_topic1", "partition": 1, "replicas": [1], "log_dirs": ["any"] }, { "topic": "test_create_topic1", "partition": 0, "replicas": [0], "log_dirs": ["any"] }] }执行之后可以看到其他的副本就被标记为删除了; 一会就会被清理掉用这样一种方式我们虽然是实现了副本的扩缩容, 但是副本的分配需要我们自己来把控好, 要做到负载均衡等等; 那肯定是没有kafka自动帮我们分配比较合理一点; 那么我们有什么好的方法来帮我们给出一个合理分配的Json文件吗?PS:我们之前已经分析过【kafka源码】创建Topic的时候是如何分区和副本的分配规则 那么我们把这样一个分配过程也用同样的规则来分配不就Ok了吗?--generate本质上也是调用了这个方法,AdminUtils.assignReplicasToBrokers(brokerMetadatas, assignment.size, replicas.size)具体的实现操作请看 【kafka思考】最小成本的扩缩容副本设计方案自己写一个工程来实现类似的方法,如果觉得很麻烦,可以直接使用LogIKM 的新增副本功能直接帮你做了这个事情;(未来会实现)3. 分区扩容kafka的分区扩容是 kafka-topis.sh脚本实现的;不支持缩容分区扩容请看 【kafka源码】TopicCommand之alter源码解析(分区扩容)4. 分区迁移分区迁移跟上面同理, 请看 1.1,1.2,1.3 部分;5. 副本跨路径迁移为什么线上Kafka机器各个磁盘间的占用不均匀,经常出现“一边倒”的情形? 这是因为Kafka只保证分区数量在各个磁盘上均匀分布,但它无法知晓每个分区实际占用空间,故很有可能出现某些分区消息数量巨大导致占用大量磁盘空间的情况。在1.1版本之前,用户对此毫无办法,因为1.1之前Kafka只支持分区数据在不同broker间的重分配,而无法做到在同一个broker下的不同磁盘间做重分配。1.1版本正式支持副本在不同路径间的迁移怎么在一台Broker上用多个路径存放分区呢?只需要在配置上接多个文件夹就行了############################# Log Basics ############################# # A comma separated list of directories under which to store log files log.dirs=kafka-logs-5,kafka-logs-6,kafka-logs-7,kafka-logs-8 注意同一个Broker上不同路径只会存放不同的分区,而不会将副本存放在同一个Broker; 不然那副本就没有意义了(容灾)怎么针对跨路径迁移呢?迁移的json文件有一个参数是log_dirs; 默认请求不传的话 它是"log_dirs": ["any"] (这个数组的数量要跟副本保持一致)但是你想实现跨路径迁移,只需要在这里填入绝对路径就行了,例如下面迁移的json文件示例{ "version": 1, "partitions": [{ "topic": "test_create_topic4", "partition": 2, "replicas": [0], "log_dirs": ["/Users/xxxxx/work/IdeaPj/source/kafka/kafka-logs-5"] }, { "topic": "test_create_topic4", "partition": 1, "replicas": [0], "log_dirs": ["/Users/xxxxx/work/IdeaPj/source/kafka/kafka-logs-6"] }] }然后执行脚本sh bin/kafka-reassign-partitions.sh --zookeeper xxxxx --reassignment-json-file config/reassignment-json-file.json --execute --bootstrap-server xxxxx:9092 --replica-alter-log-dirs-throttle 10000注意 --bootstrap-server 在跨路径迁移的情况下,必须传入此参数如果需要限流的话 加上参数|--replica-alter-log-dirs-throttle ; 跟--throttle不一样的是 --replica-alter-log-dirs-throttle限制的是Broker内不同路径的迁移流量;
文章目录1. Leader的epoch过时2. 修改Broker.id出现异常3. 文件加锁失败 Failed to acquire lock on file .lock in4. 发送消息报错 UNKNOWN_TOPIC_OR_PARTITION5. Error while reading checkpoint file xxxx/cleaner-offset-checkpoint6. InconsistentBrokerMetadataException7. log.dir相关异常 Failed to load xxx during broker startup8. meta.properties 版本信息不对日常运维问题排查怎么能够少了滴滴开源的滴滴开源LogiKM一站式Kafka监控与管控平台1. Leader的epoch过时The leader epoch in the request is older than the epoch on the broker -- Partition $topicPartition marked as failed解决方法说明 当前分区的Leader的epoch比Broker的epoch老所以导致follow去fetchleader的时候报错;只要重新发生一下Leader选举就行了;2. 修改Broker.id出现异常 Configured broker.id 0 doesn't match stored broker.id 1 in meta.properties. If you moved your data, make sure your configured broker.id matches. If you intend to create a new broker, you should remove all data in your data directories (log.dirs). 出现这种情况一般是 你可能中途修改了Broker的配置broker.id; 又或者修改了log.dir路径,然后这个路径之前存在;你可以看看log.dir文件夹下面的meta.properties#Wed Jun 23 17:59:02 CST 2021 broker.id=0 version=0 cluster.id=0这里面的内容是之前的配置,你修改了broker.id之后跟这里不一致就抛出异常了;解决方法如果这个log.dir是属于这个Broker的,那么将server.properties 的broker.id修改成更meta.properties一致就行如果你就是想修改一下BrokerId; 那么你需要把meta.properties中的broker.id该了;反正最终是要让meta.properties和server.properties 中的broker.id保持一致;如果这个log.dir是是以前的废旧数据的话,那你还是换一个路径好了;server.properties中的log.dir换个路径修改Broker.id可能出现的异常其实不是很建议修改BrokerId;修改BrokerId可能会存在一些问题,比如当前正在进行数据迁移; zk上的保存的还是原来的 broker.Id; 那就会导致这台Broker迁移失败当你修改的 broker.Id; 那么如果配置了动态配置的话, 就不会生效了;所以你要记得把原来的动态配置添加回来; zk节点是:/config/brokers/{brokerID}othermeta.properties作用其实通过这里你应该也可以理解为什么会存在meta.properties 这个文件; 他就是用来保持这个log.dir之前的Broker.id和cluster.id=0还有version的;因为你server.properties里这个个配置可以随便更改,难免会有出错; kafka会将你的配置跟这个meta.properties信息作对比,提醒你的配置不正确;3. 文件加锁失败 Failed to acquire lock on file .lock in Failed to acquire lock on file .lock in /Users/xxxx/work/IdeaPj/xxx/kafka/kafka-logs-0. A Kafka instance in another process or thread is using this directory. 异常原因:Broker在启动的时候,会把log.dirs加上一个文件锁,以防其他程序对它进行篡改;出现这种异常表示已经有一个程序对文件夹加上了锁了; 所以获取失败;解决方法这个时候你要检查一下,这个Broker是否已经启动过了,或者两个Broke中log.dirs配置了相同的文件夹;如果上面你确定没有问题,那你还可以把相应的文件夹的.lock文件删掉; 强制去掉锁文件; (不建议这样操作)4. 发送消息报错 UNKNOWN_TOPIC_OR_PARTITIONWARN [Producer clientId=console-producer] Error while fetching metadata with correlation id : {test80=UNKNOWN_TOPIC_OR_PARTITION} (org.apache.kafka.clients.NetworkClient)异常原因:发送的TopicPartition不存在; 要么是Topic不存在 要么是发送过去的Partition不存在解决方法检查一下是不是Topic不存在检查一下发送的Partition所在的Broker宕机了,导致发送失败(特别是发送消息的时候指定了分区号比较容易出现这个问题)检查是不是Topic所在的Broker全部宕机了;5. Error while reading checkpoint file xxxx/cleaner-offset-checkpoint Error while reading checkpoint file /Users/shirenchuang/work/IdeaPj/didi_source/kafka/kafka-logs-2/cleaner-offset-checkpoint6. InconsistentBrokerMetadataExceptionkafka.common.InconsistentBrokerMetadataException: BrokerMetadata is not consistent across log.dirs. This could happen if multiple brokers shared a log directory (log.dirs) or partial data was manually copied from another broker. Found: - kafka-logs-0 -> BrokerMetadata(brokerId=0, clusterId=0) - kafka-logs-1 -> BrokerMetadata(brokerId=1, clusterId=0)异常原因:在同一个Broker中,配置了多个log.dirs 日志文件夹,但是却发现这两个文件夹归属于不同的Broker, 那么就会抛出异常;假设配置文件 log.dirs=kafka-logs-1,kafka-logs-0 配置了两个文件夹. 那么启动的时候会去加载这两个文件夹的 meta.properties文件 读取里面的broker.id,cluster.id组成一个brokerMetadataMap对象; 正常情况下, 他们的值肯定是一样的,但是假如一台机器上部署了多个Broker,还想公用同一个dir,那么肯定是不行的;解决方法如果想要配置多个dir,那么找到对应哪个dir是已经被其他Broker使用了, 不用这个dir就行了;7. log.dir相关异常 Failed to load xxx during broker startupFailed to load ${dir.getAbsolutePath} during broker startup异常原因:启动的时候读取文件夹log.dirs文件里面的meta.properties的时候抛IOException,读取失败解决方法查询一下是不是对应的dir中的文件meta.properties有什么异常(是否有权限读取等等)Duplicate log directory found: xxxx异常原因:log.dirs 设置的文件夹重复了;比如: log.dirs=kafka-logs-0,kafka-logs-0解决方法检查一下是不是设置重复了 Found directory /xxxx/kafka/kafka-logs-0/test, 'test' is not in the form of topic-partition or topic-partition.uniqueId-delete (if marked for deletion). Kafka's log directories (and children) should only contain Kafka topic data.异常原因:log.dirs文件夹中存在不符合条件的文件夹,一般里面的文件夹的格式都是 topic-分区号 ,topic-分区号-future ,topic-分区号-delete解决方法自检一下不合格的文件夹8. meta.properties 版本信息不对[2021-07-21 13:38:19,246][ERROR][main]: Failed to create or validate data directory /Users/xxx/kafka/kafka-logs-0 java.io.IOException: Failed to load /Users/xxxx/kafka/kafka-logs-0 during broker startup异常原因:meta.properties 中的version的信息是不是异常了,正常情况下是0;解决方法尝试将 meta.properties 直接删除,启动的时候会重新生成
说明从2.2.4版开始,您可以直接在注释上指定Kafka使用者属性,这些属性将覆盖在使用者工厂中配置的具有相同名称的所有属性。您不能通过这种方式指定group.id和client.id属性。他们将被忽略;可以使用#{…}或属性占位符(${…})在SpEL上配置注释上的大多数属性。比如: @KafkaListener(id = "consumer-id",topics = "SHI_TOPIC1",concurrency = "${listen.concurrency:3}", clientIdPrefix = "myClientId")属性concurrency将会从容器中获取listen.concurrency的值,如果不存在就默认用3@KafkaListener详解id 监听器的id①. 消费者线程命名规则填写:2020-11-19 14:24:15 c.d.b.k.KafkaListeners 120 [INFO] 线程:Thread[consumer-id5-1-C-1,5,main]-groupId:BASE-DEMO consumer-id5 消费没有填写ID:2020-11-19 10:41:26 c.d.b.k.KafkaListeners 137 [INFO] 线程:Thread[org.springframework.kafka.KafkaListenerEndpointContainer#0-0-C-1,5,main] consumer-id7②.在相同容器中的监听器ID不能重复否则会报错Caused by: java.lang.IllegalStateException: Another endpoint is already registered with id③.会覆盖消费者工厂的消费组GroupId假如配置文件属性配置了消费组kafka.consumer.group-id=BASE-DEMO正常情况它是该容器中的默认消费组但是如果设置了 @KafkaListener(id = "consumer-id7", topics = {"SHI_TOPIC3"})那么当前消费者的消费组就是consumer-id7 ;当然如果你不想要他作为groupId的话 可以设置属性idIsGroup = false;那么还是会使用默认的GroupId;④. 如果配置了属性groupId,则其优先级最高 @KafkaListener(id = "consumer-id5",idIsGroup = false,topics = "SHI_TOPIC3",groupId = "groupId-test")例如上面代码中最终这个消费者的消费组GroupId是 “groupId-test”该id属性(如果存在)将用作Kafka消费者group.id属性,并覆盖消费者工厂中的已配置属性(如果存在)您还可以groupId显式设置或将其设置idIsGroup为false,以恢复使用使用者工厂的先前行为group.id。groupId 消费组名指定该消费组的消费组名; 关于消费组名的配置可以看看上面的 id 监听器的id如何获取消费者 group.id在监听器中调用KafkaUtils.getConsumerGroupId()可以获得当前的groupId; 可以在日志中打印出来; 可以知道是哪个客户端消费的;topics 指定要监听哪些topic(与topicPattern、topicPartitions 三选一)可以同时监听多个topics = {"SHI_TOPIC3","SHI_TOPIC4"}topicPattern 匹配Topic进行监听(与topics、topicPartitions 三选一)topicPartitions 显式分区分配可以为监听器配置明确的主题和分区(以及可选的初始偏移量)@KafkaListener(id = "thing2", topicPartitions = { @TopicPartition(topic = "topic1", partitions = { "0", "1" }), @TopicPartition(topic = "topic2", partitions = "0", partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100")) }) public void listen(ConsumerRecord<?, ?> record) { ... }上面例子意思是 监听topic1的0,1分区;监听topic2的第0分区,并且第1分区从offset为100的开始消费;errorHandler 异常处理实现KafkaListenerErrorHandler; 然后做一些异常处理;@Component public class KafkaDefaultListenerErrorHandler implements KafkaListenerErrorHandler { @Override public Object handleError(Message<?> message, ListenerExecutionFailedException exception) { return null; } @Override public Object handleError(Message<?> message, ListenerExecutionFailedException exception, Consumer<?, ?> consumer) { //do someting return null; } }调用的时候 填写beanName;例如errorHandler="kafkaDefaultListenerErrorHandler"containerFactory 监听器工厂指定生成监听器的工厂类;例如我写一个 批量消费的工厂类 /** * 监听器工厂 批量消费 * @return */ @Bean public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>> batchFactory() { ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(kafkaConsumerFactory()); //设置为批量消费,每个批次数量在Kafka配置参数中设置ConsumerConfig.MAX_POLL_RECORDS_CONFIG factory.setBatchListener(true); return factory; }使用containerFactory = "batchFactory"clientIdPrefix 客户端前缀会覆盖消费者工厂的kafka.consumer.client-id属性; 最为前缀后面接 -n n是数字concurrency并发数会覆盖消费者工厂中的concurrency ,这里的并发数就是多线程消费; 比如说单机情况下,你设置了3; 相当于就是启动了3个客户端来分配消费分区;分布式情况 总线程数=concurrency*机器数量; 并不是设置越多越好,具体如何设置请看 属性concurrency的作用及配置(RoundRobinAssignor 、RangeAssignor) /** * 监听器工厂 * @return */ @Bean public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>> concurrencyFactory() { ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(kafkaConsumerFactory()); factory.setConcurrency(6); return factory; } @KafkaListener(id = "consumer-id5",idIsGroup = false,topics = "SHI_TOPIC3", containerFactory = "concurrencyFactory",concurrency = "1)虽然使用的工厂是concurrencyFactory(concurrency配置了6); 但是他最终生成的监听器数量 是1;properties 配置其他属性kafka中的属性看org.apache.kafka.clients.consumer.ConsumerConfig ;同名的都可以修改掉;用法 @KafkaListener(id = "consumer-id5",idIsGroup = false,topics = "SHI_TOPIC3", containerFactory = "concurrencyFactory",concurrency = "1" , clientIdPrefix = "myClientId5",groupId = "groupId-test", properties = { "enable.auto.commit:false","max.poll.interval.ms:6000" },errorHandler="kafkaDefaultListenerErrorHandler")@KafkaListener使用KafkaListenerEndpointRegistry @Autowired private KafkaListenerEndpointRegistry registry; //.... 获取所有注册的监听器 registry.getAllListenerContainers(); 设置入参验证器当您将Spring Boot与验证启动器一起使用时,将LocalValidatorFactoryBean自动配置:如下@Configuration @EnableKafka public class Config implements KafkaListenerConfigurer { @Autowired private LocalValidatorFactoryBean validator; ... @Override public void configureKafkaListeners(KafkaListenerEndpointRegistrar registrar) { registrar.setValidator(this.validator); } }使用@KafkaListener(id="validated", topics = "annotated35", errorHandler = "validationErrorHandler", containerFactory = "kafkaJsonListenerContainerFactory") public void validatedListener(@Payload @Valid ValidatedClass val) { ... } @Bean public KafkaListenerErrorHandler validationErrorHandler() { return (m, e) -> { ... }; }
之前在写 多版本并行开发测试解决方案 的时候 占了个坑,今天来补上;这篇文章主要讲一下 kafka的服务复用与隔离;主要解决的问题是,在多个迭代环境下; 让消息的提供者和消费者都能正确的发出和消费;这个比dubbo的服务路由与隔离更复杂一点1.问题描述概念说明:稳定版本: ABC 属于全局共用的一套稳定服务;迭代版本: A1 C1 C2 属于他们对应系统的迭代版本, 比如针对A系统进行需求改动,部署一套新的迭代服务A1;要求: mq提供者服务提供出去的消息尽量让 相同版本的消费者进行消费;1.1. 入口是稳定服务上图, 假设入口是 稳定服务A ,发出消息; 那么消息链路中互相消费的就是 ABC ;跟迭代版本没啥事1.2.入口是迭代服务上图,假设入口是 迭代服务A1 发出消息; 则整个链路中尽量让相同迭代版本的服务去消费;A1发消息A1发了消息; 找B系统发现只有稳定的B,没有迭代版本,那么就让B消费;A1发了消息;C也是有订阅的,然后发现C系统有迭代C1,跟A1版本相同,则让C1消费; C和C2都不消费;B发消息B消费了A1过来的消息后也发出了消息; A系统有消费,那么这个时候B发出的消息应该让A1消费而不是A;同理, 也应该是C1消费而不是C或者C2C1发消息C1发消息 让A1消费;C1发消息 让B消费;1.3.dubbo服务传入迭代版本上图D1调用了B的dubbo接口并且传递了版本号; B此时发出消息也是属于迭代消息; 跟2一样;2.解决方案我们在之前的文章中有讲解如何 在dubbo中实现这样的功能; 通过spi给dubbo重新根据version来进行路由;但是在kafka中,并没有这消费者路由这么一回事,那么也就无法控制哪个服务去消费这条消息;那么下面,我给出自己的一些解决方案,如果觉得有问题,欢迎批评指正;设计方案:方案关键步骤:消息发送的时候,在Header上加上Version信息发送消息 将消息发2条出去,消息体相同,但是Topic不同; 迭代消息的Topic加上前缀 VERSION:对应的版本_迭代服务启动的时候用javaagent修改所有监听的Topic; 加上前缀 VERSION:对应的版本_迭代服务消费对应的迭代消息稳定服务 是否需要消费消息 需要判断当前消息Header不携带Version 则直接消费当前消息Header携带Version,再判断是否有对应的迭代服务存在;有则不消费,无则直接消费消费消息时,需要把Version保存到 ThreadLocal中; 以便进行链路流转使用ThreadLocal的时候,在线程池的情况下,值传递会有问题. 解决方案 用javaagent 方式使用TransmittableThreadLocal全程代码0侵入;kafka的两个拦截器的和配置 都通过Javaagent来就行增强如何判断迭代服务是否存在上面的设计方案中,在kafka consumner 拦截器 判断是否需要消费的时候 写了两种方式1. 方式一:获取当前消息的消费组currentGroupId = KafkaUtils.getConsumerGroupId()获取所有消费组adminClient.listConsumerGroups()然后再所有消费组中查找有没有 VERSION:1_currentGroupId 的消费组;如果有,则说明该消息会被迭代服务进行消费. 稳定环境就不用消费了;当前还有一部不可少,就是如何让迭代服务的 所有消费组名都加上前缀当然还是通过javaagent 去增强咯, 找到合适修改点,修改掉消费组名;合适的修改点自然是配置消费消费组名的地方; 有统一的消费组名; 每个Listener也可以配置单独的消费组名;找到Listener注解就行增强;缺点: 这种方式有一个缺点就是 如果迭代服务刚好宕机了那么 消息就会问稳定服务消费了;2.方式二(推荐)读取一个外部配置,这个配置维护了哪个服务是有迭代服务的;这样就很方便了;缺点: 就是需要维护这么一个配置优点: 规避了方式一的缺点; 也不需要用javaagent去修改消费组名称;3.需要注意的问题我们在传递version的时候,入口一般都是http接口;但是如果入口不是http,是系统内部呢,那这样外面的版本信息就传不进来了;说一个在出行行业 的情景A: 是叫单服务B: 是派单服务C: 是订单/司机服务在一个需求中, A C都有改动; B没有改动; 就有迭代服务A1 C1;假设他们使用MQ交流的;我们期望的是下面流转A1 ---->B----->C1但是A1告诉了B有订单进来了, B会把A1给的信息存到redis中; B有一个线程一直在不停从redis中捞取数据进行和司机的匹配;匹配成功了之后 再发消息出去 匹配成功了;B的这条链路就断了; B存redis之后,就没有下一步操作了, ThreadLocal中的version也就没有了; B的匹配线程获取到的是 稳定版本;自然匹配成功发出去的消息就是 稳定消息;那么接收到的不是C1 而是 C了;如何解决这类型的问题;这种情况就应该将B也弄一个迭代版本B1;那么流转路径就是A1-B1-C1 ;这样就是正确的了;还要注意: DB隔离;
目录concurrency属性作用什么情况下设置concurrency,以及设置多少RoundRobinAssignor 和 RangeAssignor 作用不同配置的实验分析分区数3|concurrency = 1|启动一个客户端(单机)分区数3|concurrency = 1|启动2个客户端(分布式模式)分区数3|concurrency = 3|启动一个客户端分区数3|concurrency = 3|启动2个客户端(分布式模式)批量消费concurrency属性作用concurrency默认是1;container.setConcurrency(3)表示创建三个KafkaMessageListenerContainer实例。一个KafkaMessageListenerContainer实例分配一个分区进行消费;如果设置为1的情况下, 这一个实例消费Topic的所有分区;如果设置多个,那么会平均分配所有分区;如果实例>分区数; 那么空出来的实例会浪费掉;如果实例<=分区数 那么会有一部分实例消费多个实例,但也是均衡分配的如果在分布式情况下, 那么总的KafkaMessageListenerContainer实例数= 服务器机器数量*concurrency ;什么情况下设置concurrency,以及设置多少这个得看我们给Topic设置的分区数量; 总的来说就是 机器数量*concurrency <= 分区数例如分区=3; 而且同时有3台机器 ,那么concurrency=1就行了; 设置多了就会浪费资源;、例如分区=9; 只有3台机器;那么可以concurrency=3 ; 每台机器3个消费者连接3个分区; 那么你可能会问我们concurrency=1不也可以吗; 反正都是一台机器消费3个分区;话是没有错; 但是他们的差别在 一个线程消费3个分区和 3个线程消费3个分区 , 单线程和多线程你选哪个RoundRobinAssignor 和 RangeAssignor 作用默认情况下 spring.kafka.consumer.properties.partition.assignment.strategy=\ org.apache.kafka.clients.consumer.RangeAssignor假如如下情况,同时监听了2个Topic; 并且每个topic的分区都是3; concurrency设置为6; @KafkaListener(id = "consumer-id6", topics = {"SHI_TOPIC3","SHI_TOPIC4"}, containerFactory = "concurrencyFactory" , clientIdPrefix = "myClientId6") public void consumer6(List<?> list) { StringBuffer sb = new StringBuffer(); list.forEach((l)->{ sb.append("|msg:").append(l); }); log.info("线程:{} consumer-id6 消费->{}",Thread.currentThread(),sb); } 那么你期望的是不是 2*3=6 刚好6个线程;一个线程分配一个分区; 那么我们运行看看结果看上图中,我们发现并没有按照我们的预期去做; 有三个消费者其实是闲置状态的; 只有另外的3个消费者负责了2个Topic的总共6个分区; 因为默认的分配策略是 spring.kafka.consumer.properties.partition.assignment.strategy=\ org.apache.kafka.clients.consumer.RangeAssignor ;如果想达到我们的预期;那你可以修改策略; spring.kafka.consumer.properties.partition.assignment.strategy=\ org.apache.kafka.clients.consumer.RoundRobinAssignor修改之后每个线程分配一个分区不同配置的实验分析分区数3|concurrency = 1|启动一个客户端(单机)创建了名为 SHI_TOPIC3并且分区数为3的Topic代码启动,设置concurrency = 1, 只启动一个客户端;启动日志2020-11-18 17:14:42 o.a.k.c.c.i.ConsumerCoordinator 611 [INFO] [Consumer clientId=myClientId5-0, groupId=consumer-id5] Finished assignment for group at generation 6: {myClientId5-0-a273480d-2370-49e5-9187-ed10fe6dcf51= Assignment(partitions=[SHI_TOPIC3-0, SHI_TOPIC3-1, SHI_TOPIC3-2])} 2020-11-18 17:14:42 o.s.k.l.KafkaMessageListenerContainer 292 [INFO] consumer-id5: partitions assigned: [SHI_TOPIC3-2, SHI_TOPIC3-1, SHI_TOPIC3-0]可以看到这个客户端myClientId5-0-a273480d-2370-49e5-9187-ed10fe6dcf51 被分配了3个分区SHI_TOPIC3-0, SHI_TOPIC3-1, SHI_TOPIC3-2;消费日志2020-11-18 17:14:45 c.d.b.k.KafkaListeners 109 [INFO] 线程:Thread[consumer-id5-0-C-1,5,main] consumer-id5 消费->record:ConsumerRecord(topic = SHI_TOPIC3, partition = 2, leaderEpoch = 0, offset = 0, CreateTime = 1605690882681, serialized key size = 13, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = 1605690882615, value = 我是data0),value:我是data0,partition:2,offset:0 2020-11-18 17:14:45 c.d.b.k.KafkaListeners 109 [INFO] 线程:Thread[consumer-id5-0-C-1,5,main] consumer-id5 消费->record:ConsumerRecord(topic = SHI_TOPIC3, partition = 2, leaderEpoch = 0, offset = 1, CreateTime = 1605690882705, serialized key size = 13, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = 1605690882705, value = 我是data4),value:我是data4,partition:2,offset:1 2020-11-18 17:14:45 c.d.b.k.KafkaListeners 109 [INFO] 线程:Thread[consumer-id5-0-C-1,5,main] consumer-id5 消费->record:ConsumerRecord(topic = SHI_TOPIC3, partition = 2, leaderEpoch = 0, offset = 2, CreateTime = 1605690882705, serialized key size = 13, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = 1605690882705, value = 我是data5),value:我是data5,partition:2,offset:2 2020-11-18 17:14:45 c.d.b.k.KafkaListeners 109 [INFO] 线程:Thread[consumer-id5-0-C-1,5,main] consumer-id5 消费->record:ConsumerRecord(topic = SHI_TOPIC3, partition = 2, leaderEpoch = 0, offset = 3, CreateTime = 1605690882706, serialized key size = 13, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = 1605690882705, value = 我是data6),value:我是data6,partition:2,offset:3 2020-11-18 17:14:45 c.d.b.k.KafkaListeners 109 [INFO] 线程:Thread[consumer-id5-0-C-1,5,main] consumer-id5 消费->record:ConsumerRecord(topic = SHI_TOPIC3, partition = 2, leaderEpoch = 0, offset = 4, CreateTime = 1605690882706, serialized key size = 13, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = 1605690882706, value = 我是data7),value:我是data7,partition:2,offset:4 .....可以看到线程都是同一个 Thread[consumer-id5-0-C-1,5,main] ; 说明的问题就是 在消费的时候是单线程消费的,并且还是一个线程去消费 3个分区的数据; 又涉及到切换消费分区的问题;查询这个消费组的消费情况;也证实只有一个消费者myClientId5-0-a273480d-2370-49e5-9187-ed10fe6dcf51在消费3个分区的数据;分区数3|concurrency = 1|启动2个客户端(分布式模式)第一个客户端不动,继续运行, 然后启动第二个客户端第一个客户端发生的变化 2020-11-18 17:34:24 o.a.k.c.c.i.ConsumerCoordinator 611 [INFO] [Consumer clientId=myClientId5-0, groupId=consumer-id5] Finished assignment for group at generation 9: {myClientId5-0-66a81e88-d924-4890-8b8e-2c6960ed0704=Assignment(partitions=[SHI_TOPIC3-2]), myClientId5-0-31c9a99f-5735-4a1d-b537-95bc5ab4533f=Assignment(partitions=[SHI_TOPIC3-0, SHI_TOPIC3-1])}第一个客户端进行了 再平衡 ; 因为多了第二个可以分担压力进行消费; 可以看到把SHI_TOPIC3-2平衡出去了第二个客户端的日志 2020-11-18 17:34:24 o.a.k.c.Metadata 277 [INFO] [Consumer clientId=myClientId5-0, groupId=consumer-id5] Cluster ID: O304VSOeSEyporzbs5AITA 2020-11-18 17:34:24 o.a.k.c.c.i.AbstractCoordinator 797 [INFO] [Consumer clientId=myClientId5-0, groupId=consumer-id5] Discovered group coordinator xxxxxx:9092 (id: 2147483645 rack: null) 2020-11-18 17:34:24 o.a.k.c.c.i.AbstractCoordinator 552 [INFO] [Consumer clientId=myClientId5-0, groupId=consumer-id5] (Re-)joining group 2020-11-18 17:34:25 o.s.k.l.KafkaMessageListenerContainer 292 [INFO] consumer-id5: partitions assigned: [SHI_TOPIC3-2]查询客户端消费情况可以看到第二个客户端分配到了SHI_TOPIC3--2的分区进行消费; 并且是单线程消费;分区数3|concurrency = 3|启动一个客户端客户端日志2020-11-18 17:50:42 o.a.k.c.c.i.ConsumerCoordinator 273 [INFO] [Consumer clientId=myClientId5-1, groupId=consumer-id5] Adding newly assigned partitions: SHI_TOPIC3-1 2020-11-18 17:50:42 o.a.k.c.c.i.ConsumerCoordinator 273 [INFO] [Consumer clientId=myClientId5-0, groupId=consumer-id5] Adding newly assigned partitions: SHI_TOPIC3-0 2020-11-18 17:50:42 o.a.k.c.c.i.ConsumerCoordinator 273 [INFO] [Consumer clientId=myClientId5-2, groupId=consumer-id5] Adding newly assigned partitions: SHI_TOPIC3-2 2020-11-18 17:50:42 o.s.k.l.KafkaMessageListenerContainer 292 [INFO] consumer-id5: partitions assigned: [SHI_TOPIC3-2] 2020-11-18 17:50:42 o.s.k.l.KafkaMessageListenerContainer 292 [INFO] consumer-id5: partitions assigned: [SHI_TOPIC3-0] 2020-11-18 17:50:42 o.s.k.l.KafkaMessageListenerContainer 292 [INFO] consumer-id5: partitions assigned: [SHI_TOPIC3-1] 上面日志显示 创建了3个消费者,他们都属于同一个消费组groupId=consumer-id5,3个分区刚好3个消费者一人一个分区平均分配;客户端日志 2020-11-18 17:50:45 c.d.b.k.KafkaListeners 109 [INFO] 线程:Thread[consumer-id5-0-C-1,5,main] consumer-id5 消费->record:ConsumerRecord(topic = SHI_TOPIC3, partition = 0, leaderEpoch = 0, offset = 11, CreateTime = 1605693042720, serialized key size = 13, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = 1605693042432, value = 我是data0),value:我是data0,partition:0,offset:11 2020-11-18 17:50:45 c.d.b.k.KafkaListeners 109 [INFO] 线程:Thread[consumer-id5-2-C-1,5,main] consumer-id5 消费->record:ConsumerRecord(topic = SHI_TOPIC3, partition = 2, leaderEpoch = 0, offset = 12, CreateTime = 1605693042751, serialized key size = 13, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = 1605693042750, value = 我是data1),value:我是data1,partition:2,offset:12 2020-11-18 17:50:45 c.d.b.k.KafkaListeners 109 [INFO] 线程:Thread[consumer-id5-1-C-1,5,main] consumer-id5 消费->record:ConsumerRecord(topic = SHI_TOPIC3, partition = 1, leaderEpoch = 0, offset = 17, CreateTime = 1605693042757, serialized key size = 13, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = 1605693042757, value = 我是data7),value:我是data7,partition:1,offset:17每个消费者都是单线程,一个线程消费一个分区分区数3|concurrency = 3|启动2个客户端(分布式模式)启动第一个客户端启动第二个客户端启动第二个客户端之后就发生了 再分配rebalance; 可以看到,总共就有6个消费者, 但是其中的3个都是处于空闲状态;因为一个分区最多只能有一个分区来进行消费;批量消费 /** * 监听器工厂 批量消费 * @return */ @Bean public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>> concurrencyFactory() { ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(kafkaConsumerFactory()); factory.setConcurrency(1); //设置为批量消费,每个批次数量在Kafka配置参数中设置ConsumerConfig.MAX_POLL_RECORDS_CONFIG factory.setBatchListener(true); return factory; }配置文件设置 批量的最大条数kafka.consumer.max-poll-records = 20消费 @KafkaListener(id = "consumer-id6", topics = "SHI_TOPIC3", containerFactory = "concurrencyFactory" , clientIdPrefix = "myClientId6") public void consumer6(List<?> list) { StringBuffer sb = new StringBuffer(); list.forEach((l)->{ sb.append("|msg:").append(l); }); log.info("线程:{} consumer-id6 消费->{}",Thread.currentThread(),sb); }
技术交流有想进滴滴LogI开源用户群的加我个人微信: jjdlmn_ 进群(备注:进群)群里面主要交流 kakfa、es、agent、LogI-kafka-manager、等等相关技术;群内有专人解答你的问题对~ 相关技术领域的解答人员都有; 你问的问题都会得到回应有想进 滴滴LogI开源用户群 的加我个人微信: jjdlmn_ 进群(备注:进群)群里面主要交流 kakfa、es、agent、以及其他技术群内有专人解答疑问,你所问的都能得到回应CORRUPT_MESSAGE这个错误一般是压缩策略为cleanup.policy=compact的情况下,key不能为空o.a.k.c.p.i.Sender 595 [WARN] [Producer clientId=producer-1] Got error produce response with correlation id 131 on topic-partition SHI_TOPIC1-0, retrying (2147483521 attempts left). Error: CORRUPT_MESSAGE查看一下压缩策略bin/kafka-topics.sh --describe --zookeeper xxxx:2181 --topic SHI_TOPIC1 Topic:SHI_TOPIC1 PartitionCount:1 ReplicationFactor:1 Configs:cleanup.policy=compact Topic: SHI_TOPIC1 Partition: 0 Leader: 0 Replicas: 0 Isr: 0 Configs:cleanup.policy=compact :然后再检查一下自己发送消息的时候是不是没有传 key参考链接问题堆栈信息org.springframework.kafka.listener.ListenerExecutionFailedException: invokeHandler Failed; nested exception is java.lang.IllegalStateException: No Acknowledgment available as an argument, the listener container must have a MANUAL AckMode to populate the Acknowledgment.; nested exception is java.lang.IllegalStateException: No Acknowledgment available as an argument, the listener container must have a MANUAL AckMode to populate the Acknowledgment. 问题原因解决方案问题堆栈信息 Failed to start bean 'org.springframework.kafka.config.internalKafkaListenerEndpointRegistry'; nested exception is java.lang.IllegalStateException: Consumer cannot be configured for auto commit for ackMode MANUAL_IMMEDIATE问题原因不能再配置中既配置kafka.consumer.enable-auto-commit=true 自动提交; 然后又在监听器中使用手动提交例如:kafka.consumer.enable-auto-commit=true @Autowired private ConsumerFactory consumerFactory; @Bean public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>> kafkaManualAckListenerContainerFactory() { ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(consumerFactory); //设置提交偏移量的方式 当Acknowledgment.acknowledge()侦听器调用该方法时,立即提交偏移量 factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE); return factory; } /** * 手动ack 提交记录 * @param data * @param ack * @throws InterruptedException */ @KafkaListener(id = "consumer-id2",topics = "SHI_TOPIC1",concurrency = "1", clientIdPrefix = "myClientId2",containerFactory = "kafkaManualAckListenerContainerFactory") public void consumer2(String data, Acknowledgment ack) { log.info("consumer-id2-手动ack,提交记录,data:{}",data); ack.acknowledge(); }解决方法:将自动提交关掉,或者去掉手动提交;如果你想他们都同时存在,某些情况自动提交;某些情况手动提交; 那你创建 一个新的consumerFactory 将它的是否自动提交设置为false;比如 @Configuration @EnableKafka public class KafkaConfig { @Autowired private KafkaProperties properties; /** * 创建一个新的消费者工厂 * 创建多个工厂的时候 SpringBoot就不会自动帮忙创建工厂了;所以默认的还是自己创建一下 * @return */ @Bean public ConsumerFactory<Object, Object> kafkaConsumerFactory() { Map<String, Object> map = properties.buildConsumerProperties(); DefaultKafkaConsumerFactory<Object, Object> factory = new DefaultKafkaConsumerFactory<>( map); return factory; } /** * 创建一个新的消费者工厂 * 但是修改为不自动提交 * * @return */ @Bean public ConsumerFactory<Object, Object> kafkaManualConsumerFactory() { Map<String, Object> map = properties.buildConsumerProperties(); map.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false); DefaultKafkaConsumerFactory<Object, Object> factory = new DefaultKafkaConsumerFactory<>( map); return factory; } /** * 手动提交的监听器工厂 (使用的消费组工厂必须 kafka.consumer.enable-auto-commit = false) * @return */ @Bean public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>> kafkaManualAckListenerContainerFactory() { ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(kafkaManualConsumerFactory()); //设置提交偏移量的方式 当Acknowledgment.acknowledge()侦听器调用该方法时,立即提交偏移量 factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE); return factory; } /** * 监听器工厂 批量消费 * @return */ @Bean public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>> batchFactory() { ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(kafkaConsumerFactory()); factory.setBatchListener(true); return factory; } }消费者监听的时候 指定对应的 容器工厂就行了kafkaManualAckListenerContainerFactory /** * 手动ack 提交记录 * @param data * @param ack * @throws InterruptedException */ @KafkaListener(id = "consumer-id2",topics = "SHI_TOPIC1",concurrency = "1", clientIdPrefix = "myClientId2",containerFactory = "kafkaManualAckListenerContainerFactory") public void consumer2(String data, Acknowledgment ack) { log.info("consumer-id2-手动ack,提交记录,data:{}",data); ack.acknowledge(); } 问题堆栈信息[WARN] Error registering AppInfo mbean javax.management.InstanceAlreadyExistsException: kafka.consumer:type=app-info,id=myClientId-3问题原因官网描述The client.id property (if set) is appended with -n where n is the consumer instance that corresponds to the concurrency. This is required to provide unique names for MBeans when JMX is enabled.意思是这个id在JMX中注册需要id名唯一;不要重复了;解决方法:将监听器的id修改掉为唯一值 或者 消费者的全局配置属性中不要知道 client-id ;则系统会自动创建不重复的client-id
目录位图基本使用SETBIT key 索引 值0/1GETBIT key 索引通过SET 一次设置单个位图的所有位BITFIELD 设置多个位BITCOUNTBITPOS 查找指定值为0或1的第一位。位图的使用场景记录用户一年的签到情况实时统计在线人数和某个用户的在线状态BITCOUNT统计大数据量的性能问题位图位图的最大优点之一是,它们在存储信息时通常可以节省大量空间位图不是一个真实的数据类型,而是定义在字符串类型上的面向位的操作的集合。由于字符串类型是二进制安全的二进制大对象,并且最大长度是 512MB,适合于设置 2^32^个不同的位。位操作分为两组:常量时间单个位的操作,像设置一个位为 1 或者 0,或者获取该位的值。对一组位的操作,例如计算指定范围位的置位数量。1字节=1B=2^3b=8位1KB=2^10^B1MB=2^10^KB512MB=2^9^ X 2^10^KB X 2^10^B X 2^3^b = 2^32^b ;基本使用我们上面知道了 位图其实是一个字符串; 那么其实我们也可以用 get set来进行操作的;位图操作的是二进制;SETBIT key 索引 值0/1SETBIT 是设置二进制索引上的某个值为0或者还是1; 如果设置了高索引位,则其余位置自动填充为0;刚刚设置的 key 为 f 在 第1、2、7 号为设置了1 其余的都是0;二进制表现形式是 0110 0001当然我们知道位图是用字符串来存的; 可以用get命令来看看 输出来的是a; 因为对于的ASCII码就是 a注意:一般我们看二进制的时候可能习惯的是从右边往左边看, 但是索引的话还是从左边往右边数的; 最左边的是以0号索引开始;GETBIT key 索引这里索引是位数索引127.0.0.1:6379> GETBIT f 1 (integer) 1 127.0.0.1:6379> GETBIT f 0 (integer) 0通过SET 一次设置单个位图的所有位例如我们上面设置的位图 f; 我们设置的时候只需了3次命令; 如果我们知道这个值是多少 可以通过SET来直接设置;比如127.0.0.1:6379> set g a OK 127.0.0.1:6379> get g "a" 127.0.0.1:6379> GETBIT g 1 (integer) 1 127.0.0.1:6379> GETBIT g 0 (integer) 0BITFIELD 设置多个位bitfield 有三个子指令,分别是get/set/incrby,它们都可以对指定位片段进行读写,但是最多只能处理 64 个连续的位,如果超过 64 位,就得使用多个子指令,bitfield 可以一次执行多个子指令BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]127.0.0.1:6379> BITFIELD mykey2 set u1 1 1 set u1 2 1 set u1 7 1 1) (integer) 0 2) (integer) 0 3) (integer) 0 127.0.0.1:6379> get mykey2 "a"BITCOUNT计算字符串中的设置位数(填充计数) 报告设置为1的位数。时间复杂度O(N)BITCOUNT key [start end] #start和end参数指的是字节的索引 不是位的索引例如设置一个字符串abcde127.0.0.1:6379> set mykey abcde OK 127.0.0.1:6379> get mykey "abcde"127.0.0.1:6379> BITCOUNT mykey 0 0 //是计算第一个字符 a的位数 (integer) 3 127.0.0.1:6379> BITCOUNT mykey 0 1 //是计算前两个个字符 ab的位数 (integer) 6 127.0.0.1:6379> BITCOUNT mykey 0 2 (integer) 10 127.0.0.1:6379> BITCOUNT mykey 1 1 //是计算第2个字符 b的位数 (integer) 3BITPOS 查找指定值为0或1的第一位。BITPOS key bit [start] [end] #start和end参数指的是字节的索引 不是位的索引还是value=abcde的值他们的二进制分别为a=0110 0001 b=0110 0010BITPOS mykey 1 0 0 表示找第一个字符a的第一个位是1的索引;那么a的第一个为是1的所以是1127.0.0.1:6379> BITPOS mykey 1 0 0 (integer) 1BITPOS mykey 1 1 1 表示找第二个字符b的第一个位是1的索引;a=0110 0001 b=0110 0010 我们自己数一下也就值得索引在9位127.0.0.1:6379> BITPOS mykey 1 1 1 (integer) 9位图的使用场景记录用户一年的签到情况假如有这么一个需求记录每个用户的一年中每天的签到情况统计某个时间段 用户的签到天数可以查询某个时间段的签到情况想要实现上面的需求. 最笨最笨的方法是当用户签到了就在数据库中插入一条数据; 然后需要的时候再查询出来;那么上面的需求也能满足;但是这种方式就太浪费内存了;一个用户一年365天就有最多365条数据;那么假如有1亿个用户 这数据是很庞大的; 当然我们还是有很多聪明的方式来解决这个问题;这里就不讨论了;我们直接讨论如何用redis中的位图来实现;一年365天的签到情况;只有 签到了或者没签到两种情况;很适合用位图 0/1来做;一年只需要 365位就足够记录一个用户的签到情况了; 365位,只需要46 个字节 (一个字节有8位) 就可以完全容纳下,这就大大节约了存储空间。可以设置功能上线当天比如 2020-1-1为索引 0; 后面签到的时候日期做一个差值就可以算出来位数了;查询某个时间段的签到情况redis中并没有批量查询的位图的命令;只有单个查询getbit ,所以只能一个个执行; 为了减少网络开销; 可以通过管道 或者写lua脚本来批量查询统计 用户的签到总天数BITCOUNT uidkey 0 0BITCOUNT统计区间范围BITCOUNT key [start end] #start和end参数指的是字节的索引 不是位的索引像这种统计区间范围的还真不是很好统计; 因为start和end参数指的是字节的索引 不是位的索引所以要做一些处理如上图所示 如何统计上面位索引5-25中的数据呢?那么我们首先把最大和最小所以计算出来 他们分别在哪个字节索引中;因为一个字节索引包含了8个位索引所以很好计算出来;5%8 取模运算 = 0;25%8取模运算 = 3位索引为5的在 字节索引为0的位图中位索引为25的在字节索引为3的位图中先去掉这首位字节 然后统计中间的位图BITCOUNT key 1 2 得到结果4再单独计算首尾的位数位索引5 占用后面的 5 6 7 三个位 用getbit一个个查询出来为1位索引25只占用 24 25 两个位 用getbit一个个查询出来为2三个一起加起来就行了 4+1+2 = 7;实时统计在线人数和某个用户的在线状态如果只是实时统计在线人数我们可能直接用 redis中的 incr 就可以很方便的统计;但是如果我们还需要记录每个用户是否在线呢?那么一般情况可能 每个用户id作为key 是否在线作为value存储; 那么这样也不是不可以但是就是比较占用内存也没有什么必要那么通过位图来做就很方便和节约空间了每个用户占用一位; 就算用一亿个用户 那么占用的内存大概在100000000/8b/1024B/1024MB 约等于 12MB ;查询某个用户在线状态用getbit key 索引就行了统计在线人数就更简单了 BITCOUNT那么我们来检测一下占用的内存是不是这样的;我们开启实时检测内存使用状态[root@t]# /usr/local/bin/redis-cli -r -1 -i 1 INFO |grep rss_human used_memory_rss_human:7.72M随时间监视RSS内存大小然后设置一下某个某个key位图127.0.0.1:6379> SETBIT bigbit 1 1 (integer) 0设置完了之后 可以看到内存有变化从7.72->7.73 ;接下来我们把第一亿位的索引也设置为1(这样做的目的是让这个key直接占用1亿个位)127.0.0.1:6379> SETBIT bigbit 100000000 1 (integer) 0设置完成之后可以看到内存马上就要变化了从7.73->20.92 跟我们计算的大概12MB左右;BITCOUNT统计大数据量的性能问题在上面的例子中, 一亿位的数据量使用 BITCOUNT进行统计;BITCOUNT 复杂度是O(N) ; 像get操作是O(1);如果数据特别大的话可能会有性能问题; 官网是这样子说的:在内存在456字节大小的时候,BITCOUNT仍然与任何其他O(1) Redis命令(如GET或INCR )一样快。当位图很大时,有两种选择:取一个单独的密钥,该密钥在每次修改位图时都会递增。使用小的Redis Lua脚本可以非常高效和原子。使用BITCOUNT 开始和结束 可选参数递增地运行位图,在客户端积累结果,并可选地将结果缓存到密钥中。
目录在Centos中安装Redis安装步骤安装可能出现的问题redis-cli,Redis命令行界面客户端回复结果作为其他的输出主机,端口,密码和数据库统计从其他程序获取内容当做redis的输入连续运行相同的命令随时间监视RSS内存大小批量操作如何批量删除指定的数据连续统计模式扫描大键获取按键列表监控Redis中的操作命令RDB文件的远程备份执行LRU模拟如果批量删除 带\n后缀的key参考文档在Centos中安装Redis安装步骤先创建一个文件夹用于存放redismkdir /data/redis & cd /data/redis1.使用以下命令下载,提取和编译Redis: wget https://download.redis.io/releases/redis-6.0.9.tar.gz tar xzf redis-6.0.9.tar.gz cd redis-6.0.9 make make test最新稳定版本请看 https://redis.io/download2. 配置在make成功以后,会在src目录下多出一些可执行文件:redis-server,redis-cli等等。方便使用用cp命令复制到usr目录下运行。cp src/redis-server /usr/local/bin/ cp src/redis-cli /usr/local/bin/ cp src/redis-sentinel /usr/local/bin/然后新建目录,存放配置文件 cd /data/redis/ 创建一个6379的文件夹 mkdir 6379 将配置模板拷贝到6379中 cp redis-6.0.9/redis.conf 6379 cd 6379 mkdir log mkdir run3.修改配置文件redis.confvim /data/redis/6379/redis.conf修改一些参数daemonize yes ## 后台运行 pidfile /data/redis/run/redis_6379.pid logfile /data/redis/log/redis_6379.log dir /data/redis/63794.启动redis/usr/local/bin/redis-server /data/redis/6379/redis.conf5. 使用客户端/usr/local/bin/redis-cli安装可能出现的问题问题1make[3]: 进入目录“/data/redis/redis-6.0.9/deps/hiredis” cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwrite-strings -Wno-missing-field-initializers -g -ggdb net.c make[3]: cc:命令未找到 make[3]: *** [net.o] 错误 127 make[3]: 离开目录“/data/redis/redis-6.0.9/deps/hiredis” make[2]: *** [hiredis] 错误异常原因:没有安装gcc解决方案:yum install gcc-c++上面安装完gcc之后执行一下make distclean清理 一下 再执行make;问题二server.c: 在函数‘redisSetProcTitle’中: server.c:5052:15: 错误:‘struct redisServer’没有名为‘cluster_enabled’的成员 if (server.cluster_enabled) server_mode = " [cluster]"; ^ server.c:5053:20: 错误:‘struct redisServer’没有名为‘sentinel_mode’的成员 else if (server.sentinel_mode) server_mode = " [sentinel]"; ^ server.c:5057:15: 错误:‘struct redisServer’没有名为‘bindaddr_count’的成员 server.bindaddr_count ? server.bindaddr[0] : "*", ^ server.c:5057:39: 错误:‘struct redisServer’没有名为‘bindaddr’的成员 server.bindaddr_count ? server.bindaddr[0] : "*", ^ server.c:5058:15: 错误:‘struct redisServer’没有名为‘port’的成员 server.port ? server.port : server.tls_port, ^ server.c:5058:29: 错误:‘struct redisServer’没有名为‘port’的成员 server.port ? server.port : server.tls_port, ^ server.c:5058:43: 错误:‘struct redisServer’没有名为‘tls_port’的成员 server.port ? server.port : server.tls_port,问题原因: redis和gcc版本问题解决方法 升级一下gcc版本yum -y install centos-release-scl && yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils && scl enable devtoolset-9 bash上面安装完gcc之后执行一下make distclean清理 一下 再执行make;make执行成功之后 会提示It's a good idea to run 'make test'所以接下来我们make test问题三You need tcl 8.5 or newer in order to run the Redis test异常原因:没有安装tcl解决方案 yum install -y tcl 。redis-cli,Redis命令行界面客户端回复结果作为其他的输出我们只想命令的时候通常需要先 /usr/local/bin/redis-cli 链接到redis的客户端上去再操作;但是有的时候 我不想那么麻烦 ,能不能直接执行客户端的命令,并且将输出 输出到别的地方/usr/local/bin/redis-cli incr mycounter > /tmp/output.txt cat /tmp/output.txt加上–no-raw 将类型也打印出来/usr/local/bin/redis-cli --no-raw incr mycounter > /tmp/output.txt注意 如果不想要带类型 则 --raw主机,端口,密码和数据库如果我们要连接指定的客户端并且还有密码怎么办,带上参数/usr/local/bin/redis-cli -h localhost -p 6379 ping如果您的实例受密码保护,则该-a <password>选项将执行身份验证,从而省去了显式使用AUTH命令的需要:/usr/local/bin/redis-cli -a myUnguessablePazzzzzword123 ping统计redis-cli -h IP地址 -p 端口 -a 密码 info keyspaceredis-cli keys "Abc*" | wc -l从其他程序获取内容当做redis的输入假如我想把某个文件作为value存到redis中; 那么有两种方式1.将我们从stdin读取的有效负载用作最后一个参数/usr/local/bin/redis-cli -x set incrcount < /tmp/output.txt2.另一种方法是提供redis-cli一系列写入文本文件的命令:vim /tmp/commands.txt创建这个文件,并写入一系列redis中的命令set foo 100 incr foo append foo xxx get foo然后执行cat /tmp/commands.txt | /usr/local/bin/redis-cli如果想打印出来不想带类型 记得在后面加上--raw上面依次commands.txt执行 所有命令,redis-cli就像它们是由用户交互键入的一样。如果需要,可以在文件内用字符串引号,以便可以在其中包含带空格或换行符的单个参数或其他特殊字符可以在最后加上 > /tmp/output.txt 将输出结果存放到别的文件中连续运行相同的命令此功能由两个选项控制:-r <count>和-i <delay>。第一个说明运行命令的次数,第二个说明配置不同命令调用之间的延迟(以秒为单位)(可以指定十进制数(如0.1,以表示100毫秒)。默认情况下,间隔(或延迟)设置为0,因此命令会尽快执行:例如 我想每2秒执行一个自增操作除了上面的方式 还可以在交互模式中前面加数字用于重复执行命令随时间监视RSS内存大小redis-cli -r -1 -i 1 INFO | grep rss_human批量操作vim /tmp/commands.txt 我们先把批量命令放到一个文件中然后执行批量执行的命令cat /tmp/commands.txt | /usr/local/bin/redis-cli --pipe > /tmp/batchout.tx使用命令 --pipe使用管道模式 进行批量插入;执行完毕之后我们可以看到输出结果全部执行成功;如何批量删除指定的数据如果我想删掉所以以 1 为前缀的key应该怎么实现/usr/local/bin/redis-cli keys '1*' |xargs /usr/local/bin/redis-cli delRedis批量执行命令如果批量删除 带\n后缀的key连续统计模式请使用该--stat选项来实时监控Redis实例-i: 更改发出新行的频率。默认值为一秒钟。在这种模式下,每秒钟都会打印一条新行,其中包含有用的信息以及旧数据点之间的差异。您可以轻松了解内存使用情况,连接的客户端等情况扫描大键在这种特殊模式下,它redis-cli充当键空间分析器。它在数据集中扫描大键,但也提供有关数据集所包含的数据类型的信息。该模式通过该–bigkeys选项启用,并产生非常详细的输出:先设置一个大值,将之前一个大文件设置为一个值/usr/local/bin/redis-cli -x set bigkeyname < /tmp/commands.txt那么这个key为 bigkeyname的值应该一会扫出来肯定是大键了;redis-cli --bigkeys该命令的扫描是使用的SCAN命令,因此不会影响操作获取按键列表redis-cli --scan | head -10扫描 并打印前面10行使用带有该选项的SCAN命令的基础模式匹配功能–pattern。redis-cli --scan --pattern '*-11*'可以过滤指定的key监控Redis中的操作命令redis-cli monitoredis的所有命令都会实时打印出来还可以加上|grep 进行过滤RDB文件的远程备份在Redis复制的第一次同步期间,主服务器和从服务器以RDB文件的形式交换整个数据集。redis-cli为了提供远程备份功能,可以利用此功能,该功能允许将RDB文件从任何Redis实例传输到运行的本地计算机redis-cli --rdb /tmp/redisdump.rdb ;执行LRU模拟警告:该测试使用流水线操作,并且会对服务器造成压力,请勿将其用于生产实例。./redis-cli --lru-test 10000000如果批量删除 带\n后缀的key有个坑,就是我在执行了这个操作之后,redis有很多测试数据 'lru:*'的数据;如何批量删除是一个问题; 用上面的批量删除不行;返回的都是0 说明删除失败;查看xargs详解的文档; 我们加上一个 -t的参数-t 表示先打印命令,然后再执行。看看我们执行的是什么可以了解到我们只想的是下面的命令redis-cli -a daimler1818 del lru:17288 lru:3818127 看起来貌似没有问题,难道是这个key有问题?那我们get一下看看有没有问题; get查询之后也是没有数据;那就奇怪了; 我们连上交互模式上去看看;keys 'lru:*'可以看到这个key 是带有\n 的并且还是有双引号的;如果获取值的话 应该get "lru:3717577\n" 这样子才行;解决方法第一步 将key拼接成我们想要的样子/data/codis/codis/redis-cli -a password keys "lru*" |head -10 | xargs -I {} -t echo ' del "{}\n"' > /tmp/lrutest.txt命令解释/data/codis/codis/redis-cli -a password keys "lru*" 是过滤我们的数据head -10 是只取上面过滤数据的前10行; 也可以去掉这句 就是全部xargs -I {} -t echo 'del "{}\n"' : 中的{} 是占位符 就是过滤出来的每一项数据; 前后的{}要一致;你也可以用其他的字符来代替; echo 'del "{}\n"' 表示输出字符串 最终输出的是拼接好的比如输出 del "lru:3717577\n"> /tmp/lrutest.txt 是将上面输出的存放到文件中第二步 批量执行cat /tmp/lrutest.txt | ./redis-cli -a password -x结果最好使用–pipe管道来进行批量操作cat /tmp/lrutest.txt | ./redis-cli -a password --pipe
今天出现了这样一个问题, A说他的kafka消息发送了; B说它没有接收到; 那么问题来了:A的消息是否发送了?如果A的消息发送成功了; B为何没有消费到?好,带着上面的问题,我们来一步步排查一下问题所在查询kafka消息是否发送成功1.1.从头消费一下对应的topic;再查询刚刚发送的关键词bin/kafka-console-consumer.sh --bootstrap-server xxx1:9092,xxx2:9092,xxx3:9092 --topic topic名称 -from-beginning这里会把所有的kafaka接受到的消息(还存在磁盘上未被删除的)都打印出来; 这里太多了;我们加上一个 |grep 关键词 过滤一下就可以知道我们发的消息有没有发送成功了这里打印出来的都是 在/data/tmp-log(这里路径是配置的)里面落盘的消息,只要落盘了就肯定发送成功了;1.2 不从头消费 实时消费消息监听如果消息太多了,消费的速度会很慢,那可以不从头消费,只有去掉 参数-from-beginning 就行了;这个命令执行之后会一直在监听消息中;这个时候 重新发一条消息 查看一下是否消费到了刚刚发的消息;如果收到了,说明发送消息这一块是没有问题的;查询kafka消息是否被消费要知道某条消息是否被消息,首先得知道是查被哪个消费组在消费; 比如 B的项目配置的kafka的group.id(这个是kafka的消费组属性)是 b-consumer-group ; 那么我们去看看 这个消费者组的消费情况bin/kafka-consumer-groups.sh --bootstrap-server xxx1:9092,xxx2:9092,xxx3:9092 --describe --group b-consumer-group 这样查询出来的结果就是 b-consumer-group消费组消费了哪些Topic; 如果想过滤某个TOPIC;可以加上|grep TOPIC名称 过滤一下;bin/kafka-consumer-groups.sh --bootstrap-server xxx1:9092,xxx2:9092,xxx3:9092 --describe --group b-consumer-group |grep TOPIC名称最终结果我查出来的是上面每个参数的意思也写情况了; 上面可以看出来, 该TOPIC有三个Partition ; 然后有三个消费者分布在三台机器上; 并且 当前消费组偏移量=当前分区最新偏移量 ;这个说明什么?说明并没有消息未被消费 ;很奇怪,不应该啊;生产者消息也能发送成功,消费组也消费了消息; 那么为什么B说他没有消费的消息呢?那我们可以再验证一下, 让A再发一条消息; 看看Partition中的偏移量是否会增加; 发送之后执行命令查看结果看到没有,从之前的1694变成了1695; 并且两者相同,那么百分之百可以确定,刚刚的消息是被 xxx.xx.xx.139这台消费者消费了;那么问题就在139这个消费者身上了经过后来排查, 139这台机器是属于另外一套环境; 但是该项目的kafka链接的zk跟 另外一套环境相同;如果zk练的是同一个,并且消费者组名(group.id)也相同; 那么他们就属于同一个消费组了; 被其他消费者消费了,另外的消费组就不能够消费了!所以, 不同环境之间的配置检查好不要串环境了,最好不同环境还是做好隔离!检查消费者的位置其他一些有用的命令检查消费者的位置
我们在kafka的log文件中发现了还有很多以 __consumer_offsets_的文件夹;总共50个;由于Zookeeper并不适合大批量的频繁写入操作,新版Kafka已推荐将consumer的位移信息保存在Kafka内部的topic中,即__consumer_offsets topic,并且默认提供了kafka_consumer_groups.sh脚本供用户查看consumer信息。__consumer_offsets 是 kafka 自行创建的,和普通的 topic 相同。它存在的目的之一就是保存 consumer 提交的位移。__consumer_offsets 的每条消息格式大致如图所示可以想象成一个 KV 格式的消息,key 就是一个三元组:group.id+topic+分区号,而 value 就是 offset 的值。考虑到一个 kafka 生成环境中可能有很多consumer 和 consumer group,如果这些 consumer 同时提交位移,则必将加重 __consumer_offsets 的写入负载,因此 kafka 默认为该 topic 创建了50个分区,并且对每个 group.id做哈希求模运算Math.abs(groupID.hashCode()) % numPartitions,从而将负载分散到不同的 __consumer_offsets 分区上。一般情况下,当集群中第一次有消费者消费消息时会自动创建__consumer_offsets,它的副本因子受 offsets.topic.replication.factor 参数的约束,默认值为3(注意:该参数的使用限制在0.11.0.0版本发生变化),分区数可以通过 offsets.topic.num.partitions 参数设置,默认值为50。1. 消费Topic消息打开一个session a,执行下面的消费者命令 ;指定了消费组:szz1-group; topic:szz1-test-topicbin/kafka-console-consumer.sh --bootstrap-server xxx1:9092,xxx2:9092,xxx3:9092 --group szz1-group --topic szz1-test-topic2.产生消息打开一个新的session b,执行生产消息命令bin/kafka-console-producer.sh --broker-list xxx1:9092,xxx2:9092,xxx3:9092 --topic szz1-test-topic发送几条消息然后可以看到刚刚打开的 session a 消费了消息;3. 查看指定消费组的消费位置offsetbin/kafka-consumer-groups.sh --bootstrap-server xxx1:9092,xxx2:9092,xxx3:9092 --describe --group szz1-group可以看到图中 展示了每个partition 对应的消费者id; 因为只开了一个消费者; 所以是这个消费者同时消费3个partition;CURRENT-OFFSET: 当前消费组消费到的偏移量LOG-END-OFFSET: 日志最后的偏移量CURRENT-OFFSET = LOG-END-OFFSET 说明当前消费组已经全部消费了;那么我把 session a 关掉;现在没有消费者之后; 我再发送几条消息看看;我发送了2条消息之后, partition-0 partition-1 的LOG-END-OFFSET: 日志最后的偏移量分别增加了1; 但是CURRENT-OFFSET: 当前消费组消费到的偏移量 保持不变;因为没有被消费;重新打开一个消费组 继续消费*重新打开session之后, 会发现控制台输出了刚刚发送的2条消息; 并且偏移量也更新了4. 从头开始消费 --from-beginning如果我们用新的消费组去消费一个Topic,那么默认这个消费组的offset会是最新的; 也就是说历史的不会消费例如下面我们新开一个session c ;消费组设置为szz1-group3bin/kafka-console-consumer.sh --bootstrap-server xxx1:9092,xxx2:9092,xxx3:9092 --group szz1-group3 --topic szz1-test-topic查看消费情况 bin/kafka-consumer-groups.sh --bootstrap-server xxx1:9092,xxx2:9092,xxx3:9092 --describe --group szz1-group3可以看到CURRENT-OFFSET = LOG-END-OFFSET ;如何让新的消费组/者 从头开始消费呢? 加上参数 --from-beginning5.如何确认 consume_group 在哪个__consumer_offsets-? 中Math.abs(groupID.hashCode()) % numPartitions6. 查找__consumer_offsets 分区数中的消费组偏移量offset上面的 3. 查看指定消费组的消费位置offset 中,我们知道如何查看指定的topic消费组的偏移量;那还有一种方式也可以查询先通过 consume_group 确定分区数; 例如 "szz1-group".hashCode()%50=32; 那我们就知道 szz-group消费组的偏移量信息存放在 __consumer_offsets_32中;通过命令 bin/kafka-simple-consumer-shell.sh --topic __consumer_offsets --partition 32 --broker-list xxx1:9092,xxx2:9092,xxx3:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter"前面的 是key 后面的是value;key由 消费组+Topic+分区数 确定; 后面的value就包含了 消费组的偏移量信息等等然后接着我们发送几个消息,并且进行消费; 上面的控制台会自动更新为新的offset;7 查询topic的分布情况bin/kafka-topics.sh --describe --zookeeper xxx:2181 --topic TOPIC名称
本文设置到的配置项有名称 描述 类型 默认num.partitions topic的默认分区数 int 1log.dirs 保存日志数据的目录。如果未设置,则使用log.dir中的值 string /tmp/kafka-logsoffsets.topic.replication.factor offset topic复制因子(ps:就是备份数,设置的越高来确保可用性)。为了确保offset topic有效的复制因子,第一次请求offset topic时,活的broker的数量必须最少最少是配置的复制因子数。 如果不是,offset topic将创建失败或获取最小的复制因子(活着的broker,复制因子的配置) short 3log.index.interval.bytes 添加一个条目到offset的间隔 int 4096首先启动kafka集群,集群中有三台Broker; 设置3个分区,3个副本;发送topic消息启动之后kafka-client发送一个topic为消息szz-test-topic的消息 public static void main(String[] args) { Properties props = new Properties(); props.put("bootstrap.servers", "xxx1:9092,xxx2:9092,xxx3:9092"); props.put("acks", "all"); props.put("retries", 0); props.put("batch.size", 16384); props.put("linger.ms", 1); props.put("buffer.memory", 33554432); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); Producer<String, String> producer = new KafkaProducer<>(props); for(int i = 0; i < 5; i++){ producer.send(new ProducerRecord<String, String>("szz-test-topic", Integer.toString(i), Integer.toString(i))); } producer.close(); }发送了之后可以去log.dirs路径下看看这里的3个文件夹分别代表的是3个分区; 那是因为我们配置了这个topic的分区数num.partitions=3; 和备份数offsets.topic.replication.factor=3; 这3个文件夹中的3个分区有Leader有Fllower; 那么我们怎么知道谁是谁的Leader呢?查看topic的分区和副本bin/kafka-topics.sh --describe --topic szz-test-topic --zookeeper localhost:2181可以看到查询出来显示分区Partition-0在broker.id=0中,其余的是副本Replicas 2,1分区Partition-1在broker.id=1中,其余的是副本Replicas 0,2…或者也可以通过zk来 查看leader在哪个broker上 get /brokers/topics/src-test-topic/partitions/0/state[zk: localhost:2181(CONNECTED) 0] get /brokers/topics/szz-test-topic/partitions/0/state {"controller_epoch":5,"leader":0,"version":1,"leader_epoch":0,"isr":[0,1,2]} cZxid = 0x1001995bf分区文件都有啥进入文件夹看到如下文件:名称描述类型默认log.segment.bytes单个日志文件的最大大小int1073741824我们试试多发送一些消息,看它会不会生成新的 segmentpublic static void main(String[] args) { Properties props = new Properties(); props.put("bootstrap.servers", "xxx1:9092,xxx2:9092,xxx3:9092"); props.put("acks", "all"); props.put("retries", 0); props.put("batch.size", 163840); props.put("linger.ms", 10); props.put("buffer.memory", 33554432); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); Producer<String, String> producer = new KafkaProducer<>(props); for(int i = 0; i < 1200; i++){ //将一个消息设置大一点 byte[] log = new byte[904800]; String slog = new String(log); producer.send(new ProducerRecord<String, String>("szz-test-topic",0, Integer.toString(i), slog)); } producer.close(); }从图中可以看到第一个segment文件00000000000000000000.log快要满log.segment.bytes的时候就开始创建了00000000000000005084.log了;并且.log和.index、.timeindex文件是一起出现的; 并且名称是以文件第一个offset命名的.log存储消息文件.index存储消息的索引.timeIndex,时间索引文件,通过时间戳做索引消息文件上面的几个文件我们来使用kafka自带工具bin/kafka-run-class.sh 来读取一下都是些啥bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.log最后一行:baseoffset:5083 position: 1072592768 CreateTime: 1603703296169.index 消息索引bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.index最后一行:offset:5083 position:1072592768.timeindex 时间索引文件bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.timeindex最后一行:timestamp: 1603703296169 offset: 5083Kafka如何查找指定offset的Message的找了个博主的图 @lizhitao比如:要查找绝对offset为7的Message:首先是用二分查找确定它是在哪个LogSegment中,自然是在第一个Segment中。打开这个Segment的index文件,也是用二分查找找到offset小于或者等于指定offset的索引条目中最大的那个offset。自然offset为6的那个索引是我们要找的,通过索引文件我们知道offset为6的Message在数据文件中的位置为9807。打开数据文件,从位置为9807的那个地方开始顺序扫描直到找到offset为7的那条Message。Kafka 中的索引文件,以稀疏索引(sparse index)的方式构造消息的索引,它并不保证每个消息在索引文件中都有对应的索引项。每当写入一定量(由 broker 端参数 log.index.interval.bytes 指定,默认值为 4096,即 4KB)的消息时,偏移量索引文件 和 时间戳索引文件 分别增加一个偏移量索引项和时间戳索引项,增大或减小 log.index.interval.bytes 的值,对应地可以缩小或增加索引项的密度。稀疏索引通过 MappedByteBuffer 将索引文件映射到内存中,以加快索引的查询速度。leader-epoch-checkpointleader-epoch-checkpoint 中保存了每一任leader开始写入消息时的offset; 会定时更新follower被选为leader时会根据这个确定哪些消息可用
打印mybatis中sql日志并存放到指定文件中logback-spring.xml (如果是logbackx.xml 动态路径会失效)<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- 子节点<property> :用来定义变量值,它有两个属性name和value,通过<property>定义的值会被插入到logger上下文中,可以使“${}”来使用变量--> <property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss} %c{1} %L [%p] %m%n %caller{0}"/> <!-- 获取Environment中的值; 属性文件中可以设置 log.path的值来动态变更路径--> <springProperty scope="context" name="log.path" source="log.path"/> <!-- 把日志输出到控制台--> <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender"> <encoder charset="UTF-8"> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,%msg:日志消息,%n是换行符--> <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{50} >>> %msg%n</pattern> </encoder> </appender> <appender name="common" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path}/common.log</file> <!-- 如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true。 --> <append>true</append> <!-- 对记录事件进行格式化 --> <encoder> <pattern>${pattern}</pattern> </encoder> <!-- 匹配>=INFO级别的日志--> <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}/common.log.%d{yyyy-MM-dd}</fileNamePattern> <!-- 可选节点,控制保留的归档文件的最大天数。--> <maxHistory>10</maxHistory> </rollingPolicy> </appender> <appender name="exception" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path}/exception.log</file> <append>true</append> <!-- 匹配>=ERROR级别的日志--> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <encoder> <pattern>${pattern}</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.path}/exception.log.%d{yyyy-MM-dd}</fileNamePattern> <maxHistory>7</maxHistory> </rollingPolicy> </appender> <appender name="mysql_log" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path}/mysql_log.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 日志文件输出文件名 --> <FileNamePattern>${log.path}/mysql_log.log.%d{yyyy-MM-dd}</FileNamePattern> <!-- 日志文件保留天数 --> <MaxHistory>7</MaxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${pattern}</pattern> </encoder> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>DEBUG</level> </filter> </appender> <!-- 打印mysql日志 name= 存放mapper的包名; 注意mybatis-plus.configuration.log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl 如果log-impl: org.apache.ibatis.logging.stdout.StdOutImpl ;则只会打印到控制台;不会存放到文件 --> <logger name="com.xxx.mapper" level="DEBUG" additivity="false"> <appender-ref ref="stdout"/> <appender-ref ref="mysql_log" /> </logger> <!-- 用来设置某一个包或具体的某一个类的日志打印级别、以及指定<appender>。 name: 用来指定受此loger约束的某一个包或者具体的某一个类。 level: 如果未设置此属性,那么当前loger将会继承上级的级别。上级是<root> addtivity:是否向上级logger传递打印信息。默认是true --> <!--它是根loger,是所有<loger>的上级。只有一个level属性,因为name已经被命名为"root",且已经是最上级了。 --> <root level="INFO"> <appender-ref ref="stdout"/> <appender-ref ref="common"/> <appender-ref ref="exception"/> </root> </configuration> 几个关键点文件名需要为logback-spring.xml; 动态日志路径才会生效; 属性文件中配置 log.path=xxx;【Log日志】logback.xml动态配置属性值(包括接入的第三方配置)levle 需要是DEBUG等级; 因为sql日志是DEBUG等级的;name= 存放mapper文件的包路径<logger name="com.xxx.mapper" level="DEBUG" additivity="false"> <appender-ref ref="stdout"/> <appender-ref ref="mysql_log" /> </logger>mybatis的log-impl需要配置正确的实现类 比如 在maybatis-plus中# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 mybatis-plus: configuration: #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 这个配置会将执行的sql打印出来,这个可以存放在文件中 StdOutImpl的是只能打印到控制台 log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl我之前就是一直配置的是 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl ;导致文件只能出现在控制台;却没有打印到文件中;参数值的默认值设置如果配置文件没有设置属性. 可以在使用的地方设置默认值 例如 ${log.path:-默认值}
之前在文章 使用Nacos简化SpringBoot配置(所有配置放入到Nacos中) 中有实现一个 EnvironmentPostProcessor的扩展接口;但是发现日志并没有打印出来, 然后就跟着源码找了一下问题;问题原因:在SpringBoot加载的过程中 EnvironmentPostProcessor 的执行比较早; 这个时候日志系统根本就还没有初始化;所以在此之前的日志操作都不会有效果;看看日志系统加载的时机日志系统初始化的地方 LoggingApplicationListener.onApplicationEnvironmentPreparedEvent()知道了日志初始化的时候是在这里;那也就知道了加载时机;那么是在哪里开始加载的呢? 我们来分析一下首先找到 spring.factories 配置文件里面的配置;找到被加载的地方; 具体为事件ApplicationEnvironmentPreparedEvent通知到对应的监听者的;可以看到; ConfigFileApplicationListener > LoggingApplicationListener ; 前面比后面先加载看看SpringBoo整体的加载时机 【SpringBoot】SpringBoot启动流程图和扩展点说明从上面的图中可以了解到在 ConfigFileApplicationListener执行的时候 会去 spring.factories 中加载所有 EnvironmentPostProcessor并执行postProcessEnvironment方法; 这个时候 LoggingApplicationListener还没有被执行;说明日志系统还没有被初始化; 自然而然的 在这之前的所有日志操作都是无效的;解决方案使用 DeferredLog 缓存日志;并在合适的时机回放日志 public class NacosEnvPostProcessor implements EnvironmentPostProcessor, ApplicationListener<ApplicationEvent>, Ordered { /** * 这个时候Log系统还没有初始化 使用DeferredLog来记录 并在onApplicationEvent进行回放 * */ private static final DeferredLog LOGGER = new DeferredLog(); @Override public int getOrder() { return 0; } @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { LOGGER.info("打印日志"); } @Override public void onApplicationEvent(ApplicationEvent event) { LOGGER.replayTo(NacosEnvPostProcessor.class); } }不要忘记了在spring.factorie 配置org.springframework.boot.env.EnvironmentPostProcessor=com.xxx.NacosEnvPostProcessor org.springframework.context.ApplicationListener=com.xxx.NacosEnvPostProcessor
spring.factories作用这个类似于Java中的SPI功能,SpringBoot启动的时候会读取所有jar包下面的META-INF/spring.factories文件; 并且将文件中的 接口/抽象类 对应的实现类都对应起来,并在需要的时候可以实例化对应的实现类下面我们来分析一下源码看看spring.factories的使用场景源码解析启动SpringApplication,看看构造方法 public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); this.webApplicationType = WebApplicationType.deduceFromClasspath(); setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = deduceMainApplicationClass(); }其中方法getSpringFactoriesInstances( ApplicationContextInitializer.class) 是用于获取Spring中指定类实例用的;并且获取的时候是根据读取整个项目中文件路径为META-INF/spring.factories 中的内容实例化对应的实例类的;例如这里的ApplicationContextInitializer 是一个接口,那么应该实例化哪些他的实现类呢?那就找META-INF/spring.factories文件 ; 那么我们在spring-boot:2.1.0jar包中找到了这个文件读取到需要实例化的实现类为 org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\ org.springframework.boot.context.ContextIdApplicationContextInitializer,\ org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\ org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer并且还在spring-boot-autoconfigure-2.1.0.RELEASE.jar中找到了这个文件那么文件中的两个实现类也会被实例化;加上上面4个总共有6个org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\ org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener可以看到不仅仅只是把org.springframework.context.ApplicationContextInitializer的实例类解析了出来;而是所有的都解析了出来并且保存下来了.下次其他的类需要被实例化的时候就可以直接从内存里面拿了;上面过程拿到了实例类之后,接下来就是实例化的过程了 private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) { ClassLoader classLoader = getClassLoader(); // Use names and ensure unique to protect against duplicates Set<String> names = new LinkedHashSet<>( SpringFactoriesLoader.loadFactoryNames(type, classLoader)); List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); AnnotationAwareOrderComparator.sort(instances); return instances; } 方法createSpringFactoriesInstances就是创建实例的过程;可以看到传入了对应的接口类org.springframework.context.ApplicationContextInitializer;接下来就会实例化 上面找到了对应的实现类; private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args, Set<String> names) { List<T> instances = new ArrayList<>(names.size()); for (String name : names) { try { Class<?> instanceClass = ClassUtils.forName(name, classLoader); Assert.isAssignable(type, instanceClass); Constructor<?> constructor = instanceClass .getDeclaredConstructor(parameterTypes); T instance = (T) BeanUtils.instantiateClass(constructor, args); instances.add(instance); } catch (Throwable ex) { throw new IllegalArgumentException( "Cannot instantiate " + type + " : " + name, ex); } } return instances; }实例化的过程如果,没有什么特别需要讲解的;上面有个方法AnnotationAwareOrderComparator.sort(instances);是用来排序所有实例的; 实现类需要实现 接口Ordered ; getOrder返回的值越小,优先级更高用法知道spring.factories的用法之后, 那么我们就可以利用这个特性实现自己的目的;例如我们也可以写一个接口类ApplicationContextInitializer的实现类;等等之类的;
Args 作用传递参数的一种方式; 例如启动的时候 java -jar --spring.profiles.active=prod或者更改自己的自定义配置信息 ;使用方式是 --key=value它的配置优先于项目里面的配置;我们现在大部分项目都是用SpringBoot进行开发的,一般启动类的格式是SpringApplication.run(SpringBootDemoPropertiesApplication.class, args);但是好像平常一直也没有用到args; 也没有穿过参数,那么这个args究竟有什么用呢?我们随着源码一探究竟!启动一个带web的项目,并且在application.yml配置文件里面定义一个自定义属性developer. name=test以下是启动类, args设置一些参数@SpringBootApplication public class SpringBootDemoPropertiesApplication { public static void main(String[] args) { args = new String[]{"1","2","--name=shienchuang","--name=shizhenzhen","age=18","--developer.name=shirenchuang666"}; SpringApplication.run(SpringBootDemoPropertiesApplication.class, args); } } Args使用场景一进入run方法看到 args第一次出现在 SpringApplication类中的private SpringApplicationRunListeners getRunListeners(String[] args) { Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class }; return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances( SpringApplicationRunListener.class, types, this, args)); }方法中getSpringFactoriesInstances( SpringApplicationRunListener.class, types, this, args) 用于实例化 SpringApplicationRunListener的实现类(配置在spring.factories中的实现类)关于spring.factories的用法可以参考: 【SpringBoot 二】spring.factories加载时机分析此项目中只在spring.factories找到了一个实现类org.springframework.boot.context.event.EventPublishingRunListener在实例化 的过程中是有把 两个参数{SpringApplication 和 String[] args} 传递过去的那么对应到的构造函数就是并且可以看到在EventPublishingRunListener的方法中都有把Args传递下去;Args使用场景二上面的SpringApplicationRunListeners完事之后,接下来就到了ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); public DefaultApplicationArguments(String[] args) { Assert.notNull(args, "Args must not be null"); this.source = new Source(args); this.args = args; }SimpleCommandLinePropertySource主要看上面的 new Source(args)方法; 这个Source继承了类SimpleCommandLinePropertySource那么SimpleCommandLinePropertySource作用是什么?SimpleCommandLinePropertySource也是一个数据源PropertySource ;但是它主要是存放命令行属性;例如启动参数Args;中的属性就会保存在这个对象中; 并且SimpleCommandLinePropertySource会被放入到Environment中; 所以也就可以通过{@link Environment#getProperty(String)}来获取命令行的值了 public SimpleCommandLinePropertySource(String... args) { super(new SimpleCommandLineArgsParser().parse(args)); }看构造函数 可以知道实例化之后的SimpleCommandLinePropertySource 是name为commandLineArgs 的数据源; 属性值的解析规则如下–key=value key=value的前面接上两个- 就会解析成kv格式key可以相同 ,并且value可以多个; 她是一个List接口;一个key可以对应多个value不能有空格如果不是 --key=value的格式,那么都会被解析到一个 key为nonOptionArgs的list中往下面走到了protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) { MutablePropertySources sources = environment.getPropertySources(); if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) { sources.addLast( new MapPropertySource("defaultProperties", this.defaultProperties)); } if (this.addCommandLineProperties && args.length > 0) { String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME; if (sources.contains(name)) { PropertySource<?> source = sources.get(name); CompositePropertySource composite = new CompositePropertySource(name); composite.addPropertySource(new SimpleCommandLinePropertySource( "springApplicationCommandLineArgs", args)); composite.addPropertySource(source); sources.replace(name, composite); } else { sources.addFirst(new SimpleCommandLinePropertySource(args)); } } }这个方法的作用就是把 我们的Args 放到到Spring的 environment中;sources.addFirst(new SimpleCommandLinePropertySource(args));看到方法是 addFirst(); 这个说明什么?说明命令行的的数据源被放到了最前面;那么命令行数据源的属性就会被最优先采用;那么我们就可以通过Environment#getProperty(String) 获取args中的值了;那么我们可以利用这个args做什么用;?可以用它来写入 配置; 并且是覆盖项目中的配置(因为他的优先级更高);例如 java -jar --spring.profiles.active=dev这里就算yml配置的是prod;最终使用的是dev;
技术交流有想进滴滴LogI开源用户群的加我个人微信: jjdlmn_ 进群(备注:进群)群里面主要交流 kakfa、es、agent、LogI-kafka-manager、等等相关技术;群内有专人解答你的问题对~ 相关技术领域的解答人员都有; 你问的问题都会得到回应坑1 no available service ‘default’ foundi.s.c.r.netty.NettyClientChannelManager : no available service 'null' found, please make sure registry config correct或no available service 'default' found, please make sure registry config correct这个问题的原因是没有找到 seata-server ;1.确认自己seata-server启动了2.确认客户端启动的时候连接配置是正确的例如:seata-server选择的注册中心是redis; 配置 cluster=default注册成功之后看看redis的值确认客户端的配置是正确的如果上面都正确,但是还是有问题,请确认一下自己是不是手动配置了 GlobalTransactionScanner,确认一下配置的txServiceGroup参数是否跟跟配置一样;如下坑2 com.alibaba.nacos.api.exception.NacosExceptionSeata 使用注册中心的时候用的是Nacos,启动报错Caused by: java.lang.ClassNotFoundException: com.alibaba.nacos.api.exception.NacosException at java.net.URLClassLoader.findClass(URLClassLoader.java:382) ~[na:1.8.0_221] at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_221] at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) ~[na:1.8.0_221] at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_221] ... 39 common frames omitted Seata-All 在引入相应的jar包的时候都是 <scope>provide</scope>说明我们要引入对应的依赖才行; 按需引入;同理 如果用的是Redis的注册中心也要引入redis的客户端 <!-- 如果注册中心选择的是nacos 需要引入下面的包--> <!-- https://mvnrepository.com/artifact/com.alibaba.nacos/nacos-client --> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>1.3.0</version> </dependency> <!-- 如果注册中心选择了redis 则需要依赖下面的--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>坑3 NotSupportYetException: not support register type: null在引入seata的过程中,启动的时候报如下的错误 nested exception is io.seata.common.exception.NotSupportYetException: not support register type: null io.seata.common.exception.ShouldNeverHappenException: Can't find any object of class org.springframework.context.ApplicationContext 他的原因就是SpringApplicationContextProvider没有被执行到因为我的是用seata-spring-boot-starter方式启动的;然后又手贱配置了GlobalTransactionScanner那么这个GlobalTransactionScanner开始加载的时候,SpringApplicationContextProvider并没有被执行;GlobalTransactionScanner需要依赖于SpringApplicationContextProvider, 所以报错了解决方法: seata-spring-boot-starter方式启动已经自动加载了GlobalTransactionScanner 见SeataAutoConfiguration如果一定要自己手动加载的话 ,请加上注解 @DependsOn({BEAN_NAME_SPRING_APPLICATION_CONTEXT_PROVIDER, BEAN_NAME_FAILURE_HANDLER}) 但是,没有必要手动配置配置 GlobalTransactionScanner,使用 seata-all 时需要手动配置,使用 seata-spring-boot-starter 时无需额外处理。坑4 can not register RM,err:can not connect to services-server.之前一直好好的,过几天启动就报这个错了,后来我看了一下注册中心,注册了好几个ip;都是之前注册过的,不知道为啥没有被清理;io.seata.common.exception.FrameworkExcio.seata.common.exception.FrameworkException: can not register RM,err:can not connect to services-server. 解决方案 :把key删掉重新启动 seata-server坑5 Could not initialize class io.seata.rm.datasource.undo.UndoLogParserFactory$SingletonHolder接入Seata的时候 有报下面的错误 java.lang.NoClassDefFoundError: Could not initialize class io.seata.rm.datasource.undo.UndoLogParserFactory$SingletonHolder表面上看起来是那个异常,但是你打个断点进去查看会发现最终的异常是下面这个java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/ObjectMapper解决方案:加入jackson-databind依赖就行 <!-- undo序列化方式 选择了哪个就要依赖哪个jar包--> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.11.0</version> </dependency>
背景与挑战为了支撑业务的飞速发展,分布式系统架构不断演进,业务链路日趋复杂,服务间相互调用,增加了服务联调的复杂性;在如此研发背景下,作为研发过程中不可或缺的一环业务链路联调,面临越来越多的挑战:联调涉及应用服务多,导致环境构建和维护的成本都非常高,手工搭建一套可用联调环境,少则1-2天,部分情况下甚至可能花费1到2周。因此,如何降低联调环境构建成本,让研发同学专注于业务联调本身?联调链路上下游依赖应用服务多,为每一个联调链路都全量搭建一套独立环境,资源消耗太大,需要对没有变更的应用服务进行复用。但是复用又带来了新的问题,每周上N个的并行研发活动,同一个应用服务可能为了支持不同需求在研发阶段存在多个并行研发,如何在资源复用的基础上,解决并行研发带来的干扰联调过程中出现了问题,排查的链路往往比较长,一般研发同学对自己负责的应用服务比较了解,如果问题出在依赖的下游,往往需要联系对应负责的同学排查,过程中有很多的沟通成本,排查效率比较低。如何协助研发同学快速定位联调问题,提升业务联调的效率?联调环境复用与隔离一般操作假如研发团队有 3套开发环境用于联调; 每套环境都部署了一套完整的N个服务;这时候公司同时有4个需求开发联调;feature_1~4 ;那么环境占用情况如下每个feature占用了一个环境,而feature_4却被阻塞联调了,只能等待环境空闲出来,或者再让运维增加一套环境 dev4 来使用;但是新增一套环境不仅增加了运维的工作量;而且又增加了研发成本;难道就没有解决方法吗?将多个需求合并到一个分支为了解决上述问题,小企业最常用最省事的方法是:拉一个新的分支 feature_1_2, 将多个分支都一起合并到这个分支上来进行联调;共用一套环境;确实,这是最省事的方法,但是这个方法存在它的局限性代码冲突, 需求冲突每次修改了bug都要将代码合并到合并分支feature_1_2代码污染, 修改bug的时候没有写在需求分支而写在的合并分支feature_1_2正常来说,严格按照约定操作,也不会出现什么问题,但是我们有更好的解决方案联调环境复用与隔离上面的方法虽然可以操作,但是使用太复杂;我们可以将没有收到需求迭代而变更的服务复用起来;全量部署所有服务的master稳定分支首先我们把所有服务都部署在一套环境里面;跟stable环境(或者生产环境)保持一致;这些服务永远都是部署master稳定分支,这些服务就是用来被复用的;假设我们有4个需求并行开发联调,那调用链就是下面这种上图假设的feature_1只变更了 S1 S3 S4 的服务,那么没有变更的S2 S5就可以直接复用master的稳定服务 M2 M5上图假设feature_2 只变更了 S3;其余的服务都可以服务稳定版本的服务;理论可以并行开发联调N个需求看到上面服务复用的模型,我们来算一个账;假设最初的时候 一个需求占用一套环境; 一套环境可能部署了N套服务;想要并行联调Y个需求,那么就需要 N*Y个服务器资源;用了服务重用之后;同样支持Y个需求占用的服务器资源要远远少的多;因为每个需求中服务变更的是少数,假如一套环境100个服务,一次需求的变更服务数目一般不超过10个,我们只需要提供变更服务的部署资源就行;而且不需要完整的重新搭建一套环境,只需要部署变更服务服务路由隔离上面介绍的是实现的思路,同一个环境下(指的是同一个zk下注册的服务)我们要怎么复用这个稳定服务呢?同一个服务被注册了多个提供者;如何准确的调用对应需求的服务呢?因为不同的RPC的实现不一样,我这里主要讲解Rpc为dubbo的情况下,如何实现上述需求;因为文字篇幅过长,故新开一篇文章讲解 Dubbo下的多版本并行开发测试解决方案调用入口处理http请求访问统一网关访问中间件隔离配置管理(Nacos、Apollo等等)消息系统(kafka、RocketMq 等等)【kafka】kafka的服务复用与隔离设计方案DB隔离先占个坑 有空再写 TODO…
文章目录IntelliJ IDEA 插件安装配置远程Arthas配置直连Host机器配置需要跳板机HostEclipse插件安装参考资料Cloud Toolkit是一个IDE 插件,帮助开发者更高效地开发、测试、诊断并部署应用。使用本插件,开发者能够方便地将本地应用一键部署到任意机器,或 ECS、EDAS、Kubernetes;并支持高效执行终端命令和 SQL 等。Toolkit是一款很强大的工具,它有以下功能但是这里我们主要讲解 Java应用诊断这个功能;IntelliJ IDEA 插件安装因 JetBrains 插件市场服务器在海外,如遇访问缓慢无法下载安装的,请点击这里下载插件包,获取离线包安装确保 IntelliJ 在 2018.1 或更高版本第 1 步:打开 Intellij 的 Settings ( Windows下 ) 或 Preferences( Mac下 )窗口第 2 步:进入 Plugins 选项,搜索“Alibaba Cloud Toolkit”,并安装即可,如下图:插件地方配置远程Arthas配置直连Host机器1、添加配置2、检测连接远程服务器成功3、打开配置列表4、点击Diagnosic 一键诊断远程服务器5、启动成功,选择要诊断的Java进程;输入命令 dashboard 查看配置需要跳板机Host选择一个机器作为跳板机就行了Eclipse插件安装
文章目录Arthas(读:阿尔萨斯) 是什么Arthas能干什么快速安装第一种:使用arthas-boot(推荐)第二种:使用as.shCloud Toolkit插件一键诊断远程服务器卸载启动异常情况正常关闭示例项目参考资料Arthas(读:阿尔萨斯) 是什么Arthas 是Alibaba开源的Java诊断工具GitHub地址: Alibaba Althas 项目地址Arthas能干什么当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!是否有一个全局视角来查看系统的运行状况?有什么办法可以监控到JVM的实时运行状态?Arthas支持JDK 6+,支持Linux/Mac/Winodws,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。快速安装第一种:使用arthas-boot(推荐)下载arthas的jar包; 然后启动jar包java -jar arthas-boot.jarwget https://alibaba.github.io/arthas/arthas-boot.jar java -jar arthas-boot.jar启动之后会列出所有的Java进程; 选择你想要诊断的进程; 比如这里我输入 4 回车这时候就进入的Arthas的操作界面了;注意:执行该程序的用户需要和目标进程具有相同的权限。比如以admin用户来执行:sudo su admin && java -jar arthas-boot.jar 或 sudo -u admin -EH java -jar arthas-boot.jar。如果attach不上目标进程,可以查看~/logs/arthas/ 目录下的日志。第二种:使用as.shArthas 支持在 Linux/Unix/Mac 等平台上一键安装,请复制以下内容,并粘贴到命令行中,敲 回车 执行即可:curl -L https://alibaba.github.io/arthas/install.sh | sh上述命令会下载启动脚本文件 as.sh 到当前目录,你可以放在任何地方或将其加入到 $PATH 中。直接在shell下面执行./as.sh,就会进入交互界面。也可以执行./as.sh -h来获取更多参数信息。如果从github下载有问题,可以使用gitee镜像curl -L https://arthas.gitee.io/install.sh | shCloud Toolkit插件一键诊断远程服务器简单配置,无须手动安装Arthas通过Cloud Toolkit插件使用Arthas一键诊断远程服务器卸载在 Linux/Unix/Mac 平台删除下面文件:rm -rf ~/.arthas/ rm -rf ~/logs/arthasWindows平台直接删除user home下面的.arthas和logs/arthas目录启动异常情况Target process 24501 is not the process using port 3658, you will connect to an unexpected process出现这种情况,是我们刚刚诊断了com.shirc.arthasexample.ArthasExampleApplication的进程;然后退出的时候用了 ctrl+c或者exit退出,其实没有把那个会话正常关闭;下次再选择其他的进程的时候会提示你有一个会话没有正常关闭,要你去正常关闭一下;解决方法: 重新进入刚刚那个会话;然后执行: shutdown 正常关闭会话; 才能重新打开其他进程的会话;正常关闭shutdown示例项目可以下载我用来调试的项目 Arthas 示例项目本Arthas系列的的文章的所有调试命令都是基于上面的项目;先把项目拉下来,本地启动;之后就可以看到进程了
前言上一篇 【Nacos源码之配置管理 十】客户端长轮询监听服务端变更数据 介绍了客户端会像服务端发起长轮询来获取变更数据, 其实在客户端发起长轮询的请求相当于向服务端发起了一个订阅; 因为服务端接受到客户端的请求之后如果没有查询到变更数据是不会返回的;而是会等待29.5s(当然时间可配),在这个29.5s时间内,服务端如果检测到有数据变更,会立马像客户端发起响应请求,因为这个时间内服务端还是有hold住客户端发过来的请求,所以能发回响应数据; hold住request是用的AsyncContext异步[x] 服务端是怎么通知到客户端数据变更的[x] 如何以 拉模式 长轮询服务端LongPollingServiceLongPollingService 是一个长轮询服务,但是它是处理客户端的长轮询;LongPollingService 还处理服务端本地数据变更之后的事情服务端数据变更事件LongPollingService实现了AbstractEventListener的onEvent方法; 这是一个发布订阅模式; 可以看 【Nacos源码之配置管理 二】Nacos中的事件发布与订阅--观察者模式;Nacos源码之配置管理 七】服务端增删改配置数据之后如何通知集群中的其他机器 中有介绍修改数据之后的一些流程,就有讲到这里;这个是一个本地数据变更事件 ;DataChangeTask的时候留下了2个问号,前面挖的坑现在是填的时候了;DataChangeTaskDataChangeTask 服务端数据变更任务配置数据有变更的时候执行这个方法; 遍历allSubs.iterator();得到对象 ClientLongPolling ;这是一个客户端长轮询的对象;里面保存了一些例如ip、clientMd5Map、asyncContext等等还有其他一些数据;asyncContext :Servlet 3.0新增了异步处理, 这个对象持有客户端的 request和 response; 就是通过这个对象,在服务端有了数据变更的情况下,能够里面的将变更数据返回响应给客户端; AsyncContext异步请求的用法: https://shirenchuang.blog.csdn.net/article/details/100809937那么就剩下一个很重要的问题就是 allSubs 是什么时候订阅上的?客户端发起长轮询上一篇文章 【Nacos源码之配置管理 十】客户端长轮询监听服务端变更数据 分析了客户端发起长轮询的请求;如下那么看看服务端这个listener做了什么 public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map, int probeRequestSize) { String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER); String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER); String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER); String tag = req.getHeader("Vipserver-Tag"); int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500); /** * 提前500ms返回响应,为避免客户端超时 @qiaoyi.dingqy 2013.10.22改动 add delay time for LoadBalance */ long timeout = Math.max(10000, Long.parseLong(str) - delayTime); if (isFixedPolling()) { timeout = Math.max(10000, getFixedPollingInterval()); // do nothing but set fix polling timeout } else { long start = System.currentTimeMillis(); List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map); if (changedGroups.size() > 0) { generateResponse(req, rsp, changedGroups); LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant", RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize, changedGroups.size()); return; } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) { LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup", RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize, changedGroups.size()); return; } } String ip = RequestUtil.getRemoteIp(req); // 一定要由HTTP线程调用,否则离开后容器会立即发送响应 final AsyncContext asyncContext = req.startAsync(); // AsyncContext.setTimeout()的超时时间不准,所以只能自己控制 asyncContext.setTimeout(0L); scheduler.execute( new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag)); }获取客户端的请求参数;str: 长轮询的超时时间,默认30s;详细可见上一篇文章noHangUpFlag:不挂起标识,这个标识为false的时候,会把客户端的请求挂起;等待超时或者数据变更通知;如果客户端监听的数据是首次初始化,这个标识为true;delayTime:延时时间;为了避免客户端请求超时,需要提前这个时间返回响应;这个数据是在配置管理中配置的,默认500毫秒,详细见 【Nacos源码之配置管理 四】DumpService如何将配置文件全部Dump到磁盘中对比客户端和服务端MD5是否相同,有不同则直接返回不同的dataid+group响应;如果2中没有不同,并且如果noHangUpFlag=true 则直接返回,不挂起请求;2和3都不满足,则使用AsyncContext,将请求异步化,直接挂起; 超时时间为str-delayTIme,str是客户端设置的时间如下所示,delayTime默认500毫秒,可以在管理后台配置(见上面具体如何配置),如果都不主动配置,那么超时时间是30000-500=29500; 29.5秒;执行ClientLongPolling任务ClientLongPolling任务这个类有如下属性asyncContext中持有客户端的请求;clientMd5Map包含了客户端所要监听的数据的MD5;ClientLongPolling这个任务类执行的时候,是把 一个任务延迟了timeoutTime之后再执行的,并且返回asyncTimeoutFuture,这个timeoutTime就是上面说到的超时时间,例如29.5s;allSubs中; 等待有数据变更的时候,可以通知到这个客户端,因为当前实例有asyncContext ,可以相应客户端的请求;29.5s之后做了什么事情看看timeoutTime之后执行的方法体做了什么isFixedPolling()下面再讲,暂时忽略;我们看到最终执行的是删除订阅关系,为啥要删除,因为这个allSubs在配置数据有变更的时候会遍历这个来进行通知,这里相当于本次请求要结束了,所以删除,不让通知了执行sendResponse(null);方法;timeoutTime超时时间到了之后,服务端会直接结束本次请求;然后客户端又会立马重新发起新一轮请求,重复这个过程;相当于是说客户端每隔timeoutTime时间之后,就发起一次请求判断服务端是否有变更数据;那么问题来了,如果只是这样的话,那么就是服务端纯粹的使用 拉模式, 并没有服务端的推模式呀?服务端变更数据使用推模式推送数据还记得文章一开头就说到一个事件吗,LocalDataChangeEvent 事件,服务端中修改了配置数据之后,就通知这个事件,这个事件最终会执行DataChangeTask任务; /**下面删除了部分代码;保留关键点**/ class DataChangeTask implements Runnable { @Override public void run() { try { for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) { ClientLongPolling clientSub = iter.next(); if (clientSub.clientMd5Map.containsKey(groupKey)) { iter.remove(); // 删除订阅关系 clientSub.sendResponse(Arrays.asList(groupKey)); } } } catch (Throwable t) { } }遍历的allSubs; 这个allSubs是上面介绍过,客户端发起长轮询的请求的时候注册上的;比较客户端订阅的配置数据MD5与当前是否一致,这个时候基本是不一致的,因为有修改嘛;做了一些过滤操作之后,sendResponse(Arrays.asList(groupKey));配置项;发送了之后,本次请求也就结束了;客户端又会重新再发起新的一轮请求;客户端拉+服务端推上面分析完了之后,我们总结一下;客户端发起订阅请求;服务端接收到请求之后,立马去查询一次数据是否变更;hold住一定的时间(默认29.5s)①.如果这期间客户端所监听的数据都一直没有变更,在时间到达之后,结束客户端的本次请求;客户端又回到步骤1;就是这样一个不停的轮询的过程; 但是注意,服务端返回的只是哪些配置项有变更(只返回dataid+group等等,并没有返回content),客户端拿到这些变更配置项之后,还有主动请求配置项的content,来更新自己的缓存isFixedPolling 固定长轮询上面在介绍的时候,我们选择性忽略了这个固定长轮询;现在来介绍一下拉如何设置固定长轮询新增一个元数据配置项, DataId是 com.alibaba.nacos.meta.switch ; Group是DEFAULT_GROUP ;注意这个配置是内置的,一定要这样配置;将配置isFixedPolling=true 打开固定长轮询fixedPollingInertval=10000;固定长轮询的间隔时间fixedDelayTime=500 延迟时间; 例如间隔时间是10s,延迟时间是0.5秒; 那么每隔9.5s执行一次轮询;0.5s的时间是为了防止请求超时的;两种模式的比较拉+推 的模式具有时效性;推荐使用 拉+推模式总结
应用场景创建订单10分钟之后自动支付订单超时取消…等等…实现方式最简单的方式,定时扫表;例如每分钟扫表一次十分钟之后未支付的订单进行主动支付 ;优点: 简单缺点: 每分钟全局扫表,浪费资源,有一分钟延迟使用RabbitMq 实现 RabbitMq实现延迟队列优点: 开源,现成的稳定的实现方案;缺点: RabbitMq是一个消息中间件;延迟队列只是其中一个小功能,如果团队技术栈中本来就是使用RabbitMq那还好,如果不是,那为了使用延迟队列而去部署一套RabbitMq成本有点大;使用Java中的延迟队列,DelayQueue优点: java.util.concurrent包下一个延迟队列,简单易用;拿来即用缺点: 单机、不能持久化、宕机任务丢失等等;基于Redis自研延迟队列既然上面没有很好的解决方案,因为Redis的zset、list的特性,我们可以利用Redis来实现一个延迟队列 RedisDelayQueue设计目标实时性: 允许存在一定时间内的秒级误差高可用性:支持单机,支持集群支持消息删除:业务费随时删除指定消息消息可靠性: 保证至少被消费一次消息持久化: 基于Redis自身的持久化特性,上面的消息可靠性基于Redis的持久化,所以如果redis数据丢失,意味着延迟消息的丢失,不过可以做主备和集群保证;数据结构Redis_Delay_Table: 是一个Hash_Table结构;里面存储了所有的延迟队列的信息;KV结构;K=TOPIC:ID V=CONENT; V由客户端传入的数据,消费的时候回传;RD_ZSET_BUCKET: 延迟队列的有序集合; 存放member=TOPIC:ID 和score=执行时间戳; 根据时间戳排序;RD_LIST_TOPIC: list结构; 每个Topic一个list;list存放的都是当前需要被消费的延迟Job;设计图任务的生命周期新增一个Job,会在Redis_Delay_Table中插入一条数据,记录了业务消费方的 数据结构; RD_ZSET_BUCKET 也会插入一条数据,记录了执行时间戳;搬运线程会去RD_ZSET_BUCKET中查找哪些执行时间戳runTimeMillis比现在的时间小;将这些记录全部删除;同时会解析出来每个任务的Topic是什么,然后将这些任务push到Topic对应的列表RD_LIST_TOPIC中;每个Topic的List都会有一个监听线程去批量获取List中的待消费数据;获取到的数据全部扔给这个Topic的消费线程池消息线程池执行会去Redis_Delay_Table查找数据结构,返回给回调接口,执行回调方法;以上所有操作,都是基于Lua脚本做的操作,Lua脚本执行的优点在于,批量命令执行具有原子性,事务性, 并且降低了网络开销,毕竟只有一次网络开销;搬运线程操作流程图设计细节搬运操作1.搬运操作的时机为了避免频繁的执行搬运操作, 我们基于 wait(time)/notify 的方式来通知执行搬运操作;我们用一个AtomicLong nextTime 来保存下一次将要搬运的时间;服务启动的时候nextTime=0;所以肯定比当前时间小,那么就会先去执行一次搬运操作,然后返回搬运操作之后的ZSET的表头时间戳,这个时间戳就是下一次将要执行的时间戳, 把这个时间戳赋值给 nextTime; 如果表中没有元素了则将nextTime=Long.MaxValue ;因为while循环,下一次又会跟当前时间对比;如果nextTime比当前时间大,则说明需要等待; 那么我们wait(nextTime-System.currentTimeMills()); 等到时间到了之后,再次去判断一下,就会比当前时间小,就会执行一次搬运操作;那么当有新增延迟任务Job的时间怎么办,这个时候又会将当前新增Job的执行时间戳跟nextTime做个对比;如果小的话就重新赋值;重新赋值之后,还是调用一下 notifyAll() 通知一下搬运线程;让他重新去判断一下 新的时间是否比当前时间小;如果还是大的话,那么就继续wait(nextTime-System.currentTimeMills()); 但是这个时候wait的时间又会变小;更精准;2.一次搬运操作的最大数量redis的执行速度非常快,在一个Lua里面循环遍历1000个10000个根本没差; 而且是在Lua里面操作,就只有一次网络开销;一次操作多少个元素根本就不会是问题;搬运操作的防护机制1.每分钟唤醒定时线程在消费方多实例部署的情况下, 如果某一台机器挂掉了,但是这台机器的nextTime是最小的,就在一分钟之后( 新增job的时候落到这台机器,刚好时间戳很小), 其他机器可能是1个小时之后执行搬运操作; 如果这台机器立马重启,那么还会立马执行一次搬运操作;万一他没有重启;那可能就会很久之后才会搬运;所以我们需要一种防护手段来应对这种极端情况;比如每分钟将nextTime=0;并且唤醒wait;那么就会至少每分钟会执行一次搬运操作! 这是可以接受的LrangeAndLTrim 批量获取且删除待消费任务1.执行时机以及如何防止频繁请求redis这是一个守护线程,循环去做这样的操作,把拿到的数据给线程池去消费;但是也不能一直不停的去执行操作,如果list已经没有数据了去操作也没有任何意义,不然就太浪费资源了,幸好List中有一个BLPOP阻塞原语,如果list中有数据就会立马返回,如果没有数据就会一直阻塞在那里,直到有数据返回,可以设置阻塞的超时时间,超时会返回NULL;第一次去获取N个待消费的任务扔进到消费线程池中;如果获取到了0个,那么我们就立马用BLPOP来阻塞,等有元素的时候 BLPOP就返回数据了,下次就可以尝试去LrangeAndLTrim一次了. 通过BLPOP阻塞,我们避免了频繁的去请求redis,并且更重要的是提高了实时性;2.批量获取的数量和消费线程池的阻塞队列执行上面的一次获取N个元素是不定的,这个要看线程池的maxPoolSize 最大线程数量; 因为避免消费的任务过多而放入线程池的阻塞队列, 放入阻塞队列有宕机丢失任务的风险,关机重启的时候还要讲阻塞队列中的任务重新放入List中增加了复杂性;所以我们每次LrangeAndLTrim获取的元素不能大于当前线程池可用的线程数; 这样的一个控制可用用信号量Semaphore来做Codis集群对BLPOP的影响如果redis集群用了codis方案或者Twemproxy方案; 他们不支持BLPOP的命令;codis不支持的命令集合那么就不能利用BLPOP来防止频繁请求redis;那么退而求其次改成每秒执行一次LrangeAndLTrim操作;集群对Lua的影响Lua脚本的执行只能在单机器上, 集群的环境下如果想要执行Lua脚本不出错,那么Lua脚本中的所有key必须落在同一台机器;为了支持集群操作Lua,我们利用hashtag; 用{}把三个jey的关键词包起来;{projectName}:Redis_Delay_Table{projectName}:Redis_Delay_Table{projectName}:RD_LIST_TOPIC那么所有的数据就会在同一台机器上了重试机制消费者回调接口如果抛出异常了,或者执行超时了,那么会将这个Job重新放入到RD_LIST_TOPIC中等待被下一次消费;默认重试2次;可以设置不重试;超时机制超时机制的主要思路都一样,就是监听一个线程的执行时间超过设定值之后抛出异常打断方法的执行;这是使用的方式是 利用Callable接口实现异步超时处理public class TimeoutUtil { /**执行用户回调接口的 线程池; 计算回调接口的超时时间 **/ private static ExecutorService executorService = Executors.newCachedThreadPool(); /** * 有超时时间的方法 * @param timeout 时间秒 * @return */ public static void timeoutMethod(long timeout, Function function) throws InterruptedException, ExecutionException, TimeoutException { FutureTask futureTask = new FutureTask(()->(function.apply(""))); executorService.execute(futureTask); //new Thread(futureTask).start(); try { futureTask.get(timeout, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { //e.printStackTrace(); futureTask.cancel(true); throw e; } } } 这种方式有一点不好就是太费线程了,相当于线程使用翻了一倍;但是相比其他的方式,这种算是更好一点的优雅停机在Jvm那里注册一个 Runtime.getRuntime().addShutdownHook(Runnable)停机回调接口;在这里面做好善后工作;关闭异步AddJob线程池关闭每分钟唤醒线程关闭搬运线程 while(!stop)的形式关闭所有的topic监听线程 while(!stop)的形式关闭关闭所有topic的消费线程 ;先调用shutdown;再executor.awaitTermination(20, TimeUnit.SECONDS);检查是否还有剩余的线程任务没有执行完; 如果还没有执行完则等待执行完;最多等待20秒之后强制调用shutdownNow强制关闭;关闭重试线程 while(!stop)的形式关闭 异常未消费Job重入List线程池优雅停止线程一般是用下面的方式①、 while(!stop)的形式 用标识位来停止线程②.先 调用executor.shutdown(); 阻止接受新的任务;然后等待当前正在执行的任务执行完; 如果有阻塞则需要调用executor.shutdownNow()强制结束;所以要给一个等待时间; /** * shutdownNow 终止线程的方法是通过调用Thread.interrupt()方法来实现的 * 如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。 * 上面的情况中断之后还是可以再执行finally里面的方法的; * 但是如果是其他的情况 finally是不会被执行的 * @param executor */ public static void closeExecutor(ExecutorService executor, String executorName) { try { //新的任务不进队列 executor.shutdown(); //给10秒钟没有停止完强行停止; if(!executor.awaitTermination(20, TimeUnit.SECONDS)) { logger.warn("线程池: {},{}没有在20秒内关闭,则进行强制关闭",executorName,executor); List<Runnable> droppedTasks = executor.shutdownNow(); logger.warn("线程池: {},{} 被强行关闭,阻塞队列中将有{}个将不会被执行.", executorName,executor,droppedTasks.size() ); } logger.info("线程池:{},{} 已经关闭...",executorName,executor); } catch (InterruptedException e) { logger.info("线程池:{},{} 打断...",executorName,executor); } }BLPOP阻塞的情况如何优雅停止监听redis的线程如果不是在codis集群的环境下,BLPOP是可以很方便的阻塞线程的;但是停机的时候可能会有点问题;假如正在关机,当前线程正在BLPOP阻塞, 那关机线程等我们20秒执行, 刚好在倒数1秒的时候BLPOP获取到了数据,丢给消费线程去消费;如果消费线程1秒执行不完,那么20秒倒计时到了,强制关机,那么这个任务就会被丢失了; 怎么解决这个问题呢?①. 不用BLPOP, 每次都sleep一秒去调用LrangeAndLTrim操作;②.关机的时候杀掉 redis的blpop客户端; 杀掉之后 BLPOP会立马返回null; 进入下一个循环体;不足因为Redis的持久化特性,做不到消息完全不丢失,如果要保证完成不丢失,Redis的持久化刷盘策略要收紧因为Codis不能使用BLPOP这种阻塞的形式,在获取消费任务的时候用了每秒一次去获取,有点浪费性能;支持消费者多实例部署,但是可能存在不能均匀的分配到每台机器上去消费;虽然支持redis集群,但是其实是伪集群,因为Lua脚本的原因,让他们都只能落在一台机器上;总结实时性正常情况下 消费的时间误差不超过1秒钟; 极端情况下,一台实例宕机,另外的实例nextTime很迟; 那么最大误差是1分钟; 真正的误差来自于业务方的接口的消费速度QPS完全视业务方的消费速度而定; 延迟队列不是瓶颈
最近生产环境出现了一个问题,就是Job服务日志好端端的不打印日志了,服务也没有挂, 现在将此次问题解决过程记录下来~问题描述生产环境有一台Job服务器,是专门用来跑所有定时任务的,然后有一天发现定时任务好像没有执行,所以上Job服务器查看日志,结果发现的情况是:最后打印的是昨天晚上九点半的,到我看的时候就一直没有日志,没有日志就没有执行Job;当时为了快速解决问题就重启了服务器,Job就正常执行了;后来第二天上去看的时候居然又停止了, 但是Job的JVM是还在的,并没有挂掉,就是没有日志,看起来就是被阻塞了一样;这个时候又把生成环境重启让他先正常执行,然后立马去测试环境看看能不能重现, 结果测试环境的日志也是一样问题代码//允许异步执行 Schedule @EnableAsync @Component public class TestSchedule { private static final Logger LOGGER = LoggerFactory.getLogger(LotterySchedule.class); // 使用线程池myAsync来执行这个Job @Async("myAsync") @Scheduled(cron = "0/1 * * * * ?") public void testDoGet(){ LOGGER.info("\ntestDoGet:"+Thread.currentThread()); //业务代码:里面调用了 String json = HttpUtil.doGet(url);来调用第三方接口 HttpUtil.doGet("www.baidu.com") } //这里没有用异步执行,单线程执行 @Scheduled(cron = "0/1 * * * * ?") public void testPrint(){ LOGGER.info("\ntestPrint:"+Thread.currentThread()); } } 然后看看线程池的代码@Configuration @EnableAsync public class ExecutorConfig { /** Set the ThreadPoolExecutor's core pool size. */ private int corePoolSize = 10; /** Set the ThreadPoolExecutor's maximum pool size. */ private int maxPoolSize = 25; /** Set the capacity for the ThreadPoolExecutor's BlockingQueue. */ private int queueCapacity = 10; @Bean public Executor myAsync() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(maxPoolSize); executor.setQueueCapacity(queueCapacity); executor.setThreadNamePrefix("MyJobExecutor-"); // rejection-policy:当pool已经达到max size的时候,如何处理新任务 // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } } 1.查看JVM情况Linux使用jstat命令查看jvm的GC情况jps 查询Jvm进程号查询Jvm jstat -gc 21738 5000发现Jvm好像没有出现频繁GC,GC处理异常的情况,而且Jvm启动也配置了:+HeapDumpOnOutOfMemoryError;但是没有看到内存溢出的Dump文件;排除 Jvm异常的情况2.查看线程栈分析jps 查询Jvm进程号jstack -l 22741 查询线程栈信息"MyJobExecutor-2" #25 prio=5 os_prio=31 tid=0x00007fc7f7374800 nid=0xa203 waiting on condition [0x0000700004def000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x0000000742196c58> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:380) at org.apache.http.pool.AbstractConnPool.access$200(AbstractConnPool.java:69) at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:246) - locked <0x00000007969b6910> (a org.apache.http.pool.AbstractConnPool$2) at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:193) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:303) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager$1.get(PoolingHttpClientConnectionManager.java:279) at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:191) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:185) at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108) at com.xxx.util.HttpUtil.doGet(HttpUtil.java:97) at com.xxx.util.HttpUtil.doGet(HttpUtil.java:70) 省略 at at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:746) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.transaction.interceptor.TransactionInterceptor$$Lambda$392/226041624.proceedWithInvocation(Unknown Source) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) at com.bfc.service.impl.LotteryHbkuai3MainServiceImpl$$EnhancerBySpringCGLIB$$4d3de61.publishPreByJob(<generated>) at com.bfc.job.LotterySchedule.publishByJob(LotterySchedule.java:45) at com.bfc.job.LotterySchedule$$FastClassBySpringCGLIB$$75ac0373.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:746) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115) at org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$391/524684837.call(Unknown Source) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Locked ownable synchronizers: - <0x0000000741db91a0> (a java.util.concurrent.ThreadPoolExecutor$Worker) "MyJobExecutor-1" #24 prio=5 os_prio=31 tid=0x00007fc7f5ff7800 nid=0xa403 waiting on condition [0x0000700004cec000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x0000000742196c58> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:380) at org.apache.http.pool.AbstractConnPool.access$200(AbstractConnPool.java:69) at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:246) - locked <0x00000007968318d8> (a org.apache.http.pool.AbstractConnPool$2) at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:193) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:303) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager$1.get(PoolingHttpClientConnectionManager.java:279) at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:191) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:185) at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108) at com.xxx.util.HttpUtil.doGet(HttpUtil.java:97) at com.xxx.util.HttpUtil.doGet(HttpUtil.java:70) at 省略 at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:746) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.transaction.interceptor.TransactionInterceptor$$Lambda$392/226041624.proceedWithInvocation(Unknown Source) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) at com.bfc.service.impl.LotteryHbkuai3MainServiceImpl$$EnhancerBySpringCGLIB$$4d3de61.publishPreByJob(<generated>) at com.bfc.job.LotterySchedule.publishByJob(LotterySchedule.java:45) at com.bfc.job.LotterySchedule$$FastClassBySpringCGLIB$$75ac0373.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:746) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115) at org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$391/524684837.call(Unknown Source) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Locked ownable synchronizers: - <0x0000000741da4a28> (a java.util.concurrent.ThreadPoolExecutor$Worker) 分析问题通过栈信息可以发现,基本上所有的线程都被阻塞了,都在wait;重点看 MyJobExecutor-" 开头的线程都在wait同一个lock,并且代码发生的地方是 HttpUtil.doGet 方法;这么看来好像是HttpUtil.doGet 发生了阻塞;然后分析问题代码,结果发现这个代码是这样的 private static PoolingHttpClientConnectionManager connMgr; private static RequestConfig requestConfig; private static final int MAX_TIMEOUT = 7000; private static CloseableHttpClient httpClient ; static { // 设置连接池 connMgr = new PoolingHttpClientConnectionManager(); // 设置连接池大小 connMgr.setMaxTotal(8); connMgr.setDefaultMaxPerRoute(connMgr.getMaxTotal()); RequestConfig.Builder configBuilder = RequestConfig.custom(); // 设置连接超时 configBuilder.setConnectTimeout(MAX_TIMEOUT); // 设置读取超时 configBuilder.setSocketTimeout(MAX_TIMEOUT); // 设置从连接池获取连接实例的超时 configBuilder.setConnectionRequestTimeout(MAX_TIMEOUT); // 在提交请求之前 测试连接是否可用 configBuilder.setStaleConnectionCheckEnabled(true); requestConfig = configBuilder.build(); httpClient = HttpClients.custom().setConnectionManager(connMgr).build(); } /** * 发送 GET 请求(HTTP),不带输入数据 * @param url * @return */ public static String doGet(String url) { return doGet(url, new HashMap<String, Object>()); } /** * 发送 GET 请求(HTTP),K-V形式 * @param url * @param params * @return */ public static String doGet(String url, Map<String, Object> params) { String apiUrl = url; StringBuffer param = new StringBuffer(); int i = 0; for (String key : params.keySet()) { if (i == 0) { param.append("?"); }else { param.append("&"); } param.append(key).append("=").append(params.get(key)); i++; } apiUrl += param; String result = null; // HttpClient httpclient = new DefaultHttpClient(); try { HttpGet httpPost = new HttpGet(apiUrl); HttpResponse response = httpClient.execute(httpPost); HttpEntity entity = response.getEntity(); if (entity != null) { InputStream instream = entity.getContent(); result = IOUtils.toString(instream, "UTF-8"); } } catch (IOException e) { e.printStackTrace(); } return result; } } 注意看这个doGet(); 流没有关闭…因为流没有关闭,这个HttpClient连接池的连接一直没有回收回去,后面的线程又一直在调用这个doGet方法;但是又获取不到连接,所以就一直阻塞在哪里,直到连接超时HttpClient内部三个超时时间的区别然后myAsync 这个线程池的线程也是有限的, Schedule每秒都在执行,很快线程不够用了,然后就阻塞了testDoGet这个定时任务了;为了确认是 流未关闭的问题 我们可以看看服务器的TCP连接netstat -anp | grep 进程号可以看到有很多的80连接端口处于CLOSE_WAIT状态的; CLOSE_WAIT状态的原因与解决方法问题的原因找到了,那么解决的方法就很简单了,把HttpClient的连接的流关闭掉就行了 HttpEntity entity = response.getEntity(); httpStr = EntityUtils.toString(entity, "UTF-8"); EntityUtils.toString方法里面有关闭流的;这样改了就没有问题了;好像问题是解决了 但是怎么觉得哪里不对呢??定时任务 public void testDoGet()是用的线程池异步执行定时任务public void testPrint()是单线程执行的;讲道理就算testDoGet阻塞了,也不应该把testPrint阻塞了吧?再看jstack -l 信息"pool-3-thread-1" #23 prio=5 os_prio=31 tid=0x00007ff371c86800 nid=0x5803 waiting on condition [0x0000700004776000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000007421cb160> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:380) at org.apache.http.pool.AbstractConnPool.access$200(AbstractConnPool.java:69) at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:246) - locked <0x0000000797dbc900> (a org.apache.http.pool.AbstractConnPool$2) at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:193) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:303) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager$1.get(PoolingHttpClientConnectionManager.java:279) at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:191) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:185) at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108) at com.xxx.util.HttpUtil.doGet(HttpUtil.java:97) at com.xxx.util.HttpUtil.doGet(HttpUtil.java:70) at com.bfc.job.LotterySchedule$$FastClassBySpringCGLIB$$75ac0373.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:746) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115) at org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$401/476029857.call(Unknown Source) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy.rejectedExecution(ThreadPoolExecutor.java:2022) at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823) at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369) at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.submit(ThreadPoolTaskExecutor.java:341) at org.springframework.aop.interceptor.AsyncExecutionAspectSupport.doSubmit(AsyncExecutionAspectSupport.java:284) at org.springframework.aop.interceptor.AsyncExecutionInterceptor.invoke(AsyncExecutionInterceptor.java:129) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) at org.springframework.aop.framework.CglibAopProxysun.reflect.GeneratedMethodAccessor96.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:93) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) 初步分析这个信息,说明 pool-3-thread-1(就是Schedule的主线程) 也去执行了我们的testDoGet;myAsync线程池不够用了,用了主线程来执行这个定时任务,然后又阻塞了,所以就阻塞了所有的其他定时任务什么鬼?为什么会这样,然后翻过去看 myAsync的线程池代码 @Bean public Executor myAsync() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(maxPoolSize); executor.setQueueCapacity(queueCapacity); executor.setThreadNamePrefix("MyJobExecutor-"); // rejection-policy:当pool已经达到max size的时候,如何处理新任务 // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }用的丢弃策略是 CallerRunsPolicy :它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务就是说线程池的队列也满了,就会触发丢弃策略,CallerRunsPolicy 是用调用线程池的那个线程来执行;
这也是在扩展 Mybatis generator 的时候遇到的问题,记录一下;在上一篇文章 如何继承Mybatis中的Mapper.xml文件很重要的一点就是要让两个Mapper.xml文件的命名空间相同,这样才能够实现继承;那么既然是自动生成插件,在生成原始 Mapper.xml的时候,我要如何去修改他的命名空间呢?例如SrcTestMapper.xml 的命名空间是<mapper namespace="com.test.dao.mapper.srctest.SrcTestMapper">那么如何按照我的意愿修改成<mapper namespace="com.test.dao.mapper.srctest.SrcTestMapperExt">呢?继承DefaultCommentGenerator类,并重写其中的方法package com.weidai.common.plugin; import com.weidai.common.util.StringUtil; import org.mybatis.generator.api.IntrospectedColumn; import org.mybatis.generator.api.IntrospectedTable; import org.mybatis.generator.api.dom.java.Field; import org.mybatis.generator.api.dom.xml.Attribute; import org.mybatis.generator.api.dom.xml.XmlElement; import org.mybatis.generator.internal.DefaultCommentGenerator; import java.util.List; import java.util.Properties; /** * 修改命名空间 * 去除 myabtis generator生成的注释 * Created by shirenchuang on 2018/6/25. */ public class CommentGenerator extends DefaultCommentGenerator { private Properties myPoperties = new Properties(); @Override public void addConfigurationProperties(Properties properties) { super.addConfigurationProperties(properties); //本地保存一份properties this.myPoperties.putAll(properties); } @Override public void addFieldComment(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) { super.addFieldComment(field, introspectedTable, introspectedColumn); if (introspectedColumn.getRemarks() != null && !"".equals(introspectedColumn.getRemarks())) { field.addJavaDocLine("/**"); field.addJavaDocLine(" * " + introspectedColumn.getRemarks()); addJavadocTag(field, false); field.addJavaDocLine(" */"); } } //将 namespace修改掉 @Override public void addRootComment(XmlElement rootElement) { super.addRootComment(rootElement); Object replaceNamespace = myPoperties.get("replaceNamespace"); if(null==replaceNamespace||replaceNamespace.toString().equals("false"))return; List<Attribute> lists = rootElement.getAttributes(); int delIndex = -1;String orginNameSpace=""; for(int i = 0;i<lists.size();i++){ if(lists.get(i).getName().equals("namespace")){ orginNameSpace = lists.get(i).getValue(); //if(orginNameSpace.endsWith("Ext"))break; delIndex = i; break; } } if(delIndex!=-1){ lists.remove(delIndex); rootElement.getAttributes().add(new Attribute("namespace", orginNameSpace+"Ext")); } } } 然后generatorConfig.xml 修改一下 <!-- 修改命名空间 --> <commentGenerator type="com.weidai.common.plugin.CommentGenerator"> <property name="suppressAllComments" value="true" /> <property name="suppressDate" value="true"/> </commentGenerator>注意一下 这个commentGenerator放置的顺序,它一定时要在 property 和 plugin 后面的;关于上面重写方法 @Override public void addConfigurationProperties(Properties properties) { super.addConfigurationProperties(properties); //本地保存一份properties this.myPoperties.putAll(properties); }主要作用就是将properties保存一份到我们的实现类里面;然后我们可以设置属性,来做一些事情;比如这里,我需要一个开关是否需要修改namespace;只需要将配置文件中的commentGenerator加上 <property name="replaceNamespace" value="true"/> <commentGenerator type="com.weidai.common.plugin.CommentGenerator"> <property name="suppressAllComments" value="true" /> <property name="suppressDate" value="true"/> <property name="replaceNamespace" value="true"/> </commentGenerator>然后做一下判断就好了Object replaceNamespace = myPoperties.get("replaceNamespace"); if(null==replaceNamespace||replaceNamespace.toString().equals("false"))return;
最近在写一个 Mybatis 代码自动生成插件,用的是Mybatis来扩展,其中有一个需求就是 生成javaMapper文件和 xmlMapper文件的时候 希望另外生成一个扩展类和扩展xml文件。原文件不修改,只存放一些基本的信息,开发过程中只修改扩展的Ext文件形式如下:SrcTestMapper.javapackage com.test.dao.mapper.srctest; import com.test.dao.model.srctest.SrcTest; import com.test.dao.model.srctest.SrcTestExample; import java.util.List; import org.apache.ibatis.annotations.Param; public interface SrcTestMapper { long countByExample(SrcTestExample example); int deleteByExample(SrcTestExample example); int deleteByPrimaryKey(Integer id); int insert(SrcTest record); int insertSelective(SrcTest record); List<SrcTest> selectByExample(SrcTestExample example); SrcTest selectByPrimaryKey(Integer id); int updateByExampleSelective(@Param("record") SrcTest record, @Param("example") SrcTestExample example); int updateByExample(@Param("record") SrcTest record, @Param("example") SrcTestExample example); int updateByPrimaryKeySelective(SrcTest record); int updateByPrimaryKey(SrcTest record); }SrcTestMapperExt.javapackage com.test.dao.mapper.srctest; import com.test.dao.model.srctest.SrcTest; import org.apache.ibatis.annotations.Param; import javax.annotation.Resource; import java.util.List; /** * SrcTestMapperExt接口 * Created by shirenchuang on 2018/6/30. */ @Resource public interface SrcTestMapperExt extends SrcTestMapper { List<SrcTest> selectExtTest(@Param("age") int age); } SrcTestMapper.xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.test.dao.mapper.srctest.SrcTestMapperExt"> <resultMap id="BaseResultMap" type="com.test.dao.model.srctest.SrcTest"> <id column="id" jdbcType="INTEGER" property="id" /> <result column="name" jdbcType="VARCHAR" property="name" /> <result column="age" jdbcType="INTEGER" property="age" /> <result column="ctime" jdbcType="BIGINT" property="ctime" /> </resultMap> <!-- 省略....--> </mapper>SrcTestMapperExt.xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.test.dao.mapper.srctest.SrcTestMapperExt"> <select id="selectExtTest" resultMap="BaseResultMap"> select * from src_test where age>#{age} </select> </mapper>注意:这里返回的resultMap=“BaseResultMap” 这个Map并没有再这个xml中定义,这样能使用吗?上面是我生成的代码;并且能够正常使用;那么SrcTestMapperExt.xml是如何继承SrcTestMapper.xml中的定义的呢?1. 修改命名空间,使他们的命名空间相同,namespace=“com.test.dao.mapper.srctest.SrcTestMapperExt”2. 光这样还不够,因为这个时候你去运行的时候会报错Caused by: org.apache.ibatis.builder.BuilderException: Wrong namespace. Expected 'com.test.dao.mapper.srctest.SrcTestMapper' but found 'com.test.dao.mapper.srctest.SrcTestMapperExt'.因为Mybatis中是必须要 xml的文件包名和文件名必须跟 Mapper.java对应起来的比如com.test.dao.mapper.srctest.SrcTestMapper.java这个相对应的是com.test.dao.mapper.srctest.SrcTestMapper.xml必须是这样子,没有例外,否则就会报错show the codeMapperBuilderAssistant public void setCurrentNamespace(String currentNamespace) { if (currentNamespace == null) { throw new BuilderException("The mapper element requires a namespace attribute to be specified."); } if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) { throw new BuilderException("Wrong namespace. Expected '" + this.currentNamespace + "' but found '" + currentNamespace + "'."); } this.currentNamespace = currentNamespace; }这个this.currentNamespace 和参数传进来的currentNamespace比较是否相等;参数传进来的currentNamespace就是我们xml中的<mapper namespace="com.test.dao.mapper.srctest.SrcTestMapperExt">值;然后this.currentNamespace是从哪里设置的呢?this.currentNamespace = currentNamespace;跟下代码:MapperAnnotationBuilder public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); Method[] methods = type.getMethods(); for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }看到 assistant.setCurrentNamespace(type.getName());它获取的是 type.getName() ;这个type的最终来源是MapperFactoryBean @Override protected void checkDaoConfig() { super.checkDaoConfig(); notNull(this.mapperInterface, "Property 'mapperInterface' is required"); Configuration configuration = getSqlSession().getConfiguration(); if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { try { configuration.addMapper(this.mapperInterface); } catch (Exception e) { logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e); throw new IllegalArgumentException(e); } finally { ErrorContext.instance().reset(); } } }看configuration.addMapper(this.mapperInterface);这行应该就明白了加载mapperInterface的时候会跟相应的xml映射,并且会去检验namespace是否跟mapperInterface相等!那么既然命名空间不能修改,那第一条不白说了?还怎么实现Mapper.xml的继承啊?别慌,既然是这样子,那我们可以让 MapperInterface 中的SrcTestMapper.java别被加载进来就行了啊!!只加载 MapperExt.java不就行了?3. 修改applicationContext.xml,让Mapper.java不被扫描Mapper.java接口扫描配置 <!-- Mapper接口所在包名,Spring会自动查找其下的Mapper --> <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.test.dao.mapper"/> <!-- 该属性实际上就是起到一个过滤的作用,如果设置了该属性,那么MyBatis的接口只有包含该注解,才会被扫描进去。 --> <property name="annotationClass" value="javax.annotation.Resource"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean>basePackage 把Mapper.java扫描进去没有关系,重点是<property name="annotationClass" value="javax.annotation.Resource"/>这样 MapperScanner会把没有配置注解的过滤掉;回头看我们的MapperExt.java配置文件是有加上注解的/** * SrcTestMapperExt接口 * Created by shirenchuang on 2018/6/30. */ @Resource public interface SrcTestMapperExt extends SrcTestMapper { List<SrcTest> selectExtTest(@Param("age") int age); }这样子之后,基本上问题就解决了,还有一个地方特别要注意一下的是.xml文件的配置 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <!-- 必须将mapper,和mapperExt也一起扫描--> <property name="mapperLocations" value="classpath:com/test/dao/mapper/**/*.xml"/> </bean>这样配置没有错,但是我之前的配置写成了<property name="mapperLocations" value="classpath:com/test/dao/mapper/**/*Mapper.xml"/>这样子 MapperExt.xml 没有被扫描进去,在我执行单元测试的时候 @Test public void selectExt(){ List<SrcTest> tests = srcTestService.selectExtTest(9); System.out.println(tests.toString()); }err_consoleorg.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.test.dao.mapper.srctest.SrcTestMapperExt.selectExtTest 但是执行 ````srcTestService.insertSelective(srcTest);不会出错 原因就是 insertSelective是在SrcTestMapper.xml中存在 ,已经被注册到com.test.dao.mapper.srctest.SrcTestMapperExt```命名空间了,但是selectExtTest由于没有被注册,所以报错了;
文章目录dubbo://问题1.将某个provider链接设置为10个,consumer不设置2.将一台comsumer的连接数配置成5个3.将consumer设置为懒连接 lazy="true"4.粘滞连接 sticky="true"5.actives="" 可建立连接数如果小于connections连接数的话tcp连接会一直尝试建立连接dubbo://Dubbo 缺省协议采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。反之,Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。Transporter: mina, netty, grizzySerialization: dubbo, hessian2, java, jsonDispatcher: all, direct, message, execution, connectionThreadPool: fixed, cached特性缺省协议,使用基于 mina 1.1.7 和 hessian 3.2.1 的 tbremoting 交互。连接个数:单连接连接方式:长连接传输协议:TCP传输方式:NIO 异步传输序列化:Hessian 二进制序列化适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo 协议传输大文件或超大字符串。适用场景:常规远程服务方法调用约束参数及返回值需实现 Serializable 接口参数及返回值不能自定义实现 List , Map , Number , Date , Calendar 等接口,只能用JDK 自带的实现,因为 hessian 会做特殊处理,自定义实现类中的属性值都会丢失。Hessian 序列化,只传成员属性值和值的类型,不传方法或静态变量,兼容情况详细查看官方文档问题看过一篇博客 讲解的是dubbotcp链接过多的情况dubbo连接池爆满然后跟着实验了一遍;先看下有总共有4个提供者 <!-- dubbo服务发布配置文件 --> <dubbo:service interface="com.**.MenuFacade" ref="menuFacadeImpl"/> <dubbo:service interface="com.**.MaterialFacade" ref="materialFacadeImpl"/> <dubbo:service interface="com.**.QrCodeFacade" ref="qrCodeFacadeImpl"/> <dubbo:service interface="com.**.WeChatCommonFacade" ref="weChatCommonFacadeImpl" /> 提供者provider端口是18220;有若干个消费者;先不做额外操作;先看一下有多少个tcp长连接netstat -an |grep 18220结果可以看到有8个连接1.将某个provider链接设置为10个,consumer不设置 <dubbo:service interface="com.*.WeChatCommonFacade" ref="weChatCommonFacadeImpl" connections="10" />启动然后查看tcp链接数netstat -an |grep 18220 | wc -l显示的结果为38;之前是8个中有三个消费者是消费WeChatCommonFacade服务的;现在变成了38,明显是多了很多个,这个多出来的是WeChatCommonFacade这个Provider跟它的消费者连接数,WeChatCommonFacade的消费者有三个3*10=30个链接数了;2.将一台comsumer的连接数配置成5个在之前的基础上,我们把其中一台ip为:..*.194 的consumer连接数改成5<dubbo:reference id="weChatCommonFacade" check="false" interface="com.*.WeChatCommonFacade" connections="5" />再看看提供者的tcp连接数总共有33个;少了5个,说明我们修改了consumer的连接数起作用了,以consumer为准了;(至于194的连接数有6个不用在意,多出的那个tcp链接是另一个消费者消费了另一个提供者)3.将consumer设置为懒连接 lazy=“true”<dubbo:reference id="weChatCommonFacade" check="false" interface="com.*.WeChatCommonFacade" connections="5" lazy="true" />netstat -an |grep 18220 | wc -l结果:28又少了五个连接;因为这个服务没有被调用,所以没有建立起tcp链接;等第一次调用这个服务的时候就会建立起这个tcp的长连接的;所以lazy延迟连接有利于减少长连接数;4.粘滞连接 sticky=“true”<dubbo:reference id="weChatCommonFacade" check="false" interface="com.*.WeChatCommonFacade" connections="5" sticky="true" />粘滞连接用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非该提供者挂了,再连另一台。粘滞连接将自动开启延迟连接,以减少长连接数。5.actives="" 可建立连接数如果小于connections连接数的话tcp连接会一直尝试建立连接
本文基于 SPRING注解。本文使用Oracle数据库。项目文件下载地址:http://download.csdn.net/detail/u010634066/8188965项目总图:现在lib中导入所有所需jar包:这里就不叙述了一:在SRC下创建一个Bean包;在bean下面添加实体类,实体类对应于数据表,其属性与数据表相同或多于数据表。/** * */ package com.szz.bean; import com.szz.base.bean.BaseObject; /** * @author Administrator * */ public class User extends BaseObject { private String ID; /** * @return the iD */ public String getID() { return ID; } /** * @param iD the iD to set */ public void setID(String iD) { ID = iD; } /** * @return the nAME */ public String getNAME() { return NAME; } /** * @param nAME the nAME to set */ public void setNAME(String nAME) { NAME = nAME; } /** * @return the pASSWORD */ public String getPASSWORD() { return PASSWORD; } /** * @param pASSWORD the pASSWORD to set */ public void setPASSWORD(String pASSWORD) { PASSWORD = pASSWORD; } private String NAME; private String PASSWORD; /* (non-Javadoc) * @see com.szz.base.bean.BaseObject#toString() */ /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return "User [ID=" + ID + ", NAME=" + NAME + ", PASSWORD=" + PASSWORD + "]"; } }二、创建com.szz.dao包;里面用来定义需要对数据进行操作的实体类型DAO接口/** * */ package com.szz.dao; import java.util.List; import com.szz.bean.User; /** * @author Administrator * */ public interface UserDao { /* * 查询 */ public List<User> selectAll(); public User findById(String id); public User findByUserName(String userName); public int countAll(); /* * 更新删除插入 */ public int insert(User user); public int update(User user); public int delete(String userName); /* //返回插入数据的ID public int findInsertUserID(User user);*/ /*//批处理 插入多条数据 public void insertUsers(List<User> users);*/ }三、创建包com.szz.tables.xml(这样命名好像不好 定义com.szz.Mappers比较直观一点) 这个是用来写sql语句的xml文件<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- 这里定义好命名空间 --> <mapper namespace="com.szz.dao.UserDao"> <select id="selectAll" resultType="User"> select * from SM_USER </select> <select id="countAll"> select count(*) c from SM_USER </select> <select id="findById" parameterType="String" resultType="User"> select * from SM_USER where ID=#{ID} </select> <select id="findByUserName" parameterType="String" resultType="User"> select * from SM_USER where NAME=#{NAME} </select> <!-- <select id="findInsertUserID" paramterType="Srtring"> select ID FROM SM_USER NAME =#{User.NAME} </select> --> <insert id="insert" parameterType="User"> insert into SM_USER(ID,NAME,PASSWORD) VALUES(#{ID},#{NAME},#{PASSWORD}) </insert> <update id="update" parameterType="User"> update SM_USER <set> <!-- 这里要注意后面的 逗号“,” 因为有多个参数需要用逗号隔开 否则会报错 --> <if test="NAME!=null">NAME=#{NAME},</if> <if test="PASSWORD!=null">PASSWORD=#{PASSWORD}</if> </set> where ID=#{ID} </update> <delete id="delete" parameterType="String"> delete FROM SM_USER WHERE ID=#{ID} </delete> </mapper>命名空间定义为我们需要对应的DAO接口;这里每个方法的ID都跟DAO里面的方法一一对应;还有说明一下parameterType="User"如果你没有在mybatis配置文件里面定义别名 这样写就会报错 你要把全类名写清楚 <typeAliases> <typeAlias type="com.szz.bean.User" alias="User"/> </typeAliases> 四、spring的配置文件 spring-context.xml<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 启动扫描szz下所有的注解--> <context:component-scan base-package="com.szz"/> <mvc:annotation-driven ignore-default-model-on-redirect="true"/> <mvc:default-servlet-handler/> <!-- 可通过注解控制事务 <tx:annotation-driven /> --> <!-- 导入外部的资源文件 一般都会把数据源的配置文件房子properties文件里面 然后用这种方式来导入 <context:property-placeholder location="classpath:jdbc.properties"/>--> <!-- 配置DataSource数据源 配置mysql方式 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8" /> <property name="username" value="root" /> <property name="password" value="root" /> <property name="maxActive" value="5" /> <property name="maxIdle" value="3" /> <property name="maxWait" value="1000" /> <property name="defaultAutoCommit" value="true" /> <property name="removeAbandoned" value="true" /> <property name="removeAbandonedTimeout" value="60" /> </bean> --> <!-- 配置DataSource数据源 oracle方式--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" > <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" /> <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:myoracle" /> <property name="username" value="SRC" /> <property name="password" value="src123456" /> </bean> <!-- (事务管理)transaction manager, use JtaTransactionManager for global tx <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> --> <!-- 创建SqlSessionFactory,同时指定数据源 <span id="blogcontent" style="font-family:tahoma, arial, 宋体, sans-serif;line-height: 24px; background-color: #ffffff;">SqlSession也是由SqlSessionFactory来产生的,但是Mybatis-Spring给我们封装了一个SqlSessionFactoryBean, 在这个bean里面还是通过SqlSessionFactoryBuilder来建立对应的SqlSessionFactory,进而获取到对应的SqlSession。 通过SqlSessionFactoryBean我们可以通过对其指定一些属性来提供Mybatis的一些配置信息。 所以接下来我们需要在Spring的applicationContext配置文件中定义一个SqlSessionFactoryBean。</span> --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="mapperLocations" value="classpath:com/szz/tables/xml/*.xml" /> <property name="configLocation" value="classpath:mybatis-config.xml"></property> </bean> <!-- jsp页面解析器,当Controller返回XXX字符串时,先通过拦截器,然后该类就会在/WEB-INF/views/目录下,查找XXX.jsp文件--> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <!-- Mapper接口所在包名,Spring会自动查找其下的Mapper <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.szz.dao" /> </bean> --> <!-- ,MapperFactoryBean会从它的getObject方法中获取对应的Mapper接口, 而getObject内部还是通过我们注入的属性调用 SqlSession接口的getMapper(Mapper接口)方法来返回对应的Mapper接口的 --> <!-- 用户Dao --> <bean id="userDao" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="com.szz.dao.UserDao" /> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean> </beans>配置文件说明: 详情见http://www.blogjava.net/ldwblog/archive/2013/07/10/401418.html在定义SqlSessionFactoryBean的时候,dataSource属性是必须指定的,它表示用于连接数据库的数据源。当然,我们也可以指定一些其他的属性,下面简单列举几个:mapperLocations:它表示我们的Mapper文件存放的位置,当我们的Mapper文件跟对应的Mapper接口处于同一位置的时候可以不用指定该属性的值。configLocation:用于指定Mybatis的配置文件位置。如果指定了该属性,那么会以该配置文件的内容作为配置信息构建对应的SqlSessionFactoryBuilder,但是后续属性指定的内容会覆盖该配置文件里面指定的对应内容。typeAliasesPackage:它一般对应我们的实体类所在的包,这个时候会自动取对应包中不包括包名的简单类名作为包括包名的别名。多个package之间可以用逗号或者分号等来进行分隔。typeAliases:数组类型,用来指定别名的。指定了这个属性后,Mybatis会把这个类型的短名称作为这个类型的别名,前提是该类上没有标注@Alias注解,否则将使用该注解对应的值作为此种类型的别名。plugins:数组类型,用来指定Mybatis的Interceptor。typeHandlersPackage:用来指定TypeHandler所在的包,如果指定了该属性,SqlSessionFactoryBean会自动把该包下面的类注册为对应的TypeHandler。多个package之间可以用逗号或者分号等来进行分隔。typeHandlers:数组类型,表示TypeHandler接下来就是在Spring的applicationContext文件中定义我们想要的Mapper对象对应的MapperFactoryBean了。通过MapperFactoryBean可以获取到我们想要的Mapper对象。MapperFactoryBean实现了Spring的FactoryBean接口,所以MapperFactoryBean是通过FactoryBean接口中定义的getObject方法来获取对应的Mapper对象的。在定义一个MapperFactoryBean的时候有两个属性需要我们注入,一个是Mybatis-Spring用来生成实现了SqlSession接口的SqlSessionTemplate对象的sqlSessionFactory;另一个就是我们所要返回的对应的Mapper接口了。定义好相应Mapper接口对应的MapperFactoryBean之后,我们就可以把我们对应的Mapper接口注入到由Spring管理的bean对象中了,比如Service bean对象。这样当我们需要使用到相应的Mapper接口时,MapperFactoryBean会从它的getObject方法中获取对应的Mapper接口,而getObject内部还是通过我们注入的属性调用SqlSession接口的getMapper(Mapper接口)方法来返回对应的Mapper接口的。这样就通过把SqlSessionFactory和相应的Mapper接口交给Spring管理实现了Mybatis跟Spring的整合。对应xml <!-- 用户Dao --> <bean id="userDao" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="com.szz.dao.UserDao" /> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean>MapperScannerConfigurer利用上面的方法进行整合的时候,我们有一个Mapper就需要定义一个对应的MapperFactoryBean,当我们的Mapper比较少的时候,这样做也还可以,但是当我们的Mapper相当多时我们再这样定义一个个Mapper对应的MapperFactoryBean就显得速度比较慢了。为此Mybatis-Spring为我们提供了一个叫做MapperScannerConfigurer的类,通过这个类Mybatis-Spring会自动为我们注册Mapper对应的MapperFactoryBean对象。如果我们需要使用MapperScannerConfigurer来帮我们自动扫描和注册Mapper接口的话我们需要在Spring的applicationContext配置文件中定义一个MapperScannerConfigurer对应的bean。对于MapperScannerConfigurer而言有一个属性是我们必须指定的,那就是basePackage。basePackage是用来指定Mapper接口文件所在的基包的,在这个基包或其所有子包下面的Mapper接口都将被搜索到。多个基包之间可以使用逗号或者分号进行分隔。最简单的MapperScannerConfigurer定义就是只指定一个basePackage属性,如:Xml代码 <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.tiantian.mybatis.mapper" /> bean> 这样MapperScannerConfigurer就会扫描指定基包下面的所有接口,并把它们注册为一个个MapperFactoryBean对象。五、创建mybatis-config.xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- <environments default="development"> <environment id="development"> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="oracle.jdbc.driver.OracleDriver" /> <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:LEARN" /> <property name="username" value="system" /> <property name="password" value="src123456" /> </dataSource> </environment> </environments> --> <!--别名定义--> <typeAliases> <typeAlias type="com.szz.bean.User" alias="User"/> </typeAliases> <!-- 映射文件,存放sql语句的配置文件 --> <!-- <mappers> <mapper resource="com/szz/tables/xml/UserDaoMapper.xml" /> </mappers> --> </configuration>六、创建services接口package com.szz.service; import java.util.List; import com.szz.bean.User; public interface UserService { public List<User> getUsers(); /* * 濡傛灉ID涓虹┖灏辨壘username 濡傛灉username涓虹┖灏辨壘ID锛�閮藉~鎸夌収ID */ public User getUserInfo(String ID,String userName); public int getCount(); // public int saveUser(User user); public int insertUser(User user); public int updateUser(User user); public int deleteUser(String ID); }七‘services接口的实现类 serviceImpl/** * */ package com.szz.service.impl; import java.util.List; import org.mybatis.spring.SqlSessionTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.szz.bean.User; import com.szz.dao.UserDao; import com.szz.service.UserService; /** * @author Administrator * */ @Service("userService") public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; /* @Autowired private SqlSessionTemplate sessionTemplate;*/ public UserDao getUserDao() { return userDao; } /** * @param userDao the userDao to set */ public void setUserDao(UserDao userDao) { this.userDao = userDao; } @Override public List<User> getUsers() { // TODO Auto-generated method stub return userDao.selectAll(); //return sessionTemplate.getMapper(UserDao.class).selectAll(); } @Override public User getUserInfo(String ID, String userName) { // TODO Auto-generated method stub if(ID!=null){ return userDao.findById(ID); } else return userDao.findByUserName(userName); } @Override public int getCount() { // TODO Auto-generated method stub return userDao.countAll(); } @Override public int insertUser(User user) { // TODO Auto-generated method stub return userDao.insert(user); } @Override public int updateUser(User user) { // TODO Auto-generated method stub return userDao.update(user); } @Override public int deleteUser(String ID) { // TODO Auto-generated method stub return userDao.delete(ID); } }八、创建控制类controller包/**/** * */ package com.szz.action; import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; import com.szz.base.acion.BaseAction; import com.szz.bean.User; import com.szz.service.UserService; /** * @author Administrator * */ @Controller @RequestMapping(value="/user") public class UserAction extends BaseAction { @Autowired private UserService userService; @RequestMapping(value="/login",method=RequestMethod.POST) public String login(){ return "redirect:/user/userList"; } @RequestMapping(value="userList") public ModelAndView showAll(){ System.out.println("index......"); ModelAndView MV = new ModelAndView("user/index"); List<User> userList = new ArrayList<User>(); userList = userService.getUsers(); MV.addObject("userList",userList); return MV; } @RequestMapping(value="/add") public ModelAndView login(HttpServletRequest request,@RequestParam(value="username", required=true, defaultValue="szz") String name){ System.out.println("/user/login...."); ModelAndView mv = new ModelAndView("user/success"); mv.addObject("add", "娣诲姞"); return mv; } @RequestMapping(value="/edituser") public ModelAndView edit(@RequestParam(value="ID") String ID){ ModelAndView mv = new ModelAndView("user/edit"); User user = userService.getUserInfo(ID, null); mv.addObject("user",user); return mv; } @RequestMapping(value="/deleteuser") public String deleteuser(@RequestParam(value="ID") String ID){ userService.deleteUser(ID); return "redirect:/user/userList"; } @RequestMapping(value="/userset",method=RequestMethod.POST) public String user(@ModelAttribute("user")User user ) { System.out.println(user.getNAME()); return "user/success"; } @RequestMapping(value="/insertuser",method=RequestMethod.POST) public String insertUser( User userInfo ) throws Exception { userService.insertUser(userInfo); return "redirect:/user/userList"; } @RequestMapping(value="/updateuser",method=RequestMethod.POST) public String updateUser( User userInfo ) throws Exception { userService.updateUser(userInfo); return "redirect:/user/userList"; } }九、web.xml<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <servlet> <servlet-name>Dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value> classpath*:spring-context.xml </param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>十、一些页面之类的;index.jsp <body> <form action="/SpringMvcMybatisFreeMarker/user/login" method="post"> 用户ID:<input type="text" name="id" value=""/> <br>用户名称:<input type="text" id="username" name="username" value=""/> <br>用户密码:<input type="text" name="password" value=""/> <br><input type="submit" value="提交" /> </form> </body>user/index.jsp<body> <form action="user/insertuser" method="post"> <table> <tr> <th>ID</th> <td><input type="text" name="ID" /> </td> </tr> <tr> <tr> <th>账号</th> <td><input type="text" name="NAME" /> </td> </tr> <tr> <th>密码</th> <td><input type="text" name="PASSWORD" /></td> </tr> <tr> <td colspan="2"><input type="submit" value="添加"> </td> </tr> </table> </form> <table> <tr> <th>id</th> <th>账号</th> <th>密码</th> <th>功能</th> </tr> <c:forEach items="${userList}" var="user"> <tr> <td>${user.ID}</td> <td>${user.NAME}</td> <td>${user.PASSWORD}</td> <td><a href="user/edituser?ID=${user.ID}">修改</a> <a href="user/deleteuser?ID=${user.ID}">删除</a> </td> </tr> </c:forEach> </table> </body> </html>user/edit.jsp<body> <form action="user/updateuser" method="post"> <input type="text" name="ID" value="${user.ID}" /> <table> <tr> <th>账号</th> <td><input type="text" name="NAME" value="${user.NAME}" /></td> </tr> <tr> <th>密码</th> <td><input type="text" name="PASSWORD" value="${user.PASSWORD}" /></td> </tr> <tr> <td colspan="2"><input type="submit" value="修改"> </td> </tr> </table> </form> </body>
有人报案最近技术群里面有几个同学碰到了 删除Topic的问题, 怎么样也删除不掉,然后我协助排查之后,就做个记录,写篇文章,大家在碰到这类型的问题的时候应该怎么去排查【领取kafka大全PDF】【进滴滴技术交流群】: https://www.szzdzhp.com/szzInfo.html收集线索报not retrying deletion 异常版本:kafka_2.11-2.0.0删除前在执行重分配,但是失败了,强制停止数据迁移,手动删除了节点/admin/reassign_partitions再次重新删除提示异常Topic test is already marked for deletion所有Broker均在线delete.topic.enable=true检查了每个Broker都没有副本被删除,甚至也没有被标记为--delete调查线索从我们收集到的线索来看,有两个突破口not retrying deletionTopic test is already marked for deletion我们先看,第2个突破口,打开kafka_2.11-2.0.0源码,全局搜索关键字is already marked for deletion这个表示,你已经标记了这个topic删除了, 在zk上写入了节点/amin/delete_topics/{topicName}上面收集线索时候我们知道是它重新执行删除的时候抛出的异常,说明zk节点已经写入了,已经准备删除了;这里没有什么问题问题在于为什么没有执行删除呢? 所以下一个突破口就在于Not retrying deletion of topic ....通过源码我们可以看到,出现了这个异常表示的是:当前这个topic不符合重试删除的条件怎么样才符合重试删除条件?在删除队列topicsToBeDeleted里面;这个队列是从zk节点/amin/delete_topics获取的数据当前还未开始对该Topic进题删除; 判定条件是没有副本处于开始删除的状态「ReplicaDeletionStarted」(当然如果delete.topic.enable=false这条肯定满足)主题没有被标记为不符合删除条件; 不符合删除条件的都保存在topicsIneligibleForDeletion抽丝剥茧,接近真相上面的3个条件,通过对方了解到/amin/delete_topics 节点下面有数据, 线索排除让对方查询了Deletion started for replicas这个日志,日志表示的是哪些副本状态变更成「开始删除」 ,日志有查询到如下然后让查询Dead Replicas (%s) found for topic %s (这个表示的是哪些副本离线了) 也查询到如下从日志,和源码我们可以得出,Not retrying deletion of topic 的原因是: 删除流程已经开始,但是存在离线的或不可用的副本 ,哪些副本异常,从上面的Dead Replicas (%s) found for topic %s 的日志可以得知, 既然知道了原因,那么解决方案:聚焦副本为何离线了,让副本恢复正常就行了 不过这里我们还有再重点说一下第3种情况前面2个说完了,接着说一下topicsIneligibleForDeletion到底是什么,什么情况下才会放到这里面来呢?不符合Topic删除的条件是什么?Controller初始化的时候判断条件kafka_2.11-2.0.0 没有这个步骤数据正在迁移中判断数据是否在迁移中是通过判断topic的是否存在要新增或者删除的副本, 查询/brokers/topics/{topicName}节点中有没有这两个属性值topic副本所在Broker有宕机导致的副本不在线副本所在的数据目录log.dirs 存在脱机磁盘运行中判断条件发起的StopReplica 请求返回异常,加入不符合删除条件删除的过程中,发现该Topic 有副本重分配的操作 则加入不符合删除条件删除的过程,有副本下线了,则加入不符合删除条件开始执行副本重分配的操作, 则加入不符合删除条件结案经过深入源码排查走访,我们基本上确定了问题的根源副本离线,导致的删除流程不能完成; 通过查询日志,也锁定了那些个嫌疑犯,好家伙还是团伙作案最后的解决方案也很粗暴,找到副本不正常的那几台Broker, 重启 ...之后副本疯狂同步(其他一些topic数据同步);最终topic正常删除了排查手册为了以后出现同样类似的问题,我总结了一下问题的排查手段,给大家指明一条思路; 快速破案确保 delete.topic.enable=true ;配置文件查询确保当前该topic没有进行 「副本重分配」 , 查询zk节点/admin/reassign_partitions的值是否有该topic、或者 节点/brokers/topics/{topicName}节点里面的属性adding_replicas、removing_replicas有没有值确保所有副本所属Broker均在线确保副本均在线, (Broker在线并且log.dirs没有脱机), 搜日志"Dead Replicas " 关键字查询到哪些副本异常解放方案根据上面的排查顺序,对应不同的解决方案; 如果正在进行 「副本重分配」 那么等待分配完成就可以正常删除了如果是副本不在线,那么就去解决为啥不在线,该重启就重启幕后黑手这就完了吗? 「log.dir为什么会脱机呢?」 「脱机跟数据迁移有关系吗?」 根据以往的问题,好像数据迁移总是会伴随着一些删除上的问题导致数据目录脱机的原因的最终BOSS是 「副本重分配」吗?留下悬念, 下期再见!【领取kafka大全PDF】【进滴滴技术交流群】: https://www.szzdzhp.com/szzInfo.html
配套视频地址全套视频在同名公众号脚本参数sh bin/kafka-topic -help 查看更具体参数下面只是列出了跟 --create 相关的参数参数描述例子--bootstrap-server 指定kafka服务指定连接到的kafka服务; 如果有这个参数,则 --zookeeper可以不需要--bootstrap-server localhost:9092--zookeeper弃用, 通过zk的连接方式连接到kafka集群;--zookeeper localhost:2181 或者localhost:2181/kafka--replication-factor 副本数量,注意不能大于broker数量;如果不提供,则会用集群中默认配置--replication-factor 3--partitions分区数量当创建或者修改topic的时候,用这个来指定分区数;如果创建的时候没有提供参数,则用集群中默认值; 注意如果是修改的时候,分区比之前小会有问题--replica-assignment 副本分区分配方式;创建topic的时候可以自己指定副本分配情况;--replica-assignment BrokerId-0:BrokerId-1:BrokerId-2,BrokerId-1:BrokerId-2:BrokerId-0,BrokerId-2:BrokerId-1:BrokerId-0 ; 这个意思是有三个分区和三个副本,对应分配的Broker; 逗号隔开标识分区;冒号隔开表示副本--config <String: name=value>用来设置topic级别的配置以覆盖默认配置;只在--create 和--bootstrap-server 同时使用时候生效; 可以配置的参数列表请看文末附件例如覆盖两个配置 --config retention.bytes=123455 --config retention.ms=600001--command-config <String: command 文件路径>用来配置客户端Admin Client启动配置,只在--bootstrap-server 同时使用时候生效;例如:设置请求的超时时间 --command-config config/producer.proterties ; 然后在文件中配置 request.timeout.ms=300000--create命令方式; 表示当前请求是创建Topic--create创建Topic脚本zk方式(不推荐)bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 3 --topic test需要注意的是--zookeeper后面接的是kafka的zk配置, 假如你配置的是localhost:2181/kafka 带命名空间的这种,不要漏掉了 kafka版本 >= 2.2 支持下面方式(推荐)bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 3 --partitions 3 --topic test更多TopicCommand相关命令请看 1.【kafka运维】TopicCommand运维脚本当前分析的kafka源码版本为 kafka-2.5创建Topic 源码分析温馨提示: 如果阅读源码略显枯燥,你可以直接看源码总结以及后面部分首先我们找到源码入口处, 查看一下 kafka-topic.sh脚本的内容exec $(dirname $0)/kafka-run-class.sh kafka.admin.TopicCommand "$@"最终是执行了kafka.admin.TopicCommand这个类,找到这个地方之后就可以断点调试源码了,用IDEA启动记得配置一下入参比如: --create --bootstrap-server 127.0.0.1:9092 --partitions 3 --topic test_create_topic31. 源码入口上面的源码主要作用是根据是否有传入参数--zookeeper 来判断创建哪一种 对象topicService如果传入了--zookeeper 则创建 类 ZookeeperTopicService的对象否则创建类AdminClientTopicService的对象(我们主要分析这个对象)根据传入的参数类型判断是创建topic还是删除等等其他 判断依据是 是否在参数里传入了--create2. 创建AdminClientTopicService 对象val topicService = new AdminClientTopicService(createAdminClient(commandConfig, bootstrapServer))2.1 先创建 Adminobject AdminClientTopicService { def createAdminClient(commandConfig: Properties, bootstrapServer: Option[String]): Admin = { bootstrapServer match { case Some(serverList) => commandConfig.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, serverList) case None => } Admin.create(commandConfig) } def apply(commandConfig: Properties, bootstrapServer: Option[String]): AdminClientTopicService = new AdminClientTopicService(createAdminClient(commandConfig, bootstrapServer)) }如果有入参--command-config ,则将这个文件里面的参数都放到map commandConfig里面, 并且也加入bootstrap.servers的参数;假如配置文件里面已经有了bootstrap.servers配置,那么会将其覆盖将上面的commandConfig 作为入参调用Admin.create(commandConfig)创建 Admin; 这个时候调用的Client模块的代码了, 从这里我们就可以看出,我们调用kafka-topic.sh脚本实际上是kafka模拟了一个客户端Client来创建Topic的过程;3. AdminClientTopicService.createTopic 创建Topic topicService.createTopic(opts) case class AdminClientTopicService private (adminClient: Admin) extends TopicService { override def createTopic(topic: CommandTopicPartition): Unit = { //如果配置了副本副本数--replication-factor 一定要大于0 if (topic.replicationFactor.exists(rf => rf > Short.MaxValue || rf < 1)) throw new IllegalArgumentException(s"The replication factor must be between 1 and ${Short.MaxValue} inclusive") //如果配置了--partitions 分区数 必须大于0 if (topic.partitions.exists(partitions => partitions < 1)) throw new IllegalArgumentException(s"The partitions must be greater than 0") //查询是否已经存在该Topic if (!adminClient.listTopics().names().get().contains(topic.name)) { val newTopic = if (topic.hasReplicaAssignment) //如果指定了--replica-assignment参数;则按照指定的来分配副本 new NewTopic(topic.name, asJavaReplicaReassignment(topic.replicaAssignment.get)) else { new NewTopic( topic.name, topic.partitions.asJava, topic.replicationFactor.map(_.toShort).map(Short.box).asJava) } // 将配置--config 解析成一个配置map val configsMap = topic.configsToAdd.stringPropertyNames() .asScala .map(name => name -> topic.configsToAdd.getProperty(name)) .toMap.asJava newTopic.configs(configsMap) //调用adminClient创建Topic val createResult = adminClient.createTopics(Collections.singleton(newTopic)) createResult.all().get() println(s"Created topic ${topic.name}.") } else { throw new IllegalArgumentException(s"Topic ${topic.name} already exists") } }检查各项入参是否有问题adminClient.listTopics(),然后比较是否已经存在待创建的Topic;如果存在抛出异常;判断是否配置了参数--replica-assignment ; 如果配置了,那么Topic就会按照指定的方式来配置副本情况解析配置--config 配置放到 configsMap中; configsMap给到NewTopic对象调用adminClient.createTopics创建Topic; 它是如何创建Topic的呢?往下分析源码3.1 KafkaAdminClient.createTopics(NewTopic) 创建Topic @Override public CreateTopicsResult createTopics(final Collection<NewTopic> newTopics, final CreateTopicsOptions options) { //省略部分源码... Call call = new Call("createTopics", calcDeadlineMs(now, options.timeoutMs()), new ControllerNodeProvider()) { @Override public CreateTopicsRequest.Builder createRequest(int timeoutMs) { return new CreateTopicsRequest.Builder( new CreateTopicsRequestData(). setTopics(topics). setTimeoutMs(timeoutMs). setValidateOnly(options.shouldValidateOnly())); } @Override public void handleResponse(AbstractResponse abstractResponse) { //省略 } @Override void handleFailure(Throwable throwable) { completeAllExceptionally(topicFutures.values(), throwable); } }; }这个代码里面主要看下Call里面的接口; 先不管Kafka如何跟服务端进行通信的细节; 我们主要关注创建Topic的逻辑;createRequest会构造一个请求参数CreateTopicsRequest 例如下图选择ControllerNodeProvider这个节点发起网络请求可以清楚的看到, 创建Topic这个操作是需要Controller来执行的;4. 发起网络请求==>服务端客户端网络模型 5. Controller角色的服务端接受请求处理逻辑首先找到服务端处理客户端请求的 源码入口 ⇒ KafkaRequestHandler.run()主要看里面的 apis.handle(request) 方法; 可以看到客户端的请求都在request.bodyAndSize()里面5.1 KafkaApis.handle(request) 根据请求传递Api调用不同接口进入方法可以看到根据request.header.apiKey 调用对应的方法,客户端传过来的是CreateTopics5.2 KafkaApis.handleCreateTopicsRequest 处理创建Topic的请求 def handleCreateTopicsRequest(request: RequestChannel.Request): Unit = { // 部分代码省略 //如果当前Broker不是属于Controller的话,就抛出异常 if (!controller.isActive) { createTopicsRequest.data.topics.asScala.foreach { topic => results.add(new CreatableTopicResult().setName(topic.name). setErrorCode(Errors.NOT_CONTROLLER.code)) } sendResponseCallback(results) } else { // 部分代码省略 } adminManager.createTopics(createTopicsRequest.data.timeoutMs, createTopicsRequest.data.validateOnly, toCreate, authorizedForDescribeConfigs, handleCreateTopicsResults) } } 判断当前处理的broker是不是Controller,如果不是Controller的话直接抛出异常,从这里可以看出,CreateTopic这个操作必须是Controller来进行, 出现这种情况有可能是客户端发起请求的时候Controller已经变更;鉴权 [【Kafka源码】kafka鉴权机制]()调用adminManager.createTopics()5.3 adminManager.createTopics()创建主题并等等主题完全创建,回调函数将会在超时、错误、或者主题创建完成时触发该方法过长,省略部分代码def createTopics(timeout: Int, validateOnly: Boolean, toCreate: Map[String, CreatableTopic], includeConfigsAndMetatadata: Map[String, CreatableTopicResult], responseCallback: Map[String, ApiError] => Unit): Unit = { // 1. map over topics creating assignment and calling zookeeper val brokers = metadataCache.getAliveBrokers.map { b => kafka.admin.BrokerMetadata(b.id, b.rack) } val metadata = toCreate.values.map(topic => try { //省略部分代码 //检查Topic是否存在 //检查 --replica-assignment参数和 (--partitions || --replication-factor ) 不能同时使用 // 如果(--partitions || --replication-factor ) 没有设置,则使用 Broker的配置(这个Broker肯定是Controller) // 计算分区副本分配方式 createTopicPolicy match { case Some(policy) => //省略部分代码 adminZkClient.validateTopicCreate(topic.name(), assignments, configs) if (!validateOnly) adminZkClient.createTopicWithAssignment(topic.name, configs, assignments) case None => if (validateOnly) //校验创建topic的参数准确性 adminZkClient.validateTopicCreate(topic.name, assignments, configs) else //把topic相关数据写入到zk中 adminZkClient.createTopicWithAssignment(topic.name, configs, assignments) } }做一些校验检查 ①.检查Topic是否存在②. 检查 --replica-assignment参数和 (--partitions || --replication-factor ) 不能同时使用③.如果(--partitions || --replication-factor ) 没有设置,则使用 Broker的配置(这个Broker肯定是Controller)④.计算分区副本分配方式createTopicPolicy 根据Broker是否配置了创建Topic的自定义校验策略; 使用方式是自定义实现org.apache.kafka.server.policy.CreateTopicPolicy接口;并 在服务器配置 create.topic.policy.class.name=自定义类; 比如我就想所有创建Topic的请求分区数都要大于10; 那么这里就可以实现你的需求了createTopicWithAssignment把topic相关数据写入到zk中; 进去分析一下5.4 写入zookeeper数据我们进入到` adminZkClient.createTopicWithAssignment(topic.name, configs, assignments)`看看有哪些数据写入到了zk中; def createTopicWithAssignment(topic: String, config: Properties, partitionReplicaAssignment: Map[Int, Seq[Int]]): Unit = { validateTopicCreate(topic, partitionReplicaAssignment, config) // 将topic单独的配置写入到zk中 zkClient.setOrCreateEntityConfigs(ConfigType.Topic, topic, config) // 将topic分区相关信息写入zk中 writeTopicPartitionAssignment(topic, partitionReplicaAssignment.mapValues(ReplicaAssignment(_)).toMap, isUpdate = false) } 源码就不再深入了,这里直接详细说明一下写入Topic配置信息先调用SetDataRequest请求往节点 /config/topics/Topic名称 写入数据; 这里一般这个时候都会返回 NONODE (NoNode);节点不存在; 假如zk已经存在节点就直接覆盖掉节点不存在的话,就发起CreateRequest请求,写入数据; 并且节点类型是持久节点这里写入的数据,是我们入参时候传的topic配置--config; 这里的配置会覆盖默认配置写入Topic分区副本信息将已经分配好的副本分配策略写入到 /brokers/topics/Topic名称 中; 节点类型 持久节点具体跟zk交互的地方在ZookeeperClient.send() 这里包装了很多跟zk的交互;6. Controller监听 /brokers/topics/Topic名称, 通知Broker将分区写入磁盘Controller 有监听zk上的一些节点; 在上面的流程中已经在zk中写入了 /brokers/topics/Topic名称 ; 这个时候Controller就监听到了这个变化并相应;KafkaController.processTopicChange private def processTopicChange(): Unit = { //如果处理的不是Controller角色就返回 if (!isActive) return //从zk中获取 `/brokers/topics 所有Topic val topics = zkClient.getAllTopicsInCluster //找出哪些是新增的 val newTopics = topics -- controllerContext.allTopics //找出哪些Topic在zk上被删除了 val deletedTopics = controllerContext.allTopics -- topics controllerContext.allTopics = topics registerPartitionModificationsHandlers(newTopics.toSeq) val addedPartitionReplicaAssignment = zkClient.getFullReplicaAssignmentForTopics(newTopics) deletedTopics.foreach(controllerContext.removeTopic) addedPartitionReplicaAssignment.foreach { case (topicAndPartition, newReplicaAssignment) => controllerContext.updatePartitionFullReplicaAssignment(topicAndPartition, newReplicaAssignment) } info(s"New topics: [$newTopics], deleted topics: [$deletedTopics], new partition replica assignment " + s"[$addedPartitionReplicaAssignment]") if (addedPartitionReplicaAssignment.nonEmpty) onNewPartitionCreation(addedPartitionReplicaAssignment.keySet) }从zk中获取 /brokers/topics 所有Topic跟当前Broker内存中所有BrokercontrollerContext.allTopics的差异; 就可以找到我们新增的Topic; 还有在zk中被删除了的Broker(该Topic会在当前内存中remove掉)从zk中获取/brokers/topics/{TopicName} 给定主题的副本分配。并保存在内存中执行onNewPartitionCreation;分区状态开始流转6.1 onNewPartitionCreation 状态流转关于Controller的状态机 详情请看: 【kafka源码】Controller中的状态机 /** * This callback is invoked by the topic change callback with the list of failed brokers as input. * It does the following - * 1. Move the newly created partitions to the NewPartition state * 2. Move the newly created partitions from NewPartition->OnlinePartition state */ private def onNewPartitionCreation(newPartitions: Set[TopicPartition]): Unit = { info(s"New partition creation callback for ${newPartitions.mkString(",")}") partitionStateMachine.handleStateChanges(newPartitions.toSeq, NewPartition) replicaStateMachine.handleStateChanges(controllerContext.replicasForPartition(newPartitions).toSeq, NewReplica) partitionStateMachine.handleStateChanges( newPartitions.toSeq, OnlinePartition, Some(OfflinePartitionLeaderElectionStrategy(false)) ) replicaStateMachine.handleStateChanges(controllerContext.replicasForPartition(newPartitions).toSeq, OnlineReplica) }将待创建的分区状态流转为NewPartition;将待创建的副本 状态流转为NewReplica;将分区状态从刚刚的NewPartition流转为OnlinePartition 0. 获取`leaderIsrAndControllerEpochs`; Leader为副本的第一个; 1. 向zk中写入`/brokers/topics/{topicName}/partitions/` 持久节点; 无数据 2. 向zk中写入`/brokers/topics/{topicName}/partitions/{分区号}` 持久节点; 无数据 3. 向zk中写入`/brokers/topics/{topicName}/partitions/{分区号}/state` 持久节点; 数据为`leaderIsrAndControllerEpoch`向副本所属Broker发送[leaderAndIsrRequest]()请求向所有Broker发送[UPDATE_METADATA ]()请求将副本状态从刚刚的NewReplica流转为OnlineReplica ,更新下内存关于分区状态机和副本状态机详情请看【kafka源码】Controller中的状态机7. Broker收到LeaderAndIsrRequest 创建本地Log上面步骤中有说到向副本所属Broker发送[leaderAndIsrRequest]()请求,那么这里做了什么呢其实主要做的是 创建本地Log代码太多,这里我们直接定位到只跟创建Topic相关的关键代码来分析KafkaApis.handleLeaderAndIsrRequest->replicaManager.becomeLeaderOrFollower->ReplicaManager.makeLeaders...LogManager.getOrCreateLog /** * 如果日志已经存在,只返回现有日志的副本否则如果 isNew=true 或者如果没有离线日志目录,则为给定的主题和给定的分区创建日志 否则抛出 KafkaStorageException */ def getOrCreateLog(topicPartition: TopicPartition, config: LogConfig, isNew: Boolean = false, isFuture: Boolean = false): Log = { logCreationOrDeletionLock synchronized { getLog(topicPartition, isFuture).getOrElse { // create the log if it has not already been created in another thread if (!isNew && offlineLogDirs.nonEmpty) throw new KafkaStorageException(s"Can not create log for $topicPartition because log directories ${offlineLogDirs.mkString(",")} are offline") val logDirs: List[File] = { val preferredLogDir = preferredLogDirs.get(topicPartition) if (isFuture) { if (preferredLogDir == null) throw new IllegalStateException(s"Can not create the future log for $topicPartition without having a preferred log directory") else if (getLog(topicPartition).get.dir.getParent == preferredLogDir) throw new IllegalStateException(s"Can not create the future log for $topicPartition in the current log directory of this partition") } if (preferredLogDir != null) List(new File(preferredLogDir)) else nextLogDirs() } val logDirName = { if (isFuture) Log.logFutureDirName(topicPartition) else Log.logDirName(topicPartition) } val logDir = logDirs .toStream // to prevent actually mapping the whole list, lazy map .map(createLogDirectory(_, logDirName)) .find(_.isSuccess) .getOrElse(Failure(new KafkaStorageException("No log directories available. Tried " + logDirs.map(_.getAbsolutePath).mkString(", ")))) .get // If Failure, will throw val log = Log( dir = logDir, config = config, logStartOffset = 0L, recoveryPoint = 0L, maxProducerIdExpirationMs = maxPidExpirationMs, producerIdExpirationCheckIntervalMs = LogManager.ProducerIdExpirationCheckIntervalMs, scheduler = scheduler, time = time, brokerTopicStats = brokerTopicStats, logDirFailureChannel = logDirFailureChannel) if (isFuture) futureLogs.put(topicPartition, log) else currentLogs.put(topicPartition, log) info(s"Created log for partition $topicPartition in $logDir with properties " + s"{${config.originals.asScala.mkString(", ")}}.") // Remove the preferred log dir since it has already been satisfied preferredLogDirs.remove(topicPartition) log } } }如果日志已经存在,只返回现有日志的副本否则如果 isNew=true 或者如果没有离线日志目录,则为给定的主题和给定的分区创建日志 否则抛出 KafkaStorageException详细请看 [【kafka源码】LeaderAndIsrRequest请求]()源码总结如果上面的源码分析,你不想看,那么你可以直接看这里的简洁叙述根据是否有传入参数--zookeeper 来判断创建哪一种 对象topicService如果传入了--zookeeper 则创建 类 ZookeeperTopicService的对象否则创建类AdminClientTopicService的对象(我们主要分析这个对象)如果有入参--command-config ,则将这个文件里面的参数都放到mapl类型 commandConfig里面, 并且也加入bootstrap.servers的参数;假如配置文件里面已经有了bootstrap.servers配置,那么会将其覆盖将上面的commandConfig 作为入参调用Admin.create(commandConfig)创建 Admin; 这个时候调用的Client模块的代码了, 从这里我们就可以猜测,我们调用kafka-topic.sh脚本实际上是kafka模拟了一个客户端Client来创建Topic的过程;一些异常检查 ①.如果配置了副本副本数--replication-factor 一定要大于0 ②.如果配置了--partitions 分区数 必须大于0 ③.去zk查询是否已经存在该Topic判断是否配置了参数--replica-assignment ; 如果配置了,那么Topic就会按照指定的方式来配置副本情况解析配置--config 配置放到configsMap中; configsMap给到NewTopic对象将上面所有的参数包装成一个请求参数CreateTopicsRequest ;然后找到是Controller的节点发起请求(ControllerNodeProvider)服务端收到请求之后,开始根据CreateTopicsRequest来调用创建Topic的方法; 不过首先要判断一下自己这个时候是不是Controller; 有可能这个时候Controller重新选举了; 这个时候要抛出异常服务端进行一下请求参数检查 ①.检查Topic是否存在 ②.检查 --replica-assignment参数和 (--partitions || --replication-factor ) 不能同时使用如果(--partitions || --replication-factor ) 没有设置,则使用 Broker的默认配置(这个Broker肯定是Controller)计算分区副本分配方式;如果是传入了 --replica-assignment;则会安装自定义参数进行组装;否则的话系统会自动计算分配方式; 具体详情请看 [【kafka源码】创建Topic的时候是如何分区和副本的分配规则 ]()createTopicPolicy 根据Broker是否配置了创建Topic的自定义校验策略; 使用方式是自定义实现org.apache.kafka.server.policy.CreateTopicPolicy接口;并 在服务器配置 create.topic.policy.class.name=自定义类; 比如我就想所有创建Topic的请求分区数都要大于10; 那么这里就可以实现你的需求了zk中写入Topic配置信息 发起CreateRequest请求,这里写入的数据,是我们入参时候传的topic配置--config; 这里的配置会覆盖默认配置;并且节点类型是持久节点;path = /config/topics/Topic名称zk中写入Topic分区副本信息 发起CreateRequest请求 ,将已经分配好的副本分配策略 写入到 /brokers/topics/Topic名称 中; 节点类型 持久节点Controller监听zk上面的topic信息; 根据zk上变更的topic信息;计算出新增/删除了哪些Topic; 然后拿到新增Topic的 副本分配信息; 并做一些状态流转向新增Topic所在Broker发送leaderAndIsrRequest请求,Broker收到发送leaderAndIsrRequest请求; 创建副本Log文件;Q&A创建Topic的时候 在Zk上创建了哪些节点接受客户端请求阶段:topic的配置信息 /config/topics/Topic名称 持久节点topic的分区信息/brokers/topics/Topic名称 持久节点Controller监听zk节点/brokers/topics变更阶段/brokers/topics/{topicName}/partitions/ 持久节点; 无数据向zk中写入/brokers/topics/{topicName}/partitions/{分区号} 持久节点; 无数据向zk中写入/brokers/topics/{topicName}/partitions/{分区号}/state 持久节点;创建Topic的时候 什么时候在Broker磁盘上创建的日志文件当Controller监听zk节点/brokers/topics变更之后,将新增的Topic 解析好的分区状态流转NonExistentPartition->NewPartition->OnlinePartition 当流转到OnlinePartition的时候会像分区分配到的Broker发送一个leaderAndIsrRequest请求,当Broker们收到这个请求之后,根据请求参数做一些处理,其中就包括检查自身有没有这个分区副本的本地Log;如果没有的话就重新创建;如果我没有指定分区数或者副本数,那么会如何创建我们都知道,如果我们没有指定分区数或者副本数, 则默认使用Broker的配置, 那么这么多Broker,假如不小心默认值配置不一样,那究竟使用哪一个呢? 那肯定是哪台机器执行创建topic的过程,就是使用谁的配置; 所以是谁执行的? 那肯定是Controller啊! 上面的源码我们分析到了,创建的过程,会指定Controller这台机器去进行;如果我手动删除了/brokers/topics/下的某个节点会怎么样?详情请看 [【kafka实战】一不小心删除了/brokers/topics/下的某个Topic]()如果我手动在zk中添加/brokers/topics/{TopicName}节点会怎么样先说结论: 根据上面分析过的源码画出的时序图可以指定; 客户端发起创建Topic的请求,本质上是去zk里面写两个数据topic的配置信息 /config/topics/Topic名称 持久节点topic的分区信息/brokers/topics/Topic名称 持久节点所以我们绕过这一步骤直接去写入数据,可以达到一样的效果;不过我们的数据需要保证准确因为在这一步已经没有了一些基本的校验了; 假如这一步我们写入的副本Brokerid不存在会怎样,从时序图中可以看到,leaderAndIsrRequest请求; 就不会正确的发送的不存在的BrokerId上,那么那台机器就不会创建Log文件;下面不妨让我们来验证一下;创建一个节点/brokers/topics/create_topic_byhand_zk 节点数据为下面数据;{"version":2,"partitions":{"2":[3],"1":[3],"0":[3]},"adding_replicas":{},"removing_replicas":{}}这里我用的工具PRETTYZOO手动创建的,你也可以用命令行创建;创建完成之后我们再看看本地有没有生成一个Log文件可以看到我们指定的Broker,已经生成了对应的分区副本Log文件;而且zk中也写入了其他的数据在我们写入zk数据的时候,就已经确定好了哪个每个分区的Leader是谁了,那就是第一个副本默认为Leader如果写入/brokers/topics/{TopicName}节点之后Controller挂掉了会怎么样先说结论:Controller 重新选举的时候,会有一些初始化的操作; 会把创建过程继续下去然后我们来模拟这么一个过程,先停止集群,然后再zk中写入/brokers/topics/{TopicName}节点数据; 然后再启动一台Broker; 源码分析: 我们之前分析过[Controller的启动过程与选举]() 有提到过,这里再提一下Controller当选之后有一个地方处理这个事情replicaStateMachine.startup() partitionStateMachine.startup()启动状态机的过程是不是跟上面的6.1 onNewPartitionCreation 状态流转 的过程很像; 最终都把状态流转到了OnlinePartition; 伴随着是不发起了leaderAndIsrRequest请求; 是不是Broker收到请求之后,创建本地Log文件了附件--config 可生效参数请以sh bin/kafka-topic -help 为准configurations: cleanup.policy compression.type delete.retention.ms file.delete.delay.ms flush.messages flush.ms follower.replication.throttled. replicas index.interval.bytes leader.replication.throttled.replicas max.compaction.lag.ms max.message.bytes message.downconversion.enable message.format.version message.timestamp.difference.max.ms message.timestamp.type min.cleanable.dirty.ratio min.compaction.lag.ms min.insync.replicas preallocate retention.bytes retention.ms segment.bytes segment.index.bytes segment.jitter.ms segment.ms unclean.leader.election.enableTips:如果关于本篇文章你有疑问,可以在评论区留下,我会在Q&A部分进行解答 PS: 文章阅读的源码版本是kafka-2.5 滴滴开源Logi-KafkaManager 一站式Kafka监控与管控平台关注公众号 获取kafka专栏全套文章
视频:【kafka运维】数据迁移、分区副本重分配、跨路径迁移、副本扩缩容 视频 日常运维、问题排查=> 滴滴开源LogiKM一站式Kafka监控与管控平台【kafka运维】数据迁移、分区副本重分配、跨路径迁移、副本扩缩容如果你不想看文章,可以直接看配套的视频; (后续的视频会在 公众号、CSDN、B站等各平台同名号[石臻臻的杂货铺]上上传 )本期视频内容:分区副本重分配教程重分配的注意事项LogiKm简化重分配流程,更高效跨副本迁移脚本参数参数描述例子--zookeeper连接zk--zookeeper localhost:2181, localhost:2182--topics-to-move-json-file指定json文件,文件内容为topic配置--topics-to-move-json-file config/move-json-file.json Json文件格式如下: --generate尝试给出副本重分配的策略,该命令并不实际执行--broker-list指定具体的BrokerList,用于尝试给出分配策略,与--generate搭配使用--broker-list 0,1,2,3--reassignment-json-file指定要重分配的json文件,与--execute搭配使用json文件格式如下例如:--execute开始执行重分配任务,与--reassignment-json-file搭配使用--verify验证任务是否执行成功,当有使用--throttle限流的话,该命令还会移除限流;该命令很重要,不移除限流对正常的副本之间同步会有影响--throttle迁移过程Broker之间现在流程传输的速率,单位 bytes/sec-- throttle 500000--replica-alter-log-dirs-throttlebroker内部副本跨路径迁移数据流量限制功能,限制数据拷贝从一个目录到另外一个目录带宽上限 单位 bytes/sec--replica-alter-log-dirs-throttle 100000--disable-rack-aware关闭机架感知能力,在分配的时候就不参考机架的信息--bootstrap-server如果是副本跨路径迁移必须有此参数1. 脚本的使用介绍该脚本是kafka提供用来重新分配分区的脚本工具;1.1 生成推荐配置脚本关键参数--generate在进行分区副本重分配之前,最好是用下面方式获取一个合理的分配文件; 编写move-json-file.json文件; 这个文件就是告知想对哪些Topic进行重新分配的计算{ "topics": [ {"topic": "test_create_topic1"} ], "version": 1 }然后执行下面的脚本,--broker-list "0,1,2,3" 这个参数是你想要分配的Brokers;sh bin/kafka-reassign-partitions.sh --zookeeper xxx:2181 --topics-to-move-json-file config/move-json-file.json --broker-list "0,1,2,3" --generate执行完毕之后会打印Current partition replica assignment//当前副本分配方式 {"version":1,"partitions":[{"topic":"test_create_topic1","partition":2,"replicas":[1],"log_dirs":["any"]},{"topic":"test_create_topic1","partition":1,"replicas":[3],"log_dirs":["any"]},{"topic":"test_create_topic1","partition":0,"replicas":[2],"log_dirs":["any"]}]} Proposed partition reassignment configuration//期望的重新分配方式 {"version":1,"partitions":[{"topic":"test_create_topic1","partition":2,"replicas":[2],"log_dirs":["any"]},{"topic":"test_create_topic1","partition":1,"replicas":[1],"log_dirs":["any"]},{"topic":"test_create_topic1","partition":0,"replicas":[0],"log_dirs":["any"]}]}需求注意的是,此时分区移动尚未开始,它只是告诉你当前的分配和建议。保存当前分配,以防你想要回滚它1.2. 执行Json文件关键参数--execute将上面得到期望的重新分配方式文件保存在一个json文件里面reassignment-json-file.json{"version":1,"partitions":[{"topic":"test_create_topic1","partition":2,"replicas":[2],"log_dirs":["any"]},{"topic":"test_create_topic1","partition":1,"replicas":[1],"log_dirs":["any"]},{"topic":"test_create_topic1","partition":0,"replicas":[0],"log_dirs":["any"]}]}然后执行sh bin/kafka-reassign-partitions.sh --zookeeper xxxxx:2181 --reassignment-json-file config/reassignment-json-file.json --execute迁移过程注意流量陡增对集群的影响Kafka提供一个broker之间复制传输的流量限制,限制了副本从机器到另一台机器的带宽上限,当重新平衡集群,引导新broker,添加或移除broker时候,这是很有用的。因为它限制了这些密集型的数据操作从而保障了对用户的影响、 例如我们上面的迁移操作加一个限流选项-- throttle 50000000> sh bin/kafka-reassign-partitions.sh --zookeeper xxxxx:2181 --reassignment-json-file config/reassignment-json-file.json --execute -- throttle 50000000在后面加上一个—throttle 50000000 参数, 那么执行移动分区的时候,会被限制流量在50000000 B/s加上参数后你可以看到The throttle limit was set to 50000000 B/s Successfully started reassignment of partitions.需要注意的是,如果你迁移的时候包含 副本跨路径迁移(同一个Broker多个路径)那么这个限流措施不会生效,你需要再加上|--replica-alter-log-dirs-throttle 这个限流参数,它限制的是同一个Broker不同路径直接迁移的限流;如果你想在重新平衡期间修改限制,增加吞吐量,以便完成的更快。你可以重新运行execute命令,用相同的reassignment-json-file1.3. 验证关键参数--verify该选项用于检查分区重新分配的状态,同时—throttle流量限制也会被移除掉; 否则可能会导致定期复制操作的流量也受到限制。sh bin/kafka-reassign-partitions.sh --zookeeper xxxx:2181 --reassignment-json-file config/reassignment-json-file.json --verify注意: 当你输入的BrokerId不存在时,该副本的操作会失败,但是不会影响其他的;例如2. 副本扩缩kafka并没有提供一个专门的脚本来支持副本的扩缩, 不像kafka-topic.sh脚本一样,是可以扩分区的; 想要对副本进行扩缩,只能是曲线救国了; 利用kafka-reassign-partitions.sh来重新分配副本2.1 副本扩容假设我们当前的情况是 3分区1副本,为了提供可用性,我想把副本数升到2;2.1.1 计算副本分配方式我们用步骤1.1的 --generate 获取一下当前的分配情况,得到如下json{ "version": 1, "partitions": [{ "topic": "test_create_topic1", "partition": 2, "replicas": [2], "log_dirs": ["any"] }, { "topic": "test_create_topic1", "partition": 1, "replicas": [1], "log_dirs": ["any"] }, { "topic": "test_create_topic1", "partition": 0, "replicas": [0], "log_dirs": ["any"] }] }我们想把所有分区的副本都变成2,那我们只需修改"replicas": []里面的值了,这里面是Broker列表,排在第一个的是Leader; 所以我们根据自己想要的分配规则修改一下json文件就变成如下{ "version": 1, "partitions": [{ "topic": "test_create_topic1", "partition": 2, "replicas": [2,0], "log_dirs": ["any","any"] }, { "topic": "test_create_topic1", "partition": 1, "replicas": [1,2], "log_dirs": ["any","any"] }, { "topic": "test_create_topic1", "partition": 0, "replicas": [0,1], "log_dirs": ["any","any"] }] }注意log_dirs里面的数量要和replicas数量匹配;或者直接把log_dirs选项删除掉; 这个log_dirs是副本跨路径迁移时候的绝对路径2.1.2 执行--execute如果你想在重新平衡期间修改限制,增加吞吐量,以便完成的更快。你可以重新运行execute命令,用相同的reassignment-json-file:2.1.2 验证--verify完事之后,副本数量就增加了; 2.2 副本缩容副本缩容跟扩容是一个意思; 当副本分配少于之前的数量时候,多出来的副本会被删除; 比如刚刚我新增了一个副本,想重新恢复到一个副本执行下面的json文件{ "version": 1, "partitions": [{ "topic": "test_create_topic1", "partition": 2, "replicas": [2], "log_dirs": ["any"] }, { "topic": "test_create_topic1", "partition": 1, "replicas": [1], "log_dirs": ["any"] }, { "topic": "test_create_topic1", "partition": 0, "replicas": [0], "log_dirs": ["any"] }] }执行之后可以看到其他的副本就被标记为删除了; 一会就会被清理掉用这样一种方式我们虽然是实现了副本的扩缩容, 但是副本的分配需要我们自己来把控好, 要做到负载均衡等等; 那肯定是没有kafka自动帮我们分配比较合理一点; 那么我们有什么好的方法来帮我们给出一个合理分配的Json文件吗?PS:我们之前已经分析过【kafka源码】创建Topic的时候是如何分区和副本的分配规则 那么我们把这样一个分配过程也用同样的规则来分配不就Ok了吗?--generate本质上也是调用了这个方法,AdminUtils.assignReplicasToBrokers(brokerMetadatas, assignment.size, replicas.size)具体的实现操作请看 【kafka思考】最小成本的扩缩容副本设计方案自己写一个工程来实现类似的方法,如果觉得很麻烦,可以直接使用LogIKM 的新增副本功能直接帮你做了这个事情;(未来会实现)3. 分区扩容kafka的分区扩容是 kafka-topis.sh脚本实现的;不支持缩容 分区扩容请看 【kafka源码】TopicCommand之alter源码解析(分区扩容)4. 分区迁移分区迁移跟上面同理, 请看 1.1,1.2,1.3 部分;5. 副本跨路径迁移为什么线上Kafka机器各个磁盘间的占用不均匀,经常出现“一边倒”的情形? 这是因为Kafka只保证分区数量在各个磁盘上均匀分布,但它无法知晓每个分区实际占用空间,故很有可能出现某些分区消息数量巨大导致占用大量磁盘空间的情况。在1.1版本之前,用户对此毫无办法,因为1.1之前Kafka只支持分区数据在不同broker间的重分配,而无法做到在同一个broker下的不同磁盘间做重分配。1.1版本正式支持副本在不同路径间的迁移怎么在一台Broker上用多个路径存放分区呢?只需要在配置上接多个文件夹就行了############################# Log Basics ############################# # A comma separated list of directories under which to store log files log.dirs=kafka-logs-5,kafka-logs-6,kafka-logs-7,kafka-logs-8注意同一个Broker上不同路径只会存放不同的分区,而不会将副本存放在同一个Broker; 不然那副本就没有意义了(容灾)怎么针对跨路径迁移呢?迁移的json文件有一个参数是log_dirs; 默认请求不传的话 它是"log_dirs": ["any"] (这个数组的数量要跟副本保持一致) 但是你想实现跨路径迁移,只需要在这里填入绝对路径就行了,例如下面迁移的json文件示例{ "version": 1, "partitions": [{ "topic": "test_create_topic4", "partition": 2, "replicas": [0], "log_dirs": ["/Users/xxxxx/work/IdeaPj/source/kafka/kafka-logs-5"] }, { "topic": "test_create_topic4", "partition": 1, "replicas": [0], "log_dirs": ["/Users/xxxxx/work/IdeaPj/source/kafka/kafka-logs-6"] }] }然后执行脚本sh bin/kafka-reassign-partitions.sh --zookeeper xxxxx --reassignment-json-file config/reassignment-json-file.json --execute --bootstrap-server xxxxx:9092 --replica-alter-log-dirs-throttle 10000注意 --bootstrap-server 在跨路径迁移的情况下,必须传入此参数如果需要限流的话 加上参数|--replica-alter-log-dirs-throttle ; 跟--throttle不一样的是 --replica-alter-log-dirs-throttle限制的是Broker内不同路径的迁移流量;源码解析源码解析请看文章【kafka源码】ReassignPartitionsCommand源码分析(副本扩缩、数据迁移、分区重分配、副本跨路径迁移)欢迎Star和共建由滴滴开源的kafka的管理平台,非常优秀非常好用的一款kafka管理平台满足所有开发运维日常需求 滴滴开源Logi-KafkaManager 一站式Kafka监控与管控平台
ConfigCommandConfig相关操作; 动态配置可以覆盖默认的静态配置;1.查询配置Topic配置查询展示关于Topic的动静态配置1.查询单个Topic配置(只列举动态配置)sh bin/kafka-configs.sh --describe --bootstrap-server xxxxx:9092 --topic test_create_topic或者sh bin/kafka-configs.sh --describe --bootstrap-server 172.23.248.85:9092 --entity-type topics --entity-name test_create_topic2.查询所有Topic配置(包括内部Topic)(只列举动态配置)sh bin/kafka-configs.sh --describe --bootstrap-server 172.23.248.85:9092 --entity-type topics 3.查询Topic的详细配置(动态+静态)只需要加上一个参数--all其他配置/clients/users/brokers/broker-loggers 的查询同理 ;只需要将--entity-type 改成对应的类型就行了 (topics/clients/users/brokers/broker-loggers)查询kafka版本信息sh bin/kafka-configs.sh --describe --bootstrap-server xxxx:9092 --version所有可配置的动态配置 请看最后面的 附件 部分2 增删改 配置 --alter--alter 删除配置: --delete-config k1=v1,k2=v2添加/修改配置: --add-config k1,k2选择类型: --entity-type (topics/clients/users/brokers/broker- loggers)类型名称: --entity-nameTopic添加/修改动态配置--add-configsh bin/kafka-configs.sh --bootstrap-server xxxxx:9092 --alter --entity-type topics --entity-name test_create_topic1 --add-config file.delete.delay.ms=222222,retention.ms=999999 Topic删除动态配置--delete-configsh bin/kafka-configs.sh --bootstrap-server xxxxx:9092 --alter --entity-type topics --entity-name test_create_topic1 --delete-config file.delete.delay.ms,retention.ms 其他配置同理,只需要类型改下--entity-type类型有: (topics/clients/users/brokers/broker- loggers)哪些配置可以修改 请看最后面的附件:ConfigCommand 的一些可选配置 附件ConfigCommand 的一些可选配置Topic相关可选配置keyvalue示例cleanup.policy清理策略 compression.type压缩类型(通常建议在produce端控制) delete.retention.ms压缩日志的保留时间 file.delete.delay.ms flush.messages持久化message限制 flush.ms持久化频率 follower.replication.throttled.replicasflowwer副本限流 格式:分区号:副本follower号,分区号:副本follower号0:1,1:1index.interval.bytes leader.replication.throttled.replicasleader副本限流 格式:分区号:副本Leader号0:0max.compaction.lag.ms max.message.bytes最大的batch的message大小message.downconversion.enablemessage是否向下兼容 message.format.versionmessage格式版本message.timestamp.difference.max.ms message.timestamp.type min.cleanable.dirty.ratio min.compaction.lag.ms min.insync.replicas最小的ISR preallocate retention.bytes日志保留大小(通常按照时间限制) retention.ms日志保留时间 segment.bytessegment的大小限制 segment.index.bytes segment.jitter.ms segment.mssegment的切割时间 unclean.leader.election.enable是否允许非同步副本选主 Broker相关可选配置keyvalue示例advertised.listeners background.threads compression.type follower.replication.throttled.rate leader.replication.throttled.rate listener.security.protocol.map listeners log.cleaner.backoff.ms log.cleaner.dedupe.buffer.size log.cleaner.delete.retention.ms log.cleaner.io.buffer.load.factor log.cleaner.io.buffer.size log.cleaner.io.max.bytes.per.second log.cleaner.max.compaction.lag.ms log.cleaner.min.cleanable.ratio log.cleaner.min.compaction.lag.ms log.cleaner.threads log.cleanup.policy log.flush.interval.messages log.flush.interval.ms log.index.interval.bytes log.index.size.max.bytes log.message.downconversion.enable log.message.timestamp.difference.max.ms log.message.timestamp.type log.preallocate log.retention.bytes log.retention.ms log.roll.jitter.ms log.roll.ms log.segment.bytes log.segment.delete.delay.ms max.connections max.connections.per.ip max.connections.per.ip.overrides message.max.bytes metric.reporters min.insync.replicas num.io.threads num.network.threads num.recovery.threads.per.data.dir num.replica.fetchers principal.builder.class replica.alter.log.dirs.io.max.bytes.per.second sasl.enabled.mechanisms sasl.jaas.config sasl.kerberos.kinit.cmd sasl.kerberos.min.time.before.relogin sasl.kerberos.principal.to.local.rules sasl.kerberos.service.name sasl.kerberos.ticket.renew.jitter sasl.kerberos.ticket.renew.window.factor sasl.login.refresh.buffer.seconds sasl.login.refresh.min.period.seconds sasl.login.refresh.window.factor sasl.login.refresh.window.jitter sasl.mechanism.inter.broker.protocol ssl.cipher.suites ssl.client.auth ssl.enabled.protocols ssl.endpoint.identification.algorithm ssl.key.password ssl.keymanager.algorithm ssl.keystore.location ssl.keystore.password ssl.keystore.type ssl.protocol ssl.provider ssl.secure.random.implementation ssl.trustmanager.algorithm ssl.truststore.location ssl.truststore.password ssl.truststore.type unclean.leader.election.enable Users相关可选配置keyvalue示例SCRAM-SHA-256 SCRAM-SHA-512 consumer_byte_rate针对消费者user进行限流 producer_byte_rate针对生产者进行限流 request_percentage请求百分比 clients相关可选配置keyvalue示例consumer_byte_rate producer_byte_rate request_percentage 关于作者:石臻臻的杂货铺, 专注于 Java领域、大数据领域 等知识分享, 内容多为 原理 、源码、实战 等等, 坚持输出干货,所写内容必定经过验证,并深入源码分析,保证内容准确性, 长期在CSDN、和公众号【石臻臻的杂货铺】发布原创文章,欢迎关注! 如果有相关技术领域问题,欢迎进群交流,各个领域都有专人解答,你所问的,都会得到回应! 欢迎Star和共建由滴滴开源的kafka的管理平台 满足所有开发运维日常需求滴滴开源Logi-KafkaManager 一站式Kafka监控与管控平台
日常运维问题排查 怎么能够少了滴滴开源的 滴滴开源LogiKM一站式Kafka监控与管控平台TopicCommand1.Topic创建bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 3 --partitions 3 --topic test相关可选参数参数描述例子--bootstrap-server 指定kafka服务指定连接到的kafka服务; 如果有这个参数,则 --zookeeper可以不需要--bootstrap-server localhost:9092--zookeeper弃用, 通过zk的连接方式连接到kafka集群;--zookeeper localhost:2181 或者localhost:2181/kafka--replication-factor 副本数量,注意不能大于broker数量;如果不提供,则会用集群中默认配置--replication-factor 3--partitions分区数量,当创建或者修改topic的时候,用这个来指定分区数;如果创建的时候没有提供参数,则用集群中默认值; 注意如果是修改的时候,分区比之前小会有问题--partitions 3--replica-assignment 副本分区分配方式;创建topic的时候可以自己指定副本分配情况;--replica-assignment BrokerId-0:BrokerId-1:BrokerId-2,BrokerId-1:BrokerId-2:BrokerId-0,BrokerId-2:BrokerId-1:BrokerId-0 ; 这个意思是有三个分区和三个副本,对应分配的Broker; 逗号隔开标识分区;冒号隔开表示副本--config <String: name=value>用来设置topic级别的配置以覆盖默认配置;只在--create 和--bootstrap-server 同时使用时候生效; 可以配置的参数列表请看文末附件例如覆盖两个配置 --config retention.bytes=123455 --config retention.ms=600001--command-config <String: command 文件路径>用来配置客户端Admin Client启动配置,只在--bootstrap-server 同时使用时候生效;例如:设置请求的超时时间 --command-config config/producer.proterties ; 然后在文件中配置 request.timeout.ms=3000002.删除Topicbin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic test支持正则表达式匹配Topic来进行删除,只需要将topic 用双引号包裹起来例如: 删除以create_topic_byhand_zk为开头的topic;bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic "create_topic_byhand_zk.*".表示任意匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用 . 。·*·:匹配前面的子表达式零次或多次。要匹配 * 字符,请使用 *。.* : 任意字符删除任意Topic (慎用)bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic ".*?" 更多的用法请参考正则表达式3.Topic分区扩容zk方式(不推荐)>bin/kafka-topics.sh --zookeeper localhost:2181 --alter --topic topic1 --partitions 2kafka版本 >= 2.2 支持下面方式(推荐)单个Topic扩容bin/kafka-topics.sh --bootstrap-server broker_host:port --alter --topic test_create_topic1 --partitions 4批量扩容 (将所有正则表达式匹配到的Topic分区扩容到4个)sh bin/kafka-topics.sh --topic ".*?" --bootstrap-server 172.23.248.85:9092 --alter --partitions 4".*?" 正则表达式的意思是匹配所有; 您可按需匹配PS: 当某个Topic的分区少于指定的分区数时候,他会抛出异常;但是不会影响其他Topic正常进行;相关可选参数参数描述例子--replica-assignment 副本分区分配方式;创建topic的时候可以自己指定副本分配情况;--replica-assignment BrokerId-0:BrokerId-1:BrokerId-2,BrokerId-1:BrokerId-2:BrokerId-0,BrokerId-2:BrokerId-1:BrokerId-0 ; 这个意思是有三个分区和三个副本,对应分配的Broker; 逗号隔开标识分区;冒号隔开表示副本PS: 虽然这里配置的是全部的分区副本分配配置,但是正在生效的是新增的分区;比如: 以前3分区1副本是这样的Broker-1Broker-2Broker-3Broker-4012 现在新增一个分区,--replica-assignment 2,1,3,4 ; 看这个意思好像是把0,1号分区互相换个BrokerBroker-1Broker-2Broker-3Broker-41023 但是实际上不会这样做,Controller在处理的时候会把前面3个截掉; 只取新增的分区分配方式,原来的还是不会变Broker-1Broker-2Broker-3Broker-40123 4.查询Topic描述1.查询单个Topicsh bin/kafka-topics.sh --topic test --bootstrap-server xxxx:9092 --describe --exclude-internal2.批量查询Topic(正则表达式匹配,下面是查询所有Topic)sh bin/kafka-topics.sh --topic ".*?" --bootstrap-server xxxx:9092 --describe --exclude-internal支持正则表达式匹配Topic,只需要将topic 用双引号包裹起来相关可选参数参数描述例子--bootstrap-server 指定kafka服务指定连接到的kafka服务; 如果有这个参数,则 --zookeeper可以不需要--bootstrap-server localhost:9092--at-min-isr-partitions查询的时候省略一些计数和配置信息--at-min-isr-partitions --exclude-internal排除kafka内部topic,比如__consumer_offsets-*--exclude-internal--topics-with-overrides仅显示已覆盖配置的主题,也就是单独针对Topic设置的配置覆盖默认配置;不展示分区信息--topics-with-overrides5.查询Topic列表1.查询所有Topic列表 sh bin/kafka-topics.sh --bootstrap-server xxxxxx:9092 --list --exclude-internal 2.查询匹配Topic列表(正则表达式)查询test_create_开头的所有Topic列表 sh bin/kafka-topics.sh --bootstrap-server xxxxxx:9092 --list --exclude-internal --topic "test_create_.*"相关可选参数参数描述例子--exclude-internal排除kafka内部topic,比如__consumer_offsets-*--exclude-internal--topic可以正则表达式进行匹配,展示topic名称--topic关于作者:石臻臻的杂货铺, 专注于 Java领域、大数据领域 等知识分享, 内容多为 原理 、源码、实战 等等, 坚持输出干货,所写内容必定经过验证,并深入源码分析,保证内容准确性, 长期在CSDN、和公众号【石臻臻的杂货铺】发布原创文章,欢迎关注! 如果有相关技术领域问题,欢迎进群交流,各个领域都有专人解答,你所问的,都会得到回应! 欢迎Star和共建由滴滴开源的kafka的管理平台 满足所有开发运维日常需求滴滴开源Logi-KafkaManager 一站式Kafka监控与管控平台
2022年08月
2022年07月
2022年06月
2022年05月
2022年04月
2022年03月
2021年12月