레이블이 검색엔진 ( 루씬 lucene )인 게시물을 표시합니다. 모든 게시물 표시
레이블이 검색엔진 ( 루씬 lucene )인 게시물을 표시합니다. 모든 게시물 표시

2009년 12월 23일 수요일

스케쥴러에 의한 대용량 처리

 열심히 적었던 글이 소스코드를 옮겨 넣는 와중에 고향 친구로부터 전화가 모르고 저장. 그대로 날라가 버렸다. 슬프다. ㅠㅠ

같은 말을 반복하는걸 매우 싫어하는 성격이므로 기억을 더듬어 대략 다음의 내용들을 적어본다.

 

1. 대용량 처리를 위해서는 작업 정보를 관리하고 정확 모니터링 할 수 있는 스케쥴러가 필요하다.

2. 스케쥴러 또한 하나 이상의 쓰레드로 구성되므로, 쓰레드의 특성에 대해 정확하게 알고 있어야 한다.

3. 쓰레드 사용시 야기되는 문제들 가운데 Race Condition에 따른 해결방법, 뮤텍스, 이벤트 세마포어, 크리티컬 섹션, 등을 이해하고 있어야 한다.

4. 자바의 최신 SDK에는 기존에 사용되던 suspand(), resume() 과 같은 메서드가 디프레취 되었고, 쓰레드를 중단하거나 재시작하는 것과 같은 생명주기를 개발자가 임의로 컨트롤하기 보다는 이전 생명주기를 완전히 종료시키고 다시 해당 쓰레드의 생명주기를 시작하는 방법을 더 권장하는것 같다.

5. 스케쥴러는 작업 도중 예기치 않은 오류를 복구하는 기능과, 실시간으로 서버 시스템의 자원(메모리 등)을 모니터링 할 수 있는 기능을 가져야 한다.

6. log4j와 같은 로그 툴을 통해 로그를 잘 남겨두어야 한다.

7. 작업 정보를 저장하는 데이터 구조인, Queue, Stack, List 등의 특성과 사용법을 잘 이해하고 있어야 한다.

8. 스케쥴러는 시스템에 과도한 부하를 주는것을 방지하고 시스템 성능등과 같은 이슈를 피하고 단위 작업에 대한 무결성을 위해 자신에게 할당된 작업을 분할 처리 할 수 있는 기능을 가져야 한다.

 

[code java] /**
 * <PRE>
 * Class/Interface Name : WebSchedulerINI
 * History
 *     1. 이지홍(hongsgo@gmail.com), 2009. 1. 8., 최초작성
 * </PRE>
 * @brief 색인 서버의 전체 작업을 관리하는 핵심 클래스.
 * @date 2009. 1. 8.
 * @version 1.0.0
 * @author 이지홍(hongsgo@gmail.com)
 * @warning
 */
public class WebSchedulerINI extends BaseService implements Runnable {
 
 //스케쥴러 쓰레드
 private Thread webjobs = null;
 //스케쥴러의 모니터링을  담당하는 클래스
 private MonitorService monitorService = null;
 //실패한 작업을 복구하는 클래스
 private JobRecoveryManager jobRecoveryManager = null;
 //스케쥴러의 작업을 비우는 플래그 클래스(최우선)
 private boolean job_clear=false;
 //서버 시작 간을 표시하기 위한 부분 .
 public Date run_time = null;  /**
  * <PRE>
  * Method Name : setSchedulerJobClearFlag
  * History
  *  1. 이지홍(hongsgo@gmail.com), 2009. 2. 18., 최초작성
  * </PRE>
  * @brief 다음 작업에 스케쥴러를 비우는 플래그
  * @date 2009. 2. 18.
  * @version 1.0.0 void
  */
 public void setSchedulerJobClearFlag(){
  this.job_clear=true;
 }  /**
  * <PRE>
  * Method Name : setSchedulerJobClear
  * History
  *  1. 이지홍(hongsgo@gmail.com), 2009. 2. 18., 최초작성
  * </PRE>
  * @brief  플래그에 따라 큐를 비우는 메소드
  * @date 2009. 2. 18.
  * @version 1.0.0 void
  */
 private void setSchedulerJobClear(){
  this.monitorService.getSchedulerMonitorInfo().setSplit_job_count(0L);
  this.que_webSchedulerJob.clear();
  this.job_clear=false;
 }  /**
  * 임시 작업결과 정보
  */
 private DBIndexJobInfo temp_dbjob = null;
 private OperateJobInfo temp_operator_job = null;
 private MemoryManager memoryManager = null;
 private DBIndexService dbIndexService = null;
 //색인을 처리하는 객체의 부모클래스
 private BaseIndexService baseIndexService = null;
 private String[] temp_arr_heapinfo = null;
 private boolean index_optimize = false;
 private int now_SCHEDULER_INDEX_OPTIMIZE_COUNT = 0;
 
