Nov 25, 2020 - kafka 를 현재 시스템에 적용한 이유

기술면접 중, 현재 시스템에 kafka 를 적용한 이유에 대해서 물어보셨다.
내부적인 문제를 조리있게 설명할 자신이 없었는데,(실수할까봐;)
그냥 넘어가주셔서, 다음에는 조리있게 설명하기 위해 해당 내용을 정리합니다.

기존 시스템

기존 레거시 시스템은 JAVA의 Bean 객체에 데이터를 담는 것입니다.
어플리케이션에 데이터를 담고, 해당 데이터에 Spring Job Scheduler 을 통해, 10초 or 20초 간격마다,
데이터를 insert 하는 로직이었습니다.

스케일업과 스케일아웃

영업과 운영이 확장되면서 기존 시스템으로 버거운 상황이 생기기 시작했습니다.
단순하게,
스케일 아웃이란 서버를 여러 대 추가하여 시스템을 확장하는 방법이며,
스케일 업은 서버에 CPU나 RAM 등을 추가하여 고성능 부품, 서버로 교환하는 방식이었습니다.

서비스가 고도화 됨에 따라, 스케일업과 스케일아웃이 동시다발적으로 진행되었고,
그로인해 기존 덤프로직으로는 점점 버티기 힘들게 되었습니다.

장애 상황

첫번째 장애의 경우 Major GC로 어플리케이션이 순간 지연이 발생하는 것이었습니다.
유입량의 증가로 인해, 10초 와 20 초 정도 담는 Bean 객체의 Heap 영역이 점점 늘어났습니다.
해당 데이터를 Major GC를 유발하였고, “Java GC Stop The World time” 이 발생이 되었습니다.
(etc. 이때 해당 문제를 해결하기 위해 단순 패러랠 GC에서 CMS 75%를 적용하게 됨)

두번째 장애는 유실률 증가입니다.
덤프의 경우, Bean 에 담긴 객체를 temp 객체에 담게, 다시금 해당 Bean 을 초기화하는 작업을 반복했습니다. 이 경우,
초기화 하는 그 순간의 유입되는 데이터가 삭제되는 문제가 생길 수 밖에 없었습니다.
해당 기능을 해결하기 위해, ConcurrentHashMap 를 적용하거나 구아바(Guava) 등을 고려했으나,
순간 유실되더라도 ad tech 는 Thread-Safe 를 보장하지 않더라도 속도를 해결해야하는 숙명을 고려해야했습니다.

세번째는 db lock 상황이 증가되는 이슈였습니다.
스케일 아웃이 발생하면서, 동일 DB에 동일 row에 insert 하는 현상이 빈번하게 발생했습니다.
이 이슈는 위의 2가지 이슈보다 더 큰 장애상황을 야기했습니다.
dead lock 이 걸리는 순간 해당 lock 상황을 DBA에 프로세스 킬을 하지 않는한, 서비스를 잠시 중단해야하는 경우가 생긴 적도 있을 정도로 크리티컬한 상황을 예상할 수 밖에 없었습니다.
(feat. 긴급하게, master 와 slave 구조를 만들어, select 는 slave로 하여 dead lock 상황이 발생하더라도 서비스는 안정적으로 운영가능하게 작업. )

메시지 큐 적용을 고려

기존 덤프로직들을 jvm 의 Bean 객체가 아닌, 다른 곳에 저장 후 처리방식에 대해 논의가 있었습니다.

1차적으로 몽고 시스템을 적용했으나, MongoDB 영속성 (Persistence) 문제가 있었습니다.
Bean 에 담는 객체보다는 데이터 유실률은 좋았으나, 중요 순간 순간, 데이터 검증에 실패하게 되어, 메시지 큐에 대해서 고민하게 되었습니다.

CTO 님은 스파크를 사용하고 싶었고, 휼룡한 개발자분들은 Nosql 쪽보다는 메시지 큐 방식을 더 선호하였습니다.

1 차적으로 몽고를 테스트 했던 이유는 단순히 현재 몽고가 운영되고 있었기 때문이었으니까요! 모든 것이 도전인 상황이었습니다.

좋은 개발자분들이 해당 상황은 메시지 큐 방식이 정당하다는 것을 몽고를 1차적으로 테스트성으로 적용하여, 어설프게나마 증명을 했습니다.
남은 것은 좋은 큐를 선택하는 것이 었습니다.

Kafka, RabbitMQ, Active MQ 중 카프카를 선택한 이유

카프카가 유명해서?
보편적으로 레빗보다는 카프카를 운영에서 많이 쓴다고 하지요.

