寝ても覚めてもこんぴうた

プログラム書いたり、ネットワーク設計したり、サーバ構築したり、車いじったり、ゲームしたり。そんなひとにわたしはなりたい。 投げ銭は kyash_id : chidakiyo マデ

Go で Spanner とよろしくやるためにガチャガチャやっている話

f:id:chidakiyo:20201214201606j:plain

この記事は Go Advent Calendar 2020 14日目 の記事です。

みなさん Go してますか?
Spanner も触ってますか?

最近やっと部分的に本番環境で Spanner を利用し始めました。

Spanner のスキーマなどを git で管理しつつ、チーム内でレビューなどをしつつ運用する方法として、 以下のツールたちを利用しています。

試したツールたち

最終的に利用しているツールは

  • wrench
  • yo

  • お好みで : hammer (としたいところだが使えてない、下記)

となっていますが、以下のイメージで利用しています。

全体の関係

以下の図のような関係になっています。

f:id:chidakiyo:20201214201516p:plain

1. DB の初期スキーマを適用する

wrench を利用します。
事前に schema.sql など、スキーマファイルを作成しておきます。

最終的には yo がスキーマの定義をみて Mutation を生成するコードを生成するので、 検索の条件にする必要があるカラムなどは適切にインデックスを付与するなどします。
SQLでは自由にかけてしまいますが、単純な利用方法であれば、インデックスを貼らないカラムに対する条件で検索ができるようなコードはかけなくなるので安全とも言えるかもしれません。

コマンドは

wrench create --directory {DDLファイルがおいてあるディレクトリ}

という感じで実行します。

2. 開発中にカラムに変更がある場合にはマイグレーションを行う

wrench で以下のコマンドを実行すると、マイグレーション用のファイルが生成されます

wrench migrate create --directory ./

コマンドを実行するとファイルが生成されますが、連番のブランクファイルになっています。
そのファイルに alter 文を記述し、カラムの変更を適用するイメージになります。

