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

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

【Perl】usageの書き方メモ

use Getopt::Long;
use Data::Validator;

my $tweet_id;
my $print_key;
my $help;
GetOptions('i=i' => \$tweet_id, 'p=s' => \$print_key, h => \$help);
if ($help) {
    die show_help();
}
validate( tweet_id => $tweet_id, print_key => $print_key );

sub validate {
    eval {
        state $rule = Data::Validator->new(
            tweet_id  => { isa => 'Int' },
            print_key => { isa => 'Str' },
        );
        my $args = $rule->validate(@_);
    };
    if ($@) {
        die show_help();
        exit;
    }
}

sub show_help {
    my $help_doc = <<EOF;
    get retweet info script
    Usage:
        perl $0 [options]
    Options:
        -i : tweet_id (Int)
        -p : print_key (Str)
            ex )
                id, screen_name, location
                https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-show            
        -h : help
    Author
        okkun_sh <okkun.sh\@gmail.com> (\@okkun_sh on Twitter)
EOF
    return $help_doc;
}

【memcached】スピンロックでatomic操作を実現するためには

概要

マルチプロセス環境配下における、同一レコードへの書き込みが大量に発生するwrite heabyな環境でmemcachedの更新をどうやってatomicに行うかを考えた時に出てきたスピンロック的なことやってみた記事。

memcachedとは書いたもののCAS操作を提供しているミドルウェアなら当てはまるしロック機構をクライアントで頑張ってるアプリケーションでも似たような話になると思う。

想定シナリオ

ユーザアクセスなり何かしらの動作が発生するたびに何かをインクリメントしたいケースがあったとしてcasを用いてある程度の原子性を持たせて実装しようとすると以下のようになる。

$ret = $mem->gets('test');
($cas, $val) = @$ret;
$val++;
$mem->cas('test', $cas, $val));

ここで問題となるのは以下が失敗する可能性のある命令であるということ。例えばこのコードを同時に並列で動かしている場合にcas値を使った更新では一番先に更新を行ったプロセス以外は全て失敗となって何もしないということになる

$mem->cas('test', $cas, $val));

さくらインターネットさんのサンプル含めこの操作自体について調べていると以下のようにリトライすれば問題ないと言った記事があった。ざっくりいうと更新が成功するまでredoでループを繰り返していくというもの。マルチプロセスで動いている場合でも更新が成功するまでcas値の取得->更新を繰り返すのでインクリメントは必ず行われるよねと言った話。

foreach (1 .. $num) {
    $ret = $mem->gets('test');
    ($cas, $val) = @$ret;
    $val++;
    unless ($mem->cas('test', $cas, $val)) {
        warn "$val update failed. retrying.\n";
        redo;
    }
}

research.sakura.ad.jp

ここで面白いなと思ったのがこの「更新が成功するまでredoを行う」という部分。(memcachedのCAS値の確認をロックと見立てれば) これはLinuxカーネルなんかでもよく使われるスピンロックに似ている!と思った。

ちなみにここではincrを例にしてますがmemcahced自体に備わっているincr命令はatomicでこっちが使えるケースなら今回取り上げる話は正直不要な話になります。

www.w3big.com

スピンロックとは

スピンロックとは共有リソースが2つ以上のプロセスによって同時に変更されるのを防ぐ方法です。複数プロセスが同時に同じリソースに更新を行おうとした際に最初のプロセスがロックを取得しレコードに関する更新権を得た状態になった際に、以降のプロセスはロックを取得できない状態となります。

この時に2つ目以降のプロセスはロックを獲得するために以下の動作をとることができます。

  • ループしてロック獲得の処理
  • sleepしてロック獲得の処理
  • イベント発生をOS側に通知してもらうために依頼

ここで取り上げるのが「ループしてロック獲得の処理」これが一般的にいうスピンロックとなる。ループで何もしないという動作でロック獲得を待ち獲得できたら処理を行うという流れです。Linuxだとハードウェア関連の操作を行う際にこの辺はよく出てきたりします。

github.com

ちなみにこれ自体はLinux以外のOSでも利用されている,一般的な排他制御機構となります。実装例とかは基本linux以外で使ってるケースをあまり見ないので使う機会があまり無いのかもしれません。(ここはよくわからない)

スピンロックのメリット/デメリットは

スレッドが休止状態にならない。と言うのがメリットです。memcachedの上記のサンプルで言うならこのコード自体はLinuxのスケジューラが意図的に実行を取り上げない限りは実行可能状態で継続します。この場合CPUリソースはプロセスが放さないのでコンテキストスイッチが発生しない分高速にロックを獲得することができます。

デメリットはそのままでスレッドが休止状態にならないのでCPUリソースを食い続けます。また、スピンロックを保持したスレッドがCPU待ち状態になると、このスレッドが再度スケジューリングされるまで、スピンロックが解放されません。このため、スピンロック待ちの頻度が高くなりアプリケーション性能は劣化したりします。

そしてここで挙げたメリットであるコンテキストスイッチが発生しないのはスケジューラのプリエンプトが指定できるカーネル空間での話でユーザ空間として実装しようとしている今回のアプリケーションでは効果がだいぶ薄まります。

stackoverflow.com

preempt_disableをカーネルはどこでやってるのか

カーネルスペースで実行されている場合、スピンロックを取得すると、実際にプリエンプションが無効になりますみたいな話を上で書いたけど結局どうやってんねんって思ったので調べた。

ざっくり言うとcurrent_thread_info()->preempt_disable_countをインクリメントしてプリエンプションを抑止

void preempt_disable(void)
{
    BUG_ON(preempt_disable_count < 0 || preempt_disable_count == INT_MAX);

    if (preempt_disable_count++)
        return;

    thread_cpu_id = nondet_int();
    assume(thread_cpu_id >= 0);
    assume(thread_cpu_id < NR_CPUS);
    lock_impl_lock(&cpu_preemption_locks[thread_cpu_id]);
}

elixir.bootlin.com

スピンロックの実装自体はこんな感じで書かれています。ロックを取得するまでループすると言った流れを取っているのがわかります。(プリエンプションの無効化なんかはここではなく呼び出し元でやってるのでしょうか。この辺はよくわからなかった。)。なんとなくでおった際のコメント入れてみました。

static __always_inline void arch_spin_lock(arch_spinlock_t *lock)
{
    register struct __raw_tickets inc = { .tail = TICKET_LOCK_INC };

    inc = xadd(&lock->tickets, inc);
    if (likely(inc.head == inc.tail))
        goto out;

    for (;;) {
        unsigned count = SPIN_THRESHOLD;

        // inc.tail(元々)とinc.head(最新)を比較(__tickets_equal)
        do { // 一致するまでSPIN_THRESHOLD回繰り返す
            inc.head = READ_ONCE(lock->tickets.head);
            if (__tickets_equal(inc.head, inc.tail))
                goto clear_slowpath;  // 一致したら(goto clear_slowpath)で後処理
            cpu_relax();
        } while (--count);
        __ticket_lock_spinning(lock, inc.tail);
    }
clear_slowpath:
    __ticket_check_and_clear_slowpath(lock, inc.head);  // tailに1を加算し前の値を記録してロック操作を完了する
out:
    barrier();  /* make sure nothing creeps before the lock is taken */
}

感想

スピンロックはユーザ空間で実装するのはメリットが少なそうなので素直にblockingなりで実装しよう。ロック期間が十分に小さい場合でも結局スケジューリング次第では期間が短いかの測定も難しそうだし。

ただユーザ空間で動くpthreadにspinlockなるものを見つけたのでいつかこれは試してみたい。(Cで今後がっつり開発する機会はないはずだけど。。)

int  pthread_spin_destroy(pthread_spinlock_t *lock);
#include <pthread.h>

pthread_spinlock_t lock;
int ret;

ret = pthread_spin_destroy(&lock); /* spinlock is destroyed */

###

int  pthread_spin_trylock(pthread_spinlock_t *lock);
#include <pthread.h>

pthread_spinlock_t lock;
int ret;

ret = pthread_spin_trylock(&lock); /* try to lock the spin lock */

ざっとみた感じpthreadでspinlockをやるにはCで書く必要がありそうな感じだしpythonやらから呼ぶのも大変そうなのでロック獲得をユーザ空間でやる場合にスピンロック使おうってケースはまあほぼなさそう。ポスグレの参考記事なんかも出てきたけどあれはあれでどうやって実装してるんだろう。

amachang.hatenablog.com

【Ansible】collection インストールでバージョン指定

docs.ansible.com

バージョン指定してインストール

$ ansible-galaxy collection install test:1.0

開発バージョンをインストール

$ ansible-galaxy collection install test:==0.0.3-alpha

ディレクトリを指定してインストール

$ ansible-galaxy collection install my_namespace-my_collection-1.0.0.tar.gz -p ./collections

【Ansible】Ansibleにおける冪等性とは

概要

仕事でひたすらplaybook書いてたら「冪等生ってなんだ?」ってなったので調べたメモ。

そもそも冪等性とは

同じ操作を何度繰り返しても、同じ結果が得られるという性質です。以下あたりが詳しく書かれてます。

qiita.com

よくある冪等生の説明の数式ですが冪等生がある関数fは何回実行しても結果が同一という性質があります。例えば絶対値を求めるabs()なんかがこの性質を持った関数です。

f() = f(f())

冪等性がない関数の場合は上記の式は成り立ちません。上の記事にあるようなsqrt()なんかがそうです。

sqrt(16) ≠ sqrt(sqrt(16))

Ansibleにおける冪等性の話

Ansibleといえば冪等性の話題が必ずと言っていいほど挙げられます。Ansibleによる冪等性はサーバの状態を保つことです。ここでポイントになるのがサーバの状態を保つとはどういうことなのかという点です。

Ansibleの公式のモジュールは冪等性を保つような実装をされていますが厳密な意味で上記で挙げたような冪等性を保つような実装はなされていません。以下がその例です。

  tasks:
    - name: yum
      yum:
        name: httpd
        state: installed

上記のtaskは実際何回実行してもhttpdがインストールされた状態というのは保たれます。これは一見して冪等性を保っている実装に見えますが実際にはyumを実行するとことでyum独自のdbファイルへのアクセスや書き込みが裏では行われます。

アクセスが発生するだけで困るケースやモジュールの先で使うようなログファイルが変更されるのが困るケースがあるかないかはいったん置いておきます。

ここで言いたいのはあくまでもAnsibleが実行するtasksの冪等性は完全なものではなくモジュールの実装次第で冪等性のカバーする範囲が変わってしまうという点です。

f() = f(f())

Ansibleがいう冪等性は上記を満たすものではなく関数に特定範囲の値を取りうるパラメータeを与えた場合に冪等生が成り立つ以下の式となる。todo: 式考える (eは「httpdがインストールされている状態」のような値を取りうる)

f(x, e) = f(f(x, e), e)

Ansibleに置いていえばこのサーバの状態が違う場合は冪等性という性質は成り立たなくなる。例えば特定のディレクトリAが存在する状態をe1。存在しない状態をe2としたらそれぞれのタスクは同一の結果を得られないような場合があります。

f(x, e1) = f(x, e2)

モジュールないで何かをビルドしたりするケースやOS設定時のパラメータファイルの存在有無のタスクなんかがこれに当てはまります。

httpにおける冪等性

冪等性の記事でhttpのgetとかdeleteとかも割とよく見かけます。httpリクエストの場合はRFCでも言及されていて割と参考になったりしたので書いておきます。

Like the definition of safe, the idempotent property only applies to what has been requested by the user; 
a server is free to log each request separately, retain a revision control history, 
or implement other non-idempotent side effects for each idempotent request.

google翻訳する

セーフの定義と同様に、べき等プロパティはユーザーが要求したものにのみ適用されます。
サーバーは、各要求を個別にログに記録したり、リビジョン管理履歴を保持したり、
各べき等要求に対して他の非べき等の副作用を実装したりすることができます。

ざっくりまとめるとユーザが要求したものについては冪等性を担保するけどその先でサーバがなんかするのは担保しないよというもの。Ansibleでも似たような性質が見れるのでこれがしっくりきた。

Ansibleの場合はさっきあげたタスクを例にすると「httpdをインストールされた状態は保つけどyumが使うDBやその他システムファイルは関与しないよ」と言った考えになります。

この辺はissueとかでも議論されていて見解としては全てのモジュールが冪等じゃないしこの辺は今後も厳密にやってく気はないと言った感じで決着がついていた。

github.com

whenで冪等性

ansible-lintでshellモジュールを使うとwhenなしで使うと怒られる。これは上のAnsibleの思想(というかhttpの冪等性の思想?)によるものなんじゃないかと思う。

shellモジュールは特性上ユーザが自由にスクリプトを書いて実行できるので冪等性のない書き方をひたすらやって実行の都度chengedステータスが出るようになってしまう。

Ansibleの冪等性は状態というパラメータを持つ必要があるのでshellモジュールでこの状態を表す方法としてwhenが必要と言っているのかなって思った。