为什么“分布式”要加引号?
与其他公司提高并发性能的场景可能不太一样,我们的系统之前是多个模块共用一个tomcat来运行的(All in One),模块有很多,光安装包就几十个。当某个模块或某几个模块出问题的时候,整个tomcat就挂了,请求时间长、内存占用高、垃圾回收频繁、错误日志不停的刷....现场工程解决问题的办法是,重启tomcat(小问题重启浏览器,大问题重启tomcat,万能的重启^_^)!!当然,这个对用户的使用体验很差,用户甚至提出要弃用我们公司系统的说法了,领导压力大啊...
因本项目历史久远、技术落后、规模庞大、交接频率高,想系统的发现及解决问题是件很困难的事。重新开发一套系统也是不可能的事(这套系统积累了太多的心血在上面了,无论从人力、时间成本来说都花费太大,下不了狠心)。于是,分布式部署的方案被提上了工作日程,目的是将各模块隔离开来,出问题的时候互不影响。但是,一套不是面向分布式的设计的系统,要想实现分布式部署,就像是将一大团的乱麻分成一团团小的乱麻(哈哈,这比喻我觉得挺恰当的,分开来后还是乱麻^_^),谈何容易啊!
艰巨而又头疼的任务分给了我们组。经过几轮讨论后,列出了几个关键的问题:
1、 静态变量共享问题。单独部署的情况下,各业务注册的数据,如字典数据、关键词类型等,存放在全局的静态变量中的;分布的情况下,各业务只能获取到自己模块注册的数据,获取不到全量的数据。
2、 定时任务重复执行问题。分布式的情况下,会有多个界面框架模块共存。界面框架中的定时任务执行次数和执行频率都会变高。其实从机器中的定时任务是不需要的。
3、 文件上传与读取问题。All in One的情况下,界面框架提供统一的上传组件,文件上传到一个固定目录下,各业务可以通过绝对路径获取到文件,直接操作文件流。分布式的情况下,业务上传的文件会上传至主框架上,当从节点通过绝对路径去访问时,肯定是获取不到文件的。
4、 session共享问题。All in One的情况下,各业务拿到的是同一个session;分布式的情况下,各业务的sessionID不通,session也不是同一个session,一个机器上修改了session的数据,其他机器要实时感知到数据的变化。
5、 请求分发问题。各业务的请求,应该被正确的分发给各业务机器处理,各业务机器处理请求时不需要登录,登录统一由主框架负责处理。
6、 安装时重复刷菜单的问题。All in One的情况下,安装或升级某个包时,会将对应的菜单刷到菜单表中。分布式的情况下,各从机器升级界面框架包时,也会刷界面框架的菜单,很可能导致菜单表的数据出问题。
7、 单例问题。不确定具体的影响范围。
8、 多线程执行顺序问题。不确定具体的影响范围。
9、 事务。不确定具体的影响范围。
10、 各机器对时。不确定具体的影响范围。
大概确定了思路如下:
1、 各模块单独部署在各自的tomcat里,各模块安装需依赖界面框架模块。
2、 通过nginx做代理,保证对用户的入口唯一。按各模块url请求规则,分配到各自的tomcat上处理请求。平台公共的请求由一个tomcat单独处理。
3、 tomcat通过共享session,与nginx呼应,确保用户会话在多个tomcat共享。
4、 引入Spring-session模块,将session存储到redis服务器上。
5、 将静态变量通过redis的发布/通知机制,修改代码,实现静态变量的同步。
6、 增加nginx配置服务,用于管理各业务的请求转发规则,并定义各机器的角色。主节点只能有一台,负责处理公共请求、执行界面定时任务、刷界面框架的菜单;从节点的界面框架负责支撑业务功能。
7、 增加文件服务器,将上传的文件存储到文件服务器上。
8、 其他问题暂不确定具体的影响范围。
未完待续,后面整理迁移过程中遇到的坑!
坑1:Spring session 提交问题
因要对session共享,使用的spring session本以为万事大吉,结果问题来了。看下代码:
UserBean user = (UserBean )session.getAttribute("currentLoginUser"); System.out.println(user.getName()); user.setName("张三");
单机的情况下,没毛病吧,我就想改下session里的用户信息。如果你这么放心的把sesson交给spring,那就等着测试部提问题吧。如果不退出或关闭浏览器的话,你会发现修改的用户信息,你刷新后,竟然没有生效!!!于是就想,spring-session是怎么保证session存储到redis上的呢?不卖关子了,redis通过hash类型存储session中的数据,session内的数据有变化时,会将修改的数据更新到redis上,为了提高效率,spring在filterchain结束的时候提交一次session数据到redis,而不是每次修改session数据都去提交一次。那spring怎么知道session的哪些数据变了呢?跟了下源码发下,只有在主动调用session的setAttribute和removeAttribute方法时,才会将修改的数据放到this.delta中,最后提交数据时,就是把这里的数据更新到redis的。所以为什么修改的user数据没有生效呢?因为没再次调用setAttribute方法,spring感知不到对象属性的变化!!坑爹啊~~自己修改了spring的提交逻辑,与旧的session进行了equals对比,把有变化的数据放入this.delta中,解决问题了?并没有!!最终发现UserBean重写了equals和Hash方法,只要用户ID相同,就返回true,我只想说,你大爷的!!
坑2:与其他系统融合的问题
暂给我们的系统取名A系统,现有个B系统也要用B系统的一些数据,于是取了个名字叫“A与B系统的融合”。所谓A与B的融合,其实就是实现B系统的免登陆,并获取A系统的一些内存数据。于是,两个不同的系统,session就共享了...
于是,当你在使用A系统的时候,很多莫名其妙的反序列的问题就出来了。为什么会报反序列化的错误?因为session里面有B系统的对象,但A系统根本就没有B系统的jar包。实现免登陆,可以走单点登录吧?想获取A系统的数据,可以让A系统提供接口吧?为什么要共享session?我也不知道了。