TECH PLAY

株式会社モバイルファクトリー

株式会社モバイルファクトリー の技術ブログ

226

こんにちは、駅メモ!開発チームエンジニアの id:maeken2010 です 🙌 今回は ChatGPT と Raspberry Pi 5 を活用して自宅に NAS を構築した経験を共有します。ホームサーバー・NAS も初めてでしたが ChatGPT と一緒に構築ができました。 Raspberry Pi 5 先日、Raspberry Pi 5(以下、ラズパイ)を購入しました。ラズパイは言わずと知れたミニ PC です。YouTube でラズパイと電子ペーパーを組み合わせて天気ステーションを作成する動画を偶然視聴し、自分も挑戦したいと考えてラズパイと電子ペーパーを購入しました。 しかし、予定していた電子ペーパーの到着が遅れ、しばらくラズパイが手持ち無沙汰な状態に。このままラズパイがインテリアになるのは勿体なく、活用する方法を模索した結果ホームサーバーとしての利用を思いつきました。そこで目指したのが NAS の構築です。丁度家に余っていた HDD もインテリアになっていたので、再利用することにしました。 NAS とは? NAS(Network Attached Storage)とは、ネットワーク接続型ストレージのことを指します。PC やスマートフォンから、ファイルブラウザを通じてアクセスできるようにするためのものです。手元に余っていた HDD をラズパイに接続し、試行することにしました。 ラズパイとHDD NAS 構築の試行錯誤 ネットでググれば、ラズパイで NAS を構築するための情報は数多く見つかります。しかし、その過程で微妙な問題に直面しました。HDD は2台ありそれぞれ容量も違うので構成はどうするべきか、フォーマット形式はどれを選ぶべきか、バックアップの必要性、Mac の TimeMachine として使う方法など、自分のユースケースに最適な解決策が必要でした。 ChatGPT の活用 そこで、ChatGPT を使うことにしました。ChatGPT も言わずと知れた AI ツールです。ChatGPT を使うことで、上で挙げたユースケースに合う提案をしてくれることを期待しました。結果は予想以上に便利でした。 プロンプトについては特別なことはぜず、作業しながら疑問に思った事や躓いた事が出てきたら随時 ChatGPT へ質問しました。 カスタマイズされた提案 ChatGPT は、私のユースケースに合わせたカスタマイズした提案を行ってくれました。一度選択した情報を覚え、そのコンテキストに基づいて最適なアドバイスを提供します。 柔軟な対応力 設定作業を進める中で、同じ質問を何度も行うことがありましたが、その都度必ず回答してくれます。曖昧な聞き方をしても、コンテキストを踏まえた上で私が求める回答をしてくれるのは非常に助かりました。 信頼性の確認 ただ、ChatGPT が誤った情報を提示することも時折あるため、提案を受け入れる際には自身でもしっかりと確認を行う必要がありました。 例えば、CUI 上でのパーティション作成の操作手順ですが、この手順ではうまくいきませんでした。おそらく fdisk の仕様が OS によって異なるのかもしれません。今回は私物の Mac のディスクユーティリティでパーティション作成をしました。 プロジェクト機能 プロジェクト機能を活用することで、チャットログを体系的に管理できます。これにより、複数のチャットを 1 箇所にまとめることができます。 さらに、プロジェクトに対して ChatGPT の振る舞いを指示することができます。「ラズパイを使ってホームサーバーを構築しようとしている」ということを指示することで、プロジェクト上で新しくチャットを開始するとその指示を踏まえて回答してくれるようになります。 ちなみに、何も指示していない場合、なぜかテキストエディタは nano を使おうしてくるので、個人的に好きな vim を指定しました。 まとめ 今回初めて NAS を構築しましたが、ChatGPT のおかげで意外と簡単に進めることができました。 Mac からの接続も問題なく、TimeMachine でのバックアップも可能になりました。 新しいことに挑戦するときって、ちょっとした不安や疑問がつきものですが、信頼できるツールがあると気持ちが楽になりますね。 ChatGPT は技術的なサポートをしてくれるだけでなく、まるで相棒のように一緒にプロジェクトを進めてくれました。特に、カスタマイズされた提案があるおかげで、自分のペースで安心して作業を進められました。 「ChatGPT と一緒なら、新しいことに挑戦しやすい」と思いました。これからも ChatGPT を活用しながら、新しいチャレンジを楽しんでいきたいと思っています。次はどんなプロジェクトに取り組もうか、今から楽しみにしています 💪
はじめに モバファク 24 卒エンジニアの id:knj-mf です。 記事が出る頃にはすっかり 2025 年となってしまいましたが、仕事を始めて最初の年というのはかなり大きいものでした。 今では仕事にもある程度慣れてきましたが、就職前や入社直後の時期では、新卒としては働くことに関して漠然と「よく見えないなあ」という感覚があったことを覚えています。 そこで、5 人のモバファク新卒エンジニアそれぞれの視点で「何を通じて」「何を見て」「何を考えたか」を直に伝えることに価値があると考えました。 本記事では、新卒エンジニアが入社からの半年間で経験したことについて、5 人全員の視点でレポートしています。 これから入社を考えている人、また新入社員を受け入れる人にとっても「新卒から見える世界」を伝えられると良いなと思っています。 また、技術ブログはアウトプットの意味も大きく、我々の経験も兼ねています。win-win となることを期待しています。 24 新卒エンジニアの配属 (執筆時点) 24 卒エンジニアの 5 人 はそれぞれ業務範囲の異なる、別のチームに配属されました。この記事の執筆時点で下記のチームの配属になっています。 駅奪取チーム (konakawa) 駅奪取について、インフラからフロントエンドまで広範に携わる 駅メモ!・開発チーム・EVENT (oshima) 駅メモ!のイベント関連 駅メモ!・開発チーム・DUEL (yang) 駅メモ!のバトル関連 駅メモ!・開発チーム・SCALE (kinjo) 駅メモ!全体の体験拡大 駅メモ!・開発基盤チーム・native (r-hayashi) ストアで配信されるアプリ関連 書くこと 5 人の新卒エンジニアがそれぞれセクションを持ち、それぞれの視点・それぞれの書きたい流れで、本配属後について書いていきます。 形式を厳密に定めることはせず、各自の考え方を元にした書きたいこと・伝えたいことを優先しています。 新卒メンバー全体での新人研修、またはエンジニア全体での新人研修については今回の記事では触れません。 同じモバファクといえど、配属が違えば仕事の内容も変わってきます。新人研修以降それぞれの業務内容や体験が分岐してからのレポートということにしました。 新人研修についての記事はこちらです。 https://corpcomn.mobilefactory.jp/archives/7546/ https://tech.mobilefactory.jp/entry/2024/11/06/160000 駅奪取 konakawa の半年間 私は駅奪取チームに配属されました。 駅奪取チームは駅メモ!チーム全体と比べると少人数のチームで、企画側も開発側も個々人が手広くタスクを行う体制になっています。 私も配属してから幅広く業務を任せていただき、様々な経験をさせていただいています。 ここではその中の主要なものを取り上げて紹介し、また、チームや会社の雰囲気についても私の受けた印象を紹介したいと思います。 新人タスク 共通の新人研修を終え、駅奪取チームに配属されて最初は、ゲームそのもので遊んでみたりコードを簡単に追ってみたりする新人課題を行い、プロダクトを一通り知るところから始まりました。 その後は「新人向けタスク」という形で比較的易しいタスク、プロダクトの文脈が浅く取りかかりやすいタスクや、ユーザに直接影響の出ない管理画面の改修などのタスクがまとめられており、そのタスクを進めていくことで、チームでの業務に少しずつ慣れていくことができました。 コーディングの際は周辺のコードの書き方を模倣しつつ進めていましたが、駅奪取は 13 周年を迎えたプロダクトで歴史が長いため、現在では非推奨とされている書き方のコードが残っていたりして辛かったのを覚えています。 そういったものは適宜リファクタリングしながら進めていましたが、なぜ非推奨なのか、なぜ A ではなく B の書き方の方が良いのか、先輩方に助言を頂きながら試行錯誤することは、最初のステップとして良い学びの機会となりました。 そういった機会があったからこそ、未来の誰かが困らないようなコードを書こうという意識を今は強く持つことができています。 また、新人向けタスクを進める中で表示や仕様など企画側の方と相談する必要がある機会は多々あり、チーム内でのコミュニケーションという点でも必要な経験を積むことができました。 総じて、チームの受け入れ体制として、階段の一段目がしっかり用意されていて、経験が浅いところからでもジョインしやすい印象がありました。 プロジェクトへの参加 新人タスクを進めて業務に慣れてきた頃、駅奪取で新イベント形式の開発が進んでおり、そのプロジェクトのサポートに入らせていただきました。 新人タスクのように文脈が浅めで単発のものでないタスクに取り組むのはそのタイミングが初めてで、かつ、既にあるイベントの仕組みに変更を入れる内容でそれまでよりも込み入っていたので、はじめは想定よりも時間がかかったりして苦戦した記憶があります。 13 年続く巨大なコードベースに変更を加えるというのに慣れていなかったのも大きかったように思います。 すでにある仕組みの上に新しい仕組みを入れようとすると考慮漏れは起きがちになるので、不具合を起こさないよう、加えようとしている変更の影響範囲を調べたり、なるべく安全な変更にしたりすることに苦心していました。 ここで、そもそも変更しやすい仕組みにすること、負債が出ないような実装をしておくことを意識しようと身をもって学習できました。 現在進めているプロジェクトでもこの経験から、拡張性を持たせた実装を考えるように意識しています。 今振り返ると、早い段階でプロジェクトに参加する機会をいただき、そこで経験を積めたことは自身のスキルアップに大きく寄与したと思います。 そういった機会があれば、すなわち、やらなきゃいけなくなった時が、結局一番成長するんだなあと思いました。 コミュニケーションについて 私がモバファクに入社するにあたり不安だった点は、フルリモート環境でのコミュニケーションが上手くいくかでした。 学生の時にリモート中心の環境にいて、必要最低限のやり取りしかなく、質問や相談がしづらかったり、そもそもメンタルに良くなかったりという経験がありました。 それがあってモバファクではどうなるか心配していましたが、実際にはその懸念は杞憂に終わりました。 入社前後共に「心理的安全性」という言葉はよく耳にしていて、実際にその面でのサポートも十分にされていました。ランチ会や雑談の場を設けるなど、取り組みはさまざまなところで見られます。 少なくとも、フルリモートでもコミュニケーションを円滑にしようという共通認識があるだけで、かなり安心できるのではないかと思います。 また、質問などをする際の心理的障壁も小さい環境だと思います。 駅奪取チームでは、メンターさんと 1 対 1 の朝会・夕会が設けられていて、そこで雑多な話も質問もできる環境になっていました。業務に慣れていない時期でも、こうした質問しやすい場で些細な疑問も払拭することができました。 チームや業務に慣れてくるとオープンな場でも質問をしやすくなってくるかと思いますが、その状態まで自然な流れで移行できて、チームに入りやすい印象でした。 最後に 私はここまでの経験から、チャレンジングなタスクをする時が最も成長するように思いました。 駅奪取チームは大きくないチームなのもあって、フロントエンド・バックエンドのコーディングに加えてインフラ周りも見る機会があるので、様々なことに挑戦できる環境です。 現在は自分がメインで担当するプロジェクトにも挑戦させていただいています。 今後も積極的に幅広いタスクを引き受け、力をつけていきたいです。 また、様々なタスクに挑戦できる一方で、いきなり物凄いハードルを越えなきゃいけない訳ではなく、そこに至るまでのステップが整備されている環境であるとも思いました。 心理的にも取り組みやすく、新人でも参画しやすい環境なのではないかと思います。 駅メモ!開発 (EVENT) oshima の半年間 10 年の歴史がある駅メモ!では、多種多様な形式のイベントが存在します。 駅メモ! EVENT チームのエンジニアは、それらイベントの改修や、新規開発を行うのが主な業務です。 遊撃隊業務 駅メモ! EVENT チームに配属され、まず初めに携わったのが「遊撃隊」の業務でした。 これは日々の開発業務や、ユーザーからの問い合わせから発生する細かいタスクを拾っていく業務で、やること自体は軽微な修正だったり、直近のイベントの動作確認、分析用のデータを SQL クエリで抽出するなどが多いですが、 対応箇所はイベントの実装から広く浅くという感じで、何も知らない状態からイベントの仕様や実装を把握するのに適している業務と言えます。 機能開発に携わる 遊撃隊業務を経て、ある程度業務に慣れてきたところで、とある機能の開発を先輩エンジニアから引き継いで担当しました。 規模は小さいですが、開発する機能はユーザーと、イベントのコラボ先の関係者とのやりとりで使用される部分で影響範囲が普段より広く、そのやりとりのフローと駅メモ!の画面やシステムの挙動が一致しているかどうかは普段以上に気にかけていました。 もちろん実装面もそうですが、特に意識したのは、本機能に関わっていたプランナー・エンジニアとのコミュニケーションです。 遊撃隊業務の時から、社会人の基礎である「報連相」は意識して取り組んでいましたが、本機能については先述の通り何か手違いがあれば大きな影響を及ぼしかねない機能です。 そのため、実際に機能の仕様を考えているプランナーと、自分、また引き継ぎ元の先輩エンジニアとの間で、仕様の認識にズレがないか、また実際に自分が実装した内容についても、挙動が想定通りかを頻繁に slack 上で確認していました。 その後本機能がリリースされ、実際にユーザーに使用されはじめて数ヶ月経っていますが、特に問題が発生していないのは適宜コミュニケーションを取っていたおかげかなと感じています。 リモート環境について 入社前はリモート環境だとコミュニケーションや、タスク進行が大変そうという懸念がありましたが、配属から半年ほど経ち、規模の大きいチーム開発にも参加した上で振り返ると、これについては全く問題なかったなと感じています。 コミュニケーション面では、各プロジェクトの規模に応じて週に数回〜毎日夕会や、場合によってはエンジニアのみで集まる朝会を開いて進捗確認や相談をすることで、仕様やデザイン、設計について認識にズレが発生することを防いでいます。 タスク進行面では、ボードツールにタスク毎に設計を書き起こすことでロードマップ化し、エンジニアがそれぞれ対応するタスクを自ら決めるという形を取っています。そのため、「今、誰がどんなタスクをやっているか」「開発の進捗はどれくらいか」が可視化されていて、一目で見やすい状態になっています。 これらアクションは先輩方が過去のリモート環境での経験を元に改善していった結果のもので、そのお陰もあって滞りなくタスクが進められています。そういった理由から、リモート下でのコミュニケーションや、タスク進行については全く問題に感じたことは無いです。 「エンジニア」の領域は広い また、入社前のエンジニアのイメージと大きく変化があった部分として、エンジニアが担当する業務は何も設計やコーディングだけでなく、特に EVENT チームでは、イベントに関するデータ抽出や分析業務、仕様や実装に関する調査業務などもあり、業務が多岐に渡っているという点があります。 そして、そういった業務ができる機会は意外と多く存在していて、自分から「やってみたい」と声を挙げれば任せてくれる環境にあります。 自分自身、開発業務以外のスキルも幅広くつけていきたいと考えているのと、データを見たり分析したりするのは好きなので、最近はそういったタスクを積極的に取るようにしています。 勿論、技術基盤の改善など技術面を深掘りしたタスクも自ら課題提起して取り組むことができる環境です。 そう言った意味では、トライできる業務が幅広く存在するし、その中で自分に合ったタスクを見つけて追求していける点が良いところかなと思っています。 最後に 駅メモ!EVENT チームの一員としての半年間を振り返ると、面白い業務がたくさんあって飽きないな、という感想を持っています。 駅メモ!の一部を作っているという実感もありますし、実際に駅メモ!で遊んだ時にその一部を目にすると感慨深い気持ちになります。 また、それ以外にもイベント関連のデータ抽出・分析業務、仕様や実装の調査業務といった開発以外のタスクも対応することができて、これはこれで普段の開発とは大きく異なる作業でやっていて新鮮に感じます。 そして、入社前に感じていたリモート環境下でのコミュニケーションや、タスク進行などについての不安は特に感じることはなく、寧ろ作業環境を自分で作れる分、リモートワークの方がタスクに集中しやすいのかなとも思っています。 今後については、まだ EVENT チームが対応する業務について全て経験したわけではないので、挑戦したことのないタスクに挑戦し続けるとともに、その中で特に興味を持った分野のタスクを深掘りして、よりできることを増やしていきたいなと考えています。 駅メモ!開発 (DUEL) yang の半年間 入社研修を終えた後、DUEL チームに配属され、この新しい環境でのスタートに心躍らせると同時に、期待と不安が入り混じった気持ちでいっぱいでした。 この半年間、チームの定常業務に触りつつ、新機能の開発もしたことで、いろいろな経験を得ました。 さて、この半年間経験した印象的なことを紹介させていただきたいと思います! 配属後研修も引き続き 入社研修では技術研修の部分もありましたが、それは今後使用することのあるプログラミング言語や開発フレームワークなどの基礎知識の学習に限られており、実際の業務開発とは関連が多くないと思いました。配属前は、実際の業務開発に対して不安を感じていましたが、配属後にも新人向けの研修が続きました。 この研修では、実際にゲーム内に小さな機能を追加することを通じて、ゲームプロジェクトについて初歩的な知識を得ました。また、実践を通じて入社研修で学んだ内容を再度強化することができました。これにより、今後の実際の業務内容について少し理解が深まり、プロジェクトのコードに徐々に慣れていくことで、当初の不安も消えていきました。もちろん、研修中のコードはチームの先輩たちがレビューしてくれました。このプロセスで、他のメンバーとのコミュニケーションが徐々に増え、自分自身がチームの一員として少しずつ溶け込んでいることを感じました。 チームの定常業務から実際業務を着手 配属後の研修が終わった後、チームの定常業務の開発に取り組み始めました。最初の開発内容は、実際には練習のためのもので、過去のでんこを再開発することでした。最初は、以前の研修と同じように、自分一人で開発ドキュメントに従って進めるものだと思っていましたが、今回の形式は非常に異なり、初めて「ペアプログラミング」という手法に触れることになりました。先輩と MTG で対面しながら直接コミュニケーションをとりつつ開発を行うというものでした。最初は少し気まずく感じることもありましたが、開発内容に集中することで、しばらくその感覚を完全に忘れてしまいました。 実際、その後の業務で私はこの方法がモバファクで広く使われていることに気付きました。特に難しい問題に直面したときには、すぐに誰かを見つけて一緒に解決することができます。分からないことがあったり、アイデアが浮かんだ時にすぐに相談できるし、フィードバックを得られるので、この方法は私にとって非常に効率的でした。特に新人にとっては、何もかもが不慣れな段階を素早く乗り越えるのに非常に役立ちました。助けてくれたモバファクのメンバーたちに心から感謝しています。 新機能の開発 定常業務以外にも、この半年間で 2 つの新機能の開発に参加しました。定常業務では新しい内容の開発も行いますが、既存のフレームワーク内での変更や設定の追加に限られることが多いです。しかし、新機能の開発は大きく異なります。新機能の企画案の共有から始まり、実際の機能が完成してプレイヤーの前に登場するまでのすべての段階に参加することができ、多くの技術に限らない知識を得ることができました。たとえば、プロジェクトから作業タスクへのブレイクダウンや各部分の工数見積もり、API などのドキュメント作成、バックエンドとフロントエンドの開発、他のチームメンバーとの共同作業など、多くのことを学びました。 モバファクのエンジニアは、ほとんどバックエンドからフロントエンドまでの開発ができます。フロントエンドの開発の経験が全くなかった私にとって、最初の新機能開発では多くのフロントエンドの経験を積むことができました。その後すぐに参加した 2 つ目の新機能の開発では、フロントエンド部分の開発における抵抗感が明らかに前回よりも少なく感じました。もちろんフロントエンドの開発は単に機能を実現するだけではなく、デザイン面も関係しているため、デザインチームのメンバーと何度も確認や修正を行う必要があります。この面では非常に多くの時間を費やしましたが、ここをしっかりと磨かなければ、プレイヤーに完成度の高い作品を提供することはできません。半年間にわたる新機能開発を通じて、フロントエンド開発の業務についてより深い理解を得ることができました。 大きなチャレンジ 自分はこれまで実際のフロントエンド開発を一度も行ったことがありませんでしたが、ティザーサイトを開発するというタスクがあったとき、自分を試すいい機会だと思い、これを通じてこの分野の知識と経験を補いたいと考え、ぜひこのタスクをやらせてほしいと申し出ました。 今振り返ると、これはほぼ CSS だけを書くプロジェクトでした。体系的に CSS を学んだことがなかった私にとって、まるで謎解きのようなものでした。そのため、このサイトの開発は、同時にパズルを解くようなプロセスでもありました。ドキュメントを何度も調べたり、AI に質問したりすることで、一歩一歩作業の進捗を進めていきました。 進捗の都合で、最終的にサイトの開発を自分一人で完了させることはできず、少し残念でした。しかし、先輩たちの手厚いサポートのおかげで、サイトは予定通りのタイミングで無事に公開することができました。今回のタスクを通じて、開発の技術だけでなく、先輩たちの行動を観察する中で、プロジェクト管理や特殊な状況の対応方式についても学ぶことができました。この経験は、チームの一員として今後活動していく上で、技術以上に貴重な知識となりました。 最後に 振り返ってみると、この半年間は私にとって非常に価値のある時間でした。 技術面だけでなく、問題解決やチームとの協力といった、多方面でのスキルを磨く機会に恵まれました。特に、未経験の分野にチャレンジし、困難を克服する経験は、自信を育むと同時に、自分に足りないものを見つめ直す貴重な機会でもありました。 以上、貴重な経験と知識をいただいた半年間のご紹介でした! 駅メモ!開発 (SCALE) kinjo の半年間 新卒エンジニア全体・駅メモ!配属エンジニアでの新人研修の後、SCALE チームでの仕事が始まりました。 初めはチームに慣れるまで KAIZEN と呼ばれるタスクを進め、その後から実際の開発に取りかかるようになりました。 1. 小規模開発 まず初めに携わった開発プロジェクトは、比較的小規模な新規機能開発でした。 エンジニアとしては自分ひとり、他はプランナーの方とデザイナーの方という構成で進めることになりました。 とはいえ、最初の仕事なので自分にはチームの先輩エンジニアの方がひとり付いて見てくださることになりました。 自分の中では、エンジニアには「既に決められた仕様やデザインをコードに落とし込んでいくことがメイン」のようなイメージを持っていましたが、実際は「開発にあたってコード周辺を何とかする役割」であって、開発メンバーとして、他職種の方と逐次連携を取りながら進める必要があることに気付きました。 コードを追っていく中で仕様漏れがあることに気付くことがあればそれを相談したり、「この仕様は実装が煩雑になりすぎるな...」という場面ではオミットできないか相談したりしました (プロダクトに必要なものであるので外せない、というものも当然あります)。 エンジニア内々では「この処理はライブラリの方に任せています」で話しを終わらせてしまうところも、ちゃんとライブラリではどういう処理をしているのかについて把握して伝える必要がある場面もありました。 実際の作業内容としてはバックエンドでは API エンドポイントを 2, 3 程度定義して対応するロジックを書き、フロントエンドからそれを呼んで、結果に応じて出すダイアログを実装する、程度の小さいものでした。 新人の初めての開発ではあるのでスピード感はなかったのですが、基本的な開発フローを見ることができました。 小規模開発でしたが、複数の職種が集まって動くチームでの開発を経験できました。 2. 中規模開発 (チーム開発) 上述のプロジェクトを終え、次のプロジェクトは中大規模開発への途中参加でした。 駅メモ!の既存の機能に大幅な変更を加え、複数の概念をひとつにまとめる...といった難度高めのプロジェクトです。 途中で一度プロジェクト自体を中断して別の開発を進めていたり、中断前後でメンバーが変わっていたりと、内容だけではない文脈の難しさもあるプロジェクトでした。 エンジニアの人数としては、先にプロジェクトに参加していた 2 人に自分が 1 人として加わる形で、適宜 1~3 人程度追加で作業に参加することもありました。 まず仕様書を読むことになったのですが、それ自体が 17,000 文字弱ある長大なもので、細かい画面遷移までみっちり書いてあり「プランナーはこんなところまで把握し、仕様を決めているのか」と驚きました。 デザインも同様に、細かいところまでほとんど実際の画面と同じように作られており、こちらもすごいものでした。 実装や仕様の全体を把握しきるまで、2 ヶ月程度かかりました。 開発に参加した初期の時点では、概要を把握していなかったために何度か手戻りが発生するタスクもありました。 開発では仕様を元に開発タスクを切り出し、チケットで管理する形式で進めていました。このタスクをチケットに切り出すというのは結構難しく、「想定していたより実装が煩雑であった」や「仕様の抜け漏れに気付き追加の工数が必要になった」など見積りと話が大きく違ってくることもありました。 特に自分が持ったタスクのなかで工数が膨らんでしまったものとして、管理画面 (中の人がお知らせの設定を入れたりする場所) の実装がありました。実際に使う人にヒアリングをしながら作業をしたのですが、色々なこと (perl のテンプレートから vue への移行など) が重なって当初の予定の何倍もの時間がかかってしまいました。 参加初期こそ難しさを感じましたが、プロジェクトに慣れてくると、バックエンドのクエリ最適化からフロントエンドのちょっとしたアニメーションまでさまざまなタスクを取ることになり、勉強になることも多かったです。 3. その他 (プロダクトと開発チーム) 駅メモ!は既に 10 周年を迎えるゲームであり、機能もすべて把握することが難しいほどある大規模なソフトウェアです。 駅メモ!開発において、自分が担当するような業務ではフロントエンド・バックエンドともに実装に関わることになります。 プロダクト特有の事情に関して幅広く知っている必要があり、まだまだ実装に際しては過去の実装や経緯の調査している時間も長かったり、先輩エンジニアに「こういった実装は過去に例があるか」「この実装が入ったときはどのようなプロジェクトのためで、どのような意図があったのか」を質問しながら進めています。 また、10 年もの期間があるとさまざまな開発負債が残ってしまうものですが、KAIZEN という取り組みによって負債を緩和しようとする土壌もあります。 今は他の開発があるから手が付けられないけど、これは KAIZEN に回して覚えているうちに直しておこう、といったことができるのは心理的安全性に繋がっていると思います。 負債は避けられないもので、ライブラリの互換性の無いバージョンのために大規模な移行がおこなわれたりするものです。なんとか開発を止めずに、コンフリクトを抑えながら移行しようとした結果難しい構成になってしまっている箇所も多々あります。 負荷が高そうな一方で、個人的にはこういった点は分かりやすい改善点であり、チーム全体に改善提案をする機会ともなって、学習のモチベーションにも繋がっています。 開発は止まるものではなく、コードやテストなどは物凄い勢いで増えていきます。 一方、開発チームの雰囲気としては「このモジュールを試験的に導入してみたい」や「新しく規約としてこういったルールを定めたい」などの提案はかなり受け入れる姿勢を持っています。 普段の開発プロジェクト外でも KAIZEN やチームへの提案という動きができる環境なので、作業が並列することもありますが、こういった運用などを考えるのが好きな人もハマるような場所だなと思います。 最後に 自分がチーム配属から経験した、小規模開発・中規模開発・その他について振り返りました。 他にも、技術広報のグループに参加するといった機会もありましたが、自分はエンジニアとしては上記のような経験をさせてもらいました。 駅メモ!開発基盤 (native) r-hayashi の半年間 新卒エンジニア全体の新人研修を終えた後、私は駅メモ!開発基盤の native チームに配属されました。チームへの配属後は、新しい学びの連続でした。 ネイティブアプリ開発の学習 native チームはその名の通り、Android や iOS といったネイティブアプリの開発・運営を担当するチームです。新人研修ではネイティブアプリ開発に触れなかったため、配属後には Android と iOS に関する開発について新たに学び始めました。 native チームでは研修形式ではなく、公式の教材を活用した自習形式が主な学習スタイルでした。Android においては、最新技術を習得できる「 Compose を用いた Android アプリ開発の基礎 」を学習しました。このチュートリアルでは、Kotlin の文法学習に始まり、シンプルな画面表示から、複雑な画面遷移のアプリ作成などを学びました。また、非同期処理を用いてインターネットからデータを取得する方法も習得しました。 同様に、 iOS については「 SwiftUI の公式チュートリアル 」を利用して学習を進めました。こちらでは、1 つのアプリを作成する過程で、SwiftUI を用いて静的な画面の作成やデータの状態による動的な画面作成方法などを学びました。 学習ペースは自身で調整でき、私は約 1 ヶ月で学習を進めましたが、わからないことがあればすぐに質問できる環境が整っていたため、不安なく学びを続けることができました。Android と iOS の最新の実装方法を習得しましたが、日々の業務では従来の技術がまだ多く使われているため、学んだことを活かし、最新技術への移行も進めていきたいと考えています。 本格的に業務開始 学習が進む中で、native チームの業務を少しずつ教えてもらえるようになりました。最初は簡単なタスクから始まり、アプリアイコンの更新や BGM 追加などを担当しました。一連の作業を見学した後、実際に同様のタスクが発生した際に挑戦する形でした。最初は、正確に覚えているか、上手にできるかという不安もありましたが、豊富なドキュメントが用意されており、それらを確認しながら作業を進められたので安心して取り組むことができました。 業務に慣れてきてからは、非推奨・廃止予定の実装方法に対する警告への対応や、コードフォーマッターが適用されていない箇所の修正など、開発効率の向上に貢献するタスクや、UX を考慮した機能改善に積極的に取り組みました。これらはすべて自ら「やりたい」と声を上げて始めたものであり、問題を見つけて解決する貴重な経験となりました。 学習が一段落した後は、より重要なタスクとして、アプリの新バージョンのリリースやライブラリ、Kotlin のバージョンアップなどを担当しました。これらの更新作業は安定した運営の要であり、開発基盤チームの重要性を改めて実感する良い機会となりました。学生時代のハッカソンでのアイデア重視の開発とは異なり、安定した運営を意識した開発は初めてで、とても新鮮な経験でした。 チームでのコミュニケーション 私が配属された native チームは、他のチームに比べて少人数ですが、静かで落ち着いた雰囲気の中、気兼ねなく話しやすい環境があります。チームメンバーの休みが重なると一人で業務をこなす日もあり、その場合には他チームからの依頼に少し緊張することもありますが、これは慣れの問題で、むしろさまざまなことに対応できるようになる良い機会だと捉えています。 日々の業務の中では、朝と夕方にミーティングが設定されています。この時間に進捗の共有や相談、その他重要な情報の共有を行うことで、全員が日々の進行状況をしっかり把握できるだけでなく、迅速にフィードバックを受けることができます。また、業務中に生じた疑問や相談内容は、ミーティング以外にも伝言用の Slack スレッドでやり取りできます。内容が複雑で対面の方が適している場合にはオンラインミーティングを利用します。これらのおかげで、何かあっても孤立せずにすぐに助けを求められるという安心感があります。 さらに、Slack でオープンな作業スレッドを作成し、進捗や感じたことを書き留めておくことで、先輩から「〇〇で悩んでいたみたいだけど大丈夫?」といった気配りをいただくこともあります。このように、非公式な形であっても進捗を共有できる場があることで、チーム内での支援を得やすくなっていると感じます。 このような体制の中で業務に取り込むことで、コミュニケーションに対する不安はほとんどなく、安心して業務に専念できています。 最後に この半年間を振り返ってみると、「自ら動くこと」の重要性を強く感じています。native チームでは新人研修で扱わない言語や開発環境を使用し、学習も主に自習形式で進めていくため、積極的に学び続ける姿勢が何よりも大切であると実感しました。 さらに、「やりたい」と思ったタスクを自ら提案し、挑戦することができます。相談することで適切なレベルのタスクを提案してもらえることも多く、自分の成長に合わせて新しい知識やスキルを習得する機会を得られると感じています。また、コミュニケーションについて特段の心配はなく、何か心配なことがあればすぐに相談できる環境が整っているため、安心して新たな挑戦を続けることができます。 このような環境で働くことで、自ら動き、新しい機会を掴むことの大切さを実感し、今後のキャリアでもこの姿勢を持ち続けていきたいと思っています。これからも多くのことを学び続け、さらに成長していきたいと考えています。 さいごに モバファク 24 新卒エンジニア 5 人の視点から、入社後半年間に経験したことや学んだことについてまとめました。 メンバー全員で自分の章を担当するという形式でブログ記事としましたが、配属が違うメンバーのエピソードを見ることができ、新卒内でも互いに刺激になりました。 これから新卒エンジニアとしてモバファクに入社される方、または新卒エンジニアをこれから迎え入れる方々にとっても参考になる内容になったかなと思います。 記事を読んでいただきありがとうございます!
はじめに 駅メモ!チームでエンジニアをしている id:wgg00sh です。 この記事では、駅メモ!内で地図クライアントとして使用している mapbox-gl-js を使うにあたって工夫した点などを紹介していきます。 【✨新機能リリース✨】 6/1 12時頃より、アプリ版駅メモ!にて「タイムラインと地図の切替機能」をリリースしました🎉 本機能では地図を見ながらチェックインが行えたり、 地図上でレーダーの対象駅や駅の属性等を確認することができます💪 詳細はお知らせよりご確認ください♪ #駅メモ #駅メモ10周年 pic.twitter.com/vneZv27AVU — 駅メモ!公式 (@ekimemo) June 1, 2024 駅メモ!では、2024 年 6 月に、「タイムラインと地図の切替機能」(以降:タイムライン地図、本機能)をリリースしました。 本機能の実現にあたって、苦労した点やその解決方法を書いていきます。 本記事で扱う内容 この記事では、主に Mapbox GL JS の以下の機能・プロパティに関する話をします。 icon-allow-overlap , text-allow-overlap icon-ignore-placement , text-ignore-placement symbol-sort-key 機能の概要説明 はじめに、本機能についての説明をします。 通常のタイムライン タイムライン地図 駅メモ!はスマートフォンの位置情報を取得して、最寄り駅にアクセスして遊ぶゲームです。タイムライン地図では画像右側のように現在地や周辺の情報を地図上に表示しながらプレイすることが可能になります。 これにより、従来よりも直感的に駅の情報を確認しながらプレイすることができるようになりました。 実現にあたって直面した問題 ここからは、いくつか本機能の実現にあたって苦労した点をその解決策と合わせて説明します。 addLayer() を使って複数の画像を組み合わせたアイコンを描画したい場合 今回のタイムライン地図機能では、駅メモ!の遊びである「駅の収集」と「駅の取り合い」の両方に焦点を合わせて駅の情報を描画しています。 タイムライン地図で描画したいアイコンの内容 タイムライン地図で使用している駅アイコンはこのようなものです。 このアイコンの描画に必要な情報として、3 つの要素に分解することができます。 駅属性 アクセス状況 駅名 駅属性は、ゲーム内で駅の取り合いをするにあたって影響するパラメータで、4 つ (heat、eco、cool, および廃駅にのみ適用される「属性無し」) のうちいずれか 1 つを必ず持ちます。 アクセス状況は、ユーザがその駅にアクセスしたかの状態で、4 つ(未アクセス、当月未アクセス、当月アクセス済み、当日アクセス済み)のいずれか 1 つの状態になります。 駅名は、その名前の通り駅に割り振られた名称で、何かしらの文字列になります。 アイコンの描画手法について ここで苦労したのが、アクセス状況と駅属性を両方含むアイコンを描画する点です。 Mapbox GL JS で地図上の特定の座標にアイコンを描画するには、大きく 2 つの手法があります。 レイヤー機能を用いて描画する方法 HTML マーカーを地図上に配置する方法 本機能では、描画されるアイコンの数は非常に多くなりうるため、HTML マーカーを用いることでパフォーマンスの悪化が懸念されます。 そのためレイヤー機能で描画する選択肢しかありませんでした。しかし、この場合描画できる内容に大きな制約がかかります。 symbol レイヤーが表示できる内容 前述のリンク先に記載されていますが、 symbol レイヤーでは同一レイヤーの各要素に対して、「1 つの文字列」と「1 つの画像」を組み合わせて描画します。(文字列や画像自体はプロパティを参照できるので要素ごとに異なるデータを渡せます) symbol レイヤーを用いてデータを描画する場合のサンプルは下記のようになります。 map . addLayer ({ id : "stations" , source : "stations" , type : "symbol" , layout : { "text-field" : "{name}" , // 駅名 "icon-image" : [ "get" , "icon-key" ] , // 画像 } , }) 1 つのアイコンには 1 つの画像までしか利用することができません。 そのため「駅属性・アクセス状況それぞれに対応する画像を用意し、それら 2 つの画像の組み合わせを用いて 1 つのアイコンを描画する」といったことはできませんでした。 この問題を解決する為に、今回 2 つの案を試しました。 案 1: レイヤーを 2 つ用意する 案 2: 2 つの画像を組み合わせた単一の画像をあらかじめ用意しておく 案 1: レイヤーを 2 つ用意する 駅属性画像とアクセス状況画像をそれぞれ用意(4+4=8 種)して、2 つのレイヤーでそれぞれ描画してみます。 map . addLayer ({ id : "stations" , source : "stations" , type : "symbol" , layout : { "icon-anchor" : "center" , "icon-image" : [ "get" , "element" ] , "icon-size" : 0 . 5 , "icon-offset" : [ 0 , -60 ] , // 駅名 "text-field" : "{name}" , "text-anchor" : "top" , "text-offset" : [ 0 , 0 . 5 ] , } , }) map . addLayer ({ id : "stations2" , source : "stations" , type : "symbol" , layout : { "icon-anchor" : "center" , "icon-image" : [ "get" , "status" ] , "icon-size" : 0 . 5 , "icon-offset" : [ 0 , -15 ] , "symbol-sort-key" : [ "get" , "sort-key" ] , } , }) このように、ほとんどの場所で正しく表示されなくなってしまいます。 通常、Mapbox GL JS の symbol レイヤーは,アイコン・テキスト同士に対して衝突検出を行い、重なっていれば一方を描画しないようにします。 これは、 icon-allow-overlap 、 text-allow-overlap がそれぞれデフォルトで false になっているためです。 (参考: https://docs.mapbox.com/help/troubleshooting/optimize-map-label-placement/) この機能のおかげで、描画すべきアイコンが膨大な場合にも適切な見やすさを保つことができ、描画負荷も抑えられます。 この仕組みを無効にするには、それぞれのレイヤーに icon-ignore-placement: true , text-ignore-placement: true を付与します。 map . addLayer ({ id : "stations" , source : "stations" , type : "symbol" , layout : { // 他の設定 "icon-ignore-placement" : true , "text-ignore-placement" : true , } , }) map . addLayer ({ id : "stations2" , source : "stations" , type : "symbol" , layout : { // 他の設定 "icon-ignore-placement" : true , "text-ignore-placement" : true , } , }) 非常に見づらくなってしまってしまいました。 これは、 icon-ignore-placement 、 text-ignore-placement を付与したことで、同じ駅の属性・アクセス状況は重ねて描画できるようになったものの、他の駅との重なりも無視されるようになってしまったためです。 駅数が少ない地域では、駅同士が密接することはなくこれでも問題ないかもしれませんが、都心部では画像のとおりに上手く描画できません。 そのため、このアプローチは不十分と判断しました。 案 2: 2 つの画像を組み合わせた単一の画像をあらかじめ用意しておく もう一方のアプローチは、あらかじめ 2 つの画像を組み合わせて 1 つの画像として書き出しておくものです。 cool かつ、アクセス済み heat かつ、今月未アクセス 属性無しかつ、本日未アクセス このように、 属性4種 x アクセス状況4種 = 16種 の画像をあらかじめ作成しておきます。 type Station = { name : string lat : number lng : number element : string status : string } [ "heat" , "cool" , "eco" , "none" ] . forEach (( element ) => { [ "unaccessed" , "unaccessed_month" , "unaccessed_today" , "accessed_today" , ] . forEach (( status ) => { map.loadImage( `/img/station_element/ ${ element } _ ${ status } .png` , ( error , image ) => { if (error || !image) { console .error( "Failed to load image" , error) return } map.addImage( ` ${ element } _ ${ status } ` , image) } ) } ) } ) const getIconKey = ( station ) => { return ` ${ station.element } _ ${ station. status} ` } map.addSource( "stations" , { type : "geojson" , data : { type : "FeatureCollection" , features : stations. map (( station : Station , index ) => { return { type : "Feature" , geometry : { type : "Point" , coordinates : [ station.lng, station.lat ] , } , properties : { name : station. name , "icon-key" : getIconKey(station), } , } } ), } , } ) map.addLayer( { id : "stations" , source : "stations" , type : "symbol" , layout : { "icon-anchor" : "center" , "icon-image" : [ "get" , "icon-key" ] , "icon-size" : 0.5 , "icon-offset" : [ 0 , - 30 ] , "text-field" : "{name}" , "text-max-width" : 10 , "text-size" : 14 , "text-anchor" : "top" , "text-offset" : [ 0 , 0.5 ] , } , } ) このようにすることで、間引きにも対応して複数画像を組み合わせたアイコンを描画することができました。 案 2 の懸念点 今回のケースでは、存在する画像の組み合わせが、16 通りと多くはないため全て予め作成しておく形で対処しましたが、ケースによってはそれが難しい場合もあるかもしれません。 例えば、ここに 4 通りのパラメータが 1 つ・2 つと増えれば全部で 64 通り・256 通りとなり、画像の生成が自動化できない場合は作成に掛かる作業コストも大きく、全て loadImage() で保持する場合メモリの圧迫も心配になってきます。 その場合、解決策としては 実際に使われる組み合わせのみ読み込む 画像の作成を実行時に動的に行う といった工夫が必要になってくると思われます。 今回は画像の数が少数かつ予め決まっていたため、全パターンを作成する方法を採りました。 駅が密集している場合に、優先して描画される駅を指定したい Mapbox GL JS では、駅が密集しているとアイコンの描画がある程度自動的に間引かれるという話を先にしました。 タイムライン地図機能では、ライブラリのデフォルトの優先度ではなく、ユーザのプレイ状況に応じて優先的に描画する駅を選択しています。 具体的には、ユーザの現在地付近の駅や、大規模な駅などは間引かれづらくなるようにしています。 例として、「山手線の各駅を優先して間引く・間引かない」という 2 つの条件で、同じ描画範囲の画面を用意してみました。 山手線をなるべく間引く 山手線を優先して残す 後者には、東京・品川・池袋など、主要な駅が間引かれずに残っていることがわかると思います。 const TARGET_STATION_IDS: number [] = [ xxx, yyy, ... ] // 山手線の各駅のID map.addSource( 'stations' , { type : 'geojson' , data : { type : 'FeatureCollection' , features : stations. map (( station : Station , index ) => { return { type : 'Feature' , geometry : { type : 'Point' , coordinates : [ station.lng, station.lat ] } , properties : { 'sort-key' : TARGET_STATION_IDS. includes (station. id ) ? 0 : 1 , } } ; } ) } } ); map.addLayer( { id : 'stations' , source : 'stations' , type : 'symbol' , layout : { 'symbol-sort-key' : [ 'get' , 'sort-key' ] , } , } ); 実際の駅メモ!内では、このコードのように特定の駅だけ優先度を上げる形ではなく、ユーザのプレイ状況に応じて複数の観点から優先度を決定しています。 おわりに タイムライン地図機能の実装にあたって、描画周りでの工夫を紹介しました。 描画内容・順序を工夫することで、ユーザにとって見やすい地図を提供することができました。 今回の開発を通して、Mapbox GL JS にこんな機能があったのかと学びもあり多く、工夫次第でさまざまな表現が可能であることを実感しました。 参考 Mapbox GL JS symbol Mapbox GL JS でカスタムマーカーを追加する Optimize map label placement
はじめに 駅メモ!開発チームエンジニアの id:kaidan388 です。 駅メモ!のフロントエンドは Vue で書かれており、およそ 1500 コンポーネントあります。 Vue2 が EOL を迎えるに際して、これをどう Vue3 に移行するかが問題になりました。 具体的には以下の 2 点をどう達成するか、というのが問題になります。 普段の機能開発を止めずに、Vue3 移行を進めたい 普段のリリースを止めずに、Vue3 のリリースをしたい 駅メモ!開発チームでは、途中メンバーの交代もありつつですが、基本的に 3 名で 1 年半かけて、上の要件を満たしつつ Vue3 へ移行を完了しました。 この記事では、いかにして Vue3 化を完了したか解説しようと思います。 技術的な難しさについてはすでに多くのブログで語り尽くされているように思うので、ここでは、チームの運用やその他特別な工夫について語ります。 具体的には以下のような工夫を行いました。 差分を可能な限り小さくする スプレッドシートを用いた、作業の見積もり及び進捗の可視化 実装に詰まったらすぐに相談できる環境の用意 Vue2/Vue3 緊急切り替えボタンの作成 はじめに 移行に際しての問題点 差分を可能な限り小さくする スプレッドシートを用いた、作業の見積もり及び進捗の可視化 実装に詰まったらすぐに相談できる環境の用意 Vue2/Vue3 緊急切り替えボタンの作成 まとめと今後の展望 移行に際しての問題点 まずは、移行開始時点で駅メモ!のフロントエンドがどのような状況にあり、移行に際して何が問題になったかを、簡単に整理します。 まずは、なんといっても、コンポーネントの多さが問題になりました。 フロントエンドの実装の大部分は Vue2 で書かれており、コンポーネント数は 1500 ほどありました。 この数の多さにより、以下の 3 点が問題として現れてきました。 作業工数が大きくなる 工数見積もりが難しい 移行作業の進捗管理が難しい また、アプリの歴史の長さからくる、実装の読みづらさも移行作業の障壁でした。 元々駅メモ!のフロントエンドは Angular で書かれており、それを Vue2 に移行したという経緯があります。そのため、駅メモ!に初期からあるコア機能の実装は、Angular と互換を保つために、独自の実装が多く含まれています。Vue3 は破壊的変更が多く、この互換性のための実装がそのままではうまく動かないケースが多々ありました。 その他にも、10 年前と今とで設計思想が微妙に変わっていたり、初期の実装には社内で作った独自のフレームワークが使われていたりと、歴史の長さからくる実装の読みづらさは様々な場所で障壁として現れてきました。 いかにして普段の機能開発への影響を抑えるか、という点も問題になりました。 正確な見積もりではないにしても、1500 コンポーネントあることを考えると半年〜1 年以上は時間のかかるプロジェクトになるだろうというのは、駅メモ! 開発チーム内でも先にわかっていました。これほどの長期間駅メモ!の新機能開発を止めることは、当然できません。移行の実装から反映まで、Vue3 化に関わらない作業への影響を最低限に抑える方法を考えることも、問題になりました。 ここまでの問題を整理して、以下に箇条書きします。 作業工数が大きくなる 工数見積もり・進捗管理が難しい 古い実装が読みづらい 普段の開発への影響を最小限にしたい これらの問題にどのように対応したか、このブログ記事で紹介していきます。 差分を可能な限り小さくする 作業工数の大きさに対応するため、移行時の差分は必要最低限にするという方針で作業を行いました。 話を始める前に、まずは、移行計画の変遷について説明させてください。 駅メモ!のフロントエンドは、Vue2 が使われていること以外にも様々な問題を抱えている状態でした。 例えば、全体的に使われているパッケージが古く、それらを更新する必要があります。他にも、ビルド時間が長い、ファイル構成がまちまちで統一感がない、など解決すべき課題は数多くあります。 それらを加味し、以下の記事で紹介した移行計画が立案され、これを実行しました。 tech.mobilefactory.jp ただ、結論としては、必要な工数が現実的でないとわかったため、この方針は却下されることになります。 実際の作業時間について説明すると、まずは、作業の合間でいくらかの中断もあったのですが、パッケージを切り分けるなどの移行の下準備に半年かかりました。 その後、試しに駅メモ!のチュートリアル画面の移行を行ったのですが、60 コンポーネントの移行に 3 ヶ月ほどの時間を要してしまいました。これは、Vue3 移行と一緒に、sass を node-sass から dart-sass に置き換えようとしたことが主な要因で、css をほぼ全て書き直す必要が生まれたからです。 作業に慣れることでいくらかのスピードアップは期待できますが、流石に、60 コンポーネントに 3 ヶ月かかる見積もりでは、1500 コンポーネントの移行を完了することは現実的でなさそうです。 というわけで、一緒に解決したかったフロントエンドの様々な問題は一旦諦め、Vue2 を Vue3 に置き換えることだけに集中して作業を行う方針に決まりました。 具体的には、Vue3 のマイグレーションガイド通りに作業を行い、レガシーコードとの兼ね合いを考えるときも負債には目をつぶり可能な限り差分を小さくする、という方針です。 方針転換後は作業スピードも目に見えて向上しました。 プロジェクトが始まったのが 2023 年の 4 月ごろ、方針が変わったのが 2024 年の 3 月で、実際に Vue3 化が完了したのは同年 9 月の末になります。 つまり、チュートリアルの移行完了(諸々合わせると 80 コンポーネントあります)までに 1 年かかり、駅メモ!本体の 1500 コンポーネントの移行完了までに 7 ヶ月かかったことになります。 方針転換とはすなわち、過去 1 年分の進捗をなかったことにする決断なので、当時はかなり悩みました。ただ、体感として、方針変更後は作業完了の目処が立つようになり、ゴールが見えることで気持ち的にも楽に作業できるようになったように思います。 作業の見積もりや作業の計画の大事さを身をもって体験した業務だったと思います。 スプレッドシートを用いた、作業の見積もり及び進捗の可視化 工数見積もり・進捗管理の難しさに対応するため、Google スプレッドシートを用いて進捗の管理を行いました。 Vue3 化の作業は、締め切りは明確には決まっていませんでした。 これは、作業量の多さから正確な作業完了時期を考えることが難しく、締め切りを作っても守れるかどうかは未知数となるためです。 しかし、締め切りがないと、つい作業ペースが遅くなってしまい、Vue3 化の反映がいつまでもできなくなるのではないかという懸念があります。 その問題を解決するために使われたのが、Google スプレッドシートです。 下図は、実際に進捗管理で使っていたスプレッドシートです。 Vue3進捗管理バーンアップグラフ 別のシートに、作業しなければ行けないコンポーネント名が 1500 列並んでいて、移行が終わって親ブランチにマージするタイミングでその別シートに完了のフラグと日付を記録するようにしていました。その作業により、上記のバーンアップグラフが描画されます。 今回の Vue3 プロジェクトでは、9/30 を作業完了の目安として理想の線を描画、そしてできるだけいつもその線を上回る状態を維持することを目指して作業を行いました。 途中でその想定より速度が出るとわかったので、1 日 14 コンポーネント作業することを目処に理想の線を引き直したりもしています。 また、Vue3 移行では週に 1 回のペースで振り返りを行いました。その振り返りでも、このグラフの傾きを一定にする、もしくは今より傾きを大きくするために、障壁になっているものは何かという視点で議論をしました。このブログで書いている工夫も、この振り返り会から生まれてきたものです。 目にみえる進捗があることで、チーム内はもちろん、チーム外に向けても進捗状況が共有しやすく、結果健全に作業を進められたのではないかと思います。 実装に詰まったらすぐに相談できる環境の用意 古い実装が読みづらい問題に対応するため、朝会の後にすぐ相談できる環境を用意していました。 前提として、モバイルファクトリーでの勤務はフルリモートになっています。 難しい実装にぶつかってうまく理解できなくなった場合は、まず Slack で声をかけ、誰かに Google Meet に参加してもらって相談する、という流れをとります。 普段はこの順序で問題は起きないのですが、Vue3 移行では古い実装を読むたび頻繁に行き詰まります。頻度も高いので声もかけづらく、相談がしづらい状況が生まれてしまいました。 これを解決するために、Vue3 移行チームでは、毎日の朝会の後お昼休みに入るまで、朝会の Google Meet の部屋に残ってもくもく会をしていました。 もくもく会とは、通話は繋いだまま基本的に黙って作業し、困ったことがあったら相談する、という会です。 これを行うことで、朝会の後少し雑談し、そしてそのままもくもく会で実装の行き詰まっている点を相談する、という流れを作ることができました。作業に行き詰まる時間も短くなり、進捗も上がるようになったと思います。 相談する側の心理としても、わざわざ声をかけて人を募るより朝会の後自然な流れで話す方が、些細な問題でも気軽に相談でき、相談しやすくなったように思います。 Vue2/Vue3 緊急切り替えボタンの作成 普段の開発への影響を最小限にするため、 Vue2/Vue3 緊急切り替えボタンを作成しました。 これは特に、反映時に、Vue3 以外の開発への影響を下げるための工夫です。 移行作業は、Vue3 移行のための親ブランチを作成して、そこから子ブランチを切り、ディレクトリ単位で進めました。 移行作業はその要領で普段の開発と並行して行えたのですが、問題は反映です。 Vue2 でも動く破壊的変更ではない差分は先に反映したりしたのですが、それでも最終的に 1300 ファイルほどに差分があるプルリクを反映する必要がありました。 また、反映は 3 日間で終える必要がありました。これは、反映期間中はできるだけ Vue3 以外のデプロイを止めて、不具合が起きてもすぐに対応できる状況を作ろうと、チーム内で相談した結果です。 これらを踏まえ、以下のような作戦で反映を行いました。 サーバ上に、Vue3 の差分を含まないビルド成果物(以下 Vue2 ビルドと記述します)と、Vue3 の差分を含むビルド成果物(以下 Vue3 ビルドと記述します)の、両方を配置する Vue2 ビルドと Vue3 ビルドの成果物の切り替えは、管理画面の操作からデプロイなしで行える 3 日のうち、最初の 1 日で様子を見ながら Vue3 ビルドを受け取るユーザの割合を上げる。2 日目で 100%にする。3 日目は様子見と不具合対応 万が一クリティカルな不具合が出た場合、チームと相談しつつ、管理画面から Vue2 ビルドに切り替える このうち特に便利だったのは、Vue2 ビルドと Vue3 ビルドの成果物の切り替えを管理画面からの操作でデプロイなしで行えるようにした点です。 これがあることで、もし、駅メモ!が遊べなくなるようなクリティカルな不具合を出したとしても、 5 分以内に切り戻せるので、心理的に安心して作業を進められました。 なお、実際には、この切り替え機能で Vue2 に戻すことはありませんでした。 反映時、軽微な表示の崩れは複数見つかりましたが、ゲーム体験にクリティカルに響くような不具合はなかったからです。これは、社内で 3 ヶ月ほどかけて入念に動作確認していたことと、どの程度の不具合であれば修正を後日に回してよいかをチーム内でよく握り合わせていた成果かと思います。 まとめと今後の展望 駅メモ!という 1500 コンポーネントある巨大な Vue2 アプリの Vue3 移行について、チーム運用やその他特別な工夫について話しました。 具体的には、以下の 4 点を紹介させていただきました 作業工数が大きくなってしまうので、差分を可能な限り小さくする 工数見積もり・進捗管理が難しいので、スプレッドシートを用いて作業の見積もり及び進捗の可視化をする 古い実装が読みづらいので、実装に詰まったらすぐに相談できる環境を用意する 普段の開発への影響を最小限にしたいので、Vue2/Vue3 緊急切り替えボタンを作成する これらの工夫により、7 ヶ月ほどで、1500 コンポーネントある Vue2 アプリを Vue3 に移行完了することができました。 今度は、駅メモ!のフロントエンドのさらなる改善に努めたいです。 Vue3 化が終わったとはいえ、古いパッケージが使用されていたり、Angular と Vue の実装が混じり合っている部分があったりと、駅メモ!のフロントエンドには多く問題があります。 チーム内で行っている改善の仕組みを用いて、これらに取り組んでいきたいです。 tech.mobilefactory.jp また、Vue3 化が終わって明確に良かった点として、駅メモ!のフロントエンドの全容を把握しているエンジニアがチーム内に生まれた、という点が挙げられます。 今まで、駅メモ!のフロントエンドは歴史の長さと実装の多さから、その全容を把握できるエンジニアがいない状況でした。結果、何か改善を入れようと思ってもそれで問題が生じないかの判断が難しく、踏ん切りがつかない場面が多々ありました。 今回の Vue3 化で、駅メモ!の実装の全体を把握しているエンジニアがチーム内に生まれ、結果改善の取り組みも進めやすくなったように思います。 エンジニアの生産効率性をさらに上げられるよう、今後も改善を継続したいです。
こんにちは、駅メモ!開発チームエンジニアの id:hayayanai です! 私が開発に関わる駅メモ!は、今年で 10 周年を迎えたゲームです。フロントエンドは Vue.js で開発されていて、現在もコード量が増加しています。 今回は、そんな駅メモ!のフロントエンドに vue-tsc を導入して、GitHub Actions で型チェックを実行し、reviewdog に Pull Request で指摘してもらえる状態を作った話を紹介します。 駅メモ!のフロントエンドの状態 はじめに駅メモ!のフロントエンドの簡単な概要を紹介します。 フレームワークは Angular → Vue 2 → Vue 3 パッケージマネージャーは Yarn Linter や Formatter の GitHub Actions は導入済み JavaScript と TypeScript が混在 JS ファイルでも @ts-check で型チェックをしているものもある TSConfig の設定は strict: false フロントエンドの規模感は以下の通りです *1 。 Vue コンポーネント数は 1650 コンポーネント JS と TS 合わせて 14 万行 Vue 内の script を含む また、型周りで次のような課題を抱えていました。 型チェックはテキストエディタによるものに頼っている ビルド時はトランスパイルのみで型チェックは行われていない 実装者のエディタ次第では TS エラーが出なかったり、気づかず対応漏れしたりすることがある Remote SSH で開発している都合、変更に対する追従が遅くなると起こりがち レビュワーが TS エラーになっていることがわからない 2024 年 4 月頃までエディタで import 時の path 解決ができておらず、型定義を書いても参照されない状態だった 型を書くことのモチベーションが下がっていた 誤った型定義を書いても気づかなかった 開発中に実装が変わっても型定義がそのままになっていることがあった 型に対する信用が落ちていた これらの問題を解決し、より堅牢なフロントエンドを開発するため、vue-tsc を導入して、GitHub Actions で型チェックを実行し、reviewdog に PR でコメントしてもらえる状態を作りました。 また、このタイミングで TSConfig の設定を strict: true に変更しました。 やったこと 1. TypeScript のアップデート vue-tsc で必要な TypeScript のバージョンは 5.0.0 以上でした。取り組み始めたときはプロジェクトにはバージョン 4.9.5 が入っていて、これでは動作しませんでした。 当然 typescript@latest までアップデートしたかったのですが、当時は Node.js 16 を使っていたのもあり諸々のパッケージのバージョンが上げられず、結局 5.3.3 となりました。 加えて、ts-loader も古いバージョンから上げられませんでした。この影響を動作確認したところ、型パラメータに const を用いるなどの新しい TS の構文がエラーになってしまうことがわかりました。 しかしながら、今回の目的は型チェック体制を作ることだったため、これらのバージョン問題は別の機会に解決することとしました。既に Vue の Language Server が TS5.0 以上を要求していて、プロジェクト内ではなくエディタ側の TypeScript を使うような設定が入っていたのもスルーした理由の1つです。 2. vue-tsc のインストール yarn add -D vue-tsc ちょうど Vue 3.5.0 がリリースされていたため、vue-tsc をインストールした後も関連して bug fix が出ていないかは確認していました。 また、Vue2 から Vue3 への移行作業が並行していて、リリースが近づいていたため Vue3 移行後のコードで正常に動くことを確かめていました。 インストールしたら早速型チェックを実行してみます。 yarn run vue-tsc --noEmit 大量にエラーが出ることを想定していたのですが、数個のエラーが出ただけで終了してしまいました。 プロジェクトや環境によるものが多いかもしれませんが、エラー文を読みつつ、vue-tsc にもっとエラーを出してもらうために直したところを書いておきます。 プロジェクト内に存在していた壊れた TS ファイルを直す 括弧が閉じられていないなど、 TS の構文としておかしなファイルを読み込んでエラー 「型定義を書いても参照されない状態」だったため、気づかれず放置されていた 数は少なかったので手動で修正 カスタムディレクティブに不要な記述がされていて TS1003 エラー Vue SFC Playground で再現してみた例 これも数は少なかったので手動で修正 JavaScript heap out of memory NODE_OPTIONS の max-old-space-size を増やすことで解決 Mac と CI を実行する Ubuntu でデフォルト値が異なっていて気づくのが遅れた NODE_OPTIONS='--max-old-space-size=4096' ./node_modules/.bin/vue-tsc 3. tsconfig.json の調整 この機会に合わせて strict: true に変更しました。経緯は以下の通りです。 今まではエラーまみれになるという理由から strict: false にして運用していた 一方で、社内に既に TS を途中導入したうえ strict: true で運用していたチームの実績があった 手元で strict: true に変更したら、型の上では null や undefined になり得る状態のまま値を扱っている箇所が多かった 今まではエンジニアの脳内で null かどうかを考えながらコードを書いていた 当然、見落としもあってフロントエラーになることもあった チェックを強化したほうが、安全で、余計な思考を減らせると感じた その他にも細かいオプションを調整しました。 4. GitHub Actions で vue-tsc + reviewdog が実行されるようにする 既に Linter と Formatter の workflow ファイルが存在しており、発火トリガーはほぼ同じものだったため、既存の workflow ファイルに追記しました。 以下に抜粋した設定ファイルとコメントを掲載します。 jobs : build : runs-on : [ self-hosted, ubuntu-20.04, large ] steps : # ブランチを checkout したり依存を install したりするステップ - uses : reviewdog/action-setup@v1 with : reviewdog_version : latest - name : Run vue-tsc run : | yarn run --silent vue-tsc | reviewdog -name="vue-tsc" -f=tsc -reporter=github-pr-review -filter-mode=file -fail-level=error env : NPM_TOKEN : ${{ secrets.GITHUB_TOKEN }} REVIEWDOG_GITHUB_API_TOKEN : ${{ secrets.GITHUB_TOKEN }} 上述した NODE_OPTIONS については package.json 側に記述 reporter は github-pr-review にした reviewdog の PR コメントに返信できる形式が便利そうだったため filter-mode は file にした 変更した行だけでなく、ファイル全体でエラーを出すため reviewdog のオプションの詳細は 公式の README を参照 実行する self-hosted runners のスペックアップ それまでのインスタンスの設定では vue-tsc が完走できずタイムアウトしてしまっていた メトリクスを確認したところ RAM が不足していた RAM の多いインスタンスが割り当たるように設定を変更して解決 GitHub-hosted runners を使っている場合は RAM が多くあまり遭遇しないかもしれない 使用されたメモリ量は vue-tsc --diagnostics 等でも確認可能 ここまでの設定で、当初の目的を達成することができました。 5. 運用ルールを決める チームで以下のようなルールを定めて運用しています。 工数やレビュワーと相談しつつ、無理のしない範囲でエラー対応をする TS エラーが無い状態から増えたものは必ず対応する 基本的に「 メリハリのある TypeScript 」で運用 まとめ 今回は駅メモ!のフロントエンドに vue-tsc を導入し、GitHub Actions と reviewdog による型チェックの仕組みを導入した話を紹介しました。 導入後、以下のような改善が得られています。 レビュー時に型エラーを自動的に検出できるようになった 開発環境に依存せず、一貫した型チェックが可能になった strict: true への移行により、より安全なコードベースを目指すことができるようになった 導入当初に起きていた TS エラーは、エラーコード単位で一括修正を行ったこともあり、1 ヶ月ほどで約 20%削減することができました。 現在はフロントエンドコード品質のメトリクスの集計も開始しており、それを踏まえて今後の方針を検討していきたいと考えています。 また、今回の導入で浮き彫りになった各種ライブラリのバージョン問題も含め、引き続き改善を進めていく予定です。 記事を読んでいただきありがとうございました! *1 : tokei コマンドにて算出
はじめに こんにちは。駅メモ!開発チームの横井です。 今回はプロダクトの機能開発をしながら改善に取り組むためのチーム構成について話します。 背景 駅メモ!はありがたいことに今年で 10 周年を迎えました。 10 年もの間、機能追加や改修をしていくことでアプリケーションは使いやすく進化してきましたが、それとともにコードベースも肥大化し、保守性の面での課題が浮き彫りになっていました。 そんな中、エンジニアとしてその課題を認識しながらも、開発チーム全体として改善に割くリソースが不足していることに気づいたのが 2022 年頃。 ビジネス側の協力を得て、機能開発と並行して改善を進められるチーム体制を構築したのが 2023 年頃です。 2024 年に入って、さらにチームの実情に沿った形へと体制が変更して今に至るのですが、そんなチーム構成の変遷と、気づいたことを説明していきます。 独立した改善チーム(2023 年) はじめは、メインである機能開発チームとは別で、独立した改善チームを結成する方針になりました。 改善チームを独立させた理由は以下の通りです。 メインである機能開発はそれだけに集中して欲しいから 規模の大きな改善をチームで連携しながら効率よく進めるため 駅メモ!開発チームはさらに小さなチームの集合体となっており、各チームから少数のメンバーを引き抜いた少数精鋭で構成されました。 成果と問題点 改善のみに集中するチームができたことで、Linter や Formatter の整備、ライブラリ導入やアップデート、他にも開発効率を上げる様々な改善が実施されました。 特に、フロントエンドの Vue3 への移行は大きなプロジェクトでしたが、改善チームがあったことで機能開発への影響を抑えて完遂できました。 tech.mobilefactory.jp しかし、改善チームによって機能開発チームのリソースが減り、プロダクトの改善に手が回らなくなってしまいました。 もともとその辺りも改善チームが実施するはずでしたが、チームの間に距離があったことで、プロダクトの課題とその重要度が改善チームに十分伝わっていなかったのが原因だと考えています。 定期的な改善デー(2024 年) 1 年ほど改善チームを運用し、先述した問題点も明らかになってきたところで、改善に取り組むためのチーム構成が再び検討されました。 新構成の方針は以下の通りです。 駅メモ!開発チーム全体として改善に割くリソースの割合を維持する 機能開発チームにも改善を進める余裕を与える 開発者体験の改善は優先度を一旦下げる 結論として、改善チームは解散して、月に 2 回の改善デーを導入することにしました。 1 ヶ月を 20 営業日とすると、業務時間の 10% を改善に充てるということになりますね。 改善デーについて 改善デーのルールは以下の通りです。業務への影響を抑えるために緩やかなルールとなっています。 プロダクトの運用効率化や保守性向上を行う チーム内で取り組むべき改善の優先度を決める 優先度の高いものから個人で進行する 緊急対応が必要な場合はそちらを優先する ビジネス側からの要求にも必要があれば対応する 定常的な会議類は普段通り参加する 成果と問題点 狙い通り、プロダクトの改善は大いに進みました。そして予想通り、規模の大きな改善や難易度の高い改善はあまり進まなくなりました。 ビルド基盤の整備、CI/CD の高速化、アーキテクチャの見直しなど、個人が短期間で完了するのは困難な改善について「やりたいけどできない。取り組むならもっと集中してやらないと無理だ」という声が上がっています。 気づき 2 つのチーム構成を運用してみての結果をまとめると 独立した改善チーム 大規模な改善を連携して進められるが、プロダクトの課題が改善チームに伝わりにくい 定期的な改善デー 大規模な改善は進みにくいが、プロダクトの運用効率化などコンテキストの深い改善が進む これらは対照的な結果となりました。 今後の展望 大規模な改善とコンテキストの深い改善をバランスよく進める必要があります。 独立した改善チームの構成を改善し、プロダクト寄りの改善の重要度を共有する体制を強化 定期的な改善デーの構成を改善し、大規模改善をチーム内で協力して進める仕組みを作る 両方のチーム構成を交互に実施 両方のチーム構成を同時に実施するため、リソースを追加 いくつか選択肢がありますが、現時点では「定期的な改善デー」のチーム構成を改善することになるのではと思っています。 おわりに お読みいただきありがとうございました。プロダクトも、開発者体験も、どんどん改善して生産性上げていきたいですね。
駅奪取チームの id:kimkim0106 です。 駅奪取チームで Qodo Merge(旧:PR-Agent) を使ってみた感想の記事になります。 結論から言いますと、人間のレビューや作業をある程度代替できており、業務の効率化につながっていると感じました。 Qodo Merge とは Qodo 社(旧:Codium-AI 社)が提供する、AI コードレビューツールです。 さまざまな LLM モデルを使ってコードレビューができるほか、GitHub や GitLab などの API を使用してプルリクエストにコメントをしてくれます。 github.com 導入背景 駅奪取チームは限られたエンジニアで開発と運用を行っており、コードレビューはチームメンバー間で分担して行っています。 しかし、チームメンバーの入れ替わりにより、コードレビューできる人が少なくなり、負荷が増大していました。 また、定期的にリードタイムを計測しているのですが、ファーストレビューの遅延によりリードタイムが伸び、ユーザへの価値提供が遅くなり始めていました。 そこで、コードレビューの負担軽減を図るために Qodo Merge を検証することになりました。 検証内容 以下の点を期待して検証を行いました。 リードタイムの短縮 コードレビュー完了までの時間を短縮することで改善を見込める 品質の向上 コードレビューの見落とし等を防ぐ 不具合や障害を減らす ただし、人間によるレビューを代替することは考えていません。 あくまで、AI による補助的なコードレビューを事前に実施し、レビュワーの負担を軽減することが目的です。 また、モデル変更によるレビュー精度の比較を行いました。 今回検証に用いたモデルは以下の 3 つです。 OpenAI GPT-4o (OpenAI API) Anthropic Claude 3 Haiku (Amazon Bedrock) Anthropic Claude 3.5 Sonnet (Amazon Bedrock) 設定内容 プルリクエスト作成時に GitHub Actions で自動実行するように設定しました。 設定方法は公式ドキュメントの通りですが、リポジトリに IP アドレスによるアクセス制限があるため社内のサーバにて Self-hosted Runner で動かしています。 qodo-merge-docs.qodo.ai レビュー結果を日本語で出力させるため、以下のような Extra Instructions を設定しています。 PR_REVIEWER.EXTRA_INSTRUCTIONS : "日本語で記述してください。" PR_DESCRIPTION.EXTRA_INSTRUCTIONS : "日本語で記述してください。" PR_CODE_SUGGESTIONS.EXTRA_INSTRUCTIONS : "日本語で記述してください。" PR_IMPROVE_COMPONENT.EXTRA_INSTRUCTIONS : "日本語で記述してください。" また、初期設定だと、レビューしやすさなどのラベルが付与されるのですが、チームが独自に設定しているラベルの視認性が低下するため、無効にしています。 無効にしてもプルリクエスト内で確認できるため、とくに問題はありません。 PR_REVIEWER.ENABLE_REVIEW_LABELS_SECURITY : false PR_REVIEWER.ENABLE_REVIEW_LABELS_EFFORT : false PR_DESCRIPTION.PUBLISH_LABELS : false 検証結果 コードレビューの負担軽減に貢献しており、導入する価値があると感じました。 一方で課題もあり、人間のコードレビューを完全に代替することは難しそうです。 以下に実際に使ってみた PR のスクショを貼っておきます。 なお、使用しているモデルは Claude 3.5 Sonnet です。 このような差分のある PR を Qodo Merge にかけてみます。 PR の差分 PR を作成すると、ユーザーが記載した内容の下に Description や Changes walkthrough を生成してくれます。 プロンプトを設定しているので日本語で表示されます。 Description と Changes walkthrough 生成後 また、PR Reviewer Guide として、レビューしやすさなどの情報を出してくれます。 PR Reviewer Guide PR Code Suggestions では、変更の提案についてスコアを含めて行ってくれます。 PR Code Suggestions 総評 良かった点 一般的な内容に対する指摘がありがたい 例外をキャッチしているか コードの可読性の向上させる改善 プルリクエストの変更内容のサマリーを日本語で出してくれる GitHub Copilot pull request summaries は英語しかサポートされていない 悪かった点 たまに致命的なハルシネーションがある 変更内容と逆のことをサマリーを出してくる 参考にできないレビューを返すこともある 差分しか見てくれないので、別モジュールまで実装を追いかけてくれない モデルによるレビュー精度 モデルによって、レビュー精度は左右されることもわかりました。 チームメンバーによる評価が高かった順は以下のとおりです。 Claude 3.5 Sonnet > GPT-4o > Claude 3 Haiku コスト プルリクエスト 1 件あたりのコストを算出しました。 Claude 3.5 Sonnet は GPT-4o よりも安く、レビュー精度が高いため、コストパフォーマンスに優れていました。 モデル名 コスト(ドル/件) GPT-4o $ 0.3 Claude 3.5 Sonnet $ 0.15 Claude 3 Haiku $ 0.1 チームメンバーへのヒアリング チームメンバーにもヒアリングを行いました。 検証を行った順番で掲載しています。 GPT-4o 良かった点 プルリクエストの変更内容のサマリーを日本語で出してくれる 日本語なので、読みやすい GitHub Copilot にも似たような機能があるが、英語でしか出せない 変更内容の説明は PR-Agent に任せ、意図のみ記載する、という形で棲み分けができて、効率が良くなった 一般的な注意すべき点をレビュー・サジェストしてくれる 例外をキャッチしているか こう書くと行数が減る・ネストが深くならない・再利用性が高まるなど レビューだけでなく、suggestion のコードを書いてくれるので助かる 見る目が一つ増えた 特に人間が読み落としそうな浅くて些細な箇所はありがたかった 悪かった点 変更内容のサマリーに、たまに致命的なハルシネーションがあるので、100%信頼はできなさそう 変更内容と逆のことを出してきたことがあった 一般的な内容に対する指摘はありがたい 誰向けのどういった機能か、といった文脈で不要と判断した実装はいろいろある けど、それは本当に不要か?といったことを考えさせてくれる 参考にできない指摘もちらほらある 間違っている理由を考えることで見える視点もあるだろうから、それはそれでよいけどノイズにはなる どうしても 2, 3 個は suggest したいのか、無理くりなものもあるような 差分しか見てくれないので、別モジュールまで実装を追いかけてくれない 「正しく実装されているか確認してください」みたいなレビューしかしてくれない 追いかけて返り値とかまでチェックしてくれるとうれしかったが、そこまでは厳しそう Claude 3 Haiku 良かった点 とくになし 悪かった点 「Respond in Japanese」とか「日本語で記述してください。」をプロンプトに入れても日本語で答えてくれない 冗長になる書き方を提案したり、使わない変数をテンプレートに渡す提案をしたり、あまり有益でない提案が多かった そもそも差分すら認識できておらず、レビューとして成立しない Claude 3.5 Sonnet 良かった点 GPT-4o に比べ、ちょうどいい粒度の description を生成してくれる ハルシネーションも少ないので、概要の手書きを PR-Agent に置き換えることができ、時短になった 誤った提案は他のモデルよりは少ない印象 typo を指摘してくれて助かった description が比較的正確だと思った 悪かった点 あまりない 存在しないコードを作ってそれに対してレビューをする?ことがあって混乱した ネクストアクション Qodo Merge 導入によって、業務効率化の効果が出ているかを検証したいと思っています。 直近のリードタイムは改善して来ているのですが、Qodo Merge 導入によるものなのかはまだ明確ではありません。 また、さらに新しいモデルに入れ替えて検証したいと思っています。 LLM の評価指標には色々とありますが、アップグレード後の Claude 3.5 Sonnet や OpenAI o1-mini あたりが、現在使っている Claude 3.5 Sonnet よりも高かったので、次はこれらを試してみたいです。
こんにちは。駅メモエンジニアの id:dorapon2000 です。 約半年前の 6 月 1 日にステーションメモリーズ!(駅メモ!)10 周年を記念してタイムラインと地図の切替機能をリリースしました。大変好評を頂いておりとても嬉しいです。 今回は、その機能の中で毎秒最寄り駅を計算するロジックをどのように実現しているのかについてお話します。様々なスペックの端末で遊ばれているため、可能な限りリソースを節約するような工夫をしました。堅い言い方をすれば、過去の計算情報を使った最近傍探索アルゴリズムを実装しました。 記事中のサンプルコードは TypeScript で記述しています。 2024/11/22 追記: はてなブックマークでのご指摘ありがとうございます。 ご指摘をいただいた「事前計算の時間計算量」と「基準点と現在地の距離が近すぎるとき」の説明部分を修正しております。 誤:事前計算を O(N) で行い 正:事前計算を O(N log(N)) で行い 誤:さて、赤円内には最低で 1 駅以上は存在していることが前提となります。 正:さて、青円内には最低で 1 駅以上は存在していることが前提となります。(ここ以外の文章もここに習って修正) ソースコードのロジックには変更を加えておりません。 実現したいこと 制約 実装方法 全探索 実際に実装したアルゴリズム 図解 アルゴリズムコードの解説 事前計算 最寄り駅計算 5km についての解説 エコ方式の計算量 まとめ 実現したいこと 駅メモ!では全国 9000 以上の鉄道駅を扱っている その中からユーザーの現在地からの最寄り駅を求めたい 最寄り駅は毎秒求めたい 最寄り駅を求めることで、地図上に最寄り駅のマークを表示したいわけですね。 制約 駅メモ!は駅の位置情報連動型ゲームです。アプリ上でボタンを押すことで、最寄り駅へ訪れたことになります。この最寄り駅の計算ロジックはバックエンドに存在します。したがって、バックエンドに API 経由で問い合わせて最寄り駅を取得する方法がまず最初に考えられます。 結論から述べると、この方法は採用しませんでした。 駅メモ!で遊びながら鉄道で移動するユーザーはそれなりの速度で動いています。駅が密集している都市部では目まぐるしく最寄り駅が変わります。毎秒最寄り駅を更新したいときに API 通信を都度していてはユーザー体験を損ないそうです。また、通信量やサーバー負荷も気になります。 そのため、フロントエンドで最寄り駅を計算することにしました。フロントエンドでは以下の情報を持っています。 日本全国 9000 駅の座標情報 現在地の座標情報 実装方法 調べてみると最近傍探索アルゴリズムにはいくつかあるようです。 ChatGPT に聞くと KD-Tree という初めて聞くデータ構造で時間計算量を落とす方法を紹介されました。ただ、業務の中で難解なアルゴリズムをあまり採用したくありません。コードレビューもメンテナンスも大変です。データ構造の初期化でそれなりの計算リソースを消費しても、地図をすぐ閉じられてしまい無駄になる可能性もあります。データ構造を保持するためのメモリ使用量についても気になります。 最近傍探索の最もシンプルなアルゴリズムは全探索です。私が実装したアルゴリズムの紹介の前に、一旦全探索のアルゴリズムではどうなるか見てみましょう。 全探索 function getNearestStation () { nearestStation = null distanceToNearest = Infinity // 現在地から最寄り駅までの距離 for (station in 日本全国9000駅) { if (distance(現在地, station) < distanceToNearest) { nearestStation = station distanceToNearest = distance(現在地, station) } } return nearestStation } 毎秒実行(地図上に最寄り駅を描画(getNearestStation())) シンプルなのであまり説明は不要かと思います。現在地と各駅との距離を計算して、その中で最も短い距離の駅が最寄り駅です。それを毎秒計算します。時間計算量は毎秒 O(N) です。 メリット わかりやすい デメリット 毎秒 9000 駅の計算をしていると端末の消費電力が心配 実際に実装したアルゴリズム このあとの説明がしづらいため、考えたアルゴリズムは「エコ方式」と名付けます。 エコ方式は大きく「事前計算」と「最寄り駅計算」の 2 つの工程に分かれます。 事前計算 最寄り駅計算の前に事前計算して、適当な時間間隔で再計算する 計算時の現在地を基準点 center とする center からみた各駅との距離を配列に保存する メソッド updateCenter で行う 最寄り駅計算 事前計算の結果をもとに毎秒最寄り駅を求める 基準点 center を中心として、基準点-現在地間の距離の 3 倍の半径を持つ円内に含まれる駅の中から全探索をする メソッド get で行う なぜこれで最寄り駅計算ができるのか図解します。 図解 事前計算については工程の説明の通りです。現在地 (基準点 center) と各駅との距離を計算し、配列に格納します。配列にはのちの計算の都合上、駅が近い順に入れておきます。 エコ方式もジャンルは全探索ですが、探索範囲をできるだけ小さくしようと試みています。現在地を中心とした探索円 (青円) を描き、その円内に最低 1 駅以上存在していれば、探索円内に最寄り駅も必ず存在していると言えます。つまり、青円に属する駅の中から全探索すればよいです。しかし、9000 個もある駅のどれが青円内にあるのかわからないことが問題です。 そこで基準点という概念を持ち込んで次のように考えます。基準点を中心とした探索円 (赤円) を描き、その中に青円が完全に内包されていれば、同じようにその中にも最寄り駅が存在しています。しかも、事前計算によって赤円に属する駅は特定可能です!その赤円というのが、「基準点-現在地間の距離の 3 倍の半径を持つ円」なんですね。 さて、青円内には最低で 1 駅以上は存在していることが前提となります。しかし現在地と基準点の距離が近すぎるときに、青円が 1 駅も含めないほど小さくなってしまうかもしれません。その場合は、基準点に最も近い駅が青円の中に入るように調整し、かつ赤円は青円を含むように赤円の半径を調整します。 アルゴリズムコードの解説 先程の説明をコードで示します。 type LngLatArray = [ number , number ] // [経度, 緯度] type MapStation = { stationId : number coordinate : LngLatArray } type StationDistance = { station : MapStation distance : number // center から駅までの距離 (座標の差) } /** * ユーザーの座標から最寄り駅を計算するクラス * * - 基準点 center と全駅との距離を事前に計算し、配列 sortedStationDistances に記録しておく * - 配列 sortedStationDistances は距離で昇順にソートする * - 最寄り駅を求める際は、ユーザーの現在地と基準点の距離 distanceCenterToUser を計算し、 * 配列の中にある distanceCenterToUser * 3 以下の駅の中で最寄り駅を再計算する * - ユーザーの現在地と基準点が離れすぎたときは、基準点を更新して sortedStationDistances を再計算する */ class NearestStation { /* 再計算の基準になる座標 */ private center : LngLatArray = null as unknown as LngLatArray /* 基準座標からの距離で昇順にソートされた駅リスト */ private sortedStationDistances : StationDistance [] constructor ( stations : MapStation []) { this .sortedStationDistances = stations. map (( station ) => ( { station , distance : Infinity, } )) } /** * 基準座標を更新し、全駅で基準座標との距離を再計算する * * @param userLocation ユーザーの座標 */ private updateCenter ( userLocation : LngLatArray ) { this .center = userLocation this .sortedStationDistances = this .sortedStationDistances . map (( { station } ) => { const distance = NearestStation.distance( this .center, station.coordinate ) return { station , distance } } ) . sort (( a , b ) => a.distance - b.distance) } /** * ユーザーの座標から最寄り駅を計算して返す * * @param userLocation ユーザーの座標 * @returns 最寄り駅 */ public get ( userLocation : LngLatArray ): MapStation { if ( this .center === null || ! this .isInEffectiveRange(userLocation)) { this .updateCenter(userLocation) return this .sortedStationDistances[ 0 ].station } // 探索円の半径 const searchCircleRadius = (() => { const distanceCenterToUser = NearestStation.distance( this .center, userLocation ) const distanceCenterToFirstStation = this .sortedStationDistances[ 0 ].distance // 基準点を中心として基準点とユーザーの距離の 3 倍の半径をもつ円の中に必ず最寄り駅がある // ただし、基準点とユーザーの距離が近過ぎた場合は、青円を内包する探索円に最低1駅は入ることを保証する return Math . max (distanceCenterToUser, distanceCenterToFirstStation) * 3 } )() let distanceUserToNearestStation = Infinity let nearestStation: MapStation = null as unknown as MapStation for ( const { station , distance : distanceCenterToStation } of this .sortedStationDistances) { // 探索円内に必ず最寄り駅がある if (searchCircleRadius <= distanceCenterToStation) { break } const distanceUserToStation = NearestStation.distance( station.coordinate, userLocation ) if (distanceUserToStation < distanceUserToNearestStation) { distanceUserToNearestStation = distanceUserToStation nearestStation = station } } return nearestStation } /** * 基準座標とユーザー座標が離れすぎているときに false を返す * * @param userLocation ユーザー座標 * @returns 基準座標を再計算するべきかどうか */ private isInEffectiveRange ( userLocation : LngLatArray ) { // 距離的に約 5 km離れていたら再計算させる const LIMIT_DISTANCE = 0.055 return NearestStation.distance( this .center, userLocation) < LIMIT_DISTANCE } /** * 2点間の距離を計算する * ただし、単位はメートルではなく座標なので注意 * * @param c1 座標1 * @param c2 座標2 * @returns 距離 */ private static distance ( c1 : LngLatArray , c2 : LngLatArray ) { return Math . sqrt ((c1[ 0 ] - c2[ 0 ]) ** 2 + (c1[ 1 ] - c2[ 1 ]) ** 2 ) } } const nearestStation = new NearestStation(日本全国9000駅) 毎秒実行(地図上に最寄り駅を描画(nearestStation. get (現在地))) 事前計算 最寄り駅計算の前に事前計算して、適当な時間間隔で再計算する public get(userLocation: LngLatArray): MapStation { if ( this .center === null || ! this .isInEffectiveRange(userLocation)) { this .updateCenter(userLocation) return this .sortedStationDistances[ 0 ].station } 最寄り駅の初回計算時、あるいは基準点 center と現在地が約 5km 離れたら基準点を現在地に更新してメソッド updateCenter で再計算します。 get は毎秒呼ばれるため、ほぼリアルタイムに基準点と現在地が 5km 離れているかどうかを監視していると言えます。 5km に大きな理由はないのですが、後述します。 計算時の現在地を基準点 center とする center からみた各駅との距離を配列に保存する private updateCenter(userLocation: LngLatArray) { this .center = userLocation this .sortedStationDistances = this .sortedStationDistances . map (( { station } ) => { const distance = NearestStation.distance( this .center, station.coordinate ) return { station , distance } } ) . sort (( a , b ) => a.distance - b.distance) } 保存先の配列は sortedStationDistances で、center への距離が短い駅順にソートしておきます。 つまり、updateCenter を呼んだ時点での現在地は center であるため、最寄り駅は sortedStationDistances[0] に入っていることになります。 最寄り駅計算 基準点 center を中心として、基準点-現在地間の距離の 3 倍の半径を持つ円 // 探索円の半径 const searchCircleRadius = (() => { const distanceCenterToUser = NearestStation.distance( this .center, userLocation ) const distanceCenterToFirstStation = this .sortedStationDistances[ 0 ].distance // 基準点を中心として基準点とユーザーの距離の 3 倍の半径をもつ円の中に必ず最寄り駅がある // ただし、基準点とユーザーの距離が近過ぎた場合は、青円を内包する探索円に最低1駅は入ることを保証する return Math . max (distanceCenterToUser, distanceCenterToFirstStation) * 3 } )() 基準点-現在地間の距離の 3 倍を探索円の半径 searchCircleRadius として求めています。 もちろん、探索円の中に駅がある場合は最寄り駅の存在が保証されているのですが、探索円の中に駅がない可能性もあります。 その場合は、青円を内包する探索円内に少なくとも 1 駅はある程度まで半径を広げています。 円内に含まれる駅の中から全探索をする let distanceUserToNearestStation = Infinity let nearestStation: MapStation = null as unknown as MapStation for ( const { station , distance : distanceCenterToStation } of this .sortedStationDistances) { // 探索円内に必ず最寄り駅がある if (searchCircleRadius <= distanceCenterToStation) { break } const distanceUserToStation = NearestStation.distance( station.coordinate, userLocation ) if (distanceUserToStation < distanceUserToNearestStation) { distanceUserToNearestStation = distanceUserToStation nearestStation = station } } return nearestStation 配列 sortedStationDistances の中から基準点に近い順に最寄り駅を走査して、探索円外に出たらその時点で探索を打ち切ります。 その時点での最寄り駅 nearestStation が真の最寄り駅と同一です。 5km についての解説 現在地が基準点から 5km 離れたら、基準点を更新するために再計算すると述べました。ここでの再計算は全国約 9000 駅に対する全探索です。 現在地が基準点に近いほど、探索円 (赤円) の半径も小さくなり、計算効率が上がります。したがって、現在地と基準点が「離れ過ぎたら」、適度に基準点を現在地に更新するほうが効率が良くなります。その「離れ過ぎたら」という基準が明確に定まっておらず、感覚で 5km と設定しました。ここでその 5km のことを「再計算距離」とラベリングしておきます。 さて、少なくとも次のことが言えます。 ユーザーの移動速度が速い場合、すぐに基準点と「離れ過ぎて」しまい全探索を何度もすることになるため、再計算距離は長いほうがよい ユーザーの移動速度が遅い場合、長い間探索円内に滞在してくれるので、現在地と基準点は近いほうがよく、早めに基準点の更新をできるよう再計算距離は短い方がよい ユーザーが地方を移動している場合、現在地と基準点が離れていても探索円内に駅が少ないため計算コストが小さく、再計算距離は長くてよい ユーザーが都内を移動している場合、逆に計算コストが高くなるため、再計算距離は短い方がよい これらすべてにちょうど良い唯一の再計算距離は自明でないので、感覚で 5km としてしまいました。 エコ方式の計算量 全駅数を N ≒ 全国約 9000 駅、探索円内の駅数を n < N とすると、ユーザが基準点から 5km 離れるたびに事前計算を O(N log(N)) で行い、ユーザーが基準点から 5km 以内の円内にいる間は毎秒 O(n) で最寄り駅探索ができます。事前計算が N log(N) になりますが、毎秒全探索の毎秒 O(N) よりはずっと計算量が落ちています。 具体例をあげます。現在地が基準点から 1km 離れている場合、n は基準点を中心として半径 3km の円内にある駅の数です。地方であれば数駅でしょうし、都内だと山手線の内側の面積の約 45% の範囲と同じなので 50 駅程度でしょうか...。現在地が基準点から 5km 離れている場合、地方だと十数駅で、都内だとおおよそ 23 区の面積と同じなので 500 駅くらいです。 まとめ 過去の計算情報を使った最近傍探索アルゴリズムを実装した 時間計算量を毎秒 O(n) < O(N) に抑える事ができる ただし、事前計算は O(N log(N)) 個人的には計算中にマジックナンバーが出てくる面白いアルゴリズムを思いついたなと思っています。 リアルタイムに最近傍探索をしたいとき、こちらのアルゴリズムを検討してみてはいかがでしょうか!
こんにちは、エンジニアの id:mp0liiu です。 かなり遅くなってしまいましたが、今年も6/10にPerlの最新安定バージョンである5.40がリリースされたので新機能や変更点についてまとめます。 安定化した実験的機能 try-catch 構文 5.34 で追加された try-catch 構文が安定化して use feature 'try' もしくは use v5.40 で有効にできるようになりました。 use v5.40 ; try { die 'Some error occurred.' ; say 'Success' ; } catch ( $e ) { say 'Failure' ; } finally ブロックに関してはまだ実験的機能なので注意してください。 finally ブロックを使用した際に発生する警告を抑制するには no warnings qw( experimental::try ); が必要です。 forループの繰り返しごとに複数の要素を参照する構文 5.36で追加された、for文の括弧内にレキシカル変数を列挙することで複数の要素に対して反復処理を行う構文が実験的でなくなりました。 my %hash = ( a => 1 , b => 2 , ); for my ( $key , $value ) ( %hash ) { say " $key => $value " ; } a => 1 b => 2 builtin モジュール 5.36で追加された、新しい組み込み関数を提供しかつ組み込み関数を扱う新しい仕組みである builtin コアモジュールが安定化しました。 builtin で提供される関数の中にはまだ実験的な関数もあります。詳しくは builtin モジュールのドキュメントを参照していただきたいですが、5.40 では 5.36 で追加されたほとんどの関数が安定化しています。 5.40 で安定化した関数をまとめて使いたい場合は use builtin ':5.40' するかもしくは use v5.40 します。 以前の記事 でも解説していますが、安定化した関数の中でもリストの各要素の順番と各要素のペアのリストを返す関数 indexed、真値と偽値を返す true と false、blessed などリファレンス判定系の関数、ceil、floor あたりの関数は使う頻度が高いと思うので積極的に利用していきたいです。 実験的な関数を使う場合は以前同様警告が発生するため、抑制したい場合は no warnings 'experimental::builtin'; する必要があります。 ちなみにこれによりforループの繰り返しごとに複数の要素を参照する構文と indexed 関数を組み合わせて配列のindexと要素の列挙が楽に書けるようになっています。 use v5.40 ; my @array = qw( red blue green ) ; for my ( $index , $value ) (indexed @array ) { say " $index => $value " ; } 0 => red 1 => blue 2 => green その他の変更 class 構文のフィールド変数に reader 属性を指定可能に 5.38 に実験的機能として追加された class 構文のフィールド変数に reader 属性を指定可能になりました。 これは getter メソッドを自動で生成する機能です。(Moose でいうところの is => 'ro' なアクセサ) field $name :reader; は field $name ; method name () { return $name ; } と同等です。 use v5.40 ; use experimental 'class' ; class People { field $name :param :reader; } say People ->new( name => 'John' )->name; # John みたいな使い方になると思います。 __CLASS__ キーワードが追加 こちらも class 構文の話で、method文, ADJUSTブロック、フィールド変数の初期化式の中で呼び出し元のクラス名を返すキーワードです。 やっていることは ref($class) と同じで、スーパークラスから呼び出された場合はスーパークラス名を返し、サブクラスから呼び出された場合はサブクラス名を返すようになっています。 use v5.40 ; use experimental 'class' ; class Parent { field $num :reader = __CLASS__->DEFAULT_NUM; sub DEFAULT_NUM { 10 } } class Child :isa(Parent) { sub DEFAULT_NUM { 20 } } say Parent ->new->num; # 10 say Child ->new->num; # 20 優先度の高い排他的論理和演算子、 ^^ が追加 Perlの論理演算子は評価優先度の高い && , || と優先度の低い and , or とがありますが、今まで排他的論理和演算子は優先度の低い xor 演算子しかありませんでした。 5.40からは優先度の高い排他的論理和演算子、 ^^ が追加され利用できるようになります。 builtin に関数 inf と nan, load_module が追加 builtin モジュールに新しい実験的関数が追加されました。 inf は無限を、 nan は非数を返す関数です。 inf は今までオーバーフローする浮動小数点演算を行って得ていた値を、 nan は inf 同士の計算で得ていた値を返してくれるので状況によっては便利になりそうです。 load_module は引数にモジュール名を渡すことで実行時にモジュールロードを行う関数です。コアモジュールである Module::Load の load 関数を builtin に持ってきた形になるかと思います。 5.40までに追加された 関数 は use v5.40; か use builtin ':5.40' で使用することができますが、5.40 で新たに追加された builtin に関数の使用は use builtin '関数名' が必要です。 また使用した際に発生する警告を抑制するには no warnings 'experimental::builtin' が必要です。 Test2::Suite がコアに追加 Test2を用いて作られたテストツール群 Test2::Suite がコアに同梱されるようになりました。 要は Test2::V0 でテストが書けるようになります。Test2::V0 は追加のテストモジュールをインストールしなくても現代的なテストが書けるテストモジュールで、複雑なデータ構造のテストを簡単にかけたり、RSpec風のテストがかけたり、テスト結果が見やすくなったりと良いことがたくさんあるので積極的に利用していきたいです。 goto による外部スコープから内部スコープへのジャンプが廃止予定に goto で外部スコープから内部スコープにジャンプすることが Perl5.42 で廃止されることになりました。 例えば以下のようなコードは動かなくなります。 { LABEL: say "smoething" ; } goto LABEL; goto を使ったコードを書いてる人はほとんどいないと思うのであまり影響はないかと思いますが、古かったり複雑なループ処理をしているコードだと使われている箇所もあるかもしれないので一度確認した方が良さそうです。 5.40 では上記のようなコードがあると警告が発生するようになっています。 まとめ 今回は5.38のときほどの大きな変更はありませんでしたが、重要な実験的機能が安定化したり細かな改善をしたりと、確実に進化し続けています。 次回以降の安定バージョンでも名前付き引数や文字列中で式展開ができる構文などが入る気配があり楽しみです。 この記事では書けなかったこともあるので詳しいことが気になった方は 公式ドキュメント もぜひ読んで見てください。
はじめに モバイルファクトリーは、21 年度から完全リモートワークに移行しています。 リモートワークではコミュニケーション不足に陥りがちです。まだ会社に慣れていない、社員の顔と名前が一致していないような状態にある新卒のエンジニア達はなおさら、コミュニケーションに困難を感じるのではないかと想像されます。 リモートワーク下でも、新卒エンジニア同士 / 新卒エンジニアと先輩社員 がコミュニケーションしやすい状況を作りたい! というわけで、今年の新卒技術研修を担当しました( id:kaidan388 )が、コミュニケーションしやすい状況作りのために新人技術研修で行った工夫について説明します。 端的にいえば、コミュニケーションするきっかけを増やすことに注力して、内容を組みました。 具体的には、新人技術研修に以下の工夫を盛り込んでいます。 朝会と夕会で雑談タイムを作り、互いのことを話す 幅広い社員を募った「座談会」を定期的に開催し、多くの社員とコミュニケーションする場を用意する 研修の参加者には同じ Google Meet の部屋に入ってもらい、参加者どうし相談しやすい環境を作る 先輩社員が常に 1 人以上いる Google Meet の部屋を用意し、いつでも相談できる場所を作る はじめに 新人技術研修の目的 新人技術研修の具体的な内容 なぜコミュニケーションのきっかけを増やすか コミュニケーションのきっかけを増やす具体的な工夫 朝会/夕会の雑談タイム 幅広く社員を募った「座談会」 研修の参加者同士の相談部屋 先輩社員がいる相談部屋 まとめ 新人技術研修の目的 研修の目的は、新卒エンジニアが開発業務に加わりやすい状態を作ることです。 参加者に対しては、意識してほしい目標として以下の 3 つを共有しました。 業務に必要な最低限の技術スタックを身に着ける 研修の場だけで全ての技術を使いこなす状態に持っていくことは考えていません ここでいう最低限とは「何ができるかをぼんやり知識として知っている/参照すれば使える状態」になることです 疑問や課題を言語化・質問して解決する能力を身に着ける 前述の通り、技術研修で業務に必要な知識を全てインストールするのは不可能です そのため、未知に遭遇した場合に、周りの環境を利用しつつそれを解決する能力が非常に重要になります いわゆる"聞く力"です 仕事を円滑に進められるための関係性を構築する 上記に関連しますが、聞く力を鍛えるだけでなく新人が気兼ねなく質問・相談できる環境を作ることも研修の狙いとしています また、この中でも、3 つめの「仕事を円滑に進められるための関係性を構築する」が重要だよ、という点を繰り返し強調して伝えました。 これは、冒頭でも述べたコミュニケーションするきっかけを増やしたいという意図を、参加者にも意識して欲しかったからです。 新人技術研修の具体的な内容 以下のようなスケジュールで進行しました。 日程 概要 詳細 4/11 研修キックオフ - 研修キックオフ - 技術スタック紹介 - 開発環境整備 - Git/GitHub 研修 4/12 ~ 4/25 サーバサイド研修 - MySQL - Perl 研修 - AWS 研修 4/26 ~ 5/9 フロントエンド研修 - JavaScript 研修 - Vue 研修 - TypeScript 研修 5/10 ~ 5/15 Web サービス開発 ユーザー認証付きの掲示板を作る 5/16 発表 社員に、開発した掲示板と研修の内容をスライド発表する モバファク では基本的にどのエンジニアもフロントとサーバの両方を扱うことが多いので、研修でも両方を満遍なく扱うようになっています。 なぜコミュニケーションのきっかけを増やすか 研修の目的である新卒エンジニアが開発業務に加わりやすい状態を作るために一番重要なことは、コミュニケーションの経験を通して先輩社員との関係性を作っておくことだと考えたからです。 例えば、同期や先輩社員との関係性があまりないまま開発業務に加わってしまい、質問や相談がうまくできずに開発効率が落ちてしまう、というのは割とよくある話なのではないかと思っています。 逆に、技術研修中でコードの書き方を学んだとしても、その場では必要性を実感しづらいですし、実際必要になった場面では内容を忘れてしまっていることもよくある話です。 実際、自分の入社直後の Slack 上でのやり取りを見返してみると、技術的に難しい実装にぶつかった時相談するのが遅くて、開発に時間かかってしまっている状況はしばしば起きていました。 特にリモートだと、実装で困っている時は自分が声をあげなければ、周りのメンバーが気づくこともできず手助けが遅れてしまいがちです。そういう意味でも、話がしやすい関係性を作ることが重要なのではないかと思います。 個人的には、新卒技術研修という名目ですが技術それ自体はコミュニケーションのきっかけになる共通の話題でしかなく、関係性を作っておくことの方が重要だ、くらいの気持ちで研修の準備を行っていました。 コミュニケーションのきっかけを増やす具体的な工夫 ここまでで、コミュニケーションの経験を通して先輩社員との関係性を作っておくことの重要性を述べました。 ここからは、どのようにしてコミュニケーションするきっかけを増やしたか、具体的な工夫について 4 点紹介します。 朝会/夕会の雑談タイム 毎日、出勤直後と退勤直前に、15 分ずつ朝会/夕会を開きました。 主な目的は、研修の参加者の進捗を聞き、必要であればサポートなど行うことです。 また、時間が余ったら雑談をしてお互いのことを知るための時間に当てました。 雑談では、よく見ているコンテンツの話や、今日食べた朝ごはんの話などをよく話しましたね。 リモートワークの影響で参加者が全国各地に住んでいるので、たとえば全国区だと思っていたパンが実はローカルでしか売られていなかったりと、住んでいる地域の違いに関する話題がよく盛り上がりました。 概ね良かったのですが、研修終盤になってくると、話題がだんだんと尽きてくるのがちょっと難点でしたね...... 幅広く社員を募った「座談会」 事前にエンジニアの社員を広く募っておいて、毎回違うメンバーをゲストとして招いて技術に関する話をする座談会を開きました。 座談会は 1 回 40 分ほどです。 まずゲスト社員からの簡単な自己紹介を行い、次に研修の参加者からゲスト社員に研修の内容に関する質問を行った後、最後に事前に考えてもらった「先輩社員から新卒に伝えたいこと」を話していただきました。 ゲスト社員は、できるだけ様々なチームから満遍なく呼ぶようにしています。 この会もコミュニケーションのきっかけを増やすことが大きな目的になっていて、自己紹介や質問を通して、ゲスト社員と研修の参加者が互いの興味関心などを知るきっかけになるように設けたものです。 研修の内容に関する質問では、JavaScript のこの機能が便利そうですが、これはプロダクトのコードでも使われていますか?など、研修の内容と実際の業務を結びつける情報を聞くようなものが多かったです。 研修後に効果について新卒メンバーに聞いてみたところ、モバファク で働いている社員が全体的にどんな雰囲気の人が多いか把握できて良かった、というようなコメントをいただきました。 ゲスト社員を様々なチームから満遍なく呼んでいた効果があったようです。 また、「先輩社員から新卒に伝えたいこと」が新卒メンバーからの評判がよく、働く際の心構えやリモートワークで便利なアイテムなどの話について、聞けて良かったという感想がありました。 研修の参加者同士の相談部屋 事前に Google Meet の部屋を作っておいて、研修の参加者には研修中その meet に入って作業してもらいました。 これは、困り事があったときに同期同士で相談しやすい状況を作ろうという意図です。業務が始まった後で一番話をしやすい他人は同期だと思うので、ここの関係値を積んでもらうことを目指していました。 ただ、研修の序盤はあまりうまく行かず、会話があまり発生していない状態になっていました。 研修の参加者は 5 名だったのですが、5 名を 1 部屋に入れるにはちょっと人数が多かったのかもしれません。 研修の中盤で、新卒同士コミュニケーションして欲しい旨を伝えた上で 2 人部屋と 3 人部屋に分けるとうまくコミュニケーションが起き始めたようで、朝会や夕会の雑談でその日話した内容を聞くようになりました。 研修後に効果について新卒メンバーに聞いてみたところ、研修中に雑談することで気が抜けるタイミングを作れた、もし相談部屋がなかったら辛かっただろう、といったコメントをいただきました。 また、新卒同士コミュニケーションして欲しい旨を伝えたことと、2 人部屋と 3 人部屋に分けたことの両方が、コミュニケーションに作用していたようでした。 2 人部屋と 3 人部屋に分かれてもらった後、今日の作業について軽く話したりと、コミュニケーションのきっかけを作るための雑談を互いに行っていたとのことです。 新卒メンバーはまだ互いに相手のことを深く知らない状態なので、相談部屋に入ってもらった後どう会話してもらうかをある程度イメージして、人数の調整や朝会/夕会のファシリテーションを行うと、初めからスムーズに進んでいたかもしれません。 先輩社員がいる相談部屋 研修の参加者が参加している GoogleMeet とは別に、先輩社員がいつでも 1 名以上いる相談用の Google Meet を用意しました。 これは何か困り事があったらそこの部屋に入るだけでいいという状態を作る意図です。研修担当者にはいつでも質問していいよと伝えてるとはいえ、実際 Slack 上でメンションをいきなり飛ばすことにはハードルがあるかと思うので、そこを解消しようと考えました。 大体 1 日 1 回以上利用があったので、質問しやすい状況を作るという狙いは達成されたかと思います。 研修後に効果について新卒メンバーに聞いてみたところ、新卒同士の部屋でもある程度疑問は解消できていたものの、たとえば実装方針の相談など自分たちだけでは決めきれない相談がある時、相談しやすくて助かったとコメントをいただきました。 質問しやすい状況を狙い通りに作れていたのではないかと思います。 また、先輩社員と話すきっかけが増えたかと聞いてみたところ、増えはしたが、疑問を聞いて解消するための会話に終始するので、関係性を作るきっかけにはなりづらそうとコメントをいただきました。 先輩社員がいる相談部屋の取り組みは、関係性の構築というよりは、心理的に安心して作業できるようになるといったメリットの方が大きいようです。 まとめ 24 年度の新人技術研修では、新卒エンジニアが開発業務に加わりやすい状態を作ることを目的に、メンバー同士が関係性を構築することを重視して内容を作りました。 この記事では、コミュニケーションするきっかけを増やすための工夫を 4 点、紹介させていただきました。その結果、 座談会の開催によって、新卒メンバーが モバファク で働いている社員にどんな雰囲気の人が多いかを把握する機会を作れた 研修の参加者同士の相談部屋によって、新卒メンバー同士が雑談するきっかけを作れた といった効果を得られました。 また、先輩社員がいる相談部屋によって、心理的に安心して研修が受けられる環境を作ることができました。 今後の課題としては、研修参加人数に応じて、うまく会話が起きるよう部屋の設定を、研修開始前に考えておくようにしたいです。 これは研修参加者にインタビューをして感じたことなのですが、入社直後の状態だと、仕事中にどのくらい人と雑談しても許されるのかという感覚がよくわからず、雑談しづらいという面があったようです。 人数の調整もそうですが、コミュニケーションしてほしい旨を伝えたり、ファシリテーターどうしが雑談してる姿を見せたりと、どういうコミュニケーションをとってほしいかがわかるような説明を追加したいですね。 研修終了から半年以上経った後の記事公開になってしまいましたが、この内容が 25 年度以降の新人研修を考えている誰かの役になったなら幸いです。
駅メモ!開発基盤チームです。 今回はサービスで利用している Amazon Aurora MySQL を v2 から v3 へ移行したときのことを書きます。 概要 駅メモ!をはじめとする弊社のサービスでは、データストアとして Amazon Aurora MySQL(以降 Aurora MySQL) を利用しています。すでにアナウンスされている通り、 Aurora MySQL v2 は 2024 年 10 月 31 日に 標準サポート終了を迎える ため、Aurora MySQL v3 への移行が重要な課題になっていました。これに対し、駅メモ!開発基盤チームでは綿密な計画を立て、今年の初め頃に無事に移行を完了させることができました。このエントリはその時にどんな手法を取ったかを書きます。誰かの参考になればと思います。 やったこと 最初にざっくりとした流れを示します。 調査 新機能・廃止された機能の調査 非推奨事項の調査 移行手段の調査 設計 パラメータグループの設計 移行手順の設計 動作確認・負荷試験の設計 開発環境で利用している MySQL の移行手順の設計 事前準備 廃止された機能を v2 の段階で無効化 動作確認・負荷試験の実装と実施 v3 に向けてアプリケーション・スキーマの最適化 移行手順の実装 MySQL 5.7/8.0 の両バージョンで CI を実行できるように 移行実施 深夜メンテナンスにて移行実施 1. 調査 まずは移行対象の調査を行いました。MySQL や AWS のドキュメントを読み、サービスに影響がありそうな変更を重点的に深堀りしていきました。 調査したものの内、移行前にやることと移行後にやることを分類しました。以下にいくつかの例を示します。 移行前にやること 整数型の表示幅変更 クエリキャッシュの無効化と代替の検討 移行後にやること utf8mb3 から utf8mb4 への変更 分類はそのタスクが必須であるかや、重さ・難易度で決めました。例えば、クエリキャッシュの無効化は廃止となってしまうため、必ず移行前に対応する必要があります。一方、MySQL 8.0.12 で追加された ALGORITHM=INSTANT で実行可能な ALTER は、INPLACE や COPY よりも高速に実行できるので、作業の簡単化のために移行後の実施としました。 などです。 2. 設計 調査内容を元にパラメータ、移行手順などの設計を行いました。ここは並行で設計できる部分があったので、チームで手分けして行うことにしました。 それぞれの設計では様々な工夫をしましたが、ここでは移行手順の設計について書きます。 移行手順の設計 Aurora MySQL v2 から Aurora MySQL v3 への移行手順を設計するうえで特に気を使ったのは切り戻しに関する部分です。移行メンテナンス中やサービスイン後など、どのタイミングで問題が発覚しても最悪の手段として Aurora MySQL v2 へ切り戻すことができるように考える必要がありました。最初は Aurora Blue/Green Deployments を利用することでうまく切り戻しを実現できないかを考えましたが、後述の理由からこれだけでは要件を満たせないことがわかりました。しかしながら、それ以外の Blue/Green Deployments の利点は採用したいものが多い状況でした。 チームで考えた最終的な構成を以下に示します。 最終的な構成 1. Blue/Green Deployments と 復旧用クラスターを構成 まずは普通に Aurora Blue/Green Deployments を構成します。これは AWS のドキュメントに従って作業を行います。 次に復旧用クラスターを Green 環境のレプリカとして作成します。 構築 この時点で Green 環境と復旧用クラスターに Blue 環境からの変更が同期されていることを確認します。 2. Green 環境を Aurora MySQL v3 にアップグレード 一通りの確認ができた後は Green 環境をアップグレードします。 アップグレード中にはあらかじめレプリケーションを停止しておきます。理由はいくつかありますが、Green 環境が期待通りになっていることを確認してから再開したいというのが主なものです。 Aurora MySQL v3へのアップグレード Green 環境のアップグレードが完了した後はレプリケーションを再開と動作確認を行います。今回は経過観察としてこの構成のまま数日稼働させました。ここまでの作業はサービスを止めること無く実施できます。 3. 切り替え サービスをメンテナンスモードにし、切り替えを実施します。切り替え後の旧 Blue クラスターは不要になるので削除します。昇格した Green のクラスターエンドポイントは変更になるので、復旧用クラスターへのレプリケーション設定を適切に変更する必要があります。 切り替え 動作確認が終わればメンテナンスを解除、サービスを再開します。サービス再開後、特に問題がなければ復旧用クラスターは不要になるので削除します。 切り替え完了 4. 切り戻し メンテナンス中、Blue/Green Deployments 切り替え前に問題がわかった場合は、Blue/Green Deployments を解除することで切り戻しを行います。 そうでなく、メンテナンス後や Blue/Green Deployments 切り替え後に切り戻しが必要になった場合は、復旧用クラスターをプライマリに昇格させることで切り戻しを実現します。 切り戻し 3. 事前準備 設計が終わったのであとはそこに向かって作業をするだけです。ここもチームで分担して行いました。 負荷試験では Locust を使った負荷試験環境を構築しました。これについてはいずれ別記事で紹介できればと思います。 4. 移行実施 移行計画通り深夜メンテナンスにて切り替えを実施しました。 入念な準備の甲斐もあってか、復旧用クラスターが必要になるような問題・パフォーマンス劣化は見つかりませんでした。 今またやるなら 現在また同じようなメンテナンスを行う場合は、下記 AWS ブログで紹介されているように Blue/Green Deployments 切り替え後の旧 Blue クラスターを再利用するのが簡単だと考えています。 aws.amazon.com 私達の設計初期段階でもこの旧 Blue を使えないか?と模索していました。調査の結果、切り替え後のログファイル名とポジションを特定できないと判断しました。先に Blue/Green Deployments だけでは要件を満たせないと書いたのはこのためです。実際には、AWS のブログにもある通り、ログファイル名とポジションを含むイベントが発行されるのでそれを利用すれば良いことになります。 このエントリで紹介した方法であれば事前にほぼすべての準備をすることができるので、メンテナンス中手順が減らせるという利点が一応あります。しかし 1 クラスター分の費用が追加でかかってしまうので一長一短です。 まとめ この記事では駅メモ!で利用している Amazon Aurora MySQL v2 クラスターを v3 にアップグレードした手法について述べました。また、今またやるならどうするかについても述べました。 DB のマイグレーション、アップグレードはかなり慎重になるタスクですよね。大きな問題が起こらなくてとても安心したのを覚えています。 今後は Aurora MySQL v3 移行後に実施しようとしていたものをひとつずつ進めていく計画です。また書けそうなことがあったら書こうと思います。 以上です。 参考文献 GitHub.com を MySQL 8.0 にアップグレード Implement a rollback strategy after an Amazon Aurora MySQL blue/green deployment switchover
駅奪取チームエンジニアの id:kimkim0106 です。 「レポートを書くまでが YAPC」とのことなので、自分も書こうと思います。 YAPC::Hakodate の概要 2024/10/5(土)に、北海道函館市の公立はこだて未来大学にて開催されました。 YAPC は Yet Another Perl Conference の略で、Perl を軸とした IT に関わる全ての人のためのカンファレンスです 前夜祭 会場入口の看板 前夜祭会場のスクリーン 前夜祭のトークテーマも気になるものが多かったので、前夜祭から行きたいと思っていました。 特に印象に残ったトークをいくつか紹介します。 アンカンファレンス いくつかトピックがあった中で、生成 AI に関するトピックがありました。 GitHub Copilot などの生成 AI を使っている人が多く、これからは生成 AI の時代なんだと改めて感じることができました。 また、ある方は「最大公約数的なコードが生成されることで、書き手の個性がなくなる」ということをおっしゃっていた。 Perl の哲学である「やり方は一つではない (TMTOWTDI: There's More Than One Way To Do It.)」という言葉みたいだなと思い、Perl のカンファレンスに来たことを実感したのでした。 小さな勉強会の始め方、広げ方、あるいは友達の作り方 by あらたま speakerdeck.com 前夜祭のトークの中で一番印象的でした。 特に、「ほんのちょっとの勇気を持って、全てに能動的に関わっていくこと」という言葉が心に残りました。 前夜祭後 技術広報っぽい仕事をしてる人たちでいか太郎来た!明日の本編も頑張りましょ〜! #yapcjapan pic.twitter.com/AC4qtXFWeT — もりけん 2 (@molmolken) 2024年10月4日 さっき聞いた話を実践できる機会がないかと考えていたところ、会場の外で飲み会に行こうと集まっている人たちを見かけました。 たまたま、技術広報に関わっている人が集まっており、「一緒に行ってもいいですか!」と勇気を出して声をかけて参加させてもらいました。 他所の技術広報がどんなことをしているのかを聞けてとても勉強になったので、声をかけて良かったなと思いました。 本編 会場のはこだて未来大学は、明るく広々としていて、とてもいいところでした。 特に、講義室には机に一人 1 つコンセントが設置されていたのが羨ましかったです。 今回、モバイルファクトリーは U25 支援のスポンサーだったので、「U25 支援企画」のセッション内でスポンサー LT をしてきました。 私が入社以来どういう経験をしてきたか、という内容で発表しました。 リアルで発表するのは学生以来数年ぶりだったので緊張しましたが、うまく発表できて良かったです。 こちらも印象に残ったトークをいくつかピックアップします。 U25 支援企画 「U25 支援企画」では、スポンサー LT と U25 支援で参加された方の LT がありました。 学生時代のエピソードや開発経験などを発表されていて、修論発表直前にシステムが動かなくなるというゾッとする話もありました。 すごく興味深い話を聞けました。 ありがとうございました! プロファイラ開発者と見る「推測するな、計測せよ」 by Daisuke Aritomo (osyoyu) fortee.jp タイトルの通り、プロファイラで計測するときの話です。 計測したら予想外なところに問題がみつかるという話は、負荷対策などでも実際経験したのでとても共感できる話でした。 また、得られた情報が正しいかどうかということは今まで考えたことがなかったので、いい学びでした。 たしかに、調査のためログに時刻を出力しても、システムの時刻が更新されていたら狂うわけですし、正しいかどうかを確認しないといけません。 テストコードの品質を客観的な数値で担保しよう 〜Mutation Testing のすすめ〜 by Kanon fortee.jp テストのカバレッジが高いからといってテストの品質が高いとは限らないという話は、以前どこかで目にしたことがあったのですが、ではどうすれば品質を高められるのかというのは分からずにいました。 Mutation Testing は初めて知ったのですが、テストの品質を高める 1 つの方法として良さそうと思いました。 品質マネジメントで抑えておきたい 2 つのリスクを見分けて未来に備えよう by 巻 宙弥 fortee.jp 2 つのリスクとは「プロジェクトリスク」と「プロダクトリスク」で、それぞれ対処しないと行けないという話でした。 要件の増加でスケジュールが遅れるみたいな話は思い当たる節があったので、どこでもそういうことはあるんだなと思いました。 誰になんと言われても「いい開発環境」を作りたくて頑張っている話 by Tatsuro Hisamori 開発者体験を「なんとかする」か「転職する」みたいな 2 択があるなか、「なんとかしたほうが社会が良くなる」という言葉が心に残りました。 また、テストの高速化や Nix を導入するなど、開発者体験が良くなっていくのは楽しいだろうなと思いました。 引用されていたこちらのツイートも心に残りました。 余所の開発者体験がよさそう、なんじゃなくてお前が所属しているチームの開発者体験をよくするんだよ!!!!!!!!!!!!!!!!!!!!!!!q — あそなす (@asonas) 2024年7月17日 ブース ブースもいくつか回りました、印象深かったブースをいくつか紹介します。 LayerX さんのブースでは、実際の ADR(Architectural Decision Records)が見られるということで、ADR をどのように書いているかや意思決定について学ぶことができました。 DELTA さんのブースでは、AWS コスト削減クイズというのをやっていました。結構難しいクイズだったのですが、なんと会場 1 位のスコアを取ることができました! 景品として、分割キーボードをいただきました! コスト削減クイズのランキング 感想 今回が初めての YAPC 参加だったのですが、Perl コミュニティの温かさを感じることができ、すごく楽しかったです。 また、Perl の歴史が長いので、幅広い年代の参加者と交流できるのも良かったです。 普段はリモートワークなので、リアルでのコミュニケーションに新鮮味を感じつつ、また人と直接話せるというのがオフラインイベントの大きなメリットだと実感しました。 楽しいカンファレンスを作っていただいたスタッフの方々や登壇者の方々、本当にありがとうございました。 そして、次の YAPC も絶対に行くぞ!という気持ちになりました。 余談 函館といえばラッキーピエロということで、滞在中 2 回行ってきました! ハンバーガーがメインのお店ですが、他にも色々とメニューがあり、焼きそばもおいしかったので、オススメです。 ラッキーピエロの焼きそば
はじめに vim に最近目覚めた。そこから NeoVim、LunarVim を使うようになった流れについて、自分が思う好きなポイントと絡めてまとめる。 書かないこと エディタ戦争 VSCode も、vim も、emacs も、みんな違ってみんないい あくまでも vim のココスキをまとめるので比較はしない どうして vim か VSCode を今まで使っていて、remote の接続が悪かったり重かったりしていたのでこれを機に、気になっていた vim に乗り換えてみた vim を選んだ理由は、 慣れるとコーディングスピードがすごいらしい 脳とコーディングを直結したい 軽そう 使ってる人が多い つまりググったときの情報が多い という辺り。 どうして NeoVim か vim について色々調べていると、どうやら新しい NeoVim というのがあるらしい *1 事に気づいたのでそっちを使うことにした。 どうせ 0 から始めるなら、後から乗り換えしなくても済むようにしたいので NeoVim から始めた vim すごい オペレータとモーション 実は学生の頃にも先輩に勧められて vim を試していて、調べてみるとコマンドが多くて複雑そうで慣れるのに時間かかりそう…と思っていた。 「なんで丸かっこ内を削除するコマンドは di( なんだ…?」「 d は delete っぽいけど i って何…?」などの疑問が出てやめてしまった。 が、今回調べ直して知ったがどうやらコマンドはオペレータ、モーション、テキストオブジェクトの組み合わせになっているらしく、それを知るとむしろすぐ慣れることができた。 さっきの例だと d が削除のオペレーター、 i( が inner () というテキストオブジェクトで、その組み合わせによって「()の内側を削除する」という結果になる。 さらに、これを応用すれば覚えてないコマンドでも自由自在にコマンドを作れる。「インデントの中をすべてコメントアウトする」を何も調べずに一発で出来たときは感動した。 拡張性 vim のコマンドを使うだけなら VS Code に vim プラグイン入れることでも出来るが、vim の場合は拡張性の高さもさらに使い勝手を良くしていると思った。 さらに、「デフォルトでは真っ更だから後はやりたいようにやりな」というのを vim から(勝手に)感じてる。なので、よく使う操作をコマンドの組み合わせを 0 から設定することができる。 プラグイン 超便利なプラグインが豊富にあるのも良い。ちょっとしたモーションを追加するものから、外観を一気に変えるものまで、なんでもござれ。 しかし うまく行かないな〜と思っていたものがいくつかある プラグイン多すぎ問題 vim の利点で書いた事と早速矛盾するが、世の中にはプラグインが多すぎる。 例えば vim からターミナルを使えるようにするプラグインをググるとめちゃくちゃ出てくる。 その中から今も動くものとか流行っているものとかを選定するのが大変。 NeoVim awesome もあるがそれ自体も複数あってなにがなにやら。 この辺は VSCode だと拡張機能マーケットプレイスがあって、人気なプラグインがすぐわかるしインストールも簡単だからスゲ〜と思う。 LSP NeoVim で LSP(構文解析したり lint したりするやつ。Language Server Protocol)の動かし方がまだよくわからない、というか動かない。いくつかプラグインを入れて、設定して、バージョンごとに相性があって、試行錯誤して… この辺も VSCode だとよしなにやってくれるからスゲ〜と思う LunarVim の出会い 悩んでたら最近 LunarVim というのを 記事を読んで知った NeoVim ディストリビューション(そういうのもあるんだというのもここで知った)の1つらしく、名前もかっこいいし使ってみた 最初から機能十分 LSP が最初から動くようになっていて、ファイルを開くと拡張子をみて自動的に LSP をインストール、起動してくれる。これがとても便利、かなり VSCode でできていたことに近づいてきた。 他にも 最初からプラグインが色々入っていて 、まっさらな状態の時点で十分に便利。もちろんそれらの設定も可能。 vim の良さとして最初は何もなくて後とから拡張できる、というメリットに反している…と思ったけど前言撤回。 VSCode に慣れてしまった自分にとってはエディタに求める当たり前品質が多いのと、現代のソフトウェア開発は複雑度が高まっていてそれに追従できるための道具としてのエディタも求められる機能が増えている、と思った。 カスタマイズもできる とはいえ、カスタマイズも十分できるしプラグイン追加も可能。素の NeoVim で難しかったことが簡単に出来るようになりつつ、使っていたキーコンフィグ・プラグインも使えて今のところ LunarVim に落ち着いている。 今の見た目はこんな感じ 便利なプラグイン 最後に自分が使っているプラグインを抜粋して紹介する。 phaazon/hop.nvim https://github.com/phaazon/hop.nvim 楽にカーソル移動できるようになるプラグイン。 カーソルから手を離さずに移動したい場所へカーソル移動が出来るようになった、これのお陰でマウスを使うことが激減した。 nvim-neo-tree/neo-tree.nvim https://github.com/nvim-neo-tree/neo-tree.nvim ディレクトリやファイルをツリーで見られるプラグイン。業務で使うときは大量のディレクトリを行ったり来たりするので必須。 echasnovski/mini.nvim https://github.com/echasnovski/mini.nvim 独立した小さめのプラグインてんこ盛りのプラグイン。2024 年 6 月現在 40 個のプラグインがあり、それぞれ個別で使うことも出来る。痒いところに手が届いて、ほとんどはここからプラグインを選んで完結する。 特に mini.surround はもうこれ無しでは生きられない体になった。 *1 : この記事を書いてる時に知ったが、NeoVim は vim の新しいもの、という事では ないらしい 。NeoVim は vim を fork して作られたもので開発者も違うしそれぞれ独自路線を進んでいる。
こんにちは、駅奪取エンジニアの id:kimkim0106 (旧: id:kaoru_k_0106 )です。 今回の記事は、駅奪取でテーブルにレコードが「無ければ INSERT、あれば UPDATE」(いわゆる UPSERT)をする箇所で Duplicate entry が出ていたのを修正したり、未然に防ぐ実装をしたときに得られた知見です。 このような処理はよく使われますが、うまく実装しないとエラーが発生したりパフォーマンスの問題が生じたりします。 この記事では、自分が試した方法のメリット・デメリットについて説明します。 目次 前提条件 Duplicate entry とは 1. Duplicate entry が出たらトランザクション自体をやり直す 2. INSERT ... ON DUPLICATE KEY UPDATE 3. とりあえず INSERT して Duplicate entry が出たら SELECT 4. 前もって必要なレコードを INSERT しておく 5. トランザクションが同時に走らないようにロックを取る まとめ 前提条件 今回、このようなテーブルがあったとして話を進めます。 CREATE TABLE report ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, report_on DATE NOT NULL , referrer VARCHAR ( 50 ) NOT NULL , dau INT UNSIGNED NOT NULL DEFAULT 0 , UNIQUE report_row(report_on, referrer) ); トランザクション分離レベルは InnoDB のデフォルトである REPEATABLE READ の場合を想定しています。 また、サンプルコードは Perl にて記載されています。 Duplicate entry とは? Duplicate entry は主キーが重複したときやユニーク制約違反が起きたときに発生するエラーです。 例えば、以下のようにユニーク制約に違反して INSERT すると Duplicate entry が出ます。 mysql> INSERT INTO report (report_on, referrer, dau) VALUES ( ' 2024-05-30 ' , ' campaign_202404 ' , 1 ); Query OK, 1 row affected ( 0 . 01 sec) mysql> INSERT INTO report (report_on, referrer, dau) VALUES ( ' 2024-05-30 ' , ' campaign_202405 ' , 1 ); Query OK, 1 row affected ( 0 . 00 sec) mysql> INSERT INTO report (report_on, referrer, dau) VALUES ( ' 2024-05-30 ' , ' campaign_202405 ' , 1 ); ERROR 1062 ( 23000 ): Duplicate entry ' 2024-05-30-campaign_202405 ' for key ' report.report_row ' 1. Duplicate entry が出たらトランザクションをやり直す use DBI; use Sub::Retry qw/retry/ ; retry $retry_times , $delay , sub { try { $dbh->begin_work ; my $report = $dbh->selectrow_hashref ( "SELECT * FROM report WHERE report_on = ? AND referrer = ?" , undef , $today , $referrer ); if ( $report ) { $dbh->do ( "UPDATE report SET dau = dau + 1 WHERE id = ?" , undef , $report->{ id } ); } else { $dbh->do ( "INSERT INTO report (report_on, referrer, dau) VALUES (?, ?, 1)" , undef , $today , $referrer ); } $dbh->commit ; } catch { $dbh->rollback ; die $@ ; } }; メリット Duplicate entry でエラーにならない デメリット トランザクションが大きい場合、時間がかかる 一番シンプルなやり方で、標準 SQL の範囲にも収まっているので、MySQL に限らず他の DB エンジンでも使える手法です。 ですが、トランザクションが大きい場合、全体をやり直すため実行時間が長くなってしまいます。 ただし、トランザクションのやり直しは同時に INSERT しようとしたときだけなので、毎回発生するわけではありません。 また、この方法では同時に複数のトランザクションが実行されていた場合に Duplicate entry が起こり得ます。 別トランザクションで INSERT されたレコードが最初の SELECT で取得できなかった場合、こちらのトランザクションでも INSERT してしまうので Duplicate entry が出ます。 注意点としては、最初の SELECT で FOR UPDATE をつけると、2 つのトランザクションが同時に実行されたときに Deadlock が起こります。 +-------------------------+-------------------------+ | Transaction 1 | Transaction 2 | +-------------------------+-------------------------+ | BEGIN; | BEGIN; | | SELECT ... FOR UPDATE; | | | | SELECT ... FOR UPDATE; | | INSERT INTO report ...; | | | | INSERT INTO report ...; | +-------------------------+-------------------------+ まず、 SELECT ... FOR UPDATE で該当レコードが存在しないとギャップロックが取得されます。 ギャップロックは共有ロックなので他のトランザクションをブロックしません。 一方、INSERT は排他ロックなので他のトランザクションをブロックし、INSERT が互いにブロックすると Deadlock になります。 トランザクション分離レベルが REPEATABLE READ の場合、ギャップロックは避けられません。 駅奪取で Duplicate entry が出ていた箇所もできればこの方法で直したかったですが、トランザクションが大きく影響範囲も大きかったのと、実行時間にも懸念があったので、別の方法にしました。 2. INSERT ... ON DUPLICATE KEY UPDATE $dbh->do ( "INSERT INTO report (report_on, referrer, dau) VALUES (?, ?, 1) ON DUPLICATE KEY UPDATE dau = dau + 1" , undef , $today , $referrer , ); メリット クエリが 1 つで済む デメリット 複数のユニークキーが存在するテーブルには非推奨 AUTO INCREMENT の値が余分に増えてしまう MySQL の場合、 INSERT ... ON DUPLICATE KEY UPDATE を使うことで 1 つのクエリで実現できます。 一見これで解決しそうですが、いくつか注意点があります。 一般に、一意のインデックスが複数含まれているテーブルに対して ON DUPLICATE KEY UPDATE 句を使用することは避けるようにしてください。 MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.2.6.2 INSERT ... ON DUPLICATE KEY UPDATE ステートメント まず、MySQL のドキュメントにも書かれている通り、ユニークキーが複数ある場合は非推奨となっているため、そのようなテーブルに対しては避けたほうが良さそうです。 また、テーブルに AUTO INCREMENT のカラムがある場合、UPDATE の場合でも AUTO INCREMENT の値が増えてしまいます。 UPDATE の頻度が高いテーブルであれば、AUTO INCREMENT の値がどんどん大きくなり、場合によってはオーバーフローのおそれもあります。 innodb_autoinc_lock_mode を 0 にすることで UPDATE で AUTO INCREMENT の値が増えなくなりますが、並列性が下がります。 また、以下のように最初に SELECT して存在しない場合だけ INSERT ... ON DUPLICATE KEY UPDATE をすることで、AUTO INCREMENT の値が増えにくくなります。 $dbh->begin_work ; my $report = $dbh->selectrow_hashref ( "SELECT * FROM report WHERE report_on = ? AND referrer = ?" , undef , $today , $referrer ); if ( $report ) { $dbh->do ( "UPDATE report SET dau = dau + 1 WHERE id = ?" , undef , $report->{ id } ); } else { $dbh->do ( "INSERT INTO report (report_on, referrer, dau) VALUES (?, ?, 1) ON DUPLICATE KEY UPDATE dau = dau + 1" , undef , $today , $referrer , ); } $dbh->commit ; 3. とりあえず INSERT して Duplicate entry が出たら SELECT use DBI; use Try::Tiny; $dbh->begin_work ; try { $dbh->do ( "INSERT INTO report (report_on, referrer, dau) VALUES (?, ?, 1)" , undef , $today , $referrer ); } catch { my $error = $_ ; if ( $error =~ / Duplicate entry / ) { $report = $dbh->selectrow_hashref ( "SELECT * FROM report WHERE report_on = ? AND referrer = ? FOR UPDATE = 1" , undef , $today , $referrer ); $dbh->do ( "UPDATE report SET dau = dau + 1 WHERE id = ?" , undef , $report->{ id } ); } else { die $error ; } }; $dbh->commit ; メリット Duplicate entry は起きなくなる トランザクション全体をやり直さないので 1. よりは早い デメリット Deadlock が起きるようになる この方法は、シンプルでわかりやすく、一見問題なさそうです。 しかし、1. と同様に SELECT するときに FOR UPDATE しているためギャップロックが発生します。 そのため、同時に INSERT しようとしたときに Deadlock が起きてしまうのでおすすめしません。 4. 前もって必要なレコードを INSERT しておく use DBI; my $rows = $dbh->selectrow_arrayref ( " SELECT DISTINCT referrer FROM report WHERE report_on = ? AND dau > 0 " , {}, $today ); for my $row ( @$rows ) { $dbh->do ( "INSERT INTO report (report_on, referrer) VALUES (?, ?)" , undef , $tomorrow , $rows->{ referrer } ); } use DBI; $dbh->begin_work ; # 前もってINSERTしているので基本的にレコードが存在する my $report = $dbh->selectrow_hashref ( "SELECT * FROM report WHERE report_on = ? AND referrer = ?" , undef , $today , $referrer ); if ( $report ) { $dbh->do ( "UPDATE report SET dau = dau + 1 WHERE id = ?" , undef , $report->{ id } ); } else { # こちらに来ることはほとんどないので、Duplicate entryが起きることもほとんどない $dbh->do ( "INSERT INTO report (report_on, referrer, dau) VALUES (?, ?, 1)" , undef , $today , $referrer ); } $dbh->commit ; メリット 既存のコードの変更が不要で、副作用が少ない デメリット Duplicate entry が絶対に出なくなるわけではない これは、どのようなレコードが INSERT されるか前もってわかるのであれば、先に INSERT しておくことで同時に INSERT されずに済むという解決策です。 例えば、今回の例に出したテーブルは、毎日 referrer ごとにレコードが INSERT されます。 そこで、翌日のレコードを前日のうちにあらかじめ INSERT しておくことで、基本的に UPDATE するだけで済みます。 これにより、Duplicate entry が発生しづらくなります。 一方で、前日存在しなかった referrer が同時に INSERT されようとした場合は Duplicate entry が発生し得ます。 ただ、referrer の種類があまり増えないものであったり、同時にくることが少なければ、これで十分な対策になります。 駅奪取では日替わりタイミングで起きていた Duplicate entry をこの方法でなくすことができました。 5. トランザクションが同時に走らないようにロックを取る use DBI; use Try::Tiny; my $key = $today . '_' . $referrer ; try { my $get_lock = $dbh->selectrow_array ( "SELECT GET_LOCK(?, ?)" , undef , $key , $timeout ); if ( $get_lock ) { $dbh->begin_work ; my $report = $dbh->selectrow_hashref ( "SELECT id, dau FROM report WHERE report_on = ? AND referrer = ?" , undef , $today , $referrer ); if ( $report ) { $dbh->do ( "UPDATE report SET dau = dau + 1 WHERE id = ?" , undef , $report_id ); } else { $dbh->do ( "INSERT INTO report (report_on, referrer, dau) VALUES (?, ?, 1)" , undef , $today , $referrer ); } $dbh->commit ; $dbh->do ( "SELECT RELEASE_LOCK(?)" , undef , $key ); } else { die "cannot get lock" ; } } catch { $dbh->rollback ; $dbh->do ( "SELECT RELEASE_LOCK(?)" , undef , $key ); }; メリット トランザクションが同時に走らないので Duplicate entry や Deadlock が起きなくなる デメリット 並列性が下がり、パフォーマンスが低下する トランザクションが同時に走るから Duplicate entry が起きるのであれば、同時に走らないようにロックしてしまうという手もあります。 MySQL で完結する仕組みとして GET_LOCK と RELEASE_LOCK を使う方法があります。 ただし、データベース分離レベルを SERIALIZABLE にするのと同じようなことをしているので、並列性が下がり、パフォーマンスが低下するので気をつけないといけません。 トランザクション内でリトライしたくない処理がある場合は、この方法を使うとうまく実装できるかもしれません。 まとめ この記事では、MySQL で「無ければ INSERT、あれば UPDATE」を実現するための方法をいくつか紹介しました。 各方法にはメリット・デメリットがあり、ケースバイケースで使い分けが必要です。 できれば 1. もしくは 2. のように修正するのがいいと思うのですが、スキーマや影響範囲、パフォーマンスの制約を考慮すると難しい場合もあります。 その場合は、他の方法を使ってみるとうまくいくかもしれません。 参考資料 MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.2.6.2 INSERT ... ON DUPLICATE KEY UPDATE ステートメント 第145回 InnoDBの行ロック状態を確認する[その1] | gihyo.jp なかったらINSERTしたいし、あるならロック取りたいやん? | PPT
駅奪取チームでエンジニアをしている id:kebhr です。 今回は、駅奪取チームにおけるプロジェクト管理のツールとして、従来利用していたガントチャートに加え、新たに バッファ傾向グラフ を導入してみた経験について書きます。 バッファ傾向グラフとは このプロジェクトでは、プロジェクト管理手法として CCPM (Critical Chain Project Management) を採用しました。 CCPM では、個別のタスクにはバッファを設けず、すべてのバッファをプロジェクトの終盤に設けます。このバッファをプロジェクトバッファと呼びます。 必然的に、プロジェクトが進行するにつれて、プロジェクトバッファを消費していきます。 プロジェクトバッファの消費量を可視化するツールがバッファ傾向グラフです。 下図のような、横軸に日付、縦軸にバッファ消費率を取るグラフです。 バッファ傾向グラフ バッファ消費率は、 プロジェクトバッファの総量に対するプロジェクトバッファの消費量の割合 として求めます。 プロジェクトバッファの消費量は、 完了していないタスクの終了予定日のうち最も早い日と、その日の差 として求めます。 重視したこと バッファ傾向グラフを運用する上で、運用に時間を掛けないよう、 グラフの運用が容易に行えること を重視しました。 そのため、横軸には、一般的にバッファ傾向グラフで用いられるプロジェクト進捗率ではなく日付を採用しました。 横軸に日付を採用することで、ガントチャートとバッファ傾向グラフの横軸が 1:1 対応するため、スプレッドシート上での管理が容易です。 バッファ消費量についても、このルールで求める場合、ガントチャートからある時点でのバッファ消費率を簡単に求めることができます。 利点 バッファ傾向グラフを使用したことで、次のような利点がありました。 プロジェクトの進捗を、プロジェクト内外のメンバーが一目で理解できる 遅延に対する介入の必要性を、統一された基準に基づいて判断できる 1 つ目の利点は、ガントチャートの利点でもあります。しかし、ガントチャートに比べて、グラフ 1 つで済むバッファ傾向グラフはより簡潔で、理解しやすいものになります。 2 つ目の利点は、バッファ傾向グラフを使用することにより得られる利点です。 プロジェクトに遅延はつきものです。しかし、その遅延が許容される程度の遅延なのか、介入の必要がある遅延なのかといった評価は、各メンバーによって異なる場合があります。 バッファ傾向グラフは明快な判断基準を与えてくれます。 問題点 一方で、現状の仕組みは不完全であり、次のような問題点を抱えていることもわかりました。 タスクの着手順序を入れ替えた際に、そのタスクが完了しないまま終了予定日を過ぎると、プロジェクトバッファの消費量が増え続ける この問題を解決する手段には、以下の 2 通りがあります。 タスクの着手順序を入れ替えた際に、タスクの終了予定日を入れ替える プロジェクトバッファの消費量の定義を変更する プロジェクトバッファの消費量から、先行して着手できたタスクの本来の着手予定日からその日までの日数を引いた値とする 1 つ目の手段は、簡単に行えて影響が小さいです。しかし、変更の履歴を残すことが難しいです。 プロジェクト完了後の振り返りにおいて、タスク着手順序の変更を振り返りの対象とすることを容易でなくする可能性があります。 2 つめの手段は、仕組みそのものの改善策になります。しかし、スプレッドシート上で行うにはやや複雑さがあります。 バッファ傾向グラフの運用が属人化することは避ける必要があります。 まとめ バッファ傾向グラフは簡単に作成することができ、ガントチャートの隙間を埋めることができるツールです。しかし、最大限の効果を発揮するためには調整を重ねる必要があります。 複数のプロジェクトを繰り返す中でより仕組みが洗練されれば、また紹介したいと思います。
駅メモ!開発基盤チームの id:xztaityozx です。 今回はテスト実行のボトルネックを OverlayFS を利用することで解消した話と、OverlayFS の動作を調べるために bpftrace を使った話をします。 かんたん概要 Test::mysqld を使って挿入済みのデータを持った mysqld をテストごとに起動していた データが増えてきたことでコピーがめちゃくちゃ遅くなり、開発体験が最悪になった コピーを OverlayFS でのマウントに置き換えてすごく速くした 動作について気になる点があったので bpftrace を使ってトレースを行い、カーネル関数の呼び出しも観察した 前提 この記事で登場する主なツールのバージョンを示します Ubuntu 22.04.4(WSL2) カーネル: 5.15.146.1-microsoft-standard-WSL2 hyperfine 1.18.0 Docker 26.0.0, build 2ae903e sysbench 1.0.20 bpftrace 0.12.0 背景 単体テストで用いる DB をどのように起動していますか? Test::mysqld などを用いて mysqld を起動したり、MySQL コンテナを起動したりなど様々かと思います。駅メモ!でも先にも登場した Test::mysqld と App::Prove::Plugin::MySQLPool を用いて、テスト開始前に専用の mysqld を起動していました。 Test::mysqld は copy_data_from にディレクトリへのパスを渡すことで、そのディレクトリのコピーを MySQL のデータディレクトリとすることができます。つまり、データを複製して起動することができるということですね。 駅メモ!ではこの仕組みを用いることで、テストで必ず使うことになるデータ(例えばマスターデータ)が事前に挿入された MySQL を起動しています。こうすることでテストの開始にかかる時間の短縮と複雑なデータセットの挿入の簡略化を狙っています。元々どんな目的で運用されていたかはわかりませんが、少なくとも現在はこの目的で運用しています。 課題点 この運用は課題点がありました。それは事前挿入しておきたいデータが増えたことにより、コピーに時間がかかりすぎるようになったことです。具体的にかかる時間として、コピー元のデータが壊れていないこと前提で、平均 40 秒ほどかかる状況でした。 1 回当たりのテスト実行のたびに 40 秒+テスト実行時間が掛かっていれば開発体験は最悪です。エンジニアは少し変更しては待機、少し変更しては待機を繰り返す必要があるわけですが、何か別のことをするには短すぎ、計算資源だけではなく人件費の無駄にもなっていました。 もちろんチームではなにも対策をしていなかったわけではないのですが、次の手が見つかっていない状況でした。 課題に対するアプローチ 先にも述べた通り課題点はコピーすべきデータが増えてしまったことです。そこでまずは挿入しておきたいデータを無くす、もしくは最小限に減らすことです。そのテストで必要なデータは before all などでテストごとに挿入します。データセットの柔軟性もよくなるのでこの方法を採用したいですが、複雑なデータセットの挿入を再現するのにかかるコストが高いこと、直すべきテストが多すぎることから取りたくない方法でした。 次に考えたのはデータ入りのコンテナイメージを作っておく方法です。なんでも試そうということでやってみましたが、これは少しだけ効果がありました。 $ docker run --rm -d -p 3306:3306 \ -e MYSQL_ALLOW_EMPTY_PASSWORD = 1 \ -v $PWD /data:/var/lib/mysql mysql:8. 0 . 34 # だいたい3GBのデータを挿入... $ docker stop .... $ cat <<EOF > Dockerfile FROM mysql:8.0.34 COPY ./data /var/lib/mysql EOF $ docker build -t hoge . $ hyperfine --warmup = 3 \ --prepare =' docker stop db||true ' -- \ ' docker run --rm -d -e MYSQL_ALLOW_EMPTY_PASSWORD=1 --name db -P hoge ' Benchmark 1: docker run --rm -d -e MYSQL_ALLOW_EMPTY_PASSWORD = 1 --name db -P hoge Time ( mean ± σ ) : 8 . 716 s ± 2 . 697 s [ User: 0 . 008 s, System: 0 . 014 s ] Range ( min … max ) : 6 . 480 s … 14 . 670 s 10 runs 大体 8 秒程度で起動できるようになりました。40 秒からかなり高速化できています。しかしながら、イメージサイズが大きくなるにつれて起動時間が増加することを確認できたため、いずれ同じ課題に直面してしまうことが考えられました。 とはいえ、データディレクトリをコピーするよりかなり速いことには変わりなく、このあたりにヒントがありそうでした。 OverlayFS ヒントというか答えは OverlayFS でした。OverlayFS は Linux カーネル 3.18 で追加された複数のファイルシステムを 1 つにマージする仕組みで、Docker にも用いられています。OverlayFS では書き込み時に元のファイルをコピーしてくる(Copy on Write)ので、OverlayFS 作成時にはコピーのコストが乗って来ません。Docker コンテナは(ストレージドライバにもよりますが)OverlayFS による重ね合わせで実現されるため、単純にコピーするより速く起動できたということでした。さらに詳しい解説は Docker のドキュメント やその他の解説記事をご参照ください。 さて、この OverlayFS は通常 mount コマンドで作成しますが、 docker run 時に --mount オプションを以下のように書けば OverlayFS の作成とマウントが同時に行えます。ちょっと複雑ですが… $ docker run --rm -it --mount type= volume, dst =/tmp/hoge,volume-driver =local ,volume-opt = type = overlay, \" volume-opt = o = lowerdir = ${PWD} /lower,upperdir = ${PWD} /upper,workdir = ${PWD} /work \" ,volume-opt = device = overlay ubuntu 作成した Ubuntu コンテナ内で df コマンドを実行し、マウントされた OverlayFS を確認してみます $ df -h /tmp/hoge Filesystem Size Used Avail Use% Mounted on overlay 1007G 29G 928G 3% /tmp/hoge OverlayFS をコンテナにマウントできることがわかったので、これを今回の課題に応用していきます。つまりホストマシンに用意したデータディレクトリを OverlayFS としてコンテナにマウントすればよいということです。データ入りコンテナと同じように hyperfine でのベンチマークを取ってみます。 $ hyperfine --warmup = 3 \ --prepare ' docker stop db||true; sudo rm -rf ./upper ./work;mkdir ./upper ./work ' -- \ ' docker run --rm -d -P --name db -e MYSQL_ALLOW_EMPTY_PASSWORD=1 --mount type=volume,dst=/var/lib/mysql,volume-driver=local,volume-opt=type=overlay,\"volume-opt=o=lowerdir=${PWD}/data,upperdir=${PWD}/upper,workdir=${PWD}/work\",volume-opt=device=overlay mysql:8.0.34 ' Benchmark 1: docker run --rm -d -P --name db -e MYSQL_ALLOW_EMPTY_PASSWORD = 1 --mount type= volume, dst =/var/lib/mysql,volume-driver =local ,volume-opt = type = overlay, \" volume-opt = o = lowerdir = ${PWD} /data,upperdir = ${PWD} /upper,workdir = ${PWD} /work \" ,volume-opt = device = overlay mysql:8. 0 . 34 Time ( mean ± σ ) : 316 . 6 ms ± 38 . 3 ms [ User: 5 . 0 ms, System: 10 . 2 ms ] Range ( min … max ) : 271 . 5 ms … 392 . 3 ms 10 runs 300 ミリ秒程度でコンテナの起動が完了しています。40 秒から比べると 100 倍以上高速です。 mysqld が使えるまでの時間を調べる Copy on Write(CoW)の影響が起動シーケンスにどれぐらい出るのかも調べてみます。今回は以下のようなシェルスクリプトと hyperfine で簡単なベンチマークを取ることで検証とします。 #!/usr/bin/env zsh CONTAINER_NAME=db function prepare-container-with-overlayfs() { docker stop " $CONTAINER_NAME " || true sudo rm -rf ./work ./upper mkdir ./work ./upper docker run --rm -d -p 3306 : 3306 \ --name " $CONTAINER_NAME " \ --mount type =volume,dst=/var/lib/mysql,volume-driver= local ,volume-opt= type =overlay, \" volume-opt=o=lowerdir= ${PWD} /data,upperdir= ${PWD} /upper,workdir= ${PWD} /work \" ,volume-opt=device=overlay \ -e MYSQL_ALLOW_EMPTY_PASSWORD= 1 \ mysql: 8.0 . 34 } function prepare-container() { docker stop " $CONTAINER_NAME " || true docker run --rm -d -p 3306 : 3306 \ --name " $CONTAINER_NAME " \ -e MYSQL_ALLOW_EMPTY_PASSWORD= 1 \ hoge } function launch() { while ! docker exec " $CONTAINER_NAME " mysqladmin ping --silent; do ; done while ! docker exec " $CONTAINER_NAME " mysql -e "SELECT 1" &> /dev/null; do ; done } if [[ " $1 " == "prepare-container-with-overlayfs" ]]; then prepare-container-with-overlayfs elif [[ " $1 " == "prepare-container" ]]; then prepare-container elif [[ " $1 " == "launch" ]]; then launch else exit 1 fi hoge イメージと ./data は以下の手順で作成しています $ docker run -d --rm \ -e MYSQL_ALLOW_EMPTY_PASSWORD = 1 \ -e MYSQL_DATABASE =test \ -- name hoge \ -v $PWD /data:/var/lib/mysql mysql:8. 0 . 34 $ docker exec hoge mysql -e ' create database test ' $ sysbench /usr/share/sysbench/oltp_read_write.lua \ --db-driver = mysql \ --mysql-db =test \ --mysql-user = root \ --mysql-password ="" \ --mysql-host = 127 . 0 . 0 . 1 \ --mysql-port = 3306 \ --tables = 7 \ --table-size = 1000000 prepare $ docker stop hoge $ cat <<EOF > Dockerfile FROM mysql:8.0.34 COPY ./data /var/lib/mysql EOF $ docker build -t hoge . 実行結果は以下の通りです。 # データ入りのコンテナの場合 $ hyperfine --warmup = 3 --prepare =' ./benchmark.zsh prepare-container ' -- ' ./benchmark.zsh launch ' Time ( mean ± σ ) : 918 . 3 ms ± 64 . 5 ms [ User: 158 . 4 ms, System: 142 . 4 ms ] Range ( min … max ) : 855 . 9 ms … 1049 . 1 ms 10 runs # OverlayFSなデータディレクトリの場合 $ hyperfine --warmup = 3 --prepare =' ./benchmark.zsh prepare-container-with-overlayfs ' -- ' ./benchmark.zsh launch ' Time ( mean ± σ ) : 935 . 5 ms ± 92 . 6 ms [ User: 165 . 7 ms, System: 174 . 3 ms ] Range ( min … max ) : 831 . 3 ms … 1149 . 7 ms 10 runs 20 ミリ秒程度の差があるようでした。OverlayFS なデータディレクトリの場合は mysqld の起動シーケンス中にほとんどのファイルで CoW が発生し、そのぶん時間がかかってしまうと思っていたのですが、20 ミリ秒ということであればそういうわけではなさそうです。 クエリを実行してみる テスト用の DB を高速にするという課題は、OverlayFS なデータボリュームを使うことで解決できそうなことがわかりました。準備を自動で行うスクリプトを実装・配布、実際に利用してもらい、改善されたことが確認できました。 しかしながら、起動時の処理では CoW がほとんど発生していないことが個人的には気になっていました。 そこで、ここをもう少し追うことにしました。具体的には、起動直後のコンテナに対して sysbench を流すことでコピーのコストが発生するかを確認してみます。CoW の動作を見たいので、 oltp_write_only を使うこととします。ここからは趣味です。 まずはデータ入りのコンテナに対して実行したものを示します $ sysbench /usr/share/sysbench/oltp_write_only.lua \ --db-driver = mysql \ --mysql-db =test \ --mysql-user = root \ --mysql-password ="" \ --mysql-host = 127 . 0 . 0 . 1 \ --mysql-port = 3306 \ --tables = 7 \ --table-size = 1000000 run sysbench 1 . 0 . 20 ( using system LuaJIT 2 . 1 .0-beta3 ) Running the test with following options: Number of threads: 1 Initializing random number generator from current time Initializing worker threads... Threads started! SQL statistics: queries performed: read: 0 write: 7164 other: 3582 total: 10746 transactions: 1791 ( 179 . 04 per sec. ) queries: 10746 ( 1074 . 25 per sec. ) ignored errors: 0 ( 0 . 00 per sec. ) reconnects: 0 ( 0 . 00 per sec. ) General statistics: total time: 10 .0029s total number of events: 1791 Latency ( ms ) : min: 2 . 58 avg: 5 . 58 max: 15 . 51 95th percentile: 9 . 91 sum: 9999 . 11 Threads fairness: events ( avg/stddev ) : 1791 . 0000 / 0 . 00 execution time ( avg/stddev ) : 9 . 9991 / 0 . 00 次に OverlayFS なデータディレクトリの場合です。 $ sysbench /usr/share/sysbench/oltp_write_only.lua \ --db-driver = mysql \ --mysql-db =test \ --mysql-user = root \ --mysql-password ="" \ --mysql-host = 127 . 0 . 0 . 1 \ --mysql-port = 3306 \ --tables = 7 \ --table-size = 1000000 run sysbench 1 . 0 . 20 ( using system LuaJIT 2 . 1 .0-beta3 ) Running the test with following options: Number of threads: 1 Initializing random number generator from current time Initializing worker threads... Threads started! SQL statistics: queries performed: read: 0 write: 5348 other: 2674 total: 8022 transactions: 1337 ( 133 . 68 per sec. ) queries: 8022 ( 802 . 11 per sec. ) ignored errors: 0 ( 0 . 00 per sec. ) reconnects: 0 ( 0 . 00 per sec. ) General statistics: total time: 10 .0008s total number of events: 1337 Latency ( ms ) : min: 2 . 86 avg: 7 . 48 max: 1651 . 00 95th percentile: 9 . 91 sum: 9997 . 96 Threads fairness: events ( avg/stddev ) : 1337 . 0000 / 0 . 00 execution time ( avg/stddev ) : 9 . 9980 / 0 . 00 注目したいのは Latency (ms) の max です。データ入りコンテナの場合は 15.51 ミリ秒 なのに対して、OverlayFS なデータディレクトリの場合は 1651.00 ミリ秒 となっています。そのほかの Latency の統計については特に差はありません。Write のどのタイミングで発生しているかは明確ではないですが、CoW のコストが Write に掛かっているように見えます。 CoW のコストがいつ乗ってくるのかを確認してみる CoW の影響があるのは、書き込みクエリ実行時であることはなんとなく予想できたので、次はそのコストがいつ発生するのかを見ていきます。CoW の性質を考えると初回の書き込みが Commit されたときであるのは自明なのですが、気になったので調べます。 調べ方としては、以下のようなシェルスクリプトを OverlayFS なデータディレクトリを持つコンテナに対して実行するというものです。 for i ($( seq 10 )) ; do s = $( date +%s%3N ) docker exec db mysql -e " insert into test.sbtest1(k, c, pad) values(99999999, 'char', 'pad') " echo $i : $(( $ ( date +%s%3N ) - $s)) ms done sysbench が作ったテーブルに対して 1 件のレコードを挿入することを 10 回繰り返しています。実行後は挿入にかかった時間をミリ秒で出力しています。 これを実行してみると以下のような結果が得られました。 1 : 384 ms 2 : 43 ms 3 : 45 ms 4 : 38 ms 5 : 38 ms 6 : 39 ms 7 : 39 ms 8 : 37 ms 9 : 38 ms 10 : 36 ms 1 回目だけ 10 倍程度遅いですね。ここに CoW のコストが乗っていると考えてよさそうです。 bpftrace で本当に 1 回目だけコピーが発生しているかを見てみる はじめて書き込みを行ったときにコピーのコストが乗ってきているのはほぼそうだと言えそうなのですが、ここまで来たらカーネルのトレースをしてコピー系の関数が最初だけ呼ばれていることを確認したいと思ったのでやってみます。 今回これを確認するために使ったのは bpftrace/bpftrace という、 awk と似た文法で書けるトレーシングツールです。詳しい解説は README やその他の解説記事などをご覧ください。 github.com OverlayFS のヘッダファイル を見てみると、OverlayFS に関する関数は ovl_ で始まることが分かるので $ bpftrace ' kprobe:ovl_* { printf("%s\n", func) } ' などとして OverlayFS で CoW が発生するとき、しないときの関数呼び出しを記録、両者を比較すると以下のような呼び出し順の違いがありました。因みにめちゃくちゃざっくりで、めちゃくちゃ端折っています CoW なとき CoW じゃないとき sys_enter_openat sys_enter_openat ovl_open ovl_open ovl_already_copied_up ovl_already_copied_up ovl_already_copied_up ovl_copy_up_start ovl_copy_up_end ovl_already_copied_up これらの関数の呼び出しをトレースしてみます。トレースするのは以下のようなシェルスクリプトを実行する間です。内容としてはコンテナの起動と sbtest2 テーブルへの書き込みを繰り返すものです。1 回目だけ CoW が発生するはずなので、その様子を確認します。 sudo rm -rf ./upper ./work mkdir ./upper ./work for i ($( seq 3 )) ; do docker stop db sleep 10 docker run --rm -d -p 3306:3306 \ --name " $CONTAINER_NAME " \ --mount type= volume, dst =/var/lib/mysql,volume-driver = local ,volume-opt = type = overlay, \" volume-opt = o = lowerdir = ${PWD} /data, upperdir = ${PWD} /upper, workdir = ${PWD} /work \" ,volume-opt = device = overlay \ -e MYSQL_ALLOW_EMPTY_PASSWORD = 1 \ mysql:8. 0 . 34 sleep 10 ; s = $( date +%s%3N ) docker exec db mysql -e " insert into test.sbtest2(k, c, pad) values( $RANDOM , 'char', 'pad') " echo $i : $(( $ ( date +%s%3N ) - $s)) ms done docker stop db bpftrace のコードは以下の通りです。 tracepoint : syscalls : sys_enter_openat /str(args->filename) == " . / test /sbtest1 . ibd" / { printf ( "elapsed: %ldms \t comm: %s\t probe: %s\t name: %s\t flags: %d\n " , elapsed / 1_000_000 , comm , probe , str(args - >filename) , args - >flags) } kfunc : ovl_open { printf ( "elapsed: %ldms \t comm: %s\t probe: %s\t name: %s\n " , elapsed / 1_000_000 , comm , probe , str(args - >file - >f_path.dentry - >d_name.name)) } kfunc : ovl_copy_up_start , kfunc : ovl_copy_up_end { printf ( "elapsed: %ldms \t comm: %s\t probe: %s\t name: %s\n " , elapsed / 1_000_000 , comm , probe , str(args - >dentry - >d_name.name)) } kretfunc : ovl_already_copied_up { printf ( "elapsed: %ldms \t comm: %s\t probe: %s\t name: %s\t retval: %d\n " , elapsed / 1_000_000 , comm , probe , str(args - >dentry - >d_name.name) , retval) } シェルスクリプトの実行ログは以下の通りです。 Error response from daemon: No such container: db 8eefa5dec9b920fa2b483a32f86e4b90ac1089fd750fd2f3e6fd28efcc1a73d5 1 : 359 ms db 0ba1c5ed2784922a515ff0ae1fd5d75dfcc2db0892b2c54f6ef5fbbc8c849992 2 : 44 ms db 4da332ad30eb565dcb0ec91e67b4a705ec52c1592298180e66c23995ccb7c853 3 : 41 ms db トレースログは以下の通りです。 # sbtest2のイベントだけに注目する # elapsed: 経過時間(ms) # comm: タスク名。/proc/<pid>/comm の内容 # prove: プローブの名前。今回は呼び出された関数の名前ととらえてOK # name: 関数の引数から取り出した操作対象のファイル名 # flags: ファイルのオープンモード # retval: 関数の戻り値。今回は ovl_already_copied_up だけ出力させていて、この関数の戻り値はbool型 $ cat trace.log | grep sbtest2 | column -t elapsed: 20757ms comm: boot probe: tracepoint:syscalls:sys_enter_openat name: ./ test /sbtest2.ibd flags: 0 elapsed: 20757ms comm: boot probe: kfunc:ovl_open name: sbtest2.ibd elapsed: 20757ms comm: boot probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 0 elapsed: 20972ms comm: boot probe: tracepoint:syscalls:sys_enter_openat name: ./ test /sbtest2.ibd flags: 0 elapsed: 20972ms comm: boot probe: kfunc:ovl_open name: sbtest2.ibd elapsed: 20972ms comm: boot probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 0 elapsed: 30413ms comm: connection probe: tracepoint:syscalls:sys_enter_openat name: ./ test /sbtest2.ibd flags: 0 elapsed: 30413ms comm: connection probe: kfunc:ovl_open name: sbtest2.ibd elapsed: 30413ms comm: connection probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 0 elapsed: 30414ms comm: connection probe: tracepoint:syscalls:sys_enter_openat name: ./ test /sbtest2.ibd flags: 2 elapsed: 30414ms comm: connection probe: kfunc:ovl_open name: sbtest2.ibd elapsed: 30414ms comm: connection probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 0 elapsed: 30414ms comm: connection probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 0 elapsed: 30414ms comm: connection probe: kfunc:ovl_copy_up_start name: sbtest2.ibd elapsed: 30729ms comm: connection probe: kfunc:ovl_copy_up_end name: sbtest2.ibd elapsed: 30729ms comm: connection probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 1 elapsed: 42805ms comm: boot probe: tracepoint:syscalls:sys_enter_openat name: ./ test /sbtest2.ibd flags: 0 elapsed: 42805ms comm: boot probe: kfunc:ovl_open name: sbtest2.ibd elapsed: 42805ms comm: boot probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 1 elapsed: 42925ms comm: boot probe: tracepoint:syscalls:sys_enter_openat name: ./ test /sbtest2.ibd flags: 0 elapsed: 42925ms comm: boot probe: kfunc:ovl_open name: sbtest2.ibd elapsed: 42925ms comm: boot probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 1 elapsed: 42953ms comm: ib_buf_dump probe: tracepoint:syscalls:sys_enter_openat name: ./ test /sbtest2.ibd flags: 0 elapsed: 42953ms comm: ib_buf_dump probe: kfunc:ovl_open name: sbtest2.ibd elapsed: 42953ms comm: ib_buf_dump probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 1 elapsed: 42953ms comm: ib_buf_dump probe: tracepoint:syscalls:sys_enter_openat name: ./ test /sbtest2.ibd flags: 2 elapsed: 42953ms comm: ib_buf_dump probe: kfunc:ovl_open name: sbtest2.ibd elapsed: 42953ms comm: ib_buf_dump probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 1 elapsed: 64644ms comm: boot probe: tracepoint:syscalls:sys_enter_openat name: ./ test /sbtest2.ibd flags: 0 elapsed: 64644ms comm: boot probe: kfunc:ovl_open name: sbtest2.ibd elapsed: 64644ms comm: boot probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 1 elapsed: 64764ms comm: boot probe: tracepoint:syscalls:sys_enter_openat name: ./ test /sbtest2.ibd flags: 0 elapsed: 64764ms comm: boot probe: kfunc:ovl_open name: sbtest2.ibd elapsed: 64764ms comm: boot probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 1 elapsed: 65764ms comm: ib_src_main probe: tracepoint:syscalls:sys_enter_openat name: ./ test /sbtest2.ibd flags: 0 elapsed: 65764ms comm: ib_src_main probe: kfunc:ovl_open name: sbtest2.ibd elapsed: 65764ms comm: ib_src_main probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 1 elapsed: 65765ms comm: ib_src_main probe: tracepoint:syscalls:sys_enter_openat name: ./ test /sbtest2.ibd flags: 2 elapsed: 65765ms comm: ib_src_main probe: kfunc:ovl_open name: sbtest2.ibd elapsed: 65765ms comm: ib_src_main probe: kretfunc:ovl_already_copied_up name: sbtest2.ibd retval: 1 1 回目だけ copy_up_start と copy_up_end が呼ばれており、その処理に 315 ミリ秒かかっていることがわかります。シェルスクリプトの 1 回目の実行時間は 359 ミリ秒なので、辻褄があっていそうです。 さらに 2 回目以降は copy_up_start と copy_up_end の前の ovl_already_copied_up が 1 を返しており、コピーアップ系の処理がスキップされているのがわかります。以上で無事 CoW の様子を観察することができました。 (ところでクエリ実行中の comm が connection や ib_buf_dump 、 ib_src_main だったりするのはなぜなんでしょう?) まとめ この記事では単体テストで利用する DB を高速に起動する方法として、OverlayFS を使った例を示しました。さらに bpftrace を使ったトレースを行い、OverlayFS の挙動の一部を観察、理解することができました。 単体テスト用 DB 高速起動の仕組みは、すでにチームには実験段階の機能として公開しており、使い心地などをフィードバックしてもらっている段階です。また何か進展があったら記事を書きたいと思います。 以上です。
言葉の定義 モバファクの 1on1 の目的 1on1 で自分が大事にしていること 1on1 はメンティーの時間である 1on1 はメンターの時間でもある 1on1 初回 今使っている 1on1 のフォーマット 体調 半期目標の進捗振り返り ネクストアクションの振り返り うまくいかなかったこと・もっとよくなりそうなところ・うまくいったこと・その他に話したいこと ネクストアクション 1on1 の中でのやりとり お休みの取り方がわからない 最近見積もりの精度が高くなっている 朝会の議事録をとるようにしたい 最近チームの動きがぎこちないと感じている 1on1 定期的な振り返り まとめ こんにちは。駅メモエンジニアの id:dorapon2000 です。 今回は自分自身がメンター側として実施している 1on1 について、どのように実施しているのかご紹介しようと思います。 1on1 のやり方はメンターとメンティーの組み合わせで千差万別です。同じ会社内でも人それぞれです。その具体的なやり方にまで踏み込んだ記事があまりないと感じているため、自分で書いていきます。なお、自分の 1on1 は 「 ヤフーの 1on1―――部下を成長させるコミュニケーションの技法 」がベースになっています。 言葉の定義 ここでは、以下のように言葉を定義します 1on1 メンティーの成長のために設けられるメンターとの会話の時間 最低月 1 回あり、人によっては毎週や隔週で実施する メンター 1on1 の聞き手 上司や先輩、メンター制度のメンター メンティー 1on1 の話し手 部下や後輩、メンター制度のメンティー モバファクの 1on1 の目的 モバファクでは 1on1 の目的は多義的であるとして 5 つ掲げています。 ① 関係性の強化 ② 行動と学習の促進 ③ 意欲の喚起 ④ 情報共有と促進 ⑤ 問題解決の促進 ひとことで言えばメンティーの成長です。 1on1 で自分が大事にしていること 3 つあります。 1on1 はメンティーの時間である 1on1 はメンターの時間でもある 1on1 初回 1on1 はメンティーの時間である 1on1 はメンティーのための時間であるという意識を大事にしています。 理想を言えば、メンティー自身が 1on1 をどのように進めたいかフォーマットを考え、話したいことを話し、1on1 の頻度を決めてもらいたいです。 メンターはメンティーが自分にあったフォーマットで 1on1 を進められるよう選択肢を与え、メンティー自身が気付いていない考えを深堀ってあげ、定期的に 1on1 の頻度は今のままでいいか聞いてあげます。 1on1 では、メンティー自らが考え内省しながら物事を進めていく体験が成長につながると考えています。例えるなら、このリップクリームいいよと教えてもメンティーがリップクリームを買うことはないでしょう。そうではなく、メンティー自身が唇のカサカサを気にしていることを認識して、わからないなりにリップクリームを買って試して失敗しながらコレがいいと思うことが大事です。 1on1 はメンターの時間でもある 1on1 はメンターの時間であることも忘れてはいけません。1on1 の中でどのようにティーチング・コーチング・メンタリングをしていくのか、その使い分けはどうするか、考える必要があり、メンター自身の成長にも繋がります。もちろん、メンティーの学びはそのままメンターの学びにもなります。 1on1 を通して、1on1 の目的はメンティーではなくメンターにも向いていることに気付かされます。 1on1 初回 自分は初回の 1on1 はメンターとメンティーの関係を決め、その後の 1on1 の内容も大きく変わる大切な時間だと考えています。そのため、自分は以下の 3 点について初回に説明します。 ① 1on1 の目的と大事にしたいこと ② あなたの協力が必要であること ③ メンターは聞き手だということ ① はここまでに書いた記事の内容のことです。隠す必要はないので、素直に伝えます。① を踏まえると ③ も自然かと思います。 ② はメンター側の気持ちが伝わるので、メンティーに 1on1 を自分ごととして考えてもらいやすくなると思っています。1on1 はメンターがメンティーを引っ張り上げるものではなく、1on1 というプロジェクトを成功させるためにお互いメンバーとして協力するものだと説明します。 今使っている 1on1 のフォーマット 現在 1on1 をしているメンティーから許可を頂いたので紹介します。 - 体調 (10 段階) - 半期目標の進捗振り返り - ネクストアクションの振り返り - うまくいかなかったこと・もっとよくなりそうなところ・うまくいったこと・その他に話したいこと - ネクストアクション ちなみにこれは、初回 1on1 に私がベースとして出したフォーマットから、お互いに話し合いながら少しずつ成長させたものです。最初は以下のフォーマットでした。 - 体調 (10 段階) - うまくいったこと - うまくいかなかったこと - その他 - ネクストアクション いくつか変わっていますね。 体調 最も大事なセクションだと思っており 1on1 に外せません。仕事 < 健康です。 体調は 10 段階で自己評価してもらいます。なぜ 10 段階かというと、5 段階ではだいたい 4 に収まってしまい、気づきたい変化に気づけなくなってしまうからです。先週の 7 と今日の 8 の違いは偶然なのか心当たりがあるのか、深堀っていきます。 体調以外に、最近の仕事量や心理的な負担があるかどうかなども併せて聞いてあげます。回答によっては、上司に相談したり業務調整を行います。 半期目標の進捗振り返り モバファクでは半期ごとに各自目標を立てて、その目標を達成するために行動することが期待されます。その進捗を話すセクションです。 ネクストアクションの振り返り 毎回の 1on1 の最後に、1on1 を通して気付いた学びをどのような具体的行動につなげるかを考えてもらっています。考えっぱなしにならないようにと、前回考えたネクストアクションはどうなったか振り返りをするセクションを設けました。 うまくいかなかったこと・もっとよくなりそうなところ・うまくいったこと・その他に話したいこと 初回に提示したフォーマットのうちの 3 項目をぎゅっと 1 つにまとめて「もっとよくなりそうなところ」を追加したセクションです。 メンティーがうまくいったことを話しづらいという気付きがあったため、「もっとよくなりそうなところ」と言い方を変えて追加しています。 うまくいったことを先に話すと、うまくいかなかったことを話す時間が圧迫されてしまうという気付きから、うまくいかなかったことを先に話せるように先頭にもってきました。 ネクストアクション ここまでの 1on1 の内容を踏まえて、メンティーにネクストアクションを考えてもらいます。どのようなネクストアクションでもメンティーが考えたものであれば尊重したいと思っていますが、以下の点に注意しています。 実現が心理的に困難なものでないか 例:本を毎日 1 ページ読む ⇒「毎日 1 行読むという目標ですら、人によっては案外難しかったりすると思いますが、実現する自分を想像できますか」 抽象的過ぎる目標 例:ミスをしないように気をつける ⇒ 「抽象的すぎると行動しづらいので、状況を絞ったり、気をつけるための手順を書いたりしてほしいです」 もちろん抽象的である理由をメンティーが持っていれば OK 具体的すぎる目標 例:次の勉強会では率先して議事録をとる ⇒ 「抽象的にしてより広いネクストアクションにするのはどうですか」 あるいは、学びではあるけれど、メンティーがネクストアクションをうまく言語化できず書かなかったというケースもあります。その場合、書かないよりは行動が変わったらラッキーくらいの気持ちで「〇〇を気をつける」と書くこと、をよく提案します。 言語化しづらいネクストアクション 例:バグがありそうという直感が正しかったエピソードのネクストアクションを書かなかった ⇒ 「直感を大事にするくらいのふわっとしたネクストアクションでもよいので書いておくと、なにか行動が変わるかもしれませんよ」 全体的に、メンティーが出したネクストアクションの主旨が変わらないことは意識しています。 例:本を毎日 1 ページ読む 本ではなく勉強会に参加するのはどうですか?は主旨が変わってしまうので避けています。 1on1 の中でのやりとり 1on1 でよくありそうなシチュエーション別に具体的なやりとり例を示します。実際のやりとりではなく、説明しやすいようにそれらしい話題で自分が創作したものです。 また、最初に記載した通り 1on1 のやり方は千差万別なので、このやり方がよいかは人によると思います。 A:メンティー B:メンター お休みの取り方がわからない A「お休みの取り方がわかりません」 B「お休みはマネージャーにメンションして、カレンダーに登録してますよ。このドキュメントに書いてあります。このドキュメントはこのインデックスドキュメントから辿れるので覚えておいてください。」 典型的なティーチングのやりとりです。社内・チームルール、所属したてで調べ方すらわからない内容についてはティーチングをします。意識するのは、似たような状況になったとき、どうすればいいのか How も教えることです。 魚を与えるだけではなくて釣り方も教える、と自分は意識しています。 最近見積もりの精度が高くなっている A「最近見積もりの精度が高くなっていると感じています」 B「いいですね。具体的にはどんなタスクでそう感じましたか?」 A「そうですね... Aのプロジェクトを進めるときにガントチャートを組みました。中盤に差し掛かっても、スケジュール通りに進んでいるので、これは精度が高かったからだと思っています」 B「なるほど、これから懸念することはありますか」 A「進捗通りなのでないです。強いて言えば、今後の動作確認次第で修正箇所が多くなったときに、ガントチャートの引き直しが発生するかもしれません」 B「そう思ったのはコレまでにもそのようなことがあったからですか」 A「はい、前回のプロジェクトで動作確認後の修正が予想より多くて大変でした」 B(ちょっと黙ってみる) A「...なので、振り返りのときに動作確認を2回に分けるようネクストアクションを出したんでした」 B「よさそうですね。効果はありそうですか」 A「...それが、2回に分けるのを入れ忘れていました」 B「おぉ、そうですか」 A「忘れないように、ガントチャートのテンプレート作ってメモしておきます」 B「はい、お願いします。」 B「では、ちょっと視点を変えてもらいたくて、精度が高い見積もりをすることによるデメリットはありますか」 A「最近見積もりの精度が高くなっていると感じています」 B「いいですね。具体的にはどんなタスクでそう感じましたか?」 1on1 で褒めることは大事ですね。隙あらば褒めていきます。「いいですね」「よさそう」「考えたことなかったです」「チームの人たちきっと喜んでますよ」 具体的なエピソードを掘り下げていきます。 A「そうですね... A のプロジェクトを進めるときにガントチャートを組みました。中盤に差し掛かっても、スケジュール通りに進んでいるので、これは精度が高かったからだと思っています」 B「なるほど、これから懸念することはありますか」 少し別の視点を持ってもらうために、良かった話から懸念点へとベクトルを曲げます。さらに掘り下げるような質問も考えられます。「ここでの精度とは期間のことですか、あるいは見積もりのことですか」「ガントチャートはどうやって組みましたか」 A「進捗通りなのでないです。強いて言えば、今後の動作確認次第で修正箇所が多くなったときに、ガントチャートの引き直しが発生するかもしれません」 今回の例では A さんが「強いて言えば」で文章を続けてくれましたが、続けてくれなかったときはメンターから「強いて言えば?」と促すこともできます。 B「そう思ったのはコレまでにもそのようなことがあったからですか」 A「はい、前回のプロジェクトで動作確認後の修正が予想より多くて大変でした」 B(ちょっと黙ってみる) 少し黙ってみると続けて話してくれることはよくあります。もちろん、メンティーの目が泳いだりして考えているように見えるときも黙るのは有効です。ただし、沈黙に耐えられる長さは人それぞれなため、注意が必要です。自分は経験ないですが、相手が居心地の悪さを示したら「ごめんなさい、ちょっと考えてもらうために黙っていました」と素直に伝えるとよいかと思います。 A「...なので、振り返りのときに動作確認を 2 回に分けるようネクストアクションを出したんでした」 B「よさそうですね。効果はありそうですか」 A「...それが、2 回に分けるのを入れ忘れていました」 B「おぉ、そうですか」 1on1 の中でネクストアクションが実行されないことはよくあることです。責めてしまうと次から失敗の話はメンティー自身から出なくなってしまうかもしれません。事実を把握したというリアクションが大事だと思います。 A「忘れないように、ガントチャートのテンプレート作ってメモしておきます」 B「はい、お願いします。」 B「では、ちょっと視点を変えてもらいたくて、精度が高い見積もりをすることによるデメリットはありますか」 一見デメリットがないようなことに対してデメリットを考えてもらう質問を自分はよくします。ここからメンティー自身でも気づかない発見がよくあります。視野の広さを持ってもらいたいですね。 注意したいことは、この質問はメンターのスタンスを示しているわけではないことを理解してもらうことです。今回の例では、別にメンターは「精度の高い見積もりはよくない」と思っているわけではありません。あくまで視野を広げてもらうための 1on1 中の質問に過ぎません。 朝会の議事録をとるようにしたい A「毎日の朝会で内容を忘れてしまうことがあるので、議事録を取るようにしたいです」 B「いいかもしれませんね。でも今まで議事録を取っていなかったということは、他のメンバーはどうしていたと思いますか」 A「わからないです。朝会で話し合う内容は簡単なものなので、覚えてしまうのかもしれません」 B「このあたりはチーム全体で話し合えるとよさそうです。今度朝会で提案してみるのはどうですか」 これは 1on1 の中で業務の相談があったシチュエーションです。もちろん 1on1 では話しづらいことを気軽に話してもらえる場として活用するのも大切です。しかし、1on1 が業務の相談ばかりになってしまったら目的とずれてしまいます。適度に切り上げて、業務の相談はより適切な場が別であることを示してあげます。 最近チームの動きがぎこちないと感じている A「最近チームの動きがぎこちないと感じています」 B「ぎこちない?」 A「はい、朝会やミーティングが淡々と進みすぎているように思います」 B「それを他の人もそう感じているいないに関わらず、その感覚や気持ちは大事にしてもらいたいです」 B「ぎこちないということはAさんはポジティブ、ネガティブで言うとネガティブに感じているということですよね」 A「うーん、そうなんでしょうけど、一概に悪くないとも思っています。なぜなら、必要最低限の時間でミーティングが完了できて、それは本来目指すべき姿だと思うからです」 B「ほうほう。でもAさんは部分的には問題だと考えているわけですよね」 A「最近チームの動きがぎこちないと感じています」 B「ぎこちない?」 気になるキーワードを拾って話を促します。もちろん、他にもいろいろな返しが考えられます。 A「はい、朝会やミーティングが淡々と進みすぎているように思います」 B「それを他の人もそう感じているいないに関わらず、その感覚や気持ちは大事にしてもらいたいです」 これは 1on1 というよりも単純に自分が大事にしていることです。もしそうでないとしても、感情はあらゆる原動力なので注目することはいいことだと思います。 B「ぎこちないということは A さんはポジティブ・ネガティブで言うとネガティブに感じているということですよね」 A「うーん、そうなんでしょうけど、一概に悪くないとも思っています。なぜなら、必要最低限の時間でミーティングが完了できて、それは本来目指すべき姿だと思うからです」 B「ふむふむ。でも A さんは部分的には問題だと考えているわけですよね」 感情をポジティブとネガティブに分類してもらう質問も自分はよくやります。感情は複雑なので、自分が誤った想定で質問を続けてしまうことを防止できます。また、感情にゆっくり向き合う時間というのは日頃の生活でないことなので、ぜひメンティーにもいろいろ考えてもらいたいですね。 1on1 定期的な振り返り 3 ヶ月ごとくらいにメンターとメンティーの 2 人で簡単な 1on1 の振り返りをします。以下のようなことを話し合います。 1on1 の頻度は今のままでいいか 1on1 のフォーマットは今のままでいいか よりメンターにこうしてもらいたいという提案はあるか メンティーのキャリアのすり合わせ 1on1 の中ではキャリアについても考えるきっかけを作っています。メンティーの成長にあわせて、より技術志向な 1on1 にしたいなどないか、すり合わせをします。メンターがメンティーにタスクを割り振る立場であれば、目指すキャリアにあわせてタスクに挑戦させることができます。 まとめ ずいぶんと長くなってしまいました。これから新年度を向かえて、1on1 を任される方がいらっしゃるかもしれません。1on1 の内容は前述した通り千差万別だと思いますが、参考にしていただけたら幸いです。 1on1 はメンターとメンティーのための時間 1on1 の初回でそれを伝える 1on1 のフォーマットをお互いに考えて成長させる 1on1 のやりとりに答えはない 定期的に振り返りをする
こんにちは、ブロックチェーンチームの id:charines です。 今回は ERC-721 コントラクト(NFT コントラクト)にメタトランザクションを導入した開発事例について紹介します。 主にブロックチェーンに関する開発者の方を対象とした内容になります。 メタトランザクションの導入理由 1. マーケットプレイスのユーザが NFT を出品しやすい 2. NFT クリエイターがコントラクトを管理しやすい 実装方針 実装 フォワーダー ERC-721 まとめ メタトランザクションの導入理由 メタトランザクションとは、トランザクションの実行に必要なガス代を実行者ではない第三者が払うシステムです。 これによりトランザクション実行者は ETH を保持する必要がなくなり、 DApps を利用する障壁の 1 つを取り除くことができます。 弊チームでメタトランザクションを導入したい具体的な目的は主に 2 つです。 1. マーケットプレイスのユーザが NFT を出品しやすい 弊チームが提供していた NFT マーケットプレイス「ユニマ」では、ユーザが NFT を出品する機能は設けていませんでしたが、新機能として所有する NFT の二次販売を可能にする予定でした。 ブロックチェーンに詳しくないユーザにも使いやすいというのがユニマのコンセプトの 1 つでもあるため、出品のハードルを下げるためにガス代の肩代わりは必須でした。 2. NFT クリエイターがコントラクトを管理しやすい マーケットプレイスのユーザと同様に、NFT を作成・販売するクリエイターも必ずしもブロックチェーンに詳しいわけではありません。 ERC-721 コントラクトには mint や pause 、メタデータ URI の変更などコントラクト管理のための関数を実装していますが、それらの管理をクリエイターがガス代なしで行えるようにすることも、販売のハードルを下げるために重要でした。 実装方針 メタトランザクションの実装方針として大きく 2 つの方針を検討しました。 1 つ目は機能ごとにメタトランザクション用の関数を実装することです。 例えば ERC-2612 は FT である ERC-20 において approve 関数のメタトランザクションを可能にする permit 関数を定義しています。 approve は自身の持つ FT を移動させる許可を与える関数ですから、 transferFrom など特定のアカウントの FT を移動させる関数を組み合わせることで transfer のメタトランザクションと同等のことを可能にします。 ERC-721 においても approve が存在するので同じ仕組みの実装が可能です。 しかしこの方法では、複数の機能についてメタトランザクションを可能にしようとすると、それぞれに対応する関数の実装が必要になります。 そこで 2 つ目の方針として、 ERC-2771 に基づいたメタトランザクションの仕組みの実装を検討しました。 ERC-2771 はフォワーダーと呼ばれる信頼できるコントラクトが署名を検証し、受信者(今回は ERC-721 コントラクト)はフォワーダーからのメタトランザクションを受け入れるというプロトコルです。 この方法では機能ごとに新しい関数を実装する必要がない上、 Gas Station Network (GSN) で既に広く利用されている実績もあったため、今回はこの方法を採用することにしました。 実装 フォワーダー フォワーダーは前述の通り、署名を検証し受信者にリクエストを転送するコントラクトですが、この部分には独自実装が必要ないため新規実装は行わずに既存のフォワーダーを利用する方針としました。 具体的には GSN で Ethereum と Polygon チェーン上に展開されているコントラクトを使用します。これらの実装は Etherscan で見られるため、信頼に足る実装であることも確認できます。 ERC-721 GSN のフォワーダーを利用するのに合わせて、こちらも GSN の実装 を利用します。 BaseRelayRecipient でメタトランザクションに必要な _msgData() と _msgSender() が実装されているのでこれを継承します。 import "@opengsn/contracts/src/BaseRelayRecipient.sol" ; また弊チームで開発しているコントラクトは元々 OpenZeppelin をベースにしています。 こちらにも _msgData() と _msgSender() を含む Context の継承が含まれており多重継承となるため、override を明記する必要があります。 function _msgData() internal view override(ContextUpgradeable, BaseRelayRecipient) returns ( bytes calldata ret) { return BaseRelayRecipient._msgData(); } function _msgSender() internal view override(ContextUpgradeable, BaseRelayRecipient) returns ( address ret) { return BaseRelayRecipient._msgSender(); } 最後に、メタトランザクションを行うには信頼できるフォワーダーを事前に知っておく必要があるため、コンストラクタで _setTrustedForwarder() を呼び出すようにすれば実装は完了です。 まとめ ERC-721 コントラクトにメタトランザクションを導入する上での技術選定や実装の流れについて紹介しました。 特に ERC-2771 によって任意の関数をメタトランザクションとして実行可能にした GSN の実装や展開済みのコントラクトを利用することで工数を抑えて開発できた の 2 つが今回のポイントです。 弊社は 4 月 1 日を以ってブロックチェーン事業を撤退することとなりましたが、この記事やこれまでの発信が、今後ブロックチェーン技術を活用する開発者の方々の助けになれば幸いです。
みなさん、こんにちは。新卒エンジニアの id:matsuda0528 です。 今日は、Mapbox GL JS を使用して地図の描画領域を変更するアニメーションを実装する方法についてお話します。 TL;DR 以下のように、 setInterval() 関数を用いて resize() 関数を繰り返し実行する方法で実装しました。 const onClickMapResizeButton = () => { clearInterval(mapResizer) mapResizer = setInterval(() => { map.value.resize() } ) } const onTransitionend = () => { clearInterval(mapResizer) } 駅メモの地図について 駅メモでは、Mapbox GL JS を使用していくつかの地図表示機能を実装しています。 最近ではスタンプラリーイベントで新たに地図表示機能が追加されました。 今回は、スタンプラリーイベントで新しく実装した 地図の大きさを変更するアニメーション について解説します。 地図のサイズ変更アニメーション 下記のような実装を用意します。 その上で <div class="map"> の大きさを変更する処理を追加すれば、地図のサイズ変更アニメーションを実装することが可能です。 < div class = "map" > <!-- 地図を描画するコンポーネント --> < v- map ... /> </ div > .map { transition : height 0.5s ease ; } しかし、Mapbox GL JS の地図は描画領域に関する情報を保持していて、CSS アニメーションだけでは地図本体が追従しません。 実際に行ってみると、以下の図のように地図を囲む要素の大きさは変わるものの、地図自体の描画領域は変化しません。 サイズ変更前、サイズ変更後の地図 地図の描画領域も同時に動かすためには、 resize() 関数を用いて随時地図領域を更新する必要があります。 今回は、地図の外枠に対して transition でアニメーションを行いつつ、その間中に resize() を実行し続けることで対応しました。 以下に Vue3 での実装例を挙げています: <template> <main> <div class = "map" : class = "mapSize" @transitionend= "onTransitionend" > <v-map ref= "map" ... /> </div> <v-button @click= "onClickMapResizeButton" >サイズ変更</v-button> </main> </template> <script setup> import { ref } from 'vue' ; const map = ref( null ); const mapSize = ref( 'map-size-normal' ); const mapResizer; const onClickMapResizeButton = () => { mapSize.value = 'map-size-large' ; mapResizer = setInterval(() => { map.value.resize(); } ); } ; const onTransitionend = () => { clearInterval(mapResizer); } ; </script> <style> .map { transition: height 0.5s ease; } .map-size-normal { height: 50px; } .map-size-large { height: 100px; } </style> clearInterval に届かない可能性を考える この実装の懸念点は、 setInterval() から clearInterval() までに別のイベント( onTransitionend )を経由するため、 clearInterval() が必ず実行される保証がないことです。 たとえば、一度「サイズ変更ボタン」を押した後、 transition の途中で「サイズ変更ボタン」をもう一度押してしまうと、 onTransitionend が発火しないまま新しく setInterval() が実行されてしまいます。 これによって、最初の setInterval() の intervalID が失われてしまい、インターバルの処理が終わらなくなってしまう可能性があります。 この問題への対応策として、transition が中断する可能性のあるアクションの前に clearInterval() を実行します。 const onClickMapResizeButton = () => { mapSize.value = "map-size-large" clearInterval(mapResizer) mapResizer = setInterval(() => { map.value.resize() } ) } まとめ 今回紹介した方法では、 setInterval() を使用して resize() 関数を繰り返し実行することで、地図のサイズ変更アニメーションを実現しました。 ただし、この方法では clearInterval() が常に適切に実行される保証がないため、使う際には注意が必要です。 参考サイト Mapbox GL JS | Mapbox. ( https://docs.mapbox.com/mapbox-gl-js ) javascript - MapBox Smooth Transition of Resizing Map - Stack Overflow. ( https://stackoverflow.com/questions/61490901/mapbox-smooth-transition-of-resizing-map )
駅メモ!チームエンジニアの id:yumlonne です。 この記事では Redis の sorted sets で実装していたランキング処理を MySQL に移行した仕組みを紹介します。 背景 駅メモ!には複数のランキングがあり、Redis の sorted sets を使うことでパフォーマンスの高いランキング処理を実現していました。 中にはリリースからの全期間に渡るデータを利用するランキングもあり、Redis のメモリ使用率は日に日に増えていく一方でした。 何度か Redis をスケールアップしてメモリを増やすことで対応していましたが、根本的に対応しなければ今後も Redis をスケールアップもしくはスケールアウトさせ続けるしか選択肢がなく、コストが増え続けてしまう状況でした。 調査したところ、一部のランキングがメモリ使用率の 2/3 程度を占めていることが判明しました。 そこで、その巨大なランキングを Redis から MySQL に移行させることを考えました。 Redis とデータベースの詳細 Redis: Amazon ElastiCache Redis 7.x データベース: Amazon RDS Aurora v2 (MySQL 5.7 InnoDB) ランキングについて 以下は対象となるランキングの要件と仕様です。 要件 1. 上位数件のユーザとそのスコアを取得できる 要件 2. ユーザは自分自身の順位を取得できる 要件 3. 順位は同率を考慮する(スコア 100,90,90,80 とある場合、順位は 1,2,2,4 となる) 要件 4. 上記の処理をそれぞれ 10ms 程度で実行できる 仕様 1. 古いデータを削除することはない 仕様 2. スコアの更新は増加のみで、減少させるような更新はない また、ランキングのスコアにはかなりの偏りがあります。 以下は偏りのイメージです。 縦軸はユーザ数ですが偏りが酷いので対数目盛にしています。 横軸はスコアです。イメージなので数値は表示していません。 MySQL でのランキング処理の課題 「要件 1. 上位数件のユーザとそのスコアを取得できる」 はスコアにインデックスを張っておけば 以下のように指定するだけで高速に取得できるため問題ありません。 SELECT * FROM ranking ORDER BY score DESC LIMIT 10 ; 問題は 「要件 2. ユーザは自分自身の順位を取得できる」 で、こちらはインデックスだけでは解決できません。ユーザの順位を求めるためには、そのユーザのスコアより高いスコアのユーザ数を知る必要があります。 下記 SQL にて自分よりスコアが高いユーザ数を取得できますが、スコアが低いユーザの場合、カウント対象行が多くなるためインデックスを有効活用しづらくなります。 SELECT COUNT (*) FROM ranking WHERE score > 50 ; この問題を回避するためにカウント対象行を絞る対応を考えました。 ランキングを区切る 順位を出すときにカウント対象行が多くなりうる問題を解消するため、ランキングを区切ることを考えてみます。 例えば、事前に「1000 位のユーザーのスコアが 500」だと集計できているとします。 スコアが 450 のユーザーの順位は、「スコアが 450 より大きく 500 以下」を満たすレコード数に 1000 (位)を加えることで求められます。 順位 スコア 1000 500 2000 340 3000 250 4000 210 ... ... 上の表のように順位を等間隔で区切ることにより、いかなる順位でもカウント対象をほぼ一定に保つことができ、パフォーマンスが安定します。 逆に、スコアを等間隔で区切るとランキングデータの偏りの影響でカウント対象がばらつくため不採用としました。 説明のため、以降はランキングを区切るデータのことをランキングインデックスと表記します。 サンプルスキーマ CREATE TABLE `ranking` ( `id` bigint( 20 ) unsigned NOT NULL AUTO_INCREMENT, `user_id` int ( 10 ) unsigned NOT NULL , `score` bigint( 20 ) unsigned NOT NULL , PRIMARY KEY (`id`), UNIQUE KEY `user_uniq` (`user_id`), KEY `score_idx` (`score`) ) CREATE TABLE `ranking_index` ( `id` bigint( 20 ) unsigned NOT NULL AUTO_INCREMENT, ` rank ` bigint( 20 ) unsigned NOT NULL , `score` bigint( 20 ) unsigned NOT NULL , PRIMARY KEY (`id`), KEY `rank_idx` (` rank `), KEY `score_idx` (`score`) ) 順位取得処理のサンプルコード 順位取得処理のサンプルコードです。シンプルなコードなので Perl がわからない方でも流れを理解できると思います。 sub get_rank_by_score { # rankを算出するスコアを受け取る my ( $score ) = @_ ; # $scoreに近いranking_indexを探す my $ranking_index = get_ranking_index_by_score( $score ); if ( defined $ranking_index ) { if ( $ranking_index->{ score } == $score ) { # ranking_indexのスコアがrankを求めたいスコアと同じだった場合はcountするまでもなく順位がわかる return $ranking_index->{ rank } ; } # ranking_indexのscoreと$scoreの間に存在するレコードをカウント my $cnt = exec_sql( "SELECT COUNT(*) AS cnt FROM ranking WHERE $score < score AND score <= $ranking_index->{ score } " )->{cnt}; return $ranking_index->{ rank } + $cnt ; } else { # ランキング上位の場合はranking_indexが存在しない # この場合$scoreより高いスコアを持つレコードが少ないので素直にカウントする # 順位は1からスタートするので+1して返す my $cnt = exec_sql( "SELECT COUNT(*) AS cnt FROM ranking WHERE $score < score" )->{cnt}; return $cnt + 1 ; } } sub get_ranking_index_by_score { my ( $score ) = @_ ; # 与えられた$score以上のscoreのうち最も$scoreに近いレコードを取得する return exec_sql( "SELECT * FROM ranking_index WHERE score >= $score ORDER BY score ASC LIMIT 1" ); } sub exec_sql { # SQLを受けとって実行結果を返す # サンプルコードをシンプルにする便宜上の関数 # 本来はSQLインジェクション対策をすべきだが単純化のため省略 } ランキングインデックスの管理 ランキングを区切ることによって順位計算時のカウント対象を減らし、パフォーマンスを向上させることができます。 しかし、ランキングを区切ったことで新たにランキングインデックスを管理する必要が出てきます。これを適切に更新しなければ、ユーザに誤った順位を返してしまうことになります。 更新 ランキングインデックスは以下の条件に当てはまるものを更新する必要があります。 更新前スコア(新規作成の場合は0) <= ランキングインデックスのスコア < 更新後スコア 例えば以下のようなランキングインデックスがあったとします。 順位 スコア 1000 500 2000 340 3000 250 4000 210 ... ... とあるユーザのスコアを 250 から 500 に更新する場合、ランキングインデックスは以下のように更新します。 順位 スコア 備考 1000 500 同率順位のデータが増えただけなので影響しない 2000 + 1 340 スコアが 340 を超えるレコードが増えたので順位が下がる 3000 + 1 250 スコアが 250 を超えるレコードが増えたので順位が下がる 4000 210 スコア 210 より上のデータが動いただけなので影響しない ... ... 順位を +1 するのがポイントです。 スコアは更新しないので他のトランザクションの更新対象に影響を与えず、順位は 順位 = 順位 + 1 のようにすれば更新前の状態でロックを取っておく必要はありません。 これによりランキングインデックスのロック時間を最小限に抑えることができるようになります。 順位取得処理のサンプルコード sub update_score { # 更新するユーザとそのスコアを受け取る my ( $user_id , $score ) = @_ ; with_transaction( sub { my $user_ranking = exec_sql( "SELECT * FROM ranking WHERE user_id = $user_id FOR UPDATE" ); my $before_score ; if ( defined $user_ranking ) { # すでにuser_idに対応するrankingレコードがある場合は更新 # 更新前スコアを保持しておき、ranking_indexの更新範囲決定に使う exec_sql( "UPDATE ranking SET score = $score WHERE id = $user_ranking->{ id } " ); $before_score = $user_ranking->{ score } ; } else { # rankingレコードがない場合はレコードを作る # 更新前スコアは0とすることで$score未満の全てのranking_indexを更新対象にする exec_sql( "INSERT INTO ranking(user_id, score) VALUES ( $user_id , $score )" ); $before_score = 0 ; } # スコアの更新幅に含まれるranking_indexを更新する increase_ranking_index( $before_score , $score ); }); return ; } sub increase_ranking_index { my ( $before_score , $after_score ) = @_ ; exec_sql( "UPDATE ranking_index SET rank = rank + 1 WHERE $before_score <= score AND score < $after_score " ); return ; } sub exec_sql { # SQLを受けとって実行結果を返す # サンプルコードをシンプルにする便宜上の関数 # 本来はSQLインジェクション対策をすべきだが単純化のため省略 } sub with_transaction { # 与えられたコードブロックをDBのトランザクション内で実行する関数 } 定期実行処理 ランキング更新によってランキングインデックスの順位がずれていってしまうため、間隔が一定に保てなくなり、徐々にパフォーマンスが劣化していってしまいます。 これを防ぐため、ランキングインデックスが順位で等間隔になるよう定期的に調整する必要があります。 上記のランキング更新の都合で、ランキングインデックスのスコアは更新できないため、新しいランキングインデックスを挿入し古いランキングインデックスを削除する実装にしました。 これによりランキングの更新や順位取得に影響を与えずにランキングインデックスの間隔調整ができます。 駅メモ!ではランキングインデックスの間隔調整処理を毎日実行しています。ランキングの更新頻度やスコア分布などの特性によってパフォーマンス劣化のスピードは変わるため、適切な頻度を見極めて実行する必要があります。 テーブルのメンテナンス 定期実行スクリプトではレコードの作成と削除をしているため、ランキングインデックステーブルが断片化してしまいます。 サービスのメンテナンス時にランキングインデックステーブルに対して OPTIMIZE TABLE を実行することで対応しています。 まとめ Redis の sorted sets で実装していたランキングを MySQL に移行するため、データベースでランキング処理をするようにしてみました。 その結果、Redis のメモリ使用量を元の 1/3 程度まで減らすことができ、Amazon ElastiCache Redis のスペックを下げることができました! Redis 内で大きくなり続けるランキングにお困りの際はデータベースに処理を移すことを検討してみてはいかがでしょうか。