Spring batch를 Parallel로 돌려보자

Monolithic 아키텍처 환경에서 가장 잘 돌아가는 어플리케이션 가운데 하나가 배치 작업이다. 모든 데이터와 처리 로직들이 한군데에 모여있기 때문에 최소한의 비용으로 빠르게 기능을 돌릴 수 있다. 데이터 존재하는 Big Database에 접근하거나 Super Application Server에 해당 기능의 수행을 요청하면 된다. 끝!!!

하지만 요즘의 우리가 개발하는 어플리케이션들은 R&R이 끝없이 분리된 Microservices 아키텍처의 세상에서 숨쉬고 있다. 배치가 실행될려면 이 서비스, 저 서비스에 접근해서 데이터를 얻어야 하고, 얻은 데이터를 다른 서비스의 api endpoint를 호출해서 최종적인 뭔가가 만들어지도록 해야한다.  문제는 시간이다!

마이크로서비스 환경에서 시간이 문제가 되는 요인은 여러가지가 있을 수 있다. 배치는 태생적으로 대용량의 데이터를 가지고 실행한다. 따라서 필요한 데이터를 획득하는게 관건이다. 이 데이터를 빠르게 획득할 수 없다면 배치의 실행 속도는 느려지게 된다. 다들 아는 바와 같이 마이크로서비스 환경이 일이 돌아가는 방식은 Big Logic의 실행이 아니라 여러 시스템으로 나뉘어진 Logic간의 Collaboration이다. 그리고 이 연동은 대부분 RESTful을 기반으로 이뤄진다.

RESTful이란 뭔가? HTTP(S) over TCP를 기반으로 한 웹 통신이다. 웹 통신의 특징은 Connectionless이다. (경우에 따라 Connection oriented) 방식이 있긴 하지만, 이건 아주 특수한 경우에나 해당한다. TCP 통신에서 가장 비용이 많이 들어가는 과정은 Connection setup 비용인데, RESTful api를 이용하는 과정에서는 API Call이 매번 발생할 때마다 계속 연결을 새로 맺어야 한다. (HTTP 헤더를 적절히 제어하면 이를 극복할 수도 있을 것 같지만 개발할 때 이를 일반적으로 적용하지는 않기 때문에 일단 스킵. 하지만 언제고 따로 공부해서 적용해봐야할 아젠다인 것 같기는 하다.)

따라서 Monolithic 환경과 같이 특정 데이터베이스들에 연결을 맺고, 이를 읽어들여 처리하는 방식과는 확연하게 대량 데이터를 처리할 때 명확하게 속도 저하가 발생한다. 그것도 아주 심각하게.

다시 말하지만 배치에서 속도는 생명이다. 그러나 개발자는 마이크로서비스를 사랑한다. 이 괴리를 맞출려면…

  1. 병렬처리를 극대화한다.
  2. 로직을 고쳐서 아예 데이터의 수를 줄인다.

근본적인 처방은 두번째 방법이지만, 시간이 별로 없다면 어쩔 수 없다. 병렬 처리로 실행하는 방법을 쓰는 수밖에…
병렬로 실해시키는 가장 간단한 방법은 ThreadPool이다. Springbatch에서 사용 가능한 TaskExecutor 가운데 병렬 처리를 가능하게 해주는 클래스들이 있다.

  • SimpleAsyncTaskExecutor – 필요에 따라 쓰레드를 생성해서 사용하는 방식이다. 연습용이다. 대규모 병렬 처리에는 비추다.
  • ThreadPoolTaskExecutor – 쓰레드 제어를 위한 몇 가지 설정들을 제공한다. 대표적으로 풀을 구성하는 쓰레드의 개수를 정할 수 있다!!! 이외에도 실행되는 작업이 일정 시간 이상 걸렸을 때 이를 종료시킬 수 있는 기능들도 지원하지만… 이런 속성들의 경우에는 크게 쓸일은 없을 것 같다.

제대로 할려면 ThreadPoolTaskExecutor를 사용하는게 좋을 것 같다. 병렬 처리 가능한 TaskExecutor들은 AsyncTaskExecutor 인터페이스 페이지를 읽어보면 알 수 있다.

@Bean(name = "candidateTaskPool")
public TaskExecutor executor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(CORE_TASK_POOL_SIZE);
    executor.setMaxPoolSize(MAX_TASK_POOL_SIZE);
    return executor;
}

이 메소드 정의를 Job Configuration 객체에 반영하면 된다. ThreadPool을 생성할 때 한가지 팁은 Pool을 @Bean annotation을 이용해서 잡아두는게 훨씬 어플리케이션 운영상에 좋다. 작업을 할 때마다 풀을 다시 생성시키는 것이 Cost가 상당하니 말이다. 어떤 Pool이든 매번 만드는 건 어플리케이션 건강에 해롭다.

전체 배치 코드에 이 부분이 어떻게 녹아들어가는지는 아래 코드에서 볼 수 있다.

@Configuration
@EnableBatchProcessing
    public class CandidateJobConfig {
    public static final int CORE_TASK_POOL_SIZE = 24;
    public static final int MAX_TASK_POOL_SIZE = 128;
    public static final int CHUNK_AND_PAGE_SIZE = 400;

    @Bean(name = "candidateTaskPool")
    public TaskExecutor executor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_TASK_POOL_SIZE);
        executor.setMaxPoolSize(MAX_TASK_POOL_SIZE);
        return executor;
    }

    @Bean(name = "candidateStep")
    public Step step(StepBuilderFactory stepBuilderFactory,
                     ItemReader<User> reader,
                     ItemProcessor<User, Candidate> processor,
                     ItemWriter<Candidate> writer) {
        return stepBuilderFactory.get("candidateStep")
                                 .<User, Candidate>chunk(CHUNK_AND_PAGE_SIZE)
                                 .reader(candidateReader)
                                 .processor(candidateProcessor)
                                 .writer(candidateWriter)
                                 .taskExecutor(executor())
                                 .build();
    }

이렇게 하면 간단하다.

하지만 이게 다는 아니다. 이 코드는 다중 쓰레드를 가지고 작업을 병렬로 돌린다. 하지만 이 코드에는 한가지 문제점이 있다. 살펴보면 Chunk라는 단위로 작업이 실행된다는 것을 알 수 있다. Chunk는 데이터를 한개씩 읽는게 아니라 한꺼번에 여러 개(이 예제에서는 CHUNK_AND_PAGE_SIZE)씩 읽어 processor를 통해 실행한다. 배치의 실제 구현에 대한 이해나 고려가 필요하다.

Chunk를 사용해서 IO의 효율성을 높이는 방법은 흔하게 사용되는 방법이다. 하지만 입력 데이터를 Serialized된 형태로 읽어들여야 하는 경우라면 좀 더 고려가 필요하다. MultiThread 방식으로 배치가 실행되면 각 쓰레드들은 자신의 Chunk를 채우기 위해서 Reader를 호출한다. 만약 한번에 해당 Chunk가 채워지지 않으면 추가적인 데이터를 Reader에게 요청한다. 이 과정에서 쓰레드간 Race condition이 발생하고, 결국 읽는 과정에서 오류가 발생될 수 있다. 예를 들어 입력으로 단순 Stream Reader 혹은 RDBMS의 Cursor를 이용하는 경우에는.

문제가 된 케이스에서는 JDBCCursorItemReader를 써서 Reader를 구현하였다. 당연히 멀티 쓰레드 환경에 걸맞는 Synchronization이 없었기 때문에 Cursor의 내부 상태가 뒤죽박죽되어 Exception을 유발시켰다.

가장 간단한 해결 방법은 한번 읽어들일 때 Chunk의 크기와 동일한 크기의 데이터를 읽어들이도록 하는 방법이다. Tricky하지만 상당히 효율적인 방법이다.  Cursor가 DBMS Connection에 의존적이기 때문에 개별 쓰레드가 연결을 따로 맺어서 Cursor를 관리하는 것도 다른 방법일 수 있겠지만, 이러면 좀 많이 복잡해진다. ㅠㅠ 동일한 크기의 데이터를 읽어들이기 위해 Paging 방식으로 데이터를 읽어들일 수 있는 JDBCPagingItemReader 를 사용한다. 관련된 샘플은 다음 두개의 링크를 참고하면 쉽게 적용할 수 있다.

이걸 바탕으로 구현한 예제 Reader 코드는 아래와 같다. 이전에 작성한 코드는 이런 모양이다.

Before

public JdbcCursorItemReader<User> itemReader(DataSource auditDataSource,
                                                @Value("#{jobExecutionContext['oldDate']}") final Date oldDate) {
    JdbcCursorItemReader<User> reader = new JdbcCursorItemReader();
    String sql = "SELECT user_name, last_login_date FROM user WHERE last_login_date < '%s'";
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    reader.setSql(String.format(sql, sdf.format(elevenMonthAgoDate)));
    reader.setDataSource(auditDataSource);
    ...
}

After

