vue + composition-api + typescript + jest + eslintのリポジトリをセットアップした

リポジトリは下。

github.com

最近 Vue追ってなかったので、Vueの大きなアップデートを見ていた。

で、気になったのが下のようなツール周りの変化を知りたいなぁと思って サンプルリポジトリを構築した。 VueはちゃんとTypeScriptで構築した記憶がなかったので その辺のツール周りの調査がメインだった。

  • React HooksのようなAPIの実装の提案はどうなったのかな
  • Single File Componentを使った時に TypeScript周りの設定はどうするんだろう
  • jestの設定は?(今回はsnapshot周りの設定はしていない。)
  • eslintの設定は?

そういえば、React HooksのようなAPIの提案がある、というのも聞いていたけど 調べたところ、composition apiという形で実装されるようだ。 とりあえず、RFCを読んだ。 github.com

RFC読んだ感想としては、方向性としては正しそう、という気持ちになった。 互換性を保ちつつ、利用が出来るのも良い。

ちなみにRFC的にはFunction-based Component APIという名前なのね、となった。

詰まったところ

構築してみたところ、以下の点で詰まったのでメモしておく。

  • vue-loaderの依存に vue-template-compilerが必要になっていた
  • typescript-eslint-parserが @typescript-eslint/parser にrenameされていた
  • tsファイルがコンパイル対象にないと、webpackする時に怒られる
    • なんでや
    • とりあえずファイル追加したら直った
  • webpackでビルドは出来るが、vue-jestが tsconfigのパースでエラーを吐く
    • なぜか tsconfigというパッケージでtsconfig.jsonをパースしているが、trailling commaでエラーになる
    • webpack, vscodeは何もエラーを吐かないので気づかなかった
  • jestのtestEnvironmentがjsdomじゃないと、@vue/test-utilsが怒る
  • .vueファイルの型定義をコピペしてきたら動かなかったので弄ったけど、やっぱりダメだったので、元に戻したら動いた
    • なんでや
  • babelは使ってないけど、vue-jestが babel-bridgeを要求してくる。
    • 追加すると治る

Spring Bootのデフォルトのヘルスチェック全てを無効にする

デフォルトで設定されるヘルスチェックを無効にしたい場合があると思います。 例えば、セッションストアとして使っているredisが落ちた場合にredisを使っていないエンドポイントがあるので サービスとしては落ちて欲しくない、というケースです。

その場合は、redisのヘルスチェックだけDisableしてもいいと思うんですが、今回は、全部OFFにしたいと思います。 management.health.defaults.enabled を使えば、デフォルトのヘルスチェックを全てDisable出来ます。

環境変数の場合は MANAGEMENT_HEALTH_DEFAULTS_ENABLED から設定しましょう。

終わり。

ドキュメント

SRVレコードを使ってgoでサービスディスカバリ経由で内部通信を行ってみた雑感

試してみた結果、厳しい気持ちになった。

なお、今回試した結果はgoの標準ライブラリの挙動をベースにしているので その他通信ライブラリ、ランタイムでの挙動を保証するものではないので注意されたし。

結論としては、可用性の高いLoad Balancerというものは良いものだ・・・になってしまった。

はじめに

筆者は仕事では、AWSを使っており、バックエンドからバックエンドへの通信をパブリックなALB経由でやっているが これを内部通信にしたい、という話が出たので少し試してみた。

何も考えずに楽にやりたい気持ちで構築すると、パブリックなALBに接続する形になっていると思う。 これの利点をまずここに書いておく。

  • LBの可用性がALBと同等のレベルになる
  • 全てのバックエンドへの通信がALB経由になるので、ALBの機能が使える(メトリクスとかアクセスログとか)

これは大きな利点だと思う。 そして、次にInternalなALBに接続するか形になっている場合の利点はこうだ。

  • LBの可用性がALBと同等のレベルになる
  • 全てのバックエンドへの通信がALB経由になるので、ALBの機能が使える(メトリクスとかアクセスログとか)
  • パブリックなネットワークへのアクセスが無くなるのでレイテンシが幾分か改善される(どのぐらい速くなるかは要検討)

