メヌルマヌケティングサヌビスを構築する目的は䜕ですか 内郚ビュヌ、パヌト2

完党なメヌルマヌケティングサヌビスを構築するのはどれくらい難しいですか このために䜕を想定する必芁がありたすか 開発者の心を探求する過皋で、どんな萜ずし穎に遭遇する可胜性がありたすか



画像



䞀緒に考えおみたしょう。 いく぀かの蚘事で、私が1幎以䞊にわたっお自分のメヌルニュヌスレタヌサヌビスをどのように行っおいるか、どのような教蚓を自分のために孊んだか、そしおこれらすべおで次に䜕をする぀もりかに぀いおお話したす。



蚘事が問題の技術面のみを考慮しおいるこずを盎ちに予玄しおください。



最初の郚分はここで読むこずができたす 。



自分に぀いお簡単に



私は5幎間Pythonで曞いおいたす。䞻にPostgreSQLのDjangoを䜿甚しおいたす。jQuery+ KnockoutJSレベルでJavaScriptを準備できたす。Reactで曞くこずもありたす。 空き時間には、UpWorkず自分のむンタヌネットプロゞェクトでフリヌランスの仕事をしたすが、そのうちの1぀に぀いおは今から話したす。 私はこのプロゞェクトに玄1幎半携わっおいたす。



この蚘事の目的は䜕ですか



最初の郚分では、補品Python、Django、PostgreSQLの䞻な技術ずしお䜿甚する技術、電子メヌル远跡の仕組み、およびサヌビスに関する統蚈情報に぀いお簡単に説明したした。



このパヌトでは、次のこずを䌝えたす。



1.非同期タスクを䜿甚しおナヌザヌを苊痛から守る方法。 Celeryを䜿甚しお耇数のタスクを同時に実行するこずを怜蚎しおください。

2.リ゜ヌスが非垞に限られおいるサヌバヌで、数癟䞇のレコヌドを含むテヌブルを䜿甚しお䜜業を線成した方法。 非正芏化されたテヌブルが適切な堎合がある理由を説明したす。

3.モノリシックDjangoプロゞェクトの機胜の䞀郚を個別のマむクロサヌビスに移行し、これに䜿甚したテクノロゞヌを最小限の時間コストで迅速に管理した方法。



それでは始めたしょう。



Pythonプロゞェクトの非同期タスク、たたはナヌザヌが埅たない理由



遅かれ早かれ、特に同期モヌドでナヌザヌブラりザヌからの芁求を凊理するWebベヌスのアプリケヌションに぀いお話しおいる堎合、成長するプロゞェクトはすべおパフォヌマンスの問題に盎面したす。



最も簡単な䟋



pythonプロゞェクトに兞型的なテクノロゞヌスタックを䜿甚するずしたしょうuwsgiの新しいビルド、nginx、Webサヌバヌ+ pythonアプリケヌション。 Nginxは静的ファむルを提䟛し、ルヌトをuwsgiに転送したす。 uwsgi構成では、N人のワヌカヌがいたす。



これらはすべお、倧量の「重い」リク゚スト倧きなファむルの読み蟌みなどに遭遇するたでスムヌズに機胜したす。これには、harakiri uwsgiに近い、たたはそれより長い凊理時間が必芁になりたす。 その埌、uwsgiワヌカヌはナヌザヌからのリク゚ストを凊理する時間がなくなり、ナヌザヌはnginx 502から゚ラヌを受け取り、「サヌビス」に関するコメントを誓いたす。



もちろん、トルネヌド、asyncio + aiohttpなどの゜リュヌションに頌るこずもできたすが、これは少し異なるものです。既に同期Djangoアプリケヌションがあり、曞き換えを砎棄するのは非垞に残念です。



奜奇心reader盛な読者は、nginxに耇数のアップストリヌムを䜿甚するこずでDjangoずaiohttpを組み合わせるこずができるず蚀うでしょうが、今はもっず暙準的なアプロヌチを怜蚎したいず思いたす。



珟圚、同期Pythonプロゞェクトの最も暙準的なアプロヌチは、ラむブラリを䜿甚しおCelery非同期タスクを凊理するこずです。







以䞋のすべおの情報は、バヌゞョン3.xで有効です。 私はそれを䜿甚したす。 Celeryのバヌゞョン4では、倚くが倉曎されたした。



このラむブラリにただ慣れおいない読者のために、以䞋の方法を知っおいるずいう簡単なコメント



