magic 发布的文章

Bmob FaaS 或“函数即服务” 介绍(2)

上一篇讲过Bmob在国内最早做这个的企业之一,当时还完全未有借鉴,所以与目前腾讯与阿里云有所不同。

他们(腾讯与阿里云)2家的服务设置与使用都非常相似,从2017年同时推出公测起都是奔着亚马逊的Lambda 的功能来的,只是2家函数代码互不兼容,展现形式一致。

1. 阿里云界面

image.png

image.png

2. 腾讯云界面

image.png

image.png

腾讯云.png

阿里云.png

3. 共同点

  1. 函数入口
  2. 函数内存
  3. 超时时间

    可以看出这几点都是相同的,由此看出他们的技术架构都来自同一套类似的东西然后对其定制加入自家云内部接口函数

我们的云逻辑发展历程

发展历程.png

第一阶段:多台机器负载一个云逻辑环境

特点第一阶段第二阶段第三阶段第四阶段
说明1. 无启动时间,开箱即用
2. 架构相对简单,易扩展,维护方便
1. 无启动时间,开箱即用
2. 函数按照用户分文件夹
3.热点代码写入内存,读取速度快
1.根据业务规则划分服务器区快,例如id 1-100的用户在服务器一组(解决了一个人影响所有人,不过还是会影响他所在的服务区域)1.完全隔离,资源独立
问题1.函数多的时候磁盘读取慢1.由于执行环境没隔离,高并发用户会影响低请求用户逻辑1.隔离有限,还是会出现个别时候一个函数影响其他1.基础成本高
2.开发成本高
3.需要预备一些硬件资源
特点1.简单快捷,云函数支持有限1.云函数支持数是第一方案的N倍1.不再一个函数影响所有1.独立运行无限扩展

自主创新走了一些弯路,在第二阶段升级到第三阶段的时候,我们直接走了第四阶段通过kubernetes+docker容器技术来实现集群编排,通过Go来暴露接口给前端应用灵活购买云函数, 续费,用户自主升级配置、github代码拉取等等。

最终由于云函数是免费功能,大部分用户只是少量使用云函数。Serverless本身最求小而美的低成本策略,高额基础成本、维护成本导致当时这块功能全部弃用,重新进入第二阶段,经过优化走出了第三阶段这条技术线路。

得益于一些游戏应用厂家对云函数有更高的要求,我们推出了第四阶段与阿里腾讯今年推出的方案有些类似,用户独立环境,只要有需求,无限资源扩展。根据实验以单秒API并发来控制实例个数。

云函数是如何工作的?

image.png

云函数分为三种调用形式

  1. SDK调用
  2. API调用
  3. 相互调用
  4. 定时调用

架构上要实现灵活扩展扩展云函数,需要一套具有动态路由器来管理函数、容器、超时、动态扩展、功能接口等,我们使用的是OpenRestyOpenResty在优酷、淘宝、360等中大型互联网企业都有应用。

在云函数这块,不久的将来我们将推出Java版本云函数,为开发者的需求,我们也在不断创新,推进中国移动互联网软件开发效率。希望未来软件开发服务端真如Amazon.com首席技术官Werner Vogels所说:"没有服务器比不管理服务器更容易管理"。

回顾

上一篇文章讲了微信小程序准备工作,并建立了小程序信箱数据表

信箱功能

第一步:在微信小程序开发工具Pages建立信箱文件


mail/
├── mail.js
├── mail.json
├── mail.wxml
└── mail.wxss

1. mail.js

  1. 页面显示时查询数据
  2. 把查询的数据setData到模版页面

