• 关于

    异常原则系统有什么用

    的搜索结果

回答

从业余程序员到职业程序员 程序员刚入行时,我觉得最重要的是把自己培养成职业的程序员。 我的程序员起步比同龄人都晚了很多,更不用说现在的年轻人了。我大学读的是生物专业,在上大学前基本算是完全没接触过计算机。军训的时候因为很无聊,我和室友每天跑去学校的机房玩,我现在还印象很深刻,我第一次走进机房的时候,别人问,你是要玩windows,还是dos,我那是完全的一抹黑。后来就只记得在机房一堆人都是在练习盲打,军训完,盲打倒是练的差不多了,对计算机就这么产生了浓厚的兴趣,大一的时候都是玩组装机,捣鼓了一些,对计算机的硬件有了那么一些了解。 到大二后,买了一些书开始学习当时最火的网页三剑客,学会了手写HTML、PS的基本玩法之类的,课余、暑假也能开始给人做做网站什么的(那个时候做网站真的好赚钱),可能那样过了个一年左右,做静态的网页就不好赚钱了,也不好找实习工作,于是就开始学asp,写些简单的CRUD,做做留言板、论坛这些动态程序,应该算是在这个阶段接触编程了。 毕业后加入了深圳的一家做政府行业软件的公司,一个非常靠谱和给我空间的Leader,使得自己在那几年有了不错的成长,终于成了一个职业的程序员。 通常来说,业余或半职业的程序员,多数是1个人,或者很小的一个团队一起开发,使得在开发流程、协作工具(例如jira、cvs/svn/git等)、测试上通常会有很大的欠缺,而职业的程序员在这方面则会专业很多。另外,通常职业的程序员做的系统都要运行较长的时间,所以在可维护性上会特别注意,这点我是在加入阿里后理解更深的。一个运行10年的系统,和一个写来玩玩的系统显然是有非常大差别的。 这块自己感觉也很难讲清楚,只能说模模糊糊有个这样的概念。通常在有兴趣的基础上,从业余程序员跨越到成为职业程序员我觉得不会太难。 编程能力的成长 作为程序员,最重要的能力始终是编程能力,就我自己的感受而言,我觉得编程能力的成长主要有这么几个部分: 1、编程能力初级:会用 编程,首先都是从学习编程语言的基本知识学起的,不论是什么编程语言,有很多共同的基本知识,例如怎么写第一个Hello World、if/while/for、变量等,因此我比较建议在刚刚开始学一门编程语言的时候,看看编程语言自己的一些文档就好,不要上来就去看一些高阶的书。我当年学Java的时候上来就看Think in Java、Effective Java之类的,真心好难懂。 除了看文档以外,编程是个超级实践的活,所以一定要多写代码,只有这样才能真正熟练起来。这也是为什么我还是觉得在面试的时候让面试者手写代码是很重要的,这个过程是非常容易判断写代码的熟悉程度的。很多人会说由于写代码都是高度依赖IDE的,导致手写很难,但我绝对相信写代码写了很多的人,手写一段不太复杂的、可运行的代码是不难的。即使像我这种三年多没写过代码的人,让我现在手写一段不太复杂的可运行的Java程序,还是没问题的,前面N年的写代码生涯使得很多东西已经深入骨髓了。 我觉得编程能力初级这个阶段对于大部分程序员来说都不会是问题,勤学苦练,是这个阶段的核心。 2、编程能力中级:会查和避免问题 除了初级要掌握的会熟练的使用编程语言去解决问题外,中级我觉得首先是提升查问题的能力。 在写代码的过程中,出问题是非常正常的,怎么去有效且高效的排查问题,是程序员群体中通常能感受到的大家在编程能力上最大的差距。 解决问题能力强的基本很容易在程序员群体里得到很高的认可。在查问题的能力上,首先要掌握的是一些基本的调试技巧,好用的调试工具,在Java里有JDK自带的jstat、jmap、jinfo,不在JDK里的有mat、gperf、btrace等。工欲善其事必先利其器,在查问题上是非常典型的,有些时候大家在查问题时的能力差距,有可能仅仅是因为别人比你多知道一个工具而已。 除了调试技巧和工具外,查问题的更高境界就是懂原理。一个懂原理的程序员在查问题的水平上和其他程序员是有明显差距的。我想很多的同学应该能感受到,有些时候查出问题的原因仅仅是因为有效的工具,知其然不知其所以然。 我给很多阿里的同学培训过Java排查问题的方法,在这个培训里,我经常也会讲到查问题的能力的培养最主要的也是熟练,多尝试给自己写一些会出问题的程序,多积极的看别人是怎么查问题的,多积极的去参与排查问题,很多最后查问题能力强的人多数仅仅是因为“无他,但手熟尔”。 我自己排查问题能力的提升主要是在2009年和2010年。那两年作为淘宝消防队(处理各种问题和故障的虚拟团队)的成员,处理了很多的故障和问题。当时消防队还有阿里最公认的技术大神——多隆,我向他学习到了很多排查问题的技巧。和他比,我排查问题的能力就是初级的那种。 印象最深刻的是一次我们一起查一个应用cpu us高的问题,我们两定位到是一段代码在某种输入参数的时候会造成cpu us高的原因后,我能想到的继续查的方法是去生产环境抓输入参数,然后再用参数来本地debug看是什么原因。但多隆在看了一会那段代码后,给了我一个输入参数,我拿这个参数一运行,果然cpu us很高!这种case不是一次两次。所以我经常和别人说,我是需要有问题场景才能排查出问题的,但多隆是完全有可能直接看代码就能看出问题的,这是本质的差距。 除了查问题外,更厉害的程序员是在写代码的过程就会很好的去避免问题。大家最容易理解的就是在写代码时处理各种异常情况,这里通常也是造成程序员们之间很大的差距的地方。 写一段正向逻辑的代码,大部分情况下即使有差距,也不会太大,但在怎么很好的处理这个过程中有可能出现的异常上,这个时候的功力差距会非常明显。很多时候一段代码里处理异常逻辑的部分都会超过正常逻辑的代码量。 我经常说,一个优秀程序员和普通程序员的差距,很多时候压根就不需要看什么满天飞的架构图,而只用show一小段的代码就可以。 举一个小case大家感受下。当年有一个严重故障,最后查出的原因是输入的参数里有一个是数组,把这个数组里的值作为参数去查数据库,结果前面输入了一个很大的数组,导致从数据库查了大量的数据,内存溢出了,很多程序员现在看都会明白对入参、出参的保护check,但类似这样的case我真的碰到了很多。 在中级这个阶段,我会推荐大家尽可能的多刻意的去培养下自己这两个方面的能力,成为一个能写出高质量代码、有效排查问题的优秀程序员。 3、编程能力高级:懂高级API和原理 就我自己的经历而言,我是在写了多年的Java代码后,才开始真正更细致的学习和掌握Java的一些更高级的API,我相信多数Java程序员也是如此。 我算是从2003年开始用Java写商业系统的代码,但直到在2007年加入淘宝后,才开始非常认真地学习Java的IO通信、并发这些部分的API。尽管以前也学过也写过一些这样的代码,但完全就是皮毛。当然,这些通常来说有很大部分的原因会是工作的相关性,多数的写业务系统的程序员可能基本就不需要用到这些,所以导致会很难懂这些相对高级一些的API,但这些API对真正的理解一门编程语言,我觉得至关重要。 在之前的程序员成长路线的文章里我也讲到了这个部分,在没有场景的情况下,只能靠自己去创造场景来学习好。我觉得只要有足够的兴趣,这个问题还是不大的,毕竟现在有各种开源,这些是可以非常好的帮助自己创造机会学习的,例如学Java NIO,可以自己基于NIO包一个框架,然后对比Netty,看看哪些写的是不如Netty的,这样会非常有助于真正的理解。 在学习高级API的过程中,以及排查问题的过程中,我自己越来越明白懂编程语言的运行原理是非常重要的,因此我到了后面的阶段开始学习Java的编译机制、内存管理、线程机制等。对于我这种非科班出身的而言,学这些会因为缺乏基础更难很多,但这些更原理性的东西学会了后,对自己的编程能力会有质的提升,包括以后学习其他编程语言的能力,学这些原理最好的方法我觉得是先看看一些讲相关知识的书,然后去翻看源码,这样才能真正的更好的掌握,最后是在以后写代码的过程中、查问题的过程中多结合掌握的原理,才能做到即使在N年后也不会忘。 在编程能力的成长上,我觉得没什么捷径。我非常赞同1万小时理论,在中级、高级阶段,如果有人指点或和优秀的程序员们共事,会好非常多。不过我觉得这个和读书也有点像,到了一定阶段后(例如高中),天分会成为最重要的分水岭,不过就和大部分行业一样,大部分的情况下都还没到拼天分的时候,只需要拼勤奋就好。 系统设计能力的成长 除了少数程序员会进入专深的领域,例如Linux Kernel、JVM,其他多数的程序员除了编程能力的成长外,也会越来越需要在系统设计能力上成长。 通常一个编程能力不错的程序员,在一定阶段后就会开始承担一个模块的工作,进而承担一个子系统、系统、跨多领域的更大系统等。 我自己在工作的第三年开始承担一个流程引擎的设计和实现工作,一个不算小的系统,并且也是当时那个项目里的核心部分。那个阶段我学会了一些系统设计的基本知识,例如需要想清楚整个系统的目标、模块的划分和职责、关键的对象设计等,而不是上来就开始写代码。但那个时候由于我是一个人写整个系统,所以其实对设计的感觉并还没有那么强力的感觉。 在那之后的几年也负责过一些系统,但总体感觉好像在系统设计上的成长没那么多,直到在阿里的经历,在系统设计上才有了越来越多的体会。(点击文末阅读原文,查看:我在系统设计上犯过的14个错,可以看到我走的一堆的弯路)。 在阿里有一次做分享,讲到我在系统设计能力方面的成长,主要是因为三段经历,负责专业领域系统的设计 -> 负责跨专业领域的专业系统的设计 -> 负责阿里电商系统架构级改造的设计。 第一段经历,是我负责HSF。HSF是一个从0开始打造的系统,它主要是作为支撑服务化的框架,是个非常专业领域的系统,放在整个淘宝电商的大系统来看,其实它就是一个很小的子系统,这段经历里让我最深刻的有三点: 1).要设计好这种非常专业领域的系统,专业的知识深度是非常重要的。我在最早设计HSF的几个框的时候,是没有设计好服务消费者/提供者要怎么和现有框架结合的,在设计负载均衡这个部分也反复了几次,这个主要是因为自己当时对这个领域掌握不深的原因造成的; 2). 太技术化。在HSF的阶段,出于情怀,在有一个版本里投入了非常大的精力去引进OSGi以及去做动态化,这个后来事实证明是个非常非常错误的决定,从这个点我才真正明白在设计系统时一定要想清楚目标,而目标很重要的是和公司发展阶段结合; 3). 可持续性。作为一个要在生产环境持续运行很多年的系统而言,怎么样让其在未来更可持续的发展,这个对设计阶段来说至关重要。这里最low的例子是最早设计HSF协议的时候,协议头里竟然没有版本号,导致后来升级都特别复杂;最典型的例子是HSF在早期缺乏了缺乏了服务Tracing这方面的设计,导致后面发现了这个地方非常重要后,全部落地花了长达几年的时间;又例如HSF早期缺乏Filter Chain的设计,导致很多扩展、定制化做起来非常不方便。 第二段经历,是做T4。T4是基于LXC的阿里的容器,它和HSF的不同是,它其实是一个跨多领域的系统,包括了单机上的容器引擎,容器管理系统,容器管理系统对外提供API,其他系统或用户通过这个来管理容器。这个系统发展过程也是各种犯错,犯错的主要原因也是因为领域掌握不深。在做T4的日子里,学会到的最重要的是怎么去设计这种跨多个专业领域的系统,怎么更好的划分模块的职责,设计交互逻辑,这段经历对我自己更为重要的意义是我有了做更大一些系统的架构的信心。 第三段经历,是做阿里电商的异地多活。这对我来说是真正的去做一个巨大系统的架构师,尽管我以前做HSF的时候参与了淘宝电商2.0-3.0的重大技术改造,但参与和自己主导是有很大区别的,这个架构改造涉及到了阿里电商众多不同专业领域的技术团队。在这个阶段,我学会的最主要的: 1). 子系统职责划分。在这种超大的技术方案中,很容易出现某些部分的职责重叠和冲突,这个时候怎么去划分子系统,就非常重要了。作为大架构师,这个时候要从团队的职责、团队的可持续性上去选择团队; 2). 大架构师最主要的职责是控制系统风险。对于这种超大系统,一定是多个专业领域的架构师和大架构师共同设计,怎么确保在执行的过程中对于系统而言最重要的风险能够被控制住,这是我真正的理解什么叫系统设计文档里设计原则的部分。 设计原则我自己觉得就是用来确保各个子系统在设计时都会遵循和考虑的,一定不能是虚的东西,例如在异地多活架构里,最重要的是如何控制数据风险,这个需要在原则里写上,最基本的原则是可接受系统不可用,但也要保障数据一致,而我看过更多的系统设计里设计原则只是写写的,或者千篇一律的,设计原则切实的体现了架构师对目标的理解(例如当时异地多活这个其实开始只是个概念,但做到什么程度才叫做到异地多活,这是需要解读的,也要确保在技术层面的设计上是达到了目标的),技术方案层面上的选择原则,并确保在细节的设计方案里有对于设计原则的承接以及执行; 3). 考虑问题的全面性。像异地多活这种大架构改造,涉及业务层面、各种基础技术层面、基础设施层面,对于执行节奏的决定要综合考虑人力投入、机器成本、基础设施布局诉求、稳定性控制等,这会比只是做一个小的系统的设计复杂非常多。 系统设计能力的成长,我自己觉得最重要的一是先在一两个技术领域做到专业,然后尽量扩大自己的知识广度。例如除了自己的代码部分外,还应该知道具体是怎么部署的,部署到哪去了,部署的环境具体是怎么样的,和整个系统的关系是什么样的。 像我自己,是在加入基础设施团队后才更加明白有些时候软件上做的一个决策,会导致基础设施上巨大的硬件、网络或机房的投入,但其实有可能只需要在软件上做些调整就可以避免,做做研发、做做运维可能是比较好的把知识广度扩大的方法。 第二点是练习自己做tradeoff的能力,这个比较难,做tradeoff这事需要综合各种因素做选择,但这也是所有的架构师最关键的,可以回头反思下自己在做各种系统设计时做出的tradeoff是什么。这个最好是亲身经历,听一些有经验的架构师分享他们选择背后的逻辑也会很有帮助,尤其是如果恰好你也在同样的挑战阶段,光听最终的架构结果其实大多数时候帮助有限。 技术Leader我觉得最好是能在架构师的基础上,后续注重成长的方面还是有挺大差别,就不在这篇里写了,后面再专门来写一篇。 程序员金字塔 我认为程序员的价值关键体现在作品上,被打上作品标签是一种很大的荣幸,作品影响程度的大小我觉得决定了金字塔的层次,所以我会这么去理解程序员的金字塔。 当然,要打造一款作品,仅有上面的两点能力是不够的,作品里很重要的一点是对业务、技术趋势的判断。 希望作为程序员的大伙,都能有机会打造一款世界级的作品,去为技术圈的发展做出贡献。 由于目前IT技术更新速度还是很快的,程序员这个行当是特别需要学习能力的。我一直认为,只有对程序员这个职业真正的充满兴趣,保持自驱,才有可能在这个职业上做好,否则的话是很容易淘汰的。 作者简介: 毕玄,2007年加入阿里,十多年来主要从事在软件基础设施领域,先后负责阿里的服务框架、Hbase、Sigma、异地多活等重大的基础技术产品和整体架构改造。

茶什i 2020-01-10 15:19:35 0 浏览量 回答数 0

回答

