RedShiftのエンコード
前回の記事では、テーブル定義まで進めて検証しましたが、今回はエンコードについて書いていきます。
前提
- RedShift
エンコード
メリットは下記の通りです。
圧縮は、データの格納時にそのサイズを小さくする列レベルの操作です。圧縮によってストレージスペースが節約され、ストレージから読み込まれるデータのサイズが小さくなり、ディスク I/O の量が減少するので、クエリパフォーマンスが向上します。
エンコードの定義方法は、カラムのデータ型、データ種類、データ数、連続性、データをみて選択可能な圧縮エンコードからテーブル定義(create table, alter table)と合わせて手動で定義するか、COPYコマンドを利用して自動圧縮エンコードを選択することができます。自動圧縮エンコードが推奨されていますが、サンプルデータがスライスで100000以上あることが条件になっていますので、それ以外については手動で選択することになります。(この条件値は変更可能ですが、100000が推奨値になっています)
エンコードの種類
デフォルト ソートキーとして定義されている、もしくは、BOOLEAN, REAL, DOUBLE PRECISION型の場合、デフォルトでROWエンコードになる。 他の型はデフォルトはLZOエンコードになる。
rawエンコード
Raw エンコードは、ソートキー、および BOOLEAN、REAL、または DOUBLE PRECISION データ型として定義された列として指定された列のエンコードのデフォルトです。raw エンコードでは、データは非圧縮の raw 形式で格納されます。
ソートキー、BOOLEANでraw型がデフォルトになっています。
ただし、create tableで、エンコードを定義せずに実行すると、全てのカラムがrawエンコードが設定されています。
- ランレングスエンコード
たとえば、大きなディメンションテーブルの列に、予測どおりに小さなドメイン (10 個未満の可能値を持つ COLOR 列など) がある場合、データがソートされなくても、これらの値はテーブル全体にわたって長いシーケンスになる可能性が高くなります。
ステータスをStringで表記しているカラムがあるので、ランレングスエンコードを適用します。
- text255 および text32k エンコード
text255 および text32k エンコードは、同じ単語が頻繁に出現する VARCHAR 列を圧縮する場合に有用です。ディスク上の列値のブロックごとに、一意の単語の個別のディクショナリが作成されます (Amazon Redshift ディスクブロックは 1 MB を占有します)。ディクショナリには、列内の最初の 245 個の一意の単語が含まれます。これらの単語は、ディスク上で、245 個の値の 1 つを表す 1 バイトインデックス値に置き換えられ、ディクショナリに表されていないすべての単語は非圧縮で格納されます。このプロセスは、1 MB のディスクブロックごとに繰り返されます。インデックス作成された単語が列内に頻繁に出現する場合、列の圧縮率は非常に高くなります。
繰り返し出てくる単語が多ければ利用する。
- LZOエンコード
LZO エンコードは、非常に高い圧縮率と良好なパフォーマンスを実現します。LZO エンコードは、非常に長い文字列を格納する CHAR および VARCHAR 列、特に製品説明、ユーザーコメント、JSON 文字列などの自由形式テキストに適しています。LZO は、ソートキー、および BOOLEAN、REAL、または DOUBLE PRECISION データ型として定義された列として指定された列以外のエンコードのデフォルトです。
データ型によってはCOPYコマンドを実行した際に、十分なサンプルデータがある場合にデフォルトでエンコードとして指定される。
Mostly エンコード
Mostly エンコードは、列のデータ型が、格納された大部分の値で必要なサイズより大きい場合に有用です。このタイプの列に Mostly エンコードを指定して、列内の大部分の値を、より小さい標準ストレージサイズに圧縮することができます。圧縮できない残りの値は、raw 形式で格納されます。たとえば、INT2 列などの 16 ビット列を 8 ビットストレージに圧縮できます。
デルタエンコード
デルタエンコードは、日時列にとって非常に有用です。 デルタエンコードは、列内の連続する値間の差を記録することにより、データを圧縮します。この差は、ディスク上の列値の各ブロックに対する個別のディクショナリに記録されます (Amazon Redshift ディスクブロックは 1 MB を占有します)。たとえば、連続する 10 個の整数 1~10 が列に含まれる場合、最初の整数は 4 バイト整数 (+ 1 バイトのフラグ) として格納され、次の 9 個の整数は、前の値よりも 1 大きいことを示す値 1 の単一バイトとしてそれぞれ格納されます。
Zstandard エンコード
Zstandard (ZSTD) エンコーディングは、多様なデータセット間で非常にパフォーマンスのいい高圧縮比率を提供します。ZSTD は、製品説明、ユーザーのコメント、ログ、JSON 文字列など、長さがさまざまな文字列を保存する CHAR および VARCHAR 列に対して、特に効果を発揮します。デルタ エンコードや Mostly エンコードのような一部のアルゴリズムでは非圧縮時よりも多くのストレージスペースを使用する場合があるのと異なり、ZSTD ではディスク使用量が増えることはほとんどありません。ZSTD では、Amazon Redshift のすべてのデータ型がサポートされています。
手動定義
// テーブル定義 create table books ( id BIGINT NOT NULL PRIMARY KEY encode zstd, name VARCHAR(255) encode zstd );
テーブルを作成する際に定義する。(alter tableも可能)
推奨エンコード
既存テーブルに推奨されるエンコードを表示
// 十分なサンプルデータがあるケース analyze compression table_name; // 結果(サンプル) Table | Column | Encoding | Est_reduction_pct -------------+-----------------+----------+------------------- table_name | id | zstd | 7.75 table_name | quantity | zstd | 52.35 // idはBIGINTではあるけど、delta32k,MOSTLY32より推奨?されていることに疑問はある。 // 十分なサンプルデータがないケース analyze compression table_name; // 結果(サンプル) Table | Column | Encoding | Est_reduction_pct -------+--------------------+----------+------------------- table_name | id | raw | 0.00 table_name | name | raw | 0.00 // サンプルデータ足りないためにrawエンコードを推奨に出している。
Est_reduction_pct
は、zstdエンコードの圧縮可能率になります。
エンコードの自動定義
COPY
コマンドがデフォルトでは、空のテーブルに対して実行する場合、、rawエンコードか指定なしの場合に自動圧縮します。また、テーブルのエンコード定義を無視して、自動圧縮を有効したい場合は、COMPUPDATEオプションをONにします。OFFで自動圧縮を無効にします。
すでにデータが入っているテーブルには、自動圧縮は適用されない。
課題
クエリの実行時間の計測 公式が掲載している内容に基づき計測をする。実行するクエリ、回線速度の変化があり、計測が難しいですが、この辺りの誤差は踏まえて計測値を信じて、ソートキー、エンコード、分散スタイルを選択する。
参考
text255 および text32k エンコード - Amazon Redshift
Unicornの参照先が変わらないケース(Capistrano)
前提条件
問題
デプロイした後にWeb + Appサーバにアクセスすると、release versionが古いバージョンにアクセスしている(しかもviewsのみ)そのrelease versionは削除されているので、templateがないと言われてAppサーバがエラーを吐いていた。
#{app_path}/current
が、シンボリックリンクで、更新されていないことでApplication Errorが起きた。unicornプロセスも古いコードを参照したままになっていた。
暫定対応
unicornをUSR2
シグナルによる再起動ではなく、QUIT
シグナルで殺してから、unicornプロセスを起動し直す。
kill -s QUIT `pid` bundle exec unicorn -c /var/www/app_name/config/unicorn.rb -E development -D
これで解決した。
対応
以前からデプロイを繰り返していたにも関わらず今回、初めて起きた事象だったので、前提がおそらく違うのだが、対応はほぼ同じになる。
# config/deploy.rb set :bundle_binstubs, -> { shared_path.join('bin') } # config/unicorn.rb app_path = '/var/www/app' Unicorn::HttpServer::START_CTX[0] = File.join(app_path, 'shared/bin/unicorn')
unicorn + capistrano 構成で、古いリリースの実行パスを参照し続けてしまう問題 - scramble cadenza
今後
capstranoを継続利用せず、Docker + ECRに寄せていければ嬉しいので提案は検討。ただそれ自体がプロダクトに価値があるのか微妙なところだ。
参考
GitHub - capistrano/bundler: Bundler support for Capistrano 3.x
GitHub - defunkt/unicorn: Unofficial Unicorn Mirror.
unicorn/http_server.rb at master · defunkt/unicorn · GitHub
独り言 - 内容と関係ないです。
備忘録と言う名目もあるので、簡単なコードは引用しますが、それ以外は自分で形を変えてをコードを取ってくるつもりです。 (参考記事を見ればその辺は把握できる人たちが読者だと想定して)
sshの接続切れ対応(Broken pipe)
以前から利用しているサーバに対してクライアントには設定(.ssh/config
)していたが、サーバに始めて接続する際に、オプションを設定していなかった。
サーバ接続中に起きたので、再設定して、ssh
した。
接続状態を維持したいので、ServerAliveIntervalとServerAliveCountMaxを設定する。(TCPKeepAliveはデフォルト値がyes
)
.ssh/config host lvh.me user ec2-user port 22 IdentityFile ~/.ssh/lvhme.pem ServerAliveInterval 15 ServerAliveCountMax 100
以上。
参考
ssh_config(5) - OpenBSD manual pages
man ssh_config
TCPKeepAlive Specifies whether the system should send TCP keepalive messages to the other side. If they are sent, death of the connection or crash of one of the machines will be properly noticed. However, this means that connections will die if the route is down temporarily, and some people find it annoying. The default is yes (to send TCP keepalive messages), and the client will notice if the network goes down or the remote host dies. This is important in scripts, and many users want it too. To disable TCP keepalive messages, the value should be set to no. ServerAliveCountMax Sets the number of server alive messages (see below) which may be sent without ssh(1) receiving any messages back from the server. If this threshold is reached while server alive messages are being sent, ssh will disconnect from the server, terminating the session. It is important to note that the use of server alive messages is very different from TCPKeepAlive (below). The server alive messages are sent through the encrypted channel and therefore will not be spoofable. The TCP keepalive option enabled by TCPKeepAlive is spoofable. The server alive mechanism is valuable when the client or server depend on knowing when a connection has become inactive. The default value is 3. If, for example, ServerAliveInterval (see below) is set to 15 and ServerAliveCountMax is left at the default, if the server becomes unresponsive, ssh will disconnect after approximately 45 seconds. ServerAliveInterval Sets a timeout interval in seconds after which if no data has been received from the server, ssh(1) will send a message through the encrypted channel to request a response from the server. The default is 0, indicating that these messages will not be sent to the server. BSD June 23, 2018 BSD
sidekiq + redisの構築(Rails 4系) 3
今回は、workerのテストを書いていきます。
前提環境
Redis Worker Classの場合
方針を決めます。retryの上限数を超えた場合のテストも書きたいので、(Dead Job Queueに移す前に通る
# app/workers/high_worker.rb class HighWorker include Sidekiq::Worker sidekiq_options queue: 'high' sidekiq_retries_exhausted do |msg, ex| puts "hoge" # loggerへの記載 or 通知 end def perform(args) raise end end
max_retries
は、sidekiq.yml
でも定義することができ、各worker classでも定義できます。
# sidekiq.yml max_retries: 1
でリトライの上限数を変更できます。
Rspec
Dead Job Queueに移動する際のテストを書く ただし、コールバックが呼ばれるかのテストではなく、そこに書いているロジックをテストしたい。これもクラスとしてくくり出せるならそちらでテストを書くことでも回避できる。
上記テストのためにgem rspec-siedkiq
を追加する。
(sidekiq
にもテスト用moduleがありますが、上記のテストを行いたいので、こちらを選びました)
# Gemfile group :test do gem 'rspec-sidekiq' end
# app/workers/high_worker_spec.rb require 'rails_helper' Rspec.describe HighWorker do describe '#perform' do it 'should be enqueue' do expect do HighWorker.perform end.to change(HighWorker.jobs, :size).by(1) end it 'shold call sidekiq_retries_exhausted' do HighWorker.within_sidekiq_retries_exhausted_block do expect(HighWorker).to receive(:puts).with('hoge') end end end end
おまけ
警告文を削除したい場合
> [rspec-sidekiq] WARNING! Sidekiq will *NOT* process jobs in this environment. See https://github.com/philostler/rspec-sidekiq/wiki/FAQ-&-Troubleshooting # rails_helper.rbに追記 RSpec::Sidekiq.configure do |config| # Warn when jobs are not enqueued to Redis but to a job array config.warn_when_jobs_not_processed_by_sidekiq = false end
以上になります。
参考
Error Handling · mperham/sidekiq Wiki · GitHub
テーブル定義/データインポート(Amazon Redshift)
前回mysqlからtsvファイルとしてダウンロードしたファイルをredshiftにデータをロードする。検証目的なので、batch処理は後日。
前提環境
- Redshift(AWS)
- MySql(5.7系)
- macOS 10.13.5
- PostgreSQL 9.6.2
- 前回の記事でtxtファイルをs3アップロードしている
Redshiftにクラスターを追加
ここは省いて、追加済みとして進めます。
redshiftにテーブルを作る。
# tables.sql create table tables ( id BIGINT NOT NULL PRIMARY KEY, name VARCHAR(255), created_at TIMESTAMP, updated_at TIMESTAMP );
- ソートキーは無視します。
- mysql側に定義したテーブルのカラム順番とredshiftのカラムの順番を一致させている。
- 値を流し込む場合にredshiftの仕様により流し込みが失敗・エラーになるケースがあります。
String length exceeds DDL length
Mysqlで扱うVARCHARの仕様のズレによりRedshiftではsizeを変更。 渡された数値を文字数として扱うかバイト数で扱うかの違い
postgresql clientからredshiftに接続して、テーブルを作ります。
コマンドを簡易化するためにshファイルを作っておく。
# redshift.sh psql -Uusername poscalc --host=endpoint -p port # redshiftのusernameに対応したpasswordを入力する
bash redshift.sh < tables.sql
データ投入
# import_to_redshift.sql copy tables from 's3://buckets/txts/tables.txt' iam_role 'arn:aws:iam::000000000:role/RedshiftRoleForClient' region 'ap-northeast-1' blanksasnull emptyasnull csv;
bash redshift.sh < import_to_redshift.sql
データの確認
bash redshift.sh select * from tables; ...=> 結果確認
以上になります。作るケースもありますが、redshiftと繋げるBIツールもありますのでユーザ(SQL必須)にはそちらを使って確認してもらうこともできます(tableau Desktop ... etc)
チューニングに関しては、まだ書く意欲があれば書きます。
次の課題
本稼働に向けて、batch処理化する必要がありますので、AWS DATAPIPEを利用することも検討しますし、記事内のスクリプトをcronに設定して、lambda(AWS)で流し込むことも可能です。
費用面と設計面でどちらを選択するか検討する。
回線速度の問題で、s3のアップロードで時間がかかるので、zip, gzipにしてからアップロードして、s3 + lambdaで解凍するとこの問題を迂回する。
参考
MySQL :: MySQL 5.7 Reference Manual :: 11.4.1 The CHAR and VARCHAR Types
tsvファイル出力(MySQL)とaws s3アップロード
RedShiftへのデータロードで、テーブルからcsvでダウンロードしてs3にアップロードします。txtファイルの出力とs3のアップロードについて書いていきます。
txtファイル
SQLファイルを作る
# dump.sql select * from c into outfile '/tmp/table_name.txt' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'; ...
tsvファイルをdump
mysql -u username -p database_name < dump.sql
s3へのアップロード
aws認証情報を登録
awsのIAMからaccess_key, secret_access_keyを取得、なければ作成する。defaultは登録されているので、プロファイル名は別で定義する。
$ aws configure --profile profile_name AWS Access Key ID [None]: access_key AWS Secret Access Key [None]: secret_access_key Default region name [None]: ap-northeast-1 Default output format [None]: json
shファイルを作る
syncコマンドを利用してアップロードします。shファイルを作ります。
# upload_to_s3.sh aws s3 sync /tmp/txts s3://bucket_name/txts --profile profile_name --exclude '*' --include '*.txt' ....
shファイルを実行
bash upload_to_s3.sh # 結果 move: /tmp/table_name.txt to s3://bucket_name/txts/table_name.txt # 成功 move failed: /tmp/table_name.txt to s3://bucket_name/txts/table_name.txt Could not connect to the endpoint URL: "endpoing" #失敗 ....
以上になります。
参考
AWS CLI の設定 - AWS Command Line Interface
AWS Command Line Interface での高レベルの S3 コマンドの使用 - AWS Command Line Interface
sync — AWS CLI 1.15.50 Command Reference
AWS CLI S3 Configuration — AWS CLI 1.15.50 Command Reference
sidekiq + redisの構築(Rails 4系) 2
前回の続きで、sidekiqのデプロイについて書いていきます。
前提環境
- rails 4.x
- ruby 2.2.10
- capistrano 3.5
capistrano
を利用していたので、capistrano/sidekiq
をGemに追加します。
unicorn, pumaなどの設定は無視していきます。
実装
# Gemfile group :development do gem 'capistrano-sidekiq' end
# config/Capfile ... require 'capistrano/sidekiq' require 'capistrano/sidekiq/monit' ...
# config/deploy.rb ... set :pty, true set :sidekiq_config, -> { File.join(shared_path, 'config', 'sidekiq.yml') } set :sidekiq_monit_conf_dir, '/etc/monit.d' ...
(sudo権限があるユーザを利用することしない場合は、pty, false(default)にする) default hookが追加されますので、deployに合わしてsidekiqプロセスが起動・停止します。
sidekiq_default_hooks Sidekiq will start or stop automatically during deployments. Just set sidekiq_default_hooks to false if you don't want this to happen.
sidekiq_default_hooksをfalseにすれば、hooksを無効に出来ます。
手動起動
cap env sidekiq:start
以上。
参考
GitHub - seuros/capistrano-sidekiq: Sidekiq integration for Capistrano
Home · seuros/capistrano-sidekiq Wiki · GitHubHome · seuros/capistrano-sidekiq Wiki · GitHub
sidekiq + redisの構築(Rails 4系)
非同期処理を取り入れる提案をして、採用されたので構築していました。アウトプットとして何回かに分けて記事を書いていきます。
前提環境
前提環境の背景を話すと、Rails 5系にアップデートできていないプロジェクトです。テストは随時入れていますので、工数が取れればアップデートは出来ますが今のところ予定はないです。
サーバ構成はこんな感じ AWS EC2( Application Server Rails) <=> AWS EC2(Batch Server Rails, sidekiq(worker), Batch(cron)) <=> Aws ElastiCache(Redis, クラスターモード無し)
Rails 4.2系でもActive Jobがあり、retry
機能が想定しているエラーを迂回できそうになかったので、worker
クラスを実装して進めています。ActiveJobを利用するとRailsがadapterをラップしていることによるメリットもあり、ユースケースを満たせれば,テストコードも楽に書けるようになり採用は出来たと思います。
まずredisのインストールは、この記事ではローカル上にインストールします。(Dockerはまた別の機会に書きます) Hometbrewを利用してインストールします。
redis
brew install redis redis-server `/usr/local/etc/redis.conf`が保存されている。
続いてAppに実装
# Gemfile gem 'sidekiq'
bundle install
# app/workers/high_worker.rb class HighWorker include Sidekiq::Worker sidekiq_options queue: :high, retry: false def perform(csv_file_id) AbcCsvImporterService.call(csv_file_id) # 完了通知 or logging end end # AbcCsvImportクラスは実装無し。クラス内の例外についても対応する。thread safeのコードにする。(gemが提供するライブラリを使う場合も注意) # retry: falseなので、Dead Job Queueにはenqueueされません。
# config/application.rb ... config.active_job.queue_adapter = :sidekiq ....
# config/sidekiq.rb --- :verbose: false :timeout: 15 :concurrency: 2 staging: :concurrency: 5 production: :concurrency: 5 :queues: - high
# database.yml development: adapter: mysql2 encoding: utf8 database: my_db pool: 7 username: my_user password: my_password host: localhost ....
# config/initializes/sidekiq.rb Sidekiq.configure_server do |config| config.redis = { url: 'redis://localhost:6379', network_timeout: 5 } end Sidekiq.configure_client do |config| config.redis = { url: `redis://localhost:6379` network_timeout: 5 } end
ローカル上で動作することに限定するなら、設定ファイルをなくして、デフォルトにしてもいいです。上記のurlは、sidekiqのデフォルトになります。本番、ステージングなどある場合は環境別に変えましょう。network_timeoutオプションは、EC2を利用する都合timeout時間を伸ばしています。デフォルトは1です。
database poolと同値かそれ以下にconcurrencyを設定。
# csv_import_controller class CsvImportController < ApplicationController before_action :set_csv_file, only: %w[new create] before_action :set_csv_files, only: %w[index] def index; end def new; end def create if @csv_file.save redirect_to new_csv_import_path, notice: '保存に成功しました' else render :new, notice: '保存に失敗しました' end end private def csv_file_params return params.require(:csv_file).permit(CsvFile.column_names) if params[:csv_file] {} end def set_csv_file @csv_file = Csvfile.new(csv_file_params) end def csv_files # index用 end end
# app/model/csv_file.rb class CsvFile < ActiveRecord after_commit :import_data, on: :create def import_data CsvFileWorker.perform_async(id) end end
viewsは省きます。CSVファイルの入力フォームのイメージです。
bundle exec sidekiq -C config/sidekiq.yml
以上です。次回、AWSの構成、テストコード、redisなどの記事を書ければ書きます。色々ノウハウ溜まっているのでそのほかにElasticsearchなど。そのうち。
参考
Home · mperham/sidekiq Wiki · GitHub
Error Handling · mperham/sidekiq Wiki · GitHub
https://github.com/mperham/sidekiq/wiki/Problems-and-Troubleshooting#threading
PostgreSQLサーバの起動コマンド
毎回ググっているので、自分の備忘録として残しています。
環境
- macOS Sierra
- PostgreSQL 9.6.2(homebrewでインストール)
## PostgreSQLサーバの起動(homebrewでインストール) postgres -D /usr/local/var/postgres
参考
drop_tableの代わりにActiveRecordMigration#revert
環境
前提
初期からRailsでアプリを作成していて、tableを作成したmigrationもそのアプリが保有していることが前提になります。
modelを途中で消す場合、./bin/rails d model model_name
で消すと作成した際、migration file
も削除される。
途中で、カラムを追加・変更・削除している場合は、./bin/rails db:migrate
が失敗することになり、modelを削除する際には、気をつけないと、migrateを一から実行することができなくなり、復旧することになります。
revert
rails app単体の構成であれば、drop_table
を使ってテーブルを削除するよりも、revert
を使うのが良いと思います。
では、手順ですが、一度作成したmodel、testファイル、該当するコードを削除した後、tableを作成したmigrationは残しておき、新規drop用のmigration fileを準備します。
サンプル
model bread作成
model rice作成
migrate実行とgit commit
./bin/rails db:migrate git add . && git commit -m "add bread and rice models"
model bread削除
./bin/rails d model bread git checkout db/migrate ./bin/rails g migration DropBreadsTable
作成したmigrationをエディタで開く
require_relative '20170629123105_create_breads' # 追記(migrationファイル名は、適切なファイル名に直してください) class DropBreadsTable < ActiveRecord::Migration[5.1] def change revert CreateBreads # 追記 create_breadsをキャメルケースに直してrevertの引数に渡す。 end end
以上です。これで、migrate, rollbackすることができます。
別サンプル
途中で、カラムを追加していた場合のケースも追加しておきます。
breadsにcolumnを追加
./bin/rails g migration AddIsSweetnessToBreads
migration file編集
class AddIsSweetnessToBreads < ActiveRecord::Migration[5.1] def change add_column :breads, :is_sweetness, :boolean, default: false end end
テーブルを削除するmigration fileを作成
require_relative '20170630135043_add_is_sweetness_to_breads' require_relative '20170630134600_create_breads' class DropBreadsTable < ActiveRecord::Migration[5.1] def change revert AddIsSweetnessToBreads revert CreateBreads end end
以上です。
./bin/rails db:migrate ./bin/rails db:rollback STEP=3 ./bin/rails db:migrate
複数テーブルの操作をmigrationに書き込んでいる場合
このケースだと対応はできないです。
migration file
は、1テーブルの操作に制限して、プロジェクトメンバーに周知して守らせる必要がありそうです。
(上記をオススメしますが、migration fileの書き方で、dropしないテーブルに関する操作をrevert後にコピーしてくることで復旧できそうなので、
今度改めて、追記します。)
参考
http://api.rubyonrails.org/classes/ActiveRecord/Migration.html#method-i-revert