LINE Corporation이 2023년 10월 1일부로 LY Corporation이 되었습니다. LY Corporation의 새로운 기술 블로그를 소개합니다. LY Corporation Tech Blog

Blog


LINE의 OpenJDK 적용기: 호환성 확인부터 주의 사항까지

2018년 오라클의 라이선스 체계가 변경되면서 2019년 1월 이후 더 이상 무료로 Oracle JDK를 사용할 수 없게 되었습니다. 이에 LINE 내부에서는 사전에 OpenJDK를 적용하기 위해 필요한 사항이나 검토 항목을 정리하기 위하여 TF(task force)를 구성하였고, 진행된 내용과 사용된 기술적인 사항들을 정리하고자 이 글을 작성하게 되었습니다. 

시작하기 전에


본격적인 이야기를 시작하기 전에 OpenJDK를 검토하면서 느낀 부분이 있어 옛날 옛적 이야기를 적어봅니다. 2000년 초반만 해도 OSS(Open Source Software)의 신뢰도에 대한 평가는 극과 극이었고, 공공 기관이나 큰 규모의 사업장에서 실제 OSS를 적용하기에는 쉽지 않은 시기였습니다. 예를 들어 Apache HTTPD Server(이하 Apache 웹 서버)의 Prefork MPM(Multi-Processing Modules)의 메모리 누수 문제와 당시 신형 MPM이였던 worker의 불안정성때문에 공공이나 상용 사이트에서는 대부분 Iplanet 웹 서버를 사용하였고, 부끄럽게도 필자는 해당 제품의 저자였습니다(그 사이 많이 늙었군요...). 요즘은 웹 서버에 도입 비용을 들인다는 것이 이상하게 들릴 수 있지만 당시에는 선택지(1CPU 200만원)가 그리 많지 않았습니다. 당시 Iplanet 웹 서버는 멀티 프로세스와 스레드를 지원하며 JAVA 서블릿 컨테이너를 내장하여 일반적인 웹 환경에서는 경쟁자가 없었고, 웹 서비스 환경이 발전하면서 Iplanet 웹 서버 + WebLogic 형태의 3 Tier 구조가 교과서처럼 적용되던 시기였습니다.

이후 약 10년 동안 멀티 프로세스+스레드(worker) MPM기반의 Apache 웹 서버가 평정했던 시기를 지나 요즘은 Event MPM형태의 Nginx나 경량 웹 서버가 다양한 형태로 적용되고 있으며, EJB의 쇠락 이후에는 대부분 Tomcat 서블릿 컨테이너를 적용하여 안정적인 서비스를 운영할 수 있도록 발전해 왔습니다. 예전을 떠올려 보았을 때 상용 인프라 소프트웨어를 사용하던 회사에서 OSS 제품에 대해 이야기하면 다음과 같은 대답을 들었습니다.

"문제(버그, 설정, 장애) 생기면 누가 처리해 줘?"
"중간에 없어지는 거 아냐? 버전 업을 어떻게 하나?"
"성능이 잘 안 나오는 것 같은데?"

저는 당시 OSS를 사용하기 위해 검토하는 조직에게 이렇게 말씀드렸습니다.

"OSS를 사용하려면 조직이 OSS를 수용할 수 있도록 문화가 바뀌어야 합니다."
"책임 이야기하지 마시고, 외부 업체 불러 장애 처리 기다리는 시간에 직접 하시고, 있는 코드 버그 수정은 직접 할 수 있도록 적당히 코딩도 하셔야 합니다."

물론 순환 보직 형태이거나 진급하면 실무를 하기 어려운 공공, 대기업의 경우 이러한 문화를 가지기 힘들었고 당시에 벤처라고 불리던 닷컴 기업들과 요즘의 스타트업에서는 점차 OSS를 수용할 수 있는 소양(?)을 가지게 되었습니다. 그리고 이제는 웹 서버, 서블릿 컨테이너 설치 또는 구성이 엄청난 기술은 아닌 환경이 되었습니다. 최근 Oracle JDK의 라이선스 변경에 따라 인터넷 상에서 나오는 OpenJDK 관련 질문과 답변들을 보면 약 20년 전의 고민이 반복되는 느낌이 듭니다. 

"OpenJDK? 문제 생기면 누가 처리하지?"  → 지금까지 그래왔던 것처럼, 혹시 모를 버그나 문제가 있을 수 있습니다. 다른 버전으로 바꿔보고 테스트하면서 해결하면 됩니다.

"계속 버전 업 되기는 하는 걸까?" → 아마도 큰 걱정 없이 진행될 겁니다. 오라클이 없어진다 해도 OpenJDK가 사라지지는 않을 것이라고 생각합니다. 

"안정성이나 성능 차이는 없을까?" → 여러 배급처들은 OpenJDK를 오라클에서 주관하는 TCK(Technology Compatibility Kit)로 테스트 후 제공하며, 따라서 JAVA의 호환성에 대한 인증이 됩니다. 성능이나 안정성 부분에서는 내부적으로 직접 확인 작업을 거쳐서 적용할 예정입니다.

인터넷의 공식/비공식 문서들과 게시판을 보면서 재미있다고 생각한 점은 Java 1.3부터 개발 및 인프라 제품을 지원하면서 당시 썬 마이크로시스템즈나 현 오라클에 비용을 지불하고 기술 지원을 따로 받는 경우가 극히 드물었음에도 이러한 걱정을 하게 된다는 것입니다. 제가 OpenJDK 추종자(evangelist)는 아닙니다만 오픈소스 환경에 대한 신뢰가 이미 시장에 만들어졌고 현재 주위를 둘러보면 많은 오픈소스 제품으로 서비스를 구축하고 있습니다. 따라서 OpenJDK를 적용하는 것 역시 '오픈 소스를 도입한다'는 정도로 생각하면 어떨까 합니다. 다만 몇 가지 차이점이 있을 수 있으니 그 부분에 대한 검토는 필요하다고 생각했습니다.  