public JdbcPagingItemReader<User> itemReader(DataSource auditDataSource,
                                             @Value("#{jobExecutionContext['oldDate']}") final Date oldDate) {
    JdbcPagingItemReader<User> reader = new JdbcPagingItemReader();

    SqlPagingQueryProviderFactoryBean factory = new SqlPagingQueryProviderFactoryBean();
    factory.setDataSource(auditDataSource);
    factory.setSelectClause("SELECT user_name, last_login_date ");
    factory.setFromClause("FROM user ");
    factory.setWhereClause(String.format("WHERE last_login_date < '%s'", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(oldDate)));
    factory.setSortKey("user_name");

    reader.setQueryProvider(factory.getObject());
    reader.setDataSource(auditDataSource);
    reader.setPageSize(CHUNK_AND_PAGE_SIZE);

    ...
}

여기에서 가장 핵심은 CHUNK_AND_PAGE_SIZE라는 Constant다. 이름에서 풍기는 의미를 대강 짐작하겠지만, Chunk에서 읽어들이는 값과 한 페이지에서 읽어들이는 개수가 같아야 한다는 것이다. 이러면 단일 Cursor를 사용하더라도 실행 Thread간의 경합 문제없이 간단히 문제를 잡을 수 있다. 하지만 명심할 건 이것도 뽀록이라는 사실.
이렇게 문제는 해결했지만 과연 마이크로서비스 환경의 배치로서 올바른 모습인가에 대해서는 의구심이 든다. 기존의 배치는 Monolithic 환경에서 개발되었고, 가급적 손을 덜 들이는 관점에서 접근하고 싶어서 쓰레드를 대량으로 투입해서 문제를 해결하긴 했다. 젠킨스를 활용하고, 별도의 어플리케이션 서버를 만들어서 구축한 시스템적인 접근 방법이 그닥 구린건 아니지만 태생적으로 다음의 문제점들이 있다는 생각이 작업중에 들었다.

  • SpringBatch가 기존의 주먹구구식 배치를 구조화시켜서 이쁘게 만든건 인정한다. 하지만 개별 서버의 한계를 넘어서지는 못했다.  한대의 장비라는 한계. 이게 문제다. 성능이 아무리 좋다고 하더라도 한대 장비에서 커버할 수 있는 동시 작업의 한계는 분명하다. 더구나 안쓰고 있는데도 장비가 물려있어야 하니 이것도 좀 낭비인 것 같기도 하고…
  • Microservice 환경의 Transaction cost는 기존 Monolithic에 비해 감내하기 힘든 Lagging effect를 유발시킨다. 그렇다고 개별 개별 서비스의 DB 혹은 Repository를 헤집으면서 데이터를 처리하는건 서비스간의 Indepedency를 유지해야한다는 철학과는 완전 상반된 짓이다. 구린 냄새를 풍기면서까지 이런 짓을 하고 싶지는 않다. Asynchronous하고, Parallelism을 충분히 지원할 수 있는 배치 구조가 필요하다.

한번에 다 할 수 없고, 일단 생각꺼리만 던져둔다. 화두를 던져두면 언젠가는 이야기할 수 있을거고, 재대로 된 방식을 직접 해보거나 대안제를 찾을 수 있겠지.

Frontend crossdomain issue in IE

최근에 서비스를 오픈하면서 겪은 경험담 하나 정리해볼려고 한다.

백엔드 개발자로써 격는 크로스도메인 이슈를 통칭해서 CORS와 관련된 문제라고 이야기한다. API에 대한 요청이 동일 도메인이 아닌 경우에 발생할 수 있는 이슈다. 대부분 정책적인 문제와 관련된 것이라 도메인에 대한 접근 제어 혹은 권한 제어를 통해 해결의 실마리를 찾는다.

이 비슷한 문제가 Frontend쪽에서도 발생할 수 있다는 걸 작업 과정에서 알게 됐다. 문제의 개요를 간단히 설명해보면…

  1. AA 도메인 및 BA 도메인은 A 도메인의 하위 도메인이다.
  2. AA 도메인에서 B 사이트에서 제공하는 기능을 이용해 공통 기능을 구현하였다.
  3. BA 도메인에서 AA 도메인의 기능을 이용하기 위해 AA 도메인에서 제공하는 웹 페이지를 팝업으로 실행한다.
  4. AA 도메인에서는 기본 설정을 잡고, B 도메인의 페이지를 호출한다.
  5. B 도메인에서는 기능을 모두 수행하고 그 결과를 AA 페이지에 반환한다.
  6. AA 도메인의 결과 페이지에서는 최종 결과를 팝업을 실행한 BA 도메인 페이지로 전달한다. 이 전달을 위해 일반적으로 widnow.opener를 사용한다. 보통은 window.opener.location.replace(…) 메소드를 활용해서 Opener 페이지를 다른 페이지로 redirection 시키는 방법이 적용된다.(물론 우리도 이런 방법을 사용했다.)

설명은 복잡하지만 아래 그림을 보면 좀 더 이해가 빠를 것이다.

일반적인 시나리오고, 대부분(?)의 경우에 이 방식은 정상적으로 잘 동작한다. 하지만 IE가 이 대부분의 경우에 포함되지 않는 것 같다.
IE는 마지막 5번 과정에서 팝업을 실행시킨 페이지가 지정된 페이지로 redirection되지 않는다. 기존 화면은 그대로 유지된 상태에서 새로운 탭 화면에서 replace() 메소드로 전달된 url이 나타난다. OMG

재미있는 건 이 현상은 크롬이나 파이어폭스에서는 나타나지 않는다. 오직 IE10, IE11등 IE 브라우저에서만 나타난다. IE에서 당췌 뭔 짓을 한거야???

문제를 좀 파보다보니 다음 문제 사항을 찾게 됐다.
팝업 혹은 frame, iframe 환경에서 서로 다른 도메인간에 데이터를 주고 받는 건 위험하다.
따라서 이걸 위한 적절한 안전 장치가 필요하다.
각 브라우저에 따라 안정 장치를 구현하는 방법이 틀린데 그럼 IE는 이걸 어떤 방식으로 구현했을까?

팝업된 화면에서 페이지 navigation등이 발생해서 opener와 다른 도메인으로 callee 화면이 이동된 경우, window.opener에 대한 callee쪽에서 호출하는 것을 차단한다.

그럼 어떤 방식으로 해야 callee쪽에서 호출했을 때 caller쪽의 페이지를 접근할 수 있는걸까? 여러 방법을 찾아봤지만, 뾰족한 방법이 보이지 않았는데 찾다보니 내부 사이트에 다음과 같은 코드가 존재하는 걸 확인했다.

document.domain

즉, document.domain의 값을 caller와 callee 사이에 맞추면 된다.  실행시킨 화면과 팝업 화면을 나타내는 document의 domain이 동일한 값을 가지면 document.location의 값을 참조하거나 replace() 등의 함수를 사용해서 변경할 수 있다. 하지만 그렇다고 아무 도메인 값을 document.domain 객체 값에 할당할 수 있는 것은 아니다. 현재 도메인이 sub.a.b.c 라고 한다면 document.domain의 값이 될 수 있는 대상자는 a.b.c, b.c 등이 될 수 있다. 아예 다른 도메인으로의 변경은 데이터의 hijacking을 고려해서 허용되지 않는 것 같다.

이 방식을 적용해서 위 그림의 시나리오를 정리해보면 아래와 같은 그림으로 풀어볼 수 있다.

 

  • AA 화면에서 “Click here to popup” 링크가 눌렸을 때 현재 화면의 document.domain의 값을 A.com 으로 JS로 변경한다.
  • 팝업 화면은 Redirection 혹은 내부 페이지 이동을 통해 각 페이지의 hostname이 domain 값이 된다.
  • 팝업의 최종 화면인 end.B.A.com에서 화면을 닫기 전에 A.com 값으로 팝업 페이지의 document.domain 값을 변경한다.  end.B.A.com 도메인의 상위 도메인인 A.com으로 값을 변경하는 것이기 때문에 보안적으로 허용된다.
  • 팝업 화면과 팝업 화면을 실행시키는 화면의 도메인이 이제 A.com 으로 동일해졌다. 이 시점부터 팝업 화면에서 실행 화면의 페이지를 window.opener 객체를 이용해서 제어할 수 있는 길이 생겼다.
  • 이 상태에서 Opener 페이지를 이동시키고, 팝업 화면은 닫히면 된다!!!

보안이라는 관점에서 보면 되려 이런 고려는 오히려 크롬이나 파이어폭스보다 IE가 좀 더 나은거 아닌가 하는 생각도 든다. (하지만 이런 정책이 있다는게 어딘가에 알려졌다면 더 좋았겠지만, 당췌 이 정보를 찾기가 넘 어렵다는게 함정!!!)

암튼 이렇게 해서 문제를 해결해서 어색한 동작을 없앴다라는게 기쁠 뿐이다.

좋은 코드에 대한 생각 – 3: 작업에 대한 기록

개발이라는 건 기록의 작업이다. 코드 한줄을 작성하더라도 이유없는 코드가 없다. 이런 이유로 코드를 작성할 때 그 근거를 기록으로 남길려고 하고 권장한다.

당신은 어떤 방식으로 기록하고 있나? Jira와 같은 티켓 관리 시스템을 이용할 수도 있겠고, 혹은 Confluence에 일지를 쓸수도 있겠다. 하지만 당신이 개발자라면 이 문제를 개발자스럽게 풀고 있을 것이라고 생각한다.

코멘트?