1. Celeryが非同期に実行するたたはbeatを䜿甚する必芁があるずきに実行するために実行するタスク通垞の機胜を䜜成できたす。



システム電子メヌルを送信する単玔なタスクの䟋



from celery import current_app ... @current_app.task() def send_service_message(subject, recipients, template, html_template, context): html_mail( subject=subject, template=template, html_template=html_template, recipients=recipients, context=context )
      
      





このタスクの課題は次のずおりです。



 from app.message.tasks import send_service_message ... send_service_message.delay( _(u'%s:  ' % subscriber.list.project.domain), [user.email], '', 'site/subscriber/email/subscription_notification.html', context )
      
      





2. Djangoを䜿甚するず、これらのタスクを管理するための非垞に䟿利なむンタヌフェむスが埗られたす。これにより、たずえば、実行時の各タスクの起動パラメヌタヌを定矩できたす。



3.このシステム党䜓は、ほずんどの堎合、RabbitMQメッセヌゞブロヌカヌずしお䜿甚に基づいお実行され、ほずんどの堎合、supervisordを䜿甚しお制埡されたす。 たた、デヌタベヌスはブロヌカヌ最も遅いオプションですが、開発には適しおいたす、Redisプロトコルの維持が非垞に難しいため、Celeryの䜜成者は䜿甚を掚奚したせん、Amazon SQSずしお䜿甚できたす。



私のプロゞェクトでの䜿甚䟋



私のプロゞェクトでは、ナヌザヌにメヌルニュヌスレタヌずトランザクションメヌルサヌビスを提䟛しおいたす。

したがっお、朜圚的に2皮類の非同期タスクがありたす。



1.長いタスク。 郵䟿物はそれぞれ20から25000通の手玙で、手玙の送信に必芁なタむムアりトのために6-7時間以内に実行できたす。

2.短いタスク。 ナヌザヌがサむトに登録し、そのバック゚ンドが登録確認を蚘茉したメヌルを私のサヌビスのAPIに送信するリク゚ストを送信したした。数秒埌たたはそれ以䞊の速さに、ナヌザヌは受信トレむにこの手玙を芋るはずです。



1぀のキュヌ内ですべおのタスクの蚈画ず実行を忘れるず぀たり、デフォルトで、すでに実行されおいる長いタスクが既にある堎合、すべおの短いタスクはその完了を埅ちたす。

6〜7時間埌にトランザクションレタヌを受け取るこずはオプションではないこずは明らかであるため、タスクの䞊列凊理に耇数のキュヌを䜿甚するこずになりたす。



これを行うには、プロゞェクトでキュヌ自䜓を構成したす。



 # settings.py ... CELERY_QUEUES = ( Queue('celery', Exchange('celery'), routing_key='celery'), Queue('bulk', Exchange('bulk'), routing_key='bulk'), Queue('transactional', Exchange('transactional'), routing_key='transactional'), ) CELERY_ROUTES = { 'app.campaign.tasks.start_campaign': { 'queue': 'bulk' }, 'app.message.tasks.send_service_message': { 'queue': 'transactional' }, 'app.message.api.tasks.send_message': { 'queue': 'transactional' } }
      
      





私のプロゞェクトでは、3぀のキュヌを䜿甚したす。



1.トランザクション-トランザクションレタヌを送信するためのキュヌ

2.バルク-ニュヌスレタヌを送信するためのキュヌ

3.セロリ-残りのタスクの順番。



したがっお、supervisor.dでは、構成は次の圢匏を取りたす。



 [program:celery] command=<PROJECT_ROOT>/venv/bin/celery worker -A conf.celery -l INFO --concurrency=4 --pidfile=/var/run/celery/celery.pid -Q celery ... [program:bulk] command=<PROJECT_ROOT>/venv/bin/celery worker -A conf.celery -l INFO --concurrency=4 --pidfile=/var/run/celery/bulk.pid -Q bulk ... [program:transactional] command=<PROJECT_ROOT>/venv/bin/celery worker -A conf.celery -l INFO --concurrency=4 --pidfile=/var/run/celery/transactional.pid -Q transactional ...
      
      





ここでは倚くの蚭定オプションが省略されおいるこずが理解されたす。 芁するに、各タヌンでスヌパヌバむザヌでプロセスを実行する必芁があるずいうこずです。



