Quantcast
Channel: IT瘾博客推荐
Viewing all 532 articles
Browse latest View live

springboot 获取enviroment.Properties的几种方式 - ppjj - 博客园

$
0
0

springboot获取配置资源,主要分3种方式:@Value、 @ConfigurationProperties、Enviroment对象直接调用。
前2种底层实现原理,都是通过第三种方式实现。

@Value 是spring原生功能,通过PropertyPlaceholderHelper.replacePlaceholders()方法,支持EL表达式的替换。

@ConfigurationProperties则是springboot 通过自动配置实现,并且最后通过JavaBeanBinder 来实现松绑定

获取资源的方式

1.1、@Value标签获取

@Component
@Setter@Getter
public class SysValue {
    @Value("${sys.defaultPW}")
    private String defaultPW;
}

1.2、@ConfigurationProperties标签获取

@Component
@ConfigurationProperties(prefix = "sys")
@Setter@Getter
public class SysConfig {
    private String defaultPW;
}

1.3、直接Enviroment对象获取回去

复制代码
@Component
public class EnvironmentValue {
    @Autowired
    Environment environment;
    private String defaultPW;
    @PostConstruct//初始化调用
    public  void init(){
        defaultPW=environment.getProperty("sys.defaultPW");
    }

}
复制代码

PropertyResolver初始化与方法。

2.1、api解释:

Interface for resolving properties against any underlying source.
(解析properties针对于任何底层资源的接口)

2.2、常用实现类

PropertySourcesPropertyResolver:配置源解析器。
Environment:environment对象也继承了解析器。

2.3、常用方法。

java.lang.String getProperty(java.lang.String key):根绝 Key获取值。
java.lang.String resolvePlaceholders(java.lang.String text)
:替换$(....)占位符,并赋予值。(@Value 底层通过该方法实现)。

2.4、springboot中environment初始化过程初始化PropertySourcesPropertyResolver代码。

复制代码
public abstract class AbstractEnvironment implements ConfigurableEnvironment {
//初始化environment抽象类是,会初始化PropertySourcesPropertyResolver,
//并将propertySources传入。
//获取逻辑猜想:propertySources是一个List<>。
//getProperty方法会遍历List根据key获取到value
//一旦获取到value则跳出循环,从而实现优先级问题。
private final ConfigurablePropertyResolver propertyResolver =
            new PropertySourcesPropertyResolver(this.propertySources);
}
复制代码

@Value获取资源源码分析。

解析过程涉及到(MMP看了一晚上看不懂,补贴代码了,贴个过程):
AutowiredAnnotationBeanPostProcessor:(@Value注解解析,赋值)

 

//赋值代码Autowired AnnotationBeanPostProcessor.AutowiredFieldElement.inject
if (value != null) {
                ReflectionUtils.makeAccessible(field);
                field.set(bean, value);
}

PropertySourcesPlaceholderConfigurer:(通过配置资源替换表达式)
PropertySourcesPropertyResolver:(根据key获取value。)

Enviroment 对象源码解析。

同上第三步,直接通过PropertySourcesPropertyResolver获取值。

2.4也能发现Enviroment new的PropertyResolver是PropertySourcesPropertyResolver

@ConfigurationProperties实现原理

核心类:
ConfigurationPropertiesBindingPostProcessor

复制代码
//通过自动配置,@EnableConfigurationProperties注入
//ConfigurationPropertiesBindingPostProcessor
@Configuration
@EnableConfigurationProperties
public class ConfigurationPropertiesAutoConfiguration {

}
复制代码

ConfigurationPropertiesBindingPostProcessor 类解析

复制代码
//绑定数据
private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
        ResolvableType type = getBeanType(bean, beanName);
        Validated validated = getAnnotation(bean, beanName, Validated.class);
        Annotation[] annotations = (validated != null)
                ? new Annotation[] { annotation, validated }
                : new Annotation[] { annotation };
        Bindable<?> target = Bindable.of(type).withExistingValue(bean)
                .withAnnotations(annotations);
        try {
            //绑定方法
            this.configurationPropertiesBinder.bind(target);
        }
        catch (Exception ex) {
            throw new ConfigurationPropertiesBindException(beanName, bean, annotation,
                    ex);
        }
}

//调用ConfigurationPropertiesBinder .bind方法。
class ConfigurationPropertiesBinder {
  public void bind(Bindable<?> target) {
        ConfigurationProperties annotation = target
                .getAnnotation(ConfigurationProperties.class);
        Assert.state(annotation != null,
                () -> "Missing @ConfigurationProperties on " + target);
        List<Validator> validators = getValidators(target);
        BindHandler bindHandler = getBindHandler(annotation, validators);
        //调用getBinder方法
        getBinder().bind(annotation.prefix(), target, bindHandler);
    }

   //getBinder方法初始化Binder对象
   // 传入熟悉的PropertySources:也来自PropertySourcesPlaceholderConfigurer对象同@Value
   //PropertySourcesPlaceholdersResolver
   private Binder getBinder() {
        if (this.binder == null) {
            this.binder = new Binder(getConfigurationPropertySources(),
                    getPropertySourcesPlaceholdersResolver(), getConversionService(),
                    getPropertyEditorInitializer());
        }
        return this.binder;
    }
}
复制代码

Binder.bind()方法解析

复制代码
//很深,最后通过JavaBeanBinder 来绑定数据
//为何ConfigurationProperties无法绑定静态对象:
//JavaBeanBinder会过滤掉静态方法
private boolean isCandidate(Method method) {
            int modifiers = method.getModifiers();
            return Modifier.isPublic(modifiers) && !Modifier.isAbstract(modifiers)&& !Modifier.isStatic(modifiers)//非静态方法
                    && !Object.class.equals(method.getDeclaringClass())&& !Class.class.equals(method.getDeclaringClass());
}
复制代码

本文转自: https://www.jianshu.com/p/62f0cdc435c8


springboot获取properties文件的配置内容(转载) - Mr_伍先生 - 博客园

$
0
0

1、使用@Value注解读取
读取properties配置文件时,默认读取的是application.properties。

 

application.properties:

demo.name=Name
demo.age=18

 

Java代码:

复制代码
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GatewayController {
    @Value("${demo.name}")
    private String name;
    @Value("${demo.age}")
    private String age;
    @RequestMapping(value = "/gateway")
    public String gateway() {
        return "get properties value by ''@Value'' :" +
                //1、使用@Value注解读取
                " name=" + name +" , age=" + age;
    }

}
复制代码

 

运行结果:

 

 

 

这里,如果要把

            @Value("${demo.name}")
            private String name;
            @Value("${demo.age}")
            private String age;

 

部分放到一个单独的类A中进行读取,然后在类B中调用,则要把类A增加@Component注解,并在类B中使用@Autowired自动装配类A,代码如下。

类A:

复制代码
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ConfigBeanValue {
    @Value("${demo.name}")
    public String name;
    @Value("${demo.age}")
    public String age;
}   
复制代码

 

类B:

复制代码
import cn.wbnull.springbootdemo.config.ConfigBeanValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GatewayController {
    @Autowired
    private ConfigBeanValue configBeanValue;
    @RequestMapping(value = "/gateway")
    public String gateway() {
        return "get properties value by ''@Value'' :" +
                //1、使用@Value注解读取
                " name=" + configBeanValue.name +" , age=" + configBeanValue.age;
    }
}
复制代码

 

运行结果:

 

注意:如果@Value${}所包含的键名在application.properties配置文件中不存在的话,会抛出异常:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'configBeanValue': Injection of autowired dependencies failed;   
nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'demo.name' in value "${demo.name}"

 

 

 

2、使用Environment读取

application.properties:

demo.sex=男
demo.address=山东

 

代码

复制代码
import cn.wbnull.springbootdemo.config.ConfigBeanValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GatewayController {
    @Autowired
    private ConfigBeanValue configBeanValue;
    @Autowired
    private Environment environment;
    @RequestMapping(value = "/gateway")
    public String gateway() {
        return "get properties value by ''@Value'' :" +
                //1、使用@Value注解读取
                " name=" + configBeanValue.name +" , age=" + configBeanValue.age +"<p>get properties value by ''Environment'' :" +
                //2、使用Environment读取
                " , sex=" + environment.getProperty("demo.sex") +" , address=" + environment.getProperty("demo.address");
    }
}
复制代码

 

运行结果:

 

 

 

这里,我们在application.properties做如下配置:

server.tomcat.uri-encoding=UTF-8
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
spring.messages.encoding=UTF-8

 

重新运行结果如下:

 

 

3、使用@ConfigurationProperties注解读取
在实际项目中,当项目需要注入的变量值很多时,上述所述的两种方法工作量会变得比较大,这时候我们通常使用基于类型安全的配置方式,将properties属性和一个Bean关联在一起,即使用注解@ConfigurationProperties读取配置文件数据。

在src\main\resources下新建config.properties配置文件:

demo.phone=10086
demo.wife=self

 

创建ConfigBeanProp并注入config.properties中的值:

复制代码
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "demo")
@PropertySource(value = "config.properties")
public class ConfigBeanProp {
    private String phone;
    private String wife;
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
    public String getWife() {
        return wife;
    }
    public void setWife(String wife) {
        this.wife = wife;
    }
}
复制代码

 

复制代码
@Component 表示将该类标识为Bean

@ConfigurationProperties(prefix = "demo")用于绑定属性,其中prefix表示所绑定的属性的前缀。

@PropertySource(value = "config.properties")表示配置文件路径。

 

使用时,先使用@Autowired自动装载ConfigBeanProp,然后再进行取值,示例如下:
复制代码

 

复制代码
import cn.wbnull.springbootdemo.config.ConfigBeanProp;
import cn.wbnull.springbootdemo.config.ConfigBeanValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GatewayController {
    @Autowired
    private ConfigBeanValue configBeanValue;
    @Autowired
    private Environment environment;
    @Autowired
    private ConfigBeanProp configBeanProp;
    @RequestMapping(value = "/gateway")
    public String gateway() {
        return "get properties value by ''@Value'' :" +
                //1、使用@Value注解读取
                " name=" + configBeanValue.name +" , age=" + configBeanValue.age +"<p>get properties value by ''Environment'' :" +
                //2、使用Environment读取
                " sex=" + environment.getProperty("demo.sex") +" , address=" + environment.getProperty("demo.address") +"<p>get properties value by ''@ConfigurationProperties'' :" +
                //3、使用@ConfigurationProperties注解读取
                " phone=" + configBeanProp.getPhone() +" , wife=" + configBeanProp.getWife();
    }
}
复制代码

 

运行结果:

 

 



4.使用PropertiesLoaderUtils

复制代码
app-config.properties

#### 通过注册监听器(`Listeners`) + `PropertiesLoaderUtils`的方式
com.zyd.type=Springboot - Listeners
com.zyd.title=使用Listeners + PropertiesLoaderUtils获取配置文件
com.zyd.name=zyd
com.zyd.address=Beijing
com.zyd.company=in
复制代码

 

PropertiesListener.java 用来初始化加载配置文件

复制代码
package com.zyd.property.listener;

import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;

import com.zyd.property.config.PropertiesListenerConfig;

/**
 * 配置文件监听器,用来加载自定义配置文件
 * 
 * @author <a href="mailto:yadong.zhang0415@gmail.com">yadong.zhang</a>
 * @date 2017年6月1日 下午3:38:25 
 * @version V1.0
 * @since JDK : 1.7
 */
public class PropertiesListener implements ApplicationListener<ApplicationStartedEvent> {

    private String propertyFileName;

    public PropertiesListener(String propertyFileName) {
        this.propertyFileName = propertyFileName;
    }

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        PropertiesListenerConfig.loadAllProperties(propertyFileName);
    }
}
复制代码

 

PropertiesListenerConfig.java 加载配置文件内容

复制代码
package com.zyd.property.config;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.springframework.beans.BeansException;
import org.springframework.core.io.support.PropertiesLoaderUtils;

/**
 * 第四种方式:PropertiesLoaderUtils
 * 
 * @author <a href="mailto:yadong.zhang0415@gmail.com">yadong.zhang</a>
 * @date 2017年6月1日 下午3:32:37
 * @version V1.0
 * @since JDK : 1.7
 */
public class PropertiesListenerConfig {
    public static Map<String, String> propertiesMap = new HashMap<>();

