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

GoのProtocプラグインを活用した効率的な負荷試験戦略 / Efficient Load Testing Strategies Utilizing the Go Protoc Plugin

Shota Iwami
December 01, 2023

GoのProtocプラグインを活用した効率的な負荷試験戦略 / Efficient Load Testing Strategies Utilizing the Go Protoc Plugin

Go Conference mini 2023 in KYOTO で発表しました。
https://kyotogo.connpass.com/event/285351/

Shota Iwami

December 01, 2023
Tweet

More Decks by Shota Iwami

Other Decks in Technology

Transcript

  1. GoのProtocプラグインを


    した効率的な負荷試験戦略
    株式会社サイバーエージェント AI事業本部


    彰太
    GitHub:@BIwashi
    X: @B_Sardine
    Go Conference mini
    202
    3
    Winter in KYOTO

    View full-size slide

  2. 自己
    紹介


    彰太 / Iwamin
    株式会社サイバーエージェント
    ೥౓৽ଔೖࣾ
    "*ࣄۀຊ෦ڠۀϦςʔϧϝσΟΞ%JW
    খചاۀͷΞϓϦ։ൃ
    @BIwashi
    @B_Sardine

    View full-size slide

  3. プロジェクトでの
    Procol Bu
    ff
    ers の活

    状況

    View full-size slide

  4. あらゆるスキーマ定義に proto を採

    • gRPC
    • RBAC
    • grpc-cli
    • validation
    gRPC
    • model struct
    • validation
    • query builder
    • validation
    Model Con
    fi
    g

    View full-size slide

  5. あらゆるスキーマ定義に proto を採

    • スキーマ定義からコードを

    成するための
    Protoc Plugin を

    々書いている
    • できるだけあらゆる情報を proto
    fi
    le に集約す
    るようにしている

    View full-size slide

  6. protoc plugin で
    負荷試験シナリオを

    成する

    View full-size slide

  7. モチベーション
    • 新規開発で全APIの単体負荷試験が必要
    •全てのAPIのシナリオを書いていくのはめんどくさい
    • せっかく gRPC を使

    しているなら proto を使

    したい
    •基本的にシナリオ作成に必要な情報のほとんがすでに proto に記述されている
    •情報を分散させたくない
    •YAML や JSON、Javascript などに負荷試験に関するロジックが存在するようになってしまうとそっち
    のお世話をしないといけない…
    •開発の過程で proto
    fi
    le に変更が

    ることも…
    •負荷試験ツールを疎結合にしたい
    •負荷試験のツール(k
    6
    , locust, ghz etc

    )などを変えられるようにしたい
    •負荷試験ツールに関しての知識が必要な

    を最低限(plugin書く

    )にしたい

    View full-size slide

  8. z
    service SampleService {
    // Coupon rpcs.
    rpc ListCoupons(ListCouponsRequest) returns (ListCouponsResponse) {}
    // Scenario options.
    option (ext.scenario_executor) = {
    scenarios : [
    {
    constant_arrival_rate : {
    rate : 100,
    time_unit : "1s",
    duration : "300s",
    pre_allocated_vus : 75,
    }
    },
    {
    constant_arrival_rate : {
    rate : 200,
    time_unit : "1s",
    duration : "300s",
    pre_allocated_vus : 150,
    }
    }
    ]
    };
    }
    z
    // Code generated by protoc-gen-go-grpc-scenario. DO NOT EDIT.
    import grpc from 'k6/net/grpc';
    import { check } from 'k6';

    export const options …= {…
    scenarios: {
    contacts: {
    executor: 'constant-arrival-rate',
    rate: 100,
    timeUnit: '1s',
    duration: '300s',
    preAllocatedVUs: 75,
    }
    },
    };

    export function listCoupons(token, req) {
    client.connect(baseURL, { plaintext: plaintext });
    const params = {
    metadata: {
    "Authorization": 'Bearer ' + token,
    },
    };
    let response = client.invoke("sampleservice.SampleService/ListCoupons", req, params);
    check(response, {
    'status is OK': (r) => r && r.status === grpc.StatusOK,
    });
    client.close();
    }

    export default function () {
    let req = randomItem(reqs);
    let token = randomItem(users).AccessToken;
    listCoupons(token, req);
    }

    成イメージ
    service.proto
    list_coupons__constant_arrival_rate__rate_
    1 00
    _time_unit_
    1
    s_duration_
    3
    0 0
    s_pre_allocated_vus_
    75
    .pb.js

    View full-size slide

  9. z
    service SampleService {
    // Coupon rpcs.
    rpc ListCoupons(ListCouponsRequest) returns (ListCouponsResponse) {}
    // Scenario options.
    option (ext.scenario_executor) = {
    scenarios : [
    {
    constant_arrival_rate : {
    rate : 100,
    time_unit : "1s",
    duration : "300s",
    pre_allocated_vus : 75,
    }
    },
    {
    constant_arrival_rate : {
    rate : 200,
    time_unit : "1s",
    duration : "300s",
    pre_allocated_vus : 150,
    }
    }
    ]
    };
    }

    成イメージ
    service.proto
    gRPC

    のrpc定義
    シナリオ

    の Options
    • 負荷シナリオなどを記述
    • service に対するextend

    View full-size slide



  10. シナリオとファイルは1:1
    • シナリオのパラメータなどは実

    時にChatOps的に

    れるやり

    もある
    • 結果を参照する際、コマンドも含めて追わないといけない
    • 実

    時に指定したファイルを

    にいけばどのようなシナリオを実

    したかわ
    かるようにしたい
    シナリオのバリエーションだけファイルを

    成する

    View full-size slide

  11. 結果
    • Commit Hash + ファイル名で実際に実

    したファイルの確認を Github にLink


    んで確認できる
    • 実施したシナリオについてのファイル名
    から分かる(詳細は Github

    にいけば
    わかる)

    View full-size slide

  12. protoc-gen-starを使って
    Pluginをサクッと書く

    View full-size slide

  13. protoc-gen-star(PG*)
    • Lyftによって開発されたPDK
    (plugin development kit)
    • protoc plugin を実装する際の処理を簡略化して準
    軟性を持たせられる
    • Protocol Bu
    ff
    ers の AST を

    成して使

    できる
    • UNSTABLEと記載されているが、先

    stable に
    なった protoc-gen-validate でも使

    されている
    • (実はvalidateの

    も元々lyftが開発してた)
    https://github.com/lyft/protoc-gen-star
    https://buf.build/blog/protoc-gen-validate-v
    1
    -and-v
    2
    /

    View full-size slide

  14. package main
    import (
    pgs "github.com/lyft/protoc-gen-star/v2"
    pgsgo "github.com/lyft/protoc-gen-star/v2/lang/go"
    "google.golang.org/protobuf/types/pluginpb"
    )
    func main() {
    optional := uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL)
    pgs.Init(
    pgs.DebugEnv("DEBUG"),
    pgs.SupportedFeatures(&optional),
    ).RegisterModule(
    NewModule(),
    ).RegisterPostProcessor(
    pgsgo.GoFmt(),
    ).Render()
    }
    main

    View full-size slide

  15. package main
    import (
    pgs "github.com/lyft/protoc-gen-star/v2"
    pgsgo "github.com/lyft/protoc-gen-star/v2/lang/go"
    "google.golang.org/protobuf/types/pluginpb"
    )
    func main() {
    optional := uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL)
    pgs.Init(
    pgs.DebugEnv("DEBUG"),
    pgs.SupportedFeatures(&optional),
    ).RegisterModule(
    NewModule(),
    ).RegisterPostProcessor(
    pgsgo.GoFmt(),
    ).Render()
    }
    main
    ੜ੒ͨ͠Goͷίʔυʹformatter͔͚ΒΕΔ
    ʢࠓճ͸JSͷίʔυΛੜ੒ͯ͠ΔͷͰ࢖༻ͯ͠ͳ͍ʣ
    moduleΛొ࿥͢Δ

    View full-size slide

  16. // Module describes the interface for a domain-specific code generation module
    // that can be registered with the PG* generator.
    type Module interface {
    // The Name of the Module, used when establishing the build context and used
    // as the base prefix for all debugger output.
    Name() string
    // InitContext is called on a Module with a pre-configured BuildContext that
    // should be stored and used by the Module.
    InitContext(c BuildContext)
    // Execute is called on the module with the target Files as well as all
    // loaded Packages from the gatherer. The module should return a slice of
    // Artifacts that it would like to be generated.
    Execute(targets map[string]File, packages map[string]Package) []Artifact
    }
    Module

    View full-size slide

  17. // Module describes the interface for a domain-specific code generation module
    // that can be registered with the PG* generator.
    type Module interface {
    // The Name of the Module, used when establishing the build context and used
    // as the base prefix for all debugger output.
    Name() string
    // InitContext is called on a Module with a pre-configured BuildContext that
    // should be stored and used by the Module.
    InitContext(c BuildContext)
    // Execute is called on the module with the target Files as well as all
    // loaded Packages from the gatherer. The module should return a slice of
    // Artifacts that it would like to be generated.
    Execute(targets map[string]File, packages map[string]Package) []Artifact
    }
    Module
    Pluginͱͯ͠ͷੜ੒ϩδοΫ
    ContextͷॳظԽ
    protoc-gen-goͷcontextͱ͔΋ೖΕΒΕΔ
    Pluginͷ໊લ

    View full-size slide

  18. package main
    import (
    pgs "github.com/lyft/protoc-gen-star/v2"
    pgsgo "github.com/lyft/protoc-gen-star/v2/lang/go"
    )
    type Module struct {
    *pgs.ModuleBase
    ctx pgsgo.Context
    }
    func NewModule() *Module {
    return &Module{
    ModuleBase: &pgs.ModuleBase{},
    }
    }
    func (m *Module) InitContext(c pgs.BuildContext) {
    m.ModuleBase.InitContext(c)
    m.ctx = pgsgo.InitContext(c.Parameters())
    }
    func (m *Module) Name() string {
    return "grpc-scenario"
    }
    Module

    View full-size slide

  19. package main
    import (
    pgs "github.com/lyft/protoc-gen-star/v2"
    pgsgo "github.com/lyft/protoc-gen-star/v2/lang/go"
    )
    type Module struct {
    *pgs.ModuleBase
    ctx pgsgo.Context
    }
    func NewModule() *Module {
    return &Module{
    ModuleBase: &pgs.ModuleBase{},
    }
    }
    func (m *Module) InitContext(c pgs.BuildContext) {
    m.ModuleBase.InitContext(c)
    m.ctx = pgsgo.InitContext(c.Parameters())
    }
    func (m *Module) Name() string {
    return "grpc-scenario"
    }
    Module
    protoc-gen-go༻ͷcontext

    View full-size slide

  20. //go:embed scenario.tpl
    var scenarioTemplate string
    var scenarioTpl = template.Must(template.New(“scenario").
    Parse(scenarioTemplate))
    func renderScenario(params TemplateParams) (string, error) {
    buf := bytes.Buffer{}
    if err := scenarioTpl.Execute(&buf, params); err != nil {
    return "", err
    }
    return buf.String(), nil
    }
    type TemplateParams struct {
    Package string
    Service string
    ProtoFilePaths []string
    etc...
    }
    Template

    View full-size slide

  21. //go:embed scenario.tpl
    var scenarioTemplate string
    ɹ
    var scenarioTpl = template.Must(template.New(“scenario").
    Parse(scenarioTemplate))
    ɹ
    func renderScenario(params TemplateParams) (string, error) {
    buf := bytes.Buffer{}
    if err := scenarioTpl.Execute(&buf, params); err != nil {
    return "", err
    }
    ɹ
    return buf.String(), nil
    }
    type TemplateParams struct {
    Package string
    Service string
    ProtoFilePaths []string
    etc...
    }
    Template
    TemplateͷಡΈࠐΈ
    TemplateͰ࢖͍͍ͨύϥϝʔλͷઃఆ

    View full-size slide

  22. func (m *Module) Execute(targets map[string]pgs.File, pkgs map[string]pgs.Package) []pgs.Artifact {
    for _, t := range targets {
    m.genFile(t)
    }
    return m.Artifacts()
    }
    func (m *Module) genFile(f pgs.File) {
    m.Push(f.Name().String())
    defer m.Pop()
    // template ʹ৘ใΛೖΕΔ
    var params TemplateParams
    out, err := renderScenario(params)
    m.CheckErr(err, "unable to render template")
    m.AddGeneratorFile(fmt.Sprintf("%s/scenario.pb.js", params.Package), out)
    }
    Execute

    View full-size slide

  23. func (m *Module) Execute(targets map[string]pgs.File, pkgs map[string]pgs.Package) []pgs.Artifact {
    for _, t := range targets {
    m.genFile(t)
    }
    return m.Artifacts()
    }
    func (m *Module) genFile(f pgs.File) {
    m.Push(f.Name().String())
    defer m.Pop()
    // template ʹ৘ใΛೖΕΔ
    var params TemplateParams
    out, err := renderScenario(params)
    m.CheckErr(err, "unable to render template")
    m.AddGeneratorFile(fmt.Sprintf("%s/scenario.pb.js", params.Package), out)
    }
    Execute
    ParameterͱTemplate͔Β
    ϑΝΠϧग़ྗ಺༰Λ࡞੒
    ੜ੒͢ΔϑΝΠϧͱͯ͠ొ࿥

    View full-size slide

  24. func (m *Module) Execute(targets map[string]pgs.File, pkgs map[string]pgs.Package) []pgs.Artifact {
    for _, t := range targets {
    m.genFile(t)
    }
    return m.Artifacts()
    }
    func (m *Module) genFile(f pgs.File) {
    m.Push(f.Name().String())
    defer m.Pop()
    // template ʹ৘ใΛೖΕΔ
    var params TemplateParams
    out, err := renderScenario(params)
    m.CheckErr(err, "unable to render template")
    m.AddGeneratorFile(fmt.Sprintf("%s/scenario.pb.js", params.Package), out)
    }
    Execute
    ͜Ε

    View full-size slide

  25. z
    // File describes the contents of a single proto file.
    type File interface {
    ParentEntity

    // Services returns the services from this proto file.
    Services() []Service

    }
    type ParentEntity interface {
    Entity
    // Messages returns the top-level messages from this entity. Nested
    // messages are not included.
    Messages() []Message
    // AllMessages returns all the top-level and nested messages from this
    Entity.
    AllMessages() []Message
    // MapEntries returns the MapEntry message types contained within this
    // Entity. These messages are not returned by the Messages or AllMessages
    // methods. Map Entry messages are typically not exposed to the end user.
    MapEntries() []Message
    // Enums returns the top-level enums from this entity. Nested enums
    // are not included.
    Enums() []Enum
    // AllEnums returns all top-level and nested enums from this entity.
    AllEnums() []Enum
    // DefinedExtensions returns all Extensions defined on this entity.
    DefinedExtensions() []Extension

    }
    PG* AST
    • PG* は Protocol Bu
    ff
    ers のASTを


    して Module で使えるようにしている
    • Plugin を書く際はこの Module を利

    すれば簡単に書くことができる
    • 依存関係を解決してくれる

    View full-size slide

  26. func (m *Module) genFile(f pgs.File) {

    protoServides := f.Services()
    methods := protoServides[0].Methods()
    protoMsgs := f.AllMessages()
    fields := protoMsgs[0].Fields()
    field := fields[0]
    filedName := field.Name()
    filedNameString := filedName.String()
    filedNameUppserCamelCase := filedName.UpperCamelCase().String()
    if ftype := field.Type().IsEmbed(); ftype.IsEmbed() {
    for _, f := range ftype.Embed().Fields() {

    }
    }
    ...
    }
    パラメータ取得例

    View full-size slide

  27. AST構築の流れを追う

    View full-size slide

  28. // AST encapsulates the entirety of the input CodeGeneratorRequest from protoc,
    // parsed to build the Entity graph used by PG*.
    type AST interface {
    // Targets returns a map of the files specified in the protoc execution. For
    // all Entities contained in these files, BuildTarget will return true.
    Targets() map[string]File
    // Packages returns all the imported packages (including those for the target
    // Files). This is limited to just the files that were imported by target
    // protos, either directly or transitively.
    Packages() map[string]Package
    // Lookup allows getting an Entity from the graph by its fully-qualified name
    // (FQN). The FQN uses dot notation of the form ".{package}.{entity}", or the
    // input path for Files.
    Lookup(name string) (Entity, bool)
    }
    AST

    View full-size slide

  29. func (wf *standardWorkflow) Init(g *Generator) AST {
    wf.Generator = g
    wf.Debug("reading input")
    data, err := ioutil.ReadAll(g.in)
    wf.CheckErr(err, "reading input")
    wf.Debug("parsing input proto")
    req := new(plugin_go.CodeGeneratorRequest)
    err = proto.Unmarshal(data, req)
    wf.CheckErr(err, "parsing input proto")
    wf.Assert(len(req.FileToGenerate) > 0, "no files to generate")
    wf.Debug("parsing command-line params")
    wf.params = ParseParameters(req.GetParameter())
    for _, pm := range wf.paramMutators {
    pm(wf.params)
    }
    //…
    return ProcessCodeGeneratorRequest(g, req)
    }
    Init
    type Generator struct {
    Debugger
    persister persister
    workflow workflow
    mods []Module
    in io.Reader // protoc input reader
    out io.Writer // protoc output writer
    debug bool
    params Parameters
    }

    View full-size slide

  30. func ProcessCodeGeneratorRequest(debug Debugger, req *plugin_go.CodeGeneratorRequest) AST {
    g := &graph{
    d: debug,
    targets: make(map[string]File, len(req.GetFileToGenerate())),
    packages: make(map[string]Package),
    entities: make(map[string]Entity),
    extensions: []Extension{},
    }
    for _, f := range req.GetFileToGenerate() {
    g.targets[f] = nil
    }
    for _, f := range req.GetProtoFile() {
    pkg := g.hydratePackage(f)
    pkg.addFile(g.hydrateFile(pkg, f))
    }
    for _, e := range g.extensions {
    e.addType(g.hydrateFieldType(e))
    extendee := g.mustSeen(e.Descriptor().GetExtendee()).(Message)
    e.setExtendee(extendee)
    if extendee != nil {
    extendee.addExtension(e)
    }
    }
    return g
    }
    Generate AST

    View full-size slide

  31. func ProcessCodeGeneratorRequest(debug Debugger, req *plugin_go.CodeGeneratorRequest) AST {
    g := &graph{
    d: debug,
    targets: make(map[string]File, len(req.GetFileToGenerate())),
    packages: make(map[string]Package),
    entities: make(map[string]Entity),
    extensions: []Extension{},
    }
    for _, f := range req.GetFileToGenerate() {
    g.targets[f] = nil
    }
    for _, f := range req.GetProtoFile() {
    pkg := g.hydratePackage(f)
    pkg.addFile(g.hydrateFile(pkg, f))
    }
    for _, e := range g.extensions {
    e.addType(g.hydrateFieldType(e))
    extendee := g.mustSeen(e.Descriptor().GetExtendee()).(Message)
    e.setExtendee(extendee)
    if extendee != nil {
    extendee.addExtension(e)
    }
    }
    return g
    }
    Generate AST

    View full-size slide

  32. func ProcessCodeGeneratorRequest(debug Debugger, req *plugin_go.CodeGeneratorRequest) AST {
    g := &graph{
    d: debug,
    targets: make(map[string]File, len(req.GetFileToGenerate())),
    packages: make(map[string]Package),
    entities: make(map[string]Entity),
    extensions: []Extension{},
    }
    for _, f := range req.GetFileToGenerate() {
    g.targets[f] = nil
    }
    for _, f := range req.GetProtoFile() {
    pkg := g.hydratePackage(f)
    pkg.addFile(g.hydrateFile(pkg, f))
    }
    for _, e := range g.extensions {
    e.addType(g.hydrateFieldType(e))
    extendee := g.mustSeen(e.Descriptor().GetExtendee()).(Message)
    e.setExtendee(extendee)
    if extendee != nil {
    extendee.addExtension(e)
    }
    }
    return g
    }
    Generate AST
    func (g *graph) hydratePackage(f *descriptor.FileDescriptorProto) Package {
    lookup := f.GetPackage()
    if pkg, exists := g.packages[lookup]; exists {
    return pkg
    }
    p := &pkg{fd: f}
    g.packages[lookup] = p
    return p
    }

    View full-size slide

  33. func ProcessCodeGeneratorRequest(debug Debugger, req *plugin_go.CodeGeneratorRequest) AST {
    g := &graph{
    d: debug,
    targets: make(map[string]File, len(req.GetFileToGenerate())),
    packages: make(map[string]Package),
    entities: make(map[string]Entity),
    extensions: []Extension{},
    }
    for _, f := range req.GetFileToGenerate() {
    g.targets[f] = nil
    }
    for _, f := range req.GetProtoFile() {
    pkg := g.hydratePackage(f)
    pkg.addFile(g.hydrateFile(pkg, f))
    }
    for _, e := range g.extensions {
    e.addType(g.hydrateFieldType(e))
    extendee := g.mustSeen(e.Descriptor().GetExtendee()).(Message)
    e.setExtendee(extendee)
    if extendee != nil {
    extendee.addExtension(e)
    }
    }
    return g
    }
    Generate AST func (g *graph) hydrateFile(pkg Package, f *descriptor.FileDescriptorProto) File {
    fl := &file{
    pkg: pkg,
    desc: f,
    }
    if pkg := f.GetPackage(); pkg != "" {
    fl.fqn = "." + pkg
    } else {
    fl.fqn = ""
    }
    g.add(fl)
    for _, dep := range f.GetDependency() {
    // the AST is built in topological order so a file's dependencies are always hydrated first
    d := g.mustSeen(dep).(File)
    fl.addFileDependency(d)
    d.addDependent(fl)
    }
    if _, fl.buildTarget = g.targets[f.GetName()]; fl.buildTarget {
    g.targets[f.GetName()] = fl
    }
    enums := f.GetEnumType()
    fl.enums = make([]Enum, 0, len(enums))
    for _, e := range enums {
    fl.addEnum(g.hydrateEnum(fl, e))
    }
    exts := f.GetExtension()
    fl.defExts = make([]Extension, 0, len(exts))
    for _, ext := range exts {
    e := g.hydrateExtension(fl, ext)
    fl.addDefExtension(e)
    }
    msgs := f.GetMessageType()
    fl.msgs = make([]Message, 0, len(f.GetMessageType()))
    for _, msg := range msgs {
    fl.addMessage(g.hydrateMessage(fl, msg))
    }
    srvs := f.GetService()
    fl.srvs = make([]Service, 0, len(srvs))
    for _, sd := range srvs {
    fl.addService(g.hydrateService(fl, sd))
    }
    for _, m := range fl.AllMessages() {
    for _, me := range m.MapEntries() {
    for _, fld := range me.Fields() {
    fld.addType(g.hydrateFieldType(fld))
    }
    }
    for _, fld := range m.Fields() {
    fld.addType(g.hydrateFieldType(fld))
    }
    }
    g.hydrateSourceCodeInfo(fl, f)
    return fl
    }
    気合い

    View full-size slide

  34. func ProcessCodeGeneratorRequest(debug Debugger, req *plugin_go.CodeGeneratorRequest) AST {
    g := &graph{
    d: debug,
    targets: make(map[string]File, len(req.GetFileToGenerate())),
    packages: make(map[string]Package),
    entities: make(map[string]Entity),
    extensions: []Extension{},
    }
    for _, f := range req.GetFileToGenerate() {
    g.targets[f] = nil
    }
    for _, f := range req.GetProtoFile() {
    pkg := g.hydratePackage(f)
    pkg.addFile(g.hydrateFile(pkg, f))
    }
    for _, e := range g.extensions {
    e.addType(g.hydrateFieldType(e))
    extendee := g.mustSeen(e.Descriptor().GetExtendee()).(Message)
    e.setExtendee(extendee)
    if extendee != nil {
    extendee.addExtension(e)
    }
    }
    return g
    }
    Generate AST
    type Package interface {
    Node
    // The name of the proto package.
    ProtoName() Name
    // All the files loaded for this Package
    Files() []File
    addFile(f File)
    setComments(c string)
    }
    func (p *pkg) addFile(f File) {
    f.setPackage(p)
    p.files = append(p.files, f)
    }

    View full-size slide

  35. まとめ
    • Protoc plugin はサクッと実装できる
    • Proto
    fi
    le に全ての情報を集約すると Plugin で柔軟に拡張できる
    • 負荷試験シナリオも宣

    的(?)に管理されている

    が確認しやすい
    • protoc-gen-star はASTの

    成をやってくれて使いやすい

    View full-size slide

  36. ありがとうございました!

    View full-size slide