开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Java 设计模式之代理模式(十二)

发表于 2017-11-23

一、前言

今天介绍结构型模式中的最后一个模式–代理模式。上篇 Java 设计模式主题为《Java 设计模式之享元模式(十一)》。

二、简单介绍

# 2.1 定义

代理(Proxy)模式是结构型的设计模式之一,它可以为其他对象提供一种代理(Proxy)以控制对这个对象的访问。

所谓代理,是指具有与被代理的对象具有相同的接口的类,客户端必须通过代理与被代理的目标类交互,而代理一般在交互的过程中(交互前后),进行某些特别的处理。

# 2.2 参与角色

  1. 抽象主题(Subject):真实主题与代理主题的共同接口。
  2. 真实主题(RealSubject):实现抽象主题,定义真实主题所要实现的业务逻辑,供代理主题调用。
  3. 代理主题(Proxy):实现抽象主题,是真实主题的代理。通过真实主题的业务逻辑方法来实现抽象方法,并可以附加自己的操作。

# 2.3 应用场景

  1. 需要控制对目标对象的访问。
  2. 需要对目标对象进行方法增强。如:添加日志记录,计算耗时等。
  3. 需要延迟加载目标对象。

三、实现方式

代理模式分类:静态代理和动态代理。

区别:静态代理需要手动指定代理对象,动态代理由系统自动生成代理对象。

我们以交学费为例,由于小孩年龄小,安全意识不高,为了安全起见,由父母代理交学费,小孩负责上学。

# 3.1 静态代理

抽象主题:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public interface Child {



public void payTuition();



public void goSchool();



}

真实主题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码public class RealChild implements Child {



@Override

public void payTuition() {

System.out.println("小孩交学费");

}



@Override

public void goSchool() {

System.out.println("小孩上学");

}



}

此处 goSchool 方法就是供代理对象调用。

代理主题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
复制代码public class Parent implements Child {



private Child child;



public Parent(Child child) {

this.child = child;

}



@Override

public void payTuition() {

System.out.println("父母交学费");

}



@Override

public void goSchool() {

this.child.goSchool();

}



}

父母代理孩子交学费,由小孩自己上学。

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码public class Client {



public static void main(String[] args) {



Child child = new RealChild();



// 手动创建代理对象

Child proxy = new Parent(child);



proxy.payTuition();



proxy.goSchool();

}

}

打印结果:

1
2
3
复制代码父母交学费

小孩上学

# 3.2 动态代理

动态代理的实现手段:JDK 自带的 Proxy 类、CGlib、Javaassist 等。

本次测试使用 Proxy 类演示,抽象主题和真实主题不变,我们创建处理器类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码public class ChildHandler implements InvocationHandler {



public Child child;



public ChildHandler(Child child) {

this.child = child;

}



@Override

public Object invoke(Object proxy, Method method, Object[] args)

throws Throwable {



Object obj = null;



// 访问控制

if ("goSchool".equals(method.getName())) {

System.out.println("父母交学费");



obj = method.invoke(child, args);

}



return obj;

}



}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
复制代码public class Client {



public static void main(String[] args) {



Child child = new RealChild();



ChildHandler handler = new ChildHandler(child);



// 代理对象

Child proxy = (Child) Proxy.newProxyInstance(

child.getClass().getClassLoader(),

child.getClass().getInterfaces(),

handler);



proxy.payTuition();



proxy.goSchool();

}

}

打印结果与上文的相同。

在执行 proxy.payTuition() 时,并不是打印“小孩交学费”,而是“父母交学费”,达到了对目标对象访问控制的目的,即控制小孩交学费的行为,让父母代交学费。

UML 类图表示如下:

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot 入门之基础篇(一)

发表于 2017-11-23

一、前言

Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。

本系列以快速入门为主,可当作工具小手册阅读

二、环境搭建

创建一个 maven 工程,目录结构如下图:

image

2.1 添加依赖

创建 maven 工程,在 pom.xml 文件中添加如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码<!-- 定义公共资源版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 上边引入 parent,因此 下边无需指定版本 -->
<!-- 包含 mvc,aop 等jar资源 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

2.2 创建目录和配置文件

创建 src/main/resources 源文件目录,并在该目录下创建 application.properties 文件、static 和 templates 的文件夹。

application.properties:用于配置项目运行所需的配置数据。

static:用于存放静态资源,如:css、js、图片等。

templates:用于存放模板文件。

目录结构如下:

image

2.3 创建启动类

在 com.light.springboot 包下创建启动类,如下:

1
2
3
4
5
6
7
8
9
10
11
复制代码/**
该注解指定项目为springboot,由此类当作程序入口
自动装配 web 依赖的环境

**/
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}

2.4 案例演示

创建 com.light.springboot.controller 包,在该包下创建一个 Controller 类,如下:

1
2
3
4
5
6
7
复制代码@RestController
public class TestController {
@GetMapping("/helloworld")
public String helloworld() {
return "helloworld";
}
}

在 SpringbootApplication 文件中右键 Run as -> Java Application。当看到 “Tomcat started on port(s): 8080 (http)” 字样说明启动成功。

打开浏览器访问 http://localhost:8080/helloworld,结果如下:

image

读者可以使用 STS 开发工具,里边集成了插件,可以直接创建 Spingboot 项目,它会自动生成必要的目录结构。

三、热部署

当我们修改文件和创建文件时,都需要重新启动项目。这样频繁的操作很浪费时间,配置热部署可以让项目自动加载变化的文件,省去的手动操作。

在 pom.xml 文件中添加如下配置:

1
2
3
4
5
6
7
复制代码<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>true</scope>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
复制代码<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 没有该配置,devtools 不生效 -->
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>

配置好 pom.xml 文件后,我们启动项目,随便创建/修改一个文件并保存,会发现控制台打印 springboot 重新加载文件的信息。

演示图如下:

image

四、多环境切换

application.properties 是 springboot 在运行中所需要的配置信息。

当我们在开发阶段,使用自己的机器开发,测试的时候需要用的测试服务器测试,上线时使用正式环境的服务器。

这三种环境需要的配置信息都不一样,当我们切换环境运行项目时,需要手动的修改多出配置信息,非常容易出错。

为了解决上述问题,springboot 提供多环境配置的机制,让开发者非常容易的根据需求而切换不同的配置环境。

在 srr/main/resources 目录下创建三个配置文件:

1
2
3
复制代码application-dev.properties:用于开发环境
application-test.properties:用于测试环境
application-prod.properties:用于生产环境

我们可以在这个三个配置文件中设置不同的信息,application.properties 配置公共的信息。

在 application.properties 中配置:

1
复制代码spring.profiles.active=dev

表示激活 application-dev.properties 文件配置, springboot 会加载使用 application.properties 和 application-dev.properties 配置文件的信息。

同理,可将 spring.profiles.active 的值修改成 test 或 prod 达到切换环境的目的。

演示图如下:

image

五、配置日志

5.1 配置 logback(官方推荐使用)

5.1.1 配置日志文件

spring boot 默认会加载 classpath:logback-spring.xml 或者 classpath:logback-spring.groovy。

如需要自定义文件名称,在 application.properties 中配置 logging.config 选项即可。

在 src/main/resources 下创建 logback-spring.xml 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 文件输出格式 -->
<property name="PATTERN" value="%-12(%d{yyyy-MM-dd HH:mm:ss.SSS}) |-%-5level [%thread] %c [%L] -| %msg%n" />
<!-- test文件路径 -->
<property name="TEST_FILE_PATH" value="d:/test.log" />
<!-- pro文件路径 -->
<property name="PRO_FILE_PATH" value="/opt/test/log" />

<!-- 开发环境 -->
<springProfile name="dev">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${PATTERN}</pattern>
</encoder>
</appender>
<logger name="com.light.springboot" level="debug" />
<root level="info">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>

<!-- 测试环境 -->
<springProfile name="test">
<!-- 每天产生一个文件 -->
<appender name="TEST-FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 文件路径 -->
<file>${TEST_FILE_PATH}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 文件名称 -->
<fileNamePattern>${TEST_FILE_PATH}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 文件最大保存历史数量 -->
<MaxHistory>100</MaxHistory>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${PATTERN}</pattern>
</layout>
</appender>
<root level="info">
<appender-ref ref="TEST-FILE" />
</root>
</springProfile>

<!-- 生产环境 -->
<springProfile name="prod">
<appender name="PROD_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PRO_FILE_PATH}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${PRO_FILE_PATH}/warn.%d{yyyy-MM-dd}.log</fileNamePattern>
<MaxHistory>100</MaxHistory>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${PATTERN}</pattern>
</layout>
</appender>
<root level="warn">
<appender-ref ref="PROD_FILE" />
</root>
</springProfile>
</configuration>

其中,springProfile 标签的 name 属性对应 application.properties 中的 spring.profiles.active 的配置。

即 spring.profiles.active 的值可以看作是日志配置文件中对应的 springProfile 是否生效的开关。

5.2 配置 log4j2

5.2.1 添加依赖

1
2
3
4
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

5.2.2 配置日志文件

spring boot 默认会加载 classpath:log4j2.xml 或者 classpath:log4j2-spring.xml。

如需要自定义文件名称,在 application.properties 中配置 logging.config 选项即可。

log4j2.xml 文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码<?xml version="1.0" encoding="utf-8"?>
<configuration>
<properties>
<!-- 文件输出格式 -->
<property name="PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} |-%-5level [%thread] %c [%L] -| %msg%n</property>
</properties>
<appenders>
<Console name="CONSOLE" target="system_out">
<PatternLayout pattern="${PATTERN}" />
</Console>
</appenders>
<loggers>
<logger name="com.light.springboot" level="debug" />
<root level="info">
<appenderref ref="CONSOLE" />
</root>
</loggers>
</configuration>

log4j2 不能像 logback 那样在一个文件中设置多个环境的配置数据,只能命名 3 个不同名的日志文件,分别在 application-dev,application-test 和 application-prod 中配置 logging.config 选项。

除了在日志配置文件中设置参数之外,还可以在 application-*.properties 中设置,日志相关的配置:

1
2
3
4
5
6
7
8
9
复制代码logging.config                    # 日志配置文件路径,如 classpath:logback-spring.xml
logging.exception-conversion-word # 记录异常时使用的转换词
logging.file # 记录日志的文件名称,如:test.log
logging.level.* # 日志映射,如:logging.level.root=WARN,logging.level.org.springframework.web=DEBUG
logging.path # 记录日志的文件路径,如:d:/
logging.pattern.console # 向控制台输出的日志格式,只支持默认的 logback 设置。
logging.pattern.file # 向记录日志文件输出的日志格式,只支持默认的 logback 设置。
logging.pattern.level # 用于呈现日志级别的格式,只支持默认的 logback 设置。
logging.register-shutdown-hook # 初始化时为日志系统注册一个关闭钩子

六、打包运行

打包的形式有两种:jar 和 war。

6.1 打包成可执行的 jar 包

默认情况下,通过 maven 执行 package 命令后,会生成 jar 包,且该 jar 包会内置了 tomcat 容器,因此我们可以通过 java -jar 就可以运行项目,如下图:

image

6.2 打包成部署的 war 包

让 SpringbootApplication 类继承 SpringBootServletInitializer 并重写 configure 方法,如下:

1
2
3
4
5
6
7
8
9
10
复制代码@SpringBootApplication
public class SpringbootApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SpringbootApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}

修改 pom.xml 文件,将 jar 改成 war,如下:

1
复制代码<packaging>war</packaging>

打包成功后,将 war 包部署到 tomcat 容器中运行即可。

七、参考资料

  • docs.spring.io/spring-boot… 官方文档

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Docker技术浅谈:私有化部署的优势以及在顶象内部的应用实

发表于 2017-11-23

顶象全景式业务安全风控体系基于新一代风控体系构建,并采用Docker技术进行私有云和公有云部署。本文主要和大家分享下Docker容器技术和顶象风控系统私有化部署的优势以及Docker容器技术在顶象内部的应用实践。

undefined

Docker容器技术概述

Docker是一个开源的容器引擎,Docke是以Docker容器为资源分割和调度的基本单位,封装软件的运行时环境,用于快速构建、发布、运行分布式应用的平台。

Docker容器本质上是宿主机上的进程,通过namespace实现资源隔离,通过cgroups实现资源限制,通过写时复制(copy-on-write)实现高效的文件操作。容器是应用程序层的一个抽象,将代码和依赖关系打包在一起。 多个容器可以在同一台机器上运行,并与其他容器共享操作系统内核,每个容器在用户空间中作为孤立进程运行。

undefined
图自官网

Docker引擎包含Docker守护进程(Docker daemon,dockerd 命令)、REST API和Docker客户端(docker 命令)。Docker采用 C/S架构,Docker客户端与Docker守护进程通信,Docker守护进程负责构建,运行和分发Docker容器。 Docker客户端和守护进程可以在同一个系统上运行,也可以将Docker客户端连接到远程的Docker守护进程。 Docker客户端和守护进程使用REST API通过UNIX套接字或网络接口进行通信。Docker容器基于开放标准,可运行在所有主要Linux发行版,Microsoft
Windows以及包括虚拟机,裸机和云上的任何基础架构上。

技术优势

Docker能够将应用程序与基础架构分离,从而可以快速交付软件。使用Docker,可以像管理应用程序一样管理基础架构。宿主机不需要去关心某一个容器运行所需要的依赖,只要它可以运行Docker,那么它就可以运行所有的Docker容器,容器将软件与其周围环境隔离开来,并有助于减少在同一基础架构上运行不同软件的团队之间的冲突。

