t-suzuki 雑記ノート

主に技術の話

はじめてnodejsを使った。はじめてbotを作った話。

これは何

nodejsを使ってslack botを作った話。 インフラエンジニアだけどコードもかけるようになりたい、といって少しだけ時間をもらってbotを作る仕事をやった。 その時のメモ的なもの。

何を使う

botkitというツールキットを使う。 hubotと何が違うの? そういうのを調べたりはしてません。お手軽にモダンな感じで作れるよ、とおすすめされたので使った見た感じ。

コード

コードは長くないのでここに貼ってしまう。
npm install --save botkit とかして、package.json作らせた(と思う)。
channel id はチャンネルがprivateの場合はhttps://api.slack.com/methods/groups.list/test で確認する。publicの場合は /channels.list/test で確認する。slackの場合。facebook messanger とかだとまた別の取得方法だと思う。
token はbot のものを使用する。https://dwango.slack.com/apps/new/bot から作成したもの。custom botについてはこちらを参照のこと:https://api.slack.com/bot-users

const Botkit = require('botkit');
const os = require('os');

// 色々変数チェック。他dest,originについてはここの説明では略
if (!process.env.token) {
  console.warn('Error: Specify token in environment');
  process.exit(1);
}

const controller = Botkit.slackbot({
  debug: true,
});

controller.spawn({
  token: process.env.token,
}).startRTM();

const destChannel = process.env.destChannel;
const originChannel = process.env.originChannel;

controller.hears('.*https?://www.example.com[sS]*', 'ambient', (bot, message) => {  // [sS]* は改行含む全一致。.*だと改行入らない。ambient はbotが全発言を拾うということ(direct_messageとかもある)。 
  // 指定のチャンネルのときのみ発言を拾う
  if (message.channel === originChannel) {
    const matchMessage = message.text;
    const userId = message.user;
    bot.api.users.info({ user: userId }, (userApiError, userApiRes) => {
      const userName = userApiRes.user.profile.real_name;
      bot.api.channels.info({ channel: message.channel }, (channelApiError, channelApiRes) => {
        // private channel はchannels apiからは情報取得できない。groups apiなら取れるが、privateチャンネル名は知られなくても良い
        const channelName = (channelApiError !== 'channel_not_found') ? channelApiRes.channel.name : 'どこかprivate channel';
        bot.say(
          {
            text: `${userName}さんが作品リンクを投稿しました(in #${channelName}) \n -------------------------- \n ${matchMessage} \n --------------------------`,
            channel: destChannel,
          });
      });
    });
  }
});

起動は

originChannel='<channel_id1>' destChannel='<channel_id2>' token=<bot_token> node urlcopy_bot.js

その他

eslint 使ってみた。 明らかな表記ブレは適当に直してくれるし、コードはきれいになるし、紛らわしい表記とか教えてくれるし、割と良かった。 他のところにもlint入れてみようかなと思った。

elasticsearchのindexを整理するcuratorのactionファイルを整理した話

これは何

elasticsearchのindexを削除するときにはcuratorを使うと思うが、そのactionファイルがわかりにくく、大きくなりがちだったのでメモ。 curator はver 4での記法(ver3だとちょっと違うみたいなので注意)

curator action fileの基本の書き方

1から順番に実行される。

actions:
  1:
    action: delete_indices
    description: "delete application log indices"
    options:
      disable_action: False
      ignore_empty_list: True
    filters:
    - filtertype: pattern
      kind: prefix
      value: "application."  # elasticsearchのindex名と"kind","value"設定が対応するようにする
      exclude: False
    - filtertype: age
      source: creation_date
      direction: older
      timestring: '%Y.%m.%d'
      unit: days
      unit_count: "30"   # 30日残す設定
  2:
    action: delete_indices
    description: "delete indices"
    options:
      disable_action: False
      ignore_empty_list: True
    filters:
    - filtertype: pattern
      kind: prefix
      value: "application."
      exclude: True   # ここでexcludeしておかないと30日保存したいのに10日までになってしまう
    - filtertype: kibana
      exclude: True  # kibanaを使っている場合には入れたほうがよい。.kibanaファイルが消えて、kibana indexを毎回登録しないといけなくなる。
    - filtertype: age
      source: creation_date
      direction: older
      timestring: '%Y.%m.%d'
      unit: days
      unit_count: 10