가장 흔하게 생각할 수 있는 방법이고 실제로 많은 개발자들이 작업에 대한 이력을 코멘트로 남긴다. 그림에서 보이는 녹색 코멘트가 가장 대표적인 경우다. 코드를 작성한 이유가 무엇이며 동작이 이런 방식으로 움직인다고 설명한다.

하지만 이런 코멘트는 비추다. 첫번째 이유는 이 코멘트에 사람들이 집중하지 않는다. 개발자라면 코드를 읽을려고 하지 문제가 있지 않는한 코멘트를 읽지 않는다. (사실 정말 그런지도 의문이다. 문제가 있다면 디버깅을 하거나 해당 코드 영역에 대한 테스트 코드를 살펴보지 않을까?)

정말 심각한 문제는 코드를 작성한 본인(들)도 본인들의 코멘트를 지키지 않는다는 사실이다. 기록을 해두면 된다라고 생각하지만 수정 가능한 코드에 대한 기록은 관리가 필요하다.  즉 코드가 바뀌면 코멘트도 바뀌어야 하는데 거의 대부분 코드만 수정하지 코멘트는 수정하지 않는다. 여러 이유가 있지만 그만큼 코멘트는 최초 작성자를 포함해 제대로 읽히지 않는다는 것을 의미한다.

코멘트는 남기지 않는게 좋다. 당신이 남기는 코멘트는 100% 쓰레기가 된다. 이런 쓰레기를 남기는 활동은 정신 건강에 좋지 않다. 그럼에도 불구하고 굳이 코멘트를 남기겠다면 코드 중간에 남기지 말고 Docs 문서 생성이 가능한 메소드 상단에 붙혀준다.

코드 중간에 남기는 코멘트는 진심 백퍼 쓰레기다. 되려 상단에 남기는 코멘트는 메소드의 의미를 설명해주기 때문에 사람들의 눈에 보여질 가능성도 훨 높을 뿐만 아니라 수정 가능성도 덩달아 높아진다.

의미에 이름을 주자.

특정 코드 블럭이 의미나 목적을 가진 부분이라면 이걸 굳이 코멘트로 남길려고 하지 말자.

질척거리는 코멘트보다 함수 혹은 메소드로 의미에 이름을 부여하는 것이 백배 좋다. 메소드의 이름을 적어주고 따로 Javadoc를 목적으로 한 것인지는 모르겠지만 따로 코멘트를 남기는 경우가 있다. 그렇게 남기는 설명이 메소드의 의미를 부연하기 위한 것이라면 차라리 이름을 충분히 길게 작성하자. 코드를 읽는 사람이 두 번 읽게 하는 것보다는 한번 읽어서 의미를 파악할 수 있게 하는게 좋다.

개발자가 싫어해야할 것은 같은 어떤 의미에서든 중복을 없애는 것이다. 그리고 이 규칙은 메소드 이름과 코멘트의 경우에도 마찬가지다. 그리고 이제 메소드의 코드를 최대한 간결하게 작성해서 술술 읽히게 만든다. 충실한 코멘트보다는 쉽게 읽히는 코드가 다른 개발자 혹은 동료를 위한 배려다.

소스 관리 도구들을 활용하자.

그럼에도 불구하고 다른 기록을 남기고 싶은 욕구가 생기는 경우가 있다. 개인 경험상 특정 버그 혹은 요청 티켓을 처리한 경우가 대표적이다. 지금 변경한 코드가 이 티켓에 해당하는 변경이라는 것으로 남기고 다른 사람도 그 티켓을 참조했으면 하고 바라기 때문이다. 앞서 이야기했지만 그렇다고 이걸 코멘트로 코드안에 남겨봐야 의미없다. 더 좋은 방법이 없을까?

코멘트보다 좀 더 효과적인 방법으로 추천할만한 내용은 커밋 로그(Commit log)를 활용하는 것이다.  커밋은 코드의 “특정 작업 단위가 마무리됐다는 것“을 말한다. 마무리된  작업의 내용을 설명하는 것이 커밋 로그이다. 또한 온라인 코드 리뷰 자체도 이 커밋 단위로 리뷰가 진행된다. 따라서 작업 설명은 충실하면 충실할수록 좋다. 작업 내용의 충분한 이해를 바탕으로, 리뷰어가 코드를 살펴보고 리뷰를 남겨준다면 리뷰를 받는 사람에게 더 도움이 된다.

결론은 충실하면 좋다긴 하지만 Source repository로 뭘 사용할 것인가에 따라 접근 방법이 틀려진다. 국내에서는 아직 SVN을 Repository로 사용하는 회사 혹은 개발팀이 많다. 하지만 알다시피 SVN은 중앙에서 소스를 관리한다. 그게 뭔 문제??? 관리 대상이 작다면 별 문제가 안되지만, 누구나 알듯이 많아지면 주구장창 느려지는 문제점이 있다. 더구나 SVN은 커밋을 하게되면 이를 서버에 전송하여 저장한다. 각 커밋 단위가 가지는 의미가 상당하다.

이런 이유로 SVN을 사용하는 대부분의 팀에서는 한 커밋 단위에서 자잘한 코드 변경을 커버하기 보다는 의미있는 단위로 작업하라고 권고한다. 한 커밋 변경에서 변경되는 작업 분량이 커지게 된다. 이 내용을 상세하기 적을려면 분량이 상당해진다. 바꿔 말하면 변경 내용을 알아보게 로그 형태로 적기 힘들다. 잘 적을까? 당연히 안적게 된다. 남길 수 있다면 티켓 번호 정도? 따라서 변경의 주요 내용을 커밋 로그에서 따라잡기 어렵다. 이 상황이 지속된다면 차라리 변경 주요 내용을 코멘트로 님기는게 더 나은 방법일 수도 있다.

그러니까 git을 사용해야 한다. Distributed source repository가 주는 장점이랄까? 혹은 로컬에서 자신의 Copy를 가지고 움직이기 때문에 갖는 장점일지는 모르겠다. 가볍고 빠른다. 그리고 Remote repository에 push를 통해 업로드하기 때문에 각 Commit 단위가 갖는 무게가 상대적으로 가볍다.

Git을 Source repository 사용한다면 다음의 규칙을 사용해서 로깅을 남겨보자.

» 서로 독립적인 변경이 있다면 각각에 대해 모두 커밋 로그를 남긴다. 예를 들어 불필요한 코멘트를 삭제했다면 그것도 커밋의 단위이고, 오타 한글자를 수정했었도 것도 따로 커밋한다.

» 코드 작성/변경 자체가 좁은 범위에 대해 이뤄진다. 로그에 쓸말도 많지 않아야 한다. 50 ~ 80자 이내로 코멘트를 작성하자. 바꿔 이야기하면 이 정도의 내용으로 커버될 내용이 한번의 커밋 대상이어야 한다.

» 다른 사람들과 공유해야하는 경우에만 Remote branch에 push한다. 모 회사의 광고 아닌 광고에 보면 불이 나도 push는 하고 가라는 말이 있긴 하지만 branch 방식으로 PR을 관리한다면 간간히 push를 하지 말고 자주 push를 하는게 좋다. 그렇게 모인걸 PR 날리면 땡이니 Remote에 얼마나 자주 push를 하던 문제가 안된다. (불날때 먼저 나가는게 중요하지 push는 하고 가라는 우스개 소리 아닌 소리를 하는 회사는 별로다.)

» 최종 PR의 타이틀은 간결해야한다. 내용에는 PR통해 반영될 기능들에 대한 설명과 리뷰를 해줬음 하는 대상자들의 이름을 적어둔다. 그리고 사람들 사이에 대화가 이어지면 된다.

 

Source repository를 쓴다면 이런 목적이어야 하지 않을까? 변경이 왜 일어났고, 언제 일어났으며, 누구에 의해 발생했는지를 한 눈에 명확하게 파악할 수 있다. 와중에 git 같은 툴을 사용하고 있다면, 코드가 발전되어 가는 형상을 관찰할 수도 있다. 그렇기 때문에 git을 써야한다.

개인적으로는 코드에 코멘트를 하나도 남기지 않는 것이 최선이라고 생각한다. 코드에 남기는 코멘트는 파일이 최초로 만들어졌을 때 자동 생성되는 작성자 이름과 작성일 정도면 충분할 것 같다. 그 이외에 자동으로 생성되는 나머지 코멘트들은 모두 지워버린다. 남겨두면 그게 득이 되는 경우를 본적이 없는 것 같다.

코딩 세상에서 미니멀리즘을 추구해야할 부분이 바로 이 포인트이지 않을까 싶다.

곱씹기 – 피터 드러커의 최고의 질문

리더쉽에 대한 조직장님의 추천이 있어서 읽게 된 책이다.

목차에 보면 위대한 질문들이 나온다.

» 미션: 왜 무엇을 위해 존재하는가?

» 고객: 반드시 만족시켜야 할 대상은 누구인가?

» 고객 가치: 그들은 무엇을 가치있게 생각하는가?

» 결과: 어떤 결과가 필요하며 그것은 무엇을 의미하는가?

» 계획 수립: 앞으로 무엇을 어떻게 할 것인가?

각 질문에서 이야기하는 것들은 책을 읽어보면 알 것 같고, 전체 문맥을 통해 저자가 이야기하고 싶은 것들을 정리하면 아래와 같은 그림이 되지 않을까 싶다. 그 최종 결과로 나오는 성과가 “고객의 일상을 어떻게 변화시키고 있는가?” 라는 질문에 똑 부르진 답을 내놓을 수 있다면 우리가 하는 일은 올바른 일을 하고 있다고 결론적으로 책에서 이야기한다.

 