我们都知道JVM的内存管理是自动化的,Java语言的程序指针也不需要开发人员手工释放,JVM的GC会自动的进行回收,但是,如果编程不当,JVM仍然会发生内存泄露,导致Java程序产生了OutOfMemoryError(OOM)错误。 产生OutOfMemoryError错误的原因包括: java.lang.OutOfMemoryError: Java heap spacejava.lang.OutOfMemoryError: PermGen space及其解决方法java.lang.OutOfMemoryError: unable to create new native threadjava.lang.OutOfMemoryError:GC overhead limit exceeded对于第1种异常,表示Java堆空间不够,当应用程序申请更多的内存,而Java堆内存已经无法满足应用程序对内存的需要,将抛出这种异常。 对于第2种异常,表示Java永久带(方法区)空间不够,永久带用于存放类的字节码和长常量池,类的字节码加载后存放在这个区域,这和存放对象实例的堆区是不同的,大多数JVM的实现都不会对永久带进行垃圾回收,因此,只要类加载的过多就会出现这个问题。一般的应用程序都不会产生这个错误,然而,对于Web服务器来讲,会产生有大量的JSP,JSP在运行时被动态的编译成Java Servlet类,然后加载到方法区,因此,太多的JSP的Web工程可能产生这个异常。 对于第3种异常,本质原因是创建了太多的线程,而能创建的线程数是有限制的,导致了这种异常的发生。 对于第4种异常,是在并行或者并发回收器在GC回收时间过长、超过98%的时间用来做GC并且回收了不到2%的堆内存,然后抛出这种异常进行提前预警,用来避免内存过小造成应用不能正常工作。 下面两个异常与OOM有关系,但是,又没有绝对关系。 java.lang.StackOverflowError ...java.net.SocketException: Too many open files对于第1种异常,是JVM的线程由于递归或者方法调用层次太多,占满了线程堆栈而导致的,线程堆栈默认大小为1M。 对于第2种异常,是由于系统对文件句柄的使用是有限制的,而某个应用程序使用的文件句柄超过了这个限制,就会导致这个问题。 上面介绍了OOM相关的基础知识,接下来我们开始讲述笔者经历的一次OOM问题的定位和解决的过程。 产生问题的现象 在某一段时间内,我们发现不同的业务服务开始偶发的报OOM的异常,有的时候是白天发生,有的时候是晚上发生,有的时候是基础服务A发生,有的时候是上层服务B发生,有的时候是上层服务C发生,有的时候是下层服务D发生,丝毫看不到一点规律。 产生问题的异常如下: Caused by: java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method)at java.lang.Thread.start(Thread.java:597)at java.util.Timer.(Timer.java:154) 解决问题的思路和过程 经过细心观察发现,产生问题虽然在不同的时间发生在不同的服务池,但是,晚上0点发生的时候概率较大,也有其他时间偶发,但是都在整点。 这个规律很重要,虽然不是一个时间,但是基本都在整点左右发生,并且晚上0点居多。从这个角度思考,整点或者0点系统是否有定时,与出问题的每个业务系统技术负责人核实,0点没有定时任务,其他时间的整点有定时任务,但是与发生问题的时间不吻合,这个思路行不通。 到现在为止,从现象的规律上我们已经没法继续分析下去了,那我们回顾一下错误本身: java.lang.OutOfMemoryError: unable to create new native thread 顾名思义,错误产生的原因就是应用不能创建线程了,但是,应用还需要创建线程。为什么程序不能创建线程呢? 有两个具体原因造成这个异常: 由于线程使用的资源过多,操作系统已经不能再提供给应用资源了。操作系统设置了应用创建线程的最大数量,并且已经达到了最大允许数量。上面第1条资源指的是内存,而第2条中,在Linux下线程使用轻量级进程实现的,因此线程的最大数量也是操作系统允许的进程的最大数量。 内存计算 操作系统中的最大可用内存除去操作系统本身使用的部分,剩下的都可以为某一个进程服务,在JVM进程中,内存又被分为堆、本地内存和栈等三大块,Java堆是JVM自动管理的内存,应用的对象的创建和销毁、类的装载等都发生在这里,本地内存是Java应用使用的一种特殊内存,JVM并不直接管理其生命周期,每个线程也会有一个栈,是用来存储线程工作过程中产生的方法局部变量、方法参数和返回值的,每个线程对应的栈的默认大小为1M。 Linux和JVM的内存管理示意图如下: 内存结构模型因此,从内存角度来看创建线程需要内存空间,如果JVM进程正当一个应用创建线程,而操作系统没有剩余的内存分配给此JVM进程,则会抛出问题中的OOM异常:unable to create new native thread。 如下公式可以用来从内存角度计算允许创建的最大线程数: 最大线程数 = (操作系统最大可用内存 - JVM内存 - 操作系统预留内存)/ 线程栈大小 根据这个公式,我们可以通过剩余内存计算可以创建线程的数量。 下面是问题出现的时候,从生产机器上执行前面小节介绍的Linux命令free的输出: free -m >> /tmp/free.log total used free shared buffers cached Mem: 7872 7163 709 0 31 3807-/+ buffers/cache: 3324 4547Swap: 4095 173 3922Tue Jul 5 00:27:51 CST 2016从上面输出可以得出,生产机器8G内存,使用了7G,剩余700M可用,其中操作系统cache使用3.8G。操作系统cache使用的3.8G是用来缓存IO数据的,如果进程内存不够用,这些内存是可以释放出来优先分配给进程使用。然而,我们暂时并不需要考虑这块内存,剩余的700M空间完全可以继续用来创建线程数: 700M / 1M = 700个线程 因此,根据内存可用计算,当OOM异常:unable to create new native thread问题发生的时候,还有700M可用内存,可以创建700个线程。 到现在为止可以证明此次OOM异常不是因为线程吃光所有的内存而导致的。 线程数对比 上面提到,有两个具体原因造成这个异常,我们上面已经排除了第1个原因,那我们现在从第2个原因入手,评估是否操作系统设置了应用创建线程的最大数量,并且已经达到了最大允许数量。 在问题出现的生产机器上使用ulimit -a来显示当前的各种系统对用户使用资源的限制: robert@robert-ubuntu1410:~$ ulimit -acore file size (blocks, -c) 0data seg size (kbytes, -d) unlimitedscheduling priority (-e) 0file size (blocks, -f) unlimitedpending signals (-i) 62819max locked memory (kbytes, -l) 64max memory size (kbytes, -m) unlimitedopen files (-n) 65535pipe size (512 bytes, -p) 8POSIX message queues (bytes, -q) 819200real-time priority (-r) 0stack size (kbytes, -s) 10240cpu time (seconds, -t) unlimitedmax user processes (-u) 1024virtual memory (kbytes, -v) unlimitedfile locks (-x) unlimited这里面我们看到生产机器设置的允许使用的最大用户进程数为1024: max user processes (-u) 1024现在,我们必须获得问题出现的时候,用户下创建的线程情况。 在问题产生的时候,我们使用前面小结介绍的JVM监控命令jstack命令打印出了Java线程情况,jstack命令的示例输出如下: robert@robert-ubuntu1410:~$ jstack 27432017-04-09 12:06:51Full thread dump Java HotSpot(TM) Server VM (25.20-b23 mixed mode): "Attach Listener" #23 daemon prio=9 os_prio=0 tid=0xc09adc00 nid=0xb4c waiting on condition [0x00000000] java.lang.Thread.State: RUNNABLE "http-nio-8080-Acceptor-0" #22 daemon prio=5 os_prio=0 tid=0xc3341000 nid=0xb02 runnable [0xbf1bd000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:241) - locked <0xcf8938d8> (a java.lang.Object) at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:688) at java.lang.Thread.run(Thread.java:745) "http-nio-8080-ClientPoller-1" #21 daemon prio=5 os_prio=0 tid=0xc35bc400 nid=0xb01 runnable [0xbf1fe000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method) at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269) at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:79) at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86) - locked <0xcf99b100> (a sun.nio.ch.Util$2) - locked <0xcf99b0f0> (a java.util.Collections$UnmodifiableSet) - locked <0xcf99aff8> (a sun.nio.ch.EPollSelectorImpl) at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97) at org.apache.tomcat.util.net.NioEndpoint$Poller.run(NioEndpoint.java:1052) at java.lang.Thread.run(Thread.java:745) ......从jstack命令的输出并统计后,我们得知,JVM一共创建了904个线程,但是,这还没有到最大的进程限制1024。 robert@robert-ubuntu1410:~$ grep "Thread " js.log | wc -l 904 这是我们思考,除了JVM创建的应用层线程,JVM本身可能会有一些管理线程存在,而且操作系统内用户下可能也会有守护线程在运行。 我们继续从操作系统的角度来统计线程数,我们使用上面小结介绍的Linux操作系统命令pstack,并得到如下的输出: PID LWP USER %CPU %MEM CMD 1 1 root 0.0 0.0 /sbin/init 2 2 root 0.0 0.0 [kthreadd] 3 3 root 0.0 0.0 [migration/0] 4 4 root 0.0 0.0 [ksoftirqd/0] 5 5 root 0.0 0.0 [migration/0] 6 6 root 0.0 0.0 [watchdog/0] 7 7 root 0.0 0.0 [migration/1] 8 8 root 0.0 0.0 [migration/1] 9 9 root 0.0 0.0 [ksoftirqd/1] 10 10 root 0.0 0.0 [watchdog/1] 11 11 root 0.0 0.0 [migration/2] 12 12 root 0.0 0.0 [migration/2] 13 13 root 0.0 0.0 [ksoftirqd/2] 14 14 root 0.0 0.0 [watchdog/2] 15 15 root 0.0 0.0 [migration/3] 16 16 root 0.0 0.0 [migration/3] 17 17 root 0.0 0.0 [ksoftirqd/3] 18 18 root 0.0 0.0 [watchdog/3] 19 19 root 0.0 0.0 [events/0] 20 20 root 0.0 0.0 [events/1] 21 21 root 0.0 0.0 [events/2] 22 22 root 0.0 0.0 [events/3] 23 23 root 0.0 0.0 [cgroup] 24 24 root 0.0 0.0 [khelper] ...... 7257 7257 zabbix 0.0 0.0 /usr/local/zabbix/sbin/zabbix_agentd: active checks #2 [idle 1 sec] 7258 7258 zabbix 0.0 0.0 /usr/local/zabbix/sbin/zabbix_agentd: active checks #3 [idle 1 sec] 7259 7259 zabbix 0.0 0.0 /usr/local/zabbix/sbin/zabbix_agentd: active checks #4 [idle 1 sec] ...... 9040 9040 app 0.0 30.5 /apps/prod/jdk1.6.0_24/bin/java -Dnop -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Ddbconfigpath=/apps/dbconfig/ -Djava.io.tmpdir=/apps/data/java-tmpdir -server -Xms2048m -Xmx2048m -XX:PermSize=128m -XX:MaxPermSize=512m -Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=192.168.10.194 -Dcom.sun.management.jmxremote.port=6969 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp -Xshare:off -Dhostname=sjsa-trade04 -Djute.maxbuffer=41943040 -Djava.net.preferIPv4Stack=true -Dfile.encoding=UTF-8 -Dworkdir=/apps/data/tomcat-work -Djava.endorsed.dirs=/apps/product/tomcat-trade/endorsed -classpath commonlib:/apps/product/tomcat-trade/bin/bootstrap.jar:/apps/product/tomcat-trade/bin/tomcat-juli.jar -Dcatalina.base=/apps/product/tomcat-trade -Dcatalina.home=/apps/product/tomcat-trade -Djava.io.tmpdir=/apps/data/tomcat-temp/ org.apache.catalina.startup.Bootstrap start 9040 9041 app 0.0 30.5 /apps/prod/jdk1.6.0_24/bin/java -Dnop -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Ddbconfigpath=/apps/dbconfig/ -Djava.io.tmpdir=/apps/data/java-tmpdir -server -Xms2048m -Xmx2048m -XX:PermSize=128m -XX:MaxPermSize=512m -Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=192.168.10.194 -Dcom.sun.management.jmxremote.port=6969 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp -Xshare:off -Dhostname=sjsa-trade04 -Djute.maxbuffer=41943040 -Djava.net.preferIPv4Stack=true -Dfile.encoding=UTF-8 -Dworkdir=/apps/data/tomcat-work -Djava.endorsed.dirs=/apps/product/tomcat-trade/endorsed -classpath commonlib:/apps/product/tomcat-trade/bin/bootstrap.jar:/apps/product/tomcat-trade/bin/tomcat-juli.jar -Dcatalina.base=/apps/product/tomcat-trade -Dcatalina.home=/apps/product/tomcat-trade -Djava.io.tmpdir=/apps/data/tomcat-temp/ org.apache.catalina.startup.Bootstrap start ......通过命令统计用户下已经创建的线程数为1021。 $ grep app pthreads.log | wc -l 1021 现在我们确定,1021的数字已经相当的接近1021的最大进程数了,正如前面我们提到,在Linux操作系统里,线程是通过轻量级的进程实现的,因此,限制用户的最大进程数,就是限制用户的最大线程数,至于为什么没有精确达到1024这个最大值就已经报出异常,应该是系统的自我保护功能,在还剩下3个线程的前提下,就开始报错。 到此为止,我们已经通过分析来找到问题的原因,但是,我们还是不知道为什么会创建这么多的线程,从第一个输出得知,JVM已经创建的应用线程有907个,那么他们都在做什么事情呢? 于是,在问题发生的时候,我们又使用JVM的jstack命令,查看输出得知,每个线程都阻塞在打印日志的语句上,log4j中打印日志的代码实现如下: public void callAppenders(LoggingEvent event) { int writes = 0; for(Category c = this; c != null; c=c.parent) { // Protected against simultaneous call to addAppender, removeAppender,... synchronized(c) { if(c.aai != null) { writes += c.aai.appendLoopOnAppenders(event); } if(!c.additive) { break; } } } if(writes == 0) { repository.emitNoAppenderWarning(this); } }在log4j中,打印日志有一个锁,锁的作用是让打印日志可以串行,保证日志在日志文件中的正确性和顺序性。 那么,新的问题又来了,为什么只有凌晨0点会出现打印日志阻塞,其他时间会偶尔发生呢?这时,我们带着新的线索又回到问题开始的思路,凌晨12点应用没有定时任务,系统会不会有其他的IO密集型的任务,比如说归档日志、磁盘备份等? 经过与运维部门碰头,基本确定是每天凌晨0点日志切割导致磁盘IO被占用,于是堵塞打印日志,日志是每个工作任务都必须的,日志阻塞,线程池就阻塞,线程池阻塞就导致线程池被撑大,线程池里面的线程数超过1024就会报错。 到这里,我们基本确定了问题的原因,但是还需要对日志切割导致IO增大进行分析和论证。 首先我们使用前面小结介绍的vmstat查看问题发生时IO等待数据: vmstat 2 1 >> /tmp/vm.logprocs -----------memory---------- ---swap-- -----io---- --system-- -----cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 3 0 177608 725636 31856 3899144 0 0 2 10 0 0 39 1 1 59 0 Tue Jul 5 00:27:51 CST 2016可见,问题发生的时候,CPU的IO等待为59%,同时又与运维部门同事复盘,运维同事确认,脚本切割通过cat命令方法,先把日志文件cat后,通过管道打印到另外一个文件,再清空原文件,因此,一定会导致IO的上升。 其实,问题的过程中,还有一个疑惑,我们认为线程被IO阻塞,线程池被撑开,导致线程增多,于是,我们查看了一下Tomcat线程池的设置,我们发现Tomcat线程池设置了800,按理说,永远不会超过1024。 maxThreads="800" minSpareThreads="25" maxSpareThreads="75" enableLookups="false" redirectPort="8443" acceptCount="100" debug="0" connectionTimeout="20000" disableUploadTimeout="true" /> 关键在于,笔者所在的支付平台服务化架构中,使用了两套服务化框架,一个是基于dubbo的框架,一个是点对点的RPC,用来紧急情况下dubbo服务出现问题,服务降级使用。 每个服务都配置了点对点的RPC服务,并且独享一个线程池: maxThreads="800" minSpareThreads="25" maxSpareThreads="75" enableLookups="false" redirectPort="8443" acceptCount="100" debug="0" connectionTimeout="20000" disableUploadTimeout="true" /> 由于我们在对dubbo服务框架进行定制化的时候,设计了自动降级原则,如果dubbo服务负载变高,会自动切换到点对点的RPC框架,这也符合微服务的失效转移原则,但是设计中没有进行全面的考虑,一旦一部分服务切换到了点对点的RPC,而一部分的服务没有切换,就导致两个现场池都被撑满,于是超过了1024的限制,就出了问题。 到这里,我们基本可以验证,问题的根源是日志切割导致IO负载增加,然后阻塞线程池,最后发生OOM:unable to create new native thread。 剩下的任务就是最小化重现的问题,通过实践来验证问题的原因。我们与性能压测部门沟通,提出压测需求: Tomcat线程池最大设置为1500.操作系统允许的最大用户进程数1024.在给服务加压的过程中,需要人工制造繁忙的IO操作,IO等待不得低于50%。经过压测压测部门的一下午努力,环境搞定,结果证明完全可以重现此问题。 最后,与所有相关部门讨论和复盘,应用解决方案,解决方案包括: 全部应用改成按照小时切割,或者直接使用log4j的日志滚动功能。Tomcat线程池的线程数设置与操作系统的线程数设置不合理,适当的减少Tomcat线程池线程数量的大小。升级log4j日志,使用logback或者log4j2。这次OOM问题的可以归结为“多个因、多个果、多台机器、多个服务池、不同时间”,针对这个问题,与运维部、监控部和性能压测部门的同事奋斗了几天几夜,终于通过在线上抓取信息、分析问题、在性能压测部门同事的帮助下,最小化重现问题并找到问题的根源原因,最后,针对问题产生的根源提供了有效的方案。 与监控同事现场编写的脚本 本节提供一个笔者在实践过程中解决OOM问题的一个简单脚本,这个脚本是为了解决OOM(unable to create native thread)的问题而在问题机器上临时编写,并临时使用的,脚本并没有写的很专业,笔者也没有进行优化,保持原汁原味的风格,这样能让读者有种身临其境的感觉,只是为了抓取需要的信息并解决问题,但是在线上问题十分火急的情况下,这个脚本会有大用处。 !/bin/bash ps -Leo pid,lwp,user,pcpu,pmem,cmd >> /tmp/pthreads.logecho "ps -Leo pid,lwp,user,pcpu,pmem,cmd >> /tmp/pthreads.log" >> /tmp/pthreads.logecho date >> /tmp/pthreads.logecho 1 pid=ps aux|grep tomcat|grep cwh|awk -F ' ' '{print $2}'echo 2 echo "pstack $pid >> /tmp/pstack.log" >> /tmp/pstack.logpstack $pid >> /tmp/pstack.logecho date >> /tmp/pstack.logecho 3 echo "lsof >> /tmp/sys-o-files.log" >> /tmp/sys-o-files.loglsof >> /tmp/sys-o-files.logecho date >> /tmp/sys-o-files.logecho 4 echo "lsof -p $pid >> /tmp/service-o-files.log" >> /tmp/service-o-files.loglsof -p $pid >> /tmp/service-o-files.logecho date >> /tmp/service-o-files.logecho 5 echo "jstack -l $pid >> /tmp/js.log" >> /tmp/js.logjstack -l -F $pid >> /tmp/js.logecho date >> /tmp/js.logecho 6 echo "free -m >> /tmp/free.log" >> /tmp/free.logfree -m >> /tmp/free.logecho date >> /tmp/free.logecho 7 echo "vmstat 2 1 >> /tmp/vm.log" >> /tmp/vm.logvmstat 2 1 >> /tmp/vm.logecho date >> /tmp/vm.logecho 8 echo "jmap -dump:format=b,file=/tmp/heap.hprof 2743" >> /tmp/jmap.logjmap -dump:format=b,file=/tmp/heap.hprof >> /tmp/jmap.logecho date >> /tmp/jmap.logecho 9 echo end

hiekay 2019-12-02 01:39:43 0 浏览量 回答数 0

回答

