JS的new关键字都干了什么?

JS的new关键字都干了什么?
写在前面:
new关键字在实例化获取对象时都做了什么?是一道经常出现在前端面试时的问题。如果只是简单的了解new关键字是实例化构造函数获取对象,是万万不能够的。更深入的层级发生了什么呢?同时面试官想从这道题里面考察什么呢?下面为各位小伙伴一一来解密。
另外,文末有给部分伙伴总结了一些入门的视频教程,一些伙伴,之前留言要的,你们自己来认领哈!需要更有难度的伙伴,可以再留言!
一、new关键字
new关键字的作用:通过new关键字实例化构造函数,获取对象。说一千道一万,不如来段代码看一看
// 定义构造函数
function Person (name, age) {
this.name = name
this.age = age
this.say = function () {
console.log(`my name is ${this.name}, my age is ${this.age}`)
}
}
// 构造函数的原型属性和方法定义
Person.prototype.color = ‘yellow’
Person.prototype.sayBye = function () {
console.log(‘Bye!’)
}
// 实例化
let p = new Person(cc’, 18)
console.log(p)
// 当前属性
console.log(p.name)
// 当前方法
p.say()
console.log(p.color)
// 原型方法
p.sayBye()
二、伪代码演示过程
通过new关键字实例化的对象p,具备了构造函数Person中this的属性:name、age,也具备了构造函数Person的原型prototype的属性color和方法sayBye。下面我们来通过伪代码来看看具体的实现过程。
初始化新对象var o = {}
原型的执行,确定对象o的原型链o.__proto__ = Person.prototype
绑定this对象为o,传入参数;执行Person构造函数,进行属性和方法的赋值操作Person.call(o, ‘cc’, 18)
返回结果注意:在通过该种方式获取对象时,*终不一定返回的是对象o,要看构造函数的返回值是什么。如果函数返回的是基本类型值,实际会生成一个对象,返回o 如果是函数返回的是引用类型值,则实际返回的是该引用类型值
给大家总结的一些:Web前端小白入门必看预习视频https://pan.baidu.com/s/1p_nQb8GBwQ_N_6CqoocEWQ 提取码:fv71
*后:伙伴们有不清楚的地方可以留言。更多的前端相关教程也会继续为大家更新!

Node.js 源码解析 util.promisify 如何将 Callback 转为 Promise

Node.js 源码解析 util.promisify 如何将 Callback 转为 Promise
Nodejs util 模块提供了很多工具函数。为了解决回调地狱问题,Nodejs v8.0.0 提供了 promisify 方法可以将 Callback 转为 Promise 对象。
工作中对于一些老项目,有 callback 的通常也会使用 util.promisify 进行转换,之前更多是知其然不知其所以然,本文会从基本使用和对源码的理解实现一个类似的函数功能。
1. Promisify 简单版本实现
在介绍 util.promisify 的基础使用之后,实现一个自定义的 util.promisify 函数的简单版本。
1.1 util promisify 基本使用
将 callback 转为 promise 对象,首先要确保这个 callback 为一个错误优先的回调函数,即 (err, value) => … err 指定一个错误参数,value 为返回值。
以下将以 fs.readFile 为例进行介绍。
创建一个 text.txt 文件
创建一个 text.txt 文件,写入一些自定义内容,下面的 Demo 中我们会使用 fs.readFile 来读取这个文件进行测试。
// text.txt
Nodejs Callback 转 Promise 对象测试
传统的 Callback 写法
const util = require(‘util’);
fs.readFile(‘text.txt’, ‘utf8’, function(err, result) {
  console.error(‘Error: ‘, err);
  console.log(‘Result: ‘, result); // Nodejs Callback 转 Promise 对象测试
});
Promise 写法
这里我们使用 util.promisify 将 fs.readFile 转为 Promise 对象,之后我们可以进行 .then、.catch 获取相应结果
const { promisify } = require(‘util’);
const readFilePromisify = util.promisify(fs.readFile); // 转化为 promise
readFilePromisify(‘text.txt’, ‘utf8’)
  .then(result => console.log(result)) // Nodejs Callback 转 Promise 对象测试
  .catch(err => console.log(err));
1.2 自定义 mayJunPromisify 函数实现
自定义 mayJunPromisify 函数实现 callback 转换为 promise,核心实现如下:
行 {1} 校验传入的参数 original 是否为 Function,不是则抛错
promisify(fs.readFile) 执行之后会返回一个函数 fn,行 {2} 定义待返回的 fn 函数,行 {3} 处返回
fn 返回的是一个 Promise 对象,在返回的 Promise 对象里执行 callback 函数
function mayJunPromisify(original) {
  if (typeof original !== ‘function’) { // {1} 校验
    throw new Error(‘The “original” argument must be of type Function. Received type undefined’)
  }
  function fn(…args) { // {2}
    return new Promise((resolve, reject) => {
      try {
        // original 例如,fs.readFile.call(this, ‘filename’, ‘utf8’, (err, result) => …)
        original.call(this, …args, (err, result) => {
          if (err) {
            reject(err);
          } else {
            resolve(result);
          }
        });
      } catch(err) {
        reject(err);
      }
    });
  }
  return fn; // {3}
}
现在使用我们自定义的 mayJunPromisify 函数做一个测试
const readFilePromisify = mayJunPromisify(fs.readFile);
readFilePromisify(‘text.txt’, ‘utf8’)
  .then(result => console.log(result)) // Nodejs Callback 转 Promise 对象测试
  .catch(err => console.log(err));
2. Promisify 自定义 Promise 函数版本实现
另一个功能是可以使用 util.promisify.custom 符号重写 util.promisify 返回值。
2.1 util.promisify.custom 基本使用
在 fs.readFile 上定义 util.promisify.custom 符号,其功能为禁止读取文件。
注意顺序要在 util.promisify 之前。
fs.readFile[util.promisify.custom] = () => {
  return Promise.reject(‘该文件暂时禁止读取’);
}
const readFilePromisify = util.promisify(fs.readFile);
readFilePromisify(‘text.txt’, ‘utf8’)
  .then(result => console.log(result))
  .catch(err => console.log(err)); // 该文件暂时禁止读取
2.2 自定义 mayJunPromisify.custom 实现
定义一个 Symbol 变量 kCustomPromisifiedSymbol 赋予 mayJunPromisify.custom
行 {1} 校验是否有自定义的 promise 化函数
行 {2} 自定义的 mayJunPromisify.custom 也要保证是一个函数,否则抛错
行 {3} 直接返回自定义的 mayJunPromisify.custom 函数,后续的 fn 函数就不会执行了,因此在这块也就重写了 util.promisify 返回值
// 所以说 util.promisify.custom 是一个符号
const kCustomPromisifiedSymbol = Symbol(‘util.promisify.custom’);
mayJunPromisify.custom = kCustomPromisifiedSymbol;
function mayJunPromisify(original) {
  if (typeof original !== ‘function’) {
    throw new Error(‘The “original” argument must be of type Function. Received type undefined’)
  }
  // 变动之处 -> start
  if (original[kCustomPromisifiedSymbol]) { // {1}
    const fn = original[kCustomPromisifiedSymbol];
    if (typeof fn !== ‘function’) { // {2}
      throw new Error(‘The “mayJunPromisify.custom” property must be of type Function. Received type number’);
    }
    // {3}
    return Object.defineProperty(fn, kCustomPromisifiedSymbol, {
      value: fn, enumerable: false, writable: false, configurable: true
    });
  }
  // end <- 变动之处
  function fn(…args) {
    …
  }
  return fn;
}
同样测试下我们自定义的 mayJunPromisify.custom 函数。
fs.readFile[mayJunPromisify.custom] = () => {
  return Promise.reject(‘该文件暂时禁止读取’);
}
const readFilePromisify = mayJunPromisify(fs.readFile);
readFilePromisify(‘text.txt’, ‘utf8’)
  .then(result => console.log(result))
  .catch(err => console.log(err)); // 该文件暂时禁止读取
3. Promisify 回调函数的多参转换
通常情况下我们是 (err, value) => … 这种方式实现的,结果只有 value 一个参数,但是呢有些例外情况,例如 dns.lookup 它的回调形式是 (err, address, family) => … 拥有三个参数,同样我们也要对这种情况做兼容。
3.1 util.promisify 中的基本使用
和上面区别的地方在于 .then 接收到的是一个对象 { address, family } 先明白它的基本使用,下面会展开具体是怎么实现的
const dns = require(‘dns’);
const lookupPromisify = util.promisify(dns.lookup);
lookupPromisify(‘nodejs.red’)
  .then(({ address, family }) => {
    console.log(‘地址: %j 地址族: IPv%s’, address, family);
  })
  .catch(err => console.log(err));
3.2 util.promisify 实现解析
类似 dns.lookup 这样的函数在回调(Callback)时提供了多个参数列表。
为了支持 util.promisify 也都会在函数上定义一个 customPromisifyArgs 参数,value 为回调时的多个参数名称,类型为数组,例如 dns.lookup 绑定的 customPromisifyArgs 的 value 则为 [‘address’, ‘family’],其主要目的也是为了适配 util.promisify。
dns.lookup 支持 util.promisify 核心实现
// https://github.com/nodejs/node/blob/v12.x/lib/dns.js#L33
const { customPromisifyArgs } = require(‘internal/util’);
// https://github.com/nodejs/node/blob/v12.x/lib/dns.js#L159
ObjectDefineProperty(lookup, customPromisifyArgs,
                     { value: [‘address’, ‘family’], enumerable: false });
customPromisifyArgs
customPromisifyArgs 这个参数是从 internal/util 模块导出的,仅内部调用,因此我们在外部 util.promisify 上是没有这个参数的。
也意味着只有 Nodejs 模块中例如 dns.klookup()、fs.read() 等方法在多参数的时候可以使用 util.promisify 转为 Promise,如果我们自定义的 callback 存在多参数的情况,使用 util.promisify 则不行,当然,如果你有需要也可以基于 util.promisify 自己封装一个。
// https://github.com/nodejs/node/blob/v12.x/lib/internal/util.js#L429
module.exports = {
  …
  // Symbol used to customize promisify conversion
  customPromisifyArgs: kCustomPromisifyArgsSymbol,
};
util.promisify 核心实现解析
参见源码 internal/util.js#L277
行 {1} 定义 Symbol 变量 kCustomPromisifyArgsSymbol
行 {2} 获取参数名称列表
行 {3} (err, result) 改为 (err, …values),原先 result 仅接收一个参数,改为 …values 接收多个参数
行 {4} argumentNames 存在且 values > 1,则回调会存在多个参数名称,进行遍历,返回一个 obj
行 {5} 否则 values *多仅有一个参数名称,即数组 values 有且仅有一个元素
// https://github.com/nodejs/node/blob/v12.x/lib/internal/util.js#L277
const kCustomPromisifyArgsSymbol = Symbol(‘customPromisifyArgs’); // {1}
function promisify(original) {
  …
  // 获取多个回调函数的参数名称列表
  const argumentNames = original[kCustomPromisifyArgsSymbol]; // {2}
  function fn(…args) {
    return new Promise((resolve, reject) => {
      try {
        // (err, result) 改为 (err, …values) {3}
        original.call(this, …args, (err, …values) => {
          if (err) {
            reject(err);
          } else {
            // 变动之处 -> start
            // argumentNames 存在且 values > 1,则回调会存在多个参数名称,进行遍历,返回一个 obj
            if (argumentNames !== undefined && values.length > 1) { // {4}
              const obj = {};
              for (let i = 0; i < argumentNames.length; i++)
                obj[argumentNames[i]] = values[i];
              resolve(obj);
            } else { // {5} 否则 values *多仅有一个参数名称,即数组 values 有且仅有一个元素
              resolve(values[0]);
            }
            // end <- 变动之处
          }
        });
      } catch(err) {
        reject(err);
      }
    });
  }
  return fn;
}
4. 实现一个完整的 promisify
上面*、第二节我们自定义实现的 mayJumPromisify 分别实现了含有 (err, result) => … 和自定义 Promise 函数功能。
第三节中介绍的回调函数多参数转换,由于 kCustomPromisifyArgsSymbol 使用 Symbol 声明(每次重新定义都会不一样),且没有对外提供,如果要实现第三个功能,需要我们每次在 callback 函数上重新定义 kCustomPromisifyArgsSymbol 属性。
例如,以下定义了一个 callback 函数用来获取用户信息,返回值是多个参数 name、age,通过定义 kCustomPromisifyArgsSymbol 属性,即可使用我们自己写的 mayJunPromisify 来转换为 Promise 形式。
function getUserById(id, cb) {
  const name = ‘张三’, age = 20;
  cb(null, name, age);
}
Object.defineProperty(getUserById, kCustomPromisifyArgsSymbol, {
  value: [‘name’, ‘age’], enumerable: false
})
const getUserByIdPromisify = mayJunPromisify(getUserById);
getUserByIdPromisify(1)
  .then(({ name, age }) => {
    console.log(name, age);
  })
  .catch(err => console.log(err));
自定义 mayJunPromisify 实现源码
https://github.com/Q-Angelo/project-training/tree/master/nodejs/module/promisify

Node.js 源码解析 util.promisify 如何将 Callback 转为 Promise

