GAE の SocketAPI とは何か?

存在は知っていたが、
普段 GAE を利用する上で特に利用する機会がなかったので、
SocketAPI について調べてみた。

SocketAPI

ドキュメントは以下。
https://cloud.google.com/appengine/docs/standard/go/sockets/

なぜか java は日本語。
https://cloud.google.com/appengine/docs/standard/java/sockets/

ドキュメントに書いてあるので説明不要だと思うが、
SocketAPI は、その名の通りソケット通信をする際に利用する API のこと。

java ドキュメントの一部を抜粋する。

・リッスンソケットは作成できません。作成できるのはアウトバウンドソケットだけです。
FTP はサポートされません。
・使用できるのは TCPUDP だけで、任意のプロトコルを使用できるわけではありません。

で、これは何に使うのか?
自分は普段 GAE を使っているものの使ったことがない。

利用用途としてパッと思いつたのが、
GAE から 外部環境の DB に接続する用途。
*あまりないケースだと思うけど・・・

ググってみると、それっぽいのが引っかかる。
https://groups.google.com/forum/#!topic/google-appengine/hmXkSr_EVvQ

以下は上記のスレッドで紹介されている mysql driver で、
GAE から外部の DB に接続するためのものらしい。
https://github.com/pjebs/GAE-Toolkit-Go/tree/master/sql

接続時に SocketAPI が利用されている。
https://github.com/pjebs/GAE-Toolkit-Go/blob/master/sql/sql.go#L44

少し古い記事だけど、
redis に接続する記事もあった。
http://www.xhroot.com/blog/2013/04/12/accessing-redis-from-google-app-engine/

GAE で外部通信といえば、
urlfetch パッケージを利用するが、
これは HTTP しか扱えないので、
その他の通信は SocketAPI を利用することになるのかな。

とはいえ、自分は GCP 上のサービスしか利用しないので、
あまり利用する機会はないかなーと思っている。


クオータ

クオータは以下になる。
https://cloud.google.com/appengine/quotas?hl=en#Sockets

SocketAPI を明示的に利用していないせいもあって、
クオータを意識したことはない。


GCPサービスとSocketAPI

最近知ったのだが、
GAE が GCP サービスと通信する場合、
内部で SocketAPI が利用されるケースがあるらしい。

パッと調べた限り、
以下の2つのパッケージを GAE で利用したところ、
SocketAPI のクオータが消費されていた。
*なぜこのパッケージを選択したのかは謎

1.cloud.google.com/go/datastore
2.cloud.google.com/go/logging

以前、
GAE Datastore と Cloud Datastore の golang パッケージって、
両方 GAE で使えそうなのに何が違うのだろう?
と思ったことがあった。

Cloud Datastore パッケージには、
Transaction.Commit(), Transaction.Rollback() のような、
GAE Datastore パッケージには存在しない API が実装されているので、
Cloud Datastore の方が高機能なパッケージだと思っていたが、
SocketAPI 経由でアクセスするかどうかの違いがあるみたい。

GAE では GAE Datastore パッケージを使った方がよさそう。

ところが、
全てのGCPサービスが SocketAPI 経由でコールされるわけではない。

試しに GAE から BigQuery にアクセスしても、
SocketAPI のクオータに変化はなかった。

BigQuery のソースをザックリと grep したところ、
BigQuery は urlfetch 経由でアクセスしているっぽい。
https://github.com/google/google-api-go-client/blob/3d8027ad5aac1c3f32ad0eeae4458a5d69682263/bigquery/v2/bigquery-gen.go#L6225-L6246

BigQuery アクセス後に urlfetch のクオータを確認してみると、変化があった。

一方、Cloud Datastore は grpc 経由でアクセスしていた。
https://github.com/google/go-genproto/blob/891aceb7c239e72692819142dfca057bdcbfcb96/googleapis/datastore/v1/datastore.pb.go#L1376-L1383

grpc を利用したアクセスだと、
SocketAPI が利用される or Socket利用 とみなされるみたい。

ザックリ grep しただけなので、
見当違いのコードを提示しているかもしれません。

この grep が正しいとした場合、
他のサービスは確認していないが、
以下のリポジトリ内にライブラリが存在するサービスは grpc 経由でアクセスしていると考えていいのだろうか?
https://github.com/google/go-genproto/tree/master/googleapis

