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

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


  • 首页

  • 归档

  • 搜索

Sentinel源码分析(第四篇):部分功能插槽原理分析

发表于 2020-02-10

1. 前言

在之前的文章中,分析了Sentinel如何统计数据指标的。本篇文章主要是分析调用链路上的一些功能插槽的功能和实现,包括系统自适应、黑白名单控制、流量控制、熔断降级四个功能。

2. Sentinel功能插槽规则

2.1. 规则的定义

在分析各种功能插槽之前,先看一下Sentinel对规则的定义。

Sentinel使用Rule表示规则,Rule是一个接口,Rule只有一个方法,passCheck(),表示规则是否通过。

AbstractRule实现了Rule,是一个抽象类,里面定义了这个规则属于的资源,限制的app。所有具体的规则都是继承该规则。

2.2. 规则的加载或更新

当定义了具体的规则之后,需要加载规则到系统中,这个加载过程是由规则管理器负责的,比如定义了系统自适应规则SystemRule,会由对应的规则管理器SystemRuleManager加载该规则。
下面就以SystemRuleManager为例,分析规则的加载。

每一个规则管理器中都会有一个继承了SimplePropertyListener规则改变处理器作为变量,当规则改变之后,做相关的更新处理工作。每个规则管理器中会定义一个内部类作为该变量的具体实现,比如SystemRuleManager:

1
2
3
4
5
6
7
复制代码//创建一个文件处理器
private final static SystemPropertyListener listener = new SystemPropertyListener();

//定义一个处理该规则的处理器类
static class SystemPropertyListener extends SimplePropertyListener<List<SystemRule>>{
。。。
}

可以看到,SystemRuleManager创建了一个SystemPropertyListener作为文件改变的处理器,SystemPropertyListener是其内部的一个内部类。

规则管理器中还会定义一个SentinelProperty,SentinelProperty代表Sentinel中的配置信息,在这个地方就是代表配置的规则。然后会将定义的规则处理器添加给规则,当规则变更的时候,使用对应的规则处理器进行处理。

1
2
3
复制代码private static SentinelProperty<List<SystemRule>> currentProperty = new DynamicSentinelProperty<List<SystemRule>>();

currentProperty.addListener(listener);

以SystemRule更新为例,当定义了规则之后,调用SystemRuleManager的loadRules()会进行规则的更新。

1
2
3
复制代码public static void loadRules(List<SystemRule> rules) {
currentProperty.updateValue(rules);
}

调用loadRules()的时候其实是调用DynamicSentinelProperty的updateValues()方法:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码 public boolean updateValue(T newValue) {
if (isEqual(value, newValue)) {
return false;
}
RecordLog.info("[DynamicSentinelProperty] Config will be updated to: " + newValue);

value = newValue;
for (PropertyListener<T> listener : listeners) {
listener.configUpdate(newValue);
}
return true;
}

可以看到,DynamicSentinelProperty的updateValues()的方法中其实是调用规则改变处理器的configUpdate()方法,所以具体是如何更新的是每个规则管理器里面定义的内部类SimplePropertyListener。

以上就是规则的加载过程,各种规则的加载大致流程是一样的,只是具体的加载逻辑都是由自己的规则管理器中定义的SimplePropertyListener来负责更新。

3. 系统自适应限流

3.1. 什么是系统自适应限流

系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。

系统规则支持以下的模式:

  • Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
  • CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围

0.0-1.0),比较灵敏。

  • 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
    并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

以上文字摘抄至sentinel官方文档。

2.2. SystemRule

SystemRule代表系统自适应保护的规则定义,定义了load、cpu、qps、rt、thread维度的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class SystemRule extends AbstractRule {
/**
* negative value means no threshold checking.
*/
private double highestSystemLoad = -1;
/**
* cpu usage, between [0, 1]
*/
private double highestCpuUsage = -1;
private double qps = -1;
private long avgRt = -1;
private long maxThread = -1;
}

2.3. SystemRuleManager

SystemRuleManager是实现系统自适应保护的核心类,负责管理系统自适应规则,包括加载更新规则,检查当前入口调用是否满足规则。

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

private static volatile double highestSystemLoad = Double.MAX_VALUE;
private static volatile double highestCpuUsage = Double.MAX_VALUE;
private static volatile double qps = Double.MAX_VALUE;
private static volatile long maxRt = Long.MAX_VALUE;
private static volatile long maxThread = Long.MAX_VALUE;

private static volatile boolean highestSystemLoadIsSet = false;
private static volatile boolean highestCpuUsageIsSet = false;
private static volatile boolean qpsIsSet = false;
private static volatile boolean maxRtIsSet = false;
private static volatile boolean maxThreadIsSet = false;

/**
* 是否存在指定的系统规则,即是否调用SystemRuleManager.loadRules()加载规则
*/
private static AtomicBoolean checkSystemStatus = new AtomicBoolean(false);

/**
* 一个获取系统当前的load和cpu使用的任务
*/
private static SystemStatusListener statusListener = null;

/**
* 系统规则改变后的处理器,主要将规则设置为指定的规则
*/
private final static SystemPropertyListener listener = new SystemPropertyListener();

/**
* 系统规则改变后的处理器,主要将规则设置为指定的规则
*/
private static SentinelProperty<List<SystemRule>> currentProperty = new DynamicSentinelProperty<List<SystemRule>>();

/**
*
*/
private final static ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1,
new NamedThreadFactory("sentinel-system-status-record-task", true));

static {
checkSystemStatus.set(false);
statusListener = new SystemStatusListener();
//定时任务5秒后开始,每秒执行一次
scheduler.scheduleAtFixedRate(statusListener, 5, 1, TimeUnit.SECONDS);
currentProperty.addListener(listener);
}

...
}

上面的参数主要含义为:

  • 前面定义的十个参数主要是描述系统自适应保护参数的。
  • checkSystemStatus:是否需要进行系统自适应保护。
  • statusListener:负责获取系统运行参数的一个任务。
  • listener:系统规则改变后的处理器。
  • currentProperty:当前配置文件。
  • scheduler:一个负责运行statusListener的定时任务。

再看静态代码块,主要是初始化scheduler,设置运行的任务和频率,并且将系统规则处理器添加给currentProperty。

SystemRuleManager中会负责规则的加载,这个部分在第二小节中已经讲过。

SystemRuleManager中还有一个最重要的方法,checkSystem(),负责校验进入的请求是否满足系统自适应设置的规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
复制代码public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
// Ensure the checking switch is on.
//如果没有加入系统规则,则不需要检查
if (!checkSystemStatus.get()) {
return;
}

// for inbound traffic only
//如果不是系统入口的资源,不检查
if (resourceWrapper.getType() != EntryType.IN) {
return;
}

//根据全局的ClusterNode获取数据指标,ClusterNode是一个类变量,全局唯一,类加载后生成。
// 每次通过check后再StatisticSlot中累加统计

// total qps
double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps();
if (currentQps > qps) {
throw new SystemBlockException(resourceWrapper.getName(), "qps");
}

// total thread
int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum();
if (currentThread > maxThread) {
throw new SystemBlockException(resourceWrapper.getName(), "thread");
}

double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt();
if (rt > maxRt) {
throw new SystemBlockException(resourceWrapper.getName(), "rt");
}

// load. BBR algorithm.
//如果当前机器的load大于设置的值
if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
if (!checkBbr(currentThread)) {
throw new SystemBlockException(resourceWrapper.getName(), "load");
}
}

// cpu usage
if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) {
if (!checkBbr(currentThread)) {
throw new SystemBlockException(resourceWrapper.getName(), "cpu");
}
}
}

checkSystem()中具体的逻辑如上,具体实现原理可以参考官方文档。

2.4. SystemSlot

SystemSlot是负责系统自适应保护的功能插槽,每一个内部entry进入的时候都会进行系统自适应保护校验。SystemSlot很简单,主要是在entry()方法中调用 SystemRuleManager.checkSystem()方法进行系统自适应保护的检查,具体逻辑还是在SystemRuleManager中。

3. AuthoritySlot

黑白名单根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

3.1. AuthorityRule

AuthorityRule代表对黑白名单设置的规则,其中主要包括个属性设置,一个是限制的来源名称,多个来源使用逗号分隔。还需要设置限制的时白名单还是黑名单。

1
2
复制代码private String limitApp;
private int strategy = RuleConstant.AUTHORITY_WHITE;

3.2. AuthorityRuleManager

AuthorityRuleManager和上一节的SystemRuleManager类似,是负责管理黑白名单的加载的。先看一下其中的属性定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码public final class AuthorityRuleManager {

/**
* 保存权限配置,以resource为key,所以同一个resource只能有一个最新的权限规则
*/
private static Map<String, Set<AuthorityRule>> authorityRules = new ConcurrentHashMap<>();

/**
* 规则改变处理器
*/
private static final RulePropertyListener LISTENER = new RulePropertyListener();

/**
* 当前规则
*/
private static SentinelProperty<List<AuthorityRule>> currentProperty = new DynamicSentinelProperty<>();

static {
currentProperty.addListener(LISTENER);
}
}

和SystemRuleManager类似,AuthorityRuleManager也有一个规则改变的处理器RulePropertyListener,这个RulePropertyListener是AuthorityRuleManager的一个内部类,然后也有一个表示当前配置的配置类currentProperty,当需要加载更新规则的时候,逻辑和SystemRuleManager类似,所以不再描述。

3.3. AuthoritySlot

AuthoritySlot是负责处理黑白名单的功能插槽,当entry进入的时候,调用checkBlackWhiteAuthority()进行校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
复制代码public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
throws Throwable {
checkBlackWhiteAuthority(resourceWrapper, context);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

/**
* 检查黑白名单
* @param resource
* @param context
* @throws AuthorityException
*/
void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {

//获取所有的规则
Map<String, Set<AuthorityRule>> authorityRules = AuthorityRuleManager.getAuthorityRules();

if (authorityRules == null) {
return;
}

//获取指定resource的规则
Set<AuthorityRule> rules = authorityRules.get(resource.getName());
if (rules == null) {
return;
}

for (AuthorityRule rule : rules) {
if (!AuthorityRuleChecker.passCheck(rule, context)) {
throw new AuthorityException(context.getOrigin(), rule);
}
}
}

可以看到,这个地方主要是获取所有的黑白名单规则,然后遍历调用AuthorityRuleChecker.passCheck()进行校验,校验的具体逻辑很简单,就不在此描述了。

4. 流量控制

流量控制原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

4.1. FlowRule

FlowRule代表流量控制规则,定义了流量控制的各种参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
复制代码public class FlowRule extends AbstractRule {
/**
* The threshold type of flow control (0: thread count, 1: QPS).
*/
private int grade = RuleConstant.FLOW_GRADE_QPS;

/**
* Flow control threshold count.
*/
private double count;

/**
* Flow control strategy based on invocation chain.
*
* {@link RuleConstant#STRATEGY_DIRECT} for direct flow control (by origin);
* {@link RuleConstant#STRATEGY_RELATE} for relevant flow control (with relevant resource);
* {@link RuleConstant#STRATEGY_CHAIN} for chain flow control (by entrance resource).
*/
private int strategy = RuleConstant.STRATEGY_DIRECT;

/**
* Reference resource in flow control with relevant resource or context.
*/
private String refResource;

/**
* Rate limiter control behavior.
* 0. default(reject directly), 1. warm up, 2. rate limiter, 3. warm up + rate limiter
*/
private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;

private int warmUpPeriodSec = 10;

/**
* Max queueing time in rate limiter behavior.
*/
private int maxQueueingTimeMs = 500;

private boolean clusterMode;
/**
* Flow rule config for cluster mode.
*/
private ClusterFlowConfig clusterConfig;

/**
* The traffic shaping (throttling) controller.
*/
private TrafficShapingController controller;

一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:

  • resource:资源名,即限流规则的作用对象
  • count: 限流阈值
  • grade: 限流阈值类型(QPS 或并发线程数)
  • limitApp: 流控针对的调用来源,若为 default 则不区分调用来源
  • strategy: 调用关系限流策略
  • controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)

4.2. FlowSlot

FlowSlot是负责流量控制的功能插槽,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码     * 流量规则检查器
*/
private final FlowRuleChecker checker;

public FlowSlot() {
this(new FlowRuleChecker());
}

...

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
checkFlow(resourceWrapper, context, node, count, prioritized);

fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
throws BlockException {
checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}


/**
* 规则提供者
*/
private final Function<String, Collection<FlowRule>> ruleProvider = new Function<String, Collection<FlowRule>>() {
@Override
public Collection<FlowRule> apply(String resource) {
// Flow rule map should not be null.
Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();
return flowRules.get(resource);
}
};
...

FlowSLot中定义了一个FlowRuleChecker,FlowRuleChecker负责对限流规则进行检查。可以看到,FlowSlot实际的限流逻辑调用FlowRuleChecker实现的。在调用FlowRuleChecker的checkFlow()方法的时候,需要传入一个ruleProvider,这是一个Function。

4.3. FlowRuleChecker

上面说到FlowRuleChecker是负责流量控制校验的,FlowSlot中调用FlowRuleChecker的checkFlow()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
if (ruleProvider == null || resource == null) {
return;
}
Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
if (rules != null) {
for (FlowRule rule : rules) {
if (!canPassCheck(rule, context, node, count, prioritized)) {
throw new FlowException(rule.getLimitApp(), rule);
}
}
}
}

checkFlow()很简单,就是获取限流规则,遍历调用canPassCheck()方法进行校验,如果校验失败,则抛出FlowException。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public boolean canPassCheck(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node, int acquireCount,
boolean prioritized) {
String limitApp = rule.getLimitApp();
if (limitApp == null) {
return true;
}

if (rule.isClusterMode()) {
return passClusterCheck(rule, context, node, acquireCount, prioritized);
}

return passLocalCheck(rule, context, node, acquireCount, prioritized);
}

canPassCheck()也很简单,就是根据不同的模式调用不同的方法。Sentinel可以使用集群模式运行或者本地模式,不同的模式限流逻辑不一样。这个地方由于没有讲到集群,所以先按本地模式进行分析。

1
2
3
4
5
6
7
8
9
复制代码 private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
boolean prioritized) {
Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
if (selectedNode == null) {
return true;
}

return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}

passLocalCheck()中,先根据配置的strategy和请求来源,获得对应的Node节点,再调用对应节点的canPass()方法进行校验。

在FlowRule中有一个TrafficShapingController,代表流量控制的类型,包括默认值、直接拒绝、Warm Up、匀速排队,所以具体如何限流需要看设置的策略。

4.4. TrafficShapingController

TrafficShapingController是一个接口,主要是定义流量控制策略,它有三个实现,分别代表不同的处理模式:

  • DefaultController:默认处理策略,直接拒绝处理
  • RateLimiterController:匀速排队
  • WarmUpController:预热/冷启动方式
  • WarmUpRateLimiterController:预热+匀速排队

在FlowRule中会有一个TrafficShapingController类型的变量rater,这个rater是在更新规则的时候根据设置的规则创建出来的,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码private static TrafficShapingController generateRater(/*@Valid*/ FlowRule rule) {
if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
switch (rule.getControlBehavior()) {
case RuleConstant.CONTROL_BEHAVIOR_WARM_UP:
return new WarmUpController(rule.getCount(), rule.getWarmUpPeriodSec(),
ColdFactorProperty.coldFactor);
case RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER:
return new RateLimiterController(rule.getMaxQueueingTimeMs(), rule.getCount());
case RuleConstant.CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER:
return new WarmUpRateLimiterController(rule.getCount(), rule.getWarmUpPeriodSec(),
rule.getMaxQueueingTimeMs(), ColdFactorProperty.coldFactor);
case RuleConstant.CONTROL_BEHAVIOR_DEFAULT:
default:
// Default mode or unknown mode: default traffic shaping controller (fast-reject).
}
}
return new DefaultController(rule.getCount(), rule.getGrade());
}

5. 熔断降级

Sentinel熔断降级会在调用链路中某个资源出现不稳定状态的时候对该资源进行限制,让请求快速失败,避免影响其他的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口内,对该资源的调用都会自动熔断。

5.1. 降级策略

  • 平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
  • 异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态

5.2. DegradeRule

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

/**
* RT threshold or exception ratio threshold count.
*/
private double count;

/**
* Degrade recover timeout (in seconds) when degradation occurs.
*/
private int timeWindow;

/**
* Degrade strategy (0: average RT, 1: exception ratio, 2: exception count).
*/
private int grade = RuleConstant.DEGRADE_GRADE_RT;

/**
* 每秒钟连续进入的请求超的平均响应时间超过阀值的请求数
* Minimum number of consecutive slow requests that can trigger RT circuit breaking.
*
* @since 1.7.0
*/
private int rtSlowRequestAmount = RuleConstant.DEGRADE_DEFAULT_SLOW_REQUEST_AMOUNT;

/**
* 每秒钟连续进入的请求发生异常的请求数
* Minimum number of requests (in an active statistic time span) that can trigger circuit breaking.
*
* @since 1.7.0
*/
private int minRequestAmount = RuleConstant.DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT;
}

DeGradeRule代表熔断降级的规则,各个字段含义为:

  • grade:熔断降级的模式,有平均响应时间、异常比例、异常数。
  • count:发生熔断降级的阈值。
  • timeWindow:发生熔断降级后持续的时间。
  • minRequestAmount:每秒连续进入的请求发生异常不熔断降级的最小阈值。
  • rtSlowRequestAmount:每秒连续进入的请求平均响应时间超过阈值的数量。

5.3. DegradeSlot

DegradeSlot是负责处理熔断降级的功能插槽,其代码非常简单,因为具体熔断降级的判断是在DegradeRuleManager中实现的。

1
2
3
4
5
复制代码public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
throws Throwable {
DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

5.4. DegradeRuleManager

DegradeRuleManager主要负责加载熔断降级规则、对调用entry进行熔断降级校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码/**
* 保存降级规则的缓存,以resource为key
*/
private static final Map<String, Set<DegradeRule>> degradeRules = new ConcurrentHashMap<>();

/**
* 规则改变处理器
*/
private static final RulePropertyListener LISTENER = new RulePropertyListener();


private static SentinelProperty<List<DegradeRule>> currentProperty
= new DynamicSentinelProperty<>();

static {
currentProperty.addListener(LISTENER);
}

DegradeRuleManager使用一个map来保存不同资源加载的熔断降级规则,并且也拥有自己的规则改变处理器,规则的加载和更新逻辑与其他功能插槽类似。

从上面可以看出,当entry进入DegradeSlot的时候,实际上是调用DegradeRuleManager的checkDegrade()方法进行熔断降级的检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码 public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count)
throws BlockException {

Set<DegradeRule> rules = degradeRules.get(resource.getName());
if (rules == null) {
return;
}

for (DegradeRule rule : rules) {
if (!rule.passCheck(context, node, count)) {
throw new DegradeException(rule.getLimitApp(), rule);
}
}
}

checkDegrade()方法先根据resource获取对应的限流规则,然后循环调用规则的passCheck()方法进行检查,如果不能通过检查,则抛出DegradeException。

所以,具体的校验逻辑是在DegradeRule中的passCheck()实现的,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
复制代码 public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) {
//如果是在限流的窗口时间内,直接降级
if (cut.get()) {
return false;
}

//降级只针对resource维度进行,不区分context,不区分orgin,在statisticSlot中会在defaultNode里面对clusterNode进行累加
ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(this.getResource());
if (clusterNode == null) {
return true;
}

if (grade == RuleConstant.DEGRADE_GRADE_RT) {

//case1:rt降级
//平均响应时间
double rt = clusterNode.avgRt();

//平均响应时间小于设置的阀值,直接通过
if (rt < this.count) {
passCount.set(0);
return true;
}

//请求数自增,如果请求数小于设置的值5,直接通过
if (passCount.incrementAndGet() < rtSlowRequestAmount) {
return true;
}

} else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) {

//case2 异常比例

double exception = clusterNode.exceptionQps();
double success = clusterNode.successQps();
double total = clusterNode.totalQps();

//如果请求总数小于设置的每秒允许的最小请求数量,直接返回
if (total < minRequestAmount) {
return true;
}
double realSuccess = success - exception;
if (realSuccess <= 0 && exception < minRequestAmount) {
return true;
}

if (exception / success < count) {
return true;
}
} else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) {

//case3: 异常数

//统计异常总数的时间窗口是分钟级别的,如果timeWindow的时间小于60s,会以60s进行熔断

double exception = clusterNode.totalException();
//如果异常数小于指定的值,直接返回
if (exception < count) {
return true;
}
}

