Elasticsearch의 노드 

Elasticsearch의 인스턴스를 시작하는 동시에 노드도 같이 시작된다. 노드들을 연결해놓은 것을 클러스터라고 한다.

만약 하나의 엘라스틱 서치 노드만을 실행시킨 경우도 하나의 노드를 가진 클러스터라고 한다. 

클러스터안에서 모든 노드는 HTTP와 Transport 트래픽을 기본적으로 다룬다. Transport 레이어는 오로지 노드들과 Java TransportClient와의 통신에만 사용된다. Http 레이어는 오직 외부 Rest Cliente들과 통신할 때 사용된다.

모든 노드는 클러스터 안에서 서로 다른 노드들에 대하여 알고 있고 client에 요청을 적적한 노드로 향하게 조절해준다. 기본적으로 노드는 master-eligible, data, ingest, machine learning이 존대한다.

 

Elasticsearch의 노드 종류

Master-eligible 노드

- node.master를 true로 지정하며 클러스터의 컨트롤을 통해 마스터 노드로 선택될 자격을 가지게 된다.

- 마스터 노드는 클러스터에서 인덱스를 만들고 지우는 행위, 클러스터에서 노드들을 트래킹하고 각각의 노드를 샤드를 할당할건지 결정한다.

- Masger Eligible Node등에서 마스터 노드는 마스터 설출 프로세스를 통해 선출된다.

(https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-discovery.html)

- 마스터 노드는 데이터 노드처럼 데이터와 폴더에 접근 권한이 있어야 한다. 노드가 재시작 하는 사이에도 클러스터 상태가 유지되어야 하기 때문에 접근이 가능해야 한다.

- 데이터를 인덱싱하고 찾고 하는 작업은 CPU, Memory, I/O 자원을 많이 사용하기 때문에 큰 규모에서는 data node와 master node를 구별한다.

- 마스터 노드도 Coordinating Node 처럼 데이터를 routing하고 모으고 하는 작업이 가능하지만 이는 마스터 노드가 하는 주 목적이 아니다. 안정적인 마스터 노드를 운영하기 위해서는 자기 일만 딱 하게 해주는 것이 좋다.

- 마스터 노드로 노드를 지정하기 위한 기본 설정 값은 다음과 같다.

node.master: true 
node.data: false 
node.ingest: false 
node.ml: false 
xpack.ml.enabled: true 
cluster.remote.connect: false

 

Data 노드

- node.data가 true로 지정된 노드는 데이터를 가지고 있을 수 있고 CRUD, 검색, aggregation 등의 데이터와 관련된 작업이 가능하다.

- 데이터 노드는 인덱싱 된 Document를 포함하고 있는 샤드를 관리한다.

- 데이터 노드는 데이터를 직접적으로 다루기 때문에 리소스 자원이 많이 필요하다.

- 데이터 노드로 노드를 지정하기 위한 기본 설정 값은 다음과 같다.

node.master: false 
node.data: true 
node.ingest: false 
node.ml: false 
cluster.remote.connect: false

 

Ingest 노드 

- node.ingest가 true로 지정된 노드가 Document가 인덱싱 되기 전에 변형되고 풍성하게 하기 위해서 Document를 ingest pipeline으로 적용할 수 있다.

- ingetst node는 pre processing 파이프라인을 실행하고 하나 또는 하나 이상의 ingest processor들을 모으는 작업을 한다.

- ingest를 로드하는건 무겁기 때문에 데이터나 마스터 노드에서는 node.ingest를 false로 지정하는 것이 좋다.

- 많은 리소스를 잡아먹기 때문에 ingest node는 별도로 지정하는 것이 좋다.

- ingest 노드로 노드를 지정하기 위한 기본 설정 값은 다음과 같다.

node.master: false 
node.data: false 
node.ingest: true 
node.ml: false 
cluster.remote.connect: false

 

Machine Learning 노드

