はじめに
こんにちは。DX本部システム開発第一事業部の鈴木康男です。エンジニア・PM・マネージャーとして、主にWeb3.0に関わるプロジェクトを担当しております。
AIで作業は「速く」なるのに、仕事はなぜか「早く」終わらない。そんな感覚を持ったことはないでしょうか。今回は、AIによる高速化で起きる「ボトルネックの移動」というテーマについて、前職で経験したリリーストラブルの話を交えながら、これからの時代に何を積み重ねておくべきかを考えてみます。
続きを読むこんにちは。DX本部システム開発第一事業部の鈴木康男です。エンジニア・PM・マネージャーとして、主にWeb3.0に関わるプロジェクトを担当しております。
AIで作業は「速く」なるのに、仕事はなぜか「早く」終わらない。そんな感覚を持ったことはないでしょうか。今回は、AIによる高速化で起きる「ボトルネックの移動」というテーマについて、前職で経験したリリーストラブルの話を交えながら、これからの時代に何を積み重ねておくべきかを考えてみます。
続きを読むシステム開発第一事業部の大泉です。普段はエンジニアとして Python でバックエンドの開発を行っています。
AWS の SQS を使用して 1500 程度のサイトを対象に 1 日数百万ページを処理するクローラーを構築しました。開発初期、ページ数の多いサイトの URL でキューが埋め尽くされ、他のサイトのクローリングがほとんど行われない問題が発生しました。本記事ではこのようなキュー詰まりを解消し、すべてのサイトを平準化してクローリングする方法をご紹介します。
なお、サンプルコードは実装のイメージが伝わりやすいように簡略化したものであり、実際の実装とは異なります。
SQS でクローリング対象の URL をキューイングするアーキテクチャです。1 つのメッセージに 1 つの URL を含めるようにします。

