월별 글 목록: 2018년 6월월

Elasticsearch 에서 Mapping 의 중요성을 느끼다.

안녕하세요.

제가 개인적으로 개발하고 있는 프로젝트가 있습니다.
https://github.com/LeeKyoungIl/illuminati Project illuminati 인데요.
요즘은 gatekeeper라는 서브 프로젝트를 열심히 개발 중이죠.
https://github.com/LeeKyoungIl/illuminati/tree/feature/gatekeeper_new

Project illuminati는 데이터를 결국 ES로 보내주어 그 데이터를 Kibana에서 사용하도록 만들었던 터라
ES로 save 만 할 뿐 Read 성 기능을 필요가 없었습니다. (어차피 Kibana에서 해주니…)

하지만 Kibana 도 제가 잘 다루지 못하고… 원하는 대로 데이터를 뽑지도 못해서 visualization 툴을 새로 만들고 있는데 그게 위의 Gatekeeper 프로젝트인데요.

여기서 직접 ES 데이터를 핸들링하려고 보니 ES에서 데이터를 읽어오는 기능이 필요하더라구요.
그래서 작업을 하는 중 일반적인 데이터의 검색은 별 문제가 없습니다. 아시다시피 간단한 Query로도 데이터를 쉽게 뽑아낼 수 있죠.

하지만 RDB에서의 Group By에 의한 값과 count를 구하는 Query와 동일한 결과를 뽑아야 하는데 쉽지 않더라구요.

(보통은 업무상에서 ES를 쓸 때 빠른 검색 결과를 뽑기 위해서 다양한 필드에 특정 키워드 검색 (날짜와 같이) 정도로 쓰지 ES를 이용해서 데이터 집계를 하는 용도로는 사용해 본 적이 없는 거 같네요.)

하지만 물론 집계 쿼리 자체는 어렵지 않습니다.

간단하게 aggregation query 만 날리면 뽑을 수 있죠… 하지만.. 저는 해보니 오류가…

저는 serverInfo 의 hostName field를 이용해 hostName 별로 호출된 수를 구하고 싶었는데 기본적으로 serverInfo.hostName의경우 text field로 설정이 되어있습니다. 이경우 집계 쿼리를 사용할 수 없나 봐요. (저도 공부 중이라..)

그래서 fielddata=true로 설정하라고 하네요. (메모리가 많이 사용될 수 있다고 합니다.)

이 방법 보다는 좀.. 찾아보니

이렇게.. 정렬이나 집계를 원하면 text 가 아닌 keyword로 설정을 하라고 되어있더라구요.

그래서 새롭게 index를 mapping 해주기 위해 쿼리를 날렸습니다.

결과는 오류…

오류가 난 이유는.. ES는 매핑을 update 할 수 없습니다. 만약 데이터가 있다면 update시 타입이 변경이 불가능하기 때문이죠.

그래서 index를 날리고 새롭게 맵핑을 해주었습니다.

다음 다시 집계 쿼리 실행!

제가 원하는 대로 데이터가 잘 나오는군요.

 

ES는 데이터를 입력 시에 자동으로 데이터를 기반으로 mapping을 생성합니다.

제가 이번에 테스트를 한 hostName는 제가 한것처럼 따로 mapping을 하지 않으면 데이터가 입력이 될때 text로 맵핑이 자동으로 된 걸 볼 수 있습니다.

그래서 ES에서는 맵핑을 직접 하는 것을 권장하는 것 같습니다.

그럼 하나하나 직접 이렇게 mapping query를 날려 줘야 하나?? 해서 만들어 보았는데요.

MappingType 이라는 enum을 만들었습니다.
https://github.com/LeeKyoungIl/illuminati/blob/feature/gatekeeper_new/illuminati-client/illuminati-client-common/src/main/java/com/leekyoungil/illuminati/common/dto/enums/MappingType.java#L3-L27

다음 해당 Mapping값을 사용할 GroupMapping 에노테이션을 하나 만들어 주었습니다.
https://github.com/LeeKyoungIl/illuminati/blob/feature/gatekeeper_new/illuminati-client/illuminati-client-common/src/main/java/com/leekyoungil/illuminati/common/dto/GroupMapping.java#L7-L13

디음은 제가 집계를 원하는 항목에 해당 에노테이션을 달아 주었습니다.
항목은 ServerInfo class의 hostName 과 serverIp입니다.
https://github.com/LeeKyoungIl/illuminati/blob/feature/gatekeeper_new/illuminati-client/illuminati-client-common/src/main/java/com/leekyoungil/illuminati/common/dto/ServerInfo.java#L25-L26

