地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【vim】vimiumを触ってみる

Chromeの操作をvimっぽくするための拡張プラグインです。結構vimってます。

インストール方法

https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb

上記サイトへアクセスしインストールするのみ

使い方

Command Description
? 使い方を説明してくれる
h 左へスクロール
j 下へスクロール
k 上へスクロール
l 右へスクロール
gg 一番上へスクロール
G 一眼下へスクロール
d 半ページ下へスクロール
u 半ページ上へスクロール
f 現在のタブでリンクを開く
F 新しいタブでリンクを開く
r 更新
gs view sourceを表示
yy URLをコピーする
yf リンク作のURLをコピーする
/ ファイル内検索

カスタム ショートカットキーを追加できます。

chromeの右上にvimiumボタンのoptionsをクリックする 個人的には左右スクロールは不要なのでブラウザの戻る/進むボタンを割り当て あとはタブキーもShift + H とShift + Lで移動できるように設定します。

# 戻る
map h goBack
# 進む
map l goForward
# タブ移動
map H previousTab
map L nextTab

特定のサイトでVimiumを止める方法

Vimiumを利用していると、特定のサイトのみ利用を停止したい場合があります。 その場合は、Vimium Optionsの「Excluded URLs and keys」に以下の形でURLを追加すればOKです。

感想

1時間くらい使ってみましたがまだ慣れないのでしばらく触りつつやってみようかなと思いました。ショートカットキーとかが自分で使ってるvimと設定が違かったりとちょっとカスタマイズしなきゃなという感じですね〜。

【Ruby】LinuxとRubyとCoW

hiboma.hatenadiary.jp

こちらのアンサー記事を書いてみますw

なぜ CoW の話が出てきたのか

なぜCoWを気にしたのかを話すとまず社内で動かしているAPIサーバがメモリ利用過多となってアラートがなったのが話のスタートでした。サービス自体はUnicornを使って動いています。

メモリ利用過多となったときに調べる手法は言語ごとに様々ですが私はRubyの経験がほぼなくプロファイラなども使ったことがありません。この状況でできることは/procを見るくらいしかなかったので/proc/$pid/smapsを見始めました。そこで始めたのがメモリ共有率の計算でした。

以下のツールを書いてUnicornの親子プロセスのメモリ共有率を確認するとなんと40~60%程度となっていて「え!?」ってなったのが始まりでした。プロセスの生存期間が長いのかと思ったのですが起動時間も出すと数分前に起動したプロセスも漏れなくそのくらいの共有率となっていました。mod_perlとかの世界で生きていたので「びっくり!」って感じでした 。この辺が話の始まりでした。

github.com

そこで颯爽と登場して疑問を解決してくれたのがhiboma (id:hiboma) さんと id:udzura さんのお二人でした。感謝!

どんなことをディスカッションしてたのか

そもそもRubyはCoWの恩恵をあんま受けないよねという話を聞きました。

techracho.bpsinc.jp

RubyVALUEとかRVALUEとか小さめのインスタンスをたくさん作っては消すのでglibcmallocとはあまり相性が良くないのではとのことでした。加えてjemallocを使うと良いかもという話も聞きました。

github.com

そこから発展してCoWが起こるときの特定のカーネル関数をトレースするにはどうすれば良いかという話になりその回答としてhiboma (id:hiboma)さんが検証記事を書いてくれました。自分じゃたどり着けないような解法で大変勉強になりました。検証コードやツールの理解に加えて当然ながらカーネルの知識なんかがあって初めてスタートラインでかつ説明をわかりやすく書いてるあたり「すごすぎる。。」という感想以外出ませんでした。いつかはその領域まで行ってみたいですね。。!!

今後

社内で動いているアプリケーションの多くはRubyで動いておりそこへの入門が最優先かなと思ってやっていきます。あとは並行してBPFに入門し、こういった視点からアプリケーション調査やサービスを支えれるようなエンジニアになりたいみたいなざっくり目標はあるのでそこへ進んでいく感じ行きたいなと思います!

感想

これまでこの辺の話を社内ですることってあまりなくて寂しさを感じていたりもしたので社内で話を出来るのはとてもとてもとても楽しく嬉しくてという思いでした。

また各レイヤごとにすごい人たちだらけの会社で議論のログなんかも全て閲覧可能だったりとめっちゃいい環境だなと改めて感じたりもしました。特にissue上でのやりとりなんかは「なぜこうなったか」みたいな思考過程とかまでまとまっていたりでいわゆる技術書からは学べない現場の知識を得られていると感じました。

転職して早2ヶ月なので3ヶ月目も頑張ろう〜〜

