在线升级系统的设计原则
在上小节中,我们给出了一个Java类热替换的实例,掌握了这项技术,就具备了实现在线升级系统的基础。但是,对于一个真正的产品系统来说,升级本省就是一项非常复杂的工程,如果要在线升级,就会更加复杂。其中,实现类的热替换只是最后一步操作,在线升级的要求会对系统的整体设计带来深远的影响。下面我们来谈谈在线升级系统设计方面的一些原则:
◆在系统设计一开始,就要考虑系统的哪些部分是需要以后在线升级的,哪些部分是稳定的
虽然我们可以把系统设计成任何一部分都是可以在线升级的,但是其成本是非常高昂的,也没有必要。因此,明确地界定出系统以后需要在线升级的部分是明智之举。这些部分常常是系统业务逻辑规则、算法等等。
◆设计出规范一致的系统状态转换方法
替换一个类仅仅是在线升级系统所要做的工作中的一个步骤,为了使系统能够在升级后正常运行,就必须保持升级前后系统状态的一致性。因此,在设计时要考虑需要在线升级的部分所涉及的系统状态有哪些,把这些状态设计成便于获取、设置和转换的,并用一致的方式来进行。
◆明确出系统的升级控制协议
这个原则是关于系统在线升级的时机和流程控制的,不考虑系统的当前运行状态就贸然进行升级是一项非常危险的活动。因此在系统设计中,就要考虑并预留出系统在线升级的控制点,并定义清晰、明确的升级协议来协调、控制多个升级实体的升级次序,以确保系统在升级的任何时刻都处在一个确定的状态下。
◆考虑到升级失败时的回退机制
即使我们做了非常缜密细致的设计,还是难以从根本上保证系统升级一定是成功的,对于大型分布式系统来说尤其如此。因此在系统设计时,要考虑升级失败后的回退机制。
在线升级系统实例
首先,我们来简单介绍一下这个实例的结构组成和要完成的工作。在我们的例子中,主要有三个实体,一个是升级控制实体,两个是工作实体,都基于ActiveObject实现,通过命令消息进行通信(关于ActiveObject的详细信息,可以参见作者的另外一篇文章“构建Java并发模型框架”)。
升级控制实体以RMI的方式对外提供了一个管理命令接口,用以接收外部的在线升级命令。工作实体有两个消息队列,一个用以接收分配给它的任务(我们用定时器定时给它发送任务命令消息),我们称其为任务队列;另一个用于和升级控制实体交互,协作完成升级过程,我们称其为控制队列。工作实体中的任务很简单,就是使用我们前面介绍的Foo类简单地打印出一个字符串,不过这次字符串作为状态保存在工作实体中,动态设置给Foo类的实例的。升级的协议流程如下:
当升级控制实体接收到来自RMI的在线升级命令时,它会向两个工作实体的任务队列中发送一条准备升级消息,然后等待回应。当工作实体在任务队列中收到准备升级消息时,会立即给升级控制实体发送一条准备就绪消息,然后切换到控制队列等待进一步的升级指令。升级控制实体收齐这两个工作实体发来的准备就绪消息后,就给这两个工作实体的控制队列各发送一条开始升级消息,然后等待结果。工作实体收到开始升级消息后,进行实际的升级工作,也就是我们前面讲述的热替换类。然后,给升级控制实体发送升级完毕消息。升级控制实体收到来自两个工作实体的升级完毕消息后,会给这两个工作实体的控制队列各发送一条继续工作消息,工作实体收到继续工作消息后,切换到任务队列继续工作,升级过程结束。主要的代码片段如下(略去命令消息的定义和执行细节):
1. // 升级控制实体关键代码
2. class UpgradeController extends ActiveObject{
3. int nready = 0;
4. int nfinished = 0;
5. Worker[] workers;
6. ......
7. // 收到外部升级命令消息时,会触发该方法被调用
8. public void askForUpgrade() {
9. for(int i=0; i<workers.length; i++)
10. workers[i].getTaskQueue().enqueue(new PrepareUpgradeCmd(workers[i]));
11. }
12.
13. // 收到工作实体回应的准备就绪命令消息时,会触发该方法被调用
14. public void readyForUpgrade(String worker_name) {
15. nready++;
16. if(nready == workers.length){
17. for(int i=0; i<workers.length; i++)
18. workers[i].getControlQueue().enqueue(new
19. StartUpgradeCmd(workers[i]));
20. }
21. }
22.
23. // 收到工作实体回应的升级完毕命令消息时,会触发该方法被调用
24. public void finishUpgrade(String worker_name) {
25. nfinished++;
26. if(nfinished == workers.length){
27. for(int i=0; i<workers.length; i++)
28. workers[i].getControlQueue().enqueue(new
29. ContineWorkCmd(workers[i]));
30.
31. }
32. }
33.
34. ......
35.
36. }
37.
38. // 工作实体关键代码
39. class Worker extends ActiveObject{
40. UpgradeController ugc;
41. HotswapCL hscl;
42. IFoo foo;
43. String state = "hello world!";
44.
45. ......
46.
47. // 收到升级控制实体的准备升级命令消息时,会触发该方法被调用
48. public void prepareUpgrade() {
49. switchToControlQueue();
50. ugc.getMsgQueue().enqueue(new ReadyForUpdateCMD(ugc,this));
51. }
52.
53. // 收到升级控制实体的开始升级命令消息时,会触发该方法被调用
54. public void startUpgrade(String worker_name) {
55. doUpgrade();
56. ugc.getMsgQueue().enqueue(new FinishUpgradeCMD(ugc,this));
57. }
58.
59. // 收到升级控制实体的继续工作命令消息时,会触发该方法被调用
60. public void continueWork(String worker_name) {
61. switchToTaskQueue();
62. }
63.
64. // 收到定时命令消息时,会触发该方法被调用
65. public void doWork() {
66. foo.sayHello();
67. }
68.
69. // 实际升级动作
70. private void doUpgrade() {
71. hscl = new HowswapCL("../swap", new String[]{"Foo"});
72. Class cls = hscl.loadClass("Foo");
73. foo = (IFoo)cls.newInstance();
74. foo.SetState(state);
75. }
76. }
77.
78. //IFoo 接口定义
79. interface IFoo {
80. void SetState(String);
81. void sayHello();
82. }
在Foo类第一个版本的实现中,只是把设置进来的字符串直接打印出来。在第二个版本中,会先把设置进来的字符串变为大写,然后打印出来。例子很简单,旨在表达规则或者算法方面的升级变化。另外,我们并没有提及诸如:消息超时、升级失败等方面的异常情况,这在实际产品开发中是必须要考虑的。