YAPC::Hakodate 2024で「データマイグレーションの成功戦略:サービスリニューアルで失敗しないための実践ガイド」という話をしました

少し、いやだいぶ時間が経ってしまったのですが、YAPC::Hakodate 2024で登壇してタイトルの話をさせていただきました。資料は以下です。

speakerdeck.com

ごく一般的なWebエンジニアならエンジニア人生で一度か二度あるくらいだろうサービスリニューアル、それに伴うデータ移行を、どういう因果か大きいもので3回も経験することができたので、その3回でどういうことを学び、どういう改善をしてきたか、という話をしたつもりです。したつもりですというのは、あまりうまく話せなかったなという反省があり、もしかしたら伝わっていないかもなと考えているからです。ちなみに「大きいもので3回」と書きましたが、細かいものや前職のものを含めると6回くらいはありそう。それだけリニューアル系の仕事に関わることができているので、振り返ってみるとラッキーだなとも思います(これは本当に思っている)。

ということでトークの内容を少し補足します。

データの「把握」は考古学

この辺りは残りの時間がほぼなくなっていてめちゃくちゃ駆け足になってしまったんですが、言いたかったのは「実際どういうデータがあるかはスキーマや仕様をみるだけではわからない」ということでした。さすがにスキーマとは違うデータが入っていることはないんですが、準備できたぞということでいざ実際にデータ移行を実行してみると、仕様から外れていたり、ドキュメントに残っておらず、想定外のデータが入っているということが経験上ほぼ100%起きています。内訳は(主観ですが)「当時はそうせざるを得なかった」というような、いわゆる歴史的経緯によって生み出されていることが多いのかなと思います。

例えば、サービスに新機能を追加するので「DBの既存のテーブルにカラムを追加したい」ということはよくあることだと思います。ではそのカラムをNOT NULLなカラムにして、今後追加/更新されるデータは確実に何かしらの値が入るとして、更新されないかもしれない既存のデータはどうするでしょうか?少なくともカラム追加の時には何かしら入れておく必要があるでしょう。それを何かしらの値で埋めておいて、コード側でエラーにならないように少し手を入れるということをやったことはないでしょうか?あるいは日付カラムなので、エラーにならないように遠く未来の日付にしておく、みたいなことをやったことはないでしょうか?ちなみにこのように運用上必要になったなど、何らかの理由で手で追加された・更新されたデータのことを「地層」と勝手に呼んでいます。

実際どう対応するべきかについてはここでは言及しませんが、それらの対応自体はその時点ではベストな対応だったのです。あるいはもしかしたら苦渋の対応だったのかもしれません。しかし時間の経過と共にチームからそういう知識とか事情が失われ、あとから見た時に少し不思議だったり、想定外な状態に見えるということが起きるのだと思います。こういうものは埋もれていることが多く、運良く(?)何かしらの不具合だったりなどと一緒に顕現することもありますが、掘り返さない限りどんどん埋もれていきます。掘り返すタイミングが、不具合だったり利用者からの問い合わせだったり、リニューアルに伴うデータ移行だったりするのです。 当日のトークではこういう手で入れるタイプの特殊なデータは作らないよう努めるのも必要という話をしましたが、実運用ではかなり難しいでしょう。何かしらやりようはあるかもしれませんが、特にほとんどのWebサービスは変わっていくことが前提のはずで、どちらかというと、チームから知識が失われないように努力するほうがいいのかもしれません。なんにしろ自分がいなくなってもサービスやその機能が続く限りはデータは残る、ということは意識するのがいいと思います。この辺の話にも思うことがあるけど、まあまた別のエントリで。

現況が優先される

