整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

宇宙厂:Vue3.0 为何用 Proxy 替代 de

宇宙厂:Vue3.0 为何用 Proxy 替代 defineProperty?

家好,很高兴又见面了,我是"高级前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

1. 什么是 Object.defineProperty

1.1 Object.defineProperty 基本用法

Object.defineProperty() 允许精确添加或修改对象属性。通过赋值添加的普通属性会在枚举属性时(例如 for...in、Object.keys() 等)出现,值可以被更改,也可以被删除。

defineProperty() 方法允许更改额外细节,以使其不同于默认值。默认情况下,使用 Object.defineProperty() 添加的属性是不可写、不可枚举和不可配置的。此外,Object.defineProperty() 使用 [[DefineOwnProperty]] 内部方法,而不是 [[Set]],因此即使属性已经存在也不会调用 setter。


Object.defineProperty(obj, prop, descriptor)

方法每一个参数定义如下:

  • obj:要定义属性的对象。
  • prop:一个字符串或 Symbol,指定了要定义或修改的属性键。
  • descriptor:要定义或修改的属性的描述符,包括:configurable(如是否可删除)、enumerable、writable、get、set 等等。

下面示例使用 Object.defineProperty 进行对象属性定义:

const obj={};
// 1. 使用 null 原型:没有继承的属性
const descriptor=Object.create(null);
descriptor.value="static";

// 默认情况下,不可枚举、不可配置、不可写
// obj.key="static modified" 赋值后依然是 "static"
Object.defineProperty(obj, "key", descriptor);

// 2. 使用一个包含所有属性的临时对象字面量来明确其属性
Object.defineProperty(obj, "key2", {
  enumerable: false,
  configurable: false,
  writable: false,
  value: "static",
});

// 3. 重复利用同一对象
function withValue(value) {
  const d=withValue.d ||
    (withValue.d={
      enumerable: false,
      writable: false,
      configurable: false,
      value,
    });

  // 避免重复赋值
  if (d.value !==value) d.value=value;
  return d;
}
// 然后
Object.defineProperty(obj, "key", withValue("static"));

// 如果 freeze 可用,防止添加或删除对象原型属性
// (value、get、set、enumerable、writable、configurable)
(Object.freeze || Object)(Object.prototype);

1.2 Object.defineProperty 优缺点

Object.defineProperty 的主要优点包括:

  • Object.defineProperty() 方法可以对属性的行为方式进行细粒度的控制
  • 允许设置只读属性,防止意外修改
  • 开发者可以决定某个属性是否在枚举期间出现,从而实现特定的功能
  • 允许开发者使属性不可删除,从而保证核心属性的安全。

