Slide 1

Slide 1 text

ZOZO OWNリプレイスにおけるGoの 活用紹介
 株式会社ZOZO
 技術本部 ECプラットフォーム部
 山添 貴哉
 Copyright © ZOZO, Inc.

Slide 2

Slide 2 text

© ZOZO, Inc. 株式会社ZOZO
 技術本部 ECプラットフォーム部 API基盤ブロック
 サーバーサイドエンジニア
 山添 貴哉
 2021年4月に新卒入社
 マイクロサービスのサーバーサイド開発、運用に従事
 好きな言語はGo
 
 2

Slide 3

Slide 3 text

© ZOZO, Inc. 3 API基盤ブロック(チーム)
 チームのミッション
 将来のZOZO OWNの基盤を作り、ZOZO OWNリプレイスを進める
 
 チーム業務
 API Gateway開発・運用、ID基盤開発・運用
 ※API基盤ブロックはアプリケーション開発を、 Eブロックはインフラを主担当している
 
 チームメンバー
 現在5名、リーダー(=マネージャー)1名、テックリード1名


Slide 4

Slide 4 text

© ZOZO, Inc. 4 ● ZOZO OWNリプレイス
 ○ リプレイスの概要
 ○ リプレイスにおけるGoとJava
 ● リプレイスにおけるGoの活用
 ○ プロダクトの紹介
 ○ Goを使って感じたメリット
 ● Goを使った開発におけるテクニック
 ○ net/httpを用いたリバースプロキシの実装
 ○ http. ound ripperを使ったH Pクライアントのテスト
 ● まとめ
 アジェンダ


Slide 5

Slide 5 text

© ZOZO, Inc. 5 ZOZO OWNリプレイス


Slide 6

Slide 6 text

© ZOZO, Inc. 6 リプレイスの概要


Slide 7

Slide 7 text

© ZOZO, Inc. 7 規模拡大時期のZOZO OWN
 ZOZO OWNリプレイス
 2004年
 2019年
 アーキテクチャを変えないまま、規模を拡大してきた


Slide 8

Slide 8 text

© ZOZO, Inc. ZOZO OWNの成長のため
 スピード
 をあげる
 コスト
 をさげる
 人材
 をふやす
 8 リプレイスの目的
 ZOZO OWNリプレイス


Slide 9

Slide 9 text

© ZOZO, Inc. 9 リプレイスにおける
 GoとJava


Slide 10

Slide 10 text

© ZOZO, Inc. 10 ZOZO OWNにおけるGoとJava
 バックエンド(マイクロサービス)の開発にGoとJavaを採用
 機能
 言語
 API Gateway
 Go
 ID基盤
 Go
 推薦基盤
 Java
 検索基盤
 Java
 商品基盤
 Java
 カート決済基盤(開発中)
 Java
 ZOZO OWNリプレイス


Slide 11

Slide 11 text

© ZOZO, Inc. 11 GoとJavaの比較
 Go Java コンテナの起動 シングルバイナリで実行可能なため、 イメージサイズも小さく Javaと比較すると速い イメージが大きくなりがちなことや、 暖機処理の必要性などから Goと比較すると遅い 周辺環境 標準パッケージや標準の フォーマッターが揃っており、 スピード感のある開発が可能 Springなどの実績あるフレームワークが 豊富でスピード感のある開発が可能 人材 ZOZOではJavaと比較すると人材の 採用や育成が進んでいない ZOZOではJava人材の採用と育成が 比較的進んでいる GoとJavaはどちらもスピード感ある開発が可能な言語 ZOZOにおいて、人材や育成コストの面ではJavaに分がある API GatewayやID基盤など、多くのサービスで共通して使う機能をGoで書く ZOZO OWNリプレイス


Slide 12

Slide 12 text

© ZOZO, Inc. 12 リプレイスにおける
 Goの活用


Slide 13

Slide 13 text

© ZOZO, Inc. 13 プロダクトの紹介
 〜 API Gateway 〜


Slide 14

Slide 14 text

© ZOZO, Inc. 14 背景:リプレイスのアプローチ
 現行システムから新システムに段階的に移行する「ストラングラーパターン」を用い て、サービス断なしでリプレイスを進める
 モダン
 レガシ
 ストラングラーファサード
 レガシ
 モダン
 ストラングラーファサード
 モダン
 ストラングラーファサード
 ストラングラーパターンを採用するためにAPI Gatewayが必要
 リプレイスにおけるGoの活用


Slide 15

Slide 15 text

© ZOZO, Inc. 15 API Gatewayの概要
 役割
 内製化した理由
 クライアントからのH Pリクエストをマイクロサービスへ転送・ルーティングする役割
 ストラングラーパターンの実現など、リプレイスを円滑に進めるための機能
 を提供する
 AW にAPI Gatewayのマネージドサービスがあるが、ZOZO OWNの複雑なAPIリクエ スト制御を実現し、その他要望にも素早く対応するため
 リプレイスにおけるGoの活用


