Amazon Auroraのポイントインタイムリカバリ(特定時点へのリストア)を触りました。

こんにちは、インフラエンジニア の 加藤(@kuromitsu_ka)です。
今回は、Amazon Auroraのポイントインタイムリカバリ(特定時点へのリストア)を触ったので、記事を残します。

概要

Auroraのバックアップ保持期間内であれば、特定の時点のデータで、DBクラスタを作成できる機能があり、これが便利でした。
DBクラスタの作成にかかる時間と、DB作成時にどこまで正確にデータを復元できるか確認したので、そのまとめを記載します。
※手順の方は、ドキュメントに記載あるので割愛します。

ざっくりよかったこと

  • バックアップ保持期間であれば、秒単位で指定した時点でDBクラスタを作成できる。
  • 運用中のDBを切り戻すのでなくDBを新規作成するため、現行DBに手を加えなくて良い。

ちょっと細かいところ。

Auroraは、フルバックアップと合わせて、トランザクションデータも保存しています。
一番早くて、最新時間の5分前の時間で、DBクラスタを作成できる模様でした。
※切り戻せる時間の範囲については、describe-db-clusters から確認できます。

参考になる公式ドキュメント

検証したこと

ざっくり2つ確認しました。

(検証1)DBクラスタ再作成にかかる時間の計測

Auroraクラスタのステータスが、作成中から使用可能になるまでの時間を確認しました。

$ cat check.sh
#!/bin/bash

while true
do
  echo $(date +"%Y%m%d %H:%M:%S";aws rds describe-db-clusters --db-cluster-identifier $1 | jq -r ".DBClusters[].Status")
  sleep 1
done
  • 結果としては、以下のようになりました。 積んでいるデータが大きいと、そこそこ時間はかかりそうなものの、まぁ大丈夫かと。
データ 所用時間 インスタンスタイプ データのサイズ
開発環境データ 15分程度 db.t3.small(vCPU:2,メモリ:2GiB) 30GB
本番環境データ 27分程度 db.r6g.4xlarge(vCPU:16,メモリ:128) 160GB
おまけの計測

Auroraのリストアも、フルバックからリストア後、差分適用してるのかな?と思い。
開発環境にて、フルバックアップ取得時間を軸に、ざっくり2パターンで検証しました。
結果として、特に大差はなかったです。

  • Auroraのフルバックアップ取得から、1時間経過した時点のデータでDB作成した場合
    →約13分30秒

  • Auroraのフルバックアップ取得より、1時間前(23時間経過した)の時点のデータでDB作成した場合
    →約14分00秒

(検証2)切り戻し時点の前後のトランザクションデータの正確性の確認

切り戻したい時間帯に発生していたトランザクションのデータは、ちゃんと復元できるのか確認しました。

検証の結果

リストアでデータが正確に復元できないのは、リストアで指定する時間の前後1秒間にコミットされていないトランザクションだけでした。

具体的な検証方法

スクリプトでINSERT,UPDATEを流して、トランザクションを発生させました。
スクリプト実行中の時間を指定して、DBクラスタを作成して起動後のデータを確認しました。

検証ログ

データの追加時間・更新時間がわかるように、こんな感じのテーブルを作成してトランザクションを発生させました。

+------------+-------------+------+-----+-------------------+-----------------------------+
| Field      | Type        | Null | Key | Default           | Extra                       |
+------------+-------------+------+-----+-------------------+-----------------------------+
| id         | int(11)     | NO   | PRI | NULL              |                             |
| comment    | varchar(20) | YES  |     | NULL              |                             |
| created_at | timestamp   | NO   |     | CURRENT_TIMESTAMP |                             |
| updated_at | timestamp   | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+------------+-------------+------+-----+-------------------+-----------------------------+

外部のサーバから、任意の数のトランザクションを発生させるスクリプトです。
※DELETEも流していた時の名残もありますが、こちらはスルーでお願いします。

for i in $(seq ${NUMBER_OF_QUERIES_INSERT_and_UPDATE})
  mysql -h $RDS -u${user} -p${password} -e "INSERT INTO ${TABLE_NAME}  (id, comment) VALUES (${i},\"insert${i}\");"
  mysql -h $RDS -u${user} -p${password} -e "UPDATE ${TABLE_NAME} set comment=\"update${i}\" where id=${i};"
#  if [ ${i} -lt ${NUMBER_OF_QUERIES_DELETE} ] || [ ${i} -eq ${NUMBER_OF_QUERIES_DELETE} ]; then
#    mysql -h $RDS -u${user} -p${password} -e "DELETE FROM ${TABLE_NAME} WHERE id = ${i};"
#  fi
done
echo $(date "+%H:%M:%S") Script END >> ${STATUS_FILE}
  • スクリプトの出力するステータスファイルから開始終了時間を確認
# cat /tmp/status.txt 
2022-01-31 11:58:21 Script START
2022-01-31 21:52:14 Script END

f:id:enigmo7:20220304153959p:plain

  • リストア後のテーブル
    2021/01/31 21:51:59秒までのデータは、問題なく切り戻せていました。
mysql> select * from  bm_messages.AWSNEXT_1215 ORDER BY id desc limit 5;
+--------+--------------+---------------------+---------------------+
| id     | comment      | created_at          | updated_at          |
+--------+--------------+---------------------+---------------------+
| 379369 | insert379369 | 2022-01-31 21:51:59 | 2022-01-31 21:51:59 |
| 379368 | update379368 | 2022-01-31 21:51:59 | 2022-01-31 21:51:59 |
| 379367 | update379367 | 2022-01-31 21:51:59 | 2022-01-31 21:51:59 |
| 379366 | update379366 | 2022-01-31 21:51:59 | 2022-01-31 21:51:59 |
| 379365 | update379365 | 2022-01-31 21:51:59 | 2022-01-31 21:51:59 |
+--------+--------------+---------------------+---------------------+
5 rows in set (0.01 sec)

感想

DBを切り戻すんじゃなくて、別途作成してくれるところがいいなぁって思いました!

株式会社エニグモ すべての求人一覧

hrmos.co

dbt x BigQueryを使ってみた

こんにちは、エニグモでデータサイエンティストをしている堀部です。 昨年末から使い始めたdbt x BigQueryについて共有します。 BigQuery歴2年、SQL歴5年ほどになります。 QUALIFY句が好きです。

dbtを使い始めたきっかけ

SQLでの集計は嫌いではないのですが、以下の2点で困っていることがありました。

1点目は、BigQuery特有のエラー Resources exceeded during query execution: Not enough resources for query planning - too many subqueries or query is too complex(以後、too complex エラー)です。 create temp table を使って一時的な中間テーブルを挟むことで回避してきたのですが、その都度書き換えるコストがかかっていました。

2点目は、似たようなクエリを以前に書いたこと覚えがあっても、過去の自分が書いたクエリ*1が長く該当箇所を見つけるのに時間がかかってしまうという課題がありました。

この2点をまとめて解決できそうだと感じ、使い始めたのがdbtでした。

使ってみてよかった点

  • with句を分割して管理できる

    → 部分的なクエリの再利用がしやすくなった

  • データモデルの種類(view、table、intermediate、ephemeral)を簡単に変更できる

    → too complexエラーの回避が簡単に

  • yaml(dbt_project.yml)で複数のクエリで共通に利用できる変数(vars)を管理することができ、CLIで変数を上書きして実行することができる

    → 汎用的なクエリを作成して、varsだけを変更することで様々なパターンを試せるようになった

また、jinja2を使ったmacroを前処理〜特徴量生成で利用してみたら便利だったので紹介します。

前処理〜特徴量生成の例

bigqueryの公開データのbigquery-public-data.ml_datasets.census_adult_incomeを使って実際に利用したファイルを元に紹介します。*2

dbt_project.yml

modelsとvarsの部分のみを変更しています。

# Name your project! Project names should contain only lowercase characters
# and underscores. A good package name should reflect your organization's
# name or the intended use of these models
name: 'techblog_202201'
version: '1.0.0'
config-version: 2

# This setting configures which "profile" dbt uses for this project.
profile: 'sample'

# These configurations specify where dbt should look for different types of files.
# The `source-paths` config, for example, states that models in this project can be
# found in the "models/" directory. You probably won't need to change these!
source-paths: ["models"]
analysis-paths: ["analysis"]
test-paths: ["tests"]
data-paths: ["data"]
macro-paths: ["macros"]
snapshot-paths: ["snapshots"]

target-path: "target"  # directory which will store compiled SQL files
clean-targets:         # directories to be removed by `dbt clean`
  - "target"
  - "dbt_modules"


# Configuring models
# Full documentation: https://docs.getdbt.com/docs/configuring-models

# In this example config, we tell dbt to build all models in the example/ directory
# as tables. These settings can be overridden in the individual model files
# using the `{{ config(...) }}` macro.
models:
  techblog_202201:
    temp_table:
      +materialized: table
      +hours_to_expiration: 1
    table:
      +materialized: table
    view:
      +materialized: view

vars:
  base_table: bigquery-public-data.ml_datasets.census_adult_income
  index_col: id
  target_col: income_bracket
  list_agg:
    - avg
    - max
    - min
    - stddev

macro

macro/get_columns_list.sql

BigQueryのINFORMATION_SCHEMAを利用してカラムの一覧を取得できるmacroを作成して利用しています。typesに型のリストを渡すことで、該当する型のカラムのみを取得することができます。

{% macro get_columns_list(table_name, types=None) -%}

{% set columns_query %}
select column_name
from `{{table_name.dataset}}.INFORMATION_SCHEMA.COLUMNS`
where
table_schema = "{{table_name.dataset}}"
and table_name = "{{table_name.name}}"
{%- if types is not none %}
and data_type in (
{%- for type in types %}
{%- if loop.last %}
    "{{type}}"
{%- else %}
    "{{type}}",
{%- endif %}
{%- endfor %}
)
{%- endif %}
{% endset %}

{% set results = run_query(columns_query) %}
{% if execute %}
{% set list_results = results.columns[0].values() %}
{% else %}
{% set list_results = [] %}
{% endif %}
{{ return(list_results) }}

{% endmacro %}

models

jinjaで書いたクエリ

dbt compile で生成されたクエリ

の順で紹介していきます。

