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

SNKRDUNKでGo+gRPCで すすめるモジュラモノリス

SNKRDUNKでGo+gRPCで すすめるモジュラモノリス

株式会社SODAで運営するSNKRDUNKでは、サービス、リポジトリ、組織の急激な拡大に伴い、開発スピード、品質を落とさないようにGo+gRPCを利用し、将来的にマイクロサービスも視野にいれながらモジュラモノリスへのリプレイスを進めています。
社内で実践しているGo+gRPCでのモジュラモノリスでの実装方法や、現状抱えている課題などをお話できればと思っています。

Shoei Watanabe

October 15, 2022
Tweet

More Decks by Shoei Watanabe

Other Decks in Programming

Transcript

  1. 技術スタック Pub/Sub 決済代行業 者 Worker Batch Server 決済Proxy JP Store

    Admin Batch Server GB Store Internal AWS Local CI/CD Monitoring (HTTPS endpoints)
  2. 課題: モノリスでコードが大きくなりすぎている • Backendエンジニアが全員で同じリポジトリで開発してる ◦ 特に境界はなく、必要な機能に応じて必要な部分を実装する ◦ コンフリクトが多発 • 認知負荷が高い

    ◦ 新規メンバーが全体を把握するのは難しい • リリーススピードの低下 ◦ CI/CDが遅い • プロダクト品質の低下 ◦ 思いも寄らない部分に影響がある
  3. 改善案: モジュラモノリスに移行していく • モジュール単位に分割し、モジュールごとに担当チームを決める ◦ チームの自立性を高める ◦ 認知負荷をさげる ◦ プロダクト品質の改善

    • 将来的にマイクロサービスへの移行も視野にいれながら、ドメインの境界 をさぐる • デプロイラインはひとつなのでCI/CDが遅い問題は解決できない
  4. ディレクトリ構成 . ├── cmd # 各アプリケーションのmain.go ├── pkg # Goのパッケージ郡

    ├── protobuf │ ├── gen # protoファイルから生成したファイル郡 │ │ ├── doc │ │ │ └── snkrdunk │ │ │ └── search │ │ │ └── v1 │ │ │ └── index.md │ │ └── go │ │ └── snkrdunk │ │ └── search │ │ └── v1 │ │ ├── service.pb.go │ │ └── service_grpc.pb.go │ ├── proto # protoファイル │ │ └── snkrdunk │ │ ├── search │ │ │ └── v1 │ │ │ └── service.proto │ │ └── buf.yaml │ ├── buf.gen.yaml │ └── buf.work.yaml ├── services # 各モジュールを実装するディレクトリ │ └── search │ └── service.go ├── go.mod └── go.sum
  5. Protocol Buffersでインターフェースを定義 syntax = "proto3"; package snkrdunk.search.v1; // `SearchService` provides

    a way to search for products by keywords. // - Each RPC may return Internal but it is not listed in the [ERRORS] section // for brevity. service SearchService { // Returns a list of suggestions for search keyword. rpc ListSuggestions(ListSuggestionsRequest) returns (ListSuggestionsResponse); } // `ListSuggestions` returns a list of suggestions and the count of results for // a search keyword. // - If the keyword is empty, an empty list is returned. // - If there are no suggestions for the search keywords, an empty list is // returned. // - The list of suggestions is returned in descending order of search count. message ListSuggestionsRequest { // Search keyword. string keyword = 1; // Limit of suggestions list. int32 limit = 15; }
  6. Protocol Buffersの運用 • bufを利用 ◦ https://github.com/bufbuild/buf • Goのコードとドキュメントを生成 • CIでformatエラー

    / lintエラー / breaking changeを検出する • mainにマージされたらProtocol Buffersの破壊的変更は不可 ◦ 破壊的変更があるとCIでおちる buf generate / buf format / buf lint / buf breaking
  7. 生成されたServerのコード // SearchServiceServer is the server API for SearchService service.

    // All implementations must embed UnimplementedSearchServiceServer // for forward compatibility type SearchServiceServer interface { // Returns a list of suggestions for search keyword. ListSuggestions(context.Context, *ListSuggestionsRequest) (*ListSuggestionsResponse, error) mustEmbedUnimplementedSearchServiceServer() } // UnimplementedSearchServiceServer must be embedded to have forward compatible implementations. type UnimplementedSearchServiceServer struct { } func (UnimplementedSearchServiceServer) ListSuggestions(context.Context, *ListSuggestionsRequest) (*ListSuggestionsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListSuggestions not implemented") } func (UnimplementedSearchServiceServer) mustEmbedUnimplementedSearchServiceServer() {} // UnsafeSearchServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to SearchServiceServer will // result in compilation errors. type UnsafeSearchServiceServer interface { mustEmbedUnimplementedSearchServiceServer() } func RegisterSearchServiceServer(s grpc.ServiceRegistrar, srv SearchServiceServer) { s.RegisterService(&SearchService_ServiceDesc, srv) }
  8. gRPC Serverのexample // https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_server/main.go // server is used to implement

    helloworld.GreeterServer. type server struct { pb.UnimplementedGreeterServer } // SayHello implements helloworld.GreeterServer func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } func main() { flag.Parse() lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) log.Printf("server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
  9. RegisterSearchServiceServerの処理 // protobuf/gen/go/snkrdunk/search/v1/service_grpc.pb.go func RegisterSearchServiceServer(s grpc.ServiceRegistrar, srv SearchServiceServer) { s.RegisterService(&SearchService_ServiceDesc,

    srv) } // https://github.com/grpc/grpc-go/blob/master/server.go // ServiceRegistrar wraps a single method that supports service registration. It // enables users to pass concrete types other than grpc.Server to the service // registration methods exported by the IDL generated code. type ServiceRegistrar interface { // RegisterService registers a service and its implementation to the // concrete type implementing this interface. It may not be called // once the server has started serving. // desc describes the service and its methods and handlers. impl is the // service implementation which is passed to the method handlers. RegisterService(desc *ServiceDesc, impl interface{}) }
  10. 生成されたClientのコード // SearchServiceClient is the client API for SearchService service.

    // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type SearchServiceClient interface { // Returns a list of suggestions for search keyword. ListSuggestions(ctx context.Context, in *ListSuggestionsRequest, opts ...grpc.CallOption) (*ListSuggestionsResponse, error) } type searchServiceClient struct { cc grpc.ClientConnInterface } func NewSearchServiceClient(cc grpc.ClientConnInterface) SearchServiceClient { return &searchServiceClient{cc} } func (c *searchServiceClient) ListSuggestions(ctx context.Context, in *ListSuggestionsRequest, opts ...grpc.CallOption) (*ListSuggestionsResponse, error) { out := new(ListSuggestionsResponse) err := c.cc.Invoke(ctx, "/snkrdunk.search.v1.SearchService/ListSuggestions", in, out, opts...) if err != nil { return nil, err } return out, nil }
  11. gRPC Clientのexample // https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_client/main.go func main() { flag.Parse() // Set

    up a connection to the server. conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewGreeterClient(conn) // Contact the server and print out its response. ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.GetMessage()) }
  12. 生成されたClientのコード // protobuf/gen/go/snkrdunk/search/v1/service_grpc.pb.go func NewSearchServiceClient(cc grpc.ClientConnInterface) SearchServiceClient { return &searchServiceClient{cc}

    } type searchServiceClient struct { cc grpc.ClientConnInterface } // https://github.com/grpc/grpc-go/blob/master/clientconn.go // ClientConnInterface defines the functions clients need to perform unary and // streaming RPCs. It is implemented by *ClientConn, and is only intended to // be referenced by generated code. type ClientConnInterface interface { // Invoke performs a unary RPC and returns after the response is received // into reply. Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...CallOption) error // NewStream begins a streaming RPC. NewStream(ctx context.Context, desc *StreamDesc, method string, opts ...CallOption) (ClientStream, error) }
  13. 生成されたClientのコード // protobuf/gen/go/snkrdunk/search/v1/service_grpc.pb.go func (c *searchServiceClient) ListSuggestions(ctx context.Context, in *ListSuggestionsRequest,

    opts ...grpc.CallOption) (*ListSuggestionsResponse, error) { out := new(ListSuggestionsResponse) err := c.cc.Invoke(ctx, "/snkrdunk.search.v1.SearchService/ListSuggestions", in, out, opts...) if err != nil { return nil, err } return out, nil }
  14. 生成されたClientのコードをそのまま利用するには type Module struct { Service interface{} } func (m

    *Module) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error { split := strings.Split(method, "/") // method -> "/snkrdunk.search.v1.SearchService/ListSuggestions" // split -> []string{"", "snkrdunk.search.v1.SearchService", "ListSuggestions"} inputs := []reflect.Value{ reflect.ValueOf(ctx), reflect.ValueOf(args), } outs := reflect.ValueOf(m.Service).MethodByName(split[2]).Call(inputs) // outs[0]がZero ValueだとpanicになるのでIsZero()でチェック, reflect: call of reflect.Value.Set on zero Value if !outs[0].IsZero() { reflect.ValueOf(reply).Elem().Set(outs[0].Elem()) } err, _ := outs[1].Interface().(error) return err } func (m *Module) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { // Streamは使用しないのでエラーを返す return nil, status.Error(codes.Unknown, "stream not supported") }
  15. モジュラモノリスのための実装 func NewModule() *Module { return &Module{} } type Module

    struct { Service interface{} } func (m *Module) RegisterService(_ *grpc.ServiceDesc, impl interface{}) { m.Service = impl } func (m *Module) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error { split := strings.Split(method, "/") inputs := []reflect.Value{ reflect.ValueOf(ctx), reflect.ValueOf(args), } outs := reflect.ValueOf(m.Service).MethodByName(split[2]).Call(inputs) if !outs[0].IsZero() { reflect.ValueOf(reply).Elem().Set(outs[0].Elem()) } err, _ := outs[1].Interface().(error) return err } func (m *Module) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { return nil, status.Error(codes.Unknown, "stream not supported") }
  16. モジュール(Server)の実装 package search import ( "context" "github.com/org/repository/pkg/grpc" searchv1 "github.com/org/repository/protobuf/gen/go/snkrdunk/search/v1" )

    func NewModule() *grpc.Module { m := grpc.NewModule() searchv1.RegisterSearchServiceServer(m, &SearchService{}) return m } var _ searchv1.SearchServiceServer = (*SearchService)(nil) type SearchService struct { searchv1.UnimplementedSearchServiceServer } func (s *SearchService) ListSuggestions(ctx context.Context, in *searchv1.ListSuggestionsRequest) (*searchv1.ListSuggestionsResponse, error) { // Suggestionの取得処理 out := &searchv1.ListSuggestionsResponse{} return out, nil }
  17. モジュールの呼び出し元(Client)の実装 package main import ( "net/http" "github.com/gin-gonic/gin" searchv1 "github.com/org/repository/protobuf/gen/go/snkrdunk/search/v1" "github.com/org/repository/services/search"

    ) func main() { searchService := search.NewModule() searchClient := searchv1.NewSearchServiceClient(searchService) r := gin.Default() r.GET("/search/suggestions", func(c *gin.Context) { in := &searchv1.ListSuggestionsRequest{ Keyword: c.Query("keyword"), } out, err := searchClient.ListSuggestions(c, in) if err != nil { c.JSON(http.StatusInternalServerError, err) return } c.JSON(http.StatusOK, out) }) r.Run() }
  18. モジュールからgRPCに切り替える (Server) func NewModule() *grpcpkg.Module { m := grpcpkg.NewModule() searchv1.RegisterSearchServiceServer(m,

    &SearchService{}) return m } // 追加 func NewServer() *grpc.Server { s := grpc.NewServer() searchv1.RegisterSearchServiceServer(s, &SearchService{}) return s } var _ searchv1.SearchServiceServer = (*SearchService)(nil) type SearchService struct { searchv1.UnimplementedSearchServiceServer } func (s *SearchService) ListSuggestions(ctx context.Context, in *searchv1.ListSuggestionsRequest) (*searchv1.ListSuggestionsResponse, error) { // Suggestionの取得処理 out := &searchv1.ListSuggestionsResponse{} return out, nil }
  19. モジュールからgRPCに切り替える (Server) package main import ( "log" "net" "github.com/org/repository/services/search" )

    func main() { s := search.NewServer() lis, err := net.Listen("tcp", ":8000") if err != nil { log.Fatal(err) } if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
  20. モジュールからgRPCに切り替える (Client) func main() { // 追加 conn, err :=

    grpc.Dial("localhost:8000", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() searchClient := searchv1.NewSearchServiceClient(conn) r := gin.Default() r.GET("/search/suggestions", func(c *gin.Context) { in := &searchv1.ListSuggestionsRequest{ Keyword: c.Query("keyword"), } out, err := searchClient.ListSuggestions(c, in) if err != nil { c.JSON(http.StatusInternalServerError, err) return } c.JSON(http.StatusOK, out) }) r.Run() }
  21. 別リポジトリからgo getするとエラーになる • 別リポジトリからProtocol Buffersで生成したファイルをimportするためにgo get すると、リポジトリのサイズが大きすぎてエラーになる • create zip:

    module source tree too large (max size is 524288000 bytes) 解決案 • Protocol Buffersを管理するリポジトリを作成する • リポジトリのルートディレクトリのgo.modとは別に、Protocol Buffersで生成した ファイルを置いているディレクトリにgo.modをおく ◦ Protocol Buffersで生成したファイルのディレクトリのみgo getする ◦ リポジトリのルートディレクトリのgo.modではreplaceを使う ◦ 現在はこの方法で対応
  22. まとめ • モノリスからモジュラモノリスに移行中 • モジュールのインターフェースはProtocol Buffersで定義 ◦ 採用して良かった ◦ コードとドキュメント生成される

    ◦ 破壊的変更が防げる • gRPCのinterfaceを満たしながらモジュラモノリスを実装 ◦ 今のところ少ない変更でモジュール -> gRPCの移行もできそう • 実装ルールはLinterを自作して機械的にチェックしていきたい