痛点
日常上线流程中经常需要对接口进行预热,因为服务器每次启动后都有一定次数访问失败,如果不处理将此请求直接抛出,会降低用户体验。当服务器数量较少时,我们可以在发布机器后,待机器启动使用本地hosts更改IP,请求对应服务器接口看(1.刷新接口,2.校验返回数据)
然而当服务器数量较多时,这样的验证过程非常麻烦,每次需要修改完hosts,再去ping一下看看修改成功没,再去请求接口,整个集群只能测试几台机器,不能完全覆盖,主要存在两个问题:
- 可能存在上线后有机器没起来等问题,对于数据的校验不够完善,只能看看大致返回的量,看不出具体缺失数据。
- 还有接口请求数据不够全面,都是使用很早以前的访问参数,如用户pin,经纬度,活动ID,版本号,客户端等,不能及时更新。
公司内部其实有很多预热工具,但都是基于固定参数的形式,类似于postman使用一套参数反复请求刷新
预热类型分为内部预热和外部预热

由此想做自动预热和报文对比,减少人为干预成本,提高覆盖率,采用外部+内部预热的方式:最大程度减少对本地代码的入侵,同时还能全面覆盖服务器,并使用最新参数刷新接口。
下面是自更新参数预热工具的交互流程图

服务端配置:
这里开发了预热注解,在需要的地方加上注解,这里会拦截请求里的body+indexRequest参数,通过本地Cache缓存请求间隔次数,每隔一个小时保存一次,参数内容保存到本地缓存中,服务端不做多版本保存,每小时动态覆盖,再加上zk开关进行判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
@Component @Aspect public class StartMockAspect {
private static final Logger logger = LoggerFactory.getLogger(StartMockAspect.class); public static final String BODY = "body"; public static final String INDEX_REQUEST = "indexRequest";
@Resource private ZkConfigManager zkConfigManager;
@Pointcut(" @annotation(com.jd.o2o.app.common.mock.DeepHttpMock)") public void pointCut() { }
private static final Cache<String, Integer> NUM_CACHE = CacheBuilder.newBuilder() .expireAfterWrite(3600, TimeUnit.SECONDS).build();
@Around("pointCut()") public Object handle(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { try { boolean startFlag = zkConfigManager.getConfig(ZkConfigPathEnum.START_PARAMETER_ASPECT_SWITCH); if (Boolean.FALSE.equals(startFlag)) { return proceedingJoinPoint.proceed(); } IndexRequest indexRequest = LocaleContextHandler.getLocaleContext().getIndexRequest(); if (proceedingJoinPoint.getSignature() instanceof MethodSignature) { MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature(); Method method = methodSignature.getMethod(); Object[] args = proceedingJoinPoint.getArgs(); Object bodyRequest = args[0]; DeepHttpMock monitor = method.getAnnotation(DeepHttpMock.class); if (monitor == null || indexRequest == null || bodyRequest == null) { return proceedingJoinPoint.proceed(); } if (NUM_CACHE.get(monitor.pageSource(), () -> -1) == -1) { START_LOAD_REQUEST_MAP.put(monitor.pageSource(), ImmutableMap.of(BODY, bodyRequest, INDEX_REQUEST, indexRequest)); NUM_CACHE.put(monitor.pageSource(), 0); } } } catch (Exception e) { logger.error("StartMockAspect失败"); } return proceedingJoinPoint.proceed(); }
}
|
外部脚本通过特定接口访问每次缓存的一份数据
1 2 3 4 5 6 7 8 9
|
@RequestMapping(value = "testStartupParameters") @ResponseBody public Map<String, Object> testStartupParameters(String host) { return START_LOAD_REQUEST_MAP.getOrDefault(host, Collections.emptyMap()); }
|
python服务
这里采用python的原因是
定时抓取参数的方式比较简单,适合用脚本开发,
python能够快速完成接口请求及提供web服务,开发成本较低,
可以随时更新脚本调整代码
python共三个模块
start_parameters模块
- 拉取集群下所有服务器IP
- 根据host与IP配置的接口,拉取对应服务器上缓存的接口参数
- 保存所有动态URL参数内容到文件中
server_verify模块
- 使用保存的动态参数,自动/主动->预热触发的接口
- 使用保存的动态参数,进行新旧服务器接口的报文对比
web模块
- 提供主动预热服务操作页面
- 手动选择报文对比接口列表
下面分为三类处理
自动服务预热:

主动预热:

报文对比:

部分可操作界面
通过引用自更新参数的预热工具,相比较传统固定参数的预热工具,上线之后服务器接口性能不再出现大幅度波动,有效提高接口可用率。
start_before.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function_app_ip(){ if [[ -n "$def_host_ip" ]]; then echo ${def_host_ip} else echo `/sbin/ip addr sh | /bin/grep -v 'global secondary' | /bin/grep inet | /bin/grep -v inet6 | /bin/grep -v '127.0.0.1' | /bin/grep -v 'lo:' | /bin/awk '{print $2}' | /bin/awk -F'/' '{print $1}'| /usr/bin/head -n 1` fi }
echo "开始请求预热" _app_ip=$(function_app_ip); echo "操作机器IP:$_app_ip" echo $(date +%Y-%m-%d\ %H:%M:%S) curl http://preheat.local/start?local=pdjhome.local\&ip=$_app_ip
|
这里是用来触发预热的任务,之后会通过python内部保存的参数URL来刷接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class startHandler(tornado.web.RequestHandler): def get(self): try: ip = self.get_argument('ip') local = self.get_argument('local') print(ip, local, "发送机器预热请求") thead_one = threading.Thread(target=server_verify.web_one_start, args=(ip, local)) thead_one.start() self.write("发送机器预热请求") except Exception as e: print(e) self.write("参数错误")
application = tornado.web.Application([(r"/add", MainHandler), (r"/diff_result", diffHandler), (r"/tool", toolHandler), (r"/start", startHandler), (r"/serverDiff", PdjserviceHandler)], static_path=os.path.join(os.path.dirname(__file__), "static"), )
if __name__ == "__main__": application.listen(80) tornado.ioloop.IOLoop.instance().start()
|