在开始谈我对架构本质的理解之前,先谈谈对今天技术沙龙主题的个人见解,千万级规模的网站感觉数量级是非常大的,对这个数量级我们战略上 要重 视 它 , 战术上又 要 藐 视 它。先举个例子感受一下千万级到底是什么数量级?现在很流行的优步(Uber),从媒体公布的信息看,它每天接单量平均在百万左右, 假如每天有10个小时的服务时间,平均QPS只有30左右。对于一个后台服务器,单机的平均QPS可以到达800-1000,单独看写的业务量很简单 。为什么我们又不能说轻视它?第一,我们看它的数据存储,每天一百万的话,一年数据量的规模是多少?其次,刚才说的订单量,每一个订单要推送给附近的司机、司机要并发抢单,后面业务场景的访问量往往是前者的上百倍,轻松就超过上亿级别了。 今天我想从架构的本质谈起之后,希望大家理解在做一些建构设计的时候,它的出发点以及它解决的问题是什么。 架构,刚开始的解释是我从知乎上看到的。什么是架构?有人讲, 说架构并不是一 个很 悬 乎的 东西 , 实际 上就是一个架子 , 放一些 业务 和算法,跟我们的生活中的晾衣架很像。更抽象一点,说架构其 实 是 对 我 们 重复性业务 的抽象和我 们 未来 业务 拓展的前瞻,强调过去的经验和你对整个行业的预见。 我们要想做一个架构的话需要哪些能力?我觉得最重要的是架构师一个最重要的能力就是你要有 战 略分解能力。这个怎么来看呢: 第一,你必须要有抽象的能力,抽象的能力最基本就是去重,去重在整个架构中体现在方方面面,从定义一个函数,到定义一个类,到提供的一个服务,以及模板,背后都是要去重提高可复用率。 第二, 分类能力。做软件需要做对象的解耦,要定义对象的属性和方法,做分布式系统的时候要做服务的拆分和模块化,要定义服务的接口和规范。 第三, 算法(性能),它的价值体现在提升系统的性能,所有性能的提升,最终都会落到CPU,内存,IO和网络这4大块上。 这一页PPT举了一些例子来更深入的理解常见技术背后的架构理念。 第一个例子,在分布式系统我们会做 MySQL分 库 分表,我们要从不同的库和表中读取数据,这样的抽象最直观就是使用模板,因为绝大多数SQL语义是相同的,除了路由到哪个库哪个表,如果不使用Proxy中间件,模板就是性价比最高的方法。 第二看一下加速网络的CDN,它是做速度方面的性能提升,刚才我们也提到从CPU、内存、IO、网络四个方面来考虑,CDN本质上一个是做网络智能调度优化,另一个是多级缓存优化。 第三个看一下服务化,刚才已经提到了,各个大网站转型过程中一定会做服务化,其实它就是做抽象和做服务的拆分。第四个看一下消息队列,本质上还是做分类,只不过不是两个边际清晰的类,而是把两个边际不清晰的子系统通过队列解构并且异步化。新浪微博整体架构是什么样的 接下我们看一下微博整体架构,到一定量级的系统整个架构都会变成三层,客户端包括WEB、安卓和IOS,这里就不说了。接着还都会有一个接口层, 有三个主要作用: 第一个作用,要做 安全隔离,因为前端节点都是直接和用户交互,需要防范各种恶意攻击; 第二个还充当着一个 流量控制的作用,大家知道,在2014年春节的时候,微信红包,每分钟8亿多次的请求,其实真正到它后台的请求量,只有十万左右的数量级(这里的数据可能不准),剩余的流量在接口层就被挡住了; 第三,我们看对 PC 端和移 动 端的需求不一样的,所以我们可以进行拆分。接口层之后是后台,可以看到微博后台有三大块: 一个是 平台服 务, 第二, 搜索, 第三, 大数据。到了后台的各种服务其实都是处理的数据。 像平台的业务部门,做的就是 数据存储和读 取,对搜索来说做的是 数据的 检 索,对大数据来说是做的数据的 挖掘。微博其实和淘宝是很类似 微博其实和淘宝是很类似的。一般来说,第一代架构,基本上能支撑到用户到 百万 级别,到第二代架构基本能支撑到 千万 级别都没什么问题,当业务规模到 亿级别时,需要第三代的架构。 从 LAMP 的架构到面向服 务 的架构,有几个地方是非常难的,首先不可能在第一代基础上通过简单的修修补补满足用户量快速增长的,同时线上业务又不能停, 这是我们常说的 在 飞 机上 换 引擎的 问题。前两天我有一个朋友问我,说他在内部推行服务化的时候,把一个模块服务化做完了,其他部门就是不接。我建议在做服务化的时候,首先更多是偏向业务的梳理,同时要找准一个很好的切入点,既有架构和服务化上的提升,业务方也要有收益,比如提升性能或者降低维护成本同时升级过程要平滑,建议开始从原子化服务切入,比如基础的用户服务, 基础的短消息服务,基础的推送服务。 第二,就是可 以做无状 态 服 务,后面会详细讲,还有数据量大了后需要做数据Sharding,后面会将。 第三代 架构 要解决的 问题,就是用户量和业务趋于稳步增加(相对爆发期的指数级增长),更多考虑技术框架的稳定性, 提升系统整体的性能,降低成本,还有对整个系统监控的完善和升级。 大型网站的系统架构是如何演变的 我们通过通过数据看一下它的挑战,PV是在10亿级别,QPS在百万,数据量在千亿级别。我们可用性,就是SLA要求4个9,接口响应最多不能超过150毫秒,线上所有的故障必须得在5分钟内解决完。如果说5分钟没处理呢?那会影响你年终的绩效考核。2015年微博DAU已经过亿。我们系统有上百个微服务,每周会有两次的常规上线和不限次数的紧急上线。我们的挑战都一样,就是数据量,bigger and bigger,用户体验是faster and faster,业务是more and more。互联网业务更多是产品体验驱动, 技 术 在 产 品 体验上最有效的贡献 , 就是你的性能 越来越好 。 每次降低加载一个页面的时间,都可以间接的降低这个页面上用户的流失率。微博的技术挑战和正交分解法解析架构 下面看一下 第三代的 架构 图 以及 我 们 怎么用正交分解法 阐 述。 我们可以看到我们从两个维度,横轴和纵轴可以看到。 一个 维 度 是 水平的 分层 拆分,第二从垂直的维度会做拆分。水平的维度从接口层、到服务层到数据存储层。垂直怎么拆分,会用业务架构、技术架构、监控平台、服务治理等等来处理。我相信到第二代的时候很多架构已经有了业务架构和技术架构的拆分。我们看一下, 接口层有feed、用户关系、通讯接口;服务层,SOA里有基层服务、原子服务和组合服务,在微博我们只有原子服务和组合服务。原子服务不依赖于任何其他服务,组合服务由几个原子服务和自己的业务逻辑构建而成 ,资源层负责海量数据的存储(后面例子会详细讲)。技 术框架解决 独立于 业务 的海量高并发场景下的技术难题,由众多的技术组件共同构建而成 。在接口层,微博使用JERSY框架,帮助你做参数的解析,参数的验证,序列化和反序列化;资源层,主要是缓存、DB相关的各类组件,比如Cache组件和对象库组件。监 控平台和服 务 治理 , 完成系统服务的像素级监控,对分布式系统做提前诊断、预警以及治理。包含了SLA规则的制定、服务监控、服务调用链监控、流量监控、错误异常监控、线上灰度发布上线系统、线上扩容缩容调度系统等。 下面我们讲一下常见的设计原则。 第一个,首先是系统架构三个利器: 一个, 我 们 RPC 服 务组 件 (这里不讲了), 第二个,我们 消息中 间 件 。消息中间件起的作用:可以把两个模块之间的交互异步化,其次可以把不均匀请求流量输出为匀速的输出流量,所以说消息中间件 异步化 解耦 和流量削峰的利器。 第三个是配置管理,它是 代码级灰度发布以及 保障系统降级的利器。 第二个 , 无状态 , 接口 层 最重要的就是无状 态。我们在电商网站购物,在这个过程中很多情况下是有状态的,比如我浏览了哪些商品,为什么大家又常说接口层是无状态的,其实我们把状态从接口层剥离到了数据层。像用户在电商网站购物,选了几件商品,到了哪一步,接口无状态后,状态要么放在缓存中,要么放在数据库中, 其 实 它并不是没有状 态 , 只是在 这 个 过 程中我 们 要把一些有状 态 的 东 西抽离出来 到了数据层。 第三个, 数据 层 比服 务层 更需要 设计,这是一条非常重要的经验。对于服务层来说,可以拿PHP写,明天你可以拿JAVA来写,但是如果你的数据结构开始设计不合理,将来数据结构的改变会花费你数倍的代价,老的数据格式向新的数据格式迁移会让你痛不欲生,既有工作量上的,又有数据迁移跨越的时间周期,有一些甚至需要半年以上。 第四,物理结构与逻辑结构的映射,上一张图看到两个维度切成十二个区间,每个区间代表一个技术领域,这个可以看做我们的逻辑结构。另外,不论后台还是应用层的开发团队,一般都会分几个垂直的业务组加上一个基础技术架构组,这就是从物理组织架构到逻辑的技术架构的完美的映射,精细化团队分工,有利于提高沟通协作的效率 。 第五, www .sanhao.com 的访问过程,我们这个架构图里没有涉及到的,举个例子,比如当你在浏览器输入www.sanhao网址的时候,这个请求在接口层之前发生了什么?首先会查看你本机DNS以及DNS服务,查找域名对应的IP地址,然后发送HTTP请求过去。这个请求首先会到前端的VIP地址(公网服务IP地址),VIP之后还要经过负载均衡器(Nginx服务器),之后才到你的应用接口层。在接口层之前发生了这么多事,可能有用户报一个问题的时候,你通过在接口层查日志根本发现不了问题,原因就是问题可能发生在到达接口层之前了。 第六,我们说分布式系统,它最终的瓶颈会落在哪里呢?前端时间有一个网友跟我讨论的时候,说他们的系统遇到了一个瓶颈, 查遍了CPU,内存,网络,存储,都没有问题。我说你再查一遍,因为最终你不论用上千台服务器还是上万台服务器,最终系统出瓶颈的一定会落在某一台机(可能是叶子节点也可能是核心的节点),一定落在CPU、内存、存储和网络上,最后查出来问题出在一台服务器的网卡带宽上。微博多级双机房缓存架构 接下来我们看一下微博的Feed多级缓存。我们做业务的时候,经常很少做业务分析,技术大会上的分享又都偏向技术架构。其实大家更多的日常工作是需要花费更多时间在业务优化上。这张图是统计微博的信息流前几页的访问比例,像前三页占了97%,在做缓存设计的时候,我们最多只存最近的M条数据。 这里强调的就是做系统设计 要基于用 户 的 场 景 , 越细致越好 。举了一个例子,大家都会用电商,电商在双十一会做全国范围内的活动,他们做设计的时候也会考虑场景的,一个就是购物车,我曾经跟相关开发讨论过,购物车是在双十一之前用户的访问量非常大,就是不停地往里加商品。在真正到双十一那天他不会往购物车加东西了,但是他会频繁的浏览购物车。针对这个场景,活动之前重点设计优化购物车的写场景, 活动开始后优化购物车的读场景。 你看到的微博是由哪些部分聚合而成的呢?最右边的是Feed,就是微博所有关注的人,他们的微博所组成的。微博我们会按照时间顺序把所有关注人的顺序做一个排序。随着业务的发展,除了跟时间序相关的微博还有非时间序的微博,就是会有广告的要求,增加一些广告,还有粉丝头条,就是拿钱买的,热门微博,都会插在其中。分发控制,就是说和一些推荐相关的,我推荐一些相关的好友的微博,我推荐一些你可能没有读过的微博,我推荐一些其他类型的微博。 当然对非时序的微博和分发控制微博,实际会起多个并行的程序来读取,最后同步做统一的聚合。这里稍微分享一下, 从SNS社交领域来看,国内现在做的比较好的三个信息流: 微博 是 基于弱关系的媒体信息流 ; 朋友圈是基于 强 关系的信息流 ; 另外一个做的比 较 好的就是今日 头 条 , 它并不是基于关系来构建信息流 , 而是基于 兴趣和相关性的个性化推荐 信息流 。 信息流的聚合,体现在很多很多的产品之中,除了SNS,电商里也有信息流的聚合的影子。比如搜索一个商品后出来的列表页,它的信息流基本由几部分组成:第一,打广告的;第二个,做一些推荐,热门的商品,其次,才是关键字相关的搜索结果。 信息流 开始的时候 很 简单 , 但是到后期会 发现 , 你的 这 个流 如何做控制分发 , 非常复杂, 微博在最近一两年一直在做 这样 的工作。刚才我们是从业务上分析,那么技术上怎么解决高并发,高性能的问题?微博访问量很大的时候,底层存储是用MySQL数据库,当然也会有其他的。对于查询请求量大的时候,大家知道一定有缓存,可以复用可重用的计算结果。可以看到,发一条微博,我有很多粉丝,他们都会来看我发的内容,所以 微博是最适合使用 缓 存 的系统,微博的读写比例基本在几十比一。微博使用了 双 层缓 存,上面是L1,每个L1上都是一组(包含4-6台机器),左边的框相当于一个机房,右边又是一个机房。在这个系统中L1缓存所起的作用是什么? 首先,L1 缓 存增加整个系 统 的 QPS, 其次 以低成本灵活扩容的方式 增加 系统 的 带宽 。想象一个极端场景,只有一篇博文,但是它的访问量无限增长,其实我们不需要影响L2缓存,因为它的内容存储的量小,但它就是访问量大。这种场景下,你就需要使用L1来扩容提升QPS和带宽瓶颈。另外一个场景,就是L2级缓存发生作用,比如我有一千万个用户,去访问的是一百万个用户的微博 ,这个时候,他不只是说你的吞吐量和访问带宽,就是你要缓存的博文的内容也很多了,这个时候你要考虑缓存的容量, 第二 级缓 存更多的是从容量上来 规划,保证请求以较小的比例 穿透到 后端的 数据 库 中 ,根据你的用户模型你可以估出来,到底有百分之多少的请求不能穿透到DB, 评估这个容量之后,才能更好的评估DB需要多少库,需要承担多大的访问的压力。另外,我们看双机房的话,左边一个,右边一个。 两个机房是互 为 主 备 , 或者互 为热备 。如果两个用户在不同地域,他们访问两个不同机房的时候,假设用户从IDC1过来,因为就近原理,他会访问L1,没有的话才会跑到Master,当在IDC1没找到的时候才会跑到IDC2来找。同时有用户从IDC2访问,也会有请求从L1和Master返回或者到IDC1去查找。 IDC1 和 IDC2 ,两个机房都有全量的用户数据,同时在线提供服务,但是缓存查询又遵循最近访问原理。还有哪些多级缓存的例子呢?CDN是典型的多级缓存。CDN在国内各个地区做了很多节点,比如在杭州市部署一个节点时,在机房里肯定不止一台机器,那么对于一个地区来说,只有几台服务器到源站回源,其他节点都到这几台服务器回源即可,这么看CDN至少也有两级。Local Cache+ 分布式 缓 存,这也是常见的一种策略。有一种场景,分布式缓存并不适用, 比如 单 点 资 源 的爆发性峰值流量,这个时候使用Local Cache + 分布式缓存,Local Cache 在 应用 服 务 器 上用很小的 内存资源 挡住少量的 极端峰值流量,长尾的流量仍然访问分布式缓存,这样的Hybrid缓存架构通过复用众多的应用服务器节点,降低了系统的整体成本。 我们来看一下 Feed 的存 储 架构,微博的博文主要存在MySQL中。首先来看内容表,这个比较简单,每条内容一个索引,每天建一张表,其次看索引表,一共建了两级索引。首先想象一下用户场景,大部分用户刷微博的时候,看的是他关注所有人的微博,然后按时间来排序。仔细分析发现在这个场景下, 跟一个用户的自己的相关性很小了。所以在一级索引的时候会先根据关注的用户,取他们的前条微博ID,然后聚合排序。我们在做哈希(分库分表)的时候,同时考虑了按照UID哈希和按照时间维度。很业务和时间相关性很高的,今天的热点新闻,明天就没热度了,数据的冷热非常明显,这种场景就需要按照时间维度做分表,首先冷热数据做了分离(可以对冷热数据采用不同的存储方案来降低成本),其次, 很容止控制我数据库表的爆炸。像微博如果只按照用户维度区分,那么这个用户所有数据都在一张表里,这张表就是无限增长的,时间长了查询会越来越慢。二级索引,是我们里面一个比较特殊的场景,就是我要快速找到这个人所要发布的某一时段的微博时,通过二级索引快速定位。 分布式服务追踪系统 分布式追踪服务系统,当系统到千万级以后的时候,越来越庞杂,所解决的问题更偏向稳定性,性能和监控。刚才说用户只要有一个请求过来,你可以依赖你的服务RPC1、RPC2,你会发现RPC2又依赖RPC3、RPC4。分布式服务的时候一个痛点,就是说一个请求从用户过来之后,在后台不同的机器之间不停的调用并返回。 当你发现一个问题的时候,这些日志落在不同的机器上,你也不知道问题到底出在哪儿,各个服务之间互相隔离,互相之间没有建立关联。所以导致排查问题基本没有任何手段,就是出了问题没法儿解决。 我们要解决的问题,我们刚才说日志互相隔离,我们就要把它建立联系。建立联系我们就有一个请求ID,然后结合RPC框架, 服务治理功能。假设请求从客户端过来,其中包含一个ID 101,到服务A时仍然带有ID 101,然后调用RPC1的时候也会标识这是101 ,所以需要 一个唯一的 请求 ID 标识 递归迭代的传递到每一个 相关 节点。第二个,你做的时候,你不能说每个地方都加,对业务系统来说需要一个框架来完成这个工作, 这 个框架要 对业务 系 统 是最低侵入原 则 , 用 JAVA 的 话 就可以用 AOP,要做到零侵入的原则,就是对所有相关的中间件打点,从接口层组件(HTTP Client、HTTP Server)至到服务层组件(RPC Client、RPC Server),还有数据访问中间件的,这样业务系统只需要少量的配置信息就可以实现全链路监控 。为什么要用日志?服务化以后,每个服务可以用不同的开发语言, 考虑多种开发语言的兼容性 , 内部定 义标 准化的日志 是唯一且有效的办法。最后,如何构建基于GPS导航的路况监控?我们刚才讲分布式服务追踪。分布式服务追踪能解决的问题, 如果 单一用 户发现问题 后 , 可以通 过请 求 ID 快速找到 发 生 问题 的 节 点在什么,但是并没有解决如何发现问题。我们看现实中比较容易理解的道路监控,每辆车有GPS定位,我想看北京哪儿拥堵的时候,怎么做? 第一个 , 你肯定要知道每个 车 在什么位置,它走到哪儿了。其实可以说每个车上只要有一个标识,加上每一次流动的信息,就可以看到每个车流的位置和方向。 其次如何做 监 控和 报 警,我们怎么能了解道路的流量状况和负载,并及时报警。我们要定义这条街道多宽多高,单位时间可以通行多少辆车,这就是道路的容量。有了道路容量,再有道路的实时流量,我们就可以基于实习路况做预警? 对应于 分布式系 统 的话如何构建? 第一 , 你要 定义 每个服 务节 点它的 SLA A 是多少 ?SLA可以从系统的CPU占用率、内存占用率、磁盘占用率、QPS请求数等来定义,相当于定义系统的容量。 第二个 , 统计 线 上 动态 的流量,你要知道服务的平均QPS、最低QPS和最大QPS,有了流量和容量,就可以对系统做全面的监控和报警。 刚才讲的是理论,实际情况肯定比这个复杂。微博在春节的时候做许多活动,必须保障系统稳定,理论上你只要定义容量和流量就可以。但实际远远不行,为什么?有技术的因素,有人为的因素,因为不同的开发定义的流量和容量指标有主观性,很难全局量化标准,所以真正流量来了以后,你预先评估的系统瓶颈往往不正确。实际中我们在春节前主要采取了三个措施:第一,最简单的就是有降 级 的 预 案,流量超过系统容量后,先把哪些功能砍掉,需要有明确的优先级 。第二个, 线上全链路压测,就是把现在的流量放大到我们平常流量的五倍甚至十倍(比如下线一半的服务器,缩容而不是扩容),看看系统瓶颈最先发生在哪里。我们之前有一些例子,推测系统数据库会先出现瓶颈,但是实测发现是前端的程序先遇到瓶颈。第三,搭建在线 Docker 集群 , 所有业务共享备用的 Docker集群资源,这样可以极大的避免每个业务都预留资源,但是实际上流量没有增长造成的浪费。 总结 接下来说的是如何不停的学习和提升,这里以Java语言为例,首先, 一定要 理解 JAVA;第二步,JAVA完了以后,一定要 理 解 JVM;其次,还要 理解 操作系统;再次还是要了解一下 Design Pattern,这将告诉你怎么把过去的经验抽象沉淀供将来借鉴;还要学习 TCP/IP、 分布式系 统、数据结构和算法。

hiekay 2019-12-02 01:39:25 0 浏览量 回答数 0

阿里云试用中心,为您提供0门槛上云实践机会!

0元试用32+款产品,最高免费12个月!拨打95187-1,咨询专业上云建议!

回答

