たごもりすメモ

コードとかその他の話とか。

象印の加湿器が最高で当面これしか使いたくないという話

ちょっとTwitterに書いたら食い付いてくる人が多かったので、ではアピールしとこうと思った。

TL;DR

  • 衛生面などを考えると加熱式加湿器*1を使いたい
  • 加熱式加湿器は加熱部に残留物が結晶化して固まるので手入れがめちゃ面倒
  • 象印のやつは電熱ポットと同じなので、たまにクエン酸ぶちこむだけで超ラク、最高です
  • 加湿器運用の手間を減らしたい人にとにかくお勧めです

自分が使ってるのはこれ。今年大きいのを買い足した。旧モデルの小さい方とあわせて2台とも使ってます。

EE-DC35・50 | 加湿器 | 生活家電 | 商品情報 | 象印マホービン

加湿器を加熱式のに替えるとき、某社従来型*2象印の(2020年モデル)と2台買ったんだけど、手入れの手間があまりに違いすぎて従来型のは春先に廃棄、今年象印のを買い足しました。当分このタイプしか買わないつもり。以下詳細。

なお、この記事で「従来型の加熱式加湿器」と表現する場合、これは加湿器本体と水タンクが分離する形で、かつ加湿器本体側に(あまり大きくない)加熱部が存在するもののことを指します。加熱式加湿器で象印以外のものはたいていこのタイプのはず*3

加熱式の加湿器を使いたい

加湿器にはいくつかタイプがあるが、前はそのうち超音波式のものをメインで使っていた。ただしこれは加湿器がカビたりすると、その菌をそのまま水蒸気といっしょに空気中に放出したりすることがあって、手入れをきちんとしないと健康によくないという話がある。気化式も同じ問題があるとされる。あちこちでいっぱい書かれてるので詳しくはググれ。

また超音波式は水道水の不純物もそのまま水蒸気にする。このため最終的に室内のものに微粒子が積もるようになってて、TVやモニタのディスプレイ面が顕著に白くなるのが気になっていた*4

なので、ちょっと健康面でも気になってきたので加熱式の加湿器を使うことにした。加熱式なら水を高温の蒸気にして放出するので、雑菌などが繁殖しづらい。また不純物も水蒸気内に入らず加湿器側に残るため、室内の機材への影響もあまり考えなくてよい。

従来型の加熱式加湿器は手入れが面倒

ところが、この残留物が加湿器側に残るというのが問題だ。従来型の加熱式加湿器*5の場合、加熱する部分およびその周辺にあるフィルタに、水蒸気化したあとに残された残留物が高密度で堆積して結晶化する。一度固くなってしまうと加湿効率が落ちてしまって期待したほど加湿できない。

なので、本来は結晶化する前に手入れしなければいけない。前に使っていた加湿器では、1週間に2回以上の頻度で、本体から排水し汚れを落としフィルタを掃除しろとあった。またフィルタは定期的に交換しろとも。これが本当に面倒で、給水の手間を減らすための分離型水タンクなのに、高頻度で本体ごと洗浄しないといけないみたいな話になってくる。やってられるか!

そして手入れをサボると加熱用のプレート周辺に不純物が堆積してしまい、これを取り除こうと思うと一度や二度のクエン酸洗浄ではぜんぜん足りなくて泣きそうになったりする。前はマイナスドライバーで結晶化したものをガンガンやって取り除いたりした。つらい。

象印の加湿器は手入れが本当に楽

ここで、象印の加熱式加湿器とはどういうものかをまず説明すると、要するにご家庭の台所にある電熱式の湯沸かしポット。あれ。お湯が沸くと上から蒸気が吹き出てくるが、その蒸気で加湿すればよくない? あったまいー! という感じ。見た目もほとんど湯沸かしポットと変わらず、給水用の上ブタを開けると本体内部ほぼ全体がそのまま水タンクになっていて、加熱部を兼ねる。もちろん注ぎ口はないが。

給水するときには、取っ手、湯沸かしポットにもありそうなそのままの取っ手をつかんで本体ごと台所に持っていき、上ブタを開けて給水作業する。取っ手があるのは本当に大事。

この加湿器が従来型と異なるのは加熱部の広さ。つまり本体サイズ*6がそのまま大きい加熱部になっているため、狭い加熱部の加湿器と異なり、不純物が残留する面積が広い。また加熱が給水タンクそのものに対して直接行われるので、不純物は使用中だんだん濃縮されるものの、給水のタイミングで最後に残った特濃残留物水を捨ててしまってから給水を行える。たぶんこのため、わざとタンク内に少し水が残留している状態で給水指示が出て加熱が切れるようになっている。

まあそれでも使っていると底面などにちょっとずつ不純物が溜まるんだけど、それもたまにクエン酸洗浄するだけでいい。湯沸かしポットでたまにやるやつと同じ。給水のタイミングで気になったらタンク内に直接クエン酸を適量ぶちこみ、お湯をちょっと入れて溶かしたら水を満水まで入れて、洗浄モードのスイッチ入れて数時間放置。完了したら教えてくれるので水を捨てる。終わり。

いまの自宅での運用だと、給水を繰り返しながら24時間動かしっぱなしという状況でも、2週間に1回も洗浄しなくても問題ない感じ。また手入れそのものが楽なので、気になったときにちょっとやるのもストレスにならない。気になったらクエン酸洗浄すればきれいさっぱり汚れが落ちる。最高。

ということで、使っていてあまりに楽なので、自宅の加湿器はぜんぶこれにしてしまった。よかったよかった。

象印の加湿器のデメリットとされる点

とはいえ、いくつかデメリットとされることもある。自分は以下のように、特に気にしていない。

消費電力

加熱式加湿器は消費電力が大きいとされる。さらに特に象印のやつは消費電力が大きい。まあね、湯沸かしポットだからね……。

とはいえ、消費電力が大きいといっても、電力が何になるかというと、要するに熱になるのだ。で、この時期はどうせ寒いから暖房動かしたりしてて、え、それってつまり単に暖房の補助だと思えばそんなものなのでは? というように思えなくもない。事実、自分の仕事部屋が超寒いのに現状マトモな暖房を据え付けていなくて、加湿器が動いていると如実に温度上がって過ごしやすくなるので助かってたりする。居間も温度上げておきたい昨今なので、エアコン使用をおさえると思えば、まあいいんじゃないかな。

加湿能力

本体サイズがほぼ水タンクと同じなので、あまり大きくできないという制約があると思う。大きくしすぎると給水後に持ち運べない。このため、高い加湿能力を求める人には向かない場合があるらしい。

湯沸かしポットなので、特に給水してから温度が上がるまでは湯沸かしポット的な音がする。しょうがないかなー。自分や家族は慣れたのでいちばん音を立ててるときでも安眠してる。

いちおう「湯沸かし音セーブ」という機能があって、これをオンにしてるとかなり音量が低下すると思う。が、自分は慣れたので今冬は一度も使ってないなこの機能。

見た目

完全に湯沸かしポット。特に前に買った2020年モデルのものは光沢のある白色の外見だしボタンのデザインなんかも調理家具っぽかったため、居間に置いていると「なぜ台所にあるはずのものがこんな場所の床置きに」って感じで違和感がすごかった。慣れたけど。

今年買ったほうは、調べてみたら2021年に新しくなったモデルっぽい。外見がオフホワイトになって、ボタンのデザインなんかも心なしか生活家電っぽいよう改良された気がする。グレーとかも出てるし。

まあ、それでもやっぱり湯沸かしポットなんだけどな。

在庫

人気商品なのか、あまり作ってないのか、家電量販店なんかではほとんど在庫を見ない。なのでAmazonで買ってる。しかし通販だと品薄からプレミア価格で売ろうとするところも出てたりするっぽいので、ちゃんと象印の公式のストアが出してるやつを買いましょう。Amazonで現状を見てみると、大きいほうのホワイト(EE-DB50-WA)だけは象印ストアで在庫があるけど、他のグレーのものや小さいほうはどれも在庫がないみたい。象印ストアじゃないところが高価格で出してますね。そして象印ストアには旧型の小さいほうが売られている。うちにあるやつだな。ちゃんと使えますよ。

象印のウェブページに公式ストアってあるんだなと思ったけど、Amazon象印ストアのより高い……。

「eedc」の検索結果 | 象印ダイレクト

似た商品

前は存在しなかったと思うんだけど、象印の製品を見てなのか、最近出てきてるみたい。「上から給水」とか書かれてるやつ。でもサイズがいまいちだったり取っ手がなかったりな気がする。特に取っ手がないのは、本体ごと持ち運んで給水しないといけないので、致命的だと思う。でも取っ手つけると格好悪いしね。悩むよね。でも取っ手は重要だと思います。

でも各社追随して使いやすいのが増えると嬉しいな。TIGERとか出さないのかな。w

まとめ

というわけで、象印の加湿器はとにかく手間をかけたくない人にお勧めですよ! というお話でした。

*1:スチーム式などとも呼ばれるが、加熱しない水蒸気と区別がつかないのでこの記事では加熱式と呼ぶ

*2:タンクと分離してるやつ

*3:と思ったんだけど、Amazon見てみたら、今年からの新モデル? で数社から似たようなのが出ている。後述。

*4:これは気化式では起きないらしい

*5:と、気化式の加湿器にも同じ問題があるはず

*6:正確にはその底面積

XcodeがMain.storyboardを見付けられずiOSアプリが落ちる

……ことがたまにあって、なんでかなと思ったけどgit checkoutなんかでstoryboardのリビジョンをXcode外部から操作してると起きるのかなという気がする。アプリが起動しかけてから以下のようなエラーメッセージ出して落ちるのがタチ悪い。

"Could not find a storyboard named 'Main' in bundle NSBundle"

Clean Build Folderとかやっても直らないので、以下のようにして直す。

  1. Main.storyboardを選択して "Delete" -> "Remove Reference"
  2. 外部でやる必要のあるgit操作など実行
  3. "File" -> "Add File to ..." してstoryboardを再度追加

起きるたびに時間を無駄にしてる気がするんでメモ。

LFAというAWS API Gateway用Lambdaをそのまま動かすRuby用Web frameworkを作った

この記事はRubyアドベントカレンダー2022の15日目の記事です。

以下のような話をお送りします:

  • AWS Lambda + API Gatewayを使ってる話
  • LFAというWeb frameworkをガッと書いた話
  • LFAでLambdaの手元開発が楽になるのではという話
  • Ruby 3.1で導入されたKernel#loadの便利機能が超便利という話
  • Lambda関数ごとに環境変数ENVの内容を変えたいという話

最近AWS Lambdaべったりな話

最近自分でサービスを作ってるんですが、サーバサイドは全部AWS Lambda + API Gatewayでやっつけてます。 EC2を常時起動して管理するのもやだし、大した規模でもないからゴツいビルドパイプラインをセットアップしてコンテナイメージをビルドするのも面倒だし、みたいなことを考えると、コードをそのまま送り付けるとあとは動きつづけてくれるAWS Lambda + API Gatewayの組み合わせが便利だなってことで。ただしRubyのバージョンが2.7だけなのがちょっとアレですよね。

