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

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


  • 首页

  • 归档

  • 搜索

Java常用异常整理 异常类的继承关系 常用异常类 常用异常

发表于 2017-11-14

填坑,整理下Java的常用异常。正确使用异常在实际编码中非常重要,但面试中的意义相对较小,因为对异常的理解和应用很难通过几句话或几行代码考查出来,不过我们至少应答出三点:异常类的继承关系、常用异常类、常用异常类的使用场景,下文将围绕这三点介绍。

异常类的继承关系

image.png

image.png

Java中,所有异常都继承自Throwable类(一个完整可用的类)。整体上分为Error、Exception两个大类,Exception大类又分为UncheckedException(继承于RuntimeException)和CheckedException(继承于Exception,但不继承于RuntimeException)。

为了帮助理解,我在每个类别下都给出了两个常用子类,如Error包括OutOfMemoryError、AssertionError等;UncheckedException包括NullPointerException、IllegalArgumentException;CheckedException包括IOException、InterruptedException。面试画异常类的继承关系时,要求能清楚的说明几个类别并分类别举几个常用的异常类。

常用异常类

下面分类别扩充一下常用的异常类,字典序排序:

类别 常用异常类
Error AssertionError、OutOfMemoryError、StackOverflowError
UncheckedException AlreadyBoundException、ClassCastException、ConcurrentModificationException、IllegalArgumentException、IllegalStateException、IndexOutOfBoundsException、JSONException、NullPointerException、SecurityException、UnsupportedOperationException
CheckedException ClassNotFoundException、CloneNotSupportedException、FileAlreadyExistsException、FileNotFoundException、InterruptedException、IOException、SQLException、TimeoutException、UnknownHostException

需要着重理解的是UncheckedException。

上述异常类都是很常见的,但其中几个异常类设计的不好,需要注意:

  • ConcurrentModificationException:实现“快速失败”的机制,但实际上,“快速失败”机制本身仍然无法保证并发环境下安全性,参考源码|从源码分析非线程安全集合类的不安全迭代器。因此,虽然该异常很常见,不要去依赖它。
  • JSONException:常见于json字符串解析失败的情况,但遮蔽了大量的失败细节,往往很难根据该异常作出处理。如果项目中大量使用json,建议使用第三方的json解析库,如gson等。
  • UnsupportedOperationException:这是一种编码上的恶性妥协,经常在抽象类的成员方法中被用户主动抛出,表示该方法还未实现等,但由于是UncheckedException,运行期才能够发现,完全无益于编码期间的安全性。自己编码时尽量不要使用。
  • SQLException:与JSONException原因相似,但其遮蔽的失败细节范围更广。同时,SQLException还是一个CheckedException,在不能解决问题的情况下,又使代码变的臃肿不堪。建议同。如果做Java Web开发,热门的ORM库都能解决上述问题。

常用异常类的使用场景

常用异常还是有点多,下面分别讲解上述三个类别的使用场景,并在每个类别中选一个例子进行讲解。

Error

Error通常描述了系统级的错误,并且程序猿无法主动处理——当然,系统级错误也有可能由代码间接导致,这不在我们的讨论范围内。发生系统级错误的时候,系统环境已经不健康了,因此,Error不强制捕获或声明,也就是不强制处理,一般情况下只需要把异常信息记录下来(如果能记下当时的系统快照更好)。

OutOfMemoryError

当可用内存不足时,会由JVM抛出OutOfMemoryError。一般由三种原因导致:

  • 堆设置过小,不满足正常的内存需求
  • 代码中存在内存泄露,占用了大量内存而不能被回收
  • 选择的GC算法与某些极端的应用场景不匹配,内存碎片过多,没有足够大的连续空间分配给对象

JVM抛出OutOfMemoryError前,会尝试进行一次Full GC,如果GC后可用内存还是不足,才会抛出OutOfMemoryError。因此,这时程序猿必然无法主动处理这一问题,只能等程序崩溃后再去查证原因。

查证OutOfMemoryError的技巧足以单开一篇文章了,本文不作深入。

UncheckedException

严格来说,Error也可以被划归UncheckedException,但我们更习惯用UncheckedException描述运行期发生,通常由于代码问题直接引起的程序相关的错误,并且程序猿无法主动处理。注意区分,系统级错误都应该用Error描述。UncheckedException发生的大部分情况是代码写挫了,因此,UncheckedException也不强制捕获或声明,也就是不强制处理,一般情况下记下日志即可。

不同的是,如果可能,要保证UncheckedException是可控的(在异常被动抛出前检查并主动抛出)。

JSONException就是不可控的。

NullPointerException

NullPointerException是最常见的UncheckedException。如果在一个空指针上引用方法或变量等,则运行期会抛出NullPointerException。空指针让程序变的不可控:如果任由空指针在程序运行期随意传递、使用,我们将无法确定程序的行为,也无法确定捕获NullPointerException时程序所处的状态。

解决这一问题的方法很简单:

  • 尽早检查并主动抛出异常
  • 单独、提前处理边界条件
  • 尽量不使用null表示状态,特别是在集合中

前两条原则通用于大部分UncheckedException,可参考String#toLowerCase()的例子。第三条原则需要在代码的健壮与简洁之间做出权衡,优先保证简洁清晰,需要健壮再去健壮。

CheckedException

猴子对CheckedException的理解不到位,如果各位有更好的理解希望能交流一下。以下讲猴子“不到位”的理解。

CheckedException描述了外部环境导致的不太严重的错误,程序猿应该主动处理。注意与系统级错误区分,系统级错误通常是不可恢复的。因此,CheckedException强制捕获或声明,程序猿必须处理。记录日志,包装后再次抛出,在方法签名中声明,是三种最常见的做法。

同UncheckedException一样,CheckedException也要保证是可控的。对CheckedException的可控性要求更高,不仅要主动检查,还要在捕获到异常时,作出合适的处理。

不过,猴子认为大量CheckedException的存在就是个错误。比如FileAlreadyExistsException,更应该由用户主动检查发现,而不应该依赖于异常。对于可以处理的异常,本质上相当于控制流问题,用异常去表达反而让控制流变模糊。不过有时候猴子写小项目,也会为了简化代码,直接将相关异常声明在方法签名中,并一路声明干到main方法。恩,everything is a trade-off。

IOException

产生IOException的原因非常多,但很多时候我们并不关心细节原因,因为文件系统是一个不太可控的因素,这时我们可以以IOException为粒度处理;某些需要关心细节的异常情况,则应使用IOException的子类,以分情况处理。

前面总结的FileAlreadyExistsException、FileNotFoundException、UnknownHostException等,都是IOException的子类。这三种异常恰好都是可以处理的。

挖坑,InterruptedException也相当重要,后面要专门写一篇来整理。

总结

实际的编码工作中,我们应正确的使用异常表达代码设计,并尽可能使用JDK提供的异常类。JDK内置了非常多的异常类,我们只需要掌握一些常用的异常类,然后举一反三。


本文链接:Java常用异常整理
作者:猴子007
出处:monkeysayhi.github.io
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名及链接。

本文转载自: 掘金

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

你真的会阅读Java的异常信息吗? 需要内化的内容 扩展 总

发表于 2017-11-14

给出如下异常信息:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码java.lang.RuntimeException: level 2 exception
at com.msh.demo.exceptionStack.Test.fun2(Test.java:17)
at com.msh.demo.exceptionStack.Test.main(Test.java:24)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Caused by: java.io.IOException: level 1 exception
at com.msh.demo.exceptionStack.Test.fun1(Test.java:10)
at com.msh.demo.exceptionStack.Test.fun2(Test.java:15)
... 6 more

学这么多年Java,你真的会阅读Java的异常信息吗?你能说清楚异常抛出过程中的事件顺序吗?

需要内化的内容

写一个demo测试

上述异常信息在由一个demo产生:

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
复制代码package com.msh.demo.exceptionStack;

import java.io.IOException;

/**
* Created by monkeysayhi on 2017/10/1.
*/
public class Test {
private void fun1() throws IOException {
throw new IOException("level 1 exception");
}

private void fun2() {
try {
fun1();
} catch (IOException e) {
throw new RuntimeException("level 2 exception", e);
}
}


public static void main(String[] args) {
try {
new Test().fun2();
} catch (Exception e) {
e.printStackTrace();
}
}
}

这次我复制了完整的文件内容,使文章中的代码行号和实际行号一一对应。

根据上述异常信息,异常抛出过程中的事件顺序是:

  1. 在Test.java的第10行,抛出了一个IOExceotion(“level 1 exception”) e1
  2. 异常e1被逐层向外抛出,直到在Test.java的第15行被捕获
  3. 在Test.java的第17行,根据捕获的异常e1,抛出了一个RuntimeException(“level 2 exception”, e1) e2
  4. 异常e2被逐层向外抛出,直到在Test.java的第24行被捕获
  5. 后续没有其他异常信息,经过必要的框架后,由程序自动或用户主动调用了e2.printStackTrace()方法

如何阅读异常信息

那么,如何阅读异常信息呢?有几点你需要认识清楚:

  • 异常栈以FILO的顺序打印,位于打印内容最下方的异常最早被抛出,逐渐导致上方异常被抛出。位于打印内容最上方的异常最晚被抛出,且没有再被捕获。从上到下数,第i+1个异常是第i个异常被抛出的原因cause,以“Caused by”开头。
  • 异常栈中每个异常都由异常名+细节信息+路径组成。异常名从行首开始(或紧随”Caused by”),紧接着是细节信息(为增强可读性,需要提供恰当的细节信息),从下一行开始,跳过一个制表符,就是路径中的一个位置,一行一个位置。
  • 路径以FIFO的顺序打印,位于打印内容最上方的位置最早被该异常经过,逐层向外抛出。最早经过的位置即是异常被抛出的位置,逆向debug时可从此处开始;后续位置一般是方法调用的入口,JVM捕获异常时可以从方法栈中得到。对于cause,其可打印的路径截止到被包装进下一个异常之前,之后打印“… 6 more”,表示cause作为被包装异常,在这之后还逐层向外经过了6个位置,但这些位置与包装异常的路径重复,所以在此处省略,而在包装异常的路径中打印。“… 6 more”的信息不重要,可以忽略。

现在,回过头再去阅读示例的异常信息,是不是相当简单?

为了帮助理解,我尽可能通俗易懂的描述了异常信息的结构和组成元素,可能会引入一些纰漏。阅读异常信息是Java程序猿的基本技能,希望你能内化它,忘掉这些冗长的描述。

如果还不理解,建议你亲自追踪一次异常的创建和打印过程,使用示例代码即可,它很简单但足够。难点在于异常是JVM提供的机制,你需要了解JVM的实现;且底层调用了很多native方法,而追踪native代码没有那么方便。

扩展

为什么有时我在日志中只看到异常名”java.lang.NullPointerException”,却没有异常栈

示例的异常信息中,异常名、细节信息、路径三个元素都有,但是,由于JVM的优化,细节信息和路径可能会被省略。

这经常发生于服务器应用的日志中,由于相同异常已被打印多次,如果继续打印相同异常,JVM会省略掉细节信息和路径队列,向前翻阅即可找到完整的异常信息。

猴哥之前使用Yarn的Timeline Server时遇到过该问题。你能体会那种感觉吗?卧槽,为什么只有异常名没有异常栈?没有异常栈怎么老子怎么知道哪里抛出的异常?线上服务老子又不能停,全靠日志了啊喂!

网上有不少相同的case,比如NullPointerException丢失异常堆栈信息,读者可以参照这个链接实验一下。

如何在异常类中添加成员变量

为了恰当的表达一个异常,我们有时候需要自定义异常,并添加一些成员变量,打印异常栈时,自动补充打印必要的信息。

