1954

Thoughts, stories and ideas.

redshift-fake-driverでAmazon Redshiftをモックする

Amazon Redshiftは便利でコスパのよいDWHですが、時間に対する従量課金なので、ちょっとした動作確認のために立ち上げっぱなしにしとくのが気がひける場合もあります。

もしJVMプロジェクトを開発しているなら、redshift-fake-driverを使うことでRedshiftを代替できます。 github.com

What is redshift-fake-driver

Redshift固有のSQL syntaxを無視したり同等のものに置き換えてPostgresqlまたはH2にプロキシする、JDBCドライバーです。

UNLOAD, COPYのようなコマンドもサポートしています。

その場合、Java system propertyでS3エンドポイントを渡すことで、fake-s3のようなS3互換のモックを使うことも可能です。

Usage

では、fake-s3とPostgresqlを使って、使い方を見ていきます。

言語はScalaで、実行はAmmonite REPLで行います。

0. Postgresqlのセットアップ

$ createuser sample_user
$ createdb -O sample_user sample_database

1. fake-s3のセットアップ

$ gem install fakes3
$ fakes3 -r ./fakes3_root -p 9444

2. 依存の追加

redshift-fake-driverは、artfifactにAWS SDKPostgresql driverを含まないため、それらの依存を追加する必要があります。

以降、Ammonite REPLは立ち上げっぱなしと仮定します。

$ amm
scala> import $ivy.`com.amazonaws:aws-java-sdk-s3:1.11.43`
scala> import $ivy.`org.postgresql:postgresql:9.4.1211`

3. バケット作成

scala> import com.amazonaws.auth.BasicAWSCredentials
scala> import com.amazonaws.services.s3.{S3ClientOptions, AmazonS3Client}
scala> val options = S3ClientOptions.builder().setPathStyleAccess(true).build()
scala> val s3Client = new AmazonS3Client(new BasicAWSCredentials("DUMMY", "DUMMY"))
scala> s3Client.setEndpoint("http://localhost:9444/")
scala> s3Client.setS3ClientOptions(options)
scala> s3Client.createBucket("bar")

4. redshift-fake-driverのインストール

scala> import $ivy.`jp.ne.opt::redshift-fake-driver:1.0.7`

Scala以外のJVM言語の場合、artifact名はredshift-fake-driver_2.11などとすればよいです。

また今回fake-s3を使うため、Java system propertyを以下のように設定します。

scala> sys.props.put("fake.awsS3Endpoint", "http://localhost:9444/")
scala> sys.props.put("fake.awsS3Scheme", "s3://")

5. CREATE TABLE

Redshift固有の情報を含んだDDLでテーブルを作成します。

redshift-fake-driverを使うときは、JDBC接続文字列をjdbc:postgresqlredshiftで始めます。

scala> import java.util.Properties
scala> import java.sql._
scala> val url = "jdbc:postgresqlredshift://localhost:5432/sample_database"
scala> val prop = new Properties()
scala> prop.setProperty("driver", "jp.ne.opt.redshiftfake.postgres.FakePostgresqlDriver")
scala> prop.setProperty("user", "sample_user")
scala> Class.forName("jp.ne.opt.redshiftfake.postgres.FakePostgresqlDriver")
scala> val conn = DriverManager.getConnection(url, prop)
scala> val stmt = conn.createStatement()
scala> stmt.execute("""
               |CREATE TABLE foo_bar(a int ENCODE ZSTD, b varchar(255))
               |DISTSTYLE ALL
               |DISTKEY(a)
               |INTERLEAVED SORTKEY(a, b);
               |""".stripMargin)

ちゃんとテーブルが出来ています。

$ psql -U sample_user -d sample_database
sample_database=> \d foo_bar
           Table "public.foo_bar"
 Column |          Type          | Modifiers
--------+------------------------+-----------
 a      | integer                |
 b      | character varying(255) |

6. UNLOAD

まずデータを入れます。

$ psql -U sample_user -d sample_database
sample_database=> INSERT INTO foo_bar (a, b) VALUES (1, 'one');
sample_database=> INSERT INTO foo_bar (a, b) VALUES (2, 'two');

UNLOADを打って、fake-s3のデータを確認します。

scala> stmt.execute("""UNLOAD ('select * from foo_bar') to 's3://bar/result'
         |CREDENTIALS 'aws_access_key_id=DUMMY;aws_secret_access_key=DUMMY'
         |ADDQUOTES""".stripMargin)

ちゃんとデータが出来ていることがわかります。

f:id:ocadaruma:20180428190123p:plain

Redis cluster + lettuceでmget/msetすると複数リクエスト飛ぶ

Redis clusterに対してmget/msetしたとき、slotが異なるものが混在しているとCROSSSLOT errorが返ります。

したがってhash tagを用いてslotを指定するか、まとめたい単位でhash型のvalueに突っ込むなどのkey設計にする必要があります。

Redis Cluster Specification – Redis

いっぽう、lettuce(async対応のJava Redis client)では、mset/mget中にslotの異なるkeyが混在している場合、エラーを返すのではなく「slotごとにリクエストを投げる」実装になっています。(2018/4/4時点)

github.com

なので、key設計ミスっててもちょっと動かす分には問題なくて、負荷をかけた時にめっちゃリクエストが飛んでパフォーマンス出なくてミスに気づく、みたいなことになりえます。(それでハマった)

goofysを使ってS3以外のストレージをfstabでマウントする

goofysはAmazon S3またはAPI互換なストレージをfile systemとしてマウントできるツールです。

