LINEのOpenJDK導入レポート:互換性の確認から注意事項まで

こんにちは。私は、Service EngineeringチームでSRE(Service Reliability Engineer)の業務を担当しているYongChan Kwonです。チーム名や業務名から分かると思いますが、一つの業務を担当するよりは、サービスのライフサイクルにおいてインフラ、開発、サービス技術の間で発生しうる死角を補う役割を担っています。

2018年、オラクルのライセンス体系が変更され、2019年1月以降は無料でOracle JDKを使用できなくなりました。そこで、LINE内部ではOpenJDKへの移行に向けて必要事項や検討項目を事前にまとめるため、TF(task force)を立ち上げました。また、その作業内容や技術のことについてまとめるためにこの記事を書くことにしました。

はじめに

本格的な話に入る前に、OpenJDKの導入検討で感じたことがあったので昔の記憶を辿ってみます。2000年初めのころまでも、OSS(Open Source Software)の信頼度についての評価は両極端で、公共機関や大規模な事業所で実際OSSを導入するのは容易ではありませんでした。例えば、Apache HTTPD Server(以下Apache Webサーバー)のPrefork MPM(Multi-Processing Modules)のメモリーリーク問題や、当時新しいMPMだったworkerの不安定性のため、公共機関や商用サイトではほとんどiplanet Webサーバーを使っていました。恥ずかしながら、私はその製品に関する書籍を書いたこともあります(今はかなり年を取りましたね…)。最近は、Webサーバーに導入コストをかけるというのは理解できないかもしれませんが、当時は、1CPU当たり約20万円かかるサービスしか選択できませんでした。ほかの選択肢は多くなかったのです。iplanet Webサーバーはマルチプロセスとマルチスレッド対応で、Javaサーブレットコンテナを実装しており、一般的なウェブ環境では競合がありませんでした。また、ウェブサービス環境が発展して、iplanet+WebLogic形の3層構造があたり前のように採用されていた時期でした。

その後約10年間、worker MPM(マルチプロセスとマルチスレッドのハイブリッド型)を使えるApache Webサーバーが一般的だった時期を経て、最近はイベント駆動型のNginxや軽量のWebサーバーがさまざまな形で構築されています。EJB(Enterprise Java Beans)が衰退してからは、ほとんどTomcatのサーブレットコンテナを実装し、安定的なサービスが運用できるように発展してきました。過去を振り返ってみると、商用のインフラソフトウェアを使っていた会社でOSS製品の話を持ち出すと、よく次のような質問が返ってきました。

「問題(バグ、設定、障害)が起きたら誰が対応してくれる?」

「途中でなくなるんじゃない?バージョンアップはどうする?」

「性能があまり出ないようだけど?」

私は当時、OSSの導入を検討している組織に次のように言っていました。

「OSSを使うためには、組織がOSSを受け入れられるように文化を変えるべきです。」

「責任の話なんかしないで、業者からの障害対応を待つよりは自ら対応して、コードのバグ修正くらいは直接できるように、ある程度コーディングもしなければなりません。」

もちろん、ジョブローテーションしたり昇進すると現場から離れることになる公共機関や大手企業の場合は、そういう文化を作るのは難しかったんですが、当時ベンチャーと呼ばれていたドットコム企業や最近のスタートアップ企業では、次第にOSSを受け入れられる下地ができました。そして、今はWebサーバー、サーブレットコンテナの設置または構成が特別な技術ではない環境になりました。しかし最近のOracle JDKのライセンスの変更により、ネット上に出てくるOpenJDKに関しての質問と回答を見ると、約20年前の悩みが続いている気がします。

「OpenJDK?問題が起きたら誰が対応する?」  → 今までと同様に、もしかしたらバグの問題があるかもしれません。他のバージョンに変えてテストしながら解決すればいいです。

「バージョンアップは続くのか?」→ 恐らく大きな心配なく、進められるでしょう。オラクルがなくなってもOpenJDKが消えることはないと思います。 

「安全性や性能の違いはないか?」 → いろいろな提供元は、OpenJDKをオラクルが主管するTCK(Technology Compatibility Kit)でテストしてから提供するので、Javaの互換性について確認されます。性能や安定性については直接社内で確認してから導入する予定です。

私はJava v1.3から開発およびインフラ製品をサポートしてきましたが、インターネットの公式・非公式ドキュメントや掲示板を見て面白いと思ったことがあります。それは、当時のサン・マイクロシステムズや現在のオラクルに費用を払って技術サポートを受けるケースはごく稀なのに、上記のような心配の声があることでした。私は、OpenJDKのエバンジェリストではないですが、すでにOSS環境への信頼が市場に形成されており、周りを見ると多くのOSS製品を使ってサービスを構築しています。そのため、OpenJDKを導入することも同じく「OSSを導入する」という程度で考えたらどうかと思います。ただ、いくつか違う点があると思いますので、それについての検討は必要になるでしょう。

OpenJDK導入にあたって考えること