OpenJDK 적용 시 고려할 사항


결과부터 말씀드리자면 적용하는 데 있어서 '큰 고생은 없었다'입니다. 여러 버전의 OpenJDK가 있어서 일일이 테스트하는 과정에 시간이 많이 소요된 것을 제외하면 심각한 결함이나 성능 편차 등에 의한 고민은 없었습니다. 마지막에 말씀드리겠지만 '생산성'과 '비용' 측면에서 조금 더 고려했습니다. 

그리고 OpenJDK를 적용함에 있어서 Java Major 버전 업그레이드는 검토하지 않았습니다. OpenJDK TF의 목표는, 많은 시스템에 적용된 Java 8 Oracle JDK에서 버그나 보안 취약점이 발견되더라도 2019년 1월 이후에는 오라클에서 업그레이드를 지원하지 않기 때문에 이에 대한 검토를 진행하는 것이었습니다. 따라서 Java 8을 Java 11로 변경하는 것과는 차원이 다른 문제가 되겠습니다. Java의 Major 버전을 변경한다는 것은 소프트웨어(JDK)를 교체하는 것 외에 애플리케이션 코드에서 deprecated 여부, 사용 중인 프레임워크의 호환성 검토 뿐 아니라 새로운 코드 스타일, 개선되는 사항에 대한 코드 개발 등을 포함하여 검토해야 하기 때문에 JDK의 호환성, 성능 문제보다 애플리케이션 코드에 대한 쟁점이 더 크다고 볼 수 있습니다.  

추가로 LINE의 시스템들은 OS가 잘 정리되어 있는 편이라 EOS(End Of Support)된 버전에 대한 고민이나 지원하지 않는 환경에 대한 고민 역시 크지 않았음을 말씀드립니다. 만약 서버가 오래 되었거나 최근 것이지만 업데이트가 적용되지 않은 서버에 OpenJDK 적용을 고려하신다면 개인적인 의견으로 현 상태 그대로 사용하시고 JDK를 더 이상 업데이트는 하지 마십시오.* 그리고 새로운 서버를 만들 때 OpenJDK를 검토하기를 권해 드리고 싶습니다.

기존 Oracle JDK에서 OpenJDK로 전환 시에 어떤 배포 버전을 선택할지, 어떤 방식으로 호환성이나 안정성 또는 성능에 대하여 검증할지에 대한 고려가 필요합니다. 문서로 확인할 수도 있고 특정 도구를 사용하여 부하를 검증할 수도 있습니다.

OpenJDK는 여러 배급처가 존재합니다. JCP(Java Community Process)를 통해 JSR(Java Specification Request) 표준이 정의되면 이 내용을 기반으로 각 배급자들이 구현하고, JSR 표준에 맞추어 개발된 JDK의 기능을 검증하는 도구가 TCK(Technology Compatibility Kit)입니다. 이미 오래 전 썬 마이크로시스템즈는 라이선스에 문제가 있는 몇몇 모듈(JFX 등)을 제외하고 OpenJDK.org에 JDK 코드를 제공하였고, 이 소스는 공개되어 있습니다. 따라서 다른 배급자들은 해당 소스를 참조하거나 개선하여 OpenJDK를 배포하고 있습니다. 예를 들어 G1GC의 다음 세대 GC(Garbage Collection)로 오라클은 Z GC를 구현하고 있고, 다른 배급자인 레드햇은 Shenandoah GC를 이끌고 있습니다. 물론 Z GC나 Shenandoah GC 모두 OpenJDK의 프로젝트로 참여하고 있으며 레드햇 OpenJDK를 설치하면 Java 11뿐 아니라 Java 8까지 백포트(backport)되어 제공됩니다. 아직은 완성된 단계라 보기는 어렵겠으나 향후 진행에 따라 OpenJDK에서 두 가지 GC를 모두 사용하거나 배급자에 따라 다르게 사용하는 등 여러 형태가 될 수 있다고 예상합니다.

안정성이나 성능을 각각 구분하여 고려하기에는 약간 모호한 부분이 있었지만, 목표 수치를 정하고 테스트를 통하여 확인하는 방법으로 검증을 수행하였습니다. 예를 들어 안정성 확인을 위해서는 프로그램을 장시간(10시간 이상) 동작시켜 문제가 없는지 확인하였고, 성능 확인은 일부 운영 서버에 OpenJDK를 적용하여 직접 사용자 요청을 처리하면서 부하를 늘려 목표치(평소의 3배)까지 확인하였습니다.

배포 버전의 종류와 차이

다음은 각 배급처에서 배포하는 OpenJDK 배포 버전의 종류와 차이에 대해 정리한 표입니다.

