diary

日記です

boxenを導入した話

OSXの環境設定を自動化出来るツール。puppetベースだけどchefこねくり回すよりは個人で使うには手軽で良かったです。 XCode入れればとりあえず動かせるし、our-boxenベースにすればrbenvとかnvm(nodenvもいける)とかhomebrewがなんか勝手に入る。便利。

OSXアプリケーションのインストールもAppStoreのもの以外はだいたい全部自動化出来ました。便利。

便利です。

手順

  • XCode(& Command Line Tools)を入れる
  • boxen用にディレクトリ掘る

    sudo mkdir -p /opt/boxen sudo chown $USER:admin /opt/boxen

  • our-boxen(boxenテンプレート) mkdir ~/src && cd ~/src && git clone git://github.com/boxen/our-boxen.git our-boxen

  • Puppetfileに使いたいpuppetのgemをモリモリ並べる
  • ~/src/our-boxen/modules/people/manifests/$USER.ppを書く
  • ~/src/our-boxen/script/boxen を実行

設定

うちで使ってるのは以下みたいな感じです

  • ~/src/our-boxen/modules/people/manifests/trapezoid.pp

      class people::trapezoid {
        #includes
        include skype
        include iterm2::stable
        include chrome
        include firefox
        include virtualbox
        include flux
        include rubymine
        include sourcetree
        include dropbox
        include sublime_text_2
        include ctags
        include java
    
        class { 'intellij':
          edition => 'ultimate',
        }
        #include osx
    
        package {
          [
            'tmux',
            'tig',
          ]:
        }
    
        package {
          'ForkLift':
            source   => "http://download.binarynights.com/ForkLift2.5.4.zip",
            provider => compressed_app;
          'Mou':
            source   => "http://mouapp.com/download/Mou.zip",
            provider => compressed_app;
          'GoogleJapaneseInput':
            source => "http://dl.google.com/japanese-ime/latest/GoogleJapaneseInput.dmg",
            provider => pkgdmg;
          'RemoteDesktopConnectionClient':
            source => "http://download.microsoft.com/download/C/F/0/CF0AE39A-3307-4D39-9D50-58E699C91B2F/RDC_2.1.1_ALL.dmg",
            provider => pkgdmg;
        }
    
        package {
          'zsh':
            install_options => [
              '--disable-etcdir'
            ]
        }
    
        file_line { 'add zsh to /etc/shells':
          path    => '/etc/shells',
          line    => "${boxen::config::homebrewdir}/bin/zsh",
          require => Package['zsh'],
          before  => Osx_chsh[$::luser];
        }
    
        osx_chsh { $::luser:
          shell   => "${boxen::config::homebrewdir}/bin/zsh";
        }
    
        $home     = "/Users/${::luser}"
        $src      = "${home}/src"
        $dotfiles = "${src}/dotfiles"
        $oh_my_zsh = "${home}/.oh-my-zsh"
        $oh_my_zsh_custom = "${home}/.oh-my-zsh-custom"
        $dust     = "${home}/.dust"
        file {$dust:
          ensure => directory
        }
        $dust_vim     = "${dust}/vim"
        file {$dust_vim:
          ensure => directory
        }
        $dust_vim_swap     = "${dust_vim}/swap"
        file {$dust_vim_swap:
          ensure => directory
        }
    
        $dust_vim_backup   = "${dust_vim}/backup"
        file {$dust_vim_backup:
          ensure => directory
        }
    
        repository { $dotfiles:
          source  => "trapezoid/dotfiles",
          require => File[$src]
        }
    
        exec { "ruby ${dotfiles}/symlink.rb":
          cwd => $dotfiles,
          creates => "${home}/.zshrc",
          require => Repository[$dotfiles],
        }
    
        repository { $oh_my_zsh_custom:
          source  => "trapezoid/oh-my-zsh-custom",
          require => File[$src]
        }
    
        $sublimetext2_packages = "${src}/sublimetext2-packages"
        repository { $sublimetext2_packages:
          source  => "trapezoid/sublimetext2_packages",
          require => File[$src]
        }
    
        exec { "ruby ${sublimetext2_packages}/symlink.rb":
          cwd => $sublimetext2_packages,
          creates => "${home}/Library/Application Support/Sublime Text 2/Installed Packages/Package Control.sublime-package",
          require => Repository[$sublimetext2_packages],
        }
      }
    
  • Puppetfile

      # ..省略
    
      # Optional/custom modules. There are tons available at
      # https://github.com/boxen.
    
    
      github "chrome",   "1.1.0"
      github "rubymine", "1.0.1"
      github "iterm2",   "1.0.2"
      github "firefox",  "1.0.5"
      github "skype",    "1.0.2"
      github "intellij", "1.1.3"
      github "vlc",      "1.0.1"
      github "flux",     "0.0.1"
      github "osx",      "1.0.0"
      github "ctags",    "1.0.0"
      github "dropbox",  "1.1.0"
      github "java",     "1.0.6"
      github "virtualbox",    "1.0.2"
      github "sourcetree",    "0.0.2"
      github "sublime_text_2","1.1.0"
    

