From c20b990a3a1925bbef92da438cdd6ea7baf5852d Mon Sep 17 00:00:00 2001 From: rapthead Date: Mon, 24 Jun 2024 10:06:02 +0300 Subject: [PATCH] considering settings & benchmarks docker-compose --- .dockerignore | 1 + README.md | 25 ++- assets/benchmark_chart.png | Bin 36433 -> 101604 bytes benchmarks/dumb-server/Dockerfile | 13 ++ benchmarks/dumb-server/docker-compose.yaml | 9 + benchmarks/dumb-server/main.go | 100 +++++++---- benchmarks/framer/README.md | 6 + benchmarks/framer/REAME.md | 4 - benchmarks/framer/docker-compose.yaml | 22 +++ benchmarks/gatling/.dockerignore | 1 + benchmarks/gatling/Dockerfile | 5 + benchmarks/gatling/README.md | 8 +- benchmarks/gatling/docker-compose.yaml | 19 ++ benchmarks/ghz/Dockerfile | 5 + benchmarks/ghz/README.md | 6 + benchmarks/ghz/REAME.md | 4 - benchmarks/ghz/docker-compose.yaml | 25 +++ benchmarks/h2load/Dockerfile | 3 + benchmarks/h2load/README.md | 6 + benchmarks/h2load/REAME.md | 4 - benchmarks/h2load/docker-compose.yaml | 24 +++ benchmarks/k6/README.md | 6 + benchmarks/k6/REAME.md | 4 - benchmarks/k6/docker-compose.yaml | 19 ++ benchmarks/pandora/Dockerfile | 5 + benchmarks/pandora/README.md | 6 + benchmarks/pandora/REAME.md | 4 - benchmarks/pandora/config.yaml | 5 +- benchmarks/pandora/docker-compose.yaml | 20 +++ cmd/framer/cmd_load.go | 15 +- cmd/framer/main.go | 31 +++- cmd/framer/main_benchmark_test.go | 13 +- consts/consts.go | 4 + datasource/file_benchmark_test.go | 7 +- datasource/inmem_benchmark_test.go | 3 +- datasource/request.go | 59 ++++--- datasource/request_test.go | 3 +- loader/e2e_test.go | 6 +- loader/loader.go | 195 +++++++++++---------- loader/reciever/processor.go | 47 +++-- loader/sender/sender.go | 12 +- loader/streams/pool/pool.go | 52 +++--- loader/streams/pool/pool_benchmark_test.go | 2 +- loader/streams/store/store.go | 5 +- loader/types/req.go | 2 +- report/phout/phout.go | 4 +- utils/hpack_wrapper/wrapper.go | 15 +- 47 files changed, 566 insertions(+), 268 deletions(-) create mode 100644 benchmarks/dumb-server/Dockerfile create mode 100644 benchmarks/dumb-server/docker-compose.yaml create mode 100644 benchmarks/framer/README.md delete mode 100644 benchmarks/framer/REAME.md create mode 100644 benchmarks/framer/docker-compose.yaml create mode 100644 benchmarks/gatling/.dockerignore create mode 100644 benchmarks/gatling/Dockerfile create mode 100644 benchmarks/gatling/docker-compose.yaml create mode 100644 benchmarks/ghz/Dockerfile create mode 100644 benchmarks/ghz/README.md delete mode 100644 benchmarks/ghz/REAME.md create mode 100644 benchmarks/ghz/docker-compose.yaml create mode 100644 benchmarks/h2load/Dockerfile create mode 100644 benchmarks/h2load/README.md delete mode 100644 benchmarks/h2load/REAME.md create mode 100644 benchmarks/h2load/docker-compose.yaml create mode 100644 benchmarks/k6/README.md delete mode 100644 benchmarks/k6/REAME.md create mode 100644 benchmarks/k6/docker-compose.yaml create mode 100644 benchmarks/pandora/Dockerfile create mode 100644 benchmarks/pandora/README.md delete mode 100644 benchmarks/pandora/REAME.md create mode 100644 benchmarks/pandora/docker-compose.yaml diff --git a/.dockerignore b/.dockerignore index 55946b4..3fea7ce 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ .git LICENSE +docker-compose.yaml *.md assets Dockerfile diff --git a/README.md b/README.md index 555cade..f1fe625 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ -# framer -framer is the most performant grpc load generator +# ozon-framer +ozon-framer is the most performant grpc load generator ## Performance ![benchmark chart](./assets/benchmark_chart.png) +Benchmarks are done with `11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz`. Load generators was limited in 2 CPU. Load generators configurations are available in [benchmarks directory](./benchmarks) @@ -17,7 +18,7 @@ Load generators configurations are available in [benchmarks directory](./benchma This is alpha version. Public api and request file format may be changed. ## Install -Download binary from [github release page](./releases/latest) and place it in your PATH. +Download binary from [github release page](https://github.com/ozontech/framer/releases/latest) and place it in your PATH. ### Compile **Build using go** @@ -77,6 +78,13 @@ framer load --addr=localhost:9090 --requests-file=test_files/requests --clients It makes 10 rps from 10 clients in 10 second. ## Converter +`framer convert` command may be used to convert requests file between different formats. +Now is supported next formats: +* ozon.binary - [see format description above](#ozon.binary-file-format); +* pandora.json - grpc json format of pandora load generator. [See documentation](https://yandex.cloud/ru/docs/load-testing/concepts/payloads/grpc-json); +* ozon.json - same as pandora.json, but has ability to store repeatable meta value. + +### Supported formats ### Ozon.binary file format Rules are using [ABNF syntax](https://tools.ietf.org/html/rfc5234). @@ -94,6 +102,9 @@ Body = 1*({any byte}) [Example requests file](https://github.com/ozontech/framer/-/blob/master/test_files/requests) +#### Programatic ozon.binary generation example +[Full example](./examples/requestsgen) + ### Usage ``` Usage: framer convert --help @@ -123,12 +134,10 @@ framer convert --from=ozon.json --to=ozon.binary --reflection-proto=formats/grpc ``` It converts requests file from ozon.json format to ozon.binary format using protofile. -### Programatic requests generation example -[Full example](./examples/requestsgen) - ## TODO -- [ ] Installation from homebrew for macOS; -- [ ] Publish to dockerhub; +- [ ] Installation + - [ ] Homebrew suport for macOS; + - [ ] Publish to dockerhub; - [ ] Configuration file support; - [ ] Requests scheduling strategys combination; - [ ] More reporting variants; diff --git a/assets/benchmark_chart.png b/assets/benchmark_chart.png index 58b67a10fda9fed277cce47e3ec780a611216074..b0886d41be26f7f90bd43df8371218106ed5737a 100644 GIT binary patch literal 101604 zcmeFZ2UJzr(k_a*6%$An1PO}doPz-g0um&{24SUAxabw5F`i5AX&1BB$AV4 z1PPm*b9l8-yU!W#-topi#=GynasNYikA}V1nrqIQHLJd=`f9yUyeoC)6vZh#JiIe9 z(h|yec*kGj;T;wE>o|N8YB1md|Lb>E(ZDL}JJHzKSsR&JU}&%|HW(U=v#Ajto^!9I zk~ZOIn!k3|i9`=SrVt2_nUS+@86|yMAoKQ(?O3QPhyVI?#HHi(caz!rlXw-0u02Mb zIW83&6)zWa{vk=LtVk;#3a{fl?rzL|c5nADqu%IdRF*5@2rJ<)5O4@cQBdS=yyqYUxD5Q$xb!T|^`@8mJwk-Dv2qhR9(MN87f8LTFVsKezJC|hd zayNUB?B(v`WP;;LkB|4PKH2{`Zm<$XAj-=SIdfMw3ZM;#w&RohkuQl&T^-iUp~ zT=9cl^$|{?jrk0#`_UDPBc$DD^=IX(P%^%fsSeW{(uPH=eFukzZFm4U;k3|;G0 zk0>#`Yg0_NM<^)|C%SyqHD8=-8Xw#fy*EF9q8($`SoGAv9~9Er6ckTGUQP&YZONu@ zXl;ODbGEbrjl{zf5qGxHN1J1?GzJ)BQ!7#0`LZfn8dF12T6JD|4tbkf7!y-zS3Ar- z*SjicS97$WA+5OBDG_HO7{C&P)u(Z`w6L-lau%gM7*_~>M?Pk!r8&3+Yc5KwA+Jbt z%i0b@!^6hI#=&~q+0>DXR_qjwh@GL4kg|m2?;+qXQCbr$)<%e(-O0&`&54`M+Rm7r zQ&3Qlor8;=i;ESmV6}I#!smByk+m0~E?+N`6@32>a^UJP`vA1@x zLt}0`Vyv)q|2l*r`tSQ~9PBI(mSc!!$5>!2;Zl2eD(AmFQc6Z%@$Wkj5g40V+8o>k z!v41_v8G1<2iT>mnZjFmlB-wKUE zhJwM_Oko@h7lxZlpM!(d&Xd}!N$#V@C~vpLa-PRtUjWrFu=j*uoj_P zb{KuEwVjH!wS_1xG9?=1&cDBwhvQ_ZkJXpZ$6{bm4lW)c4n83+9u*D_Ax=RdUS4Jn zZXu3;x!&5))X3%k>1yQg(1`rna%oe0c)rWQN54+gJ&f(IzkdB`VR~?sXlM=&g^)h_ z*AndY9WjOn^MtW}eT6pBw=%|n_4uu>e?B+;AE*NC9gh(QXn_Eyz7Z=ACl4p9ATR7B zr#_m?(1?eho73QzSpOc~-r5N3q;H40VGMEvS%G>U$cl#XV56@6`&^t%Fvu=|z*sr> zSpN~2z;A)E|6VXVVl)1%ScLumCKHi^3;xB(zb?TpR3S$K*$O(d_ zr)uIqAkaE)AS)$-w~zc2SCJ9|pPaOj*0jgNjboO@>mTx?Kd(R16zS)#IeMw){ZA`SVu9|ewcA2*=5AWcJ075PC% zNakv_OuE(BV82kxmSb$+gxY{x##aBGke5HPckl9>rd$xZPIgP0=4t%L(?#e78to!X zLmopzWYJsE`NF~N+M=+Ea*Y0k2&>`kG`kZ z8234%P^jonKR6nW{<&^}fKx~)-H=PuShLIUN09m>dKsT;1Nb+S=+7k|q#klgrm3X= zEN^W4^BLZ-+)?Sj|5&(u{qM;>Ja>rj?=Rf{f|ShfpW#~%|9&bS^T)>^Tb$b_RaX07JXr(0$&ZexM~=QPF*8>5w)x<^L5Pb z>%=mALUx*o@I_r-rs)J0rM~H|>vi*^8yn?WH#8#p4=1P&#QyMHrredpiC9lv4-XHm zVykEpHl0*;JrC9Gwb^@OvFi8kg;>cdwUNA0}|8InOOBIb*8(r?y0J(*1Ok`ojG^Tyf%PpYHCWEHfSM6yTBxft!l@v zv%7oQ3)#ViM^Xn{Jba|VmNHtv_GU#zMS5CVYc{H^qqB1iCGN76)P^gdPg7)^{PN<$ z)ckyR5S>(kc{khaP?cWxwCno0)2B`D-@gu8S~?mata9((=$o9J9ECuUrRUGQy(_-b zq5ZbDw|O}@;%>&rHNN3zKL6K3^{+=u{(AH+{h>+^J4&fm6O|0LEM);oHJEDQyEg?M z+m0pK8B0rcC|6vvnW<@OOUuha_hos1fB&?yGHqeUnU*vB{lkUH4HL_~r4AYJJmMD; z*a3&`w5%-oFicuXij=U^oC-%>js~)AVw4-RNT%_F_v29kCT8aL6!Bft!otG(BFnMm z&1<))x*d4k)+`2Gr?bo~Emzk>(lavJLlnf)utHgDYmTS~n`7zO*oSM10ce0Z*Q zUV{aZ+rx8D-oCw(rBl*|ANZErxQUXNk1s6h-0>%o{MIr2HQ&F#CZ+JYdATuyTPhg4 zGN~lG{Ed5Rc6N0({@^TAlN7u=0>doCpMN3II!9g!Ssj#!%>bA#n#F~(MFFD(Yp^59N0ZD=)mSz2BtS?-meX}Xba4K|IVIshr03)q+y46;O<^xGHS;@KTjO`P*SE7lDLi&Y zsk_^fWj$u6LAQy0t9khN-p=+FyQxZttn={jOul_%rL3t*j(-}N*^$5l%l_en8fe5+ zX9jU#ZD&VE(*1><2i0k*sV!HiZ)1*kw{tOQnwE=a|c{>oo3kWZApoy z-p&ZA!@}?lDiC&JV&bWzhtspNRAps-!&tT7!qKi|M1&zm^m|k8dwteN*;W&C)zsy& z=vB9Dij>)n-C@-(k_#AxPmVqLXdj@j8Ad@t(b3WZ7G+qKrl$x+$cL=_AA?$ee$wZceLspLO%7@&0@bllv zLHTLH}n2G?453h&Vhj}30ltC z4Y1#D-%2izH@;cg`egx(P zirgSt6-H84=SB(kiyz5k23SwZ1Pk9Bqffwk?MrF9o zl}MdAbEeX~-cVV5=f^pRosC7OX!F&n&aJNE$}xO~z>Kz7xOSHALu=5q`X4O8#4E7VtE)_dV4X!(jwJE1tU}kI;E6M{N>jROZZhS+z zf0iv#F`++Gn~nvR4*;Me^eNo6Vt(MjqXiwXNU3e|KpNy_+I? zIALziYQS@UcWP#arQ{0nt(d_<-QD36;)=$`#`T2=WZ7Rhw;vZDA0NCR&2(H2d~Zjf z=gw&G`pnEs$L{)2PNGy`*i)%C+*CUC-h#J~>xxNzf7Sl(d?BVG%wA1z3?{zXrM*?_ zxg#YnC$~U_|7WgjrQ|1Haz*%&o(c^QH}A>Alx|IB6dKkC2ZuTI6XcvG)M zn}e-f?bgdlmJR<}q`=;~x4VtlT6V*)M)_Mk%@5u#F51q=L`G7eJXSkPSK4HW&w5uZ zzmT}8AhwyrJ}mTL=lvzO8BS}fhCCkYK@pVa?y6R~bFTFqs?5=}ugLP=ah*R)!Q(cS z)cOu~4nWv+UISZuVYhAu_^h6yEI>#TF>ZIow|kn)-Kc6?hlhs?MlA=+H81j6yk646 zV3OHi<3N;^9^2)kFd2_&?V+}W+rB?bvvoap9d;+B#Frkw2?#jr;N)cfBb*ag;W#m*&;m5L-&2V|ln(N)WecKC;H=>g2 zx-QjadvKHKXpR4j_kle`*3Onro_5{L%Bo9mZ%l7$u54b-+&lw3B1?lZxEEq|o%pFK zv!UHI6|>&Ok;V7m4SB|5^Vb!6^NjvgM!3=oO~ zayS2asJd=iO^xJy4*;LlwY57;bK3<@^Sbj|H0N7j`Ml;`EHF}D9@hZ2UA|H@1Pnw| zSts?Mz5L+DOLs__=DZ>}eD;~wDvU(#hXOI0U{WH3!ue}9(gTwWjgwV2rWO|8 z@bl=?GBf4C5H1gB>*?h^R91>rnj?M(`T1}gqxbD&JFY<2eepJ&8|R6y;J|K1 z<|iv(V<0>U|0XM^+0Si<)Em0HQQxK|XpH2|_*n@$H?(MoUy+;33G$nFzjfCmRrwpU_hla(vGoN_hf#>U1fw_%g)?epiI!TJUx7E2#X%JUG5R2y*Sh`zyS>h_h^?`wr6 zgXt077=B`AVbPI?Y4|D7L`|G3_e{{ZvUc+wi)_H?hSs!Zm_t+ZgYk)o@(K3|vF_LN z#rA~+f1fcGy0|Py*mcl8 zKFi*uM~@EeZgp3!r5j61Np-fjj~>F2_%Vjp8kQf3k0^Uqv;U*9K$Es_(ag{5L$28 zm%(TZRp78%kTOWA(kPns z()**~c%%(}N2MW(rMP(UOBl>k z2E>iIkIsG1{j7fbl{9^U=l+9v0isi;dpn!KJ`8#x`oZ*omdezF!I⪙ZQ%oEC$PU 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