models/view/row_census_adult_income.sql

元々のテーブルデータにindexとなるカラムidを追加しています。

select
    row_number() over (order by 1) as {{var("index_col")}},
    *
from
    `{{var("base_table")}}`

↓ compile

select
    row_number() over (order by 1) as id,
    *
from
    `bigquery-public-data.ml_datasets.census_adult_income`

models/temp_table/stg_census_adult_income.sql

  • カテゴリ変数に対して以下の前処理を実施

    • 空白削除
    • 小文字化
    • 正規化
  • 目的変数(income_bracket)

    • 2値なので0,1に変換*3
{%- set ref_table = "row_census_adult_income" %}
{%- set list_numeric_columns = get_columns_list(ref(ref_table),types=["FLOAT64","INT64"]) -%}
{%- set list_categorical_columns = get_columns_list(ref(ref_table),types=["STRING"]) -%}
select
    {%- for col in list_numeric_columns %}
    {{col}},
    {%- endfor %}
    {%- for col in list_categorical_columns %}
    {%- if col != var("target_col") %}
    normalize(lower(trim({{col}})), NFKC) as {{col}},
    {%- endif %}
    {%- endfor %}
    case when {{var("target_col")}} = "<=50K" then 1 else 0 end as {{var("target_col")}},
from
    {{ref(ref_table)}}

↓ compile

select
    id,
    age,
    functional_weight,
    education_num,
    capital_gain,
    capital_loss,
    hours_per_week,
    normalize(lower(trim(workclass)), NFKC) as workclass,
    normalize(lower(trim(education)), NFKC) as education,
    normalize(lower(trim(marital_status)), NFKC) as marital_status,
    normalize(lower(trim(occupation)), NFKC) as occupation,
    normalize(lower(trim(relationship)), NFKC) as relationship,
    normalize(lower(trim(race)), NFKC) as race,
    normalize(lower(trim(sex)), NFKC) as sex,
    normalize(lower(trim(native_country)), NFKC) as native_country,
    case when income_bracket = "<=50K" then 1 else 0 end as income_bracket,
from
    `buyma-analytics`.`techblog_202201_dev`.`row_census_adult_income`

models/table/feature_census_adult_income.sql

  • 量的変数
    • そのまま
  • カテゴリ変数
    • dense_rank()で擬似的にLabel Encoding
  • カテゴリ変数 x 量的変数
    • カテゴリ変数ごとに統計量(平均、最小、最大、標準偏差)を取得
{%- set ref_table = "stg_census_adult_income" -%}
{%- set list_numeric_columns = get_columns_list(ref(ref_table),types=["FLOAT64","INT64"]) -%}
{%- set list_categorical_columns = get_columns_list(ref(ref_table),types=["STRING"]) -%}
select
    {%- for numeric_column in list_numeric_columns %}
        {%- if numeric_column != var("target_col")  %}
    {{numeric_column}},
        {%- endif %}
    {%- endfor %}
    {%- for categorical_column in list_categorical_columns %}
        {%- set loop_index = loop.index0 + 1 %}
    dense_rank() over (order by {{categorical_column}}) as {{categorical_column}},
        {%- for numeric_column in list_numeric_columns %}
            {%- if numeric_column not in [var("target_col"), var("index_col")]  %}
                {%- for agg in var("list_agg") %}
    {{agg}}({{numeric_column}}) over (partition by {{categorical_column}}) as {{agg}}_{{numeric_column}}_by_{{categorical_column}},
                {%- endfor %}
            {%- endif %}
        {%- endfor %}
    {%- endfor %}
    {{var("target_col")}}
from
    {{ref(ref_table)}}
order by
    1

↓ compile