Docker容器实现了应用环境的标准化,我们可以为不同应用、及其不同的版本制作各自的镜像,实现持续集成、应用的快速交付、应用快速更新与回滚。Docker的镜像存储采用分层的形式,不同的 Docker 容器共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的效率,通过合理的镜像构建方式,镜像所需的存储空间并不会随着镜像的数量而线性增长。在Docker的官方镜像仓库Docker Hub上,我们能找到100,000+的镜像,经镜像仓库的统一管理,我们只需要pull其镜像,然后通过run命令就可以快速搭建起所需的环境、部署应用。而基于Docker提供的Dockerfile,我们可以在基础镜像上自由地构建自己所需的镜像。

相较于以往的基于虚拟机部署,Docker容器更加轻量化并具备可移植性,并且不需要考虑外部的依赖问题,就像Java “Write once,run anywhere”的特性一样,JVM屏蔽了不同平台的差异性,而Docker所提出的 “Build once,Run anywhere,Configure once,Run anything”体现了其更加便捷、部署成本更低的特性,不仅能够有效屏蔽操作系统之间的差异,对于混合部署又能够屏蔽其他应用可能出现的影响,间接保证了应用的高可用,提高了资源的利用率。

Docker容器编排与集群管理

当Docker容器逐渐增多,应用的依赖关系变得复杂,依赖需要多个组件时,就可以使用Docker容器的编排。编排是一个广义的概念,它是指容器调度、集群管理和可能其他主机供应配置。为此,Docker提供了容器集群快速编排的Compose与Swarm工具。Compose是定义、运行多容器、多服务和Swarm集群配置的应用编排程序(Define application stacks built using multiple containers, services, and swarm configurations.),使用YAML文件来配置应用程序的服务,然后通过命令创建并启动配置中所有服务,实现快速部署。

当应用被扩展到多台宿主机,管理每个宿主系统和抽象化底层平台的复杂性变得更有挑战。Swarm作为容器集群的管理工具,可以很容易地部署跨主机的容器集群服务,Compose本身不支持跨主机管理容器,因为它的实现中只能连接一个docker client。Swarm把多个主机的docker engine集群抽象成一个虚拟的Docker主机。在集群中一个应用或者组件发现其运行环境以及其它应用或组件的信息通过服务发现实现,通常是key/value存储,例如Consul、Etcd、ZooKeeper等。Docker1.12及之后的版本已内置SwarmKit,这是一个Swarm的升级项目,在SwarmKit中内置key/value存储,通过构建Swarm集群,在上面就可以把任务负载到不同的机器上。Swarm集群中,节点有两种角色,manager和worker。manager节点通过实现Raft一致性算法来管理全局的集群状态,再配合Compose
YML V3的语法可以方便对所有应用的配置管理,高效实现跨主机的容器编排与集群管理。

顶象风控系统的私有部署,除了考虑业务和用户数据的安全问题,还考虑了基础设施的依赖和隔离、快速部署交付、更新等,保证所有组件和服务都可快速的弹性扩容,既满足小范围的测试与业务起步阶段,也可以动态扩容适应业务发展,实现高QPS的支撑。

Docker容器技术在顶象内部的应用

目前Docker容器技术已在顶象内部大规模推行,所有应用均通过Docker容器实现部署、交付与更新。在此列举几个简单的实践例子:

  1. 在一个Docker容器中,通常我们只运行一个应用,当使用容器编排时,不同应用的启动时间不同,同时耗费的时间又会与机器的性能有关,在docker-compose的YML文件中, depends_on, links等参数可以控制服务的启动顺序,但是实际上并不知道容器内的应用是否完成启动,当一个服务必须要依赖另一个服务时就需要控制它们之间容器启动的时间间隔,或者在启动应用的命令中预留等待的时间,也可以对两个服务分别编排启动。
  1. Compose在镜像的制作上也很方便,YML中提供了build参数用于指定Dockerfile的路径,image参数指定镜像的名称,docker-compose提供了build、push、images等命令可以为所有的应用批量制作镜像,或指定service名称,为单个服务制作镜像。同时YML支持环境变量,通过Linux export命令设置环境变量可以动态地调整参数,而docker-compose config命令则可以检查YML文件的正确性、预览最终的文件内容。
  1. Docker容器日志输出的是控制台的日志,保存在/var/lib/docker/containers下以容器ID命名的目录中,在大多数情况下,我们只需要将必要的日志内容输出到文件中,再挂载到宿主机,对此可以屏蔽一些输出到控制台的日志以减小磁盘空间的占用,同时docker run提供了参数 –log-opt 可以用于控制日志大小,在Compose中则有logging max-size相关参数。
  1. Docker提供了bridge、host、overlay、container等网络模式,在实际的使用中经常会有跨主机容器访问通信的场景,选择不同的网络模式、合理分配应用的部署可以提高应用的性能。
  1. 通过Jenkins搭建持续集成环境,自动构建代码,可以快速把应用打包成镜像并自动部署,将构建结果发送到Sonar, 展示单测覆盖率,代码基本bug检测,并把失败的构建以邮件方式通知相关的开发人员,对需要发布的镜像推送到镜像仓库。基于Docker私有仓库,应用的发布更新从仓库中获取镜像分发,对不同版本的容器区别命名,保留旧版本容器方便及时回滚。

* 更多业务安全类的技术分享,请关注顶象官方博客:www.dingxiang-inc.com/blog

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

HashMap源码解析

发表于 2017-11-23

hashMap数据结构图:

HashMap特点:

  1. 允许一个记录的键为null;
  2. 允许多条记录的值为null;
  3. 非线程安全,任意时刻多线程操作hashmap,有可能导致数据不一致,可以通过Collections的synchronizedMap来实现Map的线程安全或者使用concurrentHashMap。

HashMap是链表+数组结构组成,底层是数组,数组元素是单向链表。当产生hash碰撞事件,意味着一个位置插入多个元素,这个时候数组上面就会产生链表。

通过hashcode的高16位实现的,能保证数组table的length比较小的时候,保证高低bit都参与到hash计算中,不会有大的开销。

1
2
3
4
5
6
processing复制代码static final int hash(Object key) {
int h;
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

根据key的hash值进行value内容的查找

1
2
3
4
processing复制代码 public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
arcade复制代码final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

put实现:

对key的hashCode()进行hashing,并计算下标( n-1 & hash),判断该位置元素是否存在,不存在,创建Node元素,存在产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点。

1
2
3
maxima复制代码public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
arcade复制代码final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab数组,p每个桶
Node<K,V>[] tab; Node<K,V> p; int n, i;
//tab为空创建tab
if ((tab = table) == null || (n = tab.length) == 0)
//resize进行扩容
n = (tab = resize()).length;
//index = (n - 1) & hash 下表位置
if ((p = tab[i = (n - 1) & hash]) == null)
//创建一个新的Node元素
tab[i] = newNode(hash, key, value, null);
else {
//指定位置已经有元素,也就是说产生hash碰撞
Node<K,V> e; K k;
//判断节点是否存在,覆盖原来原来的value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//判断是否是红黑树
//是红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//不是红黑树,遍历链表准备插入
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//尾插法添加元素
p.next = newNode(hash, key, value, null);
//TREEIFY_THRESHOLD默认为8,大于8,转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//如果达到这个阈值转为红黑树
treeifyBin(tab, hash);
break;
}
//如果节点key存在,则覆盖原来位置的key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//检查e是否存在相应的key,如果存在就更新value,并且返回
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//判断hashmap是否需要resize扩容
if (++size > threshold)
resize();
//留给子类LinkedHashMap来实现的
afterNodeInsertion(evict);
return null;
}

resize实现:HashMap扩容实现:使用一个新的数组代替已有的容量小的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
haxe复制代码final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //创建一个oldTab数组用于保存之前的数组
int oldCap = (oldTab == null) ? 0 : oldTab.length; //获取原来数组的长度
int oldThr = threshold; //原来数组扩容的临界值
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) { //如果原来的数组长度大于最大值(2^30)
threshold = Integer.MAX_VALUE; //扩容临界值提高到正无穷
return oldTab; //返回原来的数组,也就是系统已经管不了了,随便你怎么玩吧
}
//else if((新数组newCap)长度乘2) < 最大值(2^30) && (原来的数组长度)>= 初始长度(2^4))
//这个else if 中实际上就是咋判断新数组(此时刚创建还为空)和老数组的长度合法性,同时交代了,
//我们扩容是以2^1为单位扩容的。下面的newThr(新数组的扩容临界值)一样,在原有临界值的基础上扩2^1
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0)
newCap = oldThr; //新数组的初始容量设置为老数组扩容的临界值
else { // 否则 oldThr == 0,零初始阈值表示使用默认值
newCap = DEFAULT_INITIAL_CAPACITY; //新数组初始容量设置为默认值
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //计算默认容量下的阈值
}
if (newThr == 0) { //如果newThr == 0,说明为上面 else if (oldThr > 0)
//的情况(其他两种情况都对newThr的值做了改变),此时newCap = oldThr;
float ft = (float)newCap * loadFactor; //ft为临时变量,用于判断阈值的合法性
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE); //计算新的阈值
}
threshold = newThr; //改变threshold值为新的阈值
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; //改变table全局变量为,扩容后的newTable
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) { //遍历数组,将老数组(或者原来的桶)迁移到新的数组(新的桶)中
Node<K,V> e;
if ((e = oldTab[j]) != null) { //新建一个Node<K,V>类对象,用它来遍历整个数组
oldTab[j] = null;
if (e.next == null)
//将e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置,
newTab[e.hash & (newCap - 1)] = e; //这个我们之前讲过,是一个取模操作
else if (e instanceof TreeNode) //如果e已经是一个红黑树的元素,这个我们不展开讲
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
//命名两组对象
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

工作原理总结:

通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JVM系列之实战内存溢出异常

发表于 2017-11-23

实战内存溢出异常

大家好,相信大部分Javaer在code时经常会遇到本地代码运行正常,但在生产环境偶尔会莫名其妙的报一些关于内存的异常,StackOverFlowError,OutOfMemoryError异常是最常见的。今天就基于上篇文章JVM系列之Java内存结构详解讲解的各个内存区域重点实战分析下内存溢出的情况。在此之前,我还是想多余累赘一些其他关于对象的问题,具体内容如下:

文章结构

  1. 对象的创建过程
  2. 对象的内存布局
  3. 对象的访问定位
  4. 实战内存异常

1 . 对象的创建过程

关于对象的创建,第一反应是new关键字,那么本文就主要讲解new关键字创建对象的过程。

1
复制代码Student stu =new Student("张三","18");

就拿上面这句代码来说,虚拟机首先会去检查Student这个类有没有被加载,如果没有,首先去加载这个类到方法区,然后根据加载的Class类对象创建stu实例对象,需要注意的是,stu对象所需的内存大小在Student类加载完成后便可完全确定。内存分配完成后,虚拟机需要将分配到的内存空间的实例数据部分初始化为零值,这也就是为什么我们在编写Java代码时创建一个变量不需要初始化。紧接着,虚拟机会对对象的对象头进行必要的设置,如这个对象属于哪个类,如何找到类的元数据(Class对象),对象的锁信息,GC分代年龄等。设置完对象头信息后,调用类的构造函数。
其实讲实话,虚拟机创建对象的过程远不止这么简单,我这里只是把大致的脉络讲解了一下,方便大家理解。

2 . 对象的内存布局

刚刚提到的实例数据,对象头,有些小伙伴也许有点陌生,这一小节就详细讲解一下对象的内存布局,对象创建完成后大致可以分为以下几个部分:

  • 对象头
  • 实例数据
  • 对齐填充

对象头: 对象头中包含了对象运行时一些必要的信息,如GC分代信息,锁信息,哈希码,指向Class类元信息的指针等,其中对Javaer比较有用的是锁信息与指向Class对象的指针,关于锁信息,后期有机会讲解并发编程JUC时再扩展,关于指向Class对象的指针其实很好理解。比如上面那个Student的例子,当我们拿到stu对象时,调用Class stuClass=stu.getClass();的时候,其实就是根据这个指针去拿到了stu对象所属的Student类在方法区存放的Class类对象。虽然说的有点拗口,但这句话我反复琢磨了好几遍,应该是说清楚了。^_^

实例数据:实例数据部分是对象真正存储的有效信息,就是程序代码中所定义的各种类型的字段内容。

对齐填充:虚拟机规范要求对象大小必须是8字节的整数倍。对齐填充其实就是来补全对象大小的。

3 . 对象的访问定位

谈到对象的访问,还拿上面学生的例子来说,当我们拿到stu对象时,直接调用stu.getName();时,其实就完成了对对象的访问。但这里要累赘说一下的是,stu虽然通常被认为是一个对象,其实准确来说是不准确的,stu只是一个变量,变量里存储的是指向对象的指针,(如果干过C或者C++的小伙伴应该比较清楚指针这个概念),当我们调用stu.getName()时,虚拟机会根据指针找到堆里面的对象然后拿到实例数据name.需要注意的是,当我们调用stu.getClass()时,虚拟机会首先根据stu指针定位到堆里面的对象,然后根据对象头里面存储的指向Class类元信息的指针再次到方法区拿到Class对象,进行了两次指针寻找。具体讲解图如下:

4 .实战内存异常

内存异常是我们工作当中经常会遇到问题,但如果仅仅会通过加大内存参数来解决问题显然是不够的,应该通过一定的手段定位问题,到底是因为参数问题,还是程序问题(无限创建,内存泄露)。定位问题后才能采取合适的解决方案,而不是一内存溢出就查找相关参数加大。

概念

  • 内存泄露:代码中的某个对象本应该被虚拟机回收,但因为拥有GCRoot引用而没有被回收。关于GCRoot概念,下一篇文章讲解。
  • 内存溢出: 虚拟机由于堆中拥有太多不可回收对象没有回收,导致无法继续创建新对象。

在分析问题之前先给大家讲一讲排查内存溢出问题的方法,内存溢出时JVM虚拟机会退出,那么我们怎么知道JVM运行时的各种信息呢,Dump机制会帮助我们,可以通过加上VM参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现内存溢出异常时生成dump文件,然后通过外部工具(作者使用的是VisualVM)来具体分析异常的原因。

下面从以下几个方面来配合代码实战演示内存溢出及如何定位:

  • Java堆内存异常
  • Java栈内存异常
  • 方法区内存异常

Java堆内存异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码/**
VM Args:
//这两个参数保证了堆中的可分配内存固定为20M
-Xms20m
-Xmx20m
//文件生成的位置,作则生成在桌面的一个目录
-XX:+HeapDumpOnOutOfMemoryError //文件生成的位置,作则生成在桌面的一个目录
//文件生成的位置,作则生成在桌面的一个目录
-XX:HeapDumpPath=/Users/zdy/Desktop/dump/
*/
public class HeapOOM {
//创建一个内部类用于创建对象使用
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
//无限创建对象,在堆中
while (true) {
list.add(new OOMObject());
}
}
}

Run起来代码后爆出异常如下:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to /Users/zdy/Desktop/dump/java_pid1099.hprof …

可以看到生成了dump文件到指定目录。并且爆出了OutOfMemoryError,还告诉了你是哪一片区域出的问题:heap space

打开VisualVM工具导入对应的heapDump文件(如何使用请读者自行查阅相关资料),相应的说明见图:

"类标签"

“类标签”

切换到”实例数”标签页
"实例数标签"

“实例数标签”

分析dump文件后,我们可以知道,OOMObject这个类创建了810326个实例。所以它能不溢出吗?接下来就在代码里找这个类在哪new的。排查问题。(我们的样例代码就不用排查了,While循环太凶猛了)

Java栈内存异常

老实说,在栈中出现异常(StackOverFlowError)的概率小到和去苹果专卖店买手机,买回来后发现是Android系统的概率是一样的。因为作者确实没有在生产环境中遇到过,除了自己作死写样例代码测试。先说一下异常出现的情况,前面讲到过,方法调用的过程就是方法帧进虚拟机栈和出虚拟机栈的过程,那么有两种情况可以导致StackOverFlowError,当一个方法帧(比如需要2M内存)进入到虚拟机栈(比如还剩下1M内存)的时候,就会报出StackOverFlow.这里先说一个概念,栈深度:指目前虚拟机栈中没有出栈的方法帧。虚拟机栈容量通过参数-Xss来控制,下面通过一段代码,把栈容量人为的调小一点,然后通过递归调用触发异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码/**
* VM Args:
//设置栈容量为160K,默认1M
-Xss160k
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
//递归调用,触发异常
stackLeak();
}

public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}

结果如下:
stack length:751
Exception in thread “main” java.lang.StackOverflowError

可以看到,递归调用了751次,栈容量不够用了。
默认的栈容量在正常的方法调用时,栈深度可以达到1000-2000深度,所以,一般的递归是可以承受的住的。如果你的代码出现了StackOverflowError,首先检查代码,而不是改参数。

这里顺带提一下,很多人在做多线程开发时,当创建很多线程时,容易出现OOM(OutOfMemoryError),这时可以通过具体情况,减少最大堆容量,或者栈容量来解决问题,这是为什么呢。请看下面的公式:

线程数*(最大栈容量)+最大堆值+其他内存(忽略不计或者一般不改动)=机器最大内存

当线程数比较多时,且无法通过业务上削减线程数,那么再不换机器的情况下,你只能把最大栈容量设置小一点,或者把最大堆值设置小一点。

方法区内存异常

写到这里时,作者本来想写一个无限创建动态代理对象的例子来演示方法区溢出,避开谈论JDK7与JDK8的内存区域变更的过渡,但细想一想,还是把这一块从始致终的说清楚。在上一篇文章中JVM系列之Java内存结构详解讲到方法区时提到,JDK7环境下方法区包括了(运行时常量池),其实这么说是不准确的。因为从JDK7开始,HotSpot团队就想到开始去”永久代”,大家首先明确一个概念,方法区和”永久代”(PermGen space)是两个概念,方法区是JVM虚拟机规范,任何虚拟机实现(J9等)都不能少这个区间,而”永久代”只是HotSpot对方法区的一个实现。为了把知识点列清楚,我还是才用列表的形式:

  • JDK7之前(包括JDK7)拥有”永久代”(PermGen space),用来实现方法区。但在JDK7中已经逐渐在实现中把永久代中把很多东西移了出来,比如:符号引用(Symbols)转移到了native heap,运行时常量池(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap.
    所以这就是为什么我说上一篇文章中说方法区中包含运行时常量池是不正确的,因为已经移动到了java heap;
  • 在JDK7之前(包括7)可以通过-XX:PermSize -XX:MaxPermSize来控制永久代的大小.
  • JDK8正式去除”永久代”,换成Metaspace(元空间)作为JVM虚拟机规范中方法区的实现。
  • 元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但仍可以通过参数控制:-XX:MetaspaceSize与-XX:MaxMetaspaceSize来控制大小。

下面作者还是通过一段代码,来不停的创建Class对象,在JDK8中可以看到metaSpace内存溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码/**
作者准备在JDK8下测试方法区,所以设置了Metaspace的大小为固定的8M
-XX:MetaspaceSize=8m
-XX:MaxMetaspaceSize=8m
*/
public class JavaMethodAreaOOM {

public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
//无限创建动态代理,生成Class对象
enhancer.create();
}
}

static class OOMObject {

}
}

在JDK8的环境下将报出异常:
Exception in thread “main” java.lang.OutOfMemoryError: Metaspace
这是因为在调用CGLib的创建代理时会生成动态代理类,即Class对象到Metaspace,所以While一下就出异常了。
提醒一下:虽然我们日常叫”堆Dump”,但是dump技术不仅仅是对于”堆”区域才有效,而是针对OOM的,也就是说不管什么区域,凡是能够报出OOM错误的,都可以使用dump技术生成dump文件来分析。

在经常动态生成大量Class的应用中,需要特别注意类的回收状况,这类场景除了例子中的CGLib技术,常见的还有,大量JSP,反射,OSGI等。需要特别注意,当出现此类异常,应该知道是哪里出了问题,然后看是调整参数,还是在代码层面优化。

附加-直接内存异常

直接内存异常非常少见,而且机制很特殊,因为直接内存不是直接向操作系统分配内存,而且通过计算得到的内存不够而手动抛出异常,所以当你发现你的dump文件很小,而且没有明显异常,只是告诉你OOM,你就可以考虑下你代码里面是不是直接或者间接使用了NIO而导致直接内存溢出。

好了,”JVM系列之实战内存溢出异常”到这里就给大家介绍完了,Have a good day .欢迎留言指错。

往期入口:

  1. JVM系列之Java内存结构详解

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

FastDFS的介绍与安装配置

发表于 2017-11-23

本篇初步介绍FastDFS,并且在CentOS上安装FastDFS。

基本介绍:

FastDFS是比较优秀的分布式文件系统。它分为服务端和客户端API两个部分,服务端又分成了跟踪器(Tracker)和服务节点(Storage)两个角色。跟踪器主要负责服务调度,起着负载均衡的作用;存储节点主要负责文件的存储、读取和同步等功能;客户端API来提供文件的上传与下载和删除的功能。

实际使用中,我们往往是使用客户端来连接跟踪器服务器集群,跟踪器管理存储节点集群,存储节点集群来为客户端提供可用的存储节点。存储节点的文件使用了分卷或者分组的组织方式,在FastDFS上一个文件的标识由卷名(或者是组名)、路径和文件名等部分组成。FastDFS支持动态扩展,我们可以通过增加新卷或者新组的方式来增加更多的存储节点。

安装步骤:

我们要安装FastDFS需要3台主机,这里我使用3台模拟器来模拟。其中的ip地址分别为:192.168.33.3 192.168.33.4 192.168.33.5,其中将192.168.33.3来作为TrackerServer,其他的两台主机作为StorageServer

  1. 登录TrackerServer,下载各个安装包:
1
2
3
4
5
6
复制代码下载FastDFS
wget http://jaist.dl.sourceforge.net/project/fastdfs/FastDFS%20Server%20Source%20Code/FastDFS%20Server%20with%20PHP%20Extension%20Source%20Code%20V5.01/FastDFS_v5.01.tar.gz
下载Nginx
wget http://nginx.org/download/nginx-1.7.0.tar.gz
下载FastDFS-Nginx-Module
wget http://jaist.dl.sourceforge.net/project/fastdfs/FastDFS%20Nginx%20Module%20Source%20Code/fastdfs-nginx-module_v1.16.tar.gz
  1. 安装C语言的编译环境:
1
复制代码yum -y install gcc gcc+ gcc-c++ openssl openssl-devel pcre pcre-deve
  1. 创建系统用户:
1
2
复制代码useradd fastdfs -M -s /sbin/nologin
useradd nginx -M -s /sbin/nologin
  1. 安装FastDFS:
1
2
3
4
复制代码tar -zxvf FastDFS_v5.01.tar.gz
cd FastDFS
./make.sh
./make.sh install
  1. 安装Nginx
1
2
3
4
5
6
7
复制代码tar -zxvf fast-nginx-module_v1.16.tar.gz
tar -zxvf nginx-1.7.0.tar.gz
cd nginx-1.7.0
./configure --user=nginx --group=nginx --prefix=/usr/local/nginx --add-module=../fastdfs-nginx-module/src
注意--add-module=../fastdfs-nginx-module/src这个配置项只在两个StorageServer上添加
make
make install
  1. Tracker Server的配置
1. 创建数据以及日志存放目录:



1
复制代码mkdir -p /data/fastdfs/tracker
2. 修改tracker.conf配置
1
2
3
4
复制代码vim /etc/fdfs/tracker.conf
修改:
base_path=/data/fastdfs/tracker
group_name=group1
3. 修改nginx.conf配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码vim /usr/local/nginx/conf/nginx.conf
配置为:

user nginx nginx;
worker_processes 4;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

pid /usr/local/nginx/nginx.pid;
worker_rlimit_nofile 51200;

events {
use epoll;
worker_connections 20480;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
复制代码                http {
include mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /usr/local/nginx/logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

upstream server_group1{
server 192.168.33.4;
server 192.168.33.5;
}
server {
listen 80;
server_name 192.168.33.3;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html;
index index.html index.htm;
}
location /group1{
include proxy.conf;
proxy_pass http://server.group1;
}

}



4. 配置TrackerServer的启动程序:

cp /usr/local/app/fastdfs/FastDFS/init.d/fdfs_trackerd /etc/init.d/
chkconfig --add fdfs_trackerd
chkconfig fdfs_trackerd on
  1. 进行StorageServer的配置。
1. 首先创建数据以及日志的保存目录:



1
复制代码mkdir -p /data/fastdfs/storage/data
2. 修改storage.conf配置:
1
2
3
4
5
6
7
8
9
10
复制代码vim /etc/fdfs/storage.conf
其中:
group_name=group1
base_path=/data/fastdfs
store_path0=/data/fastdfs/storage
tracker_server=192.168.33.3:22122
run_by_group=fastdfs
run_by_user=fastdfs
file_distribute_path_mode=1
rotate_error_log=true
3. 修改mod\_fastdfs.conf配置。
1
2
3
4
5
6
7
8
9
复制代码cp /usr/local/app/fastdfs/fastdfs-nginx-module/src/mod_fastdfs.conf /etc/fdfs
vim /etc/fdfs/mod_fastdfs.conf
其中:
connect_timeout=10
tracker_server=192.168.33.3:22122
group_name=group1
url_have_group_name = true
store_path_count=1
store_path0=/data/fastdfs/storage
4. 配置nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
复制代码    access_log  /usr/local/nginx/logs/access.log  main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
listen 88;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html;
index index.html index.htm;
}

"/usr/local/nginx/conf/nginx.conf" 128L, 2960C 43,9 29%

http {
include mime.types;
server_names_hash_bucket_size 128;
client_header_buffer_size 32k;
large_client_header_buffers 4 32k;
client_max_body_size 20m;
limit_rate 1024k;

default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /usr/local/nginx/logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
listen 88;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html;
index index.html index.htm;
}

location /group1/M00{
root /data/fastdfs/storage/data;
ngx_fastdfs_module;
}
#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#\}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#\}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#\}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
复制代码                    # another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#\}


# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;

# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;

# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;

# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;

# location / {
# root html;
# index index.html index.htm;
# }
#\}

}
创建软连接:
ln -s /data/fastdfs/storage/data /data/fastdfs/storage/data/M00
5. 配置Storage的启动程序:

cp /usr/local/app/fastdfs/FastDFS/init.d/fdfs_storaged /etc/init.d/
chkconfig --add fdfs_storaged
chkconfig fdfs_storaged on
  1. 启动TrackerServer:
1
复制代码service fdfs_trackerd start
  1. 启动StorageServer:
1
复制代码service fdfs_storaged start
  1. 启动Tracker和StorageServer的nginx服务器:
1
复制代码./usr/local/nginx/sbin/nginx
  1. 在TrackerServer中配置一个客户端:
1
2
3
4
复制代码vim /etc/fdfs/client.conf
其中:
base_path=/data/fastdfs
tracker_server=192.168.33.3:22122
  1. 进行文件上传:

fdfs_upload_file /etc/fdfs/client.conf iPhoneX.jpg

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Goroutine调度实例简要分析

发表于 2017-11-23

前两天一位网友在微博私信我这样一个问题:

抱歉打扰您咨询您一个关于Go的问题:对于goroutine的概念我是明了的,但很疑惑goroutine的调度问题, 根据《Go语言编程》一书:“当一个任务正在执行时,外部没有办法终止它。要进行任务切换,只能通过由该任务自身调用yield()来主动出让CPU使用权。” 那么,假设我的goroutine是一个死循环的话,是否其它goroutine就没有执行的机会呢?我测试的结果是这些goroutine会轮流执行。那么除了
syscall时会主动出让cpu时间外,我的死循环goroutine 之间是怎么做到切换的呢?

我在第一时间做了回复。不过由于并不了解具体的细节,我在答复中做了一个假定,即假定这位网友的死循环带中没有调用任何可以交出执行权的代码。事后,这位网友在他的回复后道出了死循环goroutine切换的真实原因:他在死循环中调用了fmt.Println。

事后总觉得应该针对这个问题写点什么? 于是就构思了这样一篇文章,旨在循着这位网友的思路通过一些例子来step by step演示如何分析go schedule。如果您对Goroutine的调度完全不了解,那么请先读一读这篇前导文 《也谈goroutine调度器》。

一、为何在deadloop的参与下,多个goroutine依旧会轮流执行

我们先来看case1,我们顺着那位网友的思路来构造第一个例子,并回答:“为何在deadloop的参与下,多个goroutine依旧会轮流执行?”这个问题。下面是case1的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码//github.com/bigwhite/experiments/go-sched-examples/case1.go
package main

import (
"fmt"
"time"
)

func deadloop() {
for {
}
}

func main() {
go deadloop()
for {
time.Sleep(time.Second * 1)
fmt.Println("I got scheduled!")
}
}

在case1.go中,我们启动了两个goroutine,一个是main goroutine,一个是deadloop goroutine。deadloop goroutine顾名思义,其逻辑是一个死循环;而main goroutine为了展示方便,也用了一个“死循环”,并每隔一秒钟打印一条信息。在我的macbook air上运行这个例子(我的机器是两核四线程的,runtime的NumCPU函数返回4):

1
2
3
4
5
复制代码$go run case1.go
I got scheduled!
I got scheduled!
I got scheduled!
... ...

从运行结果输出的日志来看,尽管有deadloop goroutine的存在,main goroutine仍然得到了调度。其根本原因在于机器是多核多线程的(硬件线程哦,不是操作系统线程)。Go从1.5版本之后将默认的P的数量改为 = CPU core的数量(实际上还乘以了每个core上硬线程数量),这样case1在启动时创建了不止一个P,我们用一幅图来解释一下:

img{512x368}

我们假设deadloop Goroutine被调度与P1上,P1在M1(对应一个os kernel thread)上运行;而main goroutine被调度到P2上,P2在M2上运行,M2对应另外一个os kernel thread,而os kernel threads在操作系统调度层面被调度到物理的CPU core上运行,而我们有多个core,即便deadloop占满一个core,我们还可以在另外一个cpu core上运行P2上的main goroutine,这也是main goroutine得到调度的原因。

Tips: 在mac os上查看你的硬件cpu core数量和硬件线程总数量:

1
2
3
4
复制代码$sysctl -n machdep.cpu.core_count
2
$sysctl -n machdep.cpu.thread_count
4

二、如何让deadloop goroutine以外的goroutine无法得到调度?

如果我们非要deadloop goroutine以外的goroutine无法得到调度,我们该如何做呢?一种思路:让Go runtime不要启动那么多P,让所有用户级的goroutines在一个P上被调度。

三种办法:

  • 在main函数的最开头处调用runtime.GOMAXPROCS(1);
  • 设置环境变量export GOMAXPROCS=1后再运行程序
  • 找一个单核单线程的机器^0^(现在这样的机器太难找了,只能使用云服务器实现)

我们以第一种方法为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码//github.com/bigwhite/experiments/go-sched-examples/case2.go
package main

import (
"fmt"
"runtime"
"time"
)

func deadloop() {
for {
}
}

func main() {
runtime.GOMAXPROCS(1)
go deadloop()
for {
time.Sleep(time.Second * 1)
fmt.Println("I got scheduled!")
}
}

运行这个程序后,你会发现main goroutine的”I got scheduled”字样再也无法输出了。这里的调度原理可以用下面图示说明:

img{512x368}

deadloop goroutine在P1上被调度,由于deadloop内部逻辑没有给调度器任何抢占的机会,比如:进入runtime.morestack_noctxt。于是即便是sysmon这样的监控goroutine,也仅仅是能给deadloop goroutine的抢占标志位设为true而已。由于deadloop内部没有任何进入调度器代码的机会,Goroutine重新调度始终无法发生。main goroutine只能躺在P1的local queue中徘徊着。

三、反转:如何在GOMAXPROCS=1的情况下,让main goroutine得到调度呢?

我们做个反转:如何在GOMAXPROCS=1的情况下,让main goroutine得到调度呢?听说在Go中 “有函数调用,就有了进入调度器代码的机会”,我们来试验一下是否属实。我们在deadloop goroutine的for-loop逻辑中加上一个函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码// github.com/bigwhite/experiments/go-sched-examples/case3.go
package main

import (
"fmt"
"runtime"
"time"
)

func add(a, b int) int {
return a + b
}

func deadloop() {
for {
add(3, 5)
}
}

func main() {
runtime.GOMAXPROCS(1)
go deadloop()
for {
time.Sleep(time.Second * 1)
fmt.Println("I got scheduled!")
}
}

我们在deadloop goroutine的for loop中加入了一个add函数调用。我们来运行一下这个程序,看是否能达成我们的目的:

1
复制代码$ go run case3.go

“I got scheduled!”字样依旧没有出现在我们眼前!也就是说main goroutine没有得到调度!为什么呢?其实所谓的“有函数调用,就有了进入调度器代码的机会”,实际上是go compiler在函数的入口处插入了一个runtime的函数调用:runtime.morestack_noctxt。这个函数会检查是否扩容连续栈,并进入抢占调度的逻辑中。一旦所在goroutine被置为可被抢占的,那么抢占调度代码就会剥夺该Goroutine的执行权,将其让给其他goroutine。但是上面代码为什么没有实现这一点呢?我们需要在
汇编层次看看go compiler生成的代码是什么样子的。

查看Go程序的汇编代码有许多种方法:

  • 使用objdump工具:objdump -S go-binary
  • 使用gdb disassemble
  • 构建go程序同时生成汇编代码文件:go build -gcflags ‘-S’ xx.go > xx.s 2>&1
  • 将Go代码编译成汇编代码:go tool compile -S xx.go > xx.s
  • 使用go tool工具反编译Go程序:go tool objdump -S go-binary > xx.s

我们这里使用最后一种方法:利用go tool objdump反编译(并结合其他输出的汇编形式):

1
2
复制代码$go build -o case3 case3.go
$go tool objdump -S case3 > case3.s

打开case3.s,搜索main.add,我们居然找不到这个函数的汇编代码,而main.deadloop的定义如下:

1
2
3
4
5
6
7
8
9
10
复制代码TEXT main.deadloop(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go
for {
0x1093a10 ebfe JMP main.deadloop(SB)

0x1093a12 cc INT $0x3
0x1093a13 cc INT $0x3
0x1093a14 cc INT $0x3
0x1093a15 cc INT $0x3
... ...
0x1093a1f cc INT $0x3

我们看到deadloop中对add的调用也消失了。这显然是go compiler执行生成代码优化的结果,因为add的调用对deadloop的行为结果没有任何影响。我们关闭优化再来试试:

1
2
复制代码$go build -gcflags '-N -l' -o case3-unoptimized case3.go
$go tool objdump -S case3-unoptimized > case3-unoptimized.s

打开 case3-unoptimized.s查找main.add,这回我们找到了它:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码TEXT main.add(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go
func add(a, b int) int {
0x1093a10 48c744241800000000 MOVQ $0x0, 0x18(SP)
return a + b
0x1093a19 488b442408 MOVQ 0x8(SP), AX
0x1093a1e 4803442410 ADDQ 0x10(SP), AX
0x1093a23 4889442418 MOVQ AX, 0x18(SP)
0x1093a28 c3 RET

0x1093a29 cc INT $0x3
... ...
0x1093a2f cc INT $0x3

deadloop中也有了对add的显式调用:

1
2
3
4
5
6
7
8
9
复制代码TEXT main.deadloop(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go
... ...
0x1093a51 48c7042403000000 MOVQ $0x3, 0(SP)
0x1093a59 48c744240805000000 MOVQ $0x5, 0x8(SP)
0x1093a62 e8a9ffffff CALL main.add(SB)
for {
0x1093a67 eb00 JMP 0x1093a69
0x1093a69 ebe4 JMP 0x1093a4f
... ...

不过我们这个程序中的main goroutine依旧得不到调度,因为在main.add代码中,我们没有发现morestack函数的踪迹,也就是说即便调用了add函数,deadloop也没有机会进入到runtime的调度逻辑中去。

不过,为什么Go compiler没有在main.add函数中插入morestack的调用呢?那是因为add函数位于调用树的leaf(叶子)位置,compiler可以确保其不再有新栈帧生成,不会导致栈分裂或超出现有栈边界,于是就不再插入morestack。而位于morestack中的调度器的抢占式检查也就无法得以执行。下面是go build -gcflags ‘-S’方式输出的case3.go的汇编输出:

1
2
3
4
5
6
7
8
9
复制代码"".add STEXT nosplit size=19 args=0x18 locals=0x0
TEXT "".add(SB), NOSPLIT, $0-24
FUNCDATA $0, gclocals·54241e171da8af6ae173d69da0236748(SB)
FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
MOVQ "".b+16(SP), AX
MOVQ "".a+8(SP), CX
ADDQ CX, AX
MOVQ AX, "".~r2+24(SP)
RET

我们看到nosplit字样,这就说明add使用的栈是固定大小,不会再split,且size为24字节。

关于在for loop中的leaf function是否应该插入morestack目前还有一定争议,将来也许会对这样的情况做特殊处理。

既然明白了原理,我们就在deadloop和add之间加入一个dummy函数,见下面case4.go代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码//github.com/bigwhite/experiments/go-sched-examples/case4.go
package main

import (
"fmt"
"runtime"
"time"
)

//go:noinline
func add(a, b int) int {
return a + b
}

func dummy() {
add(3, 5)
}

func deadloop() {
for {
dummy()
}
}

func main() {
runtime.GOMAXPROCS(1)
go deadloop()
for {
time.Sleep(time.Second * 1)
fmt.Println("I got scheduled!")
}
}

执行该代码:

1
2
3
4
复制代码$go run case4.go
I got scheduled!
I got scheduled!
I got scheduled!

Wow! main goroutine果然得到了调度。我们再来看看go compiler为程序生成的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
复制代码$go build -gcflags '-N -l' -o case4 case4.go
$go tool objdump -S case4 > case4.s

TEXT main.add(SB) github.com/bigwhite/experiments/go-sched-examples/case4.go
func add(a, b int) int {
0x1093a10 48c744241800000000 MOVQ $0x0, 0x18(SP)
return a + b
0x1093a19 488b442408 MOVQ 0x8(SP), AX
0x1093a1e 4803442410 ADDQ 0x10(SP), AX
0x1093a23 4889442418 MOVQ AX, 0x18(SP)
0x1093a28 c3 RET

0x1093a29 cc INT $0x3
0x1093a2a cc INT $0x3
... ...

TEXT main.dummy(SB) github.com/bigwhite/experiments/go-sched-examples/case4.s
func dummy() {
0x1093a30 65488b0c25a0080000 MOVQ GS:0x8a0, CX
0x1093a39 483b6110 CMPQ 0x10(CX), SP
0x1093a3d 762e JBE 0x1093a6d
0x1093a3f 4883ec20 SUBQ $0x20, SP
0x1093a43 48896c2418 MOVQ BP, 0x18(SP)
0x1093a48 488d6c2418 LEAQ 0x18(SP), BP
add(3, 5)
0x1093a4d 48c7042403000000 MOVQ $0x3, 0(SP)
0x1093a55 48c744240805000000 MOVQ $0x5, 0x8(SP)
0x1093a5e e8adffffff CALL main.add(SB)
}
0x1093a63 488b6c2418 MOVQ 0x18(SP), BP
0x1093a68 4883c420 ADDQ $0x20, SP
0x1093a6c c3 RET

0x1093a6d e86eacfbff CALL runtime.morestack_noctxt(SB)
0x1093a72 ebbc JMP main.dummy(SB)

0x1093a74 cc INT $0x3
0x1093a75 cc INT $0x3
0x1093a76 cc INT $0x3
.... ....

我们看到main.add函数依旧是leaf,没有morestack插入;但在新增的dummy函数中我们看到了CALL runtime.morestack_noctxt(SB)的身影。

四、为何runtime.morestack_noctxt(SB)放到了RET后面?

在传统印象中,morestack是放在函数入口处的。但实际编译出来的汇编代码中(见上面函数dummy的汇编),runtime.morestack_noctxt(SB)却放在了RET的后面。解释这个问题,我们最好来看一下另外一种形式的汇编输出(go build -gcflags ‘-S’方式输出的格式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码"".dummy STEXT size=68 args=0x0 locals=0x20
0x0000 00000 TEXT "".dummy(SB), $32-0
0x0000 00000 MOVQ (TLS), CX
0x0009 00009 CMPQ SP, 16(CX)
0x000d 00013 JLS 61
0x000f 00015 SUBQ $32, SP
0x0013 00019 MOVQ BP, 24(SP)
0x0018 00024 LEAQ 24(SP), BP
... ...
0x001d 00029 MOVQ $3, (SP)
0x0025 00037 MOVQ $5, 8(SP)
0x002e 00046 PCDATA $0, $0
0x002e 00046 CALL "".add(SB)
0x0033 00051 MOVQ 24(SP), BP
0x0038 00056 ADDQ $32, SP
0x003c 00060 RET
0x003d 00061 NOP
0x003d 00061 PCDATA $0, $-1
0x003d 00061 CALL runtime.morestack_noctxt(SB)
0x0042 00066 JMP 0

我们看到在函数入口处,compiler插入三行汇编:

1
2
3
复制代码        0x0000 00000 MOVQ    (TLS), CX  // 将TLS的值(GS:0x8a0)放入CX寄存器
0x0009 00009 CMPQ SP, 16(CX) //比较SP与CX+16的值
0x000d 00013 JLS 61 // 如果SP > CX + 16,则jump到61这个位置

这种形式输出的是标准Plan9的汇编语法,资料很少(比如JLS跳转指令的含义),注释也是大致猜测的。如果跳转,则进入到 runtime.morestack_noctxt,从 runtime.morestack_noctxt返回后,再次jmp到开头执行。

为什么要这么做呢?按照go team的说法,是为了更好的利用现代CPU的“static branch prediction”,提升执行性能。

五、参考资料

  • 《A Quick Guide to Go’s Assembler》
  • 《Go’s work-stealing scheduler》

文中的代码可以点击这里下载。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

微信赞赏:
img{512x368}

© 2017, bigwhite. 版权所有.

Related posts:

  1. 近期遇到的3个Golang代码问题
  2. Go 1.6中值得关注的几个变化
  3. 搭建你自己的Go Runtime metrics环境
  4. 也谈goroutine调度器
  5. Go 1.5中值得关注的几个变化

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用Spring Cloud和Docker构建微服务架构

发表于 2017-11-23

本文讲述如何使用Spring Boot、Spring Cloud、Docker和Netflix的一些开源工具来构建一个微服务架构。

通过使用Spring Boot、Spring Cloud和Docker构建的概念型应用示例,提供了解常见的微服务架构模式的起点。

我选择了一个老项目作为这个系统的基础,它的后端以前是单一应用。此应用提供了处理个人财务、整理收入开销、管理储蓄、分析统计和创建简单预测等功能。

功能服务

整个应用分解为三个核心微服务。它们都是可以独立部署的应用,围绕着某些业务功能进行组织。

使用Spring Cloud和Docker构建微服务架构使用Spring
Cloud和Docker构建微服务架构

账户服务

包含一般用户输入逻辑和验证:收入/开销记录、储蓄和账户设置。

方法 路径 描述 用户验证 UI可用
GET /accounts/{account} 获取指定账户数据
GET /accounts/current 获取当前账户数据 x x
GET /accounts/demo 获取演示账户数据(预先填入收入/开销记录等) x
PUT /accounts/current 保存当前账户数据 x x
POST /accounts/ 注册新账户 x

统计服务

计算主要的统计参数,并捕获每一个账户的时间序列。数据点包含基于货币和时间段正常化后的值。该数据可用于跟踪账户生命周期中的现金流量动态。

方法 路径 描述 用户验证 UI可用
GET /statistics/{account} 获取指定账户统计数据
GET /statistics/current 获取当前账户的统计数据 x x
GET /statistics/demo 获取演示账户统计数据 x
PUT /statistics/{account} 创建或更新指定账户的时间序列数据点。

通知服务

存储用户的联系信息和通知设置(如提醒和备份频率)。安排工作人员从其它服务收集所需的信息并向订阅的客户发送电子邮件。

方法 路径 描述 用户验证 UI可用
GET /notifications/settings/current 获取当前账户的通知i设置 x x
PUT /notifications/settings/current 保存当前账户的通知设置 x x

注意

  • 每一个微服务拥有自己的数据库,因此没有办法绕过API直接访问持久数据。
  • 在这个项目中,我使用MongoDB作为每一个服务的主数据库。拥有一个多种类持久化架构(polyglot persistence architecture)也是很有意义的。
  • 服务间(Service-to-service)通信是非常简单的:微服务仅使用同步的REST API进行通信。现实中的系统的常见做法是使用互动风格的组合。例如,执行同步的GET请求检索数据,并通过消息代理(broker)使用异步方法执行创建/更新操作,以便解除服务和缓冲消息之间的耦合。然而,这带给我们是最终的一致性。

基础设施服务

分布式系统中常见的模式,可以帮助我们描述核心服务是怎样工作的。Spring Cloud提供了强大的工具,可以增强Spring Boot应用的行为来实现这些模式。我会简要介绍一下:

基础设施服务基础设施服务

配置服务

Spring Cloud Config是分布式系统的水平扩展集中式配置服务。它使用了当前支持的本地存储、Git和Subversion等可拔插存储库层(repository layer)。

在此项目中,我使用了native profile,它简单地从本地classpath下加载配置文件。您可以在配置服务资源中查看shared目录。现在,当通知服务请求它的配置时,配置服务将响应回shared/notification-service.yml和shared/application.yml(所有客户端应用之间共享)。

客户端使用

只需要使用sprng-cloud-starter-config依赖构建Spring Boot应用,自动配置将会完成其它工作。

现在您的应用中不需要任何嵌入的properties,只需要提供有应用名称和配置服务url的bootstrap.yml即可:

复制代码1234567 复制代码spring: application: name: notification-service cloud: config: uri: http://config:8888 fail-fast: true

使用Spring Cloud Config,您可以动态更改应用配置

比如,EmailService bean使用了@RefreshScope注解。这意味着您可以更改电子邮件的内容和主题,而无需重新构建和重启通知服务应用。

首先,在配置服务器中更改必要的属性。然后,对通知服务执行刷新请求:curl -H "Authorization: Bearer #token#" -XPOST http://127.0.0.1:8000/notifications/refresh。

您也可以使用webhook来自动执行此过程。

注意

  • 动态刷新存在一些限制。@RefreshScope不能和@Configuraion类一同工作,并且不会作用于@Scheduled方法。
  • fail-fast属性意味着如果Spring Boot应用无法连接到配置服务,将会立即启动失败。当您一起启动所有应用时,这非常有用。
  • 下面有重要的安全提示

授权服务

负责授权的部分被完全提取到单独的服务器,它为后端资源服务提供OAuth2令牌。授权服务器用于用户授权以及在周边内进行安全的机器间通信。

在此项目中,我使用密码凭据作为用户授权的授权类型(因为它仅由本地应用UI使用)和客户端凭据作为微服务授权的授权类型。

Spring Cloud Security提供了方便的注解和自动配置,使其在服务器端或者客户端都可以很容易地实现。

从客户端来看,一切都与传统的基于会话的授权完全相同。您可以从请求中检索Principal对象、检查用户角色和其它基于表达式访问控制和@PreAuthorize注解的内容。

PiggyMetrics(帐户服务、统计服务、通知服务和浏览器)中的每一个客户端都有一个范围:用于后台服务的服务器、用于浏览器展示的UI。所以我们也可以保护控制器避免受到外部访问,例如:

复制代码12345 复制代码@PreAuthorize("#oauth2.hasScope('server')")@RequestMapping(value = "accounts/{name}", method = RequestMethod.GET)public List getStatisticsByAccountName(@PathVariable String name) {return statisticsService.findByAccountName(name);}

API网关

您可以看到,有三个核心服务。它们向客户端暴露外部API。在现实系统中,这个数量可以非常快速地增长,同时整个系统将变得非常复杂。实际上,一个复杂页面的渲染可能涉及到数百个服务。

理论上,客户端可以直接向每个微服务直接发送请求。但是这种方式是存在挑战和限制的,如果需要知道所有端点的地址,分别对每一段信息执行http请求,将结果合并到客户端。另一个问题是,这不是web友好协议,可能只在后端使用。

通常一个更好的方法是使用API网关。它是系统的单个入口点,用于通过将请求路由到适当的后端服务或者通过调用多个后端服务并聚合结果来处理请求。此外,它还可以用于认证、insights、压力测试、金丝雀测试(canary testing)、服务迁移、静态响应处理和主动变换管理。

Netflix开源这样的边缘服务,现在用Spring Cloud,我们可以用一个@EnabledZuulProxy注解来启用它。在这个项目中,我使用Zuul存储静态内容(UI应用),并将请求路由到适当的微服务。以下是一个简单的基于前缀(prefix-based)路由的通知服务配置:

复制代码123456 复制代码zuul: routes: notification-service: path: /notifications/** serviceId: notification-service stripPrefix: false

这意味着所有以/notification开头的请求将被路由到通知服务。您可以看到,里面没有硬编码的地址。Zuul使用服务发现机制来定位通知服务实例以及断路器和负载均衡器,如下所述。

服务发现

另一种常见的架构模式是服务发现。它允许自动检测服务实例的网络位置,由于自动扩展、故障和升级,它可能会动态分配地址。

服务发现的关键部分是注册。我使用Netflix Eureka进行这个项目,当客户端需要负责确定可以用的服务实例(使用注册服务器)的位置和跨平台的负载均衡请求时,Eureka就是客户端发现模式的一个很好的例子。

使用Spring Boot,您可以使用spring-cloud-starter-eureka-server依赖、@EnabledEurekaServer注解和简单的配置属性轻松构建Eureka注册中心(Eureka Registry)。

使用@EnabledDiscoveryClient注解和带有应用名称的bootstrap.yml来启用客户端支持:

复制代码123 复制代码spring: application: name: notification-service

现在,在应用启动时,它将向Eureka服务器注册并提供元数据,如主机和端口、健康指示器URL、主页等。Eureka接收来自从属于某服务的每个实例的心跳消息。如果心跳失败超过配置的时间表,该实例将从注册表中删除。

此外,Eureka还提供了一个简单的界面,您可以通过它来跟踪运行中的服务和可用实例的数量:http://localhost:8761

Eureka仪表盘Eureka仪表盘

负载均衡器、断路器和Http客户端

Netflix OSS提供了另一套很棒的工具。

Ribbon

Ribbon是一个客户端负载均衡器,可以很好地控制HTTP和TCP客户端的行为。与传统的负载均衡器相比,每次线上调用都不需要额外的跳跃——您可以直接联系所需的服务。

它与Spring Cloud和服务发现是集成在一起的,可开箱即用。Eureka客户端提供了可用服务器的动态列表,因此Ribbon可以在它们之间进行平衡。

Hystrix

Hystrix是断路器模式的一种实现,它可以通过网络访问依赖来控制延迟和故障。中心思想是在具有大量微服务的分布式环境中停止级联故障。这有助于快速失败并尽快恢复——自我修复在容错系统中是非常重要的。

除了断路器控制,在使用Hystrix,您可以添加一个备用方法,在主命令失败的情况下,该方法将被调用以获取默认值。

此外,Hystrix生成每个命令的执行结果和延迟的度量,我们可以用它来监视系统的行为。

Feign

Feign是一个声明式HTTP客户端,能与Ribbon和Hystrix无缝集成。实际上,通过一个spring-cloud-starter-feign依赖和@EnabledFeignClients注解,您可以使用一整套负载均衡器、断路器和HTTP客户端,并附带一个合理的的默认配置。

以下是账户服务的示例:

复制代码12345 复制代码@FeignClient(name = "statistics-service")public interface StatisticsServiceClient {@RequestMapping(method = RequestMethod.PUT, value = "/statistics/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)void updateStatistics(@PathVariable("accountName") String accountName, Account account);}
  • 您需要的只是一个接口
  • 您可以在Spring MVC控制器和Feign方法之间共享@RequestMapping部分
  • 以上示例仅指定所需要的服务ID——statistics-service,这得益于Eureka的自动发现(但显然您可以使用特定的URL访问任何资源)。

监控仪表盘

在这个项目配置中,Hystrix的每一个微服务都通过Spring Cloud Bus(通过AMQP broker)将指标推送到Turbine。监控项目只是一个使用了Turbine和Hystrix仪表盘的小型Spring Boot应用。

让我们看看系统行为在负载下:账户服务调用统计服务和它在一个变化的模拟延迟下的响应。响应超时阈值设置为1秒。

监控仪表盘监控仪表盘

日志分析

集中式日志记录在尝试查找分布式环境中的问题时非常有用。Elasticsearch、Logstash和Kibana技术栈可让您轻松搜索和分析您的日志、利用率和网络活动数据。在我的另一个项目中已经有现成的Docker配置。

安全

高级安全配置已经超过了此概念性项目的范围。为了更真实地模拟真实系统,考虑使用https和JCE密钥库来加密微服务密码和配置服务器的properties内容。

基础设施自动化

部署微服务比部署单一的应用的流程要复杂得多,因为它们相互依赖。拥有完全基础设置自动化是非常重要的。我们可以通过持续交付的方式获得以下好处:

  • 随时发布软件的能力。
  • 任何构建都可能最终成为一个发行版本。
  • 构建工件(artifact)一次,根据需要进行部署。

这是一个简单的持续交付工作流程,在这个项目的实现:

在此配置中,Travis CI为每一个成功的Git推送创建了标记镜像。因此,每一个微服务在Docker Hub上的都会有一个latest镜像,而较旧的镜像则使用Git提交的哈希进行标记。如果有需要,可以轻松部署任何一个,并快速回滚。

基础设施自动化基础设施自动化

如何运行全部?

这真的很简单,我建议您尝试一下。请记住,您将要启动8个Spring Boot应用、4个MongoDB实例和RabbitMq。确保您的机器上有4GB的内存。您可以随时通过网关、注册中心、配置、认证服务和账户中心运行重要的服务。

运行之前

  • 安装Docker和Docker Compose。
  • 配置环境变量:CONFIG_SERVICE_PASSWORD, NOTIFICATION_SERVICE_PASSWORD, STATISTICS_SERVICE_PASSWORD, ACCOUNT_SERVICE_PASSWORD, MONGODB_PASSWORD

生产模式

在这种模式下,所有最新的镜像都将从Docker Hub上拉取。只需要复制docker-compose.yml并执行docker-compose up -d即可。

开发模式

如果您想自己构建镜像(例如,在代码中进行一些修改),您需要克隆所有仓库(repository)并使用Mavne构建工件(artifact)。然后,运行docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

docker-compose.dev.yml继承了docker-compose.yml,附带额外配置,可在本地构建镜像,并暴露所有容器端口以方便开发。

重要的端点(Endpoint)

  • localhost:80 —— 网关
  • localhost:8761 —— Eureka仪表盘
  • localhost:9000 —— Hystrix仪表盘
  • localhost:8989 —— Turbine stream(Hystrix仪表盘来源)
  • localhost:15672 —— RabbitMq管理

注意

所有Spring Boot应用都需要运行配置服务器才能启动。得益于Spring Boot的fail-fast属性和docker-compsoe的restart:always选项,我们可以同时启动所有容器。这意味着所有依赖的容器将尝试重新启动,直到配置服务器启动运行为止。

此外,服务发现机制在所有应用启动后需要一段时间。在实例、Eureka服务器和客户端在其本地缓存中都具有相同的元数据之前,任何服务都不可用于客户端发现,因此可能需要3次心跳。默认的心跳周期为30秒。

原文:dzone.com/articles/mi…
作者:Alexander
Lukyanchikov
译者:Oopsguy

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一篇文章总结语言处理中的分词问题

发表于 2017-11-23

如何界定分词

中文分词指的是将一个汉字序列切分成一个一个单独的词。分词就是将连续的字序列按照一定的规范重新组合成词序列的过程;在英文中,单词之间是以空格作为自然分界符,汉语中词没有一个形式上的分界符。(见百度百科) 正因为缺乏形式上的分界符,导致我们对词的认定会出现很大的偏差。1996 年 Sproat 等通过对 6 个母语为汉语的人进行调研,让这 6 人对同一篇中文文本进行人工切分,文本包括 100 个句子,最后统计认同率,见下表:

不仅普通人有词语认识上的偏差,即使是语言专家,在这个问题上依然有不小的差异,这种差异反映在分词语料库上。不同语料库的数据无法直接拿过来混合训练。

以前曾经出过分词规范 (GB13715),以“结合紧密,使用稳定”作为分词建议,后来发现这个建议弹性太大,不同的人有不同的理解,无法有效实施。

为了统一对词语的认识,现在主要通过“分词规范、词表、分词语料库”来使得词语切分可计算,例如北大的“词语切分与词性标注”规范。基于上述种种工作,可以把词语切分问题变得可操作和标准化,大家在统一的平台上进行实验和比较。

对分词的诉求是什么?

从已有工程经验来看,几乎不存在通用而且效果非常好的分词系统,例如:在人民日报上训练的分词系统,在二次元的魔幻小说上切分效果不佳。每个领域有其独特的词汇表示,这很难通过有限的训练数据捕捉到所有语言现象。

不用使用场景对分词的要求差异很大。在搜索的索引阶段,往往会召回所有可能切分结果,对切分准确率要求不高,但对分词速度有很高的要求,例如某中型搜索系统,每天 4000 万篇文章入库,每秒要处理 500 篇文档,每秒处理的文档字节数约有 50MB;如果分词系统太慢的话,需要开大量线程才能处理这些文档。

在问答系统中,需要对文本实现较为深入的理解,对分词和实体识别的准确性要求很高。

不用的使用场景,对分词提出了不同的要求,不需要片面地追求高准确率。

别家系统的准确率怎么这么高?

在分词系统研发中,最容易产生误解的就是比较系统准确率。系统准确率与训练数据非常相关,脱离数据而谈论准确率无异于“刷流氓”。2003 年 863 分词评测中就出现了 98% 的准确率,2007 年 Sighan 评测中最高准确率是 97%,在最近某司组织的评测中,最高准确率下降到了 94%。所有不同数据下的评测结果都不能直接比较高低。

现在吹嘘分词准确率的公司和单位越来越少了。

分词稳定性很重要

分词稳定性是指分词系统的输出在不同上下文下都比较稳定,不会出现明显被上下文影响的情况。例如,在下面句子中,“黄代恒”有时识别为人名,第二次出现未识别出来:

实战 分享 三 黄代恒 /nr 与 轨道 交通 : 软硬 结合 到 人机 结合 黄代恒 “ 在 不同 的 客户 场景 下 , 我们 用 三 种 技术 实现 轨道 交通 的 数据 洞察

一般纯统计分词系统的稳定性比不上基于词典实现的分词系统。在搜索中,分词稳定性非常重要,否则极容易出现查询不到的情况。

已有分词系统小结

分词大概是投入人力非常大的 NLP 方向,几乎每一家“有追求”的公司都有员工实施过类似的任务,而且反复迭代更新;在 NLP 研究界,这个问题从上个世纪 80 年代就已经开始探索,一直到 ACL 2017 仍然有这方面的论文 (有 4 篇丛神经网络角度探索分词的文章)。

如此多的人力投入到分词理论研发和工程研发中,产生了一批各有特色的分词系统。下面仅仅就本人接触到的系统作说明 (排名无先后),比较“古老”的系统不在此罗列:

IK 系统

该系统采用 JAVA 开发,实现逻辑不复杂,由于对 Lucene 和 ES 支持较好,因而得到了比较普遍的使用。该系统可以实现英文单词、中文单词的切分,OOV 识别能力不强。该系统有几种使用模式,分别对应不同的使用场景,如索引、查询等。

IK 有些功能比较贴心,比如热更新用户词典,支持用户自定义词典非常方面,对搜索工程师比较友好。

IK 的代码实现上优化不够好,甚至能发现 BUG。我们发现的 BUG 会导致 ES 中长 Query 无法实现精准匹配。

对于中小企业的内部应用来说,使用 IK 足够了。在商业系统上使用的话,要非常慎重,参与优化的人员太少了。

Jieba 系统

Jieba 大概是最好用的基于 Python 实现的分词系统了,2-3 行代码就可以实现分词调用和词性标注,速度还不错。基于 HMM 模型实现,可以实现一定程度的未登录词识别。

Jieba 有精确模式、全模式、搜索模式三种。全模式是找到所有可能词语;搜索模式是在精确模式的基础上对长词进行切分,提高召回率。

支持繁体分词;支持自定义词典;支持并行分词,方便实现加速。

在分词速度上,精确模式能达到 400KB/ 秒,全模式下能达到 1.5MB/ 秒。

Jieba 除了 Python 版本外,还有多种语言实现的版本,包括 C++, JAVA, Golang 等。

Java 版本的 Jieba 功能上受限,仅面向搜索使用。明略 SCOPA 产品中使用了 Java 版本的 Jieba 作为分词组件,替换了 IK。

Ansj 系统

Ansj 是前同事阿健的作品。

Ansj 的所有模块全部自研,没有借用开源组件,这有助于降低包依赖。Ansj 基于 CRF 模型实现,支持导入 CRF++ 的训练结果,CRF 的推导部分全部自己实现。

对词典的双数组进行了优化,支持自定义词表,分词速度大概是 3MB/ 秒。

Ansj 的使用非常简单,几行代码即可,所有数据都打包到了 jar 中。

Ansj 的不足在于,模型的识别准确率有待提高,参与开发的人数太少。

Hanlp 平台

Hanlp 是一个功能非常全面的 NLP 平台,它的分词接口借鉴了 Ansj 的设计,形式上和命名上都非常像。

Hanlp 有“简约版”和“加强版”,简约版的模型参数较小,分词能力还可以;加强版在模型参数上扩大了若干倍,分词能力进一步提升。

Hanlp 支持基于 HMM 模型的分词、支持索引分词、繁体分词、简单匹配分词(极速模式)、基于 CRF 模型的分词、N- 最短路径分词等。实现了不少经典分词方法。

Hanlp 的部分模块做了重要优化,比如双数组,匹配速度很快,可以直接拿过来使用。

Hanlp 做了不少重现经典算法的工作,但这对于工业化的分词系统来说没有太大意义。

ICTCLAS 系统

ICTCLAS 大概是“最知名”的分词系统了,从参加 2003 年中文分词评测,一直延续到了现在。现在已经是商业系统了 (改名 NLPIR),需要 License 才能运行。

从未登录词识别准确率上说,ICTCLAS 已经明显落后于基于 CRF 的分词系统了。

尽管如此,它的优点仍然比较明显:很少出现“错得离谱”的切分结果,这在基于 CRF 模型的分词系统上不少见,尤其是迁移到其它领域时;模型和库不大,启动快;基于 C++ 实现,能够很快迁移到其它语言。

从分词稳定性上来说,ICTCLAS 值得信赖,从分词准确率、分词速度等方面来考量,有不少分词系统超过了它;NLPIR 的源代码已经不再开放,这让用户很纠结。

交大分词

所谓“交大分词”,是指上交大赵海老师个人主页上的分词系统。该系统在 2007 年 Sighan 评测中拿到了多项第一。

该系统基于 CRF 模型构建,在模型特征提取上做了大量工作,分词准确率比较高。目前可用版本支持简体、繁体分词,也支持不同分词标准。该系统被常常用来比较分词准确率。

该系统的问题是不开源,只有 Windows 上的可执行文件,C++ 源码需要向作者申请。虽然该系统不开源,但作者的一系列论文基本上揭示了其原理,复制起来并不难。

从工程角度来考虑,该系统只适合做 DEMO 组件,不适合大规模工业化使用。

Stanford 分词

Stanford 分词系统的优点是准确率高,未登录词识别能力比较强;缺点非常明显,模型很大,约 300MB-400MB,启动非常慢,大概需要 10 秒 -20 秒。在所有分词系统中,没有比 Stanford 启动更慢的系统,分词速度也不快。代码优化的空间比较大。

Stanford 系统支持自定义训练,只要用户提供训练数据,该系统可以训练新的模型参数。

Stanford 分词系统只是验证作者论文的一种手段,为了非常微小的分词准确率提升,导致了模型参数膨胀。

在 Demo 环境下可以使用 Stanford 系统,在大规模数据环境下不适合使用该系统。

GPWS 系统

GPWS 是北京语言大学语言信息处理研究所研发的分词系统,2001 年对外发布。该分词系统是 2000 年后唯一一个基于大规模规则 + 统计的分词系统(仅限个人所知),在 2004 年非常低的硬件配置下,分词速度也能达到 3MB-5MB/ 秒,对系统资源的消耗很低。后来授权给了新浪、微软等公司使用,被应用在了信息检索中。

GPWS 可以实现中文人名、外国人名、日本人名的识别,其它分词系统几乎都没有做到这个程度;对通用领域的文本切分效果较好,支持自定义词典;很少出现切分“离谱”的情况。该系统适合大规模数据处理的场景。

上述所有系统几乎都依赖于训练数据,而 GPWS 没有这方面的问题。GPWS 依赖于高质量分词词典和歧义切分机制,采用基于可信度的人名识别方法,不依赖于公开的训练数据。

GPWS 最大的问题在于很难复制,代码没有公开;在分词准确率上,GPWS 已经比不上字本位的分词系统;但从分词稳定性上,GPWS 仍然非常出色,比纯统计分词系统明显要好。

某巨头公司的分词

该分词系统的综合表现应该是最佳的,无论是启动速度、分词准确率、未登录词识别准确率等;由于使用了大规模分类词表,对于各领域的切分效果都比较不错,很少出现切分“离谱”的情况。

从实现来看,对于未登录词识别使用了 CRF 模型;对于人名识别,使用了最大熵模型;对于数字、日期等,使用了有限状态自动机;对于词性标注,使用了 HMM 模型。

除了以上这些分词系统外,还有商业系统如海量分词、Bosen 分词等。商业系统的分词效果要明显好过开源系统。例如 Bosen 系统对于企业名字的识别效果好得惊人。下面为 Bosen 公司进行的分词评测:

分词的难点在哪里?

歧义

歧义问题与词长非常相关,词语越短,发生歧义的可能性越大,词语越长,发生歧义的可能性越低,很少出现成语与其他词发生歧义的情况。歧义问题在分词中不是罪严重的问题,仅占分词错误数的 10% 左右。歧义分类包括:

  • 交集型歧义

abc -> 可以切分为 ab c 和 a bc,占所有歧义总量的 95%,也就是说歧义问题主要是指交集型歧义

例如:

研究生命的起源 | 研究生 命 的起源

这种环境下 工作 | 这种环境 下工 作

化妆 和 服装 | 化妆 和服 装

这群 山里的娃娃 |这 群山 里的娃娃

进书店 跟 进超市一样 | 进书店 跟进 超市一样

  • 组合型歧义

abc ->可以切分为 abc 和 a bc 或 abc。

组合型歧义一般要通过前后邻接搭配的可能性大小来判断。

他从 马上 下来 | 他从 马 上 下来

这个门 把手 坏了 | 这个门 把 手 坏了

  • 基于马尔科夫模型计算邻接搭配可以消除绝大部分歧义。

通过计算词语搭配的概率估计句子的概率,选择概率最大的结果即可。

分词错误的主要来源

未登录词 - 不在词典中的词,该问题在文本中出现频度远远高于歧义。

未登录词的类型包括:人名、地名、机构名、公司名、数字、日期、专业术语、新词、产品名等。一般把人名、地名、机构名、公司名叫命名实体,例如:

卢靖姗一夜爆红 (人名)

在东四十条站台见面 (地点)

银联的小兄弟网联成立了 (机构名)

公元 2017 年 8 月 24 日发生一件大事(日期)

中国外汇储备达到三点 94 万亿美元(数字)

在明略软件做大数据处理 (公司名)

基于暗网数据买牛股 (专业术语)

招行发布了朝朝盈一号的理财产品(产品名)

让你见识什么叫冻龄 (新词)

不同类型的命名实体还可以细分,比如人名可以分为中文人名、藏族人名、维族人名、日本人名、欧美人名等。

地名可以分为典型地名和非典型地名,典型地名如国、省、市、县、乡、村等;非典型地名还包括路、居委会、大厦商场、门牌单元、图书馆、门面等。理论上,只要是有经纬度坐标的实体,都可以纳入地名识别范畴。在实际工作中,这方面的识别需求非常强烈,比如在公安领域的线索或案情信息,往往涉及到这种非典型地名。

机构和公司的类型也多种多样,除了行政机构外,还有各种社团、NGO 组织、基金会、校友会等等;公司名涉及到公司全称和公司简称的识别,例如:

明略软件系统科技有限公司(全称)

明略软件(简称)

明略数据(简称)

全称识别相对容易一点,简称识别非常困难,比如:小米、滴滴、凡客、OFO 等。

机构公司名与地名之间存在很大的交集。理论上,一个机构或公司往往会有办公地点,有时也会用机构公司名来称呼该地点,这样的话机构公司名也就有了地点属性。例如:

小明在明略软件上班(公司名)

把球踢进了明略软件的门前(看做公司名还是地点?)

在实际工作中,命名实体的关注程度最高,因为这些实体往往是知识图谱的节点。其它未登录词中,专业术语的提取会对文本分类和文本理解有重要帮助。

分词中的语料问题

基于统计模型的分词系统,在分词结果上出现差异的一个原因是对语料的预处理差异导致。相当多的分词系统没有对训练数据进行一致性校验,认为训练数据是无差错的。在实际调查时发现,训练数据包含了不少标注不一致的情况。例如人民日报中的例子:

自认倒霉 | 自 认 倒霉

倒霉 鬼 | 倒霉鬼

除了切分一致性外,词性标注的不一致性更严重一些,如:“自认倒霉”有时标注为 l、有时标注为 lv;“难能可贵”有时标注为 i、有时标注为 iv。

分词语料的选择范围有限,主要包括北大人民日报标注语料、微软标注语料、社科院标注语料、CTB 语料、OntoNotes 语料、城市大学繁体语料、中研院繁体语料等。一般选择一种数据即可,数据需要购买。

分词语料之间在词语颗粒度上有一定差异,一般不混用进行训练,例如:

承租人、承租者 (北大) | 承租 商 (微软)

高 清晰度 彩电 (北大) | 高清晰度电视 (微软)

分词的理论解决方案

分词的理论解决方案是采用统计模型,基于切分语料进行训练。该方案在学术界和工程界都很常见,也是学术界的研究热点。方案的差异主要是模型和特征工程不一样。模型差异非常常见,比如隐马尔科夫模型、最大熵模型、条件随机场模型、结构感知机模型、RNN 模型等。

特征提取

特征提取的第一步是选择单元:基于字还是基于词。从实践来看,基于字的模型对未登录词识别能力较强,但基于词的模型很少会出现切分“离谱”的情况。采用什么颗粒度单元,取决于具体任务。

特征工程会对最终分词结果产生很大影响。字本位分词的常见分词特征是:

Unigram 是单字特征模板,当前字的前一个字、当前字、后一个字。Bigram 是邻接字组合特征模板,包括前一个字与当前字、当前字与后一个字的组合。Jump 是把前一个字与后一个字组合。

其它特征主要是关于字的属性,如是否数字、标点、字母等。这些特征都是形式上的特征,没有歧义。

每一个特征实例在 CRF 模型中有一个权重。由于特征越多,模型参数越大,在实际工程应用中资源消耗越大,因此在实际任务中会有一定取舍。

理论解决方案的问题

  • 训练数据规模有限

北大人民日报的原始语料的词语数为 2800 万;CTB9.0 词语数为 200 万;国家语委数据为 5000 万字。

标注语料是一个非常消耗人力的事情。北大 1998 年人民日报的标注共持续了 3 年时间才完成。CTB1.0 的标注持续了约 2 年时间。

  • 领域迁移性不佳

其他领域实施时,分词准确率下降很快。由于标注语料的词语使用无法覆盖实际语言现象,因此基于标注语料训练的分词系统在差异较大的领域会出现准确率降低的情况,例如基于北大语料训练的分词系统在微博上的切分准确率就不是很高。

  • 模型越来越大,速度越来越慢

早期使用 HMM 模型训练分词系统,在北大数据上训练大概需要 1-2 分钟,内存消耗很小。现在使用 CRF 模型训练大概需要 3 天左右,内存大概需要十几 G。CRF 模型在训练分词系统时,其参数数量跟特征数量直接相关,训练数据越多,特征数量越大,模型也就越来越大。导致系统调用存在困难,运行速度下降。

如何工程解决?

能用规则解决的,就不要靠模型了

针对文本中有规律的部分,可以利用规则或者正则表达式来识别,例如数字、标点、时间、日期、重叠式等,如笑一笑。

扩大训练语料

扩大训练语料的一种办法是购买更多语料;另外一种办法是利用其它分词系统来切分数据,对数据进行清洗,形成新数据。

这些不同的分词系统依赖的训练数据尽量不要相同,例如 Stanford 系统是基于 CTB 语料,LTP 系统是基于人民日报,这两个系统的切分结果就可以考虑混用。在混用前,要进行一定程度的预处理,比如保持切分一致性。

明略的分词系统通过使用多款不同分词系统的分词结果,扩大训练数据,在人名识别上大幅度提高了召回率。

增加词表

增加词表是提高切分准确率“立竿见影”的办法。在自然语言处理中,只要是封闭集合的词语或实体,可以考虑利用词表来切分,例如成语。该方法简单有效。

在明略分词数据中,集成了全国所有的地名、公交站名、路名等,精确到村和居委会,对国内地名识别上有很高的准确度。对机构名和公司名,集成了经常出现的国内行政机构、上市企业等名字。

在 Bosen 系统的演示中,对公司名识别准确率非常高,例如:“明略数据、明略软件”这种公司简称也能识别出来,即使没有上下文也能识别。这应该跟其后台的公司名数据有关。

最大匹配 + 大词表

从诸多实践来看,最大匹配分词策略 + 大词表的方法非常有效。在《中文分词十年回顾》中作者提到了最大匹配和大词表的效果:

Ftop 行表示没有未登录词的情况下,仅使用最大匹配达到的 F 值(准确率 + 召回率)。

实用的分词系统,都带有大量通用词表和领域词表。

收集整理领域词表,对迁移分词系统至关重要。这对统计分词系统比较困难。

结合深度学习?

ACL 2017 年有 3 篇跟分词相关的文章,都是深度学习 (神经网络) 和分词结合的内容。分别是:

Neural Word Segmentation with Rich Pretraining

Adversarial Multi-Criteria Learning for Chinese Word Segmentation

Fast and Accurate Neural Word Segmentation for Chinese

从明略的实践来看,深度学习应用到分词任务中的优点在于:模型非常小。在约 200MB 的语料上训练得到的模型只有 5MB。分词准确率接近历史上最佳评测结果,但是分词速度太慢。

从最新文献来看,利用神经网络来做分词,训练效率和运行效率都比较低,慢得无法忍受,不适合工程上部署,也不适合做 Demo。

在《Fast and Accurate …… for Chinese》中提供了运行速度对比,测试数据为 170k 左右,2015 和 2016 年的 6 项分词结果中,切分测试数据的时间从 28 秒到 125 秒。在传统方法上,比如基于 CRF 分词,运行时间大概只要 1 秒。

根据个人经验,神经网络在 NLP 上的成功应用的领域往往是准确率不高或者运行效率很低的场合,例如问答系统、机器翻译、句法分析。在准确率比较高或者运行效率不错的场景下,利用深度学习会得不偿失。

问答环节

老师您好,我想问下在做教育领域的课程名称分词时,与现在市面上的其他的分词库相比是自己研制库还是购买更好?应该注意什么?

答:专注在应用上比较好,直接使用支持自定义词典的分词系统,加上教育类的词表,就可以达到很好的效果,不需要投入人力自研系统。

您好!您开头提到分词是分场景的,目前在地址分词这块 (例如输入浙江省杭州市西湖区武林壹号 201 室,分词结果:省 - 市 - 区 - 建筑物 (武林壹号)- 门牌号 (201 室)),有没有比较成熟的分词系统?一般是基于规则的,还是基于统计学习的?

答:地址处理是个有意思的问题,刚好我们有点经验。在明略,这个问题和分词是分开实施的,是采用词表驱动,加上了少量规则。

您好,我想问一下在利用已有的分词工具下,想要提高准确率,是不是只能通过添加自定义词典呢?

答:提高准确率,有几个思路:

1)提供更多训练数据,前提是分词工具支持训练,Stanford 系统、清华分词等都支持;

2) 提供更大的词表;

3) 对分词结果进行规则处理,合并或者拆分。

公司简称的提取,即从全称中获得简称,有哪些好的实践?

答:1)从新闻中用正则或者其它规则去提取,比如 XX 简称 YY

2) 基于 ngram 提取所有字序列,看看哪些序列与全称的上下文比较相似,输出最相似的结果。这个方法效果还不错,但计算量大。

推荐下实体识别与关系抽取的来源工具吗 ?

答:命名实体识别的开源工具,主要是分词系统,只能解决人名、地名、机构名的问题。其它实体都没法解决。关系抽取,有一些开源工具,如 deepdive,但效果不怎么样。这是难题,还没看到这方面有很好的论文,更别说工具了。

分词字典的维护是相当耗人力的工作,有没有不错的 online-training 方案,一般的 HMM 和 CRF 都是 offline?

答:比较流行的模型是 Perceptron 模型。

问一下分词后输入 lstm 和按字切割输入 lstm 两者做情感分析的准确度差别大嘛?假设分词用的语料符合数据的语言风格和环境

答:这个我不清楚。从已有论文看,基于 LSTM 模型做应用的话,一般都是基于字符去实施,比如神经网络机器翻译。用字可能更好。

在机器翻译的任务中,应该怎么利用分词的结果,分词对翻译效果有怎么样的影响?

答:08 年同门做过实验,分词对统计机器翻译效果影响很小。

地址分词大概有什么思路可以提供参考一下?

答:1)要有高质量的地址表,网上有。

2)对中国的地名表达要有足够多的了解,比如单元号,门牌号之类。

能不能介绍一下分词和翻译关系的论文?

答:我手边没有这方面的论文,你可以在 Google 上搜一下,有不少。

网络评论有大量错字和误用现象,那么分词和纠错如何结合?

答:分词和纠错是分开的两个任务。

纠错,也叫自动校对,主要是通过语言模型的概率大小来实施;有些商业系统是通过大词表来实现。

分词系统推荐哪个呢?开源的、商用的

答:采用哪个分词系统取决于任务,没有最好的系统,只看是否适合你的任务。能用商业的,就尽量用商业的,毕竟不用操心了。

问答系统中,解决语序问题一般用什么方法?

答:一般是语言模型,计算不同词语顺序的概率。

问答系统中,解决纠错问题一般用什么方法?

利用语言模型实现或者大词表实现

对于金融类,比如公司新闻报道,研报,想要分词提取出里面的公司,以及相关事件的动词,有没有这方面分词实践的建议,或者案例参考……

答:提取公司的话,可以利用 CRF 模型识别公司名。提取事件的话,需要预先定义什么是事件;然后利用句法分析器识别主要动词。这方面的案例一般不公开。

讲师介绍

牟小峰,明略研究院技术经理。专攻领域:自然语言处理。从业经历:北京语言大学博士毕业,在新媒传信飞信项目组从事自然语言处理研发;后创业,从事推荐系统开发;目前在明略研究院从事自然语言处理方面的技术研发和业务探索。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Socket 深度探究 4 PHP (二)

发表于 2017-11-23

上一篇《Socket深度探究4PHP(一)》中,大家应该对 poll/select/epoll/kqueue 这几个 IO 模型有了一定的了解,为了让大家更深入的理解 Socket 的技术内幕,在这个篇幅,我会对这几种模式做一个比较详细的分析和对比;另外,大家可能也同说过 AIO 的概念,这里也会做一个简单的介绍;最后我们会对两种主流异步模式
Reactor 和 Proactor 模式进行对比和讨论。

首先,然我们逐个介绍一下 2.6 内核(2.6.21.1)中的 poll/select/epoll/kqueue 这几个 IO 模型。

> POLL

先说说 poll,poll 和 select 为大部分 Unix/Linux 程序员所熟悉,这俩个东西原理类似,性能上也不存在明显差异,但 select 对所监控的文件描述符数量有限制,所以这里选用
poll 做说明。poll 是一个系统调用,其内核入口函数为 sys_poll,sys_poll 几乎不做任何处理直接调用 do_sys_poll,do_sys_poll 的执行过程可以分为三个部分:

1、将用户传入的 pollfd 数组拷贝到内核空间,因为拷贝操作和数组长度相关,时间上这是一个 O(n) 操作,这一步的代码在 do_sys_poll 中包括从函数开始到调用 do_poll 前的部分。

2、查询每个文件描述符对应设备的状态,如果该设备尚未就绪,则在该设备的等待队列中加入一项并继续查询下一设备的状态。查询完所有设备后如果没有一个设备就绪,这时则需要挂起当前进程等待,直到设备就绪或者超时,挂起操作是通过调用
schedule_timeout 执行的。设备就绪后进程被通知继续运行,这时再次遍历所有设备,以查找就绪设备。这一步因为两次遍历所有设备,时间复杂度也是 O(n),这里面不包括等待时间。相关代码在 do_poll 函数中。

3、将获得的数据传送到用户空间并执行释放内存和剥离等待队列等善后工作,向用户空间拷贝数据与剥离等待队列等操作的的时间复杂度同样是 O(n),具体代码包括 do_sys_poll 函数中调用 do_poll 后到结束的部分。

但是,即便通过
select() 或者 poll() 函数复用事件通知具有突出的优点,不过其他具有类似功能的函数实现也可以达到同样的性能。然而,这些实现在跨平台方面没有实现标准化。你必须在使用这些特定函数实现同丧失可移植性之间进行权衡。我们现在就讨论一下两个替代方法:Solaris 系统下的 /dev/poll 和 FreeBSD 系统下的 kqueue:

1、Solaris 系统下的 /dev/poll:在Solaris 7系统上,Sun引入了/dev/poll设备。在使用 /dev/poll的时候,你首先要打开/dev/poll作为一个普通文件。然后构造pollfd结构,方式同普通的poll()函数调用一样。这些
pollfd结构随后写入到打开的 /dev/poll 文件描述符。在打开句柄的生存周期内, /dev/poll会根据pollfd结构返回事件(注意,pollfd结构内的事件字段中的特定POLLREMOVE将从/dev/poll的列表中删除对应的fd)。通过调用特定的ioctl (DP_POLL) 和dvpoll,程序就可以从/dev/poll获得需要的信息。在使用dvpoll结构的情况下,发生的事件就可以被检测到了。

2、FreeBSD 系统下的 kqueue:在FreeBSD
4.1中推出。FreeBSD的kqueue API设计为比其他对应函数提供更为广泛的事件通知能力。kqueue API提供了一套通用过滤器,可以模仿poll()语法(EVFILT_READ和EVFILT_WRITE)。不过,它还实现了文件系统变化(EVFILT_VNODE)、进程状态变更(EVFILT_PROC)和信号交付(EVFILT_SIGNAL)的有关通知。
> EPOLL

接下来分析 epoll,与 poll/select
不同,epoll 不再是一个单独的系统调用,而是由 epoll_create/epoll_ctl/epoll_wait 三个系统调用组成,后面将会看到这样做的好处。先来看 sys_epoll_create(epoll_create对应的内核函数),这个函数主要是做一些准备工作,比如创建数据结构,初始化数据并最终返回一个文件描述符(表示新创建的虚拟 epoll 文件),这个操作可以认为是一个固定时间的操作。epoll 是做为一个虚拟文件系统来实现的,这样做至少有以下两个好处:

1、可以在内核里维护一些信息,这些信息在多次
epoll_wait 间是保持的,比如所有受监控的文件描述符。

2、epoll 本身也可以被 poll/epoll。

具体 epoll 的虚拟文件系统的实现和性能分析无关,不再赘述。

在 sys_epoll_create 中还能看到一个细节,就是 epoll_create 的参数 size 在现阶段是没有意义的,只要大于零就行。

接着是 sys_epoll_ctl(epoll_ctl对应的内核函数),需要明确的是每次调用 sys_epoll_ctl
只处理一个文件描述符,这里主要描述当 op 为 EPOLL_CTL_ADD 时的执行过程,sys_epoll_ctl 做一些安全性检查后进入 ep_insert,ep_insert 里将 ep_poll_callback 做为回掉函数加入设备的等待队列(假定这时设备尚未就绪),由于每次 poll_ctl 只操作一个文件描述符,因此也可以认为这是一个 O(1) 操作。ep_poll_callback 函数很关键,它在所等待的设备就绪后被系统回掉,执行两个操作:

1、将就绪设备加入就绪队列,这一步避免了像
poll 那样在设备就绪后再次轮询所有设备找就绪者,降低了时间复杂度,由 O(n) 到 O(1)。

2、唤醒虚拟的 epoll 文件。

最后是 sys_epoll_wait,这里实际执行操作的是 ep_poll 函数。该函数等待将进程自身插入虚拟 epoll 文件的等待队列,直到被唤醒(见上面 ep_poll_callback 函数描述),最后执行 ep_events_transfer 将结果拷贝到用户空间。由于只拷贝就绪设备信息,所以这里的拷贝是一个
O(1) 操作。

还有一个让人关心的问题就是 epoll 对 EPOLLET 的处理,即边沿触发的处理,粗略看代码就是把一部分水平触发模式下内核做的工作交给用户来处理,直觉上不会对性能有太大影响,感兴趣的朋友欢迎讨论。

> POLL/EPOLL 对比:

表面上 poll 的过程可以看作是由一次 epoll_create,若干次 epoll_ctl,一次 epoll_wait,一次 close 等系统调用构成,实际上
epoll 将 poll 分成若干部分实现的原因正是因为服务器软件中使用 poll 的特点(比如Web服务器):

1、需要同时 poll 大量文件描述符;

2、每次 poll 完成后就绪的文件描述符只占所有被 poll 的描述符的很少一部分。

3、前后多次 poll 调用对文件描述符数组(ufds)的修改只是很小;

传统的 poll 函数相当于每次调用都重起炉灶,从用户空间完整读入 ufds,完成后再次完全拷贝到用户空间,另外每次
poll 都需要对所有设备做至少做一次加入和删除等待队列操作,这些都是低效的原因。

epoll 将以上情况都细化考虑,不需要每次都完整读入输出 ufds,只需使用 epoll_ctl 调整其中一小部分,不需要每次 epoll_wait 都执行一次加入删除等待队列操作,另外改进后的机制使的不必在某个设备就绪后搜索整个设备数组进行查找,这些都能提高效率。另外最明显的一点,从用户的使用来说,使用 epoll 不必每次都轮询所有返回结果已找出其中的就绪部分,O(n) 变 O(1),对性能也提高不少。

此外这里还发现一点,是不是将
epoll_ctl 改成一次可以处理多个 fd(像 semctl 那样)会提高些许性能呢?特别是在假设系统调用比较耗时的基础上。不过关于系统调用的耗时问题还会在以后分析。
> POLL/EPOLL 测试数据对比:

测试的环境:我写了三段代码来分别模拟服务器,活动的客户端,僵死的客户端,服务器运行于一个自编译的标准 2.6.11 内核系统上,硬件为 PIII933,两个客户端各自运行在另外的 PC 上,这两台PC比服务器的硬件性能要好,主要是保证能轻易让服务器满载,三台机器间使用一个100M交换机连接。

服务器接受并poll所有连接,如果有request到达则回复一个response,然后继续poll。

活动的客户端(Active
Client)模拟若干并发的活动连接,这些连接不间断的发送请求接受回复。

僵死的客户端(zombie)模拟一些只连接但不发送请求的客户端,其目的只是占用服务器的poll描述符资源。

测试过程:保持10个并发活动连接,不断的调整僵并发连接数,记录在不同比例下使用 poll 与 epoll 的性能差别。僵死并发连接数根据比例分别是:0,10,20,40,80,160,320,640,1280,2560,5120,10240。

下图中横轴表示僵死并发连接与活动并发连接之比,纵轴表示完成
40000 次请求回复所花费的时间,以秒为单位。红色线条表示 poll 数据,绿色表示 epoll 数据。可以看出,poll 在所监控的文件描述符数量增加时,其耗时呈线性增长,而 epoll 则维持了一个平稳的状态,几乎不受描述符个数影响。

但是要注意的是在监控的所有客户端都是活动时,poll
的效率会略高于 epoll(主要在原点附近,即僵死并发连接为0时,图上不易看出来),究竟 epoll 实现比 poll 复杂,监控少量描述符并非它的长处。

> epoll 的优点综述

1、支持一个进程打开大数目的socket描述符(FD):select 最不能忍受的是一个进程所打开的 FD 是有一定限制的,由 FD_SETSIZE 设置,在 Linux 中,这个值是 1024。对于那些需要支持的上万连接数目的网络服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的
Apache 方案),不过虽然 linux 上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll 则没有这个限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 1024,举个例子,在 1GB 内存的机器上大约是 10 万左右,具体数目可以 cat /proc/sys/fs/file-max 察看,一般来说这个数目和系统内存关系很大。

2、IO 效率不随 FD 数目增加而线性下降:传统的
select/poll 另一个致命弱点就是当你拥有一个很大的 socket 集合,不过由于网络延时,任一时间只有部分的 socket 是”活跃”的,但是 select/poll 每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是 epoll 不存在这个问题,它只会对”活跃”的 socket 进行操作—这是因为在内核实现中 epoll 是根据每个 fd 上面的 callback 函数实现的。那么,只有”活跃”的 socket 才会主动的去调用 callback 函数,其他
idle 状态 socket 则不会,在这点上,epoll 实现了一个”伪”AIO,因为这时候推动力在 os 内核。在一些 benchmark 中,如果所有的 socket 基本上都是活跃的 — 比如一个高速LAN环境,epoll 并不比 select/poll 有什么效率,相反,如果过多使用 epoll_ctl,效率相比还有稍微的下降。但是一旦使用 idle connections 模拟 WAN 环境,epoll 的效率就远在 select/poll 之上了。

3、使用
mmap 加速内核与用户空间的消息传递:这点实际上涉及到 epoll 的具体实现了。无论是 select,poll 还是 epoll 都需要内核把 FD 消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll 是通过内核于用户空间 mmap 同一块内存实现的。而如果你想我一样从 2.5 内核就关注 epoll 的话,一定不会忘记手工 mmap 这一步的。

4、内核微调:这一点其实不算 epoll 的优点了,而是整个 linux 平台的优点。也许你可以怀疑
linux 平台,但是你无法回避 linux 平台赋予你微调内核的能力。比如,内核 TCP/IP 协议栈使用内存池管理 sk_buff 结构,那么可以在运行时期动态调整这个内存 pool(skb_head_pool) 的大小 — 通过 echo XXXX > /proc/sys/net/core/hot_list_length 完成。再比如 listen 函数的第 2 个参数(TCP 完成 3 次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的
NAPI 网卡驱动架构。
> AIO 和 Epoll

epoll 和 aio(这里的aio是指linux 2.6内核后提供的aio api)的区别:

1、aio 是异步非阻塞的。其实是aio是用线程池实现了异步IO。

2、epoll 在这方面的定义上有点复杂,首先 epoll 的 fd 集里面每一个 fd 都是非阻塞的,但是 epoll(包括 select/poll)在调用时阻塞等待 fd
可用,然后 epoll 只是一个异步通知机制,只是在 fd 可用时通知你,并没有做任何 IO 操作,所以不是传统的异步。

在这方面,Windows 无疑是前行者,当然 Boost C++ 库已经实现了 linux 下 aio 的机制,有兴趣的朋友可以参考:http://stlchina.huhoo.net/twiki/bin/view.pl/Main/WebHome

> Reactor 和 Proactor

一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event
Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用异步IO。

在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。而在Proactor模式中,处理器–或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完成事件,然后将事件传递给对应处理器。比如,在windows上,处理器发起一个异步IO操作,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都建立在操作系统支持异步API的基础之上,我们将这种实现称为“系统级”异步或“真”异步,因为应用程序完全依赖操作系统执行真正的IO工作。

举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(类操作类似)。

在Reactor中实现读:

注册读就绪事件和相应的事件处理器

  • 事件分离器等待事件
  • 事件到来,激活分离器,分离器调用事件对应的处理器。
  • 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

与如下Proactor(真异步)中的读过程比较:

  • 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
  • 事件分离器等待操作完成事件
  • 在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。
  • 事件分离器呼唤处理器。
  • 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。

对于不提供异步 IO API 的操作系统来说,这种办法可以隐藏 Socket API 的交互细节,从而对外暴露一个完整的异步接口。借此,我们就可以进一步构建完全可移植的,平台无关的,有通用对外接口的解决方案。上述方案已经由Terabit
P/L公司实现为 TProactor (ACE compatible Proactor) :http://www.terabit.com.au/solutions.php。正是因为 linux 对 aio 支持的不完整,所以 ACE_Proactor 框架在 linux 上的表现很差,大部分在 windows 上执行正常的代码,在 linux 则运行异常,甚至不能编译通过。这个问题一直困扰着很大多数 ACE 的用户,现在好了,有一个 TProactor 帮助解决了在 Linux 不完整支持
AIO 的条件下,正常使用(至少是看起来正常)ACE_Proactor。TProactor 有两个版本:C++ 和 Java 的。C++ 版本采用 ACE 跨平台底层类开发,为所有平台提供了通用统一的主动式异步接口。Boost.Asio 库,也是采取了类似的这种方案来实现统一的 IO 异步接口。

以下是一张 TProactor 架构设计图,有兴趣的朋友可以看看:

到这里,第二部分的内容结束了,相信大家对 Socket 的底层技术原理有了一个更深层次的理解,在下一篇《Socket深度探究4PHP(三)》我将会深入 PHP 源代码,探究一下 PHP 在 Socket 这部分的一些技术内幕,然后介绍一下目前在这个领域比较活跃的项目(node.js)。

To be continued
…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…934935936…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%