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

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

【k8s】VPAの概要に触れる

HPAの親戚的な機能のVPA。HPAが水平スケールならVPAは垂直にPODをさせます。

ccvanishing.hateblo.jp

HPAとVPAの違い

動的スケーリング種別 増減対象 主な用途
HPA 水平 Pod数 Webサーバ
VPA 垂直 CPU/メモリ DBサーバ

冒頭でも述べたとおりHPAとVPAではそれぞれ役割が違う。例えばスケールアウトが性能を伸ばすのに効率的な用途ならHPAを使うしスケールアップが効率的ならVPAを使う。それぞれどっちが良いみたいなのはなくて使い分けていくイメージ

yaml

apiVersion: "poc.autoscaling.k8s.io/v1alpha1"
kind: VerticalPodAutoscaler
metadata:
  name: my-app-vpa
spec:
  selector:
    matchLabels:
      app: my-app
  updatePolicy:
    updateMode: Auto
  resourcePolicy:
    containerPolicies:
    - containerName: my-app
      mode: Auto
      minAllowed:
        cpu: 200m
        memory: 100Mi
      maxAllowed:
        cpu: 1000m
        memory: 500Mi

【Perl】配列の重複要素を削除する

CIで特定条件のファイルを抽出してテストを行うみたいな実装をしたかった際に配列にファイルパスを突っ込んでたら重複した要素を持つ配列になってしまった。

my @array = ( 'perl', 'php', 'java', 'java', 'php' );
my %count;
@array = grep { !$count{$_}++ } @array;
foreach( @array ) { 
    print $_ . "\n"
}

map使うやり方ととかもあるらしくて色々あるっぽいさすがperlだ。

ameblo.jp

【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

【k8s】etcdctlに入門する

概要

Kubernetesのリソースの永続化に使われる高信頼分散KVSのetcd。k8sを学習していくに当たって重要コンポーネントなはずだけど特に単体で情報が乗ってることってあんまなかったので興味本位で調べてみた。

k8sを使った機能自体の拡張とかそのレベルで使いこなしてる人たちがはこの辺の知識は必須なんだろうけど入門したてなので正直理解甘いかもです。

1. etcdのデータベース

etcdのデータベースは、/var/lib/etcd(デフォルト)配下に格納される。 下記2つのサブディレクトリから構成される。walとsnapは別の場所に置くことが可能。

ディレクトリの名前 用途
wal(write ahead log) write ahead log とは???
snap snapshotsが保存される。snapshotsとは何?
[root@master1 ~]# etcdctl --version
etcdctl version 2.3.7

4. etcdctlコマンドの使い方

4.1 ヘルプの使い方

# etcdctlコマンドの後に-hオプションを付ける。
[root@master1 ~]# etcdctl -h
NAME:
   etcdctl - A simple command line client for etcd.

USAGE:
   etcdctl [global options] command [command options] [arguments...]

VERSION:
   2.3.7

COMMANDS:
   backup               backup an etcd directory
   cluster-health       check the health of the etcd cluster
   mk                   make a new key with a given value
   mkdir                make a new directory
   rm                   remove a key or a directory
   rmdir                removes the key if it is an empty directory or a key-value pair
   get                  retrieve the value of a key
   ls                   retrieve a directory
   set                  set the value of a key
   setdir               create a new directory or update an existing directory TTL
   update               update an existing key with a given value
   updatedir            update an existing directory
   watch                watch a key for changes
   exec-watch           watch a key for changes and exec an executable
   member               member add, remove and list subcommands
   import               import a snapshot to a cluster
   user                 user add, grant and revoke subcommands
   role                 role add, grant and revoke subcommands
   auth                 overall auth controls
   help, h              Shows a list of commands or help for one command

サブコマンドのヘルプの使い方

[root@master1 ~]# etcdctl ls -h
NAME:
   etcdctl ls - retrieve a directory

USAGE:
   etcdctl ls [command options] [key]

OPTIONS:
   --sort       returns result in sorted order
   --recursive  returns all key names recursively for the given path
   -p           append slash (/) to directories
   --quorum     require quorum for get request

[root@master1 ~]# etcdctl backup -h
NAME:
   etcdctl backup - backup an etcd directory

USAGE:
   etcdctl backup [command options]

OPTIONS:
   --data-dir           Path to the etcd data dir
   --wal-dir            Path to the etcd wal dir
   --backup-dir         Path to the backup dir
   --backup-wal-dir     Path to the backup wal dir
   --keep-cluster-id    Do not rewrite the cluster id
   --node-id            Use custom node id instead of a random value