Community
/Vendor
Product Name OSS
/Commercial
Architecture Description
ORACLE Oracle JDK Commercial Hotspot · Java 8 2019년 1월 이후 License 필요
· 검토 시 기준 값으로 사용
AZUL Zing Commercial Zing · Azul의 자체 아키텍처를 가진 JVM입니다.
· 원래 상용 버전아키텍처 차이 및 상용 제품으로 검토에서 제외
OpenJDK.org OpenJDK OSS Hotspot · 썬 마이크로시스템즈 시절에 만들어진 커뮤니티 그룹으로 우리가 알고 있는 OpenJDK가 만들어지는 곳입니다. 
· 커뮤니티에 집중되어 있고 Source는 배포하지만 Binary 배포는 https://jdk.java.net을 통하여 이루어 집니다.
·  https://jdk.java.net에서 배포되는 Binary는 참조용으로 사용할 것을 권장합니다. (참고)
AZUL Zulu OSS Hotspot · Azul에서 오픈소스로 제공하는 JDK입니다. Zing과는 아키텍처 자체가 다릅니다. 
· OpenJDK를 기반으로 개발됩니다. 별도의 계약으로 지원을 받을 수 있습니다.
Red Hat/CentOS OpenJDK OSS Hotspot · 레드햇에서 제공됩니다.
· RHEL(Red Hat Enterprise Linux) 또는 CentOS yum repository를 통하여 rpm(binary)으로 배포됩니다. 
· OS 버전에 따라서 지원되는 버전이 조금씩 다릅니다
· 개발자 사이트 (Windows binary)
AdoptOpenJDK.net OpenJDK OSS HotspotOpenJ9(IBM) · OpenJDK 배포를 주 목적으로 하며, 여러 기업의 후원을 받아 개발자들이 운영하는 커뮤니티입니다.
· 오라클의 지원으로 OpenJDK.org가 주도하는 Hotspot(openjdk)과, IBM의 지원으로 Eclipse가 주도하는 OpenJ9 VM의 Binary를 모두 제공합니다.
· 현 상황에서 가장 다양한 플랫폼과 VM아키텍처의 Binary를 제공 받을 수 있습니다. (PC용 MAC OS 포함)
Eclipse.org OpenJ9 OSS OpenJ9(IBM) · IBM이 자체 JDK를 Eclipse에 기부하면서 시작된 프로젝트로 VM 아키텍처가 Hotspot과 다릅니다.
· 기존에 IBM 계열의 JDK를 사용 중이던 기업에서 OpenJDK를 고려할 경우 OpenJ9을 고려할 수 있습니다.
· 기존의 Hostpot 계열의 JVM과 설정 및 아키텍처에서 다수의 차이가 있습니다.
· 아키텍처 차이로 인하여 검토에서 제외

호환성 확인

코드 호환성

'기존 Oracle JDK에서 운영되던 코드가 OpenJDK에서 문제 없이 동작하는가'에 대한 고민은 기우일 수도 있고 사실 발생할 수 있는 문제이기도 합니다. 일반적인 웹 기반 애플리케이션의 경우 앞서 말한 TCK 테스트를 통과한 버전에서 호환성에 문제가 없어야 합니다(성능이나 다른 부분은 별개로 생각). 저희 역시 진행하는 동안에 심각한 문제는 없었습니다. 다만 일부 코드에서 JavaFX의 자료형 객체를 사용한 부분이 확인되었습니다(OpenJDK에는 라이선스 문제로 JavaFX가 포함되어 있지 않습니다. OpenJFX를 별도로 사용 가능합니다.). WAS를 기동하자 로그에 예외 오류가 다수 발생하여 확인해 보니 javafx.util.Pair를 사용하여 발생한 문제였고 해당 코드를 다른 객체를 사용하도록 수정하여 해결하였습니다.

JVM(Java Virtual Machine) 호환성