追踪打印异常栈的代码:

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
复制代码...
public void printStackTrace() {
printStackTrace(System.err);
}
...
public void printStackTrace(PrintStream s) {
printStackTrace(new WrappedPrintStream(s));
}
...
private void printStackTrace(PrintStreamOrWriter s) {
// Guard against malicious overrides of Throwable.equals by
// using a Set with identity equality semantics.
Set<Throwable> dejaVu =
Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
dejaVu.add(this);

synchronized (s.lock()) {
// Print our stack trace
s.println(this);
StackTraceElement[] trace = getOurStackTrace();
for (StackTraceElement traceElement : trace)
s.println("\tat " + traceElement);

// Print suppressed exceptions, if any
for (Throwable se : getSuppressed())
se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);

// Print cause, if any
Throwable ourCause = getCause();
if (ourCause != null)
ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
}
}
...

暂不关心同步问题,可知,打印异常名和细节信息的代码为:

1
复制代码            s.println(this);

JVM在运行期通过动态绑定实现this引用上的多态调用。继续追踪的话,最终会调用this实例的toString()方法。所有异常的最低公共祖先类是Throwable类,它提供了默认的toString()实现,大部分常见的异常类都没有覆写这个实现,我们自定义的异常也可以直接继承这个实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码...
public String toString() {
String s = getClass().getName();
String message = getLocalizedMessage();
return (message != null) ? (s + ": " + message) : s;
}
...
public String getLocalizedMessage() {
return getMessage();
}
...
public String getMessage() {
return detailMessage;
}
...

显然,默认实现的打印格式就是示例的异常信息格式:异常名(全限定名)+细节信息。detailMessage由用户创建异常时设置,因此,如果有自定义的成员变量,我们通常在toString()方法中插入这个变量。参考com.sun.javaws.exceptions包中的BadFieldException,看看它如何插入自定义的成员变量field和value:

1
2
3
复制代码  public String toString() {
return this.getValue().equals("https")?"BadFieldException[ " + this.getRealMessage() + "]":"BadFieldException[ " + this.getField() + "," + this.getValue() + "]";
}

严格的说,BadFieldException的toString中并没有直接插入field成员变量。不过这不影响我们理解,感兴趣的读者可自行翻阅源码。

总结

根据异常信息debug是程序员的基本技能,这里围绕异常信息的阅读和打印过程作了初步探索,后续还会整理一下常用的异常类,结合程序猿应该记住的几条基本规则,更好的理解如何用异常帮助我们写出clean code。

Java相当完备的异常处理机制是一把双刃剑,用好它能增强代码的可读性和鲁棒性,用不好则会让代码变的更加不可控。例如,在空指针上调用成员方法,运行期会抛出异常,这是很自然的——但是,是不可控的等待它在某个时刻某个位置抛出异常(实际上还是“确定”的,但对于debug来说是“不确定”的),还是可控的在进入方法伊始就检查并主动抛出异常呢?进一步的,哪些异常应该被即刻处理,哪些应该继续抛到外层呢?抛往外层时,何时需要封装异常呢?看看String#toLowerCase(),看看ProcessBuilder#start(),体会一下。


本文链接:你真的会阅读Java的异常信息吗?
作者:猴子007
出处:monkeysayhi.github.io
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名及链接。

本文转载自: 掘金

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

用经验主义方法从用户的角度预测用户观看视频时的体验质量

发表于 2017-11-14

临时链接已失效
微信公众平台运营中心

本文转载自: 掘金

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

shedlock源码解析

发表于 2017-11-14

序

本文主要解析一下shedlock的实现。

LockProvider

shedlock-core-0.16.1-sources.jar!/net/javacrumbs/shedlock/core/LockProvider.java

1
2
3
4
5
6
7
8
复制代码public interface LockProvider {

/**
* @return If empty optional has been returned, lock could not be acquired. The lock
* has to be released by the callee.
*/
Optional<SimpleLock> lock(LockConfiguration lockConfiguration);
}

LockProvider入参是lockConfiguration,返回SimpleLock。

StorageBasedLockProvider

shedlock-core-0.16.1-sources.jar!/net/javacrumbs/shedlock/support/StorageBasedLockProvider.java

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
复制代码public class StorageBasedLockProvider implements LockProvider {
private final StorageAccessor storageAccessor;
private final LockRecordRegistry lockRecordRegistry = new LockRecordRegistry();

protected StorageBasedLockProvider(StorageAccessor storageAccessor) {
this.storageAccessor = storageAccessor;
}

@Override
public Optional<SimpleLock> lock(LockConfiguration lockConfiguration) {
boolean lockObtained = doLock(lockConfiguration);
if (lockObtained) {
return Optional.of(new StorageLock(lockConfiguration, storageAccessor));
} else {
return Optional.empty();
}
}

/**
* Sets lockUntil according to LockConfiguration if current lockUntil &lt;= now
*/
protected boolean doLock(LockConfiguration lockConfiguration) {
String name = lockConfiguration.getName();

if (!lockRecordRegistry.lockRecordRecentlyCreated(name)) {
// create document in case it does not exist yet
if (storageAccessor.insertRecord(lockConfiguration)) {
lockRecordRegistry.addLockRecord(name);
return true;
}
lockRecordRegistry.addLockRecord(name);
}

return storageAccessor.updateRecord(lockConfiguration);
}

private static class StorageLock implements SimpleLock {
private final LockConfiguration lockConfiguration;
private final StorageAccessor storageAccessor;

StorageLock(LockConfiguration lockConfiguration, StorageAccessor storageAccessor) {
this.lockConfiguration = lockConfiguration;
this.storageAccessor = storageAccessor;
}

@Override
public void unlock() {
storageAccessor.unlock(lockConfiguration);
}
}

}

使用StorageAccessor来实现加锁

LockManager

shedlock-core-0.16.1-sources.jar!/net/javacrumbs/shedlock/core/LockManager.java

1
2
3
4
5
6
复制代码/**
* Executes task if not locked.
*/
public interface LockManager {
void executeWithLock(Runnable task);
}

默认实现
shedlock-core-0.16.1-sources.jar!/net/javacrumbs/shedlock/core/DefaultLockManager.java

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
复制代码public class DefaultLockManager implements LockManager {
private static final Logger logger = LoggerFactory.getLogger(DefaultLockManager.class);

private final LockingTaskExecutor lockingTaskExecutor;
private final LockConfigurationExtractor lockConfigurationExtractor;

public DefaultLockManager(LockProvider lockProvider, LockConfigurationExtractor lockConfigurationExtractor) {
this(new DefaultLockingTaskExecutor(lockProvider), lockConfigurationExtractor);
}

public DefaultLockManager(LockingTaskExecutor lockingTaskExecutor, LockConfigurationExtractor lockConfigurationExtractor) {
this.lockingTaskExecutor = requireNonNull(lockingTaskExecutor);
this.lockConfigurationExtractor = requireNonNull(lockConfigurationExtractor);
}

@Override
public void executeWithLock(Runnable task) {
Optional<LockConfiguration> lockConfigOptional = lockConfigurationExtractor.getLockConfiguration(task);
if (!lockConfigOptional.isPresent()) {
logger.debug("No lock configuration for {}. Executing without lock.", task);
task.run();
} else {
lockingTaskExecutor.executeWithLock(task, lockConfigOptional.get());
}
}
}

委托给lockingTaskExecutor来加锁
shedlock-core-0.16.1-sources.jar!/net/javacrumbs/shedlock/core/DefaultLockingTaskExecutor.java

1
2
3
> public class DefaultLockingTaskExecutor implements LockingTaskExecutor {  
> private static final Logger logger = LoggerFactory.getLogger(DefaultLockingTaskExecutor.class);
> private final LockProvider lockProvider;

复制代码public DefaultLockingTaskExecutor(LockProvider lockProvider) {
this.lockProvider = requireNonNull(lockProvider);
}

@Override
public void executeWithLock(Runnable task, LockConfiguration lockConfig) {
Optional lock = lockProvider.lock(lockConfig);
if (lock.isPresent()) {
try {
logger.debug(“Locked {}.”, lockConfig.getName());
task.run();
} finally {
lock.get().unlock();
logger.debug(“Unlocked {}.”, lockConfig.getName());
}
} else {
logger.debug(“Not executing {}. It’s locked.”, lockConfig.getName());
}
}

1
}

复制代码>这里跟lockProvider衔接上

SpringLockableTaskSchedulerFactoryBean(偷梁换柱)

shedlock-spring-0.16.1-sources.jar!/net/javacrumbs/shedlock/spring/SpringLockableTaskSchedulerFactoryBean.java

1
2


复制代码@Override
public Class<?> getObjectType() {
return LockableTaskScheduler.class;
}

@Override
protected LockableTaskScheduler createInstance() throws Exception {
return new LockableTaskScheduler(
taskScheduler,
new DefaultLockManager(lockProvider, new SpringLockConfigurationExtractor(defaultLockAtMostFor, defaultLockAtLeastFor, embeddedValueResolver))
);
}

1
2


复制代码>主要是LockableTaskScheduler的工厂方法

LockableTaskScheduler(task scheduler lock wrapper)

shedlock-spring-0.16.1-sources.jar!/net/javacrumbs/shedlock/spring/LockableTaskScheduler.java

1
2
3
public class LockableTaskScheduler implements TaskScheduler, DisposableBean {  
private final TaskScheduler taskScheduler;
private final LockManager lockManager;

复制代码public LockableTaskScheduler(TaskScheduler taskScheduler, LockManager lockManager) {
this.taskScheduler = requireNonNull(taskScheduler);
this.lockManager = requireNonNull(lockManager);
}

@Override
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
return taskScheduler.schedule(wrap(task), trigger);
}

@Override
public ScheduledFuture<?> schedule(Runnable task, Date startTime) {
return taskScheduler.schedule(wrap(task), startTime);
}

@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period) {
return taskScheduler.scheduleAtFixedRate(wrap(task), startTime, period);
}

@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period) {
return taskScheduler.scheduleAtFixedRate(wrap(task), period);
}

@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay) {
return taskScheduler.scheduleWithFixedDelay(wrap(task), startTime, delay);
}

@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long delay) {
return taskScheduler.scheduleWithFixedDelay(wrap(task), delay);
}

private Runnable wrap(Runnable task) {
return new LockableRunnable(task, lockManager);
}

@Override
public void destroy() throws Exception {
if (taskScheduler instanceof DisposableBean) {
((DisposableBean) taskScheduler).destroy();
}
}

1
}

复制代码>对task scheduler包装了一层,织入了lock的逻辑

问题

上面将了半天,讲了lockProvider以及lockManager,还有LockableTaskScheduler是如何给task scheduler加上锁的,还有LockableTaskScheduler的工厂方法SpringLockableTaskSchedulerFactoryBean。那么问题来了,spring的schedule凭什么就使用你配置的LockableTaskScheduler呢?

1
2
3
4
5
6
7
8
@Bean  
public ScheduledLockConfiguration scheduledLockConfiguration(LockProvider lockProvider) {
return ScheduledLockConfigurationBuilder
.withLockProvider(lockProvider)
.withPoolSize(10)
.withDefaultLockAtMostFor(Duration.ofMinutes(10))
.build();
}

这种配置仅仅当spring工厂里头没有配置taskScheduler的时候,起作用。如果项目已经显示指定taskScheduler的时候,那么就不会使用LockableTaskScheduler。不过可以通过实现SchedulingConfigurer接口强制指定使用LockableTaskScheduler。

doc

  • 使用shedlock将spring schedule上锁

本文转载自: 掘金

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

【Docker】Docker三剑客实践之部署集群

发表于 2017-11-14

作者:不洗碗工作室 - Marklux
出处:marklux.cn/blog/55
版权归作者所有,转载请注明出处

前言

DOCKER技术在推出后掀起了一阵容器化技术的热潮,容器化使得服务的部署变得极其简易,这为微服务和分布式计算提供了很大的便利。

为了把容器化技术的优点发挥到极致,docker公司先后推出了三大技术:docker-machine,docker-compose,docker-swarm,可以说是几乎实现了容器化技术中所有可能需要的底层技术手段。

在使用go语言实现了判题引擎并打包好docker镜像后,就需要进行分布式判题的编写,这次就让我们手动实践,尝试使用docker的三大杀器来部署一个多机器构成的判题服务集群。

三剑客简介

docker-machine

docker技术是基于Linux内核的cgroup技术实现的,那么问题来了,在非Linux平台上是否就不能使用docker技术了呢?答案是可以的,不过显然需要借助虚拟机去模拟出Linux环境来。