結果から申し上げますと、導入する過程で「大した苦労はなかった」です。いろんなバージョンのOpenJDKがあり、いちいちテストするのに時間がかかったことを除くと、深刻な欠陥や性能偏差などの問題はありませんでした。最後に話をしますが、「生産性」と「費用」の面をより考慮しました。 

OpenJDKの導入を検討したチームが目指したのは、Oracle JDKからOpenJDKに移行した時の問題を掘り出して解決方法を探ることでした。多くのシステムに採用していたJava 8 Oracle JDKからバグやセキュリティーの脆弱性が発見されても、2019年1月以降はオラクルがアップグレードを提供しないことになりました。そのため、Javaのメジャーバージョンのアップグレードは検討しませんでした。OpenJDKへの移行は、Java 8をJava 11に変更することとは違うレベルの問題です。Javaのメジャーバージョンを変更するというのは、ソフトウェア(JDK)の入れ替え以外に、アプリケーションコードで非推奨とするかどうかも考慮すべきです。また、利用しているフレームワークの互換性の検討だけでなく、新しいコードスタイルや改善事項に関するコード開発などを含めて検討が必要です。そのため、JDKの互換性、性能問題よりアプリケーションコードに関しての争点が大きいと思います。

ちなみに、LINEのシステムはOSがきちんと管理されていますので、EOS(End Of Support)となったバージョンや対応しない環境についての悩みもそれほど大きくありませんでした。もしサーバーが古かったり、最近のものであってもアップデートされていないサーバーへのOpenJDKの導入を検討するのであれば、私見ではありますが、「現状を維持し、JDKをこれ以上アップデートしないでください」**。そして、新しいサーバーを構築する際に、OpenJDKを検討することをおすすめします。

2019年1月以前のJDKは、BCL(Oracle Binary Code License)が適用されています。
該当バージョン以降アップデートしなければ、そのまま使い続けられます。
2019年1月をもってOracle Java 8 JDKのパブリックアップデートは終了し、その最後のバージョンは8u202です。 

既存のOracle JDKからOpenJDKに移行する際に、どの配布バージョンを選択するか、どのような方式で互換性や安定性または性能について検証するかを考慮する必要があります。ドキュメントで確認することもあり、特定ツールを使って負荷を検証することもあります。 

OpenJDKには、さまざまな提供元があります。JCP(Java Community Process)によってJSR(Java Specification Request)標準が策定されたため、その内容をもとに各提供元が実装を行います。そして、JSR標準に基づいて開発されたJDKの機能を検証するツールがTCK(Technology Compatibility Kit)です。すでに前々からサン・マイクロシステムズは、ライセンスに問題があるいくつかのモジュール(JFXなど)を除いてOpenJDK communityからJDKコードを提供しており、このソースは公開されています。そのため、他の提供元は、該当ソースを参照したり改善してOpenJDKを配布しています。例えば、 G1 GCの次世代GC(Garbage Collection)として、オラクルはZ GCを実装しており、他の提供元であるRed HatはShenandoah GCを提供します。もちろん、Z GCやShenandoah GC両方ともOpenJDKのプロジェクトとして参加しており、Red HatのOpenJDKをインストールすれば、Java 11だけでなくJava 8までバックポート(backport)で提供されます。まだ未完成のレベルですが、今後の流れによってはOpenJDKで両方のGCを使ったり、提供元によって違うものを使うなど、さまざまな形になるでしょう。

安定性や性能をそれぞれ区別して考慮するには、やや曖昧なところがありますが、目標数値を定めてテストで検証しました。例えば、安定性を確認するために、プログラムを長時間(10時間以上)動作させて問題がないことを確認しました。性能確認は、一部の運用サーバーにOpenJDKを導入し、実際にユーザーからのリクエストを処理しながら負荷を増やして目標値(通常の3倍)でも問題なく動作することを確認しました。

配布バージョンの種類と特徴

以下は、各提供元から配布するOpenJDKのバージョンの種類と特徴についてまとめた表です。

