(五)具体实施方法
(1)云资源适配层的作用是不同云平台的API与资源模型映射到统一资源模型中,使得系统可以统一的接口管理所有已经接入的云平台。其实现采用了前文提到的可扩展异构云管理模型适配方法,利用一个控制器对各个云平台进行统一管理。另外,系统提供了第三方适配器的接入功能,可以在运行时实时接入第三方的适配器从而管理新的异构云平台。
(2)抽象云资源池就是各个云平台的资源经过适配器的抽象后形成统一资源模型的存储位置,该层系统向上层暴露统一的API使得系统上层能够统一地使用资源池中的各种资源,如图所示,包括虚拟机、镜像、网络、存储、快照等。经过下层的资源映射,各个云平台的异构性已经被去除,对于上层来说可以使用的是一致的云资源模型。
对适配层内部进一步分析,可以根据图3看到适配层的各组件之间的依赖关系,适配层由四个模块构成:
- cloud-adapter-common
- cloud-adapter-starter-api
- cloud-zstack-starter
- cloud-adapter-web
- cloud-adapter-db
其中cloud-adapter-common模块里定义了一些公用的工具类、注解类、异常定义等,该模块在项目中被其他三大模块调用;
cloud-adapter-starter-api中是将底层各资源池的资源功能抽象出一个统一的模板,用于定义底层的抽象接口,比如Zstack或者OpenStack等平台。底层的功能以及调用功能的参数可能不同,但是总能抽象出公共的功能和公共的参数出来。
就底层云平台功能不统一来说,若某云平台具有特有的功能,我们使用default方法定义接口中的方法,这样不会影响其他云平台实现接口类;
就底层功能调用时需要的参数不同,可以有以下两种方法借鉴解决:第一种是各个平台的参数确实存在一些差异,对于这种情况,除了抽象出来的一些公共参数外, 其他的参数可以放到一个Map或者List中,各个平台各取所需;第二种情况是可能平台之间的差异很大,完全抽象不出公共参数出来,比如创建VPC网络,Vmware资源池可能只是需要调用一个接口就能完成创建,但是Zstack VPC网络的定义与别的资源池存在着较大的差异,对于Zstack资源池创建VPC网络时,则需要按顺序调用多个接口,这种情况就暂时只能定义成两个接口,适配层主程序在调用的时候根据平台标识用if else判断。
cloud-×××-starter模块是底层云平台的封装,需要实现cloud-adapter-starter-api模块定义的接口,用于对接底层云平台的SDK。该模块中有一个对应资源池的注册器类——××Register类,这个类需要继承cloud-adapter-common模块中的HandlerRegister抽象类,实现getRegistType()和getHandlerPackage()方法。其中getTegisterType()方法是定义的该平台的标识,传入的平台标识和此处标识匹配,则路由到该平台的实现类;getHandlerPackage()方法定义的是该starter的底层实现类所在的包路径,启动的时候会自动注册该包下的所有类到适配器主程序。cloud-×××-starter模块是可装卸的,通过配置文件的属性配置确定程序启动的时候是否装载该模块。配置为需要装载的starter,相应的api实现会在程序启动的时候被注册到主程序。调用的时候通过请求体中的平台表示确定使用哪个平台的实现。
cloud-adapter-web是适配器主程序,主要用于业务数据处理,比如底层调用之前的数据封装以及底层调用之后的数据库操作等。
cloud-adapter-db模块下定义了数据库表对应的实体类、数据库的配置类以及后期对数据库表的结构的变动语句。
(A)用户请求基于资源池业务的自动路由方案
整体实现流程:
(1)starter中资源池类型标识和统一接口层实现包路径注册到静态map
1.package com.cloud.adapter.zstack;
2.
3.import com.ideal.cloud.adapter.common.utils.AbstractHandlerRegister;
4.
5.public class ZstackRegister extends AbstractHandlerRegister {
6. public ZstackRegister(){
7. AbstractHandlerRegister.packageTypeMap.put(getRegistType(), getHandlerPackage());
8. }
9.
10. @Override
11. protected String getRegistType(){
12. return "zstack";
13. }
14.
15. @Override
16. protected String getHandlerPackage(){
17. return "com.ideal.cloud.adapter.zstack.handler";
18. }
19.
20.}
(2)各资源池统一接口层实现对象归类注册到路由map
1.//注册各资源池handler的方法
2.public static void registStarter(String handlerPackage, String registType){
3. try{
4. log.info("开始加载底层平台handler, handlerPackage:{}, registType:{}", handlerPackage, registType);
5. Set<String> handlerNameSet = ClassUtil.getClassName(handlerPackage, true);
6. log.info("加载到的handler为:{}", JSON.toJSONString(handlerNameSet));
7. for (String handlerName : handlerNameSet) {
8. log.info("current handlerName:{}", handlerName);
9. Class<?>[] classArr = Class.forName(handlerName).getInterfaces();
10. for(Class interClass:classArr){
11. String interfaceName = interClass.getName();
12. String[] beanDefinitionNames = BeanUtils.getApplicationContext().getBeanDefinitionNames();
13. if(AbstractHandlerRegister.handlerMap.containsKey(interfaceName)){
14. Arrays.stream(beanDefinitionNames).forEach(beanName -> {
15. String className = BeanUtils.getBean(beanName).getClass().getName();
16. if(className.equals(handlerName)
17. || (className.indexOf("EnhancerBySpringCGLIB")>-1 && className.indexOf(handlerName)>-1)){
18. AbstractHandlerRegister.handlerMap.get(interfaceName).put(registType, BeanUtils.getBean(beanName));
19. }
20. });
21. } else{
22. Arrays.stream(beanDefinitionNames).forEach(beanName -> {
23. String className = BeanUtils.getBean(beanName).getClass().getName();
24. if(className.equals(handlerName)
25. || (className.indexOf("EnhancerBySpringCGLIB")>-1 && className.indexOf(handlerName)>-1)){
26. Map<String, Object> typeMap = new ConcurrentHashMap<>();
27. typeMap.put(registType, BeanUtils.getBean(beanName));
28. AbstractHandlerRegister.handlerMap.put(interfaceName, typeMap);
29. }
30. });
31. }
32. }
33. }
34. } catch (Exception e){
35. e.printStackTrace();
36. }
37.}
(3)请求对应的资源池信息和用户信息自动填充
1./*
2. *Copyright 2021 China Telecom Global Limited
3. *天翼云科技有限公司
4. *https://www.ctyun.cn/
5. *All rights reserved.
6. */
7.
8.package com.cloud.adapter.core.common.filter;
9.
10.@Slf4j
11.@Order(1)
12.@WebFilter(filterName = "udcFilter", urlPatterns = "/*")
13.public class UdcFilter implements Filter {
14.
15. @Override
16. public void init(FilterConfig filterConfig) {
17. }
18.
19. @SneakyThrows
20. @Override
21. public void doFilter(ServletRequest rq, ServletResponse rp, FilterChain ch) throws IOException, ServletException {
22. try{
23. HttpServletRequest request = (HttpServletRequest) rq;
24.
25. //排除不需要过滤的URL
26. String path = request.getRequestURI().substring(request.getContextPath().length()).replaceAll("[/]+$", "");
27. String matchedUrl = PATH_WHITE_LIST.stream().filter(whitePath -> {
28. if (path.indexOf(whitePath) == 0) {
29. return true;
30. }
31. return false;
32. }).findFirst().orElse(null);
33.
34.
35. //从请求的header或者param中提取资源池id和用户id(此处省略提取过程)
36. String ctUserId = "";
37. String ctOsId = "";
38.
39. //slueth请求链路追踪
40. MDC.put(Constant.RID_FLAG, rid);
41.
42. //通过资源池id和用户id查询资源池信息和用户信息,并填充到ThreadLocal类型的UdcInfo对象中(此处省略查询过程)
43. UdcInfoDto udcInfoDto = new UdcInfoDto();
44. UdcInfo.setUdcInfo(udcInfoDto);
45.
46.
47. ch.doFilter(requestWrapper, rp);
48. } catch(PrePublicException e){
49. log.info("udcFilter前置错误:", e);
50. return;
51. } finally {
52. UdcInfo.remove();
53. }
54. }
55.
56.
57. @Override
58. public void destroy() {
59. }
60.}
(4)UdcInfo对象结构,其中UdcInfoDto存放该请求的用户信息和资源池信息
1.package com.ideal.cloud.adapter.common.utils;
2.
3.import com.cloud.adapter.common.dto.UdcInfoDto;
4.
5.public class UdcInfo {
6. public static ThreadLocal<UdcInfoDto> udcInfoThreadLocal = new ThreadLocal<>();
7.
8. public static UdcInfoDto getUdcInfo() {
9. return udcInfoThreadLocal.get();
10. }
11.
12. public static void setUdcInfo(UdcInfoDto udcInfo) {
13. udcInfoThreadLocal.set(udcInfo);
14. }
15.
16. public static void remove(){
17. udcInfoThreadLocal.remove();
18. }
19.}
(5)自动获取路由信息
AbstractHandlerRegister.getHander的实现代码如下,采用泛型写法,针对不同的接口层对象,根据该请求对应的资源池信息和接口类型,从路由map中获取到不同的实现对象进行接口调用:
1.public static <T> T getHandler(Class T) throws PlatformHandlerNotFoundException {
2. try{
3. log.info("handlerMap:{}, class:{}, udcInfo:{}", JSON.toJSONString(handlerMap.keySet()), T.getName(), JSON.toJSONString(UdcInfo.getUdcInfo()));
4. return (T)handlerMap.get(T.getName()).get(UdcInfo.getUdcInfo().getDC_TYPE());
5. } catch(Exception e){
6. log.error("failed to get handler for Class{}, cause:{}", T.getName(), e);
7. throw new PlatformHandlerNotFoundException();
8. }
9.}
(B)场景实操举例-创建子网
基于以上方案,创建子网的抽象业务层实现可以抽象如下,到此就实现了资源池逻辑的完全隔离:
1.@Override
2.public void createSubnet(NetworkReq networkReq) {
3. // 不同资源池的创建子网公共业务逻辑(此处省略)
4. ...
5.
6. // 不同资源池差异化处理逻辑(各个starter中封装的)
7. NetworksAction networksAction = AbstractHandlerRegister.getHandler(NetworksAction.class);
8. networksAction.createSubnet(networkReq, networkResult);
9.
10.
11. // 公共信息入库(数据库表设计是跨资源池设计的,每种资源池类型的子网数据都放到一张表中,用资源池标识区分)
12. insertSubnetInfo(networkResult);
13.}
(六)方案总结
优点:
(1)降低新资源池接入工作量:
针对现有API可覆盖新资源池类型,抽象业务层业务逻辑可复用的场景,只需要在资源池组件层新增一个新模块,逐一实现统一接口层的接口。
针对新资源池存在差异化特性时,需要在对外接口层和统一接口层增加新的接口来支撑差异化业务。根据目前接入情况看,多云之间大部分业务接口是可以复用的。
(2)提高资源池纳管的效率:
可根据当前项目需要纳管的资源池类型,动态装载资源池组件。部分公共的工作已经抽象共享完成,降低了研发的成本和时间。目前根据实践经验看,纳管资源池研发的效率平均提高20%左右。
缺点:
(1)模型的抽象通用性过于复杂:
兼容各种云厂商的模型抽象非常复杂,各种定制场景无法满足,兼容性也收到诸多挑战。需要裁剪取舍,不断迭代。
(2)适配组件的启动时间过长:
Spring Boot Starter 本身启动时间过长,对于纳管组件动态纳管受到了部分制约和挑战,后面可以用轻量的quarks框架,或者Graval VM 替代。
(七)致谢
感谢在混合云研发中同学的辛苦攻坚,保证了项目的按时上线,同时本文的内容也得到了混合云袁同学,杨同学,燕同学等的素材和代码支持。
每一份努力和付出值得被赞扬和鼓励。