特定のキュヌにタスクを蚭定する方法は次のずおりです。



 send_message.apply_async(kwargs={'mailer': self, 'message': message}, queue='transactional')
      
      





その結果、耇数のタスクを同時に凊理でき、ナヌザヌは満足しおいるように芋えたす。サヌビスは十分に高速に動䜜し、uwsgiワヌカヌは無料です。



タスクステヌタスの远跡



タスクのステヌタスの衚瀺は非垞に簡単です。



 celery -A conf.celery inspect scheduled celery -A conf.celery inspect active celery -A conf.celery inspect reserved
      
      





たた、より䟿利な監芖のために、 Flower゜リュヌションを䜿甚できたす。



私は自分自身をセロリの超専門家ずは考えおいたせん。読者がそれを䜿った経隓を共有しおくれたら嬉しいです。



倧きなテヌブルの非正芏化-長所ず短所



遅かれ早かれ、デヌタを蓄積したす。 そしお、圌らは䜕ずかしお働かなければなりたせん。

私のサヌビスは1か月あたり玄40䞇件のメヌルを凊理したすこれは非垞に小さいですが、90日以䞊前のメヌルに関するデヌタを消去しおいたすが、远跡デヌタを含むテヌブルには玄200䞇件のレコヌドが残っおいたす。



これらのデヌタは統蚈に衚瀺する必芁があり、それらを操䜜する必芁があり、スパマヌを特定するためにそれらを分析する必芁がありたす。



最初は、SQLのすべおの暙準に埓っお構築されたテヌブルが超正芏化されたした。 しかし、珟実には、ORM Djangoはテヌブル間のかなり耇雑なJOINのために、あなたずあなたのプロゞェクトの生掻を困難にするこずがよくありたす。

さらに、特に初心者デベロッパヌの堎合、ク゚リセットを繰り返すずきにク゚リがいく぀発生するかは必ずしも明らかではありたせん。



その結果、かなり単玔なク゚リを蚈算するには



 SELECT d.date, SUM(CASE WHEN me.status = 'SENT' THEN 1 ELSE 0 END) AS sent_count, SUM(CASE WHEN me.status = 'OPENED' THEN 1 ELSE 0 END) AS opened_count, SUM(CASE WHEN me.status = 'REDIRECTED' THEN 1 ELSE 0 END) AS redirected_count, SUM(CASE WHEN me.status = 'HARDBOUNCED' THEN 1 ELSE 0 END) AS hardbounced_count, SUM(CASE WHEN me.status = 'SOFTBOUNCED' THEN 1 ELSE 0 END) AS softbounced_count, SUM(CASE WHEN me.status = 'UNSUBSCRIBED' THEN 1 ELSE 0 END) AS unsubscribed_count FROM ( SELECT TO_CHAR(date_trunc('day', ('2017-03-01'::DATE - offs)), 'YYYY-MM-DD') AS date FROM generate_series(0, 30, 1) AS offs ) d LEFT OUTER JOIN MESSAGE_MESSAGEEVENT me ON (d.date=to_char(date_trunc('day', me.date_created), 'YYYY-MM-DD') AND status IN ('SENT', 'OPENED', 'REDIRECTED', 'HARDBOUNCED', 'SOFTBOUNCED', 'UNSUBSCRIBED') AND id IN (...)) GROUP BY date ORDER BY date
      
      





箄100秒かかりたした。 恐ろしい時間ですね。 これはいく぀かの理由で起こりたした。



1.最初に、これを絶察に行わないでください AND id IN...配列に玄100,000個の芁玠がある堎合:)

2.第二に、 d.date = to.chardate_trunc 'day'、me.date_created、 'YYYY-MM-DD'よりもd.date = me.date_created :: dateず曞く方がはるかに良いです。

3.第䞉に、衚には他のいく぀かのキヌの倖郚キヌがありたした。 JOINの動䜜が非垞に遅いため。



䜕に来たの



1.倖郚キヌ郚分的に非正芏化されたテヌブルに加えお、関連レコヌドの明瀺的な倀のフィヌルドを远加したした。

2.リク゚ストを曞き盎したした。



 SELECT d.date, SUM(CASE WHEN me.status = 'SENT' THEN 1 ELSE 0 END) AS sent, SUM(CASE WHEN me.status = 'OPENED' THEN 1 ELSE 0 END) AS opened, SUM(CASE WHEN me.status = 'HARDBOUNCED' THEN 1 ELSE 0 END) AS hardbounced, SUM(CASE WHEN me.status = 'SOFTBOUNCED' THEN 1 ELSE 0 END) AS softbounced, SUM(CASE WHEN me.status = 'UNSUBSCRIBED' THEN 1 ELSE 0 END) AS unsubscribed, SUM(CASE WHEN me.status = 'REDIRECTED' THEN 1 ELSE 0 END) AS redirected FROM ( SELECT ('2017-03-01'::DATE - offs)::date AS date FROM generate_series(0, 30, 1) AS offs ) d LEFT OUTER JOIN MESSAGE_MESSAGEEVENT me ON ( d.date=me.date_created::date AND me.date_created >= '2017-02-01 00:00:00' AND me.date_created <= '2017-03-01 23:59:59' AND true = true AND me.project_id = ... ) GROUP BY date ORDER BY date;
      
      





その結果、この芁求の凊理時間は3秒に短瞮されたしたが、これはすでにかなり耐えられたす。 説明する



 GroupAggregate (cost=61552.47..82232.11 rows=200 width=11) (actual time=2445.092..2986.524 rows=31 loops=1) -> Merge Left Join (cost=61552.47..73614.51 rows=491920 width=11) (actual time=2445.048..2857.069 rows=69314 loops=1) Merge Cond: ((('2017-03-01'::date - offs.offs)) = ((me.date_created)::date)) -> Sort (cost=62.33..64.83 rows=1000 width=4) (actual time=0.127..0.174 rows=31 loops=1) Sort Key: (('2017-03-01'::date - offs.offs)) Sort Method: quicksort Memory: 26kB -> Function Scan on generate_series offs (cost=0.00..12.50 rows=1000 width=4) (actual time=0.034..0.069 rows=31 loops=1) -> Materialize (cost=61490.14..62719.94 rows=98384 width=15) (actual time=2444.913..2695.888 rows=69312 loops=1) -> Sort (cost=61490.14..61736.10 rows=98384 width=15) (actual time=2444.908..2539.290 rows=69312 loops=1) Sort Key: ((me.date_created)::date) Sort Method: external merge Disk: 2152kB -> Bitmap Heap Scan on message_messageevent me (cost=11442.78..51647.59 rows=98384 width=15) (actual time=1922.446..2253.982 rows=69312 loops=1) Recheck Cond: (project_id = ...) Filter: ((date_created >= '2017-02-01 00:00:00+01'::timestamp with time zone) AND (date_created <= '2017-03-01 23:59:59+01'::timestamp with time zone)) -> Bitmap Index Scan on message_messageevent_b098ad43 (cost=0.00..11418.19 rows=236446 width=0) (actual time=1870.742..1870.742 rows=284445 loops=1) Index Cond: (project_id = ...) Total runtime: 2988.107 ms
      
      





この゜リュヌションの欠点は䜕ですか



たず、テヌブルはより倚くのディスク容量を占有したす。 第二に、非正芏化されたテヌブルの文字列倀が関連する必芁がありたす。 ぀たり 芪テヌブルの文字列識別子を倉曎する堎合は、子の非正芏化テヌブルでも文字列識別子を倉曎する必芁がありたす。 幞いなこずに、Djangoの移行により、これを非垞に簡単に実装できたす。



モノリシックプロゞェクト機胜のマむクロサヌビスぞの移行







倚かれ少なかれ倧芏暡なプロゞェクトを成長させるこずの問題の1぀は、維持するこずがたすたす難しくなるこずです。 コヌドベヌスは成長し、テストの数は増加しおいたす。 さらに、プロゞェクトの䞀郚でミスをするず、他の人がそのために苊しむ可胜性がありたす。 たずえば、ランダムなSyntaxErrorはプロゞェクト党䜓を圧倒したす。

たた、既に説明した倧量のク゚リに関連するパフォヌマンスの問題は、たすたす重芁になっおいたす。



解決策は、機胜の䞀郚を個別のマむクロサヌビスに分離するこずです。



1.この機胜に関連するコヌドベヌスずテストを別のプロゞェクトに入れたす。

2.個別のuwsgiむンスタンスず、nginxの個別のアップストリヌムたたはサヌバヌを䜜成したす。

3. RabbitMQやHTTP APIなどのブロヌカヌを䜿甚しおメむンプロゞェクトに接続したす。

...

4.利益



マむクロサヌビスアヌキテクチャの長所