redis 2.2を複数slave構成かつevictedありで運用してるとマジヤバイ話

結論

Redis 2.2使ってる奴はさっさとアップデートしろ

タイトルの条件で使用メモリがmaxmemoryに達するとキーがまるごと全部消し飛ぶ(evictedする)可能性があります。2.4ではとっく対策されているので、さっさと2.4以降、というかもうstableは2.6だし2.6にしろ

概要

わかりやすい全容(ぶっちゃけこっち見たほうが多分速い)。

だいたい下記のような感じのことが書いてあります

  • Redis 2.2でMaster-Slave構成(特に多数のslaveを接続している)を取っており
  • maxmemory-policyにnoeviction以外を指定している状態で
  • メモリ使用量がmaxmemoryに到達してキーのevictが発生し、
  • 格納されているキー1つあたりのの平均サイズが小さい (2013/04/23 21:47 紛らわしいので追記)

上記の条件で、maxmemory時のキー削除ロジックが暴走して、(ほぼ)すべてのキーがmasterから削除される

説明

まず前提として、

  • redisはキーを消すときにslaveに対して出力バッファ、AOF用のバッファにDELコマンドを書き足す
  • maxmemory到達時の押し出しに関しても、同様にふるまう

という事実があり、Redis 2.2ではmaxmemory到達時のキー削除に関しては色々省略するとまあだいたいこんなかんじで実装されていて、何らかのコマンドが走る度にチェックされる。

while (server.maxmemory && zmalloc_used_memory() > server.maxmemory) {

    /* 
     * maxmemoryポリシーに従って消すべきbestkeyを探すコードが
     * モリモリ書いてあったりするけど今回は全く関係ないオブジイヤーなので中略
     */

    /* Finally remove the selected key. */
    if (bestkey) {
        robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
        propagteExpire(db,keyobj);
        dbDelete(db,keyobj);
        server.stat_evictedkeys++;
        decrRefCount(keyobj);
        freed++;
    }

    /* だるいので省略 */
}

要するに消したいものを探して、メモリ消費量がmaxmemoryを下回るまで消しまくる。

中で何をしているかというと、実際にdbからキーを消してるのはdbDeleteなんですが、消す前にpropagteExpireの呼び出しによって各DELコマンドの発行とそれの接続中の各スレーブへのレプリケーション、そしてAOFへの追記のための出力バッファへの書き出しが行われています。

この時、あくまで出力バッファにそれぞれ書き出されるだけで、その場では実際にAOFへの追記やslaveへのレプリケーションは行われない。また、接続中の各slave毎に出力バッファは確保されます。

このため、

  • propagteExpireはメモリ解放ロジック内において(少なくとも、イベントループ側に処理を返すまで)は、出力バッファ分のメモリを専有する
  • 専有する出力バッファは、接続中のslaveの数に比例して増大する

