Vue.js でウィジェットっぽいもの (仮) その2

前回の続き

Vue.js でウィジェットっぽいものを作ってみる (仮) - kntmr-blog

続編として今回は以下をやってみようかと。

  • Webpack で出力するファイルをまとめる
  • ウィジェットにパラメータを渡せるようにする
  • 任意のタグ/キーワードで表示できるようにする

Webpack で出力するファイルをまとめる

vue-cli の Webpack テンプレートでは CommonsChunkPlugin プラグインがデフォルトで設定されている。普通はこのままでよいが、今回はウィジェットとして作成するので、HTMLから読み込むファイルをまとめたい。

出力ファイルの設定は webpack.prod.conf.js に記載されている。で、plugins の中に記載されている webpack.optimize.CommonsChunkPlugin の箇所をコメントアウトする。尚、デフォルトで出力される vendor と manifest についてはまだよく分かっていません...。

あと、CommonsChunkPlugin プラグインについては、以下が参考になりました。

qiita.com

ウィジェットにパラメータを渡せるようにする

以下が参考になりました。Vue インスタンス生成時に render 関数の中で App.vue の props に渡すというもの。

qiita.com

今回は、HTML に data 属性でパラメータを設定する。

<div id="app" data-tag="vue.js"></div>

で、main.js の中で取得して store に格納する。

new Vue({
  el: '#app',
  router,
  store,
  template: '<App/>',
  components: { App },
  render: function (createElement) {
    const dataset = this.$el.dataset
    this.$store.dispatch('setQuery', dataset.tag)
    return createElement('App')
  }
})

その他、Vue インスタンス生成前にやるなら以下でも可。

import store from './store'

const element = document.getElementById('app')
const dataset = element.dataset
store.dispatch('setQuery', dataset.tag)

任意のタグ/キーワードで表示できるようにする

上記、store に格納したパラメータを API アクセス時にクエリで渡す。とりあえず、これで任意のタグでウィジェットを表示できる。キーワードも同様にできると思うので今回は省略。

import axios from 'axios'

const api = {
  get: (url) => return axios.get(url)
}
export default {
  getItems: (query) => api.get(`https://qiita.com/api/v2/items?query=${query}`).then(resp => {
    return Promise.resolve(resp.data)
  })
}

あと、store にデータとクエリを一緒に持たせるのはどうなんだろうか。個人的には悪くない気もするけど...。

Vue.js でウィジェットっぽいものを作ってみる (仮)

Qiita Widget : Qiitaの投稿を表示できるブログパーツ

こんな感じのウィジェットっぽいものを、Vue.js と Veux の学習を兼ねて作ってみようかと。今回は Qiita の Vue.js タグを表示するもの。

github.com


以下、備忘録。

vue-cli

vue-cli の Webpack テンプレートを使用してプロジェクトを作成する。

npm install -g vue-cli

最小構成でやるのでテストとかはないです。

vue init webpack vue-widget-sample
cd vue-widget-sample
npm install

es6-promise

IE 対応のため、es6-promise をインストール。IE では Promise が使えない。

npm install --save es6-promise

src/main.js の先頭に以下を追加。

import 'es6-promise/auto'

Vuex

本家サイト(ja)GitHub の examples を参考に実装。当初、下図が全然ピンと来ていなかったのですが、コードを書いてみてようやく理解できてきました。(たぶん)

https://vuex.vuejs.org/vuex.png

ドキュメントに書いてありますが、小規模なアプリであればイベントバスで十分かもしれません。もしくは、props だけでイケるかもしれません。が、ある程度は規模に関係なく Vuex は導入してよいかと思います。このアーキテクチャに沿うことで全体的にシンプルに書けるようになると感じています。

あと、以下が参考になりました。

chibinowa.net

ウィジェット

こんな感じで読み込む。このあたりはまだ改善の余地あり...。

<!DOCTYPE html>
<html>
<head>
  <meta charset=utf-8>
  <title>vue-widget-sample</title>
  <style type=text/css>
    .my-widget {
      width: 360px;
      height: 400px;
    }
  </style>
  <link href="/css/app.css" rel="stylesheet">