두개의 장단점은 다음과 같습니다.
RabbitMQ 는 broker가 워커(kafka의 컨슈머)에서 메시지를 push 하는 방식이고,
kafka는 컨슈머가 broker 로부터 메시지를 pull 하는 방식입니다.
(feat. 카프카의 경우도 RabbitMQ처럼 메시지를 push 하는 방법이 있다고 하셨는데, 그 부분은 조금더 문서를 찾아봐야겠습니다. 제가 모르는 부분이었습니다!)

RabbitMQ 아키텍처

범용 메시지 브로커 — 다양한 요청 / 응답, 지점 간 및 게시-구독 통신 패턴을 사용합니다.
스마트 브로커 / 단순한 소비자 모델 — 브로커가 소비자 상태를 모니터링하는 것과 거의 같은 속도로 소비자에게 일관된 메시지 전달.
성숙한 플랫폼 — 잘 지원되며 Java, 클라이언트 라이브러리, .NET, Ruby, node.js에서 사용할 수 있습니다. 수십 개의 플러그인을 제공합니다.
통신 — 동기식 또는 비동기식 일 수 있습니다.
배포 시나리오 — 분산 배포 시나리오를 제공합니다.
다중 노드 클러스터 - 클러스터 연합 — 외부 서비스에 의존하지 않지만 특정 클러스터 형성 플러그인은 DNS, API, Consul 등을 사용할 수 있습니다.

Apache Kafka 아키텍처

대용량 발행-구독 메시지 및 스트림 플랫폼 — 내구성, 속도 및 확장 성.
내구성있는 메시지 저장소 — 로그와 같이 서버 클러스터에서 실행되며 주제 (범주)의 레코드 스트림을 유지합니다.
메시지 — 값, 키 및 타임 스탬프로 구성됩니다.
Dumb Broker / 스마트 소비자 모델 — 소비자 가 읽은 메시지를 추적하지 않고 읽지 않은 메시지 만 보관합니다. Kafka는 일정 기간 동안 모든 메시지를 보관합니다.
실행하려면 외부 서비스가 필요합니다 . 일부 경우 Apache Zookeeper.

실질적으로 저희 서버의 경우, 초당 처리되야하는 데이터가 엄청 많아야 하고, Consumer가 처리할 수 있을 때 메시지를 가져오므로 자원을 효율적으로 사용 가능하고, 항시적으로 데이터를 가지고 있으니까, 결과적으로 카프카를 적용하게 되었습니다.

물론, 카프카도 문제점이 있습니다.
pull 방식이라서 생기는 문제로, 데이터가 없음에도 정기적인 polling으로 인해 자원을 낭비하는 문제입니다.
컨슈머 서버가 계속 while(true) 로 기다리고 있는거지요.

이러한 단점을 보완하기 위해 실제 데이터가 도착할 때까지 long poll 대기를 할 수 있는 parameter를 지원하기도 합니다.
이번 프로젝트의 카프카 리스너에는 해당값을 적용햇습니다.

그 이후…

제가 그 후 추가하는 모든 서비스는 새로운 서비스를 구축하기보다, 기존 서비스를 사용하는 경우가 많았고, 완벽하게 별도로 구축한 카프카 서버는, 단순 토픽만을 저장하는 용도로 사용하여!!!

신규 서버 구축보다는 기존 카프카를 이용하는 형태로 운영중입니다!! (다만, 제가 구성할때, 주키퍼 서버 3대를 구축했는데 사용 CPU가 1~3%만 사용하고 있어, 해당 서버를 별도로 이용하는 방법을 계속 고민중입니다. )

결론

  • 하나의 토픽으로 다중 consumer이 받아갈수 있는 구조
  • 대량 지연 실시간 처리가능 및 데이터 공유

추가적으로 나중에 그룹으로 별도의 동일 데이터를 가져가 처리할 수 있는 기능이 필요하게 되었을 때,
정말 카프카를 유용하게 사용했습니다!
다른 메시지 큐보다 지금 운영하는 시스템에 딱 맞었다고 봅니다.

참고 주소 : https://dhsim86.github.io/web/2017/04/03/Apache_kafka-post.html
https://dhsim86.github.io/web/2017/03/22/spring_boot_features_06-post.html

Nov 25, 2020 - iptables status

iptables 을 이용해서, L4에 접근하지 않고 서비스를 재배포하며 운영할 수 있는 구조로 운영하고 있었습니다.

이번에 CentOS 6.9 -> 7.0 으로 변경되면서,

firewall 로 바뀌는 것으로 알고 있어, 기능 적용하려고 하는데,

