1 of 23

メルカリ カウルの

マスタデータの更新

2017年11月14日(火)

@golang.tokyo #10

The Go gopher was designed by Renée French.

The gopher stickers was made by Takuya Ueda.

Licensed under the Creative Commons 3.0 Attributions license.

2 of 23

2

自己紹介

上田拓也

@tenntenn

所属

コミュニティ活動

&

Go ビギナーズ

Go Conference

上田拓也

@tenntenn

3 of 23

3

ソウゾウ エキスパートチーム

技術をアウトプットするところに技術は集まる

■ エキスパートチームとは?

  • 50%以上の時間を技術コミュニティへの貢献に充てる

■ エキスパートチームの役割

  • 社内に新しい技術を取り取り込む
  • 社外のコミュニティなどを通じて社会へ還元する

■ エキスパートチームの活動

  • カンファレンス・勉強会の開催/運営
  • 対外的な講演活動
  • 執筆、雑誌への寄稿、インタビュー
  • 社内外での担当技術の普及推進

@tenntenn

担当:Go・GCP

@mhidaka

担当:Android

メンバー

4 of 23

アジェンダ

  • メルカリ カウルのマスタデータ
  • 大きなファイルを処理する
  • バイト列を変換する

4

5 of 23

メルカリ カウルのマスタデータ

5

6 of 23

Google App Engineを採用

6

7 of 23

メルカリ カウルのマスタデータ

  • 製品のカタログ情報
    • 本、CD・DVD、ゲーム
    • 外部のデータを利用している
  • 毎日の更新バッチにて更新している
    • データ提供元から毎日データをバッチで取得している
    • データの提供形式はデータの種類によって違う
      • JSONやTSVなど
  • Cloud SQL (MySQL)にて管理
    • スケールする必要のない低いマスタデータはRDBで管理
    • スケールする必要のあるデータはCloud Datastoreで管理

7

8 of 23

マスタデータ更新の例

8

9 of 23

大きなファイルを分割して処理する

9

10 of 23

マスタデータの更新

  • TSV形式
    • 1行がRDBの1レコードを表す
    • 大きいものは1ファイルが50MB以上
  • Google Cloud Storage(GCS)に保存
    • 1日1回データ提供元から取得したデータをGCSに保存
    • 1日あたり十数個のTSVがGCS上に置かれる
  • Google App Engine(GAE)のcronジョブで更新
    • 定期的にTSVを取得し、RDBへデータを更新する

10

※ すべてのマスタデータがこうなっているわけではない

ID

製品名

発売日

発売元

001

ほげ

2017年11月14日

会社A

002

ふが

2017年11月13日

会社B

11 of 23

大きなファイルを扱う問題点

  • URL Fetchの制限
    • Google Cloud StorageにURL Fetch経由で接続
    • 32MB以上のファイルを一気に読めない
  • 分割して処理する必要がある
    • どのように分割するのか?
    • 一度メモリ上に確保して分割しても意味がない
    • 固定長で分割していくしかない

11

12 of 23

大きなファイルを処理する

  • Task Queueを使う
    • 更新順序に決まりがない
    • 不要な外部キー制約を外す
    • 冪等性を担保する
  • オフセットを指定して読み込む
    • Google Clous StrageのRangeReaderの機能を使う
    • 固定長に分割して処理する

12

13 of 23

Task Queueによる処理

13

14 of 23

TSVを分割する方法

  • 固定長で分割する
    • RDBにいれるデータなので1行のサイズの最大がおおよそ分かる
    • 1レコードの最大サイズ+バッファで分割する
  • 1行ずつ処理する
    • 固定長で分割するので行をまたぐ可能性がある
    • 固定長に分割する際に若干被るように分割する
    • 改行がくるまでバッファ分をはみ出て処理する
    • 頭の部分は改行がくるまで読み飛ばす

14

001\tほげ\t2017年11月14日\t会社A\n

002\tふが\t2017年11月13日\t会社B\n

...

15 of 23

バイト列を変換する

15

16 of 23

マスタデータの変換

  • 変換の必要性
    • データ提供元の形式がUTF-8とは限らない
    • アプリ上で表示できるものとは限らない
      • 外字などを利用している場合
    • データ提供元のデータを改変して保存するわけにはいかない
      • 実行時に動的に変換する必要がある

16

17 of 23

文字コードの変換

  • x/text/encoding/japaneseを使う
    • SJISやEUP-JPなどの文字コードに対応

17

func main() {

dec := japanese.ShiftJIS.NewDecoder()

r := transform.NewReader(os.Stdin, dec)

io.Copy(os.Stdout, r)

}

$ echo "ごーふぁー" | nkf --sjis | go run sjis.go

ごーふぁー

18 of 23

x/text/transfom用いる

  • transform.Transformerインタフェース
    • ReaderWriterを提供している
    • 既存のReaderWriterをラップし、間で変換を行う
    • x/transform/encoding.Encoderも実装している

18

transform.Reader

io.Reader

Transformer

transform.Writer

Transformer

io.Writer

Read

Write

19 of 23

Transformer実装する

  • 実装はなかなか難しい
    • 出力と入力で長さが違うかもしれない
    • 出力と入力のバッファのサイズが足りないかもしれない
    • EOFがくるかもしれない
    • テストをしっかり行おう

19

type Transformer interface {

Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error)

Reset()

}

20 of 23

詳しくはAdvent Calendarで!!

20

21 of 23

バイト列の置換

  • Replacer

21

func ExampleReplaceTable() {

t := ReplaceStringTable{

"Hello", "Hi",

"World", "Gophers",

}

r := transform.NewReader(strings.NewReader("Hello, World"), ReplaceAll(t))

io.Copy(os.Stdout, r)

// Output: Hi, Gophers

}

22 of 23

まとめ

  • 大きなファイルを処理する
    • うまく分割し、並列に処理してやる
    • 途中で変更するのは大変なので先にやっておくと良い
  • バイト列を変換する
    • x/text/transformパッケージを用いる
    • transform.Transformerの実装は難しい
    • テストをしっかり行おう

22

23 of 23

23

Thank you!

twitter: @tenntenn

Qiita: tenntenn

connpass: tenntenn