From 5518398d2e49d43df4c96b1126fca05fedd7ee59 Mon Sep 17 00:00:00 2001 From: msherif1234 Date: Thu, 31 Aug 2023 09:29:19 -0400 Subject: [PATCH] Revert "change aggregation flow map to hashmap instead perCPU hashmap (#118)" This reverts commit b6e2b87386e7d3242fa079768539c5e854ed6789. --- bpf/maps_definition.h | 2 +- bpf/utils.h | 3 +- docs/architecture.md | 2 +- .../server/flowlogs-dump-collector.go | 10 ++- pkg/agent/agent.go | 2 +- pkg/agent/agent_test.go | 57 +++++++++++++++--- pkg/ebpf/bpf_bpfeb.o | Bin 92800 -> 93168 bytes pkg/ebpf/bpf_bpfel.o | Bin 94768 -> 95168 bytes pkg/ebpf/tracer.go | 23 ++++--- pkg/flow/account.go | 4 +- pkg/flow/account_test.go | 20 +++--- pkg/flow/record.go | 32 ++++++++++ pkg/flow/tracer_map.go | 24 +++++++- pkg/flow/tracer_map_test.go | 21 +++++-- pkg/test/tracer_fake.go | 10 +-- 15 files changed, 164 insertions(+), 46 deletions(-) diff --git a/bpf/maps_definition.h b/bpf/maps_definition.h index ca07543aa..8b22494c7 100644 --- a/bpf/maps_definition.h +++ b/bpf/maps_definition.h @@ -11,7 +11,7 @@ struct { // Key: the flow identifier. Value: the flow metrics for that identifier. struct { - __uint(type, BPF_MAP_TYPE_HASH); + __uint(type, BPF_MAP_TYPE_PERCPU_HASH); __type(key, flow_id); __type(value, flow_metrics); __uint(max_entries, 1 << 24); diff --git a/bpf/utils.h b/bpf/utils.h index ce05dd13c..f0932e131 100644 --- a/bpf/utils.h +++ b/bpf/utils.h @@ -253,12 +253,13 @@ static inline long pkt_drop_lookup_and_update_flow(struct sk_buff *skb, flow_id enum skb_drop_reason reason) { flow_metrics *aggregate_flow = bpf_map_lookup_elem(&aggregated_flows, id); if (aggregate_flow != NULL) { + aggregate_flow->end_mono_time_ts = bpf_ktime_get_ns(); aggregate_flow->pkt_drops.packets += 1; aggregate_flow->pkt_drops.bytes += skb->len; aggregate_flow->pkt_drops.latest_state = state; aggregate_flow->pkt_drops.latest_flags = flags; aggregate_flow->pkt_drops.latest_drop_cause = reason; - long ret = bpf_map_update_elem(&aggregated_flows, id, aggregate_flow, BPF_ANY); + long ret = bpf_map_update_elem(&aggregated_flows, id, aggregate_flow, BPF_EXIST); if (trace_messages && ret != 0) { bpf_printk("error packet drop updating flow %d\n", ret); } diff --git a/docs/architecture.md b/docs/architecture.md index 56fa49326..f659b92a2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -11,7 +11,7 @@ flowchart TD E(ebpf.FlowFetcher) --> |"pushes via
RingBuffer"| RB(flow.RingBufTracer) style E fill:#990 - E --> |"polls
HashMap"| M(flow.MapTracer) + E --> |"polls
PerCPUHashMap"| M(flow.MapTracer) RB --> |chan *flow.Record| ACC(flow.Accounter) RB -.-> |flushes| M ACC --> |"chan []*flow.Record"| DD(flow.Deduper) diff --git a/examples/flowlogs-dump/server/flowlogs-dump-collector.go b/examples/flowlogs-dump/server/flowlogs-dump-collector.go index bed9028f3..8eb925d34 100644 --- a/examples/flowlogs-dump/server/flowlogs-dump-collector.go +++ b/examples/flowlogs-dump/server/flowlogs-dump-collector.go @@ -72,7 +72,7 @@ func main() { for records := range receivedRecords { for _, record := range records.Entries { if record.EthProtocol == ipv6 { - log.Printf("%s: %v %s IP %s:%d > %s:%d: protocol:%s type: %d code: %d dir:%d bytes:%d packets:%d flags:%d ends: %v dnsId: %d dnsFlags: 0x%04x dnsLatency(ms): %v rtt(ns) %v\n", + log.Printf("%s: %v %s IP %s:%d > %s:%d: protocol:%s type: %d code: %d dir:%d bytes:%d packets:%d flags:%d ends: %v dnsId: %d dnsFlags: 0x%04x dnsLatency(ms): %v rtt(ns) %v DropPkts: %d DropBytes: %d DropCause %d\n", ipProto[record.EthProtocol], record.TimeFlowStart.AsTime().Local().Format("15:04:05.000000"), record.Interface, @@ -92,9 +92,12 @@ func main() { record.GetDnsFlags(), record.DnsLatency.AsDuration().Milliseconds(), record.TimeFlowRtt.AsDuration().Nanoseconds(), + record.GetPktDropPackets(), + record.GetPktDropBytes(), + record.GetPktDropLatestDropCause(), ) } else { - log.Printf("%s: %v %s IP %s:%d > %s:%d: protocol:%s type: %d code: %d dir:%d bytes:%d packets:%d flags:%d ends: %v dnsId: %d dnsFlags: 0x%04x dnsLatency(ms): %v rtt(ns) %v\n", + log.Printf("%s: %v %s IP %s:%d > %s:%d: protocol:%s type: %d code: %d dir:%d bytes:%d packets:%d flags:%d ends: %v dnsId: %d dnsFlags: 0x%04x dnsLatency(ms): %v rtt(ns) %v DropPkts: %d DropBytes: %d DropCause %d\n", ipProto[record.EthProtocol], record.TimeFlowStart.AsTime().Local().Format("15:04:05.000000"), record.Interface, @@ -114,6 +117,9 @@ func main() { record.GetDnsFlags(), record.DnsLatency.AsDuration().Milliseconds(), record.TimeFlowRtt.AsDuration().Nanoseconds(), + record.GetPktDropPackets(), + record.GetPktDropBytes(), + record.GetPktDropLatestDropCause(), ) } } diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 326de2b39..57bf6c5d9 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -78,7 +78,7 @@ type ebpfFlowFetcher interface { io.Closer Register(iface ifaces.Interface) error - LookupAndDeleteMap() map[ebpf.BpfFlowId]*ebpf.BpfFlowMetrics + LookupAndDeleteMap() map[ebpf.BpfFlowId][]*ebpf.BpfFlowMetrics DeleteMapsStaleEntries(timeOut time.Duration) ReadRingBuf() (ringbuf.Record, error) } diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index 1cf3a1ff5..c4a418e39 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -49,6 +49,11 @@ var ( DstPort: 456, IfIndex: 3, } + key1Dupe = ebpf.BpfFlowId{ + SrcPort: 123, + DstPort: 456, + IfIndex: 4, + } key2 = ebpf.BpfFlowId{ SrcPort: 333, @@ -76,11 +81,21 @@ func TestFlowsAgent_Deduplication(t *testing.T) { receivedKeys[f.Id] = struct{}{} switch f.Id { case key1: - assert.EqualValues(t, 3, f.Metrics.Packets) - assert.EqualValues(t, 44, f.Metrics.Bytes) + assert.EqualValues(t, 4, f.Metrics.Packets) + assert.EqualValues(t, 66, f.Metrics.Bytes) assert.False(t, f.Duplicate) assert.Equal(t, "foo", f.Interface) key1Flows = append(key1Flows, f) + case key1Dupe: + assert.EqualValues(t, 4, f.Metrics.Packets) + assert.EqualValues(t, 66, f.Metrics.Bytes) + assert.False(t, f.Duplicate) + assert.Equal(t, "bar", f.Interface) + key1Flows = append(key1Flows, f) + case key2: + assert.EqualValues(t, 7, f.Metrics.Packets) + assert.EqualValues(t, 33, f.Metrics.Bytes) + assert.False(t, f.Duplicate) } } assert.Lenf(t, key1Flows, 1, "only one flow should have been forwarded: %#v", key1Flows) @@ -104,12 +119,23 @@ func TestFlowsAgent_DeduplicationJustMark(t *testing.T) { receivedKeys[f.Id] = struct{}{} switch f.Id { case key1: - assert.EqualValues(t, 3, f.Metrics.Packets) - assert.EqualValues(t, 44, f.Metrics.Bytes) + assert.EqualValues(t, 4, f.Metrics.Packets) + assert.EqualValues(t, 66, f.Metrics.Bytes) if f.Duplicate { duplicates++ } assert.Equal(t, "foo", f.Interface) + case key1Dupe: + assert.EqualValues(t, 4, f.Metrics.Packets) + assert.EqualValues(t, 66, f.Metrics.Bytes) + if f.Duplicate { + duplicates++ + } + assert.Equal(t, "bar", f.Interface) + case key2: + assert.EqualValues(t, 7, f.Metrics.Packets) + assert.EqualValues(t, 33, f.Metrics.Bytes) + assert.False(t, f.Duplicate) } } assert.Equalf(t, 0, duplicates, "exported flows should have only one duplicate: %#v", exported) @@ -132,11 +158,21 @@ func TestFlowsAgent_Deduplication_None(t *testing.T) { receivedKeys[f.Id] = struct{}{} switch f.Id { case key1: - assert.EqualValues(t, 3, f.Metrics.Packets) - assert.EqualValues(t, 44, f.Metrics.Bytes) + assert.EqualValues(t, 4, f.Metrics.Packets) + assert.EqualValues(t, 66, f.Metrics.Bytes) assert.False(t, f.Duplicate) assert.Equal(t, "foo", f.Interface) key1Flows = append(key1Flows, f) + case key1Dupe: + assert.EqualValues(t, 4, f.Metrics.Packets) + assert.EqualValues(t, 66, f.Metrics.Bytes) + assert.False(t, f.Duplicate) + assert.Equal(t, "bar", f.Interface) + key1Flows = append(key1Flows, f) + case key2: + assert.EqualValues(t, 7, f.Metrics.Packets) + assert.EqualValues(t, 33, f.Metrics.Bytes) + assert.False(t, f.Duplicate) } } assert.Lenf(t, key1Flows, 1, "both key1 flows should have been forwarded: %#v", key1Flows) @@ -183,10 +219,13 @@ func testAgent(t *testing.T, cfg *Config) *test.ExporterFake { }) now := uint64(monotime.Now()) - key1Metrics := ebpf.BpfFlowMetrics{Packets: 3, Bytes: 44, StartMonoTimeTs: now + 1000, EndMonoTimeTs: now + 1_000_000_000} + key1Metrics := []*ebpf.BpfFlowMetrics{ + {Packets: 3, Bytes: 44, StartMonoTimeTs: now + 1000, EndMonoTimeTs: now + 1_000_000_000}, + {Packets: 1, Bytes: 22, StartMonoTimeTs: now, EndMonoTimeTs: now + 3000}, + } - ebpfTracer.AppendLookupResults(map[ebpf.BpfFlowId]*ebpf.BpfFlowMetrics{ - key1: &key1Metrics, + ebpfTracer.AppendLookupResults(map[ebpf.BpfFlowId][]*ebpf.BpfFlowMetrics{ + key1: key1Metrics, }) return export } diff --git a/pkg/ebpf/bpf_bpfeb.o b/pkg/ebpf/bpf_bpfeb.o index b7d0979401747cc2a100c166a54cac0156314c5b..5889531064dbf9e3d0d2b42bdbf077953efb30e6 100644 GIT binary patch delta 19901 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 g?(U_|Z;<#?mUVOwUyYs1(7Oh`%Uz>% delta 19725 zcmaKz3w&KwmH+p-N!ptX!Nz|Nb+U`A=^x^oKVs zOg(w(N5b6y+fqlr1g)r{)N`>X#KXH|G;$4;SHY)s0SPcHgXS~wV3D)sX^ zWuXr36y@;a=v&uv_~MA2q8xq$d>hFL{5-=tINDTI-Hg}a+pL@&n!WWEL&2`=be*DVkJ@44il|2#vP{*b@tZ$EIk-SoAZR`RPhKb0{n0h~R z<+SRU1IlA9M5aa^Yk^<0FZE%p*fNW|3@C}!-%!ig)6nlxOMEiWzo!m*U9G)Jti!!B zX_2kk!%}~2-`>=p>w)R~i#_bs9+u_wz;cP)kZ;T+_zCE{sdf8B?t@;*m4x5x=(*BK zz?=NG>$iL7`~BBnbKwQXFa%Rl&uxz}1;fx1N@y8Z zi7f3ap_9-uIHA+f_+;YH8R(=j<|^o6Xz}H0zx`uXsr68ll3m=ALP582+iXcw@?q+& zja@@ixf!!oZUqi9*CihVo+dnFrkCvJzRA+uTm$bs zzqqq5rQDJ)Avm2YNz-SQPE!ZwlklI*caTZ5u5A%Hw74d8k}J^P4F4tcWgUt;GL$n! zF2#8uLncFoeEtEl?S7`S`pj!YEhKpdc&6|$^)9z_ENM#qfNd#f8B-cj`fTV)O7HON zZ&=JaYQ3TQ@>Zu>I+Ggrq?F2>W&<-myi%D{<(JCPvNNZyp9jyBxZw#YWrLr*x61$d z4O2^a(n%?0)tQg@ogc4ah1J~ngm?b2rcX@vyo>yo_czYljWELwXs0gY8sw64G1oBs zebkruiJPiQE)6Z-5BoFD4 zO38m6e&Xi3k7d8xBoLLZ_g=?ca2B-3*3Q+z#gjINyOz zLk~h9^^e|MHxE0dNjN9r2#s{AtPuPz%|MG+-}A?Au1kHNr6Zi3aNw2=L(95)*t3~+ zs;nWQJ@}tcnvqZajM90~hlS2Jv3zI=-@`12DD;=1FM*Che?{p8^ovS288d6@YXDq~ zB@z4_`WEOk^n1`x`X_FwOEJ`Pq|@fZ5xN~(auvfZPeUi6>9V{ZdWX_SpxdG8zWgZk zP0&7c2KqK=hEP5XeGl{xnMA5-4|01IItl$aG-0hsb2^#!4B$*a2Eo^$F zTp5FY1-b(|0sRxDlhAKNi%vWAZ-n+tC3@2*D@}Z-&lY-;sk|Dx3HnFqr=jJsw35?N zWd^X_0yC9r`uW^`OpKDy?a;47XP{-W{*8{PrhiiDB=nt1r=cHEIs+}Ee1n>5`ag2} zcj_e9x1q5!JRL+>r#5!L1SisySb!X z=PG-@fqPQ%e(HAG%%LWlCJnuW`V(CKk3Y5b@&gbVn72@Kc}Zw&n_HpN(1)OL)XPB2 zJVHm3&|ic`FOs2_@>wzmRh5hur}C22Qa#fw?KIV^nx~`dN=wlsm79?ewbT8KaLNqRSAlH0LrXV;i&~I_O-I6B$F7+Kk+sE1S=@uq!bLf_Jy$+u%t*Nx`B(v2B#T+aI`Xb_uCQ%8U+f_xkVOR(EE=xm`FS zL+W!=I;2)*-{r20dQhy7Tg!8;=GVL>hsMElfztLKvE>i}7 zhRlX+V#|9ctSTma&UX`np)4`8#`e=ikpe+D1l|{@j^&<{ot}SPmQ1&ehbMybe~S zIvl6?5o)oPP}fB>30+SubehX55dDjURu_rH%v*tv3lI8sZc1nM?HI&3ncq-42|Zxxl*wyn3nq*)&!Ut@@I7d9WnKoFCyx0FV-@~Spug%4Lo?rk z{u=2-_`igv!@?xAq=A3t-~!)11RjR&hW`Or=pG!rKxqbE9Z4zZ2NXeHf)b;sE{A>| zIs-iu8qbTVs_VEBp}5d%xcw`18hTw6K>4EF2(RoASTkgYNW>`BTU6&M=w0YUV5XF3 z{=w>)>fO|`r4oX5j%B*80CosZ%l{1sH6%JBXOvQ^JGnj0W$nvEXc0NOGB?$q%r_sk zpaF0Yrp!I1S~8Q&TN+xjiKPCFhEC=i7%VO$YasWf;#f$2)L(Vyhc?RUpj0zCM(8v& zu`>);tW|$Y=@|5@N++QIO=$nfo!d8_K(n8BXgTIdIW9K>Bx9ph&1`BJG^J)Pw>vCp zYUWX={3YGrNo{tV+LhE2hO{xY&H2tk3-np=9_6Cc@&J?7ETgV%rzRRQU9}zB`@cY6 z=i*>IY*@Bv;3aFQmhWx$D4hhq4fE^n46Xt2)1Rx7Gp5&$Y7b)E@zT(ee1-0Y78iv^ zUi+BReb6kQVAIt~pv6uGK6}H`!_Z%+dkGBdv|0kC;jfi2OOVp!$?12^@G^eWUDc@< z;81FR!tF(_B$pj7l}u7Vza(@C-AS#*ww125JV8z8t_jm4F(Ej$!_*tND7F8|O+2C0 zevohcZ{JnIr>vp7R(jv{i|<}gaz2Ojr5fmS{N&wLsUL_gW~FDUnT6D^2oKF2s>uQg zJhgi~@cK33%igB>Ida!&V(HBa9t<()D-aEd9QtZ*#8o0O>qa@+T>@z53L5VS(XHdO zkr3vFXnRQhQpX|c7s_jXk76$2g41#HsQI(9t(Na%^H=T`#pzd z&v?hRNNlDSy2+Tt+0?uJfkWrdkeovV>_QbfVa!|^@NR$AJ#{6LU+oa7NBlka)Lr-= zP?Wi!;Pxv^a${Pgd%CroywmaR>JvI4_%G@BZ`{*pzmi~~?q!sB`|*40y#Mq!+}n8J ze@ScYQC?|mQELh9G3B_nN1@Y#|B{ZMxwp!G&_HjVj78%x?-pqFd7fD6^iSS<$Auz= zJGX;cVv?qntU2NL7_*?AzW684iFqGWK2_NA0%@z37hDJZTmRMj)~Cc;jIX0tQNPcX z;F8oP*0>t>QU8zdps(j97AOtk^Y1Ka8pP-Sb#!*rw9U>bqK1Rqbc|u0iiUesAW9Di z?f3V%Z#{tU0PHbEOeBIp`0w{L+9kpO7Ih){gWq(2)xHm)D2wjncG8k&(Sy|L;-b$( z|55o*QvXS5Noc1W?J}6sGGqydCpeUDcKb)~uPzafC~koM(EU%OvcuFZLA%D=F+fx$ zDgL={y>@)aO&a|#P)ic%NB;@xHC!34V1HwJ3gZx$8jmsS5T5NW;?j*rXU+HdQU8GV zpr7^^ES-cxnyGH3MHNO9$Yt2~%2RYmD6q zN4!e#hg=8VRFM)3iboi-AKzFet$$tWGLSc2q#{&BiJAKm(LT^yAGW<2Zw74iTE93E z2km5J;kE8DQFagDOY1*sn_13=Ya{*FAbd`Ri9jf8M$2kyLMvD63#vC4Y#nyP_vVSN zb6$QmZ2hk4)#vCng!HP>OTDIw3vv_}hZIXt{Hf|K%F$akuIDwP_YPi)Pig#no7&BS zX_ci-RJ}oTK2Tdr(5)>a@e8iF%pqbO7WO`8zDxwYl<4TI)6rw zsO*|+_3DiGAyJYt9`dX~^2)BW{5N{5R|j3NQg&UYU95NQ{=Y5he%B}bC4J*9=fJr~ zTW=qT|%~xyeRc~R4UJ|04LiEaUde=MH zVajZwy)ClkSYU2%$cm9kmm)NiYzy?RU(=YyA;psvECpj8E6Wz<%wu(DX;!8!EDDcs z*g~hCr0E0~Ha!xOY2m7uwXqK3S}u%xauhwt#SD0IoO+0hD0@lj?{i_$+e|GEq-ml; zi5d(PO2(X=zHE_wlr=!YDJ5`OjKNX{YwVqGcE!;gKl6tXO>5n0M(Q)~xs$4thK)V{M9GF=kBkanR}q zat1h|Jq~8&DKXHej9;;8=79S{3L+0Y4jdSrD3>J`?w~x8BTweY)d8BhBvsz;6dKEn zH%ljdzfZQ>4MP zm=PAWs{R}NDv16R#VeSkR|R+G$OjxYT8g8B*>FLF;vuK7Vh_uR&Wm+~D<0=d6dS2{ zMCD)SQz^$j$z3v{6@qcTSVz3#x8%wi_b3(lN;c^Wf^COHKpieg(mo{y63*k26V6~( zA>;7M7ULaaib~YMELI0s-orO4PGcqNpv6p?R}M_^0+f-Lrloa6CsSOoHo*75b|<$CyaOEco~nAb|NHy&L9I^b0tSFrnKUO!`9(t%zwq8 zD%{C=Ohz=Um{s3Z1RhcRGh+_%!c;MuBOi14LlI*R&*w*MMj{tiTd)xxs@}R zo;v2g5-+n15=&)5@lD30#Xy7NC-|8}I%v+3Cv)U24zJMxRJN*|ZPzF5(_uODU)i0b z(C=__F+Tzmk;wa%`OAnMgebBh3zPh#2pt+29t%jmGN|dAs8CNY&B- z5tH^C4w0G#cjw556sJ@nPdnW361kZMd0kFnW1Lfg-~q)~lRB4z2Nko*AFBbY!TPTm zb42vVRQ_EaPAuAd6T&<_^P>)H{9yzhGr|`bVSd^vY+!@~j4=O*!>8pL?+rRA$S6LW z!xeavI(Qn7>vwT*QjUX@avYrG96aA_R1YT`XV79ioSe`OzGoska3I?1a4^ECI=qRD zGsdEgb~*W`%HBPbyhb_6TDJCU)i4_!QMGe8OiHuS5JpZ2NN|!_mKOv$iDl(Hv*h4q|grg{#gIj5PKG$_7 zQq0U3bt`_1%_jDG1k-;~7;L7ZzL0`6=vVw*em5%vGXgPqdXe#7m6P632#W*5AuJt? zgfJ%b$5&*G(rMO!*5^UsL4?ByFSb5^Di*urO^b|O-yBi@oR-X0prYqixE z%m~X`6!ZGgA@VMVxAIUOWDUt{wCHc$%M*c9Lzxa>>q8tj+^Y z5bIRTR(y;R#Re7ky9_R8S=;^%W1d1@p?F)|J#;Grd;u9Ahbz^gZGtQ1VAbjjY&&4g z2~lVdVMbKhr5%XJI3x!n>2Mnndt(K-&*AMnbcb0Jl{hBy?S%Vn1~gUeTg((s)d5>v z!u(Iw4!57jmkUBZ4bQEC+xT(;JuRvDmz)SlJktULf)ikkSPOgleUbM#{g%skMzU?E z^(nrcTI{KVEq}{dSpKuZ^nmfEG40?g#YRijpfj*pw_(+g!&$kmna#Qlt45vtblLYr zplVFpvsPrA!R*_3tHVKu@ivFgoMXI4V#w1DH}d79 zp8>@CLYUs-M^*n*e4eiapUC2r_cm!Lp2;#C9&k9wbThQU>6!^=sD2=y*`)fKH>%Zv zz~Snky+kCz@4!L(+u7%m3aholAQM&(I)hmQ!wzrODX1RFk&mfdr+}R`9zj4&B7})( zO^dc?{n=8}stTG3YdRbbGF?rlV$F0lT_GIDU^;}!bTvJSHPhAfDb_?+bJXEfkm+hP z0$ViG)eLI`&2%-RiZ#>KCL9hjU2VH!&2+UofFRS=cB`CiD~V42oP{-(GB&fZ?P}G- zAk)?MYlD+6)71`!FpH>mD1^xywId-c@-c^lOh>l0QxYV)Suw%H-!j-1v*N0tnQm5> zVjPjP>MZT>j5l1So2@v=bcq08;4)o89nwr^5@XI@km%+_!SejK3_PBb&<#W%?uaUArehts2Ab*W zY{)pAXr`-ca`H1D;L9WfsA~>kwsoC4Y~_O0VVn8L6ZOsd{0~xPy*9`y9CZd;-gKF9 ze%xXFvSq$@Xfs{HnE6R354Q1q?a*l>Z&SH$#RiHQalwkj5V z3@!{h(DsXBDjzo9swRFaVu02`!Hd8d#bSRx^S@{y#Gu1PBZ}4I#SJ0M<8?7ctwE8q zsuwpqe0DWwI-Yimwf)(=@m;{SmUE!!OJZw7e~GrYxQ6*(l2ika@d;TJx)lG^cxghq zB(3{LK)vQ5&3O*t%j&EZ6fmDd*nAld70dKIxAS@!y!x}SvwNKIJkB!gyZf% zQD)l{LHR3OhnatQkysZ~#$|kl5(OGr1;NPH5z;IcgH0jK0M<2!Fl%I8GK8gli{j7l zu|@2+DozPQ6xu=x(m_WEO9!1HEFE-(u(a=1{50uVI_L>uk@w}`BMzr_ev5ZIX>e2( z1WSVxIe0+v&y8sl{lO3x{h=H@qWF)bZqXkNVbLG6IAwN<0*v)hr_jpiI%u#So`l?D zk;ijzgW`3Z=|sONghjtO2e&BR&7oHGTLq`+Kp4`XEu-!vL(V9anTKj``#0ezF>*K%Wuw^36(Mk)XMwfwM! zQ}T{*0|y=E4}gEcVdPeklK%_h%6Q!}OhJdkb!!xhLm`6& z8HWRd1!Le}bGY~wHDywoTL@2%Ib6>(U&`b@Zpa;G{-X_!5ri}<3UoM+N=me!TWAE) zA@JyU#G=Cv2N5akar!~~LbVq-TsVgOzcqsNZ+}u0Mlc0M$%zK!C&jP>Mg8FSLi$G? z4jd>F2SWoW8g@8vU`jI>Cq!TRH&faiBXD3!A2?)SO25Nd12i_jA9p~0qZAxCPzsdx zOFkzA>s##w*#7c87r`j~du)}UVX)eR)$3`)VD);q2-Kj;~%;bC1Kp;+;GUrh{PdO%{8h4x`QBvGItfqHT^5%zd<5u|~wM z_K-ol+Cv8ALvwgH6eb)FLT*?8C{NLEz^4?dL+W74e)kP~SkZwN9R!QT{=E=~5SEFN zzm#3~qL_h>Dn6;WfjYDnj=-a z{8kZ;M833Vu4Gr${RLO1&IYwVUk-Y zPZYK~95__iDf@qHd>`A)1J^rJz6v^154&uowi%2>UjP-@9d)04e;ghsIqA?FJasYmN^OJ?ShXV-0J;$>gqP= zUE{Jv64)>nUn3#;VZp~A+wFZPGEK%A@@eUh0Ol!YAW027N6>E zdNZ)MYb-*vF-wgK_GV>Ir$9d0zv1-dlR4eH!b{9))hjIvwpoFSv198yQnz+ zK~T;9EGMX-<=*!L-rF1o?x%Phc|Cb8c}vMq7|*y#gDso4hMJ(N8Kqju`x;zob}nUk}tk2ZaA@h#Z}w&|r~_UffNcVxEz%#uZ( z;jtq(2Z6WqH-n?OITr90w3#WqaYDc=Ck7cjDfBZ(0)I>BSUEHFUx8gtZRHq^Z>b35 znOZ@7bGfkRtPDFygOkGm)8Mt00k2#TWX|67Fu$HW7{kS-fx))R>9;H{`6qi-U@!hB zdyMueQ1nj@v9v6X{*ztKsAciEz}_;lFvzdxubffK$gu&ByWlgi3JtPB?yMHCP1gTMb5`@t}sKnF`Nl2gAtWWbzJDzdYZ4D z8t}@~gUmT{X}DCxPfY9OEf&g zy_P+gBhhdqYIvvAV3ObN$+_|jE4{(u#wJ!^)$%r9R)!(HgFUVd@$ZQ22@r8IQqoLz^-a^+Tk-|{oF zZ}^s;_?+Ei&aQXer*~hu;;nI6iIW^hjwk(d*X?yPc8~pnACa3jGRgmR-2{JE<7pFv zk#JebnFOR!(m&9o$Ydg;{`mDdxpKyZVN~W+fA)IsA{|BNSUhsxagO&WEQ6Nd_$yil zHp%fZI*2Eqt!>FH08NU>MO>&Y%<%I!T;T;Ju%@;kmp8%j?^=+lOBy8?-ELtP+>CF8 z|7;CL8aySwd5dnU1~!dmKGebpm}dTIOF89Y;4xS@fCcFQT21@+YV|L*gXO z4KrsIF{g*xbLxSG>bMAfrfSKUlIc=-9rk%@-=I3KJfA$Fnk;gvYR)wy6MYTwakaIe z7pM-iG$(StU)fYX)tDb8QvX*?rAx2UNaJ}I&sl0ah+e7s?`Trya{fd{h5Br=5vo`CbDNK6zN~L9 zEj}l3J5C8cGq}^y3;g^o1%7vPdYL)jGqA2EI<6(iB5V9tn+x1|{&R__|4DOU?s@84 zL2QFxwlT`wdEiSAyXWry?Z%O=Tko&BeujI2zxn!T&4q*xaI9xqUnIqtel2Kukj2oF zF6J=P@)G}|Yo+X6*Pfbte&7xNeBOWK+J~{<{H5}v8(V8Nb{Vk)9RI1Y%ki7W*8Q8t zF7+3*-bQ0TZJkxQp7^WtgMO!{qi;cPAeVF-nv{8^Uv@*ed$GUZhJxIy1C!&N$CK%6 z{@NR&#qC(SD7poUk4~bup*Q$D@ID~%+$Y^wL?_YDp}*}vcSF=|_kVT6jGBiehK-!0 zhdJWtD4UcQ(FycK^w-3h?Kmf(H~6P)DzA|{unWs&SQem@Xi59z4{Jvgc4;w=_Ruf+ zcWo*UdfUL7@DnUA<4vIZ&`j-vbg{2R-?SH&`# z*@{Ij_5k{P^!;eoICs5j7k!=T2>N!_Y3QA*)AOA42UKS=d7ePaWXePTKy?iLQ`OaZ zEaPtgL3eSygT9#p3G`plkNU6PSiZC!OPotMnO&0T2J|WDUq~c6j+X9rp_i-Pg>FDg z^WErc(RcfEZ;EEY&)Zj3TFQBEBJoGQn8_+TI zZ&b(82Q;Pu{hsQ^0>>RWE-bsg!13G(3L6TX5!L8^DoRjb4th2^iI$C`Xoo>qjs6uf zj&4BzMq<$G(fcKahHg+TLvg3-1p4c$ljw)g|2NpSA->NsK-oC?J`SlULH;Jk+vp_u z?>XK<%e?pq{ZnKd{R#Sj#GprVY*IZI%}#ks8!YLXFbn>+@HZ}MJja(f8aO6WfGh1J zIHsXn{lDD2cH#FRIzV` z<7i1=@$bH+ymC8g0Luq>GZSzMsf9`$y+(>|3vDv z+$xsP>G+mbQ{-IE=$qI{nLE|U$)v~7-PrF|+tX;7Yhr&E{omwqj=fs;>*#%G86bwy z|K6<`#U0p8!k!_+`x-Hw{>!&U=iZ~P_q2w4;lsl<1m62JMlu;k*N?BdT{wA8?0J7=TKSa$7X_TaYB#CqBsbCA#!q?F28^R;2a+v|HpXgPt@KzrkiTb@H=*Mk z-&UPO%XvOZ9;f7^=tgt`{UloERu}qd^pof$`dPF*G31iX=VT=&hJIe- zG47_P2}I==XT+YtlK}m!=#ebi=pTvZR{R6{|Da|6aS**%?Cc(7R>?$Y zL;nN2=yvoPIw-mm9Vr#f(%l7Q0~g`wM(3lSLnqNC=%1h+HXY^YO`@47C!ysjkwDKu zlQJI*vf-ub_w?d%wVk0c-}TqtQIPvRwVsdl9)I&4(cECf)Y{Evub{FKpLA7H6Z#2q zS$s4xK=}RN-cdR`=o7S+s0HK+j<2D2lP5Xkq~$6&>K^5TPL{B`%AZ$PJMxjB>e{a|B%N8!W5AS zrzz4a7ck#MV+|(6BmNUxODkoOkn|Vo_*p2Of1v3`@ci1rLUM-x{?=LUD}Loyrc{0& z$Mj=^z|56o^Hnpqokgl&^P9g?;JvQ3Emx~tY)z5y{ck{#Um7cSmP|iQkp}cRl$DW? zp}!3MYjtiw%P__{Hli;gZ{W}rS?xM($HEjj&#LsdG8CS9*b)%=oeATS!1M@`@y}Bx zi@c?p?&KP~iHobx*d_iFtJPKLO>~o#`7Y1AN4J#~{~*y+f2{hO=)6qpwfd|6ME`pCH~u~T6gSg<&Mzqbb8Wc)igN+vRiEs|8qk09 z|KgW>ht)fPCHs`nD=SSQI*a;Z=n1O3&{H)g#xTrQ9Y@DiH^}givMHND%WE%I(cpE- zGL4a8SRLx)oERV}hXQf*g&LGVuS1W)*M)BOf3S10w+YKJL|Q3Qr%V=X=j0t+Zc5BK z!!g0)wuO2|U~3~M%NeD9M&T&`=V*xA)p?6J(g`{843|W9RAAMLu%a>QCnWNL|M=ad zvp3)=#41brSdOb;QqDLlvKZaFGO%ZspkMVry*ujd$HD{NiE~KlQRE4ZKVY9tp5*vn zjgbyXV#qmtKDcVk-?+XKEAOP`L=IywA~vl~qMV#kEr-rNc35XI^G`@kcFv{L!T&Gpx8$;@;P|6H?;B??8hZvt$iae8K;1XX%lWvQxNGOP>B8O3Hlt znm_%XQqR;i5z8Z5^34BGvXZ!;YTT0l5LZszyBfDl<352|2S(#^riV5!cxYE^Kqih^ zmOk0ib1dzi8w^xXk(7}n4=EWqNiwF>0u|~SOOK>1vm+=w!3`!sIO>zM0m;fOJz2HP ziYnF8=v;J6Y#j5^OUR`I3(#LAm#cFIdJDN+mqqA%$z|*pqhBGPDD5u=hR9`T&qB)* zBf~eEo+5*u9*2;W&Jo2<8YzD&Eq$`3Wrs-$V&ay4k)>CnCz_xuEQh>oGWL!B-1|c;Jn7w8j+snbi(^^o7AQLY%a)InQ6{0 zi|51L7B7U8Ddl@D{|Nj8%1P=Qm6PWCA2qL)PwvtOwK;Xl^Ct5yx+u+QNbxtO_*<3F zC%%j)ynOp0=dc-$za)dX?r(d&rtGj4b7rl|KV{=s_ZX!tUI@2YycF(KuBu|Bb}(`5Ey)mF8q}3NkDv@ zaLnR`aJBNBsdPB*r3K%q*cC3Z3XGsYqjJ#W`ZT9W`D!W{wVV-AE|=@FCM|fIBK5V4 ze+eUm-<~^%G(I>1PyVuONP(v)u=;h*(Bj2#p2f@Im~zQ+oIxLFq-;i{)`}lNe7$mT z0s~AOYc z{RwkmxV#rs(60tFH3yZ=84YO#rouc{XBjb5VUhBrX*@Mp0AtFR!qfOX(xmLe$7QBF z-ACdX|Kll8&#p@Q^2yofl+<>CP_Y zMIMHloOGurrGiElB)h>Tb0L;Pa~d2}hi36~w@dm7~r;Q~IHbt)ImApQ`$`4oRLrF?J5 zo?|NLO9|*tsbElf8VyY3p*NJ`ujMm@oRR6Ee5WUD>aS1nw+77hHvyd~0sYEmM23`C z(Sc*w>lYP-4SWO6Vz*swaS<$E21*B>$M5o%OdeT}X=N4Yc^r;6;0D_pF_h%6_c_R! zVHaKo*ADS;;-9cdnabkNz#mgCVS;85EISa1UjY{pEL$hT`S21eK358~s^NA&T*$T1FJUqso_ZSuDUdTP18CLodoX<|S-{M?&2$l(Ko}hVI z5`XutuGDz<^Fh`Taos1q%o=K_;9;!`z3@1=2$pV7gC{m|8p<2sa}tcN#h1ZLEIt#i zvsnBM7EAfYkUd8#%)%g#K56g`c+?g*c=Q>b3Ab7PT)18N*qM~?;}+mzOZ=t6uk%qy z+4yU@Ug9@NmemUR4Vx)Yl^X^;LI@zk{(tkX~36-lk8JQhaEEhL1p7_%RAaH%bY#LvZ@K2_pzeZkXINMSV4i(HphusTnyJL zn+BSc&5*V!Uq$(>Z*T(2W)5^Ke;LzjK2=@D z!$OkSKjd(BhM%5<=kU=?8mv;ka1`d`gLvUmn0Ak_Hw#1DGDtw3#Wiq~#nM1R`4%e7 z=;jQSgGb=A;qPgrL*Kzaj+Jjf{lbOtkTMzLPddaQ1wJAmho#fuEQNDU^D3x5W`kL6Ab;`@b*A`broE<7!dTVi~~sXk)pcskQP z)?0WQFN&^Z2~~SDoYl&TrMwf)U`cLL{<$zqY+H)IKgD0ggH}48IhXjIJoJ=5FWl)m zy&<3DJceO9OC+BMf&xK@e!~5)9CWCc(@GEhgHpkMwneExMhQK8!R3JH(Fc* zHwEl5*QsDEhE^*;Dri&Qh(BuyJ8X7Laz;JUfkIY#W#b=Gen;Y$^R#EXA@L@)D$5T6 zV@-^(uxC{$A6!iXQ)r-F`CNtb2Cjnv@hary*kX=EM@_YD8E7)!*zoTauPl>NnKKXP;po&YPd=6YWjZ0#25zOaY zFKAE#$}z|eOgfN&bC)p0%J;)#*6{FD{ytpT;5yyPf0Ovjxmhe85BDp_&cNTu&YlgH z)V~(a=;KkA>xG82+P+K(&2lF&v=k6rShA?yJ%2(%p&+Pm<^l6 zuMqC$`X}>3!&VF>PthZb3*Z6e$Hm{nN^NlgoXG}CDwGaOhw_yBh@ZKiN00K!XEG-S zSca6(gC`&0`VUx!F$Xwfi*w;2Y`x-W~HoGHvV2^))6mQ zDyxgb0;b1x%4VsoS2h(kSS%~0{GL!oz>HXvvZ*hjY%WpT5x*CPbgIEzqb}tmLu_#{ z#78O!%MU4=l`^v=?2uUr^OVg>SY+5^<&vc^W(=^bgw@JsC9G98D`B0oSqU4J%}SWC zSXRPzWwR0{l|8c*I;CNc%}SW5Y*xZ5i)AIORyHePt;Mnu#+A)VC_fC7OC&4{U2Di* zV34J-EhV5+*{pGvRSqXcU%}O|+Y*xZSVR`=RQs{I}2sA|%Jung?(;znFcl6doBR4uN(Gq}VTIm?Ol zSb5*n5FQWbDL)C%-0wQI$`j7!{?8xa4An3P!`K5n z-&43jxk3DcY*>_g;F3d(Kuy@-8}Nirm=nC1kq&(d=Qn$v>sN69 z%l}wyCqNC{7A&KVT;yp({La%`33OiFUTN4VpK zQeJ2+#a?=djhNbnbK!*YZSeR{7y^q+;4X{Bzsq9r_bB&Bd51@$@}H%>BG&7WJx2_= z7zQ+;@*F z;b$WOn+E&Rnc%o?niTqD86p2lxhca74JVa{0Z9Vv3VW4Jh0X~64*{Wn()hL%E*cs7 zPiH?SsVjx6Gef@_(Jl{zH0ZGZlC(?NG&lf%fZYt)pt2c}-mIg`_bHq5^5yAYDR1KC z%TZxdpC?~_+8szJn+p2jkE{anvm9Ylq5KTTE?=Z<%FC~2{%*y~%SK`C0OQZ(vn$9{ zHWkRrynhTA2!5m@Y&sx6$&m6dPU8Xgab2-}l)2=;74LT9s?y_qSqw^P~7-a%M~+DzUdWiw)ROji5a%bRcO+RLxwyuff| zNK$5VEM+#=!ynL|8S(~YGvtSq(`%$xEWs(0TYf4a9X1{8m2Y9~$s%8L&6JW{>ydH< zr^i!Jd7g4r%3NqqVVOHJS4_vu+%a5a=7JZK*4USE?FY%tN+n;<2%BpyU(X1eYdr|t zmu`r;V6&sw+FZ)k6qaviB;GUEQofgwGd?sAD@m@M47F*nQQ7$ATMF?%73U90UUv$c zODXU6giXA>tFz<#VgY+c8juh9b_Mc*Q987mKuPj>%H}q<#&!d;cNaDtmc6;{m#w$3 zStn#`{Q>juNDqr@!+^I|NOMdcWmBMD*)-6qY|gMVrF@U_*=z(Q^($Ygd`MXC|E+3} zoul;d5oOuh39pXvha_7yVKa$j2W7A2vLmwBdfD*UOSf!r?4?^aFem+;3QYsDArUqW z$j;**mS1)n!e{3A&s|>8ku%vnF%mE6IDN8vo!j2=gURkC#qv#xoywS}@(cPy9W_(j zXt8`1WT)r355(Qwu~yujDelvl)#7gL*nzvGC|7VeeMd{j@;jcN;-26R?RkHSyFDXP zSFq>S>F%3pUYGWUY2>Wp7)s%$ys$FkmtWUO5DaxNEhY_s?hxYObW_(6-;!rc~M3in!kKK#1H(uo0!>!rzqmSGJ6 z>9hGml1uYBjy#LytjAk?9$aPd1@MU$U)b@bS?=nZn=F$IR9gz~OyS4&yg199aZFiU zFSv}2T+BL~_l%wIek*%!CM$^~iHUN^*9wvh%eM-KCn4fC@A+t{TQF{Je_~jkhSCcnf-my&b&UGDICGT3-u}|~t zYutHbtG*Dny=+^U2Y)-7uW<`Hnyzt+vNnDY+7EW@xW+v>(lDiCAH^cOsyniq$a`jX zER%f6td6Y#&+6DGdGD-_>}K-*SslyBvzF-pk!kPP(M&b{>pS*|`{4SH?2Yc^{|^nb Ba>xJx delta 20806 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 k=5.6) and use it selectively // Supported Lookup/Delete operations by kernel: https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md // Race conditions here causes that some flows are lost in high-load scenarios -func (m *FlowFetcher) LookupAndDeleteMap() map[BpfFlowId]*BpfFlowMetrics { +func (m *FlowFetcher) LookupAndDeleteMap() map[BpfFlowId][]*BpfFlowMetrics { flowMap := m.objects.AggregatedFlows iterator := flowMap.Iterate() - var flow = make(map[BpfFlowId]*BpfFlowMetrics, m.cacheMaxSize) + var flows = make(map[BpfFlowId][]*BpfFlowMetrics, m.cacheMaxSize) var id BpfFlowId - var metric BpfFlowMetrics + var metrics []BpfFlowMetrics // Changing Iterate+Delete by LookupAndDelete would prevent some possible race conditions // TODO: detect whether LookupAndDelete is supported (Kernel>=4.20) and use it selectively - for iterator.Next(&id, &metric) { + for iterator.Next(&id, &metrics) { if err := flowMap.Delete(id); err != nil { log.WithError(err).WithField("flowId", id). Warnf("couldn't delete flow entry") } - metricPtr := new(BpfFlowMetrics) - *metricPtr = metric - flow[id] = metricPtr + metricsPtr := make([]*BpfFlowMetrics, 0) + for _, m := range metrics { + metricPtr := new(BpfFlowMetrics) + *metricPtr = m + metricsPtr = append(metricsPtr, metricPtr) + } + // We observed that eBFP PerCPU map might insert multiple times the same key in the map + // (probably due to race conditions) so we need to re-join metrics again at userspace + // TODO: instrument how many times the keys are is repeated in the same eviction + flows[id] = append(flows[id], metricsPtr...) } - return flow + return flows } // DeleteMapsStaleEntries Look for any stale entries in the features maps and delete them diff --git a/pkg/flow/account.go b/pkg/flow/account.go index b88840e04..d23223fdc 100644 --- a/pkg/flow/account.go +++ b/pkg/flow/account.go @@ -65,7 +65,9 @@ func (c *Accounter) Account(in <-chan *RawRecord, out chan<- []*Record) { alog.Debug("exiting account routine") return } - if _, ok := c.entries[record.Id]; !ok { + if stored, ok := c.entries[record.Id]; ok { + Accumulate(stored, &record.Metrics) + } else { if len(c.entries) >= c.maxEntries { evictingEntries := c.entries c.entries = map[ebpf.BpfFlowId]*ebpf.BpfFlowMetrics{} diff --git a/pkg/flow/account_test.go b/pkg/flow/account_test.go index c53df299c..160054932 100644 --- a/pkg/flow/account_test.go +++ b/pkg/flow/account_test.go @@ -104,11 +104,11 @@ func TestEvict_MaxEntries(t *testing.T) { RawRecord: RawRecord{ Id: k1, Metrics: ebpf.BpfFlowMetrics{ - Bytes: 123, Packets: 1, StartMonoTimeTs: 123, EndMonoTimeTs: 123, Flags: 1, + Bytes: 444, Packets: 2, StartMonoTimeTs: 123, EndMonoTimeTs: 789, Flags: 1, }, }, TimeFlowStart: now.Add(-(1000 - 123) * time.Nanosecond), - TimeFlowEnd: now.Add(-(1000 - 123) * time.Nanosecond), + TimeFlowEnd: now.Add(-(1000 - 789) * time.Nanosecond), }, k2: { RawRecord: RawRecord{ @@ -178,15 +178,15 @@ func TestEvict_Period(t *testing.T) { RawRecord: RawRecord{ Id: k1, Metrics: ebpf.BpfFlowMetrics{ - Bytes: 10, - Packets: 1, + Bytes: 30, + Packets: 3, StartMonoTimeTs: 123, - EndMonoTimeTs: 123, + EndMonoTimeTs: 789, Flags: 1, }, }, TimeFlowStart: now.Add(-1000 + 123), - TimeFlowEnd: now.Add(-1000 + 123), + TimeFlowEnd: now.Add(-1000 + 789), }, *records[0]) records = receiveTimeout(t, evictor) require.Len(t, records, 1) @@ -194,15 +194,15 @@ func TestEvict_Period(t *testing.T) { RawRecord: RawRecord{ Id: k1, Metrics: ebpf.BpfFlowMetrics{ - Bytes: 10, - Packets: 1, + Bytes: 20, + Packets: 2, StartMonoTimeTs: 1123, - EndMonoTimeTs: 1123, + EndMonoTimeTs: 1456, Flags: 1, }, }, TimeFlowStart: now.Add(-1000 + 1123), - TimeFlowEnd: now.Add(-1000 + 1123), + TimeFlowEnd: now.Add(-1000 + 1456), }, *records[0]) // no more flows are evicted diff --git a/pkg/flow/record.go b/pkg/flow/record.go index f3e173da1..9b28caab9 100644 --- a/pkg/flow/record.go +++ b/pkg/flow/record.go @@ -81,6 +81,38 @@ func NewRecord( return &record } +func Accumulate(r *ebpf.BpfFlowMetrics, src *ebpf.BpfFlowMetrics) { + // time == 0 if the value has not been yet set + if r.StartMonoTimeTs == 0 || r.StartMonoTimeTs > src.StartMonoTimeTs { + r.StartMonoTimeTs = src.StartMonoTimeTs + } + if r.EndMonoTimeTs == 0 || r.EndMonoTimeTs < src.EndMonoTimeTs { + r.EndMonoTimeTs = src.EndMonoTimeTs + } + r.Bytes += src.Bytes + r.Packets += src.Packets + r.Flags |= src.Flags + // Accumulate Drop statistics + r.PktDrops.Bytes += src.PktDrops.Bytes + r.PktDrops.Packets += src.PktDrops.Packets + r.PktDrops.LatestFlags |= src.PktDrops.LatestFlags + if src.PktDrops.LatestDropCause != 0 { + r.PktDrops.LatestDropCause = src.PktDrops.LatestDropCause + } + // Accumulate DNS + r.DnsRecord.Flags |= src.DnsRecord.Flags + if src.DnsRecord.Id != 0 { + r.DnsRecord.Id = src.DnsRecord.Id + } + if r.DnsRecord.Latency < src.DnsRecord.Latency { + r.DnsRecord.Latency = src.DnsRecord.Latency + } + // Accumulate RTT + if r.FlowRtt < src.FlowRtt { + r.FlowRtt = src.FlowRtt + } +} + // IP returns the net.IP equivalent object func IP(ia IPAddr) net.IP { return ia[:] diff --git a/pkg/flow/tracer_map.go b/pkg/flow/tracer_map.go index 1cfd6d6df..a07556056 100644 --- a/pkg/flow/tracer_map.go +++ b/pkg/flow/tracer_map.go @@ -27,7 +27,7 @@ type MapTracer struct { } type mapFetcher interface { - LookupAndDeleteMap() map[ebpf.BpfFlowId]*ebpf.BpfFlowMetrics + LookupAndDeleteMap() map[ebpf.BpfFlowId][]*ebpf.BpfFlowMetrics DeleteMapsStaleEntries(timeOut time.Duration) } @@ -96,7 +96,7 @@ func (m *MapTracer) evictFlows(ctx context.Context, enableGC bool, forwardFlows var forwardingFlows []*Record laterFlowNs := uint64(0) for flowKey, flowMetrics := range m.mapFetcher.LookupAndDeleteMap() { - aggregatedMetrics := flowMetrics + aggregatedMetrics := m.aggregate(flowMetrics) // we ignore metrics that haven't been aggregated (e.g. all the mapped values are ignored) if aggregatedMetrics.EndMonoTimeTs == 0 { continue @@ -126,3 +126,23 @@ func (m *MapTracer) evictFlows(ctx context.Context, enableGC bool, forwardFlows } mtlog.Debugf("%d flows evicted", len(forwardingFlows)) } + +func (m *MapTracer) aggregate(metrics []*ebpf.BpfFlowMetrics) *ebpf.BpfFlowMetrics { + if len(metrics) == 0 { + mtlog.Warn("invoked aggregate with no values") + return &ebpf.BpfFlowMetrics{} + } + aggr := &ebpf.BpfFlowMetrics{} + for _, mt := range metrics { + if mt != nil { + // eBPF hashmap values are not zeroed when the entry is removed. That causes that we + // might receive entries from previous collect-eviction timeslots. + // We need to check the flow time and discard old flows. + if mt.StartMonoTimeTs <= m.lastEvictionNs || mt.EndMonoTimeTs <= m.lastEvictionNs { + continue + } + Accumulate(aggr, mt) + } + } + return aggr +} diff --git a/pkg/flow/tracer_map_test.go b/pkg/flow/tracer_map_test.go index 9ea7c1680..2198ece99 100644 --- a/pkg/flow/tracer_map_test.go +++ b/pkg/flow/tracer_map_test.go @@ -11,25 +11,36 @@ import ( func TestPacketAggregation(t *testing.T) { type testCase struct { - input ebpf.BpfFlowMetrics + input []*ebpf.BpfFlowMetrics expected ebpf.BpfFlowMetrics } tcs := []testCase{{ - input: ebpf.BpfFlowMetrics{Packets: 0x7, Bytes: 0x22d, StartMonoTimeTs: 0x176a790b240b, EndMonoTimeTs: 0x176a792a755b, Flags: 1}, + input: []*ebpf.BpfFlowMetrics{ + {Packets: 0, Bytes: 0, StartMonoTimeTs: 0, EndMonoTimeTs: 0, Flags: 1}, + {Packets: 0x7, Bytes: 0x22d, StartMonoTimeTs: 0x176a790b240b, EndMonoTimeTs: 0x176a792a755b, Flags: 1}, + {Packets: 0x0, Bytes: 0x0, StartMonoTimeTs: 0x0, EndMonoTimeTs: 0x0, Flags: 1}, + {Packets: 0x0, Bytes: 0x0, StartMonoTimeTs: 0x0, EndMonoTimeTs: 0x0, Flags: 1}, + }, expected: ebpf.BpfFlowMetrics{ Packets: 0x7, Bytes: 0x22d, StartMonoTimeTs: 0x176a790b240b, EndMonoTimeTs: 0x176a792a755b, Flags: 1, }, }, { - input: ebpf.BpfFlowMetrics{Packets: 0x5, Bytes: 0x5c4, StartMonoTimeTs: 0x17f3e9613a7f, EndMonoTimeTs: 0x17f3e979816e, Flags: 1}, + input: []*ebpf.BpfFlowMetrics{ + {Packets: 0x3, Bytes: 0x5c4, StartMonoTimeTs: 0x17f3e9613a7f, EndMonoTimeTs: 0x17f3e979816e, Flags: 1}, + {Packets: 0x2, Bytes: 0x8c, StartMonoTimeTs: 0x17f3e9633a7f, EndMonoTimeTs: 0x17f3e96f164e, Flags: 1}, + {Packets: 0x0, Bytes: 0x0, StartMonoTimeTs: 0x0, EndMonoTimeTs: 0x0, Flags: 1}, + {Packets: 0x0, Bytes: 0x0, StartMonoTimeTs: 0x0, EndMonoTimeTs: 0x0, Flags: 1}, + }, expected: ebpf.BpfFlowMetrics{ - Packets: 0x5, Bytes: 0x5c4, StartMonoTimeTs: 0x17f3e9613a7f, EndMonoTimeTs: 0x17f3e979816e, Flags: 1, + Packets: 0x5, Bytes: 0x5c4 + 0x8c, StartMonoTimeTs: 0x17f3e9613a7f, EndMonoTimeTs: 0x17f3e979816e, Flags: 1, }, }} + ft := MapTracer{} for i, tc := range tcs { t.Run(fmt.Sprint(i), func(t *testing.T) { assert.Equal(t, tc.expected, - tc.input) + *ft.aggregate(tc.input)) }) } } diff --git a/pkg/test/tracer_fake.go b/pkg/test/tracer_fake.go index 522c63326..908bfb83e 100644 --- a/pkg/test/tracer_fake.go +++ b/pkg/test/tracer_fake.go @@ -14,14 +14,14 @@ import ( // TracerFake fakes the kernel-side eBPF map structures for testing type TracerFake struct { interfaces map[ifaces.Interface]struct{} - mapLookups chan map[ebpf.BpfFlowId]*ebpf.BpfFlowMetrics + mapLookups chan map[ebpf.BpfFlowId][]*ebpf.BpfFlowMetrics ringBuf chan ringbuf.Record } func NewTracerFake() *TracerFake { return &TracerFake{ interfaces: map[ifaces.Interface]struct{}{}, - mapLookups: make(chan map[ebpf.BpfFlowId]*ebpf.BpfFlowMetrics, 100), + mapLookups: make(chan map[ebpf.BpfFlowId][]*ebpf.BpfFlowMetrics, 100), ringBuf: make(chan ringbuf.Record, 100), } } @@ -34,12 +34,12 @@ func (m *TracerFake) Register(iface ifaces.Interface) error { return nil } -func (m *TracerFake) LookupAndDeleteMap() map[ebpf.BpfFlowId]*ebpf.BpfFlowMetrics { +func (m *TracerFake) LookupAndDeleteMap() map[ebpf.BpfFlowId][]*ebpf.BpfFlowMetrics { select { case r := <-m.mapLookups: return r default: - return map[ebpf.BpfFlowId]*ebpf.BpfFlowMetrics{} + return map[ebpf.BpfFlowId][]*ebpf.BpfFlowMetrics{} } } @@ -50,7 +50,7 @@ func (m *TracerFake) ReadRingBuf() (ringbuf.Record, error) { return <-m.ringBuf, nil } -func (m *TracerFake) AppendLookupResults(results map[ebpf.BpfFlowId]*ebpf.BpfFlowMetrics) { +func (m *TracerFake) AppendLookupResults(results map[ebpf.BpfFlowId][]*ebpf.BpfFlowMetrics) { m.mapLookups <- results }