読者です 読者をやめる 読者になる 読者になる

他社サイトに設置するjsタグを作る時のTips(prototype汚染の話)

javascript 技術

こんにちは、g0eです。

アドテク業界ではjavascriptのタグ(以下、jsタグ)をクライアントサイトに埋め込んでもらうことがよくあります。 自社サイトの開発と違って、設置先のサイトでどのようなjavascriptが動いているかわからないので、他のjavascriptと競合しないように書く必要があります。

今回は他サイトに設置するjsタグを書く時に注意すべき事項の中の一つの、prototype汚染の話をしたいと思います。

プロトタイプ汚染の例

下記のコードを実行した後に、

// Objectのプロトタイプを汚染するコードの例
Object.prototype.hoge = function(){}

下記のような、オブジェクトpiyoをfor-inループで 調べるようなスクリプトを実行します。

var piyo = {foo: 1, bar: 2};
for(var k in piyo){
   console.log(k);
}

すると、コンソールに以下の文字列が表示されると思います。

foo
bar
hoge    /* !? */

そんなの当たり前だろ、と思った方はこの記事はここで読み終えていただいて大丈夫です。

あれ、hogeが混ざってきたぞ、という方はこのまま読み進めて下さい。

javascriptのプロトタイプチェーンの話はなかなか奥が深い話なので、今回は言及しませんが、上記のようにObject.prototypeやArray.prototypeなどに新しいプロパティを追加するコードがどこかにある場合、for-inループをそのまま使うのは非常に危険ということが伝わったでしょうか?

一昔前(むしろ大昔?)に流行ったprototype.jsというライブラリを利用しているサイトで、このようなプロトタイプ汚染に巻き込まれることがあります。

プロトタイプ汚染に強いfor文の書き方

hasOwnPropertyを使う

for-inループの中で取得したプロパティが、対象オブジェクト自身のプロパティ(つまり、プロトタイプチェーンをたどって取得したものではない)かどうかをチェックして利用するようにします。

var piyo = {foo: 1, bar: 2};
for(var k in piyo){
   if(piyo.hasOwnProperty(k)){
      console.log(k);
   }
}
Object.keysを使う

Object.keysを使ってプロパティの一覧を取得することでも、同じ効果を得ることができます。

var piyo = {foo: 1, bar: 2};
var keys = Object.keys(piyo);
for(var i=0;i<keys.length;i++){
   console.log(keys[i]);
}

まとめ

かなり駆け足になってしまいましたが、今回のまとめは以下の通りとなります。

  • ObjectやArrayのprototypeをいじらない(プロトタイプ汚染しない)
  • for-inループを使う時は注意する(使わない、もしくはhasOwnPropertyを併用する)

AWS Lambdaを使ってmackerelにEC2のSpotPriceを投げ込む

aws mackerel

こんにちは。g0eです。

昨日、弊社のコーポレートサイトをリニューアルしました。 なかなかかっこよく仕上がったんじゃないかと満足している一方で、随分と更新をサボっていたこのブログにも、きちんとしたリンクが貼られてしまったので、とりあえず何か書いておこうと思います。

参考までにコーポレートサイト corp.intimatemerger.com

背景

弊社ではAWS上にスポットインスタンスをうまく活用して、elasticsearchのクラスタを構築しています。スポットインスタンスは価格をかなり抑えられる一方で、時々価格が高騰してインスタンスが強制ターミネートされてちょっとドキドキします。(昨年の11月下旬〜12月上旬は特に頻度が高かったような)

冗長化して片方のAZが全滅してもデータ欠損は発生しない構成にしたり、スポットインスタンスで十分な数が確保できない場合は、オンデマンドインスタンスが自動的に起動するようにしたり、いくつか対応策はとっているので実害はほぼなかったりするのですが(ここの話はまた機会があれば改めて書きたいなとは思っています)、まずはスポットインスタンスの価格をきちんとモニタリングして傾向をつかんで行こうというのが今回の投稿の背景だったりします。

目的

