基於Serverless 重構編程學習小工具

很久之前,我做過一個在線編程的軟件,目前用戶量大概有幾十萬,通過這個APP不僅僅可以進行代碼的編寫、運行,還可以進行編程的學習。

我自己一直對Serverless架構情有獨鍾,恰好趕到這個APP學習板塊被很多人吐槽難用,所以索性就對學習板塊進行了重構,並將這個學習板塊直接搬上Serverless架構。

直接基於Serverless架構重構,主要是出於兩個方面考慮,第一是Serverless架構在很多時候會讓個人開發者的運維工作變得簡單,不用再關心服務器健康、不用關心流量洪峯;二是Serverless架構是按量付費,雖然在一定程度上,Serverless架構會讓所依賴的產品維度變多,但是實際,只要控制、評估得好,成本節約效果是非常顯著的。

整體設計

數據庫設計

之前,數據庫設計是若干個大模塊,現在我們把它們統一到一個模塊中進行項目重構。

數據庫中有四個基本模塊:新聞文章、開發文檔、基礎教程以及圖書資源。其中開發文檔包括大分類、子列表以及正文等內容,表關聯並沒有使用外鍵,而是直接用的id進行表之間的關聯。

說實話,這個數據庫設計的並不是很好,初次構建數據部分時,絕大部分數據都是從其他站點採集而來的,當時爲了快速上線,便直接按照原有格式存儲,所以數據庫中有很多表的字段其實是無效的,或者說針對這個項目是未被使用的。

後端設計

後端會整體部署到一個函數上,整體功能結構如下:

整體功能就是雲函數綁定API網關觸發器,用戶訪問API網關指定的地址,觸發雲函數,然後函數在入口處進行功能拆分,請求不同的方法獲得對應的數據。

需要額外說明的是,我把後端整體接口都部署在一個函數,是因爲這個模塊的使用量並不是特別頻繁,部署到一個函數上也不會出現超過最大實例限制的情況,如果超出限制是可以申請擴容的;其次,所有的接口都是對數據庫進行增刪改查,放入到一個函數中,在一定程度上可以保證容器的活性,降低部分冷啓動帶來的問題,同時容器的複用,也可以在一定程度上降低後臺數據庫鏈接池的壓力;除此之外,所有的接口功能都只需要最少的內存(64M)即可完整運行,不會因爲個別接口的預估內存較大,進而影響整體的成本。

前端設計

前端設計,經過評估,我預計學習資源部分需要有8個頁面,包括科技類新聞、教程、文檔、圖書等相關功能,通過墨刀繪製的原型圖如下:

前端項目開發採用了Vue.js,並將其部署到對象存儲中,通過騰訊雲對象存儲的靜態網站功能對外提供服務。

項目開發

後端函數開發

後端函數開發主要包括三部分:

  • 部分資源的初始化:這部分需要在函數外進行,複用實例的時候不會再次建立連接,防止數據庫連接池出現問題:
def getConnection(dbName):
    conn = pymysql.connect(host="",
                           user="root",
                           password="",
                           port=3306,
                           db=dbName,
                           charset='utf8',
                           cursorclass=pymysql.cursors.DictCursor,
                           )
    conn.autocommit(1)
    return conn


connectionArticle = getConnection("anycodes_article")
  • 數據庫查詢操作:這部分主要就是針對不同接口查詢數據庫,例如獲取文章分類:
def getArticleCategory():
    connectionArticle.ping(reconnect=True)
    cursor = connectionArticle.cursor()
    search_stmt = ('SELECT * FROM `category` ORDER BY `sort`')
    cursor.execute(search_stmt, ())
    data = cursor.fetchall()
    cursor.close()
    result = {}
    for eve_data in data:
        if eve_data['pre_name'] not in result:
            result[eve_data['pre_name']] = []
        result[eve_data['pre_name']].append({
            "id": eve_data["sort"],
            "name": eve_data["name"]
        })
    return result

例如獲取文章列表:

def getArticleList(cid):
    connectionArticle.ping(reconnect=True)
    cursor = connectionArticle.cursor()
    search_stmt = ('SELECT * FROM `article` WHERE `category` = %s ORDER BY `sort`')
    cursor.execute(search_stmt, (cid,))
    data = cursor.fetchall()
    cursor.close()
    result = [{
                "id": eve_data["aid"],
                "title": eve_data["title"]
            } for eve_data in data]
    return result
  • 函數入口:主要是實現功能分發和接口識別:
def main_handler(event, context):
    try:
        result_data = {
            "error": False
        }
        req_type = event["pathParameters"]["type"]
        if req_type == "get_book_list":
            result_data["data"] = getBookList()
        elif req_type == "get_book_info":
            result_data["data"] = getBookContent(event["queryString"]["id"])
        elif req_type == "get_daily_content":
            result_data["data"] = getDailyContent(event["queryString"]["id"])
        elif req_type == "get_daily_list":
            result_data["data"] = getDailyList(event["queryString"]["category"])
        elif req_type == "get_dictionary_result":
            result_data["data"] = getDictionaryResult(event["queryString"]["word"])
        elif req_type == "get_dev_content":
            result_data["data"] = getDevContent(event["queryString"]["id"])
        elif req_type == "get_dev_section":
            result_data["data"] = getDevSection(event["queryString"]["id"])
        elif req_type == "get_dev_chapter":
            result_data["data"] = getDevChapter(event["queryString"]["id"])
        elif req_type == "get_dev_list":
            result_data["data"] = getDevList()
        elif req_type == "get_article_content":
            result_data["data"] = getArticle(event["queryString"]["id"])
        elif req_type == "get_article_list":
            result_data["data"] = getArticleList(event["queryString"]["id"])
        elif req_type == "get_article_category":
            result_data["data"] = getArticleCategory()
        return result_data
    except Exception as e:
        print(e)
        return {"error": True}

函數部分完成之後,可以配置API網關部分:

學習功能模塊幾乎都是對數據庫進行查詢的操作,所以在整個開發過程中,沒有遇到太大的問題,完成得比較順利。

效果預覽

整個項目共包括十幾個頁面,這裏截取了8個主要頁面做效果展示:

整個頁面基本還原了設計稿的樣子,並與原有項目進行了部分整合,無論是列表頁面還是圖書頁面等,數據加載速度表現良好。

通過PostMan進行基本測試:

對接口進行1000次訪問測試:

接口表現良好,並未出現失敗的情況,對該測試結果進行耗時的可視化:

其中最大的時間消耗是219毫秒,最小是27毫秒,平均值是35毫秒,整體的效果還是非常不錯。

項目開發完成,上線之後,前端部分會被放到對象存儲中,後端業務被放到函數計算中,觸發器使用的是API網關,在監控層面,函數計算有着比較不錯的監控緯度:

而函數併發,彈性伸縮等問題都由雲廠商來解決。可以這樣說,自從這個組件部署到了Serverless架構上,我所做的操作就只剩下了當業務代碼有問題,進行簡單修復和簡單維護。

通過按量付費,可以看到我後端服務產生的費用:

由於雲函數沒辦法看到單個資源的費用,所以在計算時選擇了整體花費,一個月的花費要比使用服務器便宜很多。當然,API網關和對象存儲的費用要不能忘記:

項目中的API網關包括了很多服務,不僅僅Anycodes一個服務產生的,但是整體加一起2月份只有1元錢,相對來說也是蠻低的。

總結

通過個人項目中的一個子模塊重構過程,將該項目部署到Serverless架構上:

  • 整個開發過程中是比較輕鬆的,一方面自己不需要在服務器中安裝各類軟件,也不需要搭建web服務,不需要對web服務進行優化,做的只是讀取數據庫,按照一定的格式進行return,而web服務等相關模塊交給API網關來實現,整個後端開發大概耗時一個多小時;前端開發是比較耗時的,因爲我個人不是專業做前端的,所以無論是佈局還是邏輯開發都有點障礙,不過也只用了2天時間。所以,整個模塊從開發到上線只用了2天時間;

  • 項目在部署的時候非常流暢,基於Serverless Framework的開發者工具一鍵部署,後期更新維護,只需要重新部署即可,線上也是無縫切換,不會出現更新服務造成的服務中斷,也不用爲更新服務可能造成服務中斷而做額外的操作,後期更新過程快速且簡單易用;

  • 資源消耗部分是使用按量付費,通過一個月的觀察,整個資源消耗是蠻低的,在整體性能保證的同時,也逐漸降低了成本,對於個人開發者來說,確實是一個福音。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章