티스토리 뷰

Notes

DataImport 처리에 대해서 처음 접하는 경우는 아래와 같은 정보를 사전에 검토해야 한다. DataImport 처리를 처음 구성하는 경우는 이미 많은 정보들이 존재하므로 찾아서 검토를 하고, DataImport 와 관련된 다음의 정보를 검토해야 한다.

그리고 실제 작업을 진행하면서 만났던 오류를 기준으로 정리한 것으로 다른 원인과 다른 오류가 더 많을 수 있으므로 Solr관련 정보를 확인해야 한다.

DataImport on SolrCloud

단일 서버에 구성했던 Solr 로 DataImport 를 처리하는 것과 동일하게 처리하면 된다. 아래는 기존 샘플에서 사용했던 Collection 을 대상으로 DataImport (Full-Import) 를 처리하는 명령이다.

http://localhost:7070/solr/test-collection/dataimport?command=full-import&clean=true&commit=true

단, 차이점이라면 Solr Admin UI 에서 처리하는 “DataImport” 는 Collection name 을 이용하는 것이 아니라 실제 Core 를 사용한다는 점이다. 예를 들면 다음과 같은 명령이 호출된 것과 같다.

http://localhost:7070/solr/test-collection_shard1_replica1/dataimport?command=full-import&clean=true&commit=true

명령을 처리하는 방식 (Request or Admin UI) 의 차이를 제외하면 기존 방식과 동일하게 처리하면 된다.

Problems

dataimport.properties 경로 문제

문제 상황

제일 위에서 언급했던 DataImport 에서 last_index_time 의 의미와 사용법 의 내용을 보면 SolrCloud 환경에서는 propertyWriter 를 “SimplePropertiesWriter” 가 아닌 “ZKPropertiesWriter” 를 사용해야 한다는 것을 언급한 적이 있다. 그 때는 별다른 설명이 없었지만 아래와 같이 문제가 발생하기 때문에 언급을 한 것이다.

SolrCloud 에서 db-data-config.xml 에서 ZKPropertiesWriter 사용방법

위의 설정을 해 주면 ZooKeeper 상의 Configuration 경로로 dataimport.properties 파일이 생성되고 갱신 된다.

DataImport 에서 Delta-Import 는 마지막으로 수행된 정상적인 Import 작업의 시작 시간을 다음 Import 에서 시작할 시간을 설정한다고 설명을 했다. 그런데 단일 서버에서 사용하던 db-data-config.xml 을 그대로 사용하면 어떤 일이 발생할까?

결과를 확인해 보면 아래의 그림과 같이 재미있는 증상을 발견할 수 있다. (아래의 그림은 ZooKeeper 서버 1 에서 zkCli 를 이용해서 확인한 것이다)

SolrCloud 에서 dataimport.properties 파일 위치 문제

위의 예제는 실제 적용에서 DataImport 를 수행한 후에 생성된 dataimport.properties 파일이 생성된 경로를 나타내는 것이다. 문제는 Collection 이 “ewsconf” 라는 공통 Configuration Set 를 사용하며, Collection 명을 “EWS”라고 했을 때의 상황으로 SolrCloud 가 시작될 때 “ewsconf” 를 참조한다는 것이다. 그런데 Delta-Import 에서 중요하게 사용될 dataimport.properties 파일은 공통 Configuration Sets 에 존재하는 것이 아니라 “EWS” 라는 Collection 명을 그대로 사용해서 별도로 저장된다는 것이다.

물론 이렇게 사용해도 기능에 문제가 없는 것은 아니지만 (서버 재 구동 후에 Delta-Import를 실행하면 제대로 동작한다) 아무래도 ZooKeeper 관리 환경에서는 별도로 분리된 정보를 관리하는데 문제가 있을 수 있다.

해결 방안

이 방법이 100% 신뢰할 수 있는 것이라고 장담을 할 수는 없지만, 두 가지 해결 방법이 존재한다.

  • Configuration Set 의 이름을 Collection 이름과 동일하게 유지한다.
  • ZKPropertiesWriter를 상속하여 findDirectory 메서드를 confname 을 사용하는 것으로 변경하고 db-data-config.xml 에 “propertyWriter” 로 클래스를 설정하도록 한다.

현재는 운영에 별다른 문제가 없고, 또한 다른 쉬운(?) 방법이 있을 듯 하여 별도로 테스트를 해 보지는 않았다.

Notes

