From b67b8755e12fe41f1066a404b36f9a6c69d52c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E4=B9=BE?= Date: Wed, 16 Oct 2024 10:26:46 +0800 Subject: [PATCH] first commit --- .../62 重新认识开闭原则(OCP).md | 166 ++++++ .../63 接口设计的准则.md | 209 ++++++++ .../64 不断完善的架构范式.md | 136 +++++ .../65 架构范式:文本处理.md | 472 ++++++++++++++++++ .../66 架构老化与重构.md | 173 +++++++ .../67架构思维篇:回顾与总结.md | 179 +++++++ .../68软件工程的宏观视角.md | 98 ++++ .../69 团队的共识管理.md | 120 +++++ .../70怎么写设计文档?.md | 152 ++++++ .../71 如何阅读别人的代码?.md | 132 +++++ .../72 发布单元与版本管理.md | 164 ++++++ ...:单元测试、持续构建与发布.md | 121 +++++ .../74 开源、云服务与外包管理.md | 153 ++++++ .../75 软件版本迭代的规划.md | 175 +++++++ .../76 软件工程的未来.md | 86 ++++ .../77 软件工程篇:回顾与总结.md | 116 +++++ ...怎么保障发布的效率与质量?.md | 178 +++++++ .../加餐如何做HTTP服务的测试?.md | 231 +++++++++ ...战:“画图程序”的整体架构.md | 184 +++++++ ...):区块链、比特币与Libra币.md | 222 ++++++++ ...(下):深入浅出理解Libra币.md | 133 +++++ ...户故事站在更高的视角看架构.md | 95 ++++ ...师,我需要成为“全才”吗?.md | 202 ++++++++ ...段,用极限思维提升架构能力.md | 138 +++++ ...孙子兵法》看底层的自然法则.md | 85 ++++ ...争论,先来看看真正的低代码.md | 143 ++++++ ...低代码平台到底是什么样的?.md | 210 ++++++++ ...到底是银弹,还是行业毒瘤?.md | 137 +++++ ...代码平台应该具备哪些条件?.md | 122 +++++ ...用能力还是先满足业务需求?.md | 188 +++++++ ...发之前,你需要有什么家底?.md | 213 ++++++++ ...oss:如何架构低代码的引擎?.md | 159 ++++++ ...生成法:代码如何生成代码?.md | 348 +++++++++++++ ...器:如何做到鱼和熊掌兼得?.md | 197 ++++++++ ...Web组件属性与编辑器的耦合?.md | 403 +++++++++++++++ ...前后端逻辑开发的技能门槛?.md | 213 ++++++++ ...de与ProCode混合使用怎样实现?.md | 210 ++++++++ ...再好的App,没有数据也是白搭.md | 239 +++++++++ ...百合的春天为啥来得这么晚?.md | 204 ++++++++ ...对Git做改造,还是另辟蹊径?.md | 217 ++++++++ ...研发生命周期中的哪些功能?.md | 147 ++++++ ...实现插件系统并形成生态圈?.md | 262 ++++++++++ ...:如何有效发现兼容性问题?.md | 130 +++++ ...:如何有效解决兼容性问题?.md | 278 +++++++++++ ...低代码之路,我们才刚刚开始.md | 85 ++++ ...个测试人都要学好性能测试?.md | 194 +++++++ .../01JMeter的核心概念.md | 208 ++++++++ .../02JMeter参数化策略.md | 169 +++++++ ...建并执行JMeter脚本的正确姿势.md | 325 ++++++++++++ .../04JMeter二次开发其实并不难.md | 350 +++++++++++++ ...于JMeterAPI开发性能测试平台?.md | 384 ++++++++++++++ .../06Nginx在系统架构中的作用.md | 338 +++++++++++++ ...如何制定性能测试的目标吗?.md | 160 ++++++ .../08性能测试场景的分类和意义.md | 114 +++++ ...定一份有效的性能测试方案?.md | 110 ++++ ...0命令行监控Linux服务器的要点.md | 222 ++++++++ ...式服务链路监控以及报警方案.md | 232 +++++++++ ...何把可视化监控也做得酷炫?.md | 237 +++++++++ .../13Docker的制作、运行以及监控.md | 274 ++++++++++ ...何从CPU飙升定位到热点方法?.md | 195 ++++++++ ...何基于JVM分析内存使用对象?.md | 218 ++++++++ ...通过Arthas定位代码链路问题?.md | 305 +++++++++++ ...Redis缓存穿透、击穿和雪崩?.md | 203 ++++++++ .../18如何才能优化MySQL性能?.md | 269 ++++++++++ .../19如何根治慢SQL?.md | 367 ++++++++++++++ ...线上全链路性能测试实践总结.md | 121 +++++ ...篇词带给你不一样的运维思考.md | 54 ++ ...01为什么Netflix没有运维岗位?.md | 107 ++++ ...建设为什么要以应用为核心?.md | 118 +++++ ...建立应用标准化体系和模型?.md | 140 ++++++ ...础架构标准化及服务化体系?.md | 113 +++++ ...视角看待应用运维体系建设?.md | 103 ++++ .../06聊聊CMDB的前世今生.md | 99 ++++ ...为什么还需要应用配置管理?.md | 90 ++++ ...何在CMDB中落地应用的概念?.md | 123 +++++ .../09如何打造运维组织架构?.md | 110 ++++ .../10谷歌SRE运维模式解读.md | 84 ++++ ...起,运维如何培养服务意识?.md | 98 ++++ ...成这事你要理解这几个关键点.md | 88 ++++ ...交付的第一关键点:配置管理.md | 101 ++++ ...续交付中的多环境配置管理?.md | 138 +++++ ...境?是时候进行多环境建设了.md | 125 +++++ ...设,要扛得住真刀真枪的考验.md | 139 ++++++ ...聊聊持续交付中的流水线模式.md | 106 ++++ 84 files changed, 15156 insertions(+) create mode 100644 专栏/许式伟的架构课/62 重新认识开闭原则(OCP).md create mode 100644 专栏/许式伟的架构课/63 接口设计的准则.md create mode 100644 专栏/许式伟的架构课/64 不断完善的架构范式.md create mode 100644 专栏/许式伟的架构课/65 架构范式:文本处理.md create mode 100644 专栏/许式伟的架构课/66 架构老化与重构.md create mode 100644 专栏/许式伟的架构课/67架构思维篇:回顾与总结.md create mode 100644 专栏/许式伟的架构课/68软件工程的宏观视角.md create mode 100644 专栏/许式伟的架构课/69 团队的共识管理.md create mode 100644 专栏/许式伟的架构课/70怎么写设计文档?.md create mode 100644 专栏/许式伟的架构课/71 如何阅读别人的代码?.md create mode 100644 专栏/许式伟的架构课/72 发布单元与版本管理.md create mode 100644 专栏/许式伟的架构课/73软件质量管理:单元测试、持续构建与发布.md create mode 100644 专栏/许式伟的架构课/74 开源、云服务与外包管理.md create mode 100644 专栏/许式伟的架构课/75 软件版本迭代的规划.md create mode 100644 专栏/许式伟的架构课/76 软件工程的未来.md create mode 100644 专栏/许式伟的架构课/77 软件工程篇:回顾与总结.md create mode 100644 专栏/许式伟的架构课/加餐 怎么保障发布的效率与质量?.md create mode 100644 专栏/许式伟的架构课/加餐如何做HTTP服务的测试?.md create mode 100644 专栏/许式伟的架构课/加餐实战:“画图程序”的整体架构.md create mode 100644 专栏/许式伟的架构课/热点观察我看Facebook发币(上):区块链、比特币与Libra币.md create mode 100644 专栏/许式伟的架构课/热点观察我看Facebook发币(下):深入浅出理解Libra币.md create mode 100644 专栏/许式伟的架构课/用户故事站在更高的视角看架构.md create mode 100644 专栏/许式伟的架构课/答疑解惑想当架构师,我需要成为“全才”吗?.md create mode 100644 专栏/许式伟的架构课/结束语放下技术人的身段,用极限思维提升架构能力.md create mode 100644 专栏/许式伟的架构课/课外阅读从《孙子兵法》看底层的自然法则.md create mode 100644 专栏/说透低代码/00开篇词|抛开争论,先来看看真正的低代码.md create mode 100644 专栏/说透低代码/01|低代码平台到底是什么样的?.md create mode 100644 专栏/说透低代码/02|低代码到底是银弹,还是行业毒瘤?.md create mode 100644 专栏/说透低代码/03|低代码的天花板:一个完备的低代码平台应该具备哪些条件?.md create mode 100644 专栏/说透低代码/04|演进策略:先发展通用能力还是先满足业务需求?.md create mode 100644 专栏/说透低代码/05|基础设施:启动低代码平台研发之前,你需要有什么家底?.md create mode 100644 专栏/说透低代码/06|踏出新手村便遭遇大Boss:如何架构低代码的引擎?.md create mode 100644 专栏/说透低代码/07|结构化代码生成法:代码如何生成代码?.md create mode 100644 专栏/说透低代码/08|布局编辑器:如何做到鱼和熊掌兼得?.md create mode 100644 专栏/说透低代码/09|属性编辑器:如何解除Web组件属性与编辑器的耦合?.md create mode 100644 专栏/说透低代码/10可视化编程:如何有效降低App前后端逻辑开发的技能门槛?.md create mode 100644 专栏/说透低代码/11|亦敌亦友:LowCode与ProCode混合使用怎样实现?.md create mode 100644 专栏/说透低代码/12业务数据:再好的App,没有数据也是白搭.md create mode 100644 专栏/说透低代码/13|多人协同编辑:野百合的春天为啥来得这么晚?.md create mode 100644 专栏/说透低代码/14|编辑历史:是对Git做改造,还是另辟蹊径?.md create mode 100644 专栏/说透低代码/15|低代码平台应该优先覆盖应用研发生命周期中的哪些功能?.md create mode 100644 专栏/说透低代码/16|扩展与定制:如何实现插件系统并形成生态圈?.md create mode 100644 专栏/说透低代码/17|兼容性问题:如何有效发现兼容性问题?.md create mode 100644 专栏/说透低代码/18|兼容性问题:如何有效解决兼容性问题?.md create mode 100644 专栏/说透低代码/总结与展望|低代码之路,我们才刚刚开始.md create mode 100644 专栏/说透性能测试/00开篇词为什么每个测试人都要学好性能测试?.md create mode 100644 专栏/说透性能测试/01JMeter的核心概念.md create mode 100644 专栏/说透性能测试/02JMeter参数化策略.md create mode 100644 专栏/说透性能测试/03构建并执行JMeter脚本的正确姿势.md create mode 100644 专栏/说透性能测试/04JMeter二次开发其实并不难.md create mode 100644 专栏/说透性能测试/05如何基于JMeterAPI开发性能测试平台?.md create mode 100644 专栏/说透性能测试/06Nginx在系统架构中的作用.md create mode 100644 专栏/说透性能测试/07你真的知道如何制定性能测试的目标吗?.md create mode 100644 专栏/说透性能测试/08性能测试场景的分类和意义.md create mode 100644 专栏/说透性能测试/09如何制定一份有效的性能测试方案?.md create mode 100644 专栏/说透性能测试/10命令行监控Linux服务器的要点.md create mode 100644 专栏/说透性能测试/11分布式服务链路监控以及报警方案.md create mode 100644 专栏/说透性能测试/12如何把可视化监控也做得酷炫?.md create mode 100644 专栏/说透性能测试/13Docker的制作、运行以及监控.md create mode 100644 专栏/说透性能测试/14如何从CPU飙升定位到热点方法?.md create mode 100644 专栏/说透性能测试/15如何基于JVM分析内存使用对象?.md create mode 100644 专栏/说透性能测试/16如何通过Arthas定位代码链路问题?.md create mode 100644 专栏/说透性能测试/17如何应对Redis缓存穿透、击穿和雪崩?.md create mode 100644 专栏/说透性能测试/18如何才能优化MySQL性能?.md create mode 100644 专栏/说透性能测试/19如何根治慢SQL?.md create mode 100644 专栏/说透性能测试/20结束语线上全链路性能测试实践总结.md create mode 100644 专栏/赵成的运维体系管理课/00开篇词带给你不一样的运维思考.md create mode 100644 专栏/赵成的运维体系管理课/01为什么Netflix没有运维岗位?.md create mode 100644 专栏/赵成的运维体系管理课/02微服务架构时代,运维体系建设为什么要以应用为核心?.md create mode 100644 专栏/赵成的运维体系管理课/03标准化体系建设(上):如何建立应用标准化体系和模型?.md create mode 100644 专栏/赵成的运维体系管理课/04标准化体系建设(下):如何建立基础架构标准化及服务化体系?.md create mode 100644 专栏/赵成的运维体系管理课/05如何从生命周期的视角看待应用运维体系建设?.md create mode 100644 专栏/赵成的运维体系管理课/06聊聊CMDB的前世今生.md create mode 100644 专栏/赵成的运维体系管理课/07有了CMDB,为什么还需要应用配置管理?.md create mode 100644 专栏/赵成的运维体系管理课/08如何在CMDB中落地应用的概念?.md create mode 100644 专栏/赵成的运维体系管理课/09如何打造运维组织架构?.md create mode 100644 专栏/赵成的运维体系管理课/10谷歌SRE运维模式解读.md create mode 100644 专栏/赵成的运维体系管理课/11从谷歌CRE谈起,运维如何培养服务意识?.md create mode 100644 专栏/赵成的运维体系管理课/12持续交付知易行难,想做成这事你要理解这几个关键点.md create mode 100644 专栏/赵成的运维体系管理课/13持续交付的第一关键点:配置管理.md create mode 100644 专栏/赵成的运维体系管理课/14如何做好持续交付中的多环境配置管理?.md create mode 100644 专栏/赵成的运维体系管理课/15开发和测试争抢环境?是时候进行多环境建设了.md create mode 100644 专栏/赵成的运维体系管理课/16线上环境建设,要扛得住真刀真枪的考验.md create mode 100644 专栏/赵成的运维体系管理课/17人多力量大vs.两个披萨原则,聊聊持续交付中的流水线模式.md diff --git a/专栏/许式伟的架构课/62 重新认识开闭原则(OCP).md b/专栏/许式伟的架构课/62 重新认识开闭原则(OCP).md new file mode 100644 index 0000000..89f4b8a --- /dev/null +++ b/专栏/许式伟的架构课/62 重新认识开闭原则(OCP).md @@ -0,0 +1,166 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 62 重新认识开闭原则 (OCP) + 62 | 重新认识开闭原则 (OCP)你好,我是七牛云许式伟。 + +架构的本质是业务的正交分解。 + +在上一讲 “61 | 全局性功能的架构设计” 中我们提到,架构分解中有两大难题:其一,需求的交织。不同需求混杂在一起,也就是存在所谓的全局性功能。其二,需求的易变。不同客户,不同场景下需求看起来很不一样,场景呈发散趋势。 + +我们可能经常会听到各种架构思维的原则或模式。但,为什么我们开始谈到架构思维了,也不是从那些耳熟能详的原则或模式谈起? + +因为,万变不离其宗。 + +就架构的本质而言,我们核心要掌握的架构设计的工具其实就只有两个: + + +组合。用小业务组装出大业务,组装出越来越复杂的系统。 +如何应对变化(开闭原则)。 + + +开闭原则(OCP) + +今天我们就聊聊怎么应对需求的变化。 + +谈应对变化,就不能不提著名的 “开闭原则(Open Closed Principle,OCP)”。一般认为,最早提出开闭原则这一术语的是勃兰特·梅耶(Bertrand Meyer)。他在 1988 年在 《面向对象软件构造》 中首次提出了开闭原则。 + +什么是开闭原则(OCP)? + + +软件实体(模块,类,函数等)应该对于功能扩展是开放的,但对于修改是封闭的。 + + +一个软件产品只要在其生命周期内,都会不断发生变化。变化是一个事实,所以我们需要让软件去适应变化。我们应该在设计时尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现 “拥抱变化”。 + +开闭原则告诉我们,应尽量通过扩展软件实体的行为来应对变化,满足新的需求,而不是通过修改现有代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。 + +为什么会有这样的架构设计原则?它背后体现的架构哲学是什么? + +本质上,开闭原则的背后,是推崇模块业务的确定性。我们可以修改模块代码的缺陷(Bug),但不要去随意调整模块的业务范畴,增加功能或减少功能都并不鼓励。这意味着,它认为模块的业务变更是需要极其谨慎的,需要经得起推敲的。 + +我个人非常推崇 “开闭原则”。它背后隐含的架构哲学,和我说的 “架构的本质是业务的正交分解” 一脉相承。 + +与其修改模块的业务,不如实现一个新业务。只要业务的分解一直被正确执行的话,实现一个新的业务模块来完成新的业务范畴,是一件极其轻松的事情。从这个角度来说,开闭原则鼓励写 “只读” 的业务模块,一经设计就不可修改,如果要修改业务就直接废弃它,转而实现新的业务模块。 + +这种 “只读” 思想,大家可能很熟悉。比如基于 Git 的源代码版本管理、基于容器的服务治理都是通过 “只读” 设计来改善系统的治理难度。 + +对于架构设计来说同样如此。“只读” 的架构分解让我们逐步沉淀下越来越多可复用的业务模块。如此,我们不断坚持下去,随着时间沉淀,我们的组织就会变得很强大,组装复杂业务系统也将变得越来越简单。 + +所以开闭原则,是架构治理的根本哲学。 + +CPU 背后的架构思维 + +一种广泛的误解认为,开闭原则是一种 “面向对象编程(OOP)” 领域提出来的编程思想。但这种理解显然太过狭隘。虽然开闭原则的正式提出可能较晚,但是在信息科技的发展历程中,开闭原则思想的应用就太多了,它是信息技术架构的基本原则。注意我这里没有用 “软件架构” 而是用 “信息技术架构”,因为它并不只适用于软件设计的范畴。 + +我们在 “02 | 大厦基石:无生有,有生万物” 一讲介绍冯·诺依曼体系的规格时就讲过: + + +从需求分析角度来说,关键要抓住需求的稳定点和变化点。需求的稳定点,往往是系统的核心价值点;而需求的变化点,则往往需要相应去做开放性设计。 + + +冯·诺依曼体系的中央处理器(CPU)的设计完美体现了 “开闭原则” 的架构思想。它表现在: + + +指令是稳定的,但指令序列是变化的,只有这样计算机才能够实现 “解决一切可以用 ‘计算’ 来解决的问题” 这个目标。 +计算是稳定的,但数据交换是多变的,只有这样才能够让计算机不必修改基础架构却可以适应不断发展变化的交互技术革命。 + + +体会一下:我们怎么做到支持多变的指令序列的?我们由此发明了软件。我们怎么做到支持多变的输入输出设备的?我们定义了输入输出规范。 + +我们不必去修改 CPU,但是我们却支持了如此多姿多彩的信息世界。 + +多么优雅的设计。它与面向对象无关,完全是开闭原则带来的威力。 + +CPU 的优雅设计远不止于此。在 “07 | 软件运行机制及内存管理” 这一讲中,我们介绍了 CPU 对虚拟内存的支持。通过引入缺页中断,CPU 将自身与多变的外置存储设备,以及多变的文件系统格式进行了解耦。 + +中断机制,我们可以简单把它理解为 CPU 引入的回调函数。通过中断,CPU 把对计算机外设的演进能力交给了操作系统。这是开闭原则的鲜活案例。 + +插件机制 + +一些人对开闭原则的错误解读,认为开闭原则不鼓励修改软件的源代码来响应新需求。 + +这个说法当然有点极端化。开闭原则关注的焦点是模块,并不是最终形成的软件。模块应该坚持自己的业务不变,这是开闭原则所鼓励的。 + +当然软件也是一个业务系统,但对软件系统这个大模块来说,如果我们坚持它的业务范畴不变,就意味着我们放弃进步。 + +让软件的代码不变,但业务范畴却能够适应需求变化,有没有可能? + +有这个可能性,这就是插件机制。 + +常规我们理解的插件,通常以动态库(dll/so)形式存在,这种插件机制是操作系统引入的,可以做到跨语言。当然部分语言,比如 Java,它有自己的插件机制,以 jar 包的形式存在。 + +在上一讲 “61 | 全局性功能的架构设计” 中我们提到,微软的大部分软件,以 Office 和 Visual Studio 为代表,都提供了二次开发能力。 + +这些二次开发接口构成了软件的插件机制,并最终让它成为一个生态型软件。 + +一般来说,提供插件机制的二次开发接口需要包含以下三个部分。 + +其一,软件自身能力的暴露,也就是我们经常说的 DOM API。插件以此来调用软件已经实现的功能,这是最基础的部分,我们这里不进一步展开。 + +其二,插件加载机制。通常,这基于文件系统,比如我们规定把所有插件放到某个目录下。在 Windows 平台下会多一个选择,把插件信息写到注册表。 + +其三,事件监听。这是关键,也是难点所在。没有事件,插件没有机会介入到业务中去。但是应该提供什么样的事件,提供多少个事件,这非常依赖架构能力。 + +原则来说,在提供的能力相同的情况下,事件当然越少越好。但是怎么做到少而精,这非常有讲究。一般来说,事件分以下三类: + +其一,界面操作类。最原始的是鼠标和键盘操作,但它们太过于底层,提供出去会是双刃剑,一般对二次开发接口来说会选择不提供。更多的时候会选择暴露更高级的界面事件,比如菜单项或按钮的点击。 + +其二,数据变更类。在数据发生变化的时候,允许捕获它并做点什么。最为典型的是 onSelectionChanged 这个事件,基本上所有的软件二次开发接口都会提供。当然它属于界面数据变更,只能说是数据变更的特例。如果我们回忆一下 MVC 框架(参见 “22 | 桌面程序的架构建议”),就能够记得 Model 层会发出数据变更通知,也就是 onDataChanged 类的事件出来给 View 或 Controller。 + +其三,业务流程类。它通常发生在某个业务流的中间某个环节,或者业务流完成之后。比如对 Office 软件来说,打开文件之初或之后,都可能发出相应的事件,以便插件做些什么。 + +通过以上分析可以看出,完整的插件机制还是比较庞大的。但实际应用中插件机制未必要做得如此之重。 + +比如,Go语言中的 image 包,它提供的 Decode 和 DecodeConfig 等功能都支持插件,我们可以增加一种格式支持,而无需修改 image 包。 + +这里面最大的简化,是放弃了插件加载机制。我们自己手工来加载插件,比如: + +import "image" +import _ "image/jpeg" +import _ "image/png" + + +这段代码为 image 包加载了两个插件,一个支持 jpeg,一个支持 png 格式。 + +如果大家仔细研究过我们实战案例 “画图程序” 的代码(参见 “加餐 | 实战:画图程序的整体架构”)就会发现,类似的插件机制的运用有很多。我们说的架构分解,把复杂系统分解为一个最小化的核心系统,加上多个相互正交的周边系统,它背后的机制往往就是我们这里提的插件机制。 + +插件机制的确让核心系统与周边系统耦合度大大降低。但插件机制并非没有成本。插件机制本身也是核心系统的一个功能,它本身也需要考虑与核心系统其他功能的耦合度。 + +如果某插件机制没有多少客户,也就是说,没有几个功能基于它开发,而它本身代码又散落在核心系统的各个角落,那么投入产出就显然不成比例。 + +所以维持足够的通用性,是提供插件机制的重大前提。 + +单一职责原则 + +到此为止,相信大家已经对开闭原则(OCP)非常了解了。总结来说就两点: + +第一,模块的业务要稳定。模块的业务遵循 “只读” 设计,如果需要变化不如把它归档,放弃掉。这种模块业务只读的思想,是架构治理的基础哲学。 + +第二,模块的业务变化点,简单一点的,通过回调函数或者接口开放出去,交给其他的业务模块。复杂一点的,通过引入插件机制把系统分解为 “最小化的核心系统+多个彼此正交的周边系统”。事实上回调函数或者接口本质上就是一种事件监听机制,所以它是插件机制的特例。 + +平常,我们大家也经常会听到 “单一职责原则(Single Responsibility Principle,SRP)”,它强调的是每个模块只负责一个业务,而不是同时干多个业务。而开闭原则强调的是把模块业务的变化点抽离出来,包给其他的模块。它们谈的本质上是同一个问题的两个面。 + +结语 + +从来没有人这样去谈架构的本质,也没有人这样解读开闭原则(OCP),对吧? + +其实对于这部 “架构课” 的革命性,我自己从没怀疑过。它的内容是精心设计的,为此我准备了十几年。我们用了四章内容来谈信息科技的需求与架构的演进,然后才进入正题。 + +用写文章的角度来说,这个伏笔的确够深的。 + +当然这不完全是伏笔。如果我们把整个信息科技看作最大的一个业务系统,我们有无数人在为之努力奋进,迭代它的架构。大家在竟合中形成自然的分工。学习信息科技的演进史,是学习架构的必要组成部分。我们一方面从中学习怎么做需求分析,另一方面也从中体悟做架构的思维哲学。 + +当然,还有最重要的一点是,我们要知道演进的结果,也就是信息科技最终形成的基础架构。 + +作为架构师,我们除了做业务架构,还有一个同等难度的大事,就是选择合适的基础架构。基础架构+业务架构,才是你设计的软件的全部。作为架构师,千万不要一叶障目,不见泰山,忘记基础架构选择的重要性。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “接口设计的准则”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/63 接口设计的准则.md b/专栏/许式伟的架构课/63 接口设计的准则.md new file mode 100644 index 0000000..decc440 --- /dev/null +++ b/专栏/许式伟的架构课/63 接口设计的准则.md @@ -0,0 +1,209 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 63 接口设计的准则 + 63 | 接口设计的准则你好,我是七牛云许式伟。 + +上一讲 “[62 | 重新认识开闭原则 (OCP)]” 我们介绍了开闭原则。这一讲的内容非常非常重要,可以说是整个架构课的灵魂。总结来说,开闭原则包含以下两层含义: + +第一,模块的业务要稳定。模块的业务遵循 “只读” 设计,如果需要变化不如把它归档,放弃掉。这种模块业务只读的思想,是架构治理的基础哲学。我平常和小伙伴们探讨模块边界的时候,经常会说这样一句话: + + +每一个模块都应该是可完成的。 + + +这实际上是开闭原则的业务范畴 “只读” 的架构治理思想的另一种表述方式。 + +第二,模块业务的变化点,简单一点的,通过回调函数或者接口开放出去,交给其他的业务模块。复杂一点的,通过引入插件机制把系统分解为 “最小化的核心系统+多个彼此正交的周边系统”。事实上回调函数或者接口本质上就是一种事件监听机制,所以它是插件机制的特例。 + +今天,我们想聊聊怎么做接口设计。 + +不过在探讨这个问题前,我想和大家探讨的第一个问题是:什么是接口? + +你可能会觉得这个问题挺愚蠢的。毕竟这几乎是我们嘴巴里天天会提及的术语,会不知道?但让我们用科学家的严谨作风来看待这个问题。接口在不同的语义环境下,主要有两个不同含义。 + +一种是模块的使用界面,也就是规格,比如公开的类或函数的原型。我们前面在这个架构课中一直强调,模块的接口应该自然体现业务需求。这里的接口,指的就是模块的使用界面。 + +另一种是模块对依赖环境的抽象。这种情况下,接口是模块与模块之间的契约。在架构设计中我们经常也会听到 “契约式设计(Design by Contract)” 这样的说法,它鼓励模块与模块的交互基于接口作为契约,而不是依赖于具体实现。 + +对于这两类的接口语义,我们分别进行讨论。 + +模块的使用界面 + +对于模块的使用界面,最重要的是 KISS 原则,让人一眼就明白这个模块在做什么样的业务。 + +KISS 的全称是 Keep it Simple, Stupid,直译是简单化与傻瓜化。用土话来说,就是要 “让傻子也能够看得懂”,追求简单自然,符合惯例。 + +这样说比较抽象,我们拿七牛开源的 mockhttp 项目作为例子进行说明。 + +这个项目早期的项目地址为: + + +代码主页:https://github.com/qiniu/mockhttp.v1 +文档主页:https://godoc.org/github.com/qiniu/mockhttp.v1 + + +最新的项目地址变更为: + + +代码主页:https://github.com/qiniu/x/tree/master/mockhttp +文档主页:https://godoc.org/github.com/qiniu/x/mockhttp + + +mockhttp 是做什么的呢?它用于启动 HTTP 服务作为测试用途。 + +当然 Go 的标准库 net/http/httptest 已经有自己的 HTTP 服务启动方法,如下: + +package httptest + +type Server struct { + URL string + ... +} + +func NewServer(service http.Handler) (ts *Server) +func (ts *Server) Close() + + +httptest.NewServer 分配一个空闲可用的 TCP 端口,并将它与传入的 HTTP 服务器关联起来。最后我们得到的 ts.URL 就是服务器的访问地址。使用样例如下: + +import "net/http" +import "net/http/httptest" + +func TestXXX(t *testing.T) { + service := ... // HTTP 业务服务器 + ts := httphtest.NewServer(service) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/foo/bar") + ... +} + + +mockhttp 有所不同,它并不真的启动 HTTP 服务,没有端口占用。这里我们不谈具体的原理,我们看接口。mockhttp.v1 版本的使用界面如下: + +package mockhttp + +var Client rpc.Client + +func Bind(host string, service interface{}) + + +这里比较古怪的是 service,它并不是 http.Handler 类型。它背后做了一件事情,就是帮 service 这个 HTTP 服务器自动实现请求的路由分派能力。这有一定的好处,使用上比较便捷: + +import "github.com/qiniu/mockhttp.v1" + +func TestXXX(t *testing.T) { + service := ... // HTTP 业务服务器 + mockhttp.Bind("example.com", service) + resp, err := mockhttp.Client.Get("http://example.com/foo/bar") + ... +} + + +但是它有两个问题。 + +一个问题是关于模块边界上的。严谨来说 mockhttp.v1 并不符合 “单一职责原则(SRP)”。它干了两个业务: + + +启动 HTTP 测试服务; +实现 HTTP 服务器请求的路由分派。 + + +另一个是关于接口的 KISS 原则。mockhttp.Bind 虽然听起来不错,也很简单,但实际上并不符合 Go 语言的惯例语义。另外就是 mockhttp.Client 变量。按 Go 语义的惯例它可能叫 DefaultClient 会更好一些,另外它的类型是 rpc.Client,而不是 http.Client,这样方便是方便了,但却产生了多余的依赖。 + +mockhttp.v1 这种业务边界和接口的随意性,一定程度上是因为它是测试用途,所以有点怎么简单怎么来的意思。但是后来的发展表明,所有的偷懒总会还回来的。于是就有了 mockhttp.v2 版本。这个版本在我们做小型的 package 合并时,把它放到了https://github.com/qiniu/x 这个package 中。接口如下: + +package mockhttp + +var DefaultTransport *Transport +var DefaultClient *http.Client + +func ListenAndServe(host string, service http.Handler) + + +这里暴露的方法和变量,一方面 Go 程序员一看即明其义,另一方面语义上和 Go 标准库既有的HTTP package 可自然融合。它的使用方式如下: + +import "github.com/qiniu/x/mockhttp" + +func TestXXX(t *testing.T) { + service := ... // HTTP 业务服务器 + mockhttp.ListenAndServe("example.com", service) + resp, err := mockhttp.DefaultClient.Get("http://example.com/foo/bar") + ... +} + + +从上面的例子可以看出,我们说接口要 KISS,要简单自然,这里很重要的一点是符合语言和社区的惯例。如果某类业务在语言中已经有约定俗成的接口,我们尽可能沿用相同的接口语义。 + +模块的环境依赖 + +接口的另一种含义是模块对依赖环境的抽象,也就是模块与模块之间的契约。我们大部分情况下提到的接口,指的是这一点。 + +模块的环境依赖,也分两种,一种是使用界面依赖,一种是实现依赖。所谓使用界面依赖是指用户在使用该模块的使用界面时自然涉及的。所谓实现依赖则是指模块当前实现方案中涉及到的组件,它带来的依赖条件。如果我换一种实现方案,这类依赖可能就不再存在,或者变成另外的依赖。 + +在环境依赖上,我们遵循的是 “最小依赖原则”,或者叫 “最少知识原则(Least Knowledge Principle,LKP)”,去尽可能发现模块中多余的依赖。 + +具体到细节,使用界面依赖与实现依赖到处置方式往往还是有所不同。 + +从使用界面依赖来说,我们接口定义更多考虑的往往是对参数的泛化与抽象,以便让我们可以适应更广泛的场景。 + +比如,我们前面谈到 IO 系统的时候,把存盘与读盘的接口从 *.os.File 换成 io.Reader、io.Writer,以获得更强的通用性,比如对剪贴板的支持。 + +类似的情况还有很多,一个接口的参数类型稍加变化,就会获得更大的通用性。再比如,对于上面 mockhttp.v1 中 rpc.Client 这个接口就存在多余的依赖,改为 http.Client 会更好一些。 + +不过有的时候,我们看起来从接口定义似乎更加泛化,但是实际上却是场景的收紧,这需要特别注意避免的。比如上面 mockhttp.v1 的接口: + +func Bind(host string, service interface{}) + + +与 mockhttp.v2 的接口: + +func ListenAndServe(host string, service http.Handler) + + +看似 v1 版本类型用的是 interface{},形式上更加泛化,但实际上 v1 版本有更强的假设,它内部通过反射机制实现了 HTTP 服务器请求的路由分派。而 v2 版本对 service 则用的是 HTTP 服务器的通用接口,是更加恰如其分的描述方式。 + +当然,在接口参数的抽象上,也不适合过度。如果某种泛化它不会发生,那就是过度设计。不要一开始就把系统设计得非常复杂,而陷入“过度设计”的深渊。应该让系统足够的简单,而却又不失扩展性,这其中的平衡完全依赖你对业务的理解,它是一个难点。 + +聊完使用界面依赖,我们接着聊实现依赖。 + +从模块实现的角度,我们环境依赖有两个选择:一个是直接依赖所基于的组件,一个是将所依赖的组件所有被引用的方法抽象成一个接口,让模块依赖接口而不是具体的组件。 + +那么,这两种方式应该怎么选择? + +我的建议是,大部分情况下应该选择直接依赖组件,而不必去抽象它。 + +如无必要,勿增实体。 + +如果我们大量抽象所依赖的基础组件,意味着我们系统的可配置性(Configurable)更好,但学习成本也更高。 + +什么时候该当考虑把依赖抽象化? + +其一,在需要提供多种选择的时候。比较典型的是日志的 Logger 组件。对于绝大部分的业务模块,都并不希望绑定 Logger 的选择,把决策权交给使用方。 + +但是有的时候,在这一点上过度设计也会比较常见。比如,不少业务模块会选择抽象对数据库的依赖,以便于在 MySQL 和 MongoDB 之间自由切换。但这种灵活性绝大部分情况下是一种过度设计。选择数据库应该是非常谨慎严谨的行为。 + +其二,在需要解除一个庞大的外部系统的依赖时。有时候我们并不是需要多个选择,而是某个外部依赖过重,我们测试或其他场景可能会选择 mock 一个外部依赖,以便降低测试系统的依赖。 + +其三,在依赖的外部系统为可选组件时。这个时候模块会实现一个 mock 的组件,并在初始化时将接口设置为 mock 组件。这样的好处是,除非用户关心,否则客户可以当模块不存在这个可选的配置项,这降低了学习门槛。 + +整体来说,对模块的实现依赖进行接口抽象,本质是对模块进行配置化,增加很多配置选项,这样的配置化需要谨慎,适可而止。 + +结语 + +接口设计是一个老生常谈的话题。接口有分模块的使用界面和模块的环境依赖这两种理解。 + +对于模块的使用界面,我们推崇 KISS 原则,简单自然,符合业务表达的惯例。 + +对于模块的环境依赖,我们遵循的是 “最小依赖原则”,或者叫 “最少知识原则(Least Knowledge Principle,LKP)”,尽可能发现模块中多余的依赖。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “不断完善的架构范式”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/64 不断完善的架构范式.md b/专栏/许式伟的架构课/64 不断完善的架构范式.md new file mode 100644 index 0000000..95bc2ce --- /dev/null +++ b/专栏/许式伟的架构课/64 不断完善的架构范式.md @@ -0,0 +1,136 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 64 不断完善的架构范式 + 64 | 不断完善的架构范式你好,我是七牛云许式伟。 + +我们在 “[62 | 重新认识开闭原则 (OCP)]” 这一讲中介绍了开闭原则。这篇内容非常非常重要,可以说是整个架构课的灵魂。 + +总结来说,开闭原则包含以下两层含义: + +第一,模块的业务要稳定。模块的业务遵循 “只读” 设计,如果需要变化不如把它归档,放弃掉。这种模块业务只读的思想,是架构治理的基础哲学。我平常和小伙伴们探讨模块边界的时候,经常会说这样一句话: + + +每一个模块都应该是可完成的。 + + +这实际上是开闭原则的业务范畴 “只读” 的架构治理思想的另一种表述方式。 + +第二,模块业务的变化点,简单一点的,通过回调函数或者接口开放出去,交给其他的业务模块。复杂一点的,通过引入插件机制把系统分解为 “最小化的核心系统+多个彼此正交的周边系统”。事实上回调函数或者接口本质上就是一种事件监听机制,所以它是插件机制的特例。 + +上一讲我们介绍了接口设计。到此为止,我们的架构思维篇也已经基本接近尾声。可能有人会越来越奇怪,为什么我还是没有去聊那些大家耳熟能详的架构设计原则? + +实际上,并不是这些架构设计原则不好,它们之中不乏精彩绝伦、振聋发聩的总结。比如: + + +接口隔离原则(Interface Segregation Principle,ISP):一个模块与另一个模块之间的依赖性,应该依赖于尽可能小的接口。 +依赖倒置原则(Dependence Inversion Principle,DIP):高层模块不应该依赖于低层模块,它们应该依赖于抽象接口。 +无环依赖原则(Acyclic Dependencies Principle,ADP):不要让两个模块之间出现循环依赖。怎么解除循环依赖?见上一条。 +组合/聚合复用原则(Composition/Aggregation Reuse Principle,CARP):当要扩展功能时,优先考虑使用组合,而不是继承。 +高内聚与低耦合(High Cohesion and Low Coupling,HCLC):模块内部需要做到内聚度高,模块之间需要做到耦合度低。这是判断一个模块是在做一个业务还是多个业务的依据。如果是在做同一个业务,那么它所有的代码都是内聚的,相互有较强的依赖。 +惯例优于配置(Convention over Configuration,COC):灵活性会增加复杂性,所以除非这个灵活性是必须的,否则应尽量让惯例来减少配置,这样才能提高开发效率。如有可能,尽量做到 “零配置”。 +命令查询分离(Command Query Separation,CQS):读写操作要分离。在定义接口方法时,要区分哪些是命令(写操作),哪些是查询(读操作),要将它们分离,而不要揉到一起。 +关注点分离(Separation of Concerns,SOC):将一个复杂的问题分离为多个简单的问题,然后逐个解决这些简单的问题,那么这个复杂的问题就解决了。当然这条说了等于没说,难在如何进行分离,最终还是归结到对业务的理解上。 + + +这些都是很好很好的。但是,我们需要意识到的一点是,熟读架构思维并不足以让我们成为优秀的架构师。 + +要始终记住的一点是,我们做的是软件工程。软件工程的复杂性它自然存在,不会因为好的架构思维而消除。 + +所以虽然理解架构思维是非常重要的,但是架构师真正的武器库并不是它们。 + +那么架构师的武器库是什么? + +这就要从 “架构治理” 开始谈起。 + +前面我们说过,“开闭原则” 推崇模块业务 “只读” 的思想,是很好的架构治理哲学。它告诉我们,软件是可以以 “搭积木” 的方式搭出来的。 + +核心的一点是,我们如何形成更多的 “积木”,即一个个业务只读、接口稳定、易于组合的模块。 + +所以,真正提高我们工程效率的,是我们的业务分解能力和历史积累的成果。 + +前面我们说过,架构分解中有两大难题:其一,需求的交织。不同需求混杂在一起,也就是存在所谓的全局性功能。其二,需求的易变。不同客户,不同场景下需求看起来很不一样,场景呈发散趋势。 + +在 “[61 | 全局性功能的架构设计]” 这一讲我们重点聊的是第一点。对于全局性功能怎么去拆解,把它从我们的业务中剥离出来,并无统一的解决思路。 + +但好的一点是,绝大部分全局性功能都会有很多人去拆解,并最终会被基础设施化。所以具体业务中我们会碰到的全局性功能并不会非常多。 + +比如,怎么做用户的鉴权?怎么保障软件 24 小时持续服务?怎么保障快速定位用户反馈的问题?这些需求和所有业务需求是交织在一起的,也足够普适,所以就会有很多人去思考对应的解决方案。 + +作为架构师,心性非常重要。 + +架构师需要有自己的信仰。我们需要坚持对业务进行正交分解的信念,要坚持不断地探索各类需求的架构分解方法。这样的思考多了,我们就逐步形成了各种各样的架构范式。 + +这些架构范式,并不仅仅是一些架构思维,而是 “一个个业务只读、接口稳定、易于组合的模块 + 组合的方法论”,它们才是架构师真正的武器库。 + +这个武器库包含哪些内容? + +首先,它应该包括信息科技形成的基础架构。努力把前辈们的心血,变成我们自己真正的积累。光会用还不够,以深刻理解它们背后的架构逻辑,确保自己与基础架构最大程度上的 “同频共振”。 + +只有让基础架构完全融入自己的思维体系,同频共振,我们才有可能在架构设计需要的时候 “想到它们”。这一点很有趣。有些人看起来博学多才,头头是道,但是真做架构时完全想不到他的 “博学”。 + +从体系结构来说,这个基础架构包含哪些内容? + +其一,基础平台。包括:冯·诺依曼体系、编程语言、操作系统。 + +其二,桌面开发平台。包括:窗口系统、GDI 系统、浏览器与小程序。当然我们也要理解桌面开发背后的架构逻辑,MVC 架构。 + +其三,服务端开发平台。包括:负载均衡、各类存储中间件。服务端业务开发的业务逻辑比桌面要简单得多。服务端难在如何形成有效的基础架构,其中大部分是存储中间件。 + +其四,服务治理平台。主要是以容器技术为核心的 DCOS(数据中心操作系统),以及围绕它形成的整个服务治理生态。这一块还在高速发展过程中,最终它将让服务端开发变得极其简单。 + +理解了这些基础架构,再加上你自己所处行业的领域知识,基本上设计出一个优秀业务系统,让它健康运行,持续不间断地向用户提供服务就不是问题。 + +读到这里,你可能终于明白,为什么这个架构课的内容结构是目前这个样子组织的。因为消化基础架构成为架构师自身的本领,远比消化架构设计原则,架构思维逻辑要难得多。 + +消化基础架构的过程,同时也是消化架构思维的过程。 + +把虚的事情往实里做,才有可能真正做好。 + +理解了基础架构,剩下的就是如何沉淀业务架构所需的武器库。这一般来说没有太统一的体系可以参考,如果有,大部分都会被基础设施化了。 + +所以,业务只能靠你自己的架构设计能力去构建它。而这,其实也是架构师的乐趣所在。 + +还没有被基础设施化但比较通用的,有一个大门类是数据相关的体系。数据是软件的灵魂。它可能包括以下这些内容: + + +存盘与读盘(IO); +文本处理; +存储与数据结构; +Undo/Redo; +…… + + +我们在下一讲,会专门聊聊其中的 “文本处理” 这个子课题。 + +从完整性讲,我们的架构课并没有包括所有的基础架构。我们把话题收敛到了 “如何把软件跑起来,并保证它持续健康运行” 这件事情上。 + +但从企业的业务运营角度来说,这还远不是全部。“[54 | 业务的可支持性与持续运营]” 我们稍稍展开了一下这个话题。但要谈透这个话题,它会是另一本书,内容主题将会是 “数据治理与业务运营体系构建”。 + +我希望有一天能够完成它,但这可能要很久之后的事情了。它是我除架构课外的另一个心愿。 + +结语 + +我们在 “[56 | 服务治理篇:回顾与总结]” 这一讲,也就是第四章结束的时候,谈到我们下一章的内容时提到: + + +我个人不太喜欢常规意义上的 “设计模式”。或者说,我们对设计模式常规的打开方式是有问题的。理解每一个设计模式,都应该放到它想要解决的问题域来看。所以,我个人更喜欢的架构范式更多的是 “设计场景” 的总结。“设计场景” 和设计模式的区别在于它有自己清晰的问题域定义,是一个实实在在的通用子系统。 + +是的,这些 “通用的设计场景”,才是架构师真正的武器库。如果我们架构师总能把自己所要解决的业务场景分解为多个 “通用的设计场景” 的组合,这就代表架构师有了极强的架构范式的抽象能力。而这一点,正是架构师成熟度的核心标志。 + + +结合今天这一讲我们聊的内容,相信你对这段话会有新的理解。 + +“开闭原则” 推崇模块业务 “只读” 的思想,是很好的架构治理哲学。它告诉我们,软件是可以以 “搭积木” 的方式搭出来的。核心的一点是,我们如何形成更多的 “积木”,即一个个业务只读、接口稳定、易于组合的模块。 + +结合今天这一讲的内容,相信你终于完全能理解我们这个架构课的内容组织为什么是现在你看到的样子了。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “架构范式:文本处理”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/65 架构范式:文本处理.md b/专栏/许式伟的架构课/65 架构范式:文本处理.md new file mode 100644 index 0000000..d50f37a --- /dev/null +++ b/专栏/许式伟的架构课/65 架构范式:文本处理.md @@ -0,0 +1,472 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 65 架构范式:文本处理 + 65 | 架构范式:文本处理你好,我是七牛云许式伟。 + +上一讲 “[64 | 不断完善的架构范式]” 我们提到架构师的武器库是不断完善的架构范式。今天我们围绕一个具体的问题域,看看我们日常能够积累什么样的经验和成果,来完善作为一个架构师的知识体系。 + +我们选择的问题是 “文本处理”。 + +计算机之所以叫计算机,是因为计算机的能力基本上就是“计算+I/O”两部分。I/O 只是为了让计算机与物理世界打交道,它也是为计算服务的。所以数据是软件的灵魂,数据处理是软件的能力。 + +今天我们聊的文本处理,不是通用的数据处理能力,而是收敛在数据的 I/O 上。这里说的文本,是指写入到磁盘的非结构化数据。它可能真的是文本内容,比如 HTML 文档、CSS 文档;也可能是二进制内容,比如 Word 文档、Excel 文档。文本处理则是指对这类非结构化数据的处理过程,常见文本处理的需求场景有: + + +数据验证(Data Validation)。比如判断用户输入的文本是否合法,值的范围是否符合期望。 +数据抽取(Data Extraction)。比如从某 HTML 页面中抽取出结构化的机票信息(什么时间,从哪里出发,到哪里去,价格几何等等)。 +编译器(Compiler)。特殊地,在文本格式是某种语言的代码时,我们可以将文本编译成可执行的机器码,或虚拟机解释执行的字节码。当然我们也可以边解释文本的语义边执行。 +…… + + +从用户需求的角度来说,文本处理的需求场景是不可穷尽的。网络爬虫与搜索引擎需要文本处理,Office 软件需要文本处理,编程语言的编译器需要文本处理,网络协议解析需要文本处理,等等。 + +那么,怎么才能从这些多变的需求场景中,抽出正交分解后可复用的架构范式? + +我们今天聊聊文本处理的通用思路。 + +我的文本处理技术栈演进 + +文本处理,很多人都会遇到,只不过大家各自遇到的场景不同。我这里先回顾下我个人遇到的文本处理场景。我总结了一个图,如下: + + + +在 2000 年初,我作为实习生拿到的第一个任务,是金山电子表格自身的文件格式设计和 Excel 文件的读写。此后,我参与了多个版本的 Word 文件读写工作。为了便于分析 Excel 和 Word 文件的格式,我实现了 ExcelViewer 和 DocViewer 这两个文件格式查看器。 + +实际上这两个 Viewer 非常重要,因为它第一次让文件格式的理解过程用程序固化了下来。这非常利于知识传承。大家可以设想一下,假如没有 Viewer,那么后面接手的人基本上只能靠阅读 ExcelReader 和 DocReader 模块的代码来理解文件格式。 + +但是这有两个问题。其一,Reader 模块有大量的业务逻辑,对我们理解 Excel 和 Word 文件格式本身会造成干扰。其二,Reader 模块增加功能会比较慢,对于那些我们本身不支持的功能,或者我们还暂时来不及兼容的功能,是没有对应的解析代码的。 + +但是 Viewer 就不一样。我们会尽可能地把我们对 Excel 和 Word 的理解记录下来,成为稳定可传承的知识,而无需关心是否已经支持该功能。另外,从时间的维度来说,应该先有 Viewer,在理解了文件格式之后,再设计出 Reader 才比较合理。 + +这个时期的 ExcelViewer 和 DocViewer,它主要抽象出来的是界面呈现部分。具体 ExcelViewer 和 DocViewer 的代码不需要有一行涉及到界面。这有诸多好处。实际上可视化界面只是 ExcelViewer 和 DocViewer 的一种输出终端,它们同时也生成了一个纯文本结果到磁盘文件中。这有助于我们用常规的 diff 工具来对比两个文件的差异,从而加速我们对未知数据格式的了解。 + +但,此时的 ExcelViewer 和 DocViewer 并没有将文件格式的处理过程抽象出通用的模块。也可以说,还没有抽象出文本处理范式。 + +这个时期同期还有一个探索性的 WPS for Linux 项目。为了支持跨平台编译,我实现了一个简单的 mk 程序。这个程序区别于 Linux 标准化的 make 程序,没有那么复杂的逻辑需要理解。它的输入是一个类 Windows 平台的 ini 文件,里面只需要指定选择的编译器、相关的编译选项、源代码文件列表等,就可以进行编译。甚至源代码列表可以直接指定为从 Visual C++ 的项目配置 dsp 文件中抽取,极易使用。 + +这个 mk 程序除了要解析一个类 ini 的配置文件外,也会解析 C/C++ 源代码文件形成源代码文件的依赖表,以更好地支持增量编译。不只是源代码文件本身的修改会触发重新编译,任何依赖文件的修改也会触发重新编译。 + +同样地,这个时期的 mk 程序同样没有引入任何通用的文本处理范式。 + +此后大约在 2004 年,我开始在金山办公软件内部推 KSDN。KSDN 这个名字承自 MSDN,我们希望打造一个全局的文档系统,它自动从项目的源代码中提取并生成。每天日构建完毕后得到最新版本的 KSDN。 + +KSDN 处理的输入主要是 C++ 和 Delphi 源代码文件(当时的界面是 Delphi 写的),是纯文本的。这和 ExcelViewer、DocViewer 不同,他们的输入是二进制文件。 + +KSDN 第一次引入了一个通用的脚本,来表达我们想从源代码中抽取什么内容。整个 KSDN 处理单个源代码文件的工作原理可以描述为: + + +通过文件后缀选择源代码文件的解析脚本,通过该脚本解析 C++ 或 Dephi 的源代码,并输出 XML 格式的文件; +通过 XSLT 脚本,将 XML 文件渲染为一个或多个 HTML 文件。XSLT 全称是 Extensible Stylesheet Language Transformations(可扩展样式表转换),是 XML 生态中的一项技术。 + + +在 2006 年的时候,我决定实现 KSDN 2.0 版本。这个版本主要想解决第一个版本的脚本语法表达能力比较局限的问题。 + +于是 C++ 版本的 TPL(Text Processing Language)诞生了。它非常类似于 Boost Spirit,但功能要强大很多。它的项目主页为: + + +https://github.com/xushiwei/tpl + + +它依赖基础库 stdext,项目主页为: + + +https://github.com/xushiwei/stdext + + +C++ 版本的 TPL 支持的表达能力,已经完全不弱于 UNIX 经典的 LEX + YACC 组合,使用上却轻量很多。KSDN 2.0 的工作原理变成了: + + +基于 TPL 将 C++ 或 Delphi 文件转为 json 格式; +与 XSLT 类似地,我们引入了 JSPT,即以 json 为输入,PHP 为 formatter,将内容转为一个或多个 HTML 文件。 + + +这个过程非常通用,可以用于实现任意文件格式之间的变换。包括我们前面的 mk 程序,它本质上也是类 ini 文件格式变换到 Makefile 的过程,我们基于 TPL 很轻松就改造了一个 mk 2.0 版本。 + +2009 年的时候,我们基于 C++ 实现一个名为 CERL 的网络库,它和 Go 语言的 goroutine 类似,也是基于协程来实现高并发。在这个网络库中,我们定义了一个名为 SDL(Server Description Language)的语言来描述服务器的网络协议。很自然地,我们基于 TPL + JSPT 来实现了 SDL 文件的解析过程。 + +2011 年,七牛云成立,我们选择了 Go 语言作为技术栈的核心。在转 Go 语言后,除了 TPL,我个人沉淀的大部分 C++ 基础库都不再需要,因为它们往往已经被 Go 语言的标准库解决得很好。 + +在 2015 年的时候,出于某种原因我实现了一个网络爬虫,这个爬虫会在收到网页内容后,抽取网页中的结构化信息并存储起来。这个抽取信息的过程,最终导致 Go 语言版本 TPL 的诞生。它的项目主页为: + + +https://github.com/qiniu/text + + +为了验证 Go 语言版本 TPL 的有效性,我在实现了经典的 “计算器(Calculator)” 之余,顺手实现了一门语言,这就是 qlang。它的项目主页为: + + +https://github.com/qiniu/qlang + + +由于 Go 语言中实现泛型数据结构的需要,我给 qlang 实现了一个 embedded 版本,简称 eql。它是类似 erubis/erb 的东西。结合 go generate,它可以很方便地让 Go 支持模板(不是 html template,是指泛型)。 + +在 2017 年,出于 rtmp 网络协议理解的需要,我创建了 BPL(Binary Processing Language),它的项目主页为: + + +https://github.com/qiniu/bpl + + +区别于 TPL 的是,BPL 主要用于处理二进制文档。前面我们谈到 ExcelViewer 和 DocViewer 时说过,我们并没有建立任何通用的架构范式。这一直是我引以为憾的事情,所以 2006 年 C++ 版本的 TPL 诞生后就有过 BPL 相关的尝试。这里是尝试残留的痕迹: + + +tpl/binary/* + + +但是二进制文档的确很难,它的格式描述中通常有一定的条件判断逻辑,所以 BPL 背后需要依赖一门语言。在 qlang 诞生后,这个条件就得到了满足,这是最终 BPL 得以能够诞生的原因。 + +BPL 非常强大,它可以处理任意的二进制文件,也可以用于处理任意的 TCP 网络协议数据流。有了 BPL,我们最初的 ExcelViewer 和 DocViewer 可以轻松得以实现。关于 BPL 更详细的介绍,请参阅 https://github.com/qiniu/bpl 中的文档说明。 + +文本内容的处理范式 + +介绍了我个人文本处理的技术栈演进过程后,我们把话题重新回到架构范式。 + +首先,让我们把焦点放在文本内容的处理上。 + +文本内容的处理,有非常标准的方式。它通常分词法分析(Lex)和语法分析(Parser)两个阶段。UNIX 系的操作系统还提供了 lex 和 yacc 两个经典的程序来协助我们做文本文件的分析处理。 + +词法分析(Lex)通常由一个 Scanner 来完成,它负责将文本内容从字节流(Byte Stream)转为 Token 流(Token Stream)。我们以解析 Go 源代码的 Scanner 为例(参见 https://godoc.org/go/scanner),其 Scan 函数的原型如下: + +type Scanner struct { + Scan() (pos token.Pos, tok token.Token, lit string) + ... +} + + +其使用范式如下: + +import ( + "go/scanner" + "go/token" +) + +func doScan(s *scanner.Scanner) { + for { + pos, tok, lit := s.Scan() + if tok == token.EOF { + break + } + ... + // pos 是这个 token 的位置 + // tok 是这个 token 的类型,见 https://godoc.org/go/token + // lit 是这个 token 的文本内容 + } +} + + +Scanner 有时候也叫 Tokenizer。例如 Go 语言中 HTML 的 Tokenizer 类(参阅 https://godoc.org/golang.org/x/net/html)的原型如下: + +type Token struct { + Type TokenType + DataAtom atom.Atom + Data string + Attr []Attribute +} + +type Tokenizer struct { + Next() TokenType + Err() error + Token() Token + ... +} + + +其使用范式如下: + +import ( + "golang.org/x/net/html" +) + +func doScan(z *html.Tokenizer) error { + for { + if z.Next() == html.ErrorToken { + // Returning io.EOF indicates success. + return z.Err() + } + token := z.Token() + ... + } +} + + +词法分析(Lex)过程非常基础,大部分情况下我们不会直接和它打交道。我们打交道的基本都是语法分析器,通常叫 Parser。而从Parser 的使用方式来说,分为 SAX 和 DOM 两种模型。SAX 模型基于事件机制,DOM 模型则基于结构化的数据访问接口。 + +前面我们已经多次分析过 SAX 与 DOM 的优劣,这里不再展开。通常来说,我们会倾向于采用 DOM 模型。这里我们还是以 Go 文法和 HTML 文法的解析为例。 + +先看 Go 文法的 Parser(参阅 https://godoc.org/go/parser),它的原型如下: + +func ParseExpr(x string) (ast.Expr, error) + +func ParseFile( + fset *token.FileSet, + filename string, src interface{}, + mode Mode) (f *ast.File, err error) + + +这里看起来有点复杂的是 ParseFile,它输入的字节流(Byte Stream)可以是: + + +scr != nil,且为 io.Reader 类型; +src != nil,且为 string 或 []byte 类型; +src == nil,filename 非空,字节流从 filename 对应的文件中读取。 + + +而 Parser 的输出则统一是一个抽象语法树(Abstract Syntax Tree,AST)。显然,它基于的是 DOM 模型。 + +我们再看 HTML 文法的 Parser(参阅 https://godoc.org/golang.org/x/net/html),它的原型如下: + +func Parse(r io.Reader) (*Node, error) + + +超级简单的基于 DOM 模型的使用接口,任何解释都是多余的。 + +那么,我前面提的 TPL(Text Processing Language)是做什么的呢?它实现了一套通用的 Scanner + Parser 的机制。首先是词法分析,也就是 Scanner,它负责将文本流转换为 Token 序列。简单来说,就是一个从 text []byte 到 tokens []Token 的过程。 + +尽管世上语言多样,但是词法非常接近,所以在词法分析这块 ,TPL 抽象了一个 Tokenizer 接口,方便用户自定义。TPL 也内置了一个与 Go 语言词法类似的 Scanner,只是做了非常细微的调整,增加了 ?、~、@ 等操作符。 + +TPL 的 Parser 通过类 EBNF 文法表达。比如一个浮点运算的计算器(Calculator),支持加减乘除、函数调用、常量(如 pi 等)的类 EBNF 文法如下: + +term = factor *('*' factor/mul | '/' factor/quo | '%' factor/mod) + +doc = term *('+' term/add | '-' term/sub) + +factor = + FLOAT/push | + '-' factor/neg | + '(' doc ')' | + (IDENT '(' doc %= ','/ARITY ')')/call | + IDENT/ident | + '+' factor + + +关于这个类 EBNF 文法,有以下补充说明: + + +我们用 *G 和 +G 来表示重复,而不是用 {G}。要记住这条规则其实比较简单。在编译原理的图书中,我们看到往往是 G* 和 G+。但语言文法中除了 ++ 和 – 运算符,很少是后缀形式,所以我们选择改为前缀。 +我们用 ?G 来表示可选,而不是用 [G]。同上,只要能够回忆起编译原理中我们用 G? 表示可选,我们就很容易理解这里为什么可选是用 ?G 表示。 +我们直接用 G1 G2 来表示串接,而不是 G1, G2。 +我们用 G1 % G2 和 G1 %= G2 表示 G1 G2 G1 G2 … G1 这样的列表。其中 G1 % G2 和 G1 %= G2 的区别是前者不能为空列表,后者可以。在上面的例子中,我们用 doc %= , 表示函数的参数列表。 +我们用 G/action 表示在 G 匹配成功后执行 action 这个动作。action 最终是调用到 Go 语言中的回调函数。在上面这个计算器中大量使用了 G/action 文法。 + + +与 UNIX 实用程序 yacc 不同的是,TPL 中文法描述的脚本,与执行代码尽可能分离,以加业务语义的可读性。 + +从模型的归属来说,TPL 属于 SAX 模型。但 G/action 不一定真的是动作。在 extractor 模式下,G/action 被视为 G/marker,TPL 变成 DOM 模型。也就是说,此时 action 只是一个标记,用于形成输出的 DOM 树。 + +关于 TPL 更详细的介绍需要很长的篇幅,你可以参考 TPL Doc。 + +在文本内容处理的技术栈中,还有一个分支是正则表达式(Regular Expression)。在简单场景下,正则表达式是比较方便的,但是它的缺点也比较明显,可伸缩性和可读性都不强。 + +二进制内容的处理范式 + +接下来我们讨论二进制内容的通用处理范式。 + +二进制内容的处理过程整体来说,似乎比较 “容易”。如果要说出一点问题的话,那就是 “有点繁琐”。 + +还记得序列化机制吧?它基本上算得上二进制内容的 I/O 框架了。它看起来是这样的: + +type Foo struct { + A uint32 + B string + C float64 + D Bar +} + +func readFoo(foo *Foo, ar *Archive) { + readUint32(&foo.A, ar) + readString(&foo.B, ar) + readFloat64(&foo.C, ar) + readBar(&foo.D, ar) +} + + +在 C++ 的操作符重载的支持下,这段代码看起来会更简洁一些: + +Archive& operator>>(Archive& ar, Foo& foo) { + ar >> foo.A >> foo.B >> foo.C >> foo.D; + return ar; +} + + +当然,上面只是最基础的情形,所以看起来还比较简洁。但在考虑可选、重复、数组等场景,实际上并不会那么简单。比如对于数组,理想情况下代码是下面这样的: + +type Foo struct { + N uint16 + Bars []Bar // [N]Bar +} + +func readFoo(foo *Foo, ar *Archive) { + readUint16(&foo.N, ar) + readArray(&foo.Bars, int(foo.N), ar) +} + + +对于 Go 语言来说,这里我们想要的 readArray 并不存在。而在 C++ 则可以通过泛型来做到,我们示意如下: + +template +void readArray(T[]& v, int n, Archive& ar) { + v = new T[n]; + for (int i = 0; i < n; i++) { + ar >> T[i]; + } +} + + +呼唤一下 Go 语言的泛型吧。不过泛型大概率需要破坏 Go 的一些基础假设,比如不支持重载。所以 Go 的泛型之路不会那么容易。 + +回到序列化机制。常规意义的序列化,通常还提供了 Object 动态序列化与反序列化的能力。但是实际上这个机制属于过度设计。 + +为什么这么说? + +因为 Object 动态序列化的确带来了一定的便捷性,但是这个便捷性的背后是让使用者放弃了对磁盘文件格式设计的思考。这是非常不正确的指导思想。 + +数据是软件的灵魂,文件是软件最重要的资产。 + + +文件 I/O 的序列化机制,最重要的是定义严谨的数据格式,而非提供任何出于便捷性考虑的智能。 + + +所以我们只需要保留序列化的形式就好了,任何额外的 “智能” 都是多余的。 + +基于这样的基本原则,稍作探究你就会发现,在数据结构清晰的情况下,其实整个序列化的代码是非常平庸的。假如我们参考 TPL 的类 EBNF 文法,定义以下这样一条规则: + +Foo = { + N uint16 + Bars [N]Bar +} + + +这样,我们就可以自动帮这里的 Foo 类型实现它的序列化代码了。 + +而这正是 BPL 诞生的灵感来源。 + +BPL 设计的核心思想是,不破坏 TPL 的 EBNF 文法的任何语义,把自己作为 TPL 的扩展。这就好比,如果我们把 TPL 看作 C 的话,BPL 就是 C++。所有 TPL 的功能,BPL 都应该具备而且行为一致。 + +我们以 MongoDB 的网络协议为例,看看 BPL 文法是什么样的: + +document = bson + +MsgHeader = {/C + int32 messageLength; // total message size, including this + int32 requestID; // identifier for this message + int32 responseTo; // requestID from the original request (used in responses from db) + int32 opCode; // request type - see table below +} + +OP_UPDATE = {/C + int32 ZERO; // 0 - reserved for future use + cstring fullCollectionName; // "dbname.collectionname" + int32 flags; // bit vector. see below + document selector; // the query to select the document + document update; // specification of the update to perform +} + +OP_INSERT = {/C + int32 flags; // bit vector - see below + cstring fullCollectionName; // "dbname.collectionname" + document* documents; // one or more documents to insert into the collection +} + +OP_QUERY = {/C + int32 flags; // bit vector of query options. See below for details. + cstring fullCollectionName; // "dbname.collectionname" + int32 numberToSkip; // number of documents to skip + int32 numberToReturn; // number of documents to return + // in the first OP_REPLY batch + document query; // query object. See below for details. + document? returnFieldsSelector; // Optional. Selector indicating the fields + // to return. See below for details. +} + +OP_GET_MORE = {/C + int32 ZERO; // 0 - reserved for future use + cstring fullCollectionName; // "dbname.collectionname" + int32 numberToReturn; // number of documents to return + int64 cursorID; // cursorID from the OP_REPLY +} + +OP_DELETE = {/C + int32 ZERO; // 0 - reserved for future use + cstring fullCollectionName; // "dbname.collectionname" + int32 flags; // bit vector - see below for details. + document selector; // query object. See below for details. +} + +OP_KILL_CURSORS = {/C + int32 ZERO; // 0 - reserved for future use + int32 numberOfCursorIDs; // number of cursorIDs in message + int64* cursorIDs; // sequence of cursorIDs to close +} + +OP_MSG = {/C + cstring message; // message for the database +} + +OP_REPLY = {/C + int32 responseFlags; // bit vector - see details below + int64 cursorID; // cursor id if client needs to do get more's + int32 startingFrom; // where in the cursor this reply is starting + int32 numberReturned; // number of documents in the reply + document* documents; // documents +} + +OP_REQ = {/C + cstring dbName; + cstring cmd; + document param; +} + +OP_RET = {/C + document ret; +} + +Message = { + header MsgHeader // standard message header + let bodyLen = header.messageLength - sizeof(MsgHeader) + read bodyLen do case header.opCode { + 1: OP_REPLY // Reply to a client request. responseTo is set. + 1000: OP_MSG // Generic msg command followed by a string. + 2001: OP_UPDATE + 2002: OP_INSERT + 2004: OP_QUERY + 2005: OP_GET_MORE // Get more data from a query. See Cursors. + 2006: OP_DELETE + 2007: OP_KILL_CURSORS // Notify database that the client has finished with the cursor. + 2010: OP_REQ + 2011: OP_RET + default: { + body [bodyLen]byte + } + } +} + +doc = *Message + + +我们对比 MongoDB 官方的协议文档(参考 https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/),你会发现很有趣的一点是,我们 BPL 文法几乎和 MongoDB 官方采用的伪代码完全一致,除了一个小细节:在 BPL 中,我们用 {…} 表示采用 Go 语言结构体的文法,而 {/C … } 表示采用 C 语言结构体的文法。 + +当前 BPL 还只支持解释执行,但这只是暂时的。就像在 TPL 中我们除了动态解释执行外,也已经提供 tpl generator 来生成 Go 代码以静态编译执行。 + +要进一步了解 BPL 的功能,请参阅 https://github.com/qiniu/bpl。我们也还提供了不少具体 BPL 的样例,详细可参考: + + +https://github.com/qiniu/bpl/tree/master/formats + + +结语 + +文本处理是一个非常庞大的课题,本文详细解剖了我个人在这个领域下的经验总结。相信这些经验对你面对相关场景时会有帮助。 + +但是更重要的一点是,我们平常需要有意识去分析我们工作中遇到的业务场景,从中提炼通用的需求场景形成架构范式的积累。 + +如此,架构的正交分解思想方能得到贯彻。而我们的业务迭代,也就越来越容易。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “架构老化与重构”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/66 架构老化与重构.md b/专栏/许式伟的架构课/66 架构老化与重构.md new file mode 100644 index 0000000..0018c7f --- /dev/null +++ b/专栏/许式伟的架构课/66 架构老化与重构.md @@ -0,0 +1,173 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 66 架构老化与重构 + 66 | 架构老化与重构你好,我是七牛云许式伟。 + +在 “[64 | 不断完善的架构范式]” 这一讲中,我们强调了架构师在日常工作过程中不断积累和完善架构范式的重要性。而上一讲 “65 | 架构范式:文本处理” 则以我个人经历为例,介绍了文本处理领域的通用架构范式。 + +架构的老化 + +架构的功夫全在平常。 + +无论是在我们架构范式的不断完善上,还是应对架构老化的经验积累上,都是在日常工作过程中见功夫。我们不能指望有一天架构水平会突飞猛进。架构能力提升全靠平常一点一滴地不断反思与打磨得来。 + +今天我们要聊的话题是架构老化与重构。 + +架构老化源于什么? + +在我们不断给系统添加各种新功能的时候,往往会遇到功能需求的实现方式不在当初框架设定的范围之内,于是很多功能代码逸出框架的范围之外。 + +这些散落在各处的代码,把系统绞得支离破碎。久而久之,代码就出现老化,散发出臭味。 + +代码老化的标志,是添加功能越来越难,迭代效率降低,问题却是持续不断,解决了一个问题却又由此生出好几个新问题。 + +在理想的情况下,如果我们坚持以 “最小化的核心系统 + 多个相互正交的周边系统” 这个指导思想来构建应用,那么代码就很难出现老化。 + +当然,这毕竟是理想情况。现实情况下,有很多原因会导致架构老化难以避免,比如: + + +软件工程师的技术能力不行,以功能完成为先,不考虑项目的长期维护成本; +公司缺乏架构评审环节,系统的代码质量缺乏持续有效的关注; +需求理解不深刻,最初架构设计无法满足迭代发展的需要; +架构迭代不及时,大量因为赶时间而诞生的补丁式代码; +…… + + +那么,怎么应对架构老化? + +这个问题可以从两个视角来看: + + +该怎么重构系统,才能让我们的软件重新恢复活力? +在重构系统之前,我们应该如何进行局部改善,如果增加新功能又应该如何考虑? + + +我们先聊后者,毕竟重构系统听起来是一件系统性的工程。而添加新功能与局部调整则在日常经常发生。 + +老系统怎么添加新功能 + +先说说添加新功能。 + +正常来说,我们添加功能的时候,尤其是自己加入项目组比较晚,已经有大量的历史代码沉淀在那里的时候,通常我们应该把自己要添加的功能定位为周边功能。对于周边功能,往往考虑最多的点是如何少给核心系统添加麻烦,能够少改就少改。 + +但是,这其实还不够。实际上当我们视角放在周边系统的时候,其实它本身也应该被看作独立业务系统。这样看的时候,我们自然而然会有新的要求:如何让新功能的代码与既有系统解耦,能够不依赖尽量不依赖。 + +这个不依赖是有讲究的。 + +不依赖核心的含义是业务不依赖。新功能的绝大部分代码独立于既有业务系统,只有少量桥接的代码是耦合的。 + +实际上对于任何被正交分解的周边系统 B 与核心系统 A,理想情况我们最终得到的应该是三个模块:A、B(与 A 无关部分)、A 与 B 桥接代码(与 A 相关的部分)。虽然从归属来说,A 与 B 桥接代码我们通常也会放到 B 模块,但是它应该尽可能小,且尽可能独立于与核心系统无关的代码。 + +理解这一点至关重要。只有这样我们才能保护自己的投资,今天开发新功能的投入产出可以最大程度得以保留。未来,万一需要做重构,我们的重构成本也能够尽可能最小化。 + +不依赖的另一个重要话题是要不要依赖公司内部的基础库。这一点需要辩证来看,不能简单回答依赖或不依赖。完全不依赖意味着放弃生产力。 + +这里基本的判断标准是,成熟度越高的基础库越值得依赖。成熟度的评估依赖于个人经验,首先应该评估的是模块规格的成熟度,因为实现上的问题让时间来解决就行。模块规格是否符合你的预期,以及经过了多少用户使用的打磨,这些是评估成熟度的依据。 + +还是以我做办公软件时期的经历为例。从重构角度来说它很典型,既有的代码有几百万行。我第一个做读盘与存盘之外的新功能是电子表格的智能填充。这个功能比较常用,用户可以选择一个区域,然后移动鼠标到被选区域右下角,在鼠标变成十字时,按下鼠标左键不放并移动鼠标以进行单元格内容的自动填充。填充方向是上下左右都可以。 + +我怎么做这个功能?首先是实现一个基本纯算法的模块,输入一个值矩阵(可以是数值、日期,也可以是字符串等),要预测的序列个数,输出对应预测的值矩阵。为什么自动填充的方向在算法这里消失了?因为我们按填充方向构建值矩阵,而不是用户屏幕上直观看到的矩阵。 + +然后抽象了核心系统的两个接口,一个是取一个区域的单元格数据,包括值和格式,一个是设置一个单元格的值和格式。基于这个抽象接口,我们实现了完整的自动填充逻辑。 + +最后,是对接这个自动填充模块与既有的业务系统。从 Model 层来说,只需要在既有的业务系统包装对应要求的接口即可。而且取区域单元格、设置一个单元格的值,这些是非常通用的接口,无论既有系统长什么样,我们都可以轻松去实现所需接口。 + +这就是做新功能的思路,尽可能与既有系统剥离,从独立业务视角去实现业务,抽象对环境的依赖。最后,用最少量的对接代码把整个系统串起来。 + +架构的局部优化 + +聊完添加新功能,我们谈谈局部调整。它的目标是优化某个功能与核心系统的耦合关系。 + +局部调整看似收效甚微,但是它的好处是可以快速推动。而且,日拱一卒,如果我们能够坚持下来,最后的效果远比你想象得好。 + +它有两种常见做法。 + +一种是重写,或者叫局部重构。它相当于从系统中彻底移除掉与该功能相关的代码,重新写一份新的。这和开发一个新功能没什么两样,最多看看被移除的代码里面,有哪些函数设计比较合理,可以直接拿过来用,或者稍微重新包装一下能够让规格更合理的。 + +但是我们不能太热衷于做局部重构。局部重构一定要发生在你对这块代码的业务比较了解的情形,比如你已经维护过它一阵子了。 + +另外,局部重构一定要把老代码清理干净,不要残留一些不必要的代码在系统里面。剩下来的事情,完全可以参考我上面提的实现新功能的方法论来执行。 + +另一种是依赖优化。它关注的重心不是某项功能本身的实现,而是它与系统之间的关系。 + +依赖优化整体上做的是代码的搬运工。怎么搬代码?和删除代码类似,我们要找到和该功能相关的所有代码。但是我们做的不是删除,而是将散落在系统中的代码集中起来。我们把对系统的每处修改变成一个函数,比如叫 doXXX_yyyy。这里 XXX 是功能代号,yyyy 则依据这段搬走的代码语义命个名。 + +你可能觉得这个名字太丑了。但是某种程度来说这是故意的。它可以作为团队的约定俗成,代表此处待重新考虑边界。 + +不要理解错了,它不是说我们需要重新思考我们现在正在做代码优化的功能边界。它是说我们要重新考虑核心系统的边界。尤其是如果某个地方有好几个功能都加了 doXXX_yyyy 这样的调用,这就意味着这里需要提供一个事件机制,以便这些功能能够进行监听。而一旦我们做了这件事,你就发现核心系统变得更稳定了,不再需要因为添加功能而修改代码。而这不正是 “开闭原则(OCP)” 所追求的么? + +回到我们要进行依赖优化的功能。集中了这个功能所有代码后,这个功能与系统的耦合也就清楚了。有多少个 doXXX_yyyy,就有多少对系统的伤害(参阅 “[58 | 如何判断架构设计的优劣?]” 中的伤害值计算)。 + +如果伤害值不大,代表耦合在合理范围,做到这一步暂时不再往下走是可接受的。如果耦合过多,那就意味着我们需要站在这个功能本身的业务视角看依赖的合理性了。如果不合理,可以考虑推动局部重构。 + +所以,局部重构不应该很盲目,而应依赖于基于 “伤害值” 的客观判断。习惯于在不理解的情况下就重构,这实在不太好。认同他人是很重要的能力修炼。况且作为架构师,事情优先级的排列是第一位的,有太多重要的事情值得去做。 + +依赖优化的好处比较明显。其一,工作量小,做的是代码搬运,不改变任何业务逻辑。其二,可以不必深入功能的细节,只需要找到该功能的所有相关代码,这是难点,然后把它们集中起来。 + +尽可能把我们认为非核心系统的功能,都基于依赖优化的方式独立出去。这样核心系统与周边系统的耦合就理清楚了。 + +依赖优化,可以把周边系统对核心系统的代码注入,整理得清清楚楚。这是事件机制的需求来源。 + +依赖优化也能够及时发现糟糕的模块,和核心系统藕断丝连,斩不断理还乱,这时我们就需要对这个功能进行局部重构。 + +核心系统的重构 + +完成这些,我们下一步,就要进入重构的关键阶段,进行核心系统重构。 + +对于一个积弊已久的系统,要想成功完成整体的重构是非常艰难的。 + +如果我们一上来就去重构核心系统,风险太高。一方面,牵一发而动全身,我们无法保证工程的交付周期。另一方面,没有谁对全局有足够的了解,重构会过于盲目,项目的执行风险难以把控。 + +确定要对核心系统进行重构,那么最高优先级是确定它的边界,也就是使用界面(接口)。 + +能够在不修改实现的情况下调整核心系统的使用界面到我们期望的样子是最好的。 + +周边系统对核心系统的依赖无非两类:一是核心系统的功能,表现为它提供的 DOM 接口;二是核心系统提供的事件,让周边系统能够介入它的业务流程。 + +对所有周边模块进行依赖优化的整理,细加分析后可以初步确定核心系统需要暴露的事件集合。 + +进一步要做的事情是把核心系统的 DOM 接口也抽象出来。这一步比较复杂。它包含两件事情: + + +让周边系统对它的依赖,变成依赖接口,而非依赖实现; +审视核心系统功能的 DOM 接口的合理性,明确出我们期望的接口设计。 + + +我们可以分步骤做。可以先做实现依赖到接口依赖的转变。这有点像前面依赖优化的工作。只不过它不是搬代码,而是把周边模块独立出去,将它与核心系统的依赖关系全部调整为接口。这样,不管抽离出来的 DOM 接口是否合理,至少它代表了当前系统的模块边界。 + +这一步做完,理论上 mock 一个核心系统出来和周边系统对接也是可行的。只不过可能这个 DOM 模型太大,要 mock 不那么容易。 + +接下来,就是最重要的时刻。 + +我们需要对核心系统的接口进行重新设计。这一步的难点在于: + +第一,我们对业务的理解的确有了长足的进步。我们抽象的业务接口有了更加精炼符合业务本质的表达方式,而不是换汤不换药,否则我们就需要质疑这次重构的必要性。 + +第二,对周边系统切换到新接口的成本有充足的预计。对周边系统来说,这是从老接口过度到新接口的过程。虽然理论上让核心系统维护两套 DOM 接口同时存在,在技术上是可行的,但是这个过渡期不能太长,否则容易让人困惑,不清楚我们倡导的是什么。 + +完成了接口改造,剩下来就简单了。核心系统,每一个周边系统,彼此完全独立,可以单独调整和优化。嫌当前的核心系统太糟糕?那就搞搞。为什么可以这么轻松决策?因为就算我们要重新写核心系统,要做的事情也很收敛,不会影响到大局。 + +这不像那些系统边界分解得不清不楚的业务系统。要改核心系统的代码? + +不要命了么? + +结语 + +重构工作是很有技巧性的,很能培养一个人的架构能力。做多了,我们可以建立对代码耦合的条件反射,看一眼就知道架构是否合理。 + +但重构不是技巧性那么简单。 + +实际上从难度来说,重构比一个全新业务的架构过程要难得多。重构,不只是一个架构的合理性问题。它包含了架构合理性的考量,因为我们需要知道未来在哪里,我们迭代方向在哪里。 + +但重构的挑战远不只是这些。这是一个集架构设计(未来架构应该是什么样的)、资源规划与调度(与新功能开发的优先级怎么排)、阶段规划(如何把大任务变小,降低内部的抵触情绪和项目风险)以及持久战的韧性与毅力的庞大工程。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “架构思维篇:回顾与总结”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/67架构思维篇:回顾与总结.md b/专栏/许式伟的架构课/67架构思维篇:回顾与总结.md new file mode 100644 index 0000000..2558954 --- /dev/null +++ b/专栏/许式伟的架构课/67架构思维篇:回顾与总结.md @@ -0,0 +1,179 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 67 架构思维篇:回顾与总结 + 你好,我是七牛云许式伟。 + +到今天为止,我们第五章 “架构思维篇” 就要结束了。今天这篇文章我们对整章的内容做一个回顾与总结。 + +架构之道 + +架构思维篇的内容大体如下图所示。 + + + +在前面几个章节,我们已经陆续介绍了架构的全过程: + + +[17 | 架构:需求分析 (上)] +[18 | 架构:需求分析(下)-实战案例] +[32 | 架构:系统的概要设计] +[45 | 架构:怎么做详细设计?] + + +但架构师面临的问题往往是错综复杂的。 + +给你一个明确的需求说明文档,干干净净地从头开始做 “需求分析”,做 “概要设计”,做模块的 “详细设计”,最后编码实现,这是理想场景。 + +现实中,大多数情况并不是这样。而是:你拿到了一份长长的源代码,加上少得可怜的几份过时的文档。然后被安排做一个新功能,或者改一个顽固缺陷(Bug)。 + +我们应该怎么做架构设计? + +架构设计架构设计,设计为先,架构为魂。用架构的系统化和全局性思维来做设计。 + +整体来说,我们这个架构课的知识密度比较高。这在某种程度来说,也是一种必然结果,这是因为架构师需要 “掌控全局” 带来的。 + +所以这个架构课对大多数人而言,多多少少都会有一些盲点。如果遇到不能理解的地方,从构建完整知识体系的角度,建议通过其他的相关资料补上。当然也欢迎在专栏中提问。 + +相比一般的架构书籍来说,我们这一章架构思维篇的内容写得并不长。原因是架构思维的本源比架构规则重要。规则可能会因为环境变化而发生变化,会过时。但是架构思维的内核不会过时。 + +所以我们把关注的焦点放到了不变的思维内核上。 + +架构之道,是虚实结合之道。 + +我们要理论与实践相结合。架构设计不可能只需要熟读某些架构思维的理论,否则架构师早就满天飞了。如果两者只能取其一,我选实践。 + +从实悟虚,从虚就实,运用得当方得升华。这其实是最朴素的虚实结合的道理。对学架构这件事来说尤其如此。架构思维的感悟并不能一步到位,永远有进步的空间,需要我们在不断实践中感悟,升华自己的认知。 + +这个架构课内容的前四章为 “基础平台”、“桌面开发”、“服务端开发”、“服务治理”。 + +从内容上来说,由 “基础平台(硬件架构 / 编程语言 / 操作系统)”,到 “业务开发(桌面开发 / 服务端开发)”,再到 “业务治理(服务治理 / 技术支持 / 用户增长)”,基本上覆盖了信息技术主体骨架的各个方面。 + +有了骨架,就有了全貌,有了全局的视角。 + +前面四章,我们内容体系的侧重点放在了架构演变的过程。我们研究什么东西在迭代。这样,我们就不是去学习一个 “静态的”、“不变的” 信息技术的骨架,更重要的是我们也在学信息技术的发展历史。 + +有了基础平台,有了前端与后端,有了过去与未来,我们就有了真真正正的全貌。 + +我们博览群书,为的就是不拘于一隅,串联我们自身的知识体系,形成我们的认知框架。 + +信息科技的整体架构,与我们的应用软件架构息息相关。架构分基础架构和应用架构。选择基础架构也是构建业务竞争优势的重要组成部分。 + +从技能来说,我们可能把架构师能力去归结为: + + +理需求的能力; +读代码的能力; +抽象系统的能力。 + + +但架构师的成长之旅,首先是心性修炼之旅。这包括: + + +同理心的修炼,认同他人的能力; +全局观的修炼,保持好奇心和学习的韧性; +迭代能力的修炼,学会反思,学会在自我否定中不断成长。 + + +业务的正交分解 + + +架构就是业务的正交分解。每个模块都有它自己的业务。 + +这里我们说的模块是一种泛指,它包括:函数、类、接口、包、子系统、网络服务程序、桌面程序等等。 + + +这句话看似很简单,但是它太重要了,它是一切架构动作的基础。 + +架构行为的三步曲:“需求分析”、“概要设计”、模块的 “详细设计”,背后都直指业务的正交分解,只是逐步递进,一步步从模糊到越来越强的确定性,直至最终形成业务设计的完整的、精确无歧义的解决方案。 + +对业务进行分解得到的每一个模块来说,最重要的是模块边界,我们通常称之为 “接口”。 + +接口是业务的抽象,同时也是它与使用方的耦合方式。在业务分解的过程中,我们需要反复地审视模块的接口,发现其中 “过度的(或多余的)” 约束条件,把它提高到足够通用的、普适的场景来看。 + +在架构分解过程中有两大难题。 + +其一,需求的交织,不同需求混杂在一起。这是因为存在我们说的全局性功能。其二,需求的易变。不同客户,不同场景下需求看起来很不一样,场景呈发散趋势。 + +但无论如何,我们需要坚持作为一名架构师的信仰: + + +任何功能都是可以正交分解的,即使我目前还没有找到方法,那也是因为我还没有透彻理解需求。 + + +怎么做业务分解? + +业务分解就是最小化的核心系统,加上多个正交分解的周边系统。核心系统一定要最小化,要稳定。坚持不要往核心系统中增加新功能,这样你的业务架构就不可能有臭味。 + +所以业务做正交分解的第一件事情,就是要分出哪些是核心系统,哪些是周边子系统。核心系统构成了业务的最小功能集,而后通过不断增加新的周边功能,而演变成功能强大的复杂系统。 + +这里有一个周边功能对核心系统总伤害的经验公式: +\[ \\sum_ {对每一处修改} log_2(修改行数+1)\]同一个周边功能相邻的代码行算作一处修改。不同周边功能的修改哪怕相邻也算作多处。 + +这个公式核心想表达的含义是:修改处数越多,伤害越大。对于每一处修改,鼓励尽可能减少到只修改一行,更多代码放到周边模块自己那里去。 + +在 “[62 | 重新认识开闭原则 (OCP)]” 这一讲我们介绍了开闭原则。它非常非常重要,可以说是整个架构课的灵魂。总结来说,开闭原则包含以下两层含义: + +第一,模块的业务要稳定。模块的业务遵循 “只读” 设计,如果需要变化不如把它归档,放弃掉。这种模块业务只读的思想,是很好的架构治理的基础哲学。 + +这告诉我们,软件是可以以 “搭积木” 的方式搭出来的。核心的一点是,我们如何形成更多的 “积木”,即一个个业务只读、接口稳定、易于组合的模块。 + +我平常和小伙伴们探讨架构时,也经常说这样一句话: + + +每一个模块都应该是可完成的。 + + +这实际上是开闭原则业务范畴 “只读” 的架构治理思想的另一种表述方式。 + +要坚持不断地探索各类需求的架构分解方法。这样的思考多了,我们就逐步形成了各种各样的架构范式。这些架构范式,并不仅仅是一些架构思维,而是 “一个个业务只读、接口稳定、易于组合的模块 + 组合的方法论”,它们才是架构师真正的武器库。 + +第二,模块的业务变化点,简单一点的,通过回调函数或者接口开放出去,交给其他的业务模块。复杂一点的,通过引入插件机制把系统分解为 “最小化的核心系统+多个彼此正交的周边系统”。回调函数或者接口本质上就是一种事件监听机制,所以它是插件机制的特例。 + +领域理解 + + +应对业务需求的变化,最好的结构就是: 最小化的核心系统+多个彼此正交的周边系统。 + + +但是光理解了这一点,并不足以根本性地改变你的架构能力,因为这里面最难的是领域理解。所以需求分析很关键。怎么做需求分析?这一点要讲透真的很难。 + +我们用的是笨方法。把整个信息科技的演进史讲了一遍。 + +我们用穷举的方式来讲信息科技的半部演进史。为什么我说是半部?整个信息科技的发展,我们把它分为程序驱动和数据驱动两个阶段。 + +程序驱动的本质,是自动化的极致。以前,自动化是非常机械的,要完成自动化需要极大的难度。但是,软件的出现让自动化成为一种普惠价值,这是信息科技的上半部演进史带来的核心收益。 + +但到了数据驱动,事情就变了。我们甚至有了新的专有名词,比如 “智能时代”,或者 “DT 时代”。很多人想到智能,想到的是深度学习,想到的是机器视觉。但其实这非常片面。马云把上半场叫 IT,下半场叫 DT(数据科技),非常形象而且深刻。 + +我们的架构课,把话题收敛到了 “如何把软件跑起来,并保证它持续健康运行” 这件事情上。 + +但从企业的业务运营角度来说,这还远不是全部。“[54 | 业务的可支持性与持续运营]” 我们稍稍展开了一下这个话题。但要谈透这个话题,它会是另一本书,内容主题将会是 “数据治理与业务运营体系构建”。 + +我希望有一天能够完成它,但这可能要很久之后的事情了。 + +结语 + +今天我们对本章内容做了概要的回顾,“架构思维篇” 到此就结束了。理解了本章的内容,对于如何构建一个高度可扩展的软件架构你就有了基本的认知。 + +但不要让自己仅仅停留在认知上,需要多多实践。 + +架构的功夫全在平常。 + +无论是在我们架构范式的不断完善上,还是应对架构老化的经验积累上,都是在日常工作过程中见功夫。我们不能指望有一天架构水平会突飞猛进。架构能力提升全靠平常一点一滴地不断反思与打磨得来。 + +在应对架构老化这件事情上,不要轻率地选择进行全局性的重构。要把功夫花在平常,让重构在润物细无声中发生。 + +从难度来说,全局性的重构比一个全新业务的架构过程要难得多。重构,不只是一个架构的合理性问题。它包含了架构合理性的考量,因为我们需要知道未来在哪里,我们迭代方向在哪里。 + +但重构的挑战远不只是这些。这是一个集架构设计(未来架构应该是什么样的)、资源规划与调度(与新功能开发的优先级怎么排)、阶段规划(如何把大任务变小,降低内部的抵触情绪和项目风险)以及持久战所需的韧性与毅力的庞大工程。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们正式开始进入第六章:软件工程篇。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/68软件工程的宏观视角.md b/专栏/许式伟的架构课/68软件工程的宏观视角.md new file mode 100644 index 0000000..0762689 --- /dev/null +++ b/专栏/许式伟的架构课/68软件工程的宏观视角.md @@ -0,0 +1,98 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 68 软件工程的宏观视角 + 你好,我是七牛云许式伟。 + +软件工程 + +今天开始,我们进入第六章,谈谈软件工程。 + +我理解的架构师的职责其实是从软件工程出发的。也许大家都学过软件工程,但如果我们把软件工程这门课重新看待,这门学科到底谈的是什么?是软件项目管理的方法论? + +无论如何,软件工程是一门最年轻的学科,相比其他动辄跨世纪的自然科学而言,软件工程只有 50 年的历史。这门学科的实践太少了,任何一门学科的实践时间短的话,都很难沉淀出真正高效的经验总结,因为这些总结通常都是需要很多代人共同推动来完成的。 + +为什么说它只有 50 年时间呢? + +我们先来看看 C 语言,一般意义上来说,我们可能认为它是现代语言的开始。C 语言诞生于 1970 年,到现在是 49 年。再看 Fortran,它被认定为是第一个高级语言,诞生于 1954 年,那时候主要面向的领域是科学计算。Fortran 的程序代码量普遍都还不大,量不大的时候谈不上工程的概念。 + +这也是我为什么说软件工程这门学科很年轻,它只有 50 岁。对于这样一个年轻的学科,我们对它的认知肯定还是非常肤浅的。 + +我在这个架构课的序言 “[开篇词 | 怎样成长为优秀的软件架构师?]” 一上来就做了软件工程和建筑工程的对比。通过对比我们可以发现,二者有非常大的区别,具体在于两点: + +其一,不确定性。为什么软件工程有很大的不确定性?大部分大型的软件系统都有几千甚至几万人的规模,而这几千几万人中,却没有两个人的工作是重复的。 + +虽然大家都在编程,但是编程的内容是不一样的。每个人昨天和今天的工作也是不一样的,没有人会写一模一样的代码,我们总是不停地写新的东西,做新的工作。这些东西是非常不同的,软件工程从事的是创造性的工作。 + +大家都知道创造是很难的,创造意味着会有大量的试错,因为我们没有做过。大部分软件的形成都是一项极其复杂的工程,它们远比传统的工程复杂得多,无论是涉及的人力、时间还是业务的变数都要多很多。这些都会导致软件工程有非常大的不确定性。 + +其二,快速变化。建筑工程在完工以后就结束了,基本上很少会进行变更。但在软件工程里,软件生产出来只是开始。只要软件还在服务客户中,程序员们的创造过程就不会停止,软件系统仍然持续迭代更新,以便形成更好的市场竞争力。 + +这些都与传统建筑工程的模式大相径庭。一幢建筑自它完成之后,所有的变化便主要集中在一些软装的细节上,很少会再发生剧烈的变动,更不会持续地发生变动。但软件却不是这样,它从诞生之初到其生命周期结束,自始至终都在迭代变化,从未停止。 + +以上这两点都会导致软件工程区别于传统意义上的所有工程,有非常强的管理难度。过去那么多年,工业界有非常多的工程实践,但是所有的工程实践对软件工程来说都是不适用的,因为二者有很大的不一样。 + +今天如果我们站在管理的视角再看软件工程的话,我们知道管理学谈的是确定性。管理学本身的目的之一就是要抑制不确定性,产生确定性。 + +比如,开发工期、时间成本是否能确定。比如,人力成本、研发成本以及后期运维的成本是否能确定。 + +所以,软件项目的管理又期望达到确定性。但软件工程本身是快速变化的,是不确定的。这就是软件工程本身的矛盾。我们的目标是在大量的不确定性中找到确定性,这其实就是软件工程最核心的点。 + +架构师的职责 + +如果用 “瀑布模型” 的方式来表达,现代软件工程的全过程大体如下: + + + +从开始的需求与历史版本缺陷,到新版本的产品设计,到架构设计,到编码与测试,到最终的产品发布,到线上服务的持续维护。 + +贯穿整个工程始终的,还有不变的团队分工与协同,以及不变的质量管理。 + +更为重要的是,这个过程并不是只发生一遍,而是终其生命周期过程中,反复迭代演进。 + +它是一个生命周期往往以数年甚至数十年计的工程。对于传统工程,我们往往也把一个工程称为项目,项目工程。但软件工程不同,虽然我们平常也有项目的概念,但软件工程并不是一个项目,而是无数个项目。每个项目只是软件工程的一个里程碑(Milestone)。 + +所以,光靠把控软件工程师的水平,依赖他们自觉保障工程质量,是远远不够的。软件工程是一项非常复杂的系统工程,它需要依赖一个能够掌控整个工程全局的团队,来规划和引导整个系统的演变过程。这个团队就是架构师团队。 + +软件架构师的职责,并不单单是我们通常理解的,对软件系统进行边界划分和模块规格的定义。从根本目标来说,软件架构师要对软件工程的执行结果负责,这包括:按时按质进行软件的迭代和发布、敏捷地响应需求变更、防范软件质量风险(避免发生软件质量事故)、降低迭代维护成本。 + +因此,虽然架构师的确是一个技术岗,但是架构师干的事情,并不是那么纯技术。 + +首先是用户需求的解读。怎么提升需求分析能力,尤其是需求演进的预判能力?它无关技术,关键是心态,心里得装着用户。除了需要 “在心里对需求反复推敲” 的严谨态度外,对用户反馈的尊重之心也至关重要。 + +其次是产品设计。产品边界的确立过程虽然是产品经理主导,但是架构师理应深度参与其中。原因在于,产品功能的开放性设计不是一个纯粹的用户需求问题,它通常涉及技术方案的探讨。因此,产品边界的确立不是一个纯需求,也不是一个纯技术,而是两者合而为一的过程。 + +以上两点,是架构本身的专业性带来的,在前面五章中已经谈过很多,我们这里不再展开。在本章中,我们更多是从工程本身出发。这些话题是因软件工程的工程性而来,属于工程管理的范畴,但它们却又通常和架构师的工作密不可分。 + +这里面最为突出但也非常基础的,是贯穿软件工程始终的 “团队分工与协同” 问题、“软件的质量管理” 问题。从 “团队分工与协同” 来说,话题可以是团队的目标共识,也可以是做事方式的默契,各类规范的制定。从 “软件的质量管理” 来说,话题可能涉及软件的版本发布,质量保障的过程体系等等。 + +从更宏观的视角看,我们还涉及人力资源规划的问题。什么东西应该外包出去,包给谁?软件版本的计划是什么样的,哪些功能先做,哪些功能后做? + +看起来,这些似乎和架构师的 “本职工作” 不那么直接相关。但是如果你认同架构师的职责是 “对软件工程的执行结果负责”,那么就能够理解为什么你需要去关注这些内容。 + +结语 + +软件工程本身是一个非常新兴、非常复杂的话题。可能需要再花费 50 年这样漫长的时间才能形成更清晰的认知(例如,我们第四章 “服务治理篇” 专门探讨了现代软件工程全过程最后一个环节 “线上服务管理” 这个话题)。 + +作为架构课的一部分,这一章我们将主要精选部分与架构师的工作关系密切的话题来进行讨论,主要包括: + + +团队的共识管理; +如何阅读别人的代码; +怎么写设计文档; +发布单元与版本管理; +软件质量管理:单元测试、持续构建与发布; +开源、云服务与外包管理; +软件版本迭代的规划; +软件工程的未来。 + + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “团队的共识管理”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/69 团队的共识管理.md b/专栏/许式伟的架构课/69 团队的共识管理.md new file mode 100644 index 0000000..42be683 --- /dev/null +++ b/专栏/许式伟的架构课/69 团队的共识管理.md @@ -0,0 +1,120 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 69 团队的共识管理 + 69 | 团队的共识管理你好,我是七牛云许式伟。 + +软件工程是一项团体活动,大家有分工更有协同。不同的个体因为能力差别,可以形成十倍以上的生产力差距。而不同团体更是如此,他们的差距往往可以用天壤之别来形容。 + +这差距背后的原因,关乎的是协同的科学。 + +团队共识 + +有的团体像一盘散沙,充其量可以叫团伙。有的团体则有极强的凝聚力,整个团队上下同心,拧成一股绳,这种团体才是高效率组织,是真正意义上的团队。 + +团队靠什么上下同心?靠的是共识。 + +那么,什么是团队的共识? + +团队的共识分很多层次。其一,团队是不是有共同的目标。其二,团队是不是有共同的行事做人的准则。其三,对产品与市场的要与不要,以及为什么要或为什么不要,是否已达成一致。其四,对执行路径有没有共同的认知。其五,有没有团队默契,是否日常沟通交流很多地方不必赘述,沟通上一点即透。 + +一个团体如果缺乏共同的目标,那么它最多能够算得上是一个团伙,而不能称之为团队。 + +团队的目标也分很多层次。为什么很多企业都会谈他们的使命和愿景,是因为它是这个企业作为一个团队存在的意义,是企业所有人共同的长远目标。 + +人是愿景型动物,需要看到未来。越高级的人才越在乎团队存在的意义。所以高科技公司的人才通常只能去影响,而不是像一些人心中理解的那样,认为管理是去控制。 + +愿景是一种心力。人有很强的主观能动性。一旦人相信企业的使命与愿景,员工就变得有很强烈的使命感,有强烈的原动力。员工的行为方式也就会潜移默化发生变化。 + +不过,有共同的远景目标的团队仍然有可能走向分裂。 + +中国有句古话说得好:“道不同,不相为谋”。团队有没有相同的价值观,有没有相同的行事做人的准则,这些更根本性的基础共识,极有可能会成为压垮团队的稻草。 + +共识大于能力。如果一个人有很强的个人能力,但是却和团队没有共同的愿景,或者没有共同的价值观,那么能力越大产生的破坏性也就越大。 + +怎么达成共识? + +团队有了共同的使命、愿景与价值观,就有了共同努力把一件事情干成的最大基础。然而,这并不代表这个团队就不会遇到共识问题。 + +团队仅有远期的目标是不够的,还要有中短期的目标。企业的使命和愿景需要由一个个的战略行动来落地。我们的产品定位怎么样,选择哪些细分市场去切入,这些同样需要团队达成共识。 + +怎么去达成共识? + +越 “聪明” 的团队负责人,往往越容易忽视达成共识的难度。他们通常会召开会议,然后把自己的想法说给大家听。半个小时后,兄弟们迷茫地回去了。 + +在团队还小的时候,这种简单共识的方式很可能是可以奏效的,尤其是当团队负责人还能够一一去检查每个人的工作内容时,所有的理解偏差都能够得到比较及时的纠正。 + +但是团队规模稍微变大一些,这种简单共识突然就失效了。“我明明已经告诉他们要做什么了。” 负责人有时候困惑于团队成员为什么并没有理解他的话。 + +这是因为他还并不理解真正的共识意味着什么。也没有对达成共识的难度有足够的认知。 + +让更多人参与到决策形成的过程现场,是更好的共识达成的方式。通过同步足够充分的信息,通过共创而非传达决策的方式让结论自然产生。 + +这个共创过程不必团队所有人都参与,但要确保所有影响落地的关键角色都在,并确保参与这个过程的人都能够产生思想的碰撞,而非做个吃西瓜群众。 + +契约与共识效率 + +目标与执行路径达成了共识,这还不够。我们还需要把共识表达出来,形成文字。 + +为什么这很重要? + +因为共识之所以为共识,是因为它不是空中楼阁,不是口号,而是指导我们做战略选择的依据,指导我们平常行为的依据。 + +所以,共识就是团队协作的契约。契约的表达越是精确而无歧义,团队协作中主观能动性就越高,执行的效率也就越高。 + +对于架构过程同样如此。 + +架构过程实际上是团队共识形成与确认的过程。架构设计需要回答两个基本的问题: + + +系统要做成什么样? +怎么做? + + +架构设计为什么叫架构设计,是因为架构师的工作中除了架构,还有设计。设计其实谈的就是 “系统要做成什么样”。 + + +设计高于架构。 + + +设计强调规格,架构强调实现。规格设计是架构过程的最高共识。所以,规格高于实现。我们用架构的全局性和系统性思维去做设计。 + +一些架构师乐衷于画架构图,把它当作是架构师最重要的工作内容。但架构图在共识的表达上并不太好。因为共识是需要精确的、无歧义的。而架构图显然并不精确。 + +对于一个工程团队来说,没有精确的共识很可怕。它可能导致不同模块的工作牛头不对马嘴,完全无法连接起来,但是这个风险没有被暴露,直到最后一刻里程碑时间要到了,要出版本了,大家才匆匆忙忙联调,临时解决因为架构不到位产生的 “锅”。 + +这时候人们的动作通常会走形。追求的不再是架构设计的好坏,而是打补丁,怎么把里程碑的目标实现了,别影响了团队绩效。 + +我们作个类比,这种不精确的架构,就好比建筑工程中,设计师画了一个效果图,没有任何尺寸和关键细节的确认,然后大家就分头开工了。最后放在一起拼接(联调),发现彼此完全没法对上,只能临时修修改改,拼接得上就谢天谢地了。是不是能够和当初效果图匹配?让老天爷决定吧。 + +更精确描述架构的方法是定义每个模块的接口。接口可以用代码表达,这种表达是精确的、无歧义的。架构图则只是辅助模块接口,用于说明模块接口之间的关联。 + +尊重契约,尊重共识精确的、无歧义的表达,非常非常重要。 + +绝大部分哪怕是非常优秀的架构师,在系统设计(也叫概要设计)阶段通常也只会形成系统的概貌,把子系统的划分谈清楚,把子系统的接口规格谈清楚。 + +但实际上概要设计阶段最好的状态并不是只有设计文档。 + +为了降低风险,系统设计阶段也应该有代码产出。 + +这样做有两个方面的目的。其一,系统的初始框架代码。也就是说,系统的大体架子已经搭建起来了。其二,原型性的代码来验证。一些核心子系统在这个阶段提供了 mock 的系统。 + +这样做的好处是,一上来我们就关注了全局系统性风险的消除,并且给了每个子系统或模块的负责人一个更具象且确定性的认知。 + +代码即文档。代码是理解一致性更强的文档。 + +结语 + +这一讲我们谈的是协同的科学。为什么有的团队效率极高,有的团队却进展缓慢,从背后的协同效率来说,共识管理是根因中的根因。 + +共识有非常多的层次。不同层次的共识处于完全不同的维度。它们都极其重要,且相互不可替代。当某个层次的共识出问题的时候,我们需要在相应的层次去解决它。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “怎么写设计文档”。原计划我们下一讲是 “如何阅读别人的代码”,但是我想先顺着共识这个话题谈问题谈清楚。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/70怎么写设计文档?.md b/专栏/许式伟的架构课/70怎么写设计文档?.md new file mode 100644 index 0000000..27fbe97 --- /dev/null +++ b/专栏/许式伟的架构课/70怎么写设计文档?.md @@ -0,0 +1,152 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 70 怎么写设计文档? + 你好,我是七牛云许式伟。 + +在 “[68 | 软件工程的宏观视角]” 一讲中,我们用最基本的 “瀑布模型” 来描述现代软件工程的全过程,大体如下: + + + +在这个过程中,有两个阶段非常关键:一个是 “产品设计”,一个是 “架构设计”。产品设计由产品经理主导,关注的是 “如何以产品特性来系统化地满足用户需求”。架构设计由架构师主导,关注的是 “业务系统如何系统化地进行分解与交付”。 + +“设计” 一词非常精妙。无论是 “产品设计”,还是 “架构设计”,其实谈的都是 “需求如何被满足” 这件事情的共识。无论是 “产品文档”,还是 “架构文档”,它们都是设计文档的一种,都有团队内及团队间的协同价值。 + +上一讲 “[69 | 团队的共识管理]” 我们已经从团队的协同角度,谈了共识的重要性。本质上,我们也是在谈 “设计” 的重要性。换个角度来说,一个企业的使命、愿景与价值观,何尝不是这个企业最高维度的 “设计” 呢? + +产品经理与架构师是一体两面,对人的能力要求的确会比较像,但是分工不同,关注的维度不同。产品经理关注的维度,其关键词是:用户需求、技术赋能、商业成功。而架构师关注的维度,其关键词是:用户需求、技术实现、业务迭代。 + +今天我们谈的 “设计文档”,重点聊的是 “架构设计文档” 怎么写,但是本质上所有 “设计文档” 的内容组织逻辑,都应该是相通的。它们的内容大体如下: + + +现状 :我们在哪里,现状是什么样的? +需求:我们的问题或诉求是什么,要做何改进? +需求满足方式: + + +要做成什么样,交付物规格,或者说使用界面(接口)是什么? +怎么做到?交付物的实现原理。 + + + +关于设计文档内容组织的详细说明,我们在前面 “[45 | 架构:怎么做详细设计?]” 中已经进行过交代。概括来说,这些设计文档要素的关键在于以下几点。 + +现状:不要长篇累牍。现状更多的是陈述与我们要做的改变相关的重要事实,侧重于强调这些事实的存在性和重要性。 + +需求:同样不需要长篇累牍。痛点只要够痛,大家都知道,所以需求陈述是对痛点和改进方向的一次共识确认。 + +需求满足方式:要详写,把我们的设计方案谈清楚。具体来说,它包括 “交付物规格” 和 “实现原理” 两个方面。 + +交付物规格,或者说使用界面,体现的是别人要怎么使用我。对于 “产品设计”,交付物规格可能是 “产品原型”。对于 “架构设计”,交付物规格可能是 “网络 API 协议” 或者 “包(package)导出的公开类或函数”。 + +实现原理,谈的是我们是怎么做到的。对于 “产品设计”,它谈的是用户需求对应的 UserStory 设计,也就是业务流具体是怎么完成的。而对于 “架构设计”,它谈的是 UserStory 具体如何被我们的程序逻辑所实现。 + +以下这个公式大家都耳熟能详了: + + +程序 = 数据结构 + 算法 + + +它是一个很好的指导思想。当我们谈程序实现逻辑时,我们总是从数据结构和算法两个维度去描述它。其中,“数据结构” 可以是内存数据结构,也可以是外存数据结构,还可以是数据库的 “表结构”。“算法” 基于 “数据结构”,它描述的是 UserStory 的具体实现,它可以是 UML 时序图(Sequence Diagram),也可以是伪代码(Pseudo Code)。 + +多个设计方案的对比 + +在现实中,一篇设计文档有时候不是只有一个设计方案,而是有多个可能的需求实现方式。在这个时候,通常我们会概要地描述清楚两个设计方案的本质差别,并且从如下这些维度进行对比: + + +方案的易实施性与可维护性。 +方案的时间复杂度与空间复杂度。 + + +不同的业务系统倾向性不太一样。对于绝大部分业务,我们最关心的是工程效率,所以方案的易实施性与可维护性为先;但是对于部分对成本与性能非常敏感的业务,则通常在保证方案的时间复杂度与空间复杂度达到业务预期的前提下,再考虑工程效率。 + +在确定了设计方案的倾向性后,我们就不会就我们放弃的设计方案做过多的展开,整个设计文档还是以描述一种设计方案为主。 + +如果我们非要写两套设计方案,这时应该把设计文档分为两篇独立的设计文档,而不是揉在一起。 + +你可能觉得没有人会这么不怕麻烦,居然写两套设计方案。但是如果两套设计方案的比较优势没有那么显著时,现实中写两套设计方案确实是存在的,并且应该被鼓励。 + +为什么这么说? + +这是因为 “设计” 是软件工程中的头等大事,我们应该在这里 “多浪费点时间”,这样的 “浪费” 最终会得到十倍甚至百倍以上的回报。 + +使用界面(接口) + +在描述交付物的规格上,系统的概要设计,与模块的详细设计很不一样。 + +对于 “模块的详细设计” 来说,规格描述相对简单。因为我们关注的面只是模块本身,而非模块之间的关系。对于模块本身,我们核心关注点是以下两点:一是接口是否足够简单,是否自然体现业务需求。二是尽可能避免进行接口变更,接口要向前兼容。 + +关于接口变更,后面有机会我们还会进行详细的讨论,这一讲先略过。 + +但对于 “系统的概要设计” 来说,我们第一关心的是模块关系,第二关心的才是各个模块的核心接口。这些接口能够把系统的关键 UserStory 都串起来。 + +表达模块关系在某种程度来说的确非常重要,这可能是许多人喜欢画架构图的原因。 + +但描述模块间的关系的确是一件比较复杂的事情。我们在 “[32 | 架构:系统的概要设计]” 这一讲中实际上先回避了这个问题。 + +一种思路是我们不整体描述模块关系,直接基于一个个 UserStory 把模块之间的调用关系画出来。比如对于对象存储系统,我们上传一个文件的业务流程图看起来是这样的: + + + +这类图相信大家见过不少。但它从模块关系表达上并不是好的选择,因为根本并没有对模块关系进行抽象。这类图更多被用在面向客户介绍 API SDK 的背后的实现原理时采用,而非出现在设计文档。 + +如果只是对于 UserStory 业务流程的表达来说,UML 时序图通常是更好的表达方式。 + +但是,怎么表达模块关系呢? + +一个方法是对模块的调用接口进行分类。通过 “[62 | 重新认识开闭原则 (OCP)]” 这一讲我们知道,一个模块对外提供的访问接口无非是: + + +常规 DOM API,即正常的模块功能调用; +事件(Event)的发送与监听; +插件(Plugin)的注册。 + + +这些不同类型的访问接口,分别代表了模块间不同的依赖关系。我们回忆一下 MVC 的框架图,如下: + + + +在图中,View 监听 Model 层的数据变更事件。View 转发用户交互事件给 Controller。Controller 则负责将用户交互事件转为 Model 层的 DOM API 调用。 + +另一个表达模块关系的视角,是从架构分解看,我们把系统看作 “一个最小化的核心系统 + 多个彼此正交分解的周边系统”。例如,我们实战案例 — 画图程序的模块关系图如下: + + + +需要清楚的是,模块关系图的表达是非常粗糙的,虽然它有助于我们理解系统分解的逻辑。为了共识的精确,我们仍然需要将各个模块核心的使用界面(接口)表达出来。 + +实现原理 + +谈清楚了交付物的规格,我们就开始谈实现。对于 “[系统的概要设计]” 与 “[模块的详细设计]”,两者实现上的表达有所不同。 + +对于模块的详细设计来说,需要先交代清楚 “数据结构” 是什么样的,然后再将一个个 UserStory 的业务流程讲清楚。 + +对于系统的概要设计来说,核心是交代清楚不同模块的配合关系,所以无需交代数据结构,只需要把一个个 UserStory 的业务流程讲清楚。 + +无论是否要画 UML 时序图,在表达上伪代码(Pseudo Code)的设计都是必需的。 + +伪代码的表达方式及语义需要在团队内形成默契。这种伪代码的语义表达必须是精确的。 + +比如,对于网络请求相关的伪代码,我们可以基于类似 qiniu httptest 的语法,如下: + +# 请求 +post /v1/foo/bar json {...} + +# 返回 +ret json {...} + + +类似地,对于 MongoDB,我们可以直接用 MongoDB 的 JavaScript 脚本文法。对于 MySQL,则可以直接基于 SQL 语法。等等。 + +结语 + +前面在 “[45 | 架构:怎么做详细设计?]” 我们实际上已经大体介绍了模块级的设计文档怎么写。所以这一讲我们主要较为全面地补充了各类设计文档,包括产品设计、系统的概要设计等在细节上与模块设计文档的异同。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “如何阅读别人的代码”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/71 如何阅读别人的代码?.md b/专栏/许式伟的架构课/71 如何阅读别人的代码?.md new file mode 100644 index 0000000..3f6f73c --- /dev/null +++ b/专栏/许式伟的架构课/71 如何阅读别人的代码?.md @@ -0,0 +1,132 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 71 如何阅读别人的代码? + 71 | 如何阅读别人的代码?你好,我是七牛云许式伟。今天聊聊如何阅读别人的代码。 + +为何要读别人的代码? + +我们去阅读别人的代码,通常会带有一定的目的性。完整把一个系统的代码 “读懂” 需要极大的精力。所以明确阅读代码的目标很重要,因为它决定了你最终能够为这事付出多大的精力,或者说成本。 + +大体来说,我们可以把目标分为这样几种类型: + + +我要评估是否引入某个第三方模块; +我要给某个模块局部修改一个 Bug(可能是因为使用的第三方模块遇到了一个问题,或者可能是你的上级临时指定了一个模块的 Bug 给你); +我要以某个开源模块为榜样去学习; +我要接手并长期维护某个模块。 + + +为什么要把我们的目标搞清楚? + +因为读懂源代码真的很难,它其实是架构的反向过程。它类似于反编译,但是并不是指令级的反编译,而是需要根据指令反推更高维的思想。 + +我们知道反编译软件能够将精确软件反编译为汇编,因为这个过程信息是无损的,只是一种等价变换。但是要让反编译软件能够精确还原出高级语言的代码,这就比较难。因为编译过程是有损的,大部分软件实体的名字已经在编译过程中被去除了。当然,大部分编译器在编译时会同时生成符号文件。它主要用于 debug 用途。否则我们在单步跟踪时,debug 软件就没法显示变量的名字。 + +即使我们能够拿到符号文件,精确还原出原始的高级语言的代码仍然非常难。它需要带一定的模型推理在里面,通过识别出这里面我们熟悉的 “套路”,然后按照套路进行还原。 + +我们可以想像一下,“一个精确还原的智能反编译器” 是怎么工作的。 + +第一步,它需要识别出所采用的编程语言和编译器。这通常相对容易,一个非常粗陋的分类器就可以完成。尤其是很多编译器都有 “署名”,也就是在编程出的软件中带上自己签名的习惯。如果假设所有软件都有署名,那么这一步甚至不需要训练与学习。 + +第二步,通过软件的二进制,结合可选的符号文件(没有符号文件的结果是很多软件实体,比如类或函数的名字,会是一个随机分配的符号),加上它对该编译器的套路理解,就可以进行反编译了。 + +编译器的套路,就如同一个人的行为,持续进行观察学习,是可以形成总结的。这只需要反编译程序持续地学习足够多的该编译器所产生的样本。 + +我之所以拿反编译过程来类比,是希望我们能够理解,阅读源代码过程一方面是很难的,另一方面来说,也是需要有产出的。 + +有产出的学习过程,才是最好的学习方式。 + + + +那么阅读源代码的产出应该是什么?答案是,构建这个程序的思路,也就是架构设计。 + +理解架构的核心脉络 + +怎么做到? + +首先,有文档,一定要先看文档。如果原本就已经有写过架构设计的文档,我们还要坚持自己通过代码一步步去反向进行理解,那就太傻了。 + +但是,一定要记住文档和代码很容易发生脱节。所以我们看到的很可能是上一版本的,甚至是最初版本的设计。 + +就算已经发生过变化,阅读过时的架构设计思想对我们理解源代码也会有极大的帮助作用。在这个基础上,我们再看源代码,就可以相互进行印证。当然如果发生了冲突,我们需要及时修改文档到与代码一致的版本。 + +看源代码,我们首先要做到的是理解系统的概要设计。概要设计的关注点是各个软件实体的业务范畴,以及它们之间的关系。有了这些,我们就能够理解这个系统的架构设计的核心脉络。 + +具体来说,看源码的步骤应该是怎样的呢? + +首先,把公开的软件实体(模块、类、函数、常量、全局变量等)的规格整理出来。 + +这一步往往有一些现成的工具。例如,对 Go 语言来说,运行 go doc 就可以帮忙整理出一个自动生成的版本。一些开源工具例如 doxygen 也能够做到类似的事情,而且它支持几乎所有的主流语言。 + +当然这一步只能让我们找到有哪些软件实体,以及它们的规格是什么样的。但是这些软件实体各自的业务范畴是什么,它们之间有什么关系?需要进一步分析。 + +一般来说,下一步我会先看 example、unit test 等。这些属于我们研究对象的客户,也就是使用方。它们能够辅助我们理解各个软件实体的语义。 + +通过软件实体的规格、说明文档、example、unit test 等信息,我们根据这些已知信息,甚至包括软件实体的名字本身背后隐含的语义理解,我们可以初步推测出各个软件实体的业务范畴,以及它们之间的关系。 + +接下来,我们需要进一步证实或证伪我们的结论。如果证伪了,我们需要重新梳理各个软件实体之间的关系。怎么去证实或证伪?我们选重点的类或函数,通过看它们的源代码来理解其业务流程,以此印证我们的猜测。 + +当然,如果你能够找到之前做过这块业务的人,不要犹豫,尽可能找到他们并且争取一个小时左右的交流机会,并提前准备好自己遇到迷惑的问题列表。这会大幅缩短你理解整个系统的过程。 + +最后,确保我们正确理解了系统,就需要将结论写下来,形成文档。这样,下一次有其他同学接手这个系统的时候,就不至于需要重新再来一次 “反编译”。 + +理解业务的实现机制 + +业务系统的概要设计、接口理清楚后,通常来说,我们对这个系统就初步有谱了。如果我们是评估第三方模块要不要采纳等相对轻的目标,那么到此基本就可以告一段落了。 + +只有在必要的情况下,我们才研究实现机制。刚才我们谈到系统架构梳理过程中,我们也部分涉及了源代码理解。但是,需要明确的是,前面我们研究部分核心代码的实现,其目的还是为了确认我们对业务划分猜测的正确性,而不是为了实现机制本身。 + +研究实现是非常费时的,毕竟系统的 UserStory 数量上就有很多。把一个个 UserStory 的具体业务流程都研究清楚写下来,是非常耗时的。如果这个业务系统不是我们接下来重点投入的方向,就没必要在这方面去过度投入。 + +这时候目标就很重要。 + +如果我们只是顺带解决一下遇到的 Bug,无论是用第三方代码遇到的,还是上级随手安排的临时任务,我们自然把关注点放在要解决的 Bug 本身相关的业务流程上。 + +如果我们是接手一个新的业务系统,我们也没有精力立刻把所有细节都搞清楚。这时候我们需要梳理的是关键业务流程。 + +怎么搞清楚业务流程? + + +程序 = 数据结构 + 算法 + + +还是这个基础的公式。要搞清楚业务流程,接下来要做的事情是,把这些业务流程相关的数据结构先理清楚。 + +数据结构是容易梳理的,类的成员变量、数据库的表结构,通常都有快速提取的方式。除了 MongoDB 可能会难一些,因为弱 schema 的原因,我们需要通过阅读代码的方式去理解 schema。更麻烦的是,我们不确定历史上经历过多少轮的 schema 变更,这通过最新版本的源代码很可能看不出来。一个不小心,我们就可能会处理到非预期 schema 的数据。 + +理清楚数据结构,事情就解决了大半。 + +剩下来就是理各个 UserStory 的业务流程,并给这些业务流程画出它的 UML 时序图。这个过程随时可以补充。所以我们挑选对我们当前工作最为相关的来做就好了。 + +最后,还是同样地,我们要及时把我们整理的结论写下来,变成架构文档的一部分。这样随着越来越多人去补充完整架构设计文档,才有可能把我们的项目从混沌状态解脱出来。 + +结语 + +对于任何一个项目团队来说,阅读代码的能力都极其重要。哪怕你觉得你的团队共识管理很好,团队很默契,大家的工程习惯也很好,也都很乐意写文档,但这些都替代不了阅读代码这个基础活动。 + +阅读代码是不可或缺的能力。 + +为什么这么说?因为:代码即文档,代码是理解一致性更强的文档。 + +另外,作为一个小补充,我们需要指出的一点是:阅读代码的结果,有时不一定仅仅是架构设计文档的补充与完善。我们有时也会顺手修改几行代码。 + +这是正常现象,而且应该被鼓励。为什么鼓励改代码?是因为我们鼓励随时随地消除臭味。改几行明显风格不太好的代码,是非常好的一件事情。 + +但是我们也要有原则。 + +其一,不做大的改动,比如限定单个函数内的改动不能超过 10 行。 + +其二,确保改动前后的语义完全一致。这种一致需要包括所有 corner case 上的语义一致,例如错误码,条件语句的边界等。 + +其三,不管多自信,有改动就需要补全相关的单元测试,确保修改代码的条件边界都被覆盖。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “发布单元与版本管理”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/72 发布单元与版本管理.md b/专栏/许式伟的架构课/72 发布单元与版本管理.md new file mode 100644 index 0000000..7f13568 --- /dev/null +++ b/专栏/许式伟的架构课/72 发布单元与版本管理.md @@ -0,0 +1,164 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 72 发布单元与版本管理 + 72 | 发布单元与版本管理你好,我是七牛云许式伟。 + +前面我们在 “[68 | 软件工程的宏观视角]” 一讲中谈到:一个软件工程往往是生命周期以数年甚至数十年计的工程。对于传统工程,我们往往把一个工程同时也称之为项目,项目工程。但软件工程不同,虽然我们平常也有项目的概念,但软件工程并不是一个项目,而是无数个项目。每个项目只是软件工程中的一个里程碑(Milestone)。 + +这意味着软件工程终其完整的生命周期中,是在反复迭代与演进的。这种反复迭代演进的工程,要保证其质量实际上相当困难。 + +源代码版本管理 + +怎么确保软件工程的质量? + +很容易想到的一个思路是,万一出问题了,就召回,换用老版本。 + +这便是版本管理的来由。当然,如果仅仅只是为了召回,只需要对软件的可执行程序进行版本管理就好了。但我们如果要进一步定位软件质量问题的原因,那就需要找到一个方法能够稳定再现它。 + +这意味着我们需要对软件的源代码也进行版本管理,并且它的版本与可执行程序的版本保持一一对应。 + +但实际上这事并没有那么简单。 + +从软件的架构设计可知,软件是分模块开发的,不同模块可能由不同团队开发,甚至有些模块是外部第三方团队开发。这意味着,从细粒度的视角来看,一个软件工程的生命周期中,包含着很多个彼此完全独立的子软件工程。这些子软件工程它们有自己独立的迭代周期,我们软件只是它们的 “客户”。 + +这种拥有独立的迭代周期的软件实体,我们称之为 “发布单元”。你可能直觉认为它就是模块,但是实际上两者有很大的不同。 + +对于一个发布单元,我们直观的一个感受是它有自己独立的源代码仓库(repo)。 + +发布单元的输出不一定是可执行程序,它有如下可能: + + +可执行程序,或某种虚拟机的字节码程序; +动态库(so/dylib/dll); +某种虚拟机自己定义的动态库,比如 JVM 平台下的 jar 包; +静态库(.a 文件),它通常实际上是可执行程序的半成品,比较严谨来说的编译过程是先把每个模块编译成半成品,然后由链接器把各个模块组装成成品; +源代码本身,一些语言的价值主张是源代码发布,比如 Go 语言。 + + +发布单元的输入,常规理解主要包含以下两部分的内容: + + +若干自己独立演进的模块,也就是源代码仓库(repo)托管的代码; +自己依赖的发布单元列表,这些外部的发布单元有自己独立的迭代周期。 + + +源代码仓库管理系统,比如 svn、git 等等,一般只能管到第一部分。它让我们对自己独立演进的代码可以有很好的质量跟踪。 + +我们以 github 为例,它提供了以下源代码质量的管理手段。 + +其一,团队成员开发活动的独立性。每个人可以极低成本地建立一个开发分支(branch),一个开发分支做一个功能(feature),这个工作没有完成时,他的工作对所有其他人不可见,所以团队成员有很好的并行开发的能力,彼此完全独立。 + +其二,完善的代码质量检查机制。当一个团队成员完成他某项功能(feature)开发时,他可以提交一个功能合并请求(pull request),以求将代码合并进主代码库。但在此之前,我们需要对这项新功能的代码质量进行检查。常见的手段如下: + + +自动化运行单元测试案例(unit test); +单元测试覆盖率检查(code coverage); +静态代码质量检查(lint); +人工的代码互审(code review); +…… + + +代码质量检查过程,需求显然比较易变。所以在这里 github 做了开放设计。我们再一次感受到了开闭原则的威力。 + +其三,完善的回滚机制(revert)。在代码已经合并到主代码库后,如果我们突然发现它有 Bug,这时候并不是落子无悔,而是可以自己对某次有 Bug 的 pull request 做回滚(revert),这样主干就可以得到去除了该功能后的一个新的发行版本。 + +对于第二部分,也就是发布单元的外部依赖管理,通常不同语言有自己的惯例。例如,Go 语言早期并没有官方的版本管理手段,所以导致有很多社区版本的实现方案。直到最新的 go mod 机制终于统一了这一纷争。 + +从基本原理来说,所有外部依赖管理无非要达到这样一个目标:指定我这个发布单元依赖的各个模块(嗯,这是通俗说法,其实是指依赖的发布单元)的建议版本是什么。 + +这样,我们理论上就可以稳定持续地通过源代码构建出相同能力的输出结果。 + +注意,这里有一个前提假设,是要求所有人都自觉遵循的:一个打好了版本号的发布单元是只读的,我们不能对其做任何改动。这句话的意思包括: + +其一,我们不能修改发布单元自身包含的各个模块的的代码。这很容易理解,我们不展开。 + +其二,我们不能修改发布单元依赖的外部模块(同样地,其实指依赖的发布单元)的版本。比如我们依赖 opencv,把依赖的版本号从 v1.0 升级到 v2.0,这是不行的,这也是一次变更,需要修改我们的版本号。 + +如果有人破坏了版本的只读语义,就会导致所有依赖它的发布单元的版本只读语义也被破坏。这是我们需要极力去避免发生的事情。 + +从严谨意义来说,仅保证发布单元自身的源代码和依赖的外部模块只读,仍然不足以保证输出结果的确定性。为什么这么说,因为还有两个东西没有做到只读: + +其一,操作系统内核。不同版本的操作系统内核行为不完全一致,它的一些动态库可能行为不完全一致,这些都可能会导致我们的软件行为有所不同。 + +其二,编译器。不同版本的编译器同样存在理论上与编译的结果行为上不一样的可能。 + +为什么没有把它们纳入到源代码版本管理的范畴管起来?这当然是因为操作系统和编译器大部分情况下质量是有所保证的,所以当软件在不同版本的操作系统下行为不一致时,这会被看做软件 Bug 记录下来,而不是修改操作系统。 + +软件发布的版本管理 + +但并不是在所有时刻,我们都能够相信操作系统和编译器。从源代码版本管理的角度,它的好处是软件构建(build)过程是一个相对封闭可预期的环境,这个环境我们甚至直接规定操作系统的种类和版本、编译器的版本,系统预装哪些软件等等。 + +但是软件发布过程却并非如此。 + +我们大家可能都接触过各种软件发布的管理工具,比如apt、rpm、brew 等等。在这些管理工具的使用过程中,我们每个人或多或少都有过不少 “失败教训”。并不是每一次软件安装过程都能够如愿。 + +这些软件发布的管理工具,背后有不少实际上基于的就是源代码的版本管理。但是为什么这个时候它会不 work 呢?因为用户之间系统环境的差异太大了。让每个软件的发布者都能够想到多样化的环境并加以适配,这是非常高的要求。 + +所以,软件安装有时会不成功,实在是在所难免。 + +怎么才能彻底解决这个问题? + +答案是,容器化。 + +容器的镜像(image),不只是包含了软件发布的可执行程序本身,也完整包含了运行它的所有环境,包括依赖的动态库和运行时,甚至包括了它依赖的 “操作系统”。这意味着容器的镜像(image)的版本管理,比之源代码的版本管理更进一步,实现完完全全的自描述,不再依赖任何外部环境。 + +这给我们线上服务的版本管理带来了巨大的便捷性。新版本的服务有缺陷 ?回滚到老版本即可。 + +只读设计的确定性 + +版本的只读设计,带来巨大的收益,这是因为版本是一个 “基线”,对于这个基线,我们心理上对它的预期是确定性的。这种确定性非常重要。 + +在 “[68 | 软件工程的宏观视角]” 一讲中我们提到: + + +软件项目的管理期望达到确定性。但软件工程本身是快速变化的,是不确定的。这就是软件工程本身的矛盾。我们的目标是在大量的不确定性中找到确定性,这其实就是软件工程最核心的点。 + + +只读设计提升了软件工程的确定性,所以只读思想被广泛运用。前面我们说开闭原则背后的架构治理哲学,也是模块,或者说软件实体,其业务范畴只读。在业务只读,接口稳定的预期下,模块与模块之间就可以自由组合,构建越来越复杂的系统。 + +往小里说,我们开发的时候,有时候会倾向于变量只读,以提高内心对确定性的预期。我并没有去用严谨的方式实证过变量只读的收益究竟有多大,但它的确成为了很重要的一种编程流派,即函数式编程。 + +函数式编程从编程范式来说比较小众,但是其只读思想被广泛借鉴。 + +这里面最典型的就是大数据领域的 Spark。Spark 的核心是建立在统一的抽象弹性分布式数据集(Resiliennt Distributed Datasets,RDD)之上。 + +而 RDD 的核心思想正是只读。对一个只读的 RDD 施加一个变换(transform),即得到另一个 RDD,这不就是函数式编程么?但这种只读设计,让我们的分布式运算在重试、延迟计算、缓存等过程都变得极其简单。 + +版本的兼容问题 + +版本管理的最后一个问题是兼容性。让一个模块依赖另一个模块(严谨来说是发布单元)的特定版本,这解决了版本的确定性问题。 + +但是,在某个特定的时刻,我们总是会希望将依赖的模块升级到新版本。无论是基于我们需要使用该模块的新功能,又或者是为了修复的 Bug,或者纯粹是心理上想要更好的东西。 + +更换到新版本多多少少冒了一些风险。这里面最大风险是所依赖的模块完成了一次重构。 + +为什么依赖模块的重构会给我们的系统带来未知风险?这其中的原因就在于版本兼容的难度。 + +兼容一个模块的主体功能并不复杂,既然我们重构了,这部分肯定是得到了解决。但兼容的难度全在细节上。错误码、低频的分支行为等等,这些都需要兼容。 + +如果这种分支兼容太麻烦,我们干脆就放弃兼容,连软件实体(如函数)的名字都改了。这倒是干脆,客户升级版本后一看,编译不过了,老老实实用新的接口进行重写,重新测试。 + +但有时候我们无法放弃兼容。这发生在我们在做一个互联网服务时。一旦我们发布了一个 api,它就很难收回,因为使用这个 api 的客户端可能有很多。如果我们放弃这个 api 就意味着我们放弃了很多用户,这是不可接受的。 + +为了应对这个问题,比较常见的做法是为所有 api 引入版本号,如 “/v2/foo/bar”。当我们对 api 发生不兼容的修改时,就升级版本号,比如 “/v3/foo/bar”。 + +这样做有一个额外的好处。如果我们对某个复杂模块进行了全局重构,并且兼容老版本的行为细节非常困难时,我们可以直接升级所有 api 的版本号。这样在线上我们可以保留两个版本的服务同时存在。这通过前面放 nginx 作为 api 分派的网关来做到。 + +这样两个版本服务并行,就不需要重构时做太细节的行为兼容。但应当注意,这也是不得已的办法,如果能够兼容,还是鼓励尽可能去兼容。毕竟客户端在升级版本之后,不兼容的地方越多,修改的心智负担就越大。 + +结语 + +今天我们聊的是怎么做版本管理。一个复杂的软件,总可以被分割为若干个独立迭代的发布单元,以便分而治之。发布单元的切割不宜过细,应该以一个小团队负责起来比较舒服为宜,不太小但也不太大。 + +版本的只读设计提高了系统的确定性预期,这是非常非常好的收益。但我们也应注意版本兼容上带来的坑。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “软件质量管理:单元测试、持续构建与发布”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/73软件质量管理:单元测试、持续构建与发布.md b/专栏/许式伟的架构课/73软件质量管理:单元测试、持续构建与发布.md new file mode 100644 index 0000000..5f75c25 --- /dev/null +++ b/专栏/许式伟的架构课/73软件质量管理:单元测试、持续构建与发布.md @@ -0,0 +1,121 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 73 软件质量管理:单元测试、持续构建与发布 + 你好,我是七牛云许式伟。 + +上一讲 “72 | 发布单元与版本管理” 我们聊了版本管理中,只读思想给软件工程带来的确定性价值,它在软件工程质量管理中也是很核心的一点。 + +软件质量管理 + +今天我们聊聊软件工程中,我们在质量管理上其他方面的一些思考。事实上,软件质量管理横跨了整个软件工程完整的生命周期。 + + + +软件工程与传统工程非常不同。它快速变化,充满不确定性。不仅如此,一个软件工程往往是生命周期以数年甚至数十年计的工程。对于传统工程,我们往往把一个工程同时也称之为项目,项目工程。但软件工程不同,虽然我们平常也有项目的概念,但软件工程并不是一个项目,而是无数个项目。每个项目只是软件工程中的一个里程碑(Milestone)。 + +这些都决定了软件工程质量管理的思想与传统工程截然不同。在传统工程中,设计的工作往往占比极少,重复性的工作占据其生命周期的绝大部分时间。所以传统工程有极大的确定性。检查清单(Check List)很可能就已经可以很好地实现其工程质量的管理。 + +但对于软件工程来说,设计工作在整个工程中持续发生。哪怕是非设计工作,比如编码实现,也仍然依赖个体的创造力,同样存在较强的不确定性。显然,检查清单(Check List)完全无法满足软件工程的质量管理需要。 + +那么,到底应该怎么管理软件工程的质量?每次谈软件工程质量保障的时候,我总会先画下面这张图: + + + +它谈的是软件的生命周期,或者也可以理解为软件中某项功能的生命周期。我们把软件或软件的某项功能生命周期分成两个大的阶段,一个阶段是开发期,一个阶段是维护期。开发期与维护期是相对而言的,只是在表征上,开发期有更强的设计属性。维护期虽然也持续会有设计工作,但是工作量会小一个数量级以上。 + +为什么划分出开发期与维护期是重要的? + +因为开发期的时间跨度虽然可能不长,但是它的影响太大了,基本决定了后期维护期的成本有多高。 + +这也意味着软件工程是需要有极强预见性的工程。我们在开发期恰如其分地多投入一分精力,后面在维护期就有十倍甚至百倍以上的回报。 + +设计工作的质量至关重要。但是它执行上又不太有复制性,可复制的只是设计范式和设计思维。 + +我们只能在这种执行的不确定性中找工程上的确定性。 + +如何做到? + +单元测试 + +首先,做好自动化测试。自动化测试对软件工程的重要性是不言而喻的。如果是一项一次性的工程,我们可以基于常规的手工测试。但常规测试的缺点在于: + +其一,一般常规测试是基于手工的,不具备可回归性。因此,常规测试的效率不高,一次完整的测试集跑下来可能需要几天甚至一周之久。 + +其二,易于缺乏效率,所以往往为了赶工会导致测试仅仅针对典型数据,测试的覆盖率往往也很低。 + +软件工程的生命周期往往几年甚至几十年之久,我们必然关注单次测试的效率。所以自动化测试的核心价值就在于可回归性与提高测试的覆盖率。 + +自动化测试与常规测试相比,风格上有很明显的不一样,它有如下重要特征。 + + +自动化、可回归性。 +静默(Quiet)。没有发生错误的时候,就不说话。 +案例执行的安全受控。某个案例执行的失败,不会影响其他案例的正常运行。 + + +从分类来说,一般自动化测试我们分两个层次:一个是模块级的单元测试,一个是系统级的集成测试。 + +无论从什么角度来看,模块的单元测试都是重中之重的大事。原因是,单元测试的成本是最低的。 + +关于测试成本,我们可以从两个维度看。 + +其一,单元测试的实施成本低,最容易去做。不少高级语言比如 Go 语言甚至在语言内建的工具链上就直接支持。而集成测试虽然也有自动化的方法和支持工具,但是往往需要更高额的代价。 + +其二,减少问题发现的周期,进而降低问题的修复成本。单元测试将问题发现周期缩短,基本上在问题现场就发现问题,这降低了Bug的修复成本。如果问题在系统的集成测试阶段发现,那么从问题定位,到回忆当初实现这段代码时候的思路,到最终去解决掉它,必然需要多花费几倍甚至几十倍的时间。 + +因此,我们鼓励更严格的单元测试要求,更高的单元测试覆盖率,以尽可能把发现问题做到前头。 + +但仍然有不少公司在推广单元测试上遇到了不小的麻烦,推不起来。 + +对于这一点,我们认为首先要改变的是对推广单元测试这件事情的认知。我们不把推广单元测试看作是让大家去多做一件额外的事情,而是规范大家做单元测试的方法。 + +为什么这么说?因为实际上单元测试大家都会去做,很少有人会不经验证就直接交付。但是验证方式上可能有各种 “土” 方法,比如用 print,用可视化的界面做输入测试,用调试工具做单步跟踪等等。 + +但是这些方法代价其实一样不低,但是却不可回归,正确与否还需要人脑临时去判断。 + +更重要的是,这些方法最大的问题是没有办法去固化已知的 Bug,最大程度保留下来我们的测试案例。 + +这其实才是最核心的一个认知问题:我们应当重视我们的测试代码,它同样也是我们的开发成果,理应获得和模块的功能代码同等重要的地位,理应被保留下来。 + +解决了这个认知上的共识问题,自动化测试就能够被很好地推动起来。当前这方面的工具链已经非常完善,不至于会在工具上遇到太大的障碍。 + +持续构建,持续发布 + +其次,我们降低软件工程不确定性的方法是:持续构建,持续发布。 + +我们鼓励更小的发布。我们鼓励更短的发布周期,更高的发布频率。这能够让发布的负担降低到最低。 + +这种极度高频交付的机制与传统工程的质量管理机制迥异。但是它被证明是应对软件工程不确定性的最佳方式。为什么会这样? + +其一,交付的功能越少,因为错误而发生回滚的代价越低,影响面越小。如果我们同时发布了数十个功能,却因为某一个功能不达标而影响整体交付,这其实是降低了软件的功能交付效率。更好的方式显然是把这个出问题的功能回滚,把其他所有功能都放行。 + +其二,交付频率越高,我们对交付过程的训练越频繁,过程的熟练度越高,执行效率也越高。当交付成为一个自然习惯后,我们会把交付看作功能开发的一部分,而不是以前大家对研发的理解,认为做完功能就完事,后续上不上线与我无关。我们会鼓励更多把研发的绩效与功能线上的表现关联起来,面向客户价值,而非仅仅面向功能开发。 + +当然这种极度高频交付的机制,意味着它对软件工程的系统化建设有更高的要求。 + +当然,除了日构建与发布平台外,我们也需要在其中加入各种质量管理的抓手。比如: + + +自动化运行单元测试案例(unit test); +单元测试覆盖率检查(code coverage); +静态代码质量检查(lint); +人工的代码互审(code review); +灰度发布(gray release); +A/B 测试(A/B testing); +…… + + +结语 + +今天我们更加完整地探讨了软件工程的质量管理。整体来说,软件工程与传统工程在质量管理上的理念是迥异的,甚至往往是反其道而行之的。究其原因,还是因为软件工程的核心在于如何在高度的不确定性中找到确定性。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “开源、云服务与外包管理”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/74 开源、云服务与外包管理.md b/专栏/许式伟的架构课/74 开源、云服务与外包管理.md new file mode 100644 index 0000000..52c1c04 --- /dev/null +++ b/专栏/许式伟的架构课/74 开源、云服务与外包管理.md @@ -0,0 +1,153 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 74 开源、云服务与外包管理 + 74 | 开源、云服务与外包管理你好,我是七牛云许式伟。今天我们聊的话题是有关于分工的。 + +在这一讲之前,我们涉及到分工这个话题,基本上都局限于企业内部,且大多数情况下主要在同一个团队内部。但今天我们聊的是更大的分工:跨组织的分工与协作。 + +外包及其理想模型 + +在软件工程中,我们第一个接触的外部分工毫无疑问是外包。所谓外包,就是将我们软件的全部或部分模块的实现职能交给外部团队来做。 + +但是,软件工程项目的外包实际上成功率非常低。这背后有其必然性,它主要表现在以下这些方面。 + +其一,任务表达的模糊,双方容易扯皮。期望需求方能够把需求边界说清楚,把产品原型画清楚,把业务流程讲清楚,这非常难。有这样专业的需求表达能力的,通常软件工程水平不低,遇到这样的需求方,绝对应该谢天谢地。这种专业型的甲方,它大部分情况下只发生在项目交付型外包,而非产品功能外包。更多的产品外包,一般是甲方不太懂技术,需要有团队替自己把事情干了,他好拿着产品去运营。 + +其二,交付的代码质量低下,长期维护的代价高。软件工程不是项目,它都需要长长久久地运行下去。但是接包方的选择相当重要。因为接包方的质量相当参差不齐,遇上搬砖的概率远高于设计能力优良的团队。事实上,有良好设计能力的团队,多数情况下也不甘于长期做外包。 + +其三,项目交接困难,知识传承效率很低。软件工程并非普通的工程,就算交付的结果理想,项目交接也非常困难。所以外包项目第一期结束后,如果运营得好,往往项目还继续会有第二期、第三期。这里的原因是你只能找同一拨人做,如果换一波人接着做,考虑到知识传承效率低下,往往需要很长的一个交接周期。 + +那么,外包的理想模型是什么? + +上面我们已经说到,外包在通常情况下,专业的甲方需要说清楚需求,这样双方就没有分歧。但是,更好的做法其实不是外包需求,而是外包实现。 + +也就是说,作为专业的甲方,我自己做好需求分析,做好系统的概要设计。进一步,我们把每个模块的业务范畴与接口细化下来。我们以此作为外包边界。假设分了N个模块,我们可以把它们平均分给若干个接包方。 + +这种方式的外包,甲方相当于只留了架构师团队,实现完全交给了别人。但是它与普通的外包完全不同,因为根本不担心知识传承的问题。每个模块的接包方对甲方来说就真的只是干活的。 + +接包方拿到的是模块的规格说明书。他要做的是模块的详细设计的实现部分,其中最为核心的是数据结构设计。对于服务端,甲方可以规定所采用的数据库是什么,但是把表结构的设计交出去。 + +进一步,如果模块的外包说明书中还规定了单元测试的案例需要包含哪些,那么这个模块发生设计偏离的可能性就很低。 + +外包的验收需要包含模块的实现设计文档,里面描述了数据结构+算法。另外,单元测试部分,每个测试场景,也填上对应的测试函数的名称。 + +实际会有人这样去外包么? + +我不确定。但我们可以把它看作一种分工的假想实验。这个假想实验可以充分说明架构师团队的重要性。有了一个好的架构师团队,他们设计合适的系统架构,对每个模块的规格都做了相应的定义,他们验收模块的实现。 + +这样,项目就可以有条不紊地展开。甚至,研发进度可以自如控制。嫌项目进展太慢?找一倍的接包方,就可以让工程加速一倍。 + +所以,这个外包假想实验也说明了一点:我们的平常项目之所以进度无法达到预期,无他,团队缺乏优秀的架构师而已。 + +让我们把软件工程看作一门科学。我们以[工程师思维]的严谨态度来看它。我们减少项目中的随意性,把架构设计的核心,模块规格,也就是接口,牢牢把控住。这样,项目的执行风险就完全消除了。 + +哦不,还有一个最大的执行风险没有消除。我怎么证明这个系统架构的分解是对的?不会出现每个模块做好了,但是最终却拼不起来? + +我们前面在 “[架构:系统的概要设计]” 这一讲中实际上已经谈过这事的解决方法:系统设计的产出要有源代码,它是项目的原型。关键模块有 mock 的实现,业务系统的关键 UserStory 都串了一遍,确保系统设计的正确性。 + +这个假想实验是有趣的,它可以让你想明白很多事情。甚至可以把它看作理解这个专栏的架构思维核心思想的钥匙。 + +我希望,它不只是一个假想实验。 + +开源与众包 + +我们把话题拉回到跨组织的分工。 + +除了传统的外包外,在软件工程中出现的第二类外包是众包,它以开源这样一个形态出现。 + +从分工角度,开源的核心思想是让全社会的程序员共同来完成一个业务系统。 + +开源的优势非常明显。对于一个热门的开源项目,它的迭代进度是非常惊人的,因为它撬动的资源太大了。 + +但不是开源了就能够获得这样的好处。 + +虽然成功的开源项目风风火火,但是我们也应该意识到,对于那些并没有得到关注的开源项目,它们的迭代速度完全无法保障。最终,你可能还是只能靠自己的团队来完成它的演进。 + +从这个意义上看,开源是一种商业选择。你得持续经营它。没有经营的开源项目不会成功。你需要宣传它,你自己也得持续迭代它,你还要为它拉客户。有客户的开源项目自然就有了生命力。 + +另外,开源这种形态,注定了它只能做大众市场。如果一个业务系统它的受众很少,就比较难通过开源获得足够的外部支持。 + +所以绝大部分成功的开源项目,都属于基础设施性质的业务系统,有极其广泛适用的场景。例如,语言、操作系统、基础库、编程框架、浏览器、应用网关、各类中间件等等。我们这个架构课重点介绍的内容,大部分都有相应的开源实现。 + +开源对信息科技的影响极其巨大,它极大地加速了信息科技前进的进程,是全球共同精诚协作的典范。 + +没有参与过开源的程序员是需要心有遗憾的。开源沉淀下来的协同方法与工作流,今天被无数公司所借鉴。 + +没有开源,我们无法想象这件事情:那么多形形色色的企业,今天其中绝大部分,它们的软件工程协同方法与业务流竟然如此相似。 + +这是开源带来的另一种无形资产。 + +如果大家没有忘记的话,可能能够回忆起来,在谈完软件工程的宏观视角之后,我首先聊的是 “[团队的共识管理]”。为什么这很重要?因为它是团队协作效率的最大基础。如果连对协作的工作流都没有共识,那团队真的是一盘散沙了。 + +今天我们几乎不会遇到工作方式上的问题,不是别的原因,是开源给予我们的礼物。它让全球的程序员、全球的科技企业,都养成了一模一样的工程习惯。 + +云计算与服务外包 + +云服务是新的跨组织分工的形态。无论是传统的外包,还是开源的众包,它们都属于源代码外包。这类外包的共同特点是,它们不对结果负责。 + +对于传统外包,项目验收结束,双方一手交钱一手交货,至于用得好不好,那是甲方自己的事情。 + +对于开源软件来说,那更是完全免责,你爱用不用,用了有什么问题责任自负。当然有很多公司会购买开源软件的商业支持,这不难理解,除了有人能够帮助我一起完成项目上线外,最重要的是要有人能够给我分担出问题的责任。 + +互联网为跨组织协同带来了新的机会。我可以24小时为另一个组织服务,而无需跑到对方的办公室,和他们团队物理上处在一起。 + +这就是云计算。云计算从跨组织协同的角度来看,不过是一种新的交付方式。我们不再是源代码交付,而是服务交付。所以,你也可以把云计算看着一种外包,我们称之为服务外包。 + +大部分的基础设施,都可以以服务外包的方式进行交付。这中间释放的生产力是惊人的。 + +一方面,云计算与传统外包不同,它对结果负责,有服务 SLA 承诺。一旦出问题,问题也可以由云服务提供方自己解决,而无需业务方介入,这极大降低了双方的耦合,大家各司其职。 + +另一方面,它简化了业务方的业务系统,让它得以能专注自己真正的核心竞争力的构建。 + +站在生产效率角度看,不难理解为什么我们会坚信云服务是未来必然的方向。 + +外包方式的选择 + +任何企业都存在于社会生态之中,我们无法避开组织外部的分工协同问题。 + +怎么选择跨组织的协同方式? + +在七牛,自成立以来我们就一直有一句话谈我们对跨组织协同的看法: + + +我们尽可能不要做太多事情。非核心竞争力相关的,能够外包的我们尽可能外包。 + +在外包选择上,我们优先选择云服务,次选开源,最后才考虑传统的外包。 + + +这句话有它一定的道理,但也有它模糊的地方。 + +首先是关于 “核心竞争力相关”。我们并没有太清晰地去定义什么样的东西是我们核心竞争力相关,什么不相关。 + +一些程序员对此理解可能会比较 “技术化”,认为业务系统的核心模块就是核心竞争力。与它相关的东西就是核心竞争力相关。 + +但更合理的视角不是技术视角,而是业务视角。我们每一家企业都是因为服务客户而存在。所以,与服务客户的业务流越相关,越不能外包,而是要自己迭代优化,建立服务质量与效率的竞争优势。 + +另外,外包的选择需要非常谨慎。很多开发人员都有随意引用开源项目的习惯,这一定程度上给项目带来了不确定的风险。 + +我一直认为,开源项目的引入需要严格把关。严谨来说,开源项目引入大部分情况下是属于我说的 “基础架构” 选择的范畴,这同样是架构师团队需要承担的重要职责,一定要有正规的评估流程。 + +结语 + +今天我们聊的话题是跨组织的分工与协同。在形态上,我们可以分为:传统外包、开源与云服务。当然还有就是我们今天没有讨论的使用外部商业软件。 + +从形态来说,商业软件很接近传统外包,但是从它的边界来说,因为商业软件往往有明确的业务边界,所以在品质上会远高于外包。当然定制过于严重的商业软件例外,它在某种程度上来说退化为了传统外包。 + +在外包方式的选择上,我们的建议是: + + +我们尽可能不要做太多事情。非核心竞争力相关的,能够外包的我们尽可能外包。 + +在外包选择上,我们优先选择云服务,次选开源,最后才考虑传统的外包。 + + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “软件版本迭代的规划”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/75 软件版本迭代的规划.md b/专栏/许式伟的架构课/75 软件版本迭代的规划.md new file mode 100644 index 0000000..43cd503 --- /dev/null +++ b/专栏/许式伟的架构课/75 软件版本迭代的规划.md @@ -0,0 +1,175 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 75 软件版本迭代的规划 + 75 | 软件版本迭代的规划你好,我是七牛云许式伟。 + +到今天为止,我们专栏的话题主要集中在软件工程的质量与效率上。我们在专栏的开篇中就已经明确: + + +从根本目标来说,软件架构师要对软件工程的执行结果负责,这包括:按时按质进行软件的迭代和发布、敏捷地响应需求变更、防范软件质量风险(避免发生软件质量事故)、降低迭代维护成本。 + + +但是今天,我们将探讨一个更高维的话题:软件版本迭代的规划。后续我们简称为 “版本规划”。简单说,就是下一步的重点应该放在哪里,到底哪些东西应该先做,哪些东西应该放到后面做。 + +这是一个极其关键的话题。它可以影响到一个业务的成败,一个企业的生死存亡。方向正确,并不代表能够走到最后,执行路径和方向同等重要。 + +那么,版本规划的套路是什么? + +探讨这个问题前,我想先看一个实际的案例。这个案例大家很熟悉:Go 语言的版本迭代。 + +我们从 Go 语言的演进,一起来看看 Go 团队是如何做软件版本迭代规划的。这有点长,但是细致地琢磨对我们理解版本规划背后的逻辑是极其有益的。 + +Go 版本的演进历史 + +Go 语言的版本迭代有明确的周期,大体是每半年发布一个版本。 + +Go 1.0 发布于 2012 年 3 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1。它是 Go 语言发展的一个里程碑。 + +在这个版本,Go 官方发布了兼容性文档:https://tip.golang.org/doc/go1compat,承诺会保证未来的 Go 版本将保持向后兼容。也就是说,将始终兼容已有的代码,保证已有代码在 Go 新版本下编译和运行的正确性。 + +在 Go 1.0 之前,Go 在持续迭代它的使用范式,语法规范也在迭代优化。比如 os.Error 到了 Go 1.0 就变成了内置的 error 类型。这个改变看似很小,但实际上是一个至关重要的改变。因为 Go 推荐可能出错的函数返回值都带上 err 值,如果 os.Error 不改为内建类型,就会导致很多模块不得不因为 os.Error 类型而依赖 os 包。 + +Go 1.0 最被诟病的问题是它的 GC 效率。相比 Java 近 20 年的长期优化,其成熟度只能以稚嫩来形容。 + +与此相对应的是,Go 从一开始就是一门极度重视工程的语言。Go 1.0 就已经有非常完善的工程工具支持。比如: + + +单元测试:go test; +文档:go doc; +静态检查工具:go vet; +性能 Profile 工具: go tool pprof。 + + +Go 1.1 发布于 2013 年 5 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.1。这个版本主要专注于语言内在机制的改善和性能提升(编译器、垃圾回收、map、goroutine调度)。改善后的效果如下: + + + +这个版本还发布了一个竞态探测器(race detector),它对 Go 这种以高并发著称的语言显然是重要的。详细可参考 Go 官方博客文章:https://blog.golang.org/race-detector。 + +Go 1.2 发布于 2013 年 12 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.2。这个版本发布了单元测试覆盖率检查工具:go tool cover。详细可参考 Go 官方博客文章:https://blog.golang.org/cover。 + +Go 1.3 发布于 2014 年 6 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.3。这个版本栈的内存分配引入了连续段(contiguous segment)的分配模式,以提升执行效率。之前的分页式的栈分配方式(segment stack)存在频繁地分配/释放栈段导致栈内存分配耗时不稳定且效率较低。引入新机制后,分配稳定性和性能都有较大改善。 + +Go 1.3 还引入了 sync.Pool,即内存池组件,以减少内存分配的次数。标准库中的 encoding/json、net/http 等都受益于它带来的内存分配效率提升。另外,Go 还对 channel 进行了性能优化: + + + +Go 1.4 发布于 2014 年 12 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.4。从功能来说,这个版本最大的一件事情是增加了 Android/iOS 支持(http://golang.org/x/mobile),Gopher 可以使用 Go 编写简单的 Android/iOS 应用。 + +但实际上如果从重要程度来说,Go 1.4 最重要的变化是将之前版本中大量用 C 语言和汇编语言实现的 runtime 改为用 Go 实现,这让垃圾回收器执行更精确,它让堆内存的分配减少了 10~30%。 + +另外,Go 1.4 引入了 go generate 工具。这是在没有泛型之前解决重复性代码问题的方案。详细见https://blog.golang.org/generate。 + +Go 1.5 发布于 2015 年 8 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.5。这个版本让 Go 实现了自举。这让GC 效率优化成为可能。所以在这个版本中,GC 被全面重构。由于引入并发垃圾回收,回收阶段带来的延迟降低了一个数量级。 + +这个版本还有一个很重要的尝试,是引入了 vendor 机制以试图解决 Go 模块的版本管理问题。自从 Go 解决了 GC 效率后,Go 版本管理就成了老大难问题。下图是 Go 社区对 Go 面临的最大挑战的看法: + + + +当然后来事实证明 vendor 机制并不成功。 + +另外,Go 1.5 引入了 go tool trace,通过该命令我们可以实现执行器的跟踪(trace)。详细参考 https://golang.org/cmd/trace/。 + +Go 1.6 发布于 2016 年 2 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.6。垃圾回收器的延迟在这个版本中进一步降低。如下: + + + +从功能上来说,这个版本支持了 HTTP/2。 + +Go 1.7 发布于 2016 年 8 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.7。这个版本有一个很重要的变化,是 context 包被加入标准库。这事之所以重要,是因为它和 os.Error 变成内建的 error 类型类似,在网络接口中,context 是传递上下文、超时控制及取消请求的一个标准设施。 + +另外,Go 编译器的性能得到了较大幅度的优化,编译速度更快,二进制文件size更小,有些时候幅度可达 20~30%。 + +Go 1.8 发布于 2017 年 2 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.8。GC 延迟在这个版本中进一步得到改善,延迟时间降到毫秒级别以下。 + +另外,这个版本还大幅提升了defer的性能。如下: + + + +Go 1.9 发布于 2017 年 8 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.9。这个版本引入了 type alias 语法。例如: + +type byte = uint8 + + +这实际上是一个迟到的语法。我在 Go 1.0 就认为它应该被加入了。另外,sync 包增加了 Map 类型,以支持并发访问(原生 map 类型不支持)。 + +Go 1.10 发布于 2018 年 2 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.10。在这个版本中,go test 引入了一个新的缓存机制,所有通过测试的结果都将被缓存下来。当 test 没有变化时,重复执行 test 会节省大量时间。类似地,go build 也维护了一个已构建的包的缓存以加速构建效率。 + +Go 1.11 发布于 2018 年 8 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.11。这个版本最重要的新功能是 Go modules。前面我们说 Go 1.5 版本引入 vendor 机制以解决模块的版本管理问题,但是不太成功。这是 Go 团队决定推翻重来引入 module 机制的原因。 + +另外,这个版本引入了一个重要的试验功能:支持 WebAssembly。它允许开发人员将 Go 源码编译成一个兼容当前主流浏览器的 wasm 文件。这让 Go 作为 Web 开发语言成为可能。 + +Go 1.12 发布于 2019 年 2 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.12。这个版本的 go vet 命令基于 analysis 包进行了重写,使得 go vet 更为灵活并支持 Gopher 编写自己的 checker。详细参考 “How to Build Your Own Analyzer” 一文。 + +Go 1.13 发布于 2019 年 8 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.13。这个版本的 sync.Pool 性能得到进一步的改善。当 GC 时,Pool 中对象不会被完全清理掉。它引入了一个 cache,用于在两次 GC 之前清理 Pool 中未使用的对象实例。 + +另外,这个版本的逃逸分析(escape analysis)被重新实现了,这让 Go 更少地在堆上分配内存。下图是新旧逃逸分析的基准测试对比: + + + +另外,Go modules 引入的 GOPROXY 变量的默认值被改为: + +GOPROXY=https://proxy.golang.org,direct + + +但在国内无法访问 Go 官方提供的 proxy.golang.org 站点。建议改为: + +export GOPROXY=https://goproxy.cn,direct + + +这里 https://goproxy.cn 由七牛云赞助支持。 + +Go 版本迭代的背后 + +Go 语言的版本迭代的规划非常值得认真推敲与学习。 + +Go 的版本迭代还是比较高频的,但是有趣的是,在 Go 1.0 版本之后,语言本身的功能基本上已经非常稳定,只有极少量的变动。比如 type alias 这样的小特性,都已经可以算是关键语法变化了。 + +那么,这些年 Go 语言都在变化些什么? + +其一,性能、性能、性能!尤其在 GC 效率这块,持续不断地优化。为了它,大范围重构 Go 的实现,完成了自举。其他还有很多,比如连续栈、内存池(sync.Pool)、更快的编译速度、更小的可执行文件尺寸。 + +其二,强化工程能力。各种 Go tool 的增加就不说了,这其中最为突出的就是 Go 模块的版本管理,先后尝试了 vendor 和 module 机制。 + +其三,标准库的能力增强,如 context,HTTP 2.0 等等。这块大部分比较常规,但 context 的引入可以算是对网络编程最佳实践的一次标准化过程。 + +其四,业务领域的扩展。这块 Go 整体还是比较专注于服务端领域,只是对 Android、iOS、WebAssembly 三个桌面平台做了经验性的支持。 + +如何做版本规划 + +蛮多技术背景的同学在做版本规划的时候,往往容易一开始就陷入到技术细节的泥潭。但其实对于一个从 0 到 1 的业务来说,首先应该把焦点放到什么地方,这个选择才至关重要。 + +Go 语言在这一点上给出了非常好的示范。它首先把焦点放在了用户使用姿势的迭代上。凡与此无关的事情,只要达到及格线了就可以先放一放。这也是 Go 为什么一上来虽然有很多关于 GC 效率的吐槽,但是他们安之若素,仍然专注于用户使用姿势的迭代。 + +但是一旦语言开始大规模推广,进入从 1 到 100 的扩张阶段,版本迭代的关注点反而切换到了用户看不见的地方:非功能性需求。生产环境中用户最关心的指标,就成了 Go 团队最为关注的事情,日复一日,不断进行迭代优化。 + +这是很了不起的战略定力:知道什么情况下,最该做的事情是什么。 + +那么,遇到重大的客户需求,对之前我们培养的用户习惯将形成重大挑战怎么办?一些人可能会习惯选择快速去支持这类重大需求,因为这些需求通常很可能听起来很让人振奋。 + +其实 Go 语言也遇到了这样的需求:泛型的支持。 + +泛型被 Go 团队非常认真地对待。可以预期的是,Go 2.0 一定会支持泛型。但是,他们并没有急着去实现它。Go 社区不少人在 Go 1.9 的时候,很激动地期待着 Go 2.0,期待着泛型,但是 Go 出来了 Go 1.10,甚至到现在的 Go 1.13。 + +显然,泛型被放到了一个旁路的版本。这个旁路版本独立演化直到最终验证已经成熟,才会被合并到 Go 1.x 中。这时,Go 2.0 就会诞生了。 + +这其实才是正确响应会招致巨大影响面的功能需求的姿势。 + +客户是需要尊重的。而尊重客户的正确姿势毫无疑问是:别折腾他们。 + +结语 + +今天我们聊的话题是版本迭代的规划。在不同阶段,版本迭代的侧重点会有极大的不同。从 0 到 1 阶段,我们验证的是用户使用姿势,性能并不是第一位的。但是进入扩张阶段,产品竞争力就是关键指标,这时候我们迭代的是用户价值最大的,也是用户真正最在乎的那部分。 + +遇到会对产品产生巨大冲击的需求,头脑别发热,谨慎处理。回到从 0 到 1 阶段的方法论,在少量客户上先做灰度。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “软件工程的未来”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/76 软件工程的未来.md b/专栏/许式伟的架构课/76 软件工程的未来.md new file mode 100644 index 0000000..b43830b --- /dev/null +++ b/专栏/许式伟的架构课/76 软件工程的未来.md @@ -0,0 +1,86 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 76 软件工程的未来 + 76 | 软件工程的未来你好,我是七牛云许式伟。现在正值中国年,我在这里祝大家新年快乐。开开心心过大年的同时,注意安全第一,出门记得戴上口罩,少去人员聚集的地方。 + +好,那我们开始今天的学习,今天我们想聊聊软件工程的未来。 + +软件工程是一门非常年轻的学科,相比其他动辄跨世纪的自然科学而言,软件工程只有 50 年的历史。只有如此短暂实践的科学,今天我们来探讨它的未来,条件其实还并不算太充分。 + +但是我们的宗旨就是要每个领域都应该谈清楚过去(历史)与未来(趋势判断),所以今天不妨也理性来探讨一下。 + +在 “[软件工程的宏观视角]” 一讲中,我们引入了下图来表达软件工程的瀑布模型: + + + +在这样一个模型里面,涉及的角色分工已经非常多: + + +产品经理; +架构师; +开发工程师; +质量保障(QA)工程师; +网站可靠性工程师(SRE); +…… + + +但这还只是常规描述的工种。实际的分工要细致很多。更不要说对特殊的领域,比如企业服务,也就是大家常说的 2B 行业,它的基本过程是这样的: + + + +比之纯粹的产品研发上线过程,它多了单个客户的跟进与落地实施过程,也由此引入更多的角色分工,比如:售前工程师、交付(实施)工程师、售后工程师、项目经理等。 + +未来软件工程会走向何方? + +首先 “快速变化” 是软件工程的自然属性,其 “不确定性” 也只能抑制而无法消除。 + +但显而易见的是,软件工程的问题最终还是由软件解决。事实上今天很多问题已经解决得很好,比如源代码的管理。我们经历了 cvs、svn,最终到今天的 git。基本上开发人员的协同问题已经形成非常约定俗成的方法论,并以软件或云服务的方式被固化下来。 + +今天,线上服务管理正如火如荼的发展。假以时日,不需要多久之后,一个全新的时代开启,我们中大部分人不必再为线上服务的稳定性操心。关于这块更详细的讨论,可以参考第四章 “服务治理篇”。 + +需求管理与测试这块也已经得到很好的解决。唯一比较遗憾就是是界面(UI)相关的测试虽然也有相关的工具链,但当前的普及率仍然极低。 + +这可能与大部分公司都较难保证界面的稳定性有关。如果我们经常变动界面,这就如同我们经常调整一个模块对外的接口规格一样,必然导致相关的测试案例编译通不过,或者测试通不过。这会让人沮丧,进而丧失对实现界面(UI)测试自动化的信心。自动化测试极其依赖被测模块接口的稳定性,这是我们今天常规自动化测试方法的限制。 + +当然另一方面,这也与界面测试相对高维,大部分公司的质量保障水平都还没有到达这个级别有关。从现实来看,虽然单元测试方法论已经极其成熟,但是仍然有不少企业在推行中遇到不少障碍。 + +可以预期,随着企业的平均工程水平逐步提升,最终会形成越来越多的有效的界面测试最佳实践的方法论,并得以大范围的推广。 + +从全局来看,今天软件工程已经形成较为成熟的分工。但各类分工的最佳实践与软件系统,仍然是相对孤立的。 + +这一定程度上也与软件工程还很年轻有关。从软件工程的软件系统发展来说,可以预期的是,未来一定会形成更加一体化的系统,上一道 “工序” 的输出就是下一道 “工序” 的输入。 + +但是今天一些 “工序” 的输出仍然是人肉进行传递,甚至没有标准化的仓库管理它。例如,产品经理输出的产品界面设计原型、架构师输出的架构设计文档,其传递过程仍然有极大的随意性。 + +但是,软件工程的最大不确定性就来源于 “设计” 类工作,包括产品设计与软件的架构设计。今天虽然产品设计和架构设计也都有一些独立的工具,但普及度与刚才说的开发与测试类工程实践相比完全是小巫见大巫。 + +这是可以理解的,产品经理与架构师在软件工程中属于小众群体,其培养难度极高,很多经验也很难形成传统意义上的 “知识点” 来传递。所以真正意义上合格的产品经理与架构师是比较少的,和程序员(软件开发工程师)的规模完全无法相比。 + +就拿架构师这个岗位举例。架构师的职责是什么,架构师工作的方法论是什么、培养架构师的方法论又是怎样的,这些今天并没有一个被广泛接受的实践。 + +为什么我会写这个架构课专栏,以及为什么成立七牛大学开启线下的架构师实战训练营,也是希望能够在一定程度上找到这些问题的最佳答案。 + +而事实上,产品经理的培养有更高的难度。严格意义上来说,成为产品经理前,首先应该成为架构师。我这个观念可能与大部分人的常识相悖,但是我个人对此深信不疑。 + +软件工程的未来发展会怎样,细节上很难给出确定性的判断。但是,我们相信,软件工程极大成熟的标志,是一体化的软件工程支撑系统,和高效的人才培养体系。包括今天仍然极为稚嫩的架构师培养体系,和产品经理培养体系,都应该得到了极大的完善。 + +到那个时候,软件工程就成为了一门真正成熟的科学。 + +结语 + +软件工程项目迭代快速、充满变化、充满不确定性。这使得软件工程成为一门极其独特魅力的科学。今天这门科学仍然还非常年轻,其发展只能以日新月异来形容。 + +软件工程的未来,它的成熟不单单是工程方法论和业务系统软件的成熟,也需要包括人才培养体系的成熟。因为,软件工程的不确定性与它充满设计与创造有关,人的主观能动性是它的优势,但也意味着不确定性无法得到彻底的消除。 + +我们要做的,只能说在大量的不确定性中,找到尽可能多的确定性。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。至此,本章 “软件工程篇” 已经到尾声阶段,下一讲我们将对本章的内容进行回顾与总结。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/77 软件工程篇:回顾与总结.md b/专栏/许式伟的架构课/77 软件工程篇:回顾与总结.md new file mode 100644 index 0000000..8c79dd9 --- /dev/null +++ b/专栏/许式伟的架构课/77 软件工程篇:回顾与总结.md @@ -0,0 +1,116 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 77 软件工程篇:回顾与总结 + 77 | 软件工程篇:回顾与总结你好,我是七牛云许式伟。 + +我们架构课的最后一章软件工程篇到此就要结束了。今天我们就本章的内容进行回顾与总结。 + +架构师并不是一个纯技术岗位。我们从软件工程的视角来看,架构师的职责就是要对软件工程的执行结果负责,这包括:按时按质进行软件的迭代和发布、敏捷地响应需求变更、防范软件质量风险(避免发生软件质量事故)、降低迭代维护成本。 + + + +软件工程所覆盖的范畴非常广泛。从开始的需求与历史版本缺陷,到新版本的产品设计,到架构设计,到编码与测试,到最终的产品发布,到线上服务的持续维护。 + +还有贯穿整个工程始终的,是不变的团队分工与协同,以及不变的质量管理。 + +我们这个专栏并没有打算站在完整的软件工程角度去谈,更多还是从架构师与软件工程的关联入手。 + +本章的内容大体如下图所示。 + + + +软件工程是一项团体活动,大家有分工更有协同。不同的个体因为能力差别,可以形成十倍以上的生产力差距。而不同的团体更是如此,他们的差距可能更上一个数量级,达到百倍以上的生产力差距。 + +百倍以上的差距是什么概念?这就是说,一个团队只需要三四天做出来的东西,另一个团队可能需要一年才能做出来。两者之间的差距之大,只能用天壤之别来形容。 + +个人与个人的差距,你可以认为是技术上的能力差距的反映。但团队与团队的差距,不是简单的技术上的能力差距,而是有着更为深刻的原因。 + +高效团队的效率,核心体现在以下两个方面: + + +团队开发一个新功能的效率。它体现的是架构的老化程度。 +团队新人的融入效率。新人多快的速度可以融入到团队,理解业务系统的现状及团队的做事方式。 + + +开发新功能的效率,主要取决于架构的优劣。这初听起来是一项纯技术上的事情。但如果我们站在时间维度上长达数年甚至数十年的软件工程的角度看,能够维持架构设计的持续优异,这绝非某个人的技术能力可以做到的事情,而是要靠团队共同的坚持。 + +而从新人融入效率看,更非技术能力所能够简单囊括,而是仰仗团队对业务传承的坚持。 + +这些东西的背后,关乎的都是有关于协同的科学。 + +有的团体像一盘散沙,充其量可以叫团伙。有的团体则有极强的凝聚力,整个团队上下同心,拧成一股绳,这种团体才是高效率组织,是真正意义上的团队。 + +共识是团队效率的基础。 + +从软件工程角度来说,产品设计和架构设计是团队最大的共识。架构过程就是一次团队共识确认的过程,从项目的混沌之初,到团队形成越来越清晰且一致的视图( Picture)。 + +高效团队往往还有极高的团队默契,这让他们无论是维护老项目还是做什么新项目都如鱼得水。团队默契可以包含很多东西,比如: + + +共同的目标; +团队的做事态度与价值观; +编码规范; +架构设计文档的模板; +软件工程的方法论; +基础架构及技术选型; +…… + + +对于一个团队新人来说,融入一个团队或一个项目的基础过程就是阅读别人写的源代码。既有的文档越清楚,新人阅读代码的障碍就越小,融入的速度就越快。 + +文档要怎样才能把问题说清楚? + +文档传递的是思维方式。大多数程序员不善于写文档,甚至讨厌写文档。这背后的根源不在于文档本身,而在于有效的思维表达方式,这需要长期的训练。 + +软件工程的各个环节都有其交付物。理想情况下,上一个环节的输出是下一环节的输入。软件系统的质量管理一般从这些交付物的管理入手。例如:交付物的版本管理、单元测试、持续构建,灰度发布,等等。 + +从更宏观的视角看,我们还涉及人力资源规划的问题。什么东西应该外包出去,包给谁?软件版本的计划是什么样的,哪些功能先做,哪些功能后做? + +这些选择非常非常重要。因为他们属于业务架构的顶层设计。 + +除了传统意义上的外包外,外包方式还有:开源(众包)、云服务(服务外包)、商业软件(产品外包)。在外包方式的选择上,我们的建议是: + + +我们尽可能不要做太多事情。非核心竞争力相关的,能够外包的我们尽可能外包。 + +在外包选择上,我们优先选择云服务,次选开源,最后才考虑传统的外包。 + + +当然,哪些事情是非核心竞争力相关,这一点不同公司可能判断不尽相同。但基本的判断逻辑是,越与我们面向用户所提供的业务流程相关,越靠近企业的核心竞争力,也就越不能外包。 + +软件版本迭代的规划需要根据业务的发展阶段而定。在不同阶段,版本迭代的侧重点会有极大的不同。 + +从 0 到 1 阶段,我们验证的是用户使用姿势,也就是产品设计的规格。这时性能并不是第一位的。 + +但是进入扩张阶段,产品竞争力就是一些用户关心的关键指标。这时候我们迭代的不再是用户使用姿势,它已经非常稳定。我们迭代的往往是看不见的非功能性需求,是那些用户真正最在乎的部分。 + +而遇到会对产品产生巨大冲击的功能需求,头脑别发热,谨慎处理。回到从 0 到 1 阶段的方法论,在少量客户上先做灰度。 + +结语 + +软件工程还很年轻,只有 50 年的历史。有关于软件工程的系统与方法论都仍然在快速演化与迭代中。 + +这意味着意我们不必墨守成规。要勇于探索,勇于打破固有的惯例,去建立新的方法论,新的惯例。 + +但需要强调的是,打破惯例不是胡闹,不是要做不尊重科学的 “野蛮人”。今天仍然有那么一批工程师,人数还不在少数,他们随心所欲、任性而为,不喜欢写架构设计文档,不喜欢写单元测试,不喜欢代码互审(code review)。 + +我们首先需要尊重团队协同的科学,在尊重的基础上去探索新的更高效的协同方法论。 + +很早之前我说过以下这段话,它很长一段时间里,被贴在某家公司墙上: + + +严谨并非创新的对立面,而是创新的重要基础。每个人都有灵光乍现的时刻,但是唯有那些拥有严谨的科学态度的人才能抓住它,把它变成现实。 + + +我想,它非常适合作为软件工程篇的结束语。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。至此,本章 “软件工程篇” 结束,下一讲将结束本专栏的内容。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/加餐 怎么保障发布的效率与质量?.md b/专栏/许式伟的架构课/加餐 怎么保障发布的效率与质量?.md new file mode 100644 index 0000000..ef06b04 --- /dev/null +++ b/专栏/许式伟的架构课/加餐 怎么保障发布的效率与质量?.md @@ -0,0 +1,178 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 加餐 怎么保障发布的效率与质量? + 加餐 | 怎么保障发布的效率与质量?你好,我是七牛云许式伟。 + +为什么要有发布流程? + +在 “[49 | 发布、升级与版本管理]” 一讲中我们提到过: + + +变更是故障之源。 + + +这种由于业务需要而主动发起的软硬件升级与各类配置变更,我们可以统一称之为发布。例如: + + +更换交换机的类型,或升级版本。 +更换所依赖的基础软件,或升级版本。基础软件包括操作系统、负载均衡、数据库等等。 +升级业务软件本身。 +调整软硬件环境的配置项。 + + +特殊地,如果集群的服务对扩容缩容有很好的自动化支持,可以非常便捷地增加或减少服务器,那么这种情况虽然发生了集群的变化,我们可以不把它看作变更,不走发布相关的流程。尤其在硬件已经完全池化时,增加、减少服务器可能是个非常标准化且低成本的操作。 + +我们通常说的 “版本发布”,往往侧重点是在升级业务软件的版本,这是发布中最常发生的情况,当然也是我们最为关注的。 + +传统的软件公司的发布周期往往很长,有几个月甚至有的是按年来计算。而互联网公司的发布周期则非常不同。之所以快速发布、快速迭代变得简单的原因是,它们仅仅需要在服务器端发布,而不需要发布到每个使用者的电脑上。 + +一个每三年发布一次新产品的公司不需要详细的发布流程。因为发布的频率太低了,发布流程的优化能够带来的收益太小。 + +但是如果我们每天都在发布,甚至每天发布很多次,那么如此高频的发布速度,就要求我们创建和维护一个效率与质量都能够兼顾的精简的发布流程。 + +一个精简的发布流程,通常需要有发布平台这样的基础设施,把发布过程中反复遇到的问题对应的解决方案固化到系统中。 + +但是系统并不能解决所有的发布问题。变更终究是存在未知的新东西,需要人工进行检查判断。为此,SRE 部门往往还建立了一个专门的团队负责发布,即发布协调小组。团队成员称为 “发布协调工程师(Launch Coordination Engineering,LCE)”。 + +发布协调小组会针对每个业务,维护一个该业务的 “发布检查列表”,包括针对每次发布需要检查的常见问题,以及避免常见问题发生的手段。只有在发布检查表中的检查点都得到了确认,才会给版本发布放行。 + +这个列表在实践中被证实,它是保障发布可靠性的重要工具。 + +建立在系统之上的灰度发布 + +除了 “发布检查列表”,我们还有一个至关重要的保障发布质量的做法:灰度发布。 + +不管你如何小心,发布检查做得多全面,仍然只是在尽可能减少发布的风险,而不是消除。任何改动都具有一定的危险性,而任何危险性都应该被最小化,这样才能保障系统的可靠性。 + +在小型的测试环境上测试成功的变更,不见得在生产环境就没有问题,更何况从 SRE 的角度,测试的覆盖率也是不能假设的。 + +任何发布都应该灰度进行,并且在整个过程中还需要穿插必要的校验步骤。刚开始,新的服务可能会在某个数据中心的一台或几台机器上安装,并且被严密监控一段时间。如果没有发现异常,新版本会在更多台机器上安装并再次监控,直至最后完成整个发布过程。 + +发布的第一阶段通常被称为 “金丝雀”。这和煤矿工人带金丝雀下矿井检测有毒气体类似,通过使用这些“金丝雀” 服务线上流量,我们可以观察任何异常现象的发生。 + +“金丝雀” 测试适用于正常的软件版本发布,也适用于配置项的变更。负责配置变更的工具通常都会对新启动的程序监控一段时间,保证服务没有崩溃或者返回异常。如果在校验期间出现问题,系统会自动回退。 + +灰度式发布的理念甚至并不局限于软件和服务的发布。例如,我们商业上的高成本的运营活动,往往会先选择一到两个地区先做实验,然后再把成功经验复制到全国各地。 + +所以灰度发布思想的一个自然延伸是做功能开关,也就是大家熟悉的 AB 测试。很多东西在测试环境中无法模拟时,或者在真实环境中仍然存在不可预知的情况时,灰度机制就非常有用了。 + +不是所有的改动都可以一样对待。有时我们仅仅是想检查某个界面上的改动是否能提升用户感受。这样的小改动不需要几千行的程序或者非常重量级的发布流程。我们可能希望同时测试很多这方面的改动。 + +有时候我们只是想要知道是否有足够多的用户会喜欢使用某个新功能,就通过发布一个简单的原型给他们测试。这样我们就不用花费数个月的时间来优化一个没人想要使用的功能。 + +通常来说,这类 AB 测试框架需要满足以下几个要求: + + +可以同时发布多个变更,每个变更仅针对一部分服务器或用户起作用。 +变更可以灰度发布给一定数量的服务器或用户,比如 1%。 +在严重Bug发生,或者有其他负面影响时,可以迅速单独屏蔽某个变更。 +用数据来度量每个变更对用户体验的提升。 + + +LCE 的职责 + +LCE 团队负责管理发布流程,以确保整个发布过程做到又快又好。LCE 有如下这些职责: + + +审核新产品及相关的内部服务,确保它们的可靠性标准达到要求。如果不达预期,提供一些具体的建议来提升可靠性。 +在发布过程中作为多个团队之间的联系纽带。 +负责跟进发布系统相关的所有技术问题。 +作为整个发布过程中的一个守门人,决定某次发布是否是 “安全的”。 + + +整体来说,LCE 的要求其实是相当高的。LCE 的技术要求与其他的 SRE 成员一样,但这个岗位打交道的外部团队很多,需要有很强的沟通和领导能力。他需要将分散的团队聚合在一起达成一个共同目标,同时还需要偶尔处理冲突问题,还要能够为软件开发工程师提供建议和指导。 + +发布检查列表 + +我们前面已经提过,发布检查列表可以用来保障发布质量,它是可靠发布产品与服务的重要组成部分。一个完备的检查列表通常包含以下这些方面的内容。 + +其一,架构与依赖相关。针对系统架构的评审可以确定该服务是否正确使用了某类基础设施,并且确保这些基础设施的负责人加入到发布流程中来。为什么要引入基础设施的负责人,是因为需要确认相关依赖的服务都有足够的容量。 + +一些典型的问题有: + + +从用户到前端再到后端,请求流的顺序是什么样的? +是否已经将非用户请求与用户请求进行隔离? +预计的请求数量是多少?单个页面请求可能会造成后端多个请求。 + + +其二,集成和公司最佳实践相关。很多公司的对外服务都要运行在一个内部生态系统中,这些系统为如何建立新服务器、配置新服务、设置监控、与负载均衡集成,以及设置 DNS 配置等提供了指导。 + +其三,容量规划相关。新功能通常会在发布之初带来临时的用量增长,在几天后会趋于平稳。这种尖峰式的负载或流量分布可能与稳定状态下有显著区别,之前内部的压力测试可能失效。 + +公众的兴趣是很难预测的,有时甚至需要为预计容量提供 15 倍以上的发布容量。这种情况下灰度发布会有助于建立大规模发布时的数据依据与信心。 + +一些典型的问题有: + + +本次发布是否与新闻发布会、广告、博客文章或者其他类型的推广活动有关? +发布过程中以及发布之后预计的流量和增速是多少? +是否已经获取到该服务需要的全部计算资源? + + +其四,故障模式相关。针对服务进行系统性的故障模式分析可以确保发布时服务的可靠性。 + +在检查列表的这一部分中,我们可以检查每个组件以及每个组件的依赖组件来确定当它们发生故障时的影响范围。 + +一些典型的问题有: + + +该服务是否能够承受单独物理机故障?单数据中心故障?网络故障? +如何应对无效或者恶意输入,是否有针对拒绝服务攻击(DoS)的保护? +是否已经支持过载保护? +如果某个依赖组件发生故障,该服务是否能够在降级模式下继续工作? +该服务在启动时能否应对某个依赖组件不可用的情况?在运行时能否处理依赖不可用和自动恢复情况? + + +其五,客户端行为相关。最常见的客户端滥发请求的行为,是配置更新间隔的设置问题。比如,一个每 60s 同步一次的新客户端,会比600s 同步一次的旧客户端造成10倍的负载。 + +重试逻辑也有一些常见问题会影响到用户触发的行为,或者客户端自动触发的行为。假设我们有一个处于过载状态的服务,该服务由于过载,某些请求会处理失败。如果客户端重试这些失败请求,会对已经过载的服务造成更大负载,于是会造成更多的重试,更多的负载。客户端这时应该降低重试的频率,一般需要增加指数型增长的重试延迟,同时仔细考虑哪些错误值得重试。例如,网络错误通常值得重试,但是 4xx 错误(这一般意味着客户端侧请求有问题)一般不应该重试。 + +自动请求的同步性往往还会造成惊群效应。例如,某个手机 APP 开发者可能认为夜里2点是下载更新的好时候,因为用户这时可能在睡觉,不会被下载影响。然而,这样的设计会造成夜里 2 点时有大量请求发往下载服务器,每天晚上都是如此,而其他时间没有任何请求。这种情况下,每个客户端应该引入一定随机性。 + +其他的一些周期性过程中也需要引入随机性。回到之前说的那个重试场景下:某个客户端发送了一个请求,当遇到故障时,1s 之后重试,接下来是 2s、4s 等。没有随机性的话,短暂的请求峰值可能会造成错误比例升高,这个周期会一直循环。为了避免这种同步性,每个延迟都需要一定的抖动,也就是加入一定的随机性。 + +一些典型的问题有: + + +客户端在请求失败之后,是否按指数型增加重试延时? +是否在自动请求中实现随机延时抖动? + + +其六,流程与自动化相关。虽然我们鼓励自动化,但是对于发布这件事情来说,完全自动化是灾难性的。为了保障可靠性,我们应该尽量减少发布流程中的单点故障源,包括人在内。 + +这些流程应该在发布之前文档化,确保在工程师还记得各种细节的时候就完全转移到文档中,这样才能在紧急情况下派上用场。流程文档应该做到能使任何一个团队成员都可以在紧急事故中处理问题。 + +一些典型的问题有: + + +是否已将所有需要手动执行的流程文档化? +是否已将构建和发布新版本的流程自动化? + + +其七,外部依赖相关。有时候某个发布过程依赖于某个不受公司控制的因素。尽早确认这些因素的存在可以使我们为它们的不确定性做好准备。 + +例如,服务依赖于第三方维护的一个类库,或者另外一个公司提供的服务或者数据。当第三方提供商出现故障、Bug、系统性的错误、安全问题,或者未预料到的扩展性问题时,尽早计划可以使我们有办法避免影响到直接用户。 + +一些典型的问题有: + + +这次发布依赖哪些第三方代码、数据、服务,或者事件? +是否有任何合作伙伴依赖于你的服务?发布时是否需要通知他们? +当我们或者第三方提供商无法在指定截止日期前完成工作时,会发生什么? + + +结语 + +今天我们探讨 “发布与升级” 的实践,如何既保证质量,又能够兼顾效率。正确的做法当然不是为了快而去忽略流程,而是在不断的发布经历中总结经验教训,把每个环节干得更快更有效率。 + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们聊聊 “故障域与故障预案”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/加餐如何做HTTP服务的测试?.md b/专栏/许式伟的架构课/加餐如何做HTTP服务的测试?.md new file mode 100644 index 0000000..578e5e1 --- /dev/null +++ b/专栏/许式伟的架构课/加餐如何做HTTP服务的测试?.md @@ -0,0 +1,231 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 加餐 如何做HTTP服务的测试? + 你好,我是七牛云许式伟。 + +基于 HTTP 协议提供服务的好处是显然的。除了 HTTP 服务有很多现成的客户端、服务端框架可以直接使用外,在 HTTP 服务的调试、测试、监控、负载均衡等领域都有现成的相关工具支撑。 + +在七牛,我们绝大部分的服务,包括内部服务,都是基于 HTTP 协议来提供服务。所以我们需要思考如何更有效地进行 HTTP 服务的测试。 + +七牛早期 HTTP 服务的测试方法很朴素:第一步先写好服务端,然后写一个客户端 SDK,再基于这个客户端 SDK 写测试案例。 + +这种方法多多少少会遇到一些问题。首先,客户端 SDK 的修改可能会导致测试案例编不过。其次,客户端 SDK 通常是使用方友好,而不是测试方友好。服务端开发过程和客户端 SDK 的耦合容易过早地陷入“客户端 SDK 如何抽象更合理” 的细节,而不能专注于测试服务逻辑本身。 + +我的核心诉求是对服务端开发过程和客户端开发过程进行解耦。在网络协议定好了以后,整个系统原则上就可以编写测试案例,而不用等客户端 SDK的成熟。 + +不写客户端 SDK 而直接做 HTTP 测试,一个直观的思路是直接基于 http.Client 类来写测试案例。这种方式的问题是代码比较冗长,而且它的业务逻辑表达不直观,很难一眼就看出这句话想干什么。虽然可以写一些辅助函数来改观,但做多了就会逐渐有写测试专用 SDK 的倾向。这种写法看起来也不是很可取,毕竟为测试写一个专门的 SDK,看起来成本有些高了。 + +七牛当前的做法是引入一种 httptest DSL 文法。这是七牛为 HTTP 测试而写的领域专用语言。这个 httptest 工具当前已经开源,项目主页为: + + +https://github.com/qiniu/httptest(httptest 框架) +https://github.com/qiniu/qiniutest(支持七牛帐号与授权机制的 qiniutest 工具) + + +httptest 基础文法 + +这个语言的文法大概在 2012 年就已经被加入到七牛的代码库,后来有个同事根据这个 DSL 文法写了第一版本 qiniutest 程序。在决定推广用这个 DSL 来进行测试的过程中,我们对 DSL 不断地进行了调整和加强。虽然总体思路没有变化,但最终定稿的 DSL 与最初版本有较大的差异。目前来说,我已经可以十分确定地说,这个DSL可以满足 90% 以上的测试需求。它被推荐做为七牛内部的首选测试方案。 + + + +上图是这套 DSL 的 “hello world” 程序。它的执行预期是:下载 www.qiniu.com 首页,要求返回的 HTTP 状态码为 200。如果返回非 200,测试失败;否则测试通过,输出返回包的正文内容(resp.body 变量)。输出 resp.body 的内容通常是调试需要,而不是测试需要。自动化测试是不需要向屏幕去输出什么的。 + + + +我们再看该 DSL 的一个 “quick start(快速入门)” 样例。以 # 开始的内容是程序的注释部分。这里有一个很长很长的注释,描述了一个基本的 HTTP 请求测试的构成。后面我们会对这部分内容进行详细展开,这里暂时跳过。 + +这段代码的第一句话是定义了一个 auth 别名叫 qiniutest,这只是为了让后面具体的 HTTP 请求中授权语句更简短。紧接着是发起一个 POST 请求,创建一个内容为 {“a”: “value1”, “b”: 1} 的对象,并将返回的对象 id 赋值给一个名为 id1 的变量。后面我们会详细解释这个赋值过程是如何进行的。 + +接着我们发起一个获取对象内容的 GET 请求,需要注意的是 GET 的 URL 中引用了 id1 变量的值,这意味着我们不是要取别的对象的内容,而是取刚刚创建成功的对象的内容,并且我们期望返回的对象内容和刚才POST上去的一样,也是 {“a”: “value1”, “b”: 1}。这就是一个最基础的 HTTP 测试,它创建了一个对象,确认创建成功,并且尝试去取回这个对象,确认内容与我们期望的一致。这里上下两个请求是通过 id1 这个变量来建立关联的。 + +对这套DSL文法有了一个大概的印象后,我们开始来解剖它。先来看看它的语法结构。首先这套 httptest DSL 基于命令行文法: + +command switch1 switch2 … arg1 arg2 … + + +整个命令行先是一个命令,然后紧接着是一个个开关(可选),最后是一个个的命令参数。和大家熟悉的命令行比如 Linux Shell 一样,它也会有一些参数需要转义,如果参数包含空格或其他特殊字符,则可以用 \ 前缀来进行转义。比如 ‘\ ’ 表示 “(空格),‘\t’表示 TAB 等。另外,我们也支持用 ‘…’ 或者 “…” 去传递一个参数,比如 json 格式的多行文本。同 Linux Shell 类似,’…’ 里面的内容没有转义,‘\ ’ 就是 ‘\ ’,‘\t’就是 ‘\t’,而不是 TAB。而 “…” 则支持转义。 + +和 Linux Shell 不同的是,我们的 httptest DSL 虽然基于命令行文法,但是它的每一个参数都是有类型的,也就是说这个语言有类型系统,而不像 Linux Shell 命令行参数只有字符串。我们的 httptest DSL 支持且仅支持所有 json 支持的数据类型,包括: + + +string(如:”a”、application/json 等,在不引起歧义的情况下,可以省略双引号) +number(如:3.14159) +boolean(如:true、false) +array(如:[“a”, 200, {“b”: 2}]) +object/dictionary(如:{“a”: 1, “b”: 2}) + + +另外,我们的 httptest DSL 也有子命令的概念,它相当于一个函数,可以返回任意类型的数据。比如 qiniu f2weae23e6c9f jg35fae526kbce返回一个 auth object,这是用常规字符串无法表达的。 + +理解了 httptest DSL 后,我们来看看如何表达一个 HTTP 请求。它的基本形式如下: + +req +header +header +auth +body + + +第一句是 req 指令,带两个参数: 一个是 http method,即 HTTP 请求的方法,如 GET、POST 等。另一个是要请求的 URL。 + +接着是一个个自定义的 header(可选),每个 header 指令后面跟一个 key(键)和一个或多个 value(值)。 + +然后是一个可选的 auth 指令,用来指示这个请求的授权方式。如果没有 auth 语句,那么这个 HTTP 请求是匿名的,否则这就是一个带授权的请求。 + +最后一句是 body 指令,顾名思义它用来指定 HTTP 请求的正文。body 指令也有两个参数,一个是 content-type(内容格式),另一个是 body-data(请求正文)。 + +这样说比较抽象,我们看下实际的例子: + +无授权的 GET 请求: + +req GET http://www.qiniu.com/ + + +带授权的 POST 请求: + +req POST http://foo.com/objects +auth `qiniu f2weae23e6c9fjg35fae526kbce` +body application/json '{ + "a": "hello1", + "b":2 +}' + + +也可以简写成: + +无授权的GET请求: + +get http://www.qiniu.com/ + + +带授权的Post请求: + +post http://foo.com/objects +auth `qiniu f2weae23e6c9fjg35fae526kbce` +json '{ + "a": "hello1", + "b":2 +}' + + +发起了 HTTP 请求后,我们就可以收到 HTTP 返回包并对内容进行匹配。HTTP 返回包匹配的基本形式如下: + +ret +header +header +body + + +我们先看 ret 指令。实际上,请求发出去的时间是在 ret 指令执行的时候。前面 req、header、auth、body 指令仅仅表达了 HTTP 请求。如果没有调用 ret 指令,那么系统什么也不会发生。 + +ret 指令可以不带参数。不带参数的 ret 指令,其含义是发起 HTTP 请求,并将返回的 HTTP 返回包解析并存储到 resp 的变量中。而对于带参数的 ret 指令: + +ret + + +它等价于: + +ret +match $(resp.code) + + +match 指令 + +这里我们引入了一个新的指令:match 指令。 + + + +七牛所有 HTTP 返回包匹配的匹配文法,都可以用这个 match 来表达: + + + +所以本质上来说,我们只需要一个不带参数的 ret,加上 match 指令,就可以搞定所有的返回包匹配过程。这也是我们为什么说 match 指令是这套 DSL 中最核心的概念的原因。 + +和其他自动化测试框架类似,这套 DSL 也提供了断言文法。它类似于 CppUnit 或 JUnit 之类的测试框架提供 assertEqual。具体如下: + +equal + + + +与 match 不同,这里 中都不允许出现未绑定的变量。 + +与 match 不同,equal 要求的值精确相等。 + +equalSet + +这里 SET 是指集合的意思。 + +与 equal 不同,equalSet 要求 都是array,并且对 array 的元素进行排序后判断两者是否精确相等。 + +equalSet 的典型使用场景是测试 list 类的 API,比如列出一个目录下的所有文件,你可能预期这个目录下有哪些文件,但是不能预期他们会以什么样的次序返回。 + + +以上介绍基本上就是这套 DSL 最核心的内容了。内容非常精简,但满足了绝大部分测试场景的需求。 + +测试环境的参数化 + +下面我们谈谈最后一个话题:测试环境的参数化。 + +为了让测试案例更加通用,我们需要对测试依赖的环境进行参数化。比如,为了让测试脚本能够同时用于 stage 环境和 product 环境,我们需要把服务的 Host 信息参数化。另外,为了方便测试脚本入口,我们通常还需要把 用户名/密码、AK/SK 等敏感性信息参数化,避免直接硬编码到测试案例中。 + +为了把服务器的 Host 信息(也就是服务器的位置)参数化,我们引入了 host 指令。例如: + +host foo.com 127.0.0.1:8888 +get http://foo.com/objects/a325gea2kgfd +auth qiniutest +ret 200 +json '{ + "a": "hello1", + "b":2 +}' + + +这样,后文所有出现请求 foo.com 地方,都会把请求发送到 127.0.0.1:8888 这样一个服务器地址。要想让脚本测试另外的服务器实例,我们只需要调整 host 语句,将 127.0.0.1:8888 调整成其他即可。 + +除了服务器 Host 需要参数化外,其他常见的参数化需求是 用户名/密码、AK/SK 等。AK/SK 这样的信息非常敏感,如果在测试脚本里面硬编码这些信息,将不利于测试脚本代码的入库。一个典型的测试环境参数化后的测试脚本样例如下: + + + +其中,env 指令用于取环境变量对应的值(返回值类型是 string),envdecode 指令则是先取得环境变量对应的值,然后对值进行 json decode 得到相应的 object/dictionary。有了$(env) 这个对象(object),就可以通过它获得各种测试环境参数,比如 $(env.FooHost)、$(env.AK)、$(env.SK) 等。 + +写好了测试脚本后,在执行测试脚本之前,我们需要先配置测试环境: + +export QiniuTestEnv_stage='{ + "FooHost": "192.168.1.10:8888", + "AK": "…", + "SK": "…" +}' + +export QiniuTestEnv_product='{ + "FooHost": "foo.com", + "AK": "…", + "SK": "…" +}' + + +这样我们就可以执行测试脚本了: + +测试 stage 环境: + +QiniuTestEnv=stage qiniutest ./testfoo.qtf + + +测试 product 环境: + +QiniuTestEnv=product qiniutest ./testfoo.qtf + + +结语 + +测试是软件质量保障至关重要的一环。一个好的测试工具对提高开发效率的作用巨大。如果能够让开发人员的开发时间从一小时减少到半小时,那么日积月累就会得到惊人的效果。 + +去关注开发人员日常工作过程中的不爽和低效率是非常有必要的。任何开发效率提升相关的工作,其收益都是指数级的。这也是我们所推崇的做事风格。如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/加餐实战:“画图程序”的整体架构.md b/专栏/许式伟的架构课/加餐实战:“画图程序”的整体架构.md new file mode 100644 index 0000000..1c0a5d8 --- /dev/null +++ b/专栏/许式伟的架构课/加餐实战:“画图程序”的整体架构.md @@ -0,0 +1,184 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 加餐 实战:“画图程序” 的整体架构 + 你好,我是七牛云许式伟。 + +我们先回顾一下 “架构思维篇” 前面几讲的内容: + + +[57 | 心性:架构师的修炼之道] +[58 | 如何判断架构设计的优劣?] +[59 | 少谈点框架,多谈点业务] +[60 | 架构分解:边界,不断重新审视边界] + + +我们先谈了怎么才能修炼成为一个好的架构师,其中最核心的一点是修心。这听起来好像一点都不像是在谈一门有关于工程的学科,但这又的的确确是产生优秀架构师最重要的基础。 + +接下来几篇,我们核心围绕着这样几个话题: + + +什么是好的架构? +架构的本质是业务的正交分解,分解后的每个模块业务上仍然是自洽的。 + + +我们反复在强调 “业务” 一词。可以这样说,关注每个模块的业务属性,是架构的最高准则。 + +不同模块的重要程度不同,由此我们会区分出核心模块和周边模块。对于任何一个业务,它总可以分解出一个核心系统,和多个周边系统。不同周边系统相互正交。即使他们可能会发生关联,也是通过与核心系统打交道来建立彼此的间接联系。 + +今天我们将通过第二章 “桌面开发篇” 的实战案例 “画图程序” 来验证下我们这些想法。我们以最后一次迭代的版本 v44 为基础: + + +https://github.com/qiniu/qpaint/tree/v44 + + +整体结构 + +我们先来分析整个 “画图” 程序的整体结构。除了 index.htm 作为总控的入口外,我们把其他的文件分为以下四类: + + +核心系统(棕色):这些文件隶属于整个画图程序的业务核心,不可或缺; +周边系统(黄色):这些文件属于业务的可选组件; +通用控件(绿色):这些文件与画图程序的业务无关,属于通用的界面元素,由画图程序的周边系统所引用; +基础框架(紫色):这些文件与画图程序的业务无关,属于第三方代码,或者更基础的底层框架。 + + +我们可以有如下文件级别的系统组织结构: + + + +通过这个图我们可以看出,这个画图程序的 “内核” 是非常小的,就三个文件:index.htm、view.js、dom.js。为了让你看到每个文件的复杂度,我把各个文件的代码规模也在图中标了出来。如果我们把所有的周边系统以及它们的依赖代码去除,整个程序仍然是可以工作的,只不过我们得到的是一个只读的画图程序的查看器(QPaintViewer)。 + +这很有意思,因为我们把所有的 Controllers 都做成了彼此完全正交的可选组件。 + +有了这个图,我们对各个文件之间的关系就很清楚了。接下来,正如我们在 “ [58 | 如何判断架构设计的优劣?]” 中说的那样,我们最关心的还是周边系统,也就是这些 Controller 对核心系统的伤害是什么样的。 + +我们先把所有引用关系列出来: + + + +我们先看 creator/rect.js 模块。它对 View 层,主要是 QPaintView 类的引用是 10 处,对 Model 层,主要是 QPaintDoc、Shape、QShapeStyle 这三者的引用是 6 处。每处引用都是 1 行代码,直接调用 View 层或 Model 层对外提供的接口方法。 + +单就 creator/rect.js 模块而言,它对核心系统的伤害值为 10 + 6 = 16。但是实际上这些接口方法绝大部分并不是专门提供给 creator/rect.js 模块的,这意味着所有周边模块应该共担这个伤害值。比如某个接口方法被 N 个周边模块引用,那么每个周边模块分担的伤害值为 1/N。 + +这个逻辑初听起来有点奇怪,我新增一个和我互不相关的周边模块,怎么会导致一个既有周边模块对核心系统的伤害值降低? + +这是因为,我们的伤害值是工程测量值。我们往极端来说,如果有无穷多个周边模块都会引用某个接口方法,那么对于其中某个周边模块来说,它为此造成的伤害值为 0,因为这个接口太稳定了。这也证明,抽象出共性的业务方法,比给某个周边模块单独开绿灯要好。我们定义业务的接口要尽可能追求自然。 + +但是现实中,被无数个周边模块引用的接口是不存在的。你可能主观判断我这个接口是很通用的,但是它需要实证的依据。每增加一个引用方,这个实证就被加强一次。这也是为什么增加一个新周边模块会导致既有周边模块伤害值降低的原因,因为它证实了一些接口方法的确是通用的。 + +有一些接口当前只有 creator/rect.js 引用的,这些接口的引用代码在表格中我把它们标为红色,它们是: + + +new QLine +new QRect +new QEllipse +shape.onpaint + + +我们一眼看过去就很清楚,这些接口确实是非常通用的接口。之所以它们只有 creator/rect.js 引用,是因为这个 “画图” 程序当前的规模还比较小,随着越来越多的周边模块加入,逐步也会有更多人分担伤害值。 + +当前系统有 5 个周边模块。考虑多个周边模块共担伤害值的情况,creator/rect.js 模块对核心系统的伤害值是多少? + +我们做个近似,只要某个接口已经被超过一个周边模块引用,就认为它的引用次数是 5,而不是一一去统计它。这样算的话,creator/rect.js 模块对核心系统的伤害值约 12⁄5 + 4 = 6.4。 + +类似地,我们可以计算其他周边模块对核心系统的伤害值,具体如下: + + +creator/path.js 模块,伤害值约 12⁄5 + 1 = 3.4。 +creator/freepath.js 模块,伤害值约 13⁄5 = 2.6。 +accel/select.js 模块,伤害值约 10⁄5 + 6 = 8。 +accel/menu.js 模块,伤害值约 5⁄5 + 6 = 7。 + + +如果我们把所有周边模块看作整体,它和核心系统的关系如下: + + + +可以看出,整个周边系统对核心系统的引用是 31 处,也就是说它带来的伤害值为 31。这和上面我们近似计算得到的所有周边系统伤害值之和 6.4 + 3.4 + 2.6 + 8 + 7 = 27.4 不同。这中间的差异主要由于我们没有去实际统计接口方法的引用次数而直接统一用 5,所以估算的伤害值比实际会小一点。 + +Model 层 + +看完了整体,我们把关注点放到 Model 层。 + +对于这个画图程序,代码量最多的就是 Model 层,即 dom.js 文件,大约 850 多行代码。所以我们决定进一步分解它,得到如下结构: + + + +当我们把 Model 层看作一个完整的业务时,它内部仍然可以分解出一个核心系统,和多个周边系统。并且同样地,我们把代码分为四类: + + +核心系统:隶属于整个画图程序的业务核心,不可或缺,我们标记为棕色或白色; +周边系统:属于业务的可选组件,主要是各类图形; +操作系统相关的辅助函数:与业务无关,但是和平台相关,我们标记为绿色; +纯算法的辅助函数:与业务无关,与操作系统也无关,我们标记为紫色。 + + +上图的核心系统中,标记为棕色的模块与白色的模块的区别在于,标棕色的模块会被周边系统所引用,属于核心系统的 “接口级” 模块。标白色的模块只被核心系统内部所引用,不把它们画出来也是可以的。 + +另外,图中 Shape 接口因为 JavaScript 是弱类型语言,它在代码中并没有显式体现出来。这里我们将它用 Go 语法表达如下: + +type number = float64 +type any = interface{} + +type HitResult struct { + hitCode number + hitShape Shape +} + +type Shape interface { + style QShapeStyle + onpaint(ctx CanvasRenderingContext2D) + hitTest(pt Point) HitResult + bound() Rect + setProp(parent any, key string, val any) + move(parent any, dx, dy number) + toJSON() any +} + + +当然,和分析整个画图程序一样,我们最关心的还是周边系统对核心系统的伤害是什么样的。 + +我们先把所有引用关系列出来: + + + +对于 Model 层来说,目前我们需求的开放性主要体现在图形(Shape)的种类。未来是否要支持图片,是否要支持艺术字等等,这些存在很大的变数。所以我们当前的周边模块,基本上都是某种图形。 + +通过这个表格我们可以看出,不同的图形对核心系统的需求完全一模一样。我们很容易计算得到,整个周边系统对核心系统的伤害值为 4,平均每一种图形的伤害值为 1。 + +通用控件库 + +聊了文件级别的组织结构,也聊了 Model 层,我们画图程序的整体脉络也就出来了。这里我再补充一个虽然和业务无关,但是也是一个不小的体系设计:通用控件库子系统。 + +控件的种类是无穷的,我们自然而然得去考虑怎么适应未来的需求。出于开放性架构的考虑,你会发现它也可以基于核心系统和周边系统来拆分,如下: + + + +同样地,我们最关心的还是周边系统对核心系统的伤害是什么样的。 + +我们先把所有引用关系列出来: + + + +通过这个表格我们可以看出,这些控件的实现本身和核心系统,即控件框架没什么关系,它们只是把自己注册到控件框架中。所有控件对核心系统的需求完全一模一样。我们很容易计算得到,整个周边系统对核心系统的伤害值为 1,平均每一种控件的伤害值为 1/3。 + +结语 + +这一讲我们通过前面实战的画图程序作为例子,来剖析架构设计过程业务是如何被分解的。 + +对于复杂系统,一定要理清核心系统和周边系统的边界,让整个程序的内核最小化。 + +另外,我们也实际分析了画图程序中,周边模块对核心系统的伤害值。这个数据可以很好地评判不同架构方案的好坏。 + +如果你自己也实现了一个 “画图程序”,可以根据这几讲的内容,对比一下我们给出的样例代码,和自己写的有哪些架构思想上的不同,这些不同之处的得失是什么? + +如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “全局性功能的架构设计”。 + +如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/热点观察我看Facebook发币(上):区块链、比特币与Libra币.md b/专栏/许式伟的架构课/热点观察我看Facebook发币(上):区块链、比特币与Libra币.md new file mode 100644 index 0000000..4255c3a --- /dev/null +++ b/专栏/许式伟的架构课/热点观察我看Facebook发币(上):区块链、比特币与Libra币.md @@ -0,0 +1,222 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 热点观察 我看Facebook发币(上):区块链、比特币与Libra币 + 你好,我是七牛云许式伟。 + +Facebook(脸书)于6月18日发布了其加密数字货币项目白皮书。该数字货币被命名为 Libra(天秤座),象征着平衡与公正。 + +此前,BBC 报道说这个数字货币叫 GlobalCoin(全球币),但后来被纠正说这只是Facebook员工在内部对其的昵称。 + +不管是叫 Libra,还是 GlobalCoin,其实都透露出了Facebook的雄心壮志。 + +今天的Facebook 坐拥 27 亿的活跃用户,相当于全球1/3的人都在用 Facebook,这是一个比微信大得多的数字社群(微信月活为 11 亿,差不多全中国人民都在用微信)。 + +我之所以叫它“数字社群”,而不叫“数字王国”,是因为用科学的态度来谈,“国家” 这样的称呼没法随便用,“国家”得符合国家需要具备的特征。 + +国家有哪些特征? + +武装是大家能够很快想得到的,国家得有自己的军队,但是数字世界毕竟是个虚拟世界,军队似乎没啥意义。 + +发行货币是国家另一个至关重要的权力。现在,拥有 27 亿人口的 Facebook 要发币,这意味着它离真正意义上的 “数字王国” 又靠近了一步,至关重要的一步。 + +整个世界将因此发生翻天覆地的变化。 + +今天我想借此机会,谈谈区块链和数字货币背后的社会需求动因、逻辑以及它们将产生的巨大影响。 + +我们的内容将会分为上下两部分,如下: + + +我看 FB 发币(上):区块链、比特币与 Libra 币; +我看 FB 发币(下):深入浅出理解 Libra 币。 + + +区块链 + +我们先从区块链谈起。 + +介绍区块链技术的文章已经有很多了,我们今天不谈技术实现的细节。简单说,它是一个分散式防篡改的数字账本。 + +但是,区块链到底有什么用?它想解决什么样的问题? + +有人说,区块链是为了“去中心化”。 + +那么“去中心化”是趋势么?互联网的趋势是“中心化”,而不是“去中心化”。 + +效率为先。如果一个事情可以两个人干更好,那么最终的结果就是应该两个人去干,而不会是十个人去干,这是最朴素的经济学原理。 + +而微信和 Facebook 的成功,也证明了中心化是趋势。人们之所以向往着去中心化,是因为人天然对垄断有抗拒之心,被人生杀予夺的感觉怎么想都不太美妙。 + +人们因为效率而中心化,因为垄断而去中心化。所以是一个中心还是几个中心,这是自然平衡的结果。 + +历史告诉我们,它选择的“去中心化”是开辟疆土。前有哥伦布发现新大陆,今有“钢铁侠”马斯克寻求火星殖民。这才是真正的“去中心化”,找到全新的盐碱地去开拓。 + +地球相比于整个宇宙,只不过是一粒尘埃。这样的“中心化”,又算得了什么呢? + +当然也有人说,区块链是为了“去中介”。 + +那么“去中介化”是互联网的趋势么?互联网会减少尽可能多的中间环节,但是不会“去中介”。 + +每个人有他自己的专长。把自己不擅长的事情交给中介,这是特别自然的一件事情,我们为中介的专业性买单。 + +互联网让中介可以 24 小时为你服务,远程为你服务。这些都是以前没法完成的。这会导致什么呢?一些中介会茁壮成长,变成行业中的关键节点。它也有可能会顺带合并掉上下游的一些分工,让服务链条更短。 + +那么,区块链价值到底是什么? + +我认为,区块链本质上是一种“共识机制”,或者说“契约机制”。 分散式防篡改的数字账本,保障的核心是事实的不可抵赖。这对双方形成共识是极大的效率提升。在大部分情况下,没有共识往往是因为争议的双方对事实的认定不同。 + +从这个认知来看,一些鼓吹“区块链是下一个互联网基础设施”的人们可以洗洗睡了。 + +区块链不会重造一个新互联网底层,但它最有可能重塑金融与供应链。 + +比特币 + +比特币,数字货币的鼻祖,区块链技术的第一个杀手级应用。 + +为什么会出现比特币?它希望解决什么样的需求? + +其一,希望能够解决政府动不动就发币,让你手头货币凭空贬值的问题。 + +这应该是比特币创始人中本聪的本意。所以比特币发币的机制是挖矿,谁挖到就算谁的。成本是购买计算机的成本和运行计算机挖矿所消耗的电费。 + +从这个角度看,比特币不能类比法币,它没有法币的发行者,国家信用作为背书。它更像是黄金,基于资源本身的稀缺性作为背书。 + +其二,希望能够解决经济全球化带来的货币跨境流通问题。 + +这是数字货币的价值投资者们的共同期望。 + +随着互联网技术在全球范围的不断普及,越来越多的生意被放到了网上。旧的商业文明可以一言以蔽之:一手交钱,一手交货。而建立在互联网之上的新商业文明,我们一手下单付款,一手收钱发货,足不出户,货物就通过便捷的物流服务送到了你手上。 + +但建立在互联网之上的新商业文明,遭遇了国家与国家之间的边界挑战。这体现在以下两点。 + +其一,汇率。 我们都知道,法币与法币的兑换是有损的。你把钱从 A 币换成 B 币,再把 B 币换回 A 币,钱就少了。 + +其二,关税。 一个商品从一个国家买到另一个国家,成本就变高了。刨除物流成本带来的影响因素外,最大的额外代价就是关税。 + +物流成本是可以解决的。贸易全球化带来的结果是生产全球化。 + +以前国与国之间的贸易,主要成分在成品贸易,这个成品的原料和加工的零件都来自于同一个国家,我生产出成品后销往世界各地。 + +但是现在国与国之间的贸易,主要成分在原料与零部件的交易。这意味着供应链已经越来越全球化,成品的生产很可能就在销售地,或者靠近销售地。 + +这样的好处,一方面当然是产品品质的需要,可以全球寻求优质供应商。另一方面可以极大程度地降低物流成本。原材料的运输相比成品来说,更易于利用规模化效应降低单位运输成本。 + +关税问题,也是可以国家之间协调解决的。超低关税,乃至最终零关税是历史发展的必然。 + +那么汇率问题呢? + +这个问题不好解决。提供兑换货币服务的金融机构不可能是免费服务,他们自身的成本也在那里。只要有跨境交易,就有“用什么结算货币”的问题,随之而来的就有结汇的问题。 + +除非,有一种世界货币,它能够流通于各个国家,被各个国家的市场所接受。 + +解决汇率问题最好的办法当然是不用换汇。 + +比特币能够满足这个需求么? + +不能。比特币有这样一些软肋,我列在了下面。 + +其一,没法按需增发,价格波动大。 货币发行量最理想的情况是按市场需求来。如果市场对货币的需求量暴增,但是又没法增加货币发行量来调节,必然会导致货币增值。 + +比特币就属于这种情况,它只能靠挖矿来增加,而没有其他手段。那么一旦人们对它的需求增加速度超过挖矿速度,就会出现价格暴涨。 + +投机性的行为可能会助长这一点。但是从更长远的维度看,比特币的价值与“市场需求量/比特币流通量”成正比。 + +其二,性能低。 比特币基于区块链技术,参与的节点众多。节点多对比特币是有极强的正面价值的,因为参与的节点越多,其被人把控的概率也就越低。 + +但是,节点越多,比特币交易的性能也就越低。那么现实中,比特币交易到底有多慢?平均每秒2-3笔交易。 + +这意味着,比特币完全无法满足支持世界货币所需的交易频次。 + +Libra 币 + +虽然比特币并不满足需求,但数字化的世界货币一定会诞生,只不过是谁的问题,这是贸易全球化决定的。 + +Libra 币就是冲着成为世界货币去的。在 Facebook 最近发布的《加密货币项目 Libra 白皮书》中提到: + + +我们的世界真正需要一套可靠的数字货币和金融基础设施,两者结合起来必须能兑现“货币互联网”的承诺。 + + +可见,Libra 币其实不属于 Facebook,只不过是由Facebook 发起的。就像互联网一样,它期望的是一种开放式的架构。 + +“互联网”连接了世界上的所有人,但是国与国之间边界导致的“汇率”和“关税”问题,让贸易无法做到真正意义的全球化。 + +Libra 币旨在构建“货币互联网”,让贸易真正无国界,它如何做到?我们将在《我看 FB 发币(下):深入浅出理解 Libra 币》这篇文章中进行详细的介绍。 + +今天我们重点聊的是 Libra 币将带来什么。 + +第一个问题:Libra 币会不会成功? + +我认为它会成功。唯一阻碍它成功的因素是美国政府。毕竟它给世界上任何一个国家的人们都带来了一个巨大的变化: + + +除了本国的法币,还有一个货币能够用于人们之间的日常交易。 + + +虽然此前已经有比特币也能够做到这一点,但是比特币的低吞吐能力决定了它必然无法成为世界货币。 + +Libra 币最直接挑战的是美元的地位。某种意义上来说,在它之前,美元在承担着世界货币的作用。 + +但是我认为美国政府会支持 Libra 币。有两个重要的理由支持他们这样做。 + +其一,世界货币一定会产生。 与其让它发生在其他国家,不如发生在美国,由美国人来主导这件事情的发生。 + +其二,美元的物理属性,决定了美元对交易的渗透在非美元区(法币不是美元的地区)只能在线下,无法支持线上交易。 + +但线上交易的比重越来越大,意味着美国需要一个数字货币能够渗透到其他国家。Libra 币刚好满足了这个需求,所以,Libra 币很容易与美元达成结盟。 + +一旦美国支持 Libra 币,这件事情就成功了一半。欧洲本来就有欧元,相当于已经有一个自己小范围的世界货币了,货币发行本来就已经和国家政权解绑了。改用 Libra 币带来的冲击并不大。 + +搞定了欧美,Libra 币基本上就算成功了。其他地区的国家很难真正去反抗这一潮流。 + +为什么? + +不拥抱 Libra 币,意味着放弃由“互联网+Libra币”共同构建的世界自由贸易体系。这对任何一个国家来说,都意味着闭关锁国,放弃经济增长。 + +另外搞一个世界货币与之对抗? + +难。货币有很强的网络效应。如果一个国家或地区已经被 Libra 币渗透,另一个货币进来就难了。 + +所以如果非要干,最佳的时机是现在。 + +假设,另一个世界货币干起来了,全球被分成了两半,一东一西。 + +它们之间没有贸易么?贸易用哪个世界币?这带来了新的汇率问题。 + +如果可以让企业自由选择,那么这会是大鱼吃小鱼的故事。因此,第二个世界货币很难真正干起来。 + +第二个问题:如果 Libra 币成功的话,它会带来什么影响? + +最大的变化,是货币的大一统。就像欧洲已经发生的那样,国家政权和货币脱钩,政府再也无法用货币杠杆来宏观调控市场。 + +第二个大变化,是交易的透明化。链上的支撑节点都可以看到全球所有的交易往来。虽然 Libra 币是匿名的身份,但是开通 Libra 账户过程在很多国家很可能是实名的。所以某种意义上来说,交易还是会和现实身份对应起来。 + +第三个问题:中国应该怎么应对? + +这个问题,有点像是中国该不该加入世贸组织(WTO)一样。我的答案是:应该认认真真考虑下如何加入,什么时候加入,加入前应该做好哪些准备。 + +结语 + +总结一下我对 Facebook 发币这件事情的看法。 + + +世界货币一定会诞生,只不过是谁的问题。这是贸易全球化决定的。 +以前大家看好比特币,但是比特币有软肋。其一是波动大,其二是性能低。 +Libra 币不属于 Facebook,只不过是由 Facebook 发起而已。它就像互联网一样,是开放式的架构。这也是其他巨头一拍即合,毫不犹豫地去支持的原因。 +Libra 币会得到美国政府的支持,并逐步渗透到世界各个地区。它不只是流行于 Facebook 的 27 亿活跃用户,也会流行于其他如 PayPal、Booking 等主流服务。 +“互联网 + Libra 币” 将共同构建全新的世界自由贸易体系。 + + +站在未来看现在,Libra 币将会是极其重大的一个历史节点。我倾向于这样来描绘它的影响: + + +计算机 => 互联网 => Libra 币 + + +货币有着巨大的网络效应,留给后来者的时间窗口极短。我想,我们都应该思考怎么面对这件事情了。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/热点观察我看Facebook发币(下):深入浅出理解Libra币.md b/专栏/许式伟的架构课/热点观察我看Facebook发币(下):深入浅出理解Libra币.md new file mode 100644 index 0000000..bb9d46a --- /dev/null +++ b/专栏/许式伟的架构课/热点观察我看Facebook发币(下):深入浅出理解Libra币.md @@ -0,0 +1,133 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 热点观察 我看Facebook发币(下):深入浅出理解 Libra 币 + 你好,我是七牛云许式伟。今天我们接着聊由 Facebook 推动的 Libra 币。 + +听到一个陌生概念后,人们往往习惯于把它和自己熟悉的东西做一个类比,方便和他人沟通的时候,用以表达自己的理解。 + +那么,和 Libra 最像的东西是什么呢? + +货币储备 + +有人说,Libra 不就是腾讯的 Q 币么?但是,这种理解仅仅停留在非常表面的层次。两者共同之处,仅仅是因为两者都是一种虚拟币(甚至不能说它们都是数字货币,因为 Q 币是不能称之为数字货币的)。 + +首先,Q 币可以用货币购买,但是没法反向兑换现金。最关键的是,腾讯可以自由发行 Q 币,它说有多少就可以有多少,不需要被监管。所以如果 Q 币可以以稳定的价格兑换现金的话,相当于腾讯可以自由印钞票,这显然不会被政府所允许。 + +但是 Libra 并不是这样。Libra 可以自由兑换,另外 Facebook 也不能够自由发行 Libra 币。发行新的 Libra 币时,有两大核心约束。 + +其一,发行新的 Libra 币需要保值的资产做为抵押,我们称之为 “Libra 储备”。 什么样的资产可以作为 Libra 储备?被 Libra 协会认可的,稳定且信誉良好的中央银行发行的货币,或者政府货币证券。也就是说,Libra 币锚定的是它认可的“一篮子货币”,而不是某一种货币。 + +其二,由 Libra 协会来发行或销毁 Libra 币。 目前 Libra 协会执行这个动作完全是被动的。Libra 生态中,会有一些被授权的经销商,他们负责 Libra 币的买卖。 + +这些授权经销商会按照一定的汇率,以某种被 Libra 协会认可的货币,向 Libra 协会买入 Libra 币。这时,这些用于买入的货币就会成为 Libra 储备。 + +当然授权经销商也可能卖出 Libra 币,换成他所期望的某种货币。在这种情况下,被卖出的 Libra 币就会被销毁,相应地, Libra 储备就会减少。 + +所以你可以看到,Libra 并没有自己的货币政策,总共有多少 Libra 币完全是由市场的供需决定的。 + +关于货币储备更详细的内容,请参阅 “Libra 储备”。 + +稳定币 + +既然 Libra 不像 Q 币,那么它像什么?比特币吗? + +相比 Q 币来说,Libra 与比特币的确更有比较意义,毕竟它们都是一种数字货币。但是我们在 “我看Facebook发币(上):区块链、比特币与 Libra 币” 中,也已经谈到过两者一些根本上的不同。 + +如果我们忽略所有的技术细节,单从货币角度来看,比特币和 Libra 最大的区别是价格的波动性。 + +通过上面我们对 “Libra 储备” 的介绍,我们很容易知道 Libra 是一种稳定币,它看起来更像是由一些保值资产抵押所形成的 “债券”。而在币圈,大家可能都经常会听到一个词,叫做 “炒币”。 + +如果我们细想一下这背后的逻辑,就会知道这个词其实挺神奇的。 + +货币的核心价值是什么? + +货币的价值显然不是增值,而是流通。而流通用的货币,最重要的是价格稳定。而币圈则不同,这些各式各样存在的币,现在它们的核心价值变成了 “炒”,这就完全被扭曲了。 + +我这么说当然不是认为比特币不好。只不过如果我们真的对比特币好,就应该把关注点放在比特币流通价值的构建上;而不是“喜看币涨”,涨了就奔走相告。 + +世界货币 + +那么,究竟 Libra 最像什么呢? + +我个人认为,虽然目前的实现机制有所不同,但是 Libra 最像的是欧元。欧元的出现,对欧盟国家之间的自由贸易往来发挥了巨大的贡献。 + +欧元和 Libra 的初衷是一样的,都是为了构建跨国的自由经济贸易体系。两者不同之处在于,当前 Libra 还没有央行,不能凭空去发行货币。 + +凡事都有两面,“世界货币” 同样是有缺陷的。欧债危机导致欧元贬值,这个教训大家还记忆犹新。它告诉我们,一个国家经济出现问题,如果它采用的是独立的主权货币,那么只是导致本国的货币贬值。 + +但是 “世界货币” 意味着大锅饭,它会传导到整个经济体,连带整个经济体的货币贬值。 + +那么 Libra 会出现自己的央行么?短期当然不会,当前最重要的是流通,是地盘的扩张。但是长远看只要 Libra 成功了,就一定会有。毕竟,没有 Libra 央行,就没有机会解决 “大锅饭” 的问题。 + +所以站在更长远的未来看,今天的 Libra 协会,有可能就会是未来的 “Libra 央行”。 + +Libra 协会 + +Libra 协会总部位于瑞士日内瓦,协会成员包括分布在不同地理区域的各种企业、非营利组织、多边组织和学术机构。初始协会成员共有28家(如下图),未来计划达到100家。 + +- +加入 Libra 协会,需要支付不低于 1000 万美元来购买 Libra 投资代币,注意,它并不能简单理解为 Libra 币,两者有很大不同,它有很多特殊的权益。 + +其一,整个 “Libra 储备” 的处置权。 比如, Libra 储备到底放在哪里会比较安全靠谱?这是由Libra 协会决定的。但是每个成员是不是都是一票?并不是,票数是由这个成员持有的 Libra 投资代币数量决定的。具体规则,我在下面会讨论。 + +其二,整个 “Libra 储备” 的利息,会被用来支撑 Libra 协会的日常运转。 比如工资和奖金激励,也会用于派发分红。分红会按照持有的 Libra 投资代币的多少来分配。 + +其三,各类事务决策的投票权。 权重按持有的 Libra 投资代币来计算,每 1000 万美元有 1 个投票权,但是为了防止投票权过于集中,任何成员的投票权不能超过总票数的 1%。 + +超出部分的投票权将由 Libra 协会的董事会重新分配,比如授予具有社会影响力的合作伙伴(称为 SIP)或研究机构,前提是:他们有能力并致力于验证节点的运作,从而参与治理,并且确实无法作出最低 1000 万美元的投资。 + +其四,运行验证节点。 这一点可以被看作义务,但也可以看作权益。义务角度来说,显然这事是需要 IT 成本的,如果连这都做不到,就会被剔除出去。从权益角度来说,那么多的交易数据都被你拿到了,这是多么有价值的数据。 + +关于 Libra 协会储备更详细的内容,请参阅 “Libra 协会”。 + +发展目标 + +当前 Libra 的验证节点是有限制的,是许可型的,也就是所谓的 “联盟链”。但是,未来它会向非许可型治理和共识节点运营转变,降低参与的准入门槛,并减少对创始人的依赖。 + +Libra 将逐步进行网络的开放,变成所谓的 “公链”。这时,新成员也能够通过投资 1000万美元来自动获得验证节点的运行许可,并持有 Libra 投资代币,从而分享网络权益(但是我估计投票权之类是没有的,只有投资收益)。这部分的详细政策,或许未来才会逐步明朗。 + +Libra 协会致力于尽可能减少协会的干预权。比如完全通过市场机制来调节 Libra 币的供需。所以除了说服更多人加入 Libra 协会,协会最重要的工作是确定 Libra 的技术演进路线。 + +但这一点真实情况下显然并不会那么理想。货币互联网和互联网不同的是,它管的毕竟是钱。一旦 Libra 网络发展壮大,一些货币相关的治理问题就会暴露出来。 + +降维打击 + +Libra 的影响面绝对超乎大部分人的想象。我们先看看一些具备技术背景的知名互联网公司 CEO 是怎么看的。具体参见下面的截图。 + + + +显然无论王兴还是王小川,都高度重视并反复评估 Libra 带来的影响和自己应该采取的行动。 + +Libra 带来的打击是全方位的,某种程度上来说甚至是极难抵御的降维打击。我们不妨从以下这些维度看。 + + +弱小国家的货币主权会不会就此被取代? +中国的汇率管控会不会失效?就算没有失效,会不会变成闭关锁国? +Libra 是否会成为美国取代美元的新货币霸权? +从企业与企业竞争的角度,这会不会成为 Facebook 对微信、支付宝的降维打击? + + +想完影响面想对策。从对策的角度来说无非两种,一是对抗,一是谋求对等权力下的协作。当然,这两条路都不会太容易。 + +结语 + +今天,我们从分析 Libra 的运行机制重新去理解 Libra 币。这里面的关键是:理解 Libra 协会究竟如何工作的。 + +我们没有分析 Libra 的技术细节,比如它怎么改变区块链的底层实现机制,又为什么要引入 Move 语言。如王兴所说,评判 Libra 的实现技术好不好,就好比评价美元钞票的印刷技术精细不精细。这事本身是有价值的,但不那么关键。 + +为什么我会写这两篇文章? + +站在未来看现在,Libra 币将会是极其重大的一个历史节点。我倾向于这样来描绘它的影响: + + +计算机 => 互联网 => Libra 币 + + +货币有着巨大的网络效应,留给后来者的时间窗口极短。我想,我们都应该思考怎么去面对这件事情了。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/用户故事站在更高的视角看架构.md b/专栏/许式伟的架构课/用户故事站在更高的视角看架构.md new file mode 100644 index 0000000..1c62632 --- /dev/null +++ b/专栏/许式伟的架构课/用户故事站在更高的视角看架构.md @@ -0,0 +1,95 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 用户故事 站在更高的视角看架构 + +你好,许式伟老师的架构课已经更新了一段时间,不少同学反馈这段时间的学习很有收获,于是我们邀请了Aaron同学来和我们做一次分享,看看他这阶段都是怎样学习课程的,又有哪些收获。今天这节课为加餐,形式和正式的课程内容不同,需要你阅读文稿来学习,好,我们开始吧。 + + +我是 Aaron,一名 Python 软件工程师, 目前主要从事运维开发DevOps工作。 + +一般开发的系统是内部使用的运维系统,最近主要的工作就是将产品需要的一些功能嵌入到类似CMDB系统当中,比如SaltStack、ELK(EFK)、Zabbix 等。 + +目前,运维自动化的困境是运维研发资源能力的不足,底层自动化的能力可以通过IaaS公有云来解决,但是在OS之上的运维自动化都是通过一些开源工具来解决的。开源工具的引入,加大了维护的难度和复杂度,而且,很多平台的可扩展能力非常薄弱。 + +理解这些开源工具的内部实现原理、优缺点,了解可以改善的地方,实现项目的深层次进化,是自己的额外追求。因为这个追求,学习架构设计就很有必要,从架构的思维来看这些迭代了五六年的开源项目,可以看到开源软件开发者的迭代开发流程和思考。发上等愿,结中等缘,享下等福,运维开发在公司偏辅助业务,我希望努力学习基础架构来实现自身价值。 + +我为什么要学习架构课? + +极客时间推出《许式伟的架构课》的时候 ,我正在工作时间之外学习操作系统、计算机网络等课程。看了这门课的介绍觉得挺好的,知道作者是七牛云的许式伟老师,因为自己工作方向的原因,经常会接触到关于Go语言布道师(许式伟老师)的一些介绍,对于许老师非常崇拜 ,特别是许老师多年前就预测到 Go 语言会称霸云计算这一点,我特别佩服。 + +另外,我也一直希望能借助许老师的视角和高度,来看看架构这件事。 + +如何将用户的需求,一步步分解为程序员要实现的功能点,并展现给用户,这是架构师的职责。设计高可用、高性能、高并发的可靠系统需要匠心,悟心,保持谦和求取的心态。架构思维并不难 ,但是成为优秀的架构师却不易。我很想知道在许老师这样的高度上,他对架构的理解是什么样的。 + +我也希望可以通过对专栏的学习,能在工作实践中有所成长,成为优秀的架构师。所以我购买了《许式伟的架构课》,并开始学习。 + +我是怎样学习专栏的? + +我学习这个专栏没有什么特别之处,无非做到了三点:反复学习、动手实践、留言打卡坚持。 + +许老师的课程是每周二、五更新,一般情况下我都会在当天先通读一下老师的文章 ,特别好的文章会通读好几遍 ,周末的时候会再拿出来细看 。 平时上下班路上的细碎时间,也会抽空看一次 、听一次来巩固学习。 + +具体算下来,整个学习的过程,基本上也和别的同学分享的类似,基本可以读懂的文章,学习两到三次;不是很精通的,云里雾里的,会至少看到五六次;听一遍不够,再看一遍也不行,那就再多读几遍,查看留言,看看其他同学的留言内容,来检验自己是否理解了,问题是否完全解决了。 + +许老师专栏中提到的实战项目, 像JS和Golang 的项目 ,我都要自己仔细敲一遍 ,落实到代码上。因为如果只是浮光掠影的话 ,不会理解到精髓的。知识还是要下苦工夫才能消化。 + +另外,我基本一直坚持学。可能很多同学可以看到我在老师课程下坚持留言,因为极客时间的课程我购买了不少,有些跟了一段时间就暂时搁置了,所以我在跟许老师的架构课程之初,就定下了Flag:一定要在老师正式的课程下多留言打卡,虽然留言质量未必很高,但是至少有一种方式可以让我尽量坚持下去。 + +专栏中最有收获的文章是哪几篇? + +介绍几篇我个人觉得很有收获的文章: + + +《[01 | 架构设计的宏观视角]》 + + +很多开发人员对众多系统背后是如何工作的,原理一知半解 ,知其然不知其所以然,成为架构师就是成长为造房子的建筑工程师,需要宏观的全局掌握能力。 + + +《[17 | 需求分析(上)]》和《[18 | 需求分析(下)]》 + + +为什么要做需求分析呢?一是为了满足用户需求,二是满足边界的需要 ,三是架构设计的需要,防止过度设计, 把简单的事情复杂化。 + +如果只是被动接受产品需求,以按图索骥的方式做架构师,是不足以成为顶级架构师的,用户需求的深层理解是很难传递的。 + +产品设计过程需要架构师的深度参与,而不是单向的信息传递。产品是桥,一端连接用户需求 ,一端连接了先进的技术。 + +产品经理和架构师其实是一体两面,都需要关心用户需求和产品定义。架构师需要三分之一的精力(我目前感觉可能会更多一些)在需求分析上。 + +许老师介绍的这些点,都很有价值: + +“心态第一,装着用户,刨根究底,找到根源需求,理清需求,对需求进行归纳整理。需求分析,用户的需求反馈到架构师那里,需要对功能进行拆解,对于部分超前的需求,分期进行实现。” + + +《[22 | 桌面程序的架构建议]》 + + +老师对 MVC 的讲解,当时看完了之后感觉到醍醐灌顶,看到下面留言说 ,这一篇文章就值回“票价”,我也是同样的感受。 + +后面的我就不一一举例了。 + +有哪些好的学习方法? + +学习的方法无非就是坚持,坚持,坚持 !夯实基础,夯实基础,夯实基础! + +去年的时候看到一位深度学习的大佬 Lan Goodfellow 的访谈, 说他人工智能本科阶段和博士早期阶段曾经多次咨询吴恩达老师寻求建议。 吴恩达建议他彻底精通这些基础知识和技能 (编程、调试、线性代数、概率论等等), 特别是基础数学。 + +以前的他特别不理解这个建议,觉得这些实在是很无聊 ,他以为吴恩达能建议他去学习超实数或者类似的东西 ,实践几年后,他发现老师当时给的建议实在是太正确了。 + +学习没什么捷径,不存在什么一蹴而就的高超技术,要真的能稳得住啃那些基础,能沉下心坚持再坚持。 + +夯实基础,坚持下去,就几个字,做到不易,愿你我共勉。每个开发者都希望能够有银弹,架构设计的其中一种是基础架构,涉及操作系统、分布式系统、嵌入式系统、数据库、计算机网络等,这些都需要扎实的基础作为后盾。研发底层基础设施,这个是自己近期的目标。 + +有很多人说 ,第一份工作带给你的能力,眼界,和圈子是非常重要的,一个人的能力决定他的上限,圈子决定了他的下限。 + +我对毕业之后的第一份工作没有足够的重视 ,发展不佳,但是非常庆幸参加了许老师的课程, “受益终生”四个字不足以完全表达我的谢意。 希望老师的公司发展蒸蒸日上, 也祝一起在极客时间学习的我们在努力坚持之后会心想事成 ! + +平时对外输出文字的机会不多 ,些许胡言,希望能够给大家抛砖引玉,谢谢大家。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/答疑解惑想当架构师,我需要成为“全才”吗?.md b/专栏/许式伟的架构课/答疑解惑想当架构师,我需要成为“全才”吗?.md new file mode 100644 index 0000000..f9ca4a1 --- /dev/null +++ b/专栏/许式伟的架构课/答疑解惑想当架构师,我需要成为“全才”吗?.md @@ -0,0 +1,202 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 答疑解惑 想当架构师,我需要成为“全才”吗? + 你好,这里是极客时间编辑部。 + +不知不觉,“许式伟的架构课”专栏已经更新了3个多月,我们的后台收到了同学们数以千计的留言。许式伟老师每天都在实时关注着留言并回答同学们的问题,同时根据同学们的留言不断优化调整课程的设置。 + +老师和同学在留言区的互动也十分热闹精彩,今天我们就精选出一批留言,一起来看一看。 + +精选问答 + +1.老师好,人精力有限,如果什么都懂,那不是不精了?通才还能做架构师吗?还是“一专多能”,先“专”,精通一样;再“多能”,了解其它技术? + +答:挺好的问题。架构师绝对不是要把自己打造为全才。架构师掌控全局的核心思想是打通经络,让自己的内力在全身自然流通,浑然一体。在不影响理解的情况下,你需要放弃很多实现细节的专研,但有一天你需要细节的时候,你能够知道存在这些细节,并且快速钻研进去。 + +2.许老师,自己现在已经工作快三年了,想往架构师这个方向走,但现在自己有些迷茫,接触到的技术也算挺多了,但不知道该如何入手架构师,之前您也提到过先广度然后深度,但我想问达到什么算广度够了,怎么进行深度学习? + +答:架构师核心是把知识串起来,构建一个完整的认知,不留疑惑。大部分知识是不需要深入细节的,只在你需要的时候深入,但深入的时候要很深。 + +3.如何来确定需求中哪些是稳定的?对架构角度,关注需求到什么层次? + +答:挺好的问题。需求分析的重要性怎么形容都不过分。准确的需求分析是做出良好架构设计的基础。很多优秀的架构师之所以换到一个新领域一上来并不一定能够设计出好的架构,往往需要经过几次迭代才趋于稳定,原因在于新领域的需求理解需要一个过程。除了心里对需求的反复推敲的严谨态度外,对客户反馈的尊重之心也至关重要。 + +4.老师好,我有三个问题。 + + +现在运维开发基本上都用Go,Python慢慢变少了,Java也少用了……现在运维开发是要学Go吗? +架构师要学数据结构和算法吗?很多都说算法是“内功”,中小公司好像学了都基本用不到。 +现在不是流行Docker+k8s、微服务、DevOps、AI等,那些主流技术都要了解吗?OpenStack云计算这两年基本不讲了,是否不用学习? + + +答:关于你的三个问题,我的意见是下面这样的。 + + +学Go挺好,建议学,生产效率很高的开发工具。 +“算法用不到”其实更准确的说法是“想不到”,或者是已经有人实现了你只需要调用,不需要自己实现。但是只有你知道了背后的道理,你才能明白算法对应的限制在哪里,什么情况下应该用什么算法。 +高阶的技术可以按需学,按精力学,更根本的还是要打好基础,这也更有助于你判断是否应该深入学习某些技术。 + + +5.“你可以发现,引入了输入输出设备的电脑,不再只能做狭义上的计算(也就是数学意义上的计算),如果我们把交互能力也看做一种计算能力的话,电脑理论上能够解决的计算问题变得无所不包。” + +交互能力也看做一种计算能力吗?这句话应该怎么理解呢?这种交互不就是输入和输出?哪有计算? + +答:广义的计算包含有副作用的函数(有IO的函数)。因为数据交换本身也是计算的需求,否则计算没有办法与现实世界相互作用。任何工具都需要解决现实问题才有用。计算器还有交互按钮呢,并不是只有纯正的计算。 + +6.我要做一个最小机器人系统,需要考虑需求的变化点和稳定点。该怎么考虑呢? + +答:挺典型的问题。这个问法是一种典型的需求陈述误区。 + +描述需求需要有几个典型的要素: + + +用户,面向什么人群; +他们有什么要解决的问题; +我解决这个问题的核心系统。 + + +只有满足这几个要素的需求才能进一步讨论变化点和稳定点。最小机器人可能符合上面的第三点,但是用户人群和要解决的问题没有描述,也就无法进一步去思考到底哪些因素是稳定的,哪些是易变的。 + +7. 编程框架和编程范式具体有什么区别呢?感觉它们都具备约束、规范的作用。 + +答:最主要的差别是:编程框架通常是领域性的,比如面向消息编程是多核背景下的网络服务器编程框架;编程范式则是普适性的,不管解决什么领域的问题都可以适用。 + +8.老师提到了如果需要重修数据结构这门课程,大学里面学的数据结构是不顶用了。那应该学习什么呢,您可以给个建议吗? + +答:这方面的资料不太多。可以给你一个我当年翻过的资料: Purely Functional Data Structures + + +https://www.cs.cmu.edu/~rwh/theses/okasaki.pdf + + +你可以参考看看。 + +9.老师您好,我不太理解您说的继承是个过度设计的原因,我目前在架构过程中大量使用了继承,而且我也觉得继承功能将我的代码功能高度抽象化,给我带来了很大的方便。我想咨询下您如果不是用继承的话,用什么方法替代继承的功能呢? + +答:建议继承只使用接口继承;正常情况下,优先用组合;当然因为大部分语言的组合功能不够强大,有时候从便捷性的角度继承可以适度使用,但是应当意识到如果过度使用继承对工程来说是有害的。 + +10.老师授课知识的角度很有深度,更贴切地说是一种思维方式,这种深度思考,从事情的本质重新推演与复盘的思考方式是很值得学习的。因为我们大部分人应该都没有想过自己去重新设计一个计算机的实现。不知道我这么理解的对不对。 + +答:从无到有到万物,我们这个课的脉络之一就是重新从零构建整个信息世界,这一点在开篇词中提到过,这一点非常非常关键。另一个脉络是架构思维的递进,这一章重点是需求分析。这两个脉络相辅相成,交织在一起。 + +11.许老师,您好,虽然自己是科班出身,但是对于下面这个问题困扰了我很久。 + + +一般来说程序的运行需要OS的支撑,那么在BIOS之前,选择运行哪个操作系统那一段程序是怎么运行的?再问一句,编译器可以独立于操作系统运行吗? +可以用C语言去实现很多其他的语言,比如说Python、Go等,那在C语言之前,这么一直追寻下去,会衍生成鸡生蛋的问题。但是编程语言又具有自举的功能,那自举是怎么实现的?比如目前版本的Go核心实现中,很多是用Go本身实现的,它是怎么做到自己编译自己的? + + +答:先回答你的第一个问题,程序运行不需要操作系统支持,有BIOS支持就可以(把控制权交给它)。编译器可以独立于操作系统存在,而且它应该先于操作系统产生。 + +接下来是第二个问题,语言诞生的过程是这样的:机器码 =>汇编 =>C =>C写的汇编、C写的C(自举)。当然这个过程不需要每次新架构的CPU或操作系统都重新来一遍,因为人是聪明的,发明了交叉编译这样的东西,C =>新平台的C,这样就一下子完成整个语言的进化了。 + +12.许老师:选择某种语言无关的接口表示;能举个例吗? + +答:先看看是网络协议层的接口,还是跨语言的二进制接口。 + +前者比如protobuf之类就挺好,后者可以了解一下IDL之类的东西,不过我觉得都有点重。如果要跨语言,我的建议在网络协议层跨,或者用操作系统的动态库机制(有点原始但很轻);如果语言内的接口,就别太复杂了,用语言自己的机制挺好的。 + +13.请问每个应用的虚拟内存地址是怎么分配的?起始地址都是0吗?函数F可以跨多个虚拟内存页吗? + +答:操作系统会保留一个地址空间,0通常也在保留区间内,因为0开始往往是中断向量表的地址,其他的地址区间怎么分配其实应用自己说了算。函数和数据都可以跨内存页。 + +14.老师您好,有两个问题希望解答。 + + +淘汰的内存页数据保存在哪里;是保存在外置存储设备中吗; +CPU加载对应程序的代码段到内存中,那么CPU是如何知道这个对应程序的代码段在什么位置的呢? + + +答:第一个问题:是的,保存在外置存储中。对于unix系的系统往往是swap分区;windows则是一个隐藏属性的.swp文件。- +第二个问题:代码段在哪里,是操作系统约定的,因为负责加载的人是操作系统,它设计程序文件的数据格式。 + +15.使用Java四年了,看到封装,继承,多态的描述,特别精准,又有了更深刻的理解。不了解Go语言,比如有一个表单的基类,里面有基本的处理,子类继承这个基类,有自己特殊的实现。这种情况,如何用组合实现呢? + +答:这是受继承思维的影响了。其实继承实现了代码复用和多态两个东西,揉在一起。在Go里面,组合实现代码复用,接口实现多态,彼此完全独立,非常清晰。 + +16.请问一下CPU是如何检查是否有中断的。是怎么及时知道发生了中断?每执行完一条指令都去检查一次吗? + +答:挺好的问题。硬件中断和软中断不一样。硬件中断你可以理解为总是会定期检查。软中断本身是一条指令,所以不存在检查这样的概念。 + +17.交叉编译是什么意思,不是很理解,老师能讲讲吗? + +答:其实理解清楚一个实质:编译器就是把高级语言翻译成为机器码,更抽象说,它其实就是格式转换器。 + +目标格式是不是编译器正在运行的环境并不重要,只不过如果目标格式刚好是当前机器的CPU+操作系统,那么目标格式就可以直接执行,否则就编译出一个当前环境下无法执行的目标格式,这种情况就叫交叉编译。 + +18.关于外存管理,有个问题从之前就困扰我。 + +磁盘的IO是由CPU完成的吗?但之前见到的说法是“CPU只能操作内存”。既然今天又提到了这个问题,文中提到“大量的磁盘 IO 操作,非常占用 CPU 时间”,那这两种说法是否矛盾? + +还想知道磁盘中的数据是怎么被加载到内存上来的呢?另外,更多的文章是说,“CPU的速度远远大于磁盘IO,CPU经常需要‘等待’磁盘IO”,这明显也是一种将CPU和外存割舍开的一种说法,而且按这种说法,CPU不光无需分配很多时间片给IO,而且还有很多“等待”时间。这也和本文中“非常占用CPU时间”相矛盾吧? + +答:所有外设CPU都统一基于数据交换(IO)的方式操作。CPU并不知道数据的含义,但是设备的使用方和设备知道。 + +这种情况下你可以简单理解CPU只是一根网线,但是很重要的一点是它让设备使用方和设备可以交互。CPU并不负责磁盘IO,但是它要等它结束以接收数据。这方面当然也有一些新技术出现改善这一点,可以想一想可能的优化路径,这里不表。 + +19.有一个疑问:协程属于用户态的线程,它跟线程之间怎么对应呢?协程之间也需要切换,那线程切换的那些成本它一样有啊,没想明白它的优势在哪。 + +答:从单位时间成本来说,有一定优势但也不会特别大。主要少掉的代价是从用户态到内核态再回到用户态的成本。 + +这种差异类似于系统调用和普通函数调用的差异。因为高性能服务器上io次数实在太多了,所以单位成本上能够少一点,积累起来也是很惊人的。 + +20.这种对需求的前瞻性探索挺重要,但同时感觉也是最难的,应该如何培养呢 + +答:很多时候是思维方式的转变。首先要尝试去做前瞻,预测错了并不可怕,但可以事后复盘到底是缺失了什么重要的信息让你判断出现了什么偏差。 + +21.隐隐感觉到架构的主要难点在于对需求的前瞻性判断,这要求的不仅仅是技术能力。目前几乎所有的架构课程,都是基于确定的需求来讲技术架构,例如秒杀系统怎么做高可用高并发。不知道我这么理解对不对。 + +答:架构在于创造,如果你从事的事情总是重复别人,那这个公司又有何价值?即使有所参考,也应该有自己的精气神,这个精气神是需要架构师把它干出来的。 + +精选学习留言 + +恭喜@有铭和@Enthusiasm 两位同学,你们的留言被选为精选留言,极客时间将送出价值99元的专栏阅码一份。1个工作日之内,工作人员会与你取得联系。 + +@有铭 同学留言 + +对象范式的原始概念其实根本不包括类和继承,只有1.程序由对象组成,2.对象之间互相发送消息,协作完成任务。 + +最初世界上第一个面向对象语言是 Simula-67,第二个面向对象语言是 Smalltalk-71。 + +Smalltalk 受到了 Simula-67 的启发,基本出发点相同,但是最大的不同是Smalltalk是通过发消息来实现对象方法调用,而Simula是直接调用目标对象的方法。 + +Bjarne Stroustrup 在博士期间深入研究过 Simula,非常欣赏其思想,C++的面向对象思路直接受其影响,因为调用目标对象的方法来“传递消息”需要事先知道这个对象有哪些方法,因此,定义对象本身有哪些方法的“类”和“继承”的概念,一下超越了对象本身,而对象只不过是类这个模子里造出来的东西,反而不重要。 + +随着C++的大行其道,继承和封装变成了面向对象世界的核心概念,OOP 至此被扭曲为 COP ( Class Oriented Programming,面向类程序设计)。 + +但是COP这套概念本身是有缺陷的:每个程序员似乎都要先成为领域专家,然后成为领域分类学专家,然后构造一个完整的继承树,然后才能 new 出对象,让程序跑起来。 + +到了 1990 年代中期,问题已经十分明显。UML 中有一个对象活动图,其描述的就是运行时对象之间相互传递消息的模型。1994 年 Robert C. Martin 在《 Object-Oriented C++ Design Using Booch Method 》中,曾建议面向对象设计从对象活动图入手,而不是从类图入手。 + +而 1995 年出版的经典作品《 Design Patterns 》中,建议优先考虑组合而不是继承,这也是尽人皆知的事情。 + +这些迹象表明,在那个时候,面向对象社区里的思想领袖们,已经意识到“面向类的设计”并不好用。只可惜他们的革命精神还不够,Delphi 之父在创建.Net Framework 的时候,曾经不想要继承,在微软内部引起了很大的争议,最后是向市场低头,加上了继承。 + +2000 年后,工程界明确提出:“组合比继承重要,而且更灵活”,Go和Rust也许是第一批明确的对这种思路进行回应的语言,它们的对象根本不需要类本身来参与,也能完成对象范式的多态组合。 + +历史让 C++走上了舞台,历史也终将让 COP 重新回到 OOP 的本来面目 + +@Enthusiasm 同学学习笔记 + +总结:设计系统架构的前提是用户需求分析,用户需求包括分析出稳定需求点和变化需求点。从功能上看,稳定需求点一般是实现偏核心需求的需求点,变化需求点往往是实现偏扩展性需求的需求点。 + +从层次结构上看,稳定需求点往往在系统层次的底层,而变化需求点往往在更加抽象层(上层)。从从属关系上看,稳定点需要提供功能给变化点使用,变化点调用稳定点提供的功能。从时间顺序看,稳定需求往往先现是变化点实现的基础,变化点通过调用已经实现的稳定点提供的功能来实现更为抽象的功能。 + +系统架构类似于一个栈的结构,人机交互(变化点)放在栈顶,底层工作(稳定点)置于栈底。 + +这节课程让我联想到网络中的OSI 7层模型。大概其也体现了这种软件架构思想。好处就是架构清晰,职责明确,功能规范等等。 + +以往我认为的架构设计类似上面的描述,描述起来类似按自顶向下顺序,采用分治思想完成。但许老师的方法又有些巧:架构好比搭积木,许老师是先有了很多积木(需求点),然后把再确定这些积木放在哪一层次的格子里。这简化了架构设计的难度,好比用市场经济代替计划经济,很有趣。 + +架构设计博大精深,灵活多变,初学课程的我们,对架构设计的学习,也只能算是盲人摸象。 + + + +如果你在课程中有看不懂的地方,有想解答的架构问题,或者想分享的实战经验,都可以在文章下留言,如果你的留言被选中作为精选留言,我们将会为你送出价值99元的阅码一份。欢迎留言,与许式伟老师一起交流讨论,教学相长,共同精进。 + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/结束语放下技术人的身段,用极限思维提升架构能力.md b/专栏/许式伟的架构课/结束语放下技术人的身段,用极限思维提升架构能力.md new file mode 100644 index 0000000..701751e --- /dev/null +++ b/专栏/许式伟的架构课/结束语放下技术人的身段,用极限思维提升架构能力.md @@ -0,0 +1,138 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 结束语 放下技术人的身段,用极限思维提升架构能力 + 你好,我是七牛云许式伟。 + +这个专栏从去年 4 月份至今已经有 10 个月左右的时间,到了要和你说再见的时候了。感谢你的一路相伴,感谢你的坚持。也希望这些内容能够对你有所帮助。 + +从工程角度来说,架构师的存在几乎是一种必然。传统项目工程也有架构师的角色,只不过软件工程有其特殊性,它快速变化,充满了不确定性,所以架构师的重要性的比重会被进一步放大。 + +但是如何才能成为优秀的软件工程架构师? + +传统的架构图书往往从架构思维开始。但是,我认为它们错了。这里面最关键的问题在于: + + +架构并不是 “知识点”。 + + +架构思维的确非常非常重要。但是,熟读架构思维并不足以让人成为一名优秀的架构师。 + +关于这一点,我经常拿中国传统的武学文化做类比。武功招式可以精确传授,是 “知识点”,掌握了就是掌握了,理论上可以做到分毫不差。但是,架构不是武功招式。它更像内功,它不是 0 和 1,没有清晰的掌握和没有掌握这样泾渭分明的区别。 + +在架构能力上,没有最好,只有更好。 + +这是为什么我们的架构课并不是从架构思维开始,而是采用双线结构。它基本上围绕着以下两个脉络主线来展开内容: + + +如何从零开始一步步构建出整个信息世界; +在整个信息世界的构建过程中,都用了哪些重要的架构思维范式,以及这些范式如何去运用于你平常的工程实践中。 + + +这两大脉络相辅相成。 + +首先,我们通过还原信息世界的构建过程,剥离出了整个信息世界的核心骨架,这也是最真实、最宏大的架构实践案例。 + +其次,我们结合这个宏大的架构实践来谈架构思维,避免因对架构思维的阐述过于理论化而让人难以理解。 + +最后,架构就是对业务系统的正交分解。因此,整个信息科技的演化过程,自然而然形成了分层:基础架构 + 业务架构。 + +基础架构的产生是对业务架构不断深入理解的过程。越来越多的共性需求从业务架构抽离出来,成为信息科技的基础设施。 + +作为架构师,我们需要坚持对业务进行正交分解的信念,要坚持不断地探索各类需求的架构分解方法。这样的思考多了,我们就逐步形成了各种各样的架构范式。 + +这些架构范式,并不仅仅是一些架构思维,而是 “一个个业务只读、接口稳定、易于组合的模块 + 组合的方法论”,它们才是架构师真正的武器库。 + +这个武器库包含哪些内容? + +首先,它应该包括信息科技形成的基础架构。努力把前辈们的心血,变成我们自己真正的积累。光会用还不够,以深刻理解它们背后的架构逻辑,确保自己与基础架构最大程度上的 “同频共振”。 + +只有让基础架构完全融入自己的思维体系,同频共振,我们才有可能在架构设计需要的时候 “想到它们”。 + +这一点很有趣。有些人看起来博学多才,头头是道,但是真做架构时完全想不到他的 “博学”。 + +从体系结构来说,这个基础架构包含哪些内容? + +其一,基础平台。包括:冯·诺依曼体系、编程语言、操作系统。 + +其二,桌面开发平台。包括:窗口系统、GDI 系统、浏览器与小程序。当然我们也要理解桌面开发背后的架构逻辑,MVC 架构。 + +其三,服务端开发平台。包括:负载均衡、各类存储中间件。服务端业务开发的业务逻辑比桌面要简单得多。服务端难在如何形成有效的基础架构,其中大部分是存储中间件。 + +其四,服务治理平台。主要是以容器技术为核心的 DCOS(数据中心操作系统),以及围绕它形成的整个服务治理生态。这一块还在高速发展过程中,最终它将让服务端开发变得极其简单。 + +理解了这些基础架构,再加上你自己所处行业的领域知识,设计出一个优秀业务系统对你来说就只是轻车熟路而已。 + +这也是为什么这个架构课的内容结构是目前这个样子组织的。因为消化基础架构成为架构师自身的本领,远比消化架构设计原则,架构思维逻辑要难得多。 + +消化基础架构的过程,同时也是消化架构思维的过程。 + +把虚的事情往实里做,才有可能真正做好。 + +当然提升架构能力,不完全与成为架构师这件事情等同。 + +架构能力其实是一种属性,并不是只有架构师需要架构能力。软件开发工程师、SRE、甚至包括产品经理,都需要具备架构能力。 + +而架构师这个特殊的岗位,则是因为软件工程的需要而产生的。它从更全局的视角来把控工程的演进方向,以确保整个业务系统经历几年甚至几十年的迭代,仍然可以快速适应变化,而不至于老化。 + +成为架构师并不是一件纯技能的事情。 + +架构师需要放下技术人的身段,学会 “共情”。与用户共情,理解用户的所思所想。与开发人员共情,理解技术人的所思所想。与公司共情,理解公司的发展诉求。 + +架构师需要学会 “认同他人,反思迭代自己”。不要在不了解背景的情况下,随意推翻别人写的代码,而理由可能仅仅是不符合你的个人风格。当然反过来完全看不到项目的问题同样要不得,但这往往是受限于个人能力。要提升自己的架构水平,需要在实践中不断反思,不断在自我否定中成长。 + +不过我们今天把话题的重心收敛到架构能力上。怎么才有意识地通过训练来提升自己的架构能力? + +实践对架构能力不可或缺。 + +在现实中,不少技术人员连函数规格都想不清楚。他们关心你是怎么 “实现” 的,但是却不关心 “接口规格” 是什么样的,接口规格是否符合函数的 “业务语义”。 + +要提升架构能力,首先得做到规格为先,而不是实现为先。不要动不动问怎么实现的。要首先谈这个规格合不合理,是否存在多余的依赖。进一步来说,要多去谈这个函数(或软件实体)的业务范畴合不合理,是否应该换一个切分的姿势。 + +其实 review 自己的代码也是一种极佳的架构能力的提升手段。对自己刚刚写完的代码,去 review 它,从中找出问题。如此反复训练,就能实现自我能力的提升。 + +这其实是最高效的自我提升的方式。如果团队其他成员 code review 发现了你的问题,你得反思一下为什么自己发现不了。 + +很多人追逐实现新的业务系统,通过做新系统来找到满足感。但是实际上对架构师来说,恰恰是反复打磨既有系统是更加锻炼人的。如果你一年前实现的系统今天仍然很满意,那就需要警醒,因为这一年你在原地踏步。 + +在架构能力上,没有最好,只有更好。 + +这里我想分享一段我自己 review 自己代码的特殊经历。事情发生在我大学期间,当时的电脑相对我们大部分学生的购买力来说,还是非常昂贵。所以我和另外 4 个同学花了 7500 元合买了一台电脑。 + +结果就是,我们 5 个人轮流使用这台电脑。这意味着,我一周平均只能用一天多一点时间。再刨除上课时间,我真正能够上机的时间并不多。 + +而当时的我对编程非常着迷,所以我绝大部分的上机时间都花在编程上。作为物理系的学生,正常来说我学的编程语言是 Fortran。但我很快就把 Fortran 课程自学完了,并从老师口中和 Fortran 课程的附录中了解到了 C 语言。 + +于是我找物理系高年级的同学搞到了 Turbo C 2.0,开始翻遍学校图书馆的图书自学 C 语言。 + +为了能够高效利用一周只有一天多的上机时间,我尝试把程序写到纸上,并且提前进行 code review,确保尽可能多地发现程序中的错误,以减少上机过程中的调试时间。 + +在一次数学建模竞赛里,我和另外两位同学(廖唯棨和程胜峰)一组,其中用到了 Dijkstra 的最短路径算法。看完算法逻辑的介绍后,我直接一遍写成最终的代码,没有经过任何调试过程。 + +这让在旁边看着的同学廖唯棨觉得很神奇,问我是不是之前实现过 Dijkstra 算法。但其实于我而言,这不过是长期养成自我 review 代码习惯的结果而已。 + +这个习惯持续了三年之久。这三年里,我开始的时候都是先把代码写到纸上并完成 review,然后再到电脑上。但是到后期这个习惯就变了,我不再需要把所有细节都提前写到纸上,而是只需要提前准备好骨架:整个程序串起来的思路是什么。 + +我大学期间写过很多高代码量的程序。其实第一个 C 程序就不短,是一个仿 DOSKEY 的程序。后来也做过汇编语言的 IDE。这是因为学汇编的时候,发现没有好的汇编语言集成环境,于是就自己做了一个。至于为什么学汇编?是因为我想写一个 C++ 编译器,感受一下语言实现者的体验。另外,我也尝试在 DOS 操作系统下实现了一个图形界面库,并用它做了图片查看器和 MP3 播放器。 + +在代码量非常大的时候,人的脑容量就完全无法把这个实现装到头脑中。这时 “规格重于实现” 背后的意义就完全体现出来了。通过规格串起整个业务系统,以此把业务系统装到脑子里,这就是很朴素的架构 “骨架” 思维。 + +这不是一个假想实验。 + +它是我的亲身经历。这段经历启发我意识到极限思维对架构能力提升的重要性。 + +架构没有最好,只有更好。在极有限的上机时间里,在没有电脑的情况下,我们只能选择把更多的逻辑装进脑子里。 + +这个过程还可以更进一步。我们不断训练自己对不同业务领域的架构范式的理解。直至最终,我们头脑中可以装得下整个信息科技的骨架。 + +到那时,单就架构能力而言,你就是最顶级的架构师了。 + +备注:我在文末准备了一份调研问卷,也欢迎你点击下方的图片参与调研,期待你的反馈。 + +](https://jinshuju.net/f/zxjsq8) + + + + \ No newline at end of file diff --git a/专栏/许式伟的架构课/课外阅读从《孙子兵法》看底层的自然法则.md b/专栏/许式伟的架构课/课外阅读从《孙子兵法》看底层的自然法则.md new file mode 100644 index 0000000..8fcf427 --- /dev/null +++ b/专栏/许式伟的架构课/课外阅读从《孙子兵法》看底层的自然法则.md @@ -0,0 +1,85 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 课外阅读 从《孙子兵法》看底层的自然法则 + 你好,我是七牛云许式伟。 + +近日在读《孙子兵法》,颇有感触。作为我国现存第一部,也是世界最早的军事著作。《孙子兵法》一直为众多政治家、军事家及企业管理者膜拜,被当作军事外交、企业管理的圣典。 + +时至今日,新经济、科技和社会结构下,很多人认为我们身处一种复杂社会,需要全新的军事、商业和公共策略。然而,不管远古农耕刀兵还是高科技信息社会竞争,时代、人文环境和科学技术的变迁,并不能改变人类社会生存竞争所依赖的基础系统环境和人性思维的规则。 + +英国空军元帅斯莱瑟曾说过:“孙武的思想有惊人之处——把一些词句稍加变换,他的箴言就像是昨天刚写出来的。” + +《孙子兵法》讲的是战争,骨子里包含的却是人类社会生存竞争的底层规则。案例会变迁,背后的道理却很难过时。我们作为架构师,需要做到对世界的认知可宏观、可微观。从对宏观的理解来说,背后所依赖的正是我们对这些底层自然法则的体悟。 + +明道 + +《孙子兵法》讲的是规则。它通过战争,结合环境和人性探讨竞争中生存的规则;然后,在确定目标下,它又将战争行为分解成为一系列细分的规则。 + +《始计篇》开端,全书先讲总规则,就是“道”。“道者,令民与上同意也,故可以与之死,可以与之生,而不畏危。”如同国家有纲领,企业必须明确愿景,使命或是价值观一样。明心方能正道,明道方知力之所指。事实上,明白道之所求,术方能有指引,追随者才能有方向、有激情。对国家、企业而言“道”不仅是方向,还是战斗力和吸附力的来源,明道和传播非常重要。 + +“慎战”。“兵者,国之大事,生死之地,存亡之道,不可不察也。”战争和企业竞争涉及系统存亡关系重大,是管理者每日都要考虑的事情。“自古知兵非好战。”这种行为不是个人名利或得失,而是关系到万户千家,或者全体员工和家庭,必须慎之又慎,不可凭个人好恶,更不可好大喜功和拍脑袋。“上兵伐谋”、“不战而屈人之兵”。要牢记初心,保存敬畏和责任感,求胜同时要避免己方生命和资源的巨大消耗。“善战者,无智名,无勇功。”正如“善弈者通盘无妙手”,或是扁鹊治病于未发,“立于不败之地”非常之关键。 + +“避害第一”,先考虑失败的后果;“先胜后战”,不打无把握的战争。“因利而战”,考虑成本和结果;“一战而终”,准备要充分,动手要迅捷,毕其功于一役。 + +“兵无常势,水无常形。”环境、资源条件,心态、优劣势这些都可能随时变化,不能教条主义,须将策略灵活应用于变化的实际。满口教义,如赵括那般纸上谈兵,一棒子打死算了。“君将士卒皆有其道”。公司内也应各司其职,人人满口战略、创新和文化,“所有人都举着旗帜,他们用哪只手来战斗?”这样的公司很危险。 + +庙算 + +“先胜后战”,就要“未战先算”。战与棋,一样考的算力,所谓政治经济环境人心都要纳入数据输入。 + +庙算,非治一战或局部。而是知己知彼综合所获数据,依据规则全面分析,很像是系统竞争成败的“大案牍数”。 + +庙算策略,不是单一事件的静态考量,而是复杂动态的综合考量。五事,道、天、地、将、法;七计,主、将、天地、法令、兵众、士卒、赏罚都要纳入进来。知己还要知彼。内外部环境,天时地形人才人心钱粮规章,所有影响战争胜负的东西,一切可以数据化,能算计的全都计算进去。 + +依据庙算可确定“势”,依据“势”决断是否有利并采取相应行动。拥有这样的洞察和决断力,知道了什么时候该打什么时候不该打,真正战争行为才会很简单。战争中要“致人而不致于人”。曾说“朕观诸兵书,无出孙武”的李世民,还有林彪都非常擅长集中优势兵力,通过忍耐和调动对手,“多方以误”,引敌人失误而获取最终胜利。 + +恰如“木桶理论”,庙算还可以消灭己方短板,寻找或者调动对方出现短板。当然,《孙子兵》并不是每战必胜,也不是以弱胜强,化不可能为可能的魔法书。不宜“知其不可而为之”。一系列庙算都在规则下,寻找到可能性,获得相对优势来战胜对手。如集中优势兵力,实现局部优势。“以迂为直,以患为利。”利用天时地形和行军,训练扎营做饭都有明确的目标。速战还是持久战,都是依据各种条件资源推演分析的结果。“故知战之地,知战之日,则可千里而会战。”唯有算胜,方可一战。 + +庙算为术,战术不能超脱于战略。“将在外君命有所不受”只是指根据实际情况可以灵活战术,正如当年毛主席要求林彪东北战役要服从大局,大战略不容颠覆。不能因一人之利,一地之利,一时之利而改变,只能随整个系统变化而调整。 + +感知 + +“不可胜在己”,立于不败之地,自己是关键,其他都是辅因;“可胜在敌”,看敌人的软肋和什么时候失误。“内省。知彼,但先要知己。”曾国藩作战,也首先讲明己第一,稳扎稳打立于不败之地,克敌才能实现。胜利也并非都是好事,很多时候当不是自己太强,而是对手太弱或其他因素,造就的胜利只会麻痹自己和埋下根本性失败的伏笔。 + +“知己知彼”就是自我和对手的感知。“天时地利人和”是对环境的感知。战争错综复杂风云变化,很多时候考较的都是随机应变的感知力和应对能力。“用兵者,合于利而动,不合于利而止。”对是否进行战争的条件的感知。还有对于细节的感知,“汲而先饮者,渴也。”需要通过经验学习感知获取信息,将规则载入情景化和有效利用。感知,可用于内部观察和防患未然,也可以用于判断对手并拟定对策。 + +“攻其无备,出其不意。”需要重视情报和传播。“兵者,诡道也。”情报和传播,等于数据的输入和输出,很多时候决定战争或组织的成败。依据情报奇兵制胜,如邓艾灭蜀;与对手虚假的情报,像蒋干盗书。更典型的是反间计。李牧、范曾罢黜,赵国、项羽败亡。 + +法度 + +“五事七计”都提到法令和奖惩。楚汉之争刘邦胜,“约法三章”以及陈平的有效奖惩体系都为功不小,胜在乎法令执行和奖惩公平。另一方面,用兵能“破釜沉舟”的项羽,然“印刓敝,忍不能予”是其失败的重要原因。 + +“五德,智信仁勇乎。”五德相须,缺一不可。但强调“智”,将人才放第一。而法度是规则,人才必须在法度内。 + +“主孰有道,将孰有能。” + +“将之至任,不可不察也。” + +“将听吾计,用之必胜,留之。将不听吾计,用之必败,去之。” + +“令行素。”平时一样重视,战时才能发挥作用。 + +对于复杂系统,内部崩溃往往是其失败的主要原因。当然如前所述,这种内因也可由对手或外因推动演化。对于企业来说,领导者素质和人才队伍,中坚力量的支撑很重要,而成为中坚的标准就是一致的价值观和遵循规范。另外,还要建立适应自身、对手和战争形态的管理架构和组织体系,以及有效的规章和奖惩体系。 + +辨证 + +“以正合,以奇胜。”《孙子兵法》可能是辨证思维最早期的著作和运用。 + +《孙子兵法》里,强弱虚实迂直恩仇甚至胜败都是辩证的,是可以对立转化的。 + +事实上,无论战争或是组织发展,都不是直线的,而是曲线的,或者周期的,充满了变化。胜负也是辩证和变化的,便是拥有《孙子兵法》,吴国后期数战数胜,开始骄傲自负,百姓疲敝,胜负环境逆转,终致败亡。“水因地而制流,兵因敌而制胜”。所有策略必须依据时机、环境、对手、人心变化而变化。 + +兵法不可能写尽所有情况,任何行为和结果也不是简单对应,而是因果交错。战争和系统竞争要处理的是复杂数据。《孙子兵法》注定“学者生,像者死”,必须根据特定时空、自己和对手的具体情况灵活化用。 + +《孙子兵法》重规则轻案例,舍事而言理,因而得以破越时空和领域,成为竞争、管理和生存的基础规则。 + +《大败局》一书中,吴晓波将中国大多数企业失败归结为缺乏道德感和人文关怀,缺乏对规则和秩序的尊重以及系统的职业精神的缺乏,这些因果在《孙子兵法》中都能够找得到对应;相对的,所有基业长青的公司,又都具备一系列适应变化,可以持续指导成长的一般性规则,这些规则在《孙子兵法》中也都能够找到对应。 + +作为一种超乎教材而更类似于宗教的力量,《孙子兵法》的价值在于其提供了实用性同时引导了对规则的思考。跨域2500年时空岁月,它的思想精髓仍如星辰闪耀。而且随着数据、时代演进、新元素的输入还会不断推动其演进和发展,即便再过100年,1000年,只要人类、系统和竞争存在,它就永不会过时。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/00开篇词|抛开争论,先来看看真正的低代码.md b/专栏/说透低代码/00开篇词|抛开争论,先来看看真正的低代码.md new file mode 100644 index 0000000..a00e400 --- /dev/null +++ b/专栏/说透低代码/00开篇词|抛开争论,先来看看真正的低代码.md @@ -0,0 +1,143 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 00 开篇词|抛开争论,先来看看真正的低代码 + 欢迎和我一同展开低代码的学习卷轴。 + +有人说我很“多情”,毕业至今 17 年,我“勾搭”过多种计算机语言和技术,有后台类的 Java、C、C++,有前台类的 TypeScript、JavaScript、HTML 和 CSS,还有不前不后的 Node.js;甚至还差点“误入歧途”转岗做 UX,2015 年我和团队还受邀组织了多次 UX 实战讲座。现在呢,我在中兴通讯担任软件研发资深专家。 + +看到这,你一定很好奇为什么是我来带你学习低代码?这就要谈及我“专情”的一面了。在低代码领域,我有幸成为了国内早期“吃螃蟹”的那批人,一直持续到现在,也算小有成就。 + +2018 年初,我收到一封闭关研讨的邮件,正式开启了我的低代码平台的架构和实现之旅,这个低代码平台叫 Awade。现在看来,我们的低代码平台的起步时间比国内绝大多数同行,包括各大互联网巨头都要早。甚至,如果将构建低代码平台的前序工作Web 组件集 Jigsaw的开发作为起点,那我们启动的时间就可以追溯到 2017 年 4 月甚至更早了。如果你感兴趣,可以点开链接了解一下这套专为低代码可视化开发打造的组件集。 + +经过 4~5 年的持续演进和积累,现在,由我主导的低代码平台 Awade,已经非常成熟了,成了中兴通讯事实上的标准实现。2021 年,Awade 更是获得了公司级的 CTO 专项奖,以鼓励我们在研发提效方面的贡献。 + +在低代码平台的应用方面,我们也有了满满的收获。到目前为止,我们主要是对内推广应用,采用低代码平台交付了 150+ 商用功能,主要客户是通讯运营商,全面覆盖了国内各大运营商,以及其他国家的知名运营商。 + +在我写这篇文稿时,2022 年巴塞罗那世界移动通信大会(MWC2022)正在进行。其中,中兴通讯展台里就有一组 App 是采用这个低代码平台开发的。这组 App 有酷炫的展示效果、丰富密集的交互功能、流畅的运行性能,不仅打破了低代码平台只能开发出又丑、又难用的 App 的刻板印象。更重要的是,它们定义了低代码平台能开发出高质量 App 的新高度,为低代码的支持者注入了信心。 + +你可以看看公开宣传资料里展出的这组 App 的 UI 效果图: + + + + + +从 2019 年开始,我逐渐在国内各大行业大会上分享我在低代码平台研发方面的各种经验,由此也结识了业内许多专家,深入了解了低代码在不同公司的多样实现方式,以及良莠不一的应用效果。 + +这次,我将我这些年在低代码架构、实现和应用方面的积累,整理成这个专栏分享给你,希望能帮你拨开迷雾,对低代码有更客观、更深入的理解。 + +争论不休:银弹 v.s 毒瘤 + +纵观我整个职业生涯,我从来没有见过哪个技术会长时间受到如此两极分化的评价,支持者将低代码奉为“银弹”,反对者称之为“行业毒瘤”。这两种极端评价的存在,充分说明目前低代码在各个企业中的实现效果良莠不齐,方式方法也各不一样。 + +做得比较好的企业,确实利用低代码技术获得了显著的收益:或是降低成本,或是提升效率,又或是兼而有之。因此,这些企业往往会把低代码技术奉为银弹,大力推广,持续获利。相反的,那些未能帮助企业解决实际问题的低代码实现方式,不仅无法降低成本、提升效率,反而起到了相反的效果,这个情况下,低代码不免就被贬损为毒瘤。 + +其实,无论是银弹也好,毒瘤也罢,两种评论共同说明了一个问题,那就是传统 Pro Code(纯代码)的开发模式与高速增长的业务需求之间产生的矛盾越来越尖锐,我们急需一种新的模式来消除这对矛盾,低代码就是业界共同给出的新模式。 + +不过,低代码模式目前仍处于探索期,甚至到现在都还没有一个已达成共识的定义,它就像是一个大框,啥东西都可以往里装。 + +在这个时间点上,用语言去争论低代码到底是银弹还是毒瘤,其实并没有太大的意义。沉浸在争论的迷雾中,你就无法客观、理性地看待这件事情。不如我们回归技术人的处理方式,看看目前一线低代码平台真正的架构和思路,用你的技术理性做判断。毕竟,在这个话题下,没有谁比低代码的一线开发者更有发言权了(将自己代入其中也不失为一个好方法)。 + +但要拨开迷雾,深入了解、学习低代码,确实不怎么容易。 + +学习低代码难在哪儿? + +低代码平台是一种非常复杂的综合系统,它的实现过程涉及到大量的通用技术、架构设计方法,需要开发大量的功能模块,代码量动辄达到数十万乃至百万行的级别,需要使用和无缝集成数以千计的开源技术。 + +这也就导致了学习如何开发低代码平台,与学习使用任何一门具体的技术都不一样。当前世界上并没有一个公认的低代码实现技术标准,哪怕是技术白皮书,简单地说,就是没有一个清晰的学习目标。 + +举个例子,4G/5G 通信协议是极其复杂的协议,学习起来显然非常不容易,但毕竟通信技术是有公认标准的,只要有恒心和信心,不停地攻克协议中的各个章节,总有一天能完成协议的学习,成为专家。但低代码不一样,虽然它的复杂度远没有通信协议那么高,但是它没有标准,学习它就意味着:没有起点也没有终点,没有正确也没有错误,没有考试也没有答案。 + +这样的状况对于在校学生来说是最舒服的,特别好“混”,但如果你要成为一位低代码的架构师或负责人,对你来说却是噩梦: + +我学到的知识真的就是低代码所需要的吗? + +有没有更好的架构思路和实现方法? + +为什么业务团队总是提出平台能力之外的需求?是我错了还是他们错了? + +我们常说,鞋子好不好只有脚知道,同理,低代码平台好不好,只有业务才有发言权。所以,面对这样一种知识,最合适的学习方式就是倾听他人的经验,听听别人是怎么成功的,也听听别人是怎么失败的。 + +虽然现在行业大会多数都有低代码专题,但以演讲形式分享低代码的实现经验实在太有限,也不成体系。对于低代码这样复杂、综合的系统来说,50 分钟左右的分享实在是杯水车薪,只能展示一些碎片化的知识内容,学习成本很高。而且不同业务背景对应不同的实施策略,有时甚至是矛盾的,不明就里只会越听越迷糊。 + +而专栏是一种系统展示低代码知识的极佳形式。从架构设计到演进策略,从细到代码级别的技术要点说明,到总体的技术选型思路等,我都会通过这个专栏,将我的经验充分、系统地展示给你。 + +如果你是一位一线开发人员,你不仅能知道当前大热的低代码到底是怎么一回事,也可以从专栏中学习到低代码编辑器各主要功能模块的具体架构方法,从而帮你提升架构能力,为未来独立架构一个功能模块做好准备,缩短从一线研发岗转型为架构岗的周期。 + +如果你是一位架构师,你可以从中学习到如何恰当地设计低代码编辑器和编译器之间的关系和抽象,从而架构出一套具有高度通用性的低代码编辑器,你也能知道如何围绕编译器提供扩展能力,设计出比较完备的低代码插件系统,实现通用与效率兼得。 + +如果你是一位决策者,那你可以从这个专栏中了解到实现低代码平台过程中的各个阶段的特点,以及采取什么样的策略可以确保平台始终朝着高通用性的方向演进,同时你还可以了解到采用哪些方法可以让平台兼具较高的开发效率和尽可能广的适用范围。 + +我会给你讲什么 + +在这个专栏中,我主要为你提供了低代码平台的核心模块,包括低代码编辑器主要功能的技术要点,以及实现思路和具体方法。 + +除了编辑器的实现技术要点之外,你可以从这个专栏中了解到低代码平台的架构策略和思路、从零开始打造一个低代码平台需要经历的阶段以及特点,甚至还包括低代码模式对应用全生命周期的支持,插件系统和生态圈的打造等内容。 + +我把这些内容整合成了下面这张知识地图: + + + +你可以从这张图中看到,居中的低代码编辑器是低代码平台的核心功能模块,它的能力基本决定了低代码平台的能力。之所以说它是核心,不仅因为它需要提供各种基础编辑功能、所见即所得的效果,更是因为它是整个平台所有功能的锚点,低代码平台上任何内置功能、扩展功能都是以它做为入口。 + +同时,多数锚在编辑器上的功能,其本身也具有非常高的复杂度,任何一个功能点都有相对独立的演进线路。比如代码生成器,它与编辑器之间的关系甚至可以决定平台的长期演进策略。插件系统则是给应用团队开放的扩展和定制的能力,用于解决通用性低代码平台在具体业务落地时的各种个性化问题。基础设施则是低代码平台的基石,它的特殊在于逆向性,它的研发不得不先于低代码编辑器,而集成时却必须完全融入低代码编辑器。 + +不过,出于对学习梯度的考虑,我并没有完全按照这张知识地图排布内容,而是将整个专栏分成了三个部分: + + + +你可以发现,这三部分覆盖了以低代码编辑器为核心,同时包括代码生成器及策略、基础设施、插件系统及周边等三大编辑器的主要研发支线延伸。其中低代码编辑器的内容占据了专栏的绝大部分,三大延伸内容也都覆盖了关键内容。 + +第一部分是认知基础与架构策略篇。 + +这部分中,我们不会涉及具体技术,而是主要从架构设计和演进策略等角度来学习低代码。所谓磨刀不误砍柴工,在启动低代码的研发之前,你肯定要对低代码有个大致的了解,同时也需要先有一个清晰的系统架构思路,确保各个模块有序开发和相互依赖。有明确的演进策略之后,我们才能确保演进过程能让好钢都用到刀刃上,资源不发散、不做无用功。 + +不仅如此,这部分还详细给出了在低代码平台启动研发之前,我们需要准备好的“家底”,哪些是必备的,哪些是可选的,哪些必须要自主掌握,哪些可以借开源社区的力,等等,帮你做到心里有数、有备无患,更好地规划好研发计划。 + +第二部分是核心模块开发篇。 + +低代码编辑器是低代码平台的核心模块,也是我们这个专栏的重点内容,占据了专栏的大部分篇幅,这部分我们会详细说明一个通用型低代码编辑器的技术实现要点。 + +我们整体以应用 App 开发三部曲(布局、交互、数据)为线索设计内容,我会从技术实现角度详细给出低代码编辑器的布局编辑器、属性编辑器、可视化编程编排、业务数据获取可能要用到途径等的实现方法。同时,这部分还覆盖了低代码编辑器的一些重要但容易被忽略的能力,包括多人协同编辑的支持、编辑历史管理、分支管理,甚至还包括如支持 Low Code(低代码)和 Pro Code(纯代码)混合开发等内容。 + +你会从具体的目录中看到,这一部分我会以低代码平台的代码生成器作为开始。这样安排,不仅是因为代码生成器是几乎任何一个低代码平台启动伊始就需要实现的功能,更是因为,多数人在开始实现代码生成器之前,不先考虑清楚它与低代码编辑器之间的关系就贸然动手,导致整个低代码平台的长期演进空间和拓展能力大大受限。 + +如果代码生成器与编辑器之间没有一个良好的松耦合关系,我们后面要提供插件扩展能力就很难了,而失去了插件的扩展性和定制性,会进一步导致通用型低代码平台在具体业务场景中的效率无法保证。失去了效率,低代码平台的效能等于打了半折。 + +第三部分是平台功能拓展篇。 + +这部分我们关注的是低代码平台开发能力之外的内容,主要包含了低代码在业务开发全生命周期各个环节中应该起到的作用,以及技术性、方向性建议。而且我们也会从技术实现方向,详细说明如何实现低代码平台的插件系统,从而实现低代码平台在具体业务场景中的定制、扩展,进而与业务团队一起形成一个低代码生态圈。 + +具体你可以看这张目录图: + + + +到这里你可能会发现,似乎这三部分并不是这个专栏的全部。确实,这是动态更新的专栏,第一阶段更新完后的四年之内,我会以每年 5 讲的频率,继续更新,带你去看最新、最前沿的低代码技术动态。内容主要有这些方面: + +增加低代码平台在 UX、需求端的能力的技术实现要点,以及在交付端的测试、运行能力的技术实现要点; + +Awade 的新技术、新场景、新应用,我会精选参考价值较高的部分更新到专栏中,分享给你; + +新业态剖析、相关开源技术实践与解析、新的调查机构报告解读等行业性内容。 + + + +在这漫长的征途中,我希望你可以和我、还有正在学习这门课的其他同学保持交流,分享你的学习心得和实践经验,并为课程未来的内容提供建议。 + +写在最后 + +我特别想说的一句话就是,探索低代码之路,我们才刚刚开始。 + +低代码是一个饱受两极化争议的技术方向,一方面大家对它有种种殷切期望,希望低代码能成为消除传统 Pro Code 的开发模式与高速增长的业务需求之间的矛盾,另一方面,低代码落地过程中出现的大大小小问题又很容易归咎于低代码,甚至怀疑低代码这个方向到底是对是错。 + +这里我想再引用前人的一句话,与你共勉:“虽然未来藏在迷雾中,叫人看来胆怯。但当你踏足其中,就会云开雾散。” + +如果你依然对低代码抱有疑虑,我想请你踏进来,看看低代码平台真正的样子,理性地作出判断。如果你已经身处其中,是一线低代码架构者和践行者,我希望你有坚定的信念继续坚持下去,同时我也希望这个专栏的内容,能帮助你在低代码探索之路上少走弯路。并且,我将在未来至少 4 年的时间里坚持更新,在低代码的探索之路上,你,并不孤单! + +如果你已经准备好踏上低代码的探索之旅,那么我们正文见! + + + + \ No newline at end of file diff --git a/专栏/说透低代码/01|低代码平台到底是什么样的?.md b/专栏/说透低代码/01|低代码平台到底是什么样的?.md new file mode 100644 index 0000000..110980d --- /dev/null +++ b/专栏/说透低代码/01|低代码平台到底是什么样的?.md @@ -0,0 +1,210 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 01|低代码平台到底是什么样的? + 今天我们正式开始了对低代码的学习。与某种具体技术不同,对于低代码的概念,业界至今没有达成一致意见(我估计以后也不会,这是低代码的职能所决定的)。 + +但作为低代码的学习者,甚至是架构者,我们需要对低代码平台到底是什么有一个清晰且深入的了解。这也就是我们第一节课的任务。这节课里,我会通过对低代码平台进行归类带你厘清低代码的概念,并带你分析当前低代码的发展现状,让你在脑海里建立起对低代码的直观印象。 + +正如开篇词所说,我们这门课的所有内容都侧重于低代码的架构、策略和技术的实现。所以,对低代码是啥理解得越清楚,相应地,你就越容易理解我所作出的架构和策略选择,以及为啥要采用特定的技术实现选型。反之,在概念理解有误的情况下,后续的内容有可能使你陷入目标与执行相互矛盾的困境,难以自拔。 + +什么是低代码 + +要讲清楚一个模糊的概念,一个有效的手段就是先应该尝试对它,以及相关的概念进行归类,然后比对,从比对中得出关键差异。 + +但要对低代码做分类,并不容易。由于低代码概念和内涵未达成一致,业界对它进行归类的方式也多种多样。这里,我以我理解的低代码的几个重要特征作为维度,对低代码进行归类,同时你也能通过这些分析,了解我们这门课要实现的低代码平台到底是啥样的。 + +按代码量的维度来分类 + +这个维度下,App 的开发模式可以分为三种:纯代码(Pro Code)、低代码(Low Code)、无代码(No Code)。 + +这三者有着巨大的差别,我们需要非常准确地将它们分开。纯代码是这个维度下的一个基准概念,它指的是用传统的手工编码的模式开发应用。而低代码和无代码比较容易搞混。 + +从中英文字面上说,无代码意味着 App 的开发过程没有代码参与。但是这样的理解比较粗浅,为了获取更加权威的理解,我尝试从头部分析机构 Forrester 和 Gartner 发布的报告中,查找与无代码相关的调查报告,但一无所获,不知道是不是这些头部机构并不认可无代码这个概念。 + +低代码模式下的 App 开发过程需要有代码参与,特别是面对一些复杂的业务逻辑的时候,通过表达式或者直接编码的方式来表达,反而更加清晰。而无代码模式开发 App 的全过程,没有任何代码的参与,不仅是从开发者角度看是这样的,从无代码内部的实现方式看,也是这样的。 + +严格来说,把采用无代码模式生成 App 的过程称为开发是不恰当的,因为它只是对已有原子业务能力进行二次组合,形成具有特定功能的新业务而已。因此从这个角度来说,低代码和无代码完全不是一种东西,切不可将这两者混为一谈。 + +但有一个情况非常容易混淆低代码和无代码。当低代码的成熟度到一定高度时,在某些细分场合下也可以实现零代码开发。在这个情况下,从 App 开发过程的表现看,这二者差异微小,此时最容易将两者混淆。当然,我们也不排除一些低代码解决方案提供商为了夸大其低代码的效果,故意将二者混为一谈,把无代码当作一个噱头来宣传。实际上,低代码模式要将一个场景做到零代码,难度非常大,并且有诸多业务前提。 + +在代码量这个维度下,我们专栏所说的低代码是指这 3 个分类中的“低代码(Low Code)”这一类。 + +按适用范围的维度来分类 + +这个维度下,低代码平台可以分为专用型和通用型两种。 + +所谓通用,指的是开发平台不事先假设自身只能应用在特定的场景、业务、行业,而是具有广泛的适用范围。 + +具有这样特征的开发平台往往需要有一个通用的底座。这个底座是纯技术性的,它不依赖于特定的业务功能,而只与业界广泛使用的标准协议、技术标准产生耦合。不过,这个时候,我们只有深入平台架构实现的细节,才能判断平台到底是低代码还是无代码,这就导致平台的使用者难以甄别(注意,我这里的目的不是想告诉你如何甄别,而是为了告诉你这门课所说的低代码平台具有的特征)。 + +但是,通用是有代价的,越通用就往往意味着在特定业务场景下的效率越低,越通用就意味着默认配置里的个性化信息越少,为形成某个具体场景所需的配置量就越大,从这个具体场景的角度看,效率相应也就越低。 + +所以通用型的低代码平台往往伴生着这个特征:有相对完善的有插件(或类似)机制。这一点相对来说比较好识别,相对高通用性的技术底座来说,插件是廉价的,因此通用性低代码平台往往会有数量众多的插件。这些插件可以定制出各式各样具体的业务场景,通过插件的定制化和扩展性来解决效率问题。 + +这个维度下,这门课所说的低代码指的是通用型开发平台,它具有一个通用性非常高的底座,和一个相对完善的插件机制。 + +按输出的 App 的类型来分类 + +其实,在一个具有较高通用适用范围的低代码平台来说,按照输出 App 类型分类几乎是没有意义的。之所以不得不按输出 App 类型分类,是因为开发平台的通用性不足,而在有了足够高的通用适用性之后,支持开发各种类型 App 的问题,就不在于能不能了,而只是时间问题。 + +尽管我们这门课所说的低代码指的是“通用型”这一类,但这并不影响我们看看现在业界其他低代码平台都可以输出哪些类型的 App,大概有流程驱动型、表单驱动型、模型驱动(ORM)型、BI 分析类型这几种,具体你可以看看这张表格(5 星为满分): + + + +这里,我主要给你区分一下表单驱动型和模型驱动型这两个类型,因为它们比较容易混淆。 + +所谓模型驱动型 App,它的模型指的是数据模型,或是数据关系。而这里所说的关系,指的就是符合三范式的关系型数据库的关系,也就是你数据库中各个数据表之间的关系,比如表 1 的 a 字段和表 2 的 a 字段是相同的,但与表 3 中的 a 字段没有关系。在正确配置了各种数据关系之后(这个过程一般称为数据建模),页面上就可以很容易创建各种 CRUD(增删改查)类 App 了。 + +表单类 App 则是仅以数据为中心,创造各种表单来收集或呈现数据。这里的关键点在于,这类 App 并不关注数据之间的关系。所以表单类的 App 非常容易形成数据孤岛,并存在大量冗余数据,以及大量数据不一致性等问题。如果我们将表单类 App 做得比较完善的话,实际上它就会逐渐转型成模型驱动类 App 了。在完成数据建模之后,我们就分不清楚它到底是模型驱动还是表单驱动了,差异只是前端是用表单展示,还是表格展示而已。 + +按使用者的类型来分类 + +如果按照使用者的类型进行分类,我们可以将开发平台的使用者分为 3 类:专业技术人员,业务技术员,相关无专业技能人员。 + +这里所说的业务技术员是一种正在兴起的角色,它是指构建供内部和外部业务使用的技术或分析功能的非 IT 部门员工。他们担任着装备和赋能非 IT 资源以构建数字化能力的战略角色。 + + +根据 Gartner 的研究:41% 的员工可以被称为业务技术人员,不过这一比例在不同行业可能存在很大差异。例如在政府部门等技术密集度较低的行业,这一比例接近 25%,但在能源等 IT 密集型行业,这一比例接近 50%。 + + +多数的无代码开发平台将业务技术员作为主要的用户群,为他们提供对已有业务的二次组合为主的基础开发能力,一般具有专业技能的开发人员是不会使用无代码开发平台的,因为专业技能者要面对的问题域已经大大超出了无代码平台的能力范围。 + +而低代码开发平台一般会将专业技术人员和业务技术员同时作为他们的客户群,并以专业技术人员为主要用户群,业务技术员为次要用户群。 + +随着低代码开发平台的成熟度上升,业务技术员用户群的占比会有所上升。因为成熟度高的低代码平台,不仅有各式各样的可视化工具来降低业务研发的难度和代码量,同时对业务研发生命周期各个环节的覆盖也会越来越完整。从开发到测试,从测试到上线,再到高容错运行时自动化部署 / 恢复、运行时自动化运维等各个环节的可视化、自动化完成,这为无 IT 技能的业务技术员独立开发提供了可能性。同时,越发完善的可视化自动化能力不仅会牢牢抓住已有的专业技能用户,还会吸引更多的专业技能用户的加入。 + +这个维度下,这门课所说的低代码是以专业技术人员为主要用户群的一类平台。不过,在写这篇文稿的时候,我负责的低代码平台正在努力将业务技术员纳入到它的用户群中,但是这项工作才刚起步不久,当前尚没有特别成熟的经验可以分享。但这是一个动态专栏,在未来几年还会保持更新,我会在合适时机,及时把我在拓展更多用户群方面的经验分享给你。 + + + +现在我们来总结一下,我们这门课要实现的低代码平台到底是怎么样的。它是一个以专业技术人员为主要用户群的通用型低代码平台,它会有一个通用性非常高的底座,和一个相对完善的插件机制。 + +我这里还要再解释一下,在后续的内容中,我可能还会提到低代码工具和低代码平台,对于这两个概念,我所指的内涵是一致的,区别就在于规模和成熟度。低代码工具指代规模较小、成熟度较低的低代码实现,而低代码平台则指代规模较大、功能较完善、程度较高的低代码实现。 + +了解了行业内对低代码的几个分类,以及我们这门课的低代码平台的定义后,我们再来简单看看低代码的历史演进和现状,让你对低代码和低代码行业有更进一步的理解。 + +低代码的发展 + +在低代码的发展上,我们可以从基础设施的演进、时间和地域,以及中台的演进这三个方面一探究竟。 + +我们先从基础设施演进看低代码的发展,你可以先看看下面这张图: + + + +长久以来,软件的基础设施都是纯物理设备,当虚拟技术引进后,IaaS(基础设施即服务)时代就开始了。紧随着虚拟技术继续蓬勃发展,没过多久,软件技术便历经了 PaaS(平台即服务)时代、SaaS(软件即服务)时代。关于这几个概念更具体的解释,你可以看下补充材料的内容。 + +SaaS 类产品高度封装的软件服务为行业提供了巨大便利的同时,人们也渐渐发现这种形式的短板:定制性太弱。因此在 SaaS 的基础上,又演进出了一种被称为 aPaaS 的软件服务体系。 + +根据 Gartner 的说法,aPaaS 是应用程序平台即服务的缩写,它是一种云服务,可为应用程序服务提供开发和部署环境。aPaaS 平台提供的功能包括:迭代构建应用程序、即时提供应用软件、按需扩展应用程序,以及集成应用程序与其他服务。 + +很明显,Gartner 把这里的 a 作为 application 理解了。但我个人认为,这里的 a 当做 ability 来理解更为恰当,借用文言文的使动用法,将它翻译为赋能。因为很明显的,相比其他架构,aPaaS 体系多出了开发和部署应用程序的能力,也就是说,aPaaS 赋予了原来的软件服务体系开发和部署的能力。 + +我们再换个角度,从时间和地域来看低代码的发展。下面这张来自艾瑞咨询的图片总结了这个过程,图中信息量比较大,你可以点开仔细阅读。 + + + +数据来艾瑞咨询2021年低代码行业研究报告 + +我们可以从这组比对数据中明显看到,国内的低代码平台要落后于美国一个时代。现在低代码头部解决方案中已经有类似 OutSystem、Microsoft 这样的通用型低代码巨无霸,而国内多数提供商还在探索如何有效地为某个垂直行业、细分领域提供低代码服务。但这对你我这样的低代码人来说,实际上是一个好事,这仍是一片蓝海,大有可为。 + +第三种角度就是从中台演进来看低代码的发展。这里你可能会觉得很奇怪,为啥低代码又和中台扯到一起了呢? + +这是因为,低代码可以将多个“烟囱系统”归整为一个集大成者,更灵活敏捷地创建中台架构。在传统的企业系统中,每个部门有不同的系统需求,于是会各自采购自己的系统。但这些系统彼此孤立,独立运作,导致企业采购的软件系统冗杂。而低代码平台能让绝大部分部门的业务系统都能在一个平台里搭建,彼此联系,打破信息系统孤岛,同时降本增效,提升内部生产力。 + +低代码有助于横向打破传统企业的烟囱系统,将它们串联到一起,这与中台的目标不约而同。此外,低代码对外赋能的职能,也是中台建设目标之一。因此中台的发展过程,有相当一部分线路与低代码是重合的,二者可以起到相互促进,良性共生的关系。所以,如果你所在的企业同时在架构中台和低代码,不妨尝试将它们放到一起来考虑。 + +行业状态速读 + +了解了低代码的发展和演进之后,作为低代码的研究者,我们总得关心下当前低代码的行业现状吧? + +不过,网上这方面的信息实在太多了,多数说的有鼻子有眼,但不知道真假,所以我只看专业调查机构输出的报告。其中我主要关注 Forrester 和 Gartner,以及国内的艾瑞咨询,相关的报告链接我都统一附在了文末的补充材料中。 + +在这么多报告里面,我首先要向你推荐的就是 Gartner 绘制的关于低代码的魔力四象限报告,关键部分就是下面这张图,概括性非常强。 + + + +作为低代码的实现者,一般看这种报告都是以竞品调研为目的的,因此我们一般只研究 Leader 象限里的提供商就可以了。Leaders 这个象限显示的是技术能力较强、对未来的规划很清晰的厂商,其产品被市场广泛认可,对我们有极强的参考价值。 + +其次我想向你推荐的是 Forrester 的 Forrester Wave™ 报告。与分析 Garter 的魔力四象限相似,我们仍以 Leader 这一波里的厂家作为我们的调研对象。与魔力四象限的结果比对,你发现了啥? + + + +两家机构对低代码的 Leaders 给出了几乎一样的结论,对吧?在 Leaders 里,头部机构取得了一致意见。这两份报告为我们低代码平台的竞品调研给出了一个非常明确的指引,所以如果你现在还在头疼不知道如何下手做调研的话,他们就是极佳的研究和参考对象。 + +那么国内的厂商是啥样的状态呢? + +我同样有两份报告可以推荐给你:一个来自 Forrester 的报告《The State Of Low-Code Platforms In China》(下文简称中国报告),另一个来自艾瑞咨询的《艾瑞咨询 -2021 年低代码行业研究报告》(下文简称艾瑞报告),你可以在这一讲的补充材料中找到原文。 + +在《中国报告》中,Forrester 第一次将视角聚焦在中国,它认为,低代码目前在国内主要应用于银行、保险、零售、医疗、政府、制造、电信和建筑行业。Forrester 认为,国内低代码目前主要集中在如下 9 个领域,分别有: + + + +而《艾瑞报告》的信息量就更大了,主要包含了概念界定、应用场景、竞争要素、市场规模、趋势洞察四大块的内容。下图是《艾瑞报告》绘制的低代码厂商图谱,非常概要地整理出了国内外低代码厂商的分类。 + + + +大体上,《艾瑞报告》把低代码厂商分成了通用型和垂直型两种,垂直型和我前文所说的专用型是类似的,均指只能应用在某个业务领域的低代码解决方案,无法运用到其他领域。 + +无论你是要做竞调,还是打算采购,这个图都可以提供不错的指引。 + +大小厂商这么多,也从一个侧面反映了低代码在国内的发展仍处于早期的状态,按照“惯例”,风口褪去后,各个厂商会快速聚集,要么大鱼吃小鱼、要么抱团取暖,形成寡头化的局面,当前还处于“百花齐放”的状态,说明低代码仍处于投资风口,风投时不时来“奶”上一口,所以大家都还能坚持得住。 + +不过,这份《艾瑞报告》是 2021 年 3 月出的,有点老了。目前我和负责竞调的团队还没找到新版,一旦获得一手信息,在商业合规的前提下,我会在这门课的动态更新部分中第一时间分享给你。 + +小结 + +这一讲从多个维度对低代码做了分类,并简要讨论了各个分类的低代码所具有的特征,这些分类方法和特征的讨论,对帮助你理解和总结网上对低代码的各种讨论,会有莫大的帮助: + + +按代码量的维度,App 的开发模式可分为纯代码、低代码、无代码,其中低代码主要特征是 App 开发过程,平台按需开放表达式和编码等编辑入口,无代码则是对已有业务做二次组合; +按适用范围,低代码平台可以分为专用型和通用型,通用型平台有通用低代码底座,可满足大多数业务开发需要,通用型平台一般提供插件方式来提升其在特定场景下的效率和易用性; +目前各低代码平台可以输出的 App 类型汇总大概有:流程驱动型、表单驱动型、模型驱动(ORM)型、BI 分析类型,通用型低代码平台不局限输出某种类型的 App,而专用型低代码平台则一般专注其中的一种; +低代码平台的使用者可以分为有专业技术能力者和业务技术员,有的低代码平台同时支持两种用户同时使用,有的则专注于为其一提供服务。 + + +更具体的你可以看下这张脑图: + + + +现在的状况是大家把低代码当做一只大框,啥业务开发只要能少写两行代码的,都往这个框里扔,都说是低代码。这造成了不同的人带着不同的业务背景来理解低代码,进而得到差异很大的结论,甚至连啥不能算作低代码都说不清楚。我希望通过不同维度分类的方式,来帮助你对低代码的能力、职能、目标等有一定的认知。 + +最后的两个小节,主要是结合了我的经验,对低代码的发展和行业状态做了一些总结。当了一名数据搬运工,把我认为可信度较高、参考价值较高的几份机构调查报告汇总出来,便于你研读和参考。这部分的篇幅不长,但信息量很大,相信对你会有帮助。 + +思考题 + +除了代码量、使用范围、输出应用类型、使用者等维度之外,你认为还可以从其他哪些维度对低代码做分类?可以分为哪些类型?各有啥特点? + +欢迎在留言区分享你的看法。我们下节课见。 + +补充材料 + +关于基础设施演进的几个概念: + +IaaS:Infrastructure as a Service,是提供消费者处理、储存、网络以及各种基础运算资源,以部署与执行操作系统或应用程序等各种软件。 + + PaaS:Platform as a Service,平台即服务,将软件研发的平台做为一种服务提供给消费者。 + +SaaS:Software as a Service 软件即服务,也可称为“按需即用软件”(即“一经要求,即可使用”),它是一种软件交付模式。在这种交付模式中,软件仅需通过网络,不须经过传统的安装步骤即可使用,软件及其相关的数据集中托管于云端服务。用户通常使用精简客户端,一般即经由网页浏览器来访问、访问软件即服务。 + +aPaaS:application Platform as a Service 应用程序平台即服务的缩写,它是一种云服务,可为应用程序服务提供开发和部署环境。 + +关于 Gartner 的魔力四象限:把研究对象分为四类,分别是领导者,有远见者,挑战者,利基企业,通过归类可以快速了解被研究对象所在行业的状态,更多信息可以看这个文章。 + +关于 Forrester Wave 报告:和 Gartner 的魔力四象限相似,也是对被研究对象分成若干类,更多信息可以看这个文章。 + +《The Forrester Wave™: Low-Code Development Platforms For Professional Developers, Q2 2021》原文在这里。 + +《The Forrester Wave™: Low-Code Development Platforms For Professional Developers, Q4 2021》原文在这里。 + +《The State Of Low-Code Platforms In China》原文在这里。 + +《艾瑞咨询 -2021 年低代码行业研究报告:化繁为简》原文在这里。 + +‍ + + + + \ No newline at end of file diff --git a/专栏/说透低代码/02|低代码到底是银弹,还是行业毒瘤?.md b/专栏/说透低代码/02|低代码到底是银弹,还是行业毒瘤?.md new file mode 100644 index 0000000..a43ccec --- /dev/null +++ b/专栏/说透低代码/02|低代码到底是银弹,还是行业毒瘤?.md @@ -0,0 +1,137 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 02|低代码到底是银弹,还是行业毒瘤? + 说到低代码,有人说它是毒瘤,也有人说它是银弹。那到底应该怎么看呢?这就是我们今天要解决的核心问题。 + +先上结论:存在即合理。 + +这里的“存在”包括两个角度:一是银弹论,二是毒瘤论,无论从哪个角度看,既然存在这样的论调,就有它们的合理性。 + +我们暂时不介入这两个言论的细节,而是先把关注点移到低代码本身,先回答这个问题:低代码到底是要革程序员的命?还是成为程序员工具箱里的另一个工具? + +如果你觉得低代码的目的是为了革程序员的命,是要把程序员的手脚给捆住,是要束缚程序员的创造性,是要把复杂的现实世界强制机械化、特例化、流程化,那么大概率你会接受毒瘤论。你甚至会毫无保留地否定低代码,认为 Low Code 除了 Low 以外,不会有 Code。 + +毕竟,变革是有成本的,而且往往成本巨大,无论是一个人还是一个团队,保持惯性,留恋舒适区才是正常的。我们几十年来用的软件从来都是双手敲代码创造出来,抛弃这个已被无数次证实可行的方法,拥抱一个“饱受争议”的新方法,这本身就需要有莫大的勇气,也需要承受风险。 + +虽然会有很多勇于探索、期望尝试新方法的人和团队,也有很多受困于 Pro Code(手敲代码的方式)的各种痛点被迫自我变革的人和团队,但是更多的人和团队是倾向于保持现状的,即使嘴上不说,身体却很诚实。每次回顾会都能复盘出一堆的代码问题,然后一而再、再而三地立 flag 说要改进,但多数会给出各种借口说明问题无解,然后继续这样的循环。有惯性就有排斥,有排斥就有各种负面论调,毒瘤论是其中典型的代表。 + +Pro Code 创造的软件更“香”吗? + +如果能换个角度,把低代码当做程序员工具箱里的另一个工具,我们再次审视银弹论和毒瘤论的冲突时,会发现一些不同点。工具不会革谁的命,只会让大家的日子更好过。工具有好有坏,不好用时就丢弃,好用时就多用用。 + +条条大路通罗马,我们前往罗马除了传统的步行、马车外,还能开汽车、坐飞机,甚至也可以在路况好的时候开汽车,风景好的时候下车漫步抑或纵马驰骋,只要能按时到达罗马,我相信没人会太关注过程,所以在去罗马的旅途只要你开心就好。 + +开发软件写 bug 也是类似的,Pro Code 方式创造的软件不会比其他方式生产出来的软件更香、不会卖更多的米,因为软件的用户完全不关注软件是怎么被创造出来的。 + +当然,软件研发里的“罗马”不仅仅是一个目的地,所以这里带上双引号了。在软件业里,“罗马”除了指代交付的业务功能外,它的内涵还有:交付物可维护性、可扩展性、可测试性、交付过程的成本,甚至是团队的新陈代谢等等许多因素。同样地,我们审视到底是“骑马”好,还是“开车”好的时候,需要关注的不仅仅是心情,而需要关注到所采用的方式是否能在各个方面都有比较好的表现。这是一个极复杂的评判过程,且各个团队有各自的侧重,没有标准答案。 + +即使评判过程如此复杂,但是有一点是明确的(甚至早已是业界共识):Pro Code 不是软件开发的银弹,要不然就不会有低代码等多种新方式被提出来甚至展开实践了。 + +Pro Code 第一个问题是门槛高。虽然我们自贬为“码农”,但是根据 GitHub 的统计数据,去年国内只有大约 755 万多开发人员,一个人可能只会写 hello world 就被算进来,摊到各个细分研发领域后,人数就少得可怜了。我去年面试了几十人,但是最终入职的寥寥无几,招聘难成了我的痛点,我甚至将其写入年终报告。 + +Pro Code 第二个问题是跨界难。虽然都是写代码的,但是 Java 程序员可能很难玩得转 C/C++,前端程序员很难玩得转 Java/Scala 等后端技术,反之亦然。 + +一个典型例子是:全栈这个词是在在 Node.js 火热起来之后才被发明出来的,在这之前,前后端通吃的只能是极少数顶尖骨干的专属。但是,即使现在有了 node.js 实现前后端跨界,我们跨越到其他领域依然困难。总之,即使在具体业务场景下,要端到端交付一个完整业务,对一个人,甚至一个团队来说,都不是一件简单的事情。 + +第三,代码编写只是第一步,之后还有许多问题需要解决。像代码所依赖的第三方库的开源合规治理、第三方和己方的代码安全漏洞检测和治理,还有代码性能、代码测试、运行时运维等,这些工作不是难度大,就是繁琐。最后,为了对抗代码库的熵增,避免代码仓库越来越混乱,越来越难维护,还必须引入代码走查机制,让经验丰富的程序员来把关。 + +Pro Code 存在的问题显然不止这些,但这些已经足够说明问题了。 + +既然 Pro Code 有如此多的问题,而且许多问题是由于代码自身导致的,那么引入一些工具来降低代码量,许多问题也就可以缓解甚至解决了。代码本身并无直接价值,业务才具有直接价值,从来没听说过哪家公司是凭着一个百万千万行代码 repo 作为资产上市的。公司之所以能上市肯定是由于资本市场认可它的业务价值,代码只不过是用于实现业务的一种场常见方式而已,但绝对不是唯一的。 + +总之,不管是白猫黑猫,能抓老鼠的就是好猫,吃喝拉撒越少的猫则是更好的猫。只要能实现相似价值的业务,并且承载该业务的方式的副作用比代码要少,那么这就是一种更好的方式。 + +到这儿,我相信你已经有足够的理性来把低代码作为一种工具来看待,而不认为这是一种程序员自我革命的手段。 + +Low Code 银弹论合理吗? + +既然 Pro Code 不是银弹,那 Low Code 是不是银弹呢?当然也不是银弹。 + +在理解这一点之前,我们先来搞清楚 Pro Code 和 Low Code 的区别。为了说清楚这两者的异同,我要引入“第三者”No Code,我们把这三个看起来很相似的概念放一起比较。 + +Pro Code 和 No Code 实际上都很好理解,一个是纯代码,一个是无代码。假设 Pro Code 的代码量是 100,那 No Code 就是 0,所以 Pro Code 和 No Code 是截然不同的,甚至你可以认为这两者毫无关系。No Code 的最典型形态莫过于 SaaS 类的产品了。 + +那 Low Code 应该摆在哪个位置呢?似乎应该介于 0 到 100 之间?50?80?其实都不是的,关键点不在于多少,而在于有没有! + +Low Code 当然是有代码的,所以它和 Pro Code 是一路货色,从创建业务价值的最根本上说,它们是一样的,都是通过代码来创建业务价值。而 No Code 则不是,它只是对已有业务的二次组合来创建业务价值,No Code 创建业务的全过程都没有源码的参与。 + +那 Pro Code 和 Low Code 的差异在哪呢?我认为本质差异在于源码在这两者创造业务价值的过程中所扮演的角色。Pro Code 是把代码当作关键输入来创建业务的,Low Code 则不是,它的输入是一些结构化的数据。 + +Low Code 工具有能力将结构化数据生成为源码,然后再采用与 Pro Code 相同的方式将源码转为业务能力。很显然的一点是,Low Code 把源码当做中间产物,而 Pro Code 则将源码做为关键输入。相信现在你应该可以分清楚 Pro Code、Low Code、No Code 之间的差异了。 + +那么 Low Code 为啥也不是银弹呢?关键就在于,Low Code 是采用无逻辑的结构化数据来描述业务的,对于相同业务,Low Code 的描述能力要弱于有逻辑的 Pro Code。所以,Pro Code 都做不到的事情,Low Code 当然也做不到。 + +根据前面与 Pro Code 的比对,我们可以看出,Low Code 对业务的描述能力既弱于 Pro Code,也与 Pro Code 没有实质差异,看起来 Low Code 一无是处啊。我认为这可能是低代码毒瘤论的理论基础。别急,继续往下看。 + +Pro Code 开发方式是指令式的,它的开发过程是告诉计算机如何一步步实现某个业务。Low Code 则不然,它的开发过程是不停地在描述和细化这个业务最终的样子。 + +可以说,Low Code 的开发人员的思维方式与 Pro Code 的开发人员完全不一样,他们始终在思考这个业务应该是啥样的,而 Pro Code 的开发人员则是在开发过程中不停地思考如何将业务翻译成一条条指令。其中关键的一点是,对于 Pro Code 开发人员来说,这个业务最终的样子不是天上掉下来的,抑或大风刮来的,而是要在他们着手翻译成指令之前先想清楚的。 + +所以结论就是,Low Code 开发人员要比 Pro Code 开发人员少做一个事情:无需将业务翻译成指令,从而他们得以用更高的效率实现相同的业务,在单位时间内创造更多业务价值。 + +低代码模式除了效率外,还有另一个卖点,那就是低技术门槛。它可以赋能更多人,使其可以快速参与业务开发。没错,就是实现外卷! + +根据我们前面说的,Low Code 依然有代码,但 70%~90% 甚至更多比例的代码是自动生成的。这里要特别注意的是,其中 100% 的框架性的代码都是自动生成的,这部分代码不但与业务无直接关系,即没有直接业务价值,还是最难开发的,一出问题都是大问题。而剩余的小部分需要开发者填写的代码,则基本上都是表达式,或者部分复杂业务逻辑。 + +表达式也好,强业务逻辑代码也好,都是在直接实现业务功能,所以即使在低代码平台上写代码,那也是在直接实现业务逻辑。那些只有业务能力但无开发能力的人、需要写 UI 的后端开发,或者是需要写业务的前端开发等人群,都可以在低代码平台上实现跨界开发,实现端到端的业务交付能力。赋予原本没有技术能力的人快速参与的能力,是低代码的另一个重要能力。 + +基于以上提效和赋能两点,我们有足够的理由来认为低代码银弹论是合理的,银弹论并非一群“不务正业”、“整天想着再造一个轮子”的人的自嗨。 + +Low Code 离银弹还有多远? + +低代码毒瘤论者只看到表面,或者思考深度不足,没能从实质上看到 Pro Code 和 Low Code 的差异。即使如此,我也认为毒瘤论是有合理性的。毕竟很多实现 Low Code 的人自己也没想明白就匆匆动手,为了达到少代码(而非低代码)的目的而做出各种各样无理约束,你不能这个、不能那个,最终不但没能发挥出低代码的优势,反而降低了效率,被他人诟病。 + +即使抛开这类情形不说,Low Code 本身的成熟度也需要改进,一个重要改进点是:如何有效解决强逻辑场合下的可视化配置方法,这是 Low Code 采用无逻辑的结构化数据描述业务常见的重要短板。这个问题至今也在困扰着我,我认为我们目前给出的解决方案并非最优解,仍然需要孜孜不倦地探索出更合适的手段。 + +低代码要真正成为银弹,除了解决强逻辑的可视化配置外,要做的事情还有很多。有一部分我在前文已经有过描述了,这里再给你简单总结和拓展一下,一方面是为了用作毒瘤论非合理性的额外论据,另一方面也是为了给这个专栏后续的内容埋点伏笔。 + +首先,快速部署能力是要有的,最基本的需要一键导出所有运行时需要的文件,最好能做到一键部署和运行时,这样的能力将给业务的小伙伴提供巨大的便利,大幅减少他们的试错时间。 + +其次,也是最重要的,低代码平台还需要关注生成的 App 的可测试性。如果生成的 App 是一个黑盒,出错时无从下手,日志打印惜字如金,要是这时候平台再对业务团队的困境置之不理甚至推诿,那无论是谁都会厌恶这样的开发方式的。 + +可测试性是一个非常综合的能力,从最基本的部署运行开始,到丰富准确的日志,再到直接给予业务团队调试支持,甚至到直接提供自动化测试的能力,这里头涉及的不仅是技术,还有团队间如何协作的问题。 + +还有一个容易被无视的能力是,低代码平台需要支持多人协作开发,当业务越来越复杂,多人协作是刚需。这个问题往往在平台建设初期就容易被无视,在后期随着应用的复杂度上升后才被提出,这时候再去实现,成本会非常大。此时平台有可能会简单粗暴地采用互斥的方式来掩耳盗铃。 + +要知道,业务团队往往面临巨大的交付工期压力,如果小李无法在小王编辑时上手,一急之下他们可能将数据复制一份来同时编辑,结果发现无法手工合并结构化数据,这样的情形放谁身上都会抓狂。所以支持多人协作开发是低代码平台非常必要的一个能力,而这背后其实还隐藏着编辑历史管理、分支管理等潜在需求,这些都对业务团队多人协作效率有很大的影响。 + +再者,兜底能力也是很重要的。低代码平台不可能面面俱到,它总有能力边界,但这个能力边界不能束缚业务团队的探索。业务需要紧随市场甚至引领市场,而市场是千变万化的,任何公司都无法决定,所以要把“业务提出低代码平台能力之外的需求”当做一种常态。此时,低代码平台需要有一种策略帮助应用快速实现需求,哪怕直接上手编码乃至 Hack。这样的策略就是兜底策略。 + +此外还有一些增值功能,包括 UX 设计规范自动对齐、提供 UX 设计稿转代码(D2C)能力、App 的可维护性、App 的埋点 & 数据采集、App 的开源合规治理、App 的安全漏洞治理、App 的性能等等。简单来说,这些都是低代码平台的亮点能力,并且是拉开与 Pro Code 差距的重要能力。 + +讲到这里我们可以看到,低代码平台只关注开发能力是远远不够的。而毒瘤论的另一个重要基础就是来自于许多低代码平台只注重开发能力,而无视业务交付过程的其他能力,特别是对 App 可测试性的漠视。 + +作为低代码的坚定支持者和践行者,我当然不同意低代码是毒瘤这个论断,但是我不会轻易去喷这些言论,而是努力去理解和挖掘毒瘤论的言论基础,从中分析出那些容易被我们无视或者轻视的需求。只要我们常常能细心、谦虚地观察业务开发过程中的方方面面,并及时发现业务团队的痛点和需求并及时解决,以一种服务者的态度来为业务提供服务,我相信,毒瘤论会渐渐消散的。 + +总结 + +今天这一讲到这里就结束了。其实很少有一种技术像低代码这样同时被两种极端言论评价,今天我们从一个理性的角度审视了这两种言论,并尝试去理解它们的思路和痛点。 + +我们从 Pro Code、Low Code、No Code 的对比中了解到 Pro Code 与 Low Code 的关键差异不在于代码量的多少,而在于: + + +代码在这两者创造业务价值的过程中所扮演的角色,对 Pro Code 来说,代码是关键输入,而对 Low Code 来说,代码仅仅是中间产物、是副产品; + +使用 Low Code 开发虽然也需要少量编码,但是基本上都是在填写表达式,直接实现业务价值,与业务价值无关的代码则几乎全都被自动生成; + + +使用 Low Code 开发业务的人几乎时时刻刻都在描述和细化业务最终的样子,使用 Pro Code 的开发人员不仅需要思考业务最终的样子,还要将其翻译成一条条计算机指令。 + +这些差异正是 Low Code 的比较优势,这也是为啥 Low Code 可以帮助业务提效和赋能的原因:无需长篇累牍地编码可以大幅降低使用的门槛,实际上就是对无编码技能者进行赋能;几乎所有框架性、非功能代码全部自动生成,以及无需将业务翻译成一条条计算机指令,可以大幅压缩开发周期,实际上就是在提效。 + +但即使如此,当下的 Low Code 也不是银弹,因为我们在分析毒瘤论的过程中意识到 Low Code 除了注重开发能力之外,还应该具有诸多其他能力以覆盖业务研发全生命周期,包括一键导出和部署、较高的可测试性甚至自动化测试能力、多人协作、兜底能力等等,若低代码平台不具备这些能力,则很难发挥低代码技术的优势。 + +当然,我们也发现了 Low Code 带来了许多增值功能,包括自动对齐 UX 设计规范、UX 设计稿转代码(D2C)能力、App 的埋点 & 数据采集、开源合规治理、安全漏洞治理等等,这些功能是拉开 Pro Code 差距的重要抓手。 + +思考题 + +低代码平台是否只需把开发能力发挥到极致就可以了?除了开发能力之外,低代码平台还需要注重哪些能力的建设?你认为其中最重要的是哪些?欢迎在评论区留言。 + +我们下节课见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/03|低代码的天花板:一个完备的低代码平台应该具备哪些条件?.md b/专栏/说透低代码/03|低代码的天花板:一个完备的低代码平台应该具备哪些条件?.md new file mode 100644 index 0000000..86391f6 --- /dev/null +++ b/专栏/说透低代码/03|低代码的天花板:一个完备的低代码平台应该具备哪些条件?.md @@ -0,0 +1,122 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 03|低代码的天花板:一个完备的低代码平台应该具备哪些条件? + 这一讲我们来探一探低代码的天花板到底在哪儿,也就是从多角度看看,一个完备的低代码平台到底是啥样的。 + +如果你已经在构建低代码的路上,且自认为干得不错,我想给你描绘一些新的目标,帮助你做得更好,或者至少帮助你了解接下来还能有哪些值得去做的事情。 + +如果你还未开始构建工作,或者刚刚上路,我想给你一个更加具体的目标,这样你可以更加精确地估计出,如果达成这样的目标,你需要投入多少资源(人力 / 时间)。当然,不是非要触及天花板的低代码平台 / 工具,才有生产力,即使你知悉了天花板在哪,也不一定非要去摸一摸,量力而行,适合自己的才是最好的。 + +这节课,我会从适用领域、适用人员、与基础设施和谐相处、全生命周期这几个角度来说明一个完备的低代码平台的模样。为了避免啰嗦,我们这一讲讨论的“低代码平台”都特指处于天花板中的低代码平台。 + +适用领域 + +低代码平台需要同时支持以数据为中心和以流程为中心的开发模式。多数企业的软件基本上都是围绕着数据或流程打造的,因此对低代码平台的适用性提出了极高的要求,要求它能够同时满足多数企业的业务需求的开发。 + +不同企业的业务复杂程度,显然是不一样的。有的企业的业务复杂度非常高,有的企业则相反,比较简单,但简单的业务往往伴随着数量庞大的特征。 + +那怎么支撑不同复杂度的业务呢?这一点上,我们要求低代码平台对于简单的业务,需要提供简洁的开发实现方式,并且需要有非常高效的开发效率,从而可以更好地应对简单业务在数量方面的需求。对于复杂度高的业务,低代码平台需要能提供良好的分步实施策略、自然的衔接方式,同时也要充分考虑并提供兜底策略。 + +在我看来,兜底策略是应对复杂业务的一个非常有用的方法。当业务的复杂度超过低代码平台的能力边界,或者业务复杂度过高,使得可视化模式带来的效率 / 易用性收益低于 Pro Code 模式的临界值的时候,低代码平台应该能提供一种方法,允许业务开发人员回退到传统 Pro Code 模式继续开发。并且,这个回退过程应该非常自然 / 内聚、同时尽可能地不影响其他功能的开发。 + +兜底策略不是一种通用的架构方法,而是一种思路一种策略,它应该在低代码平台各个功能点的架构和开发过程中因地制宜地被实现出来。如果更加极端地运用兜底策略,你会发现我们甚至可以要求低代码平台完全回退到 Pro Code 模式。 + +实现兜底策略看似难度非常高,但事实并非如此。得益于低代码的关键特征,也就是低代码是有代码的,低代码平台能很好地处理好 Pro Code 模式和可视化开发模式之间的关系。所以即使完全回退到 Pro Code 模式,对平台来说,不但问题不大,反而平台要做的事情会更少。因为在这个模式下,代码是人工产生的,而非低代码平台自动生成的! + +当然了,能做到这点的前提是,低代码平台要有一个足够强大的编译器(代码生成器)。我会在第 6 讲、第 7 讲中,花整整两讲的篇幅,和你详细说明如何架构、实现这样的编译器,并帮助你理清编译器和编辑器之间的关系。 + +虽然一个良好的编译器能让低代码平台具有实现可视化模式到 Pro Code 模式切换回退的基础,但只有这点还不够。 + +前面我说了,回退的过程应该自然且内聚,这就给 UX 团队的交互设计师和编辑器的开发人员提出了考验。你的交互设计师需要能设计出自然流畅的回退过程,并且编辑器的开发人员要做得出来,并且做出来的性能还要足够好。 + +最后,在 Pro Code 模式下的低代码平台是一个事实上的 Web Online IDE。没错,也就是说,编码过程智能提示、智能补齐、重构、出错信息及定位等 Native IDE 的功能一个都不能少。 + +这方面概括地说,就是低代码平台必须具有充分的通用性。 + +适用人员 + +说完业务能力的适用性,我们再从低代码平台的用户的角度来看看天花板在哪儿。低代码平台要能支持各种技能水平的人同时使用,包括无软件开发技能的业务技术员,也包括掌握某种软件研发专业技能的人,以及水平介于两者之间的各种层次技能人员。 + +当然,不同层级的人的需求是不同的: + + +业务技术员更需要傻瓜化的操作、更加简洁的操作流程设计,以及“说人话”,意思就是少用编程术语,不得不用时就地给出言简意赅(而非长篇累牍)的说明; +专业软件研发人员往往要求对他们熟悉领域的底层代码,有更强的掌控能力甚至直接编码,对其他领域则需要有良好的可视化辅助。比如前端人员往往要求有可视化方式可以帮助他们获取业务数据,而后端人员则要求有可视化方式帮助他们画出 UI; +水平不济的研发人员,特别是职场新手,往往更青睐可视化方式,他们更希望研发全流程都有可视化能力的辅助。 + + +另外,对软件研发人员这类人群,我们还有一个不得不考虑因素:如何消除他们的职业危机感。他们往往会非常担心在低代码的绑架下失去职业竞争优势、失去与公司的议价能力。特别是,技能较强的人不仅对这方面的担忧会更加强烈,还会认为低代码让他们失去了职业经验带来的比较优势,这种情况下,他们会更加抵触低代码技术。这个话题超出了这一讲的内容,但却极现实,影响团队稳定,有机会我们再专门聊聊。 + +除了个性化要求之外,低代码的各类用户也有共同诉求: + + +平缓的学习曲线:完善的低代码平台往往可以覆盖业务研发全生命周期的各个环节,所以会包含数量众多的功能、流程。这些对使用人员来说都是知识,在开发流程的设计时,要管控好各种功能对知识的要求和传递,采用渐进式的方式设计开发流程; +尽可能地自动化:没人能抵御自动化的魅力,这也是低代码平台的魔力所在。但在实现各种自动化封装的同时,我们要留出一手,在自动化流程不满足需求时可以切换为手动模式,这就是兜底策略的一种应用。另外,出错时还需给出准确的说明,不能惜字如金,比如出错时弹一个对话框只告知出错的结果,但不包含出错的原因,这样的方式是非常糟糕的; +可以抄作业:也就是说需要提供数量众多的典型应用范例。低代码平台构建初期这个能力很容易被忽视,在平台逐渐成熟之后,我们必须恶补这方面的功课; +提供视频形式的教材:多数人不乐意阅读文字,一份文档有数百字时就嫌烦。视频教材最好采用无音频的外挂字幕方式制作,一方面剪辑起来更加方便,一方面外挂字幕方便搜索和定位; +出问题或者有疑问时能在第一时间得到帮助:建立客服群平台,开发者可以多与使用者互动,热情耐心地帮助他们解决问题,建立首问责任制。 + + +与基础设施和谐相处 + +这一点主要讨论的是低代码平台与基础设施之间的关系。我认为主要包含两个方面:一是对运行环境的要求;二是处理好与已有系统,特别是已有数据之间的关系。 + +对运行时的要求相对简单。有的低代码厂商可以直接提供基于公有云的运行时,这样就更加方便,基本可以做到开箱即用,但要照顾低代码客户在数据和信息安全方面的担忧。除此之外,我们还可以提供虚拟镜像,并支持在多数基于 PaaS 的平台上安装和运行。这个方式往往用于私有云的部署,这是应对客户信息安全担忧的有效方式。 + +我认为,低代码平台需要同时对这两种部署方式提供支持,特别是需要支持私有化部署方式。这样,无论是内部使用还是对外售卖,都可以做到更加灵活。当然,那些专注于内部使用且用户群明确的低代码平台来说,就不需要过多考虑这个问题了。 + +低代码平台与已有系统之间的关系,大致分为三种:一是被其他系统集成;二是将其他系统集成到平台中来;三是保持相互独立。 + +虽然,同时支持多种与存量系统融合部署,可以提升低代码平台的适用性,但是我认为没有必要支持所有方式,我们根据自身定位挑选以某种融合方式为主就可以了。 + +更多的情况下,优先发展被集成的能力是一个更好的选择。创造价值的肯定是业务能力,而低代码平台作为一种开发工具,对多数企业来说,是不具备直接创造价值的能力的,因此低代码平台也就很难冲到前面去了。即使对以售卖低代码能力为生的企业来说,低代码的开发能力是直接创造价值的业务,但一般买家购买后,也是将它作为自身的某个子系统,集成进买家自己的其他业务中。 + +既然低代码平台一般是被集成、作为子系统使用的,那么低代码平台与存量数据之间的关系也就比较明确了。低代码平台需要提供多种能力来获取外部数据,而不是将自身封闭成一个数据孤岛,要求外部将数据导入到平台中去。而外部数据的结构(关系)、存储介质、数据量、获取渠道等都是无法事先确定的。在这样的前提下,低代码平台要用好这些数据,可想而知难度是非常大的、甚至不具有可行性。 + +但如果是为确切场景下的数据提供定制化的融合方式,难度会低许多,但是代价也很明显,也就是几乎每种场景都需要提供一份定制。因此我们需要通过适当的架构设计,将定制部分与核心部分解耦,最好是能允许业务团队来自行定制。这正是插件系统需要解决的问题。我在第 15 讲里介绍这一点。 + +如何做到对单点登录的支持、如何考虑对接权限系统,也是被集成时需要着重考虑的能力。同时还需要在权限系统中区分开发态和运行态,开发态下往往要给予开发人员的账号更大的权限,而运行态下则需要做严格管控,避免数据滥用和渗透。开发态与运行态进行物理隔离,是杜绝开发人员利用开发账号越权访问数据的有效手段,这一点,我们会在接下来的全生命周期里简单阐述。 + +全生命周期 + +低代码平台不能只注重开发能力,开发能力当然是低代码平台的关键能力,但绝不是唯一的能力。低代码平台的能力必须能够覆盖从需求端到应用下线消亡的全过程。 + +这中间至少可以覆盖这些环节: + + +D2C(Design to Code):业界已经有非常成熟、成功的 D2C 实践案例,对于低代码平台来说,从设计稿中识别出关键信息,再实现与低代码平台的对接与编辑,要比纯 D2C 解决方案容易得多; +UX 设计即开发:有了 D2C 能力后,UX 设计师可以直接提供模板和业务组件。在这个意义下,UX 设计师起到了类似研发人员的作用; +App 开发能力:这点自不必说,这是低代码的重要且基本作用; +App 的自动化测试:包含两点,一是要能帮助 App 自动生成测试代码,二是提供一键式测试环境构建、测试执行、测试报告,乃至自动标注出错位置等; +应用的版本管理:主要体现为要为应用构建相互独立的开发时环境、系统测试时环境、生产环境等,并能实现应用版本的测试、灰度发布、正式上线、紧急回退等能力; +应用生产环境监控:这里包括两点,一是应用运行时基础信息(CPU/ 内存 / 磁盘空间)监控和告警,二是应用埋点数据的植入、采集、分析等。 + + +总之,虽然要做好其中的任何一条就已经很不容易了,但是这所有的功能都应该是低代码平台的“菜”,都可以实现低码化和自动化。第 15 讲我会挑重点对此进行部分讲解,比如 D2C 的实现、零代码生成自动化测试用例等,其他的内容有机会我们再聊聊如何实现。 + +总结 + +今天我从多个维度描绘了一个天花板级别的低代码平台必须具有的能力。 + +从适用领域角度看,低代码平台必须能同时支撑以数据为中心和以流程为中心的 App 的开发,这实际上等于需要覆盖大多数企业的开发业务能力。 + +同时,它还需要能支持不同复杂度业务的开发、兼顾效率。简单的业务开发要简单高效,而对复杂的业务则需要有良好的兜底策略,确保业务需求突破低代码平台能力边界时,可以有相应的应对措施,比如回退到传统 Pro Code 模式继续开发。这一点又给低代码平台提出了需要支持 Low Code 和 Pro Code 混合开发的方式的要求,而且混合的过程需要具有良好的自然过渡和内聚性。 + +从适用人员角度看,低代码平台需要能同时支持多种能力背景的人同时使用,比如业务专家群体多数无软件技术技能,需要为他们提供更多傻瓜化、可视化的操作。而且,既要照顾到有软件研发技能的群体在他们专业领域里的诉求,为他们提供更接近底层代码,甚至直接提供编码的开发方式,也要照顾这类群体软件技术能力之外的开发诉求,为他们提供可视化的方式,辅助他们完成业务的开发。 + +而且,低代码平台要有平缓的学习曲线、尽可能地自动化、提供大量的模板和素材,在帮助他们自学的同时,又能帮助他们解决实际问题。同时,低代码平台也不能只注重开发能力而忽视业务研发生命周期里的其他能力,应该积横向拓展,在需求端和交付端提供能力。 + +由此可见,要将低代码平台做到天花板上,无论哪个维度上显然难度都非常大,需要投入巨大的资源。但不是非要在触及天花板后,一个低代码平台 / 工具才能产生生产力。我们今天这讲的内容主要是给你描绘出一个完备的低代码平台可以做成啥样,希望能在某些方面对你有所启发。 + +思考题 + +在你的场景中,你更看重低代码平台在哪个领域中的表现呢?你希望主要有哪些人来使用你的低代码平台?除了开发能力之外,你还在业务开发过程中的哪些环节上对低代码有需求? + +我们下节课见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/04|演进策略:先发展通用能力还是先满足业务需求?.md b/专栏/说透低代码/04|演进策略:先发展通用能力还是先满足业务需求?.md new file mode 100644 index 0000000..4534b42 --- /dev/null +++ b/专栏/说透低代码/04|演进策略:先发展通用能力还是先满足业务需求?.md @@ -0,0 +1,188 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 04|演进策略:先发展通用能力还是先满足业务需求? + 今天我们来说说低代码平台在不同发展阶段的不同演进策略。我们可以将低代码平台的发展过程划分为 3 个主要阶段:MVP 阶段、成熟期、超越期。 + +MVP 阶段一般在 3 到 6 个月,时间比较短,主要目的是快速试错、快速闭环。这个目的之外的工作一般都“先放一放”,因此这个时候,备忘录里往往会留下许多待改进条目,但这些欠债在成熟期都要一一偿还。性能问题实际也是一种欠债,单独拎出来说是因为性能问题往往比较麻烦。它是慢性毒药,当毒性呈现出症状时,哪怕是轻微的症状,基本都已经很难搞了。而且,性能与功能是相生相克的,功能追加到一定程度就必然要停下来专门处理性能问题,两者呈现出一种螺旋式上升关系。 + +成熟期是实现低代码平台过程中的一个比较艰难的阶段。随着 MVP 阶段的需求免疫光环褪去、天使用户开始介入,实际业务需求紧跟着也就来了。此时平台团队往往面临这些直接压力: + + +偿还 MVP 阶段的欠债; + +彻底解决性能问题。 + + +功能欠债也好,性能问题也罢,始终只是技术问题。熬熬夜,牺牲一点发际线总是可以解决的。更麻烦的是,随着低代码平台的实际应用的推进,在 MVP 阶段中被有意无意忽视的业务场景逐渐显露出来,变得越来越具体。 + +这个状况会把低代码平台的发展道路的抉择推到风口浪尖上:先发展通用能力还是先满足业务需求? + +其实,先发展通用能力也好,先满足业务需求也罢,最终目的是一致的,都是着眼于解决实际业务问题。这里的关键是优先发展哪种能力,是着眼于长远,还是着眼于眼下。 + +而到了超越期,我们的目标就非常明确了:解决具体业务问题,将业务问题梳理为各种场景,然后针对场景做针对性优化,使得在已覆盖的场景里的开发能力、效率等各方面全面超越 Pro Code 模式。 + +即使是在未覆盖的场景或者特殊场合,低代码平台也可以通过部分回退到 Pro Code 模式的手段、采用高低代码混合的方式,实现对业务开发需求的支持。并保持效率、交付能力相对 Pro Code 模式的优势,从而达到低代码平台将在各方面显现出对传统 Pro Code 模式的全面超越效果,这显然是振奋人心的一个阶段。 + +通过前面对各个阶段简单的分析可以看到,MVP 阶段和超越期的线路和目标非常明确,我们不需要过多讨论。但成熟期却有两条相对清晰且都很有说服力的发展路径:先发展通用能力还是先满足业务需求。这正是我们这一讲要解决的问题。 + +选择什么样的发展路径? + +这个问题的答案实际上没有太多的悬念:优先发展通用能力。 + +“通用能力”指的是一种不与某种具体业务绑定的开发能力,是一种能够适用于各种形式业务的开发能力。在这里,我无法给通用能力画一个边界,或者给出一个精确的定义,因为它有很大的弹性,它的边界只存在于每位平台设计者对业务的理解中,或者说在他们的心中。可以说,你对通用能力的边界理解和想象,大概就是你的低代码平台未来的能力上限。 + +作为一个开发平台,哪怕只是一个开发工具,如果能力过于聚焦,带来的后果会是很难适应不断变化的业务需求,也很难搞定新形态的业务,从而失去拓展新应用领域的机会。最终的结果就是要么是推倒重来,要么就是被其他通用性更强的平台所替代。 + +毕竟,人人都希望有一个具有更多想象空间的平台,而不是只顾眼下的一亩三分地,一眼就能看到它的边界。想象空间越大表示它能做的事情越多,潜力也就越大,相应地,它能给你的回报也会越大。当你自己对这个平台的能力都没有想象空间的时候,就不可能让别人(上级或投资者)对它有多大的期待,这样就几乎不可能获得更多的资源。 + +所以,如果非要给“通用能力”画个边界,你对它的想象空间就是这个边界。而你对它的想象空间也决定了别人(上级或投资者)对它的想象空间,从而决定了它能给你带来多少资源和回报。 + +那么,不走优先发展通用能力的道路,还有其他发展道路可以走吗? + +有的。我们可以选择优先聚焦于业务场景和业务痛点,优先发展能快速解决当前业务问题的开发能力、能切实解决业务痛点的能力。仔细想一想,这样的发展线路不但没有问题,而且会显得很务实。从实际问题出发,并且在短期内可以获得成效。 + +但是过于聚焦的实现往往意味着扩展性不足。一个显而易见的后果就是难以适应后期业务场景的变化,那当这样的状况出现时,要怎么办呢? + +有人说:简单,再针对新的业务、新的痛点重新来一次就好了。但如此反复数次后,一个新情况马上就出现了: + + + +这张图就表示了这个状况。你可以看到,每次都从头开始,在短时间解决具体问题的能力确实能迅速爬升,见效快。但由于没有通用性,在最初设定的业务问题都解决了后,自然就到达顶峰,之后基本就没下文了,每个工具都是这样。 + +如果你是一名管理者,你最容易想到的,可能是如何整合已有的这些工具。一般在评估之后,你会发现这很难! + +因为这些工具是由不同的人在不同时间,采用不同方法(Shell、Native App、Web)聚焦在不同问题上开发出来的,简单地说,他们没有共同的基因。更要命的是,这种解决问题的思路形成习惯和传统之后,你会发现无论时间多长,能力的上限基本就在那,没有任何想象空间。显然谁都不希望看到这样的结果。 + +现在,我们可以来尝试回答一下这个小节标题中的问题了,你应该选择什么样的发展路径?很显然就是走优先选择发展通用能力的路。越通用,想象空间越大。 + +坚持优先发展通用能力不动摇 + +但凡事总有两面性。通用是有代价的,它的代价就是不能聚焦于具体业务,从而导致在具体业务上没有很好的表现。甚至,还有可能在开发特定业务的情况下,和传统 Pro Code 方式相比,不仅没有任何改进,甚至还倒退! + +这与低代码模式四处标榜的高效、简易等标签相悖,很多低代码的反对者将这些案例收集起来作为毒瘤论的例证。 + +这里,我放了一张图,展现了优先发展通用能力的工具 / 平台的发展过程: + + + +你可以看到,MVP 阶段与其他工具相似,只是周期很短,短时间内也有能力的迅速爬升。成熟期相对漫长,而且具体业务开发能力缓慢爬升,基本处于啥都差一点的状态。这个时期注重的是发展通用能力,为未来的各种场景做架构设计和打基础、解决性能问题。但这个阶段往往很容易夭折,不仅因为这个阶段难度巨大,而且漫长的持续投入与看到的收益不成比例。 + +如果你刚好是负责人或直接领导,在这个阶段里应该要不停地讲故事,把你的想象空间尽可能形象地描述给上级或投资者,建立他们的信心。能由此获得更多资源是最好的,但至少不要被压缩现有资源。同时也帮他们建立想象空间,让他们去影响他们的上级。 + +成熟期的一个显著特征是待实现的多数业务都会触及低代码平台的能力边界。所以几乎每面对一个新需求,你都要接受这样的灵魂拷问:要如何在确保平台的通用能力得到扩展的前提下顺便满足当前需求?即使通用能力无法得到拓展,至少也要避免为了实现某个业务团队需要的开发能力而将该具体业务耦合到平台中。 + +我的建议是,请按顺序评估如下的因素: + + +评估手里有多少可用资源,这是根本; +评估当前用户的友好程度; +评估老板或者你自己有多强势,面临业务压力时,他能扛住多久。 + + +资源是一切的根本。资源充裕时,你基本上可以忽略其他任何因素。但资源总是有限的,需要着重考虑的是当各方扛不住业务压力时,有没有备用资源可以投入,以缓解矛盾、快速提供业务需要的开发能力。那些开发能力和效率都值得充分信赖的、又肯熬夜加班的技术骨干,就是一种王炸级别的备用资源。此外,你还要仔细避开那些与投入资源数量无关的困境,比如前面我们提到的性能问题,或者所用技术大大超出已有储备。 + +平台的用户(即业务开发人员)的友好程度也是一个重要因素。通用的能力往往无法在具体业务开发场景中提供良好的易用性或效率,而难用和低效是需要由他们来直接承受的。因此,如果用户群体的友好度很低,他们三天两头地发邮件,还在各个场合下不余遗力地抱怨,我相信你或者你老板很快就会妥协。在面对低友好度用户的时候,我们就要适当地聚焦在他们的痛点上,而不能一味追求通用能力,反之则可以更多地聚焦在通用能力的研发上。 + +你或者你老板有多强势是另一个因素。如果出问题时、业务团队不停抱怨时,总有人为你站台的话,你就可以更加专注于发展通用能力上。但这时我们要学会讲故事(画饼),不失一切时机地将通用能力所构筑的想象空间描绘出来。 + +坚持优先发展通用能力的道路的收获季,是在场景化阶段,也就是超越期。在一个通用的底座之上支持某种具体场景是很容易的,因为此时你考虑的已经不是能不能做到的问题(这个问题在成熟期已经基本解决了),而是要不要做、做成啥样的问题。即使做错了,付出的成本也只是把该场景推到重来而已,并不需要将整个平台推到重来。 + +这就好比我们国家坚持先发展重工业,后发展轻工业。在重工业阶段投入巨大、艰苦卓绝,但当各个工业门类基本建设齐全之后,再发展轻工业就相对容易得多了。结果有目共睹,近 20 年我国的 GDP 嗖嗖地往上涨。 + +一旦确定如何实现某个业务场景之后,实现的方法基本上都是一样的:具体化和自动化。 + +具体化和自动化实际上说的是同一个事情,场景越具体,配置内容也就越具体,越具体的配置内容就越容易实现自动化。这里所谓的自动化,指的就是在具体的业务场景下,基于通用能力自动化生成各种各样的配置。场景越具体,自动生成代码的比例就越高。而自动化完成几乎所有业务的开发,不就是低代码的魅力所在吗? + +到了场景化阶段,低代码的魔力才开始显现,才能真正拉开与 Pro Code 模式的差距。这两种开发模式的能力终究会在某个点上出现交叉: + + + +并且,一旦 Low Code 模式的能力超过 Pro Code 模式之后,这个趋势终将不再回头。因为此时的 Low Code 模式将依托于其强大的通用能力,将各个场景逐个纳入到其能力范围内。每个被支持的场景生成的代码都凝聚了技术专家、业务专家的智慧,因此,一旦低代码平台支持了某个场景,凝聚其中的专家经验将持续为高 / 低各种技能水平的使用者赋能和提效。这是 Pro Code 无法做到的。 + +而 Pro Code 模式,它固有的内秉性知识传递的弊端、以及语言自身能力的限制,导致它的能力上升将极其缓慢。这点你从身边的编码专家身上就可以看到,他们写了十几年代码,却依然摸不到这个领域的天花板,更别提有所突破了。Pro Code 模式的业务交付能力,之所以能随着时间推进持续缓慢爬升,是因为团队人员(不考虑流失)的编码经验在缓慢地提升,但显然靠个人经验的提升来提升业务交付能力的边际收益,必然是越来越低的。 + +如何保持优先发展通用能力呢? + +前面说了,“通用能力”指的是一种不与某种具体业务绑定的开发能力,是一种能够适用于各种形式业务的开发能力。当低代码平台仍处于成熟期时,待实现的多数业务开发能力的需求,都会触及低代码平台的能力边界,我们需要创造出新的能力来拓展平台的能力边界。而且,用于拓展能力边界的功能都要是通用的,而不能只适用于当前的具体业务。 + +这样讲比较抽象,接下来我讲一个我自己的实际案例,帮你加深理解。 + +第一个例子是关于大场景的需求。在 MVP 阶段甚至更早的时候,我们的业务团队提出了两个比较典型的应用场景:一是低代码平台需要能支持表单的可视化开发;二是低代码平台需要能支持 Dashboard 的可视化开发。 + +这两个需求本身其实就已经具有很高的通用性了,直接照做也无可厚非。但深挖下去,你会发现这样的问题也需要一并解决: + + +极少有表单单独成一个 App 的(否则就变成调查问卷了),表单与表单之间如何串联? +Dashboard 里也有可能使用表单,有的 Dashboard 会有查询条件,查询条件部分就是一个表单(这在我所在的产品里很常见)。 + + +为了能同时解决这几个显式和隐式问题,低代码平台最起码需要提供一个能同时支持表单、Dashboard,以及它们相互引用的功能。实际上,能满足这样功能的编辑器的通用程度就已经很高了,再考虑到其他可能出现的应用场景,我们很容易想到需要打造一个不以任何具体场景为假设前提的场景编辑功能。后来我将这个场景称为通用场景。 + +第二个例子是关于图形的需求。应用团队提出需要提供柱状图、折线图、饼图、仪表盘等常见图形的可视化编辑功能。其他常见的图形还有散点图、漏斗图等不下 10 种图形。 + + +这里需要说明一下背景,我是 echarts 来绘制图形的。对它有一点了解的人都都知道,echarts 不仅可以绘制这 10 来种常见图形,也能绘制其他许许多多不常见的图形。 + + +每一种图的配置方式差距非常大,挨个去定制的话基本等于做 10 个独立的图形,但需求又非常多,我们不可能一个个去定制。于是我决定先提供一个支持任意图形的开发能力,而不提供可视化方式。当然,这事是先要和应用团队沟通的。这样做的话,短时间内可以满足业务团队对图形的任何需求,而我也可以将资源投入到其他功能的开发。 + +其实类似例子有很多,但这两个案例已经足以说明问题了,现在来总结一下我在这样的情况下的方法: + + +对业务提交的任何功能需求,都按照最通用情形来考虑。即使对方只要一棵树,仍按照一座森林来考虑,我们不见得就要实际交付一座“森林”,但要为它预留足够的位置; +充分考虑已知场景的共同特征,暂时把它们的个性化特征丢角落里。过多考虑个性化特征只会限制你的想象力,这会牺牲掉许多易用性,但这是权衡之下必要的取舍,这是通用的代价。 + + +还有第 3 点,是上面两个实例没体现出来的: + + +任何业务需求,只要能采用已有的功能实现的,决不新增功能;只要保持已有功能通用性不变前提下稍微扩展就能实现的,也决不新增功能;好钢用在刀刃上,资源集中投放到开发新的通用能力上,不分散。 + + +还有第 4 点,是非技术性因素: + + +之所以我能坚持这么做,是因为我的用户群友好程度较高,主要得益于我平时注重提升他们的满意度:倾听他们的痛点,主动热心协助解决问题(编码、出方案等),帮助他们脱困;一般只要他们的自身的需求能按时交付,他们就会很高兴;记住:放低姿态。 + + +采用上面总结的 4 点咬牙坚持一段时间,大约 12 到 18 个月后,基本就可以熬过成熟期了。此时你会发现你的低代码平台基本上啥业务需求都可以实现了(虽然采用的实现方法没有像传说中的低代码那样酷)。但这意味着你的平台已经活下来了,并且有了一定应用基础,可以考虑进入了超越期了。 + +现在我们就可以充分聚焦到各个场景的个性化需求上了。这时我们再回顾下前面那两个实例。 + +通用化场景下表单的开发非常繁琐,数据采集后的所有校验都需要应用自己捕捉事件,自己编写校验逻辑。小到文本是否过长过短,大到服务端异步校验,都非常繁琐。那么我们可以针对表单的特性设计出新的配置流程,把校验逻辑全部自动化实现。应用只要填写校验规则和出错提示文本就可以了。 + +通用化场景下的 Dashboard,无论是布局还是交互也都非常麻烦。现在我们可以针对它设计一个基于卡片的布局器,交互也可以简化成单传参和获取数据。这样,一个图表甚至都可以不用去关注其他图表,也能实现联动效果。 + +通用化的图形实现,实际上就是直接填写 echarts options。这个门槛很高,需要学习 echarts 的大量配置 API。那么现在我们就可以针对常见的图形,设计出针对性的可视化配置方式,屏蔽掉绝大部分 echarts 配置细节,做到只要会填表单就能开发出复杂图形。 + +那为啥一开始不这么做呢?资源! + +当我们手里没那么多资源可以同时铺开时,我们既要保证新场景的支持不至于耽误应用团队的工期(这是最容易被投诉的因素),同时又要把已有场景做得很完美,这是需要大量资源的。实现一个通用能力,可以同时解决掉许多业务的开发需求,但如果我们把相同的时间投入到某个场景,即使做得尽善尽美,那也只能解决那一丢丢需求。我前面就说过了,在资源充裕的前提下,基本不需要考虑方式方法,干就完了。但现实的资源总是有限的,我只能这么做。 + +在超越期,场景化完善之后,是否通用场景就没用了呢?绝不是! + +这些通用场景会华丽地转职为兜底策略的一部分。我说过,再完美的低代码平台也总有能力边界,总有业务团队提一些“奇葩”需求越过这条线。此时通用的功能就可以发挥兜底的作用,让应用能按期交付的同时,也给平台团队喘口气的时间。我们甚至可以根据这个需求的“奇葩”程度决定是否要无视它,即使这个需求下次再来,我还是有办法治它的。这就是兜底策略给的底气! + +总结 + +今天我详细讨论了低代码平台演进策略,将演进过程分为三个主要阶段:MVP 阶段、成熟期、超越期。针对成熟期和超越期的发展策略,给出了不同的侧重。成熟期侧重于发展通用能力,而超越期侧重于发展场景化和提效的能力。 + +成熟期是一个相对漫长的,比较难熬的阶段。在资源不充裕的前提下,为了着眼于长期演进,必须坚持优先发展通用化能力的思路。对业务提交的任何功能需求,都按照最通用情形来考虑,同时充分考虑各个场景的共同特征,而暂时忽略他们的个性化特征。坚持采用已有功能来实现各种需求,通过这个方法倒逼已有功能的进一步通用化。同时我们也要放低姿态,注重培育和改善与应用团队之间的关系,还可以与 UX 团队保持良好通畅的沟通,要求他们不要设计出一些超过当前能力的应用出来。 + +到了超越期,我们的重心就要转移到各个功能的个性化需求上来了,此时要把个性化需求和易用性提升到最高优先级。必要的话,甚至可不惜重新设计各个具体场景的开发流程,以获得更高的自动化开发水平,尽可能高地提升代码的自动生成比例,从而最大化地发挥低代码的开发优势。而此时在成熟期留下来的诸多通用能力,就成了平台的兜底策略的一部分,可以兜住场景化所未能覆盖到的那 20% 的场景。 + +思考题 + + +低代码平台发展过程中的成熟期非常关键,这个阶段的发展质量基本决定了低代码平台整体质量,你认可这个观点吗?为什么? +除了这一讲提到的 Dashboard 场景和表单场景,你认为还有哪些场景与低代码技术是“天作之合”?完成了与低代码的结合之后,将对你现在的业务产生什么样的效果? + + +期待在留言区看到你的想法。我们下节课见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/05|基础设施:启动低代码平台研发之前,你需要有什么家底?.md b/专栏/说透低代码/05|基础设施:启动低代码平台研发之前,你需要有什么家底?.md new file mode 100644 index 0000000..2f19a0a --- /dev/null +++ b/专栏/说透低代码/05|基础设施:启动低代码平台研发之前,你需要有什么家底?.md @@ -0,0 +1,213 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 05|基础设施 :启动低代码平台研发之前,你需要有什么家底? + 今天我们来谈谈建设低代码平台之前,必须准备好基础设施。 + +在过去的几年里,Web 技术得到了显著的发展,无论是功能还是性能方面,浏览器能够承载高度复杂的 Web 页面里。在这个情况下,低代码平台,如果要选择 B/S 和 C/S 的其中一种作为它的基本架构,我相信你会和我一样,毫不犹豫地选择 B/S 架构。 + +虽然低代码平台是一种非常复杂,综合要求很高的软件,但 Web 技术的长足发展、浏览器优秀的功能和性能,完全足以打消你对 B/S 能不能搞得定的各种疑虑。同时,几乎所有的 PC 端的业务、越来越多的移动端业务也都倾向于使用 B/S 架构(或其衍生架构),用 Web 技术来制造 Web 应用是一个非常自然而然的选择。即使需要同时输出安卓、iOS 等 Native App,利用 Web 技术也可以很好地在浏览器中模拟 Native App 效果,几乎不会在可视化开发方面造成麻烦。 + +Web 组件集是 Web 应用的最重要基础设施,没有之一。对于基于 B/S 架构的低代码平台来说,更是如此。而且,低代码平台的 Web 编辑器对组件集有着更多要求。Web 组件集主要在三个环节发挥作用,一是用于构筑低代码编辑器自身,二是用于构筑编辑器的开发能力,三是用于构筑业务应用。这三者基本覆盖了低代码平台 60%~80% 的功能,可见编辑器的质量和能力基本直接决定了低代码平台成败。而 Web 组件集是这一切的基石,组件集的能力彻底渗透到这三个环节的方方面面。 + +所以在开始打造低代码平台之前,请先确认你手里已经拥有一套值得托付的组件集。今天我会从多个角度说清楚什么样的组件集才值得你的托付。 + +自主可控 + +你可能会好奇,为啥不是一上来就提需要哪些功能。相比组件集的功能来说,我觉得自主可控更重要,主要是因为组件集的第二个职能的要求:我们需要利用组件集来构筑编辑器的开发能力。为了说清楚这一点,我需要先简单介绍一下低代码编辑器如何管控细节的。 + +我这里列出了一个表格,表示的是低代码编辑器在 App 开发过程中对细节的管控程度,我们主要围绕配置量、难易度、适用范围、定制能力这几个关键维度来分析。 + + + +这个表格给出了这样的结论:无论是过于严格还是过于宽松的细节管控,都对 App 的开发不利。也就是说,我们需要一种合适的细节管控手段,让 App 开发过程中的 4 个重要考量维度都有很好的表现。 + +我认为,对 App 常用功能做组件化封装就是这样一种非常合适的细节管控手段。组件化封装过程可以把大量的 HTML/CSS 细节屏蔽掉。如果再有一定的 UX 规范做约束,甚至可以做到几乎屏蔽所有的 HTML/CSS 细节,这也使得我们几乎可以在不需要 HTML/CSS 知识的前提下用到组件(当然这是站在低代码模式的角度说的)。至于定制能力和适用性,则是通过组件封装时暴露的 API 来说实现的。这里说的组件 API 是指组件的输入属性和输出事件,这是组件外部唯一用于影响组件行为和功能的通道。 + +在 2018 年和 2019 年的时候,我做过竞调。竞调显示那时候多数的低代码编辑器还在采用直接配置 HTML/CSS 的方式,把过多的细节暴露给开发者,导致这些编辑器使用起来非常啰嗦,所需的 HTML/CSS 知识一点都不能少。过于啰嗦繁复的配置过程也导致无法做出复杂的应用来。 + +但是现在几乎所有的低代码编辑器实现都采用组件化来管控细节了,这方面大家达成了一致意见。正因为低代码编辑器需要借助 Web 组件来管控 App 开发的细节,所以低代码编辑器的开发能力与组件就产生了关联。编辑器的编辑过程,实际上就是在收集到开发者的配置信息之后,为组件的各个 API 生成正确值的过程。这就是我在前文所说的组件集被“用于构筑编辑器的开发能力”的原因。 + +现在我们可以来回答为啥对组件集来说自主可控更重要的问题了。 + +由于编辑器需要委托组件集来管控开发的细节,所以低代码编辑器本身是没有开发能力的,它的开发能力来自于组件集,也就是说,组件集的能力直接决定了低代码编辑器的开发能力。 + +在这个约束下,你对组件集的定制需求是巨大的,而且有的需求是专为编辑器定制开发的,从 Pro Code 模式角度上看这种需求不可理喻。假设你把 GitHub 上一套 star 最多的组件集直接拿来用了,这种情况下即使它有着极活跃的开源社区可以快速响应你的需求,组件集的守护者们也不可能处处为你定制,因为他们从未考虑过他们的组件集会给人类以外的编辑器使用。即使你说服他们接受了定制需求,交付周期又是另一个大问题。一个小小的修改,可能需要等上数周,即使你给他们推送 PR,但 review、讨论和评估也需要时间。 + +你不可能会将你的研发进度与一个不可控的开源社区挂钩。最终的结果就是,要么是你 fork 一个仓库出来单干,要么自己做一个二次封装组件集,相对可控地实现定制化需求。这样做意味着你已经向“自主可控”屈服了,只是程度上多少而已。 + +说完技术面,我们再从另一个角度来说说:UX 规范。 + +当你开始讨论是否要打造一个低代码开发平台的时候,想必你已经有了一定规模的应用了,因此想必你也已经有了自己的 UX 规范了。 + +使用第三方组件集,就意味着要全盘接受它的 UX 规范。技术面的问题是“看不见”的、是藏在“面子”后的“里子”。而 UX 规范则是彻头彻尾的“面子”,它必须要展示在你、低代码上的业务开发者和 App 最终用户面前。 + +那么,涉及的各方能长期接受这样的 UX 规范吗?万一有人,特别是最终用户对此提出异议呢?这种情况下,即使是对 UX 规范的微调,局面也会非常尴尬。因为 UX 规范就像国家宪法一样,是一个国家法律的根基,即使 UX 规范做了很微小的改动,也会波及所有组件的外观。 + +一个工作量较小但可行的方法,是写出优先级更高的 CSS 覆盖掉原始组件的样式,但这些 CSS 样式开发难度很高且极其难以维护。更关键的是,这个做法直接侵入到了组件集的私有实现。这部分实现是没有兼容性保障的,守护团队几乎每个小版本的升级都有可能导致覆盖样式失效。 + +所以挑选组件集的时候,我们的第一要务是要选一套具有自主可控的组件集,即使它看起来没有那么强大。它最好是你自己或下级团队开发的,这样才具有完全的自主权。至少也要是兄弟单位开发的,而且你要有足够的权限修改它的源码。 + +我把给低代码编辑器使用的 Web 组件集称为可视化组件集,它和传统的 Pro Code 组件集有相似之处,也有差异。有机会我会再来详细阐述两者之间的异同点。 + +封装程度高 + +这里,我要再次提醒你所选的组件集是给编辑器使用的,而不是给人类使用的。 + +前面在讲自主可控时,我提到组件集的一个重要任务是用于解决低代码编辑器的细节管控问题。如果组件集的封装程度不高,就达不到细节管控的目的。比如下面这个例子,它来自于一套实际组件集,界面上显示 4 个 radio button: + + + +这套组件集采用如下 API,HTML 部分是: + + + + + + + +这样的 API 问题很多,我们先不说 API 是否优雅,主要关注其相当混乱的配置方式: + + +有的是通过变量配置,比如是否选中功能(Angular 采用类似 [checked]=“var” 的格式来引用变量); +有的是通过样式控制,比如是否 disabled,用 vx-radio-check-disabled 样式配置 disabled 的状态; +有的是通过 HTML 节点配置,比如单独使用 span 来配置 radio 的文本。 + + +你要注意,编辑器并不怕生成一大片代码,但害怕东一榔头西一棒,这会对代码生成器造成许多不必要的麻烦。前面就是一个非常典型的例子。一个 radio 无非就 3 个配置项:文本、状态和值,这个例子采用了各不同的方式来配置,有的用了变量,有的用了样式,有的用 HTML 节点。作为对比,还有一个比较好的方式是采用数据驱动的方式统一配置,比如: + +// html + + +// typescript +const options = [ + {label: '选中', disabled: false}, + {label: '选中禁用', disabled: true}, + {label: '未选中', disabled: false}, + {label: '未选中禁用', disabled: true}, +]; +const selected = options[0]; + + +可以看到,这个版本的 HTML 极其简洁,所有配置项均通过数据变量来实现,分别是备选列表 options 和选中条目 value 两个变量。版本 2 的封装方式是一种比较好的方式,主要体现在组件的配置项都是通过一系列变量来实现,低代码编辑只要正确生成变量的代码就可以了,无需关注 HTML/CSS 代码的生成。 + +下面我们再看一个示例: + +// html + + +// typescript +const tableData = { + header: ['列1', '列2', '列3', '列4'], + field: ['field1', 'field12', 'field13', 'field14'], + data: [ + ['cell11', 'cell12', 'cell13', 'cell14'], // 第1行 + ['cell21', 'cell22', 'cell23', 'cell24'], // 第2行 + ['cell31', 'cell32', 'cell33', 'cell34'], // 第3行 + ['cell41', 'cell42', 'cell43', 'cell44'], // 第4行 + ] +}; + + +这里,我们采用一个数据 tableData 数据结构描述了一个表格,将表头、列名、数据都通过数据的方式来配置。 + +我将这种封装方式称为数据驱动模式,这种模式的关键特征是组件将 HTML/CSS 彻底封装到其内部,只暴露出一些属性对外提供配置入口。这样的封装方式对低代码平台的代码生成器是非常友好的。我们应该优先挑选具有这种 API 特征的组件集,自行实现时也需要采用这样的封装方法。 + +为了加深你的理解,我列出了现在市面上常见的组件的封装方式,作为补充和对比: + +第一种我们叫它数据驱动封装方式。它的典型特征是所有 API 都以数据的方式来驱动,彻底将 HTML/CSS 封装在其内部。前文给的第二和第三个例子就是用这样一种方式封装出来的组件,这是一种适合给低代码编辑器使用的封装方式。 + +第二种我称之为模板驱动封装方式,典型特征是 HTML 部分非常复杂,数据、样式、状态都几乎在 HTML 模板里实现,前文给的第一个 radio 例子就是用这样的方式封装出来的组件。这种组件更加适合 Pro Code 模式来使用,由于它直接将 HTML 模板当做一种 API 暴露给应用,因此应用可以按需改造 HTML 模板,灵活实现特定功能。也就是说,它具有更强的定制性,这方面是数据驱动类型组件所不具备的能力。 + +最后一种我们暂且称它为 CSS 样式模板,典型特征是只给出了 CSS 样式,没有带动作。它的封装度极低,需要由于自己写动作来完成组件的功能,比如 boostrap 组件。这样类组件集是非常不适合用于低代码平台的开发的,不仅如此,由于封装度太低,实际上也不适合用于 Pro Code 模式的大规模开发,只能作为一种轻量的规范化的模板在特定场合使用。 + +功能强大 + +虽然我把组件集的功能排到第三位考虑因素,但不意味着组件集的功能不重要。 + +实际上,低代码平台对组件集的功能需求是非常大的。低代码编辑器自身必然不是一个简单的 Web 应用,状态多、形式多样、功能丰富、交互密集、性能要求极高等标签是可以毫不犹豫地往上贴的。要能承载这样一个复杂 Web 应用,对 Web 组件集的要求显然不会低。能同时满足前面提到的几个标签的组件集,就已经超过了市面上 90% 以上的组件集了。 + +那么,什么样的组件集可以称之为功能强大呢?我们可以参考下面这些指标: + + +组件集里至少包含 50 个以上的原子组件和容器类组件,才能基本覆盖完整日常所需; +具有良好的视图悬浮(气泡化)功能封装和多层视图叠加管理能力。低代码编辑器往往有密集的配置入口,许多配置项需要就地弹出气泡甚至多级气泡来承载,避免打断当前的开发工作; +数据采集类的组件(文本框、数字框、下拉选择等)必须对表单友好,这样才能更容易实现出表单类页面; +对常用功能要有统一封装,在 Angular 里称为指令 /Directive,特点是这些功能可以“外挂”到任何普通 dom 节点、组件节点上,实现功能扩展。比如,像任意视图下拉、上传功能、多功能徽标、下拉多级菜单、拖拽功能等功能都值得封装; +一个符号图标库,这个不一定非要自己做,但必须要有。编辑器密集的配置界面上“寸土寸金”,多用图标可以节约许多空间。当然,我们也可以直接使用开源的,font-awesome、material design都提供了不错的基于 svg 的图标库,基本满足日常使用所需。 + + +这里,我要再强调一下对组件集的性能要求,这个要求主要来自于低代码编辑器本身,以及所见即所得效果的实现方式。 + +低代码编辑器的复杂度非常高,特别是在画布界面,有各种各样的编辑器、配置界面、悬浮气泡、对话框等。活动视口(ViewPort)上同时有一两百活动组件都是家常便饭,这对组件的渲染性能和脏检查性能提出了很高的要求。 + +其次,如果画布上的所见即所得效果不是采用 iframe 实现的,而是在画布上采用一个动态模块直接渲染出来的,那对编辑器的性能要求就直接翻倍了。此时,不仅需要高效渲染编辑器本身,还要在画布上把 App 的运行效果也实时动态渲染出来。另外,有的 App 本身也具有很高的复杂度。这样一来,在画布界面上同时存在两三百个活动组件实例也是可能的。如果原子组件的性能不佳的话,整个画布操作起来就会非常卡,影响开发效率,也影响开发者的心情。 + +皮肤深度定制能力(可选) + +虽然,组件集支持多种颜色的皮肤并不是一个必备的能力,但对低代码编辑器来说,这确实是一个非常实用的功能。这个能力赋予了应用在整体外观上的配置能力,而且低代码编辑器可以很容易地做到一键换肤。在同个色系下实现不同颜色的皮肤难度并不大,而且多数还可以做到热切换皮肤颜色。 + +换肤能力更进一步的需求是,需要支持跨色系的皮肤。一般来说,至少需要支持有两种基础色系:明亮色系和黑暗色系。 + +跨黑白色系的皮肤实现的难度不小,而且只有组件自身支持跨色系还不够,还需要有一套机制可以帮助应用定制的视图也实现跨色系换肤,这方面难度就更大了。因为组件集内部的实现是可控的,但是应用定制的视图结构是不可控的。一个比较好的实现方式是利用 CSS3 的变量特性,但这个功能在 IE11 上支持不好,需要有一个取舍。下面这段 CSS 简单演示了这个特性: + +:root { + --blue: #1e90ff; + --white: #ffffff; +} +body { background-color: var(--blue); } +h2 { border-bottom: 2px solid var(--blue); } + + +换肤能力再深一步,就需要支持黑白同屏功能了。前面我介绍了组件集可以同时支持黑白不同色系的皮肤以帮助应用构建出明亮风格和黑暗风格 App。但这两种色系只能二选一,那有没有需要在同一个页面上支持两种相反色系共存呢? + +这个特性乍听起来很扯淡,但细一想,这样的场景是很多的。比如一个页面的整体是明亮色系的,但带了一个深色的 header 或侧边导航。组件支持黑白同屏的皮肤可能是换肤能力里的最顶级场景了。黑白同屏的换肤能力能让低代码编辑器自身和应用页面具有非常自由的皮肤切换能力。 + +有机会我会说说如何实现支持黑白同屏的换肤能力,以及如何做到热切换。 + +总结 + +今天我详细介绍了在启动低代码平台研发之前需要具备的基础设施:一套合适的组件集。组件集之所以如此重要,是因为贯穿了基于 B/S 架构的低代码编辑器三大基本功能:构筑低代码编辑器自身、构筑编辑器的开发能力、构筑业务应用。这些功能覆盖了低代码平台 60%~80% 的功能。可见,组件集对低代码平台的研发有着决定性的影响。 + +在挑选组件集的时候,我给出了 3 个重要的考量维度,按重要程度分别是自主可控、封装程度和功能是否强大。由于所选组件集是给低代码编辑器使用而非人类使用,因此会有一些看似奇怪的定制化需求。封装程度高使得代码生成器实现起来更加容易,组件集需要有强大的功能才能满足低代码编辑器自身需求,才能满足编辑器的开发能力,才能满足业务团队的各种需求。 + +组件集的研发务一定要比低代码平台的研发先启动。视投入的人力数量和质量,我建议你至少提前 6~12 个月。两者研发的启动时间不宜靠得太近,在组件集无法支撑低代码的研发需要的时候,往往只能暂停低代码平台的研发,先补充组件集的能力。但启动时间也无需间隔过长,在组件集有一定功能积累之后,两者可以并行演进。 + +在组件集启动研发之前,最好已经有一定基础的 UX 规范了,UX 规范对组件集和业务应用的“面子”有这巨大的影响。但 UX 规范不一定需要完全自己开发,如果资源不允许,完全可以考虑抄抄大厂的作业,阿里的Ant Design和 Google 的Material Design都是不错的选择。腾讯的ISUX和字节的ArcoDesign也是很优秀的,可以通盘接受,也可以基于某个规范做一些定制化。UX 规范会对组件集、低代码编辑器、业务应用等产生全系列的影响,不可忽视。 + +最后我还简要介绍了一个可选能力:皮肤定制能力。这是一个锦上添花功能,没有它确实不会怎么样,但是有这个能力可以让低代码平台到业务应用具有快速调整外观的能力。特别是黑白色系皮肤的支持,可以大大提升业务应用的展示张力。我常说的一句话是:深色皮肤自带高大上光环,相同的一套 UI 设计,深色系的比浅色系的要显得更加高端,也更加节能和环保。如果能做到深浅色系自由切换,那就更赞了。 + +技术积累是这一切一切的基础:一两位有深厚技术积累的领军人物是更重要的家底。 + +最后安利一下我司开源的组件集Jigsaw,由我主导研发。它对中兴低代码平台提供了非常良好的支持,没有Jigsaw就没有中兴低代码平台,Jigsaw性能和功能都非常优秀。 + +思考题 + + +为了能满足构筑低代码编辑器的开发能力,组件集需要具备哪些功能特征和非功能特征? +为了更好满足你的业务开发需要,你认为组件集的哪方面能力更加重要?为什么? + + +欢迎在留言区写下你的想法。下节课见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/06|踏出新手村便遭遇大Boss:如何架构低代码的引擎?.md b/专栏/说透低代码/06|踏出新手村便遭遇大Boss:如何架构低代码的引擎?.md new file mode 100644 index 0000000..d97134f --- /dev/null +++ b/专栏/说透低代码/06|踏出新手村便遭遇大Boss:如何架构低代码的引擎?.md @@ -0,0 +1,159 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 06|踏出新手村便遭遇大Boss:如何架构低代码的引擎? + 可视化开发是所有低代码工具 / 平台(下文简称低代码或 Low Code)的标配,是成为低代码工具 / 平台的一个必要条件。而承载可视化开发的核心基础设施,就是所见即所得的编辑器。 + +这个编辑器非常重要,它的使用体验、能力和易用性在极大程度上决定了低代码整体的成败。由于编辑器的实现是一个非常大的话题,我们需要分成 8 讲才能说清楚。所以,今天这节课我们先从编辑器的引擎切入,从架构的角度聊一聊应用代码生成器与编辑器之间的关系。 + +在讨论低代码到底是银弹还是行业毒瘤那一讲中,我们了解到低代码的开发过程就是不停地在描述和细化一个业务最终的样子。这就要求低代码编辑器能实时反馈出开发人员的操作结果,这也就是所见即所得的含义。 + +而通过模拟,是难以保持“所见”与“所得”的一致性的。为了达到“所见”与“所得”的高度一致,我们就需要实时地把应用创建出来,创建应用的过程就是生成并运行应用代码的过程。显然,生成应用代码是这一切的基础。编辑器在收集到开发人员的操作后,应该立即生成应用的代码。 + +那么,生成代码的功能在架构上和编辑器可以有什么样的关系呢?不同的关系对低代码长期演进会有什么样的影响呢?不同的组织应该如何选择合适的架构呢?这正是你我今天要探讨的主要内容。 + +代码生成器与编辑器的关系 + +我了解过许多失败的案例,它们多数有一个共同特点:开始于一个玩具。故事基本上可以归纳为某个人或者团队因为偶然的心血来潮,写了个小玩意,有个界面,支持拖来拖去,可以生成特定功能。老板了解后都觉得有用(老板觉得没用的那些自然就不会被了解到了),于是加大投入,想做得更大、更好。结果,初创团队“鸡血”了几个月后,基本就玩不转了,要么销声匿迹,要么推倒重来。 + +其实界面拖拖拽拽生成特定功能的门槛并不高,但是要承载厚重的低代码战车,则需要有很深远的设计和思考,其中最重要的一环是如何生成应用的代码。 + +代码生成器与编辑器之间的关系,可以大致分为这几个层次: + + +Level 1:没有代码生成器的概念,或者极其粗糙; +Level 2:有相对独立的模块用于生成代码,但该模块与编辑器耦合严重; +Level 3:代码生成器与编辑器基本相互独立,具有同等地位; +Level 4:插件系统与生态,编译器必须再次抽象才能实现插件系统。 + + +Level 1 + +如果连代码生成器的概念都没有,或者由散落在代码仓库各个角落里的三两个函数构成,那么这样的编辑器显然是无法区分编辑态和运行态的。即使有代码生成器,也是在编辑器的功能基础上通过 if else 来实现的,比如引入一个只读状态,在这个状态下无法编辑,并将它作为运行态来用。很显然这样的实现方式,是极不妥的。由于没有足够的抽象,功能点加多了后,编辑器的代码就会变得极其难以维护。 + +关于代码的可维护性,我常常说的一句话是:一个 if 一个坑。使用 if 就等于在尝试对事物状态进行枚举。简单事物可以枚举所有状态,但多数事物的状态是不可枚举的。每少考虑一个状态就是一个 bug 或需求,同时也是对已有逻辑的一次冲击。而为了能够区分出相似的情况,往往需要增加新的 flag,在 flag 数量达到某个数之后,这份代码就再也改不动了。 + +Level 2 + +当你意识到 if 解决不了问题时,往往就会开始考虑对编辑器做抽象了。而编辑态和运行态是编辑器两种最基本的状态,这两种状态有很多共同的部分,也有一小部分差异。所以抽象的第一步就是把公共部分抽取出来,并在编辑态和运行态下改写公共层里的某些行为。这是 OOP 思想的最基础的应用。 + +我们这一讲先不展开讲解如何去抽象,但是有一点是非常明确的:生成代码的能力是两个状态都需要的,应该归入公共层中。所以,很自然的做法是把散落在代码仓库各角落里的那三两个函数挪到一起,最起码是放到同一个文件里去,然后适当调整入参和依赖,让它们能恢复正常功能。更进一步,需要将原有的 if else 改用 OOP 的多态特性来实现。 + +到了这个时候,你就需要好好考虑一下 TypeScript 了。在这节课中,我不想展开讨论是 TypeScript 好,还是 JavaScript 好这样的细节,但是我的建议是此时应该坚决引入 TypeScript。因为 TypeScript 提供了一套非常完整的 OOP 实现,而 OOP 是用于对复杂事物做抽象的必备武器,基于 JavaScript 原型链自行实现的 OOP 没有那么完善(我敢保证!),弃之勿留恋! + +另一个关键原因是,TypeScript 提供了极完善的静态类型支持。记住,你正在开发一个编辑器,除非你打算一直把它当作一个玩具,否则你要先做好至少 5~10 万行代码的觉悟。这样量级下的代码,如果没有类型的辅助,我们的开发效率将是有静态类型支持下的 1⁄2~1/3,甚至更低。 + +当你的编辑器的公共层里有了一个相对独立的代码生成器模块之后,编辑器的长期演进就有了一个架构基础,但也仅此而已。 + +此时你的 APP 依然只能在编辑器上运行,无法独自运行,这会导致将来你的业务应用和你的低代码平台之间产生耦合。要么你把应用包住对外提供业务价值,要么应用把你包住,这取决于谁更强势。但无论如何,耦合是不可避免的。 + +到了这里,你就需要好好思考一下将来你和你的应用团队之间的关系了。如果你的老板本就打算或允许你们耦合在一起,那么到这里你就可以继续开发编辑器的其他功能了。反之,这事还没完。 + +提醒一点:请务必确认你的老板知晓平台和应用是可以实现解耦的,否则请适当给予他一些提示(写一份 PPT 吧,他会因此更喜欢你的),并且确认他确实经过一番思虑后才做出保持耦合的决策。 + +因为,与应用之间保持耦合这个事情,开弓就没有回头箭了。如果你的老板依然保持模棱两可的状态,那么我建议你将生成代码这事继续完善下去,避免日后他后悔了并给你下一个你压根就无法实现的任务。与应用解耦状态下是可以融合部署对外提供业务的,反之是走不通的。 + +Level 3 + +为了解除平台与应用之间的耦合关系,我们需要将代码生成器进一步独立出来,提到与编辑器同等地位上。在 Level 2 中,我们是把它当作一个公共模块(一个库),而到 Level 3,我们是要把它作为与编辑器对等的一个独立功能来实现。 + +一个比较好的实现方式是将它做成一个命令行,可以在 shell 终端里跑。命令行的好处很多,主要很容易与 DevOps 流水线结合使用,或者实现各种自动化。应用跑一下命令行就可以生成一份可以独立运行的代码出来,这样你爱放哪运行都与编辑器无关了。 + +将代码生成器与编辑器彻底分离,让它们可以脱离对方独立运行,需要对代码做更深一层次的抽象。在 Level2 中,两者的关系是下面这样子的: + + + +两者之间是紧耦合的关系,这意味着代码生成器很难被扩展和定制。不可否认的是,业务的发展速度是远大于代码生成器的,因此业务的需求永不终结。在代码生成器难以扩展和定制的前提下,低代码平台非常容易为了满足某个紧急业务需求,不得不将该业务耦合到代码生成器的实现中。 + +一旦开了这样的口子,代码生成器架构的腐化也就开始了,逐渐就会跟越来越多的业务耦合进来,不需要太多时间,这样的代码生成器就无法继续演进下去了。 + +而在 Level 3 架构下,编辑器和代码生成器之间的关系变成了下面这样: + + + +代码生成器和编辑器处于同一层级,并且引入了协议层。协议层的作用之一,就是对生成代码各种功能的 API 做出抽象和约束。代码生成器主要职责是实现这些协议,而编辑器的主要职责则是调用协议层提供的 API 来完成代码生成等一系列上层活动。 + +然而,引入协议层的更深层目的,是为了方便应用扩展和定制。从实现角度看,协议层实际上就是一堆接口(interface),这些接口的默认实现是由代码生成器按照其通用的、与应用无耦合的方式来实现的。当通用的实现不满足应用的需要时,应用要有渠道来编写适合其自身的协议层的实现,并覆盖掉默认通用的实现,从而实现编译器的定制化和按需扩展 *。 + +当然,让应用实现一套完整的编译协议是不合理的。我们可以在编译协议层里加入部分实现,这样应用在定制时就可以只覆盖少数不适合的实现,复用大部分的已有实现,这样可以大大减轻应用定制的工作量和难度。因此,这个架构的实际关系是下面这样的: + + + +虚线框里的部分,实际上就是应用定制代码生成过程所需的 SDK。这就是 Level 3 的架构,它不仅具有更好的长期演进基础,还具有非常好的、非常优雅的扩展性和定制性。至此,我们可以给代码生成器换一个高大上的名字了,将其称为编译器。 + +Level 3 只提供了扩展和定制的可能。如果我们对这些扩展和定制点再做一番系统性的设计,在此基础上抽象出编译流程的各个时机(可称为编译生命周期),以及对各个时机具备的可扩展点加以规范,从而形成一套完备的插件开发能力,那我们可以让这个架构演进到 Level 4 了。Level 4 的最主要目的是要形成完善的插件系统,进而具备打造生态圈的基础。Level 4 已经超出这节课的范畴太多了,在这个专栏的最后,我会留一讲专门来说这个话题。 + +最后,我再分享一点我负责的低代码平台目前的架构演进的状况,可以作为案例给你一点参考。 + +其实,我也不是一开始就有如此清晰的分层演进的考虑的。不过庆幸的是,我在编码开始前多花了点时间做了类似前文的思考,所以我直接跳过了 Level 1 和 Level 2,直接按照 Level3 的架构目标开始编码。但为了更快做出 MVP(最小可用版本),我并没有严格按照 Level 3 的架构实现,而是偷了点巧,先把编译器当作一个库实现出来,在 MVP 完成之后老板认可了,才专门花了点时间把编译器独立出来,完成 Level 3 架构的转型。 + +我想说的是,这里所说的架构分层是一个目标,你在实现时,可以根据实际情况(工期、人力等)灵活调整。但即使我未严格遵循 Level3 的架构要求实现,我还是让编译器尽可能地与编辑器保持独立,同时在过程中逐渐抽取 API 作为协议层。 + +目前,我负责的低代码平台正处于 Level 3.5,它已经拥有了相对完善的插件系统,具有发展生态的基础了。演进到了这个阶段,它的编译器已经很稳定了,目前它的主要任务是推广和布道,枯燥且乏味。 + +低代码编辑器要能实现最基础的“所见即所得”的闭环(就是达到给老板演示的最低要求),就不得不实现生成代码的功能。这个看似简单的小功能,背后却是牵动着日后长期演进的许多考虑。 + +至此,你是否觉得我们这节课的标题起得非常好?低代码开发者刚走出新手村碰到的第一个怪物就是一个实力 Boss,关键是这个 Boss 看起来和一个普通小怪差不多,我们冲上去居然不会被它秒杀,嗑点药居然还能站得住。 + +当你磨了半天都没能杀掉它的时候,不妨停下来仔细观察它,你会发现原本你以为的青铜怪居然是一位王者。此时我们应该立刻回城并想办法升级(氪金 ^_^)装备和技能,决不可恋战! + +生成代码总体流程 + +接下来,我再说说实现编译器的两种可行方法及其优劣和适用场景。注意,下面讨论的默认应用是以 UI 复杂且交互密集为主要特征的 PC 端网页,具有类似性质的移动端页面也适用。C/S 架构下的 UI 不在讨论范围内。 + +编译器的输入是一组结构化的数据。即使编辑器与编译器之间采用某种 DSL(领域编程语言)作为协议,但是输入给编译器之前,我们肯定要将输入数据解析为结构化的数据。为了描述方便,我使用 SVD 这个内部术语来指代这组结构化数据。编译器的唯一任务就是将 SVD 转成代码。这里有两种编译选择: + + +直接法:直接将 SVD 生成出浏览器能识别的代码; +间接法:先将 SVD 生成出某种 MVVM 框架的代码,再利用其编译器进一步编译成浏览器能识别的代码。 + + +两种方式各有优劣,直接法的最大好处是架构简单,特别是不需要再引入和协调另一个编译器,能少写不少代码;其次是效率高,JiT 编译器只需间接法的约 20%~30%,几乎可以做到全程百毫秒以内的 JiT 编译消耗。 + +当然,直接法的代价也是非常明显的。它最大的问题在于,你必须考虑你所在场景里的前端技术栈选型。因为到了 Level 4 的时候,你就需要考虑生态的问题了,而现在前端生态最大的割裂莫过于 AVR(Angular、VUE、React)等框架的技术栈选型了,极少有裸奔(含 jQuery)的。如果你期望到时能和它们玩到一起去,那么现在就应该选择相同的技术栈。 + +无论你选择了 AVR 中的哪个,都意味着你只能采用间接法。如果你处于一个强势的机构并且你老板坚定为你站台,所有不用你平台的应用团队一律给考核 C,那么可以无视这条(你好幸福)。或者你的平台不打算给内部开发人员使用,也可以不考虑这条。 + +直接法的另一个约束在于页面组件集的实现。等等!编译器和组件集居然也能扯上关系?实际上,我认为低代码实现之初主要的基础设施之一就是要有一套合适的、可控的组件集。你一定要记住,细节管控的程度会对低代码的易用性和适用性产生决定性的影响,而权衡之下最合适的细节管控粒度就是 Web 组件集。 + +Web 组件集将大量的 HTML/CSS 细节封装掉,只通过 API 的方式暴露出来,而低代码通过引入一套组件集可以屏蔽掉 HTML/CSS 的所有细节,大大提升易用性。如果你的组件集是用 jQuery 做的,那么可采用直接法生成代码。但是,我猜大概率你不会这么做,即使曾经有 jQuery 实现的组件集,也早就被你嫌弃并使用 AVR 之一重写了(这是一个重复再造轮子并获得晋升的绝佳机会,如果你没抓住,那就太可惜了)。 + +如果你选用的组件集是 AVR 之一实现的,那很可惜,你只能采用间接法。不过,随着 Web Component 的发展,目前 AVR 都已推出 Web Component 的转译器。利用 Web Component 技术,你就可以直接使用浏览器原生技术来动态渲染 APP。如果你有条件使用这个技术,那也可以无视这条约束了。比如,华为云的低代码平台就是采用这个技术来屏蔽 AVR 差异的,这是一个成功的案例。莫春辉老师在GMTC2021 深圳站上详细介绍了这个案例,感兴趣可以去看看。 + +只要符合上述两个条件之一,就只能采用间接法。现在我再说说间接法。 + +估计你已经看出来了,间接法就是一个备胎。只有在你没有条件使用直接法的时候,才用它。它的好处是能兼容 AVR 等各种框架,对 Web 组件集的要求也会低很多,也不需要考虑组件集提供 Web Component 版的工作放在哪。 + +间接法的代价是架构复杂,需要额外协调 AVR 提供的编译器,这事需要额外消耗许多脑细胞,而且社区里提供的资料都是如何“正常”地去用它。而协调这些编译器这事却是不走寻常路,这样碰到坑大概率要去看编译器的源码,难度很大。当然,如果你是一名技术极客,把这事当作一个优势看待,我也不拦你。 + +其实,间接法有一个绝对优势,那就是可以支持 Low Code 和 Pro Code 混合开发一个 APP。这是直接法所做不到的。支持混合开发的重要意义在于,你可以相对妥善地处理好存量代码(没错,就是你的低代码做出来之前的那些页面代码)。这点有机会我们再聊。 + +最后,如果你和我们一样选了 Angular,那恭喜,你还需要额外多协调 TypeScript 编译器。我在 QCon+ 有一次在线分享,讲的就是如何在浏览器构建 TypeScript 的 JiT 编译器以打通整个编译流水线,那个分享具有非常好的实操价值,你可以去看看PPT。 + +总结 + +今天,我们从架构的角度,对低代码编辑器与应用代码生成器(编译器)之间的关系做了详细的讨论。根据抽象程度的不同,我把代码生成器划分为 4 个层级,并详细说明了前三个层级的特点和适用的场合,以及各个层级对低代码平台的长期演进会产生啥样的影响: + + +Level 1:基本上就是一个玩具,适合用作试验品,用于快速试错、收集你的低代码潜在用户的反馈,如果确定要开发一个可以称为工具的低代码,那么强烈建议推倒重来; +Level 2:基本上可以称为是一个低代码工具了,应该可以一定程度上发挥低代码的优势来,也具备了长期演进的架构基础,但是扩展性和定制性也很弱,容易与应用产生耦合,因此只能在小范围内使用,无法规模推广; +Level 3:基本上可以称为是一个低代码平台了,它的架构清晰、层次分明,具有非常好的长期演进能力,且具备良好的扩展性和定制能力,能处理好业务团队提的各种需求,对于其能力之外的需求,也可以通过扩展和定制的方式优雅地处理,适合大规模推广。 + + +同时,这一讲我们也讨论了实现代码生成器的不同方法,主要侧重讨论了如何根据现有技术选型、长期演进方面挑选合适的实现线路。 + +直接法架构简单、性能好、实现工作量低,但是选用的先决条件苛刻,主要有两点:一是你所在机构的前端技术选型;二是所用组件集的前端技术选型。间接法作为直接法的备胎,当没有条件使用直接法时,只能选择间接法。间接法架构复杂、JiT 及实时渲染性能差、需要额外协调所用框架的编译器等。但间接法额外带给我们一个直接法所没有的能力:允许 Low Code 与 Pro Code 混合使用,进而可以相对妥善地解决低代码模式与传统模式存量代码之间的关系。 + +在下一讲中,我将会从实操角度,介绍如何做出一个和人工一样实现几乎任何业务功能的代码生成器,以及一个 Level3 层级的编译器如何实现。 + +思考题 + +根据抽象程度的不同,应用代码生成器与编辑器之间可以分为几个层级?各个层级的关键特征是什么?不同层级对低代码平台长期演进具有什么样的意义?欢迎在留言区写下你的看法。 + +我们下节课见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/07|结构化代码生成法:代码如何生成代码?.md b/专栏/说透低代码/07|结构化代码生成法:代码如何生成代码?.md new file mode 100644 index 0000000..1d84d1a --- /dev/null +++ b/专栏/说透低代码/07|结构化代码生成法:代码如何生成代码?.md @@ -0,0 +1,348 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 07|结构化代码生成法:代码如何生成代码? + 编辑器是低代码平台一个非常重要的基础设施,而代码生成器是编辑器的引擎,是编辑器做到所见即所得效果的基础。 + +在上一讲中,我从架构的角度详细理清了代码生成器和编辑器之间的关系,以及代码生成器与低代码长期演进之间的关系。 + +那么今天,我们就从实现的角度说清楚代码生成器是如何实现的。 + +人类是如何写代码的? + +虽然写代码是我们日常工作内容,没有啥特别的。但这一讲的目的,是让代码代替人工来生成代码,所以我们需要快速回顾一下我们日常敲代码的过程,以及敲出的代码都有哪些部分。为了帮你快速回顾这个过程,我把手工正常开发一个组件的部分过程录屏下来了。 + +需要特别说明的是,首先我是用 Angular 写的这段代码,即使你没学过 Angular,但你光看代码也是可以轻松理解的;其次我今天介绍的这个方法是通用的,不限于生成 Angular 的代码,你可以用这个方法生成任意框架代码,甚至用来生成 Java/C/C++ 等后端代码。 + +下面我们看第一段视频,演示的是创建组件骨架代码: + + + +把这十来秒的视频多播放几次后,你会有一个感触:正常敲代码的整个过程是按需的。显然你不会一上来就去敲第一行的 import,而是当前需要用到 Component 这个渲染器时,你才会想起来:哦,我应该去 import 一下。 + +接下来是第二段视频,给组件编写样式: + + + +如果给一个不会写代码的人看这段视频,他可能会觉得,你们敲代码怎么是东一榔头西一棒的,一会在这里插入几个字符,一会在那插入几个字符,而不是像写文章一样,基本保持自上而下、从头到尾的节奏。 + +然后是第三段视频,给组件创建输入条件(用 Angular 术语是:输入属性 /Input): + + + +组件的外部可以通过视频里的 title/content 两个参数给这个组件喂数据。显然,输入属性的种类、数量、类型都是按需定义的,我们无法事先约定一个组件需要啥输入属性。下面这行代码就很好地演示了如何使用这两个属性: + + + + +除此之外,一个普通组件的开发过程还至少包括如下的内容: + + +类成员方法的定义: + + + + + +组件对外事件的定义(用 angular 术语是:输出属性 /Output): + + + + + +某些比较复杂的组件,可能还会涉及类的继承和接口实现定义,关注下图中的 extends 和 implements 关键字: + + + + +我们再把前面几段视频展示的内容列出来,会发现还有: + + +import 其他类(含第三方的或者其他类); +组件 HTML 模板的定义; +组件样式的定义; +类成员变量的定义和初始化; +类构造函数需要注入的功能,这里,注入 /Inject 是一个 Angular 术语,比如下图中的 _http 变量就是通过注入而来的: + + + + +如果你不是用 Angular 开发前端,那可能不会涉及所有条目,但根据你所用技术栈,可能也会多出一些新的条目来。不过,没有关系,我给出的方法依然适用,你只需要将多出的内容像我一样将其列出即可。 + +结构化代码生成法 + +有了前面的铺垫,我们现在就可以来介绍一下如何让代码生成代码了。 + +前面我们提到,正常写代码的过程是按需、跳跃式地在不同部位插入一个个片段,没人能像写文章一样自上而下、从头到尾、一气呵成地写出一个视图组件的代码,利用代码来生成代码的过程也不可能是这样的方式。 + +虽然正常写代码过程是东一榔头西一棒,一会在这里插入一个片段,一会在那插入一个片段,甚至还要修改别的文件,但是插入点的类型总是有限的!如果你能意识到这一点,那你就朝着正确的方向迈出第一步了。 + +接下来需要解决的问题就是,都有哪些类型的插入点呢? + +如果此时你还无法马上回答这个问题,那么再翻回去,重温一下前面的内容。我在第一个小标题末尾处详详细细列出了开发一个普通视图组件要做的事情,每一条就是一类插入点。起码在生成一个普通视图组件的代码的时候,插入点种类就这么多了。 + +那么如何生成一段有特定功能的代码呢?我们先看一段简单但有代表性的伪代码: + +import {EventEmitter} from "@angular/core"; + +export class MyComp { + public select: EventEmitter; + + constructor() { + this.select = new EventEmitter(); + } +} + + +这段代码功能极其简单,在一个类中定义了一个名为 select 成员变量,然后在构造函数里初始化了 select 变量的值,就这么多。但是开发这段代码至少涉及了 3 类插入点,分别是: + + +import 区; +构造函数区; +成员变量区。 + + +假设现在有 3 个值分别代表这 3 个区:importSection,constructorSection,memberSection,那我们实际上只是向这 3 个值里分别放入对应的代码片段,完成之后,这 3 个值分别大概如下。 + + +importSection: + + +import {EventEmitter} from "@angular/core"; + + + +constructorSection: + + +this.select = new EventEmitter(); + + + +memberSection: + + +public select: EventEmitter; + + +仔细观察一下,你就会发现各个插入点的代码片段都是普通的文本了,此时的它们没有任何语义,也没有任何功能。 + +再仔细观察一下,你会发现代码片段里包含了许多关键字,这些关键字实际上是冗余的,可以省略。 + +最后,不难发现,每一个插入点应该是一个数组,因为任何功能的实现都可能会往一个或多个插入点插入点添加片段。所以经过一番思索后,各个插入点的内容可以先优化成这样: + + +importSection: + + +[ + {identifier: 'EventEmitter', from: '@angular/core'} +] + + + +constructorSection: + + +[ + {statement: 'this.select = new EventEmitter()'} +] + + + +memberSection: + + +[ + {modifier: 'public', identifier: 'select', type: 'EventEmitter'} +] + + +这里,我不仅删去了冗余的关键字,还把代码片段解析成了一个个结构化的数据,可以方便我们后面的处理。 + +下面我们继续给这个类添加新的修改。假如我想让 MyComp 类继承一个名为 Base 的类,并实现两个接口:OnInit/OnDestroy,也就是我们预期将这行代码改为: + +export class MyComp extends Base implements OnInit, OnDestroy + + +其中,Base 类来自工程里的另一个文件 base.ts,OnInit/OnDestroy 类来自 Angular 的 npm 包。 + +采用同样的方法,你需要往多个插入点添加新的数据,先在 importSection 这个插入点里,添加如下 3 个数据: + +[ + {identifier: 'EventEmitter', from: '@angular/core'}, // 原来已有的 + {identifier: 'Base', from: './base'}, // 下面3个是新增的 + {identifier: 'OnInit', from: '@angular/core'}, + {identifier: 'OnDestroy', from: '@angular/core'} +] + + +再往 extendSection 里添加一个数据: + +[ + {identifier: 'Base'} +] + + +最后往 implementSection 里添加如下数据: + +[ + {identifier: 'OnInit'}, + {identifier: 'OnDestroy'} +] + + +其他的任何修改只是重复上述过程,所做的事情无非就是找到对应的插入点,然后把代码片段拆解为适当的结构化数据,再追加到该插入点列表中去。 + +你可能会有疑问:插入点里会不会有重复项,出现了重复项该怎么办? + +答案是插入点出现重复项是正常的。比如多个功能点需要 import 相同的类,那么 importSection 里就会出现多个重复的条目,在转为代码时,应该先将 importSection 里的重复条目过滤掉。 + +那么是不是所有插入点都要去重呢? + +不一定,不同的插入点处理方式不一样。比如成员变量插入点里,如果有两个条目的 modifier、identifier、type 三个属性都一样,就可以认为是重复条目,过滤掉就好了;如果这三者之一有不一样,那此时就应该报错,否则代码生成器可能会生出类似下面的代码来: + +public select: EventEmitter; +private select: EventEmitter; + + +显然,这样的代码是有错误的。再比如,构造函数插入点里的重复条目就不应该过滤掉,应该按照顺序依次生成代码块。 + +你可能还会有疑问:插入点里的顺序敏感吗? + +答案依然是视不同插入点而定。比如 import 插入点的顺序一般是不敏感的,生成代码时可以按照特定顺序排列,还可以将 from 值相同的合并到一起去。而一些输出是代码块的插入点(如构造函数插入点)则对顺序是敏感的,此时不应该随意调整顺序。 + +另一个可能的疑问是:在插入点里添加结构化的代码片段非常繁琐,手工不可能完成啊。 + +请注意,往插入点里插入代码片段的,是另外一段代码,而不是人工!而这里的“另一段代码”就是我们的代码生成器了。 + +我前面花了这么大篇幅详细说明了如何把一段我们习以为常的代码拆分,并散落不同的插入点里去的过程,实际上就是对代码生成器的逆向工程,这样说是为了让你更好地理解代码生成器是如何生成代码的。只有理解了代码生成器的工作原理之后,我们才能更容易地实现它。 + +计算机非常擅长读写结构化数据,因为结构化数据没有二义性。DSL 也好、自然语言也罢、甚至包括编程语言,都充斥着各种二义性,需要上下文才能准确解释,计算机很难轻易理解这种形式的数据。 + +因此,低代码平台基本都会采用结构化数据作为持久化的数据格式,而我在前文里则给出了一个如何将一组结构化数据转为代码的方法。虽然编辑器持久化采用的结构化数据,与代码生成器所需的插入点结构化数据不是严格对应的。但是两者之间的数据结构已经非常接近了,只需要做少量简单的转换,我们就可以将编辑器持久化采用的结构化数据转为插入点数据,然后再传给代码生成器。 + +代码生成器会先将各个插入点的数据做校验,一旦发现有冲突,就会报错。校验通过之后,代码生成器需要将各个插入点的结构化代码片段,按照该类插入点的语法拼装成一个代码块。之后,再按照语法要求的顺序将代码块拼装在一起,最终就得到了一大块代码,此时的代码才具有语义和功能,也可以被其他编译器编译了。 + +这个时候,你再看看最终生成出来的代码,会发现它与手写的代码非常接近,这样的代码是可以被人类理解和二次编辑的。但我们应该极力避免这样做,保持对这些代码的只读,因为任何对这些代码的编辑都难以反向同步到输入的结构化数据上。 + +HTML 模板的生成 + +插入点生成代码的过程基本都很简单,唯独组件的 HTML 模板的生成比较复杂,需要专门拎出来说明。 + +Web 组件集提供的组件可以分为两大类,一类是普通组件,另一类是容器。容器具有普通组件的所有特性,但与普通组件不同的是,容器可以将任何普通组件、容器装到它内部去,这样逻辑上就形成了一棵树。 + +我们不难理解,从树叶到树根的各个节点都是相互独立的。那么同样,编辑器持久化时也必须保持各个节点相互独立,这样的一个特性就给生成 HTML 模板代码造成了一些小麻烦。这是因为,每个节点只能知道自己能产生啥样的 HTML片段,而不知道其父级和子级的 HTML 片段,但是现在的目标是要生成视图的完整HTML。 + +我们可以通过这个例子来理解一下: + + + +单从结构来看,上面这段 HTML 代码是这样的结构: + +jigsaw-box + ├─ div.header + │ ├─ img.logo + │ ├─ icon.xxxx + │ └─ icon.yyyy + └─ jigsaw-tab + ├─ Tab1 + │ ├─ ... + │ └─ ... + └─ Tab2 + ├─ ... + └─ ... + + +这个例子里,有两个问题需要解决。一个是各个节点生成的 HTML 片段都不一样: + + +jigsaw-box 节点需要生成这样的片段; +div 节点需要生成这样的片段; +jigsaw-tab 需要生成下面这样更复杂的片段: + + + + + + + + +另一个问题是:需要把这些独立节点的 HTML 片段融合成一个整体。 + +如果你观察得足够仔细,就会发现 jigsaw-tab(页签组件)节点实际上是一个容器。每个 tab 页签内部完全有可能再放一个独立的页签组件,也就是它的内部完全可能包含另一棵类似这个例子的节点树。这样一来,这棵节点树就形成了一棵具有递归结构的树了。 + +最后还有一个重要的要求:为了保持代码有良好的封装内聚性,不允许采用 if else 的方式来解决节点之间的差异。 + +那怎么同时满足这三个要求呢?我采用的解决方法是,给每层数节点上都定义一个相同签名的函数(我们起名为 htmlCoder),每层节点的 htmlCoder 只干两件事情: + +第一,如果该节点有子级,则正确地组织好参数并调用其子级节点的 htmlCoder,驱动其子级生成 HTML 片段; + +第二,正确地生成好自己的 HTML 片段,该生成几层包裹层就就生成几层,并将子级返回的 HTML 片段与自身生成的 HTML 包裹片段,正确组装成一个合法的 HTML 片段,并返回给父级节点。 + +这段话虽然简洁但是比较绕。这里实际上是一个深度优先的递归调用过程,HTML 片段总是从最深处(树叶)开始真正组装完成,然后一级一级向树根递归出去。直到递归到树根时,一个完整的 HTML 代码也就生成好了。 + +用这个方法可以完美达成我预设的 3 个要求:节点与节点之间松耦合,没有 if else,且无论节点自身需要多复杂的 HTML 片段,都能满足。 + +其实如果你能熟练使用 OOP,大概现在就能猜到我在实现这部分代码的时候,会让描述节点的类都去实现一个包含 htmlCoder 这个函数的接口: + +export interface IHtmlCoder { + htmlCoder(sCode: StructuredCode, env: ProjectEnv): string; +} + + +这样做的好处,是在编译时就约束各个节点的代码必须要实现这个 htmlCoder 函数,并且函数的签名和返回值必须全部保持一致。 + +这样我们在运行时就不需要去判断某个节点实例是否有 htmlCoder 函数了。如果在代码长期维护过程发现 htmlCoder 的签名发生了变化,也不用担心哪些节点代码忘了对齐,因为编译器会不厌其烦地、一遍又一遍地检查所有节点的代码。 + +作为这个知识点的补充,你可以去找找我在 2021 年 12 月 GMTC 深圳站上关于低代码平台实现方法的演讲,在演讲的 17 分 36 秒到 20 分 43 秒,我从代码级详细介绍了这个知识点。这里我们就不再展开更多的细节了。 + +实际上采用相同的方法,可以把各个节点隔离得非常彻底,可以实现节点之间的极致松耦合,而 TypeScript 为我提供一个非常完善好用的 OOP 实现。在此我要顺道再安利一波 TypeScript! + +如何应用这个方法? + +那么低代码编辑器是如何使用这个方法来生成代码的呢? + +编辑器侧重于交互的易用性和人性化设计,显然不可能按照生成代码所需的顺序设计交互过程,这就导致编辑器和人工编码类似,会在不确定的时间和位置插入某些功能逻辑。结构化代码生产法可以很好地满足编辑器的这个需求。 + +简单一点说,在新建一个 App 的时候,编辑器会创建一个类似下面结构的空白数据: + +{ + importSection: [], constructorSection: [], memberSection: [], + ... +} + + +然后将这个对象传递给各个编辑流程(可能是一个表单,或是一个文本框),所有编辑流程在采集到开发人员的编辑数据后,按照该功能所需的逻辑往这个对象不同片区插入数据。最后,编辑器通过我们前面介绍的方法,就可以将这个结构化的数据转为代码了。 + +虽然我用了 TypeScript+Angular 为例,介绍了结构化代码生成法的原理和简要实现方式,但并没有将它和这两者的任何特性绑定,所以你使用相似的原理和思路,完全可以做出一个可以生成 JavaScript+Vue 的代码生成器,抑或是用于生成 JSX+React 的代码生成器。 + +我已经将这个方法推广到了生成 TypeScript+Nodejs 的后端 Rest 服务的代码生成中了,整个推广过程异常顺利,几乎没有做任何修改就直接切换过去了,丝滑得有点不真实。实际上,任何一门指令式的计算机语言,都是可以采用相似的方法来生成代码的,也就是说,只要你愿意,你可以使用这个方法来生成 Java 代码,甚至是 C/C++/Python 等后台语言的代码。 + +不过,由于我本人比较痛恨函数式编程,对这种编码方式理解得并不深刻,所以此时此刻,我无法下结论是否这个方法也适用于函数式编程语言,但以我对 Scala 语言肤浅的了解,我觉得问题也不大。如果你对这方面比较了解,也欢迎你在留言区说说你的看法,我们一起讨论。 + +总结 + +这一讲中,我采用逆向工程法从实际代码反推出了代码生成器的工作原理,并将复杂的人工编码过程抽象成了向有限种类插入点添加代码片段的过程,再在编辑器各处按需往插入点中添加所需的代码片段。这样,我们对收集到的代码片段做过滤、合并和优化之后,再按照一定的顺序重新组装,就可以获得一份完整的代码了,这份代码与手写的代码相似,对人类友好,具有可读性。 + +为了帮助你更好地理解结构化代码生成法,在最后我再拿它生成代码的过程与手写代码做一次比较,其实结构化代码生成法创造代码的过程和你我日常写代码的过程,是一样的。你可能会反驳:我平时编码是在 IDE 上直接敲入一行行代码,从没用一个数据结构替代代码,更没将它存到哪个数组里。 + +假设有一个偏执狂码农,他写代码的时候总是极其严格地、甚至比编译器还严格地按照特定的顺序写代码。比如严格遵守这样的顺序:所有 import 代码→所有 HTML 模板→类声明→类构造函数→类成员变量→类成员函数→…,即使编译器允许某些代码交织在一起,他也会偏执地保持同一类代码聚集在一起。 + +此外,他还有令人发指地要求同一个片区内,后插入的代码一定是追加在最后面。在这个情况下,每类代码所在片区实际上就是一个数组,而他在编写特定功能代码时,就是按需在各个片区里插入代码片段。现在,你是否会觉得这个偏执狂码农的编码过程和我给出的结构化代码生成法生成代码的过程极其相似呢? + +普通码农不会按照偏执狂码农的方式来编码,你我敲的代码不存在永不交织的代码片区,但对应的永不交织的逻辑片区是存在的,它们存在于你的逻辑思维中。所以我说,结构化代码生成法创造代码的过程和你我日常写代码的过程其实是一样的。至于结构化代码生成法是往代码片区里插入一个结构化数据,这只是为了在最后拼装成代码时可以更容易做优化而已。 + +也正是因为结构化代码生成法与人工写代码的过程实际上是一致的,最后我才能下这样的结论:结构化代码生成法几乎有无限的推广空间。 + +思考题 + +如果采用结构化代码生成法来生成 Java 或 C++ 代码,你会设计出哪些插入点? + +欢迎在评论区写下你的想法。我们下节课见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/08|布局编辑器:如何做到鱼和熊掌兼得?.md b/专栏/说透低代码/08|布局编辑器:如何做到鱼和熊掌兼得?.md new file mode 100644 index 0000000..71e500c --- /dev/null +++ b/专栏/说透低代码/08|布局编辑器:如何做到鱼和熊掌兼得?.md @@ -0,0 +1,197 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 08|布局编辑器:如何做到鱼和熊掌兼得? + 从这一节课开始,我们正式学习 App 开发三部曲相关的内容,这三部曲分别是布局、交互和数据。这是 App 开发过程的三个主要步骤,也是业务团队开发 App 的三个主要工作内容。在时序上,这三个步骤并非顺序执行,而是交织进行的。但布局多数出现在 App 生命周期的早中期,交互和数据则集中在中晚期。 + +所以今天我就先来说说三部曲中的布局篇。顾名思义,布局就是按照 UX 设计稿或需求说明书里的草图,把需要的组件逐个放到界面上,并按照要求排列整齐,形成 App 雏形的过程。 + +Pro Code 开发模式下的布局过程是极抽象的过程,开发人员需要把形象化的 UX 设计稿转换为一行行抽象的指令,同时在脑海里想象这些指令的渲染效果。而在低代码模式下,布局过程是非常形象的过程。我们可以利用低代码编辑器的布局器,通过画布上的拖拉拽,可视化地完成这一过程。而且,由于新手初次尝试低代码开发所做的事儿就是布局,所以拖拉拽往往成了大家对低代码模式的第一印象。 + +显然,布局过程非常机械,低代码平台应该有能力自动化这个过程。所以,在专栏的最后一讲里,我会给你简单介绍实现一个 D2C(Design to Code)的思路,实现低代码平台的自动化布局。 + +先别急着去翻最后一讲!D2C 再牛再酷,也只是辅助手段,从设计稿里自动识别出来的 App 布局,也需要微调,如果后面业务需求更新了,还需要手工维护布局。因此 D2C 并不能替代今天这讲的内容,请耐心学习这一讲。 + +鱼和熊掌 + +首先,我们要明确,不同类型的业务场景下的布局器会有很大差别。 + +比如,表单场景总体上是以行为单位,自上而下布局。而且一行之中同时包含多个元素:标签、编辑器、附加说明、出错提示,等等。基于这样的特点,我们可以设计出类似下面这样的布局器: + + + +这样的布局器针对表单场景来说,无疑是高效、易用的。但如果我们要用这个方式来布局一个 Dashboard 类 App,不仅毫无效率和易用可言,甚至连能否做到,都要打一个疑问号。因为,一个比较好的 Dashboard 场景布局器,应该是基于卡片的,要方便对大块的区域进行切分,从而快速获得尺寸合适的卡片,而且还要能方便地将小片区域融合成更大的卡片。 + +从前面举的两个例子可以看出,不同类型的业务场景的布局器确实会有很大差别。那么,有没有一种布局器能同时用于布局表单和 Dashboard 场景呢? + +有!基于绝对坐标的布局器就能胜任。这种布局器很容易就能实现通过拖动来改变物体位置和尺寸的功能,比如下面这样的效果: + + + +不过,这里要注意的是,布局器是基于绝对坐标实现的,但这并不意味着 App 在运行时无法获得具有弹性尺寸的页面。你可以先思考一下,我们后面就会给出解决思路。 + +好,现在问题来了。既然网格布局器(布局器背景有网格线辅助对齐,由此得名)可以同时实现表单和 Dashboard,那我们是否只实现网格布局器就好了呢? + +当然不是。我们在关注功能的同时,还需要同时关注另一个维度:效率。我们可以很明显地看出来,基于行的布局方式开发表单的效率要远高于网格布局器,同理,基于卡片的布局方式来开发 Dashboard 的效率也远高于网格布局器。从这个角度看过去,网格布局器毫无优势。 + +好了,我们现在可以总结一下这部分了。效率和通用是两个相互制约的维度,无法同时获得,两者的关系可以定性地用下图来体现: + + + +鱼和熊掌不可兼得 + +而且,效率和通用性不仅会在不同场景下相互制约,即使在同一个场景下,它们也会相互制约。我们拿 Dashboard 来举例,布局之初,页面有大片的空白区域,基于卡片的布局器可以快速地将空白区域切分为多个小卡片。 + +你可以看看下图,红线示意了卡片的切分过程,我们可以非常迅速地实现下面布局: + + + +但如何在一个卡片内进行精细布局呢?如果继续采用切分的方式是否依然高效呢? + +显然不是的。你看下上面这张图,随着布局越来越精细,一次操作可以影响到的界面面积越来越小。比如对一个卡片内部进行精细布局所需的操作次数,与对页面整体进行布局所需的次数可能还要更多。也就是说,对卡片内部的布局操作而言,相同布局操作的性价比就非常低了。 + +现在请你把目光聚焦在左下角那 3 个环形图上。对于这个区域,采用相同布局方式继续切分当然是可以做到的,但是效率并不高。下面这张图上,我把这个区域所需的布局操作用黄色线画出来了。你可以数一数,一共需要 9 次操作才能完成对这个小区域的布局,而在前面的图中,我们布局整个页面也才花了 8 次操作。 + + + +此时,如果我们换成网格布局或类似自由式布局方式,是否效率会更高一些呢?效率会更高,借助网格布局的快速对齐和空间自动分布工具,我们可以更快地完成这个布局。 + +因此,效率和通用性不仅在不同场景下有相互约束,在同一个场景的不同阶段,也有相互约束。那么面对二者选其一,我们是要效率,还是要通用性呢? + +我都要 + +有道是:只有小孩子才做选择,成年人当然是全都要。如何做到效率和通用全都要呢?这就是我们这部分要解决的主要问题。 + +根据前面的分析,我们得到了这样的结论:靠单一的布局器是无法同时获得效率和通用能力的。所以,我们很容易想到可以采用组合的方式,兼顾高效率和通用能力。 + +现在,我们继续以前面那个垃圾分类页面为例讨论一下。我们已经知道,卡片布局器和网格布局器都具有很好的通用性。但在布局初始阶段,显然采用卡片的方式效率高,而在布局的后期,使用网格布局器进行精细化布局的效率更高。那么,我们将这两种布局方式组合使用,就可以得到一个既高效又通用的布局器了。下面这张图就描述了这样的过程: + + + +现在,问题就简化成了如何实现多种布局器自由组合使用了。 + +为了更好地介绍具体的实现方法,我需要带你简要回顾一下【第 6 讲《踏出新手村便遭遇大 Boss:如何架构低代码的引擎?》】中的相关内容。在第 6 讲中,我将代码生成器与编辑器之间的关系分成了 4 个 Level,其中 Level 3 架构下,编辑器和代码生成器之间的关系变成了下面这样: + + + +这个阶段,代码生成器与编辑器之间没有直接耦合。它俩各自与编译器协议产生耦合,布局器是编辑器的一部分。在这个架构下,只要各种布局器能严格遵守编译器协议来实现,那代码生成器就可以识别各个布局器的布局输出。 + +简单地说,无论布局器之间有多大差异,只要遵守同一套编译器协议,它们就可以玩到一块去,从而实现自由组合。此时的架构图可以细化成下面这个样子: + + + +得益于编译器协议这层抽象,编辑器可以根据需要实现多种截然不同的布局器。并且,由于各个布局器都可以直接与代码生成器对接,所以从代码生成的角度来看,各个布局器之间已经是可以自由组合的了。这就好比是一片竹林,从地面上看有许许多多独立竹子,但他们的根是相同的。布局器就是地表一棵棵竹子,编译器协议就是它们的根。 + +相反地,如果你并未事先理清楚代码生成器与编辑器之间的关系,没有保持两者之间足够充分的松耦合关系,那此时此刻,你要做到任意布局器输出的数据都能和代码生成器无缝对接,就没那么容易了。 + +那这要怎么解决呢?一种原始做法是在代码生成器中采用 if-else 形式来处理不同布局器的差异。这当然是能走得通的,但问题是每多出一种布局器,判断的情况就会翻倍(指数增长),用不了三五种布局器,判断条件数量就将大到无法继续。相信我,这个过程将会相当痛苦。 + +现在,问题就进一步简化成了如何在编辑器的 UI 上实现各种布局器的相互嵌套使用的问题了。这样的问题,基本已经可以直接交给 UX 交互设计师和普通开发去处理了。实现方式也不止一种,这里我简要介绍一下我使用的方法,你可以参考一下。 + +我将布局器包装成了一种容器。容器也是一种组件,它具有组件的任何特性,但具备一个普通组件没有的能力:它能装得下其他组件或容器。容器支持相互嵌套的方式简直与布局器是天生一对。 + +最后,我们再以前面那个垃圾分类页面的布局过程为例,讨论一下多种布局器的配合使用过程。 + +首先上场的一般是卡片布局器,它可以快速地将页面横竖切分为多个卡片。在布局某个卡片内容时,开发人员可以根据卡片内容特征选择一种他认为更高效的方式来完成。他可以选择继续用卡片切分,也可以选择拖入一个网格布局器。 + +如果拖入了网格布局器,这个卡片的内容就变成了网格布局方式,可以发挥网格布局的优势实现高效精细化布局。同样地,如果该卡片的内容与表单更接近,他可以拖入一个表单容器,采用表单的方式进行布局。 + +可以看到,按页面特征挑选合适布局方式进行布局,可以发挥各种布局器的优势,又有效避免了它们的缺点,实现全程高效布局。鱼和熊掌你可以都要。 + +各种布局器 + +那现在,我们再来看看这几种常用的布局器特点、适用场景,以及关键实现思路。不过这里,我们先不深入介绍他们的实现细节,有机会我会再补充一下这方面的内容。 + +首先是网格布局器。 + +将网格布局器放第一个介绍,是因为它是我第一个做出来的布局器。第一个实现它是有原因的,因为它最主要特征就是通用。在不考虑效率的前提下,它可以在页面布局的各个阶段中用到,所以把它做出来之后,我们就可以一把搞定低代码编辑器的布局能力,非常划算。我推荐你也优先实现它。 + +在编辑态下,网格布局器采用绝对坐标来定位各个组件,所以非常容易实现鼠标点选、框选、拖动位置、拖动尺寸等基本编辑过程。当然,绝对坐标过于自由和精细,会导致很难实现画布上多组件的对齐,这点我们可以通过类似 PPT 那样的磁力线自动吸附来辅助解决。 + +虽然磁力线自动吸附很酷,但这不是最优解!主要有两方面原因:一是工作量,实际上这个功能还挺难实现的;但这不是关键,第二点才是关键否决票,UX 规范对组件间留白有严格规定,UX 小姑娘让你留 8px,你就不能留 7px 或者 9px。你想靠鼠标拖动来做到刚刚好 8px?别开玩笑了。为了解决这个问题,我们就需要开发出更加复杂的磁力线。 + +那最优解是啥呢?很简单,固定拖动步长。那么固定多少合适呢?我的选择是 8px。注意:一定要和 UX 团队取得一致意见,要求设计稿中,任何两个组件间的距离和尺寸必须是 8px 的整数倍。 + +其实,8px 不是协商来的,而是对已有设计稿分析后得出的,UX 团队也认为以 8px 作为基本单位约束所有 UI 是非常合适的。所以我们在画布上拖动组件位置和尺寸时,并不是丝滑的,而是以 8px 为单位一跳一跳的。这样做就省去了开发磁力线的麻烦,又可以很容易达成 UX 规范所需的留白要求,一举两得。 + +比如下图中的网格,就是 8×8px 的,网格布局器也由此得名: + + + +那是不是网格布局器只能生成绝对坐标的页面呢? + +显然不是的。这里就涉及一个复杂的分组算法。我们简要介绍一下,这个算法是根据平铺在画布上的各个组件的位置,对它们进行适当分组,同时对分出的各个组再进行相似的操作,直到无法再继续分组为止。这个时候我们就可以得到一颗有层级关系的树,之后我们再采用 Flex 布局就可以实现弹性了。如果要说清楚这个算法的实现,一讲内容可能都不够,所以我们只能留到以后再找机会补上了。 + +接下来是卡片布局器。 + +CSS 的盒子模型把二维 UI 做了极致的抽象。在 CSS 中,整个二维世界都是盒子组成的。无疑,这个抽象是成功的,但代价却是难用。 + +我在盒子模型的基础上做了一点点扩充,把盒子分成两种,一种是水平盒子,一种是垂直盒子: + + + +左边是水平盒子,右边是垂直盒子 + +布局规则极简单:水平盒子会将它的直接子级按照水平排列,垂直盒子则将其直接子级垂直排列。关键点是,水平和垂直盒子支持任意层级的自由组合。这样做后,不可思议的事情就这样发生了,两个极简单的盒子通过自由组合,居然可以产生无限种可能,从而可以用它们来描述整个二维 UI。 + +这里我们还是以前面垃圾分类页面来举例。我们先看下面的动画: + + + +我们来数一数这个界面包含了哪些盒子: + + +土色部分是一个垂直盒子,只有 1 个; +中间蓝色部分是水平盒子,有两个; +最里头红色部分是垂直盒子,有好多。 + + +是不是很简单? + +这个盒子布局的全部代码,都随着我们的低代码可视化组件集 Jigsaw一起开源在 GitHub 上了,如果你有兴趣的话,可以打开这里看看它的其他 demo。这里有两个 box 实现,一个是用于运行时的轻量化 box,一个是用于编辑时的 box,二者的实现差异非常大。 + + +帮忙点个星星哟! + + +然后是表单布局器。 + +我在【第 5 讲《基础设施 :启动低代码平台研发之前,你需要有什么家底?》】中将市面上的组件集的封装方式归为 3 类,分别是数据驱动型、模板驱动型、以及 CSS 样式模板。 + +如果要将表单封装成一个组件,那显然采用模板驱动方式封装会更合适。因为表单对 UI 定制的需求巨大,非常难以按照数据驱动的方式来封装。但在那一讲中我也说了,数据驱动型的组件更适合用在低代码平台上。 + +这个矛盾如何解决呢?难道我们硬着头皮按照数据驱动方式来封装表单吗? + +不需要,开源社区里已经有优秀的解决方案了。我这里给你两个推荐方案。首先,我在用的是ngx-formly,这是一个非常强大的数据驱动型实现,能满足我 99% 的需要(扣 1 分是因为它在复杂异步校验方面并不完美)。如果你恰好采用的是 Angular 技术栈,那么可以选它。Jigsaw 对应 ngx-formly 表单控件封装,我们也开源了,你可以看看。 + +另一个推荐的数据驱动表单的实现是阿里的Formily,支持 React 和 Vue,也是非常优秀的一个作品。 + +有了数据驱动型的表单的帮助,低代码编辑器只要采用可视化的方式生成一个复杂的数据对象,然后将这个数据对象喂给表单组件,就可以渲染出表单视图了。甚至,Formily还提供了可视化生成数据对象的设计器,你也可以考虑直接将它集成到你的低代码平台上,节约更多工作。 + +最后是流程编排器 + +这种布局器是特定场景下专用的,通用性不高,但和表单相似,在适用的场合下,会大幅提升布局效率。我主要将它用于流程审批类 App 的布局,简单地说就是开发一些流程审批类的 App 时,直接用它当做顶层布局器,然后各个流程环节配合使用表单等其他布局器,可以实现非常高效率的 App 开发效果。 + +同时,我们还可以将它用于数据编排场景(这不是 App)。服务端给的数据不见得都是合适前端使用的,多数情况下都需要做一定的编排组合、结构转换后才能给前端组件使用,这个过程就需要用到数据编排了。数据编排过程和流程编排非常相似。我在第 11 讲《业务数据:巧妇难为无米之炊,再好的 App 没有数据也是白搭》中会对这个过程有专门介绍。 + +具体实现方面,同样地,我并未自行实现编排功能,而是集成了社区里的开源软件X6,你可以参考一下。 + +小结 + +今天我详细介绍了 App 开发三部曲中的布局篇。我们知道,不同类型的 App,布局方式迥异,即使相同的 App 在不同开发阶段也有不同的布局需求。我认为布局器最主要需要满足两方面的诉求,一是通用性,二是效率。通用性是我们在低代码编辑器研发早期主要关注的维度,随着低代码编辑器越发成熟,对效率的追求就逐渐超越了对通用性的追求。 + +得益于低代码编辑器早期充分发展了通用能力,因此在它成熟之后,我们就可以把效率作为唯一的追求,将效率发挥到极致。为了做到这一点,我们甚至需要关注不同布局方式在同一个 App 的不同开发阶段下的效率表现,通过组合的方式来发挥不同编辑器的最大效率,并规避各自的短板。从而得到鱼和熊掌兼得的效果。 + +这一讲中,我还给你介绍了网格、卡片、表单、编排等 4 种布局器的特征和实现要点,同时也介绍了相关的开源资源。多种布局方式需要相互组合才能发挥各自的最大效用,我建议你将各个布局器当作容器来实现。容器天然具有多层级相互嵌套的特性,这使得各个布局器可以自由组合使用,应用团队可以按需、按 App 特征挑选合适的布局器来实现快速布局。 + +思考题 + +在你的场景中,网格、卡片、表单、编排哪种布局器的需求是最大的?除了这 4 种布局器之外,你还需要哪些布局器? + +欢迎在评论区留言。我们下一讲再见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/09|属性编辑器:如何解除Web组件属性与编辑器的耦合?.md b/专栏/说透低代码/09|属性编辑器:如何解除Web组件属性与编辑器的耦合?.md new file mode 100644 index 0000000..21042fe --- /dev/null +++ b/专栏/说透低代码/09|属性编辑器:如何解除Web组件属性与编辑器的耦合?.md @@ -0,0 +1,403 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 09|属性编辑器:如何解除Web组件属性与编辑器的耦合? + 今天这一讲,我会带你推开编译器协议层的大门,并在协议层中实现一个功能,这个功能将会解除低代码编辑器和组件之间的耦合。我这里放了一张架构图,当然今天我们会对这个图进行详细讲解,现在你只需要有个大概印象就可以了: + + + +在开始之前,我想请你思考一下这个问题:低代码编译器(指代码生成器)是怎么知道自己应该如何使用一个组件的呢? + +这个问题乍一想挺简单的,但是思考越深,你会发现它越难。因为我们人类是通过学习组件 API 的方式来使用组件的,但编译器没有智能,它能像人一样去学习组件的 API 吗?不仅如此,我们还希望编译器除了“学会”内置组件集的用法,还能“学会”外来的其他组件,这可能吗? + +教会编译器使用组件 + +图文是人与人之间传递知识最好的方式,就像这个专栏一样,我把我的知识以图文形式记录下来,你通过图文来学习。但图文对代码(编译器实质就是一串代码)是极不友好的,对代码友好的“教材”至少需要包含这些特征: + +是指令式的:即这个“教材”必须是指出“怎么做”,而不是“做成啥”这种描述性的。 + +那如何给编译器提供一份符合这些特征的“教材”呢? + +我们通过几个例子来逐步归纳。我在【第05 讲】讲低代码基础设施的时候,用到了一个表格的例子,我们以它为例。下面这些内容就是典型给人类阅读的 API 内容: + +// html + + +// script +const tableData = new TableData(); +tableData.header = ['列1', '列2', '列3', '列4']; +tableData.field = ['field1', 'field2', 'field3', 'field4']; +tableData.data = [ + ['cell11', 'cell12', 'cell13', 'cell14'], // 第1行 + ['cell21', 'cell22', 'cell23', 'cell24'], // 第2行 + ['cell31', 'cell32', 'cell33', 'cell34'], // 第3行 + ['cell41', 'cell42', 'cell43', 'cell44'], // 第4行 +]; + + +这份手册包含了这几个主要信息: + + +HTML 模板的写法; +表格通过 data 属性来接收输入数据; +如何在脚本中创建一个 TableData 对象。 + + +而且你要注意到,这份教材完全是描述性的,也就是这份教程是在告诉读者你要做成啥样。 + +如果把这份文档翻译成计算机教材,同时满足结构化和指令式,我们大概可以这样做: + +{ + html: function() { + return ``; + }, + script: function() { + return ` + const tableData = new TableData(); + tableData.header = ['列1', '列2', '列3', '列4']; + tableData.field = ['field1', 'field2', 'field3', 'field4']; + tableData.data = [ + ['cell11', 'cell12', 'cell13', 'cell14'], // 第1行 + ['cell21', 'cell22', 'cell23', 'cell24'], // 第2行 + ['cell31', 'cell32', 'cell33', 'cell34'], // 第3行 + ['cell41', 'cell42', 'cell43', 'cell44'], // 第4行 + ]; + `; + } +} + + +看起来开始有点样子了。但是这个“教材”所体现出的“表格通过 data 属性来接收输入数据”这个重要信息,好像有点问题,因为它在 HTML 函数里被写死了。写死 data 属性至少带来了两个问题: + + +表格之外的其他组件,不一定有 data 属性,如果没有 data 属性,则生成的代码就错啦; +表格的其他属性怎么办? + + +看来,“教材”里不仅要包含 HTML 如何生成的信息,还需要包含属性如何生成的信息。所以,我们要把属性从 HTML 里单独拎出来,让它成为一个独立的函数。于是,这份教材就被改成了这样(没有变化的部分我省略了,后面的代码块也是这样): + +{ + html: function() { + const properties = this.properties(); + return ``; + }, + properties: function() { + return `[data]="tableData"`; + }, + script: function() { + // ... + } +} + + +这个版本最大的改进,就在于引进了 properties 函数,用来实现组件属性的动态创建。 + +接下来,我们再把目光挪到 script 函数,是不是感觉越看越不对劲儿?script 函数里的内容,全都是在为 data 属性服务的啊。从内聚的角度来说,这段代码应该放 properties 函数更合适,但是这里直接挪过去了。也不对,properties 函数处理的是 HTML 的一部分,和脚本无关。 + +这里,script 函数的位置之所以有争议,是因为 properties 函数身兼数职,它既要告诉编译器如何生成组件的一部分 HTML,又要告诉编译器如何生成对应的脚本。所以,问题就出在了 properties 的形式上了,它不应该是一个函数,而应该将它升级为一个类。 + +要先说明一下,从这里开始,这一讲中剩下的示例脚本都是用 TypeScript 来编写的,因为 JavaScript 已经不够我用了。不过,如果你对 TypeScript 不熟悉没关系,只要有 ES6 基础,从字面上理解并加一点猜测,也是可以理解的。 + +class Property { + property() { + return `[data]="tableData"`; + } + script() { + // ... + } +} + + +注意,这里我命名 Property 类的时候,用了单数,而非复数。因为从语义上说,我只指望 Property 类处理好一个属性就可以了,多个属性的管理不是 Property 类的职责。 + +相应地,那我们把前面那个包含 html 函数的对象,也改造为 TypeScript 类吧。改造后的关键代码如下: + +class SVD { + properties: Property[]; + html() { + const prop = this.properties.map(p => p.property()).join(' '); + return ``; + } + script() { + return this.properties.map(p => p.script()).join('\n'); + } +} + + +我这里增加了一个叫 SVD 的类,它用作描述这份给编译器的“教材”的入口。SVD 有一个名为 properties 的属性,它是前面创建的 Property 类的数组。这样看起来就顺溜多了。 + +此时,你可能会问一个问题:这份“教材”中有些内容是写死的,比如 jigsaw-table 这样的 selector,data 这样的属性等。 + +是的,所以它还需要进一步改造,我们要让这些写死的内容彻底动态化。这个改进实际上非常简单,我们直接看改好后的“教材”就行了,它变成了下面这样。 + + +首先是 Property 类: + + +class Property { + name = ''; + value = ''; + member = 'tableData'; + property() { + return `[${this.name}]="${member}"`; + } + script() { + return ` + const ${member} = new TableData(); + ${member}.header = ${this.value.header}; + ${member}.field = ${this.value.field}; + ${member}.data = ${this.value.data}; + `; + } +} + + + +然后是 SVD 类: + + +class SVD { + properties: Property[]; + selector = ''; + html() { + const prop = this.properties.map(p => p.property()).join(' '); + return `<${this.selector} ${prop}>`; + } + script() { + // ... + } +} + + + +提示:为了便于你理解关键信息,代码省略了所有的错误处理和其他细节。 + + +现在“教材”有了,接下来我们就要说说编译器如何“学习”了。 + +估计你现在也看出来了,这里的“教材”就是 SVD 和 Property 等类的代码,那么相应地,“学习”就是学如何执行这些代码了: + +// 初始化2个类 +const dataProp = new Property(); +dataProp.name = 'data'; +dataProp.value = { + header: ['列1', '列2', '列3', '列4'], + field = ['field1', 'field2', 'field3', 'field4'], + data = [ + ['cell11', 'cell12', 'cell13', 'cell14'], // 第1行 + ['cell21', 'cell22', 'cell23', 'cell24'], // 第2行 + // ... + ] +} +const tableSvd = new SVD(); +tableSvd.properties = [dataProp]; +tableSvd.selector = 'jigsaw-table'; + +// 执行并得到代码 +const html = tableSvd.html(); // +const script = tableSvd.script(); // const tableData = new TableData... + + +你应该能看出来,这份“教材”在生成代码方面,实际上还比较粗糙,还有很多细节没解决,但作为示例来用是足够了。我们暂且不去纠结这些细节问题,而是继续从架构的角度讲演进和抽象。 + +不过,我接下来要讲的内容会用到 OOP(面向对象编程)思想,所以在开始之前,请确保你能比较准确地理解类的继承、覆盖、多态、接口和实现等概念。 + +到现在,这份“教材”中,表格只有一个 data 属性。显然表格不应该只有一个属性,现在我们假设表格有另一个名为 columns 的属性。这样一来,Property 类就会有问题了,因为很明显只用一个 Property 并不能很好地描述两个属性。一个非常合适的解决方案就是派生出 Property 的子类来处理多个属性,因此,Property 类可以改造成这样: + +abstract class Property { + // 这是一个抽象函数,只有声明,无法实现 + abstract script(); + + name = ''; + value = ''; + member = ''; + property() { + return `[${this.name}]="${member}"`; + } +} + +class DataProperty extends Property { + name = 'data'; + member = 'tableData'; + // 在子类中,script函数就知道如何实现了 + script() { + // ... + } +} + +class ColumnsProperty extends Property { + name = 'columns'; + member = 'tableColumns'; + // 各个子类需要生成不同的脚本 + script() { + return ` + const ${this.member} = []; + ... + `; + } +} + + +你可以看到,我把 Property 类改成了 abstract(抽象)的了。因为从基类的角度看,它完全不知道如何去实现 script 这个函数,只有在具体的子类中才知道如何实现这个函数。所以我们派生了一个 DataProperty 子类用来描述表格的 data 属性,同时又派生了一个 ColumnsProperty 子类用来描述表格的 columns 属性。同理,表格其他的使用属性,也可以采用相似的方式来解决。 + +那么相应地,我们调用的代码也需要做些调整了: + +const tableSvd = new SVD(); +tableSvd.selector = 'jigsaw-table'; +tableSvd.properties = [ + new DataProperty(), new ColumnsProperty(), new XxxProperty() +]; + + +经过多个版本的演进,目前来看,这份“教材”终于可以很好地教会编译器如何使用 jigsaw-table 组件了。而且,我们还顺便对 Property 类做了一个重要的改进,通过派生子类的方式解决了冗余代码问题。后面,我们也可以把类似方法用到对所有组件的“教学”上。 + +学会使用任何组件 + +不过,到现在,代码编译器也才学会了如何使用表格这一个组件,接下来我们要做的当然是教会它处理多个组件如何描述的问题了。 + +我们采用和 Property 相似的方法,把 SVD 作为基类,用来描述组件中相同部分的逻辑,而各个组件特性部分的逻辑则放到子类中去,改造后的代码为: + +abstract class SVD { + properties: Property[]; + selector = ''; + // 能在基类里实现的方法都尽量在基类提供实现,这样子类直接复用就好了 + html() { + // ... + } + // 和Property类似,基类里不知道如何实现的方法,都只声明,不实现 + abstract script(); +} + + +那么,表格的教材也应该相应地调整为: + +class JigsawTable extends SVD { + properties: Property[] = [ + new DataProperty(), new ColumnsProperty(), new XxxProperty() + ]; + selector = 'jigsaw-table'; + script() { + // ... + } +} + + +此时如果再来一个其他组件,比如 select 组件,我们的代码编译器也可以轻松支持了。 + +class JigsawSelect extends SVD { + properties: Property[] = [ + new DataProperty(), new ValueProperty() + ]; + selector = 'jigsaw-select'; + script() { + // ... + } +} + + +我们可以看到,教会编译器使用组件,只是我们对这份教材多次演进的一个次要目标,我们更主要的目的是从架构上调整教材,让它有一层抽象层和一层实现层。 + +基于这样的层次架构,新来的任何组件,只要实现了抽象层中的方法,以及按需覆盖抽象层里的已有方法之后,编译就可以学会如何使用它了。 + +不知道你有没有注意到,我所说的“任何组件”不仅包括了低代码内置组件集里的任何组件,更包括了其他未知来源的组件集里的组件。也就是说,以后业务团队要是问你,你的低代码平台能否不用内置组件集,而使用一套他们指定的组件集,你就可以很爽快地答复“没问题”了。这就是低代码平台通用能力的一种体现。 + +当然,完全拒绝使用内置组件集的情形并不多见,但业务团队要求补充少量业务组件到低代码平台中来是一种非常常见的情形。 + +业务团队往往会封装出若干非常适合他们内部使用的业务组件,虽然这些业务组件没啥通用性可言,但如果这些业务组件无法在低代码平台使用,就会大大降低他们的开发效率。再加上人人都有的敝帚自珍的情感,在这些团队中推广低代码平台的阻力将会巨大。反之,如果低代码编译器能通过某种方式学会使用这些业务组件,并将他们纳入到平台中使用,那推广起来就相对容易了。这样的情形在我推广 Awade 过程中,非常常见。 + +编译器 SDK + +好了,前面做了这么多铺垫,这里终于可以来说说一个重要的知识点,如何提取编译器 SDK 了。我们前面花了九牛二虎之力,从架构上把编译器的教材分成了两层,其中有一个是抽象层,我把代码拎到一起: + +abstract class Property { + // 这是一个抽象函数,只有声明,无法实现 + abstract script(): string; + + name: string = ''; + value: string = ''; + member: string = ''; + property(): string { + // ... + } +} +abstract class SVD { + // 和Property类似,基类里不知道如何实现的方法,都只声明,不实现 + abstract script(): string; + properties: Property[]; + selector: string = ''; + // 能在基类里实现的方法都尽量在基类提供实现,这样子类直接复用就好了 + html(): string { + // ... + } +} + + +这部分代码对任何已知和未知的组件来说,都是通用的。并且,任何要纳入到低代码平台的组件都需要继承这些基类,派生出描述具体某个组件如何使用的子类。 + +那么这些子类应该由谁来实现呢?全部由平台团队来实现吗?业务团队能否参与进来?为了能让业务团队也参与进来实现这些子类,我们应该将这些基类代码从平台的代码仓库里独立出来,以 npm 包的形式发布出去。这样一来,平台团队也好,业务团队也罢,都可以通过 npm 拿到这个包,从而继承包里的基类来实现子类的开发了。 + +没错,这样的一个 npm 包,就是低代码平台插件系统的 SDK 的一部分。这个包处于下图中蓝框中的“协议部分实现”这层中: + + + +现在,再来看这张图,你会有更深的理解。编译器协议是抽象的,因为它只有一部分实现(参考前文的 Property 和 SVD 这两个抽象类),但抽象的基类完整地描述了生成代码会用到的所有 API 的定义。基于这份完整的 API 描述,我们就可以写出代码生成器的所有代码了,这份 API 描述就是图中所说的“编译器协议”。 + +当然,此时的代码生成器是无法正确运行的,因为协议层中只实现了一部分函数,另一部分函数只有定义,没有实现。那么缺失的这部分实现由谁来补呢?这项工作可以由平台或业务团队任何一方来完成,平台团队负责实现内置组件,业务团队负责实现自定义组件。 + +此外,SDK 还需要提供一个组件“教材”工厂,用来登记和生产各个组件的教材。教材工厂的实现比较简单,是一个典型的工厂模式,你可以参考这段代码: + +// 实际开发时就不会用“教材”这样的称谓了,awade将这部分命名为metadata +class MetadataFactory { + // 用于登记组件教材,不熟悉TS的小伙伴,对Type这个类型可能理解略费劲 + // Type这个类型代表着一个组件类(而非该类的实例) + register(selector: string, component: Type): void; + // 用于获取组件教材类(而非组件实例),拿到组件类后,再new一下就可以得到实例 + getMetadata(selector: string, rawSvd: object): Type; +} + + +这里你应该注意到,这个工厂将组件的 selector 作为组件的身份证了。把“教材”,即所有子类(JigsawTable / JigsawSelect / DataProperty / ColumnsProperty)都聚到另一个 npm 包,并调用工厂的 register 函数挨个在工厂里登记。 + +不过,这里别忘了,我们还有一个组件集的 npm 包。把这些包都画到一个图上是这样的: + + + +我们可以看到,“教材”、编辑器、代码生成器等模块都是低代码平台正常运行不可获取的重要部分,但图中它们三者之间却没有直接的耦合关系,相互隔离。这样的代码层次分明,职责明确,是一种非常好的关系结构: + + +编辑器负责收集开发者的编排结果,并驱动代码生成器来生成代码; +代码生成器则根据编辑器收集到的原始数据,从工厂中获取“教材”的实例,然后执行 script 函数获得代码; +“教材”负责描述各个具体组件的代码应该如何生成。 + + +我们再看编辑器和组件(集)之间,你会发现它们之间没有任何关系,甚至连间接依赖都不存在。这非常违反直觉,是不是觉得很不可思议?编辑器的所见即所得功能,就是时时刻刻在动态渲染组件,但在代码的结构中,这两者之间却没有任何关系。 + +我们千方百计地把组件和低代码核心模块编辑器分开的,根本目的就是为了达到下面这个图的效果: + + + +这张图中,右边红框里的是业务团队自行定制的组件。你看,这不就是一个插件吗?左侧绿色框是内置组件集,它和右侧插件的结构是一致的,所以内置组件集的和插件提供的组件是平起平坐的,架构上,插件并不低人一等。 + +总结 + +今天这讲,我们通过代码实例的演进,非常详细地说明了如何一步步解除低代码编辑器、代码生成器和组件三者之间的耦合关系。 + +我们从一份给人类阅读的常见 API 手册出发,将它改造为具有结构化和指令式的特征的一份教材,这样做的目的是让教材更加适合计算机阅读。然后,针对组件的多个属性,我们抽取出了一个属性的基类 Property,把通用的代码提取到基类 Property 中。 + +虽然组件属性基类并不知道如何生成代码,但我们做了一个非常重要的动作:在基类中定义了一个抽象的 script 函数,这个抽象函数的最大作用在于规定了组件属性的子类应该如何完成代码的生成。由此,虽然我们并没有完整实现组件属性基类,但是它却完整地定义了属性代码生成的流程。 + +接下来,采用相同的方法,我们抽取出了 SVD 类作为组件的基类,并也定义了一个抽象函数,用于规定组件的子类该如何生成代码。 + +到此,这一讲设定的目标就已经完成了,编辑器可以在基本无耦合的前提下调用组件的“教材”来生成正确的代码。但是我们并没有停下脚步,而是将这个方法进一步演进,做出了这个专栏迄今的第一个插件:业务组件。低代码编辑器在和业务组件完全没有耦合的情况下,通过插件,就可以知道如何正确地生成和渲染业务组件。而且,在这讲中,我们也首次实现了一条允许业务团队对通用型低代码平台做定制的通道。 + +思考题 + +你所在的团队有哪些功能适合内置到低代码平台作为通用组件来实现,哪些功能适合以成插件的形式集成到低代码平台中去?为什么? + +欢迎在留言区写下你的想法。我们下一讲再见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/10可视化编程:如何有效降低App前后端逻辑开发的技能门槛?.md b/专栏/说透低代码/10可视化编程:如何有效降低App前后端逻辑开发的技能门槛?.md new file mode 100644 index 0000000..e12ede3 --- /dev/null +++ b/专栏/说透低代码/10可视化编程:如何有效降低App前后端逻辑开发的技能门槛?.md @@ -0,0 +1,213 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 10 可视化编程:如何有效降低App前后端逻辑开发的技能门槛? + 今天我们来聊聊低代码平台实现可视化开发过程中一个难点功能:可视化编程。可视化编程解决的是应用开发三部曲(布局、交互、数据)中的交互环节。 + +但这样说有点狭隘,如果低代码平台同时支持开发后端 Rest 服务,那可视化编程的方法可以完全复用到后端的 Rest 服务开发中,而不仅限于前端交互逻辑的开发。因此,这一讲的内容实际上同时覆盖了前后端的低代码实现,如果没有特别的说明,这讲的所有内容都适用于前后端低代码场合下使用。 + +在开始之前,我想请你想一想这个问题:编码难在哪? + +作为一个写了近 20 年代码的职业码农,面对这个问题,我的第一感觉是难点很多,数都数不清,但要列个一二三来,又觉得不好下手。仔细一想,编码就像艺术创作,比如绘画,虽然绘画有一定的套路,但从开始到最终完成,有着巨大可自由发挥的空间,而填满这些自由发挥空间的,只能是作者的经验。并且,决定一幅画是否有灵魂的,也只能是作者的经验。 + +编码何尝不是这样呢?大概套路是有的,但细到每一个函数、每一个类如何编写,则完全由开发人员的经验决定。专家写的代码不仅性能好,bug 少,而且可读性非常高,反之,缺乏经验的开发人员能按预期把功能跑通就不错了,哪还顾得上可读性或性能。那有没有一种方法,可以让新手也可以写出专家级的代码呢?这正是我们今天要解决的问题。 + +可视化逻辑编排 + +代码具有非常强的流程逻辑,所以可视化编程要解决的第一个问题就是如何进行逻辑流程的编排。 + +编码时的流程逻辑是通过一行行代码自上而下来体现,可视化逻辑编排需要对逻辑有不同的组织方式。一种比较常见的逻辑可视化组织方式是流程图,通过流程图的形式来表达一个逻辑过程是非常自然的想法。 + +比如下面这个流程图,描述了一个订单审批的过程,看上去逻辑是比较清晰的: + + + +你可以注意到,这个简单的流程图里包含了代码逻辑的三种基本控制结构:循环结构、选择结构、顺序结构,并且这三种结构在图中的呈现和融合都非常自然。关键是,BOHM & Jacopini 早在 1966 年就从理论上证明了,只要能同时支持这 3 种结构的流程,就可以表达任何复杂的程序逻辑。因此,至少在理论上我们不需要担心这个方式的可行性。 + +而且,你肯定听过,或者用过“一图胜千言”这句话,它指出了我们人类大脑对所处理的信息的“偏好”。人类的大脑和计算机不一样,人类对可视化的信息(比如一张图、一幅画)的处理效率要远高于其他信息(如文字)。实际上,人脑的 80% 功能都是用于处理视觉信息的,人们对接收视觉信息具有天生的敏感度。 + +所以,无论从计算机理论,还是从人脑认知的角度看,流程图式的逻辑编排是一种对大多数人都非常友好的方式,我们在实现可视化逻辑编排时,流程图式的逻辑编排是一种不错的备选。 + +接下来,再继续深入。我们日常写代码,有些逻辑是要复用的,我们常常使用函数和类来解决代码复用的问题。那么,流程图能搞得定吗? + +在展开这个知识点之前,我要先提醒一下,可视化逻辑编排,无需要复刻任何一种编码技巧。不仅如此,实际上,可视化编程只需要实现极少量的编码技巧就可以了。 + +为啥?因为低代码平台的可视化开发手段多种多样,可视化编程只是其中的一种方法。实际上,低代码编辑器的主要职能就是实现各式各样的可视化开发方式,而这门课的主要内容就是在介绍低代码编辑器。比如前一讲我详细介绍了 App 的布局,这也是一种可视化开发手段。这样应该好理解吧? + +那么,可视化编程必备的编码技巧是啥呢?我认为只需有函数定义和调用就足够了,连类都用不上。在低代码意义下的可视化编程,实际上只需要完成交互逻辑即可。 + +而一个 App 代码的架构、框架部分的代码是不需要低代码平台的使用人员来实现的,更无须通过可视化方式生成的这些代码,这部分的逻辑,低代码编译器会 100% 生成。通过可视化方式编排出来的逻辑,会被低代码编译器添加到它自动生成的 App 的框架代码中去。因此,需要低代码的使用者通过可视化编程方式开发的逻辑,是简单而少量的。 + +那么,我们收回来,采用流程图式的逻辑编排如何来定义和使用函数呢?我认为有两种方法:第一种是显式定义,这是比较比较通用的方法,直接使用一个独立的流程来定义函数的逻辑,并允许在另一个流程里调用;第二种是融合式,也就是将函数“藏”到流程图里。 + +方法一很好理解,实现起来也简单,不需要过多讨论,我们主要说说方法二。 + +你先想想,一个逻辑流程图能有几个入口和几个出口?一进一出?一进多出?这两种就是最普通的流程图了,对应类似下面这样的逻辑结构: + +function f() { + // ... + if (xxx) { + return 1; + } + // ... + if (yyy) { + return 2; + } + // ... + if (zzz) { + return 3; + } + // ... + return 4; +} + + +那如果是多进多出呢?又是啥情况? + +仔细一想,好像无法与任何代码逻辑结构对应上啊。其实,多进多出是有的,而且多进多出的流程同时把函数的定义和调用都描述出来了。似乎有点难理解?那我们先看看下面这个图: + + + +上面这个动画演示了一个多入口流程图。从上图可以看到,它包含了两个独立的工作流。你可以认为一个工作流就对应着一个事件回调函数,或者一个工作流对应着一个 rest 服务。在这个情况,两个工作流重叠部分实际上就是一个函数了。 + +是不是很哇塞?竟然还能有如此优雅自然的函数定义和复用方式! + +如果是一个完全没有编程经验的人来用方法一,你是不是还要费一番口舌才能让他理解啥是函数,并且还要求他必须养成先定义后使用的习惯?而采用方法二呢,函数在哪?在哪调用?既在图上,又不在图上,这种方式对人脑非常友好。这就是可视化编程的魔力。 + +但是!没有一种方法是完美无瑕的。流程图方式编排逻辑方法的最大问题在于,当逻辑复杂之后,它的可读性和可维护性会极大下降。 + +比如下面这个申请商标的流程,你可以先看看: + + + +你看懂了吗?反正我没看懂。 + +那么这个问题应该怎么解决呢?有两个方法。首先我们想到的显然是折叠和展开了。之所以图会复杂,就是因为展示了过多细节,只要我们把不必要的细节隐藏起来,问题就可缓解了。目标确实非常明确,但是如何折叠?如果框选一部分节点后,不一定能叠起来呢? + + + +你看,现在上图选中的 3 个节点,就无法折叠,但如果我们把下面那个 Resubmisstion 一起框起来,貌似这块就可以折叠了。那能折叠和不能折叠的特征是啥,它们怎么区分呢? + +很简单,就是数一下框住的所有节点有几进几出就好了。一进一出时就可以折叠,否则就不能折叠。你可以再看看下面框住的 5 个节点,一进一出,所以它们可以折叠。反之这 5 个框少了哪个,都不能折叠。 + + + +这里我留个小问题给你,如果不选择图中的 Upload 节点,那么此时被框住的 4 个节点一共是几进几出?欢迎在评论区留言。 + +把这 5 块折叠以后,它们就变成了一个子流程了,然后我们再把前文的 4 个节点也叠起来,此时的逻辑图是这样的,看起来就简洁多了: + + + +要提升复杂逻辑的可读性,其实还有另一种方法,就是采用树状形式来组织逻辑。比如,把例子中的流程图改写为树状形式,是这样的: + + + +显然,采用树状形式来组织逻辑没有流程图那么“酷”,但好处是不挑逻辑复杂度:几乎任何复杂度的逻辑采用树来呈现后,都差不多,而且折叠展开也非常方便。而且我们可以通过拖放树节点的方式,做到对已有逻辑的大调整,这是流程图无法比拟的。 + + + +那么这两种逻辑组织方式,是否只能非此即彼呢? + +不是的,满足一定条件时,两种方式是可以相互转换。比如,在选中的节点只有一个入口(出口数量不限)时,这两种逻辑组织方式是等价的,可以相互转换。 + +不过,到这里,我们都在讨论逻辑流程如何编排。接下来我们更进一步,请你把眼光聚焦到流程(或逻辑树)中的单个节点,我们来看看如何实现可视化开发单个节点的逻辑。 + +填空即开发 + +如果说,可视化逻辑编排是编码时的编辑器输入框,那么编排界面上的各个节点就是代码输入框里的一行行代码。当然,这句话说得不全对,一个节点不一定对应着一行代码,有可能对应着几行甚至更多的代码,这是节点与代码行之间表面的差异,它们的本质差异是:节点是功能层面的概念,而代码则是实现层面的概念。 + +为了更好理解,我们做一个类比:如果说代码是一个个原子,节点则是一个个的分子。高中化学告诉我们,分子都是由原子通过不同的排列顺序组合而成的,原子不具有化学性质,而分子是有化学性质的。一堆氧原子不能用于你呼吸,而两个氧原子结合在一起之后成了氧气,氧气是可以拿来呼吸的。 + +代码和节点的关系,与原子和分子的关系非常相似。单独一行代码可能没有任何业务意义,但是多行代码按一定顺序组合在一起,就可以形成特定功能。因此,一个节点是由一行或多行代码组成的,具有特定功能。所以我说代码是实现层面的概念,而基于代码组合而成的节点,则是功能层面上的概念。再延伸一点,如果非要在代码层面上为功能节点找一个位置,你可以把它当作一个函数或者一个类来看待。 + +理解了代码和节点的关系之后,设计单个节点的方法就明确了:找功能(而不是找代码)。比如创建订单功能,或是订单审批功能,或是发送电邮功能,等等,还有很多很多。这样看节点的数量就没完没了了。 + +节点的功能有大有小。功能越大,意味着使用的条件就越苛刻,所需的功能数量也就越多;反之,功能越小,单个功能的可用场景就越多,通过他们的组合就能形成新的功能,因此,功能节点的数量需求就越少。 + +那么,你一定会追问,功能多小算小呢?这个问题需要分通用和业务两个层面来回答。先说业务层面,这方面没有标准,只能根据实际业务情况而定。与业务无关的通用功能,则相对明确,我们来详细说说。 + +所谓的通用功能节点,指的是与编程语言、组件操作等紧密相关的那些功能。比如下面这个图基本就把大多数通用功能节点分类都列举出来了: + + + +你可以根据上面这个图为线索来设计你的通用功能节点。 + +接下来,我就以条件判断这个功能节点为例,说明功能节点如何实现。因为我们这一讲专注的是可视化编程,那么功能节点的使用当然也是可视化的了。我们可以把一个功能节点粗糙地理解为一个函数,类似的,功能也是有输入参数和返回值的。 + +我们先来看看输入条件可视化配置。 + +即使是一个条件判断功能,也是要有输入参数的,比如下面这个图,展示了条件判断功能的配置界面: + + + +是不是没想到一个看似简单的条件判断功能节点的输入条件,也能玩出这么多花样来?这样的设计主要有两方面考虑: + + +判断方法为“表达式”是为方便有少量编程技能的用户使用,直接写一个表达式可以避免啰嗦界面配置,注意这里我把“表达式”设置成了默认值; +其他判断方法,基本都是给无编程技能者使用的,它覆盖了 JavaScript 常见的条件判断方法。 + + +基本上,其他功能节点都可以采用类似方式来处理输入参数的选择。 + +接下来我们说说功能节点的结果可视化配置。 + +无论功能节点生成的代码有多少,总要通过它的结果对外产生影响,而且这个结果必须交到开发者手里,进而开发者可以将上一个功能节点的结果作为下一个功能节点的输入,从而最终形成一串逻辑。想象一下,逻辑编排出来的逻辑就像一根管子一样,数据就是管子里的水,水从管子的一头流向另一头,中间穿过一个个节点,每一个节点都对数据产生一点影响,最终所有影响叠加到一起,就是开发者所需的最终结果。 + +那么,如何可视化配置呢?其实超级简单,比如下图的最后一行,有两个输入框,它们都接受一个字符串作为变量名来接收功能节点的结果: + + + +与函数的返回值不同,我要求开发者直接提供变量名给我,动作节点对应的代码计算好了后,把结果直接赋值给变量,这样一个配置就可以解决许多问题。 + +好了,我们现在可以来解释一下这部分的小标题为啥是填空即开发了。每个功能节点都有输入条件和结果需要配置,因此我们为每个功能节点都定制了一个表单,接收开发者对当前功能节点的配置,这就是“填空”的过程。 + +那填空后呢?填好后,功能节点就根据开发者填写的内容,把代码正确生成出来,这便完成了 Pro Code 模式下的“开发”过程。我把这个过程简洁地概括为:填空即开发。 + +兜底策略 + +接下来,我们需要讨论一下兜底策略在可视化编程中的应用。 + +无论可视化编排多么强大,无论功能节点多么丰富,无论这两者多么易用、讨喜,但是你必须承认,它们有可能无法解决所有问题,或者在特定复杂场景下,它们的效率比不上 Pro Code 方式。这个时候,我们就需要启用兜底功能了。 + +先别慌,其实兜底功能实现起来非常简单。先回顾一下咱这个专栏所说的低代码的一个重要特点:它和 Pro Code 一样,也是基于代码来构建的应用的。这个特点在今天这讲的内容中有多处体现,比如功能节点在填空背后,实际是把代码生成出来了。 + +既然填空 = 代码,那是不是可以给开发者一个代码编辑器,让开发者自己把代码填上去,就能解决所有可能出现的问题呢? + +答案是肯定的!所以说兜底策略在这里的实现其实非常简单。我这里放了两张图,是我们低代码平台 Awade 现在用的方法,你可以参考一下: + + + + + +简单粗暴,但是好用! + +虽然功能实现确实比较简单,但写过代码的人都知道,一个 IDE 绝对不仅仅是能敲代码,能跑就可以的。这些功能可能还占不到 IDE 功能的 10%,其他的如智能提示、出错提示、编译错误等功能是非常影响编码体验和开发效率的。虽然我们这里只是一个代码输入框,但往大了说,你应该把它当作一个 WebIDE 来理解。 + +但是这个功能点的实现弹性是非常大的,你可以根据你手里的资源,决定要把这个代码编辑器做到哪个程度。 + +总结 + +可视化编程是可视化开发中的一个难点,总体可以分成两个主要功能点:可视化逻辑编排和功能节点。限于篇幅,这一讲我们没有从代码实现层面讨论具体如何实现,但是我已经从方案和特性层面将这两个功能点基本吃透了。后续在动态更新部分,我会从代码实现层面来说说这两个功能落地过程中的难点。 + +我介绍了两种可视化逻辑编排的方法,分别是流程图式和逻辑树式,这两种方式可以相互结合,而不是非此即彼。可视化逻辑编排的方式,不仅可以很好地解决程序逻辑的编排场景,实际上,这个方式还可以独立出来作为一个独立的 App 场景:流程审批场景。我们只要再加上泳道,事件等功能的话,再采用这讲给出的方法,几乎可以直接实现一个符合 BPMN 规范的场景。以后有机会我们展开讨论这个话题。 + +在功能节点配置界面上,开发者填的是数据,而代码是自动生成的。不仅如此,功能节点的实现靠的不是前后端的语言专家,就是业务专家,功能节点将这些专家的经验凝聚在了它生出的代码中。所以,即使是一个无技能者,只要他正确填写了表单,就可以得到专家级的代码。这就是低代码平台的能力! + +这一讲中我只详细讨论了通用型节点的实现,对业务功能型节点则是一带而过。其实,对于一个通用型低代码平台来说,其职责也主要在通用型节点的实现,至于业务功能型节点,我建议你统统移到插件中来定制。 + +这么做有两个原因,一是业务功能节点数量非常多,胡子眉毛一把抓地全放到平台上,会非常乱;二是恐怕平台团队不知道如何去实现业务功能节点,甚至连要哪些功能节点都不知道,这本就是业务团队才知晓的知识。所以平台团队应该与应用团队合作来开发插件,这样才能利用好业务专家的经验。 + +思考题 + + +你认为哪些功能节点是比较常用,需要第一优先级实现的?条件判断、数组操作、字符串操作,还有其他哪些呢? +有的功能节点是异步的,可视化逻辑编排如何自动处理异步功能节点?这是一个有点难度的问题,欢迎挑战一下,把你的方案留在评论区。 + + +下一节课我们将要讨论可视化编程中的高低代码混合开发模式,你可以做些准备。我们下节课见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/11|亦敌亦友:LowCode与ProCode混合使用怎样实现?.md b/专栏/说透低代码/11|亦敌亦友:LowCode与ProCode混合使用怎样实现?.md new file mode 100644 index 0000000..68d19ef --- /dev/null +++ b/专栏/说透低代码/11|亦敌亦友:LowCode与ProCode混合使用怎样实现?.md @@ -0,0 +1,210 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 11|亦敌亦友:Low Code与Pro Code混合使用怎样实现? + 今天我们来聊聊低代码平台中的纯代码,理一理这对欢喜冤家的恩怨情仇。 + +一般的低代码平台,总爱宣传自己开发过程需要编码的地方是多么多么少,甚至已经消灭了所有代码。久而久之,不免给人一种感觉,如果低代码平台上还有代码的存在,就会显得很失败。低代码是新欢,纯代码是旧爱,有了新欢,抛弃旧爱,这样可不好。 + +其实,把低代码平台上的纯编码开发模式视为洪水猛兽,大可不必。编码有编码的优势,毕竟可视化不是低代码的目的,高效率才是。如果高低代码的结合使用可以提升开发效率,哪怕是只能在特定条件下提升效率,那都可以纳入低代码平台的能力范围。 + +为啥还要编码? + +最开始,我们思考一个问题,既然可视化开发是低代码平台的特色和卖点,那为啥低代码平台还要提供纯编码入口呢? + +你还记得【第 3 讲】中我描绘的天花板级别的低代码平台吗?其中一条标准和低代码平台的用户有关。天花板级别的低代码平台需要能支持各种技能水平的人同时使用。 + +也就是说,高技能水平的开发者也是低代码平台的目标用户。在这部分用户专业领域范围内的内容,他们更想要采用直接编码的开发模式。除开对这个人群来说,直接编码与可视化开发模式相比的效率因素,一个可能的原因是他们需要有职业获得感,以及出自人性深处的那种“不羁放纵爱自由”的叛逆心理。 + +我们前面说过,可视化模式凝聚了各路专家的经验,这个经验约束了你必须先这样做,然后再那样做,才能获得更好的效率和更优质的 App。从另一个角度看,就是强制规定了我们开发的套路,你只能这么做。这对于领域小白来说,当然是很舒服的,只要照做就能得到最优解。但高技能水平者往往有自己的见解,期望按照自己的思路来实现,给他们一个代码编辑功能,让他们自由发挥是更好的解决方法。 + +当然,不见得所有的低代码平台都要做到天花板上去,那发展中,甚至是刚起步的低代码平台,是否就没有纯编码开发的要求了呢? + +当然不是。如果说天花板低代码平台的编码模式是锦上添花,为满足一部分人的需要而开放的能力,但对发展中的低代码平台来说,则是刚需,某些时候甚至是雪中送炭。发展中的低代码平台由于可视化开发能力不完善,难免出现搞不定的情况,这种情况往往就需要纯编码模式来兜底。 + +即使在低代码平台成熟之后,使用纯代码作为一种兜底策略,依然是一种非常好的选择。因为任何事情都逃脱不了二八原则,低代码的可视化模式再好,也只能适用于 80% 的场景。剩余的那 20% 边边角角的场景,如果硬上可视化模式,反而可能吃力不讨好,所以我们把那剩下的 20% 的场景留给纯代码来兜底,是一种很明智的选择。 + +因此,无论是在发展中的低代码平台还是已经比较成熟的低代码平台,可视化模式和纯代码模式的混合开发,都是有很明确的需要的。 + +说到采用纯代码方式来实现兜底策略,我在这里自爆一点我们低代码平台 Awade 的黑料。我在【第 7 讲】中介绍了 Awade 的结构化代码生成法,它有一个很重要特点,就是可以生成和人类手写的代码相似的、人能读得懂的代码。多数低代码平台并不会考虑生成对人类友好的代码,为啥 Awade 要这么做呢? + +这是因为在 MVP 的初期,我们内部对 Awade 提供的可视化模式的端到端交付能力还心存疑虑,我们也不确定它能不能搞得定。于是心想如果人能读得懂、改得了 Awade 生成的代码,那万一开发哪个 App 时 Awade 搞不定,起码我们还能把代码导出来,手工继续完成剩余部分,不至于烂尾。 + +后来的事实证明我们的担心完全是多余的,Awade 提供的可视化模式可以很好地应对各种 App 的开发需要。即使如此,它能生成对人友好的代码的特性,并没有浪费,这个特性让 Awade 可以实现高低代码模式的无缝切换,这一点我们等会儿还会再说。 + +高低代码如何混合开发? + +要注意,在我自爆的黑料中,我们原本打算使用的可视化模式搞不定时就人工接着搞的方式,实际上也是一种高低代码结合的方式(高代码指的也就是纯代码)。只不过,这是一种很无奈的、很被动的混合方式,甚至可以说是一块遮羞布。 + +那么,低代码平台如何优雅地实现高低代码混合开发呢? + +在前面几讲中,我已经多次提到,低代码平台的开发过程是有代码参与的,代码是低代码平台的副产品,所以在低代码平台上能否实现高低代码混合开发,从来就不是一个问题。真正的问题在于要不要开放纯代码接口,这一点我们前面已经给出结论了。另一个问题是如何实现混合开发,这正是我们现在要讨论的问题。 + +形形色色的表达式是高低代码混合最常见的一种方式了,因为这是可视化模式的主要盲区。表达式的作用,往往就是作为一个小功能点,而如果我们用可视化模式实现一个真假表达式这样的小功能点,就像是拿大炮打蚊子,性价比极低。 + +可视化模式更适用于实现连续性的、复杂的逻辑。一个实际的 App 会包含许多这样的大块逻辑,并且这些大块逻辑之间充满了缝隙,需要使用表达式来“粘连”在一起。比如下面这个场景就是一个很好的例证: + + + +这个图展示的是用可视化方式编排一个逻辑判断的功能。我们都知道,逻辑判断功能最关键的一个点是要判断真假,但是用可视化方式来编排出真假的结果,却并不容易。因此,这个例子中,我们直接就让应用开发人员填写一个表达式,这个表达式将用于计算出这个条件的真或假。这个例子给出的混合方式非常自然,即使是没有多少开发经验的应用,也能很快掌握。 + +还是同一个例子,我们来看看采用纯可视化的方式是如何来完成真假判断的: + + + +这张图是在判断一个数组是否为空(即长度是否为 0),可以看到,纯可视化方式在这里就显得啰嗦一些了,这样的一个表单最终只生成了 array.length == 0 这样一个表达式。 + +那么常见的表达式还有哪些呢?除了真假表达式外,还有三目运算、正则表达式、求值表达式、逻辑运算、数值计算等,很多,我就不一一列举了。 + +这些表达式都很适合与可视化模式混合使用,不仅可以作为可视化模式的有用补充,在生成代码时,往往还起到直接表达业务逻辑的关键作用。表达式与可视化混用是如此常见,以至于如果我不指出来,你也许都不会注意到这也是一种混用场景。 + +也许你会反驳:表达式不算是高代码。那接下来,我们就来说说有点“代码含量”的情形:代码块。 + +可视化编程是可视化模式中最难做好的一个功能点。在上一讲中,我给出了一种通过形象化的方式来表达和编排程序逻辑的方法,但是那种方式主要是给程序编程能力很弱的人使用的,一般能写点代码的人会更乐意直接编码。 + +我认为,代码块就是可视化编程功能中实现高低代码混合的最好方式。我这里说的代码块指的是若干行完整代码组成的、有特定功能的程序块,一个函数可以包含一个或者多个代码块。比如下面这个示例图中就包含了 3 个代码块: + + + +在一个完整逻辑流程中,可视化逻辑块和代码块可以按需地交织混合在一起,如下图: + + + +图中橙色块就是纯代码块,其他块则是可视化逻辑块。这种混合方式完全是由应用开发人员按需编排出来的:有可能是开发人员觉得某个功能使用可视化块效率更高;也有可能是他不知道如何写出符合预期的代码块,而不得不用可视化块;还有可能是没有合适的可视化块,而不得不使用代码块顶替;甚至有可能随编排时的心情状况随机挑选了一种方式。总之,可视化编排器需要能支持这样一种高低代码混合的方式。 + +也许你还要反驳:代码块的“代码含量”依然不足,不能算作高低代码混合。 + +那接下来,我们就说说纯度达到 24K 的高低代码混合方式。表达式也好,代码块也罢,都是在可视化模式为主导的开发流程中,使用少量代码作为辅助的方式,也难怪会被质疑这样的方式不是纯编码。 + +那“代码含量”纯度为 24K 的开发方式是啥呢?总不能和我前面自爆的 Awad 黑料一样,把 App 整个工程的代码都生出来,让应用开发导入到 VSCode 这样的 IDE 继续完成剩余的开发吧? + +揭开谜底之前,我先介绍一下我为切分 App 工程代码设定的粒度:模块。一个完整的 App 工程由多个模块组成: + + + +你可以从这张示意图中看到: + + +一个 App 工程至少包含一个模块,也就是图中最中间的启动模块,启动模块有且只有一个; +模块有多种不同的类型,而且各类型模块的数量按照 App 所需创建; +模块是 App 工程的唯一一种粒度。 + + +其实还有几点要注意,不过这几点与这讲内容关系不大,所以我没有画到这张示意图上,但作为模块的重要知识,我们略微扩展一下: + + +模块之间可以相互引用,一个模块可以引用多个其他模块,也可以被多个模块引用; +启动模块由框架直接引用; +模块是实现 SPA(单页面应用)、应用自定义组件等的重要入口。 + + +为了减少 App 开发的学习成本,我吝啬地只用了模块这一种粒度,它承载了切分 App 工程、实现 App 代码复用、做各种关键功能入口的职能。关于模块如何做到如此复杂的职能、但又以极轻量的面貌展示在 App 开发人员面前,如果你觉得有需要详细说明,可以在留言区留言,有需要的话我会用专门一讲来介绍。 + +好了,收回来。介绍了模块后,不需要我多说,你应该已经猜到了,我所说的“代码含量”纯度为 24K 的开发方式,就是按需将一个模块的开发方式从可视化方式,切换为纯代码模式: + + + +上面动画演示了将当前模块的开发模式,从可视化模式切换为纯编码模式的过程。通过这样的操作方式将一个模块彻底转为纯编码模式之后,App 开发人员就可以按照纯代码的方式,继续完成这个模块剩余的开发工作了。 + +由于这里我生成的代码是基于 Angular 的,因此 App 开发人员必须严格按照 Angular 的方式来编码。他可以按需创建新的 Angular 组件,或者引用已有的组件来完成他剩余的开发工作。这是一种非常纯粹的编码开发模式了。 + + +提示:如果你熟悉 Angular,你会发现我这里提及的“模块”这个概念和 Angular 里的“组件”的概念是对等的。 + + +特别需要注意的是,切换成纯编码模式这个动作只影响当前模块,其他模块依然保持原有开发模式。并且,通过纯编码开发的模块可以继续被其他模块引用,它也可以继续引用已有的模块。也就是说,从 App 整体来看,它的开发模式就出现了高低代码混合的局面。 + +高低代码如何混合编译? + +老生常谈,我再强调一遍,低代码平台的开发过程是有代码参与的。因此,如果低代码编译器能够生成对人类友好的代码的话(方法见【第 7 讲】),把一个模块切换为纯编码方式的难度并不大。难的是如何实现高低代码混合编译,进而实现高低代码所见即所得的效果。 + +接下来我们就来简要说说如何实现,限于篇幅,只能把思路说清楚,而无法给出代码级的指导,如果你实现过程碰到了什么问题,欢迎在留言区提问。 + +我在【第 6 讲】的“生成代码总体流程”的部分有提到,如果低代码平台实时渲染视图采用的是直接法,也就是直接生成浏览器能识别的代码并渲染出视图,实现高低代码混合下的所见即所得效果就会非常麻烦。反之,如果采用的是间接法,即先生成某种 MVVM 框架下的代码,再实时编译成浏览器能识别的代码并渲染视图的方式,实现所见即所得则会简单许多。 + + + +间接法下从代码到视图的流程 + +从实现的流程图上看,采用间接法时,高低代码混合模式的编译流程几乎是一致的,因此实现起来非常容易。这个流程图是以代码为视角来画,如果改从所见即所得效果的实现过程来看的话,这个图应该进一步细化为这样: + + + +这张图中指出了各个关键环节之间采用了哪个编译器来处理: + + +编译开始于用户配置数据 SVD,使用低代码平台的编译器 awadec 将 SVD 编译成 TypeSript 代码; +如果是纯编码模式,则不需要 awadec 的编译,手工敲出来的就是 TypeSript 代码; +接下来使用 tsc 编译器将 TypeSript 代码编译成 JavaScript 代码; +再调用 Angular 或者其他 MVVM 框架的 JiT 编译器将 JavaScript 代码编译成组件描述符; +然后交给 MVVM 框架的渲染器,它会生成 DOM,并驱动浏览器渲染成视图。 + + +其中第 3 步将 TypeSript 代码编译成 JavaScript 代码的这个过程,目前看只有你使用 Angular 才会有,虽然 Angular 也支持 JavaScript 风格的 API,但官方不建议使用。因此我建议你采纳这个建议,先生成 TypeSript 代码。 + +如果你选用的是 React,那虽然没有 TypeSript 转 JavaScript 这步,但要多出将 JSX 转成 JavaScript 的步骤。如果你选用了 Vue2 就不用将 TypeSript/JSX 转 JavaScript 了,但现在 Vue3 也拥抱了 JSX,又需要有这一步了。 + +之所以把这个过程写得这么详细,主要是因为,为达到所见即所得的效果,代码从编译到视图实时渲染的全过程都有非常高的性能要求。因此建议你和我一样,将整个编译过程全部前置到浏览器中完成。但这样做就需要摸索出如何在浏览器中完成这所有步骤的方法了。其中 TypeSript 转 JavaScript 的详细方法,我整理成了一个 PPT,放在这里。你需要的话,可以参考。 + +而从 JavaScript 代码一路到浏览器视图渲染之间的步骤,我这里就不详细展开了。这部分和 MVVM 框架选型有直接关系,我用的 Angular,考虑到在国内 Angular 是小众,因此我的经验不一定是你需要的。当然,你也可以在留言区留言,如果很多人都需要的话,我可以专门找一讲来说清楚,这部分的难度其实还挺高的。 + +这部分的最后,我直接回复一下一个我被问了无数次的问题:一个模块被转为纯编码模式后,还能再回退到可视化模式吗? + +答:不可能!可视化编程总是要有一些条条框框来约束的,而一旦转为纯编码模式后,等于放飞了思维,人的思维有多复杂,就有可能写出多复杂的代码来。因此一旦冲出了可视化编程设定的条条框框,就再也收不回去了。 + +追问:那有没有办法给出一些约束条件,在满足约束条件前提下可以再回退到可视化模式? + +答:目前我还没仔细去思考这个问题。如果给出很强的约束那当然是可以的,但人的行为是不可控的,因此不可能给出太强的约束条件,我们需要设定出一些对人类非常友好的、简洁的规则来。关键原因是,目前我们没有这样的需求,使用可视化模式 + 代码块已经可以完成绝大多数的开发需要。 + +如何提升编码体验? + +编码体验是纯代码模式下一个绕不过去的话题,这个话题往往是你好不容易走通了高低代码混合模式后,欣喜若狂之时的一盆冷水。 + +从高低代码混合的功能来说,编码体验不能算是一个问题,但在实际编码过程中,如果没有代码智能提醒、补齐、出错提醒、全局搜索、重构等功能,想象一下,这还算是在写代码吗?现代的编码 IDE(IntelliJ/WebStorm/VSCode)已经把纯代码开发开发人员的胃口给伺候得极其刁钻了。 + +在低代码平台上的高低代码混合开发,即使在编辑一个表达式,至少也就需要有智能提醒、补齐、出错提示了。如果使用了代码块的混合方式,那么这方面的需求比较强烈了。到了模块级的纯编码,那此时的编码体验的要求,就基本和 Web IDE 甚至和 Native IDE 相似了。难道要求我们在低代码平台上做一个 VSCode 出来吗? + +怎么办?我们分成两种情况来讨论。表达式和代码块属于比较轻量混合,我们放一起讨论,而模块级的纯编码混合方式作为另一个情况单独讨论。 + +表达式和代码块对编码体验的要求其实已经不低了,但还没到非常极致的情况,因此只要有一个足够强大,扩展性足够强的编辑器,应该就可以应付。 + +这方面我走过一些弯路,过程直接跳过,我直接告诉你结果。我最终选择了Monaco这个开源编辑器,即使你没听说过这个名字,你也有可能已经是它的深度用户了,它就是 VSCode 的底层编辑器。功能自不必说,主要是扩展性极强,可以玩出非常多的花样,所以我们引入 Monaco 编辑器用于应对表达式和代码块场景的编码体验就完全足够了。 + +不过模块级的纯编码混合方式对编码体验的要求就非常高了,基本和 IDE 在同一层级了。此时,有两种可能的选择,一是继续基于 Monaco 编辑器,为它加上各种功能和扩展,深入将它融入低代码平台。这样做的好处是集成度非常好,低代码平台提供了一站式的开发体验。代价也是显而易见,你需要投入大量的资源来扩展和定制 Monaco 编辑器,把资源投入在这个方面,是低代码平台所需的吗? + +另一个可能的选择是,把球踢给某个 IDE,比如 VSCode,我们需要的功能,这些 IDE 已经都有了,而且做得已经足够好了,我们只需要想办法利用这些 IDE 即可。 + +那如何做到平滑地复用呢?插件,任何现代 IDE 都支持插件。所以,我们可以基于某个 IDE 做个插件,用来从低代码平台过渡到 Native IDE 上。这个方式的成本比较低,具有很高的可行性,但缺点就是集成度不是很好。特别是在各个 App 开发人员的电脑上安装 Native IDE 和插件,有可能会有一些不必要的困难。 + +总结 + +今天这讲,我们详细讨论了低代码平台式的高低代码混合开发模式,“代码含量”从低到高分别是表达式、代码块、模块级。总体来说,高低代码混合的开发方式是低代码平台上非常有必要的能力,特别是需要对表达式、代码块这两种混合方式提供良好的支持。无论低代码平台处于哪个发展阶段,刚起步也好,已经成熟也罢,我认为低代码平台对表达式和代码块的混合的支持都是必须的。 + +表达式和代码块这两种高代码开发方式,在低代码平台上的实现方式是相似的,都是以可视化的低代码开发流程为主。在可视化方式的不可达或者友好的位置,使用表达式或者代码块来填补,两种方式可以非常好、非常自然地融合在一起。而模块级的编码模式,则是彻底丢弃可视化模式,转为纯编码开发方式,尽管如此,在纯编码开发的模块也可以和可视化开发的模块混合使用。 + +在完成了高低代码融合之后,编码体验是我们不得不面对的一个问题。好的编码体验可以提供更高的开发效率,减少试错的次数和避免部分低级问题。但要获得好的编码体验并不容易,虽然我们引入 Monaco 这样的编辑器,可以利用其强大的基础功能和扩展能力来有效提升编码体验,但是需要有一定的扩展的工作量。 + +我建议你使用 Monaco 编辑器来解决表达式和代码块的混合。而模块级的纯编码模式,虽然 Monaco 编辑器也能搞得定,但是扩展工作量会很大,还不如牺牲一些集成度,给 VSCode 等 Native IDE 做插件,从而最大化利用这些 Native IDE 的已有能力。 + +虽然这讲到这里的篇幅已经非常大了,但是这个话题依然还有许多必要的内容没能讲到。包括: + + +高代码模式下如何处理存量代码? +如何扩展 Monaco 编辑? +如何开发 VSCode 插件? +如何处理好第三方依赖? + + +后续等有机会我们再翻出来说清楚这些内容。 + +思考题 + +你认为提供模块级的纯代码开发模式有多大的必要性?请结合你的场景聊聊你是怎么看的。 + +欢迎你在留言区写下你的想法,下一讲我们将会来讨论 App 开发过程中数据配置的问题。我们下一讲再见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/12业务数据:再好的App,没有数据也是白搭.md b/专栏/说透低代码/12业务数据:再好的App,没有数据也是白搭.md new file mode 100644 index 0000000..55c3b87 --- /dev/null +++ b/专栏/说透低代码/12业务数据:再好的App,没有数据也是白搭.md @@ -0,0 +1,239 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 12 业务数据:再好的App,没有数据也是白搭 + 今天我们来说说 App 开发过程中获取数据的配置。 + +数据配置是应用开发三部曲(布局、交互、数据)中的第三个环节,根据 App 的不同,它与数据之间的关系也不同:有的 App 可以产生数据(信息采集类);有的 App 则是数据消费者,或者兼而有之。数据采集 + 推送,包括文件上传的方式总体来说都比较简单,不在今天的讨论范围内,这一讲我们主要讨论组件如何获取和渲染数据。 + +而且,由于我们这个专栏所说的低代码平台生成的 App 都是 B/S 架构的,App 首选的获取数据方式当然是 HTTP 通道,实际上,即使是 C/S 架构的 App,HTTP 通道也依然是一个非常好的选项。所以,这一讲我们就只讨论通过 HTTP 通道来获取数据的情况。 + +请求参数、数据结构修正、数据模型 + +我们先来讨论数据获取的最基本动作,从请求发出去到数据展示到 UI 上,全程会涉及参数设置、返回的数据结构修正、数据模型映射等几个主要环节。 + +你要注意,这几个环节不包含获取数据的异常处理流程。异常处理是相对简单的一部分,只要别忘了在配置界面上增加对应的出错处理配置,生成的代码注意捕获 HTTP 异常即可。 + +第一个基本动作是 HTTP 请求的参数配置。HTTP 协议允许我们在多个不同的位置设定参数,可能传参的位置至少有三处:通过 url 传参、通过请求头传参,通过请求 body 传参。你在设计参数配置界面的时候,别忘了要给这 3 个可能传参的位置留出配置界面。 + +其中 url 传参这块是很容易被忽略的。比如下图的配置界面中,很容易把 url 输入框作为静态文本输入: + + + +这样的话,应用开发要通过类似下面这样的 url 传参,就不行了: + +# $v1 和 $v2 都是变量 +/some/data/url?p1=$v1&p2=$v2 +/some/data/p1/$v1/p2/$v2 + + +我的解决方法是,支持类似模板字符串的语法,即在 url 输入框中填写这样的 url,表示包含变量: + +/some/data/url?p1=${v1}&p2=${v2} +/some/data/p1/${v1}/p2/${v2} + + +其次,url 传参还有一个容易被忽略的问题:url 编码。 + +比如前面例子中的 v1 或者 v2 变量,如果运行时,变量值包含敏感字符如“&”,或者包含汉字,此时拼出的 url 会出错,导致请求失败。解决方法也很简单,我们只需要解析应用开发给的 url,自动添加编码函数,即在实际生成的代码中,自动把应用开发填写的 url 自动处理为: + +/data/url?p1=${encodeURIComponent(v1)}&p2=${encodeURIComponent(v2)} +/data/p1/${encodeURIComponent(v1)}/p2/${encodeURIComponent(v2)} + + +不过,在处理时,我们也不能无脑处理,因为有的应用开发有这方面的编码经验,有可能他填进来的 url 就已经有包含了 encodeURIComponent 的调用了,此时如果我们再编码就错了。真是操碎了心,有没有! + +而且,我们通过请求头传参时也要注意参数值的编码,但通过 body 传参就不需要了,HTTP 传输层会自动编码。 + +配置了正确的参数之后,数据应该就可以拿到手了。如果服务端给的数据结构不符合预期,我们还需要对数据做加工,又或者,如果你打算把拿到的数据做可视化渲染,比如渲染成各种图形,则还需要做数据模型映射。接下来我们将这两个动作放一起考虑。 + +前端组件普遍对输入的数据的结构有预设。有的要求输入的数据必须是一个一维数组,有的则要求具有特定结构,比如Jigsaw的表格要求输入这样结构的数据: + +{ + header: [ "Column1", "Column2", "Column3" ], + field: [ "field1", "field2", "field3" ], + data: [ + [ "cell11", "cell12", "cell13" ], //row1 + [ "cell21", "cell22", "cell23" ], //row2 + [ "cell31", "cell32", "cell33" ] //row3 + ] +} + + +通用性较高的低代码平台对接的服务端往往是无法事先预知的,所以对方返回的数据结构也是无法预知的。这要求在前端收到数据之后,要有数据结构如何做转换的配置。数据结构修正尽量自动完成,这样可以减少应用开发的配置工作量,以及降低应用开发的难度。一个比较好的方法是,尽可能收集各种可能的输入数据结构,然后根据特定输入结构,设定参数,从而可以达到自动生成转换代码的目的。 + +我这里给出两种比较常见的输入数据结构,分别是二维表结构以及准二维表结构。虽然现在服务端持久化数据的方式多种多样,但大多数还是采用 RMDB 来存储,所以服务端从数据库中读取到的原始数据,多数就是二维表结构的: + +[ + ['v11', 'v12', 'v13', 'v14', 'v15'], + ['v21', 'v22', 'v23', 'v24', 'v25'], + ['v31', 'v32', 'v33', 'v34', 'v35'], + ['v41', 'v42', 'v43', 'v44', 'v45'], + ['v51', 'v52', 'v53', 'v54', 'v55'], +] + + +另一种是二维表的变体,如果服务端用的是 node.js 实现的,很可能会返回这样的数据结构: + +[ + {f1: 'v11', f2: 'v12', f3: 'v13', f4: 'v14', f5: 'v15'}, + {f1: 'v21', f2: 'v22', f3: 'v23', f4: 'v24', f5: 'v25'}, + {f1: 'v31', f2: 'v32', f3: 'v33', f4: 'v34', f5: 'v35'}, + {f1: 'v41', f2: 'v42', f3: 'v43', f4: 'v44', f5: 'v45'}, +] + + +当然,这里还需要有兜底方法,用于处理预设类型之外的其他情况。但在这个情况下,只能编写数据转换逻辑了。我们可以引入第 10 讲的方法,通过可视化编程方式来编排转换逻辑,也可以直接给一个编辑器,让应用开发填写转换逻辑。两种方式的流程都是一样的,都是给出一个原始数据,要求应用返回一个处理后的数据: + +origin => { + // 把你的处理逻辑放在这里 + const result = ...; + return result; +} + + +如果说数据结构是对数据的一种逻辑表达,那么数据模型则就是对数据的一种抽象化描述。 + +数据建模往往与业务强相关,根据特定的业务模型来对数据做抽象和归类,这也就导致了不同业务可能会使用不同的模型来描述数据。我这里给出的是我们用于描述电信领域相关业务,特别是运营商大数据相关业务的数据模型,多年的应用表明在这个业务下有非常好的表现。实际上,这个模型是可以推广到所有采用 RMDB 来持久化的关系数据的。 + + + +这个模型把数据库表的所有字段,分为维度和指标两类。维度是描述一个事物的各个实体,比如省市区这 3 个维度可以用于行政区,再比如手机厂商、手机型号也是维度,以此类推。 + +指标就更好理解了,绝大多数的指标字段都是数值型的,比如今天的温度、这篇文章的字数,以及新冠确诊人数等。有的指标是可枚举的值,虽然不是数值型的,但也是指标,比如考核等级 S/A/C。时间是一个比较容易混淆的字段,它看起来是数值型的,但我们将时间作为维度来对待。 + +在对数据可视化渲染时,数据模型可以帮助低代码平台大幅降低数据可视化配置界面的复杂度,也可以让数据可视化配置过程更加具有业务含义,提高配置效率,减少试错次数。所以,在配置应答数据的结构时,我们还要把这笔数据用作可视化渲染,还必须要求应用开发填写必要的数据模型信息,越详细越好。 + +比如下面这个表格,就是描述一份天气数据的模型: + + + +数据打桩 + +接下来,我们再谈谈另一种获取数据的方式,也就是在开发态下获取数据的各种骚操作,我们可以偷,可以抢,甚至可以造假。 + +App 在开发时,一个非常普遍的情况是,它的数据还没准备好,或者不在当前开发环境下。总之,就是没有数据可用,那低代码平台应该如何帮助 App 开发者解决这个问题呢? + +数据打桩就是解决这个问题的功能。 + +数据打桩的基本实现是给一笔假数据作为模拟。实现的方法非常简单,就是给 XHR 请求加一个拦截器,通过 url 等筛选出需要模拟的 rest 服务,然后直接造假。这样甚至可以做到在不实际发出 XHR 请求的前提下,实现数据打桩。 + +更进一步,你可以让应用开发配置一定的规则,比如参数 A 的值是 a1 时,返回数据 1,值是 a2 时,返回数据 2,甚至直接让应用填写模拟的代码,这样理论上可以 100% 模拟服务端。采取哪种方式,取决于你需要模拟到啥程度。 + +那么,我们如何加 XHR 拦截器呢?其实多数前端框架都有解决方案了: + +如果你使用的是 VUE/React,一般会使用到 axios 来处理 XHR 请求,axios 有 interceptors 属性,可以用于你添加拦截器; + + +如果你用的是 Angular,那可以直接使用 HttpClient 提供的拦截功能; +如果你使用的是 jQuery,可以通过 ajaxSetup 这个函数来设置拦截器。 +如果你啥框架都没用,那有没有办法拦截呢? + + +当然也是有的,你可以直接对浏览器原生的 XmlHttpRequest 打补丁,下面这段代码演示了如何换掉 XmlHttpRequest 的 send 函数: + +const originSend = XMLHttpRequest.prototype.send; +XMLHttpRequest.prototype.send = function(body) { + var info="send data\r\n"+body; + alert(info); + originSend.call(this, body); +}; + + +我们的低代码平台 Awade 为了让打桩代码更加真实,就没有采用上述任何一种方法,而是在直接把桩代码生成到服务端,这样前端就不需要做任何处理了,只需要把 url 重定向到 Awade 生成的桩代码即可。 + +不过,仅仅有数据模拟是不够的,虽然我们可以把服务端模拟得很像,但是会多出很多无价值的配置。因此,在 App 开发的中后期,直接模拟的方式会被抛弃,改用把请求重定向到真实服务端去。此时所谓“真实”服务器,其实是指某位后端开发人员办公电脑,或者是另一个低代码服务器实例。 + +这里有个背景需要注意,我们鼓励应用团队自行部署 Awade 私服,而不是都集中到 Awade 官服上开发,主要是因为官服容量不够,所以在中兴内部有许多的 Awade 私服存在。这就造成了同一个 App 可能分开到两个私服上开发。 + +使用 web 服务器(如 Nginx)的反向代理功能来实现服务重定向,是非常容易的,但会有一个棘手的问题,就是往往低代码平台只有一个 web 服务器进程,不同应用需要重定向到不同的服务器,即使无视转发规则冲突的问题,但让转发规则生效时的 reload 操作也是无法容忍的。因为 reload web 服务器的配置会导致进行中的其他请求瞬断,这是无法接受的。 + +那可否不经过服务端,绕一圈,直接从低代码平台服务器一个请求到目标服务器上去读取数据呢? + +在解决了跨域问题的前提下,是可以的。本来,跨域问题并不难解决,在服务端配置一下 CORS 策略就可以轻松解决。但这隐含了一个前提,就是你要先知道有哪些服务端,对吧?问题就在于你有可能不知道有哪些服务端!比如我们的低代码平台 Awade 是一个开放性的平台,在它上面开发的 App 的数据来源自然是无法事先预知的。这个情况下,跨域问题似乎是无解的。 + +曾经,我也是这么认为的。不过,在走了一段弯路之后,终于找到了一个方法,可以在不配置服务端 CORS 策略的前提下,巧妙地“骗”过浏览器,绕过跨域限制,做到在浏览器中可以跨域请求任何服务器数据的效果。 + +这个方法有点绕,我会在这门课的动态更新部分,尝试只用一讲把这个方法说清楚。无视跨域约束地请求任何服务器数据,是通用型低代码平台的一个很有意义的能力。 + +个性化数据 + +前面我们详细讨论了低代码平台通过可视化方式获取数据的方法、数据模型、模拟数据等,这些都是着眼于通用的获取数据和处理数据的方法。接下来,我们再来说说如何处理个性化数据。个性化数据的通用性低,一般只适用于某个特定的业务场景,这就造成了个性化数据的获取方式形式多,数量大。 + +插件机制是低代码平台处理这种情况的最佳选择。 + +得益于个性化数据只为特定业务设计的特点,我们可以采用比通用数据更加灵活的方式来获取数据、提取模型和模拟。一个插件只专注处理好一种个性化数据就行,不需要考虑除此之外的其他场景,因而插件的实现难度低、效率高。 + +接下来,我们再看看采用啥架构可以让低代码平台支持通过插件的定制,获取个性化数据。思路和【第 9 讲】类似,因此为了更好的学习效果,你可以回顾一下【第 9 讲】看看我们是如何做出第一个插件的。部分相似思路这里我就简略带过,不再和【第 9 讲】那样详细说明了。 + +首先,我们把获取数据看作是一个动作。 + +说到这里,可能你已经想到了,无论是获取通用化的数据也好,个性化数据也罢,都是获取数据动作的一种具体实现。此时我们可以画出这样的一个图来: + + + +基础动作里包含的是两种动作的共同部分。都有哪些呢?至少包含这几部分内容,一是信息采集,一是信息保存,一是代码生成。 + +信息采集,就是要定义一个收集开发者配置信息的视图。获取数据的各个动作,需要采集的信息都大不相同,不同的个性化数据需要采集的信息也各不一样。因此,在基础动作中,这部分是抽象的,我们无法知晓具体该绘制啥样的 UI,但可以约束具体动作采用什么方法来绘制 UI,比如你可以要求动作子类采用 jQuery 的方式,或采用 MVVM 框架的动态渲染器的方式。 + +我采用的是后者,子类可以将处理 UI 的所有逻辑,都封装到动态渲染器中。并且得益于 TypeScript 特性,我还可以为各种渲染器组件提供一个父类,以减少子类开发时的难度和编码量。此时渲染器与基础动作之间会形成下图这样的关系: + + + +信息保存是可以在基础动作中直接实现的,只需要在基类中提供读写数据的 API 给子类使用即可。注意,在基础动作中不能定义一个具体的配置数据结构,这个结构必须由子类自行定义。在动作子类的渲染器中,我们就可以使用基础动作提供的配置数据读写 API,将 UI 上采集到的数据统一存到基础动作中去了。 + +你可能会有这样的担忧:既然配置数据存储的结构不能由基础动作决定,那在生成代码的时候,基础动作无法知晓配置数据的结构,自然就无法生成代码了。 + +其实,这个担忧是不存在的。和【第 9 讲】使用的方法一样,基础动作只能定义代码生成的过程,但是它无法定义具体如何生成代码,这对基础动作来说是抽象的,只能在动作子类中实现。这个过程我这里就一带而过了,如果你还是无法理解,请回顾一下【第 9 讲】,或者在评论区提问。 + +最后,把基础动作的代码合并到下图的编译器协议中,成为 SDK 的一部分。 + + + +通用获取数据的动作,作为基础动作的一种默认实现,由低代码平台提供官方的实现,个性化获取数据动作则作为一个新的插件,由业务团队实现。此时的架构图如下: + + + +当然,并不是说个性化获取数据动作只能由业务团队来实现,实际上低代码平台团队也可以提供一些个性化数据动作的插件实现。 + +一般在低代码平台推广初期,平台团队为了减少在业务团队中推广的阻力,会主动将业务团队常用的获取数据方式做成插件给他们使用,从而解决低代码平台与业务团队存量系统的对接问题。可以看到,在这个过程中, 插件起到一种类似胶水的作用,它可以很好地将低代码平台与存量系统粘在一起。 + +下面,我们再看一个实例,对比下通用和个性化获取数据的差异。对于同一笔数据,如果它在低代码平台中只能采用通用化方式来获取,我们可以看到参数部分非常复杂,要正确填写这样的参数是不容易的: + + + +但在业务团队,却可以在插件中,开发出这样的定制化 UI 来获取同一笔数据: + + + +上面这段动画操作完成之后,插件会自动生成第一个图中的参数。采用这样的形式来获取数据谁不爱呢? + +当然,除了采用插件化来定制个性化数据之外,我们还可以用其他形式获取个性化数据,比如可以采用 DAG(Directed Acyclic Graph,有向无环图)的形式来对数据做可视化编排。 + +不过,这讲说到现在,我们都是假设数据一次性就可以拿到手,但有时候实际情况并非如此。比如 Awade 曾经处理过这样一个业务需求:一个趋势图展示某个指标,它的数据有一部分来自历史数据,一部分来自实时数据。 + +对大数据有了解的小伙伴应该都知道,由于数据量过大,任何大数据系统都会把历史数据和实时数据分开,采用完全不同的方式来处理,而客户要求在一个趋势图上显示这两种数据,这就需要在后台分别读取两种数据之后,将其拼在一起。这个情况下,我们就需要用上可视化数据编排来获取深度定制的个性化数据了。 + +这讲我们就不展开可视化数据编排的具体实现了,我找机会再专门聊聊这个内容。 + +总结 + +这一讲主要专注在低代码平台如何在 App 开发过程中获取数据,从通用和个性化两个角度详细讨论了低代码平台数据的获取。通用方式获取数据的方式,可以适用于大多数 App 的开发过程,但需要对获取到的数据结构进行修正,以及无法预设获取到的数据的模型。因此,在对这个方式获取的数据进行可视化渲染之前,我们还必须配置数据的模型,以降低配置图形可视化渲染的难度,使得图形配置过程更具有业务含义。 + +有的企业做了中台化改造,在改造完成的部分,低代码平台通过中台统一获取数据就可以省去非常多的麻烦,包括数据结构和数据模型。数据中台往往会对数据进行治理,在治理完成之后,数据中台就可以给出结构统一的数据,给低代码平台开发者使用了。同时,数据中台在数据治理后,还可以将数据的模型作为资产,提供统一的 API,低代码平台通过数据资产 API 就可以获取到数据模型的信息了。我在【第 1 讲】中提到,中台和低代码的演进线路有相当一部分是重合的,这就是一个例子。 + +使用个性化数据的体验往往会比通用化数据要好得多,可以实现更加彻底的可视化方式来使用数据。但是个性化数据的获取需要有大量的定制化配置,这就要求低代码平台必须提供一套插件机制来支持业务团队在获取个性化数据方面的定制需求。这讲中,我结合【第 9 讲】的知识,扩展出了一种新的插件定制架构和方法,基于这些思路,你应该可以做出个性化数据定制机制和二次开发插件的方法了。 + +最后,低代码平台还需要提供数据打桩的方法,帮助应用在无法直接获取到数据的情况下,可以基于模拟或者转发的方式来获得数据,使得 App 的开发得以继续。 + +思考题 + +如要采用这讲给出的方法为你的一个常用的业务场景定制一个获取个性化数据的插件,你会如何设计它的 UI,以及如何生成代码?欢迎在评论区留言。 + +我们下一讲再见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/13|多人协同编辑:野百合的春天为啥来得这么晚?.md b/专栏/说透低代码/13|多人协同编辑:野百合的春天为啥来得这么晚?.md new file mode 100644 index 0000000..9a28848 --- /dev/null +++ b/专栏/说透低代码/13|多人协同编辑:野百合的春天为啥来得这么晚?.md @@ -0,0 +1,204 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 13|多人协同编辑:野百合的春天为啥来得这么晚? + 这一讲我们来说说低代码平台的一个甜蜜的烦恼:多人协同编辑。 + +为什么说这是一个甜蜜的烦恼呢?因为一旦低代码平台有了这样的需求,就意味着它已经可以开发出有相当复杂度的 App 了,也意味着各方对低代码平台已经有了较强的信心,甚至说它在复杂 App 开发方面已经相当深入了。我们可以说这样的低代码平台已经具备了较强的开发能力。 + +说它是一个烦恼,是因为往往这个时候的低代码平台已经成型了,底层数据结构必然已经固化。如果平台架构早期未考虑到多人协同的话,此时就很难采用最优解来解决这个需求了,只能退而求次,采用迂回的方法。 + +那么今天,我们就从多人协作功能的实现难点入手,聊聊它的实现方案和注意事项。 + +多人协作功能的难点是什么? + +面对这个问题,可能你会猜难点是多个编辑器之间的点对点通信和实时数据传输。不可否认,这是一个难点。但现在的 web 技术有太多的解决方案了,WebSocket,WebRTC 等都是极好的解决方案,我推荐优先选择 WebSocket。 + +因为 WebSocket 更成熟,服务端实现方案多且完善,它更加适用于一对多广播,相对来说,WebRTC 更适合用于 P2P 传输音像多媒体信息,实现更加复杂。更具体的,你可以自己搜下相关资料。 + +那么真正的难点是啥呢?我认为首先是如何解决冲突。你想,多人对同一个工程进行编辑,难免会同时对同一个组件的同一个属性做操作,或者是你在改某个组件,而我要把它删除。这样的操作就会产生冲突。 + +那么如何解决冲突呢?有一个办法,我们可以像 git 那样,标记每个冲突点,然后中断 App 开发的工作,强制要求他们做出选择呀。这是一种办法,但是不彻底。这个问题,我们可以用一种釜底抽薪式的解决方案,就是不让冲突出现! + +那么如何避免冲突呢?到这里就需要介绍 CRDT 算法了,CRDT,也就是 Conflict-free Replicated Data Type,无冲突复制数据类型。 + +实际上冲突是不可避免的,只是 CRDT 采用了某种策略,就像一个和事佬一样,帮助协同编辑的各方妥善安排了冲突。但这个策略已经超出了这讲的范畴,有兴趣你可以自行了解一下 LWW(即 Last Writer Wins)策略。当冲突发生时,谁对谁错不重要,重要的是,各方能协商一致,且各方都可稳妥地拿到这个协商结果。 + +CRDT 是一个算法,而且还挺复杂的,那么有没有实现了这个算法的库呢? + +必须有!适合 JavaScript 生态圈的,有 3 个,分别是 Yjs,automerge 和 ref-crdts。这三者的性能对比,你可以参考下面这个图(引自雪碧的文章): + + + +这三个都是当前主流的 CRDT 实现,它们主要的应用场合都是多人协作在线文档,但他们也支持 json 这样的数据结构,所以,也可以用于低代码平台的多人协作功能。 + +这里你要注意图中红色的箭头,Yjs 的效率非常高,几乎与横轴贴合在一起。所以应该选择哪个,就不需要我多说了吧? + +前面我说了,多人协作的难点首先是冲突,既然有首先,那当然就有其次了。其次的难点就是历史记录管理。 + +请你想想,多人一起编辑一个 App,你总不能把别人的修改给撤销了吧?如果真是这样,那冲突就可能从线上发展到线下去了(要打起来了)。但是大家同时操作产生的历史记录交织在一起,要完全依赖你自己设计解决方案来实现撤销功能,想想都觉得很复杂。 + +莫急,Yjs 提供了一个 UndoManager,可以用于记录使用人的历史记录和正确地执行撤销、重做等功能。非常贴心。 + +既然有其次的难点,想必还有再次的难点,那就是,很可惜,CRDT 的解决方案,你可能用不了!(手动恐惧) + +这就是所谓“多人协作功能作为一个甜蜜的烦恼”中烦恼的部分。 + +为啥这么说呢?如果你采用 CRDT 解决方案,数据保存的格式就必须采用它定义的格式。一句话说明 CRDT 的原理:对所有操作打上时间戳,然后通过网络把各人编辑产生的历史记录分发出去,本地接收后,完成合并。合并历史记录时,所有冲突点,都使用 LWW 策略处理冲突。所以历史记录的格式是所有 CRDT 解决方案的根基。 + +但是,历史记录的格式,何尝又不是低代码平台正常运行的基础之一呢?虽然不是决定性的基础,但是在低代码平台成型之后,我们要对这个部分做调整,必然是伤筋动骨啊! + +另外,应用数据的归属也是一个阻碍多人协作的问题。低代码平台基本上都会按照用户来隔离应用数据,即用户只能看到自己的应用数据,无法看到他人的数据,甚至多数平台会采用租户的方式,对应用数据做物理隔离。 + +一言蔽之就是,用户数据的存储方式也需要在平台建设之初就有多人协作的考虑,否则后期的调整也是伤筋动骨。 + +多人协作这个功能,就是一株野百合,它总能等到属于它的春天的到来,但是春天的脚步是如此缓慢,姗姗来迟,待到低代码平台的成熟度高到足以支持需要多人一起开发复杂 App 的时候,已然太晚,木已成舟,这时再对底层基础功能做调整很不现实。此时,CRDT 解决方案再巧妙,Yjs 性能再好,UndoManager 再贴心,你也只能望洋兴叹。 + +这小段文字描述的正是我的心情。但是,当你看到这一讲的时候,希望你还有机会作出选择。 + +前面描述了 CRDT 的种种好处,如果你的平台现在还未定型,还有机会选择 CRDT 的话,可能已经摩拳擦掌要试一试 CRDT 了,请先别急,我现在要来泼点冷水。 + +CRDT 一定是银弹吗? + +目前业界对 CRDT 最成熟的使用是多人协作在线文档。虽然 CRDT 算法的初衷并非只针对多人协作文档,同时 Yjs、automerge 等实现也确实可以支持 JSON 数据结构,这是低代码平台所必需的。但是,我确实很少听说有低代码平台实际使用这个功能的。 + +低代码平台的使用过程(即 App 的开发过程),与在线文档是有很大差别的,我接下来会给出一个情形,说明低代码平台即使使用 CRDT,也会出现冲突。作为一点背景知识,虽然前文已经有提了一点了,这里我们还需要进一步解释为啥 CRDT 可以做到 conflict-free。 + +简而言之就是及时,及时意味着任何修改,都要及时记录。注意只需要及时记录就好,不需要及时同步给各个协同者。 + +因为及时记录下来的同时,一个时间戳和插入位置就被生成并记录到编辑历史中。此时即使有多人在同时编辑,产生大量编辑记录,但各个历史记录的时间戳却各不一样。即使此时网络都堵塞了,导致这些编辑记录都未能及时同步,也没关系的。在网络恢复之后,他人的编辑记录同步过来后,不同插入位置的编辑记录不会有冲突,它们会按照时序在本地生效。相同插入位置的编辑记录则以 LWW 策略,挑选最后一个改写记录作为最终结果。 + +这个过程的示意图如下: + + + +那在低代码平台上,啥情况下会出现保存不及时的情形? + +这就和低代码的开发方式的特征有关了。可视化开发是低代码的主要特征,因此在开发过程中,如果涉及复杂的配置,不可避免地需要使用对话框来承载。比如下面这个例子,弹出一个对话框来组织一次复杂配置的过程,是很常见的: + + + +注意,为了使用友好,对话框会有确定按钮,等用户点击确定按钮之后,再一次性保存使用的配置,点击取消则会丢弃对话框上所有修改,这是一个很有用的功能。 + +问题就出在这个地方了。如果对话框上的修改未能及时保存下来,有人在某个对话框上编辑的同时,恰好另一个人也打开了同一个对话框开始配置,这个情况下,冲突就出现了: + + + +图中虚线的编辑记录与他人打开同一个对话框时的记录是相同的 + +你可能会有疑问,此时 LWW 策略不再适用了吗? + +其实,LWW 是适用的,但问题在于这样非常不友好。为啥呢?因为对话框承载的是复杂配置,包含大量的配置项,使用 LWW 简单粗暴地进行二选一,这样必然有人的工作会被白白浪费掉。再者,即使在同一个对话框上,也有可能未编辑到同一个属性,此时记录 1 和记录 2 是可以直接合并的,因此我们不能简单粗暴地使用 LWW 策略来处理。 + +在这个情形下,CRDT 无法避免冲突。 + +CRDT 在低代码平台上“水土不服”的第二个表现是,性能问题。在线文档也有所见即所得效果,任何修改同步过来后,都可以毫秒级生效。但与在线文档不一样的是,低代码的所见即所得效果则“昂贵”许多,它需要时间来编译,需要更多的时间来渲染。在 App 复杂时,需要三五秒才能完成一次反馈。在这样的响应速度下,如果同时编辑 App 人数太多,那基本大家的时间都会消耗在无休止的等待上了。 + +第三个“水土不服”的表现是,修改位置。在线文档的插入位置十分简单,只需两个整数记录光标所在的行和列即可。 + +但低代码平台的插入位置则复杂得多。因为低代码平台存储的数据是一份结构化的数据,因此插入位置需要使用一个类似 DOM 树的 xpath 的形式来表示。这还不够,一个 xpath 对应的是结构化数据里的一个属性,这个属性的值有可能是简单值,也有可能是多行文本,比如一段代码(关于为啥属性值会是一段代码的原因,你可以回顾一下第 11 讲高低代码混合开发)。 + +如果是简单值,那使用 LWW 策略时,二话不说,直接覆盖就完了,但当面对多行文本时,直接覆盖就不妥了。设想一下,前一个人,对某个多行属性做了数十行的改动,而后一个人只对同一个多行属性修改了 1 个字符,无脑应用 LWW 策略的话,前一个人的数十行修改就丢了。这也是一个问题。 + +那这些问题有没有解决办法呢?都有。 + + +对于冲突问题,有一个比较优雅的解决方案是,使用一个临时的独立的影子历史记录来及时保存所有修改。在确定要保存这个影子记录时,将所有的修改合入主修改记录序列中,否则将其丢弃即可。影子历史记录里的修改依然保持及时被记录的特征,所以在应用 LWW 策略规避冲突时,被直接覆盖的工作就小到可以忍受的程度; +对于性能问题,可以考虑约束同时编辑的人数,但这治标不治本。一个更好的方法是引入懒渲染的方式,即只在需要的时候启动渲染,其他情况下只记录并提示有多少未渲染的修改即可; +对于多行属性的问题,可以学习一下 git 的做法,你一定用过 git 合并过代码,对不同行所做的修改,git 合并时是不会冲突的,对吧?所以,除了 xpath 外,还需要增加一个行号。只有对同一个 xpath 属性的同一行做编辑,才需要应用 LWW 策略,其他情况直接合并即可。 + + +这一讲到现在,前半部分是在疯狂安利 CRDT,后半部分却又不停地对它泼冷水,为了避免你不知道我的目的是啥,这里我提前做一点小结。 + +总的来说,如果你现在还有得选,那么我建议你引入 CRDT 算法,作为你的低代码平台的底层数据保存和历史记录管理功能的基础,即使你现在看不到有实现多人协作功能的必要,但 CRDT 也可以提供成熟的数据结构、历史记录管理等有价值的功能。还有,万一以后做大了,需要多人协作功能的话,就不需要再去苦思解决方法了。 + +现在主流的几个实现 CRDT 的库中,我推荐你重点关注 Yjs,但可以适当了解一下 automerge 和 ref-crdts。Yjs 除了性能优越之外,还提供了 UndoManager 用来处理历史编辑记录,提供了回退和重做的功能,这两点是其他两个库所不具备的。 + +同时我们也要注意到,CRDT 算法在低代码平台的应用案例还不多,至少我还没看到有实际应的用案例,所以你需要重点关注和评估我列出的 3 个注意点,也就是由于未及时记录带来的冲突、渲染性能挑战、多行属性值的合并,对应的解决措施你可以翻到前文再看看,我不再重复。 + +万一,你和我一样已经没得选了,有没有补救的方案呢? + +有没有补救方案? + +说没有是不可能的,但补救方案要根据已有实现而定,没有标准解。我介绍一下我实际采用的补救方案,希望对你有所启发。 + +我先介绍一下我们低代码平台的背景,主要是下面两个困难让我与 CRDT 失之交臂: + + +应用工程数据与应用开发人员账号是强关联关系(物理关联),并且所有的自动化脚本都建立在这个关联关系之上,如果强制改为逻辑关联,会导致所有自动化脚本都需要改写; +前端持久化数据时,一时偷懒,每次都发送全量数据给后台,而后台则是基于全量数据为基础来实现的历史管理功能。之所以保存时采用全量数据,一方面是追求更快实现功能,内网速度快,不需要“省吃俭用”,使用全量数据可以快速实现功能;另一方面是实现非常简单可靠,全量数据拿到后直接持久化即可,如果是增量数据则还需要合并,当时的 Yjs 等完成度远不及当下,没敢入坑。 + + +从这两个困难中可以看出,当初我是一点都没有考虑到多人协作这个功能,不然也不可能实施这样的方案。 + +那么在补救方案中,我首先要解决的是应用工程数据与应用开发人员账号强关联的问题。让每个人都用同一个账号是不可能的,因此,我们还是要想办法让多人协作的应用工程数据能做到“人手一份”。 + +要实现数据的冗余是很容易的,我设计了一个分享动作,允许开发人员将一个工程分享给其他人,每一次的分享,都是对数据的一次冗余。 + +收到分享的开发人员,就可以像对普通工程那样对它做编辑。每次编辑产生的修改记录也和普通工程一样,被发送给后端,持久化到该开发人员名下的那份冗余的数据里,所有持有该冗余数据的开发人员此时是各干各的,互不影响。可以看到,这个过程对已有的流程是完全没有冲击的,除了新增的分享功能之外,完全复用原有流程。 + +难点在于冗余的数据如何实现归一。 + +如果要做到两两合并,过程会非常复杂,而且极其容易出错。因此我做了一个约束,冗余的数据能且只能合并到最原始的版本去。下面是相应的示意图: + + + +无论数据在中途被转发了多少次,它始终记录着原始账户名。只有原始账号才能发起合并操作,并且,每次合并只能指定一个目标冗余数据。这样就可以控制每次合并动作都只发生在两份数据之间,并且合并的方向是确定的,始终都是合入到原始所有者的版本中去。在合并完成之后,原始数据所有者会把合并后的数据发送给对方,对方收到数据后,不需要合并,直接覆盖掉合并前的数据即可完成两者同步。 + +在实际操作时,也可以做进一步的约束,不允许二次分享应用数据,这样会让这个拓扑图看上去简单许多,但是这对数据归一的复杂性没有帮助,只是看上去简单一些而已。 + + + +接下来我们再说说如何合并两份数据。经过前面的一番约束之后,单次合并只剩下两份数据了,看起来可以直接合并,并且不可能出现冲突了。真的是这样吗?你可以先思考思考。 + +虽然只有两份数据了,但是这两份数据依然是有 3 个状态,只要有 3 个状态的合并就必然会有潜在的冲突。其实这两份数据完全可以拿 git 的两个分支来类比,如下图。 + + + +一开始只有一份数据,在分享出去的一刹那,就产生了分叉,并且这两个分叉将在两个不同的开发人员手里独自演进。只要修改没有冲突,那就可以直接无脑合并,所以合并前的关键步骤就是如何检测有哪些冲突点。比如上面这图,是拿节点 2 和节点 3 来比对冲突点吗? + +只有两个节点是不会有冲突的,至少要有 3 个节点才会有冲突,图中的分叉点 1 就是这第三个节点。比对过程是这样的,拿节点 2 和节点 1 相比,得出一组修改集,每个修改点是由属性 xpath 和属性值变更行号组成的二元组。再拿节点 3 和节点 1 相比,得出另一组修改集。然后从这两组修改集中筛选出所有相同修改点。二元组的两个属性都相等,则认为是同一个修改点。每一个相同的修改点,就是一个冲突点。 + +如果一个冲突点都没有,那意味着可以直接合并,合并之后的历史记录如下: + + + +图中虚线框是原来两个分支各自使用修改点融合在一起后的集合。这个过程就和 git 合并时,把一些 commit squash 在一起然后 rebase,是一样的。 + +当冲突出现时,是否有 LWW 这样的策略可以用呢? + +很可惜,没有,只能手工解决冲突。单行属性值的冲突,可以二选一,多行属性值冲突,则需要采用和 git 合并代码时相似的解决方式。Monaco这个编辑器内置提供了 diff 功能,带有差异点着色功能,非常棒,再次推荐你使用。 + +解过冲突的人都知道,这不是一个愉快的过程。因此,我建议你采用这样的方法来缓解这个不愉快:任何协同者对应用数据做了修改,都在后台实时计算出是否有冲突,一旦出现第一个冲突的时候,立即给出提醒,甚至强制要求解决冲突。 + +这样可以降低解决冲突的难度,避免冲突过多导致无法合并。同时,也可以对未合并的修改计数,当数量超过某个阈值时,通过一些非侵入性的方式提醒协同者要及时合并。常见的非侵入性提示是在侧边弹个气泡,就像这个效果。 + +与代码比对不同,低代码平台上应用的“源码”是一份结构化数据。所以直觉上,你可能会首先想到要去找一个 json 比对工具来辅助。但实际上,对应用的结构化源码的比对,不能采用通用的 json 形式。 + +举个例子你就清楚为啥了。在已更新的内容中,我不止一次提到容器是一种特殊的组件,它可以把其他的容器或组件装在它内部,所以在应用结构化源码中,容器的子级是一个数组。对通用 json 数据来说,顺序是敏感的,但容器子级数组里的对象的顺序不一定是敏感的(有可能敏感,也有可能不敏感)。在子级顺序不敏感时,如果依然严格按照数组顺序来比对,必然会得到一个很大的错误的修改集。 + +所以,比对在应用结构化源码时,我们必须根据组件的 id 找出两份数据中的同一个组件,然后,再根据组件的 schema 依次遍历组件的所有属性,这样获取到的修改集才是准确的。对于同一个组件的配置数据来说,它有哪些属性是事先已知的,你在实现低代码平台的时候,一定会有一份 schema 用于描述组件所具有的配置项。 + +总结 + +我在前面已经提前对 CRDT 做了总结了,你可以翻回去回顾一下。这里我再补充一点,Yjs 在保存数据时,采用了 quill 的delta数据结构,只发送增量部分的修改,而非发送全量数据。这是一个很好的特性,一方面节约带宽、使得同步更及时,另一方面安全性更好。如果你拿捏不准以后是否需要有多人协作的功能,也可以先引入 Yjs 这样的 CRDT 解决方案,为以后的演进留一条路。 + +这讲中我给出了我所使用的一个补救的方案,不一定适合你的实际情况,但我在这部分给出了两个导致我无法使用 CRDT 的原因,你可以着重了解一下,绕开我所犯的错。 + +第一点是注意不能让应用数据与开发人员账号形成物理隔离的关系,即不要把应用数据保存到开发人员名下,形成一一对应关系。而是将所有数据都集中存在一个地方,然后通过关联关系挂到开发人员名下,从而形成逻辑隔离,这样日后要实现应用数据与开发人员账号之间的多对多关系,就会简单许多了。 + +第二点是数据持久化时偷懒采用发送全量数据的方式,这导致后端保存数据的方式与 CRDT 的各种实现所使用的增量保存的方式有很大差异,从而导致后端改造成本巨大。 + +总之,即使你后续不需要实现多人协作功能,也可以现在就引入 Yjs 这样的 CRDT,是一个很好的选择。 + +思考题 + +假设你也没有条件使用 CRDT 来解决多人协作的问题,你会采用啥样的补救方案?欢迎在评论区简要写下你的方案。 + +下一讲我会说说低代码编辑器的编辑历史的实现,你可以做些准备。我们下一讲再见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/14|编辑历史:是对Git做改造,还是另辟蹊径?.md b/专栏/说透低代码/14|编辑历史:是对Git做改造,还是另辟蹊径?.md new file mode 100644 index 0000000..d580ab6 --- /dev/null +++ b/专栏/说透低代码/14|编辑历史:是对Git做改造,还是另辟蹊径?.md @@ -0,0 +1,217 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 14|编辑历史:是对Git做改造,还是另辟蹊径? + 今天来聊聊低代码编辑器的编辑历史。人生没有后悔药,但在计算机世界里,编辑历史就是后悔药,不但常见,而且非常廉价。 + +多数编辑器,如代码编辑器,或者我正在码字的编辑器,都会配备最普通的编辑历史管理功能。你可以从近到远地撤销(Undo)掉所做的修改。如果撤销了之后,没有做过任何编辑操作,还可以重做(Redo)这些修改,即撤销掉撤销操作。一般在撤销之后,又做了任何操作了,那么所有重做的历史记录都被删除。示意图如下: + + + +编辑历史是一个非常重要且友好的功能,它最主要的目的是鼓励应用开发勇于试错。开发的过程,就是一个不停试错的过程,试错成本越低越好。 + +作为低代码编辑器,我们同样也要支持上面提到的基本历史管理功能。不仅如此,还要更进一步,支持历史记录多分支功能。比如前面的示意图就是这样,第 3 步重新编辑后,得到了编辑记录 2,此时,编辑器会把记录 1 丢掉,目的是要维持历史记录的单向性。不丢弃记录 1,并且允许该记录继续编辑下去,就会形成历史记录多分支的情形,这和 Git 的分支非常相似。 + +做过程序开发的都会用到 Git,程序员们看中 Git 的最主要一点就是它强大便捷的分支管理。低代码编辑器作为一个开发工具,对多历史记录分支的支持也是必要的。所以今天这讲,我们就来说说低代码编辑器的编辑历史功能怎么实现。 + +基本编辑历史功能的实现 + +上一讲中,我推荐你将 Yjs 这个 CRDT 实现用作多人协作功能的基础算法。即使你不打算提供多人协作功能,我依然推荐你考虑引入 Yjs,它不仅提供了一套完善的增量式编辑历史持久化机制,还提供了一个UndoManager来协助编辑器完成基本历史管理。如果你有机会使用 Yjs,那应该好好考虑一下这个工具。 + +今天我们不会展开讨论UndoManager的用法,有必要的话,给我留言,看情况我再决定是否专门聊聊。但不管你是否采用 Yjs 算法,我都建议你继续学习这一部分,我会从别的实现方式讨论编辑历史的实现,他山之石可以攻玉。 + +那么,除了 Yjs,还有其他的工具没? + +Git 就是一个很好的备选。Git 轻量小巧,性能优越。而且,它自带了一个文件数据库,用来保存修改历史数据,通过 git commit 和 git log 可以实现对编辑历史的管理。不过,也许你会担心历史记录数量大了之后 Git 的性能表现,我特地去看了Linux 内核代码仓库,现在已经超过 100 万 commit 了。而基于我们的低代码平台 Awade 开发的、那些比较活跃的 App,历史记录数也还未过万。 + +Git 有非常强大的历史分支管理能力,用于多人协作时,可以利用 Git 强大的自动合并功能,避免上一讲中自行实现多人协作的许多麻烦。而且,由于所有合并和记录操作都可以在服务端上通过 Git 来完成,前端就变得更轻量了,不需要引入 Yjs 这样的库了。同时,Git 的分支管理能力也可以用于这一讲后面要讨论的、低代码编辑器多分支编辑历史功能的实现。这方面无须对 Git 的能力有任何质疑,它生来就是为解决这个问题的。 + +虽然 Git 没有一个官方社区(或者有但我不知道),但现在世界上有数以千万计的开发人员都在用 Git 来管理代码,基本上你碰到的任何问题都可以在网络上找到大量的解决方案。这也是一个无可比拟的优势。 + +那我们可以怎么使用 Git 呢?有两种使用 Git 的方式,单仓库方式和多仓库方式。这里我们简单讨论一下。 + +单仓库方式是把所有的应用数据都放在一个 Git 仓库中,优势是简单只需初始化一次,代价是仓库可能会非常大,历史记录也会非常多,所以我并不推荐你使用这种方式。 + +我推荐你使用多仓库方式,也就是每个 App 的数据都保存在一个独立的 Git 仓库中。这是一个比较好的方式,git init 一键初始化一个仓库,各个应用数据物理隔离,安全性高。 + +那么 Git 有没有不适用的一面呢? + +我们都知道,Git 适合用于管理多行文本类的数据,它对长单行文本和二进制数据几乎没有办法。但是,我们的低代码编辑器需要管理少量二进制数据,如图片、字体等,可能还有少量的 Excel 文件。 + +对长单行文本的无能为力是 Git 更致命的一个短板。低代码编辑器一般都是基于结构化数据的,为了简单,会将结构化数据序列化(JSON.stringify)之后进行存储。结构化数据序列化后,就是一个长单行文本。 + +即使在序列化结构化数据时保持多行的结构,但应用数据里的某些多行属性值,在序列化之后却依然是长单行文本。比如下面这样代码块: + +const a = 123; +const b = '123'; +console.log(a, b); + + +如果不做任何处理,在序列化后的值是这样的: + +{ + "value": "const a = 123;\nconst b = '123';\nconsole.log(a, b);" +} + + +那么,面对这样的情况,如果拿 Git 做我们的解决方案,应该怎么办呢? + +我们可以把多行文本按照换行符拆分为字符串数组,然后再存储: + +{ + "value": [ + "const a = 123;", + "const b = '123';", + "console.log(a, b);" + ] +} + + +使用时,再把这个字符串数组读出来,然后用 arr.join(‘\n’) 就可以还原了。 + +不过,有的低代码编辑器为了进一步节约空间,不采用 JSON.stringify 来序列化数据,而是采用比如Uint8Array这样的二进制方式来持久化。这个情况下,Git 就几乎帮不上忙了。我们应该避免这样的做法,改用 JSON.stringify。 + +此外,还有一个问题不那么“显眼”,但其实非常麻烦,Git 采用 GPLv2 协议对外分发版本。这个协议有“感染性”。简单地说,任何软件如果使用了这个协议的软件,都必须开源,否则人家的基金会就会告你,基本一告一个准,赔了钱后,要继续用还必须开源。所以如果你做的是一个商业性质的低代码平台,那么要谨慎。 + +那是不是就不能用了呢?也不是。只要一个商业软件发布的版本里不包含 Git 的任何文件(二进制或者源码)就好了。所以,你可以想办法给你的客户的运行环境上预装好 Git,就可以绕过去了,或者在安装好你的低代码平台之后,再给一个独立的安装 Git 的流程也行。总之就是要解除你的平台和 Git 等 GPL 软件的捆绑。 + +也许你会对商业合规嗤之以鼻,那是因为你所在的企业没有像中兴这样,差点因商业合规而面临倒闭。但合规无小事! + +虽然上述两个方案都可行,但它们却都不适用于我们的低代码平台 Awade,不能用 Yjs 的原因我在上一讲已经说得很详细了,不用 Git 的一个很主要因素是它使用了 GPL 的协议。在中兴,除非实在是没有替代品,否则所有使用 GPL 协议的软件都默认不用,即使这个软件有一定优势,但漫长累赘的备案审批流程,就能让人退避三舍,所以我们技术选型时,一看到是 GPL 的,直接 Pass。因此,我们自己搞了一个解决方案,这里简要描述一下,供你参考。 + +在上一讲中我说过,Awade 在持久化时,为了简单,是按照全量数据来存储的,在实现编辑历史的时候,也是基于全量数据的,每个历史记录里都是一份完整的数据。因此撤销也好,重做也罢,就是重读一下数据而已,非常粗暴,但实现非常简单。代价是 Awade 的历史记录数据非常大,一个 App 动辄就有数 G 的历史记录。 + +每个历史记录都有一个配置文件,用来记录它与其他历史记录的关系。这里的关键部分实际上是一个双向链表。一个历史记录有自己的 id,next 指针指向下一个记录 id,previous 指针指向前一个记录的 id,示意图如下: + + + +示意图1 + +你可以看到,这是一个典型的双向链表结构,很好理解。同时,App 工程数据里还有一个游标(cursor),用于指向当前正在使用的历史记录。新增修改记录的时候,将游标移向下一个记录,撤销或重做的时候,实际上只要修改 cursor 的值就好了。 + +基本编辑历史的实现,大致就是这样,接下来我们更进一步,聊聊前面一直提到的编辑历史多分支的实现。 + +编辑历史多分支的实现 + +很久之前,我收到一个投诉,应用的一个开发人员气冲冲地抱怨说,他将 App 回退到了某个历史记录,但做了一个意外的编辑操作,Awade 二话不说就把回退的编辑记录全部删除了,这导致他找不到 App 的最新数据了。这个事情让我意识到,是时候要让 Awade 支持编辑历史多分支的能力了。 + +作为一个开发平台,代码托管能力是必须要有的,而多分支是代码托管的必备功能。同时,有了编辑历史多分支功能,平台就不需要为了保持单一的编辑历史分支而删除被撤销的历史编辑记录了。 + +我们依然使用双向链表来表示这个功能,非常简单。如前文的示意图,当前的游标指向的是历史记录 4,如果应用开发此时做了一个编辑,那么只需要将历史记录 4 作为一个新的链表的开始,并新建一个历史记录 7 就好了: + + + +示意图2 + +正常的链表是不会出现分叉的,但这里为了示意记录 4 的关联性,我将这两个独立的链表画在了一起。请注意,这里用链表只是为了帮助你理解历史记录的数据结构。实际实现时,我们并不会真的去创建一个独立的链表,而是直接新建一个历史记录,并将其 pre 值指向 4,将记录 4 的 next 指向 7,并将游标从记录 4 移到记录 7,仅此而已。甚至都不需要关心在编辑时的游标是不是最新记录! + +无论应用开发如何撤销、重做、甚至在历史记录中任意跳跃,都只要将游标移动到正确的位置就好了。在编辑时,无论游标在哪,都可以无脑地新建记录节点,配置好相邻 2 个记录的 next 和 previous 值,并移动游标指向新记录就好。 + +说到低代码平台需要有代码托管能力,那就不得不再说回 Git,因为 Git 最主要的目的就是解决代码托管问题,而且它是全球数以千万计的开发人员共同的选择。 + +而且,要论分支的管理能力,Git 说第二,没人敢说第一,强大的多分支管理是 Git 最有价值的能力之一。因此,如果你选型了 Git 作为历史记录管理器,那么,这里只需要开发适当的可视化界面以充分发挥 Git 的能力即可。 + +现在市面上有大量的 Git 命令行可视化工具,也有集成在软件开发 IDE 中的菜单式的 Git 命令行可视化方案。但请注意,这些解决方案没有一个适合用来解决低代码编辑器的编辑历史分支管理。这些解决方案都是为了解决 Git 命令行难用的问题而生的,都是为了 Git 而 Git。但是这里我们仅仅是为了实现编辑历史记录多分支而使用 Git,所以所采用的可视化解决方案都应该是围绕这个目标来设计。 + +其实,即使你选型了 Git,你也可以参考 Awade 的可视化方案,把多分支的增删改查都“藏”到历史记录的操作中去: + + +在回退编辑记录之后再次编辑,则使用 git branch 命令自动新建一个分支; +在删除了某个编辑记录(在 Git 中对应一个 commit),则找出所有包含这个 commit 的分支,并将其删除; +在需要列出所有的分支的时候,使用 git branch 即可,并且使用 git merge-base branch1 branch2 这个命令来找出两个分支共同的 commit,这样就可以画出类似示意图 2 的修改记录逻辑分支图了。 + + +此时,Awade 和 Git 这两个方案殊途同归,差异只在于持久化数据的方式。 + +可视化分支管理的实现 + +我们前面的内容都在讨论编辑历史记录和多分支的后台实现,无论是 Awade 的方案,还是基于 Git 的方案,都可以很好地实现编辑历史记录的存储和多分支的实现。接下来我们就要说说前端侧的实现了,也就是如何实现分支管理的可视化。 + +我们依然分两个方案进行,我先从 Awade 的方案开始讲。 + +前面我说了,Awade 在后台采用了双向链表的方式来存储编辑历史记录和编辑历史分支。我们讨论了半天都在说双向链表,那哪来的多分支呢? + +其实,示意图 2 已经剧透了,示意图 2 就是 Awade 展示在应用开发人员面前的分支视图。当然,实际展示出来的时候,与链表相关的细节是隐藏掉的,他们不关心也不需要知晓背后的机制。 + +但你是需要关注这背后的机制的。所以我这里和你简单介绍一下如何找出历史分支的算法。我在前面说过,新增历史记录时,Awade 是不用管游标的位置的,直接无脑新建历史记录即可,物理上看,这些记录实际上只是这样的一串带有编号的平铺的历史记录序列而已: + + + +我们需要从编号最小的开始遍历,根据它记录的 next 值,就可以快速找到如示意图 1 所示的一个链表了,我把示意图 1 搬过来了,你不用往回翻了: + + + +示意图1 + +这就是第一个分支,标记这个链表里的所有节点。然后我们将所有未被标记的节点归为新的一组,重复这个过程,依然从编号最小的记录开始(本例中它的编号为 7)。这里与首次处理过程的差异是,我们要先根据这个记录的 previous 值,将它“挂到”previous 值指向的记录后面去。这个记录的 previous 值所指节点(本例中它的编号为 4),就是一个分叉节点。递归这个过程直到没有未被标记的节点为止,我们就可以得到类似示意图 2 这样的逻辑关系图了: + + + +示意图2 + +为了加深理解,我再举一个更加复杂的例子,最终绘制出的逻辑分支图如下: + + + +示意图3 + +在最开始的时候,也是这样的一串带有编号的平铺的历史记录序列: + + + +从最小的记录 1 开始,持续遍历 next 属性,就可以得到第一个分支,并把未标记的节点组成新的一组: + + + +从记录数最小的 6 号开始,它的 previous 指向记录 2,于是要把 6 号记录挂到 2 号记录下,然后遍历所有 next 值,得到另一个分支: + + + +重复这个过程,找到最小的 9 号记录,挂到 3 号记录下面,遍历它的 next 后剩余 11、12 号两个记录,把 11 号记录挂到 7 号下,发现 11 号记录的 next 值就是 12 号。这样一来,所有的历史记录就都被挂到正确的位置去了,算法终止。最终得到了示意图 3 这个逻辑关系图。 + +这是一个时间复杂度为 O(n) 的算法,看起来性能不怎么样,但是 Awade 为了避免历史记录无限膨胀,做了一个最大记录数的限制(我们限制最大 600 个记录),会自动清理掉过老的历史记录。这样就可以确保这个算法的时间复杂度是常数级了。 + +我将示意图 3 称为历史记录树,你可以将这图画出来给应用开发人员看,这样可以帮助他们管理好当前所有编辑历史记录的逻辑关系。同时,你还可以在这颗历史记录树上增加一些操作,比如选择一个节点后,一步跳转过去。还可以提供历史记录删除的功能,在删除选中的节点时,它右侧(编号比它大)的所有记录都需要一起删除。 + +历史记录树,再加上树节点的选择、删除等操作,就共同组成了历史记录的可视化管理功能。你可以看到,这个过程中,我们用了许多诸如分支、链表这样的晦涩术语,但最终呈现在界面上的,却只有一棵树,以及两三个按钮,这是非常友好的。 + +接下来我们说说如果你后台用了 Git,该如何实现分支可视化管理。 + +其实,UI 部分,你完全可以使用 Awade 的解决方案。因为 Git 本身就有极强的分支管理能力,所以实现起来就更简单了。你可以通过 git branch 命令列出当前所有分支,再使用 git merge-base branch1 branch2 这个命令找出两个分支共同的 commit。这个共同的 commit 就是分叉点,如示意图 3 中编号为 2、3、7 号记录,这样就可以画出类似示意图 3 的修改记录逻辑分支图了。 + +编辑历史分支的合并 + +最后,我们再看看编辑历史的分支如何进行合并的问题。 + +Git 本身就有分支合并的功能,我们直接使用 git rebase 命令就可以了。如果采用 Awade 的方案,你就还需要有进一步的说明。你可以回顾一下上一讲的多人协作的实现,多人协作的自动合并过程,与编辑历史分支合并的过程,是异曲同工的。 + +多人协作的合并目标,是同一个应用数据在两个不同人手里的两个版本,而编辑历史分支合并的目标,是同一个应用数据,在同一个人名下的两个不同的版本。都是同一个应用数据的两个不同版本的合并过程,本质上合并过程是一致的。因此,你直接采用我们上一讲给出的方法就可以实现编辑历史分支的合并了。 + +总结 + +今天这一讲,我们主要解决了低代码编辑器的编辑历史功能。从最基本的撤销、重做这样的编辑历史功能开始,再到编辑历史多分支的实现,多分支编辑历史对低代码编辑器这样的开发工具来说,是一个很有必要的功能。这不仅是因为低代码平台需要有代码托管能力,更是因为多历史分支可以帮助应用团队更好地开发复杂度更高的 App。从这个角度来讲:编辑历史功能至少在这些方面可以发挥作用: + + +降低应用团队试错的成本,他们可以大胆地对 App 进行探索性地修改,在获得成果之后,将探索成果合并到主分支上,否则直接丢弃即可; +帮助应用团队进行一站式的应用版本管理,可以将有的分支作为发版分支,有的分支作为系统测试分支,有的分支作为 Dev 分支。 + + +我们这一讲采用了两个相互独立的后台实现方法来实现编辑历史,一个是 Awade 采用的基于文件 IO 的方式,一个是基于 Git 的方式(其背后是基于文件数据库)。这两种实现方案都可以以比较简单、高性能的方式达到后台编辑历史管理的目的。 + +其中 Git 的解决方案,我们需要考虑它是基于 GPLv2.0 的版本分发协议,需要关注商业合规,而 Awade 采用的则是纯自研的方式,具有完全知识产权,没有这方面的担忧。 + +无论后台采用哪种方式实现,从应用开发人员的角度看,在前端的可视化分支管理功能的实现上,Awade 和 Git 这两个方案没有区别,操作方式也基本一样,不需要区分后台是如何实现的。 + +但有一点需要注意,如果后台采用的是 Git,切勿掉入可视化管理功能为了 Git 而 Git 的误区。目前致力于可视化 Git 命令行的解决方案很多,但这些方案都立足于替代 Git 的命令行,而非解决编辑器的历史管理能力,在我看来,这些解决方案都不适用于这一讲需要解决的问题。 + +思考题 + +支持多分支编辑历史的功能,是低代码编辑器具有代码托管能力的一种体现,多分支编辑历史这个功能也当然需要承担代码托管的更多功能。除了帮助应用试错和版本管理之外,你认为多分支编辑历史这个功能还需要承担其他哪些代码托管的功能?如何实现? + +欢迎在评论区留下你的见解。我们下节课再见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/15|低代码平台应该优先覆盖应用研发生命周期中的哪些功能?.md b/专栏/说透低代码/15|低代码平台应该优先覆盖应用研发生命周期中的哪些功能?.md new file mode 100644 index 0000000..4025284 --- /dev/null +++ b/专栏/说透低代码/15|低代码平台应该优先覆盖应用研发生命周期中的哪些功能?.md @@ -0,0 +1,147 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 15|低代码平台应该优先覆盖应用研发生命周期中的哪些功能? + 今天我们来说说低代码平台除了开发能力之外还需要什么能力。 + +我们专栏的常规更新部分,到现在已经更新到尾声了。前面好几讲的内容,我们都在关注低代码平台的开发能力。对低代码平台来说,开发能力当然是最重要的一种能力,没有之一。毫不夸张地说,开发能力直接决定了低代码平台的综合能力上限。 + +但低代码平台不能一味追究开发能力,也需要关注开发能力之外的能力。开发能力之外的其他能力决定了低代码平台的总体能力下限。在低代码平台发展初中期,我们当然是需要坚持优先发展开发能力,但是低代码平台有了一定成熟度之后,就需要开始关注并适当发展其他的能力了。 + + + +那么低代码平台除了开发能力之外,还需要发展哪些能力呢? + +我们可以以 App 开发的生命周期为线索来寻找这些功能。以开发能力为中心,它的左侧为需求端功能,右侧为交付端的功能,上方为生产环境管理功能,下方为资产管理功能: + + + +我们简单分析一下这张图。在需求端,一般有两种协同方式: + + +产品需求→UX 设计团队→业务开发团队:一般在 App 初创时走这个流程; +产品需求→业务开发团队:一般在 App 迭代过程走这个流程。 + + +低代码至少可以在这两个环节上发挥作用。对于第一种,UX 设计团队输出的是设计稿,低代码平台可以自动将设计稿转为可用代码,这个功能也就是 D2C 功能。对于第二种,低代码平台可以提供数量众多的模板和业务组件,给业务团队“抄作业”。 + +在交付端,App 上线或者交付之前,最重要的一个环节就是测试了,自动化测试是效率最高、最可靠的测试方式,那么低代码平台能否在 App 的自动化测试方面有所作为呢?另外,App 上线之后,想要获知用户对 App 的使用情况、App 的运行状况,就需要事先植入埋点,用于采集数据,然后要有办法拿到埋点数据,并提供分析功能。 + +在生产环境管理端,低代码平台至少可以在运行时自动部署方面提供帮助,特别是一些容器化的运行时,每个 App 上线之前,都需要制作和配置镜像,蓝图、Dockerfile,以及其他配置文件林林总总可能有数十个配置项需要填写,任何一个填错就会导致部署失败。一些重要的 App 上线后还有一个灰度发布的过程,需要进行灰度配置,甚至紧急回退等功能,也可以集成到低代码平台上。 + +资产管理端是将 App 作为一种业务资产来看待,包括代码自动评审、入库等,与代码托管相关的工作,甚至还可以覆盖 App 的自动化版本管理等与办公自动化相关的一些功能。这些都可以集成到低代码平台上,实现 App 从开发到管理的全生命周期管理能力。 + +这样一分析,你看,其实低代码平台在 App 开发的全生命周期中,能做的事情还有很多。这些能做的事情,也不是全部都要我们原创。我们可以把许多传统编码开发过程中需要人工完成的事情,实现一定程度的自动化,然后集成到低代码平台上,实现一站式管理,这样就已经可以发挥许多作用了。即使无法做到全自动,哪怕实现了半自动化,也是不小的进步。 + +这一讲的内容特别多,甚至我们还能在很多方向上展开聊聊,每块儿单独成一讲。所以我想着,索性将这讲作为一个提纲来总体分析,给你一个大致方向。后续在动态更新部分,我还会根据我的经验和同学们的需要与反馈,针对性挑选相应的功能,展开聊聊它们的具体实现。 + +需求端 + +需求端的 D2C 功能和模板能力,都是非常有用的功能。其中 D2C(Design to Code)功能是 UX 设计稿转代码的功能,这是一种很酷的能力,但是它的优先级远没有模板功能高。模板是 App 的半成品,或者是 App 中常用的、有代表性的一个部分。 + +模板最大的意义和价值就在于,应用团队可以快速地抄作业,不用从零开始开发。当积累了一定数量的模板后,再对它们进行分门别类,应用就可以根据自己的目标 App 挑选接近的模板,然后在模板的基础上继续将其演进为一个完整 App。从这个角度看,模板和纯编码模式下的脚手架差不多,但比纯编码的脚手架更优秀的是,这是一个可视化的、所见即所得的脚手架。 + +对任何具有开发功能的工具来说,模板有很大的意义。毕竟,开发的过程就是探索的过程,你想一下,如果你是一个缺乏经验的开发者,即使你手里握着低代码这样的先进生产力工具,但当你面对一片空白的画布的时候,依然会无所适从,不知道从何着手吧?模板就是一种极佳的破冰解决方案,给了开发者一个探索的方向。 + +而且,模板除了可以起到给应用团队抄作业的功能外,还有一个价值:模板天然就是一种学习的教材,它是一种样板实现范例。应用开发人员拿到一个模板之后,依葫芦画瓢,逐渐摸索,就能逐渐学会使用低代码平台。根据我的经验,大家不爱看使用手册,更喜欢直接上手摸索、试错。 + +利用代码直接识别 UX 设计稿,并生成可用的代码的技术(简称为 D2C),在过去两三年里,也得到了长足的发展,各大互联网巨头都纷纷推出各自的解决方案。实现的方法多种多样,眼花缭乱,简单的就是计算机视觉技术,再复杂一点就是 AI 图像识别。之所以要搞得这么复杂,门槛这么高,是因为现在的 D2C 工具都自成一体,它不仅需要识别 UX 设计稿,还需要将识别到的设计稿转为对人类友好的代码。 + +不知道你有没有从 D2C 实现的关键步骤里发现了什么?你看,生成代码这样的功能,不就是低代码平台擅长做的事情吗?这个功能,我们已经在专栏里多次讨论到了呀!那么,在低代码平台上来实现 D2C 功能,会不会比传统的 D2C 解决方案要简单许多呢? + +事实也确实如此!我们只需要从 UX 设计稿里识别出足够多的信息,“喂”给低代码编译器,低代码编译器就可以把代码给生成出来了。 + +那么哪些信息是“足够多”的信息呢?我们以通用场景为例,必须得到的信息是设计稿中各个组件的种类、位置、尺寸这几个值,其他可选信息有文本,颜色,图标等。 + +其中组件的类型是最关键的信息。有了组件的类型,我们仅仅根据 UX 设计规范,就可以获得这个组件的大量预设数据了,而 UX 设计规范是静态的,任何设计稿都是它的一个实例而已。因此,只需要从设计稿里读出组件类型,无须其他数据,我们就可以把这个组件的大部分信息推断出来了。 + +不过,位置和尺寸是 UX 规范给不了的,这两个信息决定了生成出来的 UI 是否可用,因此我们必须从设计稿中读取出来。其他的信息就都是可选的了,不会影响最终成败,但如果我们能从设计稿里识别出更多的其他的信息,也能提升自动化率。 + +所以说,在低代码平台上实现 D2C 的功能,虽然不能说有多简单,但是也不至于需要用上计算机视觉甚至 AI 这样的高精尖技术。在动态更新部分,我们会有专门一讲,讲解如何采用“平民化”的技术为低代码平台增加 D2C 能力。 + +交付端 + +自动化测试是软件交付过程中一个非常有价值的手段,而 UI 的自动化测试,是软件自动化测试皇冠上的明珠。 + +UI 自动化测试的成本很高,这一点不是因为测试用例代码难写,主要是测试用例代码调试成本高,测试用例代码的维护成本更高。这样的特点往往会让参与 UI 自动化测试的人笑着进去,哭着出来。刚开始的时候,轻轻松松十来行代码就可以驱动浏览器完成被测 Demo 页的自动化操作,很酷,但是一旦被测页面复杂起来之后,特别是与鼠标相关的操作多起来之后,人工轻而易举就可以实现的操作,调试用例时却要花大力气才能完成。 + +然后你坚持不懈,好不容易跑通了几个功能点的测试用例调试,结果还没跑上几天,就发现测试用例跑不过了。调试一番后,才发现有人修改了页面的一个 class 名或者调整了 dom 结构,导致用例的 selector 找不到 DOM 节点了。这是导致用例跑不过的大多数原因。 + +而且日常 Web 页面的开发,就充斥着大量这样的操作,会导致测试用例代码常常莫名其妙跑不过。这样的问题主要看需求紧不紧张。需求不紧张的时候,调试调试就可以解决了,但在需求紧张起来时,你猜第一个被砍掉的代码是哪些?显然是测试用例代码,说好过几天再修复,然后就没有然后了。 + +这就是 UI 自动化测试的现实和困境。那低代码平台能为此做点啥吗? + +低代码平台对整个 App 的 UI 结构和交互过程了如指掌,甚至比开发人员自己还更了解这个 App 的 UI 和交互。虽然低代码平台没有开发人员智能,但开发人员能写得出来 UI 的自动化测试用例,低代码平台应该也能生成出一些基本的来。 + +而能自动生成测试用例,也就意味着低代码平台有能力自动维护这些用例代码,不至于 UI 上有点风吹草动就导致用例跑不过。这样看来,人工编写 UI 自动化测试用例的两大痛点,都可以被解决了。 + +那么解决这个问题的思路是啥呢?其实也很简单,就是通过跟踪 UI 上的事件整理出被测 App 的功能点,加上低代码对 App 交互过程的了解,我们就可以生成模拟人工操作的代码了,从而也就可以自动生成自动化测试代码了。这部分更详细的方法,我会在动态更新部分专门介绍。 + +App 上线之后,那我们得知道 App 的使用情况、App 的运行状况,以及异常情况等信息,这都需要事先植入埋点,用于采集数据,然后我们还得要有办法拿到埋点数据,并提供分析功能。这个做法在纯代码模式下已经很成熟了,所以,低代码平台也不需要再次发明新方法,只要提供自动化程度更高的植入埋点能力和自动分析数据的功能,就可以帮助没有经验的人获取到所需的数据,用来改进 App 了。 + +生产环境管理 + +除了前面说的需求端和交付端外,低代码平台还要能在生产环境的运维上,提供一定的自动化能力。 + +在这方面,主流的低代码平台有两种差异比较大的解决方案:第一种是低代码平台将开发和运行环境合一,直接将开发好的 App“一键”推送给运行时,自动生成一个 URL 对外提供业务价值;第二种是开发环境和运行环境物理隔离,彻底解耦。 + +我们这个专栏介绍的低代码平台不严格区分这两种模式,不过 Awade 采用的是第二种,将开发和运行时隔离的方式。 + +开发和运行时隔离的方式的好处是可分可合,按需处理。低代码平台可以再独立开发一个运行时与开发时环境对接,实现第一种方式的一样的效果,也可以不提供运行环境,由业务单位自行解决运行环境的问题。 + +无论是内置运行时,还是业务单位自建运行时,低代码平台都应该提供自动部署方面的能力。如果是容器化的运行时,我们可以直接提供应用运行时镜像,在镜像中直接把蓝图、Dockerfile,以及其他配置信息都自动处理好,应用团队拿到镜像后就可以直接部署,或者由低代码平台自动推送给自带的服务器上直接部署。如果是物理机环境,我们可以打包所需的各种依赖,直接生成所需的各种脚本,做到应用解压后直接一个 run.sh 就搞定的效果。 + +无论是集成式运行时,还是独立式运行时,低代码平台都可以为 App 上线时的灰度发布提供能力,实现可视化的灰度发布策略选择,可视化一键紧急版本回退等主要功能。当然,如果灰度发布已经是一个成熟系统了,那我们就可以考虑将它集成到低代码平台之上,作为低代码平台在生产环境管理方面的能力之一。 + +而且,如果我们已经可以提供可视化灰度策略,内置多种不同的策略,根据灰度功能的特性,由业务团队选定特定的灰度发布策略,那当然也就可以支持发布策略的自定义配置了,包括: + + +基本策略配置:包括用户规模、覆盖功能、回滚策略、新旧系统部署策略等; +用户画像配置:包括用户特征、年龄、数量、地理、终端、常用功能、友好度、净值度等,根据手里的用户画像数据而定; +分流规则配置:这部分比较灵活,多以手工修改配置文件(如 nginx.conf)或者运维脚本为主,容易出错,可视化集成后收益较高。 + + +最后还有一点,低代码平台内置植入埋点数据的功能,也可以和灰度发布策略功能配合使用,埋点数据可以收集到用户信息、运行时信息等,这些都可以作为灰度发布的策略支撑数据。 + +资产管理 + +最后我们再来说一下资产管理方面的问题。App 是一种资产,它可以统一托管在低代码平台中,低代码平台提供可视化的代码托管服务,这样可以降低非技术人员在使用 Git 时可能会碰到的困难。 + +Git 作为一个专业的源码版本管理工具,只提供 MML(人机命令)接口,这对许多非技术线的低代码用户来说,太过专业。如果代码合入过程出了问题,特别是有冲突时,非技术线的用户往往束手无策。虽然现在已经有许多 Git 可视化工具了,但也是要求用户理解 Git 的基本逻辑和概念,不然也不可能用得好。 + +低代码平台上的代码托管功能,不需要完整复刻 Git 的各种能力,只需要把代码托管和 App 的工程管理融合在一起即可。 + +比如,在应用开发者打开应用工程时,自动执行 git pull,在退出应用工程时,自动执行 git commit 提交所做的修改。甚至还可以依托于 gitlab 或者 gerrit 这样的工具,自动就本次修改发起代码走查流程,将当前所做修改推送给管理员进行代码走查。此时 gitlab 或者 gerrit 还可以触发 DevOps 流水线,对当前的修改进行自动构建和测试等一系列操作,然后才进入人工走查阶段。而这一系列操作,都可以由低代码平台在后台静默执行。 + +这个过程看起来很复杂,但是其实大多数动作都可以由 DevOps 流水线来执行,低代码平台需要做的,只是正确地配置好流水线任务,以及触发 DevOps 流水线。像 gitlab 或者 gerrit 这种一站式代码托管工具,本身就有非常完善的 API,几乎所有操作只需要一个后台 shell 命令就可以触发。其中,gitlab是一个开源软件,社区版是免费的,gerrit是一个商业软件。 + +App 的版本也可以作为一种资产来对待,一般的软件企业肯定已有 App 版本管理系统了,低代码平台可以打通 App 版本管理系统,从而实现一站式的 App 版本发布和更新。这方面离我们专栏太远了,而且没有统一的对接方法,这里就只提出目标,但实现上就不再深入了。 + +总结 + +低代码平台不能只关注开发能力,开发能力固然是低代码平台最重要的能力,即使它定义了低代码平台的能力上限,但是也不能忽略其他能力。这一讲我从 App 开发生命周期的角度,从 4 个维度整理出了低代码平台的其他能力,这些能力共同定义了低代码平台的能力下限。这两种能力相辅相成、均衡发展才能从整体上推动低代码平台综合能力的发展。 + + + +我们再次回顾一下这张雷达图,水平方向上是从需求到交付,以研发能力作为主线,从 D2C 到模板和业务组件,再到开发,再到自动化测试,再到运行时数据采集和分析等几个能力,是应用开发过程中非常重要的几个环节,这是低代码平台应该着重关注的几个着力点,应该优先发展水平线上的这几个能力。 + +垂直方向上主要是从管理的角度来看低代码的功能的,从生产环境管理到资产管理,一共有灰度发布、自动化部署、版本管理、代码托管这几个着力点。相对研发能力这根主线来说,管理线上的能力优先级相对较低,如果你的低代码平台主要定位是面向内部使用,你甚至可以不需要发展管理线上的能力,但如果你的低代码平台有需要部署到客户现场的话,那么管理线上的这几个能力就不能无视了,这个情况下它们也是必须的。 + +最后我们再回顾一下能力示意图,相信你会有更系统的认知: + + + +思考题 + + +研发能力线上,除了这讲列出的几个能力之外,在你的场景中,还有哪些能力是同等重要的? +除了研发能力和管理能力线,你认为还有其他的维度吗? + + +欢迎在留言区里留下你的看法。我们下节课再见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/16|扩展与定制:如何实现插件系统并形成生态圈?.md b/专栏/说透低代码/16|扩展与定制:如何实现插件系统并形成生态圈?.md new file mode 100644 index 0000000..c503d03 --- /dev/null +++ b/专栏/说透低代码/16|扩展与定制:如何实现插件系统并形成生态圈?.md @@ -0,0 +1,262 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 16|扩展与定制:如何实现插件系统并形成生态圈? + 我们已经在专栏中多次提到插件这个词,那么插件到底怎么来实现呢?今天我们就来系统地梳理一下。 + +在【第 9 讲】中,我们解决了低代码编辑器的属性编辑器与 Web 组件的紧耦合问题,而且在【第 12 讲】的获取组件个性化数据的方法中,我们也采用了类似的思路,实现了应用定制化的动作与低代码平台松耦合的效果。核心功能与扩展功能的松耦合架构,是实现插件的关键基础。所以,我们可以将这两讲采用的方法进行归纳和抽象,形成一个允许应用团队在更大范围内定制和扩展的能力,我将这个能力称为插件系统。这就是我们今天这讲要解决的任务。 + +特别是【第 9 讲】中,我们细化到代码层面,进行一步步地设计和解耦,最终用一套代码架构同时支撑低代码平台内部实现和外部扩展。你可以复习一下这一部分,能帮助你更好地理解今天的内容。 + +在我看来,对于一个通用型的低代码平台来说,插件系统是一个非常重要的功能,它能够解决通用型低代码平台的许多问题。我们一步步来分析,先看看通用型低代码平台都有哪些弊端。 + +通用型低代码平台的弊端有哪些? + +我们之前已经说过很多发展通用型平台的好处了,不过凡事都有代价。如果站在业务开发(即平台的用户)的角度来看,通用带来的问题主要包括这几个方面: + + +使用门槛居高不下; +效率无法最大化,高不成低不就; +平台过于“高冷”和“挑剔”; +容易与业务需求耦合,逐渐腐化; +平台容易积压需求,造成不满。 + + +我们简单分析一下这几个问题。首先要明确的是,效率和赋能(降低门槛)是建设低代码平台的两大目标。如果我们的低代码平台达不到这个目标,那它必然不能算作是一个成功的低代码平台。即使我们把效率和赋能中的其中一项做到极致,也很难说已经获得了成功。 + +不过,从通用到具体场景,我们还需要做很多的额外工作,而且这些定制化工作的难度和工作量可能还不低,或者是开发过程比较麻烦。总之,刚刚接触低代码平台的用户和低技能者是很难驾驭这个过程的。 + +当然,我们可以通过内置模板的方式解决配置方面的问题,但是深度定制部分的工作,往往通过简单的配置是无法解决的,所以模板也不行。那这方面要怎么处理呢? + +深度定制这部分的工作和应用开发的效率紧密相关,实施难度很大。我们回顾下[第 8 讲]的内容,当时我们在讲解应用开发三部曲的布局篇时说到,网格布局器是一个通用的布局器,能用来开发表单类 App,也可以用来开发 Dashboard 类 App 和其他多数分析类的 App。但是,网格布局在各个场景下的效率表现有很大的不同,它在精细化 UI 的布局中的效率更高,但是在做表单布局时的效率却会比较低。 + +从这个例子中,我们可以看到:为特定场景提供定制化的能力,是提升效率的一个非常有效且直接的方法。 + +所以说,通用型的平台实现的任何需求,都需要充分考虑各种各样应用场景下的情况。在应用团队看来我们就非常高冷了,应用团队把一个小需求、小修改提给平台,往往都需要漫长的等待和评估,而且,许多在应用团队看来理所当然的需求,却会被平台团队给拒绝掉。 + +当然,只有在低代码平台团队非常强势的时候,它才能做到如此高冷和挑剔。但当低代码平台没有这样的资本的时候,事情则会朝另一个方向演进:与业务逐渐耦合,最终失去通用性。 + +如果扩展性不够,但又不够强势时,迫于压力,我们难免会使用 if else 来解决,if 只能爽一时,但一个 if 一个坑,用不了多久,要么就 if 不下去,要么就由于 if 的情况考虑不足与应用业务产生硬耦合。当第一个 if 出现的时候,低代码平台的架构腐化就开始了,当再也 if 不下去的时候,已经病入膏肓。 + +如果低代码平台的代码架构,已经有一定的扩展能力,能从架构上隔离开业务定制性的需求和通用性需求的话,就可以避免与业务功能产生强耦合了。但这样还不够,由于应用团队无法直接参与需求的定制,这会导致应用的需求都积压到低代码平台团队中,漫长的交付周期要么把平台团队搞得精疲力尽,要么无法按期交付,导致业务团队的不满。 + +我们再换个角度,站在低代码平台角度看这些通用性的弊端,主要问题是需要有一个良好的代码架构和演进策略;其次是需要尽早规划尽早实施,越往后推历史负担越大;再者是实现难度比较大,任何修改都要能满足通用性和定制性解耦。虽然这些问题也不少,但是这些都是平台内部的问题,属于内部矛盾,解决起来相对容易。今天这讲就不深入讨论这个方向上的问题了。 + +那么,为什么说插件系统能够解决这些弊端呢? + +首先,插件非常廉价。就凭这一点,插件就可以解决通用型低代码平台的各种弊端。廉价意味着开发成本低,也意味着可以有庞大的数量。数量庞大的插件可以像细沙一样,填满低代码平台各个大功能的覆盖盲区。 + +和平台团队接到需求时的各种瞻前顾后不同,用插件实现需求,就一句话:干就完了。也因为廉价,插件的试错成本比较低,搞错了大不了推倒重来。我们的许多需求可以先以插件的形式提供、试错,验证过后,再融入低代码平台中去。 + +廉价也意味着应用团队也可以参与,这样就能挡住不少原本应该提交给平台团队的需求。同时,鞋子好不好,只有脚知道,应用需要啥功能,应用团队自己最清楚。所以,这些自己为自己量身定做的插件也可以大幅提升应用开发的效率。 + +插件系统的设计与实现 + +那么,现在我们具体来说说如何设计和实现一个插件系统。我们可以从 SDK 的提取方法、可扩展的功能建议、插件二次开发和 manifest 设计,以及插件生命周期管理这几个方面来考虑。 + +SDK 的提取方法 + +啥是 SDK 呢?SDK 的全称是 Software Development Kit,也就是软件开发套件。在这一讲里,SDK 的作用是提供一套实现插件的框架和必要的辅助功能。 + +在设计一个插件系统的时候,低代码平台要先定义好哪些需要扩展的功能,并在内部事先预留好扩展点。而 SDK 的一个主要作用,就是帮助二次开发者更方便地找到这些扩展点。而且,SDK 需要提供必要的类型定义和功能,这些能够大幅降低在扩展点上做开发的难度,从而帮助二次开发者更快、更容易地做出一个插件来。 + +通常来说,SDK 主要包括: + + +接口和类型定义。这部分代码量可能还占不到 SDK 包所有代码的 1/10,但却是最重要的一部分。这些接口和类型就是插件的架构和框架,它们勾勒了整个插件系统的轮廓和概貌; +基类的定义。扩展点通用部分的实现,需要我们尽可能完整地将各个功能实现出来,只留下尽可能少的抽象方法。而二次开发者主要的工作,就是补全所有的抽象方法、覆盖必要的父类方法; +调试工具和构建 & 部署脚本。这是二次开发过程必须的专用工具,包括调试器、构建 & 部署的脚本、脚手架等。如果没有这些工具,二次开发基本就无法继续了,所以我们必须优先实现和提供; +辅助性、功能性工具类。这些就是工具包,封装了常用的功能,目的是降低二次开发难度,提升二次开发效率。它们是辅助性的,没有它们也不会对大局产生多大影响。因此我们可以降低这部分的优先级,在资源允许之后,或者根据二次开发人员的所问所需,针对性地逐渐提供。 + + +那么,我们应该如何设计一个扩展点,以及哪些代码需要挪到 SDK 中呢? + +这里你可以复习一下【第 9 讲】,我通过在低代码的属性编辑器上做扩展的方式,非常详细地给出了如何设计扩展点以及哪些代码需要放到 SDK 中的方案。同时,你也可以复习一下【第 12 讲】,这是另一个案例,采用类似的方法,你就可以按照自己的需要,设计出新的扩展点来了。 + +那么,有哪些功能适合开放出去给应用扩展呢? + +可扩展的功能 + +总的来说,我们至少有数据与数据模型、自定义组件、自定义交互动作等等这些扩展点。我们展开分析看看。 + +扩展点一:数据与数据模型 + +数据存取是存量系统与低代码平台之间最主要的对接方式,这也是插件系统主要需要解决的问题。 + +我们都知道,App 的职能可以分为两种:生产数据和消费数据,低代码平台自身能处理掉一部分数据,但是绝非全部,特别是对存量系统的数据的使用,是一个刚需。 + +但是喜新厌旧是人之常情,在企业里往往也是这样的。有了新系统,老系统的流量会逐渐切换到新系统,然后进入只读状态,但老系统里的数据是有价值的,可能由于数据结构、数据量等这样那样的原因导致无法将数据移植到新系统中,往往就必须新老系统并行一段时间(而且这个时间往往会很久),此时,我们就可以给老系统做一个插件用来与低代码平台做对接,是一个很好的选择。等以后老系统彻底下线了,直接把插件拿掉就行了,不会有残留。 + +还有另一种情况,也是我现在面对的情况,低代码平台与存量系统是共存关系,老系统数量众多,且依然继续在演进,不可能被低代码平台顶替。而低代码平台也不可能不顾一切地融入到某个存量系统(这样会大大降低低代码平台的价值),于是就出现了一个低代码平台需要对接许多存量系统的局面。而且每个存量系统都有自己的一套获取数据机制和迥异的数据模型。这样的情况下,唯有针对各个存量系统打造一个专用插件用来获取数据、提取数据模型这一条路可走。 + +扩展点二:自定义组件 + +自定义组件是低代码平台最主要的扩展点之一。一般来说,低代码平台内置的组件集都是通用的、常见的,而应用单位自行封装的业务组件,虽然通用性差,但在它适用的那一亩三分地里,价值很高,所以我们需要允许业务团队开发和封装他们适用的业务组件。 + +另外,再牛的内置组件集也会有功能盲区,在特定场合下,应用团队可以利用这个扩展点来为低代码平台添加新的组件。当然了,作为低代码平台兜底策略的一部分,低代码平台应该要有一种能力能在应用团队无须封装插件的前提下,直接调用原生 API,快速使用第三方库。 + +扩展点三:自定义交互动作 + +我们前面也说过,可视化编程是可视化开发模式中最难的一个环节,因为可视化编程中,我们需要编排大量的逻辑。 + +业务组件也有类似场景,比如业务团队内部会积累一些程式化的交互动作。如果我们要用通用动作编排出这些逻辑,需要填写大量复杂、不好维护的参数。这时,我们就可以将这些逻辑封装成自定义交互动作,只暴露出若干输入框作为参数,这样一来,自定义动作卡的使用体验往往就会好许多。 + +除了封装复杂逻辑之外,自定义交互动作还可以对存量的复杂系统的用法进行场景化归纳,再根据归纳到的使用场景设计相应的参数。动作的使用者只要选定一个预设场景,正确填写所需参数后,低代码平台就可以按预设自动生成相应的代码了。 + +比如,我所在的产品线有一个 WebGIS 系统,它有上百个 API。有一个深度使用这个 GIS 系统的业务团队把它在产品线中的用法归纳了一下,整理出了栅格展示、小区展示、热力图等用法,然后将各个用法封装成一个个自定义动作,每个动作必填的参数很少,主要是在配置如何查询数据。完成了后,即使对这个 GIS 系统不熟悉的人,也能快速地在 GIS 上渲染出所需的图层和数据。 + +其他扩展点:导出、登录等 + +最后,我们这里再列举一下其他可做插件的扩展点。 + +首先,我们在导出应用数据时,如果有扩展点,就可以对导出的原始应用数据做一些转换,转为其他平台或者其他用途的数据。我给你介绍下我碰到过的两个实际场景:第一个是将应用数据一键导出成支持多种运行平台应用包,有的是物理机运行时,有的是 Docker 虚拟机运行时;另一个是将在线 Web 应用直接导出为离线报告,比如 Word、PDF 等格式。我们现在就有应用团队正在研究如何导出有交互能力的离线 Web 应用包。 + +另外,如果你的低代码平台需要被其他多个系统纳管,或者要部署到客户的系统中,那很可能需要支持多种不同场景的单点登录功能。这个时候,针对每个系统制作登录插件,按需登录,就是一个非常好的做法。 + +插件二次开发和 manifest 设计 + +除了前面说的 SDK 的提取和可扩展点的设置外,在插件系统的设计和实现上,我们还需要考虑插件二次开发和 manifest 设计的问题。 + +和多数其他系统的插件一样,我们需要有一个 manifest 文件来描述插件的信息,基本信息包括:插件的名字、插件的版本、所用 SDK 的主版本,以及插件功能描述等静态描述信息。 + +但更关键的是,Schema 必须给出这个插件实现了哪些扩展点、各个扩展点所在的路径,还有各个扩展点的个性化配置信息等。举个例子,如果我们添加了自定义组件,那可能需要为每个新增的组件配置一个图标。 + +比如下面这个插件 manifest 是我们的低代码平台 Awade 正在使用的: + +{ + "name": "datahub", + "module": "DataHubModule", + "path": "web/dist/@awade/plugin", + "serviceInfo": { + "services": [ + { + "label": "DataHub", + "name": "datahub", + "remoteData": { + "class": "DataHubRemoteData", "import": "web/src/lib/components/datahub/remote.data-type" + }, + "renderer": { + "class": "DataHubRemoteDataRenderer", "import": "web/src/lib/components/datahub/remote.data-renderer" + }, + "initData": { + "style": { + "dataReviserHeight": "calc(90vh - 410px)", "paramBoxHeight": "calc(90vh - 475px)", "configModalHeight": "calc(90vh - 405px)" + } + } + } + ] + }, + "actionInfo": { + "actions": [ + { + "category": "事件与数据", + "type": "datahub", + "action": { + "class": "DatahubAction", "import": "web/src/lib/components/datahub/action-type" + }, + "renderer": { + "class": "DatahubActionRenderer", "import": "web/src/lib/components/datahub/action-renderer" + }, + "initData": { + "style": { + "padding": "10px 10px 0 10px", + "dataReviserBottom": "10px", + } + } + } + ] + }, + "metadatas": [{ + "selector": "plx-table", + "class": "PaletxTable", + "import": "web/src/lib/components/table/index", + "category": "dataDisplay", + "label": "Paletx Table", + "desc": "Paletx Table", + "icon": "assets/icon/plugin-paletx-pro/table.svg" + }, + { + "selector": "plx-badge", + "class": "PaletxBadge", + "import": "web/src/lib/components/status/index", + "category": "dataDisplay", + "label": "Paletx Badge", + "desc": "Paletx Badge", + "icon": "assets/icon/plugin-paletx-pro/status.svg" + }] +} + + +插件系统在拿到一个插件包之后,首先就要读取这个文件,通过它来获取插件的所有信息。因此,这个文件在插件包里的位置和名字必须固定,比如就放在插件包的根目录下,命名为 manifest.json 就可以了。 + +而二次开发的最主要工作,就是在安装好了 SDK 包之后,按照平台的规范,正确编写各个扩展点的代码。插件的开发工作一般不会特别难,但万事开头难,因此我建议你的平台可以根据不同的扩展点给出一些 Demo 插件,这样应用团队就可以在对应的 Demo 插件包的基础上,依葫芦画瓢完成剩余的工作。 + +注意,给出的 Demo 插件一定要是可以安装和使用的,否则它的价值就大打折扣了。 + +按照我的经验来说,编写的图文文档一般没人看,多数人还是喜欢直接照抄。在照抄的同时有不明白的地方,会直接来问,没几个人有耐心去搜索文档。所以,你可以把一些常见问题的解决方案,直接通过注释的形式,写在 Demo 插件的示例代码中。必要时,还可以在注释中放上一个超链接,导航到更详细的图文文档中去,这样的效果最好。 + +插件生命周期管理 + +最后,我们还要关注插件的生命周期管理的问题。一个插件的生命周期,大概有这些主要阶段:上传、安装、激活、使用、去激活、迭代更新、卸载。插件系统需要在各个阶段提供对应的通道和工具,支持插件更新和切换自己的状态。 + +上传比较简单。我们直接在开发平台上开放一个插件上传通道,这样开发者就可以将他的插件上传到系统中来了。 + +接下来,插件系统就需要对插件包做静态校验,读取 manifest.json 文件,并检查所有必要的配置项是否合法有效。校验通过之后,就可以把插件安装到插件系统中来了。这个过程主要是文件拷贝,在我们的实践中,主要是 JSBundle、NPM 包以及其他静态文件的拷贝,插件系统此时不会去使用或执行相关的代码。完成之后,插件后台管理器会给界面推送一个安装完成的提示,收到这个提示之后,应用就可以激活这个插件了。 + +插件激活时,插件系统就会实际使用和执行插件包里的代码了,所以这个操作对低代码平台的安全性会构成一定的风险。 + +需要说明的是,我这里并未对系统的安全性做特别的关注,因为我们现在主要面对内网用户,没有面向不确定的公众开放,因此我们没有将插件系统的安全性提到特别高的优先级,目前只做到单一插件死掉不影响系统和其他插件的最低程度,不考虑插件开发者有破坏系统的主观恶意。因为在完全实名的前提下,我们通过日志是很容易抓住破坏者的。 + +如果你的系统需要向不确定的公众开放,那么系统的安全将是一个非常重要的议题,需要重点关注。 + +具体激活插件时要执行哪些操作,取决于低代码编译器的架构,一般包含前端和后端两部分。前端的部分要把 JSBundle 从服务器下载到浏览器,然后 eval 一下就可以了,后端部分则是执行插件包的初始化代码,把 NPM 包的入口注册到插件包的功能入口上。 + +可以看到,这两部分都是有安全风险的:前端的激活容易遭受 XSS 攻击,后端在执行插件的初始化脚本时,有可能会执行到恶意代码。但我这里暂时没有防范的经验可以分享,如果你有相关的经验,欢迎在评论区分享给我们。 + +插件激活的时候,插件系统会把插件所提供的各个功能植入到低代码平台的各个环节,比如组件列表中增加对应的业务组件,动作列表中增加对应的自定义动作,获取数据的功能列表里增加对应的数据获取通道,等等。这些功能都植入好了后,我们就只需要等着应用开发人员按需使用就好了。 + +与激活和安装的流程相反,插件去激活和卸载的过程需要我们在相应的功能入口处删去植入的能力,删除插件相关的文件、包等内容。 + +最后,如果有余力,你还可以开发一个插件分发平台,用于集中管理插件,包括新增与删除插件、插件的在线自动发现、插件的版本升级,等等。如果你资源有限,又很需要这个功能,也不需要从零开始搭建,可以复用已有的功能,比如制品库和内部 NPM 镜像等。如果你们有内部网盘、论坛等,只要能托管文件,也可以加以利用,实在不行,搞个 FTP 服务器也成。 + +生态圈只是一个副产品 + +根据我们前面的分析,存量系统通过插件,可以将其数据与业务流程和低代码平台相连,应用团队通过插件,可以把业务组件、业务模板、方法和低代码平台连接。当连接的节点逐渐增多之后,你会发现原本相互隔离的人和数据之间,间接地产生了连接。 + +数据与数据之间、人与数据之间、人与人之间相互打通,形成了一个圈子。随着加入成员越来越多,每个成员可以从圈子里获得更多收益,同时也会吸引更多的成员加入,这是一个正向的增强回路。 + + + +这就是一个生态圈。一般生态这样字眼,会给人一种比较虚的感觉。但是我们这讲所谈及的插件系统是非常务实的,都是从实用以及如何解决存量系统之间的关系着手,也都是扎根于这样的目的。因为,我相信,在任何正常的企业中,低代码平台都绝不是第一个系统。在它之前,必然已经有形形色色的系统在跑在用。 + +低代码平台不能仅仅是另一个新系统,而是要成为企业的核心,通过插件连通各个存量系统,贯通数据,让存量数据创造更大的价值。从这个角度看,低代码平台实际上起到的是中台的作用。这一点我在第一讲也说过,低代码平台的演进线路有相当一部分与中台是同向,甚至是重叠的,这两者可以、也必须放在一起考虑。 + +虽然我们不着重谈生态圈这样的虚头巴脑的内容,但是,依托于插件系统,实际上低代码平台和所有存量系统之间,自然而然就形成了一个生态。低代码平台能帮助存量系统发挥更大价值,而存量则一步步将低代码平台推到企业的核心。连通的系统越多,低代码平台对企业的价值就越大、越重要,这是一种良性共生关系。 + +在和其他系统打成一片的同时,又能独善其身,不与任何系统耦合,只有插件系统才有可能达到这样的目的。 + +总结 + +今天这讲,我们从全流程设计插件系统的角度,分析了对插件系统的各个环节。其中插件系统的核心功能,也就是 SDK 的设计和功能,我们在【第 9 讲】和【第 12 讲】里已经做了非常详细的阐述,而插件系统的其他功能,都是围绕着 SDK 的架构设计和功能来打造的。 + +为了方便你更好地理解这些内容,我这里贴了一张 Awade 的插件系统架构关系图。我们一边分析,一边回顾这些知识点。 + + + +你可以看到,在编译器内核之上,是一层协议层。它是编译器对外的抽象,所有的扩展点都是由协议层来定义和约束的。SDK 则是建立在协议层之上的,它提供了编译器协议的默认实现,以及所有扩展点的基类的定义。这些基类不仅能在减轻二次开发的难度的同时,更重要的是也约束了插件必须遵守的编译协议,插件的二次开发只能按照协议所画出的套路来实现。 + +SDK 之上,就是各个插件了。从架构图上可以看到,内置功能和插件一样,也必须要遵守编译器协议。因此,从架构角度来说,内置功能与插件是平起平坐的,这样才能确保插件具有充分的扩展能力。当然,这只是从架构角度的设计,实际上插件能有多大能耐,还是取决于你的 SDK。这样就可以在实现层面上,保持内置功能的优势。 + +结合【第 9 讲】和【第 12 讲】的内容以及上面的架构关系,相信你已经可以掌握 SDK 的架构和设计的方法了。在掌握了 SDK 的架构方法之后,紧接着就需要考虑有哪些功能可以作为插件进行扩展。我认为插件系统可以在数据与模型、自定义组件、自定义动作等部位发挥显著的作用。其中,最重要的是数据与模型,通用型低代码平台要处理好存量系统的数据与模型的关系,插件是必须具备的能力。 + +思考题 + +我们假设,在你日常工作所要接触的各个系统和团队中,有这样一个插件系统可以打通所有的存量业务的数据。在这个前提下,互通的数据能创造出多少新的业务价值出来? + +欢迎在评论区分享你的想法。我们动态更新部分再见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/17|兼容性问题:如何有效发现兼容性问题?.md b/专栏/说透低代码/17|兼容性问题:如何有效发现兼容性问题?.md new file mode 100644 index 0000000..bcca205 --- /dev/null +++ b/专栏/说透低代码/17|兼容性问题:如何有效发现兼容性问题?.md @@ -0,0 +1,130 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 17|兼容性问题:如何有效发现兼容性问题? + 久别重逢,我想先问你一个问题:实现一个基础平台,技术上最难的事情是啥? + +面对这样的问题,相信你可以毫不犹豫列出许多难题,而且可能理由都非常充分。我给出的答案可能不太一样,我认为兼容性才是最难的。 + +兼容性问题,与其他问题相比,多了一个时间维度。也就是说,难度再大的技术难题始终都是一时的,解决了就是解决了,时间维度可以无视,但兼容性问题却不是这样的。它难就难在它是一个随着时间递增的包袱,滚雪球似的,终有一天这个负担会越来越大,直至压垮一个团队(无论技术多强,无论人力多少)。 + +低代码平台当然也有兼容性负担,不但有,而且非常重。设想一下,平台团队合入了一个 change,导致昨天还好好的 App 今天就跑不动了,这种事情搁谁那都说不过去。从我推广低代码平台的经验来看,兼容性是劝退应用团队的一个重要因素之一,甚至与适用性问题并列。毫不夸张地说,能否妥善解决兼容性问题,决定了低代码平台是否能走得长远。 + +要解决(或者规避)兼容性问题,可以分为两个主要步骤:发现和治理。今天这一讲我们先来聊聊如何发现兼容性问题,主要和你介绍下我正在用的三个方法,下一讲我们再从技术上聊聊如何有效治理兼容性问题。 + +代码走查 + +代码走查的作用有很多,再怎么强调都不为过,兼容性问题的发现,是代码走查的作用之一。 + +而代码走查机制的核心要素(没有之一)就是评审员,一名合规的评审员应该具备这样的素质: + + +深刻掌握被评审的子系统所用的语言、工具、第三方库等的各方面性质,对它们的长短处了如指掌; +深刻理解被评审子系统的全貌,大到架构、关键流程,细到各个功能的实现,最好是原创作者,次好是长期维护者; +最好是专职的,而不是临时抓来的人头,如果是临时参与评审,Leader 需要预留充分的时间,并计入业绩。 + + +代码走查的另一个要素是要避免自我走查。不能让代码作者既当运动员又当裁判员,这很好理解,但并非所有执行代码走查的团队都能做到。 + +这一点其实是在说,要有一套管理机制(甚至是文化)来促使变更的代码能被切实有效地阅读和思考。这点展开会是一个很大的话题,而且还是一个管理层面的话题,不是我们专栏的关注点,有机会我们再在别的场合聊聊。 + +那为啥开发人员发现不了自己的代码带来的破坏性变更,反而是需要别人来发现呢?有一句古诗你肯定听过:横看成岭侧成峰,远近高低各不同。 + +资历再深的研发人员,在修改低代码平台代码的时候,总会陷入细节,往往会专注自己眼下的问题,而对所做修改可能会波及到的部分考虑不足,所谓一叶障目,不见森林。而代码变更所带来的影响,往往会在某个意想不到的角落产生破坏性变更(即不兼容修改)。这是人之常情,我们只能承认这是一个无法避免的事实。 + +如果说资深研发人员未考虑到他修改的波及是一种不该发生的无意失误,对于资历较浅的研发人员所做的代码修改的波及,则不能用失误来解释,而是限于对系统的肤浅理解,他们根本就无法预知他们所做的修改可能会产生哪些不良影响,这些不良影响中的一部分,会体现为系统的破坏性变更。 + +但专业评审员在走查代码时,不仅会关注所修改的代码本身,还会自然而然地把这份修改放到系统全貌的上下文来看,于是,许多代码作者看不到的波及,会很自然地被发现。 + +DevOps 流水线 + +代码走查是一种兼容性问题的主动发现手段,这个方法对评审员的经验和能力有强依赖。我们都知道,在软件研发领域,最不可靠的因素就是人的因素了,因此代码走查的方式是不可靠的,仅凭代码走查手段也是难以发现所有兼容性问题的。 + +DevOps 流水线是提升软件研发效能的一个非常重要,甚至是必要的手段。它主要采用形式多样的自动化检查脚本,来发现软件研发过程中包括兼容性问题在内的各种问题,从而提升软件研发效能和质量。相对代码走查来说,DevOps 是一种被动式的兼容性问题发现手段。 + +我们可以在 DevOps 流水线上加入各种专门用于发现兼容性问题的脚本,在代码走查之前就把大多数低级的、已知的兼容性问题找出来。评审员则利用其知识和对系统的深刻理解,发现深层次的兼容性问题。 + +值得提示的是,在评审员发现了某个兼容问题之后,我们可以进一步思考是否可以将这个问题的发现过程脚本化。一旦脚本化,我们基本就可以杜绝相同问题再发生了,可以点滴提升流水线的可靠性。毕竟,同样的问题,评审员下次有可能看走眼,但脚本则不会。 + +那么,具体有哪些切实可行的兼容问题检测方法可以植入到 DevOps 流水线里呢? + +首先是 UT/FT。 + +理论上,如果每个函数每个功能特性的输入输出在代码修改前后都能保持一致,我们就可以认为系统是完全兼容的。但这只是理论,在实操过程中,代码行、函数(类),条件分支的覆盖率不可能做到 100%,但我们起码可以确保有 UT/FT 用例覆盖的那部分是能向后兼容的。 + +UT/FT 成本其实挺高的,为控制成本,我们采用的是这样的做法: + + +新模块新功能在初创时,评审员会要求各个功能点都要有一定比例的测试用例代码; +功能特性在后续迭代过程中,如果在某个函数上踩到坑了,我们就会针对这个函数补充对应的用例代码,避免再次踩坑。 + + +这是一个比较经济的方法,在不需要很高的覆盖率前提下,让用例代码最大发挥价值,你可以借鉴一下。 + +其次是各种代码扫描工具。 + +代码扫描工具可以很好地弥补 UT/FT 用例覆盖率不足留下的空白地带。虽然这些代码漏洞扫描工具一般都集成了数量不少的扫描规则,被这些规则命中的代码不一定都有问题,但出问题的概率会大幅增加,所以引入并重视这些漏洞扫描工具的报告,尽可能地将报告里的所有潜在问题项都改掉,也可以避免一定数量的兼容性问题。 + +需要预警一下的是,这些漏洞扫描工具往往是收费的,而且扫描工具的安装比较费事(虽然厂家会提供技术支持),扫描过程耗时耗 CPU。另外你第一次看扫描报告的时候,要先有心理准备,数以千计的乃至上万条漏洞报告很容易让你对自己的编码技术产生怀疑。 + +最后是静态检查。 + +看到这个点,也许你会马上想到各种 lint 工具,但我们团队并没有用。lint 工具确实可以减少一定的工作,但凡事都有代价,我认为使用现成的 lint 工具会被它的能力给框住,再考虑到全员的学习成本,实际是得不偿失的。 + +根据我们的经验,兼容性问题有一个特点,就是你永远无法预知它会在哪儿、以哪种方式出现。在代码走查的部分我说了,我们习惯了将兼容性问题的排查过程脚本化,兼容性问题的特点也就导致了检测脚本必须有多种不同的形式。 + +我们的静态检查脚本的形式可谓是不拘一格,有配置文件一致性检查,也有用正则暴力对源码进行检查的,正则搞不定时,还用 AST 解析深度检查的。能用 shell 脚本完成检查,就不会用语言,shell 搞不定或太慢的,再用其他方法(一般是 node.js)编写脚本来检查。一般的 lint 工具做不到这么灵活。 + +尽量将已知的兼容性问题脚本化是很重要的,这样可以避免在同一个坑里掉两次。这一点不仅是为了减少兼容性问题的发生,更主要的是协作方面的意义。一个问题第一次出现时,应用团队往往会抱以理解的态度,但如果同一个问题一而再、再而三地出现,对方就会很不耐烦了。 + +利用实际用户数据 + +先说明一下,这里的用户数据就是实际的 App 工程配置数据。在 UT/FT,或者 ST 时,构造被测数据往往是一个非常麻烦且令人头疼的过程,而且构造出来的被测数据往往不真实或者太简单,测不出问题来。那我们为啥不直接用户数据来测试呢?而且,对于低代码平台来说,用户数据天然就受到平台的管理,从库里复制一点数据来测试,简直不要太容易。 + +复制数据容易,但要找到质量较高的被测数据,则需要花一番功夫。 + +在我们的系统里,有许多 App 工程数据是开发人员的实验性工程(我估计这是一个普遍现象),这些工程是没有验证的价值的,需要排除掉。我采用了两个条件来排除这些工程。 + +第一个是根据基础指标来筛选,我采用了工程的编辑次数,模块数量等两个维度作为基础指标。根据经验,我设定了编辑次数大于 300 次,且模块数量至少为 2 的条件,作为筛选指标。你可以根据你的实际情况适当调整数值,或者按需增减其他维度。 + +不过,根据基础指标筛选到的 App 工程数据,仍然可能包含有问题的数据,此时我们可以引入第二个条件,来对它们进行更加细致的检验。我增加了低代码编译器的检验,只有能通过低代码编译器的编译的 App 工程数据才能进入下一个环节,用来校验新的修改是否有破坏性变更。 + +对于那些对没有更改生成的 App 代码的修改,我们可以直接使用筛选后的 App 工程数据进行校验。方法很简单,就是用修改前的编译器编译生成的代码与修改后的编译器生成的代码做比较,如果有修改,则表示可能会有问题,应该由专家评估是否是有破坏性变更。 + +对于那些会修改 App 代码的变更,则会麻烦许多。虽然也可以采用比较源码的方式来比对,但会比较麻烦,甚至需要将生成的代码解析为 AST(抽象语法树)之后再一一遍历比较,测试用例的开发难度较大、可维护性不好,所以我们团队没有采用。 + +我们使用的是 UI 自动化测试的方法,也就是把 App 直接跑起来,然后自动操作 UI 到某个状态,最后截图比较修改前后的差异。比较截图时,不是严格按照 100% 比较的,而是设定了一个 90% 相似的阈值,低于这个阈值就认为 UI 不匹配,从而报错。一般来说,一旦出现兼容性问题,UI 的差异会非常大,比如取不到数据,或者状态压根就打不开等。 + +这里需要补充一点儿背景,不然你很可能会觉得,采用 UI 自动化测试的方式难度更大,甚至压根就不可行。 + +这个背景就是我们的低代码平台 Awade 提供了自动生成 App 的 UI 自动化测试代码的能力(专栏的后续部分我会给出具体如何实现),只需开发人员配置待测功能点,以及预期数据即可。根据这些信息以及 App 工程数据的其他信息,就可以生成出 UI 自动化测试用例代码了。 + +在做兼容性测试的时候,我会先找有做过功能点自动化测试的 App,然后将这些 App 的自动化测试用例跑起来,然后在关键节点抓图比对。即使 App 所配置的 UI 自动化测试是错误的也没关系,因为只要待测修改的代码也能触发相同的错误就行啦。 + +虽然你现在不一定有条件利用 UI 自动化测试的方法来做兼容性测试,但至少可以对编译器做测试。即使是只做到对编译器的测试,只要你利用实际用户数据来测试,就能发现许多兼容性问题了,并且这些问题都是其他方式发现不了的。人工走查的方式不可能做到如此细致,也无法替代基于已有 App 工程数据测试所发现的兼容性问题。 + +小结 + +这一讲我介绍了多种我正在用的发现兼容性问题的方法,这些方法多数可以被直接借鉴。我们之所以要无所不用其极地从各个角度来发现兼容问题,是因为兼容性问题是一种比任何功能性问题(如功能缺失、bug 等)都更加麻烦的问题。 + +一方面,兼容性问题是一个低代码平台所有问题中最为劝退的一种问题。兼容性问题会给应用团队带来额外的对齐工作,而且这些工作往往是应用团队计划外的、被动式的工作。即使应用团队没有做任何修改(哪怕他们的 App 已经冻结版本),但是为了能够正常运行,他们不得不花额外的资源来对齐,这样的次数如果很多,他们必将萌生退意。 + +另一方面,没有被及时发现和处理的兼容性问题,会让平台团队陷入一种奇特的“怎么改都是错的”的境地,即使是那些非致命的兼容性问题,都会引起这样的后果。比如我曾经处理过一个 UI 安全边距的问题,就让我陷入了这样的境地。 + +某天我收到一个 bug:一个 App 的安全边距消失了。我们很快就查到是大概两个月前的一个修改导致了平台生成的代码没有自动加上安全边距,这就是一个典型的非致命兼容性问题。你有没有发现,此时此刻,无论我改掉这个 bug 还是不改,都会“得罪”一些人:恢复安全边距的话,这两个月里新增的 App 都将出现安全边距过大的问题;不恢复安全边距的话,两个月前创建的 App 都将没有安全边距。 + +之所以需要从不同角度切入来发现兼容问题,就和常见的一次性医用口罩的生产是一个道理。普通的纺织布是不能用于生产一次性医用口罩的,因为纺织布是由经线纬线规规矩矩交织而成的,两根线之间会更容易留下缝隙,病毒会很容易从这个缝隙进入口腔。而熔喷布则是把塑料融化后乱七八糟喷在基材上,只要满足一定的厚度要求,就不会留下缝隙了。同理,我们必须从各种差异很大的角度切入,采用差异很大的方法,尽可能多地发现兼容性问题。 + +但兼容性问题的发现是解决的第一步,下一讲我会详细介绍如何妥善处理兼容性问题。 + +思考题 + +相信你应该会有简单或复杂的 DevOps 流水线辅助你的日常开发,你现在正在跑的流水线任务里,有哪些任务是可以协助你发现兼容问题的?有哪些是稍加改造就可以用于发现兼容问题的呢? + +欢迎在评论区留下你的看法。下一讲你不会等待那么久,我会尽快更新。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/18|兼容性问题:如何有效解决兼容性问题?.md b/专栏/说透低代码/18|兼容性问题:如何有效解决兼容性问题?.md new file mode 100644 index 0000000..d010edb --- /dev/null +++ b/专栏/说透低代码/18|兼容性问题:如何有效解决兼容性问题?.md @@ -0,0 +1,278 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 18|兼容性问题:如何有效解决兼容性问题? + 上一讲我们提到,在软件的演进过程中,特别是在低代码平台这样的庞大软件工程中,兼容性问题,也就是破坏性变更是无法避免的。只要软件的代码有改动,就有可能引入破坏性,因此破坏性变更与软件的演进之间如影随形、不可分割。 + +破坏性变更所造成的后果,有可能微乎其微,也有可能是致命的。会造成严重后果的破坏性变更往往会受到重点“照顾”从而被妥善解决,那些没那么大破坏力的破坏性变更,一旦泄露到线上,不仅会造成体验问题,还有可能造成小范围功能不可用。 + +这一讲我们就承接上一讲的内容,重点讲讲如何妥善处理兼容性问题。当然,发现破坏性变更是解决它的第一步,具体我已经在上一讲中详细介绍了,所以我建议你在继续这一讲的学习之前,先回顾一下上一讲的内容。在讲具体如何解决之前,我们先来分析下低代码平台中一般会有哪些类型的破坏性变更。 + +有哪些类型的破坏性变更? + +Schema 数据结构的变更是低代码平台演进过程中最常见的破坏性变更。 + +我们都知道,低代码平台往往会采用结构化的数据来保存开发者在平台上所作的配置,这些配置数据就是 Schema 数据。Schema 的数据结构往往是随着低代码平台的功能迭代同步发生变化的,典型如某个字段一开始只要一个简单值类型就够用,后来扩展为一个包含多个属性的对象以适应日益复杂的功能;又如从一个单值扩展为一个数组,或者反之,从一个数组简化为一个单值;再如多个复杂字段的各个属性的拆分与再组合。 + +总之,Schema 数据结构的变更中,基本上除了新增属性外的任何修改都大概率会引入破坏性。根据我长期迭代的经验来说,Schema 数据结构的变更大约贡献了 50% 破坏性问题。并且,这个比例在低代码平台功能建设初期会更高,随着低代码平台的成熟度的增加而逐渐下降。 + +除此之外,我们再来看看常见的模版文件更新。作为一个开发平台,在生成一些程式化内容的时候,使用模板文件进行替换是一个常用手段,比如下面是一个简单的 index.html 文件的模板: + + + + + ${title} + + + + + ${someContent} + + + + +可以看到,模板里有一些 {scriptHref}等格式的文本,这些就是占位符。这个模板通常是这样使用的:处理程序或将它读入到内存中,并使用直接替换的方式,将所有的占位符替换为实际值,然后将替换后的内容写入到指定位置去,这个过程简称为模板的实例化。注意,实例化过程,模板原件不会被修改。 + +但是随着低代码平台的演进,模板的内容难免要更新,此时问题就来了:在模板更新前就已经实例化所得的文件内容是不会更新的。也就是说,已被实例化出来的文件不能用了,需要强制对齐。所以,模板文件的更新,也会引入破坏性。根据我的经验,这个类型的破坏性大概占 20%。 + +不过这些都是平台内部的破坏性,那平台外部导致的破坏性会有哪些呢? + +显然,现代的软件底层都是由一堆第三方软件堆起来的,低代码平台也是如此。任何第三方库的升级,特别是跨大版本升级,都有可能引入破坏性,比如从 Vue2 升级到 Vue3,或者从 Angular8.x 升级到 Angular10.x 等。低代码平台所用的组件集如果有破坏性变更,基本上都会直接传递给应用工程。 + +与前端第三方库升级会带来破坏性一样,服务端 API 的演进也会带来破坏性。而且由于这些 API 往往是兄弟单位内部自研,相比开源库来说,自研功能在兼容性方面的考虑和投入会少很多,甚至完全不考虑兼容性,在上线前能够想起来发一个强制对齐说明邮件就算做得很到位了。 + +这些外部环境或依赖所引入的破坏性变更大概占了 20%。总体来说,低代码平台内部演进所产生破坏性变更占了绝大多数。 + +if else 方案 + +在上一讲以及这一讲的前面部分,我们完成了解决破坏性的第一步:了解和发现它们,接下来要做的就是解决它们了。低代码平台迭代的过程,任何修改都有可能引入破坏性,一个最容易想到的办法是大量使用 if else。 + +我们先来看下图,这是一个没有任何破坏性的低代码平台的迭代过程,相当丝滑: + + + +当发生了第一个破坏性(记为 A)的时候,这根直线就会产生一个分叉: + + + +那么,这个分叉点当前输入的数据是来自破坏性发生之前,还是在破坏性发生之后呢?显然不同情况下对数据处理方法是不同的(哪怕是细微的)。识别起来很简单,伪代码大概是这样的: + +if (before(A)) { + ... +} else { + ... +} + + +这里的 before(A) 是用来判断数据是否来自破坏性变更之前的,需要根据破坏性 A 的特征来编写 before(A) 的逻辑。一般来说,多数破坏性都有显著的特征,因此它的实现难度不大。为了叙述方便,橙色线条表示破坏性发生之后,而蓝色线条表示破坏性发生之前,以下都采用这个规则。 + +接下来继续迭代,我们又引入了一个新的破坏性变更,记为 B,此时又会出现一个分叉: + + + +这时情况变得复杂了一些,但看起来,再加一个 if else 还是可以搞得定。接下来再发生新的破坏性 C,也一样处理: + + + +到这里,你可能已经发现一些规律了,似乎只要判断一个数据是来自哪些破坏性之后,就可以彻底解决破坏性问题了。 + +真的是这样吗?思考一下,破坏性变更 B 在处理破坏性 A 之前产生的数据(图中蓝色分支)时,它的判断条件和在破坏性 A 之后产生的数据(图中橙色分支)的判断条件是一样的吗?答案是:在蓝色和橙色分支下的判断逻辑很有可能是不同的! + + + +甚至,破坏性 B 在蓝色分支下是否还存在都是一个疑问。同理,在橙色分支下没有破坏性的修改,在蓝色分支下是否依然没有破坏性,也是一个疑问。 + +基于这两个疑问,我们可以这样认为,上图中的B和 B’实际上不是同一个破坏性,而是两个相互独立,互不相干的破坏性,因此,在引入的时间上,破坏性B和 B’也没有关联(有可能 B 早于 B’发生,反之亦然)。所以上面这个图需要做一些修改才能更贴近实际情况: + + + +看到这里,你是否觉得这个问题一下子就变得非常难了。第一个破坏性出现时,用一个 if else 就可以搞定了(2 个情况),当第二个出现时,判断逻辑就有了 2 个条件(4 个情况),第三个破坏性出现时,判断逻辑就有了 3 个条件(8 个情况)。这个负担是随着破坏性个数为幂爆炸式增长的,大量使用 if else 是无法解决问题的。 + +先别气馁,根据前文的分析,虽然图中的蓝色和橙色分支发生破坏性的时机和判断方式没有必然的关联,但这一点反而带来了好消息,我们不难得到这样的推论:蓝色和橙色这两种情况的处理方法却是等效的。也就是说,假设有一个方法可以妥善处理橙色分支的破坏性,那么这个方法也可以直接应用到蓝色分支上,从而解决蓝色分支上的破坏性问题。 + +这就是解决问题的突破口,但此时我们还无法继续展开。在介绍新方案之前,我们还有一个重要的事情要做,那就是标记破坏性变更。 + +之所以我们要先对破坏性变更做标记,是因为我们在拿到任意一份用户数据时,需要通过标记快速找到这份数据里已经包含了哪些破坏性变更,这样才能想办法将它们逐个妥善解决。反之,如果不先做标记,在拿到一份用户数据时,我们就必须编写大量的 if else 逻辑来检测这份数据包含了哪些破坏性变更,根据前面的分析过程,这是不可行的。 + +标记破坏性变更 + +破坏性的引入显然与时间有关。所以,我们在这个图上引入一个时间维度,会有助于解决问题,我们再把各个破坏性投影到时间轴上,得到这样一个图: + + + +看起来,我们可以使用时间戳来标记破坏性。这里的关键是,在拿到任何一份应用数据之后,如果能知道这份应用数据是啥时候生成的(如图中的灰色箭头),再与这个时间戳做一下比对,我们很容易就可以知道这份应用数据已经包含了哪些破坏性变更,未包含哪些破坏性变更了。 + +但是,时间戳并不是完美的解决方案,一份代码只要没有修改,那么、在任何时候,基于这些代码所构建的版本做出的破坏性的标记,也必然是没有变化的,但是在不同时刻进行构建,时间戳却是不一样的,这就产生了矛盾。 + +那么,有没有一种具有时间戳的优点,又能避开它缺点的方法呢? + +我们会很自然地想到低代码平台的版本号。首先版本号随时间递增的性质与时间戳相似,其次,如果代码不做任何修改,版本号也就不会改变,这个特性就可以绕过使用时间戳的短板了。 + +但其实版本号也不是解决这个问题的完美解决方案,使用版本号来标记破坏性变更,会有一个小副作用:一旦某个修改带有破坏性,就必须基于该修改发布一个新版本。但是软件的版本号一般是有规划的,随意发布版本有时候并不可行。 + +不过,要解决这个问题也不难,我们可以定义一个内部版本号,与对外发布的版本号分开就可以了。这个内部的版本号可以使用一个递增的数字就够了,每构建一次就 +1,无需设计得太复杂。有许多软件的研发过程,天然就会有内部构建号,此时我们可以直接使用内部构建号来标记破坏性变更。比如我正在用的 Windows 的内部版本号是: + + + +再如我正在用的 Sublime,也有类似机制,左侧的是对外的版本号,右侧是它的内部构建号: + + + +不过,在我们的低代码平台 Awade 里,我们并没有采用内部版本号,而是直接使用正式版本号来标记破坏性变更。因为对一个在线软件来说,它的版本号实际上并不重要,开发者任何时候刷新浏览器都有可能拿到新版本,所以大家并不关注版本号是多少。 + +如何解决破坏性? + +好了,有了前面的铺垫,我们现在终于可以给出解决方案了,核心思想就是化整为零。这时我们再看下前面那张图: + + + +这张图看上去会这么复杂,就是因为当第一个破坏性变更出现的时候,我们对它置之不理导致的。如果每出现一个破坏性,我们都立即进行版本的一致性处理,那么这张图上的情况就不会出现了。 + +具体做法是这样的:每当出现一个破坏性变更的时候,我们都配套给它编写一个处理器,这个处理器的作用是消除掉这一个破坏性。这里关键是破坏性变更对应的处理器的实现,处理器的输入是变更前的用户数据,处理器的输出是将用户数据调整为符合破坏性变更后的数据格式。这样说比较抽象,我们通过下面这张图来进一步解释。 + + + +和前面其他图一样,圆圈 A 代表一个破坏性。不难理解:只有在当用户数据是版本 V1 时,破坏性 A 才存在,当用户数据是 V2 时,破坏性 A 对这份数据来说是不存在的,这一点非常重要。而前面我们说过,我们会给破坏性 A 配套编写一个处理器,这个处理器的作用,就是需要将格式为 V1 的用户数据,升级为 V2 格式的用户数据。所以,对于破坏性 A 的处理器,它的输入是 V1 格式的数据,输出是 V2 格式的数据。 + +当有新的破坏性出现的时候,我们重复这个过程,就可以实现从 V2 升级到 V3,V3 升级到 V4 了: + + + +每一个处理器都只要关注处理一个破坏性,而无需关注其他破坏性,一个破坏性的内容是确定的,因此处理逻辑也是确定的,因此很容易将一个处理器做得非常健壮。 + +不过,不同的用户数据的有可能是在不同时间使用不同版本号的低代码编辑器来创建的,这意味着不同的用户数据可能会有不同的版本号。比如下图的灰色箭头,代表着某一笔用户数据创建的时刻: + + + +显然,在箭头左边的破坏性对他来说是没有影响的,但右侧的 B/C 破坏性是有影响的。所以,对于这笔用户数据,我们需要按照顺序,依次执行破坏性 B 的处理器,再执行破坏性 C 的处理器,这样,这笔用户数据就可以被稳妥地升级到最新版本了。 + +通过这个方法,我们就可以做到将任意老的用户数据,稳妥、可靠地升级到最新版了,这就是化整为零的含义。所以,一个多年前做了最后一次编辑的应用工程,即使在这么长时间内低代码平台不断迭代,引入任意数量的破坏性变更,但这个应用工程也可以随时被打开再次编辑。 + +有了通用的解决思路后,我们再针对前面说过的三类最常见的破坏性变更,具体分析下各个类型的破坏性变更如何处理。 + +Schema 数据结构的变更 + +我们先来看 Schema 数据结构的变更造成的破坏性,案例如下: + + + +这是典型的迭代过程产生的破坏性的例子。一开始某个属性是一个简单值,后来扩展为一个复杂的对象。如果不采用处理器来做一致性处理,那么所有使用这个属性的代码都必须要永久做 if else: + +if (typeof v.property == 'string') { + ... +} else { + ... +} + + +但如果采用这一讲的方法,我们只需要在处理器里直接把 property 属性的结构从字符串扩展为一个对象即可,处理的过程需要给 type 只设置一个当前的默认值就好了。 + +我们再来看另一个例子: + + + +这里,某个功能在迭代的过程被简化或者裁剪,把 type2 和 type3 都裁剪掉了,只留 type1,且 property 的值的结构也随着被简化。在这个情况下,处理器就可以直接把结构简化,这样 V2 及以后的代码就可以“忘记”property 属性曾经是一个数组了。 + +Schema 数据结构的变更可以举的例子非常多,但基本大同小异,要么就是结构变复杂了,要么就是结构变简单了。实际上,任意的结构调整,都是可以采用处理器来完成一致性处理,从而使得使用对应数据的代码无需 if else,从而大大简化代码。 + +模板文件的变更 + +随着低代码平台的演进,模板文件内容发生变化是很正常的,比如前文的 index.html 的例子,有可能会增加新的脚本引入语句: + + + + + ${title} + + + + + + + ${someContent} + + + + +处理器消除这样的破坏性变更就更简单了。对这类问题,基本都是只要把新的模板拷贝到对应位置去即可。当然,这类破坏性变更往往会伴随着前面介绍的“Schema 数据结构的变更”,此时,一个处理器里,就需要同时处理多种不同类型的破坏性类型了。 + +外部破坏性变更 + +在 Pro Code 纯代码开发模式下,外部破坏性问题只能手工解决,但在低代码平台,多数情况下,我们也可以做到悄无声息地把外部破坏性变更给解决掉。 + +比如低代码平台所依赖的按钮组件的 API 出现了破坏性变更,按钮的文本从原来的 text 改为了 label: + + + + + + + +在收到强制变更说明之后,我们只要为此开发一个处理器,找到 App 里所有带有 text 属性的按钮,并自动将属性名从 text 改为 label 即可。 + +注意,虽然这种破坏性是在组件,但实际应该修改的是 App 的 Schema 数据。这其实非常好理解,对低代码平台来说,App 的 Schema 数据就是它的源码,低代码平台在消除外部破坏性所做的动作与纯代码所要做的动作相似,都是需要修改各自模式下的源码。 + +这个例子比较简单,但无论再怎么复杂的第三方库的破坏性的处理,都是类似的思路和做法。比如,你需要写一个升级器将 App 使用的 Vue 从 2.x 升级到 3.x,这是一个复杂的过程,但思路和这个例子一致:先实验或者阅读破坏性变更说明书了解到需要手工做哪些调整,然后找到 App 的 Schema 数据里的对应位置,做相应调整。 + +我用这个方法自动将 Angular(这是我在用的技术栈)从 4.x 升级到 8.x,后来又从 8.x 升级到现在的 9.x 版。Angular 的大版本升级更加麻烦,Webpack、Typescript、RXJS 等各种第三方库也要一起调整,但这个方法在两次升级过程中都表现得很好。 + +服务端 API 的破坏性变更是家常便饭,特别是第三方数据源的破坏性变更,非常不可控。这类破坏性变更的处理方式与前端第三方库的解决方式如出一辙。比如一个 Rest 服务返回的数据结构,以前的结构是一个二维数组: + +[ + [11, 12, 13, 14], + [21, 22, 23, 24], + [31, 32, 33, 34], + [41, 42, 43, 44] +] + + +某天突然发生变化了,变成了: + +{ + header: ['Header1', 'Header2', 'Header3', 'Header4'], + data: [ + [11, 12, 13, 14], + [21, 22, 23, 24], + [31, 32, 33, 34], + [41, 42, 43, 44] + ] +} + + +你看,这里的结构改成了一个 json 对象,且增加了 header 属性,原来的数据被挪到 data 属性中了。编写处理器处理这样的变更,方法和前面介绍的例子别无二致,直接在 App 的 Schema 里筛选出所有使用到这个服务的地方,把处理的逻辑做一些调整即可。 + +那么,有没有确实无法解决的破坏性问题呢? + +有的,在我们研发低代码平台的几年里,曾碰到过三四次这样的情况。比如最初的一次是我们重写了编辑器的底层逻辑,导致老版本的 Schema 相比新的 Schema 缺少了非常多的属性,我们尝试一一给出经验值,但效果仍达不到预期。 + +再如最近的一次是因为 UX 设计规范有大幅调整,引入了大量的 CSS 方面的破坏性,由于细节太多,几乎不可能枚举完整,我们把能想到的细节都一一用处理器消除了,但实践表明,依然有 30% 左右的 CSS 细节泄露出去了。 + +对于这类破坏性,我们采用的解决方法简单粗暴:升级大版本,并对外宣布这个破坏性无解,需要手工处理(我们会给出详细的处理方法,以及配置专人协助升级),一般在低代码平台做重大版本升级时,会采用这个方法。但这样做需要非常谨慎,要尽量避免,并且要与各个应用团队达成共识。 + +小结 + +根据我的经验,常见的破坏性变更有 Schema 数据结构的变更、模板文件的变更、外部破坏性变更等,这几个类型基本占据了我碰到的破坏性变更总数的 90% 左右。 + +虽然单个破坏性变更所产生的负面影响(兼容性问题)一般很有限,但兼容性问题也很麻烦,其他难度再高的技术问题,都是一时的,解决了就解决了,兼容性问题最大的难点在于它产生的负担会随着时间不断累积。这一讲前面的这个图,可以很好地说明这一点: + + + +任何一个破坏性都会导致处理用户数据的逻辑产生分叉,每一个分叉则有可能使得之前已有的判断逻辑翻倍,最终导致判断逻辑复杂到再也无法修改的境地。 + +根据我的经验,大概平均每 40~60 个修改会引入一个破坏性,一年能累计 20~30 个破坏性变更。所以如果没有一个有效的方法来妥善解决兼容性问题,任何团队都将迟早被它压垮。最直观的表现是代码没人敢改,没人知道为啥正确,也没人知道为啥出错。 + +这一讲给出的办法是,将兼容性问题化整为零来处理,每个破坏性变更的发现,都必须开发一个配套的处理器,用于将兼容性发生前的用户数据升级为兼容性发生之后的格式,这样就可以及时地处理掉破坏性。而且,更关键的是,一个处理器与一个破坏性修改是配套的,因此处理器只需要专注于处理 1 个破坏性即可,这就可以让处理器的实现足够简单,足够健壮,足够好维护。 + +要妥善解决破坏性问题的前提是需要能及时、稳妥地发现它们,这不只是一个技术问题,需要技术 + 管理两个手段双管齐下,无所不用其极地从各个角度采用各种形式来发现,具体的经验我一讲总结在了上一讲里了,希望你能回顾一下上一讲,并结合自身团队的经验,总结一套适合自身的发现破坏性问题的方法论。 + +思考题 + +你曾经碰到过的印象最深的兼容性问题是啥?它造成了啥后果?最终又是如何被解决的? + +欢迎在评论区里留下你的故事。我们下一讲再见。 + + + + \ No newline at end of file diff --git a/专栏/说透低代码/总结与展望|低代码之路,我们才刚刚开始.md b/专栏/说透低代码/总结与展望|低代码之路,我们才刚刚开始.md new file mode 100644 index 0000000..65c2c0e --- /dev/null +++ b/专栏/说透低代码/总结与展望|低代码之路,我们才刚刚开始.md @@ -0,0 +1,85 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 总结与展望|低代码之路,我们才刚刚开始 + 好快,这个专栏的常规更新部分到这里就正式结束了。恭喜你打败了学习的惰性,一路坚持到这里。现在,我们是时候停下来稍微总结一番,以便更好地开启后续的学习。 + +创建好了这篇文章的空白文档后,我的脑袋和新建的文档一样空白,思绪一下子飞跃到了四年前,我决定先和你分享下我是如何与低代码结缘的。听听我的故事,希望它能给你树立一些信心。 + +如果非要给我的低代码之旅设定一个明确的起点,那应该是 4 年前的 3 月,在我收到那封低代码相关的封闭研讨邮件的时候。但在这之前,我就建设低代码工具这个问题,与其他兄弟单位有过接触。受限于资源,双方决定采用分工合作的方式,我们负责 Web 组件,对方负责低代码引擎。 + +那时我们在成都组织了多次探讨,我现在许多关于低代码的设计理念,都是在这些探讨过程逐渐形成的。但可惜双方理念相差较大,最终没能走到一起。对方无法接受我先通用再具体的方案,执意要“务实”地对具体场景提供支持,因为这样“风险小、见效快”。 + +后来再收到这封闭关研讨的邮件时,我就明白老板这是下决心要自己搞了。 + +随着研讨越发地深入,我越觉得这个事情难度巨大,甚至,我还一度因畏难情绪和其他因素萌生了退意,但看着这个我当时花了近两年给老板画出来的饼,已经在我嘴边了,只要一张嘴就可以咬上一口,最终我还是选择了坚持。我至今仍在庆幸当时的选择。 + +从最开始设计 Awade 的编译器时,担心纯可视化搞不定,选择让编译器能够生成出人类可读、可改的代码;到后来慢慢走上正轨,解决了开发过程中大大小小无数的难题;再到现在,脚下的低代码之路已经逐渐变得平坦且宽阔。从最开始受到的各种质疑,到现在,进度紧急时,经理们会特地点名必须使用 Awade 以确保其交付进度是可控的。 + +一时间,感慨万千。 + +最后的事实证明,先通用、再具体才是正路。通用性创造的是未来的可能性,虽然早期道路坎坷艰辛,但我们还是坚持下来了,路是越来越平坦的。反之,如果你过于着眼于眼下,看似务实,实际上更容易因架构设计的韧性不足,导致路越走越窄,举步维艰。我一直非常注重竞品调研,我搜遍公司内外各种竞品,做了很多调研,后来成都那个团队的产品也没有再出现在我的竞品清单中。 + +在这个专栏的交流群中,我看到很多同学都是一线低代码平台的设计者和实现者。可能很多和那时候的我一样,或是正绞尽脑汁给领导画饼,或是正迷茫在架构策略的抉择中,又或者是正受困于某个技术难题,不知如何向前。 + +我非常希望这个专栏能帮助你解决眼下的困境,也非常希望你能坚持一下,再坚持一下,走到最后。如果你在此过程中有啥困惑或难题,欢迎来找我。 + +课程设计的背面 + +时间拉回今年春节前,极客时间团队的小伙伴开始和我接触,邀请我来写低代码这专栏。我当时的第一想法是非常兴奋,不谦虚地说,我是国内最早开始“吃”低代码这只“螃蟹”的一批人,坚持到现在,已经超过 4 年了。在此过程中我积累了大量的实战经验,极客时间是整理和展示我这些经验的一个很好的平台。 + +兴奋之余,我又感觉非常为难:具体的内容应该如何编排呢?毕竟,低代码技术是一个综合性非常高的话题,低代码涉及的方面非常多,可以讲的内容也非常多。我们可以从实际应用的角度来讲,也可以从系统架构实现的方向来讲。同时不同的方向上还可以有不同的侧重,不同的方向和侧重,面对的人群还不一样,适用的内容也不一样。 + +所以,我花了非常多的时间纠结到底怎么挑选内容。最终,我选定了低代码平台的架构和实现这个大的讲解方向,同时偏重于系统架构的设计,忽略掉过细的实现细节。 + +那么,在低代码的架构和实现这个大方向下,要选择哪些功能和模块进行讲解呢? + +经过一番精挑细选后,我决定围绕着低代码编辑器这个功能来打造这门课。低代码编辑器是低代码平台最关键的功能。它的能力和实现的质量,直接决定了低代码平台的能力和质量。虽然低代码平台综合性很高,可以做的功能点众多,在各家企业里落地时,大家都各有侧重,有的先解决服务端侧,有的先解决前端侧的问题。但无论你侧重建设哪个端的功能,低代码编辑器都是绕不过去的一个功能点。 + +这个专栏的常规更新部分的主体内容,就是按照这样的考虑来设计和编写的。 + +但是,我们当然不可能一上来就单刀直入,直接剖析低代码编辑器,还是要遵循学习知识的一般认知流程。所以我将内容分成了三大部分,先从认知基础与架构策略切入,着重介绍了低代码的演进策略和低代码编译器与编辑器之间的关系。 + +我希望你在开工之前先想好路怎么走,这比啥都重要。你要先想清楚你负责的子系统与上下游子系统的关系,若保持强耦合,对方值得托付吗?还是留个活扣,给自己以后多一个选择?特别是,你不要急着编码,一旦开始写代码,就容易陷入细节,不能自拔了。写代码永远是最容易的一件事,前提是你真的想清楚了。 + +接下来的第二、第三部分是我们常规更新的重点内容:低代码编辑器的架构和实现,以及低代码平台的拓展。这些内容都比较硬核,因为我们很难用短短几千字讲清楚一个架构知识点,而且抛开业务谈架构都是在耍流氓,你可以看到,每一讲中我都非常注重业务场景的讲解,也针对不同的业务场景给出了一些建议。你可以结合自己的实际情况辩证地看。 + +我注意到,前两天交流群里有同学提到,每一讲的文字量都很大。其实,我之前写文字稿时就注意到了这个问题,我往往需要用目标篇幅的 1.5 倍左右才能完成一讲的内容,常常才写完第一个小标题时就发现已经用掉了大半的目标篇幅了,不得不回头再精简内容,但总是觉得,少了这块不行,少了那块也不行。 + +即使我“精打细算”地使用好每一讲的篇幅,但你应该也可以发现,有些内容依然是蜻蜓点水般一带而过了,没能讲透,特别是第三部分的两讲更是如此。这些没涉及、没展开、没讲透的内容,就要留着在这个专栏的动态更新部分才能展开了。 + +动态更新是极客时间的一种创新,特别适合学习低代码这样综合性高、内容繁复多样的知识。和常规更新阶段事先敲定内容的学习形式不同,动态更新阶段中,你可以一边学习,一边和我互动,我会针对你的学习状况和需求,动态设计和组织剩下的内容,达到定制化的学习效果。 + +动态更新内容,将由你来定制 + +那么动态更新部分,将会有哪些内容呢? + +首先,这一阶段最主要的是把在常规更新部分里的那些“等有机会…”“下次再说…”的部分补齐。你可以将这个课程看成是一棵树。常规更新部分是大树的树干和部分枝丫,而动态更新部分是沿着树干生长出的各个重要枝丫; + +其次,这一阶段我们要尝试跳出以低代码编辑器为中心的思路,尝试围绕其他角度,来完成低代码平台的架构和实现。因为低代码编辑器并不是低代码平台的全部,它只是一个核心部件,低代码平台还有其他许多有价值的功能需要建设; + +最后,我们还会对业界进行持续跟踪和观察,包括但不限于开源社区、调查机构、甚至还有一些竞争对手的资料,但这个角度的内容篇幅不会很大。 + +动态更新内容的大方向,将和常规更新的内容保持基本一致,我将继续保持在低代码平台的架构和实现这个方向上进行讲解,不会做方向性的大幅度调整。 + +不过,此时此刻,动态更新的内容还没完全定型,你我可以一起来设计剩余的内容。欢迎你把想要学习的内容写在评论区、交流群,或者这个【调查问卷】中,我们一起来设计剩余的内容。 + +正如在开篇词中向你承诺的那样,动态更新部分将有 20 讲左右的内容,大致以每个季度至少一讲的频率,每年更新 5 讲,持续 4 年。衷心邀请你和我一起完成这场“长跑”,在接下来的学习中,不仅紧追低代码的前沿“脉动”,更重要的是在逐渐深入的学习和实践中,完善低代码的知识体系,提升自己的架构能力。 + +这里先预告一下,接下来动态更新阶段的第一篇文章将在今年 7 月更新,一定要记得回来学习呀,我也会在交流群里通知你的! + +写在最后 + +最后,我还想跟你说个题外话。有可能你曾经去词典里搜索过“awade”这个单词,想必是一无所获。因为这是一个我自创的单词,它来自“Anyone can be A Web Application Developer Exper”这句话的首字母,翻译过来就是人人都是 Web 应用开发专家。 + +这就是 Awade 的使命,也是当时在研讨和设计 Awade 过程中,大家共同的愿望。作为一个低代码平台的设计和实现者,我们希望在它的帮助和赋能下,人人都可以成为一个 Web 应用的开发专家,而不是仅仅只有那些掌握了 Web 研发技术的职业开发人员才能成为专家。 + +现在,我把 Awade 的架构和实现方法整理出来了,希望你通过对这个专栏的学习,也能设计出一个低门槛、高效率的低代码开发平台 ,赋能它的使用者,让大家都成为 Web 应用的开发专家,为业务实现真正的降本增效。 + +探索低代码之路,我们才刚刚开始,就让我们一起继续研究,探索出更好的低代码实现之路吧! + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/00开篇词为什么每个测试人都要学好性能测试?.md b/专栏/说透性能测试/00开篇词为什么每个测试人都要学好性能测试?.md new file mode 100644 index 0000000..0f9bd76 --- /dev/null +++ b/专栏/说透性能测试/00开篇词为什么每个测试人都要学好性能测试?.md @@ -0,0 +1,194 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 00 开篇词 为什么每个测试人都要学好性能测试? + 你好,我是周辰晨,欢迎来到《说透性能测试》。 + +我从事测试工作 8 年,曾就职于京东、平安、易果生鲜等公司,目前在某大型数据科技公司担任测试专家,主导全链路性能测试及测试平台开发相关工作。其间,我通过问题总结出的种种单点经验逐渐转变为自己的方法论,我也渐渐跳出了原来对性能测试的局部认知,开始从全局的视角来看待性能测试。 + +像京东这样的头部互联网公司,网站承载着数以亿计的客户群,自然对性能有着很高的要求。在京东的工作也让我对性能测试有了更深的理解,性能测试并不只是要一个结果,更多的是要从部署结构、代码链路、业务上下游等多角度来综合考量。 + +后来,我又经历了从 0 到 1 搭建性能测试体系的全过程,保证了双十一峰值期间 500 万下单量下 App 的正常运转;同时,这套测试体系也能够有效承接每分钟 150 万的访问量。 + +为什么要学性能测试 + +目前,最成熟的性能测试从业者一般都分布在各大互联网公司,这些公司对性能有着切实的需求,也具备深耕性能测试技术的土壤,所以往往能培养和聚集一批优秀的性能测试从业者。 + +那是不是说,其他公司就没有性能测试需求了呢?并不是的。 + +这两年,测试开发这个职位火了起来,许多公司招测试时都是在招测试开发。虽然招聘的不是专职的性能测试人员,但任职要求水涨船高,往往都需要你能够进行非功能测试,如性能测试、自动化测试。 + +我们来看一则快手的招聘信息,其中就明确要求求职者有性能测试的相关经验。 + + + +招聘信息来源拉勾网 + +我也看到很多测试同学会在简历上写:“熟悉 JMeter 的基本使用和性能测试。” + +但当我在面试时问:“性能测试的基本过程是什么?”很多人说“我就是用 JMeter 做了脚本”,至于“如何监控数据?”“需要监控哪些数据?”这样的问题,回答就更是模糊不清了。 + +下面我列举了几个其他的常见问题,你也可以对照自检: + + +只会使用 JMeter 但执行却不规范。在性能测试过程中,工具使用不恰当会影响到性能测试的结果。我见过很多因为工具使用不当导致的客户端瓶颈,让处理能力未达到预期的情况。很多测试没能及时发现是工具的原因,导致自己的专业能力备受质疑。 +不会制定有效的性能测试目标。如果不会制定有效的性能测试目标,那测出来的数据也没有什么参考价值,因为你不知道能不能满足上线需求,也不能准确地评估线上风险,做完了性能测试依然留有一大堆问题。 +不会定位和分析性能测试结果。测试脚本得到的数据并不能直接用来分析系统瓶颈,你只有通过监控去观察系统存在的异常点,然后根据异常点来重点监控相关组件,由表及里、层层深入才能找到根本原因。 + + +事实上,现在很多人做性能测试只是在用工具写脚本、跑压测,最后出来一个结果,至于什么是性能测试,性能测试的过程是什么样的,性能测试目的是什么,缺少系统性的认知。 + +性能测试真正的价值,并不在于你用工具完成了一份报告,而是通过对过程和结果的分析找到症结,帮助团队有效提升产品性能,比如提升了多少 TPS,降低了多少响应时间,节约了多少硬件成本,等等。 + +因此,我希望用这门课把性能测试的全过程讲给你听,不只带你玩转工具,学会制定一个有效的性能测试方案,更在把工具做到极致的基础上,和你分享如何监控数据才能迅速定位问题,如何做性能调优,攻克性能测试的重难点。 + +课程设计 + +性能测试中的很多标准其实都是非常主观的,你在网上看到的很多推导公式、二八原则之类的概念,如果不结合业务实际,盲目地学习,然后把这些作为性能测试的标准打开方式,很可能是有害无益的。 + +因此,本课程注重实战,我将以真实的互联网使用场景为导向,帮助你建立一个体系化的性能测试认知,分工具使用、场景分析、监控搭建和问题定位分析实践 4 个模块,为你全面展示性能测试的整个过程。 + +模块一:性能测试的工具原理与使用。JMeter 是目前最流行的性能测试工具之一,它具备较为完善的基础功能,还具备丰富的可拓展性,因此这一模块我将带你玩转 JMeter。 + +你在这里不仅仅能学到如何使用 JMeter,还能学到 JMeter 的二次开发和调用 JMeter 的 API 完成性能平台开发的基础步骤。二次开发可以让你了解如何通过 JMeter 提供的接口进行拓展,实现自己的定制化需求,而掌握平台化的操作可以极大地提高团队协作效率。 + +模块二:性能测试目标与场景分析。这个模块可以开启你从使用工具做性能测试到专业化性能测试的进阶之路。 + +我会聚焦正式开始性能测试之前应当明确的事情:如何制定性能测试指标;参考数据有哪些,怎么获取;常见的性能测试场景有哪些,如何通过这些场景来提高性能测试的覆盖率,等等。这些都是性能测试方案的组成部分,只有制定了正确的性能测试方案才能做出有效的性能测试。通过这一模块的学习,你可以理解性能测试的每一步,而不只是机械地执行上级派给你的任务。 + +模块三:分层监控体系建设。这一模块的重点是监控和问题定位,包括如何做硬件监控、系统链路监控,如何打造可视化的监控报表。监控是性能测试必要的步骤,是你发现性能问题的“眼睛”。 + +模块四:性能分析优化实践。我在前面提到,性能测试的标准常常是主观的,过往经验有时候不能照搬。因此,我会从服务端、中间件、数据层三个角度带你了解如何定位和优化问题,希望你看完以后可以结合自身工作场景进行性能调优。 + +很多公司担心直接在生产环境进行性能测试会影响用户体验、污染线上数据,其实这些都不是问题。我会从线上全链路性能测试的开展、组织和注意事项等多个维度来展开介绍,为你更好地实践提供思路。 + +讲师寄语 + +测试需要掌握越来越多的技能。对你来说,能多学会一门技能就可以胜任更多的工作,更可以“去同质化”,拥有更强的竞争力。 + +而且,性能测试作为非功能测试,其实是一个非常有价值、有成就感的工作,当你遇到性能瓶颈时,不是简单地说“去硬件扩容”。如果你的建议不只是简单地增加服务器成本,而是能够通过自己的定位和分析,以及一轮轮的调优和测试提升系统处理能力,一定更能够彰显你的技术视野,体现你工作的价值。 + +课前导读 | 性能测试全流程,你需要注意什么? + +作为一个测试从业者,如何在有限的测试时间里保证交付物的质量一直是绕不开的话题,性能测试作为质量保障的一部分,自然也有着重要的地位。这一讲作为本课程的导读,我想带你相对全面地了解一下性能测试的整个过程,以及在这个过程中需要落地的事情。在后面的学习中,我们将一步步展开。 + +历史访问数据 + +历史访问数据,指的是什么类型的用户通过何种终端访问服务的接口次数。 + +为什么我要把访问数据记录放在第一个呢?线上作为“案发”的第一现场,保留现场的证据是非常重要的。性能测试说白了是模拟案发现场来寻找破案线索,访问数据记录用户轨迹、作为衡量性能的重要手段,自然是不可或缺的。绝大多数公司都会封装平台来采集历史访问数据,如果要看原始的访问日志,Nginx 日志也是一种方式(如下所示),不过原始的日志都需要加工处理来提取我们需要的信息。 + +120.204.101.238 - - [29/Nov/2020:14:09:22 +0800] "GET /v1/register HTv1TP/1.1" 200 150 "- + +120.204.101.238 - - [29/Nov/2020:14:09:22 +0800] "POST /v1/login HTTP/1.1" 200 36 "- + +120.204.101.238 - - [29/Nov/2020:14:09:22 +0800] "GET /hello/map HTTP/1.1" 200 202 + + +可能你能理解为什么要通过终端类型统计服务的接口次数,但却对为什么要统计用户的类型有些困惑?在绝大多数电商场景下,电商用户等级对应不同的权益、优惠券类型和数量,这些业务规则都会影响到性能测试的结果。很多人在做性能测试的时候会忽略这一点 。 + +需求管理 + +有了参考数据,我们就可以来看需求了,对需求接入和充分的分析能帮助你在测试之前获得更多的信息,也能制定出较为完善的性能测试方案。业务测试和性能测试都是从需求入手的,但业务测试会去了解相关的业务背景和产品方案;对于性能测试而言,则在需求来源、分析方面提出了更多的要求。 + +需求来源 + +需求来源其实就是你这次性能测试的目的,调研清楚这个问题能帮助你更有针对性地获取数据,从而制定更为准确的性能目标。例如,我们这次的性能测试是为了应对“黑色星期五”的活动,那么就要考虑有没有以往的性能测试数据沉淀、当前有多少活跃用户数、网站交易数和活跃人数有没有相应的递增比例等和该活动有关的数据。 + +需求分析 + +弄清楚了性能测试的目的,我们就要来做需求分析和梳理了。 + +需求分析是在原始数据中提炼出有效的性能参考数据,通过这些数据构建性能测试的模型,再通过模型形成测试步骤。性能测试模型和性能测试执行步骤也是性能测试方案的核心内容,它决定了你做性能测试是否准确,是否更符合真实场景。 + +分析方案 + +在需求分析完成之后,就需要将你分析的内容提交一份性能测试方案了。性能测试方案的目的不仅仅在于让自己知道这次性能测试如何执行,也要让你的项目成员知道这次性能方案,它的执行周期、涉及的成员等,然后再一起评审这次方案中有没有不合理的地方。 + +性能测试环境管理 + +从目前的趋势来看,线上的全链路性能测试非常热门,但并不意味着就只做线上的性能测试了。关于性能测试环境,一般情况下我们会独立搭建一套,与业务测试环境相隔离,同时也能够在上线之前尽可能暴露一些代码中的问题。 + +我曾看到过这样一个观点:线下性能环境与生产环境机器配置相差甚大,我们直接在生产上做性能测试就可以了,没有必要在测试环境中做。 + +这个观点引起了一部分人的赞成,但我认为这个说法不够全面。环境的配置高低是决定性能结果的一个影响因素,但不是全部因素。能够提前测试、提前暴露 bug,修复 bug 的成本也就越低,所以在线下必须有专门的性能环境,它可以帮助你提前发现内存泄漏、死锁等问题。更何况,这些问题的发现和修复与服务器硬件配置并没有直接联系,如果能够在线下提早用更低的成本解决是一种更优的选择。 + +如果没有做线下性能测试的情况下直接在生产上测试,对性能中的异常测试、高可用测试可能无法充分执行;同时,修复性能 bug 也需要功能上的回归,这些都增加了过程管理的复杂度。 + +监控管理 + +监控是发现性能问题的眼睛,没有监控,性能定位分析也就无从谈起了。监控的核心在于全面和深入,因此,我将监控管理分为了客户端数据监控、硬件资源监控、链路监控和业务规则监控,通过这几个层次的监控可以让你最大限度地避免监控死角,也为你调优分析提供充足的依据。 + +客户端数据监控 + +性能测试中说的客户端一般是指测试机,测试机输出的数据是观察性能好差的关键指标。我推荐 JMeter+InfluxDB+Grafana 的框架,它具备展现直观、数据实时的特点,可以全面地展示监控的数据。 + +硬件资源监控 + +基础硬件资源监控一般包含 CPU、内存、磁盘、网络等,常用的监控方式可以分为命令行监控和可视化监控。 + + +命令行监控 + + +通过命令的监控我们能够以最直接的方式获取服务器的实时状态。以 Linux 服务器举例,top、vmstat、iostat、iftop 等都是性能监控常用的命令。 + + +可视化监控 + + +可视化监控相对于命令行监控提供了更为丰富的图表展示,这样的话看起来更直观易懂,适合监控大屏的展示,能够将监控信息传递给项目组成员,但它需要提取数据之后计算,然后再展示,有一定的延迟,不如命令行监控直接。 + +Zabbix、Prometheus+Grafana 等都是可视化监控常用的手段,它们可以把数据持久化,能够调取过往时间轴的历史数据,一般在回溯、汇报、复盘时使用比较多。 + +不管采用何种方式,在进行硬件监控时,都应该涵盖测试过程中所有的服务器,包括压测机、应用服务器、中间件服务器数据库服务器等。 + +链路监控 + +链路监控是对代码本身的追踪,代码问题常常是问题产生的根因,所以关于代码的监控不可忽视。目前常用的代码链路追踪工具有 SkyWalking、PinPoint、Arthas,在后续的学习中我会向你介绍其中的一些,帮助你定位代码问题。 + +业务规则监控 + +业务逻辑报错和用户息息相关并且用户是可以直接感受到的,比如商品库存不足、用户余额不足,它们会直接影响用户的体验。线上出现问题并不少见,重要的是如何第一时间得知并且解决这些问题。所以当出现问题即时发送报警邮件或者短信也是十分必要的,对于业务的监控同样不能忽视。 + +数据模型建设 + +为什么有数据模型这样的概念呢?数据模型的意义在于沉淀以往的历史数据,通过不同的维度去发现一些规律,我认为这也是性能测试领域中的一种探索方向。通过数据模型的建设,我们可以尝试在不同纬度建立数据之间的联系,从而发现数据间的规律,对未来的数据进行预测。这些纬度分为时间纬度和机器纬度。 + +时间纬度 + +一般的电商每年至少有两次大促,618 和双 11。它们一般会详细记录每年总的成交额、网关访问次数、各个服务访问次数等,通过每年的活动力度、广告投放,以及数据团队来预测下次大促的成交金额和网站访问量等,这些数据也会间接帮助性能测试制定目标。 + +机器纬度 + +机器纬度是一个什么概念呢?你可能会认为在线下两台机器测出来接口的处理能力是 100,线上有 10 台等配置的机器,就不用测试了,处理能力直接按照 5 倍去推算。 + +这其实是默认只要扩充机器系统的处理能力就会倍数增加,事实上是毫无道理的。不过你可以长期记录接口或者服务在性能测试环境的数据和生产环境中,相同场景下的压测数据,再进行长时间地跟踪对比,尝试发现其中是否能够存在一些规律。 + +技术建设 + +技术建设基于你的技术视野。关于技术的重要性,你可能了解,但理解得不够全面。无论是你写的测试脚本,还是在做的代码调优,其实它们都只是技术的一部分。我认为对于一名优秀的性能测试来说,需要具备以下 3 个方面的能力。 + + +熟练掌握一门编程语言。测试很难说一定要掌握哪一种语言,但是熟练地使用一门语言可以帮助你迅速上手其他编程语言。 +能够读懂服务端基本架构。如果你不懂服务端的架构,那基本只能根据你的性能测试工具去编写报告,了解不到更深层次内容。 +能够根据性能测试需求,提出系统改造建议。性能测试与业务测试不太一样:业务测试基本是通过构造测试场景去满足业务规则,而性能测试,尤其是线上全链路性能测试,为了避免造成线上数据污染和影响真实用户访问,往往会改造系统去进行流量隔离和清理。 + + +技术有个重要作用:改善测试效率。测试讲究质效合一,质代表质量,效则是效率。 + +好的测试平台可以管理测试资料,固化测试过程,自动化测试执行,可视化测试结果。它可以增加团队成员之间的协作性,不要重复造轮子,提升团队能效;对于脚本管理和监督,测试结果的回溯也有重要作用。 + +总结 + +这一讲我带你了解了性能测试全过程中的要点,你可以对性能测试有一个大概的认识。在后面的学习中,我会将上述的知识结合我的经验来讲解。你可以从这一讲中看一下自己还有哪些需要夯实的知识,也可以看看公司的性能测试开展到什么阶段了,看在发展上还有哪些自己力所能及的地方。 + +对于全过程中需要注意的事情,除了我写的这些,你还有什么要补充的吗?欢迎在评论区留言。 + +下一讲,我将带你了解 JMeter 的核心概念,它是我们现在最流行的性能测试工具。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/01JMeter的核心概念.md b/专栏/说透性能测试/01JMeter的核心概念.md new file mode 100644 index 0000000..eb80456 --- /dev/null +++ b/专栏/说透性能测试/01JMeter的核心概念.md @@ -0,0 +1,208 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 01 JMeter 的核心概念 + 从今天开始,我们将进入模块一的学习,在学习的过程中,希望你能够明白为什么 JMeter 要这么用并掌握 JMeter 的一些进阶用法。这一讲作为我们学习的第一讲,我将带你了解 JMeter 的核心概念,完善你对测试工具的认识。 + +为什么是 JMeter + +性能测试有很多工具,JMeter、Loadrunner、Locust、nGrinder 都不乏粉丝。有人认为做性能测试重要的不是工具,是思想。但从学习实践的角度讲,工具在一定程度上决定了工作效率及协作模式。要成为一名测试专家,对工具一定是要精通的。 + +JMeter 原生的方式只支持单点工作,团队成员并不能很方便地互相检查脚本和查看报告。如果我们想改变这样的协作方式,就要对 JMeter 进行改造。如果不了解工具,改造也就无从谈起。 + +说了这么多,那我为什么会选择介绍 JMeter 呢?总的来说,它有以下 3 点优势。 + + +开源免费、安装简易、多系统兼容。相对于 Loadrunner,JMeter 没有版权的困扰,脚本可以在 Windows、Linux、Mac 任意系统间切换,非常简单方便。 +丰富的基础插件。相对于 Locust,JMeter 提供了较多的插件,可以减少重复造轮子的工作。Locust 的基础功能需要写代码实现,更适合定制性较强的测试场景,如游戏类测试,在敏捷化的测试团队中需要考虑到这部分的时间成本问题。 +良好的拓展性。虽然 JMeter 已经有了丰富的基础插件,它本身还是提供了入口进行二次开发,以满足团队定制化的需求。同样,你也可以将 JMeter 平台化,通过平台化的操作来管理 JMeter,增强测试团队的协作性。 + + +我们虽然是从 JMeter 工具开始的,但网上其实已经提供了很多实例来教你 JMeter 的基础使用,所以这一讲的重点是帮你厘清 JMeter 设计上的一些核心理念。我将从 3 个方面来介绍,分别是:线程、循环、Ramp-Up,组件和元件,以及分布式压测。 + +我们先来看线程、循环、Ramp-Up。 + +线程、循环、Ramp-Up + +这是你在 JMeter 的线程组元件中的线程属性,线程组建立是你使用 JMeter 进行性能测试最基础的步骤,压力发起策略几乎都依赖于这个元件。 + +线程与循环 + +我们先来看两张图,看看它们之间有什么区别。 + + + +图 1:设置图 A + + + +图 2:设置图 B + +从两张图的对比中,我们可以看到图 1 和图 2 的区别在于线程数和循环次数,一个是 1 和 10,一个则是 10 和 1。从结果来看,图 1 和图 2 都是发送了 10 个请求,那它们的核心区别是什么呢? 我们不妨来看两段代码演示。 + +先来看图 1 的代码演示: + +for(int j=0;j<10;j++) { + + System.out.println(Thread.currentThread().getName());//打印线程名字 + +} + + +这段代码我使用线程循环的方式打印运行线程的名字,运行后的内容如下: + +Thread-0 + +...... + +Thread-0 + +Thread-0 //可以看到是基于同一个线程 + + +再来看图 2 的代码演示: + +for(int i=0;i<10;i++){ + + new Thread(new Runnable() { + + public void run() { + + System.out.println(Thread.currentThread().getName()); + + } + + }).start(); + +} //示意代码 + + +这段代码我是使用多线程的方式打印正在运行的线程,运行后效果如下: + +Thread-0 + +...... + +Thread-8 + +Thread-9 //不同的线程 + + +以上代码内容主要是打印线程的名字。不难看出,循环的方式是基于同一个线程反复进行 10 次操作,而多线程则启动了 10 个不一样的线程,虽然都是向服务器发送了 10 次请求,但这两种方式完成的时间和对系统的压力也完全不一样。 + +打个比方,我们需要掰 100 斤玉米,一组是 10 个人一起掰,一组只有 1 个人掰,每个人的速度如果是一致的,不用想就知道哪个组更快。这样的场景经常发生在使用 JMeter 利用接口造数据时,同样是造 1 万条数据,如果你觉得速度很慢,那你就可以考虑一下多线程了。但掰玉米用 10 个人的成本当然要比用 1 个人来得多,我们的压力场景也是这样的。通常压力场景都是多线程的,线程的多少也直接决定了对被测系统压力的大小。 + +Ramp-Up + +Ramp-Up 其实是一个可选项,如果没有特殊要求,保持默认配置脚本即可。如果填 1,代表在 1 秒内所有设置线程数全部启动。不过这个是理论上的,实际启动时间也依赖于硬件的接受程度。如果硬件跟不上,启动时间自然也会增加。 + +在有的性能测试场景中,如果你不想在性能测试一开始让服务器的压力过大,希望按照一定的速度增加线程到既定数值,你就可以使用这个选项。比如我想用 10 个线程进行测试,启动速度是每秒 2 个线程,就可以在这里填 5,如下所示: + + + +图 3:设置图 C + +我们来通过运行展示一下。 + + + +图 4:生成线程数 + +我使用了监听器中的用表格查看结果插件。通过这组数据可以看到,每秒产生了 2 个新的线程,合计在 5 秒内完成。 + +组件和元件 + +了解了线程、循环和 Ramp-Up,接着来聊聊组件和元件。 + +组件和元件的关系 + +要解释组件首先就要说元件。我们看图 4 中的 HTTP 请求,其实这就是一个实际的元件。同样作为元件的还可以是 JDBC 请求、Java 请求等,这一类元件我们统一称为取样器,也就是组件。我用一个示意图来表示组件和元件的关系: + + + +图 5:组件和元件关系图 + +如图所示,HTTP 请求、JDBC 请求等元件都从属于取样器。 + +组件的作用 + +JMeter 有多种组件,我们重点看下这七类: 配置元件、取样器、定时器、前置处理器、后置处理器、断言、监听器。我们来看下它们各自的作用。 + + +配置元件:用于初始化变量,以便采样器使用。类似于框架的配置文件,参数化需要的配置都在配置元件中。 +取样器:承担 JMeter 发送请求的核心功能,支持多种请求类型,如 HTTP、FTP、JDBC 等,也可以使用 Java 类型的请求进行自定义编写。 +定时器:一般用来指定请求发送的延时策略。在没有定时器的情况下,JMeter 发送请求是不会暂停的。 +前置处理器:在进行取样器请求之前执行一些操作,比如生成入参数据。 +后置处理器:在取样器请求完成后执行一些操作,通常用于处理响应数据,从中提取需要的值。 +断言:主要用于判断取样器请求或对应的响应是否返回了期望的结果。 +监听器:监听器可以在 JMeter 执行测试的过程中搜集相关的数据,然后将这些数据在 JMeter 界面上以树、图、报告等形式呈现出来。不过图形化的呈现非常消耗客户端性能,在正式性能测试中并不推荐使用。 + + +组件的顺序 + +了解正确的组件执行顺序可以帮助你明白在什么情况下应该添加什么组件,而不会添加错误的组件造成不必要的麻烦。我将它们做了一个排序,如下图所示: + + + +图 6:组件顺序 + +搞懂了组件顺序,你在测试前准备脚本生成参数化数据时,就可以在前置处理器中寻找相关元件;在要提取接口返回的数据,就可以在后置处理器中寻找相关插件,而不是在其他地方寻找数据,浪费时间。 + +我经常看到有的测试人员在需要在后置处理器中使用 BeanShell PostProcesor 的时候,错误地用了前置处理器中的 Beanshell PreProcessor,导致系统报错,无法实现预期的功能,甚至是测试无法进行下去。 + +元件作用域 + +以上说的都是组件相关的东西,这里就来看看元件作用域。我们先来看一张图: + + + +图 7:结果树 1、2、3 + +在图中可以看到,我在不同位置放了 3 个一样的元件“查看结果树”(为了方便区分,我分别标记了 1、2、3)。运行后发现,查看结果树 1(图 8)里面显示了 HTTP1 和 HTTP2,而插件结果树 2 里只有 HTTP1,查看结果树 3 里面只有 HTTP2。 + + + +图 8:查看结果树 1 的显示图 + +这是为什么呢?这就要说到元件作用域了。 + +通过截图可以发现 JMeter 元件除了从上到下的顺序外,有还具备一定的层次结构,比如图 5 中的响应断言和查看结果树,它相对于取样器存在父子组件的关系,说白了就是 HTTP 元件对取样器有效的区域,比如查看结果树 2 是 HTTP1 请求的子节点,那它就只对 HTTP1 生效;如果父节点是测试计划,那就会对测试计划下的 HTTP1 和 HTTP2 都生效。 + +分布式压测 + +压测就是 JMeter 通过产生大量线程对服务器进行访问产生负载,监听服务器返回结果并进行校验。在大部分情况下,用单台 JMeter 进行性能测试或者自动化测试是可行的,但在多线程运行过程中可能存在性能瓶颈,很多人在排查定位问题时经常会漏掉这一点。 + +从我的工作经验出发,单机的 JMeter 最好将线程数控制在 1000 以内;如果超过了 1000 线程,则建议使用 JMeter 分布式压测,这在一定程度上可以解决 JMeter 客户端自身形成的瓶颈问题。 + +在分布式 JMeter 架构下,JMeter 使用的是 Master 和 Slave。 + +Master + +Master 负责远程控制 Slave(负载机)。分布式通常有多个 JMeter 节点,其中一个节点承担 Master 的作用。Master 通过发送信号控制节点机的启动和停止,并进行收集节点机的数据等操作。 + +Slave + +Slave 一般也叫负载机,主要是发起线程来访问 target 服务器。一般在 Slave 节点机上先启动代理 jar 包,控制机远程连接,负载机运行脚本后对 Master 回传数据。流程示意图如下: + + + +图 9:Slave 流程示意图 + +JMeter 的 Master 和 Slave 配置也比较简单。将 JMeter 的 bin 目录下的 jmeter.properties 文件配置:IP 和 Port 是 Slave 机的 IP 以及默认的 1099 端口。如下所示: + +remote_hosts=ip:1099,ip:1099 + + +Slave 启动 jar 包之后,默认会启动 1099 端口。Master 配置完成启动后便可以建立和 Slave 连接,从而进行控制和收集等操作。 + +一般来说,JMeter 分布式压测都是作为缓减客户端瓶颈的重要方式。这里我要强调“缓减”,因为在性能测试领域中不存在一种技术手段能够保证永远没有问题。随着公司的体量发展,对性能的要求也是水涨船高。JMeter 自带的分布式压测作为一种缓解客户端性能问题的方式,并不是万能法则。 + +总结 + +本讲我主要讲解了 JMeter 的核心设计理念,希望能够让你能对 JMeter 的核心概念有一定的理解。JMeter 作为目前最流行的性能测试工具,它本身提供的插件可以满足绝大多测试场景的使用,并且它也提供了二次开发的接口和 API,使用起来非常灵活。同时它分布式的使用方式也能够让你在较大程度上缓减客户端瓶颈。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/02JMeter参数化策略.md b/专栏/说透性能测试/02JMeter参数化策略.md new file mode 100644 index 0000000..4fdb2c2 --- /dev/null +++ b/专栏/说透性能测试/02JMeter参数化策略.md @@ -0,0 +1,169 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 02 JMeter 参数化策略 + 上一讲我梳理了 JMeter 的核心概念,希望你能够通过课程去理解并灵活的应用到实际工作中。这一讲我会带你学习一个重要的知识点:参数化。无论是从使用频率还是从参数化对性能测试结果的影响,它都是你做性能测试必须要掌握的。 + +参数化是什么 + +简单来说,参数化就是选取不同的参数作为请求内容输入。使用 JMeter 测试时,测试数据的准备是一项重要的 工作。若要求每次传入的数据不一样,就需要进行参数化了。 + +为什么要进行参数化 + +刚才说到,若要求每次传入的数据不一样,就需要进行参数化。那为什么会有这种要求呢?我们来看两个场景。 + + +数据被缓存导致测试结果不准确 + + +缓存原本是为了让数据访问的速度接近 CPU 的处理速度而设置的临时存储区域,比如 cache。如今缓存的概念变得更广了,很多空间都可以设置客户端缓存、CDN 缓存等等。 + +当你频繁地请求某一条固定的数据时,这条数据就很容易被缓存,而不是每次都从数据库中去获取,这就可能导致和真实的场景有差别。 + +比如大促有 10w 用户会从获取不同的商品信息,而你的压测中并没有进行充分的参数化,此时用大量线程反复请求同一件商品,极有可能访问的是缓存数据。从业务逻辑看接口返回并没有问题,但这样的场景几乎不会发生,这就会导致无效压测,测试的结果并没有多少意义。 + + +流程不能正常执行 + + +数据被缓存可能会导致测试结果不准确,但至少业务能够走通。还有的情况是,在没有参数化的情况下,会产生大量的业务报错。 + +打个比方,你在测试限购商品抢购,如果用多线程模拟同一个用户操作可能会直接报错,因为在生成订单接口(支付等)都会判断是否是同一个用户。 + +在要求每次传入的数据不一样时,如果不进行参数化会造成很多问题,以上我列举的两个场景基本概括了没有参数化时会发生的问题,希望你能在性能测试时多加注意。下面我们就来看如何实现 JMeter 参数化。 + +JMeter 参数化的实现方式 + +我列举了 3 种比较常见的 JMeter 参数化的实现方式,你可以根据自己的需要进行选择。 + + +CSV Data Set Config:将参数化的数据放入文件中,参数化读取依赖于文件操作。这样的参数化方式很常用,尤其适用于参数化数据量较多的场景,而且维护比较简单灵活。 +User Defined Variables:一般来说可以配置脚本中的公共参数,如域名,端口号,不需要随着压测进行动态改变,比较方便环境切换。 +Function Helper 中的函数:使用函数的方式生成参数,如果你需要随机数、uuid 等都可以使用函数生成。JMeter 还提供了相应的接口给你二次开发,自定义需要的功能。 + + +CSV Data Set Config + +CSV Data Set Config 的可配置选项较多,也是目前性能测试参数化时使用最多的插件,这里我就重点介绍一下 CSV Data Set Config。 + +在配置组件中添加元件 CSV Data Set Config,如下图所示: + + + +图 1:CSV Data Set Config + +我们来看一下 CSV Data Set Config 各项的含义。 + +文件名:顾名思义,这里填写文件的名字即可。 + +保存参数化数据的文件目录,我这边是将 user.csv 和脚本放置在同一路径下。在这里我要推荐一个小技巧,就是“相对路径”。使用绝对路径,在做脚本迁移时大部分情况下都需要修改。如果你是先在 Windows 或 Mac 机器上修改脚本,再将脚本上传到 Linux 服务器上执行测试的,那你就可以用相对路径,这样就不用频繁修改该选项了。 + +文件编码:指定文件的编码格式,设置的格式一般需要和文件编码格式一致,大部分情况下保存编码格式为 UTF-8 即可。 + +变量名称:对应参数文件每列的变量名,类似 Excel 文件的文件头,主要是作为后续引用的标识符,一般使用英文。如下图所示: + + + +图 2:user.csv + +图中我标示了 name 和 password,相对应 user.csv 中的第一列和第二列。 + +那如何引用需要的文件数据?通过“${变量名称}”就可以了,如下图所示: + + + +图 3:引用演示图 + +忽略首行: 第一行不读取。比如图 2 的第一行我只是标示这一列是什么类型的数据,实际上并不是需要读取的业务数据,此时就可以采用忽略首行。 + +分隔符:用来标示参数文件中的分隔符号,与参数文件中的分隔符保持一致即可。 + +遇到文件结束符再次循环:是否循环读取参数文件内容。因为 CSV Data Set Config 一次读入一行,如果设置的循环次数超过文本的行数,标示为 True 就是继续再从头开始读入。 + +遇到文件结束符停止线程:读取到参数文件末尾时,是否停止读取线程,默认为 False。如果“遇到文件结束符再次循环”已经设置为 True 了,这个选项就没有意义了。 + +线程共享模式:这个适用范围是一个脚本里多线程组的情况。所有线程是默认选项,代表当前测试计划中的所有线程中的所有的线程都有效;当前线程组代表当前线程组中的线程有效;当前线程则表示当前线程有效。一般情况下,我们选择默认选项“所有线程”就好,“当前线程组”和“当前线程”很少会用到。 + +上面我介绍了参数化的意义以及常见用法,参数化对于性能测试是基础且必需的,因为在性能场景中,很多时候不进行参数化,脚本也是可以跑通的,所以有一些测试同学在这方面就会“偷工减料”,但这会造成性能数据不准确。下面,我们就来看一种特殊的参数化:关联。 + +特殊的参数化:关联 + +关联是将上个请求的数据提取需要的部分放到下个请求中,通过关联我们可以获取到满足业务特性的不同数据,因此可以认为是一种特殊的参数化。 + +关联的使用场景 + +我们来看一个例子,从例子中了解关联解决了什么问题。 + +我编写了一个查看订单接口,运行完成没有问题,并且返回正确的结果,如下所示: + +{"data":{"code":0,"count":16,"items":[{"actualPrice":8900,"gmtCreate":1601448530000,"id":357,"orderNo":"1012020091448501002","skuList":[{"barCode":"150004","gmtCreate":1601448530000,"gmtUpdate":1601448530000,"id":389,"img":"https://demo.oss-cn-shenzhen.aliyuncs.com/bg/86338c9e576342baa0d079bc1caef9cc.jpg","num":1,"orderId":357,"orderNo":"1012020091448501002","originalPrice":10690,"price":8900,"skuId":2777,"spuId":1236771,"spuTitle":"昵趣 NaTruse 山羊奶配方狗狗洁齿骨 盒装 20g*40 支","title":"山羊奶","unit":"盒"}],"status":90},"msg":"第 1 页,共 1 条","pageNo":1,"pageSize":1,"total":1,"totalPageNo":2},"errmsg":"成功","errno":200,"timestamp":1609219480400} + + +一个小时之后,我再来运行这个接口时,却发现返回用户未登录: + + {"errmsg":"用户尚未登录","errno":10001,"timestamp":1609220170295} + + +在所有入参都没有修改的情况下为什么会出现这样的情况呢?因为你看到返回的信息是用户未登录,也就是说用户信息是无效的。 + +这个接口使用 Token 验证用户,Token 有效期为 15 分钟,刚刚问题产生的原因就是 Token 过期了。 + +那如何保证查看订单接口信息中需要的 Token 都是有效的呢?其中一个方法就是查看订单接口之前调用登录接口获取 Token,把登录接口的 Token 传入查看订单接口中。这个过程其实就是“关联”。 + +JMeter 如何实现关联 + +JMeter实现关联有 3 种方式:边界提取器,通过左右边界的方式关联需要的数据;Json Extractor提取器,针对返回的 json 数据类型;正则表达式提取器,通过正则表达式去提取数据,实现关联。 + +正则表达式提取器是最为常用,也是这里我要向你介绍的关联方式。我们来看下面的例子: + + + +图 4:正则表达式提取器 + +我们来看一下正则表达式提取器中每一项的含义。 + + +引用名称:自己定义的变量名称以及后续请求将要引用到的变量名。在图中我填写的是“token”,则引用方式是“${token}”。 +正则表达式:提取内容的正则表达式。“( )”括起来的部分就是需要提取的,“.”点号表示匹配任何字符串,“+”表示一次或多次,“?”表示找到第一个匹配项后停止。 +模板:用“\( \)”引用,表示解析到的第几个值给 token,图 4 中的正则表达式如下: + + +"accesstoken":(.+?),"gender":(.+?) + + +\(1\) 表示匹配的第一个值,即 accesstoken 后匹配后的值,模板 \(2\) 则匹配 gender 后的值。图 4 演示的实例中只有 1 个 token 值,所以使用的 \(1\)。 + + +匹配数字:0 代表随机取值,1 代表第一个值。假设我返回数据的结构是: + + +[{"accesstoken":"ABDS88WDWHJEHJSHWJW","gender":null},{{"accesstoken":"NDJNW3U98SJWKISXIWN","gender":null}] + + +此时提取结果是一个数组,accesstoken 对应了多个值相当于数组,1 代表匹配第一个 accsstoken 的值“ABDS88WDWHJEHJSHWJW”。 + + +缺省值:正则匹配失败时的取值。比如这里我设置的是 null(token 值取不到时就会用 null 代替)。上面我们已经匹配了 token 值,在被测接口传参处直接用“${token}”就可以了。 + + + + +图 5:关联 Token + +关联后就可以看到,每次都能进行正常的业务返回了。 + + + +图 6:关联后正常业务返回 + +总结 + +这一讲我介绍了参数化策略以及使用场景。作为性能测试中最常用到的操作,你不仅要学会基本操作,还需要思考参数化策略适合的场景以及参数化数据对性能测试的影响。说到这里,我就要问一个问题了:不同的参数对性能结果会不会有影响? + +举个例子,在电商系统中,你准备了不同的用户数据,用户又分为不同的等级,不同的等级可能有不一样的优惠规则和对应的优惠券,每个会员的优惠券数量可能也不一样,那这些不同的会员信息分布会对性能测试的结果有什么样的的影响?欢迎在评论区给出你的思考。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/03构建并执行JMeter脚本的正确姿势.md b/专栏/说透性能测试/03构建并执行JMeter脚本的正确姿势.md new file mode 100644 index 0000000..928ef88 --- /dev/null +++ b/专栏/说透性能测试/03构建并执行JMeter脚本的正确姿势.md @@ -0,0 +1,325 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 03 构建并执行 JMeter 脚本的正确姿势 + 通过上两讲的学习,相信你已经掌握了 JMeter 的组件结构、关联、参数化等知识,这些是你使用性能测试工具的基础,那如何才能有效地执行这些脚本呢? + +说到这个话题,我回想起一些找我咨询的同学。 + +有些团队在组建之初往往并没有配置性能测试人员,后来随着公司业务体量的上升,开始有了性能测试的需求,很多公司为了节约成本会在业务测试团队里选一些技术能力不错的同学进行性能测试,但这些同学也是摸着石头过河。他们会去网上寻找一些做性能的方案,通常的步骤是写脚本,出结果然后交给开发。这虽然能够依葫芦画瓢地完成一些性能测试的内容,但整个过程中会存在不少值得商榷之处。 + +这一讲我就以脚本为切入点,和你聊聊在脚本构建与执行过程中可能存在不规范的地方有哪些,以及如何去解决。 + +脚本构建 + +脚本构建就是编写脚本,是你正式开始执行性能测试的第一步,对于常规的请求来说只需要通过界面的指引就可以完成,这个是非常容易的,但是上手容易不代表你使用方法科学,下面我带你看看常见的误区。 + +一个线程组、一条链路走到底 + +先来看下这样一张线程组的图: + + + +图 1:一个线程组 + +图中包含了注册、登录、浏览商品、查看订单等,它们在同一个线程组,基于同一线程依次进行业务。这样的做法其实和自动化非常相似。 + +比如张三先注册一个网站,然后进行登录、添加购物车等操作。但仔细想一想,对于一个网站的性能而言,这么考虑是有些问题的。 + +在正常情况下,基于同一个时间节点,一部分人在浏览商品,而另一部分人在下单。这两部分之间没有先后关系,人数占比也不一定就是 1:1。脚本中的设计思路实际上也是你对性能测试模型的理解,能够反馈出模型中的用户访问比例分布,这块内容我会在第二模块重点描述,不仅会讲述满足脚本的跑通,还会通过脚本构建基于性能模型的场景。 + +未提取公共部分,增加脚本管理难度 + +我在平时的工作中发现,有的测试会基于同一类型的 HTTP 请求,配置相同的 host、端口等,并没有很好地利用JMeter 中作用域的思想。 + +一般全链路级别的测试脚本里可能会包含上百个接口,对于一些 host 和端口号,并不需要每一个接口都去配置,我们可以使用一个 HTTP 请求默认值去做公共部分。如果说不提取这些公共部分,每改动一个配置,所有接口可能都要改动,这样脚本维护成本工作量也会比较大,有可能会造成“牵一发而动全身”的情况。 + +查看结果树使用频率高 + +在脚本调试过程中,我们通常会添加结果树来实时查看返回数据的正确性。这个插件本身是比较消耗性能的,在正式压测中应当禁止使用。一般来说,在脚本调试中通过作用域的思想去配置一个查看结果树就可以了,不要过度使用,不然等到正式压测的时候,一个个地禁用结果树不仅会消耗时间,还容易遗漏。 + +脚本逻辑复杂 + +有的测试在编写脚本的过程中为了区分业务逻辑,会使用很多插件,比如 if 判断、循环, 这些插件虽然可以让你进入不同的业务场景,但会增加脚本的复杂度,影响发起压力的效率。你可以自己做一些对比测试,看使用该插件和去除该插件实际的处理能力相差多少,不要因为自己的脚本结构而影响实际的性能测试结果。 + +以上是在脚本构建时,一些普遍存在的误区,而规范的脚本构建,我认为要做到真实和精简。 + + +真实在于你的脚本可以体现出真实的用户访问场景; +精简在于少使用周边的插件,比如通过 JMeter 去监控服务器资源,这样的监控不仅简单粗糙,而且较大地影响 JMeter 的压力发起的效率。 + + +脚本执行 + +在正确构建了脚本之后,我们就要来看如何执行脚本了。脚本执行就是你怎么去运行脚本,可能有的同学会一头雾水,我直接点击界面上的运行按钮不就行了吗?事实上真正的压测可不是这个样子的。 + +界面化执行性能测试 + +一些测试人员在 Windows 或 Mac 环境编写完脚本后,会直接用界面化的方式进行性能测试,这样的做法是非常不规范的。打开 JMeter 界面之后就会弹出提示,如图 2 所示: + + + +图 2:界面化性能测试提示 + +很多人会选择直接忽略掉,但图中的第一段是这样的: + + +Don’t use GUI mode for load testing!only for Test creation and Test debugging。For load testing,use NON GUI Mode。 + + +中文意思就是图形化模式只让你调试,不要进行压测。 + +图形化的压测方式会消耗较多的客户端性能,在压测过程中容易因为客户端问题导致内存溢出。既然官方不推荐我们使用图形化界面,那我们应当如何执行测试脚本呢? + +我们来看图 2 中的第三行内容: + + +jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder] + + +官方早已给出答案:通过命令行执行。命令行执行的方式同样适用于 Windows、Mac 和 Linux 系统,不需要纠结系统兼容性的问题。 + +那既然命令行执行的方式不会造成这样的问题,为什么还要用界面化的方式呢? + +相对于命令行执行,界面化的方式更为简单、方便,但命令行执行也并不是完美无缺的。 + +我们来回顾这段文字中的含义: + + +jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder] + + + +-n 表示在非 GUI 模式下运行 JMeter; +-t 表示要运行的 JMeter 测试脚本文件,一般是 jmx 结尾的文件; +-l 表示记录结果的文件,默认以 jtl 结尾; +-e 表示测试完成后生成测试报表; +-o 表示指定的生成结果文件夹位置。 + + +我们来看一下执行了上面的脚本后的实际效果,如图 3 所示: + + + +图 3:脚本执行后的效果 + +从图中可以看到,命令行的方式直接产生了总的 TPS、报错和一些时间层级的指标。命令行的执行方式规避了一些图形化界面存在的问题,但这样的结果输出方式存在 2 个问题: + + +看不到实时的接口返回报错的具体信息; +看不到混合场景下的每个接口的实时处理能力。 + + +这 2 个问题都有个关键词是“实时”,是在压测过程中容易遗漏的点。虽然压测之后我们有很多方式可以回溯,但在性能测试过程中,发现、排查、诊断问题是必不可少的环节,它能够帮助我们以最快的速度发现问题的线索,让问题迅速得到解决。 + +先来看第一点:看不到实时的接口返回报错的具体信息。 + +jmeter.log 刚刚执行过程中记录了哪些内容呢?如图 4 所示: + + + +图 4:jmeter.log 执行时记录的内容 + +你可以看到只能显示报错率,但看不到具体的报错内容,那如何去解决呢?一般我会使用 beanshell,把判定为报错的内容增加到 log 里。beanshell 的示意代码如下所示,你可以根据自己的需求改进。 + +String response = prev.getResponseDataAsString(); + +//获取接口响应信息 + +String code = prev.getResponseCode(); + +//获取接口响应状态码 + +if (code.equals("200")){//根据返回状态码判断 + + log.info("Respnse is " + response); + + //打印正确的返回信息,建议调试使用避免无谓的性能消耗 + +}else { + + log.error("Error Response is"+response); + + //打印错误的返回信息 + + } + + +这样就会自动在 jmeter.log 中打印出具体的报错信息,如图 5 所示: + + + +图 5:报错信息 + +客户端的日志只是我们需要关注的点之一,排查错误的根因还需要结合服务端的报错日志,一般来说服务端的报错日志都有相关的平台记录和查询,比较原始的方式也可以根据服务器的路径找相关日志。 + +我们再来看第二个问题:看不到综合场景下的每个接口的实时处理能力。 + +我个人认为原生的实时查看结果是有些鸡肋的,如果想实时且直观地看到每个接口的处理能力,我比较推荐 JMeter+InfluxDB+Grafana 的方式,基本流程如下图所示: + + + +图 6:JMeter+InfluxDB+Grafana + +JMeter 的二次开发可以满足很多定制化的需求,但也比较考验开发的能力(关于二次开发,我会在《04 | JMeter 二次开发其实并不难 》中介绍)。如果不想进行二次开发,JMeter+InfluxDB+Grafana 也是一种比较好的实现方式了。 + +下面,我会对 InfluxDB 和 Grafana 做一个简单的介绍,包括特点、安装等。 + +InfluxDB + +InfluxDB 是 Go 语言编写的时间序列数据库,用于处理海量写入与负载查询。涉及大量时间戳数据的任何用例(包括 DevOps 监控、应用程序指标等)。我认为 InfluxDB 最大的特点在于可以按照时间序列面对海量数据时候的高性能读写能力,非常适合在性能测试场景下用作数据存储。 + +安装 + +首先带你来看下 InfluxDB 具体的安装步骤(基于 CentOS 7.0),直接输入以下命令行即可: + +#wget https://dl.influxdata.com/influxdb/releases/influxdb-1.1.0.x86_64.rpm + +#rpm -ivh Influxdb-1.1.0.x86_64.rpm + +#systemctl enable Influxdb + +#systemctl start Influxdb + +#systemctl status Influxdb (查看 Influxdb 状态) + + +基本操作 + +当你已经安装完成之后,我带你了解下如何操作 InfluxDB: + +#influx + +linux 命令行模式下进入数据库 + +#show databases + +查看库 + +create database jmeter; + +建库 + +use jmeter + +使用该库 + +show measurements; + +查看库下面的表 + + +InfluxDB 成功安装并且建库之后,我们就可以来配置 JMeter 脚本了。配置过程可以分为以下 3 步。 + +(1)添加核心插件,在 listener 组件中选择 Backend Listener(如下图所示)。 + + + +图 7:添加 Backkend Listenner + +(2)Backend Listener implementation 中选择第二项(如下图所示)。 + + + +图 8:Backend Listener implementation + +(3)配置 InfluxDB URL,示例“http://127.0.0.1:8086/write?db=jmeter”;IP 为实际 InfluxDB 地址的 IP,DB 的值是 InfluxDB 中创建的库名字(如下图所示)。 + + + +图 9:配置连接 influxdb 库的具体信息 + +配置完后运行 JMeter 脚本,再去 InfluxDB 的 JMeter 数据库中查看是否有数据,有数据则代表如上链路配置没有问题。 + +我们再来了解一下 Grafana。 + +Grafana + +Grafana 是一个跨平台的开源的度量分析和可视化工具,纯 JavaScript 开发的前端工具,通过访问库(如 InfluxDB),展示自定义报表、显示图表等。大多时候用在时序数据的监控上。Grafana 功能强大、UI 灵活,并且提供了丰富的插件。 + +安装步骤 + +在 linux 命令行下直接输入以下内容即可: + +#wget https://dl.grafana.com/oss/release/grafana-6.4.4-1.x86_64.rpm + +#下载 granafa + +#yum install Grafana-6.4.4-1.x86_64.rpm + +#安装,遇到需要输入的直接 y 就 ok; + +#systemctl start Grafana-server + +#systemctl enable Grafana-server + +#启动 Grafana + +#/etc/Grafana/Grafana.ini + +配置文件路径,一般保持默认配置即可。 + +#systemctl status firewalld.service + +查看防火墙状态,防止出现其他干扰问题,最好关闭 + +登录访问 Grafana 访问:http://127.0.1.1:3000(ip 自行替换,3000 为默认端口) + +默认账号/密码:admin/admin + + +输入密码后如果出现了如下界面,说明 Grafana 安装成功了。 + + + +图 10:Grafana 界面 + +数据源配置 + +为什么要配置数据源呢,简单来说就是 Grafana 需要获取数据去展示,数据源的配置就是告诉你去哪里找数据,配置安装的 InfluxDB 地址和端口号,如下图所示: + + + +图 11:配置地址和端口号 + +然后输入 InfluxDB 中写入的数据库名字,如下图所示: + + + +图 12:数据库名字 + +输入完成之后可以 Save & Test,如出现以下示意图即配置成功: + + + +图 13:配置成功 + +导入 JMeter 模板 + +为了达到更好的展示效果,Grafana 官网提供了针对性的展示模版。先下载 JMeter 模板,然后再导入 Grafana。 + + + +图 14:导入 JMeter 模板 + +配置完成后,运行 JMeter 脚本。如果在界面右上方下拉选择 5s,则每 5s 更新一次: + + + +图 15:运行 JMeter 脚本 + +如上图便是完成了实时压测情况下运行结果的实时展示图,你可以以此为基础,进行多接口的数据采集,相应增加脚本里的 Backend Listener 插件,区分不同的 application name 名称,你会看到不同的接口数据都进入 influxdb 数据库中。并且 Grafana 从 Edit 中进入, 你可以根据不同的 application name 修改 SQL 来区分展示。 + + + +图 16:编辑 Grafana + +总结 + +这一讲我们主要介绍了构建和执行性能测试脚本时的一些注意事项,总结了目前业内使用 JMeter 常见的方法。你不仅需要知道这些常见的手段,也需要知道为什么要这么做,这么做有什么好处,同样随着实际采集数据指标的增高,这些做法可能还会存在哪些缺陷或者注意点,如果上述内容你都能考虑清楚了,相信你也就掌握工具了。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/04JMeter二次开发其实并不难.md b/专栏/说透性能测试/04JMeter二次开发其实并不难.md new file mode 100644 index 0000000..2a0e19a --- /dev/null +++ b/专栏/说透性能测试/04JMeter二次开发其实并不难.md @@ -0,0 +1,350 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 04 JMeter 二次开发其实并不难 + 上一讲我们通过学习 JMeter 的脚本编写方式和执行方式,掌握了如何让 JMeter 更加有效地运行,其技术思路是使用 JMeter 本身或者社区提供的现成方案去实现,这基本已经满足了绝大多数性能测试的需求。 + +随着互联网行业发展,各种技术方案层出不穷,但是任何方案都不是万能的,有些需求是要我们自己写代码去实现的,JMeter 也留了相应的入口便于我们编写代码,所以本讲将介绍三种插件编写方式: + + +自定义 BeanShell 功能 +自定义请求编写(Java Sampler) +自定义函数助手 + + +自定义 BeanShell 功能 + +什么是 BeanShell + +BeanShell 是由 Java 编写的,相当于一个小巧的 Java 源码解释器,简单来说就是你可以在里面写代码,然后通过 Beanshell 翻译成插件可以识别的指令去执行相关操作。 + +JMeter 中用 BeanShell 的优势 + +JMeter 也是由 Java 编写的,而 Java 运行前需要先编译,而 BeanShell 作为一款解释器直接运行源代码就可以。 + +BeanShell 在 JMeter 的作用 + +BeanShell 在 JMeter 中有着广泛的应用,包括前置处理器、后置处理器、Sampler 等,我们来看下这些主要应用是做什么的。 + + +前置处理器:主要是在接口请求前做一些逻辑,生成参数化数据。 +后置处理器:用于提取参数、参数格式设置等。 +Sampler:可以作为独立的请求,支持各类请求编写、数据生成。 + + +BeanShell 的常见用法举例 + +对我来说,BeanShell 最常被用于对请求或者返回内容进行获取或者加工,其中 prev 是对当前的取样进行访问,执行了对响应状态码、响应信息、请求头等的操作,示例如下: + +log.info("code is "+prev.getResponseCode()); + +#获取响应的状态码 + +log.info("response is "+prev.getResponseDataAsString()); + +#获取响应信息 + +log.info("content_type "+prev.getContentType()); + +#获取头文件中ContentType类型 + +log.info("header "+prev.getRequestHeaders()); + +#获取取样器请求首部字段 + + +通过以上方式,基本实现了对请求的基本信息的获取,然后你就可以对这些信息做进一步的提取、判断等操作。可能你会问我,使用 info 级别的日志打印,JMeter 还支持 error 级别的日志打印吗?答案是支持的,示例如下: + +log.error("cctester"); + +log.info("cctester"); + + +你可以在 BeanShell 中自行验证下,使用 log 和 error 的方式对于 JMeter 的界面提示信息是否有区别。 + +JMeter 调用 BeanShell 解释器来运行脚本,同样需要注意的是不建议过度使用这个插件, 因为在 JMeter 高并发时,它将会消耗较多的本地资源,所以一般遇到逻辑相对复杂且代码量较大的情况,我们会使用 JMeter 的另一种特色功能:开发自定义插件(jar 形式),一般来说自定义的插件会帮助我们实现两方面功能: + + +JMeter 本身需要自行拓展的请求或者不支持的测试协议,我们可以使用 Java 请求来完成; +自定义辅助函数,协助我们进行性能测试。 + + +自定义请求编写(Java Sampler) + +为了让你能够系统地学习 Java Sampler 的编写,我将分为如下四部分来介绍。 + + +什么是 Maven +什么是 Pom +实现 Java Sampler 功能的两种方式 +实例:使用 Java Sampler 重写 POST 请求 + + +什么是 Maven + +Maven 是一个项目管理工具,它可以很方便地管理项目依赖的第三方类库及其版本,说得再通俗一点: + + +没有它之前你得手动下载对应的 jar,并且复制到项目里面,升级的话又得重新下载; +有了 Maven 之后你只需要填写依赖的包名词及其版本号,就能自动帮你下载对应的版本然后自动进行构建,如果说 Maven 只是名字或者代号,那么灵魂就是 Pom 了。 + + +什么是 Pom + +在 Maven 里,project 可以没有代码,但是必须包含 pom.xml 文件。pom 文件是 Maven 对应的配置文件,我们依赖的相关信息可以在 pom.xml 中进行配置,它必须包含 modelVersion、groupId、artifactId 和 version 这四个元素,下面来看下这些元素具体的作用。 + + +modelVersion:指定了当前 POM 模型的版本,对于 Maven 2 及 Maven 3 来说都是 4.0.0。 + + +4.0.0 + + + +groupId:组织标识、项目名称。 + + +com.cctester + + + +artifactId:模块名称,当前项目组中唯一的 ID。 + + +mavenTest + + + +version:项目当前的版本号。 + + +1.0-SNAPSHOT + + + +packaging:打包格式,可以为 jar、war 等。 + + +jar + + +开发之前在 pom 文件里引入相应的 jar 包,这些 jar 包会给我们提供相应的类或者接口,引入方式如下所示: + + + + org.apache.jmeter + + ApacheJMeter_core + + 5.3 + + + + + + org.apache.jmeter + + ApacheJMeter_java + + 5.3 + + + + +实现 Java Sampler 功能的两种方式 + + +继承 AbstractJavaSamplerClient 抽象类; +实现 JavaSamplerClient 接口。 + + +通过阅读源码可以发现 AbstractJavaSamplerClient 抽象类是 JavaSamplerClient 接口的子类,想必我们都知道实现一个接口就必须实现接口里的所有方法,然而当你不需要实现所有方法时,继承 AbstractJavaSamplerClient 抽象类也是一个不错的选择。为了学习的全面性我就以实现 JavaSamplerClient 接口的方式去讲解所涉及的四个方法。 + +(1)如下所示,这个方法由 JMeter 在进行添加 JavaRequest 时第一个运行,它决定了你要在 GUI 中默认显示哪些属性。当每次在 GUI 里点击建立 java requst sampler 的时候会调用该方法。该方法设置了 parameters 的初始值,也可以在 sampler 的 GUI 界面做进一步的修改。 + + public Arguments getDefaultParameters() {} + + +(2)如下所示,这个方法用于初始化测试脚本里面用到的变量,这些变量会在后续执行中使用。 + + public void setupTest(JavaSamplerContext context) {} + + +(3)如下所示,这个方法是实现功能逻辑的主方法,每个线程会循环执行这个方法。 + +public SampleResult runTest(JavaSamplerContext context) {} + + + +计时开始的时刻是从 SampleResult 类里面的 sampleStart() 方法执行开始。 +计时结束的时刻是 sampleEnd() 方法执行结束。 +setSuccessful() 方法用来表示测试的成功与否,通常使用 try catch 来设置结果,也可以用 if 语句。 +setResponseData() 方法用来为测试结果传递数据。 + + +(4)如下所示,这个方法在每个线程执行完所有的测试工作之后执行,有点像 finally 的功能,比如,我开了一个数据库的连接,那么我要在所有的线程完成工作后关闭。 + +public void teardownTest(JavaSamplerContext context) {} + + +案例:使用 JavaSampler 重写 HTTP 的 POST 请求 + +相信你在平时工作中会经常接触到 POST 请求,接下来我将举一个有更多代入感的例子。 + +(1)首先我们来完成 POST 请求的核心方法,先使用 HttpClients 发送构建的 POST 数据包,然后获取到返回值,这一步是完成 POST 请求的基本步骤,示例代码如下: + +//HttpClients提供功支持 HTTP 协议的客户端工具 + +httpClient = HttpClients.createDefault(); + +//新建一个HttpPost请求的对象将url,接口参数等信息传给这个对象 + +HttpPost httpPost = new HttpPost(URL); + +//传入请求参数 + +httpPost.setEntity(new UrlEncodedFormEntity(Value, UTF8_CHARSET)); + +// 设置header信息,指定报文头Content-type等 + +httpPost.setHeader("Content-type", "xxxxx"); + +// 执行请求操作,并拿到结果 + +response = httpClient.execute(httpPost); + + +(2)接下来实现 JavaSamplerClient 接口,这是编写 Java Sampler 插件需要实现的核心接口,涉及的方法是 getDefaultParameters() 和 runTest(),作用上文已经描述过。下面带你来看具体怎么使用的,如下代码所示: + +//这是决定我们JMeter界面需要输入的内容,你可以看到有了url,username 和password信息,并且给出了默认值 + +public Arguments getDefaultParameters() { + + Arguments arguments = new Arguments(); + + arguments.addArgument("url","127.0.0.1:9081"); + + arguments.addArgument("username", "cctester"); + + arguments.addArgument("password", "password"); + + return arguments; + +} + + +这一步实际的效果图可以看下方的初始界面图。 + + + +初始界面图 + +(3)在上一步骤进行了参数的输入,接下来实现接收这些参数,并进行参数的输入、发送、返回判断等,如下代码所示: + +public SampleResult runTest(JavaSamplerContext javaSamplerContext) { + + //生成sampleResult对象,用于请求的命名、标记状态、添加返回内容等 + + SampleResult sampleResult=new SampleResult(); + + sampleResult.setSampleLabel("cctester_login"); + + //调用上文中实现的post请求 + + PostTest postTest=new PostTest(); + + //接受JMeter界面上传输的参数 + + String username = javaSamplerContext.getParameter("username"); + + String password = javaSamplerContext.getParameter("password"); + + String url = javaSamplerContext.getParameter("url"); + + //标记请求开始 + + sampleResult.sampleStart(); + + try { + + HttpResponse result =postTest.Request(url,username,password); + + String entity= EntityUtils.toString(result.getEntity()); + + //根据返回内容判断结果状态并展示结果 + + if (result.getStatusLine().getStatusCode()==200){ + + sampleResult.setSuccessful(true); + + sampleResult.setResponseCodeOK(); + + sampleResult.setResponseData(entity, "utf-8"); + + }else { + + sampleResult.setSuccessful(false); + + sampleResult.setResponseData(entity, "utf-8"); + + ) + + +(4)完成后打成 jar 包放到 /lib/ext 下重启 JMeter 即可,实际的效果图你可以参考上方的初始界面图和下方的运行图。 + + + +运行图 + +自定义函数助手 + +通过 Java Sampler 插件开发的学习,我们知道 JMeter 相关插件的开发其实都是有一定的套路可循,那 JMeter 函数助手开发也不例外,接下来进行函数助手开发流程的了解。 + +(1)引入 Maven 包,这个包会给我们提供函数助手开发相关的类,如下代码所示: + + + + org.apache.jmeter + + ApacheJMeter_functions + + 5.3 + + + + +(2)接下来新建我们的类包,此时新建的包需要特别注意,名字只能是 functions 结尾,否则打包放到 JMeter 中是没有办法识别这个插件的,然后代码中继承 AbstractFunction 类就可以实现,一起看下需要实现哪些方法。 + + public String getReferenceKey() {} + + +这一方法表示函数助手对话框中的下拉框中显示的函数名称,如下图所示: + + + +public List getArgumentDesc() {} + + +这一方法是设置入参的描述语,用于函数助手对话框中,显示函数名称提示。 + +public void setParameters(Collection collection) {} + + +这一方法用于我们的参数值传入。 + +public String execute(SampleResult sampleResult, Sampler sampler){} + + +这一方法是根据入参,执行核心逻辑,保存结果至相应的变量中。 + +总结 + +通过本讲的学习,你知道了如何使用代码方式实现自己需要的插件,beanshell 和 jar 包引入都是工作中常见的,相信这部分知识会对你的工作产生比较大的帮助,这也是 JMeter 的特色功能,不仅落地性强而且社区资料完善。 + +这里给你留个小作业:相信经过上文的讲解以及实例,你比较清楚地知道了插件开发的核心流程,你可以根据自己工作中的自定义函数助手的需求,按照上面的代码结构自行完成。在实践过程中遇到任何问题,欢迎在留言区留言。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/05如何基于JMeterAPI开发性能测试平台?.md b/专栏/说透性能测试/05如何基于JMeterAPI开发性能测试平台?.md new file mode 100644 index 0000000..d9568fa --- /dev/null +++ b/专栏/说透性能测试/05如何基于JMeterAPI开发性能测试平台?.md @@ -0,0 +1,384 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 05 如何基于 JMeter API 开发性能测试平台? + 上一讲我带你学习了 JMeter 二次开发,通过对 JMeter 提供的接口或者抽象类方法重写可以自定义所需要的 JMeter 插件。这一讲我将带你了解如何开发一个性能测试平台。 + +目前测试界比较热门的一个方向就是开发测试平台,平台级别的性能测试能减少重复劳动、提升效率,也方便统一管理,自然受到了市场的欢迎,测试平台开发能力也成了资深测试人员的必备技能之一。 + +本专栏,我们应用的主要性能测试工具是 JMeter,那开发性能测试平台需要什么样的能力呢?我认为需要以下能力: + + +具备较好的 Java 开发能力,JMeter 本身是 Java 开发,提供了较多的接口,所以使用 Java 开发具备天然的优势; +平台主要通过 Web 网页展示,需要具备较好的前端开发能力,目前 Vue 是比较流行的前端框架; +熟悉 JMeter 源码结构,尤其是 JMeter 提供的相关 API。 + + +构建性能测试平台的必要性 + +为什么我会如此推荐你去开发性能测试平台呢?回想一下你在工作中是否遇到过以下场景: + + +B 同学如果需要 A 同学写完的脚本,A 只能单独发给 B,如果 A 的脚本有变化,不能实时同步到 B,而且发送的过程也存在沟通成本和时间差; +测试执行后,需要将测试结果同步给开发者,很多测试都是手动截图,不仅方式原始而且还会存在信息缺失的情况; +结果追溯时,我们需要找一些历史数据却发现并没有存档或共享。 + + +这些场景使我们的性能测试平台具有了更多现实意义,我们希望有一个可以协作共享,并能够追溯历史数据的性能测试平台。基于这点我梳理了性能测试平台的基础功能,如下图所示: + + + +图 1:性能测试平台基础功能 + +目前市面上的性能测试平台大多是基于 JMeter 提供的 API 开发的,核心流程如下图所示: + + + +图 2:性能测试平台开发核心流程 + +接下来我们根据这 4 个阶段来学习如何使用 JMeter 的 API 实现性能测试。 + +环境初始化 + +JMeter API 在执行过程中,首先会读取 JMeter 软件安装目录文件下配置文件里的属性,所以我们要通过 JMeter API 读取指定的 JMeter 主配置文件的目录以及 JMeter 的安装目录;此外,我们还需要初始化 JMeter API 运行的语言环境(默认是英语)和资源。以上便是 JMeter API 做初始化的目的。 + +其中环境初始化主要包括以下 2 个步骤: + + +通过 JMeterUtils.loadJMeterProperties 加载安装目录的 JMeter 主配置文件 JMeter.properties,然后把 jmeter.properties 中的所有属性赋值给 JMeterUtils 对象,以便在脚本运行时可以获取所需的配置; +设置 JMeter 的安装目录,JMeter API 会根据我们指定的目录加载脚本运行时需要的配置,例如 saveservice.properties 配置文件中的所有配置。 + + +参考代码如下: + +JMeterUtils.loadJMeterProperties("C:/Program Files/JMeter/bin/jmeter.properties"); + +JMeterUtils.setJMeterHome("C:/Program Files/JMeter"); + +JMeterUtils.initLocale(); + + +这样一来,我们就实现了环境初始化,代码中的目录可以根据自己实际的目录设置。 + +脚本加载 + +脚本加载可以构建 HashTree,然后把构建的 HashTree 转成 JMeter 可执行的测试计划,进而执行测试用例。HashTree 是 JMeter API 中不可缺少的一种数据结构,在 JMeter API 中,HashTree 有 2 种构建方式,分别是本地脚本加载和创建脚本文件。 + +先来说本地脚本加载的方式。用 JMeter 客户端手动生成 jmx 脚本文件后,我们可以通过 SaveService.loadTree 解析本地的 jmx 文件来运行脚本,核心步骤如下: + +//加载本地 jmx 脚本 + +HashTree jmxTree = SaveService.loadTree(file); + + +由于本地脚本是 JMeter 客户端手动生成的,所以这里只需要做读取文件操作即可,loadTree 会把 jmx 文件转成内存对象,并返回内存对象中生成的 HashTree。 + +那创建脚本文件是怎么做的呢?它是通过 API 构建测试计划,然后再保存为 JMeter 的 jmx 文件格式。核心步骤如下图所示: + + + +图 3:脚本文件创建步骤 + +该方式需要自己构建 HashTree,我们可以参考 JMeter 客户端生成的 jmx 文件。 + +通过观察 jmx 文件我们可以知道需要构建的 jmx 结构,最外层是 TestPlan,TestPlan 是 HashTree 结构,包含 ThreadGroup(线程组)、HTTPSamplerProxy、LoopController(可选)、ResultCollector(结果收集)等节点。 + +接下来我将讲解 JMeter API 创建脚本文件的 6 个步骤,这 6 个步骤也是我们通过 JMeter 客户端创建脚本最常用的步骤,它们依次是创建测试计划、创建 ThreadGroup、创建循环控制器、创建 Sampler、创建结果收集器以及构建 tree,生成 jmx 脚本。 + +(1)创建测试计划 + +先生成一个 testplan,之后所有的测试活动都在 testplan 下面进行。代码如下: + +try { + + TestPlan testPlan = new TestPlan("创建 JMeter 测试脚本"); + + testPlan.setProperty(TestElement.TEST_CLASS, TestPlan.class.getName()); + + testPlan.setProperty(TestElement.GUI_CLASS, TestPlanGui.class.getName()); + + testPlan.setUserDefinedVariables((Arguments) new ArgumentsPanel().createTestElement()); + + +通过以上代码,我们生成了 testplan。 + +(2)创建 ThreadGroup + +ThreadGroup 是我们平时使用的线程组插件,它可以模拟并发用户数,一个线程通常认为是模拟一个用户。代码如下: + + ThreadGroup threadGroup = new ThreadGroup(); + + threadGroup.setName("Example Thread Group"); + + threadGroup.setNumThreads(1); + + threadGroup.setRampUp(1); + + threadGroup.setSamplerController(loopController); + + threadGroup.setProperty(TestElement.TEST_CLASS, ThreadGroup.class.getName()); + + threadGroup.setProperty(TestElement.GUI_CLASS, ThreadGroupGui.class.getName()); + + +以上是我们使用 JMeter API 创建 ThreadGroup 的代码,它实现了我们线程数的设置,如启动设置等。 + +(3)创建循环控制器 + +这一步是一个可选项。我们在实际测试过程中,可以选择多线程的循环或者按时间段进行。创建循环控制器是为了模拟一个用户多次进行同样操作的行为,不创建循环控制器则默认是只执行一次操作。循环控制器创建的代码如下: + + LoopController loopController = new LoopController(); + + //设置循环次数,1 代表循环 1 次 + + loopController.setLoops(1); + + loopController.setFirst(true); + + loopController.setProperty(TestElement.TEST_CLASS, LoopController.class.getName()); + + loopController.setProperty(TestElement.GUI_CLASS, LoopControlPanel.class.getName()); + + loopController.initialize() + + +(4)创建 Sampler + +这一步来创建我们的实际请求,也是我们 JMeter 真正要执行的内容。以 HttpSampler 为例,创建 HttpSampler 是为了设置请求相关的一些信息,JMeter API 执行脚本的时候就可以根据我们设置的一些信息(比如请求地址、端口号、请求方式等)发送 HTTP 请求。 + + // 2.创建一个 HTTP Sampler - 打开 本地一个模拟地址 + + HTTPSamplerProxy httpSamplerProxy = new HTTPSamplerProxy(); + + httpSamplerProxy.setDomain("127.0.0.1:8080/index"); + + httpSamplerProxy.setPort(80); + + httpSamplerProxy.setPath("/"); + + httpSamplerProxy.setMethod("GET"); + + httpSamplerProxy.setName("Open ip"); + + httpSamplerProxy.setProperty(TestElement.TEST_CLASS, HTTPSamplerProxy.class.getName()); + + httpSamplerProxy.setProperty(TestElement.GUI_CLASS, HttpTestSampleGui.class.getName()); + + +以上按照一个 HTTP 的请求方式设置了 IP、端口等。 + +(5)创建结果收集器 + +结果收集器可以保存每次 Sampler 操作完成之后的结果的相关数据,例如,每次接口请求返回的状态、服务器响应的数据。 + +我们可以根据结果数据做一些性能指标计算返回给前端,如果在这里创建了结果收集器,那第 4 个阶段“结果收集”中就不用再创建了。创建代码如下: + + ResultCollector resultCollector = new ResultCollector(); + + resultCollector.setName(ResultCollector.class.getName()); + + +(6)构建 tree,生成 jmx 脚本 + +以上第 2 步到第 5 步其实都是创建了一个 HashTree 的节点,就像我们用准备好的零件去拼装一辆赛车。我们把创建的这 4 个节点都添加到一个新建的子 HashTree 节点中,然后把子 HashTree 加到第 1 步的 testplan 中,最后再把 tesplan 节点加到构建好的父 HashTree 节点,这样就生成了我们的脚本可执行文件 jmx。代码如下: + + HashTree subTree = new HashTree(); + + subTree.add(httpSamplerProxy); + + subTree.add(loopController); + + subTree.add(threadGroup); + + subTree.add(resultCollector); + + HashTree tree = new HashTree(); + + tree.add(testPlan,subTree); + + SaveService.saveTree(tree, new FileOutputStream("test.jmx")); + +} catch (IOException e) { + + e.printStackTrace(); + +} + + +通过以上代码我们可以创建出 JMeter 可识别的 HashTree 结构,并且可以通过 saveTree 保存为 test.jmx 文件。 + +到这里我们就完成了创建脚本文件的流程。我在这一讲的开始提到:脚本加载可以构建 HashTree,然后把构建的 HashTree 转成 JMeter 可执行的测试计划,进而执行测试用例。因此,我们接下来进入第 3 个阶段:测试执行。 + +测试执行 + +通过脚本文件的执行(测试执行),我们便可以开始对服务器发起请求,进行性能测试。测试执行主要包括 2 个步骤: + + +把可执行的测试文件加载到 StandardJMeterEngine; +通过 StandardJMeterEngine 的 run 方法执行,便实现了 Runable 的接口,其中 engine.run 执行的便是线程的 run 方法。 + + +//根据 HashTree 执行测试用例 + +StandardJMeterEngine engine = new StandardJMeterEngine(); + +engine.configure(jmxTree); + +engine.run(); + + +通过以上代码,我们完成了代码方式驱动 JMeter 执行的核心步骤。 + +结果收集 + +性能实时数据采集可以更方便发现和分析出现的性能问题。我们在性能测试平台的脚本页面点击执行了性能测试脚本,当然希望能看到实时压测的性能测试数据,如果等测试完再生成测试报告,时效性就低了。 + +性能测试平台结果收集的流程图如下: + + + +图 4:结果收集流程图 + +上面流程图中与 JMeter 关联最密切的是第 1 步,获取 JMeter 结果数据。那我们如何获取这些数据呢? + +JMeter 性能测试用例执行完成之后会生成结果报告,既然生成了结果报告,那 JMeter 源码里一定有获取每次 loop 执行结果的地方。我们可以找到这个类,然后新建一个类去继承这个类,再重写每次结果获取的方法就能得到实时结果了。如果获取每次 loop 执行结果的是私有方法,我们也可以通过反射拿到它。 + +既然是这样,那关键就是找到, JMeter 执行中是在哪个类、哪个方法里拿的每次 loop 的结果。 + +通过查看 JMeter API 可以发现,JMeter API 提供了一个结果收集器(ResultCollector),从结果收集器的源码中可以找到获取每次 loop 执行结果的方法。结果收集器的部分源码如下所示: + +/** + +* When a test result is received, display it and save it. + +* + +* @param event + +* the sample event that was received + +*/ + +@Override + +public void sampleOccurred(SampleEvent event){...} + + +分析以上代码得知,我们可以重写 sampleOccurred 方法来收集每次 loop 的结果。该方法的参数 SampleEvent 中有我们需要的实时监控数据,这样实时监控就变得简单了。接下来,我以单客户端获取 QPS 实时监控数据为例,讲解性能测试平台结果收集相关代码实现的思路。 + +单客户端获取 QPS 实时监控数据,首先需要新建一个类继承 ResultCollector,并且重写 sampleOccurred 方法,但是这里有个问题:怎么接收 SampleEvent 里面的实时监控数据,或者说怎么取出来在我们的业务代码里应用呢?我们可以在 sampleOccurred 把监控数据存起来,然后写个接口读取存储的数据返回给前端。 + +图 4 中有一个中间件,这个中间件可以是内存数据库,也可以是消息组件,根据中间件的不同有以下 2 种实现方式。 + + +把需要的监控数据存在静态 map 里,接口读取 map 里的数据返回给前端。这种方法虽然有利于初学者快速实现,但它的数据是存在内存中的 ,并且没有做持久化处理,容易出现丢失的情况,所以我们一般只在演示中使用。 +把数据存到消息队列里面,接口将消费队列的数据返回给前端。这是目前在互联网公司中较为常用的使用方式,在高并发下可靠性也不错。 + + +下面我来讲解下第 2 种方式的代码: + + +新建一个类继承 ResultCollector 重写 sampleOccurred 方法,使用 Kafka 接收消息; + + +public class CCTestResultCollector extends ResultCollector { + + public static final String REQUEST_COUNT = "requestCount"; + + public CCTestResultCollector() { + + super(); + + } + + public CCTestResultCollector(Summariser summer) { + + super(summer); + + } + + @Override + + public void sampleOccurred(SampleEvent event) { + + super.sampleOccurred(event); + + ...... + + //代码片段,使用 kafka 发送消息 + + producer.send(new ProducerRecord("monitorData","requestCount", requestCountMap.get(REQUEST_COUNT) == null ? 0 : (requestCountMap.get(REQUEST_COUNT) + 1))); + + } + +} + + + +后端获取存储的实时采集数据,这一步是后端获取数据并进行计算,生成的数据给前端展示使用。 + + +@PostMapping("getMonitorData") + + public Result getMonitorData(@RequestBody MonitorDataReq monitorDataReq) { + + Map monitorDataMap = new HashMap<>(); + + Long monitorXData = monitorDataReq.getMonitorXData(); + + ...... + + //kafka 消费消息代码片段,仅做示例演示 + +while (true) { + + //获取 ConsumerRecords,一秒钟轮训一次 + + ConsumerRecords records = consumer.poll(Duration.ofMillis(1000)); + + //消费消息,遍历 records + + for (ConsumerRecord r : records) { + + System.out.println(r.key() + ":" + r.value()); + + if("requestCount".equals(r.key())){ + + //r.value 便可以获取到我们上个代码片段发送的消息,然后对 requestCount 做计算,计算后的值 put 到 monitorDataMap 后返回给前端; + + ...... + + } + + } + + } + + return Result.resultSuccess(null, monitorDataMap, ResultType.GET_PERFORMANCE_REPORT_SUCCE + + } + + +实现后的效果图如下: + + + +图 5 :效果图 + + +其中横坐标是时间,纵坐标是实时处理能力的展示,可以看到每秒请求次数在 400 ~ 600 之间波动。 + + +总结 + +这一讲我主要介绍了性能测试平台的功能模块划分,JMeter API 核心功能的 4 个阶段:环境初始化、脚本加载、测试执行和结果收集,并对脚本构建的 2 种方式和获取监控数据部分的代码实现思路做了一个详细的分析,同时贴出了关键部分的代码。 + +希望这一讲能够对你在开发性能测试平台时有所帮助,特别是关于平台实现还没有找到切入点的同学,性能测试平台开发相关的大多数需求都可以在这一讲的基础上扩展。 + +到此,我们对于工具的学习也告一段落了,通过模块一的学习,你不仅知道了工具的原理,还知道了它们的基础使用方法以及拓展方法。希望你也能在日常工作中,把这些工具用起来,有任何问题,都欢迎在留言区交流。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/06Nginx在系统架构中的作用.md b/专栏/说透性能测试/06Nginx在系统架构中的作用.md new file mode 100644 index 0000000..bd79415 --- /dev/null +++ b/专栏/说透性能测试/06Nginx在系统架构中的作用.md @@ -0,0 +1,338 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 06 Nginx 在系统架构中的作用 + 通过上一模块的学习,相信你已经掌握了 JMeter 工具的核心用法和技能,并且可以在 JMeter工具使用方面游刃有余。这些内容不仅仅可以帮助你提升工作效率,而且能够通过二次开发解决团队内部的定制化需求。 + +这一讲我将带你认识一个常用的高性能中间件 Nginx,在正式学习该讲之前,我先跟你聊聊为什么要学习 Nginx,有一位细心的读者给我留言: + + +第二模块好像都是在围绕如何写一份优秀的性能方案展开,为什么有一篇关于 Nginx 的文章呢? + + +首先不得不说这个同学的行为很值得我们学习,通过大纲尝试去理清学习的整体架构和逻辑。 + +很多同学向我反馈在写方案时有一个核心痛点,即不知道如何制定性能测试的目标。都说要参考真实数据,公司也没有提供相关的查询接口,所以不清楚去哪里获取用户的访问数据。而 Nginx 作为业内最常用的代理服务器,较为详细地记录了用户的访问数据,而且在分布式部署性能优化方面也发挥了积极的作用,所以说到性能测试,Nginx 是不得不提的一个中间件。 + +本讲就带你学习 Nginx 在应用架构中的作用,并从性能测试角度看如何利用 Nginx 数据统计用户访问量。 + +Nginx 重要的两个概念 + +代理 + +首先要来解释一下什么是代理,正向代理和反向代理是什么意思?各自作用是什么?不少同学经常听到这些名词,但往往分不清楚具体区别是什么。 + +什么是代理? + +举个例子,比如你很想到某公司去做测试,对方公司的测试主管并不认识你,你也不知道这位测试主管的联系方式,但是你的朋友小王认识,他帮你推荐了简历,此时的小王就起到代理的作用,相当于一个渠道。 + +正向代理 + +正向代理的特点是你非常清楚地知道你要去哪儿,访问什么服务器,但服务器并不关心你的出发地是哪里,它只知道你从哪个代理服务器过来。 + +举个例子,北京去哈尔滨的高铁班次,对于目的地哈尔滨而言,它只知道这部分人是从北京过来的,但是并不清楚这些人之前是不是先从上海或者其他地方先到北京,再转车过来。 + +反向代理 + +刚刚说了正向代理,那反向代理又是什么呢?我先来说一下应用场景,比如我们的内部服务器集群,是不可能直接暴露出来让外网访问的,这样安全风险就非常大;再比如现在很多网站为了提高性能都采用了分布式部署,通过多台服务器来缓减服务端的压力,这些都可以通过 Nginx 来完成。 + +那我们的外网用户如何能够访问到内部的应用呢,Nginx 可以暴露端口给外网用户访问,当接收到请求之后分发给内部的服务器,此时的 Nginx 扮演的是反向代理的角色。这样一个过程,客户端是明确的,但对于访问到哪台具体的应用服务器是不明确的。就好像一个上海飞北京的班次,可能还有很多乘客到达北京之后会去沈阳、哈尔滨等,对于出发地上海而言,这个是不关心的。 + +负载均衡 + +负载均衡是 Nginx 最重要也是最常见的功能,为什么需要负载均衡呢?你可以想一想,比如你线上只有一台应用服务器,如下图所示。 + + + +但是随着用户体量的上升,一台服务器并不能支撑现有用户的访问,那你就会考虑使用两台或者多台服务器,如下图所示: + + + +那用户如何能够相对均匀地访问到这些服务器呢,这就需要你去了解 Nginx 的负载均衡策略,简单来说,就是 Nginx 如何分发这些请求到后面的应用服务器集群,下面我介绍下 Nginx 的三种分配策略。 + +(1)轮询 + +也就是使用平均分配的方式,将每个请求依次分配到配置的后端服务器上。除非有服务宕机,才会停止分发。如下代码所示: + +upstream localhost { + +//分发到各应用服务 + + server 127.0.0.1:7070; + + server 127.0.0.1:7071; + + } + + server{ + +//Nginx核心监听端口 + + listen 8012; + + server_name localhost; + + location / { + + proxy_pass http://localhost; + + proxy_set_header Host $host; + + proxy_set_header X-Real-IP $remote_addr; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + } + +} + + +(2)权重 + +权重即配置轮询的比重,为什么需要这么配置呢?在真实的互联网场景下,很多服务器上都会配置多个应用,这样会导致每台服务器的资源占用不一致,所以在分布式部署配置下也需要注意这一点: + + +相对空闲的机器可以多配置访问比例; +比较繁忙的机器可以少配置一些。 + + +如下代码所示,其中 ip1、ip2 以及 port 需要配置你实际的部署 ip 和 port。 + + upstream test { + + server ip1:8080 weight=9; + + server ip2:8081 weight=1; + +} + + +(3)ip_hash + +但上面两种配置方式在电商场景下有个很常见的问题,比如你登录了一个网站,登录信息已经保存到 a 机器,但当你做后续操作时的请求会到 b 机器,那么就获取不到你原来登录的信息,此时你就需要重新登录了。这样的情况是用户肯定不能接受的,ip_hash 模式就可以很好地解决这个问题,让每次访问能基于同一用户访问固定的服务器。 + +ip_hash 模式配置示例如下: + + upstream test { + + ip_hash; + + server localhost:8080; + + server localhost:8081; + +} + + +接着我们来看下如何基于 Nginx 记录的数据去分析用户访问请求分布,在讲下文之前,按照我的习惯,我想先说一说为什么我要通过 shell 命令去分析 Nginx 日志。 + +首先对于测试同学而言,比较熟练地掌握了 Python 或者 Java 的用法,但对于 Linux 中的 shell 命令不是很熟悉,也有同学说 shell 能做的我觉得 Python 也可以实现。我想对于性能测试而言,处理效率是一个我们都比较关心的问题。在 Linux 服务器上,你可以处理数据的级别达到百万条以上,对于 Linux 上的文本操作而言,相对于 Python 或者 Java,shell 在处理效率方面有着得天独厚的优势,所以掌握基础的 shell 命令还是必要的。 + +再说我为什么会选择 Nginx 日志去分析,这也得从互联网行业的现状说起: + + +对于大型互联网公司,关于获取分析日志我想早已有平台化支持,一键就可以导出你需要的用户数据访问报表; +而对于中小公司的测试来说,去哪里获取可能都不是很清楚。 + + +所以我选择了使用 Nginx 这种比较原生的方式去讲解,这样对于使用过平台化操作的同学也可以了解一些底层的逻辑操作,也让没有接触过这方面数据统计的同学掌握其中一种实现方法。 + +Linux 的 shell 命令 + +Linux 的 shell 命令常见的文本操作命令有 awk、sed、sort、wc 等,通过这些命令的熟练掌握和搭配使用,相信你可以对 Linux 服务器上的文本处理如鱼得水。 + +awk + +awk 可以将文本中的内容按行去读取,然后将读取出来的行按照规定的分隔符去提取你所需要的内容。 + +awk 常用参数是 -F 指定分隔符。 + +比如以下代码就是以 : 为分隔符,寻找以 root 开头的行数据,打印第 7 列。 + +# awk -F : '/^root/{print $7}' /etc/passwd + +/bin/bash + + +以下代码表示以 begin 开头、end 结尾,打印第 1 列数据。 + +代码块示例 + +# awk -F : 'BEGIN{print "begin"}{print $1} END{print "end"}' /etc/passwd + +begin + +root + +.. + +end + + +Sed + +Sed 是一个流编辑器,一次只能处理一行内容,需要注意的是 sed 并不改变文本本身的内容,它只是把结果存放在临时缓冲区中。 + +sed 常用的参数有: + + +a 表示新增; +i 表示插入; +c 表示取代; +d 表示删除。 + + +举个例子,我们设置一个文本文件,每行只有一个数字,如下所示: + +[root@JD data]# cat sed.txt + +1 + +2 + +3 + + +在第一行下新增 4: + +[root@JD data]# sed '1a 4' sed.txt + +1 + +4 + +2 + +3 + + +看下原来的文本,你会发现没有任何改动,如下代码所示: + +[root@JD data]# cat sed.txt + +1 + +2 + +3 + + +Sort + +Sort 的默认方式就是把第一列根据 ASCII 值排序输出。常用参数有: + + +-n,依照数值的大小排序; +-r,以相反的顺序来排序; +-k,选择以某个区间进行排序。 + + +举个简单的示例,将上述的 sed.txt 倒序输出,如下代码所示: + +[root@JD data]# sort -r sed.txt + +3 + +2 + +1 + + +uniq + +uniq 用于检查或者统计文本出现的重复行,常用参数是 -c,它用于连续重复行次数的统计。 + +我们构造一个 uniq.txt,如下所示: + +[root@JD data]# cat uniq.txt + +hello + +hello + +cctester + +cctester + +cctester + +com + + +然后对 uniq.txt 进行重复数据统计,并根据重复次数由大到小排序,如下所示: + +[root@JD data]# uniq -c uniq.txt |sort -r + + 3 cctester + + 2 hello + + 1 com + + +学完了这些基础命令,我带你来看 Nginx 日志分析,如果你不清楚你的 Nginx 日志地址,查看nginx.conf 文件的配置即可,指定日志路径如下所示: + + access_log /data/logs/access.log main; + + +其中部分的日志显示,如下所示: + +120.204.101.238 - - [29/Nov/2020:14:19:39 +0800] "GET /hello/map HTTP/1.1" 200 202 + +47.92.11.105 - - [29/Nov/2020:14:19:39 +0800] "GET /hello/map HTTP/1.1" 200 202 + +185.39.101.238 - - [29/Nov/2020:14:19:39 +0800] "GET /hello/list HTTP/1.1" 200 150 "- + +101.132.114.23 - - [29/Nov/2020:14:19:39 +0800] "GET /hello/list HTTP/1.1" 200 150 "- + +120.204.101.238 - - [29/Nov/2020:14:19:39 +0800] "POST /v1/login HTTP/1.1" 200 36 "- + + +观察上述的日志,是以空格为分隔符号,第一行第一列是 120.204.101.238,第一行第二列是 -,以此类推,打印第 7 列,如下所示: + + awk '{print $7}' access.log + +/hello/list + +/v1/login + +/hello/list + +/hello/map + + +你也可以自行验证下输出是否符合预期。 + +接着我基于这份日志统计访问接口的比例分布,使用如下命令: + +cat access.log |awk '{print $7}'|sort|uniq -c|sort -n -k -r + + +这个命令,是提取 acccess.log 的第 7 列,也就是接口路径: + + +先 sort 排序,这样可以将相同的接口访问路径合并一起; +再使用 uniq -c 统计连续访问的次数; +最后根据访问次数排序,便可以得到如下结果。 + + +[root@JD logs]# cat access.log |awk '{print $7}'|sort|uniq -c|sort -n -k 1 -r + + 87280 /hello/list + + 18892 /hello/map + + 12846 /v1/login + + +通过输出结果可以看出第一列就是给定日志内的接口访问次数统计,比如 87280 就是 /hello/list 的访问次数。 + +总结 + +通过本讲的学习,你已经相对全面地了解了 Nginx 在系统架构中的作用,通过对访问日志的分析,你也能够获取用户的基本访问情况。在实际工作过程中,面对没有原始访问数据的情况下,你就多了一条思路、一种解决方案。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/07你真的知道如何制定性能测试的目标吗?.md b/专栏/说透性能测试/07你真的知道如何制定性能测试的目标吗?.md new file mode 100644 index 0000000..328efb3 --- /dev/null +++ b/专栏/说透性能测试/07你真的知道如何制定性能测试的目标吗?.md @@ -0,0 +1,160 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 07 你真的知道如何制定性能测试的目标吗? + 在上一讲中,我介绍了高性能中间件 Nginx,相信你已经有了一定层次的了解,这一讲我将带你学习如何制定性能测试的目标。 + +之前有同学咨询过我,在测试执行过程中他并不清楚测试得到的结果到底能不能满足这次活动的需求。经过我的追问,发现他连本次活动规则涉及的模块和接口都不是十分清楚,像这样的测试就是没有做性能测试目标的分析。虽然说做了性能测试,但几乎可以说是无效测试,因为没有做目标分析的性能测试会与真实的活动场景相差甚远。 + +举一个我遇到的典型案例,一次大促前,在性能测试过程中测出来的数据指标都还不错,然而大促时并没有达到测出来的指标,服务就宕机了。在分析原因后,我们发现有一个接口平时访问量很低,所以并没有纳入性能测试的范围。但这个接口存在大 Key,在大促中达到中间件流量阈值,导致了网站的不可用。 + +所以说,测试目标极其重要,是性能测试执行的先决条件。 + +制定目标可以确定既定的测试范围内需要达到的性能预期结果。制定目标后,你才会对本次性能测试的核心目标有清晰的认知,并指导你进行后续的测试活动,包括测试所需要的资源以及测试的停止条件等。 + +在回答如何制定性能测试指标之前,要搞清楚我们衡量性能测试的指标都有什么?通过本讲的学习,你将会对性能测试指标如 TPS、响应时间、报错率有更深入的了解,不仅仅知道它们各自代表的含义,还会知道它们是如何制定出来的。 + +衡量指标 1:TPS + +衡量性能测试的指标,很多人会说是并发。并发指同一时间节点发生的事情,但这个同一时间并不是一个标准的度量,也不是我们性能测试直接测量出来的指标。在性能测试中往往是通过在工具中增加虚拟用户数得到的接口每秒的调用量去衡量。 + +在实际生产中,无论是网关还是服务通常都是记录一定时间内的访问请求次数,所以在业内,性能测试往往以 TPS(Transactions Per Second)作为最重要的度量指标,因为它具备可度量和通用性的特质。 + + +可度量指 TPS 是真实客观且明确的衡量指标; +通用性指无论在运维角度还是测试角度,TPS 都可以达成一致的定义。 + + +衡量指标 2:响应时间 + +除了 TPS,第二个比较重要的衡量指标是响应时间。响应时间和用户体验密切相关,我们往往把一次请求从客户端发出到返回客户端的时间作为响应时间。在实际工作中,我们会以 TPS 的量级来限制响应时间必须在多久之内。以下图 1 为例,从最左侧的客户端到最右侧的数据持久化再返回到客户端,这样一个来回的过程就是一次完整的请求响应时间。 + + + +图 1:请求响应的过程图 + +上图描述的是在正常情况下的响应流程,但当你有了一定的性能测试实践之后,你会发现这样的过程并不是绝对的。比如有的业务第一次在数据库请求到数据后,应用层本地缓存会将数据存储在应用服务器上,也就是接下来在缓存有效时间内不会再去数据库取数据,而是在应用层得到数据后就会直接返回,所以响应时间会比第一次低不少,这也是随着性能测试的进行响应时间变低的原因之一。 + +衡量指标 3:报错率 + +这个指标也是比较重要的,报错率的计算方式是在统计时间范围内不符合返回期望的请求数除以总共的请求数。在测试中,这一指标不符合期望的话一般体现在对结果的校验上,我一般会分为三个层面进行校验: + + +状态码的校验,这在性能工具中不需要特别设置,如 4XX、5XX 这样的状态码会直接报错; +业务层面的校验,为了保证业务的基本准确性,会通过返回的数据包进行校验; +数据库校验, 相对于业务测试,性能测试的每一次请求不会都做数据库校验,这样会影响性能测试结果,我一般会在一轮性能测试之后去统计落库数据的数量和状态是否正常。 + + +以测试登录为例: + + +如果用户名和密码不匹配会返回一段错误报文,不需要走正确流程中的校验逻辑; +如果用户名和密码匹配,一般会有多层校验。 + + +这二者存在较大的差别,在实际压测过程中也存在业务没有校验,但已经发生错误的情况,这是不能及时发现的。 + +如何制定性能测试的指标 + +综上所述,我认为衡量性能最重要的三个指标是TPS、响应时间、报错率。那如何制定性能测试的指标呢?你的依据是什么呢?我列举几个面试者的常见回答: + + +我是根据二八原则,老板说我们百万日活,80% 的用户在 20% 的时间段里访问,响应时间是根据业内的 2-5-8 来制定; +我是根据竞品数据分析,他们产品的 PV 应该是百万级,所以我们的产品也是这么制定的; +这个指标是业务定的,他们和开发讨论过,应该没什么问题。 + + +以上回答不仅从道理上讲有些牵强,而且也没有任何制定性能测试可以参考的有效信息。性能测试是一项非常严谨的工作,通过间接或者普适规则不可能满足具体特定的分析,所以对这个问题的理解基本可以判断一位同学是否真正做过性能测试。 + +那有人会问:“你是如何做性能测试目标分析的呢?” + +对我来说,性能测试的目标可以分为 4 种。 + +第一种是以衡量系统的处理能力为核心目标。这一般是性能测试的主要目标,用来评估当前系统的处理能力和容量方面的规划。我认为评估这个目标最重要的是对数据的客观分析,那我们需要什么样的数据呢? + +对于每一个接口都会有访问计数,这是目前业内比较常见的,也是衡量接口访问能力最准确的指标之一。一般大公司会自己开发相应的监控工具,发展中的公司也会使用一些开源或者商业工具进行监控。有了工具之后我们应该从哪些维度去统计这些数据呢? + +通常我们会通过时间维度和服务维度来统计。 + + +时间维度 + + +根据经验,我们一般会考虑以大促当天的访问量来测试,但从分析的角度,一般前后半个月的时间都应该考虑在测试范围内。 + +首先我们需要确定大促前后哪些天数的访问量是比较高的,这里我会先以天作为维度,如图 2。可能你比较疑惑,为什么其他节点也比较高呢? + + + +图 2:大促前后访问量 + +想想一些电商大促的具体活动场景就不会感到奇怪了。现在很多电商,如京东的“双十一”活动可能从 11 月 1 日就开始了,其中穿插了一些品类活动和推送,所以有流量冲高的情况也是比较正常的。因此,我一般会对最高峰和次高峰都进行分析。 + +当我们选取了天数之后,再以小时为维度,确定哪些时间节点的访问量是比较高的。图 3 中,我选取了其中一天,按照小时分布再统计。这样一来,我们就得到了时间维度上需要的数据。 + + + +图 3:各小时访问量 + + +服务维度 + + +那什么是服务维度呢?以目前比较流行的电商微服务架构为例,我一般会做服务级别的拆分。先画一个示意图,方便你对微服务有个简单了解。 + + + +图 4:微服务架构图 + +网关一般是请求进入应用层的第一个入口,也是统计网站入口访问量的方式之一。当我们的请求通过网关之后会下发到各个业务应用服务,如图 4 中所示的服务 A、B、C,我会按照确定的时间节点去统计各个服务的访问量数据。完成服务级的访问数据统计之后,我会继续按照时间维度统计服务下的接口访问数据。你可以看到,每个服务和每个接口的调用比例都不一样,如图 5 所示。 + + + +图 5:各个服务的调用比例 + +我曾看到有的同学使用 JMeter 写性能测试脚本时,用接口串联的方式编写,比如登录 → 浏览商品 → 添加购物车,这样一个流程的方式。这也就默认登录的用户浏览了商品后都会添加购物车,这是一种典型的自动化测试的思维,在性能中不会存在这样的同等分布。 + +一般来说,在同一个时间点都会有不同的比例,比如早晨 8 点,有 1.2w 次登录请求、4w 次浏览商品的请求,而下午这个数据就会产生变化。所以我会按照服务入口将接口调用也统计出来,结合时间维度和服务维度做出性能测试的常见模型。 + +说到这里你可能会有两个问题。 + +问题一:选取了这么多时间节点,应该会产生多个比例模型吧?我们需要对每一个比例模型都进行测试吗? + +确实会产生多个比例模型,而且每个都需要测试,因为线上存在这样的访问趋势,我们应当都要覆盖。 + +问题二:你上面说的步骤好多,感觉需要很多时间,听说流量复制能一键解决? + +流量复制是不能解决指标制定的问题的,并且在落地过程中还会花费大量的时间和人力成本,所以关于目标的制定和分析我们需要一步一步进行,花费一定的时间是难以避免的,并且线上未来的访问趋势只能尽可能覆盖,无法完全预测到。这一讲不对这部分内容展开描述,如果有兴趣可以了解下 goreplay、jvmsandbox 等技术。 + +上面是我描述指标分析的第一种类型也是最常见的类型:从处理能力的角度去说如何制定性能测试的目标。那是否还有其他类型的性能测试目标呢? + +我们来看第二种性能测试目标。一个项目刚刚上线,用户访问量还不是很高,这时我们会更关注系统的健壮性,如内存泄漏、并发死锁、超卖问题,这些也需要在性能测试方向上进行。 + +值得一提的是,完成这部分性能测试并不需要在生产上进行,在线下性能测试环境也可以模拟,甚至能够更早更快地发现问题。我们都明白在测试领域,提早发现问题修复成本是最低的。如果你在上线后才发现这些问题,然后再去修复,修复完还要经过多轮的业务测试,这样一来,不管是时间成本还是人力成本都会大幅度增加。 + +第三种目标是系统的稳定性。 + +关于如何维护系统稳定性一直是一个比较大的话题。性能测试也是保障稳定的一部分,我认为,性能测试场景中的稳定性测试包含 2 个方面。 + +一个是正确率,这不一定要在高并发下完成,但我们要保证业务长时间运行的正确率能够达到 99.9999% 以上;另一个是处理能力,可以选取性能测试场景中的混合场景来执行(这部分会在第 08 讲中详解),这里我们需要观察两点: + + +整体处理能力是否稳定,会不会存在处理能力的下滑; +接口之间的比例是否稳定,随着时间的进行接口之间的访问比例会不会偏离。 + + +第四种性能测试目标是专项能力是否达标。 + +刚刚列举的几乎都是以业务接口为测试目标,其实在实际的压测活动中,也存在中间件甚至硬件的性能测试,比如 Nginx、Kafka、防火墙等。这些往往不会作为最终的性能测试目标,但会在全链路排障和专项测试中有所涉及。这部分的性能测试基本上是用来判断当前的环境配置的节点数,以及配置所能达到的最大处理能力,为全链路性能测试提供数据支撑。 + +总结 + +在性能测试领域最直接的衡量指标是 TPS、响应时间和报错率,三者相互依赖,不能孤立地只分析某一项指标。 + +性能测试是一项严谨的工作,我们要熟悉制定的方法,一切以数据作为基础,拒绝在实践中凭感觉拍板的现象。基础数据建设也是性能工作的一部分,当我们拿到数据之后需要根据上文描述的时间维度和服务维度去分析,原则上分析的颗粒度越细越好。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/08性能测试场景的分类和意义.md b/专栏/说透性能测试/08性能测试场景的分类和意义.md new file mode 100644 index 0000000..e962f8e --- /dev/null +++ b/专栏/说透性能测试/08性能测试场景的分类和意义.md @@ -0,0 +1,114 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 08 性能测试场景的分类和意义 + 上一讲我带你学习了性能测试的分类和如何制定性能目标,这一讲我将带你学习性能测试场景的分类和意义,这也是决定你是否能做好性能测试的关键。 + +性能测试场景的重要程度类似于业务测试的 case,case 是你进行业务测试的指引,case 是否完善也直接决定了测试的覆盖率。同理,场景是传递执行性能测试的步骤和目的,关于这两点是你一定要清楚的。 + +首先带你认识下我认为最重要的三个性能场景:基准场景、单接口负载场景、混合场景负载测试,这几个场景也是我经常说的“基石场景”。接下来为你讲解这三个场景是怎么操作,以及核心目的是什么? + +基准场景 + +基准场景是指单线程或者少量线程(一般在 5 个线程以下)对单接口进行测试,然后将测试结果作为基准数据,在系统调优或者评估的过程中,通过运行相同的业务接口比较测试结果,为系统的优化以及后续测试流程提供决策数据。 + +有人觉得基准测试并不是在高并发下进行的,不算是性能测试,但我认为这其实是性能测试中重要的基础步骤,它有以下作用: + + +验证测试脚本及测试参数的正确性,同时也可以验证脚本数据是否能够支持重复性测试等; +通过少量线程访问系统获取结果数据,作为对比参考基准; +根据测试结果,初步判断可能成为系统瓶颈的场景,并决定是否进行后续的测试; +基准场景的结果被一部分公司作为上线的基线指标,不达到要求是不允许上线的,这样的场景也经常被固化成自动化的脚本定时触发和巡检。 + + +单接口负载场景 + +单接口负载场景就是通过模拟多线程对单接口进行负载测试。我的具体做法是选定线程数后持续循环运行一定时间,比如分别运行 100 线程、200 线程、300线程等,一般相同线程数运行 10~15 min,然后获取事务响应时间、TPS、报错率,监测测试系统的各服务器资源使用情况(CPU、内存、磁盘、网络等),把具体数据记录之后再开始跑下一个线程数。每一组线程数级别会有对应的 TPS,直到你找到 TPS 的拐点。如下图所示,横坐标是线程数,纵坐标是 TPS,线程数增加到 400 时出现了拐点。 + + + +这里需要注意的点有两个。 + + +使用工具做性能测试时,动辄就是上千的线程数,所以如果你是一位初学者,我更加倾向于你从一个相对比较低的线程数梯度增加,这样才能够比较清晰地找到 TPS 的拐点。 +我还建议为每个虚拟用户级别做单独的场景,网上绝大部分的教程,在一个场景中做了很多梯度(如下图所示),这样只是看上去简单方便一些,其实很不利于分析和诊断,这个方式我并不推荐。因为并不是每一个量级的性能表现都是类似的,而且一个场景多梯度出来的报表也可能没你想象中的清晰明了。在 JMeter 的聚合报告中还会将结果数据平均化,这样的方式并不能准确地记录每个线程梯度对应的 TPS。而在一个场景里先固定虚拟用户可以将自己的精力聚焦在诊断上。 + + + + +混合场景负载测试 + +混合场景是性能测试中最重要的场景之一,这个场景是为了最大程度模拟用户真实的操作。真实的线上操作不只有单接口的操作,一定是多种业务同时在进行,比如张三在浏览商品,李四在添加购物车等。 + +所以混合场景测试会将多个接口按照实际大促时候的比例混合起来,然后增加线程数找出多个接口 TPS 的和对应的峰值。这个比例也是混合场景的关键,在[《07 | 你真的知道如何制定性能测试的目标吗?》]中已经较为详细地阐述了制定比例的方法,本讲就不再赘述。加用户运行的基本策略可以参考上文的单接口负载测试。混合场景执行除了要观察总的 TPS,还有一个非常关键的因素就是如何控制接口之间的调用比例,使其不能偏离预期。 + +如何使用 JMeter 去控制场景比例? + +相信你已经知道线程数可以改变接口的 TPS,但是如果每次通过线程数调整这个过程会比较烦琐。JMeter 提供了一个能较好地解决这个问题的插件,叫作吞吐量控制器,它在逻辑控制器组件中,如下图所示: + + + +我来简单介绍一下这个插件配置规则,默认的情况下使用的是百分比模式,也就是 Percent Excutions。吞吐量一栏对应的是 TPS 占比,我用 login 和 register 这两个接口来模拟下, + +login 接口配置比例是 80%,如下图所示,剩下的 20% 配置给 register。 + + + +看下运行后的效果,我直接在 JMeter 中添加聚合报告元件,如下图所示: + + + +实际计算下来的值为 1778.2⁄2222.7≈0.8,这个数据是比较准确的。 + +以上是我所说的基石场景,包括基准测试、负载测试、混合场景测试等,这三个场景是有依次执行的顺序关系的,按照顺序执行更容易发现问题且减少不必要的工作,比如你连基准测试都不通过,就没有必要进行负载测试了。所以我们在做每一次性能测试时,都不应该省去或者颠倒上述的场景步骤。 + +接着我带你继续学习其他性能测试场景,为了达到相对应的性能测试目的,这些场景可以根据需求进行选择。 + +异常性能测试 + +性能测试也是存在异常测试的,顾名思义就是在系统异常的情况下看系统的处理能力或者是通过处理后的恢复能力是如何的。 + +比如在架构的高可用方面,遇到服务的上下线、数据库的主从切换等这些情况的时延是多少、处理能力能不能达到预期标准。另外在目前的电商应用架构中,大促遇到紧急情况经常需要限流和熔断,可能你经常听到这两个词,但不是特别清楚两者的区别。 + +限流就是控制单位时间内的请求量,比如说早晚高峰坐地铁,很多入口都会放隔离带,降低乘客流动速度,这就是一种限流方式。 + +熔断就比较直接了,当判断到调用的依赖服务报错到达一定数量后,直接返回一个既定的数据,将不再访问该服务。就像家中的保险丝一样,到达一定条件后,会自行断电,以保障电路安全。所以我们也会测试触发限流和熔断所设置的阈值,并观察在触发后的系统表现是如何的。 + +稳定性性能测试 + +性能测试中的稳定性测试是通过给系统加载一定压力的情况下,运行较长一段时间,验证系统是否稳定。通常是采用典型混合场景,应用系统运行 72 小时,查看系统运行指数是否平稳。 + +稳定性测试的注意点 + +稳定性测试在性能测试中是一个相对严苛的场景,因为在 72 小时中可能发生的事情太多了,不仅仅是业务承载的问题,还包括你准备的数据、客户端稳定性,甚至硬件设备断网断电等。任何一项意外的发生,都会造成场景的失败。稳定性测试的监控级别也应当更高,一旦有问题,立即钉钉或者电话通知,所以稳定性测试之前需要有充足的预案和监控报警。 + +经常被问到的问题 + +什么情况下可以停止负载测试? + +有同学问我,无论是单接口负载测试还是混合场景的负载测试都是梯度增加线程数,那线程数增加到多少程度才可以停止呢? + +首先我们结合图 1 可以看到,在梯度增加线程数时,TPS 一般会随之发生变化,当你能够根据 TPS 的变化找到相应的峰值且这个值也是符合预期时,便可以停止负载测试了。 + +但是现实的情况并没有这么理想,很多时候当你还没有发现图 1 中的拐点时,接口就可能在报错了,那遇到这样的情况是继续测试还是停止测试呢?这其实是一个约定的问题,即测试的结束条件是什么? + + +理想的情况下自然是达到目标就停止了。 +那不理想呢?根据我的经验,会在测试之前组内协商出场景异常情况下的停止条件,比如 CPU 达到 70%,响应时间超过 500 ms,接口正确率低于 99% 等,当触发这些条件时,我将不会继续加线程进行测试了。 + + +混合场景我选取哪个线程梯度的访问量进行测试? + +这个问题经常被问到,一些同学喜欢基于峰值处理能力去进行稳定性测试,这是一个很严格的要求。其实标准因公司的实际体量而异。今年的某电商双 11 实时支付峰值达到 50 w/s,有可能这个值也未必能平稳跑 72 h,但以这个访问量为执行标准已经足够用了。所以对于不同的公司而言,自行选择适当的线程梯度就可以。我经常听到一句话,今年的峰值流量就是明年的正常流量,对于这样飞速发展的公司,我想还是需要基于峰值去执行稳定性测试场景。 + +关于场景的命名一直有同学很困惑,感觉对于同样一个场景,怎么有的人说是混合场景,还有同学说是容量场景。关于场景名字的叫法,不仅不同的公司会不一样,就包括参考资料上也没有形成非常统一的规范,但我认为并不需要用很多精力研究场景的叫法,但你一定要能描述清楚场景的核心目的是什么,执行步骤是什么,这才是需要向你的协作伙伴传递的最准确的信息。 + +总结 + +本讲带你系统地学习了性能测试的场景设计,关于三大基石场景的作用以及意义,混合场景的操作方法以及稳定性场景的注意事项,这些是你在性能测试执行过程中经常遇到的问题,也许你还做过其他性能场景,欢迎在留言区留言和我交流。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/09如何制定一份有效的性能测试方案?.md b/专栏/说透性能测试/09如何制定一份有效的性能测试方案?.md new file mode 100644 index 0000000..39461ef --- /dev/null +++ b/专栏/说透性能测试/09如何制定一份有效的性能测试方案?.md @@ -0,0 +1,110 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 09 如何制定一份有效的性能测试方案? + 上一讲我们学习了性能测试的场景,并且明确了每个场景的核心意义,这一讲我将带你学习如何做好一份性能测试方案,相信你对测试方案这个概念并不陌生,那如何做好一份性能测试方案呢?这个方案能解决什么问题呢?这一讲我们来一起探索。 + +什么是性能测试方案? + +性能测试方案,通俗一点说就是指导你进行性能测试的文档,包含测试目的、测试方法、测试场景、环境配置、测试排期、测试资源、风险分析等内容。一份详细的性能测试方案可以帮助项目成员明确测试计划和手段,更好地控制测试流程。 + +性能测试方案的要点以及解决的问题 + + +为测试活动做计划,每项测试活动的对象、范围、方法、进度和预期结果会更透明化。 +制定出有效的性能测试模型,能够排查出性能问题,从而更符合真实场景。 +确定测试所需要的人力、时间和资源,以保证其可获得性、有效性。 +预估和消除性能测试活动存在的风险,降低由不可能消除的风险所带来的损失。 + + +测试方案需要包含哪些内容? + +性能测试方案是在你正式进行性能测试之前的工作,通过前几讲的学习你已经知道了性能方案中的必备内容。 + +1.制定性能测试目的 + +性能测试目的是你做一次测试首先要考虑的内容。是要完成既定的指标,还是验证超卖问题,抑或是验证并发下的稳定性问题。如果是验证指标,你的指标如何制定,拿到业务访问数据如何转化成为性能测试模型,在[《07 | 你真的知道如何制定性能测试的目标吗?》]中已经说得比较详细了。 + +2.性能测试场景梳理 + +对于性能测试有哪些场景,每种场景的目的是什么,[《08 | 性能测试场景的分类和意义》]已经给了你答案,你需要根据性能测试的目的进行场景的设计。 + +那除了这些,性能测试方案还需要包含哪些内容呢? + +3.确定被测业务的部署架构 + +被测的业务部署架构是什么意思呢,简单来说就是被测服务涉及哪些组件,每个组件部署在哪些服务器上,服务器的配置是怎样的。你需要画一个部署架构示意图,有了这张图,才能知道如何做到全貌监控,以及遇到问题从哪些服务入手。 + +我用一个自己画的架构示意图来说明这个问题,如下图所示,这是一个经典的链路:从客户端发起到服务端,服务端从代理层到应用层,最后到数据层。需要注意的是,你需要明确被测的环境里的各个服务有多少节点,比如客户层的压测机节点有几台,分别在哪个网段。同理我们可以去调研服务层和数据层的节点。 + + + +4.对测试数据进行调研 + +关于测试数据调研,包含了非常多的内容,对于业务测试来说数据调研就是获取必要的参数来满足既定的场景可以跑通。那对于性能测试来说,需要做哪些方面的数据调研呢,我带你一一解读。 + +(1)数据库基础数据量分析 + +数据库的基础数据量就是目前线上数据库实际的数据量,为什么要统计基础数据量呢?很多公司往往有独立的性能测试环境,但是数据库的数据量与线上相比差距较大,可能出现一条 SQL 在性能测试环境执行很快,但上了生产却会很慢的问题。这就导致测试觉得该测的都测了,但上了生产还是会有问题出现。 + +这种问题可能会因为索引缺失以及性能环境数据量较少而不能将问题暴露出来,所以在性能测试环境下的数据量一定要和生产上一致。为了达到这个目的,有的公司可以将生产数据脱敏后备份,有的则需要你自己写脚本来根据业务规则批量造数据。 + +(2)压测增量数据分析 + +除了数据库的基础数据量,我们也需要考虑一轮性能测试下来会增加多少数据量。往往增加的数据量最终落到数据库,可能会经过各种中间件如 Redis、Mq 等,所以涉及的链路可能存在数据量的激增,所以这方面需要根据增加情况制定相应的兜底方案。 + +(3)参数化的数据分析 + +关于参数化,我相信你已经通过[《02 | JMeter 参数化策略》]有了深入的了解。在这里,我还想抛出一道思考题,如何参数化订单号,你可以分别从读写接口两个层面写出你的思考或者实践。 + +(4)冷热数据的分析 + +以我的从业经历来讲,能够在方案阶段考虑到冷热数据分布的公司并不多,往往都是从性能测试结果的一些异常点或者实际产线出现的问题去追溯。接下来我就带你了解下什么是冷热数据,以及如果不对其进行分析可能会带来什么影响。 + + +冷数据是指没有经常被访问的数据,通常情况下将其存放到数据库中,读写效率相对较低。 +热数据是经常被用户访问的数据,一般会放在缓存中。 + + +在性能测试的过程中,被频繁访问的冷数据会转变为热数据。如果参数化数据量比较少,持续压测会让 TPS 越来越高。而在实际大促情况下,往往有千万级的用户直接访问,但大多都是冷数据,会存在处理能力还没达到压测结果的指标,系统就出现问题的情况。所以在需求调研时,你也需要考虑数据会不会被缓存,缓存时间多久的问题。 + +5.业务规则的调研 + +对于性能测试而言,业务规则的了解也是不可或缺的。一些公司的性能测试组在进行压测时,业务线的测试也需要协助支持压测的进行,由此可以体现业务的重要性。 + +对业务的充分了解不仅可以帮助你提高写脚本的效率,也可以帮助你构造更为真实的性能测试场景。举个简单的例子,你模拟下单的时候是否考虑商品属性,比如是单一商品还是套餐商品,下单的时候购物车里有几件商品,这些都会影响性能测试的结果。 + +6.测试监控的内容确认 + +监控是你做性能测试的重点内容之一,一旦出现问题,第一反应就是查监控,关于监控管理建设我在[《01 | JMeter 的核心概念》]中也有所陈述。对于性能测试方案,不仅需要罗列清楚你所需要的监控工具和访问方式,同时也需要层次分明地传递你监控的内容。对我来说做监控最基本的一个关键词:全。 + +怎么去理解“全”呢?先举一个典型的例子,有时候做一个新的项目,询问支持的同学有没有部署监控,他们说已经部署了,但等你真正使用的时候发现只监控了一台应用服务器的 CPU。这个例子我相信大多数人都似曾相识,所以我说的全,至少包含两个方面: + + +涉及所有服务器; +涉及服务器基础监控,包括 CPU、磁盘、内存、网络等。 + + +硬件资源的监控只能算一个层面。那完成一次性能测试都需要监控什么呢,我用一个导图给你做一个概览。 + + + +监控还有个很重要的点是设置阈值来报警,无论是线上和线下的性能测试,报警功能都是必需的。因为通过人工的观察,往往不能以最快的速度发现问题。一旦能够及时报警,涉及的人员就可以快速响应,尽可能降低风险。 + +7.性能测试排期涉及的人员 + +一般来说测试是上线前的最后一道关卡,也是发现问题的重要角色,所以项目上的风险会在测试阶段集中爆发。性能测试作为测试中的一部分,也会面临类似问题,这也考验你的项目管理能力。而且性能测试需要大量的数据和专门的环境,这部分的工作内容和资源需要更多支持,所以在你的性能测试方案中,首先要标明开展的阶段和日期,还要明确主负责人和协调人员。在此基础上还需要面对面 check 和落实。 + +你可以参考如下的表格,具体的内容需要根据公司的情况来确定。这些任务并不是从上到下依次执行,可能存在并行的情况,比如某一些公司环境是由运维人员统一部署,这部分内容就可以和性能测试需求分析一起进行。 + + + +总结 + +关于如何打造性能测试方案就讲到这里了,通过本讲的学习,你已经了解了做一份性能测试方案的基本要素和关键点。性能测试方案对于一些公司来说可能只是一份流程化的文档,但对于测试个人来说,这部分内容可以体现出你的思考和计划。尤其对于性能测试新手来说,一定要充分思考每项的意义,这样你才能快速提升。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/10命令行监控Linux服务器的要点.md b/专栏/说透性能测试/10命令行监控Linux服务器的要点.md new file mode 100644 index 0000000..5bebfa8 --- /dev/null +++ b/专栏/说透性能测试/10命令行监控Linux服务器的要点.md @@ -0,0 +1,222 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 10 命令行监控 Linux 服务器的要点 + 前面两个模块带你学习了如何使用 JMeter 工具,如何做好一份性能测试方案,第三模块我将带你进行监控的学习。在你执行性能测试的过程中,监控服务端的资源消耗等也是必备内容,监控的结果是帮助你发现问题的眼睛。然而在实操过程中发现很多同学喜欢用JMeter 工具提供的插件进行监控,但是我并不推荐你使用这种方式,原因如下: + + +指标相对简单且固定,结果数据粗糙且界面显示并不是很友好; +较大地增加了客户端压测机的资源开销,影响性能测试结果; +特定环境下,在服务器上安装插件是不被允许的,会很不方便。 + + +所以这一讲我想带你了解下监控的内容有哪些?既然不推荐使用 JMeter 自带的监控方式,那我是如何做监控的呢? + +本讲作为监控模块的第一篇,我想先聊一聊如何能够把监控这件事情做好,正所谓“磨刀不误砍柴工”,监控绝不是简单地敲几个命令,做几个图表就可以的,你需要从多角度来理解这件事情。首先我认为把监控做好需要有以下三个关键词:层次清晰、全面覆盖、定向深入。我来解释下这三个关键词代表的含义。 + +层次清晰 + +从执行一次性能测试来看,你需要监控的内容有很多,重点是要能理清楚不同的监控类型,以及分别能够解决什么问题?我从下面几个层次做下介绍,从而让你对各层面的监控做一个初步了解,也为后面的章节做一些铺垫。 + +1.硬件层 + +硬件层是最容易想到的一个层面,一般包含了 CPU 的使用率、内存使用率、磁盘和网络读写速度等,通过这些指标能够反馈出系统运行的基本情况,以及不同的 TPS 量级会消耗多少硬件资源。 + +2.系统层 + +系统层监控包括连接请求数、拒绝数、丢包率、请求超时等,相对于基础的硬件监控而言,这些指标更能够反映出目前系统存在的瓶颈,从而为根因问题的定位提供有力的线索。 + +3.链路层 + +在我看来,链路层是直接面向架构和代码的,它的监控能够帮助你更加准确地看到代码执行了哪些函数,涉及哪些服务,并且能够较为清晰地看到函数之间的调用耗时,还可以帮助你定位代码存在的问题。 + +4.业务层 + +业务层监控本意是帮助你判断用户输入是否合规,代码逻辑是否健壮。对于性能测试而言,业务层的监控可以帮助你发现脚本参数问题以及高并发下业务逻辑运行是否正常等,比如随着测试的进行,可能会存在商品库存不足的情况。如果有业务层面的监控,当库存低于某阈值时,可以进行一定的提示以规避此类问题。 + +全面覆盖 + +如果你能够完整地画出应用的部署架构图(参考第 09 讲的部署架构图),并且能够按照我说的几个层次将其完整地部署落地,我想监控这件事情至少可以给你打到 85 分,剩下来的 15 分在哪里呢?我认为除了应用层的监控,你还需要考虑底层链路的监控,比如防火墙、F5 负载均衡等,这些往往是一下子考虑不到的事情。 + +在我的实际工作中,尤其是新项目监控部署经常存在“缺斤少两”的情况。虽然在测试之前做了系统监控,但出现问题后仔细分析时,经常发现某一些机器并没有被监控到,或者监控了 CPU 又发现磁盘没有被监控上。这些问题主要是考验你的组织能力,也反映了团队是否能在性能测试上更细致更深入,毕竟性能的分析是不能放过任何“蛛丝马迹”的。 + +定向深入 + +首先通过基本的监控可以获得一些异常点,比如 CPU 高了、磁盘在等待,这些说白了是表象问题。就比如说某位同学今天发烧了,通过发烧这个现象并不能直接下定论说他感冒了,医生也需要做进一步的化验分析才可以下结论。对于监控也是这样,是否有定位根因问题的手段,CPU 高了,需不需要进行线程分析,需要哪些权限和定位工具,这些在监控部署时都需要考虑到。 + +下面我从监控硬件资源开始,通过使用 Linux 命令行对服务器进行监控,为什么我要讲解 Linux 命令的监控呢?我认为它具有灵活迅速的特点,通过命令可以最快地输出对应结果。接下来我会分别从 CPU、内存、磁盘、网络维度既快又能直击要害地帮你分析硬件瓶颈。 + +CPU + +top 是我们查看各个进程的资源占用状况最常用的命令,如下代码所示,这个命令简单却包含很大的信息量,接下来我选一些常用的内容给你重点解释。 + +top - 18:17:47 up 158 days, 9:32, 2 users, + +load average: 0.07, 0.15, 0.21 + +Tasks: 154 total, 1 running, 152 sleeping, 0 stopped, 1 zombie + +%Cpu(s): 3.9 us, 1.3 sy, 0.0 ni, 94.6 id, 0.2 wa, 0.0 hi, 0.0 si, 0.0 st + +KiB Mem : 8010676 total, 337308 free, 6036100 used, 1637268 buff/cache + +KiB Swap: 0 total, 0 free, 0 used. 1223072 avail Mem + +以下省略 + + +1. load average + +关于这一内容的代码如下所示: + +load average: 0.07, 0.15, 0.21 + + +三个数字都是代表进程队列的长度,从左到右分别表示一分钟、 五分钟和十五分钟的数据,数字越小压力值就越低,数字越大则压力越高,然而这个数值多小算小呢?多大算大呢? + +以单核处理器为例,打个比方就像收费站的一个 ETC 通道一样: + + +0 表示没有任何车辆需要通过; +从 0 到 1 可以认为很流畅,车辆不需要任何等待就可以通过; +1 表示正好在这个通道可接受范围之内; +超过 1 就已经有车辆在后面排队了。 + + +所以理想情况下,希望平均负载值在 1 以下。如果是 1 就代表目前没有可用资源了。在实际情况中,很多运维同学会把理想负载设置在 0.7 以下,这也是业内的一个“经验值”。 + +刚刚说的是一个单核处理器的情况,多核 CPU 的话,负载数值 / CPU 核数在 0.00~1.00 之间表示正常,理想值也是在 0.7 以内。 + +2. CPU 状态 + +从 top 中你也可以看到每种类型进程消耗的 CPU 时间百分比,如下所示: + + %Cpu(s): 3.9 us, 1.3 sy, 0.0 ni, 94.6 id, 0.2 wa, 0.0 hi, 0.0 si, 0.0 st + + +首先来看代码中的一些重要信息。 + + +us 列显示了用户进程所花费 CPU 时间的百分比。这个数值越高,说明用户进程消耗的 CPU 时间越多,可以用来分析代码中的 CPU 消耗热点。 +sy 列表示系统进程消耗的 CPU 时间百分比。 +ni 列表示改变优先级的进程占用 CPU 的百分比。 +id 列表示 CPU 处于空闲状态的时间百分比。 +wa 列显示了 I/O 等待所占用的 CPU 时间的百分比,这里 wa 的参考值为 0.5,如果长期高于这个参考值,需要注意是否存在磁盘瓶颈。 +hi 列表示硬件中断占用 CPU 时间百分比。 +si 列表示软件中断占用 CPU 时间百分比。 +st 列表示当系统运行在虚拟机中时,当前虚拟机在等待 CPU 为它服务的时间。 + + +在已经输入 top 的情况下再输入数字 1,可以查看 CPU 的核数和每个核的运行状态。 + +如下图是两核 CPU 的运行状态。 + +%Cpu0 : 3.0 us, 1.7 sy, 0.0 ni, 95.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st + +%Cpu1 : 2.4 us, 1.0 sy, 0.0 ni, 96.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st + + +值得注意的是,很多同学看 CPU 的使用率时,只看 us 这个数值,通过上面的讲解,可以看出这是不准确的。除了用户进程,还有其他系统进程会占用 CPU,所以实际 CPU 的使用率可以用 100 减去空闲值(id)去计算。 + +如何统计内存使用情况 + +最常见的是通过 free 来查看 Linux 内存使用情况。 + +[root@JD ~]# free -m + + total used free shared buff/cache available + +Mem: 7822 5917 302 373 1602 1195 + +Swap: 0 0 0 + + +相信通过单词的意思我们也能大概看出来 total、used、free 表示什么,它们分别是总的物理内存大小、已经被使用的物理内存和空闲的物理内存值是多少。 + +曾经有同学问我,为什么 free 值很低却未必代表内存达到瓶颈呢? + +这和 Linux 内核机制有关系,简单来说,内存空间会开辟 buffer 和 cache 缓冲区,对于物理内存来说,这都属于被使用过的内存。而应用需要内存时,如果没有可用的 free 内存,内核就会从缓冲区回收内存以满足要求,当 free 值很低的时候,如上代码中的 available 就能体现出缓冲区可用内存的大小,这个指标可以比较真实地反映出内存是否达到使用上限。 + +磁盘查看 + +这一部分我们来讲两个重要的命令。 + +1.iostat + + [root@JD ~]# iostat -x + +Linux 3.10.0-514.el7.x86_64 (JD) 01/18/2021 _x86_64_ (2 CPU) + +avg-cpu: %user %nice %system %iowait %steal %idle + + 5.24 0.00 1.57 0.07 0.00 93.12 + +Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util + +vda 0.00 0.29 0.57 5.30 20.50 630.14 221.82 0.07 11.53 59.83 6.36 1.18 0.69 + + +通过这个命令你能看到磁盘实时运行的情况,一般可以优先看 idle、util 和 svctm 这几列的数值: + + +idle 代表磁盘空闲百分比; +util 接近 100%,表示磁盘产生的 I/O 请求太多,I/O 系统已经满负荷在工作,该磁盘可能存在瓶颈; +svctm 代表平均每次设备 I/O 操作的服务时间 (毫秒)。 + + +在我的经验中,会组合看这些指标,如果 idle 长期在 50% 以下,util 值在 50% 以上以及 svctm 高于 10ms,说明磁盘可能存在一定的问题。接着我会定位到具体是哪个进程造成的磁盘瓶颈,下面我就为你介绍一个关于定位的命令。 + +2.iotop + +iotop 这个命令并不是 linux 原生的,需要安装,以 CentOS 7.0 为例: + +[root@JD ~]# yum -y install iotop + + +安装完成之后,直接输入 iotop,示意如下,你就能清楚地看到哪些进程在消耗磁盘资源。 + +6448 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % ifrit-agent + +14647 be/4 root 0.00 B/s 7.70 K/s 0.00 % 0.00 % java -Dserver.port=9080 + + +网络 + +netstat + +netstat 能提供 TCP 和 UDP 的连接状态等统计信息,可以简单判断网络是否存在堵塞。 + +[root@JD ~]# netstat + +Active Internet connections (w/o servers) + +Proto Recv-Q Send-Q Local Address Foreign Address State + +tcp 0 1 JD:49190 169.254.169.250:http FIN_WAIT1 + +tcp 0 0 JD:39444 169.254.169.254:http TIME_WAIT + +tcp 0 0 JD:us-srv worker-18.:sentinel-ent ESTABLISHED + + +Proto:协议名(可以 TCP 协议或者 UDP 协议)。 + +recv-Q:网络接收队列还有多少请求在排队。 + +send-Q:网络发送队列有多少请求在排队。 + +recv-Q 和 send-Q 如果长期不为 0,很可能存在网络拥堵,这个是判断网络瓶颈的重要依据。 + +Foreign Address:与本机端口通信的外部 socket。 + +State:TCP 的连接状态。 + +总结 + +通过本讲的学习,你已经知道了如何通过命令行监控 Linux 资源,包括 CPU、磁盘、内存、网络,也知道了判断硬件瓶颈的一些策略。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/11分布式服务链路监控以及报警方案.md b/专栏/说透性能测试/11分布式服务链路监控以及报警方案.md new file mode 100644 index 0000000..4bbf48e --- /dev/null +++ b/专栏/说透性能测试/11分布式服务链路监控以及报警方案.md @@ -0,0 +1,232 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 11 分布式服务链路监控以及报警方案 + 上一讲我们主要讲解了硬件的命令行资源监控,相信你已经学会了通过命令行的方式查看硬件瓶颈。 + +那我提一个问题,为什么会有硬件瓶颈呢?或者我说得更直白一点,如果服务器上没有应用还会造成硬件瓶颈吗?显然是不会的,所以我想向你传递一个观点:呈现出来的硬件瓶颈绝大多数是表象问题,我们往往需要在系统应用上寻找问题的根因。而寻找系统问题的根因,对于系统链路监控也是必不可少的,所以这一讲我将带你学习如何进行基于系统链路的监控。 + +为什么要链路监控? + +随着微服务的流行,链路监控越来越受重视。微服务架构是根据业务进行拆分,对外统一暴露API 接口,而内部可能是分布式服务、分布式对象存储等,如图 1 所示。 + + + +图 1:微服务架构 + +这些组件共同构成了复杂的分布式网络。而分布式系统一旦出现问题,比如一个请求经过多个微服务之后出现了调用失败的问题,或者一个请求经过多个微服务之后 Response 时间过长,但具体是哪个微服务节点的问题我们并不知道。只能去服务器上查看调用经过的每个微服务的日志,当然这种方式的效率是比较低的,相当于人肉运维。 + +随着业务体系越来越复杂,加上服务间的相互依赖关系,微服务其中一个节点出现了问题,很可能牵一发而动全身,导致严重的后果。在这样的情况下,分布式链路监控的价值就体现出来了,它可以让你清晰地知道跨服务调用的链路耗时信息、执行方法等,并从整体到局部将信息呈现出来,可以帮助你节约故障排查时间。 + +全链路监控选择依据 + +全链路监控系统有很多,可以从这几方面选择: + + +探针的性能消耗,探针是搜集信息的“情报员”,尤其是在多节点情况下,搜集数据的成本会越来越高,监控组件服务的影响应该做到足够小、数据分析快、性能占用小; +对代码的非侵入性,减少开发的维护成本; +监控、分析的维度尽可能多。 + + +目前市面上的全链路监控工具很多,比如 CAT、SkyWalking、Pinpoint 等,对于工具的选型来说最重要的是采样数据对系统的性能消耗足够小、数据分析和展示快、监控的维度尽可能丰富,简单比较下这几个工具。 + + +CAT:是由美团和携程的同学开发的,通过代码埋点的侵入式方式,对应用日志分析、监控、展示等,不过侵入式的方式会带来开发以及维护成本的增加。 +SkyWalking:也是由国人开发,目前项目已经提交到 Apache 孵化组织,无侵入性、UI 展示简洁清晰。 +Pinpoint:由韩国人开发,相对于 SkyWalkingg 提供了更为详尽的链路监控信息,不过数据采集带来的性能损耗相对于 SkyWalking 来说比较大。 + + +综上我将以 SkyWalking 为例给你介绍下链路监控,希望通过介绍,你可以掌握 SkyWalking 的具体使用步骤和链路监控工具可以给我们带来什么好处,通过本讲的学习你也可以自由选择链路监控工具去实践。 + +SkyWalking 的模块分析 + +首先来看下 SkyWalking 的组件示意图: + + + +图 2:SkyWalking 的组件示意图 + + +Tracing 和 Metric : 在应用上采集 Tracing(调用链数据)和 Metric(指标)信息通过 HTTP 或者 gRPC 方式发送数据到 Analysis Platform。 +Analysis Platform:数据的采集和计算,将传输的 Tracing 和 Metric 数据进行整合分析,通过 Analysis Core 模块把数据写入相关的数据库中。 +Storage:SkyWalking 的存储,支持以 ElasticSearch、MySQL、TiDB 等数据库进行数据存储,其中 ElasticSearch、MySQL 用的居多。 +SkyWalking UI:Web 可视化平台,用来展示落地的数据以及图表,比如链路调用、服务结构等。 + + +1.安装部署过程以及相关的注意事项 + +首先下载 SkyWalking 安装包并进行解压: + +wget https://github.com/apache/SkyWalking/archive/v8.0.1.tar.gz + +tar -zxvf v8.0.1.tar.gz + + +解压后可以看到如下文件夹: + + + +我们讲解下这个主要文件的作用。 + +(1)修改配置文件 config/application.yml。在这里先进行数据库的配置,我使用当前服务器上的 mysql 来进行存储: + + mysql: + + properties: + + jdbcUrl: ${SW_JDBC_URL:"jdbc:mysql://127.0.0.1:3306/swtest"} + + dataSource.user: ${SW_DATA_SOURCE_USER:root} + + dataSource.password: ${SW_DATA_SOURCE_PASSWORD:123456} + + +将上述的配置文件根据自己的数据库实际地址修改,修改完成后进行启动: + +$ bin/oapService.sh + +SkyWalking OAP started successfully! + + +(2)接着来看 SkyWalking UI 的相关配置,由于 SkyWalking UI 的默认端口是 8080,这个端口是很多应用的默认端口,容易产生冲突,你可以修改一下,如下所示: + +# 修改webapp/webapp.yml + +server: + + port: 18080 + + +然后启动 SkyWalking UI 服务,启动完成后你会看到如下信息: + +$ bin/webappService.sh + +SkyWalking Web Application started successfully! + + +这里我强烈建议,不管是第一步还是第二步中的 started successfully,都并不意味着真正的启动成功,一般在提示 started successfully 后,还需要去 logs 文件夹下查看相关日志来判断启动过程中是否存在异常。 + +UI 界面启动成功后示意图如下: + + + +(3)本地启动微服务。我 demo 里包含 system、auth、user 等服务,通过配置 SkyWalking Agent 的方式启动服务,示意如下: + +nohup java -server -Xms256m -Xmx256m -Dspring.profiles.active=dev -Dspring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 -javaagent:/root/apm/apache-SkyWalking-apm-bin/agent/SkyWalking-agent.jar=agent.service_name=cctuser -Dspring.cloud.nacos.config.server-addr=127.0.0.1:8848 -jar blade-user.jar > log.file 2>&1 & + + +-javaagent 后的启动参数是 SkyWalking 的 agent 配置路径。 + +启动本地的微服务成功后,就可以访问服务,同时通过 SkyWalking 监控你可以看到服务部署图以及链路监控等,如下图所示: + + + +图 3:服务部署图 + + + +图 4:链路追踪图 + +在我们进行链路追踪后,可能会出现一些超时、访问错误等异常,那我们如何能够更快地收到这些异常信息呢? + +2.常见的报警方式 + +首先很多人想到了报警机制,那我带你了解下常见的几种报警方式。 + +(1)短信或者电话报警 + +这样的报警方式更适合高级别的报警提醒,用于处理紧急情况。出现级别不高而又频繁地发送短信会让人产生排斥感,而且电话或者短信的报警方式也存在一定的成本。 + +(2)邮件报警 + +邮件报警更适用于工作时的提醒,但是系统往往是不能区分你是不是在工作,有时候夜间的报警邮件你很难及时关注到,所以说邮件报警也存在一定的局限性。 + +(3)钉钉报警 + +随着钉钉越来越普及,很多公司都已经使用钉钉。员工在公司需要使用钉钉管理自己的考勤以及进行工作上的沟通,如果将监控报警信息推送到钉钉上其实就很方便的。不过也存在有的企业用的是其他沟通工具,不过对于报警推送到沟通软件上的原理都是类似的,接下来我会以钉钉作为模版来讲解如何进行报警信息的推送。 + +3.如何配置钉钉机器人? + +(1)打开机器人管理页面。以 PC 端为例,打开 PC 端钉钉,进入首页面点击头像,在弹出框里选择机器人管理,打开管理页面后可以选择自定义,如下图所示: + + + +(2)在打开的机器人详情页面点击添加按钮,如下图所示: + + + +(3)在打开的添加机器人页面输入机器人名字,选择要接收报警的钉钉群 ,设置机器人头像。根据需要勾选安全设置等就可以,点击完成之后,在页面拷贝出 Webhook 地址保存好,向这个地址发送 HTTP POST 请求,设置的 SkyWalking 钉钉报警群便能收到钉钉报警消息,如下图所示: + + + +配置好之后我们可以看到设置报警的钉钉群“SkyWalking 钉钉报警”出现了报警机器人消息,如下图所示: + + + +我们可以用 Linux 命令行工具 curl 快速验证是否可以推送成功,curl 命令行示意如下: + +[root@JD ~]# curl 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxxx' -H 'CONTENT-TyPE: application/json' -d '{"msgtype": "text","text": {"content": "业务报警"}}' + +{"errcode":0,"errmsg":"ok"} + + +你可以看到通过 curl 后可以得到基本响应 {“errcode”:0,“errmsg”:“ok”}。 + +4.如何将 SkyWalking 和钉钉报警完美结合? + +上述已经配置完成了钉钉机器人,那如何将 SkyWalking 的报警信息自动推送到钉钉机器人呢?我们可以实现一个接口作为它们沟通的“桥梁”。 + +首先在 pom 里面引入相关的 jar 包,如下所示: + + + + com.aliyun + + alibaba-dingtalk-service-sdk + + + + +然后自定义 DingTalkUtils 工具类,暴露接口访问路径 /dingdingAlarm。 + +@RequestMapping(value = "/dingdingAlarm", method = RequestMethod.POST) + +public void alarm(@RequestBody List alarmList){ + + //示意代码 + + alarmList.forEach(alarm-> { + + DingTalkUtils.sendMsg(alarm.getAlarmMessage()); + + }); + +} + + +SkyWalking 告警推送到钉钉 + +SkyWalking 提供了告警的配置,我们可以很方便地配置上面暴露的接口。在 SkyWalking 程序包里的 config 文件夹下有个 alarm-settings.yml 文件,该文件专门用来配置相关的报警。在该配置文件中我们可以搜索到 webhooks,把上面暴露的接口写上去就好了。 + + webhooks:-http://ip:port/dingdingAlarm + + +接下来我们测试下,比如 auth 服务获取验证码的接口出现错误,我们是可以在 SkyWalking 追踪页面清楚地看到的。同时对于其他相关的业务同学,也都可以在钉钉群收到报警信息,这样的方式在实际工作中非常实用。业务报错图和钉钉报警图如下所示: + + + +图 5:业务报错图 + + + +图 6:钉钉报警图 + +总结 + +这一讲主要讲解了关于 SkyWalking 的使用背景以及价值,在实操层面讲解了 SkyWalking 是如何追踪监控中出现的错误,并且把出现的错误通过钉钉通知给相关人员,相信通过这一讲的学习,你也对微服务下的报警方案会有一个更深刻的认识。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/12如何把可视化监控也做得酷炫?.md b/专栏/说透性能测试/12如何把可视化监控也做得酷炫?.md new file mode 100644 index 0000000..33bed86 --- /dev/null +++ b/专栏/说透性能测试/12如何把可视化监控也做得酷炫?.md @@ -0,0 +1,237 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 12 如何把可视化监控也做得酷炫? + 前面两讲分别讲解了硬件监控、链路监控以及相关的报警机制。一些同学在学习硬件监控的过程中可以发现,命令行操作反馈迅速及时,指哪儿打哪儿,非常灵活便捷,但并不是所有同学都需要或者都有权限直接在服务器上进行操作。而且很多中大型互联网公司是大型的服务集群,通过命令行去发现每台服务器的问题并不现实,所以很多企业都会采用大屏的图形化监控。在页面上根据自己的需要进行条件筛选,这样不仅简单、清晰、直观,而且能够很方便地向团队成员传递监控的实时信息。 + +所以作为一位优秀的性能测试工程师,你不仅仅需要掌握命令行监控是如何操作的,也需要了解监控大屏是如何制作的,二者可以说是相互补充的。 + +这一讲我们就来讲解可视化监控,你可以认为它是一节实操课,需要提前准备好你的服务器环境(推荐 CentOS 7.0),跟着我的步骤一步步进行就可以完成酷炫的监控报表。 + +命令行和图形化界面展示对比 + +对于初学者而言,你可能并不能从上述文字中感受到命令行和图形化界面展示的区别,那么我用两张图来对比下。 + + + +图 1:命令行方式 + + + +图 2:可视化监控大屏 + +我想绝大部分还是更愿意看图 2 的可视化监控大屏,本讲的开头也说了命令行监控和可视化监控是一种互补的形式,这就代表两种方式各有千秋。可视化监控除了直观外,我认为还有如下两点优势。 + +(1)信息高度集中 + +可视化监控大屏一般会根据不同的机器提供不同的维度,比如图 2 就是其中一台机器的各类监控信息汇总,可以说信息多元且海量,我们并不能在同一时间将所有机器的具体信息都看到。而可视化方式可以通过时间维度去追溯历史数据,这相对于命令行基于碎片时间收集的信息要全面很多,很方便后续的复盘或者追踪。 + +(2)加速信息传递效率 + +大屏的方式也是共享的方式,可以更快速地把信息传递给项目其他成员,每位成员可以通过大屏的链接去访问,自由选择自己所需要的信息,而且可以通过展现出来的趋势预判会触发的阈值以达到提前发现风险的效果。 + +Promethues + Exporter + Grafana + +大屏的监控并不算新概念,可以说各类方案层出不穷,老牌的监控工具如 Zabbix、Nagios 等,但随着互联网的发展,越来越多高性能且展示酷炫的方案应运而生,其中以 Promethues + Exporter + Grafana 为主的通用方案受到普遍欢迎。 + +首先来解释下 Promethues + Exporter + Grafana 这套组件的基本作用。 + + +Prometheus:既然 Exporter 作为 agent,那必然有一套中心化的数据采集存储组件,这个组件就是 Promethues,它通过接收 Exporter 采集的数据,并按照一定的规则进行计算整合,通过 AlertManager 设置报警规则,达到触发条件后就发送报警信息。 +Exporter:用于采集服务器中的监控数据,比如对服务器基础性能进行监控的 node_exporter 插件,也可以理解为 agent。 +Grafana:用于数据的渲染展现,可以展示得非常酷炫,如果仔细阅读过《03 | 构建并执行 JMeter 脚本的正确姿势》,相信你对 Grafana 已经有了一定的了解。 + + +我们再用下面这张图来总结下这个过程。 + + + +图 3:组件流程图 + +那可能有同学提问了,这套组件除了针对硬件进行监控,对于一些中间件或者数据库的监控也可以吗? + +答案是肯定的,根据 Exporter 的不同,你可以监控不同的组件,这也是这套监控最灵活的部分,不同的 Exprter 可以达到不同的监控目的和效果。 + +接下来我就分别以监控服务器硬件和数据库这两个例子来讲述这套体系的搭建以及使用技巧。 + +如何打造硬件资源报表? + +第一步安装 node_exporter + +通过对上文的学习,你应该知道关键部分是如何选择 Exporter,其中 node_exporter 就实现了对 Linux 操作系统中 CPU 使用、负载、磁盘空间、磁盘等待、网络传输等详尽数据的采集。 + +接着我就带你来看如何安装部署,比如你需要在 A、B、C 三台机器上同时监控,那必须都安装node_exporter 插件。我先以一台机器为例,带你安装下 node_exporter,使用 wget 直接下载就可以,如下所示: + +wget -c https://github.com/prometheus/node_exporter/releases/download/v0.18.1/node_exporter-0.18.1.linux-amd64.tar.gz + + +然后解压如下命令: + +tar zxvf node_exporter-0.18.1.linux-amd64.tar.gz + + +再进入相应的文件夹,使用后台启动方式开启服务: + +nohup ./node_exporter & + + +当启动完成之后,可以用 ip:9100 的方式打开页面,如下所示,即认为 node_exporter 安装成功了。 + + + +图 4:node_exporter 安装示意图 + +我们点击 Metrics 可以查看具体的采集信息,部分展示内容如下所示: + +# HELP node_cpu_seconds_total Seconds the cpus spent in each mode + +# TYPE node_cpu_seconds_total counter + +node_cpu_seconds_total{cpu="0",mode="idle"} 995721.03 + + + +HELP是解释下面指标的含义,相当于协助文档; +TYPE用于解释指标的数据类型; +下面的信息是具体的统计信息,比如 node_cpu_seconds_total{cpu=“0”,mode=“idle”} 就是指从开机到现在的 cpu0 的空闲时间。 + + +你可以自行安装下 node_exporter,就能看到 Metrics 中的海量数据了。 + +第二步安装 Prometheus + +Prometheus 作为时间序列数据库,提供本地存储和分布式存储,又支持多种数据大盘,而且性能优异,受到市场的欢迎。阿里云也全面接入了 Promethues 的生态,提供了更多开箱即用的组件。 + +首先我们使用如下命令进行下载: + +wget -c https://github.com/prometheus/prometheus/releases/download/v2.15.1/prometheus-2.15.1.linux-amd64.tar.gz + +tar zxvf prometheus-2.15.1.linux-amd64.tar.gz + + +然后进入解压文件夹 prometheus-2.15.1.linux-amd64,查看主要的配置文件 prometheus.yml。 + +该文件主要有四个核心节点,分别是 global、alerting、rule_files 和 scrape_configs。 + + +global:全局配置,比如每次数据收集的间隔、规则地扫描数据的间隔。 +alerting:设置告警的插件,在这里会设定 alertmanager 这个插件。 +rule_files:具体的报警规则设置,比如基于什么指标进行报警,类似于触发器。 +scrape_configs:采集数据的对象,job_name、target 以及 job_name 是配置主机的名称,target 是你安装的 Exporter 地址。 + + +然后我们需要增加本地的监控配置,如下所示: + +- job_name: 'cctester' + + static_configs: + + - targets: ['127.0.0.1:9100'] + + +再启动 Prometheus: + +nohup ./prometheus & + + +访问 http://ip:9090/targets,根据自己的实际情况填写 ip,出现如下截图表示安装成功。 + + + +图 5:Promethues 成功安装示意图 + +第三步是安装 Grafana + +这部分第 03 讲已经讲解过,我们就不再赘述,安装完成 Grafana 之后,添加 Prometheus 数据源,测试并保存即可。 + + + +图 6:Grafana 添加 Promethues 数据源 + +接着导入官方提供的展示模板就可以,点击链接。你可以自行选择相应的版本进行下载,也可以直接填写模板 ID,导入完成之后,便可以看到大屏了,示意图如下: + + + +图 7:可视化大屏示意 + +到目前为止,一款基于 Linux 硬件监控的大屏就打造完成了。 + +如何可视化监控数据库? + +通过以上的讲解,你可以思考下如果做 MySQL 的监控,哪些组件是可以通用的,需要改变的组件又有哪些。 + +能够思考清楚这些问题,我想你就可以基于这套组件打造出属于你自己的监控系统了。 + +对于监控来说,关键是面对不同的监控对象是怎么采集和怎么展示的,所以需要改变的是你的采集的 Export 和展示的模板,而Promethues + Exporter + Grafana这套组件的社区又非常丰富,所以我们可以快速实现这个需求。 + +下面我们下载基于 MySQL 监控的 Exporter,如下所示: + +wget https://github.com/prometheus/mysqld_exporter/releases/download/v0.12.1/mysqld_exporter-0.12.1.linux-amd64.tar.gz + + +下载完成之后对如下命令进行解压: + +tar zxvf mysqld_exporter-0.12.1.linux-amd64.tar.gz + + +对于 MySQL 的监控,还需要创建一个配置文件,比如我在解压后的文件夹下创建 my.cnf,来看看 my.cnf 有哪些内容: + +[client] + +user=root + +password=123456 + +port=3306 + +host=127.0.0.1 + + +可以看出 my.cnf 里的配置信息就是数据库的连接信息,你可以根据自己的实际部署情况进行配置,配置完成之后就可以启动了,启动命令如下: + +nohup ./mysqld_exporter --config.my-cnf=my.cnf & + + +然后通过网页访问来验证是否部署成功,访问地址一般是 ip:9104,可以看到如下展示信息: + + + +点击 Meteric 你也可以发现很多手机端 MySQL 监控信息的参数选项,部分信息如下: + +# HELP mysql_global_variables_max_connections Generic gauge metric from SHOW GLOBAL VARIABLES. + +# TYPE mysql_global_variables_max_connections gauge + +mysql_global_variables_max_connections 151 + + +这个配置表示了最大连接数的配置信息,如果能看到这一步信息也说明 mysqld_exporter 安装成功了,接着增加 promethues.yml 里的 MySQL 配置节点,示意如下: + + - job_name: 'mysql' + + static_configs: + + - targets: ['127.0.0.1:9104'] + + +关于Grafana 展示, 选择 Grafana 的 MySQL 监控相关模板导入即可,点击模板链接。下载并导入后就可以了,MySQL 展示效果如下图所示。 + + + +图 8:mysql 可视化监控示意图 + +总结 + +这一讲主要讲述了Promethues + Exporter + Grafana的监控方案: + + +首先需要掌握的是每种组件核心的意义以及使用方法,而不能满足于机械地执行完成上述步骤; +然后我是以监控硬件服务器资源和 MySQL 监控来举例,分别代表了硬件层和服务层两个维度,通过这两个例子让你更直观地明白哪些组件是可以复用的,不同的监控目标是否有配套的社区可以给你提供帮助; +再者我更想传递的信息是这套监控体系不仅仅是适用于我举的示例,它更是一揽子解决方案,比如说监控 Redis、JVM 等,它同样也是适用的。通过这套方法完全可以解决可视化监控层面的大部分需求,希望你能够多多实践,扫除你们公司可能存在的监控“死角”。 + + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/13Docker的制作、运行以及监控.md b/专栏/说透性能测试/13Docker的制作、运行以及监控.md new file mode 100644 index 0000000..d5c1e45 --- /dev/null +++ b/专栏/说透性能测试/13Docker的制作、运行以及监控.md @@ -0,0 +1,274 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 13 Docker 的制作、运行以及监控 + 模块三主要讲解了不同层级的监控以及监控的方式,作为模块三的最后一讲,我将带你来学习 Docker 的制作、运行以及监控。对于很多测试来说,经常听到 Docker 容器,但自己好像又不是很熟悉,只是用相关命令去查询日志等,而对于为什么要使用 Docker 还不是特别清楚。其实 Docker 并不难学,有时候你只是差一个学习的切入点,这一讲我会从测试的使用层面带你学习下 Docker 的要点知识,希望作为一名测试的你,对 Docker 也不会再陌生。 + +为什么要使用 Docker? + +你可以回忆下 Docker 的图标(如图 1 所示),是不是像一条船上装了很多集装箱,其实这和Docker 的设计思想有关系,集装箱能解决什么问题呢?就是货物的隔离,如果我们把食物和化学品分别放在两个集装箱中用一艘轮船运走则无妨,但是你不可以把它们放在同一个集装箱中,其实对于 Docker 设计也是如此。 + +操作系统就相当于这艘轮船,上面可以有很多集装箱,即 Docker,你可以把 Docker 看作是独立的子环境,有独立的系统和应用,比如经常因为一些历史原因开发的多个模块依赖于不同的 JDK 版本,将这两个模块部署在一台 Linux 服务器上可能很容易出问题,但是如果以 Docker 的方式便很容易解决版本冲突的问题。 + + + +图 1:Docker 图标 + +Docker 的用法(基于 CentOS 7.0) + +如何学习 Docker 呢?从应用技术维度来看它是一个容器,从学习角度来看它就是一种工具。 + +对于工具的学习我认为从实际的例子切入是最有代入感的,接下来我就在 CentOS 环境下安装一个基于 Ubuntu 的 Docker 环境,带你从使用层面了解下 Docker,知道 Docker 最基本的安装方式,如下所示: + +yum install -y docker //安装Docker + +service docker status //查看Docker运行状态 + + +接下来运行一个 Docker 容器,我目前用的是 CentOS 系统,可现在还需要一个 Ubuntu 环境,我就需要通过如下命令基于 Ubuntu 镜像启动一个容器: + +docker run -i -t ubuntu /bin/bash + + +通过这个命令,就直接创建了基于 Ubuntu 的 Docker 环境,并直接进入了交互 shell,这样你就可以认为是在 Ubuntu 系统下工作了,通过如下命令可以查看版本号: + +root@ac3874a96890:/# cat /etc/issue + +Ubuntu 20.04.1 LTS + + +同样的道理,如果你的 Java 服务有的依赖 JDK1.7,有的依赖 JDK1.8,则可以通过 Docker 来做不一样的服务。 + +上面就是一个简单的实例,在 CentOS 系统里创建一个基于 Docker 的 Ubuntu 系统以实现你特定的需求。 + +我们再来看看 Docker 常用的命令有哪些,这些可能是你和 Docker 打交道的过程中最常见的命令。 + +对于 Docker 的命令,都是在 Linux 终端直接输出就可以,比如查看 Docker 镜像,就是直接输出 docker images,展示信息如下所示: + +[root@JD ~]# docker images + +REPOSITORY TAG IMAGE ID CREATED SIZE + +docker.io/ubuntu latest f643c72bc252 + +3 weeks ago 72.9 MB + +docker.io/gitlab/gitlab-ce latest 6e2336419031 + +8 months ago 1.92 GB + + + +REPOSITORY 是指仓库名字; +TAG 一般指版本号; +IMAGE ID 是指镜像 ID; +CREATED 指镜像创建时间; +SIZE 指镜像大小; + + +如果我们要查看正在运行的 Docker 进程,可以使用命令 docker ps,如下所示: + +[root@JD ~]# docker ps + +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + +3e6ab93074c7 rancher/scheduler:v0.8.6 "/.r/r /rancher-en..." 25 hours ago Up About an hour r-scheduler-scheduler-1-056ab74a 2a6a718fa85d gitlab/gitlab-ce:latest "/assets/wrapper" 8 months ago Restarting (127) 11 hours ago gitlab + + +其中第一列是容器的 ID 号,它是一个重要的标识,通过 ID 号我们可以查看指定容器的日志以及启停容器等。读到这里你会发现,你已经知道了两个 ID: + + +一个是 IMAGE ID; +另外一个是 CONTAINER ID。 + + +当你要删除镜像时,就需要使用到 IMAGE ID 了,也就是使用命令 docker rmi image id。那 IMAGE 和 CONTAINER 是什么关系呢?按照我的理解打个比方: + + +IMAGE 相当于类; +CONTAINER 相当于实例化后的对象,是在使用层面表现出来的形态。 + + +不过你要注意的是 docker ps 只会展示运行的容器: + + +如果你想展示所有的容器,需要使用 docker ps -a,这个命令会展示运行的容器和已经停止的容器; +如果你机器上运行的容器很多,想看最近创建的 10 个容器,可以使用 docker ps -n 10。 +如果你要停止运行某个容器,可以使用 docker stop container id 来终止,并且可以结合上文说的 docker ps -a 来看终止状态的容器; +如果要使用 docker rmi删除容器镜像,你也需要先关闭对应运行的容器才能执行删除。 + + +值得注意的是一些初学者会误用 systemctl stop docker 这个命令,它是停止整个 Docker 服务,相当于你机器上的 Docker 全部关闭,这是初学者一定要注意到的。 + +作为测试或者开发,通过日志去排查问题是必不可少的,如下所示就是查看指定 Docker 容器日志的方法: + +docker logs -f 3e6asb93074c7 #最后一列为容器id号 + + +你可以将 Docker 看作是一个子系统,自然可以进入这个系统进行一定的操作。在我的使用过程中,经常会使用如下命令进入 Docker 容器找应用的 dump 信息: + +docker exec -it 3e6ab93074c7 /bin/bash + + +以上是测试同学在使用层面最常见的命令,如果你对 Docker 还不是很了解,可以将这些作为切入点,先掌握使用,在此基础上再去了解 Docker 的架构设计以及一些进阶思想。 + +Dockerfile、Docker 镜像、Docker 容器的区别是什么? + +上文带你熟悉了 Docker 的用法,相当于小试牛刀,可能你总听公司的人说 Dockerfile、Docker 容器、Docker 镜像,但又分不清楚,下面我就来解释下它们之间的具体区别是什么: + + +Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明,相当于你做镜像的材料清单和执行步骤; +Docker 镜像是根据这些原材料做出来的成品; +而 Docker 容器,你可以认为是基于镜像运行的软件。 + + +我以包饺子为例: + + +Dockerfile 相当于猪肉、葱姜蒜、饺子皮这些原料的描述以及包饺子的步骤; +Docker 镜像是你包完的生水饺; +而 Docker 容器则是已经煮熟可以食用的水饺了。 + + +通过下面这个示意图可以看出从 Dockfile 到 Docker 容器的过程: + + + +图 2:Dockfile 到 Docker 容器的过程 + +应用实例:如何制作基于 JMeter 的 Docker 镜像? + +首先来说为什么会有这样的需求,对于用户体量比较大的公司,他们需要的系统处理能力自然也越高。在压测过程中,并不是单台压力机就可以解决问题,我们可能会在压测过程中动态调度JMeter 节点,其中一个比较方便的方式就是使用 Docker 的方式动态进行。 + +接下来我主要讲解如何制作基于 JMeter 的 Docker 镜像,这也是基于 Docker 扩容的关键部分。 + +首先我新建了一个文件夹 jmeter_docker,里面存放制作 JMeter 的 Docker 的原材料,如下所示: + +[root@JD jmeter_docker]# ls + +apache-jmeter-5.2.1.tgz Dockerfile jdk-8u101-linux-x64.tar.gz + + +接着我打开 Dockerfile,看看我的“原料表”里面有哪些内容,从下面的文件描述中可以看出我需要的“原料”和执行步骤: + +FROM java:8 + +# 基础java版本 + +MAINTAINER cctester + +# 作者 + +ENV http_proxy "" + +ENV https_proxy "" + +RUN mkdir /test && \ + + chmod -R 777 /test + +# 创建/test目录,用于存放jmx脚本、jtl结果文件、html测试报告文件 + +ENV JMETER_VERSION=5.2.1 + +ENV JMETER_HOME=/usr/local/apache-jmeter-${JMETER_VERSION} + +ENV JMETER_PATH=${JMETER_HOME}/bin:${PATH} + +ENV PATH=${JMETER_HOME}/bin:${PATH} + +# 设置JMeter环境变量 + +ADD apache-jmeter-${JMETER_VERSION}.tgz /usr/local + +# 添加JMeter + +RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + + echo "Asia/Shanghai" > /etc/timezone + + +在制作 JMeter 镜像时,请不要忽略后面的一个点(.),具体如下所示: + + [root@JD jmeter_docker]# docker build -t jmeter . + +.....省略 + + Successfully built 267c5b4303a6 + +# 你还可以通过docker images查看完成的镜像 + +[root@JD jmeter_docker]# docker images + +REPOSITORY TAG IMAGE ID CREATED SIZE + +jmeter latest 267c5b4303a6 6 minutes ago 762 MB + + +为了方便替换压测脚本或者参数化文件,我在 jmeter_docker 文件下创建一个 test 文件夹来存放这些文件。 + +mkdir test + +# 在当前路径创建test目录,用户存放jmeter文件 + +docker run -d -it --name jmeter5.2.1 -v $PWD/test:/test jmeter + +31f465a1ae646c65e855084d46313754e74a2f377776d9692c0119d32949a130 //启动成功,生成运行id + + +然后进入容器,看下 JMeter 是否可用: + +root@31f465a1ae64:/test# jmeter -v + +Dec 19, 2020 6:11:34 PM java.util.prefs.FileSystemPreferences$1 run + +INFO: Created user preferences directory. + + +到这里我们就可以运行 JMeter 进行测试了,上传一个 cctester.jmx 脚本到 test 文件夹,使用方式以及结果反馈如下所示: + +root@31f465a1ae64:/test# jmeter -n -t /test/cctester.jmx + +Creating summariser + +Created the tree successfully using /test/cctester.jmx + +Starting standalone test @ Sat Dec 19 18:22:11 CST 2020 (1608373331470) + +Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445 + +summary + 3344 in 00:00:18 = 185.0/s Avg: 52 Min: 14 Max: 1312 Err: 0 (0.00%) Active: 10 St + + +到此就完成了一个基于 Docker 的 JMeter,上面演示了从制作到运行的全过程,同样对于其他Docker 的制作流程也是类似的,你可以基于一种先练习。 + +Docker 如何监控 + +通过前面章节的学习,我想对于监控你已经并不陌生,并且可以提炼出一套搭建监控体系的方法,对于 Docker 监控本质上也是换汤不换药,我主要进行思路上的一些讲解。 + +Docker 本身也是可以通过命令行来监控的,看下 docker stats 的输出,如下所示: + +CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS + +b667f6b988b4 0.07% 381.3 MiB / 7.64 GiB 4.87% 119 MB / 105 MB 275 MB / 0 B 61 + +f650d561b729 0.04% 233.1 MiB / 7.64 GiB 2.98% 94.9 MB / 118 MB 139 MB / 397 MB 49 + +c7575bf9a7d7 0.00% 4.711 MiB / 7.64 GiB 0.06% 0 B / 0 B 954 kB / 0 B 6 + +2a72f849baaa 0.10% 4.008 MiB / 7.64 GiB 0.05% 18.8 MB / 14.5 MB 68.5 MB / 3.04 MB 6 + +760e653d4324 0.00% 4.887 MiB / 7.64 GiB 0.06% 0 B / 0 B 92.5 MB / 4.1 kB 27 + + +你可以看到不同的实例都有对应包括 CPU、内存、磁盘、网络的监控,这样的数据比较详细直观。所以这一讲我给你留一个作业,自行搭建 Docker 的可视化监控,可以结合之前讲过的 Grafana、Promethues 等,欢迎在评论区留下你搭建过程中的心得体会以及问题。 + +总结 + +本讲作为第三模块的收尾,带你学习了 Docker 的基础知识,包括镜像制作、运行,以及监控的常见方式。通过对第三模块的系统学习,你也应该掌握常见的监控方法以及监控部署开展的思路。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/14如何从CPU飙升定位到热点方法?.md b/专栏/说透性能测试/14如何从CPU飙升定位到热点方法?.md new file mode 100644 index 0000000..774f66b --- /dev/null +++ b/专栏/说透性能测试/14如何从CPU飙升定位到热点方法?.md @@ -0,0 +1,195 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 14 如何从 CPU 飙升定位到热点方法? + 上一模块我带你学习了如何进行系统监控,相信你已经掌握了监控部署的常见手段,通过监控这双“眼睛”,会帮助你及时发现系统资源异常,那当你发现资源异常时候,是不是觉得已经找到问题了呢?事实上并非如此,绝大多数资源异常只是你看到的表象问题,就好比你发现一个地方着火了,你可以先灭火,但是着火的原因是必须找到的,并制定相关的措施,这样才能有效避免下一次的火情。 + +对于系统也是这样的,当你发现了资源异常,你需要继续寻找发生问题的根因,所以作为一名专业的性能测试工程师,你也应当具备顺着表象去找问题根因的能力。这一讲我就以最流行的 Java 语言为例,带你学习如何透过现象看本质。 + +对于排查问题,不要只满足于掌握一些排查工具或者命令,你应当对被测语言以及运行原理有所了解,这样得出来的结论才可能更全面。 + +这一讲我先带你理解 Java 运行过程中的核心概念。首先要明白 Java 代码在哪里运行,一些初学者说是在 idea 或者 eclipse 里面,因为它们是写代码的软件,不过细心的同学会发现,所有的 idea 或者 eclipse 要运行 Java 代码都需要配置 Java 环境,其实 idea 是我们开发的编辑器,而真正运行代码的是 JVM。 + +什么是 JVM 呢?JVM 是 Java Virtual Machine 的缩写,它是一个独立出来的运行环境,通过这样的环境去进行 Java 代码中各种逻辑运行。 + +读到这里可能同学有疑问了:“我现在接触了很多环境,比如 JVM 运行环境、Docker 运行环境,还有云服务器之类,它们到底是什么关系?”这对于不少人来说,确实是有一定疑惑的,我先用一张图来示意下: + + + +从图中你可以看到,一般在底层物理机上会部署多个云服务器,而云服务器上又可以部署多个基于 Docker 的 JVM 节点,这样的部署结构也是比较常用的,既能做到环境的隔离也能节约机器成本。 + +JVM 本身是一个较为庞大的知识体系,对于测试来说,不一定要理解 JVM 特别晦涩的概念,但至少需要了解 JVM 的结构以及运行的机制,你可以认为 JVM 是运行在 Win 或者 Linux 系统上专门运行 Java 的虚拟机,Java 虚拟机直接和操作系统交互。 + +Java 文件是如何被运行的 + +比如我们现在写了一个 HelloTester.java,这个 HelloTester.java 就类似一个文本文件,不过这个文件里面包含了符合 Java 语法规范的文本。比如我在 idea 里写一个简单的方法,如下代码所示: + + public class HelloTester { + + public void sayName(String name){ + + System.out.println("my name is "+name); + + } + + public static void main(String[] args){ + + HelloTester helloTester=new HelloTester(); + + helloTester.sayName("cctester"); + +} + + +那我们的JVM 是不认识文本文件的,所以它需要编译,让其成为一个会读二进制文件的 HelloTester.class,一般这个文件会产生在工程文件夹下的 Target 当中。 + +如果 JVM 想要执行这个 .class 文件,我们需要将其装进一个类加载器中,它就像一个搬运工一样,会把所有的 .class 文件全部搬进 JVM 里面来。如下图所示: + + + +对于如上的过程我们再总结概括一下: + + +Java 文件经过编译后变成 .class 字节码文件; +字节码文件通过类加载器被搬运到 JVM 中,生成的对象一般会在 JVM 中堆空间运行。 + + +Java 对象又是如何在堆空间运行的? + +同样还是根据以上代码示意,我带你看下 Java 对象如何进入堆空间以及在堆空间中运行的。 + +通过上文可知,编译 HelloTester.java 便会得到 HelloTester.class,执行 class 文件后系统会启动一个 JVM 进程,找到 HelloTester.class 后将类信息加载到 JVM 中。 + +JVM 找到 mian 方法后就可以执行 main 中的 HelloTester helloTester=new HelloTester(),也就是在 JVM 里创建一个 helloTester 对象,不过此时方法区里面还没有 HelloTester 类的信息,所以 JVM 就会去加载该类: + + +加载 HelloTester 类后,JVM 在堆内就会为新的 HelloTester 实例进行内存的分配使用; +然后执行 helloTester.sayName(),JVM 根据 HelloTester 对象引用定位到方法区中 HelloTester 类的类型信息的方法表,获得 sayName() 的字节码地址; +最后执行 sayName(“cctester”)。 + + +以上便是 Java 对象在 JVM 中运行的大体过程,了解了这些基本信息之后,再来了解下堆空间中 Java 运行的线程状态,当程序开始创建线程时,便开始有了生命周期,其实就和人一样,会有“生老病死”几个状态,而对于线程来说会经历六个状态,如下表所示: + + + +我们用一张图来直观地概括下这几个状态的演变: + + + +从字面上来看,NEW、RUNNABLE、TERMINATED 这几个状态比较好理解,但对于 BLOCKED、WAITING、TIMED_WAITING 很多人却分不清楚,我想通过一些实际生活中的例子来帮助你理解。 + +BLOCKED + +先来说下 BLOCKED,比如你去参加面试,可是接待室里面已经有张三正在面试,此时你是线程 T1,张三是线程 T2,而会议室是锁。这时 T1 就被 blocked,而 T2 获取了会议室的锁。 + +WAITING + +接着我们来说 WAITING,你已经进入面试环节,面试官对你的第一轮面试比较满意,让你在会议室等第二轮面试,此时就进入了 WAITING 状态,直到第二轮面试开始你才能结束 WAITING 状态。 + +TIMED_WAITING + +当你结束了所有面试环节,HR 对你说我们一般会在三天内给回复,如果三天内没有回复就不要再等了,此时你就进入 TIMED_WAITING 状态,如果三天内没答复,你可能会看其他机会或者直接入职备选公司了。 + +这几个例子我想可以帮助你理解 TIMED_WAITING、WATING、BLOCKED 状态。 + +一般哪些线程状态占用 CPU 呢? + +处于 TIMED_WAITING、WATING、BLOCKED 状态的线程是不消耗 CPU 的,而处于RUNNABLE 状态的线程要结合当前线程代码的性质判断是否消耗 CPU: + + +纯 Java 运算代码,并且未被挂起,是消耗 CPU 的; +网络 IO 操作,在等待数据时是不消耗 CPU 的。 + + +通过如上的学习,你了解了线程的状态,可以知道这个线程是在“休息”还是在“奔跑”。如果很多线程处于“奔跑”状态,必定会消耗相关的硬件资源,反过来理解,如果在性能测试过程中发现资源消耗是不是也能定位到相关的线程,从而发现代码问题呢?当你定位到具体的代码行,是不是可以和研发人员讨论下有没有优化的空间,而不是简单地将机器升级配置去解决问题,所以我将继续沿着如何定位代码问题这条思路为你讲解。 + +举一个实际例子,我以一个问题为切入点,首先看下面示意代码,可以看出 CPU 占用比较高的线程。 + +top - 17:41:39 up 168 days, 8:55, 2 users, load average: 0.71, 0.81, 0.57 + +Tasks: 155 total, 1 running, 153 sleeping, 0 stopped, 1 zombie + +%Cpu(s): 68.4 us, 6.4 sy, 0.0 ni, 23.5 id, 0.0 wa, 0.0 hi, 1.7 si, 0.0 st + +KiB Mem : 8010676 total, 326472 free, 6196656 used, 1487548 buff/cache + +KiB Swap: 0 total, 0 free, 0 used. 1120940 avail Mem + +PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + + 6937 root 20 0 4778684 518804 6 + + 140 S 141.9 6.5 17:46.36 java + +14643 root 20 0 4639440 821244 2472 S 11.6 10.3 1789:33 java + + +通过如上示例的第 3 行你可以发现服务器上 CPU 占用蛮高的,空闲值为 23.5%,也就是说占用了 76.5%;再看第 8 行,你可以看到 PID 为 6937 的进程消耗 CPU 为 141.9%。可能你有疑问了,为什么使用率可以超过 100%。这和你的服务器核数有关系,因为这个数值是每个核上该进程消耗的 CPU 之和,会有叠加关系。那你已经知道了消耗 CPU 最高的进程,然后执行如下命令: + +[root@JD jmeter_test]# top -Hp 6937 + +top - 23:20:53 up 168 days, 14:35, 3 users, load average: 1.33, 0.71, 0.88 + +Threads: 788 total, 1 running, 787 sleeping, 0 stopped, 0 zombie + +%Cpu(s): 75.0 us, 6.2 sy, 0.0 ni, 18.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st + +KiB Mem : 8010676 total, 576860 free, 5697612 used, 1736204 buff/cache + +KiB Swap: 0 total, 0 free, 0 used. 1616168 avail Mem + +PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + +25695 root 20 0 5409224 1.0g 4892 S 6.2 13.2 0:00.09 java + + +我们可以看到每个线程的使用状态,你可以选择 25695 这个线程号,将 25695 转化为 16 进制,如下所示: + +printf "%x\n" 25695 + +645f + + +然后通过 jstack 命令定位可能存在问题的方法: + +jstack 6937|grep 645f -A 30 + + +通过运行上面的命令可以查看到的内容如下图所示: + + + +标红部分就是定位的业务代码,能够比较清晰地知道哪个方法在消耗 CPU 资源。 + +总结下来,要确定哪些线程状态占用 CPU 至少需要如下步骤: + + +使用 top 命令找出有问题 Java 进程的 ID; +开启线程显示模式(top -Hp); +按照 CPU 使用率将线程排序(打开 top 后按 P 可以按 CPU 使用降序展示); +记下 Java 进程 ID 及其 CPU 高的线程 ID; +用进程 ID 作为参数,手动转换线程 ID 成十六进制,通过 jstack 去剖析对应的线程栈,以分析问题。 + + +你可以看到,实际过程略显烦琐,而有能力的同学可以做成 shell 脚本,这样会比较方便,当然社区也已经有这样的开源脚本供大家使用,点击访问地址。 + +下载完成之后进入 useful-scripts,执行 ./show-busy-java-threads.sh,执行完成后的示意图如下所示: + + + +这样的方式是可以看到这台服务上所有导致 CPU 飙升的 Java 方法的,当然直接一键也可以查看指定进程里的 java 方法,非常简单方便,方法如下所示: + + show-busy-java-threads -p <指定的Java进程Id> + + +总结 + +根据本讲的学习,相信你已经能够掌握 Java 在 JVM 中的运行过程,以及 Java 线程在 JVM 中的运行状态,并且能够从 CPU 飙升定位到代码问题。 + +那对于你来说,当你发现 CPU 占用过高怎么去处理呢?我相信不同的公司、不同的开发语言有不同的方案,欢迎在评论区给出你的实践。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/15如何基于JVM分析内存使用对象?.md b/专栏/说透性能测试/15如何基于JVM分析内存使用对象?.md new file mode 100644 index 0000000..6b5807c --- /dev/null +++ b/专栏/说透性能测试/15如何基于JVM分析内存使用对象?.md @@ -0,0 +1,218 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 15 如何基于 JVM 分析内存使用对象? + 上一讲我带你学习了基于 JVM 的线程分析,相信你已经可以通过热点线程分析出哪些方法在消耗 CPU,拿到这些方法之后你就可以和研发人员讨论后续的优化方案了。那这一讲我们就来重点学习 JVM 内存是如何管理的,有哪些手段可以分析内存对象,并帮助你定位内存的瓶颈。 + +提到分析 JVM 的内存对象,可能你会问我,之前讲过如何判断服务器内存瓶颈,那 JVM 内存和服务器内存有什么联系呢。我们先来看下这两者的关系,如下图所示: + + + +图 1:内存关系示意图 + +其实二者的关系很简单,对于服务器系统而言,JVM 只是其中的一部分。当操作系统内存出现瓶颈时,我们便会重点排查哪些应用会占用内存。不过对于更深一步分析内存的使用,并不仅仅是统计使用、空闲等这些数值,我们需要进一步去了解内存结构,以及内存如何分配、如何回收,这样你才能更好地确定内存的问题。 + +JVM 内存分配 + +通过第 14 讲的学习你可以知道,Java 文件一般是先编译成 class 结尾的文件,然后通过类加载器到 JVM 内存中。接着我们来看看 JVM 内存结构图,这样能够对它有个全局的了解。 + + + +图 2:JVM 内存分配示意图 + +1.本地方法栈 + +本地方法栈保存的是 native 方法的信息,native 方法就是 Java 调用非 Java 代码的接口,为什么会有这样的设置呢?简单来说,sun 的解释器是由 C 语言实现的,而 jre 又是基于 Java 语言,所以需要 native 方法来进行跨语言的调用。 + +2.Java 栈 + +Java 栈是常用的内存区域之一,它里面存放着基本数据类型和对象的引用,可能你不太清楚什么是对象的引用,拿上一讲中 HelloTester helloTester=new HelloTester() 为例,在 Java 栈中 HelloTester 是个引用,指向在堆空间中开辟的该对象的空间。 + +3.方法区(JDK 1.8+已经移除) + +也叫作永久区,用来存储类信息,如上文描述的 HelloTester。值得注意的是方法区在 JDK 1.8 以上已经被元空间取代,并且元空间不在 JVM 中了,而是在本地内存中独立开辟存储空间。 + +4.程序计数器 + +可以认为是线程的信号指示器,它的作用是保存线程当前程序的执行位置,以保证多线程的切换。因为在多线程的情况下,CPU 并不是完成一个线程执行再去执行另外一个线程,而是不停地切换线程执行,这时程序计数器就可以发挥作用了。 + +5.堆 + +堆区域是 JVM 调优最重要的区域,堆中存放的数据很多是对象实例,如 HelloTester 的对象存储。堆空间占据着 JVM 中最大的存储区域,存放了很多对象,所以大多数基于 JVM 的内存调优也是对堆空间的调优。 + +堆空间并非取之不尽,如果一直存放总有用完的时候,所以对于有用的对象应当保存起来,无用的对象应当回收,为了更好地实现这一机制,JVM 将堆空间分成了新生代和老生代,如下图所示: + + + +图 3:GC 示意对比图 + +通过图 3 可以看到新生代和老年代的对比,Minor GC 发生在新生代,而 Full GC 发生在老年代。新生代分为三个区,一个 Eden 区和两个 Survivor 区。 + +先来看下 Eden 区的作用,大部分新生成的对象都是在 Eden 区,Eden 区满了之后便没有内存给新对象使用,Eden 区便会 Minor GC 回收无用内存,剩下的存活对象便会转移到 Survivor 区。 + +那两个 Survivor 区的作用分别是什么呢?两者其实是对称分布的,一个是 From 区,一个是 To 区。从 Eden 区存活下来的对象首先会被复制到 From 区,当 From 区满时,此时还存活的对象会被转移到 To 区,经历了多次的 Minor GC 后,还存活的对象就会被复制到老年代,老年代的 GC 一般叫作 FullGC 或者 MajorGC。 + +我们对比下新生代垃圾回收和老年代垃圾回收的区别,如下表所示: + + + +如何定位内存占用问题 + +回到我们实际工作当中,当你发现 JVM 中使用的内存越来越多或者增长很快的时候,频繁 GC 的时候,应当如何去定位哪些对象导致的这些问题呢? + +这其实涉及两个问题: + + +如何去观察 GC 的频次; +定位占用内存的对象。 + + +1.如何观察 GC 的频次? + +本部分我以 JDK 自带的工具来讲解,我一般使用 jstat 来查看 GC 的频次。首先我们来看下基本用法,如下所示: + +[root@JD ~]# jstat -gc 26607 1000 3 + + S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT + +512.0 512.0 320.0 0.0 86016.0 27828.5 175104.0 157974.6 122840.0 116934.9 16128.0 15060.4 5328 37.311 4 1.042 38.353 + +512.0 512.0 320.0 0.0 86016.0 27981.9 175104.0 157974.6 122840.0 116934.9 16128.0 15060.4 5328 37.311 4 1.042 38.353 + +512.0 512.0 320.0 0.0 86016.0 28885.4 175104.0 157974.6 122840.0 116934.9 16128.0 15060.4 5328 37.311 4 1.042 38.353 + + +我们来解析下终端输入的命令: + +jstat -gc 26607 1000 3 + + + +26607 代表查看的 PID 的 Java 进程号; +1000 代表每隔 1000ms 也就是 1s 显示一次; +3 代表一共显示三次。 + + +接着我们再来看输出选项代表的含义有哪些?这个输出的信息含量比较大,不过信息是有对应关系的,比如 S0C 和 S0U: + + +一般 C 结尾的代表总的容量大小或者计数的次数; +U 结尾代表已使用的容量大小。 + + +这是通用的,你可以看到输出项中有很多以 C 或者 U 结尾。S0 则代表第一个 Survivor 区,也就是我上文说的 From 区。通过以上的讲解,我相信很多名词你不用死记硬背也能理解了,比如 S1C 和 S1U 则表示第二个 Survivor 区也就是 To 区的总容量和使用容量。 + +接下来我罗列下其他的输出选项含义。 + + +EC / EU:Eden 区的总容量/已使用空间的大小。 +OC / OU:老年代总容量/老年代已使用空间大小。 +MC / MU:方法区总容量/方法区已使用容量大小。 +CCSC / CCSU:压缩类总容量/压缩类空间使用大小。 +YGC / YGCT:年轻代垃圾回收的次数/年轻代垃圾回收消耗时间。 +FGC / FGCT: 老年代垃圾回收次数/老年代垃圾回收消耗时间。 +GCT:垃圾回收消耗总时间。 + + +这样对比着看会更直观一点,对于上述输出选项的含义我们都需要有一定的印象,从而通过垃圾回收频率和消耗时间初步判断 GC 是否存在可疑问题。 + +有同学问过这样的问题,堆内存区域划分了这么多代,感觉很复杂,为什么要这么做呢? + +我想不分代,内存垃圾肯定也是可以回收的。而让内存区域分代,主要就是优化垃圾回收的性能,也就是 GC 的性能。有点类似于我们日常生活中的垃圾分类,你把干湿垃圾分离,一方面有利于下一步的再利用,再者对于我们后续垃圾的处理效率也会有较大的提升。对于内存回收其实也是这样的,如果不分代那么所有的对象可能都在同一个大的区间里,GC 依次判断则效率必然是很低,如果是分代处理,对不同的区域分以不同的回收策略,这样效率会高很多。 + +2.如何定位占用内存的对象? + +这里我将推荐一个工具 jmap,通过 jmap 可以指定 Java 进程的 PID,查看该进程的对象、数量等等,接下来我做一个演示。 + +首先我们来查看进程号为 18658 的应用包,如下所示: + +[root@JD ~]# ps -ef|grep demo + +root 18658 1 0 Dec09 ?后续省略 + + +其中上述输出的第二列 18658 为进程号,然后将进程号通过命令组合可以查看以下信息: + +[root@JD ~]# jmap -histo 18658|head -n 20 + + num #instances #bytes class name + +---------------------------------------------- + + 1: 157619 18840672 [C + + 2: 8326 8324360 [B + + 3: 146319 3511656 java.lang.String + + 4: 9224 2825584 [I + + 5: 65733 2103456 com.example.demo.entity.User + + 6: 62508 2000256 java.util.HashMap$Node + + 7: 21868 1618832 [Ljava.lang.Object; + + + +num 是编号; +instances 是生成的实例个数; +bytes 是实例占用的大小; +classs name 对象的类名。 + + +其中 [C、[S、[I、[B 对应的类型如下所示: + +[C is a char[] + +[S is a short[] + +[I is a int[] + +[B is a byte[] + + +你注意下第五行,这是能够最直接看到的业务类,如果是业务对象尤其需要关注,看是否一直上升。 + +可视化的 JVM 监控工具 + +在第三模块中,你可以知道,对于监控定位我一般会采用命令行结合可视化的方案一并讲解,接下来我介绍一个 JDK 自带的 JVM 监控工具:jvisual。 + +jvisual 能做的事情很多,监控内存泄漏、跟踪垃圾回收、执行时内存分析、CPU 线程分析等,而且通过图形化的界面指引就可以完成,接下来我主要讲述 jvisual 如何使用以及如何看内存对象的占用。 + +先来看下 jvisual 是如何使用的,一般我们会在启动被测的 jar 服务里进行如下配置: + +nohup java -Djava.rmi.server.hostname=实际ip -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -jar demo-0.0.1-SNAPSHOT.jar & + + +通过这样的方式可以启动暴露 1099 端口,且连接时不需要认证。 + +然后在本机电脑 jdk 路径 bin 目录下找到 jvisualvm,双击打开,如下图所示: + + + +我们再配置相应的 jmx 连接,如下图所示: + + + +如果出现如下图所示的界面,就证明连接成功了。 + + + +这样我们就能够概览 JVM 的 CPU 和内存的使用情况,如下图所示,通过点击抽样器,你可以分别获得对象在 CPU 和内存的占用。值得注意的是很多初学者把这部分 CPU 监控或者内存监控认为是服务器硬件级别的,这是不对的,这些都是基于 JVM 的监控。 + + + +按照内存占用进行排序是非常清晰的,你可以看到随着性能测试的进行,User 类字节占用比例越来越高,如下图所示: + + + +总结 + +通过本讲的学习,你了解了 JVM 的内存结构,知道了 Java 内存对象经常活动的区域,同时列举了常见的排查手段诊断内存问题。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/16如何通过Arthas定位代码链路问题?.md b/专栏/说透性能测试/16如何通过Arthas定位代码链路问题?.md new file mode 100644 index 0000000..a13f2fc --- /dev/null +++ b/专栏/说透性能测试/16如何通过Arthas定位代码链路问题?.md @@ -0,0 +1,305 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 16 如何通过 Arthas 定位代码链路问题? + 前两讲我分别讲述了 JVM 线程分析和内存分析的实操技巧及注意点,让你可以从表象的硬件瓶颈逐渐深入到定位代码问题。类似于这样的定位方式,有一个共同点是会呈现出硬件层或者表象的一些异常。 + +然而并不是所有的性能问题都可以通过这样的方式去定位,有时候接口的 TPS 很低,但是各项资源占用也很低,你很难通过资源的异常去找到诊断的切入口。这样的情况也是很常见的,除了可以用《11 | 分布式服务链路监控以及报警方案》中讲到的链路监控工具定位外,这一讲我还会带来一个代码级定位工具——Arthas,Arthas 不仅仅能够让你看到 JVM 的运行状态去定位性能问题,对于很多线上代码异常问题的排查,Arthas 也是手到擒来。 + +下面的场景你一定很熟悉: + + +如果线上出现了偶发性问题,我们需要花费大量时间一步步排查,日志里的每个细节都不能错过,如果线上不能直接得出结论,还需要线下再去复现,很消耗时间和精力; +某行代码没有被执行,也没有出现报错信息,按照传统的方法可能会去加一些判断日志,这就涉及再次上线的问题,很多公司上线流程需要审批,这增加了内耗。 + + +而熟练使用 Arthas 这个工具便可以很好地解决以上问题,接下来我将介绍下 Arthas 以及实战中怎么用 Arthas 定位问题。 + +Arthas 是什么 + +Arthas 是阿里提供的一款 Java 开源诊断工具。能够查看应用的线程状态、JVM 信息等;并能够在线对业务问题诊断,比如查看方法调用的出入参、执行过程、抛出的异常、输出方法执行耗时等,大大提升了线上问题的排查效率。 + +Arthas 的使用方法 + +首先我们用以下命令下载 Arthas: + +wget https://alibaba.github.io/arthas/arthas-boot.jar + + +Arthas 提供的程序包是以 jar 的形式给出,因此我们可以看出 Arthas 本质也是个 Java 应用。 + +然后使用 java -jar 命令启动,如下所示: + +java -jar arthas-boot.jar + + +输入启动命令后会跳出选项让你选择要监控的后台 Java 进程,如下代码所示: + +[root@JD ~]# java -jar arthas-boot.jar + +[INFO] arthas-boot version: 3.3.3 + +[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER. + +* [1]: 689 cctestplatform.jar + +[2]: 31953 unimall.jar + +[3]: 14643 sentinel-dashboard-1.7.2.jar + +[4]: 20421 org.apache.zookeeper.server.quorum.QuorumPeerMain + +[5]: 10694 demo-0.0.1-SNAPSHOT.jar + + +其中序号表示的就是 Arthas 自动识别的所在服务器的 Java 进程,然后输入要监控的进程的序号并回车,Arthas 便会接着启动,出现 Arthas Logo 之后便可以使用 Arthas 了,如下代码所示: + +5 + +[INFO] arthas home: /root/.arthas/lib/3.4.4/arthas + +[INFO] Try to attach process 10694 + +[INFO] Attach process 10694 success. + +[INFO] arthas-client connect 127.0.0.1 3658 + +wiki https://arthas.aliyun.com/doc + +tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html + +version 3.4.4 + +pid 10694 + + +另外如果你想要打印帮助信息可以在启动命令后面加 -h 来完成,如下所示: + +java -jar arthas-boot.jar -h + + +Arthas 实操演示 + +1.利用 Arthas 线程相关命令定位死锁问题 + +在排查问题前我们先了解下死锁,死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。如果没有外力的作用,那么死锁涉及的各个线程都将永久处于循环等待状态,导致业务无法预期运行,所以我们的代码要避免死锁的情况。 + +死锁就好比打游戏排位的时候, A 和 B 不能选相同的英雄,A 选了诸葛亮,但是突然间后悔了,想重新选貂蝉,但是貂蝉已经被 B 选了,但是 B 选择之后也后悔了,想重新选诸葛亮,但是诸葛亮已经被 A 选了。这个时候 A 和 B 都不让步,结果是谁都无法选到想要的英雄。 + +死锁的代码演示如下: + + //线程Lock1代码示意 + + while(true){ + + synchronized("obj1"){ + + Thread.sleep(3000);//获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2 + + synchronized("obj2"){ + + System.out.println("Lock1 lock obj2"); + + } + + } + + } + + //线程Lock2代码示意 + + while(true){ + + synchronized("obj2"){ + + Thread.sleep(3000); //获取obj2后先等一会儿,让Lock1有足够的时间锁住obj1 + + synchronized("obj1"){ + + System.out.println("Lock2 lock obj1"); + + } + + } + + } + + +把以上代码放到服务器中执行,然后我们可以使用 Arthas 的 jvm 命令查看到线程死锁数目为 2,说明程序发生了死锁,如下图所示: + + + +图 1:死锁示意图 + +接下来我们输入 thread -b 命令查看当前阻塞其他线程的线程,然后我们可以看到 Lock 1 被阻塞了,访问的资源被 Lock 2 占用,如图 2 所示,根据提示的行数我们便能找到死锁位置,对代码进行优化。 + + + +图 2:thread 演示死锁详情图 + +2.使用 trace 命令查看耗时 + +我们写几个不同循环次数的方法,分别代表方法耗时不同,代码如下: + +//示例代码,timeCost是个接口 + +public String timeCost(){ + + cost1(); + + cost2(); + + cost3(); + + ....... + +} + +private void cost3() { + +//进行200万次循环...... + +} + +private void cost2() { + + //进行10万次循环...... + +} + +private void cost1() { + + //进行10次循环...... + +} + + +部署之后我们使用 trace 来查找最耗时的方法,在此过程中需要知道包名、类名,以及方法的名字。上述代码方法所在包名为 com.cctest.arthas_demo.controller,类名为 StressSceneController,所以我们需要输入如下 trace 命令: + +trace + +com.cctest.arthas_demo.controller.StressSceneController timeCost + + +输完命令后回车,然后 arthas 程序就进入了等待访问状态。这时候访问接口 /timeCost,我们就可以看到被测程序台在疯狂打印日志,等结束之后,arthas 命令窗口便输出了各方法耗时时间,如图 3 所示: + + + +图 3:方法耗时详情 + +我们可以看到 timeCost 方法总耗时 258391ms: + + +cost 1 调用耗时 9 ms; +cost 2 调用耗时 13909 ms; +cost 3 调用耗时 244472 ms。 + + +cost 2 和 cost 3 方法耗时都比较长,当你能够定位到方法级别的消耗时长时,基本已经能够找到问题的根因了。 + +3.使用 watch 命令观察被测方法的参数和返回值 + +当遇到线上数据问题时,我们一般有两种查找问题的途径: + + +在开发环境中模拟线上数据来复现问题,不过因为环境等各方面的不同,很多情况下模拟数据复现都有难度; +在生产日志里查找线索,如果没有相关方法的入参,以及没打印返回值的话,就难以找到有效的信息。 + + +这两种传统查找问题的方式都存在一定的局限性,而使用 Arthas 的 watch 命令可以很方便地根据观察方法入参和出参来判断是否正确定位了代码问题。 + +为了能够让你更清楚地看到方法名和出参入参,我写了一段示例代码,如下所示: + +@GetMapping("/login") + +public String login(@RequestParam(value="userName") String userName, @RequestParam(value="pwd")String pwd){ + + return "OK"; + +} + + +然后我们输入 watch 命令,其中 login 后面指定了需要控制台输出的内容,params[0] 代表第一个参数,如果参数有多个,只要 params 加上下标即可,returnObj 代表返回值,示意代码如下: + +watch com.cctest.arthas_demo.controller.StressSceneController login "{params[0],params[1],returnObj}" + + +你可以看到输入上述命令后的返回信息如下: + + + +图 4:watch 返回信息 + +4.使用 tt 命令定位异常调用 + +tt 与上面的 watch 的命令有些类似,都可以排查函数的调用情况。但是对于函数调用 n 次中有几次是异常情况,用 watch 就没有那么方便,使用 tt 命令就可以很方便地查看异常的调用及其信息。 + +使用 tt 命令示意如下: + +tt -t com.cctest.arthas_demo.controller.StressSceneController login + + +然后我们访问接口,每次访问的状态和结果显示如图 5 所示: + + + +图 5:tt 的返回信息 + +从图中可以看出,tt 显示的信息比 watch 全面。其中 IS-RET 项如果为 false,即为错误的调用。 + +以上部分介绍了 Arthas 命令在实际例子中的使用,我是通过命令行的方式来演示的,所以你需要登上服务器。之前有提到过,并不是所有的同学都有权限直接进行服务器的操作,那面对这样的情况如何使用 Arthas 呢?其实也是有解决方法的,接下来我们将介绍通过 Web 的方式操作 Arthas。 + +通过 Web 的方式操作 Arthas + +Arthas 提供了客户端访问程序 Arthas Tunnel Server,这样我们便可以操作 Arthas 了,接下来我介绍下 Arthas Tunnel Server 的操作步骤以及操作原理。 + +1.Arthas Tunnel Server 的操作步骤 + +(1)下载 arthas-tunnel-server.jar,点击下载地址; + +(2)把 Arthas Tunnel Server 部署到能和线上服务器通信的目标服务器; + +(3)确保线上服务器启动了 Arthas,线上启动 Arthas 的操作命令,如下所示: + +java -jar arthas-boot.jar --tunnel-server 'ws://目标服务器ip:目标服务器port/ws' --target-ip + + + +这里说的 target-ip 是指被测程序所在服务器的 IP; +目标服务器即 Arthas Tunnel Server 启动的服务器,端口号默认是 8080。 + + +(4)在浏览器中输入 http://目标服务器ip:目标服务器port,就可以访问 WebConsole,如图 6 所示: + + + +图 6:Web 方式 Arthas 启动 + +然后我们输入 ip、port 和 agentid 就可以连上被测程序,并且可以开始对被测程序输入 Arthas 命令。接下来的 Arthas 的使用和命令行方式是一样的,不再赘述。 + +2.Arthas Tunnel Server 的操作原理 + +通过 Arthas Tunnel Server 的操作步骤,我们可以总结出它实现 Web 访问的原理:所有节点的 Arthas 服务启动都会向注册中心(Arthas Tunnel Server)注册,注册中心维护了一个节点列表,当客户端发起访问某个节点,Arthas Tunnel Server 便会从维护的节点列表找到与请求的 ip 和端口号对应的节点进行访问,然后把访问结果返回给客户端。具体流程如图 7 所示: + + + +图 7:Arthas Tunnel Server 原理图 + +通过 Web 方式使用 Arthas 与我们上面所说的非 Web 的方式最大的不同: + + +Web 方式可以授权连接之后通过浏览器输入 Arthas 命令; +非 Web 方式则是直接 ssh 连接服务器输入命令。 + + +两者比较起来 Web 方式虽然操作麻烦些,不过在权限管控比较严格的情况下提供了使用 Arthas 的可行性。 + +总结 + +这一讲我主要介绍了 Arthas 是什么、为什么使用 Arthas,以及通过实际操作演示 Arthas 是怎么实时定位代码问题的,并且为你介绍了 Arthas Tunnel Server 的操作步骤以及原理。上面第三部分的 4 个操作实例都是比较典型的排查线上问题的方式,通过实例的演示也能看出来 Arthas 的强大和便捷性。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/17如何应对Redis缓存穿透、击穿和雪崩?.md b/专栏/说透性能测试/17如何应对Redis缓存穿透、击穿和雪崩?.md new file mode 100644 index 0000000..950fafe --- /dev/null +++ b/专栏/说透性能测试/17如何应对Redis缓存穿透、击穿和雪崩?.md @@ -0,0 +1,203 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 17 如何应对 Redis 缓存穿透、击穿和雪崩? + 上一讲我带你学习了如何应用 Arthas 定位代码以及链路问题。这一讲我将带你来学习一个关键的内存数据库中间件 Redis,希望你可以了解它的作用,以及在使用过程中的常见问题以及解决方案。 + +为什么使用内存数据库? + +首先我们来看看最早期的 Web 架构是什么样的,如图 1 所示: + + + +图 1:早期架构 + +这是互联网早期的常用架构,不过这样的架构一般只满足于基本的业务运转,一旦业务量迅速增高,就会出现各种请求延迟,甚至超时响应或者直接请求拒绝的情况 ,也就是在高访问量下会发生性能问题,而且这样的框架性能问题又集中在数据库层面。 + +那么问题来了,为什么会产生这种情况呢?由于数据库的数据是存在硬盘上,硬盘的 I/O 读写瓶颈会直接影响并发量。既然磁盘 I/O 读写时瓶颈,我们是不是可以采用速度更快的内存来存储常用但数据量不算大的数据呢?答案是肯定的。 + +为了解决上面的问题,目前通用的做法是引入基于内存的数据库,这样的数据库一般是把数据先放到内存里,引入缓存中间件之后的项目 Web 服务架构图如下所示: + + + +图 2:演变架构 + +这样便可以较大程度缓解传统数据库带来的磁盘 I/O 读写瓶颈,而我们最常使用的基于内存的数据库就是 Redis 和 MemCached。 + +Redis 和 Memcached 对比 + +1.存储方式 + + +MemCached 目前只支持单一的数据结构 Key-Value 形式; +Redis 支持多种数据结构,有字符串、列表、集合、散列表、有序集合等。 + + +2.持久化 + +持久化就是把数据从内存永久存储到磁盘里,可以防止断电等异常情况下数据丢失等问题。目前 Redis 支持持久化,而 MemCached 不支持。遇到灾难,MemCached 无法恢复数据,Redis 可以恢复数据,保证了数据的安全性。 + +从以上特点可以看出 Redis 在数据多样性和安全性上远高于 MemCached。以我的从业经历讲,MemCachded 使用频率越来越低,绝大多数的业务场景使用 Redis 居多。 + +Redis 带来的性能影响 + +我们列举一个案例来看 Redis 带来的性能影响。 + +我们使用 Spring Boot 开发连接 Redis 的 demo,分如下三步。 + +(1)在 Maven 中引入 Spring Boot 使用的 Redis 类库,如下代码所示: + + + + org.springframework.boot + + spring-boot-starter-data-redis + + 2.4.2 + + + + +(2)通过注解方式获取 RedisTemplate,如下代码所示: + +@Autowired + +private RedisTemplate redisTemplate; + + +(3)使用 Redis 提供的 API 实现业务代码的缓存读写,如下代码所示: + +@GetMapping("/getRedisTestData") + +public Result getRedisTestData(){ + + String redisTestListData = null; + + try { + + redisTestListData = redisTemplate.boundValueOps("redisTest.findAll").get(); + + //如果redis中没有数据的话 + + if(null == redisTestListData){ + + //查询数据库获得数据 + + List redisTestList = simulateSceneRepository.findAll(); + + //转换成json格式字符串 + + ObjectMapper om = new ObjectMapper(); + + redisTestListData = om.writeValueAsString(redisTestList); + + //将数据存储到redis中,下次在查询直接从redis中获得数据,不用再查询数据库 + + redisTemplate.boundValueOps("redisTest.findAll").set(redisTestListData); + + log.info("从Mysql数据库获得数据"); + + }else{ + + log.info("从redis缓存中获得数据"); + + } + + } catch (Exception e){ + + log.error("e:{}",e); + + } + + return Result.resultSuccess(null,redisTestListData,"数据读取成功"); + +} + + +通过如上三步就可以完成 Java 使用 Redis 的 demo,我大概总结下代码流程,第一次先判断 Redis 中是否存在查询的数据,如果没有就需要从数据库中读取数据了,读取成功之后把数据回写到 Redis 中,后面的请求就能直接从 Redis 中直接读取了,较大地减少了对数据库的查询压力。我们可以通过运行上面写好的代码来看下实际效果。 + +首先我们向数据表 redis_test 插入 10w 条数据,然后分两次访问该接口,对比下两次访问的响应时间。 + +第一次直接从 MySQL 数据库读取,一共花了 39.43s,如下图所示: + + + +图 3:MySQL 取数据耗时 + +而第二次数据已经进入 Redis,请求只需要 2.62s,节省了很长时间。值得注意的是为了演示效果,取出的数据条数达到 10w+,所以响应时间也达到了秒级别。在正常的互联网业务当中,Redis 读写操作均在毫秒级别。 + + + +图 4:Redis 取数据耗时 + +从上面实例可以看出使用 Redis 和不使用 Redis 性能差距明显,所以从目前的互联网项目来讲,使用 Redis 是一个非常普遍的情况,接下来我们来了解下 Redis 其他特性和优缺点。 + +Redis 其他特性以及优缺点 + +1.Redis 的特性 + +主从复制功能 + +虽然数据在内存中读写速度比较快,但是在高并发情况下也会产生读写压力特别大的情况,Redis 针对这一情况提供了主从复制功能。 + +主从复制的好处有如下两点: + + +提供了 Redis 扩展性,当一台 Redis 不够用时,可以增加多台 Redis 作为从服务器向外提供服务; +提供了数据备份和冗余服务器,当 Redis 主服务器意外宕机,从服务器可以顶替主服务器向外提供服务,增加了系统的高可用性。 + + +脚本操作 + +Redis 提供了 lua 脚本操作,你可以将 Redis 存取操作写到 lua 脚本里,然后通过 Redis 提供的 API 来执行 lua 脚本,这样就可以实现 Redis 相关操作。 + +我们同样可以用 Redis 提供的 API 直接实现 Redis 相关操作,那么为什么有时候又要绕一圈去操作 lua 脚本呢?因为 lua 脚本能够保证操作的原子性,即所有的操作当作一个操作,要么全部失败要么全部成功。而直接使用 API 不一定能保证一连串操作的原子性,所以当需要保证原子性的时候需要使用 lua 脚本。 + +发布与订阅 + +该特性可以将 Redis 作为消息中间件,在服务端产生消息,然后在客户端消费消息队列里的消息,但是作为消息队列不是 Redis 的强项,所以不推荐使用。比如 Redis 作为消息队列消息并非完全可靠,会产生消息丢失的问题,并且也不支持消息分组。在性能上,如果入队和出队操作频繁,那 Redis 性能比起 RabbitMq 等常用消息队列来说还是有差距的。 + +了解了 Redis 的一些特性,那使用过程中有没有一些注意点呢?其实我们也会踩到坑,比较常见的问题是缓存穿透、缓存击穿以及缓存雪崩,接下来就来讲讲这些问题出现的现象以及如何解决。 + +2.Redis 的缺点 + +缓存穿透 + +缓存穿透的情况是 Redis 和 MySQL 数据库都没有这条数据,但是用户不断并发发起请求,请求压力会同时落到数据库和缓存上,这样的情况相对于设计初衷来说,对系统的压力就会大很多了,而且这也是黑客发起攻击的手段之一,找寻你的系统是否存在漏洞。 + +那在项目中如果遇到缓存穿透我们该如何解决呢? + +遇到缓存穿透,我们可以在请求访问缓存和数据库都没查到数据时,给一个默认值或者 Null 值,即 Key-Null。然后该缓存值的有效时间可以设置得短点,比如 30s。在业务代码中判断如果是 Null 值就取消查询数据库,或者间隔 30s 之后重试,这样的方式可以大幅度减轻数据库的查询压力。 + +缓存击穿 + +单个数据在缓存中不存在,而在数据库中存在。一般这种情况都是缓存失效导致的,在缓存失效的时间段有大量并发用户访问,首先访问缓存,因为 Key 已经过期了,所以查不到数据,然后所有查询压力都会落到数据库上,造成数据库的压力过大。并且还有可能因为并发问题导致重复更新缓存而过多占用缓存资源。 + +在项目中如果遇到缓存击穿问题,该如何解决呢? + + +对于一些经常被访问的热点数据,可以根据业务特性主动检查使其 Redis 数据永不过期,当然这样的设置并不代表说这条数据一直不更新而处在 Redis 中,而是根据数据字段中的失效时间和系统时间的对比主动检查更新数据,使 Redis 数据不会过期; +通过后台定时刷新,根据缓存失效时间节点去批量刷新缓存数据,这个适合 Key 失效时间相对固定的场景。 + + +缓存雪崩 + +大量数据在同一时间失效,会造成数据库查询压力过大导致宕机。缓存雪崩与缓存击穿的区别在于缓存击穿是单个数据失效,缓存雪崩是多个数据同一时间失效。 + +在项目中如果遇到缓存雪崩的问题,我们该如何解决呢?以下 3 种方法可以帮我们解决。 + + +如果程序设置的缓存过期时间统一为一个固定的值,比如 5s、10s、15s 等等,那么很有可能出现大量数据在同一时间失效。这个时候我们可以设置不同的过期时间,比如统一时间加上一个随机时间,这样可以让缓存的时间尽量均匀分布一点。 +不设置过期时间,让程序的定时任务自动定时更新或者清除缓存 +使用集群化的方式,保证高可用。 + + +总结 + +通过本讲的学习你了解了 Redis 的作用,Redis 使用过程中遇到的缓存穿透、缓存击穿以及缓存雪崩现象,及如何解决此类问题,相信你已经有了一个更深刻的认识。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/18如何才能优化MySQL性能?.md b/专栏/说透性能测试/18如何才能优化MySQL性能?.md new file mode 100644 index 0000000..f7a5a31 --- /dev/null +++ b/专栏/说透性能测试/18如何才能优化MySQL性能?.md @@ -0,0 +1,269 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 18 如何才能优化 MySQL 性能? + 上一讲带你学习了 Redis,知道了它带来的好处,不过 Redis 虽然高效迅速,但如果不能合理使用依然会存在不少性能问题。 + +这一讲我会带你学习以 MySQL 为例的持久化的数据库,说到数据库优化这块,很多同学并不陌生,比如添加索引、读写分离之类。那如何第一时间发现索引有没有缺失、索引有没有生效、扫描了多少行、读写分离用的什么策略,很多同学又不知道如何回答,本讲我就围绕 MySQL 优化的点来一起聊聊。 + +为什么要对 MySQL 进行优化? + +有一句俗话叫作“Web 项目即增删改查”,虽然这句话未必精确,但足以体现 Web 项目对数据的依赖程度,MySQL 数据库作为数据的重要载体,自然围绕着 MySQL 的优化也是必不可少的。而且对于一些发展中公司来说,往往项目初期数据量比较少,并没有把数据库优化列入日常的活动当中。当业务累积到足够的数据量时,会发现系统越来越慢,这时候数据库优化才引起重视,并投入大量的人力物力,当然不仅仅消耗的是企业成本,还会牺牲用户体验。 + +一次 SQL 的查询过程是怎样的? + +简单来说,我们可以将这个过程概括为以下 5 步。 + + +客户端发送一个查询 SQL 给数据库服务器。 +服务器先检查查询缓存,如果命中,也就是查询缓存中有这条记录,那么便直接返回缓存中的结果。如果没有命中,则进入下一阶段(解析器)。 +服务器由解析器检查 SQL 语法是否正确,然后由预处理器检查 SQL 中的表和字段是否存在,最后由查询器生成执行计划,也就是 SQL 的执行方式或者步骤。 +MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询。 +将结果返回给客户端。 + + +然后我们将上述步骤使用流程图展示,如下所示: + + + +图 1:MySQL 查询过程 + +对于 MySQL 来说,影响性能的关键点有哪些? + +关于这个问题,我想大家都应该可以回答一些,比如硬件层面、系统层面等等。但在性能领域中,一个不能忽略的问题是你需要考虑影响的面有多少,以及如何优化才是最具有性价比的。以我的经验来看,如何做优化更具性价比也存在漏斗模型,如图 2 所示。 + + + +图 2:漏斗模型 + +从上往下看: + + +SQL 语句和索引相关问题是最常见的,带来的价值也是最明显的; +系统配置库表结构带来的价值次之; +而硬件层次的优化优先级是不高的。 + + +1.硬件配置 + +现在我们基本上都是使用云服务器,就会涉及服务器配置选型,对于数据库处理复杂 SQL 而言,尽量选择高频 CPU,而且数据库一般都会开辟缓存池来存放数据,所以在服务器选型的时候内存大小也需要考虑。一般来说数据库服务器的硬件配置的重要性高于应用服务器配置,这方面了解下即可,测试工作基本上不会涉及数据库服务器的选型,而且一旦选型固定之后不会轻易改变数据库的硬件配置。 + +2.MySQL 系统配置选项 + +(1)max_connections + +这个参数表示 MySQL 可以接收到的最大连接数,可以直接通过如下命令查看: + + mysql> show variables like '%max_connections%'; + ++-----------------+-------+ + +| Variable_name | Value | + ++-----------------+-------+ + +| max_connections | 151 | + ++-----------------+-------+ + + +那如何查看 MySQL 的实际连接数呢?我们可以用如下命令进行查看: + + mysql> show status like 'Threads%'; + ++-------------------+-------+ + +| Variable_name | Value | + ++-------------------+-------+ + +| Threads_cached | 7 | + +| Threads_connected | 64 | + +| Threads_created | 1705 | + +| Threads_running | 1 | + ++-------------------+-------+ + + +其中 Threads_connected 是你实际的连接数。如果 max_connections 的值设置较小,在高并发的情况下易出现 “too many connections” 这样的报错,我们可以通过如下命令调节配置从而减少此问题的发生,你可以根据所在公司的实际情况进行配置。 + + mysql> set global max_connections=500; + + +(2)innodb_buffer_pool_size + +这个参数实际定义了 InnoDB 存储引擎下 MySQL 的内存缓冲区大小。 + +来解释下这句话什么意思,首先 InnoDB 存储引擎是 MySQL 的默认存储引擎,使用也很广泛。缓冲池是什么呢?其实就和缓存类似,通过上一讲学习你可以知道,从磁盘读取数据效率是很低的,为了避免这个问题,MySQL 开辟了基于内存的缓冲池,核心做法就是把经常请求的热数据放入池中,如果请求交互的数据都在缓冲池中则会很高效,所以一般数据库缓冲池设置得会比较大,占到操作系统内存值的 70%~80%。 + +那如何评估缓冲池大小是否合理? + +我们可以通过计算缓存命中率来判断,公式为: + + +(1-innodb_buffer_pool_reads/innodb_buffer_pool_read_request) * 100 + + +一般来说,当缓存命中率低于 90% 就说明需要加大缓冲池了。 + +关于公式中的两个变量的查看方式,通过如下命令你就可以获得: + + show status like 'Innodb_buffer_pool_read_%'; + ++---------------------------------------+----------+ + +| Variable_name | Value | + ++---------------------------------------+----------+ + +| Innodb_buffer_pool_read_ahead_rnd | 0 | + +| Innodb_buffer_pool_read_ahead | 51 | + +| Innodb_buffer_pool_read_ahead_evicted | 0 | + +| Innodb_buffer_pool_read_requests | 25688179 | + +| Innodb_buffer_pool_reads | 2171 | + ++---------------------------------------+----------+ + + +3.SQL 优化 + +对于成熟的互联网公司来说,不管是硬件还是配置层面的数值都已经优化且形成了一定的经验值,其实不太可能频繁地改动。而对于业务的 SQL 来说,每天都会更新,一旦 SQL 本身执行很慢,无论从配置或者是硬件进行优化都无法根本解决问题。SQL 的问题也是你做数据库调优接触最多的,也是多样化的,所以接下来我们就继续学习慢 SQL 相关的知识点。 + +(1)什么是慢 SQL? + +从默认定义上来讲,执行超过 10s 的 SQL 都被定义为慢 SQL。不过对于互联网应用来讲,10s 的时间标准过于宽松,如果是比较热门的应用 10s 都不能返回结果,基本可以定义为事故了,所以很多企业都会修改这个配置。先来看下怎么查看慢查询配置的时间,如下命令示意,你可以看到默认的配置是 10s。 + +mysql> show variables like 'long_query_time'; + ++-----------------+-----------+ + +| Variable_name | Value | + ++-----------------+-----------+ + +| long_query_time | 10.000000 | + + +如果需要修改该配置为 1s,可以在 my.cnf 中添加,这样的方式需要重启 MySQL 服务。 + +long_query_time=1 + + +(2)如何获取慢 SQL? + +你在分析慢 SQL 之前首先需要获取慢 SQL,如何获取慢 SQL 呢,其中的一种方式是在 my.cnf 中配置,如下示意: + +slow_query_log=1 + +slow_query_log_file=/data/mysql-slow.log + + +你就可以将慢 SQL 写入相应的日志文件内。除了这个方法,在测试过程中,我也会使用 show full processlist 这个命令实时获取交互的 SQL,通过观察 state 状态以及 SQL 出现的频率也能判断出来是不是慢 SQL。 + +(3)如何分析慢 SQL? + +关于慢 SQL,绝大多数原因都是 SQL 本身的问题,比如写的业务 SQL 不合理,返回了大量数据;表设计不合理需要多表的连接查询;索引的问题等。在我的经验当中,众多 SQL 问题中索引相关的问题也是最突出的,在我看来索引的相关问题有以下几种。 + +索引缺失 + +首先来看看什么是索引,索引是一种单独地、物理地对数据库表中一列或者多列进行排序的数据库结构。索引的作用相当于图书的目录,可以根据目录的页码快速找到所需要的内容。当数据库存在大量数据做查询操作,你就需要 check 是否存在索引,如果没有索引,会非常影响查询速度。 + +在 InnoDB 中,我们可以简单地把索引分成两种:聚簇索引(主键)和普通索引。按照我的理解来看,聚簇索引是叶子节点保存了数据,而普通索引的叶子节点保存的是数据地址。 + +通常推荐在区分度较高的字段上创建索引,这样效果比较好,比如,一个会员系统中,给用户名建索引,查询时候可以快速定位到要找的数据,而给性别字段建索引则没有意义。 + +索引失效 + +添加索引只是其中的一个必要步骤,并不是添加完成后就万事大吉了。在一些情况下索引其实是不生效的,比如索引列中存在 Null 值、重复数据较多的列、前导模糊查询不能利用索引(like ‘%XX’ 或者 like ‘%XX%‘)等。在一般情况下你可以使用执行计划查看索引是否真正生效,在下一讲中,我也会用更多的实例带你看这个问题。 + +联合索引不满足最左前缀原则 + +又来新概念了,有两个问题: + + +什么是联合索引; +什么又是最左前缀。 + + +首先来解释下联合索引,用大白话解释就是一个索引会同时对应多个列,比如 c1、c2、c3 为三个字段,则可以通过 index_name(c1,c2,c3) 的方式建立联合索引,这样做的好处是什么呢?通过这样的方式建立索引,等于为 c1、(c1,c2)、(c1,c2,c3) 都建立了索引。因为每增加一个索引,也会增加写操作的磁盘开销,所以说联合索引是一种性价比比较高的建立索引的方式。 + +那么什么是最左前缀原则呢?你刚刚在 c1、c2、c3 上建立了联合索引,索引中的数据也是按 c1、c2、c3 进行排序,最左前缀顾名思义就是最左边的优先,比如如下 SQL 命令: + +SELECT * FROM table WHERE c1="1" AND c2="2" AND c3="3" + + +这条 SQL 就会按照从左到右的匹配规则,如果打破最左前缀规则联合索引是不生效的,如下写法所示: + +SELECT * FROM table WHERE c1="1" AND c3="3" + + +那如何判断 SQL 有没有走索引或者索引有没有生效呢?接下来我们要了解一个新概念叫作执行计划,什么是执行计划呢? + +执行计划通常是开发者拿到慢 SQL 之后,优化 SQL 语句的第一步。MySQL 在解析 SQL 语句时,会生成多套执行方案,然后内部会进行一个成本的计算,通过优化器选择一个最优的方案执行,然后根据这个方案会生成一个执行计划。开发者通过查看 SQL 语句的执行计划,可以直观地了解到 MySQL 是如何解析执行这条 SQL 语句的,然后再针对性地进行优化。 + +(4)如何查看 SQL 语句的执行计划? + +我们可以在执行 SQL 的前面添加 desc,如下所示: + +desc select * from user + + +或者添加 explain,如下所示: + +mysql> explain select * from user; + ++----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+ + +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ++----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+ + +| 1 | SIMPLE | user | NULL | ALL | NULL | NULL | NULL | NULL | 9984 | 100.00 | NULL | + ++----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+ + +1 row in set, 1 warning (0.01 sec) + + +对于 explain 返回的内容我选择一些重点解释一下,尤其是对性能产生不利的表现内容。 + +table + +table 显示的是这一行的数据是关于哪张表的,上述内容中显示的表名就是 user。 + +type + +这是重要的列,显示连接使用了何种类型,类型还是蛮多的,我选择最不理想的 ALL 类型和你解释一下,这个连接类型对于查询的表进行全表数据扫描,这种情况比较糟糕,应该尽量避免,上面的示例就进行了全表扫描。 + +key + +key 表示实际使用的索引。如果为 Null,则没有使用索引,这种情况也是尤其需要注意的。 + +rows + +rows 表明 SQL 返回请求数据的行数,这一行非常重要,返回的内容中 SQL 遍历了 9984 行,其实也证明了这条 SQL 遍历了一张表。 + +extra + +关于 extra,我列举两个你需要注意的状态,因为这样的状态是会对性能产生不良的影响,意味着查询需要优化了。 + +Using filesort:表示****SQL 需要进行额外的步骤来发现如何对返回的行排序。它会根据连接类型、存储排序键值和匹配条件的全部行进行排序。 + +Using temporary:表示****MySQL 需要创建一个临时表来存储结果,非常消耗性能。 + +总结 + +本讲相对系统地讲述了常见的 MySQL 数据库性能影响点,你可以从一个全局的角度去思考诊断 MySQL 性能问题的步骤,同时我也讲了执行计划,通过执行计划可以发现 SQL 性能问题产生的原因,这是一个非常实用的手段。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/19如何根治慢SQL?.md b/专栏/说透性能测试/19如何根治慢SQL?.md new file mode 100644 index 0000000..afc85bf --- /dev/null +++ b/专栏/说透性能测试/19如何根治慢SQL?.md @@ -0,0 +1,367 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 19 如何根治慢 SQL? + 上节课带你学习了 MySQL 优化的整体思路,我们将优化策略逐渐进到了索引层面,性能优化其实也是这样,一般大处着眼,小处着手。这节课我将从更多的实例出发诊断 SQL 相关的问题,你可以认为是第 18 讲的补充和进阶。 + +show full processlist + +上一讲已经提到过在你诊断 SQL 之前,首先要知道的是如何获取这些有问题的 SQL,一般有两种方式可以获取: + + +从慢日志文件中获取,上一讲也描述过配置方法; +通过 show full processlist 实时获取交互的 SQL。 + + +有同学留言说不知道具体应该如何使用 show full processlist,所以这里我演示下该命令的具体用法。show full processlist 可以显示哪些 SQL 线程正在运行,也可以在 MySQL 交互行下直接运行,来看下这个命令会给你展现哪些信息。 + +mysql> show full processlist; + ++--------+---------+---------------------+----------------+---------+------+----------+-----------------------+ + +| Id | User | Host | db | Command | Time | State | Info | + ++--------+---------+---------------------+----------------+---------+------+----------+-----------------------+ + +| 121553 | root | localhost | mall | Sleep | 48 | | NULL | + +| 139421 | netdata | localhost | NULL | Sleep | 1 | | NULL | + +| 140236 | root | localhost | cctester | Sleep | 1778 | | NULL + + +我们来解释下信息中每列的含义。 + + +ID:作为一个标识 ID,如果你打算 kill 一个 SQL,可以根据 ID 来进行。 +User:当前正在执行 SQL 的用户,只显示你登录账号权限范围内能够看到的用户。 +Host:显示这个语句是从哪个 ID 和端口上发出的。 +db:当前线程使用的库名。 +Command:连接执行的命令状态,一般是 Sleep、Query、Connect 等。 +Time:状态持续的时间(单位是秒)。 +State:显示当前 SQL 语句的状态,这是一个非常重要的判断标识,比如多次刷新命令时,发现 SQL 常处于 Sending data,那么这条 SQL 大概率是存在问题的。 +Info:显示正在执行的 SQL 语句,这也是你能直接拿到慢 SQL 的方式。 + + +实例对比:索引对性能的影响 + +关于索引的基本作用通过《18 | 如何才能优化 MySQL 性能?》的内容相信你已经了解,而在性能层面更直观的影响,我想通过对比实验结果也许会更清晰一点。 + +我们新建了一个 user 表,表结构如下: + +mysql> desc user; + ++-------------+--------------+------+-----+---------+-------+ + +| Field | Type | Null | Key | Default | Extra | + ++-------------+--------------+------+-----+---------+-------+ + +| id | int(11) | NO | | NULL | | + +| Name | varchar(18) | YES | | NULL | | + +| password | varchar(20) | YES | | NULL | | + +| description | varchar(100) | YES | | NULL | | + ++-------------+--------------+------+-----+---------+-------+ + + +通过查看表信息,你可以发现我并没有添加索引,接着我使用 10 个线程测试一条SQL,其中SQL内容是通过 ID 号来查看数据,性能结果表现如下: + + + +在 ID 列添加索引后继续基于同一条 SQL ,进行 10 线程压测,结果数据如下: + + + +从测试结果来看,在未添加索引的情况下,TPS 是 109.4,而添加逐渐索引后,同等压力下 TPS 达到了 240.2。 + +CPU 资源占用如下所示: + + + +CPU 使用率图 + + +红线:Total +黄线:User +蓝色:Iowait +绿线:System + + +在未添加索引的情况下,在 18:05 之前有 CPU 使用飙高,在添加索引后我在 18:10 基于同一场景测试,你会发现服务端资源使用率较低,而且 TPS 还翻了一倍以上。 + +通过这样一段对比,相信你能非常直观地感觉到索引带来的性能差别。 + +那我们是不是添加了索引就万事大吉呢,其实不是这样的,索引也有效率之分,也会存在索引失效等情况,接下来我就结合上一节课讲的执行计划来判断索引使用是否合理。有了执行计划我认为绝大多数 SQL 的问题你都可以找到优化的方向,而且对于我来说执行计划带来的直接好处是并不需要进行专门的性能测试就可以提前发现慢 SQL。 + +继续通过执行计划来看索引的使用技巧 + +1.索引覆盖 + +什么是索引覆盖? + +走索引查询数据时,如果该索引已经包含需要的数据,则称之为索引覆盖。若索引中不能拿到想要的数据,则需要通过主键拿一整行数据,这个过程叫回表,需要一次 IO 操作,所以我们写 SQL 时尽量使用索引覆盖,降低 IO 开销。 + +如何判断是否是索引覆盖? + +通过 extra 判断是否显示 Using index,如下示例所示: + +mysql> show create table cctester \G; + +*************************** 1. row *************************** + + Table: cctester + +Create Table: CREATE TABLE `cctester` ( + + `id` int(11) NOT NULL, + + `name` varchar(11) DEFAULT NULL, + + `mobile` bigint(20) DEFAULT NULL, + + `score` int(11) DEFAULT NULL, + + `subject` varchar(20) DEFAULT NULL, + + PRIMARY KEY (`id`), + + KEY `idx_name` (`name`), + + KEY `idx_subject` (`subject`) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + +1 row in set (0.21 sec) + +//上述这是表结构 + +mysql> desc select name from cctester where name ="cc"; + ++----+-------------+----------+------------+------+---------------+----------+---------+-------+------+----------+-------------+ + +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ++----+-------------+----------+------------+------+---------------+----------+---------+-------+------+----------+-------------+ + +| 1 | SIMPLE | cctester | NULL | ref | idx_name | idx_name | 47 | const | 1 | 100.00 | Using index | + ++----+-------------+----------+------------+------+---------------+----------+---------+-------+------+----------+-------------+ + +1 row in set, 1 warning (0.01 sec) + +//上述是第一段执行计划 + +mysql> desc select name,score from cctester where name ="cc"; + ++----+-------------+----------+------------+------+---------------+----------+---------+-------+------+----------+-------+ + +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ++----+-------------+----------+------------+------+---------------+----------+---------+-------+------+----------+-------+ + +| 1 | SIMPLE | cctester | NULL | ref | idx_name | idx_name | 47 | const | 1 | 100.00 | NULL | + ++----+-------------+----------+------------+------+---------------+----------+---------+-------+------+----------+-------+ + +1 row in set, 1 warning (0.00 sec) + +//上述是第二段执行计划 + + +首先看下 cctester 表的结构,再观察下面两个 SQL 的执行计划对比,第一个 SQL 走 name 字段,只拿 name 字段内容,第一段执行计划显示了 Using index,说明索引覆盖了;而第二个 SQL 需要额外取 mobile 字段,所以需要回表,你也可以发现第二段执行计划 Extra 列返回的 NULL,所以没有用到索引覆盖,这些细小的差别都可以通过执行计划捕捉到。 + +2.联合索引 + +联合索引就是多个字段组成联合索引,在上一讲我们也讲过基本的作用和最左前缀规则。不过我发现一个误区,同样还是这样的一个示例 SQL,索引规则同样是 index_name(c1,c2,c3),下面这样的示例一定是符合最左前缀规则的: + +SELECT * FROM table WHERE c1="1" AND c2="2" AND c3="3" + + +那么,我改变下 SQL 的查询顺序,如下所示: + +SELECT * FROM table WHERE c2="2" AND c3="3" AND c1="1" + + +请问这样还满足最左前缀规则吗?对于 AND 这样的情况,可能很多同学觉得这个顺序和索引列不一致,应该是不满足最左前缀了,事实上不是这样的。 + +不管你是使用 (c1,c2,c3) 或者是 (c2,c3,c1),都是使用了联合索引,虽然表面上 (c2,c3,c1) 不符合最左前缀规则,但是 MySQL 本身是有查询优化器,它会确定这条 SQL 根据联合索引的字段顺序,最后再确定执行计划。所以说在查询字段满足条件的情况下字段顺序查询优化器是可以帮助你“纠正”的,在你项目实操过程中,对最左前缀的理解不要只局限于字面,如果你不确定可以通过执行计划来判断。 + +上面我举的例子是 SQL 中查询条件进行 AND 连接,看上去比较简单,我再讲一个联合索引的常用场景,看下面这样一个案例: + +mysql> desc select name,subject,score from cctester where subject = 'english' order by score; + ++----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+ + +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ++----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+ + +| 1 | SIMPLE | cctester | NULL | ALL | idx_subject | NULL | NULL | NULL | 6 | 50.00 | Using where; Using filesort | + ++----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-----------------------------+ + +1 row in set, 1 warning (0.02 sec) + +//以上是第一段执行计划 + +mysql> alter table cctester add index idx_subject_score_name(subject,score,name); + +Query OK, 0 rows affected (0.15 sec) + +//以上添加联合索引 + +mysql> desc select name,subject,score from cctester where subject = 'english' order by score; + ++----+-------------+----------+------------+------+------------------------------------+------------------------+---------+-------+------+----------+--------------------------+ + +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ++----+-------------+----------+------------+------+------------------------------------+------------------------+---------+-------+------+----------+--------------------------+ + +| 1 | SIMPLE | cctester | NULL | ref | idx_subject,idx_subject_score_name | idx_subject_score_name | 83 | const | 3 | 100.00 | Using where; Using index | + ++----+-------------+----------+------------+------+------------------------------------+------------------------+---------+-------+------+----------+--------------------------+ + +//以上是第二段执行计划 + + +这里还是基于 cctester 的表结构,根据 where 条件 subject 查询之后再根据 score 排序,第一段执行计划可以看到 SQL 没有用到索引且需要额外的排序,而第二段执行计划中 SQL 使用了联合索引且不用再排序。在原理上解释就是,(subject,score,name) 索引中根据 subject 定位到的数据已经根据 score 排好顺序了,不需要再排序,这种 order by 的场景是联合索引使用最经典的案例。 + +3.索引失效的场景 + +索引虽好,但不代表你建了这条索引就一定会被使用,下面我列举了常用的索引失效的情况,也是日常工作中常见的一些情况。 + +隐式类型转换 + +表结构中类型是 varchar,SQL 中用的 int,这是开发最常忽略的问题,如下示例所示: + +(root@localhost) [t]> show create table t\G + +*************************** 1. row *************************** + + Table: t + +Create Table: CREATE TABLE `t` ( + + `id` int(11) NOT NULL, + + `name` varchar(11) DEFAULT NULL, + + `score` varchar(11) DEFAULT NULL, + + PRIMARY KEY (`id`), + + KEY `idx_name` (`name`), + + KEY `idx_score` (`score`) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + +1 row in set (0.00 sec) + +//以上是建表语句 + +(root@localhost) [t]> desc select * from t where socre = '99'; + ++----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+ + +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ++----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+ + +| 1 | SIMPLE | t | NULL | ref | idx_score | idx_score | 47 | const | 2 | 100.00 | NULL | + ++----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+ + +1 row in set, 1 warning (0.00 sec) + +(root@localhost) [t]> desc select * from t where socre = 99; + ++----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ + +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ++----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ + +| 1 | SIMPLE | t | NULL | ALL | idx_score | NULL | NULL | NULL | 4 | 25.00 | Using where | + ++----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ + +1 row in set, 3 warnings (0.00 sec) + + +观察上面例子可以发现,score 字段是 varchar 类型,当 SQL 中忘写单引号则走不到索引,接下来我继续讲解实例,我举的例子你不用过多考虑业务特性,单纯看索引问题即可。 + +模糊匹配开头 + +由于 MySQL 最左匹配原则,所以查询条件模糊开头无法命中索引,如下所示: + +(root@localhost) [t]> desc select * from t where score like '%9'; + ++----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ + +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ++----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ + +| 1 | SIMPLE | t | NULL | ALL | NULL | NULL | NULL | NULL | 4 | 25.00 | Using where | + ++----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ + +1 row in set, 1 warning (0.00 sec) + + +通过执行计划你会发现上面的情况并没有命中索引。 + +or 不同条件 + +从上面建表结构中我们可以看到 name 字段和 score 字段都有索引,但直接写 or 查询两个字段无法使用索引,这种场景,我们可以将 or 改写成 union 即可。通过如下实例的第一段和第二段执行计划中涉及的索引项就可以看出。 + +(root@localhost) [t]> desc select * from t where name = 'allen' or score = '456'; + ++----+-------------+-------+------------+------+---------------------+------+---------+------+------+----------+-------------+ + +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ++----+-------------+-------+------------+------+---------------------+------+---------+------+------+----------+-------------+ + +| 1 | SIMPLE | t | NULL | ALL | idx_name,idx_score | NULL | NULL | NULL | 4 | 43.75 | Using where | + ++----+-------------+-------+------------+------+---------------------+------+---------+------+------+----------+-------------+ + +1 row in set, 1 warning (0.00 sec) + +//第一段执行计划 + +(root@localhost) [t]> desc select * from t where name = 'allen' union all select * from t where score = '456'; + ++----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+ + +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | + ++----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+ + +| 1 | PRIMARY | t | NULL | ref | idx_name | idx_name | 47 | const | 1 | 100.00 | NULL | + +| 2 | UNION | t | NULL | ref | idx_score | idx_score | 47 | const | 1 | 100.00 | NULL | + ++----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+ + +2 rows in set, 1 warning (0.00 sec) + +//第二段执行计划 + + +总结 + +本讲从实际工作出发,以慢 SQL 中问题的重灾区索引为切入点,以执行计划为手段诊断了索引的常见问题,这些都是 SQL 优化中最常见的知识点,通过实例可以让你明白这些优化带来的好处。 + + + + \ No newline at end of file diff --git a/专栏/说透性能测试/20结束语线上全链路性能测试实践总结.md b/专栏/说透性能测试/20结束语线上全链路性能测试实践总结.md new file mode 100644 index 0000000..dc82c85 --- /dev/null +++ b/专栏/说透性能测试/20结束语线上全链路性能测试实践总结.md @@ -0,0 +1,121 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 20 结束语 线上全链路性能测试实践总结 + 作为专栏的最后一篇,我想和你聊聊线上全链路性能测试。全链路性能测试是一个非常热门的话题,不少公司也在摸索实践,这篇就以我的经验来谈谈对线上全链路性能测试的认知和实践的总结。 + +线上全链路性能测试提出的背景 + +按照我的认知,线上全链路性能测试是由阿里巴巴在 2012 年双 11 之后首次提出来的,因为当年的双 11 用户访问高峰时期系统成功率只有 50%,对于阿里同学来说,那应该是心情很沉重的双 11,复盘后发现其中的一块网卡被打满,在线下的性能测试环境中并没有发现这个问题。 + +为什么呢,说白了线下的压测理论上都是“缩容”测试,什么是缩容呢,就是服务器数量或者硬件配置是远低于线上的,规模容量是缩小了很多的。这导致了一些问题并不能充分被发现,实际访问量也不能很好被预估。在这样的问题背景下,线上的全链路性能测试就被提出来了。其实这和我开篇的一个观点是一致的,性能测试体系能够发展迅速,往往是公司各种大促的血泪史催化出来的,风险控制往往也能够带来最直接的生产力。 + +什么是线上全链路性能测试? + +线上全链路性能测试就是基于真实的业务场景,实际线上环境,模拟多线程对各个业务链路进行压力测试的过程。 + +很多人提出了一个问题,有了线上全链路测试还需要线下性能测试吗?关于这个问题我也回答过,线上线下的性能测试理论上应当是互补的关系,线下往往权限充足而且不会对生产环境产生影响,可以发现更多的基本性能问题以及各类异常性能场景的测试,而线上是最真实的环境,对于性能验收的准确性至关重要。 + +什么样的情况适合全链路性能测试? + +我并不认为所有的公司都适合直接进行线上性能测试,罗马不是一天建成,性能测试成熟的公司基本都经历了一段血泪史,是以一定的代价换的,对于线上全链路的开展我觉得需要几个前提: + +1.已经在性能环境下进行过性能测试,但线上依然存在性能事故; + +2.有频繁的促销需求,每次大促业务规则变化多; + +3.对硬件成本较为敏感,需要进行容量测试,验证扩容缩容后的系统处理能力。 + +线上全链路性能测试聚焦的目的 + +相信你已经在线下进行过多轮的测试验证,线上的性能测试绝不是让你再去一遍遍执行发现基础的性能问题,线上性能测试的核心目的是验证系统的容量、部署结构的优化空间、整体线上的稳定性。 + +线上全链路性能测试实施重要前提 + +1.评估线上全链路压测的可行性 + +最核心要思考的点是能不能做到压测数据的隔离,所谓数据隔离你应该能够想到的就是数据库中压测数据不能跟真实的数据混在一起,从而无法区分,比如用户数据、下单商品数据等。通用的做法是先对数据进行打标,打标的意思就是做标记,比如压测数据都会有一个能够区别于真实业务的标识,比如加 header,比如用户名基于相同的 test 开头的标记,一般对于数据隔离的方案有两种。 + +(1)标记清理 + +第一种做法就是把标记的数据进行删除,一般在压测完成之后根据标识统一清理数据,所以做标记是一个重要前提,还有个前提是你需要整理清楚涉及哪些库表,有没有依赖关系,并且做到和开发以及 DBA 确认,这样删除数据更安全。 + +(2)影子库表 + +什么是影子库表呢?简单来说就是做一个和生产一样的库表,然后把压测数据加入影子库表,这样的做法相对于清理数据的方式,后期的事情会少一点,并不需要每次都一定要清理数据。这样的做法也有一些注意点,影子库表并不是仅仅建一个空的库表,它也需要你将生产数据迁移,还有这样的做法前期工作量会大一点,需要进行中间件改造自动识别压测流量才能进入影子库表。 + +这两种方式各有千秋,可以根据公司的业务特性做具体的方案落地。 + +2.对业务场景梳理 + +理清核心链路流程,线上有很多的接口,所谓线上的全链路性能测试也不会把所有的接口纳入测试范围当中。我的做法是先理清楚核心的业务模块,你必须要对你的业务模块有最基本的感知,哪些是核心模块,哪些是非核心,下面我罗列一个电商基本的业务模块图。 + + + +你可以看到仅文字描述的功能就已经非常多了,如果再去细分接口,可能一张图表都放不下。你首先要考虑的就是在这些模块中,哪些是不用进行全链路性能测试的,比如类似于商家管理系统,这本身是后台系统,并不直接面向用户。同样在你确定需要测试的性能模块中进一步确定哪些接口需要进行性能测试,关于细分到接口的选择我一般将大促时的访问数据进行参考,具体的方法[《07 | 你真的知道如何制定性能测试的目标吗?》]也有所提及。 + +3.理清数据传输链路 + +什么是数据传输链路呢?可以想象一下你在线下压测的场景,是不是内网环境从你的压测机就直接到服务暴露出的访问接口。而线上的链路可能远比这个复杂得多,比如数据是不是首先经过防火墙、硬件层的负载均衡等,你需要把这些链路也理清楚,说白了就是要理清线上的部署架构。这也是性能测试需求分析的步骤,防火墙之类的底层结构是不会轻易变动的,但对于线上实际的应用部署节点可能变动就很多了,我们会针对不同的应用节点部署结构和数量经常调整,所以在压测之前需要前置好这些条件并记录清楚。 + +线上全链路性能测试实施要点 + +首先需要表明一些观点,很多测试人员在实际测试过程中要么是自己造数据构造场景,如果遇到一些需要开发配合的可能会被直接打回,在线上的全链路性能测试过程中可以说系统改造是必需的,线上全链路性能测试也是多团队协作完成的,并不是测试人员的“独角戏”。 + +很多测试同学在进行线下性能测试时,几乎是一个人完成所有的活动,包括编写脚本、压测、监控、分析等等。在开展线上全链路的时候,可能会把这一套也继续沿用,这其实是一个误区,我认为线上的性能测试相对于线下至少有三个特性。 + +1.时间敏感 + +怎么理解时间敏感呢?线上的性能测试一般都需要申请窗口期,并不是你个人决定压测就可以直接进行了,一般来说,绝大部分公司都是选择在凌晨进行,这样对实际用户的影响最小。就以测试同学来举例,线上的性能测试也有很多准备工作,测试之前会有很多试验性的工作,比如商品数据是否有效、用户 token 是否过期。一旦进行正式的压测环节,一般需要按照时间去分解任务,遇到问题讲究快速解决,需要最正确的人去做相关的任务协同,比如哪些服务集群、哪些中间件需要哪些人去监控,在有效的时间内以达到最高效率。 + +2.权限敏感 + +权限敏感很好理解,说白了你作为测试不可能像在性能测试环境一样具备充足的权限,比如可以在性能测试环境随时重启服务,增删改查数据等。作为测试在线上你不可能有这些权限,所以线上的性能测试并不是你可以自由发挥的。 + +3.高风险 + +说到高风险,通过其他两个特性也能看出来,如果风险不大,时间和权限自然也就没这么敏感了,所以基于风险较高,我们在线上全链路性能测试执行的时候,更希望有一个“项目经理”的角色,进行人员安排、任务同步、风险预警等,这样的角色需要深入了解性能流程以及系统的风险控制。 + +测试人员可以扮演的角色? + +1.全链路性能测试执行者 + +作为一名测试执行者,你需要更关注测试本身的内容,那测试本身的内容有哪些呢?我以电商网站为例来说明,根据每家公司的业务或者体量的不同,具体的事物会有差别,但一些思路是可以借鉴的: + + +压测机环境准备,压测机分布式节点检查; +压测数据准备和检查,如会员数据、商品下单数据; +参数化文件切割; +脚本以及参数化文件分发; +测试场景执行以及指标收集和记载。 + + +2.线上全链路测试的“项目经理” + +我刚刚也说了线上的性能测试需要一名“项目经理”,很多人思维定式认为这位项目经理需要一定级别的 leader 来担任,其实并不完全是这样。以我的思路来看,性能测试一线经验丰富的同学来承担更适合,当然这位同学不仅仅需要性能经验丰富,也需要对架构以及业务有基本盘的认知,至少做到心中有底。 + +如果作为一个线上全链路压测实施的“项目经理”,你需要组织什么事情呢?我用一个思维导图罗列下。 + + + +线上全链路测试的风险 + +对于很多公司还是很忌惮在线上直接开始性能测试,因为觉得这样做风险太大了。这个担心是有道理的,阿里巴巴在刚刚提出这个议题时,内部也有很多不同的声音,一旦性能测试导致线上系统出现故障,轻则影响用户体验,重则系统不可用、真金白银的损失,任何人都不想为这个测试风险而买单。 + +虽然文章中列举了很多事项和注意点,但真正实施起来还是可能会遇到问题,所以线上性能测试的手段也是需要被测试的,比如先基于单条数据的打标、隔离、清理等,而且更需要的是发散性思维去思考,能不能把相关问题都考虑全面了,比如我之前的举例,对数据库进行数据隔离,那你有没有考虑到如 ES 数据、日志数据是否也需要做到有效的隔离呢? + +监控预警 + +在线上全链路压测过程中,报警是非常重要的,出现意外情况可以及时终止压测,防止造成额外损失。报警的规则也是需要严格制定的,硬件的、系统的、业务的,包括响应延时等都应当设置相关的阈值等,第三模块有非常详细的描述,相信你应该对监控已经比较熟悉了。 + +总结 + +线上的全链路性能测试并不是高不可攀的技术,虽然有一定的技术门槛,但真正运作起来其实考验的是组织能力,线上全链路性能测试我认为是在线下专项性能测试上做的升级。这篇也是本专栏最终的收尾了,希望你能有所收获。 + +虽然本专栏已经结束了,但是后续我仍然会在微信公众号(软件测试架构师俱乐部)发表更多的知识,欢迎你来关注。 + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/00开篇词带给你不一样的运维思考.md b/专栏/赵成的运维体系管理课/00开篇词带给你不一样的运维思考.md new file mode 100644 index 0000000..fa4077e --- /dev/null +++ b/专栏/赵成的运维体系管理课/00开篇词带给你不一样的运维思考.md @@ -0,0 +1,54 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 00 开篇词 带给你不一样的运维思考 + 你好,我是赵成,来自蘑菇街。 + +大概在9月份的时候,我接到了极客时间团队的邀请,看是否可以做一个运维专栏,当时第一反应是略感兴奋,这还真是个意外的邀请。但是接来下我的反应就是诚惶诚恐,因为我自己写公众号也有一段时间了,深知持续高质量输出这个事情的挑战之大,特别是在输出专业文章上更是如此,所以当时一直拿不定主意。 + +我写公众号文章,很大程度是因为之前有过很多次公开演讲和分享,后来发现演讲所面向的受众和时间有限,分享的内容无论是在沉淀、传播以及深度上,都会受到很大的限制。总之,讲得不过瘾,索性就把一些我觉得还值得更深入探讨的话题和内容完完整整地写出来。 + +后来,在上海跟极客时间团队见面之后,他们给了我一些建议,因为之前的文章更多是针对一个个观点延伸出去写作,而专栏文章可以尝试更系统地输出,能够把一个运维体系讲透。这个建议从一定程度上打开了我写专栏的思路,后来我把内容规划了一下,感觉还是可以输出一些更有价值的内容,就启动了这个专栏的策划。 + +谈谈运维的价值 + +我们在大学的软件工程中就学过,从软件生命周期的角度看,软件开发阶段只占整个生命周期的20%~30%左右,软件运行维护阶段是最长尾的,这条规律放在当前这个时代同样适用。 + +在软件生命周期中,我们可以很清晰地划分出“开发阶段”和“运维阶段”,这个分界点就从开发完成代码开发,测试验收通过后,交付到运维手上的软件包开始,自此之后的阶段就是软件的运行维护阶段了。 + +一个公司对于开发的诉求应该是全力实现业务需求,并将需求尽快发布上线以实现商业上的收益。但是,在一个公司里,除了专注于业务需求的开发和测试角色外,还会有另外一大类开发,比如我们常见的中间件开发、稳定性开发、工具开发、监控开发、IaaS或PaaS平台开发,甚至专注于底层基础架构的内核开发、网络开发、协议开发等等。 + +这里请你跟我一起仔细思考下,我们会发现除了业务开发和测试外,前面所提到的那些技术岗位都是为软件生命周期中的运行维护阶段服务的,这些角色的作用就是提升研发效率和稳定性,进而降低成本。虽然他们并没有全部被定义为运维岗位,但是本质上他们是跟业务软件的运行维护阶段直接相关的。 + +所以,从运维的范畴上来讲,我认为,一个研发团队内,除去业务需求实现层面的事情,其它都是运维的范畴,这个范畴内的事情本质上都是在为软件生命周期中的运行维护阶段服务。 + +我之前在外部分享,一直表达的一个观点就是,运维能力是整体技术架构能力的体现,运维层面爆发的问题或故障,一定是整体技术架构中存在问题,割裂两者,单纯地看技术架构或运维都是毫无意义的。 + +但是,我们在绝大多数情况下,忽略了这个隐藏在软件生命周期中真正的运维范畴,而是简单直接地从软件生命周期分段的角度,生硬地给开发和运维划定了一条界限。 + +也正是这样一个简单直接的界限划定,让我们将运维仅仅局限在了服务器维护、网络设备配置、软件安装维护这些最末端的职责上,而我们又期望运维这个角色能够掌控全局,不要在这个阶段出现任何问题。这就很像临渴而掘井,是不现实的。 + +很显然,我认为,运维思路上的转变,远比单纯提升运维技术更有价值,而运维真正的价值应该跟研发团队保持一致,真正聚焦到效率、稳定和成本上来。 + +而这也正是我们很多公司和团队,当前所遇到的最大的痛点和问题。所以,在我的专栏里,我会针对这些痛点和问题分享一些我的思考。 + +专栏内容 + +我的专栏内容,会聚焦在分布式软件架构下的应用运维这个领域,更多的是我对运维的一些架构思考,主要有以下四个部分。 + + +应用运维体系建设。这是我们做运维的基础,我会分享从标准化和应用生命周期开始,如何一步步建立运维技术体系和组织架构,以及整个过程中的沟通协作等方面;分享我们应该如何树立正确的运维建设思路。 +效率和稳定性等方面的最佳实践。这些是运维价值的体现,我会围绕持续交付和稳定性建设两个方面,分享如何打造不需要任何运维参与的端到端交付过程,如何在实践中锤炼出稳定性保障体系。 +云计算方面的思考和实践。云计算技术的蓬勃发展为我们的业务和技术提供了更多的可能性,利用好云这个平台将会是运维升级转型的必备要求。我会分享在混合云、云存储、静态化以及CDN上的实践经验,以及这些实践所带来的在成本和体验上的巨大收益。 +个人成长与趋势热点分析。这一部分更多的会是我个人的一些思考,包括运维技术发展趋势、团队管理、个人成长、热门事件解析、观点碰撞等。 + + +希望在这个专栏里,能够跟你有更多的互动和交流,希望我们在观点碰撞中共同进步。 + +最后,开卷有益,期望能够带给你不一样的运维思考。 + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/01为什么Netflix没有运维岗位?.md b/专栏/赵成的运维体系管理课/01为什么Netflix没有运维岗位?.md new file mode 100644 index 0000000..3801596 --- /dev/null +++ b/专栏/赵成的运维体系管理课/01为什么Netflix没有运维岗位?.md @@ -0,0 +1,107 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 01 为什么Netflix没有运维岗位? + 众所周知,Netflix是业界微服务架构的最佳实践者,其基于公有云上的微服务架构设计、持续交付、监控、稳定性保障,都为业界提供了大量可遵从的原则和实践经验。 + +Netflix超前提出某些理念并付诸实践,以至于在国内众多互联网公司的技术架构上也可以看到似曾相识的影子。当然殊途同归也好,经验借鉴也罢,这都不影响Netflix业界最佳实践者的地位。 + +同样,在运维这个细分领域,Netflix仍然是最佳实践的典范。所以专栏的开始,就让我们一起看看世界顶级的互联网公司是如何定义运维以及如何开展运维工作的。 + +Netflix运维现状 + +准确一点说,Netflix是没有运维岗位的,和运维对应的岗位其实是我们熟知的SRE(Site Reliability Engineer)。但我们知道SRE≠运维,SRE理念的核心是:用软件工程的方法重新设计和定义运维工作。 + +也就是要改变之前靠人去做运维的方式,转而通过工具体系、团队协作、组织机制和文化氛围等方式去改变,同时将之前处于研发体系最末端的运维,拉回到与开发肩并肩的同一起跑线上。 + +但是Netflix的神奇和强大之处在于,连Google都非常重视和大力发展的SRE岗位,在Netflix却没有几个。按照Netflix一位技术主管(Katharina Probst)在今年9月份更新的博客中所描述,在1亿用户,每天1.2亿播放时长、万级微服务实例的业务体量下,SRE人数竟然不超过10个,他们称这样的角色为Core SRE。描述具体如下: + + +100+ Million members. 125+ Million hours watched per day. Hundreds of +microservices. Hundreds of thousands of instances. Less than 10 Core +SREs. + + +不可否认,Netflix拥有强大的技术实力和全球最优秀的工程师团队。按照SRE的理念,完全可以打造出这一系列的工具产品来取代运维和SRE的工作。但是能够做到如此极致,就不得不让人惊叹和好奇,Netflix到底是怎么做到的? + +为什么Netflix会做得如此极致? + +这确实是一个很有意思的问题,带着这样的疑问,阅读了大量的Netflix技术文章和公开的演讲内容之后,我想这个问题可以从Netflix的技术架构、组织架构、企业文化等几个方面来看。 + +1.海量业务规模下的技术架构和挑战 + +前面我也提到,Netflix在业务高速发展以及超大规模业务体量的驱动下,引入了更为灵活的微服务架构,而且已经成为业界的最佳实践典范。 + +引入微服务架构后,软件架构的细化拆分和灵活多变,大大提升了开发人员的开发效率,继而也就提升了业务需求的响应和迭代速度,我想这也正是微服务思想在业界能够被广泛接受和采用的根本原因。 + +但是这也并不是说没有一点代价和成本,随之而来的便是架构复杂度大大增加,而且这种复杂度已经远远超出人的认知能力,也就是我们已经无法靠人力去掌控了,这也就为后续的交付和线上运维带来了极大的难度和挑战。 + +这时,微服务架构下的运维就必须要靠软件工程思路去打造工具支撑体系来支持了,也就是要求微服务架构既要能够支持业务功能,还要能够提供和暴露更多的在后期交付和线上运维阶段所需的基础维护能力。 + +简单举几个例子,比如服务上下线、路由策略调整、并发数动态调整、功能开关、访问ACL控制、异常熔断和旁路、调用关系和服务质量日志输出等等,要在这些能力之上去建设我们的运维工具和服务平台。 + +讲到这里,我们可以看到,微服务架构模式下,运维已经成为整体技术架构体系中必不可少的一部分,而且与微服务架构相关的体系是紧密相连不可拆分的。 + +我们现在看到很多跟SRE相关的文章或内容,对于SRE产生原因的解释,大多是说为了缓解开发和运维之间的矛盾,树立共同的目标,让双方能够更好地协作配合。这样理解也没有太大的问题,但是我认为不够充分,或者说以上这些只能算是结果,但不是背后的根本原因。 + +我理解的这背后最根本的原因是,微服务架构复杂度到了一定程度,已经远远超出单纯的开发和单纯的运维职责范畴,也远远超出了单纯人力的认知掌控范围,所以必须寻求在此架构之上的更为有效和统一的技术解决方案来解决复杂度认知的问题。进而,在这一套统一的技术解决方案之上,开发和运维产生了新的职责分工和协作方式。 + +目前业界火热的DevOps理念及衍生出来的一系列话题,我们可以仔细思考一下,其实也是同样的背景和逻辑。DevOps想要解决的开发和运维之间日益严重的矛盾,究其根本,还是微服务架构背后带来的技术复杂度在不断提升的问题。 + + +Netflix带给我们的启示一:微服务架构模式下,我们必须换一个思路来重新定义和思考运维,运维一定要与微服务架构本身紧密结合起来。 + + +2.更加合理的组织架构和先进的工具体系及理念 + +我上面提到,在微服务架构模式下,运维已经成为整体技术架构和体系中不可分割的一部分,两者脱节就会带来后续一系列的严重问题。 + +从这一点上,不得不说Netflix的前瞻性和技术架构能力还是很强的。因为早在2012年,甚至更早之前,Netflix就已经意识到了这个问题。在组织架构上,将中间件、SRE、DBA、交付和自动化工具、基础架构等团队都放在统一的云平台工程(Cloud and Platform Engineering)这个大团队下,在产品层面统一规划和建设,从而能够最大程度地发挥组织能力,避免了开发和运维的脱节。 + +当然,这个团队不仅没让大家失望,还给我们带来了太多惊喜。业界大名鼎鼎的NetflixOSS开源产品体系里,绝大部分的产品都是这个团队贡献的。 + +比如持续交付系统Spinnaker;稳定性保障工具体系Chaos Engineering,这里面最著名的就是那只不安分的猴子,也正是这套稳定性理念和产品最大程度地保障了Netflix系统的稳定运行;被Spring Cloud引入并得以更广泛传播的Common Runtime Service&Libraries,而且大家选用Spring Cloud基本就是冲着Spring Cloud Netflix这个子项目去的。当然还有很多其它优秀的开源产品,这里我就不一一介绍了。 + + +Netflix带给我们的启示二:合理的组织架构是保障技术架构落地的必要条件,用技术手段来解决运维过程中遇到的效率和稳定问题才是根本解决方案。 + + +3.自由与责任并存的企业文化 + +上面讲了这么多,看上去好像就是SRE常见的理念和套路,只不过Netflix在开源和分享上更加开放和透明,让我们有机会更多地了解到一些细节。但是为什么Netflix会做的如此极致呢?好像我们一直没有回答到这个问题,那这里就不得不提Netflix的企业文化了。 + +Netflix的企业文化是 Freedom & Responsibility,也就是自由和责任并存,高度自由的同时,也需要员工具备更强的责任心和Owner意识。 + +体现在技术团队中就是,You Build It,You Run It。工程师可以随时向生产环境提交代码或者发布新的服务,但是同时你作为Owner,要对你发布的代码和线上服务的稳定运行负责。 + +在这种文化的驱使下,技术团队自然会考虑从开发设计阶段到交付和线上运维阶段的端到端整体解决方案,而不会是开发就只管需求开发,后期交付和维护应该是一个叫运维的角色去考虑。No,文化使然,在Netflix是绝对不允许这种情况存在的,你是开发,你就是Owner,你就要端到端负责。 + +所以,短短两个英文单词,Freedom & Responsibility,却从源头上就决定了团队和员工的做事方式。 + + +Netflix带给我们的启示三:Owner意识很重要,正确的做事方式需要引导,这就是优秀和极致的距离。 + + +总结 + +通过上面的分析,我们可以看到,Netflix在其技术架构、组织架构和企业文化等方面的独到之处,造就了其优秀的技术理念和最佳实践。从运维的角度来说,无论是SRE也好,还是DevOps也罢,都被Netflix发挥到了极致。 + +当然,Netflix能做到这一点,是需要非常强大的技术实力和人才储备的。当前我们虽然没法直接套用,但是这并不妨碍我们在某些经验和思路上去借鉴和学习。 + +比如,现在很多公司在采用了微服务架构后,就没有充分考虑到后续基于微服务架构的运维问题。而且在运维团队设置上,仍然是脱离整个技术团队,更不用说将其与中间件和架构设计等团队整合拉通去建设,自然也就谈不上在产品层面的合理规划和建设了。 + +因此导致的问题就是运维效率低下,完全靠人工,线上故障频发,但是处理效率又极低,开发和运维都处于非常痛苦的状态之中,运维团队和成员也会遭遇到转型和成长的障碍。 + +以上这些问题都很棘手,亟待解决。 + +通过今天的分享,我们了解了Netflix的技术团队运作模式和思路,不知道给你带来了哪些启示呢? + +如果今天的内容对你有帮助,也请你分享给身边的朋友。 + +欢迎你留言与我一起讨论。 + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/02微服务架构时代,运维体系建设为什么要以应用为核心?.md b/专栏/赵成的运维体系管理课/02微服务架构时代,运维体系建设为什么要以应用为核心?.md new file mode 100644 index 0000000..4f66615 --- /dev/null +++ b/专栏/赵成的运维体系管理课/02微服务架构时代,运维体系建设为什么要以应用为核心?.md @@ -0,0 +1,118 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 02 微服务架构时代,运维体系建设为什么要以应用为核心? + 今天我来讲一下微服务架构模式下的一个核心概念:应用。 + +我会从这几个方面来讲:应用的起源、应用模型和应用关系模型建模以及为什么要这样做。最终希望,在微服务的架构模式下,我们的运维视角一定转到应用这个核心概念上来,一切要从应用的角度来分析和看待问题。 + +应用的起源 + +我们知道,微服务架构一般都是从单体架构或分层架构演进过来的。软件架构服务化的过程,就是我们根据业务模型进行细化的过程,在这个过程中切分出一个个具备不同职责的业务逻辑模块,然后每个微服务模块都会提供相对应业务逻辑的服务化接口。 + +如果解释得简单点,就一个字,拆!如下图,从一个单体工程,拆分出N个独立模块。 + + + +这些模块可以独立部署和运行,并提供对应的业务能力。拆分后的模块数量与业务体量和复杂度相关,少则几个、十几个,多则几十、几百个,所以为了统一概念,我们通常称这些模块为应用。 + +为了确保每个应用的唯一性,我们给每个应用定义一个唯一的标识符,如上图的APP-1、APP-2等,这个唯一标识符我们称之为应用名。 + +接下来,这个定义为应用的概念,将成为我们后续一系列微服务架构管理的核心概念。 + +应用模型及关系模型的建立 + +上面我们定义出来的一个个应用,都是从业务角度入手进行拆分细化出来的业务逻辑单元。它虽然可以独立部署和运行,但是每一个应用都只具备相对单一的业务职能。如果要完成整体的业务流程和目标,就需要和周边其它的服务化应用交互。同时,这个过程中还需要依赖各种与业务无直接关系、相对独立的基础设施和组件,比如机器资源、域名、DB、缓存、消息队列等等。 + +所以,除了应用这个实体之外,还会存在其他各类基础组件实体。同时,在应用运行过程中,还需要不断地与它们产生和建立各种各样复杂的关联关系,这也为我们后续的运维带来很多困难。 + +那接下来,我们要做的就是应用模型以及各种关系模型的梳理和建立,因为只有模型和关系梳理清楚了,才能为我们后面一系列的运维自动化、持续交付以及稳定性保障打下一个良好的基础。 + +1.应用业务模型 + +应用业务模型,也就是每个应用对外提供的业务服务能力,并以API的方式暴露给外部,如下图商品的应用业务模型示例: + + + +这个业务模型通常都是业务架构师在进行业务需求分析和拆解时进行设计,更多的是聚焦在业务逻辑上,所以从运维的角度,我们一般不会关注太多。 + +而接下来的几部分,将是运维要重点关注的内容。 + +2.应用管理模型 + +应用管理模型,也就是应用自身的各种属性,如应用名、应用功能信息、责任人、Git地址、部署结构(代码路径、日志路径以及各类配置文件路径等)、启停方式、健康检测方式等等。这其中,应用名是应用的唯一标识,我们用AppName来表示。 + +这里我们可以把应用想象成一个人,通常一个人会具备身份证号码、姓名、性别、家庭住址、联系方式等等属性,这里身份证号码,就是一个人的唯一标识。 + +3.应用运行时所依赖的基础设施和组件 + + +资源层面:应用运行所必需的资源载体有物理机、虚拟机或容器等,如果对外提供HTTP服务,就需要虚IP和DNS域名服务; +基础组件:这一部分其实就是我们所说的中间件体系,比如应用运行过程中必然要存储和访问数据,这就需要有数据库和数据库中间件;想要更快地访问数据,同时减轻DB的访问压力,就需要缓存;应用之间如果需要数据交互或同步,就需要消息队列;如果进行文件存储和访问,就需要存储系统等等。 + + +从这里我们可以挖掘出一条规律,那就是这些基础设施和组件都是为上层的一个个业务应用所服务的。也正是因为业务和应用上的需求,才开启了它们各自的生命周期。如果脱离了这些业务应用,它们自己并没有单纯存在的意义。所以,从始至终基础设施和组件都跟应用这个概念保持着紧密的联系。 + +理清了这个思路,我们再去梳理它们之间的关系就会顺畅很多,分为两步。 + +第一步,建立各个基础设施和组件的数据模型,同时识别出它们的唯一标识。这个套路跟应用管理模型的梳理类似,以典型的缓存为例,每当我们申请一个缓存空间时,通常会以NameSpace来标识唯一命名,同时这个缓存空间会有空间容量和Partition分区等信息。 + +第二步,也是最关键的一步,就是识别出基础设施及组件可以与应用名AppName建立关联关系的属性,或者在基础组件的数据模型中增加所属应用这样的字段。 + +还是以上面的缓存为例,既然是应用申请的缓存空间,并且是一对一的关联关系,既可以直接将NameSpace字段取值设置为AppName,也可以再增加一个所属应用这样的字段,通过外键关联模式建立起应用与缓存空间的关联关系。 + +相应地,对于消息队列、DB、存储空间等,都可以参考上面这个思路去做。 + +通过上面的梳理,我们就可以建立出类似下图这样的以应用为核心的应用模型和关联关系模型了,基于这个统一的应用概念,系统中原本分散杂乱的信息,最终都被串联了起来,应用也将成为整个运维信息管理及流转的纽带。 + + + +真实的情况是怎么样的? + +上面讲了这么多理论和道理,但是我们业界真实的现状是怎样的呢? + +从我个人实际观察和经历的场景来看,大部分公司在这块的统筹规划是不够的,或者说是不成熟的。也就是软件架构上引入了微服务,但是后续的一系列运维措施和管理手段没跟上,主要还是思路没有转变过来。虽然说要做DevOps,但实际的执行还是把开发和运维分裂了去对待,不信你看下面两个常见的场景。 + + +场景一 + + +这个场景是关于线上的缓存和消息队列的。 + +开发使用的时候就去申请一下,一开始还能记住自己使用了哪些,但是时间一长,或者申请得多了,就记不住了。久而久之,线上就存在一堆无用的NameSpace和Topic,但是集群的维护者又不敢随意清理,因为早就搞不清楚是谁用的,甚至申请人已经离职,以后会不会再用也已经没人讲得清楚了,越往后就越难维护。 + +根本原因,就是前面我们讲到的,太片面地对待基础组件,没有与应用的访问建立起关联关系,没有任何的生命周期管理措施。 + + +场景二 + + +这个典型场景就体现了应用名不统一的问题。 + +按照我们前面讲的,按说应用名应该在架构拆分出一个个独立应用的时候就明确下来,并贯穿整个应用生命周期才对。 + +但是大多数情况下,我们的业务架构师或开发在早期只考虑应用开发,并不会过多地考虑整个应用的生命周期问题,会下意识地默认后面的事情是运维负责的,所以开发期间,只要将应用开发完和将服务注册到服务配置中心上就OK了。 + +而到了运维这里,也只从软件维护的角度,为了便于资源和应用配置的管理,会独立定义一套应用名体系出来,方便自己的管理。 + +这时不统一的问题就出现了,如果持续交付和监控系统这些运维平台也是独立去开发的,脱节问题就会更严重。 + +如下图所示,一个个的孤岛,无法成为体系,当这些系统需要对接时,就会发现需要做大量的应用名转化适配工作,带来非常多无谓的工作量,所谓的效率提升就是一句空话。 + + + +所以,今天一开头我就提到,微服务架构模式下的运维思路一定要转变,一定要将视角转换到应用这个维度,从一开始就要统一规划,从一开始就要将架构、开发和运维的工作拉通了去看,这一点是与传统运维的思路完全不同的。 + +当然,这里面也有一个经验的问题。虽然微服务在国内被大量应用,但是我们绝大多数技术团队的经验还集中在开发设计层面。微服务架构下的运维经验,确实还需要一个总结积累的过程。我自己也是痛苦地经历了上面这些反模式,才总结积累下这些经验教训。 + +这也是为什么我今天分享了这样一个思路,我们要转换视角,规划以应用为核心的运维管理体系。 + +不知道你目前是否也遇到了类似的问题,如果今天的内容对你有帮助,也请你分享给身边的朋友。 + +欢迎你留言与我一起讨论。 + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/03标准化体系建设(上):如何建立应用标准化体系和模型?.md b/专栏/赵成的运维体系管理课/03标准化体系建设(上):如何建立应用标准化体系和模型?.md new file mode 100644 index 0000000..60381c3 --- /dev/null +++ b/专栏/赵成的运维体系管理课/03标准化体系建设(上):如何建立应用标准化体系和模型?.md @@ -0,0 +1,140 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 03 标准化体系建设(上):如何建立应用标准化体系和模型? + 今天我专门来讲讲标准化这个工作。可以说这项工作是运维过程中最基础、最重要的,但也是最容易被忽视的一个环节。 + +我做过多次公开演讲,每次讲到这个环节,通常会有单独的一页PPT,就放四个字,字号加大加粗,重复三遍,这四个字就是“标准先行”,然后演讲过程中会大声说出“标准先行,标准先行,标准先行”,重要的事情说三遍,目的就是想反复强调这件事情的重要程度,一定不要忽视。 + + + +我们运维工作的开展常常不知从何下手,或者上来就冲着工具和自动化去了,却始终不得章法,工具做了一堆,效率却并没有提升。其实绝大多数情况下,问题和原因就是标准化这个基础工作没做扎实。 + +首先,让我们来看看为什么标准化这个事情如此重要。 + +为什么要做标准化? + +标准化的过程实际上就是对运维对象的识别和建模过程。形成统一的对象模型后,各方在统一的认识下展开有效协作,然后针对不同的运维对象,再抽取出它们所对应的运维场景,接下来才是运维场景的自动化实现。 + +这有点像我们学的面向对象编程的思想,其实我们就是需要遵循这样一个思路,我们面对的就是一个个实体和逻辑运维对象。 + +在标准化的过程中,先识别出各个运维对象,然后我们日常做的所有运维工作,都应该是针对这些对象的运维。如果运维操作脱离了对象,那就没有任何意义。同样,没有理清楚对象,运维自然不得章法。 + +比如我们说扩容,那就要先确定这里到底是服务器的扩容,还是应用的扩容,还是其它对象的扩容。你会发现,对象不同,扩容这个场景所实施的动作是完全不一样的。 + +如果把服务器的扩容套用到应用的扩容上去,必然会导致流程错乱。同时对于对象理解上的不一致,也会徒增无谓的沟通成本,造成效率低下。自然地,这种情况下的运维自动化不但不能提升效率,还会越自动越混乱。 + +这就是为什么我每次都会连续强调三遍“标准先行”的原因。虽然这个事情比较枯燥和繁琐,但是于纷繁复杂中抽象出标准规范的东西,是我们后续一系列自动化和稳定性保障的基础。万丈高楼平地起,所以请你一定不要忽略这个工作。 + +好,总结一下标准化的套路: + + +第一步,识别对象; +第二步,识别对象属性; +第三步,识别对象关系; +第四步,识别对象场景。 + + +接下来我们就按照上面这个思路,一起来分析从基础设施层面和应用层面应该识别出哪些运维对象。 + +基础设施层面的标准化 + +基础设施层面的运维对象应该不难识别,因为都是一个个物理存在的实体,我们可以进行如下分析。 + + +第一步,识别实体对象,主要有服务器、网络、IDC、机柜、存储、配件等。 +第二步,识别对象的属性,比如服务器就会有SN序列号、IP地址、厂商、硬件配置(如CPU、内存、硬盘、网卡、PCIE、BIOS)、维保信息等;网络设备如交换机也会有厂商、型号、带宽等信息。 +第三步,识别对象之间的关联关系,比如服务器所在的机柜,虚拟机所在的宿主机、机柜所在IDC等简单关系;复杂一点就会有核心交换机、汇聚交换机、接入交换机以及机柜和服务器之间的级联关系等,这些相对复杂一些,也就是我们常说的网络拓扑关系。 + + +把以上信息梳理清楚,通过ER建模工具进行数据建模,再将以上的信息固化到DB中,一个资源层面的信息管理平台就基本成型了。 + +以服务器为例简单展示一下,我们的视角就是下面这样的: + + + +但是,信息固化不是目的,也没有价值,只有信息动态流转起来才有价值。接下来我们需要做的事情,就是识别出针对运维对象所实施的日常运维操作有哪些,也就是识别出运维场景是什么。 + + +第四步,还是以服务器为例,我们针对服务器的日常操作有采购、入库、安装、配置、上线、下线、维修等等。另外,可能还会有可视化和查询的场景,如拓扑关系的可视化和动态展示,交换机与服务器之间的级联关系、状态(正常or故障)的展示等,这样可以很直观地关注到资源节点的状态。 + + +完成了这些工作,接下来才是对上述运维场景的自动化开发。所以你看,在真正执行去做工具和自动化平台之前,其实是需要先做好大量的基础准备工作的。我要再次强调这一点,一定不能忽视。 + +应用层面的标准化 + +下面我们再一起看一个逻辑上的对象,就是我们前面经常提到的运维的核心:应用。对这个逻辑对象的建模会相对复杂一些,不过我们依然可以按照上面的套路来。 + + +第一步,识别对象。 + + +我们前面讲过,这个识别过程是在做微服务架构设计或拆分的时候就确定下来的。所以严格地讲,它不应该是运维阶段才被识别出来的,而是在之前设计阶段就被识别和确认下来,然后延伸到运维这里才对。 + + +第二步,识别对象属性。 + + +一个应用是业务的抽象逻辑,所以会有业务和运维两个维度的属性。业务属性在业务架构时确定,这主要是需要业务架构师去识别的,但是它的运维属性就应该由运维来识别了。 + +下面我们一起来看一下,一个应用应该具备哪些基本的运维属性。 + +应用的元数据属性,也就是简单直接地描述一个应用的信息,如应用名、应用Owner、所属业务、是否核心链路应用以及应用功能说明等,这里的关键是应用名; + +应用代码属性,主要是编程语言及版本(决定了后续的构建方式),GitLab地址; + +应用部署模式,涉及到基础软件包,如语言包Java、C++、Go等;容器如Tomcat、JBoss等; + +应用目录信息,如运维脚本目录、日志目录、应用包目录、临时目录等; + +应用运行脚本,如启停脚本、健康监测脚本; + +应用运行时的参数配置,如运行端口、Java的JVM参数GC方式、新生代、老生代、永生代的堆内存大小配置等。 + +从应用属性的视角,应该是下面这样一个视图(简单示例,不完整): + + + + +第三步,识别对象关系。 + + +也就是应用与外部的关系,概括起来有三大类: + +第一类是应用与基础设施的关系,包括应用与资源、应用与VIP、应用与DNS等等的关系; + +第二类是平行层面的应用与应用之间的关系,这里再细分下去就是应用服务或API与其它应用服务和API的依赖关系。如果你有相关的经验,应该会联想到全链路这样的工具平台了,没错,这样的平台就是用来处理应用间关系管理的。 + +第三类是应用与各类基础组件之间的关系,比如应用与缓存,应用与消息、应用与DB等等之间的关系。 + + +第四步,识别应用的运维场景。 + + +这个就会比较多了,比如应用创建、持续集成、持续发布、扩容、缩容、监控等;再复杂点的比如容量评估、压测、限流降级等。 + +好,这里我们先收一下,聚焦到标准化的层面,通过基础设施和应用层面标准化的示例,我想你应该可以掌握基本的建模思路了,这样的思路可以应用到其它的运维对象上 。 + +同时,通过上面这些内容,你应该可以比较清晰地看到,我们的每一个运维操作都是针对某个运维对象的,这一点在规划运维体系时非常重要。 + +而在这些对象中,应用又是重中之重,是微服务架构下的核心运维对象。 + +从应用标准化的过程中我们也可以看到,针对应用的识别和建模,明显复杂很多。所以,后面我还会从理论和实践的角度来继续强化和分析这个概念。 + +最后,给你留两个小问题。 + + +标准化部分我们提到,在规划和设计一个运维技术方案时,一定要找到对象主体,那请你思考以下问题:我们现在经常听到一些高大上的词汇,如水平扩展、弹性伸缩和自动化扩缩容等,你能否说一说这些技术手段的主体是谁,也就是是谁的水平扩展?弹性伸缩的是什么?同时,这些名词之间又有什么关系? + +在对象属性识别过程中,我们进行了一些关键项的举例,但是如果换一个对象,有没有好的方法论来指导我们进行准确和全面的识别,而不至于遗漏?从我们今天的内容中,你有没有发现一些规律呢? + + +如果今天的内容对你有帮助,也请你分享给身边的朋友。 + +欢迎你留言与我一起讨论。 + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/04标准化体系建设(下):如何建立基础架构标准化及服务化体系?.md b/专栏/赵成的运维体系管理课/04标准化体系建设(下):如何建立基础架构标准化及服务化体系?.md new file mode 100644 index 0000000..27d1cc0 --- /dev/null +++ b/专栏/赵成的运维体系管理课/04标准化体系建设(下):如何建立基础架构标准化及服务化体系?.md @@ -0,0 +1,113 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 04 标准化体系建设(下):如何建立基础架构标准化及服务化体系? + 前面我们一起讨论了为什么要做标准化,标准化的套路是什么,并按照套路进行了基础设施和应用的标准化示例。我想这些内容可以帮助我们举一反三,尝试着应用到实际工作中了。 + +今天,我继续跟你聊基础架构标准化的问题,但是今天我计划不谈如何进行架构标准化的细节,而是想强调一下基础架构标准化的重要性,因为从我个人的经历和我实际观察到的情况来看,这块的问题会更普遍一些,而这一部分又影响着后续一系列效率和稳定性平台的建设方案。 + +同时,如果说上次我们讲的基础设施和应用标准化是运维团队职责的话,那今天的内容就是架构、开发和运维共同的职责。 + +常见的分布式基础架构组件 + +让我们先一起列一下,微服务的分布式架构下,涉及到的主要基础架构组件有哪些。 + + +分布式服务化框架,业界开源产品比如Dubbo、Spring Cloud这样的框架; +分布式缓存及框架,业界如Redis、Memcached,框架如Codis和Redis Cluster; +数据库及分布式数据库框架,这两者是密不可分的,数据库如MySQL、MariaDB等,中间件如淘宝TDDL(现在叫DRDS)、Sharding-JDBC等。当前非常火热的TiDB,就直接实现了分布式数据库的功能,不再额外选择中间件框架; +分布式的消息中间件,业界如Kafka、RabbitMQ、ActiveMQ以及RocketMQ等; +前端接入层部分,如四层负载LVS,七层负载Nginx或Apache,再比如硬件负载F5等。 + + +上面是几类主要的基础架构组件,为了便于理解我以开源产品举例。但在实际场景中,很多公司为了满足业务上的个性化需求,会自己研发一些基础组件,比如服务化框架、消息中间件等,这个情况在有一定技术实力的公司里比较常见。不过大部分情况下,我们会基于这些开源产品做一些封装或局部的改造,以适应我们的业务。 + +基础架构组件的选型问题 + +关于基础架构组件,业界可供我们选择的解决方案和产品非常多,但是选择多了就容易挑花眼,反而不知道从何入手。我们大概都会遇到同样的问题,是自研还是选择开源产品?有这么多的开源产品到底该选哪一个? + +按正常的思路,一定是先组织选型调研,然后进行方案验证和对比,最后确认统一的解决方案。 + +但是,由于开源产品的便利性,以及开发同学对技术探索的好奇心,实际情况往往是,整个大的技术团队中,不同的开发团队,甚至不同的开发人员,会根据开发的需要或个人喜好,选择不同的开源产品,在没有严格限制的情况下,甚至会尝试去自研。 + +按照我的观察,这个问题特别容易出现在微服务架构引入初期。在这个阶段,团队组织架构按照业务领域进行切分,产生一个个与业务架构匹配的小规模技术团队。 + +每个小团队所负责的业务相对独立,自主权就会变大,如果这个时候整个团队中没有一个强有力的架构师角色去做端到端的约束,就极其容易出现上面的这个问题,并且会一直扩散蔓延下去。 + +相比之下,成规模的大公司在这一点上做得就相对严格一些,当然也可能是因为之前尝过苦头,所以后来变得越来越规范了。所以这一点也是每个技术团队在引入微服务架构时要提前关注的。 + +我们以分布式服务化框架为例,我之前遇到的一个实际情况就是,整个大的技术团队选型时以Java技术栈为主,毕竟这块有很多的业界经验和产品可以借鉴参考。但是有的团队对PHP特别精通熟悉,就想用PHP去做微服务,有的团队对Go感兴趣,就想尝试Go的微服务。 + +从单纯的技术选型上来看,选择什么语言并没有严格的标准。而且在技术团队中,我们也应该鼓励技术多样性和尝试新技术。不过这里要有个度,我暂时先不细说这个度在哪里,我们先来看看,假设没有统一标准的约束会带来什么问题。 + +技术的应用,一般都会随着应用场景的逐步深入和业务体量的增长,逐步暴露出各种各样的问题,我们分两个层面来看。 + +1.开发层面 + +业务开发同学将大量的精力投入到基础组件和开源产品的研究、研发以及规模化之后的运维上,再加上产品形态的不统一,导致需要在技术层面的协作上做大量适配工作,而且经验还无法互通。 + +好不容易在一个产品上摸索了很长时间,踩了很多坑,积累了宝贵的经验,结果发现另外一个产品也要经历同样的一个过程,积累的经验依然不能互通和传递。 + +2.运维层面 + +当我们考虑建设一个统一的效率和稳定体系时,发现基础组件不统一,这个时候就需要做大量的针对不同组件的适配工作。 + +比如我们要在发布系统中做服务上下线处理,就要针对多个微服务化框架做适配。再举个稳定性上全链路跟踪的例子,为了在分布式复杂调用场景下的链路跟踪和问题定位,我们会在服务化框架中统一做打点功能,这样才不需要侵入业务逻辑。 + +就这样一个工作,如果服务化框架不统一,就需要到每个框架里都去开发一遍。不过现实中遇到的实际问题是,整个链路就是会有这样那样的情况而串联不起来。 + +如果你有过类似的经历,一定深有感受。其实各种各样奇葩的问题还远不止这些,继续演化下去,就是我们所说的架构失控了。 + +当我们把业务开发资源消耗在与业务开发无关的事情上,业务开发就很难聚焦于业务架构,并能够更快、更多、更好地完成业务需求,这就与公司对业务开发的诉求背道而驰了。 + +同时还会出现维护投入不足,那就必然导致故障频发等一系列问题,团队内部也会因为问题定位不清楚而形成扯皮推诿的不良氛围。 + +所以,这个时候我们需要做的,就是对基础架构有统一的规划和建设。原则上,每种基础组件只允许一种选型,至少就能满足90%甚至更多的应用场景。 + +比如数据库就只允许使用MySQL,然后版本统一,同时配套的中间件也必须统一,其它的关系型数据库没有特殊情况坚决不允许使用,如果遇到特殊情况具体分析。 + +这里就举个特殊的小例子。 + +为了更好地满足业务个性化需求,我们的消息中间件在早期选择了自研,业务上也要求各个应用使用我们统一的服务。但是对于大数据的业务来说,很多开源产品如Spark,都是原生与Kafka配套的,同时很多的新特性也都是基于Kafka去做的,在这种情况下就不能很生硬地要求大数据业务必须按照我们的标准来,这里还是得遵守大数据生态本身的标准才可以。 + +所以选型问题还是要看具体的业务和应用场景,这里只介绍大致的原则,至于具体应该如何标准化,你可以参考我们前面讲到的标准化套路去尝试梳理,先看看你梳理出来的标准化体系是什么样的,后面我也会针对案例进行分享。 + +基础架构的服务化 + +我们对基础架构组件做了统一标准之后,下一步要做的就是服务化。因为这些组件都只提供了简单的维护功能,还有很多都是命令行层面的维护,这时我们要做的就是把这些组件提供的维护API进行封装,以提供更加便捷的运维能力。 + +这里以Redis缓存为例。 + + +创建和容量申请; +容量的扩容和缩容,新增分片的服务发现及访问路由配置; +运行指标监控,如QPS、TPS、存储数据数量等等; +主备切换能力等等。 + + +以上这些,假设都依赖Redis提供的原生能力来做,基本是不可维护的。所以必须要基于这些原生能力进行封装,结合运维场景,将能力服务化,这样就大大提升了使用方的便利性。 + +同时,我们也可以看到,这个服务化的过程其实就是PaaS化的过程。换言之,如果我们能把基础架构组件服务化完成,我们的PaaS平台也就基本成型了。 + +运维的职责是什么? + +总结上面的过程,我们要做的事情,可以归纳为两步:第一步是基础架构标准化,第二步是基础架构服务化。 + +所以这个时候,运维必须要有意识去做的两件事情。 + + +参与制定基础架构标准,并强势地约束。在这里运维作为线上稳定的Owner,发挥约束作用有可能会比业务架构师这样的角色更为有效。另外,由于历史原因或其他种种因素造成的已有架构标准不统一的问题,是需要开发和运维共同合作去改造的。这里面如何保持良好的协作,制定统一的路线图也是非常重要的。所以这里强制约束是一方面,同时也要提供工具化的手段来支持开发的改造,也就是下面这个动作。 +基础架构的服务化平台开发,目标是平台自助化,让开发依赖平台的能力自助完成对基础组件的需求,而不是依赖运维的人。这个事情是驱动运维转型和改进的动力,也是运维能够深入了解架构组件细节的有效途径。同时,要注意到,如果不朝着服务化方向发展,运维将始终被拖累在这些基础组件的运维操作上。 + + +今天我们讨论的这个话题,我也和很多同行、专家交流过,发现大家都有相同的痛点,但是业界的架构资料和图书中很少涉及到这一部分的内容。我觉得根本上还是经验意识上的缺失,所以结合自己的经验专门整理出来,也很期待听到你的经验和想法。 + +如果今天的内容对你有帮助,也请你分享给身边的朋友。 + +欢迎你留言与我一起讨论。 + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/05如何从生命周期的视角看待应用运维体系建设?.md b/专栏/赵成的运维体系管理课/05如何从生命周期的视角看待应用运维体系建设?.md new file mode 100644 index 0000000..109bd94 --- /dev/null +++ b/专栏/赵成的运维体系管理课/05如何从生命周期的视角看待应用运维体系建设?.md @@ -0,0 +1,103 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 05 如何从生命周期的视角看待应用运维体系建设? + 还记得上周我们在讲标准化体系建设(上)的最后,我留了两个小问题,其中一个是这样的: + +在对象属性识别过程中,我们进行了一些关键项的举例,但是如果换一个对象,我们有没有好的方法论来指导我们进行准确和全面的识别,而不至于遗漏?从我们今天的内容中,你有没有发现些规律呢? + +这个问题的答案其实就是我们今天要讨论的内容,那就是从“应用生命周期管理”的角度分阶段去梳理。 + +简单理解下来就是,对于一个对象,既然有生命周期,就会有不同的生命周期阶段,那这个对象在不同的阶段,可能就会具备不同的属性、关系和场景。只要我们把一个对象的生命周期阶段理清楚了,顺着这条主线分阶段进行分解,就可以分析得更加清晰、明确和具体了。 + +怎样理解生命周期 + +谈到生命,首先就会联想到我们自己,所以这里以人做一个简单的类比。我们人类从出生到死亡,就是一个生命周期,在这个周期的每一个阶段,我们都会有不同的属性、关系和所要面对的场景。 + +比如从人的学生时代开始,作为学生,我们就会具备学生的属性,会有所属学校、所属年级、所属班级、所属学号等等。这个时候我们周边的关系更多的是同学关系和师生关系。我们面临的场景可能就是读书、做作业和考试。当然学生时代细分下去还会有小学生、中学生、大学生以及研究生等阶段,每个阶段里面又会有不同的细分属性以及所要面临的场景,比如大学生毕业,就面对求职的场景等。 + +当一个学生毕业走入职场之后,这个时候就开启了生命周期里的另一个阶段,那就是职场生涯。这个时候我们身上的属性又发生了变化,具备所属公司、所谓职位、所谓层级等。这个时候的关系也更为复杂,如同事关系、上下级关系以及各种各样的社会关系。我们所面临的场景也变得复杂,要完成工作、晋升考核、领取薪酬以及离职跳槽、再次面试等等。 + +再往后,我们到了一定年纪,成为老年人,又会有老年人的属性、关系和场景,这里就不详细列举了。 + +围绕着人类的生命周期,我们国家和社会提供的解决方案,就必须要有一系列对应的教育体系、职业体系、医疗体系、养老体系等。目的就是针对人生的不同阶段,提供不同形式的保障和支持,让每个人在社会体系下都够正常生存并发挥出自己的价值。 + +从上面的分析我们可以看到,人这个对象,在不同的生命周期阶段中,会有不同的角色属性和外部关系,同时要面对的社会和生存场景也不一样。所以,当我们谈论人这个对象的时候,一定是把这个对象放到具体的某个生命周期阶段中,才会有意义。 + +应用的生命周期分析 + +回到我们运维对象的生命周期上来,我们所面对的这些对象就相对规范、标准很多。 + +当一个场景下有多个对象时,就一定要找到那个核心的运维对象,这个核心对象的生命周期就会涵盖其它附属运维对象的子生命周期。结合我们前面讲过的内容,微服务架构下,一切要以应用核心。因此,我们就找到了整个运维体系,或者说软件运行阶段的核心对象,那就是应用。 + +应用就类似于我们社会中的人,凡事都会以人为本,这里就是要以应用为本。那接下来按照上面我们对一个人的生命周期的阶段分解,我们也可以对应用的生命周期阶段进行分解,大致分为五个部分,应用的创建阶段、研发阶段、上线阶段、运行阶段和销毁阶段。我们依次来分析看一下。 + +1.应用的创建阶段 + +这个阶段,最重要的工作,是确认应用的基础信息和与基础服务的关系,要同时固化下来,从应用创建之初,就将应用与各类基础服务的生命周期进行挂钩。 + +应用的基础信息,可以参考之前我们讲标准化的部分,基本上已经涵盖了比较全的信息,你可以按照生命周期的思路,再理解一下并做梳理。 + +对于同一类的应用,只需要做一次标准化即可,后续完全可以形成模板固化到工具平台上。 + +同时,另外一个很重要的工作,就是要开启与应用相关的各类基础服务的生命周期。比如这个应用需要用到缓存、消息队列和DB等,也可能需要域名DNS服务、VIP配置等,这些就要从应用创建这个动作延伸出去,启动这些关联基础服务的创建,比如需要缓存就去申请容量空间,需要消息队列要申请创建新的Topic等等。 + +当然一个应用使用到哪些基础服务,应该是在架构设计和编码阶段就确定下来的,这里做的事情,就是把这些信息通过应用关联起来,与应用的生命周期挂钩。 + +2.应用的研发阶段 + +应用的研发阶段主要是业务逻辑实现和验证的阶段。针对业务逻辑层面的场景就是开发代码和质量保证,但是这个过程中就会涉及到代码的提交合并、编译打包以及在不同环境下的发布部署过程。同时,开发和测试在不同的环境下进行各种类型的测试,比如单元测试、集成测试以及系统测试等等,这整个过程就是我们常说的持续集成。 + +所以,这个阶段,我们要做的最重要的一个事情,就是为研发团队打造完善的持续集成体系和工具链支持,在后面我们会有专门一个部分讲解这个过程。 + +3.应用的上线阶段 + +这是个过渡阶段,从应用创建过渡到线上运行。创建阶段,应用的基础信息和基础服务都已经到位,接下来就是申请到应用运行的服务器资源,然后将应用软件包发布上线运行,这个动作在下面的运行阶段也会持续迭代,我们直接看下面这个阶段。 + +4.应用的运行阶段 + +这是应用生命周期中最重要、最核心的阶段。 + +从运维角度来看,应用在线上运行起来之后就已经变成一个线上运行的进程,那这个进程形态的应用应该有什么样的属性呢?你可能已经联想到,这个时候需要应用线上运行的各种指标的输出。所以这个阶段,应用最重要的属性就是应用本身以及相关联的基础服务的各项运行指标。 + +这里,我们就需要制定每个运维对象的SLI、SLO和SLA,同时要建设能够对这些指标进行监控和报警的监控体系。 + +从业务角度看,应用是线上业务逻辑的执行载体,但是我们的业务需求是在不断变化和迭代的,所以就需要不断地去迭代更新我们的线上应用,这里仍然会依赖到上述应用研发阶段的持续集成过程,并最终与线上发布形成持续交付这样一个闭环体系。 + +从运行阶段应用的关系看,除了它跟基础服务之间相对固化的关系外,应用跟应用、以及应用包含的服务之间的调用关系也非常重要,而且这个关系可能随时都在变化,这个时候,我们应用之间依赖管理和链路跟踪的场景就出现了。 + +同时,应用线上运行还会面临外部业务量的各种异常变化,以及应用自身所依赖的基础设施、基础服务以及应用服务的各种异常状况。比如“双11”大促,外部流量激增;微博上热点事件带来的访问量剧增;或者服务器故障、IDC故障,DB故障;再或者服务层面API的报错等等。这时就出现了线上稳定性保障的场景,比如流量激增时的限流降级、大促前的容量规划、异常时的容灾、服务层面的熔断等等。 + +通过上面的这个分析过程,我们可以看到,日常接触到的各种技术解决方案,都是在解决应用生命周期不同阶段中应用自身或者应用与周边关系的问题,或者是所面对的场景问题。 + +5.应用的销毁阶段 + +这一部分就不难理解了。如果应用的业务职责不存在了,应用就可以下线销毁了。但是这里不仅仅是应用自身要销毁,我们说应用是整个运维体系的核心,所以围绕着某个应用所产生出来的基础设施、基础服务以及关联关系都要一并清理,否则将会给系统中造成许多无源(源头)的资源浪费。 + +我们在日常工作中,经常见到的缓存系统中,很多NameSpace不知道是谁的,消息系统中有很多Topic不知道是谁的,但是又不敢随意乱动,就只能让它无端占用着系统资源。 + +执行应用的销毁这一步动作,其实是取决于最前面应用与基础服务的关系模型分析和建设是否做得足够到位。 + +总结 + +今天我们分析了应用的生命周期,再结合之前讲的标准化内容,我们就找到了做运维架构的切入点,套路也就有了,总结一下就是: + +从生命周期入手,划分阶段,提炼属性,理清关系,固化基础信息,实现运维场景。 + +同理,这个思路还可以运用到基础设施和基础服务对象的生命周期管理中,虽然它们只是子生命周期,但是具体到每个基础服务上面,同样需要这个管理手段和过程。 + +我已经介绍了很多和应用相关的内容,很大一部分的原因是希望能够帮助你梳理好思路,在思考问题和设计解决方案的时候,一定要从实际出发、从问题出发、从基础出发,理清自己的需求和痛点,然后再去寻求解决方案。 + +借鉴业界思路,千万不要一上来就去套用别人的解决方案。因为别人的思路和解决方案往往是建立在一个非常稳固的基础之上的,而这些基础,往往又因为太基础、太枯燥、太不够酷炫,所以常常是一带而过,甚至是略去不讲的。一旦忽略了这一点,再优秀的解决方案也是无源之水,无本之木,是实现不了的。 + +独立思考非常重要,共勉! + +如果今天的内容对你有帮助,也请你分享给身边的朋友。 + +欢迎你留言与我一起讨论。 + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/06聊聊CMDB的前世今生.md b/专栏/赵成的运维体系管理课/06聊聊CMDB的前世今生.md new file mode 100644 index 0000000..9b168f9 --- /dev/null +++ b/专栏/赵成的运维体系管理课/06聊聊CMDB的前世今生.md @@ -0,0 +1,99 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 06 聊聊CMDB的前世今生 + 我们前面在讲标准化的时候,对关键的运维对象做了识别,主要分为两个部分: + + +基础设施层面:IDC机房、机柜、机架、网络设备、服务器等; +应用层面:应用元信息、代码信息、部署信息、脚本信息、日志信息等。 +这两部分是整个运维架构的基础部分,运维团队是维护的Owner,需要投入较大的精力去好好地规划建设。 + + +当我们识别出运维对象和对象间的关系,并形成了统一的标准之后,接下来要做的事情就是将这些标准固化,固化到某个信息管理平台中,也就是我们常说的配置管理,专业一点就叫作 CMDB(Configuration Management DataBase)。 + +其实,如果我们找准了需求和问题在哪里,你会发现技术手段和平台叫什么就真的不重要了,只要是内部能够达成一个统一共识的叫法就好。 + +关于如何打造CMDB和应用配置管理,我之前有一篇公开的文章《有了CMDB,为什么还需要应用配置管理》,写得已经比较细致了,会在下一期发布出来,但不占用我们专栏的篇幅。 + +今天我主要来聊一聊CMDB的前世今生,帮助你更加深刻地理解这个运维核心部件,对我们后面开展CMDB的建设大有裨益。 + +CMDB源起 + +CMDB并不是一个新概念,它源于ITIL(Information Technology Infrastructure Library,信息技术基础架构库)。而ITIL这套理论体系在80年代末就已经成型,并在当时和后来的企业IT建设中作为服务管理的指导理论得到广泛推广和实施。但是为什么这个概念近几年才被我们熟知?为什么我们现在才有意识把它作为一个运维的核心部件去建设呢? + +我想主要有两个因素,一个起了限制作用,一个起了助推作用。 + + +CMDB这个概念本身的定义问题,限制了CMDB的实施; +互联网技术的发展驱动了运维技术的发展和演进,进而重新定义了CMDB。 + + +传统运维思路下的CMDB + +我们先来看下第一个原因,按照ITIL的定义: + + +CMDB,Configuration Management +DataBase,配置管理数据库,是与IT系统所有组件相关的信息库,它包含IT基础架构配置项的详细信息。 + + +看完上面这个描述,我们能感觉到,这是一个很宽泛的概念描述,实际上并不具备可落地的指导意义。事实上也确实是这样,稍后我们会讲到。 + +同时,CMDB是与每个企业具体的IT软硬件环境、组织架构和流程强相关的,这就决定了CMDB一定是高度定制化的体系。虽然我们都知道它不仅仅是一个存储信息的数据库那么简单,但是它的具体形态是什么样子的,并没有统一的标准。 + +从传统IT运维的角度来看,运维的核心对象是资源层面,所谓的基础架构也就是网络设备和硬件设备这个层面;各种关联和拓扑关系,基本也是从服务器的视角去看。所以更多地,我们是把CMDB建设成为一个以设备为中心的信息管理平台。 + +这也是当前绝大多数公司在建设运维平台时最优先的切入点,因为这些运维对象都是实体存在的,是最容易被识别的和管理的;像应用和分布式中间件这种抽象的逻辑对象反而是不容易被识别的。 + +这种形态,如果是在软件架构变化不大的情况下,比如单体或分层架构,以服务器为中心去建设是没有问题的。因为无论设备数量也好,还是申请回收这些变更也好,都是很有限的,也就是整个IT基础设施的形态变化不大。 + +我没有专门调研过国外的实施情况,但就国内的情形,谈下我的经历。 + +早期,大约是在2009~2013年,我接触了一个省级运营商的全国性项目。2012年的时候日PV就到了5亿左右,日订单创建接近2000万。分层的软件架构,不到千台服务器,对于资源的管理,仍然是用Excel表格来记录的。 + +运维基于这样一个表格去管理和分配各种资源,问题也不算太大。究其根本,就是基础设施层面的架构形态相对稳定,有稳定的软件模块数量和架构。但是发展到后来,这样的软件架构无法满足业务的快速迭代,还是做了架构上的拆分,这就是后话了。 + +这里我想表达的是,在那个时期,即使是在IT架构相对先进的运营商体系下面,我也没有太多地听说过CMDB这个概念,包括运营商自身,以及为运营商提供整体技术解决方案的厂商,还有来自方方面面的资深架构师和咨询师等,在做系统架构和运维架构设计时,没有人提及过CMDB,也没有人提出把它作为核心部件去建设,更多的都是从ITIL管理服务的流程体系去给出咨询建议;在落地实施的时候,我们最终见到的大多是一条条的流程规范和约束,后来增加的也多是流程和审批,甚至是纸质的签字审批。 + +这也从一个侧面说明,CMDB在那个时期更多的还是停留在概念阶段,甚至是无概念状态,也没有什么最佳实践经验的传承,CMDB这个概念本身并不具备实践意义,管理的方式方法也就停留在原始的Excel表格中。 + +高大上的ITIL体系更多的是被当做流程规范来落地的,真正体现在技术方案和技术产品上的落地并不多。我想这是实施过程中对ITIL理解和运用的一大误区。 + +接下来,我们看第二个原因,也就是互联网运维的助推力。 + +互联网运维体系下的CMDB + +值得庆幸的是,进入到互联时代,随着互联网运维力量的崛起,CMDB这个概念也真正地得到了落地实践,从理论概念的方法论阶段过渡到了具备具体技术方案的可实施阶段,而且得到了业界的持续分享和传播。我们现在能够看到的CMDB经验分享,基本上都是中大型互联网公司的运维最佳实践。 + +不过,值得注意的是,“此CMDB”已经非“彼CMDB”。我们前面提到,传统运维阶段,我们更多是以设备为核心进行管理,但是到了互联网技术阶段,这个核心就变了,变成了应用这个核心对象。 + +至于原因,我们前面已经讲过,主要还是互联网技术的快速发展,大大推进了微服务技术架构的落地和实践,这种场景下,应用各维度的管理复杂度、应用的复杂度就逐渐体现出来了,所以我们的很多运维场景就开始围绕着应用来开展。 + +与此同时,云计算技术也在蓬勃发展,逐步屏蔽了IDC、网络设备以及硬件服务器这样的底层基础设施的复杂度,有公有云或私有云厂商来专注聚焦这些问题,让我们的运维不必再花过多的精力在这些基础设施上面;同时,单纯以硬件为核心的CMDB形态也被逐步弱化。 + +所以,此时的CMDB,仍然可以叫做配置管理数据库,但是这个配置管理的外延已经发生了很大的变化。之前所指的简单的硬件资源配置管理,只能算是狭义的理解;从广义上讲,当前的应用以及以应用为核心的分布式服务化框架、缓存、消息、DB、接入层等基础组件,都应该纳入这个配置管理的范畴。 + +所以在这个时期,我们提到的运维自动化,远不是自动化的服务器安装部署交付或网络自动化配置这种单一场景,而是出现了持续交付、DevOps、SRE等更适合这个时代的对运维职责的定义和新的方法论。 + +到了这个阶段,传统运维思路下的CMDB,因为管理范围有限,可以定义为狭义上的CMDB;而互联网运维思路下的CMDB外延更广,我们称它为广义的CMDB。新的时期,对于CMDB的理解也要与时俱进,这个时候,思路上的转变,远比技术上的实现更重要。 + +CMDB进行时 + +如果我们仔细观察,会发现一个很有意思的现象。CMDB源于80年代末的ITIL,源于传统IT运维阶段,但真正让它发扬光大的,确是新兴的互联网运维行业,而且现在很多的传统行业也在向互联网学习运维技术。 + +与此同时,在中大型的互联网公司中,比如阿里和腾讯,也越来越重视流程规范的管控,开始更多地将严格的流程控制与灵活的互联网运维技术结合起来,以避免在过于灵活多变的环境下导致不可控的事件发生。 + +所以,从这里我们可以看到,并不是说ITIL的重流程体系就一定是过时老旧的,也不是说互联网运维技术就一定代表着最先进的技术趋势,而是在这个过程中,不同体系相互借鉴、相互学习、共同进步和发展,在碰撞的过程中,催生出更适合这个时代的技术体系。 + +这确实是一个充满了机遇和挑战、但又不乏乐趣的新时代。 + +今天我们讲了CMDB的前世今生,我所讲到的对ITIL以及其定义下的CMDB的理解,更多的是根据我个人的早期经历,还有和业界同行交流的经验所得,我自己并没有完整系统地学习过,所以理解上和见识上会有一定的局限,也期望你能批评指正,我们一起讨论、共同进步。 + +如果今天的内容对你有帮助,也请你分享给身边的朋友,我们下期见! + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/07有了CMDB,为什么还需要应用配置管理?.md b/专栏/赵成的运维体系管理课/07有了CMDB,为什么还需要应用配置管理?.md new file mode 100644 index 0000000..a4d8c7e --- /dev/null +++ b/专栏/赵成的运维体系管理课/07有了CMDB,为什么还需要应用配置管理?.md @@ -0,0 +1,90 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 07 有了CMDB,为什么还需要应用配置管理? + 今天我们分享的主题是:有了CMDB,为什么还需要应用配置管理? + +你不妨先停下来,思考一下这个问题。 + +我抛出的观点是: CMDB是面向资源的管理,应用配置是面向应用的管理。 + +请注意,这里是面向“资源”,不是面向“资产”,资源 ≠资产。 + +CMDB是面向资源的管理,是运维的基石 + +我们一起来梳理一下,在建设运维的基础管理平台时通常要做的事情。 + + +第1步,把服务器、网络、IDC、机柜、存储、配件等这几大维度先定下来; +第2步,把这些硬件的属性确定下来,比如服务器就会有SN序列号、IP地址、厂商、硬件配置(如CPU、内存、硬盘、网卡、PCIE、BIOS)、维保信息等等;网络设备如交换机也会有厂商、型号、带宽等等; +第3步,梳理以上信息之间的关联关系,或者叫拓扑关系。比如服务器所在机柜,虚拟机所在的宿主机、机柜所在IDC等简单关系;复杂一点就会有核心交换机、汇聚交换机、接入交换机以及机柜和服务器之间的级联关系; +第3.5步,在上面信息的梳理过程中肯定就会遇到一些规划问题,比如,IP地址段的规划,哪个网段用于DB,哪个网段用于大数据、哪个网段用于业务应用等等,再比如同步要做的还有哪些机柜用于做虚拟化宿主机、哪些机柜只放DB机器等。 + + +以上信息梳理清楚,通过ER建模工具进行数据建模,再将以上的信息固化到DB中,一个资源层面的信息管理平台就基本成型了。 + +但是,信息固化不是目的,也没有价值,只有信息动态流转起来才有价值(跟货币一样)。接下来我们可以做的事情: + + +第4步,基于这些信息进行流程规范的建设,比如服务器的上线、下线、维修、装机等流程。同时,流程过程中状态的变更要同步管理起来; +第5步,拓扑关系的可视化和动态展示,比如交换机与服务器之间的级联关系、状态(正常or故障)的展示等,这样可以很直观地关注到资源节点的状态。 + + +至此,从资源维度的信息梳理,以及基于这些信息的平台和流程规范建设就算是基本成型了。这个时候,以服务器简单示例,我们的视角是下面这样的: + + + + + +应用配置管理是面向应用的管理,是运维的核心 + +上面说明了CMDB的基础信息部分,如果从传统的SA运维模式,这些信息已经足够,但是从应用运维的角度,这些就远远不够了。 + +这时我们就需要一个非常非常重要的句柄:应用名,或者叫应用标识。至此,应用运维里面最最重要的一条联系也就产生了:“应用名-IP“的关联关系(这里也可以是定义的其它唯一主机标识,如主机名、容器ID等等,因为我们使用的方式是IP,所以这里就以IP示例)。 + +之所以说“应用名”和“应用名-IP关联关系”非常重要,是因为它的影响力不仅仅在运维内部,而是会一直延伸到整个技术架构上。后面我们会介绍到的所有平台和系统建设,都跟这两个概念有关。 + +CMDB是IP为标识的资源管理维度,有了应用名之后,就是以应用为视角的管理维度了。首先看一下应用会涉及到的信息: + + +应用基础信息,如应用责任人、应用的Git地址等; +应用部署涉及的基础软件包,如语言包(Java、C++、GO等)、Web容器(Tomcat、JBoss等)、Web服务器(Apache、Nginx等)、基础组件(各种agent,如日志、监控、系统维护类的tsar等); +应用部署涉及的目录,如运维脚本目录、日志目录、应用包目录、临时目录等; +应用运行涉及的各项脚本和命令,如启停脚本、健康监测脚本; +应用运行时的参数配置,如Java的jvm参数,特别重要的是GC方式、新生代、老生代、永生代的堆内存大小配置等; +应用运行的端口号; +应用日志的输出规范; +其他。 + + +上面的梳理过程实际就是标准化的过程。我们梳理完上述信息后就会发现,这些信息跟CMDB里面的资源信息完全是两个维度的东西。所以从信息管理维度上讲,把资源配置和应用配置分开会更清晰,解耦之后也更容易管理。 + +好了,按照上面CMDB说的套路,梳理完成后,就是要进行信息的建模和数据的固化,这时就有了我们的“应用配置管理”。再往后,就是基于应用配置管理的流程规范和工具平台的建设,这就涉及到我们经常说的持续集成和发布、持续交付、监控、稳定性平台、成本管理等等。 + +从应用的视角,我们配置管理,应该是下面这样一个视图(简单示例,不是完整的): + + + +好了,有了资源配置信息和应用配置信息,这两个信息应该怎么统一管理起来呢。直接看图: + + + +至此,CMDB和应用配置管理的分层分解就完成了,应用名关联着应用配置信息,IP关联着资源信息,二者通过“应用名-IP”的对应关系,联系到一起。 + +总结 + +CMDB是运维的基石,但是要发挥出更大的价值,只有基础是不够的,我们要把更多的精力放到上层的应用和价值服务上,所以我们说应用才是运维的核心。 + +你可以看到,如果仅仅基于CMDB的资源信息作自动化,最多只能做出自动化的硬件资源采集、自动化装机、网络-硬件拓扑关系生成等资源层面的工具,这些工具只会在运维层面产生价值,离业务还很远,就更谈不上给业务带来价值了。 + +但是基于应用这一层去做,就可以做很多事情,比如持续集成和发布、持续交付、弹性扩缩容、稳定性平台、成本控制等等,做这些事情带来的价值就会大大不同。 + +以上就是我抛出的观点,CMDB是面向资源的管理,应用配置是面向应用的管理。希望能够抛砖引玉,听到更多你的观点和反馈。 + +如果今天的内容对你有用,也希望你分享给身边的朋友,我们下期见! + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/08如何在CMDB中落地应用的概念?.md b/专栏/赵成的运维体系管理课/08如何在CMDB中落地应用的概念?.md new file mode 100644 index 0000000..988d0ac --- /dev/null +++ b/专栏/赵成的运维体系管理课/08如何在CMDB中落地应用的概念?.md @@ -0,0 +1,123 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 08 如何在CMDB中落地应用的概念? + 我们前面讲了应用是整个微服务架构体系下运维的核心,而CMDB又是整个运维平台的基石。今天我就讲讲在CMDB中如何落地应用这个核心概念,以及如何建立应用集群分组的思路。 + +如何有效组织和管理应用 + +微服务架构下会有很多应用产生出来,少则十几、几十个,多则上百甚至上千个。这时我们面临的第一个问题就是如何有效地组织和管理这些应用,而不是让它们在各处散乱,命名方式和层次结构可能还不统一。 + +你可能接触过“服务树”的概念,这个提法是小米在早期互联网运维实践的分享中传播出来的。我第一次听到这个概念是在13年阿里技术嘉年华大会上听小米运维的分享。再往前,这个概念应该是从百度的运维体系中借鉴出来的。 + +这里的服务实际对应的就是我们前面提到的应用这个概念。据我了解,在阿里和腾讯都是叫作应用,现在业界比较通用的叫法也是应用。其实叫什么并不重要,关键还是要学习到对这个概念的管理方式。 + +从服务树这个名字中,我们就可以了解到,有效组织和管理应用的方式,就是把它组织成一个树形的层次结构。这种管理模式,无论是在BAT,还是在其它的互联网公司,基本都是一样的思路和模式,所以叫法虽然不同,但是思路上却是相通的,可谓异曲同工。 + +基于业务维度的拆分,对应产生了我们的应用拆分原则。比如对于电商公司,大的维度会有电商、支付、广告、流量和搜索等业务领域;进一步,电商业务领域里最典型的会有用户、会员、商品、交易、商家、店铺以及物流等;这里面还可以再进一步细分,比如商品会有详情、SKU、SPU、库存、评价、标签等。 + +讲到这里,我们再看一下技术团队的组织架构,基本上是对应着整个业务技术架构的拆分的。也就是业务架构决定了技术架构,而技术架构又决定了一个研发团队的组织架构,这个组织架构中不同的团队单元分别承担着对应业务的需求开发和实现职责。 + +上面这个组织架构建设的逻辑和思路,也是我们在组建团队和职责划分时可以参考的。 + +这样一个逻辑讲下来,我们的应用管理思路其实也就明晰了:产品线-业务团队-应用。 + +这里举个电商商品的例子就是:电商技术-商品团队-商品中心-商品详情等。 + + + +当然因为每个公司对组织架构定义的方式不同,也可以用一、二级部门这样的方式来指代。但是具体团队的分工和职责,一定是来自于业务架构决定的技术架构,只有这样,各业务团队才会职责清晰,配合协作才会顺畅起来。 + +对于应用名定义,要设定规范,比如: + + +应用名必须以大小写英文字母以及下划线组合; +应用名长度不超过40个字符,尽量简单易懂; +不允许出现机房代号和主机名称这样的信息。 + + +简单举例,商品中心命名为itemcenter,商品详情命名为detail。 + +这里做个小结:到了软件运维阶段,运维工作是否可以高效地组织开展,很大程度上,在前面的业务架构拆分阶段就决定了。也就是业务架构拆分得是否合理、职责是否明晰,决定了后续团队组织架构是否合理、团队职责是否明晰。如果这点没做好,到了运维阶段必然就是混乱的。 + +这一点我在开篇词中也提到过,运维能力的体现,一定是整体技术架构能力的体现,割裂两者单独去看,都是没有意义的。同时,对于当前仍然把运维割裂建设的研发团队,也需要去思考一下在组织架构建设上的合理性了。 + +应用的集群服务分组建设 + +上述讲到的是应用的组织管理,看上去逻辑思路相对清晰,组织起来也不复杂,但是再往下,应用的集群服务分组建设就会相对复杂了。 + +为什么会有集群服务分组呢?我们一起来看这么几个需求场景。 + +场景一:多环境问题。 + +我们常见的环境会有开发联调环境、集成测试环境、预发环境、线上环境等等。后面我们讨论持续交付时会讲到,实际场景下所需要的环境会更多。 + +场景二:多IDC问题。 + +对于大型互联网业务,会做业务单元化,或者有海外业务拓展需求的场景,我们会在多个IDC机房部署应用,应用代码是相同的,但是配置可能会不同。 + +场景三:多服务分组问题。 + +这个场景就跟具体业务场景相关了。举个例子,比如商品中心IC这样一个核心应用,对外会有商品详情、交易下单、订单、购物车、评价、广告、秒杀活动、会场活动、商家、店铺等一系列应用依赖它,但是这些依赖它的应用优先级是不一样的。 + + +核心应用和非核心应用:比如交易支付链路上的应用属于核心应用,任何时候都必须要优先保障,但是对于评价、商家和店铺这些应用优先级就低一些。反过来理解就是一个应用出现故障,是不是会影响业务收入,如果影响就属于核心应用,如果不是或者影响非常小,那就属于非核心应用。所以IC这个应用下面,就会有IC的交易分组,IC的广告分组、IC的电商分组等,这些分组就会相对固定和静态。 +场景因素决定。这个对于电商就会比较典型,比如大促时的秒杀场景,对于参加秒杀活动的商品,瞬时的访问量就会非常大,而不参加活动的商品就不会有这么大的访问量。所以这时为了隔离较大的流量,就需要有多个不同的秒杀IC分组,从资源层面进行隔离;同时上层秒杀活动的应用在配置中心配置依赖时,就要配置到对应的秒杀IC集群分组上,这样即使秒杀IC出现问题,也不会影响正常的商品IC访问。所以根据场景,不同阶段就会有IC的大促秒杀分组,这种类型的分组就需要根据实际的业务场景来决定,是个动态调整的过程,需要开发和运维一起来讨论和验证。 + + +一般情况下,集群服务分组会有以上三个维度中的一个或多个来决定。还是以商品中心IC为例,按照上面的介绍,就会对应如下关系: + + + + +至此,“应用-集群服务分组-资源”的对应关系就建立起来了。这里我们叫它“应用树”或者“服务树”都可以,不管叫什么,这个信息是CMDB中最为关键和核心的信息。为什么是最关键和核心的呢? + +CMDB在基础服务体系中的核心位置 + +这里我们以应用为核心来看,CMDB中会保存“应用-分组-资源”的对应关系,这个关系对于周边系统来说都是需要的,举例如下。 + +1.监控系统。 + +我们需要以上的对应关系,监控到每个应用、每个集群以及每台机器上的关键信息。 + +2.发布系统。 + +我们需要将每个应用对应的代码进行编译打包,然后发布到对应集群的主机上,也需要这个对应关系,这一点我在后面的持续交付中还会讲到。 + +3.服务化框架。 + +需要依赖应用和集群分组两个信息,其中主要是对应用名和集群分组名的依赖,对于服务化框架来说,更多的是通过其配置管理中心注册的应用名,来实现应用的服务和API管理,这里要做到与CMDB统一。同样,像LVS和Nginx这样的四七层负载,以及ZK这样的开源分布式配置管理,凡是涉及服务注册、服务发现以及服务上下线的基础服务,都是类似思路。 + +4.基础服务中。 + +如分布式DB、分布式缓存和消息等,就需要应用的应用名,以及应用与资源IP的对应关系,或者集群分组与IP的对应关系。 + + +应用名,是因为要建立应用与分布式服务实例之间的关系。如应用与缓存NameSpace的对应关系,应用与消息Topic的对应关系等,以便于这些基础服务的生命周期管理和自动化开发。 +应用与资源的对应关系,是因为有些核心资源是要做ACL访问控制的。比如对于用户、交易或支付这样非常敏感的数据,它们对应的数据库就不允许随意连接,而应该是仅限于授权过的应用访问。这时就要针对应用对应的IP地址进行白名单配置。一方面,可以通过分布式DB中间件进行配置;另一方面,也可以通过在DB层面进行设置,比如MySQL就可以直接配置白名单策略;同时也可以在机器的iptables上配置,至于如何配置就看具体需求了,但是无论怎样,应用与资源的对应关系是非常重要的。 + + +5.稳定性保障平台,或者叫服务治理平台。 + +针对系统的稳定性,我们会在应用中做很多的降级限流和开关预案策略,这些都是跟应用直接关联的。而且按照我们前面介绍的,不同的集群分组,策略可能会有不同,所以又会跟集群分组相关。同时,这些策略最终下发到具体服务器上运行的应用实例上,所以这里就会需要应用、集群分组以及对应的资源关系。 + +总结一下,简单示意图如下: + + + +总结 + +通过上述的分析,我们可以看到基于以应用为核心的CMDB中,又衍生出“应用-集群服务分组-资源”这样一个运维体系中的核心关系。经过这三部分的分析,我们之前所说的基于应用为核心的运维视图就可以建立出来了,我们再次示意如下: + + + + +今天我们讨论的内容提到了,监控、发布、基础服务以及稳定性平台会依赖CMDB中“应用、集群服务分组-资源”的对应关系信息,但是当CMDB中的这些关系信息发生变化,比如新增一个IP,或者下线一个IP,这些信息是如何传递到其它平台的呢?这些平台又是如何查询这些关键信息的呢?欢迎你留言与我一起讨论。 + +如果今天的内容对你有帮助,也请你分享给身边的朋友,我们下期见! + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/09如何打造运维组织架构?.md b/专栏/赵成的运维体系管理课/09如何打造运维组织架构?.md new file mode 100644 index 0000000..15f1511 --- /dev/null +++ b/专栏/赵成的运维体系管理课/09如何打造运维组织架构?.md @@ -0,0 +1,110 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 09 如何打造运维组织架构? + 前面几周,我们介绍了Netflix为什么没有运维岗位、应用运维标准化、基础服务标准化以及从应用生命周期的角度如何进行运维建设等内容。这一周我们就来聊聊在组织架构和运维转型方面的话题。 + +Netflix给我们的启示 + +专栏的第一篇我们就介绍了Netflix的云平台组织架构,你应该可以发现,Netflix其实已经给我们提供了一个非常好的思路和方向,就是在提供基础服务能力的同时,提供对应的自助化运维能力。也就是说,开发人员可以在这样一个平台上完成自己想要做的任何运维操作,而不再依赖运维的人。 + +我们最应该学习和借鉴的,也恰恰是我们绝大多数团队都会忽略的,就是要做好运维和整个技术架构体系的融合,一定不要割裂两者。同时,还要注意不仅仅是促进组织架构层面的融合,最重要的是要促进职能协作上的融合。 + +应该怎么理解呢? + +我先撇开组织架构,大致说一下我的思路。开篇词中我提到,运维能力的体现,一定是整体技术架构能力的体现。所以,要想做好运维就一定要跳出运维这个框框,从全局的角度来看运维,要考虑如何打造和体现出整个技术架构的运维能力,而不是运维的运维能力。这一点是根本,一定要注意。如果我们仍然片面地从运维的角度看运维,片面地从运维的角度规划运维,是无法走出运维低价值的困局的。 + +从价值呈现的角度看运维 + +当我改变了这个认知后,我的出发点就回归到了效率、稳定和成本这三个对于研发团队来说最重要的目标上来。从运维的角度来说,能够与这三个点契合的事情,我总结了以下五个。 + +1. 运维基础平台体系建设 + +这块主要包括我们前面提到的标准化体系以及CMDB、应用配置管理、DNS域名管理、资源管理等偏向运维自身体系的建设。这一部分是运维的基础和核心,我们前面讲到的标准化以及应用体系建设都属于这个范畴。 + +2. 分布式中间件的服务化建设 + +在整个技术架构体系中,分布式中间件基础服务这一块起到了支撑作用。这一部分的标准化和服务化非常关键,特别是基于开源产品的二次开发或自研的中间件产品,更需要有对应的标准化和服务化建设。这也是我们无意识地割裂运维与技术架构行为的最典型部分,这里容易出现的问题,我们前面讲过,你可以回去再复习一下。 + +3. 持续交付体系建设 + +持续交付体系是拉通运维和业务开发的关键纽带,是提升整个研发团队效率的关键部分。这个部分是整个软件或应用的生命周期的管理体系,包括从应用创建、研发阶段的持续集成,上线阶段的持续部署发布,再到线上运行阶段的各类资源服务扩容缩容等。开发和运维的矛盾往往比较容易在这个过程中爆发出来,但是这个体系建设依赖上面两部分的基础,所以要整体去看。 + +4. 稳定性体系建设 + +软件系统线上的稳定性保障,包括如何快速发现线上问题、如何快速定位问题、如何快速从故障中恢复业务、如何有效评估系统容量等等。这里面还会有一些运作机制的建设,比如如何对故障应急响应、如何对故障进行有效管理、如何对故障复盘、如何加强日常演练等等。同样,这个环节的事情也要依赖前两个基础体系的建设。 + +5. 技术运营体系建设 + +技术运营体系也是偏运作机制方面的建设,最主要的事情就是确保我们制定的标准、指标、规则和流程能够有效落地。这里面有些可以通过技术平台来实现,有些就需要管理流程,有些还需要执行人的沟通协作这些软技能。 + +最终通过这样一个规划,我把团队以虚拟形式重新规划了不同职责,分别负责基础平台体系、分布式中间件服务化体系、持续交付体系和稳定性体系,基本就是上述的前四件事情。 + +对于最后一个技术运营体系,这一点作为共性要求提出。我要求团队每个成员都要具备技术运营意识。具体来说,就是要能够有制定输出标准的意识和能力,能够有规范流程制定的能力,同时能够将标准和流程固化到工具平台中,最后能够确保承载了标准和规范的平台落地,也就是平台必须可用,确实能给运维团队或开发团队带来效率和稳定性方面的提升。这些对个人的要求还是比较高的,要有一定的规划、设计和落地能力,能具备一整套能力的人还是少数,目前这块还是靠团队协作来执行。 + +运维协作模式的改变 + +上面的这几件事情,并不是由运维团队内部封闭来做。还是我们反复强调的那个思路,要站在怎么能够打造和发挥出整个技术架构体系运维能力的视角,而不仅仅是发挥运维团队的运维能力。所以这些事情的执行可以理解为是由运维团队发起,与周边技术团队协作配合来完成的。 + +所以这些事情都需要跨团队协作。一方面运维团队要主动出击,去沟通,去推进;另一方面,必须能得到上级主管甚至是更高层技术领导的支持,所以这里要求技术管理者必须要有这个意识,促进这样的组织协作方式变革,如果技术管理者意识不到或者支持不到位的话,运维在后续的推进工作中将会遇到非常大的阻力。 + +下面来分享下我们目前正在尝试的一些调整。 + +我们运维所在的平台技术部门,包括了分布式中间件、虚拟化技术、稳定性工具平台以及大数据几个子部门。当我们发起并推进上述工作时,就需要与这些团队联合协作,朝着某个目标共同执行。下面我们来看看细分的情况。 + +1. 运维基础平台建设 + +这块大多数的工作会由运维来完成,因为这是运维的基础,也属于整个技术架构比较关键的基础平台之一,这一点我们在讲应用和集群服务管理时已经介绍过。 + +2. 分布式中间件服务化建设 + +这个部分我们就需要分布式中间件团队的配合。我们可以一起制定各种使用标准、规范和流程;中间件团队负责提供运维服务能力的接口;运维团队根据用户使用的场景进行场景化需求分析,并最终实现场景,同时负责标准和自助化工具平台的推广和落地。 + +3. 持续交付体系建设 + +这一部分也会涉及多个团队的协作。在资源使用上,我们前期会用到KVM,那么如何快速交付KVM资源,就需要与虚拟化技术团队协作。现在我们在尝试容器方案,涉及到镜像制作、网络配置以及对象存储这些底层技术,一样会与虚拟化团队配合,在资源交付效率和利用率上都有很大提升。同时,还会与中间件团队协作,因为在应用发布和扩容缩容过程中,就会涉及服务上下线,这就要与服务化框架配合,服务化框架提供底层运维服务能力,而运维要做的就是通过中间件运维能力的封装整合,进而实现用户使用的场景化需求。 + +4. 稳定性体系建设 + +这里会涉及一些链路埋点、限流降级、以及开关预案等一些技术方案需求,通常会有这样一个专门的稳定性工具团队,对外输出一些稳定性保障能力,比如一些稳定性通用SDK的开发,后台日志采集分析以及数据计算等等,这些事情会对技术能力的要求比较高,需要具备较强开发能力的人来做。所以,运维在这里发挥的作用一个是上述提到的场景化实现能力,再一个就是稳定性能力的落地,或者说是运营能力。稳定性工具提供的多是能力和支持,最终要在业务层面真正执行,就需要运维和业务开发共同来执行。比如一个应用上线,是否具备关键接口的限流降级能力,是否具备熔断能力,是否满足上线的性能及容量要求,这个工作是需要运维深入每个业务,根据不同的业务场景和实现情况,一个个具体落地才行。所以,整体上对运维技术运营能力的要求就会非常高。 + +运维在这个过程中要发挥的最关键作用就是通过用户使用场景的分析,将各项基础服务封装并友好地提供出来,并确保最终的落地。方式上,或者是通过工具平台的技术方式,比如分布式中间件基础服务;或者是通过技术运营能力,比如稳定性能力在业务层面的落地。 + +运维在这个过程中,就好像串起一串珍珠的绳子,将整个平台技术的不同部门,甚至是开发团队给串联起来,朝着发挥出整体技术架构运维能力的方向演进。 + +运维的组织架构 + +上面是我们从团队需求和运维价值呈现层面成立的虚拟组织,从实际的人员管理以及技能维度来划分的话,我们和其它互联网公司的运维团队差别不大,基本会分为如下几个岗位: + + +基础运维,包括IDC运维、硬件运维、系统运维以及网络运维; +应用运维,主要是业务和基础服务层面的稳定性保障和容量规划等工作; +数据运维,包括数据库、缓存以及大数据的运维; +运维开发,主要是提供效率和稳定性层面的工具开发。 + + +这个实体的组织架构,相当于是从技能层面的垂直划分。基础运维更擅长硬件和操作系统层面的运维;应用运维可能更擅长业务稳定性保障、疑难问题攻关以及技术运营等;数据运维就不用多说了,DBA本身就是专业性极高的一个岗位;运维开发则是支持上述几个岗位日常运维需求的,是否能将人力投入转换成工具平台支持,就看这个团队的能力。 + +而前面所说的从价值呈现层面进行的虚拟团队划分,则是将上述几个实体团队技能上的横向拉通,让他们重新组织,形成技能上的互补,从而发挥出更大的团队能力。 + +实体组织架构,相当于一个人的骨骼框架,但是价值呈现层面的虚拟组织,就更像是一个人的灵魂,体现着这个人的精神面貌和独特价值。 + +这个过程中,必然会对运维的技能模型有更新、更高的要求。 + +总结 + +今天我为你介绍了我们正在实践的一些运维组织架构方面的内容。后来当我翻阅《SRE:Google运维解密》这本书时,发现如果按照书中的章节目录进行分类的话,基本上都可以与前面我介绍的部分对应起来,这也更加坚定了我们要按照这套组织模式运作下去的信心。 + +同时,我们也要明白,业界没有一劳永逸的组织架构,也没有放之四海而皆准的组织架构标准,更没有万能的可以解决任何问题的组织架构设计,这里的关键是我们如何能够发挥出团队整体的能力和价值,而这一点又需要我们不断地与自己所在团队和业务特点去匹配和契合,这是一个不断变化的过程,也是需要持续调整的过程。 + +所以这对技术管理者要求会比较高,应该如何不断地去匹配和契合这个最佳价值点,同时如何统筹调度好团队中不同类型的技术资源并形成合力,是非常重要的。 + +你的团队在实际过程中遇到过哪些问题,你有怎样的经验和观点,欢迎你留言与我一起讨论。 + +如果今天的内容对你有帮助,也请你分享给身边的朋友,我们下期见! + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/10谷歌SRE运维模式解读.md b/专栏/赵成的运维体系管理课/10谷歌SRE运维模式解读.md new file mode 100644 index 0000000..bcdafaa --- /dev/null +++ b/专栏/赵成的运维体系管理课/10谷歌SRE运维模式解读.md @@ -0,0 +1,84 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 10 谷歌SRE运维模式解读 + 前面我和你分享了一些关于运维组织架构和协作模式转型的内容,为了便于我们更加全面地了解先进的运维模式,今天我们再来谈一下谷歌的SRE(Site Reliability Engineer)。同时,也期望你能在我们介绍的这些运维模式中找到一些共通点,只有找到这些共通点,才能更深刻地理解,并借鉴到真正对我们有用的东西。 + +专栏的第一篇文章我们介绍了Netflix的NoOps模式。这个模式并不意味着不存在任何运维工作,只是Netflix将这些事情更紧密地融入到了日常的开发工作中,又做得非常极致,所以并没有很明显地体现出来。 + +但是,谷歌的SRE却是一个真实具体的岗位,也有明晰的岗位职责。从借鉴意义上来讲,SRE可以给我们提供更好的学习思路和样板。 + +SRE这个概念,我应该是2014年下半年的时候听到的。当时可接触的资料和信息有限,只知道是谷歌对运维岗位的定义,负责稳定性保障,就没有更多其他的认识了。 + +后来,有越来越多在谷歌工作或接触过这个岗位的专家开始在公开演讲中分享这个概念。同时,《SRE:Google 运维解密》,这本由多名谷歌SRE亲笔撰写的图书也开始在国内广泛流传,让我们对很多细节有了更加细致的了解。 + +SRE岗位的定位 + +首先,SRE关注的目标不是Operation(运维),而是Engineering(工程),是一个“通过软件工程的方式开发自动化系统来替代重复和手工操作”的岗位。我们从SRE这本书的前面几个章节,可以看到谷歌不断强调SRE的工程能力。我简要摘取几段: + + +Common to all SREs is the belief in and aptitude for developing +software systems to solve complex problems. +所有的SRE团队成员都必须非常愿意,也非常相信用软件工程方法可以解决复杂的运维问题。 + +By design, it is crucial that SRE teams are focused on engineering. +SRE模型成功的关键在于对工程的关注。 + +SRE is what happens when you ask a software engineer to design an +operations team. +SRE就是让软件工程师来设计一个新型运维团队的结果。 + + +与之相对应的,还有一个很有意思的地方,整本书中提到Operation的地方并不多,而且大多以这样的词汇出现:Operation load,Operation overload,Traditional/Manual/Toil/Repetitive Operation Works。你可以仔细体会一下,这些大多就是传统的纯人工操作模式下的一些典型词汇。 + +我们可以看到,从一开始,谷歌就没把SRE定义为纯操作类运维的岗位,也正是谷歌换了一个思路,从另外一个维度来解决运维问题,才把运维做到了另一个境界。 + +SRE岗位的职责 + +书中对SRE的职责定义比较明确,负责可用性、时延、性能、效率、变更管理、监控、应急响应和容量管理等相关的工作。如果站在价值呈现的角度,我觉得可以用两个词来总结,就是“效率”和“稳定”。 + +接下来,详细说下我的理解和分析。 + +SRE的能力模型,不仅仅是技术上的,还有产品设计、标准规范制定、事后复盘总结归纳这些技术运营能力,同时还需要良好的沟通协作能力,这个就属于职场软技能。 + +SRE,直译过来是网站稳定性工程师。表面看是做稳定的,但是我觉得更好的一种理解方式是,以稳定性为目标,围绕着稳定这个核心,负责可用性、时延、性能、效率、变更管理、监控、应急响应和容量管理等相关的工作。 + +分解一下,这里主要有“管理”和“技术”两方面的事情要做。 + + +管理体系上,涉及服务质量指标(SLI、SLA、SLO)、发布规则、变更规则、应急响应机制、On-Call、事后复盘机制等一系列配套的管理规范和标准制定等。 +技术体系上,以支持和实现上述标准和规范为目标,涉及自动化、发布、监控、问题定位、容量定位,最终以电子流程串联各个环节,做到事件的闭环。 + + +可以看到技术上的平台和系统是用来支撑管理手段的。谷歌的运维其实并没有单独去提自动化、发布、监控等内容,而是通过稳定性这个核心目标,把这些事情全部串联在一起,同时又得到了效率上的提升。 + +我们来看几个主要的系统。 + + +自动化。是为了减少人为的、频繁的、重复的线上操作,以大大减少因人为失误造成的故障,同时提升效率。比如谷歌内部大名鼎鼎的Borg系统,可以随时随地实现无感知的服务迁移。现在,它的开源版本,已然成为业界容器编排体系标准的Kubernetes。 +持续交付。谷歌非常重视持续交付。由于它的需求迭代速度非常快,再加上是全球最复杂的分布式系统,所以就更加需要完善的发布系统。 +问题定位。这块跟监控相关但又有不同。我看到谷歌SRE这本书中并没有提到太多Tracing的内容,更多的是讲监控和问题管理层面的跟踪机制。其实,关于问题定位,谷歌的Dapper大名鼎鼎,功能很强大,国内外很多跟踪系统和思路都参考了Dapper的理论。这块也是为了能够快速定位问题,保障稳定而产生的,国内分享的大多关于全链路跟踪和分析、限流降级、开关和预案系统、强弱依赖等都属于这个范畴,我认为这块应该更准确地定义为分布式服务治理相关的内容。 +各类分布式系统。如分布式锁、分布式文件、分布式数据库,我们熟知的谷歌三大分布式论文,就是这些分布式系统的优秀代表,也正是这三大论文,开启了业界分布式架构理念的落地。 + + +这些系统大都是以稳定性为导向,同时带动了日常运维效率的大幅度提升,有了监控和全链路这样的问题发现和定位手段,也大大提升了我们对故障处理和问题定位的效率。容量管理,不仅仅可以保障容量充足,还能最大程度地保障资源分配的合理性,尽可能减少浪费,对于成本管控也大有好处。所以,围绕着稳定性这个核心目标,不仅达到了稳定的目的,还获得了高效的运维效率。 + +所以,SRE的理念通过稳定性这个核心点,将整个运维体系要做的事情非常系统紧密地整合起来,而不是一个个孤立的运维系统。所以,SRE是一个岗位,但更是一种运维理念和方法论。 + +如何借鉴和落地 + +在国外,SRE岗位的薪资,和SWE软件开发工程师相比,要平均高出25%。从实际的岗位要求上看,SRE的技能要求也要比SWE更高、更全面,所以这样的人才是比较紧缺的。即使在国外,除了谷歌、Facebook以及Netflix这样超一流的科技公司能够招聘到,或者内部培养出较多这样的人才,其它公司的SRE、PE或者应用运维也无法完全达到上面的要求。 + +在国内,就更难一些,那怎么做呢?一个思路就是我们之前讲组织协作模式转型时提到的,就是要依靠团队的力量、发挥团队的力量,如果是单个团队不能完成的事情,就跨团队协调完成。所以,SRE岗位的要求很高,但是我们可以靠团队中具备不同能力的人协作,共同达成SRE的职责和目标。 + +最后,还是我反复强调的观点,要想做好运维,就得跳出运维的局限,要站在全局的角度,站在价值呈现的角度,站在如何能够发挥出整体技术架构运维能力的角度,来重新理解和定义运维才可以。 + +通过今天的内容,你对于SRE有什么新的理解或者疑问?结合前面的内容,你能够挖掘出哪些共通点呢?欢迎你留言与我讨论。 + +如果今天的内容对你有帮助,也欢迎你分享给身边的朋友,我们下期见! + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/11从谷歌CRE谈起,运维如何培养服务意识?.md b/专栏/赵成的运维体系管理课/11从谷歌CRE谈起,运维如何培养服务意识?.md new file mode 100644 index 0000000..4c9d292 --- /dev/null +++ b/专栏/赵成的运维体系管理课/11从谷歌CRE谈起,运维如何培养服务意识?.md @@ -0,0 +1,98 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 11 从谷歌CRE谈起,运维如何培养服务意识? + 2016年10月,谷歌云平台博客(Google Cloud Platform Blog)上更新了一篇文章,谷歌宣布了一个新的专业岗位,CRE(Customer Reliability Engineering),直译过来就是客户稳定性工程师。我看了介绍后,发现还是一个挺有意思的岗位设置,搜索之后发现,针对这个岗位国内还没有太多的解读。下面我们就来尝个鲜,一起来看一看。 + +CRE产生的背景 + +这个岗位出现的主要背景,还是越来越多的用户选择在云上开展自己的业务,很多企业和用户将业务从原来传统的自运维IDC机房迁移到云上。这样做其实就是选择相信公有云平台,但同时也就放弃了对底层基础设施的把控,甚至把企业最为核心的数据也放到了云上。说简单点,就是一个公司的身家性命都交给公有云了。 + +虽然绝大多数的公有云都宣称自己的稳定性多么高多么好,但是我们知道实际情况并非如此。 + +其实,我们可以看下Netflix,虽然业务在相对稳定的AWS上,但是自从在AWS上遇到过几次严重故障后,就开始自己做稳定性保障的功能,我们熟知的Chaos Monkey这只猴子就是这么来的,进而发展到后来的Chaos Engineering这样一整套体系。 + +可以看到,Netflix秉承Design For Faliure,从一开始就选择在变化多端且自己不可控的环境里,加强自己系统的健壮性和容错能力,而不是依赖任何云厂商的承诺。 + +不过,并不是任何企业都具备Netflix这样的技术能力把自己打造得这么稳定。所以,当云上不稳定的情况发生时,公有云客户通常是手足无措的。因为他并不了解出了什么状况,不知道是自己的问题还是云上基础设施或基础服务的问题,也不知道自己应该从哪里入手恢复业务,所以时间长了必然就会感到非常焦虑,各种不放心。 + +CRE岗位的职责 + +CRE出现的根本目的,就是消除客户焦虑,真正地站在客户的角度去解决问题,同时对客户进行安抚、陪伴和关怀。 + +通常的售后支持,都是你问什么问题,我就回答什么问题,能马上解决的就马上解决,不能解决的就转到后端处理,然后让客户等着,承诺多长时间内给出答复。这种流程标准,严格执行SLA规范,对于一般问题还好,但要是真的出现大问题就不行了。 + +业务挂了,我都火烧眉毛了,你还跟个机器人一样,我问啥你说啥;或者你排查下对我说跟你没关系,让我自己再检查下,再或者转给后端处理,让我先等着,这个体验就非常差了。 + +所以,CRE这个角色一定是站在客户角度解决问题。加入客户的“作战室”(War Room),和客户一起排查,问题不解决,自己不撤退;还会随时通报进展,必要的时候会将故障升级到更高的级别,寻求更专业的资源投入以共同解决;同时根据客户的不同反应进行不同方式的安抚。 + +CRE还会发挥谷歌多年积累下来的非常宝贵的线上运维经验,在日常就跟客户沟通传递一些稳定性保障的知识。CRE可以按照谷歌总结出来的类似SRE的标准规范,对客户线上系统进行稳定性标准评审,并给出专业的建议。如果客户同意遵守这样的标准规范执行,在后续出现故障时,CRE就完全可以按照非常成熟的SRE的运作模式去协作用户处理故障,这样就会大大提升CRE和客户的协作效率,为故障快速处理赢得更多宝贵时间。同时CRE也可以发挥更大的专业作用,而不是之前的对客户系统不熟悉,空有一身绝世武功,却使不上劲。 + +所以,CRE这个角色,既具备良好的专业技术能力,又有非常强的问题解决能力,同时还要具有优秀的客户沟通和关怀能力。背后还有谷歌多年的全球最佳运维实践SRE的经验和方法论支持,让CRE这个角色发挥出更加独特的作用,这一点可能是其它公有云厂商难以达到的。 + +从CRE谈谈做运维为什么要有服务心态 + +上面花了些篇幅对CRE做了一个整体的介绍。我个人的整体感受,CRE更多的是一个服务性质的岗位,最终是要对客户的满意度负责,所以我们可以看到他的职责里面处处充满了紧贴客户需求和痛点的工作内容。 + +我们可能一下子达不到CRE这么高大上的水平,但是日常工作中我们要不断提升自己的服务意识还是很有必要的。而且我观察下来,有时候我们日常工作中出现的很多沟通问题、协作问题甚至是技术问题,都是因为服务意识不够而导致的。 + +我总结了一下,是不是有服务心态,表现在我们的做事方式上,就是我们是否能够站在对方的角度考虑问题、解决问题。 + +具体怎么做,可以有很多方式,这里我给出我个人的几个建议。 + +1. 多使用业务术语,少使用技术术语 + +与合作部门沟通协作,特别是对于非技术类的业务部门,尽量多使用业务语言来表达。在讨论一个需求时,如果表达的都是API、缓存、数据库、消息队列等等这些专业术语,估计业务部门的同学肯定是跟不上我们的思路的,这样的沟通通常无法正常地进行下去,所以就会经常出现业务同学说业务的事情,技术同学说技术的事情,两边不能达成一致,矛盾就产生了。 + +这里需要强调的一点是,对于绝大多数的公司来说,业务一定是最重要的,技术是实现业务功能的一种手段和方式,所以一定是从业务角度出发考虑技术解决方案,而不是从技术角度出发让业务来适配技术。 + +那怎么从业务角度出发呢?就是我刚说的尝试用业务语言去沟通,用对方能够听得懂的表达方式去表达你的技术观点。为了让业务人员理解你的想法,就自然会用业务的思路去思考和解决问题了。这个需要一点点改变,可以先从尝试开始。 + +2. 学会挖掘问题背后的真正诉求 + +外部提出的一个问题,可能并不一定是真正的问题,而是问题的一个解决方案。 + +先举个之前我遇到的例子,有个部门给我们提了一个在服务器上安装翻墙软件的需求,结果我们的工程师就照着这个需求去做了,最后发现软件怎么调都启动不了,中间还牵扯到网络同事的配合,需要检查网络上的配置,最后就差动网络设备了。 + +后来我就去问,为什么要安装翻墙软件呢?一问才知道,有个业务需求是需要爬取Twitter、Instagram和Facebook上一些时尚达人的时间线信息的,需要部署一个这样的应用,然后能够对外访问,但是部署在我们机房内部发现不行(肯定不行啊),所以就建议尝试装一个翻墙软件看看是不是能访问出去。 + +这么一问,就发现安装翻墙软件不是真正的需求,真正的需求是能够访问到海外站点。看问题的角度不同,解决方案也就不一样了。 + +因为我们有公有云的海外节点,这样的需求,我们直接将应用部署在海外节点就可以了,然后从申请资源、部署上线到调测通过,30分钟搞定。 + +这种情况非常常见,也是日常团队协作中最容易出现的问题,很多矛盾都是因为这种原因导致的。如果按照上述不假思索的方式去做,极有可能是没有结果,或者是结果无法让人满意。如果你很努力很认真地做了很多事情,但却无法得到对方的认可,那就太令人沮丧了。 + +遇到类似问题,可以不着急动手做,先多问自己和对方几个问题,比如: + + +为什么要这样做? +谁要求做这件事情的? +这样做的目的是什么? +这样做是为了解决什么问题? + + +这一点其实也是站在对方角度去考虑,去思考对方要解决的问题是什么,而不是解决我们的问题。通常情况下,两三个问题后,一般就会暴露出背后最原始的那个需求了。正所谓“磨刀不误砍柴工”,问题和背景搞清楚了,思路和方案就是顺其自然的事情了。 + +3. 解决问题的时候关注目标,而不是聚焦困难 + +我尝试写了一段话想来分享我的观点,但是读来读去感觉有点太鸡汤。所以还是上一张图,这个是我16年去腾讯交流的时候,在腾讯办公区拍到一张照片,对我启发很大。 + +两种不同的思考问题的方式,带给人的感受也是完全不一样的。 + +道理还是需要我们自己悟明白的,所以文字也好,图片也罢,期望对你也有所启发。 + + + +近些年,随着云计算技术的深入发展,公有云事业也不断拓展,运维领域的分工也在不断地精分细化,而每个细分领域对专业技术的要求也越来越高,专业的服务化程度也越来越高。我想这是一个好现象,让原来非常模糊的运维行业范畴变得越来越清晰、越来越具体。 + +对于我们运维来说,这样的发展既是机遇,也是挑战。一方面我们要不断提升自己的技术能力,另一方面也要注意自身服务意识的培养,让自己的能力得以发挥,创造更大的价值,获得更好的回报。 + +对于今天的内容你有怎样的共鸣和思考,欢迎你留言与我一起讨论。 + +如果今天的内容对你有帮助,也请你分享给身边的朋友,我们下期见! + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/12持续交付知易行难,想做成这事你要理解这几个关键点.md b/专栏/赵成的运维体系管理课/12持续交付知易行难,想做成这事你要理解这几个关键点.md new file mode 100644 index 0000000..dd788af --- /dev/null +++ b/专栏/赵成的运维体系管理课/12持续交付知易行难,想做成这事你要理解这几个关键点.md @@ -0,0 +1,88 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 12 持续交付知易行难,想做成这事你要理解这几个关键点 + 前面几篇文章,我们介绍了非常基础的运维建设环节。如果我们想要这些运维基础建设发挥出更大的作用和价值,就需要针对运维场景进行场景化设计和自动化,让效率和稳定性真正提升上来。也就是说,把基础的事情做好之后,我们就要进入效率提升的运维场景自动化阶段了。 + +在这一阶段,我个人的经验和建议是,首先要把持续交付做好。 + +为什么要先做持续交付?如果说我们完成了一些运维职责范围内的自动化工具,提升的是运维效率的话,那么,做持续交付就是提升整个研发体系效率的关键。 + +做持续交付的价值表现在哪里? + +持续交付覆盖了应用的整个生命周期,涉及产品、开发、测试、运维以及项目管理等相关方面。从生命周期出发,自然就会牵出整个自动化的全貌,就会有从全局着眼的规划设计,这时无论是在开发还是运维过程中存在的问题,都会完完整整地暴露出来。那么,应该以什么样的主线开展?各方应该如何配合?应该以怎样的优先级明确任务?这些问题就都清楚了。同时,也避免了各个环节只把注意力放在各自职责范围内的事情上,而忽略了整体的配合。所以,做好持续交付,对于整个研发体系意义重大。 + +我们面临的实际场景是怎样的? + +我们知道,随着业务复杂度的升高,不管是分层架构,还是微服务架构,都会带来一个最明显的变化,那就是应用数量增多,有时甚至多达几十个、上百个。不同的应用就有不同的代码、依赖和配置,为了协同多应用之间的在线发布,我们还要做到服务能够平滑地进行上下线切换。同时,为了最大限度地降低发布风险,我们还需要进行多环境下的验证,以及上线后的灰度策略等等。 + +应对这一切,如果只是手工维护,或者利用简单的工具脚本进行维护,都不能保证正常运作。这个时候,我们必须有一系列的流程、机制和工具链来支持和保障。 + +由杰斯·赫布尔(Jez Humble)、戴维·法利( David Farley)编著,乔梁老师翻译的《持续交付:发布可靠软件的系统方法》(Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation)这本书,针对持续交付的过程、方法和指导建议几个方面做了非常详细的描述。我向你强烈推荐这本书,不过,这本书的内容并不仅仅针对于微服务架构。 + +接下来我就分享如何把持续交付的理念和实践相结合,讲一讲在实践过程中,做好持续交付最关键的几步是什么,以及具体应该怎么做。 + +什么是持续交付? + +我们现在经常会接触到这些名词,比如持续交付、持续集成、持续部署、持续发布、DevOps和敏捷开发等等。大多有关持续交付的分享或文章中,这些名词经常会同时出现。它们之间到底有什么区别?我自己之前也弄不清楚,直到看到乔梁老师的这张图。 + + + +这里我重点解释一下持续交付这个概念,通俗地说,持续交付代表着从业务需求开始到交付上线之后的端到端的过程。后面我们会针对这个过程的关键环节进行分享。 + +受篇幅所限,其它名词就先不作过多解释了,你可以看图自己体会,都不难理解。后面针对某些概念我们还会提到。 + +持续交付的关键点 + +可以说,这一部分也是我们后面将要分享内容的提纲。 + +从前面那张图来看,做持续交付需要端到端考虑,同时还要有一些非常关键的准备工作。我把这些工作大致分为以下几个部分。 + +1. 配置管理 + +这一部分会利用到我们前面讲过的标准化和CMDB打下的基础,同时还会有更大的外延,比如环境配置、代码配置以及依赖管理等等。 + +配置管理是非常关键的基础工作。有一点值得注意,那就是标准化是一个持续的过程。我们不太可能在一开始就把所有运维对象、属性和关系全部都考虑清楚,面面俱到是不太现实的,所以,一定要具备标准化的意识,在开展运维工作的过程中,持续不断地用这个思路去标准化新出现的对象。先标准,再固化,然后自动化。 + +2. 需求拆解 + +需求拆解这个工作跟业务需求部门和业务开发有更直接的关系。在这里,运维需要做的是,明确需求拆解的粒度和我们最终发布上线的粒度相匹配。 + +3. 提交管理 + +需求拆解完成后,就进入到开发阶段,开发完成后向代码库中提交代码,这个过程中代码分支的合并策略选择就是提交管理。 + +4. 构建打包 + +这一部分是指将提交后的代码编译成可发布的软件包。 + +5. 自动化测试 + +自动化测试包括功能测试和非功能性测试。对于运维来说,会更注重非功能方面的特性,所以后面我会着重讲非功能性相关的测试环节。 + +6. 部署发布 + +这一部分是指发布到不同的环境,如开发环境、预发环境、线上Beta以及线上全量环境。针对不同的环境,发布策略和注意事项也会不同。 + +以上是一个完整的持续交付过程中最重要的几个环节,后面我们分篇进行详细介绍。 + +从我自己的实践经验来看,配置管理、提交管理、构建和部署发布是持续交付的重中之重,是关键路径,是从开发代码开始,到发布上线的必经之路。当时,因为这几个环节出现了问题,不能解决,运维同学经常做手工发布,这样效率就跟不上,还经常出现各种问题。 + +后来,我们就是先从这几个环节入手,把阻塞的问题解决掉,然后在这个主流程上不断增加外围能力,让整个流程的功能更加丰富和全面。整个系统也从原来的只具备持续部署发布功能的平台,逐步演进为具有持续交付能力的平台。由此可见,我们实现持续交付的过程,也不是一蹴而就的,而是在摸索中逐步演进完善的。 + +最后,给你留两个思考题。 + + +先放下持续交付的概念,你所理解或经历的从开发完代码到发布到线上这个过程中,会有哪些环节?和我列出来的这几部分是否有相同之处? +持续交付是谁的持续交付,它的主体是谁?或者有哪些主体? + + +欢迎你留言与我讨论。 + +如果今天的内容对你有用,也欢迎你分享给身边的朋友,我们下期见! + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/13持续交付的第一关键点:配置管理.md b/专栏/赵成的运维体系管理课/13持续交付的第一关键点:配置管理.md new file mode 100644 index 0000000..811ae98 --- /dev/null +++ b/专栏/赵成的运维体系管理课/13持续交付的第一关键点:配置管理.md @@ -0,0 +1,101 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 13 持续交付的第一关键点:配置管理 + 今天我们来看持续交付的第一个关键点:配置管理。按照持续交付的理念,这里所说的配置管理范围会更广,主要有以下几个部分。 + + +版本控制 +依赖配置 +软件配置 +环境配置 + + +讲持续交付,一上来就先讲配置管理,主要还是想强调:配置管理是基础,是关键。我们后面将要讲的每一个持续交付环节,都对配置管理有很强的依赖。这个基础工作做不好,也就谈不上的持续交付的成功。勿在浮沙筑高台,我们做工具平台或系统,一定要重视基础的建设。 + +同时,这里还有一个前提,就是一定要做到代码和配置的分离。不要让配置写死在代码里,需要依靠严格的规范和约束。同时,对于那些因历史原因遗留在代码中的配置,要多花些时间和精力把配置剥离出来,做这项工作没有什么好的方法或经验,只能多上心,多投入些精力。 + +配置管理中,对于版本控制和依赖配置目前都有比较成熟的工具体系支持,也有丰富的实践经验供我们参考学习,下面我会做一个简要的介绍。 + +对于软件配置和环境配置管理,这两项配置跟我们自身的业务软件特性强相关,是整个持续交付过程的关键,我会结合我们自身的实践经验进行重点介绍和分享。 + +版本控制 + +版本控制的主要作用是保证团队在交付软件的过程中能够高效协作,版本控制提供了一种保障机制。具体来说,就是团队在协作开发代码的情况下,记录下代码的每一次变更情况。 + +说到这里,你是不是想到了SVN和Git这样的版本管理工具?对,其实我们每天都在接触,每天都在不停地做这个事情,所以目前看来这是一件很平常的事情。 + +关于这一部分我在后面的文章里会介绍关于提交阶段的实践经验。这里我们只要知道,版本控制及其工具是必不可少的,因为这是开发团队协作最基础的工具。现在应该很少有团队不采用版本控制的管理机制吧? + +依赖管理 + +这里以Java为例,我们使用Java进行开发,必然会依赖各种第三方的开源软件包。同时,内部还会有不同组件的二方包。这些三方包和二方包就是一个应用编译和运行时所依赖的部分。 + +有过开发经验的同学肯定都知道,即使运行一个非常简单的Web应用,都会有大量的jar包依赖。如果人工去管理这些依赖,基本上是不可能的,所以就需要有依赖管理的工具。 + +对于Java来说,我们熟知的依赖管理工具有Ant、Maven和Gradle。当然这些工具不仅仅提供依赖管理这样单一的能力,一般都具备以下几个能力: + + +二方包、三方包的仓库(Repository)管理; +依赖管理; +构建打包。 + + +下面介绍下我自己的实践经验。因为我们的经验基础都在Maven上,再加上Maven周边有一些优秀插件和业界经验可以借鉴,比如后面将要介绍到的AutoConfig,所以我们选取了Maven作为主力构建工具。 + +大致用法是建立一个本地Maven源,构建时会优先从本地源中获取依赖包,本地源中没有对应的依赖时,会从公网上下载,同时缓存到本地。这也是业界绝大多数公司采用的一种通用方案。具体如何构建打包呢?这个内容会在构建阶段进行分享。 + +软件配置 + +这里我把软件配置细化为两类:一类是代码配置,一类是应用配置。 + +1. 代码配置 + +我们可以这样理解,代码配置是跟代码运行时的业务逻辑相关的。比如应用的服务接口、并发线程数、超时时间等这些运行时参数;还有类似于业务或技术上的开关,比如商品评论是否开放、优惠时间段设置等等。 + +2. 应用配置 + +还记得我们在标准化文章中提到的应用吗?应用配置就是应用这个对象的属性和关系信息。我们把应用配置放到持续交付这个场景中进行分析,对于这个配置可以细分为: + + +应用构建时配置,比如它的编程语言、Git地址以及构建方式等; +应用的部署配置,源代码目录、应用日志目录、Web日志目录、临时目录、脚本目录等; +应用的运行配置,应用启停、服务上下线方式、健康监测方式等; +应用运行时与基础组件的关联关系,比如其依赖的DB、缓存、消息以及存储的IP地址、域名、端口、用户名或Token等。 + + +从上面这种分类方式中,应该可以体会到,我们对于配置的分类,也是基于应用生命周期的不同阶段进行分解和分析的。所以,标准化的过程也是一个持续迭代的过程。不同的场景下,一个应用可能会具备不同的属性。这个时候,如果我们无法在一开始就把这些属性梳理得清清楚楚,具备标准化的意识和思路就显得更为重要。这样,当我们遇到新场景的时候,随时可以对它做标准化分析和建模。 + +3. 代码配置和应用配置的区别 + +从上面的分析中,你有没有找出两者的区别?这里建议你暂停一下,花一分钟时间自己先想想代码配置和应用配置有什么区别,再往下看。 + +从区别上讲,我们可以认为代码配置是跟业务或代码逻辑相关的,动一下就会改变系统执行状态,是运行时的配置,但不依赖周边环境。而应用配置,是跟业务和代码逻辑无关的,不管你怎么动,业务逻辑是不会改变的,但是它跟环境相关。 + +与环境相关,按阶段分又大致可以分为两个阶段、三种情况。 + + +第一种,软件在交付过程中,环境会不一样。比如我们正式发布软件前,会历经开发测试环境、预发环境和生产环境等等。那开发测试环境访问的DB,跟线上访问的DB就不能是同一套。同时这个环境中的应用,依赖的大多是本环境内的基础组件和应用,但不是必然,原因我们后面会讲到。还有日志级别也可能不同,比如测试环境可以开Debug级别,但是线上是绝对不允许开Debug的。 +第二种,软件交付上线后,线上可能会存在多机房环境,特别是有海外业务的公司,一个站点可能会在中国、北美、欧洲以及东南亚等不同区域建立当地访问的分站点;或者大型网站做了单元化,在国内也会分多机房部署,这个时候每个机房的环境配置必然不同。 +第三种,软件交付后,一套软件可能交付给不同的客户,分别独立运行,比如类似ERP、CRM这样的软件,或者私有部署的SaaS服务等。不同客户的基础环境是不一样的,有的可能是Linux,有的是Unix,还有的可能是Windows,这时应用配置中的各种目录、用户名等信息可能也是不一样的,软件的交付模式就取决于最终的客户环境。 + + +对于平台类的产品,遇到第一、二种情况的可能性更大,这两种情况更多的是对周边依赖的配置不同,比如不同的服务注册中心、DB、缓存或消息等等。对于一些针对不同客户进行私有部署的产品,可能更多的是第三种情况,这种情况就是应用的基础配置比如目录、用户名以及基础软件版本等会有不同。 + +我们回到代码配置和应用配置之间的区别这个问题上来。 + +对于代码配置,我们一般会通过Config Server这样专门的配置管理服务进行动态管理,在软件运行过程中可以对其进行动态调整。之所以增加这些配置,主要是让开发能够以更灵活的方式处理业务逻辑。当然,也有一些为了稳定性保障而增加的配置,比如限流降级、预案开关等等。对于前者运维不必关注太多,而后者是运维关注的重点,这个内容我们后面讲到稳定性部分会重点分享。 + +对于应用配置,是我们在构建软件包时就需要面对的问题。这个配置取决于环境,所以就延伸出持续交付过程中非常重要的一个配置管理:环境配置管理。解释一下就是,不同环境中的应用配置管理。 + +环境配置是我们在持续交付过程中要关注的重中之重,也是最为复杂的一部分。我们自己的团队在做多环境发布和管理的时候,遇到最头疼的问题就是环境配置管理,我们下一期就着重来聊聊环境的配置管理。 + +今天我和你分享了做持续交付的第一步:配置管理,主要包括版本控制、依赖配置、软件配置和环境配置四个部分。关于今天分享的内容,你有怎样的思考或疑问,欢迎你留言与我讨论。 + +如果今天的内容对你有帮助,也欢迎你分享给身边的朋友。我们下期见! + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/14如何做好持续交付中的多环境配置管理?.md b/专栏/赵成的运维体系管理课/14如何做好持续交付中的多环境配置管理?.md new file mode 100644 index 0000000..eec6c24 --- /dev/null +++ b/专栏/赵成的运维体系管理课/14如何做好持续交付中的多环境配置管理?.md @@ -0,0 +1,138 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 14 如何做好持续交付中的多环境配置管理? + 上一篇内容中,我们讲到软件配置中的代码配置和应用配置,这两种配置之间最大的区别就是看跟环境是否相关。由此,就引出了持续交付过程中最为复杂的环境配置管理这个问题,准确地说,应该是不同环境下的应用配置管理。 + +今天我就结合自己的经验和你聊一聊环境管理的解决方案。 + +多环境问题 + +上篇内容我们介绍了应用配置的三种情况,今天我们稍微聚焦一下,以上篇文章中提到的前两种应用配置场景为主进行介绍,也就是平台类的业务。我们一起来看同一套软件在持续交付过程中和交付上线后,多环境情况下的配置管理问题。 + +我们先从生命周期的角度,对环境做个简单说明,主要包括: + + +开发环境,主要是在应用或软件开发过程中或完成后,开发人员对自己实现的代码进行单元测试、联调和基本的业务功能验证; +集成环境,开发人员完成代码开发并自验证通过后,将应用软件发布部署到这个环境,测试人员再确保软件业务功能可用,整个业务流程是可以走通的; +预发环境,在真实的生产数据环境下进行验证,但是不会接入线上流量,这也是上线前比较重要的一个验证环节; +Beta环境,也就是灰度环境或者叫金丝雀发布模式。为了整个系统的稳定性,对于核心应用,通常会再经历一个Beta环境,引入线上万分之一,或千分之一的用户流量到这个环境中; +线上环境,经历了前面几个阶段的业务功能和流程验证,我们就可以放心地进行版本发布了,这个时候就会将应用软件包正式发布到线上 。 + + +以上是一个持续交付体系中必须要有的几个环境。环境建设,又是一个比较大的话题,我们后面会专门来讲,今天就聚焦在环境配置管理上。 + +不同环境下的应用配置管理 + +我们前面提到,环境配置管理,解释得更准确一点,应该是不同环境下的应用配置管理,所以这里的核心是应用配置管理。因为有多个环境,所以增加了其管理的复杂性。持续交付过程中涉及到应用配置管理的属性和关系如下: + + +应用属性信息,比如代码属性、部署属性、脚本信息等,你可以参考之前我们对这块的细分,这里就不细讲了; +应用对基础组件的依赖关系。这个也不难理解,应用对DB、缓存、消息以及存储有依赖,不同的环境会有不同的访问地址、端口、用户名和密码。另外,在分布式架构中,一个应用必然要依赖一个环境中的其它应用,这时对于应用的服务注册、服务发现也要求必须在本环境中完成。举一个最简单的例子,我们肯定不允许一个线上应用服务注册到线下环境中去,让线下业务测试的服务调用影响到线上服务运行,要不然就会导致线上的业务故障了。 + + +讲到这里,你应该会发现,对于我们假设的平台类业务场景,应用的基础属性信息是不会随着环境的变化而发生变化的,但是应用依赖的基础设施和依赖这个关系是会随环境不同而不同的。所以,我们可以再进一步理解,环境配置管理主要是针对应用对基础设施和基础服务依赖关系的配置管理。 + +如果是针对不同的客户进行私有化部署的软件,那么应用的基础属性信息可能也会发生变化,但是这样的场景就更会更加复杂一些,但是配置管理上的解决思路上是一样的,所以这里我们还是简化场景。 + +环境配置管理解决方案 + +上面详细讲解了环境和应用配置之间的管理,下面就结合我自己团队和业界的一些方案,来看看这个问题应该怎样解决。 + +我们的示例尽量简化场景,重点是讲清楚思路。所以我们假设现在有三个环境: + + +开发环境 +预发环境 +线上环境 + + +继续假设某应用有配置文件:config.properties,里面存储了跟环境相关的配置,简化配置如下: + + +缓存地址:cache.app.url +消息地址:message.app.url +数据库地址:db.app.url +支付调用地址:pay.url +支付调用Token:pay.app.token + + +很明显,不同的环境,配置是不完全相同的。我们看以下几个解决思路。 + +方案一,多个配置文件,构建时替换。 + +这是一个比较简单且直接有效的方式,就是不同环境会分别定义一个配置文件,比如: + + +开发环境dev_config.properties +预发环境pre_config.properties +线上环境online_config.properties + + +这几个配置文件中的配置项保持相同,但是配置的值根据环境不同是不一样的,不过都是固定的实际信息。比如开发环境配置文件中的缓存地址: + + +cache.app.url=10.88.77.66 + + +而线上环境配置文件中: + + +cache.app.url=10.22.33.44 + + +然后在构建时,我们会根据当前所选定的环境进行替换。比如,我们现在构建开发环境的软件包,这时就会选择dev_config.properties作为配置文件,并将其文件名替换为config.properties打包到整个软件包中。 + +我们看下这种方案的优缺点: + + +优点,就是简单直接。在环境相对固定且配置项变化不大的情况下,是最为简便的一种环境配置管理方式。 +缺点,也比较明显。首先是在实际的场景中,我们的环境可能会更多,且交付上线后可能还会有线上多环境。这时每多出一个环境,就要多一个配置文件,这时配置项的同步就会成为大问题,极有可能会出现配置项不同步,配置错误这些问题。特别是如果配置项也不断地增加和变化,管理上会变得非常繁琐。再就是,这里需要针对不同环境进行单独的构建过程,也就是要多次打包,这一点是跟持续发布的指导建议相悖的。 + + +方案二,占位符(PlaceHolder)模板模式。 + +这种方案在Maven这样的构建工具中就可以很好地支持,直接示例如下: + + +cache.app.url=${cache.app.url} + + +我们可以看到,这种模式下,配置项的值用变量来替代了,具体的值我们可以设置到另外一个文件中,比如antx.properits(这个文件后面在autoconfig方案中我们还会介绍),这里面保存的才是真正的实际值。 + +这时我们只需要保留一个config.properties文件即可,没必要把值写死到每个不同环境的配置文件中,而是在构建时直接进行值的替换,而不是文件替换。这个事情,Maven就可以帮我们做,而不再需要自己写脚本或逻辑进行处理。 + +不过,这个方案仍然不能很好地解决上面第一种方案提到的问题,配置文件是可以保留一个了,但是取值的antx.properties文件是不是要保留多个?同时,对于配置文件中配置项增加后,antx.propertis文件中是否同步增加了配置,或者配置项名称是否完全匹配,这一点Maven是不会帮我们去检查的,只能在软件运行时才能验证配置是否正确。 + +最后,这个方案还是没有解决只打包一次的问题,因为Maven一旦帮我们构建完成软件包之后,它并没有提供直接针对软件包变更的方式。所以,针对不同的环境,我们仍然要打包多次。 + +方案三,AutoConfig方案。 + +AutoConfig是阿里开源的Webx框架中的一个工具包,在阿里的整个持续交付体系中被广泛应用,它继承了Maven的配置管理方式,同时还可以作为插件直接与Maven配合工作,针对我们上面提到的部分问题,它也针对性地进行了加强和改进,比如: + + +配置校验问题。AutoConfig仍然是以上述第二种方案的模板模式为基础,最终通过antx.properties文件中的配置值来替换,但是它会做严格校验;同时也可以自定义校验规则,来检查配置项是否与模板中的设定严格匹配,如果不匹配,就会在构建时报错,这样就可以让我们提前发现问题,而不是软件包交付到环境中运行时才发现。 +只打包一次的问题。AutoConfig不需要重新构建就可以对软件包,比如war包或jar包的配置文件进行变更,所以它很好地解决了针对不同环境需要重复构建的问题。但是,比较遗憾的是,它的Maven插件模式并没有解决这个问题,还需要与AutoConfig工具模式配合才可以。 + + +讲到这里,我们可以看到AutoConfig的方案已经相对完善了,也可以满足我们的大部分需求,但是它仍然没有解决多环境配置值管理的问题,我们是通过多个antx.properties文件来管理,还是有其它方式? + +这里,我们就需要基于AutoConfig做一下二次开发了,也就是我们要把这些配置项做到一个管理平台中,针对不同环境进行不同值的管理,然后根据AutoConfig的规则,在变更后生成对应不同环境的配置文件,然后再结合AutoConfig针对配置管理文件的能力,这样就可以很方便地做多环境的软件包构建了。 + +这样的配置项管理平台,AutoConfig自己也没有做,所以需要我们自己开发。同时,对于比较敏感的配置信息,特别是涉及用户名、Token、核心DB地址等信息,还是不要放到配置文件中,最好是放到管理平台中,进行受控管理。同时,这里也要特别强调,密码信息一定不允许放在配置文件中出现,更不允许以明文的方式出现,这一点是需要开发、运维和安全共同来保障的。 + +总结 + +今天我们针对多环境的配置管理进行了分享,这里更多的还是思路和方案上的引导。如果你对Maven和AutoConfig不熟悉的话,建议自行查询资料进行学习了解,甚至是自己动手尝试一下。 + +另外,对于文章中的方案,我是尽量简化了场景来分享的,虽然思路上是相通的,但是实际情况下各种细节问题会更繁琐,要具体问题具体分析。 + +你在这个过程中遇到过什么问题?有什么好的解决方案?或者还有什么具体的疑问?欢迎你留言与我一起讨论。 + +如果今天的内容对你有帮助,也欢迎你分享给身边的朋友,我们下期见! + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/15开发和测试争抢环境?是时候进行多环境建设了.md b/专栏/赵成的运维体系管理课/15开发和测试争抢环境?是时候进行多环境建设了.md new file mode 100644 index 0000000..7d36b3a --- /dev/null +++ b/专栏/赵成的运维体系管理课/15开发和测试争抢环境?是时候进行多环境建设了.md @@ -0,0 +1,125 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 15 开发和测试争抢环境?是时候进行多环境建设了 + 在上一期文章里,我们介绍了多环境下的应用配置管理问题,从这期开始,我们会分两期文章详细聊聊多环境建设的问题:就是我们到底需要哪些环境?这些环境都有什么作用?环境建设的思路和方式是怎样的? + +今天我就结合自己的经验和理解与你聊一聊持续交付中的线下多环境建设。 + +环境分类 + +通常,我们主要按照环境所起到的作用,将环境分为两大类: + + +线下环境:测试验收用。 +线上环境:为用户提供服务。 + + +从建设角度来说,线下环境和线上环境,在网段上是要严格隔离的。这一点在做环境建设时就要确定网络规划,同时在网络设备或者虚拟网络的访问策略上要严格限定两个环境的互通,如果限制不严格,就极易引起线上故障,甚至是信息安全问题。 + +如果你维护过这样两套环境,我想你一定在这方面有过深刻的感受,甚至是痛苦的经历。 + +所以,从规划上,线上环境和线下环境是两套独立的区域,所有的应用、基础服务都是全套独立部署的。但是线下环境所需的资源往往是要少于线上环境,毕竟只有负责开发测试的少数人使用,不会有线上流量进来。如下图所示: + + + +但是,在实际情况中,这两个环境远远满足不了我们日常开发、测试和运维方面的需求。从保障软件质量和系统稳定的角度出发,我们在实际操作中还需要在这两个大的环境区域中,建立细分的小环境,来满足不同阶段和不同角色的工作需求。 + +线下环境分类建设 + +线下环境最初建设的时候,主要是提供给测试使用,帮助其建立一个模拟环境,在软件发布上线前进行需求功能验证,保障业务流程顺畅,以确保应用在上线前达到最低质量要求。 + +所以,我们在线下环境区域内,建设的第一个环境就是集成测试环境。甚至在一开始,线下环境=集成测试环境,这个环境下的应用和各类基础服务必须跟线上保持一致,但是集群规模不用这么大(如我们上图所示)。 + +所以,集成测试环境极其重要,这个环境中的应用有严格的发布标准,并且要求环境稳定,不能随意发生变更,否则将会大大影响测试的效率。 + +不过,随着集成测试环境建设起来,业务需求迭代越来越快,应用和开发人员数量也越来越多,软件发布和变更也会更加频繁,这个时候就会出现开发和测试人员争抢集成测试环境的问题。 + +比较典型的场景就是,测试人员正在验证一个功能,突然发现应用停止运行了,原来开发为了验证和尽快发布新功能,更新了代码,这样就阻塞了测试的正常工作,但是不更新代码,开发的工作又会停滞下来。 + +后来这个矛盾越来越严重。这时,我们就需要考虑多建设一套给开发用的环境来解决这个问题。 + +于是,我们就开始建设线下的第二套环境:开发测试环境。这个环境主要是让开发同学能够尽快发布自己开发完的代码,并在一个具备完整业务应用和基础服务的环境下,验证自己的代码功能。 + +但是,是否需要跟集成测试环境一样,再建设一套独立完整的线下环境出来呢?答案是否定的。因为这时的应用变化范围相对独立,变化也较小,周边依赖需要同时变化的应用也不会太多,就像上面说的,只要能把它们放到一个完整的环境中进行验证即可。 + +所以,这个环境只要按照最小化原则建设即可,如果有依赖,可以直接访问到集成测试环境。在这里,我们以简单的模型展示开发测试环境跟集成测试环境的关系: + + + +再往后,开发测试环境上,又会出现开发和测试的冲突和争抢,因为从场景上,业务开发团队可能要同时承担多个并行项目的研发,而且可能会有多个业务开发团队一起参与进来。 + +比如对于电商来说,到了年底,就集中会有“双11”、“双12”、“双旦节”以及“年货节”等等这样的大型营销项目,因为时间非常紧凑,所以就必须多项目并行。 + +这个时候,分解下来,对于我们的应用软件来说,有可能是存在多个开发分支的。到了项目联调和验证环节,就必然会存在同一个应用有多个版本需要同时发布和测试的情况,但是开发测试环境却只有一个,这就必然导致双方激烈的争抢。 + +所以这个时候,就必须建立解决冲突的方案,开始建设线下的第三套环境:项目环境。 + +项目环境可能有多套,一个项目对应一套环境,但是无论从资源成本还是维护成本方面考虑,项目环境仍然不会像集成测试环境那样形成一套完整的开发测试体系。 + +所以项目环境同开发测试环境一样,仍然是以最小化为原则来建设,也就是说,在这个环境里面,只部署同一项目中涉及变更的应用,而对于基础服务和不涉及项目需求变更的应用不做重复建设。如果对项目环境中不存在的应用有依赖,那么访问集成测试环境中对应的应用就可以了。 + +在这里,我们同样以简单的模型展示多个项目测试环境、开发测试环境与集成测试环境的关系: + + + +不过,如果说随项目的增加就需要分别建设对应的项目环境,那么这对于开发、测试和运维来说都会有非常大的维护负担。所以通常情况下,我们会严格限制建设项目环境的起步线。 + +比如只有公司级大促、公司战略级的项目,或者超过一定人日的跨团队项目,才允许建立独立的项目环境。一般情况下,还是引导优先使用开发测试环境。 + +环境建设上的关键技术点 + +线下环境细分出集成测试环境、开发测试环境以及多个项目环境之后,带来的最大的成本其实不在资源上,而是在管理和维护上,而且单单就线下维护的工作量来说,甚至要超过线上维护的工作。 + +复杂度和涉及到的技术点有以下四个方面。 + +第一是网段规划。每个环境都要有独立的网段,比如整个线下环境要独立占用一个B段,项目环境和开发测试环境相对较小,可以独立占用一个C段。虽然不需要做网络策略上的隔离,但是为了便于管理,如分配回收资源以及部署应用,还是要在逻辑上区分出来。同时,网段规划也是为下面的单元化调用做准备。 + +第二是服务化框架的单元化调用。这一点需要服务化框架支持,比如上面我们提到的项目环境,到了联调阶段就需要一组应用单独联调,而不能跨项目环境调用。同时,对于项目中依赖的未变化的应用,就需要调用集成测试环境中稳定版本的应用。这个服务调用的基本规则就是基于上述网段的规划来建立的,规则要放到服务化的注册中心,也就是ConfigServer这个部件中保存,同时需要服务化框架支持规则调用,优先支持本单元调用,本单元不存在可以调用集成测试环境单元。 + +第三是环境的域名访问策略。这么多的环境 ,内外部DNS域名是保持一套还是多套,比如访问蘑菇街主页(www.mogujie.com),首页域名就一个,但是怎么能确保访问到我们所期望的环境上呢。这里有几种方式: + + +第一种,DNS服务保持一套,域名,特别是外部域名多套,每个环境分别配置一个不同的域名,比如开发测试环境,dev.mogujie.com。但是如果这样,下层的二级域名和二级目录等等都要跟着变动,而且对于项目环境来说,数量不固定,每次都换一个也不方便记忆和管理,所以这个方案基本不可行。 +第二种,DNS服务保持一套,域名保持一套,但是做域名的hosts绑定,也就是自己来设置要访问域名所对应的环境。这样一来,如果相对固定的开发测试环境和集成测试环境所对应的hosts相对固定,那么只需要绑定一次就可以通用。但是项目环境始终在不断的变化中,绑定规则可能随时在变化,所以这种方案的复杂度在于对hosts配置的管理上。 +第三种,DNS服务多套,也就是不同的环境中配置独立的DNS服务,这样可以减少繁琐的hosts绑定,但是也会提高多套DNS服务管理上的复杂度,而且对于项目环境有可能依赖集成测试环境中的服务,这时仍然会涉及本地DNS服务的hosts绑定。 +第四种,对于公网域名,可以直接通过无线路由劫持的方式访问,可以在无线路由器上配置多个接入点,这样一来,连接不同的接入点,就会自动对应到不同环境的域名IP地址上去。 + + +通常,对于公网域名来说,如果具备稳定的环境,如集成测试环境,直接通过第四种劫持方式指定到对应环境中去,这样最方便,这种方案在后续线上多环境建设中还会使用。 + +对于内部域名,则有多种方案,没有优劣,主要看场景和管理成本。我们选择的是第二 种,即绑定hosts的方式。每个环境会对应一套hosts配置,当选择不同环境进行联调或访问时,就将hosts配置下发到对应部署应用的主机上。 + +在这里,我们仍然以模型展示第二、四种方案和多种线下环境之间的运行逻辑: + + + +但是,无论采取哪种方式,我们可以看到,这个管理过程仍然是比较复杂繁琐的,必须要非常仔细地规划和部署,同时还需要配套的自动化工具支持,否则靠人管理是不现实的,所以最后一点就是自动化的配套。 + +第四是自动化管理。按照我们之前分享的管理模式,这里虽然有这么多的线下环境,但我还是会把它们以应用为核心给管理起来,即应用的开发测试环境,应用的集成测试环境,应用的项目环境。这样一来,上面提到的不同环境的网段信息、配置信息、资源分配回收以及软件部署发布,都能够以应用为出发点去做,这一点我们在后面的文章会详细分享。 + +总结 + +最后,不知道你有没有感受到,单单一个线下环境就要如此复杂和繁琐,整个持续交付体系建设是多么的有挑战性,就可想而知了。 + +我们对线下环境稍微做个总结: + + +集成测试环境,主要供测试使用,这个环境会最大程度与线上版本保持同步。作为对应用的功能、需求、业务流程等在正式发布上线进行验证的主要环境,集成测试环境的稳定性和可测试需要加强保障,进行全套建设。同时,作为整个线下环境的中心节点,也要为开发测试环境和项目环境提供部分依赖服务。 +开发测试环境,主要供开发人员使用,针对偏日常的需求开发、联调和功能验证,以最小化原则进行建设,但是一般情况下不对资源进行回收。 +项目环境,供开发和测试共同使用,针对多团队多人员协作的项目型场景,可以同时存在多个项目环境,在这个环境中针对项目需求进行独立开发、联调和验证。测试也需要提前介入这个环境进行基本功能的验收,并遵循最小化的建设原则,但是要有生命周期,即项目启动时分配资源,项目结束回收资源,不能无限期占用。 + + +最后,在实际操作中,仍然会很多细节问题,这些问题会跟业务场景有关,针对这些场景又有可能有不同的建设要求,比如消息的消费问题会涉及全业务流程验证等等。 + +所以,留几个问题给你思考:在线下环境的建设过程中,你通常会遇到哪些问题?会有哪些独立环境?或者你有什么更好的经验和建议分享? + +欢迎你留言与我讨论。 + +如果今天的内容对你有帮助,也欢迎你分享给身边的朋友,我们下期见! + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/16线上环境建设,要扛得住真刀真枪的考验.md b/专栏/赵成的运维体系管理课/16线上环境建设,要扛得住真刀真枪的考验.md new file mode 100644 index 0000000..e311121 --- /dev/null +++ b/专栏/赵成的运维体系管理课/16线上环境建设,要扛得住真刀真枪的考验.md @@ -0,0 +1,139 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 16 线上环境建设,要扛得住真刀真枪的考验 + 前面几期我们分享了一些线下环境建设方面的内容,我们可以感受到,整个线下环境的建设是比较复杂的,那经过线下环境的验证,是不是就可以直接发布到线上生产环境了呢?答案同样是否定的,由线下正式交付到线上之前,我们仍然会做很多的验证和稳定性保障工作。 + +今天我们就一起来看一下线上环境是如何建设的。 + +下面,我们就生产环境、Beta环境、预发环境、办公网生产环境这四种线上环境分别展开讨论。 + +生产环境 + +我们还是进入到现实场景中。最初我们的软件代码开发完成后,就可以发布到生产环境,也就是可以正式接入用户流量,承载真实的业务场景。 + +在最早期,我们业务复杂度不高,用户量不大,集群规模小,软件架构也相对简单。在这种情况下,其实这一个环境就足够了,真有问题,也可以快速回退掉。退一步讲,即使有问题也回退不了的话,影响范围也有限。 + +所以,这个时候,线上环境=生产环境。 + +我们知道,随着业务量增大和业务复杂度升高,我们的软件架构、部署模式、集群规模等等也相应变得复杂和庞大起来。同时,业务产品在用户和业界的影响力也在变得越来越大。 + +这个时候,任何一个小的变更或一个不起眼的小问题,都有可能导致非常严重的故障,从而造成公司资损甚至是恶劣的产品口碑影响。 + +比如,我们假想一下,如果国内某个大型电商平台不可用,或者某即时通讯软件不可用,会造成何等严重的后果,就不难想象了。 + +所以,这时就需要我们非常严肃而谨慎地应对生产环境的变更。 + +我想你可能跟我一样,会想到一个问题:就是我们不是已经在线下环境经过了很多轮不同形式的验证测试环节,为什么到了生产环境还会有验证不到的严重问题? + +这里涉及一个用户和业务场景的概念,就是线下和线上的用户场景是完全不同的:线下是我们模拟出来的,线上却是真实的用户场景,这两者之间会存在巨大的差异,有差异,系统的表现状况就会不一样。 + +所以线下我们只能尽可能地确保业务功能和业务流程是正常的,但是没法百分之百模拟线上场景,特别是一些异常特殊场景方面。这一点后面的文章我们还会再分享,这篇文章我们只要知道存在差异即可。 + +这个时候,我们的第一个思路就是:即使有影响,也要把它控制在小范围内,或者是在萌芽状态时就发现。这样就可以提前处理,而不是全量发布到生产环境后才发现问题,影响全局。 + +所以,线上的第二个环境,Beta环境就产生了。这个环境也可以叫作灰度环境,包括我们常提到的金丝雀发布,也是基于这个环境的发布模式。 + +Beta环境 + +这个环境的建设,我们简单理解,就是从生产环境的集群中,再建立一个独立集群。看过我们之前介绍CMDB应用和服务分组的文章的读者应该不难理解,针对应用,就是再建立一个分组,独立出一个集群出来,但是这个集群中服务器数量1-2台即可,主要还是针对小规模真实业务流量。如何做到小规模呢?这就要在负载均衡策略上做工作了,主要两种方式: + + +调用RPC,在服务化框架的复杂均衡策略中,将其权重或者流量配比降低; +调用HTTP,在四层VIP或者七层权重上,将权重降低。 + + +这个环境同样不会全量建设,通常只针对核心应用,比如交易链路上的各个应用。同时,除了承担的流量比重不同外,其他与生产环境的应用没有任何差别。 + +后面的部署发布环节,我们会看到,针对核心应用,必须要经过Beta发布环节,才允许正式发布到生产环境。 + +有了Beta环境之后,上面说到的影响范围的问题从一定程度上就可控了。但是在Beta环境上我们仍然会有两个问题无法很好的解决: + + +影响范围再可控,其实也已经影响到了部分真实用户,特别是当访问量特别大的时候,即使是千分之一、万分之一,也是不小的数量。 +之前经历的线下环境毕竟是一个模拟环境,一方面,在数据规模、分布特点、多样性以及真实性方面,跟生产环境的数据场景还是会有很大的区别,所以有很多跟业务逻辑相关性不大,但是跟数据相关性特别强的场景功能,在线下环境就很难验证出来;另一方面,对于一些第三方的系统,特别是商家、支付和物流这样的体系,在线下环境极有可能是Mock出来的,所以验证的时候并不能代表真实场景,但是等到了线上再去发现问题,就可能就会造成真实的业务影响。业务访问失败可以重试,但是造成商家真实的销售数据错误,或者用户真实的支付资金错误,这样就会非常麻烦了。所以,从线下直接进入Beta环境,还是会给生产环境,特别是数据层面造成影响。 + + +当业务复杂度和系统规模发展到一定程度后,上面两个问题就会非常突出,所以单纯的Beta环境是无法满足要求的。 + +这时,线上第三套环境,预发环境,就产生了。 + +预发环境 + +预发环境在建设上,有以下几个规则要求: + + +状态基础服务共用,如DB、KV、文件存储以及搜索类的数据服务。这里基本就是真实的生产环境的基础了,我们上述的问题在这个基础上就可以很好地解决了。除有状态服务外,其他都需要在预发环境上进行全套建设,但是资源使用上,一般是一个应用部署一个实例即可,所以规模比生产环境要小很多。 +网络隔离上,预发环境做独立网段的划分,不承担线上真实流量,独占一个B段,同时在网络上进行逻辑隔离。业务调用必须本环境内闭环,预发不允许跨环境进行应用服务调用,如预发应用调用生产环境应用,反之亦然。 +要保证一定的稳定性。预发环节就是基于线上真实环境进行功能和业务流程的最终验证,所以对于版本质量要求是要高于线下环境,所以不允许反复频繁地变更部署,出现异常或警告也必须要以较高优先级处理。 + + +上述环境的搭建,使用的技术方案,跟我们上篇文章讲到的方案是通用的,如服务单元化调用、绑定hosts以及网络策略隔离等等。预发环境与生产环境的关系如下图: + + + +预发环境正式使用后的另一用途,就是在生产环境出现问题,但是线下复现不了时,就可以在预发环境上复现,这样对于问题定位会带来很大帮助。如果是在生产环境上做调试和问题定位,有时候会影响到正常用户访问,但是预发环境的影响就可控一些。 + +不过,定位问题可以,但是绝对不可以通过预发环境去做下面两件事: + + +与数据订正和变更相关的事情。因为这是由业务流程触发,而不应该由调测触发。而且要时刻牢记,在这个环境做的任何事情都是会对生产环境产生直接影响的,所以这里必要要靠强调意识、事先培训等方式进行避免。 +阻塞他人工作。在定位问题的过程中,如果发现有其他应用依赖,这时要停下来,优先保证环境稳定性,而不是阻塞依赖方发布前的准备工作。 + + +形象一点描述,预发环境就像是球类运动员,他们平时可以在训练场进行训练,但是正式比赛前,一定是要到正式比赛场地提前适应场地或者热身。一方面是为了了解现场的实际情况,做针对性的准备和调整;另一方面也是为了调动赛前兴奋度和氛围。 + +预发环境搭投入使用之后,有很多问题在这个阶段被发现,而且是开发和测试同学目前强依赖的一个环境,所以确实进一步保障了业务的稳定性。 + +然而,在这个环境中仍然存在一个问题。下面我还是以电商为例。 + +电商每年大促,一般都是提前几个月准备,有可能开发团队在大促活动正式开始前3-4周左右,业务功能都已经开发完成,但是这个时候是不能上线的,或者上线了也要有入口开关控制,绝对不能让用户流量提前进来。 + +与此同时,运营侧的招商、报名以及商品上架这些工作也会提前完成,所以这时线上实际已经具备了真实的大促环境,只是因为时间点不到,暂时只能等着。 + +但是,如果有一个只让员工访问,让员工们体验和反馈问题的环境,那么,在这个阶段我们是可以提前暴露很多问题,并进行很多优化改进的。这样做就更进一步保障了大促的系统问题和用户体验。 + +不过,上述Beta环境和生产环境是无法满足要求的。预发环境能满足一部分要求,但是因为这个环境主要还是供开发和测试验证功能使用,在访问的便捷性和功能体验方面,不能完全保证达到真实用户访问的要求和体验。 + +为了满足上述需求,我们会再单独建设一个环境出来,于是,线上环境的第四套环境,办公网生产环境,就应运而生了。 + +办公网生产环境 + +办公网生产环境建设的技术方案与预发环境一致,但是在要求上又有所不同: + + +访问用户是办公网内的员工用户,所以必须连接指定的办公网wifi接入点。于是,员工会通过wifi被劫持到这个环境上,这时用户就可以在这个环境中提前体验新版本软件的功能,比如我们之前说的大促活动等。 +稳定性要求上,办公网生产环境相当于生产环境,虽然不是外部用户访问,但是一个公司内的员工也算是真实用户了,他们发现的问题等同于线上问题,但是级别上会降低一级处理。 +建设规模上,公司有上千、上万名员工,他们的频繁访问行为,也产生一定的业务量,所以综合上述稳定性要求,办公网生产环境在规模上会根据应用容量进行相应的资源分配,这里至少每个应用应该以两个实例做冗余。 + + +所以这个环境,从建设规模和稳定性要求上,就相当于一个小号的生产环境,所以我们内部又把它简称为“小蘑菇”环境。 + +总结 + +我们简单构建一张模型图来对线上环境作个展示: + + + +我在这两期文章中介绍了这么多环境,我们可以看到,环境建设是一项异常繁琐复杂的工作,这些工作不是一蹴而就,而是根据实际的场景和问题催生出来的,所以是个逐步渐进的过程。 + +而且,因为不同的业务类型和场景,以及业务发展的不同阶段,场景和问题可能都是不一样的,而且其建设需求也不一样,所以在实际操作中,一定要切合具体情况进行建设。 + +再就是,环境管理是复杂的,多一个环境就多一份管理成本。所以环境并不是越多越好,反而是越精简越好。这个时候也需要各位读者能够有一定的ROI评估,毕竟能带来最大价值的投入才是有意义的,而不是盲目地建设和投入。 + +最后,给你留几个问题思考: + + +我们分别介绍了线下环境和线上环境的建设,这两个环境在持续交付体系中,分别对应哪些理念和指导思想? +我们建设了这么多的环境,都是为了解决不同场景下的问题,那么还有哪些问题是上述这些环境仍然解决不了的? + + +欢迎你留言与我讨论。 + +如果今天的内容对你有帮助,也欢迎你分享给身边的朋友,我们下期见! + + + + \ No newline at end of file diff --git a/专栏/赵成的运维体系管理课/17人多力量大vs.两个披萨原则,聊聊持续交付中的流水线模式.md b/专栏/赵成的运维体系管理课/17人多力量大vs.两个披萨原则,聊聊持续交付中的流水线模式.md new file mode 100644 index 0000000..2175a4a --- /dev/null +++ b/专栏/赵成的运维体系管理课/17人多力量大vs.两个披萨原则,聊聊持续交付中的流水线模式.md @@ -0,0 +1,106 @@ + + + 因收到Google相关通知,网站将会择期关闭。相关通知内容 + + + 17 人多力量大vs.两个披萨原则,聊聊持续交付中的流水线模式 + 在前面5期文章中,我们分别详细介绍了持续交付体系基础层面的建设,主要是多环境和配置管理,这些是持续交付自动化体系的基础,是跟我们实际的业务场景和特点强相关的,所以希望你一定要重视基础的建设。 + +本期文章是我们持续交付系列的第6篇文章,从本期开始,我们进入到交付流水线体系相关的内容介绍中。 + +持续交付流水线简要说明 + +从一个应用的代码提交开始,到发布线上的主要环节,整个流程串起来就是一个简化的流水线模式。如下图所示: + + + +我们前面介绍了持续交付的多环境以及配置管理,而流水线模式的整个过程正是在这个基础上执行,所以它的某些环节和要素与我们的多环境是有一些交叉的。比如,功能测试会在线下相关的几个环境上完成,比如我们前面介绍到的开发联调环境、项目环境和集成测试环境。 + +但是,它们要达成的测试目的是不同的:对于非功能验收,我们会在线上的预发环境完成,因为预发环境更接近真实场景,所以像容量、性能、安全这些跟线上稳定性相关的能力验收,越接近真实环境,效果越好。 + +后面几期文章,我会结合我们的实践,分环节来介绍。本期文章我们先看项目需求分解和开发模式选择。 + +项目需求分解 + +持续交付我认为更多的是针对应用层面,所以项目需求分解这一部分,这里我们就不展开讲了。这里我们的工作重点,就是将项目管理中的需求与持续发布中的应用这二者很好地关联起来。 + +比较通用的做法,就是要求业务架构师在做需求分析和功能设计时,要针对一个需求进行拆分,最终拆分成一个个的功能点,这些功能点最终落实到一个个对应的应用中,对应的逻辑体现就是应用代码的一个feature分支。 + +如下图所示: + + + +举个简单的例子,比如我们要做大促的优惠活动,同一店铺商品购满500元,可以使用10元店铺内优惠券,同时还可以使用10元全站优惠券。 + +这样一个需求最终拆解下来,可能需要店铺应用支持多优惠活动的叠加,同时下单应用和购物车应用在计算价格时也要设定相关的优惠逻辑,这一个需求可能就拆出三四个功能点。 + +这样做的好处就是,从一开始的需求管理维度,就确定了最终多个应用联调、测试以及最终发布的计划和协作方式,从而就会让我们明确同一个项目环境中到底需要部署哪些应用,这些应用的发布顺序怎样安排。 + +比如,如果A应用依赖B应用,那么B应用就必须优先发布。所以,上述这个过程对于项目进度管理、团队协作以及最终的发布计划都是有帮助的。 + +讲到这里,你是不是又进一步感受到了运维的重要性呢? + +当然,每个公司都有不同的项目管理方式,这里我们只要明确做好需求拆分与应用功能的对应即可。 + +提交阶段之开发模式选择 + +在代码提交阶段,我们遇到的第一个问题,就是分支管理问题。这反映出研发团队协作模式的问题。 + +我们所熟知的开发协作模式有以下三种: + + +主干开发模式。这也是极限编程里提倡的一种模式,每一次代码提交都是合并到master主干分支,确保master随时是可发布状态。但是它对代码开发质量以及持续集成自动化和完善程度要求非常高,通常一般的团队很难做到。 +gitflow开发模式。因为git的流行,gitflow是专门基于git代码管理的工作流工具,它的特点是在master分支之外,会有一条常驻develop开发分支,所有功能开发和缺陷修复都在这个分支上再建立分支。发布时合入一个从master分支中签出的release分支,最终发布的是release分支代码,然后release分支再合并回master和develop分支。如下图所示: + + + + + +分支开发模式。相对于gitflow模式,分支开发模式会简单清晰很多。它的特点是,功能开发或缺陷修复从master签出独立的一个feature或bug分支,发布前从master分支签出一个release分支,并将要发布的feature或bug分支合入。发布完成后,release分支合入master分支。如下图所示: + + + + +开发模式的选型原则 + +上面我分别介绍了三种开发模式的特点,那么,在实际操作中,我们选择哪一种比较好呢? + +这里的选型原则就是:一看这几种模式的适用场景;二看我们实际的使用场景是怎么样的。 + +下面,我们分别看看主干开发和gitflow开发这两种模式。 + +主干开发模式。它的特点是,所有的代码变更直接提交到master分支,这种情况比较适合规模较大的应用,这类应用自身集中了所有的需求功能点,且需求串行开发,需要多人协作共同完成同一个需求,发布时间点明确、统一。 + +这种模式最简单,且便于管理,不需要再建立各种分支。我们之所以在极限编程中提倡这种模式,也是因为这种模式最简单,最便捷,也最高效。因为我们的软件架构在早期还是单体结构且分层架构的,代码相对集中,所以,主干开发模式也是适用的。 + +但是,在现实场景下,需求总是层出不穷的,所以就需要需求并行开发。这就会产生这样一种情况:同一应用会有多个团队在同时提交不同需求的代码,且每个需求发布的时间点是不同的。 + +所以如果采用主干开发模式,就可能会将还没有经过测试验证的代码发布到线上。这时,我们就需要在代码里预设很多功能开关配置,这样一来,在应用正式上线前,代码可以发布,但是功能不开放,而这样也必然会增加代码的复杂度。 + +所以,就有了gitflow开发模式。 + +gitflow开发模式能够适应并行开发,解决上述我们所说的问题,而且gitflow工具能够从技术层面帮我们解决各种分支合并问题。 + +通过上面gitflow的图示,我们可以看出,gitflow开发模式带来的分支的管理代价还是比较高的,且随着分支增加,开发人员之间的沟通协作成本也会随之提高。 + +同时,gitflow开发模式还是在代码相对集中的应用场景中更加适用,因此,基于这个应用完成较多的并行需求,就需要通过多个分支来管理。 + +在现实场景中,尽管我们日常需求非常多,但是这些需求拆解下来的功能都是集中在某个或某几个应用上的吗? + +其实不然。我们从原来的单体或分层架构演进到微服务架构后,带来的一个好处就是每个应用的职责更加明确和独立,与此同时,针对应用的开发,团队也更加自制,规模更小,符合“两个披萨原则”。 + +所以,一个需求拆解出功能,对应到每个应用上,这样可以很好地控制并行的功能点数量,大大降低开发协作的沟通复杂度,即使有合并冲突问题,往往内部沟通一下就可以很快解决。 + +而实际上,我们设想的这种复杂的gitflow场景,在微服务架构下的组织架构中极少存在。 + +在此,经过对主干开发模式和gitflow开发模式这二者的综合对比,结合前面我对分支开发模式的介绍,我们可以看出,分支开发模式简单清晰,在实际操作中更适合我们使用。 + +最后,留个问题给你:你对于开发协作模式是如何选择的?存在哪些问题?有什么更好的建议? + +欢迎你留言与我讨论。 + +如果今天的内容对你有帮助,也欢迎你分享给身边的朋友,我们下期见! + + + + \ No newline at end of file