Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

ログ集計システムを自前で作る


Index

  1. ログ集計システムの要件
  2. DB設計
    • データ保存方針
    • table設計
    • サーバ構成
  3. Fluentd
    • fluentd,fluent-plugin-mysql-bulk install
    • td-agent.conf
    • mysqlにデータが格納される事を確認する
  4. 集計用のバッチ
  5. その他
    • Table肥大化防止
    • 可視化

ログ集計システムの要件

爆弾ログ処理班の@yutakikuchi_です。
ログ集計システムというものを作る時に皆さんはどのように対応していますか? 以下の候補から要件のレベルで使い分けをしている人が多いと予想しています。ざっくりの評価ですが、導入難易度、正確性、可視化、リアルタイム、長期集計、スケール、運用費用という点で評価を書いています。

ツール 導入難易度 正確性 可視化 リアルタイム 長期集計 スケール 運用費用 リンク
GA(スタンダード) × Google アナリティクス公式サイト - ウェブ解析とレポート機能 – Google アナリティクス はてなブックマーク - Google アナリティクス公式サイト - ウェブ解析とレポート機能 – Google アナリティクス
自前Hadoop × × × NTTデータのHadoop報告書がすごかった - 科学と非科学の迷宮 はてなブックマーク - NTTデータのHadoop報告書がすごかった - 科学と非科学の迷宮
Kibana × × Kibana入門 // Speaker Deck はてなブックマーク - Kibana入門 // Speaker Deck
TresureData × 数百億件のデータを30秒で解析――クラウド型DWH「Treasure Data」に新サービス - ITmedia エンタープライズ はてなブックマーク - 数百億件のデータを30秒で解析――クラウド型DWH「Treasure Data」に新サービス - ITmedia エンタープライズ
Redshift × Amazon Redshiftではじめるビッグデータ処理入門:連載|gihyo.jp … 技術評論社 はてなブックマーク - Amazon Redshiftではじめるビッグデータ処理入門:連載|gihyo.jp … 技術評論社
GoogleBigQuery × (2/6)システム部が担うデジタルマーケティング - 第1回 ビッグデータチームの結成を──Google BigQueryがデー...:ITpro はてなブックマーク - (2/6)システム部が担うデジタルマーケティング - 第1回 ビッグデータチームの結成を──Google BigQueryがデー...:ITpro
mysql × × × ソーシャルゲームのためのMySQL入門 - Technology of DeNA はてなブックマーク - ソーシャルゲームのためのMySQL入門 - Technology of DeNA

集計要件の切り口はリアルタイム性を求めるか、集計結果に正確性を求めるのか、大量データを後からスケールさせるか、長期保存が必要なログなのかのという点を考えればある程度は使えるツールが見えてくると思います。世間の動きとしてはTresureData、RedShift、GoogleBigQueryのようにクラウド型DWHにデータを転送して、BIツールで集計結果を見るという流れにある気がします。クラウド型DWHを使う事で開発コストを抑えれる、大量データを突っ込んでもスケール面で安心感があります。Kibanaのように検索エンジンを裏側で持つ仕組みを見た時はなるほどと思ったのですが、大量データを長期間保存するのには不向きな面があります。GAも素晴らしいツールですがスタンダード会員だと集計できる数に制限がでてきてしまいます。僕の今の仕事はクラウド型DWHは使わないという条件があり... 非効率だと叩かれそうですが安定と実績があるmysqlを利用してログを貯めるような設計を考えています。

DB設計

データ保存方針

上に貼った画像のように「食べたパンの枚数」を忘れてしまうようではディオ様にも怒られてしまいます。システム開発の現場でも過去の履歴を辿って数を抽出することはとても重要な話で、生ログに近いデータをDBに保存しておくのはシステム障害の場合等にも有効です。(当然生ログファイルも残します。mysqlで無くてMongodbでも良いです。) この生ログに近いデータをtableに保存しつつ、バッチ処理で定期的に集計値を算出し別tableに記録しBIツールはそちらを参照するような形がとれれば良いと思います。以前は生ログに近いデータを保存するのに反対で集計tableだけを用意し1行の集計カラムだけを都度updateする方針でやっていたのですが、集計値がずれた場合にはログファイルから再集計をやる必要があったので、今後は生ログtableと集計tableを分けて使いたいと思っています。その他生ログをDBに保存する用途としてはDBはAND条件指定が得意なのでURL×UAなどのクロス集計も簡単に抽出できます。

table設計

mysqlでログ集計を考える時に重要な要素は2つあってログ保存期間の仕様を決める、用途毎のtableを用意するという内容があるかと思います。大量の生ログに近いデータをmysqlに延々と貯め続けるのは非効率なのである程度のところでデータを削除しなければなりません。(削除は仕様として割り切るしかないかなと考えています。mysqlのパージコストは大きいですが)それに関連してログを保存するテーブルと集計結果を格納するtableを分けBIツールからは集計用のテーブルを参照するような仕組みが良いと思います。そこで以下のようなテーブルを設計します。パージし易いようにPARTITIONを使います。
tableを3つ用意します。

table名 用途 書き込み元
log リアルタイムでログを書き込む。1行で1アクセスを管理 fluentd
summary_realtime 短時間でlogから集計結果をsummaryする。 batch
summary_date デイリーでlogから集計結果をsummaryする。 batch
batch_history バッチが正常終了とsummary_realtimeで何分迄集計したかを記録。 batch


SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL';

CREATE SCHEMA IF NOT EXISTS `analytics` DEFAULT CHARACTER SET utf8 ;
USE `analytics` ;

-- -----------------------------------------------------
-- Table `analytics`.`log`
-- -----------------------------------------------------
CREATE  TABLE IF NOT EXISTS `analytics`.`log` (
  `log_id` INT NOT NULL AUTO_INCREMENT ,
  `path` VARCHAR(255) NOT NULL ,
  `code` INT NOT NULL ,
  `ua` VARCHAR(512) NOT NULL ,
  `referer` VARCHAR(255) NOT NULL ,
  `created_at` DATETIME NOT NULL ,
  PRIMARY KEY (`log_id`, `created_at`) ,
  INDEX `PATH` (`path` ASC, `code` ASC, `created_at` ASC) )
ENGINE = InnoDB PARTITION BY RANGE(TO_DAYS(created_at)) PARTITIONS 5( PARTITION p1 VALUES LESS THAN (TO_DAYS('2014-02-01 00:00:00')),  PARTITION p2 VALUES LESS THAN (TO_DAYS('2014-03-01 00:00:00')),  PARTITION p3 VALUES LESS THAN (TO_DAYS('2014-04-01 00:00:00')),  PARTITION p4 VALUES LESS THAN (TO_DAYS('2014-05-01 00:00:00')),  PARTITION p5 VALUES LESS THAN (TO_DAYS('2014-06-01 00:00:00')));

-- -----------------------------------------------------
-- Table `analytics`.`realcreated_at_summary`
-- -----------------------------------------------------
CREATE  TABLE IF NOT EXISTS `analytics`.`summary_realtime` (
  `summary_realtime_id` INT NOT NULL AUTO_INCREMENT ,
  `path` VARCHAR(255) NOT NULL ,
  `count` INT NOT NULL ,
  `start_at` DATETIME NOT NULL ,
  `end_at` DATETIME NOT NULL ,
  `created_at` DATETIME NOT NULL ,
  `modified_at` DATETIME NOT NULL ,
  PRIMARY KEY (`summary_realtime_id`) ,
  UNIQUE(`path`, `start_at`, `end_at`),
  INDEX `PATH1` (`path` ASC, `created_at` ASC) )
ENGINE = InnoDB;

-- -----------------------------------------------------
-- Table `analytics`.`summary_date`
-- -----------------------------------------------------
CREATE  TABLE IF NOT EXISTS `analytics`.`summary_date` (
  `summary_date_id` INT NOT NULL AUTO_INCREMENT ,
  `path` VARCHAR(255) NOT NULL ,
  `count` INT NOT NULL ,
  `date` DATE NOT NULL ,
  `created_at` DATETIME NOT NULL ,
  `modified_at` DATETIME NOT NULL ,
  PRIMARY KEY (`summary_date_id`) ,
  INDEX `PATH` (`path` ASC, `date` ASC) )
ENGINE = InnoDB;

-- -----------------------------------------------------
-- Table `analytics`.`batch_history`
-- -----------------------------------------------------

CREATE  TABLE IF NOT EXISTS `analytics`.`batch_history` (
  `batch_history_id` INT NOT NULL AUTO_INCREMENT ,
  `type` CHAR(1) NOT NULL ,
  `date` DATE NOT NULL ,
  `count` INT NOT NULL ,
  PRIMARY KEY (`batch_history_id`) ,
  UNIQUE(`type`, `date`),
  INDEX `DATE` (`type` ASC, `date` ASC))
ENGINE = InnoDB;


SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY
サーバ構成

サーバ構成は次のようなものを想定しています。一応半年前ぐらいにもmysqlとinfinidbでログ集計を扱ったエントリーを書いていますので参考になれば。
【進撃の巨大データ】Log集計用DBとシステム構成の美しい設計を考える - Yuta.Kikuchiの日記 はてなブックマーク - 【進撃の巨大データ】Log集計用DBとシステム構成の美しい設計を考える - Yuta.Kikuchiの日記

Fluentd

fluentd,fluent-plugin-mysql-bulk install

Apacheのログをリアルタイムでmysqlに格納する為の準備をします。Fluentd本体とfluent-plugin-mysql-bulkを設定します。※fluentdのmysqlpluginは他にもありますが、1レコードずつinsertするとmysqlへの接続負荷が高くなってしまうのでbulkinsertできるpluignを選びました。blukinsertとはINSERT INTO (A,B,C) VALUES(aaa,bbb,ccc), (ddd,eee,fff)のように1つのSQL文でINSERTが出来る為に高速と言われています。
Fluentd: Open Source Log Management はてなブックマーク - Fluentd: Open Source Log Management
toyama0919/fluent-plugin-mysql-bulk · GitHub はてなブックマーク - toyama0919/fluent-plugin-mysql-bulk · GitHub

$ curl -L http://toolbelt.treasuredata.com/sh/install-redhat.sh | sh
$ sudo /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-mysql-bulk
td-agent.conf

上のサーバ構成のLogAggregator Masterの設定を想定して記述します。次の内容を/etc/td-agent/td-agent.confに設定するとmysqlに接続してbulkinsertしてくれます。

<source>
  type forward
  port 24224
</source>

<match apache.access>
  type file
  path /var/log/apache/access.log
</match>

<source>
  type tail
  path /var/log/httpd/access_log
  format apache
  tag apache.log
  pos_file /var/log/td-agent/apache.pos
</source>

<match apache.log>
  type mysql_bulk
  host localhost
  database analytics
  username root
  column_names path,code,ua,referer,created_at
  key_names path,code,agent,referer,${time}
  table log
  flush_interval 5s
</match>
$ sudo /etc/init.d/td-agent restart 
mysqlにデータが格納される事を確認する

apacheへのRequest => apache logへの書き込み => fluent-plugin-mysql-bulk => mysqlの流れを確認します。

$ for i in {1..100}
do              
  curl "http://localhost/index.html"
done