- xpack.ml이 true로 지정되어 있고 node.ml이 true로 설정되어 있는 노드는 기본적으로 엘라스틱서치에서 분배하는 행위를 한다.

- 만약 머신러닝 특징을 사용하고 싶으면 적어도 클러스터 내에 하나의 머신러닝 노드가 있어야 한다.

 

Coordinating 노드

- 검색 요청과 bulk indexing과 같은 요청들은 다른 노드들의 있는 데이터를 많이 다룬다.

- 데이터가 흩어져 있는 경우 데이터가 있는 노드로 향하게 조정해준다. 각각의 데이터 노드는 요청을 자체적으로 처리하고 그것의 값을 Coordinating 노드에 전달해준다. 그럼 Coordinating 노드는 이를 모아서 하나의 데이터 형태로 정제하여 반환한다.

- 각각의 노드는 Coordinating node가 될 수 있다. 대신 node.master, node.data, node.integer가 false로 되어 있어야 한다.

- 또한 데이터를 모으고 조작하고 하는 작업이 많기 때문에 Coordinating 노드는 메모리랑 CPU에 대한 자원이 많아야 한다. 그렇기 때문에 오직 요청을 라우팅하고 검색 구절을 조절하고, bulk indexing 분배작업을 하는 노드로만 사용하는 게 좋다.

- Coordinating node는 본질적으로 로드 밸런싱 같은 역할을 한다.

- Coordinating node로 지정하기 위한 설정은 다음과 같다.

node.master: false 
node.data: false 
node.ingest: false 
node.ml: false 
cluster.remote.connect: false

 

Node Data Path Setting

path.data

- 모든 데이터와 master-eligible 노드는 샤드 그리고 인덱스, 클러스터 메타데이터가 저장되어 있는 데이터 디렉토리를 접근한다.

- path.data는 기본적으로 $ES_HOME/data로 지정되어 있지만 elasticsearch.yml을 통해서 바꿀 수 있다.

ffmpeg로 동영상 정보 추출과 썸네일을 가져올 수 있다. 간단하게 설치하고 cli를 통해 진행해보자.

 

설치


우선 mac os인경우 brew install ffmpeg로 설치하고 linux인 경우 sudo apt-get install ffmpeg로 설치하면 되다.

 

동영상 정보 가져오기 (ffprobe)


ffmpeg에서 ffprobe를 이용해서 동영상 정보를 추출 할 수 있다. print_format옵션을 사용하여 json 형태로 출력이 가능하다. 만약 프로그램에서 사용 시 node에서는 child_process를 이용해서 실행 수 결과를 받아서 사용하면 된다.

ffprobe -v quiet -print_format json -show_format -show_streams wedul.mp4

 

썸네일 만들기

썸네일만드는 것은 기준시간의 화면을 캡쳐해서 원하는 크기와 포맷으로 썸네일을 만든다. 만드는 방법은 아래 자세히 나와있고 outputStream을 적어서 파일로 내보낼수있고 s3에 올리는 작업등을 하기 위해서는 pipe로 받아서 buffer로 올려도 된다.

https://dev.to/benjaminadk/how-do-i-create-thumbnails-when-i-upload-a-video-aws-lambda-7l4

redis에 만약 200 ~ 300개가 넘는 캐시 정보를 계속 request를 날리면 레이턴시가 발생할 가능성이 크기 때문에 이런경우에 mget, mset, pipeline 등 멀티키 명령어를 사용한다.

하지만 redis가 single mode일 때는 아무 상관이 없지만 cluster mode인경우에는 다음과 같은 오류를 발생 시킨다.

"CROSSSLOT Keys in request don't hash to the same slot"

무슨 오류일까?? 처음에 redis가 싱글모드로 돌고 있던 stage에서 테스트를 해서 정상적으로 멀티키 명령어가 잘 되는줄 알고 배포 했다가 라이브에서 위와 같은 오류가 발생했다...