Slide 16

Slide 16 text

© ZOZO, Inc. 16 API Gatewayの機能
 ● ルーティング(加重ルーティングに対応)
 ○ パス、IPレンジ、クライアントに応じたルーティング
 ○ 加重ルーティング:旧サービスに80%、新サービスに20%のように比率を決められる仕組み
 ● APIクライアント認証
 ● メンバー認証
 ○ IDトークンによるメンバーの認証
 ● リトライ
 ● タイムアウト
 ● トレースID
 ● スロットリング(NEW!)
 リプレイスにおけるGoの活用


Slide 17

Slide 17 text

© ZOZO, Inc. 17 API Gatewayの動作イメージ
 リプレイスにおけるGoの活用
 標準のnet/httpを使って実装
 後編でnet/httpを使ったリバースプロキシの実装を紹介
 API Gateway
 ID基盤
 検索基盤
 O
 W
 ・・・ H Pサーバーとしての機能
 H Pクライアントとしての機能


Slide 18

Slide 18 text

© ZOZO, Inc. 18 プロダクトの紹介
 〜 ID基盤 〜


Slide 19

Slide 19 text

© ZOZO, Inc. 19 ID基盤の概要
 役割
 ZOZO OWNに関係する認証を行うための基盤
 リプレイスにおけるGoの活用


Slide 20

Slide 20 text

© ZOZO, Inc. 20 ID基盤の概要
 API Gatewayの配下にマイクロサービスとしてID基盤がある
 API Gateway
 ID基盤
 検索基盤
 O
 W
 ・・・ リプレイスにおけるGoの活用


Slide 21

Slide 21 text

© ZOZO, Inc. 21 ID基盤の概要
 リプレイスにおけるGoの活用
 API Gateway
 ID基盤
 他サービス
 W
 ①
 token:
 idaaa.bbb.1234
 ③
 internal token:
 internal1234
 ①
 token:
 idaaa.bbb.1234
 ②
 token:
 idaaa.bbb.1234
 ①ID基盤でトークン(JW )を発行
 ②クライアントからトークンと共にリクエスト
 ③送られたトークンをAPI Gateway上で検証後、 internal tokenに変換し各サービスに送信


Slide 22

Slide 22 text

© ZOZO, Inc. 22 ID基盤の機能
 ● ログイン(JW トークンの発行)
 ● ログアウト
 ● 認証情報(ID、パスワード、 M 、外部サービスID)の登録・更新・削除
 ● JW 用の公開鍵の提供
 ● トークンリフレッシュ
 ● …
 リプレイスにおけるGoの活用


Slide 23

Slide 23 text

© ZOZO, Inc. 23 ID基盤を支えるGo
 ● H Pサーバー、ルーティング
 ○ net/http
 ○ github.com/gorilla/mux
 リプレイスにおけるGoの活用
 ● データベース操作
 ○ database/sql
 var db *sql.DB q := "SELECT * from members where member_id = ?" memberID := 1 rows, e := db.Exec(q, memberID) // ... func main() { r := mux.NewRouter() r.HandleFunc("/members/{id}", memberController) log.Fatal(http.ListenAndServe(":8080", r)) } リクエストパラメータからパス 変数を取り出せる


Slide 24

Slide 24 text

© ZOZO, Inc. 24 Goを使って感じたメリット


Slide 25

Slide 25 text

© ZOZO, Inc. 25 Goを使って感じたメリット
 ● コンテナイメージが軽い、起動が速い
 ○ k8sにおけるローリングアップデートなど、再起動を頻繁に行うため
 ● 言語仕様がシンプル、学習コストが低い
 ○ テストパッケージや開発ツール(フォーマッターなど)が標準で提供されている
 ● クロスコンパイル、シングルバイナリ
 ○ 実行環境に依存しない
 リプレイスにおけるGoの活用


Slide 26

Slide 26 text

© ZOZO, Inc. 26 Goを使った開発におけるテクニック
 1. net/httpを用いたリバースプロキシの実装
 2. http. ound ripperを用いたH Pクライアントのテスト


Slide 27

Slide 27 text

© ZOZO, Inc. 27 net/httpを用いた
 リバースプロキシの実装


Slide 28

Slide 28 text

© ZOZO, Inc. 28 Goを使った開発におけるテクニック
 リバースプロキシ
 API Gateway
 ID基盤
 検索基盤
 O
 W
 ・・・ クライアントからのリクエストをマイクロサービスへ転送する機能
 H Pサーバーとしての機能
 H Pクライアントとしての機能


Slide 29

Slide 29 text