アーキテクチャは手元がM1 macなのと、なんとなく以前所属した感慨もあってarmにしてるんですが、bundleするライブラリにmysql2を使ったところネイティブビルドされた部分の読み込みが実行時にちょっと理解不能なエラーを起こしてうまく動作しませんでした*1。これを追ったりデプロイ用の環境をガチャガチャやったりというのがちょっと面倒だったんで、pure Rubyruby-mysqlを使ったりしてます。(4.0に上げるのはこれからだけど)使ってますよ!!! Pure Rubyなライブラリはすばらしい。JITで速くなるともっといいんだけど……。

で、LambdaなのはAPIハンドラの数もリクエスト数も少ない今のうちはいいんだけど、もしサービス当たってアプリケーションが複雑化したら大量にあるLambdaの管理が大変だし、コストも高くなるし、そうなったときのことを考えてるの? みたいな話は出てくると思います。

回答としては「敢えて考えてない」です。超初期のスタートアップ*2では現在の開発効率だけを考えるのが鉄則で、当たったら当たったときに考えりゃいいんです。不確かな先々のことを考えて今の開発を非効率にする理由は全くありません。

とはいえ、いざという時のことを考えてしまう

まあね、とはいえね、自分も前の会社で経験があるんですけど、いざってときにやろうと思ってても、その時はその時で最優先のサービス開発の優先度設定があって、結局その瞬間にプロダクトの価値向上に寄与しない作業はどうしても後回しになるんですよね。で、後回しにされるほどツケが貯まっていって、日々の開発効率はどんどん落ちるし、そうはいっても動いてるサービスは止められないし、みたいなことは起きる。ランニングコストも高止まりする。開発者体験も悪化する。ああ嫌だ。

みたいなことを考えると、数あるLambdaをとりあえずどうにかする方法があるといいよなあ、Lambdaとして書いたコード、そのまま普通のアプリケーションサーバに持っていけないかなあ、ということを妄想します。

LFAを作った

ということで、LambdaをそのままmountしてRackアプリケーションにしてしまえるフレームワークを作ってみました。これがあれば、Lambdaに日々デプロイしているコードをそっくりそのままUnicornやPuma上で動かせます。もちろん複数のLambda functionを1プロセス上で併存させられます。

github.com

YAMLの設定ファイルとして、API Gatewayのリソース設定みたいなやつと、あと使う関数のリストを書きます。

# config.yaml
---
resources:
  - path: /api
    resources:
      - path: /country
        methods:
          GET: myfunc-countries
      - path: /data
        resources:
          - path: /csv
            methods:
              GET: myfunc-data-csv
          - path: /json
            methods:
              GET: myfunc-data-json
functions:
  - name: myfunc-countries
    handler: myfunc.Countries.process
    env:
      DATABASE_HOSTNAME: mydb.local
      DATABASE_PASSWORD: this-is-the-top-secret
  - name: myfunc-data-csv
    handler: myfunc.Data.process
    env:
      OUTPUT_DATA_TYPE: csv
  - name: myfunc-data-json
    handler: myfunc.Data.process
    env:
      OUTPUT_DATA_TYPE: json

あとはそれをRack用のconfig.ruから、LFAを使って読み込めば、お好きなアプリケーションサーバで起動できるはずです。

require 'LFA'
run LFA.ignition!('config.yaml')

動かすLambda関数のファイルは設定ファイルと同じディレクトリに置いておきます。またその他必要なライブラリはGemfileに書いてBundler経由で起動されていれば普通に使えるはず*3

構想にAsakusa.rb数週間分、実装は2日でガッ、という感じでなんとなく動くかな? という状況なので、rubygemsにもまだリリースしていません。READMEにもありますが足りない機能もいくつもあるのと、何しろテストが皆無なので、そのへんを毎週のAsakusaなどで少しずつやってからリリースの予定です。

実際使いものになるの?

もちろん現状では駄目です。

いっぽうでAPI Gateway的なルーティングをやるだけなら実はあまり大した機能がいらないので、ちゃんと作りきれば、LambdaなコードをEC2に持っていってリライトの時間ぶんだけ延命、みたいな用途にも十分役立つんじゃないかなと思ってます。大量のLambdaを管理していたのをひとつのサーバあるいはコンテナの管理に集約できるので、それだけでも嬉しい用途がもしかしてあるかもしれません。

あと開発中に気付いたのが、これってAPI Gateway + Lambdaでホストしてるコードの手元開発用にも使えるのではという。現状だとLambdaのコードをまず書き上げて、んでAPI Gatewayでリソースの設定して、それからやっとリクエストを投げるとLambdaのコードのうっかりミスが見付かる、みたいな世界です。それを、まず手元でLFA + puma (+DB)で動作するところまで書いて、それから実際のLambda関数 + API Gatewayリソースを設定、デプロイ、という順序にできる。

普段の開発においても、コード書き換えてLambdaにアップロードしてえいやで試す*4んじゃなくて、手元でちゃんとHTTPリクエスト投げて想定の結果が帰ってくることを確認してからデプロイできるのは、普通に便利なのではという気がしてます。

LFAの実装の話

ここからRubyの面白テクニックの話です。

Kernel#loadを使って複数のLambda関数を読み込む

複数のLambda関数のコードをひとつのRubyプロセスに読み込むとき、まず考える必要があるのは、互いに衝突する(かもしれない)それぞれの関数をどうすれば衝突させずに読み込めるか。また、Lambda関数のコード*5はキャッシュなどのステートを持っている可能性があるので、例えば異なるLambda関数が同じクラス/モジュール/オブジェクトを使用していると、AWS Lambda上ではプロセス*6が異なっていたから問題なくても、1プロセス上で動作するRackアプリケーションとしては困ります。

