解决Out Of Memory问题实战

企业动态
最近用solr进行了一个做索引的测试,在长时间运行做索引的程序之后,会出现堆内存溢出的错误。本文Po出简单代码,并对该问题进行分析和解决。

最近用solr进行了一个做索引的测试,在长时间运行做索引的程序之后,会出现堆内存溢出的错误。本文Po出简单代码,并对该问题进行分析和解决。

数据

solr版本为5.5.0,使用三台服务器配置solr集群,solr以cloud方式启动,使用自己配置的zookeeper。在solr上新建一个数据集,并分为3片,每片配置两个replica,交叉备份。

要做索引的数据量是2600+万,存储在MySql数据库表中,数据一直在更新。一次从数据库表中查询5000条数据。solr搜索主要针对标题和内容,因此需要将表中的标题和内容做到solr中。其中内容占用空间非常大,在数据库中使用mediumtext进行存储。

数据集的配置如下:

  1. <field name="id" type="string" indexed="true" stored="true" required="true" />  
  2. <field name="title" type="text_ik" indexed="true" stored="true" /> 
  3. <field name="url" type="string" indexed="false" stored="true" /> 
  4. <field name="intime" type="string" indexed="true" stored="true"/> 
  5. <field name="content" type="text_ik" indexed="true" stored="false"/> 
  6. <!-- for title and content --> 
  7. <field name="allcontent" type="text_ik" indexed="true" stored="false" multiValued="true"/> 
  8. <copyField source="title" dest="allcontent" />       
  9. <copyField source="content" dest="allcontent" /> 

搜索模式分为标题检索和全文检索,因此配置了allcontent复合字段,将标题和内容都放到这里。

做索引的程序使用Java实现,具体思路如下:

  1. 由于数据一直在更新,因此使用while(true)循环进行处理,一次循环查询5000条数据;
  2. 数据量很大,如果程序出现异常停止运行,要保证下次重新启动时从上次停的“点”继续做索引,因此要将这个“点”存储在文件中,防止丢失,本程序使用数据插入时间作为这个“点”;
  3. 一次查询5000条数据做处理,统一插入到solr中。

介绍了这么多,终于把前提说完了,下面上类图和具体代码,说明问题。