Community/Vendor Product Name OSS/Commercial VM Architecture Description
ORACLE Oracle JDK Commercial Hotspot 2019年1月以降のバージョンは、Java 8を使うためにライセンスが必要です。検討の基準値として使いました。
Azul Zing Commercial Zing Azul独自のアーキテクチャーを持つJVM(Java Virtual Machine)です。最初から商用製品。商用製品であり、またアーキテクチャーが異なることもあり、検討対象から除外しました。
OpenJDK community OpenJDK OSS Hotspot サン・マイクロシステムズ時代に作られたコミュニティーグループで、一般に知られているOpenJDKが作られるところです。コミュニティーがメインなので、ここでソースは配布していますが、バイナリーはhttps://jdk.java.netで配布されています。https://jdk.java.netで配布されているバイナリーは、リファレンスとして使うことをおすすめします(参考)。
Azul Zulu OSS Hotspot Azulがオープンソースで提供するJDKです。AzulのZingとはアーキテクチャーが違います。Zuluは、OpenJDKをもとに開発されます。Azulと別途契約することで、サポートを受けられます。
Red Hat/CentOS OpenJDK OSS Hotspot Red Hatが提供しています。RHEL(Red Hat Enterprise Linux)およびCentOSのyumレポジトリからrpm(バイナリ形式)で配布されます。OSバージョンごとに、対応するOpenJDKのバージョンが異なります。参考:Windows binary
AdoptOpenJDK.net OpenJDK OSS HotspotOpenJ9(IBM) OpenJDKの配布を主な目的とする開発者が運営しているコミュニティーで、さまざまな企業からの後援を受けています。オラクルのサポートでOpenJDK communityがリードするHotspot(openjdk)と、IBMのサポートでEclipse.orgがリードするOpenJ9(IBM)のVMのバイナリーをすべて提供します。現状では、最も多様なプラットフォームとVMアーキテクチャーのバイナリーを提供しています(PC向けのMAC OSを含む)。
Eclipse.org OpenJ9 OSS OpenJ9(IBM) IBMが独自のJDKをEclipse.orgに寄付してから始まったプロジェクトで、VMアーキテクチャーがHotspotとは異なります。IBM系列のJDKを使っていた企業が、OpenJDKの導入を検討する場合、OpenJ9は有望です。Hotspot系列のJVMとOpenJ9(IBM)系列のJVMでは、設定およびアーキテクチャーにおいて多くの違いがあります。アーキテクチャーが異なるため、検討対象から除外しました。

互換性の確認

  • コードの互換性
    「従来のOracle JDKで実行していたコードがOpenJDKで問題なく動くか」について心配することは余計なことかもしれませんが、実際に問題が発生するかもしれません。一般的なウェブ基盤のアプリケーションの場合、前述のTCKによるテストを通ったバージョンでは互換性の問題はないはずです(性能などは置いておいて)。私たちも確認を進めて行くうえで深刻な問題はありませんでした。ただ、一部のコードからJavaFXのデータ型オブジェクトを使っているところが確認されました(OpenJDKでは、ライセンス問題でJavaFXが含まれていません。OpenJFXを別途使うことができます)。WAS(WebSphere Application Server)を起動したらログに例外エラーが多数発生したので、確認してみたら「javafx.util.Pair」を使いました。そのコードを他のオブジェクトを使うように修正して解決しました。


Caused by