 //스케쥴러 작업의 최상위 부모 클래스
 private BaseWebJobInfo temp_basejob= null;
 private Queue<BaseWebJobInfo> que_webSchedulerJob = null;
//
 private ArrayList<String[]> arr_indexdata= null;  /* (non-Javadoc)
  * @see com.jce.searchengine.common.service.BaseService#init()
  */
 public void init(){
  this.webjobs= new Thread(this);
  this.que_webSchedulerJob=new PriorityBlockingQueue<BaseWebJobInfo>(100,new JobtypeCompare());
  this.run_time=new Date();
  webjobs.start();
 }
 public int getQue_Count(){
  return this.que_webSchedulerJob.size();
 }     /**
     * <PRE>
     * Class/Interface Name : JobtypeCompare
     * History
     *     1. 이지홍(hongsgo@gmail.com), 2009. 2. 6., 최초작성
     * </PRE>
     * @brief 작업의 우선순위를 기준으로 비교하는 Sorter
     * @date 2009. 2. 6.
     * @version 1.0.0
     * @author 이지홍(hongsgo@gmail.com)
     * @warning
     */
    class JobtypeCompare implements Comparator<BaseWebJobInfo> {      public int compare(BaseWebJobInfo o1, BaseWebJobInfo o2) {
     if(o1 instanceof BaseWebJobInfo && o2 instanceof BaseWebJobInfo){
      BaseWebJobInfo s1 = (BaseWebJobInfo)o1;
      BaseWebJobInfo s2 = (BaseWebJobInfo)o2;
              return (s1.getPriority() < s2.getPriority())? 1 : (s1.getPriority() == s2.getPriority() ? 0 : -1);
        }
        return -1;
  }
 }         /** (non-Javadoc)
  * @see java.lang.Runnable#run()
  * 주기적으로 rss,atom,메인 플래쉬 상단 xml을 퍼블리싱 하는 메서드 (1분간격)
  */
 public void run(){
  while(true)
  {
    //꺼내기전에 현재 큐를 비우는 플래그나 특정 사이트의 작업을 지우라는 플래그가 있는지 확인한다.
    //새 작업을 꺼낸다.
    if(job_clear){
     this.setSchedulerJobClear();
    }
    else
    {      this.temp_basejob=this.getNextJob();
     if(this.temp_basejob!=null)
     {
      switch(this.temp_basejob.getWebSchedulerJobType()){
       case DBIndex: //디비인덱스 작업일 경우 수행
        try
        {
        
          //보다 명확한 작업객체의 유효성 검사.
          if(this.temp_basejob instanceof DBIndexJobInfo){
           temp_dbjob = (DBIndexJobInfo)this.temp_basejob;
           if(temp_dbjob != null)
           {
            //작업 처리 여부 플래그를 활성화 시켜두고
            //디비 인덱스 작업의 타입에 따라 디비기반 인덱싱 작업 수행
            //해당 색인경로에 락이 남아있다면 락을 제거하여 준다.
            //데이터베이스로 부터 데이터를 가져오기 전에 log4j를 통해 info레벨의 log를 남긴다.
            //메모리 매니저를 통해 현재 가용중인 메모리의 정보를 갱신한다.            }
           else{
            throw new NullPointerException(super.glbConstant.getSCHEDULER_ERROR_6());
           }
          }
          //instanceof가 null인지 아닌지 확인하는 로직.
          else{
            //작업정보에는 데이터베이스 작업이라고 했지만, 실제 형변환을 해보니 아니더라. 그래서 예외 던짐.
            throw new ClassCastException(super.glbConstant.getSCHEDULER_ERROR_1());
          }
        } catch (Exception e) {
         this.monitorService.getSchedulerMonitorInfo().setIncFailJobCount(); //실패작업 카운팅
         if(this.temp_dbjob.getJobtype().equals(DBIndexJobType.DBIndexMakeAll)){
          //현재 처리중인 작업이 일괄색인이었다면 하위 분할된 작업값들을 초기화
          this.monitorService.getSchedulerMonitorInfo().setSplit_job_count(0);
         }
         if(e instanceof SQLException){
          if(!(this.temp_dbjob.getJobtype().equals(DBIndexJobType.DBIndexMakeAll))){
           this.jobRecoveryManager.setReadyToFailedJobSave(temp_dbjob);
          }
          logger.error(super.glbConstant.getSCHEDULER_ERROR_5()+ e.getMessage());
         }
         else if(e instanceof IOException){
          if(e instanceof CorruptIndexException){
           logger.error(super.glbConstant.getSCHEDULER_ERROR_3()+ e.getMessage());
          }
          else if(e instanceof LockObtainFailedException){
           try {
            this.dbIndexService.setDelLock(temp_dbjob.getService_site_info().getArr_list_index_location().get(0));
           } catch (IOException e1) {
            logger.error(e1.getMessage());
           }
           logger.error(super.glbConstant.getSCHEDULER_ERROR_1()+ e.getMessage());
          }
          else{
           logger.error(super.glbConstant.getSCHEDULER_ERROR_2()+ e.getMessage());
          }           if(!(temp_dbjob.getJobtype().equals(DBIndexJobType.DBIndexMakeAll))){
           this.jobRecoveryManager.setReadyToFailedJobSave(temp_dbjob);
          }
         }
         else if(e instanceof NullPointerException){
          logger.error(super.glbConstant.getSCHEDULER_ERROR_4()+e.getMessage());
         }
         else if(e instanceof ClassCastException){
          logger.error(super.glbConstant.getSCHEDULER_ERROR_1()+ e.getMessage());
         }
         else{
          logger.error(super.glbConstant.getSCHEDULER_ERROR_5()+ e.getMessage());
         }
        } finally{
         //플래그를 바꾼다.
         temp_dbjob.getService_site_info().setJobflag(false);
         this.setIndex_optimize(false);
         this.monitorService.getSchedulerMonitorInfo().setIncMonitorCount(temp_dbjob.getJobtype());
        }
        break;
       case Operate:
        //운영 툴 작업과 관련된게 있으면 처리
         try {
           if(this.temp_basejob instanceof OperateJobInfo){
            this.temp_operator_job = (OperateJobInfo)this.temp_basejob;
           }
           if(this.temp_operator_job != null)
           {
            switch(this.temp_operator_job.getJobtype()){
             case Clear:
               //로케이션이 하나라고 생각하고 처리.
               this.baseIndexService.setClearIndex(super.getIndexInfoLoaderService().getIndexInfoXmlSaxHandler().getHash_table_content_service_site().get(this.temp_operator_job.getSitecode()).getArr_list_index_location().get(0));
             break;
             case Optimize:
               this.baseIndexService.setOptimizeIndex(super.getIndexInfoLoaderService().getIndexInfoXmlSaxHandler().getHash_table_content_service_site().get(this.temp_operator_job.getSitecode()).getArr_list_index_location().get(0));
             break;
            }
           }
           else{
            //작업정보에는 데이터베이스 작업이라고 했지만, 실제 형변환을 해보니 아니더라. 그래서 예외 던짐.
            throw new ClassCastException(super.glbConstant.getSCHEDULER_ERROR_1());
           }
         } catch (Exception e) {
          logger.error(e.getMessage());
         } finally{
          this.monitorService.getSchedulerMonitorInfo().setIncMonitorCount(this.temp_operator_job.getJobtype());
         }
        break;
       case FieldInex:
        //쿼리 인덱스 혹은 필드 인덱스 작업이 있으면 수행.
        break;
       default:
        break;
      }//switch 문 끝       //현재 끄집어 낸 작업을 지우고
      this.que_webSchedulerJob.remove();
      //작업 처리 횟수를 올린다.
      this.monitorService.getSchedulerMonitorInfo().setIncTotJobCount();
      if(this.arr_indexdata!=null){
       this.arr_indexdata.clear();
      }
      this.arr_indexdata=null;
      this.temp_arr_heapinfo = null;
      this.temp_dbjob = null;
      this.temp_basejob=null;
     }
     else{// 작업이 없다면 다음 실행.
      try {
       //idle 횟수를 기억해 두었다가 최적화를 수행함
       if(now_SCHEDULER_INDEX_OPTIMIZE_COUNT>Integer.parseInt(super.getGlbConstant().getSCHEDULER_INDEX_OPTIMIZE_COUNT()) && this.isIndex_optimize()==false){
        this.now_SCHEDULER_INDEX_OPTIMIZE_COUNT=0;
        this.index_optimize=true;
       }
       else{
        now_SCHEDULER_INDEX_OPTIMIZE_COUNT++;
       }
      } catch (Exception e) {
       logger.error(this.glbConstant.getSCHEDULER_ERROR_8()+e.getMessage());
      } finally{       }      }
     this.temp_basejob = null;
     //if-else 문 끝
     try {
      if(this.que_webSchedulerJob.isEmpty()){ //작업 큐가 비었다면 idle
       Thread.sleep(this.glbConstant.getSCHEDULER_RELOAD_TIME());
      }
     } catch (InterruptedException e) {
      e.printStackTrace();
     }
    }   } //while문 끝
 }  public BaseWebJobInfo getNextJob(){   return this.que_webSchedulerJob.peek();
 }   [/code]

2009년 12월 10일 목요일

색인과 검색 그리고 하이라이팅

무엇을 검색 해야 하는가에 대한 고민은

무엇을 색인해야 하는가로 이어진다.

색인 할 그 무언가가 정해진다면,

다시 어떻게 그 검색 결과를 하이라이팅 해서 보여 줄 것인가로 귀결된다.

참 아이러니 한 이야기다.

 

게시판의 글들을 색인하는 것을 예로 들어보자.

우리는 수 많은 게시글들을 검색 대상으로 하였기에,

다양한 성향의 유저들이 쓴 글을 색인엔진에 밀어 넣어야만 했다.

 

서로 다른 옵션과 코드들을 가진 에디터가 뱉어내는

게시글들로부터 양질의 색인 데이터를 뽑아내는것이 목표였다.

또한 검색된 결과에 게시글들이 가지고 있던 혹은 악의적으로 입력된 내용이

결과 페이지의 레이아웃을 망치거나 사이트의 보안 요소를 헤쳐서는 안되었다.

에디터가 escapeHTML을 수행하는지 아닌지, 에디터에 html 태그를 허용하거나 embed,object를 허용 했는지의 여부에 따라 색인시 준비해야 할 전처리기의 명세는 달라지게 되지만,

너무 많은 전처리 작업을 수행하면 유지보수 비용이 높게 발생하거나,

한정된 시스템에 부담을 주거나 또는 신규 작성된 컨텐츠가 색인에 반영되기까지의

시간 차가 커지는 원인이 되었다.

마지막으로 양질의 검색 결과에 HTML태그와 CSS를 이용한 하이라이팅 기능이

가능해야 했으므로, HTML 태그를 무조건 제거 할 수는 없는 상황이었다.

 

무작정 달려들었다간 수만건의 데이터를 쓸모없게 만들거나,

테스트 시간만도 상당한 작업을 계속 반복하는 시행착오를 겪을 판이었다.

 

차 한잔 마실 시간이 흘렀을까?

APACHE COMMONS LANG 패키지에 포함된 StringEscapeUtils 객체가 퍼뜩 떠올랐다.

내가 색인해야 할 원본 데이터가 결과를 출력 할 HTML 페이지의 내용을 가짐으로 해서

문제가 발생한다면, 헌데, 그 내용이 HTML ESCAPE 유무의 차가 있다면,

전처리기에서 색인 할 데이터를 HTML ESCAPE 처리하여 StringUtils에

HTML REMOVE REGEXPRESSION을 사용하여 관계되는 태그를 모두 다(거의 다) 제거하면 어떨까 하는 생각이었다.

그 다음은, 그렇게 걸러진 게시글들안에 어쩔 수 없이 남아있는 새로운 태그들에 대한 처리( <!-- --> 나 < >, OBJECT, Embed 와 같은 것들 이 문제가 되었지만)를 하고 색인 파일을 만들어낸다.

그 뒤,  검색서버가 색인파일로부터 검색한 결과를 추출해내면, 하이라이팅 작업을 더해 결과를 만들어 내도록 구현하면 되었다.

[code java] //데이터베이스로부터 가져온 게시물의 정보를 담은 ArrayList<String[]> 객체 arr_indexdata           int counter=0;
     for(String[] arr_string: arr_indexdata){
      doc[counter]=new Document();
       //필드 정보를 바탕으로 문서를 작성한다.
      for(int j = 0 ; j < arr_fieldinfo.size();j++){
       //html escape로 일괄 변환후 html 태그를 제거한 내용을 저장
       doc[counter].add(new Field(arr_fieldinfo.get(j).getName()
          ,StringEscapeUtils.escapeHtml(arr_string[j]).replaceAll("(?:<!--.*?(?:--.*?--\\s*)*.*?-->)|(?:<(?:[^>'\"]*|\".*?\"|'.*?')+>)","")
          ,arr_fieldinfo.get(j).getStoretype()
          ,arr_fieldinfo.get(j).getIndextype()));
      }
      counter++;
     }
     arr_indexdata.clear();
     arr_indexdata = null; [/code]

 

 

검색엔진 개발정리를 시작하다. lucene 2.9

Apache Lucene - Overview

 

오픈소스 검색엔진 루씬 2.9 버젼으로 개발을 진행하고 있다.

곧 오픈 할 예정,

이미 루씬을 사용해서 7개월 이상 개발해 왔으나,

이제서야 그것에 대한 이야기들을 이 공간에 남겨보고자 한다.
당장은 루씬을 활용법이나 루씬을 실 업무에 적용했을때 발생하는 문제들이

주류를 이룰 것이고(내가 관심이 있는게 그쪽이니), 그 나머지는

루씬의 핵심 알고리즘들을 파헤치고 싶다.(욕심)

 

여력이 남는다면 내가 만든 결과물을 정리해서 별도로 검색엔진 운용도 해보고싶다.