過去にどういう事情があって今の状態になっているかわからないこともあるものの、移行するにあたっては、今の状態から新システムに移すにあたっての困り事を解決しないと進めません。過去の事情を調べることは重要な工程ですが、一方で移行しようとしている時点でデータの状態を見た時、もう必要のない要件だったり仕様だったりするかもしれません。そういうものは捨てていくのがよいでしょう。過去の何らかの事情で不思議な状態になっていたとして、その過去の事情を新しいシステムに持ち込む必要があるかという判断が必要です。それもデータ移行時に必要な作業です。これは新しい賃貸物件や不動産を探す時にたまに見る「現況を優先する」という概念に似ているなと思うことがあります。

なんか長文になってしまった。トークでも話したのですが、もしもサービスリニューアルに伴うデータ移行をやることになったらこのことを思い出してもらえたら嬉しいです。

おわりに

ここからはトークの補足ではなく感想です。自分がYAPCに初めて参加したのは2017年のYAPC::Fukuokaでした。それまで大きい技術イベントにそれほど参加したことはなかったのですが、その時すごく刺激をもらったし、いつかはこういう場で登壇したいなと強く思ったのを覚えています。一方で自分にはあまり縁のないことかなとも思っていました。YAPCで登壇できて、一つ目標が達成できたのは良かったなと思います。もう少し上手く話せたら良かったなという反省はありつつ、それはまた次回以降ということで...

約3ヶ月も寝かせてすみません。気がついたら2024年から2025年になってしまった... かなり今更ですが、参加者の皆さん、スタッフの皆さんお疲れ様でした。また次回以降にお会いしましょう。

Next.jsでNEXT_PUBLICプレフィクスの環境変数をできるだけ避ける

あけましておめでとうございます。もう2025年とは早いですね。まだ2024年くらいの気持ちです。不思議ですね。

年が明けてやりたいことといえば今年の目標を立てる、という方が多いかなと思います。僕はそういう人間ではないというか、目標を立てても三日坊主とは言わないまでも1ヶ月くらいで目標とはなんだったのか、となってしまうことが多いので、近年は目標は意識していません。 そんな僕でも今年は意識していきたいことが一つあります。それは「Next.jsでNEXT_PUBLIC_のプレフィクスをつけた環境変数は乱用しない」です。同じような目標を立ててる方もきっと多いことでしょう。ということでその話を書いておきます。あとここからは「ですます調」ではなくなります。

Next.jsの環境変数

このエントリではNext.js 15 + App Routerについて書いている。環境変数については、まあ基本はここに書いてある通り。

nextjs.org

何かしらの設定を入れたいときに環境変数を利用したいというのはまあ当然だと思う。Node.jsだと環境変数process.env.FOO_ENVというように参照できるが、client componentでこれをやるとHydration Errorが発生する。具体的には以下のコード

'use client'

export default function Client1() {
  const apiUrl = process.env.API_URL;

  return (
    <>
      <div>
        <p>client</p>
        <p>API_URL: {apiUrl}</p>
      </div>
    </>
  );
}

考えてみるとこれは当然で、process.env.FOO_ENVはサーバーサイドでは環境変数を参照できるけど、クライアントサイド(つまりブラウザ)では参照できない。そのため、サーバーサイドとクライアントサイドで状態不一致となってしまうため、エラーとなる(と理解している)。

NEXT_PUBLIC_というprefixをつけた環境変数は、build時に静的に置換することでこの問題を解決する。

nextjs.org

問題

わかる人にはもうわかると思うけど、build時に解決されてしまうので、ランタイムで値を差し替えることができない。つまり、NEXT_PUBLIC_環境変数を利用する場合、値の内容によってはひとつの成果物を複数環境で利用できなくなってしまう。これは困る。自分達はNext.jsのstandalone buildで生成したものを含んだDockerイメージを作り、それをECSだったりk8sだったりで動かすことが多いが、build時に静的に埋め込まれてしまうとなると、単一のイメージを使えなくなってしまう。でも俺たちは単一のイメージを使いたいんや!!!

