InfoQ eMAG: Microservicesを少し読んだまとめと感想

なんだか、世間的にMicroservicesが次の注目対象になりそうな感じです。

DockerConでも複数のセッションで言及があったり、はてブにもたまに関連記事があがってきたりしてます。

実態はどんなものかイメージしにくかったのですが、最近InfoQで公開されたeMAGが情報としてまとまっている気がしたので、これを読み進めています。

本に含まれている内容

この本は、過去のInfoQ記事や各ブログを再構成したもので、以下の記事タイトルがまとめられています。

  • Microservices: Decomposing Applications for Deployability and Scalability
  • Microservices and SOA
  • Adrian Cockcroft on Microservices and DevOps
  • Microservices? What about Nanoservices?
  • Building Products at SoundCloud
  • The Strengths and Weaknesses of Microservices
  • GOTO Berlin: Microservices as an Alternative to Monoliths

そのうち、以下を読んだので、まとめと雑感を書きます。

  • Microservices: Decomposing Applications for Deployability and Scalability
  • Building Products at SoundCloud

本の主題はMicroservices Architectureですが、それに伴うソフトウェアの設計手法などの参照もたくさん含まれているので、いろんな情報への入口としても勉強になると思います。

Microservices: Decomposing Applications for Deployability and Scalability

  • ドメイン単位で複数に分割されたもの小さなサービスのの集合体をMicroservices Architectureと呼ぶ
  • 既存フレームワークとかでservice層として定義していた単位が別々のプロセスに切りだされているイメージ
  • 一方、既存のシステムはMonolithic Architectureと呼ばれる
  • システム設計における単一責任原則の適用ともいえる
  • 現実に則したSOAの焼き直しである

メリット

  • 個々にデプロイ可能である
  • 個々のMicroserviceが独立しているため、サービス全体の可用性向上を期待できる
  • 開発チームを小さい単位に分割しやすい
  • 言語やフレームワークの移行が容易になる。変更が小規模なので仮に失敗しても取り戻しやすい。

デメリット

  • 個々にはシンプルでも全体の複雑性は増す
  • サービス間のIPCメカニズムが必要
  • サービス間をまたいだテストがやりにくい
  • 他サービスに影響する変更のデプロイは手順を十分に検討する必要がある
  • 運用が大変。ハイレベルな自動化が要求される

Building Products at SoundCloud

SoundCloudのMicroservices移行話は「Building Products at SoundCLoud」という三回分のブログ記事を再構成してまとめた内容になっています。

MonolithicなRailsアプリからMicroservicesに移行するまでの手順や失敗が紹介されていて、これから移行を進めるチームにとって有益になるでしょう。

彼らは既存アプリの機能をいきなり別言語で切り出すのではなく、

分析→リファクタリング→Service分割→新技術の採用

という手順を踏んでいます。この移行における重要な点は、どのような新技術を採用するかではなく、DDDをベースとしたドメイン分析を行うことにあると思います。分割の単位を誤ったなら個々のサービスの依存性を排除しきれなくなり全体の複雑性は確実に高くなります。

ドメイン分割の一例として、Bounded Contextを利用した例が紹介されています。

また、依存性の低い個々のMicroserviceにできることで、それぞれに独立したチームを配置することも可能になります。 ただ、それぞれのチームが無秩序に技術選定を行うことでスキルや情報の局所性(Bus Factorの低下)を招くことになるため、一定のルールを決めることも重要になるようです。結果として彼らはJVMベースのScala,ClojureJRubyを中心としたシステムを作ることを決定しています。(同時にGoとRubyもサポート)

雑感

Microservicesへ移行するにはサービス規模・システム規模・開発チーム規模が大きいことが前提であり、そうでない限りはMonolithicなシステムで頑張るほうが良いかな、というのが個人的な意見です。また、DDDのような適切なドメイン分析ができるアーキテクトが存在しない場合にもMicroservicesはあまり良い選択肢とはいえないと思います。それらの前提をクリアできるのであれば、長いプロダクトのライフスパンにおいて、システムそのものだけでなく開発チームも含めて良い影響をもらたすことになるのではないでしょうか。

ということで「Microservicesの本質はドメイン分析であり、デプロイ単位や技術選定などのメリットは副次的なものである」というのがこのeMAGを読んで得たとりあえずの結論です。

参考リンク

Martin Fowler氏の記事だけでなく、DockerConでのYelpのスライドやInfoQのbitlyの記事も参考になります。

Goのjson.Marshal/Unmarshalの仕様を整理してみる

TL;DR

  • goの構造体につけるタグは、フォーマットが不正だと読み込まれない(当然)
  • json.Marshalは、構造体のjsonタグがあればその値をキーとしてJSON文字列を生成する
  • json.Unmarshalは、構造体のjsonタグがあればその値を対応するフィールドにマッピングする
    • jsonタグがなければ、完全一致もしくはcase-insensitiveなフィールドにマッピングする

では、ひとつづつ確認していきます。

goの構造体につけるタグは、フォーマットが不正だと読み込まれない(当然)

goの構造体にはタグの機能があって、型の後に特定のフォーマットでアノテーションが記述できます

1
2
3
type MyType struct {
  Hoge string `foo:"1" bar:"2"`
}

