ISUCON 9 予選に参加した

全然ダメでしたが。

いつものチーム「ワイハリマ」で参加していました。前回は結構惜しいところまで行っていたので今年こそは予選突破するぞ!と言っていたら、全然手も足も出ず 5110 点という結果に。

当日はいつも通り、前半は yuta1024 と yabuuuuu が事前準備したインフラに適合する作業をしつつ kataribe やスローログの準備をしている間に僕がデータベースのスキーマとか、見てわかりそうな部分を改善……しようとしていたのだが、今回は SQL のテーブルは最初からそんなに悪い構造になっておらず、結局効いたのは blob が入っているテーブルで検索にインデックスが効かないため画像ファイルを DB から追い出したことくらい。二人がインフラを改善してくれていたのも効いていたようだ。

その後、 3 人合流して分析にあたり、 /transactions.json がとにかく遅いので、改善に着手した。SQL のトランザクションを貼りっぱなしで ShipmentService に https 接続していたのが、切り離しに成功して少しだけ上がる。

その後は buy が遅いということになった。buy は商品を買おうとする注文で、「在庫のある状態の商品なら、 PaymentService に決済を要求し、決済にも成功すれば購入できる、そうでなければ DB の何もかもロールバック」という処理になっていた。在庫状況を確認するのが SELECT FOR UPDATE になっており、行ロックで決済完了まで進める、という処理になっており、要は「買えそうなら商品を仮抑えして決済し、カード番号が違うとかで決済が失敗すればキャンセル」というごく普通の処理になっていたのだが、どうやら「人気商品に注文が殺到する」という、これまた現実にありそうな注文をしてくるようだった。結果、同じ商品に対するほぼ同時のリクエストが全部ロックされ、 fpm のセッション数すら溢れて nginx が 502 Bad Gateway を返す、ということになっていた。

buy に対する根本的な解決策が思いつかず、行ロック緩和について話し合ったものの雑な処理ではベンチが通らなかったりするので結局改善には至らず。別途他の二人がインフラ周りをいじって、 buy だけ専用機で fpm セッションを食わせるようにしたのはそこそこ効いたようだ。

今回、問題自体はとても良いものだと感じただけに、特にその設けられた課題に対して直接的な何かを何もできず、ただただ前年の本選問題を見てすらいなかったことなどを反省することしかできなかった。しかし、どうすればよかったのか、結局わかっていない。

(追記)チームのリポジトリ https://github.com/yuta1024/isucon9

ISUCON 8 予選に参加した話

ISUCON 8 の予選にチーム「ワイハリマ」として yuta1024, yabuuuuu とともに参加していた。ソースコードは GitHub で公開: yuta1024/isucon8 yuta1024/isucon8-infra

一瞬だけなぜか 47k というスコアが出るも、最終的に、最後の方は 33k-36k 程度をさまよいつつ最後の提出が 35,379 点。予選通過ラインは 36,471 点だった模様。競技中は予選通過はもっと遠いものと思っていたら、もう一工夫どころか下手したらベンチマークガチャの引き程度で超えていたかもしれない程度まで近づいていたのか…… 思うところは色々あるが来年がんばりたい。

ということで思い出せる範囲で、自分が実施したことをメモ。

競技前準備

当日の集合のことと、 GitHub のプライベートリポジトリを使用することを決定。その後、個人的な用事により旅立ってしまい前日に戻ってきたが、その間に yuta1024 と yabuuuuu はその間にアプリケーションの配布ツールや想定されたミドルウェアの設定、 SSH 公開鍵配布などを準備してくれていたようだ。ISUCON の日程も自分の不在もずいぶん前からわかっていたし、数日前だと全員いたとしても時間が潤沢にとれたわけでもないので、もう少し早めに(例えばディストリビューションがわかった時点で)動くべきだった。

競技開始

当日集合し作業開始。作業時間は当初のアナウンス通り、 10:00-18:00。

