Quantcast
Channel: IT社区推荐资讯 - ITIndex.net
Viewing all 11847 articles
Browse latest View live

十万腾讯人,自救1000天

$
0
0

 
作者 |  蓝字原创首发 | 蓝字计划

“在腾讯就没有生活,真的是理应如此?”


5月29日凌晨1点39分,一条发布在腾讯内部论坛KM的帖子,犹如沸水泼油,炸出了三万多只鹅。


这位匿名提问者控诉道:在IEG旗下光子工作室(“和平精英”是其代表作)的家属,加班已经常态化。


“这种生存状态真的正常吗?”一位愤怒的妻子,点名责问相关部门:对加班严重的部门能否进行警告甚至强制措施?


这条发布在周六凌晨的帖子,迅速得到了五十多条跟帖,有人劝楼主用脚投票,也有人调侃“都已经这么拼,为什么做不出《原神》”。


最多的人点赞的一条回帖痛陈,个人的苦难不能全部归因于自由的选择。


换言之,在泥沙俱下的大环境,用脚投票的可行性并不高,对比外部,腾讯尚不算是最“卷”的级别。


一周过后,光子工作室发布新规:


1.周三健康日全部门下午六点下班;


2.其余工作日必须晚上九点前下班,特殊加班人数封顶10%,违规团队下周集体六点下班;


3.全面双休,特殊加班人数封顶10%,违规团队未来一个月不得加班;


4.禁止周末连续两天加班。


如此明确、严苛、不留余地的规定,在腾讯公司史上,实属罕见。


列宁说过:“堡垒往往最先从内部攻破”,这并不是KM第一次推动腾讯做出这样的决定了。


马化腾也说过: “很多病都是自救,没有人能帮你。”


2021年4月,马化腾出现在腾讯集团内部一年一度的战略见面会上,那天他和腾讯员工谈了很多内容,包括回应了对他腰部旧伤的担忧,他认为无论是自己,还是腾讯,只有自己可以拯救自己。



“腾讯病历”


时间轴拧回2018年8月12日,至今已1000天。一篇题为《五问!腾讯哪些大公司病让你不吐不快》的檄文,出现在腾讯公司KM上。

 

KM,是腾讯公司内部论坛的产品,像是企业版“知乎”,员工们可以匿名提问或畅言。在内部说法里, 就连马化腾都不能看到匿名员工的真实身份。

 

一位自称老鹅(腾讯人自称为鹅)的匿名员工,以KM发帖的形式公开上书——“我觉得腾讯病了,而且很重,到需要动大手术的地步。”

 

这位匿名老鹅列举了几年工作中观察总结的腾讯“大公司病”,简而言之:


1、我们对汇报、PPT、评奖和分享的重视,甚至超过了工作本身;


2、微信群越拉越多,群里真正解决问题的人越来越少;


3、专家越来越多,高质量创新反而减少,职称沦为养老院;


4、战略缺“大将”和“军长”,中高层权力板结;


5、规则屡屡突破,价值观摇摆,公司早期优秀文化面临稀释。


以上这条被称为“病历”的帖子中,发帖员工痛陈在过去几年,内部备受诟病的 “赛马机制”、“组织臃肿”、“权力板结”和“形式主义”。

 

比如描述公司内部“形式主义”背后,对应“叠罗汉”的怪现象:副组长向组长,副总监向总监,助理总经理向副总经理汇报,本来公司七个层级已经够多,现在官僚系统还在大膨胀七层都不够用了,“只能用起隔板,充分利用夹层隔板看似很薄,可坚不可摧”。

 

帖子结尾,匿名老鹅甚至公开向管理层喊话:中高层愿不愿自我解剖?有没有魄力自我手术?敢不敢壮士断腕?能否接受能上能下的流动?愿不愿意放下山头齐心协力补上船底的漏洞?

 

这条帖子阅读量了停留在64554次,当年腾讯财报数据显示共有4万6千名在册员工,意味着每个鹅厂人都读过这篇“檄文”帖子,还有人在反复不断浏览阅读了帖子。KM论坛里人潮涌动,“五问”腾讯的大讨论还在持续。

 

这条激烈的“檄文”帖子下方,还有不少员工实名邀请同事参与讨论回答,被邀请的同事有Ponyma、Mark和Xidan等, 他们分别是马化腾、任宇昕和奚丹。



大讨论足足持续了一个月


翌日,腾讯集团总办领导、集团高级副总裁奚丹回复了“五问”腾讯这张热帖,他认为不少观点也很到位。

 

“我代表总办给你点个赞!”

 

奚丹在回复中,承认大企业病已经成为腾讯最大的挑战之一,现在确实也到了要更加正视,并且拿出决心和行动的时候了。

 

总办领导一句表态激起万重浪。有企业文化部门员工干脆实名提问,一条《腾讯大企业病“五问”火了,谈谈你最想公司改变的一个痛点?》帖子成为了自我批评的新阵地。

 

谈痛点中,最高赞的质询来自于一位技术副总监。


他痛陈鹅厂没有CTO,自从2014年腾讯创始人之一张志东卸任后,腾讯CTO的位置空置至今。


长期以来,腾讯奉之金科玉律的“赛马机制”孵化出竞争成果的同时,也衍生出“重复造轮子”的诟病。


腾讯前CTO张志东
 

腾讯人也不是非要一个CTO,就连呼声最高的创始人之一、前CTO张志东在也公开回应称,自己不会重回公司业务一线,相信腾讯会孵化出新生力量。

 

症结背后是公司发展到了一个巨大体量,公司部门墙成了阻止公司前进的巨大阻力,万众期待CTO能够打破这种困局——“技术缺乏长期规划布局,我们技术配得起腾讯吗”、“对基础研究领域投入严重不足,追求中短期利息”、“找个CTO,让技术成为社会进步的阶梯”。

 

实际上腾讯没有CTO已经很多年,从来没有像今天这样备受诟病。有内部员工坦陈: 所谓没有CTO的吐槽,背后是腾讯人对改变现状的渴望。

 

大讨论足足持续了一个月。

 

一个月之前,“五问”腾讯缘起,不断挑战着几万腾讯人的神经,腾讯人试图在KM上问个清楚,究竟我们哪里做错了?


凭借着匿名的承诺,腾讯员工甚至抛出了《十省腾讯,公司到底怎么了,到底该关注什么问题》,直斥腾讯方向不明、管理地盘化、唯老板需求、干部甄选黑盒化......

 

在老腾讯人印象中,一向“佛系”的腾讯员工很少会情绪激烈,这次讨论的各种语气——太不客气!


不过,总办领导再也没有出面回应。



凌晨6点14分“对自己动刀”


事实上,在这场大讨论之前,腾讯总办早已预料到这种局面。 据一位腾讯不具名的亲历受访者描述,他在大讨论期间,收到公司内部一个特殊调研团队邀请,参加了一场腾讯集团滨海总部35楼的小型闭门讨论,窗帘和门紧密,参与人员囊括了腾讯老员工、腾讯新员工,甚至有从其他同行处跳槽过来的资深“新鹅”。
“你可以谈任何关于腾讯的看法,尤其欢迎批评,你的领导和同事不会知道”,调研团队保证。
这位受访者回忆,现场每个人针对自己和公司的“批评”近乎开骂,议题集中在KM大讨论议题的具像化——大家都举了自己的例子,试图证明这些“批评”与“自我批评”真实存在。 “批评意见”会被如实记录,直呈总办。 实际上早在2017年年底,腾讯核心管理团队便着手调研,试图“诊断”自身,并且意图进行腾讯公司史上的第三次组织架构变革。
“对自己动刀子”。 KM大讨论一个月后,2018年9月30日,马化腾在凌晨6点14分内部发出全员公开信,正式宣布组织架构升级,史称“930变革”。
之所以踩点凌晨6点14分,因为那是当天深圳气象台公布的日出时刻,意味深长。  930后腾讯的业务架构

组织架构升级与变革带来的变动,甚至“硝烟”,但是“930变革真的”凑效了吗?


腾讯KM上有迹可循,鹅厂人在结束了“五问”、“十问”甚至“天问”腾讯之后,提问陆续投射了新的变化,时间进入2019年,关于35岁焦虑、人到中年的讨论逐渐浮现。

 

《把老员工等同于没有创造力的贬值人,是否是一种舆论陷阱》、《35岁面临职场危机,应该怎么度过》、《鹅厂针对年轻员工发起的英才计划实施以来,进展如何》......

 

在组织架构变革官宣后的一年多时间里,腾讯KM上关于年龄焦虑和个人出路的讨论突然暴增。有一位匿名同事实在忍不住直问: “看了公司各种35岁的帖子,是不是之前所谓的年轻化,反映了公司产业升级失败的一个缩影?”

 

腾讯第三次组织架构升级协调整合全新事业群、全面拉开产业互联网战略背后,所带来的在人事组织上的“升级”,就是强化“干部能上能下”。

 

内部关于35岁焦虑、中年危机的帖子,某种程度上来源于此,属于组织架构变革的投射。

 

当有权者被打破了“铁饭碗”


“930变革”近一年后,腾讯内部正式发布《腾讯管理干部能上能下管理办法》,这意味着之前在KM大讨论中备受批评的“权力板结严重”,深深刺痛着腾讯上下几万人。

 

“干部能上能下”并非腾讯独有,亦并非新提法。事实上,我们从较权威信源得知,腾讯公司从2012年8月便正式推动了这一政策。


930战略升级和组织变革成为这一政策原则的“催化剂”。


自2018年9月30日开始,腾讯花一年时间,完成了10%的中高级管理骨干的“能下”,通过制度设计,这个“能下”比例每年被划定为5%,意思就是每年强制淘汰5%的管理层。

 

蓝字计划(微信公众号:NPO2020)观察到,腾讯知名员工网友Tegic(微信公众号:tegic0700)便公开描述了自己身边好几个朋友面临“被降职”。


Tegic在聊天记录中安慰“被能下”的中高干称: “把你们这些占着茅坑不拉屎的老家伙干掉,感觉我司又有希望了”。


有一组数据可以提供支撑:腾讯公司自930战略升级和组织变革后,推动青年英才晋升绿色通道,新晋升了35岁以下的年轻中干若干人,占当年所有新晋升中干的比例超过40%;新晋升30岁以下年轻总监首现;新晋升的组长中28岁以下年轻组长总量增加近2倍。

 

“能下”之后,出现“能上”,这也许才是腾讯人开始内部讨论“老龄化”的真正原因。


与其称KM上腾讯员工对“中年危机”的讨论是“年轻化焦虑”,不如说这是腾讯诊断“组织初老症”和“大企业病”的一道猛药。

 

员工们在内部KM的吐槽焦点,随着腾讯自身的改变而发生这潜移默化的变化,很少人会穿透一张张内部帖子, 发现鹅厂正在起变化。


腾讯活水计划介绍漫画《小T转岗记》

腾讯的To B战略进度也以同样的姿势,开始深入每一个鹅厂人的思维。

 

随着“腾讯究竟有没有To B的基因”讨论开始,腾讯人开始越来越多地思考,究竟能不能从原来的消费互联网业务领域的成功,同样实现在产业互联网。

 

KM的帖子充满了腾讯人的To B焦虑,其中一张热帖回收了一大堆“930变革”后,一线业务的真实故事,读起来“触目惊心”。

 

比如之前从来不用驻场陪同客户的程序员,因为开始服务甲方,竟然需要驻扎在客户公司度过618、双11甚至春节这种节日,“最后还被客户陪着过了自己的生日”;


有的售后员工因为经常帮客户解决问题,猝不及防收到了不得拒绝的答谢红包,拿着钱不知所措;又比如不少曾经在鹅厂内部备受宠爱的产品团队,开始产生被要求给客户道歉的恐惧所支配,惶惶不可终日......


这些都是产品为王、生于消费互联网的腾讯人转战产业互联网的阵痛。

 


一个关于英文名的2B大问题


出人意料的是,关于鹅厂人叫什么花名,在KM上就上升为一个事关企业发展的战略议题。 一直以来,每个入职腾讯公司的员工,都会起一个属于自己的英文名,即使连马化腾(Pony)、张志东(Tony)、陈一丹(Charls)这些公司创始人也不例外。
这种英文名文化,和阿里巴巴每个人都起武侠花名类似。外界解读认为,这种起名文化符合互联网公司的企业性格——平等民主处事、减少层级隔阂、便于跨部门合作。
随着战略升级和组织变革的推进,是否直呼英文名的企业文化备受挑战,工作中越来越多出现称呼“X总”和“老板”, 被一些员工认为是对腾讯价值观的变相稀释。 KM上开始陆续出现质疑:“关于同事间的称呼,X姐、X哥在渐成主流吗?”、“公司上下级称呼X老板和X总越来越多,这和五六年前以英文名互称变化挺大的!”、“拒绝惯性叫‘总’,人人都敢PK!”、“腾讯越来越社会了吗?上下都是叫老板和总了?”...... 一种全新的声音出现了。 “言必称总和老板,是我们2B业务的需要和顺应潮流的体现”,不少腾讯员工表示某种程度能能够理解称呼习惯的变化,有一线的腾讯云销售就解释称,他们去跑政府客户、国企客户的时候,对方特别在乎你的称谓是什么:——是X总?还是小X?
这是个问题,还是个“2B(To B)”问题。
类似转战产业互联网后,业务领域变化带来的冲突故事,被越来越多地出现在内部KM上。某个大客户的云服务出现问题,三家云服务巨头同时维修,风格各异:A家嘘寒问暖服务到位、H家人海战术激情在线,而腾讯云团队只派了几个工程师到现场,尽管后端整个团队密锣紧鼓解决问题。

中国最大的煤矿企业,使用企业微信进行管理

 尽管腾讯云最终能够为客户解决问题,但是却得不到客户的理解和认同,因为只派几个人到现场,场面看起来太不重视了! 腾讯团队觉得很委屈,心想我们目标是为客户解决问题,没必要的排面只是形式主义。
但这就是“2B(To B)”的现状,如何全方位地服务客户,包括照顾客户的心态,也许都属于产业客户服务的关键,这与腾讯人之前埋头苦干,产品为王的经验大相径庭。 这些故事和冲突背后,都凝聚总结了腾讯在“930变革”之后的阵痛和改变,化作一张张的帖子,浮现在内部KM上。
表面上弥漫这吐槽、不解甚至愤怒,但实际上都沉淀成了腾讯转型阵痛期的养料—— 自我批评,自我反省,自我进化。 不过,精明的腾讯员工们最近发现了一个小秘密——和总办领导一样的英文名,又开放可以注册了,比如说你也可以把英文名起作Pony,和这家公司董事长马化腾一样。
有人发现了这个秘密,把这个好消息又发在了KM上。
如今,腾讯共有4个员工选择拥有了Pony这个英文名,除了马化腾之外,其余都是普通一线员工。

从不“少数服从多数“的内部论坛


2019年,改革过去一年了,KM讨论也进入深水区。赛马机制和部门墙,成为这个阶段的重要焦点。 在过去,腾讯的技术布局更多基于各大事业群的业务需求进行规划,这样的安排不乏合理之处: 让听得见炮火声的人来决策,更能适应快速变化的业务需求,并进一步降低跨部门的沟通损耗。 三十年河东,三十年河西。 彼时所推崇的模式,是通过大中台整合所有数据,再利用算法提取相关信息,从而对内提供数据基础建设和统一的数据服务,对外提供服务商家的数据产品。
KM上腾讯人对其他竞争者的关注,开始多于过往。 抖音在短短3年时间成长为6亿用户规模的超级App,就被归功于字节跳动强大的中台与算法。
在2018年,所有的赞誉都留给了阿里、字节引领的数据中台模式,而腾讯的散养式研发,在内部更多是收到了“重复造轮子”、“重业务轻技术”类似的抱怨。 相比起”930变革“引发外部对腾讯战略布局的大讨论,在拥有数万名程序员的内部,人们更关心的反而是新近成立的「技术委员会」能够给内部带来什么改变。 新闻一出,马上有员工率先发问:“技术委员会能否真正做到以技术人员为主?”
他的观点反映了不少程序员的心声: 技术就是技术,并不应该屈从于业务。 技术委员会可以了解各事业群的业务需求,但各事业群也需要站在公司整体的角度考虑,遵从技术委员会的决定。
按照规划,腾讯技术委员会由卢山和汤道生两名腾讯总办成员牵头,各个事业群的技术负责人悉数进入技术委员会的决策圈。技术委员会下设「开源协同」和「自研上云」项目组,推动内部代码的开源和协同,以及业务在云上全面整合。
腾讯希望通过技术委员会对赛马机制进行修正,减少无效的重复开发,整合沉淀内部的技术。 两位总办成员中,卢山执掌的TEG(技术工程事业群)是腾讯内部的技术支撑部门,堪称腾讯的“神盾局”,他多次在内部表态全力支持汤道生和CSIG(腾讯云与智慧产业事业群)。 和外界恨不得一个星期就看到腾讯新面貌不同,技术委员会的宣传力度,与人们期待的并不匹配。
据悉,决策层希望低调推动这项内功的修炼,减少对外公关,并要求各事业群技术负责人尽快摸清全公司现存的“技术债”,同时展开了建设技术图谱、专属讨论区、代码社区等基础工作,HR甚至成立了专门的工作组,为支持开源的部门和个人进行考核和晋升倾斜。 员工想象中要霹雳手段拆掉部门墙、统合所有数据的场面没有出现,以至于在变革一周年的内部讨论中,有人在KM讨论道“开源协同暗流涌动,研发效率问题重视起来了”,但也有人感觉只是“换了一下事业群的名字”、“没有感受到中台的作用”,甚至吐槽“起了无数中台”,而要求打通全腾讯的算法和数据的大有人在。 但这一次,汤道生所代表的总办在内外部表态都很坚决: 腾讯不做全公司范围的数据中台。  马化腾认为不做中台有利于用户隐私保护

这些都不难理解,基层员工站在自身工作的视角,会在KM上呼吁最大限度开源、协同、共享,最大限度提高效率;但决策层又必须有另外一重考量:这一步迈出去了,除了对业务和员工,还将对公司、社会长远有什么后果,这并非多数基层员工所能观察得到的。  “KM从来都不是少数服从多数,而是民主集中制。”有腾讯员工概括道。

科技向善成为了键盘上的焦虑


民主集中制,也体现在“科技向善”概念的首次公开——KM上大家集中一脸懵逼,啥?


“科技向善”这个观点,最先由张志东在2018年一场演讲中提出。

 

这位身家千亿却开着10万元大众宝来上班的IT男,亲手搭建了QQ的架构设计并能够沿用至今。凭借务实低调的作风,他在拥有数万名程序员的腾讯被尊为“大师兄”。

 

当年正是张志东有感于诸多大企业随着人员膨胀后导致文化稀释、沟通断层等问题,下令创办KM,并规定除非人命案件,内部任何人都无权要求KM部门提供匿名发言人的身份信息。

 

在张志东看来,KM是弥合大公司内部鸿沟的理想工具,尽管已经退休,他仍保留着终身荣誉顾问、腾讯学院荣誉院长的身份,每个月会去看两次乐问,十几年来累计回答了两百多个问题。

 

时间来到2019年,马化腾在朋友圈转发了一条腾讯优图借助AI找到失踪儿童的新闻,配语是:科技向善,我们新的使命和愿景。


同年11月,腾讯在21周年司庆上正式宣布将“科技向善”加入到公司使命愿景。


在腾讯帮助下,科研团队进行的沙漠化治理项目

短短一年时间,这四个字从“倡议”迅速升级为“使命”,在内外部都掀起了一场关于什么是“科技向善”的大讨论:


“怎么做才是科技向善?”


有人从实际业务出发,希望公司的软件不要效仿同行硬塞全家桶、续费不要做默认、消灭暗扣的扣费点......


也有人认为公司应该不仅仅是对产品细节小修小补,而是关注AI算法被滥用、用户隐私遭到侵犯、科技适老化建设严重滞后等社会性问题。

 

但也有员工担忧,这个充满理想主义色彩的使命愿景,会给腾讯带来无妄之灾:


“腾讯的「科技向善」和谷歌的「不作恶」差别很大......稍有不慎,你的行为就会被诟病不够“向善”,给业务捆上了道德的枷锁。”

 

果不其然,这个四个字很快成为内部灵魂拷问的必备词:


微信接槟榔企业广告,是否科技向善?


我厂新剧的设定,符合“科技向善”价值观?


科技向善,某些部门真的有在做吗?


广告多到影响用户使用,说好的科技向善呢?

 

每当问题出现,相关业务的总经理、总监免不了在内部公开道歉并承诺整改。

 

渐渐地,“科技向善”成为悬在所有腾讯员工上头的达摩克利斯之剑,上到决策层、下到基层员工,“我死后管它洪水滔天”的做法不再可取,因为一旦被外部曝光和内网责难,丢的不仅是个人,还是整个部门的脸面,甚至还将被全公司通报批评、接受处分。

 

腾讯素有偶像包袱。


外界流传“微信监听用户聊天来匹配广告”的谣言屡禁不绝,多次辟谣无果之后,张小龙竟下定决心推出自有输入法, 防止第三方输入法将用户信息收集后泄露和售卖。

 

但对于许多腾讯员工而言,内部和外部看到的像是两个截然不同的腾讯:他们在朋友圈、公众号时常可以看到身边同事转发的很多公司的正能量新闻,而在微博、知乎、抖音上,人们对腾讯的印象仍然停留在抄袭竞品、未成年人游戏等陈年老调上。

 

许多员工在KM中表达了自己的困惑:为什么首倡科技向善的腾讯,在外界口碑如此糟糕?

 

这样的烦恼,并非腾讯员工所独有,在全球范围内,包括Google、Facebook、苹果、亚马逊等科技公司,以及国内的阿里巴巴,都面临着如何与社会共处的难题,人们对于科技巨头的不安和不信任,和百年前面对石油、钢铁托拉斯如出一辙,在国与国之间蔓延开来。

 

一个首次被提问的热门词汇


2020年春末,腾讯的股价从2018年的谷底翻了一番,曾经的“腾讯没有梦想”和“背水一战”,都成了茶余饭后的冷笑话。


2B(To B)的质疑逐渐消散,新的矛盾暗流汹涌。


这也正是刘炽平所挥之不去的疑问:腾讯公司的战略蓝图当中,“仿佛少了一块”。

 

那个春天刚刚过去的全民战疫,给他带来了新的启发:在人心惶惶的2020年1月27日,腾讯受命紧急开发健康码,在内部贴出一份技术志愿者招募令,迅速获得数千员工响应。

 

整个疫情期间,腾讯有12000多名员工直接参与到了各式各样的战疫中来。

 

在此之前,拥有6万名员工的腾讯内部,要想推动如此大规模的线上协作,往往免不了合议、争执、妥协甚至流产等流程。但在公共利益面前,那道曾经备受诟病的“部门墙”居然神奇地消失了,KM上的提问,不断在互相寻找协作。

 

“微信生态可以为疫情做些什么?”


“腾讯公益能在疫情上尽什么力?”


“游戏部门是否应该推出传染病教育游戏来践行科技向善?”


“个人以及公司能为疫情做些什么?”

 

一方面是“战时状态”激发出的创新能力,健康码、腾讯会议、腾讯课堂都在疫情期间大放异彩,不仅在外部缓解了腾讯的口碑压力,也在内部KM中得到了充分的肯定;


一方面是社会对科技平台的期待和监管在层层加码,过去企业按比例投入一定利润参与公益的路径已经过时。


检查微信健康码,已经成为社会常态

在马化腾看来,如果一个企业的发展和所作出的贡献之间,没有合理的比例,是不可能往上生长的。

 

随着KM上关于社会价值的讨论越来越多,2020年秋天,「社会价值」开始成为重要的战略议题,列入到腾讯决策层的讨论中来。

 

2021年4月19日,腾讯再次启动战略升级,提出“可持续社会价值创新”战略,并宣布将为此首期投入500亿元设立“可持续社会价值事业部”(SSV),对包括基础科学、教育创新、乡村振兴、碳中和、FEW(食物、能源与水)、公众应急、养老科技和公益数字化等领域展开探索。

 

如果说“科技向善”是一句偏理想主义的口号,新战略、新部门的成立,昭示着腾讯要真刀真枪干一场了。

 

KM上一堆关于“如何向善”的提问,已经给出答案。

 

SSV新部门负责人陈菊红的企业微信和KM很快就被毛遂自荐的员工轰炸,“凌晨一点了,都有同事小窗问我:你们那儿还要不要人?”


腾讯全员很快收到了内部开放活水(注:活水是腾讯内部重新应聘换岗的特设制度)SSV的邮件邀请:产品经理、运营经理、项目经理、后台开发工程师、数字化解决方案架构师......

 

键盘侠的自省就是0前那个1

“科技向善”听起来非常浪漫主义。

 

截至今天,腾讯人在短短两年时间里,在KM上提出了38322条问题和看法。腾讯人对“向善”的疑问和思考,甚至超越了对饭堂菜式、升职加薪的花式重视。

 

这也包括腾讯总办领导、集团首席探索官网大为。网大为英文名叫做David Wallerstein,中文名的意思据说“网络上大有可为”。


2021年6月第一天,网大为在KM上问大家: “为什么要敢于坚持与众不同”?


网大为回想起2000年,他刚来到腾讯的时候,这家公司只有45个人,当时马化腾赶在互联网世纪泡沫崩盘前拿到了一笔救命钱,当时所有人的目标都很简单:让腾讯活下去。

 

时过境迁,20多年后的腾讯已经成为了人们眼中“稳定”的大公司,而网大为也体察到了一些变化:


10年前他进去电梯,很多员工会笑着跟他打招呼聊天,但从五六年前开始,公司电梯里没有人跟他说话了。


他问身边一位同事:“你在腾讯是做什么的?”


对方感觉很诧异:“你是什么意思?”

 

这件外人眼里,问一位同事从事什么岗位,也许再正常不过的事情,但如今却变得陌生起来。

 

这个变化对网大为触动很大,他在六一儿童节这天,明白了腾讯公司已经无可避免地长大:人们各司其职,分工细化,却也无可避免地遭遇沟通障碍和文化稀释的问题。


9万多人的腾讯,工作奋斗在全国乃至全球各地,加之在线化办公的普及, 如果没有共同驱动的核心价值,它将会沦为一台层级分明、没有人味的巨型商业机器。

 

KM上近十万腾讯人,这么多年来的撕逼讨论,艰难地维持、证明自己——我还是那么腾讯。

 

在刚刚结束的腾讯志愿者大会,这位首席探索官(CXO)在访谈中提到了他对企业文化的关注。


在他看来,把科技向善作为使命愿景,关心人、关心地球,在这个基础上提供产品和体验,才能吸引到更多志同道合的人才,才是腾讯接下来的生存之道。

 

钱没有了可以再赚,业务瓶颈可以突破,利润少了可以创造,但如果腾讯人不再像腾讯人,失去了自我质疑、自我反思和自我革命的企业文化,则失去了无数个“0”前面的那个“1”。

 

凌晨1点,马化腾给KM上网大为这篇访谈来了个一键三连:点赞、收藏和评论。









NervJS/taro: 开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发微信/京东/百度/支付宝/字节跳动/ QQ 小程序/H5/React Native 等应用。 https://taro.zone/

$
0
0

Taro

PRs Welcome

简体中文| English