Node.js 源码解析 util.promisify 如何将 Callback 转为 Promise
Nodejs util 模块提供了很多工具函数。为了解决回调地狱问题,Nodejs v8.0.0 提供了 promisify 方法可以将 Callback 转为 Promise 对象。
工作中对于一些老项目,有 callback 的通常也会使用 util.promisify 进行转换,之前更多是知其然不知其所以然,本文会从基本使用和对源码的理解实现一个类似的函数功能。
1. Promisify 简单版本实现
在介绍 util.promisify 的基础使用之后,实现一个自定义的 util.promisify 函数的简单版本。
1.1 util promisify 基本使用
将 callback 转为 promise 对象,首先要确保这个 callback 为一个错误优先的回调函数,即 (err, value) => … err 指定一个错误参数,value 为返回值。
以下将以 fs.readFile 为例进行介绍。
创建一个 text.txt 文件
创建一个 text.txt 文件,写入一些自定义内容,下面的 Demo 中我们会使用 fs.readFile 来读取这个文件进行测试。
// text.txt
Nodejs Callback 转 Promise 对象测试
传统的 Callback 写法
const util = require(‘util’);
fs.readFile(‘text.txt’, ‘utf8’, function(err, result) {
  console.error(‘Error: ‘, err);
  console.log(‘Result: ‘, result); // Nodejs Callback 转 Promise 对象测试
});
Promise 写法
这里我们使用 util.promisify 将 fs.readFile 转为 Promise 对象,之后我们可以进行 .then、.catch 获取相应结果
const { promisify } = require(‘util’);
const readFilePromisify = util.promisify(fs.readFile); // 转化为 promise
readFilePromisify(‘text.txt’, ‘utf8’)
  .then(result => console.log(result)) // Nodejs Callback 转 Promise 对象测试
  .catch(err => console.log(err));
1.2 自定义 mayJunPromisify 函数实现
自定义 mayJunPromisify 函数实现 callback 转换为 promise,核心实现如下:
行 {1} 校验传入的参数 original 是否为 Function,不是则抛错
promisify(fs.readFile) 执行之后会返回一个函数 fn,行 {2} 定义待返回的 fn 函数,行 {3} 处返回
fn 返回的是一个 Promise 对象,在返回的 Promise 对象里执行 callback 函数
function mayJunPromisify(original) {
  if (typeof original !== ‘function’) { // {1} 校验
    throw new Error(‘The “original” argument must be of type Function. Received type undefined’)
  }
  function fn(…args) { // {2}
    return new Promise((resolve, reject) => {
      try {
        // original 例如,fs.readFile.call(this, ‘filename’, ‘utf8’, (err, result) => …)
        original.call(this, …args, (err, result) => {
          if (err) {
            reject(err);
          } else {
            resolve(result);
          }
        });
      } catch(err) {
        reject(err);
      }
    });
  }
  return fn; // {3}
}
现在使用我们自定义的 mayJunPromisify 函数做一个测试
const readFilePromisify = mayJunPromisify(fs.readFile);
readFilePromisify(‘text.txt’, ‘utf8’)
  .then(result => console.log(result)) // Nodejs Callback 转 Promise 对象测试
  .catch(err => console.log(err));
2. Promisify 自定义 Promise 函数版本实现
另一个功能是可以使用 util.promisify.custom 符号重写 util.promisify 返回值。
2.1 util.promisify.custom 基本使用
在 fs.readFile 上定义 util.promisify.custom 符号,其功能为禁止读取文件。
注意顺序要在 util.promisify 之前。
fs.readFile[util.promisify.custom] = () => {
  return Promise.reject(‘该文件暂时禁止读取’);
}
const readFilePromisify = util.promisify(fs.readFile);
readFilePromisify(‘text.txt’, ‘utf8’)
  .then(result => console.log(result))
  .catch(err => console.log(err)); // 该文件暂时禁止读取
2.2 自定义 mayJunPromisify.custom 实现
定义一个 Symbol 变量 kCustomPromisifiedSymbol 赋予 mayJunPromisify.custom
行 {1} 校验是否有自定义的 promise 化函数
行 {2} 自定义的 mayJunPromisify.custom 也要保证是一个函数,否则抛错
行 {3} 直接返回自定义的 mayJunPromisify.custom 函数,后续的 fn 函数就不会执行了,因此在这块也就重写了 util.promisify 返回值
// 所以说 util.promisify.custom 是一个符号
const kCustomPromisifiedSymbol = Symbol(‘util.promisify.custom’);
mayJunPromisify.custom = kCustomPromisifiedSymbol;
function mayJunPromisify(original) {
  if (typeof original !== ‘function’) {
    throw new Error(‘The “original” argument must be of type Function. Received type undefined’)
  }
  // 变动之处 -> start
  if (original[kCustomPromisifiedSymbol]) { // {1}
    const fn = original[kCustomPromisifiedSymbol];
    if (typeof fn !== ‘function’) { // {2}
      throw new Error(‘The “mayJunPromisify.custom” property must be of type Function. Received type number’);
    }
    // {3}
    return Object.defineProperty(fn, kCustomPromisifiedSymbol, {
      value: fn, enumerable: false, writable: false, configurable: true
    });
  }
  // end <- 变动之处
  function fn(…args) {
    …
  }
  return fn;
}
同样测试下我们自定义的 mayJunPromisify.custom 函数。
fs.readFile[mayJunPromisify.custom] = () => {
  return Promise.reject(‘该文件暂时禁止读取’);
}
const readFilePromisify = mayJunPromisify(fs.readFile);
readFilePromisify(‘text.txt’, ‘utf8’)
  .then(result => console.log(result))
  .catch(err => console.log(err)); // 该文件暂时禁止读取
3. Promisify 回调函数的多参转换
通常情况下我们是 (err, value) => … 这种方式实现的,结果只有 value 一个参数,但是呢有些例外情况,例如 dns.lookup 它的回调形式是 (err, address, family) => … 拥有三个参数,同样我们也要对这种情况做兼容。
3.1 util.promisify 中的基本使用
和上面区别的地方在于 .then 接收到的是一个对象 { address, family } 先明白它的基本使用,下面会展开具体是怎么实现的
const dns = require(‘dns’);
const lookupPromisify = util.promisify(dns.lookup);
lookupPromisify(‘nodejs.red’)
  .then(({ address, family }) => {
    console.log(‘地址: %j 地址族: IPv%s’, address, family);
  })
  .catch(err => console.log(err));
3.2 util.promisify 实现解析
类似 dns.lookup 这样的函数在回调(Callback)时提供了多个参数列表。
为了支持 util.promisify 也都会在函数上定义一个 customPromisifyArgs 参数,value 为回调时的多个参数名称,类型为数组,例如 dns.lookup 绑定的 customPromisifyArgs 的 value 则为 [‘address’, ‘family’],其主要目的也是为了适配 util.promisify。
dns.lookup 支持 util.promisify 核心实现
// https://github.com/nodejs/node/blob/v12.x/lib/dns.js#L33
const { customPromisifyArgs } = require(‘internal/util’);
// https://github.com/nodejs/node/blob/v12.x/lib/dns.js#L159
ObjectDefineProperty(lookup, customPromisifyArgs,
                     { value: [‘address’, ‘family’], enumerable: false });
customPromisifyArgs
customPromisifyArgs 这个参数是从 internal/util 模块导出的,仅内部调用,因此我们在外部 util.promisify 上是没有这个参数的。
也意味着只有 Nodejs 模块中例如 dns.klookup()、fs.read() 等方法在多参数的时候可以使用 util.promisify 转为 Promise,如果我们自定义的 callback 存在多参数的情况,使用 util.promisify 则不行,当然,如果你有需要也可以基于 util.promisify 自己封装一个。
// https://github.com/nodejs/node/blob/v12.x/lib/internal/util.js#L429
module.exports = {
  …
  // Symbol used to customize promisify conversion
  customPromisifyArgs: kCustomPromisifyArgsSymbol,
};
util.promisify 核心实现解析
参见源码 internal/util.js#L277
行 {1} 定义 Symbol 变量 kCustomPromisifyArgsSymbol
行 {2} 获取参数名称列表
行 {3} (err, result) 改为 (err, …values),原先 result 仅接收一个参数,改为 …values 接收多个参数
行 {4} argumentNames 存在且 values > 1,则回调会存在多个参数名称,进行遍历,返回一个 obj
行 {5} 否则 values *多仅有一个参数名称,即数组 values 有且仅有一个元素
// https://github.com/nodejs/node/blob/v12.x/lib/internal/util.js#L277
const kCustomPromisifyArgsSymbol = Symbol(‘customPromisifyArgs’); // {1}
function promisify(original) {
  …
  // 获取多个回调函数的参数名称列表
  const argumentNames = original[kCustomPromisifyArgsSymbol]; // {2}
  function fn(…args) {
    return new Promise((resolve, reject) => {
      try {
        // (err, result) 改为 (err, …values) {3}
        original.call(this, …args, (err, …values) => {
          if (err) {
            reject(err);
          } else {
            // 变动之处 -> start
            // argumentNames 存在且 values > 1,则回调会存在多个参数名称,进行遍历,返回一个 obj
            if (argumentNames !== undefined && values.length > 1) { // {4}
              const obj = {};
              for (let i = 0; i < argumentNames.length; i++)
                obj[argumentNames[i]] = values[i];
              resolve(obj);
            } else { // {5} 否则 values *多仅有一个参数名称,即数组 values 有且仅有一个元素
              resolve(values[0]);
            }
            // end <- 变动之处
          }
        });
      } catch(err) {
        reject(err);
      }
    });
  }
  return fn;
}
4. 实现一个完整的 promisify
上面*、第二节我们自定义实现的 mayJumPromisify 分别实现了含有 (err, result) => … 和自定义 Promise 函数功能。
第三节中介绍的回调函数多参数转换,由于 kCustomPromisifyArgsSymbol 使用 Symbol 声明(每次重新定义都会不一样),且没有对外提供,如果要实现第三个功能,需要我们每次在 callback 函数上重新定义 kCustomPromisifyArgsSymbol 属性。
例如,以下定义了一个 callback 函数用来获取用户信息,返回值是多个参数 name、age,通过定义 kCustomPromisifyArgsSymbol 属性,即可使用我们自己写的 mayJunPromisify 来转换为 Promise 形式。
function getUserById(id, cb) {
  const name = ‘张三’, age = 20;
  cb(null, name, age);
}
Object.defineProperty(getUserById, kCustomPromisifyArgsSymbol, {
  value: [‘name’, ‘age’], enumerable: false
})
const getUserByIdPromisify = mayJunPromisify(getUserById);
getUserByIdPromisify(1)
  .then(({ name, age }) => {
    console.log(name, age);
  })
  .catch(err => console.log(err));
自定义 mayJunPromisify 实现源码
https://github.com/Q-Angelo/project-training/tree/master/nodejs/module/promisify

JS的new关键字都干了什么?

JS的new关键字都干了什么?
new关键字在实例化获取对象时都做了什么?是一道经常出现在前端面试时的问题。如果只是简单的了解new关键字是实例化构造函数获取对象,是万万不能够的。更深入的层级发生了什么呢?同时面试官想从这道题里面考察什么呢?下面为各位小伙伴一一来解密。
一、new关键字
new关键字的作用:通过new关键字实例化构造函数,获取对象。说一千道一万,不如来段代码看一看
// 定义构造函数
function Person (name, age) {
this.name = name
this.age = age
this.say = function () {
console.log(`my name is ${this.name}, my age is ${this.age}`)
}
}
// 构造函数的原型属性和方法定义
Person.prototype.color = ‘yellow’
Person.prototype.sayBye = function () {
console.log(‘Bye!’)
}
// 实例化
let p = new Person(‘胡小帅’, 18)
console.log(p)
// 当前属性
console.log(p.name)
// 当前方法
p.say()
console.log(p.color)
// 原型方法
p.sayBye()
二、伪代码演示过程
通过new关键字实例化的对象p,具备了构造函数Person中this的属性:name、age,也具备了构造函数Person的原型prototype的属性color和方法sayBye。下面我们来通过伪代码来看看具体的实现过程。
初始化新对象var o = {}
原型的执行,确定对象o的原型链o.__proto__ = Person.prototype
绑定this对象为o,传入参数;执行Person构造函数,进行属性和方法的赋值操作Person.call(o, ‘胡小帅’, 18)
返回结果注意:在通过该种方式获取对象时,*终不一定返回的是对象o,要看构造函数的返回值是什么。如果函数返回的是基本类型值,实际会生成一个对象,返回o 如果是函数返回的是引用类型值,则实际返回的是该引用类型值

为什么要使用 Kubernetes 准入控制器

Kubernetes 控制平面由几个组件组成。其中一个组件是 kube-apiserver,简单的 API server。它公开了一个 REST 端点,用户、集群组件以及客户端应用程序可以通过该端点与集群进行通信。总的来说,它会进行以下操作:

  1. 从客户端应用程序(如 kubectl)接收标准 HTTP 请求。
  2. 验证传入请求并应用授权策略。
  3. 在成功的身份验证中,它能根据端点对象(Pod、Deployments、Namespace 等)和 http 动作(Create、Put、Get、Delete 等)执行操作。
  4. 对 etcd 数据存储进行更改以保存数据。
  5. 操作完成,它就向客户端发送响应。

%title插图%num

请求流程

现在让我们考虑这样一种情况:在请求经过身份验证后,但在对 etcd 数据存储进行任何更改之前,我们需要拦截该请求。例如:

  1. 拦截客户端发送的请求。
  2. 解析请求并执行操作。
  3. 根据请求的结果,决定对 etcd 进行更改还是拒*对 etcd 进行更改。

Kubernetes 准入控制器就是用于这种情况的插件。在代码层面,准入控制器逻辑与 API server 逻辑解耦,这样用户就可以开发自定义拦截器(custom interceptor),无论何时对象被创建、更新或从 etcd 中删除,都可以调用该拦截器。

有了准入控制器,从任意来源到 API server 的请求流将如下所示:

%title插图%num