前置きが少し長くなってしまいましたが、本題としては、弊社で監視に使っているmackerelにEC2のスポットインスタンスの価格推移を投げ込んで、アラートをあげたり、中長期のトレンドを可視化していきたいという話になります。(mackerel→slackにアラートを投げてPC/スマホから監視していますが、チャートも一緒に表示されるのでなかなか便利です。)

実装

早速ですが、lambdaのコード貼っておきます。言語はpythonです。

コード書いたのが1ヶ月以上前なので若干記憶が曖昧なのですが、スポット価格を取得するAPIの仕様がちょっと特殊で、

  • 指定した期間で価格が動いた時のデータ
  • 価格変動がなければ最後に価格が動いた時のデータ

が返ってきていたと記憶しています。

コードではちょっとトリッキーですが、未来の時間を開始時間=終了時間で指定することで、APIをコールした時点で最新の価格だけを取得しています。(気持ち悪いですが、試行錯誤した中ではこれが一番良かった)

import boto3
import datetime, time, json, urllib2
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

MACKEREL_APIKEY = "XXXXXXXXXXXXXXXXXXXX" # mackerelのAPIキー
MACKEREL_SERVICE = "xxxxxxxxx" # mackerelのサービスを指定
EC2_INSTANCE_TYPES = ["m4.4xlarge","c3.2xlarge"] # 価格を調べたいインスタンスを指定
EC2_PRODUCT_DESCRIPTIONS = ["Linux/UNIX (Amazon VPC)"]

def post_mackerel (apikey, service, payload):
    headers = { 'Content-type': 'application/json', 'X-Api-Key': apikey }
    url = "https://mackerel.io/api/v0/services/{0}/tsdb".format(service)
    data = json.dumps(payload)
    req = urllib2.Request(url, data, headers)
    res = urllib2.urlopen(req)
    return res

def get_payload():
    cl = boto3.client("ec2",region_name="ap-northeast-1")
    t = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
    res = cl.describe_spot_price_history( \
        StartTime=t,
        EndTime=t,
        ProductDescriptions=EC2_PRODUCT_DESCRIPTIONS,
        InstanceTypes=EC2_INSTANCE_TYPES,
    )
    payload = []
    now = int(time.time())
    for r in res["SpotPriceHistory"]:
        if r["AvailabilityZone"] == "ap-northeast-1a":
            az = "a"
        elif r["AvailabilityZone"] == "ap-northeast-1c":
            az = "c"
        else:
            raise Exception("Invalid AvailabilityZone")
        payload.append({ \
            "time": now,
            "name": "SpotPrice."+r["InstanceType"].replace(".","-")+"_"+az,
            "value": float(r["SpotPrice"]),
        })
    logger.info("payload length ... "+str(len(payload)))
    return payload

def lambda_handler (event, context):
    payload = get_payload()
    post_mackerel(MACKEREL_APIKEY, MACKEREL_SERVICE, payload)
    logger.info("done!!")

lambdaは、最近追加されたスケジュール実行の機能を使って、↓みたいな感じで5分毎に実行しています。 f:id:intimatemerger:20160116021742p:plain

結果mackerel上のサービスメトリックスに溜まったデータが↓みたいな感じになります。一回、スパイクしていますね…。 f:id:intimatemerger:20160116021901p:plain

今回の投稿は以上になります。ほぼ既存のコード貼っただけですね…。

はやく、LambdaがVPC対応しないかなぁ…。

Rundeckをクラスターモードで構築する

技術

こんにちはmatsです。

最近、更新サボってましたが久しぶりの投稿です。

 

今回は前に下の記事でご紹介していたジョブスケジューラー「Rundeck」について書こうかと思います。

http://tech.im-dmp.net/archives/1681

最近、2.6.0がリリースされたのでアップデートを行ったのですが、改めてドキュメントを見なおしてみるとクラスター構成が組めるみたいだったのでチャレンジしてみました。

そんなに難しくはないのですが、ドキュメントがあまり綺麗にまとまっていなかったりするので、参考になれば幸いです。

 

Rundeckとは

rundeck-logotype-512

オープンソースで開発されているJava製のジョブスケジューラーです。