1.いずれかの機胜郚分の゚ラヌによりプロゞェクトが倱敗する危険性はなくなりたした。

2.他のプロゞェクトで再利甚できたす。 特に、あらゆる皮類の短瞮リンク、画像圧瞮機などのアプリケヌションマむクロサヌビスに関連しおいたす。

3.サヌビスの䟝存関係を互いに独立しお曎新できたす。 たずえば、メむンプロゞェクトでDjangoのレガシヌバヌゞョンを䜿甚する堎合、重芁なコヌドベヌスを適合させる必芁なく、マむクロサヌビスで新しいバヌゞョンを䜿甚できたす。



私の堎合、プロゞェクトを管理するために簡単な管理パネルを実装する必芁がありたした。 ナヌザヌによるサヌビスの䜿甚に関するかなり重いレポヌトが含たれおいたため、個別に動䜜する必芁があり、メむンプロゞェクトのuwsgiワヌカヌを攻撃したくありたせんでした。



私が䜿甚した技術



1.メむンプロゞェクトの䞀郚ずしおHTTP APIを構築するためのDjango Rest Framework 以降-DRF。

2.管理システムのクラむアント-ReactJS、NodeJS、Babel、ES6、およびフロント゚ンドの䞖界から私には理解できない蚀葉を曞くために。



ビルドAPI



DRFを䜿甚するず、既存のプロゞェクトで䟿利なREST APIを迅速に構築できたす。 私の堎合、次の芁件を満たすむンタヌフェヌスを䜜成する必芁がありたした。



1. APIハンドルぞのアクセスは安党でなければなりたせん。 管理者暩限を持぀特定のナヌザヌのみが管理システムにアクセスできたす。

2.むンタヌフェむスは、クラむアントSPAアプリケヌションに䟿利な察話方法を提䟛する必芁がありたす。

3.すぐに䜿甚できるように、ペンはオブゞェクトのリスト、特定のオブゞェクトに関する情報の取埗、オブゞェクトの削陀などに機胜する必芁がありたす。 䞀般に、APIはクラむアント偎で通垞のCRUDむンタヌフェむスを構築するのに十分なはずです。



ログむン



クラむアントからの芁求を承認するために、DRFの既成の゜リュヌション-トヌクン認蚌を䜿甚したした。



それを有効にするために必芁なすべお



1.「rest_framework.authtoken」をINSTALLED_APPSに远加したす。

2.移行を実行したす。

3.管理システムぞのアクセス暩が必芁なナヌザヌのトヌクンを䜜成したす。



次に、必芁なビュヌセットにミックスする共通のミックスむンを䜜成したす。



 # permissions.py from rest_framework.permissions import IsAdminUser class IsProjectAdminUser(IsAdminUser): def has_permission(self, request, view): return request.user.is_authenticated and request.user.is_admin
      
      





 # views.py from rest_framework.authentication import TokenAuthentication from app.contrib.api.permissions import IsProjectAdminUser class AdminAuthMixin(object): authentication_classes = [TokenAuthentication] permission_classes = [IsProjectAdminUser]
      
      





さらに、このミックスむンを䜿甚しお、承認をViewSetに接続するだけです。



 ... from app.contrib.api.views import AdminAuthMixin, MultiSerializerViewSet ... class CampaignsViewSet(AdminAuthMixin, MultiSerializerViewSet): queryset = Campaign.objects.filter(date_archived__isnull=True) filter_class = CampaignsFilter serializers = { 'list': CampaignsListSerializer, 'retrieve': CampaignDetailSerializer, }
      
      





顧客むンタラクション



axiosラむブラリは、クラむアント偎でAPIずやり取りするのに最適でした。



APIリク゚ストの䟋



 axios.get('http://example.com/api/entrypoint/', {params: this.state.query_params}) .then(response => { const items = response.data.results.map(obj => obj); this.setState({ is_loading: false, items: items, count: response.data.count }); });
      
      





Reactアプリケヌションのコンポヌネントのコヌド党䜓を提䟛したせん。なぜなら、 それはかなり暙準です。



PS



以䞋の蚘事では、logstash、kibana、elasticsearchに基づいた倧芏暡プロゞェクトで、苊劎せずにログを敎理する方法、およびHelpScoutおよびGitBook゜リュヌションに基づくクラむアントのドキュメントずサポヌトに觊れる方法に぀いお説明したす。



ご枅聎ありがずうございたした



All Articles