</head>
<body>
  <div id=app></div>
  <script type="text/javascript" src="/js/manifest.js"></script>
  <script type="text/javascript" src="/js/vendor.js"></script>
  <script type="text/javascript" src="/js/app.js"></script>
</body>
</html>

これからやりたいこと

  • Webpack で出力するファイルをまとめる
  • ウィジェットにパラメータを渡せるようにする
  • 任意のタグ/キーワードで表示できるようにする

今さらながら Selenide を試してみる

簡易なテストページを作って試してみました。

サンプルコード


以下、備忘録。

各ブラウザの WebDriver をダウンロードして配置する。

Third Party Browser Drivers - Downloads

テスト対象のブラウザを設定する。

{
    Configuration.browser = WebDriverRunner.CHROME;
    System.setProperty("webdriver.chrome.driver", "driver/chromedriver.exe"); // WebDriver のパス
}

テストコードからは jQuery のようにセレクタを使って要素を指定する。

open("http://localhost:8080/");
$(By.name("input")).val("foo").pressEnter();
$("#output").shouldBe(text("foo"));

テストコードに同じセレクタが重複するのを避けるため、Page Object パターンを使う。

Page Objects - Documentation

ページ要素を Page Object クラスに隠蔽する。

@FindBy(name = "input")
public SelenideElement input;
@FindBy(id = "output")
public SelenideElement output;

テストコードからセレクタを排除できる。

IndexPage page = open("http://localhost:8080/", IndexPage.class);
page.input.val("bar").pressEnter();
page.output.shouldBe(text("bar"));

Selenide としては、ページ要素のフィールドを private にして、ページ要素に対するロジックをメソッドで提供することを推奨している模様。

IndexPage echo(String val) {
    $(By.name("input")).val(val).pressEnter();
    return this;
}
String output() {
    return $("#output").text();
}

テストコードにはページ要素を操作するメソッド呼び出しとアサーションを記述する。

IndexPage page = open("http://localhost:8080/", IndexPage.class);
page.echo("buz");
assertEquals("buz", page.output());

JJUGナイトセミナー「メッセージングミドルウェア特集」に行ってきた #jjug

先日、JJUGナイトセミナー「メッセージングミドルウェア特集」に行ってきました。簡単に所感をまとめます。

jjug.doorkeeper.jp

メモから抜粋。(資料が公開されたら貼っておきます)

実運用して分かった Rabbit MQ の良いところ・気をつけること

  • オープンソースのメッセージブローカー
  • 複数のメッセージプロトコルに対応している
  • スタンドアローンでもクラスタ構成でも構築が可能
  • 多言語サポート
  • プラグインが豊富
    • AMQP以外のプロトコルを追加できる
    • 認証機能, 管理画面
  • Web API でリソースやトラフィックの監視が可能

  • RabbitMQ 導入前はブローカーのないキュー管理の仕組みを利用していた

    • 接続設定の変更などが面倒
    • 設定ファイルをすべてのファイルに持たせる必要がある
    • Producer, Consumer が簡単に追加できない
  • キュー管理から RabbitMQ へ

    • AMQPプロトコルをサポート
    • クライアントライブラリがある (Java, PHP)
    • クラスタ構築が簡単
    • Producer, Consumer の追加が簡単
    • トピックとキーでルーティングが可能
  • バックエンドサーバの前段に RabbitMQ のクラスタを配置
  • Java から PHP へのメッセージの受け渡しができる
  • クラスタ構成で耐障害性が高い
  • クライアントは RabbitMQ クラスタを向けるだけでよい
    • Producer, Consumer の追加が簡単
  • 現在は1クラスタで 1000万msg/day を Consumer が処理している

  • RabbitMQ の前にロードバランサーを配置してみた

    • メッセージがなくなる現象が発生
    • LBの設定でセッションが維持されずコネクションが途中で切れることが原因
    • RabbitMQ の Java クライアントにロードバランスする機能が実装されている
  • RabbitMQ はデフォルトでメッセージをディスクに書き込む設定になっている

    • 大量のメッセージが処理しきれずクラスタが応答しない現象が発生
    • メッセージをメモリで扱うように設定を変更
    • マスタ1台はディスク, スレーブ2台はメモリ
    • RabbitMQ はディスクを使うことを推奨している (メモリは特殊ケース)
  • 管理プラグインの Message Rates の設定がデフォルトで Basic モード (メッセージの流量をモニタする)

    • このオプションを無効にしたらパフォーマンスが向上した
    • メトリック監視とパフォーマンスはトレードオフ
    • スループットが要求される場合はプロビジョニングに注意
  • 無停止バージョンアップ

    1. Producer を新しい RabbitMQ クラスタに向ける
    2. クラスタに残っているメッセージを捌き切ったら旧クラスタを停止
  • ネットワーク障害時
    • クラスタを落として再起動 (ドキュメントにも書いてあるので問題なし)

