欢迎光临
我们一直在努力

Spring Boot 配置文件中的花样,看这一篇足矣!

mumupudding阅读(9)

快速入门一节中,我们轻松的实现了一个简单的RESTful API应用,体验了一下Spring Boot给我们带来的诸多优点,我们用非常少的代码量就成功的实现了一个Web应用,这是传统的Spring应用无法办到的,虽然我们在实现Controller时用到的代码是一样的,但是在配置方面,相信大家也注意到了,在上面的例子中,除了Maven的配置之后,就没有引入任何的配置。

这就是之前我们所提到的,Spring Boot针对我们常用的开发场景提供了一系列自动化配置来减少原本复杂而又几乎很少改动的模板化配置内容。但是,我们还是需要去了解如何在Spring Boot中修改这些自动化的配置内容,以应对一些特殊的场景需求,比如:我们在同一台主机上需要启动多个基于Spring Boot的web应用,若我们不为每个应用指定特别的端口号,那么默认的8080端口必将导致冲突。

如果您还有在读我的Spring Cloud系列教程,其实有大量的工作都会是针对配置文件的。所以我们有必要深入的了解一些关于Spring Boot中的配置文件的知识,比如:它的配置方式、如何实现多环境配置,配置信息的加载顺序等。

配置基础

快速入门示例中,我们介绍Spring Boot的工程结构时,有提到过 src/main/resources目录是Spring Boot的配置目录,所以我们要为应用创建配置个性化配置时,就是在该目录之下。

Spring Boot的默认配置文件位置为: src/main/resources/application.properties。关于Spring Boot应用的配置内容都可以集中在该文件中了,根据我们引入的不同Starter模块,可以在这里定义诸如:容器端口名、数据库链接信息、日志级别等各种配置信息。比如,我们需要自定义web模块的服务端口号,可以在application.properties中添加server.port=8888来指定服务端口为8888,也可以通过spring.application.name=hello来指定应用名(该名字在Spring Cloud应用中会被注册为服务名)。

Spring Boot的配置文件除了可以使用传统的properties文件之外,还支持现在被广泛推荐使用的YAML文件。

YAML(英语发音:/ˈjæməl/,尾音类似camel骆驼)是一个可读性高,用来表达资料序列的格式。YAML参考了其他多种语言,包括:C语言、Python、Perl,并从XML、电子邮件的数据格式(RFC 2822)中获得灵感。Clark Evans在2001年首次发表了这种语言,另外Ingy döt Net与Oren Ben-Kiki也是这语言的共同设计者。目前已经有数种编程语言或脚本语言支援(或者说解析)这种语言。YAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递回缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言),但为了强调这种语言以数据做为中心,而不是以标记语言为重点,而用反向缩略语重新命名。AML的语法和其他高阶语言类似,并且可以简单表达清单、散列表,标量等资料形态。它使用空白符号缩排和大量依赖外观的特色,特别适合用来表达或编辑数据结构、各种设定档、倾印除错内容、文件大纲(例如:许多电子邮件标题格式和YAML非常接近)。尽管它比较适合用来表达阶层式(hierarchical model)的数据结构,不过也有精致的语法可以表示关联性(relational model)的资料。由于YAML使用空白字元和分行来分隔资料,使得它特别适合用grep/Python/Perl/Ruby操作。其让人最容易上手的特色是巧妙避开各种封闭符号,如:引号、各种括号等,这些符号在巢状结构时会变得复杂而难以辨认。 —— 维基百科

YAML采用的配置格式不像properties的配置那样以单纯的键值对形式来表示,而是以类似大纲的缩进形式来表示。比如:下面的一段YAML配置信息

environments:    dev:        url: http://dev.bar.com        name: Developer Setup    prod:        url: http://foo.bar.com        name: My Cool App

与其等价的properties配置如下。

environments.dev.url=http://dev.bar.comenvironments.dev.name=Developer Setupenvironments.prod.url=http://foo.bar.comenvironments.prod.name=My Cool App

通过YAML的配置方式,我们可以看到配置信息利用阶梯化缩进的方式,其结构显得更为清晰易读,同时配置内容的字符量也得到显著的减少。除此之外,YAML还可以在一个单个文件中通过使用spring.profiles属性来定义多个不同的环境配置。例如下面的内容,在指定为test环境时,server.port将使用8882端口;而在prod环境,server.port将使用8883端口;如果没有指定环境,server.port将使用8881端口。

server:    port: 8881---spring:    profiles: testserver:    port: 8882---spring:    profiles: prodserver:    port: 8883

注意:YAML目前还有一些不足,它无法通过@PropertySource注解来加载配置。但是,YAML加载属性到内存中保存的时候是有序的,所以当配置文件中的信息需要具备顺序含义时,YAML的配置方式比起properties配置文件更有优势。

自定义参数

我们除了可以在Spring Boot的配置文件中设置各个Starter模块中预定义的配置属性,也可以在配置文件中定义一些我们需要的自定义属性。比如在application.properties中添加:

book.name=SpringCloudInActionbook.author=ZhaiYongchao

然后,在应用中我们可以通过@Value注解来加载这些自定义的参数,比如:

@Componentpublic class Book {    @Value("${book.name}")    private String name;    @Value("${book.author}")    private String author;    // 省略getter和setter}

@Value注解加载属性值的时候可以支持两种表达式来进行配置:

  • 一种是我们上面介绍的PlaceHolder方式,格式为 ${...} ,大括号内为PlaceHolder
  • 另外还可以使用SpEL表达式(Spring Expression Language), 格式为 #{...} ,大括号内为SpEL表达式

参数引用

application.properties中的各个参数之间,我们也可以直接通过使用PlaceHolder的方式来进行引用,就像下面的设置:

book.name=SpringCloudbook.author=ZhaiYongchaobook.desc=${book.author}  is writing《${book.name}》

book.desc参数引用了上文中定义的book.namebook.author属性,最后该属性的值就是ZhaiYongchao is writing《SpringCloud》

使用随机数

在一些特殊情况下,有些参数我们希望它每次加载的时候不是一个固定的值,比如:密钥、服务端口等。在Spring Boot的属性配置文件中,我们可以通过使用${random}配置来产生随机的int值、long值或者string字符串,这样我们就可以容易的通过配置来属性的随机生成,而不是在程序中通过编码来实现这些逻辑。

${random}的配置方式主要有一下几种,读者可作为参考使用。

# 随机字符串com.didispace.blog.value=${random.value}# 随机intcom.didispace.blog.number=${random.int}# 随机longcom.didispace.blog.bignumber=${random.long}# 10以内的随机数com.didispace.blog.test1=${random.int(10)}# 10-20的随机数com.didispace.blog.test2=${random.int[10,20]}

该配置方式可以用于设置应用端口等场景,避免在本地调试时出现端口冲突的麻烦

命令行参数

回顾一下在本章的快速入门中,我们还介绍了如何启动Spring Boot应用,其中提到了使用命令java -jar命令来启动的方式。该命令除了启动应用之外,还可以在命令行中来指定应用的参数,比如:java -jar xxx.jar --server.port=8888,直接以命令行的方式,来设置server.port属性,另启动应用的端口设为8888。

在命令行方式启动Spring Boot应用时,连续的两个减号--就是对application.properties中的属性值进行赋值的标识。所以,java -jar xxx.jar --server.port=8888命令,等价于我们在application.properties中添加属性server.port=8888

通过命令行来修改属性值是Spring Boot非常重要的一个特性,通过此特性,理论上已经使得我们应用的属性在启动前是可变的,所以其中端口号也好、数据库连接也好,都是可以在应用启动时发生改变,而不同于以往的Spring应用通过Maven的Profile在编译器进行不同环境的构建。其最大的区别就是,Spring Boot的这种方式,可以让应用程序的打包内容,贯穿开发、测试以及线上部署,而Maven不同Profile的方案每个环境所构建的包,其内容本质上是不同的。但是,如果每个参数都需要通过命令行来指定,这显然也不是一个好的方案,所以下面我们看看如果在Spring Boot中实现多环境的配置。

多环境配置

我们在开发任何应用的时候,通常同一套程序会被应用和安装到几个不同的环境,比如:开发、测试、生产等。其中每个环境的数据库地址、服务器端口等等配置都会不同,如果在为不同环境打包时都要频繁修改配置文件的话,那必将是个非常繁琐且容易发生错误的事。

对于多环境的配置,各种项目构建工具或是框架的基本思路是一致的,通过配置多份不同环境的配置文件,再通过打包命令指定需要打包的内容之后进行区分打包,Spring Boot也不例外,或者说更加简单。

在Spring Boot中多环境配置文件名需要满足application-{profile}.properties的格式,其中{profile}对应你的环境标识,比如:

  • application-dev.properties:开发环境
  • application-test.properties:测试环境
  • application-prod.properties:生产环境

至于哪个具体的配置文件会被加载,需要在application.properties文件中通过spring.profiles.active属性来设置,其值对应配置文件中的{profile}值。如:spring.profiles.active=test就会加载application-test.properties配置文件内容。

下面,以不同环境配置不同的服务端口为例,进行样例实验。

  • 针对各环境新建不同的配置文件application-dev.propertiesapplication-test.propertiesapplication-prod.properties

  • 在这三个文件均都设置不同的server.port属性,如:dev环境设置为1111,test环境设置为2222,prod环境设置为3333

  • application.properties中设置spring.profiles.active=dev,就是说默认以dev环境设置

  • 测试不同配置的加载

  • 执行java -jar xxx.jar,可以观察到服务端口被设置为1111,也就是默认的开发环境(dev)

  • 执行java -jar xxx.jar --spring.profiles.active=test,可以观察到服务端口被设置为2222,也就是测试环境的配置(test)

  • 执行java -jar xxx.jar --spring.profiles.active=prod,可以观察到服务端口被设置为3333,也就是生产环境的配置(prod)

按照上面的实验,可以如下总结多环境的配置思路:

  • application.properties中配置通用内容,并设置spring.profiles.active=dev,以开发环境为默认配置
  • application-{profile}.properties中配置各个环境不同的内容
  • 通过命令行方式去激活不同环境的配置

加载顺序

在上面的例子中,我们将Spring Boot应用需要的配置内容都放在了项目工程中,虽然我们已经能够通过spring.profiles.active或是通过Maven来实现多环境的支持。但是,当我们的团队逐渐壮大,分工越来越细致之后,往往我们不需要让开发人员知道测试或是生成环境的细节,而是希望由每个环境各自的负责人(QA或是运维)来集中维护这些信息。那么如果还是以这样的方式存储配置内容,对于不同环境配置的修改就不得不去获取工程内容来修改这些配置内容,当应用非常多的时候就变得非常不方便。同时,配置内容都对开发人员可见,本身这也是一种安全隐患。对此,现在出现了很多将配置内容外部化的框架和工具,后续将要介绍的Spring Cloud Config就是其中之一,为了后续能更好的理解Spring Cloud Config的加载机制,我们需要对Spring Boot对数据文件的加载机制有一定的了解。

Spring Boot为了能够更合理的重写各属性的值,使用了下面这种较为特别的属性加载顺序:

