デジタル創作同好会traPさんと社内ISUCONイベントを開催しました

  • URLをコピーしました!

こんにちは、22新卒で、PR TIMES開発本部でバックエンド開発をしている宮崎です。

先日、デジタル創作同好会traPさんと、社内ISUCONイベントを開催しました。今回は、その準備で作成したLambda関数の紹介をします。

ISUCON」は、LINE株式会社の商標または登録商標です。

目次

当日の様子

会社にtraPの方々をお招きし、オフラインで行いました。traPからは20名以上参加いただきました。今回使用した問題はこちらです。

GitHub
GitHub - catatsuy/private-isu: 社内ISUCON 社内ISUCON. Contribute to catatsuy/private-isu development by creating an account on GitHub.

最終的に30万点を超えたチームが2つあり、398,912点を獲得したチームが優勝しました🎉

スコアの遷移

問題を解き終わった後に懇親会を行い、実際にどんな改善をしたかなどを話しました。PR TIMESの開発本部の社員もtraPの方々とコミュニケーションをとり、お互い刺激的な1日になったと思います。

当日の様子

Lambda関数の実装

構成

ISUCONのベンチマーカーをLambdaで構築する記事はいくつかありますが、今回はLambda 関数 URL (Lambda function URLs)を使って、API Gatewayを挟まずに直接Lambdaで専用のHTTPエンドポイントを用意し構築しました。

こうすることで、API Gatewayを用意せずに済みます。また、API Gateway は30秒でタイムアウト(2023年1月時点)になってしまう制限がありますが、Lambda 関数 URL は Lambda の タイムアウトの制限が適用されるため、最大15分(2023年1月時点)まで伸ばすことが可能という利点があります。

しかし、この仕組みだと、ある程度スコアの高いチームはLambdaのファイルディスクリプタが足りなくなり、ベンチマーカーが動かなくなってしまうという欠点もあります。実際に今回も発生しました。そのため、この事象が発生したチームのために、ベンチマーカー用のインスタンスをEC2で起動し、MackerelにPOSTするだけのCLIをGoで作り、シェルスクリプトでベンチマーカーを実行し、結果をPOSTする仕組みを突貫で作り対応しました。

競技者はcurlでLambda 関数 URL で作成したURLを叩くとベンチマークが走るようになっています。

グラフでのスコア表示はMackerelにしました。また、普段からLambdaはGoで書いているので、Goを採用しました。

参考記事

pixiv inside
社内ISUCON開催のための構成とノウハウを公開!Amazon Lambdaでサーバレスのベンチマーカーを構築した話 - ... こんにちは。 pixivの投稿ユーザ向けグロースを担当しているエンジニアのsestaです。 4月28日、ISUCON6本戦の問題を作ったedvakf、catatsuyと一緒に第2回社内ISUCONを開催...

構成をまとめると以下のようになります。

参考記事の構成と比較して、API Gatewayがないことがわかります。

Lambda関数

Lambdaでは以下の項目を実装します

  • リクエストを送ってきたインスタンスのIPアドレスを取得する
  • スプレッドシートにIPアドレスとチーム名を書いておいて、チーム名を取得
  • ベンチマークを実行
  • Mackerelにチーム名とスコアを送る
  • ベンチマークの結果を返す

詳しく見ていきます。

リクエストを送ってきたインスタンスのIPアドレスを取得する

LambdaFunctionURLRequest を使い、IPアドレスを取得します。

func lambdaHandler(req events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) {
		clientIP := req.RequestContext.HTTP.SourceIP
}

スプレッドシートにIPアドレスとチーム名を書いておいて、チーム名を取得

IPアドレスとチーム名を列とするシートを用意します。

スプレッドシート側で、Google Sheets APIを有効化する必要がある点に注意してください。設定を進めていくとJSONファイルがダウンロードできるので、option.WithCredentialsJSONを使い、環境変数から取得しています。

このように実装しました。

func getTeamNameFromSpreadSheets(clientIP string) (string, error) {
		spreadsheetID := os.Getenv("SPREADSHEETID")
		credential := option.WithCredentialsJSON([]byte(os.Getenv("SPREADSHEET_CREDENTIALS_JSON")))
		srv, err := sheets.NewService(context.TODO(), credential)
		if err != nil {
					log.Println(err)
					return "", err
		}

		readRange := "シート1!A2:B50"
		res, err := srv.Spreadsheets.Values.Get(spreadsheetID, readRange).Do()
		if err != nil {
			log.Println(err)
			return "", err
		}
		if len(res.Values) == 0 {
			err := errors.New("could not get data")
				log.Println(err)
				return "", err
		}
		for _, row := range res.Values {
				if row[0] == clientIP {
					s, ok := row[1].(string)
					if !ok {
							return "", errors.New("failed to convert team name")
					}
					return s, nil
				}
		}
		return "", errors.New("client ip address and team name did not match")
}