select
    id,
    age,
    functional_weight,
    education_num,
    capital_gain,
    capital_loss,
    hours_per_week,
    dense_rank() over (order by workclass) as workclass,
    avg(age) over (partition by workclass) as avg_age_by_workclass,
    max(age) over (partition by workclass) as max_age_by_workclass,
    min(age) over (partition by workclass) as min_age_by_workclass,
    stddev(age) over (partition by workclass) as stddev_age_by_workclass,
    avg(functional_weight) over (partition by workclass) as avg_functional_weight_by_workclass,
    max(functional_weight) over (partition by workclass) as max_functional_weight_by_workclass,
    min(functional_weight) over (partition by workclass) as min_functional_weight_by_workclass,
    stddev(functional_weight) over (partition by workclass) as stddev_functional_weight_by_workclass,
    avg(education_num) over (partition by workclass) as avg_education_num_by_workclass,
    max(education_num) over (partition by workclass) as max_education_num_by_workclass,
    min(education_num) over (partition by workclass) as min_education_num_by_workclass,
    stddev(education_num) over (partition by workclass) as stddev_education_num_by_workclass,
    avg(capital_gain) over (partition by workclass) as avg_capital_gain_by_workclass,
    max(capital_gain) over (partition by workclass) as max_capital_gain_by_workclass,
    min(capital_gain) over (partition by workclass) as min_capital_gain_by_workclass,
    stddev(capital_gain) over (partition by workclass) as stddev_capital_gain_by_workclass,
    avg(capital_loss) over (partition by workclass) as avg_capital_loss_by_workclass,
    max(capital_loss) over (partition by workclass) as max_capital_loss_by_workclass,
    min(capital_loss) over (partition by workclass) as min_capital_loss_by_workclass,
    stddev(capital_loss) over (partition by workclass) as stddev_capital_loss_by_workclass,
    avg(hours_per_week) over (partition by workclass) as avg_hours_per_week_by_workclass,
    max(hours_per_week) over (partition by workclass) as max_hours_per_week_by_workclass,
    min(hours_per_week) over (partition by workclass) as min_hours_per_week_by_workclass,
    stddev(hours_per_week) over (partition by workclass) as stddev_hours_per_week_by_workclass,
    dense_rank() over (order by education) as education,
    avg(age) over (partition by education) as avg_age_by_education,
    max(age) over (partition by education) as max_age_by_education,
    min(age) over (partition by education) as min_age_by_education,
    stddev(age) over (partition by education) as stddev_age_by_education,
    avg(functional_weight) over (partition by education) as avg_functional_weight_by_education,
    max(functional_weight) over (partition by education) as max_functional_weight_by_education,
    min(functional_weight) over (partition by education) as min_functional_weight_by_education,
    stddev(functional_weight) over (partition by education) as stddev_functional_weight_by_education,
    avg(education_num) over (partition by education) as avg_education_num_by_education,
    max(education_num) over (partition by education) as max_education_num_by_education,
    min(education_num) over (partition by education) as min_education_num_by_education,
    stddev(education_num) over (partition by education) as stddev_education_num_by_education,
    avg(capital_gain) over (partition by education) as avg_capital_gain_by_education,
    max(capital_gain) over (partition by education) as max_capital_gain_by_education,
    min(capital_gain) over (partition by education) as min_capital_gain_by_education,
    stddev(capital_gain) over (partition by education) as stddev_capital_gain_by_education,
    avg(capital_loss) over (partition by education) as avg_capital_loss_by_education,
    max(capital_loss) over (partition by education) as max_capital_loss_by_education,
    min(capital_loss) over (partition by education) as min_capital_loss_by_education,
    stddev(capital_loss) over (partition by education) as stddev_capital_loss_by_education,
    avg(hours_per_week) over (partition by education) as avg_hours_per_week_by_education,
    max(hours_per_week) over (partition by education) as max_hours_per_week_by_education,
    min(hours_per_week) over (partition by education) as min_hours_per_week_by_education,
    stddev(hours_per_week) over (partition by education) as stddev_hours_per_week_by_education,
    dense_rank() over (order by marital_status) as marital_status,
    avg(age) over (partition by marital_status) as avg_age_by_marital_status,
    max(age) over (partition by marital_status) as max_age_by_marital_status,
    min(age) over (partition by marital_status) as min_age_by_marital_status,
    stddev(age) over (partition by marital_status) as stddev_age_by_marital_status,
    avg(functional_weight) over (partition by marital_status) as avg_functional_weight_by_marital_status,
    max(functional_weight) over (partition by marital_status) as max_functional_weight_by_marital_status,
    min(functional_weight) over (partition by marital_status) as min_functional_weight_by_marital_status,
    stddev(functional_weight) over (partition by marital_status) as stddev_functional_weight_by_marital_status,
    avg(education_num) over (partition by marital_status) as avg_education_num_by_marital_status,
    max(education_num) over (partition by marital_status) as max_education_num_by_marital_status,
    min(education_num) over (partition by marital_status) as min_education_num_by_marital_status,
    stddev(education_num) over (partition by marital_status) as stddev_education_num_by_marital_status,
    avg(capital_gain) over (partition by marital_status) as avg_capital_gain_by_marital_status,
    max(capital_gain) over (partition by marital_status) as max_capital_gain_by_marital_status,
    min(capital_gain) over (partition by marital_status) as min_capital_gain_by_marital_status,
    stddev(capital_gain) over (partition by marital_status) as stddev_capital_gain_by_marital_status,
    avg(capital_loss) over (partition by marital_status) as avg_capital_loss_by_marital_status,
    max(capital_loss) over (partition by marital_status) as max_capital_loss_by_marital_status,
    min(capital_loss) over (partition by marital_status) as min_capital_loss_by_marital_status,
    stddev(capital_loss) over (partition by marital_status) as stddev_capital_loss_by_marital_status,
    avg(hours_per_week) over (partition by marital_status) as avg_hours_per_week_by_marital_status,
    max(hours_per_week) over (partition by marital_status) as max_hours_per_week_by_marital_status,
    min(hours_per_week) over (partition by marital_status) as min_hours_per_week_by_marital_status,
    stddev(hours_per_week) over (partition by marital_status) as stddev_hours_per_week_by_marital_status,
    dense_rank() over (order by occupation) as occupation,
    avg(age) over (partition by occupation) as avg_age_by_occupation,
    max(age) over (partition by occupation) as max_age_by_occupation,
    min(age) over (partition by occupation) as min_age_by_occupation,
    stddev(age) over (partition by occupation) as stddev_age_by_occupation,
    avg(functional_weight) over (partition by occupation) as avg_functional_weight_by_occupation,
    max(functional_weight) over (partition by occupation) as max_functional_weight_by_occupation,
    min(functional_weight) over (partition by occupation) as min_functional_weight_by_occupation,
    stddev(functional_weight) over (partition by occupation) as stddev_functional_weight_by_occupation,
    avg(education_num) over (partition by occupation) as avg_education_num_by_occupation,
    max(education_num) over (partition by occupation) as max_education_num_by_occupation,
    min(education_num) over (partition by occupation) as min_education_num_by_occupation,
    stddev(education_num) over (partition by occupation) as stddev_education_num_by_occupation,
    avg(capital_gain) over (partition by occupation) as avg_capital_gain_by_occupation,
    max(capital_gain) over (partition by occupation) as max_capital_gain_by_occupation,
    min(capital_gain) over (partition by occupation) as min_capital_gain_by_occupation,
    stddev(capital_gain) over (partition by occupation) as stddev_capital_gain_by_occupation,
    avg(capital_loss) over (partition by occupation) as avg_capital_loss_by_occupation,
    max(capital_loss) over (partition by occupation) as max_capital_loss_by_occupation,
    min(capital_loss) over (partition by occupation) as min_capital_loss_by_occupation,
    stddev(capital_loss) over (partition by occupation) as stddev_capital_loss_by_occupation,
    avg(hours_per_week) over (partition by occupation) as avg_hours_per_week_by_occupation,
    max(hours_per_week) over (partition by occupation) as max_hours_per_week_by_occupation,
    min(hours_per_week) over (partition by occupation) as min_hours_per_week_by_occupation,
    stddev(hours_per_week) over (partition by occupation) as stddev_hours_per_week_by_occupation,
    dense_rank() over (order by relationship) as relationship,
    avg(age) over (partition by relationship) as avg_age_by_relationship,
    max(age) over (partition by relationship) as max_age_by_relationship,
    min(age) over (partition by relationship) as min_age_by_relationship,
    stddev(age) over (partition by relationship) as stddev_age_by_relationship,
    avg(functional_weight) over (partition by relationship) as avg_functional_weight_by_relationship,
    max(functional_weight) over (partition by relationship) as max_functional_weight_by_relationship,
    min(functional_weight) over (partition by relationship) as min_functional_weight_by_relationship,
    stddev(functional_weight) over (partition by relationship) as stddev_functional_weight_by_relationship,
    avg(education_num) over (partition by relationship) as avg_education_num_by_relationship,
    max(education_num) over (partition by relationship) as max_education_num_by_relationship,
    min(education_num) over (partition by relationship) as min_education_num_by_relationship,
    stddev(education_num) over (partition by relationship) as stddev_education_num_by_relationship,
    avg(capital_gain) over (partition by relationship) as avg_capital_gain_by_relationship,
    max(capital_gain) over (partition by relationship) as max_capital_gain_by_relationship,
    min(capital_gain) over (partition by relationship) as min_capital_gain_by_relationship,
    stddev(capital_gain) over (partition by relationship) as stddev_capital_gain_by_relationship,
    avg(capital_loss) over (partition by relationship) as avg_capital_loss_by_relationship,
    max(capital_loss) over (partition by relationship) as max_capital_loss_by_relationship,
    min(capital_loss) over (partition by relationship) as min_capital_loss_by_relationship,
    stddev(capital_loss) over (partition by relationship) as stddev_capital_loss_by_relationship,
    avg(hours_per_week) over (partition by relationship) as avg_hours_per_week_by_relationship,
    max(hours_per_week) over (partition by relationship) as max_hours_per_week_by_relationship,
    min(hours_per_week) over (partition by relationship) as min_hours_per_week_by_relationship,
    stddev(hours_per_week) over (partition by relationship) as stddev_hours_per_week_by_relationship,
    dense_rank() over (order by race) as race,
    avg(age) over (partition by race) as avg_age_by_race,
    max(age) over (partition by race) as max_age_by_race,
    min(age) over (partition by race) as min_age_by_race,
    stddev(age) over (partition by race) as stddev_age_by_race,
    avg(functional_weight) over (partition by race) as avg_functional_weight_by_race,
    max(functional_weight) over (partition by race) as max_functional_weight_by_race,
    min(functional_weight) over (partition by race) as min_functional_weight_by_race,
    stddev(functional_weight) over (partition by race) as stddev_functional_weight_by_race,
    avg(education_num) over (partition by race) as avg_education_num_by_race,
    max(education_num) over (partition by race) as max_education_num_by_race,
    min(education_num) over (partition by race) as min_education_num_by_race,
    stddev(education_num) over (partition by race) as stddev_education_num_by_race,
    avg(capital_gain) over (partition by race) as avg_capital_gain_by_race,
    max(capital_gain) over (partition by race) as max_capital_gain_by_race,
    min(capital_gain) over (partition by race) as min_capital_gain_by_race,
    stddev(capital_gain) over (partition by race) as stddev_capital_gain_by_race,
    avg(capital_loss) over (partition by race) as avg_capital_loss_by_race,
    max(capital_loss) over (partition by race) as max_capital_loss_by_race,
    min(capital_loss) over (partition by race) as min_capital_loss_by_race,
    stddev(capital_loss) over (partition by race) as stddev_capital_loss_by_race,
    avg(hours_per_week) over (partition by race) as avg_hours_per_week_by_race,
    max(hours_per_week) over (partition by race) as max_hours_per_week_by_race,
    min(hours_per_week) over (partition by race) as min_hours_per_week_by_race,
    stddev(hours_per_week) over (partition by race) as stddev_hours_per_week_by_race,
    dense_rank() over (order by sex) as sex,
    avg(age) over (partition by sex) as avg_age_by_sex,
    max(age) over (partition by sex) as max_age_by_sex,
    min(age) over (partition by sex) as min_age_by_sex,
    stddev(age) over (partition by sex) as stddev_age_by_sex,
    avg(functional_weight) over (partition by sex) as avg_functional_weight_by_sex,
    max(functional_weight) over (partition by sex) as max_functional_weight_by_sex,
    min(functional_weight) over (partition by sex) as min_functional_weight_by_sex,
    stddev(functional_weight) over (partition by sex) as stddev_functional_weight_by_sex,
    avg(education_num) over (partition by sex) as avg_education_num_by_sex,
    max(education_num) over (partition by sex) as max_education_num_by_sex,
    min(education_num) over (partition by sex) as min_education_num_by_sex,
    stddev(education_num) over (partition by sex) as stddev_education_num_by_sex,
    avg(capital_gain) over (partition by sex) as avg_capital_gain_by_sex,
    max(capital_gain) over (partition by sex) as max_capital_gain_by_sex,
    min(capital_gain) over (partition by sex) as min_capital_gain_by_sex,
    stddev(capital_gain) over (partition by sex) as stddev_capital_gain_by_sex,
    avg(capital_loss) over (partition by sex) as avg_capital_loss_by_sex,
    max(capital_loss) over (partition by sex) as max_capital_loss_by_sex,
    min(capital_loss) over (partition by sex) as min_capital_loss_by_sex,
    stddev(capital_loss) over (partition by sex) as stddev_capital_loss_by_sex,
    avg(hours_per_week) over (partition by sex) as avg_hours_per_week_by_sex,
    max(hours_per_week) over (partition by sex) as max_hours_per_week_by_sex,
    min(hours_per_week) over (partition by sex) as min_hours_per_week_by_sex,
    stddev(hours_per_week) over (partition by sex) as stddev_hours_per_week_by_sex,
    dense_rank() over (order by native_country) as native_country,
    avg(age) over (partition by native_country) as avg_age_by_native_country,
    max(age) over (partition by native_country) as max_age_by_native_country,
    min(age) over (partition by native_country) as min_age_by_native_country,
    stddev(age) over (partition by native_country) as stddev_age_by_native_country,
    avg(functional_weight) over (partition by native_country) as avg_functional_weight_by_native_country,
    max(functional_weight) over (partition by native_country) as max_functional_weight_by_native_country,
    min(functional_weight) over (partition by native_country) as min_functional_weight_by_native_country,
    stddev(functional_weight) over (partition by native_country) as stddev_functional_weight_by_native_country,
    avg(education_num) over (partition by native_country) as avg_education_num_by_native_country,
    max(education_num) over (partition by native_country) as max_education_num_by_native_country,
    min(education_num) over (partition by native_country) as min_education_num_by_native_country,
    stddev(education_num) over (partition by native_country) as stddev_education_num_by_native_country,
    avg(capital_gain) over (partition by native_country) as avg_capital_gain_by_native_country,
    max(capital_gain) over (partition by native_country) as max_capital_gain_by_native_country,
    min(capital_gain) over (partition by native_country) as min_capital_gain_by_native_country,
    stddev(capital_gain) over (partition by native_country) as stddev_capital_gain_by_native_country,
    avg(capital_loss) over (partition by native_country) as avg_capital_loss_by_native_country,
    max(capital_loss) over (partition by native_country) as max_capital_loss_by_native_country,
    min(capital_loss) over (partition by native_country) as min_capital_loss_by_native_country,
    stddev(capital_loss) over (partition by native_country) as stddev_capital_loss_by_native_country,
    avg(hours_per_week) over (partition by native_country) as avg_hours_per_week_by_native_country,
    max(hours_per_week) over (partition by native_country) as max_hours_per_week_by_native_country,
    min(hours_per_week) over (partition by native_country) as min_hours_per_week_by_native_country,
    stddev(hours_per_week) over (partition by native_country) as stddev_hours_per_week_by_native_country,
    income_bracket
from
    `buyma-analytics`.`techblog_202201_dev`.`stg_census_adult_income`
order by
    1

このようにして、221個の特徴量を生成することができました。

Appendix:packageの利用

dbtにはpackageというライブラリのようなものがあります。*4

例えば、dbt-utilsには、get_column_valuesというカラムのユニークな値のリストを取得することができます。one-hot encodingを行いたい場合は、下記のように書くことができます。*5

{%- set categorical_column = "sex" -%}
{%- set ref_table = "stg_census_adult_income" -%}
{%- set unique_values = dbt_utils.get_column_values(ref(ref_table), categorical_column) -%}
select
    {%- for value in unique_values -%}
    case when {{categorical_column}} = "{{value}}" then 1 else 0 end as {{categorical_column}}_{{value}},
    {%- endfor %}
