解決Out Of Memory問(wèn)題實(shí)戰(zhàn)
最近用solr進(jìn)行了一個(gè)做索引的測(cè)試,在長(zhǎng)時(shí)間運(yùn)行做索引的程序之后,會(huì)出現(xiàn)堆內(nèi)存溢出的錯(cuò)誤。本文Po出簡(jiǎn)單代碼,并對(duì)該問(wèn)題進(jìn)行分析和解決。
solr版本為5.5.0,使用三臺(tái)服務(wù)器配置solr集群,solr以cloud方式啟動(dòng),使用自己配置的zookeeper。在solr上新建一個(gè)數(shù)據(jù)集,并分為3片,每片配置兩個(gè)replica,交叉?zhèn)浞荨?/p>
要做索引的數(shù)據(jù)量是2600+萬(wàn),存儲(chǔ)在MySql數(shù)據(jù)庫(kù)表中,數(shù)據(jù)一直在更新。一次從數(shù)據(jù)庫(kù)表中查詢5000條數(shù)據(jù)。solr搜索主要針對(duì)標(biāo)題和內(nèi)容,因此需要將表中的標(biāo)題和內(nèi)容做到solr中。其中內(nèi)容占用空間非常大,在數(shù)據(jù)庫(kù)中使用mediumtext進(jìn)行存儲(chǔ)。
數(shù)據(jù)集的配置如下:
- <field name="id" type="string" indexed="true" stored="true" required="true" />
- <field name="title" type="text_ik" indexed="true" stored="true" />
- <field name="url" type="string" indexed="false" stored="true" />
- <field name="intime" type="string" indexed="true" stored="true"/>
- <field name="content" type="text_ik" indexed="true" stored="false"/>
- <!-- for title and content -->
- <field name="allcontent" type="text_ik" indexed="true" stored="false" multiValued="true"/>
- <copyField source="title" dest="allcontent" />
- <copyField source="content" dest="allcontent" />
搜索模式分為標(biāo)題檢索和全文檢索,因此配置了allcontent復(fù)合字段,將標(biāo)題和內(nèi)容都放到這里。
做索引的程序使用Java實(shí)現(xiàn),具體思路如下:
- 由于數(shù)據(jù)一直在更新,因此使用while(true)循環(huán)進(jìn)行處理,一次循環(huán)查詢5000條數(shù)據(jù);
- 數(shù)據(jù)量很大,如果程序出現(xiàn)異常停止運(yùn)行,要保證下次重新啟動(dòng)時(shí)從上次停的“點(diǎn)”繼續(xù)做索引,因此要將這個(gè)“點(diǎn)”存儲(chǔ)在文件中,防止丟失,本程序使用數(shù)據(jù)插入時(shí)間作為這個(gè)“點(diǎn)”;
- 一次查詢5000條數(shù)據(jù)做處理,統(tǒng)一插入到solr中。
介紹了這么多,終于把前提說(shuō)完了,下面上類圖和具體代碼,說(shuō)明問(wèn)題。
- public abstract class SolrAbstract{
- public static final Logger log = Logger.getLogger(SolrAbstract.class);
- public HttpSolrClient server;
- public List data; // 數(shù)據(jù)庫(kù)中需要處理的數(shù)據(jù)
- public Collection docs = new CopyOnWriteArrayList();
- public SolrAbstract(HttpSolrClient server) throws IOException, SolrServerException {
- log.info("開始做索引");
- if(server==null)
- throw new SolrServerException("server不能為空");
- this.server = new HttpSolrClient(getUrl());
- }
- public SolrAbstract()throws SolrServerException,IOException{
- log.info("開始做索引");
- this.server = new HttpSolrClient(getUrl());
- }
- public SolrAbstract(List data) throws IOException, SolrServerException {
- if(data == null || data.isEmpty()) {
- try {
- throw new InvalidParameterException("List不能為空");
- } catch (InvalidParameterException e) {
- e.printStackTrace();
- }
- }
- this.data = data;
- }
- public String getUrl() {
- return "http://192.168.20.10:8983/solr/test/"; // test為數(shù)據(jù)集名稱
- }
- }
- public class DoIndex extends SolrAbstract {
- public DoIndex(String url) throws SolrServerException, IOException {
- super();
- }
- public void process() throws Exception {
- for (int i = 0; i < this.data.size(); i++) {
- Product p = (Product) this.data.get(i);
- SolrInputDocument doc = new SolrInputDocument();
- doc.addField("id", p.getId());
- doc.addField("title", p.getTitle());
- doc.addField("url", p.getUrl());
- doc.addField("intime", p.getIntime());
- doc.addField("content", p.getContent());
- doc.addField("content", p.getContent());
- docs.add(doc);
- }
- }
- public synchronized void commitIndex() throws IOException, SolrServerException {
- long start = System.currentTimeMillis();
- if (docs.size() > 0) {
- server.add(docs);
- }
- server.commit();
- long endTime = System.currentTimeMillis();
- log.info("提交索引花費(fèi)時(shí)間:"+((endTime - start)));
- docs.clear();
- log.info("結(jié)束做索引");
- }
- }
- public class ProcessData {
- DoIndex index ;
- private JdbcUtil jdbc;
- private static String RECORD_INTIME ;
- public ProcessData(JdbcUtil jdbc){
- try {
- RECORD_INTIME = "/home/solr/recordIntime.txt";
- this.jdbc = jdbc;
- index = new DoIndex();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- public void processData() throws Exception{
- int startTime = Integer.parseInt(FileUtils.readFiles(RECORD_INTIME)); // ***startTime=0,從文件中讀取記錄時(shí)間
- String sql = "select id,title,content,url,intime from testTable where intime>startTime limit 5000;
- List<HashMap> list = jdbc.queryList(sql);
- while(list!=null&&list.size()>0){
- index.data = new ArrayList<Product>();
- for (int i = 0; i < list.size(); i++) {
- Map<String,Object> item = list.get(i);
- Product p = new Product();
- p.setId(item.get("id").toString());
- p.setTitle(item.get("title").toString());
- p.setUrl(item.get("url").toString());
- p.setIntime(item.get("intime").toString());
- p.setContent(item.get("content").toString());
- index.data.add(p);
- startTime = (int)item.get("intime");
- }
- index.process(); // 組裝索引數(shù)據(jù)
- index.commitIndex(); // 提交索引
- index.data.clear();
- list.clear();
- FileUtils.writeFiles(startTime, RECORD_INTIME); // 將***的時(shí)間寫入到文件中
- }
- }
- }
上述代碼在小數(shù)據(jù)量短時(shí)間內(nèi)測(cè)試沒有問(wèn)題,但運(yùn)行幾個(gè)小時(shí)之后報(bào)錯(cuò)堆內(nèi)存溢出。
檢查程序,發(fā)現(xiàn)SolrAbstract類中定義了兩個(gè)成員變量data和docs,這兩個(gè)都是“大對(duì)象”,雖然在程序中都進(jìn)行了clear(),但還是懷疑JVM并沒有及時(shí)清理這兩個(gè)對(duì)象引用的對(duì)象。還有processData()方法中將從數(shù)據(jù)庫(kù)查詢的數(shù)據(jù)存入list中,這樣可能也會(huì)導(dǎo)致內(nèi)存不會(huì)被及時(shí)回收。
抱著試試看的態(tài)度對(duì)程序進(jìn)行了修改。修改后的程序如下:
- public class ProcessData {
- private JdbcUtil jdbc;
- private static String RECORD_INTIME ;
- public ProcessData(JdbcUtil jdbc){
- try {
- RECORD_INTIME = "/home/solr/recordIntime.txt";
- this.jdbc = jdbc;
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- public void processData() throws Exception{
- int startTime = Integer.parseInt(FileUtils.readFiles(RECORD_INTIME)); // ***startTime=0,從文件中讀取記錄時(shí)間
- String sql = "select id,title,content,url,intime from testTable where intime>startTime limit 5000;
- ResultSet rs = null;
- try{
- rs = jdbc.query(sql); // 直接使用ResultSet獲取數(shù)據(jù)結(jié)果,不再將結(jié)果存入list中
- List list = new ArrayList();
- while(rs!=null&&rs.next()){
- SolrInputDocument doc = new SolrInputDocument();
- doc.addField("id", rs.getInt("id"));
- doc.addField("title",rs.getString("title"));
- doc.addField("url",rs.getString("url"));
- doc.addField("intime",rs.getInt("intime"));
- doc.addField("content", rs.getString("content"));
- list.add(doc);
- }
- commitData(list);
- list.clear();
- list.removeAll(list);
- list = null;
- }catch(Exception e) {
- e.printStackTrace();
- }finally {
- try{
- if(rs!=null) {
- rs.close();
- rs = null;
- }
- }catch(Exception e) {
- e.prepareStatement();
- }
- }
- }
- public void commitData(Collection docs) {
- try {
- long start = System.currentTimeMillis();
- if (docs.size() > 0) {
- server.add(docs);
- }
- log.info("當(dāng)前占用內(nèi)存: " + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
- server.commit();
- long endTime = System.currentTimeMillis();
- log.info("提交索引時(shí)間:"+((endTime - start)));
- docs.clear();
- docs = null;
- log.info("提交索引結(jié)束");
- } catch (SolrServerException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
代碼進(jìn)行上述修改后,運(yùn)行了幾個(gè)小時(shí),不再報(bào)堆內(nèi)存溢出的錯(cuò)誤了。
現(xiàn)在假設(shè)業(yè)務(wù)需求修改了,要求在查詢5000條數(shù)據(jù)時(shí),對(duì)每條數(shù)據(jù)進(jìn)行處理:需要根據(jù)id去其他表中查詢修改的標(biāo)題并寫入索引中。
我在上述代碼中直接進(jìn)行了修改,在while(rs!=null&&rs.next())循環(huán)中加入了查詢另外一張表的代碼。運(yùn)行程序發(fā)現(xiàn)當(dāng)前占用的內(nèi)存越來(lái)越多。于是我在服務(wù)器上使用了jstat查詢當(dāng)前虛擬機(jī)內(nèi)存占用情況,命令如下:
- jstat -gcutil pid 10000
10秒輸出一次內(nèi)存占用及垃圾回收情況,發(fā)現(xiàn)Young GC和Full GC非常頻繁,并且Full GC之后,老年代內(nèi)存回收情況并不好,監(jiān)控如下:
這里可以看到第四列老年到剛開始只占用了28.64%,運(yùn)行一段時(shí)間后內(nèi)存占用量到81.22%,進(jìn)行Full GC之后,仍然占用52.87%。
檢查代碼,發(fā)現(xiàn)是在while(rs!=null&&rs.next())里查詢另外一張表的代碼出現(xiàn)的問(wèn)題。開發(fā)匆忙,我從網(wǎng)上隨便找了一個(gè)數(shù)據(jù)庫(kù)工具類進(jìn)行的開發(fā),發(fā)現(xiàn)里面的query方法是這樣的:
- public ResultSet query(String sql){
- ResultSet rs = null;
- PreparedStatement ps = null;
- try {
- ps = conn.prepareStatement(sql);
- rs = ps.executeQuery();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- return rs;
- }
這段程序并沒有及時(shí)釋放ps,因?yàn)椴樵冾l繁,ps引用的對(duì)象一直得不到回收,導(dǎo)致這些對(duì)象進(jìn)入了老年代,并且虛擬機(jī)檢查這些對(duì)象仍然與GC Root有關(guān)聯(lián),因此導(dǎo)致老年代垃圾回收效果不好。也是這個(gè)原因?qū)е碌腨oung GC和Full GC非常頻繁。
大致找到了問(wèn)題原因,修改代碼如下:
- public void processData() throws Exception{
- int startTime = Integer.parseInt(FileUtils.readFiles(RECORD_INTIME)); // ***startTime=0,從文件中讀取記錄時(shí)間
- String sql = "select id,title,content,url,intime from testTable where intime>startTime limit 5000;
- ResultSet rs = null;
- try{
- rs = jdbc.query(sql); // 直接使用ResultSet獲取數(shù)據(jù)結(jié)果,不再將結(jié)果存入list中
- List list = new ArrayList();
- while(rs!=null&&rs.next()){
- SolrInputDocument doc = new SolrInputDocument();
- doc.addField("id", rs.getInt("id"));
- doc.addField("title",rs.getString("title"));
- doc.addField("url",rs.getString("url"));
- doc.addField("intime",rs.getInt("intime"));
- doc.addField("content", rs.getString("content"));
- PreparedStatement ps1 = jdbc.getConn().prepareStatement("select newtitle from testTable2 where id=?");
- ps1.setInt(1, rs.getInt("id"));
- ResultSet rs1 = ps1.executeQuery();
- String newtitle = "";
- while(rs1!=null&&rs1.next()) {
- newtitle = rs1.getString("newtitle");
- }
- if(rs1!=null) {
- rs1.close();
- rs1 = null;
- }
- if(ps1!=null) {
- ps1.close();
- ps1 = null;
- }
- doc.addField("newtitle",newtitle); // 當(dāng)然solr數(shù)據(jù)集的配置文件也需要修改,這里不再贅述
- list.add(doc);
- }
- commitData(list);
- list.clear();
- list.removeAll(list);
- list = null;
- }catch(Exception e) {
- e.printStackTrace();
- }finally {
- try{
- if(rs!=null) {
- rs.close();
- rs = null;
- }
- }catch(Exception e) {
- e.prepareStatement();
- }
- }
- }
經(jīng)過(guò)上面的修改,再次運(yùn)行程序,不再發(fā)生內(nèi)存溢出了,用jstat監(jiān)控如下:
可以看到Y(jié)oung GC和Full GC正常了。Full GC在開始階段基本沒有被觸發(fā),Young GC也少了很多。而第四列的老年代回收情況也變的正常了。
上面的例子很簡(jiǎn)單,導(dǎo)致堆內(nèi)存溢出的問(wèn)題也比較常見。我想說(shuō)的是看完一本書可能能被記住的內(nèi)容并不多,但隨著經(jīng)驗(yàn)的積累和實(shí)踐的增多,你會(huì)慢慢有一種感覺,能夠大致定位到問(wèn)題在哪里,這樣就夠了。
參考:《深入理解Java虛擬機(jī):JVM高級(jí)特性與***實(shí)踐(第2版)》
【本文為51CTO專欄作者“王森豐”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)注明出處】