    private static void processProperties(Properties props) throws BeansException {
        propertiesMap = new HashMap<String, String>();
        for (Object key : props.keySet()) {
            String keyStr = key.toString();
            try {
                // PropertiesLoaderUtils的默认编码是ISO-8859-1,在这里转码一下
                propertiesMap.put(keyStr, new String(props.getProperty(keyStr).getBytes("ISO-8859-1"), "utf-8"));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (java.lang.Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void loadAllProperties(String propertyFileName) {
        try {
            Properties properties = PropertiesLoaderUtils.loadAllProperties(propertyFileName);
            processProperties(properties);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static String getProperty(String name) {
        return propertiesMap.get(name).toString();
    }

    public static Map<String, String> getAllProperty() {
        return propertiesMap;
    }
}
复制代码

 

Applaction.java 启动类

复制代码
package com.zyd.property;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.zyd.property.config.PropertiesListenerConfig;
import com.zyd.property.listener.PropertiesListener;

/**
 * @author <a href="mailto:yadong.zhang0415@gmail.com">yadong.zhang</a>
 * @date 2017年6月1日 下午3:49:30 
 * @version V1.0
 * @since JDK : 1.7
 */
@SpringBootApplication
@RestController
public class Applaction {
    /**
     * 
     * 第四种方式:通过注册监听器(`Listeners`) + `PropertiesLoaderUtils`的方式
     * 
     * @author zyd
     * @throws UnsupportedEncodingException
     * @since JDK 1.7
     */
    @RequestMapping("/listener")
    public Map<String, Object> listener() {
        Map<String, Object> map = new HashMap<String, Object>();
        map.putAll(PropertiesListenerConfig.getAllProperty());
        return map;
    }

    public static void main(String[] args) throws Exception {
        SpringApplication application = new SpringApplication(Applaction.class);
        // 第四种方式:注册监听器
        application.addListeners(new PropertiesListener("app-config.properties"));
        application.run(args);
    }
}
复制代码

Maven pom.xml中的元素modules、parent、properties以及import - 青石路 - 博客园

$
0
0

前言

  项目中用到了maven,而且用到的内容不像 利用maven/eclipse搭建ssm(spring+spring mvc+mybatis)用的那么简单;maven的核心是pom.xml,那么我就它来谈谈那些不同的地方;

  给我印象最深的就是如下四个元素:modules、parent、properties、import。

  路漫漫其修远兮,吾将上下而求索!

  github: https://github.com/youzhibing

  码云(gitee): https://gitee.com/youzhibing

modules

  从字面意思来说,module就是模块,而pom.xml中的modules也正是这个意思,用来管理同个项目中的各个模块;如果maven用的比较简单,或者说项目的模块在pom.xml没进行划分,那么此元素是用不到的;不过一般大一点的项目是要用到的。

  需求场景

    如果我们的项目分成了好几个模块,那么我们构建的时候是不是有几个模块就需要构建几次了(到每个模块的目录下执行mvn命令)?当然,你逐个构建没问题,但是非要这么麻烦的一个一个的构建吗,那么简单的做法就是使用聚合,一次构建全部模块。

  具体实现

    a.既然使用聚合,那么就需要一个聚合的载体,先创建一个普通的maven项目account-aggregator,如下图:

  

    因为是个聚合体,仅仅负责聚合其他模块,那么就只需要上述目录,该删除的就删了;注意的是pom文件的书写(红色标明的):

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.youzhibing.account</groupId><artifactId>account-aggregator</artifactId><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><name>Account Aggrregator</name><url>http://maven.apache.org</url><modules>        
    <!-- 模块都写在此处 --><module>account-register</module><module>account-persist</module></modules></project>

    b.创建子模account-register、account-persist:右击account-aggregator,new --> other --> Maven,选择Maven Module,创建moven模块。

    c.创建完成后,项目结构如下,那么此时account-aggregator可以收缩起来了,我们操作具体子模块就好了。

                         

     d.注意点,当我们打开包结构的子模块的pom文件时,发现离预期的多了一些内容,我们坐下处理就好了。

    e.那么编码完了之后,我们只需要构建account-aggregator就好了,所有的子模块都会构建。

parent

  继承,和java中的继承相当,作用就是复用

  需求场景

    若每个子模块都都用的了spring,那么我们是不是每个子模块都需要单独配置spring依赖了?,这么做是可以的,但是我们有更优的做法,那就是继承,用parent来实现。

  具体实现

    a.配置父pom.xml

      我就用聚合pom来做父pom,配置子模块的公共依赖。

      父(account-aggregator)pom.xml :

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.youzhibing.account</groupId><artifactId>account-aggregator</artifactId><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><name>Account Aggrregator</name><url>http://maven.apache.org</url><modules><!-- 模块都写在此处 --><module>account-register</module><module>account-persist</module></modules><dependencies> <!-- 配置共有依赖 --><!-- spring 依赖 --><dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>4.0.2.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId><version>4.0.2.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>4.0.2.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context-support</artifactId><version>4.0.2.RELEASE</version></dependency><!-- junit 依赖 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.7</version><scope>test</scope></dependency></dependencies></project>
View Code

    b.account-register的pom.xml :

<?xml version="1.0"?><project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><modelVersion>4.0.0</modelVersion><parent><groupId>com.youzhibing.account</groupId><artifactId>account-aggregator</artifactId><version>1.0.0-SNAPSHOT</version><relativePath>../pom.xml</relativePath> <!-- 与不配置一样,默认就是寻找上级目录下得pom.xml --></parent><artifactId>account-register</artifactId><name>account-register</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies>    <!-- 配置自己独有依赖 --><dependency><groupId>javax.mail</groupId><artifactId>mail</artifactId><version>1.4.3</version></dependency><dependency><groupId>com.icegreen</groupId><artifactId>greenmail</artifactId><version>1.4.1</version><scope>test</scope></dependency></dependencies></project>
View Code

    c.account-persist的pom.xml :

<?xml version="1.0"?><project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><modelVersion>4.0.0</modelVersion><parent><groupId>com.youzhibing.account</groupId><artifactId>account-aggregator</artifactId><version>1.0.0-SNAPSHOT</version></parent><artifactId>account-persist</artifactId><name>account-persist</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies>    <!-- 配置自己独有依赖 --><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>4.0.2.RELEASE</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.0.16</version></dependency></dependencies></project>
View Code

    d.依赖的jar包全部ok,需要做的则是在各个模块中进行代码开发了!

  依赖管理

    继承可以消除重复,那是不是就没有问题了? 答案是存在问题,假设将来需要添加一个新的子模块account-util,该模块只是提供一些简单的帮助工具,不需要依赖spring、junit,那么继承后就依赖上了,有没有什么办法了? 有,maven已经替我们想到了,那就是dependencyManagement元素,既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活性。在dependencyManagement元素下得依赖声明不会引入实际的依赖,不过它能够约束dependencies下的依赖使用。

    在父pom.xml中配置dependencyManagement元素

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.youzhibing.account</groupId><artifactId>account-aggregator</artifactId><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><name>Account Aggrregator</name><url>http://maven.apache.org</url><modules><!-- 模块都写在此处 --><module>account-register</module><module>account-persist</module></modules><dependencyManagement><dependencies> <!-- 配置共有依赖 --><!-- spring 依赖 --><dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>4.0.2.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId><version>4.0.2.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>4.0.2.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context-support</artifactId><version>4.0.2.RELEASE</version></dependency><!-- junit 依赖 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.7</version><scope>test</scope></dependency></dependencies></dependencyManagement></project>
View Code

    account-persist的pom.xml(account-register也一样) :

<?xml version="1.0"?><project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><modelVersion>4.0.0</modelVersion><parent><groupId>com.youzhibing.account</groupId><artifactId>account-aggregator</artifactId><version>1.0.0-SNAPSHOT</version></parent><artifactId>account-persist</artifactId><name>account-persist</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!-- spring 依赖 --><dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context-support</artifactId></dependency><!-- junit 依赖 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>4.0.2.RELEASE</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.0.16</version></dependency></dependencies></project>
View Code

     使用这种依赖管理机制似乎不能减少太多的POM配置,就少了version(junit还少了个scope),感觉没啥作用呀;其实作用还是挺大的,父POM使用dependencyManagement能够统一项目范围中依赖的版本,当依赖版本在父POM中声明后,子模块在使用依赖的时候就无须声明版本,也就不会发生多个子模块使用版本不一致的情况,帮助降低依赖冲突的几率。如果子模块不声明依赖的使用,即使该依赖在父POM中的dependencyManagement中声明了,也不会产生任何效果。

import

  import只在dependencyManagement元素下才有效果,作用是将目标POM中的dependencyManagement配置导入并合并到当前POM的dependencyManagement元素中,如下就是讲account-aggregator中的dependencyManagement配置导入并合并到当前POM中。

<dependencyManagement><dependencies><dependency><groupId>com.youzhibing.account</groupId><artifactId>account-aggregator</artifactId><version>1.0.0-SNAPSHOT</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>

properties

  通过<properties>元素用户可以自定义一个或多个Maven属性,然后在POM的其他地方使用${属性名}的方式引用该属性,这种做法的最大意义在于消除重复和统一管理。

  Maven总共有6类属性,内置属性、POM属性、自定义属性、Settings属性、java系统属性和环境变量属性;

  内置属性

    两个常用内置属性 ${basedir} 表示项目跟目录,即包含pom.xml文件的目录;${version} 表示项目版本

  pom属性

    用户可以使用该类属性引用POM文件中对应元素的值。如${project.artifactId}就对应了<project> <artifactId>元素的值,常用的POM属性包括:

    ${project.build.sourceDirectory}:项目的主源码目录,默认为src/main/java/

    ${project.build.testSourceDirectory}:项目的测试源码目录,默认为src/test/java/

    ${project.build.directory} : 项目构建输出目录,默认为target/

    ${project.outputDirectory} : 项目主代码编译输出目录,默认为target/classes/

    ${project.testOutputDirectory}:项目测试主代码输出目录,默认为target/testclasses/

    ${project.groupId}:项目的groupId

    ${project.artifactId}:项目的artifactId

    ${project.version}:项目的version,与${version} 等价

    ${project.build.finalName}:项目打包输出文件的名称,默认为${project.artifactId}-${project.version}

  自定义属性

    如下account-aggregator的pom.xml,那么继承了此pom.xml的子模块也可以用此自定义属性

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.youzhibing.account</groupId><artifactId>account-aggregator</artifactId><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><name>Account Aggrregator</name><url>http://maven.apache.org</url><modules><!-- 模块都写在此处 --><module>account-register</module><module>account-persist</module><module>account-another</module></modules><properties><!-- 定义 spring版本号 --><spring.version>4.0.2.RELEASE</spring.version><junit.version>4.7</junit.version></properties><dependencyManagement><dependencies> <!-- 配置共有依赖 --><!-- spring 依赖 --><dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>${spring.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId><version>${spring.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>${spring.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context-support</artifactId><version>${spring.version}</version></dependency><!-- junit 依赖 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</version><scope>test</scope></dependency></dependencies></dependencyManagement></project>
View Code

  settings属性

    与POM属性同理,用户使用以settings. 开头的属性引用settings.xml文件中的XML元素的值。

  Java系统属性

    所有java系统属性都可以用Maven属性引用,如${user.home}指向了用户目录。

  环境变量属性

    所有环境变量属性都可以使用以env. 开头的Maven属性引用,如${env.JAVA_HOME}指代了JAVA_HOME环境变量的的值。

聚合与继承的关系

  1.聚合主要是为了方便快速构建项目,继承主要是为了消除重复配置;

  2.对于聚合模块而言,它知道有哪些被聚合的模块,但那些被聚合的模块不知道这个聚合模块的存在;对于继承的父pom而言,它不知道有哪些子模块继承它,但那些子模块都必须知道自己的父POM是什么;

  3.聚合POM与继承中的父POM的packaging都必须是pom;同时,聚合模块与继承中的父模块除了POM外,都没有实际的内容

结束语

  maven越来越流行,这方面的资料也是越来越多,《Maven实战》给我的感觉就相当不错,本博客的内容大多取自其中;网上资料也越来越多,就博客园中就有不少;

  最后强调一点: 看了是好,实践更好,写博客记录下来那是最好!

漫谈单点登录(SSO) - EzrealLiu - 博客园

$
0
0

 

1. 摘要

SSO这一概念由来已久,网络上对应不同场景的成熟SSO解决方案比比皆是,从简单到复杂,各式各样应有尽有!开源的有OpenSSO、CAS ,微软的AD SSO,及基于kerberos 的SSO等等……这些优秀的解决方案尽显开发及使用者的逼格,当然需求所致无谓好坏高低,满足实际之需才是王道!

本文并不讨论上述提到的方案的整合使用、或者复杂场景如:安全、防火墙、N 多个系统层叠调用这种"巨型项目"里SSO的实现与使用,也并不涉及 C/S 、C/S+B/S 的SSO解决方案,仅关注B/S 上的SSO实现。虽是如此,然而万变不离其宗,这里我们将从一个简而小的登录场景去接触SSO的本质,描述如何原生态地自实现一个轻量、微核的SSO(本文不提供源码)。

文章将由浅入深地探讨SSO(单点登录),涉及SSO的定义、表现、原理、实现细节等方面的阐述,借助大家熟知的淘宝、天猫登录场景,通过对阿里登录的模仿实现,建立一个简单模型,然后不断由该模型进行迭代并对每一个迭代版本进行详细描述,最终得到一个支持跨域的SSO( 力求条理清晰,层层递进,简单但有深度!!!!开始部分本着让即使从未听过SSO的同学也能够从抽象文字定义的概念印象过渡到具象的视觉认知这一宏(zhuang)伟(bi)理念入手,将会有很多浅显的描述,"老司机" 可以快速掠过)

 

2. SSO简介

 

2.1 SSO定义

 

SSO( Single Sign-On ),中文意即单点登录,翻译得比较精简,个人觉得 Wiki 上的解释更细腻点—— SSO, is a property of access control of multiple related, but independent software systems. With this property a user logs with a single ID and password to gain access to connected system or systems without using different usernames or passwords, or in some configurations seamlessly sign on at each system. ( 单点登录是一种控制多个相关但彼此独立的系统的访问权限, 拥有这一权限的用户可以使用单一的ID和密码访问某个或多个系统从而避免使用不同的用户名或密码,或者通过某种配置无缝地登录每个系统 ). 注:系统,在本文特指WEB 应用或者WEB 服务;用户,下文也会称之为User;ID,用户标识;密码,本文也称其为口令,Password, Passcode 或者 Pin。

 

OK,从上面的定义中我们总结出 与 SSO 交互的2个元素:1.  用户,2. 系统,它的特点是:一次登录,全部访问。上面提到SSO是访问控制的一种,控制用户能否登录,即验证用户身份,而且是所有其它系统的身份验证都在它这里进行,那么我们是不是可以认为 SSO还是一个验证中心。那么 从整个系统层面来看SSO,它的核心就是这3个元素了:1. 用户,2. 系统,3. 验证中心。可能扯了那么多还是不足以形象地描述我们萌萌的SSO,呐,有图有真相:

 

 

既然SSO这么棒,应该如何实现呢?

 

 

2.2 SSO示例——淘宝、天猫的登录场景

 

我们暂不考虑细节,先从SSO需要解决的问题入手:使用一个账户通过一次登录,即可在多个相关的系统之间来回访问,为了更加形像我们还是上图: (多图预警)

登录页面,网址:login.taobao.com ….. 我将在 login.taobao.com 所指的系统进行登录

 

 

访问网站,第一张网址:buyertrade.taobao.com…. 访问 buyertrade.taobao.com所指的系统了;然后访问另一张网页网址为:favoriate.taobao.com, 访问 favoriate.taobao.com所指系统,两个系统的 Domain 是相同的,请注意这点;

 

    

 

 

接下来我再分别访问淘宝( www.taobao.com)和天猫( www.tmall.com)的首页 ,图中显示我仍旧是登录的( 注意:这里是不同的Domain下,系统之间的来回访问)

 

              

 

可以看到,我除了在第一张网页图那里需要输入用户名(ID)和口令(password)进行登录,再访问其它相关系统时,从图2-5 中所有的访问操作, 无论域名相同还是不同我都不需要再登录了,它们都知道我叫"望向明天"!对,没错,这就是SSO的作用: 一次登录,全部访问,读者也可以尝试下看看是不是如此;

 

3. SSO实现描述

好,经过我上面一大段废话,基本上对SSO要解决什么问题有一个清晰的认识。现在我们自行脑(yi)补(yin)下SSO 的原理是什么样的。

  1. 一个账户:嗯,规定所有系统统一使用相同账户,就能保证一个账户了;
  2. 一次登录全部访问:通过SSO登录后,让其告知其它各个系统保存该用户的信息,用户就不用重复多次的登录了;

嗯,问题解决了,没错,就这样。

 

3.1 方案1

由上面的猜想可以得到第1个解决方案,记为方案1。这里对这个猜想做一点小小的优化,猜想中第2点 "各个系统保存" 好让人闹心,同一份数据保存多份,太浪费,这里我们把每个已登录的用户信息保存到公共缓存中。好,我们再来描述下这个方案:

  1. User 发送登录请求给SSO,附上自己的 ID 和 password;
  2. SSO验证成功将用户信息保存在公共缓存 Cache 中;
  3. User每次发送请求给系统 Ai 时,将 ID 作为请求参数;
  4. 系统 Aj 通过 请求中传过来的 User ID从公共缓存 Cache 中验证 User 是否登录,完成后续动作;

 

文字完了,接下来看看方案1的架构图和时序图:

 

嗯,图文并茂的样子,难道就 么大功告成了? 我们先把方案1中完成的第一版 SSO 记为SSO_V1,接下 来我们来好好地捋一捋。

 

3.2 方案2

SSO_V1 貌似解决了问题,但是深入思考,细思极恐!因为这个设计有Bug:每次传 ID 给服务Ai,但是这个ID 每次怎么获取来呢?登录SSO的时候,这倒没有问题,可以让用户填!但第2次请求是发给Ai中的某一个 Aj 时,ID 要怎么来( 假设百度和新浪是相关但彼此独立的系统,登录百度后,再访问新浪时怎么让新浪取到与登录百度时一样的ID吧)?总不至于每次发请求时都要求用户填一遍ID 吧?

 

其实我们把 猜想 中最值得思考的问题之一忽略掉了:

如何让SSO"告知"系统Ai,当前登录的User 的ID和password?

 

这问题可以这样来描述:假设有W ( www.weidai.com)和 T( trade.weidai.com ) 两个系统,W和T 都通过S (login.weidai.com) 系统登录,当由U访问W再转向S 完成登录后,怎样做才能使 U 访问T 时不需要再一次通过 S 进行登录验证?

 

对,如果你是WEB 开发的老司机,很自然你会想到用 cookie,即把用户信息( 本文也会称之为UserInfo )保存在 cookie当中,因为 无论W 、T 或者 S 它们的Domain是一样的——都是 weidai.com ——同一Domain,这有何用?用处就在于 W 、T 以及 S 可以共享此路径下的 cookie。这里,让我们优化的心再一次燃烧起来——直接保存用户的 ID 和 口令 对于我们这么有逼格,有追求的猿来说有点太不讲究——为什么呢?不太安全, cookie中 最好保存一个 公共Session ID( 请和WEB 自己生成的Seesion ID进行区分 ),而我们的公共缓存 Cache 中保存的 UserInfo 是一个由 公共Session ID为Key以包含用户标识和口令的数据结构为Value的Map。最后附上这一流程的时序图及简要说明:

 

 

  1. U访问W ,W进行验证,验证失败,跳转至SSO,要求U登录;
  2. U通过SSO登录,SSO进行验证,成功并生成 SessionID,随后将UserInfo( SessionID、ID和口令)存储到公共缓存C 中,跳转至W(携带SessionID),并允许U访问W;
  3. U保存UserInfo ( SessionID) 至 cookie; (这里请将 U 看成一个浏览器,当下文有提到 U 保存XXX至Cookie时,读者请自行切换)
  4. U 再访问 T ( 并携带 在3 中保存至 cookie中的 UserInfo ) ,T从公共缓存中拉取UserInfo 进行验证,成功则允许访问;

 

嗯,又是图文并茂的样子,难道再一次大功告成? 我们暂时把刚才的方案记为方案2,并把方案2中完成的升级版SSO记为SSO_V2,接下来我们 再来好好地捋一捋

 

3.3 方案3

SSO_V2 能够在 Domain 相同的情况下"完美"解决问题,但是在Domain不同的情况下怎么做到免登呢?如上面图示淘宝( www.taobao.com)和天猫( www.tmall.com)若采用SSO_V2 肯定无法做到免登的,因为我们知道当访问天猫时(Domain 为tmall.com ),淘宝( Domain 为 taobao.com )下的 cookie 是无法随访问请求一并传给与天猫相关的系统的。 所以问题变成,怎么让不同Domain下的系统也"知晓"用户已经登录的实事?

 

在我们提出SSO_V3前, 我们先看看SSO 本质是什么?通过这么多的文字描述、样图解释,我们可以看到,要让用户"一次登录,全部访问"无非就是让所有的系统共享"一份"(相同)已验证的、安全可靠的验证信息。所以问题就可以转化为:不同Domain下的系统如何共享一份的验证信息?既然Domain无法做到交叉访问,那我们可以让不同Domain下的WEB应用持有相同的验证信息,这在效果上不就是一份吗!所以最终要解决的问题就是: SSO系统如何使不同的 Domain 拥有一份相同的cookie? —— 让SSO在用户进行登录时再去访问其它域下的系统,并让各个系统保存一样的验证信息,这样不同域下就会有同一份cookie

 

以下是SSO_V3的时序图和文字说明,这里我们假设 SSO 的Domain 为 SD,T 的 Domain 为 TD:

  1. U第一次访问W,W验证失败,跳转至SSO要求U进行登录验证;
  2. 登录并使各不同Domain下:
    1. U 给SSO发送登录请求,SSO验证成功,生成SessionID 并保存UserInfo;
    2. 返回给U的Response 将 UserInfo 存放至cookie中,Domain为SD;
    3. 将 2 中 cookie 内容作为query parameter 重定向至T,T验证后成功返回给U,也在Response 中设置 cookie;Domain为TD;
    4. U自动访问SSO,SSO将请求重定向至W,完成U对W 的访问;
  3. U 再访问 T,验证成功并允许U进行访问;

 

嗯,还是图文并茂的样子,这下是不是可以完事了呢?我们还是把刚才的方案记为方案3,并把方案3中完成的升级版SSO记为SSO_V3, 然后还是来好好地捋一捋

 

3.4 方案4

再细细的考虑下SSO_V3的实现方式,有没有感觉它哪里有点不对劲( 思维一直跟着我来走,是不是被绕晕了,想发现不对劲,怎么可能)? SSO_V3 使不同 Domain 获取相同的cookie 拷贝时,表面是在U处主动发出向T的请求(其实是被动), 但实际上是 SSO 返回给 U 的页面自动完成的(通过 JS、通过页面自动跳转、iframe都可以实现)。 所以方案SSO_V3要求SSO 预先知道有哪些系统是跨域的!!!而且它还有一个很严重的问题:假如与SSO相关但相互独立的系统中,有 20+ 需要跨域才能访问,而SSO要在用户登录时完成20+跳转……现在你是不是要呵呵了?貌似完美解决跨域的SSO_V3 竟然如此有问题,有没有心好塞!

 

SSO_V3 解决的核心问题是:针对跨域的系统,各系统间如何保证获取到的 验证信息是一致的,解决方法即是 在用户第一次登录时把验证信息复制给所有跨域的系统。这种方案在跨域系统少的情况下倒是不需要有太多担心,但是当跨域系统多、且验证步骤比较复杂时用户将会卡在登录界面,最后不得不怒关页面!所以当理清这些逻辑,很自然就会想到接下来要如何对SSO_V3进行优化。 核心思想就是:既然一次性解决会有问题,那就分多次解决!简单的描述下我们将要看到的SSO_V4,用户登录后,当第一次访问跨域系统W 时,跳到SSO复制一份至W的cookie中,过程结束;当访问T时,重复该处理动作。

 

以下为SSO_V4的时序图及简要说明:

 

  1. 用户U访问W ,W进行验证,验证失败,跳转至SSO,要求U登录;
  2. U通过SSO登录,SSO进行验证,成功并生成 SessionID,随后将UserInfo( SessionID、ID和口令)存储到公共缓存C 中,跳转至W(携带SessionID),并允许U访问W;U保存UserInfo ( SessionID) 至 cookie;
  3. U访问T,T 进行验证,失败跳转至SSO,SSO将触发U请求SSO将验证信息随请求一并发给SSO,经SSO验证成功跳转至T,允许U对T 的访问;使U保存UserInfo( SessionID)至cookie;

 

3.5 小结

其实我们通过上面的实用版(SSO_V2,SSO_V3,SSO_V4)SSO,可以看到除了用户的第一次登录某个应用相对来说比较特殊,其它处理都是一致的。所以当我们抛去细节之后,不仿这样联想SSO的实现: 完成登录逻辑并使各系统共享验证信息和验证逻辑,从这个层次去看SSO,我们发现它其实只负责用户登录和身份验证这2、3个点。

 

下面是用户第一次登录及SSO与其它系统交互的简图:

 

4. 设计与实现

4.1 验证信息的安全考虑

第3部分中的身份验证和验证信息方面都做得比较简单,在实际项目中不可能如此使用!在此提出一个方案以供参考(这也是比较流行的一种)。

  1. 使用 HTTPS 进行用户登录;
  2. 为每个用户生成一个对称密钥Ku;
  3. 验证信息由"ID"+ "password"+ SessionID 组成,当然你可以按需设置,比如再加个IP 地址……
  4. 存储在cookie 中的验证信息,ID 和口令部分经由用户密钥Ku和SSO公钥处理后在存放至"客户端";

这样处理后相信能够满足大部分应用的需求了!

 

4.2 SSO的概要设计

4.2.1 整体思路

SSO这一理念到目前为止已经非常成熟,关于它的各种设计、设置都可以定制一套标准了。 然而由于SSO与用户有强关联,所以很多设计在最初时往往会把SSO设计成一个用户管理系统,而使得SSO与业务耦合,随着业务的不断变化和演进,底层数据结构、接口不断的复杂化,又反过来使得上层服务的架构设计变得尴尬。

若做更进一层的抽象和划分,SSO只需负责登录这单一功能即可,设计上满足单一职责原则 [1],加上几乎所有网站的登录都大同小异(可能登录界面会变幻无常)且不与业务有过多牵连,这又使得SSO与业务完全分离,无论将来业务怎样演进,产品如何迭代,SSO作为底层应用可以以不变应万变。Really? All problem in computer science can be solved with another level of indirection,except of course for the problem of too many indirections. [2] 如何在设计中做到复杂与简洁的平衡,需要根据实际情境深度地考量,这可以扯出长篇大论了(按下不表),我们的SSO姑且就搞这几个功能:登录、记录轨迹、登出,以下是用例图:

 

第3节第5部分有提到"登录交由SSO完成,各系统共享一套验证逻辑",很自然的验证这一逻辑对SSO也是必须的,在此就由SSO来完成,其它系统只需将其配置到各自系统里即可。再加上SSO是用户"做案的第一现场",所以记录用户登录信息的事也很自然的就让SSO给干起来了,而且这一功能不仅能够让用户感受到我们对客户的用心,同时也为后期数据分析业务提供数据源!

 

4.2.2 数据表设计

经过上面的讨论,我们着手思考SSO的数据结构——数据表设计(个人认为面向对象编程中数据结构的优劣基本决定整个应用的质量)。从SSO 功能简单及其微服务的定位,SSO的表应该简洁、单一,上层服务若需要对其进行扩展,只需要对基本表进行外键引用即可!这里我们暂时只用3张表,分别为User、Trace(用户轨迹表)和使用平台表,图示与描述如下:

 

 

用户表:User

  1. uid 用户唯一标识,( varchar 是否有更好)
  2. name :账号,可以唯一标识用户,email,phone等都唯一标识用户;
  3. status:用户状态;(冻结,已删除……);
  4. key :用户密钥;
  5. info:扩展字段,用以应变需求;

 

用户轨迹表:Trace

  1. type :轨迹类型,(删除,登录,登出,修改……);
  2. time :操作时间;
  3. info同上,uid 用户表外键,pid 为Platform的外键;

 

使用平台表:Platform

  1. ip:用户登录ip
  2. address:用户登录地址,可由IP 解析得到,(手机端可以使用GPS);
  3. platform:使用平台的信息,将在请求的head上得到;
  4. info同上,tid 表示Trace 表的外键;

 

4.2.3 简要类设计

通过上面的整体思路及数据结构的定型,我们可以继续铺开将SSO要涉及到的一些主体类及主要方法定义好,仍旧上图:

 

写到这里,对于这个图示就不再做过多解释,大家基本可以开始做各种各样的脑补了!额,仅说小小的一个点:验证由Interceptor实现,这样验证逻辑则可以以插件形式配置到其它系统,实现所有系统共享一套验证逻辑,当然你也可以根据具体情况做成Filter,看个人爱好; 访问这方面交给第三方处理,比如由Shiro、Spring Security等来完成……酱紫,结束!

 

基于token的多平台身份认证架构设计 - 一点一滴的Beer - 博客园

$
0
0

1   概述

在存在账号体系的信息系统中,对身份的鉴定是非常重要的事情。

随着移动互联网时代到来,客户端的类型越来越多, 逐渐出现了  一个服务器,N个客户端的格局 。

不同的客户端产生了不同的用户使用场景,这些场景:

  1. 有不同的环境安全威胁
  2. 不同的会话生存周期
  3. 不同的用户权限控制体系
  4. 不同级别的接口调用方式

综上所述,它们的身份认证方式也存在一定的区别。

本文将使用一定的篇幅对这些场景进行一些分析和梳理工作。

2   使用场景

下面是一些在IT服务常见的一些使用场景:

  1. 用户在web浏览器端登录系统,使用系统服务
  2. 用户在手机端(Android/iOS)登录系统,使用系统服务
  3. 用户使用开放接口登录系统,调用系统服务
  4. 用户在PC处理登录状态时通过手机扫码授权手机登录(使用得比较少)
  5. 用户在手机处理登录状态进通过手机扫码授权PC进行登录(比较常见)

通过对场景的细分,得到如下不同的认证token类别:

  1. 原始账号密码类别
    • 用户名和密码
    • API应用ID/KEY
  2. 会话ID类别
    • 浏览器端token
    • 移动端token
    • API应用token
  3. 接口调用类别
    • 接口访问token
  4. 身份授权类别
    • PC和移动端相互授权的token

3   token的类别

不同场景的token进行如下几个维度的对比:

天然属性 对比:

  1. 使用成本

    本认证方式在使用的时候,造成的不便性。比如:

    • 账号密码需要用户打开页面然后逐个键入
    • 二维码需要用户掏出手机进行扫码操作
  2. 变化成本

    本认证方式,token发生变化时,用户需要做出的相应更改的成本:

    • 用户名和密码发生变化时,用户需要额外记忆和重新键入新密码
    • API应用ID/KEY发生变化时,第三方应用需要重新在代码中修改并部署
    • 授权二维码发生变化时,需要用户重新打开手机应用进行扫码
  3. 环境风险

    • 被偷窥的风险
    • 被抓包的风险
    • 被伪造的风险

可调控属性 对比:

  1. 使用频率
    • 在网路中传送的频率
  2. 有效时间
    • 此token从创建到终结的生存时间

最终的目标:安全和影响。

安全和隐私性主要体现在:

  • token 不容易被窃取和盗用(通过对传送频率控制)
  • token 即使被窃取,产生的影响也是可控的(通过对有效时间控制)

关于隐私及隐私破坏后的后果,有如下的基本结论:

  1. 曝光频率高的容易被截获
  2. 生存周期长的在被截获后产生的影响更严重和深远

遵守如下原则:

  1. 变化成本高的token不要轻易变化
  2. 不轻易变化的token要减少曝光频率(网络传输次数)
  3. 曝光频率高的token的生存周期要尽量短

将各类token的固有特点及可控属性进行调控后, 对每个指标进行量化评分(1~5分),我们可以得到如下的对比表:


备注:

  • user_name/passwd 和  app_id/app_key 是等价的效果

4   token的层级关系

参考上一节的对比表,可以很容易对这些不同用途的token进行分层,主要可以分为4层:

  1. 密码层

    最传统的用户和系统之间约定的数字身份认证方式

  2. 会话层

    用户登录后的会话生命周期的会话认证

  3. 调用层

    用户在会话期间对应用程序接口的调用认证

  4. 应用层

    用户获取了接口访问调用权限后的一些场景或者身份认证应用

token的分层图如下:

在一个多客户端的信息系统里面,这些token的产生及应用的内在联系如下:

  1. 用户输入用户名和用户口令进行一次性认证
  2. 在  不同 的终端里面生成拥有  不同 生命周期的会话token
  3. 客户端会话token从服务端交换生命周期短但曝光  频繁 的接口访问token
  4. 会话token可以生成和刷新延长  access_token 的生存时间
  5. access_token可以生成生存周期最短的用于授权的二维码的token

使用如上的架构有如下的好处:

  1. 良好的统一性。可以解决不同平台上认证token的生存周期的  归一化 问题
  2. 良好的解耦性。核心接口调用服务器的认证 access_token 可以完成独立的实现和部署
  3. 良好的层次性。不同平台的可以有完全不同的用户权限控制系统,这个控制可以在  会话层 中各平台解决掉

4.1   账号密码

广义的  账号/密码 有如下的呈现方式:

  1. 传统的注册用户名和密码
  2. 应用程序的app_id/app_key

它们的特点如下:

  1. 会有特别的意义

    比如:用户自己为了方便记忆,会设置有一定含义的账号和密码。

  2. 不常修改

    账号密码对用户有特别含义,一般没有特殊情况不会愿意修改。 而app_id/app_key则会写在应用程序中,修改会意味着重新发布上线的成本

  3. 一旦泄露影响深远

    正因为不常修改,只要泄露了基本相当于用户的网络身份被泄露,而且只要没被察觉这种身份盗用就会一直存在

所以在认证系统中应该尽量减少传输的机会,避免泄露。

4.2   客户端会话token

功能:充当着session的角色,不同的客户端有不同的生命周期。

使用步骤:

  1. 用户使用账号密码,换取会话token

不同的平台的token有不同的特点。

Web平台生存周期短

主要原因:

  1. 环境安全性

    由于web登录环境一般很可能是公共环境,被他人盗取的风险值较大

  2. 输入便捷性

    在PC上使用键盘输入会比较便捷

移动端生存周期长

主要原因:

  1. 环境安全性

    移动端平台是个人用户极其私密的平台,它人接触的机会不大

  2. 输入便捷性

    在移动端上使用手指在小屏幕上触摸输入体验差,输入成本高

4.3   access_token

功能:服务端应用程序api接口访问和调用的凭证。

使用步骤:

  1. 使用具有较长生命周期的会话token来换取此接口访问token。

其曝光频率直接和接口调用频率有关,属于高频使用的凭证。 为了照顾到隐私性,尽量减少其生命周期,即使被截取了,也不至于产生严重的后果。

注意:在客户端token之下还加上一个access_token, 主要是为了让具有不同生命周期的客户端token最后在调用api的时候, 能够具有统一的认证方式。

4.4   pam_token

功能:由已经登录和认证的PC端生成的二维码的原始串号(Pc Auth Mobile)。

主要步骤如下:

  1. PC上用户已经完成认证,登录了系统
  2. PC端生成一组和此用户相关联的pam_token
  3. PC端将此pam_token的使用链接生成二维码
  4. 移动端扫码后,请求服务器,并和用户信息关联
  5. 移动端获取refresh_token(长时效的会话)
  6. 根据 refresh_token 获取 access_token
  7. 完成正常的接口调用工作

备注:

  • 生存周期为2分钟,2分钟后过期删除
  • 没有被使用时,每1分钟变一次
  • 被使用后,立刻删除掉
  • 此种认证模式一般不会被使用到

4.5   map_token

功能:由已经登录的移动app来扫码认证PC端系统,并完成PC端系统的登录(Mobile Auth Pc)。

主要步骤:

  1. 移动端完成用户身份的认证登录app
  2. 未登录的PC生成匿名的  map_token
  3. 移动端扫码后在db中生成  map_token 和用户关联(完成签名)
  4. db同时针对此用户生成  web_token
  5. PC端一直以  map_token 为参数查找此命名用户的  web_token
  6. PC端根据  web_token 去获取  access_token
  7. 后续正常的调用接口调用工作

备注:

  • 生存周期为2分钟,2分钟后过期删除
  • 没有被使用时,每1分钟变一次
  • 被使用后,立刻删除掉

5   小结与展望

本文所设计的基于token的身份认证系统,主要解决了如下的问题:

  1. token的分类问题
  2. token的隐私性参数设置问题
  3. token的使用场景问题
  4. 不同生命周期的token分层转化关系

本文中提到的设计方法,在  应用层 中可以适用于且不限于如下场景中:

  1. 用户登录
  2. 有时效的优惠券发放
  3. 有时效的邀请码发放
  4. 有时效的二维码授权
  5. 具有时效  手机/邮件 验证码
  6. 多个不同平台调用同一套API接口
  7. 多个平台使用同一个身份认证中心

至于更多的使用场景,就需要大家去发掘了。

关于如何在技术上实现不同token的生存周期问题,将在后续文章中进行介绍,敬请期待。

 

补充内容:关于具备生命周期的token的技术实现方式

1
http://www.cnblogs.com/beer/p/6030882.html

"过期不候"--具备生命周期的数据的技术实现方案


使用logstash同步mysql 多表数据到ElasticSearch实践 - 三度 - 博客园

$
0
0

参考样式即可,具体使用配置参数根据实际情况而定

input {  
    jdbc {  
      jdbc_connection_string => "jdbc:mysql://localhost/数据库名"  
      jdbc_user => "root"  
      jdbc_password => "password"  
      jdbc_driver_library => "mysql-connector-java-5.1.45-bin.jar所在位置"  
      jdbc_driver_class => "com.mysql.jdbc.Driver"
      codec => plain {charset => "UTF-8"}
      record_last_run => true
      jdbc_paging_enabled => "true"  
      jdbc_page_size => "1000"  
      statement => "sql statement"   
      schedule => "* * * * *"  
      type => "数据库表名1"  
      tags => "数据库表名1"
    }
    jdbc {  
      jdbc_connection_string => "jdbc:mysql://localhost/数据库名"  
      jdbc_user => "root"  
      jdbc_password => "password"  
      jdbc_driver_library => "mysql-connector-java-5.1.45-bin.jar所在位置"  
      jdbc_driver_class => "com.mysql.jdbc.Driver"
      codec => plain {charset => "UTF-8"}
      record_last_run => true
      jdbc_paging_enabled => "true"  
      jdbc_page_size => "1000"  
      statement => "sql statement"   
      schedule => "* * * * *"  
      type => "数据库表名2"
      tags => "数据库表名2"
    }
}  

filter {  
    json {  
        source => "message"  
        remove_field => ["message"]  
    }  
}  

output {  
    if [type] == "数据库表名1"{
        elasticsearch {
            hosts => ["els的host地址"]  
            index => "数据库表名1对应的els的index"  
            document_id => "%{唯一id}"
        }
    }
    if [type] == "数据库表名2"{
        elasticsearch {
            hosts => ["els的host地址"]  
            index => "数据库表名2对应的els的index"  
            document_id => "%{唯一id}"
        }
    }
    stdout {   
        codec => json_lines  
    }  
}

如何做好Code Review - 麦机长 - 博客园

$
0
0

Code Review(代码审查)很多团队都会做,效果如何不好说。如果你能轻易地从一堆出自正经团队之手的代码里找出几个低级错误,往往意味着团队管理者长期忽视了Code Review的重要性。

根据经验,匆匆应付功能实现和漏洞修复而将Code Review流于形式的团队不在少数。当然,每个人都能列举一大堆“客观原因”,而且每一条理由听起来都是那么的有说服力。然而,没做好就是没做好,狡辩只会让场面变得更加恶心。

What(什么是Code Review)

A code review is the process of examining written code with the purpose of highlighting mistakes in order to learn from them.

-- Techopedia

这是目前我见过对Code Review最言简意赅的定义。其实怎么描述并不重要,重要的是我们要达到什么样的目的。

Why(为什么要做Code Review)

提高代码质量是程序员端稳饭碗、少挨点儿骂的最有效途径。其实Code Review就是很好的相互切磋、共同进步的机会,效果要比独自埋头干啃《21天精通×××》之类的“宝典”好得多。当然,前提是目的明确、态度端正。

Code Review主要目的就两个:

查错

Code Review不是用来查找低级错误的,而是参与者以提交者以外的视角阅读和审视代码,尽可能地找到逻辑上的问题。

学习

与其说Code Review重在找到问题,不如说其核心目的在于营造团队学习氛围、提升成员对软件品质的追求。我经历过不少团队,为了营造学习氛围,生拉硬拽地要求成员定期举行技术分享会,结果往往敷衍了事、不了了之。

How(怎样做Code Review)

下面根据Code Review中涉及的主要人物角色来讲讲我推荐的方式。注意,这不是标准答案。

具体划分角色责任之前,我建议每个技术团队都要找到并严格执行适合本团队技术栈的编码规范,甚至包括IDE配置和开发环境参数设定等,以确保每位成员都“说着同样的语言”,并减少在命名规则、排版样式等方面的争论,将时间和精力聚焦到对功能实现和业务优化这些实质性的问题上来。

开发小组技术负责人

每一位开发小组技术负责人都应该积极实施并维护Code Review机制,要求每位成员在提交代码的时候,都必须经过交叉Review,也就是每一次代码提交到主干时,都必须经过另一位相同技术领域同事的Review,否则将被视为提交了与存在编译时错误的代码同等的严重过失。

每次代码提交的交叉Review,开发小组技术负责人应当随机抽取包括自己在内的任何一位技术人员进行,不要让提交者能够很轻易地预知将会是谁来做自己这一次的Reviewer,否则很容易变成形式主义。

并且,对于Feature实现的Code Review,开发小组技术负责人应该较为频繁但不定期地进行公开Review。组织一场会议,召集整个开发小组的成员一起对此次提交的代码进行审查。

提交者

不论Code Review是私下的还是公开的,提交者都不能提交任何存在编译时错误的代码,这是非常低级的错误。首先在提交代码之前再次进行编译,是确保即将提交的代码不存在编译时错误的必要步骤。其次,也是很多人容易疏忽的,确保本次新增的本地资源文件都被加入了源代码管理,否则即使本地能编译通过,别人拿到你的代码也依然存在编译时问题。

提交代码之前,自己先 diff一下,首先确保代码不存在前面提到的诸如命名、格式等方面的低级错误;然后确定自己对每一处代码变动的理解都非常明确,并且自己已经找不出改进方案;最后确保所有Hard Code都已经被移除,这一点可以参考我之前写的 《没什么技术含量的Remove Before Flight》

提交者在代码被Review之前,还应该调整好心态,把别人的询问、质疑、建议、批评,通通视作可能的提升机会,而不要主观上认定自己给出的就是最优解,而别人都是“不明真相的围观群众”。也许别人在不了解背景信息或上下文的情况下,给出了错误的建议,提交者也应当将此作为锻炼思维和口才的友好辩论,而不是玻璃心受到了侵犯似的直接怼回去。

参与者

参与者应该对编码规范了然于心,对于代码中每一处不符合团队现行编码规范的地方都要不厌其烦地标注出来。很多人认为这个无所谓,反正机器最后读的都是0和1——对,机器只认识0和1,所以源代码其实是写给人看的。不管代码由多少人写就,最终看上去如同出自同一人之手,这种代码的阅读体验和效率绝对要比那种百家争鸣式的好得多。

如果是面向对象的编程语言,参与者应当考察提交者对抽象的理解和实践,是否准确以及是否过浅或过深。抽象过浅,看上去往往是大杂烩;抽象过深,读起来显得吃力。对于这两种情况,参与者都可以提出自己的看法和建议,不要抱着“你这不行,听我的”的态度,否则很容易形成对立的情绪,进而影响团队对Code Review的积极性。

对于代码实现是否存在改进空间这个问题,参与者应该在阅读新代码时,尽可能全面地去理解问题域、了解需求的具体细节,而不是想当然地抛出质疑和意见,给人以浮躁的印象。如果参与者确定自己清楚地理解了需求和问题,依然对当前的代码实现有改进建议,那么就大胆地提出来,这就是Code Review的核心目的!

一个复杂系统的拆分改造实践 - zhanlijun - 博客园

$
0
0

1 为什么要拆分?

先看一段对话。

从上面对话可以看出拆分的理由:

1)  应用间耦合严重。系统内各个应用之间不通,同样一个功能在各个应用中都有实现,后果就是改一处功能,需要同时改系统中的所有应用。这种情况多存在于历史较长的系统,因各种原因,系统内的各个应用都形成了自己的业务小闭环;

2)  业务扩展性差。数据模型从设计之初就只支持某一类的业务,来了新类型的业务后又得重新写代码实现,结果就是项目延期,大大影响业务的接入速度;

3)  代码老旧,难以维护。各种随意的if else、写死逻辑散落在应用的各个角落,处处是坑,开发维护起来战战兢兢;

4)   系统扩展性差。系统支撑现有业务已是颤颤巍巍,不论是应用还是DB都已经无法承受业务快速发展带来的压力;

5)  新坑越挖越多,恶性循环。不改变的话,最终的结果就是把系统做死了。

2 拆前准备什么?

2.1 多维度把握业务复杂度

一个老生常谈的问题,系统与业务的关系?

我们最期望的理想情况是第一种关系(车辆与人),业务觉得不合适,可以马上换一辆新的。但现实的情况是更像心脏起搏器与人之间的关系,不是说换就能换。一个系统接的业务越多,耦合越紧密。如果在没有真正把握住业务复杂度之前贸然行动,最终的结局就是把心脏带飞。

如何把握住业务复杂度?需要多维度的思考、实践。

一个是技术层面,通过与pd以及开发的讨论,熟悉现有各个应用的领域模型,以及优缺点,这种讨论只能让人有个大概,更多的细节如代码、架构等需要通过做需求、改造、优化这些实践来掌握。

各个应用熟悉之后,需要从系统层面来构思,我们想打造平台型的产品,那么最重要也是最难的一点就是功能集中管控,打破各个应用的业务小闭环,统一收拢,这个决心更多的是开发、产品、业务方、各个团队之间达成的共识,可以参考《 微服务(Microservice)那点事》一文,“按照业务或者客户需求组织资源”。

此外也要与业务方保持功能沟通、计划沟通,确保应用拆分出来后符合使用需求、扩展需求,获取他们的支持。

2.2 定义边界,原则:高内聚,低耦合,单一职责!

业务复杂度把握后,需要开始定义各个应用的服务边界。怎么才算是好的边界?像葫芦娃兄弟一样的应用就是好的!

举个例子,葫芦娃兄弟(应用)间的技能是相互独立的,遵循单一职责原则,比如水娃只能喷水,火娃只会喷火,隐形娃不会喷水喷火但能隐身。更为关键的是,葫芦娃兄弟最终可以合体为金刚葫芦娃,即这些应用虽然功能彼此独立,但又相互打通,最后合体在一起就成了我们的平台。

这里很多人会有疑惑,拆分粒度怎么控制?很难有一个明确的结论,只能说是结合业务场景、目标、进度的一个折中。但总体的原则是先从一个大的服务边界开始,不要太细,因为随着架构、业务的演进,应用自然而然会再次拆分,让正确的事情自然发生才最合理。

2.3 确定拆分后的应用目标

一旦系统的宏观应用拆分图出来后,就要落实到某一具体的应用拆分上了。

首先要确定的就是某一应用拆分后的目标。拆分优化是没有底的,可能越做越深,越做越没结果,继而又影响自己和团队的士气。比如说可以定这期的目标就是将db、应用分拆出去,数据模型的重新设计可以在第二期。

2.4 确定当前要拆分应用的架构状态、代码情况、依赖状况,并推演可能的各种异常。

动手前的思考成本远远低于动手后遇到问题的解决成本。应用拆分最怕的是中途说“他*的,这块不能动,原来当时这样设计是有原因的,得想别的路子!”这时的压力可想而知,整个节奏不符合预期后,很可能会接二连三遇到同样的问题,这时不仅同事们士气下降,自己也会丧失信心,继而可能导致拆分失败。

2.5 给自己留个锦囊,“有备无患”。

锦囊就四个字“有备无患”,可以贴在桌面或者手机上。在以后具体实施过程中,多思考下“方案是否有多种可以选择?复杂问题能否拆解?实际操作时是否有预案?”,应用拆分在具体实践过程中比拼得就是细致二字,多一份方案,多一份预案,不仅能提升成功概率,更给自己信心。

2.6 放松心情,缓解压力

收拾下心情,开干!

3 实践

3.1 db拆分实践

DB拆分在整个应用拆分环节里最复杂,分为垂直拆分和水平拆分两种场景,我们都遇到了。垂直拆分是将库里的各个表拆分到合适的数据库中。比如一个库中既有消息表,又有人员组织结构表,那么将这两个表拆分到独立的数据库中更合适。

水平拆分:以消息表为例好了,单表突破了千万行记录,查询效率较低,这时候就要将其分库分表。

3.1.1 主键id接入全局id发生器

DB拆分的第一件事情就是使用全局id发生器来生成各个表的主键id。为什么?

举个例子,假如我们有一张表,两个字段id和token,id是自增主键生成,要以token维度来分库分表,这时继续使用自增主键会出现问题。

正向迁移扩容中,通过自增的主键,到了新的分库分表里一定是唯一的,但是,我们要考虑迁移失败的场景,如下图所示,新的表里假设已经插入了一条新的记录,主键id也是2,这个时候假设开始回滚,需要将两张表的数据合并成一张表(逆向回流),就会产生主键冲突!

因此在迁移之前,先要用全局唯一id发生器生成的id来替代主键自增id。这里有几种全局唯一id生成方法可以选择。

1)snowflake: https://github.com/twitter/snowflake;(非全局递增)