github.com

起動時に自動的にマウントするにはREADMEにあるように/etc/fstabへ追記すればよいですが、S3以外のストレージを使う場合は、以下のようにendpointを指定すればOKです。

goofys#your-bucket   /path/to/mountpoint        fuse     _netdev,allow_other,--file-mode=0666,--endpoint=https://some-s3-compatible-storage.com    0       0

AIKATSU! STREAMというChrome extensionを作った

生産性をあげるため、アイカツ!のライブシーンを延々と再生し続けるAIKATSU! STREAMというChrome extensionを作りました。

dアニメストアへのログインが必要です)

chrome.google.com

とはいえ、jsで動画を解析してライブシーンを抜き出すようなことをしてるのではなく、各話のライブシーンの開始/終了をあらかじめ(人力で)抜き出してあって、それを元にシークしているだけです。

いまのところプレイリストに含まれるのは第一部のみです。dアニメストアには第二部〜スターズもあるので、やる気が出たら追加します。

そして後から気づきましたが、YouTubeaikatsuTV channelでも十分だったかもしれない...

2017年の振り返り

1-3月

4-6月

7-9月

  • オプトを退職
    • 2016年1月から在籍していたオプトを退職しました。
    • とても暖かく送り出していただき、感謝の念に堪えません。
    • 実はオプトのOSSのメンテナーは続けてたりします。

10-12月

  • LINE株式会社に入社
    • 1ヶ月有休消化したのち、10月からLINEで働き始めました。
    • 強い人たちばかりで、がんばるぞいという気持ちです。
    • アドカレ記事を書いたりしました。 engineering.linecorp.com
  • ISUCON7予選に参加
    • 初ISUCON参加でした。
    • 結構色々なチームがハマってたのと同じく、帯域律速を突破できずにあえなく敗退...
    • でもまた参加したい。

反省点

  • 音楽を全然やっていない。

来年

  • 仕事頑張る
  • 音楽やる
  • なんかすごいもの作る
    • 自分は音楽は聞くのもやるのも好きなんですが、音ゲーは全然っていう性質でして、そんな自分がハマれる音ゲーを作りたい。
    • いちおう2016年にspeedballっていうアプリ作ったんですが、これはまったく面白くなかったので...(実際の楽譜がそのまま音ゲーの譜面、っていうコンセプトを入れたいだけだった)

JavaとしてもScalaとしてもコンパイルできるHelloWorld

タイトルの通り、以下のコードはJavaとしてもScalaとしてもコンパイルできます。

/*/**/
class JavaMain {
    public static void main(String args[]) {
        System.out.println("Hello, World");
    }
}
// */

/*/**/
class App {}
// */

/*/**/
class A {
    int object = 1;
    int ScalaMain = 1;

    void foo() {
        int i =
        // */
        object
        /*/**/
        ;

        i = // */
        ScalaMain
        /*/**/
        ;
    }

    class B // */
    extends App {
        /*/**/
        void bar() { // */
            System.out.println("Hello, World");
        /*/**/ } // */
    }
/*/**/
}
// */

↑をPolyglot.java, Polyglot.scalaとして保存して試してみましょう。

$ javac Polyglot.java
$ java JavaMain
Hello, World
$ rm -f *.class
$ scalac Polyglot.scala
$ scala ScalaMain
Hello, World

わぁい動いた!

Java, Scalaのバージョンは以下です。

$ javac -version
javac 1.8.0_77
$ scalac -version
Scala compiler version 2.12.3 -- Copyright 2002-2017, LAMP/EPFL and Lightbend, Inc.

解説

ScalaJavaにおける、ブロックコメントの挙動の違いがポイントです。

Scalaでは、ブロックコメントをネストさせた場合に/**/の対応をきちんと取りますが、Javaでは、何個/*を開こうが、*/が来た時点でブロックコメントが終了します。

Java:

/* -- (1)
  /*
   */ -- (1)が閉じる
 */ -- 余ってるのでsyntax error

Scala:

/* -- (1)
  /* -- (2)
   */ -- (2)
 */ -- (1)

これを利用すると、javacにだけ認識されてscalacには無視されるコードを書くことができます。

いっぽう、scalacにだけ認識されるコードを書くのは難しいので、javacでコンパイルできるようにうまいこと色々足しています。

  • L2..L6: javacだけに認識されるMainクラス
  • L10: scala.Appを使ってScalaのMainクラスを作るので、javacで通るように空のAppを用意しておく
  • L14..L39: javacで通るように色々挟みながら、object ScalaMain extends App { System.out.println("Hello, World"); }と書いている

まとめ

今回のコードはもちろんGroovyでもコンパイルできるので、Java, Scala, GroovyのPolyglotができたことになります。

同じ感じでKotlinもいけるかな?

Ammonite REPLで任意のclasspathを追加する

MavenやGradleプロジェクトでも、sbtみたいに手軽にREPLで動作確認したいときがあります。

お手元の開発環境にAmmonite REPLを入れておけば、以下のようにScalaを使って動作確認できるので便利。

$ amm
Loading...
Welcome to the Ammonite Repl 1.0.2
(Scala 2.12.3 Java 1.8.0_144)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ interp.load.cp(ammonite.ops.Path("/path/to/classes"))

@ import com.example.MyClass

/path/to/classesは、Mavenプロジェクトだったらtarget/classesだし、Gradleプロジェクトだったらbuild/classes/mainあたりになるでしょう。

ソース書き換えたらまた読み込み直さないといけないのが面倒ではありますが。