docker-machine就是docker公司官方提出的,用于在各种平台上快速创建具有docker服务的虚拟机的技术,甚至可以通过指定driver来定制虚拟机的实现原理(一般是virtualbox)。

docker-compose

docker镜像在创建之后,往往需要自己手动pull来获取镜像,然后执行run命令来运行。当服务需要用到多种容器,容器之间又产生了各种依赖和连接的时候,部署一个服务的手动操作是令人感到十分厌烦的。

dcoker-compose技术,就是通过一个.yml配置文件,将所有的容器的部署方法、文件映射、容器连接等等一系列的配置写在一个配置文件里,最后只需要执行docker-compose up命令就会像执行脚本一样的去一个个安装容器并自动部署他们,极大的便利了复杂服务的部署。

docker-swarm

swarm是基于docker平台实现的集群技术,他可以通过几条简单的指令快速的创建一个docker集群,接着在集群的共享网络上部署应用,最终实现分布式的服务。

相比起zookeeper等集群管理框架来说,swarm显得十分轻量,作为一个工具,它把节点的加入、管理、发现等复杂的操作都浓缩为几句简单的命令,并且具有自动发现节点和调度的算法,还支持自定制。虽然swarm技术现在还不是非常成熟,但其威力已经可见一般。

浅谈docker服务架构和远程API

在正式使用docker技术部署集群应用时,我们应该先来了解一下docker工作的一些底层原理,和docker远程调用的API,这样才能大体了解集群究竟是如何运作的。

daemon

之前的docker入门文章中讲过,docker的基础服务,比如容器的创建、查看、停止、镜像的管理,其实都是由docker的守护进程(daemon)来实现的。

每次执行的docker指令其实都是通过向daemon发送请求来实现的。

daemon的运作(通信模式)主要有两种,一种是通过unix套接字(默认,但只能在本地访问到,比较安全),一种是通过监听tcp协议地址和端口来实现(这个可以实现在远程调用到docker服务)。

远程API

除了通过远程tcp协议访问远程主机上的docker服务外,docker还提供了一套基于HTTP的API,可以使用curl来实现操作远程主机上的docker服务,这为开发基于WEB的docker服务提供了便利。

远程docker使用示例

最终实现集群的时候实际是使用docker的远程调用来将不同的docker主机连接成一个整体的(通过tcp协议)。

我们不妨先来手动模拟尝试一下docker服务的远程调用吧。

首先需要在提供服务的主机上将docker的运行方式改为tcp,具体方法为修改/etc/default/docker中的DOCKER_OPTS为如下内容

1
复制代码-H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock

-H 后的参数是自己定义的要绑定的tcp地址和端口,成功绑定后重启docker服务就可以在该端口访问到docker的daemon服务。

不幸的是:

  • 在OSX平台上,并没有找到docker的daemon配置文件
  • 在OSX平台上,使用docker -H tcp://0.0.0.0:4243 -H unix:///var/run/docker.sock -d这样的命令来尝试以tcp的方式启动docker daemon也是失败的,并没有任何作用
  • 目前推测除了Linux平台上,其他平台上的配置方法都不太一样,但是在网络上暂时没有找到解决方案,所以后面的操作我只能通过在本地创建多个docker-machine的方式来模拟实现远程调用。

假设我们在192.168.1.123这台主机上开启了docker服务,监听了2375端口,那么我们就可以在同一网段的其他主机上(比如192.168.1.233)通过docker -H tcp://192.168.1.123:2345 <command>的方式调用到该主机上的docker服务。

比如

1
2
3
复制代码docker -H tcp://192.168.1.123:2345 ps
docker -H tcp://192.168.1.123:2345 images
docker -H tcp://192.168.1.123:2345 run ...

最终swarm构建集群的时候,就是通过这样的远程服务调用来调度各个节点的。

集群和分布式运算

在正式开始实践集群之前,我们有必要了解究竟什么是集群,什么是分布式计算。

首先,这两者有一个共同点,就是他们都是使用了多个服务节点的,通俗的说,就是要用到多台服务器协同工作(不一定是实体,也可能是虚拟机)。

而两者的区别在于:

  • 集群是多台机器执行同一个业务,每次根据调度算法寻找最合适的节点来执行该业务
  • 分布式计算是将一个业务拆分成多个独立的部分,由多台机器共同协作完成

集群的优点在于,当业务的需要的资源比较大时,可以避免由一个服务器去独自承担压力,而且即便有一个节点宕机了,业务仍然可以继续正常运行。这有点类似于负载均衡。

分布式的优点则是在计算上,可以协同多台机器发挥计算的威力,进行需要超高性能的运算。

构建集群

说现在我们正式开始构建集群。

使用docker-machine创建节点

由于实体机器的缺乏以及在osx上无法正常开启tcp的docker服务,我们基于docker-machine来创建多个虚拟机,作为集群中的节点。

执行下面的命令就可以创建一个新的docker-machine虚拟机manager1

1
复制代码docker-machine create --driver virtualbox manager1

在创建了虚拟机后,可以使用docker-machine env manager1来查看虚拟机manager1的相关信息,包括IP地址等

现在我们继续执行命令创建worker1和worker2两个节点,使用docker-machine ls命令可以查看到所有正在工作的虚拟机:

1
2
3
4
5
复制代码docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
manager1 - virtualbox Running tcp://192.168.99.100:2376 v17.06.1-ce
worker1 - virtualbox Running tcp://192.168.99.101:2376 v17.06.1-ce
worker2 - virtualbox Running tcp://192.168.99.102:2376 v17.06.1-ce

创建docker machine后,可以通过docker-machine ssh manager1 <command>的方式来访问虚拟机,执行指令。

创建swarm集群

初始化一个swarm集群的命令为:

1
复制代码docker swarm init --listen-addr <MANAGER-IP>:<PORT> --advertise-addr <IP>

--listen-addr参数是管理者节点的docker服务所在的IP:PORT,也就是说,可以通过这个组合访问到该节点的docker服务。

--advertise-addr是广播地址,也就是其他节点加入该swarm集群时,需要访问的IP

现在我们在manager1节点里创建swarm网络,执行

1
复制代码docker-machine ssh manager1 docker swarm init --listen-addr 192.168.99.100:2377 --advertise-addr 192.168.99.100

返回响应:

1
2
3
4
5
6
7
8
9
复制代码Swarm initialized: current node (23lkbq7uovqsg550qfzup59t6) is now a manager.

To add a worker to this swarm, run the following command:

docker swarm join \
--token SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-c036gwrakjejql06klrfc585r \
192.168.99.100:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

这样便创建了一个swarm集群,并且manager1节点目前是以管理者的身份加入在节点中的。

现在我们把worker1和worker2两个节点加入到swarm集群中去,分别在两个节点的虚拟机中执行docker swarm join --token ..即可:

1
2
3
4
复制代码docker-machine ssh worker1 docker swarm join --token \
SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-c036gwrakjejql06klrfc585r \
192.168.99.100:2377
This node joined a swarm as a worker.
1
2
3
4
复制代码docker-machine ssh worker2 docker swarm join --token \
SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-c036gwrakjejql06klrfc585r \
192.168.99.100:2377
This node joined a swarm as a worker.

在任何一个节点上执行docker node ls都可以查看到当前整个集群中的所有节点:

1
2
3
4
5
复制代码docker-machine ssh manager1 docker node ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
manager1 - virtualbox Running tcp://192.168.99.100:2376 v1.12.3
worker1 - virtualbox Running tcp://192.168.99.101:2376 v1.12.3
worker2 - virtualbox Running tcp://192.168.99.102:2376 v1.12.3

创建跨主机网络

集群建立完毕之后我们要做的就是在集群上部署我们的服务。但是首先应该让所有的节点处在一个共享的网络中,这样当我们把服务部署在这个共享网络中,就相当于部署在整个集群中了。

使用docker network ls可以查看到当前主机所参与的所有网络:

1
2
3
4
5
6
复制代码docker-machine ssh manager1 docker network ls
NETWORK ID NAME DRIVER SCOPE
764ff31881e5 bridge bridge local
fbd9a977aa03 host host local
6p6xlousvsy2 ingress overlay swarm
e81af24d643d none null local

其中SCOPE为swarm,DRIVER为overlay的即为集群节点中的共享网络。集群建立后会有一个默认的ingress共享网络,现在我们来再创建一个:

1
复制代码docker-machine ssh manager1 docker network create --driver overlay swarm_test

在跨主机网络上部署服务

在集群上部署应用,就是在共享网络上部署服务(service)。

但首先要保证每个节点上都已经有所需的镜像和环境了,这点便可以通过将同一份docker-compose配置文件共享到每个主机上,使用docker-compose在每个节点上下载镜像和搭建环境的工作。

由于judge_server的服务架构很简单,就一个镜像,所以我在这里直接在每台主机上把它pull下来就好了:

1
2
3
复制代码docker-machine ssh manager1 docker pull registry.cn-qingdao.aliyuncs.com/marklux/judge_server:1.0
docker-machine ssh worker1 docker pull registry.cn-qingdao.aliyuncs.com/marklux/judge_server:1.0
docker-machine ssh worker2 docker pull registry.cn-qingdao.aliyuncs.com/marklux/judge_server:1.0

接下来便是重头戏,我们使用manager1节点,在共享网络上启动我们的服务

1
复制代码docker service create --replicas 3 --name judge_swarm -p 8090:8090 --network=swarm_test registry.cn-qingdao.aliyuncs.com/marklux/judge_server:1.0

这个命令看起来是不是很像docker run?没错,swarm最终的目的就是把操作集群变得像操作单一的docker服务端一样简单!

–replicas 用于指定服务需要的节点数量,也就是集群的规模,这个值是弹性的,你可以在后续动态的更改它。

当服务中某个节点挂掉时,swarm将会搜寻集群中剩余的可用节点,顶替上去。也就是说,swarm会动态的调度,总是保持服务是由3个节点运行着的。

-p 用于暴露端口到宿主机,这样我们就能访问到了。

–network用于指定部署service的网络是哪一个

现在在manager1节点中使用docker service ls来查看集群中的服务:

1
2
3
复制代码docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
kofcno637cmq judge_swarm replicated 3/3 registry.cn-qingdao.aliyuncs.com/marklux/judge_server:1.0 *:8090->8090/tcp

现在我们尝试在本地访问192.168.99.100:8090/ping,就可以得到响应了,事实上,现在无论将ip换为worker1或者worker2的,响应的结果都是一样,因为此时所有节点已经处在一个共同的集群网络下了

经过大量的访问测试,可以看到hostname是在变化着的,这说明每次请求,都由swarm动态的调度,选择了不同的节点来进行处理。

遗留问题

至此集群的部署已经完成,但是我们还遗留了几个问题没有解决:

  • 集群节点的动态添加删除不是很方便,这导致在web端管理判题服务机有一定的难度,当然可以通过docker的REMOTE API来实现,不过复杂度比较高
  • 集群节点间的文件同步不太好实现,可能需要自己写脚本同步或是使用rsync之类的服务来实现
  • swarm非常适合快速构建大量集群来实现业务的处理,不过对于只有几台机器的情况而言,有些”杀鸡用牛刀”的感觉

本文转载自: 掘金

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

【Linux】Linux系统编程入门 线程

发表于 2017-11-14

作者:不洗碗工作室 - Marklux
出处:marklux.cn/blog/56
版权归作者所有,转载请注明出处

文件和文件系统

文件是linux系统中最重要的抽象,大多数情况下你可以把linux系统中的任何东西都理解为文件,很多的交互操作其实都是通过文件的读写来实现的。

文件描述符

在linux内核中,文件是用一个整数来表示的,称为 文件描述符,通俗的来说,你可以理解它是文件的id(唯一标识符)

普通文件

  • 普通文件就是字节流组织的数据。
  • 文件并不是通过和文件名关联来实现的,而是通过关联索引节点来实现的,文件节点拥有文件系统为普通文件分配的唯一整数值(ino),并且存放着一些文件的相关元数据。