2) mysql新建一张表用来专门生成全局唯一id(利用auto_increment功能)(全局递增);

3)有人说只有一张表怎么保证高可用?那两张表好了(在两个不同db),一张表产生奇数,一张表产生偶数。或者是n张表,每张表的负责的步长区间不同(非全局递增)

4)……

我们使用的是阿里巴巴内部的tddl-sequence(mysql+内存),保证全局唯一但非递增,在使用上遇到一些坑:

1)对按主键id排序的sql要提前改造。因为id已经不保证递增,可能会出现乱序场景,这时候可以改造为按gmt_create排序;

2)报主键冲突问题。这里往往是代码改造不彻底或者改错造成的,比如忘记给某一insert sql的id添加#{},导致继续使用自增,从而造成冲突;

3.1.2 建新表&迁移数据&binlog同步

1)  新表字符集建议是utf8mb4,支持表情符。 新表建好后索引不要漏掉,否则可能会导致慢sql!从经验来看索引被漏掉时有发生,建议事先列计划的时候将这些要点记下,后面逐条检查;

2)  使用全量同步工具或者自己写job来进行全量迁移;全量数据迁移务必要在业务低峰期时操作,并根据系统情况调整并发数;

3)  增量同步。全量迁移完成后可使用binlog增量同步工具来追数据,比如阿里内部使用精卫,其它企业可能有自己的增量系统,或者使用阿里开源的cannal/otter: https://github.com/alibaba/canal?spm=5176.100239.blogcont11356.10.5eNr98