//开启一个定时任务,在指定的窗口时间后执行,将请求数设置为0,降级标识设置为false
if (cut.compareAndSet(false, true)) {
ResetTask resetTask = new ResetTask(this);
pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS);
}

return false;
}

passCheck()逻辑比较简单,主要是先根据之前获取的数据判断是否满足降级设置的阈值,如果超过,则返回false,并且开启一个线程,其作用就是发生降级后指定的timeWindow内直接将后续请求当做降级处理。

6. 小结

这篇文章主要分析Sentinel中常用的功能插槽的实现原理,了解Sentinel的限流降级策略。

7.参考资料

流量控制

熔断降级

系统自适应限流

黑白名单控制

本文转载自: 掘金

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

漫画:大厂总体SaaS化部署,到底什么是IaaS、PaaS和

发表于 2020-02-10

​在之前的文章《如何给女朋友解释什么是云计算?》中,我们介绍了云计算,在文章中我们提到虽然都是使用云计算服务,但是不同的用户需要的服务是不一样的,有些公司可能只需要租借服务器、有些公司可能需要整套服务,而有些个人客户可能希望直接可以通过付费得到一个网站等。所以,根据服务类型的不同,云计算可以分为三类,即基础设施即服务(IaaS)、平台即服务(PaaS)和软件即服务(SaaS)。这3种云计算服务有时称为云计算堆栈,因为它们构建堆栈,它们位于彼此之上。

本文,就来分别介绍下到底什么是IaaS、PaaS和SaaS。

IaaS

IaaS(Infrastructure as a Service),即基础设施即服务。指把IT基础设施作为一种服务通过网络对外提供,并根据用户对资源的实际使用量或占用量进行计费的一种服务模式。

做一个简单的比喻,IaaS服务优点类似于共享厨房服务。

商家为用户提供了厨房、锅具、煤气灶、烤箱、电饭煲等等一系列厨房用品。用户需要自己购买米、面、蔬菜、调味品等,并进行加工制作。

有了IaaS服务,用户可以在云服务提供商提供的基础设施上部署和运行任何软件,包括操作系统和应用软件。

用户没有权限管理和访问底层的基础设施,如服务器、交换机、硬盘等,但是有权管理操作系统、存储内容,可以安装管理应用程序,甚至是有权管理网络组件。

简单的说用户使用IaaS,有权管理操作系统之上的一切功能。我们常见的IaaS服务有虚拟机、虚拟网络、以及存储。

PaaS

PaaS(Platform as a Service),是指平台即服务。是一种云计算服务,提供运算平台与解决方案服务。

还是使用共享厨房的例子来比喻。

餐厅除了提供厨房、厨具等,还提供了清洗干净切好的蔬菜、调好味道的肉、饺子皮和饺子馅等。用户要享受美食,首先需要炒菜、加热或者煮熟,但却不用做太多的前期多种复杂准备。

PaaS给用户提供的能力是使用由云服务提供商支持的编程语言、库、服务以及开发工具来创建、开发应用程序并部署在相关的基础设施上。

用户无需管理底层的基础设施,包括网络、服务器,操作系统或者存储。他们只能控制部署在基础设施中操作系统上的应用程序,配置应用程序所托管的环境的可配置参数。

SaaS

SaaS(Software-as-a-Service),意思为软件即服务,即通过网络提供软件服务。

SaaS平台供应商将应用软件统一部署在自己的服务器上,客户可以根据工作实际需求,通过互联网向厂商定购所需的应用软件服务,按定购的服务多少和时间长短向厂商支付费用,并通过互联网获得Saas平台供应商提供的服务。

和IaaS以及PaaS的共享厨房相比,SaaS更像外卖平台。

SaaS相当于餐厅直接为大家提供做好的菜肴,还提供外卖服务。用户只需要付费,就可以享受各种不同类型的美味佳肴,还可以在有外卖网点的地方直接点餐。

SaaS给用户提供的能力是使用在云基础架构上运行的云服务提供商的应用程序。可以通过轻量的客户端接口(诸如web浏览器(例如,基于web的电子邮件))或程序接口从各种客户端设备访问应用程序。

用户无需管理或控制底层云基础架构,包括网络,服务器,操作系统,存储甚至单独的应用程序功能,可能的例外是有限的用户特定应用程序配置设置。

根据SaaS应用是否具有可配置性,高性能,可伸缩性的特性,SaaS成熟度模型被分成四级。每一级都比前一级增加三种特性中的一种:

  • 多次开发
  • 这种模型下,软件服务提供商为每个客户定制一套软件,并为其部署。每个客户使用一个独立的数据库实例和应用服务器实例。数据库中的数据结构和应用的代码可能都根据客户需求做过定制化修改。
  • 一次开发多次部署
  • 通过不同的配置满足不同客户的需求,而不需要为每个客户进行特定定制,以降低定制开发的成本。
  • 但是,软件的部署架构没有太大的变化,依然为每个客户独立部署一个运行实例。只是每个运行实例运行的是同一份代码,通过配置的不同来满足不同客户的个性化需求。
  • 可配置性的比较通用的实现方式,就是通过MetaData(元数据)来实现。
  • 一次开发一次部署
  • 多租户单实例(Multi-Tenant)的应用架构才是通常真正意义上的SaaS应用架构,它可以有效降低SaaS应用的硬件及运行维护成本,最大化地发挥SaaS应用的规模效应。
  • 无需开发
  • 将第三级的Multi-Tenant SingleInstance系统扩展为Multi-Tenant MultiInstance。最终用户首先通过接入Tenant Load Balance层,再被分配到不同的Instance上。通过多个Instance来分担大量用户的访问,我们可以让应用实现近似无限的水平扩展。

总结

本文通过厨房最菜的例子介绍了云计算中的三种形式:IaaS、PaaS以及SaaS。

从IaaS到PaaS再到SaaS,服务商提供的服务及产品越来越多,而普通用户承担的工作量便越来越少。

从左到右,使用了对应的云服务后,用户承担的工作量(上图绿色部分)会越来越少,IaaS > PaaS > SaaS。

IaaS 是云服务的最底层,主要提供一些基础资源。常见的产品有Amazong EC2、阿里云等。

PaaS 提供软件部署平台,抽象掉了硬件和操作系统细节,可以无缝地扩展。开发者只需要关注自己的业务逻辑,不需要关注底层。常见的产品有Heroku等。

SaaS 是软件的开发、管理、部署都交给第三方,不需要关心技术问题,可以拿来即用。常见的产品有Gmail、PayPal等。

参考资料:
https://baike.baidu.com/item/IaaShttps://baike.baidu.com/item/PaaShttps://baike.baidu.com/item/saashttps://www.zhihu.com/question/20387284http://www.ruanyifeng.com/blog/2017/07/iaas-paas-saas.html![](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/00ec675705d4439545471567a9fe0532021ad35f02d9b2f77401ccf4b92ed402)

本文转载自: 掘金

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

精心整理「服务器Linux C/C++」 成长路程(附思维导

发表于 2020-02-09

前言

我不是名校毕业,更没有大厂的背景,我只是一个毕业不到 2 年的普普通通的程序员,在摸爬滚打的工作这段时间里,深知了有一个「完整的知识体系」是非常重要的。当事人非常后悔没有在大学期间知道这个道理……

在这里插入图片描述

众多大厂招人的需求也是非常注重此方面,毕竟我们不能单单只是一个只会写代码的程序员,更应该成为一个全面的工程师,能够迅速解决工作上的需求及众多问题。
在这里插入图片描述

特此,我根据众多大佬的书籍推荐和豆瓣的高分书籍总结了一份较为全面的「服务器Linux C/C++」 成长路程,我自己也是在跟着这份思维导图进一步的学习,希望这份路程也能伴随大家的成长。
「Linux C/C++ 成长路程」 思维导图

特此说明下:

  • 所有书籍资料和高清思维导图的获取的方式见文章末尾(机智的你,应该发现图就有途径)
  • 思维导图中的极客时间专栏,是我自己订阅的专栏,不是打广告,对此部分不感兴趣的小伙伴可以略过……

C 编程语言

入门:

《啊哈C语言》

《啊哈C语言!逻辑的挑战(修订版)》是一本非常有趣的编程启蒙书,书内容从中小学生的角度来讲述,没有生涩的内容,取而代之的是生动活泼的漫画和风趣幽默的文字。你可以在茶余饭后阅读,甚至坐在马桶上也可以看得津津有味。

进阶:

《C和指针》

本书提供与C语言编程相关的全面资源和深入讨论。本书通过对指针的基础知识和高级特性的探讨,帮助程序员把指针的强大功能融入到自己的程序中去。

《C专家编程》

本书展示了最优秀的C程序员所使用的编码技巧,并专门开辟了一章对C++的基础知识进行了介绍。

《C陷阱与缺陷》

本书的出发点不是要批判C语言,而是要帮助C程序员绕过编程过程中的陷阱和障碍

C++ 编程语言

入门:

《 C++ Primer 中文版(第 5 版) 》

这本久负盛名的 C++经典教程,时隔八年之久,终迎来史无前例的重大升级。除令全球无数程序员从中受益,甚至为之迷醉的——C++ 大师
Stanley B. Lippman 的丰富实践经验,C++标准委员会原负责人 Josée Lajoie 对C++标准的深入理解,以及C++
先驱 Barbara E. Moo 在 C++教学方面的真知灼见外,更是基于全新的
C++11标准进行了全面而彻底的内容更新。非常难能可贵的是,《C++ Primer 中文版(第5版)》所有示例均全部采用 C++11
标准改写,这在经典升级版中极其罕见——充分体现了 C++ 语言的重大进展及其全面实践。

进阶(Effective C 四部曲):

《Effective C++ 中文版(第3版)》

《Effective C++:改善程序与设计的55个具体做法》(中文版)(第3版)一共组织55个准则,每一条准则描述一个编写出更好的C++的方式。每一个条款的背后都有具体范例支撑。

《More Effective C++ 中文版》

《More Effective C++:35个改善编程与设计的有效方法(中文版)》是梅耶尔大师Effective思部曲之一。继Effective C++之后,Scott Meyers于1996推出这本《More Effective C++(35个改善编程与设计的有效方法)》“续集”。条款变得比较少,页数倒是多了一些,原因是这次选材比“一集”更高阶。

《Effective STL 中文版》

STL是C++标准库的一部分。本书是针对STL的经验总结,书中列出了50个条款,绝大多数条款都解释了在使用STL时应该注意的某一个方面的问题,并且详尽地分析了问题的来源、解决方案的优劣。

《Effective Modern C++ 中文版》

想要彻底理解C++11和C++14,不可止步于熟悉它们引入的语言特性(例如,auto型别推导、移动语义、lambda表达式以及并发支持)。挑战在于高效地运用这些特性——从而使你的软件具备正确性、高效率、可维护性和可移植性。这正是这本实用的图书意欲达成的定位。它描述的正是使用C++11和C++14——现代C++来撰写真正卓越的软件之道。

深入学习:

《深度探索C++对象模型》

这本书探索“对象导向程序所支持的C++对象模型”下的程序行为。对于“对象导向性质之基础实现技术”以及“各种性质背后的隐含利益交换”提供一个清楚的认识。检验由程序变形所带来的效率冲击。提供丰富的程序范例、图片,以及对象导向观念和底层对象模型之间的效率测量。

《 STL 源码剖析 》

学习编程的人都知道,阅读、剖析名家代码乃是提高水平的捷径。源码之前,了无秘密。大师们的缜密思维、经验结晶、技术思路、独到风格,都原原本本体现在源码之中。

Linux

Linux 基础知识:

《鸟哥的 Linux 私房菜》

本书是最具知名度的Linux入门书《鸟哥的Linux私房菜基础学习篇》的最新版,全面而详细地介绍了Linux操作系统。全书分为5个部分:第一部分着重说明Linux的起源及功能,如何规划和安装Linux主机;第二部分介绍Linux的文件系统、文件、目录与磁盘的管理;第三部分介绍文字模式接口
shell和管理系统的好帮手shell脚本,另外还介绍了文字编辑器vi和vim的使用方法;第四部分介绍了对于系统安全非常重要的Linux账号的管理,以及主机系统与程序的管理,如查看进程、任务分配和作业管理;第五部分介绍了系统管理员(root)的管理事项,如了解系统运行状况、系统服务,针对登录文件进行解析,对系统进行备份以及核心的管理等。

《Linux命令行与Shell脚本编程大全》

本书是一本关于Linux 命令行与shell 脚本编程的全面教程。全书分为四部分:第一部分介绍Linuxshell
命令行;第二部分介绍shell 脚本编程基础;第三部分深入探讨shell 脚本编程的高级内容;第四部分介绍如何在现实环境中使用shell
脚本。本书不仅涵盖了详尽的动手教程和现实世界中的实用信息,还提供了与所学内容相关的参考信息和背景资料。

《Linux程序设计 (第4版)》

本书是Linux程序设计领域的经典名著,以简单易懂、内容全面和示例丰富而受到广泛好评。中文版前两版出版后,在国内的Linux爱好者和程序员中也引起了强烈反响,这一热潮一直持续至今。本书是国内读者翘首以待的第4版,此次新版内容组织更加严谨,译者更是细心雕琢,保留了这部权威著作的原汁原味。

Linux 环境编程:

《Linux 高性能服务器编程》

本书是Linux服务器编程领域的经典著作,由资深Linux软件开发工程师撰写,从网络协议、服务器编程核心要素、原理机制、工具框架等多角度全面阐释了编写高性能Linux服务器应用的方法、技巧和思想。不仅理论全面、深入,抓住了重点和难点,还包含两个综合性案例,极具实战意义。

《UNIX 环境高级编程(第3版)》

《UNIX环境高级编程(第3版)》是被誉为UNIX编程“圣经”的Advanced Programming in the UNIX Environment一书的第3版。在本书第2版出版后的8年中,UNIX行业发生了巨大的变化,特别是影响UNIX编程接口的有关标准变化很大。本书在保持前一版风格的基础上,根据最新的标准对内容进行了修订和增补,反映了最新的技术发展。

Linux 网络编程:

《UNIX网络编程 卷1:套接字联网API(第3版)》

这是一部传世之作!顶级网络编程专家Bill Fenner和Andrew M. Rudoff应邀执笔,对W. Richard Stevens的经典作品进行修订。书中吸纳了近几年网络技术的发展,增添了IPv6、SCTP协议和密钥管理套接字等内容,深入讨论了最新的关键标准、实现和技术。

《UNIX网络编程 卷2:进程间通信(第2版)》

《UNIX网络编程.卷2:进程间通信(第2版)》是一部UNIX网络编程的经典之作!进程间通信(IPC)几乎是所有Unix程序性能的关键,理解IPC也是理解如何开发不同主机间网络应用程序的必要条件。

Linux 内核:

《深入理解linux内核 中文版(第3版)》

《深入理解Linux内核,第三版》指导你对内核中使用的最重要的数据结构、算法和程序设计诀窍进行一次遍历。通过对表面特性的探究,作者给那些想知道自己机器工作原理的人提供了颇有价值的见解。书中讨论了Intel特有的重要性质。相关的代码片段被逐行剖析。然而,本书涵盖的不仅仅是代码的功能,它解释了Linux以自己的方式工作的理论基础。

数据结构与算法

《算法导论》

这本书深入浅出,全面地介绍了计算机算法。对每一个算法的分析既易于理解又十分有趣,并保持了数学严谨性。本书的设计目标全面,适用于多种用途。涵盖的内容有:算法在计算中的作用,概率分析和随机算法的介绍。

计算机网络

TCP/IP 详解 三部曲:

《TCP/IP详解 卷1:协议》

《TCP/IP详解卷1:协议》是一本完整而详细的TCP/IP协议指南。描述了属于每一层的各个协议以及它们如何在不同操作系统中运行。

《TCP/IP详解·卷2:实现》

《TCP/IP详解·卷2:实现》完整而详细地介绍了TCP/IP协议是如何实现的。书中给出了约500个图例,15000行实际操作的C代码,采用举例教学的方法帮助你掌握TCP/IP实现。

《TCP/IP详解(卷3):CP事务协议.HP.P和UIX域协议》

《TCP/IP详解(卷3):CP事务协议.HP.P和UIX域协议》是“TCP/IP详解系列”的延续。主要内容包括:TCP事务协议,即T/TCP,这是对TCP的扩展,使客户-服务器事务更快、更高效和更可靠;TCP/IP应用,主要是HTTP和NNTP;UNIX域协议,这些协议提供了进程之间通信的一种手段。当客户与服务器进程在同一台主机上时,UNIX域协议通常要比TCP/IP快一倍。

计算机系统

《计算机是怎样跑起来的》

本书以图配文,以计算机的三大原则为开端、相继介绍了计算机的结构、手工汇编、程序流程、算法、数据结构、面向对象编程、数据库、TCP/IP 网络、数据加密、XML、计算机系统开发以及SE 的相关知识。

《程序是怎样跑起来的》

本书从计算机的内部结构开始讲起,以图配文的形式详细讲解了二进制、内存、数据压缩、源文件和可执行文件、操作系统和应用程序的关系、汇编语言、硬件控制方法等内容,目的是让读者了解从用户双击程序图标到程序开始运行之间到底发生了什么。

《深入理解计算机系统》

本书从程序员的视角详细阐述计算机系统的本质概念,并展示这些概念如何实实在在地影响应用程序的正确性、性能和实用性。全书共12章,主要内容包括信息的表示和处理、程序的机器级表示、处理器体系结构、优化程序性能、存储器层次结构、链接、异常控制流、虚拟存储器、系统级I/O、网络编程、并发编程等。书中提供大量的例子和练习,并给出部分答案,有助于读者加深对正文所述概念和知识的理解。

数据库

MySQL:

《MySQL必知必会》

《MySQL必知必会》MySQL是世界上最受欢迎的数据库管理系统之一。书中从介绍简单的数据检索开始,逐步深入一些复杂的内容,包括联结的使用、子查询、正则表达式和基于全文本的搜索、存储过程、游标、触发器、表约束,等等。通过重点突出的章节,条理清晰、系统而扼要地讲述了读者应该掌握的知识,使他们不经意间立刻功力大增。

《高性能mysql(第3版)》

《高性能mysql(第3版)》是mysql 领域的经典之作,拥有广泛的影响力。第3 版更新了大量的内容,不但涵盖了最新mysql 5.5版本的新特性,也讲述了关于固态盘、高可扩展性设计和云计算环境下的数据库相关的新内容,原有的基准测试和性能优化部分也做了大量的扩展和补充。全书共分为16 章和6 个附录,内容涵盖mysql 架构和历史,基准测试和性能剖析,数据库软硬件性能优化,复制、备份和恢复,高可用与高可扩展性,以及云端的mysql 和mysql相关工具等方面的内容。每一章都是相对独立的主题,读者可以有选择性地单独阅读。

Redis:

《Redis入门指南》

《Redis入门指南》是一本Redis的入门指导书籍,以通俗易懂的方式介绍了Redis基础与实践方面的知识,包括历史与特性、在开发和生产环境中部署运行Redis、数据类型与命令、使用Redis实现队列、事务、复制、管道、持久化、优化Redis存储空间等内容,并采用任务驱动的方式介绍了PHP、Ruby、Python和Node.js这4种语言的Redis客户端库的使用方法。

视频学习网

书籍 + 视频是很好的学习的方式,推荐两个学习资源较多的网站:

  • 中国大学MOOC
  • B站

END

本文的全部书籍+思维导图内容,关注公众号「小林coding」,后台回复「我要学习」,即可免费获取书籍资源+高清思维导图

在这里插入图片描述

本文转载自: 掘金

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

这几个库让 Django Web 开发事半功倍 前言 正文

发表于 2020-02-08

前言

Django 是 Python 语言开发的 Web 框架,因其功能强大,开发快速而广受欢迎。在 Github 上更是收获了 47K+ 的 Star,其社区和周边库也极其活跃,下面我就来分享一些我工作中经常使用的库,用好了事半功倍,大大增加开发效率,3 小时干一天的活,剩下时间可以好好划水(学习)!

除了列举,我还会结合实际使用情况作使用演示,方便大家根据需要筛选。以下内容较长,建议先收藏再看。

本文所有库均在 Django 2.1 以上实测过,部分库在 Django 1.11 以上都可以使用。

正文

Django Rest Framework

Django Rest Framework 是我最最推荐的,也是我开发 Django 服务的必用库。也许它的知名度已经足够大,不需要我展开介绍了,我就简单介绍下它的使用(详细的也不够写😂)

Django Rest Framework 给 Django 提供了一套 Restful 规范的 API,并提供了配套的一系列功能,如认证、鉴权、限速等,同时还提供了 UI 的测试界面。

1
复制代码pip install djangorestframework

结合 Django 的 Model,开发一个 Restful API 只需要 3 步。

1.1 创建 Model 的序列化类 Serializer

Serializer 是序列化类,用于 Model 对象和 API Json 结构的相互转换。最简单的实现如下所示。

基本的 Model Field 都有对应的 Serializer Field 自动映射,无需额外编写。同时还提供了一些特殊的 Field,如外键 ID 转换成其他字段,甚至可以在序列化时将整个关联对象引入。

1.2 创建视图 View

视图分为三类:方法视图,类视图,基于 Model 的 ViewSet。

a) 方法视图