リポジトリ内には pubsub パッケージがある。
pubsub は性質上、高トラフィックになりやすいと思うので、
注意した方がいいのかもしれない。

GAE から Cloud pubsub をガンガン利用する場合、
クオータを意識したことない とも言ってられない。

課金してないとエラー

ドキュメントにも明記してあるが、SocketAPI は課金を有効にしないと利用できない。

詳しくは覚えていないが、
課金無効状態で利用しようとすると、
TransientFailure 的なエラーが発生した気がする。

ついに自分も課金ユーザーになった。


まとめ

特にないです。
間違っていたら、twitter or この記事にコメントいただければと思います。

goddd とは何か?

これは Go Advent Calendar 2017 その2 6日目の記事です。
https://qiita.com/advent-calendar/2017/go2

みなさん、こんにちは。
pospome です。
普段は GAE/Go でサーバサイドの開発をしています。

twitter では 実装パターン, DDD, golang, GCP についてつぶやくことが多いので、
同じような分野に興味があれば、
フォローしてマサカリ投げてもらえると嬉しいです。
https://twitter.com/pospome

ということで本題に入ります。

golang.tokyo #9 で goddd という github リポジトリを知りました。
https://golangtokyo.connpass.com/event/65921/

ちょっと興味があったので、
goddd に対する自分の感想を書いていこうと思います。

goddd はDDD本のサンプル実装を golang に移植したもの

リポジトリは以下。
https://github.com/marcusolsson/goddd

cargo というディレクトリがあるので、
DDD本のサンプル実装を golang で書き直したものということが分かる。

README.md にも書いてありますね。

This is an attempt to port the DDD Sample App to idiomatic Go. This project aims to:

Demonstrate how the tactical design patterns from Domain Driven Design may be implemented in Go.
Serve as an example of a modern production-ready enterprise application.

自分も過去に PHP, Ruby で実装を考えたことがあるので、
それの golang バージョンというところでしょうか。

レイヤ構造

特徴的なのはレイヤ構造が明示的に存在しないこと。
つまり、presenter, application, domain, infra というパッケージが存在しない。

プロジェクトルートにある cargo, booking などのパッケージが、
domain だったり、
application だったりする。

例えば、
booking は application に相当するもので、
cargo は domain に相当する。

パッケージ内には presenter 相当の実装が入ってたりするので、
機能ごとの縦割りパッケージ構成に近い印象を受ける。

routing は domain service の interface でありながら、
実装(HTTP通信)も同じパッケージに持っているので、
DIPにはなっていない。

通常のレイヤ構造だと domain に interface を置き、
infra に実装を置くのだが、
そもそも goddd は明示的なレイヤ構造になっていないので、
問題ない気もする。

思ったが、
プロジェクトルートには mongo, inmem という infra 相当の概念も置かれていて、
それらの interface は domain に相当するパッケージに配置されている。
つまり、ここは DIP になっているみたい。

プロジェクトルートに各レイヤのパッケージを直置きしているので、
この実装のレイヤ構造としては、
見えないレイヤ構造が存在する or そもそもレイヤ構造ではない
という感じだろうか・・・。

プロジェクトルートに各レイヤのパッケージを直置きしているのは
golang っぽいパッケージ構成だなーと感じた。

このパッケージ構成(レイヤ構造)について気になる点は
そのパッケージが domain なのか、
domain 以外なのか が判別しづらい点だと思う。

一般的なWebシステムであれば、
mysql = infra レイヤの概念 になると思うが、
phpMyAdmin のような MySQL の Web GUI を作成する場合を考えると、
domain にも MySQL の概念が出てくる可能性が高い。

例えば、
GUI上でテーブル名を入力すると、
MySQLに CREATE TABLE を発行し、
テーブルを作成することができる「テーブル作成機能」があるとする。

プロジェクトルートに table というパッケージがあった場合、
それは domain としての table なのか?
それとも infra としての table なのか?

プロジェクトルートに sql というパッケージがあった場合、
それは domain だろうか?
infra だろうか?

地味に分かりづらくなってしまう。

明示的に domain, infra というレイヤを用意してあげれば、
以下のようにそれぞれを分かりやすく管理することができるだろう。