JVM 아키텍처의 차이에 따라 JVM 옵션이 적용되지 않을 수 있습니다. 예를 들어 Oracle JDK를 사용하던 환경에서 G1GC를 사용하기 위해 -XX:+UseG1GC를 사용했다면 Eclipse OpenJ9에서는 해당 옵션이 적용되지 않기 때문에 WAS 기동에 실패할 수 있습니다. 따라서 VM 아키텍처가 다른 Eclipse OpenJ9이나 Azul Zing은 좀 더 많은 고려가 필요합니다. 이러한 사유로 여러 JDK 중 아키텍처가 다른 Azul Zing과 Eclipse openJ9을 제외하고 호환성을 확인하기 위해 다음과 같은 테스트를 진행하였습니다.  

  1. OpenJDK를 적용한 WAS에 기존과 동일한 JVM 옵션으로 기동하는지 확인
  • 정상적으로 VM이 초기화되는지
  • 옵션 중에 호환되지 않는 옵션이 있는지
  • OpenJDK를 적용한 WAS에 WAR deploy 후 기동 시 기존에 없던 Exception이 발생하는지 확인
    • 기존에 사용하던 Class가 누락된 것이 없는지(JCE-Java Cryptography Extension- 등)
    • Oracle JDK에서만 제공되는 Class를 사용하는 경우가 있는지(JFX-JavaFX- 등)
  • Alpha, Beta WAS에 OpenJDK를 적용하여 모니터링
    • 초기 확인된 내용 외에 별도의 문제가 없는지 확인

    안정성 및 성능 확인

    예전에 Java Minor 버전 업그레이드 이후 이유 없이 JVM 프로세스가 다운되는 현상이 발생한 경험이 있습니다. 원인은 JVM의 내부 버그로 인한 process crash 현상이었고, 일단 원래 버전을 적용하였다가 다음 업데이트를 적용하여 회피한 경우가 있습니다. 하드웨어나 소프트웨어에 초기 불량 또는 버그가 없기를 기대하지만 현실에서 모든 장비는 고장날 수 있고, 어떤 소프트웨어든지 버그는 있을 수 있습니다. 우리가 상용 제품을 사용하는 이유는 이러한 문제가 발생했을 때 적절한 지원이 가능하기 때문이고, 보험의 성격으로 도입해서 매 해 유지 보수 계약을 체결합니다. 

     오픈소스의 경우 상용 버전보다 많은 정보가 빠르게 공개되고, 중요한 조치는 오히려 상용 버전보다 더 빠르게 해결되는 경우가 많습니다. 물론 자체적(기업 내부)으로 해결 가능한 기술을 보유해야 하는 전제는 있습니다. 상용 버전이라면 크리티컬 패치(hotfix)를 제공하기도 하지만, 일반적으로는 문제들을 모아서 다음 통합 패치나 버전 업그레이드를 통해서 적용합니다. 자체적인 해결 능력을 보유할 수 없는 조직의 경우 이런 지원을 더 안정적이라고 판단하기도 합니다. 또한 적용과 함께 바로 확인되는 문제는 초기에 조치할 수 있지만, 부하 증가에 따른 스레드 개수의 증가나 메모리 사용량의 증가 등에 의해 이상 동작을 일으키는 경우도 있습니다. 이런 문제는 신규 설치 또는 업그레이드 후 시간이 지나야 확인할 수 있기 때문에 서비스 측면에서는 치명적인 문제가 될 수 있습니다. 이미 수 백 대의 서버에 적용된 JDK를 원복하는 작업은 서비스 품질 면이나 엔지니어의 작업량을 보았을 때 큰 문제가 됩니다. 그래서 앞서 호환성 확인이 끝난 OpenJDK에 대하여 다음과 같은 확인 작업을 거쳤습니다.

    1. JMH(Java Microbenchmark Harness)를 이용하여 10시간 이상 동작시킵니다. (38개의 샘플코드를 모두 동작시키면 10~14시간이 소요됩니다)
    • OpenJDK.org에서 제공하는 벤치마킹 프로그램과 샘플을 사용하여 JVM에서 정상적으로 동작하는지 확인합니다.
    • 벤치마킹 프로그램을 10시간 이상 구동하여 JVM 동작에 이상이 없는지 확인합니다. (프로세스 down 여부, GC log 확인)
    • 출력된 벤치마킹 데이터를 비교하여 OpenJDK 종류에 따른 문제가 없는지 확인합니다.

    전반적으로 서로 편차가 있기는 하지만 큰 차이 없는 성능을 보여주었습니다. 

    Multi Thread Benchmark Test Loop Benchmark Test
    Thread Scope에 따라(shared/unshared) 성능을 비교합니다. 초 당 처리 건수를 의미하며 많을 수록 좋습니다. 공유/비공유에 따른 성능 차이는 유사하였고 전체 처리 건수도 Adopt OpenJDK를 제외하면 유사한 성능을 보였습니다.

    다량의 루프(loop)를 수행하여 성능을 비교합니다. 낮을 수록 좋습니다.100회의 경우 그래프 상에 차이가 있는 듯 하지만 범례가 nano sec이며 10만회에서 보듯이 모두 유사한 성능을 보입니다.

    Blocking Queue Int Benchmark Test String Concatenation Benchmark Test
    Blocking Queue에 인터럽트를 걸어 처리하는 데 걸리는 시간을 비교합니다. 짧을 수록 좋습니다. 범례가 nano sec로 1m sec 정도의 편차가 있어 재측정하였으나 유사하여 그대로 표현하였습니다.

    String 연산을 '+'와 String Builder를 사용하여 비교합니다. 낮을 수록 좋습니다. 아시는 바와 같이 '+' 연산은 append()보다 현격하게 낮은 성능을 보였고 Zulu의 경우 append()를 사용한 연산은 성능이 좋았으나 오히려 '+' 연산에서는 낮은 성능을 보였습니다. 다만 micro sec이고 편차가 크지 않아서 문제로 판단하지는 않았습니다.

    1. 운영 서버 일부에서 실제 사용자 요청을 처리하였습니다.
    • 이상 없이 동작하는지 확인
    • 부하를 높여 (평소 부하의 3배수) 이상이 없는지 확인하고 성능 확인을 함께 진행
    • 아래 그래프는 LINE 내부의 시스템 모니터링 도구(imon)를 사용하였습니다.
    • 테스트 결과 모든 OpenJDK에서 이상 없이 동작함이 확인되었습니다.
    1. 운영 서버 중 일부에 적용하여 서비스를 운영합니다.
    • LINE은 실 서비스에 확산 적용하기 전 마지막 확인 단계로 서버 중 일부를 Canary group으로 관리합니다.
    • Canary group에 포함된 서버들에 OpenJDK를 적용하여 최종 이상 여부를 확인하였습니다.
    • 2019년 2월 현재 이상 없이 정상 운영 중입니다.

    설치 자동화 (provisioning)

    LINE에서 사용하는 서버는 프로비저닝 도구인 PMC를 사용하여 JDK를 설치 후 softlink로 연결하고, JAVA_HOME 환경변수를 softlink로 설정하여 구성됩니다. 이러한 관리 방법은 서버를 생성하거나 증설할 때 빠르게 시스템을 구성할 수 있고 유지관리 비용을 낮출 수 있도록 설계되어 있습니다. 다만 이번 TF에서는 기존 체계에서 일부 만을 OpenJDK로 변경하는 작업을 수행하는 것이고 임시 작업을 위해 기존 체계를 조정할 수 없는 문제가 있었습니다. 자칫 잘못하면 운영 중인 서버에 문제가 발생할 수 도 있기 때문입니다. 그렇다고 수십 대의 서버를 손으로 일일이 작업하기란 만만치 않습니다. 서버 1대에서 터미널 접속, 복사, 설치, 링크 변경, 필요한 jar 파일 복사, 재구동 등을 작업하기 위해 아무리 빨라도 15분 정도가 소요되었고, 중간에 하나라도 누락하면 작업이 실패할 수 있었습니다. 또한 확산 시에는 동시에 수백 대에 배포할 수도 있어야 합니다. 

    그래서 선택한 방법은 Ansible입니다. Ansible이 레드햇에 인수된 이후에도 Ansible 언어 자체는 오픈소스로 사용할 수 있습니다. 이 글에서 Ansible에 대해 자세히 다루기는 힘들지만 Ansible의 장점을 몇 가지 이야기하면 다음과 같습니다.

    • YAML 형태로 작성한 Playbook으로 다수의 서버에 일괄 적용이 가능합니다.
    • 실제 작업은 Python으로 이루어지지만 Python보다는 쉬운 YAML형태의 Ansible 구문을 배우면 좀 더 쉽게 자동화가 가능합니다.
    • dry-run(Test Run)이 가능합니다. 실제 적용하기 전에 정상적으로 동작할지 확인하는 작업은 중요합니다.
    • 제공되는 Ansible module을 사용하면 실행 중 실패한다고 해도 예외 처리가 가능합니다.

    자동화를 통해 다수의 서버에 설치 및 구성하는 방법은 정의했으나 또 다른 문제가 있었습니다. 자세한 설명은 드리지 못하겠지만 LINE은 서버에 접근하기 위해 Kerberos 인증을 사용하는데 OpenJDK를 /usr 하위에 설치를 하려면 ROOT 권한도 필요합니다. OpenJDK를 설치하겠다고 보안의 근간을 무시할 수는 없었습니다. 최소한 작업하는 사용자의 권한으로 서버에 접근 권한을 취득하여 배포할 수 있도록 해야 다른 사람이 작업을 하더라도 이력이 남고 문제 발생시 추적이 가능하게 됩니다. TF를 진행하면서 필자 혼자 작업이 가능하도록 구성해서 사용할 수도 있었겠지만 다수의 서버에 혼자 작업할 수도 없었고, 향후에 필요하다면 OpenJDK를 배포할 수 있는 플랫폼이 필요했습니다. 그래서 Ansible Tower의 오픈소스 버전인 AWX를 적용하기로 하였습니다. AWX 자체의 기능도 너무 많아서 다 보여 드릴 수는 없으니 사이트를 참고해 주시기 바랍니다. 

    이 과정에서 확인된 이슈와 해결 방법을 공유합니다. (상세한 코드나 구성은 포함하지 못하는 점 양해 부탁드립니다.)

    인증 키 배포

    Ansible이 동작하기 위해서는 작업 대상 서버에 먼저 SSH Public Key를 배포해 두어야 합니다. 서버 몇 대 정도라면 터미널에서 작업해서 넣을 수 있겠지만, 작업 대상 서버가 수십~수백 대라면 SSH Public Key 없이 접속 자체가 불가능하므로 자동화의 의미가 무색합니다. 즉 Ansible을 사용해서 SSH Public key를 배포하려면 접속을 해야 하는데, SSH Public key가 없어서 접속이 안되는 딜레마(?) 상태입니다.

    1. 문제
    • SSH Public Key를 작업 대상 서버에 설치해야 합니다.
    • Ansible playbook에서 ID와 PASSWORD를 사용해서 접속하려면 보안상 문제가 될 수 있습니다.
  • 해결
    • Ansible AWX를 실행 시 대화형으로 입력 받는 Survey 기능을 사용하여, playbook을 실행할 때마다 작업자의 Kerberos ID/PASSWORD를 입력 받고, 보안 토큰(Keytab)을 생성하여 대상 서버에 접근합니다.
    • 실행할 때마다 매번 작업자의 계정 정보를 사용하므로 코드 상에 ID/PASSWORD 누출 문제를 회피할 수 있으며, 감사 정보를 남길 수 있습니다.

    Inventory 구성 자동화

    Ansible AWX에서 작업 대상 서버의 목록을 Inventory라고 합니다. Ansible CLI 환경에서 hosts 파일을 작성하는 것도 번거로운 작업이지만, AWX GUI에서 다수의 서버를 일일이 등록하기도 힘듭니다.

    1. 문제
    • 다수의 서버를 GUI로 일일이 등록하기 어렵습니다.
    • LINE의 서비스에서 서비스를 그룹셋(GroupSet)과 그룹(Group)으로 나누는 개념이 있는데 이 정보를 반영하려면 엄청나게 번거롭습니다.
  • 해결
    • Ansible AWX의 'Inventory Script' 기능을 사용합니다.
    • 회사 내의 서버 정보를 추출할 수 있는 API 서버에서 정보를 받아 AWX Inventory로 import할 수 있도록 Python 코드를 작성하였습니다.

    작성한 Python 코드의 샘플은 다음과 같습니다. 보안상 일부 정보를 제거하고 공유합니다. 코드 그대로 수행되지 않습니다. 파싱(parsing) 포맷을 이해하는 데 참고하세요.

    #!/usr/bin/env python
    import urllib
    import json
    import sys
    from collections import OrderedDict
     
    # 아래 코드는 LINE의 내부 정보를 제거하고 제공됩니다. 제공된 코드 그대로 실행되지 않을 수 있습니다.
    # 독자의 환경에 API를 통해서 JSON 형태의 서버 목록을 추출할 수 있는 경우 자신의 형태에 맞추어 수정이 필요합니다.
    # 아래 문서에서 file 기반의 hosts 파일을 읽어서 처리하는 부분도 참고하실 수 있습니다.
    # https://docs.ansible.com/ansible/latest/dev_guide/developing_inventory.html
    API_SERVER_URI="YOUR_API_SERVER"
    PROJECT_ID = "YOUR_PROJECT_NAME"
    PROJECT_PHASE = "YOUR_RELEASE"
     
    API_BASE_URL="https://" + API_SERVER_URI + "/" + PROJECT_ID + ":" + PROJECT_PHASE
     
    def getJson(url) :
        url = urllib.urlopen(url)
        data = url.read()
        parsed_data = json.loads(data)
        return parsed_data
     
    def getGroupSets():
        group_sets_url = API_BASE_URL  + "/groupsets"
        return getJson(group_sets_url)
     
    def getGroups(groupset_name):
        groups_url = API_BASE_URL  + "/groupsets/" + groupset_name + "/groups"
        return getJson(groups_url)
     
    def getNodes(groupset_name,group_name):
        nodes_url = API_BASE_URL  + "/groupsets/" + groupset_name + "/groups/" + group_name + "/nodes"
        return getJson(nodes_url)
     
    # Ansible AWX 또는 Ansible Tower의 Inventory Script에서 Parsing 가능한 형태로 출력하는 부분입니다.
    # 그룹에 서버 목록을 넣고 그룹을 그룹셋 개념으로 묶어 주는 형태입니다.
    # 그룹 아래 그룹을 추가하는 경우 'children' 엔티티가 사용됩니다.
    # 독자의 환경이 다르므로 아래 코드는 그대로 사용할 수 없을 것입니다. AWX에서 분석하는 포맷을 이해하는 데 참고하세요
    inventory = OrderedDict()
    inventory['all'] = {}
    inventory['all']['children'] = []
     
    parsed_groupset_data = getGroupSets()
     
    for groupsets in parsed_groupset_data:
        groupset_name = groupsets["name"]
        inventory[groupset_name] = {}
        inventory[groupset_name]['children'] = []
        inventory['all']['children'].append(groupset_name)
        parsed_groups_data =  getGroups(groupset_name)
        for groups in parsed_groups_data:
            group_name = groups["name"]
            inventory[groupset_name]['children'].append(group_name)
            inventory[group_name] = {}
            inventory[group_name]['hosts']  = []
            parsed_nodes_data = getNodes(groupset_name,group_name)
            for node in parsed_nodes_data:
                node_name = node["name"]
                falg_maintenance = node["attributes"]["state"]
                if falg_maintenance == "NORMAL":
                    inventory[group_name]['hosts'].append(node_name)
     
    # 아래에서 출력된 내용이 AWX 파싱 포맷과 맞으면 Inventory로 생성됩니다.
    print(json.dumps(inventory, ensure_ascii=False, indent=3))

    테스트에 사용한 도구들


    테스트 작업을 위해 사용한 도구들이 있습니다. 도구에 대한 정보를 간략히 정리하여 공유합니다.

    JMH(Java Micro benchmark Harness)

    • JVM의 벤치마크를 위한 도구로써 OpenJDK.org 프로젝트에서 제공됩니다.
    • 안정성 테스트 및 JVM 종류 별 성능 비교를 위해 사용했습니다.
    • 설치
    # mvn archetype:generate 
    -DinteractiveMode=false 
    -DarchetypeGroupId=org.openjdk.jmh 
    -DarchetypeArtifactId=jmh-java-benchmark-archetype 
    -DgroupId=org.samples 
    -DartifactId=openjdkJMH 
    -Dversion=1.0
    # cd opnjdkJMH
    # mvn clean install
    • JDK 선택 스크립트 ($YOUR_HOME 지정) - setjdk.sh
    #!/bin/bash
      
    VENDOR=$1
    YOUR_HOME="YOUR JVM PATH"
        if [ "$VENDOR" = "" ]
        then
           echo "ERROR: Need Vendor NAME [oracle|redhat|adopt|azul]"
           echo "Use : setjdk.sh $VENDOR"
           echo "ex : setjdk.sh oracle"
           exit 1
        fi
      
        case $VENDOR in
            oracle)
            export JAVA_HOME=${YOUR_HOME}/jdk/jdk1.8.0_181
            export PATH=$JAVA_HOME/bin:$PATH
            ;;
            redhat)
            export JAVA_HOME=/usr/lib/jvm/java-1.8.0
            export PATH=$JAVA_HOME/bin:$PATH
            ;;
            adopt)
            export JAVA_HOME=${YOUR_HOME}/jdk/jdk8u181-b13
            export PATH=$JAVA_HOME/bin:$PATH
            ;;
            azul)
            export JAVA_HOME=${YOUR_HOME}/jdk/zulu8.31.0.1-jdk8.0.181-linux_x64
            export PATH=$JAVA_HOME/bin:$PATH
            ;;
            *)
            echo "Need Vendor NAME [oracle|redhat|adopt|azul]";;
        esac
      
        echo "=================================="
        echo "JAVA_HOME="$JAVA_HOME
        echo "JAVA_VERSION="$JAVA_VERSION
        `$JAVA_HOME/bin/java -version`
        echo "PATH="$PATH
        echo "=================================="
    • 실행 스크립트 샘플(JDK 종류 및 출력 데이터 파일명 수정) - bmtOracle_all.sh
    #!/bin/bash
    . ./setjdk.sh oracle
    $JAVA_HOME/bin/java -jar ../target/benchmarks.jar 
    -rf csv -rff "EL7_All_oraclejdk_1.8_`date '+%Y-%m-%d_%H%M%S'`.csv" 
    -jvm "$JAVA_HOME/bin/java" 
    -jvmArgs "-Xms4g -Xmx4g -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=45" 
    -prof perfnorm

    JDK Mission Control

    • JAVA Mission Control(JMC)은 Oracle JDK에서 상용 서비스로 제공됩니다. 이 제품의 OSS 버전이 JDK Mission Control(JMC)이며 OpenJDK.org 프로젝트에서 개발됩니다. (참고)
    • 2019년 2월 현재 개발 중이며(JMC 7 Early-Access Builds) 현 버전으로 기본적인 모니터링은 가능합니다.
    • JVM의 실시간 GC 정보와 스레드 상태를 모니터링하는 데 사용했습니다.

    GC Viewer

    • JVM에서 생성한 GC 로그를 분석하기 위한 도구입니다. 
    • G1GC 로그를 지원하는 도구 중에서 오픈소스인 제품입니다. UI가 상용 제품만큼의 품질은 아니지만 데이터 분석용으로는 충분합니다.
    • 다음 그림은 이번 테스트의 내용은 아니고 다른 프로젝트에서 메모리 누수를 분석한 이미지입니다. 이번 테스트에서는 이상이 없어 분석할 내용이 따로 없었습니다.

    Ansible AWX

    • CLI 환경인 Ansible을 관리할 수 있도록 제공되는 Ansible Tower의 오픈소스 버전입니다.
    • OpenShift, Kubernetes, Docker 환경으로 사용하도록 배포되고 있습니다. Docker compose 환경에서 설치하여 사용하였습니다.
    • 작업 대상 서버 Inventory를 생성하고, SSH Public Key를 배포, OpenJDK 설치하는 데 사용했습니다.

    확인된 이슈 사항


    OpenJDK를 적용하는 동안 확인된 사항들 몇 가지를 소개합니다. OpenJDK 자체의 호환성 문제나 성능 문제는 아니고, LINE에서 개발된 애플리케이션에서 사용하는 몇몇 Class가 포함되지 않았던 사항들이었습니다.

    JCE(Java Cryptography Extension)  설치 여부 

    Java 개발 시 강화된 보안을 적용하기 위해서 JCE를 추가 설치합니다. 높은 bit 수의 암호화를 지원하기 위해 Provider 정보가 포함된 jar 파일을 JDK Classpath에 추가합니다. Oracle JDK의 경우 Java 8_151 이전 버전에서는 JCE를 다운로드 받아서 설치해야 했습니다. 이후 버전에는 기본 포함되어 있으며 아래와 같이 설정을 통해서 활성화할 수 있습니다.

    • case1 : '${JAVA_HOME}/jre/lib/security/java.security' 파일을 열어 crypto.policy=unlimited 부분의 주석을 풀거나, 없을 경우 추가합니다.
    • case2 : Security.setProperty("crypto.policy", "unlimited"); 코드에서 설정합니다.

    OpenJDK의 경우 Java 8 버전에서 기본 JCE를 포함하고 활성화된 상태이므로 별도의 설치나 구성이 필요하지 않습니다. 다만 Provider의 차이나 구현된 코드를 일일이 소스 수준에서 검토하기에는 시간 제약이 있어, 기존과 같이 JCE 파일을 복사해 넣어주는 작업을 수행하였습니다. (Ansible playbook에서 처리)

    Tomcat WAS의 JMX 성능 수집

    LINE 내부 모니터링 시스템(imon)에서 Tomcat의 스레드 정보 등을 수집하기 위해 Java의 JMX를 사용하며 자체적인 class가 구현되어 있습니다.

    정상적인 정보 수집을 위해서는 구현된 jar 파일을 복사해 주는 작업이 필요했습니다. 이 부분 역시 OpenJDK 설치 시 Ansible playbook에서 공통 파일로 복사해 넣는 방법으로 해결하였습니다. 

    배포 버전 선택과 주의 사항


    모든 테스트와 논의를 거쳐 저희는 레드햇에서 제공하는 OpenJDK를 사용하기로 결정하였고, LINE의 메인 서비스(메신저, 인증, 채널 등의 일부)에 적용하여 19년 2월 현재 문제 없이 잘 운영되고 있습니다. 물론 이번에 테스트하고 적용한 버전이 운 좋게도 버그나 문제점이 없는 버전일 수도 있습니다. 아니면 숨어있는 문제가 있어도 우리가 해당 기능을 사용하지 않아서 발현되지 않았을 수도 있습니다. 어떤 제품이 정답이라는 것은 없기 때문에 현 상황에서 최선이라고 판단되는 제품을 선택했습니다.

    배포 버전

    레드햇의 OpenJDK를 선택한 이유는 성능이나 호환성의 이유는 아닙니다. '생산성'과 '소유 비용'면에서 선택하게 되었습니다. 다른 제품들도 비슷한 성능을 가졌고 호환성에서 문제를 나타내지 않았습니다. 이 글을 읽는 독자의 환경에 따라 저희와는 다른 판단을 내려야 하는 경우가 있을 수도 있음을 안내해 드립니다.

    생산성 측면(엔지니어의 작업 편의성, 안정적인 작업)

    • yum repository를 이용한 배포, 다수의 서버에 쉽게 설치 가능
    • LINE의 다수 서버가 CentOS를 기반으로 하고 있으며 최신 버전인 6, 7로 잘 관리되고 있음
    • Ansible과 조합했을 때 yum module 등과 조합하면 자동화에서 강점
    • 버그 패치 등 작업은 CentOS에서 이루어짐

    소유 비용(인력 비용, 라이선스)

    • CentOS를 기반으로 하고 있기 때문에 도입 비용이 추가로 들지 않음
    • 자체적으로 빌드할 필요가 없으며 JDK의 호환성(TCK) 체크, 보안, 버그 패치, 버전 관리 등을 수행할 인력을 유지할 필요가 없음
    • 향후 OS 업그레이드에 따라 Java 마이너 버전 업그레이드도 자연스럽게 이루어질 수 있음(다만 안정성을 위해 특정 버전으로 고정)

    생각할 수 있는 단점과 의견

    • Zero-day : CentOS의 패치는 RHEL(Red Hat Enterprise Linux)에 반영된 이후 hot-fix 적용 없이 다음 버전으로 통합되어 제공되는 경우가 있습니다. 이러한 경우 상용 JDK보다 패치 적용이 늦어질 수 있으며 직접 작업이 필요한 경우도 있습니다.
    CVE 적용 체크
    
    # yum으로 설치된 RPM의 change log에서 cve 코드로 반영 확인이 가능합니다.
    rpm -q --changelog java-1.8.0-openjdk | grep cve
    
    #yum에서 적용 가능한 보안 패치 확인방법
    yum list-security --security
    
    #설치된 보안 패치 정보 확인
    yum updateinfo list security all
    
    #특정 CVE만 패치 
    yum update –cve CVE-2008-0947

    단기적인 보안 이슈가 발생하는 경우 문제가 될 수 있지만 이러한 경우는 대부분 일반적이지 않습니다. 아래 그림에서 볼 수 있듯이 OpenJDK에 CVE(Common Vulnerabilities and Exposures) 발생 건수는 2015년에 1건입니다.  (참고)

    물론 그 외에 확인되는 보안 패치는 지속적으로 이루어 집니다.

    • CVE Code로 관리될 정도의 보안 취약점의 경우 대부분 빠르게 배포됩니다.
    • 일반적으로 JVM(WAS)은 직접 대외에 서비스를 제공하지 않습니다.(웹 서버에 비하여)
    • 의견 : 일반적으로 서비스 오픈 시 설치된 JDK를 몇 년이고 그냥 유지하는 경우가 많이 있습니다. 되도록 OS 업그레이드나 yum 업데이트 시 패치가 반영되도록 하고, 정상 동작 여부를 확인하는 것이 더 좋은 방법이라 생각됩니다.

    주의 사항

    현재 Oracle JDK를 사용하고 있고 오라클 라이선스 계약 예정이 없는 경우 주의하셔야 할 사항이 있습니다. 필자가 전문가가 아닌 관계로 라이선스에 대한 상세한 내용을 해석해 드릴 수 없으나 지금까지 알려진 내용을 바탕으로 정리해 보았습니다.

    Server

    • 2019년 1월 Java 8 Public update(8u202) 이후 버전과 Java 11 등의 버전은 라이선스 없이 업데이트하면 안됩니다.
    • OTN(오라클 서비스 계정)이 없으면 다운로드를 할 수 없으나 구할 수 있더라도 더 이상의 업데이트는 오라클 라이선스 정책에 위배될 수 있습니다.

    PC/Notebook

    • 서버와 동일한 제한 사항이 있습니다. 
    • 2019년 2월 현재 Java8 update202를 설치하면 안내화면에 '기업 사용자는 2019년 4월 업데이트의 영향을 받습니다.'라고 출력됩니다. (Windows 로케일이 한국이라 한글로 캡처된 점 양해 바랍니다.)
    • Oracle JDK Update agent(auto update)가 활성화되어 있다면 끄거나 더 이상 업데이트하지 않도록 조치를 취하시기 바랍니다.
    • 삭제하고 AdoptOpenJDK에서 Mac/Windows용 Hotspot OpenJDK를 설치하는 것도 방법입니다.
    • 오라클은 Java 8에 한하여 2020년까지 개인 사용자에게 업데이트를 제공합니다. (참고)
    • 하지만 일반적으로 회사에 있는 PC나 노트북(개인 소유권과 무관하게)은 개인이 아닌 상용(Commercial)으로 판단되어 정책 위반이 될 수 있습니다. 

    마치며


    2018년 10월부터 2019년 1월까지 OpenJDK 적용을 위한 TF에 참여하면서 경험했던 내용들을 마무리 지어봅니다. TF에 참여하며 어떤 방법론으로 진행할 것인가, 어떤 방법으로 검증할 것인가 등을 고민하고 테스트하는 데 시간을 많이 들였습니다. 제품 자체의 큰 문제는 발생되지 않아 다행이었다는 생각을 하기도 했습니다. 

    도입부에 말씀 드렸듯이 OpenJDK도 우리가 현재 쓰고 있는 수많은 오픈소스 중 하나입니다. 다만 코드와 밀접한 관계가 있기 때문에 변경시 발생할 수 있는 문제가 걱정스러울 수 있습니다. 하지만 우리가 대량의 데이터 저장소로 사용하는 Hadoop, Object Cache용으로 쓰는 Redis, RDB인 Maria DB 등 문제가 생기면 더욱 치명적일지도 모르는 기간 시스템을 이미 다수 사용하고 있다는 점을 생각해 보았을 때, 작게는 수십 대에서 수백 대 까지 다운사이징되어 운영되는 JVM(JDK)을 오픈소스로 전환하는 것이 단지 걱정 때문에 못 할 일은 아니라는 생각을 해봅니다. 물론 너무 쉽게 접근하다가 누락되는 내용으로 인하여 서비스에 문제가 발생하지 않도록, 현황 분석(AS-IS)에 많은 공을 들이고 테스트를 진행하는 것이 필요하겠다는 말씀도 드려봅니다.

    저희의 경험이 어디에선가 OpenJDK 적용을 고민하고 계시는 분들께 조금이나마 도움이 된다면 참 좋을 것 같다는 생각을 하면서 글을 마무리합니다. 감사합니다.