ZKPropertiesWriter 를 사용하는 경우는 기존 SimplePropertiesWriter를 사용했을 때와 Attributes 설정에 차이가 존재한다.

  • type attribute - 공통 필수 지정
  • filename - SimplePropertiesWriter 만 유효
  • directory - SimplePropertiesWriter 만 유효
  • dateFormat - 공통 옵션 지정으로 java.text.SimpleDateFormat 패턴으로 지정하면 된다. (지정하지 않으면 기본 값으로 “yyyy-MM-dd HH:mm:ss”)
  • locale - DataImportHandler Wiki 에는 공통 옵션으로 설명이 되어 있지만 실제 4.10.2 버전에서 ZKPropertiesWriter 를 사용할 때 locale을 지정하면 허용되지 않는다고 오류가 발생한다.

DataImport 후에 Shard 간에 Num Docs 의 수가 다른 경우

이 상황은 문제가 아니라 Solr 의 Indexing 이 Shard 간에 나뉘어 보관되는 것 때문에 불 일치 상태가 되는 것이므로 아주 정상적인 상태로 보면 된다.

상황 파악

SolrCloud 환경에서는 하나 또는 그 이상의 Shards 로 구성이 된다. 따라서 Indexing 이 처리될 때 Collection 생성 시에 지정된 numShards 의 수에 따라서 기본적인 Hash Code 기준을 Partition 작업이 처리된다.

아래의 그림은 Index 가 구성된 후에 Solr Admin UI 의 “Cluster > Tree” 메뉴의 “clusterstate.json” 의 내용을 확인한 것이다.

Clusterstate.json 내용

위의 그림을 확인하면 shard1, shard2, shard3 에 대한 Range 구분이 지정되어 있는 것을 확인할 수 있다. 그리고 가장 마지막에 설정된 “router” 정보가 “compositeId” 로 설정된 것을 알 수 있다. 따라서 질의 요청이 오면 처음 수신한 Query Controller 가 모든 Shards 로 질의를 전송하고 각 Shard 는 자신이 관리하고 있는 문서를 결과로 반환하는 처리가 진행된다.

따라서 각 Shard 를 구성하는 Core의 Overview 상에 보여지는 NumDocs 는 Hasing 기준으로 Partition 작업된 인덱싱 문서의 수를 나타내는 것이므로 Shard 당으로 다를 수 있다. 그러나 Shard 에 Replica 들이 존재하므로 각 Replica 들 간의 numDocs는 반드시 같아야 한다. (Ex. Shard1_replica1 == Shard1_replica2 == Shard1.replica3)
만일 다른 상황이 발생한 경우라면 Replication 작업이 제대로 동작하지 않은 문제 발생 상황이므로 각 Solr Node 들을 재 시작해서 Recovery와 Replication Sync 작업이 수행될 수 있도록 해야 한다.

참고 사항

위와 같이 기본적인 처리 말고, 사용자가 데이터를 특정한 분류 기준에 맞춰서 Shard의 Hash를 조정하는 방법이 존재한다. 여기서 상세한 설명을 할 수는 없지만 간단한 예를 들어 사용 방법을 확인해 보도록 하자.

데이터가 스포츠에 대한 것으로 “축구”, “야구”, “농구” 3 가지 종목으로 분류가 가능한 상태라면 (여기서는 Shard가 3개 이므로 이렇게 한정한다) DataImport 를 이용해서 데이터를 추가할 때 문서 ID 값을 변경해서 분류를 지정할 수 있다. 분류를 위한 지정은 다음과 같다.

[ 문서 데이터 ]
sport: '축구',
docId: 1234,
...

[ 변경된 문서 데이터 ]
id: '축구!1234',
sport: '축구',
docId: 1234,
...

위의 예제와 같이 id 를 문서의 정보를 기준으로 “!” 문자로 연결해 주면 Indexing 이 처리될 때 앞의 “축구” 를 Shard Key 로 인식하여 같은 키를 가진 데이터끼리 동일한 Shard에 존재할 수 있도록 처리해 준다.

반드시 “!” 문자를 사용해야 하므로 만일 데이터 중에 “!” 문자가 있다면 다른 문자로 치환해 주어야 한다. 최대 2 개까지 “!”문자를 쓸수 있어서 Sub Category와 같은 구조를 만들 수도 있다.* (ex. “축구!국가대표!1234”, “축구!올림픽대표!4444”, …)

DataImport 후에 Solr Node 간에 numFound (q=*:*) 가 다른 경우

문제 원인