准入控制器阶段(来自官方文档)

官方文档地址:https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/

根据准入控制器执行的操作类型,它可以分为 3 种类型:

  • Mutating(变更)
  • Validating(验证)
  • Both(两者都有)

Mutating:这种控制器可以解析请求,并在请求向下发送之前对请求进行更改(变更请求)。

示例:AlwaysPullImages

Validating:这种控制器可以解析请求并根据特定数据进行验证。

示例:NamespaceExists

Both:这种控制器可以执行变更和验证两种操作。

示例:CertificateSigning

有关这些控制器更多信息,查看官方文档:https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#what-does-each-admission-controller-do

准入控制器过程包括按顺序执行的2个阶段:

  1. Mutating(变更)阶段(先执行)
  2. Validation (验证)阶段(变更阶段后执行)

Kubernetes 集群已经在使用准入控制器来执行许多任务。

Kubernetes 附带的准入控制器列表:https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#what-does-each-admission-controller-do

通过该列表,我们可以发现大多数操作,如 AlwaysPullImages、DefaultStorageClass、PodSecurityPolicy 等,实际上都是由不同的准入控制器执行的。

%title插图%num

如何启用或禁用准入控制器?

要启用准入控制器,我们必须在启动 kube-apiserver 时,将以逗号分隔的准入控制器插件名称列表传递给 --enable-ading-plugins。对于默认插件,命令如下所示:

%title插图%num

要禁用准入控制器插件,可以将插件名称列表传递给 --disable-admission-plugins。它将覆盖默认启用的插件列表。

%title插图%num

%title插图%num

默认准入控制器

  • NamespaceLifecycle
  • LimitRanger
  • ServiceAccount
  • TaintNodesByCondition
  • Priority
  • DefaultTolerationSeconds
  • DefaultStorageClass
  • StorageObjectInUseProtection
  • PersistentVolumeClaimResize
  • RuntimeClass
  • CertificateApproval
  • CertificateSigning
  • CertificateSubjectRestriction
  • DefaultIngressClass
  • MutatingAdmissionWebhook
  • ValidatingAdmissionWebhook
  • ResourceQuota

%title插图%num

为什么要使用准入控制器?

准入控制器能提供额外的安全和治理层,以帮助 Kubernetes 集群的用户使用。

执行策略:通过使用自定义准入控制器,我们可以验证请求并检查它是否包含特定的所需信息。例如,我们可以检查 Pod 是否设置了正确的标签。如果没有,那可以一起拒*该请求。某些情况下,如果请求中缺少一些字段,我们也可以更改这些字段。例如,如果 Pod 没有设置资源限制,我们可以为 Pod 添加特定的资源限制。通过这样的方式,除非明确指定,集群中的所有 Pod 都将根据我们的要求设置资源限制。Limit Range 就是这种实现。

安全性:我们可以拒*不遵循特定规范的请求。例如,没有一个 Pod 请求可以将安全网关设置为以 root 用户身份运行。

统一工作负载:通过更改请求并为用户未设置的规范设置默认值,我们可以确保集群上运行的工作负载是统一的,并遵循集群管理员定义的特定标准。这些就是我们开始使用 Kubernetes 准入控制器需要知道的所有理论。

原文链接:https://medium.com/cloudlego/kubernetes-admission-controllers-request-interceptors-47a9b12c5303

一目了然的 Docker 环境配置指南

Docker是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的、可移植的、自给自足的容器。开发者在笔记本上编译测试通过的容器可以批量地在生产环境中部署,包括VMs(虚拟机)、 bare metal、OpenStack 集群和其他的基础应用平台。

Docker通常用于如下场景:

  • web应用的自动化打包和发布;
  • 自动化测试和持续集成、发布;
  • 在服务型环境中部署和调整数据库或其他的后台应用;
  • 从头编译或者扩展现有的OpenShift或Cloud Foundry平台来搭建自己的PaaS环境。

正因为Docker强大的功能,越来越多的场景下,需要我们使用Docker部署和发布我们的代码。今天就梳理下,如何入门Docker。

%title插图%num

%title插图%num

本地环境安装docker工具

  ubutun安装

这里以阿里云ECS(ubutun)下安装docker为例。命令行安装:

sudo apt install docker.io

验证:

docker info

  Mac安装,下载MAC版本的docker:

https://hub.docker.com/editions/community/docker-ce-desktop-mac/

  Windows安装,下载Windows版本的docker:

https://hub.docker.com/editions/community/docker-ce-desktop-windows/

注意:下载成功后,直接install就可以了,一路Next即可安装完成。申请自己的docker id,登陆;

%title插图%num

创建镜像仓库

这里以申请阿里云容器镜像服务(免费),并创建仓库为例,其他仓库如dockerhub、谷歌、亚马逊、腾讯等详见对应产品说明书。

阿里云容器服务地址为:https://cr.console.aliyun.com

注册开通后产品页面如下

%title插图%num

  1. 创建命名空间

*步切换标签页到命名空间,创建地址唯一的命名空间

%title插图%num

根据大赛要求选择对应的地域,其他的按照自己需求选择或填写

%title插图%num

2. 创建镜像仓库

下一步,选择本地仓库,不建议其他选项,完成创建

%title插图%num

点击管理,可查看详情。

%title插图%num%title插图%num

3.完成本地登录

按照页面的指令在本地完成登陆:

%title插图%num

  1. export DOCKER_REGISTRY= your_registry_url<docker registry url> (注意这里your_registry_url*后字段结尾,不能多不能少
  2. E.g registry.cn-shanghai.aliyuncs.com/xxxx/xxxx) docker login $DOCKER_REGISTRY \ –username your_username \ –password your_password

%title插图%num

构建镜像并推送

在安装好Docker环境的本机/服务器构建并推送容器镜像。过程中可能会使用docker命令,如拉取docker pull,推送docker push,构建docker build等等。

为简化构建镜像的难度,天池已准备了常用的Python基础镜像,可直接拉取使用,自行构建镜像请确保安装curl.更多基础镜像说明可参考:https://tianchi.aliyun.com/forum/postDetail?postId=67720。

docker pull registry.cn-shanghai.aliyuncs.com/tcc-public/python:3

  1. 准备所需文件

新建一个文件夹(例如tianchi_submit_demo)用于存放这次任务镜像所需的文件,文件夹中内容示例,其中hello_world.py中是各位自己的代码部分:

%title插图%num

Dockerfile配置文件参考,Dockerfile是固定名称,注意首字母大写。Dockerfile中命令皆大写:

  1. # Base Images
  2. ## 从天池基础镜像构建
  3. FROM registry.cn-shanghai.aliyuncs.com/tcc-public/python:3
  4. ## 把当前文件夹里的文件构建到镜像的根目录下
  5. ADD . /
  6. ## 指定默认工作目录为根目录(需要把run.sh和生成的结果文件都放在该文件夹下,提交后才能运行)
  7. WORKDIR /
  8. ## 镜像启动后统一执行 sh run.sh
  9. CMD [“sh”“run.sh”]

run.sh参考:

python hello_world.py

  2. 构建镜像并推送(2.1及2.2皆可)

2.1 IDE + Cloud Toolkit

推荐使用 Alibaba Cloud Toolkit:

https://cn.aliyun.com/product/cloudtoolkit 进行操作。

Cloud Toolkit 与主流 IDE 及阿里云容器镜像服务无缝集成,可以简化操作。这里以在 IntelliJ IDEA 中使用 Alibaba Cloud Toolkit 为例。只需配置一次,之后都可一键推送~

2.1.1. 安装及配置

在本地 IDE 中安装 Alibaba Cloud Toolkit 并进行阿里云账户配置。

参见:在 IntelliJ IDEA 中安装和配置 Cloud Toolkit:https://help.aliyun.com/document_detail/98762.html

2.1.2. 设置环境

设置用于打包本地镜像的 Docker 环境。

  1. 在 IntelliJ IDEA 工具栏单击 Tools > Alibaba Cloud > Preferences…
  2. 在 Settings 对话框的左侧导航栏中单击 Docker
  3. 在 Docker 界面中设置 Cloud Toolkit 需要连接的 Docker 环境。

注意:如果出现连接测试报错,可进入 Docker 的 Settings 界面,单击左侧导航栏中的 General,然后选择 Expose daemon on tcp://localhost:2375 without TLS。

%title插图%num

  • 本地为 Mac 或 Linux 操作系统,勾选 Unix Socket,然后单击 Browse,输入unix:///var/run/docker.sock
  • 本地为 Windows 操作系统,勾选 TCP Connection,然后在 URI 右侧文档框输入本地 Docker 的 URI,如 http://127.0.0.1:2375。
  • 远程 Docker 环境:勾选 Tcp Connection,在 URI 右侧的文本框里输入远端的 Docker 环境的 URI(包括 IP 地址和端口),如 http://x.x.x.x:2375,并确保远程主机的 HTTP 服务开启。
  • 单击 Test Connection 进行连接测试。

2.1.3. 构建并上传应用

  1. 在 IntelliJ IDEA 的菜单栏中选择 File > Open… ,选择参赛的工程文件。
  2. 在 IntelliJ IDEA 界面左侧的 Project 中右键单击您的 Docker 应用工程名,在弹出的下拉菜单中选择 Alibaba Cloud > Deploy to ACR/ACK > Deploy to ACR
  3. 在 Deploy to ACR 对话框中进行以下配置。
  • Context Directory:参赛的工程文件所在的目录,例如上文中的 tianchi_submit_demo 。
  • Dockerfile:选择上文中创建的 Dockerfile。
  • Version:对上传的工程文件做版本标记。例如 1.0
  1. 在 Image 页签中选择Context Directory和Dockerfile。
  2. 在 Image Repositories 区域选择上文中创建的容器镜像服务的地域、命名空间和镜像仓库。

2.1.4. 单击 RUN

%title插图%num

下次就可以一键完成了~

2.2 服务器上直接操作

执行

docker build -t registry.cn-shenzhen.aliyuncs.com/test_for_tianchi/test_for_tianchi_submit:1.0 .

注意:registry.~~~是上面创建仓库的公网地址,用自己仓库地址替换。地址后面的:1.0为自己指定的版本号,用于区分每次build的镜像。*后的.是构建镜像的路径,不可以省掉。

%title插图%num

构建完成后可先验证是否正常运行,正常运行后再进行推送。

CPU镜像:

docker run your_image sh run.sh

GPU镜像:

nvidia-docker run your_image sh run.sh

推送到镜像仓库

docker push registry.cn-shenzhen.aliyuncs.com/test_for_tianchi/test_for_tianchi_submit:1.0

如果这步出错,可能你没有登录,按照仓库里描述操作登录即可。

%title插图%num

*次推送会比较耗时,可以休息一会了~o( ̄▽ ̄)d

%title插图%num

提交验证运行结果

在左侧【提交结果】中填写推送的镜像路径、用户名和密码,即可提交。根据【我的成绩】中的分数和日志可以查看运行情况。

%title插图%num

%title插图%num

常见问题及解决方案

问题1. 如果你是在本机使用脚本build 镜像如docker build -t resgist… .可能会报错如下:

ERROR: Could not open requirements file: [Errno 2] No such file or directory'C:/Users/wyx/Desktop/tianchi_docker/requirements.txt'

解决方法:在Dockerfile文件的安装依赖包之前加一行COPY requirements.txt requirements.txt

问题2.  登陆镜像仓库失败,提示账号密码错误,请注意这里的账号密码非阿里云的账号密码而是你开通仓库服务时设置的账号密码,如果忘记密码,找回路径如下:

找回容器镜像登录密码

%title插图%num

问题3. push 完成后刷新仓库网页看不到镜像版本,担心上传失败

容器镜像网页存在一定的延迟,只要你本地push命令行没有出错就大胆去大赛提交即可,如果实在不放心你可以删除本地镜像然后pull一下验证。

深度思考 Spring Cloud + Alibaba Sentinel 源码原理

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

%title插图%num

作者 | 向寒 / 孙玄

来源 | 架构之美

头图 | 下载于视觉中国

%title插图%num

关于 Sentinel 

1、理论篇

以下是经过多年分布式经验总结的两个理论基础:

(1)微服务与治理的关系

%title插图%num

(2)爬坡理论

%title插图%num

我们今天的主题分为以下两个主要部分:

  • Sentinel设计原理
  • Sentinel运行流程源码剖析

%title插图%num

Sentinel 设计原理

1、特性

丰富的应用场景:阿里 10 年双十一积累场景,含秒杀、双十一零点持续洪峰、热点商品探测、预热、消息队列削峰填谷、集群流量控制、实时熔断下游不可用应用等多样化的场景。

广泛的开源生态:提供开箱即用的与其它开源框架/库的整合模块,如Dubbo、Spring Cloud、gRPC、Zuul、Reactor 等。

完善的 SPI 扩展点:提供简单易用、完善的 SPI 扩展接口;可通过实现扩展接口来快速地定制逻辑。

完备的实时监控:提供实时的监控功能,可看到接入应用的单台机器秒级数据,及500 台以下规模的集群汇总运行情况。

2、核心关键点

(1)资源:限流的对象

如下代码/user/select即为一个资源:

  1. 1@GetMapping(“/user/select”)
  2. 2
  3. 3@SentinelResource(value = “select”, blockHandler = “exceptionHandler”)
  4. 4
  5. 5public TUser select(@RequestParam Integer userId) {
  6. 6
  7. 7    log.info(“post /user/select userid=” + userId);
  8. 8
  9. 9    return userService.select(userId);
  10. 10
  11. 11}