なので、Lambda関数のハンドラfunc.Modname.methodにおけるファイル名funcを、関数ごとに異なる名前空間で読み込めないかな、ということをまず考えます。この方法を、やっつけな方法*7から邪悪な方法*8まであれこれ考えてたんですが、基本に戻って関連しそうなメソッドのドキュメントでも再確認してみるかとeval系、Kernel#requireKernel#loadと見ていくと、見慣れないものをKernel#loadに発見しました。第2引数 wrap

load(filename, wrap=false) → true

(snip)

If the optional wrap parameter is true, the loaded script will be executed under an anonymous module, protecting the calling program’s global namespace. If the optional wrap parameter is a module, the loaded script will be executed under the given module. In no circumstance will any local variables in the loaded file be propagated to the loading environment.

https://docs.ruby-lang.org/en/3.1/Kernel.html#method-i-load

これじゃん! まさに欲しかったものじゃん!! 超便利!!!!! Kernel#loadrequireと違って同じファイルでも何度も読み直してくれることはもちろんみなさんご存知のことだと思いますが、まさかこんな機能があったなんて。あったっけ????????*9

ということで以下のようなコードで試してみる*10と、完全に期待通りの動作です。勝った!!!

# func.rb
module Function
  def self.process
    {statusCode: 200}
  end
end

# test.rb
m1 = Module.new
load('func.rb', m1)

m1.const_get(:Function) #=> Function
m1.const_get(:Function).process #=> {statusCode: 200}

Function #=> NameError (想定通り)

m2 = Module.new
load('func.rb', m2)

m1.const_get(:Function).object_id == m2.const_get(:Function).object_id
# => false
# つまりDataがふたつ、異なるインスタンスとして読み込まれている

しかしこんな機能あったっけ、と調べてみたら、なんと3.1の新機能でした。なんてこった。目にしてたはずなのに、こんな超絶便利機能に気付いていなかったなんて……。

Feature #6210: load should provide a way to specify the top-level module - Ruby master - Ruby Issue Tracking System

プロと読み解く Ruby 3.1 NEWS - クックパッド開発者ブログによると「DSL に使える、のかなぁ?」だそうですが、いやいやいや、こいつは何にでも使えそうな気配がしますよ。だって別空間でloadして主名前空間への影響を避けつつAbstractSyntaxTree.of(anonMod.const_get(:Target).method)とかできるんでしょ。便利!*11

これで複数のLambdaを別々に1プロセスに読み込める、と言いたいところですが、ちょっと問題があります。loadされたファイル内でrequireされているファイルの内容は、wrapしているモジュール以下に読み込まれると同時に主名前空間にも読み込まれてしまいます。また複数のLambda関数で同じファイルをrequire(またはrequire_relative)している場合、requireされた結果のモジュール・クラス等は共有されてしまいます。これは昨日のhsbtさんのエントリで「ライブラリを複数バージョン同時にロードする機構」のあたりでも言及されてます。

ただしこれは、LFAの用途に限ってはあまり問題じゃないかなと思います。複数のLambda関数があるといっても、ひとつのディレクトリツリーにあるなら普通はライブラリや自作クラスを共有しているはずだからです。まあそれでもハマるケースとハマらないケースがありますが、多くは問題ないでしょう*12

Lambda関数ごとに別々のENVを見せる

AWS Lambdaでは多くの場合、環境変数を用いて設定すると思います。これはもちろんLambda関数ごとに別々のものを指定できなければならず、同じLambda関数ハンドラfuncfile.Modname.methodであっても、別関数なら別の環境変数セットを持っている、みたいなこともあると思います。これもどう対応しようかなと思ってましたが、Kernel#loadwrap機能を見たらとりあえず以下のような対応が思い付きました。

