解決Out Of Memory問題實戰
最近用solr進行了一個做索引的測試,在長時間運行做索引的程序之后,會出現堆內存溢出的錯誤。本文Po出簡單代碼,并對該問題進行分析和解決。
solr版本為5.5.0,使用三臺服務器配置solr集群,solr以cloud方式啟動,使用自己配置的zookeeper。在solr上新建一個數據集,并分為3片,每片配置兩個replica,交叉備份。
要做索引的數據量是2600+萬,存儲在MySql數據庫表中,數據一直在更新。一次從數據庫表中查詢5000條數據。solr搜索主要針對標題和內容,因此需要將表中的標題和內容做到solr中。其中內容占用空間非常大,在數據庫中使用mediumtext進行存儲。
數據集的配置如下:
- <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" />
搜索模式分為標題檢索和全文檢索,因此配置了allcontent復合字段,將標題和內容都放到這里。
做索引的程序使用Java實現,具體思路如下:
- 由于數據一直在更新,因此使用while(true)循環進行處理,一次循環查詢5000條數據;
- 數據量很大,如果程序出現異常停止運行,要保證下次重新啟動時從上次停的“點”繼續做索引,因此要將這個“點”存儲在文件中,防止丟失,本程序使用數據插入時間作為這個“點”;
- 一次查詢5000條數據做處理,統一插入到solr中。
介紹了這么多,終于把前提說完了,下面上類圖和具體代碼,說明問題。
- public abstract class SolrAbstract{
- public static final Logger log = Logger.getLogger(SolrAbstract.class);
- public HttpSolrClient server;
- public List data; // 數據庫中需要處理的數據
- 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為數據集名稱
- }
- }
- 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("提交索引花費時間:"+((endTime - start)));
- docs.clear();
- log.info("結束做索引");
- }
- }
- 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,從文件中讀取記錄時間
- 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(); // 組裝索引數據
- index.commitIndex(); // 提交索引
- index.data.clear();
- list.clear();
- FileUtils.writeFiles(startTime, RECORD_INTIME); // 將***的時間寫入到文件中
- }
- }
- }
上述代碼在小數據量短時間內測試沒有問題,但運行幾個小時之后報錯堆內存溢出。
檢查程序,發現SolrAbstract類中定義了兩個成員變量data和docs,這兩個都是“大對象”,雖然在程序中都進行了clear(),但還是懷疑JVM并沒有及時清理這兩個對象引用的對象。還有processData()方法中將從數據庫查詢的數據存入list中,這樣可能也會導致內存不會被及時回收。
抱著試試看的態度對程序進行了修改。修改后的程序如下:
- 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,從文件中讀取記錄時間
- String sql = "select id,title,content,url,intime from testTable where intime>startTime limit 5000;
- ResultSet rs = null;
- try{
- rs = jdbc.query(sql); // 直接使用ResultSet獲取數據結果,不再將結果存入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("當前占用內存: " + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
- server.commit();
- long endTime = System.currentTimeMillis();
- log.info("提交索引時間:"+((endTime - start)));
- docs.clear();
- docs = null;
- log.info("提交索引結束");
- } catch (SolrServerException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
代碼進行上述修改后,運行了幾個小時,不再報堆內存溢出的錯誤了。
現在假設業務需求修改了,要求在查詢5000條數據時,對每條數據進行處理:需要根據id去其他表中查詢修改的標題并寫入索引中。
我在上述代碼中直接進行了修改,在while(rs!=null&&rs.next())循環中加入了查詢另外一張表的代碼。運行程序發現當前占用的內存越來越多。于是我在服務器上使用了jstat查詢當前虛擬機內存占用情況,命令如下:
- jstat -gcutil pid 10000
10秒輸出一次內存占用及垃圾回收情況,發現Young GC和Full GC非常頻繁,并且Full GC之后,老年代內存回收情況并不好,監控如下:
這里可以看到第四列老年到剛開始只占用了28.64%,運行一段時間后內存占用量到81.22%,進行Full GC之后,仍然占用52.87%。
檢查代碼,發現是在while(rs!=null&&rs.next())里查詢另外一張表的代碼出現的問題。開發匆忙,我從網上隨便找了一個數據庫工具類進行的開發,發現里面的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;
- }
這段程序并沒有及時釋放ps,因為查詢頻繁,ps引用的對象一直得不到回收,導致這些對象進入了老年代,并且虛擬機檢查這些對象仍然與GC Root有關聯,因此導致老年代垃圾回收效果不好。也是這個原因導致的Young GC和Full GC非常頻繁。
大致找到了問題原因,修改代碼如下:
- public void processData() throws Exception{
- int startTime = Integer.parseInt(FileUtils.readFiles(RECORD_INTIME)); // ***startTime=0,從文件中讀取記錄時間
- String sql = "select id,title,content,url,intime from testTable where intime>startTime limit 5000;
- ResultSet rs = null;
- try{
- rs = jdbc.query(sql); // 直接使用ResultSet獲取數據結果,不再將結果存入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); // 當然solr數據集的配置文件也需要修改,這里不再贅述
- 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();
- }
- }
- }
經過上面的修改,再次運行程序,不再發生內存溢出了,用jstat監控如下:
可以看到Young GC和Full GC正常了。Full GC在開始階段基本沒有被觸發,Young GC也少了很多。而第四列的老年代回收情況也變的正常了。
上面的例子很簡單,導致堆內存溢出的問題也比較常見。我想說的是看完一本書可能能被記住的內容并不多,但隨著經驗的積累和實踐的增多,你會慢慢有一種感覺,能夠大致定位到問題在哪里,這樣就夠了。
參考:《深入理解Java虛擬機:JVM高級特性與***實踐(第2版)》
【本文為51CTO專欄作者“王森豐”的原創稿件,轉載請注明出處】