方法视图就是一个方法,类似与 Django 的方法视图,只是加个装饰器就行了。

方法 api_view 的参数可以限定 Http 方法,默认是 GET,最后返回 Response 对象即可。

b) 类视图

类视图就是将 Http 方法映射到类的方法上。

c) ViewSet 视图

ViewSet 视图更简单,直接可以绑定 Model 和视图。

1.3 绑定路由

最后绑定路由就可以访问了。

可以用 Django 的 path 绑定或者用 router 对象。

还有其他的认证、鉴权、限速等强大功能,内容太多,这里就不展开了。

django-filter

这个库给 Django API 提供了筛选功能,配合 Rest Framework 使用,可以一行代码提供模型的强大筛选 API。

1
复制代码pip install django-filter

然后可以通过 URL 参数进行筛选。

1
复制代码http://example.com/api/users/1/?name=huoyan&age=20

Django CORS Headers

对于前后端分离的架构,后端 API 需要添加 CORS 相应头以提供跨域访问功能。可以自行在相应的每个 Header 中添加,当然现成的轮子有了,何乐而不为?

1
复制代码pip install django-cors-headers

使用非常简单,只要添加 installed_app 和 middlewares 就可以了。

同时提供了自定义的配置功能,可直接在 Django 的 settings 里配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_METHODS = (
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
'VIEW',
)

CORS_ALLOW_HEADERS = (
'XMLHttpRequest',
'X_FILENAME',
'accept-encoding',
'authorization',
'content-type',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
'Pragma',
)

Django Debug Toolbar

Django Debug Toolbar 提供了一个测试用的强大调试栏,可在浏览器中直接查看相应的配置、Http Header,SQL,日志等等,非常实用,开发调试必用。

1
复制代码pip install django-debug-toolbar

使用也非常简单。

需要配置 installed_app,静态资源路径和 Middlewares。

最后添加路由,判断 Debug 为 True 时启用。

然后我们访问 API 或者视图时会在侧边增加一个调试栏。

Django Environ

对于线上服务来说,多环境不同配置是必不可少的,这就需要 Django Environ 来出马了。Django Environ 可方便地将环境变量合并到 Django settings 配置中。

1
复制代码pip install django-environ

我们一般在 setting.py 中这么写

然后在需要动态配置的设置上获取值。

我们可以通过环境变量或者环境变量文件(唯一直接读取环境变量 ENV_FILE)来载入配置。

.env 文件类似这样(on/off 会被自动转换为布尔值)

1
2
3
4
复制代码DEBUG=on
CORS=on
LOG_LEVEL=INFO
LOG_FILE=app.log

同时可以直接转换数据库和缓存等配置为一个环境变量。

settings.py 中这样配置

然后环境变量或 .env 文件中这样写

1
2
复制代码DATABASE_URL=psql://postgres:123456@127.0.0.1:5432/postgres
CACHE_URL=rediscache://127.0.0.1:6379/1

这比直接用 os.environ.get 载入环境变量好在哪里呢?

  • 提供了环境变量文件载入,如果是本地调试,把所有变量写入文件当然是最方便的了。
  • 提供了统一的写环境变量初始值的地方(初始化的地方),同时可以指定变量数据类型。
  • 支持数据类型转换,因为环境变量值都是字符串,变量类型转换帮助我们方便地处理转换问题。
  • 支持多行环境变量。
  • 数据库、Redis 等支持一个 URL 环境变量值,不需要一堆环境变量了。
  • 支持环境变量引用。

Django-Redis

Django 中使用缓存很常见,又简单又好用的就是 redis 了。Django redis 库可以直接配置 redis 作为 Django 的缓存或 Session 后端,非常简单。

1
复制代码pip install django-redis

settings.py 中配置

结合上面的 django-environ 就更简单了,我一般都是结合使用,只需要一行配置代码,一个环境变量。

1
2
3
复制代码CACHES = {
'default': env.cache(),
}

环境变量

1
复制代码CACHE_URL=rediscache://127.0.0.1:6379/1

作为 Session 后端使用

1
2
复制代码SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"

django-rest-framework-simplejwt

Django Rest Framework 默认的鉴权方式是基于 Django 的(例如 Session 或者 Token),如果需要 JWT 的方式,现成的轮子也有。

1
复制代码pip install djangorestframework-simplejwt

修改 Rest Framekwork 的鉴权配置

然后添加 Token 的获取和刷新 API

使用如下请求就能获取 token 了

1
2
3
4
5
6
7
8
9
10
复制代码curl \
-X POST \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin"}' \
http://localhost:8000/api/token/

{
"access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU",
"refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImNvbGRfc3R1ZmYiOiLimIMiLCJleHAiOjIzNDU2NywianRpIjoiZGUxMmY0ZTY3MDY4NDI3ODg5ZjE1YWMyNzcwZGEwNTEifQ.aEoAYkSJjoWH1boshQAaTkf8G3yn0kapko6HFRt7Rh4"
}

临时 token 失效后这样刷新 token

1
2
3
4
5
6
7
复制代码curl \
-X POST \
-H "Content-Type: application/json" \
-d '{"refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImNvbGRfc3R1ZmYiOiLimIMiLCJleHAiOjIzNDU2NywianRpIjoiZGUxMmY0ZTY3MDY4NDI3ODg5ZjE1YWMyNzcwZGEwNTEifQ.aEoAYkSJjoWH1boshQAaTkf8G3yn0kapko6HFRt7Rh4"}' \
http://localhost:8000/api/token/refresh/

{"access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNTY3LCJqdGkiOiJjNzE4ZTVkNjgzZWQ0NTQyYTU0NWJkM2VmMGI0ZGQ0ZSJ9.ekxRxgb9OKmHkfy-zs1Ro_xs1eMLXiR17dIDBVxeT-w"}

在前后端分离的项目上,我们前端使用 axios 的拦截器自动刷新,业务无需关心,代码非常简洁。

Grappelli

Grappelli 是 Django admin 管理界面的皮肤,提供了更友好的使用体验。

1
复制代码pip install django-grappelli

使用也很简单。

配置一下 installed_app

1
2
3
4
复制代码INSTALLED_APPS = (
'grappelli',
'django.contrib.admin',
)

添加路由

1
2
3
4
复制代码urlpatterns = [
path('grappelli/', include('grappelli.urls')), # grappelli URLS
path('admin/', admin.site.urls), # admin site
]

添加请求上下文处理器

1
2
3
4
5
6
7
8
9
10
11
12
复制代码TEMPLATES = [
{
...
'OPTIONS': {
'context_processors': [
...
'django.template.context_processors.request',
...
],
},
},
]

然后重新处理一下静态文件就好了。

1
复制代码python manage.py collectstatic

django-celery-results/django-celery-beat

如果你的 Django 服务需要用到 Celery 来做异步任务,那么这两个库还不错,所以写在一起。

django-celery-results 是将 Django 的 ORM 用作 Celery 异步任务的存储结果,在需要将异步任务的结果长期保存及分析时,可以用它来存储到 Django 配置的数据库中。

1
复制代码pip install django-celery-results

简单配置

执行数据库创建

1
复制代码python manage.py migrate django_celery_results

而 django-celery-beat 是将 Celery 的定时任务配置转移到 Django 的数据库中存储,如果我们业务的管理后台需要定制定时任务,非常好用。

1
复制代码pip install django-celery-beat

简单配置

执行数据库创建

1
复制代码python manage.py migrate

最后在启动 Celery Beat 的时候指定 scheduler

1
复制代码celery -A proj beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler

后记

今天先写这么多了,这些都是这几年 Django 开发积累下来的精选库,如果大家使用中有任何疑问,欢迎评论,找我讨论。如果个别库使用人数多的话,可以再写篇详细使用及避坑指南。如果觉得有用,千万不要吝惜点赞收藏哦!

我是火眼君,愿我的写作,驱散心灵的孤单。

参考

  • Django
  • Django Rest Framework
  • django-filter
  • Django CORS Headers
  • Django Debug Toolbar
  • Django Environ
  • Django-Redis
  • django-rest-framework-simplejwt
  • Grappelli
  • django-celery-results
  • django-celery-beat
  • zhuanlan.zhihu.com/p/105009911

本文转载自: 掘金

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

在家办公7天整理Spring Cloud知识点大全

发表于 2020-02-07

前言:

今天一觉起来,发现我们开工的日期又延迟了,虽然已经在家办公一个多礼拜了,但是由于家里的环境还是不能有很高的效率。于是干脆就对Spring Cloud的一些知识点做了一些整理。

整理了一些 Spring Cloud 相关面试题,大家测测自己对 Spring Cloud 掌握情况,看看自己的自测结果。

什么是Spring Cloud?

spring cloud 是一系列框架的有序集合。它利用 spring boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 spring boot 的开发风格做到一键启动和部署。

使用Spring Cloud有什么优势?

使用Spring Boot开发分布式微服务时,我们面临以下问题

  • 与分布式系统相关的复杂性-这种开销包括网络问题,延迟开销,带宽问题,安全问题。
  • 服务发现-服务发现工具管理群集中的流程和服务如何查找和互相交谈。它涉及一个服务目录,在该目录中注册服务,然后能够查找并连接到该目录中的服务。
  • 冗余-分布式系统中的冗余问题。
  • 负载平衡 –负载平衡改善跨多个计算资源的工作负荷,诸如计算机,计算机集群,网络链路,中央处理单元,或磁盘驱动器的分布。
  • 性能-问题 由于各种运营开销导致的性能问题。
  • 部署复杂性-Devops技能的要求。

Spring Cloud 实现服务注册和发现的原理是什么?

  • 服务在发布时指定对应的服务名(服务名包括了 IP 地址和端口)将服务注册到注册中心(Eureka 或者 Zookeeper)这一过程是 Spring Cloud 自动实现的,只需要在 main 方法添加 @EnableDisscoveryClient 即可,同一个服务修改端口就可以启动多个实例。
  • 调用方法:传递服务名称通过注册中心获取所有的可用实例,通过负载均衡策略调用(Ribbon 和 Feign)对应的服务。

为什么要使用 Spring Cloud 熔断器?

当一个服务调用另一个服务,由于网络原因或者自身原因出现问题时 ,调用者就会等待被调者的响应,当更多的服务请求到这些资源时,导致更多的请求等待,这样就会发生连锁效应,断路器就是解决这一问题的。

断路器的状态有以下几种:

  • 完全打开:一定时间内,达到一定的次数无法调用,并且多次检测没有恢复的迹象,断路器完全打开,那么下次的请求不会请求到该服务。
  • 半开:短时间内有恢复迹象,断路器会将部分请求发送给服务,当能正常调用时,断路器关闭。
  • 关闭:服务一直处于正常状态,能正常调用,断路器关闭。

服务注册和发现是什么意思?Spring Cloud如何实现?

当我们开始一个项目时,我们通常在属性文件中进行所有的配置。随着越来越多的服务开发和部署,添加和修改这些属性变得更加复杂。有些服务可能会下降,而某些位置可能会发生变化。手动更改属性可能会产生问题。Eureka服务注册和发现可以在这种情况下提供帮助。由于所有服务都在Eureka服务器上注册并通过调用Eureka服务器完成查找,因此无需处理服务地点的任何更改和处理。

spring cloud 和dubbo区别?

  • 服务调用方式 dubbo是RPC springcloud Rest Api
  • 注册中心,dubbo 是zookeeper springcloud是eureka,也可以是zookeeper
  • 服务网关,dubbo本身没有实现,只能通过其他第三方技术整合,springcloud有Zuul路由网关,作为路由服务器,进行消费者的请求分发,springcloud支持断路器,与git完美集成配置文件支持版本控制,事物总线实现配置文件的更新与服务自动装配等等一系列的微服务架构要素。

微服务之间是如何独立通讯的

1.远程过程调用(Remote Procedure Invocation):也就是我们常说的服务的注册与发现,直接通过远程过程调用来访问别的service。

优点:

  • 简单,常见,因为没有中间件代理,系统更简单

缺点:

  • 只支持请求/响应的模式,不支持别的,比如通知、请求/异步响应、发布/订阅、发布/异步响应
  • 降低了可用性,因为客户端和服务端在请求过程中必须都是可用的

2.消息:使用异步消息来做服务间通信。服务间通过消息管道来交换消息,从而通信。

优点:

  • 把客户端和服务端解耦,更松耦合
  • 提高可用性,因为消息中间件缓存了消息,直到消费者可以消费
  • 支持很多通信机制比如通知、请求/异步响应、发布/订阅、发布/异步响应

缺点:

  • 消息中间件有额外的复杂

负载均衡的意义是什么?

在计算中,负载均衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载均衡旨在优化资源使用,最大吞吐量,最小响应时间并避免任何单一资源的过载。使用多个组件进行负载均衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务进程。

微服务之间是如何独立通讯的?

1.远程调用,比如feign调用,直接通过远程过程调用来访问别的service。
2.消息中间件

springcloud如何实现服务的注册?

1.服务发布时,指定对应的服务名,将服务注册到 注册中心(eureka zookeeper)

2.注册中心加@EnableEurekaServer,服务用@EnableDiscoveryClient,然后用ribbon或feign进行服务直接的调用发现。

spring cloud 断路器的作用是什么?

在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。

什么是Hystrix?

Hystrix 是一个延迟和容错库,旨在隔离远程系统,服务和第三方库的访问点,当出现故障是不可避免的故障时,停止级联故障并在复杂的分布式系统中实现弹性。通常对于使用微服务架构开发的系统,涉及到许多微服务,这些微服务彼此协作, 随着微服务数量的增加,这个问题变得更加复杂。我们将使用 Hystrix 的 Fallback 方法来处理,假设由于某种原因,公开的服务接口抛出异常,我们在这种情况下使用 Hystrix 定义一个回退方法。这种后备方法应该具有与公开服务相同的返回类型,如果暴露服务中出现异常,回退方法将返回对应信息。

Eureka和ZooKeeper都可以提供服务注册与发现的功能,请说说两个的区别

  • ZooKeeper保证的是CP,Eureka保证的是AP,ZooKeeper在选举期间注册服务瘫痪,虽然服务最终会恢复,但是选举期间不可用的。Eureka各个节点是平等关系,只要有一台Eureka就可以保证服务可用,而查询到的数据并不是最新的自我保护机制会导致Eureka不再从注册列表移除因长时间没收到心跳而应该过期的服务。Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点(高可用)。当网络稳定时,当前实例新的注册信息会被同步到其他节点中(最终一致性)。Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像ZooKeeper一样使得整个注册系统瘫痪。
  • ZooKeeper有Leader和Follower角色,Eureka各个节点平等
  • ZooKeeper采用过半数存活原则,Eureka采用自我保护机制解决分区问题
  • Eureka本质上是一个工程,而ZooKeeper只是一个进程

什么是Netflix Feign?它的优点是什么?

Feign是受到Retrofit,JAXRS-2.0和WebSocket启发的java客户端联编程序。Feign的第一个目标是将约束分母的复杂性统一到http apis,而不考虑其稳定性。在employee-consumer的例子中,我们使用了employee-producer使用REST模板公开的REST服务。

但是我们必须编写大量代码才能执行以下步骤

  • 使用功能区进行负载平衡。
  • 获取服务实例,然后获取基本URL。
  • 利用REST模板来使用服务。前面的代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码@Controller
public class ConsumerControllerClient {
​
@Autowired
private LoadBalancerClient loadBalancer;
​
public void getEmployee() throws RestClientException, IOException {
​
ServiceInstance serviceInstance=loadBalancer.choose("employee-producer");
​
System.out.println(serviceInstance.getUri());
​
String baseUrl=serviceInstance.getUri().toString();
​
baseUrl=baseUrl+"/employee";
​
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response=null;
try{
response=restTemplate.exchange(baseUrl,
HttpMethod.GET, getHeaders(),String.class);
}catch (Exception ex)
{
System.out.println(ex);
}
System.out.println(response.getBody());
}

之前的代码,有像NullPointer这样的例外的机会,并不是最优的。我们将看到如何使用Netflix Feign使呼叫变得更加轻松和清洁。如果Netflix Ribbon依赖关系也在类路径中,那么Feign默认也会负责负载平衡。

REST 和RPC对比

1.RPC主要的缺陷是服务提供方和调用方式之间的依赖太强,需要对每一个微服务进行接口的定义,并通过持续继承发布,严格版本控制才不会出现冲突。

2.REST是轻量级的接口,服务的提供和调用不存在代码之间的耦合,只需要一个约定进行规范。

什么是feigin?它的优点是什么?

  • feign采用的是基于接口的注解
  • feign整合了ribbon,具有负载均衡的能力
  • 整合了Hystrix,具有熔断的能力

使用:

1.添加pom依赖。

2.启动类添加@EnableFeignClients

3.定义一个接口@FeignClient(name=“xxx”)指定调用哪个服务

Ribbon和Feign的区别?

  • Ribbon都是调用其他服务的,但方式不同。
  • 启动类注解不同,Ribbon是@RibbonClient feign的是@EnableFeignClients
  • 服务指定的位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。
  • 调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTemplate发送给其他服务,步骤相当繁琐。Feign需要将调用的方法定义成抽象方法即可。

什么是Spring Cloud Bus?

spring cloud bus 将分布式的节点用轻量的消息代理连接起来,它可以用于广播配置文件的更改或者服务直接的通讯,也可用于监控。

如果修改了配置文件,发送一次请求,所有的客户端便会重新读取配置文件。

使用:

1.添加依赖

2.配置rabbimq

eureka和zookeeper都可以提供服务注册与发现的功能,请说说两个的区别?

zookeeper 是CP原则,强一致性和分区容错性。

eureka 是AP 原则 可用性和分区容错性。

zookeeper当主节点故障时,zk会在剩余节点重新选择主节点,耗时过长,虽然最终能够恢复,但是选取主节点期间会导致服务不可用,这是不能容忍的。

eureka各个节点是平等的,一个节点挂掉,其他节点仍会正常保证服务。

你所知道微服务的技术栈有哪些?列举一二

服务网关的作用