目录与链接

  • 正常情况下文件是通过文件名来打开的。
  • 目录是可读名称到索引编号之间的映射,名称和索引节点之间的配对称为链接。
  • 可以把目录看做普通文件,只是它包含着文件名称到索引节点的映射(链接)

进程

进程是仅次于文件的抽象概念,简单的理解,进程就是正在执行的目标代码,活动的,正在运行的程序。不过在复杂情况下,进程还会包含着各种各样的数据,资源,状态甚至虚拟计算机。

你可以这么理解进程:它是竞争计算机资源的基本单位。

进程、程序与线程

  1. 程序

程序,简单的来说就是存在磁盘上的二进制文件,是可以内核所执行的代码
2. 进程

当一个用户启动一个程序,将会在内存中开启一块空间,这就创造了一个进程,一个进程包含一个独一无二的PID,和执行者的权限属性参数,以及程序所需代码与相关的资料。

进程是系统分配资源的基本单位。

一个进程可以衍生出其他的子进程,子进程的相关权限将会沿用父进程的相关权限。
3. 线程

每个进程包含一个或多个线程,线程是进程内的活动单元,是负责执行代码和管理进程运行状态的抽象。

线程是独立运行和调度的基本单位。

进程的层次结构(父进程与子进程)

在进程执行的过程中可能会衍生出其他的进程,称之为子进程,子进程拥有一个指明其父进程PID的PPID。子进程可以继承父进程的环境变量和权限参数。

于是,linux系统中就诞生了进程的层次结构——进程树。

进程树的根是第一个进程(init进程)。

过程调用的流程: fork & exec

一个进程生成子进程的过程是,系统首先复制(fork)一份父进程,生成一个暂存进程,这个暂存进程和父进程的区别是pid不一样,而且拥有一个ppid,这时候系统再去执行(exec)这个暂存进程,让他加载实际要运行的程序,最终成为一个子进程的存在。

进程的结束

当一个进程终止时,并不会立即从系统中删除,内核将在内存中保存该进程的部分内容,允许父进程查询其状态(这个被称为等待终止进程)。

当父进程确定子进程已经终止,该子进程将会被彻底删除。

但是如果一个子进程已经终止,但父进程却不知道它的状态,这个进程将会成为 僵尸进程

服务与进程

简单的说服务(daemon)就是常驻内存的进程,通常服务会在开机时通过init.d中的一段脚本被启动。

进程通信

进程通信的几种基本方式:管道,信号量,消息队列,共享内存,快速用户控件互斥。

程序,进程和线程

现在我们再次详细的讨论这三个概念

程序(program)

程序是指编译过的、可执行的二进制代码,保存在储存介质上,不运行。

进程(process)

进程是指正在运行的程序。

进程包括了很多资源,拥有自己独立的内存空间。

线程

线程是进程内的活动单元。

包括自己的虚拟储存器,如栈、进程状态如寄存器,以及指令指针。

  • 在单线程的进程中,线程即进程。而在多线程的进程中,多个线程将会共享同一个内存地址空间
  • 参考阅读

PID

可以参考之前的基础概念部分。

在C语言中,PID是由数据类型pid_t来表示的。

运行一个进程

创建一个进程,在unix系统中被分为了两个流程。

  1. 把程序载入内存并执行程序映像的操作:exec
  2. 创建一个新进程:fork

exec

最简单的exec系统调用函数:execl()

  • 函数原型:
1
复制代码int execl(const char * path,const chr * arg,...)

execl()调用将会把path所指的路径的映像载入内存,替换当前进程的映像。

参数arg是以第一个参数,参数内容是可变的,但最后必须以NULL结尾。

  • 举例:
1
2
3
4
5
6
7
复制代码int ret;

ret = execl("/bin/vi","vi",NULL);

if (ret == -1) {
perror("execl");
}

上面的代码将会通过/bin/vi替换当前运行的程序

注意这里的第一个参数vi,是unix系统的默认惯例,当创建、执行进程时,shell会把路径中的最后部分放入新进程的第一个参数,这样可以使得进程解析出二进制映像文件的名字。

1
2
3
4
5
6
7
复制代码int ret;

ret = execl("/bin/vi","vi","/home/mark/a.txt",NULL);

if (ret == -1) {
perror("execl");
}

上面的代码是一个非常有代表性的操作,这相当于你在终端执行以下命令:

1
复制代码vi /home/mark/a.txt
  • 返回值:

正常情况下其实execl()不会返回,调用成功后会跳转到新的程序入口点。

成功的execl()调用,将改变地址空间和进程映像,还改变了很多进程的其他属性。

不过进程的PID,PPID,优先级等参数将会被保留下来,甚至会保留下所打开的文件描述符(这就意味着它可以访问所有这些原本进程打开的文件)。

失败后将会返回-1,并更新errno。

其他exec系函数

略,使用时查找

fork

通过fork()系统调用,可以创建一个和当前进程映像一模一样的子进程。

  • 函数原型
1
复制代码pid_t fork(void)

调用成功后,会创建一个新的进程(子进程),这两个进程都会继续运行。

  • 返回值

如果调用成功,
父进程中,fork()会返回子进程的pid,在子进程中返回0;
如果失败,返回-1,并更新errno,不会创建子进程。

  • 举例

我们看下面这段代码

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
复制代码#include <unistd.h>
#include <stdio.h>
int main ()
{
pid_t fpid; //fpid表示fork函数返回的值
int count=0;

printf("this is a process\n");

fpid=fork();

if (fpid < 0)
printf("error in fork!");
else if (fpid == 0) {
printf("i am the child process, my process id is %d\n",getpid());
printf("我是爹的儿子\n");
count++;
}
else {
printf("i am the parent process, my process id is %d\n",getpid());
printf("我是孩子他爹\n");
count++;
}
printf("统计结果是: %d\n",count);
return 0;
}

这段代码的运行结果比较神奇,是这样的:

1
2
3
4
5
6
7
复制代码this is a process
i am the parent process, my process id is 21448
我是孩子他爹
统计结果是: 1
i am the child process, my process id is 21449
我是爹的儿子
统计结果是: 1

在执行了fork()之后,这个程序就拥有了两个进程,父进程和子进程分别往下继续执行代码,进入了不同的if分支。

如何理解pid在父子进程中不同?

其实就相当于链表,进程形成了链表,父进程的pid指向了子进程的pid,因为子进程没有子进程,所以pid为0。

写时复制

传统的fork机制是,调用fork时,内核会复制所有的内部数据结构,复制进程的页表项,然后把父进程的地址空间按页复制给子进程(非常耗时)。

现代的fork机制采用了一种惰性算法的优化策略。

为了避免复制时系统开销,就尽可能的减少“复制”操作,当多个进程需要读取他们自己那部分资源的副本时,并不复制多个副本出来,而是为每个进程设定一个文件指针,让它们读取同一个实际文件。

显然这样的方式会在写入时产生冲突(类似并发),于是当某个进程想要修改自己的那个副本时,再去复制该资源,(只有写入时才复制,所以叫写时复制)这样就减少了复制的频率。

联合实例

在程序中创建一个子进程,打开另一个应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码pid_t pid;

pid = fork();

if (pid == -1)
perror("fork");

//子进程
if (!pid) {
const char * args[] = {"windlass",NULL};

int ret;

// 参数以数组方式传入
ret = execv("/bin/windlass",args);

if (ret == -1) {
perror("execv");
exit(EXIT_FAILURE);
}
}

上面的程序创建了一个子进程,并且使子进程运行了/bin/windlas程序。

终止进程

exit()

  • 函数原型
1
复制代码void exit (int status)

该函数用于终止当前的进程,参数status只用于标识进程的退出状态,这个值将会被传送给当前进程的父进程用于判断。

还有一些其他的终止调用函数,在此不赘述。

等待子进程终止

如何通知父进程子进程终止?可以通过信号机制来实现这一点。但是在很多情况下,父进程需要知道有关子进程的更详细的信息(比如返回值),这时候简单的信号通知就显得无能为力了。

如果终止时,子进程已经完全被销毁,父进程就无法获取关于子进程的任何信息。

于是unix最初做了这样的设计,如果一个子进程在父进程之前结束,内核就把这个子进程设定成一种特殊的运行状态,这种状态下的进程被称为僵尸进程,它只保留最小的概要信息,等待父进程获取到了这些信息之后,才会被销毁。

wait()

  • 函数原型
1
复制代码pid_t wait(int * status);

这个函数可以用于获取已经终止的子进程的信息。

调用成功时,会返回已终止的子进程的pid,出错时返回-1。如果没有子进程终止会导致调用的阻塞直到有一个子进程终止。

waitpid()

  • 函数原型
1
复制代码pid_t waitpid(pid_t pid,int * status,int options);

waitpid()是一个更为强大的系统调用,支持更细粒度的管控。

一些其他可能会遇到的等待函数

  • wait3()
  • wait4()

简单的说,wait3等待任意一个子进程的终止,wait4等待一个指定子进程的终止。

创建并等待新进程

很多时候我们会遇到下面这种情景:

你创建了一个新进程,你想等待它调用完之后再继续运行你自己的进程,也就是说,创建一个新进程并立即开始等待它的终止。

一个合适的选择是system():

1
复制代码int system(const char * command);

system()函数将会调用command提供的命令,一般用于运行简单的工具和shell脚本。

成功时,返回的是执行command命令所得到的返回状态。

你可以使用fork(),exec(),waitpid()来实现一个system()。

下面给出一个简单的实现:

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
复制代码int my_system(const char * cmd)
{
int status;
pid_t pid;

pid = fork();

if (pid == -1) {
return -1;
}

else if (pid == 0) {
const char * argv[4];

argv[0] = "sh";
argv[1] = "-c";
argv[2] = cmd;
argv[3] = NULL;

execv("bin/sh",argv);
// 这传参调用好像有类型转换问题

exit(-1);

}//子进程

//父进程
if (waitpid(pid,&status,0) == -1)
return -1;
else if (WIFEXITED(status))
return WEXITSTATUS(status);

return -1;
}

幽灵进程

上面我们谈论到僵尸进程,但是如果父进程没有等待子进程的操作,那么它所有的子进程都将成为幽灵进程,幽灵进程将会一直存在(因为等不到父进程调用,就一直不终止),导致系统运行速度的拖慢。

正常情况下我们不该让这种情况发生,然而如果父进程在子进程结束之前就结束了,或者父进程还没有机会等待其僵尸进程的子进程,就先结束了,这样就不可避免的产生了幽灵进程。

linux内核有一个机制来避免这样的情况发生。

无论何时,只要有进程结束,内核就会遍历它的所有子进程,并且把他们的父进程重新设置为init,而init会周期性的等待所有的子进程,以确保没有长时间存在的幽灵进程。

进程与权限

略,待补充

会话和进程组

进程组

每个进程都属于某个进程组,进程组就是由一个或者多个为了实现作业控制而相互关联的进程组成的。

一个进程组的id是进程组首进程的pid(如果一个进程组只有一个进程,那进程组和进程其实没啥区别)。

进程组的意义在于,信号可以发送给进程组中的所有进程。这样可以实现对多个进程的同时操作。

会话

会话是一个或者多个进程组的集合。

一般来说,会话(session)和shell没有什么本质上的区别。

我们通常使用用户登录一个终端进行一系列操作这样的例子来描述一次会话。

  • 举例
1
复制代码$cat ship-inventory.txt | grep booty|sort

上面就是在某次会话中的一个shell命令,它会产生一个由3个进程组成的进程组。

守护进程(服务)

守护进程(daemon)运行在后台,不与任何控制终端相关联。通常在系统启动时通过init脚本被调用而开始运行。

在linux系统中,守护进程和服务没有什么区别。

对于一个守护进程,有两个基本的要求:其一:必须作为init进程的子进程运行,其二:不与任何控制终端交互。

