Django開發者常犯的7個錯誤

從這篇 7 common mistakes that django developers make 整理出來的

  • Reinventing the wheel
    • 儘量找人家已經寫好的 package 來用,不要沒事自己在那邊重新刻
  • Monolith application structure
    • 要切 app ,該切就切。
    • 我自己目前有比較 confuse 的是,有用到其他 app 的東西時,該怎麼降低耦合度,又能使用方便,這中間去找到一個平衡點。這邊要等以後再來慢慢想怎麼做會更好了。
  • Writing fat representations and thin models
    • View 不要寫太多邏輯
    • 把邏輯抽到 module 或是 model 裡,讓邏輯統一,才能儘量重複使用。
  • Too many queries per view, or unoptimized queries
    • 一個 view 有太多資料庫查詢或是沒有將查詢最佳化,這個會導致速度變慢。
    • 我建議這邊同時可以考慮儘量使用 cache ,來減輕資料庫負擔。
  • Redundant model fields
    • 思考是不是要有真實的資料庫欄位,思考是不是可以使用 property 來做到同樣的事情。
  • Not adding indexes on models
    • 常用來查詢的欄位 (放在 filter 裡的) 記得加 index :db_index=True,該加就加。
  • Inconsistent data validation
    • model 的檢查跟 form 的檢查要一致。

解析 User agent

應該是有可以解析 User Agent 以取得瀏覽器的版本、作業系統等資訊的函式庫吧,然後就找到了 python-user-agents

安裝

pip install ua-parser user-agents

使用很簡單,把 User agent 丟進 parse 去就可以了。

from user_agents import parse

x = parse('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36')

print(str(x))
# PC / Linux / Chrome 78.0.3904

print(x.browser)
# Browser(family='Chrome', version=(78, 0, 3904), version_string='78.0.3904')

print(x.device)
# Device(family='Other', brand=None, model=None)

print(x.os)
# OperatingSystem(family='Linux', version=(), version_string='')

# 有這些方法/屬性可用:'_is_android_tablet', '_is_blackberry_touch_capable_device', 'browser', 'device', 'is_bot', 'is_email_client', 'is_mobile', 'is_pc', 'is_tablet', 'is_touch_capable', 'os', 'ua_string'

從 IP 取得 Country / City / 經緯度

使用的環境是 Ubuntu 16.04 + Django 2.2,先處理 geoip 資料庫。

sudo apt-get install geoipupdate

安裝完以後先填設定,編輯 /etc/GeoIP.conf ,這邊要注意 geoipupdate 版本,2.5 之前的話,AccountID 要改成 UserId,EditionIDs 要改成 ProductIds ,Ubuntu 16.04 用的是 2.5 之前的版本:

# The following AccountID and LicenseKey are required placeholders.
# For geoipupdate versions earlier than 2.5.0, use UserId here instead of AccountID.
# AccountID 0
UserId 0
LicenseKey 000000000000


# Include one or more of the following edition IDs:
# * GeoLite2-City - GeoLite 2 City
# * GeoLite2-Country - GeoLite2 Country
# For geoipupdate versions earlier than 2.5.0, use ProductIds here instead of EditionIDs.
# EditionIDs GeoLite2-City GeoLite2-Country
ProductIds GeoLite2-City GeoLite2-Country

執行 sudo -H geoipupdate,檔案會下載到 /var/lib/GeoIP

接著在 Django 環境下安裝 geoip2

pip install geoip2

然後先在 shell 裡試試看:python manage.py shell

from django.contrib.gis.geoip2 import GeoIP2
g = GeoIP2('/var/lib/GeoIP')  # 將路徑指過去
print(g.country('59.120.21.9'))
print(g.city('59.120.21.9'))
print(g.lat_lon('59.120.21.9'))

一般來說,用 city(“your_ip”) 就可以拿到足夠的資訊了

{'city': 'Taipei',
 'continent_code': 'AS',
 'continent_name': 'Asia',
 'country_code': 'TW',
 'country_name': 'Taiwan',
 'dma_code': None,
 'latitude': 25.0478,
 'longitude': 121.5318,
 'postal_code': None,
 'region': 'TPE',
 'time_zone': 'Asia/Taipei'}

參考資料

Using curl to upload file to Django without csrf_exempt

簡單的說就是利用 cookie 來取得必要的 CSRF TOKEN 資訊。

#!/bin/sh

ENDPOINT=http://localhost:8000/uploads/simple/
COOKIE_JAR=cookies.txt
# 是否要讓 curl 顯示詳細的資訊 (含 header 等)
# VERBOSE=-v
VERBOSE=

# Get
# 用 -c 存到 cookies.txt
curl \
    $VERBOSE \
    -c $COOKIE_JAR \
    $ENDPOINT

# Get CSRF token from cookies.txt
# cat $COOKIE_JAR
CSRFTOKEN=$(awk '/csrftoken/{print $7;}' $COOKIE_JAR)
echo "CSRFTOKEN=$CSRFTOKEN"

# POST 時,在標頭 X-CSRFToken 帶入剛剛找到的 CSRF token
curl \
    $VERBOSE \
    -X POST \
    -b $COOKIE_JAR \
    -H "X-CSRFToken: $CSRFTOKEN" \
    -F myfile=@image003.png \
    $ENDPOINT

jemalloc

jemalloc (github) 是效能聽說很好的 malloc library。