//index.js
//获取应用实例
var common = require('../template/getCode.js')
var Bmob = require("../../utils/bmob.js");
var that;
var molist;
var app = getApp()
Page({
  data: {
    moodList: [],
    limit: 6,
    loading: false,
    windowHeight1: 0,
    windowWidth1: 0,
    scrollTop: {
      scroll_top1: 0,
      goTop_show: false
    }
  },
  onLoad: function (e) {
    that = this;
    that.setData({
      loading: false
    })

  },
  //页面显示时执行
  onShow: function () {
    molist = new Array();
    var myInterval = setInterval(getReturn, 500);//半秒定时查询
    //获取服务端数据
    function getReturn() {
      wx.getStorage({
        key: 'user_id',
        success: function (ress) {//根据User_id查询
          if (ress.data) {
            clearInterval(myInterval)//清楚定时查询
            var Diary = Bmob.Object.extend("Diary");
            var query = new Bmob.Query(Diary);
            query.equalTo("is_hide", "1");
            query.descending("createdAt");
            query.include("publisher");
            // 查询所有数据
            query.find({
              success: function (results) {//数据查询成功
                that.setData({
                  loading: true
                });
                //数据重新初始化
                for (var i = 0; i < results.length; i++) {
                  var publisherId = results[i].get("publisher").id;
                  var title = results[i].get("title");
                  var content = results[i].get("content");
                  var id = results[i].id;
                  var createdAt = results[i].createdAt;
                  var _url;
                  var likeNum = results[i].get("likeNum");
                  var commentNum = results[i].get("commentNum");
                  var pic = results[i].get("pic");
                  if (pic) {
                    _url = results[i].get("pic")._url;
                  }
                  else {
                    _url = null;
                  }
                  var name = results[i].get("publisher").get("nickname");
                  var userPic = results[i].get("publisher").get("userPic");
                  var liker = results[i].get("liker");
                  var isLike = 0;
                  for (var j = 0; j < liker.length; j++) {
                    if (liker[j] == ress.data) {
                      isLike = 1;
                      break;
                    }
                  }
                  var jsonA;
                  if (pic) {
                    jsonA = '{"title":"' + title + '","content":"' + content + '","id":"' + id + '","avatar":"' + userPic + '","created_at":"' + createdAt + '","attachment":"' + _url + '","likes":"' + likeNum + '","comments":"' + commentNum + '","is_liked":"' + isLike + '","username":"' + name + '"}'
                  }
                  else {
                    jsonA = '{"title":"' + title + '","content":"' + content + '","id":"' + id + '","avatar":"' + userPic + '","created_at":"' + createdAt + '","likes":"' + likeNum + '","comments":"' + commentNum + '","is_liked":"' + isLike + '","username":"' + name + '"}';
                  }
                  var jsonB = JSON.parse(jsonA);
                  //每条数据重新写入到数组
                  molist.push(jsonB)
                  that.setData({
                    moodList: molist,
                    loading: true
                  })
                }
              },
              error: function (error) {
                common.dataLoading(error, "loading");
                that.setData({
                  loading: true
                })
                console.log(error)
              }
            });

          }

        },
        fail: function (error) {
          console.log("失败")
        }
      })
    }

  },
  //分享页面
  onShareAppMessage: function () {
    return {
      title: '心邮',
      desc: '倾诉烦恼,邮寄心情,分享快乐',
      path: '/pages/index/index'
    }
  },

  onPullDownRefresh: function () {
    wx.stopPullDownRefresh()
  },
  scrollTopFun: function (e) {
    if (e.detail.scrollTop > 300) {
      this.setData({
        'scrollTop.goTop_show': true
      });
    } else {
      this.setData({
        'scrollTop.goTop_show': false
      });
    }
  },
  goTopFun: function (e) {
    var _top = this.data.scrollTop.scroll_top1;//发现设置scroll-top值不能和上一次的值一样,否则无效,所以这里加了个判断  
    if (_top == 1) {
      _top = 0;
    } else {
      _top = 1;
    }
    this.setData({
      'scrollTop.scroll_top1': _top
    });
    this.onShow();
  }
})

2. mail.wxml写入

循环输出数据wx:for-items="{{moodList}}"

<loading hidden="{{loading}}">
  加载中...
</loading>
<import src="../template/list.wxml" />
<!--<scroll-view  lower-threshold="800" bindscrolltolower="pullUpLoad" upper-threshold="0" scroll-y="true" style="height: {{windowHeight1}}px; width: {{windowWidth1}}px; " scroll-top="{{scrollTop.scroll_top1}}" bindscroll="scrollTopFun">-->
<view class="index_list">
  <navigator url="../listDetail/listDetail?moodId={{item.id}}" hover-class="navigator-hover" wx:for-items="{{moodList}}" wx:key="moodListId" wx:if="{{item.id}}">
    <template is="listHeader" data="{{userPic:item.avatar,userNick:item.username,publishTime:item.created_at}}" />
    <template is="listTemp" data="{{listPic:item.attachment,listTitle:item.title,listContent:item.content}}" />
    <view class="list_footer">
      <view class="agreeNum agree_{{item.is_liked}}">{{item.likes}}</view>
      <view class="commNum">{{item.comments}}</view>
    </view>
  </navigator>
