$30 off During Our Annual Pro Sale. View Details »

ServeMuxの競合検知と性能

Hiroki Takeda
March 18, 2024
31

 ServeMuxの競合検知と性能

Hiroki Takeda

March 18, 2024
Tweet

Transcript

  1. 2 / 17 whoami 武田 大輝(Takeda Hiroki) Future Architect, Inc.

    Technology Innovation Group Software Architect & Tech Lead 📝 Future Tech Blog 見てね
  2. 3 / 17 ServeMuxの機能拡張 HTTPメソッドの指定が可能に http.Handle("GET /posts", handler) ワイルドカードの指定が可能に http.Handle("GET

    /posts/{id}", handler) マルチワイルドカードも http.Handle("GET /file/{pathname...}", handler) 末尾スラッシュのパスマッチが可能に http.Handle("GET /posts/{$}", handler)
  3. 4 / 17 ServeMuxの拡張に伴う2つの性能懸念 There are two situations where questions

    of performance arise: matching requests, and detecting conflicts during registration. cf. https://github.com/golang/go/issues/61410#issue-1809745160 リクエストのマッチング リクエストされたURLを登録済みのパターンと照らし合わせて,対象のハンドラを呼び出す 競合検知 ハンドラを登録するに際に,登録済みのパターンとの競合をチェックする
  4. 4 / 17 ServeMuxの拡張に伴う2つの性能懸念 There are two situations where questions

    of performance arise: matching requests, and detecting conflicts during registration. cf. https://github.com/golang/go/issues/61410#issue-1809745160 リクエストのマッチング リクエストされたURLを登録済みのパターンと照らし合わせて,対象のハンドラを呼び出す 競合検知 ハンドラを登録するに際に,登録済みのパターンとの競合をチェックする 👆Today's Topic 競合とはそもそも何か? なぜ競合検知が性能面での論点になるのか? 具体的にどう対応しているか?
  5. 6 / 17 そもそも競合とは?(2/2) 競合とは,呼び出すハンドラが一意に特定できない可能性がある状態 競合する場合は panic が発生 panic: pattern

    "GET /{resource}/latest" (registered at ...) conflicts with pattern "GET /posts/{id}" (registered at ...): GET /{resource}/latest and GET /posts/{id} both match some paths, like "/posts/latest". But neither is more specific than the other. GET /{resource}/latest matches "/resource/latest", but GET /posts/{id} doesn't. GET /posts/{id} matches "/posts/id", but GET /{resource}/latest doesn't. $ go run main.go
  6. 6 / 17 そもそも競合とは?(2/2) 競合とは,呼び出すハンドラが一意に特定できない可能性がある状態 競合する場合は panic が発生 panic: pattern

    "GET /{resource}/latest" (registered at ...) conflicts with pattern "GET /posts/{id}" (registered at ...): GET /{resource}/latest and GET /posts/{id} both match some paths, like "/posts/latest". But neither is more specific than the other. GET /{resource}/latest matches "/resource/latest", but GET /posts/{id} doesn't. GET /posts/{id} matches "/posts/id", but GET /{resource}/latest doesn't. $ go run main.go では,どういうパターンの組み合わせが「競合」となるのか? >>>
  7. 7 / 17 優先順位と競合 競合するのは「Overlapping」と「Equivalent」の関係性 必ずしも重複するパターンが競合するとは限らない(Most Specific Wins) パターンの関係性は5つに分類できる Disjoint

    P1とP2の両方にマッチすることはない(互いに素) 例. P1: /posts P2: /users 競合しない P1 More Specific P1にマッチするものはP2にもマッチするが,その逆は必ずしもそうではない -> 両方にマッチする場合はP1を優先 例. P1: /posts/latest P2: /posts/{id} 競合しない P1 More General P2にマッチするものはP2にもマッチするが,その逆は必ずしもそうではない -> 両方にマッチする場合はP2を優先 例. P1: /posts/{id...} P2: /posts/{id}/users/{uid} 競合しない Overlapping P1とP2両方にマッチするものもあれば片方にしかマッチしないものもある 例. P1: /posts/{id} P2: /{resource}/latest  ・ /post/latest の場合は両方にマッチ  ・ /post/1234 場合はP1のみ, /users/latest の場合はP2のみにマッチ 競合する Equivalent 必ずP1とP2の両方にマッチする 例. P1: /posts/{id} P2: /posts/{name} 競合する
  8. 8 / 17 なぜ競合検知が性能の論点になるのか Registration time is potentially more of

    an issue. With the precedence rules described here, checking a new pattern for conflicts seems to require looking at all existing patterns in the worst case. (Algorithm lovers, you are hereby nerd- sniped.) That means registering n patterns takes O(n2) time in the worst case. cf. https://github.com/golang/go/discussions/60227#discussioncomment-6204048 単純に考えるとパターン登録のたびに,登録済みの全パターンと競合をチェックする必要がある = 最悪の場合,計算量は となる Discovery Documentをもとに収集した約5000パターンのGoogle APIに対して,単純に競合チェックを実施 すると約1秒程度かかった(らしい) O(N ) 2
  9. 8 / 17 なぜ競合検知が性能の論点になるのか Registration time is potentially more of

    an issue. With the precedence rules described here, checking a new pattern for conflicts seems to require looking at all existing patterns in the worst case. (Algorithm lovers, you are hereby nerd- sniped.) That means registering n patterns takes O(n2) time in the worst case. cf. https://github.com/golang/go/discussions/60227#discussioncomment-6204048 単純に考えるとパターン登録のたびに,登録済みの全パターンと競合をチェックする必要がある = 最悪の場合,計算量は となる Discovery Documentをもとに収集した約5000パターンのGoogle APIに対して,単純に競合チェックを実施 すると約1秒程度かかった(らしい) ServeMuxでは,パターンの登録時にインデックスを作成しておき,インデックスから効率よく競合し得るパタ ーンを取得し,チェックすることで性能向上を図っている O(N ) 2
  10. 9 / 17 競合検知のフロー(ざっくり) http.Handle の中身を追ってみると… cf. https://github.com/golang/go/blob/go1.22.1/src/net/http/server.go#L2737 2737 func

    (mux *ServeMux) registerErr(patstr string, handle 2738 if patstr == "" { 2739 return errors.New("http: invalid pattern") 2740 } 2741 if handler == nil { 2742 return errors.New("http: nil handler") 2743 } 2744 if f, ok := handler.(HandlerFunc); ok && f == nil { 2745 return errors.New("http: nil handler") 2746 } 2747 2748 pat, err := parsePattern(patstr) 2749 if err != nil { 2750 return fmt.Errorf("parsing %q: %w", patstr, err) 2751 } 2752 2753 // Get the caller's location, for better conflict er 2754 // Skip register and whatever calls it.
  11. 10 / 17 パターン文字列の構造化 例. GET /posts/{id} を登録する場合 p :=

    &pattern{ str: "GET /posts/{id}", // ① method: "GET", // ② host: "", // ③ segments: []segment{ // ④ { s: "posts", // ④-1 wild: false, // ④-2 multi: false, // ④-3 }, { s: "id", wild: true, multi: false, }, }, ... } ① str - パターン文字列 ② method - メソッド名 ③ host - ホスト名 ④ segments - パスセグメント ④-1 s - セグメント名 ④-2 wild - ワイルドカードかどうか ④-3 multi - マルチワイルドカードかどうか ちなみに… 末尾三点リーダー -> &segment{s: "xxx", wild: true, multi: true} 末尾 スラッシュ -> &segment{"s": "", "wild": true, "multi": true} 末尾ドル -> &segment{"s": "/", "wild": false, "multi": false}
  12. 11 / 17 インデックスの構造 構造化したパターンがインデックスに登録される 例. 次のパターン文字列が登録されている場合 P1: /posts/{id} P2:

    /posts/latest P3: /posts/ P4: /posts/{$} インデックスの構造体 cf. https://github.com/golang/go/blob/go1.22.1/src/net/http/routing_index.go#L14 type routingIndex struct { // セグメントの名称と位置をキーとするパターン群のマップ segments map[routingIndexKey][]*pattern // マルチワイルドカードを持つパターン群 multis []*pattern } type routingIndexKey struct { pos int // セグメントの位置(0始まり) s string // セグメントの名称(ワイルドカードの場合は空) }
  13. 12 / 17 競合検知(1/4) 1.マルチワイルドカードを持つ全てのパターンと比較する 例. 登録対象のパターンが何であれ -> multis に登録されている全パターン(P3)と競合チェックを

    行う cf. https://github.com/golang/go/blob/go1.22.1/src/net/http/routing_index.go#L57 // Our simple indexing scheme doesn't try to prune multi // any of them can match the argument. if err := apply(idx.multis); err != nil { return err } if pat.lastSegment().s == "/" { // All paths that a dollar pattern matches end in a sl // an ordinary pattern matches do. So only other dolla // patterns can conflict with a dollar pattern. Furthe // dollar patterns must have the {$} in the same posit return apply(idx.segments[routingIndexKey{s: "/", pos: }
  14. 13 / 17 競合検知(2/4) 2.末尾ドルの場合は,同じ位置に末尾ドルを持つパターンと比較する 例. GET /users/{$} を登録する場合 ->

    1番目にドル( / )を持つパターン(P4)と競合チェックを行う cf. https://github.com/golang/go/blob/go1.22.1/src/net/http/routing_index.go#L57 if pat.lastSegment().s == "/" { // All paths that a dollar pattern matches end in a sl // an ordinary pattern matches do. So only other dolla // patterns can conflict with a dollar pattern. Furthe // dollar patterns must have the {$} in the same posit return apply(idx.segments[routingIndexKey{s: "/", pos: } // For ordinary and multi patterns, the only conflicts c // or a pattern that has the same literal or a wildcard // position. // We could intersect all the possible matches at each p / d hi i f d h i i i h h f
  15. 14 / 17 競合検知(3/4) 3.登録対象のリテラルセグメントと同じ位置に「同名のセグメント名もしくはワイルドカード」を持つ パターンをインデックスから取得し,最も数が少ないパターン群と比較する 例. GET /posts/latest/{categories} を登録する場合

    -> 2個のパターン(P1, P2)と競合チェックを行う 0番目が posts or "" のパターン  -> 3個(P1, P2, P4) 1番目が latest or "" のパターン -> 2個(P1, P2) cf. https://github.com/golang/go/blob/go1.22.1/src/net/http/routing_index.go#L57 // For ordinary and multi patterns, the only conflicts c // or a pattern that has the same literal or a wildcard // position. // We could intersect all the possible matches at each p // do something simpler: we find the position with the f var lmin, wmin []*pattern min := math.MaxInt hasLit := false for i, seg := range pat.segments { if seg.multi { break }
  16. 15 / 17 競合検知(4/4) 4.ここまで競合チェックが行われていない場合は,全てのセグメントパターンと比較する (ワイルドカードのみで構成される場合が該当) 例. GET /${resource}/${id} を登録する場合

    -> segments に登録されている全パターンと競合チェックを行う cf. https://github.com/golang/go/blob/go1.22.1/src/net/http/routing_index.go#L57 // This pattern is all wildcards. // Check it against everything. for _, pats := range idx.segments { apply(pats) } return err