产生一个守护进程的流程

  1. 调用fork()来创建一个子进程(它即将成为守护进程)
  2. 在该进程的父进程中调用exit(),这保证了父进程的父进程在其子进程结束时会退出,保证了守护进程的父进程不再继续运行,而且守护进程不是首进程。(它继承了父进程的进程组id,而且一定不是leader)
  3. 调用setsid(),给守护进程创建一个新的进程组和新的会话,并作为两者的首进程。这可以保证不存在和守护进程相关联的控制终端。
  4. 调用chdir(),将当前工作目录改为根目录。这是为了避免守护进程运行在原来fork的父进程打开的随机目录下,便于管理。
  5. 关闭所有的文件描述符。
  6. 打开文件描述符0,1,2(stdin,stdout,err),并把它们重定向到/dev/null。

daemon()

用于实现上面的操作来产生一个守护进程

  • 函数原型
1
复制代码int daemon(int nochdir,int noclose);

如果参数nochdir是非0值,就不会将工作目录定向到根目录。
如果参数noclose是非0值,就不会关闭所有打开的文件描述符。

成功时返回0,失败返回-1。

注意调用这个函数生成的函数是父进程的副本(fork),所以最终生成的守护进程的样子就是父进程的样子,一般来说,就是在父进程中写好要运行在后台的功能代码,然后调用daemon()来把这些功能包装成一个守护进程。

这样子看上去好像是把当前执行的进程包装成了一个守护进程,但其实包装的是它派生出的一个副本。

线程

基础概念

线程是进程内的执行单元(比进程更低一层的概念),具体包括 虚拟处理器,堆栈,程序状态等。

可以认为 线程是操作系统调度的最小执行单元。

现代操作系统对用户空间做两个基础抽象:虚拟内存和虚拟处理器。这使得进程内部“感觉”自己独占机器资源。

虚拟内存

系统会为每个进程分配独立的内存空间,这会让进程以为自己独享全部的RAM。

但是同一个进程内的所有线程共享该进程的内存空间。

虚拟处理器

这是一个针对线程的概念,它让每个线程都“感觉”自己独享CPU。实际上对于进程也是一样的。

多线程

多线程的好处

  • 编程抽象

模块化的设计模式

  • 并发

在多核处理器上可以实现真正的并发,提高系统吞吐量

  • 提高响应能力

防止串行运算僵死

  • 防止i/o阻塞

避免单线程下,i/o操作导致整个进程阻塞的情况。此外也可以通过异步i/o和非阻塞i/o解决。

  • 减少上下文切换

多线程的切换消耗的性能远比进程间的上下文切换小的多

  • 内存共享

因为同一进程内的线程可以共享内存,在某些场景下可以利用这些特性,用多线程取代多进程。

多线程的代价

调试难度极大。

在同一个内存空间内并发性的读写操作会引发多种问题(如脏数据),对多进程情景下的资源同步变得困难,而且多个独立运行的线程其时间和顺序具有不可预测性,会导致各种各样奇怪的问题。

这一点可以参考并发带来的问题。

线程模型

线程的概念同时存在于内核和用户空间中。

内核级线程模型

每个内核线程直接转换成用户空间的线程。即内核线程:用户空间线程=1:1

用户级线程模型

这种模型下,一个保护了n个线程的用户进程只会映射到一个内核进程。即n:1。

可以减少上下文切换的成本,但在linux下没什么意义,因为linux下进程间的上下文切换本身就没什么消耗,所以很少使用。

混合式线程模型

上述两种模型的混合,即n:m型。

很难实现。

*协同程序

‌提供了比线程更轻量级的执行单位。

线程模式

每个连接对应一个线程

也就是阻塞式的I/O,实际就是单线程模式

线程以串行的方式运行,一个线程遇到I/O时线程必须被挂起等待直到操作完成后,才能再继续执行。

事件驱动的线程模式

单线程的操作模型中,大部分的系统负荷在于等待(尤其是I/O操作),因此在事件驱动的模式下,把这些等待操作从线程的执行过程中剥离掉,通过发送异步I/O请求或者是I/O多路复用,引入事件循环和回调来处理线程和I/O之间的关系。

有关I/O的几种模式,参考这里

简要概括一下,分为四种:

  • 阻塞IO:串行处理,单线程,同步等待
  • 非阻塞IO:线程发起IO请求后将立即得到结果而不是等待,如果IO没有处理完将返回ERROR,需要线程自己主动去向Kernel不断请求来判断IO是否完成
  • 异步IO:线程发起IO请求后,立即得到结果,Kernel执行完IO后会主动发送SIGNAL去通知线程
  • 事件驱动IO:属于非阻塞IO的一个升级,主要用于连接较多的情况,让Kernel去监视多个socket(每个socket都是非阻塞式的IO),哪个socket有结果了就继续执行哪个socket。

并发,并行,竞争!

并发和并行

并发,是指同一时间周期内需要运行(处理)多个线程。

并行,是指同一时刻有多个线程在运行。

本质上,并发是一种编程概念,而并行是一种硬件属性,并发可以通过并行的方式实现,也可以不通过并行的方式实现(单cpu)。

竞争

并发编程带来的最大挑战就是竞争,这主要是因为多个线程同时执行时,执行结果的顺序存在不可预料性

  • 一个最简单的示范,可以参考java并发编程中的基本例子。

请看下面这行代码:

1
复制代码  x++;

假设x的初始值为5,我们使用两个线程同时执行这行代码,会出现很多不一样的结果,即运行完成后,x的值可能为6,也可能为7。(这个是并发最基本的示范,自己理解一下很容易明白。)

原因简要描述为下:

一个线程执行x++的过程大概分为3步:

1. 把x加载到寄存器
2. 把寄存器的值+1
3. 把寄存器的值写回到x中


当两个线程出现竞争的时候,就是这3步执行的过程在时间上出现了不可预料性,假设线程1,2将x加载到寄存器的时候x都是5,但当线程1写回x时,x成为6,线程2写回x时,x还是6,这就与初衷相悖。


如果有更多的线程结果将变得更加难以预料。

解决竞争的手段:同步

简要的说,就是在会发生竞争的资源上,取消并发,而是采用同步的方式访问和操作。

锁

最常见的,处理并发的机制,就是锁机制了,当然系统层面的锁比DBMS等其他一些复杂系统的锁要简单一些(不存在共享锁,排他锁等一些较为复杂的概念)。

但是锁会带来两个问题:死锁和饿死。

解决这两个问题需要一些机制以及设计理念。具体有关锁的部分可以参考DBMS的并发笔记。

关于锁,有一点要记住。

锁住的是资源,而不是代码

编写代码时应该切记这个原则。

系统线程实现:PThreads

原始的linux系统调用中,没有像C++11或者是Java那样完整的线程库。

整体看来pthread的api比较冗余和复杂,但是基本操作也主要是 创建、退出等。

需要留意的一点是linux机制下,线程存在一个被称为joinable的状态。下面简要了解一下:

Join和Detach

这块的概念,非常类似于之前父子进程那部分,等待子进程退出的内容(一系列的wait函数)。

linux机制下,线程存在两种不同的状态:joinable和unjoinable。

如果一个线程被标记为joinable时,即便它的线程函数执行完了,或者使用了pthread_exit()结束了该线程,它所占用的堆栈资源和进程描述符都不会被释放(类似僵尸进程),这种情况应该由线程的创建者调用pthread_join()来等待线程的结束并回收其资源(类似wait系函数)。默认情况下创建的线程都是这种状态。

如果一个线程被标记成unjoinable,称它被分离(detach)了,这时候如果该线程结束,所有它的资源都会被自动回收。省去了给它擦屁股的麻烦。

因为创建的线程默认都是joinable的,所以要么在父线程调用pthread_detach(thread_id)将其分离,要么在线程内部,调用pthread_detach(pthread_self())来把自己标记成分离的。

本文转载自: 掘金

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

Just for fun——PHP框架之简单的模板引擎 原理

发表于 2017-11-14

原理

使用模板引擎的好处是数据和视图分离。一个简单的PHP模板引擎原理是

  1. extract数组($data),使key对应的变量可以在此作用域起效
  2. 打开输出控制缓冲(ob_start)
  3. include模板文件,include遇到html的内容会输出,但是因为打开了缓冲,内容输出到了缓冲中
  4. ob_get_contents()读取缓冲中内容,然后关闭缓冲ob_end_clean()

实现

封装一个Template类

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
复制代码<?php

class Template {
private $templatePath;
private $data;

public function setTemplatePath($path) {
$this->templatePath = $path;
}

/**
* 设置模板变量
* @param $key string | array
* @param $value
*/
public function assign($key, $value) {
if(is_array($key)) {
$this->data = array_merge($this->data, $key);
} elseif(is_string($key)) {
$this->data[$key] = $value;
}
}


/**
* 渲染模板
* @param $template
* @return string
*/
public function display($template) {
extract($this->data);
ob_start();
include ($this->templatePath . $template);
$res = ob_get_contents();
ob_end_clean();
return $res;
}

}

测试

test.php

1
2
3
4
5
6
7
8
9
10
11
复制代码<?php

include_once './template.php';

$template = new Template();
$template->setTemplatePath(__DIR__ . '/template/');
$template->assign('name', 'salamander');
$res = $template->display('index.html');


echo $res;

template目录下index.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模板测试</title>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
h1 {
text-align: center;
padding-top: 20px;
}
</style>
</head>
<body>
<h1><?=$name?></h1>
</body>
</html>

Tip

为什么display要返回一个字符串呢?原因是为了更好的控制,嵌入到控制器类中。
对于循环语句怎么办呢?这个的话,请看流程控制的替代语法

本文转载自: 掘金

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

tensorflow-梯度下降,有这一篇就足够了 前言 梯度

发表于 2017-11-14

前言

最近机器学习越来越火了,前段时间斯丹福大学副教授吴恩达都亲自录制了关于Deep Learning Specialization的教程,在国内掀起了巨大的学习热潮。本着不被时代抛弃的念头,自己也开始研究有关机器学习的知识。都说机器学习的学习难度非常大,但不亲自尝试一下又怎么会知道其中的奥妙与乐趣呢?只有不断的尝试才能找到最适合自己的道路。

请容忍我上述的自我煽情,下面进入主题。这篇文章主要对机器学习中所遇到的GradientDescent(梯度下降)进行全面分析,相信你看了这篇文章之后,对GradientDescent将彻底弄明白其中的原理。

梯度下降的概念

梯度下降法是一个一阶最优化算法,通常也称为最速下降法。要使用梯度下降法找到一个函数的局部极小值,必须向函数上当前点对于梯度(或者是近似梯度)的反方向的规定步长距离点进行迭代搜索。所以梯度下降法可以帮助我们求解某个函数的极小值或者最小值。对于n维问题就最优解,梯度下降法是最常用的方法之一。下面通过梯度下降法的前生今世来进行详细推导说明。

梯度下降法的前世

首先从简单的开始,看下面的一维函数:

1
apache复制代码f(x) = x^3 + 2 * x - 3

在数学中如果我们要求f(x) = 0处的解,我们可以通过如下误差等式来求得:

1
subunit复制代码error = (f(x) - 0)^2

当error趋近于最小值时,也就是f(x) = 0处x的解,我们也可以通过图来观察:

通过这函数图,我们可以非常直观的发现,要想求得该函数的最小值,只要将x指定为函数图的最低谷。这在高中我们就已经掌握了该函数的最小值解法。我们可以通过对该函数进行求导(即斜率):

1
apache复制代码derivative(x) = 6 * x^5 + 16 * x^3 - 18 * x^2 + 8 * x - 12

如果要得到最小值,只需令derivative(x) = 0,即x = 1。同时我们结合图与导函数可以知道:

  • 当x < 1时,derivative < 0,斜率为负的;
  • 当x > 1时,derivative > 0,斜率为正的;
  • 当x 无限接近 1时,derivative也就无限=0,斜率为零。

通过上面的结论,我们可以使用如下表达式来代替x在函数中的移动

1
abnf复制代码x = x - reate * derivative

当斜率为负的时候,x增大,当斜率为正的时候,x减小;因此x总是会向着低谷移动,使得error最小,从而求得 f(x) = 0处的解。其中的rate代表x逆着导数方向移动的距离,rate越大,x每次就移动的越多。反之移动的越少。

这是针对简单的函数,我们可以非常直观的求得它的导函数。为了应对复杂的函数,我们可以通过使用求导函数的定义来表达导函数:若函数f(x)在点x0处可导,那么有如下定义:

上面是都是公式推导,下面通过代码来实现,下面的代码都是使用python进行实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
repl复制代码>>> def f(x):
... return x**3 + 2 * x - 3
...
>>> def error(x):
... return (f(x) - 0)**2
...
>>> def gradient_descent(x):
... delta = 0.00000001
... derivative = (error(x + delta) - error(x)) / delta
... rate = 0.01
... return x - rate * derivative
...
>>> x = 0.8
>>> for i in range(50):
... x = gradient_descent(x)
... print('x = {:6f}, f(x) = {:6f}'.format(x, f(x)))
...

执行上面程序,我们就能得到如下结果:

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
apache复制代码x = 0.869619, f(x) = -0.603123
x = 0.921110, f(x) = -0.376268
x = 0.955316, f(x) = -0.217521
x = 0.975927, f(x) = -0.118638
x = 0.987453, f(x) = -0.062266
x = 0.993586, f(x) = -0.031946
x = 0.996756, f(x) = -0.016187
x = 0.998369, f(x) = -0.008149
x = 0.999182, f(x) = -0.004088
x = 0.999590, f(x) = -0.002048
x = 0.999795, f(x) = -0.001025
x = 0.999897, f(x) = -0.000513
x = 0.999949, f(x) = -0.000256
x = 0.999974, f(x) = -0.000128
x = 0.999987, f(x) = -0.000064
x = 0.999994, f(x) = -0.000032
x = 0.999997, f(x) = -0.000016
x = 0.999998, f(x) = -0.000008
x = 0.999999, f(x) = -0.000004
x = 1.000000, f(x) = -0.000002
x = 1.000000, f(x) = -0.000001
x = 1.000000, f(x) = -0.000001
x = 1.000000, f(x) = -0.000000
x = 1.000000, f(x) = -0.000000
x = 1.000000, f(x) = -0.000000

通过上面的结果,也验证了我们最初的结论。x = 1时,f(x) = 0。
所以通过该方法,只要步数足够多,就能得到非常精确的值。

梯度下降法的今生

上面是对一维函数进行求解,那么对于多维函数又要如何求呢?我们接着看下面的函数,你会发现对于多维函数也是那么的简单。

1
apache复制代码f(x) = x[0] + 2 * x[1] + 4

同样的如果我们要求f(x) = 0处,x[0]与x[1]的值,也可以通过求error函数的最小值来间接求f(x)的解。跟一维函数唯一不同的是,要分别对x[0]与x[1]进行求导。在数学上叫做偏导数:

  • 保持x[1]不变,对x[0]进行求导,即f(x)对x[0]的偏导数
  • 保持x[0]不变,对x[1]进行求导,即f(x)对x[1]的偏导数

有了上面的理解基础,我们定义的gradient_descent如下:

1
2
3
4
5
6
7
8
9
repl复制代码>>> def gradient_descent(x):
... delta = 0.00000001
... derivative_x0 = (error([x[0] + delta, x[1]]) - error([x[0], x[1]])) / delta
... derivative_x1 = (error([x[0], x[1] + delta]) - error([x[0], x[1]])) / delta
... rate = 0.01
... x[0] = x[0] - rate * derivative_x0
... x[1] = x[1] - rate * derivative_x1
... return [x[0], x[1]]
...

rate的作用不变,唯一的区别就是分别获取最新的x[0]与x[1]。下面是整个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
repl复制代码>>> def f(x):
... return x[0] + 2 * x[1] + 4
...
>>> def error(x):
... return (f(x) - 0)**2
...
>>> def gradient_descent(x):
... delta = 0.00000001
... derivative_x0 = (error([x[0] + delta, x[1]]) - error([x[0], x[1]])) / delta
... derivative_x1 = (error([x[0], x[1] + delta]) - error([x[0], x[1]])) / delta
... rate = 0.02
... x[0] = x[0] - rate * derivative_x0
... x[1] = x[1] - rate * derivative_x1
... return [x[0], x[1]]
...
>>> x = [-0.5, -1.0]
>>> for i in range(100):
... x = gradient_descent(x)
... print('x = {:6f},{:6f}, f(x) = {:6f}'.format(x[0],x[1],f(x)))
...

输出结果为:

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
apache复制代码x = -0.560000,-1.120000, f(x) = 1.200000
x = -0.608000,-1.216000, f(x) = 0.960000
x = -0.646400,-1.292800, f(x) = 0.768000
x = -0.677120,-1.354240, f(x) = 0.614400
x = -0.701696,-1.403392, f(x) = 0.491520
x = -0.721357,-1.442714, f(x) = 0.393216
x = -0.737085,-1.474171, f(x) = 0.314573
x = -0.749668,-1.499337, f(x) = 0.251658
x = -0.759735,-1.519469, f(x) = 0.201327
x = -0.767788,-1.535575, f(x) = 0.161061
x = -0.774230,-1.548460, f(x) = 0.128849
x = -0.779384,-1.558768, f(x) = 0.103079
x = -0.783507,-1.567015, f(x) = 0.082463
x = -0.786806,-1.573612, f(x) = 0.065971
x = -0.789445,-1.578889, f(x) = 0.052777
x = -0.791556,-1.583112, f(x) = 0.042221
x = -0.793245,-1.586489, f(x) = 0.033777
x = -0.794596,-1.589191, f(x) = 0.027022
x = -0.795677,-1.591353, f(x) = 0.021617
x = -0.796541,-1.593082, f(x) = 0.017294
x = -0.797233,-1.594466, f(x) = 0.013835
x = -0.797786,-1.595573, f(x) = 0.011068
x = -0.798229,-1.596458, f(x) = 0.008854
x = -0.798583,-1.597167, f(x) = 0.007084
x = -0.798867,-1.597733, f(x) = 0.005667
x = -0.799093,-1.598187, f(x) = 0.004533
x = -0.799275,-1.598549, f(x) = 0.003627
x = -0.799420,-1.598839, f(x) = 0.002901
x = -0.799536,-1.599072, f(x) = 0.002321
x = -0.799629,-1.599257, f(x) = 0.001857
x = -0.799703,-1.599406, f(x) = 0.001486
x = -0.799762,-1.599525, f(x) = 0.001188
x = -0.799810,-1.599620, f(x) = 0.000951
x = -0.799848,-1.599696, f(x) = 0.000761
x = -0.799878,-1.599757, f(x) = 0.000608
x = -0.799903,-1.599805, f(x) = 0.000487
x = -0.799922,-1.599844, f(x) = 0.000389
x = -0.799938,-1.599875, f(x) = 0.000312
x = -0.799950,-1.599900, f(x) = 0.000249
x = -0.799960,-1.599920, f(x) = 0.000199
x = -0.799968,-1.599936, f(x) = 0.000159
x = -0.799974,-1.599949, f(x) = 0.000128
x = -0.799980,-1.599959, f(x) = 0.000102
x = -0.799984,-1.599967, f(x) = 0.000082
x = -0.799987,-1.599974, f(x) = 0.000065
x = -0.799990,-1.599979, f(x) = 0.000052
x = -0.799992,-1.599983, f(x) = 0.000042
x = -0.799993,-1.599987, f(x) = 0.000033
x = -0.799995,-1.599989, f(x) = 0.000027
x = -0.799996,-1.599991, f(x) = 0.000021
x = -0.799997,-1.599993, f(x) = 0.000017
x = -0.799997,-1.599995, f(x) = 0.000014
x = -0.799998,-1.599996, f(x) = 0.000011
x = -0.799998,-1.599997, f(x) = 0.000009
x = -0.799999,-1.599997, f(x) = 0.000007
x = -0.799999,-1.599998, f(x) = 0.000006
x = -0.799999,-1.599998, f(x) = 0.000004
x = -0.799999,-1.599999, f(x) = 0.000004
x = -0.799999,-1.599999, f(x) = 0.000003
x = -0.800000,-1.599999, f(x) = 0.000002
x = -0.800000,-1.599999, f(x) = 0.000002
x = -0.800000,-1.599999, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000000

细心的你可能会发现,f(x) = 0不止这一个解还可以是x = -2, -1。这是因为梯度下降法只是对当前所处的凹谷进行梯度下降求解,对于error函数并不代表只有一个f(x) = 0的凹谷。所以梯度下降法只能求得局部解,但不一定能求得全部的解。当然如果对于非常复杂的函数,能够求得局部解也是非常不错的。

tensorflow中的运用

通过上面的示例,相信对梯度下降也有了一个基本的认识。现在我们回到最开始的地方,在tensorflow中使用gradientDescent。

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
makefile复制代码import tensorflow as tf

# Model parameters
W = tf.Variable([.3], dtype=tf.float32)
b = tf.Variable([-.3], dtype=tf.float32)
# Model input and output
x = tf.placeholder(tf.float32)
linear_model = W*x + b
y = tf.placeholder(tf.float32)

# loss
loss = tf.reduce_sum(tf.square(linear_model - y)) # sum of the squares
# optimizer
optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)

# training data
x_train = [1, 2, 3, 4]
y_train = [0, -1, -2, -3]
# training loop
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init) # reset values to wrong
for i in range(1000):
sess.run(train, {x: x_train, y: y_train})

# evaluate training accuracy
curr_W, curr_b, curr_loss = sess.run([W, b, loss], {x: x_train, y: y_train})
print("W: %s b: %s loss: %s"%(curr_W, curr_b, curr_loss))

上面的是tensorflow的官网示例,上面代码定义了函数linear_model = W * x + b,其中的error函数为linear_model - y。目的是对一组x_train与y_train进行简单的训练求解W与b。为了求得这一组数据的最优解,将每一组的error相加从而得到loss,最后再对loss进行梯度下降求解最优值。

1
2
abnf复制代码optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)

在这里rate为0.01,因为这个示例也是多维函数,所以也要用到偏导数来进行逐步向最优解靠近。

1
2
css复制代码for i in range(1000):
sess.run(train, {x: x_train, y: y_train})

最后使用梯度下降进行循环推导,下面给出一些推导过程中的相关结果

1
2
3
4
5
6
7
8
9
10
11
12
13
stylus复制代码W: [-0.21999997] b: [-0.456] loss: 4.01814
W: [-0.39679998] b: [-0.49552] loss: 1.81987
W: [-0.45961601] b: [-0.4965184] loss: 1.54482
W: [-0.48454273] b: [-0.48487374] loss: 1.48251
W: [-0.49684232] b: [-0.46917531] loss: 1.4444
W: [-0.50490189] b: [-0.45227283] loss: 1.4097
W: [-0.5115062] b: [-0.43511063] loss: 1.3761
....
....
....
W: [-0.99999678] b: [ 0.99999058] loss: 5.84635e-11
W: [-0.99999684] b: [ 0.9999907] loss: 5.77707e-11
W: [-0.9999969] b: [ 0.99999082] loss: 5.69997e-11

这里就不推理验证了,如果看了上面的梯度下降的前世今生,相信能够自主的推导出来。那么我们直接看最后的结果,可以估算为W = -1.0与b = 1.0,将他们带入上面的loss得到的结果为0.0,即误差损失值最小,所以W = -1.0与b = 1.0就是x_train与y_train这组数据的最优解。

好了,关于梯度下降的内容就到这了,希望能够帮助到你;如有不足之处欢迎来讨论,如果感觉这篇文章不错的话,可以关注我的博客,查看我的其它文章。

博客地址

本文转载自: 掘金

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

Just for fun——PHP框架之简单的路由器(1)

发表于 2017-11-14

路由

路由的功能就是分发请求到不同的控制器,基于的原理就是正则匹配。接下来呢,我们实现一个简单的路由器,实现的能力是

  1. 对于静态的路由(没占位符的),正确调用callback
  2. 对于有占位符的路由,正确调用callback时传入占位符参数,譬如对于路由:/user/{id},当请求为/user/23时,传入参数$args结构为
1
2
3
复制代码[
'id' => '23'
]

大致思路

  1. 我们需要把每个路由的信息管理起来:http方法($method),路由字符串($route),回调($callback),因此需要一个addRoute方法,另外提供短方法get,post(就是把$method写好)
  2. 对于/user/{id}这样的有占位符的路由字符串,把占位符要提取出来,然后占位符部分变成正则字符串

