促销优化实战分享

本篇文章仅限网易公司内部分享,如需转载,请取得作者本人同意授权
      在之前分享的文章中也介绍过,促销提供给运营后台,POP商家的服务存在性能问题。在对于这些方法进行性能优化中遇到的也应该是大家在日常开发中经常遇到的问题,或者忽略的问题;将这些问题分析出来供大家参考!通过最终的压测结果可以发现,就是对这些小问题的优化,接口性能有了质的变化;希望在今后的开发中可以跳过这些坑。 
   在上一次优化中将数据库搞垮了以后,再次优化就需要知道性能问题出现在什么地方,进而有针对性地进行优化。为此需要对方法调用栈各个阶段耗时统计,找到性能问疼点,进行重点优化。
   主要介绍了
  1. 性能统计工具
  2. 如何分析性能日志
  3. 取消循环RPC调用
  4. 取消循环写缓存
  5. dubbo异步调用
  6. 减少返回给前端的数据量
   优化工具准备
     
很多时候方法存在性能问题都是存在循环查询数据库或者远程调用,但是有一些因此很深的循环调用通过走读代码是很难发现的;这个时候  为了知道方法调用栈具体耗时,需要在代码中打很多测试用的log,去查找方法耗时比较多地方,但是这样方式很难一个方法调用栈耗时进行完成统计!为了找到一个更优的方式,需要自己去实现一个统计工具。
    为了减少对于代码的侵入,实现了一个调用栈耗时统计的工具,javaagent方式对字节码进行增强,优化完成后删除了javaagent就可以了,不会对现有代码造成影响。性能日志统计工具主要是通过javaasist对字节码进行增强
if (isStarted) {
       method.insertBefore("ProfilerUtil.start(\"" + method.getLongName() + "\");");
   } else {
      method.insertBefore("ProfilerUtil.entery(\"" + method.getLongName() + "\");");
   }
   method.insertAfter("ProfilerUtil.release();", true);
   下图是性能日志工具对促销方案明细查询和保存controller调用栈的耗时统计。


   分析耗时统计日志
      通过上图中的日志可以发现在  com.netease.kaola.promotionms.converter.ActivityConverter.activityGoodsViewListTransfer(java.util.List) 这个方法中居然存在了RPC调用,而且这个地方两次dubbo调用耗时占比居然达到了46%(36%+10%)。在保存controller中同样存在 对如下方法的调用com.netease.kaola.promotionms.converter.ActivityConverter.getGoodsDetailMapByIds(java.util.List) ;而且耗时比较大,同时存在循环调用情况。需要对此方法进行优化,
由于服务提供方不在本次优化范围内,服务提供方无法优化的清空下,只能通过调用方进行优化。
   循环中进行耗时处理,这个是存在性能问题方法中普遍存在的问题,如循环中远程调用,循环中读写缓存,循环中读写数据库;循环中对单条数据进行处理,可能代码比较容易处理,但是这就给性能问题带来隐患;随着循环次数的增加,接口性能急剧变差。
    优化循环RPC调用
  下图是典型的因为在循环中进行了数据库访问和RPC调用导致的性能问题,单次数据库查询耗时只有2ms,但是如果循环了200次,这个耗时就是400ms,这个是很大的耗时了。
同样在一次循环中有两次RPC调用,单个接口性能都很好,耗时分别为6ms和9ms,循环了200次以后耗时就是3000ms,这样的接口就存在明显的性能问题,只要将其修改为批量处理性能就会有明显提升。

优化代码对比
   在优化之前,在for循环中对shopInfo进行处理后设置到GoodsDetail对象中,修改后是对goodsDetail赋一个新对象,新对象中只有基本信息,将多有goodsDetail都设置完成后,批量查询ShopInfo信息后再完善ShopInfo对象,通过值引用的特性可以知道此处的修改对于GoodsDetail对象中设置的shopInfo同样是生效的,这样就有效地减少了RPC调用(之前循环调用了200次,目前只需要调用一次)

 


优化循环写缓存


dubbo异步调用
 
部分服务提供方不支持超过1000个请求参数的查询,为此在代码中对请求参数进行了分页处理,循环同步调用!这样如果循环了多次,总耗时就是多次耗时之和。之前的优化中也遇到过在循环中进行dubbo调用,每次都是单个数据查询,优化思路是批量获取;而目前是对批量调用进行优化,由于服务提供方不在本次优化范围内,本次的为调用方优化,主要的优化思路是将数据量比较大的请求参数拆分为多个dubbo请求,并发调用服务,这样耗时则由耗时最大的一次调用决定。是不是并发次数越多越好呢?理论上并发次数越多,单次请求参数越小,响应时间则越小!可是在实际应用过程中发现并发次数过多时导致了服务提供方异常。所以在优化耗时的同时还需要考虑到大数据量时服务提供方的能力!

异步调用辅助工具类实现
**
 * Desc:Dubbo异步调用辅助工具类
 * 
 * @author wei.zw
 * @since 2017714日 下午4:36:08
 * @version v 0.1
 */
public class RpcAsyncUtil {

	private static final Logger logger = LoggerFactory.getLogger(RpcAsyncUtil.class);