/domain/table
/infra/table



そもそも domain として技術の概念を扱うシステムはレイヤ構造に関係なく、
管理が難しいものだと思うが、
goddd のレイヤ構造だと
より複雑になってしまうかもしれない。

こういった懸念はあるものの、
goddd はそのケースに該当しないので、
明示的なレイヤ構造は不要と判断したのかもしれない。

また、レイヤが明示的に存在しないことによって、
domain service と application の判別が難しいという問題もある。

例えば、
booking.Service は application で、
routing.Service は domain service になる。

両方共 interface になっており、どう使われるかで判別する必要がある。

仮にドメイン(業務)を理解していたとしても、
「業務上 booking は application だよね」というように判断するのは難しそう。

レイヤ間の循環参照に気をつける必要がある

明示的なレイヤ構造が存在する場合は
レイヤ間の依存関係は単一方向に保たれることが保証される。
つまり、レイヤ内の循環参照に気をつければいい。

goddd の場合は明示的なレイヤが存在しないので、
プロジェクトルートに置かれている各パッケージが、
どのレイヤに相当するものなのかを理解した上で、
依存関係を管理する必要がある。

例えば

cargo は domain なので、どこにも依存しないが、
booking は application なので、
cargo, routing に依存しても問題ない。

みたいな感じで各パッケージと相当するレイヤを紐付ける必要がある。
さすがにパッケージ名でこれを判断するのは厳しいかな・・・。

なので、
レイヤが存在しない前提で、
それっぽく依存関係が単一方向になっていればOK という感じに妥協することになるかもしれない。

つまり、ドメイン相当のパッケージがインフラ相当のパッケージに依存していても、
単一方向依存であれば許容するということ。

MySQLではなく、MongoDBを利用している

DDD本のサンプルは Hibernat + MySQL を利用しているが、
goddd は MongoDB を利用している。

Hibernat は Java の ORM で、
以下のように XML でモデルとDBテーブルのマッピングを定義することができる。
https://github.com/citerus/dddsample-core/blob/3f87e2ddbb27b7c62f174ebbc4ee588f52875b7c/src/main/resources/se/citerus/dddsample/infrastructure/persistence/hibernate/Cargo.hbm.xml

どこまで柔軟にマッピングしてくれるかが分からないが、
これによって、モデルのデータ構造とDBのテーブル構造が乖離していても、
いい感じにマッピングしてくれるらしい。

ちなみに、増田さんは MyBatis を利用しているみたいだが、
これも柔軟なマッピングを重視した結果みたい。



一方 goddd は MongoDB を採用しているので、
struct を JSON にして突っ込んで終了だと思う。
マッピングに困ることはない印象。

IDDD本では MongoDB によるリポジトリ実装が紹介されていたりする。

MongoDBに対するトランザクションが未実装?

Reposiroty のトランザクションはレイヤ構造を考える時に面倒だったりする。

DDD本のサンプルは Java + Spring なので、
application に @Transactional というアノテーションを付けて
トランザクションを管理している。
https://github.com/citerus/dddsample-core/blob/3f87e2ddbb27b7c62f174ebbc4ee588f52875b7c/src/main/java/se/citerus/dddsample/application/impl/BookingServiceImpl.java#L33

一方、goddd はトランザクションを利用していない。

今回のサンプルでは
わざわざトランザクションを利用する必要がないと割り切ったのかな?

ここはちょっと分からない。

補完は便利

domain, infra などで扱う概念が、
そのままパッケージになっているので、
IDEとかの補完でレイヤごとの同じパッケージ名がズラッと出てきて、
よく分からん感じになることがなさそう。

地味に便利だなーと思った。

まとめ

あまり長々と書いてもしょーがないので、
一旦ここまでにしておきます。

goddd は
モデリング済みの Java 実装を golang に移植しただけなので、
goddd と同じレイヤ構造にする = DDD になる
というわけではありません。

goddd はレイヤアーキテクチャgolang 風にアレンジしたものにすぎないのです。

ただ、
goddd と同じレイヤ構造にする = DDD にならない
とも言い切れません。

DDD を実践するにあたって、
ドメインが他の概念(レイヤ)から隔離されていれば、
アーキテクチャの形は問わないはずです。

