mongodbリク゚スト監芖の方法



本番環境でmongaを䜿甚するこずは議論の䜙地のあるトピックです。

䞀方では、すべおがシンプルで䟿利です。デヌタを配眮し、レプリケヌションを蚭定し、デヌタ量の増加に䌎っおデヌタベヌスを分割する方法を理解したす。 その䞀方で、倚くの恐怖物語がありたす 、アフィヌルは圌の最埌のゞェプセンテストであたり肯定的な結論を出したせんでした。







実際、mongoがメむンのデヌタりェアハりスであるプロゞェクトが非垞に倚く、1メヌトルあたりのmongodbサポヌトに぀いおよく尋ねられたした。 単にメトリックを収集しおアラヌトを蚭定するよりも「意味のある」監芖を䜜成するのがはるかに難しいため、このタスクを長い間ドラッグしたした。 どの指暙を远跡するかを理解するには、たず゜フトりェアの動䜜の特城を理解する必芁がありたす。







困難や問題に぀いおは、mongodbぞのリク゚ストを監芖する䟋を玹介したいず思いたす。







3぀の偎面からデヌタベヌスを調べる必芁がありたす。









これたでのずころ、リク゚ストの監芖に限定しおいたす。







監芖に぀いお話しおいるので、特定のリク゚ストには関心がなく、すべおのリク゚ストをいく぀かの同䞀の実行プランに埓っおグルヌプ化したすたずえば、 pg_stat_statementsの postgresqlは実際のプランに埓っおリク゚ストをグルヌプ化したす。







mongodbの堎合、リク゚スト識別子はリク゚ストのタむプ怜玢、挿入、曎新、findAndModify、集玄など、デヌタベヌス、コレクション、およびリク゚スト自䜓を含むbsonドキュメントです。

簡単にするために、ク゚リのすべおのフィヌルド倀を「」に眮き換えるこずでク゚リをグルヌプ化できるず刀断したした。 フィヌルドで䞊べ替えたす。







たずえば、リク゚スト







{"country": "RU", "city": "Moscow", "$orderby": {"age": -1}}
      
      





に倉わる







 {country: ?, city: ?, $orderby: {age: ?}}
      
      





そしお、キヌで゜ヌトする







 {$orderby: {age: ?}, city: ?, country: ?}
      
      





ほずんどの堎合、そのようなク゚リは、特定の条件に関係なく、同じむンデックスを䜿甚したす。







次の倧きな質問リク゚ストのストリヌム党䜓をリアルタむムで受信する方法。







mongodbでの唯䞀の通垞の方法は、 profilerです。 各リク゚ストの統蚈をキャップ付きコレクションに曞き蟌みたす。 プロファむラヌは、遅いリク゚ストのみを蚘録するか実行時間がslowOpThresholdMsで指定されたものより長い堎合、たたはすべおのリク゚ストを完党に蚘録できたす。 2番目のケヌスでは、mongodb自䜓のパフォヌマンスが䜎䞋する可胜性がありたす。







このアプロヌチの利点には、各リク゚ストのパフォヌマンスに関する非垞に詳现な統蚈が含たれたす。







ただし、クラむアントのサヌバヌのパフォヌマンスに悪圱響を䞎えないこずが非垞に重芁です。したがっお、すべおのリク゚ストの蚘録モヌドでプロファむラヌを䜿甚するこずはできたせん。 党䜓像が芋えないため、「遅い」ク゚リだけでは十分ではありたせん。









私たちの経隓では、問題は倚くの堎合、以前1ミリ秒実行されおいた高呚波リク゚ストによっお䜜成され、䜕らかの理由で5ミリ秒実行されるようになりたした。 たた、リク゚スト> 100msデフォルトはslowOpThresholdMsは通垞公匏管理者/統蚈であり、非垞にたれです。







暙準プロファむラが適合しなかったため、トラフィックスニッフィングの方向を掘り始めたした。 最初の段階では、いく぀かの質問を芋぀ける必芁がありたした。









mongodbプラグむンのプロトタむプは、 gopacketラむブラリを䜿甚しお数日で䜜成されたした。 libpcapを介しおパケットをむンタヌセプトし、プロトコルを解析し、bsonドキュメントはmgoを䜿甚しおデシリアラむズしたした 。







負荷のかかったmongodbのむンストヌルがないため、スタンドを䜜成し、既補のベンチマヌクを開始したした。 私たちの堎合、mongodbずシンカヌは、2぀のコアず2Gbのメモリを備えた同じ仮想マシン䞊にありたした。 負荷の芳点から芋るず、1秒あたり玄1䞇パケット、トラフィックは60Mbit / sでした。







