Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ZOZOTOWNリプレイスにおけるGoの活用紹介 / ZOZOTOWN REPLACE with go

takaya
February 17, 2022

ZOZOTOWNリプレイスにおけるGoの活用紹介 / ZOZOTOWN REPLACE with go

https://zozotech-inc.connpass.com/event/236413/
ZOZO.go Meetupで発表した資料

takaya

February 17, 2022
Tweet

Transcript

  1. © ZOZO, Inc. 株式会社ZOZO
 技術本部 ECプラットフォーム部 API基盤ブロック
 サーバーサイドエンジニア
 山添 貴哉


    2021年4月に新卒入社
 マイクロサービスのサーバーサイド開発、運用に従事
 好きな言語はGo
 
 2
  2. © ZOZO, Inc. 3 API基盤ブロック(チーム)
 チームのミッション
 将来のZOZO OWNの基盤を作り、ZOZO OWNリプレイスを進める
 


    チーム業務
 API Gateway開発・運用、ID基盤開発・運用
 ※API基盤ブロックはアプリケーション開発を、 Eブロックはインフラを主担当している
 
 チームメンバー
 現在5名、リーダー(=マネージャー)1名、テックリード1名

  3. © ZOZO, Inc. 4 • ZOZO OWNリプレイス
 ◦ リプレイスの概要
 ◦

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

  4. © ZOZO, Inc. 7 規模拡大時期のZOZO OWN
 ZOZO OWNリプレイス
 2004年
 2019年


    アーキテクチャを変えないまま、規模を拡大してきた

  5. © ZOZO, Inc. 10 ZOZO OWNにおけるGoとJava
 バックエンド(マイクロサービス)の開発にGoとJavaを採用
 機能
 言語
 API

    Gateway
 Go
 ID基盤
 Go
 推薦基盤
 Java
 検索基盤
 Java
 商品基盤
 Java
 カート決済基盤(開発中)
 Java
 ZOZO OWNリプレイス

  6. © ZOZO, Inc. 11 GoとJavaの比較
 Go Java コンテナの起動 シングルバイナリで実行可能なため、 イメージサイズも小さく

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

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


    レガシ
 モダン
 ストラングラーファサード
 モダン
 ストラングラーファサード
 ストラングラーパターンを採用するためにAPI Gatewayが必要
 リプレイスにおけるGoの活用

  8. © ZOZO, Inc. 15 API Gatewayの概要
 役割
 内製化した理由
 クライアントからのH Pリクエストをマイクロサービスへ転送・ルーティングする役割


    ストラングラーパターンの実現など、リプレイスを円滑に進めるための機能
 を提供する
 AW にAPI Gatewayのマネージドサービスがあるが、ZOZO OWNの複雑なAPIリクエ スト制御を実現し、その他要望にも素早く対応するため
 リプレイスにおけるGoの活用

  9. © ZOZO, Inc. 16 API Gatewayの機能
 • ルーティング(加重ルーティングに対応)
 ◦ パス、IPレンジ、クライアントに応じたルーティング


    ◦ 加重ルーティング:旧サービスに80%、新サービスに20%のように比率を決められる仕組み
 • APIクライアント認証
 • メンバー認証
 ◦ IDトークンによるメンバーの認証
 • リトライ
 • タイムアウト
 • トレースID
 • スロットリング(NEW!)
 リプレイスにおけるGoの活用

  10. © 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に変換し各サービスに送信

  11. © ZOZO, Inc. 22 ID基盤の機能
 • ログイン(JW トークンの発行)
 • ログアウト


    • 認証情報(ID、パスワード、 M 、外部サービスID)の登録・更新・削除
 • JW 用の公開鍵の提供
 • トークンリフレッシュ
 • …
 リプレイスにおけるGoの活用

  12. © 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)) } リクエストパラメータからパス 変数を取り出せる

  13. © ZOZO, Inc. 25 Goを使って感じたメリット
 • コンテナイメージが軽い、起動が速い
 ◦ k8sにおけるローリングアップデートなど、再起動を頻繁に行うため
 •

    言語仕様がシンプル、学習コストが低い
 ◦ テストパッケージや開発ツール(フォーマッターなど)が標準で提供されている
 • クロスコンパイル、シングルバイナリ
 ◦ 実行環境に依存しない
 リプレイスにおけるGoの活用

  14. © ZOZO, Inc. 28 Goを使った開発におけるテクニック
 リバースプロキシ
 API Gateway
 ID基盤
 検索基盤


    O
 W
 ・・・ クライアントからのリクエストをマイクロサービスへ転送する機能
 H Pサーバーとしての機能
 H Pクライアントとしての機能

  15. © 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を使った開発におけるテクニック

  16. © 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を使った開発におけるテクニック

  17. © 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を使った開発におけるテクニック

  18. © 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を使った開発におけるテクニック

  19. © 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を使った開発におけるテクニック

  20. © 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を 渡す
  21. © ZOZO, Inc. 35 実装の工夫
 • ルーティング情報を設定ファイルから読み込み
 ◦ 受け付けるパスと転送先のサービス、パスをyamlに記述
 ◦

    API Gatewayの起動時にinit関数で読み込む
 • 加重ルーティング機能
 ◦ カナリアリリースのため、転送先サービスの比重を決めることが可能
 • H Pクライアントとしての機能
 ◦ リクエストのタイムアウト
 ◦ リクエストのリトライ
 Goを使った開発におけるテクニック

  22. © 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を使った開発におけるテクニック

  23. © ZOZO, Inc. 38 H Pクライアントのテスト
 ID基盤 連携システム 連携システム (Dockerのモックサーバーなど)

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

  24. © 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を使った開発におけるテクニック

  25. © 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を使った開発におけるテクニック

  26. © 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を使った開発におけるテクニック

  27. © 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を使った開発におけるテクニック

  28. © 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を使った開発におけるテクニック

  29. © ZOZO, Inc. 45 まとめ
 • ZOZO OWNバックエンド開発の技術スタックの1つとしてGoを採用
 ◦ API

    Gateway
 ◦ ID基盤
 
 • Goを使う中で学んだ開発のテクニックの紹介
 ◦ net/httpを用いたリバースプロキシの実装
 ◦ http. ound ripperを用いたH Pクライアントのテスト