[XXX-01:root]/home/ssp/log/log4j/script>#firewall-cmd --state
not running

firewall 이 꺼져있는것을 확인했습니다. 시스템팀에게 확인 결과, 파이어월을 키면 완전 차단(?) 되어서,
기능을 꺼두었다고 합니다.

실제 IPS 와 웹방화벽 등이 그 역활을 하고 있어,
실질적으로 서버내의 firewall은 큰 의미가 없다고 하여,

적용하려는 이유를 설명하여, 요청했습니다.

L4 차단 : 
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address=L4_IP reject' 
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address=L4_IP drop' 

L4 다시 허용 : 
sudo firewall-cmd --permanent --remove-rich-rule='rule family="ipv4" source address=L4_IP reject '
sudo firewall-cmd --permanent --remove-rich-rule='rule family="ipv4" source address=L4_IP drop' 

위와 같은 용도를 사용하고 싶다고 하니,
서비스팀에서, 그럼 iptables 로 변경해주겠다고 하여, 어제 작업이 완료되었습니다.

아무리 테스트를 해도 안되어,

[XXX-02:root]/root/shell>#service iptables status
Redirecting to /bin/systemctl status iptables.service
● iptables.service - IPv4 firewall with iptables
   Loaded: loaded (/usr/lib/systemd/system/iptables.service; disabled; vendor preset: disabled)
   Active: inactive (dead)

….. service iptables status를 하니, inactive (dead) 였네요.

혹시 모르니, 시스템팀에게, 해당 기능을 off 된 이유를 문의한 후 on 처리 했습니다!

[XXX-02:root]/home/ssp/log/access>#service iptables status
Redirecting to /bin/systemctl status iptables.service
● iptables.service - IPv4 firewall with iptables
   Loaded: loaded (/usr/lib/systemd/system/iptables.service; disabled; vendor preset: disabled)
   Active: active (exited) since 수 2020-11-25 11:16:37 KST; 1h 9min ago
  Process: 25898 ExecStart=/usr/libexec/iptables/iptables.init start (code=exited, status=0/SUCCESS)
 Main PID: 25898 (code=exited, status=0/SUCCESS)

11월 25 11:16:37 XXX-02 systemd[1]: Starting IPv4 firewall with iptables...
11월 25 11:16:37 XXX-02 iptables.init[25898]: iptables: Applying firewall ...]
11월 25 11:16:37 XXX-02 systemd[1]: Started IPv4 firewall with iptables.
Hint: Some lines were ellipsized, use -l to show in full.

Nov 23, 2020 - Alias 만들기_2

예전, Alias 만들기라는 글을 적은 적이 있습니다.
오래되서 다시한번 정리합니다.

```shell script [XXXX-01:root]/home/ssp/log/log4j/script>#vi /home/root/.bashrc [XXXX-01:root]/home/ssp/log/log4j/script>#cd /home/ro -bash: cd: /home/ro: 그런 파일이나 디렉터리가 없습니다 [XXXX-01:root]/home/ssp/log/log4j/script>#vi ~/.bashrc


vi ~/.bashrc

alias rm=’rm -i’ alias cp=’cp -i’ alias mv=’mv -i’ alias vi=vim

alias elog=’tail -f /home/XXXX/logs/log4j/XXXX.errorlog.log’ alias dlog=’tail -f /home/XXXX/logs/log4j/XXXX.debuglog.log’ alias ilog=’tail -f /home/XXXX/logs/log4j/XXXX.infolog.log’ alias tlog=’tail -f /usr/local/tomcat/logs/catalina.out’ alias tstart=’/usr/local/tomcat/bin/startup.sh &’ alias tstop=’/usr/local/tomcat/bin/shutdown.sh’ alias data=’cd /home/XXXX/rfdata/DATA’

alias prop-reload=’curl -v http://127.0.0.1/servlet/monitor?mode=reloadConf’ alias recom-ilog=’ilog | grep -vi dump | grep -vi kafka | grep -vi rtbrdservice | grep -vi pushthread | grep -vi “덤프” ‘ alias recom-elog=’elog | grep -vi “campaignlist null” | grep -vi “lastmomentexception” | grep -vi “uuid is not found” ‘

Source global definitions

if [ -f /etc/bashrc ]; then . /etc/bashrc fi ~
```

자주 사용하는 명령어들은 약어를 사용하여 편하게 사용합니다.
저의 경우, 톰캣 재시작을 유용하게 사용합니다.

다만 중요한 명령어의 경우에는 alias 를 등록하지 않고 집적 호출하도록 유도하거나 강제하는 편입니다.

작업완료 후, source ~/.bashrc 적용합니다.