java.lang.ClassNotFoundException: javafx.util.Pair
JavaFXを使う場合、OpenJFXを別途ビルドしてOpenJDK classpathに追加するか、該当コードを削除することで問題を解決できます。  

  • JVMの互換性
  • JVMアーキテクチャーの違いにより、JVMオプションが適用できない場合もあります。例えば、Oracle JDKを使っていた環境でG1 GCを使うために「-XX:+UseG1GC」を使った場合は、Eclipse.orgのOpenJ9では該当JVMオプションが適用されないため、WASの起動が失敗することがあります。そのため、VMアーキテクチャーが異なるEclipseのOpenJ9やAzulのZingは、JVMオプションについても考慮が必要です。このような理由があり、JVMアーキテクチャーが異なるAzulのZingとEclipse.orgのOpenJ9を除き、互換性を確認するために以下のテストを行いました。 

      1. OpenJDKを導入したWASが、従来のOracle JDKと同じJVMオプションで起動するか確認
        1. VMが正常に初期化できるか確認
        2. JVMオプションの中で同じ機能で置き換えられないJVMオプションがあるか確認
      2. OpenJDKを導入したWASにWARをデプロイして、起動時に従来にはなかったExceptionが発生するか確認
        1. 従来使用していたClass(JCE-Java Cryptography Extension- など)が漏れていないか確認
        2. Oracle JDKでのみ提供されるClass(JFX-JavaFX-など)を使うことがあるか確認
      3. Alpha環境のWASおよびBeta環境のWASにOpenJDKをインストールして、初期に確認した内容以外に問題がないか確認

     

    安定性および性能の確認

    Javaのマイナーバージョンをアップグレードした後、いきなりJVMプロセスがダウンした経験があります。原因は、JVM内部のバグによるプロセスのクラッシュで、とりあえず元のバージョンをデプロイした後、次のアップデートをデプロイして問題を回避しました。ハードウェアやソフトウェアに初期不良またはバグがないことを期待しますが、現実ではどのハードウェアも故障はありますし、どのソフトウェアもバグはあります。商用製品を使う理由は、このような問題が発生したら適切なサポートが受けられるためで、保険のように導入してメンテナンス契約を結びます。
    OSSの場合、商用製品より多くの情報がより早く公開され、重要な対応はむしろ商用製品より早く解決されるケースが多くあります。もちろん、独自に(企業内部で)解決可能な技術を保有するという前提条件はあります。商用製品の場合は、クリティカルパッチ(hotfix)を提供することもありますが、一般に問題をまとめて次の統合パッチやバージョンアップグレードに反映します。独自の解決能力がない組織の場合、このようなサポートがより安定的だと判断することもあります。また、導入とともにすぐ分かる問題は初期に対応できますが、負荷の増加によるスレッド数の増加やメモリー使用量の増加など、運用しているうちに問題がわかる場合もあります。こうした問題は、新規インストールまたはアップグレードの後、時間が経ってから確認できることなので、サービスを提供する組織にとって致命的な問題になる可能性があります。すでに、数百台のサーバーに導入されているJDKを元に戻す作業は、サービス品質の面やエンジニアの作業量から見て大きな問題になります。そのため、互換性の確認が終わったOpenJDKについて以下のような確認作業を行いました。

    1. JMH(Java Microbenchmark Harness)で、OpenJDK communityが提供するベンチマーキングプログラムと、38のサンプルコードを10~14時間連続で動作させ、以下の内容を確認しました。

      • ベンチマーキングプログラムと、38のサンプルコードを利用して、JVMが正常に動くかを確認します。
      • ベンチマーキングプログラムを10時間以上動作させ、JVMの動作に異常がないか確認します(プロセスのダウンがない、GCログを見て問題がない)。
      • 出力されたベンチマーキングデータをほかのJVMの結果と比較し、OpenJDKの種類による問題はないか確認します。

    すべてのJVMで大きな性能の違いはありませんでした。JMHが提供するサンプルコードの条件(例:負荷量など)は、変更しませんでした。ここでは、38のサンプルとString演算を実行し、その中からいくつかの結果を紹介します。
    ここで紹介する結果は、OpenJDK製品間の性能差を確認するためで、絶対的な数値として扱わないでください。テスト環境によって異なります。

    Multi Thread Benchmark Test Loop Benchmark Test
    Thread Scopeによる(shared/unshared)性能を比較します。ops/sは、1秒当たりの処理件数を意味しており、数値が大きいほど高性能となります。shared/unsharedによる性能の差はあまりなく、全体の処理件数もAdopt OpenJDKを除けば性能の差はあまり見えません。
    多量のループを実行し、性能を比較します。nano sec/opsの数値が小さいほど高性能となります。
    ループ回数が100回の場合は差があるように見えますが、単位がnano sec/opsであり、10万回を見ると、全体的に性能の差はほぼないと思います。

    Blocking Queue Int Benchmark Test String Concatenation Benchmark Test
    Blocking Queueに割り込みをかけて処理にかかる時間を比較します。nano sec/opの値が小さいほど高性能となります。
    単位がnano secにもかかわらず1m sec程度の差があり再測定しましたが、あまり変わりがなかったので、そのまま示しました。
    文字列連結の処理の速さについて、String Builderの場合と「+」演算の場合を比較します。us/opの値が小さいほど高性能となります。
    ほとんどのVMで、「+」演算(StringConcatenation)は、String Builderのappend()(stringBuilderConcatenation)より性能が格段と低くなりました。AzulのZuluの場合はString Builderのappend()を使うと性能が良かっものの、「+」演算では性能が低くなりました。ただ、単位がmicro sec/opであるため、問題はないと判断しました。

     

    2. 運営サーバーの一部で実際にユーザーからのリクエストを処理しました。

    • 正常に動作するかを確認
    • 負荷を高めて(通常の3倍)異常はないか確認し、性能も確認
    • 以下は、LINEのシステムモニタリングツール(IMON)で作成したグラフです。
    • テストの結果、どのOpenJDKでも正常に動作することが確認できました。

     

    3. 運営しているサーバーの一部にOpenJDKを導入してサービスを運営します。

    • LINEでは、実際のサービスにデプロイする前、最後の確認段階でサーバーの一部をCanary groupとして管理します。
    • Canary groupに入っているサーバーにOpenJDKを導入し、異常はないか最終確認をしました。
    • 2019年2月現在で異常はなく、正常に運営中です。

    プロビジョニング(provisioning)

    LINEで使っているサーバーは、プロビジョニングツールであるPMCを利用してJDKをインストールしてからsoftlinkに接続し、JAVA_HOMEの環境変数をsoftlinkで設定して構成されます。こうした管理方法は、サーバーを生成したり増設する際、スピーディーにシステムを構築でき、メンテナンス費用を抑えられるメリットがあります。ただ、今回のTFでは従来のシステムから一部のサーバーだけをOpenJDKに移行する作業を行い、臨時作業のために従来のシステムを調整することはできませんでした。下手をすると運営中のサーバーに問題が起きる可能性があったためです。とはいえ、数十台のサーバーを1台ずつ手作業で移行するのも簡単ではありません。サーバー1台に対して、ターミナルへの接続、コピー、インストール、リンクの変更、必要なjarファイルのコピー、再駆動などを作業するには、いくら速くても15分程度かかり、途中で一つでも漏れがあったら作業は失敗になりかねません。また、デプロイの際には同時に数百台に配布する必要もあります。 

    そこで選んだのが、Ansibleです。AnsibleがRed Hatに買収されてからも、Ansibleはオープンソースで利用できます。この記事でAnsibleについては詳しく書けませんが、Ansibleのメリットを以下のようにいくつか取り上げます

    • YAML形式で作成したPlaybookで、多数のサーバーに設定をまとめて適用できます。
    • 実際の作業はPythonで行われますが、Pythonよりは簡単なYAML形式のAnsible構文を勉強すれば、より簡単に自動化できます。
    • dry-run(Test Run)が可能になります。実際に適用する前に、Ansibleが正常に動作するか確認する作業は重要です。
    • 提供されるAnsibleモジュールを使うと、実行中に失敗しても例外処理が可能となります。

     

    多数のサーバーにインストールしたり設定を行ったりする操作は自動化できましたが、また他の問題がありました。LINEはサーバーにアクセスするためにKerberos認証を使いますが、OpenJDKを/usrの配下にインストールするにはroot権限も必要です。OpenJDKをインストールするだけのために、セキュリティーの根幹を無視することはできませんでした。少なくとも作業するユーザーの権限でサーバーへのアクセス権限を取得しOpenJDKを配布できるようにして、他の人が作業しても履歴が残って問題が発生したら追跡できるようにする必要があります。TFを進めるうえで、私一人で作業できるようにすることも可能でしたが、多数のサーバーを一人で作業することは無理で、今後の状況に応じてOpenJDKを配布できるプラットフォームが必要でした。そこで、Ansible TowerのOSS版であるAWXを採用することにしました。AWXだけの機能も多いのですべて紹介することはできませんので、AWXのもとになったAnsible Towerのサイトをご参考ください。

    この過程で確認した問題と解決方法を紹介します(コードの詳細や構成は含まれていないことご了承ください)。

    認証キーの配布問題:Ansibleが動作するためには、作業対象のサーバーにSSH公開鍵を先に配布しておく必要があります。サーバー台数が少なければ、ターミナルでSSH公開鍵を入れることができますが、作業台数が多くなると1台ずつターミナルでSSH公開鍵を入れることは現実的ではありません。また、手作業でSSH公開鍵を入れる必要があるワークフローでは、作業台数が多くなったときに、自動化の意味が薄れてしまいます。つまり、Ansibleを使ってSSH公開鍵を配布するには、サーバーに接続しなければなりませんが、SSH公開鍵がなくてサーバーに接続できないジレンマ(?)状態になります。

    i. 問題

    1. SSH公開鍵を作業対象のサーバーにインストールする必要があります。
    2. Ansible playbookからIDとpasswordを使って接続しようとすると、セキュリティー問題が発生する可能性があります。

    ii. 解決 

    1. AWXを実行する際、対話型で入力するサーベイ機能を使ってplaybookを実行するたびにKerberos ID/passwordを入力してもらいます。また、セキュリティトークン(Keytab)を生成して対象のサーバーにアクセスします。
    2. 実行するたびに作業する人のアカウント情報を使うため、コードからID/passwordが漏れることも避けられ、監査情報を残すこともできます。

    Inventory構成の自動化問題:Ansible CLI環境でhostsファイルを作成するのも煩わしい作業ですが、AWXのGUIで多数のサーバーをいちいち登録することも大変です。

    i. 問題

    1. 多数のサーバーをGUIでいちいち登録することは大変です。
    2. LINEのサービスにはサービスをグループセット(GroupSet)とグループ(Group)に分ける概念がありますが、この情報を反映するのは非常に煩わしい作業です。

    ii. 解決

    1. AWXのInventory Script機能を使います。Ansible AWXで作業対象サーバーのリストをInventoryと言います。
    2. 社内のサーバー情報を抽出できるAPIサーバーから情報を受け、それをInventoryでインポートできるようにPythonコードを作成しました。
    3. 作成したPythonコードのサンプルは以下のとおりです。セキュリティーの関係で一部の情報を取り除きました。以下のコードのままでは実行できませんが、パーシンググラマー(parsing grammar)を理解するうえで参考になると思います。

    Custom Inventory script

    1. #!/usr/bin/env python
    2. import urllib
    3. import json
    4. import sys
    5. from collections import OrderedDict
    6. # 以下のコードは、LINEの内部情報を取り除いて提供します。提供されたコードのままでは実行できない可能性があります。
    7. # APIからJSON形式のサーバーリストを抽出できる環境の場合、ご自分の形式に合わせて修正が必要です。
    8. # 以下のドキュメントでfileベースのhostsファイルを読み込んで処理する部分も参考できると思います。
    9. # https://docs.ansible.com/ansible/latest/dev_guide/developing_inventory.html
    10. API_SERVER_URI="YOUR_API_SERVER"
    11. PROJECT_ID = "YOUR_PROJECT_NAME"
    12. PROJECT_PHASE = "YOUR_RELEASE"
    13. API_BASE_URL="https://" + API_SERVER_URI + "/" + PROJECT_ID + ":" + PROJECT_PHASE
    14. def getJson(url) :
    15. url = urllib.urlopen(url)
    16. data = url.read()
    17. parsed_data = json.loads(data)
    18. return parsed_data
    19. def getGroupSets():
    20. group_sets_url = API_BASE_URL + "/groupsets"
    21. return getJson(group_sets_url)
    22. def getGroups(groupset_name):
    23. groups_url = API_BASE_URL + "/groupsets/" + groupset_name + "/groups"
    24. return getJson(groups_url)
    25. def getNodes(groupset_name,group_name):
    26. nodes_url = API_BASE_URL + "/groupsets/" + groupset_name + "/groups/" + group_name + "/nodes"
    27. return getJson(nodes_url)
    28. # Ansible AWXまたはAnsible TowerのInventory ScriptでParsing可能な形式に出力する部分です。
    29. # グループにサーバーリストを入れ、グループをグループセットの概念でまとめてくれる形です。
    30. # グループの下にグループを追加する場合、'children'エンティティが使用されます。
    31. # 環境が異なるため、以下のコードのままでは実行できませんが、AWXで分析する形式を理解するうえで参考になると思います。
    32. inventory = OrderedDict()
    33. inventory['all'] = {}
    34. inventory['all']['children'] = []
    35. parsed_groupset_data = getGroupSets()
    36. for groupsets in parsed_groupset_data:
    37. groupset_name = groupsets["name"]
    38. inventory[groupset_name] = {}
    39. inventory[groupset_name]['children'] = []
    40. inventory['all']['children'].append(groupset_name)
    41. parsed_groups_data = getGroups(groupset_name)
    42. for groups in parsed_groups_data:
    43. group_name = groups["name"]
    44. inventory[groupset_name]['children'].append(group_name)
    45. inventory[group_name] = {}
    46. inventory[group_name]['hosts'] = []
    47. parsed_nodes_data = getNodes(groupset_name,group_name)
    48. for node in parsed_nodes_data:
    49. node_name = node["name"]
    50. falg_maintenance = node["attributes"]["state"]
    51. if falg_maintenance == "NORMAL":
    52. inventory[group_name]['hosts'].append(node_name)
    53. # 以下から出力された内容がAWXパーシングフォーマットに合っていれば、Inventoryで生成されます。
    54. print(json.dumps(inventory, ensure_ascii=False, indent=3))
    #!/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 community プロジェクトから提供されます。安定性テストおよびJVMごとの性能比較に使用しました。インストール方法は以下のとおりです。

    1. # mvn archetype:generate \
    2. -DinteractiveMode=false \
    3. -DarchetypeGroupId=org.openjdk.jmh \
    4. -DarchetypeArtifactId=jmh-java-benchmark-archetype \
    5. -DgroupId=org.samples \
    6. -DartifactId=openjdkJMH \
    7. -Dversion=1.0
    8. # cd opnjdkJMH
    9. # mvn clean install
    # 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=”YOUR JVM PATH”を書き換えてください。

    setjdk.sh

    1. #!/bin/bash
    2. VENDOR=$1
    3. YOUR_HOME="YOUR JVM PATH"
    4. if [ "$VENDOR" = "" ]
    5. then
    6. echo "ERROR: Need Vendor NAME [oracle|redhat|adopt|azul]"
    7. echo "Use : setjdk.sh \$VENDOR"
    8. echo "ex : setjdk.sh oracle"
    9. exit 1
    10. fi
    11. case $VENDOR in
    12. oracle)
    13. export JAVA_HOME=${YOUR_HOME}/jdk/jdk1.8.0_181
    14. export PATH=$JAVA_HOME/bin:$PATH
    15. ;;
    16. redhat)
    17. export JAVA_HOME=/usr/lib/jvm/java-1.8.0
    18. export PATH=$JAVA_HOME/bin:$PATH
    19. ;;
    20. adopt)
    21. export JAVA_HOME=${YOUR_HOME}/jdk/jdk8u181-b13
    22. export PATH=$JAVA_HOME/bin:$PATH
    23. ;;
    24. azul)
    25. export JAVA_HOME=${YOUR_HOME}/jdk/zulu8.31.0.1-jdk8.0.181-linux_x64
    26. export PATH=$JAVA_HOME/bin:$PATH
    27. ;;
    28. *)
    29. echo "Need Vendor NAME [oracle|redhat|adopt|azul]";;
    30. esac
    31. echo "=================================="
    32. echo "JAVA_HOME="$JAVA_HOME
    33. echo "JAVA_VERSION="$JAVA_VERSION
    34. `$JAVA_HOME/bin/java -version`
    35. echo "PATH="$PATH
    36. echo "=================================="
    #!/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の種類を選択し、負荷テスト(JMH)を実行するスクリプトは以下のとおりです。

    bmtOracle_all.sh

    1. #!/bin/bash
    2. . ./setjdk.sh oracle
    3. $JAVA_HOME/bin/java -jar ../target/benchmarks.jar \
    4. -rf csv -rff "EL7_All_oraclejdk_1.8_`date '+%Y-%m-%d_%H%M%S'`.csv" \
    5. -jvm "$JAVA_HOME/bin/java" \
    6. -jvmArgs "-Xms4g -Xmx4g -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=45" \
    7. -prof perfnorm
    #!/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は、Oracle JDKから商用サービスとして提供されます。Java Mission ControlのOSSバージョンがJDK Mission Control(JMC)であり、OpenJDK community プロジェクトで開発されています。2019年2月現在、JMC 7 Early-Access Buildsが開発中で、現在リリースされているバージョンで基本的なモニタリングは可能です。JVMのリアルタイムGC情報とスレッド状態をモニタリングするために使用しました。

    GC Viewer

    JVMで生成したGCログを分析するためのツールです。G1 GCログをサポートするツールの中でオープンソースの製品です。UIが商用製品ほどの品質ではありませんが、データ分析用としては十分です。以下の画像は、今回のテストの内容ではなく、他のプロジェクトでメモリー漏れを分析したものです。今回のテストでは、異常がなかったため、分析する内容が特にありませんでした。GC ViewerのGitストレージはこちらから、バイナリーはこちらから確認できます。

    Ansible AWX

    Ansible AWXは、CLI形式で提供されるAnsibleを管理するためのAnsible TowerのOSSバージョンです。OpenShift、Kubernetes、Docker環境で使用するように配布されています。私たちは、Docker compose環境でインストールして使いました。Ansible AWXを利用して作業対象のサーバーのInventory scriptを生成し、SSH公開鍵を配布してOpenJDKをインストールしました。

    確認された問題

    OpenJDKの導入を進める過程で確認された問題をいくつかご紹介します。OpenJDK自体の互換性、性能の問題ではなく、LINEが開発したアプリケーションで使われる一部のクラスがOpenJDKに含まれていないことが原因となった問題でした。

    JCE(Java Cryptography Extension)のインストールの有無 

    Java開発のセキュリティを強化するために、JCEを追加でインストールします。ビット数の高い暗号化に対応するために、プロバイダ情報が含まれたjarファイルをJDKのクラスパスに追加します。Oracle JDKの場合、Java 8_151より古いバージョンではJCEをダウンロードしてインストールする必要がありました。それ以降のバージョンには標準パッケージとして含まれており、以下のように設定することでJCEを有効にできます。

    • 1つ目の方法 : “${JAVA_HOME}/jre/lib/security/java.security”ファイルを開き、”crypto.policy=unlimited”のコメントアウトを外すか、この行がない場合は追加します。 
    • 2つ目の方法 : “Security.setProperty(“crypto.policy”, “unlimited”);”コードを追加して有効にします。

    OpenJDKの場合、Java 8バージョンにJCEが標準搭載され有効になっているので、別途インストールや設定は必要ありません。ただ、プロバイダの違いや実装されたコードをソースレベルで一つ一つレビューするには時間的に無理があるので、これまでと同様にJCEファイルをコピーしました。この処理は、Ansible playbookを利用しました。

    JMX(Java Management Extensions)を使用したTomcat WASの性能情報の収集

    LINEのモニタリングシステム(IMON)は、Tomcatのスレッドの情報などを収集するためにJavaのJMXを使用しており、独自のクラスが実装されています。 

    <Listener className="jp.naver.facility.jmxagent.MonitorLifecycleListener" .....

    OpenJDKでも正しく情報を収集するには、実装されているjarファイルをコピーする必要がありました。ここもOpenJDKをインストールする時に、Ansibleのplaybookで共通ファイルにコピーすることで解決しました。

    JavaFXの問題

    2006年11月、JDKの開発元であるサン・マイクロシステムズ社は、Javaのオープンソース化を決定しました。この過程で、サン・マイクロシステムズ社が直接開発していない一部のライブラリを外してOpenJDKとして提供されました。このとき、JavaFXも外されました。JavaFXは、デスクトップアプリケーションの開発時にGUIやマルチメディアを実装するための標準ライブラリであり、一般的なWebベースのプログラムで使われることはありません。もし必要な場合は、OpenJFXプロジェクトがありますので、ビルドしてインストールすることができます(https://openjdk.java.net/projects/openjfx)。

    通常、Webベースのアプリケーションでは使用しませんが、実際は不必要なJFXクラスを使っている場合がありました。その場合、解決策として以下の2つの方法があります。私たちは2つ目の方法を用いて、コードからJFXクラスを削除して解決しました。

    • 1つ目の方法 : OpenJFXをビルドしてインストールする
    • 2つ目の方法 : 可能であればJFXクラスをコードから削除する

    配布バージョンの選択と注意点

    テストと議論を重ねた結果、私たちはRed Hat社が提供するOpenJDKを使用することを決め、LINEの主要機能(メッセンジャー、認証、チャネルなどの一部)に導入しました。2019年2月現在、順調に運営されています。もちろん、今回テストして導入したバージョンが、運よくバグや不具合がないバージョンだったのかもしれません。または、何らかの不具合が潜んでいても、私たちがその機能を使っていないため表面化していないのかもしれません。どの製品が正解というのはないので、現在の状況でベストと判断されるものを採用したのです。

    最終テスト後に選定したバージョンは、Red Hat CentOS 6、7のOpenjdk 1.8.0_181、191です。
    マイナーバージョンが2つある理由は、最初181でスタートしていて、
    途中で191が公開されたので一緒にテストしたからです。
    ちなみに、2019年2月現在、Java 8向けOracle JDKの最後のパブリックアップデートは、
    「Java SE Development Kit 8u202」です。 

     配布バージョン

    Red HatのOpenJDKを選択したのは、性能や互換性が優れていたからではありません。以下のように、「生産性」と「所有経費」を考慮した結果でした。他の製品も大体同じ性能を出しており、互換性の面でも問題ありませんでした。ただ、皆さんの環境では、私たちが選択したバージョンをそのまま適用できない場合がありますので、ご注意ください。  

    • 生産性(エンジニアの作業の効率性と安定性)
      • yumリポジトリを使用してリリースし、複数のサーバーに簡単にインストールできる。 LINEのサーバーの多くはCentOSをベースにしており、最新版の6、7でしっかり管理されている。Ansibleのyumモジュールなどと組み合わせれば、自動化しやすい。バグのパッチなどの作業はCentOSによって行われる。 
    • 所有経費(人件費、ライセンス費用)
      • CentOSベースなので、追加の導入費用はかからない。独自でビルドする必要がなく、JDKの互換性確認(TCK)、セキュリティ、バグのパッチ、バージョン管理などを行う人材を維持する必要がない。
        今後、OSのアップグレードに合わせてJavaのマイナーバージョンのアップグレードも自然な流れで行われる可能性がある(ただし、安定性のために特定のバージョンに固定する)。
    • 想定できるデメリットと意見
      • デメリット(ゼロデイ脆弱性)
        • 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件だけです(https://www.cvedetails.com/product/23642/Oracle-Openjdk.html?vendor_id=93)。
          もちろん、それ以外のセキュリティパッチは継続的に適用されています。
        • CVEコードで管理すべきレベルのセキュリティ脆弱性は、大体迅速に対策が公開されます。
        • 一般に、JVM(WAS)は外部にサービスを直接提供しません(Webサーバーと比べた場合)。
      • 意見
        • サービスを公開した時にインストールされていたJDKを何年もずっと維持するケースが多くあります。
        • なるべく、OSのアップグレード時またはyumのアップデート時にパッチを反映し、正しく動作するかを確認することを推奨します。

    注意事項

    現在Oracle JDKを使用しており、ライセンス契約を締結する計画がない場合は、注意すべきことがあります。私は専門家ではないので、ライセンスの詳細を解説することはできませんが、現在公開されている内容をもとにまとめてみました。

    • Server
      • 2019年1月に公開されたJava 8のパブリックアップデート(Java SE Development Kit 8u202)以降のバージョンやJava 11などのバージョンは、ライセンスを取得せずにアップデートしてはいけません。OTN(Oracle Technology Network, Oracleのサービスアカウント)を持っていないとアップデートをダウンロードすることができません。たとえ入手できたとしても、それをインストールする行為はOracle社のライセンスポリシーに反することになります。
    • PC/Notebook
      • サーバーと同じ制約があります。2019年2月現在、Java SE Development Kit 8u202をインストールすると、「企業のユーザーは2019年4月になると影響を受けます。」という案内画面が表示されます。
      • Oracle JDK Update agent(auto update)が有効になっている場合は、その設定を無効に切り替えるか、それ以上アップデートが行われないように対策してください。
      • 使用していたJDKを削除して、AdoptOpenJDKからMac/Windows用のHotspot OpenJDKをインストールするのも、解決方法の一つです。
      • Oracle社は、個人ユーザー向けに2020年までJava 8のアップデートを提供します(https://www.oracle.com/technetwork/java/java-se-support-roadmap.html)。しかし、会社のPCやノートパソコン(個人の所有権とは関係なし)は、個人ユーザーではなく商用(Commercial)ユーザーとみなされ、ポリシー違反になり得ます。 

    おわりに

    最後になりましたが、2018年10月から2019年1月までOpenJDKの導入に向けたTFに参加して経験したことをまとめておきます。TFに参加している間、どんな方法論を取り入れるか、どんな方法で検証するか、などを考えてテストすることに多くの時間を費やしました。製品自体に大きな不具合がなくてよかった、と思ったりもしました。冒頭で説明したように、OpenJDKも現在私たちが使っている数多くのオープンソースの一つです。ただ、コードと密接にかかわっているので、修正時に起こり得る不具合が懸念されることもあるでしょう。でも、大量のデータを格納するために使っているHadoopやObject Cacheのために使っているRedis、RDBのMaria DBなど、問題が起こるとさらにクリティカルになりかねない基幹システムをすでに多数利用しているのが現状です。こうしたことを考慮すれば、数十台から数百台までと比較的規模が小さいJVM(JDK)のオープンソース化も、できないことではないと思います。もちろん、内容に不備があってサービスに不具合が発生したりすることがないように、丁寧な現状分析と徹底したテストが欠かせないことを忘れないでください。 

    私たちの経験が、OpenJDKの導入に悩んでいる方々に少しでも参考になれれば幸いです。ありがとうございました。