40分弱でわかる Apache Kafka

  • スケーラブルな分散pub/sub型のメッセージングシステムを実現するためのミドルウェア
  • ストリーミングプラットフォーム
  • pub -> 仲介者 (ブローカー) -> sub

    • pubとsubを非同期に分離して疎結合
  • 小規模なシステムでも使える (大規模なシステムに限らない)

  • オートスケールではない
  • プロトコルは独自バイナリ
  • パーティション単位で順序を保証する

  • 複雑なデータパイプラインをシンプルにする

  • ストリーミングデータを処理する (リアルタイム処理)

  • Kafka はディスクにメッセージを書き込む

  • オフセット (Consumerが次にどこを読むか) を柔軟にコントロールできる
  • 指定時間内はデータが損失なく読み直すことができる安心感

  • ZooKeeper (分散コーディネーションシステム)

    • 高い可用性と信頼性
    • クラスタマネジメント
    • 死活監視
    • ACL情報のストア
  • Kafka で扱うメッセージは独自フォーマットのバイナリ

  • トピックはメッセージストリームのラベル
    • ラベルの名前は任意
    • Producer は1〜複数のトピックにメッセージを投げる
    • トピックは負荷分散のためパーティションに分割される
  • パーティション
  • Consumer Group
    • 複数の Consumer を論理的にグルーピングできる
    • オフセットによって Consumer Group ごとにどこまで読み込んだかを Consumer が覚えている
  • Extract -> Transform -> Load (ETL)
    • Kafka からデータを取得して加工して Kafka に戻す

メッセージキュー「Pulsar」の紹介

  • Yahoo!で開発されたpub/sub型メッセージングシステム
  • マルチテナント
    • 1つのMQに複数のサービスが同居できる
    • 他のサービスのトピックへのアクセスは認証/認可機構でブロックできる
  • トピックが階層化されている (ネームスペース単位で設定変更が可能)
  • 地理的に離れたデータセンターのクラスタ間でレプリケーション

    • すべてのデータセンターにメッセージを Publish するのは非効率
    • MQ内部でデータセンターをまたいでレプリケーションしてくれることが望ましい
    • Producer は自分のデータセンターの Pulsar にメッセージを送るだけでよい
    • あとは Pulsar がレプリケーションしてくれる
  • クライアントライブラリは Java, C++, Python

    • 多言語からは WebSocket API で利用可能
  • pub/sub はトピックURIで Pulsar に接続する

  • Subscription Type

    • Exclusive : 1つの Subscription に対して、1つの Consumer
    • Shared : 1つの Subscription に対して、複数の Consumer (Consumer Group)
    • Failover : Exclusive + Consumer が落ちたら別の Consumer に failover する
  • Consumer から Broker に ACK を返すとキューからメッセージが削除される

  • BookKeeper

    • 先行書き込みログ (SSD) と 永続化ストレージ (HDD)
    • 速度と永続性の両立を実現する

素人目ですが、後発の Pulsar はやはりいい感じに見えました。ただ、他のミドルウェアにも特徴がいろいろあって適材適所だと思うので、システムの特性に応じて選定できるようになるとよいのかなと思います。これを書いてる時点では資料は公開されてないのですが、最後にミドルウェアの比較があったので、それはぜひ読み返してみたいです。