예를 들어 DataImport 에서 처리된 문서 수는 23,499 건인데 Node1 의 numFound 는 23,244 건이고, Node2 의 numFound 는 20,299 건이고, Node3 는 23,499 인 경우로 대 부분은 DataImport 처리 중에 실제 오류가 발생한 것이다. Solr 로그를 확인해 보면 다음과 같은 오류 들을 발견할 수 있다.

  • peerSync Error - 상세 로그를 보면 “Exception writing document id xxx to the index; possible analysis error. 가 대부분으로 Replication 처리를 하면서 추가된 문서를 처리할 때 Analysis 관련 문제가 발생했을 수 있다는 것으로 발생 메시지가 NullPointException 으로 명확한 원인을 알 수 없이 실패한 것이다. 그 외는 명확하게 오류 이유가 출력 된다.
  • DistributedUpdator Error or Warning - Replication update 요청으로 다른 서버로 update 요청을 하였지만 대상 서버에서 Internal 오류가 발생한 경우에 나타난다. 주로 해당 서버의 오류에 의해서 결과로 출력 되는 경우이므로 원인이 되는 오류를 잡아야 한다. (ex. 대상 서버에서 peerSync 오류가 발생했을 때 요청을 한 서버에 DistributedUpdator Error 가 출력되므로 peerSync 의 오류를 해결하면 DistributedUpdator 오류는 자연히 해결된다)

해결 방법

DataImport 의 결과로 출력된 처리 문서수를 완전하게 가지는 하나의 Shard 라도 존재한다면 각 서버들을 모두 Restart 하여 Replication 처리와 Sync 를 맞춰주면 된다. 주로 위에서 설명한 것과 같이 Update 처리 중에 오류가 발생한 것들이기 때문에 실패한 데이터는 Transaction Log에 존재하므로 재 시작을 하면 재 처리가 되어 복구될 수 있다.

Notes

가장 큰 문제는 모든 Shard 들 중에서 어떤 것도 완전한 데이터를 가지지 못했을 경우로 이런 상황은 Recovery 와 Replciation Sync 를 처리해도 데이터가 부족한 상황이므로 오류의 원인을 파악하여 다시 DataImport 작업을 수행해야 한다.

DataImport Scheduler 처리 문제

기존에 Solr 에 Quartz Scheduler 를 기반으로 DataImport 처리를 수행하는 방법에 대해서 정리를 한 적이 있다. 이 때 사용한 방법은 Solr Server 의 web.xml 에 ApplicationListener 를 연결해서 Server 가 시작될 때 Quartz Scheduler 를 구동 시키고 지정한 시점이 되면 DataImport (Full / Delta)를 호출하는 것이었다.

문제 확인

위와 같이 처리를 하는 것은 Solr 단일 서버에서는 전혀 문제가 없지만 SolrCloud 환경에서는 Solr Node 가 여러 개가 존재하기 때문에 각 Node Server 마다 이 설정을 하게 되면 동일한 주기에 여러 개의 Solr Node 에서 DataImport 작업이 수행되는 문제가 발생한다.

즉, localhost:7070, localhost:8080, localhost:9090 의 3 개 Solr Node 에 동일한 Quartz Schedule 이 동작하기 때문에 같은 시간에 3개의 Import 작업이 진행된다는 것이다. 물론 시차가 있어서 먼저 Import 작업이 시작되면 다음 작업은 “이미 Import가 수행 중이다” 라는 오류 메시지를 받고 종료하게 된다. 그렇지만 시차가 적다면 동시에 진행되는 상황이 발생한다.

해결 방법

해결 방법은 의외로 단순하다. 위에서 3개의 Solr Node 가 존재하고 서비스를 하지만 결국 Leader는 하나이고 나머지는 Replica 로서 동작을 하기 때문이다. 따라서 모두 동일한 Quartz Scheduler 를 기준으로 동작하지만, Leader가 아닌 것은 무시하면 된다. 이렇게 처리하는 것이 좋은 이유는 Solr Node 가 장애 발생으로 인해서 Leader가 변경될 때도 적용할 수 있기 때문이다.

자 이제 하나씩 처리 방법을 확인해 보도록 하자. 전체적인 구성과 소스 코드는 위에서 언급한 Quartz Scheduler 를 기반으로 DataImport 처리를 수행하는 방법 의 링크들 중에서 “원본 참조 경로” 링크에서 얻을 수 있다.

Quartz Configuration 에 ZooKeeper 연결을 위한 Server 설정 추가

