最近、AIを使って既存のツールとかを再実装してみるとか、そういった部分が結構あるなと思っているんですけど、実際に自分もいくつかシャリの再発明みたいなことをしていたりするんですけど、片鶴さんもあったりしますかね。
自分もそれ最近思ってて、先日、memcachedの再実装してみたんですよね。うつろって名前なんですけど。
なので今回、うつろの実装を紹介しつつ、最後にmemcached本家の話もできればなと思っています。
お、いいですね。まずはうつろの実装の紹介っていうところからですかね。うつろどういうものなんですかね。
memcachedの再実装なんで、ちょっとmemcachedの基本的なことも話していくんですけど、うつろはmemcachedテキストプロトコルっていうのをベースにしたeメモリのKVSって感じなんですけど、
memcachedのいいところって、そのプロトコルが非常に単純っていうところなんですよね。
memcachedテキストプロトコルというプロトコルが存在してるんですね。
存在していて、すっごい単純なんですよ。この後ちょっとまた話すと思うんですけど、
TCPで、テルネットとか使ってTCPでも手書きしてしゃべれちゃうぐらいのすごい手軽な仕様なんですね。
それで受け取ったデータをKVSとして保存して、それをメモリ上に保存しておく。
なので再起動したら消えちゃうっていうのがmemcachedなんですけど、うつろも同じで再起動したら消えちゃうみたいな感じですね。
メモリ上にキーバリューを構築してるって感じですよね。
そうですね。なので、キャッシュサーバーにすごく便利っていうのがmemcachedなので、割と簡単に実装できるんじゃないと思って作っちゃいました。
やってみたみたいな感じですね。
メモリ上にキャッシュを載せるっていうので、使い方的にはあれですよね。
高速にキーバリュースターとして使いたい場合に使うみたいなイメージですよね、基本的には。
そうですね。本当にキャッシュですね。消えてもいいデータしか入れちゃダメっていう。
消えると困るようなものは載せられないっていう。
そうですね。そういうのはデータベースとかに入れてくださいっていう感じで。
memcachedは高速なので、データベースとかに比べて高速なので、キャッシュとかを使うには便利。
だけど、消えたら困るデータは絶対入れちゃダメっていうんですね。
そこがちょっとRedisとかと違うようなところですよね。
そうですね。Redisは映像化もできるんですが、memcachedは基本的に映像化できないですね。
で、それを再実装してみてどうでした?
すごい雑な振りなんですけど。
意外と実装できたんですよ。
意外とあっさりできたんで、ちょっと軽くこれから実装を紹介していきたいと思います。
まず、キャッシュデータケースとプロトコルがどんだけ単純かっていうところで、
まずセット、セットとゲットがあるわけですけど、
まずセットしたい場合は、セットって打ってスペースでキー名、
その後に、この後また話すんですけど、フラグスってやつがあって、
その後にいつエクスパイヤーするのかっていう時間を入れて、
その後に何バイトなのかっていう情報を入れて、で開業しますと。
その後に実際に入れたいデータを入れてまた開業するっていうだけなんですよ。
なるほど。文字列で見るとプロトコルなのであれなんですけど、
使い方のリファレンスなのかなって感じですね。
単純に。値を保存するという命令がそこに書かれてるんだろうなっていうような。
そうですね。すごく単純で本当に最低限っていうところですね。
ゲットはゲットしてキー名で開業するだけ。
で、バリューの値が返ってくるっていう感じになります。
開業が絶対入るんですね。
開業が必要ですね。
ちょっとそこが特殊というかなんでだろうっていう。
いや特殊ではないんですよ。HTTPだって開業必要じゃないですか。
そうなんですね。そこが分かってなかったです。
HTTPやHPヘッダーとか全部開業必要なんで。割と普通ですよ。
なるほど。
で、フラグスの説明をちょっと追加するんですけど、
フラグスは基本的にイントのデータです。
なんで整数が基本的に入ります。
で、何に使うかっていうと、
これはNemcacheリーガーは自由に使ってくださいみたいな感じなんですよ。
自由。
キーに決めてくださいって感じで、結構ライブラリーによっては、
0だったら、これ数字は適当なんだけど、0だったらイントが入ってるとか、
1だったら文字列が入っているとか、2だったらGGP圧縮されてるデータが入っているとか、
そんな感じで、ライブラリーが自由に使っている領域なんですよ。
バリューの型を示すこともできたり、他の使い方もできたりってことなんですね。
そうです。Nemcacheリーガーのクライアントライブラリーが自由に使っていい領域なんですよね。
フラグスっていうのは。
ここはNemcacheリーガー扱う上で若干罠になったりする部分ではあるんですけど、
今回はNemcacheリーガー再実装しただけなんで、そんなに関係はないんですけど、
そういう実装、誘い仕様がNemcacheリーガーにはありますね。
確かに再実装でNemcacheリーガーを再実装したうつろであれば、
フラグスは自由にやっていいよっていうことだから、主導詞すればいいってことですよ。
うつろ側は特に保存しているだけで、クライアント側がどう扱ってるかっていうのはうつろは感知していないので、
今回はあまり関係ないですね。
確かに。
という感じですね。本当にそれだけなんですよ。
なので、うつろって今回、棒で作ったんですけど、本当にスペースで区切られているだけだから、
位置を取ってきて、スペースで区切って、コマンドがセットなの、ゲットなのっていうのを見て、
実際の値見てみたいな、本当にそれしかやってないですよね。
パースも簡単ってことですね。
パースも簡単。再実装しても全然大したことない。
ちょっと気になるのが、フラグスの後のエクスパイヤーの時間ですね。
ここら辺の実装ってどうなってるのかなって、すごい単純に思うんですよね。
ここが若干めんどくさくて、ゼロなら期限なしなんですけど、
一定の時間以下だったら相対秒、例えば60とかって入れたら、今から60秒間使えるってなるんですよ。
なるほど。
けど、長かったらユニクスタイムスタンプで入れるんですよね。
例えば1ヶ月後とかだったら、1ヶ月後のユニクスタイムスタンプを入れないといけない。
なるほど。その秒数ではないってことですね。
秒数ではない。
難しくないですか?
そこが若干めんどくさくて、今回CodeXが全部やってくれたんで、自分は実装しないんだけど、
何か言い区分とかで、この場合はこう、この場合はこうってちょっとめんどくさいことやってました。
これはプロトコル的に明示的に何秒以上だったらユニクスタイムスタンプ扱いみたいな。
のがあるはずだけど、何秒か忘れました。うつろんソースコード見れば書いてあると思います。
すごいナイブな実装になってますね、そこ。
若干なんでここだけよくわかんないことになってるかわかんないんだけど、なぜかそうなってるんで。
再実装する場合は実装しないといけない。
どっちかに合わせてほしいですよね。
どっちかに合わせてほしいなって、この実装を見たときに思ったんですけど、何だこの言い区分と思ったんで。
面白ポイントですね、ここ。
若干面白ポイントなんですけど、CodeXがやってくれたんで、そうなんやっていう感じでしたね。
あとはその後のバイト数ですかね。
バリュー受け取ってるわけじゃないですか、その後に。
わざわざバイト数持ってくる人ってあんのかなというのが若干疑問なんだけど。
これ多分ね、MemcachedがC言語で実装されてるんですよ。
そうなると何バイトって確保しないといけないのかってC言語で実装って書いてるとめちゃくちゃ大事じゃないですか。
メモリを事前に確保したいってことですね。
そうです。で、実際HTTPとかもサイズ返すじゃないですか、コンテンツレンジです。
なのでそれと同じといえば同じですね。
なるほど。じゃあ事前に確保しておいてそこにぶち込むよっていうのを約束してるって感じですね。
そうですね。Goとかだったら別にどうでもいいっちゃどうでもいいんだけど、Cで実装してたらこれないと本当にしんどいので。
確かに。
すごく大事なところではありますね、ここは。
ちなみに再実装ではちゃんとバリューに指定されたものとこのバイト数の再検証みたいなことしてるんですか。
どうだったかな。それはCodeXくんがやってくれたので覚えてない。
先頭になっていくんですね。
先頭に持っていって、その前後のやつをまたリンクのリストなんでくっつけ合う。
やるとどんどん先頭に行く。
一番後ろのやつが最も使われてなくて、一番最後に最初に入れたものっていうことですね。
そうですね、最初に入れてから誰もゲットしてないやつになるので、後ろから見ていって、そいつをどんどん消していけばいいんですよ、Effectionsの処理は。
確かに、そういうデータの整理になっていれば、消すほうが楽ですね。
そうなんです。それにリンクドリストって、AIに言えばばーってすぐできちゃうから、実装自体は割とシンプルですよ。
手で書こうとするとめんどくさそうだなって。
手で書くと若干めんどくさいけど、別に一個一個の関数やってることは、このポインター切ってこのポインター入れるみたいな、そんな感じでやってるだけなんで。
シンプルですよね、やってることが。
やってることがシンプルで、なのでそのLRUのリストと、あとKBSのマップ作っておいて、アクセスするときはKBSのマッププラスLRUの更新。
リンクドリストの更新で、Effectionsの処理はそのLRUのリンクドリストの一番後ろのやつをどんどん消していく。
なんかここまで聞いてて、単純にマップの中からキーを拾ってきてバリュー返すだけでしょうと思ってたんですけど、意外とやってること多いですね。
そう、Memcachedはやっぱりやらないといけないんですよ。そうじゃないとどんどんメモリーが膨らんで勝手に死んじゃうので。
いい感じにメモリーの容量を確保するためにそういう工夫をしているってことですよね。
サーバーのメモリーをうまく活用するためにって感じですね。
もう一個、例えばゲットしたときにもうすでにエクスパイヤードしてましたってなったら、その要素消せるんですけど、ずっとゲットが来ない可能性もあるじゃないですか。
ずっと残り続けちゃいますよね。
ずっと残り続けちゃうから、こういうリンクドリストの実装っていうのは必要なんですよ、絶対に。
そのときどうするんですか。単純にゲットが来なかったときに。
リンクドリストで一番後ろに来るから、エフェクションズの処理が走ったときにどんどん消えていく。
そこに入れとくってことなんですね。
一番後ろにいるから。
他のゲットが走ったときに削除が走るようにするってことなんですね。
エフェクションズの処理をどういうときに実行するかっていう問題ではあるんだけど。
確かに、エフェクションズの処理。
そのときに実行する必要はないんだよね。
ゲットのときは、その要素がエクスパイヤーとしていたら、その要素を削除してノットファウンドを返すっていう処理になる。
そのエフェクションズの処理をいつやってるんですかね。
ゲットとかセットとかがないときに行ってもいいってことですよね。
そうですね。この処理はエフェクションズの処理がいつ実行されてるかはちょっと覚えてないんですが、いつでもいいんですよね。
例えば、Goだったら簡単に書けるんで、何秒ごととかでもいいし、
あと、使ってるメモリーの容量が一定を超えたら実行するでもいいし。
なるほど。ガーベージコレクション的な感じで。
そう、これ完全にやってることがガーベージコレクションと一緒なんですよ。
それ実装してて思ったんですよね。あれこれ、結局問題になるのGCじゃんっていう。
そんな気がしますね。
このところ自体は簡単だし、あとGoのマップ使えば簡単にできるじゃんと思ったけど、
結局GCとの戦いになるんだっていうことをうつろを作ってすごい痛感したんですよね。
ちなみに、Memcachedとかにインクルメントとかレクルメントとかあるかなと思うんですけど、うつろもサポートしてるんですかね。
そうなんですよ。うつろも実装してるんですけど、ここ実はわざとMemcached互換じゃなくなっていて、
ここは自分が昔ハマったことあるからっていうところなんだけど、まずRedisの話を先にしちゃうんだけど、
Redisはキーが存在していないキーに対してインクルメントを実行すると、そのキーに対して1が入るんですよ。
存在しないものに対してインクルメント。
存在しない場合は0だと、0が入ってるって仮定して1が返ってくるようになってるんですね、Redisは。
そうですね、なんか素直な感じですね。
Memcachedは残念ながらこれができなくて、存在しないキーに対してインクルメントすると、存在してませんよっていうエラーになっちゃうんですよね。
なるほど、先にキーを確保しとけよっていうことなんですね。
やりたければ最初に0をセットしてからインクルメントしないとMemcachedはインクルメントできない。
なんか素直じゃないですね。
これ何が困るかっていうと、アトミックに処理できないんですよね。
例えば同時にリクエストが来て、インクルメントしないといけないリクエストが2つ同時に実行されたときに、1回0セットしてからインクルメントってやると、タイミングによっては本当は2にしないといけないのに1になっちゃう可能性がある。
ありますね。
だからこれって良くないんですよ。アトミックに処理できないから、レディスみたいな存在しないキーに対してインクルメントしても1以下返ってくるって仕様じゃないとアトミックな処理ができないんですよ。
自分はこれでハマったことがあるので、自分で自作するんだったらMemcachedの実装に合わせずに、レディスみたいに存在しないキーに対してインクルメントできるようにしたいなと思って、わざとMemcached互換じゃなくしてます。
これなんでMemcachedでそんなセットを先にゼロで入れないといけないんですかね。
分かんないですけど、正直自分はこれ結構困ったので、なんでこうなんだろうとは思ってるんですけど。
直してほしいですよね。
直してほしいなと思ってるんですけど、これ結構人によるというか、結構キャッシュ系のライブラリとか仕組みっていくつかあるんですけど、これ結構バラバラなんですよね。
このインクルメントの仕組みというか。
存在しないキーに対するインクルメントをどう処理するかって結構実装によってバラバラですね。
なるほど。Memcachedだったら絶対1回はセットしないといけないっていう。
存在しないキーに対してインクルメントできないので、セットしないといけない。
ここだけはちょっとレディス側に寄りたいですね。
そう思ったんで、うつろはわざと非互換にしてます。
別にこれで困ることあるかっていうとない。既存のMemcached使ってる実装はやってないはずだから、別にこれでバグるってことは基本ないはず。
なるほど。
っていう感じでやっていて、これでMemcached互換として動くじゃんってなったんですよ。
ちなみにDecrementのほうはどうなんですかね。
Decrementも一緒です。
なかったらNot Foundっていう。
Not Foundと一緒ですね。
うつろではどうしてるんですか。
うつろは存在しないキーに対してインクルメントもDecrementもできます。
Decrementするとゼロになるって感じなんですか。
マイナス1ですね。
マイナスになるんですね。ちゃんと。なるほど。あんまマイナス扱うことないですよね。
あんまないけど、別にあっても困らないんで。
なるほど。ちょっとインクルメントと巻きほどを揃えたっていう感じですね。
そうですね。
っていう感じですね。なので、これで動いたじゃんと思って。
自分が管理してるMemcached使ってるレポあるんで、実際に実装切り替えて動かしたんですけど、
動いたんですよ、普通に。
動く。
普通に動くじゃんってなったんですよ。
いいじゃないですか。
いいじゃんってなったんですけど、じゃあこれを実際のMemcachedに置き換えられるかっていうと、
自分は置き換えたくないって思ったんですよね。
なんで。
っていうのがさっきの、さっき話した話で、リンクドリストの話なんですけど、
これポインター外してるじゃないですか。
これポインターを外して打つみたいな処理をするないといけないんで、
これって平行に動かせないんですよ。
なるほど。
ゼコ5を使ってるのに、プログラミングで動かせなくて。
ロックしちゃうんですね。
ロックしちゃうし、しかもこれゲットしたときもLRUの関係で、
リンクドリストを先頭に持っていかないといけないから、
ゲットでも書き込みが走るんですよ。
ロックしちゃいますね、ゲットでも。
ゲットでも書き込みのロック、完全なロックしないといけない。
そうすると、ゲットセットと両方ともで完全にロックしないといけないから、
これ1コア以上使えないんですよね、この実装って。
あとちょっと今思ったんですけど、マルチゲットの挙動って、
もしかして先頭から1個ずつ処理していくみたいな感じになっちゃう?
グチョグチョな処理ですよ。
ですよね。
ロックして全部処理して、処理が終わったらアンロックする。
そこでも平行で全ての値を取りにいくみたいなことは絶対できないってことですね。
そうですね、その間にやることはできないですね。
なので、実装できたできたと思って実装読んでて、
あれこれ1コアしか使えないじゃんってことに気づいたんですよね。
パフォーマンスが出ないというか、リソース。
パフォーマンスが使えない。
自分はMemcachedの大規模の運用ってやったことあるんですけど、
MemcachedってCPU結構積んだマシンで動かしても、
ちゃんと複数のCPU使えるんですよね。
賢いね。
だから絶対にうつろみたいな実装にはなってないはずなんですよ。
本家のMemcachedって。
それで、本家のMemcachedって実装どうなってんの?ってなったんですよね。
うつろ作って。
すごくね?ってなりますね。
できたできた動いたじゃんって言って余裕じゃんって思って、
実装見たらあれこれじゃ全然パフォーマンス出ねえじゃんってことに気づいちゃったっていう。
このアーキテクチャだと不可能だってことですよね。
そうCPU1コア以上使うのが不可能だってことに気づいてしまったんですよ。
なるほどちょっとなんか、
机上ではいい感じだったけど、
実際にやってみると、
エコで動かせねえっていうような。
当たり前の問題にぶち当たったってことですね。
実際に自分で実装してみて、すごく当たり前なんです。
実装しちゃうと絶対にこれ1コア以上動かないって自明なんだけど、
やっぱりそんなことあんま考えたことなかったから、
あれこれMemcachedと同じけどなんないじゃんっていうことに気づいちゃったんですよ。
確かに言われて気づきました。
で、Memcachedの方の実装は読んだんですかね?
ちょっとMemcachedのブログとかあったんで、
Memcachedがどうやってんのかっていうのを軽く調べてみました。
どうでした?
これはJVMのGCみたいだなって正直思ったんですけど、
世代で分けるんですよ。
世代で分ける。
まず最初にセットしたら、基本的にはHotに入るんですよね。
で、Hotに入って、
で、GETされたら、
一回GETされたって情報は残すんだけど、
さっきのLinkedListみたいなことはやんない。
それがやっちゃうと一回ロックしないといけないから。
書き込み処理はしないってことですよね。
そうですね。
一回GET来たらインクリメントするんだけど、
それだけしかやらない。
で、なんでインクリメントするかっていうと、
それが、もしGETがセットされた後にGETが来てるんだったら
WARMっていうのに行くんですよね。
で、もし一回も見られてなかったらCOLDっていうのに行くと。
グループ分けをするってことですね、データの。
そうですね。よく参照されるやつとあまり参照されないやつっていうのをグルーピングする。
そう。なので結構思ったよりよく考えられていて、ちょっと軽い気持ちで再実装したのを本当にすいませんでしたみたいな感じに途中からなったんですけど。
でも確かに話を聞く前だったらなんでこんなことしてるんだろうなっていうところが確かに理屈ありそうっていうような感じですね。
そうですね。やっぱりこの辺も再実装して初めてちゃんと気づいたところなんですよね。
意識しないですよね。
意識しないし、ちゃんとCPU分散するからMemcachedって、Memcachedパフォーマンス出せていいよねぐらいにしか思ってなかったんで。
すごい作りになってるんだろうなという気が若干。
そうですね。だからLRUも本当にリーストじゃない可能性あるんですよね。
真面目にちゃんと管理してないから、あんまり多分こいつ使ってないだろうみたいな感じでやる。
けどそれで全然いいんですよねMemcachedって。
消えてもいいデータを残すっていう大前提があるからってことですよね。
そうですね。そのCacheでしか使わないから、そんぐらいの雑さでいいし、それでうまく動かないんだったらMemcachedの使い方として間違ってるから、これぐらいでいいんですよ。
アプリケーション側の構成というか使い方を改めてくれっていうことですね。
そうなんですよ。だからMemcachedの設計としては完全に正解で、これでいいんですよMemcachedは。
だってCacheヒットしなかったらもう一回セットしろよって話ですもんね。
そうですね。
アプリケーション側からしたら。
アプリケーション側でちゃんとやってくださいっていう。
かなり割り切ってるんだけども、割り切りが平行的にCPUちゃんと使えるとかそういったところに生きてきてるんですね。
そうなんですよね。
いい話だな。最終いい話だなと思ってきたんですけど。
軽い気持ちで再実装して、あれこれMemcachedと同じ動きにならないってなってMemcached見てMemcachedすげえってなったっていう話です。今回の話は。
いい話だなと思って。別にやらなかったら絶対知らないし気づけないですね。
そうです。自分も本当にやらなかったら絶対気づかなかったですね。
勉強になるなという感じですね。本当に。
こういうことがあるからこそ既存のツールだったとしても一回勉強がてら機械で再実装してみるっていうのは結構有用そうなんですかね。
有用だと思いますね。ツールにももちろんよると思うんですけど、自分はすごいMemcachedやって勉強になったので、軽い気持ちでやってみてもいいんじゃないかなって思いましたね。
いいですね。あとはプロトコルの主要だったりとかも結局調べることになるかなと思うのでね。再実装の過程で。