	/**
	 * dubbo异步调用,将远程调用结果组合后返回;本次耗时取决于最大的耗时
	 * 
	 * @param paramList
	 *            需要进行远程调用所有参数列表长度
	 * @param pageSize
	 *            一次远程调用使用的列表最大长度
	 * @param syncCallable
	 *            具体dubbo调用
	 * @return
	 * @author wei.zw
	 */
	public static  List async(List paramList, int pageSize, final SyncCallable syncCallable) {
		if (pageSize < 200) {
			pageSize = 200;
		}
		List> subList = subList(paramList, pageSize);
		List>> futures = new ArrayList<>();
		for (final List sub : subList) {
			futures.add(RpcContext.getContext().asyncCall(new Callable>() {

				@Override
				public List call()
						throws Exception {
					return syncCallable.call(sub);
				}
			}));
		}
		List result = new ArrayList<>();
		try {
			for (Future> future : futures) {

				List list = future.get();
				if (CollectionUtils.isNotEmpty(list)) {
					result.addAll(list);
				}
			}
		} catch (Exception e) {
			logger.warn(ToStringBuilder.reflectionToString(syncCallable) + ",调用异常", e);
			throw new RuntimeException(e);
		}

		return result;

	}

	/**
	 * 
	 * 
	 * @param paramList
	 *            请求参数
	 * @param pageSize
	 *            要求参数不能小于200; 每次dubbo调用的参数大小,请设置合适值,如果数值设置过小会导致服务提供方线程数量飙升,如果设置过大可能会导致单次响应时间变长
	 * @param syncCallable
	 *            具体的dubbo调用方法,在该接口方法中不能有其他处理逻辑只能是一个dubbo 调用
	 * @return
	 * @author wei.zw
	 */
	public static  Map async(List paramList, int pageSize, final SyncMapCallable syncCallable) {
		if (pageSize < 200) {
			pageSize = 200;
		}
		List> subList = subList(paramList, pageSize);
		List>> futures = new ArrayList<>();
		for (final List sub : subList) {
			futures.add(RpcContext.getContext().asyncCall(new Callable>() {

				@Override
				public Map call()
						throws Exception {
					return syncCallable.call(sub);
				}
			}));
		}
		Map result = new HashMap<>();
		try {
			for (Future> future : futures) {

				Map map = future.get();
				if (MapUtils.isNotEmpty(map)) {
					result.putAll(map);
				}
			}
		} catch (Exception e) {
			logger.warn(ToStringBuilder.reflectionToString(syncCallable) + ",调用异常", e);
			throw new RuntimeException(e);
		}

		return result;

	}

	/**
	 * 拆分list
	 * 
	 * @param sourceList
	 * @param splitSize
	 * @return
	 * @author roy
	 * @since 2017年1月17日
	 */
	private static  List> subList(List sourceList, int splitSize) {
		List> list = Lists.newArrayList();
		if (sourceList == null || sourceList.size() == 0) {
			return list;
		}

		if (sourceList.size() <= splitSize) {
			list.add(sourceList);
			return list;
		}

		int page = sourceList.size() / splitSize + 1;

		// 模拟分页获取
		for (int i = 0; i < page; i++) {
			List childList = Lists.newArrayList();
			for (int j = i * splitSize; j < (i + 1) * splitSize; j++) {
				// 如果交表大于源列表长度 退出
				if (j >= sourceList.size()) {
					break;
				}
				childList.add(sourceList.get(j));
			}
			list.add(childList);
		}

		return list;
	}

	public static interface SyncCallable {
		/**
		 * 该方法中只能是一个远程调用,不能使用其他方法
		 * 
		 * @param params
		 * @return
		 * @author wei.zw
		 */
		public List call(List params);
	}

	public static interface SyncMapCallable {
		/**
		 * 该方法中只能是一个远程调用,不能使用其他方法
		 * 
		 * @param params
		 * @return
		 * @author wei.zw
		 */
		public Map call(List params);
	}
 }
 针对dubbo进行优化后的统计日志,可以发现getGoodsDetailMapByIds耗时由之前占比36%  467ms降到了 占比21%,205ms。查询方法总耗减少了约25%;同样保存方法也有了15%左右性能提升。

 
     从用户角度看这样的优化是没有什么明显的效果的,针对一次耗时大约有9秒的查询请求来讲,优化了几百毫秒是无法有质上的提升的 。方法本身提升不大的情况下,就需要针对返回数据量进行优化
  减少返回数据量
      
通过查看返回数据,可以发现返回对象中存在大量值为null的数据,如果不返回值为null的数据可以有效减少数据量。同时分析返回数据结构中是否有比较大的多余信息,发现
extInfo中skuList在新系统中不在使用!在返回给前端时将该值设置为null。同时为了提升JSON序列化性能,将jackson改为fastjson.

最终优化效果
  最终优化效果是同样的数据在不同版本中的表现,因为服务器可能存在性能差异,实际效果可能没有这么突出!
 优化前

 优化后

优化前数据量为2.3M,优化后数据量为1.3M,数据量减少了大约40%,响应时间减少了约 60%
优化后性能测试同学对相关接口进行了压测,优化前后的数据对比如下

本文来自网易实践者社区,经作者张伟 授权发布。