tech.pepabo.com

イベントもやってるらしいので是非〜〜

【memcached】 pymemcacheは障害が発生したmemcachedを自動で切り離す

まとめ

  • pinterest/pymemcacheは障害が発生したmemcachedを自動でリバランスして書き込みに行かないような機能を持っている
  • 特定ノード間だけで起きてたりするとデータの不整合が発生するかもしれないから注意が必要
  • memcachedのクライアントを選ぶときはこの辺気にする必要あるよねという学びを得た

以下からが本文

概要

おうちのラズパイは現在みんなで趣味プログラミング用のmemcacehdクラスタとして稼働しています。いろいろな言語で実装されているクライアントで使ってたのですがpinterest/pymemcacheというライブラリを使っていて「おや?」となったので調べて書いてみた記事です。

memcachedの分散

memcachedmemcached同士での通信は行わない分散方式を採用しています。memcached同士で通信を行ってデータのやり取りなどは行われません。複数台のmemcachedへのデータの分散は全てクライアント側の実装に依存するようになっています。Perlなら"Cache::Memcached::Fast"、PHPなら"Memcached::addServers"、Pythonなら"pinterest/pymemcache"といったライブラリがあります。今回は最後のPythonのクライアントを使った話です。

クライアントがノードを決定するには「サーバのリストとkeyを使用する」というのとそれぞれが同一ならノードはどのサーバで計算しても同じになるというのがポイントです。(詳しくは以下)

gihyo.jp

クライアントのサンプル実装

引数で受け取った文字列をkeyとしてHashClientのいずれかにsetしに行く実装です。サーバは2台用意していてそれぞれ正常に起動している場合はどんなkeyをsetしても全て成功します。

import sys
from pymemcache.client.hash import HashClient

client = HashClient([
    'memcached01:11211',
    'memcached02:11211',
])

def main():
    client.set(sys.argv[1], "test")

if __name__ == "__main__":
    main()

接続できないサーバを追加して擬似障害を発生させてみる

意図的に接続できないサーバを1台HashClientに追加した状態で実行してみます。すると以下のようにsocket.timeoutが発生することが確認できました。