  • 简化客户端调用复杂度,统一处理外部请求。
  • 数据裁剪以及聚合,根据不同的接口需求,对数据加工后对外。
  • 多渠道支持,针对不同的客户端提供不同的网关支持。
  • 遗留系统的微服务化改造,可以作为新老系统的中转组件。
  • 统一处理调用过程中的安全、权限问题。

链路跟踪Sleuth

当我们项目中引入Spring Cloud Sleuth后,每次链路请求都会添加一串追踪信息,格式是[server-name, main-traceId,sub-spanId,boolean]:

  • server-name:服务结点名称。
  • main-traceId:一条链路唯一的ID,为TraceID。
  • sub-spanId:链路中每一环的ID,为SpanID。
  • boolean:是否将信息输出到Zipkin等服务收集和展示。

Sleuth的实现是基于HTTP的,为了在数据的收集过程中不能影响到正常业务,Sleuth会在每个请求的Header上添加跟踪需求的重要信息。这样在数据收集时,只需要将Header上的相关信息发送给对应的图像工具即可,图像工具根据上传的数据,按照Span对应的逻辑进行分析、展示。

好了各位, 本文到这里就结束了! 如果本文有任何错误,请批评指教,不胜感激 !

我是提莫! 一个节操泛滥,一身凛然正气,刚正不阿的Java程序员

本文转载自: 掘金

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

1 小时上线之用 Flask 开发一个短信微服务 前言 开始

发表于 2020-02-07

前言

Flask 是 Python 开发的轻量 Web 框架,有多轻量呢?10 行以内就可以开发一个 Web 服务,不过这只能用来做演示,今天我就用 1 个小时来开发一个用于生产环境的短信微服务。以下是我们生产环境脱敏后直接可用的服务代码,绝非示例教程。

为什么要开发短信微服务?

短信服务我们都是依赖公有云的实现,通过公有云的 API 直接调用,那为什么还要自己封装呢?

  • 因为微服务环境下我们要减少代码的重复量,如果有多个微服务需要使用短信服务,那就要复制多遍代码,把公有云的 API 包装成我们自己的微服务 API 可以将代码的复制减少为一行 Http 请求。
  • 调用 API 的 accesskey 和 secret 不需要复制给多个服务,减少安全风险。
  • 可以根据我们的业务需求加入共用的业务逻辑。

多了一层调用有没有性能影响?

多了一层调用是多了一个网络请求,但是影响微乎其微。我们不可能因为面向对象的方式太多调用就写逐行执行的代码吧。

  • 公有云短信服务本就是异步调用,错误处理也是异步回调的方式。
  • 微服务内部网络的调用应该是非常快的,可以同虚拟机部署或者同机房部署。

开始

首先我们建立项目的骨架。

为什么要建立项目的骨架呢?

因为 Flask 太过于轻量,所以例如配置、路由等规范需要由开发人员自己定义。一般成熟的开发团队都有自己的一套开发骨架,要统一配置,统一开发规范,统一集成相关系统等。我这里就分享一套适用于生产环境的非常简单的开发骨架。

新建一个项目目录,然后在里面建立 app 和 config 两个 Python 目录。app 用于存放业务相关代码,config 用于存放配置相关代码。

配置类

在 config/config.py 中添加如下内容,配置的设计因人而异,Flask 也没有做任何限制。我这里的设计是使用 BaseConfig 作为配置基类,存放所有共用的配置,而不同的环境使用不同的配置子类,子类只需要修改特定的值就可以,便于查看。

如果配置的值需要在运行是注入(如数据库连接等),则可以使用环境变量的方式(如下面的 SECRET_KEY),我同时使用 or 提供了没有环境变量的默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
复制代码import os


class BaseConfig:
"""
配置基类,用于存放共用的配置
"""
SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(16)
DEBUG = False
TESTING = False


class ProductionConfig(BaseConfig):
"""
生产环境配置类,用于存放生产环境的配置
"""
pass


class DevelopmentConfig(BaseConfig):
"""
开发环境配置类,用于存放开发环境的配置
"""
DEBUG = True


class TestingConfig(BaseConfig):
"""
测试环境配置类,用于存放开发环境的配置
"""
DEBUG = True
TESTING = True


registered_app = [
'app'
]

config_map = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig
}

至于后面的 registered_app 和 config_map 有什么用?可以做自动注入,这个我后面会讲。

然后我加一个日志的配置,日志的配置非常重要,不同的开发团队往往有一套规范的日志配置模版,一般不会改变,所以可以直接定义在代码里,也可以用配置文件的方式。

config/logger.py

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


def config_logger(enable_console_handler=True, enable_file_handler=True, log_file='app.log', log_level='ERROR',
log_file_max_bytes=5000000, log_file_max_count=5):
# 定义输出到控制台的日志处理器
console_handler = {
'class': 'logging.StreamHandler',
'formatter': 'default',
'level': log_level,
'stream': 'ext://flask.logging.wsgi_errors_stream'
}
# 定义输出到文件的日志处理器
file_handler = {
'class': 'logging.handlers.RotatingFileHandler',
'formatter': 'detail',
'filename': log_file,
'level': log_level,
'maxBytes': log_file_max_bytes,
'backupCount': log_file_max_count
}
# 定义日志输出格式
default_formatter = {
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
}
detail_formatter = {
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
}
handlers = []
if enable_console_handler:
handlers.append('console')
if enable_file_handler:
handlers.append('file')
d = {
'version': 1,
'formatters': {
'default': default_formatter,
'detail': detail_formatter
},
'handlers': {
'console': console_handler,
'file': file_handler
},
'root': {
'level': log_level,
'handlers': handlers
}
}
dictConfig(d)

上面就是一个典型的 Python 日志配置方法,把可变的部分定义为参数(日志文件、级别等),定义了两个日志处理器(文件和控制台),使用时只需要调用这个方法即可。

应用类

定义好配置,我们就开始创建我们的 Flask 应用了。用过 Flask 的同学知道,创建 Flask 应用只需要一行代码。

1
复制代码app = Flask(__name__)

但这不是生产可用的方式,为了生产和测试方便,我们需要用一个方法获取这个 app 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码def create_app(conf=None):
# initialize logger
register_logger()
# check instance path
instance_path = os.environ.get('INSTANCE_PATH') or None
# create and configure the app
app = Flask(__name__, instance_path=instance_path)
if not conf:
conf = get_config_object()
app.config.from_object(conf)
# ensure the instance folder exists
if app.instance_path:
try:
os.makedirs(app.instance_path)
except OSError:
pass
# register app
register_app(app)
return app

这里做了几个事情,一是注册日志类,二是载入配置对象,三是创建 instance 目录,四是注册应用业务。

为什么注册日志要放在第一行?

不少开发人员会把日志的配置放在配置类里,这个没太大问题,只是越早注册日志,你的日志就会越早开始收集。如果载入配置类后才配置日志,那如果创建 app 时报错就无法被我们定义的日志收集器收集到了。

注册日志的方法可以这样写

1
2
3
4
5
6
7
8
9
复制代码def register_logger():
log_level = os.environ.get('LOG_LEVEL') or 'INFO'
log_file = os.environ.get('LOG_FILE') or 'app.log'
config_logger(
enable_console_handler=True,
enable_file_handler=True,
log_level=log_level,
log_file=log_file
)

我还是从环境变量里获取配置,并调用之前的配置函数配置日志。

载入配置对象的方法。

1
2
3
4
5
6
7
8
9
10
11
复制代码def get_config_object(env=None):
if not env:
env = os.environ.get('FLASK_ENV')
else:
os.environ['FLASK_ENV'] = env
if env in config.config_map:
return config.config_map[env]
else:
# set default env if not set
env = 'production'
return config.config_map[env]

从 FLASK_ENV 这个环境变量获取运行的环境,然后根据之前配置类里的 config_map 获取对应的配置类,实现配置类的载入。

最后就是注册我们的业务代码。

1
2
3
4
5
复制代码def register_app(app):
for a in config.registered_app:
module = importlib.import_module(a)
if hasattr(module, 'register'):
getattr(module, 'register')(app)

这里就用到了配置类里的 registered_app 列表,这里定义了要载入的模块,对于微服务来说,一般只有一个模块。

我这里还需要 app/__init__.py 文件里有个 register 方法,这个方法来执行具体的注册操作,例如注册 Flask 蓝图。

1
2
3
复制代码def register(app):
api_bp = Blueprint('api', __name__, url_prefix='/api')
app.register_blueprint(api_bp)

为什么要搞个 register 方法?

因为每个业务模块有自己的路由、ORM 或蓝图等,这是业务自己的代码,必须与骨架解耦。用一个特定的方法作为规范一是便于自定义的代码扩展,二是便于团队理解,不需要灵活的配置,这里约定大于配置。当然你可以有自己的另一套实现。

我把上面的代码整理为 application.py 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
复制代码import os
import importlib
from flask import Flask
from config.logger import config_logger
from config import config


def register_logger():
log_level = os.environ.get('LOG_LEVEL') or 'INFO'
log_file = os.environ.get('LOG_FILE') or 'app.log'
config_logger(
enable_console_handler=True,
enable_file_handler=True,
log_level=log_level,
log_file=log_file
)


def register_app(app):
for a in config.registered_app:
module = importlib.import_module(a)
if hasattr(module, 'register'):
getattr(module, 'register')(app)


def get_config_object(env=None):
if not env:
env = os.environ.get('FLASK_ENV')
else:
os.environ['FLASK_ENV'] = env
if env in config.config_map:
return config.config_map[env]
else:
# set default env if not set
env = 'production'
return config.config_map[env]


def create_app_by_config(conf=None):
# initialize logger
register_logger()
# check instance path
instance_path = os.environ.get('INSTANCE_PATH') or None
# create and configure the app
app = Flask(__name__, instance_path=instance_path)
if not conf:
conf = get_config_object()
app.config.from_object(conf)
# ensure the instance folder exists
if app.instance_path:
try:
os.makedirs(app.instance_path)
except OSError:
pass
# register app
register_app(app)
return app


def create_app(env=None):
conf = get_config_object(env)
return create_app_by_config(conf)

这里提供了 create_app_by_config 方法用于从配置类直接创建 app 对象,主要是便于单元测试时直接注入特定的配置类。

我们的骨架基本上就成型了,包括了最基础的配置类、日志配置和应用注册机制。然后就可以运行我们的 Flask 应用了。

开发测试

Flask 提供了 flask run 命令来运行测试应用,不过还需要提供 FLASK_APP 和 FLASK_ENV 两个环境变量来启动,这步我们也可以简化下。

编写 run.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码import click
from envparse import env
from application import create_app


@click.command()
@click.option('-h', '--host', help='Bind host', default='localhost', show_default=True)
@click.option('-p', '--port', help='Bind port', default=8000, type=int, show_default=True)
@click.option('-e', '--env', help='Running env, override environment FLASK_ENV.', default='development', show_default=True)
@click.option('-f', '--env-file', help='Environment from file', type=click.Path(exists=True))
def main(**kwargs):
if kwargs['env_file']:
env.read_envfile(kwargs['env_file'])
app = create_app(kwargs['env'])
app.run(host=kwargs['host'], port=kwargs['port'])


if __name__ == '__main__':
main()

这里用 click 创建了一个简单的命令行脚本,可以通过命令行参数直接启动一个测试用服务。当然默认参数直接可用,使用 python run.py 或者 IDE 里右键运行即可。同时,还提供了 env-file 选项,用户可提供环境变量的文件。

为什么要使用环境变量文件?

因为生产环境和开发环境的许多配置是不同的,例如公有云密钥,数据库连接等,这些信息是绝对不能提交到 git 等版本控制软件的,所以我们可以创建一个 .env 文件如下

1
2
复制代码ACCESS_KEY=xxx
ACCESS_SECRET=xxx

把这个文件加入 gitignore 中,然后使用 --env-file 载入这个文件就可以在开发环境中直接使用了,而不需要每次都手动输入了。

部署

生产环境我们肯定不会使用测试的方式启动,需要类似 gunicorn 等工具启动一个正式服务,我们也可以使用 Docker 等容器技术把生产部署过程自动化。

编写 server.py

1
2
3
复制代码from application import create_app

app = create_app()

这里很简单,创建一个 Flask app 对象即可,然后可以通过 gunicorn server:app 启动。

编写 requirements.txt 文件,用于自动安装依赖。后期可以把用到的依赖写进去。

1
2
3
4
5
复制代码flask
flask-restful
click
envparse
gunicorn

编写 Dockerfile 文件

1
2
3
4
5
6
复制代码FROM python:3.8

COPY . /opt
WORKDIR /opt
RUN pip install --no-cache-dir -r requirements.txt
CMD ["gunicorn", "-b", "0.0.0.0:80", "server:app"]

然后就可以使用如下命令用 Docker 启动服务容器了。

1
2
复制代码docker build -t myapp:0.1 .
docker run -d --name myapp -p 80:80 myapp:0.1

至此,一个简单的 Flask 骨架就完成了,大家可以在下面看到完整的项目。

Github Flask 骨架示例

编写业务

上面大概用了 20 分钟搞了个 Flask 的骨架,对于开发团队来说,骨架只要开发一次,后续的项目直接克隆就行了。下面我们就来编写具体的发送短信业务。

使用哪个公有云?

实际业务中我们可能使用单一一个云,也可能混合使用多个云。在我们的实际业务中,具体用哪个公有云的服务,不是取决于我们,而是取决于谁的价格低,谁的优惠多,谁的功能强。😄

所以我们可以提取短信业务的共性写一个抽象类。短信服务的共同点主要有短信模版,签名,接收人,模版参数等。

一个简单的抽象类

1
2
3
4
5
6
7
复制代码class SmsProvider:

def __init__(self, **kwargs):
self.conf = kwargs

def send(self, template, receivers, **kwargs):
pass

然后有基于阿里云的实现,以下代码根据官方示例修改

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

def send(self, template, receivers, **kwargs):
from aliyunsdkcore.request import CommonRequest
client = self.get_client(self.conf['app_key'], self.conf['app_secret'], self.conf['region_id'])
request = CommonRequest()
request.set_accept_format('json')
request.set_domain(self.conf['domain'])
request.set_method('POST')
request.set_protocol_type('https')
request.set_version(self.conf['version'])
request.set_action_name('SendSms')
request.add_query_param('RegionId', self.conf['region_id'])
request.add_query_param('PhoneNumbers', receivers)
request.add_query_param('SignName', self.conf['sign_name'])
request.add_query_param('TemplateCode', self.get_template_id(template))
request.add_query_param('TemplateParam', self.build_template_params(**kwargs))
return client.do_action_with_exception(request)

def get_template_id(self, name):
if name in self.conf['template_id_map']:
return self.conf['template_id_map'][name]
else:
raise ValueError('no template {} found!'.format(name))

@staticmethod
def get_client(app_key, app_secret, region_id):
from aliyunsdkcore.client import AcsClient
return AcsClient(app_key, app_secret, region_id)

@staticmethod
def build_template_params(**kwargs):
if 'params' in kwargs and kwargs['params']:
return json.dumps(kwargs['params'])
else:
return ''

然后在 BaseConfig 添加以下配置,是一些公有云 API 的基本配置,需要在运行是通过环境变量载入,其中 template_id_map 里的内容是模版的名称和对应的 ID,用于区分不同的短信模版,如验证码,推广等,名称作为参数供调用方使用,避免了直接传递 ID。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码    # SMS config
SMS_CONF = {
'aliyun': {
'provider_cls': 'app.sms.AliyunSmsProvider',
'config': {
'domain': 'dysmsapi.aliyuncs.com',
'version': os.environ.get('ALIYUN_SMS_VERSION') or '2017-05-25',
'app_key': os.environ.get('ALIYUN_SMS_APP_KEY'),
'app_secret': os.environ.get('ALIYUN_SMS_APP_SECRET'),
'region_id': os.environ.get('ALIYUN_SMS_REGION_ID'),
'sign_name': os.environ.get('ALIYUN_SMS_SIGN_NAME'),
'template_id_map': {
'captcha': 'xxx'
}
}
}
}

其中模版 ID,签名,App Key,App Secret 需要在阿里云控制台获取,模版和签名需要审核后才能获得。

同样的方法可以添加华为云的 API,也可直接从示例修改,只是华为云暂时没有 SDK,需要通过 API 调用,大同小异。

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

def send(self, template, receivers, **kwargs):
header = {'Authorization': 'WSSE realm="SDP",profile="UsernameToken",type="Appkey"',
'X-WSSE': self.build_wsse_header(self.conf['app_key'], self.conf['app_secret'])}
form_data = {
'from': self.conf['sender'],
'to': receivers,
'templateId': self.get_template_id(template),
'templateParas': self.build_template_params(**kwargs),
}
r = requests.post(self.conf['url'], data=form_data, headers=header, verify=False)
return r

def get_template_id(self, name):
if name in self.conf['template_id_map']:
return self.conf['template_id_map'][name]
else:
raise ValueError('no template {} found!'.format(name))

@staticmethod
def build_wsse_header(app_key, app_secret):
now = time.strftime('%Y-%m-%dT%H:%M:%SZ')
nonce = str(uuid.uuid4()).replace('-', '')
digest = hashlib.sha256((nonce + now + app_secret).encode()).hexdigest()
digest_base64 = base64.b64encode(digest.encode()).decode()
return 'UsernameToken Username="{}",PasswordDigest="{}",Nonce="{}",Created="{}"'.format(app_key, digest_base64,
nonce, now)

@staticmethod
def build_template_params(**kwargs):
if 'params' in kwargs and kwargs['params']:
return json.dumps(list(kwargs['params'].values()))
else:
return ''

也是添加配置,最后的 BaseConfig 如下所示,其中 SMS_PROVIDER 配置指定 SMS_CONF 的键,指定我们现在使用的是哪个公有云服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
复制代码class BaseConfig:
SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(16)
DEBUG = False
TESTING = False

# SMS config
SMS_PROVIDER = os.environ.get('SMS_PROVIDER')
SMS_CONF = {
'aliyun': {
'provider_cls': 'app.sms.AliyunSmsProvider',
'config': {
'domain': 'dysmsapi.aliyuncs.com',
'version': os.environ.get('ALIYUN_SMS_VERSION') or '2017-05-25',
'app_key': os.environ.get('ALIYUN_SMS_APP_KEY'),
'app_secret': os.environ.get('ALIYUN_SMS_APP_SECRET'),
'region_id': os.environ.get('ALIYUN_SMS_REGION_ID'),
'sign_name': os.environ.get('ALIYUN_SMS_SIGN_NAME'),
'template_id_map': {
'captcha': 'xxx'
}
}
},
'huawei': {
'provider_cls': 'app.sms.HuaweiSmsProvider',
'config': {
'url': os.environ.get('HUAWEI_URL'),
'app_key': os.environ.get('HUAWEI_SMS_APP_KEY'),
'app_secret': os.environ.get('HUAWEI_SMS_APP_SECRET'),
'sender': os.environ.get('HUAWEI_SMS_SENDER_ID'),
'template_id_map': {
'captcha': 'xxx'
}
}
}
}

其他的公有云也可以通过类似的方式添加。

然后我们添加一个方法,获取 Provider 的单例对象。这里使用 Flask 的 g 对象,把我们的 Provider 对象注册成全局的单例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码from flask import g, current_app
from werkzeug.utils import import_string


def create_sms():
provider = current_app.config['SMS_PROVIDER']
sms_config = current_app.config['SMS_CONF']
if provider in sms_config:
cls = sms_config[provider]['provider_cls']
conf = sms_config[provider]['config']
sms = import_string(cls)(**conf)
return sms
return None