개인적으로 간만에 읽은 한글 책이다. 소설책을 제외하고. 하지만 좀 번역이 아주 상당히 이상하다. 아무래도 직역을 한 부분들이 많은 것 같고, 그렇기 때문에 당췌 글이 머리에 잘 들어오지 않는다. 걍 원서로 읽을 걸 그랬다는 생각이 든다.

원서 링크는 여기에…

 

Slack as a slack – 슬랙을 슬랙답게 쓰자

일상 생활에서 여러 메신저 어플을 사용하지만, 업무용 메신저도 따로 있다. 다른 분들은 카톡이나 라인 같은 어플을 업무용으로도 사용할지도 모르겠다. 하지만 개인용 메신저 어플에게 회사의 정보를 믿고 맞긴다는건 말이 안된다. 항상 “사고”는 존재하기 때문이다.

업무용 메신저는 따로 사용해야한다. 큰 기업인 경우 자체적으로 사내 메신저를 만들어서 사용하는 케이스를 본다. 네이버의 경우에도 사내 메신저를 PC부터 모바일까지 죄다 자체적으로 만들어서 사용했었으니까. 자체적으로 만들어 사용하는 한마디로 비추다.

몇가지 메신저를 봤지만, 기업용 메신저의 끝판왕은 슬랙(slack)인 것 같다. 여러 이유가 있지만 그 가운데서 가장 독보적인 이유는 알림 서비스다. 슬랙의 알림 기능, 특히 모바일 어플의 알림 기능은 독보적이다. 가끔은 30분 정말 느리게 올 경우에는 메시지가 입력된 이후 2시간 이후에도 도착하는 경우가 있다.  심지어 이미 내가 읽었음에도 불구하도 친절하게 다시 한번 알려준다는… 하지만 PC버전은 칼같이 메시지 알림이 잘 온다.

이건 불평 대상이 아니냐구? 오~~ No! No!! 당신이 당신의 오피스 자리 혹은 PC를 떠났다는건 여러모로 이유가 있다. 그중 첫번째 이유는 퇴근이다. 그리고 휴식이다. 휴식을 취하는 시간에 오는 연락은 정말 짜증이다. 정권이 바뀐 다음에는 이걸 금지하고 있다. 마땅히 그래야한다. (내가 이런 말을 할 자격이 있는지는 모르겠지만…)

좌우당간 PC 버전을 설치해놓은 당신은 일하고 있음을 증명한 거다. 알람 칼같이 온다. 모바일 버전은??? 깔아는 둬야겠지? 하지만 왠만히 연락 안되면 문자한다. 것도 아니라면 전화를 하던지… 당장 급한 상황임에도 불구하고 굳이 슬랙 메시지를 보내고 연락이 안된다고 성화인 사람! 스마트 폰의 본연의 기능인 전화기 기능을 사용해라.

업무용 슬랙을 잘 사용하는 방법을 좀 적어보자.

  1. Public channel에서 이야기한다.
    업무용 메신저의 가장 큰 목적은 일하는데 필요한 정보를 나누는 것이다. 그리고 대부분의 정보는 일에 관심있는 모든 사람들에게 유익하다. 이 유익한 정보에 대한 접근에 대한 제한을 굳이 둬야할 이유는 없다. 재무나 경영상의 정보와 같이 민감한 정보가 아니라면 모든 걸 회사에 있는 사람들이 접근할 수 있도록 허용하는게 옳다.
  2. Private channel을 가급적 만들지 않는다.
    하지만 정말 아무나 들여다보면 신경이 쓰이는 정보들이 있다. 그런 정보들의 경우에는 제한된 사람들이 보는게 맞고 그런 사람들을 위해 Private channel이라는게 있다. 하지만 두번 생각해볼 필요가 있다. 정말 필요하지 말이다. 뭔가 숨길만한 꺼리가 아니라면 Public channel로 채널을 유지하는게 맞고, 정히 숨겨야할 이야기라면 따로 이야기한다.
  3. 따로 이야기하지 않는다.
    왠만하면 DM(Direct messaging)을 사용하지 말라는 이야기다. 회사에서 주고 받는 대화는 대부분 일에 관련된 것이다. 일에 관련된 게 아니라면 DM을 사용하거나 카톡을 사용하는게 맞겠다. 가쉽꺼리를 원한다면 카톡으로 가서 친구들이랑 이야기하는게 옳다. 그리고 그렇게 쫄리는 이야기라면 회사에서 안하는게 좋다.
  4. 하나의 주제로 모아져야 할 필요가 있을때는 쓰레드(Thread)로 글을 모은다.
    메신저라는 메인 대화 창구로 삼았을 안타깝게도 이미지를 첨부할 수 없다는… 언능 이 기능이 생겼으면 좋겠다!
  5. 어플들을 활용해라.
    개발자들이 사용하는 거의 모든 도구들이 연동된다. Jira, Git, Jenkins, Favro,… 그래서 개발자들이 슬랙을 좋아하는 것 같다.
    어플을 연동해둬야 하는 이유는 심플하다. 관련된 정보들을 슬랙 채널에 모으기 위해서다. 새로운 이슈가 생기던 진행되던 상태의 변화가 있던 모든 내용이 업무 채널에 존재한다. 그리고 그걸 화두로 이야기가 시작된다. 얼마나 멋진가?  그리고 이렇게 모아진 정보들에 대해 슬랙은 멋진 검색 기능을 제공한다. 적절한 검색 필터를 활용하면 효과적으로 자신의 정보를 찾을 수 있다.
  6. 슬랙은 업무용이다. 이점을 잊지 말자.

마이크로소프트가 업무용 메신저를 만든다고 대들었다고 한다. 아직도 이 프로젝트가 진행중인지 모르겠지만,… 슬랙이 짱!!이다.

만약 당신 회사에서 업무용 메신저로 깨톡을 사용한다면… 당장 슬랙으로 바꾸길 권한다.

다중 도메인을 위한 HTTPS Certification 생성 방법

HTTP 환경에서 서비스를 제공하는건 여러모로 불안하다.  단순한 Packet sniffing에도 데이터가 탈취되니 말이다.  이렇게 말하지만 이 사이트 자체도 https를 사용하지 않으니 할말은 없다.

그럼에도 불구하고 회사를 통해 제공되는 서비스는 안전함을 우선해야 한다. 그렇기 때문에 HTTPS 방식으로 서비스를 제공해야하고.  개별 장비에 대한 인증서 생성을 위해서는 Certification 생성을 위해서는 CSR과 KEY 파일을 생성해야 한다.  한 도메인에 대한 생성은 아래 명령을 이용하면 간단히 만들 수 있다.

openssl req -newkey rsa:2048 -nodes -out your_domain_co_kr.csr -keyout your_domain_co_kr.key \
-subj "/C=KR/ST=Seoul/L=Seoul/O=Riot Games Inc./OU=Dev/CN=your.domain.co.kr"

주의할 점은 도메인 이름이 넘 길면 안된다.  아마 64글자까지만 지원하는 것으로 기억한다.

메인 도메인을 포함해 개발(dev) 및 QA(qa) 환경들에 대한 복수 도메인에 대한 발급하는 경우에는 설정 파일을 이용하는 방법을 사용한다.  왜 사용자들이 접하는 도메인도 아닌데 굳이 https를 사용하는지에 대해서는 다른 의견이 있을 수 있다. 하지만실제 환경과 동일한 조건에서 개발과 테스트가 이뤄지는 것이 합당하다.  환경이 차이가 존재하면 이를 맞춰주기 위한 차이가 존재한다.  이런 티끌들이 모여 결국에는 장애를 일으키니까.

복수 도메인에 대한 CSR 및 KEY 파일의 생성은 아래 명령을 참조한다.

openssl req -newkey rsa:2048 -nodes -out your_domain_co_kr.csr -keyout your_domain_co_kr.key \
-config your_domain_co_kr.cfg

your_domain_co_kr.cfg 파일의 내용은 아래와 같이 설정하면 된다.

[req]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn

[ dn ]
C=KR
ST=Seoul
L=Seoul
O=Riot Games Inc.
OU=Dev
emailAddress=owner@domain.co.kr
CN = your.domain.co.kr

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = your.domain.co.kr
DNS.2 = dev.your.domain.co.kr

 

여러 도메인을 사용할거면 차라리 와이드카드(*)를 쓰는게 더 좋지 않을까에 대한 의견도 있을 수 있다.  개인적으로 처음에 사용할 각 도메인을 모두 열거해야한다는 지침을 들었을 때 마찬가지 생각을 가졌다.  당장은 이 도메인만을 사용하지만 앞으로 더 많은 도메인들을 만들어 쓸 계획이었으니까.

하지만 곰곰히 생각해보니 보안적인 측면에서 이 방식이 주는 해로움이 더 크다고 생각했다.  만약 와일드카드 인증서가 탈취된 경우에 해커가 이걸 이용해서 다른 도메인을 만들어 피싱사이트를 만들 수 있으니 말이다.  번거롭더라도 사용할만큼 만들어서 쓰는게 올바른 방법인 것 같다.

