Sisimaiを使ったバウンスメールの管理

最近、Ninja650に乗り換えたSREチームの菅原です。今までマルチばかり乗ってきたんですが、ツインもなかなか面白いですね。シフトペダルをガチャコンいわせながら方々に出かける毎日です。

この記事では、サービスが配信しようとして何らかの理由で差し戻されたメール—バウンスメールの管理をどのように行っているかという話しを書きます。

バウンスメール

サービスがユーザに向けてメールを配信しようとすると、多かれ少なかれバウンスメールは発生します。メールアドレスが間違っている・携帯電話の設定で受信を拒否している・メールボックスが一杯にになっている・IPアドレスがブラックリストに載ってしまったためサーバにメールの受信を拒否されている…etc。完全になくすことは難しいですが、バウンスメールを放置するとメールの到達率を下げたり、送信先から一時的にメールの受信を拒否されたりすることがあるため、差し戻されてしまった宛先はリストに登録して、再送を抑止することが望ましいです。

SendGridのようなサービスを利用している場合、差し戻されたメールは自動的にリストに登録されて再度メールを送っても配送されないようになっていたりするのですが、クックパッドの場合は内部システムのPostfixサーバからメールを配信していたため、バウンスメールの管理をある程度、自前で作り込む必要がありました。

既存のシステム

以前はbounceHammerというOSSとMySQLのバウンスメール管理用のテーブルを使って、バウンスメールの管理が行われていました。

f:id:winebarrel:20170502181740p:plain

  1. アプリケーションはメール送信サーバのPostfixを経由して、メールを配信します
  2. 差し戻されたメールは、バウンスメール管理サーバのPostfixが受け取ります
  3. Postfixが受け取ったメールをbounceHammerが解析して自身のデータベースに入れます
  4. 定期実行されるスクリプトがbounceHammerのデータベースからアプリ用のデータベースにバウンスメール情報をマイグレーションします
  5. アプリケーションはユーザテーブルとバウンスリストを結合して、メールが差し戻された宛先にはメールを送らないようにします
  6. バウンスリストの情報は、管理用アプリケーションから削除することができます

既存システムの問題点

このバウンスメール管理システムには、いくつか問題点がありました。

  • bounceHammerの導入が後付けであったため、バウンスメール情報がbounceHammerとアプリ用データベースで二重管理になっていた
  • 同様に後付けが原因で、管理アプリケーションからアプリ用データベースの情報は削除できるが、bounceHammerの情報を削除(ホワイトリスト登録)できないため、手作業で同期を取る必要があった
  • ユーザテーブルとバウンスリストをSQLで結合して配信対象をフィルタリングする方式であったため、スケーラビリティに問題があった
  • バウンスリストがテーブルという単位で管理されているため、アプリケーションの各機能が個別に配信対象のフィルタリングを実装する必要があった
  • SQLの結合という形でフィルタリングを行うと、メールが配信されなかったユーザがそもそも配信の対象にならなかったのか、バウンスリストに登録されていたため配信されなかったのか、区別を行うことが難しかった
  • バウンスメールの情報がきちんと可視化されていなかったため、配信状況の把握に難があった
  • bounceHammerがEOLになった

問題点を抱えた状態での運用がつらく、またbounceHammerがEOLになったこと、さらにシステムのリニューアル作業が進行中だったこともあり、バウンスメール管理システムを新しく作り直すことにしました。そして、そのコアの部分として利用することになったのがSisimaiです。

Sisimai

SisimaiはbounceHammerの後継として開発されているバウンスメール解析ライブラリです。bounceHammerが管理用Webアプリや集計用のスクリプトも含めた複合的なシステムであったのに対して、シンプルなPerl・Rubyのライブラリです。ライブラリの依存関係も少なく、またわかりやすいAPIで、しかも自分が慣れたRubyのライブラリであったため、とても簡単に新しいシステムに組み込むことができました。

以下はSisimaiを使ってバウンスメールの解析を行うサンプルコードです(※公式ドキュメントより引用)

#! /usr/bin/env ruby
require 'sisimai'
v = Sisimai.make('/path/to/mbox') # or Path to Maildir

if v.is_a? Array
  v.each do |e|
    puts e.addresser.address    # shironeko@example.org # From
    puts e.recipient.address    # kijitora@example.jp   # To
    puts e.recipient.host       # example.jp
    puts e.deliverystatus       # 5.1.1
    puts e.replycode            # 550
    puts e.reason               # userunknown
  end
else
  # There is no bounce message in the mailbox
  # or Sisimai could not parse
end

Sisito

Sisimaiはすばらしいライブラリなのですが、bounceHammerにあったような管理用のWebアプリケーションはなくなってしまいました。エンジニア以外のスタッフが「問い合わせのあったメールアドレスをホワイトリストに登録する」等の作業が発生するため、管理用のWebアプリケーションは必須です。そこで以前の運用経験を踏まえ、バウンスメール情報保存用のデータベースとそれを管理するウェブアプリケーションを作成しました。それがSisitoです。

以下はSisitoの管理画面のスクリーンショットです。

f:id:winebarrel:20170508161014p:plain f:id:winebarrel:20170508161019p:plain

また、バウンスメール管理システム以外からバウンスメールの情報を取得するため、sisito-apiというAPIサーバも作成しました。

新バウンスメール管理システム

SisimaiとSisitoを使った新しいバウンスメール管理システムの構成が以下のようになります。

f:id:winebarrel:20170508153346p:plain

  1. アプリケーションはメール配信サーバのPostfixを経由してメールを配信します。このときPostfixの機能でblacklistに登録されているメールアドレスには配信しないようにします
  2. 差し戻されたメールはメール配信サーバに保存されます
  3. 定期実行されるSisimaiスクリプトがSisitoデータベースにバウンスメール情報をを保存します
  4. 定期実行されるblacklist更新スクリプトが一定の条件に従って(ハードバウンスのみ ・特定の理由のみ、など)blacklistを更新します
  5. Sisitoを使って統計情報の閲覧やホワイトリストへの登録を行います
  6. アプリケーションはsisito-apiを経由してバウンスメールの情報を取得します
  7. メールの送信ステータス・Subject・blacklistによるリジェクト状況などのログはElasticsearchに送信してKibanaで閲覧できるようにします

まとめ

新バウンスメール管理システムの導入により、アプリケーションの各機能で配信制御を行う必要がなくなり、Postfixでフィルタリングを行うことでスケーラビリティの問題も解決することができました。また、Sisitoによる可視化により問題が発生しても(たとえば、特定の理由のバウンスメールが増えているなど)状況をすぐに把握して迅速に対応することができるようになりました。さらにホワイトリストの登録処理がエンジニアを介さずにできるようになったため、業務フローのコストも下げることができました。

差し戻されるメールのエラー内容はサービスによって様々なパターンがあり、人間が解析することはかなりの労力を伴います。SisimaiのようなライブラリがOSSとして公開されていることは大変ありがたいことです。積極的に活用して、フィードバックなどで開発に貢献していきたいと考えています。

バウンスメールの解析で苦労しているかたは、一度Sisimaiを試してみてはどうでしょうか?

クックパッド サマーインターンシップ 2017 を開催します!

f:id:takai_naoto:20170512093753j:plain

いつもお世話になっております。エンジニア統括マネージャーの高井です。

クックパッドでは、毎年恒例になっているサマーインターンシップを今年も開催いたします! 今年のインターンシップは、昨年よりもパワーアップして、エンジニアやデザイナー志望のみなさんに向けた三つのコースと総合職を志望する方に向けた一つのコースを用意しています。

14day海外技術インターンシップ

f:id:takai_naoto:20170512093807j:plain

今年からの新しいチャレンジがこちらです。クックパッドのグローバル展開の中心であるイギリスのオフィスで働くことを通じて「クックパッドに入社して海外で活躍するってどんな働き方になるんだろう」というのを体験していただけるインターンシップです。

14day海外技術インターンシップでは、日本でクックパッドのサービス開発の考え方を学び、その後にイギリスへ渡航して実践的な開発に取り組んでいただきます。グローバルを本格化したクックパッドのミッションとバリューズを理解し、体験し、実践することができるインターンシップになっています。

ぜひとも、技術力と英語力に自信のある方の参加をお待ちしております!

17day技術インターンシップ

f:id:takai_naoto:20170512093811j:plain

クックパッドの技術を知るために最適なインターンシップがこちらです。17day技術インターンシップは、大きく前半と後半とに分かれています。

前半では、講義形式でクックパッドで使われている技術を学びます。今年は、「サービス開発」「Rails・TDD・Git」「モバイルアプリケーション(iOS/Android選択式)」「インフラストラクチャー」「SQL」「機械学習」「Ruby」と七つの講義を準備しています。去年と比べると新しく「インフラストラクチャー」「SQL」「Ruby」は新しく追加された講義です。毎年、ソフトウェアエンジニア界隈からも「学生向けでなければ自分が参加したい」と声があがるほどの豪華な講義です。今年はRubyのコア開発者でもある笹田による「Ruby」の講義もあります。

