成らぬは人の為さぬなりけり

エンジニアライフをエンジョイする為のブログ

Helm (Kubernetes) vol.3

前回: Helm(Kubernetes) vol.2 - 成らぬは人の為さぬなりけり Helmのライフサイクルや思想を理解したので、今度は実際にChartの中身を見ていく

Chart

chartの構成

wordpress/
  Chart.yaml          # A YAML file containing information about the chart
  LICENSE             # OPTIONAL: A plain text file containing the license for the chart
  README.md           # OPTIONAL: A human-readable README file
  requirements.yaml   # OPTIONAL: A YAML file listing dependencies for the chart
  values.yaml         # The default configuration values for this chart
  charts/             # OPTIONAL: A directory containing any charts upon which this chart depends.
  templates/          # OPTIONAL: A directory of templates that, when combined with values,
                      # will generate valid Kubernetes manifest files.
  templates/NOTES.txt # OPTIONAL: A plain text file containing short usage notes

こんな感じ

主要な所を説明

Chart.yaml

まぁ、メタデータだな。 名前とか、versionとか、authorとかそーいうの。

values.yaml

こいつが、設定だ。 Ansibleでいう、defaults/main.yml

この設定を、外から上書きできる感じになる。

templates

この中に、実際に使う、yamlを吐くテンプレートを書く。

deploymentとかserviceとかingressとか。。。

そのyamlを吐くtemplateにvaluesの変数を埋め込む感じ

ぶっちゃけ、yamlじゃなくても普通にERB的なテンプレートなので、なんでもできそう。

(ただ、k8sで使うので、YAML以外使わないけど…)

実際に見てみよう

Helm Create !!!!

新しいChartを作る

helm create test

すると、あら不思議、中にはサンプルが! どうやら、nginxをdeployするサンプルくさい

Chart.yaml

apiVersion: v1
description: A Helm chart for Kubernetes
name: test
version: 0.1.0

スィンプル

values.yaml

# Default values for test.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
  repository: nginx
  tag: stable
  pullPolicy: IfNotPresent
service:
  name: nginx
  type: ClusterIP
  externalPort: 80
  internalPort: 80
ingress:
  enabled: false
  # Used to create an Ingress record.
  hosts:
    - chart-example.local
  annotations:
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  tls:
    # Secrets must be manually created in the namespace.
    # - secretName: chart-example-tls
    #   hosts:
    #     - chart-example.local
resources: {}
  # We usually recommend not to specify default resources and to leave this as a conscious
  # choice for the user. This also increases chances charts run on environments with little
  # resources, such as Minikube. If you do want to specify resources, uncomment the following
  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
  # limits:
  #  cpu: 100m
  #  memory: 128Mi
  # requests:
  #  cpu: 100m
  #  memory: 128Mi

うんうん、やりたいことはよくわかる

templates/deployment.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: {{ template "fullname" . }}
  labels:
    app: {{ template "name" . }}
    chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
spec:
  replicas: {{ .Values.replicaCount }}
  template:
    metadata:
      labels:
        app: {{ template "name" . }}
        release: {{ .Release.Name }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: {{ .Values.service.internalPort }}
          livenessProbe:
            httpGet:
              path: /
              port: {{ .Values.service.internalPort }}
          readinessProbe:
            httpGet:
              path: /
              port: {{ .Values.service.internalPort }}
          resources:
{{ toYaml .Values.resources | indent 12 }}
    {{- if .Values.nodeSelector }}
      nodeSelector:
{{ toYaml .Values.nodeSelector | indent 8 }}
    {{- end }}

うん、k8sのDeploymentだ どうやら、Goの標準ライブラリに組み込まれてるtemplateの記法らしい https://golang.org/pkg/text/template/

templates/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ template "fullname" . }}
  labels:
    app: {{ template "name" . }}
    chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.externalPort }}
      targetPort: {{ .Values.service.internalPort }}
      protocol: TCP
      name: {{ .Values.service.name }}
  selector:
    app: {{ template "name" . }}
    release: {{ .Release.Name }}

templates/ingress.yaml

{{- if .Values.ingress.enabled -}}
{{- $serviceName := include "fullname" . -}}
{{- $servicePort := .Values.service.externalPort -}}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: {{ template "fullname" . }}
  labels:
    app: {{ template "name" . }}
    chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
  annotations:
    {{- range $key, $value := .Values.ingress.annotations }}
      {{ $key }}: {{ $value | quote }}
    {{- end }}
spec:
  rules:
    {{- range $host := .Values.ingress.hosts }}
    - host: {{ $host }}
      http:
        paths:
          - path: /
            backend:
              serviceName: {{ $serviceName }}
              servicePort: {{ $servicePort }}
    {{- end -}}
  {{- if .Values.ingress.tls }}
  tls:
{{ toYaml .Values.ingress.tls | indent 4 }}
  {{- end -}}
{{- end -}}

あー、ingress作るかとか、template側で制御してんのかぁ んー、この思想イケてないな 設定ファイルみたいなのつくって、どのtemplateを使うかとか、ansible的な思想の方がイケてる configurationとtemplateは分けたほうが設計的に綺麗

templates/_helper.tpl