그럼에도 불구하고 굳이 와이드카드 방식으로 사용하고 싶다면 CN = *.your.domain.co.kr로 설정하면 된다.

 

보안이라는건 보안 담당자에게 중요한게 아니라 믿고 써주는 사용자들을 지켜주는 것이니까 말이다.

AWS EC2에서 S3 Webhosting에 접속하기

라고 쓰지만 다른 이름으로  “같은 집안끼리 왜 이래!!”로 잡는다.

시스템을 구성하는 과정에서 목적에 따른 다양한 도메인을 별개로 잡기보다는 하나의 도메인에서 각 기능 제공 영역을 reverse proxy로 구성하는 방안을 적용했다. 여러 도메인들을 관리해야하는 피로감이 있었고, 각 도메인별로 따로 Certification을 받아야 하는 프로세스가 귀찮은 것도 있었다.  대강의 구조는 아래와 같이 셋업했다.

기존 설정에서 AWS S3 webhosting에 대한 리소스 접근을 제어하기 위해 Bucket Policy의 aws:SourceIp를 통해 접근 제어를 했다. Public Web Server는 IDC에 존재했기 때문에 웹서버의 Public NAT IP를 sourceIp로 등록해서 나름 안전하게 사용하고 있었다.

이번에 겪은 문제는 IDC의 장비를 AWS EC2 Instance로 이전하면서 발생했다.

당연히 S3 Webhosting으로 접근하는 IP는 EC2 Instance에 부여된 Public IP일 것이기 때문에 이걸 등록해주면 끝! 일거라고 생각했다.

curl http://ifconfig.io

이 명령으로 110.10.10.123이 Public IP로 확인됐다고 했을 때, 아래의 IP에 110.10.10.0/24 대역으로 추가해줬다.

{
"Version": "2012-10-17",
"Id": "Policy1492667932513",
"Statement": [
	{
		"Sid": "Stmt1492667929777",
		"Effect": "Allow",
		"Principal": "*",
		"Action": "s3:GetObject",
		"Resource": "arns3:::reverse1.sample.co.kr/*",
		"Condition": {
			"IpAddress": {
				"aws:SourceIp": [
					"120.10.10.0/24",
					"110.10.10.0/24"
				]
			}
		}
	}
    ]
}

잘 돌겠지? 라고 생각했지만 웬걸… EC2 장비에서 curl로 테스트를 해보니 Access Denied라는 메시지만 나올 뿐이다.

$ curl http://reverse1.sample.co.kr
... Access Denied ...

아무리 S3가 특정 Region에 구속되는 놈이 아니라고 하더라도 왜 같은 집안 사람들끼리 친하지 않지?  이때부터 집안 사람들끼리 왜 이러는지 한참을 구글링을 해봤지만 IP를 가지고 접근 제어를 풀어내지는 못했다.  회사의 다른 사람들에게 물어봤어도 뾰족한 답을 얻을 수 없었는데, 한 분이 힌트를 주셨다.  같은 AWS 환경은 같은 Region에 있는 장비들끼리는 AWS 자체의 내부망을 통해 통신한다는거!

여기에서 힌트를 얻어서 그럼 내부망을 사용한다는 건 VPC를 통해 뭔가 통신이 이뤄질꺼라구 추측하고 관련된 구글링을 해본결과,  VPC 수준에서 Bucket Policy가 있다는 걸 확인했다.

 

이를 반영한 전체 설정 내용은 아래와 같다.

{
"Version": "2017-10-17",
"Id": "PolicyBasedOnIpAndVPC",
"Statement": [
	{
		"Sid": "",
		"Effect": "Allow",
		"Principal": "*",
		"Action": "s3:GetObject",
		"Resource": "arns3:::reverse1.sample.co.kr/*",
		"Condition": {
			"IpAddress": {
				"aws:SourceIp": [
					"120.10.10.0/24"
				]
			}
		}
	},
	{
		"Sid": "",
		"Effect": "Allow",
		"Principal": "*",
		"Action": "s3:GetObject",
		"Resource": "arns3:::reverse1.sample.co.kr/*",
		"Condition": {
			"StringEquals": {
				"aws:sourceVpc": [
					"vpc-a6ccdbcf"
				]
			}
		}
	}
    ]
}

추가한 Allow 설정에서 접근하는 대상의 VPC정보를 확인해서 내부 VPC의 경우에 허용하는 정책을 설정해준다.  이런 설정으로 변경 적용한 이후에 EC2 Instance에서 curl 명령으로 확인해보면 정상적으로 컨텐츠가 노출되는 것을 확인할 수 있다.  EC2 환경을 위한 추가적인 IP 설정등은 필요없는 일이었다.

간단한 것 같지만 몰랐기 때문에 바보처럼 이틀이나 허비를 해버렸다.  VPC를 통한 Policy 설정에 대한 상세 정보는 http://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies-vpc-endpoint.html 에서 확인하면 된다.

 

변수명으로 Readability 높이기?

코드 리뷰를 하다보면 약간 복잡한 expression 혹은 statement의 결과를 변수로 치환한 다음에 아래에서는 그 변수를 사용하는 경우가 있다.  변수의 값 참조가 여러번 이뤄지면 문제가 아니지만 갸우뚱하게 되는 케이스는 한번만 사용하는 경우다.  대부분의 자동화 분석 도구는 이런 경우에 대해 고치라는 처방전을 준다.  하지만 나는 기계가 아니라 사람이고 사람이 보기에 복잡해보이는 걸 두는 것보다는 “변수가 의미를 설명해주는데 좀 더 낫지 않을까?” 라는 생각했다.

우연찮게 이 고민에 대한 글 하나를 봤는데 읽어보니 내 생각이 잘못된 것 같다는 생각이 빡! 한대 후려치고 간다.  글 내용을 간단히 정리하면.

  1. 간단한 expression or statement의 결과를 변수로 뽑는 엉뚱한 짓은 하지 마라. 걍 간단하니 직접 써라.
  2. 변수를 써서 의미를 명확하게 해야만 할 것 같은 충동을 일으키는 복잡한 경우에는 변수쓰지 말고 함수써라.

왜 변수 쓰는데 함수 쓸 생각을 못했을까? 재활용도 하고 테스트도 작성할 수 있는데 말이다. ㅠㅠ

아무래도 가장 간단한 Coding shortcut을 찾다보니까 그런게 아닐까 싶다.  반성하고 성실하게 살아야겠다.

 

Kafka monitoring and administration

카프카(Kafka)는 생각보다 쉬운 툴이다. 하지만 장기적으로 운영할 시스템 환경은 간단히 한번 돌리고 마는 경우와 고려해야 할 것들이 제법된다.  운영 관점의 설정 값들을 엉뚱하게 해놓으면 잘 차려진 밥상에 꼭 재를 뿌리게 된다.  이런 잘못을 범하지 않으려면 Kafka라는 이름뿐만 아니라 이 도구가 어떤 방식으로 동작하는지 깊게 들어가볼 필요가 있다.  물론 그 동작 방식을 운영 환경과 어울려 살펴봐야만 한다.

상태 조회하기

운영을 할려면 가장 먼저 필요한 일이 시스템의 상태를 살펴볼 수 있는 도구가 필요하다.

alias monitor='kafka-run-class.sh kafka.tools.ConsumerOffsetChecker --zookeeper localhost:2181'
alias topic='kafka-topics.sh --zookeeper localhost:2181'
alias consumer='kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic'

이미 있는 명령들을 “쉽게 쓰자”라는 차원에서 alias를 정의하긴 했다.  consumer의 경우에는 Producer를 통해 생성된 Topic 데이터가 정상적으로 생성됐는지를 확인하기 위해 필요하다.  나머지 2 개의 명령은 어떤 경우에 사용하는게 좋을까?

topic 명령은 일반적인 토픽 관리를 위한 기능들을 모두 제공한다. 만들고, 변경하고, 삭제하는 모든 기능을 이 스크립트 명령으로 제어한다. 주목하는 토픽 상태는 describe 옵션으로 확인할 수 있다. 체크해야할 사항은 2가지다.

  • 현재 토픽의 Partition의 수와 Replica가 어떤 노드에 배정이 되어 있는지를 확인할 수 있다.
  • 개별 Partition의 현재 Leader가 어떤 놈인지를 확인할 수 있다.

이 두가지 정보는 이후에 Kafka cluster의 성능 튜닝을 위해 파악해야할 정보다.

monitor 명령은 토픽의 데이터를 subscribe한 특정 그룹으로의 데이터 전달이 제대로 이뤄지고 있는지를 각 Partition별로 확인할 수 있도록 해준다.

출력 결과의 Lag 항목을 보면 현재 처리를 위해 각 파티션에서 대기하고 있는 데이터 개수를 확인할 수 있다.  특정 그룹에서 소비해야할 토픽 데이터의 건수가 증가하지 않고 일정 수준을 유지한다면 Kafka 시스템이 클러스터로써 제대로 동작을 하고 있다는 것을 의미한다.  기본 명령의 출력 결과가 파티션별이기 때문에 총합을 볼려면 각 파티션들의 합을 구하는 기능을 만들어야 아래와 같은 스크립트로 만들 수 있다.

#!/bin/sh
kafka-run-class.sh kafka.tools.ConsumerOffsetChecker --zookeeper localhost:2181 --group $1 --topic $2 | egrep -v "Group" | awk -v date="$(date +"%Y-%m-%d %H:%M:%S ")" '{sum+=$6} END {print date sum}'