ディレクトリの作成

[root@master1 ~]# etcdctl ls
/scheduler
/registry
/controller

# /a/bというディレクトリを作る(mkdir)
[root@master1 ~]# etcdctl mkdir /a/b

# /a配下にbというディレクトリができたことがわかる。
[root@master1 ~]# etcdctl ls /a
/a/b

キー&バリューの設定

# 作成したディレクトリ(/a/b)にos=centos6を設定する。
[root@master1 ~]# etcdctl set /a/b/os centos6
centos6
[root@master1 ~]# etcdctl get /a/b/os
centos6

作成したディレクトリ(/a/b)car=GT-Rを設定する。
[root@master1 ~]# etcdctl set /a/b/car GT-R
GT-R
[root@master1 ~]# etcdctl get /a/b/car
GT-R

ディレクトリ(/a/b)配下を表示する。
[root@master1 ~]# etcdctl ls /a/b
/a/b/os
/a/b/car

/a配下を再帰的に表示する。
[root@master1 ~]# etcdctl ls /a --recursive
/a/b
/a/b/os
/a/b/car

master2でもディレクトリ(/a/b)配下を確認する。master1と同じ結果が表示された。
[root@master2 ~]# etcdctl ls /a/b
/a/b/os
/a/b/car

4.5 ディレクトリ、キー&バリューの削除

master2でos=centos6を削除する。
[root@master2 ~]# etcdctl rm /a/b/os
PrevNode.Value: centos6

削除できたかどうか確認する。os=centos6が削除できた。
[root@master2 ~]# etcdctl get /a/b/os
Error:  100: Key not found (/a/b/os) [888]

car=GT-Rを削除する。
[root@master2 ~]# etcdctl rm /a/b/car
PrevNode.Value: GT-R

削除できたかどうか確認する。car=GT-Rが削除できた。
[root@master2 ~]# etcdctl get /a/b/car
Error:  100: Key not found (/a/b/car) [902]

os=centos6,car=GT-Rが両方とも削除できた。
[root@master2 ~]# etcdctl ls /a/b
[root@master2 ~]#

bディレクトリを削除する。
[root@master2 ~]# etcdctl rmdir /a/b

a配下のbディレクトリが削除できた。
[root@master2 ~]# etcdctl ls /a
[root@master2 ~]#

aディレクトリも削除する。
[root@master2 ~]# etcdctl rmdir /a

aディレクトリが削除できた。
[root@master2 ~]# etcdctl ls
/scheduler
/registry
/controller

4.6 指定した値(value)にアップデート(updateオプション)する。

"os"というキーに"centos6"という値を設定する。
[root@master1 ~]# etcdctl set /a/b/os centos6
centos6

設定した値を確認する。
[root@master1 ~]# etcdctl get /a/b/os
centos6

osをcentos7にアップデートする。
[root@master1 ~]# etcdctl update /a/b/os centos7
centos7

設定した値を確認する。指定した値にアップデートできたことがわかる。
[root@master1 ~]# etcdctl get /a/b/os
centos7

4.7 ディレクトリを再帰的(recursiveオプション)に削除する。

[root@master1 ~]# etcdctl mkdir /a/b/c
[root@master1 ~]# etcdctl ls /a --recursive
/a/b
/a/b/c
[root@master1 ~]# etcdctl rm /a --recursive
[root@master1 ~]# etcdctl ls /a --recursive
Error:  100: Key not found (/a) [1367]
[root@master1 ~]#

4.8 ディレクトリに寿命(ttlオプション)を設定する。

10秒間だけ存在するディレクトリを作成する。
[root@master1 ~]# etcdctl mkdir /a --ttl 10;date
2016年 11月 15日 火曜日 21:00:57 JST

[root@master1 ~]# etcdctl ls;date
/registry
/controller
/a
/scheduler
2016年 11月 15日 火曜日 21:00:59 JST

-中略-

[root@master1 ~]# etcdctl ls;date
/scheduler
/registry
/controller
/a
2016年 11月 15日 火曜日 21:01:07 JST

12秒経過したときには、/aディレクトリは消えている。
[root@master1 ~]# etcdctl ls;date
/scheduler
/registry
/controller
2016年 11月 15日 火曜日 21:01:09 JST

4.9 キーに設定された値の変化(watchオプション)を監視する。

