Cloud FunctionsをGo言語で動かす

# 概要

firebase で Cloud Functions を使ったときに、JavaScript と golang で作った function とで性能に差がでるかを簡単に確認したのでそのメモ

## firebase の Cloud Functions で Go言語使える？

らしい。のでやってみました。
やり方としては、以下参考の「[go言語でCloud Functions + FireStoreをhttpで使ってみた](https://www.wheatandcat.me/entry/2019/01/29/001032)」を参照しつつ、ざっくりとは次の手順で環境を構築しました

- firebase のコンソールでプロジェクトを作成
- firebase に javascript 版の Cloud Functions の関数をデプロイする
- golang で Cloud Functions の関数を作成し、GCP にデプロイする
    - firebase のプロジェクトを使いまわして、GCP の Cloud Functions を作成するのがミソ

### 参考

- [go言語でCloud Functions + FireStoreをhttpで使ってみた](https://www.wheatandcat.me/entry/2019/01/29/001032)
- [Cloud FunctionsをGoで書く。またはFirebaseのためのマイクロサービスアーキテクチャ](https://laiso.hatenablog.com/entry/2019/12/16/microservice_architecture_and_firebase)


## 測定結果

メモリは、なんとなく128MBに変更しました。測定は、vegeta を利用しました。
結果は、レイテンシが JavaScript 版が平均 5.138s に対して、Go 言語版が 161.252ms と 32倍の差がでました
あと、GCP のコンソールから目分量ですが、メモリとアクティブインスタンス数に大きな違いがでました

| - | JS | Go | 比較 |
|:-:|:-:|:-:|:-:|
| メモリ | 100MB  | 15MB  | 6.6倍 |
| アクティブインスタンス  | 50  | 3  | 16倍  |


### 参考

- [Firebase Functions の割り当てメモリを変更する](https://qiita.com/seigo-pon/items/0f6550985fa0859a05af)
- [ちょっと良さげな負荷ツール vegeta をつかって分散負荷試験を実現してみる](https://qiita.com/chidakiyo/items/f8cdfac7683216a29c56)

#### JavaScript 版

##### 10rps を 1秒間

```
gosshys@gosshysnoMacBook-Air perf % vegeta attack --rate 10 -duration=1s -targets=target.txt | vegeta report
Requests      [total, rate, throughput]         10, 11.10, 1.17
Duration      [total, attack, wait]             8.57s, 900.524ms, 7.669s
Latencies     [min, mean, 50, 90, 95, 99, max]  3.124s, 4.734s, 3.826s, 7.376s, 7.669s, 7.669s, 7.669s
Bytes In      [total, mean]                     5920, 592.00
Bytes Out     [total, mean]                     400, 40.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:10  
Error Set:

```

##### 10rps を 10秒間

```
gosshys@gosshysnoMacBook-Air perf % vegeta attack --rate 10 -duration=10s -targets=target.txt | vegeta report
Requests      [total, rate, throughput]         100, 10.10, 6.08
Duration      [total, attack, wait]             16.45s, 9.901s, 6.549s
Latencies     [min, mean, 50, 90, 95, 99, max]  2.439s, 5.138s, 4.602s, 8.011s, 8.437s, 9.379s, 9.571s
Bytes In      [total, mean]                     59200, 592.00
Bytes Out     [total, mean]                     4000, 40.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:100  
Error Set:
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38174/808fe674-d068-99c3-657b-5c7ca16618a2.png)

#### golang 版

##### 10rps を 1秒間

```
gosshys@gosshysnoMacBook-Air perf % vegeta attack --rate 10 -duration=1s -targets=target_go.txt | vegeta report
Requests      [total, rate, throughput]         10, 11.18, 9.01
Duration      [total, attack, wait]             1.11s, 894.571ms, 215.591ms
Latencies     [min, mean, 50, 90, 95, 99, max]  135.716ms, 259.473ms, 178.427ms, 563.063ms, 691.467ms, 691.468ms, 691.468ms
Bytes In      [total, mean]                     610, 61.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:10  
Error Set:
```

##### 10rps を 10秒間

```
gosshys@gosshysnoMacBook-Air perf % vegeta attack --rate 10 -duration=10s -targets=target_go.txt | vegeta report
Requests      [total, rate, throughput]         100, 10.10, 9.96
Duration      [total, attack, wait]             10.044s, 9.9s, 143.807ms
Latencies     [min, mean, 50, 90, 95, 99, max]  142.071ms, 161.252ms, 148.575ms, 163.501ms, 184.75ms, 623.21ms, 942.147ms
Bytes In      [total, mean]                     6100, 61.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:100  
Error Set:
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38174/a0cef453-650f-bcad-cef1-6773958c662f.png)

# 付録

## 測定に利用した functions

リクエストで指定された url にアクセスして、OGP(https://ogp.me/)を metaタグから抜き出して返却する関数

### JavaScript

JS の書き方が悪いせいで遅くなっていたらすみません

```
const functions = require("firebase-functions");
const axios = require('axios');
const jsdom = require('jsdom');
const { JSDOM } = jsdom;

exports.getOgpFromExternalWebsite = functions.https.onCall(async (data, context) => {

  if (!data.url) {
    console.log("not exists url");
    return {};
  }

  const headers = {'User-Agent': 'bot'};
  const res = await axios.get(data.url, {headers: headers});
  const html = res.data;
  const dom = new JSDOM(html);
  const meta = dom.window.document.head.querySelectorAll("meta");
  const ogp = {};
  let i = 0, len = meta.length;
  for (; i < len; i++) {
    if (!meta[i].hasAttribute("property")) {
      continue;
    }
    const property = meta[i].getAttribute("property");
    const content = meta[i].getAttribute("content");
    ogp[property] = content;
  }

  if (Object.keys(ogp).length===0) {
    return {};
  }

  return ogp;

})

```

### golang

```golang
package ogp

import (
  "fmt"
  "golang.org/x/net/html"
  "golang.org/x/net/html/atom"
  "log"
  "net/http"
)

type OGType int

const (
	Type OGType = iota
	Title
	Description
	URL
	Image
)

type OG struct {
	Type,
	Title,
	Description,
	URL, Image string
}

func findMeta(node *html.Node, og OG) OG {
	for c := node.FirstChild; c != nil; c = c.NextSibling {
		if c.Type == html.ElementNode {
			if c.DataAtom == atom.Meta {
				var val, t string
				for _, attr := range c.Attr {
					if attr.Key == "property" {
						t = attr.Val
						continue
					}
					if attr.Key == "content" {
						val = attr.Val
					}
				}
				switch t {
				case "og:type":
					og.Type = val
				case "og:title":
					og.Title = val
				case "og:description":
					og.Description = val
				case "og:url":
					og.URL = val
				case "og:image":
					og.Image = val
				}
			}
			if og.Type != "" && og.Title != "" && og.Description != "" && og.URL != "" && og.Image != "" {
				break
			}
			og = findMeta(c, og)
		}
	}
	return og
}

func OGPGet(w http.ResponseWriter, r *http.Request) {
	url := r.URL.Query().Get("url")
	if url == "" {
		w.Write([]byte("welcome"))
		return
	}
	resp, err := http.Get(url)
	if err != nil {
		fmt.Fprintf(w, "err: %+v", err)
		return
	}

	defer resp.Body.Close()
	node, err := html.Parse(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	og := OG{}
	og = findMeta(node, og)
	fmt.Fprintf(w, "url:%s, og:%+v", url, og)
}
```

GCP にデプロイするコマンド

```
gcloud functions deploy OGPGet --runtime go116 --trigger-http --project <firebase のプロジェクトID>
```

## 測定に利用したリクエスト (vegeta の targetsファイル)

### JavaScript

※ 東京リージョンに作成したかったが、なぜか us-central1 に作成されたようです

```:target.txt
POST https://us-central1-<firebase のプロジェクトID>.cloudfunctions.net/getOgpFromExternalWebsite
Content-Type: application/json
@req.json
```

```:req.json
{"data":{"url": "https://yahoo.co.jp"}}
```

### golang

```:target_go.txt
GET https://us-central1-<firebase のプロジェクトID>.cloudfunctions.net/OGPGet?url=https%3A%3A%2F%2Fyahoo.co.jp
```