만일 특정 그룹과 토픽에 대한 상시적인 모니터링이 필요하다면 위 스크립트를 한번 더 감싸는 아래의 monitor.sh 스크립트를 작성해서 돌리면 된다.  이 스크립트는 문제가 될 그룹과 토픽에 대한 정보를 30초 단위로 조회하여 출력한다.

#!/bin/sh
while (true) do /home/ec2-user/total.sh groupname topic 2> /dev/null; sleep 30; done;

당연한 시간을 너무 짧게 분석하는 것도 되려 시스템 자체에 부하를 줄 수 있다는 사실을 잊지는 말자!

Kafka 최적화하기

Kafka를 단순히 로그 수집용이 아닌 여러 토픽들을 섞어서 메시징 용도로 사용하는 경우에 최적화는 큰 의미를 갖는다.  특히 여러 토픽을 다루는 환경에서 Kafka 운영자는 다음 사항들을 꼼꼼하게 챙겨야 한다.

  • 토픽의 종류별로 데이터의 양이 틀려질 수 있다.
  • 연동하는 서비스 시스템의 성능이 클러스터의 성능에 영향을 미칠 수 있다.

따라서 효율적이고 안정적인 메시징 처리를 위해서 토픽이 데이터 요구량에 대응하여 적절한 파티션들로 구성되어 있는지를 확인해야 한다.  한 토픽에 여러 파티션들이 있으면, 각각의 파티션이 독립적으로 데이터를 처리해서 이를 Consumer(or ConsumerGroup)으로 전달한다. 즉 데이터가 병렬 처리된다.  병렬 처리가 되는건 물론 좋다.  하지만 이 병렬 처리가 특정 장비에서만 처리되면 한 장비만 열라 일하고, 나머지 장비는 놀게 된다.

일을 하더라도 여러 장비들이 빠짐없이 일할 수 있는 평등 사회를 실현해야한다.  Kafka 사회에서 평등을 실현할려면 토픽을 클러스터내의 여러 장비에서 고르게 나눠 실행해야한다.  이런 나눔을 시스템 차원에서 알아서 실현해주면 운영하는 사람이 신경쓸 바가 없겠지만, 현재 버전(내가 사용하는 버전)은 해줘야한다.  -_-;;;

평등 실행을 위해서는 일단 일하길 원하는 노드에 데이터가 들어가야 한다.  따라서 Replica 조정을 먼저 해준다.  조정 완료 후 이제 리더를 다시 뽑는다.  리더를 뽑는 방법은 아래 move.json 파일에서 보는 바와 같이 replicas 항목들의 클러스터 노드의 sequence 조합을 균일하게 섞어야 한다.  예처럼 2, 1, 3, 2, 1 과 같이 목록의 처음에 오는 노드 아이디 값이 잘 섞이도록 한다.

{"partitions": [
    {"topic": "Topic", "partition": 0, "replicas": [2,1,3]},
    {"topic": "Topic", "partition": 1, "replicas": [1,2,3]},
    {"topic": "Topic", "partition": 2, "replicas": [3,1,2]},
    {"topic": "Topic", "partition": 3, "replicas": [2,1,3]},
    {"topic": "Topic", "partition": 4, "replicas": [1,2,3]},
    {"topic": "Topic", "partition": 5, "replicas": [2,1,3]},
    {"topic": "Topic", "partition": 6, "replicas": [3,1,2]},
    {"topic": "Topic", "partition": 7, "replicas": [1,2,3]},
    {"topic": "Topic", "partition": 8, "replicas": [2,1,3]},
    {"topic": "Topic", "partition": 9, "replicas": [3,1,2]}
  ],
  "version":1
}

그리고 아래 명령을 실행해서 섞인 Replica설정이 먹도록 만든다.

kafka-reassign-partitions.sh --reassignment-json-file manual_assignment.json --execute

물론 시간을 줄일려면 최초 단계에 Replicas 개수가 같은 replica 노드로 섞어주기만 하면 가장 좋다. 섞는 것만 한다면 작업 자체는 얼마 시간없이 바로 처리된다.

kafka-preferred-replica-election.sh --zookeeper localhost:2181 --path-to-json-file election.json

이제 리더를 각 클러스터의 여러 노드로 분산시키면 된다.  특정한 토픽만 지정해서 분할을 할려면 분할 대상을 아래 election.json 파일과 같은 형태로 작성해서 이를 위 명령의 파라미터로 넘긴다.  특정한게 아니라 전체 시스템의 모든 토픽, 파티션 수준에서 재조정을 할려면 별도 파일없이 실행시키면 된다.

{
 "partitions":
  [
    {"topic": "Topic", "partition": 0},
    {"topic": "Topic", "partition": 1},
    {"topic": "Topic", "partition": 2},
    {"topic": "Topic", "partition": 3},
    {"topic": "Topic", "partition": 4},
    {"topic": "Topic", "partition": 5},
    {"topic": "Topic", "partition": 6},
    {"topic": "Topic", "partition": 7},
    {"topic": "Topic", "partition": 8},
    {"topic": "Topic", "partition": 9}
  ]
}

이렇게 설정을 변경하면 Kafka 시스템이 라이브 환경에서 변경을 시작한다.  하지만 변경은 쉽지 않다는거… 좀 시간이 걸린다. 작업이 완료되지 않더라도 위의 설정이 제대로 반영됐는지 topic의 describe 명령으로 확인할 수 있다.

Replicas 항목들이 분산 노드들에 공평하게 배포되었고, 등장 순서에 따라 Leader가 적절하게 분산됐음을 확인한다.  만약 Replicas의 가장 앞선 노드 번호가 리더가 아닌 경우라면 아직 리더 역할을 할 노드가 제대로 실행되지 않았다는 것이다.  막 실행한 참이라면 logs/state-change.log 파일을 tail로 걸어두면 리더 변경이 발생했을 때를 확인할 수 있다.  어제까지 멀쩡했는데 오늘 갑자기 그런다면?? OMG 장비를 확인해보는게 좋을 듯 싶다. 🙂

변경이 마무리되는데까지는 좀 시간이 걸린다.  라이브 환경에는 특히 더할 수 있다. OLTP 작업들이 많다면 이런 튜닝 작업은 눈치 안보고 할 수 있는 시간에 스리슬적 해치우는게 좋다. 아니면 새벽의 한가한 때도??? -_-;;

Conclusion

이 과정을 거쳐서 정리를 마무리하면 대강 아래와 같은 최적화 과정이 절절한 그래프를 얻을 수 있다.

  • Section A – Leader 설정이 클러스터의 특정 노드에 집중된 상태
  • Section B – __consumer_offests 토픽의 replication 변경 및 leader 설정 변경
  • Section C – 설정 변경 적용 완료.

 

Kafka 어드민 도구

어드민 활동을 좀 더 쉽게 할 수 있는 도구로 LinkedIn 개발팀에서 공유한 도구들이 아래에 있다. 이 도구의 특징은 간단한 설치 방법과 함께 전반적인 설정들을 손쉽게 변경할 수 있는 방안을 제공해준다.

  • https://github.com/linkedin/kafka-tools
  • https://github.com/linkedin/kafka-tools/wiki

예를 들어 Topic의 데이터를 Auto commit 방식이 아닌 manual commit 방식을 사용하는 경우, __consumer_offsets 이라는 topic이 자동 생성된다.  못보던 놈이라고 놀라지 말자. 기본값이라면 아마도 이 토픽의 replication이 단일 노드(replication factor = 1)로만 설정되어 있다. 메시지 큐를 Kafka로 구현한 내 경우에 모든 토픽을 manual commit 방식으로 처리한다. 그런데 이 설정이면 __consumer_offsets 토픽의 리더 노드가 맛이 가면 커밋을 못하는 지경이 되버린다. 결국 전체 데이터 처리가 멈춰버린다.

이런 경우를 사전에 방지하기 위해 __consumer_offsets 토픽의 replicas 설정이 올바른지 꼭 확인해야 한다.  그리고 설정이 잘못됐다면 replication 설정을 반드시 수정해주자.  자책하지 말자.  Kafka의 기본 설정은 노트북에서도 돌려볼 수 있도록 안배가 되어 있다.  한번쯤 고생해봐야 쓰는 맛을 알거라는 개발자의 폭넓은 아량일 것이다.

이 어드민 도구는 아래 Replication creation script에서 보는 바와 같이 별도의 json 파일없이도 이를 실행할 수 있다.

kafka-assigner -z localhost:2181 -e set-replication-factor --topic __consumer_offsets --replication-factor 3

이 기능 이외에도 대부분이 토픽들을 한방에 정리할 수 있는 여러 기능들을 제공한다.  깃헙 Wiki 페이지에서 각 명령을 실행하는 방법을 확인할 수 있다.

Kafka administration summary

몇 가지 방법들을 썰로 좀 풀어봤지만, 기본적인 사항들을 정리해보면 아래와 같다.

