adtech_so
154
2019-02-18 22:51:30 작성 2019-02-20 13:03:27 수정됨
0
1060

거대한 기술적 부채를 해결하기 위해서 걸어온 반년간의 이야기 (2)


2. 발신 서버(Java)


> 로그 스키마 설계와 반영


배속 초기부터 발신 서버는 어느정도 기능 추가나 유지보수를 해왔던터라 그나마 접근하기 쉽다고 생각했지만 만만치 않았습니다. 머리속으로만 생각하기보다는 손을 움직여서 클래스 구조를 그려보고 간단하게 클래스의 역할을 메모하면서 순서도를 차근차근 그려보는 편이 이해 및 기억 측면에서 훨씬 효과가 있었다고 생각합니다. 요새는 markdown으로도 구조도 형식을 간단하게 그릴 수 있기때문에 추천드립니다.

- http://plantuml.com/ko


새로운 로그를 생성하는 과정이 필요했기 때문에 필히 로그 스키마에 대한 리뷰도 지나칠 수 없는 관문이었습니다. 스키마를 정의할 때 아래의 항목을 토대로 최대한 알기 쉽게 기재했습니다.

- 요소명

- 데이터 타입

- 설명

- 예시값


로그 스키마에 대한 리뷰 후에 내용을 코드로 적용하기 위해서 발신 서버에서 사용하고 있는 로그 라이브러리인 https://github.com/qos-ch/logback 의 설정 파일(logback.xml)에 새로운 로그에 대한 설정을 추가하여 완성시켰습니다. 


<configuration>
  ...

  <appender name="Log Naming" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.core.filter.EvaluatorFilter"> <evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator"> <marker>ANALYSIS</marker> </evaluator> <onMatch>NEUTRAL</onMatch> <onMismatch>DENY</onMismatch> </filter> <filter class="ch.qos.logback.core.filter.EvaluatorFilter"> <evaluator> <expression>return message.contains(",\"log_type\":\"로그 타입\",");</expression> </evaluator> <OnMatch>ACCEPT</OnMatch> <OnMismatch>DENY</OnMismatch> </filter> <Prudent>true</Prudent> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>/path/to/log/file_%d{yyyy-MM-dd-HH}.log</FileNamePattern> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <Pattern>%msg%n</Pattern> </encoder> </appender> ... <root> <level value="INFO"/> ... <appender-ref ref="Log Naming"/> ... </root> </configuration>


하지만, 로그를 전송하기 위해서는 인프라 코드를 건들어야만 했습니다.


> 서버 프로비저닝으로서의 코드


발신 서버의 인프라는 https://github.com/chef/chef 라는 ruby로 만들어진 프로비저닝 도구를 사용하고 있었습니다. 명칭이 “chef” 답게 요리사에 관련된 명령어들이 존재했고, 아래와 같은 특징들이 있었습니다.

- docker와 연동하여 서버 구축에 대한 테스트를 로컬에서 가능

- knife 명령을 통해 특정 노드그룹에 대한 정보를 획득

- 인프라 코드에 대한 수정도 github 상에서 리뷰가 가능

- ruby로 테스트 코드 작성 가능


다른 로그들도 비즈니스쪽에서의 조사를 목적으로 fluentd를 이용하여 Big Query로 streaming insert를 하고 있었기 때문에 비슷하게 databag와 environment에 상수를 설정하고 td-agent 디렉토리 밑에 로그 전송에 관한 설정을 아래와 같이 정의했습니다.


<source>
  type             // 파일을 읽는 방법 정의
  tag              // 아래의 match에서 filter할 별명
  path             // 파일의 위치
  pos_file         // 프로세스 파일의 위치
  format           // 파일의 확장자 (ex. json)
  time_key         // 파일에서 시간을 나타내는 요소
  time_format      // 시간 포멧 (ex. %Y-%m-%d %H:%M:%s)
  refresh_interval // 파일을 검사하는 간격
</source>
<match // 위의 tag에 설정한 별명을 입력 >
  type               // 외부 저장소 타입
  method             // 행동 설정
  dataset            // table의 부모
  auto_create_table  // table이 존재하지 않을 경우 자동 생성
  table              // dataset의 자식 (날짜별로 만들어짐) 
  schema_path        // table의 스키마 정의
  time_field         // table에서 시간을 나타내는 요소
  time_format        // 시간 포멧 (ex. %Y-%m-%d %H:%M:%s)
</match>


위의 파일을 "/etc/td-agent"에 저장해놓고 전체 설정 관리 파일인 "/etc/td-agent/td-agent.conf"에 위 파일의 경로를 설정하면 되는 작업이었습니다. 프로비저닝은 발신 서버의 배포가 이루어질때 서버로의 트래픽이 멈추고 어플리케이션이 정지한 후에 "sudo chef-client" 명령을 통해 새로운 버전이 반영되는 느낌이었습니다.

- https://github.com/fluent/fluentd (발신 서버는 버전 2.0대를 이용하고 있었지만 현재는 3.0이 나왔으며 C언어로 구현된 fluent-bit도 등장)

- https://github.com/fluent/fluent-bit (cloud native 시대에 걸맞는 성능을 보여주는 녀석이지만 관련 plugin이 적으므로 주의)


> 스케줄링 도구


실질적인 데이터 집계에 필요한 로그를 cloud storage에 저장하는 처리가 필요했습니다. 데이터 집계의 단위가 1시간이었던 저희 서비스는 1분 단위로 생성되어있는 로그를 취합하여 gcloud 명령을 통해 전송하고 있었습니다. 다소 legacy한 방식을 택하고 있었지만 서비스의 역사가 깊은 만큼 어쩔 수 없는 부분이라고 생각했지만서도... (물론, 지금은 실시간 데이터 집계를 위해서 아키텍처를 새롭게 고안하고 있는 중) rundeck이라는 스케줄링 도구를 이용해서 정기적으로 각각의 로그들을 전송하고 있었기 때문에 rundeck에 대한 이해가 필요했습니다.