ここの alter 文を hammer を利用しようとしましたがちょっとうまくいきませんでした。(図の2')

3. miigration ファイルを DB に適用する

これも wrench を利用します。
以下のコマンドで適用します。

wrench migrate up --directory {DDLファイルが置いてあるディレクトリ}

4. 開発のためのコードを DB のスキーマから生成する

yo というツールを利用します。
以下のコマンドを実行します

yo $(SPANNER_PROJECT_ID) $(SPANNER_INSTANCE_ID) $(SPANNER_DATABASE_ID) -o {生成したコードを格納したいディレクトリ}

wrench は project_id, instance_id, database_id は環境変数で渡していましたが、 yo は直接コマンドラインで渡します。

生成されるファイルは

  • yo_db.yo.go
  • {table_name}.yo.go

が最低限生成されます。
生成されたファイルは .yo.go になるので、自動生成されたコードをファイル名から容易に判別できます。

おまけ1

Go のアドベントカレンダーなので、Go コードを出さねば・・・と思ったので...
Spanner の Go クライアントは内部的にセッションプールの仕組みを持っています。
以下のような雰囲気で設定を行います。

const healthCheckIntervalMins = 50
const numChannels = 4
var config = spanner.ClientConfig{
    SessionPoolConfig: spanner.SessionPoolConfig{
        MinOpened:           100,
        MaxOpened:           numChannels * 100,
        MaxBurst:            10,
        WriteSessions:       0.2,
        HealthCheckWorkers:  10,
        HealthCheckInterval: healthCheckIntervalMins * time.Minute,
    },
}

一度、Spanner を利用したアプリケーションを検証環境にデプロイしてテストしていた際に、 ある程度リクエストを受けるとなぜか Spanner がブロックし、アプリケーションが全く応答しなくなるという問題にぶち当たりました。

数百のリクエストを投げると、ある時からリクエストが全部タイムアウトして、全くサーバが動いている様子がない、 というなかなか恐ろしいものでした。

原因としては、 ReadOnlyTransaction() で取得したトランザクション(connection?)は、 defer で Close() しないとプールの中の Connection が枯渇して ReadOnlyTransaction() がブロックし続けるというものでした。

おまけ2

おまけ1で得た知見。

zagane (https://github.com/gcpug/zagane) を使いましょう!

zagane は以下を防いでくれます

unstopiter: it finds iterators which did not stop.
unclosetx: it finds transactions which does not close
wraperr: it finds (*spanner.Client).ReadWriteTransaction calls which returns wrapped errors

自分の使い方が悪いのか、モノレポ構造で使っているのが悪いのか、
極稀に検知してくれないケースが自分の環境では有るのですが、これは Spanner を利用するなら必須と言っても良いものです。

まとめ

と、ここまで書きましたが、実際には既存の Datastore から単純に一部移行したぐらいでしか利用できていません・・><
数百リクエスト/秒 ぐらいで利用しているので、CPU 負荷も 5% も行かないぐらいでめちゃくちゃ安定して稼働してます。Spannerすごい。

なんかもっと書きたいことがあったような気がするけど書いているうちに忘れてしまった・・・ま、いっか

明日の記事は hajimehoshi さんの 「Go におけるアラビア語描画について」 です。楽しみですね。

ではでは。

GCP の DNS を yaml ファイルを利用して管理する

f:id:chidakiyo:20201210184827j:plain

GCPDNS を利用する場合、Web UI や コマンドでレコードをポチポチ投入するの大変ですよね。

今回は Cloud DNS のレコードをまるごと yaml で管理し、 export/import する方法を書いてみます。

必要なもの

試す際に Google Cloud SDK(gcloud) のインストールは必須です。

必要ではないですが、以下の手順はすでに Cloud DNS に zone が作成され、ある程度のレコードが有る想定で書いています。

DNS に登録した内容を yaml ファイルに一括出力する

コマンドは以下のような感じになります

gcloud dns record-sets export dns-record.yaml --zone=ZONE -z {ZONE_NAME}

上記の dns-record.yaml は出力先ファイル名です。
出力したいディレクトリやファイル名を適当に渡すことで実行ディレクトリ以外の場所にもファイルを出力できます。

今回はフォーマットを特に指定していませんが、 Bind のゾーンファイル形式などでの出力も可能なようです。(未確認)

出力した yaml ファイルは後述する Import 機能で利用でき、また、git などで差分管理が行えるので、チームで変更内容のレビューをして適用する、などのフローに利用することもできるかもしれません。

出力した yaml ファイルを Import し、DNSに反映させる

出力したファイルを以下のようなコマンドで適用できます。

gcloud dns record-sets import dns-record.yaml --zone=ZONE -z {ZONE_NAME} --delete-all-existing

ポイントは --delete-all-existing フラグなんですが、これは投入前にすでに存在するレコードをすべて消すというオプションだと思われます。このフラグを付与しない場合、すでに存在する各種レコードに対して重複したレコードはエラーになるため、yaml まるごとの適用という運用ができなくなります。
このあたり深く振る舞いを調べていないため、各自運用の際に検証してみてもらえると良いと思います。(コメントもください)

おわりに

普通は Terraform などを利用して DNS のレコード管理などを行うのが主流かと思いますが、
GCP の標準機能でもある程度それっぽいことができますよ。
という感じのお話でした。

ではでは。

GCP でアプリケーションのログや特定の値をいい感じに BigQuery にエクスポートしたい

f:id:chidakiyo:20201111192602j:plain

アプリケーションを実装していて、ログをかっちり決めたカラムに挿入したい、や、
難しい API を利用せずに BQ に特定のデータを挿入したいという場合ありますよね。

普通にやると、BigQuery の API を経由し、ストリーミングインサートするなどちょっとひと手間かけてデータを BQ に送っていると思います。

今回書く方法は、ある程度意図した構造(カラム)の状態で BQ に気軽にデータを送る方法になります。

サンプルとして、 Cloud Run での例になりますが、Cloud Logging に出力できる GCP のサービスならほぼ同じように利用できると思います。

概要

Cloud Run から stdout に対して JSON Payload 形式のログを出力します。
出力したログは Log Router を経由し、対象のログを絞り込みます。
絞り込んだログは BQ の Dataset へデータを流し込みます。

Go のコードを用意する

最低限のコードは以下のようなイメージになります

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/export", func(writer http.ResponseWriter, request *http.Request) {

        // BQに出力したい構造にしておく
        type ExportData struct {
            Type    string `json:"type"`
            Name    string `json:"name"`
            Num     int64  `json:"num"`
            Message string `json:"msg"`
        }

        // 値を詰め込む
        payload := &ExportData{
            Type:    "Type_A",
            Name:    "Export Data A Type",
            Num:     3,
            Message: "Export message.",
        }

        // stdoutにJSONとして吐き出す
        json.NewEncoder(os.Stdout).Encode(payload)

        fmt.Fprintf(writer, "ok")
    })
    port := "8080"
    if s := os.Getenv("PORT"); s != "" {
        port = s
    }
    if err := http.ListenAndServe(":"+port, mux); err != nil {
        log.Fatalf("http.ListenAndServe: %v", err)
    }
}

