调优意义
系统运行缓慢,执行速度较差虽然没有对用户或公司造成实质性的损失,但它从侧面反映出系统在某些方面存在问题。可能需要对系统参数进行优化,或者对系统的设计和交互进行调整,这是后续系统性能优化的一个重要过程。我们将继续努力优化系统,以确保其高效运行和良好性能,以提升用户体验并最大程度地满足业务需求。我们希望通过系统调优的历程,解决当前存在的问题,并不断改进系统的运行,为用户提供更好的服务。
计划分析
流程相关分析优化
分析Nginx请求服务日志
使用 access_log.txt
日志文件进行分析,将在指定时间段内请求系统的 URL 进行分组计数,并生成一个根据 URL 调用次数进行排序的结果。
注意,为了实现这个目标,你需要使用适当的日志解析工具或脚本来提取日志中的 URL 信息,并结合时间范围进行计数和排序。这将帮助你了解在特定时间段内哪些 URL 受到了最多的请求,从而可以更好地进行性能优化或其他相关分析工作。
将请求热度最高的接口进行优化
对于请求次数排名靠前的URL接口,在全面分析业务场景后,经过仔细权衡和深入思考,我们决定将这些高频率的接口从同步调用方式转换为异步调用方式进行优化。
异步调用优化方式
将高频率的接口优化成异步调用是一项有效的优化策略,可以提升系统的性能和响应速度。但在实施之前,请确保充分理解业务需求和影响,并进行充分测试和评估,使优化方案能够真正发挥作用,满足预期的效果。
- 提升性能和响应速度:通过将这些接口改为异步调用,可以显著提升系统的性能和响应速度。异步调用方式可以使服务器能够更有效地处理并发请求,避免阻塞其他请求的处理,从而提高系统的并发处理能力和吞吐量。
- 降低延迟和提高性能:这种优化方案的实施需要对接口的调用逻辑进行适当的修改和重构。通过合理地设计和运用异步调用策略,可以最大程度地降低接口调用的延迟,提高系统的可用性和性能表现。
注意要点
- 建议进行详细的压力测试和性能评测:以确保在异步转换后系统仍能满足预期的响应和处理能力。
- 需要考虑业务逻辑的复杂性和接口间的依赖关系:确保所有相关的异步调用都能正确处理异常情况,并设计合适的错误处理机制,以确保系统的稳定性和可靠性。
分析调用链路追踪体系
发现在请求下游系统的过程中缺乏相关的耗时统计。为了解决这个问题,我们计划引入一个新的请求下游系统切面,以便能够准确地统计每个请求的耗时情况。
建立切面操作分析性能和数据统计
通过增加这个切面,我们可以在请求发送到下游系统并返回结果之间记录耗时的详细信息。这样做的好处是,我们能够获得关于下游系统性能的更全面和准确的数据,从而可以更好地分析和优化系统的性能瓶颈。
存储相关的调用以及耗时信息
为了实现这个切面,我们需要在请求发送和接收的过程中添加相应的拦截器或钩子,以便能够计算并记录下每个请求的耗时。我们还需要在系统中增加相应的统计模块,用来存储和分析这些耗时数据。
分析信息以及相关的耗时损耗
在实施这个方案之前,我们强烈建议进行充分的测试和验证,以确保新的切面能够正确地获取耗时数据,并且不会对系统的性能和稳定性产生负面影响。
总的来说,通过新增请求下游系统切面来统计耗时情况是一种有效的方法,可以帮助我们更好地理解系统的性能状况,并帮助我们及时发现和解决潜在的性能问题。
日志系统的升级和优化
一个比较严重的问题,即 Log4J 1.x 的阻塞问题。总共有大约90多个线程在等待同一把锁。
- 升级日志版本:为了解决这个问题,我们决定将应用程序的 Log4J 1.x 版本升级到 Log4J 2.x 版本。这样做的目的是提升性能并解决阻塞问题。升级过程需要仔细考虑,以确保与现有代码和配置的兼容性。
- 兼容性问题:在进行升级之前,我们建议先进行全面的测试和验证。确保新版本的Log4J能够正常工作,并且不会引入新的问题或破坏现有的功能。同时,我们还需要修改应用程序的配置文件,以便与新版本的Log4J兼容。
总结起来,通过将应用程序的Log4J版本升级到Log4J 2.x,我们期望能够解决阻塞问题,提升性能,并避免在压测过程中生成过多的线程和堆dump日志。升级前需要进行充分的测试和验证,确保顺利完成,并能够正常运行。
异步日志处理机制
在后续的优化过程中,我们还考虑了将请求下游系统的日志打印方式改为异步的方式,以进一步提升效果。
通过将请求下游系统的日志打印操作改为异步方式,我们能够获得更好的性能和效果。异步打印可以避免阻塞主线程,并将日志操作放到后台进行处理。这样一来,主线程可以继续执行其他任务,而无需等待日志打印完成。
这种改变能够显著提高系统的整体响应性能,特别是在高并发场景下。通过异步打印日志,系统能够更快地处理请求,并减少对下游系统的负载。
异常问题和负面影响
需要注意的是,异步日志打印虽然能够提升性能,但也需要综合考虑日志的精确性和实时性。异步打印可能会导致日志的输出顺序不再保证严格的按照时间顺序。因此,在实施异步打印之前,需要确保系统的日志需求与异步打印的特性相匹配。
总之,通过将请求下游系统的日志改为异步打印方式,我们期望进一步提升系统性能和效果。但在实施之前,需要仔细评估业务需求,并确保异步打印的特性与日志需求相符。
服务之间调用机制(参数调优)
为了针对业务的实际情况,我们可以定制一套超时参数,针对Http的工具类进行优化。这样可以尽量减少Http连接被长时间Hold住而不释放的情况,同时在必要的情况下,可以对Http请求工具类进行尝试次数的控制。
- 通过定制超时参数:可以根据具体业务需求来设置适当的超时时间。
例如,可以设置连接超时时间,用于控制建立连接的最长等待时间;同时可以设置读取超时时间,用于控制从连接中读取数据的最长等待时间。这样一来,即使在网络不稳定的情况下,我们也能够及时释放Http连接,并减少长时间的等待。
- 执行重试次数:可以对Http请求工具类进行尝试次数的控制。这意味着如果一次请求出现异常或失败,工具类会尝试重新发送请求,直到达到指定的尝试次数或成功为止。这样可以增加请求的成功率,并提高系统的容错性。
需要注意的是,超时参数的设置和尝试次数的控制都需要在合理的范围内进行。过长的超时时间和过多的重试次数可能会导致系统响应时间延长和资源浪费。因此,我们应该根据具体业务需求和性能测试结果来进行调整和优化。
数据库相关分析优化
数据库方面的优化主要通过收集产线出问题时刻的数据库指标以及其他相关信息,可以对系统进行优化和改进。以下是一些建议的优化方法:
- 数据库连接管理:通过监控数据库的连接指标,如 max_used_connections,max_user_connections 和 max_connections,可以了解数据库连接的使用情况。如果出现连接数达到或接近最大连接数的情况,可能说明系统存在连接泄露或者连接过多的问题。可以通过检查代码或者调整连接池的配置来解决这些问题。
- 数据库连接超时设置:监控数据库连接超时时间也是非常重要的。
- 如果连接超时时间过短,可能会导致连接频繁断开和重新建立,增加了数据库的负担,并降低了系统的性能。
- 如果连接超时时间过长,可能会导致连接长时间占用数据库资源而不释放,进而导致连接池耗尽和数据库性能下降。通过分析数据库连接超时时间,可以根据实际情况对其进行调整,以优化数据库连接的性能和资源利用率。
- 性能时序分析图:通过分析数据库实例前后几个小时的时序分析图,可以获取更多有关数据库的信息。
- 通过分析这些信息,可以找到潜在的优化点并进行相应的优化操作,从而提高数据库的性能和稳定性。
除了以上的建议,还需要综合考虑系统的整体架构和业务需求,进行细致的调优工作。同时,也建议定期进行性能测试和监控,以及和开发团队沟通合作,共同优化系统的性能和稳定性。
内存使用情况分析优化
分析heapDump文件
在进行压力测试过程中,建议生成更多的 heap dump 文件,然后使用 Eclipse 工具来分析其中存在的大对象。经过分析,如果没有发现任何可疑的地方,可以通过反查代码来进一步排查问题。特别是在一些定时任务需要加载大量数据的地方,建议在使用完这些数据后,立即手动释放资源,以便尽快让垃圾回收器回收内存。
- 增加 heap dump 文件数量:生成更多的 heap dump 文件可以提供更详细的内存快照,从而更好地了解内存中存在的对象和数据结构。这有助于发现潜在的内存泄漏或大对象的问题。
- 使用专业的内存分析工具:除了 Eclipse,还可以考虑使用其他专业的内存分析工具,如 VisualVM、MAT(Memory Analyzer Tool)等。这些工具提供了更多的功能和分析选项,能够更准确地检测和定位问题。
- 定期检查定时任务的代码:定时任务可能会在加载大量数据后没有及时释放资源,导致内存占用过高。通过反查代码,特别关注定时任务中的资源释放逻辑,并确保在使用完数据后手动释放相应的资源。
- 内存回收机制优化:除了手动释放资源,还可以考虑优化垃圾回收器的工作方式。根据具体情况,可以调整垃圾回收器的参数,如堆大小、年轻代和老年代的比例等,以提高内存回收的效率。
定时休眠处理
在一些后台定时轮询的任务中,有些任务需要通过for循环来处理一些任务,这个时候我们可以每循环一次或者循环数次之后通过调用Thead.sleep(xxx)休眠一下,
一是可以缓冲一下IO高密度的操作,还有就是让出CPU时间片,让有些紧急的任务可以优先获取CPU执行权。
JVM参数分析调优
在这里,我向大家推荐一本关于JVM优化和调优的实战系列书籍,《深入浅出Java虚拟机 — JVM原理与实战》。这本书是最新出版的,内容涵盖了与我们当前工作和开发实例密切相关的技术和实战案例。通过学习这本书,我们可以深入了解Java虚拟机的原理,并通过实践掌握优化和调优的技巧。我诚挚地推荐这本书给大家,相信它将为我们的工作和技术发展带来巨大的收益。希望大家能够抽出时间多多学习一下这本宝贵的资料。
下面的案例以及优化方案就是取自于本书的实战内容介绍:
- 内存泄漏问题解决实例 在这个案例中,我们将学习如何识别和解决内存泄漏问题。通过深入了解Java虚拟机的内存管理机制,我们将使用工具和技巧来诊断和分析潜在的内存泄漏原因,并提供相应的优化方案来解决这个问题。
- 垃圾回收优化实例 这个案例将带领我们深入研究垃圾回收机制,并学习如何优化垃圾回收过程以提高应用程序的性能。我们将探讨不同的垃圾回收算法和配置选项,并通过实践来选择和调整最适合应用程序的垃圾回收策略。
案例分析介绍
为了应对将来的高业务量,目前需要扩容服务器,将2台服务器扩容至4台服务器,然后将服务器由2核4G升级成为4核8G。因此在升级过程中对于参数的调整也存在了一定的迷惑期。
JVM参数以及问题的分析
JVM参数优化的方针和方向
- 可以通过完整分析GC日志,了解YGC的平均耗时和平均间隔。根据实际情况,考虑是否需要调整堆大小、年轻代占比和存活区占比。
- 通过GC日志分析FGC的平均耗时和平均间隔。特别关注FGC耗时,并尝试调整堆大小、年轻代占比、存活区占比和垃圾回收器方式。
- 观察S2区的使用占比。如果S2区占比为0且YGC平均耗时在40ms以内,并且没有FGC发生,那么这可以被认为是相对理想的情况。
- 如果S2区频繁满载,或者GC效果不佳,建议优先调整堆大小。如果问题仍然存在,进一步分析实际Heap消耗的对象占比,可能需要开发人员参与进行分析。
注意:在进行优化之前,请确保有充足的测试数据和监控日志,并在优化过程中对系统性能进行全面评估和验证。
GC执行方案指标要求
- 建议 Minor GC 的执行时间控制在50毫秒以内,以确保迅速执行。
- 建议 Minor GC 的执行频率大约为每10秒执行一次,以避免频繁执行。
- 建议 Full GC 的执行时间控制在1000毫秒以内,以确保迅速执行。
- 建议 Full GC 的执行频率大约为每10分钟执行一次,以避免频繁执行。
注意,上述建议是基于一般的指导原则,实际的优化策略还需要结合具体的系统环境和性能需求进行调整。同时,还应注意在调整参数之前进行充分的测试和评估,以确保对系统性能产生积极的影响。
JVM参数情况如下:
-Xms2048M -Xmx2048M -Xss256k
-XX:NewSize=512m -XX:MaxNewSize=512m -XX:SurvivorRatio=22
-XX:PermSize=256m -XX:MaxPermSize=512m
-XX:+UseParNewGC -XX:ParallelGCThreads=4 -XX:+ScavengeBeforeFullGC
-XX:+CMSScavengeBeforeRemark -XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=60 -XX:CMSInitiatingPermOccupancyFraction=70
-XX:+PrintGCApplicationConcurrentTime -XX:+PrintHeapAtGC -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=logs/oom.log -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
-verbose:gc -Xloggc:logs/gc.log -Djava.net.preferIPv4Stack=true
学会阅读 GC 日志
我们以JVM参数-Xms5m -Xmx5m -XX:+PrintGCDetails -XX:+UseSerialGC
为基础案例进行分析和介绍说明。
我们选择这些参数作为基础案例,是因为它们代表了一种较小的内存配置,并且使用了Serial垃圾收集器。这将使我们能够更清楚地展示内存管理和垃圾收集的工作原理。
首先,我们来解释一下这些参数的含义:
- -Xms5m 指定了JVM的初始堆内存大小为5MB。
- -Xmx5m 指定了JVM的最大堆内存大小为5MB。
- -XX:+PrintGCDetails 表示在进行垃圾回收时,JVM将打印详细的GC日志信息,包括内存使用情况和GC耗时。
- -XX:+UseSerialGC 表示使用Serial垃圾收集器,它是一种单线程的垃圾收集器,适合于小型应用或者客户端应用。
GC日志阅读片段
GC日志结构剖析
[DefNew: 1855K->1855K(1856K), 0.0000148 secs][Tenured: 2815K->4095K(4096K),0.0134819 secs] 4671K
- DefNew 指明了收集器类型,而且说明了收集发生在新生代。
- **1855K->1855K(1856K)**表示,回收前 新生代占用 1855K,回收后占用 1855K,新生代大小 1856K。
- 0.0000148 secs 表明新生代回收耗时。
- Tenured 表明收集发生在老年代
- 2815K->4095K(4096K), 0.0134819 secs:含义同新生代,最后的 4671K 指明堆的大小。
如果我们将收集器参数变为-XX:+UseParNewGC
,参数来调整垃圾收集器为ParNew。ParNew是一种并行垃圾收集器,它适用于多核系统,可以实现垃圾回收与应用程序并行进行,提高垃圾收集的效率
[ParNew: 1856K->1856K(1856K), 0.0000107 secs][Tenured: 2890K->4095K(4096K),
收集器参数变为-XX:+ UseParallelGC
或 UseParallelOldGC
。
- 收集器参数变为
-XX:+UseParallelGC
,那么这将启用并行年轻代垃圾收集器。这个收集器使用多个线程并行地进行垃圾收集,以提高垃圾收集的效率。 - 收集器参数变为
-XX:+UseParallelOldGC
,那么这将启用并行老年代垃圾收集器。该收集器与并行年轻代垃圾收集器相比,还会对老年代进行并行的垃圾收集。
[PSYoungGen: 1024K->1022K(1536K)] [ParOldGen: 3783K->3782K(4096K)] 4807K->4804K(5632K),
CMS 收集器和 G1 收集器会有明显的相关字样,其他与 GC 相关的参数调试跟踪之
打印简单的GC信息
-verbose:gc -XX:+PrintGC
参数设置为-verbose:gc
和-XX:+PrintGC
,那么它们会开启垃圾收集的详细输出日志。
-verbose:gc
参数会在每次垃圾收集发生时输出垃圾收集的相关信息,包括收集器类型、收集前后的堆内存使用情况以及回收所花费的时间等。-XX:+PrintGC
参数会打印更详细的垃圾收集日志,包括每次垃圾收集的详细信息,例如每个内存区域的收集情况、对象分配和回收的数量、堆内存的使用情况等。
通过将这两个参数组合使用,你可以获得更全面的垃圾收集日志,以便进行性能分析和调优。
打印详细的GC 信息
-XX:+PrintGCDetails, +XX:+PrintGCTimeStamps
如果你将参数设置为-XX:+PrintGCDetails
和-XX:+PrintGCTimeStamps
,那么它们会进一步增加垃圾收集的输出信息。
-XX:+PrintGCDetails
参数会打印更详细的垃圾收集信息,包括每次垃圾收集的详细统计数据,例如每个内存区域的收集情况、收集器的类型和行为、对象的分配和回收情况等。这个参数可以帮助你更深入地了解垃圾收集的过程和对应的数据。-XX:+PrintGCTimeStamps
参数会将每次垃圾收集的时间戳打印出来,以便你可以精确地知道每次垃圾收集的发生时间。这个参数可以帮助你分析和比较不同垃圾收集事件之间的时间间隔,从而更好地了解垃圾收集的影响和性能表现。
综合使用这两个参数,你可以获得更详细和准确的垃圾收集信息和时间戳记录,以帮助你进行更深入的性能分析和调优。
输出GC日志目录
应用场景: 将 gc 的日志独立写入日志文件,将 GC 日志与系统业务日志进行了分离,方便开发人员进行追踪分析。
当需要设置 gc 日志路径时,可以使用参数 -Xlogger:logpath。例如,如果你希望将 gc.log 文件保存在当前目录下的 log 目录中,可以使用以下命令进行设置:-Xlogger:logpath=log/gc.log
。
这样,垃圾收集器的日志输出将被保存在 log 目录下的 gc.log 文件中。这个设置可帮助你更好地管理和组织日志文件,方便后续的分析和调优工作。
扩展参数信息
-XX:+PrintHeapAtGC
使用参数-XX:+PrintHeapAtGC
可以在每次垃圾回收前后打印Heap的使用情况。这个参数对于跟踪和分析垃圾回收过程中的内存使用情况非常有帮助。通过查看打印输出的信息,你可以获得每次垃圾回收的前后Heap的大小、使用情况以及垃圾回收器的行为等相关信息。
注意,使用该参数可能会对性能产生一定的影响,因为在每次垃圾回收时都会进行一次打印操作。所以,只有在需要详细观察内存使用情况时才建议使用该参数。
-XX:+TraceClassLoading
- 参数设置:
-XX:+TraceClassLoading
- 应用场景:在系统控制台信息中查看类加载的过程和具体类信息,用于分析类的加载顺序和是否可以进行精简操作。
通过使用参数-XX:+TraceClassLoading
,你可以在系统控制台中观察到类的加载过程和相关的类信息。这个参数对于调试和分析类加载的顺序和加载的类的详细信息非常有帮助。
当你在控制台中启用了该参数后,系统会打印出每个类的加载过程,包括类的名称、加载时机、加载的类加载器等。通过查看这些信息,你可以了解类的加载顺序,并针对需要优化的类进行精简操作。
注意,这个参数会在系统控制台输出大量的信息,可能会影响应用程序的性能。因此,建议仅在需要详细了解类加载过程时才使用该参数。
-XX:-HeapDumpOnOutOfMemoryError
参数设置:-XX:-HeapDumpOnOutOfMemoryError
当Java程序发生内存溢出错误时,通常会导致程序无法继续执行,并抛出java.lang.OutOfMemoryError异常。开启-XX:-HeapDumpOnOutOfMemoryError参数后,JVM会在发生异常时自动触发并创建一个堆内存的快照文件,该文件可以用于后续的调试和分析。
具体使用方法是,在Java虚拟机的启动参数中添加-XX:-HeapDumpOnOutOfMemoryError。这样当出现内存溢出错误时,JVM会自动将堆内存快照输出到一个dump.core文件中。
注意,呈现java.lang.OutOfMemoryError异常的场景通常是非常严重的,而且可能导致Java应用无法正常运行。因此,在生产环境中,建议结合监控和告警系统,及时发现并解决内存溢出问题。
-XX:HeapDumpPath
参数设置:-XX:HeapDumpPath=./java_pid.hprof,该参数用于设置堆内存快照的存储文件路径。默认情况下,堆内存快照文件会保存在Java进程启动位置。
在Java应用程序运行过程中,使用该参数可以将堆内存的当前状态以快照的形式保存到指定的文件中。这对于诊断内存泄漏、分析内存使用情况以及调试内存相关的问题非常有用。
具体使用方法是,将-XX:HeapDumpPath=./java_pid.hprof添加到Java虚拟机的启动参数中。其中,为Java进程的进程ID,它会被替换为实际的进程ID。
例如,如果Java进程的进程ID是12345,则设置参数为-XX:HeapDumpPath=./java_pid12345.hprof。
TCP/应用服务器参数分析调优
TCP参数调优
在压测过程中,发现大量的TIME-WAIT的情况,于是根据实际调整系统的TCP参数,在高并发的场景中,TIME-WAIT虽然会峰值爬的很高,但是降下来的时间也是非常快的,主要是需要快速回收或者重用TCP连接。
TCP参数情况如下:
vim /etc/sysctl.conf
编辑文件,加入以下内容,您所列出的TCP参数设置如下:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
这些参数设置可以在对抗DDoS、优化TCP连接的回收和重用过程中起到一定的作用。让我为您解释一下每个参数的作用:
net.ipv4.tcp_syncookies = 1
: 启用SYN cookie防御机制。当服务器遭受SYN洪泛时,该机制能够通过检查TCP三次握手的SYN包中的cookie信息,来判断是否真正有合法的连接请求。这样可以防止服务器因为大量的伪造连接请求而耗尽资源。net.ipv4.tcp_tw_reuse = 1
: 启用TIME-WAIT状态的快速重用。当此选项启用时,新的连接可以重用之前处于TIME-WAIT状态的套接字资源。这有助于避免套接字资源不足的问题。net.ipv4.tcp_tw_recycle = 1
: 启用TIME-WAIT状态的快速回收。当此选项启用时,在短时间内可以立即回收处于TIME-WAIT状态的套接字资源,以便更快地重用这些资源。然而,请注意,此选项在某些情况下可能会导致连接失败,所以使用时需要谨慎评估。net.ipv4.tcp_fin_timeout = 30
: 设置TCP连接的FIN-WAIT-2状态的超时时间为30秒。当一端发送了FIN信号而另一端没有立即响应时,连接会进入FIN-WAIT-2状态。在此状态下,如果没有收到对方的响应,将根据该超时时间自动关闭连接。
然后执行 /sbin/sysctl -p
让参数生效。
应用服务器参数调优
Tomcat中,可以通过查看Connector和Executor的属性值来了解和调整相关参数。下面是获取Tomcat Connector和Executor属性值的方法:
- 进入Tomcat的安装目录,在conf目录下找到server.xml文件。
- 打开server.xml文件,在文件中寻找Connector元素。Connector元素定义了Tomcat与外部的连接器。
- 查找Executor元素。Executor元素定义了Tomcat线程池的配置。
可以按照以下步骤来获取相关属性值:
- 打开server.xml文件,在文件中找到Connector元素。例如,Connector元素的配置可能如下所示:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
maxThreads="200"
acceptCount="100" />
- 在上面的配置中,maxThreads表示Tomcat最大线程数,acceptCount表示最大排队数。可以根据需要调整这些值。请注意,在生产环境中,建议根据系统的需求和实际情况来确定这些参数的值。
- 类似地,可以找到Executor元素来获取或调整线程池相关的属性值。例如:
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="200" minSpareThreads="10"/>
在上述配置中,maxThreads表示最大线程数,minSpareThreads表示最小空闲线程数。
小结
经过分析和调优,系统的应用性能得到了提升。在优化的过程中,我们建立了一套独特的思考方式,当前的配置虽然不是完美的,但确实适合系统。
需要注意的是,以上数据仅供参考。如果有更好的方法或更优雅的调优方式,欢迎大家讨论和分享。希望以上内容满足您的要求。如有其他问题,请随时提问。
标签:服务,XX,调优,参数,垃圾,日志,优化,内存 From: https://blog.51cto.com/alex4dream/6601643