goddd のレイヤ構造でそれが担保でき、
チーム内の合意が取れていれば、
goddd を参考に DDD を実践することも可能だと思います。
*やりやすいかどうかは別問題ですが・・・

ここまでの説明で goddd が気になるようであれば、
各パッケージ間の依存関係を中心に一度目を通してみると面白いかもしれないですね。

golang のレイヤ構造において、他のコードに影響なくインフラレイヤのデータソース実装を差し替えることは可能か?

最近、golang のレイヤ構造において、他のコードに影響なくインフラレイヤのデータソース実装を差し替えることは可能か? という質問を受けた。

回答時間が限られている中で質問を受けたので、
「現実的には難しい」という雑な回答しかできなかった。

さすがに雑すぎるなと思ったので、
自分なりの回答をちゃんと残そうと思う。

影響を受ける対象となるコードは?

golang のレイヤ構造において、他のコードに影響なくインフラレイヤのデータソース実装を差し替えることは可能か?
というところの 他のコード とは具体的にどこになるのか?

これはレイヤアーキテクチャの依存関係上、
UIレイヤとアプリケーションレイヤが該当する。

ドメインレイヤも影響を受ける可能性はあるが、
ドメインレイヤとインフラレイヤに DIP を適用することで回避することができる。

ちなみに、
DIP はあくまで ドメインレイヤが影響を受けないようにするだけ なので、
DIP を適用する = インフラレイヤの差し替えが可能
というわけではない。
インフラレイヤに依存するUIレイヤとアプリケーションレイヤは影響を受ける。

今回はアプリケーションレイヤに注目して考えてみる。
アプリケーションレイヤへの考え方は、そのままUIレイヤに適用できるはず。

以下はアプリケーションレイヤの実装で、
AddUser() では User を保存するのに Repository を利用している。

pakcage application

func AddUser(user User) error {

	dbConnection := NewDBConnection()

	dbConnection.Begin()

	userRepository := NewUserRepository(dbConnection)

	if err := userRepository.Add(user); err != nil {
		dbConnection.Rollback()
		return error
	}

	dbConnection.Commit()

	return nil
}



UserRepository に関しては、
ドメインレイヤに interface を置き、

package domain

type UserRepository interface {
	AddUser(user User) error
}



インフラレイヤに実装を置く。

package infra

type UserRepository struct {
	conn DBConnection
}

func NewUserRepository(conn DBConnection) domain.UserRepository {
	return &UserRepository { conn }
}

func (u *UserRepository) AddUser(user domain.User) error {
	return u.conn.Insert(user)
}



今回はこの実装をベースにインフラレイヤのデータソース差し替えについて考えていく。

MySQL -> PostgresSQL への差し替え

先程紹介した実装が MySQL であるとして、
他のコードに影響なく PostgresSQL に差し替えることはできるだろうか?

結論から言うと、可能だと思う。

なぜかというと、
golangsql パッケージは、具体的な RDB に依存しないように作られているから。

先程紹介したアプリケーションレイヤ実装は擬似コードになっているが、
sql パッケージは利用するSQLドライバを差し替えるだけで MySQL -> PostgresSQL への差し替えができる。

ORM も sql パッケージに準拠しているはずなので、
問題なく差し替えられるはず。

もちろん、SQLドライバの import 文など細かい修正はあるだろうが、
基本インフラレイヤに閉じた修正になるので、
アプリケーションレイヤに影響を与えることはないはず。

ということで、
他の RDB への差し替えはレイヤ構造に関係なく可能だったりする。


MySQL -> WebAPI への差し替え

ここからが本番。
MySQL を WebAPI に差し替えることはできるだろうか?

結論から言うと、実装方法によっては可能 ということになる。

MySQL -> PostgresSQL への差し替えは sql パッケージが互換性を担保してくれていたが、
MySQL -> WebAPI は担保してくれないので、
自前で頑張る必要がある。

今回の実装例だと、
NewDBConnection() のように RDB に依存しているコードが存在する。
RDB に依存しているコードが存在する実装だと、 WebAPI への差し替えは無理。

pakcage application

