astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

Excelとscalaのケースクラス間で入出力ができるライブラリをgithubに公開しました

f:id:astamuse:20180731192645j:plain scalaでバックエンドを開発しているaxtstar(@axtstart)です。

今回は、Excelをプログラムから扱うにあたって、面倒だなと感じていた部分をバインド変数の様なアイデアで少し楽にするライブラリを作成、公開しましたのでそちらの紹介をします。

面倒な話

Excelからデータを取得するのってなくなりそうで無くならないですよね。

例えばこんなExcelシートからデータを取得したいとします。

↓こんなの(data.xlsxとします)

f:id:astamuse:20180727120440p:plain

apache-poiを使ったプログラムの場合、下記のようなコードになると思います。

import java.io.File
import org.apache.poi.ss.usermodel._

val workbook = WorkbookFactory.create(new File("data.xlsx"))

val sheet = workbook.getSheetAt(0)

val numeric = sheet.getRow(0).getCell(1).getNumericCellValue // numeric: Double = 111.0
val stringRow = sheet.getRow(1).getCell(1).getStringCellValue // stringRow: String = 111
val dateRow = sheet.getRow(2).getCell(1).getDateCellValue // dateRow: java.util.Date = Thu Jan 01 00:00:00 JST 1970
val formula = sheet.getRow(3).getCell(1).getStringCellValue // formula: String = 111
val bool = sheet.getRow(4).getCell(1).getBooleanCellValue// bool: Boolean = true
val time = sheet.getRow(5).getCell(1).getDateCellValue // time: java.util.Date = Sun Dec 31 17:25:47 JST 1899
val userDate = sheet.getRow(6).getCell(1).getDateCellValue // userDate: java.util.Date = Mon Jul 02 22:35:54 JST 2018

このようにロケーションの指定や、データ型毎の変換など、正確にデータを取得するのは結構大変な作業です。

また、Excelとの突き合わせが、行、列番号とのマッピングになるため、間違いやすいです。

場所がずれるなんてことも結構おこります。

Asta4e

そこで、弊社製Webフレームワークであるasta4dを参考にして、似たような*1テンプレート入出力の機能をapache-poiでExcelを使って行うライブラリを作成しました。

ソースはgithubで公開し、Maven Centralにおいてますのでdependencyに下記のように追加すれば試すことができます。

build.sbtの場合(scala2.11 scala2.12 共通)

libraryDependencies += "com.axtstar" %% "asta4e" % "0.0.6"

mavenの場合

scala2.11

<dependency>
    <groupId>com.axtstar</groupId>
    <artifactId>asta4e_2.11</artifactId>
    <version>0.0.6</version>
</dependency>

scala2.12

<dependency>
    <groupId>com.axtstar</groupId>
    <artifactId>asta4e_2.12</artifactId>
    <version>0.0.6</version>
</dependency>

テンプレートになるExcelシートに、変数となる値を${変数名}で記載します。

↓こんなかんじ(template.xlsxとします)

f:id:astamuse:20180727120001p:plain

データを取得したいExcelを指定します、先ほどの変数の場所に実際の値が入っているもの(つまりdata.xlsx)を、以下の要領で取得できます。

import com.axtstar.asta4e.ExcelMapper

val result = ExcelMapper.getData("template.xlsx" // 変数テンプレート(バインドする変数名を記載したExcel)
  ,"data.xlsx" // 入力用データ(読み込みたいExcel)
  ,List() // 無視するシート名(読み込まないシート名のリスト)
)

resultは下記のようになっています。

result: IndexedSeq[(String, Map[String,Any])] = 
Vector((
Sheet1,
Map(
userDate -> Mon Jul 02 22:35:54 JST 2018, 
string -> 111, 
bool -> true, 
date -> Thu Jan 01 00:00:00 JST 1970, 
formula -> 111, 
numeric -> 111.0, 
time -> Sun Dec 31 17:25:47 JST 1899)))

${変数名}で指定した変数名に対して、その位置上の値を取得格納し、 { シート名 ->Map(バインド変数名、値)} を返します。

こうすることで、セル位置の指定をコードから排除し、位置の指定をExcel上で行うようにして、生産性を向上することができます。

テンプレートエンジン機能

Excelを出力したい場合は、上記の考え方と同様にテンプレートのExcel上のバインド変数の位置にデータを出力できる関数を用意しました。(テンプレートエンジン機能)

ただ、出力はレイアウトのことを考えて、出力用フォーマットのExcelを別途指定して、そちらからコピーして出力を行います。*2

