麦田348462402

麦田348462402 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织 visonforcoding.github.io 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

麦田348462402 发布了文章 · 9月10日

glances+influxdb+granfana打造服务器监控系统

服务监控就是你的眼睛,当你对服务器运行状况一无所知时,你应该感到坐立不安。

<!--more-->

glances安装

glances是由python编写的,因此可以使用pip直接安装


pip3 install glances

influxdb安装


wget https://dl.influxdata.com/influxdb/releases/influxdb-1.8.2.x86_64.rpm

sudo yum localinstall influxdb-1.8.2.x86_64.rpm

收集数据到influxdb

配置 glances

vim /etc/glances/glances.conf


[influxdb]

# Configuration for the --export influxdb option

# https://influxdb.com/

host=localhost

port=8086

user=root

password=root

db=glances

prefix=localhost

#tags=foo:bar,spam:eggs

pip3 install influxdb

glances --export influxdb

执行 glances --export influxdb 测试下,报错

InfluxDB database 'glances' did not exist. Please create it需要新建数据库。

执行shell influx


CREATE DATABASE glances #创建数据

SHOW DATABASES # 查看数据库

再次执行glances --export influxdb ,可显示如下代表目前一切正常

granfana安装


wget https://dl.grafana.com/oss/release/grafana-7.1.5-1.x86_64.rpm

sudo yum install grafana-7.1.5-1.x86_64.rpm

启动


systemctl daemon-reload

systemctl start grafana-server

systemctl status grafana-server

  

systemctl enable grafana-server.service

配置数据源

grafana 还支持zipkin

博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议

本文永久链接是:http://visonforcoding.github.io/2020/08/25/glances-influxdb-granfana%E6%89%93%E9%80%A0%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9B%91%E6%8E%A7%E7%B3%BB%E7%BB%9F/

请我喝杯咖啡

查看原文

赞 0 收藏 0 评论 0

麦田348462402 赞了文章 · 9月9日

了不起的 IoC 与 DI

本文阿宝哥将从六个方面入手,全方位带你一起探索面向对象编程中 IoC(控制反转)和 DI(依赖注入) 的设计思想。阅读完本文,你将了解以下内容:

  • IoC 是什么、IoC 能解决什么问题;
  • IoC 与 DI 之间的关系、未使用 DI 框架和使用 DI 框架之间的区别;
  • DI 在 AngularJS/Angular 和 NestJS 中的应用;
  • 了解如何使用 TypeScript 实现一个 IoC 容器,并了解 装饰器、反射 的相关知识。

一、背景概述

在介绍什么是 IoC 容器之前,阿宝哥来举一个日常工作中很常见的场景,即创建指定类的实例。最简单的情形是该类没有依赖其他类,但现实往往是残酷的,我们在创建某个类的实例时,需要依赖不同类对应的实例。为了让小伙伴们能够更好地理解上述的内容,阿宝哥来举一个例子。

一辆小汽车 🚗 通常由 发动机、底盘、车身和电气设备 四大部分组成。汽车电气设备的内部构造很复杂,简单起见,我们只考虑三个部分:发动机、底盘和车身。

(图片来源:https://www.newkidscar.com/ve...

在现实生活中,要造辆车还是很困难的。而在软件的世界中,这可难不倒我们。👇是阿宝哥要造的车子,有木有很酷。

(图片来源:https://pixabay.com/zh/illust...

在开始造车前,我们得先看一下 “图纸”:

看完上面的 “图纸”,我们马上来开启造车之旅。第一步我们先来定义车身类:

1.定义车身类

export default class Body { }

2.定义底盘类

export default class Chassis { }

3.定义引擎类

export default class Engine {
  start() {
    console.log("引擎发动了");
  }
}

4.定义汽车类

import Engine from './engine';
import Chassis from './chassis';
import Body from './body';

export default class Car {
    engine: Engine;
    chassis: Chassis;
    body: Body;

    constructor() {
      this.engine = new Engine();
      this.body = new Body();
      this.chassis = new Chassis();
    }

    run() {
      this.engine.start();
    }
}

一切已准备就绪,我们马上来造一辆车:

const car = new Car(); // 阿宝哥造辆新车
car.run(); // 控制台输出:引擎发动了

现在虽然车已经可以启动了,但却存在以下问题:

  • 问题一:在造车的时候,你不能选择配置。比如你想更换汽车引擎的话,按照目前的方案,是实现不了的。
  • 问题二:在汽车类内部,你需要在构造函数中手动去创建汽车的各个部件。

为了解决第一个问题,提供更灵活的方案,我们可以重构一下已定义的汽车类,具体如下:

export default class Car {
    body: Body;
    engine: Engine;
    chassis: Chassis;
  
    constructor(engine, body, chassis) {
      this.engine = engine;
      this.body = body;
      this.chassis = chassis;
    }

    run() {
      this.engine.start();
    }
}

重构完汽车类,我们来重新造辆新车:

const engine = new NewEngine();
const body = new Body();
const chassis = new Chassis();

const newCar = new Car(engine, body, chassis);
newCar.run();

此时我们已经解决了上面提到的第一个问题,要解决第二个问题我们要来了解一下 IoC(控制反转)的概念。

二、IoC 是什么

IoC(Inversion of Control),即 “控制反转”。在开发中, IoC 意味着你设计好的对象交给容器控制,而不是使用传统的方式,在对象内部直接控制。  

如何理解好 IoC 呢?理解好 IoC 的关键是要明确 “谁控制谁,控制什么,为何是反转,哪些方面反转了”,我们来深入分析一下。  

  • 谁控制谁,控制什么:在传统的程序设计中,我们直接在对象内部通过 new 的方式创建对象,是程序主动创建依赖对象; 而 IoC 是有专门一个容器来创建这些对象,即由 IoC 容器控制对象的创建

    谁控制谁?当然是 IoC 容器控制了对象;控制什么?主要是控制外部资源(依赖对象)获取。

  • 为何是反转了,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在程序中主动控制去获取依赖对象,也就是正转; 而反转则是由容器来帮忙创建及注入依赖对象

    为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转了;哪些方面反转了?依赖对象的获取被反转了。

三、IoC 能做什么

IoC 不是一种技术,只是一种思想,是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。

传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试; 有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器注入组合对象,所以对象之间是松散耦合。 这样也便于测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。  

其实 IoC 对编程带来的最大改变不是从代码上,而是思想上,发生了 “主从换位” 的变化。应用程序本来是老大,要获取什么资源都是主动出击,但在 IoC 思想中,应用程序就变成被动了,被动的等待 IoC 容器来创建并注入它所需的资源了。    

四、IoC 与 DI 之间的关系

对于控制反转来说,其中最常见的方式叫做 依赖注入,简称为 DI(Dependency Injection)。

组件之间的依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。 依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。

通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

理解 DI 的关键是 “谁依赖了谁,为什么需要依赖,谁注入了谁,注入了什么”: 

  • 谁依赖了谁:当然是应用程序依赖 IoC 容器;
  • 为什么需要依赖:应用程序需要 IoC 容器来提供对象需要的外部资源(包括对象、资源、常量数据);
  • 谁注入谁:很明显是 IoC 容器注入应用程序依赖的对象;
  • 注入了什么:注入某个对象所需的外部资源(包括对象、资源、常量数据)。

那么 IoC 和 DI 有什么关系?其实它们是同一个概念的不同角度描述,由于控制反转的概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护依赖关系),所以 2004 年大师级人物 Martin Fowler 又给出了一个新的名字:“依赖注入”,相对 IoC 而言,“依赖注入” 明确描述了被注入对象依赖 IoC 容器配置依赖对象

总的来说, 控制反转(Inversion of Control)是说创建对象的控制权发生转移,以前创建对象的主动权和创建时机由应用程序把控,而现在这种权利转交给 IoC 容器,它就是一个专门用来创建对象的工厂,你需要什么对象,它就给你什么对象。 有了 IoC 容器,依赖关系就改变了,原先的依赖关系就没了,它们都依赖 IoC 容器了,通过 IoC 容器来建立它们之间的关系。 

前面介绍了那么多的概念,现在我们来看一下未使用依赖注入框架和使用依赖注入框架之间有什么明显的区别。

4.1 未使用依赖注入框架

假设我们的服务 A 依赖于服务 B,即要使用服务 A 前,我们需要先创建服务 B。具体的流程如下图所示:

从上图可知,未使用依赖注入框架时,服务的使用者需要关心服务本身和其依赖的对象是如何创建的,且需要手动维护依赖关系。若服务本身需要依赖多个对象,这样就会增加使用难度和后期的维护成本。对于上述的问题,我们可以考虑引入依赖注入框架。下面我们来看一下引入依赖注入框架,整体流程会发生什么变化。

4.2 使用依赖注入框架

使用依赖注入框架之后,系统中的服务会统一注册到 IoC 容器中,如果服务有依赖其他服务时,也需要对依赖进行声明。当用户需要使用特定的服务时,IoC 容器会负责该服务及其依赖对象的创建与管理工作。具体的流程如下图所示:

到这里我们已经介绍了 IoC 与 DI 的概念及特点,接下来我们来介绍 DI 的应用。

五、DI 的应用

DI 在前端和服务端都有相应的应用,比如在前端领域的代表是 AngularJSAngular,而在服务端领域是 Node.js 生态中比较出名的 NestJS。接下来阿宝哥将简单介绍一下 DI 在 AngularJS/Angular 和 NestJS 中的应用。

5.1 DI 在 AngularJS 中的应用

在 AngularJS 中,依赖注入是其核心的特性之一。在 AngularJS 中声明依赖项有 3 种方式:

// 方式一: 使用 $inject annotation 方式
let fn = function (a, b) {};
fn.$inject = ['a', 'b'];

// 方式二: 使用 array-style annotations 方式
let fn = ['a', 'b', function (a, b) {}];

// 方式三: 使用隐式声明方式 
let fn = function (a, b) {}; // 不推荐

对于以上的代码,相信使用过 AngularJS 的小伙们都不会陌生。作为 AngularJS 核心功能特性的 DI 还是蛮强大的,但随着 AngularJS 的普及和应用的复杂度不断提高,AngularJS DI 系统的问题就暴露出来了。

这里阿宝哥简单介绍一下 AngularJS DI 系统存在的几个问题:

  • 内部缓存: AngularJS 应用程序中所有的依赖项都是单例,我们不能控制是否使用新的实例;
  • 命名空间冲突: 在系统中我们使用字符串来标识服务的名称,假设我们在项目中已有一个 CarService,然而第三方库中也引入了同样的服务,这样的话就容易出现混淆。

由于 AngularJS DI 存在以上的问题,所以在后续的 Angular 重新设计了新的 DI 系统。

5.2 DI 在 Angular 中的应用

以前面汽车的例子为例,我们可以把汽车、发动机、底盘和车身这些认为是一种 “服务”,所以它们会以服务提供者的形式注册到 DI 系统中。为了能区分不同服务,我们需要使用不同的令牌(Token)来标识它们。接着我们会基于已注册的服务提供者创建注入器对象。

之后,当我们需要获取指定服务时,我们就可以通过该服务对应的令牌,从注入器对象中获取令牌对应的依赖对象。上述的流程的具体如下图所示:

好的,了解完上述的流程。下面我们来看一下如何使用 Angular 内置的 DI 系统来 “造车”。

5.2.1 car.ts
// car.ts
import { Injectable, ReflectiveInjector } from '@angular/core';

// 配置Provider
@Injectable({
  providedIn: 'root',
})
export class Body {}

@Injectable({
  providedIn: 'root',
})
export class Chassis {}

@Injectable({
  providedIn: 'root',
})
export class Engine {
  start() {
    console.log('引擎发动了');
  }
}

@Injectable()
export default class Car {
  // 使用构造注入方式注入依赖对象
  constructor(
    private engine: Engine,
    private body: Body,
    private chassis: Chassis
  ) {}

  run() {
    this.engine.start();
  }
}

const injector = ReflectiveInjector.resolveAndCreate([
  Car,
  Engine,
  Chassis,
  Body,
]);

const car = injector.get(Car);
car.run();

在以上代码中我们调用 ReflectiveInjector 对象的 resolveAndCreate 方法手动创建注入器,然后根据车辆对应的 Token 来获取对应的依赖对象。通过观察上述代码,你可以发现,我们已经不需要手动地管理和维护依赖对象了,这些 “脏活”、“累活” 已经交给注入器来处理了。

此外,如果要能正常获取汽车对象,我们还需要在 app.module.ts 文件中声明 Car 对应 Provider,具体如下所示:

5.2.2 app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import Car, { Body, Chassis, Engine } from './car';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [{ provide: Car, deps: [Engine, Body, Chassis] }],
  bootstrap: [AppComponent],
})
export class AppModule {}