加えて、出力バッファもdbと同じくzmalloc(redisの厨二ネーミングなmalloc。名前に反して確保した容量を管理する程度のことしか自らはしていない、薄いwrapper)で確保している為、

  • 出力バッファの分のメモリ消費も上記コードのzmalloc_used_memoryの返り値に加算される

すると、

  • dbDeleteで開放される容量 < replication,aof用の出力バッファが専有する量

となった時、zmalloc_used_memoryの返り値は解放前後で減るどころか増えるようになり、この解放ロジックはキーを全て捨て去るまで止まらない勢いで走り続けるようになります。

でかい容量を食っているキーが偶然削除されれば、運が良ければそこで止まるかもしれないですが、まあ大体ダメであろうと思われます。大人しく2.6を使え

対策は実行時に確保されてる出力バッファぶんをmaxmemoryから差っ引いた上で、dbDeleteで実際に確保されるメモリ量が目標解放量に達するまでキーを削除し続ける、というロジックに変更することでなされたみたいです。

最後に

なんか成り行きで調べてたらちょっと面白くなってきたのでまとめてみましたが、もう解決してるので2.2使わなきゃ微塵も問題ないです。

とりあえず2.6使っときましょう

Readable Git

gitのブランチ名には実は日本語が使えます

   
➜  ~ git:(develop) ✗ git branch feature/びっくりするほどウルトラ凄い機能 develop 
➜  ~ git:(develop) ✗ git branch
  feature/びっくりするほどウルトラ凄い機能
* develop
  master

これでどんな各トピックブランチがどんな機能なのか一目瞭然な気がしますね。

使える事を知ったはいいけどいかにも面倒臭い問題起きそうで運用してないので、誰か試してみて下さい。

master-slave構成のredisでttl(expire)を持つキーを使う方法

いろいろあってめんどい。

  1. ttlを指定したキーの実削除は、キーの参照があった際又は100ms毎に行われるttlを持つ全キーからのランダムルックアップによる検査により行われる
  2. 故に、短いttlを持つキーが多数存在する場合には、(実際に参照しない限り)実削除が間に合わなくなる(意図した時間に揮発しない)事がある

これだけならまあ参照すればいいんですが

  1. master-slave構成時、slaveから見たキーの削除はmaster側からの削除命令が無ければ行わず、ttl < 0となったキーに関してもこれは同様に扱われる
  2. 故に、揮発されるべきキーがslaveから参照されても、実削除は行われず、(master側の定期実削除が間に合っていない場合)slaveはそのまま揮発されているべきキーの値を返してしまう

対応策は

  1. masterに対してgetを投げる
    master-slaveとは何だったのか…
  2. redis.hのREDIS_EXPIRELOOKUPS_PER_CRONを増やす
    100msごとにルックアップするキーの数を増やす。
    揮発されるまでの時間は短くなるがCPU負荷は上がる。
    redisの再コンパイルも必要だし、そもそも根本的な解決にはならない
  3. slaveからのキー参照時に、必ずttlコマンドによる生存時間確認を行う
    実削除はされなくても、該当キーのttlは(unixtimeで時間を持っている為)正常にカウントダウンされる。
    このため、この現象が起こってしまったキーに対してttlコマンドを打つと0-1を返す。
    これを利用して論理的にそのキーがexpireしたことを確認する。
    ttlを使わないキーもttl=0ttl=-1であることを留意しないと辛いことになる。
  4. ttl使うキーを減らすか無くす、又はそのような用途にredisを使わない

まああんまりttlに期待しない設計にするのが一番よいです。memcachedのslab程あんまり断片化に気を使わないでもいいしそこそこ速いしレプリケーション出来るしmemcached代替として使っちゃいたくなるけどまあ落とし穴はありますよ、と。

redisはコード平易で読みやすくていいですねというお話でした(?)

2013/3/13 訂正:

正しくはttl=0じゃなくて-1です。ただ、expireしたのもttlを持たないのも両方-1返すんで、区別出来ないのには変わりなし