Go の構造体を作成し、適当なラベル(BQにカラムとして出力されるので適切なもの)をつけ、
stdout に対して JSON を吐き出します。

これを動かすと Cloud Logging にログが出力されます。

Logs Router で条件にマッチしたデータを BQ に送り込む Sink を作成する

Sink details, Sink destination は適当に設定しましょう。笑
Sink destination はもちろん BigQuery dataset を選択し、この設定を行う前に事前にBQ側でデータセットを作成しておく必要があります。

Choose logs to include in sink の設定だけ注意が必要で、
今回の例では

resource.type="cloud_run_revision"
jsonPayload.type = "Type_A"

f:id:chidakiyo:20201111192050p:plain

のように設定しましたが、JSON Payload の type ごとに BQ のデータセットを分けたい等の場合にはこのような設定にするのも良いでしょうし、Previewして自分が必要なログが対象になっていることを確認して利用してください。

完了したら Sink を作成します。
Sink の作成が完了しても、体感的に Sink の条件が反映され動き出すまでに 10分程度のラグがあるような気がします。
設定したのにうごかない!!と思っても、ちょっとのんびり待って、再度確認してみると良さそうです。

BQ に対してクエリする

ここまで設定がうまく行っていれば、アプリケーションにアクセスすることで BQ にログが保存されます。
先程構造体(JSON)として出力していますが、 BQ には構造体以外の Cloud Logging のログ部分も出力されていますが、
ここで構造体部分だけクエリしてみます。

以下のようなクエリを実行します

select jsonPayload.* from `{dataset}.run_googleapis_com_stdout` 

dataset名やテーブル名が異なることがあると思いますので、環境に合わせて指定します。
このクエリを実行すると以下のような結果が表示されます。

f:id:chidakiyo:20201111192115p:plain

4度ほど実行したので、4行の結果が出力されています。
今回の例では同一の値を出力してしまったので良いクエリの例が示せませんが、アプリケーションの任意の値を出力している場合には、一般的な SQL の知識で自由に利用できます。

まとめ

ちょっと雑な記事になってしまったけど、アプリケーション内から BQ に気軽に出力できると便利だと思う。
stdoutに出力する形だとローカル実行のテストなども行いやすいし。
ユースケースとしては、アプリの分析用の指標をポロポロ落としておいて、あとからざっと集計するなどアイディア次第で BQ と組み合わせて色々やれそうです。

ではでは。

たった 60 分で Go の静的解析が理解できる神コンテンツ

f:id:chidakiyo:20201008183032j:plain

タイトルの通りで 60 分ほどで静的解析が理解できるコンテンツを見つけたので共有。

静的解析ってなんじゃ?
ASTってなんじゃ?

って状態から、「なんとなく静的解析やれそう」になれたのでぜひ試してみてほしい。

静的解析をはじめよう - Gopherをさがせ!

IntelliJ で Eclipse の Outline 機能のような形でファイルのプロパティ一覧を表示したい

f:id:chidakiyo:20200908143237j:plain

Go を書いていると、 Go の文化としてファイルをあまり分割せずに、フラットにファイル内に書きづづけるということがお多くなるかと思いますが(自分は結構分割しちゃうけど)、その場合、どこにあのプロパティ/関数あったっけ?みたいになりますよね。

Eclipse だと Outline という機能があったけど、アレっぽい機能は IntelliJ にないの?と探したので記事にしました。