def get_sms():
if 'sms' not in g:
g.sms = create_sms()
return g.sms

这些都完成后,就可以添加一个视图类,这里用到了 Flask-Restful 库,生成 API 视图。

app/api/sms.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码import logging
from flask_restful import Resource, reqparse
from app.sms import get_sms


# 定义参数,参考 https://flask-restful.readthedocs.io/en/latest/reqparse.html
parser = reqparse.RequestParser(bundle_errors=True)
parser.add_argument('receivers', help='Comma separated receivers.', required=True)
parser.add_argument('template', help='Notification template name.', required=True)
parser.add_argument('params', help='Notification template params.', type=dict)


class Sms(Resource):

def post(self):
args = parser.parse_args()
sms = get_sms()
try:
res = sms.send(**args)
except Exception as e:
logging.error(e)
return {'message': 'failed'}, 500
if res.status_code < 300:
return {'message': 'send'}, 200
else:
logging.error('Send sms failed with {}'.format(res.text))
return {'message': 'failed'}, 500

然后我们定义路由。

app/api/__init__.py

1
2
3
4
5
6
7
8
9
10
复制代码from flask import Blueprint
from flask_restful import Api
from app.api.health import Health
from app.api.sms import Sms


api_bp = Blueprint('api', __name__, url_prefix='/api')
api = Api(api_bp)

api.add_resource(Sms, '/sms')

最后记得在我们的应用 app 模块里注册蓝图。

app/__init__.py

1
2
3
4
5
6
复制代码from app.api import api_bp


# register blueprint
def register(app):
app.register_blueprint(api_bp)

至此,我们的短信微服务就完成了。可以通过我们上面的方法进行测试和部署。

其中我们定义了一些环境变量,在测试时可通过环境变量文件载入,运行时可通过容器的环境变量载入。放在 instance 目录下是因为 instance 是我们默认的 Flask 实例目录,这个目录是不会提交到 git 里的。

instance/env

1
2
3
4
5
复制代码SMS_PROVIDER=huawei
HUAWEI_URL=https://rtcsms.cn-north-1.myhuaweicloud.com:10743/sms/batchSendSms/v1
HUAWEI_SMS_APP_KEY=aaa
HUAWEI_SMS_APP_SECRET=bbb
HUAWEI_SMS_SENDER_ID=ccc

运行时通过环境变量载入

1
2
3
4
5
6
7
复制代码docker run -d --name sms -p 80:80 \
-e "SMS_PROVIDER=aliyun" \
-e "ALIYUN_SMS_APP_KEY=aaa" \
-e "ALIYUN_SMS_APP_SECRET=bbb" \
-e "ALIYUN_SMS_REGION_ID=cn-hangzhou" \
-e "ALIYUN_SMS_SIGN_NAME=ccc" \
myapp:0.1

完整的项目可在这里查看。

示例项目代码

然后我们可以做如下测试,注意修改配置中的模版 ID 和环境变量,并根据自己的模版参数修改 params。

结语

对于老鸟来说,开发这个项目,可能根本不需要 1 个小时。对于规范的线上项目来说,还是缺少一些东西的,例如单元测试。大家的生产 API 服务是怎么样的?欢迎讨论!

这里的短信微服务只是抛砖引玉,其实所有的公有云 API 服务都可以一样的套用。1 小时上线一个微服务,剩下 7 小时划水刷掘金😄。

我是火眼君,愿我的写作,驱散心灵的孤单。

参考

  • palletsprojects.com/p/flask/
  • flask-restful.readthedocs.io/en/latest/
  • github.com/wwtg99/flas…
  • 阿里云短信 API
  • 华为云短信 API
  • github.com/wwtg99/sms-…
  • zhuanlan.zhihu.com/p/104380919

本文转载自: 掘金

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

Kotlin Coroutines Flow 系列(二) F

发表于 2020-02-06

三. Flow VS Sequences

每一个 Flow 其内部是按照顺序执行的,这一点跟 Sequences 很类似。

Flow 跟 Sequences 之间的区别是 Flow 不会阻塞主线程的运行,而 Sequences 会阻塞主线程的运行。