func AddUser(user User) error {

	//RDBに依存している
	dbConnection := NewDBConnection()

	dbConnection.Begin()

	userRepository := NewUserRepository(dbConnection)

	if err := userRepository.Add(user); err != nil {
		dbConnection.Rollback()
		return error
	}

	dbConnection.Commit()

	return nil
}



どうすれば差し替えられるだろうか?

インフラレイヤにDB依存のコードをまるっと実装してしまう

パッと思いつくのは
インフラレイヤにDB依存のコードをまるっと実装してしまうアプローチ。

アプリケーションレイヤから
DB接続やトランザクションのコードを消してしまい、

pakcage application

func AddUser(user User) error {

	userRepository := NewUserRepository()

	if err := userRepository.Add(user); err != nil {
		return error
	}

	return nil
}



インフラレイヤ にそれらの処理を実装してしまう。

package infra

type UserRepository struct {
	conn DBConnection
}

func NewUserRepository() domain.UserRepository {
}

func (u *UserRepository) AddUser(user domain.User) error {
	u.conn := NewDBConnection()

	u.conn.Begin()

	if err := DBConnection.Insert(user); err != nil {
		u.conn.Rollback()
		return err
	}

	u.conn.Commit()
}



こうすれば、
MySQL -> WebAPI という差し替えでも、
アプリケーションレイヤに修正は不要。
インフラレイヤ内の修正だけ済む。

これは結構万能で、
WebAPI だけではなく、
redis や物理ファイルなどもいけるはず。

PaaS を利用する場合は、
PaaS 独自のオブジェクトを引き回す必要があるかもしれない。

ただ、この実装だとトランザクションが問題になる。

以下のように User と Task を同時に保存する場合、
User と Task にトランザクションをかけることができない。

pakcage application

func AddUser(user User, task Task) error {

	userRepository := NewUserRepository()
	if err := userRepository.Add(user); err != nil {
		return error
	}

	taskRepository := NewTaskRepository()
	if err := taskRepository.Add(task); err != nil {
		return error
	}

	return nil
}

User と Task は結果整合性になるので、
Task の保存が失敗した場合のリカバリ実装が必要になる。

また、アプリケーションレイヤでトランザクション管理していないので、
オブジェクトの整合性がどのスコープで保たれているのか?
を明示できてないのも気になるところ。

User と Task にトランザクションをかけるという観点では、
以下のように User, Task を一緒に保存するような Repository を作れば解決するが、

pakcage application

func AddUser(user User, task Task) error {

	userRepository := NewUserRepository()
	if err := userRepository.Add(user, task); err != nil {
		return error
	}

	return nil
}



しかし、以下の2点により、個人的には積極的に使おうとは思わない。

1. Repository 実装(インフラレイヤ)に User, Task を利用するロジックが流出する可能性がある
流出しないように書けばいいのだが、
チーム開発とかだと流出する可能性が高くなる懸念があるので、
現実的ではないかなと思う
アプリケーションレイヤ内で扱うオブジェクトが増えれば増えるほど、
引数が増えていき、
それに伴ってロジックが流出する可能が高くなるというのも懸念としてある。

2. User, Task を引数に取るインターフェースが不自然
これは完全に個人の好みだが、
Repository は単一の集約を CRUD するものなので、違和感がある。


DDDの場合

ここで、DDDの場合についても言及しておこうと思う。

DDDには
単一のトランザクションでは単一の集約のみ変更可能
というルールがあるので、
先程の User と Task をトランザクションで扱う場合は、
以下のように User が Task を持つようなモデリングになったり、

type User struct {
	Name string
	Tasks []*Task
}



User, Task を含む別の struct を定義して、
それらを保存する Repository を実装することになると思う。
*もちろん、ちゃんと意味のある struct が見つかった場合

type SomeThing struct {
	User User
	Tasks []*Task
}



そもそも、これらの集約のモデリングが難しい という別の問題もあるが、
DDDにおいては、
インフラレイヤにDB依存のコードをまるっと実装してしまうアプローチは悪くない気がしてくる。

と思ったが、
アプリケーションレイヤでトランザクション管理できていないし、
ドメインイベントのイベントストアを扱おうとすると、
このアプローチもしっくりこない。