一、基础篇 1.1、Java基础 面向对象的特征:继承、封装和多态 final, finally, finalize 的区别 Exception、Error、运行时异常与一般异常有何异同 请写出5种常见到的runtime exception int 和 Integer 有什么区别,Integer的值缓存范围 包装类,装箱和拆箱 String、StringBuilder、StringBuffer 重载和重写的区别 抽象类和接口有什么区别 说说反射的用途及实现 说说自定义注解的场景及实现 HTTP请求的GET与POST方式的区别 Session与Cookie区别 列出自己常用的JDK包 MVC设计思想 equals与==的区别 hashCode和equals方法的区别与联系 什么是Java序列化和反序列化,如何实现Java序列化?或者请解释Serializable 接口的作用 Object类中常见的方法,为什么wait notify会放在Object里边? Java的平台无关性如何体现出来的 JDK和JRE的区别 Java 8有哪些新特性 1.2、Java常见集合 List 和 Set 区别 Set和hashCode以及equals方法的联系 List 和 Map 区别 Arraylist 与 LinkedList 区别 ArrayList 与 Vector 区别 HashMap 和 Hashtable 的区别 HashSet 和 HashMap 区别 HashMap 和 ConcurrentHashMap 的区别 HashMap 的工作原理及代码实现,什么时候用到红黑树 多线程情况下HashMap死循环的问题 HashMap出现Hash DOS攻击的问题 ConcurrentHashMap 的工作原理及代码实现,如何统计所有的元素个数 手写简单的HashMap 看过那些Java集合类的源码 1.3、进程和线程 线程和进程的概念、并行和并发的概念 创建线程的方式及实现 进程间通信的方式 说说 CountDownLatch、CyclicBarrier 原理和区别 说说 Semaphore 原理 说说 Exchanger 原理 ThreadLocal 原理分析,ThreadLocal为什么会出现OOM,出现的深层次原理 讲讲线程池的实现原理 线程池的几种实现方式 线程的生命周期,状态是如何转移的 可参考:《Java多线程编程核心技术》 1.4、锁机制 说说线程安全问题,什么是线程安全,如何保证线程安全 重入锁的概念,重入锁为什么可以防止死锁 产生死锁的四个条件(互斥、请求与保持、不剥夺、循环等待) 如何检查死锁(通过jConsole检查死锁) volatile 实现原理(禁止指令重排、刷新内存) synchronized 实现原理(对象监视器) synchronized 与 lock 的区别 AQS同步队列 CAS无锁的概念、乐观锁和悲观锁 常见的原子操作类 什么是ABA问题,出现ABA问题JDK是如何解决的 乐观锁的业务场景及实现方式 Java 8并法包下常见的并发类 偏向锁、轻量级锁、重量级锁、自旋锁的概念 可参考:《Java多线程编程核心技术》 1.5、JVM JVM运行时内存区域划分 内存溢出OOM和堆栈溢出SOE的示例及原因、如何排查与解决 如何判断对象是否可以回收或存活 常见的GC回收算法及其含义 常见的JVM性能监控和故障处理工具类:jps、jstat、jmap、jinfo、jconsole等 JVM如何设置参数 JVM性能调优 类加载器、双亲委派模型、一个类的生命周期、类是如何加载到JVM中的 类加载的过程:加载、验证、准备、解析、初始化 强引用、软引用、弱引用、虚引用 Java内存模型JMM 1.6、设计模式 常见的设计模式 设计模式的的六大原则及其含义 常见的单例模式以及各种实现方式的优缺点,哪一种最好,手写常见的单利模式 设计模式在实际场景中的应用 Spring中用到了哪些设计模式 MyBatis中用到了哪些设计模式 你项目中有使用哪些设计模式 说说常用开源框架中设计模式使用分析 动态代理很重要!!! 1.7、数据结构 树(二叉查找树、平衡二叉树、红黑树、B树、B+树) 深度有限算法、广度优先算法 克鲁斯卡尔算法、普林母算法、迪克拉斯算法 什么是一致性Hash及其原理、Hash环问题 常见的排序算法和查找算法:快排、折半查找、堆排序等 1.8、网络/IO基础 BIO、NIO、AIO的概念 什么是长连接和短连接 Http1.0和2.0相比有什么区别,可参考《Http 2.0》 Https的基本概念 三次握手和四次挥手、为什么挥手需要四次 从游览器中输入URL到页面加载的发生了什么?可参考《从输入URL到页面加载发生了什么》 二、数据存储和消息队列 2.1、数据库 MySQL 索引使用的注意事项 DDL、DML、DCL分别指什么 explain命令 left join,right join,inner join 数据库事物ACID(原子性、一致性、隔离性、持久性) 事物的隔离级别(读未提交、读以提交、可重复读、可序列化读) 脏读、幻读、不可重复读 数据库的几大范式 数据库常见的命令 说说分库与分表设计 分库与分表带来的分布式困境与应对之策(如何解决分布式下的分库分表,全局表?) 说说 SQL 优化之道 MySQL遇到的死锁问题、如何排查与解决 存储引擎的 InnoDB与MyISAM区别,优缺点,使用场景 索引类别(B+树索引、全文索引、哈希索引)、索引的原理 什么是自适应哈希索引(AHI) 为什么要用 B+tree作为MySQL索引的数据结构 聚集索引与非聚集索引的区别 遇到过索引失效的情况没,什么时候可能会出现,如何解决 limit 20000 加载很慢怎么解决 如何选择合适的分布式主键方案 选择合适的数据存储方案 常见的几种分布式ID的设计方案 常见的数据库优化方案,在你的项目中数据库如何进行优化的 2.2、Redis Redis 有哪些数据类型,可参考《Redis常见的5种不同的数据类型详解》 Redis 内部结构 Redis 使用场景 Redis 持久化机制,可参考《使用快照和AOF将Redis数据持久化到硬盘中》 Redis 集群方案与实现 Redis 为什么是单线程的? 缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级 使用缓存的合理性问题 Redis常见的回收策略 2.3、消息队列 消息队列的使用场景 消息的重发补偿解决思路 消息的幂等性解决思路 消息的堆积解决思路 自己如何实现消息队列 如何保证消息的有序性 三、开源框架和容器 3.1、SSM/Servlet Servlet的生命周期 转发与重定向的区别 BeanFactory 和 ApplicationContext 有什么区别 Spring Bean 的生命周期 Spring IOC 如何实现 Spring中Bean的作用域,默认的是哪一个 说说 Spring AOP、Spring AOP 实现原理 动态代理(CGLib 与 JDK)、优缺点、性能对比、如何选择 Spring 事务实现方式、事务的传播机制、默认的事务类别 Spring 事务底层原理 Spring事务失效(事务嵌套),JDK动态代理给Spring事务埋下的坑,可参考《JDK动态代理给Spring事务埋下的坑!》 如何自定义注解实现功能 Spring MVC 运行流程 Spring MVC 启动流程 Spring 的单例实现原理 Spring 框架中用到了哪些设计模式 Spring 其他产品(Srping Boot、Spring Cloud、Spring Secuirity、Spring Data、Spring AMQP 等) 有没有用到Spring Boot,Spring Boot的认识、原理 MyBatis的原理 可参考《为什么会有Spring》 可参考《为什么会有Spring AOP》 3.2、Netty 为什么选择 Netty 说说业务中,Netty 的使用场景 原生的 NIO 在 JDK 1.7 版本存在 epoll bug 什么是TCP 粘包/拆包 TCP粘包/拆包的解决办法 Netty 线程模型 说说 Netty 的零拷贝 Netty 内部执行流程 Netty 重连实现 3.3、Tomcat Tomcat的基础架构(Server、Service、Connector、Container) Tomcat如何加载Servlet的 Pipeline-Valve机制 可参考:《四张图带你了解Tomcat系统架构!》 四、分布式 4.1、Nginx 请解释什么是C10K问题或者知道什么是C10K问题吗? Nginx简介,可参考《Nginx简介》 正向代理和反向代理. Nginx几种常见的负载均衡策略 Nginx服务器上的Master和Worker进程分别是什么 使用“反向代理服务器”的优点是什么? 4.2、分布式其他 谈谈业务中使用分布式的场景 Session 分布式方案 Session 分布式处理 分布式锁的应用场景、分布式锁的产生原因、基本概念 分布是锁的常见解决方案 分布式事务的常见解决方案 集群与负载均衡的算法与实现 说说分库与分表设计,可参考《数据库分库分表策略的具体实现方案》 分库与分表带来的分布式困境与应对之策 4.3、Dubbo 什么是Dubbo,可参考《Dubbo入门》 什么是RPC、如何实现RPC、RPC 的实现原理,可参考《基于HTTP的RPC实现》 Dubbo中的SPI是什么概念 Dubbo的基本原理、执行流程 五、微服务 5.1、微服务 前后端分离是如何做的? 微服务哪些框架 Spring Could的常见组件有哪些?可参考《Spring Cloud概述》 领域驱动有了解吗?什么是领域驱动模型?充血模型、贫血模型 JWT有了解吗,什么是JWT,可参考《前后端分离利器之JWT》 你怎么理解 RESTful 说说如何设计一个良好的 API 如何理解 RESTful API 的幂等性 如何保证接口的幂等性 说说 CAP 定理、BASE 理论 怎么考虑数据一致性问题 说说最终一致性的实现方案 微服务的优缺点,可参考《微服务批判》 微服务与 SOA 的区别 如何拆分服务、水平分割、垂直分割 如何应对微服务的链式调用异常 如何快速追踪与定位问题 如何保证微服务的安全、认证 5.2、安全问题 如何防范常见的Web攻击、如何方式SQL注入 服务端通信安全攻防 HTTPS原理剖析、降级攻击、HTTP与HTTPS的对比 5.3、性能优化 性能指标有哪些 如何发现性能瓶颈 性能调优的常见手段 说说你在项目中如何进行性能调优 六、其他 6.1、设计能力 说说你在项目中使用过的UML图 你如何考虑组件化、服务化、系统拆分 秒杀场景如何设计 可参考:《秒杀系统的技术挑战、应对策略以及架构设计总结一二!》 6.2、业务工程 说说你的开发流程、如何进行自动化部署的 你和团队是如何沟通的 你如何进行代码评审 说说你对技术与业务的理解 说说你在项目中遇到感觉最难Bug,是如何解决的 介绍一下工作中的一个你认为最有价值的项目,以及在这个过程中的角色、解决的问题、你觉得你们项目还有哪些不足的地方 6.3、软实力 说说你的优缺点、亮点 说说你最近在看什么书、什么博客、在研究什么新技术、再看那些开源项目的源代码 说说你觉得最有意义的技术书籍 工作之余做什么事情、平时是如何学习的,怎样提升自己的能力 说说个人发展方向方面的思考 说说你认为的服务端开发工程师应该具备哪些能力 说说你认为的架构师是什么样的,架构师主要做什么 如何看待加班的问题

徐刘根 2020-03-31 11:22:08 0 浏览量 回答数 0

问题

【精品问答】python技术1000问(1)

问问小秘 2019-12-01 21:57:48 455812 浏览量 回答数 21

问题

做好软件测试需要具备的思维方式

技术小菜鸟 2019-12-01 21:25:46 2817 浏览量 回答数 1

问题

JDBC 使用经验之谈 为什么选择JDBC JDBC使用经验:报错

kun坤 2020-06-09 22:12:13 0 浏览量 回答数 1

问题

Java技术1000问(3)【精品问答】

问问小秘 2020-06-02 14:27:10 42 浏览量 回答数 1

问题

【直播回顾】21天搭建推荐系统:实现“千人千面”个性化推荐(含视频)

小柒2012 2019-12-01 21:21:27 7489 浏览量 回答数 1

回答

