(点击底部“阅读原文”获取任发科演讲完整PPT)
讲师介绍
任发科,网名常新居士,曾任职唯品会、会唐网、亚马逊、ThoughtWorks,有十余年软件开发、架构和管理经验。曾参与多个电商相关系统的研发工作,近年主要关注DevOps工具链的设计与实现,以及高效研发团队的组建与管理。
今天我主要是从两个方面去探讨DevOps,由于大部分的同学可能更多的是看到了运维这个层面,所以我更多侧重的是Dev这个层面,也就是从Dev到运维,因为正好是整个全流程走到这里,我们看到了一些实践,也看到了将来的一些机会和趋势,所以今天会谈一谈我们公司近两年做的过程,也就是我们怎么做DevOps。
一、从业务、系统发展看问题
从业务和系统的发展,我们来看当时面临的问题和解决的措施,有一些总结性和思考性的东西。就像程永新老师在企业级运维三板斧所说的,未来不是DevOps,关注方向的可能是AIOps这个层面,也就是说DevOps更要关注的是ADPaas平台,而在运维侧则更多的是AIOps,就像谷歌的系统是自治的,不需要人为介入,所以运维侧是要受到很大挑战的。
这是我当时加入公司时的一个基础组织架构,跟所有互联网公司,或者一些创业已经过了风险期的公司大致相似,这是标准化的建制,就是说业务和研发、测试、运维都有,是这样的一个结构。
敏捷与持续集成
第一个过程,当我加入到这个团队里面时,整个研发体系加上在一起大概40人左右。刚进去时我们做敏捷和持续集成,跟所有的创业团队一样,内部没有规范,做事图快,质量较低。大家在创业公司或者是一些中型公司呆过的话就会知道,基本上没有走到标准化流程阶段,基本上都会存在相同的问题,就是业务和研发对接的管理非常混乱,没有相对规范的流程。
开发做出来的东西提交测试时没有太多的责任心(在心理定位上将测试人员当作编码质量的防护网),里面Bug非常多。然后上线测试,我们当时让测试做构建,把这个包构建出来,最后交给运维,由运维负责上线。整个过程很乱,比如拷到某一个发版机器上,拷上去以后用文件夹打个标签交给运维,运维再往线上扔,扔到线上以后就要人肉一段时间,看一看没什么问题才回家睡觉。
存在问题:你可以看到基本上的问题就是,需求侧这面项目管理非常乱,研发的交付质量很低,会有很多返工的事。项目上线的周期长,问题非常多。
我们首先做的是基于持续集成的改善。从过程管理这个层面,我们把Scrum引入进来,对Scrum做了一定的调整。实际上最近在敏捷圈子里吵得非常厉害,各种方法论大家都不服,都认为自己代表了“天意”。但从企业的角度,尤其是我们解决问题的角度,用哪个都无所谓,能解决问题才是根本。所以我们根据企业当前人员情况对Scrum进行了裁剪,不是什么都要,明确哪些问题需要用Scrum去做。把Scrum流程引入之后,再用JIRA做需求管理、排期这些事情,然后内部用GitLab去做整个代码的管理,搭一个服务器,把UT做起来,很快能够得到好的反馈。这个是非常成熟的套路,如果用强制的方法做的话见效很快,基本上1-2个月就能梳理出来。
在我们做完了后会发现,这个过程不是那么顺,不是上来以后奇迹一般的就做好了,中间肯定有很多很多需要去沟通、协调的一些过程。你可以看到业务跟研发的对接相对来说变得好了,不一定非常流畅,但可控了。但研发到测试这边,你会发现它的墙变灰了,不是那么实了,因为当你有UT搭建起来后,其实提交的质量、责任已经放在研发这一侧了,这时候测试拿到的东西相对就好很多,它就不会来来回回地在底层的一些简单问题上掰扯。但这时运维这一侧还没有受到太大的影响。
存在问题:这时我们发现还是有一些问题,就是研发到测试的交付物不同源,一致性存在问题(我们后面会讲一致性的问题);测试手工验证周期还很长,因为它没有做测试的自动化,即功能测试和性能测试都没有做自动化;项目上线时间长,因为没有触动到运维侧。所以这时候我们要做的就是持续交付的第一个版本,我们把它叫做持续交付的1.0。
持续交付1.0
持续交付的1.0做了什么事?就是我们现在大部分在40人以下的小团队要做的——一个交付流水线,其实多数团队都是这样做,就是加一个pipeline的插件,部署的脚本写在Jenkins里头,然后把它跟源代码放在一起,这就是我们现大部分持续交付的事情——加一个pipeline插件。我没有截那个图,这里画了一个大概的形式。
你可以看到之前的CI我们还需要做,但Jenkins有了一定的构建能力,并且通过pipeline插件我们可以做多环境的部署,只要构建出来以后一步一步往上推(Promotion),但在UAT到生产这一步需要人工做最后的审批,并不全自动化。
这里涉及到两个概念的差别呢?就是什么是持续交付,什么是持续部署,现在行业里头谈持续交付多一些,谈持续部署少一些。持续集成是关注在研发侧,不涉及到后面的自动化验证和部署的过程,但持续交付关注到了构建后的部署和验证,但它有一部分的手工验证过程,它可能在全线是贯通的,但最后的一步和中间验证都是手工的。而持续部署强调的是全自动,中间没有任何人为的干预,只要你一提交代码马上就开始通过部署流水线向生产流动。我们现在说的是持续交付,所以这在生产过程实际上还需要手工去审核的。但这个过程实际是有问题的,在系统很小的时候可以这么做,但团队变大的时候就有问题。
存在问题:什么问题?持续交付说只构建一次,然后在这个构建的产物上进行验证和部署,这就需要引入制品库。但是我们遇到了一些团队,你会发现实际上在代码的生成和验证的过程中,它都是从版本库里头直接拉代码回来,不断地拉代码,拉了以后再动态地打包,这样的话会有一些问题。
所以我们希望把制品库引进来,就是拿的东西是第一次就构建成功的东西,然后在这个上面不断地附加原数据。什么是原数据?就是经过或没经过单测,单测的百分比是多少,然后谁去验证它,这都是跟这个制品有关的一些行为和流程数据,它必须要记录下来,因为后期的活动可以基于这些数据去做选择。
制品库:Nexus->Artifactor->自研
我们整个制品库过程选择经过了3个阶段,最早的整个项目是用Java来做的,很自然的,我们就用Nexus做私库,去做制品的构建产物管理。因为它很容易解决依赖的问题,而依赖是很麻烦的事情。因为Nexus支持产品版本,不支持构建版本的追踪,同时它没有原数据管理的能力。
所以第二步我们跟JFrog对接,希望用它的Artifactory,这是用得比较成熟且用得比较多的,目前谷歌、腾讯、阿里也在用。但问题是公司体量不大,而专业版的Artifactory比较贵,我们负担这样的花费有点不划算,跟大家一样我们想用开源版本的,但社区版的Artifactory不支持多语言,而且它构建版本的支持要专业版,所以没办法,我们调研以后自己做,拿什么做?
很简单,你可以搭一个类似S3的文件服务,把它放在文件服务上去。第二种方式我们用数据库去做,有些数据库实际上是可以去存二进制的大文件的,而且它可以很方便地做Key-Value的元数据管理,但自己研发制品库的问题在于并发、高可用和快速下发。在规模较小时,这些还不是问题。
它的好处是什么?加了制品库以后,我们能保证把构建版本和元数据打上去,在这样的话我们保证部署的都是同源的,而且一步一步可以在它的部署逻辑里头根据元数据经过选择,就是它没有经过前面的阶段或者到达一定的百分比我是不允许后面的阶段看到这个构建产物。
现在你可以看到我们的整个结构,就是说开发到测试的阶段实际上这个墙就没有了。这里说一下我们自己的业务,我们的业务基本上是支持公司内部的电商和公司内部的其它互联网产品。这时你可以看到测试到运维这边的墙实际上可以把它拿走,但运维这时还有一些问题就是他的事比较杂,为什么杂?因为应用的运维。我们可以把运维看成两部分,一部分是传统的运维或者说是基础设施和工具化的运维,另一方面是跟应用相关的运维,应用的运维我们也是放在运维团队的,这个安排实际上是不合适的。所以说运维的事就比较杂,不能专心地干自己特别擅长的事。
存在问题:这时我们发现运维人员需要维护大量的环境,包括应用的部署;第二,构建环境与生产环境的一致性是有问题的。为什么?比如说构建环境放在Jenkins里,如果去用它做构建的话,它的环境是定好的,例如JDK1.8,那我所有的东西只能在JDK1.8上去构建,但如果我要装JDK1.7呢?那就另搭一套环境去做它,比较麻烦。部署逻辑和Jenkins是紧耦合的,因为这些东西我们是在Jenkins里用脚本编程,而且它没有快速回滚机制。有些时候实际上是要快速回滚的,不能重新拉代码版本再部署一次。最后,依然需要大量的人员去做半自动化的测试。
持续交付2.0
用配置管理工具(Ansible)管理环境
这时我们就想做2.0的持续交付,最早我们是用Ansible做环境的管理,在持续交付2.0之前和持续交付1.0之间发生了一件事情:我们把大量的机器从阿里云上搬到自己的私有云上,在一年不到的时间我们搭建了OpenStack的私有云,一年里给公司节省的成本大概是在几百万左右,除了少量对外的一些服务必须要放在阿里云上,我们大部分核心计算都是放在内部的私有云上,这是我们运维团队做的非常大的贡献。我们到了2.0的阶段,实际上大部分的机器都是虚机。这时候加上Ansible以后,可以拿Jenkins去管这些集群的环境,这样的大家相对都用得比较多,就不过多解释。
存在问题:这时整个场景里可以看到,有一部分是应用的业务部署,有一部分是环境的部署、管理,但当真正去实践时发现有些问题。第一个是需要构建环境和生产环境的版本是一致的,即应用包依赖的版本一致,包括操作系统也是一致的。第二个就是需要构建工具的一致性,就是说,构建时比如说是1.9构建的,回滚时也必须要回滚到1.9进行重建,必须要把这个信息记录下来。
如果大家读过谷歌的文章,讲他们bazel整个构建系统的话,你会发现它的一个巨大的目标就是要做到一致性,最重要的经验就是把所有的构建的依赖和构建工具放到版本库里进行统一管理。现在用Ansible这个方式去管环境,用Jenkins去构建,不会有什么问题。但当发现应用包丢了,想重新构建一次时用Jenkins进行环境构建时相对比较麻烦,很难自动化再一键构建出来一个。怎么办?接下来我们在3.0里头会讲我们是怎么做的。
测试人员和测试工作的定位
这时,我们开始做测试的自动化,就是功能测试,还包括测试平台。这样的话,测试人员和测试工作的定位我们需要重新反思一下。
因为我们做的工作跟传统的业务相对还不是那么结合紧密,基本上就做数据支持的,大数据、用户行为分析,包括我们自己做APM和其它工具,这些工作大部分是技术性的工作,在这里面人肉QA的工作没有那么大量。所以这时我们希望把QA的工作极大地压缩,甚至从我们的业务流程里头去除掉,事实上我们也整个把QA给干掉了。
这里面逻辑是什么?就是留活不留人,比如说一些用户的可用性这样的测试我们也是需要人的,但这种工具类型的东西我们不需要人做测试,我们用机器做就可以了。所以这时你会发现整个流程就留下Dev和Ops了,把活留下了。
Docker与不可变部署
我们开始引入Docker,希望做基于Docker的不可变部署。Docker有个好处就是在生成一个镜像时,可以通过描述来声明其包含的内容,并将整个应用和它的环境打包成一个镜像,这样的话,测试验证了这个镜像以后,它随后进行的所有部署都不需要变更,所需要变更的东西只是配置,你可以在启动这个镜像时给它加一些不同的配置,但它内部的实现一般是不变的,回滚和前滚都是非常容易的。
我们第一个版本的Docker部署是内部的一个工具,一个在线报障工具,整个运行在Docker上。因为大部分业务还不敢把核心业务放到Docker上去,当然我们知道一些互联网公司在做尝试,而且我们没有用原生的Docker commit去打包,因为遗留系统应用的打包我们还需要用,还是有一些虚机的打包,所以我们用的是Paker。那么整个流程实际上很简单,还是用Jenkins去做打包,镜像存储到内部的私服上。
既然产品可以运行在Docker上,那么构建环境能不能也运行在Docker上?肯定能,这时候Jenkins上拉起来去构建实际是在Docker环境里头构建,就是我们在生成一个项目时,我们会给这个项目添加一些元数据:项目的名称、负责团队、代码库,我们还会指定这个项目构建的依赖是什么。
这时它会在内部选择我们已经打好的基础构建镜像,比如到底是选JDK 1.7的Docker还是JDK 1.8的Docker,将来构建是在Docker上构建,就是构建时用Docker做构建环境。这样的话就简单了,回滚时拉一个Docker进来就可以了,比Jenkins自己构建容易管理很多,同时你会发现重建的过程也随之简单了。
运维人员和运维工作的定位
这时,我们开始对运维人员和运维的工作进行定位,因为测试人员已经被我们精简了,从整个业务流程里拿掉了。那运维人员是不是也要被我们拿掉?其实不是,我们所做的是把运维的工作简化了,把不该运维负责的东西拿出来了。应用运维不需要运维团队负责,最终,产品从需求变成代码,从代码到生产,生产上以后监控,出了问题以后修复,这些具体执行都不需要运维介入,研发来做,谁构建谁运营。那运维管什么?管基础设施的运维,甚至是工具的开发,我们把工具开发的团队放在运维团队里头。
这时大家看到的就是这样一个结构,就是研发使用运维提供的两种工具,第一个是支持工具,可能在还没有成为一个集成的工具前我们可能有多个工具,比如代码管理、项目管理、分支管理、构建系统,以及最后的部署和发布系统。还有基础设施的东西也是需要运维人员去做的,所以运维团队变成了两个部分,但在亚马逊工具的开发是放在Dev这个部分的,这样的话就变成开发在整个生命流程中他全管,他管的是跟业务相关的使用,而并不是要去开发监测系统,但所有的监测系统、部署系统都交由运维做。
Jenkins承担了太多职责
这个时候,Jenkins承担了太多的工作,CI、构建、环境、部署都是放在它这儿,所以每个团队上来做一个新的部署流水线时,要根据他的东西微调,重写所有的脚本,这实际上非常浪费时间。我们希望能通过配置,不需要重写这些东西,也就是把编程性的工作变成配置性的工作。
关键工作系统化
首先,就是要把关键的工作做成单独的系统,把它系统化;对于构建,我们不能再把它作为Jenkins的一个插件,我们需要把它单独拿出来做一个系统。部署也需要单独做一个系统,都需要脱离Jenkins做,这样才好管理,好拿一些关键性的数据。于是就演变成这样一个系统,就是环境管理系统、运行环境、部署流水线、元数据服务的一个简单结构。
下一步我们要把编程性的工作变成配置性的工作,因为我们不想让程序员老写一大堆脚本,而且在专属系统内可以开展一些更细节的工作,这是什么意思呢?
实际上你可以看到每一个部分都有很多的实践要做,比如说部署策略,包括了一些分层测试、环境、流量进来怎么样分流,一些精确测试,告警这边也是一样的,有很多具体的细节实践,如果都放在Jenkins里是非常难做的。
这样的话我们软件开发交付的是什么?应该是一个运行的系统,那么这个系统的生成的过程应该是可配置、可重建、可追溯的,而且它的过程是自动化、服务化和可视化的,整个过程都能一目了然地看到。
自改进体系
自改进的体系,这个是偏运维侧的东西。第一个报障和事故分析,就是我们的系统到底运行得好与不好?业务运行的好坏怎么样判断?最简单的就是通过数据判断。有一个方法就是我们一旦发现一个问题,就要迅速发现、定位、跟进、解决,而且要促进分析,产生改进,积累知识和支撑管理。
这是传统的内部沟通的结构。它可能有个内部工单的系统,但没有全流程的打通,而是通过邮件和IM这样的东西去做沟通。
这是我给之前公司画的流程,包括已经做的、还没有做的事情,很复杂。但如果我加上两个系统,就可以产生一个是轮值、报障系统,加上卓越运营的理念,就可以基于故障做事故分析,总结经验,把它变成流程和工具的改进源头。
整个结构如上图所示,有智能报障系统、根因分析系统,根因分析系统会产生两种东西,第一种流程性的改进变成了SOP放在WIKI里头,然后项目性的东西反馈到JIRA里面跟进,即哪一个迭代需要通过系统进行改进。然后系统可以根据指标(阈值)——是3级报障去生成一个COE(事故分析)还是2级报障要生成一个COE,就是谷歌和亚马逊说的事故总结和分析,但它会有一些数据的分析和呈现。
运营目标和运营数据方面,我们可以看历史的数据和它的趋势,整个SLA趋势是不是在变好。
问题分析实际上是这样一个结构,如果大家看SRE的话,SRE中的事故分析有五个结构,我们用的是亚马逊的一套结构,跟谷歌的略有不同,但大致上是相同的,理念也是相同的。
运维侧大家都比较熟悉,你会发现研发层面已经顺畅,这时系统的运行状态就是我们需要关注的,这实际上是纵向的和横向的,纵向是从业务面的,横向是在系统架构从前到后那么多机器里头,到底哪出问题了,这时就会发现需要三个东西,第一个需要传统的APM,第二需要日志的分析,第三需要全链路的追踪能力。
全景图基本上如上图所示,可以看到左边是偏研发的,右边是偏运维的,还有很多的工具在内部运营,比如统一身份的认证,安全扫描和管理,网络这些东西都需要,一旦运维起来整个业务很多东西都需要工具来支撑。这里面绩效管理、组织人员、邮件列表、技术通讯、项目管理都需要考虑方法。
整个组织结构如上图,核心的东西实际上只考虑两个东西。第一个就是谁构建谁运维,研发要做所有的事,因为他构建这个系统,所以他运维这个系统,但他不运维基础设施,也不运维工具。第二个就是领导力,我们非常关注领导力,领导力是整个企业的核心价值,从人员的招聘、培养到淘汰,都是基于领导力考虑的。这里我说几项领导力,一是责任感,二是执行力,三是学习能力,你要判断你招来的人是怎样一个人,能否靠得住,他需要跟你企业价值观高度吻合和融合。如果你招来一个人能力很强,但不认同你的价值观,大家就很难到一块工作。
因为现在所有的原始数据都有了。当数据大量呈现出来时,要做到没有误报、没有漏报,更具不同情况动态选择阈值等等,还有很多要做的,比如说机器学习、异常检测等。
总结
运维侧的目标是自治系统
第一,在未来,运维侧的改进是下一个爆发点,而且这个改进一定是去人化的,怎样研制出自治的系统,不需要人为干预,出了问题马上知道在哪,然后它自己去修复自己,这个是将来的趋势。因为它不需要应用的运维,而是整个资源的运维,它要把所有东西都看成资源,所有的数据都能拿上来,无论是面的数据、点的数据、线的数据。目前我们也还在做这部分工作。
研发侧的目标是ADPaas
第二,研发侧的目标一定是ADPaas,将来研发要做一个系统出来时,并不是说我这个系统是一步步编程的,编程要做的应该是核心业务模块,但系统运行在什么上面环境应该很简单的搭建起来。我们基本的一个理念就是先利用开源,在公司规模比较小时开源是非常好的,先解决关键问题,不需要大生态的建设和布局,在需要的时候才去自研,因为开源的东西绝对不可能满足全部需求。
Q1:老师刚才说的谁开发谁负责,可能在你们公司做得到,但像我的公司不一定做得到,因为我们很多系统都是包给不同的供应商,他们有自己环境的标准,或者说他们开发完交付以后不一定负责运维,还是有统一的团队可能会做应用的运维。这一点上有没有什么办法?或者说建一个抽象的或统一的标准来把这个东西实现?
A1:我们现在也在做一些输出,就是帮着说银行、能源去做这样交付性的工作。你会发现互联网企业的特点,就是什么事都自己干,为什么?因为最快,跟他的业务结合得最紧。我们接触到的银行、电力,尤其是银行系统,特别喜欢外包,他只做甲方。但你可以看到银行的一个趋势就是从五六年前开始,实际上是慢慢在解冻的,银行现在也在学习互联网,而且互联网化的特别快,比如说平安、中信,这种有民资的不是完全国企化管得那么严格的银行实际上都是在向敏捷、DevOps转型,他们学得很快,会把很多关键的开发系统拿回来自己做,因为这样才是最快的。另外,电商公司的ERP系统有买的吗,有,唯品会早期是买的,但是当订单几百万时,买的软件完全跟不上业务开发节奏,就变成了自研,所以整个趋势一定是自研的。
接下来说一说我们的经验。我们给一些能源系统实施的时候,如果你要外包你的软件需要做好两个东西,第一是这个软件的标准化,就是怎么样跟你的大生态系统契合在一起,无论是架构上、还是整个管理流程上必须要有个通盘的考虑,第二你在做甲方的时候不要太舒服,需要外派一些人跟在他们那儿,这些人在系统交接完毕回来就变成了这个系统的运维层面的人。类似于谷歌的SRE,就是我要看着你把系统做出来,然后你参与到里再拉回来,因为你不可能把他的人拉进来变成你的人。谷歌的SRE通常要求研发人员运维半年以上,系统稳定以后才交给SRE做下一步的运维。
Q2:老师您好,我想问一下刚才您说的谁研发谁运维这个问题,你们所有的产品都是由开发同学自己完全负责吗?比方说有一个产品上线了以后,还需要产品运维之类的去介入吗?还有另外就是ADPaaS这块不是太了解,再简单介绍一下。
A2:您刚才说有没有产品需要其它运维的介入,现在的问题在于基本上不需要运维的介入,像CMDB这块现在都是动态化的,不再是静态的形式了,包括资源的申请也全部都是自动的,就是我要一个什么资源,到运维这儿审计一下,一个资源自动就分配过来了。所以你没有看到的那个东西就是我们之前在运维时,机器的申请还需要手工去做,但是后来上到Docker和其它的之后就不需要告诉你了,这都是可以做到的。在这个层面上,这个进步带来的一个问题就是我们确实没有再让专门的人去做应用运维,因为我不知道什么叫产品的运维,我们只有应用的运维。出了什么问题,我的工具能告诉我哪个点出了问题就行了,接下来去修复它。
这里面没细讲我们报障系统,它报账一旦出来以后我们发现是机器坏了,谁负责修这个东西呢?是我们的运维的人员去负责的,因为这个机器是你管的,这个层面还是运维团队去负责的,但是系统出了问题,逻辑出了问题,产品出了问题肯定是研发修复的。
第二个问题你刚才说的是ADPaas。亚马逊内部有个叫做Apollo的系统有很多人没有见过这个系统,这个系统非常强大,以前没有Docker时,它已经做到了Docker所有能做到的事情,但是它没有用Docker,比如Docker根本上是用文件做分区,分文件夹的方式,理念是一样的,但可以在单机上跑很多东西,做一些隔离,底层不管怎么做,最后做出来的效果是开发人员看到的是一个环境管理系统,就是告诉你这里有一个环境,这个环境包含哪些包,这些包只和运行环境有关系,还有哪些应用的包,我只声明顶级包需要什么,怎样部署和部署哪些版本全部都是控制好的。这个东西就叫做ADPaas,也就是最后研发人员需要面对的是配置性的东西,怎么上线、怎么部署都是系统做的。
谷歌的方式是侧重于运维端,谷歌的ADPaas就是围绕Borg这个系统来做,他们的方法比较程序员化,不是那么面向用户。比如他把所有东西都打成一个大包,这样的话最简单,上线的时候就往上扔就行了,这个也都可以。你最后是不是ADPaas,实际就看研发人员面对系统的时候是不是有编程性的工作。
Q3:刚才说到那个产品可能您说应用领域可能说法不太一样,但比如您说的现在所谓的IP变化就是那种服务化,但比如说你依赖的中间件,运用ADPaas自动生成,每个应用所用的都不太一样,这样怎么做一个标准化的动作,或者出了故障的话这种是应该由谁来负责?
A3:我觉得你实际上说的是另一个东西,就是如果当你出现一个问题定位到事,定位到人,定位到点,就是刚才你说的到底发生了什么事,这个事由谁来解决,它多长时间应该解决,这个实际上在我们内部是三个系统,第一个叫做问题分类系统,怎么样分类这个问题,第二个在问题分类的基础上有一个轮值的系统,就是这些问题由哪些团队进行处理,这些团队今天谁当班,如果这个人不在的话怎么样告诉他经理,如果他经理不在再向上汇报。第三个定位到点的话,这就需要我们拉通,比如APM数据有没有,其它业务日志数据分析有没有,在发生问题的一瞬间需要分析,然后把这些数据塞到报障系统里头。
很多时候自动化出现问题时,实际上我们大量情况下也能知道哪出问题了,可以自动去找。而我们需要AI处理的更多是误报和漏报的东西,哪些东西可以动态调整的需要学习,减少误报的情况。
PPT下载链接:https://pan.baidu.com/s/1hsaP59U
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721