https://github.com/alibaba/otter/wiki/QuickStart?spm=5176.100239.blogcont11356.21.UYMQ17

增量同步起始获取的binlog位点必须在全量迁移之前,否则会丢数据,比如我中午12点整开始全量同步,13点整全量迁移完毕,那么增量同步的binlog的位点一定要选在12点之前。

位点在前会不会导致重复记录?不会!线上的MySQL binlog是row 模式,如一个delete语句删除了100条记录,binlog记录的不是一条delete的逻辑sql,而是会有100条binlog记录。insert语句插入一条记录,如果主键冲突,插入不进去。

3.1.3 联表查询sql改造

现在主键已经接入全局唯一id,新的库表、索引已经建立,且数据也在实时追平,现在可以开始切库了吗?no!

考虑以下非常简单的联表查询sql,如果将B表拆分到另一个库里的话,这个sql怎么办?毕竟跨库联表查询是不支持的!

因此,在切库之前,需要将系统中上百个联表查询的sql改造完毕。

如何改造呢?

1)业务避免

业务上松耦合后技术才能松耦合,继而避免联表sql。但短期内不现实,需要时间沉淀;

2)全局表

每个应用的库里都冗余一份表,缺点:等于没有拆分,而且很多场景不现实,表结构变更麻烦;

3)冗余字段

就像订单表一样,冗余商品id字段,但是我们需要冗余的字段太多,而且要考虑字段变更后数据更新问题;

4)内存拼接

4.1)通过RPC调用来获取另一张表的数据,然后再内存拼接。1)适合job类的sql,或改造后RPC查询量较少的sql;2)不适合大数据量的实时查询sql。假设10000个ID,分页RPC查询,每次查100个,需要5ms,共需要500ms,rt太高。

4.2)本地缓存另一张表的数据

适合数据变化不大、数据量查询大、接口性能稳定性要求高的sql。

3.1.4切库方案设计与实现(两种方案)

以上步骤准备完成后,就开始进入真正的切库环节,这里提供两种方案,我们在不同的场景下都有使用。

a)DB停写方案

优点:快,成本低;

缺点:

1)如果要回滚得联系DBA执行线上停写操作,风险高,因为有可能在业务高峰期回滚;

2)只有一处地方校验,出问题的概率高,回滚的概率高

举个例子,如果面对的是比较复杂的业务迁移,那么很可能发生如下情况导致回滚:

sql联表查询改造不完全;

sql联表查询改错&性能问题;

索引漏加导致性能问题;

字符集问题

此外,binlog逆向回流很可能发生字符集问题(utf8mb4到gbk),导致回流失败。这些binlog同步工具为了保证强最终一致性,一旦某条记录回流失败,就卡住不同步,继而导致新老表的数据不同步,继而无法回滚!

b)双写方案

第2步“打开双写开关,先写老表A再写新表B”,这时候确保写B表时try catch住,异常要用很明确的标识打出来,方便排查问题。第2步双写持续短暂时间后(比如半分钟后),可以关闭binlog同步任务。

优点:

1)将复杂任务分解为一系列可测小任务,步步为赢;

2)线上不停服,回滚容易;

3)字符集问题影响小

缺点:

1)流程步骤多,周期长;

2)双写造成RT增加

3.1.5 开关要写好

不管什么切库方案,开关少不了,这里开关的初始值一定要设置为null!

如果随便设置一个默认值,比如”读老表A“,假设我们已经进行到读新表B的环节了。这时重启了应用,在应用启动的一瞬间,最新的“读新表B”的开关推送等可能没有推送过来,这个时候就可能使用默认值,继而造成脏数据!

3.2 拆分后一致性怎么保证?

以前很多表都在一个数据库内,使用事务非常方便,现在拆分出去了,如何保证一致性?

1)分布式事务

性能较差,几乎不考虑。

2)消息机制补偿如何用消息系统避免分布式事务?

3)定时任务补偿

用得较多,实现最终一致,分为加数据补偿,删数据补偿两种。

3.3 应用拆分后稳定性怎么保证?

一句话:怀疑第三方防备使用方做好自己!


1)怀疑第三方

a)防御式编程,制定好各种降级策略;

  • 比如缓存主备、推拉结合、本地缓存……

b)遵循快速失败原则,一定要设置超时时间,并异常捕获;

c)强依赖转弱依赖,旁支逻辑异步化

  • 我们对某一个核心应用的旁支逻辑异步化后,响应时间几乎缩短了1/3,且后面中间件、其它应用等都出现过抖动情况,而核心链路一切正常;

d)适当保护第三方,慎重选择重试机制

2)防备使用方

a)设计一个好的接口,避免误用

  • 遵循接口最少暴露原则;很多同学搭建完新应用后会随手暴露很多接口,而这些接口由于没人使用而缺乏维护,很容易给以后挖坑。听到过不只一次对话,”你怎么用我这个接口啊,当时随便写的,性能很差的“;
  • 不要让使用方做接口可以做的事情;比如你只暴露一个getMsgById接口,别人如果想批量调用的话,可能就直接for循环rpc调用,如果提供getMsgListByIdList接口就不会出现这种情况了。
  • 避免长时间执行的接口;特别是一些老系统,一个接口背后对应的可能是for循环select DB的场景。

b)容量限制

  • 按应用优先级进行流控;不仅有总流量限流,还要区分应用,比如核心应用的配额肯定比非核心应用配额高;
  • 业务容量控制。有些时候不仅仅是系统层面的限制,业务层面也需要限制。举个例子,对saas化的一些系统来说,”你这个租户最多1w人使用“。

3)做好自己

a) 单一职责

b) 及时清理历史坑

  • 例:例如我们改造时候发现一年前留下的坑,去掉后整个集群cpu使用率下降1/3

c)  运维SOP

  • 说实话,线上出现问题,如果没有预案,再怎么处理都会超时。曾经遇到过一次DB故障导致脏数据问题,最终只能硬着头皮写代码来清理脏数据,但是时间很长,只能眼睁睁看着故障不断升级。经历过这个事情后,我们马上设想出现脏数据的各种场景,然后上线了三个清理脏数据的job,以防其它不可预知的产生脏数据的故障场景,以后只要遇到出现脏数据的故障,直接触发这三个清理job,先恢复再排查。

d) 资源使用可预测

  • 应用的cpu、内存、网络、磁盘心中有数
    • 正则匹配耗cpu
    • 耗性能的job优化、降级、下线(循环调用rpc或sql)
    • 慢sql优化、降级、限流
    • tair/redis、db调用量要可预测
    • 例:tair、db

举个例子: 某一个接口类似于秒杀功能,qps非常高(如下图所示),请求先到tair,如果找不到会回源到DB,当请求突增时候,甚至会触发tair/redis这层缓存的限流,此外由于缓存在一开始是没数据的,请求会穿透到db,从而击垮db。

这里的核心问题就是tair/redis这层资源的使用不可预测,因为依赖于接口的qps,怎么让请求变得可预测呢?

如果我们再增加一层本地缓存(guava,比如超时时间设置为1秒),保证单机对一个key只有一个请求回源,那样对tair/redis这层资源的使用就可以预知了。假设有500台client,对一个key来说,一瞬间最多500个请求穿透到Tair/redis,以此类推到db。

再举个例子:

比如client有500台,对某key一瞬间最多有500个请求穿透到db,如果key有10个,那么请求最多可能有5000个到db,恰好这些sql的RT有些高,怎么保护DB的资源?

可以通过一个定时程序不断将数据从db刷到缓存。这里就将不可控的5000个qps的db访问变为可控的个位数qps的db访问。

4  总结

1)做好准备面对压力!

2)复杂问题要拆解为多步骤,每一步可测试可回滚!

这是应用拆分过程中的最有价值的实践经验!

3)墨菲定律:你所担心的事情一定会发生,而且会很快发生,所以准备好你的SOP(标准化解决方案)! 

某个周五和组里同事吃饭时讨论到某一个功能存在风险,约定在下周解决,结果周一刚上班该功能就出现故障了。以前讲小概率不可能发生,但是概率再小也是有值的,比如p=0.00001%,互联网环境下,请求量足够大,小概率事件就真发生了。

4)借假修真

这个词看上去有点玄乎,顾名思义,就是在借者一些事情,来提升另外一种能力,前者称为假,后者称为真。在任何一个单位,对核心系统进行大规模拆分改造的机会很少,因此一旦你承担起责任,就毫不犹豫地全力以赴吧!不要被过程的曲折所吓倒,心智的磨砺,才是本真。


启动并运行 Open Distro for Elasticsearch | 亚马逊AWS官方博客

$
0
0

简介

2019 年 3 月 11 日,我们发布了 Open Distro for Elasticsearch,这是 Elasticsearch 的一个增值发行版,100% 开源(采用 Apache 2.0 许可证)并且由 AWS 提供支持。(另请参阅 Jeff Barr 的 Open Distro for Elasticsearch和 Adrian Cockcroft 的 Keeping Open Source Open – Open Distro for Elasticsearch。) 除 源代码存储库外,Open Distro for Elasticsearch 和 Kibana 还可作为 RPM 和 Docker 容器提供,并提供适用于 SQL JDBC 驱动程序和 PerfTop CLI 的独立下载版。您可以在笔记本电脑上、在数据中心里,或者在云中运行此代码。有关详细信息,请参阅 Open Distro for Elasticsearch 文档

想通过一种简单的方式深入了解并试用这些功能吗? 对于 Mac 和 Windows,您可以使用 Docker Desktop 部署和测试 Open Distro for Elasticsearch。本博文将引导您完成整个流程。

部署 Docker Desktop

Docker Desktop (DD) 为您提供了在隔离环境中在笔记本电脑上运行 Docker 的简便方法。我的笔记本电脑是 Macintosh,因此我从 下载页面下载了 Docker Desktop 的 Mac 镜像,然后按照 安装说明将 DD 拖到我的 Applications 文件夹中。

为了使用下面的 docker-compose 测试 Open Distro for Elasticsearch,您需要增加分配给 DD 的 RAM。在 Docker 完成初始启动后,我转到菜单栏中的 Docker 图标并选择 Preferences…

选择 Advanced选项卡,然后将内存滑块移动到至少 4 GiB:

单击窗口的关闭框。单击 Apply以允许 Docker 使用新设置重新启动。等待 Docker 重新启动,然后再继续。

运行 Open Distro for Elasticsearch

您首先需要提取 Open Distro for Elasticsearch Docker 镜像。打开终端窗口,然后运行:

docker pull amazon/opendistro-for-elasticsearch:0.7.0

Docker 将获取 Elasticsearch 的容器镜像。您还需要 Kibana 发行版。运行:

docker pull amazon/opendistro-for-elasticsearch-kibana:0.7.0

现在在您的笔记本电脑上创建一个目录,该目录将保存 docker-compose 文件以及与您的项目相关的任何其他资产:

mkdir odfe-docker
cd odfe-docker

使用以下内容创建 docker-compose.yml:

version: '3'
services:
  odfe-node1:
    image: amazon/opendistro-for-elasticsearch:0.7.0
    container_name: odfe-node1
    environment:
      - cluster.name=odfe-cluster
      - bootstrap.memory_lock=true # along with the memlock settings below, disables swapping
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - odfe-data1:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
      - 9600:9600 # required for Performance Analyzer
    networks:
      - odfe-net
  odfe-node2:
    image: amazon/opendistro-for-elasticsearch:0.7.0
    container_name: odfe-node2
    environment:
      - cluster.name=odfe-cluster
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - discovery.zen.ping.unicast.hosts=odfe-node1
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - odfe-data2:/usr/share/elasticsearch/data
    networks:
      - odfe-net
  kibana:
    image: amazon/opendistro-for-elasticsearch-kibana:0.7.0
    container_name: odfe-kibana
    ports:
      - 5601:5601
    expose:
      - "5601"
    environment:
      ELASTICSEARCH_URL: https://odfe-node1:9200
    networks:
      - odfe-net

volumes:
  odfe-data1:
  odfe-data2:

networks:
  odfe-net:

从 odfe-docker 目录,运行:

docker-compose up

您可以使用 docker ps 查看正在运行的容器。(编辑以适合页面大小):

CONTAINER ID  IMAGE                                              STATUS  NAMES
fb1a78290e33  amazon/opendistro-for-elasticsearch-kibana:0.7.0   Up…      odfe-kibana
a53942e76501  amazon/opendistro-for-elasticsearch:0.7.0          Up…      odfe-node1
f33f91837f47  amazon/opendistro-for-elasticsearch:0.7.0          Up…      odfe-node2

要确保 Elasticsearch 正常响应,请运行:

curl -XGET https://localhost:9200 -u admin:admin --insecure

Elasticsearch 会做如下响应:

{"name" : "NHKRnp4","cluster_name" : "odfe-cluster","cluster_uuid" : "ItWH-yLSQSCD9eGiWbvDDQ","version" : {"number" : "6.5.4","build_flavor" : "oss","build_type" : "tar","build_hash" : "d2ef93d","build_date" : "2018-12-17T21:17:40.758843Z","build_snapshot" : false,"lucene_version" : "7.5.0","minimum_wire_compatibility_version" : "5.6.0","minimum_index_compatibility_version" : "5.0.0"
  },"tagline" : "You Know, for Search"
}

登录 Kibana

Kibana 是一个 Web 客户端,用于向 Elasticsearch 发送 API 请求以支持其可视化。在浏览器中,导航到 http://localhost:5601。您将看到 Open Distro for Elasticsearch 登录页面:

Open Distro for Elasticsearch 预先配置了 Username(admin) 和 Password(admin)。使用这些凭证登录。(请注意,此设置不安全。我们将在后续博文中向您展示如何更改这些密码。) 您会看到 Kibana 的启动页面。单击 Try our sample data。我在以下屏幕中添加了 示例 Web 日志数据集。

检查样本数据

您可以正常方式与示例 Web 日志数据进行交互。单击 Discover选项卡,将时间窗口扩展到 Last 7 days,您应该会看到如下内容

 

 

您可以使用 Kibana 的 Dev Tools窗格来运行查询。单击选项卡,然后输入以下查询:

GET kibana_sample_data_logs/_search
{"query": {"bool": {"must": [
        {"term": {"machine.os.keyword": {"value": "ios"
            }
          }
        },
        {"range": {"bytes": {"gte": 5000
            }
          }
        },
        {"term": {"clientip": {"value": "68.0.0.0/8"
            }
          }
        }
      ]
    }
  }
}