key:"value"をスペース区切りで複数かけます。 で、アクセスするにはリフレクションを使います。

1
2
3
4
5
t := reflect.TypeOf(MyType{})
tagFoo := t.Field(0).Get("foo")
tagBar := t.Field(0).Get("bar")
fmt.Printf("foo=%s, bar=%s\n", tagFoo, tagBar)
// foo=1, bar=2

タグのフォーマットが不正な場合は、値が空になります。コンパイルエラーにはなりません。

1
2
3
4
5
6
7
type MyType struct {
  Hoge string `foo:1`
}
t := reflect.TypeOf(MyType{})
tagFoo := t.Field(0).Get("foo")
fmt.Printf("foo=%#v\n", tagFoo)
// foo=""

このエラーをコンパイルする前に検知するには、go vetコマンドが使えます。

1
2
$ go vet my_type.go
my_type.go:10: struct field tag `foo:1` not compatible with reflect.StructTag.Get

できればgo compile実行したときに自動的にチェックしてほしいところですが…

で、jsonパッケージのMarshal/Unmarshalは、このタグを使ってアノテーションを記述することができます。

json.Marshalは、構造体のjsonタグがあればその値をキーとしてJSON文字列を生成する

json.Marshalは構造体からJSON文字列への変換する関数です。

1
2
3
4
5
6
7
8
9
type MyType struct {
  A string
  FooBar string
}

mt := MyType{"aaaa", "baz"}
b, _ := json.Marshal(mt)
fmt.Printf("%s\n", string(b))
// {"A":"aaaa","FooBar":"baz"}

普通にやると、フィールド名がそのままJSONのキーになります。

ここで構造体のタグ機能をつかって、フィールド名のアノテーションを書くことで、任意のフィールド名でJSONが生成できます。

1
2
3
4
5
6
7
8
9
type MyType struct {
  A string `json:"a"`
  FooBar string `json:"foo_bar"`
}

mt := MyType{"aaaa", "baz"}
b, _ := json.Marshal(mt)
fmt.Printf("%s\n", string(b))
// {"a":"aaaa","foo_bar":"baz"}

アノテーション指定した”a”がJSONのキーとして使用されていることが確認できます。

json.Unmarshalは、構造体のjsonタグがあればその値を対応するフィールドにマッピングする

json.UnmarshalはJSON文字列から構造体へ変換する関数です。

まずはタグなしのパターン

1
2
3
4
5
6
7
8
type MyType struct {
  A string
  FooBar string
}

var mt MyType
json.Unarshal([]byte(`{"A":"aaa", "FooBar":"baz"}`, &mt)
fmt.Printf("%#v\n", mt) // main.MyType{A:"aaaa",FooBar:"baz"}

JSONのフィールド名がsnake_caseのパターン

1
2
3
var mt MyType
json.Unarshal([]byte(`{"a":"aaa","foo_bar":"baz"}`, &mt)
fmt.Printf("%#v\n", mt) // main.MyType{A:"aaaa", FooBar:""}

ここで「ん?」となるわけです。タグによるアノテーションがないのになぜ小文字のフィールドがちゃんと読み込まれているのか…と。

この仕様、json.Unmarshalのgodocにちゃんと書いてあります。

func Unmarshal

To unmarshal JSON into a struct, Unmarshal matches incoming object keys to the keys used by Marshal (either the struct field name or its tag), preferring an exact match but also accepting a case-insensitive match.

どうやら、json.Unmarshalは、JSONに含まれるキーと完全一致か、case-insensitiveに一致する構造体のフィールドにマッピングする仕様となっているようです。もちろん、snake_caseなJSONのキーはアノテーションがないとマッピングされず無視されます。(構造体のフィールドとしてsnale_caseが存在しない限りは)

経緯

先日、ヒカルのGO! hikarie.go #2@yosuke_furukawaさんによるGoでJSON APIを書いてみるというハンズオンを体験してきました。

そこで、アノテーションをつけた構造体を使ってMarshalした時に、書いたコードがこれです。

1
2
3
4
5
6
type User struct {
  Name string `json:name`
}
user := User{"restartr"}
b, _ := json.Marshal(user)
fmt.Printf("%s\n", string(b)) // {"Name":"restartr"}

小文字で出力してほしいのに、アノテーションが効かない…となったわけです。 他の参加者がjson:"name"でないといけないことに気づいて、この件は一件落着。

だったのですが、もう少し詳しく調べると上に整理したような仕様が見えてきたというわけです。

1
2
3
4
5
6
type User struct {
  Name string `json:name` // 実際は無視されている
}
var user User
json.Unmarshal([]byte(`{"name":"restartr"}`, &user)
fmt.Printf("%s\n", user) // main.User{Name:"restartr"}

実はこの時書いたjson:nameというアノテーションは不正なフォーマットとして無視されていて、たまたまUser.Nameというフィールドにcase-insensitiveでUnmarshalした時にマッピングができていたにすぎなかったようです。

とまぁそんな感じでJSONの取り扱いについて正しい知識を得られたのですが、一人でやってた時には気づかなかった疑問とか知識として不足している点に気付けるので、ハンズオン形式も良いものですね。