m = Module.new
dummy_env = {
  "VARNAME" => "value",
}
m.const_set(:ENV, dummy_env)
load('funcfile.rb, m)

これでfuncfile.rbがloadされるときにはトップレベENVとして、作成しておいたダミーの環境変数が参照されます。解決。……と思ったら、もちろん、loadされたファイル内でrequireされている側で参照されているENVには効果が及ばず。どうしようかなと思ったんですが、しょうがないので大元のENVをゴニョってやることにします。

まず次のように標準添付のdelegateライブラリを使ってENVを置き換えるクラスを作ります。

require 'delegate'

module LFA
  class Adapter
    class EnvMimic < Delegator
      def initialize
        @is_active = false
        @box = nil
        @env = ENV
      end    

      def __getobj__
        if @is_active
          @box
        else
          @env
        end
      end

      def __setobj__(obj)
        @box = obj
      end

      def mimic!(env)
        @box = env.dup
        @is_active = true
        yield
      ensure
        @box = nil
        @is_active = false
      end
    end
  end
end

これは普段はすべてのメソッド呼び出しを単に本来ENVだったものに移譲しますが、mimic!(env) do ... end メソッドで指定したブロック内では、envで指定したHashに対して移譲します。これでこのブロック呼び出しを抜けるまでは、ENVを参照するとダミーの内容が返ってくるという。便利! 環境変数ENVの内容をオーバーライドしたいとか人類誰でも一度は考えると思うので、これだけ切り出しても便利かもなあ。

で、あとはこれで本来のENVを置き換えてやります。普通にやると警告が出るので、警告を放置しない良きプログラマのたしなみとして無効化しておきます。

# 必ず読まれるどこかのファイルのトップレベルで
__original_warning_level = $VERBOSE
begin
  $VERBOSE = nil
  ::ENV = LFA::Adapter::EnvMimic.new
ensure
  $VERBOSE = __original_warning_level
end

これでだいたい解決するんですが、これでもrequire対象ファイルのトップレベルやモジュール・クラス宣言部でENVを参照しているケースだけがカバーできません。loadENV.mimic!でくくっても、異なるLambda関数が同じファイルをrequireしている場合には2回目には読み込みが発生しないためです。困ったなあ、ということでこれもとりあえず注意して回避してね、ということになります。

ということで

Lambdaで楽をするぞ! と思っていたら不要不急の(?)OSSがひとつできていました。おっかしいなあ……。いやこれは趣味だから! 仕事でやったわけじゃないから!

*1:多分libmysqlclientまわりの話だと思う

*2:とも言えない個人開発なのが現状だけど

*3:自信がない書き方にちゃんとしたテストがされていないことが明らか

*4:まあstageとかblue-greenとかいろいろ使うんでしょうけど

*5:モジュールもしくはその他オブジェクト

*6:実際にはコンテナ単位での分離なのでもっと強いですが

*7:ファイルを文字列として読み込んだ上で外側をmodule Anon1; ...; end でくくってevalする

*8:TracePointを使って何とかしてゴニョゴニョする、マクロ的な方法でなんかやる、etc

*9:後述しますが3.1の新機能でした

*10:どうでもいいけど、おれ、なんかいっつもModule.newしてるなってLFA書きながら思いましたね

*11:正確には、第2引数をtrueにして無名空間に対してloadすること自体は3.0以前でもできていたようで、その場合でもObjectSpaceを経由してloadされたものを取得して……とかできなくはなかったみたいだけど

*12:あとでREADMEにLimitationセクションを追加する予定です

Fluentdのflush_mode immediateはいつ使うのか

Fluentd実践入門をあらためて手元でぱらぱらやってたら、しまった! この話をどこかにちょっとでも書こうと思ってたのに忘れてた! という話が出てきたので、忘れないうちに書いて放流する。

flush_modeとはなにか

FluentdのOutputプラグインには<buffer>セクションで指定できるflush_modeというパラメータがあって、これはOutputプラグインがどういう基準でバッファ内のデータを書き出す(writeメソッドを呼ぶ)かという戦略をコントロールする。有効な値はdefaultimmediateintervallazyの4つ。

ただし多くのケースでこのパラメータは明示的には指定されていないはず。というのも、デフォルト値であるところのdefaultの場合には、<buffer>セクションに指定されている他のパラメータ((と、例外的に<buffer>セクションの外側に指定されているflush_intervalも見ている。これはFluentd v0.12以前のプラグインの使い方をそのまま踏襲している設定について、動作を壊さないための措置。))を確認して適切なモードを自動的に選ぶようになっているからだ。たとえばバッファのチャンクキーにtimeが指定されている場合にはlazyが選ばれる。

といったようなことは「Fluentd実践入門」のp166-167あたりにもちろん書いてある。みんな買って読もう!(宣伝)

flush_mode immediateとは何か

ところで、flush_modeのうちには、他のパラメータをどうセットしても暗黙には選ばれないものがあって、それがimmediateだ。これはFluentd v1系*1における完全に新しい機能・挙動なので、特に過去の設定パターンとの互換性を考える必要がないため。

これがどう動作するかというと、バッファリングは行うが、データが書き込まれたバッファチャンクは即座にflushされる。つまり一回のemitで渡ってきたデータは即座にwriteに渡される。

flush_mode immediateは低レイテンシ、かつ出力リトライが必要な場合に使う

ここからが書き忘れたところ。

immediateは、動作としては非バッファOutputプラグインに近いが、データが一度バッファチャンクに書き込まれる点が異なる。で、このモードをどう使うのかという話。

データ処理基盤を作っていると、とにかくこのデータだけは低レイテンシで処理したい、ということがたまにある。全データに低レイテンシを求めるのは非効率だが、それでもごく一部のデータに限ってであれば、イベントの発生から処理の最後、典型的にはオペレータへの通知など、までを数秒で行いたいというような要求はありうる。

ただし、低レイテンシで処理を行うからには非バッファOutputプラグインを使えばいいかというと、それも困るケースがある。ネットワーク転送などを行っているときに障害が発生したら即座にデータが失われるようではマズい。このため、普段は可能な限り低レイテンシで出力を行うが、障害時にはバッファリングとリトライを行い、その間にOutputプラグインが受け取るデータはバッファチャンクに適切に貯まるようにしたい。flush_mode immediateを指定していればこれが実現できる。

過去に(現在でも?)これを実現する方法のひとつとしては、flush_interval01にセットしてしまうという手があり、実際にやっている例も知っている。ただしこれはデータの到着にバラつきがあるときでもflushスレッドがかなり忙しくループしてしまうため、全体としてはあまり効率的ではない、という問題があるし、設定の意図があまり明確ではなくなるという難点もあった。

immediateは気軽に使っていいのか

あまり気軽に使うのはオススメしない。バッファOutputプラグインは実装としては通常の、バッファチャンクが適切なサイズに育ってから一気に出力するような処理をするように書かれているだろうから、それを半ば無理矢理に高頻度で呼ぶようになるので、想定していなかった問題が起きないとも限らない。

またflush_mode immediateを使って出力を行っている場合、通常の状況では出力単位が1〜数イベント、サイズにして数KB程度までとなることが多いだろう。一方で障害時にはリトライを続けている間に到着したデータはchunk_limit_sizeで設定したサイズまでチャンクに溜め込まれるため、障害が解消した直後はいきなり数MBのデータがどかっと出力されることになる。出力データサイズの不均衡は後々の処理において、処理性能や使用メモリサイズなどの点で問題を誘発することが多い。

なので、データ処理パイプライン全体をちゃんと考えて、どうしても低レイテンシ・出力リトライを両立させたいところのみでflush_mode immediateを使用しましょう。

まあ、実際のところこれを必要とするのはごく一部の大規模なデータを扱っている人達だけじゃないかなとは思う(ので書き忘れてた)。他にも大規模データ基盤向けの話はいくつもあるんだけど、それをまとめた章を書こうとしたときに手が止まったんだよなあ。対象読者数があまりに少ない内容になるんで……。

というようなことが

Fluentdの他の部分についてえんえんと書かれているFluentd実践入門をよろしく!!!!!!

Fluentd実践入門 ──統合ログ基盤のためのデータ収集ツール:書籍案内|技術評論社

amzn.to

*1:v0.14以降のこと

「Fluentd実践入門」を10月8日に出版します

Fluentd実践入門

Fluentdの現バージョン(v1.15)について世界で一番詳しい本です。というか、Fluentdそのものだけについての、おそらく世界で唯一の技術書です。 出版社は技術評論社です。電子版もGihyo.jpやKindleはじめ各社で出ます。買ってね!

gihyo.jp

TL;DR

  • 発売日は10月8日です
    • 一部書店ではちょっと早く9月末に並ぶかも
    • 電子版は発売日よりちょっと前に出るかも1
  • 544ページ、Fluentd本体については考えられる限り盛り込みました
    • Fluentdをなんとなく使っている人が確信を持って使えるようになれるはず
    • 組込みプラグインの頻出用法、本番環境での運用ノウハウ、プラグイン開発からテストなどまで
  • エコシステム的な部分についてはカバーできていません
    • Kubernetes上での運用やFluent Bitとの組み合わせとか
    • AWS FireLensやGCP CloudLoggingなどで使うときのノウハウとか
    • でもそのへんを調べるときの基礎的な知識が得られるでしょう

最後のゲラのチェックと修正はさっき終わらせたぞ!

なお、本の一部に見開きで左右ぶち抜きの表があります。まあまあな数あります。これがもしかしたら、電子版ではちょっと見にくいかもしれません。残念ですが各ページには収まらなかったのでこうなっています。電子版を購入される方にはすみません。

アピールポイント

Fluentdについて、前にけっこうやったしだいたい知ってる、と思う人もいると思います。おそらくその人達の多くは次のことを知らないでしょう。

  • out_forward送信先リスト2、今では再起動なしで増やしたり減らしたりできる
  • Fluentdの設定ファイルはYAMLで書ける

ひとつめはFluentd v1.8でサポートされたService Discoveryプラグインによるもので、ふたつめはFluentd v1.15で入ったばかりのものです。この本で両方とも(後者はコラムでだけど)解説されてます。Service Discoveryプラグインを使ったプラグインの作りかたももちろん解説されています。ということで、最近便利になった情報もがっつり盛り込みました。

また、Fluentdのことは詳しくは知らないけど、なんか設定してみたら使えたしそのまま動かしている、という人もいると思います。というか、おそらくそういう人は世界中にものすごく多くて、それでなんとなく使えるFluentdはいいソフトウェアだということなんです。

が、それだと高負荷時に何か起きてしまうとか、エンタープライズ案件の超高信頼性が要求されるケースで使うことになってあらゆるパラメータを調べないといけなくなったとか、そういう場合にすごく困る。ドキュメントに書いてあることも多いんだけど英語だし、各々のプラグインのパラメータ起点でしか説明されていないから、全体像としてバッファリングやリトライがどうなっているのか、イベントはどこから来てどこへ行くのか、そもそもイベントって、レコードやタグやtimeって何???? みたいなことがたぶん起きてるはずなんですよ。

あとは、プラグイン書けば簡単に済むはずのことをすごく苦労してる人も多いんじゃないかと思ってて、その人たちにプラグインを書くということがどれだけ簡単なことなのかを伝えられてもいないんじゃないかと思います。

ということで、この本ではそのへんのことがだいたい全部書かれています。いるはずです。そのへんのことに世界で二番か、もしかしたら一番目に詳しいのが自分です。なので、この本は世界で自分しか書けなかったと思います3

Fluentdがもうちょっと流行ってる感ある数年前に出せてればよかったかなと思う反面、ログの扱いという需要が消滅することは絶対に無くて、かつ代替OSSも出てきていないと思うので、たぶんみんな静かに当たり前に使うフェイズになっているのかな、と思うんですよ。なので、もしかして今こそ、多くの人に届くのかもしれないな、とも思います。

残念だったけど今後に期待ポイント

で、Fluentdそのものについてはだいたい全部書いたんですが、もちろん書かれていないこともいっぱいあります。

サードパーティプラグイン各々の紹介とか、統合ログ基盤なるものの全体をどう作ってどう使うのかとか、極端に大規模・大トラフィックな環境に特化した運用ノウハウとか。 あと最近ではKubernetes環境での使用方法とか運用ノウハウ、Fluent Bitとの連携やFluentd Bitそのものの解説、AWS FireLensやGCP CloudLoggingなどのFluentdの陰が見え隠れするロギングサービスとの関係、連携、などなど。

もちろん1冊の本で森羅万象をカバーできないので、そのへんは、今後・他の本や記事に期待、ということになります。Kubernetes環境やFluent Bitについては書きたい気持ちもありましたが、ボリューム的な問題や、自分にその周辺のノウハウがほぼ無いことも含めて、ちょっと難しかったです。

しかし、今現在、そのへんの技術や製品のドキュメントをちょっと見ると、なんか普通にみんなFluentdについてのごく簡単な説明をするだけとか、「Fluentdについてはオンラインドキュメントを見てください。さて……」みたいな感じになってるんですよ。でも、ちょっとFluentdのドキュメントサイト見て全部分かれっていうの、だいぶ無理があるはずだったと思うんですよね。なので、この本が出たことで、「この本を読んでください」と言える状態まで持っていけたんじゃないかなとは思っています。

また、今までだとそのへんの発展的なトピックについて解説したり本を書こうとしたりしても「ええ……まずFluentdの解説するの……?」みたいな感じがあったんじゃないかなという気がします。今後はそのへんを「この本読め!」でスキップできると思うので、より発展的なトピックについて、もっと気軽に・詳しく解説してくれる人が増えてくれるといいなと期待しています。

経緯や感慨

この本、ほんとはもっと何年も早く出せるといいなと思って書きはじめたんですよねー。最初は編集さんもつけず、単独でやってました。そしたら最初の半分近くまでは一気に書けたものの、ある章のあるトピックを書こうとして「いや、これ全部書くの無理じゃない……?」みたいな感じで手が止まってしまい4、いきなりすごい期間が空いてしまって。んで、その後なんとか再開したもののぜんぜん進まず、そこから編集の @inao さんに泣きついて、もう何から何までお世話になりながら、なんとか出版までこぎつけました。@inaoさんには本当にもう絶対頭が上がりません。

ぶっちゃけ、途中の手が止まった時期には、できてる分の原稿だけで同人誌として出しちゃってお茶を濁そうかな、とか思った時期もあったんですよ。そうしなくて良かったです。今にして思えば、必要なことがまだあれこれ書かれていなかったし、全体の組み立ても考えられていなくて、本としての出来は3割以下だったんじゃないかという。あれで出して終わってたら、世間に出すべき知識と経験が埋もれたまま終わっちゃってたなと思います。

出せてよかったなと思うのは、前にも書いたけど、これ書けるの本当に自分しかいないなっていうのはあって、今回やれるだけのことはやったから肩の荷が降りたと感じてる部分かな。Fluentd v5とかv10とかになるとまた違うんだろうけど、これでFluentdについてやらないといけなかったことを全部やった、という感じはあります。5

まあ何にしろね、単著ですよ単著。自分が通っているあの本屋に自分が書いた本が置かれる! かもしれない! ウヒョー!

雑誌での執筆は何度もあったけど、本を書くとなると雑誌の原稿とは全然違う作業が大量に、死ぬほど大量にあって大変でした。正直ナメてた。みんなこんな大変なことやってたんだな。すごい。でも単著だったから自分で全部やらないといけなくて大変だったのかもしれない。が、ちょっと共著にはトラウマがあってさ……。6

しかし、とにかくこれで出ます。もう自分が何もやらなくても出るはず。いやー、正直な話、前の会社を辞めてから色々やりつつも、ずっとこの原稿のことが頭や直近のTODOにあって、それがキツくて……。自分は大きいタスクを2並行では処理できないんだなと思い知りました。この本の原稿の作業がようやく終わりそうになった最近、ついに脳内のバックグラウンドでいま書いてるサービスの実装をどうするかを考えられるようになってきて、それがちょっと嬉しいです。

まとめ

がんばって書きました。Fluentd使っている・使いそうな方はぜひどうぞ! Fluentdに関係なくてもどうぞ!


  1. 紙版の発売日10/8に対して、Kindle版は10/6で登録されていますね

  2. <server>で並べて書いてたやつ、YAMLJSONファイルに書いて読ませるか、DNSSRVレコードを使う

  3. だって@repeatedlyは技術書とか書かないじゃん、ぜったい

  4. これは実際には書こうとしていたトピックに無理があって、最終的に収録範囲からは外しました

  5. これはなんなんだ、と思ったけど、ISUCONが完全に自分と関係ないイベントとして勝手に続いているのを見るのと似た感じなのかなといま思いました

  6. だいぶ昔にある共著の話に誘ってもらって参加して、原稿の担当分の半分くらいは書いたんだけど、そのまま編集さんがどっか行って話がポシャったことがあって、あれはかなり悲しかった。あるところで技術書の共著が話題になったとき、当時の共著者メンバーが一人いたのでふと目をやると、ばっちりその人と目が合って、思わず二人で笑ったことが忘れられない。

React appを手元でProduction modeで動かす

react-scripts startで使えるDevelopment modeだとなんか変なことがちょいちょい起きるので、動作確認をProduction modeでやりたい。

ところでこのアプリからはCORSリクエストを送りまくるのでHTTPSのサイトとしてlocalhostにアクセスしたい。Development modeについては、これはpackage.jsonに以下のように書いておいてnpm startすることで実現できる。

...
  "scripts": {
    "start": "HTTPS=true react-scripts start",
...

React appをProduction modeで動かす

単に動かすならドキュメントにあるようにnpm run buildでビルドしたあと、それを配信するサーバを実行すればいい。

$ npm run build
$ npm install -g serve
$ serve -s build

serveコマンドをHTTPSを有効にして実行する

ところがこれだとHTTPなので、HTTPSにしたいときには困る。見てみたらserveコマンドには--ssl-cert--ssl-keyオプションがあって、このふたつを指定してあればHTTPSで動いてくれるっぽい。ちょいちょいと鍵と証明書を作って実行する。

$ openssl genrsa -out key.pem
$ openssl req -new -key key.pem -out csr.pem
(色々聞かれるが、全部Enter連打でいい)
$ openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
$ serve -s build --ssl-cert cert.pem --ssl-key key.pem 

   ┌─────────────────────────────────────────────────────┐
   │                                                     │
   │   Serving!                                          │
   │                                                     │
   │   - Local:            https://localhost:3000        │
   │   - On Your Network:  https://192.168.68.103:3000   │
   │                                                     │
   │   Copied local address to clipboard!                │
   │                                                     │
   └─────────────────────────────────────────────────────┘

できた。

Chromeで証明書チェックをスキップする

できたと思ってChromehttps://localhost:3000を開くと証明書チェックにひっかかって進めない。Development modeのときはAdvanced以下に開くリンクが出るんだけど、こっちでは出ないのは何の違いによるものなのか。

なんでだよと思って調べてみたら、このページ上で(どこかをクリックしてから?) thisisunsafe とタイプすると開けるとのこと。マジかよと思ってやってみたら、できた。マジかよ。

yuki.world

まとめ

できた。やれやれ。

@react-google-maps/apiでの描画地図にPolylineで線を描くと消えなくなる

という問題が起きてあれこれやってた。React難しいの巻。たぶんnpm startで起動できるDevelopment modeでだけ起きる問題。

問題

@react-google-maps/apiでReactアプリ上にGoogle Mapsを表示*1し、そこに好き勝手にマーカーとか線を描きたい。以下のような感じ。

// MyMapComponent
import { LoadScript, GoogleMap, Marker, Polyline } from "@react-google-maps/api";

// ...中略
// in function MyMapComponent
return (
  <LoadScript googleMapsApiKey={myApiKey}>
    <GoogleMaps
      id="myMap"
      mapContainerStyle={{height: "80%", width: "100%}}
      zoom={calculatedZoom}
      center={{lat: calculatedCenterLat, lng: calculatedCenterLng}}
      mapOptions={{disabledDefaultUI: true, zoomControl: true}}
    >
      <Marker key={"map-marker-start-" + start.uuid} position={{lat: start.lat, lng: start.lng}} />
      <Marker key={"map-marker-end-" + end.uuid} position={{lat: end.lat, lng: end.lng}} />
      <Polyline
        key={"map-line-" + start.uuid + end.uuid}
        path={pathFromStartToEnd}
      />
    </GoogleMaps>
  </LoadScript>
);

startとかendあるいはpathFromStartToEndなんかはpropsで親から受け取る。これでまあうまく動く、ように見える。 なんだけど、外部から与えてるpropsの中身を変えてマーカーや線を再描画するとマーカーや線が残ることがあって、なんでなんだこれってだいぶ苦労した。

調査

いろいろ調べてると、Google Maps JavaScript APIのドキュメントにこんなのを見掛けた。

Removing Polylines  |  Maps JavaScript API  |  Google Developers

なんかこれがうまく呼べてないんだろうなってことで@react-google-maps/apiの実装を調べてみると、このコードを読む限りではthis.state.polyline.setMap(null);してるように見える。おっかしーな。 で、調査してみようと以下のように<Polyline />コンポーネントの呼び出しにonLoadonUnmountってフックがあったので、引数にstateに格納しているpolylineオブジェクトを受け取れる。これを以下のように指定して動かしてみた。

const onLoadHook = (line) => {
  console.log({message:"onLoad", line});
};
const onUnmountHook = (line) => {
  console.log({message:"onUnmount", line});
};

return (
  // ... 中略
        <Polyline
        key={"map-line-" + start.uuid + end.uuid}
        path={pathFromStartToEnd}
        onLoad={onLoadHook}
        onUnmount={onUnmountHook}
      />
  // ... 中略
);

そしたらonLoadは2回呼ばれてるのにonUnmountは一回しか呼ばれてないことがわかった。えー。Polylineコンポーネントが作り直されて1回ずつ呼ばれたのか、Polylineコンポーネントは1回だけ作られて2回mountされたのかはよくわかってないんだけど *2 。でもたぶん後者かなという気がする。以下の推論がうまくハマるから。

ひとつのコンポーネントが2回マウントされているとすると、Polylineオブジェクトが作られたあとcomponentDidMountが2回呼ばれてるってことで、それぞれ呼び出しの中でnew google.maps.Polyline({...})が行われてるから、実際の地図上には同じ線が2重に引かれてることになる。

どちらの呼び出しもsetStatepolylinenewしたオブジェクトを保存しているので、1回目の呼び出しでstateに保存されたPolylineオブジェクトは上書きされて消えてしまい、setMap(null);が呼ばれることがなくなってしまう、ということっぽい。

で、これはdevelopment mode的なやつでだけ起きてるんじゃないかなとnpm run buildしたものを手元で動かしてみた*3onLoadフックが1回しか呼ばれなかったので、Production buildすればこの症状は起きない。

が、まあちょっと手元の開発でこれ起きてるの無視するのはねえ……。

解決策

しょうがないので自分でpolyline.setMap(null);を確実に呼ぶようにする。以下のようにonLoadフック経由で対象オブジェクトを受け取り、再レンダリング前のクリーンナップ時に過去描画されたものに対してsetMap(null);を呼ぶ。

import { useEffect } from `react`;

const lines = [];
const onLoadHook = (line) => {
  lines.push(line);
};

useEffect(() => {
  return () => {
    lines.forEach((line) => {
      line.setMap(null);
    });
  };
});

return (
  // ... 中略
        <Polyline
        key={"map-line-" + start.uuid + end.uuid}
        path={pathFromStartToEnd}
        onLoad={onLoadHook}
        onUnmount={onUnmountHook}
      />
  // ... 中略
);

これでうまくいった。useEffectの使いかたがやっとちゃんとわかった気がする。

余談

はてなブログMarkdown書式、コードハイライトの形式にjsx指定しても無効なの悲しいね。

*1:最初はGoogleのオフィシャルのReact Wrapper使おうと思ったんだけど、細かいところどうやるかのドキュメントが何もないのとロードがうまく動かないのと、あれこれあって諦めて使うライブラリをスイッチしたら一発でできた。なんだよ。

*2:JavaScriptのObjectにもobject_idがあったら便利なのに……。

*3:これをHTTPS有効にしてやるのにまたひとハマりした、ああもう