5.3 DI 在 NestJS 中的应用

NestJS 是构建高效,可扩展的 Node.js Web 应用程序的框架。 它使用现代的 JavaScript 或 TypeScript(保留与纯 JavaScript 的兼容性),并结合 OOP(面向对象编程),FP(函数式编程)和FRP(函数响应式编程)的元素。

在底层,Nest 使用了 Express,但也提供了与其他各种库的兼容,例如 Fastify,可以方便地使用各种可用的第三方插件。

近几年,由于 Node.js,JavaScript 已经成为 Web 前端和后端应用程序的「通用语言」,从而产生了像 AngularReactVue 等令人耳目一新的项目,这些项目提高了开发人员的生产力,使得可以快速构建可测试的且可扩展的前端应用程序。 然而,在服务器端,虽然有很多优秀的库、helper 和 Node 工具,但是它们都没有有效地解决主要问题 —— 架构。

NestJS 旨在提供一个开箱即用的应用程序体系结构,允许轻松创建高度可测试,可扩展,松散耦合且易于维护的应用程序。 在 NestJS 中也为我们开发者提供了依赖注入的功能,这里我们以官网的示例来演示一下依赖注入的功能。

5.3.1 app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}
5.3.2 app.controller.ts
import { Get, Controller, Render } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @Render('index')
  render() {
    const message = this.appService.getHello();
    return { message };
  }
}

在 AppController 中,我们通过构造注入的方式注入了 AppService 对象,当用户访问首页的时候,我们会调用 AppService 对象的 getHello 方法来获取 'Hello World!' 消息,并把消息返回给用户。当然为了保证依赖注入可以正常工作,我们还需要在 AppModule 中声明 providers 和 controllers,具体操作如下:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

其实 DI 并不是 AngularJS/Angular 和 NestJS 所特有的,如果你想在其他项目中使用 DI/IoC 的功能特性,阿宝哥推荐你使用 InversifyJS,它是一个可用于 JavaScript 和 Node.js 应用,功能强大、轻量的 IoC 容器。

InversifyJS 感兴趣的小伙伴可以自行了解一下,阿宝哥就不继续展开介绍了。接下来,我们将进入本文的重点,即介绍如何使用 TypeScript 实现一个简单的 IoC 容器,该容器实现的功能如下图所示:

六、手写 IoC 容器

为了让大家能更好地理解 IoC 容器的实现代码,阿宝哥来介绍一些相关的前置知识。

6.1 装饰器

如果你有使用过 Angular 或 NestJS,相信你对以下的代码不会陌生。

@Injectable()
export class HttpService {
  constructor(
    private httpClient: HttpClient
  ) {}
}

在以上代码中,我们使用了 Injectable 装饰器。该装饰器用于表示此类可以自动注入其依赖项。其中 @Injectable() 中的 @ 符号属于语法糖。

装饰器是一个包装类,函数或方法并为其添加行为的函数。这对于定义与对象关联的元数据很有用。装饰器有以下四种分类:

  • 类装饰器(Class decorators)
  • 属性装饰器(Property decorators)
  • 方法装饰器(Method decorators)
  • 参数装饰器(Parameter decorators)

前面示例中使用的 @Injectable() 装饰器,属于类装饰器。在该类装饰器修饰的 HttpService 类中,我们通过构造注入的方式注入了用于处理 HTTP 请求的 HttpClient 依赖对象。

6.2 反射

@Injectable()
export class HttpService {
  constructor(
    private httpClient: HttpClient
  ) {}
}

以上代码若设置编译的目标为 ES5,则会生成以下代码:

// 忽略__decorate函数等代码
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") 
      return Reflect.metadata(k, v);
};

var HttpService = /** @class */ (function () {
    function HttpService(httpClient) {
      this.httpClient = httpClient;
    }
    var _a;
    HttpService = __decorate([
        Injectable(),
        __metadata("design:paramtypes", [typeof (_a = typeof HttpClient !== "undefined" && HttpClient)
           === "function" ? _a : Object])
    ], HttpService);
    return HttpService;
}());

通过观察上述代码,你会发现 HttpService 构造函数中 httpClient 参数的类型被擦除了,这是因为 JavaScript 是弱类型语言。那么如何在运行时,保证注入正确类型的依赖对象呢?这里 TypeScript 使用 reflect-metadata 这个第三方库来存储额外的类型信息。

reflect-metadata 这个库提供了很多 API 用于操作元信息,这里我们只简单介绍几个常用的 API:

// define metadata on an object or property
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

// check for presence of a metadata key on the prototype chain of an object or property
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);

// get metadata value of a metadata key on the prototype chain of an object or property
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);

// delete metadata from an object or property
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);

// apply metadata via a decorator to a constructor
@Reflect.metadata(metadataKey, metadataValue)
class C {
  // apply metadata via a decorator to a method (property)
  @Reflect.metadata(metadataKey, metadataValue)
  method() {
  }
}

对于上述的 API 只需简单了解一下即可。在后续的内容中,我们将介绍具体如何使用。这里我们需要注意以下两个问题:

  • 对于类或函数,我们需要使用装饰器来修饰它们,这样才能保存元数据。
  • 只有类、枚举或原始数据类型能被记录。接口和联合类型作为 “对象” 出现。这是因为这些类型在编译后完全消失,而类却一直存在。

6.3 定义 Token 和 Provider

了解完装饰器与反射相关的基础知识,接下来我们来开始实现 IoC 容器。我们的 IoC 容器将使用两个主要的概念:令牌(Token)和提供者(Provider)。令牌是 IoC 容器所要创建对象的标识符,而提供者用于描述如何创建这些对象。

IoC 容器最小的公共接口如下所示:

export class Container {
  addProvider<T>(provider: Provider<T>) {} // TODO
  inject<T>(type: Token<T>): T {} // TODO
}

接下来我们先来定义 Token:

// type.ts
interface Type<T> extends Function {
  new (...args: any[]): T;
}

// provider.ts
class InjectionToken {
  constructor(public injectionIdentifier: string) {}
}

type Token<T> = Type<T> | InjectionToken;

Token 类型是一个联合类型,既可以是一个函数类型也可以是 InjectionToken 类型。AngularJS 中使用字符串作为 Token,在某些情况下,可能会导致冲突。因此,为了解决这个问题,我们定义了 InjectionToken 类,来避免出现命名冲突问题。

定义完 Token 类型,接下来我们来定义三种不同类型的 Provider:

  • ClassProvider:提供一个类,用于创建依赖对象;
  • ValueProvider:提供一个已存在的值,作为依赖对象;
  • FactoryProvider:提供一个工厂方法,用于创建依赖对象。
// provider.ts
export type Factory<T> = () => T;

export interface BaseProvider<T> {
  provide: Token<T>;
}