实现

Route.php类

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
复制代码<?php

namespace SalamanderRoute;

class Route {
/** @var string */
public $httpMethod;

/** @var string */
public $regex;

/** @var array */
public $variables;

/** @var mixed */
public $handler;

/**
* Constructs a route (value object).
*
* @param string $httpMethod
* @param mixed $handler
* @param string $regex
* @param array $variables
*/
public function __construct($httpMethod, $handler, $regex, $variables) {
$this->httpMethod = $httpMethod;
$this->handler = $handler;
$this->regex = $regex;
$this->variables = $variables;
}

/**
* Tests whether this route matches the given string.
*
* @param string $str
*
* @return bool
*/
public function matches($str) {
$regex = '~^' . $this->regex . '$~';
return (bool) preg_match($regex, $str);
}

}

Dispatcher.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
复制代码<?php
/**
* User: salamander
* Date: 2017/11/12
* Time: 13:43
*/

namespace SalamanderRoute;

class Dispatcher {
/** @var mixed[][] */
protected $staticRoutes = [];

/** @var Route[][] */
private $methodToRegexToRoutesMap = [];

const NOT_FOUND = 0;
const FOUND = 1;
const METHOD_NOT_ALLOWED = 2;

/**
* 提取占位符
* @param $route
* @return array
*/
private function parse($route) {
$regex = '~^(?:/[a-zA-Z0-9_]*|/\{([a-zA-Z0-9_]+?)\})+/?$~';
if(preg_match($regex, $route, $matches)) {
// 去掉full match
array_shift($matches);
return [
preg_replace('~{[a-zA-Z0-9_]+?}~', '([a-zA-Z0-9_]+)', $route),
$matches,
];
}
throw new \LogicException('register route failed, pattern is illegal');
}

/**
* 注册路由
* @param $httpMethod string | string[]
* @param $route
* @param $handler
*/
public function addRoute($httpMethod, $route, $handler) {
$routeData = $this->parse($route);
foreach ((array) $httpMethod as $method) {
if ($this->isStaticRoute($routeData)) {
$this->addStaticRoute($httpMethod, $routeData, $handler);
} else {
$this->addVariableRoute($httpMethod, $routeData, $handler);
}
}
}


private function isStaticRoute($routeData) {
return count($routeData[1]) === 0;
}

private function addStaticRoute($httpMethod, $routeData, $handler) {
$routeStr = $routeData[0];

if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
throw new \LogicException(sprintf(
'Cannot register two routes matching "%s" for method "%s"',
$routeStr, $httpMethod
));
}

if (isset($this->methodToRegexToRoutesMap[$httpMethod])) {
foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) {
if ($route->matches($routeStr)) {
throw new \LogicException(sprintf(
'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"',
$routeStr, $route->regex, $httpMethod
));
}
}
}

$this->staticRoutes[$httpMethod][$routeStr] = $handler;
}


private function addVariableRoute($httpMethod, $routeData, $handler) {
list($regex, $variables) = $routeData;

if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
throw new \LogicException(sprintf(
'Cannot register two routes matching "%s" for method "%s"',
$regex, $httpMethod
));
}

$this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(
$httpMethod, $handler, $regex, $variables
);
}


public function get($route, $handler) {
$this->addRoute('GET', $route, $handler);
}

public function post($route, $handler) {
$this->addRoute('POST', $route, $handler);
}

public function put($route, $handler) {
$this->addRoute('PUT', $route, $handler);
}

public function delete($route, $handler) {
$this->addRoute('DELETE', $route, $handler);
}

public function patch($route, $handler) {
$this->addRoute('PATCH', $route, $handler);
}

public function head($route, $handler) {
$this->addRoute('HEAD', $route, $handler);
}

/**
* 分发
* @param $httpMethod
* @param $uri
*/
public function dispatch($httpMethod, $uri) {
$staticRoutes = array_keys($this->staticRoutes[$httpMethod]);
foreach ($staticRoutes as $staticRoute) {
if($staticRoute === $uri) {
return [self::FOUND, $this->staticRoutes[$httpMethod][$staticRoute], []];
}
}

$routeLookup = [];
$index = 1;
$regexes = array_keys($this->methodToRegexToRoutesMap[$httpMethod]);
foreach ($regexes as $regex) {
$routeLookup[$index] = [
$this->methodToRegexToRoutesMap[$httpMethod][$regex]->handler,
$this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables,
];
$index += count($this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables);
}
$regexCombined = '~^(?:' . implode('|', $regexes) . ')$~';
if(!preg_match($regexCombined, $uri, $matches)) {
return [self::NOT_FOUND];
}
for ($i = 1; '' === $matches[$i]; ++$i);
list($handler, $varNames) = $routeLookup[$i];
$vars = [];
foreach ($varNames as $varName) {
$vars[$varName] = $matches[$i++];
}
return [self::FOUND, $handler, $vars];
}
}

配置

nginx.conf重写到index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码location / {
try_files $uri $uri/ /index.php$is_args$args;

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

}

composer.json自动载入

1
2
3
4
5
6
7
8
9
复制代码{
"name": "salmander/route",
"require": {},
"autoload": {
"psr-4": {
"SalamanderRoute\\": "SalamanderRoute/"
}
}
}

最终使用

index.php

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
复制代码<?php

include_once 'vendor/autoload.php';

use SalamanderRoute\Dispatcher;

$dispatcher = new Dispatcher();

$dispatcher->get('/', function () {
echo 'hello world';
});

$dispatcher->get('/user/{id}', function ($args) {
echo "user {$args['id']} visit";
});

// Fetch method and URI from somewhere
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];

// 去掉查询字符串
if (false !== $pos = strpos($uri, '?')) {
$uri = substr($uri, 0, $pos);
}

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case Dispatcher::NOT_FOUND:
echo '404 not found';
break;
case Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
$handler($vars);
break;
}

代码讲解,未完待续^–^

本文转载自: 掘金

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

直击阿里双11神秘技术:PB级大规模文件分发系统“蜻蜓”

发表于 2017-11-14

阿里妹导读:2017天猫双11, 交易峰值32.5万/秒,支付峰值25.6万/秒,数据库处理峰值4200万次/秒,再次刷新了记录。阿里集团基础设施蜻蜓,在双11期间,对上万台服务器同时下发5GB的数据文件,让大规模文件分发靠蜻蜓系统完美实现。

蜻蜓,通过解决大规模文件下载以及跨网络隔离等场景下各种难题,大幅提高数据预热、大规模容器镜像分发等业务能力。月均分发次数突破20亿次,分发数据量3.4PB。其中容器镜像分发比natvie方式提速可高达57倍,registry网络出口流量降低99.5%以上。今天,阿里妹邀请阿里基础架构事业群高级技术专家如柏,为我们详述蜻蜓从文件分发到镜像传输的技术之路。

蜻蜓的诞生

随着阿里业务爆炸式增长,2015年时发布系统日均的发布量突破两万,很多应用的规模开始破万,发布失败率开始增高,而根本原因就是发布过程需要大量的文件拉取,文件服务器扛不住大量的请求,当然很容易想到服务器扩容,可是扩容后又发现后端存储成为瓶颈。此外,大量来自不同IDC的客户端请求消耗了巨大的网络带宽,造成网络拥堵。

同时,很多业务走向国际化,大量的应用部署在海外,海外服务器下载要回源国内,浪费了大量的国际带宽,而且还很慢;如果传输大文件,网络环境差,失败的话又得重来一遍,效率极低。

于是很自然的就想到了P2P技术,因为P2P技术并不新鲜,当时也调研了很多国内外的系统,但是调研的结论是这些系统的规模和稳定性都无法达到我们的期望。所以就有了蜻蜓这个产品。

设计目标

针对这些痛点,蜻蜓在设计之初定了几个目标:

  1. 解决文件源被打爆的问题,在Host之间组P2P网,缓解文件服务器压力,节约跨IDC之间的网络带宽资源。
  1. 加速文件分发速度,并且保证上万服务器同时下载,跟一台服务器下载没有太大的波动。
  1. 解决跨国下载加速和带宽节约。
  1. 解决大文件下载问题,同时必须要支持断点续传。
  1. Host上的磁盘IO,网络IO必须可以被控制,以避免对业务造成影响。
系统架构

蜻蜓整体架构

蜻蜓整体架构分三层:第一层是Config Service, 他管理所有的Cluster Manager,Cluster Manager又管理所有的Host, Host就是终端,dfget就是类似wget的一个客户端程序。

Config Service 主要负责Cluster Manager的管理、客户端节点路由、系统配置管理以及预热服务等等。简单的说, 就是负责告诉Host,离他最近的一组Cluster Manager的地址列表,并定期维护和更新这份列表,使Host总能找到离他最近的Cluster Manager。

Cluster Manager 主要的职责有两个:

  1. 以被动CDN方式从文件源下载文件并生成一组种子分块数据;
  1. 构造P2P网络并调度每个peer之间互传指定的分块数据。

Host上就存放着dfget,dfget的语法跟wget非常类似。主要功能包括文件下载和P2P共享等。

在阿里内部我们可以用StarAgent来下发dfget指令,让一组机器同时下载文件,在某种场景下一组机器可能就是阿里所有的服务器,所以使用起来非常高效。除了客户端外, 蜻蜓还有Java SDK,可以让你将文件“PUSH”到一组服务器上。

下面这个图阐述了两个终端同时调用dfget,下载同一个文件时系统的交互示意图:

蜻蜓P2P组网逻辑示意图

两个Host和CM会组成一个P2P网络,首先CM会查看本地是否有缓存,如果没有,就会回源下载,文件当然会被分片,CM会多线程下载这些分片,同时会将下载的分片提供给Host们下载,Host下载完一个分片后,同时会提供出来给peer下载,如此类推,直到所有的Host全部下载完。

本地下载的时候会将下载分片的情况记录在metadata里,如果突然中断了下载,再次执行dfget命令,会断点续传。

下载结束后,还会比对MD5,以确保下载的文件和源文件是完全一致的。蜻蜓通过HTTP cache协议来控制CM端对文件的缓存时长,CM端当然也有自己定期清理磁盘的能力,确保有足够的空间支撑长久的服务。

在阿里还有很多文件预热的场景,需要提前把文件推送到CM端,包括容器镜像、索引文件、业务优化的cache文件等等。

在第一版上线后,我们进行了一轮测试, 结果如下图:

传统下载和蜻蜓P2P下载测试结果对比图

X轴是客户端数量, Y轴是下载时长,

文件源:测试目标文件200MB(网卡:千兆bit/s)

Host端:百兆bit/s网卡

CM端:2台服务器(24核 64G,网卡:千兆bit/s)

从这个图可以看出两个问题:

  1. 传统模式随着客户端的增加,下载时长跟着增加,而dfget可以支撑到7000客户端依然没变好。
  1. 传统模式到了1200客户端以后就没有数据了,因为数据源被打爆了。

从发布系统走向基础设施

2015年双11后,蜻蜓的下载次数就达到了12万/月,分发量4TB。当时在阿里还有别的下载工具,如wget,curl,scp,ftp 等等,也有自建的小规模文件分发系统。我们除了全面覆盖自身发布系统外,也做了小规模的推广。到2016年双11左右,我们的下载量就达到了1.4亿/月,分发量708TB,业务增长了近千倍。

2016年双11后我们提出了一个更高的目标, 希望阿里大规模文件分发和大文件分发90%的业务由蜻蜓来承担。

我希望通过这个目标锤炼出最好的P2P文件分发系统。此外也可以统一集团内所有的文件分发系统。统一可以让更多的用户受益,但统一从来不是终极目标, 统一的目的是:1. 减少重复建设;2. 全局优化。

只要优化蜻蜓一个系统,全集团都能受益。比如我们发现系统文件是每天全网分发的,而光这一个文件压缩的话就能给公司每天节省9TB网络流量。跨国带宽资源尤其宝贵。而如果大家各用各的分发系统,类似这样的全局优化就无从谈起。

所以统一势在必行!