SSHで対象サーバに接続し、決められた処理を行えるのが基本機能になるのですが、EC2連携のプラグインを使うことでEC2側のタグやインスタンスタイプ、AZなどで対象サーバをフィルタリング出来のが非常に強力です。

rundeck1

IMではバッチ処理等の他にデプロイ処理や覚えにくいREST APIコールなども登録して運用しています。

 

Rundeckのクラスター構成

全体構成はこんな感じです。

rundeck_infra

設定やログを外出しできるので、構成的にはオートスケール出来るのですが、さすがにジョブの最中にライフサイクルが走ると困るのでやっていません。

EC2のインスタンスはそれほど強くなくて大丈夫なのですが、javaのout of  memoryが怖いのでt2.medium(RAM:4GB)を使っています。

また、クライアント証明書による認証を行う関係でnginxを挟んでいますが、ELB→Rundeck(jetty)の様に直接受けてもいいかもしれません。

 

設定まわり

 

基本的には yum や apt-get でインストールすればすぐに動くのですが、クラスターを組む際はいくつか設定を変更する必要があります。

 

設定をMySQLに持たせるようにする

デフォルトの設定だと下記のようになっていて、サーバ内に設定を保持してしまうのでMySQL内に持つように変更します。

/etc/rundeck/rundeck-config.properties

dataSource.url = jdbc:h2:file:/var/lib/rundeck/data/rundeckdb;MVCC=true;TRACE_LEVEL_FILE=4

これを下記のように書き換えます。

dataSource.url = jdbc:mysql://{{ mysql_host }}/{{ mysql_database }}?autoReconnect=true
dataSource.username = {{ mysql_user }}
dataSource.password = {{ mysql_password }}

# Enables DB for Project configuration storage
rundeck.projectsStorageType = db

# Encryption for project config storage
rundeck.config.storage.converter.1.type = jasypt-encryption
rundeck.config.storage.converter.1.path = projects
rundeck.config.storage.converter.1.config.password = mysecret

# Enable DB for Key Storage
rundeck.storage.provider.1.type = db
rundeck.storage.provider.1.path = keys

# Encryption for Key Storage
rundeck.storage.converter.1.type = jasypt-encryption
rundeck.storage.converter.1.path = keys
rundeck.storage.converter.1.config.password = mysecret

 

ログをS3に出力するようにする

下記設定を追加し、rundeck-s3-log-pluginを有効化します。

/etc/rundeck/rundeck-config.properties

rundeck.execution.logs.fileStoragePlugin = org.rundeck.amazon-s3

 

また、下記のようにログの出力先を設定します。

/etc/rundeck/framework.properties

#name of the bucket
framework.plugin.ExecutionFileStorage.org.rundeck.amazon-s3.bucket = {{ s3_bucket }}

#path to store the logs
framework.plugin.ExecutionFileStorage.org.rundeck.amazon-s3.path = logs/${job.project}/${job.id}/${job.execid}.log

#  AWS region name
framework.plugin.ExecutionFileStorage.org.rundeck.amazon-s3.region = ap-northeast-1

 

クラスターモードの有効化

下記設定を追加し、クラスターモードを有効化します。

/etc/rundeck/rundeck-config.properties

rundeck.clusterMode.enabled = true

 

また、個々のサーバを識別するためのserverUUIDを設定します。

値のフォーマットのバリデーションがあるみたいなので、uuidgen などで生成するのがいいかと思います。

/etc/rundeck/framework.properties

rundeck.server.uuid = 00000000-0000-0000-0000-000000000000

 

IMの場合は起動時に読まれるシェルスクリプト(/etc/rundeck/profile)内で生成するようにしています。

sed -i -e &quot;/^rundeck.server.uuid/c\rundeck.server.uuid = $(uuidgen)&quot; /etc/rundeck/framework.properties

 

ELBのスティッキーセッションを設定する

Rundeckはjettyというアプリケーションサーバが発行するCookieでセッション情報を管理しているので、ELBのセッション維持にこのCookieを用いるように設定します。

Cookie名は JSESSIONID です。

jsessionid