即被SentinelResource注解修饰的API:

  1. 1@Target({ElementType.METHOD, ElementType.TYPE})
  2. 2
  3. 3@Retention(RetentionPolicy.RUNTIME)
  4. 4
  5. 5@Inherited
  6. 6
  7. 7public @interface SentinelResource {
  8. 8
  9. 9    String value() default “”;
  10. 10
  11. 11
  12. 12
  13. 13    EntryType entryType() default EntryType.OUT;
  14. 14
  15. 15
  16. 16
  17. 17    int resourceType() default 0;
  18. 18
  19. 19
  20. 20
  21. 21    String blockHandler() default “”;
  22. 22
  23. 23
  24. 24
  25. 25    Class<?>[] blockHandlerClass() default {};
  26. 26
  27. 27
  28. 28
  29. 29    String fallback() default “”;
  30. 30
  31. 31.…..
  32. 32
  33. 33}

(2)入口:sentinel为每个资源创建一个Entry。

(3)槽链:每个Entry都会有一条用于记录限流以及各种控制的信息Slot chain,以此来实现下图中绿色部分的功能。

%title插图%num

%title插图%num

Sentinel 运行流程源码剖析

此图为官网全局流程图,接下来我们通过源码,分解该过程:

%title插图%num

1、入口处

  1. 1SphU.entry(“methodA”, EntryType.IN);//入口
  2. 2
  3. 3}

核心代码

  1. 1SphU#lookProcessChain(ResourceWrapper resourceWrapper)

2、入口逻辑

  1. 1private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object… args)
  2. 2
  3. 3    throws BlockException {
  4. 4
  5. 5    // 从threadLocal中获取当前线程对应的context实例。
  6. 6
  7. 7    Context context = ContextUtil.getContext();
  8. 8
  9. 9    if (context instanceof NullContext) {
  10. 10
  11. 11        // The {@link NullContext} indicates that the amount of context has exceeded the threshold,
  12. 12
  13. 13        // so here init the entry only. No rule checking will be done.
  14. 14
  15. 15        // 如果context是nullContext的实例,表示当前context的总数已经达到阈值,所以这里直接创建entry实例,并返回,不进行规则的检查。
  16. 16
  17. 17        return new CtEntry(resourceWrapper, null, context);
  18. 18
  19. 19    }
  20. 20
  21. 21
  22. 22
  23. 23    if (context == null) {
  24. 24
  25. 25        // Using default context.
  26. 26
  27. 27        //如果context为空,则使用默认的名字创建一个,就是外部在调用SphU.entry(..)方法前如果没有调用ContextUtil.enter(..),则这里会调用该方法进行内部初始化context
  28. 28
  29. 29        context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
  30. 30
  31. 31    }
  32. 32
  33. 33
  34. 34
  35. 35    // Global switch is close, no rule checking will do.
  36. 36
  37. 37    // 总开关
  38. 38
  39. 39    if (!Constants.ON) {
  40. 40
  41. 41        return new CtEntry(resourceWrapper, null, context);
  42. 42
  43. 43    }
  44. 44
  45. 45
  46. 46
  47. 47    // 构造链路(核心实现) go in
  48. 48
  49. 49    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
  50. 50
  51. 51
  52. 52
  53. 53    /*
  54. 54
  55. 55     * Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
  56. 56
  57. 57     * so no rule checking will be done.
  58. 58
  59. 59     * 当链的大小达到阈值Constants.MAX_SLOT_CHAIN_SIZE时,不会校验任何规则,直接返回。
  60. 60
  61. 61     */
  62. 62
  63. 63    if (chain == null) {
  64. 64
  65. 65        return new CtEntry(resourceWrapper, null, context);
  66. 66
  67. 67    }
  68. 68
  69. 69
  70. 70
  71. 71    Entry e = new CtEntry(resourceWrapper, chain, context);
  72. 72
  73. 73    try {
  74. 74
  75. 75        // 开始进行链路调用。
  76. 76
  77. 77        chain.entry(context, resourceWrapper, null, count, prioritized, args);
  78. 78
  79. 79    } catch (BlockException e1) {
  80. 80
  81. 81        e.exit(count, args);
  82. 82
  83. 83        throw e1;
  84. 84
  85. 85    } catch (Throwable e1) {
  86. 86
  87. 87        // This should not happen, unless there are errors existing in Sentinel internal.
  88. 88
  89. 89        RecordLog.info(“Sentinel unexpected exception”, e1);
  90. 90
  91. 91    }
  92. 92
  93. 93    return e;
  94. 94
  95. 95}

3、上下文信息

Context

Context是当前线程所持有的Sentinel上下文。

进入Sentinel的逻辑时,会首先获取当前线程的Context,如果没有则新建。当任务执行完毕后,会清除当前线程的context。Context 代表调用链路上下文,贯穿一次调用链路中的所有 Entry。

Context 维持着入口节点(entranceNode)、本次调用链路的 当前节点(curNode)、调用来源(origin)等信息。Context 名称即为调用链路入口名称。

Node

Node是对一个@SentinelResource标记的资源的统计包装。

Context中记录本当前线程资源调用的入口节点。

我们可以通过入口节点的childList,可以追溯资源的调用情况。而每个节点都对应一个@SentinelResource标记的资源及其统计数据,例如:passQps,blockQps,rt等数据。

Entry

Entry是Sentinel中用来表示是否通过限流的一个凭证,如果能正常返回,则说明你可以访问被Sentinel保护的后方服务,否则Sentinel会抛出一个BlockException。

另外,它保存了本次执行entry()方法的一些基本信息,包括资源的Context、Node、对应的责任链等信息,后续完成资源调用后,还需要更具获得的这个Entry去执行一些善后操作,包括退出Entry对应的责任链,完成节点的一些统计信息更新,清除当前线程的Context信息等。

在构建Context时已经完成下图部分:

%title插图%num

4、核心流程

这里有两个需要注意的点:

  • ProcessorSlot chain = lookProcessChain(resourceWrapper); 构建链路。
  • chain.entry(context, resourceWrapper, null, count, prioritized, args); 进行链路调用首先来看链路是如何构建的。

5、获取槽链

  • 已有直接获取;
  • 没有去创建。
  1. 1       //在上下文中每一个资源都有各自的处理槽
  2. 2
  3. 3        ProcessorSlotChain chain = chainMap.get(resourceWrapper);
  4. 4
  5. 5        // 双重检查锁保证线程安全
  6. 6
  7. 7        if (chain == null) {
  8. 8
  9. 9            synchronized (LOCK) {
  10. 10
  11. 11                chain = ch ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {ainMap.get(resourceWrapper);
  12. 12
  13. 13                if (chain == null) {
  14. 14
  15. 15                    // Entry size limit.
  16. 16
  17. 17                    // 当链的长度达到阈值时,直接返回null,不进行规则的检查。
  18. 18
  19. 19                    if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
  20. 20
  21. 21                        return null;
  22. 22
  23. 23                    }
  24. 24
  25. 25                    // 构建链路 go in
  26. 26
  27. 27                    chain = SlotChainProvider.newSlotChain();
  28. 28
  29. 29                    Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
  30. 30
  31. 31                        chainMap.size() + 1);
  32. 32
  33. 33                    newMap.putAll(chainMap);
  34. 34
  35. 35                    newMap.put(resourceWrapper, chain);
  36. 36
  37. 37                    chainMap = newMap;
  38. 38
  39. 39                }
  40. 40
  41. 41            }
  42. 42
  43. 43        }
  44. 44
  45. 45        return chain;
  46. 46
  47. 47    }

6、创建槽链

SlotChainProvider.newSlotChain();

%title插图%num

  1. 1   // 基于spi扩展点机制来扩展,默认为DefaultSlotChainBuilder
  2. 2
  3. 3slotChainBuilder = SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class);

7、SPI加载ProcessorSlot

这里采用了spi的机制来扩展SlotChainBuilder,默认是采用DefaultSlotChainBuilder来实现的,可以看到sentinel源码的sentinel-core包下,META-INF/services/com.alibaba.csp.sentinel.slotchain.SlotChainBuilder文件下,默认属性是:

  1. 1  slotChainBuilder = SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class);

所以默认采用DefaultSlotChainBuilder来构建链路,因此找到DefaultSlotChainBuilder.build()方法。

8、DefaultSlotChainBuilder

  1. 1public ProcessorSlotChain build() {
  2. 2
  3. 3        // 定义链路起点
  4. 4
  5. 5        ProcessorSlotChain chain = new DefaultProcessorSlotChain();
  6. 6
  7. 7
  8. 8
  9. 9        // Note: the instances of ProcessorSlot should be different, since they are not stateless.
  10. 10
  11. 11        // 基于spi扩展机制,加载ProcessorSlot的实现类,从META-INF/services/com.alibaba.csp.sentinel.slotchain.ProcessorSlot文件下获取,并且按指定顺序排序
  12. 12
  13. 13        List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
  14. 14
  15. 15        // 遍历构建链路
  16. 16
  17. 17        for (ProcessorSlot slot : sortedSlotList) {
  18. 18
  19. 19            if (!(slot instanceof AbstractLinkedProcessorSlot)) {
  20. 20
  21. 21                RecordLog.warn(“The ProcessorSlot(“ + slot.getClass().getCanonicalName() + “) is not an instance of AbstractLinkedProcessorSlot, can’t be added into ProcessorSlotChain”);
  22. 22
  23. 23                continue;
  24. 24
  25. 25            }
  26. 26
  27. 27            // 将slot节点加入链,因为已经排好序了,只需要加到*后即可
  28. 28
  29. 29            chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
  30. 30
  31. 31        }
  32. 32
  33. 33
  34. 34
  35. 35        return chain;
  36. 36
  37. 37    }

9、遍历ProcessorSlots

这里也是通过spi的机制,读取文件META-INF/services/com.alibaba.csp.sentinel.slotchain.ProcessorSlot:

  1. 1# Sentinel default ProcessorSlots
  2. 2
  3. 3com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot
  4. 4
  5. 5com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot
  6. 6
  7. 7com.alibaba.csp.sentinel.slots.logger.LogSlot
  8. 8
  9. 9com.alibaba.csp.sentinel.slots.statistic.StatisticSlot
  10. 10
  11. 11com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot
  12. 12
  13. 13com.alibaba.csp.sentinel.slots.system.SystemSlot
  14. 14
  15. 15com.alibaba.csp.sentinel.slots.block.flow.FlowSlot
  16. 16
  17. 17com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot

从这里看出,链路由这些节点组成,而slot之间的顺序是根据每个slot节点的@SpiOrder注解的值来确定的。

NodeSelectorSlot -> ClusterBuilderSlot -> LogSlot -> StatisticSlot -> AuthoritySlot -> SystemSlot -> FlowSlot -> DegradeSlot

%title插图%num

链路调用 

chain.entry(…)

上面已经构建好了链路,下面就要开始进行链路的调用了。

回到CtSph#entryWithPriority

1、NodeSelectorSlot

NodeSelectorSlot(@SpiOrder(-10000))

直接进入NodeSelectorSlot类的entry方法。

根据官方文档,NodeSelectorSlot类的作用为:

负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级。

  1. 1@Override
  2. 2
  3. 3public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object… args)
  4. 4
  5. 5    throws Throwable {
  6. 6
  7. 7
  8. 8
  9. 9    // 双重检查锁+缓存 机制
  10. 10
  11. 11    DefaultNode node = map.get(context.getName());
  12. 12
  13. 13    if (node == null) {
  14. 14
  15. 15        synchronized (this) {
  16. 16
  17. 17            node = map.get(context.getName());
  18. 18
  19. 19            if (node == null) {
  20. 20
  21. 21                node = new DefaultNode(resourceWrapper, null);
  22. 22
  23. 23                HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
  24. 24
  25. 25                cacheMap.putAll(map);
  26. 26
  27. 27                cacheMap.put(context.getName(), node);
  28. 28
  29. 29                map = cacheMap;
  30. 30
  31. 31                // Build invocation tree
  32. 32
  33. 33                // 构建调用链的树形结构
  34. 34
  35. 35                ((DefaultNode) context.getLastNode()).addChild(node);
  36. 36
  37. 37            }
  38. 38
  39. 39
  40. 40
  41. 41        }
  42. 42
  43. 43    }
  44. 44
  45. 45
  46. 46
  47. 47    context.setCurNode(node);
  48. 48
  49. 49    // 进入下一个链
  50. 50
  51. 51    fireEntry(context, resourceWrapper, node, count, prioritized, args);
  52. 52
  53. 53}

2、ClusterBuilderSlot

ClusterBuilderSlot(@SpiOrder(-9000))

根据官方文档,ClusterBuilderSlot的作用为:

此插槽用于构建资源的 ClusterNode 以及调用来源节点。ClusterNode 保持某个资源运行统计信息(响应时间、QPS、block 数目、线程数、异常数等)以及调用来源统计信息列表。调用来源的名称由 ContextUtil.enter(contextName,origin) 中的 origin 标记。

3、LogSlot

LogSlot(@SpiOrder(-8000))

该类对链路的传递不做处理,只有在抛出BlockException的时候,向上层层传递的过程中,会通过该类来输入一些日志信息:

  1. 1@Override
  2. 2
  3. 3public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode obj, int count, boolean prioritized, Object… args)
  4. 4
  5. 5    throws Throwable {
  6. 6
  7. 7    try {
  8. 8
  9. 9        fireEntry(context, resourceWrapper, obj, count, prioritized, args);
  10. 10
  11. 11    } catch (BlockException e) {
  12. 12
  13. 13        // 当抛出BlockException异常时,这里会输入日志信息
  14. 14
  15. 15        EagleEyeLogUtil.log(resourceWrapper.getName(), e.getClass().getSimpleName(), e.getRuleLimitApp(),
  16. 16
  17. 17            context.getOrigin(), count);
  18. 18
  19. 19        throw e;
  20. 20
  21. 21    } catch (Throwable e) {
  22. 22
  23. 23        RecordLog.warn(“Unexpected entry exception”, e);
  24. 24
  25. 25    }
  26. 26
  27. 27}