ということでNEXT_PUBLIC_環境変数を諦めることとする。そのためには「環境ごとに変わる環境変数」をどうやってロードするかを解決する必要がある。

解決方法

解決方法といいつつ、実は自信を持ってこれだ、という解決策は持っていない。基本はサーバーサイドで環境変数を読み、それを何らかの方法でクライアントに渡す、というやり方になる。

コンポーネントからクライアントコンポーネントに渡す

素朴だけど、以下のようにprops経由で渡す。要するに環境変数が参照できるところで参照して、その値を渡していく。

  • Client Component
'use client'

export default function Client1({apiUrl}: {apiUrl: string}) {

  return (
    <>
      <div>
        <p>client1</p>
        <p>API_URL: {apiUrl}</p>
      </div>
    </>
  );
}
  • Server Component
export default async function Home() {
  await connection()
  const apiUrl = process.env.API_URL!;
  return (
    <>
      <Client1 apiUrl={apiUrl}/>
    </>
  );
}

Server Actionsを使う

設定値を取得するだけのServer Actionsを用意して、それをクライアントコンポーネントから呼び出す。環境変数自体はサーバーサイドでロードする。

'use server'

export async function getApiUrlFromEnv() {
   return process.env.API_URL
}
'use client'

import { getApiUrlFromEnv } from "@/app/actions";
import { useEffect, useState } from "react";

export default function Client1() {
  const [apiUrl, setApiUrl] = useState<string | null>(null)

  useEffect(() => {
    getApiUrlFromEnv().then((url) => {
      setApiUrl(url || '')
    })
  }, [])

  return (
    <>
      <div>
        <p>client2</p>
        <p>API_URL: {apiUrl}</p>
      </div>
    </>
  );
}

正直どちらの方法も一長一短あると思う。個人的には前者のほうが好きだけど、コンポーネント間で設定を引き回し続けるということになり、コードの見栄えは悪くなると感じる。ある程度整理はできそうだけど。後者は設定を読むだけでリクエストが一度増えるのが気になるけど、大したリクエストでもないしあまり気にしなくてもよいかも。

もっといい方法ありそうだなとは思っているので。よさそうなやり方を知っている方がいれば教えて欲しいです。

ということで

昨年これを何度もレビューで指摘されて、そうだったそうだったとなっていたので、今年はそうならないようにしたいですね。

唐突なんですが、このエントリは はてなエンジニア Advent Calendar 2024 32日目です。2025年の目標を書いたのに2024年です。だからまだ2024年の気持ちだったのかもしれませんね。ちなみにもう少し続くらしいです。去年も「もう少し続くんじゃ」みたいなことを書いた気がします。なんにしろ今年もよろしくお願いします。

ISUCON14に即興パフォーマンス2024(仮)というチームで出た

今更ですが。

id:akiymid:hitode909 と即興パフォーマンス2024(仮)というチームで出た。結果は26位で、初めて30位以内に入ることができた。

isucon.net

このチームで出たのは2回目で、今回は僕が初めにakiymさんを誘って、せっかくなら前に出たチームで出ようという話になったのでhitode909さんを誘って実現した。前回の様子は↓

blog.sushi.money

前回とはそれぞれ状況が変わっていて、事前に集まって準備するとかそういうのは少し難しかったのもあり、当日集まってぶっつけ本番でやるということになった。実際本当にそんなに準備してなくて、前日になってcosenseのチームのプロジェクトを作ったり、当日朝にこれまでやったことをちょっと集めたりしたくらい。 どの言語で参加するか、というのも集まってから、というか始まった後に各言語の参考実装を眺めた後に決めた。3人が共通して読み書きできるのがPerlとNode.jsとGoで、ざっと眺めた時に把握しやすそうなのはNode.jsだよね、となってNodeに決まった。