呼び出し側で以下のようにします。

teamName, err := getTeamNameFromSpreadSheets(clientIP)
if err != nil {
		return getError(err)
}

ベンチマークを実行

private-isu/benchmarker/をbuildし、そのファイルを任意のディレクトリに配置します。この時、GOOSの指定を忘れないようにしてください。

GOOS=linux GOARCH=amd64 go build -o benchmarker

また、ベンチマークの実行には、userdataを渡す必要があるので、こちらを参照して必要なファイルをあらかじめ配置しておきます。この時、Lambdaにデプロイする時にデプロイパッケージの制限を超えないように注意してください。Lambdaから直接アップロードの場合は50MBまで、S3からは解凍後250MBまで(2023年1月時点)。そのため、画像ファイルをいくつか削除することで制限に収まるように対応しました。

構造体を定義します。

type Output struct {
		Pass     bool     `json:"pass"`
		Score    int64    `json:"score"`
		Suceess  int64    `json:"success"`
		Fail     int64    `json:"fail"`
		Messages []string `json:"messages"`
}

exec.Commandを使い、ベンチマークを実行し、結果のJSONを構造体に変換します。

targetURL := "http://" + clientIP
result, err := exec.Command("./bin/benchmarker", "-t", targetURL, "-u", "./userdata").Output()
if err != nil {
		return getError(err)
}

var output Output
err = json.Unmarshal(result, &output)
if err != nil {
		return getError(err)
}

Mackerelにチーム名とスコアを送る

公式が提供しているライブラリを使いました。サービスメトリックの投稿のAPIを呼び出します。

GitHub
GitHub - mackerelio/mackerel-client-go: Mackerel API Client in Go Mackerel API Client in Go. Contribute to mackerelio/mackerel-client-go development by creating an account on GitHub.
apiKey := os.Getenv("MACKEREL_API_KEY")
client := mackerel.NewClient(apiKey)
err = client.PostServiceMetricValues("My-Service", []*mackerel.MetricValue{
		{
				Name:  "Score." + teamName,
				Time:  time.Now().Unix(),
				Value: output.Score,
		},
})
if err != nil {
		return getError(err)
}

ベンチマークの結果を返す

ベンチマークの実行は完了しているので、あとは結果を返すだけです。

return events.LambdaFunctionURLResponse{
		Body:       string(result),
		StatusCode: 200,
}, nil

実際に動かす

序盤は順調でしたが、途中で {"message":"exit status 2"} とだけ返ってくる事象が発生しました。これはベンチマークでFailした時に出てくるもので、ログを見てみると、このメッセージ以外何も出ていませんでした。この記事を参考に標準出力とエラーを区別して取得するようにし、ログを出すように修正しました。

cmd := exec.Command("./bin/benchmarker", "-t", targetURL, "-u", "./userdata")
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
if err != nil {
		log.Printf("Stdout: %s\n", stdout.String())
		log.Printf("Stderr: %s\n", stderr.String())
		return getError(err)
}

結果を返すところも修正します。

err = json.Unmarshal(stdout.Bytes(), &output) 

...(中略)

return events.LambdaFunctionURLResponse{
		Body:       stdout.String(),
		StatusCode: 200,
}, nil

出るようになったログの例がこちらです。

2023/01/13 06:33:35 Stdout: {
    "pass": false,
    "score": 78,
    "success": 72,
    "fail": 1,
    "messages": [
        "ユーザー名が表示されていません (GET /)"
    ]
}

最後に

社内ISUCONを開催する時に参考にしていただければと思います。

PR TIMESでは、普段はPHPを用いたアプリケーション開発や開発環境の改善を行なっていますが、サーバレスで開発する時はGoを用いてこのような開発をしています。

会社の雰囲気や技術スタックにご興味がある方は、ぜひ一度カジュアル面談に応募してくださると幸いです。

株式会社PR TIMES
カジュアル面談申し込みフォーム - 株式会社PR TIMES 株式会社PR TIMESでは現在00-0. カジュアル面談(全職種OK)を募集しています。

また、2月に新卒向けにハッカソンを行います!こちらの応募もお待ちしております。

あわせて読みたい
2月16・17日の2日間で24卒向け内定直結オンラインハッカソンを開催! - 株式会社PR TIMESのWebエンジニアの... ▶ PR TIMES HACKATHON 2023 Winter 2024年新卒エンジニア向けオンラインハッカソン「PR TIMES HA...
  • URLをコピーしました!

この記事を書いた人

2022新卒で PR TIMES に入社し、開発本部でバックエンドエンドエンジニアをしています。

目次