当然,Object.defineProperty 也有不足之处,主要体现在:

  • Object.defineProperty() 不能很好地处理数组,因为无法捕获修改索引值或长度属性以及动态属性(动态 getter 是一种没有为 property 显式定义 getter,而是在访问属性时动态创建的),详情可以看这篇文章(https://vue3js.cn/interview/vue3/proxy.html#一、object-defineproperty)。同时,也不支持嵌套对象,这意味着不会观察到嵌套对象的任何更改

下面是对象示例:

const obj={
    foo: "foo",
    bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar='xxx' // no ok

下面是数组示例:

const arrData=[1,2,3,4,5];
arrData.forEach((val,index)=>{
    defineProperty(arrData,index,val)
})
arrData.push() // no ok
arrData.pop()  // no ok
arrDate[0]=99 // ok

基于对象和数组的以上局限性,Vue2 增加了 set、delete API,并且对数组 api 方法进行一个重写。

  • Object.defineProperty() 的语法很冗长,可能会增加可读性,影响代码的模块化和可重用性。

而掌握 Object.defineProperty 的关键在于透彻理解属性描述符的属性。 例如,正确地将 writable 属性设置为 false 可以确保属性值在整个程序中保持不变,从而减少出现错误的机会。

比如下面的示例将 π 置为常量后将无法修改:

let constantObj={};
Object.defineProperty(constantObj, 'pi', {
  value: 3.14159,
  writable: false
});

console.log(constantObj.pi);
// Outputs 3.14159
constantObj.pi=3;
// Attempting to change the value
console.log(constantObj.pi);
// Still outputs 3.14159

注意:Vue 3 改用了 Proxy 。Proxy 可以拦截对象属性读取、赋值和删除操作,从而能够在属性发生变化时触发相应的更新。对于数组,Proxy 可以拦截数组的修改操作,比如: push、pop、splice 等,从而能够在数组发生变化时触发相应的更新。


此外,Proxy 还可以拦截对象的原型方法和构造函数调用,从而可以对对象的所有操作进行拦截和处理。

2. 什么是 Proxy

JavaScript 的 Proxy 对象是一项强大的功能,使开发者能够拦截和自定义对对象执行的操作,例如:属性查找、赋值、枚举和函数调用。 这种多功能工具允许开发人员创建更高效、更灵活的代码,同时还提高代码的可维护性。

Proxy 遵循以下语法规范:

const p=new Proxy(target, handler)
  • target:要使用 Proxy 包装的目标对象,可以是任何类型的对象,包括原生数组,函数,甚至另一个代理。
  • handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

值得一提的是,handler 对象是一个容纳一批特定属性的占位符对象,包含有 Proxy 的各个捕获器(trap)。而且所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。

常见的捕获器包括:

  • getPrototypeOf()
  • setPrototypeOf()
  • isExtensible()
  • preventExtensions()
  • getOwnPropertyDescriptor()
  • defineProperty()
  • has()
  • get()
  • set()
  • deleteProperty()
  • ownKeys()
  • apply()
  • construct()

在以下例子中,使用了一个原生 JavaScript 对象,Proxy 会将所有应用到它的操作转发到这个对象上。

let target={};
let p=new Proxy(target, {});

p.a=37;
// 操作转发到目标

console.log(target.a);
// 37. 操作已经被正确地转发

3.Proxy 与 Object.defineProperty 主要区别

Proxy 与 Object.defineProperty 一个主要区别在于抽象级别。 Proxy 在对象周围创建一个新层,可以 Hook 任何属性,而无需预先显式定义, 而 Object.defineProperty 直接修改对象并要求相关属性在定义时就存在。

此外,Proxies 涵盖了广泛的 property 操作,而 Object.defineProperty 则专注于 attribute 属性操作。

关于 Proxy 和 Object.defineProperty 还需要弄清楚一个常见错误,即将 Object.defineProperty 用于复杂的动态对象,期望新属性的反应行为可能会导致意外结果,因为 Object.defineProperty 只影响现有属性。

比如下面的示例:

let object={};
Object.defineProperty(object, 'property', {
    value: 42,
    writable: false
});
object.newProperty=100;
console.log(object.newProperty);
// 输出: 100
// newProperty 的行为不受 Object.defineProperty 控制

实现新属性反应性(Reactivity)的正确方法是使用 Proxy,或者为每个新属性动态实现 Object.defineProperty。比如下面的 Proxy 示例:

let targetObject={message: 'Hello, world'};
let handler={
    set: function(target, prop, value) {
        if (prop==='newProperty') {
            target[prop]=value * 2;
        } else {
            target[prop]=value;
        }
    }
};

let proxy=new Proxy(targetObject, handler);
proxy.newProperty=100;
console.log(proxy.newProperty);
// 输入: 200

4. 使用 Proxy 的场景

4.1 验证对象属性

考虑创建一个需要具有某些有条件所需属性的严格架构的对象,可以通过使用代理包装对象并在 set 中实施验证检查来管理,从而确保只有有效数据进入对象。

比如下面的代码示例使用 Proxy 实现 set 方法,验证对象的属性是否在指定的 schema 中:

let schema={
    id: {
        type: 'number',
        required: true
    },
    comment: {
        type: 'string',
        required: false
    }
};
let handler={
    set: function (target, key, value) {
        if (schema[key] && typeof value !==schema[key].type) {
            throw new Error(`Type ${typeof value} is not assignable to type ${schema[key].type}`);
        } else if (schema[key] && schema[key].required && value===undefined) {
            throw new Error(`${key} is required.`);
        }
        target[key]=value;
        return true;
    }
};

let movie=new Proxy({}, handler);

4.2 对象级访问控制

Proxy 对象可以有效控制对象属性的访问,通常可用于提供对象的只读视图或限制可访问的对象属性的范围。

比如下面的代码示例表示访问 password 属性后则会抛出错误:

let personDetails={
    firstName: 'John',
    lastName: 'Doe',
    password: '12345!'
};

let handler={
    get: function (target, prop) {
        if (prop==='password') {
            throw new Error('Access to password is denied');
        }
        return target[prop];
    }
};
let proxy=new Proxy(personDetails, handler);
console.log(proxy.password);
// 抛出错误
console.log(proxy.firstName);
// 输出: 'John'

又或者下面的代码示例在修改元素属性之前做精确的控制,从而属性相互覆盖:

function assignIfNotExists(target, source){
    for (let prop in source) {
        if (!target.hasOwnProperty(prop)) {
            target[prop]=source[prop];
        }
    }
}
let data={username: 'Zach'};
let userInput={username: 'JohnDoe', password: 'secret'};
// Avoid overwriting 'username' in data object
assignIfNotExist(data, userInput);

4.3 数据绑定和观察者

Proxy 可以帮助构建数据绑定解决方案,比如:当应用程序的状态发生变化时,开发者可能希望跟踪变化并做出响应,比如: Vue.js 就是一个很好的示例。

let state={
    count: 0
};
let handler={
    set: function (target, property, value) {
        target[property]=value;
        console.log(`State has changed. New ${property}: ${value}`);
        return true;
    }
};
let proxy=new Proxy(state, handler);
proxy.count=2;
// 输出: State has changed. New count: 2

以上代码示例,JavaScript Proxy 提供了对对象交互的精确控制,从而实现复杂行为、验证、访问控制等等。 然而,由于 Proxy 的复杂性,考虑使用 Proxy 的开销也同样重要。 因此,Proxy 的使用应该针对特定的挑战,其独特的功能可以显著提高系统操作和可读性。

5.Proxy 与 Object.defineProperty 深入比较

5.1 性能

JavaScript Proxy 比 Object.defineProperty 消耗的时间稍多, Proxy 本质上应用了一个额外的抽象层(处理程序),从而可能会使操作比 Object.defineProperty 更慢。

比如下面的代码示例:

let object={};
Object.defineProperty(object, 'property', {
    value: 42,
    writable: false
});

object 中的 property 属性值必须是常量,直接访问 property 非常简单快捷。 而对于 Proxy 来说,在获取对象值之前有一个额外的检查和验证过程:

let targetObject={property: 42};
let handler={
    get: function(target, prop) {
        return target[prop];
    }
};
let proxy=new Proxy(targetObject, handler);

5.2 代码复杂性和可读性

Object.defineProperty 重点关注属性级别, 当需要控制属性是否可以修改、配置甚至枚举时则是理想选择,同时 Object.defineProperty 的用法直接且有针对性的,使代码更容易阅读和理解。

然而,Proxy 在提供更高级别的抽象方面表现出色。 Proxy 对象可以针对整个对象,而不仅仅是单个属性,从而允许开发人员以更高级的方式拦截和重新定义对象的默认行为。

然而,Proxy 中的处理程序可能会造成复杂性,因为总是需要通过一个额外的中间层。 其他开发者也需要对 Proxy 概念有更多的了解才能轻松阅读 Proxy 代码。

5.3 模块化和可重用性

在模块化和可重用性方面,当想要为更大范围甚至整个应用程序定义全局处理程序行为时,Proxy 通常会发挥作用。 Proxy 通常提供一种极好的方法来将特定的控制行为封装在单独的处理程序中。 这样,同一个处理程序可以与多个目标对象重复使用。

相反,Object.defineProperty 允许模块化和保护单个对象属性,对于以模块化方式定义、保护或控制对象的属性非常重要。

Proxy 提供了更多的可能性,捕获更多的动作,并提供对对象的更多控制。 然而,它们也会带来性能成本,需要了解它们的用法,并且可能会使调试变得复杂。

另一方面,Object.defineProperty 虽然不如代理那么强大和灵活,但提供了一种简单、直接且易于调试的方法。

6.Proxy 常见方法

6.1 Proxy 转为普通对象

const proxy={"name":"高级前端进阶"}

const proxyObj=new Proxy(proxy, {
  get: (target, prop)=> prop in target ? target[prop] : 37
});

console.log(proxyObj.a)
// 输出 37
console.log(proxyObj.name)
// 输出 ` 高级前端进阶 `
console.log(JSON.stringify(proxyObj))
// 输出 {"name":"晴天"}

值得注意的是,使用 JSON.parse(JSON.stringify(proxyObj)) 方法会删除任何不能字符串化的内容,比如:类、函数、回调等。

如果确实需要,可以考虑使用 Lodash 的 cloneDeep 函数,该方法在将 Proxy 对象转换为 POJO(The Plain Old JavaScript Object) 的同时保持对象结构方面确实做得很好。

 convertProxyObjectToPojo(proxyObj) {
  return _.cloneDeep(proxyObj);
}

6.2 Proxy 监听数组元素变化

以下示例表示 Proxy 确实能监听到数组元素的变更,这与 defineProperty 是有差别的,至于监听嵌套对象属性变化可以自行验证。

function get(target, prop, receiver) {
  console.log('target:' + target);
  console.log('property:' + prop);
  return Reflect.get(target, prop, receiver);
}

var handler={
  'get': get
};

// 为数组添加 Proxy
var proxy=new Proxy([1,2,3,4,5], handler );

console.log('Result=> beep:' + proxy.beep );
// target: 1,2,3,4,5
// property: beep
// Result=> beep: undefined
console.log('Result=> -123:' + proxy[ -123] );
// target: 1,2,3,4,5
// property: -123
// proxy:16 Result=> -123: undefined
console.log(proxy.fill( 1) );
// target: 1,2,3,4,5
// property: fill
// target: 1,2,3,4,5
// property: length
// Proxy(Array) {0: 1, 1: 1, 2: 1, 3: 1, 4: 1}
console.log('Result=> 0:' + proxy[ 0 ] );
// target: 1,1,1,1,1
// property: 0
// Result=> 0: 1
var arr1=[10, 20, 30, 40, 50];
Object.setPrototypeOf(arr1, proxy);
console.log('Result=> beep:' + arr1.beep );
console.log('Result=> -123:' + arr1[ -123 ] );
console.log(arr1.fill( 100) );
// 输出 (5) [100, 100, 100, 100, 100]
console.log('Result=> 0:' + arr1[ 0 ] );

6.3 Proxy 监听嵌套对象

var validator={
  get(target, key) {
    if (typeof target[key]==='object' && target[key] !==null) {
        // 如果是对象则继续创建 Proxy
      return new Proxy(target[key], validator)
    } else {
      return target[key];
    }
  },
  set (target, key, value) {
    console.log(target);
    // 输出 {salary: 8250, Proffesion: '.NET Developer'}
    console.log(key);
    // 输出 salary
    console.log(value);
    // 输出 foo
    return true
  }
}
var person={
      firstName: "alfred",
      lastName: "john",
      inner: {
        salary: 8250,
        Proffesion: ".NET Developer"
      }
}
var proxy=new Proxy(person, validator)
proxy.inner.salary='foo'
// 这一句代码会先访问 proxy.inner 属性,发现是 Object
// 然后会继续访问 salary 属性

参考资料

https://borstch.com/blog/objects-in-javascript-properties-methods-and-prototypes

https://borstch.com/blog/proxies-vs-objectdefineproperty-when-to-use-which

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

https://blog.javascripttoday.com/blog/deep-dive-proxies-in-javascript/

https://vue3js.cn/interview/vue3/proxy.html#二、proxy

https://vue3js.cn/interview/vue3/proxy.html#一、object-defineproperty

https://juejin.cn/post/7306783965532717108

https://www.youtube.com/watch?app=desktop&v=_k3WiANNB4U

https://segmentfault.com/q/1010000043053833

https://www.30secondsofcode.org/js/s/dynamic-getter-setter-proxy/

https://gist.github.com/kgryte/713ab40f36c128bc1d52

https://stackoverflow.com/questions/41299642/how-to-use-javascript-proxy-for-nested-objects

he Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc). - MDN

前言

前面两篇文章我们介绍了在JS中对引用类型做非破坏性操作的一些方式,对前端程序员来说,一个非常重要的好处就是可以让我们写出正确的redux reducer。在调研reducer更新方式的过程中,我注意到了一个库,叫做ImmerJS,它允许我们用破坏性的语法来得到非破坏性的结果。由于它是基于Proxy的机制来实现的,这让我对Proxy这个ES6的新特性产生了很大的兴趣。另外Vue3的reactivity system也是基于Proxy的,如果你使用Vue那么了解Proxy的机制也是很有必要的。

所以这两天我就抽时间去学习了一下,这篇文章就给大家简单介绍一下Proxy。

什么是Proxy?

Proxy,也就是”代理“,在程序中的意思通常是在用户和真正访问的对象之间加了一个中间层,用户不再直接访问原始对象,而是通过Proxy来做一个中转。比如我们最常见的关于代理的概念就是网络代理,分为"正向代理"和"反向代理"。"反向代理"拿nginx来讲,它就是在用户(浏览器)和目标(web server)之间的一个中间层,浏览器通过nginx(代理)才能访问到web server。通过代理可以实现访问控制、负载均衡等目标。

我们这里讲的JavaScript的Proxy也是类似的概念,只不过它是在对象对象的访问之间的一层代理,应用了Proxy之后,我们将通过Proxy来访问目标对象。

而Proxy会改变JavaScript中一些基础操作的执行路径,如读取属性、写入属性、对象遍历、函数调用等。这是一种强大的元编程(meta programming)机制,可以实现很多通过之前无法实现的功能特性。虽然Proxy并没有引入新的语法,但由于它会修改JavaScript的一些底层代码的执行方式,所以它是无法被完全polyfill的。

Proxy的几个概念

在你读Proxy的相关文档时,有几个概念会频繁出现,我们先对这些概念做一个简单介绍。

  • target - target指的就是我们的目标对象,也就是被代理的对象
  • trap - trap可以理解为是一些预定义的触发点以及定义在这些触发点上的函数,比如上面提到的读取属性、写入属性、函数调用等,如果我们在这些触发点上定义了函数,那么在对Proxy执行对应操作的时候,我们定义的函数将会被调用
  • handler - handler是一个对象,它包装了所有Proxy提供的trap函数,可以理解为是trap的集合

Proxy的创建

Proxy的创建是比较简单的,参考以下代码:

// 我们的原始对象
var target={
 x: 1,
}
// handler对象,里面定义了各种trap函数
var handler={
 get: function(obj, prop) {
 return obj[prop]
 }
}
// 创建proxy,参数分别为target和handler,含义见上方解释
var proxy=new Proxy(target, handler)
// 通过proxy访问target对象
console.log(proxy.x) // 输出:1

在上面代码中,我们通过new Proxy来创建proxy对象,新创建的proxy对象将代理我们对target对象的访问。当我们访问proxy.x的时候,我们定义的handler中的get函数(trap)将被调用。get接受到的参数分别为target对象和我们要访问的属性名称。而因为我们直接返回指定属性的值,所以返回proxy.x的返回值就是target中对应属性的值。

当然这里我们可以写任意的代码,也可以返回任意的值。

有哪些traps?

上面我们介绍了Proxy的创建方式,实现了一个简单的trap:get。所有的trap都是可选的,如果我们定义了trap,当我们执行特定操作的时候就会执行我们的trap,如果没有定义trap,默认操作就是执行将操作直接传递到目标对象。

接下来我们就来看一下JavaScript提供的traps都有哪些:

  • get - 当我们获取目标对象属性值的时候被调用,返回属性的值
  • set - 和get对应,当我们写入目标对象的属性值时被调用,改写属性的值
  • deleteProperty - 当我们删除目标对象的属性值时被调用
  • has - 当我们对目标对象使用in操作符时被调用,返回指定的属性是否存在
  • apply - 当我们对目标对象(这里是一个函数)执行函数调用时这个trap会被调用
  • construct - 当我们对目标对象(要求是一个函数)执行new操作符的时候被调用
  • ownKeys - 当我们调用Object.keys的时候这个trap将会被执行
  • getPrototypeOf - 当我们对目标对象调用Object.getPrototypeOf时这个trap将被执行
  • setPrototypeOf - 当我们对目标对象调用Object.setPrototypeOf时这个trap将被执行
  • isExtensible - 当我们对目标对象执行Object.isExtensible时这个trap将被执行
  • preventExtensions - 当我们对目标对象执行Object.preventExtensions时这个trap将被执行
  • getOwnPropertyDescriptor - 当我们对目标对象执行Object.getOwnPropertyDescriptor时这个trap将被调用
  • defineProperty - 当我们对目标对象执行Object.defineProperty时这个trap将被调用

Reflect

Reflect是一个对象,针对上面提到的每一个trap,它都定义了一个static方法。它主要用于方便我们写trap函数的时候将操作传递到目标对象。比如:

var target={
 x: 1,
}
var handler={
 get: function(...args) {
 	// 我们使用Reflect将操作直接传递到目标对象
 return Reflect.get(...args)
 },
 set: function(...args) {
 return Reflect.set(...args)
 }
}
var proxy=new Proxy(target, handler)
console.log(proxy.x) // 输出:1

由于每个trap都有一个对应的Reflect方法,而且他们的参数都是一致的,所以当我们需要在trap中将操作直接传递到目标对象的时候使用Reflect中的方法是非常方便的。Reflect中的很多方法和Object中的一些方法都比较相似,但还是有一些小的差别,比如返回值的不同等。可以参考这个网址。

和getter、setter的区别

通过以上的描述,我们可以看到Proxy和getter、setter有点像。它们都可以做到当我们访问或改写一个对象的属性值时调用一个特定的函数,但有以下区别:

  1. Proxy除了对属性值的获取和改写还可以改变其他JavaScript基础操作的执行方式,也就是它的功能比getter、setter多很多
  2. getter、setter是定义在对象本身上的,而Proxy是定义在另一个对象(proxy)上的。这表示Proxy允许我们比较方便的修改一些我们不方便直接改变其定义方式的对象的执行方式,比如一些第三方库中定义的对象等。

总结

ES6的Proxy对很多人可能比较陌生,但其实它真的非常简单,以上介绍的基本就是它的全部内容了。虽然实现起来比较简单,但是它可以实现很多非常强大的功能,比如ImmerJS这个库,它提供了用破坏性的语法来实现非破坏性操作的方式,我们打算下篇文章来介绍它

升Web应用的性能从未像今天这样刻不容缓。

在线经济活动的比例日益提高,就连发展中国家和地区的经济活动都已经有5%以上在线进行了(相关数据请参考本文后面的资源)。在这个超级链接、随时在线的现代世界,用户的期望也远非昔日可比。如果你的网站不能马上响应,你的应用不能立即运行,用户转身就会投奔你的竞争对手。

亚马逊大约10年前的一项研究表明,页面加载时间减少1/10秒,能够使其营收增长1%。另一项近期的调查也显示,一多半受访站点所有者提到因为自己应用的性能不佳导致了收入减少或者用户流失。

一个网站到底多快才行?页面加载每花1秒钟,就有大约4%的用户走掉。排名最靠前的电商站点的首次交互时间为1至3秒,这个区间的转换率最高。显而易见,Web应用性能的重要性与日俱增。

提升性能其实不难,难的是怎么看到结果。本文给出能够提升大约10倍网站性能的10个建议供大家参考。如此全面地涵盖各种性能优化技术,这还是头一回,但这些建议可能需要NGINX的一点支持。除了性能,这些建议也会涉及提升安全性。

1 建议一:使用反向代理服务器让应用更快更安全

果你的Web应用只跑在一台机器上,那要提升其性能非常简单:换一台更快的,多配几个处理器,多加几条内存,磁盘阵列也要高速的。换了以后,这台机器上跑的WordPress服务器、Node.js或Java应用速度都会加快。(要是应用还会访问另一台数据库服务器,那也简单:找两台更快的机器,用更快的网络连起来就行了。)

麻烦在于,机器速度并不是问题。很多时候Web应用慢,是因为要在各种任务之间切换,一会儿要处理数千个连接上的用户请求,一会儿要向磁盘读写文件,一会儿又要运行应用的代码,一会儿又要去干别的。应用服务器因此可能出现各种状况,耗尽内存、交换文件,或者让很多请求等待一个硬盘I/O之类的任务。

除了升级硬件,其实你还可以选择另外一种完全不同的方法:加一台反向代理服务器,分担上述一些任务。反向代理服务器位于运行应用的机器之前,负责处理来自外网的请求。反向代理服务器直接连到互联网,它与应用服务器通信使用的是快速的内部网络。

反向代理服务器可以让应用服务器专注于构建页面,然后交给反向代理向外网发送,而不必理会用户与应用的交互。由于不必等待客户端的响应,应用服务器的运行速度能达到接近最优的水平。

增加反向代理服务器同时也可以为Web服务器增添灵活性。比如,假设执行某种任务的服务器过载了,那随时可以再增加一台同类服务器;而如果这台服务器挂了,替换它也很容易。

鉴于这种灵活性,反向代理服务器往往也是其他性能优化手段的先决条件,比如:

  • 负载均衡(参见“建议二”),反向代理服务器上运行负载均衡服务,把流量平均分配给几台应用服务器。有了负载均衡,添加应用服务器根本不需要修改应用。
  • 缓存静态文件(参见“建议三”),图片或代码之类的可以直接请求的文件,都可以保存在反向代理服务器中,以便直接发给客户端。这样不仅可以更快地响应请求,还能减轻应用服务器的负担,加快其运行速度。
  • 保证站点安全,可以配置反向代理服务器提升其安全级别,通过它监控来快速识别和响应攻击,从而保存应用服务器安全。

NGINX专门为使用反向代理服务器做了设计,使其天然支持上述优化。由于使用事件驱动的处理机制,NGINX比传统服务器效率更高。NGINX Plus则增加了更高端的反向代理功能,如应用体检、特有的请求路由、高级缓存和售后支持。

传统服务器与NGINX Worker的比较

建议二:增加负载均衡服务器

加负载均衡服务器相对简单,但却能显著提升站点性能和安全性。通过它把流量分配给多个服务器,就可以不必升级Web服务器了。就算应用本身写得不太好,或者难以扩展,负载均衡都可以在不做其他改变的情况下提升用户体验。

负载均衡服务器首先是一个反向代理服务器(参见“建议一”),负责把来自互联网的请求转发给其他服务器。这里关键在于负载均衡服务器可以支持两台以上的应用服务器,使用一种选择算法在不同的服务器间分配请求。最简单的负载均衡算法是循环调度,即把新请求依次转发给可用服务器中的下一台服务器。其他算法还有把请求发给活动连接最少的服务器。NGINX Plus支持一种功能,就是把用户会话保持在同一台服务器上,叫做会话保持。

负载均衡服务器可以避免一台服务器过载而其他服务器过闲,从而极大提升性能。同时,有了它还可以让Web服务器扩容更简单,因为可以选用比较便宜的服务器,同时保证物尽其用。

可以通过负载均衡调度的协议包括HTTP、HTTPS、SPDY、HTTP/2、WebSocket、FastCGI、SCGI、uwsgi、memcached,以及其他一些应用形式,包括基于TCP的应用和其他第四层的协议。为此,首先要分析Web应用,看性能短板在哪里,然后再确定使用哪一个。

同一台服务器或用于负载均衡的服务器也可以承担其他任务,比如SSL终止、视客户端不同支持HTTP/1/x或HTTP/2、缓存静态文件。

NGINX经常被用来做负载均衡,更多信息请参考我们以前发的介绍性文章、有关配置的文章、电子书和相关的在线视频,当然还有文档。我们的商业版本NGINX Plus支持更多的负载均衡功能,如基于服务器响应时间路由负载和支持微软NTLM协议的负载均衡。

建议三:缓存静态及动态内容

存能提升Web应用性能,因为可以更快地把内容交付给客户端。缓存的策略包括预处理内容、在较快的设备上存储内容、把内容保存在靠近客户端的地方,以及同时运用这些策略。

缓存有两种。

  • 静态内容缓存,不常变化的文件,如图片(JPEG、PNG)和代码(CSS、JavaScript),可以保存在边缘服务器中,以便快速从内容或磁盘中获取。
  • 动态内容缓存,很多Web应用会为每个页面请求生成全新的HTML,把生成的每个HTML都缓存一小段时间,可能显著减少需要生成的页面总数,同时又可以保证交付的内容足够新鲜。

假设一个页面每秒被查看10次,而你缓存它1秒,那么90%针对这个页面的请求都将来自在缓存。如果你单独缓存静态内容,那么即使全新生成的页面,很可能大部分都来自缓存的内容。

缓存Web应用生成内容的技术主要分三种。

  • 把内容放到离用户近的地方。离用户近,传输时间少。
  • 把内容放到较快的机器上。机器快,检索速度快。
  • 把内容从过度使用的机器中拿走。有时候机器会比在专注执行特定任务时慢很多,那是因为太多任务让它们分心。这时候把内容拿到其他机器上,不仅对缓存的内容有好处,对非缓存的内容同样有利,因为托管它们的主机的负担减轻了。

Web应用的缓存可以在Web应用服务器内部或外部实现。首先,考虑缓存动态内容,以减轻应用服务器的负载。其次,缓存用于静态内容(包括那些动态生成内容的临时副本),进一步减轻应用服务器的负担。然后,考虑把缓存转移到其他更快或更靠近用户的机器,给应用服务器减负,缩短传输时间。

用好缓存能显著加快应用的响应速度。对很多网页来说,大图片之类的静态数据,往往占据一半以上的内容。不用缓存,查询和传输这类数据可能会花好几秒钟,而用缓存,则可能只要花几分之一秒。

可以举一个例子来说明怎么使用缓存,NGINX和NGINX Plus通过两个指令来设置缓存:proxy_cache_path和proxy_cache指定缓存的位置和大小、最长缓存时间以及其他参数。使用第三个(也是很受欢迎的)指令proxy_cache_use_stale,甚至可以告诉缓存在本来应该提供新鲜内容的服务器太忙或宕机时,提供原来的旧文件,对客户端来说,拿到内容总比拿不到强。从用户角度看,这样也可以树立你的站点或应用非常稳定的形象。

NGINX Plus支持高级缓存功能,包括缓存净化(caching purging)和通过控制板以可视化的形式展示缓存状态,实现实时监控。

要了解NGINX中关于缓存的更多信息,可以看看参考文档和NGINX Plus Admin Guide中的NGINX Content Caching。另外,关注微信公众号:Java技术栈,也可以获取我整理的 NGINX 教程,都是干货。

注意: 缓存涉及开发、决策和运维,完善的缓存策略,比如本文提到的这些,能够体现从DevOps角度考虑的价值。也说是说,开发人员、架构师、运维人员此时携手,共同保障一个网站的功能、响应时间、安全和业务目标。


建议四:压缩数据

缩同样能极大提升性能。图片、视频、音乐等文件都有非常成熟和高效的压缩标准(JPEG和PNG、MPEG-4、MP3),任何一个标准都可以把文件大小缩小一个数量级甚至更多。

文本文件,包括HTML(纯文本和HTML标签)、CSS和JavaScript代码,经常在不压缩的情况下传输。压缩这些数据对提升Web应用的感知性能有时候特别明显,尤其是移动用户的网络很慢又不稳定的情况下。

因为文本数据通过对于页面交互能够起到必要的支援作用,而多媒体数据则更多是锦上添花的作用。聪明的内容压缩可以把HTML、JavaScript、CSS等文本内容的缩小30%以上,因此能够相应地减少加载时间。

如果你使用SSL,压缩又可以减少必须经过SSL编码的数据量,从而补偿了压缩这些数据的CPU时间。

压缩数据的方法非常多。比如,建议六中关于HTTP/2的部分就描述了一个新颖的压缩思路,特别适合首部数据压缩。还有一个关于文本压缩的例子,就是可以在NGINX中开启GZIP压缩。预压缩文本数据之后,可以使用gzip_static指令直接发送.gz文件。

建议五:优化SSL/TLS

来越多的网站在使用Secure Sockets Layer(SSL)及后来的Transport Layer Security(TLS)协议。SSL/TLS通过加密从源服务器发送给用户的数据来提升网站安全性。Google会提升使用SSL/TLS的网站的搜索引擎排名,将有力地推动这一进程。点击这里了解SSL/TLS运行机制详解。另外,关注微信公众号:Java技术栈,也可以获取我整理的更多 HTTPS 教程,都是干货。

尽管采用率越来越高,但SSL/TLS造成的性能损失也困扰着很多网站。SSL/TLS拖慢网站的原因有两个。

1、每次打开新连接的初次握手都必须创建加密密钥,而浏览器使用HTTP/1.x对每个2、服务器建立多个连接的方式进一步加剧了这个问题。

服务器端加密数据和客户端解密数据的操作同样也是开销。

为了鼓励人们使用SSL/TLS,HTTP/2和SPDY(参见建议六)的作者将这两个协议设计为只让浏览器针对一次会话建立一个连接。这样就把SSL导致性能降低的两个主要原因之一消灭掉了。然而,说到优化SSL/TLS性能,还是有很多事情可做。

优化SSL/TLS的方法因Web服务器而异。以NGINX为例,NGINX使用OpenSSL,运行于普通机器上,能够提供接近定制机器的性能。NGINX SSL performance详细介绍了如何将SSL/TLS加密和解密的开销降至最低。

此外,这里还有一篇文章,介绍了很多种提升SSL/TLS性能的方法。简单总结一下,涉及的技术主要有如下几种。

  • 会话缓存。使用ssl_session_cache指令开启缓存,缓存每次SSL/STL连接时用到的参数。
  • 会话票或ID。把特定SSL/TLS会话的信息保存为一个会话票或ID,以便连接重用,而不必重新握手。
  • OCSP封套。通过缓存SSL/TLS证书信息减少握手时间。

NGINX和NGINX Plus都可以来终止SSL/TLS,即处理客户端信息的加密和解密,同时与其他服务器保持明文通信。在NGINX或NGINX Plus中设置处理SSL/TLS终止可以采取这几个步骤。而对于在接受TCP连接的服务器上使用NGINX Plus而言,可以参考这里的设置步骤。

建议六:实现HTTP/2或SPDY

经使用SSL/TLS的站点,如果再使用HTTP/2或SPDY则很可能提升性能,因为一个连接只要一次握手。尚未使用SSL/TLS、HTTP/2和SPDY的站点切换到SSL/TLS(通常会降低性能),从响应速度方面看,可能是一次倒退。点击这里了解HTTP/2详解。

谷歌2012年开始SPDY项目,致力于在HTTP/1.x之上实现更快的速度。HTTP/2则是IETF最近批准的基于SPDY的标准。SPDY得到了广泛支持,但很快就将被HTTP/2取代。

SPDY和HTTP/2的关键在于只用一个连接,而非多个连接。这一个连接是多路复用的,因此可以同时承载多个请求和响应。

只维持一个连接,可以省掉多个连接所需的设置和管理消耗。而且一个连接对SSL特别重要,因为可以将SSL/TLS建立安全连接所需的握手时间降至最少。

SPDY协议要求使用SSL/TLS,HTTP/2并没有正式要求,但目前所有支持HTTP/2的浏览器都只会在启用SSL/TLS的情况下才会使用它。换句话说,支持HTTP/2的浏览器只有在网站使用SSL且服务器接受HTTP/2流量的情况下才会使用HTTP/2。否则,浏览器会基于HTTP/1.x通信。

实现了SPDY或HTTP/2之后,域名分片、资源合并、图片精灵等之前针对HTTP的性能优化措施就用不着了。因此也可以简化代码和部署。关于HTTP/2会带来哪些变化,可以参考我们的这个白皮书。

NGINX很早就开始支持SPDY,而且今天使用SPDY的大多数站点都在运行NGIN

X。NGINX同样率先支持了HTTP/2,2015年9月,NGINX开源和NGINX Plus开始支持 HTTP/2。

随着时间推移,NGINX希望大多数站点启用SSL并迁移到HTTP/2。这样不仅可以让网站更安全,而且随着新的优化技术不断涌现,也可以通过简单的代码实现更高的性能。

建议七:升级软件

升应用性能的一个简单的方法,就是根据可靠性及性能选择软件。此外,高质量组件的开发者更可能不断提升性能和修复问题,因此使用最新的稳定版本是划算。新发布的版本会得到开发者和用户更多的关注,同时也会利用新的编译器优化技术,包括针对新硬件的调优。

相对旧版本,新发布的稳定版本明显性能更高。坚持升级,也可以保证在调优、问题修复和安全警报方面与时俱进。

不升级软件也会妨碍利用新能力。比如,HTTP/2目前要求OpenSSL 1.0.1。从2016年下半年开始,HTTP/2会要求OpenSSL 1.0.2,该版本发布于2015年1月。

NGINX用户可以从NGINX开源软件的最新版本或NGINX Plus开始,它们支持套接字共享、线程池(参见下文),而且都会持续优化性能。因此,检查一下自己的软件,尽量把它们升级到最新的版本。

建议八:调优Linux

Linux是今天大多数Web服务器的底层操作系统,作为一切基础设施的基础,Linux对提升性能至关重要。默认情况下,很多Linux系统都比较保守,仅以桌面办公为需求,以占用少量资源为调优目标。对于Web应用而言,为达到性能最佳,肯定需要重新调优。

Linux优化因Web服务器而异。以NGINX为例,可以从以下几方面考虑。另外,关注微信公众号:Java技术栈,也可以获取我整理的 NGINX 教程,都是干货。

存量队列。如果发现有一些连接得不到处理,可以增大net.core.somaxconn,即等待NGINX处理的最大连接数。如果这个连接数限制过小,应该可以看到错误消息,可以逐步提高这个值,直到错误消息不再出现。

  • 文件描述符。NGINX对每个连接最多使用两个文件描述符。如果系统服务于很多连接,可能需要增大sys.fs.file_max这个对描述符的系统级限制,以及nofile这个用户文件描述符限制,以支持增大后的负载。
  • 临时端口。在作为代理使用时,NGINX会为每个上游服务器创建临时端口。可以设置net.ipv4.ip_local_port_range,增大端口值的范围,以增加可用的端口量。此外,还可以减小net.ipv4.tcp_fin_timeout的值,它控制非活动端口释放重用的等待时间,加快周转。
  • 对NGINX而言,请参考NGINX性能调优指南,了解如何不费吹灰之力将你的Linux系统优化为能够支持更大的吞吐量。

建议九:调优Web服务器

论使用什么Web服务器,都需要针对应用对其调优。以下建议适用于任何Web服务器,但会给出只有NGINX的设置说明。

  • 访问日志。不要每个请求的日志都马上写到磁盘,可以在内存里做个缓存,然后批量定入。对NGINX而言,将buffer=_size_参数添加到access_log指令,等内存缓冲区写满后再把日志写到磁盘。如果你添加了**flush=_time_**参数,那么缓冲区的内容也会按照指定时间写入磁盘。
  • 缓冲。缓冲用于在内存里保存部分响应,直到缓冲区被填满,可以实现对客户端更有效的响应。无法写入内存的响应会被写到磁盘,从而降低性能。在NGINX的缓冲启用时,可以使用proxy_buffer_size和proxy_buffers指令来管理它。
  • 客户端活动连接。活动连接可以减少时间消耗,特别是在使用SSL/TLS的情下。对NGINX而言,可以针对客户端提高keepalive_requests的数值,默认值为100;也可以增大keepalive_timeout的值,让活动连接持续时间更长,从而让后续请求得到更快响应。
  • 上游活动连接。上游连接,即连接到应用服务器、数据库服务器的连接,同样可以从活动连接的设置中获得好处。对上游连接来说,可以增加活动连接,也就是每个工作进程可用的空闲活动连接的数量。这样可以增进连接重用,减少重开连接。关于活动连接的更多信息,请参考这篇博客。
  • 限制。限制客户端使用的资源可以提升性能和安全性。对NGINX而言,limit_conn和limit_conn_zone指令限制指定源的连接数,而limit_rate限制带宽。这些设置可以防止合法用户“侵吞”资源,同时也有助于防止攻击。limit_req和limit_req_zone指令限制客户端请求。对于到上游服务器的连接,可以在上游配置区的服务器指令中使用max_conns参数,它限制对上游服务器的连接,防止过载。相关的队列指令会创建一个队列,在max_conns限制到达后将指定的请求数保存指定的时间。
  • 工作进程。工作进程负责处理请求。NGINX采用基于事件的模型和OS相关的机制有效地在工作进程间分配请求。建议将worker_processes的值设置为每个CPU一个工作进程。如果需要,大多数系统都支持提高worker_connections的值(默认为512)。可以通过试验找到最适合你系统的这个值。
  • 套接字分片。通常,一个套接字监听器向所有工作进程分发新连接。套按字分片则为每个工作进程都创建一个套接字监听器,由内核在套接字监听器可用时为其指定连接。这样可以减少锁争用,提升多核系统上的性能。要启用套接字分片,在listen指令中包含reuseport参数。
  • 线程池。一个费时的操作会阻塞任何计算机进程。对Web服务器软件来说,磁盘访问可能阻碍很多较快的操作,比如内存中的计算和复制。在使用线程池的情况下,慢操作会被指定给一组独立的任务,而主处理循环会继续运行较快的操作。磁盘操作完成后,结果会返回到主处理循环。在NGINX中,read()系统调用和sendfile()被转载到了线程池。

提示,修改任何操作系统及周边设备的设置时,每次只修改一项,然后测试性能。如果该项修改导致了问题,或者并未提升性能,再改回去。

建议十:监控实时动态以发现问题和瓶颈

存应用高性能的关键是实时监控应用性能。必须实时监控特定设备及相应Web基础设施中应用的动态。

监控站点活动多数情况下是被动的,它只告诉你发生了什么,至于如何发现和解决问题,则是你自己的事情。

监控可以捕获以下几种问题:

1、服务器停机

2、服务器不稳,漏处理连接

3、服务器出现大面积缓存失效

4、服务器发送的内容不对

New Relic或Dynatrace等全局性的性能监控工具,可以帮我们监控远程加载页面的时间,而NGINX则可以帮你监控应用交付这一端。应用的性能数据可以告诉你优化手段什么时候真正给用户带去了不同的体验,以及什么时候需要扩容以满足越来越多的流量。

为了帮助用户尽快发现问题,NGINX Plus增加了应用程序体检功能,会报告经常重复出现的问题。NGINX Plus还具备session draining特性,会在已有任务完成前阻止新连接,以及慢启动容量,从而让恢复的服务器在负载均衡集群中达到应有的速度。使用得当的情况下,健康体检会在问题显著影响用户体验之前帮你定位问题,而session draining和慢启动则让你替换服务器时不影响感知的性能和在线时间。这张图展示了NGINX Plus内置的实时活动监控的控制板,涵盖了服务器、TCP连接和缓存。

结论:10倍性能提升

能提升因Web应用不同会有巨大差异。实际的提升取决于预算、时间,以及现有实现的与理想性能的差距。那么怎么让你的应用获得10倍的性能提升呢?

为了帮大家理解每项优化建议的潜能,下面再针对之前的建议给出一些实施方针,希望大家各取所需。

  • 反向代理服务器及负载均衡。没有负载均衡或池负载均衡,可能导致极低的性能。添加一个反向代理服务器,比如NGINX,可以减少Web应用在内存和磁盘之间的往返。负载均衡可以把任务从过载的服务器转移到空闲的服务器,也便于扩展。这些改变能极大地提升性能,与原有的部署方式最差的时候相比,10倍性能提升是很轻松的事,即使不到10倍那也在总体上有了质的飞跃。
  • 缓存动态和静态内容。如果你的Web服务器同时又充当了应用服务器,那么通过缓存动态内容就可以达到高峰期10倍的性能提升。缓存静态内容也可以有几倍的性能提升。
  • 压缩数据。使用JPEG、PNG、MPEG-4以及MP3等压缩格式能显著提升性能。如果这些手段都用上了,那么压缩的文本数据(代码及HTML)可以将初始页面加载时间提升两倍。
  • 优化SSL/TLS。安全握手对性能有很大影响,因此对其进行优化可以让初次响应加快两倍,对于文本内容较多的网站尤其如此。优化SSL/TLS下的媒体文件带来的性能提升很小。
  • 实施HTTP/2和SPDY。在使用SSL/TLS的情况下,这两个协议有可能提升网站的整体性能。
  • 调优Linux和Web服务器。使用优化的缓冲策略、使用活动连接,将耗时的任务转载至独立的线程池,可以显著提升性能。比如线程池可以将磁盘操作密集性任务的性能提升至少一个数量级。

希望大家自己多尝试以上技术,也希望大家分享自己在性能改进方面的心得。

译者:为之漫笔

来源:http://www.zcfy.cc/article/10-tips-for-10x-application-performance-nginx-22.html

原文:https://www.nginx.com/blog/10-tips-for-10x-application-performance/