之前是在 為線上環境而最佳化的 Ruby:Fullstaq Ruby 看到的,看到的當下是想說,會不會也有人來編譯用 jemalloc 的 Python 版本,不過很遺憾的是沒有。後來找了 jemalloc 的資料以後,發現要替換其實不難。Ubuntu 14.04 是已經有包 libjemalloc 了,但版本較舊,是 3.5.1 版。

後來還是選擇自行編譯,然後使用 LD_PRELOAD 環境變數來替換 python 的 malloc library。

那要怎麼檢查 python 是不是真的有使用到呢?這可以使用 lsof 來檢查。(How to see the currently loaded shared objects in Linux?)

lsof /usr/local/lib/libjemalloc.so.2

同款的還有 tcmalloc (Google 出的),有空也來測試看看。

P.S. ptmalloc 是 glibc 的 malloc。

簡略的看 Tastypie

跟 DRF 不一樣,Tastypie 以 ModelResource 為主,埋下 Resource 時,就是完整的 LIST/CREATE/UPDATE/DELETE。

Resource 的 model 不一定要是 django model,也可以是自訂的 resource:https://django-tastypie.readthedocs.io/en/latest/non_orm_data_sources.html

  • Authentication 蠻多的,Basic/ApiKey/Session/Digest/OAuth/Multi 都有,OAuth 有內建。
  • Authorization 是指定允許的動作,像是 read_list / read_detail / create_list …. 等等。
  • Serializer 跟 DRF 有點不一樣,這邊僅指輸出的格式,DRF 主要是指輸出哪些欄位。
  • Throttling 可指定一秒內能呼叫的次數
  • 有支援 Paginator 翻頁。
  • 支援 GeoDjango!! 這倒是很方便,GeoDjango 看來是值得花時間來研究的。

缺點是,github 的活躍度不是太高,上次的更新是4個月前 (2018/9)。

跟 DRF 的比較可以參考這篇:https://stackshare.io/stackups/django-rest-framework-vs-tastypie

celery 無法 inspect

環境:

  • Celery 4.2.1
  • Broker backend: RabbitMQ
  • Result backend: Redis

想使用 celery inspect 來查 memory leak 問題,但是 celery inspect 時,都會出現 Error: No nodes replied within time constraint. 的訊息。

察看 log 以後,確定當 celery log 出現 warning,connection reset by peer 時,celery status 就會出現 “Error: No nodes replied within time constraint.” ,在這個同時,rabbitmq server log 也會出現 Missed heartbeats from client, timeout: 20s 的 ERROR ,有看過以下 issues,但沒幫助

後來找到這篇 rabbitmq报Heartbeat missing with heartbeat = 60 seconds ,參考內容把 celery 的 broker_heartbeat 設定改為 0,避免 RabbitMQ 做 heartbeat 的檢查。看來就解決了。

原理的解說可以參考這篇:Detecting Dead TCP Connections with Heartbeats and TCP Keepalives 大致上是說,RabbitMQ 有連線 heartbeat,當 client 沒回應的時候,RabbitMQ 會主動斷掉。而這個 heartbeat 是可以透過 client 在初始化連線時去設定的,所以調整 celery 設定,請 RabbitMQ 不要做 heartbeat 檢查就可以了。

Django jsonfield

Django 有提供 jsonfield,但只能用在 postgresql 上,有另外一個專案 django-mysql 提供了可以用在 MySQL 上的 jsonfield。是的,就目前來說,並沒有一個通用的 jsonfield。惟一能找到的,就這個 django-jsonfielddjango-jsonfield 的網站上也有特別提到這件事情,並且說 django-jsonfield 只繼續維護,不再開發,因此不建議使用這個專案。但是就跨資料庫來說,目前好像也就這個專案可用。

我有想過要依照 DATABASE 設定來區分要使用哪個 jsonfield,但仔細想想,這最大的問題可能會出在 Migration,因為 Migration 裡會直接引用 jsonfield 。migration 裡沒辦法讀取到 django settings ,也就沒辦法做到動態的處理。不過,可能還是要試試看才知道行不行。

s3proxy

網址:https://github.com/gaul/s3proxy
可以在本機端測試 S3,而不需要去開 S3 bucket 跟弄 AWS credential ,s3proxy 提供了跟 S3 一樣的 API 介面。

boto3 可以用,用法參考這邊:https://github.com/gaul/s3proxy/wiki/Client-compatibility-list

# Python3 + boto3 example
session = boto3.session.Session(aws_access_key_id='identity',
                                aws_secret_access_key='credential')
config = boto3.session.Config(s3={'addressing_style': 'path'})
# low level S3 client
client = session.client('s3', endpoint_url='http://localhost:60080',
                        config=config)
# S3 resource object
resource = session.resource('s3', endpoint_url='http://localhost:60080',
                            config=config)

用 docker 快速啟動

mkdir -p /tmp/data
docker run --publish 60080:80 -v /tmp/data:/data --env S3PROXY_AUTHORIZATION=none andrewgaul/s3proxy

django-storage 的話,應該是要改設定裡的 AWS_S3_ENDPOINT_URL

django stronghold

網址:https://github.com/mgrouchy/django-stronghold

這個 package 蠻好玩的,django 預設的 view 都是 public 的,得加上 LoginRequired decorator 或繼承 LoginRequiredMixin 才能限制只有使用者能用,但 stronghold 是反過來,在 middleware 加上 LoginRequiredMiddleware,強制所有的 view 都是 LoginRequired,只有加上 public decorator 或繼承 StrongholdPublicMixin 的才是 public。

對一個都需要驗證後才能使用的系統,這倒是方便許多。