騰訊雲新產品「雲開發」嘗鮮體驗

  • 2019 年 11 月 27 日
  • 筆記

雲開發(Tencent Cloud Base,TCB)是騰訊云為移動開發者提供的一站式後端雲服務,可以看作是雲函數SCF的升級版。

作為推進serverless的急先鋒,SCF提供了彈性的計算能力,也能通過api網關提供彈性的接入能力,但是SCF服務裡面不包括後台資料庫,因此雖然你可以通過SDK連接到雲資料庫,但是始終要多關心一個資料庫Server,這讓我們在說起serverless時,有時顯得不夠原教旨主義。

而TCB給開發者提供了一個免費和幾乎免維護的的k/v資料庫(可以付費升級),這就讓原教旨主義的無伺服器開發的有了更多的可玩性。此外還很貼心的提供了容易上手的web端、小程式端和伺服器端端SDK,讓玩轉TCB的門檻大大降低。尤其是有一套包裝好的伺服器端推送的SDK實現(見 資料庫web端SDK 的最後一段《資料庫實時推送》),可以很方便的開發實時應用。

由於閑閑還在美國幼兒園「留學」,這幾天想復刻一個異地互動的flash遊戲letters,正好可以用得上這兩個特性。

letters是一個很傻的遊戲,任何人都可以訪問網站看到當前的字母排列,然後可以在螢幕上拖動字母,其他正在訪問網頁的人會即時看到字母被拖走了,因此也可以多人合作來把字母排列成特定的樣子,或者相互搞破壞偷走別人的字母讓他拼不成想要的樣子。原遊戲基於flash實現,現在已經難以懷舊了。重新開發這個遊戲除了實現字母拖放互動之外,還想支援現代瀏覽器和平板,同時還想允許把一個字母換成別的字母,在換掉的時候別人的瀏覽器上也要實時顯示出變化。

實現上很簡單:

首先是實現一些基礎的頁面上的拖放邏輯,併兼容pad。為了兼容不同的顯示設備和不同的瀏覽器大小,字體要隨著螢幕寬度縮放,而位置全部要採用百分比定位。這是純前端部分的工作,可以在文末的連接中跳轉到遊戲網頁上查看源碼。

然後就是需要捕獲到每次拖放介紹的事件,把被拖放的字母的新位置發送給後端的雲函數,這部分的程式碼很簡單,拼湊一個http post請求吧要發送的數據丟到body裡面就好。

