一、背景
公司产品有补丁机制,一旦出现安全漏洞或者需要进行逻辑修复、bug修复等,发一个补丁出来,然后通过打补丁命令进行修复,最后重启就能避免客户大规模变更程序的问题,实现轻松变更。同时也支持补丁回退,如果发生问题可以随时回退。
期初,我以为是整个产品程序的架构上利用了如钩子Hook的架构,在某个钩子阶段,首先加载这些patch补丁,最后再运行main程序。 但是后来想了想,这玩意弄得也太复杂了吧,万一有些就是不是在钩子阶段才能解决的问题呢,那咋弄? 可能是我想多了。
最后研究发现确实是把这个事情想多了。
二、补丁实现机制
1、整体思路: 重新编译、替换
其实,最先能想到的就是,例如某个jar包代码,我们发现了漏洞,需要进行修复。那么可以基于这个版本的源代码,修复好以后,打成新的jar,每个现场客户再替换这个整体的jar,最后重启即可。
思路其实是OK的,但是往往,我们修复漏洞或者bug,只会动到几个文件的几行代码,代码范围不是很大,整体替换倒不是说不行,就是重新打这个jar包实在没必要。并且能传输更少的文件,更方便,别人也会认为这个带来的影响没有那么多。 啪啪来几M甚至几十M文件替换,难免会心生疑虑。
有时候你修改的可能就是一个java文件的几行代码,每次发新的jar都好几M,给客户就感觉很紧张,以为你改的范围很大,会对他们的业务存在重大影响或者存在隐患。 所以我们借鉴这种思路,只把我们改动到的Java代码编译好的class文件进行替换,那么整体补丁包的大小会极大降低,也就是几k的大小。
2、手动替换class的方式
按照上面的思路,我们重新编译了新的class文件。那么我们还需要通知客户,你要去找到哪个jar包,需要对jar包进行解压,然后替换class文件,最后再打包好这个jar包。同时针对旧版本的jar需要自己手动备份,以便于出现问题时进行回退。
这么一套下来,虽然不需要传输大文件的jar包了,但是还是给客户带来了操作上的不便,以及没有安全的、便捷的回退机制。 同时也没有关于现在已经升级到几号补丁了,这些都需要运维人员进行人工维护,风险高、易出错。
3、JVM已经加载过相同完全限定名的class,后序不会重复加载、也不会覆盖
不知道大家在日常开发、运维过程中有没有发现关于class类冲突的问题。 例如我要引用A包的v2版本,但是我的项目之前存在A包的v1版本,v1和v2其实存在相同的类,但是类里面的逻辑可能由于升级的原因,已经不一样了。我代码本来想用的v2,但是JVM给我加载的是V1,导致报错。最后发现问题后,把v1的jar放在CLASSPATH前面或者移除v2的jar才解决了问题。
JVM在load class的时候就存在这么一个机制, 如果我已经加载了A的v1版本,那么到加载A的v2版本时遇到相同的class则不会再次加载。 所以在java -cp指定CLASSPATH顺序时,谁在前、谁在后顺序就是一个很关键的问题。 谁在前面谁就被加载,后续遇到相同的则就不会加载了。
借鉴这个思想,我们想到了,我们的补丁可以这么设计:
1、针对存在bug、漏洞的class, 需要针对源码文件修改、最后编译为新的class文件
2、将新的class文件单独打为jar包
3、在java -cp运行时,我们设计一个CLASSPATH路径的顺序,优先加载我们的patch补丁jar文件(这些补丁jar存在在一个单独的目录进行维护), 最后再拼接上后面主程序的CLASSPATH。例如
java -cp $patch10.jar:$patch09.jar:$patch08.jar:$MAIN.jar com.demo.StartUp
并且这些补丁,理论上应该是最新补丁号在前面依次类推,倒序加载。这样加载在最前面的补丁jar能够保证是最新的逻辑。
4、JVM加载class先后顺序实验
存在2个版本的Hello.java, 分别编译为2个jar 包,但是这个2个class的完全限定名是一模一样的。 我只是在运行java -cp的时候位置顺序调整了一下,运行结果是不一样的。
哪个JAR放在-cp的前面,那么前面的JAR的class先被JVM加载,后续即使遇到相同完全限定名的class,忽略跳过,不会重新加载或者覆盖。
由此可以模拟我们的补丁过程,首先888888是第一个程序版本,后续由于某种原因(修复bug或漏洞),逻辑修改为999999。那么我们可以通过java -cp的形式把999999的jar包放在-cp前面,由此达到目的。
-cp 顺序不一样,执行结果完全就是不一样的.
Hello.java 999999的源代码如下:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, updated world! 999999");
}
}
Hello.java 888888的源代码如下:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, updated world! 888888");
}
}
当然最后我们这个补丁机制,还需要完善,例如补丁回退机制、补丁备份机制等等,不过这个都是后话,无非就是对这个补丁jar进行备份、管理的一个过程而已,在此就不赘述。
三、总结
总体,补丁机制就是替换文件或者代码的过程。只是说替换的过程能否让用户更加无感知、出现异常方便回退、补丁版本记录、兼容性强等方便需要做到位。 同时例如有些补丁需要进行热加载机制,无须重启,直接生效。 针对脚本语言的程序更是如此,就是替换代码文件,如lua、php等,替换立即生效,也没有多么神秘的面纱,呵呵。