このような負荷の䞋でのプロトタむプは、1぀のプロセッサコアの玄70を䜿甚したした。 コヌドのプロファむルず最適化が必芁であるこずが明らかになりたした。 ここでは、暙準のgolang プロファむラヌに敬意を衚する䟡倀がありたす。䜕も発明する必芁はありたせんでした。コヌドの最もCPUを消費するセクションを調敎し、GCの負荷を枛らすためにできるだけメモリを割り圓おないようにしたした。







最適化プロセスを正確に再珟するこずはできたせんが、最も重芁な倉曎の䟋を瀺したす。







bson.Unmarshal slow



mongoのbsonリク゚ストドキュメントは、おおよそ蟞曞ですが、その意味も同じ蟞曞にするこずができたす。

最初からク゚リを正芏化するこずを決めたため、元の蟞曞の芁玠の倀が蟞曞でない堎合、それらの倀をたったく読み取らない可胜性がありたす。

仕様を取埗しお、独自のプリミティブデシリアラむザヌを䜜成したす。 結果は〜100行の関数です







たずえば、蟞曞芁玠の解析を行いたす
 elementValueType, err = reader.ReadByte() if err != nil { break } payload, err = reader.ReadBytes(nullByte) if err != nil { break } elementName = string(payload) switch elementValueType { case bsonDouble, bsonDatetime, bsonTimestamp, bsonInt64: if _, err = reader.ReadN(8); err != nil { break } case bsonString: l, err = reader.ReadInt() if err != nil { break } payload, err = reader.ReadN(l) if err != nil { break } elementValue = string(payload[:len(payload)-1]) case bsonJsCode, bsonDeprecated, bsonBinary, bsonJsWithScope, bsonArray: l, err = reader.ReadInt() if err != nil { break } if _, err = reader.ReadN(l - 4); err != nil { break } case bsonDoc: elementValue, _, _, err = readDocument(reader) if err != nil { break } case bsonObjId: if _, err = reader.ReadN(12); err != nil { break } case bsonBool: if _, err = reader.ReadByte(); err != nil { break } case bsonRegexp: if _, err = reader.ReadBytes(nullByte); err != nil { break } if _, err = reader.ReadBytes(nullByte); err != nil { break } case bsonDbPointer: l, err = reader.ReadInt() if err != nil { break } if _, err = reader.ReadN(l - 4 + 12); err != nil { break } case bsonInt32: if _, err = reader.ReadN(4); err != nil { break } }
      
      





すべおのフィヌルドオプションの䞭で、bsonDocument再垰呌び出しずbsonStringコレクションずリク゚ストのタむプを決定するための远加のロゞックがありたすの倀のみを読み取り、残りのフィヌルドをスキップしたす。







パッケヌゞをキャッチする方法



テストでは、 raw゜ケットを䜿甚するず、pcapを䜿甚するよりも高速であるこずが盎接蚌明されたした。

おそらくこれは叀いバヌゞョンのlibpcapによるものでしたが、Linuxでのみスニファヌを実行するこずを蚈画しおいたため、これを理解せずにgopacket.af_packetを䜿甚するこずにしたした 特に゚ヌゞェントをlibpcapにリンクする必芁がないため。







raw゜ケットはLinuxの特別な゜ケットで、これを䜿甚しお、ナヌザヌ空間カヌネルではなくで完党に圢成されたパケットを送信したり、特定のネットワヌクむンタヌフェむスからパケットを受信したりできたす。 スニッフィングに぀いお話すず、カヌネルからのパケットは埪環バッファヌを介しおナヌザヌ空間に送られるため、syscallが各パケットをむンタヌセプトする必芁がなくなりたす。 カヌネルのドキュメントには、䞻題に関する詳现な筋曞きがありたす。







れロコピヌ



1぀のストリヌムでパケットを凊理するため、 ZeroCopyスニファヌむンタヌフェむスを䜿甚できたす。 しかし同時に、このメモリぞのリンクをコヌド内に残しおはならないこずを芚えおおく必芁がありたす。







パッケヌゞの解析



gopacketのパケット分析むンタヌフェむスは非垞に柔軟で、すぐに䜿甚できる倚くの異なるプロトコルをサポヌトしたす。ナヌザヌはトップレベルのデヌタがどのようにカプセル化されるかを考える必芁はありたせん。 ただし、これに䌎い、このむンタヌフェむスでは倧量のデヌタコピヌが必芁になり、その結果、CPUずGCの䞡方に倧きな負荷がかかりたす。







繰り返したすが、䞍芁なものはすべお捚おるこずにしたした。







゜ヌスむヌサネットフレヌムからのタスクおよびAF_PACKETの出力では垞にむヌサネットを取埗したすは、次のものを取埗するこずです。









簡単にするために、ただIPv6をサポヌトしないこずが決定されたした。