4、StatisticSlot

StatisticSlot(@SpiOrder(-7000))

官方文档:

StatisticSlot用于记录、统计不同纬度的 runtime 指标监控信息。

  1. 1@Override
  2. 2
  3. 3public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
  4. 4
  5. 5                  boolean prioritized, Object… args) throws Throwable {
  6. 6
  7. 7    try {
  8. 8
  9. 9        // Do some checking.
  10. 10
  11. 11        // 先将调用链继续下去,等到后续链调用结束了,再执行下面的步骤
  12. 12
  13. 13        fireEntry(context, resourceWrapper, node, count, prioritized, args);
  14. 14
  15. 15
  16. 16
  17. 17        // Request passed, add thread count and pass count.
  18. 18
  19. 19        node.increaseThreadNum();
  20. 20
  21. 21        node.addPassRequest(count);
  22. 22
  23. 23
  24. 24
  25. 25        if (context.getCurEntry().getOriginNode() != null) {
  26. 26
  27. 27            // Add count for origin node.
  28. 28
  29. 29            context.getCurEntry().getOriginNode().increaseThreadNum();
  30. 30
  31. 31            context.getCurEntry().getOriginNode().addPassRequest(count);
  32. 32
  33. 33        }
  34. 34
  35. 35
  36. 36
  37. 37        if (resourceWrapper.getEntryType() == EntryType.IN) {
  38. 38
  39. 39            // Add count for global inbound entry node for global statistics.
  40. 40
  41. 41            Constants.ENTRY_NODE.increaseThreadNum();
  42. 42
  43. 43            Constants.ENTRY_NODE.addPassRequest(count);
  44. 44
  45. 45        }
  46. 46
  47. 47
  48. 48
  49. 49        // Handle pass event with registered entry callback handlers.
  50. 50
  51. 51        for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
  52. 52
  53. 53            handler.onPass(context, resourceWrapper, node, count, args);
  54. 54
  55. 55        }
  56. 56
  57. 57    } catch (PriorityWaitException ex) {
  58. 58
  59. 59        node.increaseThreadNum();
  60. 60
  61. 61        if (context.getCurEntry().getOriginNode() != null) {
  62. 62
  63. 63            // Add count for origin node.
  64. 64
  65. 65            context.getCurEntry().getOriginNode().increaseThreadNum();
  66. 66
  67. 67        }
  68. 68
  69. 69
  70. 70
  71. 71        if (resourceWrapper.getEntryType() == EntryType.IN) {
  72. 72
  73. 73            // Add count for global inbound entry node for global statistics.
  74. 74
  75. 75            Constants.ENTRY_NODE.increaseThreadNum();
  76. 76
  77. 77        }
  78. 78
  79. 79        // Handle pass event with registered entry callback handlers.
  80. 80
  81. 81        for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
  82. 82
  83. 83            handler.onPass(context, resourceWrapper, node, count, args);
  84. 84
  85. 85        }
  86. 86
  87. 87    } catch (BlockException e) {
  88. 88
  89. 89        // Blocked, set block exception to current entry.
  90. 90
  91. 91        context.getCurEntry().setBlockError(e);
  92. 92
  93. 93
  94. 94
  95. 95        // Add block count.
  96. 96
  97. 97        node.increaseBlockQps(count);
  98. 98
  99. 99        if (context.getCurEntry().getOriginNode() != null) {
  100. 100
  101. 101            context.getCurEntry().getOriginNode().increaseBlockQps(count);
  102. 102
  103. 103        }
  104. 104
  105. 105
  106. 106
  107. 107        if (resourceWrapper.getEntryType() == EntryType.IN) {
  108. 108
  109. 109            // Add count for global inbound entry node for global statistics.
  110. 110
  111. 111            Constants.ENTRY_NODE.increaseBlockQps(count);
  112. 112
  113. 113        }
  114. 114
  115. 115
  116. 116
  117. 117        // Handle block event with registered entry callback handlers.
  118. 118
  119. 119        for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
  120. 120
  121. 121            handler.onBlocked(e, context, resourceWrapper, node, count, args);
  122. 122
  123. 123        }
  124. 124
  125. 125
  126. 126
  127. 127        throw e;
  128. 128
  129. 129    } catch (Throwable e) {
  130. 130
  131. 131        // Unexpected internal error, set error to current entry.
  132. 132
  133. 133        context.getCurEntry().setError(e);
  134. 134
  135. 135
  136. 136
  137. 137        throw e;
  138. 138
  139. 139    }
  140. 140
  141. 141}

StatisticSlot 会先将链往下执行,等到后面的节点全部执行完毕,再进行数据统计。

5、AuthoritySlot

@SpiOrder(-6000)

AuthoritySlot

官方文档:

AuthoritySlot:根据配置的黑白名单和调用来源信息,来做黑白名单控制

  1. 1@Override
  2. 2
  3. 3public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object… args)
  4. 4
  5. 5    throws Throwable {
  6. 6
  7. 7    // 黑白名单权限控制
  8. 8
  9. 9    checkBlackWhiteAuthority(resourceWrapper, context);
  10. 10
  11. 11    fireEntry(context, resourceWrapper, node, count, prioritized, args);
  12. 12
  13. 13}
  14. 14
  15. 15
  16. 16
  17. 17void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
  18. 18
  19. 19    Map<String, Set<AuthorityRule>> authorityRules = AuthorityRuleManager.getAuthorityRules();
  20. 20
  21. 21
  22. 22
  23. 23    if (authorityRules == null) {
  24. 24
  25. 25        return;
  26. 26
  27. 27    }
  28. 28
  29. 29
  30. 30
  31. 31    Set<AuthorityRule> rules = authorityRules.get(resource.getName());
  32. 32
  33. 33    if (rules == null) {
  34. 34
  35. 35        return;
  36. 36
  37. 37    }
  38. 38
  39. 39
  40. 40
  41. 41    for (AuthorityRule rule : rules) {
  42. 42
  43. 43        if (!AuthorityRuleChecker.passCheck(rule, context)) {
  44. 44
  45. 45            throw new AuthorityException(context.getOrigin(), rule);
  46. 46
  47. 47        }
  48. 48
  49. 49    }
  50. 50
  51. 51}

6、SystemSlot

@SpiOrder(-5000)

SystemSlot

官方文档:

SystemSlot:这个 slot 会根据对于当前系统的整体情况,对入口资源的调用进行动态调配。其原理是让入口的流量和当前系统的预计容量达到一个动态平衡。

  1. 1@Override
  2. 2public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
  3. 3                 boolean prioritized, Object… args) throws Throwable {
  4. 4   // 系统规则校验
  5. 5   SystemRuleManager.checkSystem(resourceWrapper);
  6. 6   fireEntry(context, resourceWrapper, node, count, prioritized, args);
  7. 7}

7、FlowSlot 限流规则引擎

@SpiOrder(-2000)

FlowSlot

官方文档:

这个 slot 主要根据预设的资源的统计信息,按照固定的次序,依次生效。如果一个资源对应两条或者多条流控规则,则会根据如下次序依次检验,直到全部通过或者有一个规则生效为止:

  • 指定应用生效的规则,即针对调用方限流的;
  • 调用方为 other 的规则;
  • 调用方为 default 的规则。

%title插图%num

入口

  1. 1@Override
  2. 2public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
  3. 3                 boolean prioritized, Object… args) throws Throwable {
  4. 4   // 检查限流规则
  5. 5   checkFlow(resourceWrapper, context, node, count, prioritized);
  6. 6
  7. 7   fireEntry(context, resourceWrapper, node, count, prioritized, args);
  8. 8}
  9. 9
  10. 10void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
  11. 11   throws BlockException {
  12. 12   checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
  13. 13}

1、所有规则检查

调用了FlowRuleChecker.checkFlow(…)方法。

  1. 1public void checkFlow(Function<StringCollection<FlowRule>> ruleProviderResourceWrapper resource,
  2. 2                     Context contextDefaultNode nodeint countboolean prioritizedthrows BlockException {
  3. 3   if (ruleProvider == null || resource == null) {
  4. 4       return;
  5. 5  }
  6. 6   // 根据资源名称找到对应的
  7. 7   Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
  8. 8   if (rules != null) {
  9. 9       // 遍历规则,依次判断是否通过
  10. 10       for (FlowRule rule : rules) {
  11. 11           if (!canPassCheck(rule, context, node, count, prioritized)) {
  12. 12               throw new FlowException(rule.getLimitApp(), rule);
  13. 13          }
  14. 14      }
  15. 15  }
  16. 16}

2、单个规则检查

  1. 1public boolean canPassCheck(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node, int acquireCount,
  2. 2                                               boolean prioritized) {
  3. 3   String limitApp = rule.getLimitApp();
  4. 4   if (limitApp == null) {
  5. 5       return true;
  6. 6  }
  7. 7   // 集群限流的判断
  8. 8   if (rule.isClusterMode()) {
  9. 9       return passClusterCheck(rule, context, node, acquireCount, prioritized);
  10. 10  }
  11. 11   // 本地节点的判断
  12. 12   return passLocalCheck(rule, context, node, acquireCount, prioritized);
  13. 13}

3、非集群模式的限流判断

  1. 1private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
  2. 2                                     boolean prioritized) {
  3. 3   // 根据请求的信息及策略,选择不同的node节点
  4. 4   Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
  5. 5   if (selectedNode == null) {
  6. 6       return true;
  7. 7  }
  8. 8   // 根据当前规则,获取规则控制器,调用canPass方法进行判断
  9. 9//       rule.getRater()放回的是TrafficShapingController接口的实现类,使用了策略模式,根据使用的控制措施来选择使用哪种实现。
  10. 10   return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
  11. 11}

这里是先根据请求和当前规则的策略,找到该规则下存储统计信息的节点,然后根据当前规则获取相应控制器,通过控制器的canPass(…)方法进行判断。

4、获取节点
  1. 1static Node selectNodeByRequesterAndStrategy(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node) {
  2. 2   // The limit app should not be empty.
  3. 3   String limitApp = rule.getLimitApp();
  4. 4   int strategy = rule.getStrategy();
  5. 5   String origin = context.getOrigin();
  6. 6
  7. 7   // 判断调用来源,这种情况下origin不能为default或other
  8. 8   if (limitApp.equals(origin) && filterOrigin(origin)) {
  9. 9       // 如果调用关系策略为STRATEGY_DIRECT,表示仅判断自己,则返回origin statistic node.
  10. 10       if (strategy == RuleConstant.STRATEGY_DIRECT) {
  11. 11           // Matches limit origin, return origin statistic node.
  12. 12           return context.getOriginNode();
  13. 13      }
  14. 14
  15. 15       // 采用调用来源进行判断的策略
  16. 16       return selectReferenceNode(rule, context, node);
  17. 17  } else if (RuleConstant.LIMIT_APP_DEFAULT.equals(limitApp)) { // 如果调用来源为default默认的
  18. 18       if (strategy == RuleConstant.STRATEGY_DIRECT) { // 如果调用关系策略为STRATEGY_DIRECT,则返回clusterNode
  19. 19           // Return the cluster node.
  20. 20           return node.getClusterNode();
  21. 21      }
  22. 22
  23. 23       return selectReferenceNode(rule, context, node);
  24. 24  } else if (RuleConstant.LIMIT_APP_OTHER.equals(limitApp)
  25. 25       && FlowRuleManager.isOtherOrigin(origin, rule.getResource())) { // 如果调用来源为other,且调用来源不在限制规则内,为其他来源
  26. 26       if (strategy == RuleConstant.STRATEGY_DIRECT) {
  27. 27           return context.getOriginNode();
  28. 28      }
  29. 29       return selectReferenceNode(rule, context, node);
  30. 30  }
  31. 31   return null;
  32. 32}
5、流量整形控制器

rule.getRater()方法会返回一个控制器,接口为TrafficShapingController,该接口的实现类图如下:

%title插图%num

从类图可以看出,是很明显的策略模式,分别针对不同的限流控制策略。

1、默认策略

DefaultController该策略是sentinel的默认策略,如果请求超出阈值,则直接拒*请求。

  1. 1@Override
  2. 2public boolean canPass(Node node, int acquireCount, boolean prioritized) {
  3. 3   // 当前已经统计的数
  4. 4   int curCount = avgUsedTokens(node);
  5. 5   if (curCount + acquireCount > count) {
  6. 6       // 如果是高优先级的,且是基于qps的限流方式,则可以尝试从下个未来的滑动窗口中预支
  7. 7       if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
  8. 8           long currentTime;
  9. 9           long waitInMs;
  10. 10           currentTime = TimeUtil.currentTimeMillis();
  11. 11           // 从下个滑动窗口中提前透支
  12. 12           waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
  13. 13           if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
  14. 14               node.addWaitingRequest(currentTime + waitInMs, acquireCount);
  15. 15               node.addOccupiedPass(acquireCount);
  16. 16               sleep(waitInMs);
  17. 17
  18. 18               // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
  19. 19               throw new PriorityWaitException(waitInMs);
  20. 20          }
  21. 21      }
  22. 22       return false;
  23. 23  }
  24. 24   return true;
  25. 25}
  26. 26
  27. 27private int avgUsedTokens(Node node) {
  28. 28   if (node == null) {
  29. 29       return DEFAULT_AVG_USED_TOKENS;
  30. 30  }
  31. 31   // 如果当前是线程数限流,则返回node.curThreadNum()当前线程数
  32. 32   // 如果是QPS限流,则返回node.passQps()当前已经通过的qps数据
  33. 33   return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
  34. 34}
  35. 35
  36. 36private void sleep(long timeMillis) {
  37. 37   try {
  38. 38       Thread.sleep(timeMillis);
  39. 39  } catch (InterruptedException e) {
  40. 40       // Ignore.
  41. 41  }
  42. 42}