너무 당황해서 바로 수정했다.

무엇이 문제 였을까? 하면서 문서를 찾아보니 redis에는 16384의 키 슬롯이 있는데 클러스터 모드인 경우 이 키 슬롯들이 각 node에 분할되기 때문에 멀티키 명령어를 사용할 수 없다고 한다.

만약 멀티키 명령어를 사용하고 싶다면 그 키들은 모두 같은 키 슬롯에 들어있어야 한다.

방법은 다음과 같다.

 

같은 키 슬롯에 데이터 넣기


동일한 키 슬롯에 저장하고 싶으면 강제로 지정하는 해야한다. 이때 저장하고자 하는 키들 앞에 {..}의 값을 붙여주고 저장하는 것이다. 예를 들어 저장하고자 하는 데이터에 {product}.products1, {product}.products2로 저장하면 이 두개는 동일한 키 슬롯에 포함되게 된다.

이는 aws elasticcache페이지에 상세히 나와있으니 참고하면 된다.

https://aws.amazon.com/ko/premiumsupport/knowledge-center/elasticache-crossslot-keys-error-redis/

 

ElastiCache 오류 "CROSSSLOT Keys in request don't hash to the same slot" 해결

이 오류는 키가 동일한 노드가 아니라, 동일한 해시 슬롯에 있어야 하기 때문에 발생합니다. 샤딩된 Redis ElastiCache 클러스터(클러스터 모드 활성화됨)에서 다중 키 작업을 구현하려면 키는 동일한 해시 슬롯으로 해시되어야 합니다. 해시 태그를 사용하여 키를 동일한 해시 슬롯에 강제로 배치할 수 있습니다. 이 예제에서는 "myset2" 및 "myset" 세트가 동일한 노드에 있습니다. 이 오류를 해결하려면 해시 태그를 사용하여 키를 동일한 해시

aws.amazon.com

하지만 여기서 중요한건, 우리가 클러스터 모드를 사용하는 이유는 한군데이 집중시키지 않고 값을 분산시켜서 부하를 막기 위해서인데 이를 하나의 키 슬롯에 데이터를 모두 집어 넣을 경우 의미가 없어진다.

그럼 키를 각 노드마다 분산시키면서 멀티키 명령어를 사용할 수 있는 방법이 있을까?

 

클러스터 모드에서 각 노드별 key slot에 데이터 삽입하여 멀티 명령어 사용하기


생각을 바꿔보았다. 싱글모드에서는 키 슬롯 16384개가 존재하고 mset, mget, pipeline등의 명령어가 사용 가능했다. 

그럼 각 노드를 하나의 싱글모드의 레디스라고 생각하면 어떨까? 하는 생각으로 전략을 바꿔봤다.

처음에는 가능할지 모르고 작업을 진행했는데 정상적으로 동작해서 기분이 좋았다.

#작업 순서 (명령어는 ioredis를 기준으로 작성하였다.)

1. 클러스터 모드에서 동작중인 master node에 대한 정보를 가져온다. (cluster mode에는 master 노드와 slave노드가 있는데 slave node는 단순 readonly이고 마스터 노드 값을 가지고 있기 때문에 가져올 필요 없다.)

const masterNodes = redis.nodes('master')

2. 각 노드별 key slot 현황을 확인하고 masterNode와 매핑한다. key slot 현황을 확인하기 위해서는 cluster slots 사용한다. ioredis에서는 redis.cluster('slots')

127.0.0.1:7001> cluster slots
1) 1) (integer) 0
   2) (integer) 4095
   3) 1) "127.0.0.1"
      2) (integer) 7000
   4) 1) "127.0.0.1"
      2) (integer) 7004
2) 1) (integer) 12288
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 7003
   4) 1) "127.0.0.1"
      2) (integer) 7007
3) 1) (integer) 4096
   2) (integer) 8191
   3) 1) "127.0.0.1"
      2) (integer) 7001
   4) 1) "127.0.0.1"
      2) (integer) 7005