{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}}
{{- define "fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

どうやら、railsでいうhelper的な物をtemplate内で定義してるくさい(service.yamlとかで template “fullname” . とかしてる) これもまた、イケてないなー まず新しいDSLが登場するのがいけてない せめて、YAMLかGoで書けるようになっていてほしい

次回

次回は、↑このchartをいじり倒してみよう valuesは外から上書きした時にどうなるか、、、 特にdeep mergeされるのかが、気になる (ansibleはここがイケてない)

Helm(Kubernetes) vol.2

前回

straitwalk.hatenablog.com

TODO

  • [ ] Helm上の概念の理解
  • [ ] 独自Chartsを書いてみる
  • [ ] PrivateRepositoryは使えるのか?
    • 独自repositoryを作る

Helm上の概念の理解

~ ❯ helm ls
NAME            REVISION        UPDATED                         STATUS          CHART                           NAMESPACE
errant-hare     1               Sat Mar  3 22:39:16 2018        DEPLOYED        kubernetes-dashboard-0.4.1      default

まずこれだけ見ても

  • Name
  • revision
  • chart

という用語が出て来る

それぞれ理解していくか

用語

用語 説明
Package k8s上にインストールするアプリケーション
Chart パッケージを構成するYAML
Release インストールされたパッケージ
Name -n, --name string release name. If unspecified, it will autogenerate one for you
Revision Releaseをupgradeするとrevisionが上がって履歴管理されるらしい

んー、なんのコッチャ

ライフサイクル

  • 12factor appに則っている
  • ChartをReleaseとして管理している
  • upgradeコマンドでReleaseを作る
    • rollbackコマンドで戻せる
    • historyで履歴を見れる

ふーん。 やってみるか。

dashboardの最新versionは0.4.1っぽいので、0.4.0をinstallしてみる

~ ❯ helm install stable/kubernetes-dashboard -n test-dash --version 0.4.0
~ ❯ helm ls
NAME            REVISION        UPDATED                         STATUS          CHART                           NAMESPACE
test-dash       1               Tue Mar  6 22:35:32 2018        DEPLOYED        kubernetes-dashboard-0.4.0      default

0.4.0がinstall出来た。nameも指定できた。

んじゃupgradeしてみよう。

> helm upgrade test-dash stable/kubernetes-dashboard --version 0.4.1
Error: UPGRADE FAILED: Deployment.apps "test-dash-kubernetes-dashboard" is invalid: spec.template.metadata.labels: Invalid value: map[string]string{"release":"test-dash", "app":"kubernetes-dashboard", "chart":"kubernetes-dashboard-0.4.1", "heritage":"Tiller", "kubernetes.io/cluster-service":"true"}: `selector` does not match template `labels`

( ゚д゚)ハッ! 失敗

ん?

version指定を消してみる、、、うまくいった。 0.4.0に戻してみたり、再度0.4.1にしてみたり。

0.4.0 -> 0.4.1(失敗) -> 0.4.1(成功) -> 0.4.0(成功) -> 0.4.1(失敗) -> 0.4.1(成功)

何故か2回やると上手くいく… なんかバグ踏んだ気がする

~ ❯ helm ls
NAME            REVISION        UPDATED                         STATUS          CHART                           NAMESPACE
test-dash       7               Tue Mar  6 22:43:31 2018        DEPLOYED        kubernetes-dashboard-0.4.1      default

~ ❯ helm history test-dash
REVISION        UPDATED                         STATUS          CHART                           DESCRIPTION
1               Tue Mar  6 22:35:32 2018        SUPERSEDED      kubernetes-dashboard-0.4.0      Install complete
2               Tue Mar  6 22:38:54 2018        SUPERSEDED      kubernetes-dashboard-0.4.1      Upgrade "test-dash" failed: Deployment.apps "test-dash-ku...
3               Tue Mar  6 22:40:28 2018        SUPERSEDED      kubernetes-dashboard-0.4.1      Upgrade complete
4               Tue Mar  6 22:40:49 2018        SUPERSEDED      kubernetes-dashboard-0.4.1      Upgrade complete
5               Tue Mar  6 22:40:55 2018        SUPERSEDED      kubernetes-dashboard-0.4.0      Upgrade complete
6               Tue Mar  6 22:43:03 2018        SUPERSEDED      kubernetes-dashboard-0.4.1      Upgrade "test-dash" failed: Deployment.apps "test-dash-ku...
7               Tue Mar  6 22:43:31 2018        DEPLOYED        kubernetes-dashboard-0.4.1      Upgrade complete

こんな感じ。 なるほどね。

構成

https://docs.helm.sh/architecture/

主な要素は以下の3つ

  • helmコマンド
  • Repository
    • Chartの管理
  • Tiller
    • Chartと設定を使ってreleaseを作る
    • k8sにデプロイする
    • 要はこいつが肝心

ちなみに、それぞれのやり取りは全てgRPCらしい。

なるほどね。

今日はここまで。 次回は自前でChart書いてみるかな。

Helm (Kubernetes) vol.1

環境

  • OS: macOS High Sierra 10.13.3
  • Docker(ForMac): Version 18.03.0-ce-rc1-mac54 (23022)

Helm

そう、helm。 Emacserにはお馴染み、違う。 そのhelmじゃないw

https://docs.helm.sh/

xxx.sh っていうドメイン(・∀・)イイネ!!

Helm is the best way to find, share, and use software built for Kubernetes.

そう、k8s上で動かすソフトウェアをパッケージ管理する為の物

k8sにMySQL入れたり、とかとか、楽にできるようにするやつ。

installation

kubernetes

まずは、兎にも角にもk8sが必要だ。

https://qiita.com/taishin/items/920d62a641c9cd58f289

gcloudコマンド経由でkubectlインストールしてると競合する… ので、先に消しとく

gcloud components remove kubectl
which kubectl

して

/usr/local/bin/kubectl

を参照してればOK

❯ kubectl config get-contexts
CURRENT   NAME                                                                        CLUSTER                                                                     AUTHINFO                                                                    NAMESPACE
          docker-for-desktop                                                          docker-for-desktop-cluster                                                  docker-for-desktop

いっぱいあるーね。 でもちゃんと、GKEのcontextも読めてるのでOK

docker-for-desktop がローカルのやつ。 さぁ、contextを切り替えておこう。

❯ kubectl config use-context docker-for-desktop
Switched to context "docker-for-desktop".
❯ kubectl config get-contexts
CURRENT   NAME                                                                        CLUSTER                                                                     AUTHINFO                                                                    NAMESPACE
*         docker-for-desktop                                                          docker-for-desktop-cluster                                                  docker-for-desktop

OK

helm

今時のツールはdocumentがしっかりしてるし、構築は楽にできるようになってるね。 homebrewで終了。

brew install kubernetes-helm

Helmを使う

Init

まずは、K8sがHelmを使えるようにする為にinitする必要がある。

~ ❯ helm init
$HELM_HOME has been configured at /Users/xxx/.helm.

Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.
Happy Helming!
  • $HELM_HOME が設定される
  • Tiller ってやつがk8sにinstallされたらしい
~ ❯ kubectl get po --all-namespaces
NAMESPACE     NAME                                         READY     STATUS    RESTARTS   AGE
docker        compose-5d4f4d67b6-5d7d8                     1/1       Running   0          20m
docker        compose-api-7bb7b5968f-8fwmw                 1/1       Running   0          20m
kube-system   etcd-docker-for-desktop                      1/1       Running   0          20m
kube-system   kube-apiserver-docker-for-desktop            1/1       Running   0          20m
kube-system   kube-controller-manager-docker-for-desktop   1/1       Running   0          20m
kube-system   kube-dns-6f4fd4bdf-lpjhf                     3/3       Running   0          21m
kube-system   kube-proxy-sf4n4                             1/1       Running   0          21m
kube-system   kube-scheduler-docker-for-desktop            1/1       Running   0          21m
kube-system   tiller-deploy-5bd98cbb68-skrqn               1/1       Running   0          1m

kube-system tiller-deploy-5bd98cbb68-skrqn 1/1 Running 0 1m

↑これ

パッケージを見てみよう

helmではパッケージをChartsと呼ぶ(海図らしい)

~ ❯ helm search
NAME                            VERSION DESCRIPTION
stable/acs-engine-autoscaler    2.1.0   Scales worker nodes within agent pools
stable/artifactory              6.1.0   Universal Repository Manager supporting all maj...
stable/aws-cluster-autoscaler   0.3.1   Scales worker nodes within autoscaling groups.
stable/buildkite                0.2.0   Agent for Buildkite
…

長いので、省略するけど、結構いろいろある。 デフォルトのレポジトリ。

Helm使って何か動かしてみるか

手始めにk8sのダッシュボードを入れてみるかな GKEはデフォルトではいってる

stable/kubernetes-dashboard

これ。

~ ❯ helm inspect stable/kubernetes-dashboard
appVersion: 1.7.1
description: General-purpose web UI for Kubernetes clusters
keywords:
- kubernetes
- dashboard
maintainers:
- email: Kevin.Fox@pnnl.gov
  name: kfox1111
name: kubernetes-dashboard
version: 0.4.1

---
# Default values for kubernetes-dashboard
# This is a YAML-formatted file.
# Declare name/value pairs to be passed into your templates.
# name: value

image: gcr.io/google_containers/kubernetes-dashboard-amd64
imageTag: "v1.7.1"
imagePullPolicy: "IfNotPresent"

nodeSelector: {}

httpPort: 80

serviceType: ClusterIP

resources:
  limits:
    cpu: 100m
    memory: 50Mi
  requests:
    cpu: 100m
    memory: 50Mi

ingress:
  ## If true, Kubernetes Dashboard Ingress will be created.
  ##
  enabled: false

  ## Kubernetes Dashboard Ingress annotations
  ##
  # annotations:
  #   kubernetes.io/ingress.class: nginx
  #   kubernetes.io/tls-acme: 'true'

  ## Kubernetes Dashboard Ingress hostnames
  ## Must be provided if Ingress is enabled
  ##
  # hosts:
  #   - kubernetes-dashboard.domain.com

  ## Kubernetes Dashboard Ingress TLS configuration
  ## Secrets must be manually created in the namespace
  ##
  # tls:
  #   - secretName: kubernetes-dashboard-tls
  #     hosts:
  #       - kubernetes-dashboard.domain.com

rbac:
  ## If true, create & use RBAC resources
  #
  create: false

  ## Ignored if rbac.create is true
  #
  serviceAccountName: default

Default values for kubernetes-dashboard

って書いてある所以下が、パラメータになっていて、 Ansibleの defaults/main.yml みたいな感じで、設定を外から渡せるようになっている。

ひとまず、そのまま立ち上げてみよう。

~ ❯ helm install stable/kubernetes-dashboard
NAME:   brown-whippet
LAST DEPLOYED: Sat Mar  3 22:26:48 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1beta1/Deployment
NAME                                DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
brown-whippet-kubernetes-dashboard  1        1        1           0          0s

==> v1/Service
NAME                                CLUSTER-IP     EXTERNAL-IP  PORT(S)  AGE
brown-whippet-kubernetes-dashboard  10.107.90.189  <none>       80/TCP   0s


NOTES:
*********************************************************************************
*** PLEASE BE PATIENT: kubernetes-dashboard may take a few minutes to install ***
*********************************************************************************

Get the Kubernetes Dashboard URL by running:
  export POD_NAME=$(kubectl get pods -n default -l "app=kubernetes-dashboard,release=brown-whippet" -o jsonpath="{.items[0].metadata.name}")
  echo http://127.0.0.1:9090/
  kubectl -n default port-forward $POD_NAME 9090:9090

~ ❯ helm ls
NAME            REVISION        UPDATED                         STATUS          CHART                           NAMESPACE
brown-whippet   1               Sat Mar  3 22:26:48 2018        DEPLOYED        kubernetes-dashboard-0.4.1      default

瞬殺。

~ ❯ kubectl get po
NAME                                                  READY     STATUS    RESTARTS   AGE
brown-whippet-kubernetes-dashboard-6c546f54f8-tmccq   1/1       Running   0          3m

ちゃんと動いてる じゃあ、画面見てみるか。

Get the Kubernetes Dashboard URL by running:
  export POD_NAME=$(kubectl get pods -n default -l "app=kubernetes-dashboard,release=brown-whippet" -o jsonpath="{.items[0].metadata.name}")
  echo http://127.0.0.1:9090/
  kubectl -n default port-forward $POD_NAME 9090:9090

丁寧に書いてくれてるね。

http://localhost:9090 でアクセスできる。

てか、なんか大分いろいろ見やすくなってる… 進化してるな。 こんどモニタリングでdashboard使ってみよう。

installしたChartsを落とす

helm delete

してみる。

~ ❯ helm delete stable/kubernetes-dashboard
Error: invalid release name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 53

Charts名じゃ消せないらしい。 デプロイしたChartsに対してnameが振られているので、そっちで消す。 (同じChartsでもname分けて複数デプロイできるようになっている)

~ ❯ helm ls
NAME            REVISION        UPDATED                         STATUS          CHART                           NAMESPACE
brown-whippet   1               Sat Mar  3 22:26:48 2018        DEPLOYED        kubernetes-dashboard-0.4.1      default
~ ❯ helm delete brown-whippet
release "brown-whippet" deleted
~ ❯ helm ls
~ ❯ kubectl get po
No resources found.

消えました。

今日はここまで。

TODO

  • 独自Chartsを書いてみる
  • PrivateRepositoryは使えるのか?
    • 独自repositoryを作る
  • etc…

Aerospikeを試す #2 HeartBeat & ReverseProxy

前回から引き続き、Aerospikeネタ。 前回はheartbeatのmesh-seed-address-portに1つのインスタンスを指定しましたが、これが1個だとSPOFになってしまう。 mesh-seed-address-port複数指定できるっぽいので、いくつか指定しておけば良いのだろうけど、それはそれでめんどくさい。 インスタンスの追加・削除時に設定を撒かないといけないし。 というわけで、ReverseProxy挟んだらいけんじゃね?という仮説を検証してみようと思ったので、やってみました。 (どうやるのが正解なんだろうか?)

追記

やっぱりこの方法ダメっぽい。 Cluster Visibilityがずっとfalseだったりとか Repl'n Factor1のまま(2に設定しているのに)だったりする。

やっぱり公式に書いてある通り mesh-seed-address-port複数設定するのが正解なんだろうか。

今日のテーマ

  • Proxyサーバを立てる
  • heartbeatの向き先を変える
  • AMCから確認する

以上

環境

  • Mac OSX 10.10.3
  • Vagrant 1.7.2
  • Aerospike 3.5.14

Proxyサーバを立てる

前回同様vagrantインスタンスを立てる。 今回は超単純なReverseProxyができれば良いのでHAProxyを使います。

Vagarntfileの修正

...
  config.vm.define "proxy" do |server|
    server.vm.box = "proxy-base"
    server.vm.box_url = "http://developer.nrel.gov/downloads/vagrant-boxes/CentOS-6.4-x86_64-v20130731.box"
    server.vm.network :private_network, ip: "192.168.33.200"
  end
...
  • OSは特になんでも良い
  • VMインスタンス間で通信したいので、private_networkでprivate ipを固定しておく

インスタンス起動

vagrant up proxy

HAProxyのインストール&設定

まずはインストール

vagrant ssh proxy
sudo yum install -y haproxy

したら、設定を書き換える

...
frontend  forward_mesh *:3002
    mode tcp
    default_backend             aerospike_mesh

backend aerospike_mesh
    balance     roundrobin
    server  node1 192.168.33.101:3002 check
    server  node2 192.168.33.102:3002 check
...

3002ポートをroundrobinで前回立てたインスタンスに振り分けます

HAProxyを再起動

sudo /etc/init.d/haproxy restart

heartbeatの向き先を変える

以下、node1``node2両方に行います

vagrant ssh node1
sudo vi /etc/aerospike/aerospike.conf
network {
...
        heartbeat {
                mode mesh
                port 3002
                address 192.168.33.101
                mesh-seed-address-port 192.168.33.200 3002

                interval 5
                timeout 10
        }
...
}

mesh-seed-address-portproxyサーバのアドレスに変更する

aerospikeも再起動

sudo /etc/init.d/aerospike restart

AMCから確認してみる

http://localhost:8081

にアクセスします。

img

無事、2台いることが確認できました。 めでたしめでたし(?)

on EC2にするときは、PrivateELB的な物を立てるんだろうか。

けど、なんかAMCを見てるとRepilicationがうまくいって無い気がする... もうちょい調査する。

まとめ

  • heartbeatはreverse proxy通してもいけたけど、なんか怪しい
  • 次回以降
    • プログラムからCRUD
    • UDF

Aerospikeを試す #1 環境構築

最近ちょくちょく見るようになったAerospikeです。 OSSになってからほんとよく見かけますが、あんまり日本語の情報が多くないですね。

WEB+DB PRESS Vol.87

WEB+DB PRESS Vol.87

これのAerospike入門を読んで触発されて触ってみることにしたので、 環境構築からやってみたいなと思います。

今日のテーマ

  • VagrantでAerospike謹製のboxを起動する
  • AMC (Aerospike Management Console)を見てみる
  • 2台のクラスター環境を構築する

環境

VagrantでAerospike謹製のboxを起動する

http://www.aerospike.com/docs/operations/install/vagrant/mac/

ここに書いてある通りなんですけど、今回は2台でクラスタ環境つくりたいので、 vagrant initした後にちょいと書き換えます

vagrant init aerospike/centos-6.5

してVagrantfileをこんな感じに

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure(2) do |config|
  aerospike_box = "aerospike/centos-6.5"

  config.vm.define "node1" do |server|
    server.vm.box = aerospike_box
    server.vm.network :private_network, ip: "192.168.33.101"
    server.vm.network "forwarded_port", guest: 8081, host: 8081
    server.vm.network "forwarded_port", guest: 3000, host: 3000
  end

  config.vm.define "node2" do |server|
    server.vm.box = aerospike_box
    server.vm.network :private_network, ip: "192.168.33.102"
  end
end
  • ゲスト間通信したいので、private_networkでPrivateIPをふる
  • AMCにアクセスしたいのでnode18081ポートをフォワーディング
  • Aerospikeにホストからアクセスしたいので、node13000ポートをフォワーディング

では起動します。

vagrant up

起動したら、中に入って、ちょっと確認してみます。

$ vagrant ssh node1
$ asmonitor -e info
...
1 hosts in cluster: 127.0.0.1:3000
...

てなのが出てればとりあえずOKかな。

AMCを見てみる

Aerospike Management Consoleを見てみます。

この為に8081ポートでアクセスできるようにしたので、下記をブラウザで開きます。

http://localhost:8081/

なんかかっこいい画面が出てきましたね。

  • Disk Usage
  • RAM Usage
  • Cluster Summary
  • Cluster Throughput
  • etc ...

これは良い感じですね。 マジ運用で使う場合は、踏み台経由のポートフォワーディングとかだけ見れるようにしとけば楽そう。

2台のクラスター環境を構築する

さて、ここからが今回のメインです。 クラスター環境を構築してみます。 (今回はAWSではないですが、最終的にAWSに構築する事を前提にしているので、multicastは使わず、meshでやります)

それぞれ以下のようにconfを書き換え

  • node1
network {
        service {
                address any
                port 3000
                access-address 192.168.33.101
                network-interface-name eth1
        }

        heartbeat {
                mode mesh
                port 3002
                address 192.168.33.101
                mesh-seed-address-port 192.168.33.101 3002

                interval 5
                timeout 10
        }

        fabric {
                port 3001
        }

        info {
                port 3003
        }
}
  • node2
network {
        service {
                address any
                port 3000
                access-address 192.168.33.102
                network-interface-name eth1
        }

        heartbeat {
                mode mesh
                port 3002
                address 192.168.33.102
                mesh-seed-address-port 192.168.33.101 3002

                interval 5
                timeout 10
        }

        fabric {
                port 3001
        }

        info {
                port 3003
        }
}

ポイントは - service - access-adressをVagrantfileで指定したIPアドレスに書き換える - network-interface-nameeth1にする - heartbeat - modemeshにする - addressをVagrantfileで指定したIPアドレスにする - mesh-seed-address-portnode1を見るようにする(2台とも) - ここは、両方のconfに両方のIP書いても大丈夫でした

network-interface-nameを変えないといけないことに気づくのにめっちゃ時間かかった...

[vagrant@localhost ~]$ ifconfig
eth0      Link encap:Ethernet  HWaddr 08:00:27:78:30:4D  
          inet addr:10.0.2.15  Bcast:10.0.2.255  Mask:255.255.255.0
          inet6 addr: fe80::a00:27ff:fe78:304d/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:3323 errors:0 dropped:0 overruns:0 frame:0
          TX packets:2423 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:267581 (261.3 KiB)  TX bytes:579927 (566.3 KiB)

eth1      Link encap:Ethernet  HWaddr 08:00:27:CA:31:8D  
          inet addr:192.168.33.102  Bcast:192.168.33.255  Mask:255.255.255.0
          inet6 addr: fe80::a00:27ff:feca:318d/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:27960681 errors:0 dropped:0 overruns:0 frame:0
          TX packets:27846070 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:5863570470 (5.4 GiB)  TX bytes:5871987364 (5.4 GiB)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:7999 errors:0 dropped:0 overruns:0 frame:0
          TX packets:7999 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:5992988 (5.7 MiB)  TX bytes:5992988 (5.7 MiB)

両方再起動して確認

$ sudo /etc/init.d/aerospike restart
$ asmonitor -e info

Enter help for commands

2 hosts in cluster: 192.168.33.101:3000,192.168.33.102:3000
===NODES===
2015-07-10 00:29:19.964079
Sorting by IP, in Ascending order: 
ip:port                 Build   Cluster      Cluster   Free   Free   Migrates              Node         Principal   Replicated    Sys
                            .      Size   Visibility   Disk    Mem          .                ID                ID      Objects   Free
                            .         .            .    pct    pct          .                 .                 .            .    Mem
192.168.33.101:3000    3.5.14         2         true     98     98   (1961,1)   BB98262CE270008   BB98D31CA270008      197,932     87
192.168.33.102:3000    3.5.14         2         true     98     95   (2008,1)   BB98D31CA270008   BB98D31CA270008      197,932     86
Number of nodes displayed: 2


 ===NAMESPACE===
Total (unique) objects in cluster for test : 197,932
Note: Total (unique) objects is an under estimate if migrations are in progress.


ip/namespace           Avail   Evicted    Master     Repl     Stop      Used   Used      Used   Used    hwm   hwm
                         Pct   Objects   Objects   Factor   Writes      Disk   Disk       Mem    Mem   Disk   Mem
                           .         .         .        .        .         .      %         .      %      .     .
192.168.33.101/test       98         0   101,982        2    false   84.42 M      2   36.89 M      2     50    60
192.168.33.102/test       98         0    95,950        2    false   84.42 M      2   84.23 M      5     50    60
Number of rows displayed: 2

2 hosts in cluster: 192.168.33.101:3000,192.168.33.102:3000 2台のクラスタになっていることがわかりますね

AMCから見てみる

img

img

ちゃんと2台いることがわかりますね。

まとめ

  • Aerospikeは手軽にクラスタ環境構築できる
  • AMC便利
  • meshで使う場合にmesh-seed-address-portが1個だとSPOFになってしまう気がする
    • どうするんだろうか?
    • ReverseProxyかませてRoundRobinで数台にしたりするんだろうか
  • 次回以降
    • ReverseProxyかませてみる
    • プログラムからCRUD
    • AQL
    • UDF

Rubiniusを試す #1 (Install,Benchmark)

だいぶ久しぶりの投稿です。

ちょいとふと思い立ってやってみたので、メモ。

Macにrbenv(ruby-build)でRubiniusをインストールする。

Rubiniusとは

http://rubini.us/doc/ja/what-is-rubinius/

Rubinius は プログラミング言語 Ruby の実装です。 Rubiniusには、バイトコード仮想マシンRuby構文解析器、バイトコードコンパイラ、世代別ガベージコレクタ、ジャストインタイム (JIT) ネイティブマシンコードコンパイラ、そして、Ruby のコアライブラリと標準ライブラリが含まれています。 Rubinius は Ruby 2.1.0 を目標としています。

とのこと。

https://github.com/rubinius/rubinius/tree/master/kernel/bootstrap 読みやすい(Rubyで実装されている)

昔見た時は「全部Rubyで書いてある」って聞いた気がするんだけどVMC++で書かれてるな。

といっても、当時ちゃんと知らなかったので、何か変わったのか、変わってないのかは、わからない。

一番の特徴はスレッドがGVL(Giant VM Lock)の制約を受けない為、マルチコアが活かせること。

つまり、PumaやSidekiqのようにマルチスレッドで動作する場合に優位性があるのではないかと思う。

環境

インストールマシン

  • OS: Mac OSX 10.10.3
  • CPU: 3.1 GHz Intel Core i7
  • Memory: 16 GB 1867 MHz DDR3

Rubinius

  • version: 2.5.7

Requirements

事前に必要なモノを入れておく

といっても、多分LLVMだけ入れておけば良い。

が、Homebrewで最新のLLVMを入れると3.6が入ってしまう。

Rubinius(rbx-2.5.7)はLLVM3.0-3.5じゃないと怒られてしまうので、

LLVM3.5を入れる(インストールしなくてもバイナリがあれば良い)

もしかしたら

gem install rubinius-melbourne

しといた方がよいのかも

ruby-buildを最新にする

※以下、ruby-buildbrewでinstallしている場合

ruby-buildのFormulaがまだ、2.5.7が入ってるrevisionを見ていないので、強制的にmasterのHEADを向ける

brew upgrade --HEAD ruby-build

Installation

さて、やっとインストールするが、ちょいと環境変数が多くなるので、整理

LDFLAGS=-L{解凍先}/lib \
CPPFLAGS=-I{解凍先}/include \
CC={解凍先}/clang \
CXX={解凍先}/clang++ \
RUBY_CONFIGURE_OPTS="--llvm-config={解凍先}/bin/llvm-config" \
rbenv install -v rbx-2.5.7

こんな感じで多分行ける

試してみる

❯ rbenv versions
  system
  2.0.0-p353
  2.2.1
* 2.2.2
  rbx-2.5.7
rbenv local rbx-2.5.7
❯ ruby -v
rubinius 2.5.7 (2.1.0 d4fdeaad 2015-06-28 3.5.2 JI) [x86_64-darwin14.3.0]

ベンチってみる

簡単なfibonacciの関数書いて試してみる

require 'benchmark'

def fib(n)
  if n == 0
    0
  elsif n == 1
    1
  else
    fib(n-1) + fib(n-2)
  end
end

Benchmark.bmbm do |x|
  x.report("fibonacci") { fib(40) }
end

負荷がかかれば何でも良いので、中身はあまり気にしない

Ruby 2.2.2

❯ ruby fib.rb
Rehearsal ---------------------------------------------
fibonacci  20.300000   0.050000  20.350000 ( 21.297663)
----------------------------------- total: 20.350000sec

                user     system      total        real
fibonacci  20.620000   0.060000  20.680000 ( 21.640249)

Rubinius 2.5.7

❯ ruby fib.rb
Rehearsal ---------------------------------------------
fibonacci   7.426329   0.117764   7.544093 (  7.756742)
------------------------------------ total: 7.544093sec

                user     system      total        real
fibonacci   5.223988   0.064434   5.288422 (  6.797919)

感想

  • パフォーマンス的には以外と差がでた
    • ただ、実行する度に割りと速度にばらつきがある
      • 5s〜10s
    • 原因までは追ってない
  • 厳密にマルチスレッドになると、ちゃんとマルチスレッドプログラミングできてないと危険
  • Rubiniusの運用ノウハウが無いので本番で使うことはきっと無いだろう
    • 日本で使ってるところあるんだろうか?
  • 最近も開発は割りと進んでいるようなので、もうすこしいろいろやってみたい

Goでテストを書く

久しぶりにちゃんとgolangを勉強していこうという事で、ログを残します。

今日のテーマ

  • testingパッケージを使ってテストを書いてみる
  • gomgospelをインストール
  • gospelでテスト書いてみる
  • 独自matcherを書いてみる

環境

構成

$ tree .
.
├── Gomfile
├── src
│   └── model
│       ├── user.go
│       ├── user_gospel_test.go
│       └── user_test.go
└── vendor

testingパッケージを使ってテストを書いてみる

まずは、標準のお作法にしたがってテストを書いてみます。

テスト対象はこんな感じです。

user.go

package model

import (
    "strings"
)

type User struct {
    FirstName string
    LastName string
    Divisions []string
}

func NewUser(name string) *User {
    names := strings.Split(name, " ")
    if len(names) >= 2 {
        return &User{FirstName: names[0], LastName: names[1]}
    } else {
        return &User{FirstName: names[0]}
    }
}

func (user *User) FullName() string{
    return user.FirstName + " " + user.LastName
}

func (user *User) AddDivision(division string) *User{
    user.Divisions = append(user.Divisions, division)
    return user
}

さて、テストを書いてみます。

user_test.go

package model

import (
    "testing"
)

func TestConstructorWithFullname(t *testing.T) {

    user := NewUser("hoge aaa")

    if user.FirstName != "hoge" {
        t.Error("user's first name should be hoge")
    }

    if user.LastName != "aaa" {
        t.Error("user's last name should be aaa")
    }

}

func TestConstructorWithFirstName(t *testing.T) {

    user := NewUser("hoge")

    if user.FirstName != "hoge" {
        t.Error("user's first name should be hoge")
    }

    if user.LastName != "" {
        t.Error("user's last name should be empty")
    }

}

func TestConstructorWithEmptyString(t *testing.T) {

    user := NewUser("")

    if user.FirstName != "" {
        t.Error("user's first name should be empty")
    }
    if user.LastName != "" {
        t.Error("user's last name should be empty")
    }

}

func TestDevision(t *testing.T) {
 
    user := NewUser("hoge aaa")

    if len(user.Divisions) != 0 {
        t.Error("default divisions is empty slice")
    }

}

func TestFullName(t *testing.T) {

    fullname := "hoge aaa"
    user := NewUser(fullname)

    if user.FullName() != fullname {
        t.Errorf("fullname should be %s, but %s", fullname, user.FullName())
    }
}

func TestAddDevision(t *testing.T) {
 
    user := NewUser("hoge aaa")
    division := "test"

    user.AddDivision(division)
    if user.Divisions[0] != division {
        t.Log(user.Divisions)
        t.Errorf("%s division was not added", division)
    }

}
  • xxx_test.goというファイル名にする
  • テスト関数はTestというprefixをつける
  • アサーションは自分でifなどでチェックしてt.Errorでfailさせる

testingにはアサートしてくれる関数がありません。 その辺の考え方的には

Go の Test に対する考え方 - Qiita

あたりを見るとよく分かります。 ふむ。

では、テスト実行してみます。

まずは、GOPATHをカレントディレクトリにしておきます。

$ export GOPATH=`pwd`

したら、実行します。

$ go test -v ./src/...
=== RUN TestConstructorWithFullname
--- PASS: TestConstructorWithFullname (0.00 seconds)
=== RUN TestConstructorWithFirstName
--- PASS: TestConstructorWithFirstName (0.00 seconds)
=== RUN TestConstructorWithEmptyString
--- PASS: TestConstructorWithEmptyString (0.00 seconds)
=== RUN TestDevision
--- PASS: TestDevision (0.00 seconds)
=== RUN TestFullName
--- PASS: TestFullName (0.00 seconds)
=== RUN TestAddDevision
--- PASS: TestAddDevision (0.00 seconds)
PASS
ok      model   0.010s

全部通りました。

gomgospelをインストール

依存パッケージはやっぱり設定ファイルとかで管理したいので、今回はgomを使ってみます。 (gondlerの方がスタンダードなのかな?依存パッケージのバージョンとか、あまり管理しないのかな?)

まずはgomをインストール

ここではgo getつかいます。

$ go get github.com/mattn/gom

Gomfileを書く

gom 'github.com/r7kamura/gospel', :commit => 'd575dd12c2eb84612ae5c84fab56ccb4ce156a1e'

Rubyっぽい... mattn/gom · GitHub とか見てもほぼGemfileですね。 tagも使えるし、groupも使える。

じゃあ、こんなのもいけるのか??

gom 'github.com/r7kamura/gospel', commit: 'd575dd12c2eb84612ae5c84fab56ccb4ce156a1e'

$ gom install
gom:  Syntax Error at line 1

怒られました... orz

気を取り直して、Gomfileを元に戻してインストールしなおします。

$ gom install
downloading github.com/r7kamura/gospel

無事完了。

gospelでテスト書いてみる

では、使い方を参考に書いてみます。

user_gospel_test.go

package model

import (
    . "github.com/r7kamura/gospel"
    "testing"
)

func TestUser(t *testing.T) {

    Describe(t, "NewUser", func() {

        Context("フルネーム(hoge aaa)を指定した場合", func() {
            user := NewUser("hoge aaa")

            It("FirstName should be hoge", func() {
                Expect(user.FirstName).To(Equal, "hoge")
            })

            It("LastName should be aaa", func() {
                Expect(user.LastName).To(Equal, "aaa")
            })

        })

        Context("空文字を渡した場合", func() {
            user := NewUser("")

            It("FirstName should be empty string", func() {
                Expect(user.FirstName).To(Equal, "")
            })

            It("LastName should be empty string", func() {
                Expect(user.LastName).To(Equal, "")
            })

        })

    })
    Describe(t, "Divisions", func() {
        user := NewUser("")

        It("default divisions is empty slice", func() {
            Expect(len(user.Divisions)).To(Equal, 0)
        })

    })

}

Rspec風に書けてBDDな感じで書きやすいですね。

実行する時はgom exec使います。 bundle execみたいな。

$ gom exec go test -v ./src/...
=== RUN TestUser
NewUser
  フルネーム(hoge aaa)を指定した場合
    FirstName should be hoge
    LastName should be aaa
  空文字を渡した場合
    FirstName should be empty string
    LastName should be empty string
Divisions
  default divisions is empty slice
PASS
ok      model   0.011s

※ 急に日本語で書いたのは気にしない ※ だいぶテストケースが減ってるのも気にしない

結果も見やすくて良い感じです。 (ちなみに、Describeをネストするとエラーになりました)

独自matcherを書いてみる

スライスとか配列のlengthをチェックするmatcherとかあっても良いかなぁという気がしていて、 この辺とか見ていたら、 自分で書けそうなので、書いてみました。

package model

import (
    . "github.com/r7kamura/gospel"
    "testing"
    "fmt"
    "reflect"
)

func EqualLength(values ...interface{}) (failureMessage string) {
    actualValue := reflect.ValueOf(values[0])

    if actualValue.Kind() != reflect.Slice && actualValue.Kind() != reflect.Array {
        failureMessage = fmt.Sprintf("`%v` is not slice or array", values[0])
        return
    }

    if actualValue.Len() != values[1] {
        failureMessage = fmt.Sprintf("Expected `%v` length `%v` to equal `%v`", values[0], actualValue.Len(), values[1])
    }
    return
}

func TestUser(t *testing.T) {

    Describe(t, "Divisions", func() {
        user := NewUser("")

        It("default divisions is empty slice", func() {
            Expect(user.Divisions).To(EqualLength, 0)
        })

    })

}

reflectパッケージ使って、TypeがSliceかArrayならlengthチェックするみたいな事をしています。

ためしに、

It("length", func() {
    Expect([]string{"hoge"}).To(EqualLength, 0)
})
It("length", func() {
    Expect([1]string{"hoge"}).To(EqualLength, 0)
})
It("length", func() {
    Expect(100).To(EqualLength, 0)
})

こんなんでfailさせてみます。

=== RUN TestUser
Divisions
  length
  Expected `[hoge]` length `1` to equal `0`
  /Users/natsuki/workspace/go-samples/test/unit-test/src/model/user_gospel_test.go:63
    62.    It("length", func() {
    63.      Expect([]string{"hoge"}).To(EqualLength, 0)
    64.    })
  length
  Expected `[hoge]` length `1` to equal `0`
  /Users/natsuki/workspace/go-samples/test/unit-test/src/model/user_gospel_test.go:66
    65.    It("length", func() {
    66.      Expect([1]string{"hoge"}).To(EqualLength, 0)
    67.    })
  length
  `100` is not slice or array
  /Users/natsuki/workspace/go-samples/test/unit-test/src/model/user_gospel_test.go:69
    68.    It("length", func() {
    69.      Expect(100).To(EqualLength, 0)
    70.    })
--- FAIL: TestUser (0.00 seconds)
FAIL
exit status 1
FAIL    model   0.009s
gom:  exit status 1

おぉ、悪くないかも。

まとめ

  • golangはテストを大事にしている
  • テストコードとディレクトリを分けない文化はめずらしい気がした
    • これもテスト重要っていう文化故になのか
  • GomfileはRubyっぽいだけでRubyではない(?)
  • go testベンチマークも取れるし、カバレッジも取れるっぽい
    • go test -hでいろいろ出てくる
    • -coverオプション付けるとなんか色々足りないって('go get`しろって)怒られる
  • gospelのMatcherは結構簡単に書ける