from
    {{ref(ref_table)}}

↓ compile

select
    case when sex = "male" then 1 else 0 end as sex_male,
    case when sex = "female" then 1 else 0 end as sex_female,
from
    `your-project`.`your_dataset`.`stg_census_adult_income`

まとめ

個人で使っているレベルですが、SQLをエディターで書いていた時より効率よくクエリを作成することができとても便利に感じています。今回紹介しきれなかったtestdocsなども業務では活用しています。今後はBigQueryMLと組み合わせて、前処理〜モデルの学習・推論までを全てdbt x BigQueryで完結させられたらなと考えています。*6


株式会社エニグモ 正社員の求人一覧

hrmos.co

*1:めちゃくちゃ多段のwith句を使っていることが多いです。

*2:dbtの環境構築方法は丁寧に紹介くださっている記事がたくさんあるので割愛します。

*3:ここも汎用的な処理にしたかったのですが手を抜きました。

*4:packageの導入方法については割愛します。

*5:BigQueryのカラム名として使えない文字列が入っているとエラーになるので要注意です。

*6:dbt-mlというBigQueryMLを実行するpackageがあります。

BUYMAサービスを運営するエニグモ/VPoEインタビュー「メンバーがやりたいことを後押しするのが自分の仕事」

f:id:enigmo7:20211223152715p:plain

こんにちは、人事総務グループの廣島です。エニグモで中途・新卒採用、採用広報などを担当しています。

エニグモは「世界を変える、新しい流れを。」をミッションに、世界166ヶ国に900万人以上の会員を擁するソーシャルショッピングサイト「BUYMA」を運営しています。

今回は、エンジニア部署の部長小澤さんのインタビューをお届けします。 エンジニア組織や開発体制、エニグモのカルチャーなどについて伺いました。

※この記事は Enigmo Advent Calendar 2021 の25日目の記事です。 あっというまでアドベントカレンダーも最終日です!


 

目次


 

まずは簡単に経歴や自己紹介をお願いします

前職は新卒で入社したSIerで勤怠管理や人材管理などのパッケージシステムの開発をしていました。会社の中でも比較的開発を担当できる部署でしたが、役職が上がると開発から離れ管理がメイン(電話片手にエクセルとにらめっこみたいな)となる為、開発に関わりつづけたい、Webに行きたいと思い転職を決意し、ご縁がありエニグモに入社しました。 入社してかれこれ10年が経ち、現在エンジニアの部長を務めています。

f:id:enigmo7:20211224124143p:plain

エンジニア組織や開発体制について

現在のエンジニアの開発組織について教えて下さい

現在、エンジニアの組織は、業務委託として参画してくださっている方も含め約50名の組織となっており、4つのグループ(インフラグループ、データテクノロジーグループ、アプリケーション開発グループ、グローバルグループ)に分かれています。

小澤さんが入社してから今までで組織はどのように変わりましたか?

入社した当時、エンジニア組織としては8人程でしたので、それから比べると組織はだいぶ大きくなりましたね。私が部長になってからは、社員15人くらいまでは部長以下の役職を設けず全員フラットな組織でしたが、組織の拡大とともに、各グループにマネージャーの役職を置き、現在の組織体制となりました。

データテクノロジーグループはここ数年で新しくできた組織ですが、グループ立ち上げの経緯を教えて下さい

データテクノロジーグループは元々アプリケーション開発グループの一部で、性能改善やバックエンドの安定化などを担うチームでした。全社的にデータドリブンな環境が加速していく中で、データ収集や機械学習、検索性向上などのデータ領域に対して専門性の高いチームへ進化していき、今のデータテクノロジーグループへとなりました。 これらの進化は会社側からのトップダウンの方針ではなく、現場のマネージャーからのボトムアップで組織が進化していった経緯があります。

開発体制の特徴は?

現在、大きく3つの機能(購入者向け機能、出品者向け機能、サービスインフラ)別チームに分かれて開発案件に対応しています。各機能別にエンジニア、デザイナー、ディレクター、データアナリスト、ビジネスサイド(CS・MD等)が組織横断でプロジェクトにアサインされ開発を進める、機能別の開発体制になっています。

職種で役割を完全に分けてしまうのではなく、企画・設計段階からみんなで意見を出し合い、サービスや機能を作っていくのが特徴です。

機能別の開発体制に移行した背景やその後の変化はありましたか?

元々はプロジェクトへのメンバーのアサインは、プロジェクトの難易度や特性とメンバーの得意分野、やりたいこと、稼働等を見て判断しておりました。 しかし、BUYMAは1つのサービスとして成り立つために様々な機能が組み合わさっています。 例えば、購入者向けの機能(商品詳細ページやレコメンド、クーポン等)と出品者向け機能(出品管理・ショップ連携・お問い合わせ管理等)ではサービスの性質・要件や抱える課題は異なり、仕様は複雑になっているため、開発メンバー全員が全ての機能の特性をキャッチアップすることは難しくなっていました。

そこで、開発メンバーが機能(ドメイン)別に特化することで、専門性を持ち効果的にスピードを上げて案件に対応できるのではないかと考え、機能別開発体制となりました。

開発体制の変更によって、機能ごとにエンジニア一人一人が当事者意識を持って仕事に取り組むことができ、エンジニアもプロダクト開発の目標設定への責任感・コミット力があがったと思います。

BUYMAの開発を行う魅力は?

まずは、BUYMAというそれなりのユーザーがいるECサイト・大規模サービスに関われることでしょうか。やった事への影響も大きいですし、画面を変えれば良くなったとユーザーから褒めていただいたり、時には厳しいご指摘をいただくなど反応もありますし、トラフィックもあるのでパフォーマンスチューニングのやりがいもあります。また、ローンチして17年を超えるWebサービスなので、古いシステム・技術もあり直すことも多いので、そういうところが好きな方であればやりがいに感じる方もいるかと思います。

開発や組織の課題について

前段でもお話ししましたが、17年を超えるWebサービスの為、古いシステムやレガシーな技術もいっぱいあるところですね(新しい技術もどんどん導入していますが)。リフレッシュしないと開発速度が落ちてしまうので、常に技術のアップデートは行う必要があります。 技術のアップデートをするにも、機能やデータベース等が複雑に絡み合っている為、1つの技術を変えると他の機能への影響範囲も大きいため、それぞれチューニングが必要です。そのあたり開発・運用サイクルを効率よく回すためにも、いくつかの改善案を検討しています。

上記のような課題もある為、新しいサービスや機能を開発したいというエンジニアだけでなく、レガシーな技術をモダン化したい、開発が上手くまわる仕組みや環境を作りたい・整えたいというエンジニアも組織全体としてスピードアップして開発する為には必要であり、活躍の機会があります。

エンジニア採用で大切にしていることや、活躍するメンバーとは

エンジニア採用で大切にしていることはありますか?

新メンバーが入社した際にも、その人がやりたいことをやる方がきっといいと思っているので、「こういうことがやりたい」というモチベーションが高く、且つ自走力があり実行できる人がいいですね。実は、私も上からあれしろこれしろとあまり言われないので、エンジニアにもあれしろこれしろ言わないようにしています。やりたい人に任せることが一番いい結果を生むと感じています。

また、成長意欲の高い人や向学心の強い方にはオススメな環境です。 なぜなら、BUYMAはさまざまな機能が絡み合うため、システムとして非常に複雑になっています。新メンバーが、特に若手メンバーがシステムの概要をキャッチアップ・理解するまでに時間がかかり若手に与える適切な課題・タスクを切り分けるのが難しい場面もあります。 「教科書で学んできたことが教科書通りにはいかず、開発する上であれもこれも詰め込まれるので、一気に10人くらいに殴られる感覚になる」と、ある若手メンバーが言っていたのが印象的です。

1つ1つ少しずつ成長したい人にとっては、最初は我慢が必要かもしれません。その分成長のスピードは早く、BUYMAで開発ができればだいたい何でも開発できるようになると思うので、臆せずチャレンジしていただきたいですね。

エンジニアの雰囲気や活躍するメンバーの特徴は?

色々なキャラクターの人がいますが共通項としては、みんなまじめで何事も一生懸命な頑張り屋が多い印象です。また、エンジニアに関わらず、エニグモのメンバーはいい人が多いと思います。

活躍するメンバーの特徴は、アイデアがある人じゃないでしょうか。アイデアというのは、新規の提案だけなく、定例やMTG内でも自分の意見や考えを発言し、発言して終わりではなく行動が伴う方です。そういったメンバーが周囲からも信頼を得られ活躍しているように感じます。

f:id:enigmo7:20211223160246p:plain

エニグモの技術選定について

メンバーやマネージャーからのボトムアップで決まる場合が多いですね。 経営判断や、エンジニアのマネージャー会議等で、これを導入していこうというよりは、 専門性のある各メンバーが今の課題の中からこの技術が良いのでないかと判断し導入します。

もちろん例外もあります。規模感にもよりますが、さすがにBUYMA全体の言語をPHPからRubyへ変更しようとなった時や、BUYMAのインフラ環境をオンプレからAWSへ移行等の規模が大きい案件の場合は役員プレゼンし承認を得ています。そのほか、技術導入による影響範囲が大きい場合や、導入にお金が絡む際も私のところに相談が来ますね。

エンジニアのキャリアについて

どのようなキャリアアップの選択肢がありますか?

スペシャリストとしてもマネージメントとしてもキャリアアップの選択の機会があります。 役職や肩書がなくてもスペシャリストとして高い報酬が得られる給与体系となっており、本人の志向や経験スキルに応じて柔軟なキャリアステップを歩んでいただけます。

マネージャーへはどのようにアサインされるのでしょうか?

エニグモのエンジニアの特徴としては、チームを作りたいという人よりも、スペシャリストになりたい人の方が多いように感じます。その為、マネージャーを任せたいメンバーには1on1等でやってみる?と聞いています。

一概には言えませんが、任せたいと思うエンジニアはすでにメンバーの役割を超えて、チームをまとめていたり、PM・リーダーとしての動きをしているので、そこからマネージャーをやってみようかとなるパターンも多いように感じます。意図的というよりも自然とそうなっています。

フロントエンドエンジニア、サーバーサイドエンジニアを明確に分けていないのも特徴ですよね

