来自百度百科对灰度发布的阐述:
灰度发布是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
任何一个产品,为了达到快速开发,快速迭代,快速上线,并且又要保证质量,保证上线的功能在出现问题之后可以快速回切来控制影响的范围,那么就需要一套灰度发布的系统。同样的,有时候,系统上的某一个功能需要上线验证,但是因为改动会影响现有的用户,为了测试用户的实际体验,通常会将新功能发布上去,提供给部分用户使用(一般都是公司的忠实用户),然后评估新功能的影响。
最近公司因为对账流水的糟糕设计导致了一些性能问题。
公司被迫只能通过一些时间上来做限制,这个导致部分用户意见非常之大,所以只能针对现在的对账流水进行重新设计。
因为担心新的设计上线会对整个体验造成比较大的影响,毕竟现有用户对目前的对账流水没有流露出不满的情绪,如果大肆修改可能会导致用户的反感,所以我们就采取了一套灰度发布的设计。
首先,说一下灰度发布的两种情况吧【目前我所了解】。
1、当系统进行大量的改造,同时涉及多个服务同时改造的时候,包括数据库底层可能都经过修改,为了防止数据错乱,保证用户体验的体验一致,那么可以考虑在nginx这一层做转发处理,将测试用户的IP转到新的系统,新的服务处理,而正常用户则转发到旧的系统,其他的方案目前也没想到。淘宝,腾讯等大企业很少听到停机等问题,他们怎么做的我是不清楚,不过目前很多游戏都是采用开放部分地区玩家的更新策略,原理可能与这种情况类似。
2、针对部分用户,查不同的数据,跑不同的计算,跳转至不同的页面,最终展示出不同的效果。这种在在接口级别上做处理相对简单很多。但是这种也有很大的弊端,app是无法及时更新的,所以处理PC端页面展示之外,同时要好考虑兼容移动端。
因为本次对账流水改造采用的是第二种,所以我们就聊聊在接口上是我们是如何做处理的。
我们采用Java体系,那么技术框架自然就是目前流行的springmvc了,要在restful api上处理,我们就要做拦截,当前一个http请求达到tomcat容器之后,会起调个线程来做处理,调用后端的接口。这个时候,我们就要在spring mvc的controller上做拦截。
拦截有多种实现方式,比方说直接写一个拦截器,将请求全部拦截,抽取出特殊请求再做其他处理,又或者当请求访问到这个接口的时候,通过切面拦截,再做其他处理,等等……
因为是小巧的工作,只需要做灰度的流水发布,如果在拦截全部请求,那不是一种损耗吗?所以我们就采取了切面拦截的方式。
当请求起调某个接口的时候,通过切面将方法调用转到其他接口去处理。
/** * 对需要做灰度发布的接口进行标注 * * @author lennon * @time 2017年6月24日上午11:27:07 * */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface GrayscaleRelease { /** 类名*/ String targetClass() default ""; /** 方法名*/ String targetMethod() default "";}
除此之外,我们还需要一个拦截的切面
/** * * * @author lennon * @time 2017年6月24日上午11:32:11 * */@Component@Aspectclass GrayscaleReleaseAspect implements Ordered { @Around(value = "execution(* org.cn.lennon.*.*(..)) && @annotation(GrayscaleRelease)") public void around(ProceedingJoinPoint joinPoint) { boolean GrayscaleRelease = true; // 是否要进行灰度发布,即将不满足灰度发布的的请求直接穿过拦截层进入到最里边的层进行处理 if(!GrayscaleRelease) { return joinPoint.proceed(); } // 通过反射来调用另外一个方法 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Annotation methodsAnnotation = method.getAnnotation(GrayRelease.class); // 如果在灰度注解,则按灰度发布执行,否则按原有流程执行 if (methodsAnnotation != null) { // 灰度类名 Class targetClass = ((GrayRelease) methodsAnnotation).targetClass(); if (targetClass == null || targetClass.getClass().equals(Object.class)) { targetClass = joinPoint.getThis().getClass(); } // 灰度方法名 String targetMethodName = ((GrayRelease) methodsAnnotation).targetMethod(); if (StringUtil.isBlank(targetMethodName)) { targetMethodName = method.getName(); } // 注意,如果本方法与目标方法是同一个方法,应直接报错,不然会出现死循环 if (joinPoint.getThis().getClass().equals(targetClass) && method.getName().equals(targetMethodName)) { logger.error("灰度发布方法有误,不能是本方法!class=" + targetClass + ",method=" + targetMethodName); throw new Exception("灰度发布方法有误,不能是本方法!class=" + targetClass + ",method=" + targetMethodName); } Object currentBean = SpringContextHolder.getBean(targetClass); // 从bean中取出所有方法,并找到灰度目标方法 Method[] methods = currentBean.getClass().getMethods(); for (Method grayMethod : methods) { if (grayMethod.getName().equals(targetMethodName)) { // 执行目标方法,获得返回值 Object result = grayMethod.invoke(currentBean, joinPoint.getArgs()); return result; } } logger.debug("没有匹配到目标方法:" + currentBean.getClass() + "." + targetMethodName); } return joinPoint.proceed(); } public int getOrder() { // TODO Auto-generated method stub return 5; }}
通过这种办法,我们可以对部分接口做灰度发布,案例如下:
public class GrayReleaseController { @GrayRelease(targetClass=GrayReleaseHandler.class, targetMethod="XXXMethod") @AuthorizationAnnotation(resourceEnName="XXX",description="灰度发布案例") @ResponseBody @RequestMapping(value = { "/grayRelease" }) public JSON handler() { // do something }}public class GrayReleaseHandler { public JSON XXXMethod() { }}
总结:
在做灰度发布的时候,如果是系统级别的灰度发布,那么可以考虑在nginx层做转发,直接转发到某个对应的服务上。如果是部分小功能,部分接口的灰度发布,那么可以考虑通过这种切面与反射来处理,直接在代码层上做处理,这种适合小范围的修改。因为我们是周版本任务,所以经常会有这种灰度发布的情况。