Goの練習 PHPerが業務用Webアプリケーションでありそうな処理をGoと速度比較してみる

# はじめに

PHPerとして仕事を始めて6年半(2019年現在)。様々な現場で業務用Webアプリケーションの構築に携わってきました。
Webアプリケーションであれば業務で使うような大体の機能はPHPで実現できるのですが、1システムに1機能はPHPだと物足りないというか、痒い所に手が届かないなと思う箇所はどうしても出てきます。

顕著にそれを思うのは大量データの処理などのパフォーマンスを発揮したい機能です。
PHP7の登場でPHP5と比較して格段にパフォーマンスが向上しました。ですがそれでももっと速く処理したいと思う場面がしばしばあります。

そこで自身のスキルの幅を広げることも含めて、スクリプト言語のように記述出来てハイパフォーマンスが期待できるGoを学んでみようと思いました。

# 本記事の概要

経験上Webアプリケーションでありそう、かつ処理が重そうな機能を考えた結果、CSVアップロード/ダウンロード機能が思いつきました。
今回はフロント部分は省略して、コマンドラインでCSVをDBに保存する機能とDBからCSVを作成する機能をGoとPHPで作成し、そのパフォーマンスの差を検証します。
諸先輩方が通ってきた道かと思いますが、自学のため。

# 機能の概要

### アップロード

- あらかじめ用意したCSVを読み込み、DBにアップロードする。
- 指定した件数をひとかたまりとして処理する
- 新規追加フラグを設け、これが1の場合は新規追加とみなしDBとの重複チェックを行う。

使用したCSVは国交省提供の[位置参照情報ダウンロードサービス](http://nlftp.mlit.go.jp/isj/index.html)から全自治体のデータをダウンロードし加工したものになります。
およそ21万件ほど。

UPSERTをするようにしたので結局重複チェックは意味をなさないのですが、それっぽく作るため入れてみました。

### ダウンロード

- DBからデータを全量取得し、CSVに出力する。
- 指定した件数をひとかたまりとして処理する

こちらの方が純粋なパフォーマンス比較ができそうです。

両方の機能でGoでは並行処理での処理を指定可能。並行処理では2スレッドで処理します。

### やらないこと

細かいことは抜きにする方針にします。

- フロント部分(GUI)の作成
- 細かいバリデーション
  - アップロード用CSVの存在チェック
  - 形式チェック
- SQLインジェクション対策

等々

ハマったら容赦なくOSSライブラリを使おうと思います。
逆に言えばハマるまでは標準パッケージで頑張ります。


# 実行環境等

**サーバ**
Amazon Linux 2 AMI (HVM), SSD Volume Type
t2.medium(CPU 2コア メモリ 4GB)

**言語**
go v1.13
php v7.2.22

**DB**
MySQL 5.7

AmamzonLinuxの上にそれぞれのアプリケーションとDBのDockerコンテナを建てて実行しています。

# 実行結果
ソースは→ [GitHub](https://github.com/tshackstream/go-practice)

GNU版timeコマンドを利用しています。Goはコンパイル済みのファイルを実行。

まずはアップロードから。重複エラーがないパターン。

|言語|real|user|sys|消費メモリ|
|---|---|---|---|---|
|Go|7.91s|1.00s|0.05s|3.62MB|
|Go(並行処理)|4.18s|1.00s|0.09s|5.65MB|
|PHP|7.00s|1.61s|0.11s|6.32MB|

全件重複エラーするパターン。

|言語|real|user|sys|消費メモリ|
|---|---|---|---|---|
|Go|8.46s|1.20s|0.30s|3.62MB|
|Go(並行処理)|5.27s|1.23s|0.23s|6.18MB|
|PHP|8.91s|1.90s|0.15s|16.99MB|

次はダウンロード

|言語|real|user|sys|消費メモリ|
|---|---|---|---|---|
|Go|0.92s|0.74s|0.05s|2.42MB|
|Go(並行処理)|0.70s|0.82s|0.04s|3.97MB|
|PHP|1.07s|0.54s|0.24s|7.47MB|

グラフにしてみます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58841/73632d72-c08a-b089-7cb0-9b70eca2f4ec.png" width="860">
実際に処理にかかった時間。意外とPHPと差はない、どころか逐次実行の場合はGoがPHPより1秒ほどかかっています。並行処理の場合は流石に速いですね。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58841/7193fa62-ec0c-ccb4-3fd5-a5eb7acfec27.png" width="860">
CPUが処理をした時間。並行処理と逐次処理の間に大きな差はなし。アップロードにおいてはPHPの方が占有時間が長いようです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58841/1a851514-9d64-0787-c85f-a61965ef4ef7.png" width="860">
OSがシステムコールに使った時間。正直なところ語れるほど詳しくないのですが場合によりけりといった印象です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58841/c8e2fb46-be08-525c-b877-615a62faf4bd.png" width="860">
消費メモリ。Goの方が軒並み少ないですね。

# 所感

## 測定結果に関して

- 当初想定していたよりもGoとPHPの差はあまりありませんでした。実際の処理時間に焦点を当てると場合によってはPHPの方が速かったりということもあり、驚きました。
私自身がGo初学者というのもあり、パフォーマンスチューニングができていない可能性も高いです。
ただし並行処理になるとやはりというべきか結構な時間短縮になりますね。

- メモリ消費量はGoの方がパフォーマンスが良さそうなので、同じ処理でもサーバの負荷を抑えられるなどのメリットはありそうです。
ここはスクリプト言語とコンパイル形式の言語の違いもありそうですが。

- データ量が増えたりするとまた差がでたりするのでしょうか。

## 実装に関して

- Scan関数の引数で取得したカラムを一つ一つ指定して変数に入れてあげないといけないのは少し面倒。
- DBから取得したデータをうまいことCSVに書き出す方法を見つけるのにハマったので結局[gocsv](https://github.com/gocarina/gocsv)に頼りました。
- エラーハンドリングはもう少ししっかりやった方がいいかなと思いました。特に問答無用のexitは実務レベルではあまりやらないと思います。
- アップロードで指定した件数をひとかたまりとしてに重複チェックと投入をしていますが、1件でも重複チェックに引っかかったかたまりは投入されず、引っかからなかったかたまりは投入されるというよくわからない状況になっています。業務では引っかからなかったデータだけ投入するとか、エラーが1件でもあったら投入しないという仕様にするのが妥当だと思います。完成してから気づきました…
今回は重複チェック意味ないのでご容赦を。
- 今回Goの実装に費やした時間は余暇時間を使って環境構築したりハマったり調べたりを繰り返し、大体1週間ぐらいでできました。時間がかかるのは想定していましたが、それでも結構時間がかかった印象です。本人のスキルが多分にありそうですが、毛色の違う言語から来たときの学習コストがなんとなくわかりました。PHPの方は1時間もかからずにできました。

なんとなく触ることはできたので、次は何か開発してみたいと思います。いつできるかはわかりませんが。