Django, celery, gevent 實作 long polling

我們的一個新的網站平台, 有讓使用者上傳圖片的功能。當照片上傳過後, 我們會縮放圖片至適當的比例, 並且貼上一些浮水印之類的東西。如果我是貼上一個單眼拍下來的相片, 使用 PIL 縮圖是挺耗費時間的, 因此為了有更好的 User Experience, 我覺得這些耗費時間變成 offline 來做。然而, 我希望 server 在做完之後, browser 能夠被通知到並且將縮圖抓回來。

一開始, 我打定的主義是使用 node.js 的 socketio, 可惜我完全沒碰過 node.js, 雖然 socketio 在前後台之間的結合很完美, 但有下列問題要先解決:

  1. Ubuntu 12.04 LTS 上的 nginx 版本還沒有支援 websocket。當然也是可以更新版本。
  2. nginx 的後端有兩種 solution, django + node.js。
  3. 我要想辦法讓 django 跟 node.js 可以一起溝通, 因為網站整個都是 django, user upload 完照片後, 整個都是在 django/python 裡頭, task 的資訊要在兩邊共享

基於上述會耗費太多開發時間, 因此我多 survey 了一下, 發現 long polling 基本上就是把 request 卡住, 等到一有結果就直接回傳。用最基本的東西就可以實作。

Celery

Celery 之前就大概用過, 這一次想說直接用 SQS 作為 Message Queue, 文件上有很基本的說明, 但是有一行講得非常模糊

If you specify AWS credentials in the broker URL, then please keep in mind that the secret access key may contain unsafe characters that needs to be URL encoded.

看完沒有 sample 真的很痛苦, 因為 python urllib 的 API 文件好像找不到合用的, 一開始看大概會選擇 urllib.urlencode(query[, doseq]), 但其實這是用在 GET parameter, 會有 key 跟 value, 而 urllib.quote(string[, safe]) 用起來卻不會將 / (slash) 做 encode。搞了半天, 原來要把 safe 變成空的才 ok:

BROKER_URL = 'sqs://%s:%s@' % (AWS_ACCESS_KEY_ID, urllib.quote(AWS_SECRET_ACCESS_KEY, ''))

然而, 實作之後, 發現有不穩定的狀況, 才發現在 Brokers 的頁面上有寫著, SQS 的 status 是 experimental, 因此就火速換掉使用之前用的 rabbitMQ。(不過後來發現不穩定的狀況應該是我自己耍腦殘, 請看下面段落敘述)

gevent & long polling

查了很多 long polling, python 還有 django 的資料, 後來發現可以使用 Django+gevent 來實作。如果不使用 gevent, Django 是不太適合直接拿來做這件事情, 因為如果有多個 request 都在等待, 很快就會把 worker 全部都佔滿 (如果有錯請糾正 >”<), 而使用 gevent, 則可以使用底層的 libevent 來做多工的切換, Gevent Tutorial 的這張圖有很清楚的表達他的運作方式。因此, 如果有多個 user 在做 long polling, 也不致於會卡住。

結合在一起

接在一起的流程很簡單, 步驟如下:

  1. User 上傳檔案到後端的某隻 view。
  2. 這個 view 製作一個 celery task (apply_async), 然後將 task_id 回傳給 browser
  3. Browser 定期帶著這個 task_id 去 check 另一個叫做 poll 的 view
  4. poll 的 view 只做一件事情就是 get celery task result, 並且設定一個 timeout。所以, 只要一 timeout, browser 就會再發一次 request 卡在 poll 裡頭。

當然, 如果一次上傳多個圖片, 就可以使用 gevent 的 group, 一次開多個 greenlet 去等待, 哪一個先回傳就把整個 group 幹掉回給 browser。我寫了一個很簡單的 django-longpolling-example 放在 github, 就只是簡單的示範這個概念。

Debug 很久之自己耍笨

前天下午在睡午覺的時候, 收到 E-mail 說有使用者講無法上傳圖片, 我就跳起來 debug, 用了非長久, 發現會有漏 task 的事情發生。於是乎我 trace code trace 到了 celery、kombu、amqp 等 python library, 才發現 message 是真的都有丟出去, 但是為什麼我的 celery worker 就是會掉 task 呢? 我今天才突然驚覺, 原來我 dev、stage、production 用到了同一個 queue, 所以我在測試機器上傳的圖片, 都委派給其他 server 去做了, 而這邊 code 沒有 sync, 或是檔案也不在那台 instance 上, 所以就無法完成 task。

這告訴了我們, follow tutorial 還滿重要的。在 Using RabbitMQ 的 Setting up RabbitMQ 提到, 要設定 virtual host, 才會把 queue 給隔開。

Deployment

在 django 使用 gevent, 若是使用 gunicorn, 可以直接將 worker 換成 gevent

gunicorn app.wsgi:application --worker-class gevent

如果直接執行的話, 可以參考 django-longpolling-examplerungevent.py