ちなみにこの設定をしないと別のサーバにバランシングされた瞬間にログアウトします。。。

 

 

あとはRundeckのプロセスを起動するだけでクラスターに参加します。楽ちんですね(´∀`)

 

 

おすすめプラグイン

大したものは入れてませんが、一応紹介しておきます。

rundeck-ec2-nodes-plugin

RundeckからEC2のAPIをコールして、インスタンス情報を取得することが出来るプラグインです。

 

rundeck-s3-log-plugin

ジョブの実行ログをS3におくプラグインです。

 

rundeck-slack-incoming-webhook-plugin

ジョブの通知をSlackのIncomming Webnoohに送るプラグインです。

↓こんな感じです。

Slack

 

 

Rundeckのクラスタリングについて日本語で書いてあるドキュメントはあまりないみたいなので、参考にして頂ければ幸いです。

(本家のドキュメントはこちら → http://rundeck.org/docs/administration/scaling-rundeck.html

 

 

AnsibleのPlaybookも公開しているので、Vagrantで試す際は使って頂ければと。

https://github.com/mats116/ansible-playbook-rundeck

Elasticsearch勉強会でLTしてきました

elasticsearch 勉強会

こんにちはmatsです。

 

昨日行われていた「第11回elasticsearch勉強会」でLTをしてきました。

今回は来日していたElasticsearchのCTO向けに英語スライドで事例紹介をとのオーダーがあったので、全編英語になっています。(さすがに日本語でしゃべりましたが)

内容的にはESというよりかはDMPとは?みたいな話が中心になりますが、世界的に見てもリアルタイムに検索クエリを投げることが出来るDMPはほぼ無い気がするのでいい事例なんじゃないかなと思います。

 

ちょっと失敗したのは、オーディエンスの特徴が出やすい(興味、関心が反映されやすい)デモ用のキーワードを用意していなかったことですね。。

参考までに「薄毛」で検索した時のキャプチャを載せておきます。

 

audiencesearch_demo

次があるのであればヘビーなAggregationに対応するためのチューニングの話とかをしたいですね。

 

おしまい。

 

Aerospikeのスケールアウト

aerospike 技術

こんにちはmatsです。

 

先日、「Aerospike Deep Dive」に登壇したのですが、その際にスケールアウト(ノードの追加)について聞かれる方が多い様に思いました。

http://tech.im-dmp.net/archives/3971

今回はそのスケールアウトの手順と注意事項について書こうかと思います。

 

スケールアウトの手順

0. 環境について

今回の環境は下記の想定です。

既存のノード

aerospike1 10.0.0.1
aerospike2 10.0.0.2
aerospike3 10.0.0.3

追加するノード

aerospike4 10.0.0.4

追加するノードのハードウェア構成は同じもので、既存のノードと同様の手順で aerospike-server がインストールされているものとします。

 

1. 設定の編集

基本的には既存のノードと全く同じになりますが、注意点があります。

AWSなどマルチキャスト通信が出来ない環境では、aerospike.confの設定が全ノードで同じにならず、追加を繰り返す毎に差分が増えていきます。

IMでは全ノード分のIPをconfに書き、自ノードのIPはコメントアウトする形でansibleで管理しています。ノード追加の際に既存のノードの設定も修正しますが、読み込みには再起動が伴うのであくまで形式上の変更になります。

また、この設定はオンラインでは変更不可のため設定の非対称性は残り続けます。(運用上問題ありませんが・・・)

aerospike.conf 設定例

network {
    service {
        address any
        port 3000
    }

    heartbeat {
        mode mesh
        address any
        port 3002
        mesh-seed-address-port 10.0.0.1 3002 # aerospike1
        mesh-seed-address-port 10.0.0.2 3002 # aerospike2
        mesh-seed-address-port 10.0.0.3 3002 # aerospike3
        #mesh-seed-address-port 10.0.0.4 3002 # aerospike4

        interval 150
        timeout 20
    }

    fabric {
        port 3001
    }
    info {
        port 3003
    }
}

 

2. 書き込み処理の停止

公式のドキュメントには記載がありませんが、書き込みは停止しておくのが推奨な様です。

 

3. 新規追加ノードの起動

asdのサービスを起動します。clusterへの参加は自動で行われます。

 

4. マッピングテーブルの生成を待つ

マッピングテーブルの生成が完了するまで1〜5分ほど待ちます。この間、無応答になりAMCなどでステータスを確認することが出来ません。

データ量に依存して時間が伸びることがあります。(IMでの実績ベースなので未確認)

 

5. マイグレーション完了まで待つ

一応、マッピングテーブルの生成が完了していれば、動作上は正常な状態に戻るので書き込み処理を再開しても問題は無いかと思います。

ただ、推奨的にはマイグレーションの完了まで待ったほうが良いとのことです。

 

6. 書き込み処理の再開

aerospikeへの書き込みを再開します。

 

気になる点

  • 無停止で行うことが出来ません
  • データ量の増加に伴い、無応答時間が伸びるなど不安程度が増します
  • マイグレーション中はレプリカの数が0になります

 

設定変更の際などローリングで全ノードを入れ替えたいと言っていた方もいらっしゃいましたが、個人的にはノードの数はあまり変更しないことをオススメしています。(作業は簡単なのですが、毎回挙動が違うのでドキドキします。。)

 

おしまい

「Data Connector for Amazon S3」でELBのログを取り込む

embulk s3 TreasureData 技術

こんにちは。matsです。

 

トレジャーデータからこんなサービスがリリースされました。

http://blog-jp.treasuredata.com/entry/2015/06/22/125518

たまたま社内でELBのログを調査する機会があったので、「Data Connector」でELBのアクセスログをTDに送るのに使用してみました。

今回は「Data Connector」を使う時の手順について書こうかと思います。

 

TreasureData ToolBeltのインストール

公式サイトから落としてインストールします。

私は自分のMacbookにインストールしました。

 

TreasureData ToolBeltの設定

アカウント認証

下記コマンドでアカウント認証をします。(対話形式です)

$ td -e https://api.treasuredata.com account -f

Enter your Treasure Data credentials.
Email: kazuki.matsuda@intimatemerger.com
Password (typing will be hidden):
Authenticated successfully.
Use 'td -e https://api.treasuredata.com db:create &lt;db_name&gt;' to create a database.

 

APIエンドポイントの登録

下記コマンドでコールするAPIを設定します。(切り替えるとYBIでもいけるのかな?)

$ td server:endpoint https://api.treasuredata.com

 

設定ファイルの作成

基本的にはembulkの設定と同じようです。

ELBのログファイルを読み込む設定はこんな感じになりました。

elb.yml

config:
  in:
    type: s3
    bucket: &lt;bucket_name&gt;
    path_prefix: [prefix]/AWSLogs/[account_id]/elasticloadbalancing/ap-northeast-1/[YYYY]/[MM]/[DD]/
    endpoint: s3-ap-northeast-1.amazonaws.com
    access_key_id: XXXXXXXXXXXXXXXXXXXXXX
    secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    parser:
      charset: UTF-8
      newline: CRLF
      type: csv
      delimiter: ' '
      skip_header_lines: 1
      allow_extra_columns: true
      allow_optional_columns: true
      columns:
        - name: time
          type: timestamp
          format: "%Y-%m-%dT%H:%M:%S"
        - name: elb_name
          type: string
        - name: client:port
          type: string
        - name: backend:port
          type: string
        - name: request_processing_time
          type: double
        - name: backend_processing_time
          type: double
        - name: response_processing_time
          type: double
        - name: elb_status_code
          type: string
        - name: backend_status_code
          type: string
        - name: received_bytes
          type: long
        - name: sent_bytes
          type: long
        - name: request
          type: string
        - name: user_agent
          type: string
        - name: ssl_cipher
          type: string
        - name: ssl_protocol
          type: string
  out:
    mode: append

 

ログフォーマットはこちらを参考にしました。が、実際にはかれているログをよく見ると

  • user_agent
  • ssl_cipher
  • ssl_protocol

といった項目があるので、注意が必要です。(これ公式のドキュメントに書いてほしいな。。)

embulk使用時とちがうのは、outの書き方ぐらいかと思います。デフォルトの出力先がTDになっているので、アウトプットプラグインの指定が必要ない感じです。

 

今回は特定の日のログが目当てだったので、path_prefixで日にちまで指定して取り込みました。

階層を上げれば月別とかで取り込むことができるので便利ですね。

 

ジョブの登録

次のコマンドでTDにジョブを登録します。

$ td connector:issue elb.yml --database embulk --table elb_accesslog --time-column time

Job 27708303 is queued.
Use 'td job:show 27708303' to show the status.

databaseとtableをあらかじめ作っていないと、ジョブを登録できないようなので注意が必要です。

 

実行するとTDの画面上からはこんな感じで「RUNNING」のジョブに積まれます。

 

td_bulk

 

ジョブの定期実行

ジョブをタスクスケジューラーで定期的に動かす場合は、下記コマンドで登録することができます。

 

$ td connector:create 
  daily_import 
  "10 0 * * *" 
  embulk 
  elb_accesslog 
  elb.yml 
  --time-column time

 

が、定期的に出力されるログを取り込む場合は、取り込む際に対象ファイルの削除か、path_prefixの動的な変更が出来ないと正直使いにくいです。。

 

定期実行とは相性が悪いみたいなので、Lambdaを絡めてファイルの生成と同時にTDに取り込むというのをそのうちやってみようかと思います。

 

おしまい

 

 

AWSでAerospikeをインストールする

aerospike aws 技術

こんにちは。matsです。

 

ちょうどAerospikeを増強する機会があったので、インストールについてまとめてみました。

基本的にはこちらのリファレンスを参考にしています。

 

サーバの起動

下記の構成で立ち上げます。

起動したら次のコマンドで拡張ネットワーキングが有効になっていることを確認します。

「driver: ixgbevf」とあれば大丈夫です。

# ethtool -i eth0

driver: ixgbevf
version: 2.14.2+amzn
firmware-version: N/A
bus-info: 0000:00:03.0
supports-statistics: yes
supports-test: yes
supports-eeprom-access: no
supports-register-dump: yes
supports-priv-flags: no

 

Aerospikeのインストール

rpmパッケージを落としてきてインストールするだけです。楽ちん。

(ここまで楽ならyumリポジトリあってもいいのに。。)

# wget --output-document=aerospike.tgz http://www.aerospike.com/download/server/3.5.14/artifact/el6
# tar -xzvf aerospike.tgz
# cd aerospike-server-community-*-el6
# ./asinstall

 

ネットワーク(NIC)の最適化

Amazon Linux (HVM)の各NICは、毎秒約250Kパケットを処理することができますが、より高いパフォーマンスを求める場合は、以下のどちらかの対応が必要な様です。

(これは一つのNICから受けるパケットは一つのCPUで受けて処理する仕様に起因しているようです)

 

ENI(仮想NIC)を追加する

NICを追加し、それぞれに対応するCPUを分けることで対応します。

が、結構設定が面倒くさいので、私のオススメは次のやつです。

 

RPSを使う

Linux kernelの2.6.35以降で利用できる機能にRPSというものがあります。

詳細な仕様は私も分かっていないのですが、簡単な設定をすることで特定のNICのパケットを処理するCPUの個数を指定することができる様です。

設定方法は下記コマンドになります。

echo ffffffff &gt; /sys/class/net/eth0/queues/rx-0/rps_cpus

AMIに固めるときなどは /etc/rc.local の最後に書いています。

 

Ephemeral Diskのアンマウント

Aerospikeはファイルシステムを介さず、直接ブロックデバイスに書き込むのでマウントは不要です。

EC2ではデフォルトでマウントされてしまうので外しておきます。

# umount /media/ephemeral0
# sed -i -e '/ephemeral0/d' /etc/fstab

 

Aerospikeの起動

あとは起動するだけです。

ちなみに、自動起動は設定していません。(サーバが落ちた時などは予期しないことが起こっている事が多いので)

# service aerospike start

 

 

おわり