使用 flow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码fun main() = runBlocking {

launch {
for (j in 1..5) {
delay(100)
println("I'm not blocked $j")
}
}

flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.collect { println(it) }

println("Done")
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
复制代码1
I'm not blocked 1
2
I'm not blocked 2
3
I'm not blocked 3
4
I'm not blocked 4
5
Done
I'm not blocked 5

使用 sequence:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码fun main() = runBlocking {

launch {
for (k in 1..5) {
delay(100)
println("I'm blocked $k")
}
}

sequence {
for (i in 1..5) {
Thread.sleep(100)
yield(i)
}
}.forEach { println(it) }

println("Done")
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
复制代码1
2
3
4
5
Done
I'm blocked 1
I'm blocked 2
I'm blocked 3
I'm blocked 4
I'm blocked 5

由此,可以得出 Flow 在使用各个 suspend 函数时(本例子中使用了collect、emit函数)不会阻塞主线程的运行。

四. Flow VS RxJava

Kotlin 协程库的设计本身也参考了 RxJava ,下图展示了如何从 RxJava 迁移到 Kotlin 协程。(火和冰形象地表示了 Hot、Cold Stream)

migration from rxjava.jpeg

4.1 Cold Stream

flow 的代码块只有调用 collected() 才开始运行,正如 RxJava 创建的 Observables 只有调用 subscribe() 才开始运行一样。

4.2 Hot Stream

如图上所示,可以借助 Kotlin Channel 来实现 Hot Stream。

4.3. Completion

Flow 完成时(正常或出现异常时),如果需要执行一个操作,它可以通过两种方式完成:imperative、declarative。

4.3.1 imperative

通过使用 try … finally 实现

1
2
3
4
5
6
7
8
9
10
11
12
复制代码fun main() = runBlocking {
try {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.collect { println(it) }
} finally {
println("Done")
}
}

4.3.2 declarative

通过 onCompletion() 函数实现

1
2
3
4
5
6
7
8
9
复制代码fun main() = runBlocking {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.onCompletion { println("Done") }
.collect { println(it) }
}

4.3.3 onCompleted (借助扩展函数实现)

借助扩展函数可以实现类似 RxJava 的 onCompleted() 功能,只有在正常结束时才会被调用:

1
2
3
4
5
6
复制代码fun <T> Flow<T>.onCompleted(action: () -> Unit) = flow {

collect { value -> emit(value) }

action()
}

它的使用类似于 onCompletion()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码fun <T> Flow<T>.onCompleted(action: () -> Unit) = flow {

collect { value -> emit(value) }

action()
}

fun main() = runBlocking {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.onCompleted { println("Completed...") }
.collect{println(it)}
}

但是假如 Flow 异常结束时,是不会执行 onCompleted() 函数的。

4.4 Backpressure

Backpressure 是响应式编程的功能之一。

RxJava2 Flowable 支持的 Backpressure 策略,包括:

  • MISSING:创建的 Flowable 没有指定背压策略,不会对通过 OnNext 发射的数据做缓存或丢弃处理。
  • ERROR:如果放入 Flowable 的异步缓存池中的数据超限了,则会抛出 MissingBackpressureException 异常。
  • BUFFER:Flowable 的异步缓存池同 Observable 的一样,没有固定大小,可以无限制添加数据,不会抛出 MissingBackpressureException 异常,但会导致 OOM。
  • DROP:如果 Flowable 的异步缓存池满了,会丢掉将要放入缓存池中的数据。
  • LATEST:如果缓存池满了,会丢掉将要放入缓存池中的数据。这一点跟 DROP 策略一样,不同的是,不管缓存池的状态如何,LATEST 策略会将最后一条数据强行放入缓存池中。

而 Flow 的 Backpressure 是通过 suspend 函数实现。

4.4.1 buffer() 对应 BUFFER 策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码fun currTime() = System.currentTimeMillis()

var start: Long = 0

fun main() = runBlocking {

val time = measureTimeMillis {
(1..5)
.asFlow()
.onStart { start = currTime() }
.onEach {
delay(100)
println("Emit $it (${currTime() - start}ms) ")
}
.buffer()
.collect {
println("Collect $it starts (${currTime() - start}ms) ")
delay(500)
println("Collect $it ends (${currTime() - start}ms) ")
}
}

println("Cost $time ms")
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码Emit 1 (104ms) 
Collect 1 starts (108ms)
Emit 2 (207ms)
Emit 3 (309ms)
Emit 4 (411ms)
Emit 5 (513ms)
Collect 1 ends (613ms)
Collect 2 starts (613ms)
Collect 2 ends (1114ms)
Collect 3 starts (1114ms)
Collect 3 ends (1615ms)
Collect 4 starts (1615ms)
Collect 4 ends (2118ms)
Collect 5 starts (2118ms)
Collect 5 ends (2622ms)
Collected in 2689 ms

4.4.2 conflate() 对应 LATEST 策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码fun main() = runBlocking {

val time = measureTimeMillis {
(1..5)
.asFlow()
.onStart { start = currTime() }
.onEach {
delay(100)
println("Emit $it (${currTime() - start}ms) ")
}
.conflate()
.collect {
println("Collect $it starts (${currTime() - start}ms) ")
delay(500)
println("Collect $it ends (${currTime() - start}ms) ")
}
}

println("Cost $time ms")
}

执行结果:

1
2
3
4
5
6
7
8
9
10
复制代码Emit 1 (106ms) 
Collect 1 starts (110ms)
Emit 2 (213ms)
Emit 3 (314ms)
Emit 4 (419ms)
Emit 5 (520ms)
Collect 1 ends (613ms)
Collect 5 starts (613ms)
Collect 5 ends (1113ms)
Cost 1162 ms

4.4.3 DROP 策略

RxJava 的 contributor:David Karnok, 他写了一个kotlin-flow-extensions库,其中包括:FlowOnBackpressureDrop.kt,这个类支持 DROP 策略。

1
2
3
4
5
复制代码/**
* Drops items from the upstream when the downstream is not ready to receive them.
*/
@FlowPreview
fun <T> Flow<T>.onBackpressurureDrop() : Flow<T> = FlowOnBackpressureDrop(this)

使用这个库的话,可以通过使用 Flow 的扩展函数 onBackpressurureDrop() 来支持 DROP 策略。

该系列的相关文章:

Kotlin Coroutines Flow 系列(一) Flow 基本使用

Kotlin Coroutines Flow 系列(三) 异常处理

Kotlin Coroutines Flow 系列(四) 线程操作

Kotlin Coroutines Flow 系列(五) 其他的操作符

本文转载自: 掘金

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

Kotlin Coroutines Flow 系列(一) F

发表于 2020-02-06

woman-in-blue-spaghetti-strap-dress-2266519.jpg

一. Kotlin Flow 介绍

Flow 库是在 Kotlin Coroutines 1.3.2 发布之后新增的库。

官方文档给予了一句话简单的介绍:

Flow — cold asynchronous stream with flow builder and comprehensive operator set (filter, map, etc);

Flow 从文档的介绍来看,它有点类似 RxJava 的 Observable。因为 Observable 也有 Cold 、Hot 之分。

二. Flow 基本使用

Flow 能够返回多个异步计算的值,例如下面的 flow builder :

1
2
3
4
5
6
7
8
复制代码        flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.collect{
println(it)
}

其中 Flow 接口,只有一个 collect 函数

1
2
3
4
5
复制代码public interface Flow<out T> {

@InternalCoroutinesApi
public suspend fun collect(collector: FlowCollector<T>)
}

如果熟悉 RxJava 的话,则可以理解为 collect() 对应subscribe(),而 emit() 对应onNext()。

2.1 创建 flow

除了刚刚展示的 flow builder 可以用于创建 flow,还有其他的几种方式:

flowOf()

1
2
3
4
5
6
7
复制代码    flowOf(1,2,3,4,5)
.onEach {
delay(100)
}
.collect{
println(it)
}

asFlow()

1
2
3
4
5
6
复制代码    listOf(1, 2, 3, 4, 5).asFlow()
.onEach {
delay(100)
}.collect {
println(it)
}

channelFlow()

1
2
3
4
5
6
7
8
复制代码    channelFlow {
for (i in 1..5) {
delay(100)
send(i)
}
}.collect{
println(it)
}

最后的 channelFlow builder 跟 flow builder 是有一定差异的。

flow 是 Cold Stream。在没有切换线程的情况下,生产者和消费者是同步非阻塞的。
channel 是 Hot Stream。而 channelFlow 实现了生产者和消费者异步非阻塞模型。

下面的代码,展示了使用 flow builder 的情况,大致花费1秒:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码fun main() = runBlocking {

val time = measureTimeMillis {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.collect{
delay(100)
println(it)
}
}

print("cost $time")
}

flow.png

使用 channelFlow builder 的情况,大致花费700毫秒:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码fun main() = runBlocking {

val time = measureTimeMillis{
channelFlow {
for (i in 1..5) {
delay(100)
send(i)
}
}.collect{
delay(100)
println(it)
}
}

print("cost $time")
}

channelFlow.png

当然,flow 如果切换线程的话,花费的时间也是大致700毫秒,跟使用 channelFlow builder 效果差不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码fun main() = runBlocking {

val time = measureTimeMillis{
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.flowOn(Dispatchers.IO)
.collect {
delay(100)
println(it)
}
}

print("cost $time")
}

2.2 切换线程

相比于 RxJava 需要使用 observeOn、subscribeOn 来切换线程,flow 会更加简单。只需使用 flowOn,下面的例子中,展示了 flow builder 和 map 操作符都会受到 flowOn 的影响。

1
2
3
4
5
6
7
8
9
10
11
复制代码    flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.map {
it * it
}.flowOn(Dispatchers.IO)
.collect {
println(it)
}

而 collect() 指定哪个线程,则需要看整个 flow 处于哪个 CoroutineScope 下。

例如,下面的代码 collect() 则是在 main 线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码fun main() = runBlocking {

flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.map {
it * it
}.flowOn(Dispatchers.IO)
.collect {
println("${Thread.currentThread().name}: $it")
}
}

执行结果:

1
2
3
4
5
复制代码main: 1
main: 4
main: 9
main: 16
main: 25

值得注意的地方,不要使用 withContext() 来切换 flow 的线程。

2.3 flow 取消

如果 flow 是在一个挂起函数内被挂起了,那么 flow 是可以被取消的,否则不能取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码fun main() = runBlocking {

withTimeoutOrNull(2500) {
flow {
for (i in 1..5) {
delay(1000)
emit(i)
}
}.collect {
println(it)
}
}

println("Done")
}

执行结果:

1
2
3
复制代码1
2
Done

2.4 Terminal flow operators

Flow 的 API 有点类似于 Java Stream 的 API。它也同样拥有 Intermediate Operations、Terminal Operations。

Flow 的 Terminal 运算符可以是 suspend 函数,如 collect、single、reduce、toList 等;也可以是 launchIn 运算符,用于在指定 CoroutineScope 内使用 flow。

1
2
3
4
复制代码@ExperimentalCoroutinesApi // tentatively stable in 1.3.0
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect() // tail-call
}

整理一下 Flow 的 Terminal 运算符

  • collect
  • single/first
  • toList/toSet/toCollection
  • count
  • fold/reduce
  • launchIn/produceIn/broadcastIn

该系列的相关文章:

Kotlin Coroutines Flow 系列(二) Flow VS RxJava2

Kotlin Coroutines Flow 系列(三) 异常处理

Kotlin Coroutines Flow 系列(四) 线程操作

Kotlin Coroutines Flow 系列(五) 其他的操作符

本文转载自: 掘金

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

你要问我应用层?我就和你扯扯扯

发表于 2020-02-05

网络应用是计算机网络存在的理由,一批早起的网络应用主要有电子邮件、远程访问、文件传输等,但是随着计算机网络的发展和人类无穷无尽的需求,越来越多的网络应用被开发出来,例如即时通讯和对等(P2P)文件共享,IP 电话、视频会议等。还有一些多方在线游戏被开发出来如《魔兽世界》等,可以说计算机网络是一切应用演变出来的基础。人要怀有一颗感恩的心,感谢这些前辈的努力,才让我们现在的生活如此丰富多彩。但是我们作为程序员,不仅要能够享受这些成果,还要知道为什么,这样生活才会和谐。

应用层协议原理

研发网络应用程序的核心是写出能够运行在不同的端系统和通过网络彼此通信的程序。例如,在网络应用程序中,有两个互相通信的不同程序:一个是运行在用户主机上的浏览器程序;另一个是运行在 Web 服务器主机上的 Web 服务器程序。

网络应用程序体系结构

网络应用程序的体系结构(application architecture)主要有两种,一种是 客户-服务器体系结构(client-server architecture) ,在客户-服务器体系结构中,有一个持续打开,等待连接的主机称为服务器,它服务于来自许多其他称为 客户 的主机请求。比如 Web 服务器总会等待来自浏览器(运行在客户主机上)的请求。注意这种客户-服务器体系结构中,客户之间是不会彼此交流信息的,它们只与相应的服务器进行通信。还有一点是服务器具有固定的 IP 地址。下图显示了这种体系结构

这种客户-服务器体系结构存在弊端,那就是有的时候服务器的响应跟不上客户请求速度的情况,鉴于此,这种体系结构需要经常配备数据中心(data center)用来创建更强大的服务器。例如搜索引擎(谷歌、Bing和百度)、互联网商店(亚马逊、e-Bay 和阿里巴巴)、基于 Web 的电子邮件(Gmail 和 雅虎)、社交网络(脸书、Instagram、推特和微信),就是用了多个数据中心。

另外一种体系结构是 P2P体系结构(P2P architecture),相对于对数据中心有过多依赖的客户-服务器体系结构,P2P 体系结构则直接通过两台相连的主机直接通信,这些主机称为对等方。典型的 P2P 体系结构的应用包括 文件共享(BitTorrent)、下载器(迅雷)、互联网电话和视频会议(Skype),下图显示了 P2P 体系结构图

P2P 体系结构最重要的一个特性就是它的自扩展性(self-scalability)。例如,在一个 P2P 文件共享的应用中,尽管每个对等方都由于请求文件产生工作负载,但每个对等方通过向其他对等方分发文件也为系统增加服务器能力。

进程通信

我们上面说到了两种体系结构,一种是客户-服务器模式,一种是P2P 对等模式。我们都知道一个计算机允许同时运行多个应用程序,在我们看起来这些应用程序好像是同时运行的,那么它们之间是如何通信的呢?不可能存在同是一个母亲,兄弟俩不交流的情况吧。

用操作系统的术语来说,进行通信实际上是 进程(process)而不是程序。一个进程可以被认为是运行在端系统中的程序。当多个进程运行在相同的端系统上,它们使用进程间的通信机制相互通信。进程间的通信规则由操作系统来确定。我们暂不关心运行在同一主机上不同应用程序是如何通信的,我们主要探讨的目标是不同端系统中两个进程是如何通信的。还是分为两种结构来探讨。

####客户和服务器进程

网络应用程序由成对的进程组成,这些进程通过网络相互发送报文。例如,在 Web 应用程序中,文件从一个对等方中的进程传输到另一个对等方中的进程。而在每对通信的进程中,都会有一对客户(client) 和 服务器(server) 存在。比如我们上面提到的 Web ,对于 Web 来说,浏览器是一个客户进程,而 Web 服务器是一台服务器进程。也许你也应该能猜到,在 P2P 体系结构中,一个进程能够扮演两种角色,既是客户又是服务器的情况。但是在实际通信的过程中,我们还是很容易区分的,我们通常通过下面这种方式进行区分。

在一对进程之间的通信会话场景中,发起通信(即在会话开始时发起与其他进程的联系)的进程称为客户,在会话开始时等待联系的被称为服务器。

进程与计算机网络之间的接口

计算机是庞大且繁杂的,计算机网络也是,应用程序不可能只有一个进程组成,它同样是多个进程共同作用协商运行,然而,分布在多个端系统之间的进程是如何进行通信的呢?实际上,每个进程之间会有一个 套接字(socket) 的软件接口存在,套接字是应用程序的内部接口,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将 I/O 插入到网络中,并与网络中的其他应用程序进行通信。

通过一个实例来简单类比一下套接字和网络进程:进程可类比一座房子,而它的套接字相当于是房子的门,当一个进程想要与其他进程进行通信时,它会把报文推出门外,然后通过运输设备把报文运输到另外一座房子,通过门进入房子内部使用。

下图是一个通过套接字进行通信的流程图

从图可以看到,Socket 属于主机或者服务进程的内部接口,由应用程序开发人员进行控制,两台端系统之间进行通信会通过 TCP 的缓冲区经由网络传输到另一个端系统的 TCP 缓冲区,Socket 从 TCP 缓冲区读取报文供应用程序内部使用。

套接字是建立网络应用程序的可编程接口,因此套接字也被称为应用程序和网络之间的 应用程序编程接口(Application Programming Interface,API)。应用程序开发人员可以控制套接字内部细节,但是无法控制运输层的传输,只能对运输层的传输协议进行选择,还可以对运输层的传输参数进行选择,比如最大缓存和最大报文长度等。

进程寻址

我们上面提到网络应用程序之间会相互发送报文,那么你怎么知道你应该向哪里发送报文呢?是不是存在某种机制能够让你知道你能够发到哪里?这就好比你要发送电子邮件,你写好了内容但是你不知道发发往哪里,所以这个时候必须要有一种知道对方地址的机制,这种机制能够辨明对方唯一的一个地址,这种地址就是 IP地址。我们会在后面的文章中详细讨论 IP 地址的内容,目前只需要知道 IP 是一个32比特的量并且能够唯一标示互联网中任意一台主机的地址就可以了。

只知道 IP 地址是否就可以了呢?我们知道一台计算机可能回运行多个网络应用程序,那么如何确定是哪个网络应用程序接受发送过来的报文呢?所以这时候还需要知道网络应用程序的 端口号(port number)。例如, Web 应用程序需要用 80 端口来标示,邮件服务器程序需要使用 25 来标示。

应用程序如何选择运输服务

我们知道应用程序是属于互联网四层协议的 应用层 协议,并且四层协议必须彼此协助共同完成工作。好了,这时候我们只有应用层协议,我们需要发送报文,我们如何发送报文呢?这就好比你知道目的地是哪里了,你该如何到达目的地呢?是走路,公交,地铁还是打车?

应用程序发送报文的交通工具的选择也有很多,我们可以从 数据传输是否可靠、吞吐量、定时和安全性 来考虑,下面是你需要考虑的具体内容。

  • 数据传输是否可靠

我们之前探讨过,分组在计算机网络中会存在丢包问题,丢包问题的严重性跟网络应用程序的性质有关,如果像是电子邮件、文件传输、远程主机、Web 文档传输的过程中出现问题,数据丢失可能会造成非常严重的后果。如果像是网络游戏,多人视频会议造成的影响可能比较小。鉴于此,数据传输的可靠性也是首先需要考虑的问题。因此,如果一个协议提供了这样的确保数据交付的服务,就认为提供了 可靠数据传输(reliable data transfer),能够忍受数据丢失的应用被称为 容忍丢失的应用(loss-tolerant application)。

  • 吞吐量

在之前的文章中我们引入了吞吐量的概念,吞吐量就是在网络应用中数据传输过程中,发送进程能够向接收进程交付比特的速率。具有吞吐量要求的应用程序被称为 带宽敏感的应用(bandwidth-sensitive application)。带宽敏感的应用具有特定的吞吐量要求,而 弹性应用(elastic application) 能够根据当时可用的带宽或多或少地利用可供使用的吞吐量。

  • 定时

定时是什么意思?定时能够确保网络中两个应用程序的收发是否能够在指定的时间内完成,这也是应用程序选择运输服务需要考虑的一个因素,这听起来很自然,你网络应用发送和接收数据包肯定要加以时间的概念,比如在游戏中,你一包数据迟迟发送不过去,对面都推塔了你还卡在半路上呢。

  • 安全性

最后,选择运输协议一定要能够为应用程序提供一种或多种安全性服务。

因特网能够提供的运输服务

说完运输服务的选型,接下来该聊一聊因特网能够提供哪些服务了。实际上,因特网为应用程序提供了两种运输层的协议,即 UDP 和 TCP,下面是一些网络应用的选择要求,可以根据需要来选择适合的运输层协议。

应用 数据丢失 带宽 时间敏感
文件传输 不能丢失 弹性 不敏感
电子邮件 不能丢失 弹性 不敏感
Web 文档 不能丢失 弹性 不敏感
因特网电话/视频会议 容忍丢失 弹性 敏感,100ms
流式存储音频/视频 容忍丢失 弹性 敏感,几秒
交互式游戏 容忍丢失 弹性 是,100ms
智能手机消息 不能丢失 弹性 无所谓

下面我们就来聊一聊这两种运输协议的应用场景

TCP

TCP 服务模型的特性主要有下面几种

  • 面向连接的服务

在应用层数据报发送后, TCP 让客户端和服务器互相交换运输层控制信息。这个握手过程就是提醒客户端和服务器需要准备好接受数据报。握手阶段后,一个 TCP 连接(TCP Connection) 就建立了。这是一条全双工的连接,即连接双方的进程都可以在此连接上同时进行收发报文。当应用程序结束报文发送后,必须拆除连接。

  • 可靠的数据传输

通信进程能够依靠 TCP,无差错、按适当顺序交付所有发送的数据。应用程序能够依靠 TCP 将相同的字节流交付给接收方的套接字,没有字节的丢失和冗余。

  • 拥塞控制

TCP 的拥塞控制并不一定为通信进程带来直接好处,但能为因特网带来整体好处。当接收方和发送方之间的网络出现拥塞时,TCP 的拥塞控制会抑制发送进程(客户端或服务器),我们会在后面具体探讨拥塞控制

UDP

UDP 是一种轻量级的运输协议,它仅提供最小服务。UDP 是无连接的,因此在两个进程通信前没有握手过程。UDP 也不会保证报文是否传输到服务端,它就像是一个撒手掌柜。不仅如此,到达接收进程的报文也可能是乱序到达的。

下面是上表列出来的一些应用所选择的协议

应用 应用层协议 支撑的运输协议
电子邮件 SMTP TCP
远程终端访问 Telnet TCP
Web HTTP TCP
文件传输 FTP TCP
流式多媒体 HTTP TCP
因特网电话 SIP、RTP TCP或UDP

应用层协议

现在我们会探讨一些应用层协议,首先来认识一下什么是 应用层协议,应用层协议(application-layer protocol) 定义了运行在不同端系统上的应用进程如何相互传递报文。

应用层协议会定义

  • 交换的报文类型,如请求报文和响应报文;
  • 各种报文类型的语法,如报文中的各个字段公共详细描述;
  • 字段的语义,即包含在字段中信息的含义;
  • 进程何时、如何发送报文及对报文进行响应。

应用层协议分类

  • 域名系统(Domain Name System, DNS):用于实现网络设备名字到 IP 地址映射的网络服务。
  • 文件传输协议(File Transfer Protocol,FTP):用于实现交互式文件传输功能。
  • 邮件传送协议(Simple Mail Transfer Protocol, SMTP):用于实现电子邮箱传送功能。
  • 超文本传输协议(HyperText Transfer Protocol,HTTP):用于实现 Web 服务。
  • 远程登录协议(Telnet):用于实现远程登录功能。

Web 和 HTTP

Web(World Wide Web)即全球广域网,也就是 URL 为 www 开头的网络,它是 HTTP 协议的主要载体,是建立在 Internet 上的一种网络服务,我们一般讲的 Web ,其实就是指的 HTTP 协议,HTTP 协议作为 web 程序员必须要掌握并理解的一门协议,有必要好好了解一下。

超文本传输协议可以进行文字分割:超文本(Hypertext)、传输(Transfer)、协议(Protocol),它们之间的关系如下

按照范围的大小 协议 > 传输 > 超文本。下面就分别对这三个名次做一个解释。

什么是超文本

在互联网早期的时候,我们输入的信息只能保存在本地,无法和其他电脑进行交互。我们保存的信息通常都以文本即简单字符的形式存在,文本是一种能够被计算机解析的有意义的二进制数据包。而随着互联网的高速发展,两台电脑之间能够进行数据的传输后,人们不满足只能在两台电脑之间传输文字,还想要传输图片、音频、视频,甚至点击文字或图片能够进行超链接的跳转,那么文本的语义就被扩大了,这种语义扩大后的文本就被称为超文本(Hypertext)。

什么是传输

那么我们上面说到,两台计算机之间会形成互联关系进行通信,我们存储的超文本会被解析成为二进制数据包,由传输载体(例如同轴电缆,电话线,光缆)负责把二进制数据包由计算机终端传输到另一个终端的过程(对终端的详细解释可以参考 你说你懂互联网,那这些你知道么?这篇文章)称为传输(transfer)。

通常我们把传输数据包的一方称为请求方,把接到二进制数据包的一方称为应答方。请求方和应答方可以进行互换,请求方也可以作为应答方接受数据,应答方也可以作为请求方请求数据,它们之间的关系如下

如图所示,A 和 B 是两个不同的端系统,它们之间可以作为信息交换的载体存在,刚开始的时候是 A 作为请求方请求与 B 交换信息,B 作为响应的一方提供信息;随着时间的推移,B 也可以作为请求方请求 A 交换信息,那么 A 也可以作为响应方响应 B 请求的信息。

什么是协议

协议这个名词不仅局限于互联网范畴,也体现在日常生活中,比如情侣双方约定好在哪个地点吃饭,这个约定也是一种协议,比如你应聘成功了,企业会和你签订劳动合同,这种双方的雇佣关系也是一种 协议。注意自己一个人对自己的约定不能成为协议,协议的前提条件必须是多人约定。

那么网络协议是什么呢?

网络协议就是网络中(包括互联网)传递、管理信息的一些规范。如同人与人之间相互交流是需要遵循一定的规矩一样,计算机之间的相互通信需要共同遵守一定的规则,这些规则就称为网络协议。

没有网络协议的互联网是混乱的,就和人类社会一样,人不能想怎么样就怎么样,你的行为约束是受到法律的约束的;那么互联网中的端系统也不能自己想发什么发什么,也是需要受到通信协议约束的。

那么我们就可以总结一下,什么是 HTTP?可以用下面这个经典的总结回答一下: HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范

持久性连接和非持久性连接

HTTP 是可以使用持久性连接和非持久性连接的,下面我们着重探讨一下这两种方式

非持久性连接

我们首先来探讨一下持久性连接的 HTTP

你是不是很好奇,当你在浏览器中输入网址后,到底发生了什么事情?你想要的内容是如何展现出来的?让我们通过一个例子来探讨一下,我们假设访问的 URL 地址为 http://www.someSchool.edu/someDepartment/home.index,当我们输入网址并点击回车时,浏览器内部会进行如下操作

  • DNS 服务器会首先进行域名的映射,找到访问www.someSchool.edu所在的地址,然后 HTTP 客户端进程在 80 端口发起一个到服务器 www.someSchool.edu 的 TCP 连接(80 端口是 HTTP 的默认端口)。在客户和服务器进程中都会有一个套接字与其相连。
  • HTTP 客户端通过它的套接字向服务器发送一个 HTTP 请求报文。该报文中包含了路径 someDepartment/home.index 的资源,我们后面会详细讨论 HTTP 请求报文。
  • HTTP 服务器通过它的套接字接受该报文,进行请求的解析工作,并从其存储器(RAM 或磁盘)中检索出对象 www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到 HTTP 响应报文中,并通过套接字向客户进行发送。
  • HTTP 服务器随即通知 TCP 断开 TCP 连接,实际上是需要等到客户接受完响应报文后才会断开 TCP 连接。
  • HTTP 客户端接受完响应报文后,TCP 连接会关闭。HTTP 客户端从响应中提取出报文中是一个 HTML 响应文件,并检查该 HTML 文件,然后循环检查报文中其他内部对象。
  • 检查完成后,HTTP 客户端会把对应的资源通过显示器呈现给用户。

至此,键入网址再按下回车的全过程就结束了。上述过程描述的是一种简单的请求-响应全过程,真实的请求-响应情况可能要比上面描述的过程复杂很多。

上面的步骤举例说明了非持久性连接的使用,其中每个 TCP 链接都在服务器发送完成后关闭。每个 TCP 连接只传输一个请求报文和响应报文。

持久性连接的 HTTP

非持久性连接有一些缺点。第一,必须为每个请求的对象建立和维护一个全新的连接。对于每个这样的连接来说,在客户端和服务器中都要分配 TCP 的缓冲区和保持 TCP 变量,这给 Web 服务器带来了严重的负担。因为一台 Web 服务器可能要同时服务于数百甚至上千个客户请求。

在采用 HTTP 1.1 持续连接的情况下,服务器在发送响应后保持该 TCP 连接打开不关闭。在相同的客户与服务器之间,后续的请求和响应报文能够通过相同的连接进行传送。一般来说,如果一跳连接经过一定的时间间隔(可配置)后仍未使用,HTTP 服务器就应该关闭其连接。

HTTP 报文格式

我们上面描述了一下 HTTP 的请求响应过程,流程比较简单,但是凡事就怕认真,你这一认真,就能拓展出很多东西,比如 HTTP 报文是什么样的,它的组成格式是什么? 下面就来探讨一下

HTTP 协议主要由三大部分组成:

  • 起始行(start line):描述请求或响应的基本信息;
  • 头部字段(header):使用 key-value 形式更详细地说明报文;
  • 消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。

其中起始行和头部字段并成为 请求头 或者 响应头,统称为 Header;消息正文也叫做实体,称为 body。HTTP 协议规定每次发送的报文必须要有 Header,但是可以没有 body,也就是说头信息是必须的,实体信息可以没有。而且在 header 和 body 之间必须要有一个空行(CRLF),如果用一幅图来表示一下的话,我觉得应该是下面这样

我们使用上面的那个例子来看一下 http 的请求报文

如图,这是 http://www.someSchool.edu/someDepartment/home.index 请求的请求头,通过观察这个 HTTP 报文我们就能够学到很多东西,首先,我们看到报文是用普通 ASCII 文本书写的,这样保证人能够可以看懂。然后,我们可以看到每一行和下一行之间都会有换行,而且最后一行(请求头部后)再加上一个回车换行符。

每个报文的起始行都是由三个字段组成:方法、URL 字段和 HTTP 版本字段。

HTTP 请求方法

HTTP 请求方法一般分为 8 种,它们分别是

  • GET 获取资源,GET 方法用来请求访问已被 URI 识别的资源。指定的资源经服务器端解析后返回响应内容。也就是说,如果请求的资源是文本,那就保持原样返回;
  • POST 传输实体,虽然 GET 方法也可以传输主体信息,但是便于区分,我们一般不用 GET 传输实体信息,反而使用 POST 传输实体信息,
  • PUT 传输文件,PUT 方法用来传输文件。就像 FTP 协议的文件上传一样,要求在请求报文的主体中包含文件内容,然后保存到请求 URI 指定的位置。

但是,鉴于 HTTP 的 PUT 方法自身不带验证机制,任何人都可以上传文件 , 存在安全性问题,因此一般的 W eb 网站不使用该方法。若配合 W eb 应用程序的验证机制,或架构设计采用REST(REpresentational State Transfer,表征状态转移)标准的同类 Web 网站,就可能会开放使用 PUT 方法。

  • HEAD 获得响应首部,HEAD 方法和 GET 方法一样,只是不返回报文主体部分。用于确认 URI 的有效性及资源更新的日期时间等。
  • DELETE 删除文件,DELETE 方法用来删除文件,是与 PUT 相反的方法。DELETE 方法按请求 URI 删除指定的资源。
  • OPTIONS 询问支持的方法,OPTIONS 方法用来查询针对请求 URI 指定的资源支持的方法。
  • TRACE 追踪路径,TRACE 方法是让 Web 服务器端将之前的请求通信环回给客户端的方法。
  • CONNECT 要求用隧道协议连接代理,CONNECT 方法要求在与代理服务器通信时建立隧道,实现用隧道协议进行 TCP 通信。主要使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加 密后经网络隧道传输。

我们一般最常用的方法也就是 GET 方法和 POST 方法,其他方法暂时了解即可。下面是 HTTP1.0 和 HTTP1.1 支持的方法清单

HTTP 请求 URL

HTTP 协议使用 URI 定位互联网上的资源。正是因为 URI 的特定功能,在互联网上任意位置的资源都能访问到。URL 带有请求对象的标识符。在上面的例子中,浏览器正在请求对象 /somedir/page.html 的资源。

我们再通过一个完整的域名解析一下 URL

比如 http://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument 这个 URL 比较繁琐了吧,你把这个 URL 搞懂了其他的 URL 也就不成问题了。

首先出场的是 http

http://告诉浏览器使用何种协议。对于大部分 Web 资源,通常使用 HTTP 协议或其安全版本,HTTPS 协议。另外,浏览器也知道如何处理其他协议。例如, mailto: 协议指示浏览器打开邮件客户端;ftp:协议指示浏览器处理文件传输。

第二个出场的是 主机

www.example.com 既是一个域名,也代表管理该域名的机构。它指示了需要向网络上的哪一台主机发起请求。当然,也可以直接向主机的 IP address 地址发起请求。但直接使用 IP 地址的场景并不常见。

第三个出场的是 端口

我们前面说到,两个主机之间要发起 TCP 连接需要两个条件,主机 + 端口。它表示用于访问 Web 服务器上资源的入口。如果访问的该 Web 服务器使用HTTP协议的标准端口(HTTP为80,HTTPS为443)授予对其资源的访问权限,则通常省略此部分。否则端口就是 URI 必须的部分。

上面是请求 URL 所必须包含的部分,下面就是 URL 具体请求资源路径

第四个出场的是 路径

/path/to/myfile.html 是 Web 服务器上资源的路径。以端口后面的第一个 / 开始,到 ? 号之前结束,中间的 每一个/ 都代表了层级(上下级)关系。这个 URL 的请求资源是一个 html 页面。

紧跟着路径后面的是 查询参数

?key1=value1&key2=value2 是提供给 Web 服务器的额外参数。如果是 GET 请求,一般带有请求 URL 参数,如果是 POST 请求,则不会在路径后面直接加参数。这些参数是用 & 符号分隔的键/值对列表。key1 = value1 是第一对,key2 = value2 是第二对参数

紧跟着参数的是锚点

#SomewhereInTheDocument 是资源本身的某一部分的一个锚点。锚点代表资源内的一种“书签”,它给予浏览器显示位于该“加书签”点的内容的指示。 例如,在HTML文档上,浏览器将滚动到定义锚点的那个点上;在视频或音频文档上,浏览器将转到锚点代表的那个时间。值得注意的是 # 号后面的部分,也称为片段标识符,永远不会与请求一起发送到服务器。

更多有关 HTTP1.1 的内容可以参考博主的这三篇博文,我感觉已经把 HTTP 讲清楚了

看完这篇HTTP,跟面试官扯皮就没问题了

你还在为 HTTP 的这些概念头疼吗?

震惊 | HTTP 在疫情期间把我吓得不敢出门了

因特网中的电子邮件

自从有了因特网,电子邮件就在因特网上流行起来。与普通邮件一样,电子邮件是一种异步通信媒介,即人们方便的情况下就可以和他人进行邮件往来,而不必与他人进行沟通后在发送。现代电子邮件具有许多强大的特性,包括具有附件、超链接、HTML 格式文本和图片的报文。下面是电子邮件系统的总体概览

从图中我们可以看到它有三个主要组成部分:用户代理(user agent)、邮件服务器(mail server)、和简单邮件传输协议(Simple Mail Transfer Protocol,SMTP)。下面我们就来描述一下邮件收发的过程。

用户代理允许用户阅读、回复、转发、保存和撰写报文。微软的 Outlook 和 Apple Mail 是电子邮件用户代理的例子。当用户编写完邮件时,他的用户代理向邮件服务器发送邮件,此时用户发送的邮件会放在邮件服务器的外出消息队列(Outgoing message queue)中,当接收方用户想要阅读邮件时,他的用户代理直接从外出消息队列中去取得该报文。

邮件服务器构成了整个邮件系统的核心。每个接收方在其中的邮件服务器上会有一个邮箱(mailbox) 存在。用户的邮箱管理和维护发送给他的报文。一个典型的邮件发送过程是:从发送方的用户代理开始,传输到发送方的邮件服务器,再传输到接收方的邮件服务器,然后在这里被分发到接收方的邮箱中。用接收方的用户想要从邮箱中读取邮件时,他的邮件服务器会对用户进行认证。如果发送方发送的邮件无法正确交付给接收方的服务器,那么发送方的用户代理会把邮件存储在一个报文队列(message queue)中,并在以后尝试再次发送,通常每30分钟发送一次,如果一段时间后还发送不成功,服务器就会删除报文队列中的邮件并以电子邮件的方式通知发送方。

SMTP 是因特网电子邮件中的主要的应用层协议。SMTP 也使用 TCP 作为运输层协议,保证数据传输的可靠性。

SMTP 协议传输过程

为了描述 SMTP 的基本操作,我们观察一下下面这种常见的情景。

我们假设 Alice 想给 Bob 发送一封简单的 ASCII 报文

  • Alice 调用她的邮件代理程序并提供 Bob 的邮件地址 (例如 bob@someschool.edu),编写邮件报文,然后指示用户代理发送该报文
  • Alice 的用户代理把报文发送给她的邮件服务器,在那里该报文被放在消息队列中。
  • 运行在 Alice 的邮件服务器上的 SMTP 客户端发现了报文队列中的邮件,它就创建一个到运行在 Bob 邮件服务器上的 SMTP 服务器的 TCP 连接
  • 在经过一些初始化 SMTP 握手后,SMTP 客户端通过该 TCP 连接发送 Alice 的邮件。
  • 在 Bob 的邮件服务器上,SMTP 的服务端接收该邮件,Bob 的邮件服务器将邮件放在 Bob 的邮箱中
  • 在 Bob 想要看邮件时,他会调用用户代理阅读该邮件

上面说的邮件其实就是报文,指的就是一系列 ASCII 码,SMTP 传输邮件之前,需要将二进制多媒体数据编码为 ASCII 码进行传输。

SMTP 一般不使用中间邮件服务器发送邮件,即使这两个邮件服务器位于地球的两端也是这样的。TCP 连接通常直接连接 Alice 的邮件服务器和 Bob 的邮件服务器。

现在你知道了两台邮件服务器邮件发送的大体过程,那么,SMTP 是如何将邮件从 Alice 邮件服务器发送到 Bob 的邮件服务器的呢?主要分为下面三个阶段

  • 建立连接:在这一阶段,SMTP 客户请求与服务器的25端口建立一个 TCP 连接。一旦连接建立,SMTP 服务器和客户就开始相互通告自己的域名,同时确认对方的域名。
  • 邮件传送:一旦连接建立后,就开始邮件传输。SMTP 依靠 TCP 能够将邮件准确无误地传输到接收方的邮件服务器中。SMTP 客户将邮件的源地址、目的地址和邮件的具体内容传递给 SMTP 服务器,SMTP 服务器进行相应的响应并接收邮件。
  • 连接释放:SMTP 客户发出退出命令,服务器在处理命令后进行响应,随后关闭 TCP 连接。

下面我们分析一个实际的 SMTP 邮件发送过程,以下统称为SMTP客户(C)和 SMTP服务器(S)。客户的主机名为 crepes.fr,服务器的主机名为 hamburger.edu。以 C: 开头的 ASCII 码文本就是客户交给 TCP 套接字的那些行,以 S: 开头的 ASCII 码则是服务器发送给其 TCP 套接字的那些行。一旦创建了连接,就开始了如下过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码S: 220 hamburger.edu
C: HELO crepes.fr
S: 250 Hello crepes.fr, pleased to meet you
C: MAIL FROM: <alice@crepes.fr>
S: 250 alice@crepes.fr ... Sender ok
C: RCPT TO: <bob@hamburger.edu>
S: 250 bob@hamburder.edu ... Recipient ok
C: DATA
S: 354 Enter mail, end with "." on a line by itself
C: Do you like ketchup?
C: How about pickles?
C: .
S: 250 Message accepted for delivery
C: QUIT
S: 221 hamburger.edu closing connection

在上述例子中,客户从邮件服务器 crepes.fr 向邮件服务器 hamburger.edu 发送了一个报文 (“ Do you like ketchup? How about pickles? “) 。作为对话的一部分,该客户发送了 5 条命令: HELO(是 HELLO 的缩写)、 MAMIL FROM、RCPT TO、DATA 以及 QUIT。这些命令都是自解释的。

什么是自解释,就是不需要再进行解释了,命令自己就能解释自己所要表述的功能。

上面是一个简单的 SMTP 交换过程,包括了连接建立、邮件传送和连接释放三个具体过程

首先建立 TCP 连接、SMTP 调用 TCP 协议的25号端口监听连接请求,然后客户端发送 HELO 指令用来表明自己是发送方的身份,然后服务端作出响应。然后,客户端发送 MAIL FROM 命令,表明客户端的邮件地址是 <alice@crepes.fr>,服务器以 OK 作为响应,表明准备接收。客户端发送 RCPT TO 表明接收方的电子邮件地址,可以有多个 RCPT 行,即一份邮件可以同时发送给多个收件人。服务器端则表示是否愿意为收件人接收邮件。协商结束后,客户端用 DATA 命令发送信息,结束标志是CRLF.CRLF ,也就是 回车换行.回车换行。最后,控制交互的任一端可选择终止会话,为此它发出一个 QUIT 命令,另一端用命令221响应,表示同意终止连接,双方将关闭连接。

上述过程中会涉及几个类似 HTTP 的状态码。250 就表示 OK ,类似 HTTP 的 200。在命令成功时,服务器返回代码250,如果失败则返回代码550(命令无法识别)、451(处理时出错)、452(存储空间不够)、421(服务器不可用)等,354则表示开始信息输入。

SMTP 的报文会有局限性,SMTP 的局限性表现在只能发送 ASCII 码格式的报文,不支持中文、法文、德文等,它也不支持语音、视频的数据。通过 MIME协议,对 SMTP 补充。MIME 使用网络虚拟终端(NVT)标准,允许非ASCII码数据通过SMTP传输。

SMTP 与 HTTP 的对比

HTTP 是我们学习的第一个应用层协议,SMTP 是我们学习的第二个应用层协议,那么我们就对这两个协议进行比对。

这两个协议都用于从一台主机向另一台主机传送文件:HTTP 从 Web 服务器向 Web 客户端(通常是浏览器)传送文件,SMTP 是从一个邮件服务器向另一个邮件服务器传送文件(即电子邮件报文)。

这两个协议也会有几个重要的区别

  • 首先,HTTP 是一个 拉协议(pull protocol),客户端发送请求,请求获取服务端的资源,然后服务端进行响应,把需要下载的文件传输给客户端;而 SMTP 是一个 推协议(push protocol),SMTP 的客户端会主动把邮件推送给 SMTP 的服务端。
  • 第二个区别是,SMTP 要求每个报文都采用 7 比特的 ASCII 码格式,如果某报文包含了非 7 比特的 ASCII 自负或二进制数据,则该报文必须按照7比特 ASCII 码进行编码。HTTP 数据则不受这种限制。
  • 第三个区别是如何处理一个既包含文本又包含图形的文档,HTTP 把每个对象封装到它自己的 HTTP 响应报文中,而 SMTP 则把所有报文对象放在一个报文之中。

DNS 因特网目录服务协议

试想一个问题,我们人类可以有多少种识别自己的方式?可以通过身份证来识别,可以通过社保卡号来识别,也可以通过驾驶证来识别,尽管我们有多种识别方式,但在特定的环境下,某种识别方法可能比另一种方法更为适合。因特网上的主机和人类一样,可以使用多种识别方式进行标识。互联网上主机的一种标识方法是使用它的 主机名(hostname) ,如 www.facebook.com、 www.google.com 等。但是这是我们人类的记忆方式,路由器不会这么理解,路由器喜欢定长的、有层次结构的 IP地址,so,还记得 IP 是什么吗?

IP 地址现在简单表述一下,就是一个由 4 字节组成,并有着严格的层次结构。例如 121.7.106.83 这样一个 IP 地址,其中的每个字节都可以用 . 进行分割,表示了 0 - 255 的十进制数字。(具体的 IP 我们会在后面讨论)

然而,路由器喜欢的是 IP 地址进行解析,我们人类却便于记忆的是网址,那么路由器如何把 IP 地址解析为我们熟悉的网址地址呢?这时候就需要 DNS 出现了。

DNS 的全称是 Domain Name System,DNS ,它是一个由分层的 DNS 服务器(DNS server)实现的分布式数据库;它还是一个使得主机能够查询分布式数据库的应用层协议。DNS 服务器通常是运行 BIND(Berkeley Internet Name Domain) 软件的 UNIX 机器。DNS 协议运行在 UDP 之上,使用 53 端口。

DNS 基本概述

与 HTTP、FTP 和 SMTP 一样,DNS 协议也是应用层的协议,DNS 使用客户-服务器模式运行在通信的端系统之间,在通信的端系统之间通过下面的端到端运输协议来传送 DNS 报文。但是 DNS 不是一个直接和用户打交道的应用。DNS 是为因特网上的用户应用程序以及其他软件提供一种核心功能。

DNS 通常不是一门独立的协议,它通常为其他应用层协议所使用,这些协议包括 HTTP、SMTP 和 FTP,将用户提供的主机名解析为 IP 地址。

下面根据一个示例来描述一下这个 DNS 解析过程,这个和你输入网址后,浏览器做了什么操作有异曲同工之处

你在浏览器键入 www.someschool.edu/index.html 时会发生什么现象?为了使用户主机能够将一个 HTTP 请求报文发送到 Web 服务器 www.someschool.edu ,会经历如下操作

  • 同一台用户主机上运行着 DNS 应用的客户端
  • 浏览器从上述 URL 中抽取出主机名 www.someschool.edu ,并将这台主机名传给 DNS 应用的客户端
  • DNS 客户向 DNS 服务器发送一个包含主机名的请求。
  • DNS 客户最终会收到一份回答报文,其中包含该目标主机的 IP 地址
  • 一旦浏览器收到目标主机的 IP 地址后,它就能够向位于该 IP 地址 80 端口的 HTTP 服务器进程发起一个 TCP 连接。

除了提供 IP 地址到主机名的转换,DNS 还提供了下面几种重要的服务

  • 主机别名(host aliasing),有着复杂的主机名的主机能够拥有一个或多个其他别名,比如说一台名为 relay1.west-coast.enterprise.com 的主机,同时会拥有 enterprise.com 和 www.enterprise.com 的两个主机别名,在这种情况下,relay1.west-coast.enterprise.com 也称为 规范主机名,而主机别名要比规范主机名更加容易记忆。应用程序可以调用 DNS 来获得主机别名对应的规范主机名以及主机的 IP地址。
  • 邮件服务器别名(mail server aliasing),同样的,电子邮件的应用程序也可以调用 DNS 对提供的主机名进行解析。
  • 负载分配(load distribution),DNS 也用于冗余的服务器之间进行负载分配。繁忙的站点例如 cnn.com 被冗余分布在多台服务器上,每台服务器运行在不同的端系统之间,每个都有着不同的 IP 地址。由于这些冗余的 Web 服务器,一个 IP 地址集合因此与同一个规范主机名联系。DNS 数据库中存储着这些 IP 地址的集合。由于客户端每次都会发起 HTTP 请求,所以 DNS 就会在所有这些冗余的 Web 服务器之间循环分配了负载。

DNS 工作概述

DNS 是一个复杂的系统,我们在这里只是就其运行的主要方面进行学习,下面给出一个 DNS 工作过程的总体概述

假设运行在用户主机上的某些应用程序(如 Web 浏览器或邮件阅读器) 需要将主机名转换为 IP 地址。这些应用程序将调用 DNS 的客户端,并指明需要被转换的主机名。用户主机上的 DNS 收到后,会使用 UDP 通过 53 端口向网络上发送一个 DNS 查询报文,经过一段时间后,用户主机上的 DNS 会收到一个主机名对应的 DNS 回答报文。因此,从用户主机的角度来看,DNS 就像是一个黑盒子,其内部的操作你无法看到。但是实际上,实现 DNS 这个服务的黑盒子非常复杂,它由分布于全球的大量 DNS 服务器以及定义了 DNS 服务器与查询主机通信方式的应用层协议组成。

DNS 最早的一种简单设计只是在因特网上使用一个 DNS 服务器。该服务器会包含所有的映射。这是一种集中式的设计,这种设计并不适用于当今的互联网,因为互联网有着数量巨大并且持续增长的主机,这种集中式的设计会存在以下几个问题

  • 单点故障(a single point of failure),如果 DNS 服务器崩溃,那么整个网络随之瘫痪。
  • 通信容量(traaffic volume),单个 DNS 服务器不得不处理所有的 DNS 查询,这种查询级别可能是上百万上千万级
  • 远距离集中式数据库(distant centralized database),单个 DNS 服务器不可能 邻近 所有的用户,假设在美国的 DNS 服务器不可能临近让澳大利亚的查询使用,其中查询请求势必会经过低速和拥堵的链路,造成严重的时延。
  • 维护(maintenance),维护成本巨大,而且还需要频繁更新。

所以 DNS 不可能集中式设计,它完全没有可扩展能力,因此采用分布式设计,所以这种设计的特点如下

分布式、层次数据库

首先分布式设计首先解决的问题就是 DNS 服务器的扩展性问题,因此 DNS 使用了大量的 DNS 服务器,它们的组织模式一般是层次方式,并且分布在全世界范围内。没有一台 DNS 服务器能够拥有因特网上所有主机的映射。相反,这些映射分布在所有的 DNS 服务器上。

大致来说有三种 DNS 服务器:根 DNS 服务器、 顶级域(Top-Level Domain, TLD) DNS 服务器 和 权威 DNS 服务器 。这些服务器的层次模型如下图所示

假设现在一个 DNS 客户端想要知道 www.amazon.com 的 IP 地址,那么上面的域名服务器是如何解析的呢?首先,客户端会先根服务器之一进行关联,它将返回顶级域名 com 的 TLD 服务器的 IP 地址。该客户则与这些 TLD 服务器之一联系,它将为 amazon.com 返回权威服务器的 IP 地址。最后,该客户与 amazom.com 权威服务器之一联系,它为 www.amazom.com 返回其 IP 地址。

我们现在来讨论一下上面域名服务器的层次系统

  • 根 DNS 服务器 ,有 400 多个根域名服务器遍及全世界,这些根域名服务器由 13 个不同的组织管理。根域名服务器的清单和组织机构可以在 root-servers.org/ 中找到,根域名服务器提供 TLD 服务器的 IP 地址。
  • 顶级域 DNS 服务器,对于每个顶级域名比如 com、org、net、edu 和 gov 和所有的国家级域名 uk、fr、ca 和 jp 都有 TLD 服务器或服务器集群。所有的顶级域列表参见 tld-list.com/ 。TDL 服务器提供了权威 DNS 服务器的 IP 地址。
  • 权威 DNS 服务器,在因特网上具有公共可访问的主机,如 Web 服务器和邮件服务器,这些主机的组织机构必须提供可供访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。一个组织机构的权威 DNS 服务器收藏了这些 DNS 记录。

一般域名服务器的层次结构主要是以上三种,除此之外,还有另一类重要的 DNS 服务器,它是 本地 DNS 服务器(local DNS server)。严格来说,本地 DNS 服务器并不属于上述层次结构,但是本地 DNS 服务器又是至关重要的。每个 ISP(Internet Service Provider) 比如居民区的 ISP 或者一个机构的 ISP 都有一台本地 DNS 服务器。当主机和 ISP 进行连接时,该 ISP 会提供一台主机的 IP 地址,该主机会具有一台或多台其本地 DNS 服务器的 IP地址。通过访问网络连接,用户能够容易的确定 DNS 服务器的 IP地址。当主机发出 DNS 请求后,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 服务器层次系统中。

DNS 缓存

DNS 缓存(DNS caching) 有时也叫做 DNS 解析器缓存,它是由操作系统维护的临时数据库,它包含有最近的网站和其他 Internet 域的访问记录。也就是说, DNS 缓存只是计算机为了满足快速的响应速度而把已加载过的资源缓存起来,再次访问时可以直接快速引用的一项技术和手段。那么 DNS 的缓存是如何工作的呢?

DNS 缓存的工作流程

在浏览器向外部发出请求之前,计算机会拦截每个请求并在 DNS 缓存数据库中查找域名,该数据库包含有最近的域名列表,以及 DNS 首次发出请求时 DNS 为它们计算的地址。

DNS 记录和报文

共同实现 DNS 分布式数据库的所有 DNS 服务器存储了资源记录(Resource Record, RR),RR 提供了主机名到 IP 地址的映射。每个 DNS 回答报文中会包含一条或多条资源记录。RR 记录用于回复客户端查询。

资源记录是一个包含了下列字段的 4 元组

1
复制代码(Name, Value, Type, TTL)

RR 会有不同的类型,下面是不同类型的 RR 汇总表

DNS RR 类型 解释
A 记录 IPv4 主机记录,用于将域名映射到 IPv4 地址
AAAA 记录 IPv6 主机记录,用于将域名映射到 IPv6 地址
CNAME 记录 别名记录,用于映射 DNS 域名的别名
MX 记录 邮件交换器,用于将 DNS 域名映射到邮件服务器
PTR 记录 指针,用于反向查找(IP地址到域名解析)
SRV 记录 SRV记录,用于映射可用服务。

DNS 报文

DNS 有两种报文,一种是查询报文,一种是响应报文,并且这两种报文有着相同的格式,下面是 DNS 的报文格式

下面对报文格式进行解释

  • 前 12 个报文是 首部区域,也就是说首部区域有 12 个字节,第一个字段(标识符)是一个 16 比特的数,用于标示该查询。这个标识符会被复制到对查询的回答报文中,以便让客户用它来匹配发送的请求和接受到的回答。 标志字段含有若干标志,标志字段表示为 1 比特,它用于指出报文是 0-查询报文还是 1-响应报文。
  • 问题区域包含着正在进行的查询信息。这个区域包括:1) 名字字段,包含正在被查询的主机名字;2) 类型字段,指出有关该名字的正被询问的问题类型,例如主机地址是与一个名字相关联(类型 A)还是与某个名字的邮件服务器相关联(类型 MX)。
  • 在来自 DNS 服务器的回答中,回答区域包含了对最初请求的名字的资源记录。上面说过 DNS RR记录是个四元组,而且元组中的 Type 会有不同的类型。在回答报文的回答区域中可以包含多条 RR,因此一个主机名能够有多个 IP 地址。
  • 权威区域 包含了其他权威服务器的记录
  • 附加区域 包含了其他有帮助的记录。