まずは問題を把握。題材はイベントの予約サイトで、管理者はイベントの追加や売り上げの集計が行え、ユーザは席の予約(ランクだけ選んであとはランダム)とそのキャンセル、自分の予約状況の確認が行える。テーブル構成は先ほどのリポジトリの isucon8/db/schema.sql にもとのものが上がっているが、概ねの構成は以下の通り。全てのイベントを一つの会場で実施していることになかなか気付かず、理解するのに少し時間がかかった。

  • ユーザのテーブル。ユーザごとに一行で、普通。
  • イベントのテーブル。公開されているかや価格(一番安い C 席の価格。上位席は席のグレードごとに決まった金額(後述)が加算)などが格納されている。普通。
  • 席のテーブル。S席(番号1-50で価格は(C席と比べた差額が)5000円)みたいなのが S, A, B, C の 4 種類、 1000 席あり、これで 1000 行を構成している。それぞれに id とは別に種別ごとの ID もついている(例えば A 席なら id=51 が 1 番、 id=60 が 10 番……)。なお、会場は一つしかないようで、この席とイベントの相関はない。中身は不変。まあなんというか、要するに明らかに無駄が多い。
  • 予約テーブル。予約ごとに1行だが、キャンセルした予約も参照することがあるため論理削除しか行われない。しかも、キャンセルフラグはわかれておらず canceled_at の時刻が NULL かどうかで判定している。席の空き状況もここで管理しているようだ(ただ、 UNIQUE KEY で二重予約が防止されている)。初期データで19万行程度あるが、そのうちキャンセルされていないのは 15k 程度しかないようだ。もちろん構成としては席テーブルよりはわかるがここも色々と問題になりやすそう。
  • 管理者の一覧。あんまり見ていなかったが、結果的にこれをいじるようなリクエストはあまり問題にならなかった。

また、今回は PHP を選択した。その場合、初期実装は Slim フレームワーク利用だった。時間はかかるが脱却すると速くはなる気はしていたのだが、初期ベンチで明らかに SQL が重かったこともあり、最後まで Slim に載せたままだった。初期状態でスコアは 1k 程度。

与えられた VM が 3 台あったが、初期実装は一台で動作するものだった。

初動

アプリケーションのロジックを主担当とすることになり、ミドルウェアの設定は残りの二人に任せてとりあえず VM のうち 1 台を使って調査開始。最初にどうにかすべきと判断したのはとりあえず席の一覧(1000件)取得後にその一件一件に対して予約状況を確認する SQL 文を発行していたこと(修正途中、別方面で分析を進めていた二人からもここがやばいと報告あり)。席テーブルの内容をハードコードし、ざっくり書き換えたのと、他の二人のミドルウェア改善と合わせてこの時点で 10k 突破(ただ、ベンチごとの揺らぎが激しく、同じコードで 5k を下回ることもあった)

これでも mysql の CPU 使用率がほとんどなので、これはデータベースチューニング回になると判断。

中盤

とりあえず見るまでもなく修正する点をつぶしている間にミドルウェアの整備が進んで kataribe や MySQL のスローログが使えるようになった。/admin/ や / が重いとわかるが、純粋にデータ量が多いため他と同じ処理時間になるわけもなく、他のボトルネックの検討も含めて相談。予約済みのもののみに興味があるクエリでは明らかに無駄で、特にキャンセル済み判定にインデックスが使えていないという yabuuuuu の指摘もあり、キャンセル判定や最終更新時刻など、テーブルにカラムをいくつか追加。変更点は ALTER やサブクエリを使えば SQL 文で完結しそうだったので /initialize の init-db.sh の修正を行い、保存された sqldump には手を付けなかった(その方が後々の変更も楽だし)。追加で必要となったインデックスも追加。

なお、空席選択のコードは初期実装では「空席の中からランダムに選ぶ」になっていたが、ランダムで通るならルール上はどのような規則で選んでも構わないと判断して、手を加える必要があった際にデバッグのしやすい「条件を満たす空席のうち id が最も若いものを選ぶ」コードを書いたところ、「ランダムではない」ことがシステムにバレて失敗扱いになったのはおもしろかった。検出のロジックが少しだけ気になった。

またついでに、潰せそうな SQL 文(SHA256 のためだけにクエリ発行しているクエリなどもあった)も潰してみたが、こちらは効果がほとんどなかった。

この間に他の二人が、ミドルウェア周りの整備および関連コードの修正を進めて DB×1, FE×2 構成に変更になった。MySQL が入っているマシンは MySQL が本気を出せるようになり、この段階で 30k 突破。一回だけなぜか 47,473 点をマークしたが、ベンチを走らせた本人曰く「修正で想定以上に改善したと思ったら、修正が反映されていなかったのに一回だけスコアが上がった」とのことで、理由は謎に包まれている。

また、この時点で必然的に環境が共有になったため、誰かがデプロイをしたくなっても他の人の作業を待つ必要があり、複数台にデプロイする手間も相まってデプロイ頻度が低下してきた。