後半は、メンターとなるエンジニアと一緒にクックパッドの実環境で開発をしていただきます。社内のエンジニアと一緒になって、クックパッドの開発がどうやって進められているのか、どのような開発基盤が整備されているのかなど、クックパッドでの働き方を知るための、またとない機会となっています。

昨年の資料は下記になりますので、ぜひとも参考になさってください。

5dayサービス開発インターンシップ

f:id:takai_naoto:20170512093800j:plain

クックパッドのサービス開発の真髄に触れることができるのが、こちらのインターンシップです。

このインターンシップでは、エンジニアとデザイナーとでチームを組んで、サービス開発に取り組んでいただきます。ユーザーの課題についてひたすら考え抜き、悩み、それをどうやって解決するのか、そのためにはどんなサービスを作ればいいのかを考えぬくインターンシップです。

インターン中は、クックパッドの一線で活躍するエンジニアやデザイナーから常にフィードバックがされますので、それを通じて大きく成長できるチャンスです。将来は自分でサービスを立ち上げたいぞ、というエンジニアやデザイナーの方におすすめです。

14day 海外事業開発インターンシップ

f:id:takai_naoto:20170512105036j:plain

海外での事業開発にチャレンジしたいという方に向けたインターンシップがこちらです。総合職に向けたプログラムになっています。クックパッドの海外拠点へ赴き現地にて企画の実行まで体験してもらうという内容です。

海外ビジネスに携わり世界を舞台に活躍したい方や、世の中に大きなインパクトを与える事業を創り上げたい方、自分で考え、行動し、達成することに価値をおく環境で働きたい方の応募をお待ちしております。

どうぞ、お友達に紹介してくださいね。


毎年恒例となりつつありますが、毎回参加してくださる学生のみなさんのために会社を挙げて全力で取り組んでいます。 学生時代にしか経験できない貴重な機会になるとおもいます。学生のみなさんの参加をお待ちしております!

最近のサービス間のデータとイベントの連携について

こんにちは。牧本 (@makimoto) です。最近はバックエンドシステムの設計をやったりしています。

今回は複数のサービスが存在するとき、その間でどのようにデータ連携を実現するかついて述べていきます。

背景と問題定義

cookpad.com は世界有数の規模の Ruby on Rails で作られたウェブアプリケーションです。

巨大な Rails アプリケーションは単純に起動やデプロイ、テストが遅いという問題以上に、自分の変更が与える影響範囲を予測するのが困難という大きな問題があります。cookpad のメインレポジトリ (cookpad_all と呼ばれる) には1つの mountable engine を共有する Rails アプリケーションは7つがあり、困難さに拍車をかけています。cookpad_all を触る開発者は新しい機能を追加する、既存の機能に手を入れる、不用な機能を消すなど様々な場面でこの困難さと対峙することになります。

そのため、われわれは既存の機能を切り出したり、新機能を新しいアプリケーションとして実装するという取り組みを行なっています。その際問題となることの一つに、複数のサービス間から必要とされるデータをどのように共有したり、またそれらデータの変更などをどのように伝播させるかというものがあります。

1つのデータストレージがモデルロジックの異なる複数のサービスで共有されるのはデータの不備や不整合の温床となるので原則として行ないません。*1 そのため、一方のサービスのデータを他方で使いたい場合は、直接データストレージにアクセスするのではなく、データを管理するサービスを通して取得するというのが基本となります。

そのような複数のサービスの間でデータを連携したいという状況では、一方のサービスで発生した変更を受けて他方のサービスで処理をする、といった場面が増えてきます。たとえば、「あるユーザーが退会したことにより、他のサービスに存在するそのユーザーに関連するアイテムを削除してアクセスできなくする」といった場合です。

これらの課題を現実的なコストで解決することが必要となってきます。

アプローチ

Garage と GarageClient の導入による RESTful Hypermedia API の社内標準化

まず、データ連携の第一歩としてウェブ API によるデータの作成・閲覧・更新・削除についてです。

クックパッドではウェブ API の開発・利用には Garage とそのクライアントである GarageClient を用いています。 Garage は RESTful Hypermedia API を Ruby on Rails 上で開発するためのライブラリであり、クックパッドで4年近くの開発・利用されています。Garage 自体については過去の紹介記事を参照いただくことにして詳細は割愛します。

