2.X/6. Modeling Your Data

6-1-5. Solving Concurrency Issues

drscg 2017. 9. 23. 15:15

The problem comes when we want to allow more than one person to rename files or directories at the same timeImagine that you rename the /clinton directory, which contains hundreds of thousands of files. Meanwhile, another user renames the single file /clinton/projects/elasticsearch/README.txt. That user’s change, although it started after yours, will probably finish more quickly.

한 명 이상이, 동시에, 파일이나 디렉토리의 이름을 변경하려는 경우, 문제가 발생한다. 어떤 사용자가 수십만 개의 파일을 가지고 있는, /clinton 디렉토리의 이름을 변경하고, 또 다른 사용자는 /clinton/projects/elasticsearch/README.txt 라는 단일 파일의 이름을 변경한다고 가정해보자. 파일 이름 변경은 디렉토리 이름 변경 후에 시작하지만, 아마 더 빨리 완료될 것이다.

One of two things will happen:

다음 두 가지 중 하나가 발생할 것이다.

  • You have decided to use version numbers, in which case your mass rename will fail with a version conflict when it hits the renamed README.asciidoc file.

    README.asciidoc 로 이름을 변경했을 때, 많은 양의 이름 변경이 버전 충돌로 실패하는 경우,version number를 사용하기로 결정해야 한다.

  • You didn’t use versioning, and your changes will overwrite the changes from the other user.

    version number를 사용하지 않고, 다른 사용자의 변경 사항을 덮어 쓴다.

The problem is that Elasticsearch does not support ACID transactions. Changes to individual documents are ACIDic, but not changes involving multiple documents.

문제는 Elasticsearch가 ACID transactions을 지원하지 않는 것이다. 개별 document에 대한 변경은 ACID를 보장하지만, 여러 document에 대한 변경은 아니다.

If your main data store is a relational database, and Elasticsearch is simply being used as a search engine or as a way to improve performance, make your changes in the database first and replicate those changes to Elasticsearch after they have succeeded. This way, you benefit from the ACID transactions available in the database, and all changes to Elasticsearch happen in the right order. Concurrency is dealt with in the relational database.

주 데이터 저장소가 RDB이고, Elasticsearch는 단순히 검색엔진이나 성능 향상의 수단으로 사용된다면, 먼저 데이터베이스를 변경하고, 그것이 성공한 후에, 그 변경 사항을 Elasticsearch로 한번에 복제한다. 이것은 데이터베이스에서 이용할 수 있는 ACID Transaction의 혜택을 받는 방식이고, Elasticsearch에 대한 모든 변경 사항은 올바른 순서로 일어난다. 동시성은 RDB에서 처리한다.

If you are not using a relational store, these concurrency issues need to be dealt with at the Elasticsearch level. The following are three practical solutions using Elasticsearch, all of which involve some form of locking:

관계형 저장소를 사용하지 않는다면, 이런 동시성 이슈를 Elasticsearch에서 처리해야 한다. 아래에, Elasticsearch에서 사용하는, 모두 어떤 잠금(locking)의 형태를 포함하는, 3가지 현실적인 해법을 제시할 것이다.

  • Global Locking
  • Document Locking
  • Tree Locking
Tip

The solutions described in this section could also be implemented by applying the same principles while using an external system instead of Elasticsearch.

아래에 언급된 해법은, Elasticsearch 대신, 외부의 어떤 시스템을 이용하여, 동일한 원리를 적용하여, 구현될 수도 있다.

Global Lockingedit

We can avoid concurrency issues completely by allowing only one process to make changes at any time. Most changes will involve only a few files and will complete very quickly. A rename of a top-level directory may block all other changes for longer, but these are likely to be much less frequent.

항상 단일 프로세스만 변경을 하도록 허용하여, 동시성 문제를 완벽하게 피할 수 있다. 대부분의 변경은 몇 개의 파일만을 포함하고, 매우 빠르게 완료된다. 최상위 단계 디렉토리의 이름 변경은 더 오랫동안 다른 모든 변경 모두를 막을 수 있지만, 이런 동작은 그리 자주 있는 동작이 아니다.

Because document-level changes in Elasticsearch are ACIDic, we can use the existence or absence of a document as a global lock. To request a lock, we try to create the global-lock document:

Elasticsearch에서 document 수준의 변경은 ACID를 보장하기 때문에, global lock으로 document의 존재 여부를 사용할 수 있다. 잠금(lock)을 request하기 위해, global-lock document를 생성(create) 해 보자.

PUT /fs/lock/global/_create
{}

If this create request fails with a conflict exception, another process has already been granted the global lock and we will have to try again later. If it succeeds, we are now the proud owners of the global lock and we can continue with our changes. Once we are finished, we must release the lock by deleting the global lock document:

만약 이 create request가 충돌 예외로 실패하면, 그것은 다른 프로세스가 이미 global lock을 사용하고 있고, 나중에 다시 시도해야 한다는 것을 의미한다. 만약 성공한다면, 이제 global lock의 소유자로서, 변경 작업을 계속할 수 있다. 작업이 완료되면, global lock document를 삭제하여, 잠금을 해제해야 한다.

DELETE /fs/lock/global

Depending on how frequent changes are, and how long they take, a global lock could restrict the performance of a system significantly. We can increase parallelism by making our locking more fine-grained.

변경이 얼마나 자주 일어나고, 얼마나 오래 걸리느냐에 따라, global lock은 시스템의 성능을 크게 제한할 수 있다. 잠금을 더 세분화하여, 병렬 처리를 증가시킬 수 있다.

Document Lockingedit

Instead of locking the whole filesystem, we could lock individual documents by using the same technique as previously described. We can use a scrolled search to retrieve all documents that would be affected by the change and create a lock file for each one:

전체 file system에 대한 잠금 대신, 위에서와 동일한 기술을 사용하여, 개별 document를 잠글 수 있다. 변경에 영향을 받는 모든 document를 가져오기 위하여, scrolled search 를 사용할 수 있고, 그들 각각에 대한 잠금 파일을 생성해야 한다.

PUT /fs/lock/_bulk
{ "create": { "_id": 1}} 
{ "process_id": 123    } 
{ "create": { "_id": 2}}
{ "process_id": 123    }

lock document의 ID는 잠겨야 하는 파일의 ID와 동일해야 한다.

process_id 는 변경을 수행하려는 프로세스를 나타내는 어떤 고유한 ID이다.

If some files are already locked, parts of the bulk request will fail and we will have to try again.

특정 파일이 이미 잠겨 있다면, bulk request의 일부는 실패하고, 다시 시도해야 한다.

Of course, if we try to lock all of the files again, the create statements that we used previously will fail for any file that is already locked by us! Instead of a simple create statement, we need an update request with an upsert parameter and this script:

물론, 파일 모두 를 잠그려고 다시 시도하면, 위에서 사용했던 create 문장은, 이미 잠긴, 어떤 파일에 대해 실패할 것이다. 간단한 create 문장 대신, upsert 매개변수를 사용한, update request와 아래 script가 필요하다.

if ( ctx._source.process_id != process_id ) { 
  assert false;  
}
ctx.op = 'noop'; 

process_id 는 script에 전달한 매개변수다.

assert false 는 exception(update 실패의 원인)을 발생시킨다.

op 를 update 에서 noop 로 바꾸어, update request가 변경되는 것을 방지한다. 하지만 success를 반환한다.

The full update request looks like this:

전체 update request는 아래와 같다.

POST /fs/lock/1/_update
{
  "upsert": { "process_id": 123 },
  "script": "if ( ctx._source.process_id != process_id )
  { assert false }; ctx.op = 'noop';"
  "params": {
    "process_id": 123
  }
}

If the document doesn’t already exist, the upsert document is inserted—much the same as the previous create request. However, if the document does exist, the script looks at the process_idstored in the document. If the process_id matches, no update is performed (noop) but the script returns successfully. If it is different, assert false throws an exception and you know that the lock has failed.

document가 아직 존재하지 않는 경우, upsert document는 이전의 create request와 거의 동일하게, insert 된다. 그러나 document가 존재한다면, script는 document에 저장된 process_id 를 살펴볼 것이다. 만약 process_id 가 동일하면, 업데이트를 중지하고(noop), success를 반환할 것이다. 만약 다르면, assert false 라는 exception을 발생시켜, 잠금이 실패했다는 것을 알린다.

Once all locks have been successfully created, you can proceed with your changes.

모든 잠금이 성공적으로 생성되면, 변경 작업을 진행할 수 있다.

Afterward, you must release all of the locks, which you can do by retrieving all of the locked documents and performing a bulk delete:

그 후, 모든 잠금을 해제해야 한다. 이는 잠긴 document를 모두 가져와 bulk delete를 수행함으로써 가능하다.

POST /fs/_refresh 

GET /fs/lock/_search?scroll=1m 
{
    "sort" : ["_doc"],
    "query": {
        "match" : {
            "process_id" : 123
        }
    }
}

PUT /fs/lock/_bulk
{ "delete": { "_id": 1}}
{ "delete": { "_id": 2}}

refresh 호출은 모든 잠긴(lock) document를 search request에 보이도록 한다.

단일 search request로 많은 결과를 가져와야 할 경우, scroll query를 사용할 수 있다.

Document-level locking enables fine-grained access control, but creating lock files for millions of documents can be expensive. In some cases, you can achieve fine-grained locking with much less work, as shown in the following directory tree scenario.

document 수준의 잠금은 세분화된 액세스 제어가 가능하지만, 수백 만개의 잠금 파일을 생성하려면, 비용이 많이 들 수 있다. 디렉토리 tree 같은 예처럼, 어떤 경우에는, 훨씬 적은 작업으로, 세분화된 잠금을 하는 것이 가능하다.

Tree Lockingedit

Rather than locking every involved document as in the previous example, we could lock just part of the directory tree. We will need exclusive access to the file or directory that we want to rename, which can be achieved with an exclusive lock document:

이전의 예제에서처럼, 관련된 모든 document를 잠그기 보다는, 디렉토리 tree의 일부만을 잠글 수 있다. 이름을 변경하려는 파일이나 디렉토리를 배타적으로 액세스해야 할 필요도 있다. 이것은 exclusive lockdocument로 가능하다.

{ "lock_type": "exclusive" }

And we need shared locks on any parent directories, with a shared lock document:

그리고, shared lock document로, 모든 부모 디렉토리와 잠금을 공유해야 한다.

{
  "lock_type":  "shared",
  "lock_count": 1 
}

lock_count 는 shared lock을 가지고 있는 프로세스의 수를 기록한다.

A process that wants to rename /clinton/projects/elasticsearch/README.txt needs an exclusivelock on that file, and a shared lock on /clinton/clinton/projects, and /clinton/projects/elasticsearch.

/clinton/projects/elasticsearch/README.txt 의 이름을 변경하려는 프로세스는, 해당 파일에 대한 exclusive lock과, /clinton/clinton/projects/clinton/projects/elasticsearch 에 대한 shared lock 이 필요하다.

A simple create request will suffice for the exclusive lock, but the shared lock needs a scripted update to implement some extra logic:

간단한 create request는 exclusive lock으로 충분히 가능하지만, shared lock은 추가로 몇 가지를 구현한, script로 된 update가 필요하다.

if (ctx._source.lock_type == 'exclusive') {
  assert false; 
}
ctx._source.lock_count++ 

lock_type 이 exclusive 이면, assert 문장은 exception(update request가 실패한 원인)을 발생시킨다.

그렇지 않으면, lock_count 를 증가시킨다.

This script handles the case where the lock document already exists, but we will also need an upsert document to handle the case where it doesn’t exist yet. The full update request is as follows:

이 script는 lock document가 이미 존재하는 경우를 처리하지만, 아직 존재하지 않는 경우를 처리하기 위해, upsert document도 필요하다. 전체 update request는 아래와 같다.

POST /fs/lock/%2Fclinton/_update 
{
  "upsert": { 
    "lock_type":  "shared",
    "lock_count": 1
  },
  "script": "if (ctx._source.lock_type == 'exclusive')
  { assert false }; ctx._source.lock_count++"
}

document의 ID는, URL-Encode된 %2fclinton 으로, /clinton 이다.

upsert document는, document가 아직 존재하지 않으면, insert된다.

Once we succeed in gaining a shared lock on all of the parent directories, we try to create an exclusive lock on the file itself:

모든 부모 디렉토리에 대한 shared lock 확보에 성공하면, 파일 자체에 대한 exclusive lock의 create 를 시도한다.