我有 8 条来自 IOS 设备的结果,其中返回 5000 多个字节,IP 地址位于 68.CIDR 块。

通常,您可以继续浏览 Kibana 和 Elasticsearch,构建或导入现有的可视化效果和控制面板等。

小结

祝贺您! 您已成功在笔记本电脑上以本地方式部署了 Open Distro for Elasticsearch,登录并浏览了 Kibana 的其中一个示例数据集。请随时关注! 我们将在即将发布的博文中深入探讨 Open Distro for Elasticsearch 的插件。

有问题或疑问? 希望参与讨论? 您可以在 我们的论坛上获得帮助并讨论 Open Distro for Elasticsearch。您可以 在这里提出问题

本篇作者

Jon Handler

Jon Handler

Jon Handler (@_searchgeek) 是总部位于加利福尼亚州帕罗奥图市的 Amazon Web Services 的首席解决方案架构师。Jon 与 CloudSearch 和 Elasticsearch 团队密切合作,为想要将搜索工作负载迁移到 AWS 云的广大客户提供帮助和指导。在加入 AWS 之前,Jon 作为一名软件开发人员,曾为某个大型电子商务搜索引擎编写代码长达四年。Jon 拥有宾夕法尼亚大学的文学学士学位,以及西北大学计算机科学和人工智能理学硕士和博士学位。

树莓派来做电视盒子 - 就是为了好玩 - 博客园

$
0
0

本期的树莓派专题,我们来将树莓派设置为电视盒子来使用。目前市面上的盒子非常多,办个宽带也送个电视盒子。

电视盒子是一个小型的计算终端设备,只要简单的通过HDMI或色差线等技术将其与传统电视连接,就能在传统电视上实现网页浏览、网络视频播放、应用程序安装,甚至能将你手机、平板中的照片和视频投射到家中的大屏幕电视当中。它可以将互联网内容通过其在电视机上进行播放,此前在互联网领域被称之为网络高清播放机,后被广电总局定义为互联网电视机顶盒。它与可接入互联网的智能电视一起,统称为“互联网电视”。

其实树莓派单板加个外壳也确实挺像个”盒子“的,通过安装KODI软件实现电视盒子的功能。

KODI安装

树莓派中我们通过apt安装方式,安装如下:

sudo apt update
sudo apt install kodi

等待代码跑完就可以了,树莓派3B安装的KODI版本已经是kodi18,而树莓派4B安装的KODI版本还是kodi17。

为什么会这么关注kodi版本呢?主要还是kodi18已经支持硬件加速,而kodi17还不支持, 这就导致树莓派4B虽然硬件支持h.265(HEVC)编码,但是由于KODI版本不支持,播放相关视频还是可以看到肉眼可见的卡顿。 简直是无法忍受!!!

对于Linux这样的开源系统,它的好处就是如果官方没给升级,你可以自己动手改源代码升级。 理论上哈,毕竟有些代码给了也看不懂不是

对于树莓派4B,这里推荐安装Pplware提供的kodi18编译版本。需要通过添加源的方式进行安装,如下:

sudo curl -sSL http://pipplware.pplware.pt/pipplware/key.asc | apt-key add -
sudo echo 'deb http://pipplware.pplware.pt/pipplware/dists/buster/main/binary /' > /etc/apt/sources.list.d/kodi.list
sudo apt update
sudo apt install kodi

树莓派设置

通过树莓派的 主菜单>影音>Kodi,即可打开KODI软件。

中文设置,可以通过点击做上方中间的齿轮状设置按钮,选择 Interface Setting

选择 Skin,右侧 Font选择更改为 Arial based。设置字体是为了避免语言乱码。

然后就可以选择 Regional,右侧点击 language选择 Chinese(Simple)即可。

系统界面

系统信息

ps.我这里还是树莓派4B版本默认安装的kodi17版本。

媒体设置

播放设置

小结

就简单介绍到这里吧,盒子功能都差不多,KODI的功能设置非常丰富,大家折腾就可以了。

最近将树莓派连接上显示器、键盘、鼠标,开始树莓派桌面体验,目前来看还挺不错的,下期分享~~

欢迎关注我的公众号,持续更新中~~~

oracle存储过程通过JOB来实现并行执行_u012209894的专栏-CSDN博客

$
0
0

1、oracle版本为10G及以上,由于网上很多都没有完善,所以特此完善记录下来,仅供参考

需求:多个无依赖关系的存储过程并行执行(使用该功能前测试下服务器情况,貌似并行任务和数据库的CPU个数有直接关系,小于等于CPU个数*4)。

实现思路:在存储过程中创建任务,以便能直接通过JAVA输入动态参数调用。

第一步:建测试表

create table A ( A INTEGER);

第二步:创建存储过程

create or replace procedure test1

(
    i_tjrq   in    number,    --统计日期
    i_err_no out number   --输出参数
)
as
begin
insert into a values(i_tjrq);
end;


create or replace procedure test2
(
    i_tjrq   in    number,     --统计日期
        i_err_no out number   --输出参数
)
as
begin
insert into a values(i_tjrq);
end;



第三步:在存储过程中创建任务,以便操作

CREATE OR REPLACE PROCEDURE test3 (i_tjrq IN NUMBER)  as
x   NUMBER;
--m varchar2(100):='declare i_err_no NUMBER;begin test2('||i_tjrq||',i_err_no);test1('||i_tjrq||',i_err_no); end;';
BEGIN
   --SYS.DBMS_OUTPUT.put_line(m);
   sys.DBMS_JOB.submit (
      job         => x,
      what        => 'declare i_err_no NUMBER;begin test2('||i_tjrq||',i_err_no);test1('||i_tjrq||',i_err_no); end;',
      next_date   => TO_DATE ('14-05-2013 00:00:00', 'dd-mm-yyyy hh24:mi:ss'),
      interval    => 'trunc(sysdate+1)',
      no_parse    => FALSE);
   --SYS.DBMS_OUTPUT.put_line ('Job Number is: ' || TO_CHAR (x));
   COMMIT;
END;


--值得注意的是sys.DBMS_JOB.submit()里面的参数只在这里做简单的介绍

job是任务号--系统自行生成

what是可以存储pl/sql代码的函数,动态输入的变量可以在此拼接。

其他参数直接参考oracle官方文档或者自行搜索,就不一一叙述了。


select * from user_jobs 可以查看你用户名下任务的jobid 也就是上面的任务号,单个执行任务用下列语句实现。

begin
--dbms_job.remove(39); --删除任务
dbms_job.run(39);  --运行任务
end;

由于是直接创建在存储过程里面的,所以直接调用存储过程即可插入当天的数据。

begin
  -- Call the procedure
  test3(i_tjrq => :i_tjrq);
end;  --或者直接在pl/sql工具里面执行,查看a表你会发现系统数据已经插入。


到底啥叫运营管理?_阿朱=行业趋势+开发管理+架构-CSDN博客

$
0
0

今天早上有朋友问我啥叫运营管理。

(1)中国人对于运营这个词的滥用

一、来自互联网公司的运营

中国人听到运营这个词的时候,主要是来自中国互联网公司的营销部门:

1、拉新:用户运营

2、黏住:内容运营

3、活跃:社区运营

但这其实根本不叫运营。

二、在电子商务公司也有一套所谓运营的说法:

1、BD:供应商合作

2、营销活动:大促活动的策划、整合资源、执行

三、在传统企业也有一套所谓的运营说法:

1、渠道管理:全国分销网络的发展、总经理(选择/培训培养/考核激励)、销售目标下达/销售业绩管理,销售推进(甚至包含给渠道压货)

(2)运营的最初来源

其实运营这个词已经有100多年了,在工业革命的时候已经有了,在泰勒开启现代企业管理的时候已经有了。

工业革命大工业,按价值链条来分有主要三个环节段:

1、原材料采购

2、生产制造

3、仓储物流运输(含铁路运输)

运营一词首先来自铁路。因为铁路要全国连在一起,火车很多,但火车道很少。所以谁进站谁出站谁在什么时间使用那条铁道,这需要有计划编排、资源调度的专业人。这是运营人员和运营管理的最初起源。在没有自动化、计算机、互联网的一百年前,想高效地安全地保证铁路日常运转,是非常高难度的事情,许多数学家当时出了很多运筹算法来保证。

在工厂里,运营也被引入。一切都是因为大生产、托拉斯大并购引起的。当年沃顿这个人多牛,一手雇佣了现代企业科学管理之父泰勒当总工程师,进行最佳工序制定与最佳工艺制定、工人训练、工人排班调度、质量管理来完成炼钢任务的达成;一手又创办了美国第一个商学院沃顿商学院,培养职业经理人,做好财务目标的制定、财务核算、财务利润成本费用管控。

所以,现在很多人还把生产工厂、生产制造、仓储物流运输的负责人叫做运营管理负责人,把运营管理就认为生产制造管理/供应链管理仓储物流运输就是运营管理。

(3)现代企业运营和首席运营官COO

现代企业职能早就不限于生产制造、仓储物流运输了。

企业管理,经过百年的发展,也形成了完整的体系:

1、通用管理:战略规划、战略执行管理

2、通用管理:预算管理、计划管理、质量管理、风险管理、合规审计监察管理

3、通用管理:组织管理、流程管理

4、通用管理:考核管理、激励管理

5、职能管理:财务管理、人力管理、法务管理、IT管理...

6、业务管理:研发管理、生产制造管理、采购管理、仓储物流管理、营销管理、销售管理、服务管理...

一个企业也从CEO,逐步因为太专业而细拆出来CFO(财务)、CHO(人力),后来又拆出来CMO(营销)(西方是产品至上和品牌至上思维,中国是销售思维,所以中国对于营销地位不高,不设真正的CMO)。

在这些首席CXO之下,是研发(研发总负责人、产品设计、技术总监/总工)、生产制造负责人、采购负责人、仓储物流负责人、全国销售渠道管理负责人....。

中国人嘛,不太注重企业架构设计,只是因为有一个商机所以创办了一家企业,因为商机这个事要交付完成,所以招聘了一些人来落地。这样,事多了加人,人多了加事,企业就这样每天每年地跑下去。

所以,大部分中国人只了解业务管理和职能管理,不太了解通用管理。

尤其是,大部分中国企业,既不做战略严谨地成方法成体系的规划,也不做真正的强有力的战略执行落地。很多事情是是而非,口头说说而已。开完会,继续回到各部门干自己日拱一卒的事情,为销售业绩达成而努力。

但真正注重企业架构、注重战略执行的企业,往往会设置首席运营官COO这个岗位。

这个岗位设置的目的就是:保证战略执行落地。

那怎么保证战略执行落地呢?这就需要有组织、有流程制度、有工具地护航保证了。

所以,首席运营官会负责以下部门和职责:

1、战略执行运营部:雇佣不少PMO项目总监,搞公司级战略专项的项目计划编排、立项与评审、整合推进、Review、复盘、考核的PDCA

2、战略执行运营侧翼保证:战略执行运营部还有一些侧翼部门来辅助保障战略专项的执行,如战略专项预算管理部(主要偏资金预算和人力编制预算)、质量与风险管理部

3、流程制度与IT部:西方人一般是通过流程制度的制定和执行,来达成战略专项的落地。而中国人是不看重流程制度,主要靠人来强力推动战略专项的落地。西方人制定好流程制度后,会通过IT来保证流程的固化、透明、风控、变更、量化。

中国人完全没有以上这套完整的勾稽关联逻辑。

有些首席运营官会管理战略规划部,这样规划和执行就贯通了。战略规划部一般负责:

1、战略情报

2、战略规划

3、战略研讨

4、战略宣讲

当然,有些企业对战略非常关注,会设置专门的首席战略官岗位来专门负责战略规划。有句话叫:走对路、选对人。战略规划就是让企业走对路。选对人,是人力部门的核心事情。而人们常说的资源组织能力、资源组织管理能力、执行落地能力,其实说的就是上述首席运营官搞的那一堆事情。

战略规划,走对路;

战略领导,选对领军人物;

战略落地,战略专项管理。

这就是核心三要素。

有些首席运营官还会负责合规审计监察管理部,来保证大家日常落地的合规性。但这样搞是双刃剑,容易出现战略执行和合规审计一手抓监守自盗,所以合规审计监察管理一般会直接汇报给董事会。我过去专门写过一篇文章《 中国式企业治理》来说中国式企业治理方法。


k8s资源需求和限制, 以及pod驱逐策略 - ainimore - 博客园

$
0
0

容器的资源需求和资源限制

requests:需求,最低保障, 保证被调度的节点上至少有的资源配额
limits:限制,硬限制, 容器可以分配到的最大资源配额

QoS Classes分类

如果Pod中所有Container的所有Resource的limit和request都相等且不为0,则这个Pod的QoS Class就是Guaranteed。
注意,如果一个容器只指明了limit,而未指明request,则表明request的值等于limit的值。

Burstable

至少有一个容器设置CPU或内存资源的requests属性
Best-Effort

如果Pod中所有容器的所有Resource的request和limit都没有赋值,则这个Pod的QoS Class就是Best-Effort.

Qos Class优先级排名

Guaranteed > Burstable > Best-Effort
可压缩资源与不可压缩资源

Pod 使用的资源最重要的是 CPU、内存和磁盘 IO,这些资源可以被分为可压缩资源(CPU)和不可压缩资源(内存,磁盘 IO)。

可压缩资源(CPU)不会导致pod被驱逐

因为当 Pod 的 CPU 使用量很多时,系统可以通过重新分配权重来限制 Pod 的 CPU 使用

不可压缩资源(内存)则会导致pod被驱逐

于不可压缩资源来说,如果资源不足,也就无法继续申请资源(内存用完就是用完了),此时 Kubernetes 会从该节点上驱逐一定数量的 Pod,以保证该节点上有充足的资源。

那么,Kubernetes 为 Pod 设置这样三种 QoS 类别,具体有什么作用呢?实际上,QoS 划分的主要应用场景,是当宿主机资源紧张的时候,kubelet 对 Pod 进行 Eviction(即资源回收)时需要用到的。具体地说,当 Kubernetes 所管理的宿主机上不可压缩资源短缺时,就有可能触发 Eviction。比如,可用内存(memory.available)、可用的宿主机磁盘空间(nodefs.available),以及容器运行时镜像存储空间(imagefs.available)等等。
目前,Kubernetes 为你设置的 Eviction 的默认阈值如下所示:

memory.available<100Mi
nodefs.available<10%
nodefs.inodesFree<5%
imagefs.available<15%

当然,上述各个触发条件在 kubelet 里都是可配置的。比如下面这个例子:

kubelet --eviction-hard=imagefs.available<10%,memory.available<500Mi,nodefs.available<5%,nodefs.inodesFree<5% --eviction-soft=imagefs.available<30%,nodefs.available<10% --eviction-soft-grace-period=imagefs.available=2m,nodefs.available=2m --eviction-max-pod-grace-period=600

在这个配置中,你可以看到 Eviction 在 Kubernetes 里其实分为 Soft 和 Hard 两种模式。
其中,Soft Eviction 允许你为 Eviction 过程设置一段“优雅时间”,比如上面例子里的 imagefs.available=2m,就意味着当 imagefs 不足的阈值达到 2 分钟之后,kubelet 才会开始 Eviction 的过程。
而 Hard Eviction 模式下,Eviction 过程就会在阈值达到之后立刻开始。Kubernetes 计算 Eviction 阈值的数据来源,主要依赖于从 Cgroups 读取到的值,以及使用 cAdvisor 监控到的数据。当宿主机的 Eviction 阈值达到后,就会进入 MemoryPressure 或者 DiskPressure 状态,从而避免新的 Pod 被调度到这台宿主机上。而当 Eviction 发生的时候,kubelet 具体会挑选哪些 Pod 进行删除操作,就需要参考这些 Pod 的 QoS 类别了。首当其冲的,自然是 BestEffort 类别的 Pod。其次,是属于 Burstable 类别、并且发生“饥饿”的资源使用量已经超出了 requests 的 Pod。最后,才是 Guaranteed 类别。并且,Kubernetes 会保证只有当 Guaranteed 类别的 Pod 的资源使用量超过了其 limits 的限制,或者宿主机本身正处于 Memory Pressure 状态时,Guaranteed 的 Pod 才可能被选中进行 Eviction 操作。当然,对于同 QoS 类别的 Pod 来说,Kubernetes 还会根据 Pod 的优先级来进行进一步地排序和选择。

SaaS成功,需要四种运营服务_阿朱=行业趋势+开发管理+架构-CSDN博客

$
0
0

2015年,我做过一次外部演讲,我讲的内容是:SaaS不仅需要运维,更需要运营。

一提起运营,互联网人想到的是:拉新(下载/注册)、留存(内容运营)、活跃(社区运营)。

一提起SaaS运营,SaaS厂商就想到了客户成功运营,想到了客户运营老做的几个事:用户开通/用户使用情况监控、培训学习社区运营、支持社区运营。

但是我想说的是,SaaS运营,一共是四个运营:

1、客户成功使用IT系统的运营

2、促进客户业务成功的运营

3、数据服务运营

4、智能服务运营

客户成功就是客户成功使用IT系统的运营,咱们刚才已经讲过,不赘述。当然也有很多人讲:客户成功不应该狭义指客户使用IT系统成功,应该指客户业务真正成功。

那好,如果你真要促进客户业务成功,你必须做下面三种运营服务。

促进客户业务成功的运营,我就列举两个例子:一个是数据驱动的精准广告投放服务,一个是数据驱动的供应商寻源服务。这就有业务+IT一体化商业模式的意味了。不过大家也别着急害怕自己会去找一堆业务运营的人,因为咱们还有后续的数据运营服务和智能运营服务。当然,如果你数据运营服务和智能运营服务跟不上,那你只好招一大堆业务BD和业务运营人了,最终会拖垮你整个企业。

第三个运营我讲的是数据运营服务。其实我要讲的数据服务运营,和促进客户业务成功具有密切关系,如产业主数据服务(企业/商品/客户/BOM、画像标签/关系图谱),如客户评价聆听、企业舆情服务、企业信用认证服务、企业测评服务、客户调研服务....

最后我讲的智能运营服务,也是和促进客户业务成功具有密切关系,如客户识别、精准搜索、关联推荐、智能采购、智能选址、智能选品、智能定价、智能促销、智能仓储规划、智能物流路径、智能客服....。想做到这些,就必须要汇集社会大数据,而不能就某个企业一家的数据。而且人工智能算法得天天根据数据来进行持续的训练与调参才能越变越智能。

业务运营服务、数据运营服务、智能运营服务,都必须公有云多租户在线的。一旦专属化私有化了就没法发挥价值了。

而且即使如客户成功运营,也大多是在线监控、在线学习和在线支持。

很多人说客户看不到SaaS的好处、客户要把SaaS当成软件部署到自己的专属云或私有云中。我想说:你可能是没开展其他三种运营服务,你也没有建设那三种运营服务的部门组织和人员专业能力。


百亿数据,毫秒级返回,如何设计?--浅谈实时索引构建之道 - ErnestEvan - 博客园

$
0
0

本文已整理致我的 github 地址 https://github.com/allentofight/easy-cs,欢迎大家 star 支持一下

前言