イベントストアは集約更新と同じローカルトランザクションで管理し、
集約更新とイベント発行に整合性を持たせる必要があるので、
Repository の中でトランザクションを完結させてしてしまうと、
Repository の中でドメインイベントを発行する実装にする必要がある。
*ここは実装方法によって変わるので、「必要がある」とは限らないけど・・・。

しかし、
ドメインイベントは集約の振る舞い(メソッド)を通して発行されるものなので、
Repository の中でドメインイベントを発行する実装も基本的にNG。
Repository はドメインイベントを発行する責務を持たない。
*ただし、集約の振る舞いを通して発行されないドメインイベントは、ドメインイベント自体を集約として扱うので、Repository 内で発行してもOK。

独自の接続オブジェクトを作る

独自の接続オブジェクトを作るというのも、選択肢としてはある気がする。

以下の DBConnection を、

pakcage application

func AddUser(user User) error {

	dbConnection := NewDBConnection()

	dbConnection.Begin()

	userRepository := NewUserRepository(dbConnection)

	if err := userRepository.Add(user); err != nil {
		dbConnection.Rollback()
		return error
	}

	dbConnection.Commit()

	return nil
}



以下のように独自で作った AppConnection に変更する。

pakcage application

func AddUser(user User) error {

	appConnection := NewAppConnection()

	appConnection.Begin()

	userRepository := NewUserRepository(appConnection)

	if err := userRepository.Add(user); err != nil {
		appConnection.Rollback()
		return error
	}

	appConnection.Commit()

	return nil
}



AppConnection の実装は以下。

type AppConnection struct {
	conn DBConnection
}

func NewAppConnection() *AppConnection {
	conn := NewDBConnection()
	return &AppConnection {conn}
}

func (a *AppConnection) Begin() {
	//省略
}
func (a *AppConnection) Commit() {
	//省略
}
func (a *AppConnection) Rollback() {
	//省略
}

このようにDB接続を AppConnection で隠蔽することによって、
アプリケーションレイヤはDB接続に依存することがなくなる。

MySQL -> WebAPI に変更したいときは、以下のように AppConnection を修正する。

type AppConnection struct {
	webAPI WebAPI
}

func NewAppConnection() *AppConnection {
	webAPI := NewWebAPII()
	return &AppConnection {webAPI}
}

func (a *AppConnection) Begin() {
	//WebAPI にはトランザクションがないので、空実装にする。
	return
}
func (a *AppConnection) Commit() {
	//WebAPI にはトランザクションがないので、空実装にする。
	return
}
func (a *AppConnection) Rollback() {
	//WebAPI にはトランザクションがないので、空実装にする。
	return
}



考えるまでもなく、この実装には問題がある。

まず、WebAPI にはトランザクションがない可能性が高い。
WebAPI 以外のデータソースでもトランザクションが存在しないものは多い。

トランザクションが存在しないのに、
AppConnection はトランザクションを利用するための Begin(), Commit(), Rollback() を持っているので、
それらは空実装に変更する必要がある。

アプリケーションレイヤのコードを読むと、
さもトランザクションを利用しているように見えるが、
実際は利用していないということになる。
罠でしかない。

さらに、一部の Repository は WebAPI で、
他の Repository は RDB の場合、
AppConnection は利用できない。

おそらく、
以下のように NewRDBAppConnection() と NewRDBAppConnection() のように、
接続を生成する関数を複数作って、
アプリケーションレイヤで使い分けるような仕組みが必要になる。

type AppConnection struct {
	webAPI WebAPI
	db DBConnection
}

func NewAppConnection() *AppConnection {
	webAPI := NewWebAPII()
	return &AppConnection {
		WebAPI: webAPI,
	}
}

func NewAppConnection() *AppConnection {
	db := NewDBConnection()
	return &AppConnection {
		db: db,
	}
}



アプリケーションレイヤで使い分けるということは、
実装差し替えによって影響が出てしまうということなので、
この実装も微妙。

ちなみに、
こういったDBのトランザクションをラップする独自オブジェクトを作成する実装方法はあまり好きではない。

好きではないというか、
それなりの要件がない限り、
接続オブジェクトのファクトリを作れば事足りると思うので、
実装して嬉しくなるイメージがない。

自分も昔はDBのトランザクションをラップする独自オブジェクトを量産するタイプの人間だったけど、
こういった独自オブジェクトは DBManager, TransactionController のような
パッと見でヤバそうな命名がされている傾向にある気がする。

