解决 ShardingSphere 第一次请求慢的喷血日子

落日映苍穹つ 2022-10-21 13:58 647阅读 0赞

学习起因:整合了 ShardingSphere 的项目第一次访问的时候很慢,第二次访问的时候速度就快了,然后老大让我去一探究竟,然后下面就是探索的血泪史。

20210505110851470.png

通过 Arthas 生成火焰图,定位到慢在 ShardingSphere sql 解析,第一次请求使用 ANTLR 解析 SQL 耗时较长,而第二次请求由于缓存原因耗时很小

  1. curl -O https://arthas.aliyun.com/arthas-boot.jar
  2. java -jar arthas-boot.jar
  3. profiler start
  4. profiler stop

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d5ZHlkMTEw_size_16_color_FFFFFF_t_70

相关源码:

  1. private SQLStatement parse0(final String sql, final boolean useCache) {
  2. if (useCache) {
  3. Optional<SQLStatement> cachedSQLStatement = cache.getSQLStatement(sql);
  4. // 缓存存在一样的sql,就从缓存里取,sql 是还含有占位符 ?的 sql
  5. if (cachedSQLStatement.isPresent()) {
  6. return cachedSQLStatement.get();
  7. }
  8. }
  9. ParseTree parseTree = new SQLParserExecutor(databaseTypeName, sql).execute().getRootNode();
  10. SQLStatement result = (SQLStatement) ParseTreeVisitorFactory.newInstance(databaseTypeName, VisitorRule.valueOf(parseTree.getClass())).visit(parseTree);
  11. if (useCache) {
  12. cache.put(sql, result);
  13. }
  14. return result;
  15. }

神奇的现象:解析一条 sql 的平均时间为 500 ms,但在没有缓存的情况下,第二条 sql 的解析时间为 25 ms左右

  1. long start = System.currentTimeMillis();
  2. String databaseType = "MySQL";
  3. SQLParserEngine parseEngine = SQLParserEngineFactory.getSQLParserEngine(databaseType);
  4. String sql = "select 'X'";
  5. parseEngine.parse(sql, true);
  6. System.out.println("第一次解析 sql 耗时:" + (System.currentTimeMillis() - start));
  7. long start2 = System.currentTimeMillis();
  8. SQLParserEngine parseEngine2 = SQLParserEngineFactory.getSQLParserEngine(databaseType);
  9. String sql2 = "select * from t_order_n where sharding_id=? and parent_order_no=?";
  10. parseEngine2.parse(sql2, true);
  11. System.out.println("第二次解析 sql 耗时:" + (System.currentTimeMillis() - start2));

20210505123546988.png

充满好奇的我去剖析源码,然后发现 shardingJDBC 第一次解析 sql 主要慢在用工厂类创建单例对象,第二次解析时不用重新创建对象。

下面测试的代码是从 org.apache.shardingsphere.sql.parser.SQLParserEngine#parse 中抽离出来的

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d5ZHlkMTEw_size_16_color_FFFFFF_t_70 1

解决方案1:项目一启动就提前用 Antrl 去解析一条公共的sql :selec ‘x’,或者提前实例化相关对象

  1. @PostConstruct
  2. public void load() {
  3. // 提前解析一条公共的 sql
  4. String databaseType = "MySQL";
  5. SQLParserEngine parseEngine = SQLParserEngineFactory.getSQLParserEngine(databaseType);
  6. String sql = "select 'X'";
  7. parseEngine.parse(sql, true);
  8. // 提前实例化相关对象
  9. SQLParser sqlParser = SQLParserFactory.newInstance(databaseType, sql);
  10. ParseASTNode execute = (ParseASTNode)sqlParser.parse();
  11. ParseTree parseTree = execute.getRootNode();
  12. ParseTreeVisitorFactory.newInstance(databaseType, VisitorRule.valueOf(parseTree.getClass()));
  13. }