在日常开发中,我们会经常要在类中定义布尔类型的变量,比如在给外部系统提供一个RPC接口的时候,我们一般会定义一个字段表示本次请求是否成功的。 关于这个"本次请求是否成功"的字段的定义,其实是有很多种讲究和坑的,稍有不慎就会掉入坑里,作者在很久之前就遇到过类似的问题,本文就来围绕这个简单分析一下。到底该如何定一个布尔类型的成员变量。 一般情况下,我们可以有以下四种方式来定义一个布尔类型的成员变量: boolean success boolean isSuccess Boolean success Boolean isSuccess 以上四种定义形式,你日常开发中最常用的是哪种呢?到底哪一种才是正确的使用姿势呢? 通过观察我们可以发现,前两种和后两种的主要区别是变量的类型不同,前者使用的是boolean,后者使用的是Boolean。 另外,第一种和第三种在定义变量的时候,变量命名是success,而另外两种使用isSuccess来命名的。 首先,我们来分析一下,到底应该是用success来命名,还是使用isSuccess更好一点。 success 还是 isSuccess 到底应该是用success还是isSuccess来给变量命名呢?从语义上面来讲,两种命名方式都可以讲的通,并且也都没有歧义。那么还有什么原则可以参考来让我们做选择呢。 在阿里巴巴Java开发手册中关于这一点,有过一个『强制性』规定:  那么,为什么会有这样的规定呢?我们看一下POJO中布尔类型变量不同的命名有什么区别吧。 class Model1 { private Boolean isSuccess; public void setSuccess(Boolean success) { isSuccess = success; } public Boolean getSuccess() { return isSuccess; } } class Model2 { private Boolean success; public Boolean getSuccess() { return success; } public void setSuccess(Boolean success) { this.success = success; } } class Model3 { private boolean isSuccess; public boolean isSuccess() { return isSuccess; } public void setSuccess(boolean success) { isSuccess = success; } } class Model4 { private boolean success; public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } } 以上代码的setter/getter是使用Intellij IDEA自动生成的,仔细观察以上代码,你会发现以下规律: 基本类型自动生成的getter和setter方法,名称都是isXXX()和setXXX()形式的。包装类型自动生成的getter和setter方法,名称都是getXXX()和setXXX()形式的。 既然,我们已经达成一致共识使用基本类型boolean来定义成员变量了,那么我们再来具体看下Model3和Model4中的setter/getter有何区别。 我们可以发现,虽然Model3和Model4中的成员变量的名称不同,一个是success,另外一个是isSuccess,但是他们自动生成的getter和setter方法名称都是isSuccess和setSuccess。 Java Bean中关于setter/getter的规范 关于Java Bean中的getter/setter方法的定义其实是有明确的规定的,根据JavaBeans(TM) Specification规定,如果是普通的参数propertyName,要以以下方式定义其setter/getter: public <PropertyType> get<PropertyName>(); public void set<PropertyName>(<PropertyType> a); 但是,布尔类型的变量propertyName则是单独定义的: public boolean is<PropertyName>(); public void set<PropertyName>(boolean m);  通过对照这份JavaBeans规范,我们发现,在Model4中,变量名为isSuccess,如果严格按照规范定义的话,他的getter方法应该叫isIsSuccess。但是很多IDE都会默认生成为isSuccess。 那这样做会带来什么问题呢。 在一般情况下,其实是没有影响的。但是有一种特殊情况就会有问题,那就是发生序列化的时候。 序列化带来的影响 关于序列化和反序列化请参考Java对象的序列化与反序列化。我们这里拿比较常用的JSON序列化来举例,看看看常用的fastJson、jackson和Gson之间有何区别: public class BooleanMainTest { public static void main(String[] args) throws IOException { //定一个Model3类型 Model3 model3 = new Model3(); model3.setSuccess(true); //使用fastjson(1.2.16)序列化model3成字符串并输出 System.out.println("Serializable Result With fastjson :" + JSON.toJSONString(model3)); //使用Gson(2.8.5)序列化model3成字符串并输出 Gson gson =new Gson(); System.out.println("Serializable Result With Gson :" +gson.toJson(model3)); //使用jackson(2.9.7)序列化model3成字符串并输出 ObjectMapper om = new ObjectMapper(); System.out.println("Serializable Result With jackson :" +om.writeValueAsString(model3)); } } class Model3 implements Serializable { private static final long serialVersionUID = 1836697963736227954L; private boolean isSuccess; public boolean isSuccess() { return isSuccess; } public void setSuccess(boolean success) { isSuccess = success; } public String getHollis(){ return "hollischuang"; } } 以上代码的Model3中,只有一个成员变量即isSuccess,三个方法,分别是IDE帮我们自动生成的isSuccess和setSuccess,另外一个是作者自己增加的一个符合getter命名规范的方法。 以上代码输出结果: Serializable Result With fastjson :{"hollis":"hollischuang","success":true} Serializable Result With Gson :{"isSuccess":true} Serializable Result With jackson :{"success":true,"hollis":"hollischuang"} 在fastjson和jackson的结果中,原来类中的isSuccess字段被序列化成success,并且其中还包含hollis值。而Gson中只有isSuccess字段。 我们可以得出结论:fastjson和jackson在把对象序列化成json字符串的时候,是通过反射遍历出该类中的所有getter方法,得到getHollis和isSuccess,然后根据JavaBeans规则,他会认为这是两个属性hollis和success的值。直接序列化成json:{"hollis":"hollischuang","success":true} 但是Gson并不是这么做的,他是通过反射遍历该类中的所有属性,并把其值序列化成json:{"isSuccess":true} 可以看到,由于不同的序列化工具,在进行序列化的时候使用到的策略是不一样的,所以,对于同一个类的同一个对象的序列化结果可能是不同的。 前面提到的关于对getHollis的序列化只是为了说明fastjson、jackson和Gson之间的序列化策略的不同,我们暂且把他放到一边,我们把他从Model3中删除后,重新执行下以上代码,得到结果: Serializable Result With fastjson :{"success":true} Serializable Result With Gson :{"isSuccess":true} Serializable Result With jackson :{"success":true} 现在,不同的序列化框架得到的json内容并不相同,如果对于同一个对象,我使用fastjson进行序列化,再使用Gson反序列化会发生什么? public class BooleanMainTest { public static void main(String[] args) throws IOException { Model3 model3 = new Model3(); model3.setSuccess(true); Gson gson =new Gson(); System.out.println(gson.fromJson(JSON.toJSONString(model3),Model3.class)); } } class Model3 implements Serializable { private static final long serialVersionUID = 1836697963736227954L; private boolean isSuccess; public boolean isSuccess() { return isSuccess; } public void setSuccess(boolean success) { isSuccess = success; } @Override public String toString() { return new StringJoiner(", ", Model3.class.getSimpleName() + "[", "]") .add("isSuccess=" + isSuccess) .toString(); } } 以上代码,输出结果: Model3[isSuccess=false] 这和我们预期的结果完全相反,原因是因为JSON框架通过扫描所有的getter后发现有一个isSuccess方法,然后根据JavaBeans的规范,解析出变量名为success,把model对象序列化城字符串后内容为{"success":true}。 根据{"success":true}这个json串,Gson框架在通过解析后,通过反射寻找Model类中的success属性,但是Model类中只有isSuccess属性,所以,最终反序列化后的Model类的对象中,isSuccess则会使用默认值false。 但是,一旦以上代码发生在生产环境,这绝对是一个致命的问题。 所以,作为开发者,我们应该想办法尽量避免这种问题的发生,对于POJO的设计者来说,只需要做简单的一件事就可以解决这个问题了,那就是把isSuccess改为success。这样,该类里面的成员变量时success,getter方法是isSuccess,这是完全符合JavaBeans规范的。无论哪种序列化框架,执行结果都一样。就从源头避免了这个问题。 引用以下R大关于阿里巴巴Java开发手册这条规定的评价(https://www.zhihu.com/question/55642203):  所以,在定义POJO中的布尔类型的变量时,不要使用isSuccess这种形式,而要直接使用success! Boolean还是boolean 前面我们介绍完了在success和isSuccess之间如何选择,那么排除错误答案后,备选项还剩下: boolean success Boolean success 那么,到底应该是用Boolean还是boolean来给定一个布尔类型的变量呢? 我们知道,boolean是基本数据类型,而Boolean是包装类型。关于基本数据类型和包装类之间的关系和区别请参考一文读懂什么是Java中的自动拆装箱 那么,在定义一个成员变量的时候到底是使用包装类型更好还是使用基本数据类型呢? 我们来看一段简单的代码 /** * @author Hollis */ public class BooleanMainTest { public static void main(String[] args) { Model model1 = new Model(); System.out.println("default model : " + model1); } } class Model { /** * 定一个Boolean类型的success成员变量 */ private Boolean success; /** * 定一个boolean类型的failure成员变量 */ private boolean failure; /** * 覆盖toString方法,使用Java 8 的StringJoiner */ @Override public String toString() { return new StringJoiner(", ", Model.class.getSimpleName() + "[", "]") .add("success=" + success) .add("failure=" + failure) .toString(); } } 以上代码输出结果为: default model : Model[success=null, failure=false] 可以看到,当我们没有设置Model对象的字段的值的时候,Boolean类型的变量会设置默认值为null,而boolean类型的变量会设置默认值为false。 即对象的默认值是null,boolean基本数据类型的默认值是false。 在阿里巴巴Java开发手册中,对于POJO中如何选择变量的类型也有着一些规定: 这里建议我们使用包装类型,原因是什么呢? 举一个扣费的例子,我们做一个扣费系统,扣费时需要从外部的定价系统中读取一个费率的值,我们预期该接口的返回值中会包含一个浮点型的费率字段。当我们取到这个值得时候就使用公式:金额*费率=费用 进行计算,计算结果进行划扣。 如果由于计费系统异常,他可能会返回个默认值,如果这个字段是Double类型的话,该默认值为null,如果该字段是double类型的话,该默认值为0.0。 如果扣费系统对于该费率返回值没做特殊处理的话,拿到null值进行计算会直接报错,阻断程序。拿到0.0可能就直接进行计算,得出接口为0后进行扣费了。这种异常情况就无法被感知。 这种使用包装类型定义变量的方式,通过异常来阻断程序,进而可以被识别到这种线上问题。如果使用基本数据类型的话,系统可能不会报错,进而认为无异常。 以上,就是建议在POJO和RPC的返回值中使用包装类型的原因。 但是关于这一点,作者之前也有过不同的看法:对于布尔类型的变量,我认为可以和其他类型区分开来,作者并不认为使用null进而导致NPE是一种最好的实践。因为布尔类型只有true/false两种值,我们完全可以和外部调用方约定好当返回值为false时的明确语义。 后来,作者单独和《阿里巴巴Java开发手册》、《码出高效》的作者——孤尽 单独1V1(qing) Battle(jiao)了一下。最终达成共识,还是尽量使用包装类型。 但是,作者还是想强调一个我的观点,尽量避免在你的代码中出现不确定的null值。 总结 本文围绕布尔类型的变量定义的类型和命名展开了介绍,最终我们可以得出结论,在定义一个布尔类型的变量,尤其是一个给外部提供的接口返回值时,要使用success来命名,阿里巴巴Java开发手册建议使用封装类来定义POJO和RPC返回值中的变量。但是这不意味着可以随意的使用null,我们还是要尽量避免出现对null的处理的。

montos 2020-06-01 21:26:05 0 浏览量 回答数 0

问题

【精品问答】Java必备核心知识1000+(附源码)

问问小秘 2019-12-01 22:00:28 870 浏览量 回答数 1

回答

我们先从整体上看一下Kubernetes的一些理念和基本架构,然后从网络、资源管理、存储、服务发现、负载均衡、高可用、rollingupgrade、安全、监控等方面向大家简单介绍Kubernetes的这些主要特性。  当然也会包括一些需要注意的问题。主要目的是帮助大家快速理解Kubernetes的主要功能,今后在研究和使用这个具的时候有所参考和帮助。  1.Kubernetes的一些理念:  用户不需要关心需要多少台机器,只需要关心软件(服务)运行所需的环境。以服务为中心,你需要关心的是api,如何把大服务拆分成小服务,如何使用api去整合它们。  保证系统总是按照用户指定的状态去运行。  不仅仅提给你供容器服务,同样提供一种软件系统升级的方式;在保持HA的前提下去升级系统是很多用户最想要的功能,也是最难实现的。  那些需要担心和不需要担心的事情。  更好的支持微服务理念,划分、细分服务之间的边界,比如lablel、pod等概念的引入。  对于Kubernetes的架构,可以参考官方文档。  大致由一些主要组件构成,包括Master节点上的kube-apiserver、kube-scheduler、kube-controller-manager、控制组件kubectl、状态存储etcd、Slave节点上的kubelet、kube-proxy,以及底层的网络支持(可以用Flannel、OpenVSwitch、Weave等)。  看上去也是微服务的架构设计,不过目前还不能很好支持单个服务的横向伸缩,但这个会在Kubernetes的未来版本中解决。  2.Kubernetes的主要特性  会从网络、服务发现、负载均衡、资源管理、高可用、存储、安全、监控等方面向大家简单介绍Kubernetes的这些主要特性->由于时间有限,只能简单一些了。  另外,对于服务发现、高可用和监控的一些更详细的介绍,感兴趣的朋友可以通过这篇文章了解。  1)网络  Kubernetes的网络方式主要解决以下几个问题:  a.紧耦合的容器之间通信,通过Pod和localhost访问解决。  b.Pod之间通信,建立通信子网,比如隧道、路由,Flannel、OpenvSwitch、Weave。  c.Pod和Service,以及外部系统和Service的通信,引入Service解决。  Kubernetes的网络会给每个Pod分配一个IP地址,不需要在Pod之间建立链接,也基本不需要去处理容器和主机之间的端口映射。  注意:Pod重建后,IP会被重新分配,所以内网通信不要依赖PodIP;通过Service环境变量或者DNS解决。  2)服务发现及负载均衡  kube-proxy和DNS,在v1之前,Service含有字段portalip和publicIPs,分别指定了服务的虚拟ip和服务的出口机ip,publicIPs可任意指定成集群中任意包含kube-proxy的节点,可多个。portalIp通过NAT的方式跳转到container的内网地址。在v1版本中,publicIPS被约定废除,标记为deprecatedPublicIPs,仅用作向后兼容,portalIp也改为ClusterIp,而在serviceport定义列表里,增加了nodePort项,即对应node上映射的服务端口。  DNS服务以addon的方式,需要安装skydns和kube2dns。kube2dns会通过读取KubernetesAPI获取服务的clusterIP和port信息,同时以watch的方式检查service的变动,及时收集变动信息,并将对于的ip信息提交给etcd存档,而skydns通过etcd内的DNS记录信息,开启53端口对外提供服务。大概的DNS的域名记录是servicename.namespace.tenx.domain,“tenx.domain”是提前设置的主域名。  注意:kube-proxy在集群规模较大以后,可能会有访问的性能问题,可以考虑用其他方式替换,比如HAProxy,直接导流到Service的endpints或者Pods上。Kubernetes官方也在修复这个问题。  3)资源管理  有3个层次的资源限制方式,分别在Container、Pod、Namespace层次。Container层次主要利用容器本身的支持,比如Docker对CPU、内存、磁盘、网络等的支持;Pod方面可以限制系统内创建Pod的资源范围,比如最大或者最小的CPU、memory需求;Namespace层次就是对用户级别的资源限额了,包括CPU、内存,还可以限定Pod、rc、service的数量。  资源管理模型-》简单、通用、准确,并可扩展  目前的资源分配计算也相对简单,没有什么资源抢占之类的强大功能,通过每个节点上的资源总量、以及已经使用的各种资源加权和,来计算某个Pod优先非配到哪些节点,还没有加入对节点实际可用资源的评估,需要自己的schedulerplugin来支持。其实kubelet已经可以拿到节点的资源,只要进行收集计算即可,相信Kubernetes的后续版本会有支持。  4)高可用  主要是指Master节点的HA方式官方推荐利用etcd实现master选举,从多个Master中得到一个kube-apiserver保证至少有一个master可用,实现highavailability。对外以loadbalancer的方式提供入口。这种方式可以用作ha,但仍未成熟,据了解,未来会更新升级ha的功能。  一张图帮助大家理解:  也就是在etcd集群背景下,存在多个kube-apiserver,并用pod-master保证仅是主master可用。同时kube-sheduller和kube-controller-manager也存在多个,而且伴随着kube-apiserver同一时间只能有一套运行。  5)rollingupgrade  RC在开始的设计就是让rollingupgrade变的更容易,通过一个一个替换Pod来更新service,实现服务中断时间的最小化。基本思路是创建一个复本为1的新的rc,并逐步减少老的rc的复本、增加新的rc的复本,在老的rc数量为0时将其删除。  通过kubectl提供,可以指定更新的镜像、替换pod的时间间隔,也可以rollback当前正在执行的upgrade操作。  同样,Kuberntes也支持多版本同时部署,并通过lable来进行区分,在service不变的情况下,调整支撑服务的Pod,测试、监控新Pod的工作情况。  6)存储  大家都知道容器本身一般不会对数据进行持久化处理,在Kubernetes中,容器异常退出,kubelet也只是简单的基于原有镜像重启一个新的容器。另外,如果我们在同一个Pod中运行多个容器,经常会需要在这些容器之间进行共享一些数据。Kuberenetes的Volume就是主要来解决上面两个基础问题的。  Docker也有Volume的概念,但是相对简单,而且目前的支持很有限,Kubernetes对Volume则有着清晰定义和广泛的支持。其中最核心的理念:Volume只是一个目录,并可以被在同一个Pod中的所有容器访问。而这个目录会是什么样,后端用什么介质和里面的内容则由使用的特定Volume类型决定。  创建一个带Volume的Pod:  spec.volumes指定这个Pod需要的volume信息spec.containers.volumeMounts指定哪些container需要用到这个VolumeKubernetes对Volume的支持非常广泛,有很多贡献者为其添加不同的存储支持,也反映出Kubernetes社区的活跃程度。  emptyDir随Pod删除,适用于临时存储、灾难恢复、共享运行时数据,支持RAM-backedfilesystemhostPath类似于Docker的本地Volume用于访问一些本地资源(比如本地Docker)。  gcePersistentDiskGCEdisk-只有在GoogleCloudEngine平台上可用。  awsElasticBlockStore类似于GCEdisk节点必须是AWSEC2的实例nfs-支持网络文件系统。  rbd-RadosBlockDevice-Ceph  secret用来通过KubernetesAPI向Pod传递敏感信息,使用tmpfs(aRAM-backedfilesystem)  persistentVolumeClaim-从抽象的PV中申请资源,而无需关心存储的提供方  glusterfs  iscsi  gitRepo  根据自己的需求选择合适的存储类型,反正支持的够多,总用一款适合的:)  7)安全  一些主要原则:  基础设施模块应该通过APIserver交换数据、修改系统状态,而且只有APIserver可以访问后端存储(etcd)。  把用户分为不同的角色:Developers/ProjectAdmins/Administrators。  允许Developers定义secrets对象,并在pod启动时关联到相关容器。  以secret为例,如果kubelet要去pull私有镜像,那么Kubernetes支持以下方式:  通过dockerlogin生成.dockercfg文件,进行全局授权。  通过在每个namespace上创建用户的secret对象,在创建Pod时指定imagePullSecrets属性(也可以统一设置在serviceAcouunt上),进行授权。  认证(Authentication)  APIserver支持证书、token、和基本信息三种认证方式。  授权(Authorization)  通过apiserver的安全端口,authorization会应用到所有http的请求上  AlwaysDeny、AlwaysAllow、ABAC三种模式,其他需求可以自己实现Authorizer接口。  8)监控  比较老的版本Kubernetes需要外接cadvisor主要功能是将node主机的containermetrics抓取出来。在较新的版本里,cadvior功能被集成到了kubelet组件中,kubelet在与docker交互的同时,对外提供监控服务。  Kubernetes集群范围内的监控主要由kubelet、heapster和storagebackend(如influxdb)构建。Heapster可以在集群范围获取metrics和事件数据。它可以以pod的方式运行在k8s平台里,也可以单独运行以standalone的方式。  注意:heapster目前未到1.0版本,对于小规模的集群监控比较方便。但对于较大规模的集群,heapster目前的cache方式会吃掉大量内存。因为要定时获取整个集群的容器信息,信息在内存的临时存储成为问题,再加上heaspter要支持api获取临时metrics,如果将heapster以pod方式运行,很容易出现OOM。所以目前建议关掉cache并以standalone的方式独立出k8s平台。 答案来源网络,供您参考

问问小秘 2019-12-02 02:13:31 0 浏览量 回答数 0

回答

我们先从整体上看一下Kubernetes的一些理念和基本架构,然后从网络、资源管理、存储、服务发现、负载均衡、高可用、rollingupgrade、安全、监控等方面向大家简单介绍Kubernetes的这些主要特性。  当然也会包括一些需要注意的问题。主要目的是帮助大家快速理解Kubernetes的主要功能,今后在研究和使用这个具的时候有所参考和帮助。  1.Kubernetes的一些理念:  用户不需要关心需要多少台机器,只需要关心软件(服务)运行所需的环境。以服务为中心,你需要关心的是api,如何把大服务拆分成小服务,如何使用api去整合它们。  保证系统总是按照用户指定的状态去运行。  不仅仅提给你供容器服务,同样提供一种软件系统升级的方式;在保持HA的前提下去升级系统是很多用户最想要的功能,也是最难实现的。  那些需要担心和不需要担心的事情。  更好的支持微服务理念,划分、细分服务之间的边界,比如lablel、pod等概念的引入。  对于Kubernetes的架构,可以参考官方文档。  大致由一些主要组件构成,包括Master节点上的kube-apiserver、kube-scheduler、kube-controller-manager、控制组件kubectl、状态存储etcd、Slave节点上的kubelet、kube-proxy,以及底层的网络支持(可以用Flannel、OpenVSwitch、Weave等)。  看上去也是微服务的架构设计,不过目前还不能很好支持单个服务的横向伸缩,但这个会在Kubernetes的未来版本中解决。  2.Kubernetes的主要特性  会从网络、服务发现、负载均衡、资源管理、高可用、存储、安全、监控等方面向大家简单介绍Kubernetes的这些主要特性->由于时间有限,只能简单一些了。  另外,对于服务发现、高可用和监控的一些更详细的介绍,感兴趣的朋友可以通过这篇文章了解。  1)网络  Kubernetes的网络方式主要解决以下几个问题:  a.紧耦合的容器之间通信,通过Pod和localhost访问解决。  b.Pod之间通信,建立通信子网,比如隧道、路由,Flannel、OpenvSwitch、Weave。  c.Pod和Service,以及外部系统和Service的通信,引入Service解决。  Kubernetes的网络会给每个Pod分配一个IP地址,不需要在Pod之间建立链接,也基本不需要去处理容器和主机之间的端口映射。  注意:Pod重建后,IP会被重新分配,所以内网通信不要依赖PodIP;通过Service环境变量或者DNS解决。  2)服务发现及负载均衡  kube-proxy和DNS,在v1之前,Service含有字段portalip和publicIPs,分别指定了服务的虚拟ip和服务的出口机ip,publicIPs可任意指定成集群中任意包含kube-proxy的节点,可多个。portalIp通过NAT的方式跳转到container的内网地址。在v1版本中,publicIPS被约定废除,标记为deprecatedPublicIPs,仅用作向后兼容,portalIp也改为ClusterIp,而在serviceport定义列表里,增加了nodePort项,即对应node上映射的服务端口。  DNS服务以addon的方式,需要安装skydns和kube2dns。kube2dns会通过读取KubernetesAPI获取服务的clusterIP和port信息,同时以watch的方式检查service的变动,及时收集变动信息,并将对于的ip信息提交给etcd存档,而skydns通过etcd内的DNS记录信息,开启53端口对外提供服务。大概的DNS的域名记录是servicename.namespace.tenx.domain,“tenx.domain”是提前设置的主域名。  注意:kube-proxy在集群规模较大以后,可能会有访问的性能问题,可以考虑用其他方式替换,比如HAProxy,直接导流到Service的endpints或者Pods上。Kubernetes官方也在修复这个问题。  3)资源管理  有3个层次的资源限制方式,分别在Container、Pod、Namespace层次。Container层次主要利用容器本身的支持,比如Docker对CPU、内存、磁盘、网络等的支持;Pod方面可以限制系统内创建Pod的资源范围,比如最大或者最小的CPU、memory需求;Namespace层次就是对用户级别的资源限额了,包括CPU、内存,还可以限定Pod、rc、service的数量。  资源管理模型-》简单、通用、准确,并可扩展  目前的资源分配计算也相对简单,没有什么资源抢占之类的强大功能,通过每个节点上的资源总量、以及已经使用的各种资源加权和,来计算某个Pod优先非配到哪些节点,还没有加入对节点实际可用资源的评估,需要自己的schedulerplugin来支持。其实kubelet已经可以拿到节点的资源,只要进行收集计算即可,相信Kubernetes的后续版本会有支持。  4)高可用  主要是指Master节点的HA方式官方推荐利用etcd实现master选举,从多个Master中得到一个kube-apiserver保证至少有一个master可用,实现highavailability。对外以loadbalancer的方式提供入口。这种方式可以用作ha,但仍未成熟,据了解,未来会更新升级ha的功能。  一张图帮助大家理解:  也就是在etcd集群背景下,存在多个kube-apiserver,并用pod-master保证仅是主master可用。同时kube-sheduller和kube-controller-manager也存在多个,而且伴随着kube-apiserver同一时间只能有一套运行。  5)rollingupgrade  RC在开始的设计就是让rollingupgrade变的更容易,通过一个一个替换Pod来更新service,实现服务中断时间的最小化。基本思路是创建一个复本为1的新的rc,并逐步减少老的rc的复本、增加新的rc的复本,在老的rc数量为0时将其删除。  通过kubectl提供,可以指定更新的镜像、替换pod的时间间隔,也可以rollback当前正在执行的upgrade操作。  同样,Kuberntes也支持多版本同时部署,并通过lable来进行区分,在service不变的情况下,调整支撑服务的Pod,测试、监控新Pod的工作情况。  6)存储  大家都知道容器本身一般不会对数据进行持久化处理,在Kubernetes中,容器异常退出,kubelet也只是简单的基于原有镜像重启一个新的容器。另外,如果我们在同一个Pod中运行多个容器,经常会需要在这些容器之间进行共享一些数据。Kuberenetes的Volume就是主要来解决上面两个基础问题的。  Docker也有Volume的概念,但是相对简单,而且目前的支持很有限,Kubernetes对Volume则有着清晰定义和广泛的支持。其中最核心的理念:Volume只是一个目录,并可以被在同一个Pod中的所有容器访问。而这个目录会是什么样,后端用什么介质和里面的内容则由使用的特定Volume类型决定。  创建一个带Volume的Pod:  spec.volumes指定这个Pod需要的volume信息spec.containers.volumeMounts指定哪些container需要用到这个VolumeKubernetes对Volume的支持非常广泛,有很多贡献者为其添加不同的存储支持,也反映出Kubernetes社区的活跃程度。  emptyDir随Pod删除,适用于临时存储、灾难恢复、共享运行时数据,支持RAM-backedfilesystemhostPath类似于Docker的本地Volume用于访问一些本地资源(比如本地Docker)。  gcePersistentDiskGCEdisk-只有在GoogleCloudEngine平台上可用。  awsElasticBlockStore类似于GCEdisk节点必须是AWSEC2的实例nfs-支持网络文件系统。  rbd-RadosBlockDevice-Ceph  secret用来通过KubernetesAPI向Pod传递敏感信息,使用tmpfs(aRAM-backedfilesystem)  persistentVolumeClaim-从抽象的PV中申请资源,而无需关心存储的提供方  glusterfs  iscsi  gitRepo  根据自己的需求选择合适的存储类型,反正支持的够多,总用一款适合的:)  7)安全  一些主要原则:  基础设施模块应该通过APIserver交换数据、修改系统状态,而且只有APIserver可以访问后端存储(etcd)。  把用户分为不同的角色:Developers/ProjectAdmins/Administrators。  允许Developers定义secrets对象,并在pod启动时关联到相关容器。  以secret为例,如果kubelet要去pull私有镜像,那么Kubernetes支持以下方式:  通过dockerlogin生成.dockercfg文件,进行全局授权。  通过在每个namespace上创建用户的secret对象,在创建Pod时指定imagePullSecrets属性(也可以统一设置在serviceAcouunt上),进行授权。  认证(Authentication)  APIserver支持证书、token、和基本信息三种认证方式。  授权(Authorization)  通过apiserver的安全端口,authorization会应用到所有http的请求上  AlwaysDeny、AlwaysAllow、ABAC三种模式,其他需求可以自己实现Authorizer接口。  8)监控  比较老的版本Kubernetes需要外接cadvisor主要功能是将node主机的containermetrics抓取出来。在较新的版本里,cadvior功能被集成到了kubelet组件中,kubelet在与docker交互的同时,对外提供监控服务。  Kubernetes集群范围内的监控主要由kubelet、heapster和storagebackend(如influxdb)构建。Heapster可以在集群范围获取metrics和事件数据。它可以以pod的方式运行在k8s平台里,也可以单独运行以standalone的方式。  注意:heapster目前未到1.0版本,对于小规模的集群监控比较方便。但对于较大规模的集群,heapster目前的cache方式会吃掉大量内存。因为要定时获取整个集群的容器信息,信息在内存的临时存储成为问题,再加上heaspter要支持api获取临时metrics,如果将heapster以pod方式运行,很容易出现OOM。所以目前建议关掉cache并以standalone的方式独立出k8s平台。 “答案来源于网络,供您参考” 希望以上信息可以帮到您!