はい。エニグモではエンジニアの担当領域をフロントエンド、バックエンドではなく、サービス・プロジェクト単位でアサインしている為、フルスタックな知識・経験やスキルをつけることが出来ます。

なぜ分けていないかというと、フロントエンド・バックエンド両方できた方がやりがいや完成した際の達成感があると考えているためです。 せっかく画面があるサービスなので裏側から表に出てくるまでの一連の流れをやった方が楽しいと思うため、やりたいメンバーにはサーバーサイド、フロントエンドとわずにプロジェクトにアサインし任せています。

部長として大切にしていることはありますか?

みんなが楽(ラク)になればよいなと思っています。楽というのは業務が上手くまわる感じですね。

1on1でも、今の仕事は楽しいか、何をやりたいのかを聞くようにしています。何が好きで何がやりたいかを聞いてそれを実現できる環境を作ることが今の私の役割だと思ってます。 経営陣もエンジニア組織のみならず、各部門の意思を最大限尊重してくれる文化なので、部長としてもやりやすい環境だと思っています。


 

以上、エンジニア部長の小澤さんのインタビューでした! こちらで、 Enigmo Advent Calendar 2021 は以上となります。今年も色々な記事がありましたね。 2022年もよろしくお願いします!


株式会社エニグモ 正社員の求人一覧

hrmos.co

オンプレミスMySQLをAuroraへ移行する際に、困ったこととその対応

こんにちは、インフラエンジニア の 加藤(@kuromitsu_ka)です。

この記事は Enigmo Advent Calendar 2021 の 24 日目の記事です。

今回は、オンプレミスのMySQLを、Auroraへ移行する際、困ったことと対応したことを記載します。

移行方式をざっくりいうと、オンプレミスのMySQLより取得した、論理バックアップ(mysqldump)とバイナリログを使用してAuroraへ移行しました。移行のため、リストア環境と、リストア後のデータのチェック環境を構築したので先にその説明を記載して、困ったところと対応を記載していきます。

リストア環境

オンプレミスMySQLサーバから取得した、バックアップファイルをAuroraへ適用する環境を作りました。EC2のMySQLと、Auroraとでレプリケーションを貼り、EC2のMySQLにデータを投入してリストアしました。バックアップファイルのダウンロードや、リストアジョブのスクリプト実行は、リストアジョブサーバから実行します。 f:id:enigmo7:20211220161725p:plain

リストア後のデータのチェック環境

Auroraへのデータのリストア後に、データの差分確認をする環境としてMemcachedも用意しました。それぞれのテーブルのデータの合計チェックサム値をMemcachedに入れて比較しました。こちらも、チェックのジョブスクリプト実行は、リストアジョブサーバから実行します。 f:id:enigmo7:20211222002116p:plain

困ったこといくつか。

本題のAuroraへの移行で困ったことは、5個あり、順を追って記載します。

  1. やんごとなき理由で、Auroraへのリストアに物理バックアップが使えず困った。
  2. Auroraの仕様上、バイナリログを直接適用できずに困った。
  3. Auroraへのリストアで、バイナリログの適用に2日間もかかって困った。
  4. リストア環境を作ったものの、バイナリのログ適用がコケて困った。
  5. リストア後、バイナリログ適用したデータの時間がなぜか9時間ずれて困った。

困ったこと その1

やんごとなき理由で、Auroraへのリストアに物理バックアップが使えず困った。

Auroraへのリストアには、物理バックアップが、サポートされていました。こちらが使えると安心だったのですが、問題があり使用できませんでした。結果的に論理バックアップから、リストアすることとなりました。

物理リストアできなかった原因

オンプレミスのMySQLには、パラメータ(innodb_undo_tablespace)が設定されていました。Auroraでは、こちらが変更不能になっているため、物理バックアップでは、Auroraへのリストアでコケていました。パラメータ変更には、MySQLサービスの再起動が必要なため、論理バックアップを使用することとなりました。

  • エラーログ
    Auroraのerror/mysql-error-running.log、error/mysql-error.logより確認したエラーログ