初動の準備は自然と分担して、ログ設定を仕込んだり、デプロイする方法を準備したり、ドキュメントを眺めたりできた。それぞれそんな難しいことはしていない。たとえばログは、nginxにalpで集計可能なフォーマットでログを吐き出す設定を入れるとか、デプロイは手元からrsyncだよね、とかそれくらい。そのあとは合流して、ベンチマークを一度走らせた上でアクセスログを集計して様子を見るとか、ベンチマークの出力を見たりして作戦を立てたりした。ベンチマークの出力を見たところ、ユーザーの満足度が低く、というか誰も満足していませんみたいな状態だったのでそこを改善していきましょう、となった。というか基本的にはベンチマークの出力を見て作戦を立て続けていた気がする。どういうことをやっていたかはhitodeさんのブログを見てもらうといい。

blog.sushi.money

当日はほぼ3人でモブプロしてるかペアプロしてるかみたいな状態だった。完全に分担して作業したのは最初の1時間くらいだったと思う。これは結構よかった。実装だけではなく、ログを仕込んで、ベンチを回しているときにログが流れる様子を見てこれいいじゃんとかこれ変じゃんとか話しながら作業していた。これはかなり良かったと思うし、何より一日すごく楽しかった。

hitodeさんも書いているけど、リーダーボードを全く見なかったのも結果としてはよかった。あとでわかったのだけど、リーダーボードが隠れる前の順位も30位周辺だったらしく、仮にそれを知ったら残り1時間でRedis入れようぜとか言い出していた可能性が高い。ちなみに残り1時間のところでSSEやりはじめたのだけど、結局うまくいかなかった(Failするようになってしまった)ので最後にやめた。SSEやったきっかけ自体が「他のチームは絶対SSEやっているはずで、やらなかったら後の感想戦で寂しい思いをするはず」とかだったので、当初の目的は達成できたと言える。

普通の日記レベルの振り返りになったけど、まあとにかく楽しかった。30位以内に入れたのが初めてだったのも理由だけど、hitode909さん / akiymさんと久しぶりに一緒に何かできたのも楽しかった。問題もとても丁寧に作られていて面白かったし、楽しい一日を過ごせたと思う。関係者の皆さんお疲れ様でした & ありがとうございました。

古いコードに向き合い、未来に何を遺すか

ここ数年は仕事で「最後のコミットが10年前」みたいなコードを触ることが多く、古いコードに対してどのように向き合うかと同時に、 コードを長く維持していく上でどのいう振る舞いをするとよいかを考えることが多くなった。 年末なので、自分が特に最近意識していることをを紹介する。

要らないコードはさっさと消す

年末といえば大掃除、ということで年末らしい話題。普段仕事をしている中で「これは使われてなさそうだけど、消していいかわからないな」とか、「これは今は使わなくなったけど、残しておいたらあとで使うかもしれないし残しておく」という場面がある。 消すためにもちょっと調べないといけないし、消すより残しておいたほうが安全だし、面倒なので残しておくか・・・ということをやったことはないだろうか。僕はある。 しかし必要のないコードなのであればさっさと消したほうがよい。現代だと大抵gitなりなんなりで管理されているのだから、必要だったら過去コミットから拾ったらいい。

事情を知っているメンバーがいるうちはいいが、気がついたらメンバー全員が入れ替わっているということもある。もしかしたら2周くらいしてることもあるかもしれない。 そうなった時に事情を知る人がいなくなり、あるいは残っているけど忘れてしまったということが起こり、より消しにくくなって、気がついたら最初に想像していた以上にコードが増えてしまうということもあるだろう。 コード量の多いリポジトリは新メンバーへの導入の心理的なハードルが高くなる。部屋を片付けてくださいと言われ、物が少ない部屋と、仮に整理されていたとしても物が多い部屋だと、後者に感じると思う。

消すことで得られるものは大きい。消すことでコードの見通しがよくなるし、全体のボリュームをできるだけ小さく保つ方向に力が働いて、より把握しやすくなる。 しかし使わなくなったと判断したその場で消すのが一番簡単だと思う。消していきましょう。