$ tail -n 3 /var/log/httpd/access_log
::1 - - [08/Feb/2014:23:10:40 +0900] "GET /index.html HTTP/1.1" 200 - "-" "curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.0.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2"
::1 - - [08/Feb/2014:23:10:40 +0900] "GET /index.html HTTP/1.1" 200 - "-" "curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.0.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2"
::1 - - [08/Feb/2014:23:10:40 +0900] "GET /index.html HTTP/1.1" 200 - "-" "curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.0.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2"

$ tail -n 3 /var/log/td-agent/td-agent.log
2014-02-08 23:10:42 +0900 [info]: bulk insert sql => [INSERT INTO log (path,code,ua,referer,created_at) VALUES ('/index.html','200','curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.0.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2','-','2014-02-08 23:10:37'),('/index.html','200','curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.0.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2','-','2014-02-08 23:10:37'),....
mysql> SELECT * FROM analytics.log ORDER BY log_id DESC LIMIT 3 ;
+--------+-------------+------+--------------------------------------------------------------------------------------------------------+---------+---------------------+
| log_id | path        | code | ua                                                                                                     | referer | created_at          |
+--------+-------------+------+--------------------------------------------------------------------------------------------------------+---------+---------------------+
|    100 | /index.html |  200 | curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.0.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2 | -       | 2014-02-08 23:10:40 |
|     99 | /index.html |  200 | curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.0.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2 | -       | 2014-02-08 23:10:40 |
|     98 | /index.html |  200 | curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.0.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2 | -       | 2014-02-08 23:10:40 |
+--------+-------------+------+--------------------------------------------------------------------------------------------------------+---------+---------------------+

集計用のバッチ

以下のpythonコードでlogtableから5分毎に集計結果をsummary_realtimeに格納、1日毎にsummary_dateに格納する事ができます。それぞれのプログラムをcronで回せばOKですね。実行した結果mysqlにデータが格納されている事も確認します。logtableの格納件数とsummarytableの集計値が一致しているので問題無さそうでした。

#!/usr/bin/env python
#coding:utf-8
import sys,mysql.connector
from datetime import *
import time
date =  datetime.now().strftime( '%Y-%m-%d' )
base_timestamp = int( time.mktime( datetime.strptime( date + ' 00:00:00', "%Y-%m-%d %H:%M:%S" ).timetuple() ) )

# mysql connector
con = mysql.connector.connect(
    host='localhost',
    db='analytics',
    user='root',
    passwd='',
    buffered=True)
cur = con.cursor()

# get batch_count
cur.execute('SELECT count FROM %s WHERE date = "%s" AND type = "%s"' % ('batch_history', date, 'R') )
res = cur.fetchone()
try:
    batch_count = res[0]
except TypeError:
    batch_count = 0
start_timestamp = base_timestamp +  300 * ( batch_count -1 );
end_timestamp = base_timestamp + 300 * batch_count; 

# get logdata
cur.execute('SELECT COUNT(path),path,ua FROM %s WHERE code = %d AND created_at BETWEEN FROM_UNIXTIME("%s") AND FROM_UNIXTIME("%s") GROUP BY path' % ('log', 200, start_timestamp, end_timestamp))
res = cur.fetchall()

# insert into summary_realtime
for row in res:
    cur.execute('INSERT INTO %s (path, count, start_at, end_at, created_at, modified_at) VALUES("%s", %d, FROM_UNIXTIME("%s"), FROM_UNIXTIME("%s"), NOW(), NOW()) ON DUPLICATE KEY UPDATE count = %d, start_at = FROM_UNIXTIME("%s"), end_at = FROM_UNIXTIME("%s"), modified_at = NOW(), summary_realtime_id = LAST_INSERT_ID(summary_realtime_id)' %('summary_realtime', row[1], row[0], start_timestamp, end_timestamp, row[0], start_timestamp, end_timestamp ))

# history
cur.execute('INSERT INTO %s (type,date,count) VALUES("%s", "%s", 1) ON DUPLICATE KEY UPDATE batch_history_id = LAST_INSERT_ID(batch_history_id), count = count + 1' % ('batch_history', 'R', date))

con.commit()
cur.close()
con.close()
#!/usr/bin/env python
#coding:utf-8
import sys,mysql.connector
from datetime import *
import time
yesterday = datetime.now() - timedelta(0,3600*24)
start_date =  yesterday.strftime( '%Y-%m-%d 00:00:00' )
end_date =  datetime.now().strftime( '%Y-%m-%d 00:00:00' ) 
date =  yesterday.strftime( '%Y-%m-%d' )

# mysql connector
con = mysql.connector.connect(
    host='localhost',
    db='analytics',
    user='root',
    passwd='',
    buffered=True)
cur = con.cursor()

# get batch_count
cur.execute('SELECT count FROM %s WHERE date = "%s" AND type = "%s"' % ('batch_history', date, 'D') )
res = cur.fetchone()
try:
    batch_count = res[0]
except TypeError:
    batch_count = 0

# get logdata
cur.execute('SELECT COUNT(path),path,ua FROM %s WHERE code = %d AND created_at BETWEEN "%s" AND "%s" GROUP BY path' % ('log', 200, start_date, end_date))
res = cur.fetchall()

# insert into summary_date
for row in res:
    cur.execute('INSERT INTO %s (path, count, date, created_at, modified_at) VALUES("%s", %d, "%s", NOW(), NOW()) ON DUPLICATE KEY UPDATE count = %d, date = "%s", modified_at = NOW(), summary_date_id = LAST_INSERT_ID(summary_date_id)' %('summary_date', row[1], row[0], end_date, row[0], end_date))

# history
cur.execute('INSERT INTO %s (type,date,count) VALUES("%s", "%s", 1) ON DUPLICATE KEY UPDATE batch_history_id = LAST_INSERT_ID(batch_history_id), count = count + 1' % ('batch_history', 'D', date))

con.commit()
cur.close()
con.close()
mysql> SELECT COUNT(*) FROM log WHERE created_at BETWEEN '2014-02-08' AND '2014-02-09';
+----------+
| COUNT(*) |
+----------+
|      100 |
+----------+
1 row in set (0.00 sec)

mysql> SELECT COUNT(*) FROM log WHERE created_at BETWEEN '2014-02-09' AND '2014-02-10';
+----------+
| COUNT(*) |
+----------+
|     1288 |
+----------+
1 row in set (0.00 sec)

mysql> SELECT * FROM summary_date;
+-----------------+-------------+-------+------------+---------------------+---------------------+
| summary_date_id | path        | count | date       | created_at          | modified_at         |
+-----------------+-------------+-------+------------+---------------------+---------------------+
|               1 | /index.html |   100 | 2014-02-09 | 2014-02-09 15:48:37 | 2014-02-09 15:48:37 |
+-----------------+-------------+-------+------------+---------------------+---------------------+
1 row in set (0.00 sec)

mysql> SELECT SUM(count) FROM summary_date;
+------------+
| SUM(count) |
+------------+
|        100 |
+------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM summary_realtime;
+---------------------+-------------+-------+---------------------+---------------------+---------------------+---------------------+
| summary_realtime_id | path        | count | start_at            | end_at              | created_at          | modified_at         |
+---------------------+-------------+-------+---------------------+---------------------+---------------------+---------------------+
|                   1 | /index.html |   144 | 2014-02-09 01:00:00 | 2014-02-09 01:05:00 | 2014-02-09 15:45:21 | 2014-02-09 15:45:21 |
|                   2 | /index.html |   144 | 2014-02-09 01:10:00 | 2014-02-09 01:15:00 | 2014-02-09 15:45:21 | 2014-02-09 15:45:21 |
|                   3 | /index.html |  1000 | 2014-02-09 01:50:00 | 2014-02-09 01:55:00 | 2014-02-09 15:45:23 | 2014-02-09 15:45:23 |
+---------------------+-------------+-------+---------------------+---------------------+---------------------+---------------------+
3 rows in set (0.00 sec)

mysql> SELECT SUM(count) FROM summary_realtime;
+------------+
| SUM(count) |
+------------+
|       1288 |
+------------+
1 row in set (0.00 sec)

その他

Table肥大化防止

logtableが肥大化して来た時はパージを行います。予めpartitionを月毎に切っているので仕様で決めた蓄積期間を経過したらサヨナラします。summary_dateのtableは容量を食わないはずなのでそのまま残しておいても良いと思います。

mysql> ALTER TABLE log DROP PARTITION p1;
可視化

Highcharts - Interactive JavaScript charts for your webpage はてなブックマーク - Highcharts - Interactive JavaScript charts for your webpage
morris.js はてなブックマーク - morris.js
グラフ描画のJSで注目したいのはHighchartsとmorris.jsぐらいでしょうか。Highchartsは商用での利用はライセンス契約しないとだめなので、その辺を気にせず使いたい方はmorris.jsが良いかなと思います。

7万5千円で会社を作ったった【後編】

7万5千円で会社を作ったった【前編】のまとめ

7万5千円で会社を作ったった【前編】 - Yuta.Kikuchiの日記 はてなブックマーク - 7万5千円で会社を作ったった【前編】 - Yuta.Kikuchiの日記


@yutakikuchi_です。7万5千円で合同会社を作った話の続きをします。後編では設立後の手続きについて触れます。まずは前回のまとめから。

  • 印鑑の扱いは注意が必要。個人の印鑑は市役所系で、法人の印鑑の扱いは法務局で登録申請や証明書の発行を行う。
  • 印鑑は一般的に4種類必要。1.代表者個人としての印鑑/印鑑証明書、2.会社代表としての印鑑、3.会社銀行印、4.会社認め印。
  • 資本金は一時的な資金。代表の口座に振込をして通帳を証明としてコピーする。登記時に法務局にコピーを提出し、登記後はお金は自由にしてもOK。
  • 合同会社設立は勉強時間〜登記までを含めて1か月程見積もっておくと良い。@yutakikuchi_7万5千円で設立した。
  • 設立までにやった事は次の内容。
やる事 必須 手続き場所 必要経費
会社概要の検討/決定 無し 無し
会社実印の作成 はんこ屋さん 3本セットで8000円
人印鑑登録 市区役所 300円だったような...
人印鑑証明書の取得 市区役所 300円
登記書類作成システム利用料 × サービス会社 2100円
電子定款作成依頼 × 行政書士 3000円
資本金の払込 銀行 資本額
資本金の通帳コピー 無し 30円
登記書類の作成/押印 無し 無し
登記書類の提出 市区の法務局 6万円(印紙代)
人印鑑カード取得 市区の法務局 無し
履歴事項全部証明 × 市区の法務局 一部600円

設立後の役所手続き

設立後にも決め事や書類の届出を各役所に対して行わなければなりません。提出期限があったりするので提出には注意が必要です。以下は原則としてのルールを記載しておきます。特に税務署への届け出は最重要なので期限より早めに対応しておくと良いと思います。おそらく税理士さんに依頼すると直ぐに作成してくれます。僕の場合は登記書類作成システムの利用料の一環で税務署への資料は作成してもらいました。税務署以外の書類は社労士さんにお願いする事になります。※税務署は国税を扱うところで税事務所は都道府県税を扱うところです。間違えないようにしましょう。あと、下の項目に漏れがあった場合はごめんなさい...

提出書類 届け出先 期限 資料
役員報酬決め 無し 設立日より3か月 役員報酬の決め方と税金の基礎:起業前に必ず知っておきたい基礎知識 はてなブックマーク - 役員報酬の決め方と税金の基礎:起業前に必ず知っておきたい基礎知識
法人設立届け出 税務署 設立日より2か月 [手続名]内国普通法人等の設立の届出|法人税|国税庁 はてなブックマーク - [手続名]内国普通法人等の設立の届出|法人税|国税庁
青色申告の承認申請書 税務署 設立から3か月 [手続名]青色申告書の承認の申請|法人税|国税庁 はてなブックマーク - [手続名]青色申告書の承認の申請|法人税|国税庁
給与事務所等の開設届出書 税務署 第一回給与支払日まで [手続名]給与支払事務所等の開設・移転・廃止の届出|源泉所得税関係|国税庁 はてなブックマーク - [手続名]給与支払事務所等の開設・移転・廃止の届出|源泉所得税関係|国税庁
源泉所得税の納金の特例の承認に関する申請書 税務署 任意 [手続名]源泉所得税の納期の特例の承認に関する申請|源泉所得税関係|国税庁 はてなブックマーク - [手続名]源泉所得税の納期の特例の承認に関する申請|源泉所得税関係|国税庁
棚卸資産の評価方法の届出書 税務署 任意(設立第1期の確定申告の提出期限日まで) [手続名]棚卸資産の評価方法の届出|法人税|国税庁 はてなブックマーク - [手続名]棚卸資産の評価方法の届出|法人税|国税庁
減価償却資産の償却方法の届出書 税務署 任意(設立第1期の確定申告の提出期限の日まで) [手続名]減価償却資産の償却方法の届出|法人税|国税庁 はてなブックマーク - [手続名]減価償却資産の償却方法の届出|法人税|国税庁
法人設立届け出 税事務所 設立日から2か月 [手続名]内国普通法人等の設立の届出|法人税|国税庁 はてなブックマーク - [手続名]内国普通法人等の設立の届出|法人税|国税庁
法人設立届け出 市区町村役場 設立日から2か月 [手続名]内国普通法人等の設立の届出|法人税|国税庁 はてなブックマーク - [手続名]内国普通法人等の設立の届出|法人税|国税庁
適用事業報告 労働基準監督署 従業員を雇うようになってから滞り無く 主要様式ダウンロードコーナー|厚生労働省 はてなブックマーク - 主要様式ダウンロードコーナー|厚生労働省
就業規則 労働基準監督署 10人以上の従業員を雇うようになってから滞り無く 主要様式ダウンロードコーナー|厚生労働省 はてなブックマーク - 主要様式ダウンロードコーナー|厚生労働省
労働保険関係成立届 労働基準監督署 労働保険関係が成立した日の翌日から10日間 主要様式ダウンロードコーナー|厚生労働省 はてなブックマーク - 主要様式ダウンロードコーナー|厚生労働省
労働保険概算保険料申告書 労働基準監督署 労働保険関係が成立した日の翌日から50日間 主要様式ダウンロードコーナー|厚生労働省 はてなブックマーク - 主要様式ダウンロードコーナー|厚生労働省
時間外労働・休日労働に関する協定届 労働基準監督署 時間外や休日労働させる場合は滞り無く 主要様式ダウンロードコーナー|厚生労働省 はてなブックマーク - 主要様式ダウンロードコーナー|厚生労働省
雇用保険被保険者資格取得届 公共職業安定所 雇用保険適用事業所となった日の翌日から10日以内 ハローワークインターネットサービス - 帳票一覧 はてなブックマーク - ハローワークインターネットサービス - 帳票一覧
雇用保険適用事業所設置届 公共職業安定所 雇用保険適用事業所となった日の翌日から10日以内 ハローワークインターネットサービス - 帳票一覧 はてなブックマーク - ハローワークインターネットサービス - 帳票一覧
新規適用届 社会保険事務所 会社設立後5日以内 健康保険・厚生年金保険適用関係届書・申請書一覧 日本年金機構 はてなブックマーク - 健康保険・厚生年金保険適用関係届書・申請書一覧  日本年金機構
新規適用事業所現況書 社会保険事務所 無し 健康保険・厚生年金保険適用関係届書・申請書一覧 日本年金機構 はてなブックマーク - 健康保険・厚生年金保険適用関係届書・申請書一覧  日本年金機構
被保険者資格取得届 社会保険事務所 被保険者の資格取得から5日以内 健康保険・厚生年金保険適用関係届書・申請書一覧 日本年金機構 はてなブックマーク - 健康保険・厚生年金保険適用関係届書・申請書一覧  日本年金機構
健康保険被扶養者(異動)届 社会保険事務所 扶養者がいる場合は滞り無く 健康保険・厚生年金保険適用関係届書・申請書一覧 日本年金機構 はてなブックマーク - 健康保険・厚生年金保険適用関係届書・申請書一覧  日本年金機構
国民年金3号被保険者資格取得届 社会保険事務所 無し 健康保険・厚生年金保険適用関係届書・申請書一覧 日本年金機構 はてなブックマーク - 健康保険・厚生年金保険適用関係届書・申請書一覧  日本年金機構

※参考 会社設立後に必ず届出しなければいけない書類とその作成法まとめ はてなブックマーク - 会社設立後に必ず届出しなければいけない書類とその作成法まとめ

銀行手続き

スタートアップの場合法人用の銀行口座を作るのに凄く苦労すると聞きます。特に三菱UFJ、住友、みずほ等のメガバンクは審査のハードルが高く、その逆に信用金庫は作り易い、楽天銀行等のネットバンクも審査がほぼ無いというような噂は良く耳にします。 どこの銀行口座を作るかはいわば会社の見栄のようなものなので労力が掛かるなら地方銀行でも良いのでは無いでしょうか。僕は愛着のある横浜銀行さんで作って頂きました。横浜銀行さんは審査という厳格なものではありませんでしたが、支配人らしき人の面談が10分程ありました。前職ではどんな仕事をしていたのかやどんな会社を作りたいか等細かく質問されますので的確に教えてあげると良いと思います。
履歴事項全部証明書、会社の印鑑登録証明書等の書類を忘れずに持って行けばそれほど難なく作れると思います。メガバンクを狙って行く人はホームページの作成や固定電話の開設等事務所としての設備も整えておきましょう。
※銀行とは関係ありませんが、会社用のクレジットカードを作るのは楽天が良いようです。税理士さんにも薦められました。

終わりに

2回に分けて合同会社の設立について書いてみましたが如何でしたでしょうか。株式会社より手続きが楽なはずなので、株式会社設立の人は他の資料も参考にしていただくと良いと思います。準備から設立期間として1か月、手続き料7万5千円で合同会社を設立したお話は以上です。

7万5千円で会社を作ったった【前編】

退職エントリー後

Yahoo!を退職します。 - Yuta.Kikuchiの日記 はてなブックマーク - Yahoo!を退職します。 - Yuta.Kikuchiの日記
同窓会連絡のノウハウをブログに書いたら2chでdisられた@yutakikuchi_です。
月日が経つのは早いもので「退職エントリー」たるものを昨年に書いてから7か月も経過してしまいました。Yahoo辞めてから何をやっていたのかというと、起業準備、事業プラン作成、システム開発のお手伝いやコンサルティング...等々の仕事でたくさんの人と出会い、今迄経験の無い内容もたくさん対応させていただきました。


まだココでも報告していなかったのですが2013年11月11日に起業しました。法人の種別は合同会社です。今のところ社員は僕だけです。当然起業に関する知識は0から始め、書類作成以外は全て自分で対応したのでそこそこ時間が掛かってしまいました。手続きの失敗等もいくつかあったので今日は手順や掛かった費用について経験談を書きたいと思います。

合同会社設立の理由

会社設立の理由は人それぞれだと思いますが、僕の場合はスマフォ新規ビジネスを一緒に造って行く仲間を集めたかったのと取引の場で法人化する必要があったというのが大きな要因です。以下個人事業主と会社を区別して話しますが、ロイヤリティを持って仕事に臨むには個人事業主を束ねた集団では難しく、会社という組織の中で共感できるビジョンを造って行く必要があると思います。外部と契約を交わす時も個人事業主という立場だと倒産した時等は無限責任となっているのでリスクが大きく契約にたどり着けないという話を良く聞きます。会社の場合は有限責任なのである程度のリスクを回避する事ができます。その他出資や融資は会社の方が受け易い、ある程度の年商ラインからは会社の方が節税で有利などの話もあるので色々と調べてみると良いと思います。


合同会社 - Wikipedia はてなブックマーク - 合同会社 - Wikipedia
なぜ合同会社なのかというと株式会社設立よりも安くできる、書類の手続きが少し楽、決算公告が不要という対株式会社のメリットを活かしたいと考えたためです。僕みたいな不安定な人はとにかくスモールスタートでいい。費用と手続きコストを抑えられるのはスタートアップ向きですよね。社員にロイヤリティを植え付けたいなら株式会社だろっていう意見も聞こえてきそうですが(確かに株式会社と比べると合同会社は認知度が低い)、シスコシステムズやAppleJapanも合同会社という体制でやっているんですよね〜。

設立には1か月と7万5千円を見積もっておくと良い

一日も早く起業したい人が「やっておくこと、知っておくべきこと」読了 - Yuta.Kikuchiの日記 はてなブックマーク - 一日も早く起業したい人が「やっておくこと、知っておくべきこと」読了 - Yuta.Kikuchiの日記
手順については上のエントリーの「会社設立のためのスケジュール」という項目通りになりますが、箇条書きだと「どの役所」で「細かい何をすれば良い」のかが分からないので再度纏め直します。


特にややこしかったのが印鑑の扱いです。印鑑は4種類必要と考えておくと良いと思います。1.代表者個人としての印鑑/印鑑証明書、2.会社代表としての印鑑、3.会社銀行印、4.会社認め印。このうち登記時に必要なのは個人としての印鑑証明書と会社代表としての印鑑の2点です。個人の印鑑登録および印鑑証明書の発行は市役所や一部の出張所でできます。会社の代表者印は法務局に登記する際にあれば大丈夫です。 当然と言えば当然の話なのですが、この印鑑の違いをしっかり理解しておいた方がいいです。僕は手続きの際に最初は自宅近くの出張所に行って個人の印鑑登録を受け付けて貰らえず(※出張所も証明書を発行するだけのところがあります)、案内に従って印鑑登録ができる出張所に行き案内のアルバイト?の人に「法人関係の印鑑の扱いは全て法務局になる」という指示を受けて法務局に行き、法務局では個人の印鑑登録は全て市役所へという案内を受けて... たらい回しにされた感じの役所巡りで半日以上を潰してしまいました。2回目の出張所での僕の説明と案内の人の受け答えが食い違っている事に気づけば良かったんですが、そこまで知識が無かったので残念な結果になってしまいました。


注意したい点としては定款を紙でなく電子でやると4万円安くなります。僕は電子定款作成を行政書士の方にお願いをしてやっていただきました。法務局に登記する書類フォーマットは以下のURLにありますが、ありすぎて訳が分からないので書類作成の代行サービスを利用しました。法務省:商業・法人登記申請 はてなブックマーク - 法務省:商業・法人登記申請


その他として資本金は自分で決めた額を自分の個人口座に振込をして通帳の表紙と1枚目と入金があるページの3枚のコピーを取れば大丈夫です。順番としては定款書類を作成した後に振込をします。コピーは法務局に登記する際に必要になります。あまり知られていない事で資本金は一時的な入金証明であり、登記が完了したら自由に出し入れしても良いようです。どうでも良い事ですが会社の設立日は登記申請した日になります。書類がミスしてやり直しになっても最初の申請した日が設立日になります。(僕は電子定款ファイルが壊れていて後日やり直しになりました。)大安や1の付く日を狙って申請する人が多いと法務局の担当の人から聞きました。印鑑は良い物だと高価なのでこだわりがある人は色々と調べてみると良いです。僕は3本で8000円のものにしました。

やる事 必須 手続き場所 必要経費
会社概要の検討/決定 無し 無し
会社実印の作成 はんこ屋さん 3本セットで8000円
人印鑑登録 市区役所 300円だったような...
人印鑑証明書の取得 市区役所 300円
登記書類作成システム利用料 × サービス会社 2100円
電子定款作成依頼 × 行政書士 3000円
資本金の払込 銀行 資本額
資本金の通帳コピー 無し 30円
登記書類の作成/押印 無し 無し
登記書類の提出 市区の法務局 6万円(印紙代)
人印鑑カード取得 市区の法務局 無し
履歴事項全部証明 × 市区の法務局 一部600円

だいたい僕が登記にやった手続きは上の通りです。知識0からのスタートと仕事を請け負いながらの作業でしたが、勉強期間に2週間、手続き書類の準備と申請に2週間ぐらいの計1か月で登記できました。掛かった経費は交通費を含めて大体75000円です。自治体によって手数料は若干異なるようなので上の必要経費は目安としてお考えください。(印紙代の6万は変わらないと思います。)手続きは非常に面倒なので時間がもったいないと思う方は設立代行を依頼すると良いでしょう。登記後にも税務署への届け出、会社の銀行口座作成などの手続きが必要です。税務署への届け出や銀行口座作成についての知識はまた別で書きたいと思います。以上、合同会社の設立を考えている人の参考になればと思い記載させていただきました。

FacebookとLINEで呼びかけ、16年ぶりの再会をした同窓会の結果報告

16年ぶりの再会

LevelInfinity.Labという会社の代表をやっています@yutakikuchi_です。2014年1月3日に新潟市ANAクラウンプラザホテルという素晴らしい会場にて新潟市立小新中学校同窓会を実施しました。今回は幹事代表として全員への連絡や会の企画等全てにおいて責任を持たせて頂きました。1,2次会ともに2時間半ずつの時間でしたが、正直あっという間と思える程充実した時間であり、参加者からは賞賛の嵐でした。素晴らしい時間と場所を提供していただいた会場並びにスタッフの方々には感謝の気持ちで一杯です。


最初の方針として「必ず全員に案内連絡が行き届くように」というものを掲げていたので、準備期間の9か月間、連絡に関してはとことん対応しました。結果として連絡が行き届かなかった人は2名だけでした。参加結果ですが、生徒/学年主任/担任を合せて172名中96名が参加、率は6割近くになりました。近隣の中学校の実績が2割ぐらいで居酒屋開催という話を聞いていたので、かなり高い参加率だったことが分かります。ここまでの実績を残すのには長い道のりと辛い事ばかりでした。「幹事代行サービス」というものが巷に存在する意味も良く分かりましたが、終わってしまえばやって良かったと思います。今日のエントリーでは幹事として工夫した点やその効果について書きたいと思います。

同窓会をやる事になった経緯と東京からの旗上げ

そもそも16年ぶりに会う同窓会を実施する事になった経緯ですが、僕のところに開催して欲しい依頼が過去10年間で昨年が一番殺到したからです。昨年は30歳を迎えた歳であり、僕自身も昨年会社を辞めて起業しましたし、生徒それぞれが人生について何かしら思うタイミングがちょうど合ったのかもしれないです。


新潟の同窓会なのになんで東京在住の人が代表をやるのか?という疑問を持った方もいると思いますが、僕の中学校の問題は「適切なリーダーが新潟に存在していなかった」という一言になります。同窓生みんなが「誰かがリーダーをしてくれれば協力はする」という姿勢であったので、16年間一度も開催されずにここまで来たのだと思っています。地元のみんなは会の準備等細かい作業は率先してできたり得意ですが、全体の方針を打ち出したり意思決定をするのに時間がかかるというリーダー特性が少し弱かったようにも思います。地元の人が自ら事を起こせるようにと半年以上意識改革を促してきたので少しは浸透し始めていると思いますし、会中の僕の挨拶で次回代表は新潟在住の人を強引に指名して来たので、今回の成功事例に従って今後は地元から盛り上げて行ってくれると思います。

葉書案内を全員に出すのは辞めた方がいいと思う理由

僕の中学校では卒業アルバムに先生/生徒の住所一覧が載っていました。「そこに案内葉書を送ればいいじゃん?」と思う方もいると思うのですが、この方法だとおそらく参加率が3割ぐらいになってしまうと思います。葉書だけの送付だと参加者はいったいどんな感じの会になるかも不安ですし、そもそもアルバムの住所に住んでいないので連絡が届かない、返信締め切りを忘れてしまう、葉書を無くすケースが多いとネットで調べました。一般的な事例だと葉書への返信は大体5割、その中の6割ぐらいが参加としても全体の参加率は3割程度になってしまいます。


個人的には葉書案内を出す人数を最小限にし、Facebook/LINE/メールで呼びかける事に注力した方が良いと思います。 日々同窓会の更新情報をアップデート出来ますし、参加者への伝達も速く、また他の人のTLやLINE上でのコミュニケーションも活性化し、皆が最近どんな感じなのか雰囲気を知る事ができます。メールもメーリングリストを使う等連絡の手間を極力少なくするための有効な手段だと思います。


ただし、Facebook/LINEを連絡の軸とする場合は「みんなの理解」が必要です。IT系の人にとっては使っていない人なんていないぐらいの常用ツールですが、他の業界の人にとってはビックリするぐらい意味不明なツールらしいです(笑) 実際に連絡の軸とする方針を打ち出した時は一部の人からの反発が酷かったです。「誰もFacebookやLINEは使わないし、なんで同窓会専用会員サイトをお前が造らないのか?」とまで言われました。会員サイトを造る為に100万ぐらいみんなから貰えたなら実行したかもしれませんが(笑)、今あるツールを旨く使いこなして手軽に連絡ができるようにしました。実際に理解を求めるためにひと月ぐらいメリットを最大限に伝えるためのメールを打ちまくりました。その頑張りもあってFacebookとLINEのグループに総計100名集まりました。 ネットワークは強力で人から人を呼ぶと2か月あれば100名集まります。

コミュニケーションリテラシー

人をFacebook、LINEのグループに集める事は成功したのですがここで更なる問題が起ります。特にLINEで表出した問題としてはグループでの発言ルールが曖昧だと色んなコミュニケーションを取ろうとする人が出てきます。みんなとLINE上でおしゃべりしたい人は積極的に時間やタイミング関係無く発言し雑談が湧く、そうすると同窓会事務連絡のどれが大切なのかを把握できなくなってしまう。ノートとTLを旨く使いこなして大切な情報はノートへ書いたとしても、そもそもLINEの着信音が五月蝿くてイライラする等色んな問題が起きました。


最終的には目的毎にグループを用意して、連絡事項を伝えるグループ、雑談をしたいグループ等、交通整備をしました。またLINEの着信をグループでOFFにするようルールも徹底化したことで落ち着きました。更には雑談グループの長を決め、盛り上げるネタを毎日呟いてもらうなどのコンテンツ強化も行いました。この運用を7か月以上も続けたことが効いて、Facebook/LINEに集まって貰った人は8割が参加となりました。下らない話に思えるかもしれませんがここは凄く重要で、小さい問題を常に潰していく地道な努力が必要です。


参加希望の受付もFacebook/LINE/メールで行いました。Facebookはイベント機能があるのでボタン一つで意志がわかります。LINEでは参加する/参加しないをノートのコメントに記載してもらうようにしました。

幹事のモチベーションコントロール

僕以外の幹事のモチベーションをコントロールする事は正直うまくできませんでした。幹事をやったからと言って何かを得する訳ではなく、寧ろ個別にメールや電話で連絡した返事で厳しい意見を貰う事もあったようで実らない努力をしていると考えた人もいたと思います。参加費を低く抑えないと参加率も低くなるので、幹事への報償も無しで対応しました。最終的には同窓会への成功が最大の喜びに成るとは幹事全員が感じていたものの、一般参加者との温度差もあったりしてやる気を継続できませんでした。


ただ代表は何があっても成功に向けて突き進むぐらいの強い意志がある人で無いと駄目だと思います。最終的には全てを背負わないといけないので、途中で投げ出すような人であれば会自体を中止にするか直ぐに交代した方が良さそうです。


また幹事の打ち合わせも対面でやる必要もありました。幹事同士の意識合わせもLINEでやっていましたが、ちゃんと顔を合せて話し合わないとお互いの考えが伝わらない場面も多くありました。

実家訪問

連絡が全く取れなかった方への対応は実家訪問も合せて行いました。そこまでするか?という意見もあったと思いますが誠意を見せたかっただけです。僕も東京から新潟に帰省して自宅を訪問しましたが結果として誰一人本人に会う事は出来ず、親御様に事情を全部説明して東京に戻ってきました。実家訪問も最終的には成果として実りませんでしたが、個人的にはやり切った満足感を得る事はできました。

参加費受付用口座作成

当日の受付でのお金のやり取り時間を減らしたり、払った/払わなかったの意識違いを生まないようにするために参加者には事前にこちらで作成した銀行口座に振り込んでもらいました。これはかなり有効でした。多くの人が振込締め切り期限のギリギリにならないと対応してくれなかったという自体は発生しましたが、振込をしなかった人はいませんでした。振込手数料が200円ぐらい掛かってしまいますが、有効な手段であったことは間違いないです。当日100名のお金を1次会、2次会ともに預かるのは難しいと思います。口座を作るのが面倒で無ければお勧めします。

会当日

分刻みで時間を管理するぐらいの綿密な進行表を作りましたが、100名近い参加者を誘導したり説明したり等うまく行かなかったことがほとんどです。ちょっとした練習しかしないで本番に臨むのであれば正確な進行表は不要で、むしろ参加者に自由にやってもらった方が良いと思いました。2時間半の会場確保では短すぎるというのが正直なところで、本当に一瞬です。費用や会場都合もあると思いますが3時間以上は確保したいですね。


話に夢中になりすぎて料理に全く手がついていませんでした。僕たちは人数-15人分ぐらいでホテル側に依頼し、会中も料理を積極的に食べるよう促しましたが、それでも大量に余ってしまいました。あまりにも料理人数を削りすぎると逆にホテル側から断られるケースもありそうなので塩梅が難しいところだと思います。

まとめ

  • 同窓会の案内をFacebook/LINEを使って積極的に行うのは得策だと思います。
  • Facebook/LINEの導入時での説明がうまく対応し、コミュニケーションのルールを決めておけば自動的に盛り上がれる環境を作り出す事ができると思います。
  • Facebook/LINEのグループに参加してくれた人の8割が同窓会にも参加してくれました。
  • お金の管理を口座振込にすると当日の集金が不要となり、振込履歴が通帳に残るので安心。
  • 料理人数はできるかぎり抑えたいですが、会場の都合もあるので良く相談した方がいいです。

「DSP/RTBオーディエンスターゲティング入門」読了

DSP/RTBオーディエンスターゲティング入門読了

あどてくやってます@yutakikuchi_です。
今日は帰省中の新幹線で読んだ「DSP/RTBオーディエンスターゲティング入門」についてのまとめを書きたいと思います。すごく基礎的な事しか書いてなかったり同じ説明が何度も繰り返されたりしていますが、あどてくやあどまーけてぃんぐに関わっている人は読んでおいて損はないかと思いました。一言で本の内容をまとめると「DSPを導入するとオーディエンスデータが見えるから施策が打ちやすいよ!欲求施策にはリタゲが効果的だよ!」って感じかなと。個人的にはもう少しDSPの予測技術について内容を書いて欲しかったなぁというところもあったりしました。下記で記載する内容はかなり噛み砕いているのと、最後の章を記載していません。また初版が2012年5月という情報なのでそこには注意してください。

まとめ

Chapter 1-1 進化した広告配信
  • 広告は「枠」から「人」へパラダイムシフトしている。
  • 初期のネット広告は期間保障で特定に1社の画像をベタ張りしていた。サイトのPV=掲載料という仕組み。
  • その次に登場したのが各種メディアが作ったアドサーバー。Webページからアドサーバーの広告を読み込むタグを埋め込む。広告を全てアドサーバーで管理。
  • 複数の掲載面と掲載場所に広告を配信するアドネットワークの登場。広告営業、配信管理、レポート業務を全て代行してくれるのでメディアはサイト作りに専念。
  • 三者配信サーバーは一つの広告主キャンペーンが複数のサイトにまたがる場合でも一箇所で管理できる。
  • 三者配信の利点は広告主側のサイトへの流入管理をしやすい、ポストインプレッションを捕らえやすい、SEMと統合的に流入管理がしやすい。
  • 三者配信はクリエイティブの評価もしやすい。その結果最適化もしやすい。
Chapter 1-2 DSP/RTBの基本的な仕組み
  • DSP(Demand Side Platform)とは広告主などの広告を張りたい側のPF。SSP(Suply Side Platform)は媒体社が使うPF。
  • DSPの入札とSSPの応札を1Imp毎に瞬時に行うのがRTB(Real Time Bidding)
  • ユーザーが訪れた媒体はまずSSPにRequestする。SSPDSPにユーザーID、IPアドレス、ブラウザ、OS、掲載先ドメイン、カテゴリ、広告枠ID、広告サイズ、許可広告主、その他業種などの情報をRequestしている。
  • DSPはBid Requestに対して条件に一致する広告を探し、SSPに対して金額を含めたBid Responseを返す。
  • 1Impについてユーザーが媒体にアクセスした瞬間にBid Requestによって買う/売るをRealTimeでやり取りしている。
  • SSPは複数のDSPに対してBid Requestを行うので、DSP側としては他のDSPに条件で勝つ必要がある。
  • こういった仕組みがでてきた背景にはユーザーの行動履歴を分析できるようになったこと、膨大な処理を一瞬で行うコンピューティングの向上がある。
Chaper 1-3 広告の価格はどのように決まるか
  • 従来の枠に対する広告販売と管理はコストがかかる。
  • DSP/RTBは売り手/買い手ともに都合が良いエコシステム。買いたい側は買う側の理屈に合わせて、売りたい側も広く受注を受けて最適なものを選択できるから。
  • DSP/RTBが生まれた背景にはリーマンショックで失業した金融工学のエンジニアが広告業界に転職したこともある。
Chapter 1-4 トレーディングデスクの業務

※たいした事書いてないので飛ばし。

Chapter 1-5 DMPの役割
  • ユーザーのリアル行動、ネット行動、デモグラフィックを集めて様々な業種、商品カテゴリーのブランドに対して有効な広告配信データを活用できるようにするのがDMP。
  • 日本でもCCC、Optの合弁会社Platform IDがO2Oに参入している。
Chapter 1-6 アドエクスチェンジというビジネス
  • アドエクスチェンジがアドネットワークを束ねた事によりRTBの土台となる環境ができた。
  • アドエクスチェンジはDSPの機能を持つプレイヤーもいるので役割が重複している。ゆえにカオスな業界と言われる。
Chapter 1-7 「枠」から「人」へのパラダイムシフト
  • DSP/RTBでは誰に出すかということが条件付けされている。
  • 広告を買う側は最小単位でチューニングが可能になった。
  • DSP/RTBでも効率は良いが絶対量は保証されない。
  • 人を選ぶDSPではユーザープロフィールでターゲティングが可能でより本質的。
  • DSP/RTBは企業マーケティングの新しいビジネスチャンスである。
Chapter 2-1 内部データと外部データ
  • Impを判断するデータはSSPのBid Request、広告主が持っている情報、第三者のオーディエンスデータの3つ。
  • サイトの訪問履歴から再度訪問を狙うリターゲティング広告は効果が高く、高値での入札が期待できる。Bid Requestの内容と組み合わせることで他社と異なる手法で展開可能。
  • オーディエンスターゲティング以外にはGEOターゲティング、プロバイダーターゲティング、行動ターゲティングがる。
  • オーディエンスターゲティングはデモグラフィックデータ、興味データ、検索データが含まれる。
  • 外部オーディエンスデータを提供するDMPは様々なサイトにデータを管理するタグを貼ってもらい、閲覧データを取得する。取得したデータはカテゴリごとに管理する。日本ではオムニバスなどがサービスを展開している。
  • 3Stepのパーチェスファネルを意識してオーディエンスデータを活かす。
    • 知らない人に知ってもらう(認知施策)
    • 知ってる人に欲しいと思ってもらう(欲求施策)
    • 欲しい人に買ってもらう(獲得施策)
  • 外部オーディエンスデータは認知施策、内部オーディエンスデータは欲求施策からの開始となる。
Chapter 2-2 リターゲティング拡張で配信先を広げる
  • 内部オーディエンスデータは自社サイトに訪問済みという条件のデータなので対象者が少ない。一方外部オーディエンスデータは対象者は多いものの、自社サイトとの関連付けが難しい。
  • リターゲティング拡張とは広告主の特定ページに訪問したユーザーと同じようなユーザーを探す技術。
  • リターゲティング拡張はDSPが多くのSSPと提携し沢山のオーディエンスデータを保持していないと精度を上げることは難しい。リターゲティング拡張もただの外部オーディエンスデータに過ぎないので取り扱いが難しい。
  • ゴールまでの障壁が低い、類推の精度が高くなくてもOK、ユーザーが特定(連想)しやすいものという条件ではリターゲティング拡張は有効な手段となる。
Chapter 2-3 ネットの行動とリアル行動の統合
  • リアル店舗での購買行動とネット上での行動をCookieをベースに紐付けが可能。しかしこれにより特定できるユーザは10%にも満たないのでユーザークラスタを作ることが良い。
  • ネット広告とWebで把握できるユーザーデータはマス広告を含む全てのマーケティング施策全体の改善に繋がる。
Chapter 2-4 人の連想ではできないデータから読み取るターゲティング
  • 外部データと自社サイトのCVデータを掛け合わせることでCVした人がどういうオーディエンスなのかを知ることができる。この手法はDiscoveryやLook alikeと呼ばれるもので今後のDSPやオーディエンスデータの活用には欠かせない。
  • リターゲティング拡張はCVしたユーザーに似ている人を探すこと、Look-alikeはCVした人が外部オーディエンスのどこに所属していたか(カテゴリやデータ名)を知る手法。
  • CVデータから自社の顧客像を明確にし、クリエイティブの最適化につなげる。
Chapter 3-1 レスポンスを最適化する
  • ユーザーの反応として「誰が」、「いつ」、「どこで」、「どのようなクリエイティブ」、「どんな反応」をしたのかを解析することで最適化を図る。
  • 最適化の方法は反応の良い媒体、時間帯、オーディエンス、クリエイティブ、回数を探すこと。
  • 媒体に対しての配信は最高のタイミングで行うと効果が上がる。たとえば媒体×時間でより平均CTRが高い時間に配信するなど。さらにクリエイティブやオーディエンスを掛け合わせて3、4次元データから最適化することが可能であればパフォーマンスが向上する。
Chapter 3-2 反応からターゲットを探す
  • 新しいターゲティングは想像で反応する人を決めるのではなく、反応した人をターゲットとすること。都度ユーザーの反応を見てPDCAを細かく回していく。そのためには広い認知獲得が必要。最適化は認知の後に行えば良い。
  • 一度反応があったユーザーに対しては継続的にコミュニケーションをとることが重要。
Chapter 3-3 予測モデル
  • あらゆるデータから相関関係を探し意思決定プロセスに繋げる必要がある。
  • 取得したデータは過去のデータにすぐに加えて、加えたことによる予測の変化をまた直ぐに活用できるようにしなければならない。
Chapter 3-4 フリークエンシーとロードバケット
  • ユーザ一人に広告を何回表示させるかを制限することで効果を最大化し、その値を見直すことも必要である。
  • ロードバケットとは一度広告に反応し、しばらくは広告に反応しないユーザーにも一定間隔の経過後に広告を表示する手法。
Chapter 4-1 リスティング広告依存からの脱却
  • リスティング広告の問題点
    • CPAを安く抑えられるキーワードはそんなに多くない。
    • 未来においてどれぐらい検索されるかが読めない。
    • 検索数が読めないので比較的多く検索されるビッグワードに一致するよう広告を入稿してしまう。そうするとImpsは増えるが効果は良くなるとはいえない。
    • ユーザーは広告を順位の上から見ていくので他社と差別化ができないと印象付けることができない。広告1位のランディングページのUSP(独自売り)が力を発揮すると2位以降の訴求効果を弱めることができる。
    • 1位をキープするにはコストがかかるし、CPAがそれによりどんどん悪化する。
    • リスティング広告CPAが良い、ディスプレイ広告はCPAが悪いといわれるがリスティング広告でもCPAが良いのはブランドワードだけである。
  • 今までは認知施策と獲得施策に大きな隔たりがあったが内部オーディエンスデータをリターゲティングに利用することで離脱ユーザーへの再プッシュができるようになっている。
Chapter 4-2 3Stepsパーチェスファネルの構築
  • ゴールを明確にする、ゴールまでのプロセスを可視化、対象ユーザーに最適な方法でコミュニケーションを取り次のSTEPに進めることがパーチェスファネルの構築では最も重要なこと。
Chapter 4-3 並列のメディアプランから直列のコミュニケーションプランへ
  • 全てのゴールをCV獲得としてしまい、そのための一番効率の良い施策だけを実施することは良くない。
  • CTRも適切な広告が表示されているかを判断する重要な要素で、一般的にはノンターゲティング広告の場合はCTRが0.04〜0.5%ぐらい。
  • 広告を全くClickしないNon Clickerはネット人口の68%、広告を4回以上ClickするHeavy Clickerは6%で全体のClickの50%を占める。
  • Non Clickerはポータルサイト、検索サイト、ニュースやファイナンスサイトによく滞在する。それに対してHeavy Clickerはギャンブルサイト、転職サイト、ゲームサイトによく滞在する。
  • Heavy Clickerが過剰にCTRを上げてしまうこともあるので、適切なCTRを把握しておく必要がある。
  • ○○商品を知っていますか?という問いにYESとなるような想起を助成想起、○○が直接欲しいと思うことが純粋想起である。この純粋想起をしてもらうことが重要。
  • ディスプレイ広告で認知欲求施策を行うとブランドキーワードが向上する例がたくさんある。獲得施策でリスティング広告を使えば数のジレンマが無い。
Chapter 4-4 インプレッションを計測する
  • 三者配信を使って広告枠に外部ソースコードを記述することでインプレッション、クリックともに計測が可能。
  • 三者配信を使うことで媒体を横断した統一のカウントとレポートが作成可能。
  • 三者配信を行うことで媒体ごとのユーザー重複が分かる。
  • 媒体ごとにImpression数とUB数を把握することででどれぐらいフリークエンシーが発生したかを計算することができる。
  • 従来は事前にクリエイティブを渡し、配信中は見守り、実施後はレポートをもらうという最適化のできない配信だったが、第三者配信ではクリエイティブの切り替えも広告主や代理店側で対応することができる。
  • 全媒体でどれだけユーザーが広告に接触したかを図る指標としてグローバルフリークエンシーと呼ぶものがある。
  • フリークエンシーに合わせて広告クリエイティブを切り替えることも可能。このようなシーケンス配信は平均を考慮したやり方なので全てのユーザーに当てはめると精度が落ちる場合がある。
Chapter 4-5 CVまでのフェーズをブレイクダウンする
  • 獲得までのストーリを探す。例えば通販サイトであれば認知から申し込みの間にトライアルの要素を入れるだけで申し込みの障壁が低くなる。
  • 送料無料などの施策は認知フェーズのユーザにはあまり効果を発揮しないが、商品決定後の送料無料の情報は効果的。
  • デジタルマーケティングではリーチよりも適切なゴールが重要。ブランディングでは「忘れられない」ための施策が必要。
  • ビッグワードや他商品から流入してきたユーザーに対しては広告を届ける事でより強くブランドのイメージを植え付ける事が可能。
Chapter 4-6 サイトのシナリオと評価軸を構築する
  • ユーザが初回訪問でゴールに達成することはほとんどない。複数回サイトを訪れてサイトの安全性、欲しい商品の有無、そのサイトで買うべきかを判断する。
  • 認知施策では初回訪問を促す。TV、純広、DSPが効果的。
  • インプレッションからの検索という事が発生するので、クリックだけの最適化というのは良く無い。クリック + インプレッションからの検索の流入を加えて評価する。
  • 離脱者に対してはリターゲティングをする。トライアル前/後、カートへ商品を入れた後の離脱のそれぞれで訴求方法を変える。
  • メールアドレスを既に取得しているのであれば再訪問をメールで促すのも良い。個人の興味に合せた内容や画像を送信する事でより訴求効果がある。ただしやりすぎるとスパム扱いになる。
  • 最後の購買のタイミングではリスティング広告が一番強い。
  • シナリオを最後から設計すると良い。
  • 階段を適切に設ける事でそれぞれの施策に個別の指標を設ける事ができる。
  • 各階段を定義しそれぞれの貢献度を認めようとすることがアトリビューション分析である。CVだけがアトリビューションではなく、全ての施策がCVにどのように貢献したかを判断する事が必要。
  • アトリビューション分析は広告データだけでなく、サイトデータと連動する必要がある。
Chapter 4-7 コンシューマーディシジョンポイントを発見する
  • 獲得までのゴールを広告とサイトを横断して管理する事でシナリオやコミュニケーションの王道を発見し、ユーザーが何を求めているかを見つけられるようになる。
  • 広告は無駄も含まれるが排除してしまうと成功も減ってしまう。無駄は無駄と証明が出来ず何らかの因果関係がある場合が多い。
  • 無駄を探す事よりは成功のシナリオを探すべき。
Chapter 4-8 フェーズごとのメッセージ・クリエイティブ設計
  • 3STEP パーチェスファネルで各施策毎にメッセージやクリエイティブを変えてここのパフォーマンスを改善する事が出来る。
Chapter 4-9 リスティングやメールなど他施策と統合する
  • アメリカではAdobe社のAdobeGenesisがあらゆるツールを連携できるが、Genesisが連携できるツールはほとんど海外製品で日本のデジタルマーケティング業界のツールは遅れを取っている。
  • 連携ツールによってビッグデータを一元管理する。
  • 並列のメディアプランでは無く、直列のコミュニケーションプランへ舵を取らなければならない。
Chapter 5-1 世界中のインプレッションにアクセスできる時代
Chapter 5-2 ECのグローバル化で世界中にキャンペーン
  • 世界中で広告を配信して反応の良い国により多くの予算を投じる事が可能。
Chapter 5-3 スマートフォンDSPと4スクリーン
  • 4スクリーンとはPC、スマートフォンタブレット、ネットTVの事。
  • 広告をそれらに最適な形で配信するようにしたい。
  • まだPCでのターゲティングがスマートフォンでは出来ない。
  • 1配信の訴求力/情報量が多くなれば配信先を特定する事も更に大きな意味が生まれる。
Chapter 5-4 マスマーケティング企業のためのDSP活用
  • 広告主のサイトのどのコンテンツが顧客獲得に寄与しているかを把握する事が大切。そのためにはCookieベースでの複数のセッションを見たり、SEM/SEOでの流入管理、サイトのログ解析管理が一緒になっている必要がある。
Chapter 5-5 トリプルメディアマーケティング時代のDSP活用
  • トリプルメディアあとは自社メディア、ソーシャルメディア、ペイドメディア(マス4媒体)から成る。
  • 今後は自社メディアがソーシャル/ペイドメディアが持つ機能を拡張していくであろうと予想。
Chapter 5-6 デジタルCMOが活躍する時代
  • トリプルメディアを統合的に管理するチーフマーケティングオフィサー(CMO)が必要になる。CMOこそ経営のトップ候補。
  • またソーシャルCRMを活用できる者も経営トップ候補。

Redisにマルチプロセスで接続する時に気をつけたい事

Redis in Action

Redis in Action

Amazon

Redis

広告配信やっています@yutakikuchi_です。
Redisの内部処理が1スレッドで受けているようなので、マルチプロセスからRedisに書き込み処理を大量に流した時にどうなるのかを検証してみました。言語はCを、Libraryはhiredisを使います。redis/hiredis はてなブックマーク - redis/hiredis
hiredisを使って単一プロセスで実行した場合と、Apache Moduleにhiredisを組み込んでマルチプロセスの実行状態で検証します。検証機はCentOS6.4です。

hiredis

Redisのinstall、version確認、起動

RedisのVersionは2.4.10です。

$ sudo yum install redis -y

$ redis-server -v
Redis server version 2.4.10 (00000000:0)

$ sudo chkconfig --add redis

$ sudo /etc/init.d/redis start
hiredisのinstallとテストプログラム

hiredisのinstall後に単純にHashをincrbyするプログラムを実行してみます。まずはプロセス内で単純に指定回数incrbyを繰り返します。今回は指定回数を1万としたので、かなり高速に書き込みが完了している事が分かると思います。実行後にはredis-cliで書き込みが出来ているかを確認します。

$ sudo yum install hiredis hiredis-devel -y

$ gcc -I/usr/include/hiredis  -L/usr/lib64 -lhiredis redis-test.c -o redis-test

$ time ./redis-test
./redis-test  0.04s user 0.25s system 48% cpu 0.592 total

$ redis-cli HGET foo bar
"10000"
#include <stdio.h>
#include <hiredis.h>
#include <assert.h>

#define IP "127.0.0.1"
#define PORT 6379
#define COUNT 10000

int main(){
    redisContext *c = redisConnect(IP, PORT);
    if (c != NULL && c->err) {
        printf("Error: %s\n", c->errstr);
        return 1;
    }
    unsigned int i=0;
    for(; i<COUNT; i++) {
        redisReply *reply;
        reply = redisCommand(c,"HINCRBY foo bar 1");
        if(reply){ 
            freeReplyObject(reply);
        }
    }
    redisFree(c);
    return 0;
}

Apache Moduleへの組み込み

Moduleを入れる前のPerformance

Redisに接続するApache Moduleを入れる前のPerformanceを見てみます。4,5年前に購入したPCで試しているのでさほどSpeedが出ていませんが、ApacheのSampleページ表示で並列10/合計1万のabベンチマークの場合、2065.80rpsとなりました。

$ cat /proc/meminfo 
MemTotal:        2327156 kB
MemFree:         2058828 kB
Buffers:           21396 kB
Cached:            94164 kB
SwapCached:            0 kB

$ ab -n 10000 -c 10 "http://localhost/"

Concurrency Level:      10
Time taken for tests:   4.841 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Non-2xx responses:      10006
Total transferred:      52401422 bytes
HTML transferred:       50420234 bytes
Requests per second:    2065.80 [#/sec] (mean)
Time per request:       4.841 [ms] (mean)
Time per request:       0.484 [ms] (mean, across all concurrent requests)
Transfer rate:          10571.35 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2   1.4      2      32
Processing:     1    3   1.9      2      64
Waiting:        0    2   1.5      2      36
Total:          2    5   2.4      4      67
Apache ModuleからRedisへの書き込み

次にRedisに接続する為のApache Moduleを入れて行きます。まずは接続設定をapacheの設定ファイルに記述します。ここでは新たにredis.confを用意しました。
次にRedisに接続する為のmod_redisという自作Moduleを入れて行きます。apxsコマンドで入れて、apacheをrestartします。
一回curlで接続してみてRedisのPermission deniedで怒られたらhttpd_can_network_connect=1設定して上げましょう。

$ sudo vim /etc/httpd/conf.d/redis.conf
RedisHost 127.0.0.1
RedisPort 6379

$ sudo apxs -i -a -c -I/usr/include/hiredis -L/usr/lib64 -lhiredis mod_redis.c
$ sudo /etc/init.d/httpd restart
$ curl "http://localhost/redis"  
[Fri Dec 06 01:15:43 2013] [error] [client ::1] Redis Connection Error : Permission denied

$ sudo /usr/sbin/setsebool httpd_can_network_connect=1
#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "apr_hash.h"
#include "apr_tables.h"
#include "apr_strings.h"
#include "ap_config.h"
#include "util_script.h"
#include "http_log.h"
#include "hiredis.h"

typedef struct {
  int port;
  char *host;
} redis_env;

module AP_MODULE_DECLARE_DATA redis_module;

static int redis_handler(request_rec *r) {
  redis_env *db = ap_get_module_config(r->per_dir_config, &redis_module);
  redisContext *c = redisConnect(db->host, db->port);
  if (c != NULL && c->err) {
    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Redis Connection Error : %s", c->errstr);
    return DECLINED;
  }
  redisReply *reply;
  reply = redisCommand(c,"HINCRBY foo bar 1");
  if(reply){ 
  	freeReplyObject(reply);
  }
  reply = redisCommand(c,"QUIT");
  if(reply){ 
  	freeReplyObject(reply);
  }
  redisFree(c);
  return OK;
}

static void *make_redis_dir(apr_pool_t *p, char *d) {
  redis_env *db;
  db = (redis_env *) apr_pcalloc(p, sizeof(redis_env));
  return db;
}

static const char *set_redis_host(cmd_parms *cmd, void *mconfig, const char *name) {
  redis_env *db;
  db = (redis_env *) mconfig;
  db->host = ap_getword_conf(cmd->pool, &name);
  return NULL;
}

static const char *set_redis_port(cmd_parms *cmd, void *mconfig, const char *port) {
  redis_env *db;
  db = (redis_env *) mconfig;
  char *p;
  p = ap_getword_conf(cmd->pool, &port);
  db->port = atoi(p);
  return NULL;
}

static const command_rec redis_conf_cmds[] = {
  AP_INIT_TAKE1("RedisHost", set_redis_host, NULL, OR_FILEINFO, "redis hostname"),
  AP_INIT_TAKE1("RedisPort", set_redis_port, NULL, OR_FILEINFO, "redis port"),
  {NULL}
};

static void redis_register_hooks(apr_pool_t *p) {
	ap_hook_handler(redis_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

module AP_MODULE_DECLARE_DATA redis_module = {
	STANDARD20_MODULE_STUFF, 
	make_redis_dir,        /* create per-dir    config structures */
	NULL,                  /* merge  per-dir    config structures */
	NULL,                  /* create per-server config structures */
	NULL,                  /* merge  per-server config structures */
	redis_conf_cmds,       /* table of config file commands       */
	redis_register_hooks   /* register hooks                      */
};
Moduleを入れた後のPerfomanceテスト

RedisModuleを入れた後のPerformanceテストをしてみます。上と同様に並列10/合計1万のabベンチマークで実行した場合、726.98rpsとなりました。実行後にRedisのHashの値も10000に更新されています。

$ ab -n 10000 -c 10 "http://localhost/redis"
Concurrency Level:      10
Time taken for tests:   13.756 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      1670000 bytes
HTML transferred:       0 bytes
Requests per second:    726.98 [#/sec] (mean)
Time per request:       13.756 [ms] (mean)
Time per request:       1.376 [ms] (mean, across all concurrent requests)
Transfer rate:          118.56 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    3   4.7      2      76
Processing:     1   11  10.3      8     228
Waiting:        0    9   9.2      7     211
Total:          1   14  11.0     11     228

$ redis-cli HGET foo bar  
"10000"

もう少しRedisをいじめてみます。上のabスクリプトを更に繰り返し10回実行してみます。そうするとFailed Requestが多発し226.90rpsまでパフォーマンスが落ちています。この原因はRedisへの接続でTIME_WAITが大量に発生しているので、ApacheがRedisとの接続エラーを返しているためと思われます。Apacheのerror_logを見てもRedis Connection Error : Cannot assign requested addressのように記載されているのでConnectionErrorである事は間違いないと思います。

$ for i in {1..10}
do
ab -n 10000 -c 10 "http://localhost/redis"
done

Concurrency Level:      10
Time taken for tests:   44.073 seconds
Complete requests:      10000
Failed requests:        2619
   (Connect: 0, Receive: 0, Length: 2619, Exceptions: 0)
Write errors:           0
Total transferred:      2015708 bytes
HTML transferred:       81189 bytes
Requests per second:    226.90 [#/sec] (mean)
Time per request:       44.073 [ms] (mean)
Time per request:       4.407 [ms] (mean, across all concurrent requests)
Transfer rate:          44.66 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    4   8.5      0      75
Processing:     1   40  49.0     18    1188
Waiting:        0   36  46.7     16    1096
Total:          2   44  51.1     21    1188

$ netstat | grep "localhost:6379" 
tcp        0      0 localhost:37067             localhost:6379              TIME_WAIT   
tcp        0      0 localhost:37730             localhost:6379              TIME_WAIT   
tcp        0      0 localhost:45624             localhost:6379              TIME_WAIT   
tcp        0      0 localhost:52429             localhost:6379              TIME_WAIT

$ netstat | grep "localhost:6379" | grep TIME_WAIT | wc -l
19309

$ tail -f /var/log/httpd/error_log
[Fri Dec 06 00:42:17 2013] [error] [client ::1] Redis Connection Error : Cannot assign requested address
[Fri Dec 06 00:42:17 2013] [error] [client ::1] Redis Connection Error : Cannot assign requested address
[Fri Dec 06 00:42:17 2013] [error] [client ::1] Redis Connection Error : Cannot assign requested address
[Fri Dec 06 00:42:17 2013] [error] [client ::1] Redis Connection Error : Cannot assign requested address
[Fri Dec 06 00:42:17 2013] [error] [client ::1] Redis Connection Error : Cannot assign requested address
Cannot assign requested addressの原因を探る

straceを使ってredis-serverの内部処理とCannot assign requested addressが発生している時にどんな処理が行われているのかを見てみます。結論としては良く分からず...。Redisのメモリからbackground saveでdiskに移す時が怪しいと思っていたんですが、どうやらあまり関係無かったようです。同じようにredis-cli slowlog getでslow queryを取得してstraceのlogと照らし合わせてみましたが、これといって変な処理は入っていませんでした。単純にmemoryを大量に消費した時の問題か...この辺り詳しい方教えて頂けると助かります。

$ sudo strace -t -f -p `pgrep redis-server` > redis-log 2>&1

$ tail -f /var/log/httpd/error_log
[Fri Dec 06 01:32:15 2013] [error] [client ::1] Redis Connection Error : Cannot assign requested address

$ grep "01:32:15" redis-log | vim -
[pid  1393] 01:32:15 read(9, "*1\r\n$4\r\nQUIT\r\n", 16384) = 14
[pid  1393] 01:32:15 epoll_ctl(3, EPOLL_CTL_MOD, 9, {EPOLLIN|EPOLLOUT, {u32=9, u64=9}}) = 0 
[pid  1393] 01:32:15 write(6, ":932884\r\n", 9) = 9 
[pid  1393] 01:32:15 epoll_ctl(3, EPOLL_CTL_MOD, 6, {EPOLLIN, {u32=6, u64=6}}) = 0 
[pid  1393] 01:32:15 read(5, "*4\r\n$7\r\nHINCRBY\r\n$3\r\nfoo\r\n$3\r\nba"..., 16384) = 42
[pid  1393] 01:32:15 epoll_ctl(3, EPOLL_CTL_MOD, 5, {EPOLLIN|EPOLLOUT, {u32=5, u64=5}}) = 0 
[pid  1393] 01:32:15 epoll_wait(3, {{EPOLLIN, {u32=4, u64=4}}, {EPOLLOUT, {u32=9, u64=9}}, {EPOLLIN, {u32=6, u64=6}}, {EPOLLOUT, {u32=5, u64=5}}, {EPOLLIN, {u32=7, u64=7}}}, 10240, 98) = 5 
[pid  1393] 01:32:15 accept(4, {sa_family=AF_INET, sin_port=htons(35569), sin_addr=inet_addr("127.0.0.1")}, [16]) = 8 
[pid  1393] 01:32:15 fcntl(8, F_GETFL)  = 0x2 (flags O_RDWR)
[pid  1393] 01:32:15 fcntl(8, F_SETFL, O_RDWR|O_NONBLOCK) = 0 
[pid  1393] 01:32:15 setsockopt(8, SOL_TCP, TCP_NODELAY, [1], 4) = 0 
[pid  1393] 01:32:15 epoll_ctl(3, EPOLL_CTL_ADD, 8, {EPOLLIN, {u32=8, u64=8}}) = 0 
[pid  1393] 01:32:15 write(9, "+OK\r\n", 5) = 5 
[pid  1393] 01:32:15 epoll_ctl(3, EPOLL_CTL_MOD, 9, {EPOLLIN, {u32=9, u64=9}}) = 0 
[pid  1393] 01:32:15 epoll_ctl(3, EPOLL_CTL_DEL, 9, {0, {u32=9, u64=9}}) = 0 
[pid  1393] 01:32:15 close(9)           = 0 


$ redis-cli slowlog get
 1) 1) (integer) 69
    2) (integer) 1386261178
    3) (integer) 67530
    4) 1) "HINCRBY"
       2) "foo"
       3) "bar"
       4) "1"
 2) 1) (integer) 68
    2) (integer) 1386261168
    3) (integer) 14837
    4) 1) "HINCRBY"
       2) "foo"
       3) "bar"
       4) "1"