2、匀速排队策略

RateLimiterController

  1. 1@Override
  2. 2public boolean canPass(Node node, int acquireCount, boolean prioritized) {
  3. 3   // Pass when acquire count is less or equal than 0.
  4. 4   if (acquireCount <= 0) {
  5. 5       return true;
  6. 6  }
  7. 7   // Reject when count is less or equal than 0.
  8. 8   // Otherwise,the costTime will be max of long and waitTime will overflow in some cases.
  9. 9   if (count <= 0) {
  10. 10       return false;
  11. 11  }
  12. 12
  13. 13   long currentTime = TimeUtil.currentTimeMillis();
  14. 14   // Calculate the interval between every two requests.
  15. 15   // 计算两个请求之间的时间间隔
  16. 16   long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
  17. 17
  18. 18   // Expected pass time of this request. 该请求的预计通过时间 = 上一次通过的时间 + 时间间隔
  19. 19   long expectedTime = costTime + latestPassedTime.get();
  20. 20
  21. 21   // 如果预计时间比当前时间小,表示可以请求完全可以通过
  22. 22   if (expectedTime <= currentTime) {
  23. 23       // Contention may exist here, but it’s okay.
  24. 24       // 这里可能存在竞争,但是不影响。
  25. 25       latestPassedTime.set(currentTime);
  26. 26       return true;
  27. 27  } else {
  28. 28       // Calculate the time to wait.
  29. 29       // 计算等待时间
  30. 30       long waitTime = costTime + latestPassedTime.get() – TimeUtil.currentTimeMillis();
  31. 31       // 如果等待时间超出了等待队列的*大时间,则无法放入等待队列,直接拒*
  32. 32       if (waitTime > maxQueueingTimeMs) {
  33. 33           return false;
  34. 34      } else {
  35. 35           long oldTime = latestPassedTime.addAndGet(costTime);
  36. 36           try {
  37. 37               // 重新计算等待时间
  38. 38               waitTime = oldTime – TimeUtil.currentTimeMillis();
  39. 39               // 判断等待时间是否超过等待队列的*大时间,如果超过了,拒*,并且将latestPassedTime*后一次请求时间重新设置为原值
  40. 40               if (waitTime > maxQueueingTimeMs) {
  41. 41                   latestPassedTime.addAndGet(-costTime);
  42. 42                   return false;
  43. 43              }
  44. 44               // in race condition waitTime may <= 0
  45. 45               // 线程等待
  46. 46               if (waitTime > 0) {
  47. 47                   Thread.sleep(waitTime);
  48. 48              }
  49. 49               return true;
  50. 50          } catch (InterruptedException e) {
  51. 51          }
  52. 52      }
  53. 53  }
  54. 54   return false;
  55. 55}

从代码可以看出,匀速排队策略是使用了虚拟队列的方法,通过控制阈值来计算出请求的时间间隔,然后将上一次请求的时间加上时间间隔,表示下一次请求的时间,如果当前时间比这个值大,说明已经超出时间间隔了,当然可以请求,反之,表示需要等待,那么等待的时长就应该是要等到当前时间达到预期时间才能请求,这里就有个虚拟的等待队列,而等待其实是通过线程的等待来实现的。而这里所说的虚拟队列实际上是由一系列的处于sleep状态的线程组成的,但是实际的数据结构上并没有构成队列。

3、预热/冷启动策略

WarmUpController

首先看WarmUpController的属性和构造方法:

  1. 1// 阈值
  2. 2protected double count;
  3. 3/**
  4. 4* 冷启动的因子 ,默认为3 {@link SentinelConfig#coldFactor()}
  5. 5*/
  6. 6private int coldFactor;
  7. 7// 转折点的令牌数
  8. 8protected int warningToken = 0;
  9. 9// *大令牌数
  10. 10private int maxToken;
  11. 11// 折线初始斜率,标志流量的变化程度
  12. 12protected double slope;
  13. 13
  14. 14// 累积的令牌数 ,累积的令牌数越多,说明系统利用率越低,说明当前流量低,是冷状态
  15. 15protected AtomicLong storedTokens = new AtomicLong(0);
  16. 16// *后更新令牌的时间
  17. 17protected AtomicLong lastFilledTime = new AtomicLong(0);
  18. 18
  19. 19public WarmUpController(double count, int warmUpPeriodInSec, int coldFactor) {
  20. 20   construct(count, warmUpPeriodInSec, coldFactor);
  21. 21}
  22. 22
  23. 23public WarmUpController(double count, int warmUpPeriodInSec) {
  24. 24   construct(count, warmUpPeriodInSec, 3);
  25. 25}
  26. 26
  27. 27/**
  28. 28* @param count             用户设定的阈值(这里假设设定为100)
  29. 29* @param warmUpPeriodInSec 默认为10
  30. 30* @param coldFactor       默认为3
  31. 31*/
  32. 32private void construct(double count, int warmUpPeriodInSec, int coldFactor) {
  33. 33
  34. 34   if (coldFactor <= 1) {
  35. 35       throw new IllegalArgumentException(“Cold factor should be larger than 1”);
  36. 36  }
  37. 37
  38. 38   this.count = count;
  39. 39
  40. 40   this.coldFactor = coldFactor;
  41. 41
  42. 42   // thresholdPermits = 0.5 * warmupPeriod / stableInterval.
  43. 43   // warningToken = 100;
  44. 44
  45. 45   // 按默认的warmUpPeriodInSec = 10,表示1秒钟10个请求,则每个请求为间隔stableInterval = 100ms,那么coldInterval=stableInterval * coldFactor = 100 * 3 = 300ms
  46. 46   // warningToken = 10 * 100 / (3 – 1) = 500
  47. 47   // thresholdPermits = warningToken = 0.5 * warmupPeriod / stableInterval = 0.5 * warmupPeriod / 100ms = 500 ==>> warmupPeriod = 100000ms
  48. 48   warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor – 1);
  49. 49
  50. 50   // / maxPermits = thresholdPermits + 2 * warmupPeriod /
  51. 51   // (stableInterval + coldInterval)
  52. 52   // maxToken = 200
  53. 53
  54. 54   // maxPermits = 500 + 2 * 100000ms / (100ms + 300ms) = 1000
  55. 55   // maxToken = 500 + (2 * 10 * 100 / (1.0 + 3)) = 1000
  56. 56   // maxPermits = maxToken
  57. 57   maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
  58. 58
  59. 59   // slope
  60. 60   // slope = (coldIntervalMicros – stableIntervalMicros) / (maxPermits
  61. 61   // – thresholdPermits);
  62. 62
  63. 63   // slope = (3 – 1.0) / 100 / (600 – 500) = 0.0002
  64. 64   slope = (coldFactor – 1.0) / count / (maxToken – warningToken);
  65. 65
  66. 66}

属性说明:

  • count: 用户设定的qps阈值。
  • coldFactor: 冷启动的因子,初始默认为3,通过SentinelConfig类的coldFactor()方法获取,这里会有个判断,如果启动因子小于等于1,则会设置为默认值3,因为如果是小于等于1,是没有意义的,就不是预热启动了。
  • warningToken:转折点的令牌数,当令牌数开始小于该值得时候,就要开启预热了。
  • maxToken:*大令牌数。
  • slope:折线的斜率。
  • storedTokens:当前存储的令牌数。
  • lastFilledTime:上一次更新令牌的时间。

总体思路:当系统存储的令牌为*大值时,说明系统访问流量较低,处于冷状态,这时候当有正常请求过来时,会让请求通过,并且会补充消耗的令牌数。当瞬时流量来临时,一旦剩余的令牌数小于警戒令牌数(restToken <= warningToken),则表示有大流量过来,需要开启预热过程,开始逐渐增大允许的qps。当qps达到用户设定的阈值后,系统已经预热完毕,这时候就进入了正常的请求阶段。

源码分析如下:

  1. 1@Override
  2. 2public boolean canPass(Node node, int acquireCount, boolean prioritized) {
  3. 3   // 当前已经通过的qps
  4. 4   long passQps = (long) node.passQps();
  5. 5
  6. 6   // 上一个滑动窗口的qps
  7. 7   long previousQps = (long) node.previousPassQps();
  8. 8   // 同步令牌,如果是出于冷启动或预热完毕状态,则考虑要添加令牌
  9. 9   syncToken(previousQps);
  10. 10
  11. 11   // 开始计算它的斜率
  12. 12   // 如果进入了警戒线,开始调整他的qps
  13. 13   long restToken = storedTokens.get();
  14. 14   if (restToken >= warningToken) { // 说明一瞬间有大流量过来,消耗了大量的存储令牌,造成剩余令牌数*警戒值,则要开启预热默认,逐渐增加qps
  15. 15       // 计算当前离警戒线的距离
  16. 16       long aboveToken = restToken – warningToken;
  17. 17       // 消耗的速度要比warning快,但是要比慢
  18. 18       // current interval = restToken*slope+1/count
  19. 19       // restToken越小,interval就越小,表示系统越热
  20. 20       // 随着aboveToken的减小,warningQps会逐渐增大
  21. 21       double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
  22. 22       if (passQps + acquireCount <= warningQps) { // 随着warningQps的增大,acquireCount = 1,那么passQps允许的范围就变大,相应的流量就越大,系统越热
  23. 23           return true;
  24. 24      }
  25. 25  } else {
  26. 26       if (passQps + acquireCount <= count) {
  27. 27           return true;
  28. 28      }
  29. 29  }
  30. 30
  31. 31   return false;
  32. 32}
  33. 33
  34. 34/**
  35. 35* 同步令牌
  36. 36* @param passQps
  37. 37*/
  38. 38protected void syncToken(long passQps) {
  39. 39   long currentTime = TimeUtil.currentTimeMillis();
  40. 40   // 把当前时间的后三位置为0 e.g. 1601456312835 = 1601456312835 – 1601456312835 % 1000 = 1601456312000
  41. 41   currentTime = currentTime – currentTime % 1000;
  42. 42   // 获取上一次更新令牌的时间
  43. 43   long oldLastFillTime = lastFilledTime.get();
  44. 44   if (currentTime <= oldLastFillTime) {
  45. 45       return;
  46. 46  }
  47. 47
  48. 48   // 获得目前的令牌数
  49. 49   long oldValue = storedTokens.get();
  50. 50   // 获取新的令牌数
  51. 51   long newValue = coolDownTokens(currentTime, passQps);
  52. 52
  53. 53   // 更新累积令牌数
  54. 54   if (storedTokens.compareAndSet(oldValue, newValue)) {
  55. 55       // 去除上一次的qps,设置剩下的令牌数
  56. 56       long currentValue = storedTokens.addAndGet(0 – passQps);
  57. 57       if (currentValue < 0) {
  58. 58           // 如果剩下的令牌数小于0,则置为0。
  59. 59           storedTokens.set(0L);
  60. 60      }
  61. 61       // 设置令牌更新时间
  62. 62       lastFilledTime.set(currentTime);
  63. 63  }
  64. 64}
  65. 65
  66. 66private long coolDownTokens(long currentTime, long passQps) {
  67. 67   // 当前拥有的令牌数
  68. 68   long oldValue = storedTokens.get();
  69. 69   long newValue = oldValue;
  70. 70
  71. 71   // 添加令牌的判断前提条件:
  72. 72   // 当令牌的消耗程度远远低于警戒线的时候
  73. 73   if (oldValue < warningToken) { // 这种情况表示已经预热结束,可以开始生成令牌了
  74. 74       // 这里按照count = 100来计算的话,表示旧值oldValue + 距离上次更新的秒数时间差 * count ,表示每秒增加count个令牌
  75. 75       // 这里的currentTime 和 lastFilledTime.get() 都是已经去掉毫秒数的
  76. 76       newValue = (long)(oldValue + (currentTime – lastFilledTime.get()) * count / 1000);
  77. 77  } else if (oldValue > warningToken) { // 进入这里表示当前是冷状态或正处于预热状态
  78. 78       if (passQps < (int)count / coldFactor) { // 如果是冷状态,则补充令牌数,避免令牌数为0
  79. 79           newValue = (long)(oldValue + (currentTime – lastFilledTime.get()) * count / 1000);
  80. 80      }
  81. 81       // 预热阶段则不添加令牌数,从而限制流量的急剧攀升
  82. 82  }
  83. 83   // 限制令牌数不能超过*大令牌数maxToken
  84. 84   return Math.min(newValue, maxToken);
  85. 85}
4、预热的匀速排队策略

WarmUpRateLimiterController

这种是匀速排队模式和预热模式的结合,这里不深入了。搞懂了上面两种,再看这种也比较清晰了。

5、DegradeSlot

官方文档说明:

这个 slot 主要针对资源的平均响应时间(RT)以及异常比率,来决定资源是否在接下来的时间被自动熔断掉。

源码解析:

  1. 1@Override
  2. 2public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
  3. 3                 boolean prioritized, Object… args) throws Throwable {
  4. 4   //降级判断
  5. 5   performChecking(context, resourceWrapper);
  6. 6
  7. 7   // 如果有自定义的slot,还会继续进行
  8. 8   fireEntry(context, resourceWrapper, node, count, prioritized, args);
  9. 9}
  10. 10
  11. 11void performChecking(Context context, ResourceWrapper r) throws BlockException {
  12. 12   // 使用DegradeRuleManager获得当前资源的熔断器
  13. 13   List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
  14. 14   if (circuitBreakers == null || circuitBreakers.isEmpty()) {
  15. 15       return;
  16. 16  }
  17. 17   // 遍历熔断器,只要有任何一个满足熔断条件,就抛出DegradeException异常。
  18. 18   for (CircuitBreaker cb : circuitBreakers) {
  19. 19       if (!cb.tryPass(context)) {
  20. 20           throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
  21. 21      }
  22. 22  }
  23. 23}

这里有个关键类,DegradeRuleManager,该类中会保存所有的熔断规则,使用Map<String, List>的格式进行保存。当需要使用的时候,就直接根据资源名称,从该map中获取对应的熔断器列表。

那么规则是如何加载的呢?我们看到DegradeRuleManager这个类,在加载时候,有个静态代码块:

  1. 1private static final RulePropertyListener LISTENER = new RulePropertyListener();
  2. 2private static SentinelProperty<List<DegradeRule>> currentProperty
  3. 3   = new DynamicSentinelProperty<>();
  4. 4
  5. 5static {
  6. 6   currentProperty.addListener(LISTENER);
  7. 7}

currentProperty.addListener(LISTENER);继续分析该段代码,找到DynamicSentinelProperty的addListener(…)方法:

  1. 1@Override
  2. 2public void addListener(PropertyListener<T> listener) {
  3. 3   listeners.add(listener);
  4. 4   listener.configLoad(value);
  5. 5}
  6. 612345

发现会调用监听器的configLoad(…)方法,*终会调用RulePropertyListener这个类的reloadFrom(…)方法。具体怎么解析的其实就是将规则根据资源名称进行归类,并保存为map格式。

%title插图%num

FlowSlot 限流规则引擎之限流算法原理

1、滑动窗口实现原理

%title插图%num

  • 每个时间窗口*大流量为100QPS;
  • 20和80表示当时的真实QPS数量;
  • 一个时间窗口分为两个半限,上半限和下半限;
  • 如果时间窗口1的下半限和时间窗口2的上半限的峰值超过100QPS,那么就丢失一部分流量。

但是这样并不是我们想要的,那么我们来看看计数器滑动窗口。

2、计数器滑动窗口原理

%title插图%num

  • 在滑动窗口算法上优化;
  • 相邻的两个半限总和>总阈值,才丢弃流量。

3、令牌桶算法

%title插图%num

  • 令牌漏斗桶存着所有的Token;
  • 按期发放Token;
  • 如果桶满了,就会熔断;
  • 达到Token的Request可以获取资源;
  • 得不到的就抛弃。

俯瞰云原生,这便是供应层

都在说云原生,它的技术图谱你真的了解吗?中,我们对 CNCF 的云原生技术生态做了整体的介绍。从本篇开始,将详细介绍云原生全景图的每一层。

云原生全景图的*底层是供应层(provisioning)。这一层包含构建云原生基础设施的工具,如基础设施的创建、管理、配置流程的自动化,以及容器镜像的扫描、签名和存储等。供应层也跟安全相关,该层中的一些工具可用于设置和实施策略,将身份验证和授权内置到应用程序和平台中,以及处理 secret 分发等

%title插图%num

接下来让我们看一下供应层的每个类别,它所扮演的角色以及这些技术如何帮助应用程序适应新的云原生环境。

%title插图%num

自动化和配置

是什么

自动化和配置工具可加快计算资源(虚拟机、网络、防火墙规则、负载均衡器等)的创建和配置过程。这些工具可以处理基础设施构建过程中不同部分的内容,大多数工具都可与该空间中其他项目和产品集成。

解决的问题

传统上,IT 流程依赖高强度的手动发布过程,周期冗长,通常可达 3-6 个月。这些周期伴随着许多人工流程和管控,让生产环境的变更非常缓慢。这种缓慢的发布周期和静态的环境与云原生开发不匹配。为了缩短开发周期,必须动态配置基础设施且无需人工干预

如何解决问题

供应层的这些工具使工程师无需人工干预即可构建计算环境。通过代码化环境设置,只需点击按钮即可实现环境配置。手动设置容易出错,但是一旦进行了编码,环境创建就会与所需的确切状态相匹配,这是一个巨大的优势。

尽管不同工具实现的方法不同,但它们都是通过自动化来简化配置资源过程中的人工操作。

对应工具

当我们从老式的人工驱动构建方式过渡到云环境所需的按需扩展模式时,会发现以前的模式和工具已经无法满足需求,组织也无法维持一个需要创建、配置和管理服务器的 7×24 员工队伍。Terraform 之类的自动化工具减少了扩展数服务器和相关网络以及防火墙规则所需的工作量。Puppet,Chef 和 Ansible 之类的工具可以在服务器和应用程序启动时以编程方式配置它们,并允许开发人员使用它们

一些工具直接与 AWS 或 vSphere 等平台提供的基础设施 API 进行交互,还有一些工具则侧重于配置单个计算机以使其成为 Kubernetes 集群的一部分。Chef 和 Terraform 这类的工具可以进行互操作以配置环境。OpenStack 这类工具可提供 IaaS 环境让其他工具使用。

从根本上讲,在这一层,你需要一个或多个工具来为 Kubernetes 集群搭建计算环境、CPU、内存、存储和网络。此外,你还需要其中的一些工具来创建和管理 Kubernetes 集群本身。

在撰写本文时,该领域中有三个 CNCF 项目:KubeEdge(一个沙盒项目)以及 Kubespray 和 Kops(后两个是 Kubernetes 子项目,虽然未在全景图中列出,但它们也属于 CNCF)。此类别中的大多数工具都提供开源和付费版本。

%title插图%num

%title插图%num

%title插图%num

Container Registry

是什么

在定义 Container Registry 之前,我们首先讨论三个紧密相关的概念:

  • 容器是执行流程的一组技术约束。容器内启动的进程会相信它们正在自己的专用计算机上运行,而不是在与其他进程(类似于虚拟机)共享的计算机上运行。简而言之,容器可以使你在任何环境中都能控制自己的代码运行。
  • 镜像是运行容器及其过程所需的一组存档文件。你可以将其视为模板的一种形式,可以在其上创建无限数量的容器。
  • 仓库是存储镜像的空间。

回到 Container Registry,这是分类和存储仓库的专用 Web 应用程序。

镜像包含执行程序(在容器内)所需的信息,并存储在仓库中,仓库被分类和分组。构建、运行和管理容器的工具需要访问(通过引用仓库)这些镜像。

%title插图%num

解决的问题

云原生应用程序被打包后以容器的方式运行。Container Registry 负责存储和提供这些容器镜像。

如何解决

通过在一个地方集中存储所有容器镜像,这些容器镜像可以很容易地被应用程序的开发者访问。

对应工具

Container Registry 要么存储和分发镜像,要么以某种方式增强现有仓库。本质上,它是一种 Web API,允许容器引擎存储和检索镜像。许多 Container Registry 提供接口,使容器扫描/签名工具来增强所存储镜像的安全性。有些 Container Registry 能以特别有效的方式分发或复制图像。任何使用容器的环境都需要使用一个或多个仓库。

该空间中的工具可以提供集成功能,以扫描,签名和检查它们存储的镜像。在撰写本文时,Dragonfly 和 Harbor 是该领域中的 CNCF 项目,而 Harbor *近成为了*个遵循 OCI 的仓库。主要的云提供商都提供自己的托管仓库,其他仓库可以独立部署,也可以通过 Helm 之类的工具直接部署到 Kubernetes 集群中。

%title插图%num

%title插图%num

%title插图%num

安全和合规

是什么

云原生应用程序的目标是快速迭代。为了定期发布代码,必须确保代码和操作环境是安全的,并且只能由获得授权的工程师访问。这一部分的工具和项目可以用安全的方式创建和运行现代应用程序。

解决什么问题

这些工具和项目可为平台和应用程序加强、监控和实施安全性。它们使你能在容器和 Kubernetes 环境中设置策略(用于合规性),深入了解存在的漏洞,捕获错误配置,并加固容器和集群。

如何解决

为了安全地运行容器,必须对其进行扫描以查找已知漏洞,并对其进行签名以确保它们未被篡改。Kubernetes 默认的访问控制比较宽松,对于想攻击系统的人来说, Kubernetes 集群很容易成为目标。该空间中的工具和项目有助于增强群集,并在系统运行异常时提供工具来检测。

对应工具

为了在动态、快速发展的环境中安全运行,我们必须将安全性视为平台和应用程序开发生命周期的一部分。这部分的工具种类繁多,可解决安全领域不同方面的问题。大多数工具属于以下类别:

  • 审计和合规;
  • 生产环境强化工具的路径:
    • 代码扫描
    • 漏洞扫描
    • 镜像签名
  • 策略制定和执行
  • 网络层安全

其中的一些工具和项目很少会被直接使用。例如 TrivyClaire 和 Notary,它们会被 Registry 或其他扫描工具所利用。还有一些工具是现代应用程序平台的关键强化组件,例如 Falco 或 Open Policy Agent(OPA)。

该领域有许多成熟的供应商提供解决方案,也有很多创业公司的业务是把 Kubernetes 原生框架推向市场。在撰写本文时,FalcoNotary/TUF 和 OPA 是该领域中仅有的 CNCF 项目。

%title插图%num

%title插图%num

%title插图%num

密钥和身份管理

是什么

在进入到密钥管理之前,我们首先定义一下密钥。密钥是用于加密或签名数据的字符串。和现实中的钥匙一样,密钥锁定(加密)数据,只有拥有正确密钥的人才能解锁(解密)数据。

随着应用程序和操作开始适应新的云原生环境,安全工具也在不断发展以满足新的需求。此类别中的工具和项目可用于安全地存储密码和其他 secrets(例如 API 密钥,加密密钥等敏感数据)、从微服务环境中安全删除密码和 secret 等

解决的问题

云原生环境是高度动态的,需要完全编程(无人参与)和自动化的按需 secret 分发。应用程序还必须知道给定的请求是否来自有效来源(身份验证),以及该请求是否有权执行操作(授权)。通常将其称为 AuthN 和 AuthZ。

如何解决

每个工具或项目实施的方法不同,但他们都提供:

  • 安全分发 secret 或密钥的方法。
  • 身份认证或(和)授权的服务或规范。

对应的工具

此类别中的工具可以分为两组:

  • 一些工具专注于密钥生成、存储、管理和轮转。
  • 另一些专注于单点登录和身份管理。

拿 Vault 来说,它是一个通用的密钥管理工具,可管理不同类型的密钥。而 Keycloak 则是一个身份代理工具,可用于管理不同服务的访问密钥。

在撰写本文时,SPIFFE/SPIRE 是该领域中唯一的 CNCF 项目。

%title插图%num

%title插图%num

供应层专注于构建云原生平台和应用程序的基础,其中的工具涉及基础设施供应、容器注册表以及安全性。本文是详细介绍了云原生全景图的*底层。在下一篇文章中,我们将重点介绍运行时层,探索云原生存储、容器运行时和网络的相关内容。

原文链接:https://thenewstack.io/the-cloud-native-landscape-the-provisioning-layer-explained/

关于 Serverless 函数计算的字体安装

前言

首先介绍下在本文出现的几个比较重要的概念:

函数计算(Function Compute):函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息 参考%title插图%num

Fun:Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档 参考%title插图%num

备注: 本文介绍的技巧需要 Fun 版本大于等于 3.6.7。

函数计算运行环境中内置一些常用字体,但仍不满足部分用户的需求。如果应用中需要使用其它字体,需要走很多弯路。本文将介绍如何通过 ‍Fun 工具将自定义字体部署到函数计算,并正确的在应用中被引用。%title插图%num

1. 你需要做什么

  • 在代码(CodeUri)目录新建一个 fonts 目录;
  • 将字体复制到 fonts 目录;
  • 使用 fun deploy 进行部署。

2. 工具安装

建议直接从这里下载二进制可执行程序,解压后即可直接使用。‍下载地址

执行 fun –version 检查 Fun 是否安装成功。

  1. 1$ fun –version
  2. 23.6.7

3. 示例

demo 涉及的代码,托管在 ‍github 上。项目目录结构如下:

  1. 1$ tree -L -a 1
  2. 2
  3. 3├── index.js
  4. 4├── package.json
  5. 5└── template.yml

index.js 中代码:

  1. 1‘use strict’;
  2. 2
  3. 3var fontList = require(‘font-list’)
  4. 4
  5. 5module.exports.handler = async function (request, response, context{
  6. 6    response.setStatusCode(200);
  7. 7    response.setHeader(‘content-type’‘application/json’);
  8. 8    response.send(JSON.stringify(await fontList.getFonts(), null4));
  9. 9};

index.js 中借助 node 包 ‍font-list 列出系统上可用的字体。

template.yml:

  1. 1ROSTemplateFormatVersion: ‘2015-09-01’
  2. 2Transform: ‘Aliyun::Serverless-2018-04-03’
  3. 3Resources:
  4. 4  fonts-service: # 服务名
  5. 5    Type: ‘Aliyun::Serverless::Service’
  6. 6    Properties:
  7. 7      Description: fonts example
  8. 8    fonts-function: # 函数名
  9. 9      Type: ‘Aliyun::Serverless::Function
  10. 10      Properties:
  11. 11        Handlerindex.handler
  12. 12        Runtimenodejs8
  13. 13        CodeUri: ./
  14. 14        InstanceConcurrency: 10
  15. 15      Events:
  16. 16        httptest:
  17. 17          TypeHTTP
  18. 18          Properties:
  19. 19            AuthTypeANONYMOUS
  20. 20            Methods:
  21. 21              – GET
  22. 22              – POST
  23. 23              – PUT
  24. 24
  25. 25  tmp_domain: # 临时域名
  26. 26    Type: ‘Aliyun::Serverless::CustomDomain
  27. 27    Properties:
  28. 28      DomainNameAuto
  29. 29      ProtocolHTTP
  30. 30      RouteConfig:
  31. 31        Routes:
  32. 32          /:
  33. 33            ServiceNamefontsservice
  34. 34            FunctionNamefontsfunction
  1. template.yml 中定义了名为 fonts-service 的服务,此服务下定义一个名为 fonts-functionhttp trigger 函数。tmp_domain 中配置自定义域名中路径(/)与函数(fontsservice/fontsfunction)的映射关系。

1)下载字体