InternalなALBに接続するような構成にした場合、幾分かコストが増えるようになる。 アプリケーションがN個あった場合、雑に作ると、2 * N個のALBが立ち上がることになる。 パスルーティングやホストによるルーティングを使えば、2個まで減らせるとは思う。

ここで内部通信なら、サービスディスカバリを使って接続すればどうだろうか、みたいな話がある。 DNSSRVレコードを使ってサービスディスカバリするか、サービスディスカバリ専用のツールを使って名前解決するか、その他の方法がいろいろ考えられるが この記事では、DNSSRVレコードを使ってサービスディスカバリすることを考えてみる。 理由としては、AWSのECSを使っているので、ECSのサービスディスカバリの機能でSRVレコードが登録・解除される仕組みが整っているからである。

なお、実際のAWSでは試していないので 実際に試した人がいれば教えて欲しい。

goでSRVレコードを使ってサービスディスカバリをする方法

gRPCでは標準でライブラリにサービスディスカバリの機能があるが、単なるREST APIの場合はサービスディスカバリの機能とかは作らないといけないので 以下のようなライブラリを使うことになる。

www.kaoriya.net

今回は検証のために、上記の記事で紹介されているライブラリを使用させてもらった。 利用方法は上記の記事で紹介されているので割愛する。

なお、gRPCの場合は、以下の記事が詳しい。 christina04.hatenablog.com

検証する環境

今回は以下のような環境の構成で docker-composeを使って環境を再現する。

  • 自分で立てるバックエンドサーバが2つ
  • サービスディスカバリのためのconsul
  • コンテナをサービスディスカバリに登録・解除するためのgliderlab/registrator
version: '3'

services:
  service1: 
    build: .
    image: service
    container_name: service_1
    ports:
      - 8080
  service2: 
    build: .
    image: service
    container_name: service_2
    ports:
      - 8080
  registrator:
    image: gliderlabs/registrator:latest
    command: "-ip ${MYHOST} consul://${MYHOST}:8500"
    container_name: registrator
    hostname: ${MYHOST}
    depends_on:
    - consul-server-bootstrap
    volumes:
    - /var/run/docker.sock:/tmp/docker.sock

  consul-server-bootstrap:
    image: consul:latest
    networks:
      - consul-demo
    ports:
      - "8400:8400"
      - "8500:8500"
      - "8600:8600"
      - "8600:8600/udp"
    command: "agent -server -bootstrap -ui -client 0.0.0.0"

networks:
  consul-demo:

ちなみにSRVレコードを解決してバックエンドのサーバに接続するアプリケーションは ホストPCで起動する。(ちなみにdockerの上で動くアプリケーションも一緒だ)

理由としては、DNS周りの設定が面倒だからだ。

実際に使うアプリケーションのソース

特に難しいことはしてなくて SRVレコードで解決したバックエンドから帰ってきたレスポンスをそのまま返却するエンドポイントと バックエンドとして呼び出されるエンドポイントの二つを持ったアプリケーションだ。

func main() {
    // SRVレコードを解決して、バックエンドのサービスに接続するためのエンドポイント
    // ホストPCからアクセスするときに使う。
    http.Handle("/", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
        httpReq, _ := http.NewRequest("GET", "http://srv+service.service.consul/backend", nil)
        httpRes, err := HTTPClient.Do(httpReq)
        if err != nil {
            log.Println(err)
            res.WriteHeader(500)
            return
        }
        defer httpRes.Body.Close()
        io.Copy(res, httpRes.Body)
    }))
    // サービスディスカバリ経由で呼び出されるエンドポイント
    http.Handle("/backend", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
        res.WriteHeader(200)
        res.Write([]byte("ok"))
    }))
    http.ListenAndServe(":8080", nil)
}

実際に検証する

これらをApache Benchで負荷を掛けた上で 以下の操作を行った時の挙動を検証した。

  1. dockerで立てたバックエンドを1つを止めてみる
  2. dockerで立てたバックエンドを2つを止めてみる
  3. registratorを止めてバックエンドを1つ止めてみる
  4. dockerで立てたバックエンドを1つ止めた状態から増やしてみる

1. dockerで立てたバックエンドを1つを止めてみる

まずはスケールイン時の動作を確認する。 docker stopで止めるだけだ。