どうやって問題を解決するか?

僕が思いついたのは1.LinuxのKernel設定を修正する2.Apache Module内でmutexによりConnectionを共有する3.Redisの設定ファイルを修正してチューニングするの3つです。2.は既にmod_redisがやっています。sneakybeaky/mod_redis はてなブックマーク - sneakybeaky/mod_redis 3.はRedisの運用を細かくやってみないとできなそうな気がしたので、ここでは1の例を紹介します。


net.ipv4.tcp_tw_recycleというsysctlのパラメータを1に設定します。このパラメータは高速にSocketのRecycle処理をしてくれます。ただし導入する時は注意してください。同一IPからのアクセスは直ぐにSocketを解放してしまう可能性があるためです。十分なテストをしてから導入しましょう。


今回は導入した結果、Failed requestsが無くなりCannot assign requested addressがerror_logに表示されなくなり更にrpsも1912.64まで上がるという非常に効果の良い設定となりました。実行結果もRequest数に従った登録結果になっています。


今回の件とはあまり関係ありませんが、その他kernel関係の設定でvm.overcommit_memory=1のように設定するとbackground save時にmemoryの問題が回避される等の事例があるので、時間がある時に検証してみたいと思います。Redisを使う時は見積の二倍の容量必要だよね、という話 - Qiita [キータ] はてなブックマーク - Redisを使う時は見積の二倍の容量必要だよね、という話 - Qiita [キータ]