近年来公司业务迅猛发展,数据量爆炸式增长,随之而来的的是海量数据查询等带来的挑战,我们需要数据量在十亿,甚至百亿级别的规模时依然能以秒级甚至毫秒级的速度返回,这样的话显然离不开搜索引擎的帮助,在搜索引擎中,ES(ElasticSearch)毫无疑问是其中的佼佼者,连续多年在 DBRanking 的搜索引擎中评测中排名第一,也是绝大多数大公司的首选,那么它与传统的 DB 如 MySQL 相比有啥优势呢,ES 的数据又是如何生成的,数据达到 PB 时又是如何保证 ES 索引数据的实时性以更好地满足业务的需求的呢。

本文会结合我司在 ES 上的实践经验与大家谈谈如何构建准实时索引的一些思路,希望对大家有所启发。本文目录如下

  1. 为什么要用搜索引擎,传统 DB 如 MySQL 不香吗
    • MySQL 的不足
    • ES 简介
  2. ES 索引数据构建
  3. PB 级的 ES 准实时索引数据构建之道

为什么要用搜索引擎,传统 DB 如 MySQL 不香吗

MySQL 的不足

MySQL 架构天生不适合海量数据查询,它只适合海量数据存储,但无法应对海量数据下各种复杂条件的查询,有人说加索引不是可以避免全表扫描,提升查询速度吗,为啥说它不适合海量数据查询呢,有两个原因:

1、加索引确实可以提升查询速度,但在 MySQL 中加多个索引最终在执行 SQL 的时候它只会选择成本最低的那个索引,如果没有索引满足搜索条件,就会触发全表扫描,而且即便你使用了组合索引,也要符合最左前缀原则才能命中索引,但在海量数据多种查询条件下很有可能不符合最左前缀原则而导致索引失效,而且我们知道存储都是需要成本的,如果你针对每一种情况都加索引,以 innoDB 为例,每加一个索引,就会创建一颗 B+ 树,如果是海量数据,将会增加很大的存储成本,之前就有人反馈说他们公司的某个表实际内容的大小才 10G, 而索引大小却有 30G!这是多么巨大的成本!所以千万不要觉得索引建得越多越好。

2、有些查询条件是 MySQL 加索引都解决不了的,比如我要查询商品中所有 title 带有「格力空调」的关键词,如果你用 MySQL 写,会写出如下代码

SELECT * FROM product WHERE title like '%格力空调%'        

这样的话无法命中任何索引,会触发全表扫描,而且你不能指望所有人都能输对他想要的商品,是人就会犯错误,我们经常会犯类似把「格力空调」记成「格空间」的错误,那么 SQL 语句就会变成:

SELECT * FROM product WHERE title like '%格空调%'        

这种情况下就算你触发了全表扫描也无法查询到任何商品,综上所述,MySQL 的查询确实能力有限。

ES 简介

与其说上面列的这些点是 MySQL 的不足,倒不如说 MySQL 本身就不是为海量数据查询而设计的,术业有专攻,海量数据查询还得用专门的搜索引擎,这其中 ES 是其中当之无愧的王者,它是基于 Lucene 引擎构建的开源分布式搜索分析引擎,可以提供针对 PB 数据的近实时查询,广泛用在全文检索、日志分析、监控分析等场景。

它主要有以下三个特点:

  • 轻松支持各种复杂的查询条件: 它是分布式实时文件存储,会把 每一个字段都编入索引(倒排索引),利用高效的倒排索引,以及自定义打分、排序能力与丰富的分词插件等,能实现任意复杂查询条件下的全文检索需求
  • 可扩展性强:天然支持分布式存储,通过极其简单的配置实现几百上千台服务器的分布式横向扩容,轻松处理 PB 级别的结构化或非结构化数据。
  • 高可用,容灾性能好:通过使用主备节点,以及故障的自动探活与恢复,有力地保障了高可用

我们先用与 MySQL 类比的形式来理解 ES 的一些重要概念

通过类比的形式不难看出 ES 的以下几个概念
1、MySQL 的数据库(DataBase)相当于 Index(索引),数据的逻辑集合,ES 的工作主要也是创建索引,查询索引。
2、一个数据库里会有多个表,同样的一个 Index 也会有多个 type
3、一个表会有多行(Row),同样的一个 Type 也会有多个 Document。
4、Schema 指定表名,表字段,是否建立索引等,同样的 Mapping 也指定了 Type 字段的处理规则,即索引如何建立,是否分词,分词规则等
5、在 MySQL 中索引是需要手动创建的,而在 ES 一切字段皆可被索引,只要在 Mapping 在指定即可

那么 ES 中的索引为何如此高效,能在海量数据下达到秒级的效果呢?它采用了多种优化手段,最主要的原因是它采用了一种叫做 倒排索引的方式来生成索引,避免了全文档扫描,那么什么是倒排索引呢,通过文档来查找关键词等数据的我们称为正排索引,返之,通过关键词来查找文档的形式我们称之为倒排索引,假设有以下三个文档(Document)

要在其中找到含有 comming 的文档,如果要正排索引,那么要把每个文档的内容拿出来查找是否有此单词,毫无疑问这样的话会导致全表扫描,那么用倒排索引会怎么查找呢,它首先会将每个文档内容进行分词,小写化等,然后建立每个分词与包含有此分词的文档之前的映射关系,如果有多个文档包含此分词,那么就会按重要程度即文档的权重(通常是用 TF-IDF 给文档打分)将文档进行排序,于是我们可以得到如下关系

这样的话我们我要查找所有带有 comming 的文档,就只需查一次,而且这种情况下查询多个单词性能也是很好的,只要查询多个条件对应的文档列表,再取交集即可,极大地提升了查询效率。

画外音:这里简化了一些流程,实际上还要先根据单词表来定位单词等,不过这些流程都很快,可以忽略,有兴趣的读者可以查阅相关资料了解。

除了倒排索引外,ES 的分布式架构也天然适合海量数据查询,来看下 ES 的架构

一个 ES 集群由多个 node 节点组成,每个 index 是以分片(Shard,index 子集)的数据存在于多个 node 节点上的,这样的话当一个查询请求进来,分别在各个 node 查询相应的结果并整合后即可,将查询压力分散到多个节点,避免了单个节点 CPU,磁盘,内存等处理能力的不足。

另外当新节点加入后,会 自动迁移部分分片至新节点,实现负载均衡,这个功能是 ES 自动完成的,对比一个下 MySQL 的分库分表需要开发人员引入 Mycat 等中间件并指定分库分表规则等繁琐的流程是不是一个巨大的进步?这也就意味着 ES 有非常强大的水平扩展的能力,集群可轻松扩展致几百上千个节点,轻松支持 PB 级的数据查询。

当然 ES 的强大不止于此,它还采用了比如主备分片提升搜索吞吐率,使用节点故障探测,Raft 选主机制等提升了容灾能力等等,这些不是本文重点,读者可自行查阅,总之经过上面的简单总结大家只需要明白一点: ES 的分布式架构设计天生支持海量数据查询

那么 ES 的索引数据(index)是如何生成的呢,接下来我们一起来看看本文的重点

如何构建 ES 索引

要构建 ES 索引数据,首先得有数据源,一般我们会使用 MySQL 作为数据源,你可以直接从 MySQL 中取数据然后再写入 ES,但这种方式由于直接调用了线上的数据库查询,可能会对线上业务造成影响,比如考虑这样的一个场景:

在电商 APP 里用的最多的业务场景想必是用户输入关键词来查询相对应的商品了,那么商品会有哪些信息呢,一个商品会有多个 sku(sku 即同一个商品下不同规格的品类,比如苹果手机有 iPhone 6, iPhone 6s等),会有其基本属性如价格,标题等,商品会有分类(居家,服饰等),品牌,库存等,为了保证表设计的合理性,我们会设计几张表来存储这些属性,假设有 product_sku(sku 表), product_property(基本属性表),sku_stock(库存表),product_category(分类表)这几张表,那么为了在商品展示列表中展示所有这些信息,就必须把这些表进行 join,然后再写入 ES,这样查询的时候就会在 ES 中获取所有的商品信息了。

这种方案由于直接在 MySQL 中执行 join 操作,在商品达到千万级时会对线上的 DB 服务产生极大的性能影响,所以显然不可行,那该怎么生成中间表呢,既然直接在 MySQL 中操作不可行,能否把 MySQL 中的数据同步到另一个地方再做生成中间表的操作呢,也就是加一个中间层进行处理,这样就避免了对线上 DB 的直接操作,说到这相信大家又会对计算机界的名言有进一步的体会:没有什么是加一个中间层不能解决的,如果有,那就再加一层。

这个中间层就是 hive

什么是 hive

hive 是基于 Hadoop 的一个数据仓库工具,用来进行数据提取、转化、加载,这是一种可以存储、查询和分析存储在 Hadoop 中的 大规模数据的机制,它的意义就是把好写的 hive 的 sql 转换为复杂难写的 map-reduce 程序(map-reduce 是专门用于用于大规模数据集(大于1TB)的并行运算编程模型),也就是说 如果数据量大的话通过把 MySQL 的数据同步到 hive,再由 hive 来生成上述的 product_tmp 中间表,能极大的提升性能。hive 生成临时表存储在 hbase(一个分布式的、面向列的开源数据库) 中,生成后会定时触发 dump task 调用索引程序,然后索引程序主要从 hbase 中读入全量数据,进行业务数据处理,并刷新到 es 索引中,整个流程如下

这样构建索引看似很美好,但我们需要知道的是首先 hive 执行 join 任务是非常耗时的,在我们的生产场景上,由于数据量高达几千万,执行 join 任务通常需要几十分钟,从执行 join 任务到最终更新至 ES 整个流程常常需要至少半小时以上,如果这期间商品的价格,库存,上线状态(如被下架)等重要字段发生了变更,索引是无法更新的,这样会对用户体验产生极大影响,优化前我们经常会看到通过 ES 搜索出的中有状态是上线但实际是下架的产品,严重影响用户体验,那么怎么解决呢,有一种可行的方案: 建立宽表

既然我们发现 hive join 是性能的主要瓶颈,那么能否规避掉这个流程呢,能否在 MySQL 中将 product_sku,product_property,sku_stock 等表组合成一个大表(我们称其为宽表)

这样在每一行中商品涉及到的的数据都有了,所以将 MySQL 同步到 hive 后,hive 就不需要再执行耗时的 join 操作了,极大的提升了整体的处理时间,从 hive 同步 MySQL 再到 dump 到 ES 索引中从原来的半小时以上降低到了几分钟以内,看起来确实不错,但几分钟的索引延迟依然是无法接受的。

为什么 hive 无法做到实时导入索引

因为 hive 构建在基于静态批处理的Hadoop 之上,Hadoop 通常都有较高的延迟并且在作业提交和调度的时候需要大量的开销。因此,hive 并不能够在大规模数据集上实现低延迟快速的查询等操作,再且千万级别的数据全量从索引程序导入到 ES 集群至少也是分钟级。

另外引入了宽表,它的维护也成了一个新问题,设想 sku 库存变了,产品下架了,价格调整了,那么除了修改原表(sku_stock,product_categry 等表)记录之外,还要将所有原表变更到的记录对应到宽表中的所有记录也都更新一遍,这对代码的维护是个噩梦,因为你需要在所有商品相关的表变更的地方紧接着着变更宽表的逻辑,与宽表的变更逻辑变更紧藕合!

PB 级的 ES 准实时索引构建之道

该如何解决呢?仔细观察上面两个问题,其实都是同一个问题,如果我们能实时监听到 db 的字段变更,再将变更的内容实时同步到 ES 和宽表中不就解决了我们的问题了。

怎么才能实时监听到表字段的变更呢?

答案:binlog

来一起复习下 MySQL 的主从同步原理

  1. MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
  2. MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
  3. MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

可以看到主从复制的原理关键是 Master 和 Slave 遵循了一套协议才能实时监听 binlog 日志来更新 slave 的表数据,那我们能不能也开发一个遵循这套协议的组件,当组件作为 Slave 来获取 binlog 日志进而实时监听表字段变更呢?阿里的开源项目 Canal 就是这个干的,它的工作原理如下:

  • canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
  • MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
  • canal 解析 binary log 对象(原始为 byte 流)

这样的话通过 canal 就能获取 binlog 日志了,当然 canal 只是获取接收了 master 过来的 binlog,还要对 binlog 进行解析过滤处理等,另外如果我们只对某些表的字段感兴趣,该如何配置,获取到 binlog 后要传给谁? 这些都需要一个统一的管理组件,阿里的 otter 就是干这件事的。

什么是 otter

Otter 是由阿里提供的基于数据库增量日志解析,准实时同步到本机房或异地机房 MySQL 数据库的一个分布式数据库同步系统,它的整体架构如下:


注:以上是我司根据 otter 改造后的业务架构,与原版 otter 稍有不同,不过大同小异

主要工作流程如下

  1. 在 Manager 配置好 zk,要监听的表 ,负责监听表的节点,然后将配置同步到 Nodes 中
  2. node 启动后则其 canal 会监听 binlog,然后经过 S(select),E(extract),T(transform),L(load) 四个阶段后数据发送到 MQ
  3. 然后业务就可以订阅 MQ 消息来做相关的逻辑处理了

画外音:zookeeper 主要协调节点间的工作,如在跨机房数据同步时,可能要从 A 机房的节点将数据同步到 B 机房的节点,要用 zookeeper 来协调,

大家应该注意到了node 中有 S,E,T,L 四个阶段,它们的主要作用如下

  • Select 阶段: 为解决数据来源的差异性,比如接入 canal 获取增量数据,也可以接入其他系统获取其他数据等。

  • Extract阶段: 组装数据,针对多种数据来源,mysql,oracle,store,file等,进行数据组装和过滤。

  • Transform 阶段: 数据提取转换过程,把数据转换成目标数据源要求的类型

  • Load 阶段: 数据载入,把数据载入到目标端,如写入迁移后的数据库, MQ,ES 等

以上这套基于阿里 otter 改造后的数据服务我们将它称为 DTS(Data Transfer Service),即数据传输服务。

搭建这套服务后我们就可以通过订阅 MQ 来实时写入 ES 让索引实时更新了,而且也可以通过订阅 MQ 来实现宽表字段的更新,实现了上文中所说的宽表字段更新与原表紧藕合的逻辑,基于 DTS 服务的索引改进架构如下:

注意:「 build 数据」这一模块对实时索引更新是透明的,这个模块主要用在更新或插入 MySQL 宽表,因为对于宽表来说,它是几个表数据的并集,所以并不是监听到哪个字段变更就更新哪个,它要把所有商品涉及到的所有表数据拉回来再更新到宽表中。

于是,通过 MySQL 宽表全量更新+基于 DTS 的实时索引更新我们很好地解决了索引延迟的问题,能达到秒级的 ES 索引更新!

这里有几个问题可能大家比较关心,我简单列一下

需要订阅哪些字段

对于 MySQL 宽表来说由于它要保存商品的完整信息,所以它需要订阅所有字段,但是对于红框中的实时索引更新而言,它只需要订阅库存,价格等字段,因为这些字段如果不及时更新,会对销量产生极大的影响,所以我们实时索引只关注这些敏感字段即可。

有了实时索引更新,还需要全量索引更新吗

需要,主要有两个原因:

  • 实时更新依赖消息机制,无法百分百保证数据完整性,需要全量更新来支持,这种情况很少,而且消息积压等会有告警,所以我们一天只会执行一次全量索引更新
  • 索引集群异常或崩溃后能快速重建索引

全量索引更新的数据会覆盖实时索引吗

会,设想这样一种场景,你在某一时刻触发了实时索引,然后此时全量索引还在执行中,还未执行到实时索引更新的那条记录,这样在的话当全量索引执行完之后就会把之前实时索引更新的数据给覆盖掉,针对这种情况一种可行的处理方式是如果全量索引是在构建中,实时索引更新消息可以延迟处理,等全量更新结束后再消费。也正因为这个原因,全量索引我们一般会在凌晨执行,由于是业务低峰期,最大可能规避了此类问题。

总结

本文简单总结了我司在 PB 级数据下构建实时 ES 索引的一些思路,希望对大家有所帮助,文章只是简单提到了 ES,canal,otter 等阿里中间件的应用,但未对这些中间件的详细配置,原理等未作过多介绍,这些中间件的设计非常值得我们好好研究下,比如 ES 为了提高搜索效率、优化存储空间做了很多工作,再比如 canal 如何做高可用,otter 实现异地跨机房同步的原理等,建议感兴趣的读者可以之后好好研究一番,相信你会受益匪浅。

如果大家对方案有些疑问,欢迎大家积极留言探讨,共同进步^_^,更多精品文章,欢迎大家扫码关注「码海」

巨人的肩膀

  • Elasticsearch简介及与MySQL查询原理对比:https://www.jianshu.com/p/116cdf5836f2
  • https://www.cnblogs.com/zhjh256/p/9261725.html
  • otter安装之otter-node安装(单机多节点安装):https://blog.csdn.net/u014642915/article/details/96500957
  • MySQL和Lucene索引对比分析: https://developer.aliyun.com/article/50481
  • 10 分钟快速入门海量数据搜索分析引擎 Elasticearch: https://www.modb.pro/db/29806
  • ElasticSearch和Mysql查询原理分析与对比:https://www.pianshen.com/article/4254917942/
  • 带你走进神一样的Elasticsearch索引机制:https://zhuanlan.zhihu.com/p/137574234

Spring Cloud之Ribbon转发请求头(header参数)_justlpf的专栏-CSDN博客

$
0
0

目录

简介

1.自定义RestTemplate

2.将MyRestTemplate注册为Bean

3.构建spring拦截器

4.构建拦截器配置类

5.controller调用

简介

使用spring cloud的ribbon组件可以实现对下游微服务的负载均衡调度,但是官方ribbon是没有header转发功能的,这里我们在ribbon的restTemplate基础上,自定义实现header的转发功能。

1.自定义RestTemplate

package com.cloud.base.config.ribbon;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.client.*;

import java.io.IOException;
import java.net.URI;
import java.util.*;

import static feign.Util.checkNotNull;

/**
 * @author langpf 2019/2/19
 */
public class MyRestTemplate extends RestTemplate {
    private final Map<String, List<String>> headers = new LinkedHashMap<>();

    @Nullable
    @Override
    protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
                              @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

        Assert.notNull(url, "URI is required");
        Assert.notNull(method, "HttpMethod is required");
        ClientHttpResponse response = null;
        try {
            ClientHttpRequest request = createRequest(url, method);

            // lpf: 添加headers
            HttpHeaders myHeaders = request.getHeaders();
            addAllHeaders(myHeaders);

            if (requestCallback != null) {
                requestCallback.doWithRequest(request);
            }
            response = request.execute();
            handleResponse(url, method, response);
            return (responseExtractor != null ? responseExtractor.extractData(response) : null);
        }
        catch (IOException ex) {
            String resource = url.toString();
            String query = url.getRawQuery();
            resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
            throw new ResourceAccessException("I/O error on " + method.name()
                    + " request for \"" + resource + "\": " + ex.getMessage(), ex);
        }
        finally {
            if (response != null) {
                response.close();
            }
        }
    }

    public void header(String name, String... values) {
        checkNotNull(name, "header name");
        if (values == null || (values.length == 1 && values[0] == null)) {
            headers.remove(name);
        } else {
            List<String> headers = new ArrayList<>(Arrays.asList(values));
            this.headers.put(name, headers);
        }
    }

    private void addAllHeaders(HttpHeaders myHeaders) {
        for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
            String key = entry.getKey();
            List<String> values = entry.getValue();
            myHeaders.addAll(key, values);
        }
    }
}

2.将MyRestTemplate注册为Bean

下面代码一般放在SpringBoot工程的启动类中。

/**
 * 让restTemplate具备Ribbon负载均衡的能力。
 * 由于使用feign, 弃用该方式
 */