export interface ClassProvider<T> extends BaseProvider<T> {
  provide: Token<T>;
  useClass: Type<T>;
}

export interface ValueProvider<T> extends BaseProvider<T> {
  provide: Token<T>;
  useValue: T;
}

export interface FactoryProvider<T> extends BaseProvider<T> {
  provide: Token<T>;
  useFactory: Factory<T>;
}

export type Provider<T> =
  | ClassProvider<T>
  | ValueProvider<T>
  | FactoryProvider<T>;

为了更方便的区分这三种不同类型的 Provider,我们自定义了三个类型守卫函数:

// provider.ts
export function isClassProvider<T>(
  provider: BaseProvider<T>
): provider is ClassProvider<T> {
  return (provider as any).useClass !== undefined;
}

export function isValueProvider<T>(
  provider: BaseProvider<T>
): provider is ValueProvider<T> {
  return (provider as any).useValue !== undefined;
}

export function isFactoryProvider<T>(
  provider: BaseProvider<T>
): provider is FactoryProvider<T> {
  return (provider as any).useFactory !== undefined;
}

6.4 定义装饰器

在前面我们已经提过了,对于类或函数,我们需要使用装饰器来修饰它们,这样才能保存元数据。因此,接下来我们来分别创建 InjectableInject 装饰器。

6.4.1 Injectable 装饰器

Injectable 装饰器用于表示此类可以自动注入其依赖项,该装饰器属于类装饰器。在 TypeScript 中,类装饰器的声明如下:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) 
  => TFunction | void;

类装饰器顾名思义,就是用来装饰类的。它接收一个参数:target: TFunction,表示被装饰的类。下面我们来看一下 Injectable 装饰器的具体实现:

// Injectable.ts
import { Type } from "./type";
import "reflect-metadata";

const INJECTABLE_METADATA_KEY = Symbol("INJECTABLE_KEY");

export function Injectable() {
  return function(target: any) {
    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
    return target;
  };
}

在以上代码中,当调用完 Injectable 函数之后,会返回一个新的函数。在新的函数中,我们使用 reflect-metadata 这个库提供的 defineMetadata API 来保存元信息,其中 defineMetadata API 的使用方式如下所示:

// define metadata on an object or property
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

Injectable 类装饰器使用方式也简单,只需要在被装饰类的上方使用 @Injectable() 语法糖就可以应用该装饰器:

@Injectable()
export class HttpService {
  constructor(
    private httpClient: HttpClient
  ) {}
}

在以上示例中,我们注入的是 Type 类型的 HttpClient 对象。但在实际的项目中,往往会比较复杂。除了需要注入 Type 类型的依赖对象之外,我们还可能会注入其他类型的依赖对象,比如我们希望在 HttpService 服务中注入远程服务器的 API 地址。针对这种情形,我们需要使用 Inject 装饰器。

6.4.2 Inject 装饰器

接下来我们来创建 Inject 装饰器,该装饰器属于参数装饰器。在 TypeScript 中,参数装饰器的声明如下:

declare type ParameterDecorator = (target: Object, 
  propertyKey: string | symbol, parameterIndex: number ) => void

参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:

  • target: Object —— 被装饰的类;
  • propertyKey: string | symbol —— 方法名;
  • parameterIndex: number —— 方法中参数的索引值。

下面我们来看一下 Inject 装饰器的具体实现:

// Inject.ts
import { Token } from './provider';
import 'reflect-metadata';

const INJECT_METADATA_KEY = Symbol('INJECT_KEY');

export function Inject(token: Token<any>) {
  return function(target: any, _: string | symbol, index: number) {
    Reflect.defineMetadata(INJECT_METADATA_KEY, token, target, `index-${index}`);
    return target;
  };
}

在以上代码中,当调用完 Inject 函数之后,会返回一个新的函数。在新的函数中,我们使用 reflect-metadata 这个库提供的 defineMetadata API 来保存参数相关的元信息。这里是保存 index 索引信息和 Token 信息。

定义完 Inject 装饰器,我们就可以利用它来注入我们前面所提到的远程服务器的 API 地址,具体的使用方式如下:

const API_URL = new InjectionToken('apiUrl');

@Injectable()
export class HttpService {
  constructor(
    private httpClient: HttpClient,
    @Inject(API_URL) private apiUrl: string
  ) {}
}

6.5 实现 IoC 容器

目前为止,我们已经定义了 Token、Provider、Injectable 和 Inject 装饰器。接下来我们来实现前面所提到的 IoC 容器的 API:

export class Container {
  addProvider<T>(provider: Provider<T>) {} // TODO
  inject<T>(type: Token<T>): T {} // TODO
}
6.5.1 实现 addProvider 方法

addProvider() 方法的实现很简单,我们使用 Map 来存储 Token 与 Provider 之间的关系:

export class Container {
  private providers = new Map<Token<any>, Provider<any>>();

  addProvider<T>(provider: Provider<T>) {
    this.assertInjectableIfClassProvider(provider);
    this.providers.set(provider.provide, provider);
  }
}

在 addProvider() 方法内部除了把 Token 与 Provider 的对应信息保存到 providers 对象中之外,我们定义了一个 assertInjectableIfClassProvider 方法,用于确保添加的 ClassProvider 是可注入的。该方法的具体实现如下:

private assertInjectableIfClassProvider<T>(provider: Provider<T>) {
  if (isClassProvider(provider) && !isInjectable(provider.useClass)) {
    throw new Error(
        `Cannot provide ${this.getTokenName(
          provider.provide
     )} using class ${this.getTokenName(
          provider.useClass
     )}, ${this.getTokenName(provider.useClass)} isn't injectable`
   );
  }
}

在 assertInjectableIfClassProvider 方法体中,我们使用了前面已经介绍的 isClassProvider 类型守卫函数来判断是否为 ClassProvider,如果是的话,会判断该 ClassProvider 是否为可注入的,具体使用的是 isInjectable 函数,该函数的定义如下:

export function isInjectable<T>(target: Type<T>) {
  return Reflect.getMetadata(INJECTABLE_METADATA_KEY, target) === true;
}

在 isInjectable 函数中,我们使用 reflect-metadata 这个库提供的 getMetadata API 来获取保存在类中的元信息。为了更好地理解以上代码,我们来回顾一下前面 Injectable 装饰器:

const INJECTABLE_METADATA_KEY = Symbol("INJECTABLE_KEY");

export function Injectable() {
  return function(target: any) {
    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
    return target;
  };
}

如果添加的 Provider 是 ClassProvider,但 Provider 对应的类是不可注入的,则会抛出异常。为了让异常消息更加友好,也更加直观。我们定义了一个 getTokenName 方法来获取 Token 对应的名称:

private getTokenName<T>(token: Token<T>) {
  return token instanceof InjectionToken
    ? token.injectionIdentifier
    : token.name;
}

现在我们已经实现了 Container 类的 addProvider 方法,这时我们就可以使用它来添加三种不同类型的 Provider:

const container = new Container();
const input = { x: 200 };

class BasicClass {}
// 注册ClassProvider
container.addProvider({ provide: BasicClass, useClass:  BasicClass});
// 注册ValueProvider
container.addProvider({ provide: BasicClass, useValue: input });
// 注册FactoryProvider
container.addProvider({ provide: BasicClass, useFactory: () => input });

需要注意的是,以上示例中注册三种不同类型的 Provider 使用的是同一个 Token 仅是为了演示而已。下面我们来实现 Container 类中核心的 inject 方法。

6.5.2 实现 inject 方法

在看 inject 方法的具体实现之前,我们先来看一下该方法所实现的功能:

const container = new Container();
const input = { x: 200 };

container.addProvider({ provide: BasicClass, useValue: input });
const output = container.inject(BasicClass);
expect(input).toBe(output); // true

观察以上的测试用例可知,Container 类中 inject 方法所实现的功能就是根据 Token 获取与之对应的对象。在前面实现的 addProvider 方法中,我们把 Token 和该 Token 对应的 Provider 保存在 providers Map 对象中。所以在 inject 方法中,我们可以先从 providers 对象中获取该 Token 对应的 Provider 对象,然后在根据不同类型的 Provider 来获取其对应的对象。

好的,下面我们来看一下 inject 方法的具体实现:

inject<T>(type: Token<T>): T {
  let provider = this.providers.get(type);
  // 处理使用Injectable装饰器修饰的类
  if (provider === undefined && !(type instanceof InjectionToken)) {
    provider = { provide: type, useClass: type };
    this.assertInjectableIfClassProvider(provider);
  }
  return this.injectWithProvider(type, provider);
}

在以上代码中,除了处理正常的流程之外。我们还处理一个特殊的场景,即没有使用 addProvider 方法注册 Provider,而是使用 Injectable 装饰器来装饰某个类。对于这个特殊场景,我们会根据传入的 type 参数来创建一个 provider 对象,然后进一步调用 injectWithProvider 方法来创建对象,该方法的具体实现如下:

private injectWithProvider<T>(type: Token<T>, provider?: Provider<T>): T {
  if (provider === undefined) {
    throw new Error(`No provider for type ${this.getTokenName(type)}`);
  }
  if (isClassProvider(provider)) {
    return this.injectClass(provider as ClassProvider<T>);
  } else if (isValueProvider(provider)) {
    return this.injectValue(provider as ValueProvider<T>);
  } else {
    return this.injectFactory(provider as FactoryProvider<T>);
  }
 }

injectWithProvider 方法内部,我们会使用前面定义的用于区分三种不同类型 Provider 的类型守卫函数来处理不同的 Provider。这里我们先来看一下最简单 ValueProvider,当发现注入的是 ValueProvider 类型时,则会调用 injectValue 方法来获取其对应的对象:

// { provide: API_URL, useValue: 'https://www.semlinker.com/' }
private injectValue<T>(valueProvider: ValueProvider<T>): T {
  return valueProvider.useValue;
}