ES로 데이터를 전송할 class 를 시작으로 하위 클래스들을 전부 돌면서 해당 @GroupMapping 에노테이션과 GSON의 @Expose 에노테이션이 걸린 것들을 전부 찾아서 MappingBuilder 에 set 해주는 부분 입니다.

https://github.com/LeeKyoungIl/illuminati/blob/feature/gatekeeper_new/illuminati-client/illuminati-client-elasticsearch/src/main/java/com/leekyoungil/illuminati/elasticsearch/model/IlluminatiEsTemplateInterfaceModelImpl.java#L263-L286

이제 ES로 전송을 해야겠죠 이 부분에서 고민 많이 했습니다.

mapping 은 이미 생성되어있으면 수정이 불가능하기 때문에 ES로 데이터를 저장하기 전에 먼저 mapping 정보가 있는지를 먼저 체크를 한 다음 없다면 mapping을 만들어 주고 (@GroupMapping 에노 테이션을 걸은 것들만 생성이 먼저 날아가며 나머지는 최초 데이터 저장 시 자동으로 mapping 됨) 나서 다음 ES로 데이터를 저장하는 방식으로 일단 구현해 보았습니다.

이게 항상 ES에 데이터를 저장할 때 먼저 체크하는 호출이 날아가야 하기 때문에 정말 마음에 들지 않습니다만.. 일단 이렇게 한 뒤 차차 수정해 나가도록 할 생각입니다.

아무튼 로직은 아래와 같습니다.

ES 에 save 를 할때 먼저 index를 체크하고 없다면 만들어 준다. (원래 mapping 이지만 RDB에 친숙하게 index로 메서드 명을 만들었습니다.)

https://github.com/LeeKyoungIl/illuminati/blob/feature/gatekeeper_new/illuminati-client/illuminati-client-elasticsearch/src/main/java/com/leekyoungil/illuminati/elasticsearch/infra/ESclientImpl.java#L56-L57

ES에 해당 mapping 이 있는지 없는지 질의하고 없다면 index 를 생성해 주는 질의를 날리는 부분 입니다.

https://github.com/LeeKyoungIl/illuminati/blob/feature/gatekeeper_new/illuminati-client/illuminati-client-elasticsearch/src/main/java/com/leekyoungil/illuminati/elasticsearch/infra/ESclientImpl.java#L165-L173

제가 아직 ES를 잘 모르고 공부를 하는 중 이라.. 이렇게 만들어 보았는데요.

역시나… ES에 데이터를 저장 할때마다 mapping 을 체크 하는 부분이 걸리네요..

좋은 의견 있으면 부탁좀 드리겠습니다~

코드 작성 시 무엇을 우선시할 것인가? 성능? 가독성? 유지보수성?

제가 개인적으로 개발하고 있는 프로젝트가 있습니다.
https://github.com/LeeKyoungIl/illuminati Project illuminati 인데요.
요즘은 gatekeeper라는 서브 프로젝트를 열심히 개발 중이죠.
https://github.com/LeeKyoungIl/illuminati/tree/feature/gatekeeper_new

이 서브 프로젝트를 개발 하면서 코드가 전반적으로 구조나 내용이 개선이 많이 되고 있는데요.

요즘 들어 작업을 계속하면서 생각하는 게 우리가 코드를 짤 때 무엇을 우선시 하며 짤 것인가?

  1. 성능
  2. 가독성
  3. 유지 보수성

3가지 다 중요합니다.

하지만 요즘처럼 하드웨어 파워가 좋은 시절은 1번은 정말 심하지 않은 경우 (막 loop 몇 개씩 중첩) 크게 상관이 없지 않을까 싶네요.

제가 요즘 개발을 하면서 (Java 기준으로) 생각하는 부분이 있는데 가령 Boolean 관련 조건문 처리에서

많은 분들이 이런 식으로 코드를 짜고 있습니다.

가장 단순 하지만 폰트에 따라 false 체크의 경우 앞에 ! 표가 잘 안 보일 때도 있고.. (노안이..) 실수를 할 수도 있는 여지가 있다고 생각해요.

그래서 저는 요즘 이런 코드를

이렇게 작성하고 있는데요.

당연히 Boolean 은 bool 의 레퍼 타입 이기 때문에 성능적으로 떨어지기야 하겠죠..