結論 : ある。

IntelliJ には Structure という機能で提供されています。
ドキュメントは このへん

Structure には2つの機能があって、

  • 構造ツールウィンドウ : ⌘ + 7
  • 構造ポップアップ : ⌘ + F12

があります

構造ポップアップは一時的に Structure のウィンドウをポップアップする機能で
私が求めていたのは「構造ツールウィンドウ」になるのでそちらで説明します。

構造ツールウィンドウを利用する

早速コマンドを実行して、ウィンドウを表示してみます。

⌘ + 7

を実行すると、IntelliJ 上のどこかに Structure のウィンドウが表示されます。
その他の IntelliJ のウィンドウと同様に適当な箇所にスナップできますので、私は右側にこのように表示するようにしてみました。

f:id:chidakiyo:20200908143036p:plain

Structure 上の関数名やプロパティ名をクリックすると実装箇所にエディタ側が飛ぶのでサクサク目的の箇所に移動することができ便利ですね。

ではでは。

Spanner で NOT Nullのカラムを追加したい

f:id:chidakiyo:20200908094813j:plain

Spanner は RDB のようにかっちりとスキーマを定義する DB なので、
Datastore などのように、プロパティをふわっと追加してデータ投入などできません。

RDB と同じように DDL を利用してカラム追加して利用しますが、 NOT NULL のカラムの追加ができないという特性があります。

今回は以下のようなスキーマで考えます。

CREATE TABLE Member (
    ID STRING(256) NOT NULL,
    Name STRING(256) NOT NULL,
) PRIMARY KEY (ID);

NOT NULL カラムが追加できないことを確認する

以下のようなクエリを実行し、 NOT NULL の制約のあるカラムを追加しようとしてみます

ALTER TABLE Member ADD COLUMN Nickname STRING(256) NOT NULL;

→ 実行すると、 NOT NULL なカラムは追加できないよ。 という感じに怒られてしまいます。

NULL 許可したカラムを追加してみる

NOT NULL なカラムは追加できないが、 Nullable なカラムは追加できるはずなので追加してみます。

ALTER TABLE Member ADD COLUMN Nickname STRING(256);

→ 問題なく追加できますね。

Nickname カラムをなんとか NOT NULL にしてみる

例えば、 NOT NULL にカラムを変更しようとしても、 Nullable なカラムを追加した状態では NULL が入っているので、変更は失敗するはず。
なので、 NULL な値をなくして、 NOT NULL なカラムに変更するというのを試してみる。

ためしに、 Nullable なカラムに NULL が入った状態で NOT NULL に 変更してみる。

ALTER TABLE Member ALTER COLUMN Nickname STRING(256) NOT NULL;

→ 予想通り Adding a NOT NULL constraint on a column Member.Nickname is not allowed because it has a NULL value at key: [xxx] といった形で NULL な値があるので NOT NULL の制約はつけられないと怒られた

NULL 値を Empty String に置き換えてみる(なにか特定のデフォルト値でもいいと思います)

UPDATE Member SET Nickname = '' WHERE Nickname IS NULL;

NULL を置き換えたら NOT NULL 制約をつけてみる

ALTER TABLE Member ALTER COLUMN Nickname STRING(256) NOT NULL;

→ 結果うまく行きます。

最終的にこのようなテーブル構成になります

CREATE TABLE Member (
    ID STRING(256) NOT NULL,
    Name STRING(256) NOT NULL,
    Nickname STRING(256) NOT NULL,
) PRIMARY KEY (ID)

NOT NULL なカラムを追加するための流れの要約

  1. NULL 許可する形でカラムを追加する
  2. NOT NULL にしたいカラムの場合には NULL ではない値にカラムを書き換える (注意点あり *1)
  3. NOT NULL 制約を満たせる状態になったら ALTER 文で改めてスキーマ変更する。

の3つの手順を経る必要がある。(default的なものがない)

注意(この点注意しましょう!!)

*1) カラム書き換えはUPDATEステートメントなどを利用するが、Spannerの制約で 20k 以上の変更が1クエリで行えないので注意。(また、20k はレコード数でもない点も注意すること)

まとめ