コミットログには変更の理由を残す

VCSで管理されているコードに対してなんらか修正したあとに「fix」みたいなログでコミットすることないですか。僕も昔はそういうことを結構やっていた。 しかし10年後にそういうログが残ったコミットを見たら「修正したのはわかるんよ」ってツッコミたくなると思う。ログを追っているのは変更した理由を知りたいからで、10年後にfixとだけ書いてあったらものすごくがっかりするだろう。 よほど単純な修正じゃない限り、ちゃんと理由を残すとすると、どの順番で修正するかというのを先に考えるとか、どういう粒度でコミットするとかを最初に検討することになり、結果としてコードの品質も上がるだろうと思う。

とはいえ試行錯誤の過程で雑なコミットが増える、みたいなことも普通に起こる。gitを使っているなら後から改めて修正することもできる。自分では後で全部の修正を見返して、コミットをまとめたり入れ替えたり、みたいなことをやっている。 あとうっかり同じファイルに対して複数の修正を一つのコミットに含めそうになってしまう場合とかはgit add -pでコミットする範囲を選んでコミットするとかして、粒度を調整するとよい。

【余談】サービスの寿命よりもチームの人員の入れ替わりのほうが早いことがある

これは振る舞いの話ではないけど、サービスが未来永劫同じメンバーで開発・運用され続けるとは限らない。メンバーが入ってくることもあるし、何らかの事情でいなくなることもあるだろう。 気がついたらメンバーが全員入れ替わっているということも起こる。当然引継ぎはされていくだろう。常に100%完璧に引継ぎされているのであれば問題ない。実際には良くて8割〜9割くらいで、残りはコードや残されたドキュメントから読み取ることになる。そうして何年かすると、考古学のように様々な資料から過去の経緯を推察する、ということになる。長く続けばそうなっていくのは仕方ないと思う。

だからこそ、ドキュメントにしてもコミットログにしても、あるいはコードのコメントにしても、今いる人や直近でチームに入ってくるだろう人向けにはもちろん、 今のチームメンバーが関わることがない未来のチームに対して残す、という意識で書くのが良いと思う。方法論ではなく意識の話になってしまうが、これは明日、つまり新年からでもできる。

ここまで技術的な話というよりは振る舞いの話に終始したけど、自分が大事だと考えている振る舞いについて紹介した。こんなの当然でしょという人たちはそのまま継続してほしいし、 もしもこんなこと意識してなかったという人がいれば参考にしてほしい。ここで紹介した振る舞いでもしかしたら10年後の誰かを助けることができるかもしれない。

ということで

無理やりエンディングって感じだけど、このエントリははてなエンジニア Advent Calendar 2023 - Hatena Developer Blogの31日目のエントリだったのでした。ちなみに昨日はid:stefafafanエンジニアリングマネージャーの4領域はEM以外のメンバーでも濃淡はあれど意識する必要がある - stefafafan の fa は3つですというエントリでした。

2023年の大晦日だから今日で最後かと思ったけど、もうちっとだけ続くんじゃということで 2024年になってももう少し続くようです。明日はid:nakatakiさんとのこと。お楽しみに。

ぼく個人の挨拶としては「来年もよろしくお願いします」ということで。

YAPC::Kyoto 2023にスタッフとして参加した

ということでスタッフとして参加した。その後も個人的にやることがたくさんあってエントリかけてなかったけど...(これは言い訳です)

京都でYAPCをやると聞いた時からスタッフとして参加したいと考えていた。自分が住んでいる京都で開催されるなら、せっかくならイベントをつくる側に回りたいと思っていて、YAPC::Kyoto 2020のスタッフを募集しているのを見かけて応募したのが始まり。YAPC::Kyoto 2020は結局延期されてしまったけど、YAPC::Kyoto 2023としてrebootするときに改めて声を掛けてもらって、スタッフとして参加することができた。無事に当初の願いが叶ったのだった。