PUT /fs/lock/%2Fclinton%2fprojects%2felasticsearch%2fREADME.txt/_create
{ "lock_type": "exclusive" }

Now, if somebody else wants to rename the /clinton directory, they would have to gain an exclusive lock on that path:

이제, 누군가가 /clinton 디렉토리의 이름을 변경하려 한다면, 해당 경로에 대한 exclusive lock을 얻어야 한다.

PUT /fs/lock/%2Fclinton/_create
{ "lock_type": "exclusive" }

This request would fail because a lock document with the same ID already exists. The other user would have to wait until our operation is done and we have released our locks. The exclusive lock can just be deleted:

동일한 ID를 가진 lock document가 이미 존재하기 때문에, 이 request는 실패한다. 다른 사용자는 연산이 종료되고, 잠금이 해제될 때까지 기다려야 한다. exclusive lock은 삭제만 가능하다.

DELETE /fs/lock/%2Fclinton%2fprojects%2felasticsearch%2fREADME.txt

The shared locks need another script that decrements the lock_count and, if the count drops to zero, deletes the lock document:

shared lock은, lock_count 를 감소시키는, 또 다른 script가 필요하다. 그리고 count가 0이 되면, lockdocument를 삭제한다.

if (--ctx._source.lock_count == 0) {
  ctx.op = 'delete' 
}

lock_count 가 0 이 되면, ctx.op 는 update 에서 delete 로 변경된다.

This update request would need to be run for each parent directory in reverse order, from longest to shortest:

이 update request는 각각의 부모 디렉토리에 대해, 역순(가장 긴 것부터 가장 짧은 것까지)으로 동작해야 한다.

POST /fs/lock/%2Fclinton%2fprojects%2felasticsearch/_update
{
  "script": "if (--ctx._source.lock_count == 0) { ctx.op = 'delete' } "
}

Tree locking gives us fine-grained concurrency control with the minimum of effort. Of course, it is not applicable to every situation—the data model must have some sort of access path like the directory tree for it to work.

tree locking은 최소한의 노력으로, 세분화된 동시성 제어를 가능하게 한다. 물론, 이것이 모든 상황에 적합하지는 않다. 이 데이터 모델이 동작하려면, 디렉토리 tree처럼, 액세스 경로(access path)의 일종을 가지고 있어야 한다.

Note

None of the three options—global, document, or tree locking—deals with the thorniest problem associated with locking: what happens if the process holding the lock dies?

잠금을 가진 프로세스가 죽었을 경우 어떤 일이 발생하는가 라는, 잠금과 관련된 골치 아픈 문제는, 3가지 옵션(global, document, tree locking)중 어떤 것도 처리하지 못한다.

The unexpected death of a process leaves us with two problems:

프로세스의 예기치 않은 죽음은 2가지 문제를 남긴다.

  • How do we know that we can release the locks held by the dead process?

    죽은 프로세스가 가지고 있던 lock을 해제할 수 있는 방법이 있는가?

  • How do we clean up the change that the dead process did not manage to complete?

    죽은 프로세스가 완료하지 못한 변경 사항을 정리할 방법은 무엇인가?

These topics are beyond the scope of this book, but you will need to give them some thought if you decide to use locking.

이런 주제는 이 책의 범위를 벗어난다. 그러나, 잠금을 사용하기로 결정했다면, 그것들에 대해 생각해야 한다.

While denormalization is a good choice for many projects, the need for locking schemes can make for complicated implementations. Instead, Elasticsearch provides two models that help us deal with related entities: nested objects and parent-child relationships.

비정규화는 많은 프로젝트에서 좋은 선택이지만, 잠금 방식에 대한 필요성은 구현을 복잡하게 만들 수 있다. Elasticsearch는 관련된 entity(related entities)를 다루기 위해, 2가지 모델(nested objects 과 parent-child relationships)을 제공한다.


'2.X > 6. Modeling Your Data' 카테고리의 다른 글

6-1-3. Field Collapsing  (0) 2017.09.23
6-1-4. Denormalization and Concurrency  (0) 2017.09.23
6-2. Nested Objects  (0) 2017.09.23
6-2-1. Nested Object Mapping  (0) 2017.09.23
6-2-2. Querying a Nested Object  (0) 2017.09.23