osにcentos6を設定する。
[root@master1 ~]# etcdctl mkdir /a/b
[root@master1 ~]# etcdctl set /a/b/os centos6
centos6

watchオプションで値の変化を監視する。
[root@master1 ~]# etcdctl watch /a/b/os

master2でosにcentos7を設定する。
[root@master2 ~]# etcdctl set /a/b/os centos7
centos7

osの値が変化したことで、etcdctlコマンドのブロックが解除された。
[root@master1 ~]# etcdctl watch /a/b/os
centos7

4.10 データベースをバックアップする。

データベースのディレクトリ(--data-dir)を確認する。
[root@master1 ~]# ps aux|grep etcd
etcd       1024  7.8  2.2  40112 22232 ?        Ssl  19:36   9:17 /usr/bin/etcd --name=master1 --data-dir=/var/lib/etcd/default.etcd --listen-client-urls=http://0.0.0.0:2379

バックアップ用のディレクトリを作成する。
[root@master1 ~]# mkdir /backup

データベースを/backup配下にバックアップする。
[root@master1 ~]# etcdctl backup --data-dir=/var/lib/etcd/default.etcd --backup-dir=/backup

バックアップしたデータベースを確認する。
[root@master1 ~]# ls -R /backup/
/backup/:
member

/backup/member:
snap  wal

/backup/member/snap:

/backup/member/wal:
0000000000000000-0000000000000000.wal

4.11 データベース(walのみ)をバックアップする。

バックアップ用のディレクトリを作成する。
[root@master1 ~]# mkdir /backup

データベースを/backup配下にバックアップする。
[root@master1 ~]# etcdctl backup --wal-dir=/var/lib/etcd/default.etcd/member/wal --backup-wal-dir=/backup

バックアップしたデータベースを確認する。
[root@master1 ~]# ls -R /backup/
/backup/:
0000000000000000-0000000000000000.wal

4.12 コマンド実行時の詳細情報(--debug)を出力する。

[root@master1 ~]# etcdctl --debug ls
start to sync cluster using endpoints(http://127.0.0.1:2379,http://127.0.0.1:4001)
cURL Command: curl -X GET http://127.0.0.1:2379/v2/members
got endpoints(http://192.168.0.20:2379,http://192.168.0.10:2379) after sync
Cluster-Endpoints: http://192.168.0.20:2379, http://192.168.0.10:2379
cURL Command: curl -X GET http://192.168.0.20:2379/v2/keys/?quorum=false&recursive=false&sorted=false
/registry
[root@master1 ~]#

4.13 ディレクトリに"/"を付けてわかりやすく表示する(-p)。

オプション(-p)を付けずに実行する。ディレクトリがどれだかわからない。
[root@master1 ~]# etcdctl ls --recursive
/registry
-中略-
/registry/minions
/registry/minions/master2
/registry/minions/master1

オプション(-p)を使うと、minionsはディレクトリだとわかる。master1,master2はディレクトリ内のキーだとわかる。
[root@master1 ~]# etcdctl ls -p --recursive
/registry/
-中略-
/registry/minions/
/registry/minions/master2
/registry/minions/master1

HTTPでアクセスする。

# ディレクトリを表示する。現在は、/registryのみ存在する。
[root@master1 ~]# etcdctl ls
/registry

# /直下にOS=linuxを設定する。
[root@master1 ~]# curl http://localhost:2379/v2/keys/os -XPUT -d "value=linux"
{"action":"set","node":{"key":"/os","value":"linux","modifiedIndex":8815,"createdIndex":8815}}

# etcdctlコマンドでキーの値を表示する。
[root@master1 ~]# etcdctl ls
/os
/registry

# etcdctlコマンドでバリューの値を表示する。osにlinuxが設定されていることが確認できる。
[root@master1 ~]# etcdctl get /os
linux

# HTTPでosに設定されているバリューを読みだす。etcdctlコマンドと同じ結果が得られた。
[root@master1 ~]# curl http://localhost:2379/v2/keys/os -XGET
{"action":"get","node":{"key":"/os","value":"linux","modifiedIndex":8893,"createdIndex":8893}}

# キーを削除する。
[root@master1 ~]# curl http://localhost:2379/v2/keys/os -XDELETE
{"action":"delete","node":{"key":"/os","modifiedIndex":8962,"createdIndex":8893},"prevNode":{"key":"/os","value":"linux","modifiedIndex":8893,"createdIndex":8893}}

[root@master1 ~]# etcdctl ls
/registry

参考

www.redhat.com