  1. 命令行中传入的参数。
  2. SPRING_APPLICATION_JSON中的属性。SPRING_APPLICATION_JSON是以JSON格式配置在系统环境变量中的内容。
  3. java:comp/env中的JNDI属性。
  4. Java的系统属性,可以通过System.getProperties()获得的内容。
  5. 操作系统的环境变量
  6. 通过random.*配置的随机属性
  7. 位于当前应用jar包之外,针对不同{profile}环境的配置文件内容,例如:application-{profile}.properties或是YAML定义的配置文件
  8. 位于当前应用jar包之内,针对不同{profile}环境的配置文件内容,例如:application-{profile}.properties或是YAML定义的配置文件
  9. 位于当前应用jar包之外的application.propertiesYAML配置内容
  10. 位于当前应用jar包之内的application.propertiesYAML配置内容
  11. @Configuration注解修改的类中,通过@PropertySource注解定义的属性
  12. 应用默认属性,使用SpringApplication.setDefaultProperties定义的内容

优先级按上面的顺序有高到低,数字越小优先级越高。

可以看到,其中第7项和第9项都是从应用jar包之外读取配置文件,所以,实现外部化配置的原理就是从此切入,为其指定外部配置文件的加载位置来取代jar包之内的配置内容。通过这样的实现,我们的工程在配置中就变的非常干净,我们只需要在本地放置开发需要的配置即可,而其他环境的配置就可以不用关心,由其对应环境的负责人去维护即可。

2.x 新特性

在Spring Boot 2.0中推出了Relaxed Binding 2.0,对原有的属性绑定功能做了非常多的改进以帮助我们更容易的在Spring应用中加载和读取配置信息。下面本文就来说说Spring Boot 2.0中对配置的改进。

配置文件绑定

简单类型

在Spring Boot 2.0中对配置属性加载的时候会除了像1.x版本时候那样移除特殊字符外,还会将配置均以全小写的方式进行匹配和加载。所以,下面的4种配置方式都是等价的:

  • properties格式:
spring.jpa.databaseplatform=mysqlspring.jpa.database-platform=mysqlspring.jpa.databasePlatform=mysqlspring.JPA.database_platform=mysql
  • yaml格式:
spring:  jpa:    databaseplatform: mysql    database-platform: mysql    databasePlatform: mysql    database_platform: mysql

Tips:推荐使用全小写配合-分隔符的方式来配置,比如:spring.jpa.database-platform=mysql

List类型

在properties文件中使用[]来定位列表类型,比如:

spring.my-example.url[0]=http://example.comspring.my-example.url[1]=http://spring.io

也支持使用逗号分割的配置方式,上面与下面的配置是等价的:

spring.my-example.url=http://example.com,http://spring.io

而在yaml文件中使用可以使用如下配置:

spring:  my-example:    url:      - http://example.com      - http://spring.io

也支持逗号分割的方式:

spring:  my-example:    url: http://example.com, http://spring.io

注意:在Spring Boot 2.0中对于List类型的配置必须是连续的,不然会抛出UnboundConfigurationPropertiesException异常,所以如下配置是不允许的:

foo[0]=afoo[2]=b

在Spring Boot 1.x中上述配置是可以的,foo[1]由于没有配置,它的值会是null

Map类型

Map类型在properties和yaml中的标准配置方式如下:

  • properties格式:
spring.my-example.foo=barspring.my-example.hello=world
  • yaml格式:
spring:  my-example:    foo: bar    hello: world

注意:如果Map类型的key包含非字母数字和-的字符,需要用[]括起来,比如:

spring:  my-example:    '[foo.baz]': bar

环境属性绑定

简单类型

在环境变量中通过小写转换与.替换_来映射配置文件中的内容,比如:环境变量SPRING_JPA_DATABASEPLATFORM=mysql的配置会产生与在配置文件中设置spring.jpa.databaseplatform=mysql一样的效果。

List类型

由于环境变量中无法使用[]符号,所以使用_来替代。任何由下划线包围的数字都会被认为是[]的数组形式。比如:

MY_FOO_1_ = my.foo[1]MY_FOO_1_BAR = my.foo[1].barMY_FOO_1_2_ = my.foo[1][2]

另外,最后环境变量最后是以数字和下划线结尾的话,最后的下划线可以省略,比如上面例子中的第一条和第三条等价于下面的配置:

MY_FOO_1 = my.foo[1]MY_FOO_1_2 = my.foo[1][2]

系统属性绑定

简单类型

系统属性与文件配置中的类似,都以移除特殊字符并转化小写后实现绑定,比如下面的命令行参数都会实现配置spring.jpa.databaseplatform=mysql的效果:

-Dspring.jpa.database-platform=mysql-Dspring.jpa.databasePlatform=mysql-Dspring.JPA.database_platform=mysql

List类型

系统属性的绑定也与文件属性的绑定类似,通过[]来标示,比如:

-D"spring.my-example.url[0]=http://example.com"-D"spring.my-example.url[1]=http://spring.io"

同样的,他也支持逗号分割的方式,比如:

-Dspring.my-example.url=http://example.com,http://spring.io

属性的读取

上文介绍了Spring Boot 2.0中对属性绑定的内容,可以看到对于一个属性我们可以有多种不同的表达,但是如果我们要在Spring应用程序的environment中读取属性的时候,每个属性的唯一名称符合如下规则:

  • 通过.分离各个元素
  • 最后一个.将前缀与属性名称分开
  • 必须是字母(a-z)和数字(0-9)
  • 必须是小写字母
  • 用连字符-来分隔单词
  • 唯一允许的其他字符是[],用于List的索引
  • 不能以数字开头

所以,如果我们要读取配置文件中spring.jpa.database-platform的配置,可以这样写:

this.environment.containsProperty("spring.jpa.database-platform")

而下面的方式是无法获取到spring.jpa.database-platform配置内容的:

this.environment.containsProperty("spring.jpa.databasePlatform")

注意:使用@Value获取配置内容的时候也需要这样的特点

全新的绑定API

在Spring Boot 2.0中增加了新的绑定API来帮助我们更容易的获取配置信息。下面举个例子来帮助大家更容易的理解:

例子一:简单类型

假设在propertes配置中有这样一个配置:com.didispace.foo=bar

我们为它创建对应的配置类:

@Data@ConfigurationProperties(prefix = "com.didispace")public class FooProperties {    private String foo;}

接下来,通过最新的Binder就可以这样来拿配置信息了:

@SpringBootApplicationpublic class Application {    public static void main(String[] args) {        ApplicationContext context = SpringApplication.run(Application.class, args);        Binder binder = Binder.get(context.getEnvironment());        // 绑定简单配置        FooProperties foo = binder.bind("com.didispace", Bindable.of(FooProperties.class)).get();        System.out.println(foo.getFoo());    }}

例子二:List类型

如果配置内容是List类型呢?比如:

com.didispace.post[0]=Why Spring Bootcom.didispace.post[1]=Why Spring Cloudcom.didispace.posts[0].title=Why Spring Bootcom.didispace.posts[0].content=It is perfect!com.didispace.posts[1].title=Why Spring Cloudcom.didispace.posts[1].content=It is perfect too!

要获取这些配置依然很简单,可以这样实现:

ApplicationContext context = SpringApplication.run(Application.class, args);Binder binder = Binder.get(context.getEnvironment());// 绑定List配置List<String> post = binder.bind("com.didispace.post", Bindable.listOf(String.class)).get();System.out.println(post);List<PostInfo> posts = binder.bind("com.didispace.posts", Bindable.listOf(PostInfo.class)).get();System.out.println(posts);

代码示例

本教程配套仓库:

如果您觉得本文不错,欢迎Star、Follow支持!您的关注是我坚持的动力!

基于Jenkins Pipeline自动化部署

mumupudding阅读(4)


微信公众号「后端进阶」,专注后端技术分享:Java、Golang、WEB框架、分布式中间件、服务治理等等。老司机倾囊相授,带你一路进阶,来不及解释了快上车!

最近在公司推行Docker Swarm集群的过程中,需要用到Jenkins来做自动化部署,Jenkins实现自动化部署有很多种方案,可以直接在jenkins页面写Job,把一些操作和脚本都通过页面设置,也可以在每个项目中直接写Pipeline脚本,但像我那么优秀,那么追求极致的程序员来说,这些方案都打动不了我那颗骚动的心,下面我会跟你们讲讲我是如何通过Pipeline脚本实现自动化部署方案的,并且实现多分支构建,还实现了所有项目共享一个Pipeline脚本。

使用Jenkins前的一些设置

为了快速搭建Jenkins,我这里使用Docker安装运行Jenkins:

$ sudo docker run -it -d \  --rm \  -u root \  -p 8080:8080 \  -v jenkins-data:/var/jenkins_home \  -v /var/run/docker.sock:/var/run/docker.sock \  -v "$HOME":/home \  --name jenkins jenkinsci/blueocean

初次使用jenkins,进入Jenkins页面前,需要密码验证,我们需要进入docker容器查看密码:

$ sudo docker exec -it jenkins /bin/bash$ vi /var/jenkins_home/secrets/initialAdminPassword

Docker安装的Jenkins稍微有那么一点缺陷,shell版本跟CenOS宿主机的版本不兼容,这时我们需要进入Jenkins容器手动设置shell:

$ sudo docker exec -it jenkins /bin/bash$ ln -sf /bin/bash /bin/sh

由于我们的Pipeline还需要在远程服务器执行任务,需要通过ssh连接,那么我们就需要在Jenkins里面生成ssh的公钥密钥:

$ sudo docker exec -it jenkins /bin/bash$ ssh-keygen -C "root@jenkins"

在远程节点的~/.ssh/authorized_keys中添加jenkins的公钥(id_rsa.pub)

还需要安装一些必要的插件:

