42
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RE:CODEAdvent Calendar 2019

Day 25

ClickHouseとは【2020年3月最新】【サンプルあり】

Last updated at Posted at 2020-01-02
スクリーンショット 2020-01-02 16.09.11.png

ClickHouseの使い方についてまとめました!
これを読めば運用できるはずです。
ClickHouseは、主に集計などに最適化されたDBで、容量削減と高速化が期待できます。
書く内容は、「dockercompose,migration,go-clientサンプル」「テーブルの作成テクニック」「パーティション、ORDER BY」「レプリケーション」などを載せます。

2020/01最新

ネットに上がってる記事では、ClickHouseはupdateできないと書いてありますが、updateできるようになってます。これによってテーブルの持ち方などが変わります。

ClickHouse概要

・集計など、大量のデータをreadする処理に最適化されたDBです。
・トランザクションなどを使う処理は苦手です。
・インサート時に「sumやavgなどをその時に計算」してくれる特殊なテーブルなどもある。
・mysqlなどと違い、カラムごとで列でデータを保存するので、同じデータが重複してると、めっちゃ圧縮してくれる。
・コスパ最強。awsなら、EC2とかで動かせば良い。
・マルチマスター形式で、データのシャーディングもできる。
・mysqlとほぼ同じ構文。
・集計するなら、mysqlで同じことをやろうとするとclickhouseの方が圧倒的に早い。

ベンチマーク

・ClickHouse比較用ベンチマーク

容量削減

1/10ぐらいになると言われています。
ただ、データによります。
完全にランダムな文字列が連続するデータではほぼ圧縮されないです。
逆に、似通ったデータや空が多いデータはめっちゃ圧縮される。

メモリとCPUはどっちを増やせば高速になる?

クエリの実行速度をあげたいときは、cpuコア数を増やします。
複数コアで並列に実行してくれるため、高速になります。
メモリ(RAM)は、「同時のクエリ呼び出し数」に比例して増やすと良いです。
また、データキャッシュもされているので、同じデータを連続で叩くこと多い場合はメモリを増やすと効果的です。

テーブル作成

ClickHouseは適切にテーブル作成をする必要があります。
検索高速化のためテーブル作成を以下に書きます。

ORDER BY(重要)

CREATE TABLE時にORDER BYを指定できます。
「order by」には、「group by」で使うカラムを指定すると、高速化することができます。
データインサート時に、指定したカラムで順にソートされて保存されます。

CREATE TABLE テーブル名 (
     id String,
     group_id String,
     name String,
     date Date
)
ENGINE = MergeTree()
ORDER BY (date, id, group_id); 

上のように作成すると、「date, id, group_id」順でデータがソートされた状態でデータが保存されます。
主に、select時の「group by」がめっちゃ高速になります。

PARTITION BY

「partition by」は、高速化のために使うものでは無いです。
削除が高速にできます。
一般的な用途では、「データを入れ直したい単位(日付など)」などでパーティションを切っておけば良いです。
https://clickhouse.yandex/docs/en/query_language/alter/#alter_manipulations-with-partitions