↓出力用フォーマット(レイアウト)

f:id:astamuse:20180731185046p:plain

import com.axtstar.asta4e.ExcelMapper

ExcelMapper.setData(
  "template.xlsx" // 変数テンプレートパス
  ,"layout.xlsx" // 出力用フォーマットExcelパス
  ,"output.xlsx" // 出力先のパス
  , result :_* // 出力用データ
)

↓出力結果

f:id:astamuse:20180731195036p:plain

ケースクラス変換

scalaではエンティティの定義をケースクラスで行うことがほとんどだと思いますので、Excelとケースクラスの相互変換の関数も用意しておきました。

テンプレートExcelと同じ変数名*3を持つケースクラスを作成します。

import java.util.Date

case class Data(
              numeric: Double,
              string:String,
              date:Date,
              formula:String,
              bool:Boolean,
              time:Date,
              userDate:Date
)

Excel → ケースクラス

下記の方法で先程のExcelからケースクラスを取得できます。

val data = ExcelMapper.by[Data].getDataAsAny("template.xlsx" // バインド変数テンプレート
  ,"data.xlsx" // 入力用データ
  ,List() // 無視するシート名
)

結果は下記です

data: IndexedSeq[(String, Option[Data])] = Vector((Sheet1,
Some(
Data(111.0,
111,
Thu Jan 01 00:00:00 JST 1970,
111,
true,
Sun Dec 31 17:25:47 JST 1899,
Mon Jul 02 22:35:54 JST 2018)
)
))

少しわかりにくいですが構造的に

Sheet名 --+ Option(ケースクラス) のリスト構造を持っています。

ケースクラス → Excel

こちらは逆変換、ケースクラスを含むIndexedSeq[(String, Option[ケースクラス])]を指定してExcelファイルの出力を行います。

ExcelMapper.By[Data].setData4cc("template.xlsx" // バインド変数テンプレート
  ,"layout.xlsx" // 出力用フォーマットExcelパス
  ,"output.xlsx" // 出力パス
  , data
)

上記でExcelファイルの作成ができます。

全体イメージ

f:id:astamuse:20180801023957p:plain

まとめ

Excelでの開発がだいぶ楽になりました。

いかがでしたでしょうか?

アスタミューゼでは現在、エンジニア・デザイナーを募集中です。 興味のある方はぜひ下記バナーからご応募ください。

*1:Asta4dでは、CSSセレクタを使って、HTML要素にアクセスすることができます。

*2:変数テンプレートは位置だけを指定、実際のフォーマットはレイアウトである出力用フォーマットで指定する考え方

*3:今の実装だとケースクラスの変数が過剰にある場合は動作せずNoneが帰ります。不足している場合は動作します。

Java9でDeprecatedになったfinalize()をjava.lang.ref.Cleanerで代替する

f:id:astamuse:20180722232243p:plain
はじめまして、お初にお目にかかります。
4月に入社したomiと申します。現在開発部で唯一の女子部員です。
優しく愉快な先輩方に囲まれて、日々感謝の気持ちで胸がいっぱいになりながら楽しく過ごしています。

さて、Javaの話をします。
弊社のJavaシステムの中で、Java9でDeprecated対象となったjava.lang.Object.finalize()を使用している箇所があり、同じくJava9で追加されたjava.lang.ref.Cleanerを用いて代替する方法を検討しましたので、その内容をご紹介します。

そもそもfinalizeのような機構のものを使うべきでない、という話は置いときます。
いろいろな事情でしょうがなく・もしくは意図的にfinalize()を使ってきた人たちのための代替方法です。

finalize()について

java.lang.Object.finalize()は、このオブジェクトへの参照はもうないとガベージ・コレクションによって判断されたときに、ガベージ・コレクタによって呼び出されるメソッドです。(公式API参照)
finalize()内にリソースのクローズ処理を記述しておくことで、GCのタイミングでリソースの破棄前にクローズ処理を実行させることができるものです。(とは言ってもご存知の通りfinalize()は呼ばれるタイミングが不定、確実に呼ばれる保証はない、など多くの問題があります)

 実際に動きを確認してみます。

public class TestFinalize {
    public static void main (String[] args) throws Exception {
        Finalize s = new Finalize();
        // using resource
        s = null; // 参照の削除
        System.out.println("System.gc()-start");
        System.gc(); // 強制GC
        System.out.println("System.gc()-end");
    }
}

class Finalize {
    public Finalize() {
        System.out.println("constructor");
    }

    public void close() {
        System.out.println("close");
    }
    