今回はミドルウェアの話がメインでしたが、そのうちメッセージングシステムの設計の話とか聞いてみたいです。

JSUG勉強会 2017年その7 〜 俺たちのマイクロサービス に行ってきた #jsug

JSUG勉強会 2017年その7 に行ってきました。簡単に所感をまとめます。

jsug.doorkeeper.jp

今回のテーマはマイクロサービスです。メモから抜粋します。

無理をしないマイクロサービス

  • マイクロサービスアーキテクチャは手段
  • 組織や体制に合うやり方でこれまで実現できなかったことを実現する

    • やり方はそれぞれ違う
  • 各チームの機能追加を独立でデプロイできるようにする

  • リリーススピードを上げることが目的

  • よくあるアーキテクチャ

    • フロントエンドUI <--> サービスA, B, C, ...
    • フロントエンドUI <--> API Gateway <--> サービスA, B, C, ...
  • フロントエンドUIに変更が集中する

  • 各サービスがUIを持ち、各チームで開発する

  • ユーザ情報をどうやって連携するか

    • ユーザサービスを立ててSSOにする
    • 認証情報は Cookie に保存する
    • 各サービスはサブドメインで分ける
    • 昔からあるSSOのやり方
  • Cookie には JWT (JSON Web Token) で保存する
    • ..
    • Base64エンコードしてCookieに保存する
    • クライアント側はデコードするだけで使える
    • なりすまし防止のため、Header と Claims を秘密鍵で署名する (Signature)
    • 各サービスに公開鍵を渡しておく
  • Spring Security

    • JWT を公開鍵で検証、Base64 でデコードして Spring Security の UserDetails を生成する
    • Cookieがなければユーザサービスのログイン画面を表示する
  • 各サービスの画面レイアウトを統一したい

    • レイアウトを管理するチームを用意する
    • 共通レイアウトを jar でパッケージングして配布する
    • src/resources/templates/layout.html
  • aタグのURIはプロパティから取得する

    • 全サービスでプロパティのコピペが発生する
    • 通化する (共通化し過ぎると独立してデプロイし辛くなるため、独立性を妨げない程度に共通化する)
    • Spring Cloud Config
    • Config Server で管理する
  • 他のサービスとの連携はAPI化する

    • どこかのAPIで障害が発生したときに呼び出し元に障害が伝搬する
  • Circuit Breaker
    • 障害の伝搬を防ぐ (fail fast)
      • アクセスできないようにする
    • 自動で復旧する
    • fallback が書ける
  • Spring Cloud Netflix (Hystrix)
  • それぞれのAPI呼び出しに Circuit Breaker を入れる
    • それぞれで HystrixCommand を書くのは煩雑
  • 中間に API Gateway を入れる
    • 各サービスは API Gateway を経由して各APIを利用する
    • fallback をここに書く
  • Zuul / Zuul2

  • Service Discovery

  • Envoy

    • サイドカーパターン
    • C++ベースのリバースプロキシ
    • サービスの代わりに Envoy 同士が連携して Service Mesh を構成する
    • Service Mesh の管制塔の役割として Istio がある
  • API呼び出しで必要となる入出力クラスをどうするか

    • 共通ライブラリにはしない (他チームの変更の影響を受けやすい)
    • 各サービスで必要なフィールドだけを定義したクラスを用意する
    • JSONをそのまま使う
    • API提供側がクライアントライブラリを配布する
  • プラットフォーム

    • k9s, Cloud Foundry
    • Spring Boot なら Docker コンテナを使う必要性はあまりないかも
      • jar自体がjavaコマンドで起動できるしポータブルなので
  • CI/CD

    • ビルド/デプロイの自動化
    • Concourseのパイプライン