終盤

時間があれば実施したいことは山ほどあったが、残り二時間を下回ってはそうも言っていられず、改善できそうなところから修正。複数クエリにまたがるトランザクションの削減などを試みるも、効果は限定的。相変わらずベンチごとにスコアが揺らぐ状況は続いていた。

最後の30分はベンチマークガチャに充てよう、などと話しつつ、終了30分前になって実施した再起動試験で、なんと 502 Bad Gateway になるという事件が発生した。再起動後も正常に稼働できるようになっていないと、レギュレーション上当然ながら失格となってしまうため全員で慌てて対処し、かろうじて残った約5分でベンチを回し始めたが、最後のベンチが0点でなすすべなく☀となることは避けたかったので、 17:59:10 に走行し終えたベンチを最後に終了。その時は予選突破はもっと遠い点数であり最後のベンチの点数こそが自分たちの成績になると思っていたが、結果を見た今ではもう一回回していれば結果は変わったかもしれないという大変微妙なところにいたと言える。

反省会

競技終了後、準備不足であったことを感じながら、移動および夕食のため池袋に向かった。サンシャイン60通り商店街周辺から探索を始めたが、三連休の中日の夜でどの店も長い列ができており、徐々に南下しながら探索を行い、最終的には東通り(JR 池袋駅南口の東南東)に到達した。この間に早くも ISUCON 運営の方々による再起動等の試験・集計が完了して公表され、予選突破ラインとの差がベンチマークガチャ程度の差であったことを知った上で、反省会が始まった。

感想

ウェブアプリケーションとして素直な構成でありながら、最初に一台構成で組まれたコードを複数台に分散する要素が活用されており、奇をてらったり知っているかどうかを競うだけのゲームになったりもしなかった。そのため、チューニングらしくて良いと思った去年にも増して楽しめる競技だった。

そして、そのような素晴らしい競技で今回は惜しいところで本選出場を逃したこと、事前準備を早くにしていれば当日時間短縮できそうなところが多くあったところなど、悔しい点もあった。前回の ISUCON 予選は複数台サーバの時計のずれなど、重要な点に気付く必要があり、それには時間があっても気付けなかっただろうと感じたが、今回は時間があればできたと思うことは多くあった。もちろん、 ISUCON の競技としては事前準備が効きすぎてしまうのはよくないだろうと思うし、その想定で情報を掲出している点もあるとは思うが、それでも8時間という短い時間ですべてを出し切るには特にデプロイ周り、時間の使い方や締切、(ISUCON 7 予選で複数 VM となる前例があった今)複数 VM を複数人で活用する方法など、事前準備が避けられない点は準備しておくべきだった。

ISUCON 7 予選参加

ISUCON 7 予選二日目にチーム「ワイハリマ」(yuta1024, nhirokinet, tyabuki)で参加しました。76,904 点でした。

今回の題材は「isutaba」というチャットツールで、

  • ログインすると部屋がたくさんあるので、それぞれの部屋でチャットができる(部屋の権限などはない)
  • 部屋の発言およびほかの部屋の未読バッヂは、ストリーム風にリアルタイムに表示される。それを実現するために、各ブラウザが js で無限に新着を確認する「/fetch」 JSON APIをたたき続ける。
  • ブラウザは /fetch が返ってきてそのレスポンスを処理したら、また次の /fetch をたたく。サーバ側の /fetch 側はデフォルトで 1 秒待機(sleep 1)だが、こちらは変更可能。
  • デフォルトで、 web app 2 台、 db (MySQL) 1 台の計 3 台構成。ただし、構成は自由に変更可能で、 FE サーバの集合は自由に選択可能。
  • デフォルトではユーザアイコンも含めて自由に変更可能で、全部 db に載っている。ユーザアイコンの URL は、デフォルトでは画像の sha + 拡張子。画像への権限管理なし。

という内容でした。

リポジトリ

isubata リポジトリ by チーム「ワイハリマ」

準備内容

最低限、よくある MySQL や nginx の設定、kataribe、公開鍵一覧と配布スクリプトを準備。プライベートリポジトリに関しては、性能に余裕のあるマシンで GitLab を用意し、全員のアカウントを作り、外からも使用可能なようにしておいた。

当日