    @Override
    protected void finalize() throws Throwable{
        try{
            System.out.println("finalize()");
            this.close();
        } finally {
            super.finalize();
        }
    }
}

結果はこのようになりました。

$ java TestFinalize
constructor
System.gc()-start
System.gc()-end
finalize()
close

GCが走ったタイミングでfinalize()が呼ばれ、記述しておいたclose処理が実行されていることがわかります。

Cleanerについて

java.lang.ref.Cleanerは、クローズ対象のオブジェクトと対応するクリーニングアクション(Runnable実装クラス)を登録しておくことで、オブジェクトがルートセットから参照されなくなった時(PhantomReachableになった時)に該当クリーニングアクションを呼び出してくれるものです。(公式API参照)

CleanerによるRunnable実行(クリーニングアクション実行)タイミングには2通りあります。

  • Cleaner.Cleanable.clean()メソッドが明示的に呼び出された時
  • 上記clean()の呼び出しがされておらず、オブジェクトがPhantomReachableになった時

前者はAutoCloseableを継承しオーバーライドしたclose()メソッドにCleanable.clean()呼び出しを記述しておけば、実質try-with-resourcesにおいてリソースがスコープから外れた際に自動でRunnable実行しクローズを行うよう記述することができます。(実装は公式API参照)これは「最も効率的な方法」と書かれています。
(しかし従来通り、オーバーライドしたclose()メソッドに直接クローズ処理を記載しておけば、わざわざCleanerを実装する必要がないように思います。。??私には需要がわかりませぬ。。だれか教えてください。。)

今回はfinalize()の代替として同じ動きを再現したいので、後者のタイミングでのクローズ処理について動きを確認してみます。
クローズ処理を記述するRunnable実装クラスを作っておき、コンストラクタでCleaner.registerメソッドを呼び出し、オブジェクトと対応Runnableを登録しておきます。

import java.lang.ref.Cleaner;

public class TestClean {
    public static void main (String[] args) throws Exception {
        Clean s = new Clean();
        // using resource
        s = null; // 参照の削除
        System.out.println("System.gc()-start");
        System.gc(); // 強制GC
        System.out.println("System.gc()-end");
    }
}

class Clean {
    private CloseObject obj = new CloseObject();
    private static final Cleaner cleaner = Cleaner.create();
    private final CloseRunnable closeRunnable;
    private final Cleaner.Cleanable cleanable;

    public Clean() {
        System.out.println("constructor");
        this.closeRunnable = new CloseRunnable(obj);
        this.cleanable = cleaner.register(this, closeRunnable);
    }
}

class CloseObject {
    public void close() {
        System.out.println("close");
    }
}

class CloseRunnable implements Runnable{
    CloseObject obj;
    CloseRunnable (CloseObject obj) {
        this.obj = obj;
    }
    public void run() {
        System.out.println("Runnable.run()");
        obj.close();
    }
}

結果はこのようになりました。

$ java TestClean
constructor
System.gc()-start
System.gc()-end
Runnable.run()
close

finalize()同様、GCが走ったタイミングで登録しておいたRunnableが呼ばれ、記述しておいたclose処理が行われていることがわかります。

finalize()とCleanerとメモリ解放について

finalize()やCleanerを使用する上で留意しておかなければならないことがあります。
finalize()、Cleanerによるメモリ解放は、Full GCが少なくとも2回以上発生しないと実現しない、ということです。
finalize()、Cleanerのメモリ解放の流れは以下の通りです。

  1. ルートセットからオブジェクト参照がなくなる。
  2. Full GCが実行される。(オブジェクトがPhantomReachableになる)
  3. (PhantomReachableになったのを検知し、)FinalizerやCleanerの処理が実行される。
  4. (オブジェクトがReferenceQueに格納され、)オブジェクトが削除対象となる。
  5. 次に起こるFull GCでオブジェクト削除され、メモリ解放される。

※()内は、少し難しいので読み飛ばして頂いて結構です。
GCによるクリーンアップはjava.lang.refパッケージの各種Reference達やReferenceQueを用い制御することができます。
PhantomReachable:オブジェクトの参照レベルが、一番弱い参照レベルのPhantomReferenceのみになった状態。
参照レベルによってオブジェクトがReferenceQueに格納されるタイミングが異なります。Phantomの場合はオブジェクトがfinalize()処理された後にReferenceQueに入ります。Queに格納対象となったオブジェクトは次GCで削除されます。

実際に確認してみます。まずfinalize()実行時のメモリ解放。
メモリの増減が分かりやすいようにインスタンスを大量生産しています。