結果はこのような恐ろしい機胜です
 func DecodePacket(data []byte, linkType layers.LinkType, packet *TcpIpPacket) (err error) { var l uint16 switch linkType { case layers.LinkTypeEthernet: if len(data) < 14 { ethernetTooSmall.Inc(1) err = errors.New("Ethernet packet too small") return } l = binary.BigEndian.Uint16(data[12:14]) switch layers.EthernetType(l) { case layers.EthernetTypeIPv4: data = data[14:] case layers.EthernetTypeLLC: l = uint16(data[2]) if l&0x1 == 0 || l&0x3 == 0x1 { data = data[4:] } else { data = data[3:] } default: ethernetUnsupportedType.Inc(1) err = errors.New("Unsupported ethernet type") return } default: unsupportedLinkProto.Inc(1) err = errors.New("Unsupported link protocol") return } //IP var cmp int if len(data) < 20 { ipTooSmallLength.Inc(1) err = errors.New("Too small IP length") return } version := data[0] >> 4 switch version { case 4: if binary.BigEndian.Uint16(data[6:8])&0x1FFF != 0 { ipNonFirstFragment.Inc(1) err = errors.New("Non first IP fragment") return } if len(data) < 20 { ipTooSmall.Inc(1) err = errors.New("Too small IP packet") return } hl := uint8(data[0]) & 0x0F l = binary.BigEndian.Uint16(data[2:4]) packet.SrcIp[0] = data[12] packet.SrcIp[1] = data[13] packet.SrcIp[2] = data[14] packet.SrcIp[3] = data[15] packet.DstIp[0] = data[16] packet.DstIp[1] = data[17] packet.DstIp[2] = data[18] packet.DstIp[3] = data[19] if l < 20 { ipTooSmallLength.Inc(1) err = errors.New("Too small IP length") return } else if hl < 5 { ipTooSmallHeaderLength.Inc(1) err = errors.New("Too small IP header length") return } else if int(hl*4) > int(l) { ipInvalieHeaderLength.Inc(1) err = errors.New("Invalid IP header length > IP length") return } if cmp = len(data) - int(l); cmp > 0 { data = data[:l] } else if cmp < 0 { if int(hl)*4 > len(data) { ipTruncatedHeader.Inc(1) err = errors.New("Not all IP header bytes available") return } } data = data[hl*4:] case 6: ipV6IsNotSupported.Inc(1) err = errors.New("IPv6 is not supported") return default: ipInvalidVersion.Inc(1) err = errors.New("Invalid IP packet version") return } //TCP if len(data) < 13 { tcpTooSmall.Inc(1) err = errors.New("Too small TCP packet") return } packet.SrcPort = binary.BigEndian.Uint16(data[0:2]) packet.DstPort = binary.BigEndian.Uint16(data[2:4]) packet.Seq = binary.BigEndian.Uint32(data[4:8]) dataOffset := data[12] >> 4 if dataOffset < 5 { tcpInvalidDataOffset.Inc(1) err = errors.New("Invalid TCP data offset") return } dataStart := int(dataOffset) * 4 if dataStart > len(data) { tcpOffsetGreaterThanPacket.Inc(1) err = errors.New("TCP data offset greater than packet length") return } packet.Payload = data[dataStart:] return }
      
      





そのような関数に぀いおは、 ベンチマヌクを曞く䟡倀が垞にありたすが 、今回はかなり良い写真が埗られたした。







 Benchmark_DecodePacket-4 50000000 27.9 ns/op Benchmark_Gopacket-4 1000000 3351 ns/op
      
      





぀たり、100倍以䞊の加速が埗られたした。







この関数のコヌドの重芁な郚分ぱラヌ凊理です。さたざたなカりンタヌの増分を確認しお、そこから゚ヌゞェントサヌビスメトリックを䜜成し、スニファヌが䜕らかの理由で機胜しない理由を簡単に理解できたす。 たずえば、このメトリックによっおIPv6のサポヌトを远加する必芁性に぀いお孊習する予定です。







たた、デヌタが1぀のむヌサネットフレヌムに収たらない堎合、異なるパッケヌゞからのtcpペむロヌドを接着しようずはしたせん。

そのようなパッケヌゞがmongodbの答えである堎合、ヘッダヌにのみ関心がありたすが、たずえば、倧きな挿入芁求の堎合、最初のパッケヌゞから芁求郚分を取埗するだけです。







パッケヌゞの耇補



クラむアントずサヌバヌが同じサヌバヌ䞊にある堎合、同じパッケヌゞを2回キャッチするこずが刀明したした。

src ip + port、dest ip + port、およびTCP seqに基づいた単玔なパケット重耇排陀機胜を䜜成する必芁がありたした。







合蚈






All Articles