思い出せる範囲で。個人で書いているので、他の二人の実施内容に関しては違っている可能性がある。

  • 開始が10時から12時に変更だったのでそれに合わせて集合したが、最終的には13時になったので最初の進捗
  • まず、サーバに全員分の鍵や、 MySQL には InnoDB の書き出し間隔の遅延、 MySQL インデックスの追加などを実施。
  • 一旦一台構成に移行し、 icons を MySQL ではなく静的ファイルとして扱うよう変更。(初期段階としては)ある程度伸びる。
  • サーバのミドルウェアを一部は新しいものに更新しつつ、設定をざっくりチューニング。また、設定ファイルを扱いやすくするなどを実施。キャッシュ回りもいじっていたようだ。ここは担当していないので詳細はよくわからないがここで点数が大幅に伸びていた模様。
  • リクエストが多い /fetch, /message について、 Slim 脱却を試みる。テンプレートを使っていなかったのでそれ自体の修正量は少なめ。
    • ここで、 /fetch での sleep(1) が話題に上る。単につぶすと性能が下がる。 /fetch 自体はスコアに勘定されないルールになっており、単に排除するといたずらに負荷をかけることになる。
  • 40k を超えたあたりで、 php-fpm のワーカ数を調整しようとして、 sleep(1) が話題に上る。これが php のプロセス数を食っているようだ。とりあえず増加。
  • 静的ファイルの icons を相手のサーバにも送り付ける口を作り web app を二台に戻すが、性能が上がらない。
  • とにかく /fetch の sleep の問題が大きいということで、 /fetch だけは専用の php-fpm プールに逃がすように変更してスコアが上昇。
  • 同時に、ベンチマーカーのエラーメッセージや kataribe の結果などから、 icons が異常に遅延(nginx 視点でテイルレイテンシ 10 秒)していることに気付く
  • 外向け 100Mbps 、一分間での画像リクエスト約 1.4k ということで、画像ファイル平均 500kB 程度とすると帯域をほぼ使い切っている計算。
  • メモリを使い切ってキャッシュが減っているのでは?という疑惑はあった
  • ベンチマークを何度か走らせると、 +/- 15% 程度は平気で増減する状態であり、ガチャの影響も大きかった
  • icons が大幅遅延するかどうかもガチャ次第だった。
  • web app を二台活用できない理由も、 icons が遅延する(あるいは上位チームがこの何倍ものスコアを叩き出せる)根本原因もわからないが、終盤が近づいたので web app を一台運用することを決定。
  • 再起動試験実施。
  • 再起動後二回目の試験で、運よく過去最高得点が出た。この時終了約15分前だったので、以後外れベンチマークを引かないように全ての作業を終了。

感想

今回は、ウェブアプリケーションとしてはかなり王道なチューニングが主で、それでいて難易度が高く仕上がっていて、良い問題だったと感じました。

まだほかのチームの write up はあまり見られていませんが、それでも特に複数台構成/ローカルファイルシステム同期における /icons の更新日時や、せめてもの二台目の活用方法など、少し読んだだけでも見直すべき部分が色々出てきました。

二日目の通過ラインは 210,464 点で、ここからさらに三倍食えるようにしないといけなかったようです。とはいえ、学びはあったので、今後予選通過を狙えるようにしたいです。

ISUCON 6 予選参加

チーム「にゃーん」で ISUCON 6 予選参加して惨敗してきました。厳しい。というわけで記録です。といっても、結局自分が担当した部分以外はあまり把握していないので、それは各自がブログか何かを書くのを待ちます。

まず、インスタンスを起動して、あまり分ける必要がない二つのアプリケーションが動いていることを確認したので、れにが二つをマージしていました。その間に DB のインデックスで露骨にまずいものがないか見ていましたが、一応張ったもののそれほど大幅ということはありませんでした。

htmlify なる関数があり、これが今回のメインのようでした。記事(初期状態で約七千)のタイトルを本文中から自動抽出してリンクに変換するものですが、全ての記事のタイトルを抽出したうえで正規表現や SHA1 を活用した実装になっていて遅いこと遅いこと。

なお、今回は Ruby だったこともあり初期実装は0点でした。

そのあと、他のチームメンバーは Web 周りのチューニングなどをしてそれなりに点数を伸ばしていたようですがあまり把握しておらず、僕は htmlify をなんとか早くしようとしていました。

他のメンバーと違い、僕は Ruby の知識があまりなかった状態でこれを見たので、 Ruby の置換が同様に書かれたほかの言語と比べても遅いことに絶望して、なんとか早くするアルゴリズムを模索しました。とりあえずなんにしてもその部分を Ruby で書くのは諦めて、 C++ で書いて外部コマンドで呼び出すことにしました。(引数で辞書と入力を渡し、出力は標準出力)