- https://github.com/rundeck/rundeck


- defaultTab: summary
  description: // 작업 설명
  executionEnabled: // 실행 활성화 유무
  id: // 자동 생성 ID
  loglevel: // 로그 래밸
  loglimit:  // 로그 제한 용량
  loglimitAction:  // 로그가 제한 용량을 초과했을 때의 거동
  loglimitStatus:  // 작업이 중지되었을 때의 거동
  multipleExecutions:  // 병렬 실행 유무
  name:  // 작업명
  nodeFilterEditable:  // node filter 기능 사용 유무
  nodefilters:
    dispatch:
      excludePrecedence:  // 우선 순위 전략
      keepgoing:  // 특정 노드가 실패했을 때의 거동
      rankOrder:  // 노드 실행 전략
      successOnEmptyNodeFilter:  //
      threadcount:  // 병렬 실행 노드 수
    filter:  // 노드를 필터 (정규표현식 사용가능)
  nodesSelectedByDefault:  //
  notification:  // 통지
    onfailure:  // 실패할 경우
      plugin:
        configuration:
          webhook_url:  // 웹 훅 URL
        type:  // 웹 훅의 타입
  notifyAvgDurationThreshold:  // 알림을 통지할 간격
  retry:  // 재시도 횟수
  schedule:  // 스케줄 시간
    dayofmonth:
      day:
    month:
    time:
      hour:
      minute:
      seconds:
    year:
  scheduleEnabled:  // 스케줄 활성화 유무
  sequence:
    commands:  // 실행할 명령 기입 (shell script 가능)
    - 실행 명령1
    - 실행 명령2
    keepgoing:  // 특정 명령이 실패했을 때의 거동
    strategy:  // 명령들을 실행하는 전략
  timeout:  // 타임아웃 설정
  uuid:  // 자동 생성 ID


배포에서는 chef를 service discovery 도구로서 사용하고 있었지만 rundeck에서는 consul을 사용하고 있었습니다. (같은 역할을 가진 도구는 한 가지만 이용하도록 합시다...) rundeck에서는 consul의 REST API를 호출하여 특정 tag로 그룹화되어 있는 서버 목록을 받아와서 명령을 실행할 노드를 필터하는 이미지였습니다.


curl http://${CONSUL_HOST}:8500/v1/health/service/${SERVICE}?passing | grep -o -E '{"Node":{"Node[^,]+","Address[^,]+"' | awk '{gsub( "\"", "", $0 );print $0}'


어느정도 작업은 마무리가 되었지만 데이터 집계쪽이 핵심이었기 때문에 DEV와 STG에 필요한 샘플 데이터를 정기적으로 자동 생성시킬 필요가 있었습니다.


> 결합 테스트를 위한 샘플 데이터 확보


데이터의 흐름

- 리퀘스트

- 발신 서버의 처리

- 로그 생성

- 로그 저장

- 데이터 집계

- 데이터 집계의 결과물 저장

- 데이터 베이스에 격납

- 관리화면에서 데이터 베이스의 정보를 참조


위와 같은 큰 데이터의 흐름이 있었기 때문에 개발 환경에서도 "결합 테스트"는 꼭 필요했습니다. (솔직히 이 부분을 좀 간과했는데 상당히 중요한 부분) 저희 서비스의 경우, 응답시간이 대략 100ms 내외로 되어야 하기때문에 조그마한 배포 실수가 큰 문제로 이어질 수 있는 구조였습니다. 그래서 사전 리허설같은 느낌으로 데이터의 흐름이 끊기지 않는지 확인할 필요가 있었습니다. 하지만, 정기적으로 트래픽을 생성하는 코드는 무려 4년전에 commit이 끊긴 shell script 였고, 아무개씨 스크립트라는 명칭이 붙여져 있는 아주 무시무시한(?) 녀석었습니다. 집념을 가지고 리팩토링을 거쳐 새로운 로그에 대한 트래픽처리까지 구현했더니 gitub의 conversation이 80건을 넘어가고 있었습니다. (이 기회를 빌어 리뷰를 꼼꼼하게 해주신 선임분께 감사하다는 인사를)


아래는 자잘하게 습득한 shell technique

- 파라미터에 대한 유효성 검사는 초반부에 하도록 한다.

if [ $# -ne 3 ]; then
  usage
end

- 스크립트의 사용법은 꼭 작성한다.

usage() {
  cat << EOF
  Usage:
    ...
EOF
}

- 스크립트를 실패시키고 싶을 때에는 exit 1을 기재한다.

- 상수에는 되도록 readonly 예약어를 사용한다.

- 명령어는 ``가 아니라 $()로 감싸도록 한다.

- 특정 명령에 대한 결과값은 $?로 취득해서 if 조건으로 판단하도록 한다.

- JSON 응답의 경우에는 되도록 jq command를 사용한다.

- 문자열을 대입하고 싶을 때에는 sed command를 사용한다.

sed 's/${site}/okky/g'

- 명령어가 길어질 경우 \를 사용하여 가독성을 높인다.


개발 환경에서의 리허설은 딱히 문제가 없었고 롤백에 대한 메뉴얼(매우 중요)도 구비해놓은 상태였기때문에 용기를 내서 배포를 진행했고 눈에 띄는 문제 없이 발신 서버라는 산을 넘어섰습니다. 이제 다음 단계인 데이터 집계로...

1
0
  • 댓글 0

  • 로그인을 하시면 댓글을 등록할 수 있습니다.