得物技术

得物技术 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

得物技术 发布了文章 · 3月5日

得物技术介绍WebRTC入门

简介

WebRTC (Web Real-Time Communications) 是Google于2010购买的一项实时通讯技术,能够在浏览器内部进行实时音频和视频通信,目前支持所有主流浏览器及移动设备。

在不通过中间媒介的情况下,它允许网络应用或者站点建立浏览器之间p2p(Peer-to-Peer)的连接,实现视频流和音频流或者其他任意数据的传输。

为了做到不通过中间媒介这点,一个典型的 WebRTC 通信过程,包含了找到对方,进行协商,建立连接,开始通讯。

可以通过appr.tc体验下简单的webrtc应用:

  1. 在浏览器中打开appr.tc
  2. 点击Join进入聊天室并允许摄像头和麦克风权限
  3. 分享连接给其他人或者使用其他设备打开连接进入聊天室。

如何开发一个webRTC应用

webRTC相关的API

  • MediaStream 一个媒体内容的流。一个流包含几个轨道,比如视频和音频轨道。
  • RTCPeerConnection 代表一个由本地计算机到远端的WebRTC连接。该接口提供了创建,保持,监控,关闭连接的方法的实现。

webRTC建立步骤

  • 获取视频流,音频流。
  • 获取网络信息,例如:ip地址、端口号,用以和其他用户相连接。
  • 连接到信令服务器启动或关闭会话并报告错误信息。(信令后文会提到)
  • 交换媒体及客户端功能等信息,例如:分辨率、帧率、编解码器。
  • 互相传输视频流,音频流。

获取本地音视频流

// 需要开启https,并保证设备完好
<template>
  <div id="app" class="oa">
    <div>
      <button id="start">开始</button>
    </div>
    <div>
      <video id="stream" autoplay controls></video>
    </div>
  </div>
</template>
<script>
export default {
  name: 'App',
  mounted() {
    // 通过true or false 控制 音视频
    const constraints = { audio: true, video: false }
    const startBtn = document.getElementById('start')
    const video = document.getElementById('stream')

    startBtn.onclick = function () {
      window.navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
        video.srcObject = stream
        window.stream = stream
      })
        .catch(function (err) {
          console.log(err)
        })
    }
  },
}
</script>

关于信令及SDP

信令

虽然webRTC不需要经过服务器进行点到点的通信,但是在开始通信之前,必须知道对方的存在,这个时候就需要信令服务器。

信令服务器简单的讲就是用来让客户端交换元数据来协调通信的东西,是一个帮助双方建立链接的中间人角色。

信令服务器除了在寻找对等端阶段,标识与验证参与者的身份外,还负责传递一些特定信息,例如房间号,双方的信息等,除此之外还会交换会话控制消息用于打开或关闭通信,错误消息,媒体元数据。

SDP

会话描述协议(Session Description Protocol)是一个描述多媒体连接内容的协议,例如分辨率,格式,编码,加密算法等,主要用来描述多媒体会话,用途包括会话声明、会话邀请、会话初始化等。所以在数据传输时两端都能够理解彼此的数据。本质上,这些描述内容的元数据并不是媒体流本身。

下面列举了一个SDP并通过属性名的说明能清晰知道每个属性名的作用。

v=0 
o=alice 2890844526 2890844526 IN IP4 host.anywhere.com 
s= 
c=IN IP4 host.anywhere.com 
t=0 0 
m=audio 49170 RTP/AVP 0 
a=rtpmap:0 PCMU/8000 
m=video 51372 RTP/AVP 31 
a=rtpmap:31 H261/90000 
m=video 53000 RTP/AVP 32 
a=rtpmap:32 MPV/90000

总结

之前有做过和webrtc相关的项目,但时间一长相关知识也有了些生疏。本文只是简单介绍了下webrtc的入门知识,也是自己对此的梳理巩固。

参考

[1]. https://developer.mozilla.org...

[2]. https://cloud.tencent.com/dev...

文|sheen16

关注得物技术,携手走向技术的云端

查看原文

赞 1 收藏 0 评论 0

得物技术 发布了文章 · 2月26日

得物技术Web Components初探

简介

在我们的日常工作中,'组件'一词很是熟悉。无论是在react、vue项目中,都经常会去封装一些自定义个性化的组件,来达到某些特定的功能,但是这些组件都需要外部模块的支持。Web Components API便提供了一种方式,在不依赖外部模块的情况下封装自定义的组件。

Web Components API 介绍

它由三项主要技术组成:

  • Custom elements(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们。
  • Shadow DOM(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML templates(HTML模板): template 和 slot 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

Custom elements(自定义元素)

自定义元素 Custom elements 是 Web components 技术的核心。

CustomElementRegistry

CustomElementRegistry:包含自定义元素相关功能。

CustomElementRegistry.define()方法可以用来注册新的自定义元素。

window.customElements返回的就是CustomElementRegistry对象的引用。

注意点:

  • 自定义元素类的基类是 HTMLElement ,所以继承了 HTML 元素的特性。也可以继 HTMLElement 的子类,比如 HTMLDivElement 、 HTMLTableElement 、 HTMLButtonElement 等。
  • 自定义元素标签的名称必须包含连字符 - ,用来和内置的HTML标签做区别。
  • 浏览器如果没有解析到自定义元素,会当做空的 div 元素处理。
class MyComponent extends HTMLElement {
  constructor() {
    super();
    // ...
  }
}
// 注册一个自定义元素
window.customElements.define('my-component', MyComponent);

创建自定义内置元素的扩展

is 属性:HTML的全局属性,使用 is 属性可以把一个HTML的内置属性标记成一个已注册的自定义内置元素。

class NewButton extends HTMLButtonElement {
  // ...
}
// 注册时,提供元素的扩展
window.customElements.define('new-button', NewButton, { extends: 'button' })

// 使用时
<button is="new-button">NewButton</button>

生命周期

自定义组件的特殊回调函数:

class MyComponent extends HTMLElement {
  constructor() {
    super()
    
  }
  
  connectedCallback(){
    // 当自定义元素第一次被连接到文档DOM时被调用
  }
  disconnectedCallback(){
    // 当自定义元素与文档DOM断开连接时被调用
  }
  adoptedCallback(){
    // 当自定义元素被移动到新文档时被调用
  }
  attributeChangedCallback(){
    // 当自定义元素的一个属性被增加、移除或更改时被调用
  }
}

自定义方法和属性

自定义元素就是javascript的类,因此自定义元素的方法和属性的用法和class一样。

class MyComponent extends HTMLElement {
  constructor() {
    super()
    
  }
  // 自定义方法
  hello() {
    const name = this.getAttribute('name')
    console.log('hello' + name)
  }
  
  // 也可以设置取值器和赋值器
  set name(name) {
    const oldName = this.getAttribute('name')
    if(name !== oldName) {
      this.setAttribute('name', name)
    }
  }
  get name() {
    return this.getAttribute('name')
  }
}
window.customElements.define('my-component', MyComponent);

// 使用
<my-component name="zhangsan"></my-component>

const el = document.querySelector('my-component');
el.hello();

css伪类

  • :defined :匹配任何已定义的元素,包括内置元素和自定义的元素。
  • :host :Shaow Host,组自定元素挂载的节点。

Shadow DOM(影子DOM)

如果我们希望自定义元素的内部代码不允许被外部访问到,我们可以设置Shadow DOM来将其与外部隔离,这样外部的设置无法影响到其内部,而内部的设置也不会影响到外部。

Shadow DOM 特有的术语:

  • Shadow host:一个常规 DOM节点,Shadow DOM 会被附加到这个节点上。
  • Shadow tree:Shadow DOM内部的DOM树。
  • Shadow boundary:Shadow DOM结束的地方,也是常规 DOM开始的地方。
  • Shadow root: Shadow tree的根节点。

Shadow DOM 最大的好处:

  • 向用户隐藏细节,直接提供组件。
  • 可以封装内部样式表,不会影响到外部。

例子:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>Web Components</title>
  <style>
    .btn {
      color: #000;
      font-size: 30px;
    }
  </style>
</head>
<body>
<button class="btn">获取shadowRoot</button>
<my-component></my-component>
</body>
<script>
 class MyComponent extends HTMLElement {
  constructor() {
    super();
    var con = document.createElement('div');
    con.classList.add('btn');
    con.innerHTML = "自定义元素";

    let style = document.createElement('style');
    style.innerText = '.btn { font-weight: 600; color: red;}';

    this.appendChild(style);
    this.appendChild(con);
  }
}
window.customElements.define('my-component', MyComponent); 
</script>
</html>

不使用shadow Dom

在不使用shadow Dom的时,内外部样式相互影响,优先级受选择器和加载顺序影响。

使用shadow Dom

使用shadow Dom的时,内外部样式互不影响。

class MyComponent extends HTMLElement {
  constructor() {
    super();
    // 使用 shadow dom
    var shadow = this.attachShadow( { mode: 'open' } );
    var con = document.createElement('div');
    con.classList.add('btn');
    con.innerHTML = "自定义元素";

    let style = document.createElement('style');
    style.innerText = '.btn { font-weight: 600; color: red;}';

    shadow.appendChild(style);
    shadow.appendChild(con);
  }
}

mode设置

通过设置 mode 为 open 或 closed 能够控制是否可以在外部访问到组件的shadowRoot。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>Web Components</title>
  <style>
    .btn {
      color: #000;
      font-size: 30px;
    }
  </style>
</head>
<body>
<button class="btn">获取shadowRoot</button>
<my-component></my-component>
</body>
<script>
 class MyComponent extends HTMLElement {
  constructor() {
    super();
    var shadow = this.attachShadow( { mode: 'open' } );
    var con = document.createElement('div');
    con.classList.add('btn');
    con.innerHTML = "自定义元素";

    let style = document.createElement('style');
    style.innerText = '.btn { font-weight: 600; color: red;}';

    shadow.appendChild(style);
    shadow.appendChild(con);
  }
}
window.customElements.define('my-component', MyComponent); 

const btn = document.querySelector('.btn')
btn.addEventListener('click',() => {
  const el = document.querySelector('my-component');
  // 获取 shadowRoot
  console.log(el.shadowRoot)
})
</script>
</html>

当 mode:'open' 时,点击获取shadowRoot按钮:

当 mode:'closed' 时,点击获取shadowRoot按钮:

HTML templates(HTML模板)

template模版

上文中在JavaScript中撸dom、style是很麻烦的一件事。Web Components API提供了 template 标签,包含一个HTML片段,不会在文档初始化时渲染。但是可以在运行时使用JavaScript显示。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
</head>
<body>
<my-component
  title="这是标题"
  content="这是内容这是内容这是内容这是内容这是内容这是内容"
></my-component>
  
<template id="template">
  <style>
   :host {
     display: flex;
     align-items: center;
     width: 450px;
     height: 180px;
     background-color: #ccc;
     border-radius: 8px;
   }
   .container {
     margin: 20px;
     height: 150px;
   }
   .container > .title {
     font-size: 24px;
     line-height: 30px;
     margin-bottom: 15px;
   }
   .container > .content {
     padding: 10px;
     font-size: 14px;
   }
  </style>
  
  <div class="container">
    <p class="title"></p>
    <p class="content"></p>
  </div>
</template>

</body>
<script>
class MyComponent extends HTMLElement {
  constructor() {
    super();
    var shadow = this.attachShadow( { mode: 'closed' } );
    
    var template = document.getElementById('template');
    var content = template.content.cloneNode(true);
    content.querySelector('.container>.title').innerText = this.getAttribute('title');
    content.querySelector('.container>.content').innerText = this.getAttribute('content');

    shadow.appendChild(content);
  }
}
window.customElements.define('my-component', MyComponent);
</script>
</html>

Slot

slot> 元素,也叫插槽。作为Web Components技术的一部分,是Web组件内的一个占位符。该占位符可以在后期使用自己的标记语言填充,这样您就可以创建单独的DOM树,并将它与其它的组件组合在一起。插槽也分为默认插槽和具名插槽。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
</head>
<body>
<my-component>
  <div slot="left">left</div>
  <div slot="right">right</div>
  <div>content</div>
</my-component>
<template id="template">
  <style>
   :host {
     display: flex;
     align-items: center;
     width: 480px;
     height: 180px;
     background-color: #ccc;
     border-radius: 8px;
   }
   .container {
     display: flex;
   }
   .container > .con {
     height: 100px;
     width: 100px;
     flex-grow: 1;
     font-size: 24px;
   }
  </style>
  
  <div class="container">
    <p class="con">
      <slot name="left"></slot>
    </p>
    <p class="con">
      <slot></slot>
    </p>
    <p class="con">
      <slot name="right"></slot>
    </p>
  </div>
</template>

</body>
<script>
class MyComponent extends HTMLElement {
  constructor() {
    super();
    var shadow = this.attachShadow( { mode: 'closed' } );
    var template = document.getElementById('template');
    var content = template.content.cloneNode(true);
    shadow.appendChild(content);
  }
}
window.customElements.define('my-component', MyComponent);
</script>
</html>

参考文献

MDN Web Components:
https://developer.mozilla.org...

文|zhousibao
关注得物技术,携手走向技术的云端

查看原文

赞 1 收藏 1 评论 0

得物技术 发布了文章 · 2月19日

得物技术浅谈Keep-alive原理及业务解决方案

背景:在 B 端系统中,为了使用方便我们会在页面设计中加上标签页类似浏览器上方标签页的功能,为了使用体验更加接近浏览器标签页,我们需要针对路由进行缓存。本文主要介绍 Vue 项目针对不同业务场景如何利用 keep-alive 来实现标签页动态缓存。

关于 keep-alive

keep-alive 是一个抽象组件,不会和子组件建立父子关系,也不会作为节点渲染到页面上。

关于抽象组件 Vue 的文档没有提这个概念,它有一个属性 abstract 为 true,在抽象组件的生命周期过程中,我们可以对包裹的子组件监听的事件进行拦截,也可以对子组件进行 Dom 操作,从而可以对我们需要的功能进行封装,而不需要关心子组件的具体实现。除了kepp-alive还有<transition>、<transition-group>等。

作用

  • 能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
  • 避免反复渲染影响页面性能,同时也可以很大程度上减少接口请求,减小服务器压力。
  • 能够进行路由缓存和组件缓存。

Activated

keep-alive 的模式下多了 activated 这个生命周期函数, keep-alive 的声明周期执行:

  • 页面第一次进入,钩子的触发顺序

created-> mounted-> activated,退出时触发 deactivated 当再次进入(前进或者后退)时,只触发 activated。

  • 事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated 中。

keep-alive解析

渲染

keep-alive 是由 render 函数决定渲染结果,在开头会获取插槽内的子元素,调用 getFirstComponentChild 获取到第一个子元素的 VNode。

const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)

接着判断当前组件是否符合缓存条件,组件名与 include 不匹配或与 exclude 匹配都会直接退出并返回 VNode,不走缓存机制。

// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
  return vnode
}

匹配条件通过会进入缓存机制的逻辑,如果命中缓存,从 cache 中获取缓存的实例设置到当前的组件上,并调整 key 的位置将其放到最后(LRU 策略)。
如果没命中缓存,将当前 VNode 缓存起来,并加入当前组件的 key。如果缓存组件的数量超出 max 的值,即缓存空间不足,则调用 pruneCacheEntry 将最旧的组件从缓存中删除,即 keys[0] 的组件。之后将组件的 keepAlive 标记为 true,表示它是被缓存的组件。

LRU 缓存策略:从内存中找出最久未使用的数据置换新的数据.算法根据数据的历史访问记录来进行淘汰数据,其核心思想是如果数据最近被访问过,那么将来被访问的几率也更高。
const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
  vnode.componentInstance = cache[key].componentInstance
  // make current key freshest
  remove(keys, key)
  keys.push(key)
} else {
  cache[key] = vnode
  keys.push(key)
  // prune oldest entry
  if (this.max && keys.length > parseInt(this.max)) {
    pruneCacheEntry(cache, keys[0], keys, this._vnode)
  }
}

pruneCacheEntry 负责将组件从缓存中删除,它会调用组件 $destroy 方法销毁组件实例,缓存组件置空,并移除对应的 key。

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

渲染总结

  • 通过 getFirstComponentChild 获取第一个子组件,获取该组件的 name;
  • 通过 include 与 exclude 属性进行匹配,判断当前组件是否要被缓存,如果匹配成功;
  • 命中缓存则直接获取,同时更新 key 的位置;
  • 不命中缓存则设置进缓存,同时检查缓存的实例数量是否超过 max, 超过则根据 LRU 策略删除最近最久未使用;
  • 如果在中途有对 include 和 exclude 进行修改,通过 watch 来监听 include 和 exclude,在其改变时调用 pruneCache 以修改 cache 缓存中的缓存数据。

基于 keep-alive 缓存实现方案

方案一:整个页面缓存

一般采用在 router 的 meta 属性里增加一个 keepAlive 字段,然后在父组件或者根组件中,根据 keepAlive 字段的状态使用 keep-alive 标签,实现对路由的缓存:

<keep-alive>
    <router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />

方案二:动态组件缓存

使用 vuex 配合 exclude 和 include,通过 include 和 exclude 决定那些组件进行缓存。注意这里说的是组件,并且 cachedView 数组存放的是组件的名字,如下:

<keep-alive :include="$store.state.keepAlive.cachedView">
    <router-view></router-view>
</keep-alive>

场景分析

在 SPA 应用中用户希望在 Tab 多个页面来回切换的时候,不要丢失查询的结果,关闭后清除缓存。

如下图:

期望是用户在切换 Tab 时 页面时缓存的,当用户关闭 Tab ,重新从左侧菜单打开时是不缓存。

路由缓存方案

这样是持久缓存了整个页面,问题也就出现当用户通过 Tab 关闭页面然后从左侧打开菜单时是缓存的页面,这个不符合日常使用习惯,所以为了解决数据新鲜度的问题可以在 activated 触发查询请求就能保证数据的新鲜度。

activated(){
 getData()
}

但是使用后发现由于你切换 Tab 时每次都会请求数据,但是如果项目的数据量有很大频繁请求会给后端造成很大压力 。

动态组件缓存方案

版本一需要频繁拉去数据导致此方案已不合适只能动态缓存组件方案。

<keep-alive :include="cachedViews">
  <router-view :key="key"></router-view>
</keep-alive>

其中 cachedViews 是通过监听路由动态增加删除维护要缓存的组件名称(所以组件名称不要重复)数组:

const state = {
  cachedViews: [],
}
const mutations = {
  ADD_VIEWS: (state, view) => {
    if (state.cachedViews.includes(view.name)) return
    state.cachedViews.push(view.name)
  },
  DEL_CACHED_VIEW: (state, view) => {
    const index = state.cachedViews.indexOf(view.name)
    index > -1 && state.cachedViews.splice(index, 1)
  },
}
const actions = {
  addCachedView({ commit }, view) {
    commit('ADD_VIEWS', view)
  },
  deleteCachedView({ commit }, view) {
    commit('DEL_CACHED_VIEW', view)
  },
}
export default {
  namespaced: true,
  state,
  mutations,
  actions,
}

通过监听路由变化:

watch: {
    '$route'(newRoute) {
      const { name } = newRoute
      const cacheRout = this.ISCACHE_MAP[name] || []
      cacheRout.map((item) => {
        store.dispatch('cached/addCachedView', { name: item })
      })
    },
  },
当通过 Tab 关闭页面时清除组件名称:
closeTag(newRoute) {
   const { name } = newRoute
   const cacheRout = this.ISCACHE_MAP[name] || []
   cacheRout.map((item) => {
     store.dispatch('cached/deleteCachedView', { name: item })
   })
 }

但是在遇到嵌套路由时在层级不同的 router-view 中切换 Tab 会出现缓存数据失效的问题,无法缓存组件,嵌套路由如下:

如何解决?

  • 方案一:菜单嵌套,路由不嵌套

通过维护两套数据,一套嵌套给左侧菜单,一套扁平化后注册路由,改造后的路由:

  • 方案二:修改 keep-alive 把 catch 对象到全局

通过上面 keep-alive 解析可以知道,keep-alive就是把通过 include 匹配的组件的 vnode,放到 keep-alive 组件的一个 cache 对象中,下次渲染时,如果能在这里面找到,就直接渲染vnode。所以把这个 cache 对象,放到全局去(全局变量或者 vuex),这样我就可以不用缓存 ParnetView 也能缓存其指定的子组件了。

import Vue from 'vue'
const cache = {}
const keys = []
export const removeCacheByName = (name) => {/* 省略移除代码 */}
export default Object.assign({}, Vue.options.components.KeepAlive, {
  name: 'NewKeepAlive',
  created() {
    this.cache = cache
    this.keys = keys
  },
  destroyed() {},
})
  • 方案三:修改 keep-alive 根据路由 name 缓存

从上文可以知道 keep-alive 是从 cache 中获取缓存的实例设置到当前的组件上,key 是组件的名称,可以通过改造 getComponentName 方法,组件名称获取更改为路由名称使其缓存的映射关系只与 route name 值有关系。

function getComponentName(opts) {
  return this.$route.name
}

cache 缓存 key 也更改为路由名称。

参考链接

文|揣歪

关注得物技术,携手走向技术的云端

查看原文

赞 0 收藏 0 评论 0

得物技术 发布了文章 · 2月10日

和得物技术一起走进Web3D的世界(1) 画个立方体吧

需要了解一下的前置知识(下面是推荐阅读的链接)

GLSL github.com/wshxbqq/GLS…

Shader thebookofshaders.com/01/

矩阵 bk.tw.lvfukeji.com/wiki/%E7%9F…

齐次坐标 bk.tw.lvfukeji.com/wiki/%E9%BD…

最终效果

step1:建立webgl渲染上下文

这个就是简单的获取dom然后获取上下文 (注意下这里因为是画3d所以要开启深度检测)

const canvasDom = document.getElementById('canvas')
gl = canvasDom.getContext('webgl')

//开启深度检测
gl.enable(gl.DEPTH_TEST)

step2:创建顶点着色器与片元着色器

关于着色器shader是一个超级大的话题(推荐看TheBooksOfShader,尴尬的是作者没写完)。

大致可以这么理解:

• 顶点着色器确定了画布上点的位置

• 3d世界中基础的几何图形是三角形,片元着色器代表了区域的表现形式

先看一下webgl的坐标系,z+轴是面向我们的视角:

截屏2021-02-10 上午11.07.24.png

下面这段是顶点着色器:

const Vertex = `
        attribute vec3 vPosition;
        void main() { 
            gl_PointSize = 1.0; 
            gl_Position =  mat4(1,0, 0, 0 ,0, 0.52, -0.85, 0, 0, 0.85, 0.52, 0, 0, 0, 0,1)*mat4(0.52,0, 0.85, 0 ,0, 1, 0, 0, -0.85, 0, 0.52, 0, 0, 0, 0,1)*vec4(vPosition, 1);
        }
    `

attribute : 只能存在于vertex shader中,一般用于保存顶点或法线数据,它可以在数据缓冲区中读取数据。

vec3 vPosition 定义了一个3维向量
因为3d空间一个点(x,y,z)

mat4(1,0, 0, 0 ,0, 0.52, -0.85, 0, 0, 0.85, 0.52, 0, 0, 0, 0,1)是一个齐次矩阵 表示绕y轴旋转45度

mat4(0.52,0, 0.85, 0 ,0, 1, 0, 0, -0.85, 0, 0.52, 0, 0, 0, 0,1)表示绕z轴旋转45度

这样我们才能看到3d的效果。

下面这个就是编译shader,固定套路记住就好了 (开发中不大会用原生手写webgl)

const vertexShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vertexShader, Vertex);
    gl.compileShader(vertexShader);

下面这段是片元着色器:

const Fragment = `
        #ifdef GL_ES
        precision highp float;
        #endif
        void main() {
            gl_FragColor = vec4(1.0,0,0,1.0);
        }
    `

表示的意思是画布上的颜色是红色 vec4(1.0,0,0,1.0) 然后也是固定套路:

const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fragmentShader, Fragment);
    gl.compileShader(fragmentShader);

step3:创建一个程序

记住就好,就是调用api

const program = gl.createProgram();

step4:链接程序与着色器

记住就好,就是调用api

gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);

step5:建立缓冲数据

cube是用来获取顶点坐标

剩下的都是套路,api就不展开了,可以去mdn上查阅

//cube是用来获取顶点坐标
function cube(size = 1.0) {
        const h = 0.5 * size;
        const vertices = [
            [-h, -h, -h],
            [-h, h, -h],
            [h, h, -h],
            [h, -h, -h],
            [-h, -h, h],
            [-h, h, h],
            [h, h, h],
            [h, -h, h],
        ];
        const positions = [];
        function quad(a, b, c, d, e, f, g, h) {
            [a, b, c, d, e, f, g, h].forEach((i) => {
                positions.push(vertices[i]);
            });
        }
        quad(0, 1, 1, 2, 2, 3, 3, 0);
        quad(4, 5, 5, 6, 6, 7, 7, 4);
        quad(1, 2, 2, 6, 6, 5, 5, 1);
        quad(0, 3, 3, 7, 7, 4, 4, 0);
        quad(0, 1, 1, 5, 5, 4, 4, 0);
        quad(3, 7, 7, 6, 6, 2, 2, 3);
        return { positions};
    }
    const geometry = cube(1.0);
    console.log(geometry)
    const points = new Float32Array(geometry.positions.flat());
    const bufferId = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
    gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);

step6渲染

const Position = gl.getAttribLocation(program, 'vPosition');//获取顶点着色器中的position变量的地址
gl.vertexAttribPointer(Position, 3, gl.FLOAT, false, 0, 0);//给变量设置长度和类型
gl.enableVertexAttribArray(Position);//激活这个变量

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.drawArrays(gl.LINES, 0, 48)

gl.drawArrays(gl.LINES, 0, 48)就是渲染的api

webgl中有7种图元 (表示怎么画)

gl.POINTS 孤立点 绘制一系列点

gl.LINES 绘制一系列单独线段。每两个点作为端点,线段之间不连接

gl.LINE_STRIP 连续线段 绘制一个线条。即,绘制一系列线段,上一点连接下一点

gl.LINE_LOOP 连续线圈 绘制一个线圈。即,绘制一系列线段,上一点连接下一点,并且最后一点与第一个点相连

gl.TRIANGLES 孤立三角形

gl.TRIANGLE_STRIP 三角带

gl.TRIANGLE_FAN 三角扇

0,48表示从0取48个点绘制

总结

上述过程就是一个完整的webgl绘画流程。

完整的代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D立方体</title>
</head>

<body>
    <canvas id='canvas' width="800" height="800"></canvas>
</body>
<script>

    // 第一步 创建webgl上下文
    const canvasDom = document.getElementById('canvas')
    gl = canvasDom.getContext('webgl')

    //开启深度检测
    gl.enable(gl.DEPTH_TEST)
    console.log(gl)

    // 第二步 创建顶点着色器与片元着色器
    const Vertex = `
        attribute vec3 vPosition;
        void main() { 
            gl_PointSize = 1.0; 
            gl_Position =  mat4(1,0, 0, 0 ,0, 0.52, -0.85, 0, 0, 0.85, 0.52, 0, 0, 0, 0,1)*mat4(0.52,0, 0.85, 0 ,0, 1, 0, 0, -0.85, 0, 0.52, 0, 0, 0, 0,1)*vec4(vPosition, 1);
        }
    `

    const vertexShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vertexShader, Vertex);
    gl.compileShader(vertexShader);

    const Fragment = `
        #ifdef GL_ES
        precision highp float;
        #endif
        void main() {
            gl_FragColor = vec4(1.0,0,0,1.0);
        }
    `

    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fragmentShader, Fragment);
    gl.compileShader(fragmentShader);

    //第三步 创建程序对象
    const program = gl.createProgram();

    // 第四步 链接程序与着色器
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);

    gl.linkProgram(program);
    gl.useProgram(program);

    //第五步 建立缓冲数据
    function cube(size = 1.0) {
        const h = 0.5 * size;
        const vertices = [
            [-h, -h, -h],
            [-h, h, -h],
            [h, h, -h],
            [h, -h, -h],
            [-h, -h, h],
            [-h, h, h],
            [h, h, h],
            [h, -h, h],
        ];

        const positions = [];
        function quad(a, b, c, d, e, f, g, h) {
            [a, b, c, d, e, f, g, h].forEach((i) => {
                positions.push(vertices[i]);
            });
        }

        quad(0, 1, 1, 2, 2, 3, 3, 0);
        quad(4, 5, 5, 6, 6, 7, 7, 4);
        quad(1, 2, 2, 6, 6, 5, 5, 1);
        quad(0, 3, 3, 7, 7, 4, 4, 0);
        quad(0, 1, 1, 5, 5, 4, 4, 0);
        quad(3, 7, 7, 6, 6, 2, 2, 3);

        return { positions};
    }

    const geometry = cube(1.0);

    console.log(geometry)

    const points = new Float32Array(geometry.positions.flat());
    const bufferId = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
    gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);

    // 第六步 渲染
    const Position = gl.getAttribLocation(program, 'vPosition');//获取顶点着色器中的position变量的地址
    gl.vertexAttribPointer(Position, 3, gl.FLOAT, false, 0, 0);//给变量设置长度和类型
    gl.enableVertexAttribArray(Position);//激活这个变量

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.drawArrays(gl.LINES, 0, 48)
</script>

</html>

文|alex
关注得物技术,携手走向技术的云端

查看原文

赞 0 收藏 0 评论 0

得物技术 发布了文章 · 2月5日

得物技术浅谈AB实验设计实现与分流算法

背景

在现实的产品设计场景中以及业务决策中,需要对方案进行决策。例如,App或网页端某个页面的某个按钮的颜色是用蓝色还是红色,是放在左边还是右边?传统的解决方案通常是集体表决或由某位Leader拍板,类似的选择还有很多,从概率上很难保证传统的选择策略每次都是有效的,而ABTest显然是一种更加科学的方法。

业务价值

研发视角

  • 先验性:采用流量分割与小流量测试的方式,先让线上部分小流量用户使用,来验证我们的想法,再根据数据反馈来推广到全流量,减少产品损失。
  • 并行性:我们可以同时运行两个或两个以上版本的试验同时去对比,而且保证每个版本所处的环境一致的,这样以前整个季度才能确定要不要发版的情况,现在可能只需要一周的时间,避免流程复杂和周期长的问题,节省验证时间。
  • 科学性:统计试验结果的时候,ABTest 要求用统计的指标来判断这个结果是否可行,避免我们依靠经验主义去做决策。

PM视角

  • 实验流程化,系统化,降低用户使用数据产品的门槛
  • 数据分析可视化,将通用指标系统化分析,降低手工分析频率;对实验结果做到智能解读,直接给出用户数据分析结果,降低用户数据理解门槛

核心概念

  1. 场景
    对应业务场景,场景之间完全独立,比如首页推荐瀑布流、金刚位等。

  2. 从属于场景,一个场景可以有多个桶,一个桶中可以多个实验,不同的桶之间流量是互斥的。

  3. 一类(种)实验的集合,从属于场景,一个场景可以有多个层,处于同一层的各实验之间流量互斥,各实验流量之和为总流量,处于不同层的各实验之间流量正交。
  4. 实验
    用来验证某个决定请求处理方式的功能或策略的一部分流量,通常用来验证某个功能或策略对系统指标(如PV/UV,CRT,下单转化率等)的影响。
  5. 流量
    指所有用户请求。
  6. 流量正交
    不同层之间流量分配方式完全独立,不会互相影响MECE。

实验建模

image.png

这里我们将用户流量分成了三份,分层桶、小流量桶和基准桶。其中:

  • 层和桶的数量支持扩展(理论上支持任意多个)
  • 层和桶之间没有关联关系
  • 实验处于层和桶的交叉部位
  • 每一层的hash因子不同(通过加盐实现)
  • 每一层和桶的交叉部位有一个基准实验,当没命中实验(流量没有完全利用)时,走基准实验

    分层桶类似于“测试环境”,由算法方较为自由的验证一些想法,因此管控比较松,在分层桶里我们允许多个实验组交叉的去验证不同的策略通路,以此来挑选更优的算法组合模式,如果某种组合模式经过初步验证,产生了更好的指标,就可以进入小流量桶。

    小流量桶类似于“预发环境”,我们将这个组合模式单独灌入新的一批流量,相当于排除干扰,再次double check下,如果确实产生了更好的指标,我们将其晋升为基准桶。

    基准桶类似于“生产环境”,作为一种稳定的算法策略持续使用。

    image.png

分流算法

进行分流算法的目的是将线上用户按照固定的流量比例分配到不同实验(桶)中,并且保持这种实验(桶)分配关系,以此来对照验证相关的指标是否有所好转,所以为了保持这种用户和实验(桶)的分配关系,我们使用了hash取模的方式将一个用户固定在了一个0到100的区间中,这样只要对应实验(桶)的区间没有变化,这个用户和实验(桶)的分配关系就不会变化。

所以我们的做法就是将每个实验(桶)的流量占比分配到一个0到100的区间中,根据用户id和每一层不同的hash因子组合进行hash,然后取模,余数落到哪个区间就取包含该区间的实验(桶)。

image.png

如上图所示,A实验流量占比30%,B实验30%,C实验40%,将它们分配到0到100的区间中,即A实验占[0,30),B实验占[30,60),C实验占[60,100),计算一个用户模为50则命中B实验。

如果不需要进行流量调整,这种模式能够很稳定的运行并且保持这种实验(桶)流量分配关系,但是如果进行流量调整,就会存在一些问题,比如此时我们将A实验减少15%,B实验不变,C实验增加15%,则A实验占[0,15),B实验占[15,45),C实验占[45,100),因为每个层的hash因子不变,相同的用户请求产生相同的模数,最后模为50会落入C实验的区间,如下图:

image.png

这样的结果在业务上是不可接受的,因为A和C实验流量的调整对B实验的用户进行了污染,导致本应该属于B实验的一部分用户却走到了实验C中,所以在这里我们进行一些调整,每次流量的调整只会调整它的邻边,即尽可能的减少流量调整对实验区间重新分配带来的影响,我们以上图的场景为例,进行改进后的AB算法的拆解。

image.png

如上图,算法第一步是优先在自己的区域内进行选择,A实验流量调整为15%,从自己的区域[0,30)中选取[0,15),B实验保持[30,60),C实验因为流量调整为55%,先把自己的区域选满,即[60,100)。

第二步是填补间隙,C实验因为还有15%没有填补,就把间隙[15,30)补上。

这样调整后,A和C实验的流量调整不会给B实验带来影响,原先B实验的用户调整后依然还是走B实验。然后还能让流量调整后原来A实验中的一半用户继续留存在A实验中,C实验原来的用户依然还是走C实验,尽可能减少了用户集变动给实验(桶)最终效果带来的影响。

依此类推,如果继续对上述实验集进行流量调整,A实验调整为25%,B实验调整为35%,C实验调整为40%,进行算法的拆解,如下图:

image.png

第一步是优先在自己的区域内进行选择,A实验因为流量调整为25%,先把自己的区域选满,即[0,15),B实验调整为35%,也是优先把自己的区域选满,即[30,60),C实验调整为40%,从自己的区域中选择[15,30)和[60,85)。

第二步填补间隙,A实验因为还有10%没有填补,就把间隙[85,95)补上,B实验因为还有5%没有填补,就把间隙[95,100)补上,最终形成上图的区间分布。

这样经过多次调整后,每个实验都尽可能的减少了自己区间的变动,进行相应的“多退少补”,保证自己用户的留存性,减少对实验指标的影响。

从上面的例子可以看出,经过多次的流量调整后,各个实验的区间分布会变得比较复杂,但是从使用者的角度看,他只需要关心每个实验所占的流量配比,不需要关心底层实验流量的区间分布情况(这块对他是黑匣子),因此不会增加使用者操作的难度。

系统设计

AB平台在系统设计上尽可能的减少了外部依赖和IO调用,将处理都尽量放在了本地内存中,如下图:

image.png

仅依赖了配置中心和数据库,然后上游流量的请求进入AB获取实验决策全部都是本地内存操作。其中重载配置过程如上图所示。

  • 后台用户编辑实验信息(①)
  • 进行相应的增删改操作修改DB(②)
  • 触发发布一个静态配置,如果该配置已存在则配置值加一(③)
  • 配置中心将该配置推送给所有AB机器(④)
  • 触发所有机器重新加载数据库配置进本地缓存(⑤)

这样上游的请求进入任意一台机器都是在本地内存进行处理,并且每台机器都是加载的最新的配置,大大提高了AB平台的处理能力降低了rt。

系统设计没有银弹,之所以这样设计是因为AB平台具有如下的特点:

1.弱一致性

依赖配置中心推送随着订阅机器的增加一定会有一些延迟,但是用户修改实验配置后对于即时生效并不是特别敏感,延迟几秒都是可以接受的。

2.缓存内容较少

本身实验的配置信息就比较少,哪怕未来实验数量达到万量级,单机内存中都完全足够存放。

3.读多写少

用户修改实验配置的次数远远小于上游调用AB平台获取决策的次数,所以对于配置中心的调用压力足够小,是在可预期范围内。

这样设计也存在一些缺陷:

1.数据库毛刺

随着机器数量的增加,用户每次修改实验配置后,所有机器都需要重新加载数据库配置进入本地缓存,每台机器都要触发场景、桶、层、实验、白名单等等的数据库查询,而且是所有机器瞬间一起的查询,在机器数达到百台千台以上的时候会对数据库造成较大的瞬时查询压力,造成查询超时甚至是更严重的问题,不过可以通过一些方式去缓解:

  • 随机时间等待,每台机器在收到配置变更后随机等待一个短暂的时间将所有机器加载数据库的时间点错开一些。
  • 数据库缓存,每次修改完数据库后都主动的触发一次缓存加载,不过这里缓存要谨慎使用,如果触发缓存加载失败,此时去触发配置中心推送,机器会加载到脏数据导致实验配置修改没有生效,所以修改数据库和主动触发缓存加载一定要在一个事务中。
  • sql优化,每次重新加载所有配置信息本身sql不复杂,加索引带来的提升也不是特别提效,因为配置信息达到MB级别的时候,对于网络开销也是较大的,然后机器数量上去后,网络IO吞吐会很大,所以可以修改为查询最近一段时间内发生变化的配置信息,这样可以大大减少查询的压力。当然如果将推送版本号改为推送修改的id然后单独加载id也是可行的,不过要考虑到多个用户并发修改不同的实验配置导致配置中心合并配置带来的推送丢失问题,之所以推送版本号也是考虑到这个问题。

2.推送效率

随着订阅机器数的增加,推送rt的增加是必然的,当然这个缺陷是在可接受范围内的。

3.僵尸节点

如果机器因为网络问题失联导致配置项推送失败会造成实验配置修改没有完全生效的问题,会造成一些诡异的现象,不过概率较低,只能依赖运维平台的各种端口检测来提前发现处理。

文|尉迟繁缕

关注得物技术,携手走向技术的云端

查看原文

赞 0 收藏 0 评论 0

得物技术 发布了文章 · 1月29日

无侵入式mock平台在得物的实践

一、概述

1.1 背景介绍

作为测试应该都遇到过如下两大痛点:

1.只想测试被测系统A,却需要从依赖系统开始一层层造自己想要的测试数据,造数花费时间长,边界值及异常场景不好模拟。

2.接口自动化,UI自动化,埋点自动化因为服务或者测试数据的不稳定性导致自动化维护成本高。

要解决上述问题,基本都会想到mock。目前市面上有很多优秀的开源mock框架:Mockito、PowerMock、EasyMock、JMockit等,但这些框架对于我们现在的业务场景及主要是在集成测试过程中使用,显然不是我们想要的。因为我们希望在不改动开发代码的情况下支持随心所欲的构造mock接口的返回报文来测试不同的业务场景,基于这种外部依赖服务走http形式的技术架构,一套无侵入式的mock平台应运而生。

hulk是一个无侵入式的http mock平台,支持客户端代理,从网关层mock,支持后端服务之间的mock。支持返回报文函数配置,并且具备放行逻辑。未来还将支持filter,根据不同的入参返回不同的mock数据。

1.2 系统架构

基于Django + mitmproxy + vue + MongoDB + MySQL

目前整个技术架构比较简单,mock服务基于Django框架开发,代理层主要是在开源框架mitmproxy基础上做了二次开发打通和mock系统的交互,前端配置平台使用了公司的脚手架poizon-cli。数据存储主要用了MongoDB和MySQL,提高性能后续会考虑引入redis,将配置信息缓存到redis中降低接口响应时间。

image.png

1.2.1 服务端mock时序图

mock2.png

1.2.2 客户端mock时序图

mock1.png

二、mock服务

2.1 部署及性能

通过Nginx + Uwsgi + Django部署,支持高并发

可以直接通过测试组jenkins构建部署

部署脚本:

# 服务器项目地址
# shellcheck disable=SC2164
cd /home/dhk/workspace/hulk
python37 -m venv venv            # 生成虚拟环境
source venv/bin/activate            # 启动虚拟环境
python37 -m pip install --upgrade pip           # 升级pip
python37 -m pip install -r requirements.txt     # 安装依赖库
# shellcheck disable=SC2164
cd /home/dhk/workspace/hulk/hulk  #进到uwsgi.ini目录

# shellcheck disable=SC2006
# shellcheck disable=SC2009
# 获取uwsgi父进程
pid=`ps -ef | grep "uwsgi" | grep -v grep | awk '{print $2}' | awk 'NR==1{print}'`
if [ -n "$pid" ]
then
    uwsgi --reload uwsgi.pid
else
    uwsgi --ini uwsgi.ini
fi

性能:在4C8G的机器上的单机性能指标

image.png

2.2 框架设计原理

mock服务可以理解为一个类似于业务系统的应用,可以请求该服务的接口地址并返回对应的报文。并提供了前端配置功能,支持配置自定义的mock接口信息及放行接口对应的业务系统host映射关系。想要设计成一个支持动态接收自定义接口路径的mock服务,需要先了解Django处理请求的原理及路由配置。

2.2.1 Django 如何处理一个请求

先看一下Django框架处理请求的原理,熟悉该原理后,可以很好地利用这点来设计成一个mock服务所需要的支持自定义路由的功能。感兴趣的可以参照官方文档。

当一个用户请求Django 站点的一个页面,下面是Django 系统决定执行哪个Python 代码使用的算法:

  1. Django 确定使用根 URLconf 模块。通常,这是 ROOT_URLCONF 设置的值,但如果传入 HttpRequest 对象拥有 urlconf 属性(通过中间件设置),它的值将被用来代替 ROOT_URLCONF 设置。
  2. Django 加载该 Python 模块并寻找可用的 urlpatterns 。它是 django.urls.path() 和(或) django.urls.re_path() 实例的序列(sequence)。
  3. Django 会按顺序遍历每个 URL 模式,然后会在所请求的URL匹配到第一个模式后停止,并与 path_info 匹配。
  4. 一旦有 URL 匹配成功,Djagno 导入并调用相关的视图,这个视图是一个Python 函数(或基于类的视图class-based view)。视图会获得如下参数:
  • 一个 HttpRequest 实例。
  • 如果匹配的 URL 包含未命名组,那么来自正则表达式中的匹配项将作为位置参数提供。
  • 关键字参数由路径表达式匹配的任何命名部分组成,并由 django.urls.path()django.urls.re_path() 的可选 kwargs 参数中指定的任何参数覆盖。
    Changed in Django 3.0:
    在旧版本里,带有 None 值的关键字参数也可以由未提供的命名部分组成。

5.如果没有 URL 被匹配,或者匹配过程中出现了异常,Django 会调用一个适当的错误处理视图。参照下面的错误处理( Error handling )。

2.2.2 路由配置

对于高质量的Web 应用来说,使用简洁、优雅的URL 模式是一个非常值得重视的细节。Django 允许你自由地设计你的URL,不受框架束缚。

这里用了Django框架的url正则表达式配置规则,达到了类似于动态注入接口地址的效果。这时候随便请求一个接口进来,会按顺序依次匹配,直到匹配到对应的path为止,除了web端的接口路径,其余的都会匹配上正则,例如请求:/rec/sns/du/ct_push_V2/recommend会进到view_mock.mock()函数中。

urlpatterns = [
    path('admin/', admin.site.urls),
    path('hulk/attention', view_attention.attention),
    path('hulk/check', view_attention.check),
    path('hulk/query_url_info', view_web.query_url_info),
    path('hulk/insert_url_info', view_web.insert_url_info),
    path('hulk/update_is_del_1', view_web.update_is_del_1),
    path('hulk/update_url_info_doc', view_web.update_url_info_doc),
    path('hulk/update_is_open', view_web.update_is_open),
    path('hulk/proxy_query_url_info', view_proxy.proxy_query_url_info),

    url(r'^(.*)$', view_mock.mock)
]

2.2.3 mock逻辑

根据上述路由配置的原理,业务系统所有请求被mock系统的接口都将走到mock()函数中,这里会先去读数据库的接口配置信息,如果该接口配置了mock那么直接把配置的response返回,没有配置mock时,会继续往下走,去查询该接口的路由配置信息,对应放行到被mock系统做正常的业务请求,相当于做了一层转发。

注:目前只做了常见的POST和GET请求方式的放行逻辑,POST请求的body类型也是默认json。
logger = logging.getLogger('log')
def mock(request, interface):
    logger.info(request.body)
    path = request.path
    # 查询mongodb
    data = MockApiInfo().query_url_info(path)
    if data:
        url_info = data[0]
        res = url_info['response']
        # 处理配置的返回报文
        response = JsonResponse(process_response.handle_variate(res))
        # 组装返回header
        if url_info.get('response_headers'):
            response_headers = json.loads(url_info['response_headers'])
            for k, v in response_headers.items():
                response.__setitem__(k, v)
        return response
    else:
        # 放行逻辑
        config = MockTransConfig().query_config(path)
        headers = request.headers
        if config:
            if request.method == 'POST':
                host = request.scheme + '://' + config[0]['host'] + path
                headers['Content-Type'] = 'application/json'
                res = requests.request(request.method, url=host, headers=headers,
                                       data=request.body)
                logger.info(res.json())
                return JsonResponse(res.json())
            elif request.method == 'GET':
                host = request.scheme + '://' + config[0]['host'] + request.get_full_path_info()
                res = requests.request(request.method, url=host, headers=request.headers)
                logger.info(res.json())
                return JsonResponse(res.json())
        else:
            response = JsonResponse({"code": 1001, "status": 200, "msg": '请先配置接口或者开启,当前接口路径:' + path})
            response.__setitem__("Content-Type", "application/json; charset=utf-8")
            return response
            

2.2.4 数据库设计

1.放行接口、host映射关系配置

这里选择的是关系型数据库mysql来存储配置信息

image.png

字段说明:

path -> 接口路径

host -> 域名

bussiness -> 业务域

description -> 描述

2.接口信息配置,考虑到接口返回报文是json,很显然MongoDB比较适合

接口配置表mock_api_info

db.createCollection("mock_api_info");

{
    "_id": ObjectId("5fcc546448bfde3202d2eaf4"),
    "url": "/rec/sns/du/ct_hot/recommend",
    "sys_name": "",
    "method": "",
    "content_type": "",
    "response_headers": "",
    "response": "{}",
    "rich_response": "",
    "description": "推荐流-算法",
    "is_del": "0",
    "is_open": "1",
    "add_time": "2020-12-06 11:47:48",
    "update_time": "2020-12-07 11:11:40"
}

字段说明:

url -> 接口路径

sys_name -> 系统名

method -> 请求方法:GET,POST。。。

content_type -> body类型:预留字段

response_headers -> 返回header头

response -> 返回报文

rich_response -> 富文本返回报文:预留字段

description -> 描述

is_del -> 是否删除:0:没有删除,1:删除

is_open -> 是否激活:0:关闭,1:打开

2.2.5 前端配置页面

image.png

image.png

2.3 如何mock服务端接口

2.3.1 配置需要mock的接口信息

1.在mock配置平台配置需要mock的接口信息

image.png

如果需要自定义返回的header头,可以配置对应期望返回的header信息,都是json格式。没有配置会返回默认header头。

2.配置完验证,可以用接口测试工具比如postman请求配置好的接口,正常返回配置的报文说明配置正确。

例子:http://mock服务地址/rec/sns/du/ct_push_V2/recommend

image.png

3.修改服务端系统配置,下面列出了社区的两种配置

2.3.2 Apollo配置

Apollo地址

比如社区的go服务,依赖算法的接口,对应的请求域名都配置在Apollo上

去修改需要mock的接口请求域名

保存完,点击发布,然后重启对应的服务。

image.png

2.3.3 项目中配置

比如社区的php服务都是在项目中的environment.php配置文件

直接通过跳板机在服务器上修改,jumpserver地址

image.png

2.4 日志

通过跳板机连上root@dw-test-test-interfaces-01服务器

进入/home/dhk/workspace/hulk目录

实时日志可以查看tailf uwsgi.log

所有日志都保存在logs文件下

image.png

三、代理层

3.1 常用的代理工具

测试过程经常用到app抓包工具比如:charles、fiddler、wireshark等。

上述列出的都是需要安装在自己电脑上的工具,但有些时候我们更希望有个代理服务器使用,比如玩过爬虫或者科学参与电商平台活动的同学工具库中都会有这么一款工具。这块目前有很多优秀的开源框架,比如whistle,支持写js脚本。还有anyproxy、mitmproxy等。

框架选择:这里我选择了mitmproxy这个开源框架,理由很简单因为它是Python实现的。。。考虑到要做二次开发。

在使用mock功能的场景中mitmproxy和Charles的对比

  • 通过Charles进行mock:

优点:方便,不会相互影响

缺点:需要本地安装,mock的接口多了管理不方便,无法二次开发特性功能

  • 使用hulk平台

优点:无需本地安装软件,配置完后,只要大家连上该代理服务都可使用,比较方便的支持UI自动化,

埋点自动化的使用。并且支持很灵活的打开关闭mock。支持二次开发,可扩展性高。

缺点:共享一个代理服务,抓包信息需要自己过滤

注:后续会支持userId的filter配置,就可以做到相互不影响

3.2 二次开发后的mitmproxy

3.2.1 介绍

目前该代理服务已经部署在内网服务器上,该框架非常强大,这里不做详细介绍。

Introduction

顾名思义,mitmproxy 就是用于 MITM 的 proxy,MITM 即中间人攻击(Man-in-the-middle attack)。用于中间人攻击的代理首先会向正常的代理一样转发请求,保障服务端与客户端的通信,其次,会适时的查、记录其截获的数据,或篡改数据,引发服务端或客户端特定的行为。

不同于 fiddler 或 wireshark 等抓包工具,mitmproxy 不仅可以截获请求帮助开发者查看、分析,更可以通过自定义脚本进行二次开发。举例来说,利用 fiddler 可以过滤出浏览器对某个特定 url 的请求,并查看、分析其数据,但实现不了高度定制化的需求,类似于:“截获对浏览器对该 url 的请求,将返回内容置空,并将真实的返回内容存到某个数据库,出现异常时发出邮件通知”。而对于 mitmproxy,这样的需求可以通过载入自定义 python 脚本轻松实现。

但 mitmproxy 并不会真的对无辜的人发起中间人攻击,由于 mitmproxy 工作在 HTTP 层,而当前 HTTPS 的普及让客户端拥有了检测并规避中间人攻击的能力,所以要让 mitmproxy 能够正常工作,必须要让客户端(APP 或浏览器)主动信任 mitmproxy 的 SSL 证书,或忽略证书异常,这也就意味着 APP 或浏览器是属于开发者本人的——显而易见,这不是在做黑产,而是在做开发或测试。

事实上,以上说的仅是 mitmproxy 以正向代理模式工作的情况,通过调整配置,mitmproxy 还可以作为透明代理、反向代理、上游代理、SOCKS 代理等。

3.2.2 设计原理

一般mock客户端,直接拦截报文修改response即可。这里是当配置了需要mock的接口时,会把原来请求的接口域名改成mock服务的域名地址,从而使得客户端请求到mock服务,到达同样的效果。这样做完全隔离了和业务系统的交互。

mitmproxy 支持 mitmproxymitmdumpmitmweb 三个启动命令,这三个命令功能一致,且都可以加载自定义脚本,唯一的区别是交互界面的不同。这里考虑到保留前端抓包信息的展示,选用了mitmweb来启动,并且直接修改了底层源码来打通和mock服务的交互。

修改的核心代码片段如下,所有进来的请求都会先去判断是否需要mock,如果需要mock即直接将请求转发到mock系统,不需要mock的接口原路放行。

# 是否走mock逻辑
if hulk_api.proxy_query_url_info(path.decode("utf-8")) > 0:
    print(('命中mock,接口:' + str(path)).center(100, '='))
    host = config.HULK_HOST
    scheme = b'http'
    port = 80
    headers.__delitem__('Host')
    headers.insert(0, b'Host', bytes(host))
    

3.3 如何mock客户端接口

第一步:连代理。

第二步:在mock平台配置需要mock的接口。

接下来就可以非常丝滑的进行客户端接口mock,不需要mock了可以关闭mock按钮,或者直接断开代理即可。

四、社区实践

背景:4.60版本推荐流「负反馈优化」需求,需要测试服务端在不同业务场景返回不同负反馈文案的逻辑,

推荐流接口/sns/v2/feed/recommend依赖算法接口/rec/sns/du/ct_hot/recommend,没法固定自己想要的内容。

方案:
1.算法侧帮忙造数据(显然成本相对较高,比较麻烦)

       2.mock掉算法的接口

操作:

1.配置需要mock的接口

image.png

返回报文结构填和算法约定好的数据结构形式,并将数据改为自己需要测试用的数据

2.推荐流接口在PHP服务,直接去跳板机上修改php server的配置

image.png

3.开始测试

①接入mock后返回的数据是自己配置的  

image.png
 ②不需要mock,关闭mock开关正常走算法逻辑
image.png

既很方便的测试到了服务端的逻辑,也同时测试到了客户端的取值逻辑,并且方便于产品同学的验收。

五、结语

随着技术的不断发展,如果想要做一套通用的mock平台,任重而道远。比如现在流行的rpc怎么去支持?不同的技术架构有不一样的需求,这些都是需要去考虑的。当然各种测试工具平台的设计开发初衷都是提效,服务于业务。后续会不断的结合业务特性来迭代,希望能打造出贴合业务特点,真正的能带来提效的一个mock平台。

文|dhk
关注得物技术,携手走向技术的云端

查看原文

赞 0 收藏 0 评论 0

得物技术 发布了文章 · 1月15日

得物技术教你秒懂mongoDB之分片集群

背景

随着公司直播平台的全面开放,加上直播玩法的多样升级,最明显的感受就是:

① 数据增量明显。

② 并发量明显变大。

应对这两种变化,作为后端开发就需要在技术选型方面做一些选择。当前直播量大的业务都是利用mysql进行了分表处理。但是观察一段时间下来发现以下几个问题:

① 部分业务的分表上存储的数据量不是很均衡,会存在一些大主播对应的分表数据量明显偏大。

② 上新业务或者一些新活动就要建几十张表以提前应对已知的数据量,操作繁琐且后续变更不方便。

所以在做技术选型的时候就联想到了mongoDB的分片集群。下面我们来一探究竟:

一、回忆一下MongoDB的特点

1 灵活的模型

区别于关系数据库最大的一个特点就是字段灵活变更,这就非常适合一些迭代频繁且数据模型多变的业务场景。例如我们直播的活动业务场景,当前直播活动越来越多,玩的花样也是越来越多,其实就很适合用mongoDB来做活动业务的数据存储。

2 json数据格式

mongoDB的数据存储都是json格式,非常适合微服务Restful Api。

3 横向扩展能力

与mysql的分库分表不同,它可以在业务代码不变更的情况下做自动水平扩展。能够很好的解决上文描述的两个场景
① 分表数据不均匀
② 分表数据过大要调整原有数据分布。

二、说一说MongoDB的分片集群的原理和机制

在正式描述分片集群之前,我们先看一下当前MongoDB的架构部署方式如下图:

image.png

从上图我们可以很清晰的看到常用的架构模式氛围三种:
1、单机版:只用于开发和测试环境,没有高可用。
2、复制集:一般是一主二从,当然也可以配置更多从库。大部分用户的生产环境在使用,能够满足绝大多数业务场景,且是高可用。
3、分片集群:满足横向扩展能力且高可用。

分片的组成部分

如下图所示:

image.png

我们把上图分为四个模块来讲:

1. 应用程序+驱动程序

2. 路由节点(mongos)

路由节点主要是用来控制分发到哪一个分区(shard),因为mongos他存储有一个映射表。这个映射表的数据就是config节点存储的映射数据同步过来的。一般是在mongo启动的时候load到mongos的内存。

从上图可以看到mongos有多个,其实这也是为了满足高可用的场景。

3. 配置节点(config)

数据节点主要存储了集群元数据的存储,数据分布映射关系表。

存储的数据表格式如下图展示:

image.png

4. 数据节点(shard)

上图最下面的区域就是分片的数据节点,每一个分片都满足高可用,一般都是一主二从。分片的个数最大可以到1024个。所有分片合到一起就是完整的数据。不存在重复数据。

到此我们对分片集群的组成部分大概描述完了。

你会不会有这样的疑问?

  1. 配置表里存的数据分布范围是怎么定的?
  2. mongoDB的分片集群到底是怎么做数据均衡的?

如果你有其他疑问可以在文末给我留言,在回答上面问题前我先解释几个概念,我们看下图

image.png

如图所示我们可以看到以下几个名词,分表是集群>片>块>文档。

一个集群是由多个分片组成,一个分片存储了多个块(备注:逻辑上的数据分块),一个块包含多个文档。文档不是最小单位,文档里存着一个个字段,一个字段或者多个字段可以组成为一个片键。

片键有什么用?片键决定了你的数据可以分成多少块。

到此基础概念介绍完了,我来回答一下上面的疑问。

关于数据分布,mongoDB提供了三种方式

a. 基于范围

image.png

如图所示,数据在逻辑上被分成了4个块:举个例子你的系统存的是公司用户信息,如果你按年龄来分,假设公司年龄段在18-60,且按一个年龄分一个块,那么最大可以分43个块。再把块分到多个分片上去。这时我们查询某一个年龄段的数据,比如22-25之间的数据,一般很大可能是在同一个片或者两个片上。故而在范围查询性能上表现较好。那么问题来了,这时我们把数据无限放大,公司年龄在22-25之间的占比80以上,这时我们会发现22-25岁的用户数据所在的片和别的片比起来就很显得特别大,这也就是我们说的数据分布不均衡的情况。新增数据都是在22-25岁之间,这时也就导致了热点片的情况。

b. 基于hash

image.png

如上图所示,片键不是连续的而是通过hash散列到不同的片区,这就很好的解决了数据不均匀的情况。但是同时带来的问题就是,范围查询的场景效率特别低,需要遍历全部的分片才能满足业务查询。举个例子用户的订单系统,我们按照下单用户id去做hash,这样不同用户的订单数据就会被均匀的分布到不同的分片。单查某个用户的订单数据是非常高效的,但是如果去根据时间范围去查则要去扫描全部分片。

c. 基于zone

image.png

顾名思义,基于地域去划分。有时候我们服务的可能是全球用户,又因其地域性则可以天然的按地域去划分分片。这样很好的保证,在某固定地域可以访问到最近的数据节点。

到此对于上文中的第一个疑问是否豁然开朗?下面我们再来回答第二个问题:

简单来说,mongo他提供了两个程序:

① 切割器

切割器可以对某个源分片的数据按chunk(数据逻辑块)去做切割。

② 平衡器

当某些分片数据不均匀的情况下,平衡器就发挥作用了,他会发出一个命令让切割器去需要移动的分片上去做数据切割,再把数据移动到数据少的分片上。具体的步骤如下:

  • 平衡器向源分片发送moveChunk的命令
  • 源分片收到命令后,会启动自己内部的一个moveChunk命令,如果在数据移动过程中有客户端发来读写请求的话,都会发送到源分片。(因为配置服务器上的元数据还没有改变)
  • 目标片开始向源分片请求将要移动的数据块的文档,准备拷贝文档数据。
  • 当目标分片接收到据块的最后一个文档后,目标分片会启动一个同步进程来检查,是否已经拷贝完全部的文档。
  • 当同步完成后,目标分片会连接配置服务器,更新元数据列表中数据块的地址。
  • 当目标分片完成元数据更新后,源分片就会删除原来的数据块.如果有新的数据块需要移动的话,可以继续进行移动。
  • 配置服务器会通知monogs进程更新自己的映射表。

到此我们对于上文的第二个问题,也做了较为详尽的描述。下面来归纳一下MongoDB的分片集群的特点:

  • 应用全透明,无特殊处理

对于开发很友好,不需要做改动,mongo架构部署变化不影响业务代码。

  • 数据自动均衡

再也不用担心某个分片过大的情况,但是这里要注意的是你的分块必须要做到不能太大,不然老天也帮不了你。因为太大的话,经常会出现移动失败的情况。

  • 动态扩容,无须下线

如果当前生产环境是mongo复制集的架构,可以在线上环境直接切到分片集群架构。如果你使用阿里云的mongoDb服务,阿里提供了两种方式:1全量切 2增量切(注意增量切是需要另外付费的)对于用户来说操作很方便。

三、说一说分片集群适用的场景

上面我们把分片集群的概念,包括组成,实现机制,特点都描述完了。我相信大家也对分片集群有了一个较为完整的认识。下面我们聊一聊哪些场景我们需要考虑使用分片集群呢?

1.数据容量日益增大,访问性能降低

2.新品上线异常火爆,如何支撑更多的并发用户

直播打榜活动如火如荼的举行,部分业务数据增量和并发也有明显上升,目前来看无需调整部署策略可以轻松应对,假设这个增量无限陡增,那么就可以考虑使用分片集群,以解决数据库性能瓶颈。

3.单库已有海量数据,出了故障如何快速恢复

这时候我们需要提前预判风险问题,不能等出了故障再考虑使用分片集群。海量数据(假设数据已达TB级别)去做数据恢复耗时很长。

4.地理分布

数据来源不同地域,可以考虑使用

四、在使用分片集群前需要考虑的问题

1、合理的架构,需要考虑以下几个问题

问题1:是否需要分片

这个就需要结合自己的业务场景和当前的架构部署情况去做甄别。可以结合上文提到的适用场景去对号入座。

问题2: 需要多少分片

这一般需要考虑三个维度

① 你预估的业务在未来一年或者某一段时间,存储总量来计算,一般经验之谈2TB一个分片

② 根据你业务的最大并发量来计算

③ 根据你的硬件设备条件去衡量

这可能更需要dba去做一个评估来决定。

问题3:数据分布规则

这是非常关键的一点,关于数据分布这个,如果你的数据分布不合理,直接导致你没办法横向扩展、查询效率低等等性能问题

2、使用正确的姿势

① 选择你需要分片的表

mongo的分片都是基于表的,这是一个大前提。只对需要做分片的表做分片。

② 选择正确的片键

上文我们已经说过片键的作用,片键直接决定了你数据的分块。下面我们着重对片键的选择来阐述什么才是正确的片键。

首先我们需要遵守以下几个准则:

基数要大!

为了基数要大?如果你的基础小,例如上文举的例子,按年龄来分段,那么你的数据分块最多分几十块,如果你的数据是10TB,那么你每一块就算平均分布每一块的数据就要几百GB,这么大的块,是很难移动的,直接导致均衡策略失效。

保证分布均匀

为什么要保证分布均匀,如果不均匀我们会出现什么情况?

a、导致热片产生 (可以回忆一下上文讲分片数据分布方式时候提的例子)

b、导致分片数据经常大小差异大,频繁进行数据均衡

要保证定向查询好

好的架构都是服务于业务的,如果你的某些查询业务比较多,你肯定不希望你每次查询都要遍历所有的分片吧。

所以我们需要尽可能让自己查询的数据落在同一个分片上。

3、我们通过两个业务场景来聊聊

① 商家订单系统

业务描述:假设平台每日下单量在百万级,不同商家需要经常统计不同周期的订单数据

针对这一业务场景,我们按照上面思考的步骤考虑一下?

思考步骤一:要不要用分片?

回答:可以使用,因为数据量非常大,而且查询统计很频繁。

思考步骤二:使用什么做片键最合适?

回答:结合选片键的准则,最佳为商家id和下单时间。如果只用商家id做片键,他的确满足基数够大,另外数据均匀也没问题,但是查询某段连续时间内的订单需要遍历所有的分片。所以要选择商家id和下单时间的组合片键,这样才能保证定向查询。

② 直播打赏记录

业务描述:  记录用户给主播打赏数据,,假设打赏记录日增百万,只存在查询近期数据

从业务描述来分析,这个日志的历史数据没有查询的必要其实我们可以对历史数据做归档处理。无需考虑使用分片集群。

四、小结

看完全文,我相信你们对于分片集群都有了一定的认识。在工作中针对某些业务场景希望能给你提供一个多的选择。最后我终结两点:

  1. 分片集群可以有效解决性能瓶颈及系统扩容问题
  2. 分片管理复杂,成本高,能不分片尽量不用

文/荣荣
关注得物技术,带你走向技术的云端

查看原文

赞 1 收藏 0 评论 0

得物技术 发布了文章 · 2020-12-31

得物技术的交易轨迹系统

我们不生产数据,我们只是数据的搬运工。

背景

  • 现有系统操作日志都分散在各个域的系统中,而且没有形成规范的系统。导致排查线上问题全是去日志系统各种搜,效率很低。
  • 我们针对系统调用链路有trace串联,跨域查问题很爽。但是我们对单据纬度没有trace串联,单据流转对于我们来说是黑盒。
  • 开发人员对自己的功能线上情况不清楚,比如:每天卖家发货量多少?不清楚;成功率多少?不清楚;平均响应多少?不清楚;针对当前量,我们是不是需要做一些优化?其实有时候并不是不去关注,只是没有一个很直观的系统去很直观的呈现出来。
  • 为了解决上述问题,交易轨迹系统就应运而生了。

实现思路

怎样去埋数据?

1. 方案一:手动埋点(不推荐)

对现有系统侵入性较高,而且埋点对系统稳定性有一定的影响,不到万不得已不推荐。但是我们提供了这项功能。支持三种接入方式,feign、dubbo和rocketmq。

2. 方案二:日志清洗(推荐)

怎样对现有系统不改造,利用现有的资源去寻找事件action?我们发现access.log可以完美的利用起来。但是有一些超时任务和mq触发的任务没有access.log,现阶段需要考虑其他方式接入。后续我们考虑提供公共工具,将mq和超时任务触发的动作,写入access.log并提供全套的清洗入库套件。

3. 方案三:binlog(不推荐)

为什么不推荐,因为binlog拉取不到入参出参,对排查问题没有太大的作用。但是有一点优势很明显,我们可以追踪前后两次的表更。比如:修改地址(A地址->B地址)

怎样去数据清洗?

1. 功能独立

和业务系统隔离,独立一套系统去做数据清洗

2. 实时生效

利用groovy脚本做清洗逻辑,脚本写好后保存实时生效。

3. 数据扩展

日志里面的数据不满足埋点需求,我们提供了dubbo和feign的扩展,可以去业务系统查询数据

4. 批量操作支持

批量操作会涉及到多个单号的流转,脚本默认返回List,对多个单号进行埋点,支持一次解析,多条数据落库

5. 主子单号支持

(同理批量操作)

6. 日志太复杂?解析费劲?

针对日志分析,我们提供的公共的数据获取工具,直接调用方法即可

例子(寄售申请):

package com.shizhuang.duapp.trade.script.biz.deposit.js

import com.alibaba.fastjson.JSONArray
import com.alibaba.fastjson.JSONObject
import com.google.common.collect.Lists
import com.shizhuang.duapp.trade.cycle.api.resultdata.BizResult
import com.shizhuang.duapp.trade.cycle.api.script.AccessLogToBizResultBaseScript
import com.shizhuang.duapp.trade.cycle.api.sourcedata.AccessLogData
import com.shizhuang.duapp.trade.cycle.api.util.AccessLogUtils

/**
 * @program: trade-cycle-center
 *
 * @author: 小猪佩奇*
 * @create: 2020-11-20 17:08
 * */
class ConsignApplyCreate2BizResult extends AccessLogToBizResultBaseScript {
    List<BizResult> parse(AccessLogData accessLogData) {
        // 响应结果
        String responseData = accessLogData.getResponseData()
        // 执行时间
        String takeTime = accessLogData.getTakeTime()
        // http 返回code码
        String httpStatus = accessLogData.getHttpStatus()
        // 获取uid
        String uid = AccessLogUtils.getUid(accessLogData)
        List<String> applyNoList = this.analyzeApplyItemNo(responseData);
        List<BizResult> bizResultList = Lists.newArrayList();
        for (applyItemNo in applyNoList) {
            BizResult result = new BizResult()
            result.setOperatorId(uid)
            result.setOperationBizNo(applyItemNo)
            result.setOperationTime(accessLogData.getOperationTime())
            result.setOperationSubType("app寄售申请")
            result.setOperationTrace(accessLogData.getOperationTrace())
            result.setOperationCostTime(Long.valueOf(takeTime))
            result.setOperationResult("200" == httpStatus ? 0 : 1)
            //result.setOperationExtend()
            result.setOperationDetailReq(accessLogData.getPayload())
            result.setOperationDetailResp(accessLogData.getResponseData())
            bizResultList.add(result);
        }
        return bizResultList;
    }

    /**
     * 分析单号
     * @param responseData
     * @return
     */
    List<String> analyzeApplyItemNo(String responseData) {
        List<String> list = Lists.newArrayList()

        JSONObject jsonObject = JSONObject.parseObject(responseData)
        JSONObject data = jsonObject.getJSONObject("data")
        JSONObject applyProduct = data.getJSONObject("applyProduct")
        if (Objects.isNull(applyProduct)) {
            return list
        }

        JSONArray applyItems = applyProduct.getJSONArray("applyItems")
        if (Objects.nonNull(applyItems)) {
            for (Object applyItem : applyItems) {
                JSONObject parseObject = JSONObject.parseObject(applyItem.toString())
                String applyItemNo = parseObject.getString("applyItemNo");
                list.add(applyItemNo);
            }
        }
        return list
    }
}

怎样去做数据呈现?

1. 单据纬度

寄存单交易轨迹

客服工单轨迹

查询时段段内,单据的流转轨迹(支持跨域跨系统展示)。根据来源数据和结果数据,排查问题根源。如需查看详细日志,可以根据traceId去日志系统去查看详细trace日志。

2. 人纬度

统计时间段内用户的操作行为,方便排查某个时间段内用户的操作。

怎样自定义图形化?

TDengine时序数据库存储的存储的数据都是带有时间戳的,且本身很好地支持时间维度的聚合,正好可以结合grafana对操作日志中的数据进行分析,展示各类业务运转的情况。

系统接入

access log方式接入

  • 下载脚本项目,编写脚本代码
  • 新增事件脚本类,将脚本代码配置到交易轨迹系统

  • 添加路由规则,关联事件和脚本

rocketMq方式接入

mq接入方式和rpc方式相同,各业务系统把数据封装好,通过硬编码的方式将数据发送到mq,轨迹系统订阅消息进行数据清洗并持久化。有时候获取事件不是很方便,通过mq的方式接入也未尝不是一种好的方式。

扩展

  • 在存储上我们支持跨度一年的轨迹查询,在存储上已经做过优化,接入方可以不考虑存储问题。
  • 在和财务系统、客服系统的对接中。我们发现还有更多的方向去扩展功能,不仅仅是局限于交易轨迹。
  • 财务系统需要记录和第三方交互的请求报文,排查问题的时候直接通过单号快速查询第三方报文。
  • 客服系统需要记录客服会话的生命周期,通过会话id可以直观的查看会话链路。并且可以通过客服id可以快速统计客服的接入量统计。
  • 后续会加强图形化的功能和简易数据分析的能力
  • 理论上我们只提供了一种存储能力,规范了数据存储的格式。具体怎样使用,使用方可以自定义。
  • 最后打个广告,欢迎大家接入。

文/得物技术 Ambition

查看原文

赞 0 收藏 0 评论 0

得物技术 发布了文章 · 2020-12-25

得物技术谈如何测试概率性事件 - 二项分布置信区间

【得物技术】如何测试概率性事件-二项分布置信区间

前言

日常开发测试可能会遇到这样一种情况,有一个接口或方法概率触发,那么需要多少次抽样落在一个什么区间内,才能断定是否按照设定概率进行呢?

本文将以二项分布作为研究手段,分两种情况求解此类问题的置信区间范围,并结合实际案例进行分析。

背景

某一天,测试同学在验证一个接口时遇到了一个问题。

该接口设定为50%概率触发,测试同学写了自动化脚本进行多次调用。

但是问题来了,他并不知道应该调用多少次,然后落在一个什么区间内才算测试通过。

极大的扩大样本容量,然后给一个模糊的范围边界确实能解决这个问题,但是测试同学并不满足于此,他要一个精确的数字

因此我只能满足他任性的要求,提笔拯救测试同学。

解题思路

在这种情况下,触发 or 不触发 此类概率问题可以看作一个二项分布,我们的目的在于经过一定量的样本测试之后判断测试出来的概率是否落在置信区间内

那么就需要考虑这几种极端情况:

1.概率太小

2.概率太大

3.样本数很小

4.样本数很大

在二项分布中,可以将样本数与事件概率综合起来考虑,合并为以下两种情况

(a) 概率比较正常或样本数较大 —— np > 5 and n(1-p) > 5

(b) 概率比较极端或样本数较小  —— np <= 5 or n(1-p) <= 5

(n :样本总数,p:概率)

二项分布的置信区间估计方法常用的有两种,一是正态分布近似方法,即 Normal Approximation Method;二是精确置信区间法,即 Clopper-Pearson Method。

对于这两种情况可分别应用这两种方法进行求解。

Normal Approximation Method

很容易可以想象出当样本数足够大的情况下,二项分布的曲线分布会愈发趋向于近似正态分布。

因此在情况a中适用于正态分布近似。

置信区间计算公式:

因此使用了正态分布近似,因此Z value用的也是正态分布的Z value。

p: 样本中所测得的事件发生概率

a: I类错误率

Z:  Z value常量,可查表得知

n: 样本总数

该方法的优点在于简单易于理解,但是在极端情况中精度会很差。

Clopper-Pearson Method

在情况b中适用于精确置信区间法

置信区间计算公式(贼复杂,网上大多数贴的都是这个):

但是可以发现上述公式是基于Binomial Cumulative Distribution Function,可以通过Beta Distribution来计算,因此经过简化可得如下置信区间计算公式。

置信区间计算公式(经过简化):

n: 样本总数

k: 成功数量

a: I类错误率

BetaInv: 一个算法函数,完全不用理解具体细节,找个别人实现的直接调用即可(包括excel)

这样一来就简单的多了,我甚至可以拿excel解出来。

该方法的优点就在于可以处理极端情况,p是0或1的情况也阔以。

实际案例

案例一(正态分布近似法)

问题:一个50%概率的接口,测试50次,成功28次,判断是否正常。

p = 28/50 = 0.56

np = 0.56* 50 = 28 > 5

n(1-p) = 0.45 * 50 = 22 > 5

因此使用正态分布近似法。

置信度假设为95%,因此查表得知Z值为1.96

Z = 1.96

p = 28/50 = 0.56

n = 50

代入可得,置信区间为[0.56 - 0.14, 0.56 + 0.14]这个范围,因此0.5确实落入这个置信区间,所以可以暂时认为这个我这个接口没问题!

案例二(精确置信区间法)

问题:一个17%概率的接口,测试50次,成功3次,判断是否正常。

p = 3 / 50 = 0.06

np = 0.06* 50 = 3 < 5

适用于精确置信区间法。

假设置信度为95%

a = 0.05

n = 50

k = 3

使用Excel或其他方法

计算可得:

Pub = 0.1655

Plb = 0.0125

置信区间为[0.0125, 0.1655],所以17%概率的接口没有落在置信区间内,可以认为在95%置信度的情况下,该接口出现了问题。

接着有请测试同学发言=。=!

测试的实践

实践的功能与背景

基于鉴定460版ai鉴定师智能回复这个功能。简单来说这个功能就是,当你对鉴定贴进行回复时,你的回复内容完全匹配到回复词库时,就会触发ai自动回复。而触发ai自动回复是有一个概率控制的,在测试的时候想不单单校验返回值,也想同时校验这个概率功能的准确性。

测试执行

首先遇到这个功能,我想到的测试步骤就是:

目前就是这个测试步骤,然后通过接口的返回值来判断做判定:

1.检查回复中有没有子评论

2.检查子评论的回复人是不是ai鉴定师

校验的代码如下

if (replyListResponse.getJSONObject("data").getJSONObject("simpleReply").

 getJSONArray("list").getJSONObject(j).getJSONObject("childReply").

 getJSONArray("list")!=null){

 DuAssert.getInstance().assertTrue(replyListResponse.

 getJSONObject("data").getJSONObject("simpleReply").

 getJSONArray("list").getJSONObject(j).getJSONObject("childReply").

 getJSONArray("list").getJSONObject(0).getString("userName").equals("ai鉴别"));

}

有人会问,为什么没有检查回复的信息的正确性呢?首先,测试服的数据不可控因素比较多,比如今天校验了他的回复信息,但是过两天回复信息在验收之后改了,那就还需要对用例进行改动。其次,在正常的测试流程中肯定会覆盖不同的入参信息与不同的返回信息对应情况,只要验证一次这个逻辑,剩下这个数据准确性的验证反而觉得有一些冗余,写针对这么长的测试用例更像是为了保证整个流程:从发帖到审核,从回复到审核这两个老流程的准确性保证;非鉴定师触发ai鉴别这个基础流程的保证情况。

概率验证

进行到概率验证,做法很简单,比如我们随机50次,然后有子评论,且评论用户为ai鉴别,则n+1。但是这样又会引入一个新问题,这个最后叠加出的n肯定是在一定区间内波动的。比如服务端现在设置为50%回复,那n就是25左右,那这个具体要怎样来设置这个区间,才能较为准确的验证这个概率的正确性,从而在数值没有落在区间内的时候,可以光明正大的给开发提bug,说你的概率算的不准确呢?

从上文中我们得知,需要测试的事件概率为50%,尝试的次数为50次,所以:

np=50X50%;

n(1-p)=50X50%,

均大于5,由此可知概率应符合正态分布曲线,所以用来计算这个置信区间的公式为:

代码实现

public static double getProbability(int times,double targetP) {

 double z=1.96;

 return Math.sqrt(targetP*(1-targetP)/times)*z;

 }

很简单,一行代码就搞定了。有同学可能会问这个1.96是哪来的,实际上这个是查表查的,暂定要求的准确度为95%,于是根据下方的表格查得0.975对应的横纵坐标为0.06和1.9,于是相加得1.96

最后用例代码添加

 double realP=n/50.00;

 double max =Math.ceil(50*realP+50* Probability.getProbability(50,realP));

 double min =Math.ceil(50*realP-50* Probability.getProbability(50,realP));

 if (25<=max &&25>min){

 DuAssert.getInstance().assertEquals("在区间内,概率可信","在区间内,概率可信");

 }

 else{

 DuAssert.getInstance().assertEquals("不在区间内,不可信,提bug!","在区间内,概率可信");

 }

综上所述,我们基于统计的原理校验了这个功能的准确性。

总结

测试概率是测试过程中一个比较模棱两可的事情,如何进行概率事件的测试并有效的发现问题是非常必要的。

因此在上文中主要将此类问题模拟成二项分布进行求解,求得置信区间从而进行较为准确的判断。

但是有一点要注意的是在上文两个案例中都是以95%置信度作为前提,实际上是存在发生I类错误的可能性,所以测出了问题只能说大概率可能出现了问题,而不能立马给一个绝对性的结论,这样是不科学的。

建议是根据统计学经验,先测30+次,能用正态分布近似覆盖就尽量先使用正态分布近似。

查看原文

赞 0 收藏 0 评论 0

得物技术 发布了文章 · 2020-12-25

得物 App 亮相 QCon 全球软件开发大会,分享百倍增长背后的技术力量

得物App亮相QCon全球软件开发大会,分享百倍增长背后的技术力量

12月20-21日,得物App亮相QCon全球软件开发大会上海站。

得物App CTO陈思淼、交易平台及稳定性平台负责人金思宇作为演讲嘉宾与诸多信息技术探索前沿技术专家共同探讨,并分享了《得物App百倍增长背后的技术力量》演讲。

得物App

近年来说起年轻消费,就不得不提到得物App。作为新一代潮流网购社区平台,正品潮流电商和潮流生活社区是得物的两大核心服务。

而其首创的“先鉴别,后发货”的网购流程以强中心化平台定位,很大程度上为用户提供了正品保障

创立以来,得物App一直以"帮助用户得到美好事物"为目标而努力,并凭借对年轻用户的深刻洞察,在"90后"年轻人群体中建立起强烈的平台归属感和品牌认同感。

潮流意见领袖和潮人们聚集在得物平台上,“新、潮、酷、炫”的商品和潮流文化成为他们了解、获取、交流的社交载体。

截至目前,得物App平台用户中,"90后"占比超过八成。

技术升级加持网购体验,得物成为更多中国年轻人的选择

说起得物App技术平台构建,得追溯到2015年。当时,得物App初版以潮流社区App上线,帮助年轻人了解球鞋文化和潮流资讯,打造国内主流Sneaker互动社区

之后基于对潮流文化的了解和年轻消费的洞察,于2017年下半年衍生出了交易业务

在经历业务百倍增长的背后,得物App的技术团队一刻也没停下脚步,交易平台开始大范围的迭代优化。

2019年下半年,得物CTO陈思淼决定启动交易平台化,即“五彩石”项目,统一规划及重构交易体系。

此后,得物的技术升级让得物经受住了一次次考验。

随着业务不断发展,得物技术团队作为基石,也在通过不断更新迭代升级为用户"更快、更好"的网购体验保驾护航。

另外,得物通过大量技术创新打造具备科技感的一站式网购体验,并获得2020环球金趋势“赋能创新奖”

2020年1月,得物上线 “AR虚拟试穿”功能赢得了众多年轻用户的好评。

由于贴合度、真实性等达到“像素级”还原,截至目前,每天能够帮助超过30%的用户进行消费决策。

2020年9月,得物首创AR技术应用于“防伪”的使用场景中。

通过扫描“鉴别证书”可以查看部分商品的AR模型,进一步为用户的网购安全感“保驾护航”。

围绕“稳定”“效率”“成长”“体验”四个关键词,主抓技术团队建设

2020年,得物App取得了不俗的成绩,不仅获评“2020年度新经济企业”、“2020上海百强成长企业50强榜首”,更是连续几个月霸榜AppStore总榜前十位

支撑业务需求快速发展同时,得物技术围绕“稳定”“效率”“成长”“体验”等几个关键指标主抓团队建设。

如今,得物技术团队不断壮大,到访过得物办公室的人都说:“没想到得物的程序员,也能这么‘潮’!”。

正得益于为员工营造了多维度的学习成长氛围,持续吸引着优秀且年轻的伙伴加入得物。

从员工入职开始,通过“新人落地计划”等一系列的措施来帮助员工更快融入团队。

激励员工自主学习,通过得物特色渠道学习输入,也鼓励员工通过分享和博客等方式输出。

在半年时间内,仅得物技术就产出了五百多篇博客技术夜校举办超过十二期,已有几千人次参加了学习

通过知识分享相关的举措,员工将自己工作、学习的心得沉淀下来,分享给他人,既能够教学相长,共同进步,同时也营造了技术部的分享氛围,有利于打造学习型组织

促进产研协同,得物提出新思路和最佳实践

作为高速成长的互联网公司,在产研协同方面,得物提出了新的思路和最佳实践

通过种种措施,得物技术针对重点项目进行资源保障,在今年通过多个重点项目的发布,不仅助力业务取得成果,项目的准时上线率达到100%

此外,618、双十一、双十二等大促保障,也为业务提供了有力支撑。

陈思淼回顾加入得物一年半时间里的变化,堪称一日千里。公司业务蒸蒸日上,在潮流时尚和年轻人中的影响力越来越大

得物持续打造年轻活泼、平等成长、快乐工作的文化氛围,并建立"求真"、"向前一步"、"拥抱变化"的价值观。

陈思淼说:“得物相信技术能够创造商业价值,我们推崇聪明人一起下笨功夫,开创一番事业。我们正在大力吸收志同道合的人才,希望成为年轻人向往的互联网企业,围绕打造‘上海最好的技术团队’目标,一起共创未来。”

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 8 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-10-27
个人主页被 1.6k 人浏览