关于具体 DNS 记录的详细介绍我会出一篇文章专门探讨。

P2P 文件分发

我们上面探讨的协议 HTTP、SMTP、DNS 都采用了客户-服务器 模式,这种模式会极大依赖总是打开的基础设施服务器。而 P2P 是客户端与客户端模式,对总是打开的基础设施服务器有最小的依赖。

P2P 的全称是 Peer-to-peer, P2P ,是一种分布式体系结构的计算机网络。在 P2P 体系中,所有的计算机和设备都被称为对等体,他们互相交换工作。对等网络中的每个对等方都等于其他对等方。网络中没有特权对等体,也没有主管理员设备。

从某种意义上说,对等网络是计算机世界中最平等的网络。每个对等方都相等,并且每个对等方具有与其他对等方相同的权利和义务。对等体同时是客户端和服务器。

实际上,对等网络中可用的每个资源都是在对等之间共享的,而无需任何中央服务器。P2P 网络中的共享资源可以是诸如处理器使用率,磁盘存储容量或网络带宽等。

P2P 用来做什么

P2P 的主要目标是共享资源并帮助计算机和设备协同工作,提供特定服务或执行特定任务。如前面说到的,P2P 用于共享各种计算资源,例如网络带宽或磁盘存储空间。 但是,对等网络最常见的例子是 Internet 上的文件共享。 对等网络非常适合文件共享,因为它们允许连接到它们计算机等同时接收文件和发送文件。