在大量数据分析基础上,我们得出全集团文件分发的量大概是3.5亿次/周,而我们当时的占比只有10%不到。

经过半年努力,在2017年4月份,我们终于实现了这个目标, 达到90%+的业务占有率,业务量增长到3亿次/周(跟我们之前分析的数据基本吻合),分发量977TB,这个数字比半年前一个月的量还大。

当然,不得不说这跟阿里容器化也是密不可分的,镜像分发流量大约占了一半。下面我们就来介绍下蜻蜓是如何支持镜像分发的。在说镜像分发之前先说下阿里的容器技术。

阿里的容器技术

容器技术的优点自然不需要多介绍了,全球来看,容器技术以Docker为主占了大部分市场,当然还有其他解决方案:比如rkt,Mesos Uni Container,LXC等,而阿里的容器技术命名为Pouch。早在2011年,阿里就自主研发了基于LXC的容器技术T4,只是当时我们没有创造镜像这个概念,T4还是当做虚拟机来用,当然比虚拟机要轻量的多。

2016年阿里在T4基础上做了重大升级,演变为今天的Pouch,并且已经开源。目前Pouch容器技术已经覆盖阿里巴巴集团几乎所有的事业部,在线业务100%容器化,规模高达数十万。镜像技术的价值扩大了容器技术的应用边界,而在阿里如此庞大的应用场景下,如何实现高效“镜像分发”成为一个重大命题。

回到镜像层面。宏观上,阿里巴巴有规模庞大的容器应用场景;微观上,每个应用镜像在镜像化时,质量也存在参差不齐的情况。

理论上讲用镜像或者用传统“基线”模式,在应用大小上不应该有非常大的区别。但事实上这完全取决于Dockerfile写的好坏,也取决于镜像分层是否合理。阿里内部其实有最佳实践,但是每个团队理解接受程度不同,肯定会有用的好坏的之分。尤其在一开始,大家打出来的镜像有3~4GB这都是非常常见的。

所以作为P2P文件分发系统,蜻蜓就有了用武之地,无论是多大的镜像,无论是分发到多少机器,即使你的镜像打的非常糟糕,我们都提供非常高效的分发,都不会成瓶颈。这样就给我们快速推广容器技术,让大家接受容器运维模式,给予了充分消化的时间。

容器镜像

在讲镜像分发之前先简单介绍下容器镜像。我们看下Ubuntu系统的镜像:我们可以通过命令 docker history ubuntu:14.04 查看 ubuntu:14.04,结果如下:

需要注意的是:镜像层 d2a0ecffe6fa 中没有任何内容,也就是所谓的空镜像。

镜像是分层的,每层都有自己的ID和尺寸,这里有4个Layer,最终这个镜像是由这些Layer组成。

Docker镜像是通过Dockerfile来构建,看一个简单的Dockerfile:

镜像构建过程如下图所示:

可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。

当容器启动时,一个可写层会被加载到镜像的顶层,这个可读可写层也被称为“容器层”,容器层之下都是“镜像层”,都是只读的。

如果镜像层内容为空,相应的信息会在镜像json文件中描述,如果镜像层内容不为空,则会以文件的形式存储在OSS中。

镜像分发

Docker 镜像下载流程图

以阿里云容器服务为例,传统的镜像传输如上图所示,当然这是最简化的一种架构模式,实际的部署情况会复杂的多,还会考虑鉴权、安全、高可用等等。

从上图可以看出,镜像传输跟文件分发有类似的问题,当有一万个Host同时向Registry请求时,Registry就会成为瓶颈,还有海外的Host访问国内Registry时候也会存在带宽浪费、延时变长、成功率下降等问题。

下面介绍下Docker Pull的执行过程:

Docker 镜像分层下载图

Docker Daemon调用Registry API得到镜像的Manifest,从Manifest中能算出每层的URL,Daemon随后把所有镜像层从Registry并行下载到Host本地仓库。

所以最终,镜像传输的问题变成了各镜像层文件的并行下载的问题。而蜻蜓擅长的正是将每层镜像文件从Registry用P2P模式传输到本地仓库中。

那么具体又是如何做到的呢?

事实上我们会在Host上启动dfGet proxy,Docker/Pouch Engine的所有命令请求都会通过这个proxy,我们看下图:

蜻蜓P2P容器镜像分发示意图

首先,docker pull命令,会被dfget proxy截获。然后,由dfget proxy向CM发送调度请求,CM在收到请求后会检查对应的下载文件是否已经被缓存到本地,如果没有被缓存,则会从Registry中下载对应的文件,并生成种子分块数据(种子分块数据一旦生成就可以立即被使用);如果已经被缓存,则直接生成分块任务,请求者解析相应的分块任务,并从其他peer或者supernode中下载分块数据,当某个Layer的所有分块下载完成后,一个Layer也就下载完毕了,同样,当所有的Layer下载完成后,整个镜像也就下载完成了。

蜻蜓支持容器镜像分发,也有几个设计目标:

  1. 大规模并发:必须能支持十万级规模同时Pull镜像。
  1. 不侵入容器技术内核(Docker Daemon, Registry):也就是说不能改动容器服务任何代码。
  1. 支持Docker,Pouch,Rocket ,Hyper等所有容器/虚拟机技术。
  1. 支持镜像预热:构建时就推送到蜻蜓集群CM。
  1. 支持大镜像文件:至少30GB。
  1. 安全

Native Docker V.S 蜻蜓

我们一共做了两组实验:

实验一:1个客户端

  1. 测试镜像大小:50MB、200MB、500MB、1GB、5GB
  1. 镜像仓库带宽:15Gbps
  1. 客户端带宽:双百兆bit/s网络环境
  1. 测试规模:单次下载

单客户端不同模式对比图

Native和蜻蜓(关闭智能压缩特性)平均耗时基本接近,蜻蜓稍高一点,因为蜻蜓在下载过程中会校验每个分块数据的MD5值,同时在下载之后还会校验整个文件的MD5,以保证下载的文件跟源文件是一致的;而开启了智能压缩的模式下,其耗时比Native模式还低!

实验二:多客户端并发

  1. 测试镜像大小:50MB、200MB、500MB、1GB、5GB
  1. 镜像仓库带宽:15Gbps
  1. 客户端带宽:双百兆bit/s网络环境
  1. 多并发:10并发、200并发、1000并发

不同镜像大小和并发数的对比图

上图可以看出,随着下载规模的扩大,蜻蜓与Native模式耗时差异显著扩大,最高可提速可以达20倍。在测试环境中源的带宽也至关重要,如果源的带宽是2Gbps,提速可达57倍。

下图是下载文件的总流量(并发数 * 文件大小)和回源流量(去Registry下载的流量)的一个对比:

蜻蜓镜像分发出流量对比图

向200个节点分发500M的镜像,比docker原生模式使用更低的网络流量,实验数据表明采用蜻蜓后,Registry的出流量降低了99.5%以上;而在1000并发规模下,Registry的出流量更可以降低到99.9%左右。

阿里巴巴实践效果

蜻蜓在阿里投入使用大概已有两年,两年来业务发展迅速,从分发的次数来统计目前一个月接近20亿次,分发3.4PB数据。其中容器镜像的分发量接近一半。

蜻蜓在阿里文件vs镜像分发流量趋势图

在阿里最大的一次分发应该就是今年双11期间, 要对上万台服务器同时下发5GB的数据文件。

走向智能化

阿里在AIOps起步虽然不是最早, 但是我们近年来投入巨大,并在很多产品上有所应用。蜻蜓这个产品中有以下应用:

智能流控

流控在道路交通中很常见,比如中国道路限速规定,没有中心线的公路,限速为40公里/小时;同方向只有1条机动车道的公路,限速为70公里/小时;快速道路80公里;高速公路最高限速为120公里/小时等等。这种限速对每辆车都一样,显然不够灵活,所以在道路非常空闲的情况下,道路资源其实是非常浪费的,整体效率非常低下。

红绿灯其实也是流控的手段,现在的红绿灯都是固定时间,不会根据现实的流量来做智能的判断,所以去年10月召开的云栖大会上,王坚博士曾感慨,世界上最遥远的距离不是从南极到北极,而是从红绿灯到交通摄像头,它们在同一根杆上,但从来没有通过数据被连接过,摄像头看到的东西永远不会变成红绿灯的行动。这既浪费了城市的数据资源,也加大了城市运营发展的成本。

蜻蜓其中一个参数就是控制磁盘和网络带宽利用率的,用户可以通过参数设定使用多少网络IO/磁盘IO。如上所述,这种方法是非常僵化的。所以目前我们智能化方面的主要思想之一是希望类似的参数不要再人为来设定,而是根据业务的情况结合系统运行的情况,智能的决定这些参数的配置。最开始可能不是最优解,但是经过一段时间运行和训练后自动达到最优化的状态,保证业务稳定运行同时又尽可能的充分利用网络和磁盘带宽,避免资源浪费。

智能调度

分块任务调度是决定整个文件分发效率高低与否的关键因素,如果只是通过简单的调度策略,比如随机调度或者其他固定优先级的调度,这种做法往往会引起下载速率的频繁抖动,很容易导致下载毛刺过多,同时整体下载效率也会很差。为了最优化任务调度,我们经历了无数次的尝试和探索,最终通过多维度(比如机器硬件配置、地理位置、网络环境、历史下载结果和速率等等维度的数据)的数据分析(主要利用了梯度下降算法,后续还会尝试其他算法),智能动态决定当前请求者最优的后续分块任务列表。

智能压缩

智能压缩会对文件中最值得压缩的部分实施相应的压缩策略,从而可以节约大量的网络带宽资源。

对容器镜像目前的实际平均数据来看,压缩率(Compression Ration) 是40%,也就是说100MB镜像可以压缩到40MB。针对1000并发规模,通过智能压缩可以减少60%的流量。

安全

在下载某些敏感的文件(比如秘钥文件或者账号数据文件等)时,传输的安全性必须要得到有效的保证,在这方面,蜻蜓主要做了两个工作:

  1. 支持携带HTTP的header数据,以满足那些需要通过header来进行权限验证的文件源;
  1. 利用对称加密算法,对文件内容进行传输加密。

开源

随着容器技术的流行,容器镜像这类大文件分发成为一个重要问题,为了更好的支持容器技术的发展,数据中心大规模文件的分发,阿里决定开源蜻蜓来更好的推进技术的发展。阿里将持续支持开源社区,并把自己经过实战检验的技术贡献给社区。敬请期待。

总结

蜻蜓通过使用P2P技术同时结合智能压缩、智能流控等多种创新技术,解决大规模文件下载以及跨网络隔离等场景下各种文件分发难题,大幅提高数据预热、大规模容器镜像分发等业务能力。

蜻蜓支持多种容器技术,对容器本身无需做任何改造,镜像分发比natvie方式提速可高达57倍,Registry网络出流量降低99.5%以上。承载着PB级的流量的蜻蜓,在阿里已然成为重要的基础设施之一,为业务的极速扩张和双11大促保驾护航。

PS:云效2.0 智能运维平台 - 致力于打造具备世界级影响力的智能运维平台,诚聘资深技术/产品专家,工作地点:杭州、北京、美国,有意者可以点击文末“阅读原文”了解详情。

Reference

[1]Docker Overview:

https://docs.docker.com/engine/docker-overview/

[2]Where are docker images stored:

http://blog.thoward37.me/articles/where-are-docker-images-stored/

[3]Image Spec:

https://github.com/moby/moby/blob/master/image/spec/v1.md

[4]Pouch开源地址:

https://github.com/alibaba/pouch

[5]蜻蜓开源地址:

https://github.com/alibaba/dragonfly

[6]阿里云容器服务:

https://www.aliyun.com/product/containerservice

[7]飞天专有云敏捷版:

https://yq.aliyun.com/articles/224507

[8]云效智能运维平台:

https://www.aliyun.com/product/yunxiao

你可能还喜欢

点击下方图片即可阅读

阿里巴巴CTO行癫:

阿里双11是世界互联网技术超级工程

关注 「阿里技术」

把握前沿技术脉搏

本文转载自: 掘金

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

1…946947948…956

开发者博客

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