ECS で行うクローリングの処理フローです。
クローリング処理フロー
クローラーの立ち上げ時はキューにメッセージが何も入っていないため、トリガーとなるメッセージが必要です。それを投入するのが Lambda です。例えば以下のようなフローになります。
https://example.com/ を取得https://example.com/ をキューに送信問題になるのがリンクが多いサイトです。1 URL から大量の URL が抽出され、さらにそこから大量の URL が抽出され、というように一部のサイトの URL でキューが埋め尽くされます。その結果、他のサイトの URL を取り出すのに非常に時間がかかります。そこで以下を導入しました。
「何回目のフェッチで見つかった URL か」をクローリングの深さとします。 例えばクローリングのトリガーとなる Lambda によってキューに送信された URL は深さ 0 です。深さ 0 の URL から取得した URL は深さ 1 です。
クローリングの深さによって優先度高キューと優先度低キューの 2 つに分離しました。優先度高キューには深さが小さい URL を入れます。優先度低キューには深さが大きい URL を入れます。
例えば、深さ 0 と 1 は優先度高キュー、深さ 2 以降は優先度低キューに入れるようにします。
優先度高キューのポーリングを先に行い、メッセージを受信しなければ優先度低キューのポーリングを行います。これにより深さが小さい URL のクローリングが優先して行われるようになります。
同時実行制限は次の 2 種類を導入しました。
2 についてはキャッシュサーバ(ElastiCache for Valkey)にメッセージ処理回数を保存します。キャッシュサーバを共有ストレージとして利用することで、各サーバ間でサイトごとのメッセージ処理回数を共有できるようになります。それぞれの同時実行制限に引っかかったメッセージは、次に述べる「メッセージ単位での可視性タイムアウト」を利用し、一定期間受信できないようにします。
サーバ内でサイトごとのメッセージ処理回数を管理するため、専用のクラスを実装しました。辞書型(マップ)で {"siteA": 1, "siteB": 2} のようにサイトごとのメッセージ処理回数を保持します。
以下はサンプルコードです。
class ConcurrencyManager: """サイトごとの同時実行数を管理""" def __init__(self): self.active_tasks = defaultdict(int) def can_acquire(self, site_id: int, limit: int) -> bool: """上限を超えていないかチェック""" if self.active_tasks[site_id] < limit: self.active_tasks[site_id] += 1 return True return False def release(self, site_id: int): """クローリングが完了(メッセージ処理が完了)したときはデクリメント""" if self.active_tasks[site_id] > 0: self.active_tasks[site_id] -= 1
キャッシュサーバに処理回数を保存し、1 時間経過後にキャッシュが消えるように有効期限を設定しています(ttl_seconds = 3600)。
以下はサンプルコードです。制限を超えている場合は呼び出し元に False を返し、クローリングを行わないようにします。
def check_limit(site_id: int, limit: int) -> bool: """サイトごとのメッセージ処理回数をチェック""" key = f"count:{site_id}" ttl_seconds = 3600 with client.pipeline(transaction=True) as pipe: pipe.incr(key) pipe.ttl(key) count, ttl = pipe.execute() # TTLが設定されていない場合、有効期限を設定 if ttl == -1: client.expire(key, ttl_seconds) # 制限を超えているかチェック return count <= limit
可視性タイムアウトとは、あるコンシューマーが処理中のメッセージを他のコンシューマーが重複処理しないようにするため、一定期間キューから見えなくする機能です。SQS ではキュー自体の可視性タイムアウトとメッセージ単位での可視性タイムアウトを設定できます。
ここではメッセージ単位での可視性タイムアウトを利用します。同時実行制限に引っかかったサイトの URL に対してフェッチを行わず、可視性タイムアウトを延長してキューに戻します。これにより、制限に引っかかったメッセージが見えなくなっている時間が長くなり、他サイトのメッセージを先に取り出せるようになります。
可視性タイムアウトの設定時間は、あらかじめ決めた範囲(例: 5~10 秒)の中でランダムに決定します。ランダムに設定することで同じ条件のメッセージが同時に見えるようになることを防ぎます。
一斉にメッセージを処理するため、boto3 ライブラリの change_message_visibility_batch を使用して可視性タイムアウトを変更します。
以下はサンプルコードです。
entries = [] for i, receipt_handle in enumerate(receipt_handles): # 最小設定時間と最大設定時間を決定 min_delay, max_delay = self.delay_calculator.determine_visibility_timeout() # ランダムに可視性タイムアウトを決定 entries.append({ "Id": str(i), "ReceiptHandle": receipt_handle, "VisibilityTimeout": random.randint(min_delay, max_delay) }) # バッチ(1度に10メッセージ)で可視性タイムアウト変更 for i in range(0, len(entries), 10): batch_entries = entries[i : i + 10] async with self._create_client() as client: await client.change_message_visibility_batch( QueueUrl=queue_url, Entries=batch_entries )
遅延キューは、メッセージが初めてキューに入ってから一定期間キューから見えなくなる機能です。つまり、すぐにメッセージが取り出せないようにします。これを使うことで、クローリング回数の多いサイトの処理を抑制し、回数の少ないサイトのメッセージを優先的に取り出せるようにします。
なお、可視性タイムアウトはコンシューマーがメッセージを受信してから見えなくなるのに対し、遅延キューはメッセージがキューに入ってから見えなくなります。
フェッチレスポンスから抽出した URL (次のクローリング URL)をキューに送信するときに、以下の条件によって遅延キューの設定時間を決定します。
これらを考慮し、最小設定時間~最大設定時間の間でランダムに設定します(ここでも揺らぎを持たせることで、同時にメッセージが取り出されるのを防ぎます)。これにより、リンクが多いサイトでは階層が深くなるほど新たに見つかる URL の遅延が長くなり、埋もれていた小規模サイトの URL を取り出しやすくなります。
例)
可視性タイムアウトと同様に、一斉にメッセージを処理するため、boto3 ライブラリの send_message_batch を使用して遅延キューを設定します。
以下はサンプルコードです。
for i in range(0, len(urls), 10): # クローリングで見つかったURLをバッチ送信のため分割 batch_urls = urls[i : i + 10] # クローリングの深さとサイトごとのメッセージ処理数から最小設定時間と最大設定時間を決定 min_delay_seconds, max_delay_seconds = self.delay_calculator.determine_delay_seconds() # キューに送信するためのエントリーを作成. 遅延キューの設定はDelaySeconds entries = [] for idx, url in enumerate(batch_urls): base_site_info.url = url entries.append({ "Id": str(idx), "MessageBody": json.dumps(base_site_info.to_dict(), ensure_ascii=False), "DelaySeconds": random.randint(min_delay_seconds, max_delay_seconds), }) # バッチ(1度に10メッセージ)でキューに送信 async with self._create_client() as client: await client.send_message_batch( QueueUrl=self.crawler_low_queue_url, Entries=entries, )
SQS を利用した大規模クローラーにおけるキュー詰まりの問題とその解決策について紹介しました。クローリング平準化のため以下の 4 つのアプローチを組み合わせました。
以上のように、SQS で大規模なメッセージを扱う際は単に流し込むだけでなく流量制御が重要になります。本記事が同様の課題を抱える方の参考になれば幸いです。
テコテックでは新卒採用、中途採用共に積極的に募集をしています。
採用サイトにて会社の雰囲気や福利厚生、募集内容をご確認いただけます。
ご興味を持っていただけましたら是非ご覧ください。
こんにちは。DX本部システム開発第一事業部の鈴木康男です。エンジニア・PM・マネージャーとして、主にWeb3.0に関わるプロジェクトを担当しております。
7/24(金)、エンジニアリングマネージャーの方々が集まる「EM Meetup」という社外勉強会に参加してきました。テーマは、AIのトークンをどう使うか。「とにかく使え」という発想から、「使った分でどれだけ成果を出すか」という発想への移行がテーマの会でした。
今回参加したのは、こちらの「EM Meetup #21 〜トークンマキシングとトークンマネジメントのはざま〜」という回です。
engineering-manager-meetup.connpass.com
少人数のグループに分かれて議論する時間が中心だったのですが、今回はイベントの内容をそのまま紹介するのではなく、そこでの議論を自分なりに咀嚼して持ち帰った考えを言語化してみたいと思います。
続きを読むシステム開発第二事業部の冨永です。
主にiOS/iPadアプリの開発を担当しております。
これまでの運営記では、企画が3つに分裂した話、人集めの苦労、ペルソナを通じた運営アプリの設計、そして葬儀やトークン地獄を乗り越えた実装フェーズまでをお届けしてきました。
そして番外編の伊香保開発合宿も挟みつつ、いよいよ——
社内アプリコンテスト、本戦当日です。
3ヶ月かけて準備してきた運営として、やれることは出し切りたい。
そんな想いで迎えた一日を、振り返っていきたいと思います。
続きを読むこんにちは。DX本部システム開発第一事業部の鈴木康男です。エンジニア・PM・マネージャーとして、主にWeb3.0に関わるプロジェクトを担当しております。
AIや自動化ツールが当たり前になった今だからこそ、立ち返ってみるべきことを日々考えています。その一つが「業務を効率化する前に、まずは素直に今のやり方でやってみることが大事だ」ということです。なぜ手段から入るとうまくいかないのか、具体例を交えてまとめてみます。
続きを読むこんにちは。DX本部システム開発第一事業部の鈴木康男です。エンジニア・PM・マネージャーとして、主にWeb3.0に関わるプロジェクトを担当しております。
今回は、AIを全面的に活用する開発プロジェクトの現場で感じた、ある気づきについて書いてみます。それは「AIが当たり前になった時代だからこそ、エンジニアは自分の言葉で説明する力を磨く必要がある」ということです。なぜ「話せること」の重要性が増しているのか、そしてAIとどう付き合っていくのがよいのか。順を追ってまとめてみます。
続きを読むシステム開発第一事業部の大泉です。普段はエンジニアとして Python でバックエンドの開発を行っています。 この記事では、OpenSearch を知らない方向けに OpenSearch の概要と Python を使った CRUD 操作・検索について解説します。
OpenSearch は分散型の検索・分析エンジンです。Elasticsearch のオープンソースのフォークとして開発されています。
主な用途は次のとおりです。