하지만 가독성은 월등히 높져? 그리고 솔직히 성능도 뭐 크게 차이 안 날 듯 합니다.

(그리고 소스 보면 public static final 임)

그래도 성능이 얼마나 차이가 날까? 하고 테스트를 해보았는데 결과는 아래와 같습니다.
(단순 for문 1억번 조건문 비교)

nanos 기준
if (a) : 4090656
if (a == Boolean.TRUE) : 6280227

ms 기준
if (a) : 3
if (a == Boolean.TRUE) : 5

그리고 다른 사람들 의견을 구글링을 해서 좀 찾아보았는데 글 중에 마음에 와 닿은 글이

정리를 하면 성능보다는 단순성, 가독성, 유지보수성이 무엇보다도 중요하다 라는 소리죠.

위에 제가 개인적으로 하는 프로젝트의 경우는 저 혼자 하는 것이라 어떤 방법으로 짜던 상관없지만… (오픈 소스이긴 해도)
우리가 업무적으로 협업을 할 때는 가독성이 최우선되어 유지보수가 쉬운 코드를 짜야하지 않을까요?

지옥에서 온 CTO 님 (제가 존경하는 형님이 그분을 그렇게 부르더라구요?) 이 예전에 페이스북에 남기신 글이 있는데 주석이 필요 없이 코드만 보고도 이해가 가능한 코드를 짜야한다.

저도 업무를 하면서 지난날을 돌이켜 보며 많이 반성을 하고 (동료들에게 미안…) 글고 저 말을 가슴에 새기며.. 개발을 해야겠습니다.

저는 아직 실력이 부족해서 이렇게 까지 코드를 가독성 있고 주석이 필요 없을 정도로 짤 능력이 안되지만 목표가 생겨서 즐겁네요.

 

ps. 해당 주제로 시니어 분과 대화결과 프리미티브 타입만으로도 boolean이라는 것이 전달이 잘 되는데 굳이 레퍼런스 타입을 사용할 필요가 없고 레퍼런스 타입이 가치가 있는 경우가 있는 부분이 있긴 한데 이런 부분에서는 사용하는건 좀 오버엔지니어링 것 같다는 의견을 주셨습니다. 곰곰히 생각해본 결과 시니어 님이 의견 주신대로 true 는 if (a) false 는 if (false == a) 이정도로 사용해도 충분히 가독성을 높일 수 있을꺼 같습니다. (정상혁님 의견 감사 드립니다.)

대용량 Job 처리시 Hibernate Session Memory leak 경험

안녕하세요. 이경일입니다.

그동안 illuminati 개발을 하느라고 블로그에 정리를 하는 것을 소홀히 했네요.

Project illuminati git : https://github.com/LeeKyoungIl/illuminati <- 허접하지만..별 좀 주세요… (굽신굽신)

오늘은 hibernate를 사용하는데 대용량 Job 처리 시에 발생할 수 있는 OOM 에 관해서 정리를 해보려고 합니다.

제가 하는 업무에서 외부와 연동하는 Job을 돌릴 일이 있었는데요. 종종 오류가 발생을 하는 현상이 발견되었습니다.

해당 Job은 실행이 될 때마다 대상의 개수가 변하기 때문에 작게는 몇백 개 많게는 몇십만 개가 넘을 때도 있었죠.

현상은 몇백 개 돌 때는 괜찮은데 10만 개가 넘어가면 오류가 발생을 하더라구요…

오류부터 보겠습니다.

 

 

OOM 이 발생하고 있는 것을 볼 수 있습니다.

해당 잡은 하루에 한 번 돌고 한번 돌면 한 시간 이상을 그냥 돌아가는 대용량의 잡이죠…

Job 서버라 10만 개가 넘어갈 무렵 heap Dump를 떠 보았습니다.

보니 메모리 릭이 발생하고 있네요.

전체 Heap의 74%를 점유하고 있는 부분을 찾아보았습니다. StatefulPersistenceContext 에 메모리가 누적이 되어있어서 자세하게 어떤 Object 인지 찾아보았습니다.

 

헐… HashMap 노드 개수 보이시나요?

 

동일한 이슈가 있나 구글링을 한 결과 발견을 했고.

(구글신 없으면 개발 어떻게 할지 막막하네요 ㅜㅜ…)

