專案完成 - 基於Django3.x版本 - 開發部署小結

程式設計實驗室發表於2022-04-24

前言

最近因為政企部門的工作失誤,導致我們的專案差點掛掉,客戶意見很大,然後我們只能被動進入007加班狀態,忙得嗷嗷叫,直到今天才勉強把專案改完交付,是時候寫一個小結。

技術

因為前期需求不明確,資料量不大,人手也不多,所以我直接用Django做了後端,Django自帶的admin可以作為管理後臺使用,可以很快完成這個需求。

我們的前端有兩個,一個資料展示大屏,一個視覺化地圖。前者使用Vue+ElementUI+DataV實現,後者使用jQuery+百度MapV。

大概的效果如下所示,涉及到資料的部分只能打碼,感謝理解~

這個是Django的admin介面,主頁是我重新寫的

image

資料展示大屏

image

視覺化地圖

image

技術含量其實不高,但專案在具體實施和落地的過程中,有一些問題和細節,還是有必要記錄一下

切換MySQL資料庫

開發的時候預設用的SQLite資料庫,到了正式環境,需要切換到CS架構的伺服器,比如MySQL

我的配置是這樣

# 資料庫配置
database_config = {
    'sqlite3': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    },
    'mysql': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'name',
        'USER': 'root',
        'PASSWORD': 'password',
        'HOST': 'mysql' if DOCKER else 'localhost',
        'PORT': '3306',
    }
}
DATABASES = {'default': database_config['sqlite3']}

這樣方便切換不同的資料庫,其實還可以把資料庫切換用環境變數來控制,docker中使用MySQL,本地開發環境使用sqlite。

MySQL的HOST配置是:'mysql' if DOCKER else 'localhost' 也是為了適配本地環境和docker環境的切換,後續我再封裝一下 DjangoStarter 的資料庫配置部分,實現前面說的環境變數切換配置。

大量資料匯入問題

本專案遇到的第一個麻煩的問題是大量的資料匯入

客戶提供的資料是Excel格式,大概幾百萬條吧,我首先使用Python對資料進行預處理,做了一些去重、資料清洗之類的操作,然後匯出成JSON檔案。

然後在匯入Django資料庫的時候就遇到了問題,DjangoORM的速度太慢了!

讓他跑資料跑了一個晚上,才匯入了80w條資料左右,這肯定不行啊,因為被業務部門捅了婁子,專案還有幾天的時間就要上線了,要趕!總共資料有幾百萬呢……

沒辦法,只能直接上SQL了,掏出Navicat,連上伺服器的MySQL資料庫,然後把資料直接匯入臨時表,再用SQL一番折騰,匯入到Django生成的表裡,這樣資料的問題就搞定了。

(當然後續還有一系列的資料問題,前期的資料清洗還是不夠的,後面發現的一些資料缺失啥的問題,在趕進度的過程中邊處理,做了一些補救措施,最後也還好勉強可以用)

下次資料清洗還是得試一下Pandas這種專業的工具,單靠Python本身不夠。

介面快取

由於資料量太大,有幾個需要計算操作的介面是比較慢的,該優化的暫時都優化了(下面或許會寫一下DjangoORM的優化),所以只能上快取了

專案用了我的「DjangoStarter」專案模板,本身整合了Redis支援,所以快取直接用Redis的就好了。

快取有兩種使用方式,用Django預設的cache_page裝飾器,或者第三方庫rest_framework_extensions

前者作用於 function view 上,當然 class view 也能用,但是得加 method_decorator,然後得自己封裝一個快取過期配置。

後者可以使用 rest_framework 的快取配置,相對來說更方便,只是需要安裝一個庫。(另外提一點,rest_framework_extensions這個庫還有其他的一些功能,有空再介紹,感興趣的同學可以探索一下)

最終我選擇了第二個,哈哈~

不過我都介紹一下吧,很簡單

cache_page 的使用

from django.views.decorators.cache import cache_page
from rest_framework.decorators import api_view

@cache_page(CACHE_TIMEOUT)
@api_view()
def overview(request: Request):
	...

其中 CACHE_TIMEOUT 的單位是秒,也可以設定成 None,這樣快取就永不過期了。

rest_framework_extensions

rest_framework 的快取配置

REST_FRAMEWORK = {
    # 快取過期時間
    'DEFAULT_CACHE_RESPONSE_TIMEOUT': 60 * 60,
}

安裝

pip install drf-extensions

使用

常用的兩種方式,裝飾器和Mixin

裝飾器用法

from rest_framework_extensions.cache.decorators import cache_response

class ViewSet(viewsets.ModelViewSet):
    ...

    @cache_response()
    def list(self, request, *args, **kwargs):
        ...

Mixin用法,注意 CacheResponseMixin 要放在 ModelViewSet 的前面

from rest_framework_extensions.cache.mixins import CacheResponseMixin

class ViewSet(CacheResponseMixin, viewsets.ModelViewSet):
    ...

參考資料

響應資料量太大問題

前面那個視覺化地圖的頁面,需要獲取幾萬條人員資訊,這個介面一開始沒做優化,返回的資料大小有50MB,就很離譜,單純網路傳輸就用了40秒,卡的一批。