接着我们来看如何处理 FactoryProvider 类型的 Provider,如果发现是 FactoryProvider 类型时,则会调用 injectFactory 方法来获取其对应的对象,该方法的实现也很简单:

// const input = { x: 200 };
// container.addProvider({ provide: BasicClass, useFactory: () => input });
private injectFactory<T>(valueProvider: FactoryProvider<T>): T {
  return valueProvider.useFactory();
}

最后我们来分析一下如何处理 ClassProvider,对于 ClassProvider 类说,通过 Provider 对象的 useClass 属性,我们就可以直接获取到类对应的构造函数。最简单的情形是该类没有依赖其他对象,但在大多数场景下,即将实例化的服务类是会依赖其他的对象的。所以在实例化服务类前,我们需要构造其依赖的对象。

那么现在问题来了,怎么获取类所依赖的对象呢?我们先来分析一下以下代码:

const API_URL = new InjectionToken('apiUrl');

@Injectable()
export class HttpService {
  constructor(
    private httpClient: HttpClient,
    @Inject(API_URL) private apiUrl: string
  ) {}
}

以上代码若设置编译的目标为 ES5,则会生成以下代码:

// 已省略__decorate函数的定义
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};

var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};

var HttpService = /** @class */ (function () {
    function HttpService(httpClient, apiUrl) {
        this.httpClient = httpClient;
        this.apiUrl = apiUrl;
    }
    var _a;
    HttpService = __decorate([
        Injectable(),
        __param(1, Inject(API_URL)),
        __metadata("design:paramtypes", [typeof (_a = typeof HttpClient !== "undefined" && HttpClient) 
          === "function" ? _a : Object, String])
    ], HttpService);
    return HttpService;
}());

观察以上的代码会不会觉得有点晕?不要着急,阿宝哥会逐一分析 HttpService 中的两个参数。首先我们先来分析 apiUrl 参数:

在图中我们可以很清楚地看到,API_URL 对应的 Token 最终会通过 Reflect.defineMetadata API 进行保存,所使用的 Key 是 Symbol('INJECT_KEY')。而对于另一个参数即 httpClient,它使用的 Key 是 "design:paramtypes",它用于修饰目标对象方法的参数类型。

除了 "design:paramtypes" 之外,还有其他的 metadataKey,比如 design:typedesign:returntype,它们分别用于修饰目标对象的类型和修饰目标对象方法返回值的类型。

由上图可知,HttpService 构造函数的参数类型最终会使用 Reflect.metadata API 进行存储。了解完上述的知识,接下来我们来定义一个 getInjectedParams 方法,用于获取类构造函数中声明的依赖对象,该方法的具体实现如下:

type InjectableParam = Type<any>;
const REFLECT_PARAMS = "design:paramtypes";

private getInjectedParams<T>(target: Type<T>) {
  // 获取参数的类型
  const argTypes = Reflect.getMetadata(REFLECT_PARAMS, target) as (
      | InjectableParam
      | undefined
  )[];
  if (argTypes === undefined) {
      return [];
  }
  return argTypes.map((argType, index) => {
    // The reflect-metadata API fails on circular dependencies, and will return undefined
    // for the argument instead.
    if (argType === undefined) {
      throw new Error(
        `Injection error. Recursive dependency detected in constructor for type ${target.name} 
           with parameter at index ${index}`
      );
    }
    const overrideToken = getInjectionToken(target, index);
    const actualToken = overrideToken === undefined ? argType : overrideToken;
    let provider = this.providers.get(actualToken);
    return this.injectWithProvider(actualToken, provider);
  });
}

因为我们的 Token 的类型是 Type<T> | InjectionToken 联合类型,所以在 getInjectedParams 方法中我们也要考虑 InjectionToken 的情形,因此我们定义了一个 getInjectionToken 方法来获取使用 @Inject 装饰器注册的 Token,该方法的实现很简单:

export function getInjectionToken(target: any, index: number) {
  return Reflect.getMetadata(INJECT_METADATA_KEY, target, `index-${index}`) as Token<any> | undefined;
}

现在我们已经可以获取类构造函数中所依赖的对象,基于前面定义的 getInjectedParams 方法,我们就来定义一个 injectClass 方法,用来实例化 ClassProvider 所注册的类。

// { provide: HttpClient, useClass: HttpClient }
private injectClass<T>(classProvider: ClassProvider<T>): T {
  const target = classProvider.useClass;
  const params = this.getInjectedParams(target);
  return Reflect.construct(target, params);
}

这时 IoC 容器中定义的两个方法都已经实现了,我们来看一下 IoC 容器的完整代码:

// container.ts
type InjectableParam = Type<any>;

const REFLECT_PARAMS = "design:paramtypes";

export class Container {
  private providers = new Map<Token<any>, Provider<any>>();

  addProvider<T>(provider: Provider<T>) {
    this.assertInjectableIfClassProvider(provider);
    this.providers.set(provider.provide, provider);
  }

  inject<T>(type: Token<T>): T {
    let provider = this.providers.get(type);
    if (provider === undefined && !(type instanceof InjectionToken)) {
      provider = { provide: type, useClass: type };
      this.assertInjectableIfClassProvider(provider);
    }
    return this.injectWithProvider(type, provider);
  }

  private injectWithProvider<T>(type: Token<T>, provider?: Provider<T>): T {
    if (provider === undefined) {
      throw new Error(`No provider for type ${this.getTokenName(type)}`);
    }
    if (isClassProvider(provider)) {
      return this.injectClass(provider as ClassProvider<T>);
    } else if (isValueProvider(provider)) {
      return this.injectValue(provider as ValueProvider<T>);
    } else {
      // Factory provider by process of elimination
      return this.injectFactory(provider as FactoryProvider<T>);
    }
  }

  private assertInjectableIfClassProvider<T>(provider: Provider<T>) {
    if (isClassProvider(provider) && !isInjectable(provider.useClass)) {
      throw new Error(
        `Cannot provide ${this.getTokenName(
          provider.provide
        )} using class ${this.getTokenName(
          provider.useClass
        )}, ${this.getTokenName(provider.useClass)} isn't injectable`
      );
    }
  }

  private injectClass<T>(classProvider: ClassProvider<T>): T {
    const target = classProvider.useClass;
    const params = this.getInjectedParams(target);
    return Reflect.construct(target, params);
  }

  private injectValue<T>(valueProvider: ValueProvider<T>): T {
    return valueProvider.useValue;
  }

  private injectFactory<T>(valueProvider: FactoryProvider<T>): T {
    return valueProvider.useFactory();
  }

  private getInjectedParams<T>(target: Type<T>) {
    const argTypes = Reflect.getMetadata(REFLECT_PARAMS, target) as (
      | InjectableParam
      | undefined
    )[];
    if (argTypes === undefined) {
      return [];
    }
    return argTypes.map((argType, index) => {
      // The reflect-metadata API fails on circular dependencies, and will return undefined
      // for the argument instead.
      if (argType === undefined) {
        throw new Error(
          `Injection error. Recursive dependency detected in constructor for type ${target.name} 
             with parameter at index ${index}`
        );
      }
      const overrideToken = getInjectionToken(target, index);
      const actualToken = overrideToken === undefined ? argType : overrideToken;
      let provider = this.providers.get(actualToken);
      return this.injectWithProvider(actualToken, provider);
    });
  }

  private getTokenName<T>(token: Token<T>) {
    return token instanceof InjectionToken
      ? token.injectionIdentifier
      : token.name;
  }
}

最后我们来简单测试一下我们前面开发的 IoC 容器,具体的测试代码如下所示:

// container.test.ts
import { Container } from "./container";
import { Injectable } from "./injectable";
import { Inject } from "./inject";
import { InjectionToken } from "./provider";

const API_URL = new InjectionToken("apiUrl");

@Injectable()
class HttpClient {}

@Injectable()
class HttpService {
  constructor(
    private httpClient: HttpClient,
    @Inject(API_URL) private apiUrl: string
  ) {}
}

const container = new Container();

container.addProvider({
  provide: API_URL,
  useValue: "https://www.semlinker.com/",
});

container.addProvider({ provide: HttpClient, useClass: HttpClient });
container.addProvider({ provide: HttpService, useClass: HttpService });

const httpService = container.inject(HttpService);
console.dir(httpService);

以上代码成功运行后,控制台会输出以下结果:

HttpService {
  httpClient: HttpClient {},
  apiUrl: 'https://www.semlinker.com/' }

很明显该结果正是我们所期望的,这表示我们 IoC 容器已经可以正常工作了。当然在实际项目中,一个成熟的 IoC 容器还要考虑很多东西,如果小伙伴想在项目中使用的话,阿宝哥建议可以考虑使用 InversifyJS 这个库。

若需要获取完整 IoC 容器源码的话,可在 全栈修仙之路 公众号回复 ioc 关键字,即可获取。

七、参考资源

八、推荐阅读

查看原文

赞 25 收藏 15 评论 2

麦田348462402 赞了文章 · 8月20日

使用graphite和grafana进行应用程序监控

本文仅涉及报警数据存储盒展现部分,关于数据收集部分,请参阅【监控系统】使用collect进行进程监控; 关于报警服务部分,请参阅【监控系统】配合Graphite使用的报警系统

graphite+grafana 介绍

grafana,按照官方的说法是 Beautiful metric & analytic dashboards。grafana 负责数据的展示,可以配置各种不同的数据源,其中包括 graphite。

graphite 包含多个模块,这里我们使用的模块包括:

  • Whisper:固定大小的数据库,存储方式类似RRD (round-robin-database),用来存储收集到的 metrics

  • Carbon:metrics 接收服务,接收到 metrics 以后调用 Whisper 进行存储

  • graphite-api:WSGI webapp 接口服务,grafana 在需要展现数据的时候使用其提供的 REST API 进行数据的获取