你可以通过 ‍这里 下载自定义字体 Hack,然后复制字体到 fonts 目录。

此时 demo 目录结构如下:

  1. 1$ tree -L 2 -a
  2. 2
  3. 3├── fonts(+)
  4. 4│   ├── Hack-Bold.ttf
  5. 5│   ├── Hack-BoldItalic.ttf
  6. 6│   ├── Hack-Italic.ttf
  7. 7│   └── Hack-Regular.ttf
  8. 8├── index.js
  9. 9├── package.json
  10. 10└── template.yml
  11. 2)安装依赖
  1. 1$ npm install
  2. 3)部署到函数计算

可以通过 fun deploy 直接发布到远端。

%title插图%num

4)预览线上效果

fun deploy 部署过程中,会为此函数生成有时效性的临时域名:

%title插图%num

打开浏览器,输入临时域名并回车:

%title插图%num

可以看到字体 Hack 已生效!!!

%title插图%num

原理介绍

  • fun deploy 时,如果检测到 CodeUri 下面有 fonts 目录,则为用户在 CodeUri 目录生成一个 .fonts.conf 配置文件。在该配置中,相比于原来的 /etc/fonts/fonts.conf 配置,添加了 /code/fonts 作为字体目录。
  • 自动在 template.yml 中添加环境变量,FONTCONFIG_FILE = /code/.fonts.conf,这样在函数运行时就可以正确的读取到自定义字体目录。

如果依赖过大,超过函数计算的限制(50M)则:

  • 将 fonts 目录添加到 .nas.yml;
  • 将 fonts 对 nas 的映射目录追加到 .fonts.conf 配置。

fun deploy 对大依赖的支持可参考 ‍《开发函数计算的正确姿势——轻松解决大依赖部署》

%title插图%num

小结

只需要在代码(CodeUri)目录新建一个 fonts 目录,然后复制所有字体到该目录即可。Fun 会自动帮你处理配置文件(.fonts.conf),环境变量以及大依赖场景的情况。

七种分布式事务的解决方案

%title插图%num

什么是分布式事务

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器「分别位于不同的分布式系统的不同节点之上」。一个大的操作由N多的小的操作共同完成。而这些小的操作又分布在不同的服务上。针对于这些操作,「要么全部成功执行,要么全部不执行」。

%title插图%num

为什么会有分布式事务?

举个例子:

%title插图%num

转账是*经典的分布式事务场景,假设用户 A 使用银行 app 发起一笔跨行转账给用户 B,银行系统首先扣掉用户 A 的钱,然后增加用户 B 账户中的余额。

如果其中某个步骤失败,此时就有可能会出现 2 种「异常」情况:

  • 1.用户 A 的账户扣款成功,用户 B 账户余额增加失败
  • 2.用户 A 账户扣款失败,用户 B 账户余额增加成功。

对于银行系统来说,以上 2 种情况都是「不允许发生」,此时就需要事务来保证转账操作的成功。

在「单体应用」中,我们只需要贴上@Transactional注解就可以开启事务来保证整个操作的「原子性」。

但是看似以上简单的操作,在实际的应用架构中,不可能是单体的服务,我们会把这一系列操作交给「N个服务」去完成,也就是拆分成为「分布式微服务架构」。

%title插图%num

比如下订单服务,扣库存服务等等,必须要「保证不同服务状态结果的一致性」,于是就出现了分布式事务。

%title插图%num

分布式理论

  CAP定理

在一个分布式系统中,以下三点特性无法同时满足,「鱼与熊掌不可兼得」

一致性(C):
在分布式系统中的所有数据备份,「在同一时刻是否拥有同样的值」。(等同于所有节点访问同一份*新的数据副本)

可用性(A):
在集群中一部分节点「故障」后,集群整体「是否还能响应」客户端的读写请求。(对数据更新具备高可用性)

分区容错性(P):
即使出现「单个组件无法可用,操作依然可以完成」。

具体地讲在分布式系统中,在任何数据库设计中,一个Web应用「至多只能同时支持上面的两个属性」。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。

  BASE理论

在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢?

前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。BASE理论指的是:

  • 「Basically Available(基本可用)」
  • 「Soft state(软状态)」
  • 「Eventually consistent(*终一致性)」

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到*终一致性(Eventual consistency)。

%title插图%num

分布式事务解决方案

  两阶段提交(2PC)

熟悉mysql的同学对两阶段提交应该颇为熟悉,mysql的事务就是通过「日志系统」来完成两阶段提交的。

两阶段协议可以用于单机集中式系统,由事务管理器协调多个资源管理器;也可以用于分布式系统,「由一个全局的事务管理器协调各个子系统的局部事务管理器完成两阶段提交」。

%title插图%num

这个协议有「两个角色」,

A节点是事务的协调者,B和C是事务的参与者。

事务的提交分成两个阶段

*个阶段是「投票阶段」

1.协调者首先将命令「写入日志」

2. 「发一个prepare命令」给B和C节点这两个参与者

3.B和C收到消息后,根据自己的实际情况,「判断自己的实际情况是否可以提交」

4.将处理结果「记录到日志」系统

5.将结果「返回」给协调者

%title插图%num

第二个阶段是「决定阶段」

当A节点收到B和C参与者所有的确认消息后

  • 「判断」所有协调者「是否都可以提交」
    • 如果可以则「写入日志」并且发起commit命令
    • 有一个不可以则「写入日志」并且发起abort命令
  • 参与者收到协调者发起的命令,「执行命令」
  • 将执行命令及结果「写入日志」
  • 「返回结果」给协调者

  可能会存在哪些问题?

  • 「单点故障」:一旦事务管理器出现故障,整个系统不可用
  • 「数据不一致」:在阶段二,如果事务管理器只发送了部分 commit 消息,此时网络发生异常,那么只有部分参与者接收到 commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
  • 「响应时间较长」:整个消息链路是串行的,要等待响应结果,不适合高并发的场景
  • 「不确定性」:当事务管理器发送 commit 之后,并且此时只有一个参与者收到了 commit,那么当该参与者与事务管理器同时宕机之后,重新选举的事务管理器无法确定该条消息是否提交成功。

  三阶段提交(3PC)

三阶段提交又称3PC,相对于2PC来说增加了CanCommit阶段和超时机制。如果段时间内没有收到协调者的commit请求,那么就会自动进行commit,解决了2PC单点故障的问题。

但是性能问题和不一致问题仍然没有根本解决。下面我们还是一起看下三阶段流程的是什么样的?

  • *阶段:「CanCommit阶段」这个阶段所做的事很简单,就是协调者询问事务参与者,你是否有能力完成此次事务。
    • 如果都返回yes,则进入第二阶段
    • 有一个返回no或等待响应超时,则中断事务,并向所有参与者发送abort请求
  • 第二阶段:「PreCommit阶段」此时协调者会向所有的参与者发送PreCommit请求,参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。
  • 第三阶段:「DoCommit阶段」在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从“预提交状态”转变为“提交状态”。然后向所有的参与者节点发送”doCommit”请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。

  补偿事务(TCC)

TCC其实就是采用的补偿机制,其核心思想是:「针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作」。它分为三个阶段:

「Try,Confirm,Cancel」

  • Try阶段主要是对「业务系统做检测及资源预留」,其主要分为两个阶段
    • Confirm 阶段主要是对「业务系统做确认提交」,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
    • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,「预留资源释放」。

比如下一个订单减一个库存:

%title插图%num

执行流程:

  • Try阶段:订单系统将当前订单状态设置为支付中,库存系统校验当前剩余库存数量是否大于1,然后将可用库存数量设置为库存剩余数量-1,
    • 如果Try阶段「执行成功」,执行Confirm阶段,将订单状态修改为支付成功,库存剩余数量修改为可用库存数量
    • 如果Try阶段「执行失败」,执行Cancel阶段,将订单状态修改为支付失败,可用库存数量修改为库存剩余数量

TCC 事务机制相比于上面介绍的2PC,解决了其几个缺点:

  • 1.「解决了协调者单点」,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
  • 2.「同步阻塞」:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
  • 3.「数据一致性」,有了补偿机制之后,由业务活动管理器控制一致性

总之,TCC 就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,并且很大程度的「增加」了业务代码的「复杂度」,因此,这种模式并不能很好地被复用。

  本地消息表

%title插图%num

执行流程:

  • 消息生产方,需要额外建一个消息表,并「记录消息发送状态」。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。
    • 如果消息发送失败,会进行重试发送。
  • 消息消费方,需要「处理」这个「消息」,并完成自己的业务逻辑。
    • 如果是「业务上面的失败」,可以给生产方「发送一个业务补偿消息」,通知生产方进行回滚等操作。
    • 此时如果本地事务处理成功,表明已经处理成功了
    • 如果处理失败,那么就会重试执行。
  • 生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。

  消息事务

消息事务的原理是将两个事务「通过消息中间件进行异步解耦」,和上述的本地消息表有点类似,但是是通过消息中间件的机制去做的,其本质就是’将本地消息表封装到了消息中间件中’。

执行流程:

  • 发送prepare消息到消息中间件
  • 发送成功后,执行本地事务
    • 如果事务执行成功,则commit,消息中间件将消息下发至消费端
    • 如果事务执行失败,则回滚,消息中间件将这条prepare消息删除
  • 消费端接收到消息进行消费,如果消费失败,则不断重试

这种方案也是实现了「*终一致性」,对比本地消息表实现方案,不需要再建消息表,「不再依赖本地数据库事务」了,所以这种方案更适用于高并发的场景。目前市面上实现该方案的「只有阿里的 RocketMQ」。

  *大努力通知

*大努力通知的方案实现比较简单,适用于一些*终一致性要求较低的业务。

执行流程:

  • 系统 A 本地事务执行完之后,发送个消息到 MQ;
  • 这里会有个专门消费 MQ 的服务,这个服务会消费 MQ 并调用系统 B 的接口;
  • 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么*大努力通知服务就定时尝试重新调用系统 B, 反复 N 次,*后还是不行就放弃。

  Sagas 事务模型

Saga事务模型又叫做长时间运行的事务

其核心思想是「将长事务拆分为多个本地短事务」,由Saga事务协调器协调,如果正常结束那就正常完成,如果「某个步骤失败,则根据相反顺序一次调用补偿操作」。

Seata框架中一个分布式事务包含3种角色:

「Transaction Coordinator (TC)」:事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。「Transaction Manager (TM)」:控制全局事务的边界,负责开启一个全局事务,并*终发起全局提交或全局回滚的决议。「Resource Manager (RM)」:控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

seata框架「为每一个RM维护了一张UNDO_LOG表」,其中保存了每一次本地事务的回滚数据。

具体流程:

1.首先TM 向 TC 申请「开启一个全局事务」,全局事务「创建」成功并生成一个「全局唯一的 XID」。

2.XID 在微服务调用链路的上下文中传播。

3.RM 开始执行这个分支事务,RM首先解析这条SQL语句,「生成对应的UNDO_LOG记录」。下面是一条UNDO_LOG中的记录,UNDO_LOG表中记录了分支ID,全局事务ID,以及事务执行的redo和undo数据以供二阶段恢复。

4.RM在同一个本地事务中「执行业务SQL和UNDO_LOG数据的插入」。在提交这个本地事务前,RM会向TC「申请关于这条记录的全局锁」。

如果申请不到,则说明有其他事务也在对这条记录进行操作,因此它会在一段时间内重试,重试失败则回滚本地事务,并向TC汇报本地事务执行失败。

6.RM在事务提交前,「申请到了相关记录的全局锁」,然后直接提交本地事务,并向TC「汇报本地事务执行成功」。此时全局锁并没有释放,全局锁的释放取决于二阶段是提交命令还是回滚命令。

7.TC根据所有的分支事务执行结果,向RM「下发提交或回滚」命令。

  • RM如果「收到TC的提交命令」,首先「立即释放」相关记录的全局「锁」,然后把提交请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。异步队列中的提交请求真正执行时,只是删除相应 UNDO LOG 记录而已。
  • RM如果「收到TC的回滚命令」,则会开启一个本地事务,通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。将 UNDO LOG 中的后镜与当前数据进行比较,
  • 如果不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。

    如果相同,根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句并执行,然后提交本地事务达到回滚的目的,*后释放相关记录的全局锁。

%title插图%num

总结

本文介绍了分布式事务的一些基础理论,并对常用的分布式事务方案进行了讲解。

分布式事务本身就是一个技术难题,业务中具体使用哪种方案还是需要不同的业务特点自行选择,但是我们也会发现,分布式事务会大大的提高流程的复杂度,会带来很多额外的开销工作,「代码量上去了,业务复杂了,性能下跌了」。

所以,当我们真实开发的过程中,能不使用分布式事务就不使用。