$ sudo vim /etc/sysctl.conf
net.ipv4.tcp_tw_recycle = 1

$ sudo sysctl -p

$ for i in {1..10}
do
ab -n 10000 -c 10 "http://localhost/redis"
done

Concurrency Level:      10
Time taken for tests:   5.228 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      1670835 bytes
HTML transferred:       0 bytes
Requests per second:    1912.64 [#/sec] (mean)
Time per request:       5.228 [ms] (mean)
Time per request:       0.523 [ms] (mean, across all concurrent requests)
Transfer rate:          312.08 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2   1.9      2      23
Processing:     1    5   3.6      4     131
Waiting:        0    4   3.3      3     105
Total:          1    7   3.5      7     134

$ redis-cli HGET foo bar 
"100039"

Apache ModuleでRequest ParameterをParseしてDBからデータを取得する

Request Parameter取得とDB接続

母校の同窓会幹事代表を務めています@yutakikuchi_です。
最近C++のエントリーを書く事が多いですが、今日もApache Moduleについて書きます。


Apache ModuleでRequest Parameterを取得する際はApacheのVersionに気をつけましょう。Versionによって使える関数が異なるようです。基本的には上位互換が保たれているようですが、最新Versionのドキュメントを参照している時に、実際には古いVersionを使ってしまっていると実装時に長い時間嵌る可能性があります。また単にRequest Paramterを取得しただけでは面白くないので、Parameterに従ってDB上のデータを参照する事を行いたいと思います。Apache ModuleやCのPreparedStatementに関する日本語ドキュメントは少ないので少しでも開発者の方々へ貢献できるように今後も頑張ります。

GitHub

GitHub Path

このエントリーで使用するソースコードGitHubに置きました。
CPlus/apache_module/ps/mod_db.c at master · yutakikuchi/CPlus はてなブックマーク - CPlus/apache_module/ps/mod_db.c at master · yutakikuchi/CPlus

Compile & Install

以下のコマンドでcompile & installしてくれます。installが完了したらApacheを再起動します。

$ sudo yum install httpd-devel mysql-devel -y
$ git clone git@github.com:yutakikuchi/CPlus.git
$ cd CPlus/apache_module/ps/ 
$ sudo apxs -i -a -c -I /usr/include/mysql -L /usr/lib64/mysql -lmysqlclient mod_db.c
$ sudo /etc/init.d/httpd restart

Apache VersionとParameter取得方法

ApacheのVersion確認

開発作業前にご自身のApache Versionを確認しておきましょう。

$ httpd -v
Server version: Apache/2.2.15 (Unix)
Server built:   Aug 13 2013 17:29:28
1.3系〜2.2系

Apache2: HTTP Daemon Routine はてなブックマーク - Apache2: HTTP Daemon Routine
モジュールの Apache 1.3 から Apache 2.0 への移植 - Apache HTTP サーバ はてなブックマーク - モジュールの Apache 1.3 から Apache 2.0 への移植 - Apache HTTP サーバ
Apacheの1系はドキュメントさえあまり残っていない状況なので、1系で開発することはお勧めしませんが念のために書いておきます。2.2系までの場合ap_getword,ap_getword_nc,apr_pstrdupのいずれかの関数を使って取得します。下の例ではapr_pstrdupを使っています。これらの関数は上位互換なので、ApacheのVersionをアップしても書き換えなくそのまま利用できそうです。( 1.3系から2.2系への移行は特に何も問題無いように思います。 )


その他の手段としてはap_add_cgi_vars(r);apr_table_get(r->subprocess_env, "QUERY_STRING");を組み合わせてParameterを取得する方法がありますが、上と同じようにParse処理は自前で書かないといけないので、大差は無いかと思っています。


下のサンプルの処理としてはとても単純でパラメータを&で区切ってhashにkey,valueとして入れ、それを後から参照しているだけです。使い易いようにparse_parameterとget_parameterに分けました。Validationは特にやっていないので適宜加えてください。

/* parse parameter */
static apr_hash_t *parse_parameter(request_rec *r) {
    char *str = apr_pstrdup(r->pool, r->args);
    if( str == NULL ) {
        return NULL;
    }
    
    apr_hash_t *hash = NULL;
    const char *del = "&";
    char *items, *last, *st;
    hash = apr_hash_make(r->pool);

    // set hash
    for ( items = apr_strtok(str, del, &last); items != NULL; items = apr_strtok(NULL, del, &last) ){
        st = strchr(items, '=');
        if (st) {
            *st++ = '\0';
            ap_unescape_url(items);
            ap_unescape_url(st);
        } else {
            st = "";
            ap_unescape_url(items);
        }
        apr_hash_set( hash, items, APR_HASH_KEY_STRING, st );
    }
    return hash;
}

/* get parameter */
static char *get_parameter(request_rec *r, apr_hash_t *hash, char *find_key) {
    apr_hash_index_t *hash_index;
    char *key, *val;
    hash_index = apr_hash_first(r->pool, hash);
    while (hash_index) {
        apr_hash_this(hash_index, (const void **)&key, NULL, (void **)&val);
        if( strcmp(key, find_key) == 0 ) {
            return (char*)val;
        }
        hash_index = apr_hash_next(hash_index);
    }
    return NULL;
}

// mainで使う
apr_hash_t *hash = parse_parameter(r);
char *id = get_parameter(r, hash, "id");
ap_rprintf(r, "id = [%d]\n", atoi(id));
2.4系

Developing modules for the Apache HTTP Server 2.4 - Apache HTTP Server はてなブックマーク - Developing modules for the Apache HTTP Server 2.4 - Apache HTTP Server
2.4になるとParameterのParseを便利なap_args_to_table関数の中でやってくれます。Parseされたデータをapr_table_getで参照するだけです。2.2まででやっていたような面倒な処理は一切不要になります。ap_args_to_tableは2.2以前は存在しないので気をつけてください。

apr_table_t *GET; 
ap_args_to_table(r, &GET);
const char *id = apr_table_get(GET, "id");
ap_rprintf(r, "id = [%d]\n", atoi(id));

DB上のデータを参照

上で取得したParameterの内容と一致するデータをDBから参照できるようにします。
参照時にはPreparedStatementを利用します。

apacheの設定ファイルを用意する

DBへの接続設定はapacheのconfファイルに記述します。設定が完了したらApacheが認識する必要があるので再起動します。

$ sudo vim /etc/httpd/conf.d/db.conf

# 以下を追記
DBHost localhost
DBPort 3306
DBUser root
#DBPass
DBName test
DBTableName sample

$ sudo /etc/init.d/httpd restart

上の定義をApache Moduleのcmd_recで呼び出します。

/*
 * Set the value for the 'DBHost' attribute.
 */
static const char *set_db_host(cmd_parms *cmd, void *mconfig, const char *name)
{
    db_env *db;
    db = (db_env *) mconfig;
    db->host = ap_getword_conf(cmd->pool, &name);
    return NULL;
}

(略)

static const command_rec db_conf_cmds[] = {
    AP_INIT_TAKE1("DBHost", set_db_host, NULL, OR_FILEINFO, "db hostname"),
    AP_INIT_TAKE1("DBPort", set_db_port, NULL, OR_FILEINFO, "db port"),
    AP_INIT_TAKE1("DBUser", set_db_user, NULL, OR_FILEINFO, "db username"),
    AP_INIT_TAKE1("DBPass", set_db_pass, NULL, OR_FILEINFO, "db password"),
    AP_INIT_TAKE1("DBName", set_db_name, NULL, OR_FILEINFO, "db name"),
    AP_INIT_TAKE1("DBTableName", set_db_table, NULL, OR_FILEINFO, "db tablename"),
    {NULL}
};
参照するTable

以下のようなTableを参照します。

mysql> SELECT * FROM test.sample;
+----+---------------------+
| id | created_at          |
+----+---------------------+
|  1 | 2013-11-21 22:05:43 |
|  2 | 2013-11-21 22:05:45 |
|  3 | 2013-11-21 22:05:49 |
|  4 | 2013-11-21 22:05:51 |
+----+---------------------+
4 rows in set (0.00 sec)
PreparedStatement

CのPreparedStatementはちょっと複雑です。以下に大まかな処理の流れを書いておきます。

1. StatementのInit
2. StatementのPrepare
3. ParameterのBind
4. StatementのExecute
5. ResultのBind
6. ResultのStore
7. ResultのFetch

これをコードに置き換えると以下のような感じになります。僕もついついやってしまう例としてはMYSQL_BINDのbuffer_typeの指定やresultの領域確保の数値を間違えてsegfaultを起こしたりします。buffer_typeについては以下のマニュアルを参考にすると良いと思います。 MySQL :: MySQL 5.1 リファレンスマニュアル :: 23.2.5 準備されたC APIステートメントデータタイプ はてなブックマーク - MySQL :: MySQL 5.1 リファレンスマニュアル :: 23.2.5 準備されたC APIステートメントデータタイプ

static int getDBContents(request_rec *r, int id) {
    // connect
    MYSQL *conn;
    conn = mysql_init(NULL);
    db_env *db = ap_get_module_config(r->per_dir_config, &db_module);
    int rid;
    MYSQL_TIME ts;

    if (!mysql_real_connect(conn, db->host, db->user, db->pass, db->name, db->port, NULL, 0)) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Mysql Connection Error : %s", mysql_error(conn));
        mysql_close(conn);	
        return DECLINED;
    }

    // issue query
    char query[100];
    sprintf(query, "SELECT id, created_at FROM %s.%s where id = ?", db->name, db->table_name);

    // stmt
    MYSQL_STMT *stmt = mysql_stmt_init(conn);
    if (mysql_stmt_prepare(stmt, query, strlen(query)) != 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Mysql Prepare Error : %s", mysql_stmt_error(stmt));
        mysql_close(conn);	
        return DECLINED;
    }

    // bind
    MYSQL_BIND bind[1];
    memset(bind, 0, sizeof(id));

    bind[0].buffer = &id;
    bind[0].buffer_type = MYSQL_TYPE_LONG;
    bind[0].buffer_length = sizeof(id);
    bind[0].is_null = 0;

    // bind_param
    if (mysql_stmt_bind_param(stmt,bind) != 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Mysql Bind Param Error : %s", mysql_stmt_error(stmt));
        mysql_stmt_close(stmt);
        mysql_close(conn);
        return DECLINED;
    }

    // execute
    if (mysql_stmt_execute(stmt) != 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Mysql Execute Error : %s", mysql_stmt_error(stmt));
        mysql_close(conn);	
        return DECLINED; 		
    }  

    // bind_result
    MYSQL_BIND result[2];
    memset(result, 0, sizeof(result));

    result[0].buffer = &rid;
    result[0].buffer_type = MYSQL_TYPE_LONG;
    result[0].buffer_length = sizeof(rid);
    result[0].is_null = 0;

    result[1].buffer = &ts;
    result[1].buffer_type = MYSQL_TYPE_DATETIME;
    result[1].buffer_length = sizeof(ts);
    result[1].is_null = 0;

    // bind_result
    if (mysql_stmt_bind_result(stmt,result) != 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Mysql Bind Result Error : %s", mysql_stmt_error(stmt));
        mysql_stmt_close(stmt);
        mysql_close(conn);
        return DECLINED;
    }

    // store_result
    if (mysql_stmt_store_result(stmt) != 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Mysql Store Result Error : %s", mysql_stmt_error(stmt));
        mysql_stmt_close(stmt);
        mysql_close(conn);
        return DECLINED;
    }

    // stmt_fetch
    while (!mysql_stmt_fetch(stmt)) {
        ap_rprintf(r, "id = [%d]\n", rid);
        char str[30];
        sprintf(str, "%04d-%02d-%02d %02d:%02d:%02d", ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second);
        ap_rprintf(r, "datetime = [%s]\n", str);
    }

    // close
    mysql_stmt_close(stmt);
    mysql_close(conn);
    return OK;
}
データ取得のテスト

curlをする際にid=2, id=3のように指定して、データが取得できる事を確認します。

$ curl -i "http://localhost/getid?id=2"
HTTP/1.1 200 OK
Date: Thu, 21 Nov 2013 16:47:39 GMT
Server: Apache/2.2.15 (CentOS)
Content-Length: 42
Connection: close
Content-Type: text/plain; charset=UTF-8

id = [2]
datetime = [2013-11-21 22:05:45]

$ curl -i "http://localhost/getid?id=3"
HTTP/1.1 200 OK
Date: Thu, 21 Nov 2013 16:48:19 GMT
Server: Apache/2.2.15 (CentOS)
Content-Length: 42
Connection: close
Content-Type: text/plain; charset=UTF-8

id = [3]
datetime = [2013-11-21 22:05:49]