</view>
<!--</scroll-view>-->
<view class="bord"></view>


<view class="returnTop"  wx:if="{{scrollTop.goTop_show}}" catchtap="goTopFun">
</view>

第二步:在微信小程序开发工具更改配置文件 app.json


加入信箱栏目

第三步:在管理后台数据库添加一些数据


Bmob小程序后端添加数据

微信小程序展示

至此微信小程序信箱功能已经完成

我们可以看到这里主要的代码就是页面显示查询出数据,写在onshow里面。模版页面无非是展示出来,下面贴一段查询数据的源码。

  var Diary = Bmob.Object.extend("表名称");
    var query = new Bmob.Query(Diary);
    query.get("4edc3f6ee9", {
      success: function(result) {
        // The object was retrieved successfully.
        console.log("该日记标题为"+result.get("title"));
      },
      error: function(result, error) {
        console.log("查询失败");
      }
    });

Bmob后端云 公众号邀请返利开发

APP里有个注册页面,希望互相转发微信,邀请返利。 为了方便传播,注册页面单独用H5页面实现。 为了提高邀请用户质量,提高邀请用户的有效性,我们强制要求微信里面打开才能注册。所以基于公众号开发。 为了快速开发,这里使用Bmob云Serverless 里的Faas服务与文件CDN存储服务。

1.准备材料

  1. 公众号
  2. Bmob云Serverless
  3. 如果想用自己域名,备案好的域名

2.设置公众号

一、设置AppSecret值

开发者ID(AppID)

开发者密码(AppSecret)

二、设置白名单

IP白名单
image.png

三、设置授权域名

接口权限,登陆,找到网页授权域名

四、设置调试微信开发者

开发者工具-》web开发者工具 【这个跟小程序一样的工具】,添加微信到开发者,最多可以绑定50个
image.png

3.开发过程

一、注册Bmob账号

二、创建一个应用

image.png

三、点击进入云函数

编写微信获取用户昵称代码
image.png

四、新建云函数

云函数getOpenId 获取用户openid,拿到openid调用getUserInfo获取用户信息云函数

function onRequest (request, response, modules) {

  var code = request.body.code ? request.body.code : '011jOWiN0vmFa42ynziN0B6bjN0jOWir'

  //获取数据库对象
  var db = modules.oData;
  var functions = modules.oFunctions;

  //http请求方式: GET
  var appid = '';
  var secret = '';
  var url = 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=' + appid + '&secret=' + secret + '&code=' + code + '&grant_type=authorization_code';
  //获取Http模块
  var http = modules.oHttp;

  //发起Get请求
  http(url, function (error, res, body) {
    if (!error && res.statusCode == 200) {
      //写入数据库
      var resultObject = JSON.parse(body);
      //如果返回错误则打印
      if (resultObject.errcode) {
        response.send(resultObject);
      }

      functions.run({
        "name": "getUserInfo",
        "data": { "openid": resultObject.openid, "access_token": resultObject.access_token }
      }, function (err, data) {
        //回调函数
        
     
        response.send(data);
      });

    }
  });

}                                                                                    

getUserInfo云函数

function onRequest (request, response, modules) {
  var db = modules.oData;
  var openid = request.body.openid || 'ol2Ey0tePs0fdFdVRZKqzu50_83c';
  var access_token = request.body.access_token || 'xxxx';

  getUserInfoMp(openid, access_token);

  function getUserInfoMp (openid, access_token) {
    var http = modules.oHttp;
    http('https://api.weixin.qq.com/sns/userinfo?access_token=' + access_token + '&openid=' + openid + '&lang=zh_CN', function (error, res, body) {
      if (!error && res.statusCode == 200) {
        var at = JSON.parse(body);
        response.send(body);

      }
      response.send(body);
    
  });
}
}                                                                                    

五.运行流程介绍

image.png

用户打开一个url,这个url打开后会提示用户是否授权,同意后跳转到自己的网址,并带上code。

(URL地址)https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxbafdda995607cc&redirect_uri=https%3a%2f%2fgoldenage.xxxxx.com%2f&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect

redirect_uri参数关键:指定统一授权后打开的网址,接下来就1步,通过code拿到openid。上面函数getOpenId就可以拿到openid,如果需要昵称头像,可以调用第二个云函数getUserInfo

https://mp.weixin.qq.com/wiki?action=doc&id=mp1421140842&t=0.35600785609885244#3)