© ZOZO, Inc. 29 実現したいルーティング
 API Gateway
 ID基盤
 検索基盤
 ・ ・ ・ /auth/login /search/category/tops /search/category/shoes/sneakers http://api.auth.example/login http://api.search.example/category/tops http://api.search.example/category/shoes/sneakers Goを使った開発におけるテクニック


Slide 30

Slide 30 text

© ZOZO, Inc. 30 Goを用いたWebサーバーの実装
 package main import ( "io" "log" "net/http" ) func main() { // Hello world, the web server helloHandler := func(w http.ResponseWriter, req *http.Request) { io.WriteString(w, "Hello, world!\n") } http.HandleFunc("/hello", helloHandler) log.Fatal(http.ListenAndServe(":8080", nil)) } https://pkg.go.dev/net/http#example-ListenAndServe http.ListenAnd erve(addr string, handler http.Handler) でサーバーが起動する
 Goを使った開発におけるテクニック


Slide 31

Slide 31 text

© ZOZO, Inc. 31 Goを用いたWebサーバーの実装
 http.ListenAnd erve(addr string, handler http.Handler) でサーバーが起動する
 package main import ( "io" "log" "net/http" ) func main() { // Hello world, the web server helloHandler := func(w http.ResponseWriter, req *http.Request) { io.WriteString(w, "Hello, world!\n") } http.HandleFunc("/hello", helloHandler) log.Fatal(http.ListenAndServe(":8080", nil)) } https://pkg.go.dev/net/http#example-ListenAndServe ① ルーティングの登録
  /helloへリクエストがきたら
  helloHandlerを実行
 ② サーバーの起動
 Goを使った開発におけるテクニック


Slide 32

Slide 32 text

© ZOZO, Inc. 32 http.Handler
 type Handler interface { ServeHTTP(ResponseWriter, *Request) } ● http. esponseWriterと*http. equestを引数に取る、 erveH Pメソッドを持つインターフェー ス
 type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } ● http.HandlerFuncはラップした関数を実行するだけの、最もシンプルなhttp.Handler
 Goを使った開発におけるテクニック


Slide 33

Slide 33 text

© ZOZO, Inc. 33 何を実装すべきか
 ● 独自のhttp.Handlerを実装して、http.ListenAnd erveの引数に渡す
 ● erveH P内でhttp. equestから適切なマイクロサービスにリクエストを送る
 type ReverseProxy struct{} func (r *ReverseProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { // ルーティングとHTTPクライアントの処理 } func main() { reverseProxy := ReverseProxy{} log.Fatal(http.ListenAndServe(":8080", &reverseProxy)) } Goを使った開発におけるテクニック


Slide 34

Slide 34 text

© ZOZO, Inc. 34 http.Handlerの実装例
 var re = regexp.MustCompile("^/search/(.+)$") type ReverseProxy struct{} func (r *ReverseProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { if re.MatchString(req.URL.Path) { path := re.ReplaceAllString(req.URL.Path, "/$1") targetURL := "http://api.search.example" + path resp, _ := http.Get(targetURL) io.Copy(w, resp.Body) } } func main() { reverseProxy := ReverseProxy{} log.Fatal(http.ListenAndServe(":8080", &reverseProxy)) } Goを使った開発におけるテクニック
 リクエストのパスを集約するための 正規表現を用意
 erveH Pを実装した everseProxy型を用意 用意した正規表現に一致したら
 ホストとパスをマイクロサービス向き に置き換える 置き換えた Lにリクエストを送る http.Handler引数に everseProxyを 渡す

Slide 35

Slide 35 text

© ZOZO, Inc. 35 実装の工夫
 ● ルーティング情報を設定ファイルから読み込み
 ○ 受け付けるパスと転送先のサービス、パスをyamlに記述
 ○ API Gatewayの起動時にinit関数で読み込む
 ● 加重ルーティング機能
 ○ カナリアリリースのため、転送先サービスの比重を決めることが可能
 ● H Pクライアントとしての機能
 ○ リクエストのタイムアウト
 ○ リクエストのリトライ
 Goを使った開発におけるテクニック


Slide 36

Slide 36 text

© ZOZO, Inc. 36 API Gatewayに関するテックブログ
 【ZOZO OWNマイクロサービス化】
 API Gatewayを自社開発したノウハウ大公開!
 【ZOZO OWNマイクロサービス化】API Gatewayの可用性を 高めるノウハウを惜しみなく大公開
 https://techblog.zozo.com/entry/zozotown-api-gateway-availability https://techblog.zozo.com/entry/zozotown-api-gateway-intro Goを使った開発におけるテクニック


Slide 37

Slide 37 text

© ZOZO, Inc. 37 http. ound ripperを用いた
 H Pクライアントのテスト


Slide 38

Slide 38 text

© ZOZO, Inc. 38 H Pクライアントのテスト
 ID基盤 連携システム 連携システム (Dockerのモックサーバーなど) 本番のAPIリクエスト テスト実行時の APIリクエスト テスト実行時に 本番環境は使わない 外部サーバーにH Pリクエストを送るクライアントのテスト
 テスト時には、複数のモックサーバー(成功用、失敗用、・・・)を使い分けたい
 本番環境 成功レスポンスを返すサーバー 失敗レスポンスを返すサーバー ID基盤 テスト環境 Goを使った開発におけるテクニック


Slide 39

Slide 39 text

© ZOZO, Inc. 39 方法の選択肢
 ● 環境変数を使ってAPIホストを上書きする ● http.RoundTripperを使ってリクエストの向き先を書き換える func TestClient(t *testing.T) { original := os.Getenv("API_SERVER_HOST") t.Cleanup(func() { os.Setenv("API_SERVER_HOST", original) }) os.Setenv("API_SERVER_HOST", "http://api.example.com") // Clientを使ったテスト } 本番のAPIホストを環境変数で管理していない場合は使えない 今回はこちらの方法を紹介 Goを使った開発におけるテクニック


Slide 40

Slide 40 text

© ZOZO, Inc. 40 http.Client
 type Client struct { // Transport specifies the mechanism by which individual // HTTP requests are made. // If nil, DefaultTransport is used. Transport RoundTripper CheckRedirect func(req *Request, via []*Request) error Jar CookieJar Timeout time.Duration } type RoundTripper interface { // RoundTrip executes a single HTTP transaction, returning // a Response for the provided Request. RoundTrip(*Request) (*Response, error) } ● http.Clientの通信の実態は、 http. ound ripperインターフェースの実 装が担う
 
 ● ransportフィールドがnilの場合は、 Default ransportが使われる
 Goを使った開発におけるテクニック


Slide 41

Slide 41 text

© ZOZO, Inc. 41 リクエスト書き換えの実装方針
 http.Client.Do(req *Request) http.Client.do(req *Request) DefaultTransport.RoundTrip(req *Request) http.Client.Do(req *Request) http.Client.do(req *Request) OverrideTransport.RoundTrip(req *Request) DefaultTransport.RoundTrip(req *Request) 通常の処理 リクエスト先を書き換える処理 デフォルトのRoundTripperをラップしたRoundTripperを作り、その中でリクエストの向きを変える リクエストの向きを変えた後はデフォルトのRoundTripperの処理を呼び出す Goを使った開発におけるテクニック


Slide 42

Slide 42 text

© ZOZO, Inc. 42 http. ound ripperを用いた実装
 type OverrideTransport struct { Transport http.RoundTripper Overrides map[string]string } func (t OverrideTransport) RoundTrip(req *http.Request) (*http.Response, error) { if to, ok := t.Overrides[req.URL.String()]; ok { toURL, e := url.Parse(to) if e != nil { return nil, e } req.URL.Scheme = toURL.Scheme req.URL.Host = toURL.Host req.URL.Path = toURL.Path req.Host = toURL.Host } rt := t.Transport if rt == nil { return nil, errors.New("transport is undefined") } return rt.RoundTrip(req) } 書き換えるホストを管理するためのフィー ルド
 
 
 
 
 リクエスト先が、書き換え対象だった場合 は Lを書き換える
 
 
 デフォルトの ound rip()を実行
 Goを使った開発におけるテクニック


Slide 43

Slide 43 text

© ZOZO, Inc. 43 Override ransportを使ったテスト
 func OverrideHTTPRequest(t *testing.T, overrides map[string]string) { original := http.DefaultTransport http.DefaultTransport = OverrideTransport{Transport: original, Overrides: overrides} t.Cleanup(func() { http.DefaultTransport = original }) } func TestHTTPClient(t *testing.T) { OverrideHTTPRequest(t, map[string]string{"http://api.example.com": "http://test.example.com"}) // http://api.example.comへのリクエストを送るクライアントのテスト } ● Default ransportをラップするための関数を用意
 ● テスト終了後に、Default ransportを元の値に戻す
 ● テストから呼び出す
 Goを使った開発におけるテクニック


Slide 44

Slide 44 text

© ZOZO, Inc. 44 まとめ


Slide 45

Slide 45 text

© ZOZO, Inc. 45 まとめ
 ● ZOZO OWNバックエンド開発の技術スタックの1つとしてGoを採用
 ○ API Gateway
 ○ ID基盤
 
 ● Goを使う中で学んだ開発のテクニックの紹介
 ○ net/httpを用いたリバースプロキシの実装
 ○ http. ound ripperを用いたH Pクライアントのテスト


Slide 46

Slide 46 text

No content