削除日数を細かく制御したい場合(ansible templateで解決)

上の例の1の記述のところみたいなのを並べて、さらに2のexcludeにも突っ込んでいかないといけない。コード量が増えて管理しづらい。 ansibleで配置するなら以下のようにすればvarsのリストに追加するだけで簡単。

  • varsで以下のように設定する
CURATOR_DELETE_LIST:
 - { TARGET: "application."          , DAYS: "30" }
 - { TARGET: "httpd.error."          , DAYS: "30" }
 - { TARGET: "httpd.access.hoge." , DAYS: "7" }
 - { TARGET: "httpd.access.hoge."     , DAYS: "7" }
  • loopでまわす
actions:
{# 明示的な削除対象のためのブロック-#}
{% for t in CURATOR_DELETE_LIST %}
  {{ loop.index }}: 
    action: delete_indices
    description: "delete {{ t.TARGET }} indices"
    options:
      disable_action: False
      ignore_empty_list: True
    filters:
    - filtertype: pattern
      kind: prefix
      value: "{{ t.TARGET }}"
      exclude: False
    - filtertype: age
      source: creation_date
      direction: older
      timestring: '%Y.%m.%d'
      unit: days
      unit_count: "{{ t.DAYS }}"
{% endfor %}

{# .kibana を除くその他の index は 10 日間保持。最後のactionになるように99としておく。 #}
  99: 
    action: delete_indices
    description: "delete indices"
    options:
      disable_action: False
      ignore_empty_list: True
    filters:
{% for t in CURATOR_DELETE_LIST %}
    - filtertype: pattern
      kind: prefix
      value: "{{ t.TARGET }}"
      exclude: True 
{% endfor %}
    - filtertype: kibana
      exclude: True
    - filtertype: age
      source: creation_date
      direction: older
      timestring: '%Y.%m.%d'
      unit: days
      unit_count: 10

もっとまとめて綺麗にできるかも。でもまあ役割ごとにブロックで分けれたからよし。

memcachedのサイズ別データ数のカウント

これは何

memcachedからredisへ移行したいので、redisのベンチマークを取っていたが、その時にmemcachedに保存されているデータがどれくらいのサイズがどれくらいの量で出現するのか確認した。その話を適当にまとめておく。

chunkサイズとslabについて

参考: http://gihyo.jp/dev/feature/01/memcached/0002

memachedはメモリのフラグメンテーションを防ぐために slab allocator というのを使っているっぽい。単にmalloc と freeを使ってメモリを扱うとばらばらになってしまう。確保したメモリをいろいろなサイズの塊(chunk)にわけて同じサイズの塊をクラスとしてひとまとまりにする。stats itemの出力はどうもこのchunk別に別れている模様。

class別個数カウント

$ echo "stats items" | nc localhost 11211    | grep number
STAT items:1:number 6
STAT items:2:number 4990
STAT items:3:number 1
STAT items:4:number 39
STAT items:5:number 160
STAT items:6:number 3
STAT items:7:number 17
STAT items:8:number 19
STAT items:9:number 46
STAT items:10:number 1
STAT items:11:number 5326
STAT items:12:number 1397
STAT items:13:number 60
以下略

このコマンドは↓でもよい

$ echo "stats slabs" | nc localhost 11211  | grep used_chunk

classのサイズを確認する

上からサイズ順になっている。

$ echo "stats slabs" | nc localhost 11211  | grep chunk_size
STAT 1:chunk_size 96
STAT 2:chunk_size 120
STAT 3:chunk_size 152
STAT 4:chunk_size 192
STAT 5:chunk_size 240
STAT 6:chunk_size 304
STAT 7:chunk_size 384
STAT 8:chunk_size 480
STAT 9:chunk_size 600
STAT 10:chunk_size 752
STAT 11:chunk_size 944
STAT 12:chunk_size 1184
STAT 13:chunk_size 1480
以下略

トータル個数とか一定サイズ以上のものとか

$ echo "stats slabs" | nc localhost 11211  | grep used_chunk | awk 'BEGIN {num = 0};{ sum =+ sum + $3} END {print sum }'

大きいサイズの個数が知りたいななど。例えばclass 19以上のもの(size 5kでの区切りで見たやつ)

$  echo "stats slabs" | nc localhost 11211  | grep used_chunk | egrep "(STAT 19|STAT [2-4][0-9])" |  awk 'BEGIN {num = 0};{ sum =+ sum + $3} END {print sum }'

redisとmemcachedのベンチマークをmemtier-benchmarkで取る

これは何

redisとmemcachedベンチマークを memtier-benchmark というツールで取った。 ざっくりまとめておく。

github.com

memtier-benchmark と redis-benchmark の比較

主にmemtier-benchmarkのメリット。redis-benchmarkにできないこと。

  • redis, memcache の両方のプロトコルを喋れる(redis-benchはredisのみ)
  • SET/GETの重み付けができる(redis-benchはそれぞれ別に全力で投げる)
  • keyとvalueサイズを変えられて(redis-benchはkeyサイズを変えるのが大変。valueサイズはどちらもオプションで変えられる。)

memtier-benchmark のインストー

概ねREADMEどおりにできた。 インストールしたlibevent-2.0がmemtier-benchmark実行時に見つからなかったので雑にlinkして見えるようにした(多分良い解決策ではないが、使い捨て負荷試験環境なので良し)。

sudo ln -s /usr/local/lib/libevent-2.0.so.5 /usr/lib64/libevent-2.0.so.5

コマンド

memtier_benchmark -p 6379 -P redis --threads 4 -c 250  --test-time=10 --ratio=1:10 --data-size-list=1000:60,5000:30,100000:10  --key-prefix=memtier-memtier-memtier- --key-maximum=100000

実際にやったコマンドとは異なるが、サンプルとして書いておく。以下雑な説明

  • -p port
  • -P protocol
  • –threads クライアントのスレッド数
  • -c スレッド数あたりのコネクション数。トータル4*250=1000となる
  • –test-time テスト時間
  • –data-size-list. 今回はmemcachedからredisへの移行が目的。現行のmemcachedのデータに合うようにweightを決めた(別記事として書くかもしれない)
  • –key-prefix key長を適当に調整するために使える
  • –key-maximum キーのバリエーションになると思う

fluent-plugin-ping-messageプラグインを最新のを入れるとtd-agentがunstable versionになるためバージョン指定する話

これは何

https://rubygems.org/gems/fluent-plugin-ping-message/ という Fluentd の plugin を最新化すると、同時に td-agent 本体も unstable な最新の 0.14 系にあがってしまい、ログ転送がたまにできなくなる事象が発生する。 その対策。

原因

この現象はpluginの依存するfluentdのバージョンが指定されているので、gemで最新のplugin version1.0.0が入るとfluentdも v0.14が必要になり、強制的にあげられてしまうため。 参考: https://github.com/tagomoris/fluent-plugin-ping-message

対策

ansibleのtasksで書くならこんな感じ。-vで指定する。

- name: add fluent-plugin-ping-message
  shell: td-agent-gem install fluent-plugin-ping-message -v {{ FLUENT_PLUGIN_PING_MESSAGE_VERSION }}

stableなtd-agentのバージョンは? どこで確認する?

http://www.fluentd.org/download

  • stable v0.12 系
  • experimental version が v0.14となっている。

td-agent本体 version確認方法

$ which td-agent
/usr/sbin/td-agent
$ td-agent --version
td-agent 0.12.29

plugin version確認方法 

td-agent-gemで入れているので listコマンドから確認

$ td-agent-gem list  | grep ping
fluent-plugin-ping-message (1.0.0, 0.2.0)

左が新しいもの、右が古いversionっぽい。gemで入れると古いバージョンも残るらしい。 $ td-agent-gem list | grep ping fluent-plugin-ping-message (0.2.0)

入れなおし方法

  • sudo yum remove td-agentを実行する
  • rm -rf /opt/td-agentを実行して残存プログラムを完全消去する。これをやらないと/opt/td-agentにプログラム本体がいるっぽく yumで再インストールしてもversionが新しいプログラムのままになる。
  • td-agent再インストー

実験

plugin入れる前

$ td-agent --version
td-agent 0.12.31

入れてみる

$ sudo  td-agent-gem install fluent-plugin-ping-message
Fetching: strptime-0.1.9.gem (100%)
Building native extensions.  This could take a while...
Successfully installed strptime-0.1.9
Fetching: serverengine-2.0.4.gem (100%)
Successfully installed serverengine-2.0.4
Fetching: msgpack-1.1.0.gem (100%)
Building native extensions.  This could take a while...
Successfully installed msgpack-1.1.0
Fetching: fluentd-0.14.13.gem (100%)
Successfully installed fluentd-0.14.13   ★ここで fluentd 0.14 を入れている。
Fetching: fluent-plugin-ping-message-1.0.0.gem (100%)
Successfully installed fluent-plugin-ping-message-1.0.0
Parsing documentation for strptime-0.1.9
Installing ri documentation for strptime-0.1.9
Parsing documentation for serverengine-2.0.4
Installing ri documentation for serverengine-2.0.4
Parsing documentation for msgpack-1.1.0
Installing ri documentation for msgpack-1.1.0
Parsing documentation for fluentd-0.14.13
Installing ri documentation for fluentd-0.14.13
Parsing documentation for fluent-plugin-ping-message-1.0.0
Installing ri documentation for fluent-plugin-ping-message-1.0.0
Done installing documentation for strptime, serverengine, msgpack, fluentd, fluent-plugin-ping-message after 8 seconds
5 gems installed
$ td-agent --version
td-agent 0.14.13

haproxy設定の自分メモ(redisのrevproとして)

これは何

redis+sentinelの前段にhaproxyを置いてクライアントがあんまりnode poolを意識しなくてもよさそうな感じにしたい。 devだけで適用して本番には適用していないので甘いところはあるかも。

方針

redis slaveにgetを負荷分散したりするほど負荷は高くない系に使えるかも。 常にmasterに接続しに行き、slaveは待機系。

設定ファイル

ansible templateの形。

global
  {# chroot内で動いているため/dev/logへアクセスできない。そのためUDP経由でrsyslogにログを渡す。-#}
  log         127.0.0.1 local2
  chroot      {{ HAPROXY_JAIL_DIR }}
  user        {{ HAPROXY_USER }}
  group       {{ HAPROXY_GROUP }}
  daemon
  {# maxconn: redisで設定しているものより大きければよい。このレイヤでは特に制限はかけない。 -#}
  maxconn     10000

{# stats: haproxyのステータスを取れるようにするための設定 -#}
listen stats 
  {# bind: ポート9000への外部からの経路はないが念のためback側からのみ疎通可能としておく。-#}
  bind :9000 interface eth0
  mode http
  stats enable
  stats uri /haproxy_stats 
  {# refresh: stats画面の自動リフレッシュを有効にする -#}
  stats refresh 10    

defaults
  mode                    tcp
  log                     global
  option                  dontlognull
  {# retries: バックエンドサーバへ接続失敗した時の再試行回数-#}
  retries                 3
  {# timeout queue: maxconnを超えたリクエストはキューに入る。そのキューに入った接続を開放するまでの時間。そんなに待たせても意味が無いのであまり大きな値じゃなくてよい。 -#}
  timeout queue           10s
  {# timeout connect: haproxyのbackendへの接続のタイムアウト。近いネットワークにいれば基本的に短い時間で接続できるはず。 -#}
  timeout connect         5s 
  {# timeout check: コネクションが確立したあとの追加のヘルスチェックのタイムアウト。これに時間がかかるということはラグの大きなサーバなので、長い時間待つ必要はない。-#}
  timeout check           5s
  {# timeout client-fin/server-fin: 正しく接続断しなかった場合に切る(こちらからFIN送信したがクライアントがFINを送ってこなかった場合)。FIN_WAIT状態が続くのを防ぐ。 -#}
  timeout client-fin      10s
  timeout server-fin      10s

{# その他入れない設定について。いれるのが推奨のようなので要検討。
* timeout client: 非活性なクライアントからの接続を切る設定。redis設定に合わせて設定しない。 
* timeout server: haproxyから非活性backendへの接続を切る時間。redis設定に合わせて設定しない。 
-#}


frontend  main
  bind *:5000
  default_backend             myredis

backend myredis
  {# balance: どれか1台だけがコネクションを受け付ける設定(first)。id設定があれば一番値が小さいもの。なければリストの1番上から優先される。負荷分散よりも1台のmasterにキャッシュがのることを優先する。 -#} 
  balance    first
  {# 通常時のヘルスチェック間隔はredisのマスター昇格のタイムスケールにあわせて秒単位でよい。haproxyのreload直後などでヘルスチェックをしていない場合には即座に行う(fastinter)。-#}
  {% for srv in HAPROXY_BACKEND_LIST -%}
  server  redis{{ loop.index }} {{ srv }} check inter 1s fall 2 rise 1 fastinter 10ms
  {% endfor %}

  option tcp-check
  tcp-check connect
  tcp-check send info\ replication\r\n
  tcp-check expect string role:master
  tcp-check send QUIT\r\n
  tcp-check expect string +OK

ちなみに rsyslog設定のansible taskはこんな感じ

# haproxyのログ出力のためのrsyslog設定
- name: enable UDP log acceptance for rsyslog for haproxy 
  lineinfile:
    dest: "/etc/rsyslog.conf"
    line: "{{ item }}"
  with_items:
    - "$ModLoad imudp"
    - "$UDPServerRun 514" 
    - "local2.* {{ HAPROXY_LOG_DIR }}/haproxy.log"

redis設定について自分メモ

これはなに

redis設定をした時の検討項目。 devで設定したのみでまだ本番で使っていないのであまいところはあるかも。

設定diff

以下は redis-3.2.8 に同梱されていた設定ファイルとplaybook template とのdiff。

diff /tmp/redis.conf roles/install_redis/templates/redis.conf.j2
1c1,3
< # Redis configuration file example.
---
> # USAGE: {{ item.USAGE }}
> # Redis configuration file.
> # This configuration is based on redis-3.2.8 default configuration file.
61c63,64
< bind 127.0.0.1
---
> {# bind: 特に認証はかけない。やるならサーバが居るネットワークセグメントごとに裏からのみ疎通できるようにするなどの設定をいれる。 -#}
> # bind
80c83,84
< protected-mode yes
---
> {# protected-mode: キャッシュサーバはインターネットに直接露出しなければ切っておいてもよいかもしれない -#}
> protected-mode no
84c88,89
< port 6379
---
> {# port: 同一サーバ内に別ポートで複数インスタンスを立ち上げることができるようにしておきたい -#}
> port {{ item.PORT }}
93c98,99
< tcp-backlog 511
---
> {# tcp-backlog: これと一緒にsomaxconnもsysctlで設定しておく必要がある。TCPの接続要求を格納するキューの最大長なので、front系サーバapacheがある程度詰まっても大丈夫な値にしておくのがよさそう -#}
> tcp-backlog 2500
104a111
> {# timeout: idle状態のコネクションはタイムアウトさせない(デフォルト設定)。アプリケーションが側できちんと切断処理がされていれば問題にならないはず-#}
127a135
> {# daemonize: 直接redis-serverコマンドにこの設定ファイルを食わせて起動した場合にはデバッグのためにdaemonize noでよい。systemctlで起動した場合にはこの設定に関わらずデーモン化される -#}
150c158,159
< pidfile /var/run/redis_6379.pid
---
> {# pidfile: 一応設定しているが、systemdではpidファイルは不要 -#}
> pidfile /var/run/redis_{{ item.PORT }}.pid
163c172,173
< logfile ""
---
> {# logfile: redisユーザに権限があるディレクトリにログを吐くようにしておく。ローテートも忘れずに。redisはログを吐くごとにファイルを開き直すのでSIGHUPなどは不要。 -#}
> logfile "{{ REDIS_LOG_DIR }}/redis.log"
177a188
> {# database: keyspaceが独立したdatabaseを何個にするか。使えるDatabaseがの数が大きくてもデフォルトは0が使われるので細かい値はなんでもよさそう。dev, productionとかで使う用途を想定しているみたい。-#}
201c212,216
<
---
> {# save: redisが死んでて復旧した時にRDBには古いデータが残っている。しかしexpireは実時間基準なのできちんと古いデータは消えておいてくれる。
> RDBを取る時に親プロセスがやるのは子プロセスをforkさせるだけなので、親インスタンスにdisk I/Oが発生して待ちになることはないとのこと。あまりにもデータが大きいもしくはCPUがいまいちな場合にはforkに時間かかるのでclientへのレスポンス遅れることもあるらしい。
> RDBの方がAOFをよりも起動が早く、データサイズも小さいのがメリット。マスタデータはredisには無ければ厳密に永続化を考えなくても良い。パフォーマンスが落ちない程度に使う方針。
> 設定はデフォルトで問題なさそうならそのまま。
> -#}
219c234,235
< stop-writes-on-bgsave-error yes
---
> {# stop-writes-on-bgsave-error: persistentが止まるよりもwriteが止まるほうが不都合。TODO 適切な監視はほしい。 -#}
> stop-writes-on-bgsave-error no
224a241
> {# rdbcompression: 多少CPUを使っていても圧縮しておいてくれた方が良い。あまりにCPU不可が高くなりそうなら変えても良い。 -#}
233a251
> {# rdbchecksum: 10%程度の性能劣化はあるがRDBファイルの破損の検知は入れたままでもよさそう。 -#}
247c265,266
< dir ./
---
> {# dir: ファイル名はデフォルトで良いが、保存先は適切なpermissionの場所に配置しなければならない。-#}
> dir {{ REDIS_DATABASE_DIR }}
265c284,288
< # slaveof <masterip> <masterport>
---
> # slaveof <masterip> <masterport>
> {# slaveof: 設定に書かないでDBと同じようにコマンドで指定するのもあり。ただしmysqlの場合にはchange master toではその設定がファイルに保存される。redisの場合には設定に書いてないと再起動時に消えてしまうと思われる。意図せず消えてしまうか、意図しないmasterを向いているかで比較的後者の方が事故が少ないと考えてファイルに記載する。原則としてシステムに組み込むときにはmasterノードがどれか確認してから組み込むのがよい。 -#}
> {% if REPL_NODE=="slave" %}
> slaveof {{ REDIS_MASTER_NAME }} {{ REDIS_MASTER_PORT }}
> {% endif %}
284a308
> {# slave-serve-stale-data: レプリケーション遅延で多少古いデータが帰ってしまったとしてもエラーを返すよりマシかもしれない(デフォルト) -#}
300a325,326
> {# slave-read-only: masterとの再同期によりslaveのみに保持されたデータは削除されるのでslaveに一時的に書き込みたいデータを書いておく用途に使える。特に使いたい理由がなければreadonlyのほうがよい(デフォルト)。
> -#}
414c440,441
< slave-priority 100
---
> {# slave-priority: 基本的には変更しないが、仮にmasterにしたくないslaveを作ることになったらansible EXTRA_VARSで0に設定できるようにしておく。-#}
> slave-priority {{ REDIS_SLAVE_PRIORITY | default("100") }}
511a539
> {# maxclients: 十分に大きいのでデフォルトのままでよい。pconnect使うならそれを意識して増やしておいたほうがよい。コネクションの数の監視も必要だろう-#}
537a566,570
> {% if ENVIRONMENT == "production" -%}
> maxmemory {{ item.MAXMEMORY }}
> {% else %}
> maxmemory 1G
> {% endif %}
592c625
<
---
> {# appendonly: redisにはマスタデータは乗らなければ永続化より性能を重視してOffにしておく。 -#}
623,624c656,657
< appendfsync everysec
< # appendfsync no
---
> # appendfsync everysec
> appendfsync no
834a868
> {# slowlog-log-slower-than: デフォルトでも秒に直すと0.01秒なので十分小さいのでそのままでよい。-#}
838a873
> {# slowlog-max-len: ここで設定した以上古いログは消えてしまう。まずはデフォルトで良い。あとで値を上げたければCONFIG SETコマンドを用いて設定できる。ログを見るためにはSLOWLOG GETコマンドを用いる -#}
905a941
> {# notify-keyspace-events: TODO 何かに使えそう。更新されたらPUBされるので他のサーバが更新を検知できる。サーバリストなど。PUBされた時にSUBしているサーバが接続していなければ受け取れないので注意が必要。pub/subを使いつつ、定期的に値を見直すなどを使えば信頼性をあるていど担保できるかもしれない。 -#}