色々やらせてもらったけど、目にみえるところだと個人スポンサー向けのノベルティの企画立てたりした。

blog.yapcjapan.org

裏話だけど、最初はタンブラーではなくて、全然違うものを電電宮で祈祷してもらってノベルティにしようということを企画していた。ただ企画を進める途中でタンブラーを見つけて、この方が普段使いできそうだなと思ってタンブラーに変えさせてもらった。せっかくなら普段から使えるものがいいだろうし、今回のようなタンブラーならコード書いてるときに横に置いてもらえるんじゃないかと思う。キラキラステッカーの方は電電宮で祈祷してもらったけど、祈祷するにあたり電電宮にメールしたりして調整を進めたりした。寺社仏閣にメールする経験なんてないので毎回妙に緊張していた。結果としていい感じのYAPC::Kyoto 2023のOP動画も撮れたので良かったと思う。と書いているけど、実は祈祷当日は別の用事があって行けなかったのでちょっとざんねん。まあそのうち自分で行こうと思う。

今回はLTでも話すことができた。会社関連のイベントで登壇したことは何度かあるけど、ああいうイベントで、あれくらいの人数の前で話すのは実は初めてで、今までにないくらい緊張した。一つ経験できたのは良かったと思う。次はトークにも応募できたら良い。

正直なことを書くと、実は始まるまで「オフライン開催」自体に懐疑的というか、オンラインとどういう違いがあるかがあんまり想像できていなかったというか、遠くの人も参加できるという点ではオンラインにまだ分があると考えていた。でも、集まった人々があちこちでコミュニケーションとっているのを見て、ああこういうイベントはオフラインでやるべきだなと思い直した。久々に会ったとか、ネットではお見かけするけど実際に話すのは初めてみたいなコミュニケーションがあちこちで起こっているのは良かった。自分はそういう中に入っていくのは苦手で、今回は多少頑張ったと思うけど、次はもっと積極的に行きたい。

イベント準備から当日まで、自分も十分楽しかったけど、今回来てくれた人たちが楽しいと思ってくれたのであれば、自分にとってはそっちの方が成功と言える。来て良かったと思う人がたくさんいると嬉しい。

なんかだいぶ雑なエントリになったけど、とにかく楽しかった。実はトークは全然見られてないので、後でアーカイブでゆっくり見ようと思う。

GolangでSMTPを使ってメールを送る処理を書く

最近Golangでメール送信処理を書くことがあったのだけど、あまり事情を知らなかったのでまとめた。

golang+SMTPでメールを送る

Goには標準ライブラリでnet/smtpというのがある。

smtp package - net/smtp - Go Packages

これは名前の通りGoからSMTPでメールを送信するためのライブラリなのだけど、例えばヘッダとボディの間には空行を一行自分で挟まないといけないとか、素朴すぎて結構辛い。 さすがに2023年にもなってさすがにそういうことはやりたくないので、もう少しいいやつないかなと探して、今回は以下のライブラリを使った。

github.com

これはそこそこ高機能だと思う。少なくとも自分でヘッダ部とボディの間に空行を入れる、みたいなことをしなくてもいい。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サーバーを立てる方法。

github.com

これを自前でビルドするとかではなくて、DockerHubに既にイメージが存在するのでそれを使う。

registry.hub.docker.com

これがいいのは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_HOSTSMTP_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分あることになるが、これは少し長いかもしれないし、そんなことないかもしれない
  • 悔しい結果とはなったけどシンプルに楽しかった
    • 今回は一番ネックになっていそうな部分を順に直していきましょうというのだけ決めていて、その通りに動けたのはよかった

来年また出直します...運営の皆様お疲れ様でした & ありがとうございました。 

メンバーの振り返り

yashigani.hatenablog.com

wtatsuru.hatenadiary.com