共通の API 実装のためのライブラリを用いることで、各サービス間のインターフェースの差異をなくし、NantokaClient を乱立させることなくスムーズにサービスが管理しているデータにアクセスできるようにしています。また、Garage は認証認可の機能も提供しているので、共通基盤である認証システムと連携させることで適切なアクセス制御も実現できるようになっています。

イベントにもとづくデータの連携

前節では各サービスの管理しているデータにアクセスする方法を説明しました。 次は前述した「あるユーザーが退会したことにより、他のサービスに存在するそのユーザーに関連するアイテムを削除してアクセスできなくする」というケースについて考えていきます。

シンプルな方法

複数のサービスをまたいだデータの連携を実現するためのもっとも単純な方法は、データの変更があったタイミングで、そのデータに依存しているサービスに対し処理を促すリクエストを行なう方法です。この方法は、2つのサービス間でしか連携がないことがわかっている場合はうまく動きます。しかし、サービスが増えてくると複雑さが増していきます。また、この方法では、データの提供元と提供先の両方の実装に手を入れる必要があるため開発自体も煩雑になります。

Amazon SNS を用いた pub-sub メッセージング

そこでクックパッドでは、 Amazon Web Services の提供する Simple Notification Service (SNS) を用いた pub-sub メッセージングを採用しています。

これは以下の流れで実現します:

  1. あるイベント (たとえば、ユーザーの削除など) が発生するとそれに対応する SNS トピックに通知を行なう *2
  2. SNS はトピックを購読している HTTPS エンドポイントに対しイベントが発生した旨を通知する
  3. 通知を受けた HTTPS エンドポイントのサービスはその通知内容に応じて処理を行なう

なお、 SNS のメッセージには 256 KB のサイズ制限があるので、SNS のメッセージとしてはデータの ID のみを渡して、データ本体は Garage 経由で取得するという方式がよく取られます。

これらは Ping という名前の社内ライブラリによって実現されており、各サービスで簡単に導入できるようになっています。

この方法の問題点

しかしながら、この方法はいくつかの問題があります:

  • 本来内部通信しかないサービスであったとしても HTTPS エンドポイントを SNS の通知を受け取るためにインターネットに公開する必要がある
  • 通知を HTTP リクエストとして受けるので、リクエスト数が増えたらアプリケーションサーバ (Unicorn であるケースが多い) が詰まるリスクがある

Barbeque と Amazon SQS を用いたファンアウト

これを解決するため、通知の広報先を HTTPS エンドポイントから AWS の Simple Queue Service (SQS) のキューに変更し、そのキューをポーリングしてジョブを実行するという方法に転換しようとしています。 (いわゆるファンアウト)

この仕組みは、クックパッドのジョブキューシステムである Barbeque の機能として実装しています。 Barbeque は SQS をキューとして使っているため、実装的には、キューとジョブと SNS トピックを関連付けてキューでトピックを購読する機能を追加するだけでこの仕組みは実現可能でした。 (https://github.com/cookpad/barbeque/pull/20)

この方法での流れは以下の通りです:

  1. あるサービスでイベント (たとえば、ユーザーが退会する、など) が発生すると、サービスは SNS トピックに通知を行なう
  2. SNS はトピックを購読している SQS のキューに対しイベントが発生した旨を通知する
  3. Barbeque のワーカーが対象のキューをポーリングし、通知を受けとる
  4. Barbeque のワーカーが通知をもとに関連付けられたジョブを実行する

Barbeque は Docker の利用を前提としたジョブキューシステムなので、ジョブの実行は Docker コンテナ内で行なわれます。すでに稼動しているサービスとは別のコンテナでジョブが実行されるため、ジョブが増えたことによりリクエストが増えて Unicorn のワーカーが詰まる問題を避け、よりスケーラブルなジョブ実行環境となります。クックパッドでは、現状ほとんどすべての新規サービスが Docker コンテナ上で動いているため、この仕組みの恩恵を十分に受けることができます。

まとめ

本稿では、クックパッドでどのように複数のサービス間でのデータ連携を行なっているかについて述べました。 Docker、AWS のメッセージング系のサービス、内製のソフトウェアを組み合わせて複数のサービスで協調してデータを扱うことを実現しています。

サービスを分割するときの心配事の1つとして、既存サービスのデータとうまく連動させるのが難しいというのがありますが、今回紹介した方法である程度は解決できると考えています。

*1:ただし、既存サービスから機能を徐々に切り出す場合 などのケースで例外はあります。

*2:実際の運用では SNS にリクエストを送るために fluentd を経由させています。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/