前端小夥伴反映這個問題後,我檢視一下伺服器的日誌,發現響應時間就達到了5秒,這忍不了啊。一開始是加了快取,效果顯著,響應時間直接壓縮到了0.1秒!不過沒用,傳輸時間還是很長。

繼續分析,因為是用DjangoStarter的自動程式碼生成功能實現的介面,所以請求之後會預設返回人員資訊的所有欄位,但很明顯,地圖上只需要三個欄位:ID、經緯度。

所以我重新寫了個 serializer

class BasicPersonSerializer(serializers.ModelSerializer):
    class Meta:
        model = BasicPerson
        fields = ['id', 'address_lng', 'address_lat']

然後在 viewsets 裡也重寫了 list 方法,用上新定義的這個 serializer

class BasicPersonViewSet(viewsets.ModelViewSet):
    ...

    @cache_response()
    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = BasicPersonSerializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = BasicPersonSerializer(queryset, many=True)
        return Response(serializer.data)

然後再來測試一下,響應時間69毫秒,資料量變成4MB+,效果很顯著!

整個傳輸時間只需要1.2秒左右~

妙啊,但是我還不滿足,既然還有優化空間,那就繼續優化。

給介面加個gzip壓縮吧~

為圖省事,直接上全站壓縮

MIDDLEWARE = [
    'django.middleware.gzip.GZipMiddleware',
]

搞定

再次請求看看

資料大小被壓縮到480kb,傳輸時間只需要356毫秒!妙啊

這個問題就搞定了。

參考資料:Django使用gzip實現壓縮請求 - https://www.pythonf.cn/read/116970

聚合查詢

既然用Django了,有了這麼方便好用的ORM,就別老想著用什麼SQL語句了。

我在這個專案裡比較常用的是這幾個

  • aggregate
  • annotate
  • values (雖然這個可能不算,但用的很多)
  • Count
  • Sum

每個函式的具體用法我就不復制貼上了,看下面的參考資料吧~

參考資料:

效能優化

Django效能確實有點一言難盡,效能優化也老生常談了,不過就實際運用而已,我們這也還是在探索之中,因為大部分場景是夠用的,沒有多高的併發。

不過有個比較慢的地方是展示大屏的資料介面,因為要彙集好幾個表的資料進行統計,幾十萬上百萬的資料,有點慢,一開始響應時間需要40秒,這也太離譜了。

肯定是得優化的,優化思路從減少資料庫訪問次數、合併運算、增加快取入手,優化完成之後冷啟動速度5秒,命中快取60毫秒內,效果還是可以的。

關於效能優化這塊以後還是得繼續看看,Django有太多可以優化的地方了……

(或者不行的話直接用.Net Core這種高效能的平臺重寫?)

部署

部署方面依然是 uwsgi / docker / docker-compose 這套組合,之前用了好多次了,比較穩定,配置檔案都是現成的,直接把程式碼上傳伺服器 up一下就啟動了,非常方便。

對了還需要配置一下nginx,uwsgi是專有協議,需要做個轉發,才能使用http訪問到。

部署之後關閉debug模式,還需要進入docker容器裡,在bash裡執行 collectstatics 收集靜態檔案。

之後要更新的話,只需要在pycharm裡配置 commit 的時候順便deploy,把修改的程式碼檔案提交到伺服器,然後修改一下 readme.md 檔案(我配置了監聽這個檔案)即可重啟服務。

專案監控

對了,還有一個關鍵的,專案上線之後,需要監控專案的執行狀態

對於Django專案,我用的是sentry來做監控,很好用,整合也很方便,這個sentry我準備後面寫篇文章來介紹。

PS:對於.Net Core專案,我用的是.Net專用的ExceptionLess,這個介面很簡潔直觀,但文件沒有sentry詳細,不過docker搭建還是很方便的。

可以看我之前的這篇文章:[Vue2.x專案整合ExceptionLess監控](

小結

OK,大概就是這樣了,專案也不是到這裡就結束了,只是暫告一個段落,接下來看看客戶那邊有什麼新的需求再來繼續開發。

Django框架陸陸續續也用了兩三年的時間了,雖然應用場景都比較簡單,但屬於是基本摸清了開發流程和定製的上限,像django-admin這種內建的管理後臺,儘管有大量的自定義配置功能,還有simpleUI這種優秀的第三方介面,但他的上限還是擺在那,遇到稍微需要定製化的管理後臺需求,還是得自己搞一套,好在用RestFramework寫介面實在是方便,介面匯出來,套個vue+elementUI的前端,一套後臺就搞定了。

如果還需要資料大屏這類視覺化功能,我們現在也積累了一些這方面的技術和經驗,可以比較快的出產品。

接下來很多新的專案還是優先使用.Net Core技術,從穩定性和效能的方面考慮,我還是更信賴.Net Core。

Django的優勢主要還是在於開發效率和背靠Python社群的強大生態,不過效能和部署方便就要稍遜一點點。

總之,都好用,看場景使用~

相關文章