대강 핵심만 정리해 보면 Hibernate session 이 열려서 객체를 영속화 하면 당연히 cache를 하는데요. job의 경우 보통은 jenkins를 이용해서 실행을 시키면 빌드를 하고 -> Job 실행 -> 종료 이런 라이프 사이클을 가지죠 그러면 ORM을 사용하면 Hibernate session 은 Job이 실행을 하고 종료를 할 때까지 살아 있다고 보면 됩니다. 따라서 해당 Job에서 100개던 20만 개던 데이터를 DB에서 영속화 시키는 object를 전부다 cache 해서 가지고 있겠죠. 이게 Heap 용량을 넘어가지 않는다면 잡이 종료되며 클리어가 되겠지만 넘어가면 OOM이 발생하며 Job이 중지가 되는 상황인 겁니다. 따라서 session을 flush 하고 clear를 해줘야 하는 이슈가 생기는 것이죠.

 


따라서 코드에 해당 부분을 추가해 주었습니다.

 

결과는 짠~? 해결 되었습니다.

오늘도 이렇게 하루를 넘기는군요.

 

 

나만 몰랐던 Http Delete Method payload body 문제

오늘 해당 문제로 좀 삽질을 했습니다.  (다 저의 무지함 때문에 발생한 일이지만… ㅜㅜ)

간단한 Delete 요청을 처리하기 위해 동료 개발자와 협업을 하고 있었죠.  서로 다른 도메인을 담당하기 때문에 직접 해당 DB에 붙어 CRUD를 날릴 수가 없었고
시스템 간의 의존도를 없애기 위해 동료 개발자는 API를 만들어 주고 저는 해당 API를 이용해서 특정 정보를 CRUD 하는 작업을 하고 있었죠.
CRU까진 완료된 상황 이제 D (Delete)가 남아서 Http의 Delete 메서드를 이용해서 Pqyload Body에는 간단한 ID 정보만 실어서 요청해주면 끝.

집에 가야지… 했는데 해당 API요청이 오류가 나는 것이 었습니다.

해당 코드입니다. 저희는 Grails + Groovy를 사용 중인데 Grails의 restBuilder를 사용해서 Json으로 변환된 Map을 이용해 Rest API 호출을 하는 간단한 코드입니다.

restBuilder 는 Spring 의 RestTemplate 으로 구현되어있습니다.

이제 동료 개발자가 만든 받는 쪽 소스를 보면

이렇게 간단하게 Delete 메서드로 요청한 Json String을 RequestBody로 파싱 해서 객체로 만들어 주는 간단한 Controller 코드입니다.
하지만 요청을 하면 오류…… API 서버 로그를 보면

이렇게 오류가 나오고 있었죠.
처음에는 또 Grails 가 이상하게 구현이 되어있는 줄 알고 Break 포인트 걸어가며 한줄한줄 소스를 까 보았는데 문제가 없는 상황…

그래서 구글링을 해서 Http Spec을 확인…

아 젠장 ㅜㅜ Spec먼저 보고 할껄…

허접 번역을 하면 “Delete 요청은 payload body 가 필요가 없다.  Delete 메서드로 payload body를 실어 요청을 하면 요청이 거절될 수 있다.

일단 API 서버가 제 담당이 아니지만 당장 일정이 있기에 최소한의 수정으로 끝내고자 일단 Delete Method를 이용해 payload body 를 보낼 방법을 찾기 시작했습니다.

일단 나온것이

톰켓의 컨넥터 설정을 바꿔서 DELETE 메서드와 같이 오는 payload body를 파싱 하는 방법 , 해당 코드는 Tomcat를 사용할때 설정으로 하는 방법이고

Spring Boot 의 embedded tomcat 을 사용한다면.

대략 이런식으로 해주면 됩니다.

하지만 위 방법으로 결국 동작 하지 않았습니다…. 결국 그냥 동료에게 양해를 구하고 …

그냥 내가 직접 API서버의 코드를 PathVariable 로 ID 값을 받도록 수정을 했습니다.

이상으로 나만 모르는 Delete method 사용법이었습니다.

오늘의 교훈… 머리가 나쁘면 몸이 고생하며 괜한 야근을 한다… ㅜㅜ 스펙을 읽어보고 개발을 하자.

그런데 의문점 하나는 Swagger 에서는 delete method 로 payload body 에 json 데이타를 같이 보내면
동작을 한다는거…. 이거 소스한번 까봐야 겠어요.. 지금은 너무 힘들어서 일단 여기까지. ㅜㅜ