これ自体は問題ない。 切り離しが早いおかげか特に通信に影響は出ないように見える。

2. dockerで立てたバックエンドを2つを止めてみる

これ自体は予期しているようにエラーになる。 が、httptraceでログを調べてみるとDNSのLookupが走っている。 これはgoのランタイムはDNSのキャッシュをしない + SRV対応のDialerもキャッシュしないので コネクションのKeepAliveが効いてない場合やIdleしているコネクションがない場合は、DNSのLookupが走るようである。

詳しくは下の記事を見て欲しい。 shogo82148.github.io

2で検証したコンテナが全て止まった場合に、DNSの解決が出来ずエラーになり、 毎回DNSのLookupをするようになってしまう。これはDNSの負荷が増加する。

また、ALB経由で通信している時はバックエンドが全部落ちてしまったらALBが高速に503を返してくれる。 しかし、SRVレコードによるサービスディスカバリを使う場合、DNSの解決のタイムアウトまで処理が伸びてしまう。 これはサーキットブレイカーの導入などで障害から切り離す必要がありそうだ。

これは非常に検証が大変な上に、実装が大変になりそうである・・・

3. registratorを止めてバックエンドを1つ止めてみる

これはDNSの反映遅延を検証するための検証だ。 consul上のDNSからは止めたはずのバックエンドが解決されてエラーがごく稀に発生する。 接続数が多いほどエラーが出ると思われる。

理由としては、コネクションが再利用されるかどうかに由来する。 再利用されない場合、SRVレコードのルックアップで古い止めたはずのバックエンドを引いた場合にエラーになる。

ちなみにこの場合は、MaxIdleConnsPerHostが高い値に設定されていると、エラー率が下がると思われる。(要検証)

また、障害の発生したバックエンドの高速な切り離しのためにヘルスチェックの実装などが考えられる。

4. dockerで立てたバックエンドを1つ止めた状態から増やしてみる

この場合、普通にスケールアウトするように見える。 しかし、これは、KeepAliveするコネクション数が少ないからだ。 net/httpのDefaultMaxIdleConnsPerHostが2だからだ。 MaxIdleConnsPerHostが大きな値に設定されている場合は、挙動が変わると思われる。

検証中に起きた問題

docker for Macで立てている上に負荷を掛けたせいなのか、稀にdocker上のコンテナと通信が出来なくなる時がある。 これはdocker for Mac再起動する必要がある。

検証で思った所感

  • コネクションを確立時にSRVレコードの解決の後、Aレコードの解決を行うので、DNSの負荷が増えそう
  • DNSをキャッシュしていない状態で、KeepAliveを設定していないとDNSへの負荷が増えそう
  • MaxIdleConnsPerHostが小さいと、コネクションの再利用されず、DNSへの負荷が増えそう
  • DNSのキャッシュが必要そう
    • キャッシュした場合は、高速な障害のあったバックエンドの切り離しにliveness probeみたいなことしないといけなさそう
  • DNSのキャッシュに対するネガティブTTL
    • DNSへのクエリが帰ってこなかった場合に必要そう
    • DNSのキャッシュしてれば必要ないか?(ちなみに、Armeriaの場合はネガティブTTLが実装されていたように思う)
  • ECSのサービスディスカバリのDNS反映遅延はどのくらいになるんだろうか・・・
    • 誰か知ってたら教えて欲しい
    • 後、DNSに登録されるタイミングっていつなんだろう(単なる起動時?)
  • ECSのサービスディスカバリで、単なる起動時にDNSに登録されてもアクセスできない可能性もありそう?
    • アプリケーション側でliveness probeみたいなことをしないといけない・・・?
    • 流石にだるいのでは・・・

おまけ: Armeriaはすごい

以前Armeriaを見た時に調べたが、ArmeriaにはDNSキャッシュやそれに対するnagativeTTLやDNSのクエリ失敗時のbackoffによる再更新などが全部入っていて 欲しいものが全てそこにある感があってすごい。

まとめ

色々試して思ったけど、普通にパブリックALB使って、レイテンシに問題が出たらInternalなALB立てろ、って気持ちになったので この記事はここで終わりです。