Slide 1

Slide 1 text

ServeMuxの競合検知と性能 Hiroki Takeda 2024/03/18 Go 1.22 Release Party

Slide 2

Slide 2 text

2 / 17 whoami 武田 大輝(Takeda Hiroki) Future Architect, Inc. Technology Innovation Group Software Architect & Tech Lead 📝 Future Tech Blog 見てね

Slide 3

Slide 3 text

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)

Slide 4

Slide 4 text

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を登録済みのパターンと照らし合わせて,対象のハンドラを呼び出す 競合検知 ハンドラを登録するに際に,登録済みのパターンとの競合をチェックする

Slide 5

Slide 5 text

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 競合とはそもそも何か? なぜ競合検知が性能面での論点になるのか? 具体的にどう対応しているか?

Slide 6

Slide 6 text

5 / 17 そもそも競合とは?(1/2) [問] どちらのハンドラが呼ばれるでしょうか curl /posts/latest -> http.Handle("GET /posts/{id}", handlerA) http.Handle("GET /{resource}/latest", handlerB)

Slide 7

Slide 7 text

5 / 17 そもそも競合とは?(1/2) [問] どちらのハンドラが呼ばれるでしょうか curl /posts/latest -> 判別できない!!! http.Handle("GET /posts/{id}", handlerA) http.Handle("GET /{resource}/latest", handlerB)

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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 では,どういうパターンの組み合わせが「競合」となるのか? >>>

Slide 10

Slide 10 text

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} 競合する

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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.

Slide 14

Slide 14 text

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}

Slide 15

Slide 15 text

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 // セグメントの名称(ワイルドカードの場合は空) }

Slide 16

Slide 16 text

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: }

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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 }

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

16 / 17 まとめと所感 ServeMuxはインデックスを利用して競合検知の効率化を図っている アルゴリズムとして最速は目指していない(恐らく),性能と同じくらいシンプルさも大切にしている ソースを読み、理解することでServeMuxと仲良くなれる = アルゴリズムフレンドリなパターン/アンフレンドリなパターンを意識することができる

Slide 21

Slide 21 text

17 / 17 Thank You Documentations · GitHub