解决方案2:预热 sql。启动过程中从存储捞取 SQL ( 当然项目初始上线必然为空 ) 进行 SQL 解析并使用反射填充缓存,而在容器销毁之前则同样通过反射将这个期间内收集到 SQL 保存作为后续的预加载。此外,考虑到平滑发布的必要性,启动过程加载及时抛错;同时只在启动和销毁的只执行一次,因此性能和并发并无问题。

  1. @Configuration
  2. @ConditionalOnProperty(
  3. value = {"warmer.enable"},
  4. havingValue = "true"
  5. )
  6. public class WarmerConfiguration {
  7. public static String prefix;
  8. @Value("${spring.application.name}")
  9. public void setPrefix(String name) {
  10. prefix = name;
  11. }
  12. @Bean
  13. @ConditionalOnClass(RedissonClient.class)
  14. @ConditionalOnBean(RedissonClient.class)
  15. @ConditionalOnMissingBean(WarmerStorage.class)
  16. public WarmerStorage warmerStorage(RedissonClient redissonClient) {
  17. return new RedissonWarmerStorage(redissonClient);
  18. }
  19. @Bean
  20. @DependsOn("shardingDataSource")
  21. public Warmer4SQL warmer4SQL(WarmerStorage warmerStorage) {
  22. return new Warmer4SQL(warmerStorage);
  23. }
  24. // fixme 需要考虑 sql 的数量和长度问题
  25. @Slf4j
  26. public static class Warmer4SQL implements ApplicationContextAware, InitializingBean {
  27. private WarmerStorage warmerStorage;
  28. public Warmer4SQL(WarmerStorage warmerStorage) {
  29. this.warmerStorage = warmerStorage;
  30. }
  31. private static Map<String, SQLParseEngine> ENGINES;
  32. private static Field cacheInSQLParseEngine;
  33. private static Field cacheInSQLParseResultCache;
  34. private static volatile boolean init = false;
  35. static {
  36. try {
  37. Field _ENGINES = SQLParseEngineFactory.class.getDeclaredField("ENGINES");
  38. _ENGINES.setAccessible(true);
  39. ENGINES = (Map<String, SQLParseEngine>) _ENGINES.get(null);
  40. cacheInSQLParseEngine = SQLParseEngine.class.getDeclaredField("cache");
  41. cacheInSQLParseEngine.setAccessible(true);
  42. cacheInSQLParseResultCache = SQLParseResultCache.class.getDeclaredField("cache");
  43. cacheInSQLParseResultCache.setAccessible(true);
  44. classesInClassLoader = ClassLoader.class.getDeclaredField("classes");
  45. classesInClassLoader.setAccessible(true);
  46. init = true;
  47. } catch (Exception e) {
  48. log.warn("cannot get fields because of !", e);
  49. }
  50. }
  51. @PostConstruct
  52. public void load() {
  53. if (!init) return;
  54. long start = System.currentTimeMillis();
  55. log.info("loading at {}", start);
  56. loadSQL();
  57. long end = System.currentTimeMillis();
  58. log.info("loaded at {}, consumed {} milliseconds", end, end - start);
  59. }
  60. private void loadSQL() {
  61. ENGINES.keySet().forEach(databaseTypeName -> {
  62. Set<String> sqls = warmerStorage.load(databaseTypeName);
  63. log.info("the amount of sql fetched from storage is {}", sqls.size());
  64. sqls.forEach(sql -> ENGINES.get(databaseTypeName).parse(sql, true));
  65. log.debug("the details of sql fetched from storage are {}", sqls);
  66. });
  67. }
  68. // 异常已由 postProcessBeforeDestruction 处理
  69. @PreDestroy
  70. public void save() {
  71. if (!init) return;
  72. long start = System.currentTimeMillis();
  73. log.info("saving at {}", start);
  74. saveSQL();
  75. long end = System.currentTimeMillis();
  76. log.info("saved at {}, consumed {} milliseconds", end, end - start);
  77. }
  78. private void saveSQL() {
  79. ENGINES.keySet().forEach(databaseTypeName -> {
  80. Set<String> sqls = getParsedSQL(databaseTypeName);
  81. log.info("the amount of sql going to be saved is {}", sqls.size());
  82. warmerStorage.save(databaseTypeName, sqls);
  83. log.debug("the details of sql going to be saved are {}", sqls);
  84. });
  85. }
  86. @SneakyThrows
  87. private Set<String> getParsedSQL(String databaseTypeName) {
  88. return ((Cache) cacheInSQLParseResultCache.get(cacheInSQLParseEngine.get(ENGINES.get(databaseTypeName)))).asMap().keySet();
  89. }
  90. }
  91. }

在项目启动类加入上述代码后,发现第一次还是有点慢,重新生成火焰图,发现慢在了 sql 路由

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d5ZHlkMTEw_size_16_color_FFFFFF_t_70 2

解决方案:项目启动前执行一次完整的查询,即 sql解析,改写,路由,执行

  1. @PostConstruct
  2. public void load() throws Exception {
  3. Connection connection = dataSource.getConnection();
  4. String sql = "select 'X'";
  5. PreparedStatement preparedStatement = connection.prepareStatement(sql);
  6. preparedStatement.execute();
  7. }

结果:从 678 ms 变成 379 ms

发表评论

表情:
评论列表 (有 0 条评论,647人围观)

还没有评论,来说两句吧...

相关阅读

    相关 第一访问

    一、背景 无论在测试中还是在线上,我们都会发现在java服务刚开始启动之后,第一个请求会比正常的请求响应时间慢很多,一般会到达几百ms乃至1秒。 如果我们的调用方服务设