👽Taro['tɑ:roʊ],泰罗·奥特曼,宇宙警备队总教官,实力最强的奥特曼。

简介

Taro是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发微信/京东/百度/支付宝/字节跳动/ QQ 小程序/H5 等应用。现如今市面上端的形态多种多样,Web、React Native、微信小程序等各种端大行其道,当业务要求同时在不同的端都要求有所表现的时候,针对不同的端去编写多套代码的成本显然非常高,这时候只编写一套代码就能够适配到多端的能力就显得极为需要。

版本说明

当前 Taro 已进入 3.x 时代,相较于 Taro 1/2 采用了重运行时的架构,让开发者可以获得完整的 React/Vue 等框架的开发体验,具体请参考 《小程序跨框架开发的探索与实践》

如果你想使用 Taro 1/2,可以访问 文档版本获得帮助。

学习资源

5 分钟上手 Taro 开发

awesome-taro

掘金小册: Taro 多端开发实现原理与实战

社区共享

Taro 交流社区——让每一次交流都被沉淀

Taro 物料市场——让每一个轮子产生价值

使用案例

Taro 已经投入了我们的生产环境中使用,业界也在广泛地使用 Taro 开发多端应用。

征集更多优秀案例

Taro 特性

框架支持

React/Nerv 支持

在 Taro 3 中可以使用完整的 React/Nerv 开发体验,具体请参考 基础教程——React

代码示例

importReact,{Component}from'react'import{View,Text}from'@tarojs/components'exportdefaultclassIndexextendsComponent{state={msg:'Hello World! '}componentWillUnmount(){}componentDidShow(){}componentDidHide(){}render(){return(<ViewclassName='index'><Text>{this.state.msg}</Text></View>)}}

Vue 支持

在 Taro 3 中可以使用完整的 Vue 开发体验,具体请参考 基础教程——Vue

代码示例

<template><viewclass="index"><text>{{msg}}</text></view></template><script>exportdefault{data() {return{msg:'Hello World!'}},created() {},onShow() {},onHide() {}}</script>

多端转换支持

Taro 方案的初心就是为了打造一个多端开发的解决方案。

目前 Taro 3 可以支持转换到 微信/京东/百度/支付宝/字节跳动/QQ 小程序以及 H5 端

POD驱逐-配置资源不足时的处理方式 | Kubernetes

$
0
0

配置资源不足时的处理方式

本页介绍如何使用 kubelet配置资源不足时的处理方式。

当可用计算资源较少时, kubelet需要保证节点稳定性。 这在处理如内存和硬盘之类的不可压缩资源时尤为重要。 如果任意一种资源耗尽,节点将会变得不稳定。

驱逐信号

kubelet支持按照以下表格中描述的信号触发驱逐决定。 每个信号的值在 description 列描述,基于 kubelet摘要 API。

驱逐信号描述
memory.availablememory.available:= node.status.capacity[memory]- node.stats.memory.workingSet
nodefs.availablenodefs.available:= node.stats.fs.available
nodefs.inodesFreenodefs.inodesFree:= node.stats.fs.inodesFree
imagefs.availableimagefs.available:= node.stats.runtime.imagefs.available
imagefs.inodesFreeimagefs.inodesFree:= node.stats.runtime.imagefs.inodesFree
pid.availablepid.available:= node.stats.rlimit.maxpid- node.stats.rlimit.curproc

上面的每个信号都支持字面值或百分比的值。基于百分比的值的计算与每个信号对应的总容量相关。

memory.available的值从 cgroupfs 获取,而不是通过类似 free -m的工具。 这很重要,因为 free -m不能在容器中工作,并且如果用户使用了 节点可分配资源特性,资源不足的判定将同时在本地 cgroup 层次结构的终端用户 Pod 部分和根节点做出。 这个 脚本复现了与 kubelet计算 memory.available相同的步骤。 kubeletinactive_file(意即活动 LRU 列表上基于文件后端的内存字节数)从计算中排除, 因为它假设内存在出现压力时将被回收。

kubelet只支持两种文件系统分区。

  1. nodefs文件系统,kubelet 将其用于卷和守护程序日志等。
  2. imagefs文件系统,容器运行时用于保存镜像和容器可写层。

imagefs可选。 kubelet使用 cAdvisor 自动发现这些文件系统。 kubelet不关心其它文件系统。当前不支持配置任何其它类型。 例如,在专用 filesytem中存储卷和日志是 不可以的。

在将来的发布中, kubelet将废除当前存在的 垃圾回收机制,这种机制目前支持将驱逐操作作为对磁盘压力的响应。

驱逐阈值

kubelet支持指定驱逐阈值,用于触发 kubelet回收资源。

每个阈值形式如下:

[eviction-signal][operator][quantity]

  • 合法的 eviction-signal标志如上所示。
  • operator是所需的关系运算符,例如 <
  • quantity是驱逐阈值值标志,例如 1Gi。合法的标志必须匹配 Kubernetes 使用的数量表示。 驱逐阈值也可以使用 %标记表示百分比。

举例说明,如果一个节点有 10Gi内存,希望在可用内存下降到 1Gi以下时引起驱逐操作, 则驱逐阈值可以使用下面任意一种方式指定(但不是两者同时)。

  • memory.available<10%
  • memory.available<1Gi

软驱逐阈值

软驱逐阈值使用一对由驱逐阈值和管理员必须指定的宽限期组成的配置对。在超过宽限期前, kubelet不会采取任何动作回收和驱逐信号关联的资源。如果没有提供宽限期, kubelet启动时将报错。

此外,如果达到了软驱逐阈值,操作员可以指定从节点驱逐 pod 时,在宽限期内允许结束的 pod 的最大数量。 如果指定了 pod.Spec.TerminationGracePeriodSeconds值, kubelet将使用它和宽限期二者中较小的一个。 如果没有指定, kubelet将立即终止 pod,而不会优雅结束它们。

软驱逐阈值的配置支持下列标记:

  • eviction-soft描述了驱逐阈值的集合(例如 memory.available<1.5Gi),如果在宽限期之外满足条件将触发 pod 驱逐。
  • eviction-soft-grace-period描述了驱逐宽限期的集合(例如 memory.available=1m30s),对应于在驱逐 pod 前软驱逐阈值应该被控制的时长。
  • eviction-max-pod-grace-period描述了当满足软驱逐阈值并终止 pod 时允许的最大宽限期值(秒数)。

硬驱逐阈值

硬驱逐阈值没有宽限期,一旦察觉, kubelet将立即采取行动回收关联的短缺资源。 如果满足硬驱逐阈值, kubelet将立即结束 Pod 而不是体面地终止它们。

硬驱逐阈值的配置支持下列标记:

  • eviction-hard描述了驱逐阈值的集合(例如 memory.available<1Gi),如果满足条件将触发 Pod 驱逐。

kubelet有如下所示的默认硬驱逐阈值:

  • memory.available<100Mi
  • nodefs.available<10%
  • imagefs.available<15%

在Linux节点上,默认值还包括 nodefs.inodesFree<5%

驱逐监控时间间隔

kubelet根据其配置的整理时间间隔计算驱逐阈值。

  • housekeeping-interval是容器管理时间间隔。

节点状态

kubelet会将一个或多个驱逐信号映射到对应的节点状态。

如果满足硬驱逐阈值,或者满足独立于其关联宽限期的软驱逐阈值时, kubelet将报告节点处于压力下的状态。

下列节点状态根据相应的驱逐信号定义。

节点状态驱逐信号描述
MemoryPressurememory.available节点上可用内存量达到逐出阈值
DiskPressurenodefs.available, nodefs.inodesFree, imagefs.available, 或 imagefs.inodesFree节点或者节点的根文件系统或镜像文件系统上可用磁盘空间和 i 节点个数达到逐出阈值
PIDPressurepid.available在(Linux)节点上的可用进程标识符已降至驱逐阈值以下

kubelet将以 --node-status-update-frequency指定的频率连续报告节点状态更新,其默认值为 10s

节点状态振荡

如果节点在软驱逐阈值的上下振荡,但没有超过关联的宽限期时,将引起对应节点的状态持续在 true 和 false 间跳变,并导致不好的调度结果。

为了防止这种振荡,可以定义下面的标志,用于控制 kubelet从压力状态中退出之前必须等待的时间。

  • eviction-pressure-transition-periodkubelet从压力状态中退出之前必须等待的时长。

kubelet将确保在设定的时间段内没有发现和指定压力条件相对应的驱逐阈值被满足时,才会将状态变回 false

回收节点层级资源

如果满足驱逐阈值并超过了宽限期, kubelet将启动回收压力资源的过程,直到它发现低于设定阈值的信号为止。

kubelet将尝试在驱逐终端用户 pod 前回收节点层级资源。 发现磁盘压力时,如果节点针对容器运行时配置有独占的 imagefskubelet回收节点层级资源的方式将会不同。

使用 imagefs

如果 nodefs文件系统满足驱逐阈值, kubelet通过驱逐 pod 及其容器来释放磁盘空间。

如果 imagefs文件系统满足驱逐阈值, kubelet通过删除所有未使用的镜像来释放磁盘空间。

未使用 imagefs

如果 nodefs满足驱逐阈值, kubelet将以下面的顺序释放磁盘空间:

  1. 删除停止运行的 pod/container
  2. 删除全部没有使用的镜像

驱逐最终用户的 pod

如果 kubelet在节点上无法回收足够的资源, kubelet将开始驱逐 pod。

kubelet首先根据他们对短缺资源的使用是否超过请求来排除 pod 的驱逐行为, 然后通过 优先级, 然后通过相对于 pod 的调度请求消耗急需的计算资源。

kubelet按以下顺序对要驱逐的 pod 排名:

  • BestEffortBurstable,其对短缺资源的使用超过了其请求,此类 pod 按优先级排序,然后使用高于请求。
  • Guaranteedpod 和 Burstablepod,其使用率低于请求,最后被驱逐。 GuaranteedPod 只有为所有的容器指定了要求和限制并且它们相等时才能得到保证。 由于另一个 Pod 的资源消耗,这些 Pod 保证永远不会被驱逐。 如果系统守护进程(例如 kubeletdocker、和 journald)消耗的资源多于通过 system-reservedkube-reserved分配保留的资源,并且该节点只有 GuaranteedBurstablePod 使用少于剩余的请求,然后节点必须选择驱逐这样的 Pod 以保持节点的稳定性并限制意外消耗对其他 pod 的影响。 在这种情况下,它将首先驱逐优先级最低的 pod。

必要时, kubelet会在遇到 DiskPressure时逐个驱逐 Pod 来回收磁盘空间。 如果 kubelet响应 inode短缺,它会首先驱逐服务质量最低的 Pod 来回收 inodes。 如果 kubelet响应缺少可用磁盘,它会将 Pod 排在服务质量范围内,该服务会消耗大量的磁盘并首先结束这些磁盘。

使用 imagefs

如果是 nodefs触发驱逐, kubelet将按 nodefs用量 - 本地卷 + pod 的所有容器日志的总和对其排序。

如果是 imagefs触发驱逐, kubelet将按 pod 所有可写层的用量对其进行排序。

未使用 imagefs

如果是 nodefs触发驱逐, kubelet会根据磁盘的总使用情况对 pod 进行排序 - 本地卷 + 所有容器的日志及其可写层。

最小驱逐回收

在某些场景,驱逐 pod 会导致回收少量资源。这将导致 kubelet反复碰到驱逐阈值。除此之外,对如 disk这类资源的驱逐时比较耗时的。

为了减少这类问题, kubelet可以为每个资源配置一个 minimum-reclaim。 当 kubelet发现资源压力时, kubelet将尝试至少回收驱逐阈值之下 minimum-reclaim数量的资源。

例如使用下面的配置:

--eviction-hard=memory.available<500Mi,nodefs.available<1Gi,imagefs.available<100Gi
--eviction-minimum-reclaim="memory.available=0Mi,nodefs.available=500Mi,imagefs.available=2Gi"`

如果 memory.available驱逐阈值被触发, kubelet将保证 memory.available至少为 500Mi。 对于 nodefs.availablekubelet将保证 nodefs.available至少为 1.5Gi。 对于 imagefs.availablekubelet将保证 imagefs.available至少为 102Gi, 直到不再有相关资源报告压力为止。

所有资源的默认 eviction-minimum-reclaim值为 0

调度器

当资源处于压力之下时,节点将报告状态。调度器将那种状态视为一种信号,阻止更多 pod 调度到这个节点上。

节点状态调度器行为
MemoryPressure新的 BestEffortPod 不会被调度到该节点
DiskPressure没有新的 Pod 会被调度到该节点

节点 OOM 行为

如果节点在 kubelet回收内存之前经历了系统 OOM(内存不足)事件,它将基于 oom-killer做出响应。

kubelet基于 pod 的 service 质量为每个容器设置一个 oom_score_adj值。

Service 质量oom_score_adj
Guaranteed-998
BestEffort1000
Burstablemin(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)

如果 kubelet在节点经历系统 OOM 之前无法回收内存, oom_killer将基于它在节点上 使用的内存百分比算出一个 oom_score,并加上 oom_score_adj得到容器的有效 oom_score,然后结束得分最高的容器。

预期的行为应该是拥有最低服务质量并消耗和调度请求相关内存量最多的容器第一个被结束,以回收内存。

和 pod 驱逐不同,如果一个 Pod 的容器是被 OOM 结束的,基于其 RestartPolicy, 它可能会被 kubelet重新启动。

最佳实践

以下部分描述了资源外处理的最佳实践。

可调度资源和驱逐策略

考虑以下场景:

  • 节点内存容量: 10Gi
  • 操作员希望为系统守护进程保留 10% 内存容量(内核、 kubelet等)。
  • 操作员希望在内存用量达到 95% 时驱逐 pod,以减少对系统的冲击并防止系统 OOM 的发生。

为了促成这个场景, kubelet将像下面这样启动:

--eviction-hard=memory.available<500Mi
--system-reserved=memory=1.5Gi

这个配置的暗示是理解系统保留应该包含被驱逐阈值覆盖的内存数量。

要达到这个容量,要么某些 pod 使用了超过它们请求的资源,要么系统使用的内存超过 1.5Gi - 500Mi = 1Gi

这个配置将保证在 pod 使用量都不超过它们配置的请求值时,如果可能立即引起内存压力并触发驱逐时,调度器不会将 pod 放到这个节点上。

DaemonSet

我们永远都不希望 kubelet驱逐一个从 DaemonSet派生的 Pod,因为这个 Pod 将立即被重建并调度回相同的节点。

目前, kubelet没有办法区分一个 Pod 是由 DaemonSet还是其他对象创建。 如果/当这个信息可用时, kubelet可能会预先将这些 pod 从提供给驱逐策略的候选集合中过滤掉。

总之,强烈推荐 DaemonSet不要创建 BestEffort的 Pod,防止其被识别为驱逐的候选 Pod。 相反,理想情况下 DaemonSet应该启动 Guaranteed的 pod。

现有的回收磁盘特性标签已被弃用

kubelet已经按需求清空了磁盘空间以保证节点稳定性。

当磁盘驱逐成熟时,下面的 kubelet标志将被标记为废弃的,以简化支持驱逐的配置。

现有标签新标签
--image-gc-high-threshold--eviction-hardor eviction-soft
--image-gc-low-threshold--eviction-minimum-reclaim
--maximum-dead-containersdeprecated
--maximum-dead-containers-per-containerdeprecated
--minimum-container-ttl-durationdeprecated
--low-diskspace-threshold-mb--eviction-hardor eviction-soft
--outofdisk-transition-frequency--eviction-pressure-transition-period

已知问题

以下部分描述了与资源外处理有关的已知问题。

kubelet 可能无法立即发现内存压力

kubelet当前通过以固定的时间间隔轮询 cAdvisor来收集内存使用数据。如果内存使用在那个时间窗口内迅速增长, kubelet可能不能足够快的发现 MemoryPressureOOMKiller将不会被调用。我们准备在将来的发行版本中通过集成 memcg通知 API 来减小这种延迟。当超过阈值时,内核将立即告诉我们。

如果您想处理可察觉的超量使用而不要求极端精准,可以设置驱逐阈值为大约 75% 容量作为这个问题的变通手段。这将增强这个特性的能力,防止系统 OOM,并提升负载卸载能力,以再次平衡集群状态。

kubelet 可能会驱逐超过需求数量的 pod

由于状态采集的时间差,驱逐操作可能驱逐比所需的更多的 pod。将来可通过添加从根容器获取所需状态的能力 https://github.com/google/cadvisor/issues/1247来减缓这种状况。


ClickHouse 物化视图在微信的实战经验

$
0
0

前言

ClickHouse广泛用于用户和系统日志查询场景中,借助腾讯云提供基础设施,微信也在分阶段逐步推进clickhouse的建设和应用,目前作为基础建设的一部分,主要针对于OLAP场景,为业务方提供稳定高效的查询服务。在业务场景下,实时事件流上报可能会在不同的日志,以不同的格式、途径写入到clickhouse。在之前的使用中,通过查询多个日志表join实现多个指标的整合。用传统JOIN方式,我们遇到如下困难: 1.每个查询会有非常长的代码,有的甚至1500行、2000行sql,使用和理解上特别痛苦; 2.性能上无法满足业务诉求,日志量大会爆内存不足; 如何将这些数据进行整合,以ClickHouse宽表的方式呈现给上层使用,用户可以在一张表中查到所需的所有指标,避免提供多表带来的代码复杂度和性能开销问题?本文将重点介绍如何通过物化视图有效解决上述场景的问题。在介绍之前,先铺垫一下物化视图的简单使用,包括如何创建,如何增加维度和指标,如何结合字典增维等场景。

准备工作

很多情况下,没有场景和数据,就很难感同身受的去了解整个过程,所以在写这篇文章前,利用python的Faker库先生成一些模拟数据,模拟真实场景,以数据入手,来介绍关于物化视图的一些使用经验。环境:wsl单节点 centos7 版本:21.3.12.2-lts 数据库: ods,dim,dwm,dws,test 环境相关配置以及本文后续提到代码和模拟数据,均已上传到github的个人项目中 https://github.com/IVitamin-C/clickhouse-learning,供参考。如有问题,可以提issues或者私信我。

用户维度数据

通过代码生成15000个用户,其中Android 10000,ios 5000。

create table ods.user_dim_local on cluster cluster    
(
 day Date comment '数据分区-天',
 uid UInt32 default 0 comment 'uid',
 platform String default '' comment '平台 android/ios',
 country String default '' comment '国家',
 province String default '' comment '省及直辖市',
 isp String default '' comment '运营商',
 app_version String default '' comment '应用版本',
 os_version String default '' comment '系统版本',
 mac String default '' comment 'mac',
 ip String default '' comment 'ip',
 gender String default '' comment '性别',
 age Int16 default -1 comment '年龄'
)
engine = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/ods.user_dim_local','{replica}')
PARTITION BY day
PRIMARY KEY day
ORDER BY day
TTL day + toIntervalDay(3) + toIntervalHour(3)
SETTINGS index_granularity = 8192

--drop table dim.user_dim_dis on cluster cluster;
create table dim.user_dim_dis on cluster cluster
as ods.user_dim_local
engine=Distributed(cluster,ods,user_dim_local,rand());

物品维度数据

通过代码生成100个物品。

create table ods.item_dim_local on cluster cluster    
(
 day Date comment '数据分区-天',
 item_id UInt32 default 0 comment 'item_id',
 type_id UInt32 default 0 comment 'type_id',
 price UInt32 default 0 comment 'price'
)
engine = ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/ods.item_dim_local','{replica}')
PARTITION BY day
PRIMARY KEY day
ORDER BY day
TTL day + toIntervalDay(3) + toIntervalHour(3)
SETTINGS index_granularity = 8192

--drop table dim.item_dim_dis on cluster cluster;
create table dim.item_dim_dis on cluster cluster
as ods.item_dim_local
engine=Distributed(cluster,ods,item_dim_local,rand());

action_001行为数据

通过代码生成最近3小时的数据,模拟用户的实际访问,主要是曝光、点击、和曝光时间3个指标

--drop table ods.action_001_local on cluster cluster;   
create table ods.action_001_local on cluster cluster (
day Date default toDate(second) comment '数据分区-天(Date)'
,hour DateTime default toStartOfHour(second) comment '数据时间-小时(DateTime)'
,second DateTime default '1970-01-01 08:00:00' comment '数据时间-秒'
,insert_second DateTime default now() comment '数据写入时间'
,platform String default '' comment '平台 android/ios'
,ip String default '' comment 'client-ip'
,isp String default '' comment '运营商'
,uid UInt32 default 0 comment 'uid'
,ver String default '' comment '版本'
,item_id UInt32 default 0 comment '物品id'
,show_cnt UInt32 default 0 comment '曝光次数'
,click_cnt UInt32 default 0 comment '点击次数'
,show_time UInt32 default 0 comment '曝光时间'
)
engine=ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/ods.action_001_local','{replica}')
PARTITION BY day
PRIMARY KEY (day,hour)
ORDER BY (day,hour,platform,item_id)
TTL day + toIntervalDay(10) + toIntervalHour(4)
SETTINGS index_granularity = 8192
;
--drop table dws.action_001_dis on cluster cluster;
create table dws.action_001_dis on cluster cluster
as ods.action_001_local
engine=Distributed(cluster,ods,action_001_local,rand());

action_002 行为数据

通过代码生成最近3小时的数据,模拟用户点击之后的一些其他操作。这里对指标简单命名。

--drop table ods.action_002_local on cluster cluster;   
create table ods.action_002_local on cluster cluster (
day Date default toDate(second) comment '数据分区-天(Date)'
,hour DateTime default toStartOfHour(second) comment '数据时间-小时(DateTime)'
,second DateTime default '1970-01-01 08:00:00' comment '数据时间-秒'
,insert_second DateTime default now() comment '数据写入时间'
,platform String default '' comment '平台 android/ios'
,ip String default '' comment 'client-ip'
,isp String default '' comment '运营商'
,uid UInt32 default 0 comment 'uid'
,ver String default '' comment '版本'
,item_id UInt32 default 0 comment '商品id'
,action_a_cnt UInt32 default 0 comment 'actionA次数'
,action_b_cnt UInt32 default 0 comment 'actionB次数'
,action_c_cnt UInt32 default 0 comment 'actionC次数'
,action_a_time UInt32 default 0 comment 'actionA时间'
,action_b_time UInt32 default 0 comment 'actionA时间'
,action_c_time UInt32 default 0 comment 'actionA时间'
,action_d_sum UInt32 default 0 comment 'action_d_sum'
,action_e_sum UInt32 default 0 comment 'action_e_sum'
,action_f_sum UInt32 default 0 comment 'action_f_sum'
)
engine=ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/ods.action_002_local','{replica}')
PARTITION BY day
PRIMARY KEY (day,hour)
ORDER BY (day,hour,platform,item_id)
TTL day + toIntervalDay(10) + toIntervalHour(4)
SETTINGS index_granularity = 8192
;
--drop table dws.action_002_dis on cluster cluster;
create table dws.action_002_dis on cluster cluster
as ods.action_002_local
engine=Distributed(cluster,ods,action_002_local,rand());

这里准备两份日志,主要是为了文章后半部分提到的物化视图的进阶用法,解决某些join场景。

物化视图的简单case

场景

在action_log接入到clickhouse之后,就可以直接通过分布式表去查询了。但是,随着数据量的慢慢积累,比如action_001,它是主页的曝光和点击的数据,一天可能会非常大,百亿级别。这个时候,查一天的去重uv可能还能勉强接受,但是查一周,查一月可能就没法玩了,耗时上巨大,有些也可能超过内存限制。得提速,业务不管你实现方案,要看数据结果,这个时候,物化视图就派上用场了。A产品整理后提了一个诉求,希望可以看到每小时的每个商品的主页统计指标。有时也可能要查1周,1月。经过梳理得到了下面这个需求


时间(最细小时)商品id平台版本
曝光人数



曝光次数



点击人数



点击次数



人均曝光时间



每次平均曝光时间



人均点击次数



ctr



首先,在创建物化视图前评估一下数据量。物化视图会计算当前批次的数据汇总一次,然后根据维度自动merge聚合统计的指标,但是不会跨节点和分区,所以理想状况下,数据量的估算sql为

select uniqCombined(hostName(),hour,item_id,platform,ver)   
from dws.action_001

经过计算发现,数据量只是原表的1/n,主要取决于数据的重合度,这个只是最完美的理想状态,但是实际上差距也不会很大,已经比原表少很多数据量了。现在需求明确,也估算完数据量了,在这个数据量下,查询1周或者1月的数据是完全可以接受的。开搞物化视图。

创建过程

首先贴下官方文档https://clickhouse.tech/docs/en/sql-reference/statements/create/view/物化视图的创建有两种方式,一种是


CREATE MATERIALIZED VIEW [IF NOT EXISTS] [db.]table_name [ON CLUSTER]
ENGINE = engine
AS SELECT 

这种创建物化视图的好处是创建简单,避免自己写错聚合函数类型带来数据上的写入失败。缺点是alter有局限性,每次更改都需要替换或者修改物化视图的计算逻辑,而且也不能实现文章后续的有限替代join场景。第二种方式是先创建一个存储表,存储表是[Replicated]AggregatingMergeTree,然后通过创建的物化视图使用to的方式写入到存储表中,相当于存储的数据和计算的逻辑分为了两张表分别处理。


CREATE MATERIALIZED VIEW [IF NOT EXISTS] [db.]table_name [ON CLUSTER] TO db.]name
AS SELECT 

因为已经指定了存储的表,所以物化视图的创建也不需要指定engine,在查询中,查物化视图和查实际的存储表得到一样的数据,因为都是来自于同一份存储数据。在建表之前还有个细节,TO db.name 后面的表不一定是本地表对本地表,还可以本地表对分布式表,可以基于shard_key处理一些分桶策略,但是会存在写放大的问题,导致集群写入频率增大,负载提高,可以但是慎用。 必须要注意的是,from的表一定是本地表。这里大家区分下存储表和计算表两个名词,后续的场景会用到。以下为完整的建表sql 创建ReplicatedAggregatingMergeTree作为数据存储表和分布式表

--drop table dwm.mainpage_stat_mv_local on cluster cluster;   
create table dwm.mainpage_stat_mv_local on cluster cluster
(
day Date comment '数据分区-天'
,hour DateTime comment '数据时间-小时(DateTime)'
,platform String comment '平台 android/ios'
,ver String comment '版本'
,item_id UInt32 comment '物品id'
,shown_uv AggregateFunction(uniqCombined,UInt32) comment '曝光人数'
,shown_cnt SimpleAggregateFunction(sum,UInt64) comment '曝光次数'
,click_uv AggregateFunction(uniqCombined,UInt32) comment '点击人数'
,click_cnt SimpleAggregateFunction(sum,UInt64) comment '点击次数'
,show_time_sum  SimpleAggregateFunction(sum,UInt64) comment '总曝光时间/秒'
)
engine=ReplicatedAggregatingMergeTree('/clickhouse/tables/{layer}-{shard}/dwm.mainpage_stat_mv_local','{replica}')
PARTITION by day
PRIMARY KEY (day,hour)
ORDER by (day,hour,platform,ver,item_id)
TTL day + toIntervalDay(92) + toIntervalHour(5)
SETTINGS index_granularity = 8192

--drop table dws.mainpage_stat_mv_dis on cluster cluster
create table dws.mainpage_stat_mv_dis on cluster cluster
as dwm.mainpage_stat_mv_local
engine=Distributed(cluster,dwm,mainpage_stat_mv_local,rand());

创建物化视图作为计算逻辑并使用to将数据流向ReplicatedAggregatingMergeTree

create  MATERIALIZED VIEW dwm.mv_main_page_stat_mv_local on cluster cluster to dwm.mainpage_stat_mv_local (   
day Date comment '数据分区-天'
,hour DateTime comment '数据时间-小时(DateTime)'
,platform String comment '平台 android/ios'
,ver String comment '版本'
,item_id UInt32 comment '物品id'
,shown_uv AggregateFunction(uniqCombined,UInt32) comment '曝光人数'
,shown_cnt SimpleAggregateFunction(sum,UInt64) comment '曝光次数'
,click_uv AggregateFunction(uniqCombined,UInt32) comment '点击人数'
,click_cnt SimpleAggregateFunction(sum,UInt64) comment '点击次数'
,show_time_sum  SimpleAggregateFunction(sum,UInt64) comment '总曝光时间/秒'
)
AS SELECT day
     ,hour
     ,platform
     ,ver
     ,item_id
     ,uniqCombinedStateIf(uid,a.show_cnt>0) as shown_uv
     ,sum(a.show_cnt) as show_cnt
     ,uniqCombinedStateIf(uid,a.click_cnt>0) as click_uv
     ,sum(a.click_cnt) as click_cnt
     ,sum(toUInt64(show_time/1000)) as show_time_sum
from ods.action_001_local as a
group by
      day
     ,hour
     ,platform
     ,ver
     ,item_id

查询数据

SELECT   
    day,
    platform,
    uniqCombinedMerge(shown_uv) AS shown_uv,
    sum(shown_cnt) AS shown_cnt,
    uniqCombinedMerge(click_uv) AS click_uv,
    sum(click_cnt) AS click_cnt,
    sum(show_time_sum) AS show_time_sum
FROM dws.mainpage_stat_mv_dis
GROUP BY
    day,
    platform

Query id: f6d4d3dd-33f1-408e-92a7-4901fcad50aa

┌────────day─┬─platform─┬─shown_uv─┬─shown_cnt─┬─click_uv─┬─click_cnt─┬─show_time_sum─┐
│ 2021-06-06 │ ios      │     5000 │         0 │     4509 │    554927 │        781679 │
│ 2021-06-05 │ android  │     9613 │         0 │     5249 │    342910 │        491502 │
│ 2021-06-06 │ android  │     9995 │         0 │     8984 │   1126905 │       1570323 │
│ 2021-06-05 │ ios      │     4819 │         0 │     2636 │    175932 │        248274 │
└────────────┴──────────┴──────────┴───────────┴──────────┴───────────┴───────────────┘

4 rows in set. Elapsed: 0.013 sec. Processed 58.70 thousand rows, 14.38 MB (4.42 million rows/s., 1.08 GB/s.)

处理中的细节

这个地方再细描述下物化视图的处理逻辑,先贴一下官方说明

Important Materialized views in ClickHouse are implemented more like insert triggers. If there’s some aggregation in the view >query, it’s applied only to the batch of freshly inserted data. Any changes to existing data of source table (like update, >delete, drop partition, etc.) does not change the materialized view.

根据说明,物化视图是计算每批次写入原表的数据,假设一批写入了10w,那么物化视图就计算了这10w的数据,然后可能聚合之后就剩1w了写入到表中,剩下的过程就交给后台去merge聚合了,这个时候就要去理解物化视图的核心字段类型,AggregateFunction和SimpleAggregateFunction了。这里主要讲两个场景的计算,去理解这个字段类型,一个是uniqCombined计算uv,一个是sum计算pv。

首先是uv计算场景在大数据量下,使用uniqExact去计算精确uv,存储开销大,不便于网络传输数据,查询耗时长,还容易爆内存。除非个别情况下,不推荐使用。uniqCombined(HLL_precision)(x[, ...]) 官方说明 1.为聚合中的所有参数计算一个散列(为String计算64位散列,否则为32位散列),然后在计算中使用它。这里只当输入1个或者多个参数时,会先计算一个hash散列,这里的hash随着基数的增大,会发生碰撞。2.使用三种算法的组合:数组、哈希表和带纠错表的HyperLogLog。对于少量不同的元素,使用数组。当数据量较大时,使用哈希表。对于大数量的元素集,使用HyperLogLog,它将占用固定数量的内存。3.确定地提供结果(它不依赖于查询处理顺序)。所以在使用这个函数时,误差主要来源于两个地方,一个是计算散列时的hash碰撞,一个是在基数较大时的HyperLogLog的本身误差。但是从生产使用的表现来说,计算高效且稳定,计算结果确定且误差较小,值得使用。毕竟主要针对分析场景而不是金融等对数据准确性要求非常高的情况。正常计算uniqCombined时返回的是UInt64计算好的结果,因为是uv去重的计算场景,所以在使用物化视图计算每批次数据结果后,这个结果是无法迭代累加得到正确结果的(这里的累加不是加法运算哈)。所以要存储成为可以累加的状态,这个时候就要使用-State函数组合器,并使用AggregateFunction字段存储聚合函数提供的这个可以累加的中间状态而不是结果值。uniqCombinedState会得到AggregateFunction(uniqCombined,[String,UInt,Int])这样的一个字段类型。同时,uniqCombined是一个聚合函数,那么我们在group by之后会得到一个元素的组合,同时不管进行了多少个批次的数据计算,每个批次的计算结果不外乎是上面arr,set,hyperLogLog中的一种(具体会涉及序列化和反序列化,更复杂一些,这里简单理解),本身是支持添加元素或者合并多个的操作的,那么每个批次的计算结果也是可以合并的。以集合举例,我们在两次计算分别得到了

批次platformveruv

1android1.1{1001,1002,1003,1004}

2android1.2{1009,1010,1130,1131}

3android1.1{2001,3002,1003,3004}

4android1.2{2009,1010,2130,2131}

在写入到表之后没有merge之前,存储的实际是4个批次的数据,在这个时候进行计算时,计算过程会聚合,这个中间状态会合并,但是这个时候如果直接使用uniqCombined计算这个中间状态会得到什么样的结果呢,我们举例说明下

SELECT   
    platform,
    ver,
    uniqCombined(xx)
FROM
(
    SELECT
        platform,
        ver,
        uniqCombinedState(uid) AS xx
    FROM
    (
        SELECT
            a.1 AS platform,
            a.2 AS ver,
            a.3 AS uid
        FROM system.one
        ARRAY JOIN [('android', '1.1', 1001), ('android', '1.1', 1002), ('android', '1.1', 1003), ('android', '1.1', 1004)] AS a
    )
    GROUP BY
        platform,
        ver
    UNION ALL
    SELECT
        platform,
        ver,
        uniqCombinedState(uid) AS xx
    FROM
    (
        SELECT
            a.1 AS platform,
            a.2 AS ver,
            a.3 AS uid
        FROM system.one
        ARRAY JOIN [('android', '1.2', 1009), ('android', '1.2', 1010), ('android', '1.2', 1130), ('android', '1.2', 1131)] AS a
    )
    GROUP BY
        platform,
        ver
    UNION ALL
    SELECT
        platform,
        ver,
        uniqCombinedState(uid) AS xx
    FROM
    (
        SELECT
            a.1 AS platform,
            a.2 AS ver,
            a.3 AS uid
        FROM system.one
        ARRAY JOIN [('android', '1.1', 2001), ('android', '1.1', 3002), ('android', '1.1', 1003), ('android', '1.1', 3004)] AS a
    )
    GROUP BY
        platform,
        ver
    UNION ALL
    SELECT
        platform,
        ver,
        uniqCombinedState(uid) AS xx
    FROM
    (
        SELECT
            a.1 AS platform,
            a.2 AS ver,
            a.3 AS uid
        FROM system.one
        ARRAY JOIN [('android', '1.2', 2009), ('android', '1.2', 1010), ('android', '1.2', 2130), ('android', '1.2', 2131)] AS a
    )
    GROUP BY
        platform,
        ver
)
GROUP BY
    platform,
    ver

Query id: 09069556-65a8-42a2-9b0b-c002264a1bb4

┌─platform─┬─ver─┬─uniqCombined(xx)─┐
│ android  │ 1.2 │                2 │
│ android  │ 1.1 │                2 │
└──────────┴─────┴──────────────────┘

2 rows in set. Elapsed: 0.007 sec.

这个结果是明显不对的,因为他将这个中间状态也作为了计算的输入重新计算了,所以在使用上一定要注意AggregateFunction中的State状态使用Merge解析才能得到正确的结果。正确的sql


SELECT
    platform,
    ver,
    uniqCombinedMerge(xx) AS uv
FROM
(
    SELECT
        platform,
        ver,
        uniqCombinedState(uid) AS xx
    FROM
    (
        SELECT
            a.1 AS platform,
            a.2 AS ver,
            a.3 AS uid
        FROM system.one
        ARRAY JOIN [('android', '1.1', 1001), ('android', '1.1', 1002), ('android', '1.1', 1003), ('android', '1.1', 1004)] AS a
    )
    GROUP BY
        platform,
        ver
    UNION ALL
    SELECT
        platform,
        ver,
        uniqCombinedState(uid) AS xx
    FROM
    (
        SELECT
            a.1 AS platform,
            a.2 AS ver,
            a.3 AS uid
        FROM system.one
        ARRAY JOIN [('android', '1.2', 1009), ('android', '1.2', 1010), ('android', '1.2', 1130), ('android', '1.2', 1131)] AS a
    )
    GROUP BY
        platform,
        ver
    UNION ALL
    SELECT
        platform,
        ver,
        uniqCombinedState(uid) AS xx
    FROM
    (
        SELECT
            a.1 AS platform,
            a.2 AS ver,
            a.3 AS uid
        FROM system.one
        ARRAY JOIN [('android', '1.1', 2001), ('android', '1.1', 3002), ('android', '1.1', 1003), ('android', '1.1', 3004)] AS a
    )
    GROUP BY
        platform,
        ver
    UNION ALL
    SELECT
        platform,
        ver,
        uniqCombinedState(uid) AS xx
    FROM
    (
        SELECT
            a.1 AS platform,
            a.2 AS ver,
            a.3 AS uid
        FROM system.one
        ARRAY JOIN [('android', '1.2', 2009), ('android', '1.2', 1010), ('android', '1.2', 2130), ('android', '1.2', 2131)] AS a
    )
    GROUP BY
        platform,
        ver
)
GROUP BY
    platform,
    ver

Query id: 2a7137a7-f8fb-4b36-a37f-642348ab3ac6

┌─platform─┬─ver─┬─uv─┐
│ android  │ 1.2 │  7 │
│ android  │ 1.1 │  7 │
└──────────┴─────┴────┘

2 rows in set. Elapsed: 0.009 sec.

这里使用union all 模拟的是每个批次的写入数据。通过这个case主要是介绍uniqCombined生成中间态和解中间态的过程,避免大家错误使用哈。通过刚才的错误sql也侧面说明了,中间态存储的记录数要小于原表写入的数据,主要是按照group by的字段进行聚合计算得到的。

接着讲第二个场景,pv的计算。一般情况下,pv通常采用sum进行计算,sum计算和uv计算存在一个比较大的差异,那就是结果值可以累加。所以从逻辑上来讲,每批次计算可以直接是结果值,那么在聚合的时候可以再次进行sum操作可以得到正确的结果。那么这个时候除了采用AggregateFunction外存储中间态外也可以选择SimpleAggregateFunction存储每次计算结果,存储开销是不一样的

SELECT byteSize(xx)   
FROM
(
    SELECT sumSimpleState(a) AS xx
    FROM
    (
        SELECT 1001 AS a
        UNION ALL
        SELECT 1002 AS a
    )
)

Query id: ac6c5354-d59e-49a0-a54f-ea480acc8f3f

┌─byteSize(xx)─┐
│            8 │
└──────────────┘

SELECT byteSize(xx)
FROM
(
    SELECT sumState(a) AS xx
    FROM
    (
        SELECT 1001 AS a
        UNION ALL
        SELECT 1002 AS a
    )
)

Query id: 01b2ecb5-9e14-4f85-8cc6-5033671560ac

┌─byteSize(xx)─┐
│           16 │
└──────────────┘

2倍的存储差距,再来简单测试下查询效率

--SimpleAggregateFunction   
SELECT sum(xx)
FROM
(
    SELECT
        a % 1000 AS b,
        sumSimpleState(a) AS xx
    FROM
    (
        SELECT number AS a
        FROM numbers(1000000000)
    )
    GROUP BY b
)

Query id: 7c8f4b77-1033-4184-ad2f-1e6719723aca

┌────────────sum(xx)─┐
│ 499999999500000000 │
└────────────────────┘
1 rows in set. Elapsed: 4.140 sec. Processed 1.00 billion rows, 8.00 GB (241.58 million rows/s., 1.93 GB/s.)

--AggregateFunction
SELECT sumMerge(xx)
FROM
(
    SELECT
        a % 1000 AS b,
        sumState(a) AS xx
    FROM
    (
        SELECT number AS a
        FROM numbers(1000000000)
    )
    GROUP BY b
)

Query id: 401c0a9f-30fe-4d9a-88b0-1a33ffcf4f43

┌───────sumMerge(xx)─┐
│ 499999999500000000 │
└────────────────────┘
1 rows in set. Elapsed: 3.201 sec. Processed 1.00 billion rows, 8.00 GB (312.42 million rows/s., 2.50 GB/s.)

查询上有些许差距,这里的数据是通过numbers()函数生成,但是如果是写入和查询完全通过磁盘io的话,这个差距理论上会非常小,SimpleAggregateFunction会读数据更少,写数据更少,存储差距为刚好一半。其中,几乎所有的聚合函数都可以使用AggregateFunction,而只有某些场景可以使用SimpleAggregateFunction,所以在于推广使用和上层查询统一时,可以只选择使用AggregateFunction。根据业务场景自行取舍。

除了uniqCombined和sum外,还有非常多的聚合函数通过物化视图可以实现,这里主要列举一下uv和pv使用的案例,其他的函数也是相同的用法。这个里有个注意事项,需要注意,AggregateFunction严格要求输入字段的类型,比如1就是UInt8,不能是UInt16,AggregateFunction(sum,UInt32)不能被写入到AggregateFunction(sum,UInt8)里,这个错误在创建物化视图的时候是不会感知到的(建表校验问题,已提issues),但是在写入的时候是会报错的,所以在错误感知上要弱一些,数据一致性会受到影响。SimpleAggregateFunction和AggregateFunction在sum场景有些不一样,它的输入参数如果是UInt或者Int行,那么它的输入参数只能是UInt64或者Int64,而不是必须按照输入字段。可能的事SimpleAggragateFunction的输出又是下个过程的输入,所以SimpleAggregateFunction(sum,type)中的type是按照输出参数类型去创建,max,min等输入输出同类型的没有这个情况。

物化视图的进阶使用

上面是物化视图的一个简单case,主要针对一些单日志的固化场景处理,减少数据量级,提高查询效率。

背景

其实在实际使用的场景下,经常会遇到一个维度关联的问题,比如将物品的类别带入,用户的画像信息带入等场景。这里简单列举下在clickhouse中做维度补全的操作。主要用到了用户维度数据和物品维度数据两个本地表,基于这两个本地表去生成内存字典,通过内存字典去做关联(字典有很多种存储结构,这里主要列举hashed模式)。

字典处理过程

通过离线导入将数据写入了ods.user_dim_local和ods.item_dim_local两个本地表,然后通过查询dim.user_dim_dis和dim.item_dim_dis两个表提供完整数据(这里只是单机列举案例,集群模式同理)。通过从clickhouse查询数据写入到内存字典中,创建字典的sql如下:

--创建user字典   
CREATE DICTIONARY dim.dict_user_dim on cluster cluster (
 uid UInt64 ,
 platform String default '' ,
 country String default '' ,
 province String default '' ,
 isp String default '' ,
 app_version String default '' ,
 os_version String default '',
 mac String default '' ,
 ip String default '',
 gender String default '',
 age Int16 default -1
) PRIMARY KEY uid 
SOURCE(
  CLICKHOUSE(
    HOST 'localhost' PORT 9000 USER 'default' PASSWORD '' DB 'dim' TABLE 'user_dim_dis'
  )
) LIFETIME(MIN 1800 MAX 3600) LAYOUT(HASHED());

--创建item字典
CREATE DICTIONARY dim.dict_item_dim on cluster cluster (
 item_id UInt64 ,
 type_id UInt32 default 0,
 price UInt32 default 0
) PRIMARY KEY item_id 
SOURCE(
  CLICKHOUSE(
    HOST 'localhost' PORT 9000 USER 'default' PASSWORD '' DB 'dim' TABLE 'item_dim_dis'
  )
) LIFETIME(MIN 1800 MAX 3600) LAYOUT(HASHED())

这里创建字典的语法不做详细介绍,想要更深了解可以参考官方文档。如果使用clickhouse查询分布式表提供字典数据来源,建议Host为一个查询代理,避免对某个节点产生负面效应。DB和table也可以使用view封装一段sql实现。字典的数据是冗余在所有节点的,默认字典的加载方式是惰性加载,也就是需要至少一次查询才能将字典记载到内存,避免一些不使用的字典对集群带来影响。也可以通过hash分片的方式将用户指定到某个shard,那么字典也可以实现通过hash分片的方式存储在每个节点,间接实现分布式字典,减少数据存储,篇幅有限不展开介绍。在创建字典之后,可以有两种模式使用字典,一种是通过dictGet,另外一种方式是通过join,如果只查询一个key建议通过dictGet使用,代码复杂可读性高,同时字典查的value可以作为另一个查询的key,如果查多个key,可以通过dictGet或者join。类似于 select 1 as a,a+1 as b,b+1 as c from system.one这样。

--单value方法1:   
SELECT
    dictGet('dim.dict_user_dim', 'platform', toUInt64(uid)) AS platform,
    uniqCombined(uid) AS uv
FROM dws.action_001_dis
WHERE day = '2021-06-05'
GROUP BY platform

Query id: 52234955-2dc9-4117-9f2a-45ab97249ea7

┌─platform─┬───uv─┐
│ android  │ 9624 │
│ ios      │ 4830 │
└──────────┴──────┘

2 rows in set. Elapsed: 0.009 sec. Processed 49.84 thousand rows, 299.07 KB (5.37 million rows/s., 32.24 MB/s.)

--多value方法1:
SELECT
    dictGet('dim.dict_user_dim', 'platform', toUInt64(uid)) AS platform,
    dictGet('dim.dict_user_dim', 'gender', toUInt64(uid)) AS gender,
    uniqCombined(uid) AS uv
FROM dws.action_001_dis
WHERE day = '2021-06-05'
GROUP BY
    platform,
    gender

Query id: ed255ee5-9036-4385-9a51-35923fef6e48

┌─platform─┬─gender─┬───uv─┐
│ ios      │ 男     │ 2236 │
│ android  │ 女     │ 4340 │
│ android  │ 未知   │  941 │
│ android  │ 男     │ 4361 │
│ ios      │ 女     │ 2161 │
│ ios      │ 未知   │  433 │
└──────────┴────────┴──────┘

6 rows in set. Elapsed: 0.011 sec. Processed 49.84 thousand rows, 299.07 KB (4.70 million rows/s., 28.20 MB/s.)
--单value方法2:
SELECT
    t2.platform AS platform,
    uniqCombined(t1.uid) AS uv
FROM dws.action_001_dis AS t1
INNER JOIN dim.dict_user_dim AS t2 ON toUInt64(t1.uid) = t2.uid
WHERE day = '2021-06-05'
GROUP BY platform

Query id: 8906e637-475e-4386-946e-29e1690f07ea

┌─platform─┬───uv─┐
│ android  │ 9624 │
│ ios      │ 4830 │
└──────────┴──────┘

2 rows in set. Elapsed: 0.011 sec. Processed 49.84 thousand rows, 299.07 KB (4.55 million rows/s., 27.32 MB/s.)

--多value方法2:
SELECT
    t2.platform AS platform,
    t2.gender AS gender,
    uniqCombined(t1.uid) AS uv
FROM dws.action_001_dis AS t1
INNER JOIN dim.dict_user_dim AS t2 ON toUInt64(t1.uid) = t2.uid
WHERE day = '2021-06-05'
GROUP BY
    platform,
    gender

Query id: 88ef55a6-ddcc-42f8-8ce3-5e3bb639b38a

┌─platform─┬─gender─┬───uv─┐
│ ios      │ 男     │ 2236 │
│ android  │ 女     │ 4340 │
│ android  │ 未知   │  941 │
│ android  │ 男     │ 4361 │
│ ios      │ 女     │ 2161 │
│ ios      │ 未知   │  433 │
└──────────┴────────┴──────┘

6 rows in set. Elapsed: 0.015 sec. Processed 49.84 thousand rows, 299.07 KB (3.34 million rows/s., 20.07 MB/s.)

从查询结果来看,dictGet要更快一些,同时在代码可读性上也要更好一些,可以结合场景使用。

业务场景

产品随着分析的不断深入,提了一个新的诉求,希望增加1个维度(通过字典获得),1个指标(这里只是列举下物化视图的维度和指标的添加过程)。维度:gender 指标: 曝光时长中位数

创建过程

因为涉及到新增维度和指标,所以需要对原表进行ddl操作。首先新增维度,新增维度比较麻烦一些,因为不光需要新增字段,还可能需要将新增的字段加到索引里面提高查询效率。操作sql如下:

--新增维度并添加到索引   
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists gender String comment '性别' after item_id,modify order by 
(day,hour,platform,ver,item_id,gender);
alter table dwm.mainpage_stat_mv_local on cluster cluster modify column if exists gender String default '未知' comment '性别' after item_id;
alter table dws.mainpage_stat_mv_dis on cluster cluster add column if not exists gender String comment '性别' after item_id;

--新增指标
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists show_time_median AggregateFunction(medianExact,UInt32) comment '曝光时长中位数';
alter table dws.mainpage_stat_mv_dis on cluster cluster add column if not exists show_time_median AggregateFunction(medianExact,UInt32) comment '曝光时长中位数';

修改物化视图计算逻辑

drop TABLE dwm.mv_main_page_stat_mv_local on cluster cluster;   
CREATE MATERIALIZED VIEW dwm.mv_main_page_stat_mv_local on cluster cluster to dwm.mainpage_stat_mv_local (
day Date comment '数据分区-天'
,hour DateTime comment '数据时间-小时(DateTime)'
,platform String comment '平台 android/ios'
,ver String comment '版本'
,item_id UInt32 comment '物品id'
,gender String  comment '性别'
,shown_uv AggregateFunction(uniqCombined,UInt32) comment '曝光人数'
,shown_cnt SimpleAggregateFunction(sum,UInt64) comment '曝光次数'
,click_uv AggregateFunction(uniqCombined,UInt32) comment '点击人数'
,click_cnt SimpleAggregateFunction(sum,UInt64) comment '点击次数'
,show_time_sum  SimpleAggregateFunction(sum,UInt64) comment '总曝光时间/秒'
,show_time_median AggregateFunction(medianExact,UInt32) comment '曝光时长中位数'
)
AS 
 SELECT day
     ,hour
     ,platform
     ,ver
     ,item_id
     ,dictGet('dim.dict_user_dim', 'gender',toUInt64(uid)) as gender
     ,uniqCombinedStateIf(uid,a.show_cnt>0) as shown_uv
     ,sum(a.show_cnt) as show_cnt
     ,uniqCombinedStateIf(uid,a.click_cnt>0) as click_uv
     ,sum(a.click_cnt) as click_cnt
     ,sum(toUInt64(show_time/1000)) as show_time_sum
     ,medianExactState(toUInt32(show_time/1000)) as show_time_median
from ods.action_001_local as a
group by
      day
     ,hour
     ,platform
     ,ver
     ,item_id
     ,gender

通过这个case主要讲了三个方面,一是外部字典的创建和使用,二是物化视图的增加维度和指标,三物化视图结合字典进行增维。

物化视图的再进阶

本文在创建log的时候创建了2个log,在上面的case中只用到了一个,接下来的case主要讲一个物化视图的进一步用法。

背景

很多时候,我们的日志上报并不是在一个日志中的,比如上文中创建的action_001和action_002,一个是主页物品的曝光和点击,一个是点击进行物品详情的其他行为。这个时候,产品提了一个诉求,希望可以知道曝光到点击,点击到某个更一步的行为的用户转换率。我们最常规的方法是,使用join去将结果关联,这里只是两个log,那么后续有非常多的log,写起join来就会相当麻烦,甚至会有上千行代码去作逻辑处理,效率上也会差很多。所以就衍生了接下来主要讲的用法,基于物化视图实现有限join场景。主要是多个不同日志指标的合并。其实更应该理解为union all max。

可行性分析

物化视图在每批次写入数据之后,后台会按照聚合key进行merge操作,将相同维度的数据的记录聚合在一起,降低数据量,提高查询效率。如果在这一批数据,没有满足条件的列(if组合器)或者并没有写这一指标(指定字段写),那么指标会怎么存,如果下一批数据写入数据,那么这两批数据的这个指标,会怎么样?答案是存可迭代的空数据( 注意这里的不写,存的数据不能理解为null),同时可以和其他批数据进行合并,没有数据的行会被忽略。

举个例子:

CREATE TABLE test.mv_union_max   
(
    `id` UInt32,
    `m1` AggregateFunction(uniqCombined, UInt32),
    `m2` AggregateFunction(sum, UInt32)
)
ENGINE = AggregatingMergeTree
ORDER BY id

Query id: 20dcd6cb-e336-4da8-9033-de42527d2bf0

Ok.

0 rows in set. Elapsed: 0.103 sec.

# 写入数据(这里需要注意指定字段写)
INSERT INTO test.mv_union_max (id, m1) SELECT
    id,
    uniqCombinedState(uid) AS m1
FROM
(
    SELECT
        a1.1 AS id,
        toUInt32(a1.2) AS uid
    FROM system.one
    ARRAY JOIN [(1, 10001), (2, 10002), (3, 10003), (3, 10001)] AS a1
)
GROUP BY id

Query id: f04953f6-3d8a-40a6-bf7e-5b15fe936488

Ok.

0 rows in set. Elapsed: 0.003 sec.

SELECT *
FROM test.mv_union_max

Query id: af592a63-b17d-4764-9a65-4ab33e122d81

┌─id─┬─m1──┬─m2─┐
│  1 │ l��
               │    │
│  2 │ $a6� │    │
│  3 │ ��Gwl��
                 │    │
└────┴─────┴────┘

3 rows in set. Elapsed: 0.002 sec.

在写入m1指标后显示有3条记录,其中m2为空数据(这里需要注意的是,m2不是null),如下:

SELECT isNotNull(m2)   
FROM test.mv_union_max

Query id: b1ac77df-af77-4f2e-9368-2573a7214c99

┌─isNotNull(m2)─┐
│             1 │
│             1 │
│             1 │
└───────────────┘

3 rows in set. Elapsed: 0.002 sec.

SELECT toTypeName(m2)
FROM test.mv_union_max

Query id: fcb15349-4a33-4253-bf64-37f5dc7078ea

┌─toTypeName(m2)─────────────────┐
│ AggregateFunction(sum, UInt32) │
│ AggregateFunction(sum, UInt32) │
│ AggregateFunction(sum, UInt32) │
└────────────────────────────────┘

3 rows in set. Elapsed: 0.002 sec.

这个时候再写入m2指标,不写入m1指标,那么会发生什么情况。

SELECT *   
FROM test.mv_union_max

Query id: 7eaa2d42-c50e-4467-9dca-55a0b5eab579

┌─id─┬─m1──┬─m2─┐
│  1 │ l��
               │    │
│  2 │ $a6� │    │
│  3 │ ��Gwl��
                 │    │
└────┴─────┴────┘
┌─id─┬─m1─┬─m2─┐
│  1 │    │ �   │
│  2 │    │ '  │
│  3 │    │ '  │
└────┴────┴────┘

6 rows in set. Elapsed: 0.003 sec.

存了6条记录,分别上两次写入的数据。在手动触发merge之前先确认下,查询的数据是否是正确的。

SELECT   
    id,
    uniqCombinedMerge(m1) AS m1,
    sumMerge(m2) AS m2
FROM test.mv_union_max
GROUP BY id

Query id: 3f92106a-1b72-4d86-ab74-59c7ac53c202

┌─id─┬─m1─┬────m2─┐
│  3 │  2 │ 10001 │
│  2 │  1 │ 10001 │
│  1 │  1 │  2003 │
└────┴────┴───────┘

3 rows in set. Elapsed: 0.003 sec.

数据完全正确,首先可以确认的是,就算不后台merge,查询数据是完全符合需求的。

OPTIMIZE TABLE test.mv_union_max FINAL   

Query id: 62465025-da30-4df0-a597-18c0c4eb1b2f

Ok.

0 rows in set. Elapsed: 0.001 sec.

cluster-shard1-ck01 :) select * from test.mv_union_max ;

SELECT *
FROM test.mv_union_max

Query id: f7fb359f-3860-4598-b766-812ac2f65755

┌─id─┬─m1──┬─m2─┐
│  1 │ l��
               │ �   │
│  2 │ $a6� │ '  │
│  3 │ ��Gwl��
                 │ '  │
└────┴─────┴────┘

3 rows in set. Elapsed: 0.002 sec.
SELECT
    id,
    uniqCombinedMerge(m1) AS m1,
    sumMerge(m2) AS m2
FROM test.mv_union_max
GROUP BY id

Query id: 2543a145-e540-43dc-8754-101ebb294b5d

┌─id─┬─m1─┬────m2─┐
│  3 │  2 │ 10001 │
│  2 │  1 │ 10001 │
│  1 │  1 │  2003 │
└────┴────┴───────┘

3 rows in set. Elapsed: 0.003 sec.

数据是可以后台merge在一起的。所以说通过这个case能简单了解到实现原理和可行性。通过这种方式就可以避免了两个log之间的查询关联,可以通过一个物化视图存储表组织好维度和指标,查询基于一张宽表实现。众所周知,clickhouse的单表性能非常强,能不join就尽量不join,这个场景可以减少一部分join的场景(维度补全通过字典,如果维度基数特别大,可以借用flink或者redis字典或者高并发接口补全,这里不做细述),便于使用和上层平台的查询规范,另一方面这样也可以减少存储占用,将相同维度的数据尽可能压在一起。

业务场景

随着需求的进一步细化,上报了新的action_002,用来分析用户在进入商品页面后的行为。产品希望可以实现基础指标统计和用户的漏斗分析,(简化一下,对维度没有发生变化)。结合对需求的了解,对原有的物化视图增加了一些指标。这里uv,pv,bitmap3个场景都进行了列举,bitmap也可以实现uv,但是效率上慢一些。新增指标:

指标名指标解释
acta_uv行为A用户数
acta_cnt行为A记录数
actb_uv行为B用户数
actb_cnt行为B记录数
actc_uv行为C用户数
actc_cnt行为C记录数
show_bm曝光Bitmap
click_bm点击Bitmap
acta_bm行为A Bitmap
actb_bm行为B Bitmap
actc_bm行为C Bitmap
actd_bm行为D Bitmap

action_002从生成逻辑上假设了一条用户交互路径。

a->b->c->d

action_001从生成逻辑上假设了一条用户路径。

show->click

但是为了降低代码复杂度 click->a并没有强制关联(主要讲方法,这个细节忽略)。

操作过程

需要对原有物化视图存储表新增上述所有指标,同时对物化视图计算表001新增show_bm、click_bm,物化视图计算表002为新建的计算表,都会写入到最开始建的物化视图存储表中。操作过程如下(sql有些长):

--物化视图存储表新增指标   
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists acta_uv AggregateFunction(uniqCombined,UInt32) comment 'acta_uv';
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists acta_cnt SimpleAggregateFunction(sum,UInt64) comment 'acta_cnt';
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists actb_uv AggregateFunction(uniqCombined,UInt32) comment 'actb_uv';
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists actb_cnt SimpleAggregateFunction(sum,UInt64) comment 'actb_cnt';
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists actc_uv AggregateFunction(uniqCombined,UInt32) comment 'actc_uv';
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists actc_cnt SimpleAggregateFunction(sum,UInt64) comment 'actc_cnt';
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists show_bm AggregateFunction(groupBitmap,UInt32) comment 'show_bm';
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists click_bm AggregateFunction(groupBitmap,UInt32) comment 'click_bm';
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists acta_bm AggregateFunction(groupBitmap,UInt32) comment 'acta_bm';
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists actb_bm AggregateFunction(groupBitmap,UInt32) comment 'actb_bm';
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists actc_bm AggregateFunction(groupBitmap,UInt32) comment 'actc_bm';
alter table dwm.mainpage_stat_mv_local on cluster cluster add column if not exists actd_bm AggregateFunction(groupBitmap,UInt32) comment 'actd_bm';
--物化视图计算表重建 因为medianExact 耗时较大,接下来的case里去掉了。
drop TABLE dwm.mv_main_page_stat_mv_local on cluster cluster;
CREATE MATERIALIZED VIEW dwm.mv_main_page_stat_mv_001_local on cluster cluster to dwm.mainpage_stat_mv_local (
day Date comment '数据分区-天'
,hour DateTime comment '数据时间-小时(DateTime)'
,platform String comment '平台 android/ios'
,ver String comment '版本'
,item_id UInt32 comment '物品id'
,gender String  comment '性别'
,shown_uv AggregateFunction(uniqCombined,UInt32) comment '曝光人数'
,shown_cnt SimpleAggregateFunction(sum,UInt64) comment '曝光次数'
,click_uv AggregateFunction(uniqCombined,UInt32) comment '点击人数'
,click_cnt SimpleAggregateFunction(sum,UInt64) comment '点击次数'
,show_time_sum  SimpleAggregateFunction(sum,UInt64) comment '总曝光时间/秒'
,show_bm AggregateFunction(groupBitmap,UInt32) comment 'show_bm'
,click_bm AggregateFunction(groupBitmap,UInt32) comment 'click_bm'
)
AS 
 SELECT day
     ,hour
     ,platform
     ,ver
     ,item_id
     ,dictGet('dim.dict_user_dim', 'gender',toUInt64(uid)) as gender
     ,uniqCombinedStateIf(uid,a.show_cnt>0) as shown_uv
     ,sum(a.show_cnt) as show_cnt
     ,uniqCombinedStateIf(uid,a.click_cnt>0) as click_uv
     ,sum(a.click_cnt) as click_cnt
     ,sum(toUInt64(show_time/1000)) as show_time_sum
     ,groupBitmapStateIf(uid,a.show_cnt>0) as show_bm
     ,groupBitmapStateIf(uid,a.click_cnt>0) as click_bm
from ods.action_001_local as a
group by
      day
     ,hour
     ,platform
     ,ver
     ,item_id
     ,gender

drop table dwm.mv_main_page_stat_mv_002_local on cluster cluster;
CREATE MATERIALIZED VIEW dwm.mv_main_page_stat_mv_002_local on cluster cluster to dwm.mainpage_stat_mv_local (
day Date comment '数据分区-天'
,hour DateTime comment '数据时间-小时(DateTime)'
,platform String comment '平台 android/ios'
,ver String comment '版本'
,item_id UInt32 comment '物品id'
,gender String  comment '性别'
,acta_uv AggregateFunction(uniqCombined,UInt32) comment 'acta_uv'
,acta_cnt SimpleAggregateFunction(sum,UInt64) comment 'acta_cnt'
,actb_uv AggregateFunction(uniqCombined,UInt32) comment 'actb_uv'
,actb_cnt SimpleAggregateFunction(sum,UInt64) comment 'actb_cnt'
,actc_uv AggregateFunction(uniqCombined,UInt32) comment 'actc_uv'
,actc_cnt SimpleAggregateFunction(sum,UInt64) comment 'actc_cnt'
,acta_bm AggregateFunction(groupBitmap,UInt32) comment 'acta_bm'
,actb_bm AggregateFunction(groupBitmap,UInt32) comment 'actb_bm'
,actc_bm AggregateFunction(groupBitmap,UInt32) comment 'actc_bm'
,actd_bm AggregateFunction(groupBitmap,UInt32) comment 'actd_bm'
)
AS 
 SELECT day
     ,hour
     ,platform
     ,ver
     ,item_id
     ,dictGet('dim.dict_user_dim', 'gender',toUInt64(uid)) as gender
     ,uniqCombinedStateIf(uid,a.action_a_cnt>0) as acta_uv
     ,sum(a.action_a_cnt) as acta_cnt
     ,uniqCombinedStateIf(uid,a.action_b_cnt>0) as actb_uv
     ,sum(a.action_b_cnt) as actb_cnt
     ,uniqCombinedStateIf(uid,a.action_c_cnt>0) as actc_uv
     ,sum(a.action_c_cnt) as actc_cnt
     ,groupBitmapStateIf(uid,a.action_a_cnt>0) as acta_bm
     ,groupBitmapStateIf(uid,a.action_b_cnt>0) as actb_bm
     ,groupBitmapStateIf(uid,a.action_c_cnt>0) as actc_bm
     ,groupBitmapStateIf(uid,a.action_d_sum>0) as actd_bm
from ods.action_002_local as a
group by
      day
     ,hour
     ,platform
     ,ver
     ,item_id
     ,gender

操作完成之后就得到了一个物化视图的指标宽表(假设它很宽)。就可以用它来解决一些查询场景。查询场景1:多个日志指标的合并

SELECT   
    day,
    gender,
    uniqCombinedMerge(shown_uv) AS shown_uv,
    uniqCombinedMerge(click_uv) AS click_uv,
    uniqCombinedMerge(acta_uv) AS acta_uv,
    uniqCombinedMerge(actb_uv) AS actb_uv,
    uniqCombinedMerge(actc_uv) AS actc_uv
FROM dws.mainpage_stat_mv_dis
WHERE day = '2021-06-06'
GROUP BY
    day,
    gender

Query id: 5d4eed47-78f1-4c22-a2cd-66a6a4db14ab

┌────────day─┬─gender─┬─shown_uv─┬─click_uv─┬─acta_uv─┬─actb_uv─┬─actc_uv─┐
│ 2021-06-06 │ 男     │     6845 │     6157 │    6845 │    5824 │    4826 │
│ 2021-06-06 │ 未知   │     1421 │     1277 │    1421 │    1232 │    1029 │
│ 2021-06-06 │ 女     │     6734 │     6058 │    6733 │    5776 │    4826 │
└────────────┴────────┴──────────┴──────────┴─────────┴─────────┴─────────┘

3 rows in set. Elapsed: 0.025 sec. Processed 48.70 thousand rows, 24.23 MB (1.98 million rows/s., 983.52 MB/s.)

--如果使用join的话 这里因为没有分开创建物化视图,只列举语法,所以也不对性能进行对比。
SELECT
    t1.day,
    t1.gender,
    shown_uv,
    click_uv,
    acta_uv,
    actb_uv,
    actc_uv
FROM
(
    SELECT
        day,
        dictGet('dim.dict_user_dim', 'gender', toUInt64(uid)) AS gender,
        uniqCombinedIf(uid, a.show_cnt > 0) AS shown_uv,
        uniqCombinedIf(uid, a.click_cnt > 0) AS click_uv
    FROM dws.action_001_dis AS a
    WHERE day = '2021-06-06'
    GROUP BY
        day,
        gender
) AS t1
LEFT JOIN
(
    SELECT
        day,
        dictGet('dim.dict_user_dim', 'gender', toUInt64(uid)) AS gender,
        uniqCombinedIf(uid, a.action_a_cnt > 0) AS acta_uv,
        uniqCombinedIf(uid, a.action_b_cnt > 0) AS actb_uv,
        uniqCombinedIf(uid, a.action_c_cnt > 0) AS actc_uv
    FROM dws.action_002_dis AS a
    GROUP BY
        day,
        gender
) AS t2 USING (day, gender)

Query id: 2ab32451-e373-4757-9e25-f089aef1e9f4

┌────────day─┬─gender─┬─shown_uv─┬─click_uv─┬─acta_uv─┬─actb_uv─┬─actc_uv─┐
│ 2021-06-06 │ 男     │     6845 │     6209 │    6845 │    5824 │    4826 │
│ 2021-06-06 │ 未知   │     1421 │     1283 │    1421 │    1232 │    1029 │
│ 2021-06-06 │ 女     │     6734 │     6096 │    6733 │    5776 │    4826 │
└────────────┴────────┴──────────┴──────────┴─────────┴─────────┴─────────┘

3 rows in set. Elapsed: 0.032 sec. Processed 360.36 thousand rows, 5.85 MB (11.11 million rows/s., 180.47 MB/s.)

查询场景2:基于bitmap的用户行为分析。

SELECT   
    day,
    gender,
    bitmapCardinality(groupBitmapMergeState(show_bm)) AS shown_uv,
    bitmapAndCardinality(groupBitmapMergeState(show_bm), groupBitmapMergeState(click_bm)) AS show_click_uv,
    bitmapAndCardinality(groupBitmapMergeState(show_bm), bitmapAnd(groupBitmapMergeState(click_bm), groupBitmapMergeState(acta_bm))) AS show_click_a_uv,
    bitmapAndCardinality(groupBitmapMergeState(show_bm), bitmapAnd(bitmapAnd(groupBitmapMergeState(click_bm), groupBitmapMergeState(acta_bm)), groupBitmapMergeState(actb_bm))) AS show_click_ab_uv,
    bitmapAndCardinality(groupBitmapMergeState(show_bm), bitmapAnd(bitmapAnd(bitmapAnd(groupBitmapMergeState(click_bm), groupBitmapMergeState(acta_bm)), groupBitmapMergeState(actb_bm)), groupBitmapMergeState(actc_bm))) AS show_click_abc_uv,
    bitmapAndCardinality(groupBitmapMergeState(show_bm), bitmapAnd(bitmapAnd(bitmapAnd(bitmapAnd(groupBitmapMergeState(click_bm), groupBitmapMergeState(acta_bm)), groupBitmapMergeState(actb_bm)), groupBitmapMergeState(actc_bm)), groupBitmapMergeState(actd_bm))) AS show_click_abcd_uv
FROM dws.mainpage_stat_mv_dis
WHERE day = '2021-06-06'
GROUP BY
    day,
    gender

Query id: b79de70f-6091-4d0a-9a33-12af8f210931

┌────────day─┬─gender─┬─shown_uv─┬─show_click_uv─┬─show_click_a_uv─┬─show_click_ab_uv─┬─show_click_abc_uv─┬─show_click_abcd_uv─┐
│ 2021-06-06 │ 男     │     6845 │          6157 │            6157 │             5244 │              4341 │
  4341 │
│ 2021-06-06 │ 未知   │     1421 │          1277 │            1277 │             1113 │               928 │
   928 │
│ 2021-06-06 │ 女     │     6734 │          6058 │            6057 │             5211 │              4367 │
  4367 │
└────────────┴────────┴──────────┴───────────────┴─────────────────┴──────────────────┴───────────────────┴────────────────────┘

3 rows in set. Elapsed: 0.052 sec. Processed 48.70 thousand rows, 54.89 MB (944.42 thousand rows/s., 1.06 GB/s.)

还有一些其他用法篇幅有限不展开了,大家自由探索。因为bitmap函数只支持同时输入两个bitmap,所以层级越深需要不断进行合并。不过这个也整合到一个指标,会对基于superset这样的上层平台,配置指标时方便许多,不用通过join实现,也不需要非常多的子查询了,从查询性能上,存储上,都是一个很友好的方案。同时不管是多log分开写多个指标,也可以进行合并写在一个指标,都可以很方便的进行指标整合。

总结

物化视图是clickhouse一个非常重要的功能,同时也做了很多优化和函数扩展,虽然在某些情况可能会带来一定的风险(比如增加错误字段导致写入失败等问题),但是也是可以在使用中留意避免的,不能因噎废食。本文主要讲解了

  1. 物化视图的创建、新增维度和指标,聚合函数的使用和一些注意事项;
  2. 物化视图结合字典的使用;
  3. 通过物化视图组合指标宽表。

欢迎大家指出文章中的问题,我会及时修改。感兴趣的可以顺着文章或者下载代码尝试,同时也欢迎交流clickhouse的相关使用经验和案例分享,一起学习,一起进步。

高吞吐、低延迟 Java 应用的 GC 优化实践

$
0
0

“以下信息节选自涤生的翻译内容”

本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和知识点非常有学习和参考意义。

背景

高性能应用构成了现代网络的支柱。LinkedIn 内部有许多高吞吐量服务来满足每秒成千上万的用户请求。为了获得最佳的用户体验,以低延迟响应这些请求是非常重要的。

例如,我们的用户经常使用的产品是 Feed —— 它是一个不断更新的专业活动和内容的列表。Feed 在 LinkedIn 的系统中随处可见,包括公司页面、学校页面以及最重要的主页资讯信息。基础 Feed 数据平台为我们的经济图谱(会员、公司、群组等)中各种实体的更新建立索引,它必须高吞吐低延迟地实现相关的更新。如下图,LinkedIn Feeds 信息展示:
5.jpg
为了将这些高吞吐量、低延迟类型的 Java 应用程序用于生产,开发人员必须确保在应用程序开发周期的每个阶段都保持一致的性能。确定最佳垃圾收集(Garbage Collection, GC)配置对于实现这些指标至关重要。

这篇博文将通过一系列步骤来明确需求并优化 GC,它的目标读者是对使用系统方法进行 GC 优化来实现应用的高吞吐低延迟目标感兴趣的开发人员。在 LinkedIn 构建下一代 Feed 数据平台的过程中,我们总结了该方法。这些方法包括但不限于以下几点:并发标记清除(Concurrent Mark Sweep,CMS(参考[2]) 和 G1(参考 [3]) 垃圾回收器的 CPU 和内存开销、避免长期存活对象导致的持续 GC、优化 GC 线程任务分配提升性能,以及可预测 GC 停顿时间所需的 OS 配置。

优化 GC 的正确时机?

GC 的行为可能会因代码优化以及工作负载的变化而变化。因此,在一个已实施性能优化的接近完成的代码库上进行 GC 优化非常重要。而且在端到端的基本原型上进行初步分析也很有必要,该原型系统使用存根代码并模拟了可代表生产环境的工作负载。这样可以获取该架构延迟和吞吐量的真实边界,进而决定是否进行纵向或横向扩展。

在下一代 Feed 数据平台的原型开发阶段,我们几乎实现了所有端到端的功能,并且模拟了当前生产基础设施提供的查询工作负载。这使我们在工作负载特性上有足够的多样性,可以在足够长的时间内测量应用程序性能和 GC 特征。

优化 GC 的步骤

下面是一些针对高吞吐量、低延迟需求优化 GC 的总体步骤。此外,还包括在 Feed 数据平台原型实施的具体细节。尽管我们还对 G1 垃圾收集器进行了试验,但我们发现 ParNew/CMS 具有最佳的 GC 性能。

1. 理解 GC 基础知识

由于 GC 优化需要调整大量的参数,因此理解 GC 工作机制非常重要。Oracle 的 Hotspot JVM 内存管理白皮书(参考 [4] )是开始学习 Hotspot JVM GC 算法非常好的资料。而了解 G1 垃圾回收器的理论知识,可以参阅(参考 [3])。

2. 仔细考量 GC 需求

为了降低对应用程序性能的开销,可以优化 GC 的一些特征。像吞吐量和延迟一样,这些 GC 特征应该在长时间运行的测试中观察到,以确保应用程序能够在经历多个 GC 周期中处理流量的变化。

  • Stop-the-world 回收器回收垃圾时会暂停应用线程。停顿的时长和频率不应该对应用遵守 SLA 产生不利的影响。
  • 并发 GC 算法与应用线程竞争 CPU 周期。这个开销不应该影响应用吞吐量。
  • 非压缩 GC 算法会引起堆碎片化,进而导致的 Full GC 长时间 Stop-the-world,因此,堆碎片应保持在最小值。
  • 垃圾回收工作需要占用内存。某些 GC 算法具有比其他算法更高的内存占用。如果应用程序需要较大的堆空间,要确保 GC 的内存开销不能太大。
  • 要清楚地了解 GC 日志和常用的 JVM 参数,以便轻松地调整 GC 行为。因为 GC 运行随着代码复杂性增加或工作负载特性的改变而发生变化

我们使用 Linux 操作系统、Hotspot Java7u51、32GB 堆内存、6GB 新生代(Young Gen)和 -XX:CMSInitiatingOccupancyFraction 值为 70(Old GC 触发时其空间占用率)开始实验。设置较大的堆内存是用来维持长期存活对象的对象缓存。一旦这个缓存生效,晋升到 Old Gen 的对象速度会显著下降。

使用最初的 JVM 配置,每 3 秒发生一次 80ms 的 Young GC 停顿,超过 99.9% 的应用请求延迟 100ms(999线)。这样的 GC 效果可能适合于 SLA 对延迟要求不太严格应用。然而,我们的目标是尽可能减少应用请求的 999 线。GC 优化对于实现这一目标至关重要。

3. 理解 GC 指标

衡量应用当前情况始终是优化的先决条件。了解 GC 日志的详细细节(参考 [5])(使用以下选项):

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps 
-XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime复制

可以对该应用的 GC 特征有总体的把握。

在 LinkedIn 的内部监控 inGraphs 和报表系统 Naarad,生成了各种有用的指标可视化图形,比如 GC 停顿时间百分比、一次停顿最大持续时间以及长时间内 GC 频率。除了 Naarad,有很多开源工具比如 gclogviewer 可以从 GC 日志创建可视化图形。在此阶段,可以确定 GC 频率和暂停持续时间是否满足应用程序满足延迟的要求。

4. 降低 GC 频率

在分代 GC 算法中,降低 GC 频率可以通过:(1) 降低对象分配/晋升率;(2) 增加各代空间的大小。

在 Hotspot JVM 中,Young GC 停顿时间取决于一次垃圾回收后存活下来的对象的数量,而不是 Young Gen 自身的大小。增加 Young Gen 大小对于应用性能的影响需要仔细评估:

  • 如果更多的数据存活而且被复制到 Survivor 区域,或者每次 GC 更多的数据晋升到 Old Gen,增加 Young Gen 大小可能导致更长的 Young GC 停顿。较长的 GC 停顿可能会导致应用程序延迟增加和(或)吞吐量降低。
  • 另一方面,如果每次垃圾回收后存活对象数量不会大幅增加,停顿时间可能不会延长。在这种情况下,降低 GC 频率可能会使整个应用总体延迟降低和(或)吞吐量增加。

对于大部分为短期存活对象的应用,仅仅需要控制上述的参数;对于长期存活对象的应用,就需要注意,被晋升的对象可能很长时间都不能被 Old GC 周期回收。如果 Old GC 触发阈值(Old Gen 占用率百分比)比较低,应用将陷入持续的 GC 循环中。可以通过设置高的 GC 触发阈值可避免这一问题。

由于我们的应用在堆中维持了长期存活对象的较大缓存,将 Old GC 触发阈值设置为

-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly复制

来增加触发 Old GC 的阈值。我们也试图增加 Young Gen 大小来减少 Young GC 频率,但是并没有采用,因为这增加了应用的 999 线。

5. 缩短 GC 停顿时间

减少 Young Gen 大小可以缩短 Young GC 停顿时间,因为这可能导致被复制到 Survivor 区域或者被晋升的数据更少。但是,正如前面提到的,我们要观察减少 Young Gen 大小和由此导致的 GC 频率增加对于整体应用吞吐量和延迟的影响。Young GC 停顿时间也依赖于 tenuring threshold (晋升阈值)和 Old Gen 大小(如步骤 6 所示)。

在使用 CMS GC 时,应将因堆碎片或者由堆碎片导致的 Full GC 的停顿时间降低到最小。通过控制对象晋升比例和减小 -XX:CMSInitiatingOccupancyFraction 的值使 Old GC 在低阈值时触发。所有选项的细节调整和他们相关的权衡,请参考 Web Services 的 Java 垃圾回收(参考 [5] )和 Java 垃圾回收精粹(参考 [6])。

我们观察到 Eden 区域的大部分 Young Gen 被回收,几乎没有 3-8 年龄对象在 Survivor 空间中死亡,所以我们将 tenuring threshold 从 8 降低到 2 (使用选项:-XX:MaxTenuringThreshold=2 ),以降低 Young GC 消耗在数据复制上的时间。

我们还注意到 Young GC 暂停时间随着 Old Gen 占用率上升而延长。这意味着来自 Old Gen 的压力使得对象晋升花费更多的时间。为解决这个问题,将总的堆内存大小增加到 40GB,减小 -XX:CMSInitiatingOccupancyFraction 的值到 80,更快地开始 Old GC。尽管 -XX:CMSInitiatingOccupancyFraction 的值减小了,增大堆内存可以避免频繁的 Old GC。在此阶段,我们的结果是 Young GC 暂停 70ms,应用的 999 线在 80ms。

6. 优化 GC 工作线程的任务分配

为了进一步降低 Young GC 停顿时间,我们决定研究 GC 线程绑定任务的参数来进行优化。

-XX:ParGCCardsPerStrideChunk 参数控制 GC 工作线程的任务粒度,可以帮助不使用补丁而获得最佳性能,这个补丁用来优化 Young GC 中的 Card table(卡表)扫描时间(参考[7])。有趣的是,Young GC 时间随着 Old Gen 的增加而延长。将这个选项值设为 32678,Young GC 停顿时间降低到平均 50ms。此时应用的 999 线在 60ms。

还有一些的参数可以将任务映射到 GC 线程,如果操作系统允许的话,-XX:+BindGCTaskThreadsToCPUs 参数可以绑定 GC 线程到个别的 CPU 核(见解释 [1])。使用亲缘性 -XX:+UseGCTaskAffinity 参数可以将任务分配给 GC 工作线程(见解释 [2])。然而,我们的应用并没有从这些选项带来任何好处。实际上,一些调查显示这些选项在 Linux 系统不起作用。

7. 了解 GC 的 CPU 和内存开销

并发 GC 通常会增加 CPU 使用率。虽然我们观察到 CMS 的默认设置运行良好,但是 G1 收集器的并发 GC 工作会导致 CPU 使用率的增加,显著降低了应用程序的吞吐量和延迟。与 CMS 相比,G1 还增加了内存开销。对于不受 CPU 限制的低吞吐量应用程序,GC 导致的高 CPU 使用率可能不是一个紧迫的问题。

下图是 ParNew/CMS 和 G1 的 CPU 使用百分比:相对来说 CPU 使用率变化明显的节点使用 G1 参数 -XX:G1RSetUpdatingPauseTimePercent=20:
6.jpg
下图是 ParNew/CMS 和 G1 每秒服务的请求数:吞吐量较低的节点使用 G1 参数 -XX:G1RSetUpdatingPauseTimePercent=20
7.jpg

8. 为 GC 优化系统内存和 I/O 管理

通常来说,GC 停顿有两种特殊情况:(1) 低 user time,高 sys time 和高 real time (2) 低 user time,低 sys time 和高 real time。这意味着基础的进程/OS设置存在问题。情况 (1) 可能意味着 JVM 页面被 Linux 窃取;情况 (2) 可能意味着 GC 线程被 Linux 用于磁盘刷新,并卡在内核中等待 I/O。在这些情况下,如何设置参数可以参考该 PPT(参考 [8])。

另外,为了避免在运行时造成性能损失,我们可以使用 JVM 选项 -XX:+AlwaysPreTouch 在应用程序启动时先访问所有分配给它的内存,让操作系统把内存真正的分配给 JVM。我们还可以将 vm.swappability 设置为0,这样操作系统就不会交换页面到 swap(除非绝对必要)。

可能你会使用 mlock 将 JVM 页固定到内存中,这样操作系统就不会将它们交换出去。但是,如果系统用尽了所有的内存和交换空间,操作系统将终止一个进程来回收内存。通常情况下,Linux 内核会选择具有高驻留内存占用但运行时间不长的进程(OOM 情况下杀死进程的工作流(参考[9])进行终止。在我们的例子中,这个进程很有可能就是我们的应用程序。优雅的降级是服务优秀的属性之一,不过服务突然终止的可能性对于可操作性来说并不好 —— 因此,我们不使用 mlock,只是通过 vm.swapability 来尽可能避免交换内存页到 swap 的惩罚。

LinkedIn 动态信息数据平台的 GC 优化

对于该 Feed 平台原型系统,我们使用 Hotspot JVM 的两个 GC 算法优化垃圾回收:

Young GC 使用 ParNew,Old GC 使用 CMS。
Young Gen 和 Old Gen 使用 G1。G1 试图解决堆大小为 6GB 或更大时,暂停时间稳定且可预测在 0.5 秒以下的问题。在我们用 G1 实验过程中,尽管调整了各种参数,但没有得到像 ParNew/CMS 一样的 GC 性能或停顿时间的可预测值。我们查询了使用 G1 发生内存泄漏相关的一个 bug(见解释[3]),但还不能确定根本原因。
使用 ParNew/CMS,应用每三秒进行一次 40-60ms 的 Young GC 和每小时一个 CMS GC。JVM 参数如下:

// JVM sizing options
-server -Xms40g -Xmx40g -XX:MaxDirectMemorySize=4096m -XX:PermSize=256m -XX:MaxPermSize=256m   
// Young generation options
-XX:NewSize=6g -XX:MaxNewSize=6g -XX:+UseParNewGC -XX:MaxTenuringThreshold=2 -XX:SurvivorRatio=8 -XX:+UnlockDiagnosticVMOptions -XX:ParGCCardsPerStrideChunk=32768
// Old generation  options
-XX:+UseConcMarkSweepGC -XX:CMSParallelRemarkEnabled -XX:+ParallelRefProcEnabled -XX:+CMSClassUnloadingEnabled  -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly   
// Other options
-XX:+AlwaysPreTouch -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:-OmitStackTraceInFastThrow复制

使用这些参数,对于成千上万读请求的吞吐量,我们应用程序的 999 线降低到 60ms。

解释

[1] -XX:+BindGCTaskThreadsToCPUs 参数似乎在Linux 系统上不起作用,因为 hotspot/src/os/linux/vm/oslinux.cpp 的 distributeprocesses 方法在 JDK7 或 JDK8 中没有实现。

[2] -XX:+UseGCTaskAffinity 参数在 JDK7 和 JDK8 的所有平台似乎都不起作用,因为任务的亲缘性属性永远被设置为 sentinelworker = (uint) -1。源码见 hotspot/src/share/vm/gcimplementation/parallelScavenge/{gcTaskManager.cpp,gcTaskThread.cpp, gcTaskManager.cpp}。

[3] G1 存在一些内存泄露的 bug,可能 Java7u51 没有修改。这个 bug 仅在 Java 8 修正了。

HTTP/2 in Netty

$
0
0

1. Overview

Netty is an NIO-based client-server framework that gives Java developers the power to operate on the network layers. Using this framework, developers can build their own implementation of any known protocol, or even custom protocols.

For a basic understanding of the framework,  introduction to Netty is a good start.

In this tutorial, we'll see how to implement an HTTP/2 server and client in Netty.

2. What Is  HTTP/2?

As the name suggests,  HTTP version 2 or simply HTTP/2, is a newer version of the Hypertext Transfer Protocol.

Around the year 1989, when the internet was born, HTTP/1.0 came into being. In 1997, it was upgraded to version 1.1. However, it wasn't until 2015 that it saw a major upgrade, version 2.

As of writing this,  HTTP/3 is also available, though not yet supported by default by all browsers.

HTTP/2 is still the latest version of the protocol that is widely accepted and implemented. It differs significantly from the previous versions with its multiplexing and server push features, among other things.

Communication in HTTP/2 happens via a group of bytes called frames, and multiple frames form a stream.

In our code samples, we'll see how Netty handles the exchange of  HEADERSDATA and  SETTINGS frames.

3. The Server

Now let's see how we can create an HTTP/2 server in Netty.

3.1.  SslContext

Netty supports  APN negotiation for HTTP/2 over TLS. So, the first thing we need to create a server is an  SslContext:

SelfSignedCertificate ssc = new SelfSignedCertificate();
SslContext sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
  .sslProvider(SslProvider.JDK)
  .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
  .applicationProtocolConfig(
    new ApplicationProtocolConfig(Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE,
      SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2))
  .build();

Here, we created a context for the server with a JDK SSL provider, added a couple of ciphers, and configured the Application-Layer Protocol Negotiation for HTTP/2.

This means that our server will only support HTTP/2 and its underlying  protocol identifier h2.

3.2. Bootstrapping the Server with a  ChannelInitializer

Next, we need a  ChannelInitializer for our multiplexing child channel, so as to set up a Netty pipeline.

We'll use the earlier  sslContext in this channel to initiate the pipeline, and then bootstrap the server:

public final class Http2Server {

    static final int PORT = 8443;

    public static void main(String[] args) throws Exception {
        SslContext sslCtx = // create sslContext as described above
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.option(ChannelOption.SO_BACKLOG, 1024);
            b.group(group)
              .channel(NioServerSocketChannel.class)
              .handler(new LoggingHandler(LogLevel.INFO))
              .childHandler(new ChannelInitializer() {
                  @Override
                  protected void initChannel(SocketChannel ch) throws Exception {
                      if (sslCtx != null) {
                          ch.pipeline()
                            .addLast(sslCtx.newHandler(ch.alloc()), Http2Util.getServerAPNHandler());
                      }
                  }
            });
            Channel ch = b.bind(PORT).sync().channel();

            logger.info("HTTP/2 Server is listening on https://127.0.0.1:" + PORT + '/');

            ch.closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

As part of this channel's initialization, we're adding an APN handler to the pipeline in a utility method  getServerAPNHandler() that we've defined in our own utility class  Http2Util:

public static ApplicationProtocolNegotiationHandler getServerAPNHandler() {
    ApplicationProtocolNegotiationHandler serverAPNHandler = 
      new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) {
        @Override
        protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {
            if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
                ctx.pipeline().addLast(
                  Http2FrameCodecBuilder.forServer().build(), new Http2ServerResponseHandler());
                return;
            }
            throw new IllegalStateException("Protocol: " + protocol + " not supported");
        }
    };
    return serverAPNHandler;
}

This handler is, in turn, adding a Netty provided  Http2FrameCodec using its builder and a custom handler called  Http2ServerResponseHandler.

Our custom handler extends Netty's  ChannelDuplexHandler and acts as both an inbound as well as an outbound handler for the server. Primarily, it prepares the response to be sent to the client.

For the purpose of this tutorial, we'll define a static  Hello World response in an  io.netty.buffer.ByteBuf – the preferred object to read and write bytes in Netty:

static final ByteBuf RESPONSE_BYTES = Unpooled.unreleasableBuffer(
  Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8));

This buffer will be set as a DATA frame in our handler's  channelRead method and written to the  ChannelHandlerContext:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (msg instanceof Http2HeadersFrame) {
        Http2HeadersFrame msgHeader = (Http2HeadersFrame) msg;
        if (msgHeader.isEndStream()) {
            ByteBuf content = ctx.alloc().buffer();
            content.writeBytes(RESPONSE_BYTES.duplicate());

            Http2Headers headers = new DefaultHttp2Headers().status(HttpResponseStatus.OK.codeAsText());
            ctx.write(new DefaultHttp2HeadersFrame(headers).stream(msgHeader.stream()));
            ctx.write(new DefaultHttp2DataFrame(content, true).stream(msgHeader.stream()));
        }
    } else {
        super.channelRead(ctx, msg);
    }
}

And that's it, our server is ready to dish out  Hello World.

For a quick test, start the server and fire a curl command with the  –http2 option:

curl -k -v --http2 https://127.0.0.1:8443

Which will give a response similar to:

> GET / HTTP/2> Host: 127.0.0.1:8443> User-Agent: curl/7.64.1> Accept: */*> 
* Connection state changed (MAX_CONCURRENT_STREAMS == 4294967295)!< HTTP/2 200 < 
* Connection #0 to host 127.0.0.1 left intact
Hello World* Closing connection 0

4. The Client

Next, let's have a look at the client. Of course, its purpose is to send a request and then handle the response obtained from the server.

Our client code will comprise of a couple of handlers, an initializer class to set them up in a pipeline, and finally a JUnit test to bootstrap the client and bring everything together.

4.1.  SslContext

But again, at first, let's see how the client's  SslContext is set up. We'll write this as part of setting up of our client JUnit:

@Before
public void setup() throws Exception {
    SslContext sslCtx = SslContextBuilder.forClient()
      .sslProvider(SslProvider.JDK)
      .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
      .trustManager(InsecureTrustManagerFactory.INSTANCE)
      .applicationProtocolConfig(
        new ApplicationProtocolConfig(Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE,
          SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2))
      .build();
}

As we can see, it's pretty much similar to the server's S slContext, just that we are not providing any  SelfSignedCertificate here. Another difference is that we are adding an  InsecureTrustManagerFactory to trust any certificate without any verification.

Importantly, this trust manager is purely for demo purposes and should not be used in production. To use trusted certificates instead, Netty's  SslContextBuilder offers many alternatives.

We'll come back to this JUnit at the end to bootstrap the client.

4.2. Handlers

For now, let's take a look at the handlers.

First, we'll need a handler we'll call  Http2SettingsHandler, to deal with HTTP/2's SETTINGS frame.  It extends Netty's  SimpleChannelInboundHandler:

public class Http2SettingsHandler extends SimpleChannelInboundHandler<Http2Settings> {
    private final ChannelPromise promise;

    // constructor

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) throws Exception {
        promise.setSuccess();
        ctx.pipeline().remove(this);
    }
}

The class is simply initializing a  ChannelPromise and flagging it as successful.

It also has a utility method  awaitSettings that our client will use in order to wait for the initial handshake completion:

public void awaitSettings(long timeout, TimeUnit unit) throws Exception {
    if (!promise.awaitUninterruptibly(timeout, unit)) {
        throw new IllegalStateException("Timed out waiting for settings");
    }
}

If the channel read does not happen in the stipulated timeout period, then an  IllegalStateException is thrown.

Second, we'll need a handler to deal with the response obtained from the server, we'll name it  Http2ClientResponseHandler:

public class Http2ClientResponseHandler extends SimpleChannelInboundHandler {

    private final Map<Integer, MapValues> streamidMap;

    // constructor
}

This class also extends  SimpleChannelInboundHandler and declares a  streamidMap of  MapValues, an inner class of our  Http2ClientResponseHandler:

public static class MapValues {
    ChannelFuture writeFuture;
    ChannelPromise promise;

    // constructor and getters
}

We added this class to be able to store two values for a given  Integer key.

The handler also has a utility method  put, of course, to put values in the  streamidMap:

public MapValues put(int streamId, ChannelFuture writeFuture, ChannelPromise promise) {
    return streamidMap.put(streamId, new MapValues(writeFuture, promise));
}

Next, let's see what this handler does when the channel is read in the pipeline.

Basically, this is the place where we get the DATA frame or  ByteBuf content from the server as a  FullHttpResponse and can manipulate it in the way we want.

In this example, we'll just log it:

@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
    Integer streamId = msg.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
    if (streamId == null) {
        logger.error("HttpResponseHandler unexpected message received: " + msg);
        return;
    }

    MapValues value = streamidMap.get(streamId);

    if (value == null) {
        logger.error("Message received for unknown stream id " + streamId);
    } else {
        ByteBuf content = msg.content();
        if (content.isReadable()) {
            int contentLength = content.readableBytes();
            byte[] arr = new byte[contentLength];
            content.readBytes(arr);
            logger.info(new String(arr, 0, contentLength, CharsetUtil.UTF_8));
        }

        value.getPromise().setSuccess();
    }
}

At the end of the method, we flag the  ChannelPromise as successful to indicate proper completion.

As the first handler we described, this class also contains a utility method for our client's use. The method makes our event loop wait until the  ChannelPromise is successful. Or, in other words, it waits till the response processing is complete:

public String awaitResponses(long timeout, TimeUnit unit) {
    Iterator<Entry<Integer, MapValues>> itr = streamidMap.entrySet().iterator();        
    String response = null;

    while (itr.hasNext()) {
        Entry<Integer, MapValues> entry = itr.next();
        ChannelFuture writeFuture = entry.getValue().getWriteFuture();

        if (!writeFuture.awaitUninterruptibly(timeout, unit)) {
            throw new IllegalStateException("Timed out waiting to write for stream id " + entry.getKey());
        }
        if (!writeFuture.isSuccess()) {
            throw new RuntimeException(writeFuture.cause());
        }
        ChannelPromise promise = entry.getValue().getPromise();

        if (!promise.awaitUninterruptibly(timeout, unit)) {
            throw new IllegalStateException("Timed out waiting for response on stream id "
              + entry.getKey());
        }
        if (!promise.isSuccess()) {
            throw new RuntimeException(promise.cause());
        }
        logger.info("---Stream id: " + entry.getKey() + " received---");
        response = entry.getValue().getResponse();
        itr.remove();
    }        
    return response;
}

4.3.  Http2ClientInitializer

As we saw in the case of our server, the purpose of a  ChannelInitializer is to set up a pipeline:

public class Http2ClientInitializer extends ChannelInitializer {

    private final SslContext sslCtx;
    private final int maxContentLength;
    private Http2SettingsHandler settingsHandler;
    private Http2ClientResponseHandler responseHandler;
    private String host;
    private int port;

    // constructor

    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        settingsHandler = new Http2SettingsHandler(ch.newPromise());
        responseHandler = new Http2ClientResponseHandler();
        
        if (sslCtx != null) {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast(sslCtx.newHandler(ch.alloc(), host, port));
            pipeline.addLast(Http2Util.getClientAPNHandler(maxContentLength, 
              settingsHandler, responseHandler));
        }
    }
    // getters
}

In this case, we are initiating the pipeline with a new  SslHandler to add the  TLS SNI Extension at the start of the handshaking process.

Then, it's the responsibility of the  ApplicationProtocolNegotiationHandler to line up a connection handler and our custom handlers in the pipeline:

public static ApplicationProtocolNegotiationHandler getClientAPNHandler(
  int maxContentLength, Http2SettingsHandler settingsHandler, Http2ClientResponseHandler responseHandler) {
    final Http2FrameLogger logger = new Http2FrameLogger(INFO, Http2ClientInitializer.class);
    final Http2Connection connection = new DefaultHttp2Connection(false);

    HttpToHttp2ConnectionHandler connectionHandler = 
      new HttpToHttp2ConnectionHandlerBuilder().frameListener(
        new DelegatingDecompressorFrameListener(connection, 
          new InboundHttp2ToHttpAdapterBuilder(connection)
            .maxContentLength(maxContentLength)
            .propagateSettings(true)
            .build()))
          .frameLogger(logger)
          .connection(connection)
          .build();

    ApplicationProtocolNegotiationHandler clientAPNHandler = 
      new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) {
        @Override
        protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
            if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
                ChannelPipeline p = ctx.pipeline();
                p.addLast(connectionHandler);
                p.addLast(settingsHandler, responseHandler);
                return;
            }
            ctx.close();
            throw new IllegalStateException("Protocol: " + protocol + " not supported");
        }
    };
    return clientAPNHandler;
}

Now all that is left to do is to bootstrap the client and send across a request.

4.4. Bootstrapping the Client

Bootstrapping of the client is similar to that of the server up to a point. After that, we need to add a little bit more functionality to handle sending the request and receiving the response.

As mentioned previously, we'll write this as a JUnit test:

@Test
public void whenRequestSent_thenHelloWorldReceived() throws Exception {

    EventLoopGroup workerGroup = new NioEventLoopGroup();
    Http2ClientInitializer initializer = new Http2ClientInitializer(sslCtx, Integer.MAX_VALUE, HOST, PORT);

    try {
        Bootstrap b = new Bootstrap();
        b.group(workerGroup);
        b.channel(NioSocketChannel.class);
        b.option(ChannelOption.SO_KEEPALIVE, true);
        b.remoteAddress(HOST, PORT);
        b.handler(initializer);

        channel = b.connect().syncUninterruptibly().channel();

        logger.info("Connected to [" + HOST + ':' + PORT + ']');

        Http2SettingsHandler http2SettingsHandler = initializer.getSettingsHandler();
        http2SettingsHandler.awaitSettings(60, TimeUnit.SECONDS);
  
        logger.info("Sending request(s)...");

        FullHttpRequest request = Http2Util.createGetRequest(HOST, PORT);

        Http2ClientResponseHandler responseHandler = initializer.getResponseHandler();
        int streamId = 3;

        responseHandler.put(streamId, channel.write(request), channel.newPromise());
        channel.flush();
 
        String response = responseHandler.awaitResponses(60, TimeUnit.SECONDS);

        assertEquals("Hello World", response);

        logger.info("Finished HTTP/2 request(s)");
    } finally {
        workerGroup.shutdownGracefully();
    }
}

Notably, these are the extra steps we took with respect to the server bootstrap:

  • First, we waited for the initial handshake, making use of  Http2SettingsHandler‘s  awaitSettings method
  • Second, we created the request as a  FullHttpRequest
  • Third, we put the  streamId in our  Http2ClientResponseHandler‘s  streamIdMap, and called its  awaitResponses method
  • And at last, we verified that  Hello World is indeed obtained in the response

In a nutshell, here's what happened – the client sent a HEADERS frame, initial SSL handshake took place, and the server sent the response in a HEADERS and a DATA frame.

5. Conclusion

In this tutorial, we saw how to implement an HTTP/2 server and client in Netty using code samples to get a  Hello World response using HTTP/2 frames.

We hope to see a lot more improvements in Netty API for handling HTTP/2 frames in the future, as it is still being worked upon.

As always, source code is available  over on GitHub.

weex的工作原理

$
0
0

18年的时候,公司有一段时间在推行weex技术栈,我们这边刚好有一个项目,于是有幸体验了一把weex开发。今天想把之前整理&总结一些的关于weex内容输出成文,同时回顾一下weex的工作原理。btw,白天上班,晚上写文,保持每日一篇真的好难🤦

weex介绍

Weex是一套支持跨平台、动态更新的使用Javascript进行原生APP开发的解决方案。 Weex的终极目标是带来iOS端、Android端和H5端一致的开发体验与代码复用。

当然了,到目前为止,weex离她的终极目标还是有一定的距离,那当然是祝福她早日实现目标了,但是她解决了快速发版,提高性能,统一三端三个难点。

Weex实现了统一的JSEngine和DOM API,因此并不完全限定在其上层使用的JS框架,理论上Weex允许在其上层使用Vue、React和Angular,我司用的是上层框架用的是Vue。

weex工作原理

1. 将weex源码生成JS Bundle

Weex首先将编写的Weex源码,也就是后缀名为.we的文件,由template、style 和 script等标签组织好的内容,通过transformer(转换器,weex-toolkit提供的工具)转换成JS Bundle。

这个过程分为三步:

  1. 把template中的内容转换为类JSON的树状数据结构, 转换数据绑定为返回数据的函数原型
  2. 把style转换为类JSON的树状数据结构
  3. 把上面两部分的内容和script中的内容结合成一个JavaScript AMD模块(AMD:异步模块规范)

除此之外,转换器还会做一些额外的事情:合并Bundle,添加引导函数,配置外部数据等等。当前大部分Weex工具最终输出的JSBundle格式都经过了Webpack的二次处理,所以实际使用工具输出的JS Bundle会有不同。

2. 服务端部署JS Bundle

将JS Bundle部署在服务器,当接收到终端(Web端、iOS端或Android端)的JS Bundle请求,将JS Bundle下发给终端。客户端从服务器更新包后即可在下次启动执行新的版本,而无需重新下载 app,因为运行依赖的WeexSDK 已经存在于客户端了,除非新包依赖于新的 SDK。

3. WEEX SDK初始化

JS Framework 以及 Vue 和 Rax 的代码都是内置在了 Weex SDK 里的,随着 Weex SDK一起初始化。Weex SDK的初始化一般在App启动时就已经完成了,只会执行一次。

Weex SDK初始化主要包含以下操作:

  1. 初始化 JS 引擎,准备好 JS 执行环境,向其中注册一些变量和接口,如 WXEnvironment、callNative。
  2. 执行 JS Framework 的代码。
  3. 注册原生组件和原生模块。

针对第二步,执行 JS Framework 的代码的过程又可以分成如下几个步骤:

  1. 注册上层 DSL 框架,如 Vue 和 Rax。这个过程只是告诉 JS Framework 有哪些 DSL 可用,适配它们提供的接口,如init、createInstance,但是不会执行前端框架里的逻辑。
  2. 初始化环境变量,并且会将原生对象的原型链冻结,此时也会注册内置的 JS Service,如 BroadcastChannel。
  3. 如果 DSL 框架里实现了 init 接口,会在此时调用。
  4. 向全局环境中注入可供客户端调用的接口,如callJS、createInstance、registerComponents,调用这些接口会同时触发 DSL 中相应的接口。

下面详细介绍一下JS Framework。

JS Framework 的功能

Weex 是一个既支持多个前端框架又能跨平台渲染的框架,JS Framework 介于前端框架和原生渲染引擎之间,处于承上启下的位置,也是跨框架跨平台的关键。无论你使用的是 Vue 还是 Rax,无论是渲染在 Android 还是 iOS,JS Framework 的代码都会运行到(如果是在浏览器和 WebView 里运行,则不依赖 JS Framework)。

1. 适配前端框架

前端框架在 Weex 和浏览器中的执行过程不一样,这个应该不难理解。如何让一个前端框架运行在 Weex 平台上,是 JS Framework 的一个关键功能。

以 Vue.js 为例,在浏览器上运行一个页面大概分这么几个步骤:首先要准备好页面容器,可以是浏览器或者是 WebView,容器里提供了标准的 Web API。然后给页面容器传入一个地址,通过这个地址最终获取到一个 HTML 文件,然后解析这个 HTML 文件,加载并执行其中的脚本。想要正确的渲染,应该首先加载执行 Vue.js 框架的代码,向浏览器环境中添加 Vue 这个变量,然后创建好挂载点的 DOM 元素,最后执行页面代码,从入口组件开始,层层渲染好再挂载到配置的挂载点上去。

在 Weex 里的执行过程也比较类似,不过 Weex 页面对应的是一个 js 文件,不是 HTML 文件,而且不需要自行引入 Vue.js 框架的代码,也不需要设置挂载点。过程大概是这样的:首先初始化好 Weex 容器,这个过程中会初始化 JS Framework,Vue.js 的代码也包含在了其中。然后给 Weex 容器传入页面地址,通过这个地址最终获取到一个 js 文件,客户端会调用 createInstance 来创建页面,也提供了刷新页面和销毁页面的接口。大致的渲染行为和浏览器一致,但是和浏览器的调用方式不一样,前端框架中至少要适配客户端打开页面、销毁页面(push、pop)的行为才可以在 Weex 中运行。

在 JS Framework 里提供了如上图所示的接口来实现前端框架的对接。图左侧的四个接口与页面功能有关,分别用于获取页面节点、监听客户端的任务、注册组件、注册模块,目前这些功能都已经转移到 JS Framework 内部,在前端框架里都是可选的,有特殊处理逻辑时才需要实现。图右侧的四个接口与页面的生命周期有关,分别会在页面初始化、创建、刷新、销毁时调用,其中只有 createInstance 是必须提供的,其他也都是可选的(在新的 Sandbox 方案中,createInstance 已经改成了 createInstanceContext)。

2. 构建渲染指令树

在浏览器上它们都使用一致的 DOM API 把 Virtual DOM 转换成真实的 HTMLElement。在 Weex 里的逻辑也是类似的,只是在最后一步生成真实元素的过程中,不使用原生 DOM API,而是使用 JS Framework 里定义的一套 Weex DOM API 将操作转化成渲染指令发给客户端。

JS Framework 提供的 Weex DOM API 和浏览器提供的 DOM API 功能基本一致,在 Vue 和 Rax 内部对这些接口都做了适配,针对 Weex 和浏览器平台调用不同的接口就可以实现跨平台渲染。

3. JS-Native 通信

在开发页面过程中,除了节点的渲染以外,还有原生模块的调用、事件绑定、回调等功能,这些功能都依赖于 js 和 native 之间的通信来实现。

首先,页面的 js 代码是运行在 js 线程上的,然而原生组件的绘制、事件的捕获都发生在 UI 线程。在这两个线程之间的通信用的是 callNative 和 callJS 这两个底层接口(现在已经扩展到了很多个),它们默认都是异步的,在 JS Framework 和原生渲染器内部都基于这两个方法做了各种封装。

callNative 是由客户端向 JS 执行环境中注入的接口,提供给 JS Framework 调用,界面的节点(上文提到的渲染指令树)、模块调用的方法和参数都是通过这个接口发送给客户端的。为了减少调用接口时的开销,其实现在已经开了更多更直接的通信接口,其中有些接口还支持同步调用(支持返回值),它们在原理上都和 callNative 是一样的。

callJS 是由 JS Framework 实现的,并且也注入到了执行环境中,提供给客户端调用。事件的派发、模块的回调函数都是通过这个接口通知到 JS Framework,然后再将其传递给上层前端框架。

4. JS Service

Weex 是一个多页面的框架,每个页面的 js bundle 都在一个独立的环境里运行,不同的 Weex 页面对应到浏览器上就相当于不同的“标签页”,普通的 js 库没办法实现在多个页面之间实现状态共享,也很难实现跨页通信。

JS Framework 中实现了 JS Service 的功能,主要就是用来解决跨页面复用和状态共享的问题的,例如 BroadcastChannel 就是基于 JS Service 实现的,它可以在多个 Weex 页面之间通信

5. 准备环境接口

由于 Weex 运行环境和浏览器环境有很大差异,在 JS Framework 里还对一些环境变量做了封装,主要是为了解决解决原生环境里的兼容问题,底层使用渲染引擎提供的接口。主要的改动点是:

  • console: 原生提供了 nativeLog 接口,将其封装成前端熟悉的 console.xxx 并可以控制日志的输出级别。
  • timer: 原生环境里 timer 接口不全,名称和参数不一致。目前来看有了原生 C/C++ 实现的 timer 后,这一层可以移除。
  • freeze: 冻结当前环境里全局变量的原型链(如 Array.prototype)。

另外还有一些 ployfill:Promise 、Arary.from 、Object.assign 、Object.setPrototypeOf 等。

4. 执行JS Bundle

在初始化好 Weex SDK 之后,就可以开始渲染页面了。通常 Weex 的一个页面对应了一个 js bundle 文件,页面的渲染过程也是加载并执行 js bundle 的过程。

首先是调用原生渲染引擎里提供的接口来加载执行 js bundle,在 Android 上是 renderByUrl,在 iOS 上是 renderWithURL。在得到了 js bundle 的代码之后,会继续执行 SDK 里的原生 createInstance 方法,给当前页面生成一个唯一 id,并且把代码和一些配置项传递给 JS Framework 提供的 createInstance 方法。

在 JS Framework 接收到页面代码之后,会判断其中使用的 DSL 的类型(Vue 或者 Rax),然后找到相应的框架,执行 createInstanceContext 创建页面所需要的环境变量。

在旧的方案中,JS Framework 会调用 runInContex 函数在特定的环境中执行 js 代码,内部基于 new Function 实现。在新的 Sandbox 方案中,js bundle 的代码不再发给 JS Framework,也不再使用 new Function,而是由客户端直接执行 js 代码。

创建 weex 实例

当WEEX SDK获取到JS Bundle后,第一时间并不是立马渲染页面,而是先创建WEEX的实例。

每一个JS bundle对应一个实例,同时每一个实例都有一个instance id。

由于所有的js bundle都是放入到同一个JS执行引擎中执行,那么当js执行引擎通过WXBridge将相关渲染指令传出的时候,需要通过instance id才能知道该指定要传递给哪个weex实例。

在创建实例完成后,接下来才是真正将js bundle交给js执行引擎执行。

weex渲染流程

Weex 里页面的渲染过程和浏览器的渲染过程类似,整体可以分为【创建前端组件】-> 【构建 Virtual DOM】->【生成“真实” DOM】->【发送渲染指令】->【绘制原生 UI】这五个步骤。前两个步骤发生在前端框架中,第三和第四个步骤在 JS Framework 中处理,最后一步是由原生渲染引擎实现的。 页面渲染的大致流程如下:

创建前端组件

Vue 框架在执行渲染前,会先根据开发时编写的模板创建相应的组件实例,可以称为 Vue Component,它包含了组件的内部数据、生命周期以及 render 函数等。如果给同一个模板传入多条数据,就会生成多个组件实例,渲染时会创建多个 Vue Component 的实例,每个组件实例的内部状态是不一样的。

构建 Virtual DOM

Vue Component 的渲染过程,可以简单理解为组件实例执行 render 函数生成 VNode 节点树的过程,也就是构建 Virtual DOM 的生成过程。

生成“真实” DOM

上面两个过程,在Weex 和浏览器里都是完全一样的,从生成真实 DOM 这一步开始,Weex 使用了不同的渲染方式。JS Framework 中提供了和 DOM 接口类似的 Weex DOM API,在 Vue 里会使用这些接口将 VNode 渲染生成适用于 Weex 平台的 Element 对象,和 DOM 很像,但并不是“真实”的 DOM。在 Vue 和 Rax 内部对这些接口都做了适配,针对 Weex 和浏览器平台调用不同的接口就可以实现跨平台渲染。

发送渲染指令

在 JS Framework 内部和客户端渲染引擎约定了一系列的指令接口,对应了一个元素的 DOM 操作,如 addElement removeElement updateAttrs updateStyle 等。JS Framework 使用这些接口将自己内部构建的 Element 节点树以渲染指令的形式发给客户端。

绘制原生 UI

客户端接收 JS Framework 发送的渲染指令,创建相应的原生组件,最终调用系统提供的接口绘制原生 UI。

同样的一份JSON 数据,在不同平台的渲染引擎中能够渲染成不同版本的 UI,这是 Weex 可以实现动态化的原因。

事件的响应过程

无论是在浏览器还是 Weex 里,事件都是由原生 UI 捕获的,然而事件处理函数都是写在前端里的,所以会有一个传递的过程。

如上图所示,如果在 Vue.js 里某个标签上绑定了事件,会在内部执行 addEventListener 给节点绑定事件,这个接口在 Weex 平台下调用的是 JS Framework 提供的 addEvent 方法向元素上添加事件,传递了事件类型和处理函数。JS Framework 不会立即向客户端发送添加事件的指令,而是把事件类型和处理函数记录下来,节点构建好以后再一起发给客户端,发送的节点中只包含了事件类型,不含事件处理函数。客户端在渲染节点时,如果发现节点上包含事件,就监听原生 UI 上的指定事件。

当原生 UI 监听到用户触发的事件以后,会派发 fireEvent 命令把节点的 ref、事件类型以及事件对象发给 JS Framework。JS Framework 根据 ref 和事件类型找到相应的事件处理函数,并且以事件对象 event 为参数执行事件处理函数。目前 Weex 里的事件模型相对比较简单,并不区分捕获阶段和冒泡阶段,而是只派发给触发了事件的节点,并不向上冒泡,类似 DOM 模型里 level 0 级别的事件。

上述过程里,事件只会绑定一次,但是很可能会触发多次,例如 touchmove 事件,在手指移动过程中,每秒可能会派发几十次,每次事件都对应了一次 fireEvent -> invokeHandler 的处理过程,很容易损伤性能,浏览器也是如此。针对这种情况,可以使用用 expression binding 来将事件处理函数转成表达式,在绑定事件时一起发给客户端,这样客户端在监听到原生事件以后可以直接解析并执行绑定的表达式,而不需要把事件再派发给前端。

以上就是weex的基本工作原理了,下面看下weex的应用。

weex的三种工作模式

1. 全页模式

目前支持单页使用或整个App使用weex开发(还不完善,需要开发Router和生命周期管理),这个可以类比React Native。

2. Native Component模式

把weex当作一个IOS/Android组件来使用,类比ImageView。但是局部动态化需求旺盛会导致频繁发版。

3. H5 Component模式

在H5中使用weex,类比WMC。在现有的H5页面上做微调,引入Native解决长列表内存暴增、滚动不流畅、动画/手势体验差等问题。

weex与H5 Hybird比较

从前,实现一个需求,需要三种程序员(iOS,android,前端)写三份代码,这就带来了很大的开发成本,所以业界一直在探索跨平台技术方案。从之前的Hybrid,到现在的Weex,React Native,这些方案的根本目的都是一套代码,多端运行。

H5 Hybrid方案的本质是利用客户端APP的内置浏览器功能(即webview),通过JSBridge实现客户端Native和前端JS的通信问题,然后开发H5页面内嵌于APP中,该方案提升了开发效率,也满足了跨端的需求,但是有一个问题就是,前端H5的性能和客户端的性能相差甚远。

而weex采取H5页面的开发方式,同时在终端的运行体验不输Native App。weex利用Native的能力去做了部分浏览器去做的工作。

weex的优势

参考: 详细介绍 Weex 的 JS Framework

SpringBoot-Metrics监控

$
0
0


1.介绍

Metrics基本上是成熟公司里面必须做的一件事情,简单点来说就是对应用的监控,之前在一些技术不成熟的公司其实是不了解这种概念,因为业务跟技术是相关的
当业务庞大起来,技术也会相对复杂起来,对这些复杂的系统进行监控就存在必要性了,特别是在soa化的系统中,完整一个软件的功能分布在各个系统中,针对这些功能进行监控就更必要了
而Spring Boot Actuator 提供了metrics service,让监控变得统一化了,方便管理

2.快速开始

核心是 spring-boot-starter-actuator这个依赖,增加这个依赖之后SpringBoot就会默认配置一些通用的监控,比如jvm监控、类加载、http监控,
当然这些都是一些简单的监控,理论上来说大部分若要在生产中使用,还是需要定制化一下的。先来看一下pom

pom.xml

<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>1.4.1.RELEASE</version></parent><modelVersion>4.0.0</modelVersion><artifactId>springboot-10</artifactId><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jetty</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>1.4.1.RELEASE</version><configuration><fork>true</fork></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>1.8</source><target>1.8</target></configuration></plugin></plugins></build></project>

启动类

packagecom.start;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;/**
     * ClassName: AppApplication
     * Description:
     *
     *@authorkang.wang03
     *         Date 2016/11/8
     */@SpringBootApplicationpublicclassAppApplication{publicstaticvoidmain(String[] args)throwsException {
            SpringApplication.run(AppApplication.class, args);
        }

    }

上述配置完成以后运行程序,你会发觉打印日志的时候多了很多mapping的日志

org.springframework.web.servlet.resource.ResourceHttpRequestHandler]2016-11-2115:48:53.733INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/heapdump || /heapdump.json],methods=[GET],produces=[application/octet-stream]}"ontopublicvoidorg.springframework.boot.actuate.endpoint.mvc.HeapdumpMvcEndpoint.invoke(boolean,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse) throws java.io.IOException,javax.servlet.ServletException2016-11-2115:48:53.734INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/health || /health.json],produces=[application/json]}"ontopublicjava.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(java.security.Principal)2016-11-2115:48:53.739INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/info || /info.json],methods=[GET],produces=[application/json]}"ontopublicjava.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()2016-11-2115:48:53.740INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/beans || /beans.json],methods=[GET],produces=[application/json]}"ontopublicjava.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()2016-11-2115:48:53.742INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/env/{name:.*}],methods=[GET],produces=[application/json]}"ontopublicjava.lang.Object org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint.value(java.lang.String)2016-11-2115:48:53.742INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/env || /env.json],methods=[GET],produces=[application/json]}"ontopublicjava.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()2016-11-2115:48:53.743INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/dump || /dump.json],methods=[GET],produces=[application/json]}"ontopublicjava.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()2016-11-2115:48:53.745INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/mappings || /mappings.json],methods=[GET],produces=[application/json]}"ontopublicjava.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()2016-11-2115:48:53.746INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/trace || /trace.json],methods=[GET],produces=[application/json]}"ontopublicjava.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()2016-11-2115:48:53.749INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/autoconfig || /autoconfig.json],methods=[GET],produces=[application/json]}"ontopublicjava.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()2016-11-2115:48:53.752INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/configprops || /configprops.json],methods=[GET],produces=[application/json]}"ontopublicjava.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()2016-11-2115:48:53.754INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/metrics/{name:.*}],methods=[GET],produces=[application/json]}"ontopublicjava.lang.Object org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint.value(java.lang.String)2016-11-2115:48:53.754INFO3883--- [           main] o.s.b.a.e.mvc.EndpointHandlerMapping     : Mapped"{[/metrics || /metrics.json],methods=[GET],

2.1 /heapdump

/heapdump这个主要会dump目前堆的状况出来,可以用jvm的工具大家,并查看目前堆的状况,但是总的来说没人会这么干,会造成系统的短暂停止,大流量容易压垮系统

2.2 /health

/health这个路径主要是用来统计系统的状况,默认里面目前只有系统状况和磁盘状况

{"status":"UP","diskSpace":{"status":"UP","total":120108089344,"free":20677521408,"threshold":10485760}}

2.3 /info

/info

2.4 /beans

/beans可以查看到目前Spring里面加载的所有bean,在生产中感觉没有什么用处,可能在开发中会有一些帮助,方便查看bean是否被扫描

2.5 /env

/env里面包含了目前的环境变量,包括 application.yaml配置的属性,以及systemProperties都能够拿到

2.6 /dump

/dump里面包含了目前线程的一个快照信息

2.7 /mappings

/mappings里面包含了Controller的所有mapping信息,开发中新手经常会遇到访问不到controller的情况,可以根据这个查看是否被扫描

2.8 /trace

/tracetrace目前主要是监控http请求的,监控每个请求的状况,如下所示:

[
    {"timestamp":1479717912091,"info":{"method":"GET","path":"/","headers":{"request":{"Connection":"keep-alive","User-Agent":"GoogleSoftwareUpdateAgent/1.2.6.1370 CFNetwork/807.0.4 Darwin/16.0.0 (x86_64)","Host":"127.0.0.1:8080"},"response":{"X-Application-Context":"application","Date":"Mon, 21 Nov 2016 08:45:12 GMT","Content-Type":"application/json;charset=UTF-8","status":"404"}}}},
    {"timestamp":1479717910980,"info":{"method":"GET","path":"/","headers":{"request":{"Connection":"keep-alive","User-Agent":"GoogleSoftwareUpdate/1.2.6.1370 CFNetwork/807.0.4 Darwin/16.0.0 (x86_64)","Host":"127.0.0.1:8080"},"response":{"X-Application-Context":"application","Date":"Mon, 21 Nov 2016 08:45:10 GMT","Content-Type":"application/json;charset=UTF-8","status":"404"}}}},
    {"timestamp":1479717908924,"info":{"method":"GET","path":"/","headers":{"request":{"Connection":"keep-alive","User-Agent":"GoogleSoftwareUpdate/1.2.6.1370 CFNetwork/807.0.4 Darwin/16.0.0 (x86_64)","Host":"127.0.0.1:8080"},"response":{"X-Application-Context":"application","Date":"Mon, 21 Nov 2016 08:45:08 GMT","Content-Type":"application/json;charset=UTF-8","status":"404"}}}},
    {"timestamp":1479716651440,"info":{"method":"GET","path":"/trace","headers":{"request":{"Cache-Control":"max-age=0","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8","Upgrade-Insecure-Requests":"1","Connection":"keep-alive","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36","Host":"localhost:8080","Accept-Encoding":"gzip, deflate, sdch, br","Accept-Language":"zh-CN,zh;q=0.8,en;q=0.6"},"response":{"X-Application-Context":"application","Date":"Mon, 21 Nov 2016 08:24:11 GMT","Content-Type":"application/json;charset=UTF-8","status":"200"}}}}
]

2.9 /autoconfig

/autoconfig显示当前SpringBoot,已经自动配置的的属性

2.10 /metrics

/metrics 是整个监控里面的核心信息,

{"mem":323984,"mem.free":239989,"processors":4,"instance.uptime":1670490,"uptime":1691378,"systemload.average":4.22265625,"heap.committed":271872,"heap.init":131072,"heap.used":31882,"heap":1864192,"nonheap.committed":54056,"nonheap.init":2496,"nonheap.used":52113,"nonheap":0,"threads.peak":15,"threads.daemon":5,"threads.totalStarted":17,"threads":15,"classes":6662,"classes.loaded":6662,"classes.unloaded":0,"gc.ps_scavenge.count":10,"gc.ps_scavenge.time":153,"gc.ps_marksweep.count":2,"gc.ps_marksweep.time":150,"gauge.response.trace":19,"gauge.response.autoconfig":41,"gauge.response.error":6,"gauge.response.configprops":138,"counter.status.200.configprops":1,"counter.status.404.error":3,"counter.status.200.autoconfig":1,"counter.status.200.trace":2}

目前来说包含了如下信息

The total system memoryinKB (mem)
    The amount of free memoryinKB (mem.free)
    The number of processors (processors)
    The system uptimeinmilliseconds (uptime)
    The application context uptimeinmilliseconds (instance.uptime)
    The average system load (systemload.average)
    Heap informationinKB (heap, heap.committed, heap.init, heap.used)
    Thread information (threads, thread.peak, thread.daemon)
    Class load information (classes, classes.loaded, classes.unloaded)
    Garbage collection information (gc.xxx.count, gc.xxx.time)

3 总结

这些是SpringBoot提供的一些比较简单的Metrics,其实在生产中可以借鉴一些,但是大多数基本都用不上,SpringBoot针对DataSource和Cache都做了Metrics,这个在之后的中级篇会做更进一步的讲解


中国“名校俱乐部”毕业生流向:首选广东上海,教育行业火爆,金融持续“降温”

$
0
0

高考结束,下一件大事就是填志愿。

六月,高考择校正在进行。各大高校的毕业生就业去向成为重要指标,其中“九校联盟” (以下简称为“C9”)的毕业生就业表现无疑将被纳入重点考量范围。

作为国内首个顶尖高校联盟,组成C9的九所高校均为国家首批985工程重点建设高校,包括北京大学、清华大学、复旦大学、上海交通大学、南京大学、浙江大学、中国科学技术大学、哈尔滨工业大学以及西安交通大学。

哪些城市仍具潜力,哪些行业前景明朗?在总人口受教育程度不断增高的情况下,继续深造和加入就业大军,C9毕业生的选择又如何?

21世纪经济研究院梳理了C9高校最新毕业生就业质量报告(除北京大学、哈尔滨工业大学使用2019年报告外,其余7座高校均为2020年报告数据),试图为即将面临选择专业的考生和家长提供一些线索和参考。

哈工大、西安交大本地就业回流显著

志愿填报,究竟是选大学还是选城市?

一方面,大学所在城市的重要性不言而喻,甚至关系到学生的未来择业。另一方面,在城市的人才竞争中,以C9高校为代表的毕业生也是各地争抢的目标群体。

注:哈工大本部统计口径不包括博士,其余高校人数包括本硕博。

数据来源:C9就业质量报告 21世纪经济研究院统计

统计显示,近两年,广东和上海并驾齐驱,已成为C9毕业生首选的就业城市。上海在毕业生心中仍是 “最能留住人”“来了就不想走”的城市,复旦大学和上海交通大学毕业生留沪比例分别高达66%和72%。

而选择北京的意愿似乎正在C9毕业生心中悄然降低,除北大清华占了属地优势依然榜上有名外,北京仅出现在西安交大TOP3榜单中的第三名。这一点,从清华大学2020届毕业生超半数选择在京外就业也可见端倪。清华大学2020届毕业生就业质量报告显示,本校赴北京以外就业的毕业生比例已连续八年超过五成。

人才流失一直是东北地区面临的问题。2018年,哈工大留在黑龙江的本硕毕业生占比仅为9.20%,且多以本科生为主,远低于其他8所高校。由于考生报考哈尔滨工业大学深圳校区的分数高于本部,使得哈工大成为“选学校还是选城市”这个话题的典型案例。不少人认为,正是地理位置限制了本部的自身发展。

可喜的是,从报告来看,哈工大毕业生在近两年有了回流趋势。相较2017届毕业生,2018年、2019年哈工大本科毕业生留在黑龙江的意愿有所增强。2018年,哈工大本科毕业生留黑就业人数共增加72人。2019年,哈工大毕业生留本地占比从9.20%持续上升至11.89%。2019年,哈工大本科签约就业毕业生共1264人,有326人留在黑龙江;硕士签约毕业生共2730人,有149人留在黑龙江。

实际上,黑龙江省在2019年接连出台多项人才引进举措,其中以“头雁行动”尤为突出。这一行动为黑龙江省留住了科教领域的重点优秀人才,集聚了各类高层次人才873人。新晋4位院士,在黑龙江工作的两院院士达42位。

与哈工大的情况类似,西安交大毕业生本地就业也呈回流趋势。2020年陕西省就业毕业生占总人数的比例相对于2017年来说增长了约20%,并且已经保持了连续四年增加。2017年以来,陕西省各地出台了人才新政、户籍新政、创新创业新政等一系列吸引人才的政策和配套措施,城市整体吸引力不断增强,效果已初步显现。

“一带一路”、京津冀地区、长江经济带、粤港澳大湾区等重大战略重大工程汇集的优秀资源,也吸引了C9毕业生的大量流入。从城市群的视角来看,C9毕业生的选择具有显著特征。

其中,京津冀地区和长江经济带对C9毕业生的吸引力相对较高,在就业地区TOP3榜单中各上榜6次。北京大学毕业生选择留在京津冀地区发展仍属主流。虽然毕业生留在京津冀地区的总人数变化不大,从2017~2019三年间一直稳定保持在46%~48%之间。但值得注意的是,博士生所占比重却在稳步上升,从2018年的294人跃升至2019年的465人,增幅超过10%。

数据来源:C9就业质量报告 21世纪经济研究院统计

值得一提的是,在国家鼓励扎根基层、深入西部的大背景下,推动毕业生奔赴西部地区成为C9高校研究毕业生就业质量的标准之一。21世纪经济研究院发现,除了西安交通大学由于地处陕西属西部地区,属地效应使得西安交通大学毕业生留陕就业比例较大之外,整体而言,C9高校毕业生赴西部就业表现并不突出。仅有复旦大学、南京大学在近三年赴西部就业的毕业生人数微弱上升,上升幅度均在0.5%左右。北京大学在已公布的近三年数据中甚至出现逐年递减的趋势,2017年毕业生赴西部地区就业比例为8.3%,2018年为7.5%,到2019年这一数字降至6.9%。可以看出,人才持续地向东部地区涌入,扎根西部的意愿不足。

教育业与IT热度持平

在毕业去向上,C9中有7所高校本科毕业生的第一选择是升学深造。

据统计,C9高校本科毕业生的平均升学率(包含国内升学和出国出境升学)为34.72%,其中中国科学技术大学本科毕业生的升学率最高,占毕业生总人数的47%,其次是清华大学,为43.5%。

对于加入就业大军的学生而言,哪些行业和公司最受C9毕业生青睐? 从C9就业报告显示的Top3行业可以看出,IT依然热门抢手,共上榜7次,教育行业也显示出对毕业生的强劲吸引力,同样上榜了7次。制造业和金融业的热度较往年有所减退,分别只上榜了4次和2次。(注:为统一指标口径,不同院校使用的“信息传输、软件和信息技术服务业”、“IT业”、“软件和信息技术服务业”等统称为“IT”。)

数据来源:C9就业质量报告 21世纪经济研究院统计

较高的薪酬待遇,是吸引C9毕业生大量进入教育行业的重要原因。国家统计局数据显示,教育行业人员2019年平均工资为97681元,2018年平均工资为92383元,增幅为5.7%。

再来看C9毕业生选择就业单位的情况。上榜次数显示,华为依然在抢占优秀毕业生资源中独占鳌头。除浙江大学暂未对外公布就业单位数据外,另8所高校中,有7所院校就业榜单Top1是华为。此外,阿里巴巴也成为破除腾讯“独占” 榜单局面的互联网公司,与腾讯“二分天下”,分别上榜3次和2次。与此同时,C9毕业生依然非常青睐央企,中国建筑、中国航天科技、国家电网等企事业单位仍保持着对人才的吸引力,均在TOP3中榜上有名。

数据来源:C9就业质量报告 21世纪经济研究院统计

对于C9博士毕业生来说,进入高等教育单位仍属主流,平均占C9博士毕业生总人数的46.52%。其中,南京大学和哈尔滨工业大学(本部)最为突出,分别有60.77%和59.61%的博士生毕业后进入高校。相比之下,清华博士毕业生更倾向于进入业界工作,流入高校占比仅为25.3%。

数据来源:C9就业质量报告 21世纪经济研究院统计

(实习生罗文利对本文亦有贡献)

(作者:刘美琳 编辑:李博)

21世纪经济报道及其客户端所刊载内容的知识产权均属广东二十一世纪环球经济报社所有。未经书面授权,任何人不得以任何方式使用。详情或获取授权信息 请点击此处

to B软件为啥用户体验不好_阿朱=行业趋势+开发管理+架构-CSDN博客

$
0
0

to B软件为啥用户体验不好?我今天从机制根源层面给大家说说。否则大家还停留在UI、UE的认知层面上。

(1)从甲方视角看

to B软件其实分为:高层决策软件、中层管理软件、基层业务操作软件。

大家抱怨的焦点其实是:中层管理软件。

实际上,对于高层决策软件,一般会呈现为大屏可视化,用户体验可好呢。因为用户体验不好的话,决策层就决定着合同的生死大权。

实际上,对于基层业务操作软件,尤其是面向消费者进行收费结算收银计费的,更是用户体验好。我过去做过零售业收费系统、医院计费收费系统,那操作人员,快的很。因为用户体验不好的话,就影响企业招财进宝。

大家抱怨的焦点其实是:中层管理软件。为啥?

就是因为:管理层有病,让员工吃药。

管理软件,买单的是领导,主要用的人却是员工(为了输入采集管理层需要的数据)。

买的人不用,用的人不买,这是企业管理软件用户体验不好的重要原因。

另外一个原因也是此问题的延伸。为啥买的人不用,用的人不买?

嘿嘿嘿,管理软件,在中国就容易变成管控软件,变成管理者对员工的控制工具。

所以买的人不用,用的人不买,这也是有道理的。

既然是控制工具,那控制的招儿就多了去了。

有人说,西方的管理软件也用户体验不好啊。难道西方也把管理软件像中国一样变成控制工具了?

嘿嘿嘿,你还真说对了。

你以为西方的管理软件是企业运营管理团队买的吗?错了。西方的管理软件是董事会买的。

500年前,1603年,世界上第一个股份制有限责任公司在荷兰产生。这家公司:

  • 社会公开发行股票来募资

  • 成立股票交易所进行股票售卖发行、转让

  • 这么多社会股东,于是选举大股东成立董事会

  • 董事会的人都是投资者股东,他们也不懂具体业务经营。所以由董事会雇佣职业经理人团队来具体日常经营

一帮都是投资者股东的董事会,为了防止这帮高级打工人为了短期利益骗他们,于是:

  • 事前:董事会雇佣咨询公司做战略设计咨询、业务流程设计咨询

  • 事中:董事会雇佣IT公司,把战略设计、组织设计、业务流程、绩效考核指标都固化到IT代码中,让经营团队日常开展业务时使用,把数据沉淀在IT系统中

  • 事后:董事会雇佣会计师审计师事务所,从IT系统中抽取数据,来做事后审计

所以,西方企业服务(咨询服务、IT服务、会计师审计师服务)是兴盛了上百年。而我国,因为机制不同,所以这整个体系都没有。所以也谈不上前进发展。

很多人说中国企业的企业管理水平不成熟,等待中国企业管理水平成熟。

我想说:....。

(2)从乙方视角看

其实这还是从甲方视角来说的,但屁股是坐在乙方的。

首先说一个不知道对错的话,这个话是我展开话题的基础,如果这个前提假设错了,我的这个展开就是错的了。

这个话就是:中国,知识不值钱。

所以,咱们才会出现那两个网上段子:

1、中国段子:雷军被人骗了,花了三年时间,花了两百万,只是把小米Logo的直角改成了圆角。哈哈哈哈,给我15秒钟,给我100块,我一行JS代码就能做到

2、西方段子:我用粉笔画这根线,确实只值一美元。但是我在这个地方画这根线,值9999美元。

很多甲方抱怨乙方的企业管理软件做的太不人性了。然后我说:如果我有本事,把现在这1000个功能点简化成十个功能点,还卖现在这个价格,你们能同意吗?

答案是:不同意。

对,这就是中国。1000个功能点你可以卖100万,但你达到同样功效的十个功能点却卖不了100万。这就是认知。

我们还停留在为数量认知的水平,还没有上升到为质量认知的水平。

优化企业管理软件的用户体验其实很容易:

  • 从业务视角出发:做企业业务流程重组

  • 从技术视角出发:应用自动化/传感器、智能化/RPA,让企业管理软件静默后台工作,不需要前端人工来操作

  • 从考核视角出发:每年要求软件操作(按钮点击和文本输入)减少15%。如此坚持6年,就会减少90%的按钮和文本输入

但是,你敢这么要求吗?

我想起了京东和四通一达的例子。京东物流主要是自己用,所以如果不提高效率,成本全是自己的。所以京东物流拼命做无人仓、无人机、无人车。而四通一达主要是给别人搬箱子,搬一次就收一次的钱。所以四通一达没有优化效率的动力,因为机制原因,自己越优化,自己越不容易赚钱。

这就是机制问题,怎么优化也是杯水车薪。如果做软件是自己自用而不是卖给别人,那自己自用当然是越简单越好,否则就是给自己自找麻烦。如果是卖给别人,那就越复杂越好,这样就能在竞标时说:我们为啥比竞争对手卖的贵、我们为啥比竞争对手好,就是因为,我们有1500个功能点,竞争对手才1000个功能点。

(3)从乙方商业模式视角看

过去是软件模式,一旦发版,就安装到了一家家客户的内部。如果这时候你才发现一个BUG,这要升级,那花费的成本、效率可就费死劲了。

所以过去开发软件,都是一年发一个版本:3个月产品设计、3个月架构设计、3个月开发、3个月测试。

所以你会看到SAP,1993年发布C/S模式的R/3 ERP套件,2014年才发布B/S模式的S/4 ERP套件。这一下子就过去了20年。而且再过三年就2024年了,从2014年发布S/4,到2024年,十年又过去了。

所以很多人抱怨为啥很多企业管理软件还在用IE6。无他,因为企业管理软件的节奏是:十年甚至二十年才一个大改变,每年一点小修补。

但是SaaS时代不一样了。很多人不明白为啥欧美SaaS公司大多创办于1997-2003这个年头。

其实是因为欧美SaaS这个新模式的产生,是当初由两拨人融合而成的。一拨人是传统软件人出身,但想搞互联网。因为1997年亚马逊、雅虎上市,市值都飞到天上去了。人人都想做互联网,想发财。像Salesforce的创始人就是Oracle传统软件出身,想模仿Amazon的电子商务网站模式,在网上卖软件、支付软件费、运行软件。还有另一帮人是做互联网创业的,可惜2001年世界互联网泡沫破裂了,不好融资了,要么死,要么转型。所以这帮互联网人就被迫转型搞了企业软件。但是这两拨人搞企业软件都不是用新技术把过去的传统企业软件重搞一次,而是这两拨人融合成了一种新模式,这就是既不像传统软件,又不像互联网电子商务网站的一种新模式,那就是:公有云、多租户、网上营销、网上试用、网上支持、网上使用、网上社区支持。你说这像不像不带流量的天猫商铺啊。

而中国从2015年以来兴起新一波SaaS,进来的人却大多是传统软件人,主要是用新技术把老东西重做一次,而且研发模式也是传统软件研发模式,只不过周期缩短成了一个季度发一个版本。还是版本管理模式。

我在京东的时候,我没听说京东的网站是按照版本来研发的。

我今天从机制根源层面给大家分析了:TO B软件为啥用户体验不好了。

大家也就明白了:to B软件,用户体验会永远不好。

这是机制决定的。

只有业务+IT一体化的羊毛出在狗身上的互联网公司电子商务公司,做企业生产力工具,才会体验好。如果互联网公司电子商务做企业内部管理软件,也是一样的烂用户体验德行。


.Net Core 全局性能诊断工具

$
0
0

前言

现在.NET Core 上线后,不可避免的会出现各种问题,如内存泄漏、CPU占用高、接口处理耗时较长等问题。这个时候就需要快速准确的定位问题,并解决。

这时候就可以使用.NET Core 为开发人员提供了一系列功能强大的诊断工具。

接下来就详细了解下:.NET Core 全局诊断工具

  • dotnet-counters
  • dotnet-dump
  • dotnet-gcdump
  • dotnet-trace
  • dotnet-symbol
  • dotnet-sos

1、dotnet-counters

简介

dotnet-counters 是一个性能监视工具,用于初级运行状况监视和性能调查。它通过 EventCounter API 观察已发布的性能计数器值。例如,可以快速监视CUP使用情况或.NET Core 应用程序中的异常率等指标

安装

通过nuget包安装:

dotnet tool install --global dotnet-counters

主要命令

  • dotnet-counters ps
  • dotnet-counters list
  • dotnet-counters collect
  • dotnet-counters monitor

a)dotnet-counters ps:显示可监视的 dotnet 进程的列表


b)dotnet-counters list命令:显示按提供程序分组的计数器名称和说明的列表

包括:运行时和Web主机运行信息

c)dotnet-counters collect 命令:定期收集所选计数器的值,并将它们导出为指定的文件格式

dotnet-counters collect [-h|--help] [-p|--process-id] [-n|--name] [--diagnostic-port] [--refresh-interval] [--counters <COUNTERS>] [--format] [-o|--output] [-- <command>]

参数说明:

示例:收集dotnet core 服务端所有性能计数器值,间隔时间为3s

d)dotnet-counters monitor命令:显示所选计数器的定期刷新值

dotnet-counters monitor [-h|--help] [-p|--process-id] [-n|--name] [--diagnostic-port] [--refresh-interval] [--counters] [-- <command>]

示例: dotnet-counters monitor --process-id 18832 --refresh-interval 2

2、dotnet-dump

简介

通过 dotnet-dump 工具,可在不使用本机调试器的情况下收集和分析 Windows 和 Linux 核心转储。

安装

dotnet tool install --global dotnet-dump

命令

  • dotnet-dump collect
  • dotnet-dump analyze

a) dotnet-dump collect:从进程生成dump

dotnet-dump collect [-h|--help] [-p|--process-id] [-n|--name] [--type] [-o|--output] [--diag]

参数说明:

示例

dotnet-dump collect -p 18832

b)dotnet-dump analyze:启动交互式 shell 以了解转储

dotnet-dump analyze <dump_path> [-h|--help] [-c|--command]

示例: dotnet-dump analyze dump_20210509_193133.dmp进入dmp分析,查看堆栈和未处理异常

Sos命令列表:

3、dotnet-gcdump

简介

dotnet-gcdump 工具可用于为活动 .NET 进程收集 GC(垃圾回收器)转储。

dotnet-gcdump全局工具使用 EventPipe 收集实时 .NET 进程的 GC(垃圾回收器)转储。创建 GC 转储时需要在目标进程中触发 GC、开启特殊事件并从事件流中重新生成对象根图。此过程允许在进程运行时以最小的开销收集 GC 转储。

这些转储对于以下几种情况非常有用:

  • 比较多个时间点堆上的对象数。
  • 分析对象的根(回答诸如“还有哪些引用此类型的内容?”等问题)。
  • 收集有关堆上的对象计数的常规统计信息。

安装:

dotnet tool install --global dotnet-gcdump

示例:从当前正在运行的进程中收集 GC 转储

dotnet-gcdump collect [-h|--help] [-p|--process-id <pid>] [-o|--output <gcdump-file-path>] [-v|--verbose] [-t|--timeout <timeout>] [-n|--name <name>]

参数说明:

生成示例:dotnet-gcdump collect -p 18832

查看生成文件:使用perfview查看:

4、dotnet-trace

简介

分析数据通过 .NET Core 中的 EventPipe公开。通过 dotnet-trace 工具,可以使用来自应用的有意思的分析数据,这些数据可帮助你分析应用运行缓慢的根本原因。

安装

dotnet tool install --global dotnet-trace

命令

dotnet-trace [-h, --help] [--version] <command>

常用命令

示例

收集进程18832诊断跟踪:

使用Vs打开生成的跟踪文件如下:

5、dotnet-symbol

简介

dotnet-symbol 用于下载打开核心转储或小型转储所需的文件(符号、DAC/DBI、主机文件等)。如果需要使用符号和模块来调试在其他计算机上捕获的转储文件,请使用此工具。

安装

dotnet tool install --global dotnet-symbol

命令

dotnet-symbol [-h|--help] [options] <FILES>

options

6、dotnet-sos

简介


dotnet-sos 在 Linux 和 macOS(如果使用的是 Windbg/cdb,则在 Windows 上)安装 SOS调试扩展。

安装

dotnet tool install --global dotnet-sos

命令

在本地安装用于调试 .NET Core 进程的 SOS 扩展

dotnet-sos install

示例

总结

微软提供了一套强大的诊断工具,熟练的使用这些工具,可以更快更有效的发现程序的运行问题,解决程序的性能问题。

过程中主要使用:counters、dump、trace 工具用于分析.NET Core性能问题。

最近又了解到微软已对这些基础工具已封装了对应包(Microsoft.Diagnostics.NETCore.Client),可以用来开发出自己的有界面的诊断工具。后续将了解实现一个。

转自:chaney1992
链接: http://cnblogs.com/cwsheng/p/14748477.html

远大集团预制装配技术让一栋10层高楼在一天内建成

$
0
0

据外媒报道,预制装配式建筑被设计成可以在很短的时间内组装在一起--这是它们的一大吸引力所在。不过估计很难找到一栋楼房能像中国远大集团最近建成的这座10层住宅楼那样快。据悉, 这座大楼仅用了一天多一点的时间--准确地说,是28小时45分钟。

该高层建筑采用了远大集团的Living Building预制结构体系。这个系统最吸引人的地方之一是,每个建筑模块在折叠时都拥有跟集装箱相同的尺寸,这使得它可以很容易地使用现有的运输方法在世界上的任何地方运输。

每个模块都是在工厂预制的,主要由不锈钢结构组成,包括布线、绝缘、玻璃和通风系统(远大集团还是通风专家)。简单地说,基本的想法是你把一组集装箱大小的模块带到建筑工地,按要求堆叠起来并用螺栓固定。然后,它们被连上电力和水并准备投入使用。

当然,正如你在下面视频中所看到的,为了这个项目,远大集团动用了一小群建筑商和至少3台起重机来加快进度并在引人注目的时间框架内完成工作,不管怎样这仍是一项了不起的成就。

远大集团表示,它的预制装置非常耐用、抗震,如果有需要还可以拆卸和移动。该公司还表示,它可以用于建造高层住宅、宿舍、酒店、医院等。

更有野心的是,该公司认为该系统可用于建造高达200层的高层建筑。从这个角度来看,世界上最高的建筑迪拜哈利法塔(Burj Khalifa)只有163层。目前还不知道该建筑的价格,但据Treehugger网站报道,消费者可以花不到300万美元(不包括运费)就能买到一整栋20个单元的公寓,运费非常便宜。

一文理解如何实现接口的幂等性

$
0
0

幂等,这个词来源自数学领域。幂等性衍生到软件工程中,它的语义是指:函数/接口可以使用相同的参数重复执行, 不应该影响系统状态,也不会对系统造成改变。

举一个简单的例子:正常设计的查询接口,不管调用多少次,都不会破坏当前的系统或数据,这就是一个幂等操作。

幂等的业务场景

在分布式系统中, 由于分布式天然特性的时序问题以及网络的不可靠性(机器、机架、机房故障、电缆被挖断等等), 重复请求很常见,接口幂等性设计就显得尤为重要。

幂等需要考虑的场景有很多,例如系统A是处理用户客户端发送过来的请求,无论是前端bug、脚本恶意发包、用户重复点击又或是网络超时导致的网络重发,都会造成系统A收到相同参数的网络请求。

对于处理消息队列请求的系统B和处理服务上游发送请求的系统C,也都存在网络超时导致的网络重发,所以要考虑接口的幂等性。

保障幂等性的原理

对于分布式系统来说,在JVM层面的锁已经失去作用,所以保证系统幂等性需要满足3个条件:

  1. 请求唯一标识:每一个请求必须有一个唯一标识。

  2. 处理唯一标识:每次处理完请求之后,必须有一个记录标识这个请求处理过了。

  3. 逻辑判断处理:每次接收请求需要进行判断之前是否处理过的逻辑处理。根据请求唯一标识查询是否存在处理唯一标识。

实际执行中要结合自身业务。

幂等性实现方案

1. token机制

针对客户端重复连续多次点击的情况,例如用户购物提交订单,提交订单的接口就可以通过token机制实现防止重复提交。

主要流程就是:

  1. 服务端提供生成请求token的接口。在存在幂等问题的业务执行前,向服务器获取请求token,服务器会把token保存到Redis中。

  2. 然后调用业务接口请求时,把请求token携带过去,一般放在请求头部。

  3. 服务器判断请求token是否存在redis中:存在则表示第一次请求,这时把Redis中的token删除,继续执行业务;如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。

这里要结合业务考虑这种场景:如果请求处理失败,前端是否需要重新申请token进行重试(因为此时token在服务端已经被删除)。

2. 数据库唯一索引

往数据库表里插入数据的时候,利用数据库的唯一索引特性,保证唯一的逻辑。唯一序列号可以是一个字段,例如订单的订单号,也可以是多字段的唯一性组合。

事务中包含多表数据的更新,业务要考虑处理事务回滚的问题。

3. Redis实现

Redis实现的方式就是将唯一序列号作为Key存入Redis,在请求处理之前,先查看Key是否存在。唯一序列号可以是一个字段,例如订单的订单号,也可以是多字段的唯一性组合。当然这里需要设置一个key的过期时间,否则Redis中会存在过多的key。具体校验流程如下图所示:

如果想要基于Redis实现幂等性防重框架,需要考虑如下两个问题:

  1. 如果第一次请求失败了,客户端重试,是否需要放行?

  2. 网络请求可能是get或者post(内部rpc协议除外),唯一序列号参数可能在url或是在body体里。则使用防重框架的新接口以及之前老业务接口能否做到版本兼容性?

建议业务使用方最好针对指定业务进行Redis的幂等方案。

Zookeeper同样也能实现上述功能,但由于Zookeeper是CP模型,性能不如Redis,另外针对防重场景,也并不需要Zookeeper高可靠性,所以优先推荐Redis。

4. ON DUPLICATE KEY UPDATE

有些业务场景是先根据索引从表中查询数据是否存在,如果存在则更新状态,不存在才插入数据。

这种情况下在并发量不大的时候没有问题,但是在高并发场景,可能会出现同时插入两条相同索引的情况,导致"Duplicate entry for key 'PRIMARY'"问题。

解决方法首先想到的当然是分布式锁。但分布式锁降低了吞吐量而且分布式锁依赖的组件,如Zookeeper或Redis如果出现网络超时,同样会影响在线服务。

所以一个简单的解决方法是使用mysql的INSERT INTO ...ON DUPLICATE KEY UPDATE语法,从而保证了接口的幂等性。

5. 状态机

对于很多业务,都存在业务流转状态的,每个状态都有前置状态以及最后的结束状态。

以订单为例,已支付的状态的前置状态只能是待支付,而取消状态的前置状态只能是待支付,通过这种状态机的流转就可以控制请求的幂等。假设当前状态是已支付,这时候如果支付接口又接收到了支付请求,则会抛异常或拒绝此次请求。


publicenumOrderStatusEnum {
UN_SUBMIT(0,0,"待提交"),
UN_PADING(0,1,"待支付"),
PAYED(1,2,"已支付待发货"),
DELIVERING(2,3,"已发货"),
COMPLETE(3,4,"已完成"),
CANCEL(0,5,"已取消"),
;

//前置状态
privateintpreStatus;

//状态值
privateintstatus;

//状态描述
privateString desc;

OrderStatusEnum(intpreStatus,intstatus, String desc) {
this.preStatus = preStatus;
this.status = status;
this.desc = desc;
}
//...
}

6. MVCC方案

这个方案严格上并不是解决幂等问题,更确切来说是解决并发问题。但高并发场景下,也是一种必须的保障措施。

多版本并发控制,该策略主要使用 update with condition来保证多次外部请求调用对系统的影响是一致的。在系统设计的过程中,合理的使用乐观锁,通过version或者updateTime(timestamp)等其他条件,来做乐观锁的判断条件,这样保证更新操作即使在并发的情况下,也不会有太大的问题。例如

select*fromtablenamewherecondition=@condition //取出要更新的对象,带有版本versoin        
updatetableNamesetname=#name#,version=version+1whereversion=@version

在更新的过程中利用version来防止其他操作对对象的并发更新。如果直接拒绝是不理想的操作,则服务端需要一定的事务回滚与重试机制。

7. 分布式锁

有关分布式锁的讲解,可以查看博客《 一文理解分布式锁的实现方式

分布式锁同样可以实现接口的幂等性,但由于分布式锁对系统负担来说相对要重一些,可以结合业务场景进行技术选型。

参考文档:

  1. https://zh.wikipedia.org/wiki/冪等


用虚拟机搭建Kubernetes集群_The_shy等风来的博客-CSDN博客_虚拟机搭建k8s集群

$
0
0

一、Docker到底做了什么:

docker就是个容器服务。一个轻量级的,在宿主机(比如你的云服务器centos或ubuntu虚机)基础上建立的一个隔离的主机环境,我们把这个隔离的虚拟主机环境叫容器。跟传统的虚拟机相比,docker最大的区别就是它复用了外部物理宿主机内核,所以很轻量。docker主要解决了开发与部署时的环境冲突问题以及部署项目的成本问题:

1、保证部署和开发环境一样docker环境
2、任何语言写的程序+程序的依赖环境都被封装打包为docker镜像
3、所有程序都通过统一的docker命令将镜像运行成docker容器

二、Dockerfile:

一切镜像都是基于Dockerfile来构建的,除了使用官方或别人的镜像,你也可以自己编写Dockerfile构建镜像,Dockerfile是个特殊的文本文件,就叫Dockerfile名称不能随便取。

三、裸跑docker的痛点:

1、单机使用,无法有效集群。
2、随着容器数量上升,管理成本攀升。
3、没有有效的容灾/自愈机制,容器死了你只能手动重启或新建。
4、没有预设编排模板,无法实现快速、大规模容器调度。
5、没有统一的配置管理中心工具。
6、没有容器生命周期的管理工具。
7、没有图形化运维管理工具。

综上所诉, 当容器量上升之后,我们需要一套容器编排工具。基于docker容器引擎的开源容器编排工具目前市场上主要有:
(1)docker compose,docker swarm
(2)Mesosphere + Marathon
(3) Kubernetes

四、K8S概述:

由来:谷歌的Brog系统,后经Go语言重写并捐献给CNCF基金会,彻底开源。
含义:词根源于希腊语:舵手/飞行员。
作用:开源的容器编排工具( 生态极其丰富)。
在这里插入图片描述
如果装有集装箱的船没有帆,是没有灵魂和方向的。

五、K8S核心架构:

在这里插入图片描述
1、看上图,一共有三台节点,一台master两台node。k8s集群中所谓的节点其实就是服务器,k8s中的节点分为两类。一类是主节点master,它只负责做管理调度,不跑业务,主节点上的主要组件有三个,一是 apiserver,它是k8s的大脑,k8s集群所有运转都要经过apiserver;第二个 scheduler,用来从后端的n多个工作节点node中按照特定算法选出一个或多个最合适的节点进行弹性伸缩pod,为什么是pod不是docker容器?因为K8S中最小的资源调度单元就是pod,pod其实就是一个包含多个docker容器的容器组而已;第三个 controller manager,用来控制刚刚选出来的后端节点启动或销毁pod,维持pod数量始终在我们规定的预期中。

2、还有一类是k8s的工作节点或运算节点worker,就是上面说的node。node中主要组件也有三个,一个是 docker,node是专门用来跑pod的,所以必须要有docker服务;第二个是 kubelet,kubelet接受master上的controller manager的指令,收到指令后告诉这个node上的docker服务,docker收到指令就会去启动或销毁哪些docker容器;最后一个是 kube-proxy服务发现,现在node上的这些pod跑起来了,但K8S集群终究是个私有网络或是内网,现在还不能被外部访问,我们要先把k8s的内部网络打通,k8s已经提供了解决方案,本例我们用flannel的cni网络插件,让每个node上都跑一个flannel的pod,救能把k8s内网互相打通。外部访问每个node的唯一的物理网卡指定ip,随后物理网卡端口映射到内网node上pod的端口,达到访问pod目的,后面会专门讲K8S的服务发现。

3、另外K8S还有个核心组件: etcd,看上图它是跑在master上,其实生产环境etcd集群是单独跑在其他节点上的。etcd可以看成K8S数据库,所有的master和node的节点信息,服务,K8S账户密码都在这里存着。

六、再次理解K8S:

K8S就是一组服务器的集群,只不过是每个master和node上面都安装了上诉各自的三个组件,让我们能在master上统一调度这些node节点来跑pod。

七、 K8S的运转流程以及其它介绍

首先 etcd存储了k8s(master)的认证账户和密码,还有注册进k8s集群的所有工作节点node的ip和主机信息。假如现在我想启动一个nginx容器组pod来提供服务:首先你用K8S客户端命令kubectl进行认证或者说登录k8s, apiserver会对你的账号和密码去etcd对比,通过的话apiserver马上让 scheduler去帮你找一个合适的node,scheduler就会根据特定算法去后端找一个特别合适的node,找到之后马上返回给apiserver,apiserver接着会去找 controller manager,让它去这个node上通知 kubelet让它运行一个nginx的pod,kubelet收到指令马上在本机找 docker服务,docker收到指令就会在本机启动一个nginx的pod。然后 kube-proxy给这个pod分配一个ip,让外界通过service或是ingress来访问这个nginx的pod(服务发现)。

八、案例:

不使用k8s之前:现在一个网站需要抗1w的并发,假设网站做的nginx反向代理引流。但是现在假设一个nginx只能抗2k的流量,所以我们需要跑五台nginx容器,前面说过K8S最小执行单元是pod,现在假设每个pod中只运行一个nginx的docker容器。这五个pod全跑在一个服务器肯定是性能没有保障。现在我们拿三个节点来跑,前两个节点跑两个pod,第三个跑只跑一个pod。现在抗1w并发肯定是没问题了。但是一段时间后第一个节点宕机了,那么它的docker进程也就没了,所以里面运行的这两个pod也肯定没了,你只能去检查这个节点为什么崩了,检查好了还要重启pod。 使用k8s:第一个节点崩了之后,k8s的controller-manager就发现,诶,之前用户给我规定好要维持五个pod为什么现在只有三个pod,它就会告诉apiserver,apiserver又去找scheduler让它选出一个或多个节点来跑还差的两个pod,scheduler自动帮你选出一个最合适的节点比如第三个节点,再通过apiser转发到controllermanager,controller-manager再到第三个node的kubelet,让kubelet通知到本地docker,最后docker再跑两个pod,现在就成了集群中第三个node跑三个pod,加上第二个node的两个,保证你始终是有五个pod,这就叫 k8s的容灾机制,允许你集群中的某个节点宕机。现在又来了,假设突然并发升到了1.5w,那怎么办?k8s会再在剩下的两个节点中继续选一个或多个再创建三个nginx容器(假设一个nginx抗2k)。

九、K8S优势:

1、 自我修复。某个节点宕机之后,k8s会自动在其他节点跑这个宕机节点上所有死去的docker容器。注意修复是指维持docker数量。不是说你节点死机了k8s帮你修复这个节点(服务器)。
2、 服务发现,负载均衡。k8s自带负载均衡,第一个请求分到第一个nginx容器,第二个分到第二个容器,以此轮询。
3、 自动部署和回滚。部署服务只需命令,k8s会自动装依赖。回滚顾名思义就是新版部署到k8s之后出问题了,你可以轻松地回滚到上一个稳定的版本。
4、集中化配置管理和密钥管理。
5、存储编排。
6、任务批处理运行。
7、 自动弹性伸缩

十、常见的K8S安装部署方式:

1、Minikube,仅供学习使用,无意义,它是个单节点的微信K8S。
2、 二进制安装部署(生产上首选,学习首选,因为自己配置的好排错)。
3、 kubeadmin,K8S的一种部署工具,相对简单,熟手推荐。

十一、K8S服务发现:

在这里插入图片描述
也就是前面说的kube-proxy组件,实现让外部访问node上跑的pod一共有两种方式,一是 service,二是 ingress。以service为例:K8S中service是pod的对外访问的统一接口,为什么要加这一层service或ingress呢,因为pod的生命周期可长可短,当pod被销毁重启后,K8S会自动随机分配ip,这时ip一定会变,如果外部客户端再次访问之前的pod时,如果这个pod被重启过是访问不到的,显然很不友好,而service或ingress就是为了解决这个问题。即外部只访问service或ingress,不需要管pod的ip是多少变没变。因为这个service或ingress的ip是定死的,就算pod的ip变了,service或ingress会在内部自动修改对应起来,可以把service或ingress跟pod的关系理解为映射。并且service或ingress收到请求后是RR算法(轮询)。那service或ingress怎么知道这个请求对应要去轮询哪些pod呢?所以K8S中又引入了 label标签,每个pod都有自己的label标签。pod和label是多对多,比如现在一个node上跑了八个pod,有三个pod我打label标签为v1,有两个pod我打标签为v2,最后三个pod我打标签为v2和v3。然后我在node上定义了两个service服务,service1只管v1和v2,外部访问service1会映射到v1和v2标签,service2映射到v3;当客户端访问service1的ip+port时,service1就会自动调度到标签包含v1和v2的pod上去,因为这里所有pod都带有v1或v2,那它就会对这八个pod来轮询给客户端访问,当客户端访问service2的ip+port时,service2就只会调度到最后两个pod上去轮询。注意到一点,现在service只能是ip+port,如果我想要相同ip+port不同的访问路径来映射到不同pod就没办法,这时,ingress就用场了,ingress跟service唯一区别就是,ingress能细到访问路径,相同ip+port不同路径能对应到不同的label标签。service和ingress都是pod的对外访问接口,当外部访问pod时,如果是访问service服务就用:127.0.0.1:8000,如果是访问ingress服务就用:127.0.0.1:8000/a/b/c。

K8S有三层网络:Node网络,POD网络,Service网络。
注意真实的物理网络就只有一个,就是Node节点网络,也就意味着我们在构建服务器的时候只需要一张网卡(下文我们只有一个网卡配置文件)。Service网络和POD网络都是虚拟网络,即内部网络。你想要访问POD就需要在service网络中去访问,service就会通过iptables或lvs的转换以及label标签达到访问pod的目的。

最后一个 namespace命名空间,比如一个场景,你是pod提供商,你在同一个工作节点启了10个pod,每个pod都有label对吧,并且这些个pod肯定是能通过localhost互相访问,假如现在腾讯买了你5个pod,网易买了你5个pod,但他们是两家不同公司,肯定是不希望相互之间访问。而这个namespace就可以实现业务隔离,为不同公司提供隔离的pod运行环境。另外还有一个场景,你也是跑了10个pod仅个人使用,你有五个pod用于线上环境,5个pod用于测试环境,如果测试环境能访问线上环境肯定也是不合理,所以第二个功能就能隔离线上、测试、开发环境。一句话namespace能隔离同一个工作节点上的pod。

十二、搭建一个完整的Kubernetes集群:

1、 测试环境K8S平台规划(单master):一台master接三台node再加三个etcd服务
在这里插入图片描述

2、 生产环境K8S平台规划(多master):三台master接两台LB(下图load balancer)负载均衡到三台node加三个etcd服务
在这里插入图片描述
生产环境建议使用3个master节点,防止master宕机,node节点越多越好,前面说了node宕机不用怕,K8S会自动把宕机节点上的所有pod迁移到其他node上,这个迁移操作是master来自动调度安排的,所以master是不允许宕机的,这就是我们生产环境必需做master高可用的原因,就像你电脑的cpu,你的cpu挂了你电脑就废了。本例中我们用LB(load balancer)实现负载均衡,即node和master是通过LB来通讯的,让LB来接受node请求再调度到某个master来处理,而LB同样是为了防止单点故障所以是两个,最后etcd,etcd在生产环境中也 必须大于三台节点以上的奇数个防止单点故障,etcd会自动做主从,奇数个是因为选主节点是投票机制。

十三、本次实验集群虚机配置介绍:

生产环境中每个master要保证16G内存,每个node要保证64G内存且越大越好,因为node是用来跑pod的。 本次条件限制我们的所有虚机都调为1G内存。我的笔记本是8G内存,我是这样安排的,两台master,两台node,两台LB,然后 etcd服务我就不单独开虚机,直接跑在master1和两台node上,我需要安装6个虚机。三台虚机都是2核1G,就算6台机内存占满了,也还留了2G内存给我的外部windows机。这六台虚机的内存设置太低了,只能用于实验跑几个pod,稍微多跑一点就要卡,或者直接导致集群崩溃。

从下一步开始,到第二十六步结束。我先只部署一个master加两个node,然后部署三个etcd服务在这三台节点上。第二十七步再开始实现master高可用,即再加一台master和两台LB。

十四、本次实验集群规划:

1、下面链接有我这次离线部署K8S集群所需的全部压缩包。链接: https://pan.baidu.com/s/11AsmR_Zr0rqlioRjBGATnQ提取码: 1dh7 。

2、前面说了我先用单master加两个node一共三台节点来启一个k8s集群,安装虚拟机的时候要 一、选择桥接网络。二、内存1G,如果你的物理机内存大可以每台机2G。三、2个内核。四、50G硬盘。安装三台虚机是比较耗时的,所以我只安装一台虚机,取名叫k8s-master1,剩下的两台node用安装好的k8s-master1克隆出,改一下名称就行了。我们是桥接网络,所以虚机的ip要设置成跟我的windows物理机网段一致,我的物理机是192.168.0网段。centos镜像版本是centos7.7,点击阿里云centos镜像站点 http://mirrors.aliyun.com/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-2003.iso下载这个镜像来安装,安装系统就不用说了,挂载好了镜像之后,点开机跟着操作就行了。安装完master1的centos操作系统后,进去改网卡配置文件 vi /etc/sysconfig/network-scripts/ifcfg-ens33,文件如下:

TYPE=Ethernet # 网卡类型:为以太网
PROXY_METHOD=none # 代理方式:关闭状态
BROWSER_ONLY=no # 只是浏览器:否
BOOTPROTO=static#设置网卡获得ip地址的方式,我们直接静态ip定死
DEFROUTE=yes # 默认路由
IPV4_FAILURE_FATAL=no # 是否开启IPV4致命错误检测:否
IPV6INIT=yes # 现在还没用到IPV6不用管
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=no
IPV6_ADDR_GEN_MODE=stable-privacy
NAME=ens33 # 网卡物理设备名称,看你自己的网卡文件后缀
UUID=e06fa942-1b0e-426d-b262-ca2cd3375fa2 # 通用唯一识别码我们不一样
DEVICE=ens33 # 网卡设备名称, 必须和 NAME 值一样
ONBOOT=yes# 系统启动时是否应用此网卡,前面服务发现提到k8s真实的物理网络就只有一个,所以只需这一张物理网卡。
IPADDR=192.168.0.63# 网卡对应的ip地址
GATEWAY=192.168.0.1# 网关,最后一位是1,前三个是你的网段
PREFIX=24# 子网 24就是255.255.255.0
DNS1=8.8.8.8# 域名解析dns服务器

3、上面我master节点的网卡配置文件,我把master1的ip设为192.168.0.63,其余两台node机,克隆开机后只需进去换下上图的IPADDR保存退出,重启网络即可。顺便克隆出master2和LB1和LB2待用。然后 systemctl restart network重启网络,ifconfig命令在centos7不可用,用 ip ad查看本机master1的ip,发现就变成了设置的192.168.0.63。再去ping外部机和百度,现在你就可以在xshell上操作了,要方便很多,我在vmware上连个复制都没有。
在这里插入图片描述
5、刚刚克隆出来的master2、LB1、LB2先放着。这里我们必需完成三台虚拟机(master1,node1和node2)的创建还要修改好ip(修改网卡后一定要重启网络),并且都能ping通外部主机和外网达到上图效果。另外再介绍一下我的虚机配置:

master1:虚机名为k8s-master1 IP为192.168.0.63。
node1:虚机名为k8s-node1 IP为192.168.0.65。
node2:虚机名为k8s-node2 IP为192.168.0.66。
master2:虚机名为k8s-master2 IP为192.168.0.64。
LB1:虚机名为LB-backup1 IP为192.168.0.67。
LB2:虚机名为LB-backup2 IP为192.168.0.68。
k8s版本:1.16, 安装方式:二进制离线安装。 虚机使用的操作系统:centos7.7。

十五、初始化三台虚机:

1、关闭三台机的防火墙以及防火墙开机自启动: systemctl stop firewalld以及 systemctl disable firewalld,最后在三台机上用 firewall-cmd --state检查一下三台机是不是not running。
在这里插入图片描述

2、关闭selinux
三个节点都执行 setenforce 0然后再 vi /etc/selinux/config修改画圈的这行,保存退出。
在这里插入图片描述

3、配置各个节点的主机名hostname,不要跟前面的虚拟机名混淆,虚拟机名只是描述没什么卵用。这一步需要在每台机上分别修改自己不同的名称,比如master1上执行 hostnamectl set-hostname k8s-master1,修改好之后你xshell关闭,重连一下三台虚机就会看到像这样的[root@k8s-master1]。

4、配置名称解析
名字取好了,下一步在每台节点上修改hosts文件,让他们各自认识彼此的主机名, vi /etc/hosts,每个节点加上这四行,保存退出。

192.168.0.63k8s-master1192.168.0.64k8s-master2192.168.0.65k8s-node1192.168.0.66k8s-node2192.168.0.67LB_backup1# 这个是load banlance1的主机名先加上,不影响的192.168.0.68LB-backup2# 这是load banlance2的主机名先加上

在这里插入图片描述

达到下面的效果,不用ip用主机名就可以互相ping通:
在这里插入图片描述
5、配置时间同步
第一步配置master:选择一个节点作为时间服务器的服务端,剩下的作为客户端,一般选master节点,配置k8s-master1: yum install chrony -y,然后再 vi /etc/chrony.conf修改下图三个位置:

在这里插入图片描述
systemctl restart chronyd重启master机上的时间服务, systemctl enable chronyd设置开机启动,最后检验时间服务器是否正常启动: ss -unl | grep 123
在这里插入图片描述
第二步配置node:现在配好了master,再去配两个工作节点node,两台node上也是分别先安装 yum install chrony -y,然后 vi /etc/chrony.conf,但这两个工作节点只需要改一个地方,如下图改成主服务器master的ip即可。

在这里插入图片描述

两台node分别 systemctl restart chronyd重启时间服务,再 systemctl enable chronyd设置开机启动,最后分别用命令 chronyc sources来检验是否和master同步。如果是个*而不是?说明两个node已经和master同步,如果是?说明chrony.conf配置文件有问题。最后你在三台机执行 date校验时间是否一致。

在这里插入图片描述

6、关闭交换分区
三个节点上分别执行 swapoff -a然后再 vi /etc/fstab,把最后一行注释掉,

在这里插入图片描述

再分别执行 free -m检查交换分区是否成功关闭,整个命令我们经常用来查看剩余内存空间,比如下图我们还剩余650M:
在这里插入图片描述

十六、为etcd颁发证书做准备:

因为 etcd和master通讯以及 node和master通讯都必须是https,所以部署之前首先要给etcd,master,node颁发ssl自签证书,我们先部署etcd服务,再部署master的三个组件和node的三个组件。关于加密:

对称加密:数据发送方和接收方用相同的密钥,隐患就是发送方会把密钥连同数据一起发过去,一旦被抓包,数据就被破了。

非对称加密:用公钥-私钥实现加解密,公钥保存在发送方用来加密数据,私钥保存在接收方用来解密,并且这一对密钥是一一对应的,一般都是有提供商。即加解密用的不是一个密钥,用这个公钥加密的数据只能由它唯一对应的私钥解密。

单向加密:只能加密,不能解密,比如MD5。

如果你想你的网站是https协议,关于ssl加密证书你要了解三点:
1、 ssl证书来源:可以网络第三方机构购买,证书加密级别越高,越贵,一般两三千,通常是公网上。另外你还可以自签一个证书,但必须是你用https在内网使用不是在公网上,通常用在公司内部,
2、 签证机构:(CA)
自建CA,可以用openssl和cfssl,本例中我们使用cfssl,也是k8s官方推荐的,比openssl更简单。让我们的k8s节点间通过cfssl来实现https访问。
3、 给xxx服务颁发证书

前面说了由于硬件配置问题,三个etcd服务我是跑在master和两个node上。我先在 master1的/root上传了网盘里的 TLS.tar.gz,这些包在上面我提供了网盘链接。先 tar -zxvf TLS.tar.gz解压,进入TLS,里边生成四个文件两个目录,一个是为etcd颁发证书,一个是为k8s的master和node颁发证书:
在这里插入图片描述

十七、在master机上给etcd服务生成自签证书:

1、前面说过三个etcd服务是跑在master和两台node上,所以要在master和两台node上为etcd颁发,现在先在master。首先执行脚本cfssl.sh,执行之前先看一下这个脚本:
在这里插入图片描述
2、我们 ./cfssl.sh执行这个脚本,然后你可以去看一下/usr/local/bin,确实就有了这三个文件。然后我们cd刚刚解压生成的etcd目录, vi server-csr.json
在这里插入图片描述
3、写明etcd所在节点ip之后,下一步向证书颁发机构申请证书,由于我们是自建CA(颁发机构),只需执行刚刚解压生成的 etcd目录里的 generate_etcd_cert.sh脚本,就能拿到证书文件了。
在这里插入图片描述
4、此时会生成四个颁发的证书文件,公钥加密私钥解密,任何人想向目前我们这个CA申请证书就必须携带这个生成的公钥 ca.pem,而另外两个server文件CA颁发好的证书,针对的就是我们前面设置的三个etcd的节点ip虚机。
在这里插入图片描述

十八、部署master上的etcd服务:

1、首先上传 etcd.tar.gz到/root目录下解压,会生成一个文件,一个目录 在这里插入图片描述
2、因为etcd.service文件指定了这两个位置,你要按照指定的位置移动一下这个文件和目录。执行

mv etcd.service/usr/lib/systemd/system
mv etcd/opt

3、紧接着就去修改这个 etcd.conf配置文件: vi /opt/etcd/cfg/etcd.conf
在这里插入图片描述
4、现在已经把etcd的配置文件改好了接下来去把上一步生成的etcd的其中三个证书文件放到etcd目录的ssl下。 \cp ca.pem server.pem server-key.pem /opt/etcd/ssl
在这里插入图片描述
其中\cp加反斜杠的意思就是不使用linux命令别名,好处就是有覆盖时不提醒,因为cp实际是有别名的cp=“ap -i”,-i表示交互式有提示,linux命令别名有时会帮我们提升工作效率:用alias可以看到所有linux命令别名,你可以用unalias删除,也可以加反斜杠临时不使用,就用原生的,下图详解
在这里插入图片描述
5为两台node上的etcd颁发证书,生产环境是不会把etcd服务附加在master或node机上跑,我是因为宿主机硬件配置有限,所以才这样做。现在我们就已经给这个单master节点上跑的etcd颁发好证书了。其它两个node上给etcd颁发证书也是重复操作,没必要。所以我们直接在master节点上把以上生成好的etcd证书和管理程序拷到两个node上,然后在每个node改一下配置文件ip即可。不建议去重复操作,一是麻烦,二是80%出错导致etcd启动报错,直接在master用下面这四行命令:

scp/usr/lib/systemd/system/etcd.service root@k8s-node1:/usr/lib/systemd/system
scp/usr/lib/systemd/system/etcd.service root@k8s-node2:/usr/lib/systemd/system

scp-r/opt/etcd root@k8s-node1:/opt
scp-r/opt/etcd root@k8s-node2:/opt

6、然后在两个node机上分别修改etcd.conf配置文件, vi /opt/etcd/cfg/etcd.conf。修改里面的主机名和ip即可(上面有详细说明怎么改),配置改好之后我们就分别在三台机上执行一下两条命令。

systemctl start etcd  (启etcd服务)
systemctl enable etcd(允许开机启动)

7、最后任意在一台机执行命令检查etcd集群是否启动成功,自己看情况改成你的节点ip:

/opt/etcd/bin/etcdctl--ca-file=/opt/etcd/ssl/ca.pem--cert-file=/opt/etcd/ssl/server.pem--key-file=/opt/etcd/ssl/server-key.pem--endpoints="https://192.168.0.63:2379,https://192.168.0.65.https://192.168.0.66"cluster-health

出现以下提示表示etcd已经在三台机上部署成功

在这里插入图片描述

十九、为k8s的master颁发自签证书:

1、前面两步已经给master和两台node上的etcd服务颁发了证书,并且已经跑起来了这个etcd集群。下面就要给master和node颁发证书了。先给master颁发,上传 tar -zxvf k8s-master.tar.gz到/root目录下解压,会生成三个文件一个目录,压缩包看前面网盘链接。
在这里插入图片描述
2、像之前给etcd颁发证书一样,先移动这3个文件和一个目录到k8s默认设定好去找的位置:

mv kube-apiserver.service kube-controller-manager.service kube-scheduler.service/usr/lib/systemd/system

mv kubernetes//opt

3、再进入之前解压证书生成的 TLS/k8s目录:
在这里插入图片描述
4、跟etcd一样,你也要改一下你为哪些node和master节点生成证书,里面ip可能有点多,因为我们只有三台机,不用删,全部改就行了不影响,保证你的三台机ip在里面即可: vi server-csr.json
在这里插入图片描述
4、现在再像之前一样执行里边的 generate_k8s_cert.sh脚本,就会生成k8s的自签证书,一共6个pem证书文件。然后把一下4个证书文件复制到ssl下:

cp ca-key.pem ca.pem server.pem server-key.pem/opt/kubernetes/ssl/

二十、正式部署master节点的三个组件:

部署master也就是部署master的三个组件。上一步为master颁发好证书之后,现在我们就去修改master上的三个组件的配置文件:

1、修改apiserver的配置文件(先只改三处): vi /opt/kubernetes/cfg/kube-apiserver.conf,我的etcd集群分别对应了我master和两台node的ip:
在这里插入图片描述
2、修改scheduler的配置文件(目前不需要改动): vi /opt/kubernetes/cfg/kube-scheduler.conf
在这里插入图片描述
3、修改controller-manager的配置文件(目前不需要改动): vi /opt/kubernetes/cfg/kube-controller-manager.conf
在这里插入图片描述
4、开启master上的三个组件服务(在master上执行)

systemctl start kube-apiserver
systemctl enable kube-apiserver

systemctl start kube-scheduler
systemctl enable kube-scheduler

systemctl start kube-controller-manager
systemctl enable kube-controller-manager

5、验证是否启动成功ps aux | grep kube

在这里插入图片描述

master上的所有日志文件都在/opt/kubernetes/logs下,可以去tail -f看一下三个组件的日志文件(.INFO结尾的),看下是否有异常。也可以执行 /opt/kubernetes/bin/kubectl get cs,如果出现下图信息,也表示你的配置是成功的,顺便把kubectl命令移动到系统bin目录下方便以后在master上操作node。

在这里插入图片描述

二十一、master节点最后一项配置,配置TLS基于bootstrap自动颁发证书(有两个要求):

1】刚才在看apiserver的配置文件的时候,里边有两行,解释如下
--enable-bootstrap-token-auth=true \指定基于bootstrap可以做认证
--token-auth-file=/opt/kubernetes/cfg/token.csv \只给token.csv文件里面指定的用户进行颁发。

2】执行命令:

kubectl create clusterrolebinding kubelet-bootstrap--clusterrole=system:node-bootstrapper--user=kubelet-bootstrap

配置好了之后就可以自动地为访问kubelet的服务颁发证书。到目前为止master节点就完全部署好了。

在这里插入图片描述

二十二、为K8S的node颁发证书并部署node节点,只示例node1:

我只介绍node1的部署流程,node2自己重复第22和23步,部署node其实也是部署node的三个组件: dockerkubeletkube-proxy。我们切换到第一台node1,把网盘中的 k8s-node.tar.gz传到node1的/root下。

1安装docker(采用离线安装):先解压文件 tar -zxvf k8s-node.tar.gz
在这里插入图片描述
2、node上执行以下命令:

mv docker.service/usr/lib/systemd/system
mkdir/etc/docker
cp daemon.json/etc/docker/tar-zxvf docker-18.09.6.tgz# 解压dockermv docker/*/bin# 把解压的docker移动过去就代表docker安装好了(离线安装)

在这里插入图片描述
3、到目前为止docker组件就安装好了,现在启动docker服务并设置开启自启动

systemctl start docker
systemctl enable docker

docker info查看真启动情况:
在这里插入图片描述

4、现在node节点的第一个组件docker已经安装并启动了,接着把刚刚解压生成的node节点上剩余的两个组件管理脚本移动到system下,最后把刚刚解压生成的kubernetes目录放到 /opt下:

mv kubelet.service kube-proxy.service/usr/lib/systemd/system
mv kubernetes//opt

5给node颁发ssl证书:我们之前生成了两个证书目录etcd和k8s,一个是给etcd颁发,一个是给master和node颁发,前面已经给master颁发了,但现在node机上还没有,所以我们需要 在master机上!!把证书scp到node上,现在是在处理node1,后面记得在node2重复这些操作:

cd/root/TLS/k8s
scp ca.pem kube-proxy-key.pem kube-proxy.pem root@k8s-node1:/opt/kubernetes/ssl/

6、来到这台node机上,修改kubelet和kube-proxy组件配置文件:

vi /opt/kubernetes/cfg/kube-proxy.kubeconfig
在这里插入图片描述
vi /opt/kubernetes/cfg/kube-proxy-config.yml
在这里插入图片描述
vi /opt/kubernetes/cfg/kubelet.conf
在这里插入图片描述
vi /opt/kubernetes/cfg/bootstrap.kubeconfig,这个是基于bootstrap自动颁发证书:
在这里插入图片描述
8、在node1上启动kube-proxy和kubelet服务,还要设置开机自启动:

systemctl start kube-proxy
systemctl enable kube-proxy
systemctl start kubelet
systemctl enable kubelet

node上两个组件的日志跟master一样也在logs下,docker的日志是用 docker logs 容器来查看。比如现在 tail -f /opt/kubernetes/logs/kubelet.INFO就可以查看node的kubelet组件的日志,学会看这些组件的日志文件,对以后排错很有用:
在这里插入图片描述
9、如果日志的 最后一行信息提示上图所示就表示node中两个服务启动正常,最后一行的意思是没有给node1颁发( 正确的)证书,因为node上的证书是从master拷过来的。所以现在我们需要在master上给node自动颁发证书(基于bootstrap),首先 在master机上查看是否有node1发过来的请求颁发证书的请求: kubectl get csr
在这里插入图片描述
收到请求后就执行 kubectl certificate approve 图中的token或者说NAME,然后再次查看请求就发现是已颁发的状态:
在这里插入图片描述

10、给node1颁发好正确的证书之后就能再master上看到node1节点了: kubectl get node
在这里插入图片描述
11、现在node1已经完成,现在再配置node2就重复操作就行了,从第二十二步解压k8s-node.tar.gz开始重复操作,注意:修改kubelet.conf配置文件中的主机名的时候,现在是k8s-node2。

12、node2也部署好三个组件之后,再次在master上执行 kubectl get node如果能看到两个node已经加入到k8s集群就说明你的K8S搭建没问题了。至于为什么是NotReady,因为现在我们没有把k8s内部网络打通,外部无论如何都访问不到node上的pod服务,所以是NotReady,等k8s网络打通了,这里就是Ready了,还好k8s已经帮我们提前想好了打通内部网络的解决方案。
在这里插入图片描述
13、查看两台node日志比如看kubelet组件:下图都提示没有在对应目录下找到cni网络插件。所以下一步就是给两台node安装网络插件,让上面的STATUS变为Ready。
在这里插入图片描述

二十三、给node安装网络插件(也是只示例node1):

1、确认node上是否启用CNI插件:在node上执行 grep "cni" /opt/kubernetes/cfg/kubelet.conf(出现下图表示已启动)
在这里插入图片描述
2、在node上创建两个目录,然后解压之前解压k8s-node.tar.gz生成的cni插件压缩包到bin下:

mkdir-p/opt/cni/bin/etc/cni/net.d
tar-zxvf cni-plugins-linux-amd64-v0.8.2.tgz-C/opt/cni/bin/

3配置docker国内镜像源,因为node节点是要跑pod的,肯定是要拉docker镜像的,pod是自动跑,我们只需在master上执行yaml文件或创建控制器。我们要修改docker镜像源为国内,要不然拉在线镜像太慢了。修改方式很简单只需要两步:修改两台node机上的 vi /etc/docker/daemon.json,然后全部删了修改成你的docker镜像源地址,我是用的自己的阿里云加速镜像地址,你可以设置网易163的如下。保存退出,执行 systemctl daemon-reload && systemctl restart docker,记住以后凡是重启K8S中配置文件被修改过的组件都要加reload使配置生效再restart。

{"registry-mirrors":["http://hub-mirror.c.163.com"]}

4一定要退到第1步把node2也做好才走这一步执行yaml文件现在在master机上!!执行yaml脚本kube-flannel.yaml,实现在node上安装和启动网络插件功能。首先把网盘中的 YAML上传到master的/root下。cd进YAML执行 kubectl apply -f kube-flannel.yaml命令,k8s中启动pod的方式之一就是执行yaml文件,也是我们最最常用的方式,这个yaml文件中就可以指定把pod启动在哪个名称空间以及pod暴漏的端口。这个命令的作用就是让两个node下载镜像,启动容器。这一步跟你自己在两台node上docker pull这个yaml文件中的镜像下来run是一个效果,只不过yaml文件能在master直接控制node启动pod以及怎么启动。

5、在master上执行 kubectl get pods -n kube-system查看namespace为kube-system(这个yaml文件指定的)的所有pod,这里就会出现两个pod,一个是node1上的一个是node2上的,这两个pod就是我们说的cni网络插件。如果yaml文件执行成功,两个pod的STATUS就会变为Running,否则为Pending。再次执行 kubectl get node查看集群中的node状态,就能看到两个节点已经变为了Ready。注意执行yaml文件后,在node上启动pod这个操作受限于网络。我们已经给两个node都换成国内163docker镜像源,所以基本一分钟就搞定了。用yaml文件来跑pod,如果要停止pod,要使用 kubectl delete -f kube-flannel.yaml删除,直接 kubectl delete pod -n 名称空间 pod名称是删不掉的。包括以后用创建控制器的方式来启动pod,也是删控制器才有用,比如: kubectl delete deployment 控制器名称,原因就是你删pod,会自动新建副本,这是k8s的容灾机制。成功后如下图:可以看到两个pod正常启动。除了get node之外,其它k8s的名词比如svc,pod,deployment等的get和delete都是需要指定名称空间的,不用-n指定名称空间会默认在default名称空间。

在这里插入图片描述

6、最后授权master的apiserver可以访问node上的kubelet,在master上执行如下命令:

kubectlapply-f apiserver-to-kubelet-rbac.yaml

二十四、小结:

这23步做完之后,包含一台master和两台node以及(跑在这三台机上的)三个etcd服务的K8S集群就搭建成功了,后续有异常的话有个简单的解决办法,重启master上的三个组件(kube-apiserver,kube-controller-manager,kube-scheduler),node上的三个组件(docker,kubelet,kube-proxy)。k8s说白了就是这六个组件在运作。

二十五、开始正式使用K8S集群,在node上跑pod:

1、K8S集群搭建成功之后,我们就不需要去管node节点了,一切都在master机上操作。本例中我们用master来调度集群中的两个node跑nginx的pod,并且实现K8S的滚动更新和回滚。nginx容器镜像版本使用1.7.9和1.8,前面我们修改docker的镜像源为国内163,我们就用官方的nginx镜像。

2、除了执行yaml文件可以启动pod,还有一种不常用的方式:手动创建控制器来启动pod,现在我们分别去两个node上pull下来nginx的1.8和1.7.9版本两个镜像。由于有加速地址,是很快就能拉下来的。然后再回到master执行 kubectl create deployment myweb --image=nginx:1.8,这个命令就相当于 kubectl create deployment myweb --image=nginx:1.8 -n default,这个命令的意思是创建了一个名叫myweb的deployment控制器,让它去找个node在默认名称空间default( 你也可以-n指定一个名称空间)启一个1.8版本nginx的pod。控制器除了deployment还有其它好几个,后面会单独说。然后过个一两分钟执行 kubectl get deployment,发现deployment启动了,执行 kubectl get pod发现pod也启动好了。
在这里插入图片描述
3、用 kubectl describe pod NAME,这个NAME就是执行kubectl get pod展示的第一列pod信息,上图开头的myweb就已经表示这个pod是由哪个名称空间的deploment启动的:下图能看到这个pod运行在node1上,这个查看pod详细信息的命令在以后pod出问题排错时会经常用到。为啥不是node2呢,这是scheduler自动按特定算法选出来的不是我们定的,具体流程上一篇博文详细讲过。你还可以继续 kubectl create deployment web1 --image=nginx:1.7.9再创建一个名为web1的控制器并且让它启动一个1.7.9版本nginx的pod,这里我们就先只启动一个。
在这里插入图片描述
4、我们最常用的就是这个kubectl,它可以get node,get deployment,get pod。最后两个命令的最后都可以加-n 来指定查看哪个名称空间,不指定的话pod的默认名称空间是 default,deployment的名称空间是 kube-system。上文有专门讲过名称空间作用。用 kubectl get ns可以查看现有的所有名称空间。

5、那我们怎么样能访问这个node1上pod跑的nginx页面呢?你看下图那个ip就是我们k8S给pod分配的ip,这个ip是个内网ip。回想一下我们用docker跑nginx的时候,是用 -p来让宿主机的某个端口来映射到nginx容器的80端口,达到我们访问nginx容器的目的。我们前面讲过服务发现,就是service和ingress,下面我们就用service的方式,在K8S中也是要暴露pod的端口到物理机。暴露名为myweb的控制器启动的pod上的80端口,因为nginx的默认端口是80,除非你改了nginx的配置文件,那我们这儿跟着改就行了。执行 kubectl expose deployment myweb --port=80 --type=NodePort,然后K8S就会把80映射到node的一个随机端口。用 kubectl get svc就能查看到底映射到了虚机的哪个端口(svc代表service):下图看到是30616
在这里插入图片描述
6、访问任意一台node的ip加30616端口,就能看到nginx的welcome页面了。为什么明明看到pod是跑在node1,node2也能访问,这就是集群,所有工作节点都在k8s集群中提供服务。k8s的node默认只能向外界暴漏的端口范围为 30000-32767,就算是随机端口也是在这个范围。所以以后你自己写yaml文件创建service指定暴漏端口来发布服务的时候要注意这个端口范围。

二十六、配置K8S的web管理页面:

1、K8S官方的 dashboard,不建议(这一步你可以不做):

先安装dashboard,在master上 cd YAML,然后 kubectl apply -f /root/YAML/dashboard.yaml,yaml文件方式启动pod会自动暴漏端口,k8s就会自动去node上下载镜像启动pod,这个pod的默认名称空间为kubernetes-dashboard。 kubectl get pod -n kubernetes-dashboard查看pod启动状态,再用 kubectl get svc -n kubernetes-dashboard来查看pod暴露到了node的哪个端口。因为我们在两个node上都修改了docker镜像源,随便它调度到哪个node都很快。所以不到一分钟pod就跑起来了。 在这里插入图片描述
在这里插入图片描述

最后也是用两个node的任意一个ip(也是要用109或104才行)的30001端口访问(记住一定要用https协议)

在这里插入图片描述

2、第三方的 kuboard,也是推荐的,功能更全面:把网盘中的kubectl apply -f start_kuboard.yaml文件拿到master上来。

3、然后就可以执行(k8s集群搭建好之后所有调度都在master上) kubectl apply -f start_kuboard.yaml,这个yaml文件中指定了在kube-system名称空间下,并且指定启动在node1上,如果你的主机名称不是k8s-node1需要进去该一下,执行完了之后就 kubectl get pod -n kube-system查看pod执行状态,还可以 kubectl describe pod -n kube-system pod的NAME确认到底是不是跑在我们规定的node1上。

4、最后:我们执行 kubectl get svc -n kube-system查看映射到了哪个端口,其实不用看,就是在yaml文件中就能看到,映射到了node1的32567端口。访问任意node节点的ip加32567端口:

在这里插入图片描述

5、显然,下一步我们就需要生成token完成登录。只需在master执行这个命令即可,会马上返回一个token,这个token就是我们的k8s令牌。

kubectl-n kube-system get secret $(kubectl-n kube-system get secret|grep kuboard-user|awk'{print $1}')-o go-template='{{.data.token}}'|base64-d

6、使用token完成登录:进来之后你就会看到有这四个名称空间, 是k8s默认创建好的。你执行yaml文件或创建控制器时用-n指定一个不存在的名称空间,k8s就会在这儿给你生成。我们前面用deployment的方式手动创建了一个名叫myweb的控制器,让它在default名称空间中(因为我们启动时没有指定名称空间)跑一个nginx的pod。你现在点default进去就能看到。

在这里插入图片描述

7、前面我们都是通过yaml文件或deployment来创建pod,现在有了图形页面我们还可以直接在页面上创建pod,并且kubeboard还支持像gitlab上的CI/CD持续集成,你的代码更新之后自动生成更新后的yaml文件,然后你再来k8s页面上删除pod,k8s就会重启一个pod达到更新代码功能的效果。

在这里插入图片描述
比如我们在web上用deployment来启动一个nginx的pod:
在这里插入图片描述
3、最常用的dashboard。直接在master上执行两个yaml文件交付两个pod上去, kubectl apply -f dashboard.yaml 和 dashboard-adminuser.yaml,就完成了dashboard图形化界面的部署。最后使用此命令来获取k8s的RBAC(基于角色权限管理)的token令牌: kubectl -n kubernetes-dashboard describe secret $(kubectl -n kubernetes-dashboard get secret | grep admin-user | awk '{print $1}')完成登录。用任意工作节点的ip加端口(用get svc来查看端口),用火狐浏览器来访问,一定要使用https协议。

在这里插入图片描述

二十七、部署master2。

1、最开始我们克隆出来的master2和两台LB还没有使用。现在就加一台master2来实现高可用。首先也是初始化master2,首先更改网卡配置文件中的IPADDR一项我是配置为192.168.0.64,重启网络,然后照我们之前的初始化六个步骤做,那个时间配置那儿,就不要照master1了,因为时间服务器一台就够了,照配置node来做,然后在master1上执行五条命令:

scp-r/opt/etcd root@k8s-master2:/opt
scp-r/opt/kubernetes/root@k8s-master2:/opt
cd/usr/lib/systemd/system
scp kube-apiserver.service kube-controller-manager.service kube-scheduler.service root@k8s-master2:/usr/lib/systemd/system
scp/bin/kubectl root@k8s-master2:/bin

2、回到master2节点,修改apiserver的配置文件: vi /opt/kubernetes/cfg/kube-apiserver.conf,只修改两行,把下图换成你的master2的ip。

--advertise-address=192.168.0.64\--service-cluster-ip-range=10.0.0.0/24\

3、在master2上启动三个组件,执行:

systemctl daemon-reloadsystemctl restart kube-apiserver
 systemctl enable kube-apiserver
systemctl restart kube-controller-manager
systemctl enable kube-controller-manager
systemctl restart kube-scheduler
systemctl enable kube-scheduler

4、检查是否启动三个组件成功:ps -aux | grep kube。最直观就是这个命令: kubectl get node,如果你在这台master2也能get到node1和node2,就可以了,代表,你的这个master2已经作为主节点加入到K8S集群中。
在这里插入图片描述

二十八、安装dns组件

1、k8s内部的pod之间访问是用内网ip,但是pod的ip是不定的,当pod被重启后,ip是会变化的。为了解决这个问题,我们前面引入了label标签来关联上service,通过固定集群service的ip来让接入点的ip稳定,但是这样还是不够自动化,我能不能不要serviceip,要service名称就可以呢?如何自动关联上service和集群网络的ip呢?你马上想到我们常用的dns,它能把baidu.com解析为39.156.69.79这个ip。但是我们现在是想实现集群内部的service名称跟集群ip对应起来,比如service1能解析为192.168.0.65这个ip,并调度到映射的pod端口上实现服务发现。k8s中用来实现service名称解析的就是这个CoreDNS插件。
2、在master1上 kubectl apply -f coredns.yaml
在这里插入图片描述

二十九、node管理k8s集群

1、默认情况下,k8s只能在master上来管理,来执行命令。get信息,create控制器,apply我们的yaml文件。这些操作都只能在master上。node上执行不了,根本原因就是:node上没有kubectl这个命令。那我就把它scp到node1下

scp/bin/kubectl root@k8s-node1:/bin

2、结果执行node1提示localhost:8080拒绝访问:
在这里插入图片描述
3、再回到master上:
a、颁发admin证书

cd TLS/k8s
vi admin-csr.json
cfssl gencert-ca=ca.pem-ca-key=ca-key.pem-config=ca-config.json-profile=kubernetes admin-csr.json|cfssljson-bare admin

b、创建kubeconfig文件,设置集群参数,然后就会再本目录下生成admin.pem

kubectl configset-cluster kubernetes \--server=https://192.168.0.63:6443\--certificate-authority=ca.pem \--embed-certs=true \--kubeconfig=config

在这里插入图片描述

c、设置客户端认证参数

kubectl configset-credentials cluster-admin \--certificate-authority=ca.pem \--embed-certs=true \--client-key=admin-key.pem \--client-certificate=admin.pem \--kubeconfig=config

在这里插入图片描述
d、设置上下文参数,这一步做完之后就会生成一个名叫config的文件。

kubectl configset-context default \--cluster=kubernetes \--user=cluster-admin \--kubeconfig=config

在这里插入图片描述
再执行:

kubectl config use-context default--kubeconfig=config

e、把config文件发送到node上:

scp config root@k8s-node1:/root

f、在node节点基于config实现执行kubectl命令:

kubectl get node--kubeconfig=config

在这里插入图片描述
但是这样指定config太麻烦,我想想再master一直方便。直接在node上执行: mv /root/config /root/.kube,就可以不加–指定也可以执行了。
在这里插入图片描述
这个config是比较重要的,任何人拿到你这个文件,就都可以管理你的整个k8s集群了,里面有你的master的ip以及刚刚生成的访问密钥。

面试:你对于devops的了解?

1、devops自动化:python docker+k8s git+jenkins。
2、k8s >> docker三剑客(compose做容器编排来启动容器,swam容器具体启动到那个node,主机数量不够通过machine来加node)。
3、pod与pod控制器:一个pod是一个或多个容器,控制器用来启动、管理pod数量和状态。
创建控制器的方式: kubectl run pod名称 --images=nginx:1.8 --replicas=1 -n 名称空间kubectl create 控制器类型 控制器名称 --images=nginx:1.8 --replicas=1 -n 名称空间(run是在集群跑pod,create是在集群创建资源,实际也是跑pod);通过kuboard的web端来创建;通过 kubectl apply -f yaml文件创建deployment,然后再写一个yaml文件来运行,就可以创建service对象来映射pod的端口。其实完全可以在kuboard上操作,创建pod之后有自动生成的yaml文件,创建service之后也有yaml文件。那两个yaml文件你拿来手动apply,跟你在board运行是一个效果。

4、service干什么用的:因为pod地址容易发生改变,通过service可以为外界提供一个统一的pod入口,端口映射。通常来说一类pod就有一个service。
5、查看k8s对象状态(pod故障分析): kubectl get 资源类型,可能有node,pod,svc,deployment,结尾加-o wide可以查看控制器或pod具体调度到哪个node,但没有 kubectl describe pod pod名详细。还有一些选项: -n(不指定默认名称空间为default) ; -A显示所有名称空间(常用) ;最后除了查看组件日志,还能像查看某个docker容器日志一样 docker logs 容器名称或ID查看具体pod的日志信息: kubectl logs pod名称。也能像docker进入容器一样进入pod: kubectl exec -it pod名称 bash,还可以 -l指定看哪些标签的资源,如app=nginx
6、服务伸缩:根据客户端的请求流量,改副本数,重新apply一下yaml文件。还可以自动弹性伸缩,
在这里插入图片描述
7、滚动更新:先启动新的pod再杀死之前版本的pod,创建一个,杀死一个。比如你想把之前的nginx1.8版本更新为nginx1.7,就改一下yaml文件中的nginx的版本号,再apply一下,就实现更新了。怎么滚回1.8呢

8、先创建pod,再检查pod是否启动正常,再发布,再伸缩,再滚动更新

项目迁移到k8s通用流程

注意:在k8s环境中,开发交付的是 镜像,而不是程序源代码。
1、制作镜像:大部分是基于Dockerfile文件。
2、将镜像启动为pod。
3、暴露pod,让内部pod可以互相访问。
4、对外发布应用。
5、监控和日志收集

镜像类别(三类):
镜像作用:一个镜像就是一个服务。基础镜像:用官方centos,不需要做;运行环境镜像:在容器里跑python,java代码,以及为项目提供运行环境;项目镜像:这三类镜像的最终版,最后整合成这个项目镜像。

规划:
master:192.168.0.63
node:192.168.0.65 192.168.0.66

五大Kubernetes最佳实践_Docker的专栏-CSDN博客

$
0
0

640?wxfrom=5&wx_lazy=1

在最近的一次Weave用户组在线会议WOUG[1]上两个工程师做了Kubernetes相关的分享。
谷歌云的开发者布道师Sandeep Dinesh(@SandeepDinesh)做了一个演讲,给大家列举了在Kubernetes上运行应用的最佳实践清单;Jordan Pellizzari(@jpellizzari),是来自Weaveworks的工程师,随后也做了一个分享,内容是在他们使用Kubernetes开发运行SaaS Weave Cloud两年之后学到的经验教训。


Kubernetes最佳实践

640?wxfrom=5&wx_lazy=1


这篇演讲中的最佳实践来源于Sandeep和团队进行的关于在Kubernetes上以多种不同方式运行同一任务的讨论。他们把讨论的结果总结为一个最佳实践的清单。
这些最佳实践被分成以下类别:
  1. 构建容器

  2. 容器内部

  3. 部署

  4. 服务

  5. 应用架构


1、构建容器

640


不要信任任意的基础镜像
不幸的是我们看到这个错误一直在发生, Pradeep说到。人们从DockerHub上随便拉一个某人做的基础镜像——这么做的理由仅仅是第一眼看过去这个镜像里面打包有他们需要的包——接着他们就把这个随便选的镜像推到生产环境中。
这么做是非常错误的:你使用的代码可能有很多漏洞,bug,错误版本,或者本身就被人有意把恶意软件打包进去——只是你不知道罢了。
要减轻这种风险,你可以使用静态分析工具,比如CoreOS’ Clair[2]或者Banyon Collector[3]来对容器进行漏洞扫描。
保持基础镜像尽量小
基于最简洁的可用基础镜像,然后基于它构建软件包,这样你就知道镜像里面到底有哪些东西。
越小的基础镜像开销也越小。你的应用可能只要5M, 但是如果你盲目的随便找一个镜像,比如Node.js, 它里面就包括了额外500M你根本要不到的库文件。
使用小镜像的其它优势有:
  • 快速构建

  • 节约存储

  • 拉去镜像更快

  • 更小的潜在攻击面


640


使用构建器模式
这种模式对静态语言特别有用,编译类似Go,C++或者Typescript for Node.js这些语言时。
在这种模式里你有一个构建容器,里面打包有编译器,依赖包,以及单元测试。 代码通过第一步之后产出构建的artifacts,这包括所有的文件,bundles等。然后再通过一个运行时容器,包括有监控和调试工具等。
到最后, 你的Dockerfile里面将会只包含你的基础镜像以及运行时环境容器。

640


2、容器内部

640


在容器的内部使用非root用户
如果你在容器内使用root来更新包,那么你要把用户改成非root用户。
原因很简单,如果你的容器有后门被人利用了而且你还没把它的用户改成root之外的,那么一个简单的容器逃离将会导致你整个主机的root权限都被利用。但是如果你改成了非root用户,黑客就没那么容易得到root用户的权限了。
做为最佳实践,你要对你的基础设施加多层外壳保护。
在Kubernetes里面你可以通过设置安全上下文[4]runAsNonRoot: true来实现,这样会对整个集群cluster来生效。
文件系统只读
这一个最佳实践通过设置readOnlyFileSystem: true来实现。
每个容器里面跑一个进程
你当然可以在一个容器里面跑多个进程,但是推荐跑一个。这是由编排器的工作方式决定的。Kubernetes基于一个进程是否健康来管理容器。如果你在一个容器里面有20个进程,它如何知道容器是否健康呢?
不要使用 Restart on Failure, 而应当 Crash Cleanly
Kubernetes会重新启动失败的容器,因此你应该干净的做崩溃退出(给出一个错误码),这样Kubernetes就可以不用你的人工干预来成功重起了。
日志打到标准输出和标准错误输出(stdout & stderr)
Kubernetes缺省会监听这些管道,然后将输出传到日志服务上面去。在谷歌云上可以直接用StackDriver日志系统。
3、部署

640


使用record选项来使回滚更方便
在引用一个yaml文件时,请使用--record选项:
kubectlapply-fdeployment.yaml--record        

带了这个选项之后,每次升级的时都会保存到部署的日志里面,这样就提供了回滚一个变更的能力。

640


多使用描述性的标签label
因为标签可以是任意的键值对,其表达力非常强。参考下图,以有名字为'Nifty‘的应用部署到四个容器里面。 通过选择BE标签你可以挑选出后端容器。

640


使用sidecar来做代理、监视器等
有时候你需要一组进程跟其它某个进程通讯。但是你又不希望把它们所有的都放进一个容器里面(前面提到的一个容器跑一个进程), 你希望的是把相关的进程都放到一个Pod里面。
常见情况是你需要运行进程依赖的一个代理或者监视器,比如你的进程依赖一个数据库, 而你不希望把数据库的密码硬编码进每个容器里面,这个时会你可以把密码放到一个代理程序里面当作sidecar,由它来管理数据库连接:

640


不要使用sidecar来做启动引导
尽管sidecar在处理集群内外的请求时非常有用,Sandeep不推荐使用它做启动。再过去,引导启动(bootstraping)是唯一选项,但是现在Kubernetes有了“init 容器”。
当容器里面的一个进程依赖于其它的一个微服务时, 你可以使用init容器来等到进程启动以后再启动你的容器。这可以避免当进程和微服务不同步时产生的很多错误。
基本原则就是: 使用sidecar来处理总是发生的事件,而用init容器来处理一次性的事件。
不要使用:latest或者无标签
这个原则是很明显的而且大家基本都这么在用。如果你不给你的容器加标签,那么它会总是拉最新的,这个“最新的”并不能保证包括你认为它应该有的那些更新。
善用readiness、liveness探针
使用探针可以让Kubernetes知道节点是否正常,以此决定是否把流量发给它。缺省情况下Kubernetes检查进程是否在运行。但是通过使用探针, 你可以在缺省行为下加上你自己的逻辑。

640


4、服务

640


不要使用type: Loadbalancer
每次你在部署文件里面加一个公有云提供商的loadbalancer(负载均衡器)的时候,它都会创建一个。 它确实是高可用,高速度,但是它也有经济成本。
使用Ingress来代替,同样可以实现通过一个end point来负载均衡多个服务。这种方式不但更简单,而且更经济。当然这个策略只有你提供http和web服务时有用,对于普通的TCP/UDP应用就没用了。

640


Type: Nodeport可能已经够用了
这个更多是个人喜好,并不是所有人都推荐。NodePort把你的应用通过一个VM的特定端口暴露到外网上。 问题就是它没有像负载均衡器那样有高可用。比如极端情况,VM挂了你的服务也挂了。
使用静态IP, 它们免费!
在谷歌云上很简单,只需要为你的ingress来创建全局IP。类似的对你的负载均衡器可以使用Regional IP。这样当你的服务down了之后你不必担心IP会变。
将外部服务映射到内部
Kubernetes提供的这个功能不是所有人都知道。如果您需要群集外部的服务,您可以做的是使用ExternalName类型的服务。这样你就可以通过名字来调用这个服务,Kubernetes manager会把请求传递给它,就好像它在集群之中一样。Kubernetes对待这个服务就好像它在同一个内网里面,即使实际上它不在。
5、应用架构

640


使用Helm Charts
Helm基本上就是打包Kubernetes应用配置的仓库。如果你要部署一个MongoDB, 存在一个预先配置好的Helm chart,包括了它所有的依赖,你可以十分容易的把它部署到集群中。
很多流行的软件/组件都有写好了的Helm charts, 你可以直接用,省掉大量的时间和精力。
所有下游的依赖是不可靠的
你的应用应该有逻辑和错误信息负责审计你不能控制的所有依赖。Sandeep建议说你可以使用Istio或者Linkerd这样的服务网格来做下游管理。
使用Weave Cloud
集群是很难可视化管理的。 使用Weave Cloud[5]可以帮你监视集群内的情况和跟踪依赖。
确保你的微服务不要太“微小”
你需要的是逻辑组件,而不是每个单独的功能/函数都变成一个微服务。
使用命名空间来分离集群
例如, 你可以在同一个集群里面创建prod、dev、test这样不同的命名空间,同时可以对不同的命名空间分配资源, 这样万一某个进程有问题也不会用尽所有的集群资源。
基于角色的访问控制RBAC
实施时当的访问控制来限制访问量, 这也是最佳的安全实践。
从运行Weave Cloud生产环境学到的教训

640


接下来Jordan Pellizzari做了一个演讲,题目是在过去两年我们在Kubernetes上开发运行Weave Cloud学到的经验。 我们当前运行在AWS EC2上, 总共有72个Kubernetes部署运行在13个主机和150个容器里面。我们所有的持续性存储保存在S3,DynamoDB或者RDS里面, 我们并不在容器里面保存状态信息。 关于我们如何搭建基础设施的细节可以参看这篇文档[6]。
挑战1:对基础设施做版本控制
在Weaveworks我们把所有的基础架构保存在Git中, 如果我们要对基础设施做变更,要像代码一样提Pull request。我们把这称为GitOps,也写了多篇博文。你可以从这篇读起: GitOps - Pull Request支撑的运维[7]。
在Weave, 所有的Terraform脚本,Ansible以及Kubernetes YAML文件都被保存在Git里面做版本控制。
把基础架构放在Git里面是一个最佳实践,这有多个原因:
  • 发布可以很方便的回滚

  • 对谁做了什么修改有追踪审计

  • 灾难恢复相当简单


问题: 当生产与版本控制不一致时该怎么办?
除了把所有内容保存在Git中之外,我们也有一个流程会检查生产集群中运行的状态与版本控制中的内容差异。如果检查到有不同,就会给我们的Slack频道发一个报警。
我们使用一个叫Kube-Diff[8]的开源工具来检查不同。
挑战2:自动化的持续交付
自动化你的CI/CD流水线,避免手工的Kubernetes部署。因为我们一天内做多次部署,这种方式节约了团队的时间也避免了手工容易发生错误的步骤。在Weaveworks,开发人员只需要做一个Git push,然后Weave Cloud会做以下的事情:
  • 打过标签的代码通过CircleCI的测试然后构建一个新的容器镜像,推送这个新的镜像到仓库中。

  • Weave Cloud的“Deploy Automator‘检测到新镜像,从库中拉取新镜像然后在配置库里面更新对应的YAML文件。

  • Deploy Synchronizer会检测到集群需要更改in了,然后它会从配置库里面拉更新的配置清单,最后将新的镜像部署到集群中。


GitOps流水线


640


这里有一篇稍长的文章[9],我们认为的构建自动化CI/CD流水线的最佳实践都在里面描述了。


总结

640


Sandeep Dinesh做了一个关于创建、部署、运行应用到Kubernetes里面的五个最佳实践的深度分享。随后Jordan Pellizzari做了Weave如何在kubernetes中管理SaaS产品Weave Cloud和经验教训的分享。

相关链接:
  1. https://www.meetup.com/pro/weave/

  2. https://github.com/coreos/clair

  3. https://github.com/banyanops/collector

  4. https://kubernetes.io/docs/tasks/configure-pod-container/security-context/

  5. https://www.weave.works/features/troubleshooting-dashboard/

  6. https://www.weave.works/technologies/weaveworks-on-aws/

  7. https://www.weave.works/blog/gitops-operations-by-pull-request

  8. https://github.com/weaveworks/kubediff

  9. https://www.weave.works/blog/the-gitops-pipeline


原文链接:https://dzone.com/articles/top-5-kubernetes-best-practices-from-sandeep-dines




K8s网络插件flannel与calico - 小雨淅淅o0 - 博客园

$
0
0

  Kubernetes的网络通信问题:
  1. 容器间通信: 即同一个Pod内多个容器间通信,通常使用loopback来实现。
  2. Pod间通信: K8s要求,Pod和Pod之间通信必须使用Pod-IP 直接访问另一个Pod-IP
  3. Pod与Service通信: 即PodIP去访问ClusterIP,当然,clusterIP实际上是IPVS 或 iptables规则的虚拟IP,是没有TCP/IP协议栈支持的。但不影响Pod访问它.
  4. Service与集群外部Client的通信,即K8s中Pod提供的服务必须能被互联网上的用户所访问到。

需要注意的是,k8s集群初始化时的service网段,pod网段,网络插件的网段,以及真实服务器的网段,都不能相同,如果相同就会出各种各样奇怪的问题,而且这些问题在集群做好之后是不方便改的,改会导致更多的问题,所以,就在搭建前将其规划好。

CNI(容器网络接口):
  这是K8s中提供的一种通用网络标准规范,因为k8s本身不提供网络解决方案。
  目前比较知名的网络解决方案有:
    flannel
    calico
    canel
    kube-router
    .......
等等,目前比较常用的时flannel和calico,flannel的功能比较简单,不具备复杂网络的配置能力,calico是比较出色的网络管理插件,单具备复杂网络配置能力的同时,往往意味着本身的配置比较复杂,所以相对而言,比较小而简单的集群使用flannel,考虑到日后扩容,未来网络可能需要加入更多设备,配置更多策略,则使用calico更好
所有的网络解决方案,它们的共通性:
  1. 虚拟网桥
  2. 多路复用:MacVLAN
  3. 硬件交换:SR-IOV(单根-I/O虚拟网络):它是一种物理网卡的硬件虚拟化技术,它通过输出VF(虚拟功能)来将网卡虚拟为多个虚拟子接口,每个VF绑定给一个VM后,该VM就可以直接操纵该物理网卡。

kubelet来调CNI插件时,会到 /etc/cni/net.d/目录下去找插件的配置文件,并读取它,来加载该插件,并让该网络插件来为Pod提供网络服务。

flannel网络插件要怎么部署?
 1. flannel部署到那个节点上?
  因为kubelet是用来管理Pod的,而Pod运行需要网络,因此凡是部署kubelet的节点,都需要部署flannel来提供网络,因为kubelet正是通过调用flannel来实现为Pod配置网络的(如:添加网络,配置网络,激活网络等)。

 2. flannel自身要如何部署?
  1》它支持直接运行为宿主机上的一个守护进程。
  2》它也支持运行为一个Pod
  对于运行为一个Pod这种方式:就必须将flannel配置为共享当前宿主机的网络名称空间的Pod,若flannel作为控制器控制的Pod来运行的话,它的控制器必须是DaemonSet,在每一个节点上都控制它仅能运行一个Pod副本,而且该副本必须直接共享宿主机的网络名称空间,因为只有这样,此Pod才能设置宿主机的网络名称空间,因为flannel要在当前宿主机的网络名称空间中创建CNI虚拟接口,还要将其他Pod的另一半veth桥接到虚拟网桥上,若不共享宿主机的网络名称空间,这是没法做到的。

3. flannel的工作方式有3种:
  1) VxLAN:
   而VxLAN有两种工作方式:
    a. VxLAN: 这是原生的VxLAN,即直接封装VxLAN首部,UDP首部,IP,MAC首部这种的。
    b. DirectRouting: 这种是混合自适应的方式, 即它会自动判断,若当前是相同二层网络
       (即:不垮路由器,二层广播可直达),则直接使用Host-GW方式工作,若发现目标是需要跨网段
       (即:跨路由器)则自动转变为使用VxLAN的方式。
  2) host-GW: 这种方式是宿主机内Pod通过虚拟网桥互联,然后将宿主机的物理网卡作为网关,当需要访问其它Node上的Pod时,只需要将报文发给宿主机的物理网卡,由宿主机通过查询本地路由表,来做路由转发,实现跨主机的Pod通信,这种模式带来的问题时,当k8s集群非常大时,会导致宿主机上的路由表变得非常巨大,而且这种方式,要求所有Node必须在同一个二层网络中,否则将无法转发路由,这也很容易理解,因为如果Node之间是跨路由的,那中间的路由器就必须知道Pod网络的存在,它才能实现路由转发,但实际上,宿主机是无法将Pod网络通告给中间的路由器,因此它也就无法转发理由。
  3) UDP: 这种方式性能最差的方式,这源于早期flannel刚出现时,Linux内核还不支持VxLAN,即没有VxLAN核心模块,因此flannel采用了这种方式,来实现隧道封装,其效率可想而知,因此也给很多人一种印象,flannel的性能很差,其实说的是这种工作模式,若flannel工作在host-GW模式下,其效率是非常高的,因为几乎没有网络开销。

4. flannel的网络配置参数:
  1) Network: flannel使用的CIDR格式的网络地址,主要用于为Pod配置网络功能。
   如: 10.10.0.0/16 --->
    master: 10.10.0.0/24
    node01: 10.10.1.0/24
    .....
    node255: 10.10.255.0/24

  2) SubnetLen: 把Network切分为子网供各节点使用时,使用多长的掩码来切分子网,默认是24位.
  3) SubnetMin: 若需要预留一部分IP时,可设置最小从那里开始分配IP,如:10.10.0.10/24 ,这样就预留出了10个IP
  4) SubnetMax: 这是控制最多分配多个IP,如: 10.10.0.100/24 这样在给Pod分配IP时,最大分配到10.10.0.100了。
  5) Backend: 指定后端使用的协议类型,就是上面提到的:vxlan( 原始vxlan,directrouter),host-gw, udp

flannel的配置:
  .....
  net-conf.json: |
    {
     "Network": "10.10.0.0/16",
     "Backend": {
     "Type": "vxlan",    #当然,若你很确定自己的集群以后也不可能跨网段,你完全可以直接设置为 host-gw.
     "Directrouting": true  #默认是false,修改为true就是可以让VxLAN自适应是使用VxLAN还是使用host-gw了。
     }
    }

#在配置flannel时,一定要注意,不要在半道上,去修改,也就是说要在你部署k8s集群后,就直接规划好,而不要在k8s集群已经运行起来了,你再去修改,虽然可能也不会出问题,但一旦出问题,你就!!

  

  #在配置好,flannel后,一定要测试,创建新Pod,看看新Pod是否能从flannel哪里获得IP地址,是否能通信。

 

Calico:
  Calico是一种非常复杂的网络组件,它需要自己的etcd数据库集群来存储自己通过BGP协议获取的路由等各种所需要持久保存的网络数据信息,因此在部署Calico时,早期是需要单独为Calico部署etcd集群的,因为在k8s中,访问etcd集群只有APIServer可以对etcd进行读写,其它所有组件都必须通过APIServer作为入口,将请求发给APIServer,由APIServer来从etcd获取必要信息来返回给请求者,但Caclico需要自己写,因此就有两种部署Calico网络插件的方式,一种是部署两套etcd,另一种就是Calico不直接写,而是通过APIServer做为代理,来存储自己需要存储的数据。通常第二种使用的较多,这样可降低系统复杂度。
  当然由于Calico本身很复杂,但由于很多k8s系统可能存在的问题是,早期由于各种原因使用了flannel来作为网络插件,但后期发现需要使用网络策略的需求,怎么办?
  目前比较成熟的解决方案是:flannel + Calico, 即使用flannel来提供简单的网络管理功能,而使用Calico提供的网络策略功能。


Calico网络策略:

  

  Egress:是出站的流量,即自己是源,远端为服务端,因此我自己的源IP可确定,但端口不可预知, 目标的端口和IP都是确定的,因此to 和 ports都是指目标的IP和端口。
  Ingress:是入站的流量,即自己为目标,而远端是客户端,因此要做控制,就只能对自己的端口 和 客户端的地址 做控制。
我们通过Ingress 和 Egress定义的网络策略是对一个Pod生效 还是 对一组Pod生效?
这个就要通过podSelector来实现了。
而且在定义网络策略时,可以很灵活,如:入站都拒绝,仅允许出站的; 或 仅允许指定入站的,出站都允许等等。
另外,在定义网络策略时,也可定义 在同一名称空间中的Pod都可以自由通信,但跨名称空间就都拒绝。

网络策略的生效顺序:
  越具体的规则越靠前,越靠前,越优先匹配

网络策略的定义:
  kubectl explain networkpolicy
  spec:
   egress: <[]Object> :定义出站规则
   ingress: <[]Object>: 定义入站规则
   podSelector: 如论是入站还是出站,这些规则要应用到那些Pod上。
   policyType:[Ingress|Egress| Ingress,Egress] :
     它用于定义若同时定义了egress和ingress,到底那个生效?若仅给了ingress,则仅ingress生效,若设置为Ingress,Egress则两个都生效。
        注意:policyType在使用时,若不指定,则当前你定义了egress就egress生效,若egress,ingress都定义了,则两个都生效!!
    还有,若你定义了egress, 但policyType: ingress, egress ; egress定义了,但ingress没有定义,这种要会怎样?
    其实,这时ingress的默认规则会生效,即:若ingress的默认规则为拒绝,则会拒绝所有入站请求,若为允许,则会允许所有入站请求,
    所以,若你只想定义egress规则,就明确写egress !!

  egress:<[]Object>
      ports: <[]Object> :因为ports是有端口号 和 协议类型的,因此它也是对象列表
       port :
    protocol: 这两个就是用来定义目标端口和协议的。
   to :<[]Object>
       podSelector: <Object> : 在控制Pod通信时,可控制源和目标都是一组Pod,然后控制这两组Pod之间的访问。
      ipBlock:<[]Object> : 指定一个Ip地址块,只要在这个IP范围内的,都受到策略的控制,而不区分是Pod还是Service。
      namespaceSelector: 这是控制对指定名称空间内的全部Pod 或 部分Pod做访问控制。

  Ingress:
      from: 这个from指访问者访问的IP
      ports: 也是访问者访问的Port

复制代码
#定义网络策略:
vim   networkpolicy-demo.yaml
apiVersion:  networking.k8s.io/v1  
    #注意:虽然kubectl  explain networkpolicy中显示为 extensions/v1beta1 ,但你要注意看说明部分.
kind:  NetworkPolicy
metadata:
     name:  deny-all-ingressnamespace:  dev
spec:
     podSelector:  {}  #这里写空的含义是,选择指定名称空间中所有Pod
     policyTypes:-Ingress        #这里指定要控制Ingress(进来的流量),但又没有指定规则,就表示全部拒绝,只有明确定义的,才是允许的。
                       #egress: 出去的流量不控制,其默认规则就是允许,因为不关心,所以爱咋咋地的意思。
    
#写一个简单的自主式Pod的定义:
vim  pod1.yaml
    apiVersion:  v1
    kind: Pod
    metadata: 
      name:  pod1
    spec:
      containers:-name:  myapp
        image:  harbor.zcf.com/k8s/myapp:v1

#创建dev名称空间,并应用规则
kubectl apply-f networkpolicy-demo.yaml -n dev

# kubectl describe-n dev networkpolicies
    Name:         deny-all-ingress
    Namespace:    dev
   ........................
    Spec:
      PodSelector:<none> (Allowing the specific traffic to all podsinthisnamespace)
      Allowing ingress traffic:<none> (Selected pods are isolatedforingress connectivity)
      Allowing egress traffic:<none> (Selected pods are isolatedforegress connectivity)


#查看dev名称空间中的网络规则:
kubectlgetnetworkpolicy -n dev     
 或   
 kubectlgetnetpol  -n  dev

#然后在dev 和 prod 两个名称空间中分别创建pod
kubectl  apply-f  pod1.yaml   -n   dev
kubectl  apply-f  pod1.yaml   -n   prod

#接着测试访问这两个名称空间中的pod
kubectlgetpod  -n  dev   -o   wide
    
    #测试访问:
        curl   http://POD_IPkubectlgetpod  -n  prod   -o   wide

    #测试访问:
        curl   http://POD_IP#通过以上测试,可以看到,dev名称空间中的pod无法被访问,而prod名称空间中的pod则可被访问。
复制代码
复制代码
#测试放行所有dev的ingress入站请求。
# vim  networkpolicy-demo.yaml
    apiVersion:  networking.k8s.io/v1   
    kind:  NetworkPolicy
    metadata:
       name: allow-all-ingressnamespace:  dev
    spec:
       podSelector:  {}
       ingress:-{}      #这就表示允许所有,因为定义了规则,但规则是空的,即允许所有。
       policyTypes:-Ingress

#接着测试,和上面测试一样,也是访问dev 和 prod两个名称空间中的pod,若能访问,则成功。
# kubectl describe-n dev netpol
    Name:         deny-all-ingress
    Namespace:    dev
    .....................
    Spec:
      PodSelector:<none> (Allowing the specific traffic to all podsinthisnamespace)
      Allowing ingress traffic:
        To Port:<any>(traffic allowed to all ports)
        From:<any>(traffic not restricted by source)
      Allowing egress traffic:<none> (Selected pods are isolatedforegress connectivity)
      Policy Types: Ingress
复制代码

  #测试定义一个仅允许访问dev名称空间中,pod标签 app=myapp 的一组pod的80端口

  

复制代码
#先给pod1打上app=myapp的标签
#kubectl label pod pod1 app=myapp -n dev
    
vim  allow-dev-80.yaml
 apiVersion: networking.k8s.io/v1
 kind: NetworkPolicy
 metadata:
    name: allow-myapp-ingress
 spec:
   podSelector:
      matchLabels:
        app: myapp
     ingress:-from:-ipBlock:
            cidr:10.10.0.0/16except:-10.10.1.2/32ports:-protocol:  TCP
         port:88-protocol:  TCP
         port:443#查看定义的ingress规则
kubectlgetnetpol  -n  dev

#然后测试访问 dev 名称空间中的pod
curl  http://Pod_IPcurl  http://Pod_IP:443curl   http://Pod_IP:88
复制代码

复制代码
上图测试:1. 先给dev名称空间打上标签
  kubectl   labelnamespacedev   ns=dev2. 编写网络策略配置清单
  vim  allow-ns-dev.yaml
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: allow-ns-dev
    spec:
      podSelector: {}
      ingress:-from:-namespaceSelector:
              matchLabels:
                ns: dev
      egress:-to:-namespaceSelector:
             matchLabels:
               ns: dev



#要控制egress,也是如此,只是将ingress替换为egress即可,然后在做测试。
另外,关于网络策略,建议:
 名称空间内:
     拒绝所有出站,入站流量
     仅放行出站目标为当前名称空间内各Pod间通信,因为网络策略控制的颗粒度是Pod级别的,不是名称空间级别。
     具体的网络策略,要根据实际需求,来定义ingress 和 egress规则。
复制代码

 

库克称未来iPhone不再消耗地球资源:采用回收再利用材料生产

$
0
0

目前,环保问题收到全球关注,而对于更新换代频繁的电子设备来说,不少厂商已经开始对这么淘汰的电子废弃物进行回收再利用。

据媒体报道,日前,在VivaTech 2021峰会上, 苹果CEO蒂姆·库克(Tim Cook)表示,未来新iPhone的生产不再消耗地球资源,将依靠可再生资源制造。

库克表示,苹果公司目前已经是碳中和企业,并且在几年前就已经做到了这一点。现在要做的是进一步拓展人们对碳中和的理解,并希望在2030年前实现从供应端到用户端的整条链的碳中和化。

为了实现该愿景,苹果未来将使用100%的可再生资源,并且现在也在影响供应商完全使用可再生资源。令人欣慰的是,如今已经有超过一百家供应商承诺这么做,这就像在池塘里激起的一片涟漪,影响将会越来越大。

值得一提的是, 苹果已经定了一个目标,在未来生产新iPhone中不在消耗地球上的任何东西,虽然目前还没没有实现,但是已经取得了成效。例如Mac电脑上40%的铝都是回收再利用的,iPhone 12中98%的稀土也都是回收再利用的。

苹果已经在新、旧、二手三类产品之间建立了一个闭环,通过使用机器人把旧iPhone上的可再利用的东西换到新iPhone的相关部件上。

据悉,今年3月,苹果CEO库克发微博称,水是世界上最珍贵的资源之一,我们将继续竭尽全力,帮助我们的供应商和合作伙伴来保护水资源和节水。

此外,2020年10月,苹果曾宣布, 出于环保考虑,iPhone 12系列手机将不再附送充电器。不过,对此大多数网友并不买账,觉得苹果只不过是在节省成本。

文章

为了效率不应该做的7件事

$
0
0

把自己弄得忙忙碌碌,但回头看碌碌无为。无意看到这篇文章,感觉对自己非常有用,记录下来自勉。

设想一下有一个不停工作的小业务员,努力工作并不能帮助他战胜成千上万的竞争对手。 时间是有限的商品。一个企业家最多可以每周7天每天工作24小时,他的竞争对手可以花更多的钱,建立一个更大的团队,花更多的时间在这个项目上。但是为什么有一些小的初创公司完成了大公司不能完成的事情?Facebook花了10亿美元收购了一个只有13名员工的Instagram。Snapchat,一家只有30名员工的年轻初创公司拒绝了莱斯Facebook和Google的收购。他们成功的部分原因是运气,其他主要靠效率。

成功的关键不是努力工作而是聪明的工作。忙与产出有着显著的区别。忙未必就说明有产出。 要想产出,更多的是要管理好你的精力而不是时间。要经营好你的生活。我们需要学会花费最小的精力得到最大的收益。

停止通过加班来增加产出

你有没有想过40小时的工作时间是从哪里来的?每周5 天,每天8小时的工作制是福特在1926年的发现。实验表明,把每天工作时间从 10 小时降至 8 小时,每周工作时间从 6 天降至 5 天后,生产力反而提升了。

1980年由商业圆桌会议发布的《施工项目的加班效应》指出,工作得越多,无论是短期还是长期上来看,你的工作效率和生产力都会下降。当每周工作时间超过60 小时,并持续超过两个月,生产力下降的累积效应将使完工日期推迟,而人数相同但每周只工作40 小时的团队执行同样工作,甚至还会更早完工。

在AlterNet的一篇文章中,Sara Robinson回顾美军执行的一项研究,这项发现“每晚都减少1小时睡眠,持续一周,将导致认知功能退化,等同于喝酒使血液酒精浓度升高至0.10 ”。当个人过于劳累,使其以比平常还要负面的角度看事情,导致普遍地心情低落。比心情更重要的是,其思维往往伴随着减少「主动思考与行动」──包括控制冲动、自我感觉良好、同情他人与情绪智力──的意愿。

维持高程度的生产力,避免让自己过度工作并睡眠充足很重要。下次您思想为何工作缺乏生产力,原因很简单,您是70%的人缺少足够的睡眠的一员。

不要老说“好的”

根据 20/80 原理(帕累托原理),20%的努力创造出80%的结果;但反过来20%的结果消耗了80%的努力。因此我们应该把精力集中在能产出80%结果的事情上,然后放弃其他的事情。如此就能把更多的时间集中在最重要的任务上。我们应该对低产出甚至无结果的任务停止说 “yes”。

这就引出一个问题:我们该对哪些事情说“yes”,对哪些事情说“no”呢?如果想不出哪些事情值得花时间,不妨来个简单的分离测试。跟踪自己所做的一切事情然后尽可能优化。

我们中的大多数往往都说了太多的“yes”,因为这比拒绝要容易得多。没人想当坏人。

2012 年的消费者杂志发表了一项研究,研究人员把 120 人分成了 2 组。一组人训练成说“我不能(I can’t)”,另一组则说“我不(I don’t)”。结果很有趣:告诉自己“我不能吃 X”的学生在 61% 的时间内选择了吃巧克力糖,而告诉自己“我不吃 X”的只在 36% 的时间里抵挡不住诱惑。在说法上作这么简单的一个变化就能显著改善健康食品的选择。所以,下次需要避免说 yes 的时候,直接说“我不”。

另一个避免不必要活动的技巧是 20 秒规则:对于不应该做的事情多给自己 20 秒的时间考虑。降低在你想丢弃的习惯上的能量消耗,提升你想培养的习惯上的能力。我们能降低或消除的越多的能量浪费,越能提高我们积极拥抱变化的能力。

停止什么都事必躬亲,让其他人帮忙

为什么品牌需要用户创建内容。消费者知道他们想要什么,以及他们想要如何让它更好,更甚于任何营销人员。 根据Octoly的报告,一支使用者自制影片的观看次数要比品牌自制影片多上十倍。当寻找关于一个特定品牌的资讯,超过半数(51%)的美国人相信使用者自制内容大过于品牌官网(16%)与媒体报导(14%),对营销人员来说,寻求品牌社区的帮忙至关重要。

成为一个伟大的内容创造者,并不是他要创造最好的内容,而是创造一个伟大的社区来帮能产生高质量的内容。

我们必须意识到,在需要的时候可以去寻求帮助,这一点很重要。让做得更好的人接管你的一些工作对你来说更好。这可以让你花更多的时间在自己最重要的任务上。不要把时间浪费在自己解决问题上,让专家帮助你。

很多时候,哪怕朋友不能帮你,他们的陪伴也能让你更有生产力。

在ADHD的治疗中有一种“双重身体”的概念,人们会在有其他人在场的情况下做更多的内容,尽管那些人没有协助或指导,也可以完成更多的工作事项。如果你面临的任务是沉闷或困难的,比如清理你的衣柜或整理您的所有账单,找一个朋友当你的“双重身体”吧。

停止完美主义

Dalhousie University心理学教授Simon Sherry博士Simon Sherry执行一项完美主义与生产力的研究 ,她指出:我们发现完美主义是绊倒教授的研究生产力的大石头。完美主义倾向越高的教授就越没有效率。

当一位完美主义者有以下问题:

  • 他们在一项花费的时间比任务要求所花费的时间还多。
  • 他们会拖延并等到最佳的时刻。 在企业中,如果这是最完美的时刻,就代表已经太迟了。
  • 他们过度聚焦在细节,反而忽略整体。

市场营销人员经常等待最好的时机,如果这样去做,最终会失去机会。 最好时刻就是现在

停止作重复的事情,并使它自动化

根据一项Tethy Solutions的研究,一个5人团队分别花3%、20%、25%、30%与70%的时间处理相同的事情,导入工作自动化软体两个月后,分别将处理重复事情的时间降至3%、10%、15%、15% 与10%。

你需要为了让自己的工作能够自动化而让自己成为一个程序员。如果你拥有这样的能力或资源是最好的,但并不是必须的。如果你不懂得开发,那就去买一个!

人们时常忘记时间就是金钱,因此经常土法炼钢地处理事情,因为这样比较容易,且不需要花费心力研究。假设您办了一个Instagram 活动,号召网友上传的照片总数只有30 张,您可以手动一张一张处理。 但如果总共有从5个不同平台上传的30000张照片与影片时,您就需要一个好的数位管理系统了。如果你不能找到解决方案,你可以雇佣一个专家来帮助你。在你的脑海中始终记得你需要花钱去赚钱,而时间是你最有价值的商品。

停止猜测,并开始用数据支撑决策

如果你可以为搜索引擎进行网站优化,那么你也可以优化你的人生,让它成长并发挥最大的潜能。

不同领域有许多研究可供参考。比方说,你是否知道下午 4 点的时候人最容易分心?这是宾州大学助理教授 Robert Matchock 领导的一项研究。哪怕你找不到需要的数据,进行分离测试也不需要花太多的时间。

要不断问问自己,你打算如何去衡量和优化自己所做的一切事情?

停止工作,并拥有无所事事的时间

大部分的人都没有了解到,当我们专注在某件事上,基本上就像是把自己锁在一个箱子里。 很重要的是要每隔一段时间离开工作现场,享受独处的时光。 独处时光对大脑与灵魂都有益处,波士顿环球时报的一篇文章:

哈佛研究表明,如果某人相信自己正在独自体验某件事情时,其记忆会更持久更精确。另一项研究表明,一定时间的独处可令人更具同理心。尽管没人会质疑早期过多的孤僻生活是不健康的,但一定时间的独处可令青少年改善情绪提高成绩。

对我们来说,要花时间去思考是很重要的。我们经常发现在我们不是刻意寻找解决方案的时候解决方案会突然出现。

我们不会因为熬夜而更有效率。 就像是生命中的每件事情,需要耗费心力。 如果您什么都不做只是坐着等,不会有什么改变,所以我们要更了解自己的限制与潜能,并将精力作有效的配置,过一个更成功、更快乐的人生。

原文链接: 7 Things You Need to Stop Doing to Be More Productive

中国公司日益使用高科技工具监视员工

$
0
0
日经 报道,Andy Wang 是上海一家游戏公司的 IT 工程师,他有时会对自己的工作产生罪恶感。他的大部分工作时间花在名为“第三只眼”的监视软件上,该软件安装在每一位同事的笔记本电脑上,实时跟踪他们的屏幕,记录他们的聊天、浏览活动和编辑的每一份文档。软件会自动的标记可疑活动,如访问求职网站和流视频平台,它会每周生成一份报告总结员工在网站和应用程序上花费的时间。上司会定期检查这些报告。甚至Wang 自己也不能豁免于监视。在工作了两年之后,不堪重负的他选择了辞职,他说这没有意义,我们无法不停的工作,需要一些喘息的空间。就像美团的蓝领骑手受制于算法,办公室的白领工人们也日益受到监视软件的监督。中国公司正日益使用高科技工具持续的监视员工。中国最大的监视平台供应商深信服称它的企业客户超过 5 万家,其中包括阿里巴巴、字节跳动、新浪、小米和中兴。在员工连接到公司 WiFi 之后,深信服的服务能访问员工的移动浏览历史和应用使用记录。它无需用户批准,能屏蔽被认为对生产力有害的特定应用。系统还会根据花在与工作无关的网站和应用上的时间给低效的员工进行排名。它还能根据对招聘网站的访问和发送简历识别可能辞职的员工。佳能在中国的子公司去年使用了微笑识别技术,只允许微笑的员工进入办公室和会议室,佳能称此举旨在在后疫情时代给办公室带来欢乐,创造一种积极的气氛。公司发言人说,大多数人都羞于微笑,当他们习惯微笑之后会保持微笑。

ElasticSearch 双数据中心建设在新网银行的实践

$
0
0


本文公众号读者飞熊的投稿,本文主要讲述了ElasticSearch 双数据中心建设在新网银行的实践。

作者简介: 

飞熊,目前就职于新网银行大数据中心,主要从事大数据实时计算和平台开发相关工作,对Flink ,Spark 以及ElasticSearch等大数据技术有浓厚兴趣和较深入的理解。

引言

新网银行是作为西部首家互联网银行,一直践行依靠数据和技术驱动业务的发展理念。自开业以来,已经积累了大量数据。早期因为数据量不大全部存入在 Hbase 集群,随着数据 量的增多,Hbase 集群的缺点逐渐被暴露,最显著的问题就是查询返回耗时太长。为了更快, 更好的响应业务,引入了 Elastic Search。Elastic Search 作为大数据搜索查询的一把“利剑”, 能够在海量数据下实现多维分析下近实时返回。并逐渐取代 Hbase,嵌入到新网银行核心业 务线条,成为业务必不可少的一环。

技术方案

银行作为金融机构,对线上业务的连续性有着近乎苛刻的要求,一旦出现问题必然面临 监管机构的问责。因此,为了保证 ElasticSearch 集群的高可用性和灾难恢复性,需要考虑 针对 Elastic Search 集群的双数据中心建设。目前主流的技术方案如下:

表 1. Elastic Search 双数据中心建设方案对比

ElasticSearch 集群是 P2P 模式的分布式系统架构,任意 2 个节点之间的互相通信将会 很频繁。如果考虑单集群跨机房部署,那么可能造成节点之间频繁的通信,那么通信延时会比较高,甚至造成集群运行频繁不正常,且后期维护成本较高。因此采用多集群多机房部署方案。 

针对多集群多机房的部署方案,在实际建设的时候也存在多种选择。如考虑应用双写方法或则考虑利用 ElasticSearch 的白金会员特性 CCR(跨集群复制)。但是这 2 中方案也有缺 点:如双写方法需要额外的操作保障一致性;CCR(跨集群复制)的白金会员会提高建设成本。因此,经过多方对比,决定采用解析 ElasticSearch 的 Translog 文件方案。这种方案的优点在于:保证实时性,对外屏蔽应用对数据的感知和实现读写分离。

技术建设

1.Translog 文件介绍

Translog 是 Elastic search 的事务日志文件,它记录所有对分片的事务操作 (add/update/delete),并且每个分片对应一个 translog 文件。Elastic Search 写入数据的时候, 是先写到内存和 translog 文件。因此可以通过对 translog 文件中数据的拦截,实时写入另一 个数据中心。在 Elastic Search 的分片目录下,存在如下 2 种数据文件:

(1) translog-N.tlog: 日志文件,N 表示 generation(代)的意思。每次当 flush 的时 候就会产生一个 generation(代)。

(2) translog-N.ckp: 记录日志信息的元数据文件,N 表示 generation(代)的意思, 记录 3 个信息:偏移量,事务操作数据量和当前代。

对于包含 N 的文件名,意味着没有数据再写入;正在写入的文件,其文件名是不包含 N。

2. Translog 解析

对于日志文件的解析,采用的思想是:部分先行,结束补全。即每次跳过上次读取偏 移量后读取数据,同时等待当前日志文件写完后再读取一次全量数据写入。这样做的目的是为了,补全截取正在写入日志文件时丢失的数据,同时保证数据的时效性。整个解析过程如下:

图 1.分片下 Translog 解析方法

3.线上部署

目前部署方式是采用非嵌入式的,即将代码作为一个单独的应用程序,即命名为 X-CCR 工具,部署到 Elastic Search 的节点服务器上。通过 X-CCR 实现双数据中心数据同步, 同时从业务层面实现数据读写分离,冷热查询分离。部署情况见图 2 所示:

图 2. Elastic Search 双数据中心部署效果

性能表现

目前新网银行有 2 个 Elastic Search 数据中心,每个数据中心各自有 3 台物理机。通过在线上观察和验证测试,X-CCR 工具可以确保在主分片写入 TPS=50000/s 下,75%的数 据在 2s 内,实现数据相互可见。相关的统计数据见图 3:

图 3. Translog解析同步工具X-CCR 工具性能测试

总结

本文介绍了新网银行在Elastic Search双数据中心建设上的实践。目前,已经完成了第一个版本的建设,从功能上和性能上满足了业务需求,但还需更加完善;后期打算将其与Elastic Search 插件集成,方便部署和管理。

Viewing all 11847 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>