BitTorrent 是 P2P 使用的主要协议。

P2P 网络的作用

P2P 网络具有一些使它们有用的特征

  • 很难完全掉线,即使其中的一个对等方掉线,其他对等方仍在运行并进行通信。 为了使 P2P(对等)网络停止工作,你必须关闭所有对等网络。对等网络具有很强的可扩展性。 添加新的对等节点很容易,因为你无需在中央服务器上进行任何中央配置。
  • 当涉及到文件共享时,对等网络越大,速度越快。 在 P2P 网络中的许多对等点上存储相同的文件意味着当某人需要下载文件时,该文件会同时从多个位置下载。

视频流和内容分发网

因特网视频

在流式存储视频应用中,最基础的媒体是预先录制的视频例如电影、电视节目、录制好的体育事件或者用户生成的视频。这些预先录制好的视频会放置在服务器上,用户按需向服务器发送请求来观看视频。许多因特网公司现在提供流式视频,这些公司包括 Netflix、YouTube 、亚马逊和优酷等。

视频式一系列的图像,通常会以一种恒定的速率(如每秒 24 或 30 张图像)来展现。一幅未压缩、数字编码的图像由像素阵列组成,其中每个像素又一些比特编码来表示亮度和颜色。视频的一个重要特征是它能够被压缩、因而可用比特率来权衡视频质量。

HTTP 流和 DASH

在 HTTP 流中,视频只是存储在 HTTP 服务器中的一个文件,每个文件有特定的 URL。当用户想要看视频时,客户与服务器创建一个 TCP 连接并发送该 URL 的 HTTP GET 请求。服务器则以底层网络协议和流量条件允许的尽可能快的速率,在一个 HTTP 响应中发送该文件视频。

尽管 HTTP 流在实践中已经得到广泛部署,但是它由严重缺陷,即所有客户接收到相同编码的视频,但是对于客户而言,带宽时动态变化的,在不同的时间,带宽大小有很大不同。这种情况导致了一种新型 HTTP 流的研发,它常常被称为 经 HTTP 的动态适应性流(Dynamic Adaptive Streaming over HTTP, DASH)。在 DASH 中,视频编码为几个不同的版本,每个版本对应不同的比特率。

DASH 允许客户使用不同的以太网接入速率流失播放具有不同编码速率的视频。使用 3G 连接的客户能够接受一个低比特率的版本,使用光纤能够接受高比特率的版本。

使用 DASH 后,每个视频版本存储在 HTTP 中,每个版本都有一个不同的 URL。HTTP 服务器也会有一个 告示文件(manifest file),为每个版本提供了一个 URL 及其比特率。

内容分发网

现如今,许多因特网视频公司日复一日地向数以百万计的用户按需分发每秒数兆比特的流。对于一个因特网视频公司,或许提供流式视频服务最为直接的方法是建立一个单一的超大规模的数据中心。在数据中心内部存储所有视频,然后把视频返回到全世界范围内的客户。这种方式存在三个问题

  • 如果客户远离数据中心,服务器到客户的分组将跨越许多通信链路并可能通过很多 ISP,造成通信延迟
  • 流式视频可能经过相同的链路发送了许多次,造成带宽和资源浪费。
  • 单点问题,如果单一结点故障,这可能是灾难性的。

为了应对向分布于去啊按时接的用户分发巨量视频数据的挑战,几乎所有主要的视频流公司都利用 内容分发网(Content Distribution Network, CDN)。 CDN 管理分布在多个地理位置上的服务器,在它的服务器上存储视频副本,并且所有试图将每个用户请求定向到一个提供最好用户体验的 CDN 位置。那么服务器如何选址呢?事实上有两种服务器安置原则

  • 深入,它的主要目标是靠近用户,通过减少端用户和 CDN 集群之间链路和路由器的数量,从而改善了用户感受的时延和吞吐量。
  • 邀请做客,这个原则是通过在少量(例如 10 个)关键位置建造大集群来邀请 ISP 来做客,与深入设计原则相比,邀请做客设计通常产生较低的维护和管理开销。

CDN 可以是专用 CDN(private CDN), 即它由内容提供商自己所拥有;另一种 CDN 是 第三方 CDN(third-party CDN),它代表多个内容提供商分发内容。

CDN 分发过程

上面我们探讨了一下 CDN 的选址过程,那么 CDN 是如何工作的呢?

当用户主机中的一个浏览器指令检索一个特定的视频(由 URL 标识)时,CDN 必须能够截获请求,来进行下面的操作

  • 确定此时适用于该客户的 CDN 服务器集群
  • 将客户的请求重定向到集群中的某台服务器上

大多数 CDN 利用 DNS 协议来截获和重定向请求。

下面是 CDN 的具体工作流程

假设一个内容提供商 NetCinema ,雇用了第三方 CDN 公司 KingCDN 来向它的客户分发视频。在 NetCinema 的 Web 网页上,它的每个视频都被指派了一个 URL,该 URL 包括了字符串 video 以及视频本身的标识符。下面要访问 http://video.netcinema.com/6Y7B23V ,它的工作过程如下

  1. 用户访问位于 NetCinema 的 Web 网页
  2. 当用户点击链接 http://video.netcinema.com/6Y7B23V 时,该用户主机发送了对于 video.netcinema.com 的 DNS 请求
  3. 用户本地 DNS 服务器(LDNS, Local DNS) 将该 DNS 请求中继到一台用于 NetCinema 的权威 DNS 服务器,该服务器观察到主机名 video.netcinema.com 中的字符串 video。为了将该 DNS 请求移交给 KingCDN,NetCinema 权威 DNS 服务器并不返回一个 IP 地址,而是向 LDNS 返回一个 KingCDN 域的主机名,如 a1105.kingcdn.com
  4. 从此时起,DNS 请求就会进入 KingCDN 专用 DNS 基础设施,用户的 LDNS 则发送第二个请求,此时是对 a1105.kingcdn.com 的 DNS 请求,KingCDN 的 DNS 系统最终向 LDNS 返回 KingCDN 内容服务器的 IP 地址。所以正是这里,在 KingCDN 的 DNS 系统中,指定了 CDN 服务器,客户将能够从这台服务器接收它的内容
  5. LDNS 向用户主机转发内容服务 CDN 节点的 IP 地址
  6. 一旦客户收到 KingCDN 内容服务器的 IP 地址,它与具有该 IP 地址的服务器创建一条 TCP 连接,并且发出对该视频的 HTTP GET 请求。如果使用了 DASH,服务器将首先向客户发送具有 URL 列表的告示文件,每个 URL 对应视频的每个版本,并且客户将动态的选择来自不同版本的块。

CDN 的集群选择策略

任何 CDN 的部署,其核心是 集群选择策略(cluster selection strategy), 即动态的将客户定向到 CDN 中某个服务器集群或数据中心的机制。一种简单的策略是指派客户到 地理上最为临近(geographically closest) 的集群。这种选择策略忽略了时延和可用带宽随因特网路径时间而变化,总是为特定的客户指派相同的集群;还有一种选择策略是 实时测量(real-time measurement),该机制是基于集群和客户之间的时延和丢包性能执行周期性检查。

文章参考

《计算机网络-自顶向下方法》

baike.baidu.com/item/应用层协议/…

developer.mozilla.org/en-US/docs/…

baike.baidu.com/item/WEB服务器…

baike.baidu.com/item/内容分发网络…

baike.baidu.com/item/HTML/9…

www.jianshu.com/p/3dd8f1879…

DNS原理及其解析过程

en.wikipedia.org/wiki/Decent…

en.wikipedia.org/wiki/Domain…

www.lifewire.com/what-is-a-d…

blog.csdn.net/tianxuhong/…

www.omnisecu.com/tcpip/what-…

www.digitalcitizen.life/what-is-p2p…

本文转载自: 掘金

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

细数springboot中的那些连接池

发表于 2020-02-02

hello~各位读者新年好!

回想起前几天在部署springboot项目到正线时,线上环境要求jdk7,可项目是基于jdk8开发的,springboot也是用的springboot2以上的版本,可以说缝缝补补一整天才搞好能满足线上环境的代码,搞完后当然需要小小的了解一下背后的秘密。

好了,话不多说,我们直接进入正题。

其实切换还不算太麻烦,坑就坑在SpringBoot2切换到SpringBoot1后,默认使用的连接池发生了变化,之前做的压力测试又重新搞了一遍。

怨天尤人貌似消极了哈,小编我可是一个正能量满满的人,所以总结下自己就是:虽然会用,但是没了解技术背后的真相而闹出的乌龙。

接下里我们就一起来检验下SpringBoot2和SpringBoot1使用的默认数据源吧!

一、SpringBoot2的HikariCP

  • 首先在pom文件中需要引入的依赖包:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
dust复制代码    <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<mybatis.spring.boot.version>1.3.1</mybatis.spring.boot.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

</dependencies>
  • 其次在配置文件中需要定义如下属性(不定义时会自动使用默认值)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nestedtext复制代码# spring的相关配置
spring:
application:
name: HikariCP测试
# 数据源的配置
datasource:
# 连接池的配置
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 5
maximum-pool-size: 15
connection-test-query: SELECT 1
max-lifetime: 1800000
connection-timeout: 30000
pool-name: DatebookHikariCP
  • 配置好后,启动成功时你能看到类似这样子的打印信息:
1
2
3
4
5
stylus复制代码2020-01-16 16:23:12.911  INFO 9996 --- [ main ] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2020-01-16 16:23:12.913 INFO 9996 --- [ main ] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'dataSource' has been autodetected for JMX exposure
2020-01-16 16:23:12.924 INFO 9996 --- [ main ] o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource]
2020-01-16 16:23:12.994 INFO 9996 --- [ main ] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 18001 (http) with context path ''
2020-01-16 16:23:13.002 INFO 9996 --- [ main ] c.j.mmzsblog.DatasourceTestApplication : Started DatasourceTestApplication in 6.724 seconds (JVM running for 8.883)

其中第3行[com.zaxxer.hikari:name=dataSource,type=HikariDataSource]这部分就点明了使用的连接池类型

二、SpringBoot1的tomcat-jdbc

降低版本后,我没有看到上面的信息打印,一时差点不知道使用了什么连接池,不过网上都说是tomcat-jdbc;但是相信眼见为实的我,肯定要在哪里打印一下才放心,于是乎,我进行了如下操作:

搞了一个controller来简单的打印一下连接池的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码@RestController
public class testController {

@Resource
private DataSource dataSource;

@GetMapping("/query")
public void query(){
System.out.println("查询到的数据源连接池信息是:"+dataSource);
System.out.println("查询到的数据源连接池类型是:"+dataSource.getClass());
System.out.println("查询到的数据源连接池名字是:"+dataSource.getPoolProperties().getName());
}
}

然后我就看到了如下的打印信息,果真是用的tomcat-jdbc

1
2
3
routeros复制代码查询到的数据源连接池信息是:org.apache.tomcat.jdbc.pool.DataSource@181d8899{ConnectionPool[defaultAutoCommit=null; defaultReadOnly=null; defaultTransactionIsolation=-1; defaultCatalog=null; driverClassName=com.mysql.jdbc.Driver; maxActive=100; maxIdle=100; minIdle=10; initialSize=10; maxWait=30000; testOnBorrow=true; testOnReturn=false; timeBetweenEvictionRunsMillis=5000; numTestsPerEvictionRun=0; minEvictableIdleTimeMillis=60000; testWhileIdle=false; testOnConnect=false; password=********; url=jdbc:mysql://localhost:3306/xxxxxx; username=xxxx; validationQuery=SELECT 1; validationQueryTimeout=-1; validatorClassName=null; validationInterval=3000; accessToUnderlyingConnectionAllowed=true; removeAbandoned=false; removeAbandonedTimeout=60; logAbandoned=false; connectionProperties=null; initSQL=null; jdbcInterceptors=null; jmxEnabled=true; fairQueue=true; useEquals=true; abandonWhenPercentageFull=0; maxAge=0; useLock=false; dataSource=null; dataSourceJNDI=null; suspectTimeout=0; alternateUsernameAllowed=false; commitOnReturn=false; rollbackOnReturn=false; useDisposableConnectionFacade=true; logValidationErrors=false; propagateInterruptState=false; ignoreExceptionOnPreLoad=false; useStatementFacade=true; }
查询到的数据源连接池类型是:class org.apache.tomcat.jdbc.pool.DataSource
查询到的数据源连接池名字是:Tomcat Connection Pool[1-1715657818]

其实,我们从pom文件也能看出其中的门道:

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

依赖文件中的这一个依赖其实就是表明了SpringBoot1使用的是tomcat-jdbc连接池。

哎,现在才知道SpringBoot2.0和SpringBoot1.0版本使用的默认数据库是不一样的。

现在原因是找到了,可是如何解决呢?要不然把SpringBoot1版本的默认连接池修改成和SpringBoot2版本的一样。好,有了想法,那就开干。

其实,在SpringBoot1的版本也是可以使用HikariCP连接池的,操作就是:

  • 首先引入默认配置的数据源处排除掉tomcat-jdbc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码        <!--配置默认数据源 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<!-- 排除默认的tomcat-jdbc数据源 -->
<exclusion>
<groupId>org.apache</groupId>
<artifactId>tomcat-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引用SpringBoot2默认的HikariCP数据源 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.3.1</version>
</dependency>
  • 再在.yml文件中配置HikariCP数据源的相关信息
1
2
3
4
5
6
7
8
9
10
11
12
nestedtext复制代码# spring的相关配置
spring:
# 数据源的配置
datasource:
# 连接池的配置
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 5
maximum-pool-size: 15
connection-test-query: SELECT 1
max-lifetime: 1800000
connection-timeout: 30000

为什么说我此处要将数据源切换成SpringBoot2.0使用的默认数据源呢?因为使用SpringBoot1.0的tomcat-jdbc数据源我怕压力测试出来达不到要求,为了不给测试增加工作压力(小编我就是这么好的一个人)

所以我进行了上面的替换操作。

不过这样做肯定也是有好处的。好处就在于HikariCP那迷人的优势:

  • 1、字节码级别优化(很多方法通过JavaAssist生成)
  • 2、大量小改进
    • 用FastStatementList代替ArrayList
    • 无锁集合ConcurrentBag
    • 代理类的优化(比如:,用invokestatic代替invokevirtual)

正如官网的这个对比图显示的一样:它更快

其实话又说回来,要是我一开始就是用第三方数据库,岂不是就不存在这些自己搞出来的幺蛾子了!

比如阿里巴巴的Druid连接池不就是个优秀的产品么!它到底有多优秀呢?你先看它的使用:

三、其它连接池(如:Druid)

3.1、SpringBoot1.0中引用Druid

和前文的SpringBoot1.0中引用HikariCP一样,先排除默认数据源tomcat-jdbc再引用想要使用的连接池

  • 3.1.1、首先引入默认配置的数据源处排除掉tomcat-jdbc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xml复制代码        <!--配置默认数据源 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<!-- 排除默认的tomcat-jdbc数据源 -->
<exclusion>
<groupId>org.apache</groupId>
<artifactId>tomcat-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引用阿里巴巴的druid数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
  • 3.1.2、再在.yml文件中配置Druid数据源的相关信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yaml复制代码spring:
# 数据源的配置
datasource:
# 连接池的配置
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
max-active: 10
min-idle: 5
max-wait: 30000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
validation-query: SELECT 1 FROM DUAL
validation-query-timeout: 60000
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 100000
  • 3.1.3、再写个配置类加载数据源
1
2
3
4
5
6
7
8
9
10
11
less复制代码@Configuration
@ConditionalOnClass(DruidDataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.alibaba.druid.pool.DruidDataSource", matchIfMissing = true)
public class DataSourceConfig {

@Bean
@ConfigurationProperties("spring.datasource.druid")
public DataSource dataSourceOne() {
return DruidDataSourceBuilder.create().build();
}
}
  • 3.1.4、启动效果:
1
2
3
4
5
6
7
stylus复制代码2020-01-17 16:59:32.804  INFO 8520 --- [ main ] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2020-01-17 16:59:32.806 INFO 8520 --- [ main ] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'dataSourceOne' has been autodetected for JMX exposure
2020-01-17 16:59:32.808 INFO 8520 --- [ main ] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'statFilter' has been autodetected for JMX exposure
2020-01-17 16:59:32.818 INFO 8520 --- [ main ] o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'dataSourceOne': registering with JMX server as MBean [com.alibaba.druid.spring.boot.autoconfigure:name=dataSourceOne,type=DruidDataSourceWrapper]
2020-01-17 16:59:32.822 INFO 8520 --- [ main ] o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'statFilter': registering with JMX server as MBean [com.alibaba.druid.filter.stat:name=statFilter,type=StatFilter]
2020-01-17 16:59:32.932 INFO 8520 --- [ main ] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 18001 (http)
2020-01-17 16:59:32.943 INFO 8520 --- [ main ] c.j.mmzsblog.DatasourceTestApplication : Started DatasourceTestApplication in 8.328 seconds (JVM running for 10.941)

3.2、SpringBoot2.0中引用Druid

在SpringBoot2.0中引用Druid和在SpringBoot1.0中引入类似;

  • 3.2.1、不需要排除默认配置的数据源,直接引入置Druid数据源
1
2
3
4
5
6
7
8
9
10
11
xml复制代码        <!-- 引用阿里巴巴的druid数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
  • 3.2.2、在.yml文件中配置的Druid数据源的相关信息同3.1.3一样
  • 3.2.3、再写个配置类加载数据源同3.1.3一样
  • 3.2.4、启动后你同样能看到打印出类似的信息
1
pgsql复制代码Located MBean 'dataSourceOne': registering with JMX server as MBean [com.alibaba.druid.spring.boot.autoconfigure:name=dataSourceOne,type=DruidDataSourceWrapper]

3.3、优秀在哪?

看了上面的使用,超级简单又木有?

首先我们看看druid官网给出的几个传统连接池之间的对比吧:从上表可以看出,Druid连接池在性能、监控、诊断、安全、扩展性这些方面远远超出竞品。

官网是这样介绍它的:

Druid连接池是阿里巴巴开源的数据库连接池项目。Druid连接池为监控而生,内置强大的监控功能,监控特性不影响性能。功能强大,能防SQL注入,内置Loging能诊断Hack应用行为。

所以,小编我倒腾了这么久,想明白了一件事,我以后还是用阿里爸爸的Druid连接池吧,接入简单,还自带监控,并且它可是经过阿里巴巴各大系统考验过的产品,值得信赖,省事省心啊。

参考:

  • 1:HikariCP的优点:https://www.jianshu.com/p/129efe2c8e49
  • 2、druid官网:https://github.com/alibaba/druid/

欢迎关注公众号:Java学习之道

个人博客网站:www.mmzsblog.cn

本文转载自: 掘金

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

1…834835836…956

开发者博客

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