Traceback (most recent call last):
  File "//memd_cli.py", line 14, in <module>
    main()
  File "//memd_cli.py", line 11, in main
    client.set(sys.argv[1], "test")
  File "/usr/local/lib/python3.9/site-packages/pymemcache/client/hash.py", line 361, in set
    return self._run_cmd('set', key, False, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/pymemcache/client/hash.py", line 336, in _run_cmd
    return self._safely_run_func(
  File "/usr/local/lib/python3.9/site-packages/pymemcache/client/hash.py", line 215, in _safely_run_func
    result = func(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/pymemcache/client/base.py", line 462, in set
    return self._store_cmd(b'set', {key: value}, expire, noreply,
  File "/usr/local/lib/python3.9/site-packages/pymemcache/client/base.py", line 1091, in _store_cmd
    self._connect()
  File "/usr/local/lib/python3.9/site-packages/pymemcache/client/base.py", line 420, in _connect
    sock.connect(sockaddr)
socket.timeout: timed out

この状態はいつまで続くのかを知りたく以下のようにloopさせつつsetするよう修正してみました。実行結果の予想としては永遠にfailedが出続けるのが期待値です(詳細は後述)

import time
import sys
from pymemcache.client.hash import HashClient

client = HashClient([
    'memcached01:11211',
    'memcached02:11211',],
    connect_timeout=1
    )

def main():
    while True:
        time.sleep(3)
        try:
            client.set(sys.argv[1], "test")
            print("set")
        except:
            print("failed")


if __name__ == "__main__":
    main()

上記の状態でmemcached02を停止させて実行!!すると実行途中からsetが成功するパスを通ってsetが出力されました。

failed
failed
failed
failed
set
set
set
(省略)

なぜ

pymemcache.readthedocs.io

This will use a consistent hashing algorithm to choose which server to set/get the values from. It will also automatically rebalance depending on if a server goes down.

ドキュメントには上記のように記載があった。サーバダウンしたらリバランスするよ的な機能を搭載しているとのこと。

詳細な実装はよくわからなかったのでソースを見てみます。具体的な処理は_mark_failed_server()というメソッドでconnect失敗回数を記憶して一定回数(オプションで指定可能)でそのサーバをmemcachedのノードリストから削除するような実装となっていることがわかりました。ちなみにretry_attemptsなどを初期化時に指定することで調整も可能になっているようです(ドキュメントにも書いてあった)。

github.com

クライアントがノードの障害を動的に検出すると起こりそうな問題

最初の方にも書いたがmemcachedクラスタはサーバ間でデータのやり取りをしない。もちろんクライアントも同様。で、ここでクライアントが複数いる構成で上記のように障害ノードを動的に外すと何が起こるかを考えると一番怖いのがデータの不整合です。不整合が起こりうるシナリオは以下

  • ① クライアント1とmemcached1の間のネットワークで障害が起きる
  • ② クライアント1はmemcached1をnodeリストから削除してコンシステントハッシュを使ってノードを決定する
  • ③ クライアント2とmemcached1の間では疎通が問題なくできる

こういう状況になってしまうとクライアント1は本来memcached1へ書き込みたいkeyをmemcached2へ書き込んでしまうような流れになってしまう。またpymemcacheはノードの復旧を検出してノードリストを書き換える仕組みも持っていてこちらもデフォルトでオンになっている。この復旧のタイミングではmemcached1とmemcached2にデータが新旧で存在しうることになってしまう。リアルタイム性を伴うキャッシュなんかだったら事故につながってしまいそうな話。

どうするのが良いのか

クライアントで機能を無効にできるなら無効にするのが良さそうかなと思っています。ただこの場合は本来memcached01に存在するべきデータはmemcachedのダウン中は一切参照も書き込みもできなくなる。これが例えばホットキーで大量のreadがある場合は全てが読み取り失敗かフォールバック処理がDBへ行くなら全てがDBへ直撃してしまう可能性があります。

ただこれもmemcachedを横にたくさん並べれば全体の数%がDBへいくだけとなるので大きく問題にはならなそう。全て直撃するとDBの負荷が気になるみたいな場合はそもそもキャッシュの戦略を変えた方が良さそうと思っています。(きちんとkeyが分散されてないる前提)

あとはキャッシュレイヤで不整合が起きても問題ないようなキャッシュ戦略を取るとかもありなのかなと思ったりもしました。(私が書いてたコードだとネガティブキャッシュとかの用途で使ってたのでこの不整合はもろに影響を受けてしまうので設計から怪しいというのにこの辺で気付きました)

前職だったらこれくらいしか案が思いつかなかっただろうけど今だとConsulというものを教えてもらったのでクライアント間で障害情報を共有することが容易になっている。例えば特定ノードで通信障害が起きたらその結果をConsulに伝えてtempleteからリストを生成してクライアントへシグナルを送信して再読み込みなんてこともできてしまう。

blog.nomadscafe.jp

他にもReplica Poolを用いたダブルライトパターンとかで複数memcachedに書き込んだりするのも手としてあるようです。この辺までやるとなるとRedisとかサーバ側でのレプリケーションを提供してくれるミドルウェアへの乗りかえの検討とかも必要そうですね。

mixi-developers.mixi.co.jp

【Redis】キャッシュスタンピード対策をPythonで実装してみる

背景

ryuichi1208.hateblo.jp

この前描いてた記事のスピンオフ的な記事でキャッシュスタンピード対策をPythonで実装するとしたらどんな感じかなと思って調べてみた記事。内容としては以下の記事でやっていることの一部を掘り下げて考えてみたという感じです。

techblog.zozo.com

検証環境

やりたいこととしては重い処理を行うWebサーバがあるような構成でそこへのリクエストがその前段のクライアントで絞れるかを実装するのがゴール。以下のような構成を作って検証を行う。

* サーバ -> Flaskを用いたアプリケーションサーバ。"/sleepで重い処理を再現"
* クライアント -> Python
* ロック用サーバ -> Redis: 6.3

検証用のソース

get_and_setという関数でロックの取得とバックエンドへのデータの取得/kvsへのデータの設定を行う。

#!/usr/bin/env python

import sys
import os
import redis
import requests
import time

r = redis.Redis(host='redis', port=6379, db=0)

def get_and_set(key: str):
    val = r.get(key)
    if val:
        return val.decode()

    # keyがnullの場合はこちらの処理に来る。ロック取得成功でバックエンドにリクエストを飛ばし、valueをsetする
    if r.set(f"lock_{key}", "1", ex=10, nx=True):
        print("get lock: pid=", os.getpid())
        val = ""
        try:
            val = requests.get("http://localhost:5000/sleep").text #激重処理
            r.set(key, val, ex=10)
        except:
            r.delete(f"lock_{key}") # バックエンドへの疎通失敗時もロックは解除
        return val

    print("didn't get lock: pid=", os.getpid())
    # keyが設定されるまでsleepしつつポーリング
    while val is None:
        val = r.get(key)
        time.sleep(0.2) # sleep時間がレスポンスタイムへのボトルネックにならないようなるべく短くする
    return val.decode()


def main(args):
    print(get_and_set(args[1]))


if __name__ == "__main__":
    main(sys.argv)

ポイントはこの部分でsetにオプションnx=Trueを加えることでkeyの存在確認を行なって存在しなければsetするようになる。また、exを設定することで例えばバックエンドへのリクエスト中のpythonで補足できない突然死が発生してもロックが自動的に解放される。

if r.set(f"lock_{key}", "1", ex=10, nx=True):

ちなみにRedisサーバが死んだとしてもttl自体はUnixTimestampで保存されているのでフェールオーバ先でEXPIREされるので無駄に長く待ちすぎるみたいなケースは発生しづらい。

EXPIREAT は EXPIRE と同じ効果と意味を持ちますが、TTL (生存期間)を表す秒数を指定する代わりに、Unixの絶対タイムスタンプ (January 1, 1970 からの秒数)を取ります。過去のタイムスタンプはキーを直ちに削除します。
参考: http://mogile.web.fc2.com/redis/commands/expireat.html

実験

クライアントを2プロセス起動させてみる。それぞれロック取得の成功可否で処理が分かれてうまくいっている!良かった!

# クライアント①
root@8bac3935d5ce:/# python redis_cli.py key
get lock: pid= 217
keys-py

# クライアント②
root@8bac3935d5ce:/# python redis_cli.py key
didn't get lock: pid= 236
keys-py

# バックエンドのログも1回しかきていないことを確認
flask_1  | 127.0.0.1 - - [28/Aug/2021 04:33:42] "GET /sleep HTTP/1.1" 200 -

負荷的な話を気にしてみる

    # keyが設定されるまでsleepしつつポーリング
    while val is None:
        val = r.get(key)
        time.sleep(0.2) # sleep時間がレスポンスタイムへのボトルネックにならないようなるべく短くする
    return val.decode()

負荷が気になるのはこの部分。ポーリングしてビジーウェイト的にロックを取得したプロセスがvalueを設定するまで待ち続ける。気になるポイントの1つはgetをひたすら回し続けることによるRedisサーバへの負荷で、もう一つはsleepで回し続けることによるクライアント側のCPU負荷。Redisが暇なサーバだったりプロセス数が少ないようなモデルのアプリケーションならそれぞれ無視することはできそう。

以下は待つ側のプロセスのstraceによるsystemcall呼び出しの回数。getも40回近くが1プロセスあたり呼び出すことになる。

root@8bac3935d5ce:/# strace -wc python redis_cli.py ddd
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 96.40    8.486300      202054        42           select
  0.77    0.067874          74       907        69 stat
  0.36    0.031764         352        90        44 recvfrom

どうするのが良いのか?

Redisが提供しているXADDやXREADを使うのがよさそうと思った。ストリームとしての使い方としてタイムアウト機能を備えたコマンドとなっている。以下のようにBLOCKにタイムアウト秒を指定することでクライアントはその時間までデータの到着を待つという仕組み。

XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]

参考: Redisストリームについて

待ち方としてはサーバにタイムアウトの時間を送信してクライアントはその時間になるまで以下でブロックする。Redisの場合はコネクション管理をサーバでやってるのでクライアントはとりあえず待っているだけでタイムアウトの処理も行われる。

ssize_t redisNetRead(redisContext *c, char *buf, size_t bufcap) {
    // 指定した時間を以下で待つ。タイムアウトはサーバ側で行う
    ssize_t nread = recv(c->fd, buf, bufcap, 0);
    if (nread == -1) {
        if ((errno == EWOULDBLOCK && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) {
            /* Try again later */
            return 0;
        } else if(errno == ETIMEDOUT && (c->flags & REDIS_BLOCK)) {
            /* especially in windows */
            __redisSetError(c, REDIS_ERR_TIMEOUT, "recv timeout");
            return -1;
        } else {
            __redisSetError(c, REDIS_ERR_IO, NULL);
            return -1;
        }
    } else if (nread == 0) {
        __redisSetError(c, REDIS_ERR_EOF, "Server closed the connection");
        return -1;
    // エラー以外はここに入ってない場合はnilが返る
    } else {
        return nread;
    }
}

github.com

straceするとこんな感じ。recvfrom(2)でほぼほぼ待つだけと言った結果になる。Redis側でのクエリ数を見ても増えていないので負荷が減っているはず。

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 98.73    3.122772     1561386         2           recvfrom
  0.43    0.013498         207        65           write
  0.20    0.006480         137        47           mmap
  0.09    0.002700         128        21           close
  0.07    0.002266         141        16           openat