ログ集計システムを自前で作る
Index
ログ集計システムの要件
爆弾ログ処理班の@yutakikuchi_です。
ログ集計システムというものを作る時に皆さんはどのように対応していますか? 以下の候補から要件のレベルで使い分けをしている人が多いと予想しています。ざっくりの評価ですが、導入難易度、正確性、可視化、リアルタイム、長期集計、スケール、運用費用という点で評価を書いています。
ツール 導入難易度 正確性 可視化 リアルタイム 長期集計 スケール 運用費用 リンク GA(スタンダード) ○ × ○ ○ ○ ○ ○ Google アナリティクス公式サイト - ウェブ解析とレポート機能 – Google アナリティクス 自前Hadoop × ○ × × ○ △ ○ NTTデータのHadoop報告書がすごかった - 科学と非科学の迷宮 Kibana ○ ○ ○ ○ × × ○ Kibana入門 // Speaker Deck TresureData ○ ○ ○ △ ○ ○ × 数百億件のデータを30秒で解析――クラウド型DWH「Treasure Data」に新サービス - ITmedia エンタープライズ Redshift ○ ○ ○ △ ○ ○ × Amazon Redshiftではじめるビッグデータ処理入門:連載|gihyo.jp … 技術評論社 GoogleBigQuery △ ○ ○ △ ○ ○ × (2/6)システム部が担うデジタルマーケティング - 第1回 ビッグデータチームの結成を──Google BigQueryがデー...:ITpro mysql × ○ × ○ ○ × ○ ソーシャルゲームのための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の日記
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
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
morris.js
グラフ描画のJSで注目したいのはHighchartsとmorris.jsぐらいでしょうか。Highchartsは商用での利用はライセンス契約しないとだめなので、その辺を気にせず使いたい方はmorris.jsが良いかなと思います。
7万5千円で会社を作ったった【後編】
7万5千円で会社を作ったった【前編】のまとめ
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円
設立後の役所手続き
設立後にも決め事や書類の届出を各役所に対して行わなければなりません。提出期限があったりするので提出には注意が必要です。以下は原則としてのルールを記載しておきます。特に税務署への届け出は最重要なので期限より早めに対応しておくと良いと思います。おそらく税理士さんに依頼すると直ぐに作成してくれます。僕の場合は登記書類作成システムの利用料の一環で税務署への資料は作成してもらいました。税務署以外の書類は社労士さんにお願いする事になります。※税務署は国税を扱うところで税事務所は都道府県税を扱うところです。間違えないようにしましょう。あと、下の項目に漏れがあった場合はごめんなさい...
銀行手続き
スタートアップの場合法人用の銀行口座を作るのに凄く苦労すると聞きます。特に三菱UFJ、住友、みずほ等のメガバンクは審査のハードルが高く、その逆に信用金庫は作り易い、楽天銀行等のネットバンクも審査がほぼ無いというような噂は良く耳にします。 どこの銀行口座を作るかはいわば会社の見栄のようなものなので労力が掛かるなら地方銀行でも良いのでは無いでしょうか。僕は愛着のある横浜銀行さんで作って頂きました。横浜銀行さんは審査という厳格なものではありませんでしたが、支配人らしき人の面談が10分程ありました。前職ではどんな仕事をしていたのかやどんな会社を作りたいか等細かく質問されますので的確に教えてあげると良いと思います。
履歴事項全部証明書、会社の印鑑登録証明書等の書類を忘れずに持って行けばそれほど難なく作れると思います。メガバンクを狙って行く人はホームページの作成や固定電話の開設等事務所としての設備も整えておきましょう。
※銀行とは関係ありませんが、会社用のクレジットカードを作るのは楽天が良いようです。税理士さんにも薦められました。
7万5千円で会社を作ったった【前編】
退職エントリー後
Yahoo!を退職します。 - Yuta.Kikuchiの日記
同窓会連絡のノウハウをブログに書いたら2chでdisられた@yutakikuchi_です。
月日が経つのは早いもので「退職エントリー」たるものを昨年に書いてから7か月も経過してしまいました。Yahoo辞めてから何をやっていたのかというと、起業準備、事業プラン作成、システム開発のお手伝いやコンサルティング...等々の仕事でたくさんの人と出会い、今迄経験の無い内容もたくさん対応させていただきました。
まだココでも報告していなかったのですが2013年11月11日に起業しました。法人の種別は合同会社です。今のところ社員は僕だけです。当然起業に関する知識は0から始め、書類作成以外は全て自分で対応したのでそこそこ時間が掛かってしまいました。手続きの失敗等もいくつかあったので今日は手順や掛かった費用について経験談を書きたいと思います。
合同会社設立の理由
会社設立の理由は人それぞれだと思いますが、僕の場合はスマフォ新規ビジネスを一緒に造って行く仲間を集めたかったのと取引の場で法人化する必要があったというのが大きな要因です。以下個人事業主と会社を区別して話しますが、ロイヤリティを持って仕事に臨むには個人事業主を束ねた集団では難しく、会社という組織の中で共感できるビジョンを造って行く必要があると思います。外部と契約を交わす時も個人事業主という立場だと倒産した時等は無限責任となっているのでリスクが大きく契約にたどり着けないという話を良く聞きます。会社の場合は有限責任なのである程度のリスクを回避する事ができます。その他出資や融資は会社の方が受け易い、ある程度の年商ラインからは会社の方が節税で有利などの話もあるので色々と調べてみると良いと思います。
合同会社 - Wikipedia
なぜ合同会社なのかというと株式会社設立よりも安くできる、書類の手続きが少し楽、決算公告が不要という対株式会社のメリットを活かしたいと考えたためです。僕みたいな不安定な人はとにかくスモールスタートでいい。費用と手続きコストを抑えられるのはスタートアップ向きですよね。社員にロイヤリティを植え付けたいなら株式会社だろっていう意見も聞こえてきそうですが(確かに株式会社と比べると合同会社は認知度が低い)、シスコシステムズやAppleJapanも合同会社という体制でやっているんですよね〜。
設立には1か月と7万5千円を見積もっておくと良い
一日も早く起業したい人が「やっておくこと、知っておくべきこと」読了 - 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人分ぐらいでホテル側に依頼し、会中も料理を積極的に食べるよう促しましたが、それでも大量に余ってしまいました。あまりにも料理人数を削りすぎると逆にホテル側から断られるケースもありそうなので塩梅が難しいところだと思います。
「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する。SSPはDSPにユーザーID、IPアドレス、ブラウザ、OS、掲載先ドメイン、カテゴリ、広告枠ID、広告サイズ、許可広告主、その他業種などの情報をRequestしている。
- DSPはBid Requestに対して条件に一致する広告を探し、SSPに対して金額を含めたBid Responseを返す。
- 1Impについてユーザーが媒体にアクセスした瞬間にBid Requestによって買う/売るをRealTimeでやり取りしている。
- SSPは複数のDSPに対してBid Requestを行うので、DSP側としては他のDSPに条件で勝つ必要がある。
- こういった仕組みがでてきた背景にはユーザーの行動履歴を分析できるようになったこと、膨大な処理を一瞬で行うコンピューティングの向上がある。
Chaper 1-3 広告の価格はどのように決まるか
Chapter 1-4 トレーディングデスクの業務
※たいした事書いてないので飛ばし。
Chapter 1-5 DMPの役割
Chapter 1-6 アドエクスチェンジというビジネス
- アドエクスチェンジがアドネットワークを束ねた事によりRTBの土台となる環境ができた。
- アドエクスチェンジはDSPの機能を持つプレイヤーもいるので役割が重複している。ゆえにカオスな業界と言われる。
Chapter 1-7 「枠」から「人」へのパラダイムシフト
Chapter 2-1 内部データと外部データ
- Impを判断するデータはSSPのBid Request、広告主が持っている情報、第三者のオーディエンスデータの3つ。
- サイトの訪問履歴から再度訪問を狙うリターゲティング広告は効果が高く、高値での入札が期待できる。Bid Requestの内容と組み合わせることで他社と異なる手法で展開可能。
- オーディエンスターゲティング以外にはGEOターゲティング、プロバイダーターゲティング、行動ターゲティングがる。
- オーディエンスターゲティングはデモグラフィックデータ、興味データ、検索データが含まれる。
- 外部オーディエンスデータを提供するDMPは様々なサイトにデータを管理するタグを貼ってもらい、閲覧データを取得する。取得したデータはカテゴリごとに管理する。日本ではオムニバスなどがサービスを展開している。
- 3Stepのパーチェスファネルを意識してオーディエンスデータを活かす。
- 知らない人に知ってもらう(認知施策)
- 知ってる人に欲しいと思ってもらう(欲求施策)
- 欲しい人に買ってもらう(獲得施策)
- 外部オーディエンスデータは認知施策、内部オーディエンスデータは欲求施策からの開始となる。
Chapter 2-2 リターゲティング拡張で配信先を広げる
- 内部オーディエンスデータは自社サイトに訪問済みという条件のデータなので対象者が少ない。一方外部オーディエンスデータは対象者は多いものの、自社サイトとの関連付けが難しい。
- リターゲティング拡張とは広告主の特定ページに訪問したユーザーと同じようなユーザーを探す技術。
- リターゲティング拡張はDSPが多くのSSPと提携し沢山のオーディエンスデータを保持していないと精度を上げることは難しい。リターゲティング拡張もただの外部オーディエンスデータに過ぎないので取り扱いが難しい。
- ゴールまでの障壁が低い、類推の精度が高くなくてもOK、ユーザーが特定(連想)しやすいものという条件ではリターゲティング拡張は有効な手段となる。
Chapter 2-3 ネットの行動とリアル行動の統合
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 リスティングやメールなど他施策と統合する
Chapter 5-1 世界中のインプレッションにアクセスできる時代
Chapter 5-2 ECのグローバル化で世界中にキャンペーン
- 世界中で広告を配信して反応の良い国により多くの予算を投じる事が可能。
Redisにマルチプロセスで接続する時に気をつけたい事
Redis
広告配信やっています@yutakikuchi_です。
Redisの内部処理が1スレッドで受けているようなので、マルチプロセスからRedisに書き込み処理を大量に流した時にどうなるのかを検証してみました。言語はCを、Libraryは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 starthiredisの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 67Apache 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 addressCannot 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 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 [キータ]
$ 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/CPlusCompile & 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:281.3系〜2.2系
Apache2: HTTP Daemon Routine
モジュールの 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
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ステートメントデータタイプ
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]