复杂问题 拆解为状态转移的递归问题 (比如factorial(n)=factorial(n-1)*2) edge cases(比如null场景) Desular(栈/参数优化) Curry(多参数转为单参数) Top-down DP/空间优化 CPS inline 拆解方法也是有方法论的,它可以通过DOE(Design of experiments) 找出输入与输出的关系,比如
通过强制固定一些参数降低动态复杂度,DP一定是确定性的/无状态的,非NP问题 输出并不一定确切的值。比如计算EditDistance,只用求最终总和即可,而没要求具体的排列组合,这两个难度完全不一致。 除此之外,还有一些技巧性强的常数复杂度方案甚至经验公式/数学方法,但是这种方法是需要学习与记忆的
What is CPS? 将阻塞的请求改为异步的Callback形式(或者返回一个Promise/Next的结构体),仅此而已。注意网上有很多文章说通过CPS可以消除栈,但是只说对了一半,在没有编译器优化的情况下,并不能将栈优化为JMP调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void fibonacciCps (int i, Consumer<Integer> call) { if (i <= 1 ) { call.accept(i); } else { fibonacciCps(i - 1 , new Consumer <Integer>() { @Override public void accept (Integer a) { fibonacciCps(i - 2 , new Consumer <Integer>() { @Override public void accept (Integer b) { call.accept(a + b); } }); } }); } }
使用方法
1 2 3 4 5 6 fibonacciCps(6 , new Consumer <Integer>() { @Override public void accept (Integer sum) { System.out.println("sum = " + integer); } });
平时编码这种写法可读性很差,很难显示区分同步还是异步执行,所以基本上没有利用的机会,一般会“人肉AST优化”到DP的第三种。
不过这样的好处是
作为AST的中间态,方便编译器进行栈消除 实现在JVM上自己的“栈”与“VM”,以及持久化 实现数据“状态”的持久化(Checkpoint/Restore),比如Jenkins/CRIU Jenkinsfile的CPS实现 此部分介绍用户输入的Jenkinsfile脚本在Jenkins底层执行的流程
字符串编译为BlockAST 原始的脚本虽然写的像字符串,执行底层也是Groovy,但是它不是Groovy语言
所有Groovy语言的Token关键词(比如if/常数/变量)将不再是一个简单的JVM字节码,而有各个对应的Block,通过MetaMethodSite实现了"编译"为CPS的Block 静态工具类调用(比如forEach)/Closure表达式,通过CpsTransformer在编译时/运行时生成CPS转换,除非加入@NoCPS
注解。有人可能看过王垠的40行代码,但是王垠的显然更高级。 定制的Step,通过FunctionCallBlock
封装调用,它内部再使用CpsScript引擎去Eval 在执行侧,Jenkins通过Groovy的Script实现了自己封装了一个Runtime,具体可以看Groovy 相关文章,避免自己从零写AST,最终输入的Groovy字符串将转为如下结构体
$2CpsThreadGroup (将被持久化为program.dat) Continuable & Continuation Env BlockList 基础Block FunctionCallBlock 执行steps等功能 执行Groovy工具类CpsDefaultGroovyMethods CPS层执行与栈消除 在CPS的内部AST,是一个Block
单向链表,除了Parallel外,其它Block都是线性遍历的,因此设计上很简洁,这个与MyBatis 的MixedSqlNode
/S-Expression是相同的设计思路
$2Next Block Env Continuation Block Env Continuation Next2 Block Env Continuation Block2 eval.. eval.. eval.. 内部通过循环一个单向链表进行消除,是不是很像DP优化的第三种。
1 2 3 4 5 6 Next n = cn.resumeFrom(e,k);while (n.yield ==null ) { n = n.step(); }
JenkinsCPS的缺点 由于它是对Jenkins深度定制的
执行CPS必须需要Jenkins服务端(注意Agent只是执行原生字节码的,不处理CPS编排),没有成熟的离线解释器,因此执行调度性能限制于Jenkins单点 基于文件进行状态的持久化比较鸡肋,大部分场景中,master重启都会导致任务失败。毕竟是单点工具,没法比得过flink这种级别的Persisted States 断点调试很困难,基本上只能靠echo
来分析脚本;CPS编程范式导致参数固定 作为插件生态开发者,那么Groovy/CPS的学习门槛的确有点高,从整个插件只有Kawaguchi一人主刀也可以看出 更深的语义定制 假如你希望Jenkins能够实现更复杂的编排,或者自己的构建语法,可以尝试基于CPS再实现一层Interceptor,我基于此方案实现了
YAML interceptor: 配合Macro解析 ,实现用Java/Groovy实现AzureDevOps的效果,这样就可以将很多cps测试转换为无状态的测试。 Parallels BFS Tree嵌套并行执行构建 Log live tail 这些是有门槛的定制场景,网上找不到开源的方案,全靠知识广度的积累。
附录 关于研发效能/DevOps工具的前景 DevOps平台一般由业务部门出预算,属于成本部门,比如“质量与效率工程部”,“研发效能部”,“基础研发平台”,“流程与IT”等名称,裁员很容易是第一批。当然也有可能发展起来后也兼任对外售卖。
国内除了大型公司的工具组有预算买现成的或者做这种改进,其它中小公司选择很有限,举例如下
阿里:经常看到阿里的人抱怨打了一天包都打不出来 百度:前百度研发效能团队自主创业“简单云 ”,招聘要求那么高,结果北京顶格才4.5万,同样能力还不如在大公司/外企接着苟。 效率云:方案上还不支持config as code 腾讯的蓝鲸对外宣称腾讯全链路构建,结果腾讯内部的Coding/Orange/TYPD等各立山头。 华为云:CloudDev,ROMA Factory DevOps,云龙平台等各搞各的,价格吃水线很高。 假如工具要实现产品化可销售,由于技术上难度并不高,导致同质化严重,最好还是背靠大树来做,即和硬件/云一起去做,国内注定是强政府与强销售,要么出海和巨头(Gitlab/Github/Azure DevOps)竞争。
我个人认为这类工作很有挑战,因为它需要全局考虑,肯定比单独的业务项目有难度。
机房、机柜、三层交换、云服务的配置 分布式存储(块存储、对象存储)、VPC、分布式调度器(cgroup与容器)的测试与选型 权限设计(IAM/ACL)、大数据度量与分析 参考链接 https://www.kimsereylam.com/racket/lisp/2019/02/14/recursion-with-fibonacci.html
http://arasio.hatenablog.com/entry/2016/10/08/220843
插件开发技巧(如何利用当前Step/Node信息) 由于我不希望定制场景的代码引入到Java层(重启Jenkins成本很高),但是又想获取到实时的Context信息,可以通过如下将业务代码移动到Groovy侧实现热部署
参考例本体的还原
1 2 3 4 5 6 7 8 9 10 11 12 13 @NoCPS def callback (result, Step s, CpsContext c) { } node{ def result = sh("xxxxx" ) customStep(callback: this .&callback.curry(result)) }
Jenkins核心调试断点位置 从MQ取到Executor开始执行的位置(之前都是Jenkins的调度过程)
1 org.jenkinsci.plugins.workflow.job.WorkflowRun#run
整个Workflow总断点位置
1 org.jenkinsci.plugins.workflow.cps.CpsFlowExecution#start
如果只关注Step相关流程,可以在这里进行分析
1 org.jenkinsci.plugins.workflow.cps.DSL#invokeMethod
RSS , Theme Curry. All contents are not allowed to be redistributed or synthesised without an explicit permission.