牧明 2019-12-02 02:16:53 0 浏览量 回答数 0

回答

我们先从整体上看一下Kubernetes的一些理念和基本架构, 然后从网络、 资源管理、存储、服务发现、负载均衡、高可用、rolling upgrade、安全、监控等方面向大家简单介绍Kubernetes的这些主要特性。 当然也会包括一些需要注意的问题。主要目的是帮助大家快速理解 Kubernetes的主要功能,今后在研究和使用这个具的时候有所参考和帮助。 1.Kubernetes的一些理念: 用户不需要关心需要多少台机器,只需要关心软件(服务)运行所需的环境。以服务为中心,你需要关心的是api,如何把大服务拆分成小服务,如何使用api去整合它们。 保证系统总是按照用户指定的状态去运行。 不仅仅提给你供容器服务,同样提供一种软件系统升级的方式;在保持HA的前提下去升级系统是很多用户最想要的功能,也是最难实现的。 那些需要担心和不需要担心的事情。 更好的支持微服务理念,划分、细分服务之间的边界,比如lablel、pod等概念的引入。 对于Kubernetes的架构,可以参考官方文档。 大致由一些主要组件构成,包括Master节点上的kube-apiserver、kube-scheduler、kube-controller-manager、控制组件kubectl、状态存储etcd、Slave节点上的kubelet、kube-proxy,以及底层的网络支持(可以用Flannel、OpenVSwitch、Weave等)。 看上去也是微服务的架构设计,不过目前还不能很好支持单个服务的横向伸缩,但这个会在 Kubernetes 的未来版本中解决。 2.Kubernetes的主要特性 会从网络、服务发现、负载均衡、资源管理、高可用、存储、安全、监控等方面向大家简单介绍Kubernetes的这些主要特性 -> 由于时间有限,只能简单一些了。 另外,对于服务发现、高可用和监控的一些更详细的介绍,感兴趣的朋友可以通过这篇文章了解。 1)网络 Kubernetes的网络方式主要解决以下几个问题: a. 紧耦合的容器之间通信,通过 Pod 和 localhost 访问解决。 b. Pod之间通信,建立通信子网,比如隧道、路由,Flannel、Open vSwitch、Weave。 c. Pod和Service,以及外部系统和Service的通信,引入Service解决。 Kubernetes的网络会给每个Pod分配一个IP地址,不需要在Pod之间建立链接,也基本不需要去处理容器和主机之间的端口映射。 注意:Pod重建后,IP会被重新分配,所以内网通信不要依赖Pod IP;通过Service环境变量或者DNS解决。 2) 服务发现及负载均衡 kube-proxy和DNS, 在v1之前,Service含有字段portalip 和publicIPs, 分别指定了服务的虚拟ip和服务的出口机ip,publicIPs可任意指定成集群中任意包含kube-proxy的节点,可多个。portalIp 通过NAT的方式跳转到container的内网地址。在v1版本中,publicIPS被约定废除,标记为deprecatedPublicIPs,仅用作向后兼容,portalIp也改为ClusterIp, 而在service port 定义列表里,增加了nodePort项,即对应node上映射的服务端口。 DNS服务以addon的方式,需要安装skydns和kube2dns。kube2dns会通过读取Kubernetes API获取服务的clusterIP和port信息,同时以watch的方式检查service的变动,及时收集变动信息,并将对于的ip信息提交给etcd存档,而skydns通过etcd内的DNS记录信息,开启53端口对外提供服务。大概的DNS的域名记录是servicename.namespace.tenx.domain, "tenx.domain"是提前设置的主域名。 注意:kube-proxy 在集群规模较大以后,可能会有访问的性能问题,可以考虑用其他方式替换,比如HAProxy,直接导流到Service 的endpints 或者 Pods上。Kubernetes官方也在修复这个问题。 3)资源管理 有3 个层次的资源限制方式,分别在Container、Pod、Namespace 层次。Container层次主要利用容器本身的支持,比如Docker 对CPU、内存、磁盘、网络等的支持;Pod方面可以限制系统内创建Pod的资源范围,比如最大或者最小的CPU、memory需求;Namespace层次就是对用户级别的资源限额了,包括CPU、内存,还可以限定Pod、rc、service的数量。 资源管理模型 -》 简单、通用、准确,并可扩展 目前的资源分配计算也相对简单,没有什么资源抢占之类的强大功能,通过每个节点上的资源总量、以及已经使用的各种资源加权和,来计算某个Pod优先非配到哪些节点,还没有加入对节点实际可用资源的评估,需要自己的scheduler plugin来支持。其实kubelet已经可以拿到节点的资源,只要进行收集计算即可,相信Kubernetes的后续版本会有支持。 4)高可用 主要是指Master节点的 HA方式 官方推荐 利用etcd实现master 选举,从多个Master中得到一个kube-apiserver 保证至少有一个master可用,实现high availability。对外以loadbalancer的方式提供入口。这种方式可以用作ha,但仍未成熟,据了解,未来会更新升级ha的功能。 一张图帮助大家理解: 也就是在etcd集群背景下,存在多个kube-apiserver,并用pod-master保证仅是主master可用。同时kube-sheduller和kube-controller-manager也存在多个,而且伴随着kube-apiserver 同一时间只能有一套运行。 5) rolling upgrade RC 在开始的设计就是让rolling upgrade变的更容易,通过一个一个替换Pod来更新service,实现服务中断时间的最小化。基本思路是创建一个复本为1的新的rc,并逐步减少老的rc的复本、增加新的rc的复本,在老的rc数量为0时将其删除。 通过kubectl提供,可以指定更新的镜像、替换pod的时间间隔,也可以rollback 当前正在执行的upgrade操作。 同样, Kuberntes也支持多版本同时部署,并通过lable来进行区分,在service不变的情况下,调整支撑服务的Pod,测试、监控新Pod的工作情况。 6)存储 大家都知道容器本身一般不会对数据进行持久化处理,在Kubernetes中,容器异常退出,kubelet也只是简单的基于原有镜像重启一个新的容器。另外,如果我们在同一个Pod中运行多个容器,经常会需要在这些容器之间进行共享一些数据。Kuberenetes 的 Volume就是主要来解决上面两个基础问题的。 Docker 也有Volume的概念,但是相对简单,而且目前的支持很有限,Kubernetes对Volume则有着清晰定义和广泛的支持。其中最核心的理念:Volume只是一个目录,并可以被在同一个Pod中的所有容器访问。而这个目录会是什么样,后端用什么介质和里面的内容则由使用的特定Volume类型决定。 创建一个带Volume的Pod: spec.volumes 指定这个Pod需要的volume信息 spec.containers.volumeMounts 指定哪些container需要用到这个Volume Kubernetes对Volume的支持非常广泛,有很多贡献者为其添加不同的存储支持,也反映出Kubernetes社区的活跃程度。 emptyDir 随Pod删除,适用于临时存储、灾难恢复、共享运行时数据,支持 RAM-backed filesystemhostPath 类似于Docker的本地Volume 用于访问一些本地资源(比如本地Docker)。 gcePersistentDisk GCE disk - 只有在 Google Cloud Engine 平台上可用。 awsElasticBlockStore 类似于GCE disk 节点必须是 AWS EC2的实例 nfs - 支持网络文件系统。 rbd - Rados Block Device - Ceph secret 用来通过Kubernetes API 向Pod 传递敏感信息,使用 tmpfs (a RAM-backed filesystem) persistentVolumeClaim - 从抽象的PV中申请资源,而无需关心存储的提供方 glusterfs iscsi gitRepo 根据自己的需求选择合适的存储类型,反正支持的够多,总用一款适合的 :) 7)安全 一些主要原则: 基础设施模块应该通过API server交换数据、修改系统状态,而且只有API server可以访问后端存储(etcd)。 把用户分为不同的角色:Developers/Project Admins/Administrators。 允许Developers定义secrets 对象,并在pod启动时关联到相关容器。 以secret 为例,如果kubelet要去pull 私有镜像,那么Kubernetes支持以下方式: 通过docker login 生成 .dockercfg 文件,进行全局授权。 通过在每个namespace上创建用户的secret对象,在创建Pod时指定 imagePullSecrets 属性(也可以统一设置在serviceAcouunt 上),进行授权。 认证 (Authentication) API server 支持证书、token、和基本信息三种认证方式。 授权 (Authorization) 通过apiserver的安全端口,authorization会应用到所有http的请求上 AlwaysDeny、AlwaysAllow、ABAC三种模式,其他需求可以自己实现Authorizer接口。 8)监控 比较老的版本Kubernetes需要外接cadvisor主要功能是将node主机的container metrics抓取出来。在较新的版本里,cadvior功能被集成到了kubelet组件中,kubelet在与docker交互的同时,对外提供监控服务。 Kubernetes集群范围内的监控主要由kubelet、heapster和storage backend(如influxdb)构建。Heapster可以在集群范围获取metrics和事件数据。它可以以pod的方式运行在k8s平台里,也可以单独运行以standalone的方式。 注意: heapster目前未到1.0版本,对于小规模的集群监控比较方便。但对于较大规模的集群,heapster目前的cache方式会吃掉大量内存。因为要定时获取整个集群的容器信息,信息在内存的临时存储成为问题,再加上heaspter要支持api获取临时metrics,如果将heapster以pod方式运行,很容易出现OOM。所以目前建议关掉cache并以standalone的方式独立出k8s平台。 此答案来源于网络,希望对你有所帮助。

养狐狸的猫 2019-12-02 02:13:33 0 浏览量 回答数 0

回答

本文介绍了创建及配置集群的基本配置流程和查看配置清单的方法,并详细说明了各高级配置项的用法。 基本配置流程 开通并创建NAS 首次创建E-HPC集群之前,需要先登录文件存储产品页面 开通NAS服务,NAS服务开通后,登录到NAS控制台开始 创建NAS文件系统,并为文件系统 添加挂载点,操作完成之后,就可以登录到EHPC控制台创建集群了。 创建集群 1.. 登录E-HPC管理控制台。如果尚未注册,请先单击 免费注册 完成注册流程(按照最新国家规定,需要实名制注册)。登录后定位到 弹性高性能计算,会直接显示如下的集群界面: ClusterView 2.. 在该 集群 界面,先选择地域(如华东1),单击右上角开始 创建集群。 注意1:请先了解地域和可用区。 注意2: 在创建、管理或使用E-HPC集群时,非特殊情况请勿使用云服务器ECS管理控制台调整单个集群节点。建议通过E-HPC集群管控平台操作。详情见 为什么不能使用ECS管理控制台对E-HPC集群节点进行操作? 第一步:硬件配置 硬件配置项包括:可用区、付费类型、部署方式和节点配置,如下图所示:HardwareConfig 选择可用区。 ZoneSelect 说明:为了保证E-HPC节点间的网络通讯效率,所有开通的节点均位于同一地域同一可用区,请参见地域和可用区。如果在开通E-HPC集群时发现想用的区域不可选,请参见为什么某些地域无法开通E-HPC集群 选择付费类型 付费类型是指集群节点ECS实例的计费方式,其中不包括弹性IP、NAS存储的费用。共有三种付费类型供您选择:包年包月、按量付费和竞价实例。ChargeMode 选择部署方式 DeployMode 说明: 标准:登录节点、管控节点和计算节点分离部署,管控节点可以选2台或4台(HA)。 简易:登录、管控服务混合部署在一台节点上,计算节点分离。 One-box: 所有类型的服务都部署在一台计算节点上,整个集群只有一个节点,可选择使用本地存储或NAS存储。使用NAS存储时可支持集群扩容。 4. 节点配置 NodeSelect 如上图,系统中默认分配2个管控节点实例,还可以自己选择1个或者4(HA)。计算节点的数量指定为3台。登录节点默认分配1台。点击节点的打开下拉菜单可进一步选择所需机型。 说明: E-HPC集群主要由以下3种节点构成 计算节点:用于执行高性能计算作业的节点 管控节点:用于进行作业角度和域账户管理的节点,包括相互独立的2种节点: 作业调度节点:部署作业调度器 域帐户管理节点:部署集群的域账号管理软件 登录节点:具备公网IP,用户可远程登录该节点,通过命令行操作HPC集群 一般来说,作业调度节点只处理作业调度,域帐户管理节点只处理帐户信息,都不参与作业运算,因此原则上管控节点选用较低配置的企业级实例(如小于4CPU核的sn1ne实例)保证高可用性即可。计算节点的硬件配置选择是影响集群性能的关键点。登录节点通常会被配置为开发环境,需要为集群所有用户提供软件开发调试所需的资源及测试环境,因此推荐登录节点选择与计算节点配置一致或内存配比更大的实例。各种机型的详细信息可参考推荐配置。 完成硬件配置后,点击下一步进入软件配置界面。 第二步:软件配置 软件配置项包括:镜像类型、操作系统、调度器和软件包,如下图所示:SoftwareConfig 说明: 选择不同的镜像类型,操作系统的可选项也会变化。操作系统指部署在集群所有节点上的操作系统。“镜像类型”说明: 若用户选择镜像为"自定义镜像类型",则不能使用基于已有E-HPC集群节点创建产生的自定义镜像,否则,创建集群计算节点将会产生异常。 调度器是指HPC集群上部署的作业调度软件。选择不同的作业调度软件,向集群提交作业时作业脚本和参数也会有相应的不同。 软件包是指HPC集群上部署的HPC软件,HPC提供多种类型的典型配置软件包如GROMACS、OpenFOAM和LAMMPS等,包含相应的软件和运行依赖,集群创建完毕之后,所选的软件将会预装到集群上。 第三步:基础配置 基础配置项包括:基本信息和登录设置,如下图所示:BaseConfig 说明: 基本信息中的名称是指集群名称,该名称将会在集群列表中显示,便于用户查找。 登录设置填写的是登录该集群的密码,该密码用于远程SSH访问集群登录节点时使用,对应的用户名为root。 完成基本配置后,勾选《E-HPC服务条款》,点击确认即可创建集群。 查看配置清单 您可以在创建集群界面的右侧查看当前配置清单。默认情况下,配置清单仅显示基础配置,您可以勾选高级配置选项查看更多配置项。 ConfigList 查看配置拓扑图 在创建完集群之后,点击右上角查看详情,我们可以查看到集群的在拓扑图。TopoButton 可以看到当前配置拓扑图中,包括VPC名称、交换机名称、NAS实例名、登录节点、管控节点、计算节点的配置及数量等。ClusterTopo 查询创建状态 大约20分钟后,您可以回到E-HPC集群页面,查看新集群状态。若新集群所有节点皆处于 运行中 的状态,则集群已创建完毕。下一步用户可登录到集群进行操作,请参见指引使用集群。 高级配置 按照基本配置流程可创建通用E-HPC集群,如果用户需要更灵活的配置,可以在高级配置选项下进行选择。创建集群的三个步骤中前两个步骤都有高级配置可供用户选择。 硬件高级配置 依次打开创建集群 > 硬件配置 > 高级配置,可以看到如下配置选项(本例在创建集群前已事先创建了网络、存储等基础服务): HardwareAdvConfig 网络配置 上图中的网络配置部分,用户可自行在阿里云专有网络控制台创建VPC、交换机,在阿里云云服务器控制台创建安全组,创建完成后即可在这里可以选择所需的VPC、交换机、安全组等网络配置。如果不想跳转到其他服务的控制台,也可点击此处的“创建VPC”、“创建子网(交换机)”链接,在右侧的滑动窗口中创建相应的组件。 说明:如果用户事先没有创建VPC和交换机,创建集群的流程将会自动创建默认一个默认的VPC和交换机,VPC网段为192.168.0.0/16,交换机网段为192.168.0.0/20。用户如果自行创建了VPC,需要在所需的可用区下自行创建交换机才可继续创建集群。如果用户自行创建了VPC和交换机,使用基础配置流程创建集群时将会自动选择第一个VPC和交换机,请确保交换机下的IP地址空间足够(可用IP数大于集群所有节点的数量),用户也可以在高级配置下的VPC和交换机配置中自行选择任何已创建的VPC和交换机。 共享存储配置 上图中的共享存储部分,E-HPC所有用户数据、用户管理、作业共享数据等信息都会存储在共享存储上以供集群各节点访问。目前共享存储是由文件存储NAS提供。而要使用NAS还要配套挂载点和远程目录。 说明:如果用户事先没有在当前可用区创建NAS实例和挂载点,创建集群的流程将会在可用区下自动创建默认一个默认的NAS实例与挂载点。如果用户在当前可用区自行创建了NAS实例和挂载点,使用基础配置流程创建集群时将会自动选择第一个NAS实例和挂载点。如果在该NAS实例在可用区下没有可用的挂载点,创建集群的流程会自动创建一个挂载点。请确保该NAS实例还有可用的挂载点余量。 系统盘大小配置 用户可以根据自己实际需求,在这里指定创建集群计算节点的系统盘大小,默认值是40,范围在40-500(G)之间。 该值与集群扩容时系统盘大小的默认值保持一致,用户也可以在集群扩容时为新扩容的节点重新设置系统盘大小。 软件高级配置 依次打开创建集群 > 软件配置 > 高级配置,进行高级选项配置。 队列配置 用户可在此处为创建的集群指定队列,当不指定时集群会加入到默认的队列,如,PBS集群的默认队列为workq,slurm集群的默认队列为comp. queueconfig 安装后执行脚本 集群部署完毕后,用户可以在此处执行脚本。PostScript 说明: 下载地址是指脚本文件所在的地址,一般将脚本上传到OSS服务,这里填写OSS文件的url。 执行参数是指执行脚本时需要传入的命令行参数。 软件版本 用户可以在此处选择域账号服务软件类型和具体的软件清单:VersionConfig 注意:在选择预装高性能计算应用软件时,必须选择所依赖的软件包(如mpich或openmpi,参见软件包名后缀)。如选择”-gpu”后缀的软件,必须确保计算节点使用GPU系列机型。否则会有集群创建失败或软件无法正常运行的风险。

1934890530796658 2020-03-23 16:48:30 0 浏览量 回答数 0

问题

Nginx性能为什么如此吊

小柒2012 2019-12-01 21:20:47 15038 浏览量 回答数 3

问题

SSH面试题

琴瑟 2019-12-01 21:46:22 3489 浏览量 回答数 0

问题

SaaS模式云数据仓库MaxCompute 百问百答合集(持续更新20200921)

亢海鹏 2020-05-29 15:10:00 22066 浏览量 回答数 10