二分探索をしようとしたりさまよっていましたが、最終的にはハッシュマップで実装しました。与えられた文字列に対して短いほうから辞書にある最大の長さまで試す、ただし途中で打ち切るための情報を埋め込む、というものです。ポインタでいろいろ扱える方が楽だったのでポインタをベースとし、何かとコピー回数の増える std::string はなるべく使わないように変更、 C++ の std::map は hashmap ではないことを思い出して std::unordered_map に変えましたが、パフォーマンス計測値、スコアともに改善はしたものの依然遅いままでした。

#include <vector>
#include <string>
#include <string.h>
#include <algorithm>
#include <stdio.h>
#include <unistd.h>
#include <map>
#include <iostream>
#include <unordered_map>

using namespace std;

void urlwrite(const char *str) {
	while(*str!='\0'){
		if ((*str>='a' && *str<='z') || (*str>='A' && *str<='Z') || (*str>='0' && *str<='9') || *str=='.' || *str=='-' || *str == '_' || *str == '~') {
			fwrite(str, 1, 1, stdout);
			++str;
			continue;
		}
		int t = *((unsigned char*)str);
		int b1 = t/16;
		int b2 = t%16;

		char tmp[3];
		tmp[0]='%';
		tmp[1]=(b1<10)?(b1+'0'):(b1+'A'-10);
		tmp[2]=(b2<10)?(b2+'0'):(b2+'A'-10);

		fwrite(tmp,1,3,stdout);
		++str;
	}
}

void htmlwrite(const char *str) {
	while(*str!='\0'){
		switch (*str) {
			case '&':
				fwrite("&amp;", 1, 5,stdout);
				break;
			case '<':
				fwrite("&lt;", 1, 4, stdout);
				break;
			case '>':
				fwrite("&gt;", 1, 4, stdout);
				break;
			case '\'':
				fwrite("&#039;", 1, 6, stdout);
				break;
			case '\"':
				fwrite("&quot;", 1, 6, stdout);
				break;
			case '\n':
				fwrite("<br />\n",1,7,stdout);
				break;
			case '\r':
				fwrite("<br />\r",1,7,stdout);
				break;
			default:
				fwrite(str,1,1,stdout);
		}
		++str;
	}
}


int main (int argc, char *argv[]) {
	unordered_map <string, int> dic;

	setvbuf(stdout, NULL, _IOFBF, 102400);

	size_t longest=0;
	for(int i=2;i<argc; ++i){
		string key=string(argv[i]);
		for(int j=1;j<4;++j){
			string tmp=key.substr(0,j);
			if(!dic.count(tmp)){
				dic[key.substr(0,j)]=1;
			}
		}
		{
			int j=6;
			string tmp=key.substr(0,j);
			if(!dic.count(tmp)){
				dic[key.substr(0,j)]=1;
			}
		}
		dic[key]=2;
		longest=max(longest, key.length());
	}

	while (*argv[1]!='\0') {
		int skiplen=1;
		string title;
		int flag=0;

		for(int i=1;i<=longest;++i){
			char tmp=argv[1][i];
			argv[1][i]='\0';
			string key=string(argv[1]);
			argv[1][i]=tmp;
		
			if(dic.count(key)){
				if(dic[key]==2){
					title=key;
					flag=1;
				} 
			}else{
				if(i<4 || i==6){
					break;
				}
			}
		}

		if(flag){
			fwrite("<a href=\"/keyword/", 1,18,stdout);
			urlwrite(title.c_str());
			fwrite("\">", 1,2,stdout);
			htmlwrite(title.c_str());
			fwrite("</a>", 1,4,stdout);
			skiplen=title.length();
		}else{
			char tmp=argv[1][1];
			argv[1][1]='\0';
			htmlwrite(argv[1]);
			argv[1][1]=tmp;	
		}
		argv[1]+=skiplen;
	}
}

※ちなみに、<a href=”/keyword/~~”> の部分は、 Ruby の見本実装では <a href=”http://192.0.2.1/keyword/~~”> のように URL 変換済み、 PHP の見本実装では <a href=”/keyword/~~”> のように path 部分のみでした。後者で書きましたが、不思議でした。