六.如何调用

image.png

https://cloud.bmob.cn/19b475f87c366db2/getOpenId
方法:post
参数:code: 微信code
返回:
{"openid":"ol2Ey0tePs0fdFdVRZKqzu50_83c","nickname":"magic","sex":1,"language":"zh_CN","city":"广州","province":"广东","country":"中国","headimgurl":"http:\/\/thirdwx.qlogo.cn\/mmopen\/vi_32\/DYAIOgq83epySXbnQqg4MialjffKNKbxj42prvL2aIaIFbs3pj7svlT4gbQeQPbricZNBGYWfv1zcOo86fjwc99Q\/132","privilege":[]}}

总结:

相比自己服务器开发优势,成本低,速度快。 基于Serverless,的Faas 只需要一个函数,即可实现用户openid功能。如果熟悉,整个微信开发不需要10分钟。这里有个用户点击授权的页面是HTML的,这个HTML我们可以用CDN云存储,把静态html页面保存到文件里面,支持绑定自己域名,让用户访问。 整个开发,不需要购买服务器,ip,环境,微信设置好,一个函数,暴露接口就可以实现。

前端网页调用效果截图:
image.png

好久没更新mongo运维这块知识了,这次介绍 db.currentOp与db.killOp命令

数据库优化方法有很多,但所有数据库优化都离不开慢查询优化。mysql、mongodb都可以开启慢查询,来对数据库查询进行分析。开启慢查询日志,对性能会有一定的影响。mongoDB 有时我们只想临时看下慢查询日志,应该如何处理。

这时,我们可以用到mongdb的db.currentOp命令 ,他可以列出当前真正跑的op相关信息。

1.查看目前正在执行的所有查询语句

db.currentOp();

2.查询所有操作xxx集合并且执行时间已超过3s的请求

db.currentOp(
   {
     "active" : true,
     "secs_running" : { "$gt" : 3 },
     "ns" : /^xxx\./
   }
)

3. 当然,如果我当前db集群有非常多的集合,我也可以不限制xxx集合

db.currentOp(
   {
     "active" : true,
     "secs_running" : { "$gt" : 3 }
   }
)

currentOp的过滤条件包括

  1. 请求操作类型,insert、update、delete…
  2. 请求对应的connectionId,threadId
  3. 请求是否正在等待锁
  4. 请求执行时间
  5. 请求操作的DB或collection
  6. 请求query的内容
  7. …等等

返回结果如下

{
            "desc" : "conn44266",
            "threadId" : "140419266524928",
            "connectionId" : 44266,
            "client" : "10.10.68.209",
            "active" : true,
            "opid" : 4495651,
            "secs_running" : 25,
            "microsecs_running" : NumberLong(25459008),
            "op" : "command",
            "ns" : "xxxxx",
            "query" : {
                "count" : "c74dc2de71",
                "query" : {
                    "video" : {
                        "$exists" : true
                    },
                    "_isdel" : 0,
                    "deleted" : {
                        "$ne" : true
                    },
                    "verify" : {
                        "$ne" : false
                    }
                }
            },
            "planSummary" : "IXSCAN { video: 1 }",
            "numYields" : 189,
            "locks" : {
                "Global" : "r",
                "Database" : "r",
                "Collection" : "r"
            },
            "waitingForLock" : false,
            "lockStats" : {
                "Global" : {
                    "acquireCount" : {
                        "r" : NumberLong(380)
                    }
                },
                "Database" : {
                    "acquireCount" : {
                        "r" : NumberLong(190)
                    }
                },
                "Collection" : {
                    "acquireCount" : {
                        "r" : NumberLong(190)
                    }
                }
            }
        }

结果

当我们知道某条语句是锁库的罪魁祸首的时候,我们就可以通过另一条语句,干掉对应的请求。

killOp 停止正在执行的查询

用法:

db.killOp(opid)

目前Mongodb手册,还未有一次清掉当前所有查询,文档地址:https://docs.mongodb.com/manual/reference/method/db.killOp/

执行后返回

{ "info" : "attempting to kill op", "ok" : 1 }

image.png

db.killOp(opid)的实现原理如下

每个连接对应的服务线程存储了一个killPending的字段,当发送killOp时,会将该字段置1;请求在执行过程中,可以通过不断的调用OperationContext::checkForInterrupt()来检查killPending是否被设置,如果被设置,则线程退出。

一个请求要支持killOp,必须在请求的处理逻辑里加上checkForInterrupt()检查点才行,否则即使发送了killOp,也只能等待请求完全处理完毕线程才会退出。

比如createIndex的处理逻辑里包含了类似如下的代码,在createIndex的循环过程中,一旦killPending被置1了,createIndex的执行可以在当前循环结束时退出。

while (!createIndexFinished) {
    createIndexForOneElement();
    checkForInterupt();
}

所以发送killOp后,请求要执行到下一个『检查点』线程才会退出,MongoDB在很多可能耗时长的请求中,都加入了checkForInterrupt()检查点,如创建索引,repair database,mapreduce、aggregation等。

批量一次清楚当前慢查询

上面说还未有一次清掉当前所有查询, 不过我们可以通过手动写脚本实现。此脚本由diggzhang大神贡献。文章链接:http://yangcongchufang.com/kill-mongo-ops.html

实现功能:传入自己的IP地址,强制关停自己的异常查询。

打开家目录下的.mongorc.js拷贝下面的killMyRunningOps函数进去,重新打开mongoshell即可加载这个函数(mongoshell启动时会预读这个文件)。

➜  ~ cat ~/.mongorc.js
killMyRunningOps = function (clientIp) {
    var currOp = db.currentOp();
    for (op in currOp.inprog) {
        if (clientIp == currOp.inprog[op].client.split(":")[0]) {
            db.killOp(currentOp.inprog[op].opid)
        }
    }
}

用法很简单,知道自己IP后,调用这个函数:

> killMyRunningOps("12.23.32.21")

(1) 安装Shadowsocks和Chrome



brew install shadowsocks-libev
brew cask install google-chrome 

(2) 安装Chrome插件Proxy SwitchySharp



https://chrome.google.com/webstore/detail/proxy-switchysharp/dpplabbmogkhghncfbfdeeokoefdjegm 

(3) 配置



emacs /usr/local/etc/shadowsocks-libev.json

修改下面的配置
 { "server":"", "server_port":, "local_port":, "password":"", "timeout":600, "method":"aes-256-cfb" }

服务器请到https://get发邮件获取,配置完成后是如下:
 { "server":"hongxchen.github.io", "server_port":1080, "local_port":8104, "password":"abc", "timeout":600, "method":"aes-256-cfb" }
 注意:method字段一定要小写 

(4) 配置Proxy SwitchySharp



1. 添加新的的配置New Profile 2. Profile Name:Shadowsocks 3. 选择Manual Configuration 4. 在SOCKS Host中填入127.0.0.1和shadowsocks-libev.json中设置的local_port,如8104。HTTP Proxy,HTTPS Proxy,FTP Proxy空着不填。 5. 选择SOCKS v5 6. 切换到 Switch Rules 7. 勾选Enable Switch Rules 8. 勾选Online Rule List,在Rule List URL中填入**list.googlecode.com/svn/trunk/**list.txt,Proxy Profile选择Shadowsocks 9. 勾选AutoProxy Compatible List 

(5) 测试



终端执行
/usr/local/opt/shadowsocks-libev/bin/ss-local -c /usr/local/etc/shadowsocks-libev.json

打开Chrome,Proxy SwitchySharp选择Auto Switch Mode
打开www.youtube.com等被墙网站,如果不能打开则https://get发邮件获取其他服务的的配置 

(6) 开机启动



Ctrl+C关闭在终端中打开执行的ss-local

制作/usr/local/opt/shadowsocks-libev/homebrew.mxcl.shadowsocks-libev.plist文件的软连接到~/Library/LaunchAgents
% ln -s /usr/local/opt/shadowsocks-libev/homebrew.mxcl.shadowsocks-libev.plist ~/Library/LaunchAgents/homebrew.mxcl.shadowsocks-libev.plist 

(7) 加载配置文件



launchctl load ~/Library/LaunchAgents/homebrew.mxcl.shadowsocks-libev.plist ** 卸载为launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.shadowsocks-libev.plist 

(8) 完成



到此配置完成,以后开机打开Chrome就能够翻出墙外,.shadowsocks.net/get提供的服务器在一段时间后可能不能连接,只需关闭服务在shadowsocks-libev.json文件中写入新的配置项,重新启动服务就行了。

launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.shadowsocks-libev.plist emacs /usr/local/etc/shadowsocks-libev.json launchctl load ~/Library/LaunchAgents/homebrew.mxcl.shadowsocks-libev.plist 

转: