Go:MySQLから大きなテーブルのフェッチを高速化

Goを使用して、ほぼ1年間広告ネットワークを作成しています。 Intel i7-7700サーバー、16Gb RAM、256Gb SSDで開発しています。 また、1日に1回実行されるスクリプトでは、過去1日間のすべてのインプレッションを選択し、複数のオブジェクト(ウェブサイト、キャンペーン、バナー)の毎日の統計を一度に再計算するタスクが表示されました。



Goイディオムでは、すべてが非常に簡単に行われます。



type Hit struct { siteID, zoneID, poolID, mediaID, campaignID uint32 } rows, err := db.Query("SELECT siteID, zoneID, poolID, mediaID, campaignID FROM "+where) if err != nil { log.Fatal("Query fail", err) } defer rows.Close() var ( c uint32 h Hit ) for rows.Next() { rows.Scan(&h.siteID, &h.zoneID, &h.poolID, &h.mediaID, &h.campaignID) campCounter.Inc(h.campaignID) siteCounter.Inc(h.siteID) zoneCounter.Inc(h.zoneID) poolCounter.Inc(h.poolID) mediaCounter.Inc(h.mediaID) c++ } if err := rows.Err(); err != nil { log.Fatal("Scan Rows err", err) } log.Println(name, " ", c, " ", where, "in", time.Since(now))
      
      





すべてが機能します。 また、ほぼ5600万レコードの36秒のサンプリング速度。



 hit_20180507 55928930 time BETWEEN 1525640400 AND 1525726799 in 36.331342451s
      
      





goツールのpprofパフォーマンスアナライザーの内部では、次のようなものが表示されます。



  flat flat% sum% cum cum% 7130ms 18.32% 18.32% 10800ms 27.75% runtime.mallocgc 2380ms 6.12% 24.43% 5710ms 14.67% fmt.(*pp).doPrintf 2140ms 5.50% 29.93% 13300ms 34.17% github.com/go-sql-driver/mysql.(*textRows).readRow 1800ms 4.62% 34.56% 2170ms 5.58% runtime.mapassign_fast32 1700ms 4.37% 38.93% 1700ms 4.37% runtime.heapBitsSetType 1170ms 3.01% 41.93% 36350ms 93.40% main.loadHits 1110ms 2.85% 44.78% 8500ms 21.84% runtime.convT2Eslice 1070ms 2.75% 47.53% 1970ms 5.06% fmt.(*fmt).fmt_integer 950ms 2.44% 49.97% 1380ms 3.55% github.com/go-sql-driver/mysql.readLengthEncodedString 930ms 2.39% 52.36% 1060ms 2.72% runtime.freedefer 930ms 2.39% 54.75% 930ms 2.39% runtime.mapaccess1_fast32 910ms 2.34% 57.09% 2070ms 5.32% runtime.deferreturn 860ms 2.21% 59.30% 1220ms 3.13% runtime.scanobject
      
      





mysql。(* TextRows).readRowを使用して、MySQLテキストプロトコルで作業していることがわかります。それぞれ、変換された文字列はuin32型に変換されます。 しかし、そもそもメモリ割り当て機能があります。



ここで何を加速できますか?



偶然、データベースドライバからのバイトがコピーせずにユーザーに転送されることを保証するRawBytesタイプに出会いました 。 まあ何。 Scanをsql.RawBytesフィールドを持つ中間構造に抽出し、急いで書かれた関数bu2を使用して[bytes]をuint32に変換して、エラーチェックを除外します(データベースからのテキストで検索しないため)。



 func b2u(b []byte) uint32 { n := uint32(0) for _, c := range b { n = n*uint32(10) + uint32(c-'0') } return n } type HitRaw struct { siteID, zoneID, poolID, mediaID, campaignID sql.RawBytes }
      
      





その結果、処理時間は28秒に短縮され、すでに200万行/秒になりました!



そして、プロファイラーはすでにそのような写真を提供しています



  4690ms 15.68% 15.68% 7630ms 25.51% runtime.mallocgc 2400ms 8.02% 23.70% 2700ms 9.03% runtime.mapaccess1_fast32 1660ms 5.55% 29.25% 1660ms 5.55% runtime.heapBitsSetType 1640ms 5.48% 34.74% 28110ms 93.98% main.loadHits 1590ms 5.32% 40.05% 1860ms 6.22% runtime.mapassign_fast32 1300ms 4.35% 44.40% 12450ms 41.62% github.com/go-sql-driver/mysql.(*textRows).readRow 1140ms 3.81% 48.21% 2090ms 6.99% runtime.deferreturn 1060ms 3.54% 51.76% 1470ms 4.91% github.com/go-sql-driver/mysql.readLengthEncodedString 1050ms 3.51% 55.27% 1050ms 3.51% main.b2u 1040ms 3.48% 58.74% 1130ms 3.78% database/sql.convertAssign 910ms 3.04% 61.79% 8640ms 28.89% runtime.convT2Eslice 730ms 2.44% 64.23% 2540ms 8.49% database/sql.(*Rows).Scan
      
      





まあ、スタートとして悪くない。 さらに、MySQLドライバーを調査するために登りました。MySQLドライバーは、Go専用に記述されており、ソケットを使用して低レベルのプロトコル自体を実装しています。 そして、2番目のMySQLプロトコルはバイナリであることが判明しました。 理論的には、MySQLサーバー応答の生成がより速くなります。 したがって、ドライバーは、変換関数text-integerを呼び出しません。 バイナリプロトコルを使用するには、db.Queryからdb.Prepareに切り替える必要があります-stsm.Query-ソースコードへの最小限の変更と完了までに26.70秒。



 stmtOut, err := db.Prepare(sqlQ) defer stmtOut.Close() if err != nil { log.Fatal("prepare", err, sqlQ) } rows, err := stmtOut.Query() if err != nil { log.Fatal("query", err, sqlQ) } defer rows.Close()
      
      





プロファイラーは、プロトコルが実際にバイナリ(* binaryRows).readRowであることを示していますが、RawBytesで読み取る場合、テキストに変換してから元に戻します。



  flat flat% sum% cum cum% 2910ms 10.79% 10.79% 3310ms 12.27% runtime.mallocgc 2280ms 8.45% 19.24% 2600ms 9.64% runtime.mapaccess1_fast32 1960ms 7.27% 26.51% 7070ms 26.21% database/sql.convertAssign 1530ms 5.67% 32.18% 1810ms 6.71% runtime.mapassign_fast32 1460ms 5.41% 37.60% 6660ms 24.69% github.com/go-sql-driver/mysql.(*binaryRows).readRow 1420ms 5.27% 42.86% 26680ms 98.92% main.loadHits 1210ms 4.49% 47.35% 3010ms 11.16% strconv.AppendInt 1100ms 4.08% 51.43% 1320ms 4.89% strconv.formatBits 950ms 3.52% 54.95% 1650ms 6.12% runtime.deferreturn 820ms 3.04% 57.99% 820ms 3.04% reflect.ValueOf 810ms 3.00% 60.99% 4120ms 15.28% runtime.convT2E64 750ms 2.78% 63.77% 4240ms 15.72% database/sql.asBytes
      
      





uint32構造体をすぐにスキャンしましょう! もう何も変換する必要はありません。全体変換のみです。



結果は悲しかった-49.827306314sつまり、減速は一般的に恐ろしい。 最速の結果を得るための優れた理論的根拠にも関わらず、可能な限り最も退屈なバージョン。 問題は何ですか?



私たちは見ます:



  4620ms 9.22% 9.22% 29230ms 58.32% database/sql.convertAssign 3610ms 7.20% 16.42% 4010ms 8.00% runtime.mallocgc 3010ms 6.01% 22.43% 8610ms 17.18% reflect.(*rtype).Name 2980ms 5.95% 28.37% 5600ms 11.17% reflect.(*rtype).String 2770ms 5.53% 33.90% 3330ms 6.64% runtime.mapaccess1_fast32 2570ms 5.13% 39.03% 2570ms 5.13% reflect.ValueOf 1760ms 3.51% 42.54% 1980ms 3.95% runtime.mapassign_fast32 1640ms 3.27% 45.81% 6630ms 13.23% github.com/go-sql-driver/mysql.(*binaryRows).readRow 1540ms 3.07% 48.88% 3870ms 7.72% strconv.FormatInt 1240ms 2.47% 51.36% 49600ms 98.96% main.loadHits 1150ms 2.29% 53.65% 1150ms 2.29% reflect.Value.Type 1120ms 2.23% 55.89% 1120ms 2.23% reflect.Value.Elem 1070ms 2.13% 58.02% 30950ms 61.75% database/sql.(*Rows).Scan 1070ms 2.13% 60.16% 1070ms 2.13% strconv.ParseUint
      
      





strconv.ParseUintの存在から判断すると、2つの型の変換は文字列を介して行われます! ほんと? reflect-transformsは、実行時に最初の行にあります。 Rob Pikeがリフレクションの注意深い使用について語るのも不思議ではありません。 あなたは物事をすることができます。



MySQLドライバーを研究した結果、バイナリプロトコルからすべてのデータがint64に変換されるという事実に出会いました。これを利用してみましょう。 スキャンイン構造



 type HitRaw struct { siteID, zoneID, poolID, mediaID, campaignID int64 } ... h.siteID = uint32(raw.siteID) h.zoneID = uint32(raw.zoneID) h.poolID = uint32(raw.poolID) h.mediaID = uint32(raw.mediaID) h.campaignID = uint32(raw.campaignID)
      
      





結果は33.98秒です。 この機能別レイアウト



  3600ms 10.48% 10.48% 14360ms 41.79% database/sql.convertAssign 2860ms 8.32% 18.80% 3340ms 9.72% runtime.mallocgc 2560ms 7.45% 26.25% 2920ms 8.50% runtime.mapaccess1_fast32 1660ms 4.83% 31.08% 6730ms 19.59% github.com/go-sql-driver/mysql.(*binaryRows).readRow 1540ms 4.48% 35.56% 33970ms 98.86% main.loadHits 1410ms 4.10% 39.67% 1690ms 4.92% runtime.mapassign_fast32 1340ms 3.90% 43.57% 1340ms 3.90% reflect.ValueOf 1290ms 3.75% 47.32% 4010ms 11.67% reflect.Value.Set 940ms 2.74% 50.06% 15960ms 46.45% database/sql.(*Rows).Scan 900ms 2.62% 52.68% 900ms 2.62% reflect.Value.Elem 840ms 2.44% 55.12% 840ms 2.44% reflect.Value.Type 840ms 2.44% 57.57% 1500ms 4.37% runtime.deferreturn 810ms 2.36% 59.92% 810ms 2.36% reflect.directlyAssignable 760ms 2.21% 62.14% 760ms 2.21% runtime.getitab 730ms 2.12% 64.26% 900ms 2.62% reflect.Value.assignTo 720ms 2.10% 66.36% 4060ms 11.82% runtime.convT2E64
      
      





sql.convertAssignがバイナリプロトコルを使用する利点をすべて減らすことがわかります。 データはテキストを介してコピーされませんが、内部の反映により、ユーザーのint64変数にコピーできるint64を決定することは依然として非常に困難です。 また、テキストとの間で数値をコピーすることは、reflect.directlyAssignable-reflect.Value.assignToよりも高速です。



ウォームアップとして、b2u関数をGoアセンブラーに変換しようとしました。 アセンブラーは、ドライブとカセットレコーダーなしで学校で学んだ最初のBK-0011プログラミング言語の1つでした。 Goはほぼ最適なコードを生成しますが、アルゴリズムのトリックや非標準のASMコマンドを使用しない場合は、これらの関数を書くことに特別な意味はありません。



 // func b2u(data []byte) uint32 // // memory layout of the stack relative to FP // +0 data slice ptr // +8 data slice len // +16 data slice cap #include "textflag.h" TEXT ·B2u(SB),NOSPLIT,$0-24 // data ptr MOVQ data+0(FP), SI // data len MOVQ data+8(FP), CX // result in AX MOVBLZX (SI), AX // - '0' SUBL $48, AX // check end of loop DECQ CX JZ AX2RET LOOPBYTE: //move to one byte upper INCQ SI MOVBLZX (SI), BX //prev result *= 10 IMULL $10, AX // bx -= '0' SUBL $48, BX ADDL BX, AX // check end of loop while (cx--) DECQ CX JNZ LOOPBYTE AX2RET: MOVL AX, ret+24(FP) RET
      
      





テストによると、Goバージョンの2〜20%の加速が得られます。 数字の桁数に依存します。

その結果、作業例は26.94秒に加速しました。



この記事の結論は、テキストを見たばかりの人-大量の整数データをMySQLからメモリに読み込む最速の方法-db.Prepare-stmt.Query-スキャンしてインターフェイス{}に変換し、uint32(hit.siteID。(Int64) )上位ビットを削除せずにuint64で機能する符号なし整数に。

つまり、標準的な例に示されているドライバーの操作方法は、常に最適とは限りません。 おそらく、開発者はドライバーの動作に注意を払うでしょう。これは、単純な言語とオーバーロードされていない言語の両方に多くの隠れた呼び出しとオーバーヘッドがあるためです。 結局のところ、データベースからのSELECT選択が表示されるテストのGoは、パフォーマンスに影響しません 。 さらに、すべての経験豊富なプログラマーが内部を掘り下げる時間と欲求を持っているわけではありません。 私の知る限り、このようなSELECTテストを行った人はいません。



UPD:コメントは、すぐに読めるように設計されたと思われる奇跡的なドライバーgithub.com/lazada/sqleの例を提供しました。 結果は読み通したときでした

uint32変数のrows.Scan(&h.siteID、&h.zoneID、&h.poolID、&h.mediaID、&h.campaignID)は非常に悲しい

55928930 time BETWEEN 1525640400 AND 1525726799 in 1m0.307942824s





そして、プログラムが1分間何をしたかを見ると、彼らがこのケースを最適化することを考えていなかったことが明らかになります。 標準ドライバーが2回勝ちます。

flat flat% sum% cum cum%

4.63s 7.62% 7.62% 29.25s 48.11% database/sql.convertAssign

4.22s 6.94% 14.56% 4.68s 7.70% runtime.mallocgc

2.97s 4.88% 19.44% 8.48s 13.95% reflect.(*rtype).Name

2.96s 4.87% 24.31% 5.51s 9.06% reflect.(*rtype).String

2.90s 4.77% 29.08% 41.28s 67.89% github.com/lazada/sqle.(*Rows).Scan

2.51s 4.13% 33.21% 2.95s 4.85% runtime.mapaccess1_fast32

2.38s 3.91% 37.12% 2.38s 3.91% reflect.ValueOf

1.92s 3.16% 40.28% 3.69s 6.07% runtime.assertE2I2

1.77s 2.91% 43.19% 1.77s 2.91% runtime.getitab

1.65s 2.71% 45.90% 7.04s 11.58% github.com/go-sql-driver/mysql.(*binaryRows).readRow

1.49s 2.45% 48.36% 1.86s 3.06% runtime.mapassign_fast32

1.35s 2.22% 50.58% 60.27s 99.13% main.loadHits

1.31s 2.15% 52.73% 4.01s 6.60% github.com/lazada/sqle.typeCheck

1.31s 2.15% 54.88% 4.19s 6.89% strconv.FormatInt

1.28s 2.11% 56.99% 2.88s 4.74% strconv.formatBits

1.25s 2.06% 59.05% 1.25s 2.06% reflect.(*rtype).Kind

1.12s 1.84% 60.89% 31.05s 51.07% database/sql.(*Rows).Scan







[]バイト変数でスキャンを読み取り、b2u()を介してuint32に変換すると、44秒になります。 私の意見では、あなたはもはや標準的なデータベース/ SQLの人たちが書いた素晴らしい代替品をテストすることはできません。 加速に関するもう1つの神話は、実際のテストについて暴かれています。



UPD2:MySQLからの読み取り速度を理解するために、変換と計算を行わずにサイクルを作成しました

 for rows.Next() { c++ }
      
      





結果は11.9秒です。 したがって、27のうち15秒がハッシュマップの変換と操作に費やされます。



UPD3:記事を書き終えた後、Goの内部についての理解を深め始めました。私の鼻の真下に私のケースの錠剤が見つかりました。

golang.org/pkg/database/sql/#Rows.Scan

引数のタイプが* interface {}の場合、スキャンは変換せずに基礎となるドライバーによって提供された値をコピーします。

これが、私が皆さんにお勧めするコードの最終バージョンです。

 type HitRaw struct { siteID, zoneID, poolID, mediaID, campaignID, status interface{} } var ( hit HitRaw h Hit ) for rows.Next() { rows.Scan(&hit.siteID, &hit.zoneID, &hit.poolID, &hit.mediaID, &hit.campaignID, &hit.status) h.siteID = uint32(hit.siteID.(int64)) h.zoneID = uint32(hit.zoneID.(int64)) h.siteID = uint32(hit.siteID.(int64)) h.poolID = uint32(hit.poolID.(int64)) h.mediaID = uint32(hit.mediaID.(int64)) h.campaignID = uint32(hit.campaignID.(int64)) h.status = uint8(hit.status.(int64))
      
      





その結果、誰もが無関心になることはありません。

同じ5,590万件のレコードの20.126921479。 そして、これは27秒後です。その時点で、私はnに到達したと思いました。

そして、プロファイラーはすでにかなり良いレイアウトを提供しています-ほとんどの場合、プログラムはハッシュでカウンターを更新します。 バイナリデータは、ドライバーから直接待機している場所に送信されます。 アセンブラーはオプションであることが判明しました。

flat flat% sum% cum cum%

3110ms 15.20% 15.20% 3460ms 16.91% runtime.mallocgc

2350ms 11.49% 26.69% 2700ms 13.20% runtime.mapaccess1_fast32

1850ms 9.04% 35.73% 2150ms 10.51% runtime.mapassign_fast32

1460ms 7.14% 42.86% 6670ms 32.60% github.com/go-sql-driver/mysql.(*binaryRows).readRow

1420ms 6.94% 49.80% 20070ms 98.09% main.loadHits

1170ms 5.72% 55.52% 1190ms 5.82% database/sql.convertAssign

840ms 4.11% 59.63% 4300ms 21.02% runtime.convT2E64

710ms 3.47% 63.10% 2470ms 12.07% database/sql.(*Rows).Scan

680ms 3.32% 66.42% 1260ms 6.16% runtime.deferreturn

680ms 3.32% 69.75% 680ms 3.32% sync.(*RWMutex).RLock

650ms 3.18% 72.92% 650ms 3.18% runtime.aeshash32

630ms 3.08% 76.00% 630ms 3.08% sync.(*RWMutex).RUnlock








All Articles