import java.util.concurrent.TimeUnit;
import java.lang.management.MemoryMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.MemoryUsage;

public class TestFinalizeMemoryCheck {
    public static void main (String[] args) throws Exception {
        check("初期値");
        // using resource
        for(int i = 0 ; i < 100000 ; i++) {
            Finalize s = new Finalize();
            s = null; // 参照の削除
        }
        check("インスタンスnew後");
        System.out.println("System.gc()-1回目");
        System.gc(); // 強制GC
        check("GC-1回目後");
        TimeUnit.SECONDS.sleep(3);
        System.out.println("System.gc()-2回目");
        System.gc(); // 強制GC
        check("GC-2回目後");
    }

    // 使用メモリを計測
    public static void check(String str){
        StringBuilder buff = new StringBuilder();
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage usage = memoryBean.getHeapMemoryUsage();
        buff.append("usedmemory=");
        buff.append(usage.getUsed());
        System.out.println(buff.toString()+ " : " + str);
    }
}

class Finalize {
    public Finalize() {
        // System.out.println("constructor");
    }

    public void close() {
        // System.out.println("close");
    }

    @Override
    protected void finalize() throws Throwable{
        try{
            this.close();
        } finally {
            super.finalize();
        }
    }
}

結果はこうなりました。

$ java TestFinalizeMemoryCheck
usedmemory=2684392 : 初期値
usedmemory=8053248 : インスタンスnew後
System.gc()-1回目
usedmemory=7273832 : GC-1回目後
System.gc()-2回目
usedmemory=1674040 : GC-2回目後

2回目のGCでメモリ解放されています。
同様に、Cleanerの場合は

import java.util.concurrent.TimeUnit;
import java.lang.ref.Cleaner;
import java.lang.management.MemoryMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.MemoryUsage;

public class TestCleanerMemoryCheck {
    public static void main (String[] args) throws Exception {
        check("初期値");
        // using resource
        for(int i = 0 ; i < 100000 ; i++) {
            Clean s = new Clean();
            s = null; // 参照の削除
        }
        check("インスタンスnew後");
        System.out.println("System.gc()-1回目");
        System.gc(); // 強制GC
        check("GC-1回目後");
        TimeUnit.SECONDS.sleep(3);
        System.out.println("System.gc()-2回目");
        System.gc(); // 強制GC
        check("GC-2回目後");
    }

    // 使用メモリを計測
    public static void check(String str){
        StringBuilder buff = new StringBuilder();
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage usage = memoryBean.getHeapMemoryUsage();
        buff.append("usedmemory=");
        buff.append(usage.getUsed());
        System.out.println(buff.toString()+ " : " + str);
    }
}

class Clean {
    private CloseObject obj = new CloseObject();
    private static final Cleaner cleaner = Cleaner.create();
    private final CloseRunnable closeRunnable;
    private final Cleaner.Cleanable cleanable;

    public Clean() {
        // System.out.println("constructor");
        this.closeRunnable = new CloseRunnable(obj);
        this.cleanable = cleaner.register(this, closeRunnable);
    }
}

class CloseObject {
    public void close() {
        // System.out.println("close");
    }
}

class CloseRunnable implements Runnable{
    CloseObject obj;
    CloseRunnable (CloseObject obj) {
        this.obj = obj;
    }
    public void run() {
        // System.out.println("Runnable.run()");
        obj.close();
    }
}

結果は

$ java TestCleanerMemoryCheck
usedmemory=2821744 : 初期値
usedmemory=13617704 : インスタンスnew後
System.gc()-1回目
usedmemory=9668736 : GC-1回目後
System.gc()-2回目
usedmemory=2118416 : GC-2回目後

同じく2回目のGCでメモリ解放されています。
Full GCが起こる頻度にも気をつけなければいけないということですね。

おわりに

とうとう死亡フラグがたったfinalize()の代替方法について検討してみました。
何らかの理由でfinalize()を使っていた方のとりあえずの回避策として参考になれば嬉しいです。

最後に、アスタミューゼではエンジニア・デザイナーを大募集しています!
少しでも気になった方は下のバナーから是非採用サイトをご覧下さい!優しくて愉快な先輩方と私があなたをお待ちしてます!わくわく!
では。サリュ!

アクセスログ収集用Elasticsearchの運用話

こんにちは。インフラ・開発部のtorigakiです。

早いもので2回目の投稿となります。

弊社はアクセスログの収集・解析にElasticsearchを使用しているのですが、今回はこのElasticsearchの運用環境について書きたいと思います。