  1. Pipeline Maven Integration
  2. SSH Pipeline Steps

安装完插件后,还需要去全局工具那里添加maven:

maven

这里后面Jenkinsfile有用到。

mutiBranch多分支构建

由于我们的开发是基于多分支开发,每个开发环境都对应有一条分支,所以普通的Pipeline自动化构建并不能满足现有的开发部署需求,所以我们需要使用Jenkins的mutiBranch Pipeline。

首先当然是新建一个mutiBranch多分支构建job:

maven

接着设置分支源,分支源就是你项目的git地址,选择Jenkinsfile在项目的路径

maven

接下来Jenkins会在分支源中扫描每个分支下的Jenkinsfile,如果该分支下有Jenkinsfile,那么就会创建一个分支Job

maven

该job下的分支job如下:

maven

这里需要注意的是,只有需要部署的分支,才加上Jenkinsfile,不然Jenkins会将其余分支也创建一个分支job。

通用化Pipeline脚本

到这里之前,基本就可以基于Pipeline脚本自动化部署了,但如果你是一个追求极致,不甘于平庸的程序员,你一定会想,随着项目的增多,Pipeline脚本不断增多,这会造成越来越大的维护成本,随着业务的增长,难免会在脚本中修改东西,这就会牵扯太多Pipeline脚本了,而且这些脚本基本都相同,那么对于我这么优秀的程序员,怎么会想不到这个问题呢,我第一时间就想到通用化pipeline脚本。所幸,Jenkins已经看出了我不断骚动的心了,Jenkins甩手就给我一个Shared Libraries。

Shared Libraries是什么呢?顾名思义,它就是一个共享库,它的主要作用是用于将通用的Pipeline脚本放在一个地方,其它项目可以从它那里获取到一个全局通用化的Pipeline脚本,项目之间通过不通的变量参数传递,达到通用化的目的。

接下来我们先创建一个用于存储通用Pipeline脚本的git仓库:

maven

仓库目录不是随便乱添加了,Jenkins有一个严格的规范,下面是官方说明:

maven

官方已经讲得很清楚了,大概意思就是vars目录用于存储通用Pipeline脚本,resources用于存储非Groovy文件。所以我这里就把Pipeline需要的构建脚本以及编排文件都集中放在这里,完全对业务工程师隐蔽,这样做的目的就是为了避免业务工程师不懂瞎几把乱改,导致出bug。

创建完git仓库后,我们还需要在jenkins的Manage Jenkins » Configure System » Global Pipeline Libraries中定义全局库:

maven

这里的name,可以在jenkinsfile中通过以下命令引用:

@Library 'objcoding-pipeline-library'

下面我们来看通用Pipeline脚本的编写规则:

#!groovydef getServer() {    def remote = [:]    remote.name = 'manager node'    remote.user = 'dev'    remote.host = "${REMOTE_HOST}"    remote.port = 22    remote.identityFile = '/root/.ssh/id_rsa'    remote.allowAnyHosts = true    return remote}def call(Map map) {    pipeline {        agent any        environment {            REMOTE_HOST = "${map.REMOTE_HOST}"            REPO_URL = "${map.REPO_URL}"            BRANCH_NAME = "${map.BRANCH_NAME}"            STACK_NAME = "${map.STACK_NAME}"            COMPOSE_FILE_NAME = "docker-compose-" + "${map.STACK_NAME}" + "-" + "${map.BRANCH_NAME}" + ".yml"        }        stages {            stage('获取代码') {                steps {                    git([url: "${REPO_URL}", branch: "${BRANCH_NAME}"])                }            }            stage('编译代码') {                steps {                    withMaven(maven: 'maven 3.6') {                        sh "mvn -U -am clean package -DskipTests"                    }                }            }            stage('构建镜像') {                steps {                    sh "wget -O build.sh https://git.x-vipay.com/docker/jenkins-pipeline-library/raw/master/resources/shell/build.sh"                    sh "sh build.sh ${BRANCH_NAME} "                }            }            stage('init-server') {                steps {                    script {                        server = getServer()                    }                }            }            stage('执行发版') {                steps {                    writeFile file: 'deploy.sh', text: "wget -O ${COMPOSE_FILE_NAME} " +                        " https://git.x-vipay.com/docker/jenkins-pipeline-library/raw/master/resources/docker-compose/${COMPOSE_FILE_NAME} \n" +                        "sudo docker stack deploy -c ${COMPOSE_FILE_NAME} ${STACK_NAME}"                    sshScript remote: server, script: "deploy.sh"                }            }        }    }}
  1. 由于我们需要在远程服务器执行任务,所以定义一个远程服务器的信息其中remote.identityFile就是我们上面在容器生成的密钥的地址;
  2. 定义一个call()方法,这个方法用于在各个项目的Jenkinsfile中调用,注意一定得叫call;
  3. 在call()方法中定义一个pipeline;
  4. environment参数即是可变通用参数,通过传递参数Map来给定值,该Map是从各个项目中定义的传参;
  5. 接下来就是一顿步骤操作啦,“编译代码”这步骤需要填写上面我们在全局工具类设置的maven,“构建镜像”的构建脚本巧妙地利用wget从本远程仓库中拉取下来,”执行发版“的编排文件也是这么做,“init-server”步骤主要是初始化一个server对象,供“执行发版使用”。

从脚本看出来Jenkins将来要推崇的一种思维:配置即代码。

写完通用Pipeline脚本后,接下来我们就需要在各个项目的需要自动化部署的分支的根目录下新建一个Jenkinsfile脚本了:

maven

接下来我来解释一下Jenkinsfile内容:

#!groovy// 在多分支构建下,严格规定Jenkinsfile只存在可以发版的分支上// 引用在jenkins已经全局定义好的librarylibrary 'objcoding-pipeline-library'def map = [:]// 远程管理节点地址(用于执行发版)map.put('REMOTE_HOST','xxx.xx.xx.xxx')// 项目gitlab代码地址map.put('REPO_URL','https://github.com/objcoding/docker-jenkins-pipeline-sample.git')// 分支名称map.put('BRANCH_NAME','master')// 服务栈名称map.put('STACK_NAME','vipay')// 调用library中var目录下的build.groovy脚本build(map)
  1. 通过library 'objcoding-pipeline-library'引用我们在Jenkins定义的全局库,定义一个map参数;
  2. 接下来就是将项目具体的参数保存到map中,调用build()方法传递给通用Pipeline脚本。

Shared Libraries共享库极大地提升了Pipeline脚本的通用性,避免了脚本过多带来的问题,也符合了一个优秀程序员的审美观,如果你是一个有追求的程序员,你一定会爱上它。

流程图:

maven

demo git 地址:

pipeline:https://github.com/objcoding/jenkins-pipeline-library

单项目部署:https://github.com/objcoding/docker-jenkins-pipeline-sample

多项目部署:https://github.com/objcoding/docker-jenkins-pipeline-sample2

公众号「后端进阶」,专注后端技术分享!

RPC的负载均衡策略

mumupudding阅读(7)

抽空自己写了个简易版的rpc框架,想了下怎么搞负载均衡,最简单的方式就是搞个配置文件放置服务地址,直接读配置文件,转而想到配置文件可以放zk,相当于用zk来做配置中心或者服务发现。优秀的dubbo项目就可以这么做,马上参考了下谷歌的grpc,发现了一篇谷歌很棒的文章,拜读了下(也借用了谷歌这篇文章的图片),很不错,想写一些我自己的见解。

传送门:https://grpc.io/blog/loadbalancing/

rpc通信本身并不复杂,只要定好协议怎么处理问题不大,但是负载均衡的策略是值得推敲的。

一般情况下,负载均衡的策略有以下两种

1. 代理服务

客户端并不知道服务端的存在,它所有的请求都打到代理服务,由代理服务去分发到服务端,并且实现公平的负载算法。客户机可能不可信,这种情况通过用户面向用户的服务,类似于我们的nginx将请求分发到后端机器。

缺点:客户端不知道后端的存在,且客户端不可信,延迟会更高且代理服务会影响服务本身的吞吐量

优点:在中间层做监控等拦截操作特别棒。

如图:

2. 客户端负载均衡

客户端知道有多个后端服务,由客户端去选择服务端,并且客户端可以从后端服务器中自己总结出一份负载的信息,实现负载均衡算法。这种方式最简单的实现就是我上面说的直接搞个配置文件,调用的时候随机或者轮询服务端即可。

如图:

优点:

高性能,因为消除了第三方的交互

缺点:

客户端会很复杂,因为客户端要跟踪服务器负载和健康状况,客户端实现负载均衡算法。多语言的实现和维护负担也很麻烦,且客户端需要被信任,得是靠谱的客户端。

以上是介绍了两种负载均衡的方案,下面要说的就是使用代理方式负载均衡的几种详细的方案

代理方式的负载均衡有很多种

代理负载平衡可以是 L3/L4(传输级别)L7(应用程序级别)

L3/L4 中,服务器终止TCP连接并打开另一个连接到所选的后端。

L7 只是在客户端连接到服务端端连接之间搞一个应用来做中间人。

L3/L4 级别的负载均衡按设计只做很少的处理,与L7级别的负载均衡相比的延迟会更少,而且更便宜,因为它消耗更少的资源。

在L7(应用程序级)负载平衡中,负载均衡服务终止并解析协议。负载均衡服务可以检查每个请求并根据请求内容分配后端。这就意味监控拦截等操作可以非常方便快捷的做在这里。

L3/L4 vs L7

正确的打开方式有一下几种

  1. 这些连接之间的RPC负载变化很大: 建议使用L7.
  2. 存储或计算相关性很重要 :建议使用L7,并使用cookie或类似的路由请求来纠正服务端.
  3. 设备资源少(缺钱): 建议使用 L3/L4.
  4. 对延迟要求很严格(就是要快): L3/L4.

下面要说的就是客户端实现负载均衡方式的详细方案:

1. 笨重的客户端

这就意味着客户端中实现负载平衡策略,客户端负责跟踪可用的服务器以及用于选择服务器的算法。客户端通常集成与其他基础设施(如服务发现、名称解析、配额管理等)通信的库,这就很复杂庞大了。

2. Lookaside 负载均衡 (旁观?)

旁观式负载平衡也称为外部负载平衡,使用后备负载平衡,负载平衡的各种功能智能的在一个单独的特殊的负载均衡服务中实现。客户端只需要查询这个旁观式的负载均衡服务, 这个服务就能给你最佳服务器的信息,然后你拿这个数据去请求那个服务端。就像我一开说的比如把服务端的信息注册到zk,由zk去做负载均衡的事情,客户端只需要去zk取服务端数据,拿到了服务端地址后,直接向服务端请求。

如图:

以上说了这么多,到底服务间的负载均衡应该用哪个,总结以下几点:

  1. 客户端和服务器之间非常高的流量,且客户端是可信的,建议使用‘笨重’的客户端 或者 Lookaside 负载均衡

  2. 传统设置——许多客户端连接到代理背后的大量服务,需要服务端和客户端之间有一个桥梁,建议使用代理式的负载均衡

  3. 微服务- N个客户端,数据中心有M个服务端,非常高的性能要求(低延迟,高流量),客户端可以不受信任,建议使用 Lookaside 负载均衡

分布式项目:根据工作时间设置对请求拦截并跳转

mumupudding阅读(5)


1 问题描述

如题目所示,即实现根据工作时间的设置,创建一个拦截器将用户操作限定在工作时间范围内.
逻辑本身不复杂,但由于项目使用了很多之前没接触过的技术栈,所以写起来有点缺乏信心.好在最后写完了,故将之总结下来.
先列举项目中涉及到的技术栈:

  • 前台: 组件vue,vuex,iview,axios
  • 后台(分布式): SpringBoot,redis

之后是根据技术栈,列举大致开发步骤:

