コネヒト開発者ブログ

コネヒト開発者ブログ

CakePHP3から4へのバージョンアップ時に困ったキャッシュ周りの話

こんにちは。バックエンドエンジニアのTOCです。

このエントリは、 コネヒト Advent Calendar 2021 の10日目のエントリです。 9日目は @otukutunさん による 洋書読み始めるならThe Minimalist Entrepreneurがオススメ でした。

弊社では多くのプロダクトで CakePHP を利用しております。エンジニア組織の取り組みで CakePHP のバージョンアップ対応を行ったのですが、リリース時に起こった困りごとについて、今回は紹介したいと思います。


目次


はじめに

弊社では半期ごとに目標を設定するのですが、各チームが持つ目標とは別にエンジニア組織としての目標も持ちます。この目標は開発組織をよりよくするための取り組みであり、事業に直結しないかもしれないけど、開発組織としてやっていきたいライブラリのアップデートなどをやっていくために置かれているものです。 2021年度の上期では PHP・CakePHP のアップデートや TypeScript 化などが題材として挙げられ、それぞれについて希望した取り組みを目標としてもつことになりました。 やりたいけど、なかなか優先度が上げづらい取り組みを会社として進めようとしてくれるのはありがたいですね🙌

僕はあまりフレームワークのアップデート経験がなかったので PHP・CakePHP のアップデートに取り組むことになりました。

どんなことをやったのか

弊社では PHP を用いたプロジェクトがいくつかありますが、半年で全てのプロジェクトを扱うのは難しかったので、対象を絞ってアップデートを行うことにしました。 その中で、弊社のプロジェクトの中でも規模が大きいリポジトリをアップデート対象にすることに決めました。このリポジトリについて、下記のようにアップデートを行いました。

before after
PHP 7.1.33 7.4.15
CakePHP 3.8.1 4.2.8

アップデート作業はCakePHPドキュメントのアップグレードガイドを参考にアップデートし、非推奨機能を愚直に潰していきました。 困った時はドキュメントを見るだけでなく、実際の CakePHP のコードを見て、ディレクトリ構成や処理の仕方を参考にすることで納得感を持って修正ができました。

余談ではありますが、途中PHPバージョンアップ kickoffというドンピシャなイベントがあり、チームメンバーと参加しました。規模が大きいプロジェクトでは中々骨が折れる作業ではありますが、uzullaさんのPHPバージョンアップけもの道という発表を聞いて、強い気持ちを持ってバージョンアップに取り組もうと決意を改めました。 これからバージョンアップ作業を行う方やバージョンアップに疲弊したときは読んでみてください。

リリースしてどうなった?

このエントリの本題に入っていきたいと思います。 今回規模が大きめのプロジェクトで、修正範囲も大きめだったこともありカナリアリリースを行うことにしました。 インフラチームに協力してもらい、Canary 環境を作成し、いざリリース!

...ん、なんかエラーが出始めた...

Trying to get property of non-object なるエラー通知が起こるようになりました。 一旦カナリアリリースによる影響なのかを調査し、どうやらバージョンアップが原因そうだったのでカナリア環境を一旦停止し、第一弾リリースは撤退することとなりました...orz

原因を調べる

開発環境では起きなかったのに、本番環境で起きたエラーだったので、なんで起きているのかパッとはわかりませんでした。ただエラーが起きている箇所を見てみると、Redis を使用したキャッシュを利用してることがわかりました。そこで一度キャッシュを利用しないで動かしてみるとエラーが発生しなかったので、どうやらキャッシュを利用してる箇所が怪しいぞ、ということになりました。

調査を続けていると、ある仮説が挙がりました。今回カナリアリリースにより本番環境と分けているけど、キャッシュは同じものを利用している。つまり

「CakePHP3と4で扱うデータ構造に互換性がないのではないか...?」

そうです、CakePHP4になったとき、Entity の _properties_fields に変わっていたことにより、下記のような事象が起きていました。

  1. カナリアリリースにより、本番(CakePHP3環境)とカナリア環境(CakePHP4環境)が共存する
  2. CakePHP3環境で値をキャッシュする
  3. キャッシュされた値をCakePHP4環境で取得
  4. _fields にアクセスしようとするが、_propertiesに格納されているため、Trying to get property of non-object が発生

cf. _properties_fields に変わっているcommit

なるほど、開発環境ではCakePHP4環境のみになっていたので、エラーが起きなかったことも頷けます。 バージョンアップにおけるエラー箇所の修正時に知っていたことではありましたが、カナリアリリース時に問題になることは盲点でした。

なんとか原因究明ができました。カナリアリリースのおかげでキャッシュによるエラーを検知しつつ、影響範囲を最小限に抑えることができました...

どう対応したのか

今回は CakePHP3 と CakePHP4 でデータ構造の互換性が問題だったので、CakePHP3と4環境でキャッシュキーを別にして、それぞれの環境でキャッシュした値を利用する形に修正しました。 幸運なことに、組織目標における別の取り組みをしていたチームが Redis の容量を大幅に削減してくれていたこともあり、キャッシュの容量には余裕がありました。 なので、CakePHP4 環境では〇〇_cakephp4-tempといったサフィックスをつけてキャッシュを分けることにより対応を行いました。

リリース再挑戦とその後の対応

再度開発環境での確認や、リリーススケジュールを調整し、2回目のカナリアリリースに挑戦しました! 今度は前回起きたエラーも起きず、その他にもバージョンアップによるエラーが起きていない状態でした。無事カナリア1%リリースが成功し、徐々に比率を上げていき、最終的に本番環境へのリリースが完了しました。

リリース後は一時的につけていたサフィックスを取り外す対応を行い、CakePHP4アップデート作業に終止符を打つことができました。

まとめ

今回はPHP・CakePHPバージョンアップにおいて、特にリリース時に起こった困りごとについて紹介しました。 リリース時に起きうる事象として、今後同じような現象を踏まないように参考になると幸いです。

バージョンアップ作業はかなり愚直な作業が多いかもしれませんが、そのフレームワーク自体の理解も深まったりして非常に学びが多い挑戦でした。 また、バージョンアップに関わったメンバーはもちろん、インフラチームや他のチームメンバーにもテストやリリースに協力いただき、組織的にコラボしながら進めることができて、恵まれているなぁと改めて実感しました。

明日は @mryhryki さんによる、TypeScript 化のお話しです🙌

コネヒトの今回のような活動に少しでも興味をもたれた方は、ぜひ一度お話させてもらえるとうれしいです。

hrmos.co

おまけ

CakePHP3から4へアップデートするにあたり、キャッシュ周りでもう一つ変更があったので、そちらもご紹介いたします。 CakePHPでキャッシュの読み込みをする際に

<?php
Cache::read('my_data');

という形で取得できますが、キャッシュデータがない場合、CakePHP3と4で return する値が変わっていました。CakePHP4の場合 null CakePHP3の場合 false が返ってくるように変更されています。 これにより型の違いでエラーになる可能性があるので気をつけてください。

CakePHP4

<?php
/**
 * Read a key from the cache.
 * .
 * .
 * .
 * @return mixed The cached data, or null if the data doesn't exist, has expired,
 *  or if there was an error fetching it.
 */
public static function read(string $key, string $config = 'default')
{
    return static::pool($config)->get($key);
}

CakePHP3

<?php
/**
 * Read a key from the cache.
 * .
 * .
 * .
 * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it
 */
public static function read($key, $config = 'default')
{
    // TODO In 4.x this needs to change to use pool()
    $engine = static::engine($config);

    return $engine->read($key);
}