その後、 Trie 木を実装しましたが、初期化コストが大きく辞書を作るのに 100ms 程度かかってしまいました。外部コマンドなので毎回初期化していたのと、特にバイナリでいい感じに複数のデータを受け渡す実装の時間もなく、トップページを表示するのに 10 回読みだしてしまい、却って遅かったのでこちらは導入に至りませんでした。Trie 木生成後の処理は一桁速くなったように思われる(雑な計測からの体感)ので、 Ruby のモジュールにするか、これ専用のサーバを立てて記事追加・削除・データ変換が毎回行えるようにすればかなり高速化できたものと思われますが、すでに終盤でそこまでする時間がありませんでした。

#include <vector>
#include <string>
#include <string.h>
#include <algorithm>
#include <stdio.h>
#include <unistd.h>
#include <map>
#include <iostream>
#include <unordered_map>

using namespace std;

void urlwrite(const char *str) {
        while(*str!='\0'){
                if ((*str>='a' && *str<='z') || (*str>='A' && *str<='Z') || (*str>='0' && *str<='9') || *str=='.' || *str=='-' || *str == '_' || *str == '~') {
                        fwrite(str, 1, 1, stdout);
                        ++str;
                        continue;
                }
                int t = *((unsigned char*)str);
                int b1 = t/16;
                int b2 = t%16;

                char tmp[3];
                tmp[0]='%';
                tmp[1]=(b1<10)?(b1+'0'):(b1+'A'-10);
                tmp[2]=(b2<10)?(b2+'0'):(b2+'A'-10);

                fwrite(tmp,1,3,stdout);
                ++str;
        }
}

void htmlwrite(const char *str) {
        while(*str!='\0'){
                switch (*str) {
                        case '&':
                                fwrite("&amp;", 1, 5,stdout);
                                break;
                        case '<':
                                fwrite("&lt;", 1, 4, stdout);
                                break;
                        case '>':
                                fwrite("&gt;", 1, 4, stdout);
                                break;
                        case '\'':
                                fwrite("&#039;", 1, 6, stdout);
                                break;
                        case '\"':
                                fwrite("&quot;", 1, 6, stdout);
                                break;
                        case '\n':
                                fwrite("<br />\n",1,7,stdout);
                                break;
                        case '\r':
                                fwrite("<br />\r",1,7,stdout);
                                break;
                        default:
                                fwrite(str,1,1,stdout);
                }
                ++str;
        }
}

class Trie {
        public:
                Trie* children[256];
                uint32_t ex[8];
                int exist=0;

                Trie(){
                        memset(ex,'\0',32);
                }

                void insert(unsigned char *str) {
                        if(*str=='\0') {
                                exist=1;
                                return;
                        }


                        if(!((ex[(*str)/32]>>((*str)%32))&1)){
                                children[*str]=new Trie();
                                ex[(*str)/32]|=1UL<<((*str)%32);
                        }
                        children[*str]->insert(str+1);
                }

                int find(unsigned char *str) {
                        int tmp=-2;
                        if (exist) {
                                tmp=-1;
                        }

                        if((ex[(*str)/32]>>((*str)%32))&1){
                                tmp=max(tmp, children[*str]->find(str+1));
                        }

                        if(tmp==-2)
                                return -2;
                        else
                                return tmp+1;

                }

};

int main (int argc, char *argv[]) {
        unordered_map <string, size_t> dic;
        Trie trie;

        setvbuf(stdout, NULL, _IOFBF, 102400);

        for(int i=2;i<argc; ++i){
                trie.insert((unsigned char*)argv[i]);
        }


        cout<<"<br>clock="<<clock()<<"<br>";

        while (*argv[1]!='\0') {
                int ret = trie.find((unsigned char*)argv[1]);
                int skiplen=1;

                if(ret>0){
                        char tmp=argv[1][ret];
                        argv[1][ret]='\0';
                        fwrite("<a href=\"/keyword/", 1,18,stdout);
                        urlwrite(argv[1]);
                        fwrite("\">", 1,2,stdout);
                        htmlwrite(argv[1]);
                        fwrite("</a>", 1,4,stdout);
                        skiplen=strlen(argv[1]);
                        argv[1][ret]=tmp;
                }else{
                        char tmp=argv[1][1];
                        argv[1][1]='\0';
                        htmlwrite(argv[1]);
                        argv[1][1]=tmp;
                }
                argv[1]+=skiplen;

        }
        cout<<"<br>clock="<<clock()<<"<br>";
}

この辺をバックグラウンドで走らせる準備などが必要なのだと学ぶことができました。