Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

php 代码调试里的神器就是 echo 111;exit; 但是对于使用了接口和继承比较多的话,有时候比较难找,可能定位到了一段代码但是不知道怎么调用过来的,这时候就可以用这个方法

总结下来有三种,

第一种是最巧妙的

1
2
3
function a() {
echo 111;exit;
}

比如本来是上面这样子,那么其实我们可以主动new 个异常

1
2
3
4
5
function a() {
$e = new Exception();
print_r($e->getTraceAsString());
echo 111;exit;
}

这样我的 trace 调用链路就出来了

第二种

这个就是比较简单的,调用 php 自身提供的方法

1
debug_backtrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): array

第一个参数是个掩码

debug_backtrace()Populates both indexes
debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT)
debug_backtrace(1)
debug_backtrace(0)Omits index "object" and populates index "args".
debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)Omits index "object" and index "args".
debug_backtrace(2)
debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECTDEBUG_BACKTRACE_IGNORE_ARGS)
debug_backtrace(3)

第二个参数是限制栈深度

第三种

这个也是用自身的方法

1
debug_print_backtrace(int $options = 0, int $limit = 0): void

这里的第一个参数只有一个可以传的

DEBUG_BACKTRACE_IGNORE_ARGSWhether or not to omit the “args” index, and thus all the function/method arguments, to save memory.

就是隐藏参数,不然如果对于一些框架代码,这个打印会非常大,需要注意下

昨天同学问我是不是数据库主从延迟有点高,可能有一分多钟,然后我就去看了rds 的监控,发现主实例上的监控显示的延迟才 1.2 秒,而且是最高 1.2 秒,感觉这样的话应该就没啥问题,然后同学跟我说他加了日志,大致的逻辑是主库数据落库以后就会发一条 mq 消息出来,然后消费者接收到以后回去从库查一下这个数据,结果发现延迟了 90 多秒才查到数据,这种情况比较可能的猜测是阿里云这个监控的逻辑可能是从库在获得第一条同步数据的时候,而不是最终同步完成,但是跟阿里云咨询了并不是,使用的就是 show slave status 结果里的 Seconds_Behind_Master 指标,那这第一种情况就否定掉了,这里其实是犯了个错误,应该去从库看这个延迟的,不过对于阿里云来说在 rds 监控是看不到从库的监控的,只能到 das,也就是阿里云的数据库自治服务可以看到,这里能看到从库的延迟监控,发现的确有这么高,这样就要考虑为啥会出现这种情况,阿里云同学反馈的是这段时间的 iops 很高,并且 cpu 也比较高,让我排查下 binlog,这里就碰到一个小问题,阿里云 rds 的线上实例我们没法在本地连接,并且密码也是在代码里加密过的,去服务器上连接一方面需要装 mysql 客户端,另一方面是怕拉取日志会有性能影响,幸好阿里云这点做的比较好,在 rds 的”备份恢复”–> “日志备份”菜单里可以找到binlog 文件, 在这里其实大致就发现了问题,因为出问题的时间段内升成 binlog 的量大大超过其他时间段,然后通过内网下载后就可以对 binlog 进行分析了,这里我们用到了 mysqlbinlog 工具,主要就是找具体是哪些写入导致这个问题,mysqlbinlog 可以在 mysql 官网下载 mysql 的压缩包,注意是压缩包,这样解压了直接用就好了,不用完整安装 mysql,一般我们需要对 binlog 进行 base64 的反编码,

1
./mysqlbinlog -vv --base64-output=decode-rows mysql-bin.xxxx | less

这样查看里面的具体信息,发现是大量的 insert 数据,再经过排查发现同一个 binlog 文件近 500M 全是同一个表的数据插入,再根据表对应的业务查找发现是有个业务逻辑会在这个时间点全量删除后在生成插入数据,后续需要进行优化

最近同学在把 springboot 升级到 2.x 版本的过程中碰到了小问题,可能升级变更里能找到信息,不过我们以学习为目的,可以看看代码是怎么样的
报错是在这段代码里的
org.apache.tomcat.util.http.fileupload.util.LimitedInputStream#checkLimit

1
2
3
4
5
private void checkLimit() throws IOException {
if (count > sizeMax) {
raiseError(sizeMax, count);
}
}

其中的 raiseError 是个抽象方法

1
2
protected abstract void raiseError(long pSizeMax, long pCount)
throws IOException;