本文的搭建的监控系统结构如下:

图片描述

在本文档中,我们会尽量将相关文件安装在/opt/graphite目录

准备Python 2.7 环境

对于某些默认Python环境不是2.7的系统,需要安装Python2.7。

从源码编译Python2.7

configure
make
make install

创建Python2.7的virtualenv环境

virtualenv /opt/graphite --python=/usr/local/bin/python

加载virtualenv环境

source /opt/graphite/bin/activate

安装Carbon+Whisper

pip install carbon --install-option="--prefix=/opt/graphite" --install-option="--install-lib=/opt/graphite/lib"
pip install whisper

使用默认的配置文件:

cp /opt/graphite/conf/storage-schemas.conf.example /opt/graphite/conf/storage-schemas.conf
cp /opt/graphite/conf/carbon.conf.example /opt/graphite/conf/carbon.conf

启动 Carbon

/opt/graphite/bin/carbon-cache.py start

carbon的文件目录在配置文件 /opt/graphite/conf/carbon.conf 中进行定义,下面是默认的配置:

LOCAL_DATA_DIR = /opt/graphite/storage/whisper/

安装graphite-api

yum install libffi-devel
pip install graphite-api --install-option="--prefix=/opt/graphite"

使用nginx+uwsgi的方式部署graphite-api

首先安装uwsgi

pip install uwsgi

创建graphite-api的配置文件:/opt/graphite/etc/graphite-api.yml

search_index: /opt/graphite/index
finders:
 - graphite_api.finders.whisper.WhisperFinder
functions:
 - graphite_api.functions.SeriesFunctions
 - graphite_api.functions.PieFunctions
whisper:
 directories:
 - /opt/graphite/storage/whisper
carbon:
 hosts:
 - 127.0.0.1:7002
 timeout: 1
 retry_delay: 15
 carbon_prefix: carbon
 replication_factor: 1

在这个配置文件中,whisper的数据路径配置为/opt/graphite/storage/whisper,这个是在carbon配置文件 /opt/graphite/conf/carbon.conf 中使用配置项LOCAL_DATA_DIR进行定义的。

centos中没有uwsgi的package,需要自行下载相关的启动脚本,这里使用 https://github.com/jgoldschrafe/rpm-uwsgi

wget -O /etc/init.d/uwsgi https://raw.githubusercontent.com/jgoldschrafe/rpm-uwsgi/master/SOURCES/uwsgi.init
chmod +x /etc/init.d/uwsgi

编辑文件 /etc/init.d/uwsgi 进行目录配置

uwsgi="/opt/graphite/bin/uwsgi"
prog=$(basename "$uwsgi")
UWSGI_CONF_DIR="/etc/uwsgi"
UWSGI_LOG_DIR="/var/log/uwsgi"
PIDFILE_DIR="/var/run/uwsgi"

创建文件/etc/uwsgi/graphite-api.ini

[uwsgi]
processes = 2
socket = 0.0.0.0:5000
module = graphite_api.app:app
home = /opt/graphite
env = GRAPHITE_API_CONFIG=/opt/graphite/conf/graphite-api.yml

启动uwsgi

service uwsgi start

启动以后会监听5000端口,注意这里5000端口是uwsgi协议,需要后面配置nginx进行代理。

server {
    listen 81;
    location / {
        include uwsgi_params;
        uwsgi_pass localhost:5000;
    }
}

nginx启动后可以访问洗面的链接检查数据返回是否正常

http://127.0.0.1:81/render?target=test.metric

向监控系统中写入数据

可以使用多种个方式向监控系统中写入数据,例如 dropwizard metrics。这里为了方便使用Python进行数据上报:

import socket
import time
CARBON_SERVER = 'xxx.xxx.xxx.xxx'
CARBON_PORT = 2003

for k in xrange(100000):
    sock = socket.socket()
    sock.connect((CARBON_SERVER, CARBON_PORT))
    message = "test.meter.qps %d %d\n" % (k % 10, int(time.time()))
    print message
    sock.sendall(message)
    sock.close()
    time.sleep(5)

程序运行以后可以在carbon的数据目录中会发现如下的文件结构:

test
  |-- metric.wsp
  |-- meter
  |     |-- qps.wsp

配置grafana展现metric

首先配置grafana使用graphite作为数据源

图片描述

配置数据显示:

图片描述

历史数据处理

对于监控系统,长期运行以后必然会积攒大量的历史数据,whisper 通过配置数据保存的时间段以及在时间短内每间隔多长时间保存一条数据来解决历史数据问题。

配置文件 /opt/graphite/conf/storage-schemas.conf 中描述了上述信息:

[default]
pattern = .*
retentions = 1m:30d,1h:1y

这个配置中,30天内的数据每间隔1分钟保存一条数据,30天-1年之间的数据,每个小时保存一条数据。

由于 whisper 是一个固定大小的数据库,所以当 storage-schemas.conf 设定以后,一个metrics所占的磁盘空间就已经确定了。在系统运行的周期中只要根据 metrics 的增加进行适当扩容即可。

注意:storage-schemas.conf修改以后对于已经在磁盘上进行记录的Metrics不会生效,需要删除数据重新写入或者进行数据迁移才行。

查看原文

赞 3 收藏 5 评论 0

麦田348462402 赞了文章 · 8月20日

系统之眼!Linux系统性能监控工具Glances

一、Glances介绍

glances是一个基于python语言开发,可以为linux或者UNIX性能提供监视和分析性能数据的功能。glances在用户的终端上显示重要的系统信息,并动态的进行更新,让管理员实时掌握系统资源的使用情况,而动态监控并不会消耗大量的系统资源,比如CPU资源,通常消耗小于2%,glances默认每两秒更新一次数据。同时glances还可以将相同的数据捕获到一个文件,便于以后对报告进行分析和图形绘制,支持的文件格式有.csv电子表格格式和和html格式。

github地址:https://github.com/nicolargo/...

glances工具的功能如下:

  • CPU使用率
  • 内存使用率
  • 内核统计信息和运行队列信息
  • 磁盘I/O速度、传输和读/写比率
  • 磁盘适配器
  • 网络I/O速度、传输和读/写比率
  • 页面监控
  • 进程监控-消耗资源最多的进程
  • 计算机信息和系统资源

效果图

二、glances安装方式

  • 源码安装
  • pip命令安装
  • yum安装

源码安装比较复杂,可能会遇到依赖问题不好解决;pip安装和yum安装都可以选择。文档手册:https://glances.readthedocs.i...

三、安装glances

Glances 一般已集成到大多数 Linux 发行版的官方软件源中,可以直接使用系统的包管理器(如 apt-get、yum)安装:

sudo apt-get install glances
yum install epel* -yyum -y install glances

当然也可以使用 Python 的包管理器(pip 命令)进行安装:

pip install glances

温度监控工具安装

lm_sensors 的软件可以帮助我们来监控主板、CPU 的工作电压、风扇转速、温度等数据。这些数据我们通常在主板的 BIOS 也可以看到。当我们可以在机器运行的时候通过 lm_sensors 随时来监测着 CPU 的温度变化,可以预防呵保护因为 CPU 过热而会烧掉。lm_sensors 软件监测到的数据可以被 glances 调用并且显示_。_

yum -y install lm_sensors

其实,这些开源软件的安装非常简单,基本100%是傻瓜式的,没有什么太大的问题。

四、Glances的使用


Glances 有 4 种颜色标记,分别表示不同的紧急程度:

  • 绿色:OK
  • 蓝色:CAREFUL
  • 紫色:WARNING
  • 红色:CRITICAL
绿色表示性能良好;(此时 CPU 使用率、磁盘空间使用率和内存使用率低于 50%,系统负载低于 0.7)。

蓝色表示系统性能有一些小问题,用户应当开始关注系统性能;(此时 CPU 使用率、磁盘空间使用率和内存使用率在 50%-70% 之间,系统负载在 0.7-1 之间)。

品红表示性能报警,应当采取措施比如备份数据;(此时 CPU 使用率、磁盘空间使用率和内存使用率在 70%-90% 之间,,系统负载在 1-5 之间)。

红色表示性能问题严重,可能宕机;(此时 CPU 使用率、磁盘空间使用率和内存使用率在大于 90%,系统负载大于 5)。

glances 使用方法


glances 是一个命令行工具包括如下命令选项:

  • -b:显示网络连接速度 Byte/ 秒
  • -B @IP|host :绑定服务器端 IP 地址或者主机名称
  • -c @IP|host:连接 glances 服务器端
  • -C file:设置配置文件默认是 /etc/glances/glances.conf
  • -d:关闭磁盘 I/O 模块
  • -e:显示传感器温度
  • -f file:设置输出文件(格式是 HTML 或者 CSV)
  • -m:关闭挂载的磁盘模块
  • -n:关闭网络模块
  • -p PORT:设置运行端口默认是 61209
  • -P password:设置客户端 / 服务器密码
  • -s:设置 glances 运行模式为服务器
  • -t sec:设置屏幕刷新的时间间隔,单位为秒,默认值为 2 秒,数值范围:1~32767
  • -h : 显示帮助信息
  • -v : 显示版本信息

glances 工作界面的说明 :
在图中 的上部是 CPU 、Load(负载)、Mem(内存使用)、 Swap(交换分区)的使用情况。在图中的中上部是网络接口、Processes(进程)的使用情况。通常包括如下字段:

  • VIRT: 虚拟内存大小
  • RES: 进程占用的物理内存值
  • %CPU:该进程占用的 CPU 使用率
  • %MEM:该进程占用的物理内存和总内存的百分比
  • PID: 进程 ID 号
  • USER: 进程所有者的用户名
  • TIME+: 该进程启动后占用的总的 CPU 时间
  • IO_R 和 IO_W: 进程的读写 I/O 速率
  • NAME: 进程名称
  • NI: 进程优先级
  • S: 进程状态,其中 S 表示休眠,R 表示正在运行,Z 表示僵死状态。

另外 glances 可以使用交互式的方式运行该工具,用户可以使用如下快捷键:

  • h :显示帮助信息
  • q :离开程序退出
  • c :按照 CPU 实时负载对系统进程进行排序
  • m :按照内存使用状况对系统进程排序
  • i:按照 I/O 使用状况对系统进程排序
  • p:按照进程名称排序
  • d :显示磁盘读写状况
  • w :删除日志文件
  • l :显示日志
  • s:显示传感器信息
  • f :显示系统信息
  • 1 :轮流显示每个 CPU 内核的使用情况

五、Glances的C/S模式

glances还支持C/S模式监控,被监控机运行服务端,监控端运行客户端既可以实现远程监控,两端都安装Glances服务即可。

服务端启动

服务端使用的端口默认是61209,启动命令如下:

glances -s -B 192.168.1.253glances server is running on 192.168.1.253:61209

客户端访问

glances -c 192.168.1.253

也可以使用用户名与密码进行访问,默认用户名是glances,如果想更改的话可以使用--username进行更换。下面是官方的原文:

In client/server mode, limits are set by the server side.

You can set a password to access to the server using the --password. By default, the username is glances but you can change it with --username.

glances -s --usernameDefine the Glances server username: testDefine the Glances server password (test username):Password (confirm):Do you want to save the password? [Yes/No]: YesGlances XML-RPC server is running on 0.0.0.0:61209

客户端连接方式如下:

glances -c 192.168.1.253 --username test

六、Glances的WebServer 模式


在 glances 的 WebServer 模式下,客户端只通过浏览器访问就可以获取远程服务器的运行状态。只需要安装 Python 的 Bottle 模块:

pip install bottle

安装成功后,使用glances -w命令即可开启 WebServer 模式。
客户端使用浏览器访问http://SERVER_IP:61208/进入监控界面。

它的WEB模式还可以在手机上看,如下图。

来源官方文档

七、其它高级应用

导出数据为CSV格式,命令如下:

glances --export-csv /tmp/1.csv

glances还可以与InfluxDB、Grafana这些开源软件一起配合,构建成一个监控平台,和其它监控软件一样,能形成实时化、图表化的数据显示。

感兴趣的朋友可以自己在测试环境玩玩,一个很好的,功能强大的工具。

你有什么好用的性能工具?

欢迎留言分享一起交流下!

查看原文

赞 46 收藏 28 评论 0

麦田348462402 赞了文章 · 7月22日

PHP性能被动分析工具之xhgui加tideways的安装实践

PHP性能被动分析工具之xhgui加tideways的安装实践

By:0x584A Date:2016-11-23 17:55:42

前言

最近一直想做个接口性能分析,但是手打log实在能把人给累死。怎么办呢?想到之前有写过一篇我所知道的PHP相关调优汇总,里面有一个Xdebug + kcachegrind的调优方式。 但是呢,每次都需要将它产生的cachegrind.out.*文件下到本地,再用kcachegrind打开做分析,而且体验感也不是特别好(原谅我英语不过三的渣渣...)

性能分析的UI组合

  • uprofiler点击下载
  • xhprof + xhprof.io 【因年久失修,用uprofiler替换即可或用修复板。支持保存分析数据至mysql,函数调用记录完整,内核级别函数都能显示,支持域名索引】修复板下载
  • xhprof or uprofiler or tideways + xhgui 【推荐,保存数据至MongoDB,UI界面友好,有中文UI版,不支持域名索引对于线上调试支持较差】中文版下载英文原版下载
  • tideways【推荐,这个最绚的而且一直在持续维护。但是使用它酷炫的UI需要付费,扩展则不需要。】tideways下载地址

安装

  • 环境

    • tideways + xhgui
    • php>5.5
    • mongodb
    • php5-mcrypt
    • apt-get install libcurl4-openssl-dev libpcre3-dev
  1. 安装mongodb

    前置需安装php-dev`sudo apt-get install php5-dev`
    
    ```shell
    $ sudo pecl install mongodb
     $ cd /etc/php5/mods-available
     $ sudo sh -c "echo 'extension=mongodb.so' > /etc/php5/mods-available/mongodb.ini"
     [sudo] 0x584A 的密码:
     $ cd ../fpm/conf.d
     $ sudo ln -s ../../mods-available/mongodb.ini 20-mongodb.ini
     $ sudo service php5-fpm restart
     $ sudo apt-get install mongodb -y
    ```
    
  2. 安装xhgui

    $ git clone https://github.com/maxincai/xhgui.git
     $ cd xhgui
     $ php install.php
    加索引
    
    $ mongo
     > use xhprof
     > db.results.ensureIndex( { 'meta.SERVER.REQUEST_TIME' : -1 } )
     > db.results.ensureIndex( { 'profile.main().wt' : -1 } )
     > db.results.ensureIndex( { 'profile.main().mu' : -1 } )
     > db.results.ensureIndex( { 'profile.main().cpu' : -1 } )
     > db.results.ensureIndex( { 'meta.url' : 1 } )
  3. 安装tideways

    见该安装地址,选择系统安装方式`https://tideways.io/profiler/docs/setup/installation`
    
    • 需要在nginx应用中加入fastcgi_param TIDEWAYS_SAMPLERATE "25";
    • 需要在nginx应用中加入fastcgi_param PHP_VALUE "auto_prepend_file=/home/0x584A/www/xhgui/external/header.php";
  4. ngxin应用配置

    应用
    server {
        listen 127.0.10.1:80;
        server_name  app.com;
        root   /home/0x584A/www/app;
        index  index.html index.htm index.php;
    
        location / {
            if (!-e $request_filename) {
                rewrite ^(.*)$ /index.php?$1 last ;
                break;
            }
        }
    
        location ~ ^(.+\.php)(.*)$ {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index   index.php;
            fastcgi_split_path_info         ^(.+\.php)(.*)$;
            fastcgi_param TIDEWAYS_SAMPLERATE "25";
             fastcgi_param PHP_VALUE "auto_prepend_file=/home/0x584A/www/xhgui/external/header.php";
            fastcgi_param       PATH_INFO                $fastcgi_path_info;
            fastcgi_param       PATH_TRANSLATED        $DOCUMENT_ROOT$fastcgi_path_info;
            fastcgi_param       SCRIPT_FILENAME  $DOCUMENT_ROOT/$fastcgi_script_name;
            include             fastcgi_params;
        }
    }
    xhgui
    server {
            listen 127.0.10.2:80;
            server_name  debug.com;
            root   /home/0x584A/www/xhgui/webroot;
            index  index.html index.htm index.php;
    
        location / {
            try_files $uri $uri/ /index.php?$uri&$args;
        }
    
        location ~ \.php$ {
            try_files $uri =404;
            include /etc/nginx/fastcgi_params;
            fastcgi_pass    127.0.0.1:9000;
            fastcgi_index   index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }
    }
    

    访问配置好的页面即可。

注意

  • 分析方式请自行更具url设置

    'profiler.enable' => function() {
        // url 中包含debug=1则百分百捕获
        if(!empty($_GET['debug'])){
            return True;
        }else{
            // 1%采样
            return rand(1, 100) === 42;
        }
    },
  • 在xhgui的config/config.default.php中,可设置采样命中次数;

    • return rand(1, 100) === 42;1%的采样率,改成return True;则标识每次都采样
  • 分析参数过多则清除mongodb数据

    $ mongo
      $ use xhprof;
      $ db.dropDatabase();

终极特效

1.png

2.png

3.png

参考

查看原文

赞 21 收藏 55 评论 0

麦田348462402 关注了用户 · 4月13日

CrazyCodes @crazycodes

https://github.com/CrazyCodes... 我的博客
_
| |__ __ _
| '_ | | | |/ _` |
| |_) | |_| | (_| |
|_.__/ __,_|__, |

         |___/   感谢生命可以让我成为一名程序员

                         CrazyCodes To Author

关注 4654

麦田348462402 赞了文章 · 2月3日

Java13的新特性

Java语言特性系列

本文主要讲述一下Java13的新特性

版本号

java -version
openjdk version "13" 2019-09-17
OpenJDK Runtime Environment (build 13+33)
OpenJDK 64-Bit Server VM (build 13+33, mixed mode, sharing)
从version信息可以看出是build 13+33

特性列表

350: Dynamic CDS Archives

JDK5引入了Class-Data Sharing可以用于多个JVM共享class,提升启动速度,最早只支持system classes及serial GC
JDK9对其进行扩展以支持application classes及其他GC算法
java10的新特性JEP 310: Application Class-Data Sharing扩展了JDK5引入的Class-Data Sharing,支持application的Class-Data Sharing并开源出来(以前是commercial feature)
JDK11将-Xshare:off改为默认-Xshare:auto,以更加方便使用CDS特性
JDK12的341: Default CDS Archives即在64-bit平台上编译jdk的时候就默认在&dollar;{JAVA_HOME}/lib/server目录下生成一份名为classes.jsa的默认archive文件(大概有18M)方便大家使用
JDK13的这个特性支持在Java application执行之后进行动态archive
  • 导出jsa
java -XX:ArchiveClassesAtExit=hello.jsa -cp hello.jar Hello
  • 使用jsa
java -XX:SharedArchiveFile=hello.jsa -cp hello.jar Hello

351: ZGC: Uncommit Unused Memory

Java12的346: Promptly Return Unused Committed Memory from G1新增了两个参数分别是G1PeriodicGCInterval及G1PeriodicGCSystemLoadThreshold用于GC之后重新调整Java heap size,然后将多余的内存归还给操作系统
Java12的189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental)拥有参数-XX:ShenandoahUncommitDelay=<milliseconds>来指定ZPage的page cache的失效时间,然后归还内存
Java13则给ZGC新增归还unused heap memory给操作系统的特性;它新增了几个参数,-XX:ZUncommitDelay=<seconds>用于指定ZPage的page cache的失效时间;ZGC的归还内存默认是开启的,可以使用-XX:-ZUncommit来显式禁用

353: Reimplement the Legacy Socket API

本特性替换了java.net.Socket以及java.net.ServerSocket API的底层实现;它使用NioSocketImpl来替换JDK1.0的PlainSocketImpl;如果要继续使用旧版的Socket实现,可以使用-Djdk.net.usePlainSocketImpl参数来切换到旧版本

/Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home/lib/src.zip!/java.base/java/net/SocketImpl.java

public abstract class SocketImpl implements SocketOptions {
    private static final boolean USE_PLAINSOCKETIMPL = usePlainSocketImpl();

    private static boolean usePlainSocketImpl() {
        PrivilegedAction<String> pa = () -> NetProperties.get("jdk.net.usePlainSocketImpl");
        String s = AccessController.doPrivileged(pa);
        return (s != null) && !s.equalsIgnoreCase("false");
    }

    /**
     * Creates an instance of platform's SocketImpl
     */
    @SuppressWarnings("unchecked")
    static <S extends SocketImpl & PlatformSocketImpl> S createPlatformSocketImpl(boolean server) {
        if (USE_PLAINSOCKETIMPL) {
            return (S) new PlainSocketImpl(server);
        } else {
            return (S) new NioSocketImpl(server);
        }
    }

    //......
}
SocketImpl的USE_PLAINSOCKETIMPL取决于usePlainSocketImpl方法,而它会从NetProperties读取dk.net.usePlainSocketImpl配置,如果不为null且不为false,则usePlainSocketImpl方法返回true;createPlatformSocketImpl会根据USE_PLAINSOCKETIMPL来创建PlainSocketImpl或者NioSocketImpl

354: Switch Expressions (Preview)

本特性主要是使用yield替换了break来避免歧义,因为break可以用来进行跳转执行类似goto的操作
    @Test
    public void testSwitchYield(){
        String dayOfWeek = switch(1){
            case 1 -> {
                String day = "Monday";
                yield day;
            }
            case 2 -> {
                String day = "Tuesday";
                yield day;
            }
            default -> "Unknown";
        };
        System.out.println(dayOfWeek);
    }

355: Text Blocks (Preview)

本特性主要引入了Text Blocks,使用"""来包围一段text block,可以内置占位符最后使用String.format来填充
    @Test
    public void testTextBlock(){
        // Without Text Blocks
        String html = "<html>\n" +
                "   <body>\n" +
                "      <p>Hello, Escapes</p>\n" +
                "   </body>\n" +
                "</html>\n";
        System.out.println(html);

        // With Text Blocks
        String html2 = """
            <html>
                <body>
                <p>Hello, %s</p>
                </body>
            </html>""";

        System.out.println(String.format(html2, "World"));

        String htmlWithNewLine = """
            <html>
                <body>
                <p>Hello World</p>
                </body>
            </html>
            """;
        System.out.print(htmlWithNewLine);
        System.out.println("a new line");
    }
需要注意text blocks不能在一行,另外如果结尾的"""在新的一行则会输出新的一行

细项解读

上面列出的是大方面的特性,除此之外还有一些api的更新及废弃,主要见JDK 13 Release Notes,这里举几个例子。

添加项

  • 添加FileSystems.newFileSystem(Path, Map<String, ?>) Method
  • 新的java.nio.ByteBuffer Bulk get/put Methods Transfer Bytes Without Regard to Buffer Position
  • 支持Unicode 12.1
  • 添加-XX:SoftMaxHeapSize Flag,目前仅仅对ZGC起作用
  • ZGC的最大heap大小增大到16TB

移除项

  • 移除awt.toolkit System Property
  • 移除Runtime Trace Methods
  • 移除-XX:+AggressiveOpts
  • 移除Two Comodo Root CA Certificates、Two DocuSign Root CA Certificates
  • 移除内部的com.sun.net.ssl包

废弃项

  • 废弃-Xverify:none及-noverify
  • 废弃rmic Tool并准备移除
  • 废弃javax.security.cert并准备移除

已知问题

  • 不再支持Windows 2019 Core Server
  • 使用ZIP File System (zipfs) Provider来更新包含Uncompressed Entries的ZIP或JAR可能造成文件损坏

其他事项

  • GraphicsEnvironment.getCenterPoint()及getMaximumWindowBounds()已跨平台统一
  • 增强了JAR Manifest的Class-Path属性处理
jdk.net.URLClassPath.showIgnoredClassPathEntries属性设置为true可以用来帮助查看非法的Class-Path entries
  • 针对Negatively Sized Argument,StringBuffer(CharSequence)及StringBuilder(CharSequence)会抛出NegativeArraySizeException
  • linux的默认进程启动机制已经使用posix_spawn
  • Lookup.unreflectSetter(Field)针对static final fields会抛出IllegalAccessException
  • 使用了java.net.Socket.setSocketImplFactory及java.net.ServerSocket.setSocketFactory方法的要注意,要求客户端及服务端要一致,不能一端使用自定义的factory一端使用默认的factory
  • SocketImpl的supportedOptions, getOption及setOption方法的默认实现发生了变化,默认的supportedOptions返回空,而默认的getOption,及setOption方法抛出UnsupportedOperationException
  • JNI NewDirectByteBuffer创建的Direct Buffer为java.nio.ByteOrder.BIG_ENDIAN
  • Base64.Encoder及Base64.Decoder可能抛出OutOfMemoryError
  • 改进了Serial GC Young pause time report
  • 改进了MaxRAM及UseCompressedOops参数的行为

小结

  • Java13主要新增了如下特性

    • 350: Dynamic CDS Archives
    • 351: ZGC: Uncommit Unused Memory
    • 353: Reimplement the Legacy Socket API
    • 354: Switch Expressions (Preview)
    • 355: Text Blocks (Preview)
  • 语法层面,改进了Switch Expressions,新增了Text Blocks,二者皆处于Preview状态;API层面主要使用NioSocketImpl来替换JDK1.0的PlainSocketImpl
  • GC层面则改进了ZGC,以支持Uncommit Unused Memory

doc

查看原文

赞 16 收藏 8 评论 0

麦田348462402 赞了文章 · 1月17日

PHP反射机制

PHP反射机制

PHP反射机制从PHP5开始支持,做业务开发的话应该很少接触反射。我其实也是接触不多,最近在学习laravel的"优雅",就接触了到它其中的反射用法,已经我自己的看法想法。

反射

按照之前的套路,我们来看一下官方手册,官方是怎么说的。

Reflection

PHP 5 具有完整的反射 API,添加了对类、接口、函数、方法和扩展进行反向工程的能力。 此外,反射 API 提供了方法来取出函数、类和方法中的文档注释。我的理解就是php反射机制能拿到类里面的属性方法,private 和 protected的也可以

  • 以上是官方文档中给出的东西,说实话我看了感觉没什么感觉。
  • 能get到的点就是我们能够通过这个窥探一个类所有信息,就像在别人的窗上桶了一个洞一样。
  • 我应该怎么用,或者基于什么场景去用呢?这还是很伤的。

laravel中的反射

laravel整个框架设计的"优雅"就是在于container、IOC、依赖注入。我们来看一下容器中一段关于反射的代码:
IlluminateContainerContainer:

/**
     * Instantiate a concrete instance of the given type.
     *
     * @param  string  $concrete
     * @param  array   $parameters
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    public function build($concrete, array $parameters = [])
    {
        // If the concrete type is actually a Closure, we will just execute it and
        // hand back the results of the functions, which allows functions to be
        // used as resolvers for more fine-tuned resolution of these objects.
        if ($concrete instanceof Closure) {
            return $concrete($this, $parameters);
        }

        $reflector = new ReflectionClass($concrete);

        // If the type is not instantiable, the developer is attempting to resolve
        // an abstract type such as an Interface of Abstract Class and there is
        // no binding registered for the abstractions so we need to bail out.
        if (! $reflector->isInstantiable()) {
            if (! empty($this->buildStack)) {
                $previous = implode(', ', $this->buildStack);

                $message = "Target [$concrete] is not instantiable while building [$previous].";
            } else {
                $message = "Target [$concrete] is not instantiable.";
            }

            throw new BindingResolutionException($message);
        }

        $this->buildStack[] = $concrete;

        $constructor = $reflector->getConstructor();

        // If there are no constructors, that means there are no dependencies then
        // we can just resolve the instances of the objects right away, without
        // resolving any other types or dependencies out of these containers.
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }

        $dependencies = $constructor->getParameters();

        // Once we have all the constructor's parameters we can create each of the
        // dependency instances and then use the reflection instances to make a
        // new instance of this class, injecting the created dependencies in.
        $parameters = $this->keyParametersByArgument(
            $dependencies, $parameters
        );

        $instances = $this->getDependencies(
            $dependencies, $parameters
        );

        array_pop($this->buildStack);

        return $reflector->newInstanceArgs($instances);
    }
    

就是实现绑定类的方法,build方法。下面我们就来分析一下:

  • 参数:$concreate string 类似于Model::class这种嘛,不难理解。$parameters array 参数 更不难理解了吧。
  • 判断 $concreate 是否是匿名类(闭包),是匿名类就执行这个函数.
  • 创建反射类,去映射这个类。
  • 判断这个类能否被实例化,也就是看构造函数是否是private。否就抛出出异常。
  • 在容器成员变量中数组维护这个类,反射实例调用构造函数,获取返回值。
  • 判断返回值是否为空,如果为空就说明不需要参数依赖,那么就直接实例化。否则就获取构造函数的参数依赖,将传入的参数和依赖参数进行对照。
  • 最后,在调用newInstanceArgs进行实例化,之后返回实例。

后记

其实在上面这个laravel中的例子已经很好的阐明了反射机制的使用方式,或许你现在的业务场景还未必能够使用到这种机制。但是,当碰到的时候请记得还有这种方式能够使用。

  • 当你需要去实例化一个类,但是这个类对你来说完全就是封闭或者说是未知的,你可以创建反射来与映射这个类,通过一系列的探测来最终实例化这个类,尤其还在动态运行中的。
  • 基于这种机制,其实可以玩出很多的花样。比如说能够自动生成文档。
  • 实现MVC这种架构,使用反射自动调用实现
$class = new ReflectionClass(ucfirst($controller));
$controller = $class->newInstance();
if ($class->hasMethod($method)) {
    $method = $class->getMethod($method);
    $method->invokeArgs($controller, $arguments);
} else {
    throw new Exception("{$controller} controller method {$method} not exists!");
}
  • 实现单元测试
    $class = new ReflectionClass($class);
    if ($class->hasMethod($method)) {
        $method = $class->getMethod($method);
        $object = $class->newInstance();
        $class = $method->invokeArgs(new $object, $params);
        var_dump($res === $assert);
    }
  • laravel中的反射帮助它解决DI容器的依赖注入的问题。

还有很多好玩的等着你自己去尝试,这种机制究竟能玩出多少花样,就看你自己怎么玩了。

查看原文

赞 12 收藏 17 评论 0

麦田348462402 评论了文章 · 2019-04-29

vue 里主动销毁 keep-alive 缓存的组件

问题产生的背景

我们一个后台,在切换一些标签页的时候,是使用的 keep-alive 缓存的标签页,也使用了 include 属性来决定哪个页面进行缓存,而标签页的切换实际上是路由的切换,也就是说打开一个新标签页的时候,url 会跟着变化,老的标签页如果在 keep-aliveinclude 范围内那就会缓存下来。
然后客服人员就反馈页面开的久了就会崩溃,因为他们基础上不会刷新页面(工作需要),又总有切换标签的习惯,最后导致内存越来越大最后崩溃。

依赖环境

这个项目是基于一个开源 vue 后台框架:https://github.com/PanJiaChen/vue-element-admin,然后代码一直由几个后端开发维护的!所以后端没找出问题在哪,然后就我来接手这个问题了。
写文章时,标签里竟然没有 vue 这一项,差评!

定位问题

先梳理下业务逻辑:从业务场景来说,我们在标签页之间切换时,如果刚进入的这个标签页已被缓存了,那被缓存的标签页就直接拿出来展示就行,而关闭这个标签页的时候就应该销毁对应的组件。

clipboard.png

花了点时间查看了一下代码,发现问题在于关闭标签页的时候,虽然这个页面没在 keep-aliveinclude 里了,但是组件也没有被销毁掉,还是在缓存状态,我们可以通过 Vue Devtools 插件看到关闭后的标签页对应的组件一直还存在着:

clipboard.png

当然,在这块 keep-alive 本身的逻辑我觉得是没问题的,主要是我们项目比较复杂,缓存的组件太多了而且会一直增加,所以最终导致崩溃。

解决问题

既然问题已经定位了,那就好解决问题了,只需要在关闭标签页的时候把对应的组件也销毁掉就好了。

经过网上一翻查找,发现销毁一个组件可以使用: this.$destroy(‘组件名’) 来销毁。

先说下大概思路:keep-aliveinclude 里存的其实是一个 vuex 中的一个数据源(数据源保存的是路由名称),当关闭标签页时,这个数据源中的一项会被移除。这之前,我们在组件里监听到这个数据源的变化,如果此组件对应的路由(这个路由应在 mounted 的时候保存下来)已经不在数据源中了,那就应该销毁此组件。

代码大概如下:

const mixin = {
  data() {
    return {
      routePath: ''
    }
  },
  mounted() {
    this.routePath = this.$route.path
  },
  computed: {
    visitedViews() {
      return this.$store.state.tagsView.visitedViews
    }
  },
  watch: {
    visitedViews(value) {
      if(value 里没有了 routePath 这一项)
        this.$destroy(this.$options.name)
      }
    }
  }
}

export default mixin

这一段代码单独拎出来了,然后在需要缓存的组件里使用 mixins 混入到组件对象中,这样组件中要添加的代码量就比较少了。

更改后经过测试,关闭标签页后对应的组件就会被销毁掉,使用 Vue Devtools 能看的很清楚。

更多思考

在我们后台操作这么频繁的业务场景下,使用 keep-alive 其实并不是一个好的选择。

在我们修复这个问题后,我们通过控制台里的 Memory 查看页面内存的变化时,发现组件即便被销毁,也要经过一段时间才能回收完,当我们在这一段时间一直创建/打开新的标签页时,内存还是会在短时间内高涨。而且有时候,内存在经过一段时间后也并没有回收掉。

keep-alive 本质上是把整个 dom 节点及对应的事件等都缓存下来了,当这样的组件很多的时候,自然会占用很多内存。而如果我们只缓存这个组件中的数据,在需要这个组件再次显示的时候再临时渲染那肯定要节省很多内存的,毕竟数据占的空间其实很小的,而渲染组件要花的时间也不会很长(只要组件不是特别特别复杂)。

所以,下一阶段的优化工作就是把 keep-alive 去掉,然后使用 vuex 来缓存组件中的数据,当需要重新显示数据时再把数据取出来并重新渲染。当然,这是一个比较大的工程!

查看原文

麦田348462402 收藏了文章 · 2019-04-29

vue 里主动销毁 keep-alive 缓存的组件

问题产生的背景

我们一个后台,在切换一些标签页的时候,是使用的 keep-alive 缓存的标签页,也使用了 include 属性来决定哪个页面进行缓存,而标签页的切换实际上是路由的切换,也就是说打开一个新标签页的时候,url 会跟着变化,老的标签页如果在 keep-aliveinclude 范围内那就会缓存下来。
然后客服人员就反馈页面开的久了就会崩溃,因为他们基础上不会刷新页面(工作需要),又总有切换标签的习惯,最后导致内存越来越大最后崩溃。

依赖环境

这个项目是基于一个开源 vue 后台框架:https://github.com/PanJiaChen/vue-element-admin,然后代码一直由几个后端开发维护的!所以后端没找出问题在哪,然后就我来接手这个问题了。
写文章时,标签里竟然没有 vue 这一项,差评!

定位问题

先梳理下业务逻辑:从业务场景来说,我们在标签页之间切换时,如果刚进入的这个标签页已被缓存了,那被缓存的标签页就直接拿出来展示就行,而关闭这个标签页的时候就应该销毁对应的组件。

clipboard.png

花了点时间查看了一下代码,发现问题在于关闭标签页的时候,虽然这个页面没在 keep-aliveinclude 里了,但是组件也没有被销毁掉,还是在缓存状态,我们可以通过 Vue Devtools 插件看到关闭后的标签页对应的组件一直还存在着:

clipboard.png

当然,在这块 keep-alive 本身的逻辑我觉得是没问题的,主要是我们项目比较复杂,缓存的组件太多了而且会一直增加,所以最终导致崩溃。

解决问题

既然问题已经定位了,那就好解决问题了,只需要在关闭标签页的时候把对应的组件也销毁掉就好了。

经过网上一翻查找,发现销毁一个组件可以使用: this.$destroy(‘组件名’) 来销毁。

先说下大概思路:keep-aliveinclude 里存的其实是一个 vuex 中的一个数据源(数据源保存的是路由名称),当关闭标签页时,这个数据源中的一项会被移除。这之前,我们在组件里监听到这个数据源的变化,如果此组件对应的路由(这个路由应在 mounted 的时候保存下来)已经不在数据源中了,那就应该销毁此组件。

代码大概如下:

const mixin = {
  data() {
    return {
      routePath: ''
    }
  },
  mounted() {
    this.routePath = this.$route.path
  },
  computed: {
    visitedViews() {
      return this.$store.state.tagsView.visitedViews
    }
  },
  watch: {
    visitedViews(value) {
      if(value 里没有了 routePath 这一项)
        this.$destroy(this.$options.name)
      }
    }
  }
}

export default mixin

这一段代码单独拎出来了,然后在需要缓存的组件里使用 mixins 混入到组件对象中,这样组件中要添加的代码量就比较少了。

更改后经过测试,关闭标签页后对应的组件就会被销毁掉,使用 Vue Devtools 能看的很清楚。

更多思考

在我们后台操作这么频繁的业务场景下,使用 keep-alive 其实并不是一个好的选择。

在我们修复这个问题后,我们通过控制台里的 Memory 查看页面内存的变化时,发现组件即便被销毁,也要经过一段时间才能回收完,当我们在这一段时间一直创建/打开新的标签页时,内存还是会在短时间内高涨。而且有时候,内存在经过一段时间后也并没有回收掉。

keep-alive 本质上是把整个 dom 节点及对应的事件等都缓存下来了,当这样的组件很多的时候,自然会占用很多内存。而如果我们只缓存这个组件中的数据,在需要这个组件再次显示的时候再临时渲染那肯定要节省很多内存的,毕竟数据占的空间其实很小的,而渲染组件要花的时间也不会很长(只要组件不是特别特别复杂)。

所以,下一阶段的优化工作就是把 keep-alive 去掉,然后使用 vuex 来缓存组件中的数据,当需要重新显示数据时再把数据取出来并重新渲染。当然,这是一个比较大的工程!

查看原文

认证与成就

  • 获得 12 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2013-09-04
个人主页被 324 人浏览