Basic configuration

  • Cluster size = 3 – Cluster는 최소 3개 이상이어야 하며, 운영 노드의 개수는 홀수가 되도록 한다.
  • Partition size = 8 – 기본 Partition은 개별 노드의 CPU Core 개수 혹은 그 이상을 잡는다.
  • Replication factor = cluster size – Replication factor는 처리하는 데이터가 극적으로 작지 않는다면 Cluster 개수와 동일한 값이 되도록 한다.
  • JVM Option – 마지막까지 괴롭히는 놈이다. 현재 테스트중인 옵션
export KAFKA_HEAP_OPTS="-Xmx9g -Xms9g"
export KAFKA_JVM_PERFORMANCE_OPTS="-XX:MetaspaceSize=96m -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -XX:G1HeapRegionSize=16M -XX:MinMetaspaceFreeRatio=50 -XX:MaxMetaspaceFreeRatio=80"

Tuning

  • Partition의 개수를 늘린다. 토픽의 alter 명령을 사용해서 partition의 크기를 변경한다.
  • 토픽의 replication factor 확인하고, 각 partition의 리더가 전 클러스터에 균일하게 배분되었는지 확인한다.
  • 수동 commit 모드를 사용중이라면 __consumer_offsets 토픽의 partition, replication, leader 설정이 올바른지 확인한다.
  • 위에서 설명한 total.sh 등 명령으로 특정 topic 및 group의 consuming이 누적되지 않고 즉각적으로 처리되는지 확인한다.
  • Kafka의 garbage collection이 정상적으로 실행되고 있는지 확인한다. 데이터 적체가 발생되면 처리되던 데이터도 처리안된다.
  • 주기적으로 토픽의 leader 및 replication 설정이 적절한지 점검해준다.

대강의 경험을 종합해봤다.

앞으로는 잘 돌아야 하는데 말이다.

https://media1.giphy.com/media/10gHM7SWWMi8hi/200_s.gif

 

Reading for considerations

  • Java G1 GC – http://www.oracle.com/technetwork/articles/java/g1gc-1984535.html
  • Kafka optimization and configuration – http://docs.confluent.io/current/kafka/deployment.html

앞으로 설정할 때 이 부분을 좀 더 파봐야할 것 같다.

 

 

 

좋은 코드에 대한 개인적인 생각 – 2

개발자는 코드를 작성해야한다.  그리고 코드들이 엮이고 엮여 시스템이 만들어진다.  시스템은 필요를 요청한 사용자에게 기능을 제공한다.  물론 시스템을 구성하기 위해 필요한 노력을 개발자만 하는 건 아니다.  인프라 엔지니어는 장비와 네트워크를 준비하고, 데이터베이스 엔지니어는 데이터를 보관할 수 있는 저장소를 준비한다.  최근에는 Data Scientist가 데이터를 분석하고 빅데이터 도구를 통해 적절한 값들을 생성해낸다. 이외에도 다양한 노력들이 합쳐져 시스템이 만들어진다.  이것들을 아우리고 잘 버물려서 하나의 응집된 최종적인 기능으로 만들어내는 역할을 개발자가 담당한다.  그리고 코드는 이것들을 결합시키는 접착제이다.

코드는 여러 재료들을 잘 활용해 작성되야 한다.  어떤 재료의 쓰임이 과해지면 들인 것에 비해 제대로 된 시스템의 맛을 낼 수 없다.  음식을 하다보면 죽은 맛도 살려내는 것이 바로 MSG다.  라면 스프가 그렇고, 다시다가 그렇다.  기막힌 능력이다. 시스템을 만드는 과정에서도 MSG처럼 빠지지 않고 등장하는 재료가 있다. 바로 데이터베이스(DBMS)다.

데이터베이스란 뭔가? 간단히 이야기하면 데이터 저장소이고 시스템을 동작하는데 필요한 데이터를 저장하고 읽을 수 있는 기능을 제공한다. 데이터베이스 가운데 가장 개발자들이 선호도를 가진 것이 아마도 RDBMS이다.  가장 유명한 Oracle, MySQL, MS-SQL 등이 이 범주에 해당한다. 일반적인 경우라면 RDBMS를 빼고는 시스템을 이야기하기 어렵다.

그런데 왜 이걸 시스템 구축하는 MSG라고 이야기했을까?  데이터의 저장소일 뿐인데!  맞다, 모든 데이터베이스가 MSG 역할을 하는 건 물론 아니다. 개발자들이 주로 MSG로 사용하는건 RDBMS이다.  왜? 이유는….

RDBMS는 SQL이라는 언어를 사용해서 데이터를 처리한다.  SQL이라는 언어는 C++나 Java와 마찬가지로 뛰어난 프로그래밍 언어다. 특히 데이터에 직접 접근하면서 이를 우리가 원하는 다양한 조건 혹은 형태로 추출해낼 수 있다는 건 개발자에게 아주 좋은 매력이다. 와중에 언어 자체가 아주 어렵지 않기 때문에 손쉽게 배우고 써 볼 수 있다.

만약 CSV 파일 형태로 데이터가 있다고 가정하자.  만약 즉시 사용할 수 있는 RDBMS가 있다면 이걸 쓰는 것이 가장 빠른 방법이다. 아무리 빠른 시간에 코딩을 한다고 하더라도 Java로 비슷한 코드를 작성하는 건 SQL늘 작성하는 것보다 훨 많은 시간과 노력이 필요하다.  하지만 기본적인 전제는 RDBMS가 있어야 한다는 사실. 하지만 있다면 정말 손쉽게 결과를 이끌어낼 수 있다.

이것처럼 있다면 손쉽게 것도 나이스하게 결과를 만들어낼 수 있다는 관점에서 RDBMS는 개발자에게 MSG다.  빠른 것만이 아니다. MSG가 일류 쉐프의 음식맛에 뒤지지 않는 맛을 만들어내는 것처럼 RDBMS를 뒷배경으로 깔면 좋은 성능의 시스템이 툭 튀어나온다.  많은 데이터를 효과적으로 다루기 위해 멀티 쓰레딩을 구현할 필요도 없고, 데이터 참조등을 위해 Hash Table을 구성할 필요도 없다. 심지어는 굳이 머리를 굴려 알고리즘을 생각해낼 필요도 없다. 이 모든걸 데이터베이스가 처리해준다.  그래서 RDBMS를 사용하는 서비스의 성능의 개떡같다면 DB 튜닝을 하면 완전 새로운 물건이 되기도 한다.  인덱스 혹은 쿼리 힌트(Query hint) 한 스푼이면 죽어가던 시스템이 당장 마라톤 풀 코스라도 뛸 정도의 스테미나를 발휘한다.

데이터베이스, 좋아~

데이터베이스가 이런 물건이라면 개발자들이 더욱 더 많이 써줘야 하지 않을까?  구글링을 통해 찾아낸 아래 SQL을 살펴보자.

from: http://www.apex-at-work.com/2015/03/oracle-sql-calculate-amount-of-workdays.html

 

start_date와 end_date로 주어진 두 날짜 사이에 존재하는 일하는 날을 구하는 쿼리다. 뭐 억지로 발굴해낸 쿼리긴 하지만 SQL의 매력에 빠져드는 개발자라면 이 기능을 SQL 쿼리를 사용해 작성할 수 있다. 흠… 나쁘지 않을 것 같은데???

정~말~ 나쁘지 않다고 생각하면??? 아래 그림을 한번 살펴보고 한번 다시 생각해보는게 좋겠다.

이 그림이 주는 가장 큰 시사점은 뭘까? 바로 장비 대수이다. Application Server는 4대이고, 데이터베이스는 1대이다. 위 쿼리는 극단적인 예이지만, 데이터를 읽어들이기보다는 데이터를 이용한 연산, 심지어 내부적으로 분기문에 해당하는 CASE 구문까지를 사용한다.  이런 계산을 1대 밖에 없는 데이터베이스에 부탁한다면 어떨까?  시키는 일이니까 하긴 하겠지만 데이터베이스라는 친구는 열불이 나서 속이 터져나갈 지경이 될 것이다.  이에 반해 어플리케이션 서버는? 데이터베이스가 결과를 안주니 놀면서 유유자적한 시간을 보낼 수 밖에 없다.

이런 식의 안이한 혹은 편리한 시스템 개발은 결국 병목을 만들어낼 수 밖에 없다. 물론 잘 설계된 DB 구조와 적절한 “어플리케이션 – 데이터 계층”간 역할 분담이 정의된다면 이런 일은 크게 발생하지 않는다.  하지만 데이터는 시스템의 동작하는 가장 바탕이 되고, 이를 가공처리하는 단계 단계를 통해 우리가 제공하는 서비스가 완성된다. 그 과정에서 MSG같은 RDBMS가 있다. 시간에 쫓기는 개발 세상에서 쉽지 않은 유혹이다.

그럼에도 불구하고 우리는 유혹에 맞써야 한다.  어플리케이션 서버가 HTTP/TCP Connection을 받아서 Business Logic에 해당하는 SQL 문장을 DBMS에 던지는 역할만 한다면 이름에서 “어플리케이션” 이라는 단어를 빼야한다.  그렇기 때문에 처리에 필요한 데이터를 불러들여 어플리케이션에서 실행해야할 로직을 합당하게 실행해야 한다.  이 과정에서 DBMS는 효과적인 데이터 관리를, 어플리케이션 서버는 합당한 로직의 완성을 위한 계산과 판단을 수행해야한다.