俺のマイクロサービス -マイクロサービスに関する経験と考察-

  • 問題意識をベースにすることが大事
  • 誰かの真似ではない

  • マイクロサービスは、設計、実装、デプロイ、運用など多岐に亘る概念である

  • 結果論
  • グッドプラクティスに名前を付けたものがマイクロサービス
  • マイクロサービスの目的はスケーラビリティとアジリティを向上すること

    • それが必要な企業は決して多くはない
    • 多くの企業にとってはマイクロサービスは必要ではないかも
  • HTML5 + API

    • APIが再利用できない問題
    • 画面デザイン > API の順番で作ると、特定画面向けのAPIになってしまう
  • 深夜リリース

  • Start simple, not small

  • Spring Boot

    • Executable JAR == マイクロサービス
    • デプロイの局所化、Spring Cloud との連携が可能
  • Service Discovery + Load Balancing
    • Eureka
    • Spring Boot と組み合わせやすい
  • Queuing
  • Session Replication
    • スティッキーセッション
    • Spring Session + Hazelcast (ライトスルーができる)
  • Versioned Platform
    • サービスごとに言語やフレームワークが選べる
    • ノウハウの分散が懸念
    • 親pomでフレームワークのバージョンを統一する
    • Spring IO Platform の親pomを使う
      • 今は使っていない
      • バージョンアップ待ちがボトルネックになることも
    • 管理されたマイクロサービス
  • Shared Interface
    • サービス側の変更によってコンシューマ側の実装を変更しないといけない
    • サービス側とコンシューマ側で同じBeanを実装するのは無駄
    • 共通のインタフェース
      • サービス側とコンシューマ側が使う
      • 同じようなコンシューマの実装があちこちに
    • 実際はクライアントライブラリのアプローチをとった
      • 共通インタフェースやBeanもクライアントライブラリに入れる
  • Service Unit Test
    • 各サービスの Controller 以降を JUnit でテスト
      • DBアクセスを含めたテスト
    • 他のマイクロサービスの呼び出しはモックにする
      • Mockito などは使わず、オーバーライドでモックする
    • Spring Boot の機能で end-to-end を JUnit でテスト
  • Service Integration Test
    • マイクロサービスごとを結合するテストは必要
    • 各サービスを手動で立ち上げて JUnit から HTTP 通信でテストする
    • 正常系と異常系の数パターンをテストする
    • エラーの発生を正しく伝搬できるか確認する
    • インタフェースの齟齬は意外と多い
  • CI/CD
    • デプロイ数/対象が多すぎて手動だと大変、サーバの準備も大変
    • 環境構築を自動化する
    • 環境構築は Ansible で自動化
    • ビルドは Jenkins で自動化
    • 将来的にはブルーグリーンデプロイを目指したい
    • APIのバージョニングも大事 (初期段階から考慮する)
  • Monitoring
    • 監視は大事
    • Elasticsearchを中心としたモニタリングの仕組み
    • メトリクスやログの収集
    • Metricbeat, Filebeat, ...
  • Front Service & Backend Service
  • Serverless
  • Shared Data Store
    • マイクロサービスごとに Data Store を用意する
    • マイクロサービス間で Data Store を共有する
    • KVS
  • Event Sourcing
    • 例) 買い物かご (商品追加、削除、空にする、など)
    • 買い物かごにイベントを発生させる
    • イベントの種類に商品追加、削除、空にする、などのイベントがある
    • 発生したイベントはすべて記録する
    • イベントの積み重ねによって買い物かごの状態を判断する
    • 状態のソーシングではなくイベントのソーシング
    • 処理の追い越しや不正な画面遷移が発生た場合にどう対処するか

これが正直な感想ではあります。あとマイクロサービスの起原は以下のブログです。(たぶん)

martinfowler.com

日本語訳はこれ。

kimitok.hateblo.jp

これを読むとマイクロサービスはあくまでも「結果論」であることが分かります。まずは、自分たちが解決したい問題や課題はなにか、これを明確にする必要があります。マイクロサービスは手段であり目的ではないわけです。

初期段階からマイクロサービスアーキテクチャを見据えることは大事だと思います。が、マイクロサービスは最先端の開発手法だから取り入れていくべきだ、みたいなひとたちはまだまだたくさんいると思います。先日、某大企業のセミナー資料にもそんなニュアンスのことが書かれていて、ちょっと残念な気持ちになりました...。

最後に Spring Fest 2017 の告知がありました。2017/11/24 開催です。

springfest2017.springframework.jp