DDD本 & IDDD本のサンプルはどうなっているか?

他にもデータソースを差し替えれる実装はあるかもしれないが、
そもそも DDD本 & IDDD本 はデータソースの差し替えを担保してるのか?

サンプルも今回の実装例と同じようにデータソースとして RDB を利用している。

ただし、
java + spring ということもあり、
アノテーションRDBトランザクションを管理できるようになっている。
明示的なコミット、ロールバックのコードは不要だし、
ドメインイベントの発行も単一のトランザクションスコープで管理できている。

RDB を WebAPI にするとどうなるだろうか?

RDBトランザクションは効かなくなるが、
アプリケーションレイヤに明示的なコミット、ロールバックのコードが存在しないので、
トランザクションアノテーションを削除すれば修正完了な気がする。

サンプルでアプリケーションの修正が必要と思われるパターンとしては、
アノテーショントランザクションを制御できないデータソースに差し替える
というのが該当するのかな。

どんなデータソースにも差し替え可能というわけではないはず。

自分は java よく分からないので、
ほぼ勘で考察しただけだが、
javaAOP によるサポートがあるので、
golang ほど苦労しない印象を受ける。

差し替える必要はあるのか?

ということで、
自分が考えた結果、
アプリケーションレイヤに影響なく差し替えるのは難しいかなと思った。

自分はどうしているのかというと、
インフラレイヤの差し替えは考慮して実装していない。
差し替えが発生した場合、アプリケーションレイヤの修正も発生するのは仕方ないと思っている。

差し替え対象によって考慮する要素が替わってくるし、
差し替えが発生するかどうかも分からないので、
必要になった際にそれなりの工数をかけるのがシンプルな気がする。
もちろん、直近で想定されるのであれば、それなりの仕組みを実装するのがいいかもしれないが・・・。

ただ、ドメインレイヤに影響を与えることは避けたいので、
DIPを利用し、
ドメインレイヤに置かれている interface は特定の技術に依存しないように気をつけている。

具体的には、
以下のように interface の引数が RDB などの特定技術に依存していると、
間接的にインフラレイヤの修正に引きづられることになるので、

package domain

type UserRepository interface {
	// DBConnection は RDB に依存している
	GetByID(conn DBConnection, id int) (User, error)
}



以下のように依存しないようにしている。

package domain

type UserRepository interface {
	GetByID(id int) (User, error)
}



この場合、DBConnection は interface を実装する struct が持つことになる。

package infra

type UserRepository struct {
	conn DBConnection
}

func (u *UserRepository) GetByID(id int) (User, error) {
}

まとめ

今回はインフラレイヤの差し替えの中で一番面倒であろう、
データソース実装の差し替えについて考えた。
トランザクションを考慮する必要があるので、面倒かなーと。

データソース実装の差し替え以外の差し替えは、
アプリケーションレイヤに影響がない実装にすることが可能なケースが多いと思う。
ex. 暗号化、メール送信

長々と書いたが、
データソース実装の差し替えは色々面倒なので、
DIPドメインレイヤだけ守っていればいーかなと思っている。

Datastore へのキャッシュに Memcache を利用している場合、Memcache が落ちると Datastore のパフォーマンスが劣化することがある

Datastore へのキャッシュに Memcache を利用している場合、
Memcache が落ちると Datastore のパフォーマンスが劣化することがある。

当たり前と言えば、当たり前だが、
こういったケースを想定できていなかったので、
書いておく。

パフォーマンス劣化について

Memcache が落ちた際のインシデントが以下。

The Memcache service has recovered from a disruption between 12:30 US/Pacific and 15:30 US/Pacific.
https://status.cloud.google.com/incident/appengine/17007#5750790484393984

上記のインシデントに以下の記載がある。

Some customers experienced elevated Datastore latency and errors while Memcache was unavailable. At this time, we believe that all the Datastore issues were caused by surges of Datastore activity due to Memcache being unavailable. When Memcache failed, if an application sent a surge of Datastore operations to specific entities or key ranges, then Datastore may have experienced contention or hotspotting, as described in https://cloud.google.com/datastore/docs/best-practices#designing_for_scale. Datastore experienced elevated load on its servers when the outage ended due to a surge in traffic.