機能的には NOT NULL のカラムを追加することはできるようなのですが、最後の 注意 の点だけ気をつけましょう
運用が始まって大量のレコードが追加されているテーブルの場合、Spanner の一度に更新できる制限(20k)のために非常に大変なことになります
場合によってはパーティション DML を利用したりするのかな?(参考のリンク先参照)

参考

cloud.google.com

cloud.google.com

Spanner のスキーマ管理をする wrench を試してみた

f:id:chidakiyo:20200903171222j:plain

先日 Spanner のスキーマ管理ツール hammer を利用した ブログ を書きましたが、
Cloud Spanner Echosystem から wrench というツールが提供されていることに気づきました。

hammer 最近更新されてないのかな(?)、と思いながら試していたところもあるので、今回はこちらの wrench を試してみます。

手順そのものは公式の ドキュメント にほぼ沿ったものになります

wrench のインストール

go get -u github.com/cloudspannerecosystem/wrench
wrench

を実行することで色々表示が出れば成功。
(何故か hammer もそうでしたが、 wrench も -v オプションでバージョンを見ようとすると unknown と表示されます・・・)

環境変数に接続情報をもたせる

以下のような環境変数を事前に登録します

export SPANNER_PROJECT_ID=your-project-id
export SPANNER_INSTANCE_ID=your-instance-id
export SPANNER_DATABASE_ID=your-database-id

スキーマファイルを用意する

最初にまずスキーマのファイルを用意します。
今回は _ddl というディレクトリを利用しようと思います。

作成するファイルは以下のようになります

cat ./_ddl/schema.sql
CREATE TABLE users (
  user_id STRING(36) NOT NULL,
) PRIMARY KEY(user_id);

スキーマを適用する

以下のコマンドで DB にスキーマを適用します。
事前にすでに DB が存在している場合にはコマンドがエラーになるので注意してください。

wrench create --directory ./_ddl

コマンド実行が成功すると、 database と table が作成されます。

マイグレーションファイルを作成する

コマンドを実行してマイグレーションファイルのテンプレートを作成します。

wrench migrate create --directory ./_ddl

_ddl/migrations/000001.sql is created と表示され、 {連番}.sql という空のファイルが作成されます。

カラムの変更を migrate ファイルに記述する

試しに新しいカラムを追加してみましょう。
先程生成された {連番}.sql ファイルに対して以下のように記述します。

ALTER TABLE users ADD COLUMN age INT64
ALTER TABLE users ADD COLUMN name STRING(MAX)
ALTER TABLE users ALTER COLUMN name STRING(MAX) NOT NULL
CREATE INDEX idx_users_name ON users(name)

マイグレーションの実行

以下のコマンドでマイグレーションを実行します

wrench migrate up --directory ./_ddl

1/up などと表示されれば成功です。

schema.sql に DB 内のスキーマを反映する

この段階ではローカルの schema.sql はマイグレートした内容を反映していないので、wrench コマンドを使って最新化してみます。

wrench load --directory ./_ddl

コマンドが成功すると、 schema.sql ファイルの中身は以下のように更新されてるはずです。

CREATE TABLE SchemaMigrations (
  Version INT64 NOT NULL,
  Dirty BOOL NOT NULL,
) PRIMARY KEY(Version);

CREATE TABLE users (
  user_id STRING(36) NOT NULL,
  age INT64,
  name STRING(MAX) NOT NULL,
) PRIMARY KEY(user_id);

CREATE INDEX idx_users_name ON users(name);

注意点としては、スキーマ管理用のテーブル SchemaMigrations も併せて生成されています。
Spanner のコンソールから確認するとわかりますが、最後に適用したマイグレーションファイルの番号がそのテーブルに追加されていることがわかります。

まとめ

hammer は ALTER 文の生成までツールでやってくれていましたが、 wrench はあくまでも DDL はユーザが記述し、 wrench は適用した DDL のバージョンの管理まで、という感じで似たようなツールではありますが、やれることが割と違います。

wrench のほうが暗黙でやっていることが少ないので、 本番運用に使うツールとしては wrench が良さそうで、
開発時に、テーブルスキーマを軸に修正しつつコツコツ作業したい場合には hammer が便利なんじゃないかと思ったりしました。

ではでは。