java.lang.IllegalArgumentException: Comparison method violates its general contract!

自作の Comparator でリストをソートしたら初めて見るエラーメッセージが。

java.lang.IllegalArgumentException: Comparison method violates its general contract!

再現コード

Java 1.8.0_92 です。

なかなか再現できずいろいろ試した結果、以下のようになりました。

public class Foo {
    private String name;
    public Foo(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
public class Sample {
    void execute(List<Foo> list) {
        list.stream()
                .map(o -> map(o.getName()))
                .sorted((o1, o2) -> compare(o1.getName(), o2.getName()))
                .count();
    }
    static Foo map(String name) {
        return new Foo(name.startsWith("a") ? name.toUpperCase() : null);
    }
    static int compare(String str, String other) {
        if (str == null) {
            return 1;
        }
        if (other == null) {
            return -1;
        }
        return str.compareTo(other);
    }
}
// リストの要素は32個以上
List<Foo> list = Arrays.asList(
        new Foo("xyz"), new Foo("abc"), new Foo("xyz"), ... , new Foo("xyz"), new Foo("abc"), new Foo("xyz"));

new Sample().execute(list);

解決 (とりあえず)

compare メソッドを以下のように変更するとエラーが発生しなくなります。

static int compare(String str, String other) {
    // ここから
    if (str == null && other == null) {
        return 0;
    }
    // ここまでを追加
    if (str == null) {
        return 1;
    }
    if (other == null) {
        return -1;
    }
    return str.compareTo(other);
}

TimSort

詳しくは理解してなくて申し訳ないですが、sort メソッドでは TimSort というアルゴリズムが使われており、Comparator のロジックに矛盾がある場合に今回のエラーが発生するようです。

上の再現コードでは、compare に渡す要素が両方 null になるケースの考慮が抜けており、これらの比較結果を返すロジックを含める必要があります。

その他

尚、リストの要素が32個未満の場合、別のアルゴリズム (mini-TimSort) に切り替わるようで、この場合には同様のエラーは発生しませんでした。

Spring で Bean Validation のエラーメッセージに任意のフィールド名を埋め込む

Bean Validation のエラーメッセージに任意のフィールド名を埋め込む方法を調べたときのメモ。タイトルには Bean Validation と書いていますが、正確には Spring が提供する機能によってエラーメッセージに任意のフィールド名を埋め込みます。

サンプルコード。

github.com


バリデーション用のアノテーションは以下の通り。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Constraint(validatedBy = { SampleValidator.class })
public @interface SampleValidation {
    String value();
    String message() default "{com.example.demo.constraint.SampleValidation.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

今回は Controller で @Validated を利用して Form バリデーションをするパターン。

@RestController
public class DemoController {
    @Autowired
    MessageSource messageSource;
    @PostMapping("index")
    public String index(@RequestBody @Validated FooForm form, Errors errors) {
        if (errors.hasErrors()) {
            errors.getAllErrors().stream()
                    .map(e -> messageSource.getMessage(e, Locale.getDefault()))
                    .forEach(System.out::println);
            return "NG";
        }
        return "OK";
    }
    static class FooForm {
        @SampleValidation("foo")
        private String name; // バリデーション対象のフィールド
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
}

パターン1

プロパティ未定義の場合、Form クラスのフィールド名がメッセージ定義の {0} にそのまま出力される。

com.example.demo.constraint.SampleValidation.message={0} is invalid.

出力結果:

> name is invalid.

パターン2

フィールド名をキーにしてプロパティを定義した場合、定義した名前がメッセージ定義の {0} に埋め込まれて出力される。<Bean 名>.<フィールド名> 形式も可。

com.example.demo.constraint.SampleValidation.message={0} is invalid.
name=Name

出力結果:

> Name is invalid.

パターン3

<フィールド名> 形式と <Bean 名>.<フィールド名> 形式を一緒に定義した場合、<Bean 名>.<フィールド名> 形式のプロパティが優先される。

com.example.demo.constraint.SampleValidation.message={0} is invalid.
name=Name
fooForm.name=NAME

出力結果:

> NAME is invalid.

現場からは以上です。