システム構成

Elasticsearch環境は以下となります。

  • GCPの仮想インスタンスで構築
  • Elasticsearchサーバー3台(1クラスタ)+運用管理サーバー1台
  • Elasticsearchサーバー情報(3台)
    • Elasticsearchのバージョンは2.4.6。
OS CPUコア数 メモリ HDD
Ubuntu 16.04.4 8コア           8GB 200GB
  • 運用管理用サーバーはCPU:1コア、メモリ:2GB。

f:id:astamuse:20180717114148p:plain

  • NginxのアクセスログをFluentd経由でElasticsearchに送信。
  • ElasticsearchはKibanaの他にJavaのアプリからも使用しています。
  • Elasticsearchの運用スクリプト(バックアップ等)は運用管理サーバーから実行しています。

運用管理サーバーの役割

Nginxでバランシング

Nginxで以下アクセスをバランシングしています。

  • kibanaへのアクセス
  • Elasticsearchの運用スクリプト(過去データ削除、バックアップ、インデックス作成)

※3台のうちどのノードが落ちたときでもスクリプトを実行できるようにするため、Elasticsearchへのアクセスはバランサーを通すようにしています。

Elasticsearch運用

以下を実行するスクリプトを用意して、cronで定期実行するようしています。

  • index(alias)作成

    • 月末に来月分のindex(alias)を作成します。(例:logstash-2018.XX)
  • 過去データ削除

    • Curatorを使って定期的に削除しています。

Curatorの設定は以下ようにしています。

actions:
  1:
    action: delete_indices
    description: "Delete logstash indices"
    options:
      ignore_empty_list: True
      continue_if_exception: False
      disable_action: False
    filters:
    - filtertype: pattern
      kind: prefix
      value: logstash-
      exclude:
    - filtertype: age
      source: name
      direction: older
      timestring: '%Y.%m'
      unit: days
      unit_count: XXX
      exclude:

「unit_count」に保持日数を設定します。

上記のymlファイルを以下のコマンドでcronで定期実行するように設定します。

/usr/local/bin/curator delete_indices.yml
  • バックアップ
    • elasticsearch-dumpを使ってJSONファイルとして保存し、GCSにアップロードしています。

elasticsearch-dumpを実行するスクリプトは以下のようにして、analyzer、mapping、dataをJSONファイルとして個別に保存し、gzipで圧縮するようにしてあります。

  /usr/local/bin/elasticdump \
    --input=http://localhost:9200/${index} \
    --output=$ \
    --type=analyzer \
    | gzip > ${BACKUP_DIR}/$1/analyzer.json.gz

  /usr/local/bin/elasticdump \
    --input=http://localhost:9200/${index} \
    --output=$ \
    --type=mapping \
    | gzip > ${BACKUP_DIR}/$1/mapping.json.gz

  /usr/local/bin/elasticdump \
    --input=http://localhost:9200/${index} \
    --output=$ \
    --type=data \
    | gzip > ${BACKUP_DIR}/$1/data.json.gz

環境の移設

このアクセスログ収集用Elasticsearchは他環境から移設してきたのですが、移設時の切り替えで行った作業内容について簡単に書きたいと思います。

f:id:astamuse:20180717114448p:plain

事前準備

  • 旧環境と同一のテンプレートを新環境のElasticsearchにインポートしておきます。
  • 新環境のElasticsearchに過去データをコピーしておきます。
    • データコピーにはelasticsearch-dump使用してクラスタ間でダイレクトコピーしました。
  • コピーしたデータがKinbanaで問題なく表示できることを確認しておきます。
  • 新・旧両方のElasticsearchに同一データを送信するためのFluentdのコンフィグファイルを準備しておきます。

作業手順

  1. Fluentdを停止します。
  2. 新環境のElasticsearchに、最新データをコピーします。
  3. Fluentdのコンフィグファイルを新・旧同時送信用のコンフィグに差し替えます。
  4. Fluentdを起動します。
  5. 新・旧のElasticsearchに新規の同一データが送信されていることを確認します。

作業後

  • 1ヶ月は新・旧並行運用して様子を見ます。
  • Javaアプリの接続先を新環境に切り替えます。

まとめ

今回はElasticsearchの運用で工夫した点について紹介させていただきました。

少しでもElasticsearchの運用されている方のお役に立てれば幸いです。

弊社では引き続きエンジニア・デザイナーを募集中ですので、ご興味のある方は下からご応募いただければと思います。

Copyright © astamuse company, ltd. all rights reserved.