다른 관점에서 시스템은 횡적 확장(Scale out)이 가능하게 설계되고 개발되야한다. 시작은 미미했으나 그 끝이 창대할려면 말이다. 하지만 RDBMS에 의존한 시스템은 명확한 한계를 갖는다. RDBMS는 높은 성능을 발휘하기 위해서는 Scale Out 방식의 확장 모델보다는 Scale Up 방식이어야 하기 때문이다.. 왜? 당연히 데이터를 다루는 작업을 해야하니까! 어떻게든 많은 데이터를 한꺼번에 처리할려면 많은 메모리가 필요고 또한 인메모리 데이터를 빠르게 소모시킬려면 많은 CPU(Core)가 필요한건 당연한 이야기니까. 오라클의 RAC를 이야기할지 모르겠지만, 어찌됐든 오라클도 개별 장비는 좋아야 한다는 사실에는 변함이 없다.

데이터베이스, 다시 한번 생각해보자!

좀 더 높은 관점에서 우리가 데이터베이스를 어떻게 사용하는지 살펴보자. 시스템은 혼자서는 존재할 수 없으며, 많은 경우에 다른 서비스 시스템과 이야기를 주고 받아야 한다. 서비스간의 Collaboration을 통해 궁극적으로 사용자가 원하는 기능에 도달할 수 있다. 서비스간의 소통을 위해 데이터의 교환은 필수적이다. DBMS는 그런 관점에서 아주 요긴한 도구가 될 수 있다.

아래 그림에서 알 수 있는 바와 같이 서비스들이 하나의 데이터베이스를 공유한다면 RESTful과 같은 번잡한 프로토콜을 통해 데이터를 주고 받는 것을 최소화할 수 있다. 뭐하러 그 많은 데이터를 서로 주고받는가? 데이터가 존재하는 테이블의 스키마를 안다면 기초적인 정보를 가지고도 충분히 원하는 데이터를 획득할 수 있는데… 오직 필요한 건 DBMS에 연결만 하면 된다!

하지만 여기에서도 마찬가지로 성장의 딜레마에 빠진다.  더 많은 서비스들이 하나의 물리적인 데이터베이스로 매개화된다면 결국 이 부분이 병목 구간이 된다.  병목 구간일 뿐만 아니라 치명적인 아킬레스 건이 될 수도 있다.

  • 한 서비스에 공유된 데이터베이스에 과도한 데이터 처리를 유발한다고 가정해보자. 이 부하는 그 서비스만의 문제가 아니다. 궁극적으로는 연결된 모든 서비스들에 영향을 준다. 부하를 유발한 서비스의 입장에서야 자신의 기준에 부합한다고 생각하지만 다른 서비스들은 뭔 죄가 있겠는가? 이 상황에 대한 당장의 타계책은 결국 DBMS를 확장하는 방법 이다. 다른 대안은 궁색하다.
  • 보안등의 이슈로 DBMS 엔진을 업데이트 해야하는 경우가 있다고 하자. 통상적으로 엔진의 업데이트는 데이터베이스 접근을 위한 클라이언트 모듈의 업데이트를 동반한다. 뭔말이냐하면 엔진을 업데이트하고 정상적은 데이터베이스 연결을 위해서는 모든 서비스들에서 동작되는 모든 어플리케이션의 일괄 패치를 의미한다. 국내에서 이런 일괄 패치를 진행하는 시점은 추석이나 설날 같은 명절날이다.  개발자들이 종종 명절임에도 불구하고 고향에 내려가지 못하는 불상사가 발생한다.

데이터베이스에 대한 과도한 의존은 개발자 자신의 문제일 뿐만 아니라 조직 전체에 영향을 줄 수 있는 파급력을 갖는다. 그렇기 때문에 데이터베이스를 대하는 올바른 자세가 개발자들에게 더욱 더 요구된다고 볼 수 있다.

그래서 우리는…

이제 정리를 해볼려고 한다. 먼저 언급할 부분은 “개발자의 관점에서 DB를 어떻게 바라볼 것인가?” 라는 점이다.

RDBMS든 뭐든 데이터베이스는 데이터의 저장소이다. 물론 MSG 역할을 하는 좋은 데이터베이스라면 더욱 좋을 것이다. 하지만 우리가 생각해야할 점은 “데이터를 저장”하는 역할이 데이터베이스라는 사실이다. 그 이상을 바란다면 당신은 개발자가 아니라 DBA가 되야 맞다. 데이터라는 관점에서 RDBMS에 대해 다음의 사항을 고려하면 좋겠다.

  1. 어떤 데이터를 저장할 것인가? 경우에 따라 굳이 데이터베이스에 저장할 필요가 없는 데이터임에도 불구하고 이를 저장해두는 경우가 있다. 예를 들어 1년에 한번 특정한 이벤트에 맞춰 변경하는 1~2k 건수의 데이터들이 있다고 하자. 이 데이터를 굳이 데이터베이스에 저장할 필요가 있을까? 가장 빠른 데이터의 저장소는 어플리케이션의 메모리 영역이다. 그럼 당연히 메모리에 이 데이터를 상주시키는게 맞고, 이를 위한 코드를 작성하면 된다.
  2. 얼마나 간편하게 저장하고 데이터를 로딩할 수 있는가? 데이터는 데이터 자체로 접근하는게 맞다. 앞서 언급한 경우와 마찬가지로 복잡한 Biz Logic을 뒤섞은 SQL 쿼리로 데이터를 로딩한다고 보자.  개발자인 당신이 그 로직을 테스트 케이스를 통해 신뢰성이 있다는 것을 보장할 수 있을까? 당연히 없다. 가장 간단한 형태로 로딩하고 로딩된 결과를 Biz Logic으로 테스트 가능하도록 만들자.  이를 위한 방안으로 JPA와 같은 여러 프레임웍들이 이미 존재한다. 찾아보면 각 언어별로 다 나온다.
  3. iBatis/MyBatis를 사용하더라도 쿼리가 한 페이지를 넘는다면 이미 상당히 꾸리하다라는 증거다. 절대 쿼리는 1페이지 이내에서 결론을 내야한다. 그 이상 넘어가는 쿼리는 이해하기도 힘들뿐만 아니라 읽기도 힘겹다. SQL도 프로그래밍 언어다. 여기에서도 클린 코드를 실현해야하지 않을까?
  4. 마지막으로 원론적인 이야기지만 데이터를 무조건 RDBMS에 저장할려고 하지 마라. 아는게 무서운거라고 알기 때문에 RDBMS에 무조건 넣을려는 경향이 있다. 다양한 통계를 만들겠다고 웹 엑세스 로그를 RDBMS에 넣고 돌리는 행위가 과연 맞는 짓일까?  Transactional Queue를 만들겠다고 그 데이터를 RDBMS에 넣고 넣었다가 뺏다하는 짓이 과연 맞는 짓일까? MongoDB, Elasticsearch, DynamoDB 혹은 심지어 Hadoop도 사용할 수 있다.

 

마이크로서비스 아키텍쳐에서 DBMS의 역할

개발자의 관점뿐만 아니라 이제 데이터베이스의 관점을 아키텍쳐의 관점에서도 새롭게 봐야할 시점이다. 특히나 마이크로서비스 아키텍쳐 환경에서는 더욱 더 데이터베이스의 역할을 어떻게 정의할지를 좀 더 깊게 생각해야 한다.

마이크로서비스 아키텍쳐는 세분화된 서비스가 독립적인 형태가 되는 것을 지향한다.  개별 서비스는 자신과 관련된 자원을 독립적으로 운영하고, 자원에 대한 다른 서비스에 대한 의존성을 없애야 한다.  이유는 간단하다. 그래야만 단위 서비스가 보다 빠르게 움직일 수 있기 때문이다.

앞서 설명한 예시와 같이 서비스간 데이터베이스와 같은 자원이 공유되는 상황에서는 자원에 의한 의존성 문제로 원하는 시점에 원하는 형태로 서비스를 개발하기 어렵다. 특히나 정서상 이미 있는걸 쓰라고 강요하는 경우가 비일비재하다. 이런 간섭이 존재하는 상황에서는 자유로운 데이터 저장소로써의 데이터베이스의 사용이 어렵다.

독립적인 서비스의 개발/배포/운영이 이뤄지는 상황이라면 각 상황에 적합한 데이터 저장소를 활용할 필요가 있다. 마이크로서비스 세상은 이를 권장할 뿐만 아니라 개발자로써 환경에 적합한 물건을 사용해야만 한다. 그래야 최적의 성능을 낼 수 있을 뿐만 아니라 서비스간 유기적인 협업이 가능하니까.

 

개발자가 거버넌스 혹은 좋은게 좋은거니까에 익숙해지면 개발자가 정말 해야할 개발 혹은 코딩을 잊게 된다. 좋은 도구는 활용하면 최선이겠지만, 그에 앞서 자신의 발전을 위해 놓지 말아야 하는 “개념”에 대해서는 항상 생각해보는게 좋겠다.

– 끝 –

 

ps. 글의 처음을 쓴지는 반년이 넘은 것 같은데 완전하지 않지만 가락이 대강 갖춰진 것 같다. 말이 말은 만드는 것 같기도 하고 원래 내가 하고 싶은 말이 이 말이었나 싶기도 하다. 코딩하는 것과 마찬가지로 글도 생각이 있을 때 마무리짓는게 최선인 것 같다.