  1. 创建拦截器,拦截请求
  2. 将工作时间的数据放到缓存中
  3. 逻辑判断:当前时间是否在(缓存中取到的)工作时间内
  4. (如果不在允许工作时间范围内)跳转至登录页面并提示

2 创建拦截器

由于此前没有创建过拦截器,在做这一步的时候还是很担心能否成功的.
好在这一步比想象中要更轻松,也是多些各位大佬的分享.大致而言,主要分两个步骤:

  1. 创建拦截器
@Componentpublic class WorkHourInterceptor implements HandlerInterceptor {    private static Logger logger= LoggerFactory.getLogger(WorkHourInterceptor.class);    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        // 如果不是映射到方法直接通过        if(!(handler instanceof HandlerMethod)){            return true;        }        logger.info("============================拦截器启动==============================");        request.setAttribute("starttime",System.currentTimeMillis());                //TODO 这里是逻辑代码,放在逻辑判断中说        return true;    }    @Override    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {        logger.info("===========================执行处理完毕=============================");        long starttime = (long) request.getAttribute("starttime");        request.removeAttribute("starttime");        long endtime = System.currentTimeMillis();        logger.info("============请求地址:/cf"+request.getRequestURI()+":处理时间:{}",(endtime-starttime)+"ms");    }    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {        logger.info("============================拦截器关闭==============================");    }}
  1. 创建拦截器配置
@Configurationpublic class WebConfigurer implements WebMvcConfigurer {    @Override    public void addInterceptors(InterceptorRegistry registry) {        //工作时间拦截器        registry.addInterceptor(workHourInterceptor()).addPathPatterns("/**");    }    @Bean    public WorkHourInterceptor workHourInterceptor(){        return new WorkHourInterceptor();    }}

对于SpringBoot而言,创建拦截器就是这么简单.
以下参考链接是我在查询相关链接时比较精华的,当然代码和以上是没有太大区别的.

参考链接:

  1. Spring Boot 优雅的配置拦截器方式
  2. SpringBoot 2.X配置登录拦截器
  3. Spring Boot 拦截器
  4. Spring Boot配置拦截器

3 将工作时间数据放到缓存中

对于redis在java中的使用,项目中使用的是二级缓存j2cache.
其具体的原理本文不再详述,这里只谈使用.(由于该代码与逻辑代码是放在一个类中的,故不再分开.其中可能涉及到一些在其他项目中没有必要追加的计算,会简要说明)

public class WorkHourUtil {    private static final String WORK_HOUR="WorkHour";    private static final String SERVICE_NAME="CfApi";    private static final String COMMON="Common";    public static boolean isWorkHourEmpty(String strCompanyId){        //工作时间缓存数据是否为空        return getCacheData(strCompanyId)==null;    }    /**     * Is in work hour boolean.     *     * @param strCompanyId the str company id     * @return the boolean     * @author YangYishe     * @date 2019年06月27日 16:08     */    static boolean isInWorkHour(String strCompanyId){        //是否在工作时间内(含获取数据过程)        List<WorkingHours> lstWh=getCacheData(strCompanyId);        return isInWorkHour(lstWh);    }    private static boolean isInWorkHour(List<WorkingHours> lstWorkHour){        //是否在工作时间内(仅需逻辑有关)        //此处的代码在下面的逻辑判断中有用到,不再额外贴出        if(lstWorkHour==null||lstWorkHour.size()==0){            return true;        }        Date mToday=new Date();        //此处获取到的星期是把周日当第1天算的        //hutool的DateUtil        int intWeekDay= DateUtil.dayOfWeek(mToday)-1;        if(intWeekDay<=0){            intWeekDay+=7;        }        DateTime mNow=DateUtil.parseTime(DateUtil.formatTime(mToday));        for(WorkingHours mWh:lstWorkHour){            if(mWh.getSort().equals(intWeekDay)){                boolean blnIsEnable=mWh.getDayState();                DateTime mStartTime=DateUtil.parseTime(mWh.getStartTime());                DateTime mEndTime=DateUtil.parseTime(mWh.getEndTime());                boolean blnAfterStart=mNow.isAfterOrEquals(mStartTime);                boolean blnBeforeEnd=mNow.isBeforeOrEquals(mEndTime);                return (!blnIsEnable)||(blnAfterStart&&blnBeforeEnd);            }        }        //此处一般不会触发,如果触发了,就需要检查代码看哪儿有问题了.        return false;    }    private static String getRegion(String strCompanyId){        //由于项目是分企业的,每个企业都有自己独自的工作时间缓存数据        return WORK_HOUR+"∥"+strCompanyId;    }    public static void setCacheData(String strCompanyId,List<WorkingHours> lstWh){        CacheChannel channel = J2Cache.getChannel();        String cacheKey = StrUtil.format("{}{}",SERVICE_NAME, COMMON);        String strRegion=getRegion(strCompanyId);        //不确定缓存能否保存WorkHours的结合,以防万一,这里直接转换成了字符串        //这里的JSON是FastJson,我目前用过最方便的JSON解析包        String strWhList=JSON.toJSONString(lstWh);        channel.set(strRegion,cacheKey,strWhList);    }    private static List<WorkingHours> getCacheData(String strCompanyId){        List<WorkingHours> lstWh=null;        CacheChannel channel = J2Cache.getChannel();        //这里的次级索引合并在其他项目中作用也不是很大,可只取一个字符串        String cacheKey = StrUtil.format("{}{}",SERVICE_NAME, COMMON);        Object object=channel.get(getRegion(strCompanyId),cacheKey).getValue();        if(ObjectUtil.isNotNull(object)){            String strWhList= (String) object;            lstWh=JSON.parseArray(strWhList,WorkingHours.class);        }        return lstWh;    }}

一二级缓存的名称应该是不需要讲究太多.

参考链接:

  1. J2Cache官方API
  2. java项目集成J2Cache

4 逻辑判断

这里首先要参考的是,获取当前时间是否在工作时间内,直接调用WorkHourUtil的isInWorkHour方法即可.
该方法要在拦截器中拦截.代码大致如下(以下方法写在WorkHourInterceptor类preHandle方法的TODO处):

List<String> lstUrlLogin= Arrays.asList("/login","/logout");//这里用lambda表达式判断当前地址是否为登录或登出地址boolean blnIsLogin=lstUrlLogin.stream().anyMatch(m->m.equals(request.getRequestURI()));if(!blnIsLogin){    //获取企业id(这里的getCompanyId是获取公司id的方法,其他项目可以不关注,或者有这里的获取逻辑)    String strCompanyId=getCompanyId(request);    //此处判断是否在工作时间    boolean blnIsInWorkHour=WorkHourUtil.isInWorkHour(strCompanyId);    if(!blnIsInWorkHour){        Map<String,Object> mapResult=new HashMap<>();        mapResult.put("logout",true);        mapResult.put("message","当前时间不允许使用系统!");        //注:returnJson是判断后的页面跳转,放在第5大点说        returnJson(response,JSON.toJSONString(mapResult));        return false;    }}

5 判断后的页面跳转

这里先贴上后台的returnJson方法(同样在WorkHourInterceptor中),用以表示请求被拦截:

private void returnJson(HttpServletResponse response, String json) throws Exception {    response.setCharacterEncoding("UTF-8");    response.setContentType("text/html; charset=utf-8");    try (PrintWriter writer = response.getWriter()) {        writer.print(json);    } catch (IOException e) {        logger.error("response error", e);    }}

不同于以前开发的页面和后台放在一起的项目,当前项目只有后台,发送回的请求也全部都是JSON数据,换言之,并不关联页面.
所以,想要判断后进行页面跳转,必须在前台项目中的拦截器中拦截该请求再进行跳转.(这里前台页面拦截器的代码很长,但相当多的部分与本文所述的要求并无直接关系,同时不方便删除,故保留,阅读时请注意甄别)
登录拦截js(需在axios.js中调用)

import store from '../../store/index.js'import router from '../../router/index.js'import { isEmpty } from '../../view/components/about-text/about-string'import { Message } from 'iview'/** * 登出拦截器(结果是登出的拦截器) * @param res */export const logoutInterceptor = (res) => {  // 判断,当返回结果中含有返回值提示应当跳转到logout页面时,则跳转logout页面  if (typeof res.data !== 'undefined') {    if (res.data.logout) {    //这里的handleLogOut是发送登出请求的mapAction中的方法,用以去除一些session和token数据      store.dispatch('handleLogOut').then(() => {        router.push({          name: 'login'        })        //登录页面的同时提示是由于何原因登出        if (!isEmpty(res.data.message)) {          Message.warning(res.data.message)        }      })    }  }}

前台拦截器(axios),注意loginInterceptor仅在相应拦截中调用了,其他地方均与本文要说的内容无关,只是为了避免个别人摸不清头脑所以把全部代码都写上了:

import axios from 'axios'import { getToken } from '@/libs/util'import { isEmpty } from '../view/components/about-text/about-string'import { logoutInterceptor } from '../api/system/login'let arrRequestUrl = {}const CancelToken = axios.CancelTokenclass HttpRequest {  constructor (baseUrl = baseURL) {    this.baseUrl = baseUrl    this.queue = {}  }  getInsideConfig () {    const config = {      baseURL: this.baseUrl,      headers: {        Authorization: getToken()      }    }    return config  }  destroy (url) {    delete this.queue[url]    if (!Object.keys(this.queue).length) {      // Spin.hide()    }  }  interceptors (instance, url) {    // 请求拦截    instance.interceptors.request.use(config => {      // 发起请求时,如果正在发送的请求中,已有对应的url,则驳回,否则记录      // 此处的get方法,不加拦截也ok,加之领导要求,于是改为get方法不加拦截.      if (arrRequestUrl[url] && config.method !== 'get') {        return Promise.reject(new Error('repeatSubmit'))      } else {        arrRequestUrl[url] = true      }      // 添加全局的loading...      if (!Object.keys(this.queue).length) {        // Spin.show() // 不建议开启,因为界面不友好      }      this.queue[url] = true      return config    }, error => {      return Promise.reject(error)    })    // 响应拦截    instance.interceptors.response.use(res => {      // 去掉正在发送请求的记录      delete arrRequestUrl[url]      // 结果是登出的拦截器!!!      logoutInterceptor(res)      this.destroy(url)      const { data, status } = res      return { data, status }    }, error => {      // 对重复提交的错误信息进行解析      if (error.message === 'repeatSubmit') {        throw new Error('请不要重复提交')      } else {        delete arrRequestUrl[url]      }      this.destroy(url)      let errorInfo = error.response      if (!errorInfo) {        const { request: { statusText, status }, config } = JSON.parse(JSON.stringify(error))        errorInfo = {          statusText,          status,          request: { responseURL: config.url }        }      }      // addErrorLog(errorInfo)      return Promise.reject(error)    })  }  request (options) {    const instance = axios.create()    options = Object.assign(this.getInsideConfig(), options)    this.interceptors(instance, options.url)    return instance(options)  }}export default HttpRequest

说明:

  1. 由于后台是分布式项目,所以对于每一个api项目都可能需要添加拦截器(WorkHourInterceptor)和拦截器(WebConfigurer)配置文件.
  2. 本文没有对设置缓存数据进行说明,在本项目中有两个对应场景,一是登录时判断有无当前公司的工作时间设置数据,如无则查询并将之放在缓存中,二是修改工作时间设置时,会覆盖本公司当前的工作时间设置缓存数据.

Monkey命令参数详解

mumupudding阅读(2)


什么是monkey

Monkey是Android中的一个命令行工具,可以运行在模拟器里或实际设备中。它向系统发送伪随机的用户事件流(如按键输入、触摸屏输入、手势输入等),实现对正在开发的应用程序进行压力测试.

基本语法

$ adb shell monkey [options]

如果不指定options,Monkey将以无反馈模式启动,并把事件任意发送到安装在目标环境中的全部应用程序

$ adb shell monkey -p package -v 500

指定对package这个应用程序进行monkey测试,并向其发送500个伪随机事件。其中 -p 表示对象包包,–v 表示反馈信息级别

命令参数

可以使用命令 adb shell monkey -help 查看命令参数

1、参数: -p

用于约束限制,用此参数指定一个或多个应用。指定应用之后,monkey将只允许系统启动指定的app;如果不指定应用,将允许系统启动设备中的所有应用

  • 指定一个应用: adb shell monkey -p com.ifeng.news2 100
  • 指定多个应用:adb shell monkey -p com.ifext.news –p com.ifeng.news2  100
  • 不指定应用:adb shell monkey 100
2、参数:-c

用于约束限制,用此参数指定了一个或几个类别,Monkey将只允许系统启动被这些类别中的某个类别列出的Activity。如果不指定任何类别,Monkey将选择下列类别中列出的Activity:Intent.CATEGORY.LAUNCHER 或 Intent.CATEGORY.MONKEY。要指定多个类别,需要使用多个-c选项,每个-c选项只能用于一个类别。

3、参数:-v

用于指定反馈信息级别(信息级别就是日志的详细程度),总共分3个级别:

  • 默认级别 Level 0:-v
    • adb shell monkey -p com.ifeng.news2 –v 100:说明仅提供启动提示、测试完成和最终结果等少量信息
  • 日志级别 Level 1:-v -v
    • adb shell monkey -p com.ifeng.news2 –v -v 100:说明提供较为详细的日志,包括每个发送到Activity的事件信息
  • 日志级别 Level 2:-v -v -v
    • adb shell monkey -p com.ifeng.news2 –v -v –v 100:说明最详细的日志,包括了测试中选中/未选中的Activity信息
4、参数: -s

伪随机数生成器的seed值。如果用相同的seed值再次运行Monkey,它将生成相同的事件序列

  • Monkey 测试1:adb shell monkey -p com.ifeng.news2 -s 10  100
  • Monkey 测试2:adb shell monkey -p com.ifeng.news2 –s 10 100

两次测试的效果是相同的,因为模拟的用户操作序列(每次操作按照一定的先后顺序所组成的一系列操作,即一个序列)是一样的。

5、参数: –throttle<毫秒>

用于指定用户操作(即事件)间的延时,单位是毫秒

adb shell monkey -p com.ifeng.news2 --throttle 5000 100
6、参数: –ignore-crashes

用于指定当应用程序崩溃时(Force& Close错误),Monkey是否停止运行。如果使用此参数,即使应用程序崩溃,Monkey依然会发送事件,直到事件计数完成。

  • adb shellmonkey -p com.ifeng.news2 –ignore-crashes 1000测试过程中即使程序崩溃,Monkey依然会继续发送事件直到事件数目达到1000为止
  • adb shellmonkey -p com.ifeng.news2 1000测试过程中,如果acg程序崩溃,Monkey将会停止运行
7、参数: –ignore-timeouts

用于指定当应用程序发生ANR(Application No Responding)错误时,Monkey是否停止运行。如果使用此参数,即使应用程序发生ANR错误,Monkey依然会发送事件,直到事件计数完成。

adb shellmonkey -p com.ifeng.news2--ignore-timeouts 1000
8、参数: –ignore-security-exceptions

用于指定当应用程序发生许可错误时(如证书许可,网络许可等),Monkey是否停止运行。如果使用此参数,即使应用程序发生许可错误,Monkey依然会发送事件,直到事件计数完成。

adb shellmonkey -p com.ifeng.news2 --ignore-security-exception 1000
9、参数: –kill-process-after-error

用于指定当应用程序发生错误时,是否停止其运行。如果指定此参数,当应用程序发生错误时,应用程序停止运行并保持在当前状态(注意:应用程序仅是静止在发生错误时的状态,系统并不会结束该应用程序的进程)

adb shellmonkey -p cn.emoney.acg --kill-process-after-error 1000
10、参数: –monitor-native-crashes

用于指定是否监视并报告应用程序发生崩溃的本地代码。

adb shellmonkey -p cn.emoney.acg --monitor-native-crashes 1000
11、参数: –pct-{+事件类别}{+事件类别百分比}

用于指定每种类别事件的数目百分比(在Monkey事件序列中,该类事件数目占总事件数目的百分比)

  • –pct-touch{+百分比}:调整触摸事件的百分比(触摸事件是一个down-up事件,它发生在屏幕上的某单一位置)

    adb shell monkey -p com.ifeng.news2 --pct-touch 10 1000
  • –pct-motion {+百分比}:调整动作事件的百分比(动作事件由屏幕上某处的一个down事件、一系列的伪随件机事和一个up事件组成)

    adb shell monkey -p com.ifeng.news2 --   pct-motion 20 1000
  • –pct-trackball {+百分比}:调整轨迹事件的百分比(轨迹事件由一个或几个随机的移动组成,有时还伴随有点击)

    adb shell monkey -p com.ifeng.news2 --pct-trackball 30 1000
  • –pct-nav {+百分比}:调整“基本”导航事件的百分比(导航事件由来自方向输入设备的up/down/left/right组成)

    adb shell monkey -p com.ifeng.news2 --pct-nav 40 1000
  • –pct-majornav {+百分比}:调整“主要”导航事件的百分比(这些导航事件通常引发图形界面中的动作,如:5-way键盘的中间按键、回退按键、菜单按键)

    adb shell monkey -p com.ifeng.news2 --pct-majornav 50 1000
日志输出输出日志的方法:

C:\Documents and Settings\Administrator>adb shell monkey -p 包名 -v 300  >D:\log.txt

使用Active-Choices-Plugin插件将十个Job合成一个

mumupudding阅读(6)

本文首发于:Jenkins 中文社区

作者:eryajf

现在Spring Cloud越来越火爆,许多公司也都在如火如荼投入使用中,而微服务最大的一个特点,就是多,同一大项目之下,可能会被拆分成十几二十几个子服务,对于运维而言,可能也需要一个对应一个地在Jenkins中创建十几二十几个Job。

刚刚还在一个博主的自我介绍里看到这样一句话:喜欢一切优雅的运维方式···

于是,我一直在想着,通过一些合理的参数变幻,从而将刚刚提到的十几二十几个服务,汇集到一个Job当中就能完成。

1,环境说明

  • 主机:CentOS-7.5
  • Jdk:jdk1.8.0_192
  • Tomcat:8
  • Jenkins:2.177

如上环境的准备工作本文就不赘述了。

2,安装插件。

3,使用前介绍。

插件安装之后,可以在项目配置中的参数化配置中看到一些新增了的选项。

  • 1,Active Choices Parameter(主动选择参数)Active Choices参数使用Groovy脚本或Scriptler目录中的脚本动态生成构建参数的值选项列表。
  • 2,Active Choices Reactive Parameter(主动选择反应参数)根据主动选择参数的选项而提供不同的对应值或者列表选项。
  • 3,Active Choices Reactive Reference Parameter(主动选择反应参考参数)根据主动选择参数的选项而展示对应参数的一些说明,与第二项的区别在于本参数只作为说明信息,而不能够作为变量往下传递。

可能刚刚这些说明都比较抽象,接下来容我通过项目实战,来对其进行一一解析。

4,配置前分析。

优秀的插件,它的优秀之处,往往是需要我们结合生产实际,开动聪明的大脑,打破常规使用套路来成就的。

因此,如何才能更好地应用插件的优秀功能,需要我们先对项目进行分析,从全局的眼光,判断项目前后该配置什么样的参数来进行联动。

这里我说明一下我准备的实验项目情况,为了简便测试,我这里仅使用两个项目来进行举例,聪明的你也一定能够通过案例进行举一反三,将二合一推广为十合一的。

两个项目的情况如下:

1,eureka

2,user

从刚刚这些配置里边可以看出,有不少共同点,也有不少不同点,我们只需把眼光放在不同点上,通过一些统一的变量控制,就能达到二合一的目的。

另外说明一点,这个项目已经部署在k8s环境当中,因此我的脚本内容也就展示成了k8s项目部署的流程了。

5,创建项目。

首先创建一个自由风格的Jenkins项目,然后配置一下项目构建保存历史。

6,字符参数。

添加一个名为branch的字符参数以用于控制代码分支的变量。

7,选项参数。

添加一个名为mode的选项参数以用于控制部署或者回滚的变量。

8,选择参数。

1,主动选择参数

首先添加一个主动选择参数,用于控制项目名称这个变量。

  • Name:project
  • Groovy Script:
return["eureka","user"]
  • Description:选择对应的应用名称部署对应的应用。
  • Choice Type:Radio Buttons

2,主动选择反应参数

接着添加一个主动选择反应参数,用于控制项目类型这个变量。

Name:type

  • Groovy Script:
A=["server"]B=["service"]if(project.equals("eureka")){return A} else if(project.equals("user")){return B}
  • Description:跟随项目的选择自动弹出对应类型
  • Choice Type:Single Select
  • Referenced parameters:project

3,主动选择反应参数

然后再添加一个主动选择反应参数,用于控制项目端口这个变量。

  • Name:port
  • Groovy Script:
if(project.equals("eureka")){return ["8761"]} else if (project.equals("user")){return ["6666"]}
  • Description:跟随跟随项目的选择自动弹出对应端口
  • Choice Type:Single Select
  • Referenced parameters:project

这样,对应的参数都创建完毕了,大概有以下几个小的注意点需要注意:

  • 1,参数的名称将是整个构建流程使用的一个变量,因此起名的时候需要注意。
  • 2,创建了一个主动选择参数,和两个主动选择反应参数,是因为我们的实际需求需要两个真实有效的参数,如果最后的port项选择了主动选择反应参考参数,那么到后边是无法显式使用的。
  • 3,注意后两个跟随参数中的Referenced parameters,都需要填写主动参数的名称,才能够前后贯通,实现联动。

9,Git地址配置。

接着就该添加Git地址了,同样,这个地方也应该合理利用项目标准化的优势,合理应用变量来进行配置。

具体如下图所示:

10,执行脚本。

接下来就该通过脚本来完成构建的主要流程了。

#!/bin/bashsource /etc/profile###set color##echoRed() { echo $'\e[0;31m'"$1"$'\e[0m'; }echoGreen() { echo $'\e[0;32m'"$1"$'\e[0m'; }echoYellow() { echo $'\e[0;33m'"$1"$'\e[0m'; }##set color###version=`date +%Y%m%d%H%M%S`echo -------------------------------------# 克隆项目并编译echoGreen "开始进行mvn编译!"cd  $WORKSPACE && mvn clean install -DskipTests=true[ $? != 0 ] && echoRed "请注意,在执行mvn编译时出错,故而退出构建,需开发同学自检代码!" && exit 1cd  $WORKSPACE/target/ && mv ishangjie-$project-$type-1.0.0.jar app.jar# 创建docker镜像cat > run.sh << EOF#!/bin/bashsource /etc/profilejava -jar /opt/app.jar --spring.profiles.active=test1EOFchmod +x run.shcat >Dockerfile << EOFFROM 192.168.10.1/public/jdk:1.8MAINTAINER eryajf <liqilong@edspay.com>ENV LANG en_US.UTF-8ADD   app.jar /opt/app.jarADD   run.sh  /EXPOSE $portENTRYPOINT [ "sh", "-c", "/run.sh" ]EOF# 构建镜像echoGreen "开始构建当次镜像!"docker build -t 192.168.10.1/isj/$project:$version .[ $? != 0 ] && echoRed "请注意,在执行镜像构建时出错,故而退出构建,请联系运维同学处理!" && exit 1# 上传到docker私服echoGreen "开始将镜像push到私服!"docker push 192.168.10.1/isj/$project:$version[ $? != 0 ] && echoRed "请注意,在执行镜像上传时出错,故而退出构建,请联系运维同学处理!" && exit 1docker rmi 192.168.10.1/isj/$project:$version#更新镜像echoGreen "开始将最新镜像部署到远端!"rancher kubectl set image deployment/isj-$project isj-$project=192.168.10.1/isj/$project:$version -n isj-wfw[ $? != 0 ] && echoRed "请注意,在执行镜像更新时出错,故而退出构建,请联系运维同学处理!" && exit 1echoGreen "部署完成!"

针对这个脚本有几点简单说明:

  • 1,因为应用到了颜色输出,因此记得在构建环境当中开启color颜色输出。
  • 2,尽量在关键地方添加一下判断,然后输出明了的内容以提高生产效率,比如编译有问题,直接退出构建,输出开发自检,如果是后边构建问题,同样退出构建,输出联系运维解决。
  • 3,巧用cat的EOF特性,从而也可以将不同的变量控制进来。
  • 4,尽量将所有构建过程的内容都写到Jenkins这里来,以便于后期问题排查与分析。
  • 5,因为这是实验,因此没有添加回滚功能,如果添加的话,就针对mode参数做一个判断即可。

11,构建后操作。

因为是多个项目在同一个WORKSPACE下工作,因此,为了避免出现不可预知问题,这里添加了构建后清空WORKSPACE的选项。

12,效果展示。

一切配置完成之后,就可以尝试一下点击构建了。

好了,这就是本期的分享,当然,关于这个插件,这里也只是介绍了其中一种一个思路,可能还有很多种其他的方案,期待各位发挥思维发掘更多妙用。

高性能MySQL06-查询优化(慢查询)

mumupudding阅读(2)

一、分析原因

SQL语句慢查询的原因有多种,如:
1)数据方面:
需要查询的表数据量太大导致性能下降;
是否向数据库请求了不需要的数据行或数据列;
MySQL是否在扫描额外的记录

2)SQL语句太过于冗余

3)等

下面我们列出一下分析SQL查询慢的一些方法:

1、记录慢查询日志

分析查询日志,不要直接打开慢查询日志进行分析,这样比较浪费时间和精力,可以使用pt-query-digest工具进行分析。

2、使用show profile
使用步骤:

1)开启,服务器上执行的所有语句会检测消耗的时间,存到临时表中
set profiling = 1;
2)进行需要分析的SQL查询
...
3)show profiles
4)show profile for query 临时表id

如:

mysql> set profiling = 1;
Query OK, 0 rows affected (0.00 sec)

mysql> show profiles;
Empty set (0.00 sec)

mysql> select * from a;
+------+--------+
| id   | name   |
+------+--------+
|    1 | nosee  |
|    2 | chan   |
|    3 | cheese |
|    4 | xyz    |
+------+--------+
4 rows in set (0.00 sec)

mysql> show profiles;
+----------+------------+-----------------+
| Query_ID | Duration   | Query           |
+----------+------------+-----------------+
|        1 | 0.00054875 | select * from a |
+----------+------------+-----------------+
1 row in set (0.00 sec)

mysql> show profile for query 1;
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000063 |
| checking permissions | 0.000013 |
| Opening tables       | 0.000050 |
| System lock          | 0.000018 |
| init                 | 0.000023 |
| optimizing           | 0.000007 |
| statistics           | 0.000018 |
| preparing            | 0.000010 |
| executing            | 0.000004 |
| Sending data         | 0.000104 |
| end                  | 0.000006 |
| query end            | 0.000004 |
| closing tables       | 0.000008 |
| freeing items        | 0.000196 |
| logging slow query   | 0.000022 |
| cleaning up          | 0.000006 |
+----------------------+----------+
16 rows in set (0.00 sec)

3、使用show status

show status会返回一些计数器,show global status 查看服务器级别的所有计数。有时根据这些计数,可以猜测出哪些操作代价较高或消耗时间多。

4、使用explain

explain命令用于分析单条SQL语句,是查看优化器如何决定执行查询的主要方法。

如:

mysql> explain select id from a where id =3\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: a
         type: ref
possible_keys: id
          key: id
      key_len: 5
          ref: const
         rows: 1
        Extra: Using where; Using index
1 row in set (0.00 sec)

关于EXPLAIN更详细的内容请查看下一篇文章。

二、查询执行原理

当希望MySQL能够以更高的性能运行查询时,最好的办法就是弄清楚MySQL是如何优化和执行查询的。

当我们向MySQL发送一个请求时,MySQL到底做了些什么,下面我们通过一个简单的图解来进行分析:

高性能MySQL06-查询优化(慢查询)

1)客户端发送一条查询给服务器。
2)服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果,否则进入下一阶段。
3)服务器进行SQL解析、预处理,再由优化器生成对应的执行计划。
4)MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询。
5)将结果返回给客户端。

