えんじにあのじゆうちょう

勉強したことを中心にアウトプットしていきます。

【読書】スケーラブルデータサイエンス 第2章

はじめに

スケーラブルデータサイエンスを読み始めたので、読みつつ気になったところのまとめを実施していこうと思います。
まずは第2章のクラウドへのデータの取り込みです。

まとめてみる

データ投入の方法

基本的に外部から発生したデータをpush or pullで運んでくるわけですが、この本では外部のデータをWebスクレイピングをして自動で持ってくるケースについて考えています。

ここでまず考えがちな選択肢は、サーバを立ててそこでcronjobを実行することです。
これ、個人的には運用もめんどくさいし、基本的にありえないのですが、その理由もしっかり書かれていたのは良いかなと思いました。

今回くらいの処理であれば一般的にはサーバレスでの実行となると思いますが、この本ではGoogle App Engineを採用しています。
個人的に、なぜCloud Functionsじゃないの??と思ったりしたので、少しこのあたりを調べてみることにしました。

GCPのサーバレスオプション

先に結論から言ってしまうと、この本が出版されたのが2018年とあり少し古く、実際、最新のコードリポジトリをみるとFunctions用に書き直されていました。直感どおりです。
github.com


とはいえなので、少しまとめてみます。
まず、GCPのサーバレスに関するオプションについては以下に詳しくまとまっています。
cloud.google.com

これによると、いまは「App Engineのスタンダード環境」「Cloud Functions」「Cloud Run」が主流のようです。

この本ではもともとApp EngineのFlexible環境を使っていましたが、今回はおまけ程度にFlaskの画面がついていましたが、それを採用した理由はStandard環境ではzipファイルをローカルに解凍しておけないと言う制約があるためのようです。

ちなみに、このあたりの制約については以下にまとまっています。
cloud.google.com

いまなら、Standard環境 かつ Pythonであっても /tmpへ書き込み可能のようで、
この本のタイミングはおそらく第1世代の頃だったんだろうなぁと推測できます。

ではGAEのStandard環境でいいではないかと思えなくもないですが、やはりFunctionsのほうが適切だと考えられます。

この本のデモではWeb画面もついてきますが、実際にこれは必要なく、今回は起動を試しやすさの観点からHTTPベースにしているというように読めます。

実際にやりたいことは、「時刻ベース」で「外部Webにあるデータを取得」し「簡単な整形」をかけ「Google Cloud Storageにアップロード」です。
これであれば、Cloud Functionsのほうが適切なように考えられます。

もう少し複雑な作業になってきて、環境ごとコンテナでパッケージングしたいようなケースではCloud Runが楽に見えますが、今回はそこまででもないので不要でしょう。

データの置き場所としてのGoogle Cloud Storage

いわゆるオブジェクトストレージのサービスですが、AWS S3の印象が強くこの手のサービスは結果整合性しか保証しないものだと思っていました。

docs.aws.amazon.com

ところが本を読むと、Google Cloud Storageのリージョナルストレージに関しては強整合性を保証するという記載があり、念の為調べてみました。

cloud.google.com

外部公開しなければ、という制約はあるものの、この手の制約が重要となる局面では外部公開は必須ではないと思いますので、非常に有用な違いだなと思います。

ローカルにデータを出力しないで処理する方法

いまとなってはない制約ですが、本の中で「Standard環境使いたければメモリ上で処理するように」という記載がありました。
制約から考えればすでに不要ですが、メモリ上で処理することでIOを低減させ実行時間を早めることが可能になると考えられます。
(まぁ、ファイルの大きさや数次第ではあまり変わらないということもあるかもしれませんが)

ちょうど先日のPyConJP 2020の中で素晴らしいセッションがあり、Python上でどのようにメモリ上でファイル等を扱うかについて勉強できたので、そのアウトプットも行うことにします。

一般的に外部Webから取得したzipをPythonで処理する場合、ローカルに書き出してzipfileモジュールを使って解凍という手段を取ると思います。
そこを上記のインメモリストリームを用いて書き直してみました。

github.com
本家をforkして変更しました。

ポイントは以下のように取得したZipをio.BytesIOに渡すところです。

response = urlopen(url, PARAMS)
zipfile_stream = io.BytesIO(response.read())

これにより、後続するZipFileモジュールにメモリ上でファイルを渡すことが可能です。

また、ZipFileモジュールの展開コマンドを使うと現在のディレクトリ直下にファイルが展開されてしまいます。
メモリ上で扱う場合は、以下のようにzipされているファイル名をキーにreadメソッドを呼び出すことで元データの取り出しが可能です。

zip_ref = zipfile.ZipFile(zipfile_stream, "r")
name = zip_ref.namelist()[0]
data = zip_ref.read(name)

今回は1つのzipファイルに1つしかファイルが入っていないことがわかっているので、このように扱ってみました。

感想

今回は第2章を読みつつ、気になったところを調べたり、実装を少しいじったりしてみました。
多少クラウド慣れしているつもりではあったのですが、クラウドによって細かい所の差異は結構あるんだな、というところは改めて勉強になりました。