Memcache に Datastore のデータをキャシュしている場合、
Memcache が落ちると、
Datastore へのアクセスが急増してしまう。
その結果、エラーになる確率が上がったり、レスポンスが遅くなったりする。

Datastore へのアクセスに goon を利用していると、
このケースに該当する可能性がある。

ちなみに、エラーになる確率が上がったり、レスポンスが遅くなったりするだけで
全てのアクセスがエラーになるわけではない。
必ずしもサービスに影響が出るレベルでエラーが発生するとは限らない。

対策

Datastore の Entity の key 範囲を広くする

以下に記載があるように Datastore の Entity の key 範囲が狭い場合は、
特定のタブレットへのアクセスが集中し、
パフォーマンスが劣化する可能性が高くなる。
https://cloud.google.com/datastore/docs/best-practices#high_readwrite_rates_to_a_narrow_key_range

key 範囲を広くしましょう。
とはいえ、これは Datastore を利用する上で基本となるルールなので、守れていることが多いと思う。

シャーディング & レプリケーション

key 範囲を広くしていても、
ある特定の key に対するアクセスが集中する場合は
パフォーマンスが劣化してしまう。

以下に記載があるようにシャーディング、レプリケーションを検討するといい。
https://cloud.google.com/datastore/docs/best-practices#sharding_and_replication

シャーディングに関してはこちらにもまとめてある。
http://pospome.hatenablog.com/entry/2017/02/05/171635#エンティティグループは1秒間に1回しか更新できない--解決策

レプリケーションについては調べても分かりませんでした・・・。
知っている人は教えてください・・・。

タブレットの分割

key 範囲を広くしている & 特定の key に対するアクセスが集中しない
という場合でも、
アクセスに対してタブレットの数が少ないと(十分に分割されていないと)、
パフォーマンスが劣化する可能性がある。
https://cloud.google.com/datastore/docs/best-practices#ramping_up_traffic

過去 Memcache が落ちている期間に Datastore のエラーが発生しなくなったことがあるので、
アクセスが集中することによってタブレットが分割してくれたのかなと思っている。

事前にタブレットを分割させた方がいいのか? と言われると、
それなりに手間かかりそうなので微妙かもしれない・・・。

アプリケーションコードでのリトライ実装

わざわざリトライさせるのか? という感じではあるが、
Datastore のアクセスがエラーになると困るような場合はリトライを実装するといい。
全てのアクセスがエラーになるわけではないので、
短期間のリトライであっても成功するかもしれない。

まとめ

アクセスがエラーになった場合でも
データの整合性を損なわないように実装されていれば、
利用側のリトライによってケアできる問題なので、
あまり気にする必要はないかもしれない。

ただ、マイクロサービス化によって一部の処理が非同期化されていたりすると、
なかなか整合性を担保するのが難しかったりするので、
このようなケースがあるということを頭の片隅に入れておくといいかもしれない。

datastore のバックアップが失敗する際のエラー

Datastore のバックアップが失敗すると以下のようなエラーが出る。

Transaction collision. Retrying... (/base/data/home/runtimes/python27_experiment/python27_lib/versions/1/google/appengine/api/datastore.py:2688)

Contention on slice xxxxxxxxx execution. Will retry again. (/base/data/home/runtimes/python27_experiment/python27_lib/versions/1/google/appengine/_internal/mapreduce/handlers.py:305)

Datastore のバックアップは MapReduce によって処理されるので、
普段見慣れない MapReduce のエラーが発生する。

ここで疑問に思ったのが、
Datastore のトランザクション競合エラーが発生していること。
トランザクション競合が原因で MapReduce が失敗した感じなのかな?

バックアップだと、Datastore を read することはあっても、
write することはないので、
トランザクション競合が発生するとは思わなかった。

このエラーが発生した時刻には、
複数のバッチ処理が同時に動作していたので、
バックアップする時間帯に同じ EntityGroup に対してアクセスがあると、
トランザクション競合によって、
失敗する可能性が高くなるのかもしれない。

とはいえ、
ログに Will retry again とあるように失敗しても再実行されるので、
最終的にバックアップが失敗する確率は低いと思う。