问题

【Java问答学堂】4期 如何保证消息的可靠性传输?(如何处理消息丢失的问题?)

剑曼红尘 2020-04-21 10:04:26 105 浏览量 回答数 2

回答

Layout Go工程项目的整体组织 首先我们看一下整个 Go 工程是怎么组织起来的。 很多同事都在用 GitLab 的,GitLab 的一个 group 里面可以创建很多 project。如果我们进行微服务化改造,以前很多巨石架构的应用可能就拆成了很多个独立的小应用。那么这么多小应用,你是要建 N 个 project 去维护,还是说按照部门或者组来组织这些项目呢?在 B 站的话,我们之前因为是 Monorepo,现在是按照部门去组织管理代码,就是说在单个 GitLab 的 project 里面是有多个 app 的,每一个 app 就表示一个独立的微服务,它可以独立去交付部署。所以说我们看到下面这张图里面,app 的目录里面是有好多个子目录的,比方说我们的评论服务,会员服务。跟 app 同级的目录有一个叫 pkg,可以存放业务有关的公共库。这是我们的一个组织方式。当然,还有一种方式,你可以按照 GitLab 的 project 去组织,但我觉得这样的话可能相对要创建的 project 会非常多。 如果你按部门组织的话,部门里面有很多 app,app 目录怎么去组织?我们实际上会给每一个 app 取一个全局唯一名称,可以理解为有点像 DNS 那个名称。我们对业务的命名也是一样的,我们基本上是三段式的命名,比如账号业务,它是一个账号业务、服务、子服务的三段命名。三段命名以后,在这个 app 目录里面,你也可以按照这三层来组织。比如我们刚刚说的账号目录,我可能就是 account 目录,然后 VIP,在 VIP 目录下可能会放各种各样的不同角色的微服务,比方说可能有一些是做 job,做定时任务或者流式处理的一些任务,有可能是做对外暴露的 API 的一些服务,这个就是我们关于整个大的 app 的组织的一种形式。 微服务中的 app 服务分类 微服务中单个 app 的服务里又分为几类不同的角色。我们基本上会把 app 分为 interface(BFF)、service、job(补充:还有一个 task,偏向定时执行,job 偏向流式) 和 admin。 Interface 是对外的业务网关服务,因为我们最终是面向终端用户的 API,面向 app,面向 PC 场景的,我们把这个叫成业务网关。因为我们不是统一的网关,我们可能是按照大的业务线去独立分拆的一些子网关,这个的话可以作为一个对外暴露的 HTTP 接口的一个目录去组织它的代码,当然也可能是 gRPC 的(参考 B 站对外的 gRPC Moss 分享)。 Service 这个角色主要是面向对内通信的微服务,它不直接对外。也就是说,业务网关的请求会转发或者是会 call 我们的内部的 service,它们之间的通讯可能是使用自己的 RPC,在 b 站我们主要是使用 gRPC。使用 gRPC 通讯以后,service 它因为不直接对外,service 之间可能也可以相互去 call。 Admin 区别于 service,很多应用除了有面向用户的一些接口,实际上还有面向企业内部的一些运营侧的需求,通常数据权限更高,从安全设计角度需要代码物理层面隔离,避免意外。 第四个是 ecode。我们当时也在内部争论了很久,我们的错误码定义到底是放在哪里?我们目前的做法是,一个应用里面,假设你有多种角色,它们可能会复用一些错误码。所以说我们会把我们的 ecode 给单独抽出来,在这一个应用里面是可以复用的。注意,它只在这一个应用里面复用,它不会去跨服跨目录应用,它是针对业务场景的一个业务错误码的组织。 App 目录组织 我们除了一个应用里面多种角色的这种情况,现在展开讲一下具体到一个 service 里面,它到底是怎么组织的。我们的 app 目录下大概会有 api、cmd、configs、 internal 目录,目录里一般还会放置 README、CHANGELOG、OWNERS。 API 是放置 api 定义以及对应的生成的 client 代码,包含基于 pb 定义(我们使用 PB 作为 DSL 描述 API) 生成的 swagger.json。 而 cmd,就是放 main 函数的。Configs 目录主要是放一些服务所需的配置文件,比方说说我们可能会使用 TOML 或者是使用 YAML 文件。 Internal 的话,它里面有四个子目录,分别是 model、dao、service 和 server。Model 的定位职责就是对我们底层存储的持久化层或者存储层的数据的映射,它是具体的 Go 的一个 struct。我们再看 dao,你实际就是要操作 MySQL 或者 Redis,最终返回的就是这些 model(存储映射)。Service 组织起来比较简单,就是我们通过 dao 里面的各个方法来完成一个完整的业务逻辑。我们还看到有个 server,因为我一个微服务有可能企业内部不一定所有 RPC 都统一,那我们处于过渡阶段,所以 server 里面会有两个小目录,一个是 HTTP 目录,暴露的是 HTTP 接口,还有一个是 gRPC 目录,我们会暴露 gRPC 的协议。所以在 server 里面,两个不同的启动的 server,就是说一个服务和启动两个端口,然后去暴露不同的协议,HTTP 接 RPC,它实际上会先 call 到 service,service 再 call 到 dao,dao 实际上会使用 model 的一些数据定义 struct。但这里面有一个非常重要的就是,因为这个结构体不能够直接返回给我们的 api 做外对外暴露来使用,为什么?因为可能从数据库里面取的敏感字段,当我们实际要返回到 api 的时候,可能要隐藏掉一些字段,在 Java 里面,会抽象的一个叫 DTO 的对象,它只是用来传输用的,同理,在我们 Go 里面,实际也会把这些 model 的一些结构体映射成 api 里面的结构体(基于 PB Message 生成代码后的 struct)。 Rob Pike 当时说过的一句话,a little copying is better than a little dependency,我们就遵循了这个理念。在我们这个目录结构里面,有 internal 目录,我们知道 Go 的目录只允许这个目录里面的人去 import 到它,跨目录的人实际是不能直接引用到它的。所以说,我们看到 service 有一个 model,那我的 job 代码,我做一些定时任务的代码或者是我的网关代码有可能会映射同一个 model,那是不是要把这个 model 放到上一级目录让大家共享?对于这个问题,其实我们当时内部也争论过很久。我们认为,每一个微服务应该只对自己的 model 负责,所以我们宁愿去做一小部分的代码 copy,也不会去为了几个服务之间要共享这一点点代码,去把这个 model 提到和 app 目录级别去共用,因为你一改全错,当然了,你如果是拷贝的话,就是每个地方都要去改,那我们觉得,依赖的问题可能会比拷贝代码相对来说还是要更复杂的。 这个是一个标准的 PB 文件,就是我们内部的一个 demo 的 service。最上面的 package 是 PB 的包名,demo.service.v1,这个包使用的是三段式命名,全局唯一的名称。那这个名称为什么不是用 ID?我见过有些公司对内部做的 CMDB 或者做服务树去管理企业内部微服务的时候,是用了一些名称加上 ID 来搞定唯一性,但是我们知道后面那一串 ID 数字是不容易被传播或者是不容易被记住的,这也是 DNS 出来的一个意义,所以我们用绝对唯一的一个名称来表示这个包的名字,在后面带上这一个 PB 文件的版本号 V1。 我们看第二段定义,它有个 Service Demo 代码,其实就表示了我们这个服务要启动的服务的一个名称,我们看到这个服务名称里面有很多个 RPC 的方法,表示最终这一个应用或者这个 service 要对外暴露这几个 RPC 的方法。这里面有个小细节,我们看一下 SayHello 这个方法,实际它有 option 的一个选项。通过这一个 PB 文件,你既可以描述出你要暴露的是 gRPC 协议,又暴露出 HTTP 的一个接口,这个好处是你只需要一个 PB 文件描述你暴露的所有 api。我们回想一下,我们刚刚目录里面有个 api 目录,实际这里面就是放这一个 PB 文件,描述这一个工程到底返回的接口是什么。不管是 gRPC 还是 HTTP 都是这一个文件。还有一个好处是什么?实际上我们可以在 PB 文件里面加上很多的注释。用 PB 文件的好处是你不需要额外地再去写文档,因为写文档和写服务的定义,它本质上是两个步骤,特别容易不一致,接口改了,文档不同步。我们如果基于这一个 PB 文件,它生成的 service 代码或者调用代码或者是文档都是唯一的。 依赖顺序与 api 维护 就像我刚刚讲到的,model 是一个存储层的结构体的一一映射,dao 处理一些数据读写包,比方说数据库缓存,server 的话就是启动了一些 gRPC 或者 HTTP Server,所以它整个依赖顺序如下:main 函数启动 server,server 会依赖 api 定义好的 PB 文件,定义好这些方法或者是服务名之后,实际上生成代码的时候,比方说 protocbuf 生成代码的时候,它会把抽象 interface 生成好。然后我们看一下 service,它实际上是弱依赖的 api,就是说我的 server 启动以后,要注册一个具体的业务代码的逻辑,映射方法,映射名字,实际上是弱依赖的 api 生成的 interface 的代码,你就可以很方便地启动你的 server,把你具体的 service 的业务逻辑给注入到这个 server,和方法进行一一绑定。最后,dao 和 service 实际上都会依赖这个 model。 因为我们在 PB 里面定义了一些 message,这些 message 生成的 Go 的 struct 和刚刚 model 的 struct 是两个不同的对象,所以说你要去手动 copy 它,把它最终返回。但是为了快捷,你不可能每次手动去写这些代码,因为它要做 mapping,所以我们又把 K8s 里类似 DeepCopy 的两个结构体相互拷贝的工具给抠出来了,方便我们内部 model 和 api 的 message 两个代码相互拷贝的时候,可以少写一些代码,减少一些工作量。 上面讲的就是我们关于工程的一些 layout 实践。简单回溯一下,大概分为几块,第一就是 app 是怎么组织的,app 里面有多种角色的服务是怎么组织的,第三就是一个 app 里面的目录是怎么组织的,最后我重点讲了一下 api 是怎么维护的。 Unittest 测试方法论 现在回顾一下单元测试。我们先看这张图,这张图是我从《Google 软件测试之道》这本书里面抠出来的,它想表达的意思就是最小型的测试不能给我们的最终项目的质量带来最大的信心,它比较容易带来一些优秀的代码质量,良好的异常处理等等。但是对于一个面向用户场景的服务,你只有做大型测试,比方做接口测试,在 App 上验收功能的这种测试,你应用交付的信心可能会更足。这个其实要表达的就是一个“721 原则”。我们就是 70% 写小型测试,可以理解为单元测试,因为它相对来说好写,针对方法级别。20% 是做一些中型测试,可能你要连调几个项目去完成你的 api。剩下 10% 是大型测试,因为它是最终面向用户场景的,你要去使用我们的 App,或者用一些测试 App 去测试它。这个就是测试的一些简单的方法论。 单元测试原则 我们怎么去对待 Go 里面的单元测试?在《Google 软件测试之道》这本书里面,它强调的是对于一个小型测试,一个单元测试,它要有几个特质。它不能依赖外部的一些环境,比如我们公司有测试环境,有持续集成环境,有功能测试环境,你不能依赖这些环境构建自己的单元测试,因为测试环境容易被破坏,它容易有数据的变更,数据容易不一致,你之前构建的案例重跑的话可能就会失败。 我觉得单元测试主要有四点要求。第一,快速,你不能说你跑个单元测试要几分钟。第二,要环境一致,也就是说你跑测试前和跑测试后,它的环境是一致的。第三,你写的所有单元测试的方法可以以任意顺序执行,不应该有先后的依赖,如果有依赖,也是在你测试的这个方法里面,自己去 setup 和 teardown,不应该有 Test Stub 函数存在顺序依赖。第四,基于第三点,你可以做并行的单元测试,假设我写了一百个单元测试,一个个跑肯定特别慢。 doker-compose 最近一段时间,我们演进到基于 docker-compose 实现跨平台跨语言环境的容器依赖管理方案,以解决运行 unittest 场景下的容器依赖问题。 首先,你要跑单元测试,你不应该用 VPN 连到公司的环境,好比我在星巴克点杯咖啡也可以写单元测试,也可以跑成功。基于这一点,Docker 实际上是非常好的解决方式。我们也有同学说,其他语言有一些 in-process 的 mock,是不是可以启动 MySQL 的 mock ,然后在 in-process 上跑?可以,但是有一个问题,你每一个语言都要写一个这样的 mock ,而且要写非常多种,因为我们中间件越来越多,MySQL,HBase,Kafka,什么都有,你很难覆盖所有的组件 Mock。这种 mock 或者 in-process 的实现不能完整地代表线上的情况,比方说,你可能 mock 了一个 MySQL,检测到 query 或者 insert ,没问题,但是你实际要跑一个 transaction,要验证一些功能就未必能做得非常完善了。所以基于这个原因,我们当时选择了 docker-compose,可以很好地解决这个问题。 我们对开发人员的要求就是,你本地需要装 Docker,我们开发人员大部分都是用 Mac,相对来说也比较简单,Windows 也能搞定,如果是 Linux 的话就更简单了。本地安装 Docker,本质上的理解就是无侵入式的环境初始化,因为你在容器里面,你拉起一个 MySQL,你自己来初始化数据。在这个容器被销毁以后,它的环境实际上就满足了我们刚刚提的环境一致的问题,因为它相当于被重置了,也可以很方便地快速重置环境,也可以随时随地运行,你不需要依赖任何外部服务,这个外部服务指的是像 MySQL 这种外部服务。当然,如果你的单元测试依赖另外一个 RPC 的 service 的话,PB 的定义会生成一个 interface,你可以把那个 interface 代码给 mock 掉,所以这个也是能做掉的。对于小型测试来说,你不依赖任何外部环境,你也能够快速完成。 另外,docker-compose 是声明式的 API,你可以声明你要用 MySQL,Redis,这个其实就是一个配置文件,非常简单。这个就是我们在单元测试上的一些实践。 我们现在看一下,service 目录里面多了一个 test 目录,我们会在这个里面放 docker-compose 的 YAML 文件来表示这次单元化测试需要初始化哪些资源,你要构建自己的一些测试的数据集。因为是这样的,你是写 dao 层的单元测试的话,可能就需要 database.sql 做一些数据的初始化,如果你是做 service 的单元测试的话,实际你可以把整个 dao 给 mock 掉,我觉得反而还相对简单,所以我们主要针对场景就是在 dao 里面偏持久层的,利用 docker-compose 来解决。 容器的拉起,容器的销毁,这些工作到底谁来做?是开发同学自己去拉起和销毁,还是说你能够把它做成一个 Library,让我们的同学写单元测试的时候比较方便?我倾向的是后者。所以在我们最终写单元测试的时候,你可以很方便地 setup 一个依赖文件,去 setup 你的容器的一些信息,或者把它销毁掉。所以说,你把环境准备好以后,最终可以跑测试代码也非常方便。当然我们也提供了一些命令函,就是 binary 的一些工具,它可以针对各个语言方便地拉起容器和销毁容器,然后再去执行代码,所以我们也提供了一些快捷的方式。 刚刚我也提到了,就是我们对于 service 也好,API 也好,因为依赖下层的 dao 或者依赖下层的 service,你都很方便 mock 掉,这个写单元测试相对简单,这个我不展开讲,你可以使用 GoMock 或者 GoMonkey 实现这个功能。 Toolchain 我们利用多个 docker-compose 来解决 dao 层的单元测试,那对于我刚刚提到的项目的一些规范,单元测试的一些模板,甚至是我写了一些 dao 的一些占位符,或者写了一些 service 代码的一些占位符,你有没有考虑过这种约束有没有人会去遵循?所以我这里要强调一点,工具一定要大于约束和文档,你写了约束,写了文档,那么你最终要通过工具把它落实。所以在我们内部会有一个类似 go tool 的脚手架,叫 Kratos Tool,把我们刚刚说的约定规范都通过这个工具一键初始化。 对于我们内部的工具集,我们大概会分为几块。第一块就是 API 的,就是你写一个 PB 文件,你可以基于这个 PB 文件生成 gRPC,HTTP 的框架代码,你也可以基于这个 PB 文件生成 swagger 的一些 JSON 文件或者是 Markdown 文件。当然了,我们还会生成一些 API,用于 debug 的 client 方便去调试,因为我们知道,gRPC 调试起来相对麻烦一些,你要去写代码。 还有一些工具是针对 project 的,一键生成整个应用的 layout,非常方便。我们还提了 model,就是方便 model 和 DTO,DTO 就是 API 里面定义的 message 的 struct 做 DeepCopy,这个也是一个工具。 对于 cache 的话,我们操作 memcache,操作 Redis 经常会要做什么逻辑?假如我们有一个 cache aside 场景,你读了一个 cache,cache miss 要回原 DB,你要把这个缓存回塞回去,甚至你可能这个回塞缓存想异步化,甚至是你要去读这个 DB 的时候要做归并回源(singleflight),我们把这些东西做成一些工具,让它整个回源到 DB 的逻辑更加简单,就是把这些场景描述出来,然后你通过工具可以一键生成这些代码,所以也是会比较方便。 我们再看最后一个,就是 test 的一些工具。我们会基于项目里面,比方说 dao 或者是 service 定义的 interface 去帮你写好 mock 的代码,我直接在里面填,只要填代码逻辑就行了,所以也会加速我们的生产。 上图是 Kratos 的一个 demo,基本就是支持了一些 command。这里就是一个 kratos new kratos-demo 的一个工程,-d YourPath 把它导到某一个路径去,--proto 顺便把 API 里面的 proto 代码也生成了,所以非常简单,一行就可以很快速启动一个 HTTP 或者 gRPC 服务。 我们知道,一个微服务的框架实际非常重,有很多初始化的方式等等,非常麻烦。所以说,你通过脚手架的方式就会非常方便,工具大于约定和文档这个这个理念就是这么来的。 Configuration 讲完工具以后,最后讲一下配置文件。我为什么单独提一下配置文件?实际它也是工程化的一部分。我们一个线上的业务服务包含三大块,第一,应用程序,第二,配置文件,第三,数据集。配置文件最容易导致线上出 bug,因为你改一行配置,整个行为可能跟 App 想要的行为完全不一样。而且我们的代码的开发交付需要经过哪些流程?需要 commit 代码,需要 review,需要单元测试,需要 CD,需要交付到线上,需要灰度,它的整个流程是非常长的。在一步步的环境里面,你的 bug 需要前置解决,越前置解决,成本越低。因为你的代码的开发流程是这么一个 pipeline,所以 bug 最终流到线上的概率很低,但是配置文件没有经过这么复杂的流程,可能大家发现线上有个问题,决定要改个线上配置,就去配置中心或者配置文件改,然后 push 上线,接着就问题了,这个其实很常见。 从 SRE 的角度来说,导致线上故障的主因就是来自配置变更,所以 SRE 很大的工作是控制变更管理,如果能把变更管理做好,实际上很多问题都不会出现。配置既然在整个应用里面这么重要,那在我们整个框架或者在 Go 的工程化实践里面,我们应该对配置文件做一些什么事情? 我觉得是几个。第一,我们的目标是什么?配置文件不应该太复杂,我见过很多框架,或者是业务的一些框架,它实际功能非常强大,但是它的配置文件超级多。我就发现有个习惯,只要有一个同事写错了这个配置,当我新起一个项目的时候,一定会有人把这个错误的配置拷贝到另外一个系统里面去。然后当发现这个应用出问题的时候,我们一般都会内部说一下,你看看其他同事有没有也配错的,实际这个配错概率非常高。因为你的配置选项越多,复杂性越高,它越容易出错。所以第一个要素就是说,尽量避免复杂的配置文件。配得越多,越容易出错。 第二,实际我们的配置方式也非常多,有些用 JSON,有些用 YAML,有些用 Properties,有些用 INI。那能不能收敛成通用的一种方式呢?无论它是用 Python 的脚本也好,或者是用 JSON 也好,你只要有一种唯一的约定,不需要太多样的配置方式,对我们的运维,对我们的 SRE 同时来说,他跨项目的变更成本会变低。 第三,一定要往简单化去努力。这句话其实包含了几个方面的含义。首先,我们很多配置它到底是必须的还是可选的,如果是可选,配置文件是不是就可以把它踢掉,甚至不要出现?我曾经有一次看到我们 Java 同事的配置 retry 有一个重试默认是零,内部重试是 80 次,直接把 Redis cluster 打故障了,为什么?其实这种事故很低级,所以简单化努力的另外一层含义是指,我们在框架层面,尤其是提供 SDK 或者是提供 framework 的这些同事尽量要做一些防御编程,让这种错配漏配也处于一个可控的范围,比方重试 80 次,你觉得哪个 SDK 会这么做?所以这个是我们要考虑的。但是还有一点要强调的是,我们对于业务开发的同事,我们的配置应该足够的简单,这个简单还包含,如果你的日志基本上都是写在这个目录,你就不要提供这个配置给他,反而不容易出错。但是对于我们内部的一些 infrastructure,它可能需要非常复杂的配置来优化,根据我的场景去做优化,所以它是两种场景,一种是业务场景,足够简单,一种是我要针对我的通用的 infrastructure 去做场景的优化,需要很复杂的配置,所以它是两种场景,所以我们要想清楚你的业务到底是哪一种形态。 还有一个问题就是我们配置文件一定要做好权限的变更和跟踪,因为我们知道上线出问题的时候,我们的第一想法不是查 bug,是先止损,止损先找最近有没有变更。如果发现有变更,一般是先回滚,回滚的时候,我们通常只回滚了应用程序,而忘记回滚了配置。每个公司可能内部的配置中心,或者是配置场景,或者跟我们的二进制的交付上线都不一样,那么这里的理念就是你的应用程序和配置文件一定是同一个版本,或者是某种意义上让他们产生一个版本的映射,比方说你的应用程序 1.0,你的配置文件 2.0,它们之间存在一个强绑定关系,我们在回滚的时候应该是一起回滚的。我们曾经也因为类似的一些不兼容的配置的变更,二进制程序上线,但配置文件忘记回滚,出现过事故,所以这个是要强调的。 另外,配置的变更也要经过 review,如果没问题,应该也是按照 App 发布一样,先灰度,再放量,再全量等等类似的一种方式去推,演进式的这种发布,我们也叫滚动发布,我觉得配置文件也是一样的思路。 加入阿里云钉钉群享福利:每周技术直播,定期群内有奖活动、大咖问答 原文链接