SolrCloud 에서 Leader를 찾기 위해서는 SolrCloud 에 접속을 하여야 한다. 이 때 필요한 것이 ZooKeeper Server 들의 정보이므로 Quartz_schedule.xml 파일에 아래와 같이 zkServers 라는 이름으로 파라미터를 추가하도록 한다.
SolrCloud Quartz Scheduler 에 ZooKeeper 서버 정보 추가

아래에서 설명할 DataImportJob 에서는 이 정보를 이용해서 SolrCloud 에 접속을 하고 그 정보를 통해서 Leader Server 의 Base Url 정보를 추출할 것이다.

DataImportJob 클래스의 execute 에서 Leader 찾기

이제 Quartz Schedule 에 따라서 동작하는 DataImportJob 에서 SolrCloud 에 접속하여 Leader의 Base Url을 확인하고 Leader가 아닌 경우는 제외 처리하면 된다.


public class DataImportJob implements Job {
    private static final Logger LOG = LoggerFactory.getLogger(DataImportJob.class);

    private String              solrUrl;
    private String              cores;
    private String              zkServers;

    // Quartz 에 의한 호출
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // Get values from JOB data map.
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        JobDataMap triggerDataMap = context.getTrigger().getJobDataMap();

        // Leader 확인 후 처리
        if (this.checkLeader(jobDataMap)) {
            String command = this.getString(triggerDataMap, "command", "full-import");
            String entity = this.getString(triggerDataMap, "entity", "");
            String clean = this.getString(triggerDataMap, "clean", "true");
            String commit = this.getString(triggerDataMap, "commit", "true");
            String optimize = this.getString(triggerDataMap, "optimize", "false");

            if (cores == null) {
                // Use default core
                this.post(solrUrl, command, null, entity, clean, commit, optimize);
            } else {
                for (String core : cores.split(", *")) {
                    this.post(solrUrl, command, core, entity, clean, commit, optimize);
                }
            }
        } else {
            LOG.info(MessageFormat.format("Scheduler executed by - url : {0}, but this url is not leader. so dataimport canceled", this.solrUrl));
        }
    }

    // Leader 여부 검사
    private boolean checkLeader(JobDataMap jobDataMap) {
        this.solrUrl = this.getString(jobDataMap, "solrUrl", "http://localhost:8080/solr");
        this.cores = jobDataMap.getString("cores");
        this.zkServers = jobDataMap.getString("zkServers");
        if (cores == null || cores.equals("") || zkServers == null || zkServers.equals("")) {
            // 단일 Solr Server로 판단
            return true;
        } else {
            // SolrCloud Server 연결
            CloudSolrServer server = new CloudSolrServer(this.zkServers);
            for (String core : cores.split(", *")) {
                server.setDefaultCollection(core);
                server.connect();
                // Leader 확인
                if (this.isLeader(server, core, this.solrUrl)) {
                    return true;
                }
            }
        }
        return false;
    }

    // Leader 검사 및 Url 재 설정
    private boolean isLeader(CloudSolrServer server, String collectionName, String currentUrl) {
        ZkStateReader reader = server.getZkStateReader();
        // ZooKeeper Cluster상 Active Slice
        for (Slice slice : reader.getClusterState().getSlices(collectionName)) {
            // Leader를 찾고 Base Url 속성 추출
            String baseUrl = slice.getLeader().get("base_url").toString();
            if (baseUrl.equals(currentUrl)) {
                this.solrUrl = baseUrl;
                return true;s
            }
        }
        return false;
    }

위와 같이 처리를 하면 항상 Leader 의 Base Url 과 Quartz Schedule 에 설정된 Solr Server Url 이 같은 경우만 실행이 되기 때문에 Leader Node 에서만 DataImport 작업이 처리 된다.

테스트는 localhot:7070, localhost:8080, localhost:9090 으로 실행을 하고 DataImport 가 Leader에 대해서 동작하는지 로그를 확인한 후에 현재 Leader인 Server 를 Shutdown 하면 다음 Schedule에 따라서 DataImport 가 수행되면 ZooKeeper에 의해서 변경된 Leader Server 에서만 DataImport 가 수행하는 것을 확인할 수 있다.

즉, localhost:7070 Server 가 Leader인 상태에서 localhost:7070 Server를 Shutdown 하면 ZooKeeper 는 localhost:8080 또는 localhsot:9090 중에 하나를 Leader로 다시 선출하게 된다. 이후에 Schedule 이 동작하면 Leader로 선출된 Server (예를 들면 localhost:8080) 에서 DataImport 가 처리되고 나머지 Server는 Leader가 아니라는 로그를 출력하고 종료하게 된다.

Written by Morris (MSFL)

댓글
댓글쓰기 폼