如題,贈送一個 Amazon Prime 3 個月試用激活碼。需要的自取,去年買過 39 的年費(fèi)試用,沒發(fā)疊加了。 Ps:需要之前沒有試用過的賬戶才能使用。
[交通銀行信用卡] 恭喜您獲得中國亞馬遜官網(wǎng) Prime 會員 3 個月免費(fèi)試用資格,5/24 前使用 bcca4MZtPZq2yu9wk4 兌換碼即可免費(fèi)領(lǐng)取權(quán)益,7/31 前續(xù)費(fèi) prime 會員更可獲得 100 元刷卡金獎勵,轉(zhuǎn)發(fā)無效,激活地址: cc.bankcomm.com/amapri
Undeliverable : Returned Your package was damaged in transit and will not be delivered. Please contact Amazon for assistance. amazon.com 訂單顯示這個,要怎么辦呀 rt,我看到的 runner 目前好像只能是拉取某分支最新的一次 commit 進(jìn)行構(gòu)建,或者通過 tag 來標(biāo)記,想知道是否可以指定構(gòu)建某個分支的某一次提交來 run 一個 pipline ? dev 分支上(是最新的)的代碼和我本地分支的代碼明明有不同的地方,但是為什么我執(zhí)行 git merge --no-ff -m 'merge from develop' develop 之后就提醒我'Already up to date.'.但是在 gitlab 里面比較的時候就會出現(xiàn)好多不同的地方,這是什么原因造成的呢? 是用遠(yuǎn)程服務(wù)器還是自建本地服務(wù)器,本地服務(wù)器有什么推薦的嗎?感謝各位大佬。 我們小組搭建了一個工程的基礎(chǔ)項目結(jié)構(gòu)和一些基本配置代碼托管在 gitlab 上, 后續(xù)業(yè)務(wù)團(tuán)隊,都是在這個模版工程的工程上完成業(yè)務(wù)開發(fā) 基礎(chǔ)工程現(xiàn)在有時候需要一些小修改,已經(jīng)有一兩個業(yè)務(wù)團(tuán)隊在用了,怎么才能拉取模版工程的更新,并且不代碼沖突尼。 現(xiàn)在復(fù)制的模版工程的代碼,再傳到對應(yīng)業(yè)務(wù)小組代碼倉庫上,不知道有沒有其他方式? 我本來想通過 fork 來管理的,但是不太好。 配置都是在根目錄下的一些 eslint 等前端代碼風(fēng)格配置,如果用 git 子模塊可以實現(xiàn)嗎? 自己項目上寫的簡答的 ci,job 會執(zhí)行服務(wù)器上的一個腳本,怎么去判斷腳本中的失敗尼,手動的拋出異常后,好像 CI 這邊的執(zhí)行還是算成功的。只有 yml 文件語法異常這種才會失敗。 項目上在用 gitlab runner 做代碼管理,以后 CI/CD 也沒用過,最近幾天了解下面,說下自己的理解不知道有沒有問題。 現(xiàn)有需求: uat 分支有新 commit 后打成 zip 包,提交到一個質(zhì)控的倉庫地址上。 gitlab runner 實現(xiàn): 看了下服務(wù)器上配的 run,選擇的工作方式是 shell,初步了解下來,實現(xiàn)上面的要求大概這么實現(xiàn) 1、服務(wù)器上寫一個腳本,用于對 gitlab runner clone 下來的代碼進(jìn)行打包,對打包后的文件進(jìn)行上傳。 runner 的 yml 文件中 script 應(yīng)該只要配一句執(zhí)行腳本的命令如:node xxx/xxx/index.js 用的功能比較簡單哈,yml 文件暫時寫不了多復(fù)雜的。 不知道我上面的理解對不對? 機(jī)房突然斷電后 gitlab 服務(wù)異常,gitlab 服務(wù)器已經(jīng)運(yùn)行了 2 年多 具體表現(xiàn)為 1、本地 提交 提示輸入用戶名和密碼 2、404 錯誤的步驟是,服務(wù)器端 web 頁面點(diǎn)擊項目--再點(diǎn)具體的 http 鏈接 就報 404 錯誤 gitlab-ctl tail 日志提示 如下: ==> /var/log/gitlab/gitlab-workhorse/current <== 2018-12-15_03:39:45.37943 172.171.3.200 @ - - [2018-12-15 11:39:45.319377255 +0800 CST] "POST /ci/api/v1/builds/register.json HTTP/1.1" 404 27 "" "Go 1.1 package http" 0.059992 2018-12-15_03:39:48.35456 172.171.3.200 @ - - [2018-12-15 11:39:48.319478391 +0800 CST] "POST /ci/api/v1/builds/register.json HTTP/1.1" 404 27 "" "Go 1.1 package http" 0.035015 2018-12-15_03:39:51.34217 172.171.3.200 @ - - [2018-12-15 11:39:51.320915977 +0800 CST] "POST /ci/api/v1/builds/register.json HTTP/1.1" 404 27 "" "Go 1.1 package http" 0.021163 2018-12-15_03:39:54.33908 172.171.3.200 @ - - [2018-12-15 11:39:54.319917074 +0800 CST] "POST /ci/api/v1/builds/register.json HTTP/1.1" 404 27 "" "Go 1.1 package http" 0.019063 2018-12-15_03:39:57.34063 172.171.3.200 @ - - [2018-12-15 11:39:57.319838653 +0800 CST] "POST /ci/api/v1/builds/register.json HTTP/1.1" 404 27 "" "Go 1.1 package http" 0.020724 2018-12-15_03:40:00.34507 172.171.3.200 @ - - [2018-12-15 11:40:00.320364296 +0800 CST] "POST /ci/api/v1/builds/register.json HTTP/1.1" 404 27 "" "Go 1.1 package http" 0.024610 2018-12-15_03:40:03.34200 172.171.3.200 @ - - [2018-12-15 11:40:03.320454932 +0800 CST] "POST /ci/api/v1/builds/register.json HTTP/1.1" 404 27 "" "Go 1.1 package http" 0.021466 2018-12-15_03:40:06.34277 172.171.3.200 @ - - [2018-12-15 11:40:06.320509472 +0800 CST] "POST /ci/api/v1/builds/register.json HTTP/1.1" 404 27 "" "Go 1.1 package http" 0.022184 2018-12-15_03:40:09.34080 172.171.3.200 @ - - [2018-12-15 11:40:09.320631368 +0800 CST] "POST /ci/api/v1/builds/register.json HTTP/1.1" 404 27 "" "Go 1.1 package http" 0.020098 2018-12-15_03:40:12.34852 172.171.3.200 @ - - [2018-12-15 11:40:12.320874867 +0800 CST] "POST /ci/api/v1/builds/register.json HTTP/1.1" 404 27 "" "Go 1.1 package http" 0.027549 3、500 錯誤步驟是,服務(wù)器端 web 頁面點(diǎn)擊項目就報 500 錯誤 錯誤提示如下 ++++++++++++++++++++++++++++++++++++++++++++++++++++ ==> /var/log/gitlab/gitlab-rails/production.log <== Started GET "/admin/projects/SDN-COMPETITION/controller" for 172.171.9.250 at 2018-12-15 11:04:56 +0800 Processing by Admin::ProjectsController#show as HTML Parameters: {"namespace_id"=>"SDN-COMPETITION", "id"=>"controller"} Completed 500 Internal Server Error in 277ms (ActiveRecord: 21.2ms) ActionView::Template::Error (Failed to inflate loose object.): 66: 67: = render 'shared/outdated_browser' 68: 69: - if @project && [email?protected] _repo? 70: - if ref = @ref || @project.repository.root_ref 71: :javascript 72: var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, ref)}"; app/models/repository.rb:768:in local_branches' app/models/repository.rb:541:in branch_count' app/models/repository.rb:48:in block (2 levels) in cache_method' lib/repository_cache.rb:20:in fetch' app/models/repository.rb:1152:in cache_method_output' app/models/repository.rb:48:in block in cache_method' app/models/repository.rb:88:in has_visible_content?' app/models/repository.rb:1055:in empty_repo?' app/models/project.rb:850:in empty_repo?' app/views/layouts/header/_default.html.haml:69:in _app_views_layouts_header__default_html_haml__1019618398472655816_70250847488020' app/views/layouts/application.html.haml:10:in _app_views_layouts_application_html_haml__2481968966533998274_70250858996420' app/views/layouts/admin.html.haml:5:in _app_views_layouts_admin_html_haml__2532952863642744480_70250894889820' lib/gitlab/middleware/multipart.rb:93:in call' lib/gitlab/request_profiler/middleware.rb:15:in call' lib/gitlab/middleware/go.rb:16:in `call' 請問應(yīng)該怎么排查,查找 goole 和官方 以及 stackoverflow 都沒有頭緒 .gitlab-ci.yml stages: - deploy deploy_staging: tags: - server1 - server2 stage: deploy script: - echo "Hello World" only: - master server1 和 server2 部署了 runner 并且連接到 gitlab 了,但是如何在 deploy 階段同時部署到多臺機(jī)器。 當(dāng) tags 字段只有一個的時候,是沒有任何問題的,但是配置了多個以后,push 后會在 deploy 階段會阻塞,很奇怪,望指導(dǎo)。 目前公司項目開發(fā)基于 GitLab Flow 流程,既 feature->master->pre-production->production,其中 master、pre-production 與 production 均 protected。 由于經(jīng)常多分支并行開發(fā),所以 master 上一般都會包含多分支的代碼。由 master->pre-production 時,研發(fā)主管需要 cherry-pick。目前遇到的問題是: 每次 cherry-pick 需要選擇的 commit 數(shù)目很多,容易出現(xiàn)遺漏或者多選; 公司項目多,導(dǎo)致研發(fā)主管需要每個項目自行 cherry-pick,沒有找到可由員工自行 cherry-pick 后發(fā)起 Merge Requests 的方法。 所以在此咨詢下大家的意見。 項目一般都是建立在組下面的,如果一個人在組里是 master,我在項目的 members 選項里再添加他為 reporter,那這個人在這個項目貌似還是 master 權(quán)限
也就是說,我現(xiàn)在想在一個組內(nèi),針對不同項目分配不同的權(quán)限,是不是沒辦法做到呢?如果把組里面權(quán)限改了,整個組下面的所有項目權(quán)限也就都改了 最近在搞 gerrit,git 倉庫用 gitlab,現(xiàn)在配置好 replication 之后代碼的同步都沒有問題,但唯獨(dú)新建的項目無法同步步驟是這樣:gerrit 中 create project,之后 replication 日志中就會報錯 [2018-03-13 11:16:20,169] [d685449c] Created remote repository: [email?protected] :xxxxx/xxxx.git[2018-03-13 11:16:20,169] [d685449c] Missing repository created; retry replication to [email?protected] :xxxxx/xxxx.gitreplication.config 配置如下: [remote "xxxxx"] url = [email?protected] :${name}.gitpush = +refs/heads/*:refs/heads/* push = +refs/tags/*:refs/tags/* push = +refs/changes/*:refs/changes/* timtout = 30 threads = 3 createMissingRepositories = true replicateProjectDeletions = true mirror = true 不知是不是 gerrit 還是 gitlab 配置的不對,有木有大神可以幫忙解決? --------- 補(bǔ)充:在 error.log 中有這樣的 warning WARN org.apache.sshd.common.keyprovider.FileKeyPairProvider : Failed (StreamCorruptedException) to load key resource=/home/appweb/chuixue/review_site/etc/ssh_host_ecdsa_key: Invalid DER: object is not an OID: SEQUENCE 求教樂高 土星 5 號 (21309)的購買時機(jī) 最近才開始關(guān)注樂高,21309 是比較經(jīng)典的款,沒有抓住去年年底的促銷,去年年底的時候最低到過 700 元左右,而現(xiàn)在基本上已經(jīng) 1200 以上。我在官網(wǎng)上看到它已經(jīng)是"Retired"狀態(tài)(停產(chǎn)?),不知道是不是因為這個原因。 我現(xiàn)在打算是等 618 看一波是否有降價,如果沒有降價說明確實是因為停產(chǎn)造成的,無論如何也得按這個價格買了。不知道有沒有對樂高有研究的 V 友,給個建議? 非常感謝 如圖, 準(zhǔn)備買樂高新出的遙控車,有沒有老哥有便宜的路子,求介紹 ,就是 1999 款的那個 給小孩買了大粒小粒的樂高都買了不少,幾千塊錢出去了,但是除了按照固定的教程拼接下,一般一次就不會再拼第二次了。怎么樣才能培養(yǎng)有能力創(chuàng)造出自己的一些東西呢。 有沒有一些基礎(chǔ)的教程,比如基本構(gòu)造搭建之類的學(xué)學(xué),而不是盲目亂搭(亂搭興趣很快就沒了) 我自己搭的話也很盲目 入手了 布加迪 ,按照說明書,昨晚把第一袋的零件拼完,發(fā)現(xiàn)還剩了 4 個零件,感覺有點(diǎn)害怕啊,是不是拼錯了?! https://xguox.me/lego-42077-open-box.html 拼完以后, 美滋滋的想到拿手機(jī)拍個照, 得瑟, 才猛然想起, 手機(jī)已經(jīng)在洗衣機(jī)被洗了一個小時了 (つд?) 大早上拿去 Apple Store, 說泡水的只換不修, 最后, 只好乖乖掏了 ¥ 2800 換了一臺新的 7p 128 ( TДT)
之前愚人節(jié) lego 放出了新版千年隼的照片,大家都以為是愚人節(jié)玩笑,這次 10 月?lián)f要推出史上最大 7541 塊 (泰姬陵 5900 塊)千年隼。按 10 美分一塊算加版權(quán)費(fèi)要到 800 美金了。 只有泄露圖,沒有官方圖片。 第一套 lego ,看了好久,終于決定買了 購買 比較了美亞,中亞,淘寶旗艦店之后,決定中亞購買, 130RMB 。 Amazon 的紙殼包裝很好的, lego 的盒子很新,沒有一點(diǎn)擠壓和變形。 圖 拼裝前 中 最后 這款是有能兩種玩法的,到家已很晚,大概 1 個小時搞定第一種就睡覺了 零件是有多余的,注意下履帶式多了兩個的,不要都裝上~ 包裝我以為會有更加優(yōu)雅的打開方式,看來并沒有??坶_,有膠粘連,輕輕扯開,只能這樣了 我只玩過 Super Heros ,但是看宣傳片,感覺差別似乎不大的樣子,所以,想知道有沒有兩個都玩過的人,請教一下新出的 Marvel's Avengers 有什么不同的地方,新作是不是值得買。 已買過60004,但主要是我們搭好了他再玩?,F(xiàn)在想買些基礎(chǔ)套件讓他自己瞎搭著玩,現(xiàn)在看上10682,有沒有其他的推薦?書有必要買嗎,如果有必要的話,順便也推薦下?。≈x謝 還沒看過的,抓緊看,說不定就莫明消失了 B 站視頻 讓人細(xì)思極恐的 PPT 車?。?! Serverless 通常翻譯為 “無服務(wù)架構(gòu)”,是一種軟件系統(tǒng)設(shè)計架構(gòu)思想和方法,并不是一個開發(fā)框架或者工具。他的出現(xiàn)是為了讓開發(fā)者更加關(guān)注業(yè)務(wù)的開發(fā),而將繁雜的運(yùn)維和部署交給云廠商。Serverless 由 Faas 和 Baas 組成,Faas 為開發(fā)者提供業(yè)務(wù)運(yùn)算環(huán)境,然后與 Baas 提供的數(shù)據(jù)和存儲服務(wù),進(jìn)行交互,從而提供與傳統(tǒng)服務(wù)一致的體驗。但是由于 Faas 是無狀態(tài)的,并且其運(yùn)行環(huán)境是有讀寫限制的,最重要的是它是基于事件觸發(fā)的。因此如果傳統(tǒng) Web 服務(wù)想遷移到 Serverless 上,是需要進(jìn)行相關(guān)改造和特殊處理的,為此遷移成本是必不可少的。本文將具體幫助大家剖析下,如何 Serverless 化傳統(tǒng)的 Web 服務(wù)。 讀完本文將了解到: 傳統(tǒng) Web 服務(wù)特點(diǎn) Serverless 適用場景 Web 框架如何遷移到 Serverless 使用 Serverless Components 快速部署 Web 框架 傳統(tǒng) Web 服務(wù)特點(diǎn) Web 服務(wù)定義: Web 服務(wù)是一種 面向服務(wù)的架構(gòu) (SOA) 的技術(shù),通過標(biāo)準(zhǔn)的 Web 協(xié)議提供服務(wù),目的是保證不同平臺的應(yīng)用服務(wù)可以互操作。 日常生活中,接觸最多的就是基于 HTTP 協(xié)議的服務(wù),客戶端發(fā)起請求,服務(wù)端接受請求,進(jìn)行計算處理,然后返回響應(yīng),簡單示意圖如下: 傳統(tǒng) Web 服務(wù)部署流程:通常需要將項目代碼部署到服務(wù)器上,啟動服務(wù)進(jìn)程,監(jiān)聽服務(wù)器的相關(guān)端口,然后等待客戶端請求,從而響應(yīng)返回處理結(jié)果。而這個服務(wù)進(jìn)程是常駐的,就算沒有客戶端請求,也會占用相應(yīng)服務(wù)器資源。 一般我們的服務(wù)是由高流量和低流量場景交替組成的,但是為了考慮高流量場景,我們需要提供較高的服務(wù)器配置和多臺服務(wù)進(jìn)行負(fù)載均衡。這就導(dǎo)致服務(wù)處在低流量場景時,會多出很多額外的閑置資源,但是購買的資源卻需要按照高流量場景進(jìn)行付費(fèi),這是非常不劃算的。 如果我們的服務(wù)能在高流量場景自動擴(kuò)容,低流量場景自動縮容,并且只在進(jìn)行計算處理響應(yīng)時,才進(jìn)行收費(fèi),而空閑時間不占用任何資源,就不需要收費(fèi)呢? 答案就是 Serverless 。 Serverless 適用場景 上面已經(jīng)提到了 Serverless 的兩個核心特點(diǎn): 按需使用和收費(fèi) 和 自動擴(kuò)縮容 。而且近幾年 Serverless 的應(yīng)用也越來越廣泛,但是它并不是銀彈,任何技術(shù)都是有它的適合場景和不適合場景。我們不能因為一項技術(shù)的火熱,而盲目的追捧。Serverless 是有它的局限性的,一般 Serverless 適合如下幾種場景: 異步的并發(fā),組件可獨(dú)立部署和擴(kuò)展 應(yīng)對突發(fā)或服務(wù)使用量不可預(yù)測 無狀態(tài),計算耗時較短服務(wù) 請求延時不敏感服務(wù) 需要快速開發(fā)迭代的業(yè)務(wù) 如果你的服務(wù)不滿足以上條件,筆者是不推薦遷移到 Serverless 。 Web 框架如何遷移到 Serverless 如果你的服務(wù)是以上提到的任何話一個場景,那么就可以嘗試遷移到 Serverless 上。 常見的 Serverless HTTP 服務(wù)結(jié)構(gòu)圖如下: 那么我們?nèi)绾螌?Web 服務(wù)進(jìn)行遷移呢? 我們知道 Faas (云函數(shù))是基于事件觸發(fā)的,也就是云函數(shù)被觸發(fā)運(yùn)行時,接收到的是一個 JSON 結(jié)構(gòu)體 ,它跟傳統(tǒng) Web 請求時有區(qū)別的,這就是為什么需要額外的改造工作。而改造的工作就是圍繞 如何將事件 JSON 結(jié)構(gòu)體轉(zhuǎn)化成標(biāo)準(zhǔn)的 Web 請求 。 所以 Serverless 化 Web 服務(wù)的核心就是需要開發(fā)一個 適配層 ,來幫我們將觸發(fā)事件轉(zhuǎn)化為標(biāo)準(zhǔn)的 Web 請求。 整個處理流程圖如下: 接下來將介紹如何為 Express 框架開發(fā)一個適配層。 Serverless Express 適配層開發(fā) 實現(xiàn)原理 首先我們先來看看一個標(biāo)準(zhǔn)的云函數(shù)結(jié)構(gòu): module.exports.handler = (event, context) => { // do some culculation return res; }; 在介紹如何開發(fā)一個 Express 的適配層前,我們先來熟悉下 Express 框架。 一個簡單的 Node.js Web 服務(wù)如下: const http = require("http"); const server = http.createServer(function (req, res) { res.end("helloword"); }); server.listen(3000); Express 就是基于 Node.js 的 Web 框架,而 Express 核心就是 通過中間件的方式,生成一個回調(diào)函數(shù) ,然后提供給 http.createServer() 方法使用。 Express 核心架構(gòu)圖如下: 由此可知,我們可以將 Express 框架生成的回調(diào)函數(shù),作為 http.createServer() 的參數(shù),來創(chuàng)建可控的 HTTP Server,然后將云函數(shù)的 event 對象轉(zhuǎn)化成一個 request 對象,通過 http.request() 方法發(fā)起 HTTP 請求,獲取請求響應(yīng),返回給用戶,就可以實現(xiàn)我們想要的結(jié)果。 Node.js Server 的監(jiān)聽方式選擇 對于 Node.js 的 HTTP Server,可以通過調(diào)用 server.listen() 方法來啟動服務(wù), listen() 方法支持多種參數(shù)類型,主要有兩種監(jiān)聽方式 從一個 TCP 端口啟動監(jiān)聽 和 從一個 UNIX Socket 套接字啟動監(jiān)聽 。 server.listen(port[, hostname][, backlog][, callback]) :從一個 TCP 端口啟動監(jiān)聽 server.listen(path, [callback]) :從一個 UNIX Domain Socket 啟動監(jiān)聽 服務(wù)器創(chuàng)建后,我們可以像下面這樣啟動服務(wù)器: // 從'127.0.0.1'和 3000 端口開始接收連接 server.listen(3000, '127.0.0.1', () => {}); // 從 UNIX 套接字所在路徑 path 上監(jiān)聽連接 server.listen('path/to/socket', () => {}) 無論是 TCP Socket 還是 Unix Domain Socket ,每個 Socket 都是唯一的。 TCP Socket 通過 IP 和端口 描述,而 Unix Domain Socket 通過 文件路徑 描述。 TCP 屬于傳輸層的協(xié)議,使用 TCP Socket 進(jìn)行通訊時,需要經(jīng)過傳輸層 TCP/IP 協(xié)議的解析。 而 Unix Domain Socket 可用于不同進(jìn)程間的通訊和傳遞,使用 Unix Domain Socket 進(jìn)行通訊時不需要經(jīng)過傳輸層,也不需要使用 TCP/IP 協(xié)議 。所以,理論上講 Unix Domain Socket 具有更好的傳輸效率。 因此這里在設(shè)計啟動服務(wù)時,采用了 Unix Domain Socket 方式,以便減少函數(shù)執(zhí)行時間,節(jié)約成本。 關(guān)于 Node.js 如何實現(xiàn) IPC 通信,這里就不詳細(xì)介紹的,感興趣的小伙伴可以深入研究下,這里有個簡單的示例, nodejs-ipc 代碼實現(xiàn) 原理大概介紹清楚了,我們的核心實現(xiàn)代碼需要以下三步: 通過 Node.js HTTP Server 監(jiān)聽 Unix Domain Socket,啟動服務(wù) function createServer(requestListener, serverListenCallback) { const server = http.createServer(requestListener); server._socketPathSuffix = getRandomString(); server.on("listening", () => { server._isListening = true; if (serverListenCallback) serverListenCallback(); }); server .on("close", () => { server._isListening = false; }) .on("error", (error) => { // ... }); server.listen(`/tmp/server-${server._socketPathSuffix}.sock`) return server; } 將 Serverless Event 對象轉(zhuǎn)化為 Http 請求 function forwardRequestToNodeServer(server, event, context, resolver) { try { const requestOptions = mapApiGatewayEventToHttpRequest( event, context, getSocketPath(server._socketPathSuffix), ); // make http request to node server const req = http.request(requestOptions, (response) => forwardResponseToApiGateway(server, response, resolver), ); if (event.body) { const body = getEventBody(event); req.write(body); } req .on('error', (error) => // ... ) .end(); } catch (error) { // ... return server; } } 將 HTTP 響應(yīng)轉(zhuǎn)化為 API 網(wǎng)關(guān)標(biāo)準(zhǔn)數(shù)據(jù)結(jié)構(gòu) function forwardResponseToApiGateway(server, response, resolver) { response .on("data", (chunk) => buf.push(chunk)) .on("end", () => { // ... resolver.succeed({ statusCode, body, headers, isBase64Encoded, }); }); } 最后函數(shù)的 handler 將異步請求返回就可以了。 借助 tencent-serverless-http 庫實現(xiàn) 如果不想手寫這些適配層代碼,可以直接使用 tencent-serverless-http 模塊。 它使用起來很簡單,創(chuàng)建我們的 Express 應(yīng)用入口文件 sls.js : const express = require("express"); const app = express(); // Routes app.get(`/`, (req, res) => { res.send({ msg: `Hello Express`, }); }); module.exports = app; 然后創(chuàng)建云函數(shù) sl_handler.js 文件: const { createServer, proxy } = require("tencent-serverless-http"); const app = require("./app"); exports.handler = async (event, context) => { const server = createServer(app); const result = await proxy(server, event, context, "PROMISE").promise; }; 接下來,將業(yè)務(wù)代碼和依賴模塊一起打包部署到云函數(shù)就可以了(記得指定 執(zhí)行方法 為 sl_handler.handler )。 其他 Node.js 框架 除了 Express 框架,其他的 Node.js 框架也基本類似,只需要按照要求, exports 一個 HTTP Server 的回調(diào)函數(shù)就可以。 比如 Koa ,我們拿到初始化的 Koa 應(yīng)用后,只需要將 app.callback() 作為 createServer() 方法的參數(shù)就可以了,如下: const { createServer, proxy } = require("tencent-serverless-http"); const app = require("./app"); exports.handler = async (event, context) => { // 這里和 Express 略有區(qū)別 const server = createServer(app.callback()); const result = await proxy(server, event, context, "PROMISE").promise; }; 其他語言框架 對于非 Node.js 框架,比如 Python 的 Flask 框架,原理都是一樣的,核心只需要做到 將 Serverless Event 對象轉(zhuǎn)化為 Http 請求 ,就可以了。由于筆者對其他語言不太熟悉,這里就不做深入介紹了,感興趣的小伙伴,可以到 Github 社區(qū)搜索下,已經(jīng)有很多對應(yīng)的解決方案了,或者自己嘗試手?jǐn)]也是可以的。 使用 Serverless Components 快速部署 Web 框架 讀到這里,相信你已經(jīng)清楚,如何將自己的 Node.js 框架遷移到 Serverless 了。但是在這之前,我們都是手動處理的,而且每次都需要自己創(chuàng)建 handler.js 文件,還是不夠方便。 為此開源社區(qū)提供了一套優(yōu)秀的解決方案 Serverless Component ,通過組件,我們進(jìn)行簡單的 yaml 文件配置后,就可以方便的將我們的框架代碼部署到云端。 比如上面提到的 Express 框架,就有對應(yīng)的組件,我們只需要在項目根目錄下創(chuàng)建 serverless.yml 配置文件: component: express name: expressDemo inputs: src: ./ region: ap-guangzhou runtime: Nodejs10.15 apigatewayConf: protocols: - https environment: release 然后全局安裝 serverless 命令 npm install serverless -g 之后,執(zhí)行部署命令即可: $ serverless deploy 耐心等待幾秒,我們的 Express 應(yīng)用就成功部署到云端了。更多詳細(xì)信息,請參考 Express 官方文檔 注意:本文 Serverless 服務(wù)均基于 騰訊云 部署。 Serverless Express 組件不僅能幫我們快速部署 Express 應(yīng)用,而且它還提供了 實時日志 和 云端調(diào)試 的能力。 只需要在項目目錄下執(zhí)行 serverless dev 命令,serverless 命令行工具就會自動監(jiān)聽項目業(yè)務(wù)代碼的更改,并且實時部署,同時我們可以通過打開 Chrome Devtools 來調(diào)試 Express 應(yīng)用。 關(guān)于云端調(diào)試, 騰訊云 Serverless Framework 正式發(fā)布公告 中有詳細(xì)的介紹,并且有視頻演示。 而且除了 Express 組件,還支持:Koa.js ,Egg.js ,Next.js ,Nuxt.js..... 發(fā)現(xiàn)更多組件 最后 當(dāng)然 Serverless 化 Web 服務(wù)并沒有本文介紹的那么簡單,比如文件讀寫,服務(wù)日志存儲, Cookie/Session 存儲等......實際開發(fā)中,我們還會面臨各種未知的坑,但是比起困難,Serverless 帶給我們的收益是值得去嘗試的。當(dāng)然傳統(tǒng) Web 服務(wù)真的適合遷移到 Serverless 架構(gòu)上,也是值得我們?nèi)ニ伎嫉膯栴},畢竟現(xiàn)有的 Web 框架都是面向傳統(tǒng) Web 服務(wù)開發(fā)實現(xiàn)的 (推薦閱讀 利與弊-傳統(tǒng)框架要不要部署在 Serverless 架構(gòu)上 )。但是筆者相信,很快就會出現(xiàn)一個專門為 Serverless 而生的 Web 框架,可以幫助我們更好地基于 Serverless 開發(fā)應(yīng)用 ~ 歡迎訪問: Serverless 中文社區(qū) 我怎么感覺就是 SAE (新浪云)類似產(chǎn)品的一個換皮重來?相比于更早的“虛擬主機(jī)”,也就是多了個資源彈性使用的特性。 或者說,這類 Serverless 產(chǎn)品解決了哪些 SAE 無法實現(xiàn)的痛點(diǎn)呢? 之前做了 這個 ,在 lambda 上跑的 headless chrome 。 這次看上了騰訊家的云函數(shù),主要是國內(nèi)的廠有國內(nèi)的節(jié)點(diǎn),可以做一些事情。 直接跑 lambda 的版本是跑不起來的,只能自己重新編譯了個 chromium,終于可以用了,項目在 這兒 ,有這需求的可以來看看,點(diǎn)個星星什么的 傳統(tǒng)業(yè)務(wù)實現(xiàn) Websocket 并不難,然而函數(shù)計算基本上都是事件驅(qū)動,不支持長鏈接操作。如果將函數(shù)計算與 API 網(wǎng)關(guān)結(jié)合,是否可以有 Websocket 的實現(xiàn)方案呢? API 網(wǎng)關(guān)觸發(fā)器實現(xiàn) Websocket WebSocket 協(xié)議是基于 TCP 的一種新的網(wǎng)絡(luò)協(xié)議。它實現(xiàn)了瀏覽器與服務(wù)器全雙工 (full-duplex) 通信,即允許服務(wù)器主動發(fā)送信息給客戶端。WebSocket 在服務(wù)端有數(shù)據(jù)推送需求時,可以主動發(fā)送數(shù)據(jù)至客戶端。而原有 HTTP 協(xié)議的服務(wù)端對于需推送的數(shù)據(jù),僅能通過輪詢或 long poll 的方式來讓客戶端獲得。 由于云函數(shù)是無狀態(tài)且以觸發(fā)式運(yùn)行,即在有事件到來時才會被觸發(fā)。因此,為了實現(xiàn) WebSocket,云函數(shù) SCF 與 API 網(wǎng)關(guān)相結(jié)合,通過 API 網(wǎng)關(guān)承接及保持與客戶端的連接。您可以認(rèn)為云函數(shù)與 API 網(wǎng)關(guān)一起實現(xiàn)了服務(wù)端。當(dāng)客戶端有消息發(fā)出時,會先傳遞給 API 網(wǎng)關(guān),再由 API 網(wǎng)關(guān)觸發(fā)云函數(shù)執(zhí)行。當(dāng)服務(wù)端云函數(shù)要向客戶端發(fā)送消息時,會先由云函數(shù)將消息 POST 到 API 網(wǎng)關(guān)的反向推送鏈接,再由 API 網(wǎng)關(guān)向客戶端完成消息的推送。 具體的實現(xiàn)架構(gòu)如下: 對于 WebSocket 的整個生命周期,主要由以下幾個事件組成: 連接建立:客戶端向服務(wù)端請求建立連接并完成連接建立; 數(shù)據(jù)上行:客戶端通過已經(jīng)建立的連接向服務(wù)端發(fā)送數(shù)據(jù); 數(shù)據(jù)下行:服務(wù)端通過已經(jīng)建立的連接向客戶端發(fā)送數(shù)據(jù); 客戶端斷開:客戶端要求斷開已經(jīng)建立的連接; 服務(wù)端斷開:服務(wù)端要求斷開已經(jīng)建立的連接。 對于 WebSocket 整個生命周期的事件,云函數(shù)和 API 網(wǎng)關(guān)的處理過程如下: 連接建立:客戶端與 API 網(wǎng)關(guān)建立 WebSocket 連接,API 網(wǎng)關(guān)將連接建立事件發(fā)送給 SCF ; 數(shù)據(jù)上行:客戶端通過 WebSocket 發(fā)送數(shù)據(jù),API 網(wǎng)關(guān)將數(shù)據(jù)轉(zhuǎn)發(fā)送給 SCF ; 數(shù)據(jù)下行:SCF 通過向 API 網(wǎng)關(guān)指定的推送地址發(fā)送請求,API 網(wǎng)關(guān)收到后會將數(shù)據(jù)通過 WebSocket 發(fā)送給客戶端; 客戶端斷開:客戶端請求斷開連接,API 網(wǎng)關(guān)將連接斷開事件發(fā)送給 SCF ; 服務(wù)端斷開:SCF 通過向 API 網(wǎng)關(guān)指定的推送地址發(fā)送斷開請求,API 網(wǎng)關(guān)收到后斷開 WebSocket 連接。 因此,云函數(shù)與 API 網(wǎng)關(guān)之間的交互,需要由 3 類云函數(shù)來承載: 注冊函數(shù):在客戶端發(fā)起和 API 網(wǎng)關(guān)之間建立 WebSocket 連接時觸發(fā)該函數(shù),通知 SCF WebSocket 連接的 secConnectionID 。通常會在該函數(shù)記錄 secConnectionID 到持久存儲中,用于后續(xù)數(shù)據(jù)的反向推送; 清理函數(shù):在客戶端主動發(fā)起 WebSocket 連接中斷請求時觸發(fā)該函數(shù),通知 SCF 準(zhǔn)備斷開連接的 secConnectionID 。通常會在該函數(shù)清理持久存儲中記錄的該 secConnectionID ; 傳輸函數(shù):在客戶端通過 WebSocket 連接發(fā)送數(shù)據(jù)時觸發(fā)該函數(shù),告知 SCF 連接的 secConnectionID 以及發(fā)送的數(shù)據(jù)。通常會在該函數(shù)處理業(yè)務(wù)數(shù)據(jù)。例如,是否將數(shù)據(jù)推送給持久存儲中的其他 secConnectionID 。 Websocket 功能實現(xiàn) 根據(jù)騰訊云官網(wǎng)提供的該功能的整體架構(gòu)圖: 這里我們可以使用對象存儲 COS 作為持久化的方案,當(dāng)用戶建立鏈接存儲 ConnectionId 到 COS 中,當(dāng)用戶斷開連接刪除該鏈接 ID 。 其中注冊函數(shù): # -*- coding: utf8 -*- import os from qcloud_cos_v5 import CosConfig from qcloud_cos_v5 import CosS3Client bucket = os.environ.get('bucket') region = os.environ.get('region') secret_id = os.environ.get('secret_id') secret_key = os.environ.get('secret_key') cosClient = CosS3Client(CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key)) def main_handler(event, context): print("event is %s" % event) connectionID = event['websocket']['secConnectionID'] retmsg = {} retmsg['errNo'] = 0 retmsg['errMsg'] = "ok" retmsg['websocket'] = { "action": "connecting", "secConnectionID": connectionID } cosClient.put_object( Bucket=bucket, Body='websocket'.encode("utf-8"), Key=str(connectionID), EnableMD5=False ) return retmsg 傳輸函數(shù): # -*- coding: utf8 -*- import os import json import requests from qcloud_cos_v5 import CosConfig from qcloud_cos_v5 import CosS3Client bucket = os.environ.get('bucket') region = os.environ.get('region') secret_id = os.environ.get('secret_id') secret_key = os.environ.get('secret_key') cosClient = CosS3Client(CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key)) sendbackHost = os.environ.get("url") def Get_ConnectionID_List(): response = cosClient.list_objects( Bucket=bucket, ) return [eve['Key'] for eve in response['Contents']] def send(connectionID, data): retmsg = {} retmsg['websocket'] = {} retmsg['websocket']['action'] = "data send" retmsg['websocket']['secConnectionID'] = connectionID retmsg['websocket']['dataType'] = 'text' retmsg['websocket']['data'] = data requests.post(sendbackHost, json=retmsg) def main_handler(event, context): print("event is %s" % event) connectionID_List = Get_ConnectionID_List() connectionID = event['websocket']['secConnectionID'] count = len(connectionID_List) data = event['websocket']['data'] + "(===Online people:" + str(count) + "===)" for ID in connectionID_List: if ID != connectionID: send(ID, data) return "send success" 清理函數(shù): # -*- coding: utf8 -*- import os import requests from qcloud_cos_v5 import CosConfig from qcloud_cos_v5 import CosS3Client bucket = os.environ.get('bucket') region = os.environ.get('region') secret_id = os.environ.get('secret_id') secret_key = os.environ.get('secret_key') cosClient = CosS3Client(CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key)) sendbackHost = os.environ.get("url") def main_handler(event, context): print("event is %s" % event) connectionID = event['websocket']['secConnectionID'] retmsg = {} retmsg['websocket'] = {} retmsg['websocket']['action'] = "closing" retmsg['websocket']['secConnectionID'] = connectionID requests.post(sendbackHost, json=retmsg) cosClient.delete_object( Bucket=bucket, Key=str(connectionID), ) return event Yaml 文件如下: Conf: component: "serverless-global" inputs: region: ap-guangzhou bucket: chat-cos-1256773370 secret_id: secret_key: myBucket: component: '@serverless/tencent-cos' inputs: bucket: ${Conf.bucket} region: ${Conf.region} restApi: component: '@serverless/tencent-apigateway' inputs: region: ${Conf.region} protocols: - http - https serviceName: ChatDemo environment: release endpoints: - path: / method: GET protocol: WEBSOCKET serviceTimeout: 800 function: transportFunctionName: ChatTrans registerFunctionName: ChatReg cleanupFunctionName: ChatClean ChatReg: component: "@serverless/tencent-scf" inputs: name: ChatReg codeUri: ./code handler: reg.main_handler runtime: Python3.6 region: ${Conf.region} environment: variables: region: ${Conf.region} bucket: ${Conf.bucket} secret_id: ${Conf.secret_id} secret_key: ${Conf.secret_key} url: http://set-gwm9thyc.cb-guangzhou.apigateway.tencentyun.com/api-etj7lhtw ChatTrans: component: "@serverless/tencent-scf" inputs: name: ChatTrans codeUri: ./code handler: trans.main_handler runtime: Python3.6 region: ${Conf.region} environment: variables: region: ${Conf.region} bucket: ${Conf.bucket} secret_id: ${Conf.secret_id} secret_key: ${Conf.secret_key} url: http://set-gwm9thyc.cb-guangzhou.apigateway.tencentyun.com/api-etj7lhtw ChatClean: component: "@serverless/tencent-scf" inputs: name: ChatClean codeUri: ./code handler: clean.main_handler runtime: Python3.6 region: ${Conf.region} environment: variables: region: ${Conf.region} bucket: ${Conf.bucket} secret_id: ${Conf.secret_id} secret_key: ${Conf.secret_key} url: http://set-gwm9thyc.cb-guangzhou.apigateway.tencentyun.com/api-etj7lhtw 注意,這里需要先部署 API 網(wǎng)關(guān)。當(dāng)部署完成,獲得回推地址,將回推地址以 url 的形式寫入到對應(yīng)函數(shù)的環(huán)境變量中: 理論上應(yīng)該是可以通過 ${restApi.url[0].internalDomain} 自動獲得到 url 的,但是我并沒有成功獲得到這個 url,只能先部署 API 網(wǎng)關(guān),獲得到這個地址之后,再重新部署。 部署完成之后,我們可以編寫 HTML 代碼,實現(xiàn)可視化的 Websocket Client,其核心的 JavaScript 代碼為: window.onload = function () { var conn; var msg = document.getElementById("msg"); var log = document.getElementById("log"); function appendLog(item) { var doScroll = log.scrollTop === log.scrollHeight - log.clientHeight; log.appendChild(item); if (doScroll) { log.scrollTop = log.scrollHeight - log.clientHeight; } } document.getElementById("form").onsubmit = function () { if (!conn) { return false; } if (!msg.value) { return false; } conn.send(msg.value); //msg.value = ""; var item = document.createElement("div"); item.innerText = "發(fā)送↑:"; appendLog(item); var item = document.createElement("div"); item.innerText = msg.value; appendLog(item); return false; }; if (window["WebSocket"]) { //替換為 websocket 連接地址 conn = new WebSocket("ws://service-01era6ni-1256773370.gz.apigw.tencentcs.com/release/"); conn.onclose = function (evt) { var item = document.createElement("div"); item.innerHTML = "Connection closed."; appendLog(item); }; conn.onmessage = function (evt) { var item = document.createElement("div"); item.innerText = "接收↓:"; appendLog(item); var messages = evt.data.split('\n'); for (var i = 0; i < messages.length; i++) { var item = document.createElement("div"); item.innerText = messages[i]; appendLog(item); } }; } else { var item = document.createElement("div"); item.innerHTML = "Your browser does not support WebSockets."; appendLog(item); } }; 完成之后,我們打開兩個頁面,進(jìn)行測試: 總結(jié) 通過云函數(shù) + API 網(wǎng)關(guān)進(jìn)行 Websocket 的實踐,絕對不僅僅是一個聊天工具這么簡單,它可以用在很多方面,例如通過 Websocket 進(jìn)行實時日志系統(tǒng)的制作等。 單獨(dú)的函數(shù)計算,僅僅是一個計算平臺,只有和周邊的 BaaS 結(jié)合,才能展示出 Serverless 架構(gòu)的價值和真正的能力。這也是為什么很多人說 Serverless=FaaS+BaaS 的一個原因。 期待更多小伙伴,可以通過 Serverless 架構(gòu),創(chuàng)造出更多有趣的應(yīng)用。 歡迎訪問: Serverless 中文社區(qū) 作為騰訊云 Serverless 的產(chǎn)品經(jīng)理,我經(jīng)常會收集到小伙伴們在使用 Serverless Framework 的一些問題和吐槽,比如近期小伙伴們反饋: 依賴庫安裝和本地調(diào)試成功,但在云端部署為何失敗? Serverless 應(yīng)用內(nèi)部的監(jiān)控,無法直接查看,每次定位問題的流程好長啊! 怎樣組織 Serverless 應(yīng)用? 不同的函數(shù)之間的調(diào)用關(guān)系、環(huán)境劃分、資源的管理及權(quán)限控制是怎樣的呢? 近期 Serverless 團(tuán)隊發(fā)布了一款里程碑新特性產(chǎn)品,產(chǎn)品通過支持應(yīng)用級別監(jiān)控和 Dashboard 資源管理,有效解決小伙伴們的痛點(diǎn)問題,一起來看看吧! Serverless Dashboard 新特性 1. 應(yīng)用管理 本次發(fā)布的應(yīng)用管理頁面則以 Component 為粒度,聚合了所有 Serverless Framework 部署的資源,并且展示了實例狀態(tài)、訪問鏈接以及上次的部署信息。此外,在管理詳情中還支持刪除 Serverless 應(yīng)用、下載項目代碼進(jìn)行二次開發(fā)等操作,開發(fā)者可以更方便、集中的管理賬號下的 Serverless 應(yīng)用。如下圖所示: 2. 部署詳情及輸出 Serverless Framework 的特性之一就是可以便捷的聯(lián)動關(guān)聯(lián)的云上資源,因此不同的 Serverless Component,可能會聯(lián)動不同的云上資源,如網(wǎng)關(guān)、云函數(shù)、COS 等。相信許多小伙伴在進(jìn)行二次開發(fā)時,都想要了解每個 Component 具體創(chuàng)建了的資源信息。 在本次發(fā)布的部署詳情頁中,不僅可以查看到 Serverless 實例的基本信息,還可以在輸出( output )頁面中查看到 Serverless Component 對應(yīng)的輸入、輸出信息。通過該頁面,可以查看到對應(yīng)的資源配置,如:地域信息、資源 id 、使用的語言環(huán)境、支持的協(xié)議信息等。有了這個頁面,可以直觀的看到對應(yīng)的資源配置,再也不擔(dān)心不同應(yīng)用之間搞混配置啦。 3. 應(yīng)用級別監(jiān)控 當(dāng)前 Serverless Framework 已經(jīng)支持了多種 Web 框架的一鍵部署。在部署完畢后,相信許多開發(fā)者會希望查看到基于應(yīng)用級別的監(jiān)控數(shù)據(jù)。而這往往在基礎(chǔ)資源的監(jiān)控中是難以體現(xiàn)出來的。 那么本次發(fā)布最為亮眼的能力,即支持了應(yīng)用級別的監(jiān)控頁面,實現(xiàn)了”0“配置的監(jiān)控指標(biāo)展示。當(dāng)前已經(jīng)支持 Express.js Component 的應(yīng)用級別監(jiān)控。無需去多個產(chǎn)品的控制臺查看監(jiān)控,無需自助上報數(shù)據(jù),無需借助第三方 APM 插件,只需一次部署,立刻查看 Express 應(yīng)用的監(jiān)控信息! 當(dāng)前的 Express.js 組件監(jiān)控主要支持下列指標(biāo): 函數(shù)觸發(fā)次數(shù) /錯誤次數(shù):function invocations & errors 函數(shù)延遲:function latency API 請求次數(shù) /錯誤次數(shù):api requests & errors API 請求延遲:api latency API 5xx 錯誤次數(shù):api 5xx errors API 4xx 錯誤次數(shù):api 4xx errors API 錯誤次數(shù)統(tǒng)計:api errors 不同路徑下 API 的請求方法、請求次數(shù)和平均延遲統(tǒng)計:api path requests 由于 Serverless Dashboard 是基于新版的 Serverless Component 開發(fā),因此同樣支持新版 Serverless Component 的特性: [門檻低] 交互式的一鍵部署指引:對于新用戶而言,只需要在終端輸入 serverless 命令,即可按照引導(dǎo)快速部署一個 Express 或 靜態(tài)網(wǎng)站應(yīng)用。 [部署快] 將一個 Express.js 應(yīng)用部署到云端只需要 5-6s 的時間,使本地和云端代碼可以順暢、快速同步。 [可復(fù)用] 支持云端注冊中心,每位開發(fā)者都可以貢獻(xiàn)自己的組件到注冊中心中,便于團(tuán)隊進(jìn)行復(fù)用。 [實時日志查看] 支持部署階段實時輸出請求日志、錯誤等信息,此外支持檢測本地代碼變化并自動部署云端,方便的進(jìn)行云端代碼開發(fā)。 [云端調(diào)試] 針對 Node.js 應(yīng)用,支持一鍵開啟云端 debug 能力,對云端代碼打斷點(diǎn)調(diào)試,真正實現(xiàn)了在云端進(jìn)行開發(fā)和調(diào)試的能力,無需考慮本地環(huán)境和遠(yuǎn)端環(huán)境的不一致問題。 [狀態(tài)共享] 通過云端部署引擎存儲應(yīng)用部署狀態(tài),便于賬號和團(tuán)隊之間共享資源,協(xié)作開發(fā)。 針對 Express.js 框架的應(yīng)用級別監(jiān)控主要基于騰訊云自定義監(jiān)控能力實現(xiàn)。在部署過程中,框架中使用 Serverless SDK,收集應(yīng)用級別的監(jiān)控信息進(jìn)行自定義上報和展示。因此用戶可以做到 “0”配置 查看應(yīng)用級別監(jiān)控指標(biāo)。真正實現(xiàn)快速部署一個開箱即用的 Serverless 應(yīng)用框架。 下面讓我?guī)Т蠹乙黄饘崙?zhàn)體驗一下我們的新產(chǎn)品吧! 玩轉(zhuǎn) Dashboard 使用實戰(zhàn) 本次實戰(zhàn),我們將通過一個 Express.js 框架的部署,來體驗 最新發(fā)布的 Dashboard 應(yīng)用管理、監(jiān)控視圖等能力。 首先,點(diǎn)擊? Express 鏈接 ,掃碼,登錄騰訊云賬號授權(quán),一鍵部署你的 Express 應(yīng)用。 完成后,可以看到如下圖所示: 你的 Express 應(yīng)用已經(jīng)部署好了! 等待幾分鐘,就可以在 Dashboard 上看到對應(yīng)的監(jiān)控數(shù)據(jù)啦! 如下圖所示: 當(dāng)前支持 15 分鐘,60 分鐘,24 小時和 7 天的監(jiān)控數(shù)據(jù)。 如果您希望進(jìn)行二次開發(fā),則在本地安裝 Serverless Framework,并點(diǎn)擊右上角的 [下載項目代碼] ,對代碼進(jìn)行修改和部署。 參考: 更多文檔資料 歡迎訪問: Serverless 中文網(wǎng) Rt,想做一個小型的社區(qū)聊天類(發(fā)帖、即時聊天)應(yīng)用,不知道現(xiàn)有的 serverless 平臺是不是已經(jīng)足夠滿足 ,( PS 有狀態(tài)的服務(wù)轉(zhuǎn)換成可支持 serverless 的無狀態(tài)服務(wù)復(fù)雜度怎樣?) 對文本進(jìn)行自動摘要的提取和關(guān)鍵詞的提取,屬于自然語言處理的范疇。提取摘要的一個好處是可以讓閱讀者通過最少的信息判斷出這個文章對自己是否有意義或者價值,是否需要進(jìn)行更加詳細(xì)的閱讀;而提取關(guān)鍵詞的好處是可以讓文章與文章之間產(chǎn)生關(guān)聯(lián),同時也可以讓讀者通過關(guān)鍵詞快速定位到和該關(guān)鍵詞相關(guān)的文章內(nèi)容。 文本摘要和關(guān)鍵詞提取都可以和傳統(tǒng)的 CMS 進(jìn)行結(jié)合,通過對文章 / 新聞等發(fā)布功能進(jìn)行改造,同步提取關(guān)鍵詞和摘要,放到 HTML 頁面中作為 Description 和 Keyworks 。這樣做在一定程度上有利于搜索引擎收錄,屬于 SEO 優(yōu)化的范疇。 關(guān)鍵詞提取 關(guān)鍵詞提取的方法很多,但是最常見的應(yīng)該就是 tf-idf 了。 通過 jieba 實現(xiàn)基于 tf-idf 關(guān)鍵詞提取的方法: jieba.analyse.extract_tags(text, topK=5, withWeight=False, allowPOS=('n', 'vn', 'v')) 文本摘要 文本摘要的方法也有很多,如果從廣義上來劃分,包括提取式和生成式。其中提取式就是在文章中通過 TextRank 等算法,找出關(guān)鍵句然后進(jìn)行拼裝,形成摘要,這種方法相對來說比較簡單,但是很難提取出真實的語義等;另一種方法是生成式,通過深度學(xué)習(xí)等方法,對文本語義進(jìn)行提取再生成摘要。 如果簡單理解,提取式方式生成的摘要,所有句子來自原文,而生成式方法則是獨(dú)立生成的。 為了簡化難度,本文將采用提取式來實現(xiàn)文本摘要功能,通過 SnowNLP 第三方庫,實現(xiàn)基于 TextRank 的文本摘要功能。我們以《海底兩萬里》部分內(nèi)容作為原文,進(jìn)行摘要生成: 原文: 這些事件發(fā)生時,我剛從美國內(nèi)布拉斯加州的貧瘠地區(qū)做完一項科考工作回來。我當(dāng)時是巴黎自然史博物館的客座教授,法國政府派我參加這次考察活動。我在內(nèi)布拉斯加州度過了半年時間,收集了許多珍貴資料,滿載而歸,3 月底抵達(dá)紐約。我決定 5 月初動身回法國。于是,我就抓緊這段候船逗留時間,把收集到的礦物和動植物標(biāo)本進(jìn)行分類整理,可就在這時,斯科舍號出事了。 我對當(dāng)時的街談巷議自然了如指掌,再說了,我怎能聽而不聞、無動于衷呢?我把美國和歐洲的各種報刊讀了又讀,但未能深入了解真相。神秘莫測,百思不得其解。我左思右想,搖擺于兩個極端之間,始終形不成一種見解。其中肯定有名堂,這是不容置疑的,如果有人表示懷疑,就請他們?nèi)ッ幻箍粕崽柕膫诤昧恕?我到紐約時,這個問題正炒得沸反盈天。某些不學(xué)無術(shù)之徒提出設(shè)想,有說是浮動的小島,也有說是不可捉摸的暗礁,不過,這些個假設(shè)通通都被推翻了。很顯然,除非這暗礁腹部裝有機(jī)器,不然的話,它怎能如此快速地轉(zhuǎn)移呢? 同樣的道理,說它是一塊浮動的船體或是一堆大船殘片,這種假設(shè)也不能成立,理由仍然是移動速度太快。 那么,問題只能有兩種解釋,人們各持己見,自然就分成觀點(diǎn)截然不同的兩派:一派說這是一個力大無比的怪物,另一派說這是一艘動力極強(qiáng)的“潛水船”。 哦,最后那種假設(shè)固然可以接受,但到歐美各國調(diào)查之后,也就難以自圓其說了。有哪個普通人會擁有如此強(qiáng)大動力的機(jī)械?這是不可能的。他在何地何時叫何人制造了這么個龐然大物,而且如何能在建造中做到風(fēng)聲不走漏呢? 看來,只有政府才有可能擁有這種破壞性的機(jī)器,在這個災(zāi)難深重的時代,人們千方百計要增強(qiáng)戰(zhàn)爭武器威力,那就有這種可能,一個國家瞞著其他國家在試制這類駭人聽聞的武器。繼夏斯勃步槍之后有水雷,水雷之后有水下撞錘,然后魔道攀升反應(yīng),事態(tài)愈演愈烈。至少,我是這樣想的。 通過 SnowNLP 提供的算法: from snownlp import SnowNLP text = " 上面的原文內(nèi)容,此處省略 " s = SnowNLP(text) print("。".join(s.summary(5))) 輸出結(jié)果: 自然就分成觀點(diǎn)截然不同的兩派:一派說這是一個力大無比的怪物。這種假設(shè)也不能成立。我到紐約時。說它是一塊浮動的船體或是一堆大船殘片。另一派說這是一艘動力極強(qiáng)的“潛水船” 初步來看,效果并不是很好,接下來我們自己計算句子權(quán)重,實現(xiàn)一個簡單的摘要功能,這個就需要 jieba : import re import jieba.analyse import jieba.posseg class TextSummary: def __init__(self, text): self.text = text def splitSentence(self): sectionNum = 0 self.sentences = [] for eveSection in self.text.split("\n"): if eveSection: sentenceNum = 0 for eveSentence in re.split("!|。|?", eveSection): if eveSentence: mark = [] if sectionNum == 0: mark.append("FIRSTSECTION") if sentenceNum == 0: mark.append("FIRSTSENTENCE") self.sentences.append({ "text": eveSentence, "pos": { "x": sectionNum, "y": sentenceNum, "mark": mark } }) sentenceNum = sentenceNum + 1 sectionNum = sectionNum + 1 self.sentences[-1]["pos"]["mark"].append("LASTSENTENCE") for i in range(0, len(self.sentences)): if self.sentences[i]["pos"]["x"] == self.sentences[-1]["pos"]["x"]: self.sentences[i]["pos"]["mark"].append("LASTSECTION") def getKeywords(self): self.keywords = jieba.analyse.extract_tags(self.text, topK=20, withWeight=False, allowPOS=('n', 'vn', 'v')) def sentenceWeight(self): # 計算句子的位置權(quán)重 for sentence in self.sentences: mark = sentence["pos"]["mark"] weightPos = 0 if "FIRSTSECTION" in mark: weightPos = weightPos + 2 if "FIRSTSENTENCE" in mark: weightPos = weightPos + 2 if "LASTSENTENCE" in mark: weightPos = weightPos + 1 if "LASTSECTION" in mark: weightPos = weightPos + 1 sentence["weightPos"] = weightPos # 計算句子的線索詞權(quán)重 index = [" 總之 ", " 總而言之 "] for sentence in self.sentences: sentence["weightCueWords"] = 0 sentence["weightKeywords"] = 0 for i in index: for sentence in self.sentences: if sentence["text"].find(i) >= 0: sentence["weightCueWords"] = 1 for keyword in self.keywords: for sentence in self.sentences: if sentence["text"].find(keyword) >= 0: sentence["weightKeywords"] = sentence["weightKeywords"] + 1 for sentence in self.sentences: sentence["weight"] = sentence["weightPos"] + 2 * sentence["weightCueWords"] + sentence["weightKeywords"] def getSummary(self, ratio=0.1): self.keywords = list() self.sentences = list() self.summary = list() # 調(diào)用方法,分別計算關(guān)鍵詞、分句,計算權(quán)重 self.getKeywords() self.splitSentence() self.sentenceWeight() # 對句子的權(quán)重值進(jìn)行排序 self.sentences = sorted(self.sentences, key=lambda k: k['weight'], reverse=True) # 根據(jù)排序結(jié)果,取排名占前 ratio% 的句子作為摘要 for i in range(len(self.sentences)): if i < ratio * len(self.sentences): sentence = self.sentences[i] self.summary.append(sentence["text"]) return self.summary 這段代碼主要是通過 tf-idf 實現(xiàn)關(guān)鍵詞提取,然后通過關(guān)鍵詞提取對句子盡心權(quán)重賦予,最后獲得到整體的結(jié)果,運(yùn)行: testSummary = TextSummary(text) print("。".join(testSummary.getSummary())) 可以得到結(jié)果: Building prefix dict from the default dictionary ... Loading model from cache /var/folders/yb/wvy_7wm91mzd7cjg4444gvdjsglgs8/T/jieba.cache Loading model cost 0.721 seconds. Prefix dict has been built successfully. 看來,只有政府才有可能擁有這種破壞性的機(jī)器,在這個災(zāi)難深重的時代,人們千方百計要增強(qiáng)戰(zhàn)爭武器威力,那就有這種可能,一個國家瞞著其他國家在試制這類駭人聽聞的武器。于是,我就抓緊這段候船逗留時間,把收集到的礦物和動植物標(biāo)本進(jìn)行分類整理,可就在這時,斯科舍號出事了。同樣的道理,說它是一塊浮動的船體或是一堆大船殘片,這種假設(shè)也不能成立,理由仍然是移動速度太快 我們可以看到,整體效果要比剛才的好一些。 發(fā)布 API 通過 Serverless 架構(gòu),將上面代碼進(jìn)行整理,并發(fā)布。 代碼整理結(jié)果: import re, json import jieba.analyse import jieba.posseg class NLPAttr: def __init__(self, text): self.text = text def splitSentence(self): sectionNum = 0 self.sentences = [] for eveSection in self.text.split("\n"): if eveSection: sentenceNum = 0 for eveSentence in re.split("!|。|?", eveSection): if eveSentence: mark = [] if sectionNum == 0: mark.append("FIRSTSECTION") if sentenceNum == 0: mark.append("FIRSTSENTENCE") self.sentences.append({ "text": eveSentence, "pos": { "x": sectionNum, "y": sentenceNum, "mark": mark } }) sentenceNum = sentenceNum + 1 sectionNum = sectionNum + 1 self.sentences[-1]["pos"]["mark"].append("LASTSENTENCE") for i in range(0, len(self.sentences)): if self.sentences[i]["pos"]["x"] == self.sentences[-1]["pos"]["x"]: self.sentences[i]["pos"]["mark"].append("LASTSECTION") def getKeywords(self): self.keywords = jieba.analyse.extract_tags(self.text, topK=20, withWeight=False, allowPOS=('n', 'vn', 'v')) return self.keywords def sentenceWeight(self): # 計算句子的位置權(quán)重 for sentence in self.sentences: mark = sentence["pos"]["mark"] weightPos = 0 if "FIRSTSECTION" in mark: weightPos = weightPos + 2 if "FIRSTSENTENCE" in mark: weightPos = weightPos + 2 if "LASTSENTENCE" in mark: weightPos = weightPos + 1 if "LASTSECTION" in mark: weightPos = weightPos + 1 sentence["weightPos"] = weightPos # 計算句子的線索詞權(quán)重 index = [" 總之 ", " 總而言之 "] for sentence in self.sentences: sentence["weightCueWords"] = 0 sentence["weightKeywords"] = 0 for i in index: for sentence in self.sentences: if sentence["text"].find(i) >= 0: sentence["weightCueWords"] = 1 for keyword in self.keywords: for sentence in self.sentences: if sentence["text"].find(keyword) >= 0: sentence["weightKeywords"] = sentence["weightKeywords"] + 1 for sentence in self.sentences: sentence["weight"] = sentence["weightPos"] + 2 * sentence["weightCueWords"] + sentence["weightKeywords"] def getSummary(self, ratio=0.1): self.keywords = list() self.sentences = list() self.summary = list() # 調(diào)用方法,分別計算關(guān)鍵詞、分句,計算權(quán)重 self.getKeywords() self.splitSentence() self.sentenceWeight() # 對句子的權(quán)重值進(jìn)行排序 self.sentences = sorted(self.sentences, key=lambda k: k['weight'], reverse=True) # 根據(jù)排序結(jié)果,取排名占前 ratio% 的句子作為摘要 for i in range(len(self.sentences)): if i < ratio * len(self.sentences): sentence = self.sentences[i] self.summary.append(sentence["text"]) return self.summary def main_handler(event, context): nlp = NLPAttr(json.loads(event['body'])['text']) return { "keywords": nlp.getKeywords(), "summary": "。".join(nlp.getSummary()) } 編寫項目 serverless.yaml 文件: nlpDemo: component: "@serverless/tencent-scf" inputs: name: nlpDemo codeUri: ./ handler: index.main_handler runtime: Python3.6 region: ap-guangzhou description: 文本摘要 / 關(guān)鍵詞功能 memorySize: 256 timeout: 10 events: - apigw: name: nlpDemo_apigw_service parameters: protocols: - http serviceName: serverless description: 文本摘要 / 關(guān)鍵詞功能 environment: release endpoints: - path: /nlp method: ANY 由于項目中使用了 jieba ,所以在安裝的時候推薦在 CentOS 系統(tǒng)下與對應(yīng)的 Python 版本下安裝,也可以使用我之前為了方便做的一個依賴工具: 通過 sls --debug 進(jìn)行部署: 部署完成,可以通過 PostMan 進(jìn)行簡單的測試: 從上圖可以看到,我們已經(jīng)按照預(yù)期輸出了目標(biāo)結(jié)果。至此,文本摘要 / 關(guān)鍵詞提取的 API 已經(jīng)部署完成。 總結(jié) 相對來說,通過 Serveless 架構(gòu)做 API 是非常容易和方便的,可實現(xiàn) API 的插拔行,組件化,希望本文能夠給讀者更多的思路和啟發(fā)。 歡迎訪問: Serverless 中文社區(qū) 在實際生產(chǎn)中,我們經(jīng)常需要做一些監(jiān)控腳本來監(jiān)控網(wǎng)站服務(wù)或者 API 服務(wù)是否可用。傳統(tǒng)的方法是使用網(wǎng)站監(jiān)控平臺(例如 DNSPod 監(jiān)控、360 網(wǎng)站服務(wù)監(jiān)控,以及阿里云監(jiān)控等),它們的原理是通過用戶自己設(shè)置要監(jiān)控的服務(wù)地址和監(jiān)測的時間閾值,由監(jiān)控平臺定期發(fā)起請求對網(wǎng)站或服務(wù)的可用性進(jìn)行判斷。 這些方法很大眾化,通用性很強(qiáng),但也不是所有場景都適合。例如,如果我們的需求是監(jiān)控網(wǎng)站狀態(tài)碼,不同區(qū)域的延時,并且通過監(jiān)控得到的數(shù)據(jù),設(shè)定一個閾值,一旦超過閾值就通過郵件等進(jìn)行統(tǒng)治告警,目前大部分的監(jiān)控平臺是很難滿足這些需求的,這時就需要定制開發(fā)一個監(jiān)控工具。 Serverless 服務(wù)的一個重要應(yīng)用場景就是運(yùn)維、監(jiān)控與告警,所以本文將會通過現(xiàn)有的 Serverless 平臺,部署一個網(wǎng)站狀態(tài)監(jiān)控腳本,對目標(biāo)網(wǎng)站的可用性進(jìn)行監(jiān)控告警。 Web 服務(wù)監(jiān)控告警 針對 Web 服務(wù),我們先設(shè)計一個簡單的監(jiān)控告警功能的流程: 在這個流程中,我們僅對網(wǎng)站的狀態(tài)碼進(jìn)行監(jiān)控,即返回的狀態(tài)為 200,則判定網(wǎng)站可正常使用,否則進(jìn)行告警: # -*- coding: utf8 -*- import ssl import json import smtplib import urllib.request from email.mime.text import MIMEText from email.header import Header ssl._create_default_https_context = ssl._create_unverified_context def sendEmail(content, to_user): sender = ' [email?protected] ' receivers = [to_user] mail_msg = content message = MIMEText(mail_msg, 'html', 'utf-8') message['From'] = Header(" 網(wǎng)站監(jiān)控 ", 'utf-8') message['To'] = Header(" 站長 ", 'utf-8') subject = " 網(wǎng)站監(jiān)控告警 " message['Subject'] = Header(subject, 'utf-8') try: smtpObj = smtplib.SMTP_SSL("smtp.exmail.qq.com", 465) smtpObj.login('發(fā)送郵件的郵箱地址', '密碼') smtpObj.sendmail(sender, receivers, message.as_string()) except smtplib.SMTPException as e: print(e) def getStatusCode(url): return urllib.request.urlopen(url).getcode() def main_handler(event, context): url = "http://www.anycodes.cn" if getStatusCode(url) == 200: print(" 您的網(wǎng)站 %s 可以訪問!" % (url)) else: sendEmail(" 您的網(wǎng)站 %s 不可以訪問!" % (url), " 接受人郵箱地址 ") return None 通過 ServerlessFramework 可以部署,在部署的時候可以增加時間觸發(fā)器: MyWebMonitor: component: "@serverless/tencent-scf" inputs: name: MyWebMonitor codeUri: ./code handler: index.main_handler runtime: Python3.6 region: ap-guangzhou description: 網(wǎng)站監(jiān)控 memorySize: 64 timeout: 20 events: - timer: name: timer parameters: cronExpression: '*/5 * * * *' enable: true 在這里,timer 表示時間觸發(fā)器, cronExpression 是表達(dá)式: 創(chuàng)建定時觸發(fā)器時,用戶能夠使用標(biāo)準(zhǔn)的 Cron 表達(dá)式的形式自定義何時觸發(fā)。定時觸發(fā)器現(xiàn)已推出秒級觸發(fā)功能,為了兼容老的定時觸發(fā)器,因此 Cron 表達(dá)式有兩種寫法。 Cron 表達(dá)式語法一(推薦) Cron 表達(dá)式有七個必需字段,按空格分隔。 其中,每個字段都有相應(yīng)的取值范圍: Cron 表達(dá)式語法二(不推薦) Cron 表達(dá)式有五個必需字段,按空格分隔。 其中,每個字段都有相應(yīng)的取值范圍: 通配符 注意事項 在 Cron 表達(dá)式中的“日”和“星期”字段同時指定值時,兩者為“或”關(guān)系,即兩者的條件分別均生效。 示例 */5 * * * * * * 表示每 5 秒觸發(fā)一次 0 0 2 1 * * * 表示在每月的 1 日的凌晨 2 點(diǎn)觸發(fā) 0 15 10 * * MON-FRI * 表示在周一到周五每天上午 10:15 觸發(fā) 0 0 10,14,16 * * * * 表示在每天上午 10 點(diǎn),下午 2 點(diǎn),4 點(diǎn)觸發(fā) 0 */30 9-17 * * * * 表示在每天上午 9 點(diǎn)到下午 5 點(diǎn)內(nèi)每半小時觸發(fā) 0 0 12 * * WED * 表示在每個星期三中午 12 點(diǎn)觸發(fā) 因此,我們上面的代碼可以認(rèn)為是每 5 秒觸發(fā)一次,當(dāng)然,也可以根據(jù)網(wǎng)站監(jiān)控密度,自定義設(shè)置觸發(fā)的間隔時間。當(dāng)我們網(wǎng)站服務(wù)不可用時,就可以收到告警: 這種網(wǎng)站監(jiān)控方法比較簡單,準(zhǔn)確度可能會有問題,對于網(wǎng)站或服務(wù)的監(jiān)控不能簡單的看返回值,還要看鏈接耗時、下載耗時以及不同區(qū)域、不同運(yùn)營商訪問網(wǎng)站或者服務(wù)的延時信息等。 所以,我們需要對這個代碼進(jìn)行額外的更新與優(yōu)化: 通過在線網(wǎng)速測試的網(wǎng)站,抓包獲取不同地區(qū)不同運(yùn)營商的請求特征; 編寫爬蟲程序,進(jìn)行在線網(wǎng)速測試模塊的編寫; 集成到剛剛的項目中; 下面以站長工具網(wǎng)站中國內(nèi)網(wǎng)站測速工具 為例,通過網(wǎng)頁查閱相關(guān)信息。 對網(wǎng)站測速工具進(jìn)行封裝,例如: 通過對網(wǎng)頁進(jìn)行分析,獲取請求特征,包括 Url,Form data,以及 Headers 等相關(guān)信息,其中該網(wǎng)站在使用不同監(jiān)測點(diǎn)對網(wǎng)站進(jìn)行請求時,是通過 Form data 中的 guid 的參數(shù)實現(xiàn)的,例如部分監(jiān)測點(diǎn)的 guid: 廣東佛山 電信 f403cdf2-27f8-4ccd-8f22-6f5a28a01309 江蘇宿遷 多線 74cb6a5c-b044-49d0-abee-bf42beb6ae05 江蘇常州 移動 5074fb13-4ab9-4f0a-87d9-f8ae51cb81c5 浙江嘉興 聯(lián)通 ddfeba9f-a432-4b9a-b0a9-ef76e9499558 此時,我們可以編寫基本的爬蟲代碼,來對 Response 進(jìn)行初步解析,以 62a55a0e-387e-4d87-bf69-5e0c9dd6b983 江蘇宿遷 [電信] 為例,編寫代碼: import urllib.request import urllib.parse url = "* 某測速網(wǎng)站地址 *" form_data = { 'guid': '62a55a0e-387e-4d87-bf69-5e0c9dd6b983', 'host': 'anycodes.cn', 'ishost': '1', 'encode': 'ECvBP9vjbuXRi0CVhnXAbufDNPDryYzO', 'checktype': '1', } headers = { 'Host': 'tool.chinaz.com', 'Origin': '* 某測速網(wǎng)站地址 *', 'Referer': '* 某測速網(wǎng)站地址 *', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36', 'X-Requested-With': 'XMLHttpRequest' } print(urllib.request.urlopen( urllib.request.Request( url=url, data=urllib.parse.urlencode(form_data).encode('utf-8'), headers=headers ) ).read().decode("utf-8")) 獲得結(jié)果: ({ state: 1, msg: '', result: { ip: '119.28.190.46', httpstate: 200, alltime: '212', dnstime: '18', conntime: '116', downtime: '78', filesize: '-', downspeed: '4.72', ipaddress: '新加坡新加坡', headers: 'HTTP/1.1 200 OK br>Server: ...', pagehtml: '' } }) 在這個結(jié)果中,我們可以提取部分?jǐn)?shù)據(jù),例如江蘇宿遷 [電信] 訪問目標(biāo)網(wǎng)站的基礎(chǔ)數(shù)據(jù): 總耗時:alltime:'212' 鏈接耗時:conntime:'116' 下載耗時:downtime:'78' 此時,我們可以改造代碼對更多的節(jié)點(diǎn),進(jìn)行測試: 江蘇宿遷 [電信] 總耗時:223 鏈接耗時:121 下載耗時:81 廣東佛山 [電信] 總耗時:44 鏈接耗時:27 下載耗時:17 廣東惠州 [電信] 總耗時:56 鏈接耗時:34 下載耗時:22 廣東深圳 [電信] 總耗時:149 鏈接耗時:36 下載耗時:25 浙江湖州 [電信] 總耗時:3190 鏈接耗時:3115 下載耗時:75 遼寧大連 [電信] 總耗時:468 鏈接耗時:255 下載耗時:170 江蘇泰州 [電信] 總耗時:180 鏈接耗時:104 下載耗時:69 安徽合肥 [電信] 總耗時:196 鏈接耗時:110 下載耗時:73 ... 并對項目中的 index.py 進(jìn)行代碼修改: # -*- coding: utf8 -*- import ssl import json import re import socket import smtplib import urllib.request from email.mime.text import MIMEText from email.header import Header socket.setdefaulttimeout(2.5) ssl._create_default_https_context = ssl._create_unverified_context def getWebTime(): final_list = [] final_status = True total_list = '''62a55a0e-387e-4d87-bf69-5e0c9dd6b983 江蘇宿遷 [電信] f403cdf2-27f8-4ccd-8f22-6f5a28a01309 廣東佛山 [電信] 5bea1430-f7c2-4146-88f4-17a7dc73a953 河南新鄉(xiāng) [多線] 1f430ff0-eae9-413a-af2a-1c2a8986cff0 河南新鄉(xiāng) [多線] ea551b59-2609-4ab4-89bc-14b2080f501a 河南新鄉(xiāng) [多線] 2805fa9f-05ea-46bc-8ac0-1769b782bf52 黑龍江哈爾濱 [聯(lián)通] 722e28ca-dd02-4ccd-a134-f9d4218505a5 廣東深圳 [移動] 8e7a403c-d998-4efa-b3d1-b67c0dfabc41 廣東深圳 [移動]''' url = "* 某測速網(wǎng)站地址 *" for eve in total_list.split('\n'): id_data, node_name = eve.strip().split(" ") form_data = { 'guid': id_data, 'host': 'anycodes.cn', 'ishost': '1', 'encode': 'ECvBP9vjbuXRi0CVhnXAbufDNPDryYzO', 'checktype': '1', } headers = { 'Host': '* 某測速網(wǎng)站地址 *', 'Origin': '* 某測速網(wǎng)站地址 *', 'Referer': '* 某測速網(wǎng)站地址 *', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36', 'X-Requested-With': 'XMLHttpRequest' } try: result_data = urllib.request.urlopen( urllib.request.Request( url=url, data=urllib.parse.urlencode(form_data).encode('utf-8'), headers=headers ) ).read().decode("utf-8") try: alltime = re.findall("alltime:'(.*?)'", result_data)[0] conntime = re.findall("conntime:'(.*?)'", result_data)[0] downtime = re.findall("downtime:'(.*?)'", result_data)[0] final_string = "%s\t 總耗時:%s\t 鏈接耗時:%s\t 下載耗時:%s" % (node_name, alltime, conntime, downtime) except: final_string = "%s 鏈接異常!" % (node_name) final_status = False except: final_string = "%s 鏈接超時!" % (node_name) final_status = False final_list.append(final_string) print(final_string) return (final_status,final_list) def sendEmail(content, to_user): sender = ' [email?protected] ' receivers = [to_user] mail_msg = content message = MIMEText(mail_msg, 'html', 'utf-8') message['From'] = Header(" 網(wǎng)站監(jiān)控 ", 'utf-8') message['To'] = Header(" 站長 ", 'utf-8') subject = " 網(wǎng)站監(jiān)控告警 " message['Subject'] = Header(subject, 'utf-8') try: smtpObj = smtplib.SMTP_SSL("smtp.exmail.qq.com", 465) smtpObj.login(' [email?protected] ', '密碼') smtpObj.sendmail(sender, receivers, message.as_string()) except smtplib.SMTPException: pass def getStatusCode(url): return urllib.request.urlopen(url).getcode() def main_handler(event, context): url = "http://www.anycodes.cn" final_status,final_list = getWebTime() if not final_status: sendEmail(" 您的網(wǎng)站 %s 的狀態(tài): %s" % (url, " ".join(final_list)), " [email?protected] ") 由于本文是以學(xué)習(xí)為主,所以我們將節(jié)點(diǎn)列表進(jìn)行縮減,只保留幾個。通過部署,可得到結(jié)果: 告警的靈敏度和監(jiān)控的頻率,在實際生產(chǎn)過程中可以根據(jù)自己的需求進(jìn)行調(diào)整。 云服務(wù)監(jiān)控告警 前文,我們對網(wǎng)站狀態(tài)以及健康等信息進(jìn)行了監(jiān)控與告警,在實際的生產(chǎn)運(yùn)維中,還需要對服務(wù)進(jìn)行監(jiān)控,例如在使用 Hadoop 、Spark 的時候?qū)?jié)點(diǎn)的健康進(jìn)行監(jiān)控,在使用 K8S 的時候?qū)?API 網(wǎng)關(guān)、ETCD 等多維度的指標(biāo)進(jìn)行監(jiān)控,在使用 Kafka 的時候,對數(shù)據(jù)積壓量,以及 Topic 、Consumer 等進(jìn)行監(jiān)控… 而這些服務(wù)的監(jiān)控,往往不能通過簡單的 URL 以及某些狀態(tài)來進(jìn)行判斷。傳統(tǒng)運(yùn)維的做法是在額外的機(jī)器上設(shè)置一個定時任務(wù),對相關(guān)的服務(wù)進(jìn)行旁路監(jiān)控。而在本文中,我們則通過 Serverless 技術(shù),對云產(chǎn)品進(jìn)行相關(guān)的監(jiān)控與告警。 在使用云上的 Kafka 時,我們通常要看數(shù)據(jù)積壓量,因為如果 Consumer 集群掛掉了,或者消費(fèi)能力突然降低導(dǎo)致數(shù)據(jù)積壓,很可能會對服務(wù)產(chǎn)生不可預(yù)估的影響,這個時候?qū)?Kafka 的數(shù)據(jù)積壓量進(jìn)行監(jiān)控告警,就顯得額外重要。 本文以監(jiān)控騰訊云的 Ckafka 為例進(jìn)行實踐,并通過多個云產(chǎn)品進(jìn)行組合(包括云監(jiān)控、Ckafka 、云 API 以及云短信等)來實現(xiàn)短信告警、郵件告警以及企業(yè)微信告警功能。 首先,可以設(shè)計簡單的流程圖: 在開始項目之前,我們要準(zhǔn)備一些基礎(chǔ)的模塊: Kafka 數(shù)據(jù)積壓量獲取模塊: def GetSignature(param): # 公共參數(shù) param["SecretId"] = "" param["Timestamp"] = int(time.time()) param["Nonce"] = random.randint(1, sys.maxsize) param["Region"] = "ap-guangzhou" # param["SignatureMethod"] = "HmacSHA256" # 生成待簽名字符串 sign_str = "GETckafka.api.qcloud.com/v2/index.php?" sign_str += "&".join("%s=%s" % (k, param[k]) for k in sorted(param)) # 生成簽名 secret_key = "" if sys.version_info[0] > 2: sign_str = bytes(sign_str, "utf-8") secret_key = bytes(secret_key, "utf-8") hashed = hmac.new(secret_key, sign_str, hashlib.sha1) signature = binascii.b2a_base64(hashed.digest())[:-1] if sys.version_info[0] > 2: signature = signature.decode() # 簽名串編碼 signature = urllib.parse.quote(signature) return signature def GetGroupOffsets(max_lag, phoneList): param = {} param["Action"] = "GetGroupOffsets" param["instanceId"] = "" param["group"] = "" signature = GetSignature(param) # 生成請求地址 param["Signature"] = signature url = "https://ckafka.api.qcloud.com/v2/index.php?Action=GetGroupOffsets&" url += "&".join("%s=%s" % (k, param[k]) for k in sorted(param)) req_attr = urllib.request.urlopen(url) res_data = req_attr.read().decode("utf-8") json_data = json.loads(res_data) for eve_topic in json_data['data']['topicList']: temp_lag = 0 result_list = [] for eve_partition in eve_topic["partitions"]: lag = eve_partition["lag"] temp_lag = temp_lag + lag if temp_lag > max_lag: result_list.append( { "topic": eve_topic["topic"], "lag": lag } ) print(result_list) if len(result_list)>0: KafkaLagRobot(result_list) KafkaLagSMS(result_list,phoneList) 接入企業(yè)微信機(jī)器人模塊: def KafkaLagRobot(content): url = "" data = { "msgtype": "markdown", "markdown": { "content": content, } } data = json.dumps(data).encode("utf-8") req_attr = urllib.request.Request(url, data) resp_attr = urllib.request.urlopen(req_attr) return_msg = resp_attr.read().decode("utf-8") 接入騰訊云短信服務(wù)模塊: def KafkaLagSMS(infor, phone_list): url = "" strMobile = phone_list strAppKey = "" strRand = str(random.randint(1, sys.maxsize)) strTime = int(time.time()) strSign = "appkey=%s&random=%s&time=%s&mobile=%s" % (strAppKey, strRand, strTime, ",".join(strMobile)) sig = hashlib.sha256() sig.update(strSign.encode("utf-8")) phone_dict = [] for eve_phone in phone_list: phone_dict.append( { "mobile": eve_phone, "nationcode": "86" } ) data = { "ext": "", "extend": "", "params": [ infor, ], "sig": sig.hexdigest(), "sign": " 你的 sign", "tel": phone_dict, "time": strTime, "tpl_id": 你的模板 id } data = json.dumps(data).encode("utf-8") req_attr = urllib.request.Request(url=url, data=data) resp_attr = urllib.request.urlopen(req_attr) return_msg = resp_attr.read().decode("utf-8") 發(fā)送郵件告警模塊: def sendEmail(content, to_user): sender = ' [email?protected] ' message = MIMEText(content, 'html', 'utf-8') message['From'] = Header(" 監(jiān)控 ", 'utf-8') message['To'] = Header(" 站長 ", 'utf-8') message['Subject'] = Header(" 告警 ", 'utf-8') try: smtpObj = smtplib.SMTP_SSL("smtp.exmail.qq.com", 465) smtpObj.login(' [email?protected] ', '密碼') smtpObj.sendmail(sender, [to_user], message.as_string()) except smtplib.SMTPException as e: logging.debug(e) 完成模塊編寫,和上面的方法一樣,進(jìn)行項目部署。部署成功之后進(jìn)行測試,測試可看到功能可用: 短信告警樣式: 企業(yè)微信告警樣式: 總結(jié) 通過本文的實踐,希望讀者可以了解到 Serverless 相關(guān)產(chǎn)品在運(yùn)維行業(yè)中的基本應(yīng)用,尤其是監(jiān)控告警的基本使用方法和初步靈感。設(shè)計一個網(wǎng)站監(jiān)控程序?qū)嶋H上是一個很初級的入門場景,希望大家可以將更多的監(jiān)控告警功與 Serverless 技術(shù)進(jìn)行結(jié)合,例如監(jiān)控自己的 MySQL 壓力情況、監(jiān)控已有服務(wù)器的數(shù)據(jù)指標(biāo)等,通過對這些指標(biāo)的監(jiān)控告警,不僅僅可以讓管理者及時發(fā)現(xiàn)服務(wù)的潛在風(fēng)險,也可以通過一些自動化流程實現(xiàn)項目的自動化運(yùn)維。 通過本場景實踐,我們也可以對項目進(jìn)行額外的優(yōu)化或者應(yīng)用在不同的領(lǐng)域以及場景中。例如,我們可以通過增加短信告警、微信告警、企業(yè)微信告警等多個維度,來確保相關(guān)人員可以及時收到告警信息;我們也可以通過監(jiān)控某個小說網(wǎng)站、視頻網(wǎng)站等,看到我們關(guān)注的小說或者視頻的更新情況,便于追更等。 傳送門: GitHub: github.com/serverless 官網(wǎng): serverless.com 社區(qū) Serverless 中文網(wǎng) 開發(fā)前言 作為一名 Serverless 架構(gòu)的重度使用者,我一直對調(diào)試感到恐慌:經(jīng)常在測試接口的時候,會通過網(wǎng)頁 /PostMan 觸發(fā)函數(shù),然后沒得到預(yù)期的結(jié)果,我就只能傻乎乎的一直點(diǎn)控制臺的日志,等待他能早點(diǎn)出來結(jié)果,看看為啥和我預(yù)期結(jié)果不同。 雖然說 10S,20S 的日志輸出還能接受,但是在調(diào)試過程中,真的就是噩夢,一直在想有什么方法可以實現(xiàn)實時日志,我觸發(fā)函數(shù),就馬上能看到,無論是控制臺 /API 網(wǎng)關(guān)還是 COS 觸發(fā)器,只要被觸發(fā),我就能實時看到日志,這將會對我寫代碼,調(diào)試產(chǎn)生重大,超級重大幫助,所以我開發(fā)了這個組件。 為了更加方便,清晰,直觀,我這里做了個使用方法的教程: 使用方法教程: 說明 該模塊用于實現(xiàn)云函數(shù) SCF Python Runtime 的實時日志功能,通過該組件,您可以實時查看到函數(shù)輸出的日志(包括 print 和 logging 等),本組件目前在測試階段,歡迎測試提意見,目前不建議上業(yè)務(wù)。 準(zhǔn)備 安裝 scflog : pip install scflog 安裝時,可能需要 root 權(quán)限,否則可能無法使用。安裝完成,可以執(zhí)行 scflog -v 查看是否安裝成功: scflog 0.1.1 部署實時日志組件,新建項目,并且建立 serverless.yaml ,內(nèi)容: 組件部署 PythonLogs: component: '@gosls/tencent-pythonlogs' inputs: region: ap-guangzhou 這里的參數(shù)是您要將這個組件部署的區(qū)域。該組件可以復(fù)用,也就是說這個組件部署完成之后可以一直被使用。 通過 sls --debug 部署: DEBUG ─ Setting tags for function PythonRealTimeLogs_Cleanup DEBUG ─ Creating trigger for function PythonRealTimeLogs_Cleanup DEBUG ─ Deployed function PythonRealTimeLogs_Cleanup successful PythonLogs: websocket: ws://service-laabz6zm-1256773370.gz.apigw.tencentcs.com/test/python_real_time_logs 26s ? PythonLogs ? done 此時我們需要配置組件: scflog set -w ws://service-laabz6zm-1256773370.gz.apigw.tencentcs.com/test/python_real_time_logs 配置成功輸出: DFOUNDERLIU-MB0:~ dfounderliu$ scflog set -w ws://service-laabz6zm-1256773370.gz.apigw.tencentcs.com/test/python_real_time_logs 設(shè)置成功 websocket: ws://service-laabz6zm-1256773370.gz.apigw.tencentcs.com/test/python_real_time_logs region: ap-guangzhou namespace: default 通過 sls remove --debug 移除 DEBUG ─ Removing any previously deployed API. api-rzm1uzik DEBUG ─ Removing any previously deployed API. api-07wq4u9a DEBUG ─ Removing any previously deployed service. service-laabz6zm 6s ? PythonLogs ? done 項目中使用 在項目中使用該組件的方法很簡單。 創(chuàng)建一個文件夾,并進(jìn)入 mkdir scflogs && cd scflogs 初始化項目 scflog init 創(chuàng)建 index.py 文件以及 serverless.yaml 文件: vim index.py 內(nèi)容是: from logs import * import time import logging def main_handler(event, context): print("event is: ", event) time.sleep(1) logging.debug("this is debug_msg") time.sleep(1) logging.info("this is info_msg") time.sleep(1) logging.warning("this is warning_msg") time.sleep(1) logging.error("this is error_msg") time.sleep(1) logging.critical("this is critical_msg") time.sleep(1) print("context is: ", event) return "hello world" vim serverless.yaml 內(nèi)容是: Hello_World: component: "@serverless/tencent-scf" inputs: name: Hello_World codeUri: ./ handler: index.main_handler runtime: Python3.6 region: ap-guangzhou description: My Serverless Function memorySize: 64 timeout: 20 exclude: - .gitignore - .git/** - node_modules/** - .serverless - .env events: - apigw: name: serverless parameters: protocols: - http serviceName: serverless description: the serverless service environment: release endpoints: - path: /test method: ANY 通過 sls --debug 部署: DEBUG ─ Deployed function Hello_World successful Hello_World: Name: Hello_World Runtime: Python3.6 Handler: index.main_handler MemorySize: 64 Timeout: 20 Region: ap-guangzhou Namespace: default Description: My Serverless Function APIGateway: - serverless - http://service-89bjzrye-1256773370.gz.apigw.tencentcs.com/release 30s ? Hello_World ? done 此時,我們配置了 APIGW 的觸發(fā)器,地址是上面輸出的地址 + endpoints 中的 path 例如: http://service-89bjzrye-1256773370.gz.apigw.tencentcs.com/release/test 此時,我們可以打開實時日志: scflog logs -n Hello_World -r ap-guangzhou 此時會提醒我們實時日志開啟成功: DFOUNDERLIU-MB0:~ dfounderliu$ scflog logs -n Hello_World -r ap-guangzhou 實時日志開啟 ... 我們可以用瀏覽器通過剛才函數(shù)部署完成返回給我們的地址觸發(fā)函數(shù): 實時日志開啟 ... [2020-03-04 16:36:08] : ......} [2020-03-04 16:36:09] : DEBUG debug_msg [2020-03-04 16:36:10] : INFO info_msg [2020-03-04 16:36:11] : WARNING warning_msg [2020-03-04 16:36:14] : ERROR error_msg [2020-03-04 16:36:14] : CRITICAL critical_msg [2020-03-04 16:36:16] : context is: .......} ....... 至此,實現(xiàn)實時日志功能。 總結(jié) 至此,完成了 Python 語言的實時日志功能,根據(jù)測試來看,性能還算不錯,也還算穩(wěn)定。通過 3 個函數(shù) + APIGW + COS + CAM 完成了一個實時日志功能,理論上也可以復(fù)用到 Nodejs 等 Runtime 。 傳送門: GitHub: github.com/serverless 官網(wǎng): serverless.com 社區(qū): Serverless 中文網(wǎng) 本文介紹了跨域資源共享的基本知識,以及如何避免云函數(shù)上 Serverless web API 的問題 構(gòu)建 Web API 是 Serverless 應(yīng)用中最流行的用例之一,您能在不增加其他操作開銷的情況下,獲得簡單、可擴(kuò)展的后端優(yōu)勢。 然而,如果您的網(wǎng)頁正在調(diào)用后端 API,那么必須處理 跨域資源共享 (CORS) 的問題 ,如果您的網(wǎng)頁向與您當(dāng)前所在域的不同域發(fā)出 HTTP 請求,則它必須是 CORS 友好的。 如果您發(fā)現(xiàn)以下錯誤: No 'Access-Control-Allow-Origin' header is present on the requested resource 那么本文可能對您有所幫助。 接下來,我們將介紹 Serverless + CORS 的相關(guān)信息,目錄如下: 預(yù)檢請求 (Preflight requests) 響應(yīng)頭 (Response headers) CORS with cookie credentials TL;DR 快速開始 ?? 如果您想快速解決 Serverless 應(yīng)用中的 CORS,可以執(zhí)行以下操作: 要處理 preflight requests ,在每個 HTTP 端點(diǎn)中添加 enableCORS: true 和 integratedResponse: true 標(biāo)記: # serverless.yml service: products-service provider: name: tencent region: ap-guangzhou runtime: Nodejs8.9 # Nodejs8.9 or Nodejs6.10 plugins: - serverless-tencent-scf functions: getProduct: handler: handler.getProduct events: - apigw: name: api parameters: path: /product stageName: release # 修改成你的 API 服務(wù) ID serviceId: service-xxx httpMethod: GET # 開啟集成相應(yīng),這里必須開啟,才能自定義響應(yīng) headers integratedResponse: true, # 開啟 CORS enableCORS: true createProduct: handler: handler.createProduct events: - apigw: name: api parameters: path: /product stageName: release # 修改成你的 API 服務(wù) ID serviceId: service-xxx httpMethod: POST # 開啟集成相應(yīng),這里必須開啟,才能自定義響應(yīng) headers integratedResponse: true, # 開啟 CORS enableCORS: false 要處理 CORS headers ,請在響應(yīng)中返回 CORS headers 。主要標(biāo)頭是 Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials 。示例如下: 'use strict'; // mock function function retrieveProduct(event) { return { id: 1, name: 'good1', price: 10, }; } // mock function function createProduct(event) { const { queryString } = event; return { id: Number(queryString.id), name: 'good1', price: 10, }; } module.exports.getProduct = (event, context, callback) => { const product = retrieveProduct(event); const response = { statusCode: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true, }, body: JSON.stringify({ product: product, }), }; callback(null, response); }; module.exports.createProduct = (event, context, callback) => { // Do work to create Product const product = createProduct(event); const response = { statusCode: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true, }, body: JSON.stringify({ product: product, }), }; callback(null, response); }; CORS preflight requests 如果您不是在進(jìn)行「 simple request 」,那么瀏覽器會用 OPTIONS 方法,發(fā)送 preflight request 到資源里,您請求的資源將使用 安全發(fā)送到資源 的方法返回,并且可以選擇返回有效發(fā)送的標(biāo)頭。 我們拆開來看看: 瀏覽器什么時候發(fā)送 preflight requests? 您的瀏覽器會對幾乎所有的跨域請求發(fā)送一個 preflight requests 。(例外是「 simple requests 」,但這只是請求的一小部分)。大體上看,一個簡單請求只是一個 GET request 或者 POST request,如果您不在此范圍內(nèi),則需要進(jìn)行預(yù)檢。 對 preflight requests 的響應(yīng)是什么? 對一個 preflight requests 的 響應(yīng)包括其允許訪問的資源,它允許在該資源的方法,如 GET , POST , PUT 等。還可以包括被允許在該資源標(biāo)頭,如 Authentication 。 如何處理 Serverless 中的 preflight requests? 要設(shè)置 preflight requests,您只需要在 API Gateway 的端點(diǎn)上配置一個 OPTIONS 。幸運(yùn)的是,你可以非常簡單地使用 Serverless Framework 來完成。 只需要在 serverless.yml 添加設(shè)置 enableCORS: true : # serverless.yml service: products-service provider: name: tencent region: ap-guangzhou runtime: Nodejs8.9 # Nodejs8.9 or Nodejs6.10 plugins: - serverless-tencent-scf functions: getProduct: handler: handler.getProduct events: - apigw: name: api parameters: path: /product stageName: release serviceId: service-lanyfiga httpMethod: GET # 開啟集成相應(yīng),這里必須開啟,才能自定義響應(yīng) headers integratedResponse: true, # 開啟 CORS enableCORS: true createProduct: handler: handler.createProduct events: - apigw: name: api parameters: path: /product stageName: release serviceId: service-lanyfiga httpMethod: POST # 開啟集成相應(yīng),這里必須開啟,才能自定義響應(yīng) headers integratedResponse: true, # 開啟 CORS enableCORS: false CORS Response Headers 盡管 preflight request 僅適用于某些跨域請求,但每個跨域請求中都必須存在 CORS Response Headers,這意味著您必須將 Access-Control-Allow-Origin 添加進(jìn) handlers 的響應(yīng)中。 如果您使用 cookies,還需要添加 Access-Control-Allow-Credentials 。 要與上面的 serverless.yml 匹配, handler.js 文件應(yīng)該如下設(shè)置: // handler.js 'use strict'; module.exports.getProduct = (event, context, callback) => { // Do work to retrieve Product const product = retrieveProduct(event); const response = { statusCode: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true, }, body: JSON.stringify({ product: product, }), }; callback(null, response); }; module.exports.createProduct = (event, context, callback) => { // Do work to create Product const product = createProduct(event); const response = { statusCode: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true, }, body: JSON.stringify({ product: product, }), }; callback(null, response); }; 這里需要注意 response 的 headers 屬性,其中包含 Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials 。 下面是一個簡單的示例: // hello.js const middy = require('middy'); const { cors } = require('middy/middlewares'); // This is your common handler, no way different than what you are used to do every day const hello = (event, context, callback) => { const response = { statusCode: 200, body: 'Hello, world!', }; return callback(null, response); }; // Let's "middyfy" our handler, then we will be able to attach middlewares to it const handler = middy(hello).use(cors()); // Adds CORS headers to responses module.exports = { handler }; CORS with Cookie credentials 在上面的示例中,我們給定了 "*" 作為 Access-Control-Allow-Origin 的值。但是,如果您使用 request using credentials 則不被允許。為了使瀏覽器能夠響應(yīng), Access-Control-Allow-Origin 需要包含發(fā)出請求的特定來源。有兩種方法可以解決。 首先,如果只有一個發(fā)出請求的原始網(wǎng)站,則可以將其硬編碼到云函數(shù)的響應(yīng)中: // handler.js 'use strict'; module.exports.getProduct = (event, context, callback) => { // Do work to retrieve Product const product = retrieveProduct(event); const response = { statusCode: 200, headers: { 'Access-Control-Allow-Origin': 'https://myorigin.com', // <-- Add your specific origin here 'Access-Control-Allow-Credentials': true, }, body: JSON.stringify({ product: product, }), }; callback(null, response); }; 如果有多個原始網(wǎng)站使用您的 API,那么需要采用一種更加動態(tài)的方法。你可以檢查 origin header 看看是否在被批準(zhǔn)的來源列表中,如果是,則在 Access-Control-Allow-Origin 返回原點(diǎn)值。 // handler.js 'use strict'; const ALLOWED_ORIGINS = [ 'https://myfirstorigin.com', 'https://mysecondorigin.com' ]; module.exports.getProduct = (event, context, callback) => { const origin = event.headers.origin; let headers; if (ALLOWED_ORIGINS.includes(origin) { headers = { 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Credentials': true, }, } else { headers = { 'Access-Control-Allow-Origin': '*', }, } // Do work to retrieve Product const product = retrieveProduct(event); const response = { isBase64Encoded: false, statusCode: 200, headers body: JSON.stringify({ product: product }), }; callback(null, response); }; 在這個示例中,我們檢查 origin header 是否匹配。如果匹配,我們會在 Access-Control-Allow-Origin 包含特定來源,并聲明 Access-Control-Allow-Credentials 允許的來源。如果 origin 不是我們允許的來源之一,則我們將包含標(biāo)準(zhǔn) headers,如果來源嘗試進(jìn)行憑據(jù)請求,則將被拒絕。 小結(jié) 處理 CORS 確實是一件麻煩的事情,但是使用 Serverless Framework 會讓處理步驟變得簡單得多!而這也就意味著再也不會出現(xiàn) No 'Access-Control-Allow-Origin' header is present on the requested resource 這樣的錯誤啦!?? 傳送門: GitHub: github.com/serverless 官網(wǎng): serverless.com 社區(qū): Serverless 中文網(wǎng) 本文系譯文,Serverless 團(tuán)隊新成員 Charmmie 是從高級時裝行業(yè)轉(zhuǎn)行到技術(shù)領(lǐng)域的,本文是她的故事,太酷了。 我是一名藝術(shù)家、時尚行業(yè)從業(yè)者。講真的,我從沒想過自己有一天會像現(xiàn)在這樣,坐在舊金山一家科技初創(chuàng)公司的辦公桌前撰寫這篇博客。 事實上,我過去對科技持抗拒態(tài)度(只是「過去」)。我認(rèn)為科技沒什么意思,科技行業(yè)太過強(qiáng)調(diào)顛覆性,不夠注重人文關(guān)懷。 但正如您所看到的,現(xiàn)在我在 Serverless 上班。我怎么會到這里上班呢?又是什么讓我在這個行業(yè)中找到激情?這段歷程其實也是充滿意外和曲折的。 科技?算了吧 我長大的地方離加州比蒂諾 Apple 總部只有一英里遠(yuǎn)。雖然新興科技無處不在,但是我對此毫不關(guān)心。 1987 年,喬布斯來到我的母校宣傳 Macintosh SE 和 Macintosh II 個人電腦。他當(dāng)時穿著一雙 Teva 涼鞋,而且還穿了雙襪子。我當(dāng)時內(nèi)心 OS 是「這也太土了吧!」。 他滔滔不絕地講,而我全程神游。我當(dāng)時在想,「我又不想了解這些,我也構(gòu)建不出這些東西來」。但是我知道,三四年級的同學(xué)會花數(shù)小時玩電腦上的 Oregon Trail 游戲。 高中時期,我熱衷于各種藝術(shù)。我是 90 年代中期灣區(qū)音樂舞臺的忠實參與者,在 KSCU 103.3 FM 當(dāng)一名電臺 DJ,后來在 UC Irvine 學(xué)習(xí)藝術(shù)和德語。之后我去了德國科隆,在那里當(dāng)英語教師和畫廊助理。 最后,我回到了灣區(qū),并在 SFMOMA 工作。那時,我以為自己會成為一名策展人,甚至還考慮過重返學(xué)校攻讀藝術(shù)史碩士學(xué)位。 在 SFMOMA 工作兩年后,我變得非常不開心。我討厭無人認(rèn)可的感覺,而且我好窮(在非盈利組織中工作就是這樣)。于是我辭職了。 后來我遇到了 Sam 。 從非盈利性藝術(shù)到快時尚 在一個 party 上,我遇見了 Sam,關(guān)于時裝這個話題,我們倆 仿佛有說不完的話 。兩個月后,我們在舊金山簽訂了業(yè)務(wù)合作文件。 我們的時裝銷售代理機(jī)構(gòu) Varjak 就此誕生。 我和 Sam 一起工作的那幾年里,我們一起參加了在紐約、倫敦和巴黎舉行的時裝周,成為新興設(shè)計師的代表,會見了很多時尚界大咖,包括美國《 Vogue 》總編輯、社交界女王 Anna Wintour 。 然后……額,一切都開始數(shù)字化了。線上銷售賺得盆滿缽滿,全球零售業(yè)都遭受損失,我們的買家報告說銷售業(yè)績欠佳。Varjak 面臨著重大打擊。 科技在無形之中又開始影響著我的生活。于是,我將屬于自己的一半生意賣給了 Sam,然后思考我接下來究竟要做什么。 初入科技領(lǐng)域:偶遇技術(shù)招聘人員 在舊金山的一個酒吧,我偶遇一名技術(shù)行業(yè) HR,然后開始聊起了工作這個話題。她對我說:“你是一個充滿激情和上進(jìn)心的人,很適合從事技術(shù)方面的工作?!? 我當(dāng)時的唯一反應(yīng)是不可思議, 我從內(nèi)心抗拒。我回答說:“我不適合,技術(shù)工作也不是我喜歡的類型。雖然從小時候起,我身邊就有科技公司,但我不感興趣?!钡€是勸我投遞一份簡歷過去。鬼使神差地,我照做了。 很快,她就給我提供了一個職位。她說:“我有個崗位,你的文化背景可能很適合。這個職位有著很明顯的技術(shù)特點(diǎn),員工們也很不錯。你的加入可能會使這個團(tuán)隊更加優(yōu)秀,而且你的才華和時尚能帶來更多新的活力!” 我有點(diǎn)勉強(qiáng),但也很好奇。最終我決定:為什么不去試下呢?我曾經(jīng)愿意嘗試任何事情。于是我答應(yīng)了,然后就在那一周,我成了 Serverless 的行政助理。 我很緊張。但同時,我也喜歡未知事物,我從不是那種逃避挑戰(zhàn)的人。正是這種態(tài)度,我才能迅速地學(xué)習(xí)和不斷地成長。 上班第一周 加入 Serverless 后,我注意到的第一件事就是每一個人都很真誠和友善。 CEO 兼創(chuàng)始人 Austen Collins 是在好萊塢開始職業(yè)生涯的,最后創(chuàng)建了這個成千上萬名開發(fā)者每天都在使用的框架。他來自創(chuàng)意行業(yè),但找到了技術(shù)問題的解決方案。 我逐漸認(rèn)識了其他同事,發(fā)現(xiàn)這個集體中的每一個人都很風(fēng)趣、聰明且多才多藝。雖然每個人除科技外還有著其他興趣,但大家都非常熱愛 Serverless 。作為一個公司,Serverless 專注于社區(qū),且將服務(wù)他人作為自己的使命。Serverless 希望 每個人 都能更輕松地開發(fā)軟件。 我對 Serverless 了解地越多,就越想盡自己所能讓 Serverless 在這個技術(shù)飽和的世界里脫穎而出。 未來屬于 Serverless 技術(shù)并非是無情或冷漠的,其實它和時尚很相似。技術(shù)需要擁有創(chuàng)新解決方案,保持快節(jié)奏的步伐,處于最前沿以及與社區(qū)開發(fā)者互動。 很多現(xiàn)代技術(shù)公司都在努力地為這個行業(yè)帶來真正的 人文關(guān)懷 。他們也開始吸納像我這樣的外行人,科技公司正逐漸變得更加多元化。 Serverless 吸引我的不僅是這里的人,而且還有它作為公司而肩負(fù)的使命。它嘗試讓軟件開發(fā)大眾化,希望技術(shù)的使用變得更加容易,任何人都可以學(xué)習(xí)。 即使像我這樣的純外行也能。 和聆聽 1987 年喬布斯那次的演講時不同,現(xiàn)在的我非常激動,我很好奇我的想法能給我?guī)硎裁唇?jīng)歷和成長。我想了解技術(shù)世界的原理,想構(gòu)建技術(shù)項目。 從在時裝行業(yè)開始,我就努力創(chuàng)造我自己的風(fēng)格。每天我都會精心挑選服裝,想借此進(jìn)行深刻的視覺表達(dá)。現(xiàn)在,我希望能夠幫助 Serverless 成為「經(jīng)典黑」這種所有人都適合的恒久風(fēng)格。 讓我們拭目以待吧……這就是我的 Serverless style 。 傳送門: GitHub: github.com/serverless 官網(wǎng): serverless.com 社區(qū): Serverless 中文網(wǎng) 日常生活中,我們常常會想要「搜索照片」。每當(dāng)尋找很久遠(yuǎn)的照片時,記憶模糊,檢索照片時只能想起大致的時間,然后一張張查看。這樣不僅效率低下,還經(jīng)常會漏掉我們想找的照片。 近幾年微信小程序發(fā)展迅速,如果有這么一款軟件,我們只需要用文字簡單描述,就能實現(xiàn)圖片的快速檢索,豈不是很棒! 本項目將以小程序為例,在 Serverless 架構(gòu)上進(jìn)行開發(fā)。該小程序在保留相冊基礎(chǔ)功能(新建相冊、刪除相冊、上傳圖片、查看圖片、刪除圖片)上,增加人工智能搜索 —— 即用戶上傳圖片之后,基于 Image Caption 技術(shù),自動對圖片進(jìn)行描述,實現(xiàn) Image to Text 的過程。這樣,當(dāng)用戶進(jìn)行搜索時,通過文本間的相似度,就可以返回最貼近的圖片。 基礎(chǔ)設(shè)計 該項目設(shè)計主要擁有登錄、相冊、圖片上傳和預(yù)覽功能,以及搜索功能。如圖所示: 注冊功能的主要作用是:通過獲取用戶的唯一 id (微信中的 OpenId ),來將用戶信息存儲到數(shù)據(jù)庫中,之后的所有操作,都需要以該 id 作為區(qū)分; 相冊功能主要包括相冊的增刪查改等功能; 圖片功能包括圖片上傳、刪除和查看; 搜索功能主要是可以查看指定標(biāo)簽對應(yīng)的圖片列表,以及指定搜索內(nèi)容對應(yīng)的列表。 當(dāng)然這四個主要功能和模塊是和前端關(guān)系緊密的部分,除此之外還有后端異步操作的兩個模塊,分別是圖像壓縮和圖像描述功能。 1. 注冊功能: 注冊功能是用戶點(diǎn)擊注冊賬號之后,執(zhí)行的動作。 該動作需要注意,注冊之前需先判斷用戶是否已經(jīng)注冊過。如果已注冊則默認(rèn)登陸,否則進(jìn)行注冊并登陸。當(dāng)用戶不想注冊時,可以點(diǎn)擊體驗程序,對程序大部分頁面進(jìn)行預(yù)覽。但是不能實現(xiàn)有關(guān)數(shù)據(jù)庫的增刪改查。登錄功能頁面如圖所示: 2. 相冊功能: 當(dāng)用戶注冊登錄之后,可以在相冊管理頁面進(jìn)行相冊相關(guān)的管理,包括編輯、刪除和新建。在進(jìn)行添加和修改的時候,需要注意相冊名稱是否已經(jīng)存在;在進(jìn)行刪除、修改相冊等操作時要判斷用戶是否有操作該相冊的權(quán)限等。相冊功能原型如圖所示: 3. 圖片功能: 圖片功能主要包括圖片列表以及圖片獲取、上傳和刪除。在圖片獲取與刪除的過程中,要對用戶是否有該項操作的權(quán)限進(jìn)行判斷,上傳時也要判斷是否有上傳到指定相冊的權(quán)限。圖片功能相關(guān)原型圖如所示。 圖片功能部分除了用戶側(cè)可見的功能,還有定時任務(wù)。當(dāng)用戶上傳圖片之后,系統(tǒng)會在后臺異步進(jìn)行圖像壓縮、圖像描述和關(guān)鍵詞提取等。整體流程如圖所示。 4. 搜索功能: 搜索功能指的是通過關(guān)鍵詞或使用者的描述,得到目標(biāo)數(shù)據(jù)的過程。這一功能原型圖如圖所示。 這一部分的難點(diǎn)在于通過用戶的描述,搜索到目標(biāo)數(shù)據(jù)的過程。這個過程的基本流程如圖所示。 項目開發(fā) 1. 數(shù)據(jù)庫建立 數(shù)據(jù)庫部分主要對相關(guān)的表和表之間的關(guān)系進(jìn)行建立。 首先需要創(chuàng)建項目所必須的表: CREATE DATABASE `album`; CREATE TABLE `album`.`tags` ( `tid` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(255) NOT NULL , `remark` TEXT NULL , PRIMARY KEY (`tid`)) ENGINE = InnoDB; CREATE TABLE `album`.`category` ( `cid` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(255) NOT NULL , `sorted` INT NOT NULL DEFAULT '1' , `user` INT NOT NULL , `remark` TEXT NULL , `publish` DATE NOT NULL , `area` VARCHAR(255) NULL , PRIMARY KEY (`cid`)) ENGINE = InnoDB; CREATE TABLE `album`.`users` ( `uid` INT NOT NULL AUTO_INCREMENT , `nickname` TEXT NOT NULL , `wechat` VARCHAR(255) NOT NULL , `remark` TEXT NULL , PRIMARY KEY (`uid`)) ENGINE = InnoDB; CREATE TABLE `album`.`photo` ( `pid` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(255) NOT NULL , `small` VARCHAR(255) NOT NULL , `large` VARCHAR(255) NOT NULL , `category` INT NOT NULL , `tags` VARCHAR(255) NULL , `remark` TEXT NULL , `creattime` DATE NOT NULL , `creatarea` VARCHAR(255) NOT NULL , `user` INT NOT NULL , PRIMARY KEY (`pid`)) ENGINE = InnoDB; CREATE TABLE `album`.`photo_tags` ( `ptid` INT NOT NULL AUTO_INCREMENT , `tag` INT NOT NULL , `photo` INT NOT NULL , `remark` INT NULL , PRIMARY KEY (`ptid`)) ENGINE = InnoDB; 創(chuàng)建之后,逐步添加表之間的關(guān)系以及部分限制條件: ALTER TABLE `photo_tags` ADD CONSTRAINT `photo_tags_tags_alter` FOREIGN KEY (`tag`) REFERENCES `tags`(`tid`) ON DELETE CASCADE ON UPDATE RESTRICT; ALTER TABLE `photo_tags` ADD CONSTRAINT `photo_tags_photo_alter` FOREIGN KEY (`photo`) REFERENCES `photo`(`pid`) ON DELETE CASCADE ON UPDATE RESTRICT; ALTER TABLE `photo` ADD CONSTRAINT `photo_category_alter` FOREIGN KEY (`category`) REFERENCES `category`(`cid`) ON DELETE CASCADE ON UPDATE RESTRICT; ALTER TABLE `photo` ADD CONSTRAINT `photo_user_alter` FOREIGN KEY (`user`) REFERENCES `users`(`uid`) ON DELETE CASCADE ON UPDATE RESTRICT; ALTER TABLE `category` ADD CONSTRAINT `category_user_alter` FOREIGN KEY (`user`) REFERENCES `users`(`uid`) ON DELETE CASCADE ON UPDATE RESTRICT; ALTER TABLE `tags` ADD unique(`name`); 2. 讓 Code 飛起來 在使用之前您需要有一個騰訊云的賬號,并且開通了 SCF 、COS 、APIGW 以及 CDB 等相關(guān)產(chǎn)品權(quán)限; 將項目 clone 到本地,配置自己的密鑰信息、數(shù)據(jù)庫信息。配置文件在 cloudFunction 目錄下的 serverless.yaml 中: # 函數(shù)們的整體配置信息 Conf: component: "serverless-global" inputs: region: ap-shanghai runtime: Python3.6 handler: index.main_handler include_common: ./common mysql_host: gz-c************************.com mysql_user: root mysql_password: S************************! mysql_port: 6************************0 mysql_db: album mini_program_app_id: asdsa************************dddd mini_program_app_secret: fd340c4************************8744ee tencent_secret_id: AKID1y************************l1q0kK tencent_secret_key: cCoJ************************FZj5Oa tencent_appid: 1256773370 cos_bucket: 'album-1256773370' domain: album.0duzahn.com 由于我目前使用的是 Serverless Components,沒有全局變量等。所以在此處增加了全局變量組件,在這里設(shè)置好全局變量,在之后的 Components 中可以直接引用,例如: # 創(chuàng)建存儲桶 CosBucket: component: '@serverless/tencent-website' inputs: code: src: ./cos region: ${Conf.region} bucketName: ${Conf.cos_bucket} 安裝必備工具:Serverless Framework 、小程序云開發(fā) IDE 。由于本項目后臺開發(fā)語言是 Python,您也需要一些 Python 的開發(fā)工具以及包管理工具( Python 版本不低于 3.6 ) 在部分文件夾下安裝相對應(yīng)的依賴: cloudFunction/album/prdiction 需要安裝 Pillow, opencv, tensorflow, jieba cloudFunction/album/getPhotoSearch 需要安裝 gensim, jieba 以及 collections cloudFunction/album/compression 需要安裝 Pillow (注意,在安裝的時候一定要用 CentOS 操作系統(tǒng)。如果沒相對應(yīng)系統(tǒng),可以在這里打包對應(yīng)的依賴: http://serverless.0duzhan.com/app/scf_python_package_download/) 將項目部署到云端,只需要通過指令 serverless --debug 即可: DEBUG ─ Resolving the template's static variables. DEBUG ─ Collecting components from the template. DEBUG ─ Downloading any NPM components found in the template. DEBUG ─ Analyzing the template's components dependencies. DEBUG ─ Creating the template's components graph. DEBUG ─ Syncing template state. DEBUG ─ Executing the template's components graph. DEBUG ─ Starting API-Gateway deployment with name APIService in the ap-shanghai region ... ... DEBUG ─ Updating configure... DEBUG ─ Created function Album_Get_Photo_Search successful DEBUG ─ Setting tags for function Album_Get_Photo_Search DEBUG ─ Creating trigger for function Album_Get_Photo_Search DEBUG ─ Deployed function Album_Get_Photo_Search successful DEBUG ─ Uploaded package successful /Users/dfounderliu/Documents/code/AIAlbum/.serverless/Album_Prediction.zip DEBUG ─ Creating function Album_Prediction DEBUG ─ Updating code... DEBUG ─ Updating configure... DEBUG ─ Created function Album_Prediction successful DEBUG ─ Setting tags for function Album_Prediction DEBUG ─ Creating trigger for function Album_Prediction DEBUG ─ Trigger timer: timer not changed DEBUG ─ Deployed function Album_Prediction successful Conf: region: ap-shanghai ... ... - path: /photo/delete method: ANY apiId: api-g9u6r9wq - path: /album/delete method: ANY apiId: api-b4c4xrq8 - path: /album/add method: ANY apiId: api-ml6q5koy 156s ? APIService ? done 這個過程,只用了 156s 便部署了所有函數(shù)。然后打開小程序的 id 帶入 miniProgram 目錄,并且填寫自己的 appid 在文件 project.config.json 的第 17 行,同時也要配置自己項目的基礎(chǔ)目錄,就是 API 網(wǎng)關(guān)給我們返回的地址,寫在 app.js 的第 10 行,此時項目就可以運(yùn)行起來了。 自取 ?? 后臺的壓縮包 小結(jié) Serverless 架構(gòu)憑借著按量付費(fèi)、低成本運(yùn)維和高效率開發(fā)等眾多優(yōu)點(diǎn)于一身,幫助我們的項目快速開發(fā)和迭代。而 Serverless Framework 則是一個非常高效的工具,兼容 Tencent Cloud, AWS, Google Cloud 等多家廠商的 Serverless 架構(gòu)。 本項目以 騰訊云 Serverless Framework 為例,詳細(xì)信息可以移步 官方說明 。 參考: GitHub: github.com/serverless 官網(wǎng): serverless.com 社區(qū): Serverless 中文網(wǎng) 前言 實事求是地說,Serverless Framework 的 Components 真的好用,原先使用 SCF CLI 和 VSCode 插件部署騰訊云函數(shù)盡管也方便,但也只能部署云函數(shù)。 假如我有靜態(tài)資源,想配置 CDN,想綁定域名,或者其他更多的操作......可能都離不開控制臺。但是 Serverless Framework 的 Components 幾乎可以讓我暫時告別控制臺。對這樣的一個工具,我真的 respect ! 然而就在我嘗試使用 Components 做稍微大一點(diǎn)的項目,遇到了兩個不算問題的問題,但也著實讓人抓狂。 Component 沒有全局變量; Component 不能單獨(dú)部署; 再進(jìn)行下文閱讀前,可以先了解這些背景知識: Serverless Component 是什么,我怎樣使用它? 如何開發(fā)自己的第一個 Serverless Component 全局變量組件 如果只有一個組件需要部署,例如下面這個 Yaml,那么全局變量存在的意義的確不大。 hello_world: component: "@serverless/tencent-website" inputs: code: src: ./public index: index.html error: index.html region: ap-shanghai bucketName: hello_world 但是實際生產(chǎn)中,一個 Yaml 中會寫很多的部分。 例如我的 Blog 的 Yaml: https://github.com/anycodes/ServerlessBlog/blob/master/serverless.yaml 。這里面共有十幾個函數(shù),如果沒有全局變量的話,那可能真的是噩夢。 比方有 10 個函數(shù),這些函數(shù)都要部署在 ap-guangzhou 。部署完成之后,我又要把它們部署到 ap-shanghai 區(qū),如果沒有全局變量,就要修改十幾個函數(shù)的配置。即使批量替換修改,也可能出現(xiàn)問題。所以,此時若有全局變量的組件,就顯得尤為重要。 為此,我貢獻(xiàn)了這樣一個組件:serverless-global 。通過這個組件,我們可以設(shè)置一些全局變量,在程序中使用: Conf: component: "serverless-global" inputs: region: ap-shanghai mysql_host: gz-cdb-mytest.sql.tencentcdb.com mysql_user: mytest mysql_password: mytest mysql_port: 62580 mysql_db: mytest Album_Login: component: "@serverless/tencent-scf" inputs: name: Album_Login codeUri: ./album/login handler: index.main_handler runtime: Python3.6 region: ${Conf.region} environment: variables: mysql_host: ${Conf.mysql_host} mysql_port: ${Conf.mysql_port} mysql_user: ${Conf.mysql_user} mysql_password: ${Conf.mysql_password} mysql_db: ${Conf.mysql_db} 通過 serverless-global,我們可以定義一些全局的公共參數(shù),并且通過變量的方法引用這些參數(shù),例如 ${Conf.region} 就是引用 Conf-inputs 中定義的 region 變量。期待 Serverless 團(tuán)隊在未來能支持全局變量的功能。 單獨(dú)部署組件 還是 Serverless Blog 這個例子,里面有多個模塊,包括十幾個函數(shù)、API 網(wǎng)關(guān)以及 Website 等。初次部署真的爽歪歪+美滋滋:一鍵部署就是爽! 但是,當(dāng)我修改其中的某個函數(shù),僅僅修改了一個配置信息,我再執(zhí)行 sls --debug 部署的時候,它竟然又為我重新部署了一次!部署一次約 10min,可我僅僅修改了一行代碼。雖說不是什么大問題,但體驗也不如人意:為什么 Component 沒有指定部署某個資源的功能? 我猜想:如果可通過某個參數(shù),來控制我要部署那個資源,該有多好? 例如:我用命令 sls --debug -n website 可以只部署 website,而不是全部資源再跑一次部署,那多方便啊!于是我思前想后,通過簡單的幾行代碼,實現(xiàn)了一套非常簡單的 Component: 是的,我就是在官方 Component 上層,嵌套了一個 tempComponent 。使用方法很簡單,例如,有這么一個 website 的部分: test1: component: "@serverless/tencent-website" inputs: code: src: ./public index: index.html error: index.html region: ap-shanghai bucketName: test1 把里面 component 的名字改一下,改成 @ gosls : test1: component: "@gosls/tencent-website" inputs: code: src: ./public index: index.html error: index.html region: ap-shanghai bucketName: test1 這樣就變成了支持部署單個組件的 component 了,并且所有騰訊云的組件都可以通過修改前面的前綴進(jìn)行變化,如果不想用了,可以隨時修改回 @ serverless ,下面的 inputs 的內(nèi)容和格式,和官方的一模一樣,直接轉(zhuǎn)發(fā)給對應(yīng)的 @ serverless /tencent-website 。例如: # serverless.yml test1: component: "@gosls/tencent-website" inputs: code: src: ./public index: index.html error: index.html region: ap-shanghai bucketName: test1 test2: component: "@gosls/tencent-website" inputs: code: src: ./public index: index.html error: index.html region: ap-shanghai bucketName: test2 test3: component: "@gosls/tencent-website" inputs: code: src: ./public index: index.html error: index.html region: ap-shanghai bucketName: test3 執(zhí)行 sls --debug : DFOUNDERLIU-MB0:website_test dfounderliu$ sls --debug DEBUG ─ Resolving the template's static variables. DEBUG ─ Collecting components from the template. DEBUG ─ Downloading any NPM components found in the template. DEBUG ─ Analyzing the template's components dependencies. ..... DEBUG ─ Website deployed successfully to URL: http://test2-1256773370.cos-website.ap-shanghai.myqcloud.com. DEBUG ─ Website deployed successfully to URL: http://test3-1256773370.cos-website.ap-shanghai.myqcloud.com. test1: url: http://test1-1256773370.cos-website.ap-shanghai.myqcloud.com env: test2: url: http://test2-1256773370.cos-website.ap-shanghai.myqcloud.com env: test3: url: http://test3-1256773370.cos-website.ap-shanghai.myqcloud.com env: 19s ? test1 ? done 可以看到完成了三個的部署,當(dāng)我使用參數(shù),執(zhí)行部署 test2 的時候: DFOUNDERLIU-MB0:website_test dfounderliu$ sls --debug -n test2 DEBUG ─ Resolving the template's static variables. DEBUG ─ Collecting components from the template. DEBUG ─ Downloading any NPM components found in the template. DEBUG ─ Analyzing the template's components dependencies. DEBUG ─ Creating the template's components graph. ...... DEBUG ─ Uploading directory /Users/dfounderliu/Desktop/ServerlessComponents/test/website_test/public to bucket test2-1256773370 DEBUG ─ Website deployed successfully to URL: http://test2-1256773370.cos-website.ap-shanghai.myqcloud.com. test1: test2: url: http://test2-1256773370.cos-website.ap-shanghai.myqcloud.com env: test3: 6s ? test3 ? done 可以看到,通過 -n 參數(shù),只部署了 test2,其他的組件沒有發(fā)生任何變化。 目前這個功能支持絕大部分 Tencent 官方提供的組件( https://github.com/gosls ): @serverless/tencent-apigateway @serverless/tencent-cam-policy @serverless/tencent-cam-role @serverless/tencent-cdn @serverless/tencent-cos @serverless/tencent-egg @serverless/tencent-express @serverless/tencent-flask @serverless/tencent-koa @serverless/tencent-laravel @serverless/tencent-scf @serverless/tencent-website 快來開始動手吧~ 傳送門: GitHub: github.com/serverless 官網(wǎng): serverless.com 社區(qū): Serverless 中文網(wǎng) |