[ERROR] InnoDB: Unable to open undo tablespace './/undo001'.
[ERROR] InnoDB: Plugin initialization aborted with error Generic error
[ERROR] Plugin 'InnoDB' init function returned error.
[ERROR] Plugin 'InnoDB' registration as a STORAGE ENGINE failed.
[ERROR] Failed to initialize builtin plugins.
[ERROR] Aborting
  • 問題のパラメータ
    undo ログが分割するテーブルスペース数を設定するパラメータ(innodb_undo_tablespace
物理リストアできなかった値
- innodb_undo_tablespace = 2
物理リストアできる値
+ innodb_undo_tablespace = 0
パラメータ名 変更可能
innodb_undo_tablespace いいえ

困ったこと その2

Auroraの仕様上、バイナリログを直接適用できずに困った。

Auroraへは、直接バイナリログ適用できない仕様で、リストア環境を用意することになりました。BINLOGコマンドは、スーパーユーザーの権限での実行が必要なのですが、Aurora では、スーパーユーザー権限を利用することはできないそうで、コケてしまいました。そのため、リストア環境としては、EC2にMySQLを作成して、レプリケーションを貼りました。
※他社事例では、バイナリログをデコードして生のクエリを直接Auroraに適用する方法もありましたが、移行作業をしていた当時は見ていませんでした...。

  • 適用コマンド
# mysqlbinlog --no-defaults --database=${DB_NAME} --start-datetime=${START_TIME} --stop-datetime=${STOP_TIME} bin-log.00xxx 
  | mysql -h ${AURORA_ENDPOINT} -P 3306 -u admin -p
  • エラーログ
ERROR 1227 (42000) at line 7: Access denied; you need (at least one of) the SUPER privilege(s) for this operation

困ったこと その3

Auroraへのリストアで、バイナリログの適用に時間がかかって困った。

Auroraへのリストアでは、バイナリログ適用(1桁GB程度でも)に2日間ほどかかりました。こちらは、AuroraのDBインスタンスのマシンリソースの増強と、Auroraのパラメータを変更したところ高速化できました。結果、バイナリログ適用は、20分程度に納まるようにできました。

  • 対応したこと
    AuroraのCPU、メモリ使用率を見つつインスタンスタイプを変更
サーバ インスタンスタイプ CPU メモリ(GB)
オンプレミスMySQL 12 252
EC2のMySQL(リソース増強前) db.t3.small 2 2
EC2のMySQL(リソース増強後) db.r6g.4xlarge 16 128
  • Auroraのパラメータより、バイナリログ出力を一時的にOFFに変更
Binlog_format = OFF

困ったこと その4

リストア環境を作ったものの、バイナリのログ適用がコケて困った。

バイナリログ適用がタイムアウトしたり、2,3割適用できたところでコネクションエラーになったりもしました。デバッグの際も、タイムアウトとコネクションエラーしか出ず、原因がぱっと見ではわからずで困りました。結果として、リストア環境のMySQLパラメータを変更することで、解決しました。

  • エラーログ
ERROR -- : Lost connection to MySQL server during query (Mysql2::Error::ConnectionError)

MySQLパラメータ変更

リストア環境のEC2のMySQLで、パラメータチューニングを行いました。ひとまず、タイムアウト関係のパラメータを操作しましたが、一部バイナリログデータを適用できず止まってしまうものもありました。続いて、パケットサイズのパラメータも変更しましたが、エラーは続いていました。最終的に、プロセスが、十分なメモリを確保できていないのかなと考えて、メモリキャッシュ関係のパラメータを変更して解決しました。

connect_timeout = 172800
net_write_timeout = 172800
net_read_timeout = 172800
wait_timeout = 172800
interactive_timeout = 172800
  • パケットの最大サイズのMySQLパラメータ(最大値に設定)
max_allowed_packet = 1073741824
  • メモリキャッシュ関係のパラメータ(最大値に設定)
    ※変更したパラメータ「innodb_buffer_pool_size」は、読み込み、書き込みのパフォーマンス向上にも使われるパラメータです。
innodb_buffer_pool_size = 1G

困ったこと その5

リストア後、バイナリログ適用したデータのうち、タイムスタンプ関係のデータで、なぜか9時間ズレが発生して困った。

バイナリログで適用したデータのみ、何故か9時間ズレるという問題も経験しました。MySQLタイムゾーン関係の、パラメータを変更して対応しましたが、解消しませんでした。結果として、バックアップ取得元の、バイナリログフォーマットのパラメータを変更して解決しました。

タイムゾーンのパラメータ
オンプレミスのMySQLも、Auroraも、リストア環境のEC2のMySQLJSTに設定していました。

  • バイナリログ取得元のMySQLと、EC2に作成したMySQLのパラメータ
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | JST    |
| time_zone        | SYSTEM |
+------------------+--------+
  • Auroraのパラメータ
    Auroraの場合、パラメータ「time_zone」が、データベースのデフォルト値になります。
+------------------+------------+
| Variable_name    | Value      |
+------------------+------------+
| system_time_zone | UTC        |
| time_zone        | Asia/Tokyo |
+------------------+------------+

バイナリログフォーマットのパラメータ

  • binlog_format」をMIXEDから、ROW変更
    レプリケーション元の binlog_format を ROW とすることで、バイナリログ適用の際、クエリでなく実行結果がレプリケーションされるようになるそうでした。バックアップ取得元の、オンプレミスのMySQLにてパラメータ変更した結果、無事、データが一致しました。
バイナリログを適用した、データのタイムスタンプのデータが9時間ズレたパラメータ
- binlog_format = 'MIXED'
バイナリログを適用した、データで時間のズレがなかったパラメータ
+ binlog_format = 'ROW'

感想

オンプレミスMySQLのAurora移行は、たいへんでしたが楽しかったです。

明日の記事の担当は 人事総務 の 廣島 さんです。お楽しみに。


株式会社エニグモ すべての求人一覧

hrmos.co

Apache Airflowを使ってみた感想

こんにちは。サーバーサイドを担当している橋本です。 この記事は Enigmo Advent Calendar 2021 の23日目の記事です。
普段は Ruby on Railsを書くことが多いですが、とあるプロジェクトでAirflowを使った既存バッチの性能改善を行いました。プロジェクトはAirflowについて全く知らない状態からスタートして学ぶことが多かったので、この記事でAirflowの紹介と開発の感想を書いていきたいと思います。

Airflowとは?

Airflowとはワークフローの管理ツールで、あるタスクを実行したら次のタスクを実行するといった形で一連のタスクを管理するものになります。Airflowではjobの実行順や依存関係をDAGで定義していて、DAG自体はPythonで作成されています。

簡単にDAGのコードのご紹介です。(環境構築は省略させていただきます。)
下記のプログラムは 文字列をprintするタスクを順番に実行するものです。DAG で実行時刻やDAGの名前の設定をします。PythonOperatorを使ってタスクを定義し、一番下の行の execute_task1 >> execute_task2 で実行するタスクの順番を定義します。

import airflow
from airflow.operators.python_operator import PythonOperator
from airflow.models import DAG
from datetime import datetime

args = {
    'owner': 'airflow'
}

dag = DAG(
    dag_id = 'advent_calendar_tasks',
    default_args = args,
    schedule_interval = '0 0 * * *'
)

def task1():
    print('task1')

def task2():
    print('task2')

execute_task1 = PythonOperator(
    task_id = 'execute_task1',
    retries = 2,
    python_callable = task1,
    dag = dag)

execute_task2 = PythonOperator(
    task_id = 'execute_task2',
    retries = 2,
    python_callable = task2,
    dag = dag)

execute_task1 >> execute_task2

管理画面にアクセスするとDAGの情報を見ることができ、実際に実行することもできます。

f:id:enigmo7:20211222093417p:plain

使ってみてよかったこと

モニタリングがGUIから管理できる

DAGのスケジュールや実行状況を同一画面で見ることができるので、モニタリングがしやすかったです。また、DAGの実行やスケジュールの on/offの設定も管理画面からできるので、コマンドを実行する手間を減らすことができたのが便利でした。

タスク単位で確認できる

管理画面のDAGの詳細画面ではタスクを個別で見ることができます。プログラム作成時にタスクを上手く分割すればDAGが実行するタスクを簡単に確認できます。

f:id:enigmo7:20211221144035p:plain

タスク実行に便利なライブラリが提供されている

実際のタスクではSQLを実行したりファイルをアップロードする必要があると思いますが、DAGで使用できるライブラリが提供されています。プロジェクトではFTPをアップロードするために FTPHookというライブラリを使いましたが、Pythonで標準で使える ftplibよりも簡単にFTPを扱うことができました。

使ってみて難しかったこと

Pythonに不慣れだったこと

プロジェクトではRuby on Rails => Pythonへ刷新したのですが、普段はRubyを書いているのでPythonに慣れるのが大変でした。

セキュアな情報の設定が初見だとわかりにくかったこと

Airflowではセキュアな情報をConnectionsで管理していますが、最初はDAGに接続情報を書いてしまいました。あらかじめドキュメントを読んでAirflowのコンセプトを理解する必要があると感じました。

まとめ

ここまでAirflowの紹介と感想を書きましたが、開発中は学ぶことが多く新鮮な気持ちで開発ができました。 明日の記事の担当は インフラエンジニアの加藤さんです。お楽しみに。


株式会社エニグモ すべての求人一覧

hrmos.co

macOSでdocker環境をどう早くしたか

こんにちは、サーバーサイドエンジニアの Steven です。

この記事は Enigmo Advent Calendar 2021 の22日目の記事です。

今回は Vagrant 環境をリプレースすることとなった Docker 環境をどう早くしたかについて説明します。

スタート地点は Vagrant 環境

エニグモでは以前から VirtualBoxVagrant によるローカル環境を使って、開発してました。 使い勝手は完璧ではなかったのですが、開発する分には問題がとくになく長年活用されました。 ただし、それは構築ができたらの話で、構築時間が長いのと、時間が立てば立つほど自ずと新しい構築エラーが発生して、随時対応しないといけない状態でした。 エンジニアの場合、超えられない問題ではなかったのですが、デザイナーなどテクニカルな知識がそれほどない方だと、サポートしてもハードルがかなり高かったです。 VM 内で使っていた OS も古いバージョンの CentOS で、いずれ更新しないといけなかったです。

Docker 環境ができました

Docker で新しいローカル環境を作ることで以上の問題を解消できないかと動いてくださったエンジニアがいました。 そうすれば、構築時間の短縮と、安定性の改善、使い勝手の向上を実現できるからです。

構築して想定通り改善はできましたが、代わりに新しい問題が現れました。 それもよくあるパターンのようで、Vagrant 環境よりパフォーマンスが悪く、使い物にならない環境になってしまいました。 ローカル環境とはいえデータセンターにあるサーバーとつなげたりするので、もともとの Vagrant 環境でもはじめからそれほど早くはなかったです。 なので、それ以上パフォーマンスが落ちると、対策が必須となってしまいます。

当時はチューニングを試みましたが、根本的な改善が見られず、Docker 環境の導入は一旦保留となりました。

レスポンスタイム比較

ページ Docker環境 Vagrant環境
トップ 9.95s 1.07s
検索結果 9.22s 1.89s
マイページ 9.30s 1.79s

Docker Desktop for Mac について

この場合 Docker 環境のパフォーマンスが悪かったのはコンテナーと macOS 間のファイル IO のパフォーマンスが悪かったからです。 アプリケーションコードをすべてメモリーに保持するなど、ファイル IO がそれほど発生しないアプリケーションの場合は問題にならないこともあると思いますが、私達の場合はファイル IO がどうしても多く発生する環境なので、必然的にパフォーマンスが悪かったです。

Docker Desktop for Mac では、Linux 環境と違って、コンテナーはそのまま macOSカーネルに実行されておらず、macOS 上で動く VM の中にある Linux カーネルによって実行されています。 なぜそうなっているかというと、macOSカーネルではコンテナー化のサポートがなくて docker のようなコンテナーを実装することができないからです。 なので、bind ボリュームを通してコンテナー内から macOS 側にあるファイルにアクセスする時は、VM の中から osxfs(レガシー)か gRPC FUSE というファイルシステムレイヤーを通して、macOS 側のファイルが読み込まれます。 ただし、抽象化が多いところから、ケースによってそのレイヤーがかなり遅くて、ネイティブのアプリケーションと比べ物にならないことが珍しくないです(当然といえば当然ですが)。

Docker Desktop for Mac でキャッシュのオプションもありますが、試した結果それほど影響が大きくなくて、違いに気づけるかどうかというレベルでした。

Docker チームでパフォーマンスの問題を認識していて、改善を以前から試していますが、ファイルシステムの実装はかなり難しいもので、トレードオフが多いです。 パフォーマンスを高くするために工夫すると、整合性などの面で新しい問題が現れたりします。 パフォーマンスは改善傾向にありますが、満足の行かないケースがまだ多いと思います。

Mutagen とは

Docker Desktop が提供するオプションだけでは解決できない問題なので、サードパーティーによる解決策を探しました。 最初は docker-sync を試しましたが、最終的に Mutagen に落ち着きました。

Mutagen はファイル同期とネットワークのフォワーディングのためのツールで、本来はクラウドにあるリソースをローカル環境で使うためのものかと思いますが、最近は docker compose のサポートが追加されて、docker 環境と合わせて使うことが可能になりました。 ファイル同期は rsync によるものなので、パフォーマンスがよくて、かなり堅牢なものです。

docker compose と合わせて使う場合は macOS とコンテナーの間に、VM 内に同期されているファイルのコピーが用意されます。 コンテナ内から本来 bind であったボリュームへのファイルアクセスが発生した場合は macOS 側のファイルを読みに行かず、VM のファイルにのみアクセスするようになります。 アプリケーションのファイル処理が VM 内で完結するため、macOSVM 間のファイルのやり取りが激減して、パフォーマンスのボトルネックがなくなります。

Docker チームでも Docker Desktop に Mutagen を正式的に導入する動きが以前ありましたが、導入で追加の複雑さが生じることから、やめることとなったようです。その代わりに gRPC FUSE を優先するようになりました。

導入例

Mutagen の導入はかなり簡単です。

まずはbrewで Mutagen をインストールします。

$ brew install mutagen-io/mutagen/mutagen-beta # 現在はβバージョンが必要です

続いて、docker-compose.ymlmacOS 側のファイルにアクセスするためのボリュームを用意します。

services:
  bm_on_rails:
    # ...
    volumes:
      - rails-source-sync:/bm_on_rails

  bm_php:
    # ...
    volumes:
      - php-source-sync:/home/web/bm_php

volumes:
  rails-source-sync:
  php-source-sync:

最後に、同じファイルで、x-mutagenの項目の配下に Mutagen の設定を指定します。

x-mutagen:
  sync:
    rails-source-sync:
      mode: 'two-way-resolved'
      alpha: './volumes/bm_on_rails'
      beta: 'volume://rails-source-sync'
    php-source-sync:
      mode: 'two-way-resolved'
      alpha: './volumes/bm_php'
      beta: 'volume://php-source-sync'

alphabetaは同期のエンドポイントとなります。 意味合いはmodeによりますが、以上ではalphamacOS 側のパス、betaは Docker のボリュームを指しています。 modeにはいくつかの選択肢がありますが、コンフリクトを自動解消するとして、alphaの変更をどんな時も優先したい場合はtwo-way-resolvedが適切です。 詳しくはこちらをご確認ください。

セットアップができたら、次は Docker 環境をmutagen compose upで立ち上げます(Mutagen の新しいバージョンではmutagen-compose up)。 docker composeコマンドを使うと、Mutagen の処理がスキップされるので、間違えないよう注意してください。 ただのラッパーなので、docker composeでできることはmutagen composeでもできるはずです。 ちょっと不便かもしれませんが、Mutagen の開発者側でdocker composeをそのまま使えるように検討されているようです。

環境の初回起動に macOS 側のファイルが VM 内にコピーされるので、ファイルの量によって時間がかなりかかってしまう可能性があります(私達の場合は 10分ぐらい)が、二回目以降は Mutagen を使ってないのとあまり変わらなくなります。

導入後、アプリケーションのパフォーマンスは Vagrant 環境よりやや早くなりました。 データセンターへのアクセスがどうしても発生するので、そのパフォーマンスで目標を達成としました。

注意

Mutagen は Docker Desktop のバージョンに依存していますので、Mutagen のバージョンと Docker Desktop のバージョンに気をつけてください。 Docker Desktop のアップデートが来る度にすぐアップデートすると、Mutagen が動かなくなってしまう恐れがあります。

Mutagen の docker compose サポートはまだβですが、バグがほぼなくとても安定しています。

調整

デフォルトで macOS 側のファイルすべてが VM 内に同期されるので、.gitディレクトリなど VCS 用のファイルを同期したくない場合は追加の設定が必要となります。 任意のファイルの同期をスキップすることも可能です。詳しくはこちらをご参照ください。

x-mutagen:
  sync:
    defaults:
      ignore:
        vcs: true
    # ...

特に設定がない状態では Mutagen に同期されているファイルのオーナーとグループ、パーミッションはコンテナー内でデフォルトなものとなってしまいます(オーナーとグループはおそらく root となります)。実行権限のみ同期されます。 なので、コンテナー内のファイルのオーナーとグループ、パーミッションを調整したい場合は追加の設定が必要となります。 詳しくはこちらをご確認ください。

x-mutagen:
  sync:
    # ...
    php-source-sync:
      # ...
      configurationBeta:
        permissions:
          # php コンテナー内ではファイルのオーナーとグループを apache にする
          defaultOwner: 'id:2000'
          defaultGroup: 'id:2000'

同期オプションは他にもいろいろありますので、必要に応じてご確認ください。

同期セッション重複問題

Mutagen を導入してから、社内で特にファイル同期に関する問題が報告されなかったのですが、少しずつ、MacBook の CPU 使用率が高い、見覚えのない差分がgit statusに出てる、などと相談が来るようになりました。

差分の問題はファイル同期と関係がありそうだと思ったので、その方向で調査を進めたら、相談者の MacBookmutagen sync listが本来2つしかないはずのセッションを大量出力しました。

問題出力

--------------------------------------------------------------------------------
Name: rails-source-sync
Identifier: sync_93JIPMqNq5WkYV20nV9Wq4XdvyBr3CXz3oonMfIkyYQ
Labels:
    io.mutagen.compose.daemon.identifier: JH67_K5QB_5FG6_F4UH_OG45_7EKG_LBBY_7IPY_Z4ME_IKQY_HVSG_PPTU
    io.mutagen.compose.project.name: docker_buyma
...
--------------------------------------------------------------------------------
Name: php-source-sync
Identifier: sync_bzryoXJaLbxevdit2ODkZuGz2RChyN2C2W5wS8CdbdU
Labels:
    io.mutagen.compose.daemon.identifier: JH67_K5QB_5FG6_F4UH_OG45_7EKG_LBBY_7IPY_Z4ME_IKQY_HVSG_PPTU
    io.mutagen.compose.project.name: docker_buyma
...
--------------------------------------------------------------------------------
Name: php-source-sync
Identifier: sync_FaC9uwjuhGziEeggNVbPI2EFgGUE1sxtKArzva4rSck
Labels:
    io.mutagen.compose.daemon.identifier: T3PW_AONQ_MWDI_T5BO_Z6EH_6PQB_6CJZ_336T_M2KO_AXQH_ZSAQ_DQ7E
    io.mutagen.compose.project.name: docker_buyma
...
--------------------------------------------------------------------------------
Name: rails-source-sync
Identifier: sync_tPPnFvmEjlwKhLtkrudgukM3Qc7AHdTOc0QANYjwAmN
Labels:
    io.mutagen.compose.daemon.identifier: T3PW_AONQ_MWDI_T5BO_Z6EH_6PQB_6CJZ_336T_M2KO_AXQH_ZSAQ_DQ7E
    io.mutagen.compose.project.name: docker_buyma
...
--------------------------------------------------------------------------------
...

出力を見てわかりますが、同期セッションが重複しています。 設定は一緒ですが、daemon.identifierというものだけがそれぞれ違います。

daemon.identifierは Docker デーモンの id です。 デーモンはもちろん一つしかなくて、再起動しない限り id も変わらないはずです。

問題は Docker 開発環境を終了せず、MacBook を再起動すると、発生していました。 原因としては再起動前に Mutagen のセッションを終了しなければ、再起動後に Docker 環境を立ち上げた時、古い同期セッションが残っていながらも、Docker デーモンの id が変わった影響で、同期セッションがまだ作成されてないと Mutagen が判断して、新しい同期セッションを作ってしまうということでした。

該当するイッシューはあります(問題を解消できないか検討中のようです)。 https://github.com/mutagen-io/mutagen/issues/243

対策としては Docker 環境起動後にmutagen sync listの出力を確認して、重複したセッション(現在の Docker デーモン id を使ってないセッション)があった場合、mutagen sync terminateでそのセッションを終了するようにスクリプトを作成しました。

MacBook 停止の際に Mutagen の同期セッションを必ず終了するようにするのも考えられる対策です。

M1 対応

新しい MacBookARM アーキテクチャーの M1 チップを使うことで macOS の業界で動かなくなってしまったものが多くあります。 なので、M1 対応をした時はもしかすると Mutagen が動かなくなってしまうと懸念しましたが、問題なく動きました。 インストールで調整は必要なく、同期も支障なく行われていますので、M1 で使う分には問題ないと思います。

docker-sync について

Mutagen を使うようになる前に 0.5.1 の docker-sync をまず試しました。

docker-sync は ruby の gem と unison を生かした、Docker Desktop 専用のファイル同期ツールです。 仕組みも設定方法も Mutagen に似ていますが、Mutagen と違って macOS 側で動くプログラムが多く、rbenv/ruby などのインストールが必要です。 Vagrant での環境構築ではそれらのインストール時に様々な問題が発生していたため、今回は rbenv/ruby などのインストールは避けたかったです。

docker-sync で初回同期は問題なくて、パフォーマンスも Mutagen と同じぐらい改善されましたが、ファイル同期が不安定で、macOS 側でファイルが変わっても、コンテナー内に反映されないことが多々ありました。 ファイル同期を強制するにも docker-sync のデーモンを再起動するしかなく、そうする度に CPU 使用率が跳ね上がって、MacBook がドライヤーなみにうるさくなったりしていました。 docker-sync のイッシューを確認したら、開発者が問題を認識していても解決策が思いつかない状態のようでした。

なので、以上のことから docker-sync はあまりおすすめできません。

終わりに

Docker Desktop for Mac のファイル同期のパフォーマンスの悪さで悩まされた期間が割と長かったのですが、Mutagen を導入することで完全に解消して、デメリットもほぼないので、同じ悩みを抱えられているなら、ぜひ導入をご検討ください。

Docker チームによる Docker Desktop のパフォーマンス改善に期待したいところですが、Mutagen レベルのパフォーマンスが実現されるまでどれくらい時間がかかるのかがわからない状態なので、そうなるまでサードパーティーに頼るしかないかと思います。

明日の記事の担当はエンジニアの橋本さんです。お楽しみに。


株式会社エニグモ すべての求人一覧

hrmos.co

New Normalなオフィス作り

f:id:enigmo7:20211115184555p:plain こんにちは、Corporate IT/Business ITを担当している足立です。

この記事は Enigmo Advent Calendar 2021 の 21日目の記事です。 代打として2回目の登場です。

2021年の前期はオフィスリニューアルPJの業務に追われていました。
コロナ禍に突入し出社とリモートワーク両方に最適化された環境を構築する事をミッションとして動きましたので、 今回はコーポレートIT目線で実施した事を書きたいと思います。

旧オフィスの課題 電源問題

リニューアルするなら旧オフィスで課題だった部分を改善したいと思いました。
総務的な話になりますが、特に電源については下記問題がありました。

  • 電源
    • 旧オフィス時にはOAタップが少なくタコ足配線かつ数珠繋ぎ状態で、あちらこちらにあり危険な状態
    • 電子レンジを2台同時に利用した際にブレーカーが落ちiMacが強制シャットダウン状態になった事がありアンペア増強・単独系統への見直しが必要

→ リニューアル後
座席毎に6個口OAタップが設置されました。

f:id:enigmo7:20211111175745p:plain
6個口のOAタップ
全てではありませんが、座席にモニターを常設しUSB-Cによる電源供給も可能にしました。 最大65 Wの電力を供給出来るので、PCの電源供給と映像信号が1つのケーブルで対応可能になりました。
f:id:enigmo7:20211111180216p:plain
DELL P2419HC

オフィス レイアウト作成時に社内に設置する電子レンジは執務エリアと電源系統と別にして頂きました。 絶対にサーバールームの電源系統は他に影響しないように手配しました。

電話周り

電話システムはオンプレ型のPBX(レガシーPBX)で構成されていたため、頭を悩ませていました。 これも総務的な話ですが、気づいたら電話周りも担当していたので、どうにかしようと思いました。

課題

  • オフィスに設置する物理的な機器であるため、移設時には工事などが必要(PBX・配線)
  • 運用保守は専門の業者に依頼をしている
  • 今後、移転の場合は通信キャリア、専門業者との調整が必要になり時間がかかる
  • 当然、電話は社内でしか使用出来ない
  • PBXがサーバールームに設置してあり場所を取る(兎に角、邪魔でしょうがない)
    f:id:enigmo7:20211111181622p:plain
    旧 電話構成

→ リニューアル後
クラウドPBXであるDialpadを導入しました。

f:id:enigmo7:20211111182341p:plain
Dialpad
導入した事によりPCやスマホで会社の電話を利用出来るようになりました。
着信については会社が保有している電話番号(03番)にて着信するように、キャリア自動転送を利用してDialpadで発行された050の番号へ転送するようにしました。 ただし、発信はDialpadで発行した050の番号で発信する事になります。
f:id:enigmo7:20211111183643p:plain
シン・電話構成

最近、Dialpadでも0ABJ番号(03番)を別途、機器を設置せずにライセンス購入のみで利用出来るようになりましたので03番で発着信したい場合は、そちらを利用すると良いと思います。
導入後はWEB上で電話関連の設定が出来るようになりました。

  • 転送機能
  • 留守番電話
  • IVR設定(音声ガイダンス)
    他にも色々と機能がありますが、特にIVRが利用出来るようになったので内容に応じてBUYMAカスタマーサポートやfondeskへ誘導出来るようになりました。

キャリア自動転送をする際にエニグモの固定電話回線はISDNだった為、キャリアに依頼してオペレーターの方に対応してもらうか電話機の操作で転送設定をする必要がありました。 ただ、従来の電話機は処分する必要もあり電話機の操作による設定は利用出来なくなり、かと言ってオペレーターの方に依頼する場合、変更まで日数がかかるので思い切って電話回線を光収容化しキャリアのWEBページ上で転送設定が出来るようにしました。

f:id:enigmo7:20211111191750p:plain
IVR構成一部
(この構成はモダンな情シス Kajinariさんの事例を参考とさせて頂きました。)

IP固定電話
電話機は完全に無くすつもりで考えていましたが、バックオフィス部門から電話機が必要と言われたので 5台 POLYCOM VVX 350を設置しました。

f:id:enigmo7:20211111192945p:plain
VVX350
こいつが厄介だったのは、日本国内では購入は可能だが、サポートが受けられないのが難点でした。 お取引があるベンダーさん何社かに問い合わせしましたが、どこも販売までしか対応出来ないと言われ 覚悟を決めた上での購入でした。 不明な部分については基本、英語のマニュアルを翻訳して調べたり勘に頼ったりして設定しました。 それでも分からない部分についてはDialpadサポートの方が教えてくれました。

FAX
コロナ禍直後は複合機から受信したFAXをメール送信→Zapier→Slackで通知、Googleドライブに格納と言う構成でしたが、課題もありました。

  • 複合機の仕様上、用紙が切れるとFAXのメール送信が出来ない
  • 送信が社内のみでしか利用出来ない
  • PBXを撤去したいから電話線引きたくない

課題を無くす為、KDDIのペーパーレスFAXを導入しクラウド化を行いました。 当初は「FAX.PLUS」の導入も検討していましたが、FAXを利用する業務の大半が経理チームで主に銀行とのやり取りだったので、何かトラブルがあった際に日本企業の方が意思疎通が早いのかなと思いKDDIにしました。 WEB上でFAXの送受信が出来れば支障は無いので、特にこだわりはありませんでした。

f:id:enigmo7:20211115101934p:plain
シン・FAX システム構成
導入後、FAX受信はメール添付機能を利用し、旧構成と同じくZapierを利用してSlack上に受信通知、Googleドライブへ保存するようにし FAX送信時はペーパーレスFAXのサイトにログインして使用するので自宅からでもFAX送信出来るようになりました。

受付システム・座席予約(ホテリング)

電話の話にも通ずるのですが、受付をどうするかも課題でした。 従来の受付ではエントランスに電話機を設置し総務担当が取次するスタイルで お客様がお見えになる際は「ご来社カード」と呼ばれる入退室管理の用紙に必要事項を記入して頂くフローでした。

ただ、このままだと下記課題が残ります。

  • 受付の内線電話を撤去しなければオンプレPBXを撤去出来ない
  • 外来者の入退室管理が紙による記入かつ集計作業が業務負担になっている

f:id:enigmo7:20211115120848p:plain
旧エントランス

そこで「ACALL RECEPTION」を導入しました。
導入後、来客通知はSlackに変わり、お客様がお見えになった際は別途、設置したラベルプリンターから入館証が発行されるようになり 内線電話による受電対応が無くなり、入館履歴がシステム化され集計作業も不要になりました。

f:id:enigmo7:20211115120156p:plain
シン・エントランス

ただ、一部部署にて宅配業者がお見えになった際にSlackの通知だと作業中に気づかないと言う相談があった為 宅配業者専用ボタンを作成しACALLとDialpadを連携、そこだけはIP固定電話にて着信する運用となりました。

座席予約システム(ホテリング)
新しいオフィスは面積が今までの半分になる為、フリーアドレスを採用する事になりました。 リモートワークが主流になり、座席数がそこまで必要無くなりますが、その際に上司から座席予約システム導入を検討するよう指示がありました。

f:id:enigmo7:20211115144007p:plain
フリーアドレス
f:id:enigmo7:20211115144049p:plain
フリーアドレス2

個人的には必要なのか?と思いましたが、席のダブルブッキングや出社している人をひと目で可視化する為にも必要でした。 また、WEB MTG用に個室ブースを8箇所設置する為、座席予約システム導入がマストになりました。

f:id:enigmo7:20211115144139p:plain
WEB MTG用個室ブース

導入検討をしていた当時、ベストなソリューション選定に大変悩みました。 現在はコロナ禍を想定した座席予約システムが増えて来ましたが、当時としては・・・

  • 社内のコミュニケーション 円滑化を目的としたシステム(抽選式で座席を決めるシステム)
  • IoTに特化した座席予約システム(各座席にセンサーを設置する為、費用も高額、電池交換も必要)

弊社としてはWEB上かスマホで予約してはい、終わり!ぐらいなシンプルな機能を求めました。 ただ、上記の様に希望する要件と違ったシステムばかりでした。

悩んでいる矢先に受付システムで導入しようとしていたACALLに座席予約(ホテリング)機能がローンチされるとの情報が耳に入りました。 内容を見てみると希望する要件を満たすのではと感じ、受付システムと一緒に導入してみました。

f:id:enigmo7:20211115145935p:plain
座席マップ 一部
導入当初は予約した席にチェックインする作業に慣れない人も居ましたが、現在ではすっかり慣れました。 チェックインとは事前に予約した席に来たらACALLのスマホアプリで座席に貼ってあるQRコードを読み込む作業です。 予約した時刻から15分以内にチェックインしないと予約がキャンセルされます。 この機能が無いと予約したにも関わらず席を利用しない人が現れた際に他の人が利用する事が出来ないので重要な機能です。

特にWEB MTG用の個室ブースは1時間単位で利用する事が多く、システムの利用頻度は高めです。通常予約システムを導入した会社によっては利用しなくなるケースがあると聞いたことがありますが、弊社は業務に欠かせないシステムとなりました。

入退出システム

同時進行でシステム導入や移設手配をしている矢先に今度は入退出システム導入の話が出てきました。 オフィスの一部分だけは入居しているビルが管轄している入退出システムを利用しているのですが、C工事に該当するエリアについては自社で入退出システムを入れる必要がありました。従来ではセキュリティ会社が販売しているオンプレのシステムを利用していましたが、このタイミングでリプレイスする事となりました。 (管理コンソールがIEでしかログイン出来ない・・・)

代表からは「顔認証」で入退出出来るようにして欲しいとオーダーがありました。 当初はクラウドタイプの入退出システムをベースに顔認証を付ける構成を考えていましたが、お見積を取ると費用がかなり高額になる事が分かりました。 顔認証を諦めてもらい、スマホアプリやFeliCaを利用したシステムを役員へ提案しました。

プレゼン時に代表からは「顔認証」を捨てきれない印象があり、再検討するよう指示があり 「色々、見た上で提案しているんだから、他にあるんだろうか・・・」と思っていたので思わず「じゃあ、おすすめのシステムあれば見てみます」と 逆質問に近いように返答したところ、代表の口から「Safieとか」と言われ早速、見てみると「Safie Entrance」なるものが。

クラウド録画カメラのシステムとしては知っていたのですが、顔認証での入退出システムがあるのは知りませんでした。 早速、連絡を取り仕様や費用面を確認すると従来の顔認証システムに比べて圧倒的に安価に導入でき月々の費用もかなり安くなる事が分かり導入が決定。 若干、「こんなに良いものあるなら早く言ってよ」と思いましたが、代表のひと言が無ければ導入出来なかったと思います。

f:id:enigmo7:20211115155257p:plain
SafieEntrance
エニグモでは5箇所に設置しました。事前にSafieEntranceで使用するiPadを準備しケーブルを最小限にする為に電源はPoEにて供給する事にしました。 通信についてはテストした結果、Wi-Fiでも動作は問題ありませんでしたが有線LANで行っています。以前は鍵を使って物理的に入退出していた場所は全て顔認証になりエリア毎に入退出出来る従業員を制限しています。導入してみると顔さえあれば入出出来るのでが大変便利です。以前だと鍵にかけ忘れなどあり、それが無くなったので物理的なセキュリティレベル向上にも繋がりました。

個室ブース

以前に比べWEB MTGの需要が増えました。エニグモでも基本、MTGはZoomで行う事が増え、その度に会議室が不足する事になりました。 その為、上司の案で個室ブースを設置しました。

f:id:enigmo7:20211115161344p:plain
個室ブース
設計当初、防音になると聞いていたので各ブースにはLANケーブルを敷設しました。昔、ITサポートの仕事をしていた時にあるお客様のご家庭に防音部屋があり、その部屋だけWi-Fiが受信出来なかった経験がありました。使用する材質によっては著しくWi-Fiの強度が低下する恐れがあったのでお守り代わりに引くことをリクエストしました。 設置後に通信が不安定になるとWEB MTGに取っては致命傷になる事を危惧していました。実際に設置後にZoomを利用した際にインターネットの速度が不安定ですと表示された事があり、やっぱり引いといて正解だと心から感じました。 現在はオフィスに必要不可欠な場所となり、出社が多い日は予約出来なくなる事もあります。

もし個室ブースを設置を検討している場合はブース内の空調と照明にはこだわった方が良いと思います。 密封されている空間なので空調を重要ですし、窓が無い場合は照明を明るく出来るようする事をオススメします。

最後に

社内ネットワークについて書こうと思いましたが基本、現状維持のままでの移設だったので特段書く事も無く割愛しました。 ただ、Wi-Fi6に対応させようかと思ったのですが、サポート切れか今後会社が移転する機会があればその時で良いかと思ったので何もしませんでした。 あとは社内のAV機器周りについても担当したのですが、それはまた別の機会にでも書きたいと思います。 (これで1本書ける内容なので。)

新しいオフィスになり半年以上経過し、日々働いていると本当に良いオフィスになったと実感します。 このタイミングで様々なものを導入させて頂いて、短期間で担当した事は自分に取って財産になりました。 そして、導入に関してGoサインを出してくれた上司、役員の皆様に感謝の気持ちでいっぱいです。

明日の記事の担当は サーバーサイドエンジニアのstevenさんです。お楽しみに。


株式会社エニグモ すべての求人一覧

hrmos.co