做索引的程序使用Java实现图

  1. public abstract class SolrAbstract{ 
  2.   
  3.     public static final Logger log = Logger.getLogger(SolrAbstract.class); 
  4.       
  5.     public HttpSolrClient server; 
  6.     public List data; // 数据库中需要处理的数据 
  7.     public Collection docs = new CopyOnWriteArrayList(); 
  8.       
  9.     public  SolrAbstract(HttpSolrClient server) throws IOException, SolrServerException { 
  10.         log.info("开始做索引");   
  11.         if(server==null) 
  12.             throw new SolrServerException("server不能为空"); 
  13.         this.server = new HttpSolrClient(getUrl()); 
  14.     } 
  15.       
  16.     public SolrAbstract()throws SolrServerException,IOException{ 
  17.         log.info("开始做索引"); 
  18.         this.server = new HttpSolrClient(getUrl()); 
  19.     } 
  20.   
  21.     public SolrAbstract(List data) throws IOException, SolrServerException { 
  22.         if(data == null || data.isEmpty()) { 
  23.             try { 
  24.                 throw new InvalidParameterException("List不能为空"); 
  25.             } catch (InvalidParameterException e) { 
  26.                 e.printStackTrace(); 
  27.             } 
  28.         } 
  29.         this.data = data; 
  30.     } 
  31.   
  32.     public String getUrl() { 
  33.         return "http://192.168.20.10:8983/solr/test/"; // test为数据集名称 
  34.     } 
  35.   
  36. public class DoIndex extends SolrAbstract { 
  37.       
  38.     public DoIndex(String url) throws SolrServerException, IOException { 
  39.         super(); 
  40.     } 
  41.       
  42.     public void process() throws Exception { 
  43.         for (int i = 0; i < this.data.size(); i++) { 
  44.             Product p = (Product) this.data.get(i); 
  45.             SolrInputDocument doc = new SolrInputDocument(); 
  46.             doc.addField("id", p.getId()); 
  47.             doc.addField("title", p.getTitle()); 
  48.             doc.addField("url", p.getUrl()); 
  49.             doc.addField("intime", p.getIntime()); 
  50.             doc.addField("content", p.getContent()); 
  51.             doc.addField("content", p.getContent()); 
  52.             docs.add(doc); 
  53.         } 
  54.     } 
  55.   
  56.     public synchronized void commitIndex() throws IOException, SolrServerException { 
  57.         long start = System.currentTimeMillis(); 
  58.         if (docs.size() > 0) { 
  59.             server.add(docs); 
  60.         }                
  61.         server.commit(); 
  62.         long endTime = System.currentTimeMillis(); 
  63.         log.info("提交索引花费时间:"+((endTime - start))); 
  64.         docs.clear(); 
  65.         log.info("结束做索引"); 
  66.     } 
  67.   
  68. public class ProcessData { 
  69.       
  70.     DoIndex index ; 
  71.     private JdbcUtil jdbc; 
  72.     private static String RECORD_INTIME ; 
  73.       
  74.     public ProcessData(JdbcUtil jdbc){ 
  75.         try { 
  76.             RECORD_INTIME = "/home/solr/recordIntime.txt"
  77.             this.jdbc = jdbc; 
  78.             index = new DoIndex(); 
  79.         } catch (Exception e) { 
  80.             e.printStackTrace(); 
  81.         } 
  82.     } 
  83.   
  84.     public void processData() throws Exception{ 
  85.         int startTime = Integer.parseInt(FileUtils.readFiles(RECORD_INTIME)); // ***startTime=0,从文件中读取记录时间 
  86.         String sql = "select id,title,content,url,intime from testTable where intime>startTime limit 5000; 
  87.         List<HashMap> list = jdbc.queryList(sql); 
  88.         while(list!=null&&list.size()>0){ 
  89.             index.data = new ArrayList<Product>(); 
  90.             for (int i = 0; i < list.size(); i++) { 
  91.                 Map<String,Object> item =  list.get(i); 
  92.                 Product p = new Product(); 
  93.                 p.setId(item.get("id").toString()); 
  94.                 p.setTitle(item.get("title").toString()); 
  95.                 p.setUrl(item.get("url").toString()); 
  96.                 p.setIntime(item.get("intime").toString()); 
  97.                 p.setContent(item.get("content").toString()); 
  98.                 index.data.add(p); 
  99.                 startTime = (int)item.get("intime"); 
  100.             }        
  101.             index.process(); // 组装索引数据 
  102.             index.commitIndex(); // 提交索引 
  103.             index.data.clear(); 
  104.             list.clear(); 
  105.             FileUtils.writeFiles(startTime, RECORD_INTIME); // 将***的时间写入到文件中 
  106.         } 
  107.     } 

上述代码在小数据量短时间内测试没有问题,但运行几个小时之后报错堆内存溢出。

检查程序,发现SolrAbstract类中定义了两个成员变量data和docs,这两个都是“大对象”,虽然在程序中都进行了clear(),但还是怀疑JVM并没有及时清理这两个对象引用的对象。还有processData()方法中将从数据库查询的数据存入list中,这样可能也会导致内存不会被及时回收。

抱着试试看的态度对程序进行了修改。修改后的程序如下:

  1. public class ProcessData { 
  2.       
  3.     private JdbcUtil jdbc; 
  4.     private static String RECORD_INTIME ; 
  5.     public ProcessData(JdbcUtil jdbc){ 
  6.         try { 
  7.             RECORD_INTIME = "/home/solr/recordIntime.txt"
  8.             this.jdbc = jdbc; 
  9.         } catch (Exception e) { 
  10.             e.printStackTrace(); 
  11.         } 
  12.     } 
  13.   
  14.     public void processData() throws Exception{ 
  15.         int startTime = Integer.parseInt(FileUtils.readFiles(RECORD_INTIME)); // ***startTime=0,从文件中读取记录时间 
  16.         String sql = "select id,title,content,url,intime from testTable where intime>startTime limit 5000; 
  17.         ResultSet rs = null
  18.         try{ 
  19.             rs = jdbc.query(sql); // 直接使用ResultSet获取数据结果,不再将结果存入list中 
  20.             List list = new ArrayList(); 
  21.             while(rs!=null&&rs.next()){ 
  22.                 SolrInputDocument doc = new SolrInputDocument(); 
  23.                 doc.addField("id", rs.getInt("id")); 
  24.                 doc.addField("title",rs.getString("title")); 
  25.                 doc.addField("url",rs.getString("url")); 
  26.                 doc.addField("intime",rs.getInt("intime")); 
  27.                 doc.addField("content", rs.getString("content")); 
  28.                 list.add(doc); 
  29.             } 
  30.             commitData(list); 
  31.             list.clear(); 
  32.             list.removeAll(list); 
  33.             list = null
  34.               
  35.         }catch(Exception e) { 
  36.             e.printStackTrace(); 
  37.         }finally { 
  38.             try{ 
  39.                 if(rs!=null) { 
  40.                     rs.close(); 
  41.                     rs = null
  42.                 } 
  43.             }catch(Exception e) { 
  44.                 e.prepareStatement(); 
  45.             } 
  46.         } 
  47.     } 
  48.       
  49.     public void commitData(Collection docs) { 
  50.         try { 
  51.             long start = System.currentTimeMillis(); 
  52.             if (docs.size() > 0) { 
  53.                 server.add(docs); 
  54.             } 
  55.             log.info("当前占用内存: " + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())); 
  56.             server.commit(); 
  57.             long endTime = System.currentTimeMillis(); 
  58.             log.info("提交索引时间:"+((endTime - start))); 
  59.             docs.clear(); 
  60.             docs = null
  61.             log.info("提交索引结束"); 
  62.         } catch (SolrServerException e) { 
  63.             e.printStackTrace(); 
  64.         } catch (IOException e) { 
  65.             e.printStackTrace(); 
  66.         } 
  67.     } 

代码进行上述修改后,运行了几个小时,不再报堆内存溢出的错误了。

现在假设业务需求修改了,要求在查询5000条数据时,对每条数据进行处理:需要根据id去其他表中查询修改的标题并写入索引中。

我在上述代码中直接进行了修改,在while(rs!=null&&rs.next())循环中加入了查询另外一张表的代码。运行程序发现当前占用的内存越来越多。于是我在服务器上使用了jstat查询当前虚拟机内存占用情况,命令如下:

  1. jstat -gcutil pid 10000 

10秒输出一次内存占用及垃圾回收情况,发现Young GC和Full GC非常频繁,并且Full GC之后,老年代内存回收情况并不好,监控如下:

10秒输出一次内存占用及垃圾回收情况

这里可以看到第四列老年到刚开始只占用了28.64%,运行一段时间后内存占用量到81.22%,进行Full GC之后,仍然占用52.87%。

检查代码,发现是在while(rs!=null&&rs.next())里查询另外一张表的代码出现的问题。开发匆忙,我从网上随便找了一个数据库工具类进行的开发,发现里面的query方法是这样的:

  1. public ResultSet query(String sql){ 
  2.     ResultSet rs = null
  3.     PreparedStatement ps = null
  4.     try { 
  5.         ps = conn.prepareStatement(sql); 
  6.         rs = ps.executeQuery(); 
  7.     } catch (SQLException e) { 
  8.         e.printStackTrace(); 
  9.     } 
  10.     return rs; 

这段程序并没有及时释放ps,因为查询频繁,ps引用的对象一直得不到回收,导致这些对象进入了老年代,并且虚拟机检查这些对象仍然与GC Root有关联,因此导致老年代垃圾回收效果不好。也是这个原因导致的Young GC和Full GC非常频繁。

大致找到了问题原因,修改代码如下:

  1. public void processData() throws Exception{ 
  2.     int startTime = Integer.parseInt(FileUtils.readFiles(RECORD_INTIME)); // ***startTime=0,从文件中读取记录时间 
  3.     String sql = "select id,title,content,url,intime from testTable where intime>startTime limit 5000; 
  4.     ResultSet rs = null
  5.     try{ 
  6.         rs = jdbc.query(sql); // 直接使用ResultSet获取数据结果,不再将结果存入list中 
  7.         List list = new ArrayList(); 
  8.         while(rs!=null&&rs.next()){ 
  9.             SolrInputDocument doc = new SolrInputDocument(); 
  10.             doc.addField("id", rs.getInt("id")); 
  11.             doc.addField("title",rs.getString("title")); 
  12.             doc.addField("url",rs.getString("url")); 
  13.             doc.addField("intime",rs.getInt("intime")); 
  14.             doc.addField("content", rs.getString("content")); 
  15.             PreparedStatement ps1 = jdbc.getConn().prepareStatement("select newtitle from testTable2 where id=?"); 
  16.             ps1.setInt(1, rs.getInt("id")); 
  17.             ResultSet rs1 = ps1.executeQuery(); 
  18.             String newtitle = ""
  19.             while(rs1!=null&&rs1.next()) { 
  20.                 newtitle = rs1.getString("newtitle"); 
  21.             } 
  22.             if(rs1!=null) { 
  23.                 rs1.close(); 
  24.                 rs1 = null
  25.             } 
  26.             if(ps1!=null) { 
  27.                 ps1.close(); 
  28.                 ps1 = null
  29.             } 
  30.             doc.addField("newtitle",newtitle); // 当然solr数据集的配置文件也需要修改,这里不再赘述 
  31.             list.add(doc); 
  32.         } 
  33.         commitData(list); 
  34.         list.clear(); 
  35.         list.removeAll(list); 
  36.         list = null
  37.           
  38.     }catch(Exception e) { 
  39.         e.printStackTrace(); 
  40.     }finally { 
  41.         try{ 
  42.             if(rs!=null) { 
  43.                 rs.close(); 
  44.                 rs = null
  45.             } 
  46.         }catch(Exception e) { 
  47.             e.prepareStatement(); 
  48.         } 
  49.     } 

经过上面的修改,再次运行程序,不再发生内存溢出了,用jstat监控如下:

用jstat监控

可以看到Young GC和Full GC正常了。Full GC在开始阶段基本没有被触发,Young GC也少了很多。而第四列的老年代回收情况也变的正常了。

上面的例子很简单,导致堆内存溢出的问题也比较常见。我想说的是看完一本书可能能被记住的内容并不多,但随着经验的积累和实践的增多,你会慢慢有一种感觉,能够大致定位到问题在哪里,这样就够了。

参考:《深入理解Java虚拟机:JVM高级特性与***实践(第2版)》

【本文为51CTO专栏作者“王森丰”的原创稿件,转载请注明出处】

责任编辑:赵宁宁 来源: 神算子
相关推荐

2012-10-08 09:50:45

2017-02-24 15:28:33

Android内存溢出方法总结

2023-07-26 15:46:52

Docker管理容器

2021-11-09 10:20:15

MySQL深分页数据库

2021-09-26 06:43:07

MySQL深分页优化

2021-09-27 13:33:03

MySQL深分页数据库

2022-09-02 16:07:02

团队问题

2012-01-13 13:05:41

Scale Out网络

2013-03-20 09:54:07

2009-08-21 17:48:28

.NET框架DLL Hell问题

2009-12-08 16:30:29

WCF程序

2010-03-10 10:24:16

Linux ssh后门

2013-12-05 09:45:04

HadoopHadoop架构图

2009-08-06 10:35:27

C# lock thi

2012-09-05 11:09:15

SELinux操作系统

2009-09-22 17:32:38

Hibernate A

2009-12-29 11:40:50

2010-02-06 16:13:49

Ubuntu Auda

2010-07-15 14:40:42

AIX TELNET

2012-03-02 13:52:26

Javajstack
点赞
收藏

51CTO技术栈公众号