CREATE TABLE テーブル名 (
     a_id String,
     b_id String,
     date Date
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(date)
ORDER BY (date, a_id, b_id); 

マージテーブルとは

マージテーブルは、複数のテーブルに対してクエリを実行できるテーブルです。
正規表現で検索して全テーブルから検索します。
updateができなかった頃は、日別にテーブルを作成して、マージテーブルで検索していましたが、今はupdateができるので、なかなかこのテーブルは使わないはずです。

マージテーブルの注意

「そのDBに含まれるテーブル数」が増えるにつれて、検索速度が落ちます。
(正規表現で検索しているため。思ったよりも速度落ちる。)

テーブル1個辺り、めっちゃメモリ(RAM)を使うので注意

基本的にはテーブル分割でなく「order by」を使えばOK。
データが増えすぎてきたら、cpuコア数を増やすorサーバを増やす。

各言語のClickHouseクライアントライブラリ

以下にまとまっています。
https://clickhouse.yandex/docs/en/interfaces/third-party/client_libraries/

レプリーケーションとシャーディング

zookeeperと一緒に使う。
データを複製しておき、一つのサーバが落ちたら、もう一つのサーバを見に行くには、
レプリケーションテーブル」と「分散テーブル」を使えばOK。

・レプリケーションテーブルとは
データを各サーバに「分割」or「複製」することができるテーブル。
・分散テーブルとは
各サーバのテーブルを跨いで操作することができるテーブル。

シャードとは

分散させるときに、シャードという言葉が出てきます。
具体例を挙げると、
シャード1に「サーバA、サーバB」
シャード2に「サーバC、サーバD」
シャード3に「サーバE、サーバF」
clickqiita.jpg

以上の構成だった場合、
「サーバA」「サーバB」には、「同じく複製されたデータ」が入ります。
「サーバA」「サーバC」「サーバE」には、分割されたデータが入ります。
このようにデータの分割する単位がシャードだと思います。

実際に試しにデータインサートしてみたら、
うまい具合にシャードごとに分散された状態になりました。
dockerのサンプルも数日後に載せます。

フェイルオーバー

複数サーバの内、「一つが落ちた場合」に「生きてるサーバに行くようにする」には、
レプリケーションしたサーバを、ALBやHAProxyなどを使って見に行くようにする。
上の図で言うと、シャード1のClickhouseAが死んだらClickHouseBをみに行くようにする。
実際には、マルチマスターなので、どれか生きてるサーバにアクセスするようにすれば、ClickHouseがzookeeperと連動して自動的にみに行ってくれる。

テーブルデータタイプ

mysqlなどとほぼ同じの型があります。
一覧はここにあります。

気をつけるとこと

・nullはなるべく使わない。
・mysqlのvarchar型と、ClickHouseのFixedStringとはちょっと挙動が違うので、stringで入れる。
(FixedString(10)は、10文字キッカリ入れないといけない。検索するなら、ヌル文字を入れて検索しないといけへん)
・Bool型は、int8型で扱う。

clickhouse環境をローカルで作成する。

以下のファイルを用意します。

docker-compose.yml
version: "3.7"
services:
  clickhouse:
    image: yandex/clickhouse-server
    container_name: "clickhouse"
    ports:
      - 8129:8129
      - 9000:9000

以下のコマンドでclickhouseクライアントを立ち上げます。

// コンテナ起動する
docker-compose up -d
// コンテナに入る
docker-compose exec clickhouse bash
// コンテナに入ったらクライアントに入る。
clickhouse-client

以下のようにクエリを叩けます。
スクリーンショット 2020-01-17 20.59.17.png

マイグレーション

今回はこのライブラリを使用しました。
・golang-migrate

以下の3ファイル+1フォルダを作成します。

・docker-compose.yml

docker-compose.yml
version: "3.7"
services:
  ch-migrate:
    build:
      context: ./
      dockerfile: Dockerfile
    container_name: "ch_migrate"
    volumes:
      - ./migrations:/src/migrations
      - ./Makefile:/src/Makefile
    tty: true

・Dockerfile

FROM golang:1.13-alpine

ENV VERSION v4.7.1

RUN apk add make vim --no-cache git

RUN go get -v -d github.com/golang-migrate/migrate/cli \
    && go get -v -d github.com/lib/pq

WORKDIR /go/src/github.com/golang-migrate/migrate

RUN git checkout ${VERSION} \
    && go build -tags 'clickhouse' -o ./bin/migrate ./cli

WORKDIR /src

・migrationsフォルダの作成
(ここに1ファイルずつマイグレーションのファイルが作成される。)

mkdir migrations

・Makefile

PATH := ./migrations/
HOST := clickhouse
PORT := 9000
DATABSE := default
DATABASE_URL := 'clickhouse://${HOST}:${PORT}/${DATABSE}'
EXT := sql
MIGRATE := /go/src/github.com/golang-migrate/migrate/bin/migrate

# コマンド
## make create NAME=create_table_name
.PHONY: create
create:
	${MIGRATE} create -ext ${EXT} -dir ${PATH} ${NAME}

.PHONY: up
up:
	${MIGRATE} -database ${DATABASE_URL} -path ${PATH} up

.PHONY: down
down:
	${MIGRATE} -database ${DATABASE_URL} -path ${PATH} down

コンテナの中に入ります。

docker-compose up -d
docker-compose exec ch-migrate ash

以下のコマンドでマイグレーションファイルの作成をします。

// マイグレーションファイルの作成
make create NAME=create_table_name

作成されたファイルにクエリを書きます。

vim ./migrations/2019000000000_create_table_name.up.sql

// 以下を書く
create table user(id UInt64, name String, date Date) ENGINE = MergeTree() PARTITION BY toYYYYMMDD(date) ORDER BY (date, id);
vim ./migrations/2019000000000_create_table_name.down.sql

// 以下を書く
drop table user;

ここまでできれば、マイグレートが行えます。
Clickhouseクライアントと共に確認してみてください。

// マイグレート
make up
// ロールバック
make down
スクリーンショット 2020-01-17 21.13.47.png

goでclickohuseへインサート

clickhouse-goを使用します。
READMEのサンプルとほぼ同じです。
クエリビルダーを使うのがオススメですが、以下では使わずに実装してます。

package main

import (
	"database/sql"
	"fmt"
	"log"
	"time"

	"github.com/ClickHouse/clickhouse-go"
)

var ConnectionCH *sql.DB

func setDriver() error {
	var err error
	databseURL := fmt.Sprintf("tcp://%s:%s", "127.0.0.1", "9000")
	ConnectionCH, err = sql.Open("clickhouse", databseURL)
	if err != nil {
		return err
	}
	if err := ConnectionCH.Ping(); err != nil {
		if exception, ok := err.(*clickhouse.Exception); ok {
			fmt.Printf("[%d] %s \n%s\n", exception.Code, exception.Message, exception.StackTrace)
		} else {
			return err
		}
		return err
	}
	return nil
}

func repositoryInsertUser() error {
	var (
		tx, _   = ConnectionCH.Begin()
		stmt, _ = tx.Prepare("INSERT INTO user (id, name ,date) VALUES (?, ?, ?)")
	)
	defer stmt.Close()

	for i := 0; i < 100; i++ {
		if _, err := stmt.Exec(
			i,
			"name",
			time.Now(),
		); err != nil {
			return err
		}
	}

	if err := tx.Commit(); err != nil {
		return err
	}
	return nil
}

func main() {
	if err := setDriver(); err != nil {
		log.Fatal(err)
	}
	if err := repositoryInsertUser(); err != nil {
		log.Fatal(err)
	}
}

上のでは、Prepareでクエリを先にDBに保存し、トランザクションによって、データを一括でインサートすることによって高速にインサートできます。
もしトランザクションやPrepareを使っていなかったら、5倍ぐらいの時間がかかるはずです。
これで、clickhouseへインサートできるようになりました。
githubのREADMEにより詳しく書いてあります。

amazon linux2で動かす

・インストール


sudo su -

sudo yum install -y curl epel-release

curl -s https://packagecloud.io/install/repositories/altinity/clickhouse/script.rpm.sh | 
sudo os=centos dist=7 bash

// インストールする
sudo yum install -y clickhouse-server clickhouse-client

// clickhouseサーバーを起動する
sudo /etc/init.d/clickhouse-server restart

・クライアントの起動


// clickhouseクライアント起動する
clickhouse-client

// デバック用 ログが出る
clickhouse-server --config-file=/etc/clickhouse-server/config.xml

同時接続に耐えられるか

goで並列にクエリ実行して、検証したら普通に問題ないぐらい耐えました。
細かいところは今度載せます。

まとめ

・「圧縮により容量削減」と「高速化」ができるので、コスト削減できる。
・集計サーバを構築中で、コスパが重要の時はおすすめ。
・古いシステムで、RDB上に集計の役割を持たせている場合、その部分だけClickHouseに移動するとかも良い。
・clikchouseクライアントアプリとか誰か作って欲しい。

42
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?