YAPC::Kyoto 2023にスタッフとして参加した
ということでスタッフとして参加した。その後も個人的にやることがたくさんあってエントリかけてなかったけど...(これは言い訳です)
京都でYAPCをやると聞いた時からスタッフとして参加したいと考えていた。自分が住んでいる京都で開催されるなら、せっかくならイベントをつくる側に回りたいと思っていて、YAPC::Kyoto 2020のスタッフを募集しているのを見かけて応募したのが始まり。YAPC::Kyoto 2020は結局延期されてしまったけど、YAPC::Kyoto 2023としてrebootするときに改めて声を掛けてもらって、スタッフとして参加することができた。無事に当初の願いが叶ったのだった。
色々やらせてもらったけど、目にみえるところだと個人スポンサー向けのノベルティの企画立てたりした。
裏話だけど、最初はタンブラーではなくて、全然違うものを電電宮で祈祷してもらってノベルティにしようということを企画していた。ただ企画を進める途中でタンブラーを見つけて、この方が普段使いできそうだなと思ってタンブラーに変えさせてもらった。せっかくなら普段から使えるものがいいだろうし、今回のようなタンブラーならコード書いてるときに横に置いてもらえるんじゃないかと思う。キラキラステッカーの方は電電宮で祈祷してもらったけど、祈祷するにあたり電電宮にメールしたりして調整を進めたりした。寺社仏閣にメールする経験なんてないので毎回妙に緊張していた。結果としていい感じのYAPC::Kyoto 2023のOP動画も撮れたので良かったと思う。と書いているけど、実は祈祷当日は別の用事があって行けなかったのでちょっとざんねん。まあそのうち自分で行こうと思う。
今回はLTでも話すことができた。会社関連のイベントで登壇したことは何度かあるけど、ああいうイベントで、あれくらいの人数の前で話すのは実は初めてで、今までにないくらい緊張した。一つ経験できたのは良かったと思う。次はトークにも応募できたら良い。
正直なことを書くと、実は始まるまで「オフライン開催」自体に懐疑的というか、オンラインとどういう違いがあるかがあんまり想像できていなかったというか、遠くの人も参加できるという点ではオンラインにまだ分があると考えていた。でも、集まった人々があちこちでコミュニケーションとっているのを見て、ああこういうイベントはオフラインでやるべきだなと思い直した。久々に会ったとか、ネットではお見かけするけど実際に話すのは初めてみたいなコミュニケーションがあちこちで起こっているのは良かった。自分はそういう中に入っていくのは苦手で、今回は多少頑張ったと思うけど、次はもっと積極的に行きたい。
イベント準備から当日まで、自分も十分楽しかったけど、今回来てくれた人たちが楽しいと思ってくれたのであれば、自分にとってはそっちの方が成功と言える。来て良かったと思う人がたくさんいると嬉しい。
なんかだいぶ雑なエントリになったけど、とにかく楽しかった。実はトークは全然見られてないので、後でアーカイブでゆっくり見ようと思う。
GolangでSMTPを使ってメールを送る処理を書く
最近Golangでメール送信処理を書くことがあったのだけど、あまり事情を知らなかったのでまとめた。
golang+SMTPでメールを送る
Goには標準ライブラリでnet/smtp
というのがある。
smtp package - net/smtp - Go Packages
これは名前の通りGoからSMTPでメールを送信するためのライブラリなのだけど、例えばヘッダとボディの間には空行を一行自分で挟まないといけないとか、素朴すぎて結構辛い。 さすがに2023年にもなってさすがにそういうことはやりたくないので、もう少しいいやつないかなと探して、今回は以下のライブラリを使った。
これはそこそこ高機能だと思う。少なくとも自分でヘッダ部とボディの間に空行を入れる、みたいなことをしなくてもいい。middlewareを差し込めるようになっていて、middlewareによって挙動を少し変える、みたいな最近ぽいこともできるのもよい(そういう場面がどれくらいあるかは置いといて)。あと最近もメンテされているという点もよい。
SMTPでメール送るコードはこういう感じ。
package main import ( "log" "mime" "os" "strconv" "github.com/wneessen/go-mail" ) func main() { msg := mail.NewMsg() host := os.Getenv("SMTP_HOST") if host == "" { log.Fatal("SMTP_HOST required") } port, err := strconv.Atoi(os.Getenv("SMTP_PORT")) if err != nil { log.Fatal(err) } if err := msg.From("hoge@example.test"); err != nil { log.Fatal(err) } if err := msg.To("fuga@example.test"); err != nil { log.Fatal(err) } msg.Subject(mime.BEncoding.Encode("UTF-8", "こんにちはこんにちは")) msg.SetBodyString(mail.TypeTextPlain, "ようこそこんにちは") c, err := mail.NewClient(host, mail.WithPort(port)) if err != nil { log.Fatal(err) } if err := c.DialAndSend(msg); err != nil { log.Fatal(err) } }
こういう感じで動く。Subjectはmime.BEncoding
したりする必要がある。あとSMTPのホストとかポートも環境変数で渡せるようにしておくほうが使いやすいけど、この辺はそれぞれの事情による。
送信されるメールを確認する
メールを送信するコードを書いたり、送信用のメールのテンプレートを追加したような時に、見た目を確認するために実際に自分なりクローズドな何かにメールを送る、というのをやったことがある人は結構いると思う。こういう作業でミスしないように慎重に送信テストするぞ・・・とか言ってストレスMAXになったりしたことありませんか。僕はある。ローカルの環境にメールサーバーを立てておいて、そこに送って確認したらまあいいのだけど、そんな面倒なことはしたくない・・・という人におすすめなのがMailhog
というテスト用のSMTPサーバーを立てる方法。
これを自前でビルドするとかではなくて、DockerHubに既にイメージが存在するのでそれを使う。
これがいいのはWebUIがついている点で、これで立てられたテスト用のSMTPサーバーにメールを送ると、送られたメールをそのWebUIで確認できる。
開発時にはdocker compose
でアプリケーションと同時に立てると、アプリケーションのメール送信処理をテストする仕組みを簡単につくることができる。
docker-compose.ymlの該当部分はこういう感じになる。
version: '3.8' services: app: build: context: . command: [ ] # 任意のコマンド env: SMTP_HOST: 'mail' SMTP_PORT: '1025' mail: image: mailhog/mailhog:latest ports: - "8025:8025" - "1025:1025"
mailhogはデフォルトだとSMTPサーバーは1025番で、WebUIは8025番で受けているので、必要であればポートフォワードするとよい。
前述のメール送信のコードにも書いたけど、アプリケーションにはSMTP_HOST
やSMTP_PORT
を環境変数で渡すようにしておけば、ローカル開発ではmail
コンテナの1025番にメールを送信して、本番環境では任意のホスト/ポートを設定する、というようにできる。
あとはメールを送信する仕組みを実際に動かして、WebUIで確認したらよい。個人的には、本当に送信されることがないよう、RFC2606で予約済みのTLDである.test
みたいなドメインにテストメールを送るようにしている(これが正しい使い方かは微妙なラインかもしれない)
https://tex2e.github.io/rfc-translater/html/rfc2606.html
まとめ
近年だとメールを送ることあまりないだろうし、あってもAWS SNSみたいなマネージドサービスを使ってSDK経由でメール送信する、みたいなことの方が多いと思うけど、もしもSMTP経由でメール送信することになったら参考にしてほしい。あとMailhogは便利。
ISUCON12に出て予選敗退した
id:yashigani_wさん,
id:wtatsuruさんと、「デジタルトランスフォーメーションズ」というチームでISUCON12に出たけど、振り返ってみたらほぼ初期データを弄って終了してしまったな、という振り返りエントリ。何を振り返ったらええねんと思いながら過ごしていたら時間が経ってしまった。
そういえばチーム名の最後に
2022
ってつけたいと思ってたのを今思い出した。
当日やったこと
我々のチームでは15分スプリントというのをやっていて、「15分作業 → 5分スプリント会」みたいなサイクルを回していた。これはwtaturuさんが去年のチームでやっていたらしく、今年もやることにした。ということでスプリント会の記録がissueに残っている
https://github.com/tatsuru/isucon12-yosen/issues/3
終盤少しグダッとした場面もあったが、それでも18スプリント回せていたのはよかった
初期データをつくる
手を動かしていた場面はほとんどこれで終わってしまった...
- 初回のベンチを回した結果を眺めていて、rankingのエンドポイントが遅いので、そこから直そうとなった
- とはいえどういう作戦にするかをパッと決めることはできなくて、どうやって手を入れるかの材料を集めるのに結構時間がかかってしまった。最終的には以下の作戦でよかろうという話になる。
- 初期データでは
player_score
から一つのユーザーが複数のスコアをもっていたので、これを各ユーザー最新の一件のみになるようにデータを作り直す - データを入れる時も最新の一件だけになるように
player_score
をひく時は単純にORDER BY score
でソートするように
- 初期データでは
- rankingが遅いのでranking作るところだけ直すか、と話していたところ、結局データを入れるところも直さないと意味がないよねとなり、セットで修正することに
- 最初は
/initialize
でこの辺りの処理全部やってしまうつもりで進めていたが、一度ベンチマークを試したら初期化処理に落ちてしまって、作戦を少し変える- 振り返ってみるとこの時どういう理由で落ちたかあんまり見てなかったのはよくなかった
- スコアに関するデータはSQLiteのDBに乗っていて、実体としてはテナントごとに分割されたファイルなので、初期データを手元で作り直してそれをホストに置く作戦にした
- 各ユーザーの最新のスコアだけ取り出すのは以下のSELECT文で取ってきていた(たぶん合ってると思う)
SELECT * from player_score WHERE competition_id = ? GROUP BY player_id HAVING MAX(row_num);
- ちなみに初期データを作り直すのもGoで書いた。と言っても元データ取り出してSELECTでフィルタリングして別のDBに書き込むだけなので難しいことはしていない
- これで
player_score
のレコード数が大幅に減った- 一番多いところで160万レコードくらいあったのが8万レコードくらいになった
- 一方でこの辺りでミスが増え始める
player_score
だけ新しいDBに移してしまい、その他のテーブルを移し忘れて/initialize
に落ちる- 100テナントあることはわかっていたけど、forの条件普通にミスって99テナント分しか移しておらず、やはり
/initialize
に落ちる- 全テーブル移したのになんで落ちるんとか思っていたけど、ちゃんとログ出したら原因判明したし、わかった後はあまりにも恥ずかしくてすまん・・・すまん・・・となっていた
- 他にもあった気がするけど忘れてしまった。ただ修正→確認のサイクルの時間が長くてだいぶ時間使ってしまった
メモリリークの修正
- 初期データを作っている横でメモリ使用量が膨らんでパフォーマンスが落ちる問題が起きる
- 完全に勘だったけど、メモリを使いまくるような処理はcsvの読み込みじゃないのと思ったので、アクセスの傾向を改めて眺めたところ、csvによるスコア登録のリクエスト数が初期実装と比較して増えていることに気づく
- ここを少し修正してベンチが最後まで通るようになったあたりで概ね時間終了
- github.com
ふりかえり
- id:wtatsuruさんも書いていたが、とにかく手が遅い。去年も遅いなと思っていたけど改めて痛感した
- 与えられた環境で勝負しすぎた。仕事でやったとしてもさっさとSQLiteからMySQLに移行しましょうと言うと思う
- DB移行は頭には浮かんだものの時間かかりそうだからって後回しにしてしまった
- 最終的にはどうせやるだろう作業は最初にやるのがよい
- ログ出すのも困ったらすぐにやるべきだった
- 三人で集まって会話しながら作業できたのはよかった
- ディスプレイを2台置いて、常に片方がペアプロしてるような状況を作れた
- ホワイトボードで絵を描いて意見を擦り合わせたりがやりやすかった
- 15分スプリント、5分振り返りで18スプリント回したけど、そうすると会話の時間は90分あることになるが、これは少し長いかもしれないし、そんなことないかもしれない
- 悔しい結果とはなったけどシンプルに楽しかった
- 今回は一番ネックになっていそうな部分を順に直していきましょうというのだけ決めていて、その通りに動けたのはよかった
来年また出直します...運営の皆様お疲れ様でした & ありがとうございました。
メンバーの振り返り
2021年
良くも悪くも子供が中心だった一年だったように思う。子供が楽しそうにしてるのを見たくて休みにあれこれ計画したり、子供が体調を崩すと仕事ができなくて家の中がめちゃくちゃになったりというのを繰り返していたら一年が終わった。
本当は住む場所を変えたりしたかったのだけど、これも子供の都合で少し先送りすることにした。つぎに引っ越すときは地元へ帰ることも検討しているけど、まあもう少し先になりそう。
2022年にはなにか新しいことも始めてみたい。
AWS S3バッチオペレーションのちょっとしたtipsなどのご紹介
このエントリは、はてなエンジニアAdvent Calendarの9日目の記事としてかかれました。
AWS S3にはバッチオペレーションというマネージドサービスがあって、これは指定したバケット/オブジェクトに対して一括で何かしらの操作ができる。例えば「バケット内のすべてのオブジェクトを別バケットにコピーしたい」とかそういう時に使うと便利。
その一括操作ではLambdaを利用することもできる。Lambdaを使うとかなり柔軟な操作ができるようになるが、ドキュメントを見ただけでは最初どうしたらいいかわからなかった上に、利用する機会もそんなに無いので覚えられない。その他にも最初に知ってたらよかったみたいなのが細々とあるので、そういうのを少しまとめておく。
なお、このエントリではS3 バッチオペレーション自体のジョブの登録のやり方自体は割愛する。まずS3バッチオペレーションがどういうものか知りたいという人には、以下のエントリが雰囲気を掴むのに良いと思う。
マニフェストファイル
バッチオペレーションのジョブを登録するにはどのオブジェクトを操作するのかを指定するためのマニフェストファイルが必要になる。マニフェストファイルにはS3インベントリレポートというのを使うと良いというのをよく見るけど、実は自分で作ったCSVを使うこともできる。フォーマットとしては以下のような感じ。
{bucket},{object_key}
つまりバケットと操作したいオブジェクトのキーの一覧だけでよい。最初から操作したいオブジェクトがわかっている場合はインベントリレポートを出力せずとも上のフォーマットのCSVを作ればよいし、インベントリレポートを全部チェックした上で、一部の操作対象を出力する、とかでもよい。初回のインベントリレポートの出力には少し時間がかかる(24h程度?)ので、操作対象が完全にわかっている場合は自分でCSVファイルを作ったほうが実は手早いなどはありそう。
S3 バッチオペレーションからLambdaへのリクエスト
Lambdaへは以下のようなJSONがリクエストされる(ドキュメント から引用)
{ "invocationSchemaVersion": "1.0", "invocationId": "YXNkbGZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZza2RmaAo", "job": { "id": "f3cc4f60-61f6-4a2b-8a21-d07600c373ce" }, "tasks": [ { "taskId": "dGFza2lkZ29lc2hlcmUK", "s3Key": "customerImage1.jpg", "s3VersionId": "1", "s3BucketArn": "arn:aws:s3:us-east-1:0123456788:awsexamplebucket1" } ] }
tasksは配列になっているが、実際には要素は一つしかない。Lambdaでどういう操作をするかにもよるが、操作するにあたって重要なのはs3Key
となる。これを利用して、例えばObjectをGetしてきて加工して別のバケットにPUTしたりする。Lambdaからのレスポンスは以下のようなものを返す(こちらもドキュメントから引用)
{ "invocationSchemaVersion": "1.0", "treatMissingKeysAs" : "PermanentFailure", "invocationId" : "YXNkbGZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZza2RmaAo", "results": [ { "taskId": "dGFza2lkZ29lc2hlcmUK", "resultCode": "Succeeded", "resultString": "[\"Mary Major", \"John Stiles\"]" } ] }
こちらもresults
は配列となっているが、実際には要素を一つだけ返せばよい。results内のresultCode
にはSucceeded
/TemporaryFailure
/PermanentFailure
のどれかを指定する。ジョブのレポートを出力して、オブジェクトごとに処理の成否を確認したいような場合は成否に合わせてSucceeded
/PermanentFailure
を指定し、resultString
に成功した場合は成功メッセージを、失敗した場合は失敗理由がわかるようなメッセージを埋めておき、レポートを精査するのが良い。
個人的にはresultString
にはJSON文字列を埋めておくのがよいと考えている。大量のオブジェクトを操作した結果について精査する場合、レポートのCSVを何かしらのスクリプトで処理した上でresultString
を確認することになるはずで、その時にJSONであれば扱いやすいと思う。
また、レスポンスの以下のフィールドにはリクエストのJSONに含まれる値をそのまま利用するとよい。あと特に詳細には触れなかったけど、バッチオペレーションからLambdaを実行するとか、LambdaからS3にアクセスするための権限設定はそれぞれ必要になる。
- invocationSchemaVersion
- invocationId
- results.taskId
特にresults.taskId
はバッチオペレーションのジョブで一意になっていないと失敗してしまうので、リクエストされたJSONに含まれている値をそのまま使うのが一番安全。
これ以外にtreatMissingKeysAs
フィールドがあり、これは特にドキュメントに記載されていないが、例えば先程のresults.taskId
がジョブで一意になっておらず失敗した場合、ここで指定されたレスポンスコードが利用される。下のメッセージはresults.taskId
が一意ではなかった場合にレポートに出力されていたもの。
"Lambda function didn't return the tasks/keys in the function response, using ""treatMissingKeysAs"" (default to TemporaryFailure) as error code: PERMANENT_FAILURE"
ここにあるように、デフォルトだとTemporaryFailure
が使われるけど、ここにはPermanentFailure
を指定しておけば良いと思う。
任意の値をLambdaに渡したい
任意の値をLambdaに渡す方法も用意されている。バッチオペレーションのジョブを登録する時に操作するオブジェクトのバケットとキーを指定するためのCSVファイルを渡すことになるのだけど、そのCSVのキーの位置にURLエンコードしたJSON文字列を入れるとよい。以下のドキュメントに記載されている。
例えば次のようなJSONをLambdaに渡したいとする。
{ "s3Key": "1aaaa.txt", "newKey": "new-1aaaa.txt" }
この場合、バッチオペレーションのジョブに渡すCSVファイルは以下のような感じになる。
tkzwtks-s3,%7B%22s3Key%22%3A%221aaaa.txt%22%2C%22newKey%22%3A%22new-1aaaa.txt%22%7D tkzwtks-s3,%7B%22s3Key%22%3A%222aaaa.txt%22%2C%22newKey%22%3A%22new-2aaaa.txt%22%7D tkzwtks-s3,%7B%22s3Key%22%3A%223aaaa.txt%22%2C%22newKey%22%3A%22new-3aaaa.txt%22%7D tkzwtks-s3,%7B%22s3Key%22%3A%224aaaa.txt%22%2C%22newKey%22%3A%22new-4aaaa.txt%22%7D tkzwtks-s3,%7B%22s3Key%22%3A%229aaaa.txt%22%2C%22newKey%22%3A%22new-9aaaa.txt%22%7D
このCSVは1つ目のフィールドにバケット名、二つ目のフィールドにURLエンコードしたJSON文字列が入っている。これをジョブの登録時に渡すと、Lambdaには以下のようなJSONがリクエストされる。
{ "invocationSchemaVersion": "1.0", "invocationId": "YXNkbGZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZza2RmaAo", "job": { "id": "f3cc4f60-61f6-4a2b-8a21-d07600c373ce" }, "tasks": [ { "taskId": "dGFza2lkZ29lc2hlcmUK", "s3Key": "%7B%22s3Key%22%3A%221aaaa.txt%22%2C%22newKey%22%3A%22new-1aaaa.txt%22%7D", "s3VersionId": "1", "s3BucketArn": "arn:aws:s3:us-east-1:0123456788:awsexamplebucket1" } ] }
JSON内のs3Key
にはCSVファイルの2つ目のフィールドのURLエンコードされたJSON文字列が入っているため、これをデコードすることで外から任意の値を渡すことができる。実際のコードは以下の通り(このコードではオブジェクトに対して実際に何か操作をしているわけではない点には注意)。
console.log('Loading function'); const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); exports.handler = async (event, context) => { var date = new Date(); console.log('invoke: ' + date.toISOString()); const task = event.tasks[0]; const payload = JSON.parse(decodeURIComponent(task.s3Key)); await _sleep(3000); const message = payload.newKey; return { invocationSchemaVersion: event.invocationSchemaVersion, treatMissingKeysAs: 'PermanentFailure', invocationId: event.invocationId, results: [{ taskId: task.taskId, resultCode: 'Succeeded', resultString: message }] }; };
使い所が難しいが、例えばオブジェクトを全然違う名前にリネームしたい場合に、元のkeyと新しいkeyを渡してリネームする、みたいな場合に使うと良いと思う(実際そういう場面で使った)。リネームに近いけど、オブジェクトを整理するために、移動先のディレクトリを指定する、みたいな使い方もできると思う。
余談
ここまで試していて気づいたのだけど、実はS3 バッチオペレーションでLambdaを使うような場合、(このエントリを書いている時点では)実際にオブジェクトが存在しなくてもよく、マニフェストファイルさえ作れたらよい。つまりS3オブジェクトとは全く関係のない、任意のJSONをURLエンコードしてマニフェストファイルを作ってバッチオペレーションのジョブを登録するだけで大量にLambdaを実行させまくることができる。どういう場面で使えるかは全然わからないけど・・・
Lambdaの同時実行数
実際にLambdaを使うようなジョブを実行する場合、Lambdaの同時実行数には注意が必要。Lambdaからレスポンスが返って来たら次のを実行するという感じではなく、レスポンスが返る前に次から次にLambdaを実行していくため、油断していたらLambdaの同時実行数の制限に引っかかってしまう、ということも起こりそう。特にS3 バッチオペレーション以外でもLambdaを普通に使っているようなサービスの場合、通常のサービスに影響しうるので注意したほうが良いと思う。ちなみに上のコードで実際に10000件のオブジェクトに対してバッチオペレーションを実行した場合、LambdaのConcurrent executionsがMAX250くらいまで行った。
バッチオペレーションの使用感
「大量のオブジェクト操作をできるだけ素早く終わらせたい」というのがこれを使う一番のモチベーションだったため、実際に使ったときの処理時間をざっくり書く。
- 27000件のオブジェクトコピーはほぼ一瞬で終わる
- 2000万件のオブジェクトコピーは7時間弱で終わる
- 500万件のLambdaを利用したオブジェクトのリネームは2時間程度で終わる
ということで、何をやるにも結構早く終わらせることができる。大量のオブジェクトを操作する時に自分でバッチ処理を書いたりすると考えることが多い。並列実行の仕組みを用意しない場合、量が多ければ多いほど処理時間は増えていくだし、失敗した場合のリカバー方法も検討する必要がある。S3バッチオペレーションを利用する場合、そういう心配が完全には消えないものの、バッチ処理を書かなくてもよくなるため、だいぶ緩和されると思う。特に処理時間が短くなれば精神的にもだいぶ楽になる。レポートを出力しておけば、後からは失敗したものだけ再実行する、というのも簡単にできる。標準でできる処理は少ないが、Lambdaを使って様々操作できるのはいい。準備は若干面倒な部分もあるし、常用するのには向いていないが、いざというときには検討してみてもいいと思う。
まとめ
ということでS3バッチオペレーションを使う上で最初に知ってたらよかったみたいなポイントをまとめた。S3バッチオペレーション自体は大技って感じで使うタイミングはなかなか無いけど、大量のS3オブジェクトに何かしたい場合にはぜひ検討してみてほしい。
明日のはてなエンジニアアドベントカレンダーの担当はid:stefafafanさんです。よろしくおねがいします!
■
このゴール、というよりはゴールセレブレーションのことははっきり覚えている。 当時珍しくテレビで放送されていたので(今みたいに全試合生放送されていたわけではなく、見に行かなかった試合のゴールはダイジェストで見るしかなかった)かじり付きで観ていた。 たぶん家で夕食を食べる直前くらいだったと思う。「もう夕食だぞ」と言われたけど、食卓につくとテレビの位置的に試合が観られなくなってしまうので、生返事しながら観ていたと思う。
ぼんやりと覚えているのは、このペットボトルの水が「実は名古屋の選手用の水ではない」というエピソードで、たぶん翌日の新聞に書いてあったか、その日の実況が言っていたような記憶がある。 しかしこれが25年前なの衝撃的だな・・・あとピクシーをはじめこの試合に出ている選手が例えば監督として何年もやってたりして歴史を感じる。