有只黑白猫 2020-01-09 17:29:54 0 浏览量 回答数 0

问题

【精品问答】110+数据挖掘面试题集合

珍宝珠 2019-12-01 21:56:45 2713 浏览量 回答数 3

问题

【精品问答】python技术1000问(2)

问问小秘 2019-12-01 22:03:02 3129 浏览量 回答数 1

回答

Go 的优势在于能够将简单的和经过验证的想法结合起来,同时避免了其他语言中出现的许多问题。本文概述了 Go 背后的一些设计原则和工程智慧,作者认为,Go 语言具备的所有这些优点,将共同推动其成为接替 Java 并主导下一代大型软件开发平台的最有力的编程语言候选。很多优秀的编程语言只是在个别领域比较强大,如果将所有因素都纳入考虑,没有其他语言能够像 Go 语言一样“全面开花”,在大型软件工程方面,尤为如此。 基于现实经验 Go 是由经验丰富的软件行业老手一手创建的,长期以来,他们对现有语言的各种缺点有过切身体会的痛苦经历。几十年前,Rob Pike 和 Ken Thompson 在 Unix、C 和 Unicode 的发明中起到了重要作用。Robert Griensemer 在为 JavaScript 和 Java 开发 V8 和 HotSpot 虚拟机之后,在编译器和垃圾收集方面拥有数十年的经验。有太多次,他们不得不等待 Google 规模的 C++/Java 代码库进行编译。于是,他们开始着手创建新的编程语言,将他们半个世纪以来的编写代码所学到的一切经验包含进去。 专注于大型工程 小型工程项目几乎可以用任何编程语言来成功构建。当成千上万的开发人员在数十年的持续时间压力下,在包含数千万行代码的大型代码库上进行协作时,就会发生真正令人痛苦的问题。这样会导致一些问题,如下: 较长的编译时间导致中断开发。代码库由几个人 / 团队 / 部门 / 公司所拥有,混合了不同的编程风格。公司雇佣了数千名工程师、架构师、测试人员、运营专家、审计员、实习生等,他们需要了解代码库,但也具备广泛的编码经验。依赖于许多外部库或运行时,其中一些不再以原始形式存在。在代码库的生命周期中,每行代码平均被重写 10 次,被弄得千疮百痍,而且还会发生技术偏差。文档不完整。 Go 注重减轻这些大型工程的难题,有时会以使小型工程变得更麻烦为代价,例如,代码中到处都需要几行额外的代码行。 注重可维护性 Go 强调尽可能多地将工作转给自动化的代码维护工具中。Go 工具链提供了最常用的功能,如格式化代码和导入、查找符号的定义和用法、简单的重构以及代码异味的识别。由于标准化的代码格式和单一的惯用方式,机器生成的代码更改看起来非常接近 Go 中人为生成的更改并使用类似的模式,从而允许人机之间更加无缝地协作。 保持简单明了 初级程序员为简单的问题创建简单的解决方案。高级程序员为复杂的问题创建复杂的解决方案。伟大的程序员找到复杂问题的简单解决方案。 ——Charles Connell 让很多人惊讶的一点是,Go 居然不包含他们喜欢的其他语言的概念。Go 确实是一种非常小巧而简单的语言,只包含正交和经过验证的概念的最小选择。这鼓励开发人员用最少的认知开销来编写尽可能简单的代码,以便许多其他人可以理解并使用它。 使事情清晰明了 良好的代码总是显而易见的,避免了那些小聪明、难以理解的语言特性、诡异的控制流和兜圈子。 许多语言都致力提高编写代码的效率。然而,在其生命周期中,人们阅读代码的时间却远远超过最初编写代码所需的时间(100 倍)。例如,审查、理解、调试、更改、重构或重用代码。在查看代码时,往往只能看到并理解其中的一小部分,通常不会有完整的代码库概述。为了解释这一点,Go 将所有内容都明确出来。 错误处理就是一个例子。让异常在各个点中断代码并在调用链上冒泡会更容易。Go 需要手动处理和返回每个错误。这使得它可以准确地显示代码可以被中断的位置以及如何处理或包装错误。总的来说,这使得错误处理编写起来更加繁琐,但是也更容易理解。 简单易学 Go 是如此的小巧而简单,以至于人们可以在短短几天内就能研究通整个语言及其基本概念。根据我们的经验,培训用不了一个星期(相比于掌握其他语言需要几个月),初学者就能够理解 Go 专家编写的代码,并为之做出贡献。为了方便吸引更多的用户,Go 网站提供了所有必要的教程和深入研究的文章。这些教程在浏览器中运行,允许人们在将 Go 安装到本地计算机上之前就能够学习和使用 Go。 解决之道 Go 强调的是团队之间的合作,而不是个人的自我表达。 在 Go(和 Python)中,所有的语言特性都是相互正交和互补的,通常有一种方法可以做一些事情。如果你想让 10 个 Python 或 Go 程序员来解决同一个问题,你将会得到 10 个相对类似的解决方案。不同的程序员在彼此的代码库中感觉更自在。在查看其他人的代码时,国骂会更少,而且人们的工作可以更好地融合在一起,从而形成了一致的整体,人人都为之感到自豪,并乐于工作。这还避免了大型工程的问题,如: 开发人员认为良好的工作代码很“混乱”,并要求在开始工作之前进行重写,因为他们的思维方式与原作者不同。 不同的团队成员使用不同的语言子集来编写相同代码库的部分内容。 ![image.png](https://ucc.alicdn.com/pic/developer-ecology/e64418f1455d46aaacfdd03fa949f16d.png) 简单、内置的并发性 Go 专为现代多核硬件设计。 目前使用的大多数编程语言(Java、JavaScript、Python、Ruby、C、C++)都是 20 世纪 80 年代到 21 世纪初设计的,当时大多数 CPU 只有一个计算内核。这就是为什么它们本质上是单线程的,并将并行化视为边缘情况的马后炮。通过现成和同步点之类的附加组件来实现,而这些附加组件既麻烦又难以正确使用。第三方库虽然提供了更简单的并发形式,如 Actor 模型,但是总有多个可用选项,结果导致了语言生态系统的碎片化。今天的硬件拥有越来越多的计算内核,软件必须并行化才能高效运行。Go 是在多核处理器时代编写的,并且在语言中内置了简单、高级的 CSP 风格并发性。 面向计算的语言原语 就深层而言,计算机系统接收数据,对其进行处理(通常要经过几个步骤),然后输出结果数据。例如,Web 服务器从客户端接收 HTTP 请求,并将其转换为一系列数据库或后端调用。一旦这些调用返回,它就将接收到的数据转换成 HTML 或 JSON 并将其输出给调用者。Go 的内置语言原语直接支持这种范例: 结构表示数据 读和写代表流式 IO 函数过程数据 goroutines 提供(几乎无限的)并发性 在并行处理步骤之间传输管道数据 因为所有的计算原语都是由语言以直接形式提供的,因此 Go 源代码更直接地表达了服务器执行的操作。 OO — 好的部分 更改基类中的某些内容的副作用 面向对象非常有用。过去几十年来,面向对象的使用富有成效,并让我们了解了它的哪些部分比其他部分能够更好地扩展。Go 在面向对象方面采用了一种全新的方法,并记住了这些知识。它保留了好的部分,如封装、消息传递等。Go 还避免了继承,因为它现在被认为是有害的,并为组合提供了一流的支持。 现代标准库 目前使用的许多编程语言(Java、JavaScript、Python、Ruby)都是在互联网成为当今无处不在的计算平台之前设计的。因此,这些语言的标准库只提供了相对通用的网络支持,而这些网络并没有针对现代互联网进行优化。Go 是十年前创建的,当时互联网已全面发展。Go 的标准库允许在没有第三方库的情况下创建更复杂的网络服务。这就避免了第三方库的常见问题: 碎片化:总是有多个选项实现相同的功能。 膨胀:库常常实现的不仅仅是它们的用途。 依赖地狱:库通常依赖于特定版本的其他库。 未知质量:第三方代码的质量和安全性可能存在问题。 未知支持:第三方库的开发可能随时停止支持。 意外更改:第三方库通常不像标准库那样严格地进行版本控制。 关于这方面更多的信息请参考 Russ Cox 提供的资料 标准化格式 Gofmt 的风格没有人会去喜欢,但人人都会喜欢 gofmt。 ——Rob Pike Gofmt 是一种以标准化方式来格式化 Go 代码的程序。它不是最漂亮的格式化方式,但却是最简单、最不令人生厌的格式化方式。标准化的源代码格式具有惊人的积极影响: 集中讨论重要主题: 它消除了围绕制表符和空格、缩进深度、行长、空行、花括号的位置等一系列争论。 开发人员在彼此的代码库中感觉很自在, 因为其他代码看起来很像他们编写的代码。每个人都喜欢自由地按照自己喜欢的方式进行格式化代码,但如果其他人按照自己喜欢的方式格式化了代码,这么做很招人烦。 自动代码更改并不会打乱手写代码的格式,例如引入了意外的空白更改。 许多其他语言社区现在正在开发类似 gofmt 的东西。当作为第三方解决方案构建时,通常会有几个相互竞争的格式标准。例如,JavaScript 提供了 Prettier 和 StandardJS。这两者都可以用,也可以只使用其中的一个。但许多 JS 项目并没有采用它们,因为这是一个额外的决策。Go 的格式化程序内置于该语言的标准工具链中,因此只有一个标准,每个人都在使用它。 快速编译 ![image.png](https://ucc.alicdn.com/pic/developer-ecology/8a76f3f07f484266af42781d9e7b8692.png) 对于大型代码库来说,它们长时间的编译是促使 Go 诞生的原因。Google 主要使用的是 C++ 和 Java,与 Haskell、Scala 或 Rust 等更复杂的语言相比,它们的编译速度相对较快。尽管如此,当编译大型代码库时,即使是少量的缓慢也会加剧编译的延迟,从而激怒开发人员,并干扰流程。Go 的设计初衷是为了提高编译效率,因此它的编译器速度非常快,几乎没有编译延迟的现象。这给 Go 开发人员提供了与脚本类语言类似的即时反馈,还有静态类型检查的额外好处。 交叉编译 由于语言运行时非常简单,因此它被移植到许多平台,如 macOS、Linux、Windows、BSD、ARM 等。Go 可以开箱即用地为所有这些平台编译二进制文件。这使得从一台机器进行部署变得很容易。 快速执行 Go 的运行速度接近于 C。与 JITed 语言(Java、JavaScript、Python 等)不同,Go 二进制文件不需要启动或预热的时间,因为它们是作为编译和完全优化的本地代码的形式发布的。Go 的垃圾收集器仅引入微秒量级的可忽略的停顿。除了快速的单核性能外,Go 还可以轻松利用所有的 CPU 内核。 内存占用小 像 JVM、Python 或 Node 这样的运行时不仅仅在运行时加载程序代码,每次运行程序时,它们还会加载大型且高度复杂的基础架构,以进行编译和优化程序。如此一来,它们的启动时间就变慢了,并且还占用了大量内存(数百兆字节)。而 Go 进程的开销更小,因为它们已经完全编译和优化,只需运行即可。Go 还以非常节省内存的方式来存储数据。在内存有限且昂贵的云环境中,以及在开发过程中,这一点非常重要。我们希望在一台机器上能够快速启动整个堆栈,同时将内存留给其他软件。 部署规模小 Go 的二进制文件大小非常简洁。Go 应用程序的 Docker 镜像通常比用 Java 或 Node 编写的等效镜像要小 10 倍,这是因为它无需包含编译器、JIT,以及更少的运行时基础架构的原因。这些特点,在部署大型应用程序时很重要。想象一下,如果要将一个简单的应用程序部署到 100 个生产服务器上会怎么样?如果使用 Node/JVM 时,我们的 Docker 注册表就必须提供 100 个 docker 镜像,每个镜像 200MB,那么一共就需要 20GB。要完成这些部署就需要一些时间。想象一下,如果我们想每天部署 100 次的话,如果使用 Go 服务,那么 Docker 注册表只需提供 10 个 docker 镜像,每个镜像只有 20MB,共只需 2GB 即可。大型 Go 应用程序可以更快、更频繁地部署,从而使得重要更新能够更快地部署到生产环境中。 独立部署 Go 应用程序部署为一个包含所有依赖项的单个可执行文件,并无需安装特定版本的 JVM、Node 或 Python 运行时;也不必将库下载到生产服务器上,更无须对运行 Go 二进制文件的机器进行任何更改。甚至也不需要讲 Go 二进制文件包装到 Docker 来共享他们。你需要做的是,只是将 Go 二进制文件放到服务器上,它就会在那里运行,而不用关心服务器运行的是什么。前面所提到的那些,唯一的例外是使用net和os/user包时针对对glibc的动态链接。 供应依赖关系 Go 有意识避免使用第三方库的中央存储库。Go 应用程序直接链接到相应的 Git 存储库,并将所有相关代码下载(供应)到自己的代码库中。这样做有很多好处: 在使用第三方代码之前,我们可以对其进行审查、分析和测试。该代码就和我们自己的代码一样,是我们应用程序的一部分,应该遵循相同的质量、安全性和可靠性标准。 无需永久访问存储依赖项的各个位置。从任何地方(包括私有 Git repos)获取第三方库,你就能永久拥有它们。 经过验收后,编译代码库无需进一步下载依赖项。 若互联网某处的代码存储库突然提供不同的代码,这也并不足为奇。 即使软件包存储库速度变慢,或托管包不复存在,部署也不会因此中断。 兼容性保证 Go 团队承诺现有的程序将会继续适用于新一代语言。这使得将大型项目升级到最新版本的编译器会非常容易,并且可从它们带来的许多性能和安全性改进中获益。同时,由于 Go 二进制文件包含了它们需要的所有依赖项,因此可以在同一服务器上并行运行使用不同版本的 Go 编译器编译的二进制文件,而无需进行复杂的多个版本的运行时设置或虚拟化。 文档 在大型工程中,文档对于使软件可访问性和可维护性非常重要。与其他特性类似,Go 中的文档简单实用: 由于它是嵌入到源代码中的,因此两者可以同时维护。 它不需要特殊的语法,文档只是普通的源代码注释。 可运行单元测试通常是最好的文档形式。因此 Go 要求将它们嵌入到文档中。 所有的文档实用程序都内置在工具链中,因此每个人都使用它们。 Go linter 需要导出元素的文档,以防止“文档债务”的积累。 商业支持的开源 当商业实体在开放式环境下开发时,那么一些最流行的、经过彻底设计的软件就会出现。这种设置结合了商业软件开发的优势——一致性和精细化,使系统更为健壮、可靠、高效,并具有开放式开发的优势,如来自许多行业的广泛支持,多个大型实体和许多用户的支持,以及即使商业支持停止的长期支持。Go 就是这样发展起来的。 缺点 当然,Go 也并非完美无缺,每种技术选择都是有利有弊。在决定选择 Go 之前,有几个方面需要进行考虑考虑。 未成熟 虽然 Go 的标准库在支持许多新概念(如 HTTP 2 Server push 等)方面处于行业领先地位,但与 JVM 生态系统中的第三方库相比,用于外部 API 的第三方 Go 库可能不那么成熟。 即将到来的改进 由于清楚几乎不可能改变现有的语言元素,Go 团队非常谨慎,只在新特性完全开发出来后才添加新特性。在经历了 10 年的有意稳定阶段之后,Go 团队正在谋划对语言进行一系列更大的改进,作为 Go 2.0 之旅的一部分。 无硬实时 虽然 Go 的垃圾收集器只引入了非常短暂的停顿,但支持硬实时需要没有垃圾收集的技术,例如 Rust。 结语 本文详细介绍了 Go 语言的一些优秀的设计准则,虽然有的准则的好处平常看起来没有那么明显。但当代码库和团队规模增长几个数量级时,这些准则可能会使大型工程项目免于许多痛苦。总的来说,正是这些设计准则让 Go 语言成为了除 Java 之外的编程语言里,用于大型软件开发项目的绝佳选择。

有只黑白猫 2020-01-07 14:11:38 0 浏览量 回答数 0
阿里云大学 云服务器ECS com域名 网站域名whois查询 开发者平台 小程序定制 小程序开发 国内短信套餐包 开发者技术与产品 云数据库 图像识别 开发者问答 阿里云建站 阿里云备案 云市场 万网 阿里云帮助文档 免费套餐 开发者工具 企业信息查询 小程序开发制作 视频内容分析 企业网站制作 视频集锦 代理记账服务 2020阿里巴巴研发效能峰会 企业建站模板 云效成长地图 高端建站 阿里云双十一主会场 阿里云双十一新人会场 1024程序员加油包 阿里云双十一拼团会场 场景化解决方案 阿里云双十一直播大厅