具体的实现是在

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
public FileItemStreamImpl(FileItemIteratorImpl pFileItemIterator, String pName, String pFieldName, String pContentType, boolean pFormField, long pContentLength) throws FileUploadException, IOException {
this.fileItemIteratorImpl = pFileItemIterator;
this.name = pName;
this.fieldName = pFieldName;
this.contentType = pContentType;
this.formField = pFormField;
long fileSizeMax = this.fileItemIteratorImpl.getFileSizeMax();
if (fileSizeMax != -1L && pContentLength != -1L && pContentLength > fileSizeMax) {
FileSizeLimitExceededException e = new FileSizeLimitExceededException(String.format("The field %s exceeds its maximum permitted size of %s bytes.", this.fieldName, fileSizeMax), pContentLength, fileSizeMax);
e.setFileName(pName);
e.setFieldName(pFieldName);
throw new FileUploadIOException(e);
} else {
final MultipartStream.ItemInputStream itemStream = this.fileItemIteratorImpl.getMultiPartStream().newInputStream();
InputStream istream = itemStream;
if (fileSizeMax != -1L) {
istream = new LimitedInputStream(itemStream, fileSizeMax) {
protected void raiseError(long pSizeMax, long pCount) throws IOException {
itemStream.close(true);
FileSizeLimitExceededException e = new FileSizeLimitExceededException(String.format("The field %s exceeds its maximum permitted size of %s bytes.", FileItemStreamImpl.this.fieldName, pSizeMax), pCount, pSizeMax);
e.setFieldName(FileItemStreamImpl.this.fieldName);
e.setFileName(FileItemStreamImpl.this.name);
throw new FileUploadIOException(e);
}
};
}

this.stream = (InputStream)istream;
}
}

后面也会介绍到,这里我们其实主要是要找到这个 pSizeMax 是哪里来的
通过阅读代码会发现跟这个类 MultipartConfigElement 有关系
而在升级后的 springboot 中这个类已经有了自动装配类,也就是
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration

有了这个自动装配

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
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class})
@ConditionalOnProperty(
prefix = "spring.servlet.multipart",
name = {"enabled"},
matchIfMissing = true
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@EnableConfigurationProperties({MultipartProperties.class})
public class MultipartAutoConfiguration {
private final MultipartProperties multipartProperties;

public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
this.multipartProperties = multipartProperties;
}

@Bean
@ConditionalOnMissingBean({MultipartConfigElement.class, CommonsMultipartResolver.class})
public MultipartConfigElement multipartConfigElement() {
return this.multipartProperties.createMultipartConfig();
}

而这个 MultipartProperties 类中

1
2
3
4
5
6
7
8
9
10
11
@ConfigurationProperties(
prefix = "spring.servlet.multipart",
ignoreUnknownFields = false
)
public class MultipartProperties {
private boolean enabled = true;
private String location;
private DataSize maxFileSize = DataSize.ofMegabytes(1L);
private DataSize maxRequestSize = DataSize.ofMegabytes(10L);
private DataSize fileSizeThreshold = DataSize.ofBytes(0L);
private boolean resolveLazily = false;

并且在前面 createMultipartConfig 中就使用了这个maxFileSize 的默认值

1
2
3
4
5
6
7
8
9
public MultipartConfigElement createMultipartConfig() {
MultipartConfigFactory factory = new MultipartConfigFactory();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.fileSizeThreshold).to(factory::setFileSizeThreshold);
map.from(this.location).whenHasText().to(factory::setLocation);
map.from(this.maxRequestSize).to(factory::setMaxRequestSize);
map.from(this.maxFileSize).to(factory::setMaxFileSize);
return factory.createMultipartConfig();
}

而在 org.apache.catalina.connector.Request#parseParts 中,会判断 mce 的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void parseParts(boolean explicit) {

// 省略一部分代码

ServletFileUpload upload = new ServletFileUpload();
upload.setFileItemFactory(factory);
upload.setFileSizeMax(mce.getMaxFileSize());
upload.setSizeMax(mce.getMaxRequestSize());

parts = new ArrayList<>();
try {
List<FileItem> items =
upload.parseRequest(new ServletRequestContext(this));
int maxPostSize = getConnector().getMaxPostSize();
int postSize = 0;
Charset charset = getCharset();

主要 org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public List<FileItem> parseRequest(final RequestContext ctx)
throws FileUploadException {
final List<FileItem> items = new ArrayList<>();
boolean successful = false;
try {
final FileItemIterator iter = getItemIterator(ctx);
final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
"No FileItemFactory has been set.");
final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
while (iter.hasNext()) {
final FileItemStream item = iter.next();
// Don't use getName() here to prevent an InvalidFileNameException.
final String fileName = item.getName();
final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
item.isFormField(), fileName);
items.add(fileItem);
try {
Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
} catch (final FileUploadIOException e) {

其中 org.apache.tomcat.util.http.fileupload.FileUploadBase#getItemIterator

1
2
3
4
5
6
7
8
9
public FileItemIterator getItemIterator(final RequestContext ctx)
throws FileUploadException, IOException {
try {
return new FileItemIteratorImpl(this, ctx);
} catch (final FileUploadIOException e) {
// unwrap encapsulated SizeException
throw (FileUploadException) e.getCause();
}
}

这里就创建了 org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl

1
2
3
4
5
6
7
8
9
10
public FileItemIteratorImpl(final FileUploadBase fileUploadBase, final RequestContext requestContext)
throws FileUploadException, IOException {
this.fileUploadBase = fileUploadBase;
sizeMax = fileUploadBase.getSizeMax();
fileSizeMax = fileUploadBase.getFileSizeMax();
ctx = Objects.requireNonNull(requestContext, "requestContext");
skipPreamble = true;
findNextItem();
}

内部使用了前面给 upload 设置的文件大小上限 upload.setFileSizeMax(mce.getMaxFileSize());

然后在 findNextItem 里执行了初始化

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
private boolean findNextItem() throws FileUploadException, IOException {
if (eof) {
return false;
}
if (currentItem != null) {
currentItem.close();
currentItem = null;
}
final MultipartStream multi = getMultiPartStream();
for (;;) {
final boolean nextPart;
if (skipPreamble) {
nextPart = multi.skipPreamble();
} else {
nextPart = multi.readBoundary();
}
if (!nextPart) {
if (currentFieldName == null) {
// Outer multipart terminated -> No more data
eof = true;
return false;
}
// Inner multipart terminated -> Return to parsing the outer
multi.setBoundary(multiPartBoundary);
currentFieldName = null;
continue;
}
final FileItemHeaders headers = fileUploadBase.getParsedHeaders(multi.readHeaders());
if (currentFieldName == null) {
// We're parsing the outer multipart
final String fieldName = fileUploadBase.getFieldName(headers);
if (fieldName != null) {
final String subContentType = headers.getHeader(FileUploadBase.CONTENT_TYPE);
if (subContentType != null
&& subContentType.toLowerCase(Locale.ENGLISH)
.startsWith(FileUploadBase.MULTIPART_MIXED)) {
currentFieldName = fieldName;
// Multiple files associated with this field name
final byte[] subBoundary = fileUploadBase.getBoundary(subContentType);
multi.setBoundary(subBoundary);
skipPreamble = true;
continue;
}
final String fileName = fileUploadBase.getFileName(headers);
currentItem = new FileItemStreamImpl(this, fileName,
fieldName, headers.getHeader(FileUploadBase.CONTENT_TYPE),
fileName == null, getContentLength(headers));
currentItem.setHeaders(headers);
progressNotifier.noteItem();
itemValid = true;
return true;
}
} else {
final String fileName = fileUploadBase.getFileName(headers);
if (fileName != null) {
currentItem = new FileItemStreamImpl(this, fileName,
currentFieldName,
headers.getHeader(FileUploadBase.CONTENT_TYPE),
false, getContentLength(headers));
currentItem.setHeaders(headers);
progressNotifier.noteItem();
itemValid = true;
return true;
}
}
multi.discardBodyData();
}
}

这里面就会 new 这个 FileItemStreamImpl

1
2
3
4
currentItem = new FileItemStreamImpl(this, fileName,
fieldName, headers.getHeader(FileUploadBase.CONTENT_TYPE),
fileName == null, getContentLength(headers));

构造方法比较长

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
public FileItemStreamImpl(final FileItemIteratorImpl pFileItemIterator, final String pName, final String pFieldName,
final String pContentType, final boolean pFormField,
final long pContentLength) throws FileUploadException, IOException {
fileItemIteratorImpl = pFileItemIterator;
name = pName;
fieldName = pFieldName;
contentType = pContentType;
formField = pFormField;
final long fileSizeMax = fileItemIteratorImpl.getFileSizeMax();
if (fileSizeMax != -1 && pContentLength != -1
&& pContentLength > fileSizeMax) {
final FileSizeLimitExceededException e =
new FileSizeLimitExceededException(
String.format("The field %s exceeds its maximum permitted size of %s bytes.",
fieldName, Long.valueOf(fileSizeMax)),
pContentLength, fileSizeMax);
e.setFileName(pName);
e.setFieldName(pFieldName);
throw new FileUploadIOException(e);
}
// OK to construct stream now
final ItemInputStream itemStream = fileItemIteratorImpl.getMultiPartStream().newInputStream();
InputStream istream = itemStream;
if (fileSizeMax != -1) {
istream = new LimitedInputStream(istream, fileSizeMax) {
@Override
protected void raiseError(final long pSizeMax, final long pCount)
throws IOException {
itemStream.close(true);
final FileSizeLimitExceededException e =
new FileSizeLimitExceededException(
String.format("The field %s exceeds its maximum permitted size of %s bytes.",
fieldName, Long.valueOf(pSizeMax)),
pCount, pSizeMax);
e.setFieldName(fieldName);
e.setFileName(name);
throw new FileUploadIOException(e);
}
};
}
stream = istream;
}

fileSizeMax != 0 的时候就会初始化 LimitedInputStream,这就就是会在前面的

org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest

1
Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);

这里的 item

1
2
3
4
5
6
final FileItemIterator iter = getItemIterator(ctx);
final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
"No FileItemFactory has been set.");
final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
while (iter.hasNext()) {
final FileItemStream item = iter.next();

调用了 FileItemIterator 迭代器的 next

1
2
3
4
5
6
7
8
@Override
public FileItemStream next() throws FileUploadException, IOException {
if (eof || (!itemValid && !hasNext())) {
throw new NoSuchElementException();
}
itemValid = false;
return currentItem;
}

这个 currentItem 就是前面 new 的 FileItemStreamImpl

然后在 Streams.copy 的时候调用 openStream 也就是 org.apache.tomcat.util.http.fileupload.impl.FileItemStreamImpl#openStream

1
2
3
4
5
6
7
@Override
public InputStream openStream() throws IOException {
if (((Closeable) stream).isClosed()) {
throw new FileItemStream.ItemSkippedException();
}
return stream;
}

这里的 stream 就是 FileItemStreamImpl 构造方法最后赋值的 stream,会在大小超过限制时抛出错误

而这个可以通过设置 properties 来修改,spring.servlet.multipart.max-file-size 和 spring.servlet.multipart.max-request-size

1
2
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB

而老版本的 spring.http.multipart.maxFileSize
其实就是配置名称改了下,但是能看一下代码也是有点收获的。

这部分其实之前在讲线程池的时候也有点带到了, 主要是在这个类里
org.apache.catalina.core.ContainerBase.ContainerBackgroundProcessor

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
protected class ContainerBackgroundProcessor implements Runnable {

@Override
public void run() {
processChildren(ContainerBase.this);
}

protected void processChildren(Container container) {
ClassLoader originalClassLoader = null;

try {
if (container instanceof Context) {
Loader loader = ((Context) container).getLoader();
// Loader will be null for FailedContext instances
if (loader == null) {
return;
}

// Ensure background processing for Contexts and Wrappers
// is performed under the web app's class loader
originalClassLoader = ((Context) container).bind(false, null);
}
// 调用 Container 的 backgroundProcess
container.backgroundProcess();
// 然后寻找 children
Container[] children = container.findChildren();
for (Container child : children) {
// 如果 backgroundProcessorDelay <= 0 就调用执行
// 否则代表这个 Container 有之前第八篇说的 StartChild 这种
if (child.getBackgroundProcessorDelay() <= 0) {
processChildren(child);
}
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("containerBase.backgroundProcess.error"), t);
} finally {
if (container instanceof Context) {
((Context) container).unbind(false, originalClassLoader);
}
}
}
}

这个触发方式是在 ContainerBase 里的

1
2
3
4
5
6
7
8
protected class ContainerBackgroundProcessorMonitor implements Runnable {
@Override
public void run() {
if (getState().isAvailable()) {
threadStart();
}
}
}

而在这个 threadStart 里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected void threadStart() {
if (backgroundProcessorDelay > 0
&& (getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState()))
&& (backgroundProcessorFuture == null || backgroundProcessorFuture.isDone())) {
if (backgroundProcessorFuture != null && backgroundProcessorFuture.isDone()) {
// There was an error executing the scheduled task, get it and log it
try {
backgroundProcessorFuture.get();
} catch (InterruptedException | ExecutionException e) {
log.error(sm.getString("containerBase.backgroundProcess.error"), e);
}
}
backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor()
.scheduleWithFixedDelay(new ContainerBackgroundProcessor(),
backgroundProcessorDelay, backgroundProcessorDelay,
TimeUnit.SECONDS);
}
}

就调用了线程池的 scheduleWithFixedDelay 方法提交了这个 ContainerBackgroundProcessor
仔细看代码会发现,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public StandardEngine() {

super();
pipeline.setBasic(new StandardEngineValve());
/* Set the jmvRoute using the system property jvmRoute */
try {
setJvmRoute(System.getProperty("jvmRoute"));
} catch(Exception ex) {
log.warn(sm.getString("standardEngine.jvmRouteFail"));
}
// By default, the engine will hold the reloading thread
backgroundProcessorDelay = 10;

}

这个就不用开启后台热加载,而主要的热加载同学应该是
org.apache.catalina.core.StandardContext#backgroundProcess

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 void backgroundProcess() {

if (!getState().isAvailable()) {
return;
}

Loader loader = getLoader();
if (loader != null) {
try {
// 这里就用了 loader 的 backgroundProcess
loader.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString(
"standardContext.backgroundProcess.loader", loader), e);
}
}
Manager manager = getManager();
if (manager != null) {
try {
manager.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString(
"standardContext.backgroundProcess.manager", manager),
e);
}
}
WebResourceRoot resources = getResources();
if (resources != null) {
try {
resources.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString(
"standardContext.backgroundProcess.resources",
resources), e);
}
}
InstanceManager instanceManager = getInstanceManager();
if (instanceManager != null) {
try {
instanceManager.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString(
"standardContext.backgroundProcess.instanceManager",
resources), e);
}
}
super.backgroundProcess();
}

loader 的后台处理就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void backgroundProcess() {
if (reloadable && modified()) {
try {
Thread.currentThread().setContextClassLoader
(WebappLoader.class.getClassLoader());
if (context != null) {
context.reload();
}
} finally {
if (context != null && context.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(context.getLoader().getClassLoader());
}
}
}
}

然后又会回到 context 的 reload,也就是 StandardContext 的 reload

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
@Override
public synchronized void reload() {

// Validate our current component state
if (!getState().isAvailable()) {
throw new IllegalStateException
(sm.getString("standardContext.notStarted", getName()));
}

if(log.isInfoEnabled()) {
log.info(sm.getString("standardContext.reloadingStarted",
getName()));
}

// Stop accepting requests temporarily.
setPaused(true);

try {
stop();
} catch (LifecycleException e) {
log.error(
sm.getString("standardContext.stoppingContext", getName()), e);
}

try {
start();
} catch (LifecycleException e) {
log.error(
sm.getString("standardContext.startingContext", getName()), e);
}

setPaused(false);

if(log.isInfoEnabled()) {
log.info(sm.getString("standardContext.reloadingCompleted",
getName()));
}

}

这样就是线程池结合后台处理,还是有些复杂的。

之前在 Windows 里用 vmware workstation 搭了个黑裙,然后硬盘直通,硬盘跑着倒还好,但是宿主机 Windows 隔一段时间就会重启,就去搜索了下,发现其实 Windows 里的事件查看器就有点像是 Linux 系统里的 dmesg 或者 journal 日志,然后根据重启的时间排查事件查看器里,Windows 日志 –> 系统,

然后可以在右侧 “筛选当前日志”里选择 eventlog 类型的,发现有这样一条事件
进程 C:\Windows\system32\svchost.exe (APP) 为用户 NT AUTHORITY\SYSTEM 开始计算机 APP 的 重新启动,原因如下: 操作系统: 恢复(计划内)
然后在网上搜索的时候发现有一些相关解答
可能的一种原因是系统在应用一些更新失败时,默认设置的策略是失败后重启,我们可以自己选择不重启,因为老是重启在我虚拟机里的黑裙没关机的情况下直接就断电重启了,还是可能会造成一些影响,
可以在资源管理器里右键计算机,选左侧的”高级系统设置” –> 然后在”高级” tab 下面有个”启动和故障恢复” –> “设置”
在弹出弹窗里将 “自动重新启动” 取消勾选

这样到目前还没继续发生过重启

0%