@!r9Fpp$vu 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 Q|E)P`;HBB0OV;H70hDgG>Hq)$ diff --git a/benchmarks/dumb-server/Dockerfile b/benchmarks/dumb-server/Dockerfile new file mode 100644 index 0000000..b307298 --- /dev/null +++ b/benchmarks/dumb-server/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.22-alpine AS build-stage +WORKDIR /app +RUN apk add make +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +WORKDIR /app/benchmarks/dumb-server +RUN make build + +FROM alpine +WORKDIR / +COPY --from=build-stage /tmp/bin/dumb-server /dumb-server +ENTRYPOINT ["/dumb-server"] diff --git a/benchmarks/dumb-server/docker-compose.yaml b/benchmarks/dumb-server/docker-compose.yaml new file mode 100644 index 0000000..0694d20 --- /dev/null +++ b/benchmarks/dumb-server/docker-compose.yaml @@ -0,0 +1,9 @@ +version: "3.8" +services: + server: + container_name: server-${COMPOSE_PROJECT_NAME} + image: server + build: + context: ../.. + dockerfile: benchmarks/dumb-server/Dockerfile + network_mode: host diff --git a/benchmarks/dumb-server/main.go b/benchmarks/dumb-server/main.go index 799aee4..710add1 100644 --- a/benchmarks/dumb-server/main.go +++ b/benchmarks/dumb-server/main.go @@ -122,8 +122,6 @@ func newResps() (resp1 []byte, resp1Mod respModifier, resp2 []byte, resp2Mod res resp = append(resp, headerBuf.Bytes()...) msgPrefix := make([]byte, 5) // не кодированный ответ 0 длины = любое сообщение со всеми пустыми полями - // TODO: remove debug - // msgPrefix := []byte{255, 254, 253, 252, 251} dataLen := len(msgPrefix) respMod.dataStreamIDindx = len(resp) + 5 @@ -305,8 +303,16 @@ func handleConn(conn net.Conn, i int) (err error) { processor := reciever.NewProcessor([]reciever.FrameTypeProcessor{ http2.FrameData: endStreamProcessor, http2.FrameHeaders: endStreamProcessor, - http2.FrameContinuation: endStreamProcessor, http2.FramePing: &pingProcessor{cmds}, + http2.FrameContinuation: endStreamProcessor, + + http2.FrameRSTStream: noopFrameProcessor{}, + http2.FrameGoAway: noopFrameProcessor{}, + http2.FrameWindowUpdate: noopFrameProcessor{}, + + // http.FramePriority not supported + http2.FrameSettings: settingsProcessor{}, + // http.FramePushPromise not supported }) g.Go(func() error { defer println("processor done") @@ -369,7 +375,6 @@ func handleConn(conn net.Conn, i int) (err error) { } func writeLoop(ctx context.Context, conn io.Writer, commands <-chan *command, _ types.FlowControl, _ int) (err error) { - f := http2.NewFramer(conn, nil) r1Original, r1mod, r2Original, r2mod := newResps() select { case cmd := <-commands: @@ -381,7 +386,7 @@ func writeLoop(ctx context.Context, conn io.Writer, commands <-chan *command, _ return err } case commandTypePing: - err := f.WritePing(true, cmd.pingPayload) + err := http2.NewFramer(conn, nil).WritePing(true, cmd.pingPayload) if err != nil { return err } @@ -393,8 +398,9 @@ func writeLoop(ctx context.Context, conn io.Writer, commands <-chan *command, _ const limit = 1024 type t struct { - bufSrc [limit][]byte - requests [limit][]byte + bufSrc [limit][]byte + requests [limit][]byte + pingFrame []byte } var t1, t2 t @@ -403,16 +409,23 @@ func writeLoop(ctx context.Context, conn io.Writer, commands <-chan *command, _ t2.requests[i] = bytes.Clone(r2Original) } - writeChan := make(chan net.Buffers, 1) - go func() { - ticker := time.NewTicker(time.Millisecond) - defer close(writeChan) - defer ticker.Stop() + { pingFrame := make([]byte, 9+8) pingHeader := frameheader.FrameHeader(pingFrame) pingHeader.SetType(http2.FramePing) + pingHeader.SetLength(8) pingHeader.SetFlags(http2.FlagPingAck) + t1.pingFrame = pingFrame + t2.pingFrame = bytes.Clone(pingFrame) + } + + writeChan := make(chan net.Buffers) // важно чтобы канал был небуферизованный! + go func() { + ticker := time.NewTicker(time.Millisecond) + defer close(writeChan) + defer ticker.Stop() + nextT, t := &t1, &t2 for { t, nextT = nextT, t @@ -430,16 +443,16 @@ func writeLoop(ctx context.Context, conn io.Writer, commands <-chan *command, _ scheduledBytesOUT.Add(uint64(len(r2))) if len(buf) == limit { - writeBuf = buf[1:] // не пишем резервную позицию + writeBuf = buf[1:] // НЕ пишем резервную позицию } case commandTypePing: - copy(pingFrame[9:], cmd.pingPayload[:]) - buf[0] = pingFrame - writeBuf = buf + copy(t.pingFrame[9:], cmd.pingPayload[:]) + buf[0] = t.pingFrame + writeBuf = buf // пишем резервную позицию } commandsPool.Release(cmd) case <-ticker.C: - writeBuf = buf[1:] // не пишем резервную позицию + writeBuf = buf[1:] // НЕ пишем резервную позицию case <-ctx.Done(): return } @@ -463,7 +476,7 @@ func writeLoop(ctx context.Context, conn io.Writer, commands <-chan *command, _ cmdsAfter := len(commands) since := time.Since(writeStart) - if since > time.Millisecond*10 { + if since > time.Millisecond*10 && false { println(since.String(), cmdsBefore, cmdsAfter) } bytesOUT.Add(l * uint64(len(r2Original))) @@ -503,18 +516,16 @@ func configureConn(conn io.ReadWriter, buf []byte) (settings, error) { return s, fmt.Errorf("first frame from other end is not settings, got %T", frame) } - if !sf.IsAck() { - if val, ok := sf.Value(http2.SettingInitialWindowSize); ok { - s.InitialWindowSize = val - } - if val, ok := sf.Value(http2.SettingMaxConcurrentStreams); ok { - s.MaxConcurrentStreams = val - } + if val, ok := sf.Value(http2.SettingInitialWindowSize); ok { + s.InitialWindowSize = val + } + if val, ok := sf.Value(http2.SettingMaxConcurrentStreams); ok { + s.MaxConcurrentStreams = val + } - err = framer.WriteSettingsAck() - if err != nil { - return s, fmt.Errorf("writing settings ack: %w", err) - } + err = framer.WriteSettingsAck() + if err != nil { + return s, fmt.Errorf("writing settings ack: %w", err) } // у h2load на валидное значение происходит переполнение буффера с отвалом соедиенинения, поэтому: - 65_535 @@ -527,6 +538,16 @@ func configureConn(conn io.ReadWriter, buf []byte) (settings, error) { return s, nil } +type noopFrameProcessor struct{} + +func (p noopFrameProcessor) Process( + _ frameheader.FrameHeader, + _ []byte, + incomplete bool, +) error { + return nil +} + type endStreamProcessor struct { respChan chan<- *command } @@ -557,11 +578,11 @@ type pingProcessor struct { } func (p *pingProcessor) Process( - _ frameheader.FrameHeader, + header frameheader.FrameHeader, payload []byte, incomplete bool, ) error { - if incomplete { + if incomplete || !header.Flags().Has(http2.FlagPingAck) { return nil } @@ -575,3 +596,20 @@ func (p *pingProcessor) Process( return nil } + +type settingsProcessor struct{} + +func (p settingsProcessor) Process( + header frameheader.FrameHeader, + payload []byte, + incomplete bool, +) error { + if incomplete { + return nil + } + + if !header.Flags().Has(http2.FlagSettingsAck) { + return errors.New("update settings in runtime not supported") + } + return nil +} diff --git a/benchmarks/framer/README.md b/benchmarks/framer/README.md new file mode 100644 index 0000000..0181536 --- /dev/null +++ b/benchmarks/framer/README.md @@ -0,0 +1,6 @@ +How to run +- Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` +- Run local: + * Install [framer](../../README.md#install) + * Run dumb server: `(cd ../dumb-server && make run)` + * Execute `./run.sh` diff --git a/benchmarks/framer/REAME.md b/benchmarks/framer/REAME.md deleted file mode 100644 index 5705f01..0000000 --- a/benchmarks/framer/REAME.md +++ /dev/null @@ -1,4 +0,0 @@ -How to run -- Install [framer](../../README.md#install) -- Run dumb server: `(cd ../dumb-server && make run)` -- Execute `./run.sh` diff --git a/benchmarks/framer/docker-compose.yaml b/benchmarks/framer/docker-compose.yaml new file mode 100644 index 0000000..26eb442 --- /dev/null +++ b/benchmarks/framer/docker-compose.yaml @@ -0,0 +1,22 @@ +name: framer-benchmark +include: + - ../dumb-server/docker-compose.yaml +services: + framer: + container_name: framer + image: framer + build: + context: ../.. + dockerfile: Dockerfile + network_mode: host + volumes: + - ./requests.bin:/tmp/requests.bin + command: > + load --addr=localhost:9090 --inmem-requests + --requests-file=/tmp/requests.bin --clients 10 + unlimited --duration 1m + deploy: + resources: + limits: + cpus: '2' + memory: 2G diff --git a/benchmarks/gatling/.dockerignore b/benchmarks/gatling/.dockerignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/benchmarks/gatling/.dockerignore @@ -0,0 +1 @@ +build diff --git a/benchmarks/gatling/Dockerfile b/benchmarks/gatling/Dockerfile new file mode 100644 index 0000000..ac58c20 --- /dev/null +++ b/benchmarks/gatling/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:17-oracle +WORKDIR /app +COPY . /app +RUN ./gradlew +ENTRYPOINT ["./gradlew", "gatlingRun-bench.BenchKt"] diff --git a/benchmarks/gatling/README.md b/benchmarks/gatling/README.md index 0fad7e0..fc0479f 100644 --- a/benchmarks/gatling/README.md +++ b/benchmarks/gatling/README.md @@ -1,4 +1,6 @@ How to run -- Install java/gradle -- Run dumb server: `(cd ../dumb-server && make run)` -- Execute `./run.sh` \ No newline at end of file +- Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` +- Run local: + * Install java and gradle + * Run dumb server: `(cd ../dumb-server && make run)` + * Execute `./run.sh` diff --git a/benchmarks/gatling/docker-compose.yaml b/benchmarks/gatling/docker-compose.yaml new file mode 100644 index 0000000..c264fb7 --- /dev/null +++ b/benchmarks/gatling/docker-compose.yaml @@ -0,0 +1,19 @@ +name: gatling-benchmark +include: + - ../dumb-server/docker-compose.yaml +services: + gatling: + container_name: gatling + image: gatling + build: + context: ./ + dockerfile: Dockerfile + network_mode: host + working_dir: /app + volumes: + - ./:/app + deploy: + resources: + limits: + cpus: '2' + memory: 2G diff --git a/benchmarks/ghz/Dockerfile b/benchmarks/ghz/Dockerfile new file mode 100644 index 0000000..b54c863 --- /dev/null +++ b/benchmarks/ghz/Dockerfile @@ -0,0 +1,5 @@ +FROM ubuntu:24.04 +WORKDIR /app +ADD https://github.com/bojand/ghz/releases/download/v0.120.0/ghz-linux-x86_64.tar.gz ./ +RUN tar xvf ghz-linux-x86_64.tar.gz +ENTRYPOINT ["/app/ghz"] diff --git a/benchmarks/ghz/README.md b/benchmarks/ghz/README.md new file mode 100644 index 0000000..b395057 --- /dev/null +++ b/benchmarks/ghz/README.md @@ -0,0 +1,6 @@ +How to run +- Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` +- Run local: + - Install [ghz](https://ghz.sh/docs/install) + - Run dumb server: `(cd ../dumb-server && make run)` + - Execute `./run.sh` diff --git a/benchmarks/ghz/REAME.md b/benchmarks/ghz/REAME.md deleted file mode 100644 index 9677fb5..0000000 --- a/benchmarks/ghz/REAME.md +++ /dev/null @@ -1,4 +0,0 @@ -How to run -- Install [framer](https://ghz.sh/docs/install) -- Run dumb server: `(cd ../dumb-server && make run)` -- Execute `./run.sh` diff --git a/benchmarks/ghz/docker-compose.yaml b/benchmarks/ghz/docker-compose.yaml new file mode 100644 index 0000000..e282ee1 --- /dev/null +++ b/benchmarks/ghz/docker-compose.yaml @@ -0,0 +1,25 @@ +name: ghz-benchmark +include: + - ../dumb-server/docker-compose.yaml +services: + ghz: + container_name: ghz + image: ghz + build: + context: ./ + dockerfile: Dockerfile + network_mode: host + volumes: + - ../dumb-server/api/api.proto:/tmp/api.proto + - ./ghz-data.bin:/tmp/ghz-data.bin + command: > + --insecure --async + --proto /tmp/api.proto + --call test.api.TestApi/Test + -c 10 --total 100000 + -B /tmp/ghz-data.bin localhost:9090 + deploy: + resources: + limits: + cpus: '2' + memory: 2G diff --git a/benchmarks/h2load/Dockerfile b/benchmarks/h2load/Dockerfile new file mode 100644 index 0000000..c0613db --- /dev/null +++ b/benchmarks/h2load/Dockerfile @@ -0,0 +1,3 @@ +FROM ubuntu:24.04 +RUN apt-get update && apt-get install -y nghttp2-client +ENTRYPOINT ["h2load"] diff --git a/benchmarks/h2load/README.md b/benchmarks/h2load/README.md new file mode 100644 index 0000000..eb4ea08 --- /dev/null +++ b/benchmarks/h2load/README.md @@ -0,0 +1,6 @@ +How to run +- Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` +- Run local: + * Install h2load: `sudo apt install nghttp2-client` + * Run dumb server: `(cd ../dumb-server && make run)` + * Execute `./run.sh` diff --git a/benchmarks/h2load/REAME.md b/benchmarks/h2load/REAME.md deleted file mode 100644 index 7391af4..0000000 --- a/benchmarks/h2load/REAME.md +++ /dev/null @@ -1,4 +0,0 @@ -How to run -- Install h2load: `sudo apt install nghttp2-client` -- Run dumb server: `(cd ../dumb-server && make run)` -- Execute `./run.sh` diff --git a/benchmarks/h2load/docker-compose.yaml b/benchmarks/h2load/docker-compose.yaml new file mode 100644 index 0000000..e6f3a5c --- /dev/null +++ b/benchmarks/h2load/docker-compose.yaml @@ -0,0 +1,24 @@ +name: h2load-benchmark +include: + - ../dumb-server/docker-compose.yaml +services: + h2load: + container_name: h2load + image: h2load + build: + context: ./ + dockerfile: Dockerfile + network_mode: host + volumes: + - ./data:/tmp/data + command: > + http://localhost:9090/test.api.TestService/Test + -n 6000000 -c 10 -m 200 + --data=/tmp/data + -H 'x-my-header-key1: my-header-val1' -H 'x-my-header-key2: my-header-val2' + -H 'te: trailers' -H 'content-type: application/grpc' + deploy: + resources: + limits: + cpus: '2' + memory: 2G diff --git a/benchmarks/k6/README.md b/benchmarks/k6/README.md new file mode 100644 index 0000000..f561e8b --- /dev/null +++ b/benchmarks/k6/README.md @@ -0,0 +1,6 @@ +How to run +- Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` +- Run local: + * [Install k6](https://k6.io/docs/get-started/installation/) + * Run dumb server: `(cd ../dumb-server && make run)` + * Execute `./run.sh` diff --git a/benchmarks/k6/REAME.md b/benchmarks/k6/REAME.md deleted file mode 100644 index 92edb3f..0000000 --- a/benchmarks/k6/REAME.md +++ /dev/null @@ -1,4 +0,0 @@ -How to run -- [Install k6](https://k6.io/docs/get-started/installation/) -- Run dumb server: `(cd ../dumb-server && make run)` -- Execute `./run.sh` diff --git a/benchmarks/k6/docker-compose.yaml b/benchmarks/k6/docker-compose.yaml new file mode 100644 index 0000000..e345c4e --- /dev/null +++ b/benchmarks/k6/docker-compose.yaml @@ -0,0 +1,19 @@ +name: k6-benchmark +include: + - ../dumb-server/docker-compose.yaml +services: + k6: + container_name: k6 + image: grafana/k6:0.51.0 + network_mode: host + volumes: + - ./script.js:/app/k6/script.js + - ../dumb-server/api/api.proto:/app/dumb-server/api/api.proto + working_dir: /app + command: > + run /app/k6/script.js + deploy: + resources: + limits: + cpus: '2' + memory: 2G diff --git a/benchmarks/pandora/Dockerfile b/benchmarks/pandora/Dockerfile new file mode 100644 index 0000000..70a66cd --- /dev/null +++ b/benchmarks/pandora/Dockerfile @@ -0,0 +1,5 @@ +FROM ubuntu:24.04 +WORKDIR /app +ADD https://github.com/yandex/pandora/releases/download/v0.5.26/pandora_0.5.26_linux_amd64 /usr/local/bin/pandora +RUN chmod +x /usr/local/bin/pandora +ENTRYPOINT ["pandora"] diff --git a/benchmarks/pandora/README.md b/benchmarks/pandora/README.md new file mode 100644 index 0000000..9930db1 --- /dev/null +++ b/benchmarks/pandora/README.md @@ -0,0 +1,6 @@ +How to run +- Run in docker (needs docker-compose v2): `docker compose up --abort-on-container-exit` +- Run local: + * Install [pandora](https://github.com/yandex/pandora#how-to-start) + * Run dumb server: `(cd ../dumb-server && make run)` + * Execute `./run.sh` diff --git a/benchmarks/pandora/REAME.md b/benchmarks/pandora/REAME.md deleted file mode 100644 index bd4b9a5..0000000 --- a/benchmarks/pandora/REAME.md +++ /dev/null @@ -1,4 +0,0 @@ -How to run -- Install [pandora](https://github.com/yandex/pandora#how-to-start) -- Run dumb server: `(cd ../dumb-server && make run)` -- Execute `./run.sh` diff --git a/benchmarks/pandora/config.yaml b/benchmarks/pandora/config.yaml index 3eaf129..97cc276 100644 --- a/benchmarks/pandora/config.yaml +++ b/benchmarks/pandora/config.yaml @@ -12,8 +12,9 @@ pools: type: discard rps: - duration: 1m - type: unlimited - discard_overflow: true + ops: 200_000 + type: const # type unlimited has bug + discard_overflow: false startup: type: once # Clients count. Yandex pandora don't use http/2 multiplexing, diff --git a/benchmarks/pandora/docker-compose.yaml b/benchmarks/pandora/docker-compose.yaml new file mode 100644 index 0000000..3736a4e --- /dev/null +++ b/benchmarks/pandora/docker-compose.yaml @@ -0,0 +1,20 @@ +name: pandora-benchmark +include: + - ../dumb-server/docker-compose.yaml +services: + pandora: + container_name: pandora + image: pandora + build: + context: ./ + dockerfile: Dockerfile + network_mode: host + volumes: + - ./:/app/ + command: > + config.yaml + deploy: + resources: + limits: + cpus: '2' + memory: 2G diff --git a/cmd/framer/cmd_load.go b/cmd/framer/cmd_load.go index 2210aa5..ab48667 100644 --- a/cmd/framer/cmd_load.go +++ b/cmd/framer/cmd_load.go @@ -15,6 +15,7 @@ import ( "go.uber.org/zap" "golang.org/x/sync/errgroup" + "github.com/ozontech/framer/consts" "github.com/ozontech/framer/datasource" "github.com/ozontech/framer/loader" "github.com/ozontech/framer/loader/types" @@ -131,33 +132,29 @@ func (c *LoadCommand) Run( g, ctx := errgroup.WithContext(ctx) - loaderConfig := loader.DefaultConfig() - - var reporter types.Reporter = supersimpleReporter.New(loaderConfig.Timeout) + timeout := consts.DefaultTimeout + var reporter types.Reporter = supersimpleReporter.New(timeout) if c.Phout != "" { f, err := os.Create(c.Phout) if err != nil { return fmt.Errorf("creating phout file(%s): %w", c.Phout, err) } - phoutReporter := phoutReporter.New(f, loaderConfig.Timeout) - + phoutReporter := phoutReporter.New(f, timeout) reporter = multi.NewMutli(phoutReporter, reporter) } g.Go(reporter.Run) loaders := make([]*loader.Loader, clients) for i := 0; i < clients; i++ { - conn, err := createConn(ctx, loaderConfig.Timeout, addr) - // conn, err := createUnixConn(addr) + conn, err := createConn(ctx, timeout, addr) if err != nil { return fmt.Errorf("dialing: %w", err) } l, err := loader.NewLoader( - ctx, conn, reporter, + timeout, log, - loader.DefaultConfig(), ) if err != nil { return fmt.Errorf("loader setup: %w", err) diff --git a/cmd/framer/main.go b/cmd/framer/main.go index d4f76f2..57fd021 100644 --- a/cmd/framer/main.go +++ b/cmd/framer/main.go @@ -6,6 +6,7 @@ import ( "math" "net/http" _ "net/http/pprof" //nolint:gosec + "runtime/debug" "github.com/alecthomas/kong" mangokong "github.com/alecthomas/mango-kong" @@ -19,14 +20,12 @@ var CLI struct { DebugServer bool `help:"Enable debug server."` } -var Version = "unknown" - type VersionFlag string func (v VersionFlag) Decode(ctx *kong.DecodeContext) error { return nil } func (v VersionFlag) IsBool() bool { return true } func (v VersionFlag) BeforeApply(app *kong.Kong, vars kong.Vars) error { - fmt.Println(Version) + fmt.Println(getVersion()) app.Exit(0) return nil } @@ -66,3 +65,29 @@ The framer is used to generate test requests to grpc servers and measure codes a err := kongCtx.Run() kongCtx.FatalIfErrorf(err) } + +const unknownVersion = "unknown" + +var Version = unknownVersion + +func getVersion() string { + if Version != unknownVersion { + return Version + } + + info, ok := debug.ReadBuildInfo() + if !ok { + return Version + } + + for _, kv := range info.Settings { + if kv.Value == "" { + continue + } + if kv.Key == "vcs.revision" && kv.Value != "" { + return kv.Value + } + } + + return Version +} diff --git a/cmd/framer/main_benchmark_test.go b/cmd/framer/main_benchmark_test.go index 348e157..5ad1d1a 100644 --- a/cmd/framer/main_benchmark_test.go +++ b/cmd/framer/main_benchmark_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/ozontech/framer/consts" "github.com/ozontech/framer/datasource" "github.com/ozontech/framer/loader" "github.com/ozontech/framer/report/simple" @@ -52,7 +53,7 @@ func BenchmarkE2E(b *testing.B) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - conn, err := createConn(ctx, 5*time.Second, "localhost:9090") + conn, err := createConn(ctx, consts.DefaultTimeout, "localhost:9090") if err != nil { b.Fatal(err) } @@ -61,14 +62,11 @@ func BenchmarkE2E(b *testing.B) { go func() { a.NoError(reporter.Run()) }() defer func() { a.NoError(reporter.Close()) }() - conf := loader.DefaultConfig() - conf.StreamsLimit = 16 l, err := loader.NewLoader( - ctx, conn, reporter, + consts.DefaultTimeout, zaptest.NewLogger(b), - loader.DefaultConfig(), ) if err != nil { b.Fatal(fmt.Errorf("loader setup: %w", err)) @@ -127,14 +125,11 @@ func BenchmarkE2EInMemDatasource(b *testing.B) { reportErr := make(chan error) go func() { reportErr <- reporter.Run() }() - conf := loader.DefaultConfig() - conf.StreamsLimit = 16 l, err := loader.NewLoader( - ctx, conn, reporter, + consts.DefaultTimeout, zaptest.NewLogger(b), - loader.DefaultConfig(), ) a.NoError(err) diff --git a/consts/consts.go b/consts/consts.go index eec6a72..d6939a7 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -7,4 +7,8 @@ const ( RecieveBufferSize = 2048 SendBatchTimeout = time.Millisecond RecieveBatchTimeout = time.Millisecond + + DefaultInitialWindowSize = 65_535 + DefaultTimeout = 11 * time.Second + DefaultMaxFrameSize = 16384 // Максимальная длина пейлоада фрейма в grpc. У http2 ограничение больше. ) diff --git a/datasource/file_benchmark_test.go b/datasource/file_benchmark_test.go index dd32c3f..5054497 100644 --- a/datasource/file_benchmark_test.go +++ b/datasource/file_benchmark_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "github.com/ozontech/framer/consts" "github.com/ozontech/framer/loader/types" hpackwrapper "github.com/ozontech/framer/utils/hpack_wrapper" ) @@ -27,7 +28,7 @@ func BenchmarkFileDataSource(b *testing.B) { go func() { defer close(done) for r := range rr { - r.SetUp(0, &noopHpackFieldWriter{}) + r.SetUp(consts.DefaultMaxFrameSize, 0, &noopHpackFieldWriter{}) b.SetBytes(int64(r.Size())) r.Release() } @@ -60,7 +61,7 @@ func BenchmarkRequestSetupNoop(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - r.SetUp(0, &noopHpackFieldWriter{}) + r.SetUp(consts.DefaultMaxFrameSize, 0, &noopHpackFieldWriter{}) b.SetBytes(int64(r.Size())) } } @@ -81,7 +82,7 @@ func BenchmarkRequestSetupHpack(b *testing.B) { hpackwrapper := hpackwrapper.NewWrapper() b.ResetTimer() for i := 0; i < b.N; i++ { - r.SetUp(0, hpackwrapper) + r.SetUp(consts.DefaultMaxFrameSize, 0, hpackwrapper) b.SetBytes(int64(r.Size())) } } diff --git a/datasource/inmem_benchmark_test.go b/datasource/inmem_benchmark_test.go index 993a93d..0247766 100644 --- a/datasource/inmem_benchmark_test.go +++ b/datasource/inmem_benchmark_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/ozontech/framer/consts" "github.com/ozontech/framer/loader/types" ) @@ -33,7 +34,7 @@ func BenchmarkInmemDataSource(b *testing.B) { }() for r := range rr { - r.SetUp(0, &noopHpackFieldWriter{}) + r.SetUp(consts.DefaultMaxFrameSize, 0, &noopHpackFieldWriter{}) b.SetBytes(int64(r.Size())) r.Release() } diff --git a/datasource/request.go b/datasource/request.go index 7e1b934..9d81fad 100644 --- a/datasource/request.go +++ b/datasource/request.go @@ -41,31 +41,6 @@ func NewRequestAdapterFactory(ops ...Option) *RequestAdapterFactory { return f } -type additionalHeadersOpts []string - -func (h additionalHeadersOpts) apply(f *RequestAdapterFactory) { - for i := 0; i < len(h); i += 2 { - k, v := h[i], h[i+1] - if strings.HasPrefix(k, ":") { - f.staticPseudoHeaders = append(f.staticPseudoHeaders, k, v) - } else { - f.staticRegularHeaders = append(f.staticRegularHeaders, k, v) - } - } -} - -func WithAdditionalHeader(k, v string) Option { - return additionalHeadersOpts([]string{k, v}) -} - -func WithAdditionalHeaders(headers []string) Option { - return additionalHeadersOpts(headers) -} - -func WithTimeout(t time.Duration) Option { - return additionalHeadersOpts([]string{"grpc-timeout", grpcutil.EncodeDuration(t)}) -} - func (f *RequestAdapterFactory) Build() *RequestAdapter { return NewRequestAdapter( f.isAllowedMeta, @@ -142,8 +117,6 @@ func (a *RequestAdapter) setData(data model.Data) { a.data = data } func (a *RequestAdapter) FullMethodName() string { return unsafeString(a.data.Method) } func (a *RequestAdapter) Tag() string { return unsafeString(a.data.Tag) } -const maxFramePayloadLen int = 16384 // Максимальная длина пейлоада фрейма в grpc. У http2 ограничение больше. - // TODO(pgribanov): после реализации собственной системы энкодинга хедеров, // отказаться от unsafe func unsafeString(b []byte) string { @@ -152,6 +125,7 @@ func unsafeString(b []byte) string { } func (a *RequestAdapter) setUpHeaders( + maxFramePayloadLen int, streamID uint32, hpack types.HPackFieldWriter, ) { @@ -219,6 +193,7 @@ func (a *RequestAdapter) setUpHeaders( } func (a *RequestAdapter) setUpPayload( + maxFramePayloadLen int, streamID uint32, ) { data := a.data @@ -293,6 +268,7 @@ func (a *RequestAdapter) Size() int { } func (a *RequestAdapter) SetUp( + maxFramePayloadLen int, streamID uint32, hpackFieldWriter types.HPackFieldWriter, ) []types.Frame { @@ -301,8 +277,33 @@ func (a *RequestAdapter) SetUp( a.frames = a.frames[:0] - a.setUpHeaders(streamID, hpackFieldWriter) - a.setUpPayload(streamID) + a.setUpHeaders(maxFramePayloadLen, streamID, hpackFieldWriter) + a.setUpPayload(maxFramePayloadLen, streamID) return a.frames } + +func WithAdditionalHeader(k, v string) Option { + return additionalHeadersOpts([]string{k, v}) +} + +func WithAdditionalHeaders(headers []string) Option { + return additionalHeadersOpts(headers) +} + +func WithTimeout(t time.Duration) Option { + return additionalHeadersOpts([]string{"grpc-timeout", grpcutil.EncodeDuration(t)}) +} + +type additionalHeadersOpts []string + +func (h additionalHeadersOpts) apply(f *RequestAdapterFactory) { + for i := 0; i < len(h); i += 2 { + k, v := h[i], h[i+1] + if strings.HasPrefix(k, ":") { + f.staticPseudoHeaders = append(f.staticPseudoHeaders, k, v) + } else { + f.staticRegularHeaders = append(f.staticRegularHeaders, k, v) + } + } +} diff --git a/datasource/request_test.go b/datasource/request_test.go index 5b10eb2..eaa8231 100644 --- a/datasource/request_test.go +++ b/datasource/request_test.go @@ -9,6 +9,7 @@ import ( "golang.org/x/net/http2" "golang.org/x/net/http2/hpack" + "github.com/ozontech/framer/consts" "github.com/ozontech/framer/formats/model" hpackwrapper "github.com/ozontech/framer/utils/hpack_wrapper" ) @@ -76,7 +77,7 @@ func TestRequest1(t *testing.T) { hpw := hpackwrapper.NewWrapper() const streamID uint32 = 123 - frames := r.SetUp(streamID, hpw) + frames := r.SetUp(consts.DefaultMaxFrameSize, streamID, hpw) a.Len(frames, 2) for _, f := range frames { for _, c := range f.Chunks { diff --git a/loader/e2e_test.go b/loader/e2e_test.go index cc0f472..93f384e 100644 --- a/loader/e2e_test.go +++ b/loader/e2e_test.go @@ -10,6 +10,7 @@ import ( "os" "testing" + "github.com/ozontech/framer/consts" "github.com/ozontech/framer/datasource" "github.com/ozontech/framer/loader/types" "github.com/stretchr/testify/assert" @@ -26,7 +27,10 @@ func TestE2E(t *testing.T) { log := zaptest.NewLogger(t) a := assert.New(t) clientConn, serverConn := net.Pipe() - l := newLoader(clientConn, nooReporter{}, log, DefaultConfig()) + l := newLoader( + clientConn, nooReporter{}, + loaderConfig{timeout: consts.DefaultTimeout}, log, + ) requestsFile, err := os.Open("../test_files/requests") if err != nil { diff --git a/loader/loader.go b/loader/loader.go index e255450..f98cce2 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -15,12 +15,14 @@ import ( "golang.org/x/net/http2" "golang.org/x/sync/errgroup" + "github.com/ozontech/framer/consts" fc "github.com/ozontech/framer/loader/flowcontrol" "github.com/ozontech/framer/loader/reciever" "github.com/ozontech/framer/loader/sender" streamsPool "github.com/ozontech/framer/loader/streams/pool" streamsStore "github.com/ozontech/framer/loader/streams/store" "github.com/ozontech/framer/loader/types" + hpackwrapper "github.com/ozontech/framer/utils/hpack_wrapper" ) var clientPreface = []byte(http2.ClientPreface) @@ -37,8 +39,6 @@ type Loader struct { streamsStore types.StreamStore timeoutQueue TimeoutQueue - conf Config - sender *sender.Sender reciever *reciever.Reciever loaderID int32 @@ -46,61 +46,79 @@ type Loader struct { log *zap.Logger } -type Config struct { - Timeout time.Duration // таймаут для запросов - StreamsLimit uint32 `config:"max-open-streams" validate:"min=0"` -} - -const defaultTimeout = 11 * time.Second - -func DefaultConfig() Config { - return Config{ - Timeout: defaultTimeout, - StreamsLimit: 0, - } -} - -var i int32 - func NewLoader( - ctx context.Context, conn net.Conn, reporter types.Reporter, + timeout time.Duration, log *zap.Logger, - conf Config, ) (*Loader, error) { - l := newLoader(conn, reporter, log, conf) - setupCtx, setupCancel := context.WithTimeout(ctx, conf.Timeout) - defer setupCancel() - return l, l.setup(setupCtx) + conf := loaderConfig{timeout: timeout} + + err := conn.SetDeadline(time.Now().Add(conf.timeout)) + if err != nil { + return nil, fmt.Errorf("set conn deadline: %w", err) + } + + err = setupHTTP2(conn, conn, &conf) + if err != nil { + return nil, err + } + + return newLoader(conn, reporter, conf, log), nil } +type loaderConfig struct { + timeout time.Duration + maxConcurrentStreams uint32 + initialWindowSize uint32 + maxDymanicTableSize uint32 + maxFrameSize uint32 +} + +var i int32 + func newLoader( conn net.Conn, reporter types.LoaderReporter, + conf loaderConfig, log *zap.Logger, - conf Config, ) *Loader { + if conf.timeout == 0 { + conf.timeout = consts.DefaultTimeout + } loaderID := atomic.AddInt32(&i, 1) log = log.Named("loader").With(zap.Int32("loader-id", loaderID)) log.Debug("loader created") - // streamsStore := streamsStore.NewStreamsNoop() streamsStore := streamsStore.NewShardedStreamsMap(16, func() types.StreamStore { return streamsStore.NewStreamsMap() }) - // streamsStore := NewLimitedStreams(NewStreamsSyncMap(), loaderID, conf.StreamsLimit) - // streamsStore := NewLimitedStreams(NewStreamsMap(), loaderID, conf.StreamsLimit) - timeoutQueue := NewTimeoutQueue(conf.Timeout) + timeoutQueue := NewTimeoutQueue(conf.timeout) + fcConn := fc.NewFlowControl(consts.DefaultInitialWindowSize) // для соединения (по спеке игнорирует SETTINGS_INITIAL_WINDOW_SIZE) - // TODO(pgribanov): math.MaxUint32? - fcConn := fc.NewFlowControl(math.MaxUint32) // для соединения (не может меняться в течение жизни соединения) + var streamPoolOpts []streamsPool.Opt + if conf.maxConcurrentStreams != 0 { + streamPoolOpts = append(streamPoolOpts, streamsPool.WithMaxConcurrentStreams(conf.maxConcurrentStreams)) + } + if conf.initialWindowSize != 0 { + streamPoolOpts = append(streamPoolOpts, streamsPool.WithInitialWindowSize(conf.initialWindowSize)) + } + streamsPool := streamsPool.NewStreamsPool(reporter, streamPoolOpts...) + + var hpackWrapperOpts []hpackwrapper.Opt + if conf.maxDymanicTableSize != 0 { + hpackWrapperOpts = append(hpackWrapperOpts, hpackwrapper.WithMaxDynamicTableSize(conf.maxDymanicTableSize)) + } + hpackWrapper := hpackwrapper.NewWrapper(hpackWrapperOpts...) + + maxFrameSize := consts.DefaultMaxFrameSize + if conf.maxFrameSize != 0 { + maxFrameSize = int(conf.maxFrameSize) + } priorityFramesCh := make(chan []byte, 1) - streamsPool := streamsPool.NewStreamsPool(reporter, 1024, conf.StreamsLimit) return &Loader{ conn: conn, - conf: conf, timeoutQueue: timeoutQueue, log: log, loaderID: loaderID, @@ -108,34 +126,16 @@ func newLoader( streamsStore: streamsStore, streamsPool: streamsPool, - sender: sender.NewSender(conn, fcConn, priorityFramesCh, streamsPool, streamsStore), - reciever: reciever.NewReciever(conn, fcConn, priorityFramesCh, streamsStore), + sender: sender.NewSender( + conn, fcConn, priorityFramesCh, streamsPool, + streamsStore, hpackWrapper, maxFrameSize, + ), + reciever: reciever.NewReciever( + conn, fcConn, priorityFramesCh, streamsStore, + ), } } -func (l *Loader) setup(ctx context.Context) (err error) { - deadline, ok := ctx.Deadline() - if ok { - err = l.conn.SetDeadline(deadline) - if err != nil { - return fmt.Errorf("set conn deadline: %w", err) - } - } - - connConf, err := setupHTTP2(l.conn, l.conn) - if err != nil { - return err - } - - if connConf.InitialWindowSize != 0 { - l.streamsPool.SetInitialWindowSize(connConf.InitialWindowSize) - } - if connConf.MaxConcurrentStreams != 0 { - l.streamsPool.SetLimit(connConf.MaxConcurrentStreams) - } - return nil -} - func (l *Loader) Shutdown(ctx context.Context) (err error) { defer func() { l.timeoutQueue.Close() @@ -264,58 +264,59 @@ func (l *Loader) DoRequest(req types.Req) { l.sender.Send(req) } -type connConfig struct { - InitialWindowSize uint32 - MaxConcurrentStreams uint32 -} - -func setupHTTP2(r io.Reader, w io.Writer) (connConfig, error) { - var conf connConfig - +func setupHTTP2(r io.Reader, w io.Writer, conf *loaderConfig) error { // we should not check n, because Write must return error on n < len(clientPreface) _, err := w.Write(clientPreface) if err != nil { - return conf, fmt.Errorf("write http2 preface: %w", err) + return fmt.Errorf("write http2 preface: %w", err) } framer := http2.NewFramer(w, r) // handle settings - { - frame, err := framer.ReadFrame() - if err != nil { - return conf, fmt.Errorf("read settings frame: %w", err) - } + frame, err := framer.ReadFrame() + if err != nil { + return fmt.Errorf("read settings frame: %w", err) + } - sf, ok := frame.(*http2.SettingsFrame) - if !ok { - return conf, errors.New("protocol error: first frame from server is not settings") - } - if val, ok := sf.Value(http2.SettingInitialWindowSize); ok { - conf.InitialWindowSize = val - } - if val, ok := sf.Value(http2.SettingMaxConcurrentStreams); ok { - conf.MaxConcurrentStreams = val - } + sf, ok := frame.(*http2.SettingsFrame) + if !ok { + return errors.New("protocol error: first frame from server is not settings") + } - err = framer.WriteSettings(http2.Setting{ - ID: http2.SettingInitialWindowSize, - Val: math.MaxUint32 & 0x7fffffff, // mask off high reserved bit - }) - if err != nil { - return conf, fmt.Errorf("write settings frame: %w", err) + for i := 0; i < sf.NumSettings(); i++ { + s := sf.Setting(i) + switch s.ID { + case http2.SettingInitialWindowSize: + conf.initialWindowSize = s.Val + case http2.SettingMaxConcurrentStreams: + conf.maxConcurrentStreams = s.Val + case http2.SettingHeaderTableSize: + conf.maxDymanicTableSize = s.Val + case http2.SettingMaxFrameSize: + conf.maxFrameSize = s.Val + default: + return fmt.Errorf("got not supported setting: %s (%d)", s.ID.String(), s.Val) } + } - err = framer.WriteSettingsAck() - if err != nil { - return conf, fmt.Errorf("write settings ack: %w", err) - } + err = framer.WriteSettings(http2.Setting{ + ID: http2.SettingInitialWindowSize, + Val: math.MaxUint32 & 0x7fffffff, // mask off high reserved bit + }) + if err != nil { + return fmt.Errorf("write settings frame: %w", err) + } - err = framer.WriteWindowUpdate(0, math.MaxUint32&0x7fffffff) - if err != nil { - return conf, fmt.Errorf("write window update frame: %w", err) - } + err = framer.WriteSettingsAck() + if err != nil { + return fmt.Errorf("write settings ack: %w", err) } - return conf, nil + err = framer.WriteWindowUpdate(0, math.MaxUint32&0x7fffffff) + if err != nil { + return fmt.Errorf("write window update frame: %w", err) + } + + return nil } diff --git a/loader/reciever/processor.go b/loader/reciever/processor.go index e3c5002..0f12598 100644 --- a/loader/reciever/processor.go +++ b/loader/reciever/processor.go @@ -2,6 +2,7 @@ package reciever import ( "bytes" + "errors" "fmt" "github.com/ozontech/framer/frameheader" @@ -10,6 +11,8 @@ import ( "golang.org/x/net/http2/hpack" ) +var ErrFrameTypeNotSupported = errors.New("frame type not supported") + type FrameTypeProcessor interface { Process(header frameheader.FrameHeader, payload []byte, incomplete bool) error } @@ -30,9 +33,12 @@ func NewDefaultProcessor( ) *Processor { headersFrameProcessor := newHeadersFrameProcessor(streams) return NewProcessor([]FrameTypeProcessor{ - http2.FrameData: newDataFrameProcessor(priorityFramesChan, streams), - http2.FrameHeaders: headersFrameProcessor, - http2.FrameRSTStream: newRSTStreamFrameProcessor(streams), + http2.FrameData: newDataFrameProcessor(priorityFramesChan, streams), + http2.FrameHeaders: headersFrameProcessor, + // http2.FramePriority not supported + http2.FrameRSTStream: newRSTStreamFrameProcessor(streams), + http2.FrameSettings: settingsProcessor{}, + // http2.FramePushPromise not supported http2.FramePing: newPingFrameProcessor(priorityFramesChan), http2.FrameGoAway: newGoAwayFrameProcessor(), http2.FrameWindowUpdate: newWindowUpdateFrameProcessor(streams, fcConn), @@ -70,11 +76,13 @@ func (p *Processor) process(buf []byte) error { // } sp := p.subprocessors[header.Type()] - if sp != nil { - err = sp.Process(header, b, status == StatusPayloadIncomplete) - if err != nil { - return err - } + if sp == nil { + return fmt.Errorf("%w: %s", ErrFrameTypeNotSupported, header.Type().String()) + } + + err = sp.Process(header, b, status == StatusPayloadIncomplete) + if err != nil { + return err } if status == StatusFrameDone { @@ -175,12 +183,6 @@ func newHeadersFrameProcessor(streams types.StreamStore) *headersFrameProcessor return p } -// func printAllocs() { -// var m runtime.MemStats -// runtime.ReadMemStats(&m) -// println(m.Mallocs, m.Frees) -// } - func (p *headersFrameProcessor) OnHeader(f hpack.HeaderField) { p.currentStream.OnHeader(f.Name, f.Value) } @@ -314,3 +316,20 @@ func (p *goAwayFrameProcessor) Process(_ frameheader.FrameHeader, payload []byte p.debugData = p.debugData[:0] return err } + +type settingsProcessor struct{} + +func (p settingsProcessor) Process( + header frameheader.FrameHeader, + payload []byte, + incomplete bool, +) error { + if incomplete { + return nil + } + + if !header.Flags().Has(http2.FlagSettingsAck) { + return errors.New("update settings in runtime not supported") + } + return nil +} diff --git a/loader/sender/sender.go b/loader/sender/sender.go index 10f73e7..1a30be9 100644 --- a/loader/sender/sender.go +++ b/loader/sender/sender.go @@ -23,8 +23,9 @@ type frame struct { } type Sender struct { - streamPool *streamsPool.StreamsPool - streamStore types.StreamStore + maxFrameSize int + streamPool *streamsPool.StreamsPool + streamStore types.StreamStore fcConn types.FlowControl conn io.Writer @@ -43,13 +44,16 @@ func NewSender( priorityChunkChan chan []byte, streamPool *streamsPool.StreamsPool, streamStore types.StreamStore, + hpackEncWrapper *hpackwrapper.Wrapper, + maxFrameSize int, ) *Sender { return &Sender{ + maxFrameSize, streamPool, streamStore, fcConn, conn, - hpackwrapper.NewWrapper(), + hpackEncWrapper, 1, make(chan writeCmd), @@ -212,7 +216,7 @@ func (s *Sender) Send(a types.Req) { func (s *Sender) send(a types.Req) { // n.Add(1) s.streamID += 2 - frames := a.SetUp(s.streamID, s.hpackEncWrapper) + frames := a.SetUp(s.maxFrameSize, s.streamID, s.hpackEncWrapper) stream := s.streamPool.Acquire(s.streamID, a.Tag()) stream.SetSize(a.Size()) diff --git a/loader/streams/pool/pool.go b/loader/streams/pool/pool.go index 0ef2bc9..9d3b7d0 100644 --- a/loader/streams/pool/pool.go +++ b/loader/streams/pool/pool.go @@ -4,12 +4,11 @@ import ( "math" "sync" + "github.com/ozontech/framer/consts" "github.com/ozontech/framer/loader/flowcontrol" "github.com/ozontech/framer/loader/types" ) -const defaultInitialWindowSize = 65535 - type StreamsPool struct { reporter types.LoaderReporter @@ -21,21 +20,18 @@ type StreamsPool struct { initialWindowSize uint32 } -func NewStreamsPool( - reporter types.LoaderReporter, - initSize uint32, - limit uint32, // limit = 0 интерпретируется как неограниченное количество -) *StreamsPool { - if limit == 0 { - limit = math.MaxUint32 - } - return &StreamsPool{ +func NewStreamsPool(reporter types.LoaderReporter, opts ...Opt) *StreamsPool { + p := &StreamsPool{ reporter: reporter, cond: sync.NewCond(&sync.Mutex{}), - pool: make([]*streamImpl, 0, initSize), - maxConcurrentStreams: limit, - initialWindowSize: defaultInitialWindowSize, + pool: make([]*streamImpl, 0, 1024), + maxConcurrentStreams: math.MaxUint32, + initialWindowSize: consts.DefaultInitialWindowSize, + } + for _, o := range opts { + o.apply(p) } + return p } func (p *StreamsPool) Acquire(streamID uint32, tag string) types.Stream { @@ -79,18 +75,6 @@ func (p *StreamsPool) InUse() uint32 { return p.inUse } -func (p *StreamsPool) SetInitialWindowSize(size uint32) { - p.cond.L.Lock() - defer p.cond.L.Unlock() - p.initialWindowSize = size -} - -func (p *StreamsPool) SetLimit(limit uint32) { - p.cond.L.Lock() - defer p.cond.L.Unlock() - p.maxConcurrentStreams = limit -} - func (p *StreamsPool) WaitAllReleased() <-chan struct{} { ch := make(chan struct{}) @@ -121,3 +105,19 @@ func (s *streamImpl) End() { s.StreamState.End() s.pool.release(s) } + +type Opt interface { + apply(*StreamsPool) +} + +type WithMaxConcurrentStreams uint32 + +func (s WithMaxConcurrentStreams) apply(p *StreamsPool) { + p.maxConcurrentStreams = uint32(s) +} + +type WithInitialWindowSize uint32 + +func (s WithInitialWindowSize) apply(p *StreamsPool) { + p.initialWindowSize = uint32(s) +} diff --git a/loader/streams/pool/pool_benchmark_test.go b/loader/streams/pool/pool_benchmark_test.go index 7530428..14ae755 100644 --- a/loader/streams/pool/pool_benchmark_test.go +++ b/loader/streams/pool/pool_benchmark_test.go @@ -8,7 +8,7 @@ import ( ) func BenchmarkStreamsPool(b *testing.B) { - p := pool.NewStreamsPool(noop.New(), 1024, 0) + p := pool.NewStreamsPool(noop.New()) for i := 0; i < b.N; i++ { s := p.Acquire(0, "") s.End() diff --git a/loader/streams/store/store.go b/loader/streams/store/store.go index dd4ed26..7027526 100644 --- a/loader/streams/store/store.go +++ b/loader/streams/store/store.go @@ -95,7 +95,7 @@ type ShardedStreamsMap struct { func NewShardedStreamsMap(size uint32, build func() types.StreamStore) *ShardedStreamsMap { shards := make([]types.StreamStore, size*2) - for i := 1; i < len(shards); i++ { + for i := 1; i < len(shards); i += 2 { shards[i] = build() } return &ShardedStreamsMap{shards, size - 1} @@ -106,7 +106,8 @@ func (s *ShardedStreamsMap) shard(id uint32) types.StreamStore { } func (s *ShardedStreamsMap) Each(fn func(types.Stream)) { - for _, shard := range s.shards { + for i := 1; i < len(s.shards); i += 2 { + shard := s.shards[i] shard.Each(fn) } } diff --git a/loader/types/req.go b/loader/types/req.go index 3b28d98..5083e2c 100644 --- a/loader/types/req.go +++ b/loader/types/req.go @@ -8,7 +8,7 @@ type HPackFieldWriter interface { } type Req interface { - SetUp(streamID uint32, fieldWriter HPackFieldWriter) []Frame + SetUp(maxFramePayloadLen int, streamID uint32, fieldWriter HPackFieldWriter) []Frame Tag() string Size() int Releaser diff --git a/report/phout/phout.go b/report/phout/phout.go index fd445d3..0dadd48 100644 --- a/report/phout/phout.go +++ b/report/phout/phout.go @@ -141,7 +141,7 @@ func (s *streamState) result() []byte { s.reportLine = strconv.AppendInt(s.reportLine, rtt, 10) s.reportLine = append(s.reportLine, tabChar) - // keyConnectMicro // TODO (skipor): set all for HTTP using httptrace and helper structs + // keyConnectMicro s.reportLine = append(s.reportLine, '0', tabChar) // keySendMicro s.reportLine = append(s.reportLine, '0', tabChar) @@ -149,7 +149,7 @@ func (s *streamState) result() []byte { s.reportLine = append(s.reportLine, '0', tabChar) // keyReceiveMicro s.reportLine = append(s.reportLine, '0', tabChar) - // keyIntervalEventMicro // TODO: understand WTF is that mean and set it right. + // keyIntervalEventMicro s.reportLine = append(s.reportLine, '0', tabChar) // keyRequestBytes s.reportLine = strconv.AppendInt(s.reportLine, int64(s.reqSize), 10) diff --git a/utils/hpack_wrapper/wrapper.go b/utils/hpack_wrapper/wrapper.go index f0f4da6..f4e2156 100644 --- a/utils/hpack_wrapper/wrapper.go +++ b/utils/hpack_wrapper/wrapper.go @@ -11,9 +11,12 @@ type Wrapper struct { enc *hpack.Encoder } -func NewWrapper() *Wrapper { +func NewWrapper(opts ...Opt) *Wrapper { wrapper := &Wrapper{} wrapper.enc = hpack.NewEncoder(wrapper) + for _, o := range opts { + o.apply(wrapper) + } return wrapper } @@ -24,3 +27,13 @@ func (ww *Wrapper) WriteField(k, v string) error { Value: v, }) } + +type Opt interface { + apply(*Wrapper) +} + +type WithMaxDynamicTableSize uint32 + +func (s WithMaxDynamicTableSize) apply(w *Wrapper) { + w.enc.SetMaxDynamicTableSize(uint32(s)) +}