4) 1) (integer) 8192
   2) (integer) 12287
   3) 1) "127.0.0.1"
      2) (integer) 7002
   4) 1) "127.0.0.1"
      2) (integer) 7006
const slots = await redis.cluster('slots');

// 각 슬롯정보에 있는 ip, port정보를 확인하여 각 master와 슬롯의 범위를 정리한다.
{
 'ip' : ,
 'port' : ,
 'node' : masterNode[0],
 'slotStart' : 0
 'slotEnd': 4460
},
{
 'ip' : ,
 'port' : ,
 'node' : masterNode[1],
 'slotStart' : 4461
 'slotEnd': 8845
},
.
..
...
....

3. 찾고자하는 키들의 slot 정보를 정리하고 slot 범위에 맞는 오브젝트에 키 값을 배열에 정리한다. slot 정보를 뽑는 알고리즘이 CRC16이기 때문에 이를 이용해서 구할수도 있고 npm에 모듈로 나와있는게 있으니 활용해도 된다. (https://www.npmjs.com/package/cluster-key-slot)

{
 'ip' : ,
 'port' : ,
 'node' : masterNode[0],
 'slotStart' : 0
 'slotEnd': 4460,
 'keys': ['product::1', 'product::2', 'product::5'] // 범위가 0 ~ 4460에 속하고 masterNode[0]에 존재하는 키 슬롯에 저장되는 키들
},
{
 'ip' : ,
 'port' : ,
 'node' : masterNode[1],
 'slotStart' : 4461
 'slotEnd': 8845,
 'keys': ['product::4', 'product::3', 'product::6'] // 범위가 4461 ~ 8845에 속하고 masterNode[1]에 존재하는 키 슬롯에 저장되는 키들
},
.
..
...
....

4. 그리고 정리된 Object를 순회하면서 각 키에 있는 데이터를 조회하기 위한 pipeline을 생성하고 get명령어를 매핑하고 실행시킨다. 없는 데이터는 pipeline을 set으로 설정하고 exec 실행시킨다.

const objs = [{
 'ip' : ,
 'port' : ,
 'node' : masterNode[0],
 'slotStart' : 0
 'slotEnd': 4460,
 'keys': ['product::1', 'product::2', 'product::5']
},
{
 'ip' : ,
 'port' : ,
 'node' : masterNode[1],
 'slotStart' : 4461
 'slotEnd': 8845,
 'keys': ['product::4', 'product::3', 'product::6']
}];

for(const obj of objs) {
 const pipeline = obj.node.pipeline();
 
 obj.keys.forEach(keySlot => pipeline.get(key => pipeline.get);
 
 // 결과
 const ret = await keys.exec();
 
 //..... 없으면 없는 것들은 set로 pipeline 열어서 설정(예를 들어서 keys 배열에 1, 2번째 값이 없다고 가정)
 const setPipeline = obj.node.pipeline();
 
 pipeline.set(obj.keys[1]);
 pipeline.set(obj.keys[2]);
 
 await setPipeline.exec();
}

 

소스를 모두 다 적기에는 양도 많기 때문에 간단하게 내가 진행한 방식에 대한 설명만 적었다.

이 글을 보고 방법을 찾고자 하는 사람들에게 도움이 되지 않을수도 있지만  클러스터 모드에서 레디스의 멀티키 사용법에 대해 자료가 없어서 고민을 많이 했었기 때문에 작은 힌트라도 되었으면 좋겠다.

Multer는 파일 업로드를 위해서 사용되는 multipart/form-data를 다루기 위한 node.js 미들웨어이다. busyboy를 기반으로 하고 있다.
자세한 내용은 https://github.com/expressjs/multer/blob/master/doc/README-ko.md 이곳에서 참고하면 된다.

그럼 간단하게 multipart/form-data로 올린 이미지 파일과 텍스트파일을 request post로 받아서 처리하는 코드를 만들어보자.

우선 multer를 설치한다.
npm i multer

그리고 이미지 파일을 특정 경로에 저장해놓고 사용할 수 있지만 나는 메모리 스토리지를 사용해서 조작하는 방식으로 진행해보겠다.

multer 라이브러리를 선언하고 memoryStorage를 사용할 수 있도록 추가적으로 선언해준다.

1
2
3
const multer = require('multer');
const storage = multer.memoryStorage();
const upload = multer({ storage });
cs

그리고 router에 미들웨어로 upload.single('productImage')를 넣어서 productImage 필드로 넘어온 값을 버퍼로 가져오도록 한다. array, fields 등등 다른 메소드들도 있으나 나는 이미지가 하나라서 single을 사용했다.

1
2
3
4
5
6
7
8
9
router.post('/test', upload.single('productImg'), async (req, res, next) => {
    try {
      console.log(req.file);
      res.json(req.body);
    } catch (e) {
      next(e);
    }
  });
 
cs

그럼 postman을 통해서 데이터를 보내보자.


전송정보

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"method": "POST",
"url": "/info/test",
"headers": {
"authorization": "Bearer 8c6076013db8af716df89b1b48b90c9b2b0fad6c",
"content-type": "multipart/form-data; boundary=--------------------------820616490467091968093229",
"cache-control": "no-cache",
"postman-token": "217b466f-1f40-402e-b2cd-153426c61cc7",
"user-agent": "PostmanRuntime/7.6.0",
"accept": "*/*",
"host": "127.0.0.1:8081",
"accept-encoding": "gzip, deflate",
"content-length": "97764",
"connection": "keep-alive"
},
"body": {},
"query": {},
"level": "info",
"message": "Access"
}
 
cs

이렇게 전송하고 debug로 전송된 정보를 체크해보면 다음과 같이 출력된다.

이 정보를 이용해서 text 파일들은 파일대로 다루고 이미지 파일은 버퍼를 사용해서 s3에 저장을 하던지 여러가지 동작을 진행할 수 있다.

Nginx과 Apache 비교


 

 Nginx

Apache 

특징 

- Nginx는 싱글 스레드 Event driven 방식

- 미리 설정된 worker 프로세스 안에서 요청이 들어올 때 마다 요청을 분배하여 worker에게 역할을 분배

- 기존에 정해놓은 리소스를 사용하기 때문에 CPU, Memory 등의 자원 사용률이 낮음

- 요청이 올 때마다 쓰레드를 생성하여 할당한다. 작업이 많아질 경우 많은 쓰레드할당이 필요하다. 그리고 쓰레드들이 작업을 진행 할 때마다  CPU를 사용하려 하기 때문에 문맥교환이 자주 발생된다. 

차이점 

 - Apache에 경우 Blocking 방식으로 Network, DB 등 별도의 동작이 진행 될 때 Block되지만 Nginx는 Non-blocking 방식을 지원함


Non-blocking, Event Loop 방식의 Node.js


- Event Loop 방식은 요청이 오면 무조건 이벤트 핸들러에게 요청을 넘김. 그 것이 처리되면 완료되었다고 연락이온다. 그래서 Single Thread Non-blocking 방식이 가능

- Single Thread이기 때문에 Multi thread의 자원에 대한 크리티컬 섹션에 대한 문제가 발생하지 않음.

- Node.js는 Single Thread를 사용하기 때문에 CPU에 문맥교환이 발생하지 않아서 Multi Thread를 지원하는 프레임워크보다 무조건 빠를다고 하지만 그렇지는 않다. Node.js에서 요청은 비동기를 지원하지만 내부적으로 Event를 처리할 때는 libio의 Thread Pool에 의해 동작하기 때문에 I/O 작업이 많을 때는 Multi Thread 방식이 유리하다.(Node.js는 메시지 처리와 같이 I/O가 적은 작업이 어울림.) 

- Single Thread이기 때문에 CPU를 많이 잡아먹는 연산일 경우 다른 작업도 모두 느려지기 때문에 연산이 오래걸리는 작업에는 어울리지 않음.

- Node.js에 특성상 오류가 발생하면 프로그램이 죽어버림

- Single Thread Async를 지원하고 있지만 네트워크, DB와 같은 I/O가 발생할 때는 libio의 Thread Pool에 작업이 전달되고 완료되면 EventQueue에 CallBack 함수에 전달된다.


=> 결론 

Single Thread를 사용. 비동기지만 I/O 발생 시 내부적으로 비동기처럼 동작하기 위해 쓰레드 풀을 사용하여 진행한다. 그래서 요청이 많을 경우 문맥교환이 일어나는 Multi Thread보다 오히려 성능이 나쁠 수 있다.


Nginx와 Node.js

Nginx와 Node.js를 사용할 때 성능적으로 도움이 되는 설정을 정리했다.


Port Range

- Nginx에서 Node.js에 요청을 보낼 때는 2개의 TCP 소켓이 필요하다.(2개의 포트가 필요) 그래서 가용 가능한 포트가 적을 경우에 요청을 받지 못하는 문제가 발생한다. 그래서 상황에 따라서 sysctl의 net.ipv4.ip_local_port_range를 사용하여 설정한다.


Time Wait

- Time wait는 커넥션이 종료 되었으나 할당된 연결이 아직 release가 되지 않은 자원이다. 그래서 요청이 와도 자원을 사용할 수 없어서 문제가 발생할 수 있어서 이를 사용할 수있게 해줘야 한다. sysctl의 net.ipv4.tcp_tw_reuse값을 1로 변경해줘야 한다.


Time Wait 시간

- Time Wait 자원을 사용할 수 있는 자원으로 변경되는 시간을 줄여서 빠르게 사용할 수 있게 해주는 것이 좋다. sysctl의 net.ipv4.tcp_fin.timeout값을 줄이면 된다.


Context Switch 

Node.js에서 사용중인 CPU 코어가 다른 프로세스에서 사용되지 않도록하면 성능 향상에 도움이 된다. 여러 프로세스에서 하나의 코어를 같이 사용할 수 있다. 각각의 프로세스는 돌아가면서 코어 스케줄대로 이용한다. 이 경우 문맥교환이 많이 발생하여 CPU 부하가 증가한다. 그렇기 때문에 node에서 사용하는 CPU 코어는 다른 업무를 못하게하여 서로 코어를 잡아먹으려 하는 문맥교환을 줄여야한다.

※ Context switch

- 문맥 교환(Context Switch)이란 하나의 프로세스가 CPU를 사용 중인 상태에서 다른 프로세스가 CPU를 사용하도록 하기 위해, 이전의 프로세스의 상태(문맥)를 보관하고 새로운 프로세스의 상태를 적재하는 작업을 말한다.


현재 Connection 상태를 확인하는 명령어

netstat -tan | awk '{print $6}' | sort | uniq -c


Nginx에서 Upstream이란?

Nginx 설정에서 Upstream이 있다. 여기서 Upstream은 순차적으로 서비스를 처리하기 위해 사용되는 서버를 의미한다. Nginx에서 내장된 Upstream 모듈은 설정된 서버들의 부하분산, 속도 개선을 담당한다. 일반적으로 설정된 서버의 Upstream은 라운드 로빈 방식으로 진행된다.


※ 라운드 로빈

라운드 로빈 스케줄링(Round Robin Scheduling, RR)은 시분할 시스템을 위해 설계된 선점형 스케줄링의 하나로서, 프로세스들 사이에 우선순위를 두지 않고, 순서대로 시간단위(Time Quantum)로 CPU를 할당하는 방식의 CPU 스케줄링 알고리즘이다.


+ Recent posts