@Bean(name="myRestTemplate")
@LoadBalanced
MyRestTemplate restTemplate() {
	return new MyRestTemplate();
}

3.构建spring拦截器

package com.cloud.base.interceptor.ribbon;

import com.alibaba.fastjson.JSONObject;
import com.cloud.base.config.ribbon.MyRestTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

// @Component("ribbonInterceptor1")
public class RibbonInterceptor implements HandlerInterceptor {
    private static final Logger log = LoggerFactory.getLogger(RibbonInterceptor.class);

    private MyRestTemplate myRestTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                             Object handler) throws Exception {
        String lpfId = request.getHeader("lpf");

        if (myRestTemplate == null) {  //解决service为null无法注入问题
            BeanFactory factory = 
              WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
            myRestTemplate = (MyRestTemplate) factory.getBean("myRestTemplate");
        }

        myRestTemplate.header("lpf", lpfId);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        // TODO Auto-generated method stub;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                              Object handler, Exception ex)
            throws Exception {
        // TODO Auto-generated method stub;
    }
}

4.构建拦截器配置类

package com.cloud.base.config;

import com.cloud.base.interceptor.ribbon.RibbonInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器配置类
 */
@Configuration
public class WebMvcSessionConfigurer implements WebMvcConfigurer {
    // @Autowired
    // RibbonInterceptor ribbonInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 多个拦截器组成一个拦截器链
        // addPathPatterns 用于添加拦截规则
        // excludePathPatterns 用于排除拦截
        // old: new RedisSessionInterceptor()
        // registry.addInterceptor(ribbonInterceptor).addPathPatterns("/**");
        registry.addInterceptor(new RibbonInterceptor()).addPathPatterns("/**");
    }
}

5.controller调用

通过controller调用ribbon时会自动转发header

package com.cloud.controller;

import com.cloud.base.config.ribbon.MyRestTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

import static com.cloud.base.constant.Constants.SPRING_DATA_SRV;
import static com.cloud.base.constant.Constants.SPRING_SIDECAR_SRV_URL;

/**
 * ribbon调用方式, 参考博客: https://www.jianshu.com/p/470a30f493cf
 * @author langpf 2019/2/19
 */
@RestController
public class RibbonController {
    @Autowired
    MyRestTemplate myRestTemplate;

    /**
     * ribbon -- get请求
     */
    @RequestMapping("ribbonDataSrvHi")
    public String ribbonDataSrvHi() {
        // myRestTemplate.headForHeaders()
        return myRestTemplate.getForEntity(SPRING_DATA_SRV + "hi", String.class).getBody();
    }

    /**
     * ribbon -- get请求
     */
    @RequestMapping("ribbonGetHi")
    public String ribbonGetHi() {
        return myRestTemplate.getForEntity(SPRING_DATA_SRV + "hi", String.class).getBody();
    }

    /**
     * ribbon -- get请求
     * http://localhost:10232/python-user
     */
    @RequestMapping("python-user")
    public String pythonUser() {
        // return myRestTemplate.getForEntity("http://spring-sidecar-python-server/getUser", String.class).getBody();
        return myRestTemplate.getForEntity(SPRING_SIDECAR_SRV_URL + "getUser", String.class).getBody();
    }

    /**
     * ribbon -- post请求
     */
    @RequestMapping(value = "model/serving", method = RequestMethod.POST)
    public String modelServing(@RequestBody HttpEntity entity) {
        return myRestTemplate.postForEntity(SPRING_SIDECAR_SRV_URL + "model/serving", entity, String.class).getBody();
    }

}

 

分享一次排查CLOSE_WAIT过多的经验 - 踩刀诗人 - 博客园

$
0
0

关键词:TCP、CLOSE_WAIT

 

问题背景

某日下午有测试人员急匆匆的跑来跟我反馈:“有客户反馈供应商附件预览不了,流程阻塞,需要紧急处理”,我立马精神起来,毕竟都是付费客户 目前做B端业务,客户都是付费用户,不像C端,出了问题发个道歉声明也就过去了)

 

等她说完,我第一时间用测试账号登上去验证,浏览器一直在转圈,差不多一分钟以后出了nginx的504错误页。

 

 

 

初步排查

也许有人对504这个错误码不熟悉,这里借用百度百科的内容简单介绍下这个错误码。

 

504 Gateway Timeout

作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应。

                                                                                                             ( 内容来源于百度百科)

 

看到这个错误码第一反应就是下游服务太慢,导致nginx请求超时了,这里的下游服务是自研的附件预览服务,采用SpringBoot开发,整体的请求链路如下图所示:

 

 

在线预览的处理流程如下:

1.用户在业务方站点发起预览请求;

2.业务方拼接相关参数重定向到预览服务;

3.预览服务通过业务方传递的附件链接调用业务方接口下载附件到本地;

4.预览服务将下载的附件转换成html供用户在线预览;

 

结合上面的处理流程和请求链路,初步得到以下两点猜测:

1.预览服务调用业务方下载接口过慢;

2.预览服务本身处理过慢;

 

带着这两点猜测我立马去查看日志,结果却大跌眼镜,从昨天14点以后就没有日志输出了。

请求进不来了?假死?挂了?

我首先确认进程是否存在,进程跑的好好的,为什么会没有请求呢,我第一反应是业务代码有问题,导致线程被hang占满了tomcat的线程池,所以立即使用jstack查看线程情况,意外的是一切正常,线程数不多,更没有发现死锁、socket read0等可能导致线程hang住的情况。

 

自身没问题,难道是被其他任务影响了,我继续使用top查看系统负载、cpu占用等情况

 

 显而易见,负载、cpu占用都不高,似乎陷入了僵局。

 

 我猜测可能不是业务代码的问题,需要跳出业务代码去查问题,既然没有请求,那就先从网络开始查起。

 

渐入佳境

先确认下服务端监听端口是不是正常。

第一步:netstat 查看网络状态

netstat -aonp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp       81      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      1/java               off (0.00/0/0)
tcp        0      0 127.0.0.1:8100          0.0.0.0:*               LISTEN      24/soffice.bin       off (0.00/0/0)
tcp      936      0 172.20.4.203:8080       172.20.5.59:40576       CLOSE_WAIT  -                    off (0.00/0/0)
tcp      867      0 172.20.4.203:8080       172.20.4.172:57166      CLOSE_WAIT  -                    off (0.00/0/0)
tcp      964      0 172.20.4.203:8080       172.20.5.59:50106       CLOSE_WAIT  -                    off (0.00/0/0)
tcp     1701      0 172.20.4.203:8080       172.20.4.172:45428      CLOSE_WAIT  -                    off (0.00/0/0)
tcp     1169      0 172.20.4.203:8080       172.20.4.172:61582      CLOSE_WAIT  -                    off (0.00/0/0)
tcp      963      0 172.20.4.203:8080       172.20.4.172:64474      CLOSE_WAIT  -                    off (0.00/0/0)
tcp     1058      0 172.20.4.203:8080       172.20.5.59:44564       CLOSE_WAIT  -                    off (0.00/0/0)
tcp      962      0 172.20.4.203:8080       172.20.4.172:64160      CLOSE_WAIT  -                    off (0.00/0/0)
tcp     1733      0 172.20.4.203:8080       172.20.4.172:46810      CLOSE_WAIT  -                    off (0.00/0/0)
tcp     1587      0 172.20.4.203:8080       172.20.5.59:40032       CLOSE_WAIT  -                    off (0.00/0/0)
tcp      994      0 172.20.4.203:8080       172.20.4.172:47474      CLOSE_WAIT  -                    off (0.00/0/0)
tcp      867      0 172.20.4.203:8080       172.20.5.59:47134       CLOSE_WAIT  -                    off (0.00/0/0)
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
CLOSE_WAIT 103
ESTABLISHED 2

  

从输出结果中可以确认预览服务的监听端口(8080)虽然是存活的,但有大量的CLOSE_WAIT出现,这显然是不正常的,难道是CLOSE_WAIT过多导致超过了文件描述符的限制,但是我在日志中没有找到“Too manay open files”,这个猜想随之破灭,有没有可能是tcp队列溢出了?

  linux中一切皆文件,ServerSocket每次accept一个socket意味着需要开启一个文件描述符,这个数量并不是无限的,系统中有限制,如果超过限制了就会报Too manay open files。

第二步:查看tcp队列是否溢出

netstat -s | egrep "listen|LISTEN" 
    243 times the listen queue of a socket overflowed
    243 SYNs to LISTEN sockets dropped

  

上面看到的 243 times ,表示全连接队列溢出的次数,隔一阵刷新一下,发现这个数字还在涨。

既然tcp队列有溢出,自然是有部分请求到不了预览服务了,在tcp层就被扔了,但是从昨天14点到现在一点日志也没有,难道都被扔了,继续确认当前tcp队列的情况。

关于tcp队列的知识,这里推荐去阅读淘宝技术团队写的一篇文章,通俗易懂。http://jm.taobao.org/2017/05/25/525-1/

 

第三步:查看tcp队列当前情况

ss -lnt
State       Recv-Q Send-Q                                                                                           Local Address:Port                                                                                                          Peer Address:Port              
LISTEN      101    100

  

Recv-Q代表当前全连接队列的大小,也就是三次握手完成,目前在全连接队列中等待被应用程序accept的socket个数。

Send-Q代表全连接队列的最大值,应用程序可以在创建ServerSocket的时候指定,tomcat默认为100,但是这个值不能超过系统的/proc/sys/net/core/somaxconn,看看jdk中关于这个值的解释,专业名词叫backlog。

The maximum queue length for incoming connection indications (a
   request to connect) is set to the {@code backlog} parameter. If
   a connection indication arrives when the queue is full, the
     connection is refused.
 public ServerSocket(int port, int backlog) throws IOException {
        this(port, backlog, null);
 }

  

从上面的输出可以发现Recv-Q已经大于Send-Q,而且这个数量长时间不变,可以得出两个结论:

1.部分socket一直堆积在队列中没有被accept;

2.由于tcp全连接队列已满,所以新的socket自然是进不来了。

至此可以初步解释为什么从昨天14点开始就一直没有请求进来了。

 

深入分析

截止现在可以确定是由于tcp队列满导致一直没有请求进来,但tcp队列怎么能从昨天14点一直满到现在呢,jstack查看当前线程并没有什么异常、top查看系统负载、cpu都不高,是什么让tomcat不去tcp队列中accept新的socket呢?

 

另一方面,通过tcp队列满这个现象也可以分析出为什么那么多的CLOSE_WAIT,因为socket在tcp的队列中一直堆积着,还没等到应用程序处理呢,就达到了nginx的超时时间,nginx主动关闭了连接,这里贴一张经典的四次握手的图。

 

 左边的Initiator(发起者)就是nginx,右边的Receiver(接受者)就是tomcat,nginx和tomcat通过三次握手已经建立了tcp连接,此时连接暂存在全连接队列中,等待着tomcat去accept,但是tomcat迟迟不accept,一分钟过后,nginx等不住了,主动发起了FIN开始关闭连接,此时tomcat侧的tcp连接就处在CLOSE_WAIT状态,理论上来讲,tomcat收到nginx的FIN接着就应该close socket,CLOSE_WAIT状态不会持续太久,难道是tomcat出bug了,没有响应?

 

截止现在有两个疑问:

1.为什么tomcat不去tcp队列中accept新的socket呢?

2.为什么tomcat不响应nginx的关闭socket请求呢?

 

我们先看第一个疑问 “为什么tomcat不去tcp队列中accept新的socket呢?”

要揭开这个疑问我们先看一张图来感受下TCP握手过程中建连接的流程和队列

 

 ( 图片来源于https://tech.ebayinc.com/engineering/a-vip-connection-timeout-issue-caused-by-snat-and-tcp-tw-recycle/)

 

接下来的任务就是搞清楚tomcat是如何处理上图中的accept逻辑的,我之前看过一部分tomcat源码,所以这里直接抛个答案出来吧,就不延伸了,tomcat是通过一个单独的Acceptor线程去accept socket的,accept之后扔给IO多路复用模块进行后续的业务处理,在这里只需要关注处理accept那段逻辑就好,贴一下源码:

protected class Acceptor extends AbstractEndpoint.Acceptor {

        @Override
        public void run() {

            int errorDelay = 0;

            // Loop until we receive a shutdown command
            while (running) {

                // Loop if endpoint is paused
                while (paused && running) {
                    state = AcceptorState.PAUSED;
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }

                if (!running) {
                    break;
                }
                state = AcceptorState.RUNNING;

                try {
                    //if we have reached max connections, wait
                    countUpOrAwaitConnection();

                    SocketChannel socket = null;
                    try {
                        // Accept the next incoming connection from the server
                        // socket
                        socket = serverSock.accept();
                    } catch (IOException ioe) {
                        // We didn't get a socket
                        countDownConnection();
                        if (running) {
                            // Introduce delay if necessary
                            errorDelay = handleExceptionWithDelay(errorDelay);
                            // re-throw
                            throw ioe;
                        } else {
                            break;
                        }
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;
                    // Configure the socket
                    if (running && !paused) {
                        // setSocketOptions() will hand the socket off to
                        // an appropriate processor if successful
                        if (!setSocketOptions(socket)) {
                            closeSocket(socket);
                        }
                    } else {
                        closeSocket(socket);
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
            }
            state = AcceptorState.ENDED;
        }

  

逻辑比较简单,就是一直从ServerSocket中accept socket然后扔给IO多路复用模块,重点关注下60行, ExceptionUtils.handleThrowable(t)。

如果accept过程中抛了一个异常会怎样?接着看下ExceptionUtils.handleThrowable(t)的处理逻辑:

/**
     * Checks whether the supplied Throwable is one that needs to be
     * rethrown and swallows all others.
     * @param t the Throwable to check
     */
    public static void handleThrowable(Throwable t) {
        if (t instanceof ThreadDeath) {
            throw (ThreadDeath) t;
        }
        if (t instanceof StackOverflowError) {
            // Swallow silently - it should be recoverable
            return;
        }
        if (t instanceof VirtualMachineError) {
            throw (VirtualMachineError) t;
        }
        // All other instances of Throwable will be silently swallowed
    }

  

如果是ThreadDeath和VirtualMachineError这两类异常就继续往上抛了,抛出去意味着什么呢?

 思考三秒钟

 

 

 如果继续往上抛说明Acceptor线程意外退出运行,自然就不会去tcp队列中accept连接,队列不堆积才怪呢,想到这儿我立马去翻一下预览服务的日志,看会不会有什么发现,其中有一条日志引起了我的关注。

Exception in thread "http-nio-8080-Acceptor" java.lang.OutOfMemoryError: Java heap space

  

"http-nio-8080-Acceptor" 正是Acceptor线程的名字,说明Acceptor抛了一个OutOfMemoryError。

OutOfMemoryError和Acceptor退出有什么关系呢,想想前面提到的handleThrowable逻辑 “如果是ThreadDeath和VirtualMachineError这两类异常就继续抛出”,这里的OutOfMemoryError正是VirtualMachineError的一个子类。

public class OutOfMemoryError extends VirtualMachineError

到这里可以说真相大白,是因为内存不足导致Acceptor线程在accept的时候抛了OutOfMemoryError,线程直接退出,所以导致大量连接堆积在tcp队列中。

 

其实到这儿第二个疑问“ 为什么tomcat不响应nginx的关闭socket请求呢?”也就很好解释了,因为Acceptor的退出,堆积在tcp队列中的连接tomcat消费不到,自然没有机会去响应nginx的关闭socket请求了,这里留个思考题,如果Acceptor不意外退出,那tomcat在拿到一个处于CLOSE_WAIT状态的连接会怎么处理? 

写在最后

通过一系列的分析最终得出是由于内存不足导致tomct的Acceptor线程异常退出,进而导致连接堆积在tcp队列中无法消费,最终引发了两个问题:

1.新请求一直进不去;

2.大量CLOSE_WAIT状态的连接存在,而且不会消失。

 

也许有人会问究竟是什么导致了内存不足呢,这里简单提一下,之前在提到在线预览处理流程的时候说过,预览服务会将附件转化为html提供给用户在线预览,转化这一步是比较耗内存的,有些客户的附件会达到百兆以上。

 

文中提到了一些非常有用的命令,比如jstack,top,netstat,ss等,之所以没有花太多篇幅去详细解释,一是我对命令的使用经验有限,二是网络上不乏铺天盖地的说明,讲的已经很好。

 

通过这篇文章,只是想分享一种排查此类问题的思路,希望你在遇到相似问题的时候带来些许帮助。

 

推荐阅读

关于TCP 半连接队列和全连接队列 

服务端close-wait或者time-wait状态过多会导致什么样的后果?

PHP升级导致系统负载过高问题分析

浅谈CLOSE_WAIT

 

 

 

Hive优化之小文件问题及其解决方案_lavimer-CSDN博客

$
0
0

小文件是如何产生的

1.动态分区插入数据,产生大量的小文件,从而导致map数量剧增。

2.reduce数量越多,小文件也越多(reduce的个数和输出文件是对应的)。

3.数据源本身就包含大量的小文件。


小文件问题的影响

1.从Hive的角度看,小文件会开很多map,一个map开一个JVM去执行,所以这些任务的初始化,启动,执行会浪费大量的资源,严重影响性能。

2.在HDFS中,每个小文件对象约占150byte,如果小文件过多会占用大量内存。这样NameNode内存容量严重制约了集群的扩展。


小文件问题的解决方案

从小文件产生的途经就可以从源头上控制小文件数量,方法如下:

1.使用Sequencefile作为表存储格式,不要用textfile,在一定程度上可以减少小文件。

2.减少reduce的数量(可以使用参数进行控制)。

3.少用动态分区,用时记得按distribute by分区。


对于已有的小文件,我们可以通过以下几种方案解决:

1.使用hadoop archive命令把小文件进行归档。

2.重建表,建表时减少reduce数量。

3.通过参数进行调节,设置map/reduce端的相关参数,如下:

设置map输入合并小文件的相关参数:

//每个Map最大输入大小(这个值决定了合并后文件的数量)
set mapred.max.split.size=256000000;  
//一个节点上split的至少的大小(这个值决定了多个DataNode上的文件是否需要合并)
set mapred.min.split.size.per.node=100000000;
//一个交换机下split的至少的大小(这个值决定了多个交换机上的文件是否需要合并)  
set mapred.min.split.size.per.rack=100000000;
//执行Map前进行小文件合并
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;

设置map输出和reduce输出进行合并的相关参数:

//设置map端输出进行合并,默认为true
set hive.merge.mapfiles = true
//设置reduce端输出进行合并,默认为false
set hive.merge.mapredfiles = true
//设置合并文件的大小
set hive.merge.size.per.task = 256*1000*1000
//当输出文件的平均大小小于该值时,启动一个独立的MapReduce任务进行文件merge。
set hive.merge.smallfiles.avgsize=16000000


一次死锁导致CPU异常飘高的整个故障排查过程 - 自由早晚乱余生 - 博客园

$
0
0

一、问题详情

linux一切皆文件

2021年4月2号,晚上10.45分左右,线上业务异常,后排查 线上服务器CPU 异常高,机器是 16核 64G的。但是实际负载已经达到了 140左右。

top 命令截图

image-20210408094444156

联系腾讯云排查

  1. 虚拟机所属于物理机是否有故障。
  2. 虚拟机所用的资源是否有抖动或者变更。(网络/存储等)

腾讯云回复暂无异常。

检查系统日志发现异常

Apr  2 22:45:22 docker-machine systemd: Reloading.
Apr  2 22:46:37 docker-machine systemd-logind: Failed to start session scope session-175098.scope: Connection timed out
Apr  2 22:47:26 docker-machine systemd-logind: Failed to start session scope session-175101.scope: Connection timed out
Apr  2 22:47:51 docker-machine systemd-logind: Failed to start session scope session-175102.scope: Connection timed out
Apr  2 22:48:26 docker-machine systemd-logind: Failed to start session scope session-175104.scope: Connection timed out
Apr  2 22:48:51 docker-machine systemd-logind: Failed to start session scope session-175105.scope: Connection timed out
Apr  2 22:49:06 docker-machine kernel: INFO: task systemd:1 blocked for more than 120 seconds.
Apr  2 22:49:06 docker-machine kernel:      Not tainted 4.4.108-1.el7.elrepo.x86_64 #1
Apr  2 22:49:06 docker-machine kernel: "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
Apr  2 22:49:06 docker-machine kernel: systemd         D ffff880fd8bebc68     0     1      0 0x00000000
Apr  2 22:49:06 docker-machine kernel: ffff880fd8bebc68 ffff880fd5e69c00 ffff880fd8be0000 ffff880fd8bec000
Apr  2 22:49:06 docker-machine kernel: ffff880fd8bebdb8 ffff880fd8bebdb0 ffff880fd8be0000 ffff88039c6a9140
Apr  2 22:49:06 docker-machine kernel: ffff880fd8bebc80 ffffffff81700085 7fffffffffffffff ffff880fd8bebd30
Apr  2 22:49:06 docker-machine kernel: Call Trace:
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81700085>] schedule+0x35/0x80
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81702d97>] schedule_timeout+0x237/0x2d0
Apr  2 22:49:06 docker-machine kernel: [<ffffffff813392cf>] ? idr_remove+0x17f/0x260
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81700b81>] wait_for_completion+0xf1/0x130
Apr  2 22:49:06 docker-machine kernel: [<ffffffff810aa6a0>] ? wake_up_q+0x80/0x80
Apr  2 22:49:06 docker-machine kernel: [<ffffffff810e2804>] __synchronize_srcu+0xf4/0x130
Apr  2 22:49:06 docker-machine kernel: [<ffffffff810e1c70>] ? trace_raw_output_rcu_utilization+0x60/0x60
Apr  2 22:49:06 docker-machine kernel: [<ffffffff810e2864>] synchronize_srcu+0x24/0x30
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81249b3b>] fsnotify_destroy_group+0x3b/0x70
Apr  2 22:49:06 docker-machine kernel: [<ffffffff8124b872>] inotify_release+0x22/0x50
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81208b64>] __fput+0xe4/0x210
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81208cce>] ____fput+0xe/0x10
Apr  2 22:49:06 docker-machine kernel: [<ffffffff8109c1e6>] task_work_run+0x86/0xb0
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81079acf>] exit_to_usermode_loop+0x73/0xa2
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81003bcd>] syscall_return_slowpath+0x8d/0xa0
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81703d8c>] int_ret_from_sys_call+0x25/0x8f
Apr  2 22:49:06 docker-machine kernel: INFO: task fsnotify_mark:135 blocked for more than 120 seconds.
Apr  2 22:49:06 docker-machine kernel:      Not tainted 4.4.108-1.el7.elrepo.x86_64 #1
Apr  2 22:49:06 docker-machine kernel: "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
Apr  2 22:49:06 docker-machine kernel: fsnotify_mark   D ffff880fd4993c88     0   135      2 0x00000000
Apr  2 22:49:06 docker-machine kernel: ffff880fd4993c88 ffff880fdf597648 ffff880fd8375900 ffff880fd4994000
Apr  2 22:49:06 docker-machine kernel: ffff880fd4993dd8 ffff880fd4993dd0 ffff880fd8375900 ffff880fd4993e40
Apr  2 22:49:06 docker-machine kernel: ffff880fd4993ca0 ffffffff81700085 7fffffffffffffff ffff880fd4993d50
Apr  2 22:49:06 docker-machine kernel: Call Trace:
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81700085>] schedule+0x35/0x80
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81702d97>] schedule_timeout+0x237/0x2d0
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81062aee>] ? kvm_clock_read+0x1e/0x20
Apr  2 22:49:06 docker-machine kernel: [<ffffffff81700b81>] wait_for_completion+0xf1/0x130
Apr  2 22:49:11 docker-machine kernel: INFO: task java:12560 blocked for more than 120 seconds.
Apr  2 22:49:11 docker-machine kernel:      Not tainted 4.4.108-1.el7.elrepo.x86_64 #1
Apr  2 22:49:11 docker-machine kernel: "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
Apr  2 22:49:11 docker-machine kernel: java            D ffff880bfbdc7b00     0 12560   4206 0x00000180
Apr  2 22:49:11 docker-machine kernel: ffff880bfbdc7b00 ffff880bfbdc7b40 ffff880bfbdac2c0 ffff880bfbdc8000
Apr  2 22:49:11 docker-machine kernel: ffff8809beb142d8 ffff8809beb14200 0000000000000000 0000000000000000
Apr  2 22:49:11 docker-machine kernel: ffff880bfbdc7b18 ffffffff81700085 ffff880b155adfc0 ffff880bfbdc7b98
Apr  2 22:49:11 docker-machine kernel: Call Trace:
Apr  2 22:49:11 docker-machine kernel: [<ffffffff81700085>] schedule+0x35/0x80
Apr  2 22:49:11 docker-machine kernel: [<ffffffff8124ca55>] fanotify_handle_event+0x1b5/0x2f0
Apr  2 22:49:11 docker-machine kernel: [<ffffffff810c2b50>] ? prepare_to_wait_event+0xf0/0xf0
Apr  2 22:49:11 docker-machine kernel: [<ffffffff8124933f>] fsnotify+0x26f/0x460
Apr  2 22:49:11 docker-machine kernel: [<ffffffff810a1fd1>] ? in_group_p+0x31/0x40
Apr  2 22:49:11 docker-machine kernel: [<ffffffff812111fc>] ? generic_permission+0x15c/0x1d0
Apr  2 22:49:11 docker-machine kernel: [<ffffffff812b355b>] security_file_open+0x8b/0x90
Apr  2 22:49:11 docker-machine kernel: [<ffffffff8120484f>] do_dentry_open+0xbf/0x320
Apr  2 22:49:11 docker-machine kernel: [<ffffffffa02cb552>] ? ovl_d_select_inode+0x42/0x110 [overlay]
Apr  2 22:49:11 docker-machine kernel: [<ffffffff81205e15>] vfs_open+0x55/0x80
Apr  2 22:49:11 docker-machine kernel: [<ffffffff81214143>] path_openat+0x1c3/0x1300