上面的每一步都比想象的要复杂,这里将不深入讨论。

三、优化查询过程中的数据访问

1、定位问题

访问数据太多导致查询性能下降。

确定应用程序是否在检索大量超过需要的数据,可能是太多行或列。确认MySQL服务器是否在分析大量不必要的数据行。

2、避免使用如下SQL语句

1)查询不需要的数据,使用limit解决
2)多表关联返回全部列,指定如A.id, A.name, B.age
3)总是取出全部列,SELECT *会让优化器无法完成索引覆盖扫描的优化
4)重复查询相同的数据,可以缓存数据,下次直接读取缓存

3、是否在扫描额外的记录

使用explain来进行分析,如果发现查询需要扫描大量的数据但只返回少数的行,可以通过如下技巧去优化:
1)使用索引覆盖扫描,把所有用的列都放到索引中,这样存储引擎不需要回表获取对应行就可以返回结果
2)改变数据库和表的结构,修改数据表范式
3)重写SQL语句,让优化器可以以更优的方式执行查询

四、优化长难的查询语句

MySQL内部每秒能扫描内存中上百万行数据,相比之下,响应数据给客户端就要慢得多。使用尽可能少的查询是最好的,但有时将一个大的查询分解为多个小的查询也是很有必要的。

1、切分查询

将一个大的查询分为多个小的相同的查询。如一次性删除1000万的数据,要比一次删除1万暂停一会的方案更加损耗服务器开销。

2、分解关联查询

1)可以将一条关联语句分解成多条SQL来执行
2)执行单个查询可以减少锁的竞争
3)在应用层做关联可以更容易对数据库进行拆分

五、优化特定类型的查询语句

1、优化count()查询

1)count(*)中的*会忽略所有的列,直接统计所有列数,因此不要使用count(列名)。MyISAM中,没有任何WHERE条件的count(*)非常快。
2)可以使用explain查询近似值,用近似值替代count(*)
3)增加汇总表
4)使用缓存

2、优化关联查询

1)确定ON或USING子句的列上有索引
2)确保GROUP BY和ORDER BY中只有一个表中的列,这样MySQL才有可能使用索引

3、优化子查询

尽可能使用关联查询来替代

4、优化GROUP BY和DISTINCY

1)这两种查询均可使用索引来优化,是最有效的优化方法
2)关联查询中,使用标识列进行分组的效率会更高
3)如果不需要ORDER BY,进行GROUP BY时使用ORDER BY NULL,MySQL不会进行文件排序
4)WITH ROLLUP超级聚合,可以挪到应用程序处理

5、优化LIMIT分页

LIMIT偏移量大的时候,查询效率较低。可以记录上次查询的最大ID,下次查询时直接根据该ID来查询。

6、优化UNION查询

UNION ALL的效率高于UNION。

经典实例

1、请简述项目中优化SQL语句执行效率的方法,从哪些方面,SQL语句性能如何分析?

考官考点:
1)查找分析查询速度慢的原因
2)优化查询过程中的数据访问
3)优化长难的查询语句
4)优化特定类型的查询语句

对于此类问题,先说明如何定位低效率SQL语句,然后根据SQL语句可能低效的原因做排查,先从索引着手,如果索引没问题,考虑以上几个方面:数据访问的问题、长难查询句的问题、还是一些特定类型优化的问题,逐步排除。

觉得不错请点赞支持,欢迎留言或进我的个人群855801563领取【架构资料专题目合集90期】、【BATJTMD大厂JAVA面试真题1000+】,本群专用于学习交流技术、分享面试机会,拒绝广告,我也会在群内不定期答题、探讨

 

2019年互联网架构设计:高性能的后端

mumupudding阅读(4)

先简略回顾一下。对于互联网产品的高性能架构设计通常包括以下几个大方面:

  1. Web浏览器高性能设计
  2. App客户端高性能设计
  3. 高性能的网络和硬件
  4. 后台服务高性能设计

2019年互联网架构设计:高性能的后端

后端服务一般指用户直接看到的远程服务,涉及到网络硬件、逻辑计算、通信协议和数据存储等部分。下面我们将着重介绍高性能后台服务的设计方法和策略。

一、高性能的网络和硬件

  1. 网络硬件是提供实现高性能服务的先决条件,如果网络硬件失败,再优秀的团队也是“巧妇难为无米之炊”。互联网产品在网络硬件方面经常需要使用的高性能方案有如下几种:
  2. CDN加速技术。CDN加速将网站的内容缓存在网络边缘(离用户接入网络最近的地方),然后在用户访问网站内容的时候,通过调度系统将用户的请求路由或者引导到离用户接入网络最近或者访问效果最佳的缓存服务器上,由该缓存服务器为用户提供内容服务;相对于直接访问源站,这种方式缩短了用户和内容之间的网络距离,从而达到加速的效果。
  3. 足够的带宽。带宽应该满足在网站峰值的情况还能足够快速的使用,所以网络带宽应该大于峰值流量 = 峰值QPS * 平均请求大小。只有在保证带宽的情况才能实现高性能服务。
  4. 服务器性能。服务器性能主要从CPU、内存和磁盘三个方面来考虑,CPU核心数量能尽量多点,内存大小最好大一点,利用到磁盘存储的话SSD会优于机械磁盘。
  5. 硬件负载均衡设备。对于有条件的团队可以采购硬件负载均衡设备,加强后台服务负载均衡的能力,比如F5。

二、后台服务高性能设计

后台服务的高性能设计是互联网产品高性能架构设计中最重要的一环,对服务整体性能起到决定性的作用。我们来看看设计高性能后台服务的方法:

1分布式缓存。

缓存的本质是通过key-value形式的Hash表提升读写速度,一般情况是O(1)的读写速度。读写量比较高,变化量不大的数据比较适合使用缓存。业内比较成熟的分布式缓存系统有redis/memcache。在此我向大家推荐一个架构学习交流圈。交流学习企鹅圈号:948368769 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

一般的缓存设计架构如下:用户第一次请求应用程序时,通过存储服务直接读取数据,然后将数据存储到缓存系统去,用户第二次请求的时候就直接从缓存系统读取,从而提升读取速度。

2019年互联网架构设计:高性能的后端

对于分布式缓存系统可以Set化部署,比如商品数据缓存到Set1,用户数据缓存到Set2,或者一类用户的数据缓存到Set1,另一类的用户缓存到Set2,如下图:

 

2019年互联网架构设计:高性能的后端

此外,也可以按集群化部署,每一个缓存服务存储的数据都是对等的,可以对外提供同等的服务,所以外部请求需要负载均衡到不同有缓存服务器,如下图:

 

2019年互联网架构设计:高性能的后端

Set化部署的目的主要在于将不同类型的数据路由到不同的地方,好处就是可以减少不同业务数据的耦合,可以针对不同业务进行不同的优化,从而提升整体性能。集群式部署的目的在于,提高缓存系统的对外服务能力,上层业务的路由策略简单灵活,扩缩容比较容易。

2.服务分层

在经典的三层(接入层、逻辑层和存储层)后台服务架构中,三层的划分的原则就是同层次的系统专注处理自己的事情。接入层专注于处理前端和后台服务的接入连通、安全认证和数据转发。逻辑层专注于处理不同业务的无状态逻辑服务。存储层专注于处理业务数据的存储。这样分层的好处在于各个层次能够依据业务特点专注于自己的事情,提高系统复用性,降低业务间的耦合性。在中小型网站中三层架构的典型实现是Nginx(接入层)、Apache Web(逻辑层)、Mysql/Redis(存储层)。

2019年互联网架构设计:高性能的后端

3.操作异步化

目前大型系统中普遍消息队列来将调用异步化,不仅可以提升系统性能还可以提升系统的扩展性。对于大量的数据库写请求,对于数据库的压力很大,同时也会造成数据库响应不及时。可以考虑使用消息队列,数据库的写请求可以直接写入到消息队列,然后通过多线程或多进程从消息队列中读取数据慢慢写入到数据库。消息队列服务器的处理速度会远远快于数据库,所以用户在写入操作时会感觉到很快的写入速度。

此外,消息队列对于请求不均衡的系统,还具有削峰填谷的作用,将短时间内的高峰请求,逐步平摊到更长的时间里去,从而避免短时间内大量请求压跨系统。

 

2019年互联网架构设计:高性能的后端

4.服务拆分

服务拆分有多种说法,比如大系统小做,分布式拆分,分层结构以及目前很流行的微服务化。不过服务拆分一般来说有以下原则:

  • a.高内聚、低耦合: 将耦合性低的业务逻辑划分为不同系统,将聚合性高的业务逻辑划分为同一个系统。
  • b.单一职责原则:对于一个层次或者一个模块应该保持相对单一的职责,专注于自己的服务。
  • c.故障隔离:不同系统必须相对独立设计和运行,能够独立处理自己的故障,而不至于影响全局。
  • d.独立运维和持续交互:对于不同的系统可以随时迭代更新,而不至于影响其他服务。

对于服务拆分主要有纵向拆分和水平拆分两种方法。三层架构就是典型的纵向拆分模式,第2)点有所阐述。对于不同的业务模块,针对业务逻辑和存储服务可以按水平拆分的方法将拆分为不同的系统。比如商品系统逻辑层、订单系统逻辑层、商品系统存储层、订单系统存储层。在此我向大家推荐一个架构学习交流圈。交流学习企鹅圈号:948368769 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

5分布式集群化

分布式集群化是指将不同的业务用集群化的方式部署到不同的机器上去,对于每一个业务都具备大规模集群化的能力,从而提升系统的扩展性和高性能。

 

2019年互联网架构设计:高性能的后端

对于无状态化的被调服务A,在基于负载均衡的技术下,可以通过集群化部署成倍的提升服务性能,比如A1服务的性能是1万请求每秒,那么部署3台A服务机器,那么A服务的性能就是3万请求每秒了。

6.代码优化

对于IO操作的请求可以采用基于状态机的异步化编程。如下图的请求需要业务系统调用三个接口才能返回响应数据,当业务系统收到请求时,将给该请求分配一个seqid,该seqid在接口响应中也应该原封返回,然后并发三个IO接口的请求包,将该seqid对应的请求上下文保存到timer中,然后去并发处理其他请求,从而极大的提升系统性能。

 

2019年互联网架构设计:高性能的后端

此外,高性能的编程模型还有多线程模型、多进程模型、多协程模型和事件驱动模型。

对于数据结构的设计可以采用高效的数据结构,比如典型的key-value缓存系统就是基于hash的基本原理来实现的,hash表的查询效率是O(1),效率极快。

如何打造VUCA时代的敏捷型组织?

mumupudding阅读(2)

e3e90777aae5c0ac7693da9292213437.jpg

王明兰 –原华为、微软创新与转型教练、华为云SaaS产品总监,著名精益&敏捷转型专家

VUCA最早来源于冷战时期,在现代世界意指商业世界越来越不确定性,越来越易变,越来越不可预测,我们已经进入到了VUCA时代。

屏幕快照 2019-06-25 上午10.22.38.png

我们再也不能用原来的那种传统的、计划驱动的方式来工作,因为时代的不确定性,所以大家要拥抱变化。敏捷本身也是在那样一个时代背景下,越来越发展壮大。如果倒回来很多年之前,敏捷不会发展壮大,因为我们还没有进入这样的时代。

以前,企业的生存周期比较长,但是现在企业的生存周期越来越短,从前的几十年、上百年,现在是十年,十五年。不确定性越发严重,所以企业不断的探索怎么能够让自己更加适应市场的变化,适应这个时代,这是很多企业都在尝试敏捷转型背后的驱动力。

屏幕快照 2019-06-25 上午10.26.57.png

这本书大家知道是什么吗? TEAM OF TEAMS ,这本书是很火的畅销书,叫 赋能

在我们看来这本书的内容和名字,实际上讲的就是大规模组织里面怎么能够提升组织的适应能力。赋能讲的大家都知道,在伊拉克战争期间,美国军队怎么改变自己来应对恐怖组织的袭击。我总结了整个书里面介绍了美国军队做了哪几项改变:

1-从集中化军事集团变为小规模特种部队作战单元;2-高度可视化的一些战形迅速做出科学决策;3-持续的反馈和调整

这三点实际上 不仅适用于战争 ,这本书 成为畅销书并不是因为描写战争 ,而是 他在商业领域非常通用

我在很多公司做过敏捷咨询,发现不同的企业里对敏捷转型的迫切性是不一样的,有的企业非常迫切,有的企业还是属于无所谓,有的企业觉得想,但是又不敢。

屏幕快照 2019-06-25 上午10.43.12.png

什么样的企业会更加迫切呢? 有这样一个调查,如果企业的经济环境越来越不确定,这种企业它对转型就更加迫切。比如通讯业:像诺基亚、诺基亚西门子这种通信行业的企业做敏捷非常早,华为虽然不怎么讲,但是在中国的民企里华为做敏捷是最早的一个,华为从08年就在做,默默无闻,因为华为不鼓励员工出来说。

此外,银行业里基本所有的国有大行都在尝试敏捷转型。为什么要转型呢?也是一样地面临业务的压力。

那到底什么样的组织才能称为一个敏捷型组织呢?

屏幕快照 2019-06-25 上午10.46.59.png

我总结了敏捷型组织需要三力:

第一个快速的响应力;第二个强大的执行力;第三个是需要持续的创新力。

光说三力挺容易的事情,同时具备三力,具备极致的情况下的企业非常非常少。

屏幕快照 2019-06-25 上午10.56.13.png

快速的响应力

快速的响应力需要一家企业它的组织结构需要能够快速响应。

什么样的组织机构需要快速响应?如果做一件事情都要层层去审批汇报,然后到最顶层就是审批下来才能往下下达,这种企业,它的响应力一定不会快。

敏捷型的组织是什么样的?敏捷型组织倡导的是端到端的价值链的打通。 比如我我在一些银行企业里做咨询,银行里的研发团队想在手机银行里面上线一个小功能,这本身一个很简单的事情,一般来说一个团队就搞定的事情,但是他们不一样,上线一个功能需要拉通七、八个团队,七、八个团队里又跨越了三、四个部门,上线这个功能,需要两个月的时间。

如果对于一个创业公司来说,非常快就上线了,但是在银行这种组织里就做不到。做不到并不是因为人的能力差,也不是因为技术差,就是因为 组织结构太复杂。做任何一件事情,都需要跨越其他的团队,甚至是其他的部门去协同才能完成。敏捷组织需要的是把这些组织结构打通,构建为全功能的团队,让这些团队能够向一个创业型团队一样具备高速的响应力。

因此, 建设敏捷型组织的结构,就是要把组织结构扁平化,打破壁垒,让每个团队都像创业团队一样

屏幕快照 2019-06-25 上午11.01.23.png

Scrum团队本质上就是最小的敏捷型组织单元 ,Scrum团队每个成员他们构成一个团队之后,就不再依赖于其他团队,就能够交付一个端到端的,这是Scrum的本质之一。

屏幕快照 2019-06-25 上午11.01.47.png

Scrum的发明人 Jeff Sutherland 在这两年新出了一本书叫  《敏捷革命》  ,这本书出的意义就是它描述了Scrum在IT以外的行业里的应用,这点是蛮有突破性的。敏捷虽然发展这么多年,大家以为只是在软件开发领域,没有想过在软件开发以外还可以应用。但是Jeff Sutherland在这本书里介绍了他将Scrum应用在各种各样的行业,包括联合国、政府、电视台等等没有任何软件开发的工作里。

Scrum的本质并不是和软件开发相关的,它的本质前面列的《赋能》里的三点一样:

1-跨职能的团队 2-本质是小循环3-早失败

acb50083-6c2d-4c65-8454-7a6e3bd53a22.png

强大的执行力

执行力这个话题已经谈了很多年了,历史上敏捷跟执行力没什么关系,敏捷不要什么命令控制,好像对执行力感觉是自上而下的。

我之前也是这样想的,也是这样理解的,但是这几年我一些企业的研发以外的部门做咨询,发现这些部门的领导是非常希望有方法帮助他们来去构建执行力的,但是他们没有这样一个有效的方法。我发现敏捷其实是一个非常好的方法帮助他们打造执行力。

屏幕快照 2019-06-25 上午11.54.34.png

这个畅销书叫 《执行》 ,虽然没有提到任何敏捷的理念,但是里面讲的执行的方法,都很有敏捷的思想,这点是蛮让我吃惊的。比如讲的领导需要具备的七条基本行为:

第一、条了解企业和员工第二、坚持以事实为基础第三、确立明确目标和实现目标的先后顺序第四、跟进第五、对执行进行奖励第六、提高员工能力和素质第七、了解你自己

屏幕快照 2019-06-25 下午1.48.40.png

持续的创新力

屏幕快照 2019-06-25 下午1.51.06.png

从组织来说,最核心的就是最大的动力就是危机感,因为一个组织如果没有危机感就没有动力去创新,华为为什么有持续的创新的动力?因为它太有危机感了。

光有危机感还不够,企业需要一套机制融合了自上向下和自下而上来构建持续的创新力,OKR正是提供了这套机制

屏幕快照 2019-06-25 下午1.52.41.png

OKR机制让组织从体制上拥抱创新机制,自下向上。

我觉得OKR整套体现了敏捷的原则,它相当于是敏捷原则在HR领域里面探索出了一套方法,但是又跟敏捷这个词又没有什么关系,这是很奇怪的。因为它允许组织它可以确定除了设定不确定目标之外,还可以自上而下反馈自己的目标。

屏幕快照 2019-06-25 下午1.54.05.png

最后总结,敏捷组织需要构建的三力是:快速的响应力,强大的执行力,持续的创新力。光有响应力没有执行力,这样组织就会想做的很多但是没有办法落地执行;如果没有持续的创新力,这个组织就会容易被颠覆。

文章来源:Worktile敏捷博客

欢迎访问交流更多关于技术及协作的问题。

文章转载请注明出处。

从MySQL源码看其网络IO模型

mumupudding阅读(3)


从MySQL源码看其网络IO模型

前言

MySQL是当今最流行的开源数据库,阅读其源码是一件大有裨益的事情(虽然其代码感觉比较凌乱)。而笔者阅读一个Server源码的习惯就是先从其网络IO模型看起。于是,便有了本篇博客。

MySQL启动Socket监听

看源码,首先就需要找到其入口点,mysqld的入口点为mysqld_main,跳过了各种配置文件的加载之后,我们来到了network_init初始化网络环节,如下图所示:

下面是其调用栈:

mysqld_main (MySQL Server Entry Point) |-network_init (初始化网络)  /* 建立tcp套接字 */  |-create_socket (AF_INET)  |-mysql_socket_bind (AF_INET)  |-mysql_socket_listen (AF_INET)  /* 建立UNIX套接字*/  |-mysql_socket_socket (AF_UNIX)  |-mysql_socket_bind (AF_UNIX)  |-mysql_socket_listen (AF_UNIX)

值得注意的是,在tcp socket的初始化过程中,考虑到了ipv4/v6的两种情况:

// 首先创建ipv4连接ip_sock= create_socket(ai, AF_INET, &a);// 如果无法创建ipv4连接,则尝试创建ipv6连接if(mysql_socket_getfd(ip_sock) == INVALID_SOCKET)  ip_sock= create_socket(ai, AF_INET6, &a);

如果我们以很快的速度stop/start mysql,会出现上一个mysql的listen port没有被release导致无法当前mysql的socket无法bind的情况,在此种情况下mysql会循环等待,其每次等待时间为当前重试次数retry * retry/3 +1秒,一直到设置的–port-open-timeout(默认为0)为止,如下图所示:

MySQL新建连接处理循环

通过handle_connections_sockets处理MySQL的新建连接循环,根据操作系统的配置通过poll/select处理循环(非epoll,这样可移植性较高,且mysql瓶颈不在网络上)。
MySQL通过线程池的模式处理连接(一个连接对应一个线程,连接关闭后将线程归还到池中),如下图所示:
对应的调用栈如下所示:

handle_connections_sockets |->poll/select |->new_sock=mysql_socket_accept(...sock...) /*从listen socket中获取新连接*/ |->new THD 连接线程上下文 /* 如果获取不到足够内存,则shutdown new_sock*/ |->mysql_socket_getfd(sock) 从socket中获取  /** 设置为NONBLOCK和环境有关 **/ |->fcntl(mysql_socket_getfd(sock), F_SETFL, flags | O_NONBLOCK); |->mysql_socket_vio_new  |->vio_init (VIO_TYPE_TCPIP)   |->(vio->write = vio_write)   /* 默认用的是vio_read */   |->(vio->read=(flags & VIO_BUFFERED_READ) ?vio_read_buff :vio_read;)   |->(vio->viokeepalive = vio_keepalive) /*tcp层面的keepalive*/   |->..... |->mysql_net_init  |->设置超时时间,最大packet等参数 |->create_new_thread(thd) /* 实际是从线程池拿,不够再新建pthread线程 */  |->最大连接数限制  |->create_thread_to_handle_connection   |->首先看下线程池是否有空闲线程    |->mysql_cond_signal(&COND_thread_cache) /* 有则发送信号 */   /** 这边的hanlde_one_connection是mysql连接的主要处理函数 */   |->mysql_thread_create(...handle_one_connection...)   

MySQL的VIO

如上图代码中,每新建一个连接,都随之新建一个vio(mysql_socket_vio_new->vio_init),在vio_init的过程中,初始化了一堆回掉函数,如下图所示:
我们关注点在vio_read和vio_write上,如上面代码所示,在笔者所处机器的环境下将MySQL连接的socket设置成了非阻塞模式(O_NONBLOCK)模式。所以在vio的代码里面采用了nonblock代码的编写模式,如下面源码所示:

vio_read

size_t vio_read(Vio *vio, uchar *buf, size_t size){  while ((ret= mysql_socket_recv(vio->mysql_socket, (SOCKBUF_T *)buf, size, flags)) == -1)  {    ......    // 如果上面获取的数据为空,则通过select的方式去获取读取事件,并设置超时timeout时间    if ((ret= vio_socket_io_wait(vio, VIO_IO_EVENT_READ)))        break;  }}

即通过while循环去读取socket中的数据,如果读取为空,则通过vio_socket_io_wait去等待(借助于select的超时机制),其源码如下所示:

vio_socket_io_wait |->vio_io_wait  |-> (ret= select(fd + 1, &readfds, &writefds, &exceptfds,               (timeout >= 0) ? &tm : NULL))

笔者在jdk源码中看到java的connection time out也是通过这,select(…wait_time)的方式去实现连接超时的。
由上述源码可以看出,这个mysql的read_timeout是针对每次socket recv(而不是整个packet的),所以可能出现超过read_timeout MySQL仍旧不会报错的情况,如下图所示:

vio_write

vio_write实现模式和vio_read一致,也是通过select来实现超时时间的判定,如下面源码所示:

size_t vio_write(Vio *vio, const uchar* buf, size_t size){  while ((ret= mysql_socket_send(vio->mysql_socket, (SOCKBUF_T *)buf, size, flags)) == -1)  {    int error= socket_errno;    /* The operation would block? */    // 处理EAGAIN和EWOULDBLOCK返回,NON_BLOCK模式都必须处理    if (error != SOCKET_EAGAIN && error != SOCKET_EWOULDBLOCK)      break;    /* Wait for the output buffer to become writable.*/    if ((ret= vio_socket_io_wait(vio, VIO_IO_EVENT_WRITE)))      break;  }}

MySQL的连接处理线程

从上面的代码:

mysql_thread_create(...handle_one_connection...)

可以发现,MySQL每个线程的处理函数为handle_one_connection,其过程如下图所示:


代码如下所示:

for(;;){ // 这边做了连接的handshake和auth的工作 rc= thd_prepare_connection(thd); // 和通常的线程处理一样,一个无限循环获取连接请求 while(thd_is_connection_alive(thd)) {  if(do_command(thd))   break; } // 出循环之后,连接已经被clientdu端关闭或者出现异常 // 这边做了连接的销毁动作 end_connection(thd);end_thread: ... // 这边调用end_thread做清理动作,并将当前线程返还给线程池重用 // end_thread对应为one_thread_per_connection_end if (MYSQL_CALLBACK_ELSE(thread_scheduler, end_thread, (thd, 1), 0))  return;  ... // 这边current_thd是个宏定义,其实是current_thd(); // 主要是从线程上下文中获取新塞进去的thd // my_pthread_getspecific_ptr(THD*,THR_THD); thd= current_thd; ...}

mysql的每个woker线程通过无限循环去处理请求。

线程的归还过程

MySQL通过调用one_thread_per_connection_end(即上面的end_thread)去归还连接。

MYSQL_CALLBACK_ELSE(...end_thread) one_thread_per_connection_end  |->thd->release_resources()  |->......  |->block_until_new_connection

线程在新连接尚未到来之前,等待在信号量上(下面代码是C/C++ mutex condition的标准使用模式):

static bool block_until_new_connection(){  mysql_mutex_lock(&LOCK_thread_count); ......    while (!abort_loop && !wake_pthread && !kill_blocked_pthreads_flag)      mysql_cond_wait(&x1, &LOCK_thread_count);   ......   // 从等待列表中获取需要处理的THD   thd= waiting_thd_list->front();   waiting_thd_list->pop_front();   ......   // 将thd放入到当前线程上下文中   // my_pthread_setspecific_ptr(THR_THD,  this)       thd->store_globals();   ......   mysql_mutex_unlock(&LOCK_thread_count);   .....}

整个过程如下图所示:


由于MySQL的调用栈比较深,所以将thd放入线程上下文中能够有效的在调用栈中减少传递参数的数量。

总结

MySQL的网络IO模型采用了经典的线程池技术,虽然性能上不及reactor模型,但好在其瓶颈并不在网络IO上,采用这种方法无疑可以节省大量的精力去专注于处理sql等其它方面的优化。