function updateLetters(letter) {  	let url = "https://service-j3t56zj5-1258721286.ap-shanghai.apigateway.myqcloud.com/release/"; //雲開發  	if (letter && !isNaN(letter.id)) {  		let body = {  			"id": letter.id  		}  		if (!isNaN(letter.x) && !isNaN(letter.y) && (letter.x != map[letter.id].x || letter.y != map[letter  				.id].y)) {  			console.log("發出:" + map[letter.id].char + "從(" + map[letter.id].x + "," + map[letter.id].y +  				")移到(" + letter.x + "," + letter.y + ")")  			body.x = letter.x;  			body.y = letter.y;  			map[letter.id].x = letter.x;  			map[letter.id].y = letter.y;  		}  		if (letter.char && letter.char.length > 0) {  			var d = document.createElement("span");  			d.innerHTML = letter.char;  			var s = d.innerText;  			if (s.length > 0 && s.length < 4) {  				body.char = s;  			}  			$("b" + letter.id).innerHTML = map[letter.id].char = s;  		}  		fetch(url, {  			cache: "no-cache",  			method: 'POST',  			headers: {  				'Accept': 'application/json',  				'Content-Type': 'application/json'  			},  			body: JSON.stringify(body)  		}).then(function(response) {  			return response.json()  		}).then(function(json) {  			if (isNaN(json.errorCode)) {  				console.log("發送成功")  			} else {  				console.log(JSON.stringify(json))  			}  		}).catch(function(ex) {  			console.log('parsing failed', ex)  		})  	}  }

後端的服務程式也很簡單,接收到更新的字母后往資料庫裡面寫。不過要考慮到第一個進來玩的人由於資料庫里什麼都沒有,要生成一點原始數據給他玩。

'use strict';  const app = require("tcb-admin-node");  const chars = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9","?","?","?","?","?","?","?","?","?","?","❤️","?","?"]    exports.main = async (event, context, callback) => {      console.log("nevent.body:"+event.body)      let parameters = JSON.parse(event.body),id=-1,x=-1,y=-1,c='';      if(!!parameters){          if(!isNaN(parameters["id"])){              id=parseInt(parameters["id"]);          }          if(!isNaN(parameters["x"])){              x=parameters["x"]-0;          }          if(!isNaN(parameters["y"])){              y=parameters["y"]-0;          }          if(parameters["char"] && parameters["char"].length<3){              c=parameters["char"]          }      }      app.init({env: 'game-acebe0'})      const db = app.database();      const _ = db.command      let collection = db.collection('game');      let all = collection.where({char:_.neq('')})      let data = (await all.orderBy("_id", "asc").get()).data      if(!data){          await db.createCollection("game")          collection = db.collection('game');          data = [];      }      if(data.length<chars.length){          var dataHash = {};          for(var i=0;i<data.length;i++){              dataHash[data[i]["_id"]]=true;          }          for(let i=0;i<chars.length;i++){              if(!dataHash[i]){                  let t = {                      _id:i,                      char:chars[i],                      x:6*(i%15)+5,                      y:7*Math.floor(i/15),                      color:Math.floor(Math.random()*256*256*256)                  }                  collection.add(t)                  data.push(t)              }          }      }      if(id>=0 && id<chars.length){//id 合法          let query =collection.where({"_id":id-0})          if(x>=0 && x<=100 && y>=0 && y<=100 && c.length==0){ //坐標合法             let now = Date.now();              let q=await query.update({"x":x,"y":y})              console.log("n 更新時間"+(Date.now()-now)+"毫秒,更新了"+q.updated+"條記錄")              data[id]["x"]=x              data[id]["y"]=y          }          if(c && c.length>0){              console.log("change char")              await query.update({"char":c})          }      }      return data  }

TCB的雲函數和SCF的一樣可以通過api-gateway提供http/https接入能力,只是不像SCF那樣可以直接在雲函數裡面添加api-gateway,而是需要在同服務區的api-gateway裡面去吧cloud function綁定到api的後端配置中。如果不熟悉api-gateway配置也可以直接吧TCB的雲函數程式碼粘貼到SCF的編輯器裡面當普通的雲函數配置運行,然後就可以在函數服務的觸發方式裡面添加api-gateway並一步到位的發布服務了。TCB的伺服器SDK在SCF中不但工作良好,而且beta用戶還可以啟用依賴自動安裝這樣的黑科技,連開發的「伺服器」都不需要,真正實現原教旨主義的serverless。唯一要注意的是TCB和SCF的雲函數入口有exports.main_handler 和 exports.main 的寫法差異。

這裡我遇到了雲開發的第一個大坑:寫初始數據的時候無法批量寫入。這就導致我只能一條一條的寫入,而很多條數據如果串列寫入的話等待資料庫返回的時間就會被累加,任何網路抖動都會導致整個函數超時退出。如果並行寫入的話會超過免費用戶可用的資料庫連接數上限導致部分寫失敗。剛開始我每次檢查到資料庫的初始數據不完整就重新初始化,然後又一次大量的寫入導致部分失敗……10分鐘不到就把一整天的寫操作限額用光了。

一個填坑方案是等待雲開發的批量寫入介面放出來,不過我想到了 《雲原生應用的「十二要素」》中大家一再強調的一個原則:

面向失敗的設計:

……大多數雲計算的基礎設施天生就是短暫的,與本地環境的硬體相比,也更容易出現故障。結合我們大多數人在設計分散式系統時所遵循的原則,你必須設計出能夠容忍服務消失或者被重新部署的系統。

是的,我們可以並且應該在設計上讓系統容忍這個錯誤並繼續良好運作。具體講,我先一股腦把所有的初始數據同步丟進去寫資料庫然後基於部分成功的結果讓系統運作起來,然後在下次函數運行的時候看看哪些操作寫失敗了,再補寫一下。通常補寫兩三次以後就全部都成功了。

在寫操作完成後,需要讓所有的網頁端偵聽到資料庫發生了什麼變化,並把這些變化用動畫表現到網頁上。參考SDK文檔,很容易就用webSDK實現了網頁端到偵聽:

fetch("http://service-1c18mywx-1258721286.ap-shanghai.apigateway.myqcloud.com/release/").then(  	(response) => {  		return response.json()  	}).then((json) => {  	ticket = json.ticket;  	return auth.signInWithTicket(ticket)  }).then(() => {  	// 登錄成功  	const db = app.database();  	const _ = db.command  	const collection = db.collection('game');  	let ref = collection.where({  		_id: _.gt(-1)  	}).watch({  		onChange: snapshot => {  			var docs = snapshot.docChanges;  			if (docs.length > 0) {  				show(docs);  			} else {  				console.log("獲取初始化數據失敗")  			}  		},  		onError: error => {  			console.log("收到error", error)  		}  	})  })

這裡有個小坑,是偵聽需要伺服器端通過下發一個ticket來授權。為此需要寫一個ticket生成的雲函數並通過API網關把它發布出去:

'use strict';  const tcb = require('tcb-admin-node');  exports.main = (event, context, callback) => {      tcb.init({      // ...      env: 'game-acebe0',      credentials: {"private_key_id":".........................",      "private_key":"-----BEGIN RSA PRIVATE KEY-----n...............n-----END RSA PRIVATE KEY-----n"}      });      let customUserId = Math.floor(Math.random()*10000000).toString();    const ticket = tcb.auth().createTicket(customUserId, {    refresh: 10 * 60 * 1000 // 每十分鐘刷新一次登錄態, 默認為一小時  });  return {"ticket":ticket};  };

因為這是個開放對匿名遊戲,所以沒有對授權做什麼限制,誰都可以申請都ticket來獲取伺服器數據,但是這樣有DDoS隱患。還好用的是免費服務,如果被攻擊導致訪問量超過限額,服務會自動終止,危害有限。

此外對資料庫許可權也要限制為「所有用戶可讀,僅管理員可寫」,這樣稍微提升一點點幹壞事的門檻(雖然還是誰都可以通過雲函數匿名寫,但是能寫的欄位和寫的方式收到程式碼邏輯的限制)。

完整了授權和客戶端登錄,在網頁上就可以偵聽到所有人的拖動和字元修改操作並且做相應的渲染了。至此一個遊戲的邏輯大體完成。網頁端發補到了gitee page上:http://stonelf.gitee.io/letter/letter.html 需要看完整的前端程式碼只要查看頁面源碼就可以了。

總結一下過程中遇到的坑和吸取的經驗教訓:

1 雲開發暫時還沒有批量寫的能力,大量數據不管是順序寫還是並行寫都容易失敗,這不能通過全量重新寫來解決。

——老祖宗教導我們,要面向失敗做設計

2 雲開發的雲函數沒有打通api-gateway來提供http服務,要自己去同服務區的api-gateway中綁定雲函數,或者把雲函數放到SCF中去。

3 web前端的server-push比自己主動輪詢後台數據要更及時,而且可以提供大得多的免費並發訪問能力,而且,還不用耗費api-gateway和雲函數的訪問和執行限額。

4 web前端的SDK能力很強大,需要配合資料庫的許可權配置來限制訪問者進行意想之外的寫操作。

Tags: