iOS 3D变换

CATransform3D

iOS中的3D变换是通过QuartzCore框架实现的,利用transform属性(CATransform3D类型)可以让图层在3D空间内移动或者旋转。CATransform3D是一个可以在3维空间内做变换的4×4的矩阵,定义为

struct CATransform3D

{

  CGFloat m11, m12, m13, m14;

  CGFloat m21, m22, m23, m24;

  CGFloat m31, m32, m33, m34;

  CGFloat m41, m42, m43, m44;

};

,如图1

  • x1 = m11x + m21y + m31z +m41       (1)
  • y1 = m12x + m22y + m32z + m42   (2)
  • z1 = m13x + m23y + m33z + m43   (3)
  • △ = m14x + m24y + m34z + m44   (4)

x',由于只有m11是在直接改变x的倍数,而m41是在改变x的坐标,我们可以得到下面的结论:

  • m11:控制x方向上的缩放
  • m41:控制x方向上的平移

以此类推:我们也可以得到关于yz轴上的关系

  • m22:控制y方向上的缩放
  • m42:控制y方向上的平移
  • m33:控制z方向上的缩放
  • m43:控制z方向上的平移

以在z轴旋转为例如下图

  • x1 = cosΦx – sinΦy
  • y1 = sinΦx + cosΦy

因为以z轴旋转,z轴的坐标不变,只有x和y的坐标改变,结合(1)(2),m12和m21 决定z轴旋转

透视投影

在真实世界中,当物体远离我们的时候,由于视角的原因看起来会变小,理论上说远离我们的视图的边要比靠近视角的边跟短,CATransform3D的透视效果通过一个矩阵中m34来控制,m34的默认值是0,我们可以通过设置m34为-1.0 / d来应用透视效果,d代表了想象中视角相机和屏幕之间的距离,以像素为单位,m34必须在有旋转的情况下才有效果;

  1.    UIView *layerView = [[UIView alloc] init];
  2.     self.layerView = layerView;
  3.     [self.view addSubview:layerView];
  4.     layerView.frame = CGRectMake(100, 200, 100, 100);
  5.     layerView.backgroundColor = [UIColor redColor];
  6.     CATransform3D transform = CATransform3DIdentity;
  7.     transform.m34 = – 1.0 / 100.0;
  8.     transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
  9.     self.layerView.layer.transform = transform;

如何实现一个简单的WebViewJavascriptBridge

在移动应用的开发的过程中,经常会有应用内部的活动,以及部分对灵活性要求很高的功能会采用基于WebView的方式实现,用来免去应用提交到应用市场的审核时间,以及用户的升级成本。这种最基础的Hybrid应用场景中,WebView页面中的javascript环境经常需要共享native环境(iOSAndroid)中的部分数据,例如: 用户登录授权信息、应用的版本信息等等,以及需要借助native的能力实现某些功能,例如: 录音、拍照、GPS位置信息、蓝牙等等,还有另外的场景是重用部分native已经实现的功能,例如:跳转到App内其他页面,调起native的某个功能弹窗等等,因此提供一个js native交互的通道是非常基础的一个需求。

原理

如果对AndroidiOS 的相应的WebViewapi比较熟悉的话,有这么几个API可以实现javascriptnative之间的互相调用。

Android下边的android.webkit.WebView, javascript调用native的实现方式:

首先在native端调用addJavascriptInterface (Object object, String name),将Java对象object注册到Javascript环境的主Frame上,这样就可以在javascript中调用到nativeJava对象的方法。为了安全方面的考虑在JELLY_BEAN_MR1 版本以上的Android系统中,只有Java对象的公开的并且有JavascriptInterface注解的方法才能被Javascript调用到。

native调用Javascript的方式:

KitKat版本之后,WebView提供了一个evaluateJavascript方法,执行传入的字符串表示的js代码并能返回结果。在这个版本之前只能通过loadUrl(“javascript:”)这种方式执行js代码。

iOS8新加入了WebView的实现WKWebView, 现在基本上应用支持到iOS8以上就够了,因此这里只考虑WKWebView。在WKWebViewjs调用native端可以通过WKUserContentControlleraddScriptMessageHandler:name实现,在native中调用这个方法将相应的handler添加到WKWebViewWKUserContentController上,在Javascript中就可以通过调用window.webkit.messageHandlers.handlerName.postMessagejs消息发送到nativeWKWebView同样实现了一个跟AndroidWebView类似的evaluateJavaScript的方法,用来在native端直接在WKWebViewjs环境执行字符串表示的Javascript代码。

业内的开源实现

当然业内也有比较出色的开源实现,最出名的是marcuswestin/WebViewJavascriptBridge, 这是一个iOS下边的WebViewJavascriptBridge库实现,实现了一个iOSjs-native双向调用的bridge,可以支持UIWebViewWKWebView。这个项目的解析在网上很多,在这里不再赘述。只讲几个很关键的设计:

1) 执行环境的保持

jsnative的互相调用过程中,如果能保持执行环境,以js调用native举例,如果可以封装给上层一个调用接口,同时支持方法回调,这样上层的代码写起来就会非常顺畅。这个的实现其实也比较简单,给回调生成一个唯一id,在js端保存这个id和回调方法的映射,将id传给native, native端处理完,回调的时候同时将这个id传回给js端,这样js端就能找到相应的回调函数并执行。

2) 消息排队机制

native端和js端,这个库都实现了一个消息排队机制,用来缓存bridge准备好之前的消息,这样上层就可以不再关心bridge是否准备好了的逻辑,更方便调用。

3) iFrame的方式

js端新插入了一个不可见的iFrame,通过修改这个iFramesrc为自定义的schema来通知native端更新,这样可以保证完全不影响任何主Frame的逻辑。当然这种方式主要受限于UIWebView的接口,只能通过修改src的方式来让native端监听到变化,如果是WKWebView则完全没有必要用这种方式处理。

简化的设计

我们希望做一个iOS/Android下边通用的WebViewJavascriptBridge,在js端、iOS端、Android端统一接口,因此并不能直接使用这个开源实现。理论上我们可以根据这个库的实现,在js端稍作修改,判断Android环境上走javascript调用java的方法,而不是修改iFramesrc,同时在Java端解析发过来的消息,跟iOS端一样的解析和处理逻辑,就能实现三端统一的接口。

但是,这是最简单的方式吗?结合我们自己的需求,因为不需要兼容UIWebView, iOS端和Android端都可以在一开始就将相应handler注册进去,所以js一开始加载就可以调用到相应的handler,在js端不需要做消息排队机制,完全可以直接调用。同样因为不用兼容UIWebViewjs也就没必要创建iFrame来实现通知native端消息。那么native端消息排队的机制是必须的吗?如果native主动调用javascript,而javascript加载是异步的,native端除非限制只能在页面加载完之后再调用,否则必须增加消息排队机制,来缓存bridge准备好之前的调用消息。

我们考虑软件分层设计,把客户端作为更为基础的不易变的下层,把h5做为更灵活易变的上层,通常下层是不允许主动调用上层的,而是提供好基础的接口,等待上层调用,如果有状态变化或者其他需要通知上层的,通常是通过上层注册通知回调、消息监听等机制来实现。因此我们应该只需要实现js调用native的方法,并支持方法回调的异步处理机制,以及支持消息监听机制即可满足所有需求。

js调用native的方法可以很自然的联想到RPC(远程过程调用)js等价于RPC中的客户机,native等价于RPC中的服务端,中间的传输不是经过网络而是经过webview提供的js环境和native环境之间的字符串传输。因此可以参考RPC的设计,js需要实现的是每次调用生成唯一id,组装方法名和参数,保存这次调用的回调方法,将这些信息:调用id,调用方法名、方法参数转成字符串,传递给nativenative需要实现的就是接收方法名、方法参数,然后找到对应的方法并执行,并将执行结果返回给jsjs再根据客户端传回的调用id和结果信息找到相应的回调方法并执行。实现js监听native的事件也非常简单,对比RPC调用的每次调用一次回调,监听的方式是一次调用(或者说注册监听),可以允许多次回调,这样native需要将js端注册的回调记录下,当事件发生时,多次调用这个回调,js端在收到native的回调成功后不把回调方法移除,这样就能够收到native的多次回调。

模块化

如果native提供方法很多,则需要根据不同的业务来提供不同的方法实现。可用的模块化方式很多,比如最基本的每个模块自己注册js方法的处理函数的方式,在iOS下也可以利用OCruntime机制将js调用的方法名映射到OC中的方法,在Android下类似,也可以利用Java的反射机制将js调用的方法名映射成Java的类和方法名。

安全性

如果我们nativewebview打开了一个第三方的网站,如果这些方法暴露给了这些第三方网站的js,假设native实现的方法返回了一些敏感信息,那么第三方网站就能获取这些敏感信息。因此native需要判断调用的合法性,我们自己网站可以全部支持httpsnative需要禁止任何http站点的调用,强校验站点的https证书,只有自己站点才允许调用native提供的方法。当然native提供的方法最好不要返回任何敏感信息。

 

koa中间件源码解析

 

  • Koa是现在最流行的基于Node.js平台的web开发框架;
  • 中间件是 Koa 中一个非常重要的概念,利用中间件,可以很方便的处理用户的请求,比如日志,参数解析,权限控制等;
  • 中间件格式为一个高阶函数,一般来说外部的函数接收一个 options 参数,这样方便中间件提供一些配置信息;
  • 内部函数是一个async函数,接收两个参数ctx,next;
  • ctx为请求上下文,调用next将请求扔给下一个中间件处理;
  • 通常将404中间件放在最后,处理所有未知请求

常见的koa中间件实例

// log info 
const log =(level='debug')=>{
    return async (ctx,next) => {
       console.debug(`request url is ${ctx.url}`);
       next() // next() 将请求丢给下一个中间件
        
    };
};


// 404 page
const page404 = () => {
    return async (ctx) => {
        ctx.body = {code: 404, msg: 'not found'}; 
       // 没有调用next ,请求在此返回数据
    }
};

如何使用中间件

const Koa = require('koa');
const app = new Koa();
app.use(log('debug')); // 调用app.use添加中间件
app.use(page404())
app.listen(3000)
  • koa 实例有如下属性
    this.proxy = false;
    this.middleware = []; //存放中间件的数组
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);

 

  • app.use 将async function 添加到 middleware数组中
use(fn) {
   
    this.middleware.push(fn); //添加到 middleware数组中
    return this;
  }
  • app.listen 创建http服务,并且调用callback函数
listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    //http.createServe回调函数接收两个参数 request和response
    return server.listen(...args);
  }
  • callback回调,将请求交给第一个中间件处理
callback() {
    const fn = compose(this.middleware);  
    //compose 将中间件组合起来,并且返回第一个中间件

    ...

    const handleRequest = (req, res) => {
      // 将req和res封装到ctx中
      const ctx = this.createContext(req, res); 
      // fun 为第一个中间件,最先处理ctx上下文
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

  • 通过函数 compose 将中间件组合起来,并且返回第一个中间件
 function compose (middleware) {
  


  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0) // 返回第一个中间件处理
    function dispatch (i) {
      if (i <= index) 
      // 如果同一个中间件 多次调用 next ,抛出错误
      return Promise.reject(new Error('next() called multiple times'))
      
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve() 
    // 所有中间件都处理完毕 ,返回。
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1) 
// 当中间件调用next()时 ,交给下一个中间件处理
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

总结:koa中间件处理流程大致如下

 

  • app = new Koa()生成koa实例
  • app.use 将中间件添加到middleware数组里
  • compose函数将中间件组合起来,并且返回第一个中间件
  • app.listen创建服务,同时调用app.callback()
  • app.callback()将请求交给compose函数返回的第一个中间件处理
  • 调用next将请求扔给下一个中间件处理
  • 最后一个中间件处理后,将数据返回

 

 

 

 

 

 

 

 

 

 

Appium环境搭建介绍

本文主要介绍在mac环境下搭建appium,window版只能执行Android自动化,不能执行IOS

一、安装软件

1. 安装brew,在终端下执行:

/usr/bin/ruby -e “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”

2. 安装libimobiledevice,在终端下执行:

brew install libimobiledevice –HEAD

3. 安装carthage,在终端下执行:

brew install carthage

4. 安装node,在终端下执行:

brew install node

5.安装ios-deploy

npm install -g ios-deploy

6. 安装jdk,可自行下载,建议1.8版本的jdk

7、安装Android SDK,可自行下载最新版,用于Android自动化,如果只跑IOS可不安装

8. 配置环境变量,在终端下编辑/etc/profile文件

vi /etc/profile

内容如下:

export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home

export ANDROID_HOME=/Users/zhanghao/android-sdk

export NODE_PATH=/usr/local/lib/node_module

export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools:$PATH

然后保存,保存后在终端下再输入source /etc/profile

9. 安装appium-doctor,在终端下执行:

npm install -g appium-doctor

10. 安装appium,可在官网下载最新版,建议安装desktop版本

官网:https://github.com/appium/appium-desktop/releases

image2018-10-10_21-50-0.png

11. 检查环境,在终端下执行以下命令

appium-doctor

正确结果如图:

image2018-10-10_21-39-35.png

二、配置WebDriverAgent

1、执行脚本

进入目录:cd /Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-xcuitest-driver/WebDriverAgent

创建目录:mkdir -p Resources/WebDriverAgent.bundle

执行: ./Scripts/bootstrap.sh

2、编译WebDriverAgent

1)appium是通过手机上WebDriverAgentRunner,来运行测试的,没有这个在真机上没有办法测试(模拟器上需要这个,不过会自动安装)

2)用Xcode打开WebDriverAgent,并且编译(编译之前需要一些设置)

进入目录:cd /Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-xcuitest-driver/WebDriverAgent

选中WebDriverAgent.xcodeproj 文件,用xcode打开,并做如图设置。

这时候,会在手机上安装 WebDriverAgentRunner 的app。

请注意手机,如果提示是不收信任的开发者,请在设置-通用-设备管理(描述文件)信任你的apple id就可以了。

至此,Appium环境搭建就结束了,可以执行自动化验证下环境是否OK

用canvas实现一个粒子系统

工作需要实现一些酷炫的烟花特效,本来想上github上当个勤奋的搬运工,无奈没有找到专门的粒子特效库,只有一些零散的文章,于是准备自己实现一个。

写之前呢首先要了解粒子系统的基本概念,有两篇文章对粒子系统描述的比较清楚,一篇是:维基百科/粒子系统 ,另外一篇是unreal的游戏引擎文档粒子系统的关键概念 粒子系统表示三维计算机图形学中模拟一些特定的模糊现象的技术,而这些现象用其它传统的渲染技术难以实现的真实感的游戏图形。经常使用粒子系统模拟的现象有火、爆炸、烟、水流、火花、落叶、云、雾、雪、尘、流星尾迹或者象发光轨迹这样的抽象视觉效果等等。

好了,知道了粒子系统的概念,那开始动手做吧,首先我们可以把粒子系统抽象成三个类

1. 粒子:包含粒子的宽、高、形状、颜色、大小等属性
2. 发射器:用来维护单个粒子的轨迹、状态变化、颜色变化等。
3. 粒子系统:用来维护多个粒子的数量、状态等。

首先,我们来实现第一个类:粒子。(点击看效果

// 粒子
class Particle {
constructor(option) {
if(!option.ctx) {
console.error('ctx must be defined');
return;
}
this.ctx = option.ctx;
this.color = option.color || 'black';
this.x = option.x || 0;
this.y = option.y || 0;
this.radius = option.radius || 20;
}
render(option) {
Object.keys(option).forEach( key => {
this[key] = option[key];
});
ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
}


let canvas = document.getElementById('particle-system');
let ctx = canvas.getContext('2d');
let particle = new Particle({ctx})
canvas.addEventListener('click', (e) => {
ctx.clearRect(0,0, canvas.width, canvas.height);
particle.render({x: e.clientX, y: e.clientY})
})

接下来我们实现发射器来让小球动起来(点击看效果

// 粒子
class Particle {
constructor(option) {
if(!option.ctx) {
console.error('ctx must be defined');
return;
}
this.ctx = option.ctx;
this.color = option.color || 'black';
this.x = option.x || 0;
this.y = option.y || 0;
this.radius = option.radius || 20;
}
render(option) {
Object.keys(option).forEach( key => {
this[key] = option[key];
});
ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
}


// 发射器
class Emitter {
constructor({ctx, x, y, id}) {
this.particle = new Particle({ctx, x, y});
let offset = Math.random()*7 + 3; //3-10随机
let radian = Math.random() * 2 * Math.PI;
this.x = x;
this.y = y;
this.radius = Math.random()*10 + 10; // 10-20随机
this.hue = Math.random() * 200 - 100;
this.color = `hsla(${this.hue}, 100%, 50%, 1)`;
this.offsetX = offset * Math.cos(radian);
this.offsetY = offset * Math.sin(radian);
this.id = id;
}
updateX() { this.x += this.offsetX; }
updateY() { this.y += this.offsetY; }
updateColor() {
this.hue -= 0.5;
this.color = `hsla(${this.hue}, 100%, 50%, 1)`;
}
updateRadius() {
this.radius = this.radius*0.98;
}

update() {
this.updateX();
this.updateY();
this.updateColor();
this.updateRadius();
this.particle.render({
x: this.x,
y: this.y,
color: this.color,
radius: this.radius,
});
}
}

let canvas = document.getElementById('particle-system');
let ctx = canvas.getContext('2d');
let emitters = []
canvas.addEventListener('click', (e) => {
let emitter = new Emitter({ctx, x: e.clientX, y: e.clientY});
emitters.push(emitter);
});
let loop = () => {
window.requestAnimationFrame(() => {
ctx.clearRect(0,0, canvas.width, canvas.height);
emitters.forEach(emitter => {
emitter.update();
})
loop();
})
}
loop();

最后一步,我们来实现一个粒子系统来维护发射器的数量和生命周期(点击看效果

// 粒子
class Particle {
constructor(option) {
if(!option.ctx) {
console.error('ctx must be defined');
return;
}
this.ctx = option.ctx;
this.color = option.color || 'black';
this.x = option.x || 0;
this.y = option.y || 0;
this.radius = option.radius || 20;
}
render(option) {
Object.keys(option).forEach( key => {
this[key] = option[key];
});
ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
}


// 发射器
class Emitter {
constructor({ctx, x, y, id}) {
this.particle = new Particle({ctx, x, y});
let offset = Math.random()*7 + 3; //3-10随机
let radian = Math.random() * 2 * Math.PI;
this.x = x;
this.y = y;
this.radius = Math.random()*10 + 10; // 10-20随机
this.hue = Math.random() * 200 - 100;
this.color = `hsla(${this.hue}, 100%, 50%, 1)`;
this.offsetX = offset * Math.cos(radian);
this.offsetY = offset * Math.sin(radian);
this.id = id;
}
updateX() { this.x += this.offsetX; }
updateY() { this.offsetY += 0.5 ; this.y += this.offsetY; }
updateColor() {
this.hue -= 0.5;
this.color = `hsla(${this.hue}, 100%, 50%, 1)`;
}
updateRadius() {
this.radius = this.radius*0.97;
}

update() {
this.updateX();
this.updateY();
this.updateColor();
this.updateRadius();
this.particle.render({
x: this.x,
y: this.y,
color: this.color,
radius: this.radius,
});
}
}


// 粒子系统
class ParticleSystem {

constructor({ctx, width, height }) {
this.ctx = ctx;
this.particleNu = Math.random() * 20 + 30; // 30~50
this.width = width;
this.height = height;
this.requestId = 0;
}
addParticle(x, y) {
for(let i=0; i< this.particleNu; i++) { let id = ParticleSystem.emitterOffset; let emitter = new Emitter( {ctx: this.ctx, x, y, id}) ParticleSystem.emitters.set(id, emitter); ParticleSystem.emitterOffset++; } } nextTick() { if(ParticleSystem.emitters.size === 0) { return; } ctx.clearRect(0,0, canvas.width, canvas.height); ParticleSystem.emitters.forEach(emitter => {
if(emitter.x < 0 || emitter.y < 0 || emitter.x > this.width || emitter.y > this.height) {
ParticleSystem.emitters.delete(emitter.id);
return;
}
emitter.update();
})
this.requestId = window.requestAnimationFrame(() => {
this.nextTick();
});
}
burst(x, y) {
this.addParticle(x, y);
window.cancelAnimationFrame(this.requestId);
this.nextTick()
}
}


ParticleSystem.emitters = new Map();
ParticleSystem.emitterOffset = 0;

let canvas = document.getElementById('particle-system');
let ctx = canvas.getContext('2d');
let particleSystem = new ParticleSystem({
ctx,
width: canvas.width,
height: canvas.height
})
canvas.addEventListener('click', (e) => {
console.log(e);
particleSystem.burst(e.clientX, e.clientY);
})

大功告成,是不是很简单。后续我们还可以继承我们的类来进行扩展,比方重写particle的render方法来把小球变成雪花、五角星,给Emitter类添加新的方法来给小球的轨迹增加重力、加速度、旋转等物理效果,还可以修改ParticleSystem的逻辑来将目前的爆炸效果改成下雨、云雾等效果,限制我们的只是我们的想象力。

kubernetes基于filebeat日志的动态采集

背景需求

当业务迁移到kubernetes中以后,由于服务多副本可能存在不同的机器上面,在传统日志落盘宿主机文件会造成同个服务日志散落在不同机器上面,这就需要考虑如何把每个副本日志汇总在一起提供给开发人员查看,对于传统日志采集器会将其部署在Pod中以sidecar方式存在采集某个目录下的日志文件,但是会带来如下问题:

  1. 需要对kubernetes的yaml文件进行单独配置,略显繁琐;增加配置复杂度
  2. 需要被采集日志的每个Pod都要配置,依赖中;不便于管理以及升级维护

对于熟悉Prometheus的开发者而言,我们知道它提供SD(service discovery)的工作,只需要在每个service注明annotations需要被采集的端口、metrics接口、是否采集等字样标注即可。对于日志系统,保证日志稳定性、低延迟的同时也可以简化配置项、带来如下的好处:

  1. 每个pod通过annotations来选择是否需要被日志采集,不需要额外繁杂配置
  2. 支持非json输出以及json decode

基于以上需求考虑,我们可在每个Node节点通过Daemonset方式运行filebeat插件做日志采集.

架构

采集原理介绍:

在进行日志收集的过程中, 我们采用filebeat, 因为它足够轻量级, 内存占用不到30M左右,同时filebeat 6.0以上支持 add_kubernetes_metadata插件为日志添加kubernetesmetadata。其原理是扫描docker container /var/lib/docker/container/*/*.log日志的同 时,会通过container id去kubernetes api匹配对应的pod为每条日志打上kubernetes pod相关的标签,然后我们可以利用这些标签信息做一些日志处理。

原始数据输出格式,多了一个kubernetes 的filed:

{
"@timestamp": "2017-10-11T21:28:55.426Z",
"@metadata": {
"beat": "filebeat",
"type": "doc",
"version": "6.0.0-rc1"
}, "beat": {
"name": "filebeat-filebeat-dwjf3",
"hostname": "filebeat-filebeat-dwjf3",
"version": "6.0.0-rc1"
},
"kubernetes": {
"container": {
"name": "filebeat"
}, "pod": {
"name": "filebeat-filebeat-dwjf3"
},
"namespace": "default",
"labels": {
"release": "filebeat",
"app": "filebeat",
"controller-revision-hash": "4244555777",
"pod-template-generation": "1"
} },
"source":"/var/lib/docker/containers/0caeb502469d7f0dfb90c4154294f95f2f9dd206a21cc003f118353d0d6fad75/0caeb502469d7f0dfb90c4154294f95f2f9dd206a21cc003f118353d0d6fad75-json.log","offset":13157,"message": "{\"log\":\"2017/10/11 21:28:50.328022 metrics.go:39: INFO Non-zero metrics in the last 30s: beat.memstats.gc_next=14161552 beat.memstats.memory_alloc=8355272 beat.memstats.memory_total=415714016 filebeat.events.active=4117 filebeat.events.added=90167 filebeat.events.done=86050 filebeat.harvester.open_files=34 filebeat.harvester.running=34filebeat.harvester.started=34 libbeat.output.events.acked=86016x1x`libbeat.output.events.active=2048 libbeat.output.events.batches=43 libbeat.output.events.total=88064 libbeat.output.type=console libbeat.output.write.bytes=39752840 libbeat.pipeline.clients=1 libbeat.pipeline.events.active=4117 libbeat.pipeline.events.filtered=34libbeat.pipeline.events.published=90132libbeat.pipeline.events.total=90167 libbeat.pipeline.queue.acked=86016registrar.states.current=34registrar.states.update=86050registrar.writes=44\\n\",\"stream\":\"stderr\",\"time\":\"2017-10-11T21:28:
50.32829028Z\"}"
}

filebeat daemonset 配置:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: filebeat-config
  namespace: kube-system
  labels:
    k8s-app: filebeat
data:
  filebeat.yml: |-
    filebeat.config:
      inputs:
        # Mounted `filebeat-inputs` configmap:
        path: /usr/share/filebeat/inputs.d/*.yml
        # Reload inputs configs as they change:
        reload.enabled: true
      modules:
        path: /usr/share/filebeat/modules.d/*.yml
        # Reload module configs as they change:
        reload.enabled: true
    output.kafka:
      hosts:
        10.100.7.203:9092
        10.100.8.49:9092
        10.100.7.88:9092
      version: 0.9.0.1
      topic: 'filebeat-kubernetes-palfish'
      partition.round_robin:
        reachable_only: false
      compression: snappy
    spool_size: 2048
    idle_timeout: 5s
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: filebeat-inputs
  namespace: kube-system
  labels:
    k8s-app: filebeat
data:
  kubernetes.yml: |-
    - type: docker
      # filebeat 7.0
      combine_partials: true
      containers.ids:
        "*"
      encoding: plain
      fields_under_root: true
      ignore_older: 0
         close_older: 1h
      scan_frequency: 10s
      harvester_buffer_size: 16384
      max_bytes: 10485760
      tail_files: true
      backoff: 1s
      max_backoff: 10s
      backoff_factor: 2
      # comment(wangyichen),pod templateannotatioES.
      # filebeat.harvest: Only harvest pods that have a value of `true`
      # filebeat.index: Set the index value stored in elasticsearch
      processors:
        - add_kubernetes_metadata:
            in_cluster: true
            include_annotations:
              "filebeat.harvest"
              "filebeat.index"
        # comment(wangyichen): drop_eventdrop_fields drop_eventkubernetes.annotations.
        - drop_event:
            when:
              not:
                equals:
                  kubernetes.annotations.filebeat.harvest: "true"
        - decode_json_fields:
            fields: ["message"]
            target: ""
            overwrite_keys: true
            when:
              regexp:
                message: "^{.*}$"
        - rename:
             fields:
               - from: "message"
                 to: "logs"
             when:
               not:
                 regexp:
                   message: "^{.*}$"
        - drop_fields:
            fields:
              "beat"
              "@metadata"
                “kubernetes.labels"
              "prospector"
              "input"
              "offset"
              "stream"
              "source"
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
     name: filebeat
  namespace: kube-system
  labels:
    k8s-app: filebeat
spec:
  template:
    metadata:
      labels:
        k8s-app: filebeat
    spec:
      serviceAccountName: filebeat
      terminationGracePeriodSeconds: 30
      containers:
        - name: filebeat
          image: wangyichen/filebeat:6.3.1
          args: [
            "-c""/etc/filebeat.yml","-e", ]
          securityContext:
            runAsUser: 0
          resources:
            limits:
              memory: 200Mi
            requests:
              cpu: 100m
              memory: 100Mi
          volumeMounts:
            - name: config
              mountPath: /etc/filebeat.yml
              readOnly: true
              subPath: filebeat.yml
            - name: inputs
              mountPath: /usr/share/filebeat/inputs.d
              readOnly: true
            - name: data
              mountPath: /usr/share/filebeat/data
            - name: varlibdockercontainers
              mountPath: /var/lib/docker/containers
              readOnly: true
      volumes:
        - name: config
          configMap:
            defaultMode: 0600
            name: filebeat-config
        - name: varlibdockercontainers
          hostPath:
            path: /data/data/docker/containers
        - name: inputs
          configMap:
            defaultMode: 0600
            name: filebeat-inputs
        # We set an `emptyDir` here to ensure the manifest will deploy correctly.
        # It's recommended to change this to a `hostPath` folder, to ensure internal data
        # files survive pod changes (ie: version upgrade)
        - name: data
          emptyDir: {}
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: filebeat
subjects:
  - kind: ServiceAccount
    name: filebeat
    namespace: kube-system
roleRef:
  kind: ClusterRole
  name: filebeat
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: filebeat
  labels:
    k8s-app: filebeat
rules:
  "" indicates the core API group
  - apiGroups: [""]
    resources:
      - namespaces
      - pods
    verbs:
      - get
      - watch
      - list
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: filebeat
  namespace: kube-system
  labels:
    k8s-app: filebeat
---

业务使用

需要在被采集的pod template 中添加如下annotation字段

  • filebeat.harvest: “true” #只有当此annotation 设置为“true”时,日志才会被采集。
  • filebeat.index: “account” # 根据这个值定义我们在日志最终输出到ES中哪个索引,最终的名称是kubernetes-account-%{+YYYY.MM.dd}”
栗子如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: account
  namespace: base-platform
  labels:
    app.kubernetes.io/name: account
    app.kubernetes.io/component: base-platform
    app.kubernetes.io/part-of: account
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
    rollingUpdate:
maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      product: base-platform
      app: account
      env: production
  template:
    metadata:
      annotations:
        filebeat.harvest: "true"
        filebeat.index: "account"
      labels:
        product: base-platform
        app: account
        env: production
    spec:
      imagePullSecrets:
        - name: docker-secret
      dnsConfig:
        options:
          - name: ndots
            value: '2'
          - name: timeout
......

存在的问题

Docker json-file driver splits log lines larger than 16k bytes
# https://github.com/moby/moby/issues/32923

在docker1.26版本以后,每行日志大小不超过16K,否则将被截断放入下一行,这将导致超过16K的日志不完整。filebeat master代码已经实现了日 志的拼接,将在7.0得到支持。https://www.elastic.co/guide/en/beats/filebeat/master/filebeat-input-docker.html

Android APP集成FCM介绍

1. 集成需求介绍

安卓系统的APP,如果涉及推送系统,一般会基于Socket在后台维持一个与APP服务器通信的长连接。但是这个长连接不稳定,当APP退到后台运行以后,系统会关闭APP并收回分配的资源,Socket长连接会被断掉,从而使APP失去与服务器的通信联系。

APP与服务器的通信断开之后,APP就不再能收到服务器发出的通知,导致用户不能及时收到相应的通知而错失及时处理问题的机会。

类似苹果推送,在安卓手系统上,谷歌提供推送服务FCM(Firebase Cloud Messaging)。FCM的运行依赖谷歌提供的Google Play Services服务,但是因为网络问题谷歌的服务无法在国内使用,中国大陆手机厂商都会把安卓手机上的谷歌相关的服务移除,结果在大陆地区就没有类似苹果设备统一的推送服务可用。

大陆地区有很多第三方的推送服务商,提供的服务大致分为两种,一种是手机厂商自己提供的推送服务,例如华为和小米;另一种是非手机厂商提供的消息推送服务,例如百度推送、极光推送、腾讯信鸽等,比较杂乱,效果也众说纷纭。

大陆地区有一个叫做统一推送联盟的组织,2017年由中国信息通信研究院泰尔终端实验室发起(包含:华为、小米、vivo、OPPO、三星等手机厂商;百度、阿里、腾讯、奇虎科技等互联网企业;个推、极光等第三方推送商),意在构建一个国内可用的,统一的推送服务入口,逐步解决国内安卓推送碎片化的局面。效果如何,需要持续关注。

2. FCM原理介绍

FCM集成的实现包括用于发送和接收的两个主要组件:

  • 一个受信任的环境,例如向用户APP发送消息的后台服务;
  • 一个接收消息的客户端应用;

FCM提供了连接两个组件的服务:

  • FCM的消息服务,连接用户消息服务器和用户设备。提供了HTTP和XMPP API供用户服务器管理和发送消息,还提供了Admin SDK支持开发一个基于移动设备的消息管理程序;

  • Google Play services,用于接收FCM消息服务发来的消息,分发到客户端SDK进行处理;

运行集成FCM的客户端的移动设备上需要运行安卓4.0及以上版本系统,并安装运行Google Play services服务15.0.0或者更高版本。Google Play services服务在终端设备上是常驻的,开启后不会被系统自动关闭,这样就能保证客户端在有网络连接的情况下,随时收到来自FCM消息服务器的通知和消息。如果移动终端没有安装Google Play services或者因为网络原因无法连接到谷歌的FCM消息服务器,就不能正常地接收到消息。

FCM的主要功能 功能描述
发送通知消息或数据消息 发送向用户显示的通知消息,或者发送数据消息并完全确定应用代码中会发生的情况
通用消息定位 使用以下三种方式中的任意一种将消息分发到客户端应用:分发至单一设备、分发至群组设备、分发至订阅特定主题的设备。
从客户应用发送消息 通过FCM可靠而省电的连接通道,将确认消息、聊天消息及其他消息从设备发回至服务器

FCM可以发送通知类消息和数据消息两种,这两种消息的有效负载上线均为4KB,数据中的Token是指后面要说到的注册令牌。

  • 通知类消息负载
{
  "message":{
    "token":"...",
    "notification":{
      "title":"Portugal vs. Denmark",
      "body":"great match!"
    }
  }
}
  • 数据类消息负载
{
  "message":{
    "token":"...",
    "data":{
      "Nick" : "Mario",
      "body" : "great match!",
      "Room" : "PortugalVSDenmark"
    }
  }
}

3. FCM和APP内推送的协调

应用在后台运行时,通知类消息负载会被传递到通知面板;应用在前台运行时,通知类型的消息负载不会被传递到通知面板,APP可以在FirebaseMessagingService中定义的回调函数onMessageReceived中处理接收到的消息负载,包括通知类型负载和数据类型的负载;数据类型的消息负载不论在前后台运行,都可以在这里接收处理。

如果希望对FCM展示的通知有点击效果,例如点击某个通知后可以调起某个APP内的界面或者功能,可以给消息同时加上通知类型负载和数据类型负载,通知类型负载会被显示在通知面板中,数据类型的负载可以在点击事件后,在启动器Intent的extras中获取处理。

集成FCM就要在APP内的推送和FCM之间就需要做一个协调,避免出现一个用户通知在移动设备上被展示两遍的现象,一遍来做FCM,一遍来自APP内推送通道。

建议的方式是,只对通知面板的显示做协调处理,对APP内功能性的弹出式提醒或或静默式的触发操作依旧只在APP内推送通道处理,既FCM只负责传递需要在通知面板展示的通知类消息推送。

APP启动设置默认关闭FCM通道,在Application初始化过程中检查Google Play Services的可用性,一旦确定打开FCM就关闭APP内推送通道的Notification功能。

4. 集成流程介绍

  • 第一步,要到Firebase控制台申请创建APP,按照下面的的步骤,可以得到一个名为google-services.json的配置文件,把这个文件下载并放在工程内app/目录下。




  • 第二步,配置Google Services插件,向根级build.gradle文件中添加规则,以引入google-services插件和Google的maven仓库:

buildscript {
    // ...
    dependencies {
        // ...
        classpath 'com.google.gms:google-services:4.0.1' // google-services plugin
    }
}
allprojects {
    // ...
    repositories {
        // ...
        maven {
            url "https://maven.google.com" // Google's Maven repository
        }
    }
}
  • 第三步,在app/build.gradle的底部添加apply plugin代码,启用google-services插件,并添加FCM所需要的库

需要及时更新firebase-core和firebase-messaging,否则可能会收不到FCM服务器发来的消息。

apply plugin: 'com.android.application'
android {
  // ...
}
dependencies {
  // ...
  compile 'com.google.firebase:firebase-core:16.0.1'
  compile 'com.google.firebase:firebase-messaging:17.0.0'
}
// 虽然通常把apply放在文件顶部,但在文档中明确要求放在文件的底部,就放在底部
apply plugin: 'com.google.gms.google-services'
  • 第四步,在应用清单中设置通知面板图标和颜色并设置FCM默认不启动,ic_notification图片不宜过大,过大的图片在一些手机上显示不出来,建议100*100左右合适,不要留边框或者边缘透明区域;
<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_notification" />
<meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/color_src" />
<meta-data android:name="firebase_messaging_auto_init_enabled"
    android:value="false" />
  • 第五步,APP初始化过程中检查Google Play Services的可用性,并决定是否开启FCM,如果开启则根据“协调”中的建议或者自选的逻辑关闭APP内推送通知的Notification功能。
// 示例程序,这个方法是写在Application类中的,所以这里的this指的是Application实例对象
private void checkGoogleService() {
    final int available = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this);
    switch (available) {
        case ConnectionResult.SUCCESS:
            FirebaseMessaging.getInstance().setAutoInitEnabled(true);
            // TODO:对APP推送通知的处理
            break;
        case ConnectionResult.SERVICE_MISSING:
            Log.d(TAG,"check google service service missing");
            break;
        case ConnectionResult.SERVICE_UPDATING:
            Log.d(TAG,"check google service service updating");
            break;
        case ConnectionResult.SERVICE_DISABLED:
            Log.d(TAG,"check google service service disabled");
            break;
        case ConnectionResult.SERVICE_INVALID:
            LogEx.d(TAG,"check google service service invalid");
            break;
        case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
            Log.d(TAG,"check google service version too old");
            ToastUtil.showLENGTH_LONG("Google play service need to update");
            break;
        default:
            Log.d(TAG,"check google service default: available = " + available);
            break;
    }
}
  • 第六步,注册令牌。FCM消息服务器是通过一个叫注册令牌的字符串来定位设备的,APP初次启动时,FCM SDK会为客户端应用生成一个注册令牌,客户端应用需要获取到这个注册令牌并发送到APP服务器并和当前APP用户关联保存,当APP服务器需要向指定用户发送通知消息,需要检索到与用户关联保存的注册令牌,通过这个注册令牌向FCM消息服务器发送通知消息,FCM消息服务器通过这个注册令牌,定位移动设备,并发送通知消息。

注册令牌在以下几种情况会有变更:

    1. 通过FirebaseInstanceId.getInstance().deleteToken()主动删除注册令牌
    1. 用户卸载重装APP
    1. 用户清除应用数据
    1. 应用在新设备上恢复

以上可知,注册令牌跟客户端APP具体的登录用户是没有关联的。所以,在APP启动完成后要确认获取注册令牌并与用户关联保存到APP服务器,在用户退出客户端APP时,要通知APP服务器把关联保存的注册令牌删除,避免服务器错误地把已经退出的用户的通知消息,发送到无效的设备上。

监听注册令牌的生成,需要继承FirebaseInstanceIdService并在onTokenRefresh回调中使用FirebaseInstanceId.getInstance().getToken()方法获取并在本地持久化保存。

<service android:name="com.fcm.MyFireBaseInstanceIDService">
    <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
    </intent-filter>
</service>
package com.fcm;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.FirebaseInstanceIdService;
public class MyFireBaseInstanceIDService extends FirebaseInstanceIdService {
    @Override
    public void onTokenRefresh() {
        String token = FirebaseInstanceId.getInstance().getToken();
        // TODO: 本地持久化保存获取到的注册令牌
    }
}
  • 第七步,如果按照”协调”中的建议,FCM只处理通知类型的消息,这一步不需要。如果希望对FCM消息做更多的处理,需要继承PalFishFireBaseMessagingService并在onMessageReceived回调方法中拆解消息内容,根据APP内部协议进行相应的处理。
<service android:name="com.fcm.MyFireBaseMessagingService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
package com.fcm;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
public class MyFireBaseMessagingService extends FirebaseMessagingService {
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // TODO: 处理接收到的FCM消息
    }
    // 在某些情况下,FCM 可能不会传递消息。如果在特定设备连接 FCM 时,您的应用在该设备上的待处理消息过多(超过 100 条),
    // 或者如果设备超过一个月未连接到 FCM,就会发生这种情况。在这些情况下,您可能会收到对 FirebaseMessagingService.onDeletedMessages() 的回调。
    // 当应用实例收到此回调时,应会执行一次与您的应用服务器的完全同步。如果您在过去 4 周内未向该设备上的应用发送消息,FCM 将不会调用 onDeletedMessages()。
    @Override
    public void onDeletedMessages() {
    }
}

5. 集成过程中的问题

    1. 在app/build.gradle中引入firebase-core和firebase-messaging库时,遇到firebase-messaging版本不匹配,导致开启FCM功能FirebaseMessaging.getInstance().setAutoInitEnabled(true)时,出现问题:No virtual method zzcz(Z)V in class com.google.firebase.iid.FirebaseInstanceId,解决办法就是调整firebase-core和firebase-messaging的版本,两个库的版本可以在maven仓库中搜索。
    1. 检查Google Play Services可用性的方法GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)并不总是可靠的,例如中国大陆运行的手机,如果安装了Google Play Services服务,返回值也会是true,对于从中国大陆以外进入大陆地区的用户,会因为网络不通无法接收FCM消息,而启动时又依据这个判定打开FCM并关闭APP内推动通知,导致用户在一段时间内收不到任何通知。建议在APP设置中给一个FCM的开关,如果遇到这种情况,关闭FCM开关之后不做可用性检查。