查看日志,觉得很大可能性是: cache 落盘故障,有可能是 io 的问题。通过 iotop进行排查,未发现异常。

当时我们认为是 腾讯云底层存储或者网络出现问题导致的。

在排查了近一个小时,机器上面的cpu 还是没有降低。我们对机器进行了重启。重启后,一些恢复了正常。

二、 问题解析

  • 认为是存储的问题

    首先上面的故障是同时出现在两台机器(A和B)的, 询问腾讯云 A 的系统盘和A的数据盘以及B的数据盘都是在同一个远端存储的,所以这更加深了我们认为是存储导致的问题,有可能是到物理机到存储之间的网络,也有可能是存储本身的性能问题。

    腾讯云排查后说这两个机器,所用的存储和存储网络没有问题,所以存储问题不成立。

  • 系统的僵尸进程很多

    在上面top 命令我们可以看到有僵死进程,后面也是一直在增加僵死进程。

    image-20210408131034043

    僵死进程的来源:

    1. 上面的僵死进程来源是我们的定时任务导致的,我们定时任务脚本执行的进程变成的僵死进程。

    如何看僵死进程

    ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
  • /var/log/message 异常信息

    我们再看看 /var/log/message的日志,我们可以看到一个很关键的信息 kernel: INFO: task systemd:1 blocked for more than 120 seconds.

    网上大多数时是说 vm.dirty_ratiovm.dirty_background_ratio这两个参数设置的有问题。

    我们查看了我们这两个内核参数的配置,都是正常合理的。

    $ sysctl -a|grep -E  'vm.dirty_background_ratio|vm.dirty_ratio'
    vm.dirty_background_ratio = 10  # 
    vm.dirty_ratio = 30

    具体的参数详解,见下文。

  • 我们再看看 /var/log/message的日志,我们可以看到一个很关键的信息

    Apr  2 22:45:22 docker-machine systemd: Reloading.
    Apr  2 22:49:06 docker-machine kernel: INFO: task systemd:1 blocked for more than 120 seconds.
    Apr  2 22:49:06 docker-machine kernel: systemd         D ffff880fd8bebc68     0     1      0 0x00000000
    Apr  2 22:49:06 docker-machine kernel: INFO: task fsnotify_mark:135 blocked for more than 120 seconds.
    Apr  2 22:49:06 docker-machine kernel: fsnotify_mark   D ffff880fd4993c88     0   135      2 0x00000000
    Apr  2 22:49:11 docker-machine kernel: INFO: task java:12560 blocked for more than 120 seconds.
    Apr  2 22:49:11 docker-machine kernel: java            D ffff880bfbdc7b00     0 12560   4206 0x00000180

    就是 systemdReloading, systemdfsnotify_mark都被block了,那么被锁了原因是什么,按道理来说应该 io的问题啊,就是写得慢啊,但是我们忽略了一个问题,如果要写的文件加锁了,那么也是会出现这个情况的啊。

    寻找加锁的原因: 腾讯云主机安全产品 云镜, 没错就很大可能性是它导致的。 具体内容见下文。

三、问题原因

为什么会定位到云镜产品,首先是我们认为如果底层 io 没有问题的话,那么就只能是文件可能被锁了,并且如果你细心的话,你会发现僵死进程里面,有云镜的身影

image-20210408221649277

为什么云镜会变成僵死进程,是因为云镜启动失败了,一直在启动。

我们再说回为什么会定位到云镜上面,主要是因为云镜会对系统上文件有定期扫描的,为什么会想到就是安全产品( https://access.redhat.com/solutions/2838901)。 安全产品就是云镜。

我们观察云镜的日志,我们又发现了一个问题,原来在 22:45左右,云镜在更新,这个很巧合啊,我们出问题的两个机器都在这个时间段进行了更新,而没有异常的机器,都没有更新操作。

  1. 云镜更新的日志

    image-20210409153620789

  2. 更新后一直没有云镜一直启动失败

    image-20210409153748281

  3. redhat官方文档

    https://access.redhat.com/solutions/2838901

    也是说到安全产品会可能触发这个问题。

    image-20210409153148717

最终结论

最终让腾讯云排查云镜此次版本升级,得到答复:

​ 推测 YDServiceexit group退出的时未及时对 fanotify/inotify进行适当的清理工作,导致其它进程阻塞等待,因此针对此点进行了优化。

问题1: 针对为什么只有两台机器在那个时间点进行更新,是因为那个云镜后端调度策略是分批升级。

四、扩展

进程的几种状态

https://liam.page/2020/01/10/the-states-of-processes-on-Linux/

进程通常处于以下两种状态之一:

  • 在 CPU 上执行(此时,进程正在运行) 在 ps或是 top中,状态标识为 R的进程,即处于正在运行状态。

  • 不在 CPU 上执行(此时,进程未在运行)

    未在运行的进程可能处于不同状态:

    • 可运行状态 (R)

      进程获取了所有所需资源,正等待 CPU 时,就会进入可运行状态。处于可运行状态的进程在 ps的输出中,也已 R标识。

      举例来说,一个正在 I/O 的进程并不立即需要 CPU。当进程完成 I/O 操作后,就会触发一个信号,通知 CPU 和调度器将该进程置于运行队列(由内核维护的可运行进程的列表)。当 CPU 可用时,该进程就会进入正在运行状态。

    • 可中断之睡眠状态 (S)

      可中断之睡眠状态表示进程在等待时间片段或者某个特定的事件。一旦事件发生,进程会从可中断之睡眠状态中退出。 ps命令的输出中,可中断之睡眠状态标识为 S

    • 不可中断之睡眠状态(D)

      不可中断之睡眠状态的进程不会处理任何信号,而仅在其等待的资源可用或超时时退出(前提是设置了超时时间)。

      不可中断之睡眠状态通常和设备驱动等待磁盘或网络 I/O 有关。在内核源码 fs/proc/array.c中,其文字定义为 "D (disk sleep)", /* 2 */。当进程进入不可中断之睡眠状态时,进程不会处理信号,而是将信号都积累起来,等进程唤醒之后再处理。在 Linux 中, ps命令使用 D来标识处于不可中断之睡眠状态的进程。

      系统会为不可中断之睡眠状态的进程设置进程运行状态为:

      p->state = TASK_UNINTERRUPTABLE;

      由于处于不可中断之睡眠状态的进程不会处理任何信号,所以 kill -9也杀不掉它。解决此类进程的办法只有两个:

      • 对于怨妇,你还能怎么办,只能满足它啊:搞定不可中断之睡眠状态进程所等待的资源,使资源可用。
      • 如果满足不了它,那就只能 kill the world——重启系统。
    • 僵死状态(Z)

      进程可以主动调用 exit系统调用来终止,或者接受信号来由信号处理函数来调用 exit系统调用来终止。

      当进程执行 exit系统调用后,进程会释放相应的数据结构;此时,进程本身已经终止。不过,此时操作系统还没有释放进程表中该进程的槽位(可以形象地理解为,「父进程还没有替子进程收尸」);为解决这个问题,终止前,进程会向父进程发送 SIGCHLD信号,通知父进程来释放子进程在操作系统进程表中的槽位。这个设计是为了让父进程知道子进程退出时所处的状态。

      子进程终止后到父进程释放进程表中子进程所占槽位的过程,子进程进入僵尸状态(zombie state)。如果在父进程因为各种原因,在释放子进程槽位之前就挂掉了,也就是,父进程来不及为子进程收尸。那么,子进程就会一直处于僵尸状态。而考虑到,处于僵尸状态的进程本身已经终止,无法再处理任何信号,所以它就只能是孤魂野鬼,飘在操作系统进程表里,直到系统重启。

    马后炮

    在前面的日志中,也就是下面:

    Apr  2 22:49:06 docker-machine kernel: systemd         D ffff880fd8bebc68     0     1      0 0x00000000
    Apr  2 22:49:06 docker-machine kernel: INFO: task fsnotify_mark:135 blocked for more than 120 seconds.
    Apr  2 22:49:06 docker-machine kernel: fsnotify_mark   D ffff880fd4993c88     0   135      2 0x00000000

    我们部分进程处于 不可中断之睡眠状态(D), 在这个状态的服务,前面也说到只能给他资源,或者重启系统。 也就可以说明:

    解释疑问:

    1. 为什么我们故障机器上面部分服务存在问题,部分服务正常。

      因为部分进程处于 不可中断之睡眠状态(D)。文件(linux一切皆文件)被锁,导致了部分服务进程进入了不可中断睡眠状态。

如何快速清理僵尸进程(Z)

用top查看系统中的僵尸进程情况
top
再看看这些僵尸是什么程序来的
ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
kill -s SIGCHLD pid  (父进程pid)

内核参数相关

  • dirty_background_ratio 指当文件系统缓存脏页数量达到系统内存百分之多少时(默认10%)唤醒内核的 flush 等进程,写回磁盘。
  • dirty_ratio 为最大脏页比例,当脏页数达到该比例时,必须将所有脏数据提交到磁盘,同时所有新的 IO 都会被阻塞,直到脏数据被写入磁盘,通常会造成 IO 卡顿。系统先会达到 vm.dirty_background_ratio的条件然后触发 flush 进程进行异步的回写操作,此时应用进程仍然可以进行写操作,如果达到 vm.dirty_ratio这个参数所设定的值,此时操作系统会转入同步地处理脏页的过程,阻塞应用进程。

如何查看哪些文件被哪些进程被锁

http://blog.chinaunix.net/uid-28541347-id-5678998.html

cat /proc/locks
1: POSIX  ADVISORY  WRITE 3376 fd:10:805736756 0 EOF
2: FLOCK  ADVISORY  WRITE 1446 00:14:23843 0 EOF
3: FLOCK  ADVISORY  WRITE 4650 00:14:32551 0 EOF
4: POSIX  ADVISORY  WRITE 4719 fd:01:531689 1073741824 1073742335
5: OFDLCK ADVISORY  READ  1427 00:06:1028 0 EOF
6: POSIX  ADVISORY  WRITE 4719 00:14:26155 0 EOF
7: POSIX  ADVISORY  WRITE 4443 00:14:26099 0 EOF
8: FLOCK  ADVISORY  WRITE 4561 00:14:34870 0 EOF
9: POSIX  ADVISORY  WRITE 566 00:14:15509 0 EOF
10: POSIX  ADVISORY  WRITE 4650 fd:01:788600 0 EOF
11: OFDLCK ADVISORY  READ  1713 00:06:1028 0 EOF
12: FLOCK  ADVISORY  WRITE 1713 fd:10:268435553 0 EOF
13: FLOCK  ADVISORY  WRITE 1713 fd:10:268435528 0 EOF
14: POSIX  ADVISORY  WRITE 12198 fd:01:526366 0 EOF
15: POSIX  ADVISORY  WRITE 3065 fd:10:805736741 0 EOF
16: FLOCK  ADVISORY  WRITE 1731 fd:10:268435525 0 EOF
17: FLOCK  ADVISORY  WRITE 4459 00:14:37972 0 EOF
18: POSIX  ADVISORY  WRITE 1444 00:14:14793 0 EOF

我们可以看到 /proc/locks下面有锁的信息:我现在分别叙述下含义:

  1. POSIX FLOCK这个比较明确,就是哪个类型的锁。flock系统调用产生的是FLOCK,fcntl调用F_SETLK, F_SETLKW或者 lockf产生的是POSIX类型,有次可见两种调用产生的锁的类型是不同的;

  2. ADVISORY表明是劝告锁;

  3. WRITE顾名思义,是写锁,还有读锁;

  4. 18849 是持有锁的进程ID。当然对于flock这种类型的锁,会出现进程已经退出的状况。

  5. 08:02:852674表示的对应磁盘文件的所在设备的主设备好,次设备号,还有文件对应的inode number。

  6. 0 表示的是所的其实位置

  7. EOF表示的是结束位置。 这两个字段对fcntl类型比较有用,对flock来是总是0 和EOF。

Oracle数据库给字段设置默认时间及更新字段之后时间更新 - 牧雨 - 博客园

$
0
0

一、给字段设置默认时间

1、建表时运用 DEFAULT SYSDATE 给字段设置默认时间:

CREATETABLE"TEST"."TEST_DATE" (
idVARCHAR2(2BYTE)NOTNULL,valuesNUMBERNOTNULL,
create_time DATEDEFAULTSYSDATE,
update_timeTIMESTAMP(6)DEFAULTSYSDATE
)。

 

2、运用 alter table 来给字段添加默认值:

altertableTEST_DATEadd"creat_time" DATEDEFAULTSYSDATE;

 

其中:

TEST_DATE 为表名。

"creat_time"为具体字段名。

DATE :为字段类型。

注意所选字段为当前表的字段,且字段正确性要验证,否则会多添加出一个字段 。

二、字段更新后自动更新update_time.

  通过给表设置触发器,当触发器触发时则会自动调用触发条件:

  

createorreplacetriggerTEST_DATE_trigger
beforeupdateonTEST_DATEforeach rowbegin:new.UPDATE_TIME :=sysdate;end;

其中:

TEST_DATE_trigger 为触发器名称。

TEST_DATE :为表名

UPDATE_TIME:为字段名

 

Viewing all 532 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>